22#define TINYEXR_USE_MINIZ 0
24#define TINYEXR_IMPLEMENTATION
31#define PNG_SKIP_SETJMP_CHECK 1
42using namespace helios;
46 std::cerr << error_message << std::endl;
48 throw(std::runtime_error(error_message));
98 weight =
clamp(weight, 0.f, 1.f);
99 color_out.
r = weight * color1.
r + (1.f - weight) * color0.
r;
100 color_out.
g = weight * color1.
g + (1.f - weight) * color0.
g;
101 color_out.
b = weight * color1.
b + (1.f - weight) * color0.
b;
107 weight =
clamp(weight, 0.f, 1.f);
108 color_out.
r = weight * color1.
r + (1.f - weight) * color0.
r;
109 color_out.
g = weight * color1.
g + (1.f - weight) * color0.
g;
110 color_out.
b = weight * color1.
b + (1.f - weight) * color0.
b;
111 color_out.
a = weight * color1.
a + (1.f - weight) * color0.
a;
120 if (theta == 0.f && phi == 0.f) {
124 float Ry[3][3], Rz[3][3];
126 const float st = sin(theta);
127 const float ct = cos(theta);
129 const float sp = sin(phi);
130 const float cp = cos(phi);
155 float rotMat[3][3] = {0.f};
157 for (
int i = 0; i < 3; i++) {
158 for (
int j = 0; j < 3; j++) {
159 for (
int k = 0; k < 3; k++) {
160 rotMat[i][j] = rotMat[i][j] + Rz[i][k] * Ry[k][j];
167 tmp.
x = rotMat[0][0] * position.
x + rotMat[0][1] * position.
y + rotMat[0][2] * position.
z;
168 tmp.
y = rotMat[1][0] * position.
x + rotMat[1][1] * position.
y + rotMat[1][2] * position.
z;
169 tmp.
z = rotMat[2][0] * position.
x + rotMat[2][1] * position.
y + rotMat[2][2] * position.
z;
183 vec3 tmp = line_direction;
189 const float u = tmp.
x;
190 const float v = tmp.
y;
191 const float w = tmp.
z;
193 const float a = line_base.
x;
194 const float b = line_base.
y;
195 const float c = line_base.
z;
197 const float x = point.
x;
198 const float y = point.
y;
199 const float z = point.
z;
201 const float st = sin(theta);
202 const float ct = cos(theta);
204 position.
x = (a * (v * v + w * w) - u * (b * v + c * w - u * x - v * y - w * z)) * (1 - ct) + x * ct + (-c * v + b * w - w * y + v * z) * st;
205 position.
y = (b * (u * u + w * w) - v * (a * u + c * w - u * x - v * y - w * z)) * (1 - ct) + y * ct + (c * u - a * w + w * x - u * z) * st;
206 position.
z = (c * (u * u + v * v) - w * (a * u + b * v - u * x - v * y - w * z)) * (1 - ct) + z * ct + (-b * u + a * v - v * x + u * y) * st;
212 vec3 edge1 = v1 - v0;
213 vec3 edge2 = v2 - v0;
218 int skips_leap[] = {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335};
219 int skips_nonleap[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
225 skips = skips_nonleap;
236 bool leap = isLeapYear();
237 const int maxJD = leap ? 366 : 365;
238 if (jd < 1 || jd > maxJD) {
239 helios_runtime_error(
"ERROR (incrementDay): current date out of range (JD=" + std::to_string(jd) +
")");
258 if (year % 400 == 0) {
260 }
else if (year % 100 == 0) {
262 }
else if (year % 4 == 0) {
270 return float(rand()) / float(RAND_MAX + 1.);
276 if (imin == imax || imin > imax) {
279 return imin + (int) lround(
float(imax - imin) * ru);
300 constexpr float EPSILON = 1e-9f;
302 float ax = q1.
x - p1.
x;
303 float ay = q1.
y - p1.
y;
305 float bx = p2.
x - q2.
x;
306 float by = p2.
y - q2.
y;
308 float dx = p2.
x - p1.
x;
309 float dy = p2.
y - p1.
y;
311 float det = ax * by - ay * bx;
313 if (std::abs(det) < EPSILON) {
316 float cross = dx * ay - dy * ax;
317 if (std::abs(
cross) > EPSILON) {
323 float dot_aa = ax * ax + ay * ay;
324 if (dot_aa < EPSILON) {
332 float t2 = ((p2.
x - p1.
x) * ax + (p2.
y - p1.
y) * ay) / dot_aa;
333 float t3 = ((q2.
x - p1.
x) * ax + (q2.
y - p1.
y) * ay) / dot_aa;
341 return !(t1 < t2 || t3 < t0);
345 float r = (dx * by - dy * bx) / det;
346 float s = (ax * dy - ay * dx) / det;
348 return (r >= 0 && r <= 1 && s >= 0 && s <= 1);
352 constexpr float EPSILON = 1e-9f;
355 float cross = (point.
y - seg_start.
y) * (seg_end.
x - seg_start.
x) - (point.
x - seg_start.
x) * (seg_end.
y - seg_start.
y);
356 if (std::abs(
cross) > EPSILON) {
361 float min_x = std::min(seg_start.
x, seg_end.
x);
362 float max_x = std::max(seg_start.
x, seg_end.
x);
363 float min_y = std::min(seg_start.
y, seg_end.
y);
364 float max_y = std::max(seg_start.
y, seg_end.
y);
366 return (point.
x >= min_x - EPSILON && point.
x <= max_x + EPSILON && point.
y >= min_y - EPSILON && point.
y <= max_y + EPSILON);
370 constexpr float EPS = 1e-6f;
371 const std::size_t n = poly.size();
377 for (
const vec2 &v: poly) {
378 if (std::abs(p.
x - v.x) < EPS && std::abs(p.
y - v.y) < EPS) {
385 for (std::size_t i = 0; i < n; ++i) {
386 const vec2 &a = poly[i];
387 const vec2 &b = poly[(i + 1) % n];
389 if ((a.
y > p.
y) != (b.
y > p.
y)) {
391 float x_hit = a.
x + (p.
y - a.
y) * (b.
x - a.
x) / (b.
y - a.
y);
393 if (x_hit >= p.
x - EPS) {
399 return (crossings & 1) == 1;
402helios::ProgressBar::ProgressBar(
size_t total,
int width,
bool enable,
const std::string &progress_message) : total_steps(total), current_step(0), bar_width(width), enabled(enable), message(progress_message) {
407 double progress = (double) current_step / total_steps;
410 int filled = (int) (progress * bar_width);
412 std::cout <<
"\r" << message <<
": [";
413 for (
int i = 0; i < bar_width; ++i) {
416 else if (i == filled)
421 std::cout <<
"] " << (int) (progress * 100) <<
"% (" << current_step <<
"/" << total_steps <<
")";
424 if (current_step >= total_steps) {
425 std::cout << std::endl;
430 callback(
static_cast<float>(progress), message);
435 current_step = step_number;
436 if (current_step > total_steps) {
437 current_step = total_steps;
440 double progress = (double) current_step / total_steps;
443 int filled = (int) (progress * bar_width);
445 std::cout <<
"\r" << message <<
": [";
446 for (
int i = 0; i < bar_width; ++i) {
449 else if (i == filled)
454 std::cout <<
"] " << (int) (progress * 100) <<
"% (" << current_step <<
"/" << total_steps <<
")";
457 if (current_step >= total_steps) {
458 std::cout << std::endl;
463 callback(
static_cast<float>(progress), message);
471 if (current_step < total_steps) {
485 callback = std::move(cb);
489 int msec = (int) lround(seconds * 1000.f);
490 std::this_thread::sleep_for(std::chrono::milliseconds(msec));
494 float sx = sin(rotation);
495 float cx = cos(rotation);
497 if (strcmp(axis,
"x") == 0) {
510 }
else if (strcmp(axis,
"y") == 0) {
523 }
else if (strcmp(axis,
"z") == 0) {
539 T[12] = T[13] = T[14] = 0.f;
547 float sx = sin(rotation);
548 float cx = cos(rotation);
550 T[0] = cx + u.
x * u.
x * (1.f - cx);
551 T[1] = u.
x * u.
y * (1.f - cx) - u.
z * sx;
552 T[2] = u.
x * u.
z * (1.f - cx) + u.
y * sx;
554 T[4] = u.
y * u.
x * (1.f - cx) + u.
z * sx;
555 T[5] = cx + u.
y * u.
y * (1.f - cx);
556 T[6] = u.
y * u.
z * (1.f - cx) - u.
x * sx;
558 T[8] = u.
z * u.
x * (1.f - cx) - u.
y * sx;
559 T[9] = u.
z * u.
y * (1.f - cx) + u.
x * sx;
560 T[10] = cx + u.
z * u.
z * (1.f - cx);
563 T[12] = T[13] = T[14] = 0.f;
572 Ttrans[3] = -origin.
x;
573 Ttrans[7] = -origin.
y;
574 Ttrans[11] = -origin.
z;
580 float sx = sin(rotation);
581 float cx = cos(rotation);
586 Trot[0] = cx + u.
x * u.
x * (1.f - cx);
587 Trot[1] = u.
x * u.
y * (1.f - cx) - u.
z * sx;
588 Trot[2] = u.
x * u.
z * (1.f - cx) + u.
y * sx;
590 Trot[4] = u.
y * u.
x * (1.f - cx) + u.
z * sx;
591 Trot[5] = cx + u.
y * u.
y * (1.f - cx);
592 Trot[6] = u.
y * u.
z * (1.f - cx) - u.
x * sx;
594 Trot[8] = u.
z * u.
x * (1.f - cx) - u.
y * sx;
595 Trot[9] = u.
z * u.
y * (1.f - cx) + u.
x * sx;
596 Trot[10] = cx + u.
z * u.
z * (1.f - cx);
603 Ttrans[3] = origin.
x;
604 Ttrans[7] = origin.
y;
605 Ttrans[11] = origin.
z;
614 T[3] = translation.
x;
618 T[7] = translation.
y;
622 T[11] = translation.
z;
630 transform[0] = scale.
x;
635 transform[5] = scale.
y;
640 transform[10] = scale.
z;
649 transform[0] = scale.
x;
652 transform[3] = point.
x * (1 - scale.
x);
654 transform[5] = scale.
y;
656 transform[7] = point.
y * (1 - scale.
y);
659 transform[10] = scale.
z;
660 transform[11] = point.
z * (1 - scale.
z);
670 for (
int i = 0; i < 4; i++) {
671 for (
int j = 0; j < 4; j++) {
672 for (
int k = 0; k < 4; k++) {
673 M[4 * i + j] = M[4 * i + j] + ML[4 * i + k] * MR[4 * k + j];
678 for (
int i = 0; i < 16; i++) {
684 float v[4] = {v3.
x, v3.
y, v3.
z, 1.f};
688 for (
int i = 0; i < 4; ++i) {
689 for (
int k = 0; k < 4; ++k) {
690 V[i] += M[4 * i + k] * v[k];
701 float v4[4] = {v[0], v[1], v[2], 1.f};
703 for (
int j = 0; j < 4; j++) {
704 for (
int k = 0; k < 4; k++) {
705 V[j] = V[j] + v4[k] * M[k + 4 * j];
709 for (
int i = 0; i < 3; i++) {
750 return deg * float(
M_PI) / 180.f;
754 return rad * 180.f / float(
M_PI);
763 if (y >= 0.f && x < 0.f) {
764 v = float(
M_PI) + atanf(y / x);
766 if (y < 0.f && x < 0.f) {
767 v = -float(
M_PI) + atanf(y / x);
769 if (y > 0.f && x == 0.f) {
770 v = 0.5f * float(
M_PI);
772 if (y < 0.f && x == 0.f) {
773 v = -0.5f * float(
M_PI);
776 v = v + 2.f * float(
M_PI);
782 float radius = sqrtf(Cartesian.x * Cartesian.x + Cartesian.y * Cartesian.y + Cartesian.z * Cartesian.z);
787 float x_safe = Cartesian.x;
788 float y_safe = Cartesian.y;
789 if (fabsf(x_safe) < 1e-7f && fabsf(y_safe) < 1e-7f) {
804 std::istringstream stream(str);
806 while (stream >> tmp && c < 2) {
808 helios_runtime_error(
"ERROR (string2vec2): Invalid float value '" + tmp +
"' in input string '" + std::string(str) +
"'");
814 helios_runtime_error(
"ERROR (string2vec2): Insufficient values in input string '" + std::string(str) +
"'. Expected 2 values, got " + std::to_string(c));
824 std::istringstream stream(str);
826 while (stream >> tmp && c < 3) {
828 helios_runtime_error(
"ERROR (string2vec3): Invalid float value '" + tmp +
"' in input string '" + std::string(str) +
"'");
834 helios_runtime_error(
"ERROR (string2vec3): Insufficient values in input string '" + std::string(str) +
"'. Expected 3 values, got " + std::to_string(c));
844 std::istringstream stream(str);
846 while (stream >> tmp && c < 4) {
848 helios_runtime_error(
"ERROR (string2vec4): Invalid float value '" + tmp +
"' in input string '" + std::string(str) +
"'");
854 helios_runtime_error(
"ERROR (string2vec4): Insufficient values in input string '" + std::string(str) +
"'. Expected 4 values, got " + std::to_string(c));
857 return make_vec4(o[0], o[1], o[2], o[3]);
864 std::istringstream stream(str);
866 while (stream >> tmp && c < 2) {
868 helios_runtime_error(
"ERROR (string2int2): Invalid int value '" + tmp +
"' in input string '" + std::string(str) +
"'");
874 helios_runtime_error(
"ERROR (string2int2): Insufficient values in input string '" + std::string(str) +
"'. Expected 2 values, got " + std::to_string(c));
884 std::istringstream stream(str);
886 while (stream >> tmp && c < 3) {
888 helios_runtime_error(
"ERROR (string2int3): Invalid int value '" + tmp +
"' in input string '" + std::string(str) +
"'");
894 helios_runtime_error(
"ERROR (string2int3): Insufficient values in input string '" + std::string(str) +
"'. Expected 3 values, got " + std::to_string(c));
904 std::istringstream stream(str);
906 while (stream >> tmp && c < 4) {
908 helios_runtime_error(
"ERROR (string2int4): Invalid int value '" + tmp +
"' in input string '" + std::string(str) +
"'");
914 helios_runtime_error(
"ERROR (string2int4): Insufficient values in input string '" + std::string(str) +
"'. Expected 4 values, got " + std::to_string(c));
917 return make_int4(o[0], o[1], o[2], o[3]);
921 float o[4] = {0, 0, 0, 1};
924 std::istringstream stream(str);
926 while (stream >> tmp) {
928 helios_runtime_error(
"ERROR (string2RGBcolor): Too many values in input string '" + std::string(str) +
"'. Expected at most 4 values (RGBA), but found additional value '" + tmp +
"'");
932 helios_runtime_error(
"ERROR (string2RGBcolor): Invalid float value '" + tmp +
"' in input string '" + std::string(str) +
"'");
938 helios_runtime_error(
"ERROR (string2RGBcolor): Insufficient values in input string '" + std::string(str) +
"'. Expected at least 3 values (RGB), got " + std::to_string(c));
948 double converted_double = std::stod(str, &read);
949 converted_float = (float) converted_double;
950 if (str.size() != read)
952 }
catch (std::invalid_argument &e) {
962 converted_double = std::stod(str, &read);
963 if (str.size() != read)
965 }
catch (std::invalid_argument &e) {
975 converted_int = std::stoi(str, &read);
976 if (str.size() != read)
978 }
catch (std::invalid_argument &e) {
985 std::istringstream vecstream(input_string);
986 std::vector<std::string> tmp_s(2);
987 vecstream >> tmp_s[0];
988 vecstream >> tmp_s[1];
993 converted_int2 = tmp;
999 std::istringstream vecstream(input_string);
1000 std::vector<std::string> tmp_s(3);
1001 vecstream >> tmp_s[0];
1002 vecstream >> tmp_s[1];
1003 vecstream >> tmp_s[2];
1008 converted_int3 = tmp;
1017 int converted_int = std::stoi(str, &read);
1018 if (str.size() != read || converted_int < 0) {
1021 converted_uint = (
uint) converted_int;
1023 }
catch (std::invalid_argument &e) {
1030 std::istringstream vecstream(input_string);
1031 std::vector<std::string> tmp_s(2);
1032 vecstream >> tmp_s[0];
1033 vecstream >> tmp_s[1];
1038 converted_vec2 = tmp;
1044 std::istringstream vecstream(input_string);
1045 std::vector<std::string> tmp_s(3);
1046 vecstream >> tmp_s[0];
1047 vecstream >> tmp_s[1];
1048 vecstream >> tmp_s[2];
1053 converted_vec3 = tmp;
1059 std::istringstream vecstream(input_string);
1060 std::vector<std::string> tmp_s(3);
1061 vecstream >> tmp_s[0];
1062 vecstream >> tmp_s[1];
1063 vecstream >> tmp_s[2];
1068 if (tmp.
r < 0 || tmp.
g < 0 || tmp.
b < 0 || tmp.
r > 1.f || tmp.
g > 1.f || tmp.
b > 1.f) {
1071 converted_rgb = tmp;
1077 const std::string &fn = xml_file;
1079 if (ext !=
".xml" && ext !=
".XML") {
1080 error_string =
"XML file " + fn +
" is not XML format.";
1085 std::filesystem::path resolvedPath;
1088 }
catch (
const std::runtime_error &e) {
1089 error_string = std::string(e.what());
1094 pugi::xml_parse_result load_result = xmldoc.load_file(resolvedPath.string().c_str());
1098 error_string =
"XML file " + xml_file +
" parsed with errors: " + load_result.description();
1102 pugi::xml_node helios = xmldoc.child(
"helios");
1104 if (helios.empty()) {
1105 error_string =
"XML file " + xml_file +
" does not have tag '<helios> ... </helios>' bounding all other tags.";
1113 std::string value_string = node.child_value();
1114 if (value_string.empty()) {
1119 helios_runtime_error(
"ERROR (" + calling_function +
"): Could not parse tag '" + tag +
"' integer value.");
1125 std::string value_string = node.child_value();
1126 if (value_string.empty()) {
1131 helios_runtime_error(
"ERROR (" + calling_function +
"): Could not parse tag '" + tag +
"' float value.");
1137 std::string value_string = node.child_value();
1138 if (value_string.empty()) {
1143 helios_runtime_error(
"ERROR (" + calling_function +
"): Could not parse tag '" + tag +
"' vec2 value.");
1149 std::string value_string = node.child_value();
1150 if (value_string.empty()) {
1155 helios_runtime_error(
"ERROR (" + calling_function +
"): Could not parse tag '" + tag +
"' vec3 value.");
1161 return deblank(node.child_value());
1166 out.reserve(std::strlen(input));
1167 for (
const char *p = input; *p; ++p) {
1176 return deblank(input.c_str());
1180 static const std::string WHITESPACE =
" \n\r\t\f\v";
1183 size_t start = input.find_first_not_of(WHITESPACE);
1184 if (start == std::string::npos) {
1189 size_t end = input.find_last_not_of(WHITESPACE);
1192 return input.substr(start, end - start + 1);
1196 std::vector<std::string> separated_string;
1199 if (delimiter.empty()) {
1200 helios_runtime_error(
"ERROR (helios::separate_string_by_delimiter): Delimiter cannot be an empty string.");
1205 size_t max_characters = inputstring.size();
1207 while ((found = inputstring.find(delimiter, pos)) != std::string::npos && iter <= max_characters) {
1208 separated_string.push_back(
trim_whitespace(inputstring.substr(pos, found - pos)));
1209 pos = found + delimiter.size();
1216 return separated_string;
1225 for (
float i: vect) {
1238 for (
float i: vect) {
1241 m /= float(vect.size());
1251 return *std::min_element(vect.begin(), vect.end());
1259 return *std::min_element(vect.begin(), vect.end());
1267 vec3 vmin = vect.at(0);
1269 for (
int i = 1; i < vect.size(); i++) {
1270 if (vect.at(i).x < vmin.
x) {
1271 vmin.
x = vect.at(i).x;
1273 if (vect.at(i).y < vmin.
y) {
1274 vmin.
y = vect.at(i).y;
1276 if (vect.at(i).z < vmin.
z) {
1277 vmin.
z = vect.at(i).z;
1289 return *std::max_element(vect.begin(), vect.end());
1297 return *std::max_element(vect.begin(), vect.end());
1305 vec3 vmax = vect.at(0);
1307 for (
int i = 1; i < vect.size(); i++) {
1308 if (vect.at(i).x > vmax.
x) {
1309 vmax.
x = vect.at(i).x;
1311 if (vect.at(i).y > vmax.
y) {
1312 vmax.
y = vect.at(i).y;
1314 if (vect.at(i).z > vmax.
z) {
1315 vmax.
z = vect.at(i).z;
1327 size_t size = vect.size();
1330 for (
float i: vect) {
1336 for (
float i: vect) {
1337 stdev += powf(i - m, 2.0);
1340 return sqrtf(
stdev /
float(size));
1348 size_t size = vect.size();
1350 sort(vect.begin(), vect.end());
1352 size_t middle_index = size / 2;
1355 if (size % 2 == 0) {
1356 median = (vect.at(middle_index) + vect.at(middle_index - 1)) / 2.f;
1358 median = vect.at(middle_index);
1365 if (Julian_day < 1 || Julian_day > 366)
1371 const bool leap = (year % 4 == 0 && year % 100 != 0) ||
1374 if (!leap && Julian_day == 366)
1379 int month_lengths[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1381 month_lengths[1] = 29;
1384 int d_remaining = Julian_day;
1388 for (
int i = 0; i < 12; ++i) {
1389 if (d_remaining > month_lengths[i]) {
1390 d_remaining -= month_lengths[i];
1398 return make_Date(d_remaining, month, year);
1408 int month = date.month;
1409 int year = date.year;
1412 if (month < 1 || month > 12) {
1413 helios_runtime_error(
"ERROR (JulianDay): Month of year is out of range (month of " + std::to_string(month) +
" was given).");
1417 int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1420 if (
bool isLeapYear = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
1421 daysInMonth[2] = 29;
1424 if (day < 1 || day > daysInMonth[month]) {
1425 helios_runtime_error(
"ERROR (JulianDay): Day of month is out of range (day of " + std::to_string(day) +
" was given for month " + std::to_string(month) +
").");
1433 int dayOfYear = day;
1434 for (
int m = 1; m < month; m++) {
1435 dayOfYear += daysInMonth[m];
1446 std::string fn(filename);
1447 auto dot_pos = fn.find_last_of(
'.');
1448 if (dot_pos == std::string::npos) {
1451 std::string ext = fn.substr(dot_pos + 1);
1452 if (ext !=
"png" && ext !=
"PNG") {
1457 auto fileCloser = [](FILE *f) {
1461 std::unique_ptr<FILE,
decltype(fileCloser)> fp(std::fopen(fn.c_str(),
"rb"), fileCloser);
1467 unsigned char header[8];
1468 if (std::fread(header, 1, 8, fp.get()) != 8 || png_sig_cmp(header, 0, 8)) {
1472 png_structp png_ptr =
nullptr;
1473 png_infop info_ptr =
nullptr;
1477 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
1479 throw std::runtime_error(
"png_create_read_struct failed.");
1481 info_ptr = png_create_info_struct(png_ptr);
1483 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
1484 throw std::runtime_error(
"png_create_info_struct failed.");
1488 if (setjmp(png_jmpbuf(png_ptr))) {
1489 throw std::runtime_error(
"Error during PNG initialization.");
1493 png_init_io(png_ptr, fp.get());
1494 png_set_sig_bytes(png_ptr, 8);
1495 png_read_info(png_ptr, info_ptr);
1498 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1499 bool has_tRNS = png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) != 0;
1502 bool has_alpha = ((color_type & PNG_COLOR_MASK_ALPHA) != 0) || has_tRNS;
1505 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
1508 }
catch (
const std::exception &e) {
1512 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
1514 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
1524 const std::string &fn = filename;
1525 auto dot = fn.find_last_of(
'.');
1526 if (dot == std::string::npos) {
1529 std::string ext = fn.substr(dot + 1);
1530 std::transform(ext.begin(), ext.end(), ext.begin(), [](
unsigned char c) { return std::tolower(c); });
1535 std::vector<std::vector<bool>> mask;
1536 png_structp png_ptr =
nullptr;
1537 png_infop info_ptr =
nullptr;
1541 auto fileDeleter = [](FILE *f) {
1545 std::unique_ptr<FILE,
decltype(fileDeleter)> fp(fopen(filename.c_str(),
"rb"), fileDeleter);
1547 throw std::runtime_error(
"File " + filename +
" could not be opened for reading.");
1551 unsigned char header[8];
1552 if (fread(header, 1, 8, fp.get()) != 8) {
1553 throw std::runtime_error(
"Failed to read PNG header from " + filename);
1555 if (png_sig_cmp(header, 0, 8)) {
1556 throw std::runtime_error(
"File " + filename +
" is not a valid PNG.");
1560 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
1562 throw std::runtime_error(
"png_create_read_struct failed.");
1565 info_ptr = png_create_info_struct(png_ptr);
1567 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
1568 throw std::runtime_error(
"png_create_info_struct failed.");
1572 if (setjmp(png_jmpbuf(png_ptr))) {
1573 throw std::runtime_error(
"Error during PNG initialization.");
1576 png_init_io(png_ptr, fp.get());
1577 png_set_sig_bytes(png_ptr, 8);
1578 png_read_info(png_ptr, info_ptr);
1580 uint width = png_get_image_width(png_ptr, info_ptr);
1581 uint height = png_get_image_height(png_ptr, info_ptr);
1582 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1583 png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
1584 bool has_alpha = (color_type & PNG_COLOR_MASK_ALPHA) != 0 || png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) != 0;
1586 mask.resize(height);
1587 for (
uint i = 0; i < height; i++) {
1588 mask.at(i).resize(width);
1592 for (
uint j = 0; j < height; ++j) {
1593 std::fill(mask.at(j).begin(), mask.at(j).end(),
true);
1595 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
1600 if (bit_depth == 16) {
1601 png_set_strip_16(png_ptr);
1603 if (color_type == PNG_COLOR_TYPE_PALETTE) {
1604 png_set_palette_to_rgb(png_ptr);
1606 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
1607 png_set_expand_gray_1_2_4_to_8(png_ptr);
1609 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
1610 png_set_tRNS_to_alpha(png_ptr);
1613 if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) && (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)) {
1614 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
1616 if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
1617 png_set_gray_to_rgb(png_ptr);
1620 png_set_interlace_handling(png_ptr);
1621 png_read_update_info(png_ptr, info_ptr);
1624 size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);
1625 std::vector<std::vector<png_byte>> row_data(height, std::vector<png_byte>(rowbytes));
1626 std::vector<png_bytep> row_pointers(height);
1627 for (
uint y = 0; y < height; ++y) {
1628 row_pointers[y] = row_data[y].data();
1632 if (setjmp(png_jmpbuf(png_ptr))) {
1633 throw std::runtime_error(
"Error during PNG read.");
1635 png_read_image(png_ptr, row_pointers.data());
1638 for (
uint j = 0; j < height; j++) {
1639 png_byte *row = row_pointers[j];
1640 for (
uint i = 0; i < width; i++) {
1641 png_byte *ba = &(row[i * 4]);
1642 float alpha = ba[3];
1643 mask.at(j).at(i) = (alpha >= 250);
1647 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
1649 }
catch (
const std::exception &e) {
1651 png_destroy_read_struct(&png_ptr, info_ptr ? &info_ptr :
nullptr,
nullptr);
1661 auto ext_pos = filename.find_last_of(
'.');
1662 if (ext_pos == std::string::npos) {
1665 std::string ext = filename.substr(ext_pos + 1);
1666 std::transform(ext.begin(), ext.end(), ext.begin(), [](
unsigned char c) { return std::tolower(c); });
1671 png_structp png_ptr =
nullptr;
1672 png_infop info_ptr =
nullptr;
1678 auto fileDeleter = [](FILE *f) {
1682 std::unique_ptr<FILE,
decltype(fileDeleter)> fp(fopen(filename.c_str(),
"rb"), fileDeleter);
1684 throw std::runtime_error(
"File " + filename +
" could not be opened.");
1688 unsigned char header[8];
1689 if (fread(header, 1, 8, fp.get()) != 8) {
1690 throw std::runtime_error(
"Failed to read PNG header from " + filename);
1692 if (png_sig_cmp(header, 0, 8)) {
1693 throw std::runtime_error(
"File " + filename +
" is not a valid PNG.");
1697 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
1699 throw std::runtime_error(
"Failed to create PNG read struct.");
1701 info_ptr = png_create_info_struct(png_ptr);
1703 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
1704 throw std::runtime_error(
"Failed to create PNG info struct.");
1708 if (setjmp(png_jmpbuf(png_ptr))) {
1709 throw std::runtime_error(
"Error during PNG initialization.");
1713 png_init_io(png_ptr, fp.get());
1714 png_set_sig_bytes(png_ptr, 8);
1715 png_read_info(png_ptr, info_ptr);
1718 png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
1719 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1721 if (bit_depth == 16) {
1722 png_set_strip_16(png_ptr);
1724 if (color_type == PNG_COLOR_TYPE_PALETTE) {
1725 png_set_palette_to_rgb(png_ptr);
1727 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
1728 png_set_expand_gray_1_2_4_to_8(png_ptr);
1730 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
1731 png_set_tRNS_to_alpha(png_ptr);
1734 if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) && (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)) {
1735 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
1737 if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
1738 png_set_gray_to_rgb(png_ptr);
1742 png_set_interlace_handling(png_ptr);
1745 png_read_update_info(png_ptr, info_ptr);
1748 size_t w = png_get_image_width(png_ptr, info_ptr);
1749 size_t h = png_get_image_height(png_ptr, info_ptr);
1751 constexpr size_t max_pixels = (std::numeric_limits<size_t>::max)() /
sizeof(
helios::RGBAcolor);
1752 if (w == 0 || h == 0 || w > max_pixels / h) {
1753 throw std::runtime_error(
"Invalid image dimensions: " + std::to_string(w) +
"×" + std::to_string(h));
1755 width = scast<uint>(w);
1756 height = scast<uint>(h);
1759 size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);
1760 if (rowbytes < width * 4) {
1761 throw std::runtime_error(
"Unexpected row size: " + std::to_string(rowbytes));
1763 std::vector<std::vector<png_byte>> row_data(height, std::vector<png_byte>(rowbytes));
1764 std::vector<png_bytep> row_pointers(height);
1765 for (
uint y = 0; y < height; ++y) {
1766 row_pointers[y] = row_data[y].data();
1770 if (setjmp(png_jmpbuf(png_ptr))) {
1771 throw std::runtime_error(
"Error during PNG read.");
1773 png_read_image(png_ptr, row_pointers.data());
1776 texture.resize(scast<size_t>(width) * height);
1777 for (
uint y = 0; y < height; ++y) {
1778 png_bytep row = row_pointers[y];
1779 for (
uint x = 0; x < width; ++x) {
1780 png_bytep px = row + x * 4;
1781 auto &c = texture[y * width + x];
1782 c.r = px[0] / 255.0f;
1783 c.g = px[1] / 255.0f;
1784 c.b = px[2] / 255.0f;
1785 c.a = px[3] / 255.0f;
1788 }
catch (
const std::exception &e) {
1792 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
1794 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
1802 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
1804 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
1810 FILE *fp = fopen(filename.c_str(),
"wb");
1815 png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
1820 png_infop info = png_create_info_struct(png);
1825 if (setjmp(png_jmpbuf(png))) {
1829 png_init_io(png, fp);
1832 png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
1833 png_write_info(png, info);
1839 std::vector<unsigned char *> row_pointers;
1840 row_pointers.resize(height);
1842 std::vector<std::vector<unsigned char>> data;
1843 data.resize(height);
1845 for (
uint row = 0; row < height; row++) {
1846 data.at(row).resize(4 * width);
1847 for (
uint col = 0; col < width; col++) {
1848 data.at(row).at(4 * col) = (
unsigned char) round(
clamp(pixel_data.at(row * width + col).r, 0.f, 1.f) * 255.f);
1849 data.at(row).at(4 * col + 1) = (
unsigned char) round(
clamp(pixel_data.at(row * width + col).g, 0.f, 1.f) * 255.f);
1850 data.at(row).at(4 * col + 2) = (
unsigned char) round(
clamp(pixel_data.at(row * width + col).b, 0.f, 1.f) * 255.f);
1851 data.at(row).at(4 * col + 3) = (
unsigned char) round(
clamp(pixel_data.at(row * width + col).a, 0.f, 1.f) * 255.f);
1853 row_pointers.at(row) = &data.at(row).at(0);
1856 png_write_image(png, &row_pointers.at(0));
1857 png_write_end(png,
nullptr);
1861 png_destroy_write_struct(&png, &info);
1868 size_t pixels = width * height;
1870 std::vector<RGBAcolor> rgb_data;
1871 rgb_data.resize(pixels);
1873 size_t channels = pixel_data.size() / pixels;
1880 for (
size_t i = 0; i < pixels; i++) {
1881 rgb_data[i].r = float(pixel_data[i]) / 255.0f;
1882 rgb_data[i].g = float(pixel_data[i + pixels]) / 255.0f;
1883 rgb_data[i].b = float(pixel_data[i + 2 * pixels]) / 255.0f;
1884 rgb_data[i].a = channels > 3 ? float(pixel_data[i + 3 * pixels]) / 255.0f : 1.0f;
1888 writePNG(filename, width, height, rgb_data);
1894 char buffer[JMSG_LENGTH_MAX];
1895 (*cinfo->err->format_message)(cinfo, buffer);
1896 throw std::runtime_error(buffer);
1901 if (file_extension !=
".jpg" && file_extension !=
".JPG" && file_extension !=
".jpeg" && file_extension !=
".JPEG") {
1905 jpeg_decompress_struct cinfo{};
1907 jpeg_error_mgr jerr{};
1911 std::unique_ptr<FILE, int (*)(FILE *)> infile(fopen(filename.c_str(),
"rb"), fclose);
1913 helios_runtime_error(
"ERROR (Context::readJPEG): File " + filename +
" could not be opened. Check that the file exists and that you have permission to read it.");
1916 cinfo.err = jpeg_std_error(&jerr);
1917 jerr.error_exit = jpg_error_exit;
1920 jpeg_create_decompress(&cinfo);
1921 jpeg_stdio_src(&cinfo, infile.get());
1922 (void) jpeg_read_header(&cinfo, (
boolean) 1);
1924 (void) jpeg_start_decompress(&cinfo);
1926 row_stride = cinfo.output_width * cinfo.output_components;
1927 buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
1929 width = cinfo.output_width;
1930 height = cinfo.output_height;
1932 if (cinfo.output_components != 3) {
1934 }
else if (width == 0 || height == 0) {
1938 pixel_data.resize(width * height);
1942 while (cinfo.output_scanline < cinfo.output_height) {
1943 (void) jpeg_read_scanlines(&cinfo, buffer, 1);
1947 for (
int col = 0; col < row_stride; col += 3) {
1948 pixel_data.at(row * width + col / 3) =
make_RGBcolor(ba[col] / 255.f, ba[col + 1] / 255.f, ba[col + 2] / 255.f);
1954 (void) jpeg_finish_decompress(&cinfo);
1956 jpeg_destroy_decompress(&cinfo);
1958 jpeg_destroy_decompress(&cinfo);
1965 if (file_extension !=
".jpg" && file_extension !=
".JPG" && file_extension !=
".jpeg" && file_extension !=
".JPEG") {
1966 helios_runtime_error(
"ERROR (Context::getImageResolutionJPEG): File " + filename +
" is not JPEG format.");
1969 jpeg_decompress_struct cinfo{};
1971 jpeg_error_mgr jerr{};
1972 std::unique_ptr<FILE, int (*)(FILE *)> infile(fopen(filename.c_str(),
"rb"), fclose);
1974 helios_runtime_error(
"ERROR (Context::getImageResolutionJPEG): File " + filename +
" could not be opened. Check that the file exists and that you have permission to read it.");
1977 cinfo.err = jpeg_std_error(&jerr);
1978 jerr.error_exit = jpg_error_exit;
1981 jpeg_create_decompress(&cinfo);
1982 jpeg_stdio_src(&cinfo, infile.get());
1983 (void) jpeg_read_header(&cinfo, (
boolean) 1);
1984 (void) jpeg_start_decompress(&cinfo);
1986 jpeg_destroy_decompress(&cinfo);
1988 jpeg_destroy_decompress(&cinfo);
1992 return make_int2(cinfo.output_width, cinfo.output_height);
1995static void writeJPEGInternal(
const std::string &a_filename,
uint width,
uint height,
const std::vector<helios::RGBcolor> &pixel_data,
const helios::ImageEXIFData *metadata) {
1997 std::string filename = a_filename;
1999 if (file_extension !=
".jpg" && file_extension !=
".JPG" && file_extension !=
".jpeg" && file_extension !=
".JPEG") {
2000 filename.append(
".jpeg");
2003 if (pixel_data.size() != width * height) {
2004 helios_runtime_error(
"ERROR (Context::writeJPEG): Pixel data does not have size of width*height.");
2007 const uint bsize = 3 * width * height;
2008 std::vector<unsigned char> screen_shot_trans(bsize);
2011 for (
size_t i = 0; i < width * height; i++) {
2012 screen_shot_trans.at(ii) = (
unsigned char) round(
helios::clamp(pixel_data.at(i).r, 0.f, 1.f) * 255);
2013 screen_shot_trans.at(ii + 1) = (
unsigned char) round(
helios::clamp(pixel_data.at(i).g, 0.f, 1.f) * 255);
2014 screen_shot_trans.at(ii + 2) = (
unsigned char) round(
helios::clamp(pixel_data.at(i).b, 0.f, 1.f) * 255);
2018 struct jpeg_compress_struct cinfo{};
2020 struct jpeg_error_mgr jerr{};
2022 cinfo.err = jpeg_std_error(&jerr);
2023 jerr.error_exit = jpg_error_exit;
2025 JSAMPROW row_pointer;
2028 std::unique_ptr<FILE, int (*)(FILE *)> outfile(fopen(filename.c_str(),
"wb"), fclose);
2030 helios_runtime_error(
"ERROR (Context::writeJPEG): File " + filename +
" could not be opened. Check that the file path is correct you have permission to write to it.");
2033 jpeg_create_compress(&cinfo);
2034 jpeg_stdio_dest(&cinfo, outfile.get());
2036 cinfo.image_width = width;
2037 cinfo.image_height = height;
2038 cinfo.input_components = 3;
2039 cinfo.in_color_space = JCS_RGB;
2041 jpeg_set_defaults(&cinfo);
2043 jpeg_set_quality(&cinfo, 100, (
boolean) 1 );
2045 jpeg_start_compress(&cinfo, (
boolean) 1);
2047 if (metadata !=
nullptr) {
2050 jpeg_write_marker(&cinfo, JPEG_APP0 + 1, exif_seg.data(),
static_cast<unsigned int>(exif_seg.size()));
2051 if (metadata->xmp_valid) {
2053 jpeg_write_marker(&cinfo, JPEG_APP0 + 1, xmp_seg.data(),
static_cast<unsigned int>(xmp_seg.size()));
2058 row_stride = width * 3;
2060 while (cinfo.next_scanline < cinfo.image_height) {
2061 row_pointer = (JSAMPROW) &screen_shot_trans[(cinfo.image_height - cinfo.next_scanline - 1) * row_stride];
2062 (void) jpeg_write_scanlines(&cinfo, &row_pointer, 1);
2065 jpeg_finish_compress(&cinfo);
2066 jpeg_destroy_compress(&cinfo);
2068 jpeg_destroy_compress(&cinfo);
2074 writeJPEGInternal(a_filename, width, height, pixel_data,
nullptr);
2078 writeJPEGInternal(a_filename, width, height, pixel_data, &metadata);
2085 size_t pixels = width * height;
2087 std::vector<RGBcolor> rgb_data;
2088 rgb_data.resize(pixels);
2090 size_t channels = pixel_data.size() / pixels;
2097 for (
size_t i = 0; i < pixels; i++) {
2098 rgb_data[i].r = scast<float>(pixel_data[i]) / 255.0f;
2099 rgb_data[i].g = scast<float>(pixel_data[i + pixels]) / 255.0f;
2100 rgb_data[i].b = scast<float>(pixel_data[i + 2 * pixels]) / 255.0f;
2104 writeJPEG(a_filename, width, height, rgb_data);
2107void helios::writeEXR(
const std::string &filename,
uint width,
uint height,
const std::vector<float> &pixel_data,
const std::string &channel_name) {
2109 if (pixel_data.size() != width * height) {
2110 helios_runtime_error(
"ERROR (writeEXR): pixel_data size (" + std::to_string(pixel_data.size()) +
") does not match width*height (" + std::to_string(width * height) +
").");
2114 InitEXRHeader(&header);
2117 InitEXRImage(&image);
2119 image.num_channels = 1;
2120 image.width = scast<int>(width);
2121 image.height = scast<int>(height);
2123 float *image_ptr[1];
2124 image_ptr[0] =
const_cast<float *
>(pixel_data.data());
2126 image.images =
reinterpret_cast<unsigned char **
>(image_ptr);
2128 header.num_channels = 1;
2129 header.channels = scast<EXRChannelInfo *>(malloc(
sizeof(EXRChannelInfo)));
2130 strncpy(header.channels[0].name, channel_name.c_str(), 255);
2131 header.channels[0].name[255] =
'\0';
2133 header.pixel_types = scast<int *>(malloc(
sizeof(
int)));
2134 header.requested_pixel_types = scast<int *>(malloc(
sizeof(
int)));
2135 header.pixel_types[0] = TINYEXR_PIXELTYPE_FLOAT;
2136 header.requested_pixel_types[0] = TINYEXR_PIXELTYPE_FLOAT;
2138 header.compression_type = TINYEXR_COMPRESSIONTYPE_ZIP;
2140 const char *err =
nullptr;
2141 int ret = SaveEXRImageToFile(&image, &header, filename.c_str(), &err);
2143 free(header.channels);
2144 free(header.pixel_types);
2145 free(header.requested_pixel_types);
2147 if (ret != TINYEXR_SUCCESS) {
2148 std::string error_msg =
"ERROR (writeEXR): Failed to write EXR file '" + filename +
"'";
2150 error_msg +=
": " + std::string(err);
2151 FreeEXRErrorMessage(err);
2157void helios::writeEXR(
const std::string &filename,
uint width,
uint height,
const std::vector<std::vector<float>> &channel_data,
const std::vector<std::string> &channel_names) {
2159 if (channel_data.size() != channel_names.size()) {
2160 helios_runtime_error(
"ERROR (writeEXR): channel_data size (" + std::to_string(channel_data.size()) +
") does not match channel_names size (" + std::to_string(channel_names.size()) +
").");
2162 if (channel_data.empty()) {
2165 for (
size_t c = 0; c < channel_data.size(); c++) {
2166 if (channel_data[c].size() != width * height) {
2167 helios_runtime_error(
"ERROR (writeEXR): channel_data[" + std::to_string(c) +
"] size (" + std::to_string(channel_data[c].size()) +
") does not match width*height (" + std::to_string(width * height) +
").");
2171 int num_channels = scast<int>(channel_data.size());
2175 auto mapChannelName = [](
const std::string &name) -> std::string {
2176 std::string lower = name;
2177 std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
2178 if (lower ==
"r" || lower ==
"red")
return "R";
2179 if (lower ==
"g" || lower ==
"green")
return "G";
2180 if (lower ==
"b" || lower ==
"blue")
return "B";
2181 if (lower ==
"a" || lower ==
"alpha")
return "A";
2185 std::vector<std::string> exr_channel_names(num_channels);
2186 for (
int c = 0; c < num_channels; c++) {
2187 exr_channel_names[c] = mapChannelName(channel_names[c]);
2191 std::vector<size_t> sort_indices(num_channels);
2192 for (
size_t i = 0; i < sort_indices.size(); i++) {
2193 sort_indices[i] = i;
2195 std::sort(sort_indices.begin(), sort_indices.end(), [&](
size_t a,
size_t b) {
2196 return exr_channel_names[a] < exr_channel_names[b];
2200 InitEXRHeader(&header);
2203 InitEXRImage(&image);
2205 image.num_channels = num_channels;
2206 image.width = scast<int>(width);
2207 image.height = scast<int>(height);
2209 std::vector<float *> image_ptrs(num_channels);
2210 for (
int c = 0; c < num_channels; c++) {
2211 image_ptrs[c] =
const_cast<float *
>(channel_data[sort_indices[c]].data());
2213 image.images =
reinterpret_cast<unsigned char **
>(image_ptrs.data());
2215 header.num_channels = num_channels;
2216 header.channels = scast<EXRChannelInfo *>(malloc(
sizeof(EXRChannelInfo) * num_channels));
2217 header.pixel_types = scast<int *>(malloc(
sizeof(
int) * num_channels));
2218 header.requested_pixel_types = scast<int *>(malloc(
sizeof(
int) * num_channels));
2220 for (
int c = 0; c < num_channels; c++) {
2221 strncpy(header.channels[c].name, exr_channel_names[sort_indices[c]].c_str(), 255);
2222 header.channels[c].name[255] =
'\0';
2223 header.pixel_types[c] = TINYEXR_PIXELTYPE_FLOAT;
2224 header.requested_pixel_types[c] = TINYEXR_PIXELTYPE_FLOAT;
2227 header.compression_type = TINYEXR_COMPRESSIONTYPE_ZIP;
2229 const char *err =
nullptr;
2230 int ret = SaveEXRImageToFile(&image, &header, filename.c_str(), &err);
2232 free(header.channels);
2233 free(header.pixel_types);
2234 free(header.requested_pixel_types);
2236 if (ret != TINYEXR_SUCCESS) {
2237 std::string error_msg =
"ERROR (writeEXR): Failed to write EXR file '" + filename +
"'";
2239 error_msg +=
": " + std::string(err);
2240 FreeEXRErrorMessage(err);
2249 if (u < 0 || u > 1.f) {
2250 static bool spline_interp3_clamp_warning_shown =
false;
2251 if (!spline_interp3_clamp_warning_shown) {
2252 std::cerr <<
"WARNING (spline_interp3): Clamping query point 'u' to the interval (0,1)" << std::endl;
2253 spline_interp3_clamp_warning_shown =
true;
2255 u =
clamp(u, 0.f, 1.f);
2259 float B[16] = {2.f, -2.f, 1.f, 1.f, -3.f, 3.f, -2.f, -1.f, 0, 0, 1.f, 0, 1.f, 0, 0, 0};
2262 const float C[12] = {x_start.
x, x_start.
y, x_start.
z, x_end.
x, x_end.
y, x_end.
z, tan_start.
x, tan_start.
y, tan_start.
z, tan_end.
x, tan_end.
y, tan_end.
z};
2265 const float P[4] = {u * u * u, u * u, u, 1.f};
2267 float R[12] = {0.f};
2269 for (
int i = 0; i < 4; i++) {
2270 for (
int j = 0; j < 3; j++) {
2271 for (
int k = 0; k < 4; k++) {
2272 R[3 * i + j] =
R[3 * i + j] + B[4 * i + k] * C[3 * k + j];
2277 float xq[3] = {0.f};
2279 for (
int j = 0; j < 3; j++) {
2280 for (
int k = 0; k < 4; k++) {
2281 xq[j] = xq[j] + P[k] *
R[3 * k + j];
2289 const char *field_str = node.child_value(field);
2292 if (strlen(field_str) == 0) {
2304 const char *field_str = node.child_value(field);
2307 if (strlen(field_str) == 0) {
2319 const std::string field_str =
deblank(node.child_value(field));
2322 if (field_str.empty()) {
2332 const char *field_str = node.child_value(field);
2335 if (strlen(field_str) == 0) {
2345 const char *field_str = node.child_value(field);
2348 if (strlen(field_str) == 0) {
2358 const char *field_str = node.child_value(field);
2361 if (strlen(field_str) == 0) {
2362 value =
make_vec4(99999, 99999, 99999, 99999);
2371 const char *field_str = node.child_value(field);
2374 if (strlen(field_str) == 0) {
2384 const char *field_str = node.child_value(field);
2387 if (strlen(field_str) == 0) {
2397 const char *field_str = node.child_value(field);
2400 if (strlen(field_str) == 0) {
2401 value =
make_int4(99999, 99999, 99999, 99999);
2410 const char *field_str = node.child_value(field);
2413 if (strlen(field_str) == 0) {
2423 const char *field_str = node.child_value(field);
2426 if (strlen(field_str) == 0) {
2435float helios::fzero(
float (*f)(
float, std::vector<float> &,
const void *), std::vector<float> &vars,
const void *params,
float init_guess,
float err_tol,
int max_iter,
WarningAggregator *warnings) {
2436 constexpr float DELTA_SEED = 1e-3f;
2437 constexpr float DENOM_EPS = 1e-10f;
2438 constexpr float MAX_STEP_FACTOR = 0.5f;
2441 float x0 = init_guess;
2442 float x1 = (std::fabs(init_guess) > 1.0f) ? init_guess * (1.0f + DELTA_SEED) : init_guess + DELTA_SEED;
2444 float f0 = f(x0, vars, params);
2445 float f1 = f(x1, vars, params);
2448 bool use_bisection = (f0 * f1 < 0);
2449 float bracket_low = use_bisection ? std::min(x0, x1) : 0;
2450 float bracket_high = use_bisection ? std::max(x0, x1) : 0;
2452 for (
int iter = 0; iter < max_iter; ++iter) {
2454 float denom = f1 - f0;
2457 if (std::fabs(denom) < DENOM_EPS) {
2458 if (std::fabs(f1) < err_tol) {
2462 if (use_bisection) {
2463 float x2 = 0.5f * (bracket_low + bracket_high);
2464 if (std::fabs(x2 - x1) < err_tol * std::fabs(x2)) {
2467 float f2 = f(x2, vars, params);
2480 warnings->
addWarning(
"fzero_stagnation",
"fzero stagnated (|f'|≈0).");
2486 float x2 = x1 - f1 * (x1 - x0) / denom;
2489 float step = x2 - x1;
2490 float max_step = MAX_STEP_FACTOR * std::max(std::fabs(x1), 1.0f);
2491 if (std::fabs(step) > max_step) {
2492 step = (step > 0) ? max_step : -max_step;
2496 if (!std::isfinite(x2)) {
2498 warnings->
addWarning(
"fzero_nonfinite",
"fzero produced non-finite iterate.");
2503 float f2 = f(x2, vars, params);
2506 if (use_bisection) {
2515 float rel_step = std::fabs(x2 - x1) / (std::fabs(x2) + 1.0f);
2516 if (std::fabs(f2) < err_tol && rel_step < err_tol) {
2528 warnings->
addWarning(
"fzero_convergence_failure",
"fzero did not converge after " + std::to_string(max_iter) +
" iterations.");
2533float helios::fzero(
float (*f)(
float, std::vector<float> &,
const void *), std::vector<float> &vars,
const void *params,
float init_guess,
bool &converged,
float err_tol,
int max_iter) {
2534 constexpr float DELTA_SEED = 1e-3f;
2535 constexpr float DENOM_EPS = 1e-10f;
2536 constexpr float MAX_STEP_FACTOR = 0.5f;
2541 float x0 = init_guess;
2542 float x1 = (std::fabs(init_guess) > 1.0f) ? init_guess * (1.0f + DELTA_SEED) : init_guess + DELTA_SEED;
2544 float f0 = f(x0, vars, params);
2545 float f1 = f(x1, vars, params);
2548 bool use_bisection = (f0 * f1 < 0);
2549 float bracket_low = use_bisection ? std::min(x0, x1) : 0;
2550 float bracket_high = use_bisection ? std::max(x0, x1) : 0;
2552 for (
int iter = 0; iter < max_iter; ++iter) {
2554 float denom = f1 - f0;
2557 if (std::fabs(denom) < DENOM_EPS) {
2558 if (std::fabs(f1) < err_tol) {
2563 if (use_bisection) {
2564 float x2 = 0.5f * (bracket_low + bracket_high);
2565 if (std::fabs(x2 - x1) < err_tol * std::fabs(x2)) {
2569 float f2 = f(x2, vars, params);
2586 float x2 = x1 - f1 * (x1 - x0) / denom;
2589 float step = x2 - x1;
2590 float max_step = MAX_STEP_FACTOR * std::max(std::fabs(x1), 1.0f);
2591 if (std::fabs(step) > max_step) {
2592 step = (step > 0) ? max_step : -max_step;
2596 if (!std::isfinite(x2)) {
2600 float f2 = f(x2, vars, params);
2603 if (use_bisection) {
2612 float rel_step = std::fabs(x2 - x1) / (std::fabs(x2) + 1.0f);
2613 if (std::fabs(f2) < err_tol && rel_step < err_tol) {
2631 if (points.empty()) {
2636 if (points.size() == 1) {
2642 constexpr float EPSILON = 1.0E-5f;
2643 bool is_likely_increasing = points.size() < 2 || points[1].x > points[0].x;
2645 if (is_likely_increasing) {
2647 bool is_valid_increasing =
true;
2648 for (
size_t i = 1; i < points.size() && is_valid_increasing; ++i) {
2649 float deltaX = points[i].x - points[i - 1].x;
2650 if (deltaX <= EPSILON) {
2651 is_valid_increasing =
false;
2655 if (is_valid_increasing) {
2657 if (x <= points.front().x) {
2658 return points.front().y;
2660 if (x >= points.back().x) {
2661 return points.back().y;
2665 auto it = std::lower_bound(points.begin(), points.end(), x, [](
const vec2 &point,
float value) { return point.x < value; });
2667 size_t upper_idx = std::distance(points.begin(), it);
2668 size_t lower_idx = upper_idx - 1;
2670 const vec2 &p1 = points[lower_idx];
2671 const vec2 &p2 = points[upper_idx];
2674 float t = (x - p1.
x) / (p2.
x - p1.
x);
2675 return p1.
y + t * (p2.
y - p1.
y);
2680 bool is_increasing =
true;
2681 bool is_decreasing =
true;
2683 for (
size_t i = 1; i < points.size(); ++i) {
2684 float deltaX = points[i].x - points[i - 1].x;
2686 if (std::abs(deltaX) < EPSILON) {
2691 is_decreasing =
false;
2693 is_increasing =
false;
2697 if (!is_increasing && !is_decreasing) {
2698 helios_runtime_error(
"ERROR (interp1): X points must be monotonic (either all increasing or all decreasing).");
2702 if (is_decreasing) {
2703 if (x >= points.front().x) {
2704 return points.front().y;
2706 if (x <= points.back().x) {
2707 return points.back().y;
2711 auto it = std::lower_bound(points.begin(), points.end(), x, [](
const vec2 &point,
float value) { return point.x > value; });
2713 size_t upper_idx = std::distance(points.begin(), it);
2716 size_t lower_idx = upper_idx - 1;
2718 const vec2 &p1 = points[lower_idx];
2719 const vec2 &p2 = points[upper_idx];
2722 float t = (x - p1.
x) / (p2.
x - p1.
x);
2723 return p1.
y + t * (p2.
y - p1.
y);
2732 std::filesystem::path output_path_fs = filepath;
2733 return output_path_fs.extension().string();
2737 std::filesystem::path output_path_fs = filepath;
2738 return output_path_fs.stem().string();
2742 std::filesystem::path output_path_fs = filepath;
2743 return output_path_fs.filename().string();
2747 std::filesystem::path output_path_fs = filepath;
2748 std::filesystem::path output_path = output_path_fs.parent_path();
2749 std::string out_str = output_path.make_preferred().string();
2750 if (trailingslash && !out_str.empty()) {
2751 char last = out_str.back();
2752 if (last !=
'/' && last !=
'\\') {
2753 out_str += std::filesystem::path::preferred_separator;
2760 if (output_path.empty()) {
2764 std::filesystem::path output_path_fs = output_path;
2766 std::string output_file = output_path_fs.filename().string();
2767 std::string output_file_ext = output_path_fs.extension().string();
2768 std::string output_dir = output_path_fs.parent_path().string();
2770 if (output_file.empty()) {
2773 if (output_dir.find_last_of(
'/') != output_dir.length() - 1) {
2779 if (output_path.back() !=
'/' && output_path.back() !=
'\\') {
2785 if (!output_dir.empty() && !std::filesystem::exists(output_dir)) {
2786 if (!std::filesystem::create_directory(output_dir)) {
2791 if (!output_file.empty() && !allowable_file_extensions.empty()) {
2793 bool valid_extension =
false;
2794 for (
const auto &ext: allowable_file_extensions) {
2795 if (output_file_ext == ext) {
2796 valid_extension =
true;
2800 if (!valid_extension) {
2814 if (std::filesystem::exists(path) && std::filesystem::is_directory(path)) {
2821 if (path.back() ==
'/' || path.back() ==
'\\') {
2826 std::filesystem::path path_obj(path);
2827 std::string extension = path_obj.extension().string();
2831 if (extension.empty()) {
2832 std::string filename = path_obj.filename().string();
2835 if (filename.front() ==
'.' && filename !=
"." && filename !=
"..") {
2853 if (
const char *buildDir = std::getenv(
"HELIOS_BUILD")) {
2854 return std::string(buildDir);
2859 std::filesystem::path currentPath = std::filesystem::current_path();
2862 if (std::filesystem::exists(currentPath /
"plugins")) {
2863 return currentPath.string();
2867 std::filesystem::path parent = currentPath.parent_path();
2868 if (std::filesystem::exists(parent /
"plugins")) {
2869 return parent.string();
2873 return currentPath.string();
2883 std::string pluginAssetPath =
"plugins/" + pluginName +
"/" + assetPath;
2888 std::string pluginAssetPath =
"plugins/" + pluginName +
"/" + assetPath;
2895 if (filename.empty()) {
2900 std::filesystem::path filepath(filename);
2901 if (filepath.is_absolute()) {
2902 if (std::filesystem::exists(filepath)) {
2903 return std::filesystem::canonical(filepath);
2910 std::filesystem::path currentDirPath = std::filesystem::current_path() / filename;
2911 if (std::filesystem::exists(currentDirPath)) {
2912 return std::filesystem::canonical(currentDirPath);
2917 std::filesystem::path buildDirPath = std::filesystem::path(buildDir) / filename;
2919 if (std::filesystem::exists(buildDirPath)) {
2920 return std::filesystem::canonical(buildDirPath);
2929 if (filename.empty()) {
2930 return std::filesystem::current_path();
2936 if (!result.empty()) {
2941 std::filesystem::path currentDirPath = std::filesystem::current_path() / filename;
2943 std::filesystem::path buildDirPath = std::filesystem::path(buildDir) / filename;
2945 helios_runtime_error(
"ERROR (helios::resolveFilePath): Could not locate asset file: " + filename +
" (checked: " + currentDirPath.string() +
" and " + buildDirPath.string() +
"). " +
2946 "Ensure file exists relative to current directory or HELIOS_BUILD path.");
2952 std::string spectraPath =
"plugins/radiation/spectral_data/" + spectraFile;
2957 return std::filesystem::exists(assetPath) && std::filesystem::is_regular_file(assetPath);
2961 std::filesystem::path currentPath = std::filesystem::absolute(startPath);
2963 while (!currentPath.empty() && currentPath != currentPath.parent_path()) {
2964 std::filesystem::path cmakeFile = currentPath /
"CMakeLists.txt";
2965 if (std::filesystem::exists(cmakeFile) && std::filesystem::is_regular_file(cmakeFile)) {
2968 currentPath = currentPath.parent_path();
2976 if (relativePath.empty()) {
2981 std::filesystem::path inputPath(relativePath);
2982 if (inputPath.is_absolute()) {
2986 helios_runtime_error(
"ERROR (resolveProjectFile): Absolute path '" + relativePath +
"' does not exist or is not a regular file.");
2991 std::filesystem::path cwdPath = std::filesystem::current_path() / relativePath;
2993 return std::filesystem::absolute(cwdPath);
2998 if (!projectRoot.empty()) {
2999 std::filesystem::path projectPath = projectRoot / relativePath;
3001 return std::filesystem::absolute(projectPath);
3006 std::string errorMsg =
"ERROR (resolveProjectFile): Could not locate file '" + relativePath +
"'. Searched in:\n";
3007 errorMsg +=
" - Current working directory: " + std::filesystem::current_path().string() +
"\n";
3008 if (!projectRoot.empty()) {
3009 errorMsg +=
" - Project directory: " + projectRoot.string() +
"\n";
3011 errorMsg +=
" - Project directory: (not found - no CMakeLists.txt found in parent directories)\n";
3013 errorMsg +=
"Ensure the file exists in one of these locations.";
3020 std::ifstream stream(filepath.c_str());
3022 if (!stream.is_open()) {
3023 helios_runtime_error(
"ERROR (helios::importVectorFromFile): File " + filepath +
" could not be opened for reading. Check that it exists and that you have permission to read it.");
3026 std::istream_iterator<float> start(stream), end;
3027 std::vector<float> vec(start, end);
3034 std::gamma_distribution<float> dist_nu(nu, 1.0);
3035 std::gamma_distribution<float> dist_mu(mu, 1.0);
3037 float X = dist_nu(*generator);
3038 float Y = dist_mu(*generator);
3041 float b = X / (X + Y);
3044 return 0.5f *
PI_F * b;
3048float compute_elliptic_integral_first_kind(
float e) {
3051 float b = std::sqrt(1.0f - e * e);
3052 for (
int iter = 0; iter < 10; ++iter) {
3053 float an = 0.5f * (a + b);
3054 float bn = std::sqrt(a * b);
3058 return PI_F / (2.0f * a);
3063float evaluate_ellipsoidal_azimuth_PDF(
float phi,
float e,
float phi0,
float K_e) {
3064 float d = phi - phi0;
3065 float c2 = (1.f - e * e) * std::cos(d) * std::cos(d) + std::sin(d) * std::sin(d);
3066 return 1.f / (4.f * K_e * std::sqrt(c2));
3072 if (e < 0.f || e > 1.f) {
3073 helios_runtime_error(
"ERROR (helios::sample_ellipsoidal_azimuth): Eccentricity must be in [0,1].");
3077 float phi0 =
deg2rad(phi0_degrees);
3081 float b = std::sqrt(1.f - e * e);
3084 std::uniform_real_distribution<float> distT(0.f, 2.f *
PI_F);
3085 float t = distT(*generator);
3088 float x = a * std::cos(t);
3089 float y = b * std::sin(t);
3092 float phi = std::atan2(y, x) + phi0;
3097 else if (phi >= 2.f *
PI_F)
3112 std::vector<float> result(num);
3113 float step = (end - start) / (num - 1);
3115 for (
int i = 0; i < num; ++i) {
3116 result[i] = start + i * step;
3119 result[num - 1] = end;
3133 std::vector<vec2> result(num);
3134 vec2 step = (end - start) /
float(num - 1);
3136 for (
int i = 0; i < num; ++i) {
3137 result[i] = start + step * float(i);
3140 result[num - 1] = end;
3154 std::vector<vec3> result(num);
3155 vec3 step = (end - start) /
float(num - 1);
3157 for (
int i = 0; i < num; ++i) {
3158 result[i] = start + step * float(i);
3161 result[num - 1] = end;
3175 std::vector<vec4> result(num);
3176 vec4 step = (end - start) /
float(num - 1);
3178 for (
int i = 0; i < num; ++i) {
3179 result[i] = start + step * float(i);
3182 result[num - 1] = end;
3218 std::lock_guard<std::mutex> lock(mutex_);
3221 counts_[category]++;
3224 auto &messages = warnings_[category];
3225 if (messages.size() < MAX_EXAMPLES) {
3226 messages.push_back(message);
3231 std::lock_guard<std::mutex> lock(mutex_);
3233 if (counts_.empty()) {
3238 for (
const auto &entry: counts_) {
3239 const std::string &category = entry.first;
3240 size_t count = entry.second;
3242 stream <<
"WARNING: " << count <<
" instance" << (count > 1 ?
"s" :
"") <<
" of '" << category <<
"'";
3246 const auto &messages = warnings_[category];
3249 size_t examples_to_show = std::min(
size_t(3), messages.size());
3250 stream <<
" (showing first " << examples_to_show <<
"):" << std::endl;
3252 for (
size_t i = 0; i < examples_to_show; ++i) {
3253 stream <<
" - " << messages[i] << std::endl;
3256 if (count > MAX_EXAMPLES) {
3257 stream <<
" (Note: More than " << MAX_EXAMPLES <<
" warnings of this type were encountered)" << std::endl;
3259 stream << std::endl;
3262 stream << std::endl;
3272 std::lock_guard<std::mutex> lock(mutex_);
3274 auto it = counts_.find(category);
3275 if (it != counts_.end()) {
3282 std::lock_guard<std::mutex> lock(mutex_);