1.3.64
 
Loading...
Searching...
No Matches
RadiationModel.cpp
Go to the documentation of this file.
1
16#include "RadiationModel.h"
17#include "BufferIndexing.h"
18#include <climits>
19#include <cmath>
20#include <ctime>
21#include <fstream>
22#include <iomanip>
23#include <sstream>
24#include <unordered_set>
25
26using namespace helios;
27
29
30 context = context_a;
31
32 // Asset directory registration removed - now using HELIOS_BUILD resolution
33
34 // All default values set here
35
36 message_flag = true;
37
38 directRayCount_default = 100;
39 diffuseRayCount_default = 1000;
40
41 diffuseFlux_default = -1.f;
42
43 minScatterEnergy_default = 0.1;
44 scatteringDepth_default = 0;
45
46 rho_default = 0.f;
47 tau_default = 0.f;
48 eps_default = 1.f;
49
50 kappa_default = 1.f;
51 sigmas_default = 0.f;
52
53 temperature_default = 300;
54
55 periodic_flag = make_vec2(0, 0);
56
57 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/camera_spectral_library.xml").string());
58 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/light_spectral_library.xml").string());
59 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/soil_surface_spectral_library.xml").string());
60 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/leaf_surface_spectral_library.xml").string());
61 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/bark_surface_spectral_library.xml").string());
62 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/fruit_surface_spectral_library.xml").string());
63 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/solar_spectrum_ASTMG173.xml").string());
64 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/color_board/Calibrite_ColorChecker_Classic_colorboard.xml").string());
65 spectral_library_files.push_back(helios::resolvePluginAsset("radiation", "spectral_data/color_board/DGK_DKK_colorboard.xml").string());
66
67 // Initialize backend abstraction layer
68 backend = helios::RayTracingBackend::create("optix6");
69 backend->initialize();
70}
71
73 // Backend's unique_ptr will automatically clean up OptiX context
74}
75
77 message_flag = false;
78}
79
81 message_flag = true;
82}
83
85
86 if (strcmp(label, "reflectivity") == 0 || strcmp(label, "transmissivity") == 0) {
87 output_prim_data.emplace_back(label);
88 } else {
89 std::cout << "WARNING (RadiationModel::optionalOutputPrimitiveData): unknown output primitive data " << label << std::endl;
90 }
91}
92
93void RadiationModel::setDirectRayCount(const std::string &label, size_t N) {
94 if (!doesBandExist(label)) {
95 helios_runtime_error("ERROR (RadiationModel::setDirectRayCount): Cannot set ray count for band '" + label + "' because it is not a valid band.");
96 }
97 radiation_bands.at(label).directRayCount = N;
98}
99
100void RadiationModel::setDiffuseRayCount(const std::string &label, size_t N) {
101 if (!doesBandExist(label)) {
102 helios_runtime_error("ERROR (RadiationModel::setDiffuseRayCount): Cannot set ray count for band '" + label + "' because it is not a valid band.");
103 }
104 radiation_bands.at(label).diffuseRayCount = N;
105}
106
107void RadiationModel::setDiffuseRadiationFlux(const std::string &label, float flux) {
108 if (!doesBandExist(label)) {
109 helios_runtime_error("ERROR (RadiationModel::setDiffuseRadiationFlux): Cannot set flux value for band '" + label + "' because it is not a valid band.");
110 }
111 radiation_bands.at(label).diffuseFlux = flux;
112}
113
114void RadiationModel::setDiffuseRadiationExtinctionCoeff(const std::string &label, float K, const SphericalCoord &peak_dir) {
116}
117
118void RadiationModel::setDiffuseRadiationExtinctionCoeff(const std::string &label, float K, const vec3 &peak_dir) {
119 if (!doesBandExist(label)) {
120 helios_runtime_error("ERROR (RadiationModel::setDiffuseRadiationExtinctionCoeff): Cannot set diffuse extinction value for band '" + label + "' because it is not a valid band.");
121 }
122
123 vec3 dir = peak_dir;
124 dir.normalize();
125
126 int N = 100;
127 float norm = 0.f;
128 for (int j = 0; j < N; j++) {
129 for (int i = 0; i < N; i++) {
130 float theta = 0.5f * M_PI / float(N) * (0.5f + float(i));
131 float phi = 2.f * M_PI / float(N) * (0.5f + float(j));
132 vec3 n = sphere2cart(make_SphericalCoord(0.5f * M_PI - theta, phi));
133
134 float psi = acos_safe(n * dir);
135 float fd;
136 if (psi < M_PI / 180.f) {
137 fd = powf(M_PI / 180.f, -K);
138 } else {
139 fd = powf(psi, -K);
140 }
141
142 norm += fd * cosf(theta) * sinf(theta) * M_PI / float(N * N);
143 // note: the multipication factors are dtheta*dphi/pi = (0.5*pi/N)*(2*pi/N)/pi = pi/N^2
144 }
145 }
146
147 radiation_bands.at(label).diffuseExtinction = K;
148 radiation_bands.at(label).diffusePeakDir = dir;
149 radiation_bands.at(label).diffuseDistNorm = 1.f / norm;
150}
151
152void RadiationModel::setDiffuseSpectrumIntegral(float spectrum_integral) {
153
154 if (spectrum_integral < 0) {
155 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Spectrum integral must be non-negative.");
156 } else if (global_diffuse_spectrum.empty()) {
157 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Global diffuse spectrum has not been set. Call setDiffuseSpectrum() first.");
158 }
159
160 // Scale the global spectrum
161 float current_integral = integrateSpectrum(global_diffuse_spectrum);
162 if (current_integral > 0) {
163 float scale_factor = spectrum_integral / current_integral;
164 for (vec2 &wavelength: global_diffuse_spectrum) {
165 wavelength.y *= scale_factor;
166 }
167 }
168
169 // Apply scaled spectrum to all existing bands
170 for (auto &band: radiation_bands) {
171 band.second.diffuse_spectrum = global_diffuse_spectrum;
172 }
173
174 radiativepropertiesneedupdate = true;
175}
176
177void RadiationModel::setDiffuseSpectrumIntegral(float spectrum_integral, float wavelength1, float wavelength2) {
178
179 if (spectrum_integral < 0) {
180 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Spectrum integral must be non-negative.");
181 } else if (global_diffuse_spectrum.empty()) {
182 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Global diffuse spectrum has not been set. Call setDiffuseSpectrum() first.");
183 }
184
185 // Scale the global spectrum based on the integral within the specified wavelength range
186 float current_integral = integrateSpectrum(global_diffuse_spectrum, wavelength1, wavelength2);
187 if (current_integral > 0) {
188 float scale_factor = spectrum_integral / current_integral;
189 for (vec2 &wavelength: global_diffuse_spectrum) {
190 wavelength.y *= scale_factor;
191 }
192 }
193
194 // Apply scaled spectrum to all existing bands
195 for (auto &band: radiation_bands) {
196 band.second.diffuse_spectrum = global_diffuse_spectrum;
197 }
198
199 radiativepropertiesneedupdate = true;
200}
201
202void RadiationModel::setDiffuseSpectrumIntegral(const std::string &band_label, float spectrum_integral) {
203
204 if (spectrum_integral < 0) {
205 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Source integral must be non-negative.");
206 } else if (!doesBandExist(band_label)) {
207 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Cannot set integral for band '" + band_label + "' because it is not a valid band.");
208 } else if (radiation_bands.at(band_label).diffuse_spectrum.empty()) {
209 std::cerr << "WARNING (RadiationModel::setDiffuseSpectrumIntegral): Diffuse spectral distribution has not been set for radiation band '" + band_label + "'. Cannot set its integral." << std::endl;
210 return;
211 }
212
213 float current_integral = integrateSpectrum(radiation_bands.at(band_label).diffuse_spectrum);
214
215 for (vec2 &wavelength: radiation_bands.at(band_label).diffuse_spectrum) {
216 wavelength.y *= spectrum_integral / current_integral;
217 }
218
219 radiativepropertiesneedupdate = true;
220}
221
222void RadiationModel::setDiffuseSpectrumIntegral(const std::string &band_label, float spectrum_integral, float wavelength1, float wavelength2) {
223
224 if (spectrum_integral < 0) {
225 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Source integral must be non-negative.");
226 } else if (!doesBandExist(band_label)) {
227 helios_runtime_error("ERROR (RadiationModel::setDiffuseSpectrumIntegral): Cannot set integral for band '" + band_label + "' because it is not a valid band.");
228 }
229
230 float current_integral = integrateSpectrum(radiation_bands.at(band_label).diffuse_spectrum, wavelength1, wavelength2);
231
232 for (vec2 &wavelength: radiation_bands.at(band_label).diffuse_spectrum) {
233 wavelength.y *= spectrum_integral / current_integral;
234 }
235
236 radiativepropertiesneedupdate = true;
237}
238
239void RadiationModel::addRadiationBand(const std::string &label) {
240
241 if (radiation_bands.find(label) != radiation_bands.end()) {
242 std::cerr << "WARNING (RadiationModel::addRadiationBand): Radiation band " << label << " has already been added. Skipping this call to addRadiationBand()." << std::endl;
243 return;
244 }
245
246 RadiationBand band(label, directRayCount_default, diffuseRayCount_default, diffuseFlux_default, scatteringDepth_default, minScatterEnergy_default);
247
248 // Apply global diffuse spectrum if one was set
249 if (!global_diffuse_spectrum.empty()) {
250 band.diffuse_spectrum = global_diffuse_spectrum;
251 }
252
253 radiation_bands.emplace(label, band);
254
255 // Initialize all radiation source fluxes
256 for (auto &source: radiation_sources) {
257 source.source_fluxes[label] = -1.f;
258 }
259
260 radiativepropertiesneedupdate = true;
261}
262
263void RadiationModel::addRadiationBand(const std::string &label, float wavelength1, float wavelength2) {
264
265 if (radiation_bands.find(label) != radiation_bands.end()) {
266 std::cerr << "WARNING (RadiationModel::addRadiationBand): Radiation band " << label << " has already been added. Skipping this call to addRadiationBand()." << std::endl;
267 return;
268 } else if (wavelength1 > wavelength2) {
269 helios_runtime_error("ERROR (RadiationModel::addRadiationBand): The upper wavelength bound for a band must be greater than the lower bound.");
270 } else if (wavelength2 - wavelength1 < 1) {
271 helios_runtime_error("ERROR (RadiationModel::addRadiationBand): The waveband range of a radiation band must be at least 1 nm.");
272 }
273
274 RadiationBand band(label, directRayCount_default, diffuseRayCount_default, diffuseFlux_default, scatteringDepth_default, minScatterEnergy_default);
275
276 band.wavebandBounds = make_vec2(wavelength1, wavelength2);
277
278 // Apply global diffuse spectrum if one was set
279 if (!global_diffuse_spectrum.empty()) {
280 band.diffuse_spectrum = global_diffuse_spectrum;
281 }
282
283 radiation_bands.emplace(label, band);
284
285 // Initialize all radiation source fluxes
286 for (auto &source: radiation_sources) {
287 source.source_fluxes[label] = -1.f;
288 }
289
290 radiativepropertiesneedupdate = true;
291}
292
293void RadiationModel::copyRadiationBand(const std::string &old_label, const std::string &new_label) {
294
295 if (!doesBandExist(old_label)) {
296 helios_runtime_error("ERROR (RadiationModel::copyRadiationBand): Cannot copy band " + old_label + " because it does not exist.");
297 }
298
299 vec2 waveBounds = radiation_bands.at(old_label).wavebandBounds;
300
301 copyRadiationBand(old_label, new_label, waveBounds.x, waveBounds.y);
302}
303
304void RadiationModel::copyRadiationBand(const std::string &old_label, const std::string &new_label, float wavelength_min, float wavelength_max) {
305
306 if (!doesBandExist(old_label)) {
307 helios_runtime_error("ERROR (RadiationModel::copyRadiationBand): Cannot copy band " + old_label + " because it does not exist.");
308 }
309
310 RadiationBand band = radiation_bands.at(old_label);
311 band.label = new_label;
312 band.wavebandBounds = make_vec2(wavelength_min, wavelength_max);
313
314 radiation_bands.emplace(new_label, band);
315
316 // copy source fluxes
317 for (auto &source: radiation_sources) {
318 source.source_fluxes[new_label] = source.source_fluxes.at(old_label);
319 }
320
321 radiativepropertiesneedupdate = true;
322}
323
324bool RadiationModel::doesBandExist(const std::string &label) const {
325 if (radiation_bands.find(label) == radiation_bands.end()) {
326 return false;
327 } else {
328 return true;
329 }
330}
331
332void RadiationModel::disableEmission(const std::string &label) {
333
334 if (!doesBandExist(label)) {
335 helios_runtime_error("ERROR (RadiationModel::disableEmission): Cannot disable emission for band '" + label + "' because it is not a valid band.");
336 }
337
338 radiation_bands.at(label).emissionFlag = false;
339}
340
341void RadiationModel::enableEmission(const std::string &label) {
342
343 if (!doesBandExist(label)) {
344 helios_runtime_error("ERROR (RadiationModel::enableEmission): Cannot disable emission for band '" + label + "' because it is not a valid band.");
345 }
346
347 radiation_bands.at(label).emissionFlag = true;
348}
349
354
358
360
361 if (direction.magnitude() == 0) {
362 helios_runtime_error("ERROR (RadiationModel::addCollimatedRadiationSource): Invalid collimated source direction. Direction vector should not have length of zero.");
363 }
364
365 uint Nsources = radiation_sources.size() + 1;
366 if (Nsources > 256) {
367 helios_runtime_error("ERROR (RadiationModel::addCollimatedRadiationSource): A maximum of 256 radiation sources are allowed.");
368 }
369
370 bool warn_multiple_suns = false;
371 for (auto &source: radiation_sources) {
372 if (source.source_type == RADIATION_SOURCE_TYPE_COLLIMATED || source.source_type == RADIATION_SOURCE_TYPE_SUN_SPHERE) {
373 warn_multiple_suns = true;
374 }
375 }
376 if (warn_multiple_suns) {
377 std::cerr << "WARNING (RadiationModel::addCollimatedRadiationSource): Multiple sun sources have been added to the radiation model. This may lead to unintended behavior." << std::endl;
378 }
379
380 RadiationSource collimated_source(direction);
381
382 // initialize fluxes
383 for (const auto &band: radiation_bands) {
384 collimated_source.source_fluxes[band.first] = -1.f;
385 }
386
387 radiation_sources.emplace_back(collimated_source);
388
389 radiativepropertiesneedupdate = true;
390
391 return Nsources - 1;
392}
393
394uint RadiationModel::addSphereRadiationSource(const vec3 &position, float radius) {
395
396 if (radius <= 0) {
397 helios_runtime_error("ERROR (RadiationModel::addSphereRadiationSource): Spherical radiation source radius must be positive.");
398 }
399
400 uint Nsources = radiation_sources.size() + 1;
401 if (Nsources > 256) {
402 helios_runtime_error("ERROR (RadiationModel::addSphereRadiationSource): A maximum of 256 radiation sources are allowed.");
403 }
404
405 RadiationSource sphere_source(position, 2.f * fabsf(radius));
406
407 // initialize fluxes
408 for (const auto &band: radiation_bands) {
409 sphere_source.source_fluxes[band.first] = -1.f;
410 }
411
412 radiation_sources.emplace_back(sphere_source);
413
414 uint sourceID = Nsources - 1;
415
416 if (islightvisualizationenabled) {
417 buildLightModelGeometry(sourceID);
418 }
419
420 radiativepropertiesneedupdate = true;
421
422 return sourceID;
423}
424
428
432
434
435 uint Nsources = radiation_sources.size() + 1;
436 if (Nsources > 256) {
437 helios_runtime_error("ERROR (RadiationModel::addSunSphereRadiationSource): A maximum of 256 radiation sources are allowed.");
438 }
439
440 bool warn_multiple_suns = false;
441 for (auto &source: radiation_sources) {
442 if (source.source_type == RADIATION_SOURCE_TYPE_COLLIMATED || source.source_type == RADIATION_SOURCE_TYPE_SUN_SPHERE) {
443 warn_multiple_suns = true;
444 }
445 }
446 if (warn_multiple_suns) {
447 std::cerr << "WARNING (RadiationModel::addSunSphereRadiationSource): Multiple sun sources have been added to the radiation model. This may lead to unintended behavior." << std::endl;
448 }
449
450 RadiationSource sphere_source(150e9 * sun_direction / sun_direction.magnitude(), 150e9, 2.f * 695.5e6, sigma * powf(5700, 4) / 1288.437f);
451
452 // initialize fluxes
453 for (const auto &band: radiation_bands) {
454 sphere_source.source_fluxes[band.first] = -1.f;
455 }
456
457 radiation_sources.emplace_back(sphere_source);
458
459 radiativepropertiesneedupdate = true;
460
461 return Nsources - 1;
462}
463
464uint RadiationModel::addRectangleRadiationSource(const vec3 &position, const vec2 &size, const vec3 &rotation_rad) {
465
466 if (size.x <= 0 || size.y <= 0) {
467 helios_runtime_error("ERROR (RadiationModel::addRectangleRadiationSource): Radiation source size must be positive.");
468 }
469
470 uint Nsources = radiation_sources.size() + 1;
471 if (Nsources > 256) {
472 helios_runtime_error("ERROR (RadiationModel::addRectangleRadiationSource): A maximum of 256 radiation sources are allowed.");
473 }
474
475 RadiationSource rectangle_source(position, size, rotation_rad);
476
477 // initialize fluxes
478 for (const auto &band: radiation_bands) {
479 rectangle_source.source_fluxes[band.first] = -1.f;
480 }
481
482 radiation_sources.emplace_back(rectangle_source);
483
484 uint sourceID = Nsources - 1;
485
486 if (islightvisualizationenabled) {
487 buildLightModelGeometry(sourceID);
488 }
489
490 radiativepropertiesneedupdate = true;
491
492 return sourceID;
493}
494
495uint RadiationModel::addDiskRadiationSource(const vec3 &position, float radius, const vec3 &rotation_rad) {
496
497 if (radius <= 0) {
498 helios_runtime_error("ERROR (RadiationModel::addDiskRadiationSource): Disk radiation source radius must be positive.");
499 }
500
501 uint Nsources = radiation_sources.size() + 1;
502 if (Nsources > 256) {
503 helios_runtime_error("ERROR (RadiationModel::addDiskRadiationSource): A maximum of 256 radiation sources are allowed.");
504 }
505
506 RadiationSource disk_source(position, radius, rotation_rad);
507
508 // initialize fluxes
509 for (const auto &band: radiation_bands) {
510 disk_source.source_fluxes[band.first] = -1.f;
511 }
512
513 radiation_sources.emplace_back(disk_source);
514
515 uint sourceID = Nsources - 1;
516
517 if (islightvisualizationenabled) {
518 buildLightModelGeometry(sourceID);
519 }
520
521 radiativepropertiesneedupdate = true;
522
523 return sourceID;
524}
525
527
528 if (sourceID >= radiation_sources.size()) {
529 helios_runtime_error("ERROR (RadiationModel::deleteRadiationSource): Source ID out of bounds. Only " + std::to_string(radiation_sources.size() - 1) + " radiation sources have been created.");
530 }
531
532 radiation_sources.erase(radiation_sources.begin() + sourceID);
533
534 radiativepropertiesneedupdate = true;
535}
536
537void RadiationModel::setSourceSpectrumIntegral(uint source_ID, float source_integral) {
538
539 if (source_ID >= radiation_sources.size()) {
540 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrumIntegral): Source ID out of bounds. Only " + std::to_string(radiation_sources.size() - 1) + " radiation sources have been created.");
541 } else if (source_integral < 0) {
542 helios_runtime_error("ERROR (RadiationModel::setSourceIntegral): Source integral must be non-negative.");
543 }
544
545 float current_integral = integrateSpectrum(radiation_sources.at(source_ID).source_spectrum);
546
547 for (vec2 &wavelength: radiation_sources.at(source_ID).source_spectrum) {
548 wavelength.y *= source_integral / current_integral;
549 }
550}
551
552void RadiationModel::setSourceSpectrumIntegral(uint source_ID, float source_integral, float wavelength1, float wavelength2) {
553
554 if (source_ID >= radiation_sources.size()) {
555 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrumIntegral): Source ID out of bounds. Only " + std::to_string(radiation_sources.size() - 1) + " radiation sources have been created.");
556 } else if (source_integral < 0) {
557 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrumIntegral): Source integral must be non-negative.");
558 } else if (radiation_sources.at(source_ID).source_spectrum.empty()) {
559 std::cout << "WARNING (RadiationModel::setSourceSpectrumIntegral): Spectral distribution has not been set for radiation source. Cannot set its integral." << std::endl;
560 return;
561 }
562
563 RadiationSource &source = radiation_sources.at(source_ID);
564
565 float old_integral = integrateSpectrum(source.source_spectrum, wavelength1, wavelength2);
566
567 for (vec2 &wavelength: source.source_spectrum) {
568 wavelength.y *= source_integral / old_integral;
569 }
570}
571
572void RadiationModel::setSourceFlux(uint source_ID, const std::string &label, float flux) {
573
574 if (!doesBandExist(label)) {
575 helios_runtime_error("ERROR (RadiationModel::setSourceFlux): Cannot add set source flux for band '" + label + "' because it is not a valid band.");
576 } else if (source_ID >= radiation_sources.size()) {
577 helios_runtime_error("ERROR (RadiationModel::setSourceFlux): Source ID out of bounds. Only " + std::to_string(radiation_sources.size() - 1) + " radiation sources have been created.");
578 } else if (flux < 0) {
579 helios_runtime_error("ERROR (RadiationModel::setSourceFlux): Source flux must be non-negative.");
580 }
581
582 radiation_sources.at(source_ID).source_fluxes[label] = flux * radiation_sources.at(source_ID).source_flux_scaling_factor;
583}
584
585void RadiationModel::setSourceFlux(const std::vector<uint> &source_ID, const std::string &band_label, float flux) {
586 for (auto ID: source_ID) {
587 setSourceFlux(ID, band_label, flux);
588 }
589}
590
591float RadiationModel::getSourceFlux(uint source_ID, const std::string &label) const {
592
593 if (!doesBandExist(label)) {
594 helios_runtime_error("ERROR (RadiationModel::getSourceFlux): Cannot get source flux for band '" + label + "' because it is not a valid band.");
595 } else if (source_ID >= radiation_sources.size()) {
596 helios_runtime_error("ERROR (RadiationModel::getSourceFlux): Source ID out of bounds. Only " + std::to_string(radiation_sources.size() - 1) + " radiation sources have been created.");
597 } else if (radiation_sources.at(source_ID).source_fluxes.find(label) == radiation_sources.at(source_ID).source_fluxes.end()) {
598 helios_runtime_error("ERROR (RadiationModel::getSourceFlux): Cannot get flux for source #" + std::to_string(source_ID) + " because radiative band '" + label + "' does not exist.");
599 }
600
601 const RadiationSource &source = radiation_sources.at(source_ID);
602
603 if (!source.source_spectrum.empty() && source.source_fluxes.at(label) < 0.f) { // source spectrum was specified (and not overridden by setting source flux manually)
604 vec2 wavebounds = radiation_bands.at(label).wavebandBounds;
605 if (wavebounds == make_vec2(0, 0)) {
606 wavebounds = make_vec2(source.source_spectrum.front().x, source.source_spectrum.back().x);
607 }
608 return integrateSpectrum(source.source_spectrum, wavebounds.x, wavebounds.y) * source.source_flux_scaling_factor;
609 } else if (source.source_fluxes.at(label) < 0.f) {
610 return 0;
611 }
612
613 return source.source_fluxes.at(label);
614}
615
616void RadiationModel::setSourceSpectrum(uint source_ID, const std::vector<helios::vec2> &spectrum) {
617
618 if (source_ID >= radiation_sources.size()) {
619 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrum): Cannot add radiation spectra for this source because it is not a valid radiation source ID.\n");
620 }
621
622 // validate spectrum
623 for (auto s = 0; s < spectrum.size(); s++) {
624 // check that wavelengths are monotonic
625 if (s > 0 && spectrum.at(s).x <= spectrum.at(s - 1).x) {
626 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrum): Source spectral data validation failed. Wavelengths must increase monotonically.");
627 }
628 // check that wavelength is within a reasonable range
629 if (spectrum.at(s).x < 0 || spectrum.at(s).x > 100000) {
630 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrum): Source spectral data validation failed. Wavelength value of " + std::to_string(spectrum.at(s).x) + " appears to be erroneous.");
631 }
632 // check that flux is non-negative
633 if (spectrum.at(s).y < 0) {
634 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrum): Source spectral data validation failed. Flux value at wavelength of " + std::to_string(spectrum.at(s).x) + " appears is negative.");
635 }
636 }
637
638 radiation_sources.at(source_ID).source_spectrum = spectrum;
639
640 radiativepropertiesneedupdate = true;
641}
642
643void RadiationModel::setSourceSpectrum(const std::vector<uint> &source_ID, const std::vector<helios::vec2> &spectrum) {
644 for (auto ID: source_ID) {
645 setSourceSpectrum(ID, spectrum);
646 }
647}
648
649void RadiationModel::setSourceSpectrum(uint source_ID, const std::string &spectrum_label) {
650
651 if (source_ID >= radiation_sources.size()) {
652 helios_runtime_error("ERROR (RadiationModel::setSourceSpectrum): Cannot add radiation spectra for this source because it is not a valid radiation source ID.\n");
653 }
654
655 std::vector<vec2> spectrum = loadSpectralData(spectrum_label);
656
657 radiation_sources.at(source_ID).source_spectrum = spectrum;
658 radiation_sources.at(source_ID).source_spectrum_label = spectrum_label;
659 radiation_sources.at(source_ID).source_spectrum_version = context->getGlobalDataVersion(spectrum_label.c_str());
660
661 radiativepropertiesneedupdate = true;
662}
663
664void RadiationModel::setSourceSpectrum(const std::vector<uint> &source_ID, const std::string &spectrum_label) {
665 for (auto ID: source_ID) {
666 setSourceSpectrum(ID, spectrum_label);
667 }
668}
669
670void RadiationModel::setDiffuseSpectrum(const std::string &spectrum_label) {
671
672 std::vector<vec2> spectrum;
673
674 // standard solar spectrum
675 if (spectrum_label == "ASTMG173") {
676 spectrum = loadSpectralData("solar_spectrum_diffuse_ASTMG173");
677 global_diffuse_spectrum_label = "solar_spectrum_diffuse_ASTMG173";
678 } else {
679 spectrum = loadSpectralData(spectrum_label);
680 global_diffuse_spectrum_label = spectrum_label;
681 }
682
683 // Store globally so new bands will also get this spectrum
684 global_diffuse_spectrum = spectrum;
685 global_diffuse_spectrum_version = context->getGlobalDataVersion(global_diffuse_spectrum_label.c_str());
686
687 // Apply to all existing bands
688 for (auto &band_pair: radiation_bands) {
689 band_pair.second.diffuse_spectrum = spectrum;
690 }
691
692 radiativepropertiesneedupdate = true;
693}
694
695float RadiationModel::getDiffuseFlux(const std::string &band_label) const {
696
697 if (!doesBandExist(band_label)) {
698 helios_runtime_error("ERROR (RadiationModel::getDiffuseFlux): Cannot get diffuse flux for band '" + band_label + "' because it is not a valid band.");
699 }
700
701 const RadiationBand &band = radiation_bands.at(band_label);
702
703 // For emission-enabled bands: spectra are not relevant, only use manual flux
704 if (band.emissionFlag) {
705 if (band.diffuseFlux >= 0.f) {
706 return band.diffuseFlux;
707 }
708 return 0.f;
709 }
710
711 // For non-emission bands: check manual flux first, then spectrum
712 if (band.diffuseFlux >= 0.f) {
713 return band.diffuseFlux;
714 }
715
716 const std::vector<vec2> &spectrum = band.diffuse_spectrum;
717 if (!spectrum.empty()) {
718 vec2 wavebounds = band.wavebandBounds;
719 if (wavebounds == make_vec2(0, 0)) {
720 wavebounds = make_vec2(spectrum.front().x, spectrum.back().x);
721 }
722 return integrateSpectrum(spectrum, wavebounds.x, wavebounds.y);
723 }
724
725 return 0.f;
726}
727
729 islightvisualizationenabled = true;
730
731 // build the geometry of any existing sources at this point
732 for (int s = 0; s < radiation_sources.size(); s++) {
733 buildLightModelGeometry(s);
734 }
735}
736
738 islightvisualizationenabled = false;
739 for (auto &UUIDs: source_model_UUIDs) {
740 context->deletePrimitive(UUIDs.second);
741 }
742}
743
745 iscameravisualizationenabled = true;
746
747 // build the geometry of any existing cameras at this point
748 for (auto &cam: cameras) {
749 buildCameraModelGeometry(cam.first);
750 }
751}
752
754 iscameravisualizationenabled = false;
755 for (auto &UUIDs: camera_model_UUIDs) {
756 context->deletePrimitive(UUIDs.second);
757 }
758}
759
760void RadiationModel::buildLightModelGeometry(uint sourceID) {
761
762 assert(sourceID < radiation_sources.size());
763
764 RadiationSource source = radiation_sources.at(sourceID);
765 if (source.source_type == RADIATION_SOURCE_TYPE_SPHERE) {
766 source_model_UUIDs[sourceID] = context->loadOBJ("SphereLightSource.obj", true);
767 } else if (source.source_type == RADIATION_SOURCE_TYPE_SUN_SPHERE) {
768 source_model_UUIDs[sourceID] = context->loadOBJ("SphereLightSource.obj", true);
769 } else if (source.source_type == RADIATION_SOURCE_TYPE_DISK) {
770 source_model_UUIDs[sourceID] = context->loadOBJ("DiskLightSource.obj", true);
771 context->scalePrimitive(source_model_UUIDs.at(sourceID), make_vec3(source.source_width.x, source.source_width.y, 0.05f * source.source_width.x));
772 std::vector<uint> UUIDs_arrow = context->loadOBJ("Arrow.obj", true);
773 source_model_UUIDs.at(sourceID).insert(source_model_UUIDs.at(sourceID).begin(), UUIDs_arrow.begin(), UUIDs_arrow.end());
774 context->scalePrimitive(UUIDs_arrow, make_vec3(1, 1, 1) * 0.25f * source.source_width.x);
775 } else if (source.source_type == RADIATION_SOURCE_TYPE_RECTANGLE) {
776 source_model_UUIDs[sourceID] = context->loadOBJ("RectangularLightSource.obj", true);
777 context->scalePrimitive(source_model_UUIDs.at(sourceID), make_vec3(source.source_width.x, source.source_width.y, fmin(0.05f * (source.source_width.x + source.source_width.y), 0.5f * fmin(source.source_width.x, source.source_width.y))));
778 std::vector<uint> UUIDs_arrow = context->loadOBJ("Arrow.obj", true);
779 source_model_UUIDs.at(sourceID).insert(source_model_UUIDs.at(sourceID).begin(), UUIDs_arrow.begin(), UUIDs_arrow.end());
780 context->scalePrimitive(UUIDs_arrow, make_vec3(1, 1, 1) * 0.15f * (source.source_width.x + source.source_width.y));
781 } else {
782 return;
783 }
784
785 if (source.source_type == RADIATION_SOURCE_TYPE_SPHERE) {
786 context->scalePrimitive(source_model_UUIDs.at(sourceID), make_vec3(source.source_width.x, source.source_width.x, source.source_width.x));
787 context->translatePrimitive(source_model_UUIDs.at(sourceID), source.source_position);
788 } else if (source.source_type == RADIATION_SOURCE_TYPE_SUN_SPHERE) {
789 vec3 center;
790 float radius;
791 context->getDomainBoundingSphere(center, radius);
792 context->scalePrimitive(source_model_UUIDs.at(sourceID), make_vec3(1, 1, 1) * 0.1f * radius);
793 vec3 sunvec = source.source_position;
794 sunvec.normalize();
795 context->translatePrimitive(source_model_UUIDs.at(sourceID), center + sunvec * radius);
796 } else {
797 context->rotatePrimitive(source_model_UUIDs.at(sourceID), source.source_rotation.x, "x");
798 context->rotatePrimitive(source_model_UUIDs.at(sourceID), source.source_rotation.y, "y");
799 context->rotatePrimitive(source_model_UUIDs.at(sourceID), source.source_rotation.z, "z");
800 context->translatePrimitive(source_model_UUIDs.at(sourceID), source.source_position);
801 }
802
803 context->setPrimitiveData(source_model_UUIDs.at(sourceID), "twosided_flag", uint(3)); // source model does not interact with radiation field
804}
805
806void RadiationModel::buildCameraModelGeometry(const std::string &cameralabel) {
807
808 assert(cameras.find(cameralabel) != cameras.end());
809
810 RadiationCamera camera = cameras.at(cameralabel);
811
812 vec3 viewvec = camera.lookat - camera.position;
813 SphericalCoord viewsph = cart2sphere(viewvec);
814
815 camera_model_UUIDs[cameralabel] = context->loadOBJ("Camera.obj", true);
816
817 context->rotatePrimitive(camera_model_UUIDs.at(cameralabel), viewsph.elevation, "x");
818 context->rotatePrimitive(camera_model_UUIDs.at(cameralabel), -viewsph.azimuth, "z");
819
820 context->translatePrimitive(camera_model_UUIDs.at(cameralabel), camera.position);
821
822 context->setPrimitiveData(camera_model_UUIDs.at(cameralabel), "twosided_flag", uint(3)); // camera model does not interact with radiation field
823}
824
825void RadiationModel::updateLightModelPosition(uint sourceID, const helios::vec3 &delta_position) {
826
827 assert(sourceID < radiation_sources.size());
828
829 RadiationSource source = radiation_sources.at(sourceID);
830
831 if (source.source_type != RADIATION_SOURCE_TYPE_SPHERE && source.source_type != RADIATION_SOURCE_TYPE_DISK && source.source_type != RADIATION_SOURCE_TYPE_RECTANGLE) {
832 return;
833 }
834
835 context->translatePrimitive(source_model_UUIDs.at(sourceID), delta_position);
836}
837
838void RadiationModel::updateCameraModelPosition(const std::string &cameralabel) {
839
840 assert(cameras.find(cameralabel) != cameras.end());
841
842 context->deletePrimitive(camera_model_UUIDs.at(cameralabel));
843 buildCameraModelGeometry(cameralabel);
844}
845
846float RadiationModel::integrateSpectrum(uint source_ID, const std::vector<helios::vec2> &object_spectrum, float wavelength1, float wavelength2) const {
847
848 if (source_ID >= radiation_sources.size()) {
849 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Radiation spectrum was not set for source ID. Make sure to set its spectrum using setSourceSpectrum() function.");
850 } else if (object_spectrum.size() < 2) {
851 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Radiation spectrum must have at least 2 wavelengths.");
852 } else if (wavelength1 > wavelength2 || wavelength1 == wavelength2) {
853 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Lower wavelength bound must be less than the upper wavelength bound.");
854 }
855
856 std::vector<helios::vec2> source_spectrum = radiation_sources.at(source_ID).source_spectrum;
857
858 int istart = 0;
859 int iend = (int) object_spectrum.size() - 1;
860 for (auto i = 0; i < object_spectrum.size() - 1; i++) {
861
862 if (object_spectrum.at(i).x <= wavelength1 && object_spectrum.at(i + 1).x > wavelength1) {
863 istart = i;
864 }
865 if (object_spectrum.at(i).x <= wavelength2 && object_spectrum.at(i + 1).x > wavelength2) {
866 iend = i + 1;
867 break;
868 }
869 }
870
871 float E = 0;
872 float Etot = 0;
873 for (auto i = istart; i < iend; i++) {
874
875 float x0 = object_spectrum.at(i).x;
876 float Esource0 = interp1(source_spectrum, object_spectrum.at(i).x);
877 float Eobject0 = object_spectrum.at(i).y;
878
879 float x1 = object_spectrum.at(i + 1).x;
880 float Eobject1 = object_spectrum.at(i + 1).y;
881 float Esource1 = interp1(source_spectrum, object_spectrum.at(i + 1).x);
882
883 E += 0.5f * (Eobject0 * Esource0 + Eobject1 * Esource1) * (x1 - x0);
884 Etot += 0.5f * (Esource1 + Esource0) * (x1 - x0);
885 }
886
887 return E / Etot;
888}
889
890float RadiationModel::integrateSpectrum(const std::vector<helios::vec2> &object_spectrum, float wavelength1, float wavelength2) const {
891
892 if (object_spectrum.size() < 2) {
893 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Radiation spectrum must have at least 2 wavelengths.");
894 } else if (wavelength1 > wavelength2 || wavelength1 == wavelength2) {
895 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Lower wavelength bound must be less than the upper wavelength bound.");
896 }
897
898 int istart = 1;
899 int iend = (int) object_spectrum.size() - 1;
900 for (auto i = 0; i < object_spectrum.size() - 1; i++) {
901
902 if (object_spectrum.at(i).x <= wavelength1 && object_spectrum.at(i + 1).x > wavelength1) {
903 istart = i;
904 }
905 if (object_spectrum.at(i).x <= wavelength2 && object_spectrum.at(i + 1).x > wavelength2) {
906 iend = i + 1;
907 break;
908 }
909 }
910
911 float E = 0;
912 for (auto i = istart; i < iend; i++) {
913 float E0 = object_spectrum.at(i).y;
914 float x0 = object_spectrum.at(i).x;
915 float E1 = object_spectrum.at(i + 1).y;
916 float x1 = object_spectrum.at(i + 1).x;
917 E += (E0 + E1) * (x1 - x0) * 0.5f;
918 }
919
920 return E;
921}
922
923float RadiationModel::integrateSpectrum(const std::vector<helios::vec2> &object_spectrum) const {
924 float wavelength1 = object_spectrum.at(0).x;
925 float wavelength2 = object_spectrum.at(object_spectrum.size() - 1).x;
926 float E = RadiationModel::integrateSpectrum(object_spectrum, wavelength1, wavelength2);
927 return E;
928}
929
930float RadiationModel::integrateSpectrum(uint source_ID, const std::vector<helios::vec2> &object_spectrum, const std::vector<helios::vec2> &camera_spectrum) const {
931
932 if (source_ID >= radiation_sources.size()) {
933 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Radiation spectrum was not set for source ID. Make sure to set its spectrum using setSourceSpectrum() function.");
934 } else if (object_spectrum.size() < 2) {
935 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Radiation spectrum must have at least 2 wavelengths.");
936 }
937
938 std::vector<helios::vec2> source_spectrum = radiation_sources.at(source_ID).source_spectrum;
939
940 float E = 0;
941 float Etot = 0;
942 for (auto i = 1; i < object_spectrum.size(); i++) {
943
944 if (object_spectrum.at(i).x <= source_spectrum.front().x || object_spectrum.at(i).x <= camera_spectrum.front().x) {
945 continue;
946 }
947 if (object_spectrum.at(i).x > source_spectrum.back().x || object_spectrum.at(i).x > camera_spectrum.back().x) {
948 break;
949 }
950 float x1 = object_spectrum.at(i).x;
951 float Eobject1 = object_spectrum.at(i).y;
952 float Esource1 = interp1(source_spectrum, x1);
953 float Ecamera1 = interp1(camera_spectrum, x1);
954
955
956 float x0 = object_spectrum.at(i - 1).x;
957 float Eobject0 = object_spectrum.at(i - 1).y;
958 float Esource0 = interp1(source_spectrum, x0);
959 float Ecamera0 = interp1(camera_spectrum, x0);
960
961 E += 0.5f * ((Eobject1 * Esource1 * Ecamera1) + (Eobject0 * Ecamera0 * Esource0)) * (x1 - x0);
962 Etot += 0.5f * (Esource1 + Esource0) * (x1 - x0);
963 }
964
965
966 return E / Etot;
967}
968
969float RadiationModel::integrateSpectrum(const std::vector<helios::vec2> &object_spectrum, const std::vector<helios::vec2> &camera_spectrum) const {
970
971 if (object_spectrum.size() < 2) {
972 helios_runtime_error("ERROR (RadiationModel::integrateSpectrum): Radiation spectrum must have at least 2 wavelengths.");
973 }
974
975 float E = 0;
976 float Etot = 0;
977 for (auto i = 1; i < object_spectrum.size(); i++) {
978
979 if (object_spectrum.at(i).x <= camera_spectrum.front().x) {
980 continue;
981 }
982 if (object_spectrum.at(i).x > camera_spectrum.back().x) {
983 break;
984 }
985
986 float x1 = object_spectrum.at(i).x;
987 float Eobject1 = object_spectrum.at(i).y;
988 float Ecamera1 = interp1(camera_spectrum, x1);
989
990
991 float x0 = object_spectrum.at(i - 1).x;
992 float Eobject0 = object_spectrum.at(i - 1).y;
993 float Ecamera0 = interp1(camera_spectrum, x0);
994
995 E += 0.5f * ((Eobject1 * Ecamera1) + (Eobject0 * Ecamera0)) * (x1 - x0);
996 Etot += 0.5f * (Ecamera1 + Ecamera0) * (x1 - x0);
997 }
998
999 return E / Etot;
1000}
1001
1002float RadiationModel::integrateSourceSpectrum(uint source_ID, float wavelength1, float wavelength2) const {
1003
1004 if (source_ID >= radiation_sources.size()) {
1005 helios_runtime_error("ERROR (RadiationModel::integrateSourceSpectrum): Radiation spectrum was not set for source ID. Make sure to set its spectrum using setSourceSpectrum() function.");
1006 } else if (wavelength1 > wavelength2 || wavelength1 == wavelength2) {
1007 helios_runtime_error("ERROR (RadiationModel::integrateSourceSpectrum): Lower wavelength bound must be less than the upper wavelength bound.");
1008 }
1009
1010 return integrateSpectrum(radiation_sources.at(source_ID).source_spectrum, wavelength1, wavelength2);
1011}
1012
1013void RadiationModel::scaleSpectrum(const std::string &existing_global_data_label, const std::string &new_global_data_label, float scale_factor) const {
1014
1015 std::vector<helios::vec2> spectrum = loadSpectralData(existing_global_data_label);
1016
1017 for (helios::vec2 &s: spectrum) {
1018 s.y *= scale_factor;
1019 }
1020
1021 context->setGlobalData(new_global_data_label.c_str(), spectrum);
1022}
1023
1024void RadiationModel::scaleSpectrum(const std::string &global_data_label, float scale_factor) const {
1025
1026 std::vector<vec2> spectrum = loadSpectralData(global_data_label);
1027
1028 for (vec2 &s: spectrum) {
1029 s.y *= scale_factor;
1030 }
1031
1032 context->setGlobalData(global_data_label.c_str(), spectrum);
1033}
1034
1035void RadiationModel::scaleSpectrumRandomly(const std::string &existing_global_data_label, const std::string &new_global_data_label, float minimum_scale_factor, float maximum_scale_factor) const {
1036
1037 scaleSpectrum(existing_global_data_label, new_global_data_label, context->randu(minimum_scale_factor, maximum_scale_factor));
1038}
1039
1040
1041void RadiationModel::blendSpectra(const std::string &new_spectrum_label, const std::vector<std::string> &spectrum_labels, const std::vector<float> &weights) const {
1042
1043 if (spectrum_labels.size() != weights.size()) {
1044 helios_runtime_error("ERROR (RadiationModel::blendSpectra): number of spectra and weights must be equal");
1045 } else if (sum(weights) != 1.f) {
1046 helios_runtime_error("ERROR (RadiationModel::blendSpectra): weights must sum to 1");
1047 }
1048
1049 std::vector<vec2> new_spectrum;
1050 uint spectrum_size = 0;
1051
1052 std::vector<std::vector<vec2>> spectrum(spectrum_labels.size());
1053
1054 uint lambda_start = 0;
1055 uint lambda_end = 0;
1056 for (uint i = 0; i < spectrum_labels.size(); i++) {
1057
1058 spectrum.at(i) = loadSpectralData(spectrum_labels.at(i));
1059
1060 if (i == 0) {
1061 lambda_start = spectrum.at(i).front().x;
1062 lambda_end = spectrum.at(i).back().x;
1063 } else {
1064 if (spectrum.at(i).front().x > lambda_start) {
1065 lambda_start = spectrum.at(i).front().x;
1066 }
1067 if (spectrum.at(i).back().x < lambda_end) {
1068 lambda_end = spectrum.at(i).back().x;
1069 }
1070 }
1071 }
1072
1073 spectrum_size = lambda_end - lambda_start + 1;
1074 new_spectrum.resize(spectrum_size);
1075 for (uint j = 0; j < spectrum_size; j++) {
1076 new_spectrum.at(j) = make_vec2(lambda_start + j, 0);
1077 }
1078
1079 // trim front
1080 for (uint i = 0; i < spectrum_labels.size(); i++) {
1081 for (uint j = 0; j < spectrum.at(i).size(); j++) {
1082
1083 if (spectrum.at(i).at(j).x >= lambda_start) {
1084 if (j > 0) {
1085 spectrum.at(i).erase(spectrum.at(i).begin(), spectrum.at(i).begin() + j);
1086 }
1087 break;
1088 }
1089 }
1090 }
1091
1092 // trim back
1093 for (uint i = 0; i < spectrum_labels.size(); i++) {
1094 for (int j = spectrum.at(i).size() - 1; j <= 0; j--) {
1095
1096 if (spectrum.at(i).at(j).x <= lambda_end) {
1097 if (j < spectrum.at(i).size() - 1) {
1098 spectrum.at(i).erase(spectrum.at(i).begin() + j + 1, spectrum.at(i).end());
1099 }
1100 break;
1101 }
1102 }
1103 }
1104
1105 for (uint i = 0; i < spectrum_labels.size(); i++) {
1106 for (uint j = 0; j < spectrum_size; j++) {
1107 assert(new_spectrum.at(j).x == spectrum.at(i).at(j).x);
1108 new_spectrum.at(j).y += weights.at(i) * spectrum.at(i).at(j).y;
1109 }
1110 }
1111
1112 context->setGlobalData(new_spectrum_label.c_str(), new_spectrum);
1113}
1114
1115void RadiationModel::blendSpectraRandomly(const std::string &new_spectrum_label, const std::vector<std::string> &spectrum_labels) const {
1116
1117 std::vector<float> weights;
1118 weights.resize(spectrum_labels.size());
1119 for (uint i = 0; i < spectrum_labels.size(); i++) {
1120 weights.at(i) = context->randu();
1121 }
1122 float sum_weights = sum(weights);
1123 for (uint i = 0; i < spectrum_labels.size(); i++) {
1124 weights.at(i) /= sum_weights;
1125 }
1126
1127 blendSpectra(new_spectrum_label, spectrum_labels, weights);
1128}
1129
1130void RadiationModel::interpolateSpectrumFromPrimitiveData(const std::vector<uint> &primitive_UUIDs, const std::vector<std::string> &spectra, const std::vector<float> &values, const std::string &primitive_data_query_label,
1131 const std::string &primitive_data_radprop_label) {
1132
1133 // Validate that spectra and values have the same length
1134 if (spectra.size() != values.size()) {
1135 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromPrimitiveData): The 'spectra' vector (size=" + std::to_string(spectra.size()) + ") and 'values' vector (size=" + std::to_string(values.size()) +
1136 ") must have the same length.");
1137 }
1138
1139 // Validate that vectors are not empty
1140 if (spectra.empty()) {
1141 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromPrimitiveData): The 'spectra' and 'values' vectors cannot be empty.");
1142 }
1143
1144 // Validate that primitive_UUIDs is not empty
1145 if (primitive_UUIDs.empty()) {
1146 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromPrimitiveData): The 'primitive_UUIDs' vector cannot be empty.");
1147 }
1148
1149 // Validate that query and target data labels are not empty
1150 if (primitive_data_query_label.empty()) {
1151 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromPrimitiveData): The 'primitive_data_query_label' cannot be empty.");
1152 }
1153
1154 if (primitive_data_radprop_label.empty()) {
1155 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromPrimitiveData): The 'primitive_data_radprop_label' cannot be empty.");
1156 }
1157
1158 // Search for existing config with matching query and target labels
1159 SpectrumInterpolationConfig *existing_config = nullptr;
1160 for (auto &config: spectrum_interpolation_configs) {
1161 if (config.query_data_label == primitive_data_query_label && config.target_data_label == primitive_data_radprop_label) {
1162 existing_config = &config;
1163 break;
1164 }
1165 }
1166
1167 if (existing_config != nullptr) {
1168 // Check if spectra/values match the existing config
1169 bool spectra_match = (existing_config->spectra_labels == spectra && existing_config->mapping_values == values);
1170
1171 if (spectra_match) {
1172 // Merge UUIDs into existing config (unordered_set handles duplicates automatically)
1173 existing_config->primitive_UUIDs.insert(primitive_UUIDs.begin(), primitive_UUIDs.end());
1174 } else {
1175 // Replace entire config with new spectra/values and UUIDs
1176 existing_config->spectra_labels = spectra;
1177 existing_config->mapping_values = values;
1178 existing_config->primitive_UUIDs.clear();
1179 existing_config->primitive_UUIDs.insert(primitive_UUIDs.begin(), primitive_UUIDs.end());
1180 }
1181 } else {
1182 // Create new config
1183 SpectrumInterpolationConfig config;
1184 config.primitive_UUIDs.insert(primitive_UUIDs.begin(), primitive_UUIDs.end());
1185 config.spectra_labels = spectra;
1186 config.mapping_values = values;
1187 config.query_data_label = primitive_data_query_label;
1188 config.target_data_label = primitive_data_radprop_label;
1189
1190 spectrum_interpolation_configs.push_back(config);
1191 }
1192}
1193
1194void RadiationModel::interpolateSpectrumFromObjectData(const std::vector<uint> &object_IDs, const std::vector<std::string> &spectra, const std::vector<float> &values, const std::string &object_data_query_label,
1195 const std::string &primitive_data_radprop_label) {
1196
1197 // Validate that spectra and values have the same length
1198 if (spectra.size() != values.size()) {
1199 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromObjectData): The 'spectra' vector (size=" + std::to_string(spectra.size()) + ") and 'values' vector (size=" + std::to_string(values.size()) + ") must have the same length.");
1200 }
1201
1202 // Validate that vectors are not empty
1203 if (spectra.empty()) {
1204 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromObjectData): The 'spectra' and 'values' vectors cannot be empty.");
1205 }
1206
1207 // Validate that object_IDs is not empty
1208 if (object_IDs.empty()) {
1209 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromObjectData): The 'object_IDs' vector cannot be empty.");
1210 }
1211
1212 // Validate that query and target data labels are not empty
1213 if (object_data_query_label.empty()) {
1214 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromObjectData): The 'object_data_query_label' cannot be empty.");
1215 }
1216
1217 if (primitive_data_radprop_label.empty()) {
1218 helios_runtime_error("ERROR (RadiationModel::interpolateSpectrumFromObjectData): The 'primitive_data_radprop_label' cannot be empty.");
1219 }
1220
1221 // Search for existing config with matching query and target labels
1222 SpectrumInterpolationConfig *existing_config = nullptr;
1223 for (auto &config: spectrum_interpolation_configs) {
1224 if (config.query_data_label == object_data_query_label && config.target_data_label == primitive_data_radprop_label) {
1225 existing_config = &config;
1226 break;
1227 }
1228 }
1229
1230 if (existing_config != nullptr) {
1231 // Check if spectra/values match the existing config
1232 bool spectra_match = (existing_config->spectra_labels == spectra && existing_config->mapping_values == values);
1233
1234 if (spectra_match) {
1235 // Merge object IDs into existing config (unordered_set handles duplicates automatically)
1236 existing_config->object_IDs.insert(object_IDs.begin(), object_IDs.end());
1237 } else {
1238 // Replace entire config with new spectra/values and object IDs
1239 existing_config->spectra_labels = spectra;
1240 existing_config->mapping_values = values;
1241 existing_config->object_IDs.clear();
1242 existing_config->object_IDs.insert(object_IDs.begin(), object_IDs.end());
1243 }
1244 } else {
1245 // Create new config
1246 SpectrumInterpolationConfig config;
1247 config.object_IDs.insert(object_IDs.begin(), object_IDs.end());
1248 config.spectra_labels = spectra;
1249 config.mapping_values = values;
1250 config.query_data_label = object_data_query_label;
1251 config.target_data_label = primitive_data_radprop_label;
1252
1253 spectrum_interpolation_configs.push_back(config);
1254 }
1255}
1256
1257void RadiationModel::setSourcePosition(uint source_ID, const vec3 &position) {
1258
1259 if (source_ID >= radiation_sources.size()) {
1260 helios_runtime_error("ERROR (RadiationModel::setSourcePosition): Source ID out of bounds. Only " + std::to_string(radiation_sources.size() - 1) + " radiation sources.");
1261 }
1262
1263 vec3 old_position = radiation_sources.at(source_ID).source_position;
1264
1265 if (radiation_sources.at(source_ID).source_type == RADIATION_SOURCE_TYPE_COLLIMATED) {
1266 radiation_sources.at(source_ID).source_position = position / position.magnitude();
1267 } else {
1268 radiation_sources.at(source_ID).source_position = position * radiation_sources.at(source_ID).source_position_scaling_factor;
1269 }
1270
1271 if (islightvisualizationenabled) {
1272 updateLightModelPosition(source_ID, radiation_sources.at(source_ID).source_position - old_position);
1273 }
1274}
1275
1277 setSourcePosition(source_ID, sphere2cart(position));
1278}
1279
1281 if (source_ID >= radiation_sources.size()) {
1282 helios_runtime_error("ERROR (RadiationModel::getSourcePosition): Source ID does not exist.");
1283 }
1284 return radiation_sources.at(source_ID).source_position;
1285}
1286
1287void RadiationModel::setScatteringDepth(const std::string &label, uint depth) {
1288
1289 if (!doesBandExist(label)) {
1290 helios_runtime_error("ERROR (RadiationModel::setScatteringDepth): Cannot set scattering depth for band '" + label + "' because it is not a valid band.");
1291 }
1292 radiation_bands.at(label).scatteringDepth = depth;
1293}
1294
1295void RadiationModel::setMinScatterEnergy(const std::string &label, uint energy) {
1296
1297 if (!doesBandExist(label)) {
1298 helios_runtime_error("ERROR (setMinScatterEnergy): Cannot set minimum scattering energy for band '" + label + "' because it is not a valid band.");
1299 }
1300 radiation_bands.at(label).minScatterEnergy = energy;
1301}
1302
1303void RadiationModel::enforcePeriodicBoundary(const std::string &boundary) {
1304
1305 if (boundary == "x") {
1306
1307 periodic_flag.x = 1;
1308
1309 } else if (boundary == "y") {
1310
1311 periodic_flag.y = 1;
1312
1313 } else if (boundary == "xy") {
1314
1315 periodic_flag.x = 1;
1316 periodic_flag.y = 1;
1317
1318 } else {
1319
1320 std::cout << "WARNING (RadiationModel::enforcePeriodicBoundary()): unknown boundary of '" << boundary << "'. Possible choices are x, y, or xy." << std::endl;
1321 }
1322}
1323
1327
1328
1329void RadiationModel::updateGeometry(const std::vector<uint> &UUIDs) {
1330
1331 if (message_flag) {
1332 std::cout << "Updating geometry in radiation transport model..." << std::flush;
1333 }
1334
1335 // Upload geometry through backend abstraction layer
1336 buildGeometryData();
1337 buildUUIDMapping(); // Build UUID↔position mapping for efficient indexing
1338
1339 // CRITICAL: context_UUIDs must match GPU buffer ordering (primitive_UUIDs_ordered)
1340 // Emission data is indexed by position, which corresponds to primitive_UUIDs order
1341 context_UUIDs = geometry_data.primitive_UUIDs;
1342
1343 backend->updateGeometry(geometry_data);
1344 backend->buildAccelerationStructure();
1345
1346 radiativepropertiesneedupdate = true;
1347 isgeometryinitialized = true;
1348
1349 if (message_flag) {
1350 std::cout << "done." << std::endl;
1351 }
1352}
1353
1354void RadiationModel::updateRadiativeProperties() {
1355
1356 // Possible scenarios for specifying a primitive's radiative properties
1357 // 1. If primitive data of form reflectivity_band/transmissivity_band is given, this value is used and overrides any other option.
1358 // 2. If primitive data of form reflectivity_spectrum/transmissivity_spectrum is given that references global data containing spectral reflectivity/transmissivity:
1359 // 2a. If radiation source spectrum was not given, assume source spectral intensity is constant over band and calculate using primitive spectrum
1360 // 2b. If radiation source spectrum was given, calculate using both source and primitive spectrum.
1361
1362 // Create warning aggregator
1364 warnings.setEnabled(message_flag);
1365
1366 if (message_flag) {
1367 std::cout << "Updating radiative properties..." << std::flush;
1368 }
1369
1370 uint Nbands = radiation_bands.size(); // number of radiative bands
1371 uint Nsources = radiation_sources.size();
1372 uint Ncameras = cameras.size();
1373 size_t Nobjects = primitiveID.size();
1374 size_t Nprimitives = context_UUIDs.size();
1375
1376 scattering_iterations_needed.clear();
1377 for (auto &band: radiation_bands) {
1378 scattering_iterations_needed[band.first] = false;
1379 }
1380
1381 std::vector<std::vector<std::vector<float>>> rho, tau; // first index is the source, second index is the primitive, third index is the band
1382 std::vector<std::vector<std::vector<std::vector<float>>>> rho_cam, tau_cam; // Fourth index is the camera
1383 float eps;
1384
1385 std::string prop;
1386 std::vector<std::string> band_labels;
1387 for (auto &band: radiation_bands) {
1388 band_labels.push_back(band.first);
1389 }
1390
1391 rho.resize(Nsources);
1392 tau.resize(Nsources);
1393 for (size_t s = 0; s < Nsources; s++) {
1394 rho.at(s).resize(Nprimitives);
1395 tau.at(s).resize(Nprimitives);
1396 for (size_t p = 0; p < Nprimitives; p++) {
1397 rho.at(s).at(p).resize(Nbands);
1398 tau.at(s).at(p).resize(Nbands);
1399 }
1400 }
1401 if (Ncameras) {
1402 rho_cam.resize(Nsources);
1403 tau_cam.resize(Nsources);
1404 for (size_t s = 0; s < Nsources; s++) {
1405 rho_cam.at(s).resize(Nprimitives);
1406 tau_cam.at(s).resize(Nprimitives);
1407 for (size_t p = 0; p < Nprimitives; p++) {
1408 rho_cam.at(s).at(p).resize(Nbands);
1409 tau_cam.at(s).at(p).resize(Nbands);
1410 for (size_t b = 0; b < Nbands; b++) {
1411 rho_cam.at(s).at(p).at(b).resize(Ncameras);
1412 tau_cam.at(s).at(p).at(b).resize(Ncameras);
1413 }
1414 }
1415 }
1416 }
1417
1418 // Cache all unique camera spectral responses for all cameras and bands
1419 std::vector<std::vector<std::vector<helios::vec2>>> camera_response_unique;
1420 camera_response_unique.resize(Ncameras);
1421 if (Ncameras > 0) {
1422 uint cam = 0;
1423 for (const auto &camera: cameras) {
1424
1425 camera_response_unique.at(cam).resize(Nbands);
1426
1427 for (uint b = 0; b < Nbands; b++) {
1428
1429 if (camera.second.band_spectral_response.find(band_labels.at(b)) == camera.second.band_spectral_response.end()) {
1430 continue;
1431 }
1432
1433 std::string camera_response = camera.second.band_spectral_response.at(band_labels.at(b));
1434
1435 if (!camera_response.empty()) {
1436
1437 if (!context->doesGlobalDataExist(camera_response.c_str())) {
1438 if (camera_response != "uniform") {
1439 warnings.addWarning("missing_camera_response", "Camera spectral response \"" + camera_response + "\" does not exist. Assuming a uniform spectral response.");
1440 }
1441 } else if (context->getGlobalDataType(camera_response.c_str()) == helios::HELIOS_TYPE_VEC2) {
1442
1443 std::vector<helios::vec2> data = loadSpectralData(camera_response.c_str());
1444
1445 camera_response_unique.at(cam).at(b) = data;
1446
1447 } else if (context->getGlobalDataType(camera_response.c_str()) != helios::HELIOS_TYPE_VEC2 && context->getGlobalDataType(camera_response.c_str()) != helios::HELIOS_TYPE_STRING) {
1448 camera_response.clear();
1449 std::cout << "WARNING (RadiationModel::runBand): Camera spectral response \"" << camera_response << "\" is not of type HELIOS_TYPE_VEC2 or HELIOS_TYPE_STRING. Assuming a uniform spectral response..." << std::endl;
1450 }
1451 }
1452 }
1453 cam++;
1454 }
1455 }
1456
1457 // Spectral integration cache to avoid redundant computations
1458 std::unordered_map<std::string, float> spectral_integration_cache;
1459
1460#ifdef USE_OPENMP
1461 // Temporary cache for this thread group (will be merged later)
1462 std::unordered_map<std::string, float> temp_spectral_cache;
1463#endif
1464
1465 // Helper function to create cache keys for spectral integrations
1466 auto createCacheKey = [](const std::string &spectrum_label, uint source_id, uint band_id, uint camera_id, const std::string &type) -> std::string {
1467 return spectrum_label + "_" + std::to_string(source_id) + "_" + std::to_string(band_id) + "_" + std::to_string(camera_id) + "_" + type;
1468 };
1469
1470 // Helper function to get from cache (thread-safe)
1471 auto getCachedValue = [&](const std::string &cache_key, bool &found) -> float {
1472 float result = 0.0f;
1473 found = false;
1474
1475#ifdef USE_OPENMP
1476#pragma omp critical
1477 {
1478#endif
1479 // Check shared cache
1480 auto cache_it = spectral_integration_cache.find(cache_key);
1481 if (cache_it != spectral_integration_cache.end()) {
1482 found = true;
1483 result = cache_it->second;
1484 }
1485#ifdef USE_OPENMP
1486 }
1487#endif
1488 return result;
1489 };
1490
1491 // Helper function to store in cache (thread-safe)
1492 auto setCachedValue = [&](const std::string &cache_key, float value) {
1493#ifdef USE_OPENMP
1494#pragma omp critical
1495 {
1496#endif
1497 spectral_integration_cache[cache_key] = value;
1498#ifdef USE_OPENMP
1499 }
1500#endif
1501 };
1502
1503 // Helper function for cached interpolation (thread-safe)
1504 auto cachedInterp1 = [&](const std::vector<helios::vec2> &spectrum, float wavelength, const std::string &spectrum_id) -> float {
1505 // Create cache key for this specific interpolation
1506 std::string cache_key = "interp_" + spectrum_id + "_" + std::to_string(wavelength);
1507
1508 bool found = false;
1509 float cached_result = getCachedValue(cache_key, found);
1510 if (found) {
1511 return cached_result;
1512 }
1513
1514 // Perform interpolation and cache result
1515 float result = interp1(spectrum, wavelength);
1516 setCachedValue(cache_key, result);
1517 return result;
1518 };
1519
1520 // Cached version of integrateSpectrum with source spectrum
1521 auto cachedIntegrateSpectrumWithSource = [&](uint source_ID, const std::vector<helios::vec2> &object_spectrum, float wavelength1, float wavelength2, const std::string &object_spectrum_id) -> float {
1522 if (source_ID >= radiation_sources.size() || object_spectrum.size() < 2 || wavelength1 >= wavelength2) {
1523 return 0.0f; // Handle edge cases gracefully
1524 }
1525
1526 std::vector<helios::vec2> source_spectrum = radiation_sources.at(source_ID).source_spectrum;
1527 std::string source_id = "source_" + std::to_string(source_ID);
1528
1529 int istart = 0;
1530 int iend = (int) object_spectrum.size() - 1;
1531 for (auto i = 0; i < object_spectrum.size() - 1; i++) {
1532 if (object_spectrum.at(i).x <= wavelength1 && object_spectrum.at(i + 1).x > wavelength1) {
1533 istart = i;
1534 }
1535 if (object_spectrum.at(i).x <= wavelength2 && object_spectrum.at(i + 1).x > wavelength2) {
1536 iend = i + 1;
1537 break;
1538 }
1539 }
1540
1541 float E = 0;
1542 float Etot = 0;
1543 for (auto i = istart; i < iend; i++) {
1544 float x0 = object_spectrum.at(i).x;
1545 float Esource0 = cachedInterp1(source_spectrum, x0, source_id);
1546 float Eobject0 = object_spectrum.at(i).y;
1547
1548 float x1 = object_spectrum.at(i + 1).x;
1549 float Eobject1 = object_spectrum.at(i + 1).y;
1550 float Esource1 = cachedInterp1(source_spectrum, x1, source_id);
1551
1552 E += 0.5f * (Eobject0 * Esource0 + Eobject1 * Esource1) * (x1 - x0);
1553 Etot += 0.5f * (Esource1 + Esource0) * (x1 - x0);
1554 }
1555
1556 return (Etot != 0.0f) ? E / Etot : 0.0f;
1557 };
1558
1559 // Cached version of integrateSpectrum with source and camera spectra
1560 auto cachedIntegrateSpectrumWithSourceAndCamera = [&](uint source_ID, const std::vector<helios::vec2> &object_spectrum, const std::vector<helios::vec2> &camera_spectrum, uint camera_index, uint band_index,
1561 const std::string &object_spectrum_id) -> float {
1562 if (source_ID >= radiation_sources.size() || object_spectrum.size() < 2) {
1563 return 0.0f;
1564 }
1565
1566 std::vector<helios::vec2> source_spectrum = radiation_sources.at(source_ID).source_spectrum;
1567 std::string source_id = "source_" + std::to_string(source_ID);
1568 std::string camera_id = "camera_" + std::to_string(camera_index) + "_band_" + std::to_string(band_index); // Include band for unique cache key per band
1569
1570 float E = 0;
1571 float Etot = 0;
1572 for (auto i = 1; i < object_spectrum.size(); i++) {
1573 if (object_spectrum.at(i).x <= source_spectrum.front().x || object_spectrum.at(i).x <= camera_spectrum.front().x) {
1574 continue;
1575 }
1576 if (object_spectrum.at(i).x > source_spectrum.back().x || object_spectrum.at(i).x > camera_spectrum.back().x) {
1577 break;
1578 }
1579
1580 float x1 = object_spectrum.at(i).x;
1581 float Eobject1 = object_spectrum.at(i).y;
1582 float Esource1 = cachedInterp1(source_spectrum, x1, source_id);
1583 float Ecamera1 = cachedInterp1(camera_spectrum, x1, camera_id);
1584
1585 float x0 = object_spectrum.at(i - 1).x;
1586 float Eobject0 = object_spectrum.at(i - 1).y;
1587 float Esource0 = cachedInterp1(source_spectrum, x0, source_id);
1588 float Ecamera0 = cachedInterp1(camera_spectrum, x0, camera_id);
1589
1590 E += 0.5f * ((Eobject1 * Esource1 * Ecamera1) + (Eobject0 * Ecamera0 * Esource0)) * (x1 - x0);
1591 Etot += 0.5f * (Esource1 + Esource0) * (x1 - x0);
1592 }
1593
1594 return (Etot != 0.0f) ? E / Etot : 0.0f;
1595 };
1596
1597 // Apply spectral interpolation based on primitive data values
1598 for (const auto &config: spectrum_interpolation_configs) {
1599 // Validate that all spectra in this config exist in global data and have correct type
1600 for (const auto &spectrum_label: config.spectra_labels) {
1601 if (!context->doesGlobalDataExist(spectrum_label.c_str())) {
1602 helios_runtime_error("ERROR (RadiationModel::updateRadiativeProperties): Spectral interpolation config references global data '" + spectrum_label + "' which does not exist.");
1603 }
1604 if (context->getGlobalDataType(spectrum_label.c_str()) != helios::HELIOS_TYPE_VEC2) {
1605 helios_runtime_error("ERROR (RadiationModel::updateRadiativeProperties): Spectral interpolation config references global data '" + spectrum_label + "' which must be of type HELIOS_TYPE_VEC2 (std::vector<helios::vec2>).");
1606 }
1607 }
1608
1609 for (uint uuid: config.primitive_UUIDs) {
1610 // Check if primitive still exists in context (it may have been deleted)
1611 if (!context->doesPrimitiveExist(uuid)) {
1612 continue;
1613 }
1614
1615 // Check if the query data exists for this primitive and has correct type
1616 if (context->doesPrimitiveDataExist(uuid, config.query_data_label.c_str())) {
1617 // Check that query data is of type float
1618 if (context->getPrimitiveDataType(config.query_data_label.c_str()) != helios::HELIOS_TYPE_FLOAT) {
1619 helios_runtime_error("ERROR (RadiationModel::updateRadiativeProperties): Primitive data '" + config.query_data_label + "' for UUID " + std::to_string(uuid) + " must be of type HELIOS_TYPE_FLOAT for spectral interpolation.");
1620 }
1621
1622 // Get the query value
1623 float query_value;
1624 context->getPrimitiveData(uuid, config.query_data_label.c_str(), query_value);
1625
1626 // Perform nearest-neighbor interpolation
1627 size_t nearest_idx = 0;
1628 float min_distance = std::abs(query_value - config.mapping_values[0]);
1629 for (size_t i = 1; i < config.mapping_values.size(); i++) {
1630 float distance = std::abs(query_value - config.mapping_values[i]);
1631 if (distance < min_distance) {
1632 min_distance = distance;
1633 nearest_idx = i;
1634 }
1635 }
1636
1637 // Set the target primitive data to the selected spectrum label
1638 context->setPrimitiveData(uuid, config.target_data_label.c_str(), config.spectra_labels[nearest_idx]);
1639 }
1640 }
1641
1642 // Apply spectral interpolation based on object data values
1643 for (uint objID: config.object_IDs) {
1644 // Check if object still exists in context (it may have been deleted)
1645 if (!context->doesObjectExist(objID)) {
1646 continue;
1647 }
1648
1649 // Check if the query data exists for this object and has correct type
1650 if (context->doesObjectDataExist(objID, config.query_data_label.c_str())) {
1651 // Check that query data is of type float
1652 if (context->getObjectDataType(config.query_data_label.c_str()) != helios::HELIOS_TYPE_FLOAT) {
1653 helios_runtime_error("ERROR (RadiationModel::updateRadiativeProperties): Object data '" + config.query_data_label + "' for object ID " + std::to_string(objID) + " must be of type HELIOS_TYPE_FLOAT for spectral interpolation.");
1654 }
1655
1656 // Get the query value
1657 float query_value;
1658 context->getObjectData(objID, config.query_data_label.c_str(), query_value);
1659
1660 // Perform nearest-neighbor interpolation
1661 size_t nearest_idx = 0;
1662 float min_distance = std::abs(query_value - config.mapping_values.at(0));
1663 for (size_t i = 1; i < config.mapping_values.size(); i++) {
1664 float distance = std::abs(query_value - config.mapping_values.at(i));
1665 if (distance < min_distance) {
1666 min_distance = distance;
1667 nearest_idx = i;
1668 }
1669 }
1670
1671 // Get object's primitive UUIDs and set their primitive data using vector overload
1672 std::vector<uint> prim_uuids = context->getObjectPrimitiveUUIDs(objID);
1673 context->setPrimitiveData(prim_uuids, config.target_data_label.c_str(), config.spectra_labels.at(nearest_idx));
1674 }
1675 }
1676 }
1677
1678 // Cache all unique primitive reflectivity and transmissivity spectra before assigning to primitives
1679
1680 // first, figure out all of the spectra referenced by all primitives and store it in "surface_spectra" to avoid having to load it again
1681 std::map<std::string, std::vector<helios::vec2>> surface_spectra_rho;
1682 std::map<std::string, std::vector<helios::vec2>> surface_spectra_tau;
1683 for (size_t u = 0; u < Nprimitives; u++) {
1684
1685 uint UUID = context_UUIDs.at(u);
1686
1687 if (context->doesPrimitiveDataExist(UUID, "reflectivity_spectrum")) {
1688 if (context->getPrimitiveDataType("reflectivity_spectrum") == HELIOS_TYPE_STRING) {
1689 std::string spectrum_label;
1690 context->getPrimitiveData(UUID, "reflectivity_spectrum", spectrum_label);
1691
1692 // get the spectral reflectivity data and store it in surface_spectra to avoid having to load it again
1693 if (surface_spectra_rho.find(spectrum_label) == surface_spectra_rho.end()) {
1694 if (!context->doesGlobalDataExist(spectrum_label.c_str())) {
1695 if (!spectrum_label.empty()) {
1696 warnings.addWarning("missing_reflectivity_spectrum", "Primitive spectral reflectivity \"" + spectrum_label + "\" does not exist. Using default reflectivity of 0.");
1697 }
1698 std::vector<helios::vec2> data;
1699 surface_spectra_rho.emplace(spectrum_label, data);
1700 } else if (context->getGlobalDataType(spectrum_label.c_str()) == HELIOS_TYPE_VEC2) {
1701
1702 std::vector<helios::vec2> data = loadSpectralData(spectrum_label.c_str());
1703 surface_spectra_rho.emplace(spectrum_label, data);
1704
1705 } else if (context->getGlobalDataType(spectrum_label.c_str()) != helios::HELIOS_TYPE_VEC2 && context->getGlobalDataType(spectrum_label.c_str()) != helios::HELIOS_TYPE_STRING) {
1706 spectrum_label.clear();
1707 std::cout << "WARNING (RadiationModel::runBand): Object spectral reflectivity \"" << spectrum_label << "\" is not of type HELIOS_TYPE_VEC2 or HELIOS_TYPE_STRING. Assuming a uniform spectral distribution..." << std::flush;
1708 }
1709 }
1710 }
1711 }
1712
1713 if (context->doesPrimitiveDataExist(UUID, "transmissivity_spectrum")) {
1714 if (context->getPrimitiveDataType("transmissivity_spectrum") == HELIOS_TYPE_STRING) {
1715 std::string spectrum_label;
1716 context->getPrimitiveData(UUID, "transmissivity_spectrum", spectrum_label);
1717
1718 // get the spectral transmissivity data and store it in surface_spectra to avoid having to load it again
1719 if (surface_spectra_tau.find(spectrum_label) == surface_spectra_tau.end()) {
1720 if (!context->doesGlobalDataExist(spectrum_label.c_str())) {
1721 if (!spectrum_label.empty()) {
1722 warnings.addWarning("missing_transmissivity_spectrum", "Primitive spectral transmissivity \"" + spectrum_label + "\" does not exist. Using default transmissivity of 0.");
1723 }
1724 std::vector<helios::vec2> data;
1725 surface_spectra_tau.emplace(spectrum_label, data);
1726 } else if (context->getGlobalDataType(spectrum_label.c_str()) == HELIOS_TYPE_VEC2) {
1727
1728 std::vector<helios::vec2> data = loadSpectralData(spectrum_label.c_str());
1729 surface_spectra_tau.emplace(spectrum_label, data);
1730
1731 } else if (context->getGlobalDataType(spectrum_label.c_str()) != helios::HELIOS_TYPE_VEC2 && context->getGlobalDataType(spectrum_label.c_str()) != helios::HELIOS_TYPE_STRING) {
1732 spectrum_label.clear();
1733 std::cout << "WARNING (RadiationModel::runBand): Object spectral transmissivity \"" << spectrum_label << "\" is not of type HELIOS_TYPE_VEC2 or HELIOS_TYPE_STRING. Assuming a uniform spectral distribution..." << std::flush;
1734 }
1735 }
1736 }
1737 }
1738 }
1739
1740 // second, calculate unique values of rho and tau for all sources and bands
1741 std::map<std::string, std::vector<std::vector<float>>> rho_unique;
1742 std::map<std::string, std::vector<std::vector<float>>> tau_unique;
1743
1744 std::map<std::string, std::vector<std::vector<std::vector<float>>>> rho_cam_unique;
1745 std::map<std::string, std::vector<std::vector<std::vector<float>>>> tau_cam_unique;
1746
1747 std::vector<std::vector<float>> empty;
1748 empty.resize(Nbands);
1749 for (uint b = 0; b < Nbands; b++) {
1750 empty.at(b).resize(Nsources, 0);
1751 }
1752 std::vector<std::vector<std::vector<float>>> empty_cam;
1753 if (Ncameras > 0) {
1754 empty_cam.resize(Nbands);
1755 for (uint b = 0; b < Nbands; b++) {
1756 empty_cam.at(b).resize(Nsources);
1757 for (uint s = 0; s < Nsources; s++) {
1758 empty_cam.at(b).at(s).resize(Ncameras, 0);
1759 }
1760 }
1761 }
1762
1763 // Convert maps to vectors for OpenMP indexing
1764 std::vector<std::pair<std::string, std::vector<helios::vec2>>> spectra_rho_vector(surface_spectra_rho.begin(), surface_spectra_rho.end());
1765
1766 // Pre-initialize all map entries before parallel processing to avoid race conditions
1767 for (const auto &spectrum: spectra_rho_vector) {
1768 rho_unique[spectrum.first] = empty;
1769 if (Ncameras > 0) {
1770 rho_cam_unique[spectrum.first] = empty_cam;
1771 }
1772 }
1773
1774 // Process reflectivity spectra with OpenMP parallelization
1775#ifdef USE_OPENMP
1776#pragma omp parallel for schedule(dynamic)
1777#endif
1778 for (int spectrum_idx = 0; spectrum_idx < (int) spectra_rho_vector.size(); spectrum_idx++) {
1779 const auto &spectrum = spectra_rho_vector[spectrum_idx];
1780
1781 for (uint b = 0; b < Nbands; b++) {
1782 std::string band = band_labels.at(b);
1783
1784 for (uint s = 0; s < Nsources; s++) {
1785
1786 // integrate with caching
1787 auto band_it = radiation_bands.find(band);
1788 if (band_it != radiation_bands.end() && band_it->second.wavebandBounds.x != 0 && band_it->second.wavebandBounds.y != 0 && !spectrum.second.empty()) {
1789 if (!radiation_sources.at(s).source_spectrum.empty()) {
1790 std::string cache_key = createCacheKey(spectrum.first, s, b, 0, "rho_source");
1791 bool found;
1792 float cached_result = getCachedValue(cache_key, found);
1793 if (found) {
1794 rho_unique[spectrum.first][b][s] = cached_result;
1795 } else {
1796 float result = cachedIntegrateSpectrumWithSource(s, spectrum.second, band_it->second.wavebandBounds.x, band_it->second.wavebandBounds.y, spectrum.first);
1797 setCachedValue(cache_key, result);
1798 rho_unique[spectrum.first][b][s] = result;
1799 }
1800 } else {
1801 // source spectrum not provided, assume source intensity is constant over the band
1802 std::string cache_key = createCacheKey(spectrum.first, s, b, 0, "rho_no_source");
1803 bool found;
1804 float cached_result = getCachedValue(cache_key, found);
1805 if (found) {
1806 rho_unique[spectrum.first][b][s] = cached_result;
1807 } else {
1808 float result = integrateSpectrum(spectrum.second, band_it->second.wavebandBounds.x, band_it->second.wavebandBounds.y) / (band_it->second.wavebandBounds.y - band_it->second.wavebandBounds.x);
1809 setCachedValue(cache_key, result);
1810 rho_unique[spectrum.first][b][s] = result;
1811 }
1812 }
1813 } else {
1814 // No wavelength bounds, can't integrate spectrum without camera response
1815 // Set to default for now, will use camera average if available
1816 rho_unique[spectrum.first][b][s] = rho_default;
1817 }
1818
1819 // cameras
1820 if (Ncameras > 0) {
1821 uint cam = 0;
1822 float rho_cam_sum_for_averaging = 0.f;
1823 for (const auto &camera: cameras) {
1824
1825 if (camera_response_unique.at(cam).at(b).empty()) {
1826 rho_cam_unique[spectrum.first][b][s][cam] = rho_unique[spectrum.first][b][s];
1827 } else {
1828
1829 // integrate with caching
1830 if (!spectrum.second.empty()) {
1831 if (!radiation_sources.at(s).source_spectrum.empty()) {
1832 std::string cache_key = createCacheKey(spectrum.first, s, b, cam, "rho_cam_source");
1833 bool found;
1834 float cached_result = getCachedValue(cache_key, found);
1835 if (found) {
1836 rho_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = cached_result;
1837 rho_cam_sum_for_averaging += cached_result;
1838 } else {
1839 float result = cachedIntegrateSpectrumWithSourceAndCamera(s, spectrum.second, camera_response_unique.at(cam).at(b), cam, b, spectrum.first);
1840 setCachedValue(cache_key, result);
1841 rho_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = result;
1842 rho_cam_sum_for_averaging += result;
1843 }
1844 } else {
1845 std::string cache_key = createCacheKey(spectrum.first, s, b, cam, "rho_cam_no_source");
1846 bool found;
1847 float cached_result = getCachedValue(cache_key, found);
1848 if (found) {
1849 rho_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = cached_result;
1850 rho_cam_sum_for_averaging += cached_result;
1851 } else {
1852 float result = integrateSpectrum(spectrum.second, camera_response_unique.at(cam).at(b));
1853 setCachedValue(cache_key, result);
1854 rho_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = result;
1855 rho_cam_sum_for_averaging += result;
1856 }
1857 }
1858 } else {
1859 rho_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = rho_default;
1860 }
1861 }
1862
1863 cam++;
1864 }
1865
1866 // CRITICAL FIX: If wavelength bounds weren't set but camera integration produced values,
1867 // use camera average as the base reflectivity. This allows regular scatter to work
1868 // when only reflectivity_spectrum + camera response are provided.
1869 if (rho_unique[spectrum.first][b][s] == rho_default && rho_cam_sum_for_averaging > 0 && cam > 0) {
1870 rho_unique[spectrum.first][b][s] = rho_cam_sum_for_averaging / float(cam);
1871 }
1872 }
1873 }
1874 }
1875 }
1876
1877 // Convert tau spectra to vector for OpenMP indexing
1878 std::vector<std::pair<std::string, std::vector<helios::vec2>>> spectra_tau_vector(surface_spectra_tau.begin(), surface_spectra_tau.end());
1879
1880 // Pre-initialize all map entries before parallel processing to avoid race conditions
1881 for (const auto &spectrum: spectra_tau_vector) {
1882 tau_unique[spectrum.first] = empty;
1883 if (Ncameras > 0) {
1884 tau_cam_unique[spectrum.first] = empty_cam;
1885 }
1886 }
1887
1888 // Process transmissivity spectra with OpenMP parallelization
1889#ifdef USE_OPENMP
1890#pragma omp parallel for schedule(dynamic)
1891#endif
1892 for (int spectrum_idx = 0; spectrum_idx < (int) spectra_tau_vector.size(); spectrum_idx++) {
1893 const auto &spectrum = spectra_tau_vector[spectrum_idx];
1894
1895 for (uint b = 0; b < Nbands; b++) {
1896 std::string band = band_labels.at(b);
1897
1898 for (uint s = 0; s < Nsources; s++) {
1899
1900 // integrate with caching
1901 auto band_it = radiation_bands.find(band);
1902 if (band_it != radiation_bands.end() && band_it->second.wavebandBounds.x != 0 && band_it->second.wavebandBounds.y != 0 && !spectrum.second.empty()) {
1903 if (!radiation_sources.at(s).source_spectrum.empty()) {
1904 std::string cache_key = createCacheKey(spectrum.first, s, b, 0, "tau_source");
1905 bool found;
1906 float cached_result = getCachedValue(cache_key, found);
1907 if (found) {
1908 tau_unique[spectrum.first][b][s] = cached_result;
1909 } else {
1910 float result = cachedIntegrateSpectrumWithSource(s, spectrum.second, band_it->second.wavebandBounds.x, band_it->second.wavebandBounds.y, spectrum.first);
1911 setCachedValue(cache_key, result);
1912 tau_unique[spectrum.first][b][s] = result;
1913 }
1914 } else {
1915 std::string cache_key = createCacheKey(spectrum.first, s, b, 0, "tau_no_source");
1916 bool found;
1917 float cached_result = getCachedValue(cache_key, found);
1918 if (found) {
1919 tau_unique[spectrum.first][b][s] = cached_result;
1920 } else {
1921 float result = integrateSpectrum(spectrum.second, band_it->second.wavebandBounds.x, band_it->second.wavebandBounds.y) / (band_it->second.wavebandBounds.y - band_it->second.wavebandBounds.x);
1922 setCachedValue(cache_key, result);
1923 tau_unique[spectrum.first][b][s] = result;
1924 }
1925 }
1926 } else {
1927 tau_unique[spectrum.first][b][s] = tau_default;
1928 }
1929
1930 // cameras
1931 if (Ncameras > 0) {
1932 uint cam = 0;
1933 for (const auto &camera: cameras) {
1934
1935 if (camera_response_unique.at(cam).at(b).empty()) {
1936
1937 tau_cam_unique[spectrum.first][b][s][cam] = tau_unique[spectrum.first][b][s];
1938
1939 } else {
1940
1941 // integrate with caching
1942 if (!spectrum.second.empty()) {
1943 if (!radiation_sources.at(s).source_spectrum.empty()) {
1944 std::string cache_key = createCacheKey(spectrum.first, s, b, cam, "tau_cam_source");
1945 bool found;
1946 float cached_result = getCachedValue(cache_key, found);
1947 if (found) {
1948 tau_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = cached_result;
1949 } else {
1950 float result = cachedIntegrateSpectrumWithSourceAndCamera(s, spectrum.second, camera_response_unique.at(cam).at(b), cam, b, spectrum.first);
1951 setCachedValue(cache_key, result);
1952 tau_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = result;
1953 }
1954 } else {
1955 std::string cache_key = createCacheKey(spectrum.first, s, b, cam, "tau_cam_no_source");
1956 bool found;
1957 float cached_result = getCachedValue(cache_key, found);
1958 if (found) {
1959 tau_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = cached_result;
1960 } else {
1961 float result = integrateSpectrum(spectrum.second, camera_response_unique.at(cam).at(b));
1962 setCachedValue(cache_key, result);
1963 tau_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = result;
1964 }
1965 }
1966 } else {
1967 tau_cam_unique.at(spectrum.first).at(b).at(s).at(cam) = tau_default;
1968 }
1969 }
1970
1971 cam++;
1972 }
1973 }
1974 }
1975 }
1976 }
1977
1978 for (size_t u = 0; u < Nprimitives; u++) {
1979
1980 uint UUID = context_UUIDs.at(u);
1981
1982 helios::PrimitiveType type = context->getPrimitiveType(UUID);
1983
1984 if (type == helios::PRIMITIVE_TYPE_VOXEL) {
1985
1986 } else { // other than voxels
1987
1988 // Reflectivity
1989
1990 // check for primitive data of form "reflectivity_spectrum" that can be used to calculate reflectivity
1991 std::string spectrum_label;
1992 if (context->doesPrimitiveDataExist(UUID, "reflectivity_spectrum")) {
1993 if (context->getPrimitiveDataType("reflectivity_spectrum") == HELIOS_TYPE_STRING) {
1994 context->getPrimitiveData(UUID, "reflectivity_spectrum", spectrum_label);
1995 }
1996 }
1997
1998 uint b = 0;
1999 for (const auto &band: band_labels) {
2000
2001 // check for primitive data of form "reflectivity_bandname"
2002 prop = "reflectivity_" + band;
2003
2004 float rho_s = rho_default;
2005 if (context->doesPrimitiveDataExist(UUID, prop.c_str())) {
2006 context->getPrimitiveData(UUID, prop.c_str(), rho_s);
2007 }
2008
2009 for (uint s = 0; s < Nsources; s++) {
2010 // if reflectivity was manually set, or a spectrum was given and the global data exists
2011 if (rho_s != rho_default || spectrum_label.empty() || !context->doesGlobalDataExist(spectrum_label.c_str()) || rho_unique.find(spectrum_label) == rho_unique.end()) {
2012
2013 rho.at(s).at(u).at(b) = rho_s;
2014
2015 // cameras
2016 for (uint cam = 0; cam < Ncameras; cam++) {
2017 rho_cam.at(s).at(u).at(b).at(cam) = rho_s;
2018 }
2019
2020 // use spectrum
2021 } else {
2022
2023 rho.at(s).at(u).at(b) = rho_unique.at(spectrum_label).at(b).at(s);
2024
2025 // cameras
2026 for (uint cam = 0; cam < Ncameras; cam++) {
2027 rho_cam.at(s).at(u).at(b).at(cam) = rho_cam_unique.at(spectrum_label).at(b).at(s).at(cam);
2028 }
2029 }
2030
2031 // error checking
2032 if (rho.at(s).at(u).at(b) < 0) {
2033 rho.at(s).at(u).at(b) = 0.f;
2034 warnings.addWarning("reflectivity_negative_clamped", "Reflectivity cannot be less than 0. Clamping to 0 for band " + band + ".");
2035 } else if (rho.at(s).at(u).at(b) > 1.f) {
2036 rho.at(s).at(u).at(b) = 1.f;
2037 warnings.addWarning("reflectivity_exceeded_clamped", "Reflectivity cannot be greater than 1. Clamping to 1 for band " + band + ".");
2038 }
2039 if (rho.at(s).at(u).at(b) != 0) {
2040 scattering_iterations_needed.at(band) = true;
2041 }
2042 for (auto &odata: output_prim_data) {
2043 if (odata == "reflectivity") {
2044 context->setPrimitiveData(UUID, ("reflectivity_" + std::to_string(s) + "_" + band).c_str(), rho.at(s).at(u).at(b));
2045 }
2046 }
2047 }
2048 b++;
2049 }
2050
2051 // Transmissivity
2052
2053 // check for primitive data of form "transmissivity_spectrum" that can be used to calculate transmissivity
2054 spectrum_label.resize(0);
2055 if (context->doesPrimitiveDataExist(UUID, "transmissivity_spectrum")) {
2056 if (context->getPrimitiveDataType("transmissivity_spectrum") == HELIOS_TYPE_STRING) {
2057 context->getPrimitiveData(UUID, "transmissivity_spectrum", spectrum_label);
2058 }
2059 }
2060
2061 b = 0;
2062 for (const auto &band: band_labels) {
2063
2064 // check for primitive data of form "transmissivity_bandname"
2065 prop = "transmissivity_" + band;
2066
2067 float tau_s = tau_default;
2068 if (context->doesPrimitiveDataExist(UUID, prop.c_str())) {
2069 context->getPrimitiveData(UUID, prop.c_str(), tau_s);
2070 }
2071
2072 for (uint s = 0; s < Nsources; s++) {
2073 // if transmissivity was manually set, or a spectrum was given and the global data exists
2074 if (tau_s != tau_default || spectrum_label.empty() || !context->doesGlobalDataExist(spectrum_label.c_str()) || tau_unique.find(spectrum_label) == tau_unique.end()) {
2075
2076 tau.at(s).at(u).at(b) = tau_s;
2077
2078 // cameras
2079 for (uint cam = 0; cam < Ncameras; cam++) {
2080 tau_cam.at(s).at(u).at(b).at(cam) = tau_s;
2081 }
2082
2083 } else {
2084
2085 tau.at(s).at(u).at(b) = tau_unique.at(spectrum_label).at(b).at(s);
2086
2087 // cameras
2088 for (uint cam = 0; cam < Ncameras; cam++) {
2089 tau_cam.at(s).at(u).at(b).at(cam) = tau_cam_unique.at(spectrum_label).at(b).at(s).at(cam);
2090 }
2091 }
2092
2093 // error checking
2094 if (tau.at(s).at(u).at(b) < 0) {
2095 tau.at(s).at(u).at(b) = 0.f;
2096 warnings.addWarning("transmissivity_negative_clamped", "Transmissivity cannot be less than 0. Clamping to 0 for band " + band + ".");
2097 } else if (tau.at(s).at(u).at(b) > 1.f) {
2098 tau.at(s).at(u).at(b) = 1.f;
2099 warnings.addWarning("transmissivity_exceeded_clamped", "Transmissivity cannot be greater than 1. Clamping to 1 for band " + band + ".");
2100 }
2101 if (tau.at(s).at(u).at(b) != 0) {
2102 scattering_iterations_needed.at(band) = true;
2103 }
2104 for (auto &odata: output_prim_data) {
2105 if (odata == "transmissivity") {
2106 context->setPrimitiveData(UUID, ("transmissivity_" + std::to_string(s) + "_" + band).c_str(), tau.at(s).at(u).at(b));
2107 }
2108 }
2109 }
2110 b++;
2111 }
2112
2113 // Emissivity (only for error checking)
2114
2115 b = 0;
2116 for (const auto &band: band_labels) {
2117
2118 prop = "emissivity_" + band;
2119
2120 if (context->doesPrimitiveDataExist(UUID, prop.c_str())) {
2121 context->getPrimitiveData(UUID, prop.c_str(), eps);
2122 } else {
2123 eps = eps_default;
2124 }
2125
2126 if (eps < 0) {
2127 eps = 0.f;
2128 warnings.addWarning("emissivity_negative_clamped", "Emissivity cannot be less than 0. Clamping to 0 for band " + band + ".");
2129 } else if (eps > 1.f) {
2130 eps = 1.f;
2131 warnings.addWarning("emissivity_exceeded_clamped", "Emissivity cannot be greater than 1. Clamping to 1 for band " + band + ".");
2132 }
2133 if (eps != 1) {
2134 scattering_iterations_needed.at(band) = true;
2135 }
2136
2137 assert(doesBandExist(band));
2138
2139 for (uint s = 0; s < Nsources; s++) {
2140 if (radiation_bands.at(band).emissionFlag) { // emission enabled
2141 if (eps != 1.f && rho.at(s).at(u).at(b) == 0 && tau.at(s).at(u).at(b) == 0) {
2142 rho.at(s).at(u).at(b) = 1.f - eps;
2143 } else if (eps + tau.at(s).at(u).at(b) + rho.at(s).at(u).at(b) != 1.f && eps > 0.f) {
2144 helios_runtime_error("ERROR (RadiationModel): emissivity, transmissivity, and reflectivity must sum to 1 to ensure energy conservation. Band " + band + ", Primitive #" + std::to_string(UUID) + ": eps=" +
2145 std::to_string(eps) + ", tau=" + std::to_string(tau.at(s).at(u).at(b)) + ", rho=" + std::to_string(rho.at(s).at(u).at(b)) + ". It is also possible that you forgot to disable emission for this band.");
2146 } else if (radiation_bands.at(band).scatteringDepth == 0 && eps != 1.f) {
2147 eps = 1.f;
2148 rho.at(s).at(u).at(b) = 0.f;
2149 tau.at(s).at(u).at(b) = 0.f;
2150 }
2151 } else if (tau.at(s).at(u).at(b) + rho.at(s).at(u).at(b) > 1.f) {
2152 helios_runtime_error("ERROR (RadiationModel): transmissivity and reflectivity cannot sum to greater than 1 ensure energy conservation. Band " + band + ", Primitive #" + std::to_string(UUID) + ": eps=" + std::to_string(eps) +
2153 ", tau=" + std::to_string(tau.at(s).at(u).at(b)) + ", rho=" + std::to_string(rho.at(s).at(u).at(b)) + ". It is also possible that you forgot to disable emission for this band.");
2154 }
2155 }
2156 b++;
2157 }
2158 }
2159 }
2160
2161 std::vector<float> rho_flat = flatten(rho);
2162 std::vector<float> tau_flat = flatten(tau);
2163 std::vector<float> rho_cam_flat = flatten(rho_cam);
2164 std::vector<float> tau_cam_flat = flatten(tau_cam);
2165
2166 // Upload material properties to backend
2167 material_data.num_primitives = Nprimitives;
2168 material_data.num_bands = radiation_bands.size();
2169 material_data.num_sources = radiation_sources.size();
2170 material_data.num_cameras = cameras.size();
2171 material_data.reflectivity = rho_flat;
2172 material_data.transmissivity = tau_flat;
2173 material_data.reflectivity_cam = rho_cam_flat;
2174 material_data.transmissivity_cam = tau_cam_flat;
2175
2176 // Specular reflection properties
2177 material_data.specular_exponent.resize(Nprimitives, -1.f);
2178 material_data.specular_scale.resize(Nprimitives, 0.f);
2179
2180 bool specular_exponent_specified = false;
2181 bool specular_scale_specified = false;
2182
2183 for (size_t u = 0; u < Nprimitives; u++) {
2184 uint UUID = context_UUIDs.at(u);
2185
2186 if (context->doesPrimitiveDataExist(UUID, "specular_exponent") && context->getPrimitiveDataType("specular_exponent") == HELIOS_TYPE_FLOAT) {
2187 context->getPrimitiveData(UUID, "specular_exponent", material_data.specular_exponent.at(u));
2188 if (material_data.specular_exponent.at(u) >= 0.f) {
2189 specular_exponent_specified = true;
2190 }
2191 }
2192
2193 if (context->doesPrimitiveDataExist(UUID, "specular_scale") && context->getPrimitiveDataType("specular_scale") == HELIOS_TYPE_FLOAT) {
2194 context->getPrimitiveData(UUID, "specular_scale", material_data.specular_scale.at(u));
2195 if (material_data.specular_scale.at(u) > 0.f) {
2196 specular_scale_specified = true;
2197 }
2198 }
2199 }
2200
2201 // Auto-enable specular reflection if specular properties are specified on any primitive
2202 if (specular_exponent_specified) {
2203 if (specular_scale_specified) {
2204 specular_reflection_mode = 2; // Mode 2: use primitive specular_scale
2205 } else {
2206 specular_reflection_mode = 1; // Mode 1: use default 0.25 scale
2207 }
2208 } else {
2209 specular_reflection_mode = 0; // Disabled
2210 }
2211
2212 backend->updateMaterials(material_data);
2213
2214 radiativepropertiesneedupdate = false;
2215
2216 if (message_flag) {
2217 std::cout << "done\n";
2218 }
2219
2220 // Report aggregated warnings
2221 warnings.report(std::cerr);
2222}
2223
2224std::vector<float> RadiationModel::updateAtmosphericSkyModel(const std::vector<std::string> &band_labels, const RadiationCamera &camera) {
2225 // Prague Sky Model implementation for atmospheric sky radiance
2226 // Uses validated spectral radiance from brute-force atmospheric simulations
2227 // (Wilkie et al. 2021, Vévoda et al. 2022)
2228
2229 size_t Nbands_launch = band_labels.size();
2230 std::vector<float> sky_base_radiances(Nbands_launch, 0.0f);
2231
2232 // Only run atmospheric sky model if user has explicitly enabled it by setting atmospheric parameters
2233 // This prevents the model from running with default values in tests/scripts that don't want it
2234 bool has_atmospheric_data =
2235 context->doesGlobalDataExist("atmosphere_pressure_Pa") || context->doesGlobalDataExist("atmosphere_temperature_K") || context->doesGlobalDataExist("atmosphere_humidity_rel") || context->doesGlobalDataExist("atmosphere_turbidity");
2236
2237 if (!has_atmospheric_data) {
2238 // No atmospheric parameters set - return zeros (camera will use user-set diffuse flux or 0)
2239 return sky_base_radiances;
2240 }
2241
2242 // Read atmospheric parameters from Context global data (set by SolarPosition plugin)
2243 // Default values match SolarPosition::getAtmosphericConditions() defaults
2244 float pressure_Pa = 101325.f; // Standard atmosphere (1 atm)
2245 float temperature_K = 300.f; // 27°C
2246 float humidity_rel = 0.5f; // 50% relative humidity
2247 float turbidity = 0.02f; // Clear sky - Ångström's aerosol turbidity coefficient (AOD at 500nm)
2248
2249 if (context->doesGlobalDataExist("atmosphere_pressure_Pa")) {
2250 context->getGlobalData("atmosphere_pressure_Pa", pressure_Pa);
2251 }
2252 if (context->doesGlobalDataExist("atmosphere_temperature_K")) {
2253 context->getGlobalData("atmosphere_temperature_K", temperature_K);
2254 }
2255 if (context->doesGlobalDataExist("atmosphere_humidity_rel")) {
2256 context->getGlobalData("atmosphere_humidity_rel", humidity_rel);
2257 }
2258 if (context->doesGlobalDataExist("atmosphere_turbidity")) {
2259 context->getGlobalData("atmosphere_turbidity", turbidity);
2260 }
2261
2262 // --- Check Prague data availability from Context ---
2263 int prague_valid = 0;
2264 if (context->doesGlobalDataExist("prague_sky_valid")) {
2265 context->getGlobalData("prague_sky_valid", prague_valid);
2266 }
2267
2268 // Get sun direction from first radiation source (assumed to be sun)
2269 helios::vec3 sun_dir(0, 0, 1); // Default zenith
2270 if (!radiation_sources.empty()) {
2271 sun_dir = radiation_sources[0].source_position;
2272 sun_dir.normalize();
2273 }
2274
2275 // Compute per-band sky radiance parameters
2276 std::vector<helios::vec4> sky_params(Nbands_launch);
2277
2278 // Check if Prague data is available
2279 bool use_prague_fallback = (prague_valid != 1);
2280 if (use_prague_fallback) {
2281 // Will use Rayleigh sky fallback - warn user once
2282 std::cerr << "WARNING (RadiationModel::updateAtmosphericSkyModel): "
2283 << "Prague sky model data not available in Context. "
2284 << "Using simple Rayleigh sky fallback. "
2285 << "Call SolarPosition::updatePragueSkyModel() for accurate sky radiance." << std::endl;
2286 }
2287
2288 // Prepare spectral data (either Prague or Rayleigh fallback)
2289 std::vector<float> wavelengths;
2290 std::vector<float> L_zenith_spectrum;
2291 std::vector<float> circ_str_spectrum;
2292 std::vector<float> circ_width_spectrum;
2293 std::vector<float> horiz_bright_spectrum;
2294 std::vector<float> norm_spectrum;
2295
2296 if (use_prague_fallback) {
2297 // --- Create simple Rayleigh sky spectrum (λ^-4 dependence) ---
2298 // 360-750 nm at 10 nm spacing (visible range only for fallback)
2299 const int n_wavelengths = 40; // (750-360)/10 + 1
2300 wavelengths.resize(n_wavelengths);
2301 L_zenith_spectrum.resize(n_wavelengths);
2302 circ_str_spectrum.resize(n_wavelengths);
2303 circ_width_spectrum.resize(n_wavelengths);
2304 horiz_bright_spectrum.resize(n_wavelengths);
2305 norm_spectrum.resize(n_wavelengths);
2306
2307 const float L_base = 0.4f; // W/m²/sr/nm at 550 nm (typical clear sky zenith)
2308 const float lambda_ref = 550.0f; // Reference wavelength
2309
2310 for (int i = 0; i < n_wavelengths; ++i) {
2311 float lambda = 360.0f + i * 10.0f;
2312 wavelengths[i] = lambda;
2313
2314 // Rayleigh scattering: L(λ) ∝ λ^-4 (blue sky)
2315 float rayleigh_factor = std::pow(lambda_ref / lambda, 4.0f);
2316 L_zenith_spectrum[i] = L_base * rayleigh_factor;
2317
2318 // Simple angular parameters (no strong circumsolar for fallback)
2319 circ_str_spectrum[i] = 0.5f;
2320 circ_width_spectrum[i] = 20.0f;
2321 horiz_bright_spectrum[i] = 1.8f;
2322 norm_spectrum[i] = 0.7f;
2323 }
2324 } else {
2325 // --- Read spectral parameters from Context ---
2326 std::vector<float> spectral_params;
2327 context->getGlobalData("prague_sky_spectral_params", spectral_params);
2328
2329 const int params_per_wavelength = 6;
2330 const int n_wavelengths = spectral_params.size() / params_per_wavelength;
2331
2332 // Parse into structured format
2333 wavelengths.resize(n_wavelengths);
2334 L_zenith_spectrum.resize(n_wavelengths);
2335 circ_str_spectrum.resize(n_wavelengths);
2336 circ_width_spectrum.resize(n_wavelengths);
2337 horiz_bright_spectrum.resize(n_wavelengths);
2338 norm_spectrum.resize(n_wavelengths);
2339
2340 for (int i = 0; i < n_wavelengths; ++i) {
2341 int base = i * params_per_wavelength;
2342 wavelengths[i] = spectral_params[base + 0];
2343 L_zenith_spectrum[i] = spectral_params[base + 1];
2344 circ_str_spectrum[i] = spectral_params[base + 2];
2345 circ_width_spectrum[i] = spectral_params[base + 3];
2346 horiz_bright_spectrum[i] = spectral_params[base + 4];
2347 norm_spectrum[i] = spectral_params[base + 5];
2348 }
2349 }
2350
2351 // --- Process each band ---
2352 for (size_t b = 0; b < Nbands_launch; b++) {
2353 const std::string &band_label = band_labels[b];
2354 if (radiation_bands.find(band_label) == radiation_bands.end()) {
2355 continue;
2356 }
2357
2358 const RadiationBand &band = radiation_bands.at(band_label);
2359
2360 // Skip thermal/longwave bands - Prague Sky Model only handles shortwave radiation
2361 if (band.emissionFlag) {
2362 continue;
2363 }
2364
2365 // Get camera spectral response for this band
2366 std::string spectral_response_label = "uniform";
2367 if (camera.band_spectral_response.find(band_label) != camera.band_spectral_response.end()) {
2368 spectral_response_label = camera.band_spectral_response.at(band_label);
2369 if (spectral_response_label.empty() || trim_whitespace(spectral_response_label).empty()) {
2370 spectral_response_label = "uniform";
2371 }
2372 }
2373
2374 // Get camera spectral response data
2375 std::vector<helios::vec2> camera_response;
2376
2377 if (spectral_response_label == "uniform") {
2378 helios::vec2 wavelength_range = band.wavebandBounds;
2379
2380 if (wavelength_range.x <= 0.f || wavelength_range.y <= 0.f) {
2381 bool bounds_inferred = false;
2382
2383 if (band_label == "red" || band_label == "R") {
2384 wavelength_range = helios::make_vec2(620.f, 750.f);
2385 bounds_inferred = true;
2386 } else if (band_label == "green" || band_label == "G") {
2387 wavelength_range = helios::make_vec2(495.f, 570.f);
2388 bounds_inferred = true;
2389 } else if (band_label == "blue" || band_label == "B") {
2390 wavelength_range = helios::make_vec2(450.f, 495.f);
2391 bounds_inferred = true;
2392 }
2393
2394 if (!bounds_inferred) {
2395 if (!band.diffuse_spectrum.empty()) {
2396 wavelength_range.x = band.diffuse_spectrum.front().x;
2397 wavelength_range.y = band.diffuse_spectrum.back().x;
2398 } else {
2399 helios_runtime_error("ERROR (RadiationModel::updateAtmosphericSkyModel): Camera '" + camera.label + "' band '" + band_label + "' has uniform spectral response but no wavelength bounds set.");
2400 }
2401 }
2402 }
2403
2404 camera_response.push_back(helios::make_vec2(wavelength_range.x, 1.0f));
2405 camera_response.push_back(helios::make_vec2(wavelength_range.y, 1.0f));
2406
2407 } else {
2408 camera_response = loadSpectralData(spectral_response_label);
2409
2410 if (camera_response.empty()) {
2411 helios_runtime_error("ERROR (RadiationModel::updateAtmosphericSkyModel): Camera spectral response '" + spectral_response_label + "' not found for camera '" + camera.label + "' band '" + band_label + "'.");
2412 }
2413 }
2414
2415 // Integrate radiance and weight-average angular parameters over camera response
2416 // L_zenith: Integrate to get W/m²/sr (band-integrated radiance)
2417 float integrated_L_zenith = integrateOverResponse(wavelengths, L_zenith_spectrum, camera_response);
2418
2419 // Angular parameters: Weighted average (unitless quantities)
2420 // Weight by L_zenith(λ) × R(λ) to get radiance-weighted average
2421 float integrated_circ_str = weightedAverageOverResponse(wavelengths, circ_str_spectrum, L_zenith_spectrum, camera_response);
2422 float integrated_circ_width = weightedAverageOverResponse(wavelengths, circ_width_spectrum, L_zenith_spectrum, camera_response);
2423 float integrated_horiz_bright = weightedAverageOverResponse(wavelengths, horiz_bright_spectrum, L_zenith_spectrum, camera_response);
2424
2425 // Recompute normalization from averaged angular parameters
2426 float integrated_norm = computeAngularNormalization(integrated_circ_str, integrated_circ_width, integrated_horiz_bright);
2427
2428 // CRITICAL: GPU multiplies by normalization (see rayHit.cu:evaluateSkyRadiance)
2429 // Since normalization < 1 (typically 0.6-0.7), this darkens the sky
2430 // Pre-divide by normalization so it cancels: GPU does (L/norm) × pattern × norm = L × pattern
2431 float base_radiance_for_gpu = integrated_L_zenith / std::max(integrated_norm, 0.1f);
2432
2433 sky_base_radiances[b] = base_radiance_for_gpu;
2434 sky_params[b] = helios::make_vec4(integrated_circ_str, integrated_circ_width, integrated_horiz_bright, integrated_norm);
2435 }
2436
2437 // Sky parameters will be uploaded to backend via updateSkyModel()
2438 return sky_base_radiances;
2439}
2440
2441void RadiationModel::updatePragueParametersForGeneralDiffuse(const std::vector<std::string> &band_labels) {
2442 // Update Prague sky model angular parameters for general diffuse radiation
2443 // Reads spectral parameters from Context (set by SolarPosition::updatePragueSkyModel())
2444 // Integrates over band spectral response to get band-averaged parameters
2445
2446 // Check Prague data availability
2447 int prague_valid = 0;
2448 if (!context->doesGlobalDataExist("prague_sky_valid") || (context->getGlobalData("prague_sky_valid", prague_valid), prague_valid != 1)) {
2449 // No Prague data - leave params at zero (will use power-law or isotropic)
2450 return;
2451 }
2452
2453 // Read spectral parameters from Context
2454 std::vector<float> spectral_params;
2455 context->getGlobalData("prague_sky_spectral_params", spectral_params);
2456
2457 // Parse into wavelength-resolved arrays
2458 const int params_per_wavelength = 6;
2459 const int n_wavelengths = spectral_params.size() / params_per_wavelength;
2460
2461 std::vector<float> wavelengths(n_wavelengths);
2462 std::vector<float> L_zenith_spectrum(n_wavelengths);
2463 std::vector<float> circ_str_spectrum(n_wavelengths);
2464 std::vector<float> circ_width_spectrum(n_wavelengths);
2465 std::vector<float> horiz_bright_spectrum(n_wavelengths);
2466 std::vector<float> norm_spectrum(n_wavelengths);
2467
2468 for (int i = 0; i < n_wavelengths; ++i) {
2469 int base = i * params_per_wavelength;
2470 wavelengths[i] = spectral_params[base + 0];
2471 L_zenith_spectrum[i] = spectral_params[base + 1];
2472 circ_str_spectrum[i] = spectral_params[base + 2];
2473 circ_width_spectrum[i] = spectral_params[base + 3];
2474 horiz_bright_spectrum[i] = spectral_params[base + 4];
2475 norm_spectrum[i] = spectral_params[base + 5];
2476 }
2477
2478 // Get sun direction
2479 helios::vec3 sun_dir;
2480 context->getGlobalData("prague_sky_sun_direction", sun_dir);
2481
2482 // Process each band
2483 for (const auto &label: band_labels) {
2484 RadiationBand &band = radiation_bands.at(label);
2485
2486 // SKIP if user has explicitly set power-law (priority 1)
2487 if (band.diffuseExtinction > 0.0f) {
2488 continue;
2489 }
2490
2491 // Integrate Prague parameters over band spectrum
2492 std::vector<helios::vec2> band_spectrum = band.diffuse_spectrum;
2493 if (band_spectrum.empty()) {
2494 // Use waveband bounds if no detailed spectrum
2495 float lambda_min = band.wavebandBounds.x;
2496 float lambda_max = band.wavebandBounds.y;
2497 if (lambda_min > 0 && lambda_max > lambda_min) {
2498 band_spectrum = {{lambda_min, 1.0f}, {lambda_max, 1.0f}};
2499 }
2500 }
2501
2502 if (band_spectrum.empty()) {
2503 // No spectral info - skip Prague for this band
2504 continue;
2505 }
2506
2507 // Weighted integration (weight by L_zenith for physical consistency)
2508 float int_circ_str = weightedAverageOverResponse(wavelengths, circ_str_spectrum, L_zenith_spectrum, band_spectrum);
2509 float int_circ_width = weightedAverageOverResponse(wavelengths, circ_width_spectrum, L_zenith_spectrum, band_spectrum);
2510 float int_horiz_bright = weightedAverageOverResponse(wavelengths, horiz_bright_spectrum, L_zenith_spectrum, band_spectrum);
2511
2512 // Recompute normalization from integrated parameters
2513 float int_norm = computeAngularNormalization(int_circ_str, int_circ_width, int_horiz_bright);
2514
2515 // Store in RadiationBand
2516 band.diffusePragueParams = helios::make_vec4(int_circ_str, int_circ_width, int_horiz_bright, int_norm);
2517 band.diffusePeakDir = sun_dir;
2518 }
2519}
2520
2521float RadiationModel::integrateOverResponse(const std::vector<float> &wavelengths, const std::vector<float> &values, const std::vector<helios::vec2> &camera_response) const {
2522
2523 if (wavelengths.empty() || camera_response.empty()) {
2524 return 0.0f;
2525 }
2526
2527 // CRITICAL: This integrates spectral radiance L(λ) in W/m²/sr/nm over wavelength
2528 // to produce band-integrated radiance in W/m²/sr (same as Prague computeIntegratedSkyRadiance)
2529 double integrated_radiance = 0.0;
2530
2531 // Trapezoidal integration over camera response wavelength range
2532 for (size_t i = 0; i < camera_response.size() - 1; ++i) {
2533 float lambda1 = camera_response[i].x;
2534 float lambda2 = camera_response[i + 1].x;
2535
2536 // Skip if outside spectral data range
2537 if (lambda2 < wavelengths.front() || lambda1 > wavelengths.back()) {
2538 continue;
2539 }
2540
2541 float r1 = camera_response[i].y;
2542 float r2 = camera_response[i + 1].y;
2543
2544 // Interpolate spectral values at these wavelengths using linear interpolation
2545 float v1, v2;
2546
2547 // Interpolate v1 at lambda1
2548 if (lambda1 <= wavelengths.front()) {
2549 v1 = values.front();
2550 } else if (lambda1 >= wavelengths.back()) {
2551 v1 = values.back();
2552 } else {
2553 auto it = std::lower_bound(wavelengths.begin(), wavelengths.end(), lambda1);
2554 size_t idx = std::distance(wavelengths.begin(), it);
2555 if (idx == 0)
2556 idx = 1;
2557 float t = (lambda1 - wavelengths[idx - 1]) / (wavelengths[idx] - wavelengths[idx - 1]);
2558 v1 = values[idx - 1] + t * (values[idx] - values[idx - 1]);
2559 }
2560
2561 // Interpolate v2 at lambda2
2562 if (lambda2 <= wavelengths.front()) {
2563 v2 = values.front();
2564 } else if (lambda2 >= wavelengths.back()) {
2565 v2 = values.back();
2566 } else {
2567 auto it = std::lower_bound(wavelengths.begin(), wavelengths.end(), lambda2);
2568 size_t idx = std::distance(wavelengths.begin(), it);
2569 if (idx == 0)
2570 idx = 1;
2571 float t = (lambda2 - wavelengths[idx - 1]) / (wavelengths[idx] - wavelengths[idx - 1]);
2572 v2 = values[idx - 1] + t * (values[idx] - values[idx - 1]);
2573 }
2574
2575 float dlambda = lambda2 - lambda1;
2576
2577 // Integrate: ∫ L(λ) × R(λ) dλ
2578 // L(λ) in W/m²/sr/nm, R(λ) unitless, dλ in nm → result in W/m²/sr
2579 integrated_radiance += 0.5 * (v1 * r1 + v2 * r2) * dlambda;
2580 }
2581
2582 // Return band-integrated radiance in W/m²/sr (matches Prague computeIntegratedSkyRadiance)
2583 return static_cast<float>(integrated_radiance);
2584}
2585
2586float RadiationModel::weightedAverageOverResponse(const std::vector<float> &wavelengths, const std::vector<float> &param_values, const std::vector<float> &weight_values, const std::vector<helios::vec2> &camera_response) const {
2587
2588 if (wavelengths.empty() || camera_response.empty()) {
2589 return 0.0f;
2590 }
2591
2592 // CRITICAL: Angular parameters are unitless - compute radiance-weighted average
2593 // Formula: Σ param(λ) × L(λ) × R(λ) dλ / Σ L(λ) × R(λ) dλ
2594 double weighted_sum = 0.0;
2595 double total_weight = 0.0;
2596
2597 for (size_t i = 0; i < camera_response.size() - 1; ++i) {
2598 float lambda1 = camera_response[i].x;
2599 float lambda2 = camera_response[i + 1].x;
2600
2601 if (lambda2 < wavelengths.front() || lambda1 > wavelengths.back()) {
2602 continue;
2603 }
2604
2605 float r1 = camera_response[i].y;
2606 float r2 = camera_response[i + 1].y;
2607
2608 // Interpolate parameter values
2609 float p1, p2;
2610 if (lambda1 <= wavelengths.front()) {
2611 p1 = param_values.front();
2612 } else if (lambda1 >= wavelengths.back()) {
2613 p1 = param_values.back();
2614 } else {
2615 auto it = std::lower_bound(wavelengths.begin(), wavelengths.end(), lambda1);
2616 size_t idx = std::distance(wavelengths.begin(), it);
2617 if (idx == 0)
2618 idx = 1;
2619 float t = (lambda1 - wavelengths[idx - 1]) / (wavelengths[idx] - wavelengths[idx - 1]);
2620 p1 = param_values[idx - 1] + t * (param_values[idx] - param_values[idx - 1]);
2621 }
2622
2623 if (lambda2 <= wavelengths.front()) {
2624 p2 = param_values.front();
2625 } else if (lambda2 >= wavelengths.back()) {
2626 p2 = param_values.back();
2627 } else {
2628 auto it = std::lower_bound(wavelengths.begin(), wavelengths.end(), lambda2);
2629 size_t idx = std::distance(wavelengths.begin(), it);
2630 if (idx == 0)
2631 idx = 1;
2632 float t = (lambda2 - wavelengths[idx - 1]) / (wavelengths[idx] - wavelengths[idx - 1]);
2633 p2 = param_values[idx - 1] + t * (param_values[idx] - param_values[idx - 1]);
2634 }
2635
2636 // Interpolate weight (radiance) values
2637 float w1, w2;
2638 if (lambda1 <= wavelengths.front()) {
2639 w1 = weight_values.front();
2640 } else if (lambda1 >= wavelengths.back()) {
2641 w1 = weight_values.back();
2642 } else {
2643 auto it = std::lower_bound(wavelengths.begin(), wavelengths.end(), lambda1);
2644 size_t idx = std::distance(wavelengths.begin(), it);
2645 if (idx == 0)
2646 idx = 1;
2647 float t = (lambda1 - wavelengths[idx - 1]) / (wavelengths[idx] - wavelengths[idx - 1]);
2648 w1 = weight_values[idx - 1] + t * (weight_values[idx] - weight_values[idx - 1]);
2649 }
2650
2651 if (lambda2 <= wavelengths.front()) {
2652 w2 = weight_values.front();
2653 } else if (lambda2 >= wavelengths.back()) {
2654 w2 = weight_values.back();
2655 } else {
2656 auto it = std::lower_bound(wavelengths.begin(), wavelengths.end(), lambda2);
2657 size_t idx = std::distance(wavelengths.begin(), it);
2658 if (idx == 0)
2659 idx = 1;
2660 float t = (lambda2 - wavelengths[idx - 1]) / (wavelengths[idx] - wavelengths[idx - 1]);
2661 w2 = weight_values[idx - 1] + t * (weight_values[idx] - weight_values[idx - 1]);
2662 }
2663
2664 float dlambda = lambda2 - lambda1;
2665
2666 // Weighted average: Σ param × weight × response × dλ
2667 weighted_sum += 0.5 * (p1 * w1 * r1 + p2 * w2 * r2) * dlambda;
2668 total_weight += 0.5 * (w1 * r1 + w2 * r2) * dlambda;
2669 }
2670
2671 // Return weighted average (unitless)
2672 if (total_weight > 1e-10) {
2673 return static_cast<float>(weighted_sum / total_weight);
2674 }
2675 return 0.0f;
2676}
2677
2678float RadiationModel::computeAngularNormalization(float circ_str, float circ_width, float horiz_bright) const {
2679 // Numerical integration of angular pattern over hemisphere
2680 const int N = 50;
2681 float integral = 0.0f;
2682
2683 // Sun at zenith for normalization calculation
2684 helios::vec3 sun_dir = make_vec3(0, 0, 1);
2685
2686 for (int j = 0; j < N; ++j) {
2687 for (int i = 0; i < N; ++i) {
2688 float theta = 0.5f * float(M_PI) * (i + 0.5f) / N; // 0 to π/2
2689 float phi = 2.0f * float(M_PI) * (j + 0.5f) / N; // 0 to 2π
2690
2691 helios::vec3 dir = sphere2cart(make_SphericalCoord(0.5f * float(M_PI) - theta, phi));
2692
2693 // Angular distance from sun (degrees) - matches GPU calculation
2694 float cos_gamma = std::max(-1.0f, std::min(1.0f, dir.x * sun_dir.x + dir.y * sun_dir.y + dir.z * sun_dir.z));
2695 float gamma = std::acos(cos_gamma) * 180.0f / float(M_PI);
2696
2697 // Compute angular pattern (same as GPU: rayHit.cu evaluateDiffuseAngularDistribution)
2698 float cos_theta = std::max(0.0f, dir.z);
2699 float horizon_term = 1.0f + (horiz_bright - 1.0f) * (1.0f - cos_theta);
2700 float circ_term = 1.0f + circ_str * std::exp(-gamma / circ_width);
2701
2702 float pattern = circ_term * horizon_term;
2703
2704 // Solid angle element: sin(θ) dθ dφ
2705 integral += pattern * std::cos(theta) * std::sin(theta) * (float(M_PI) / (2.0f * N)) * (2.0f * float(M_PI) / N);
2706 }
2707 }
2708
2709 return 1.0f / std::max(integral, 1e-10f);
2710}
2711
2712std::vector<helios::vec2> RadiationModel::loadSpectralData(const std::string &global_data_label) const {
2713
2714 std::vector<helios::vec2> spectrum;
2715
2716 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
2717
2718 // check if spectral data exists in any of the library files
2719 bool data_found = false;
2720 for (const auto &file: spectral_library_files) {
2721 if (Context::scanXMLForTag(file, "globaldata_vec2", global_data_label)) {
2722 context->loadXML(file.c_str(), true);
2723 data_found = true;
2724 break;
2725 }
2726 }
2727
2728 if (!data_found) {
2729 helios_runtime_error("ERROR (RadiationModel::loadSpectralData): Global data for spectrum '" + global_data_label + "' could not be found.");
2730 }
2731 }
2732
2733 if (context->getGlobalDataType(global_data_label.c_str()) != HELIOS_TYPE_VEC2) {
2734 helios_runtime_error("ERROR (RadiationModel::loadSpectralData): Global data for spectrum '" + global_data_label + "' is not of type HELIOS_TYPE_VEC2.");
2735 }
2736
2737 context->getGlobalData(global_data_label.c_str(), spectrum);
2738
2739 // validate spectrum
2740 if (spectrum.empty()) {
2741 helios_runtime_error("ERROR (RadiationModel::loadSpectralData): Global data for spectrum '" + global_data_label + "' is empty.");
2742 }
2743 for (auto s = 0; s < spectrum.size(); s++) {
2744 // check that wavelengths are monotonic
2745 if (s > 0 && spectrum.at(s).x <= spectrum.at(s - 1).x) {
2746 helios_runtime_error("ERROR (RadiationModel::loadSpectralData): Source spectral data validation failed. Wavelengths must increase monotonically.");
2747 }
2748 // check that wavelength is within a reasonable range
2749 if (spectrum.at(s).x < 0 || spectrum.at(s).x > 100000) {
2750 helios_runtime_error("ERROR (RadiationModel::loadSpectralData): Source spectral data validation failed. Wavelength value of " + std::to_string(spectrum.at(s).x) + " appears to be erroneous.");
2751 }
2752 // check that flux is non-negative
2753 if (spectrum.at(s).y < 0) {
2754 helios_runtime_error("ERROR (RadiationModel::loadSpectralData): Source spectral data validation failed. Flux value at wavelength of " + std::to_string(spectrum.at(s).x) + " appears is negative.");
2755 }
2756 }
2757
2758 return spectrum;
2759}
2760
2761void RadiationModel::runBand(const std::string &label) {
2762 std::vector<std::string> labels{label};
2763 runBand(labels);
2764}
2765
2766void RadiationModel::runBand(const std::vector<std::string> &label) {
2767
2768 //----- VERIFICATIONS -----//
2769
2770 // We need the band label strings to appear in the same order as in radiation_bands map.
2771 // this is because that was the order in which radiative properties were laid out when updateRadiativeProperties() was called
2772 std::vector<std::string> band_labels;
2773 for (auto &band: radiation_bands) {
2774 if (std::find(label.begin(), label.end(), band.first) != label.end()) {
2775 band_labels.push_back(band.first);
2776 }
2777 }
2778
2779 // Check to make sure some geometry was added to the context
2780 if (context->getPrimitiveCount() == 0) {
2781 std::cerr << "WARNING (RadiationModel::runBand): No geometry was added to the context. There is nothing to simulate...exiting." << std::endl;
2782 return;
2783 }
2784
2785 // Check to make sure geometry was built in OptiX
2786 if (!isgeometryinitialized) {
2788 }
2789
2790 // Check that all the bands passed to the runBand() method exist
2791 for (const std::string &band: label) {
2792 if (!doesBandExist(band)) {
2793 helios_runtime_error("ERROR (RadiationModel::runBand): Cannot run band " + band + " because it is not a valid band. Use addRadiationBand() function to add the band.");
2794 }
2795 }
2796
2797 // if there are no radiation sources in the simulation, add at least one but with zero fluxes
2798 if (radiation_sources.empty()) {
2800 }
2801
2802 // Check if any source spectra have changed in global data and reload if necessary
2803 for (auto &source: radiation_sources) {
2804 if (!source.source_spectrum_label.empty() && source.source_spectrum_label != "none") {
2805 uint64_t current_version = context->getGlobalDataVersion(source.source_spectrum_label.c_str());
2806 if (current_version != source.source_spectrum_version) {
2807 // Reload spectrum from global data
2808 source.source_spectrum = loadSpectralData(source.source_spectrum_label);
2809 source.source_spectrum_version = current_version;
2810 radiativepropertiesneedupdate = true;
2811 }
2812 }
2813 }
2814
2815 // Check if global diffuse spectrum has changed and reload if necessary
2816 if (!global_diffuse_spectrum_label.empty() && global_diffuse_spectrum_label != "none") {
2817 uint64_t current_version = context->getGlobalDataVersion(global_diffuse_spectrum_label.c_str());
2818 if (current_version != global_diffuse_spectrum_version) {
2819 // Reload diffuse spectrum from global data
2820 global_diffuse_spectrum = loadSpectralData(global_diffuse_spectrum_label);
2821 global_diffuse_spectrum_version = current_version;
2822 // Also update all band diffuse spectra
2823 for (auto &band_pair: radiation_bands) {
2824 band_pair.second.diffuse_spectrum = global_diffuse_spectrum;
2825 }
2826 radiativepropertiesneedupdate = true;
2827 }
2828 }
2829
2830 if (radiativepropertiesneedupdate) {
2831 // Use old material path (handles spectrum interpolation)
2832 updateRadiativeProperties();
2833 // DON'T call backend->updateMaterials() - old code already uploaded via direct OptiX calls
2834 } else {
2835 // Use new backend path (per-band materials only)
2836 buildMaterialData();
2837 backend->updateMaterials(material_data);
2838 }
2839
2840 // Upload sources to backend (always use new path)
2841 buildSourceData();
2842 backend->updateSources(source_data);
2843
2844 // Prepare launch parameters (these will be passed to backend via RayTracingLaunchParams)
2845 size_t Nbands_launch = band_labels.size();
2846 size_t Nbands_global = radiation_bands.size();
2847
2848 // Build band launch flags
2849 std::vector<char> band_launch_flag(Nbands_global);
2850 uint bb = 0;
2851 for (auto &band: radiation_bands) {
2852 if (std::find(band_labels.begin(), band_labels.end(), band.first) != band_labels.end()) {
2853 band_launch_flag.at(bb) = 1;
2854 }
2855 bb++;
2856 }
2857
2858 // Get dimensions
2859 size_t Nobjects = primitiveID.size();
2860 size_t Nprimitives = context_UUIDs.size();
2861 uint Nsources = radiation_sources.size();
2862 uint Ncameras = cameras.size();
2863
2864 // Note: Atmospheric sky radiance model is updated per-camera (see camera trace loop below)
2865 // This allows us to use camera-specific spectral responses for each band
2866
2867 // Set scattering depth for each band
2868 std::vector<uint> scattering_depth(Nbands_launch);
2869 bool scatteringenabled = false;
2870 for (auto b = 0; b < Nbands_launch; b++) {
2871 scattering_depth.at(b) = radiation_bands.at(band_labels.at(b)).scatteringDepth;
2872 if (scattering_depth.at(b) > 0) {
2873 scatteringenabled = true;
2874 }
2875 }
2876
2877 // Issue warning if rho>0, tau>0, or eps<1
2878 for (int b = 0; b < Nbands_launch; b++) {
2879 if (scattering_depth.at(b) == 0 && scattering_iterations_needed.at(band_labels.at(b))) {
2880 std::cout << "WARNING (RadiationModel::runBand): Surface radiative properties for band " << band_labels.at(b)
2881 << " are set to non-default values, but scattering iterations are disabled. Surface radiative properties will be ignored unless scattering depth is non-zero." << std::endl;
2882 }
2883 }
2884
2885 // Set diffuse flux for each band
2886 std::vector<float> diffuse_flux(Nbands_launch);
2887 bool diffuseenabled = false;
2888 for (auto b = 0; b < Nbands_launch; b++) {
2889 diffuse_flux.at(b) = getDiffuseFlux(band_labels.at(b));
2890 if (diffuse_flux.at(b) > 0.f) {
2891 diffuseenabled = true;
2892 }
2893 }
2894 // NOTE: diffuse_flux now passed to backend via launch params, not uploaded here
2895
2896 // Initialize camera sky radiance buffer to zeros (will be set per-camera if atmospheric model is used)
2897 std::vector<float> camera_sky_radiance(Nbands_launch, 0.0f);
2898
2899 // Update Prague parameters for general diffuse (if available in Context)
2900 // This must be done before uploading diffuse parameters to GPU
2901 if (diffuseenabled) {
2902 updatePragueParametersForGeneralDiffuse(band_labels);
2903 }
2904
2905 // Set diffuse extinction coefficient for each band
2906 std::vector<float> diffuse_extinction(Nbands_launch, 0);
2907 if (diffuseenabled) {
2908 for (auto b = 0; b < Nbands_launch; b++) {
2909 diffuse_extinction.at(b) = radiation_bands.at(band_labels.at(b)).diffuseExtinction;
2910 }
2911 }
2912 // NOTE: diffuse_extinction now passed to backend via launch params, not uploaded here
2913
2914 // Set diffuse distribution normalization factor for each band
2915 std::vector<float> diffuse_dist_norm(Nbands_launch, 0);
2916 if (diffuseenabled) {
2917 for (auto b = 0; b < Nbands_launch; b++) {
2918 diffuse_dist_norm.at(b) = radiation_bands.at(band_labels.at(b)).diffuseDistNorm;
2919 }
2920 }
2921 // NOTE: diffuse_dist_norm now passed to backend via launch params, not uploaded here
2922
2923 // Set diffuse distribution peak direction for each band
2924 std::vector<helios::vec3> diffuse_peak_dir(Nbands_launch);
2925 if (diffuseenabled) {
2926 for (auto b = 0; b < Nbands_launch; b++) {
2927 helios::vec3 peak_dir = radiation_bands.at(band_labels.at(b)).diffusePeakDir;
2928 diffuse_peak_dir.at(b) = helios::make_vec3(peak_dir.x, peak_dir.y, peak_dir.z);
2929 }
2930 }
2931 // NOTE: diffuse_peak_dir now passed to backend via launch params, not uploaded here
2932
2933 // Upload Prague parameters for general diffuse (reuses camera buffer)
2934 // This allows general diffuse to use Prague sky model if available
2935 std::vector<helios::vec4> prague_params(Nbands_launch);
2936 if (diffuseenabled) {
2937 for (auto b = 0; b < Nbands_launch; b++) {
2938 const auto &params = radiation_bands.at(band_labels.at(b)).diffusePragueParams;
2939 prague_params.at(b) = helios::make_vec4(params.x, params.y, params.z, params.w);
2940 }
2941 // Prague params will be uploaded to backend via updateSkyModel() during scattering
2942 }
2943
2944 // Determine whether emission is enabled for any band
2945 bool emissionenabled = false;
2946 for (auto b = 0; b < Nbands_launch; b++) {
2947 if (radiation_bands.at(band_labels.at(b)).emissionFlag) {
2948 emissionenabled = true;
2949 }
2950 }
2951
2952 // Figure out the maximum direct ray count for all bands in this run and use this as the launch size
2953 size_t directRayCount = 0;
2954 for (const auto &band: label) {
2955 if (radiation_bands.at(band).directRayCount > directRayCount) {
2956 directRayCount = radiation_bands.at(band).directRayCount;
2957 }
2958 }
2959
2960 // Figure out the maximum diffuse ray count for all bands in this run and use this as the launch size
2961 size_t diffuseRayCount = 0;
2962 for (const auto &band: label) {
2963 if (radiation_bands.at(band).diffuseRayCount > diffuseRayCount) {
2964 diffuseRayCount = radiation_bands.at(band).diffuseRayCount;
2965 }
2966 }
2967
2968 // Figure out the maximum diffuse ray count for all bands in this run and use this as the launch size
2969 size_t scatteringDepth = 0;
2970 for (const auto &band: label) {
2971 if (radiation_bands.at(band).scatteringDepth > scatteringDepth) {
2972 scatteringDepth = radiation_bands.at(band).scatteringDepth;
2973 }
2974 }
2975
2976 // Zero radiation buffers via backend
2977 backend->zeroRadiationBuffers(Nbands_launch);
2978
2979 std::vector<float> TBS_top, TBS_bottom;
2980 TBS_top.resize(Nbands_launch * Nprimitives, 0);
2981 TBS_bottom = TBS_top;
2982
2983 std::map<std::string, std::vector<std::vector<float>>> radiation_in_camera;
2984
2985 size_t maxRays = 1024 * 1024 * 1024; // maximum number of total rays in a launch
2986
2987 // ***** DIRECT LAUNCH FROM ALL RADIATION SOURCES ***** //
2988
2989 helios::int3 launch_dim_dir;
2990
2991 bool rundirect = false;
2992 for (uint s = 0; s < Nsources; s++) {
2993 for (uint b = 0; b < Nbands_launch; b++) {
2994 if (getSourceFlux(s, band_labels.at(b)) > 0.f) {
2995 rundirect = true;
2996 break;
2997 }
2998 }
2999 }
3000
3001 if (Nsources > 0 && rundirect) {
3002
3003 // update radiation source buffers
3004
3005 std::vector<std::vector<float>> fluxes; // first index is the source, second index is the band (only those passed to runBand() function)
3006 fluxes.resize(Nsources);
3007 std::vector<helios::vec3> positions(Nsources);
3008 std::vector<helios::vec2> widths(Nsources);
3009 std::vector<helios::vec3> rotations(Nsources);
3010 std::vector<uint> types(Nsources);
3011
3012 size_t s = 0;
3013 for (const auto &source: radiation_sources) {
3014
3015 fluxes.at(s).resize(Nbands_launch);
3016
3017 for (auto b = 0; b < label.size(); b++) {
3018 fluxes.at(s).at(b) = getSourceFlux(s, band_labels.at(b));
3019 }
3020
3021 positions.at(s) = helios::make_vec3(source.source_position.x, source.source_position.y, source.source_position.z);
3022 widths.at(s) = helios::make_vec2(source.source_width.x, source.source_width.y);
3023 rotations.at(s) = helios::make_vec3(source.source_rotation.x, source.source_rotation.y, source.source_rotation.z);
3024 types.at(s) = source.source_type;
3025
3026 s++;
3027 }
3028
3029 // Upload band-specific source fluxes to backend buffer (indexed by Nbands_launch, not Nbands_global)
3030 backend->uploadSourceFluxes(flatten(fluxes));
3031 // Note: positions, widths, rotations, types are uploaded once in buildSourceData()
3032 // Only fluxes need per-launch update because they depend on which bands are being run
3033
3034 // Compute camera response weighting factors for specular reflection (if cameras exist)
3035 // Factor = ∫(source_spectrum × camera_response) / ∫(source_spectrum)
3036 // This must be done before ray tracing so the weights are available during miss_direct()
3037 if (Ncameras > 0) {
3038 std::vector<float> source_fluxes_cam;
3039 source_fluxes_cam.resize(Nsources * Nbands_launch * Ncameras, 1.0f);
3040
3041 for (uint s = 0; s < Nsources; s++) {
3042 const RadiationSource &source = radiation_sources.at(s);
3043
3044 uint cam = 0;
3045 for (const auto &camera: cameras) {
3046 for (uint b = 0; b < Nbands_launch; b++) {
3047 std::string band_label = band_labels.at(b);
3048
3049 // Default weighting factor (no camera response)
3050 float weight = 1.0f;
3051
3052 // Check if camera has spectral response for this band
3053 if (camera.second.band_spectral_response.find(band_label) != camera.second.band_spectral_response.end()) {
3054 std::string response_label = camera.second.band_spectral_response.at(band_label);
3055
3056 if (!response_label.empty() && response_label != "uniform" && context->doesGlobalDataExist(response_label.c_str()) && context->getGlobalDataType(response_label.c_str()) == helios::HELIOS_TYPE_VEC2 &&
3057 source.source_spectrum.size() > 0) {
3058
3059 // Load camera spectral response
3060 std::vector<helios::vec2> camera_response;
3061 context->getGlobalData(response_label.c_str(), camera_response);
3062
3063 // Get band wavelength range
3064 helios::vec2 wavelength_range = radiation_bands.at(band_label).wavebandBounds;
3065
3066 // If no wavelength bounds, use overlapping range of source and camera
3067 if (wavelength_range.x == 0 && wavelength_range.y == 0) {
3068 wavelength_range.x = fmax(source.source_spectrum.front().x, camera_response.front().x);
3069 wavelength_range.y = fmin(source.source_spectrum.back().x, camera_response.back().x);
3070 }
3071
3072 // Integrate source_spectrum × camera_response over band
3073 // Note: integrateSpectrum already returns ratio: ∫(source × camera) / ∫(source)
3074 weight = integrateSpectrum(s, camera_response, wavelength_range.x, wavelength_range.y);
3075 }
3076 }
3077
3078 source_fluxes_cam[s * Nbands_launch * Ncameras + b * Ncameras + cam] = weight;
3079 }
3080 cam++;
3081 }
3082 }
3083
3084 // Update source_data with camera-weighted fluxes and re-upload to backend
3085 for (uint s = 0; s < Nsources; s++) {
3086 source_data[s].fluxes_cam.clear();
3087 for (uint b = 0; b < Nbands_launch; b++) {
3088 for (uint cam = 0; cam < Ncameras; cam++) {
3089 source_data[s].fluxes_cam.push_back(source_fluxes_cam[s * Nbands_launch * Ncameras + b * Ncameras + cam]);
3090 }
3091 }
3092 }
3093 backend->updateSources(source_data);
3094 }
3095
3096 // -- Ray Trace (Using Backend) -- //
3097
3098 if (message_flag) {
3099 std::cout << "Performing primary direct radiation ray trace for bands ";
3100 for (const auto &band: label) {
3101 std::cout << band << ", ";
3102 }
3103 std::cout << "..." << std::flush;
3104 }
3105
3106 // Launch direct rays through backend
3108 params.launch_offset = 0;
3109 params.launch_count = Nprimitives; // Launch all primitives at once
3110 params.rays_per_primitive = directRayCount;
3111 params.random_seed = std::chrono::system_clock::now().time_since_epoch().count();
3112 params.num_bands_global = Nbands_global;
3113 params.num_bands_launch = Nbands_launch;
3114 params.specular_reflection_enabled = false;
3115
3116 // Use the band_launch_flag already built above (lines 3375-3383)
3117 std::vector<bool> band_flags(band_launch_flag.begin(), band_launch_flag.end());
3118 params.band_launch_flag = band_flags;
3119
3120 backend->launchDirectRays(params);
3121
3122 if (message_flag) {
3123 std::cout << "done." << std::endl;
3124 }
3125
3126 } // end direct source launch
3127
3128 // --- Extract scattered energy from direct rays for diffuse/emission and scattering ---//
3129 // This needs to happen BEFORE diffuse/emission block so scattered direct energy
3130 // is available for both diffuse/emission (via flux_top/flux_bottom) and scattering (via radiation_out)
3131 std::vector<float> flux_top, flux_bottom;
3132 flux_top.resize(Nbands_launch * Nprimitives, 0);
3133 flux_bottom = flux_top;
3134
3135 // Camera scatter accumulation vectors (declare early for use throughout ray tracing)
3136 std::vector<float> scatter_top_cam;
3137 std::vector<float> scatter_bottom_cam;
3138 if (Ncameras > 0) {
3139 scatter_top_cam.resize(Nprimitives * Nbands_launch, 0.0f);
3140 scatter_bottom_cam.resize(Nprimitives * Nbands_launch, 0.0f);
3141 }
3142
3143 if (scatteringenabled && rundirect) {
3144 // Get scattered energy from direct rays for primary diffuse/emission
3145 helios::RayTracingResults scatter_results;
3146 backend->getRadiationResults(scatter_results);
3147 flux_top = scatter_results.scatter_buff_top;
3148 flux_bottom = scatter_results.scatter_buff_bottom;
3149
3150 // Accumulate camera scatter from direct rays
3151 if (Ncameras > 0) {
3152 for (size_t i = 0; i < scatter_results.scatter_buff_top_cam.size(); i++) {
3153 scatter_top_cam[i] += scatter_results.scatter_buff_top_cam[i];
3154 scatter_bottom_cam[i] += scatter_results.scatter_buff_bottom_cam[i];
3155 }
3156 // Zero GPU camera scatter buffers to prevent double-counting on next iteration
3157 backend->zeroCameraScatterBuffers(Nbands_launch);
3158 }
3159
3160 // For one-sided primitives, make scattered energy accessible from both faces
3161 // This is necessary because scattering rays can hit from either direction
3162 RadiationBufferIndexer rad_indexer(Nprimitives, Nbands_launch);
3163
3164 for (size_t i = 0; i < Nprimitives; i++) {
3165 uint UUID = context_UUIDs.at(i);
3166 uint twosided = context->getPrimitiveTwosidedFlag(UUID, 1);
3167
3168 if (twosided == 0) { // one-sided primitive - combine top+bottom scattered energy
3169 for (size_t b = 0; b < Nbands_launch; b++) {
3170 size_t ind = rad_indexer(i, b);
3171 float total = flux_top[ind] + flux_bottom[ind];
3172 flux_top[ind] = total;
3173 flux_bottom[ind] = total;
3174 }
3175 }
3176 }
3177
3178 // Upload scattered energy to backend's radiation_out buffers for scattering iterations
3179 backend->uploadRadiationOut(flux_top, flux_bottom);
3180 backend->zeroScatterBuffers();
3181 }
3182
3183 // --- Diffuse/Emission launch ---- //
3184
3185 if (emissionenabled || diffuseenabled) {
3186
3187 // add any emitted energy to the outgoing energy buffer
3188 if (emissionenabled) {
3189 // Update primitive outgoing emission
3190 float eps, temperature;
3191
3192 // Create indexer for emission flux buffers
3193 RadiationBufferIndexer emission_indexer(Nprimitives, Nbands_launch);
3194
3195 for (auto b = 0; b < Nbands_launch; b++) {
3196 //\todo For emissivity and twosided_flag, this should be done in updateRadiativeProperties() to avoid having to do it on every runBand() call
3197 if (radiation_bands.at(band_labels.at(b)).emissionFlag) {
3198 std::string prop = "emissivity_" + band_labels.at(b);
3199 for (size_t u = 0; u < Nprimitives; u++) {
3200 // Use BufferIndexer: [primitive][band]
3201 size_t ind = emission_indexer(u, b);
3202 uint p = context_UUIDs.at(u);
3203 if (context->doesPrimitiveDataExist(p, prop.c_str())) {
3204 context->getPrimitiveData(p, prop.c_str(), eps);
3205 } else {
3206 eps = eps_default;
3207 }
3208 if (scattering_depth.at(b) == 0 && eps != 1.f) {
3209 eps = 1.f;
3210 }
3211 if (context->doesPrimitiveDataExist(p, "temperature")) {
3212 context->getPrimitiveData(p, "temperature", temperature);
3213 if (temperature < 0) {
3214 temperature = temperature_default;
3215 }
3216 } else {
3217 temperature = temperature_default;
3218 }
3219 float out_top = sigma * eps * pow(temperature, 4);
3220 flux_top.at(ind) += out_top;
3221 if (Ncameras > 0) {
3222 scatter_top_cam[ind] += out_top;
3223 }
3224 // Check twosided_flag - check material first, then primitive data
3225 uint twosided_flag = context->getPrimitiveTwosidedFlag(p, 1);
3226 if (twosided_flag != 0) { // If two-sided, emit from bottom face too
3227 flux_bottom.at(ind) += flux_top.at(ind);
3228 if (Ncameras > 0) {
3229 scatter_bottom_cam[ind] += out_top;
3230 }
3231 }
3232 }
3233 }
3234 }
3235 }
3236
3237 // Upload camera scatter buffers accumulated from emission, direct rays, and primary diffuse
3238 // Camera scatter is accumulated on CPU from GPU after each ray launch
3239 if (Ncameras > 0) {
3240 backend->uploadCameraScatterBuffers(scatter_top_cam, scatter_bottom_cam);
3241 }
3242
3243 // Note: radiation_specular_RTbuffer is populated on GPU via atomicFloatAdd during ray tracing, don't overwrite it here
3244
3245 // Compute diffuse launch dimension
3246 size_t n = ceil(sqrt(double(diffuseRayCount)));
3247 uint rays_per_primitive = n * n;
3248
3249 size_t maxPrims = floor(float(maxRays) / float(rays_per_primitive));
3250
3251 int Nlaunches = ceil(rays_per_primitive * Nprimitives / float(maxRays));
3252
3253 size_t prims_per_launch = fmin(Nprimitives, maxPrims);
3254
3255 for (uint launch = 0; launch < Nlaunches; launch++) {
3256
3257 size_t prims_this_launch;
3258 if ((launch + 1) * prims_per_launch > Nprimitives) {
3259 prims_this_launch = Nprimitives - launch * prims_per_launch;
3260 } else {
3261 prims_this_launch = prims_per_launch;
3262 }
3263
3264 if (message_flag) {
3265 std::cout << "Performing primary diffuse radiation ray trace for bands ";
3266 for (const auto &band: label) {
3267 std::cout << band << " ";
3268 }
3269 std::cout << " (batch " << launch + 1 << " of " << Nlaunches << ")..." << std::flush;
3270 }
3271
3272 // Build launch parameters for diffuse rays
3274 params.launch_offset = launch * prims_per_launch;
3275 params.launch_count = prims_this_launch;
3276 params.rays_per_primitive = rays_per_primitive;
3277 params.random_seed = std::chrono::system_clock::now().time_since_epoch().count();
3278 params.current_band = 0;
3279 params.num_bands_global = Nbands_global;
3280 params.num_bands_launch = Nbands_launch;
3281 std::vector<bool> band_flags(band_launch_flag.begin(), band_launch_flag.end());
3282 params.band_launch_flag = band_flags;
3283 params.scattering_iteration = 0;
3284 params.max_scatters = scatteringDepth;
3285 params.radiation_out_top = flux_top;
3286 params.radiation_out_bottom = flux_bottom;
3287
3288 // Pass diffuse radiation parameters to backend
3289 params.diffuse_flux = diffuse_flux;
3290 params.diffuse_extinction = diffuse_extinction;
3291 params.diffuse_dist_norm = diffuse_dist_norm;
3292 // Convert helios::vec3 to helios::vec3
3293 std::vector<helios::vec3> peak_dirs(diffuse_peak_dir.size());
3294 for (size_t i = 0; i < diffuse_peak_dir.size(); i++) {
3295 peak_dirs[i] = helios::make_vec3(diffuse_peak_dir[i].x, diffuse_peak_dir[i].y, diffuse_peak_dir[i].z);
3296 }
3297 params.diffuse_peak_dir = peak_dirs;
3298
3299 // Top surface launch
3300 params.launch_face = 1;
3301 backend->launchDiffuseRays(params);
3302
3303 // Bottom surface launch
3304 params.launch_face = 0;
3305 backend->launchDiffuseRays(params);
3306
3307 // Retrieve and accumulate camera scatter from primary diffuse
3308 if (Ncameras > 0) {
3309 helios::RayTracingResults primary_results;
3310 backend->getRadiationResults(primary_results);
3311 for (size_t i = 0; i < primary_results.scatter_buff_top_cam.size(); i++) {
3312 scatter_top_cam[i] += primary_results.scatter_buff_top_cam[i];
3313 scatter_bottom_cam[i] += primary_results.scatter_buff_bottom_cam[i];
3314 }
3315 // Zero GPU camera scatter buffers to prevent double-counting on next iteration
3316 backend->zeroCameraScatterBuffers(Nbands_launch);
3317 }
3318
3319 if (message_flag) {
3320 std::cout << "\r \r" << std::flush;
3321 }
3322 }
3323
3324 if (message_flag) {
3325 std::cout << "Performing primary diffuse radiation ray trace for bands ";
3326 for (const auto &band: label) {
3327 std::cout << band << ", ";
3328 }
3329 std::cout << "...done." << std::endl;
3330 }
3331 }
3332
3333 // After primary diffuse, prepare scatter_buff for scattering iterations
3334 // When direct rays ran, scatter_buff was already copied to radiation_out at line 3710
3335 // For emission/diffuse without direct rays, we need to do this now
3336 if (scatteringenabled && (emissionenabled || diffuseenabled) && !rundirect) {
3337 backend->copyScatterToRadiation();
3338 backend->zeroScatterBuffers();
3339 }
3340
3341 if (scatteringenabled && (emissionenabled || diffuseenabled || rundirect)) {
3342
3343 for (auto b = 0; b < Nbands_launch; b++) {
3344 diffuse_flux.at(b) = 0.f;
3345 }
3346 // NOTE: diffuse_flux zeroed for scattering, passed to backend via launch params
3347
3348 size_t n = ceil(sqrt(double(diffuseRayCount)));
3349 uint rays_per_primitive = n * n;
3350
3351 size_t maxPrims = floor(float(maxRays) / float(rays_per_primitive));
3352
3353 int Nlaunches = ceil(n * n * Nobjects / float(maxRays));
3354
3355 size_t prims_per_launch = fmin(Nobjects, maxPrims);
3356
3357 uint s;
3358 // FIX: Use a copy of band_launch_flag for scattering so modifications don't affect primary launch indices
3359 std::vector<char> scatter_band_flags = band_launch_flag;
3360
3361 for (s = 0; s < scatteringDepth; s++) {
3362 if (message_flag) {
3363 std::cout << "Performing scattering ray trace (iteration " << s + 1 << " of " << scatteringDepth << ")..." << std::flush;
3364 }
3365
3366 int b = -1;
3367 int active_bands = 0;
3368 for (uint b_global = 0; b_global < Nbands_global; b_global++) {
3369
3370 if (scatter_band_flags.at(b_global) == 0) {
3371 continue;
3372 }
3373 b++;
3374
3375 uint depth = radiation_bands.at(band_labels.at(b)).scatteringDepth;
3376 if (s + 1 > depth) {
3377 if (message_flag) {
3378 std::cout << "Skipping band " << band_labels.at(b) << " for scattering launch " << s + 1 << std::flush;
3379 }
3380 scatter_band_flags.at(b_global) = 0; // FIX: Modify copy, not original
3381 } else {
3382 active_bands++;
3383 }
3384 }
3385
3386 // Copy scatter buffers to radiation_out when needed
3387 // For s=0 with emission+direct: primary diffuse uploaded emission+scatter via params, but we need to copy scatter to avoid double-counting emission on next iteration
3388 // For s>0: scatter from previous iteration needs to be copied for next iteration
3389 if (s > 0 || (emissionenabled && rundirect)) {
3390 backend->copyScatterToRadiation();
3391 }
3392 backend->zeroScatterBuffers();
3393
3394 // Extract radiation_out to ensure it's uploaded for scattering rays
3395 helios::RayTracingResults scatter_results;
3396 backend->getRadiationResults(scatter_results);
3397 std::vector<float> flux_top_scatter = scatter_results.radiation_out_top;
3398 std::vector<float> flux_bottom_scatter = scatter_results.radiation_out_bottom;
3399
3400 for (uint launch = 0; launch < Nlaunches; launch++) {
3401
3402 size_t prims_this_launch;
3403 if ((launch + 1) * prims_per_launch > Nprimitives) {
3404 prims_this_launch = Nprimitives - launch * prims_per_launch;
3405 } else {
3406 prims_this_launch = prims_per_launch;
3407 }
3408
3409 // Build launch parameters for scattering diffuse rays
3411 params.launch_offset = launch * prims_per_launch;
3412 params.launch_count = prims_this_launch;
3413 params.rays_per_primitive = rays_per_primitive;
3414 params.random_seed = std::chrono::system_clock::now().time_since_epoch().count();
3415 params.current_band = 0;
3416 params.num_bands_global = Nbands_global;
3417 params.num_bands_launch = Nbands_launch;
3418 std::vector<bool> band_flags(scatter_band_flags.begin(), scatter_band_flags.end()); // FIX: Use scatter copy
3419 params.band_launch_flag = band_flags;
3420 params.scattering_iteration = s;
3421 params.max_scatters = scatteringDepth;
3422
3423 // Pass diffuse radiation parameters to backend
3424 params.diffuse_flux = diffuse_flux;
3425 params.diffuse_extinction = diffuse_extinction;
3426 params.diffuse_dist_norm = diffuse_dist_norm;
3427 // Convert helios::vec3 to helios::vec3
3428 std::vector<helios::vec3> peak_dirs(diffuse_peak_dir.size());
3429 for (size_t i = 0; i < diffuse_peak_dir.size(); i++) {
3430 peak_dirs[i] = helios::make_vec3(diffuse_peak_dir[i].x, diffuse_peak_dir[i].y, diffuse_peak_dir[i].z);
3431 }
3432 params.diffuse_peak_dir = peak_dirs;
3433
3434 // Set radiation_out for scattering rays
3435 params.radiation_out_top = flux_top_scatter;
3436 params.radiation_out_bottom = flux_bottom_scatter;
3437
3438 // Top surface launch
3439 params.launch_face = 1;
3440 backend->launchDiffuseRays(params);
3441
3442
3443 // Bottom surface launch
3444 params.launch_face = 0;
3445 backend->launchDiffuseRays(params);
3446 }
3447
3448 // Accumulate camera scatter from this scattering iteration
3449 if (Ncameras > 0) {
3450 helios::RayTracingResults post_launch;
3451 backend->getRadiationResults(post_launch);
3452 for (size_t i = 0; i < post_launch.scatter_buff_top_cam.size(); i++) {
3453 scatter_top_cam[i] += post_launch.scatter_buff_top_cam[i];
3454 scatter_bottom_cam[i] += post_launch.scatter_buff_bottom_cam[i];
3455 }
3456 // Zero GPU camera scatter buffers to prevent double-counting on next iteration
3457 backend->zeroCameraScatterBuffers(Nbands_launch);
3458 }
3459
3460 if (message_flag) {
3461 std::cout << "\r \r" << std::flush;
3462 }
3463 }
3464
3465 if (message_flag) {
3466 std::cout << "Performing scattering ray trace...done." << std::endl;
3467 }
3468 }
3469
3470 // **** CAMERA RAY TRACE **** //
3471 if (Ncameras > 0) {
3472
3473 // Upload accumulated camera scatter to radiation_out for cameras to read
3474 // scatter_top_cam contains camera-weighted scattered energy from all ray types
3475 // Cameras read from radiation_out during hits, so we upload camera scatter there
3476 if (Ncameras > 0 && scatteringenabled) {
3477 backend->uploadRadiationOut(scatter_top_cam, scatter_bottom_cam);
3478 }
3479
3480 // Setup solar disk rendering for cameras (enables lens flare effects)
3481 // Find sun-like sources (collimated or sun_sphere) and compute solar disk radiance
3482 vec3 sun_dir(0, 0, 1); // Default zenith
3483 std::vector<float> solar_radiances(Nbands_launch, 0.0f);
3484 bool has_sun_source = false;
3485
3486 for (size_t s = 0; s < radiation_sources.size(); s++) {
3487 const RadiationSource &source = radiation_sources.at(s);
3488 if (source.source_type == RADIATION_SOURCE_TYPE_COLLIMATED || source.source_type == RADIATION_SOURCE_TYPE_SUN_SPHERE) {
3489 // Get sun direction from source position (normalized)
3490 sun_dir = source.source_position;
3491 sun_dir.normalize();
3492 has_sun_source = true;
3493
3494 // Compute solar disk radiance for each band
3495 for (size_t b = 0; b < Nbands_launch; b++) {
3496 float flux = getSourceFlux(s, band_labels.at(b));
3497
3498 if (source.source_type == RADIATION_SOURCE_TYPE_SUN_SPHERE) {
3499 // For sun sphere: flux is surface exitance (σT⁴)
3500 // Radiance as seen from Earth: L = F_surface / π
3501 solar_radiances[b] = flux / M_PI;
3502 } else {
3503 // For collimated: flux is irradiance at Earth
3504 // Solar solid angle: π × (4.63e-3)² ≈ 6.74×10⁻⁵ sr
3505 const float solar_solid_angle = 6.74e-5f;
3506 solar_radiances[b] = flux / solar_solid_angle;
3507 }
3508 }
3509 break; // Use first sun-like source found
3510 }
3511 }
3512
3513 if (scatteringenabled && (emissionenabled || diffuseenabled || rundirect)) {
3514 // re-set diffuse radiation fluxes (will be passed via launch params)
3515 if (diffuseenabled) {
3516 for (auto b = 0; b < Nbands_launch; b++) {
3517 diffuse_flux.at(b) = getDiffuseFlux(band_labels.at(b));
3518 }
3519 }
3520
3521 size_t n = ceil(sqrt(double(diffuseRayCount)));
3522
3523 // Upload sky model parameters to backend (for camera rendering)
3524
3525 if (!cameras.empty() && prague_params.size() == Nbands_launch) {
3526 // Get sky radiances for first camera (already computed above)
3527 std::vector<float> sky_for_backend = updateAtmosphericSkyModel(band_labels, cameras.begin()->second);
3528
3529 // Upload to backend
3530 backend->updateSkyModel(prague_params, sky_for_backend, sun_dir, solar_radiances,
3531 has_sun_source ? 0.999989f : 0.0f // solar_disk_cos_angle
3532 );
3533 }
3534
3535 uint cam = 0;
3536 for (auto &camera: cameras) {
3537
3538
3539 // Validate antialiasing samples don't exceed maximum
3540 if (camera.second.antialiasing_samples > maxRays) {
3541 helios_runtime_error("ERROR (runBand): Camera '" + camera.second.label + "' antialiasing samples (" + std::to_string(camera.second.antialiasing_samples) + ") exceeds OptiX maximum launch size (" + std::to_string(maxRays) +
3542 "). Reduce antialiasing samples.");
3543 }
3544
3545 // Compute tiling if needed
3546 std::vector<CameraTile> tiles = computeCameraTiles(camera.second, maxRays);
3547
3548 if (message_flag && tiles.size() > 1) {
3549 std::cout << "Camera '" << camera.second.label << "' requires " << tiles.size() << " tiles" << std::endl;
3550 }
3551
3552 // Launch camera rays (tiled or full)
3553 for (size_t tile_idx = 0; tile_idx < tiles.size(); tile_idx++) {
3554 const auto &tile = tiles[tile_idx];
3555
3556 // Build params for this tile
3557 helios::RayTracingLaunchParams params = buildCameraLaunchParams(camera.second, cam, camera.second.antialiasing_samples, tile.resolution, tile.offset);
3558
3559 // Set band parameters (CRITICAL for materials!)
3560 params.num_bands_launch = Nbands_launch;
3561 params.num_bands_global = Nbands_global;
3562 params.random_seed = std::chrono::system_clock::now().time_since_epoch().count();
3563 std::vector<bool> band_flags(band_launch_flag.begin(), band_launch_flag.end());
3564 params.band_launch_flag = band_flags;
3565
3566 // Progress message
3567 if (message_flag) {
3568 if (tiles.size() == 1) {
3569 std::cout << "Performing scattering radiation camera ray trace for camera " << camera.second.label << "..." << std::flush;
3570 } else {
3571 std::cout << "Performing scattering radiation camera ray trace for camera " << camera.second.label << " (tile " << (tile_idx + 1) << " of " << tiles.size() << ")..." << std::flush;
3572 }
3573 }
3574
3575 // Launch through backend
3576 backend->launchCameraRays(params);
3577
3578 if (message_flag) {
3579 if (tiles.size() > 1) {
3580 std::cout << "\r" << std::string(120, ' ') << "\r" << std::flush;
3581 } else {
3582 std::cout << "done." << std::endl;
3583 }
3584 }
3585 }
3586
3587 if (message_flag && tiles.size() > 1) {
3588 std::cout << "Performing scattering radiation camera ray trace for camera " << camera.second.label << "...done." << std::endl;
3589 }
3590
3591 // Get results from backend
3592 std::vector<float> radiation_camera;
3593 std::vector<uint> dummy_labels;
3594 std::vector<float> dummy_depths;
3595 backend->getCameraResults(radiation_camera, dummy_labels, dummy_depths, cam, camera.second.resolution);
3596
3597 // Process pixel data (KEEP EXISTING LOGIC)
3598 std::string camera_label = camera.second.label;
3599
3600 for (auto b = 0; b < Nbands_launch; b++) {
3601
3602 camera.second.pixel_data[band_labels.at(b)].resize(camera.second.resolution.x * camera.second.resolution.y);
3603
3604 std::string data_label = "camera_" + camera_label + "_" + band_labels.at(b);
3605
3606 for (auto p = 0; p < camera.second.resolution.x * camera.second.resolution.y; p++) {
3607 camera.second.pixel_data.at(band_labels.at(b)).at(p) = radiation_camera.at(p * Nbands_launch + b);
3608 }
3609
3610 context->setGlobalData(data_label.c_str(), camera.second.pixel_data.at(band_labels.at(b)));
3611 }
3612
3613 //--- Pixel Labeling Trace ---//
3614
3615 // Compute tiling for pixel labeling (no antialiasing, 1 ray per pixel)
3616 RadiationCamera pixel_label_camera = camera.second;
3617 pixel_label_camera.antialiasing_samples = 1;
3618 std::vector<CameraTile> pixel_tiles = computeCameraTiles(pixel_label_camera, maxRays);
3619
3620 // Zero camera pixel buffers once before tile loop
3621 backend->zeroCameraPixelBuffers(camera.second.resolution);
3622
3623 // Launch pixel label rays (tiled or full)
3624 for (size_t tile_idx = 0; tile_idx < pixel_tiles.size(); tile_idx++) {
3625 const auto &tile = pixel_tiles[tile_idx];
3626
3627 // Build params (reuse buildCameraLaunchParams, antialiasing=1)
3628 helios::RayTracingLaunchParams params = buildCameraLaunchParams(pixel_label_camera, cam,
3629 1, // No antialiasing for pixel labeling
3630 tile.resolution, tile.offset);
3631
3632 // Progress message
3633 if (message_flag) {
3634 if (pixel_tiles.size() == 1) {
3635 std::cout << "Performing camera pixel labeling ray trace for camera " << camera.second.label << "..." << std::flush;
3636 } else {
3637 std::cout << "Performing camera pixel labeling ray trace for camera " << camera.second.label << " (tile " << (tile_idx + 1) << " of " << pixel_tiles.size() << ")..." << std::flush;
3638 }
3639 }
3640
3641 // Launch through backend
3642 backend->launchPixelLabelRays(params);
3643
3644 if (message_flag) {
3645 if (pixel_tiles.size() > 1) {
3646 std::cout << "\r" << std::string(120, ' ') << "\r" << std::flush;
3647 } else {
3648 std::cout << "done." << std::endl;
3649 }
3650 }
3651 }
3652
3653 if (message_flag && pixel_tiles.size() > 1) {
3654 std::cout << "Performing camera pixel labeling ray trace for camera " << camera.second.label << "...done." << std::endl;
3655 }
3656
3657 // Get pixel label results
3658 std::vector<float> dummy_pixel_data;
3659 backend->getCameraResults(dummy_pixel_data, camera.second.pixel_label_UUID, camera.second.pixel_depth, cam, camera.second.resolution);
3660
3661 // Convert IDs to actual UUIDs
3662 // Pixel labels contain position+1 (1-indexed), need to convert to UUIDs
3663 for (uint ID = 0; ID < camera.second.pixel_label_UUID.size(); ID++) {
3664 if (camera.second.pixel_label_UUID.at(ID) > 0) {
3665 uint position = camera.second.pixel_label_UUID.at(ID) - 1; // Convert to 0-indexed position
3666
3667 // Check if this is a bbox hit (position >= primitive_count)
3668 if (position >= context_UUIDs.size()) {
3669 // Bbox: calculate UUID directly (bbox_UUID = bbox_UUID_base + bbox_index)
3670 uint bbox_index = position - context_UUIDs.size();
3671 uint bbox_UUID = geometry_data.bbox_UUID_base + bbox_index;
3672 camera.second.pixel_label_UUID.at(ID) = bbox_UUID + 1; // Store as 1-indexed
3673 } else {
3674 // Real primitive: look up UUID from context_UUIDs
3675 camera.second.pixel_label_UUID.at(ID) = context_UUIDs.at(position) + 1;
3676 }
3677 }
3678 }
3679
3680 // Store results in context (KEEP EXISTING LOGIC)
3681 std::string data_label = "camera_" + camera_label + "_pixel_UUID";
3682 context->setGlobalData(data_label.c_str(), camera.second.pixel_label_UUID);
3683
3684 data_label = "camera_" + camera_label + "_pixel_depth";
3685 context->setGlobalData(data_label.c_str(), camera.second.pixel_depth);
3686
3687 cam++;
3688 }
3689 } else {
3690 // if scattering is not enabled or all sources have zero flux, we still need to zero the camera buffers
3691 for (auto &camera: cameras) {
3692 for (auto b = 0; b < Nbands_launch; b++) {
3693 camera.second.pixel_data[band_labels.at(b)].resize(camera.second.resolution.x * camera.second.resolution.y);
3694
3695 std::string data_label = "camera_" + camera.second.label + "_" + band_labels.at(b);
3696
3697 for (auto p = 0; p < camera.second.resolution.x * camera.second.resolution.y; p++) {
3698 camera.second.pixel_data.at(band_labels.at(b)).at(p) = 0.f;
3699 }
3700 context->setGlobalData(data_label.c_str(), camera.second.pixel_data.at(band_labels.at(b)));
3701 }
3702 }
3703 }
3704 }
3705
3706 // Apply camera exposure based on each camera's exposure setting
3707 for (auto &camera: cameras) {
3708 camera.second.applyCameraExposure(context);
3709 }
3710
3711 // Apply camera white balance based on each camera's white_balance setting
3712 for (auto &camera: cameras) {
3713 camera.second.applyCameraWhiteBalance(context);
3714 }
3715
3716 // deposit any energy that is left to make sure we satisfy conservation of energy
3717
3718 // Extract ALL results from backend instead of old OptiX buffers
3720 backend->getRadiationResults(results);
3721
3722 std::vector<float> radiation_flux_data = results.radiation_in;
3723
3724 // Extract scatter buffer data from backend results
3725 TBS_top = results.scatter_buff_top;
3726 TBS_bottom = results.scatter_buff_bottom;
3727
3728 std::vector<uint> UUIDs_context_all = context->getAllUUIDs();
3729
3730 // Create indexer for result extraction
3731 RadiationBufferIndexer result_indexer(Nprimitives, Nbands_launch);
3732
3733 for (auto b = 0; b < Nbands_launch; b++) {
3734
3735 std::string prop = "radiation_flux_" + band_labels.at(b);
3736 std::vector<float> R(Nprimitives);
3737 for (size_t u = 0; u < Nprimitives; u++) {
3738 // Use BufferIndexer: [primitive][band]
3739 size_t ind = result_indexer(u, b);
3740 R.at(u) = radiation_flux_data.at(ind) + TBS_top.at(ind) + TBS_bottom.at(ind);
3741 }
3742 context->setPrimitiveData(context_UUIDs, prop.c_str(), R);
3743
3744 if (UUIDs_context_all.size() != Nprimitives) {
3745 for (uint UUID: UUIDs_context_all) {
3746 if (context->doesPrimitiveExist(UUID) && !context->doesPrimitiveDataExist(UUID, prop.c_str())) {
3747 context->setPrimitiveData(UUID, prop.c_str(), 0.f);
3748 }
3749 }
3750 }
3751 }
3752}
3753
3755
3757 backend->getRadiationResults(results);
3758
3759 float Rsky = 0.f;
3760 for (size_t i = 0; i < results.sky_energy.size(); i++) {
3761 Rsky += results.sky_energy.at(i);
3762 }
3763 return Rsky;
3764}
3765
3767
3768 std::vector<float> total_flux;
3769 total_flux.resize(context->getPrimitiveCount(), 0.f);
3770
3771 for (const auto &band: radiation_bands) {
3772
3773 std::string label = band.first;
3774
3775 for (size_t u = 0; u < context_UUIDs.size(); u++) {
3776
3777 uint p = context_UUIDs.at(u);
3778
3779 std::string str = "radiation_flux_" + label;
3780
3781 float R;
3782 context->getPrimitiveData(p, str.c_str(), R);
3783 total_flux.at(u) += R;
3784 }
3785 }
3786
3787 return total_flux;
3788}
3789
3790
3792
3793 vec3 dir = view_direction;
3794 dir.normalize();
3795
3796 float Gtheta = 0;
3797 float total_area = 0;
3798 for (std::size_t u = 0; u < primitiveID.size(); u++) {
3799
3800 uint UUID = context_UUIDs.at(primitiveID.at(u));
3801
3802 vec3 normal = context->getPrimitiveNormal(UUID);
3803 float area = context->getPrimitiveArea(UUID);
3804
3805 Gtheta += fabsf(normal * dir) * area;
3806
3807 total_area += area;
3808 }
3809
3810 return Gtheta / total_area;
3811}
3812
3813void RadiationModel::exportColorCorrectionMatrixXML(const std::string &file_path, const std::string &camera_label, const std::vector<std::vector<float>> &matrix, const std::string &source_image_path, const std::string &colorboard_type,
3814 float average_delta_e) {
3815
3816 std::ofstream file(file_path);
3817 if (!file.is_open()) {
3818 helios_runtime_error("ERROR (RadiationModel::exportColorCorrectionMatrixXML): Failed to open file for writing: " + file_path);
3819 }
3820
3821 // Determine matrix type (3x3 or 4x3)
3822 std::string matrix_type = "3x3";
3823 if (matrix.size() == 4 || (matrix.size() >= 3 && matrix[0].size() == 4)) {
3824 matrix_type = "4x3";
3825 }
3826
3827 // Write XML header with informative comments
3828 file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
3829 file << "<!-- Camera Color Correction Matrix -->" << std::endl;
3830 file << "<!-- Source Image: " << source_image_path << " -->" << std::endl;
3831 file << "<!-- Camera Label: " << camera_label << " -->" << std::endl;
3832 file << "<!-- Colorboard Type: " << colorboard_type << " -->" << std::endl;
3833 if (average_delta_e >= 0.0f) {
3834 file << "<!-- Average Delta E: " << std::fixed << std::setprecision(2) << average_delta_e << " -->" << std::endl;
3835 }
3836 file << "<!-- Matrix Type: " << matrix_type << " -->" << std::endl;
3837 file << "<!-- Generated: " << getCurrentDateTime() << " -->" << std::endl;
3838
3839 // Write matrix data
3840 file << "<helios>" << std::endl;
3841 file << " <ColorCorrectionMatrix camera_label=\"" << camera_label << "\" matrix_type=\"" << matrix_type << "\">" << std::endl;
3842
3843 for (size_t i = 0; i < matrix.size(); i++) {
3844 file << " <row>";
3845 for (size_t j = 0; j < matrix[i].size(); j++) {
3846 file << std::fixed << std::setprecision(6) << matrix[i][j];
3847 if (j < matrix[i].size() - 1) {
3848 file << " ";
3849 }
3850 }
3851 file << "</row>" << std::endl;
3852 }
3853
3854 file << " </ColorCorrectionMatrix>" << std::endl;
3855 file << "</helios>" << std::endl;
3856
3857 file.close();
3858}
3859
3860std::string RadiationModel::getCurrentDateTime() {
3861 auto now = std::time(nullptr);
3862 auto tm = *std::localtime(&now);
3863 std::stringstream ss;
3864 ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
3865 return ss.str();
3866}
3867
3868std::vector<std::vector<float>> RadiationModel::loadColorCorrectionMatrixXML(const std::string &file_path, std::string &camera_label_out) {
3869
3870 std::ifstream file(file_path);
3871 if (!file.is_open()) {
3872 helios_runtime_error("ERROR (RadiationModel::loadColorCorrectionMatrixXML): Failed to open file for reading: " + file_path);
3873 }
3874
3875 std::vector<std::vector<float>> matrix;
3876 std::string line;
3877 bool in_matrix = false;
3878 std::string matrix_type = "";
3879
3880 while (std::getline(file, line)) {
3881 // Remove leading/trailing whitespace
3882 line.erase(0, line.find_first_not_of(" \t"));
3883 line.erase(line.find_last_not_of(" \t") + 1);
3884
3885 // Look for ColorCorrectionMatrix opening tag
3886 if (line.find("<ColorCorrectionMatrix") != std::string::npos) {
3887 in_matrix = true;
3888
3889 // Extract camera_label attribute
3890 size_t camera_start = line.find("camera_label=\"");
3891 if (camera_start != std::string::npos) {
3892 camera_start += 14; // Length of "camera_label=\""
3893 size_t camera_end = line.find("\"", camera_start);
3894 if (camera_end != std::string::npos) {
3895 camera_label_out = line.substr(camera_start, camera_end - camera_start);
3896 }
3897 }
3898
3899 // Extract matrix_type attribute
3900 size_t type_start = line.find("matrix_type=\"");
3901 if (type_start != std::string::npos) {
3902 type_start += 13; // Length of "matrix_type=\""
3903 size_t type_end = line.find("\"", type_start);
3904 if (type_end != std::string::npos) {
3905 matrix_type = line.substr(type_start, type_end - type_start);
3906 }
3907 }
3908 continue;
3909 }
3910
3911 // Look for ColorCorrectionMatrix closing tag
3912 if (line.find("</ColorCorrectionMatrix>") != std::string::npos) {
3913 in_matrix = false;
3914 break;
3915 }
3916
3917 // Parse row data
3918 if (in_matrix && line.find("<row>") != std::string::npos && line.find("</row>") != std::string::npos) {
3919 // Extract content between <row> and </row>
3920 size_t start = line.find("<row>") + 5;
3921 size_t end = line.find("</row>");
3922 std::string row_data = line.substr(start, end - start);
3923
3924 // Parse float values from row
3925 std::vector<float> row;
3926 std::istringstream iss(row_data);
3927 float value;
3928 while (iss >> value) {
3929 row.push_back(value);
3930 }
3931
3932 if (!row.empty()) {
3933 matrix.push_back(row);
3934 }
3935 }
3936 }
3937
3938 file.close();
3939
3940 // Validate loaded matrix
3941 if (matrix.empty()) {
3942 helios_runtime_error("ERROR (RadiationModel::loadColorCorrectionMatrixXML): No matrix data found in file: " + file_path);
3943 }
3944
3945 if (matrix.size() != 3) {
3946 helios_runtime_error("ERROR (RadiationModel::loadColorCorrectionMatrixXML): Invalid matrix size. Expected 3 rows, found " + std::to_string(matrix.size()) + " rows in file: " + file_path);
3947 }
3948
3949 // Validate matrix type consistency
3950 bool is_3x3 = (matrix[0].size() == 3 && matrix[1].size() == 3 && matrix[2].size() == 3);
3951 bool is_4x3 = (matrix[0].size() == 4 && matrix[1].size() == 4 && matrix[2].size() == 4);
3952
3953 if (!is_3x3 && !is_4x3) {
3954 helios_runtime_error("ERROR (RadiationModel::loadColorCorrectionMatrixXML): Invalid matrix dimensions. All rows must have either 3 or 4 elements. File: " + file_path);
3955 }
3956
3957 // Check matrix type attribute matches actual dimensions
3958 if (!matrix_type.empty()) {
3959 if ((matrix_type == "3x3" && !is_3x3) || (matrix_type == "4x3" && !is_4x3)) {
3960 helios_runtime_error("ERROR (RadiationModel::loadColorCorrectionMatrixXML): Matrix type attribute ('" + matrix_type + "') does not match actual matrix dimensions in file: " + file_path);
3961 }
3962 }
3963
3964 return matrix;
3965}
3966
3967std::string RadiationModel::autoCalibrateCameraImage(const std::string &camera_label, const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, const std::string &output_file_path,
3968 bool print_quality_report, ColorCorrectionAlgorithm algorithm, const std::string &ccm_export_file_path) {
3969
3970 // Step 1: Validate camera exists and get pixel UUID data
3971 if (cameras.find(camera_label) == cameras.end()) {
3972 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Camera '" + camera_label + "' does not exist. Make sure the camera was added to the radiation model.");
3973 }
3974
3975 // Get camera pixel UUID data from global data (needed for segmentation)
3976 std::string pixel_UUID_label = "camera_" + camera_label + "_pixel_UUID";
3977 if (!context->doesGlobalDataExist(pixel_UUID_label.c_str())) {
3978 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Camera pixel UUID data '" + pixel_UUID_label + "' does not exist for camera '" + camera_label + "'. Make sure the radiation model has been run.");
3979 }
3980
3981 // Step 2: Detect all colorboard types using CameraCalibration helper
3982 CameraCalibration calibration(context);
3983 std::vector<std::string> colorboard_types;
3984 try {
3985 colorboard_types = calibration.detectColorBoardTypes();
3986 } catch (const std::exception &e) {
3987 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Failed to detect colorboard types. " + std::string(e.what()));
3988 }
3989
3990 // Step 3: Get reference Lab values for all detected colorboards
3991 std::vector<CameraCalibration::LabColor> reference_lab_values;
3992 std::vector<std::string> colorboard_type_per_patch; // Track which colorboard each patch belongs to
3993
3994 for (const auto &colorboard_type: colorboard_types) {
3995 std::vector<CameraCalibration::LabColor> current_reference_values;
3996
3997 if (colorboard_type == "DGK") {
3998 current_reference_values = calibration.getReferenceLab_DGK();
3999 } else if (colorboard_type == "Calibrite") {
4000 current_reference_values = calibration.getReferenceLab_Calibrite();
4001 } else if (colorboard_type == "SpyderCHECKR") {
4002 current_reference_values = calibration.getReferenceLab_SpyderCHECKR();
4003 } else {
4004 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Unsupported colorboard type '" + colorboard_type + "'.");
4005 }
4006
4007 // Add to combined list
4008 reference_lab_values.insert(reference_lab_values.end(), current_reference_values.begin(), current_reference_values.end());
4009
4010 // Track which colorboard type each patch belongs to
4011 for (size_t i = 0; i < current_reference_values.size(); i++) {
4012 colorboard_type_per_patch.push_back(colorboard_type);
4013 }
4014 }
4015
4016 // Step 4: Generate segmentation masks for all colorboard patches
4017 std::vector<uint> pixel_UUIDs;
4018 context->getGlobalData(pixel_UUID_label.c_str(), pixel_UUIDs);
4019 int2 camera_resolution = cameras.at(camera_label).resolution;
4020
4021 // Create segmentation masks by finding pixels that belong to colorboard patches
4022 std::map<int, std::vector<std::vector<bool>>> patch_masks;
4023 int global_patch_idx = 0; // Global patch index across all colorboards
4024
4025 for (const auto &colorboard_type: colorboard_types) {
4026 // Get the number of patches for this colorboard type
4027 int num_patches = 0;
4028 if (colorboard_type == "DGK") {
4029 num_patches = 18;
4030 } else if (colorboard_type == "Calibrite" || colorboard_type == "SpyderCHECKR") {
4031 num_patches = 24;
4032 }
4033
4034 // Generate masks for each patch in this colorboard
4035 for (int local_patch_idx = 0; local_patch_idx < num_patches; local_patch_idx++) {
4036 std::vector<std::vector<bool>> mask(camera_resolution.y, std::vector<bool>(camera_resolution.x, false));
4037
4038 // Find pixels that correspond to this colorboard patch
4039 for (int y = 0; y < camera_resolution.y; y++) {
4040 for (int x = 0; x < camera_resolution.x; x++) {
4041 int pixel_index = y * camera_resolution.x + x;
4042 uint pixel_UUID = pixel_UUIDs[pixel_index];
4043
4044 if (pixel_UUID > 0) { // Valid primitive
4045 pixel_UUID--; // Convert from 1-based to 0-based indexing
4046
4047 // Check if this primitive belongs to this specific colorboard patch
4048 std::string colorboard_data_label = "colorboard_" + colorboard_type;
4049 if (context->doesPrimitiveDataExist(pixel_UUID, colorboard_data_label.c_str())) {
4050 uint patch_id;
4051 context->getPrimitiveData(pixel_UUID, colorboard_data_label.c_str(), patch_id);
4052 // Patch indices are 0-based, compare directly
4053 if ((int) patch_id == local_patch_idx) {
4054 mask[y][x] = true;
4055 }
4056 }
4057 }
4058 }
4059 }
4060
4061 patch_masks[global_patch_idx] = mask;
4062 global_patch_idx++;
4063 }
4064 }
4065
4066 // Step 5: Extract RGB colors from processed camera data (same source as writeCameraImage)
4067 // Use the same data source that writeCameraImage() uses: cameras.pixel_data
4068 std::vector<float> red_data, green_data, blue_data;
4069
4070 // Check if bands exist in camera
4071 auto &camera_bands = cameras.at(camera_label).band_labels;
4072 if (std::find(camera_bands.begin(), camera_bands.end(), red_band_label) == camera_bands.end()) {
4073 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Red band '" + red_band_label + "' not found in camera '" + camera_label + "'.");
4074 }
4075 if (std::find(camera_bands.begin(), camera_bands.end(), green_band_label) == camera_bands.end()) {
4076 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Green band '" + green_band_label + "' not found in camera '" + camera_label + "'.");
4077 }
4078 if (std::find(camera_bands.begin(), camera_bands.end(), blue_band_label) == camera_bands.end()) {
4079 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Blue band '" + blue_band_label + "' not found in camera '" + camera_label + "'.");
4080 }
4081
4082 // Read processed camera data (same as writeCameraImage uses)
4083 red_data = cameras.at(camera_label).pixel_data.at(red_band_label);
4084 green_data = cameras.at(camera_label).pixel_data.at(green_band_label);
4085 blue_data = cameras.at(camera_label).pixel_data.at(blue_band_label);
4086
4087 // Check data range and normalize if needed
4088 float max_r = *std::max_element(red_data.begin(), red_data.end());
4089 float max_g = *std::max_element(green_data.begin(), green_data.end());
4090 float max_b = *std::max_element(blue_data.begin(), blue_data.end());
4091
4092 // Normalize camera data to [0,1] range if values are > 1
4093 float scale_factor = 1.0f;
4094 if (max_r > 1.0f || max_g > 1.0f || max_b > 1.0f) {
4095 scale_factor = 1.0f / std::max({max_r, max_g, max_b});
4096
4097 for (size_t i = 0; i < red_data.size(); i++) {
4098 red_data[i] *= scale_factor;
4099 green_data[i] *= scale_factor;
4100 blue_data[i] *= scale_factor;
4101 }
4102 }
4103
4104 std::vector<helios::vec3> measured_rgb_values;
4105 int visible_patches = 0;
4106
4107 for (const auto &[patch_idx, mask]: patch_masks) {
4108 float sum_r = 0.0f, sum_g = 0.0f, sum_b = 0.0f;
4109 int pixel_count = 0;
4110
4111 // Average RGB values over all pixels in this patch
4112 for (int y = 0; y < camera_resolution.y; y++) {
4113 for (int x = 0; x < camera_resolution.x; x++) {
4114 if (mask[y][x]) {
4115 int pixel_index = y * camera_resolution.x + x;
4116 sum_r += red_data[pixel_index];
4117 sum_g += green_data[pixel_index];
4118 sum_b += blue_data[pixel_index];
4119 pixel_count++;
4120 }
4121 }
4122 }
4123
4124 if (pixel_count > 10) { // Only consider patches with sufficient pixels
4125 helios::vec3 avg_rgb = make_vec3(sum_r / pixel_count, sum_g / pixel_count, sum_b / pixel_count);
4126 measured_rgb_values.push_back(avg_rgb);
4127
4128 visible_patches++;
4129 } else {
4130 // Add placeholder for missing patch
4131 measured_rgb_values.push_back(make_vec3(0, 0, 0));
4132 }
4133 }
4134
4135 // Convert measured RGB to Lab and calculate correction matrix
4136 std::vector<CameraCalibration::LabColor> measured_lab_values;
4137 for (const auto &rgb: measured_rgb_values) {
4138 if (rgb.magnitude() > 0) { // Only process non-zero values
4139 measured_lab_values.push_back(calibration.rgbToLab(rgb));
4140 }
4141 }
4142
4143 // Calculate color correction matrix based on selected algorithm
4144 std::vector<std::vector<float>> correction_matrix = {{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}};
4145
4146 // Report which algorithm is being used
4147 std::string algorithm_name;
4148 switch (algorithm) {
4150 algorithm_name = "Diagonal scaling (white balance only)";
4151 break;
4153 algorithm_name = "3x3 matrix with auto-fallback to diagonal";
4154 break;
4156 algorithm_name = "3x3 matrix (forced)";
4157 break;
4158 }
4159
4160 if (measured_lab_values.size() >= 6 && reference_lab_values.size() >= 6) {
4161 // Convert reference Lab back to RGB for matrix fitting
4162 std::vector<helios::vec3> target_rgb;
4163
4164 for (size_t i = 0; i < reference_lab_values.size(); i++) {
4165 CameraCalibration::LabColor ref_lab = reference_lab_values[i];
4166 helios::vec3 ref_rgb = calibration.labToRgb(ref_lab);
4167 target_rgb.push_back(ref_rgb);
4168 }
4169
4170 // Build matrices for least squares: M * Measured = Target
4171 // We need to solve for M where M is 3x3 matrix
4172 // This becomes: M = Target * Measured^T * (Measured * Measured^T)^(-1)
4173
4174 // Collect valid patches for matrix calculation
4175 std::vector<helios::vec3> valid_measured, valid_target;
4176 std::vector<float> patch_weights;
4177
4178 for (size_t i = 0; i < std::min(measured_rgb_values.size(), target_rgb.size()); i++) {
4179 if (measured_rgb_values[i].magnitude() > 0.01f) {
4180 valid_measured.push_back(measured_rgb_values[i]);
4181 valid_target.push_back(target_rgb[i]);
4182
4183 // Perceptually-weighted patch selection based on colour-science best practices
4184 float weight = 1.0f;
4185
4186 // Neutral patches (white to black series) - highest priority for white balance
4187 if (i >= 18 && i <= 23) {
4188 // White and light grays get highest weight (most visually important)
4189 if (i == 18)
4190 weight = 5.0f; // White patch - critical for white balance
4191 else if (i == 19)
4192 weight = 4.0f; // Light gray - very important for tone mapping
4193 else
4194 weight = 3.0f; // Darker grays - important for contrast
4195 }
4196 // Primary color patches - important for color accuracy
4197 else if (i == 14 || i == 13 || i == 12)
4198 weight = 3.0f; // Red, Green, Blue primaries
4199
4200 // Skin tone approximates - patches that represent common skin tones
4201 else if (i == 3 || i == 10)
4202 weight = 2.5f; // Foliage and Yellow Green (skin-like hues)
4203
4204 // Well-lit patches get higher weight based on luminance
4205 helios::vec3 measured_rgb = measured_rgb_values[i];
4206 float luminance = 0.299f * measured_rgb.x + 0.587f * measured_rgb.y + 0.114f * measured_rgb.z;
4207
4208 // Boost weight for brighter patches (they're more visually prominent)
4209 if (luminance > 0.6f)
4210 weight *= 1.5f;
4211 else if (luminance < 0.2f)
4212 weight *= 0.7f; // Reduce weight for very dark patches
4213
4214 // Additional quality checks for measured RGB values
4215 // Reduce weight for patches that seem poorly measured (extreme values)
4216 if (measured_rgb.magnitude() > 1.2f || measured_rgb.magnitude() < 0.1f) {
4217 weight *= 0.5f; // Reduce weight for suspicious measurements
4218 }
4219
4220 patch_weights.push_back(weight);
4221 }
4222 }
4223
4224 if (valid_measured.size() >= 6 && algorithm != ColorCorrectionAlgorithm::DIAGONAL_ONLY) {
4225
4226 // Compute robustly regularized weighted least squares solution
4227 // Use adaptive regularization to prevent extreme matrix coefficients
4228 bool matrix_valid = true;
4229
4230 // Try moderate regularization values - avoid extreme regularization that creates bad matrices
4231 std::vector<float> lambda_values = {0.01f, 0.05f, 0.1f, 0.15f, 0.2f};
4232 int lambda_attempt = 0;
4233
4234 while (lambda_attempt < lambda_values.size()) {
4235 float lambda = lambda_values[lambda_attempt];
4236 matrix_valid = true;
4237
4238 for (int row = 0; row < 3; row++) {
4239 // Build weighted normal equations with adaptive regularization
4240 float ATA[3][3] = {{0}}; // A^T * W * A + λI
4241 float ATb[3] = {0}; // A^T * W * b
4242
4243 for (size_t i = 0; i < valid_measured.size(); i++) {
4244 float weight = patch_weights[i];
4245 helios::vec3 m = valid_measured[i]; // measured RGB
4246 float target_val = (row == 0) ? valid_target[i].x : (row == 1) ? valid_target[i].y : valid_target[i].z;
4247
4248 // Update normal equations
4249 ATA[0][0] += weight * m.x * m.x;
4250 ATA[0][1] += weight * m.x * m.y;
4251 ATA[0][2] += weight * m.x * m.z;
4252 ATA[1][0] += weight * m.y * m.x;
4253 ATA[1][1] += weight * m.y * m.y;
4254 ATA[1][2] += weight * m.y * m.z;
4255 ATA[2][0] += weight * m.z * m.x;
4256 ATA[2][1] += weight * m.z * m.y;
4257 ATA[2][2] += weight * m.z * m.z;
4258
4259 ATb[0] += weight * m.x * target_val;
4260 ATb[1] += weight * m.y * target_val;
4261 ATb[2] += weight * m.z * target_val;
4262 }
4263
4264 // Add color-preserving regularization
4265 // Stronger regularization on diagonal (preserves primary colors)
4266 // Weaker regularization on off-diagonal (allows some color mixing)
4267 float diag_reg = lambda * 2.0f; // Stronger on diagonal
4268 float offdiag_reg = lambda * 0.5f; // Weaker on off-diagonal
4269
4270 ATA[0][0] += diag_reg; // Red preservation
4271 ATA[1][1] += diag_reg; // Green preservation
4272 ATA[2][2] += diag_reg; // Blue preservation
4273
4274 // Light off-diagonal regularization to prevent extreme color mixing
4275 ATA[0][1] += offdiag_reg;
4276 ATA[1][0] += offdiag_reg;
4277 ATA[0][2] += offdiag_reg;
4278 ATA[2][0] += offdiag_reg;
4279 ATA[1][2] += offdiag_reg;
4280 ATA[2][1] += offdiag_reg;
4281
4282 // Solve regularized 3x3 system using Cramer's rule
4283 float det = ATA[0][0] * (ATA[1][1] * ATA[2][2] - ATA[1][2] * ATA[2][1]) - ATA[0][1] * (ATA[1][0] * ATA[2][2] - ATA[1][2] * ATA[2][0]) + ATA[0][2] * (ATA[1][0] * ATA[2][1] - ATA[1][1] * ATA[2][0]);
4284
4285 if (fabs(det) < 1e-3f) {
4287 matrix_valid = false;
4288 break;
4289 }
4290 }
4291
4292 float inv_det = 1.0f / det;
4293 correction_matrix[row][0] = inv_det * (ATb[0] * (ATA[1][1] * ATA[2][2] - ATA[1][2] * ATA[2][1]) - ATb[1] * (ATA[0][1] * ATA[2][2] - ATA[0][2] * ATA[2][1]) + ATb[2] * (ATA[0][1] * ATA[1][2] - ATA[0][2] * ATA[1][1]));
4294
4295 correction_matrix[row][1] = inv_det * (ATb[1] * (ATA[0][0] * ATA[2][2] - ATA[0][2] * ATA[2][0]) - ATb[0] * (ATA[1][0] * ATA[2][2] - ATA[1][2] * ATA[2][0]) + ATb[2] * (ATA[1][0] * ATA[0][2] - ATA[1][2] * ATA[0][0]));
4296
4297 correction_matrix[row][2] = inv_det * (ATb[2] * (ATA[0][0] * ATA[1][1] - ATA[0][1] * ATA[1][0]) - ATb[0] * (ATA[1][0] * ATA[2][1] - ATA[1][1] * ATA[2][0]) + ATb[1] * (ATA[0][0] * ATA[2][1] - ATA[0][1] * ATA[2][0]));
4298 }
4299
4300 // Validate computed matrix elements are reasonable
4301 if (matrix_valid) {
4302 bool elements_reasonable = true;
4303 for (int i = 0; i < 3; i++) {
4304 for (int j = 0; j < 3; j++) {
4305 if (fabs(correction_matrix[i][j]) > 5.0f) {
4307 elements_reasonable = false;
4308 break;
4309 }
4310 }
4311 }
4312 if (!elements_reasonable)
4313 break;
4314 }
4315 matrix_valid = elements_reasonable;
4316 }
4317
4318 if (matrix_valid) {
4319 break; // Success!
4320 } else {
4321 lambda_attempt++;
4322 }
4323 }
4324
4325 if (!matrix_valid) {
4326
4327 // Enhanced perceptually-weighted diagonal correction
4328 // Calculate weighted averages for each color channel
4329 float total_weight = 0.0f;
4330 helios::vec3 weighted_correction = make_vec3(0, 0, 0);
4331
4332 for (size_t i = 0; i < valid_measured.size(); i++) {
4333 float weight = patch_weights[i];
4334 helios::vec3 measured = valid_measured[i];
4335 helios::vec3 target = valid_target[i];
4336
4337 // Calculate per-channel correction factors
4338 if (measured.x > 0.01f && measured.y > 0.01f && measured.z > 0.01f) {
4339 helios::vec3 channel_correction = make_vec3(target.x / measured.x, target.y / measured.y, target.z / measured.z);
4340
4341 weighted_correction.x += weight * channel_correction.x;
4342 weighted_correction.y += weight * channel_correction.y;
4343 weighted_correction.z += weight * channel_correction.z;
4344 total_weight += weight;
4345 }
4346 }
4347
4348 if (total_weight > 0.1f) {
4349 // Apply weighted average correction factors
4350 correction_matrix[0][0] = weighted_correction.x / total_weight;
4351 correction_matrix[1][1] = weighted_correction.y / total_weight;
4352 correction_matrix[2][2] = weighted_correction.z / total_weight;
4353
4354 // Apply conservative limits
4355 correction_matrix[0][0] = std::max(0.5f, std::min(2.0f, correction_matrix[0][0]));
4356 correction_matrix[1][1] = std::max(0.5f, std::min(2.0f, correction_matrix[1][1]));
4357 correction_matrix[2][2] = std::max(0.5f, std::min(2.0f, correction_matrix[2][2]));
4358 }
4359 }
4360
4361 if (!matrix_valid || algorithm == ColorCorrectionAlgorithm::DIAGONAL_ONLY) {
4362 // Use diagonal correction using white patch
4363 correction_matrix = {{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}};
4364
4365 // Enhanced perceptually-weighted diagonal correction using all valid patches
4366 // This is more robust than using just the white patch
4367 if (valid_measured.size() > 0 && patch_weights.size() == valid_measured.size()) {
4368 float total_weight = 0.0f;
4369 helios::vec3 weighted_measured_avg = make_vec3(0, 0, 0);
4370 helios::vec3 weighted_target_avg = make_vec3(0, 0, 0);
4371
4372 // Compute weighted averages using perceptual patch weights
4373 for (size_t i = 0; i < valid_measured.size(); i++) {
4374 float weight = patch_weights[i];
4375 weighted_measured_avg = weighted_measured_avg + weight * valid_measured[i];
4376 weighted_target_avg = weighted_target_avg + weight * valid_target[i];
4377 total_weight += weight;
4378 }
4379
4380 if (total_weight > 0) {
4381 weighted_measured_avg = weighted_measured_avg / total_weight;
4382 weighted_target_avg = weighted_target_avg / total_weight;
4383
4384 if (weighted_measured_avg.x > 0.05f && weighted_measured_avg.y > 0.05f && weighted_measured_avg.z > 0.05f) {
4385 correction_matrix[0][0] = weighted_target_avg.x / weighted_measured_avg.x;
4386 correction_matrix[1][1] = weighted_target_avg.y / weighted_measured_avg.y;
4387 correction_matrix[2][2] = weighted_target_avg.z / weighted_measured_avg.z;
4388
4389 // Apply conservative limits for stability
4390 correction_matrix[0][0] = std::max(0.5f, std::min(2.0f, correction_matrix[0][0]));
4391 correction_matrix[1][1] = std::max(0.5f, std::min(2.0f, correction_matrix[1][1]));
4392 correction_matrix[2][2] = std::max(0.5f, std::min(2.0f, correction_matrix[2][2]));
4393
4394 } else {
4395 // Fallback to original white-patch method
4396 size_t white_idx = 18;
4397 if (white_idx < measured_rgb_values.size() && white_idx < target_rgb.size()) {
4398 helios::vec3 measured_white = measured_rgb_values[white_idx];
4399 helios::vec3 target_white = target_rgb[white_idx];
4400 if (measured_white.x > 0.05f && measured_white.y > 0.05f && measured_white.z > 0.05f) {
4401 correction_matrix[0][0] = std::max(0.5f, std::min(2.0f, target_white.x / measured_white.x));
4402 correction_matrix[1][1] = std::max(0.5f, std::min(2.0f, target_white.y / measured_white.y));
4403 correction_matrix[2][2] = std::max(0.5f, std::min(2.0f, target_white.z / measured_white.z));
4404 }
4405 }
4406 }
4407 }
4408 } else {
4409 // Original simple approach as ultimate fallback
4410 size_t white_idx = 18;
4411 if (white_idx < measured_rgb_values.size() && white_idx < target_rgb.size()) {
4412 helios::vec3 measured_white = measured_rgb_values[white_idx];
4413 helios::vec3 target_white = target_rgb[white_idx];
4414 if (measured_white.x > 0.05f && measured_white.y > 0.05f && measured_white.z > 0.05f) {
4415 correction_matrix[0][0] = std::max(0.5f, std::min(2.0f, target_white.x / measured_white.x));
4416 correction_matrix[1][1] = std::max(0.5f, std::min(2.0f, target_white.y / measured_white.y));
4417 correction_matrix[2][2] = std::max(0.5f, std::min(2.0f, target_white.z / measured_white.z));
4418 }
4419 }
4420 }
4421 }
4422 } else if (algorithm == ColorCorrectionAlgorithm::DIAGONAL_ONLY) {
4423 // Apply diagonal correction using white patch
4424 size_t white_idx = 18;
4425 if (white_idx < measured_rgb_values.size() && white_idx < target_rgb.size() && measured_rgb_values[white_idx].magnitude() > 0) {
4426
4427 helios::vec3 measured_white = measured_rgb_values[white_idx];
4428 helios::vec3 target_white = target_rgb[white_idx];
4429
4430 if (measured_white.x > 0.05f && measured_white.y > 0.05f && measured_white.z > 0.05f) {
4431 correction_matrix[0][0] = target_white.x / measured_white.x;
4432 correction_matrix[1][1] = target_white.y / measured_white.y;
4433 correction_matrix[2][2] = target_white.z / measured_white.z;
4434
4435 // Apply limits
4436 correction_matrix[0][0] = std::max(0.5f, std::min(2.0f, correction_matrix[0][0]));
4437 correction_matrix[1][1] = std::max(0.5f, std::min(2.0f, correction_matrix[1][1]));
4438 correction_matrix[2][2] = std::max(0.5f, std::min(2.0f, correction_matrix[2][2]));
4439 }
4440 }
4441 } else {
4442 std::cout << "Insufficient valid patches (" << valid_measured.size() << " available), using identity matrix" << std::endl;
4443 }
4444 } else if (algorithm == ColorCorrectionAlgorithm::DIAGONAL_ONLY && measured_lab_values.size() > 0) {
4445 // Apply diagonal correction using white patch
4446 size_t white_idx = 18;
4447 if (white_idx < measured_rgb_values.size() && white_idx < reference_lab_values.size() && measured_rgb_values[white_idx].magnitude() > 0) {
4448
4449 CameraCalibration::LabColor ref_lab = reference_lab_values[white_idx];
4450 helios::vec3 target_white = calibration.labToRgb(ref_lab);
4451 helios::vec3 measured_white = measured_rgb_values[white_idx];
4452
4453 if (measured_white.x > 0.05f && measured_white.y > 0.05f && measured_white.z > 0.05f) {
4454 correction_matrix[0][0] = target_white.x / measured_white.x;
4455 correction_matrix[1][1] = target_white.y / measured_white.y;
4456 correction_matrix[2][2] = target_white.z / measured_white.z;
4457
4458 // Apply limits
4459 correction_matrix[0][0] = std::max(0.5f, std::min(2.0f, correction_matrix[0][0]));
4460 correction_matrix[1][1] = std::max(0.5f, std::min(2.0f, correction_matrix[1][1]));
4461 correction_matrix[2][2] = std::max(0.5f, std::min(2.0f, correction_matrix[2][2]));
4462 }
4463 }
4464 } else {
4465 std::cout << "Insufficient patches for correction (" << measured_lab_values.size() << " available), using identity matrix" << std::endl;
4466 }
4467
4468 // Generate quality of fit report if requested
4469 if (print_quality_report) {
4470 std::cout << "\n========== COLOR CALIBRATION QUALITY REPORT ==========" << std::endl;
4471 std::cout << "Colorboard types: ";
4472 for (size_t i = 0; i < colorboard_types.size(); i++) {
4473 std::cout << colorboard_types[i];
4474 if (i < colorboard_types.size() - 1) {
4475 std::cout << ", ";
4476 }
4477 }
4478 std::cout << std::endl;
4479 std::cout << "Number of patches analyzed: " << visible_patches << std::endl;
4480 std::cout << "Algorithm used: " << algorithm_name << std::endl;
4481
4482 // Display matrix conditioning information
4483 bool is_diagonal_only = true;
4484 for (int i = 0; i < 3; i++) {
4485 for (int j = 0; j < 3; j++) {
4486 if (i != j && fabs(correction_matrix[i][j]) > 1e-6f) {
4487 is_diagonal_only = false;
4488 break;
4489 }
4490 }
4491 if (!is_diagonal_only)
4492 break;
4493 }
4494
4495 if (is_diagonal_only) {
4496 std::cout << "Color correction factors applied: R=" << correction_matrix[0][0] << ", G=" << correction_matrix[1][1] << ", B=" << correction_matrix[2][2] << std::endl;
4497 std::cout << "Matrix type: Diagonal (white balance only)" << std::endl;
4498 } else {
4499 std::cout << "Full 3x3 color correction matrix applied:" << std::endl;
4500 for (int i = 0; i < 3; i++) {
4501 std::cout << "[" << std::fixed << std::setprecision(4);
4502 for (int j = 0; j < 3; j++) {
4503 std::cout << std::setw(8) << correction_matrix[i][j];
4504 if (j < 2)
4505 std::cout << " ";
4506 }
4507 std::cout << "]" << std::endl;
4508 }
4509 std::cout << "Matrix type: Full 3x3 (corrects color casts and chromatic errors)" << std::endl;
4510
4511 // Calculate matrix determinant for conditioning info
4512 float det = correction_matrix[0][0] * (correction_matrix[1][1] * correction_matrix[2][2] - correction_matrix[1][2] * correction_matrix[2][1]) -
4513 correction_matrix[0][1] * (correction_matrix[1][0] * correction_matrix[2][2] - correction_matrix[1][2] * correction_matrix[2][0]) +
4514 correction_matrix[0][2] * (correction_matrix[1][0] * correction_matrix[2][1] - correction_matrix[1][1] * correction_matrix[2][0]);
4515
4516 std::cout << "Matrix determinant: " << std::scientific << std::setprecision(3) << det << std::endl;
4517 if (fabs(det) > 0.1f) {
4518 std::cout << "Matrix conditioning: Good (well-conditioned)" << std::endl;
4519 } else if (fabs(det) > 0.01f) {
4520 std::cout << "Matrix conditioning: Fair (moderately conditioned)" << std::endl;
4521 } else {
4522 std::cout << "Matrix conditioning: Poor (ill-conditioned)" << std::endl;
4523 }
4524 std::cout << std::fixed; // Reset formatting
4525 }
4526
4527 // Calculate and display quality metrics for each patch
4528 double total_delta_e = 0.0;
4529 int valid_patches = 0;
4530
4531 std::cout << "\nPer-patch analysis (after correction):" << std::endl;
4532 std::cout << "Patch | Corrected RGB | Reference RGB | Delta E " << std::endl;
4533 std::cout << "------|--------------------|--------------------|---------" << std::endl;
4534
4535 for (size_t i = 0; i < std::min(measured_rgb_values.size(), reference_lab_values.size()); i++) {
4536 if (measured_rgb_values[i].magnitude() > 0) {
4537 // Apply color correction to measured RGB values (with optional affine terms)
4538 helios::vec3 measured_rgb = measured_rgb_values[i];
4539 float corrected_r = correction_matrix[0][0] * measured_rgb.x + correction_matrix[0][1] * measured_rgb.y + correction_matrix[0][2] * measured_rgb.z;
4540 float corrected_g = correction_matrix[1][0] * measured_rgb.x + correction_matrix[1][1] * measured_rgb.y + correction_matrix[1][2] * measured_rgb.z;
4541 float corrected_b = correction_matrix[2][0] * measured_rgb.x + correction_matrix[2][1] * measured_rgb.y + correction_matrix[2][2] * measured_rgb.z;
4542
4543
4544 // Clamp corrected values to [0,1] - DISABLED FOR TESTING
4545 // corrected_r = std::max(0.0f, std::min(1.0f, corrected_r));
4546 // corrected_g = std::max(0.0f, std::min(1.0f, corrected_g));
4547 // corrected_b = std::max(0.0f, std::min(1.0f, corrected_b));
4548
4549 helios::vec3 corrected_rgb = make_vec3(corrected_r, corrected_g, corrected_b);
4550
4551 // Convert corrected RGB to Lab
4552 CameraCalibration::LabColor corrected_lab = calibration.rgbToLab(corrected_rgb);
4553 CameraCalibration::LabColor reference_lab = reference_lab_values[i];
4554
4555 // Calculate Delta E between corrected and reference
4556 // Use ΔE2000 for better perceptual color difference assessment
4557 double delta_E = calibration.deltaE2000(corrected_lab, reference_lab);
4558
4559 std::cout << std::setw(5) << i << " | " << std::fixed << std::setprecision(3) << "(" << std::setw(5) << corrected_rgb.x << "," << std::setw(5) << corrected_rgb.y << "," << std::setw(5) << corrected_rgb.z << ") | ";
4560
4561 helios::vec3 ref_rgb = calibration.labToRgb(reference_lab);
4562 std::cout << "(" << std::setw(5) << ref_rgb.x << "," << std::setw(5) << ref_rgb.y << "," << std::setw(5) << ref_rgb.z << ") | " << std::setw(7) << delta_E << std::endl;
4563
4564 total_delta_e += delta_E;
4565 valid_patches++;
4566 }
4567 }
4568
4569 // Overall statistics
4570 double mean_delta_e = total_delta_e / valid_patches;
4571 std::cout << "\n========== OVERALL CALIBRATION QUALITY ==========" << std::endl;
4572 std::cout << "Mean Delta E: " << std::fixed << std::setprecision(2) << mean_delta_e << std::endl;
4573
4574 std::cout << "======================================================\n" << std::endl;
4575 }
4576
4577 // Step 7: Apply correction to entire image with same pixel ordering as writeCameraImage
4578 std::vector<helios::RGBcolor> corrected_pixels;
4579 corrected_pixels.resize(red_data.size());
4580
4581 // Apply correction using the same pixel transformation as writeCameraImage uses
4582 for (int j = 0; j < camera_resolution.y; j++) {
4583 for (int i = 0; i < camera_resolution.x; i++) {
4584 // Get pixel from source data (no flip)
4585 int source_index = j * camera_resolution.x + i;
4586 float r = red_data[source_index];
4587 float g = green_data[source_index];
4588 float b = blue_data[source_index];
4589
4590 // Apply correction matrix (with optional affine terms)
4591 float corrected_r = correction_matrix[0][0] * r + correction_matrix[0][1] * g + correction_matrix[0][2] * b;
4592 float corrected_g = correction_matrix[1][0] * r + correction_matrix[1][1] * g + correction_matrix[1][2] * b;
4593 float corrected_b = correction_matrix[2][0] * r + correction_matrix[2][1] * g + correction_matrix[2][2] * b;
4594
4595
4596 // Clamp values to [0,1] - DISABLED FOR TESTING
4597 // corrected_r = std::max(0.0f, std::min(1.0f, corrected_r));
4598 // corrected_g = std::max(0.0f, std::min(1.0f, corrected_g));
4599 // corrected_b = std::max(0.0f, std::min(1.0f, corrected_b));
4600
4601 // Apply same coordinate transformation as writeCameraImage
4602 uint ii = camera_resolution.x - i - 1; // Horizontal flip
4603 uint jj = camera_resolution.y - j - 1; // Vertical flip
4604 uint dest_index = jj * camera_resolution.x + ii;
4605
4606 corrected_pixels[dest_index] = make_RGBcolor(corrected_r, corrected_g, corrected_b);
4607 }
4608 }
4609
4610 // Step 8: Write corrected image using writeJPEG
4611 std::string output_path = output_file_path;
4612 if (output_path.empty()) {
4613 output_path = "auto_calibrated_" + camera_label + ".jpg";
4614 }
4615
4616 try {
4617 helios::writeJPEG(output_path, camera_resolution.x, camera_resolution.y, corrected_pixels);
4618 std::cout << "Wrote corrected image to: " << output_path << std::endl;
4619 } catch (const std::exception &e) {
4620 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Failed to write corrected image. " + std::string(e.what()));
4621 }
4622
4623 // Export CCM to XML file if requested
4624 if (!ccm_export_file_path.empty()) {
4625 try {
4626 // Calculate quality metrics for export (even if not printed)
4627 double total_delta_e = 0.0;
4628 int valid_patches = 0;
4629
4630 for (size_t i = 0; i < std::min(measured_rgb_values.size(), reference_lab_values.size()); i++) {
4631 if (measured_rgb_values[i].magnitude() > 0) {
4632 // Apply color correction to measured RGB values
4633 helios::vec3 measured_rgb = measured_rgb_values[i];
4634 float corrected_r = correction_matrix[0][0] * measured_rgb.x + correction_matrix[0][1] * measured_rgb.y + correction_matrix[0][2] * measured_rgb.z;
4635 float corrected_g = correction_matrix[1][0] * measured_rgb.x + correction_matrix[1][1] * measured_rgb.y + correction_matrix[1][2] * measured_rgb.z;
4636 float corrected_b = correction_matrix[2][0] * measured_rgb.x + correction_matrix[2][1] * measured_rgb.y + correction_matrix[2][2] * measured_rgb.z;
4637
4638 helios::vec3 corrected_rgb = make_vec3(corrected_r, corrected_g, corrected_b);
4639
4640 // Convert corrected RGB to Lab
4641 CameraCalibration::LabColor corrected_lab = calibration.rgbToLab(corrected_rgb);
4642 CameraCalibration::LabColor reference_lab = reference_lab_values[i];
4643
4644 // Calculate Delta E between corrected and reference
4645 double delta_E = calibration.deltaE2000(corrected_lab, reference_lab);
4646 total_delta_e += delta_E;
4647 valid_patches++;
4648 }
4649 }
4650
4651 double mean_delta_e = (valid_patches > 0) ? (total_delta_e / valid_patches) : -1.0;
4652
4653 // Export CCM to XML file
4654 // Concatenate all colorboard types into a single string
4655 std::string colorboard_types_str;
4656 for (size_t i = 0; i < colorboard_types.size(); i++) {
4657 colorboard_types_str += colorboard_types[i];
4658 if (i < colorboard_types.size() - 1) {
4659 colorboard_types_str += ", ";
4660 }
4661 }
4662 exportColorCorrectionMatrixXML(ccm_export_file_path, camera_label, correction_matrix, output_path, colorboard_types_str, (float) mean_delta_e);
4663
4664 std::cout << "Exported color correction matrix to: " << ccm_export_file_path << std::endl;
4665 } catch (const std::exception &e) {
4666 helios_runtime_error("ERROR (RadiationModel::autoCalibrateCameraImage): Failed to export CCM to XML. " + std::string(e.what()));
4667 }
4668 }
4669
4670 return output_path;
4671}
4672
4673void RadiationModel::applyCameraColorCorrectionMatrix(const std::string &camera_label, const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, const std::string &ccm_file_path) {
4674
4675 // Step 1: Validate camera exists
4676 if (cameras.find(camera_label) == cameras.end()) {
4677 helios_runtime_error("ERROR (RadiationModel::applyCameraColorCorrectionMatrix): Camera '" + camera_label + "' does not exist. Make sure the camera was added to the radiation model.");
4678 }
4679
4680 // Step 2: Validate band labels exist in camera
4681 auto &camera_bands = cameras.at(camera_label).band_labels;
4682 if (std::find(camera_bands.begin(), camera_bands.end(), red_band_label) == camera_bands.end()) {
4683 helios_runtime_error("ERROR (RadiationModel::applyCameraColorCorrectionMatrix): Red band '" + red_band_label + "' not found in camera '" + camera_label + "'.");
4684 }
4685 if (std::find(camera_bands.begin(), camera_bands.end(), green_band_label) == camera_bands.end()) {
4686 helios_runtime_error("ERROR (RadiationModel::applyCameraColorCorrectionMatrix): Green band '" + green_band_label + "' not found in camera '" + camera_label + "'.");
4687 }
4688 if (std::find(camera_bands.begin(), camera_bands.end(), blue_band_label) == camera_bands.end()) {
4689 helios_runtime_error("ERROR (RadiationModel::applyCameraColorCorrectionMatrix): Blue band '" + blue_band_label + "' not found in camera '" + camera_label + "'.");
4690 }
4691
4692 // Step 3: Load color correction matrix from XML file
4693 std::string loaded_camera_label;
4694 std::vector<std::vector<float>> correction_matrix;
4695 try {
4696 correction_matrix = loadColorCorrectionMatrixXML(ccm_file_path, loaded_camera_label);
4697 } catch (const std::exception &e) {
4698 helios_runtime_error("ERROR (RadiationModel::applyCameraColorCorrectionMatrix): Failed to load CCM from XML file. " + std::string(e.what()));
4699 }
4700
4701 // Step 4: Validate matrix dimensions (should be 3x3 or 4x3)
4702 if (correction_matrix.size() != 3) {
4703 helios_runtime_error("ERROR (RadiationModel::applyCameraColorCorrectionMatrix): Invalid matrix dimensions. Expected 3x3 or 4x3 matrix, got " + std::to_string(correction_matrix.size()) + " rows.");
4704 }
4705
4706 bool is_3x3 = (correction_matrix[0].size() == 3);
4707 bool is_4x3 = (correction_matrix[0].size() == 4);
4708
4709 if (!is_3x3 && !is_4x3) {
4710 helios_runtime_error("ERROR (RadiationModel::applyCameraColorCorrectionMatrix): Invalid matrix dimensions. Expected 3x3 or 4x3 matrix, got " + std::to_string(correction_matrix.size()) + "x" + std::to_string(correction_matrix[0].size()) +
4711 " matrix.");
4712 }
4713
4714 // Step 5: Get camera data (same approach as applyImageProcessingPipeline)
4715 std::vector<float> &red_data = cameras.at(camera_label).pixel_data.at(red_band_label);
4716 std::vector<float> &green_data = cameras.at(camera_label).pixel_data.at(green_band_label);
4717 std::vector<float> &blue_data = cameras.at(camera_label).pixel_data.at(blue_band_label);
4718
4719 int2 camera_resolution = cameras.at(camera_label).resolution;
4720 size_t pixel_count = red_data.size();
4721
4722 // Step 6: Apply color correction matrix to all pixels in-place
4723 for (size_t i = 0; i < pixel_count; i++) {
4724 float r = red_data[i];
4725 float g = green_data[i];
4726 float b = blue_data[i];
4727
4728 // Apply color correction matrix (3x3 or 4x3)
4729 if (is_3x3) {
4730 // Standard 3x3 matrix transformation
4731 red_data[i] = correction_matrix[0][0] * r + correction_matrix[0][1] * g + correction_matrix[0][2] * b;
4732 green_data[i] = correction_matrix[1][0] * r + correction_matrix[1][1] * g + correction_matrix[1][2] * b;
4733 blue_data[i] = correction_matrix[2][0] * r + correction_matrix[2][1] * g + correction_matrix[2][2] * b;
4734 } else {
4735 // 4x3 matrix transformation with affine offset
4736 red_data[i] = correction_matrix[0][0] * r + correction_matrix[0][1] * g + correction_matrix[0][2] * b + correction_matrix[0][3];
4737 green_data[i] = correction_matrix[1][0] * r + correction_matrix[1][1] * g + correction_matrix[1][2] * b + correction_matrix[1][3];
4738 blue_data[i] = correction_matrix[2][0] * r + correction_matrix[2][1] * g + correction_matrix[2][2] * b + correction_matrix[2][3];
4739 }
4740 }
4741
4742 if (message_flag) {
4743 std::cout << "Applied color correction matrix from '" << ccm_file_path << "' to camera '" << camera_label << "'" << std::endl;
4744 std::cout << "Matrix type: " << (is_3x3 ? "3x3" : "4x3") << ", processed " << pixel_count << " pixels" << std::endl;
4745 }
4746}
4747
4748std::vector<float> RadiationModel::getCameraPixelData(const std::string &camera_label, const std::string &band_label) {
4749 if (cameras.find(camera_label) == cameras.end()) {
4750 helios_runtime_error("ERROR (RadiationModel::getCameraPixelData): Camera '" + camera_label + "' does not exist.");
4751 }
4752
4753 auto &camera_pixel_data = cameras.at(camera_label).pixel_data;
4754 if (camera_pixel_data.find(band_label) == camera_pixel_data.end()) {
4755 helios_runtime_error("ERROR (RadiationModel::getCameraPixelData): Band '" + band_label + "' does not exist in camera '" + camera_label + "'.");
4756 }
4757
4758 return camera_pixel_data.at(band_label);
4759}
4760
4761void RadiationModel::setCameraPixelData(const std::string &camera_label, const std::string &band_label, const std::vector<float> &pixel_data) {
4762 if (cameras.find(camera_label) == cameras.end()) {
4763 helios_runtime_error("ERROR (RadiationModel::setCameraPixelData): Camera '" + camera_label + "' does not exist.");
4764 }
4765
4766 cameras.at(camera_label).pixel_data[band_label] = pixel_data;
4767}
4768
4769// ========== Phase 1: Backend Integration Methods ==========
4770
4772 if (backend) {
4773 backend->queryGPUMemory();
4774 } else {
4775 std::cout << "Backend not initialized - cannot query GPU memory." << std::endl;
4776 }
4777}
4778
4779
4780helios::RayTracingLaunchParams RadiationModel::buildCameraLaunchParams(const RadiationCamera &camera, uint camera_id, uint antialiasing_samples, const helios::int2 &tile_resolution, const helios::int2 &tile_offset) {
4781
4783
4784 // Camera position and orientation
4785 params.camera_position = camera.position;
4786 helios::SphericalCoord dir = cart2sphere(camera.lookat - camera.position);
4788
4789 // Camera optical properties
4790 params.camera_focal_length = camera.focal_length;
4791 params.camera_lens_diameter = camera.lens_diameter;
4792 params.camera_fov_aspect = camera.FOV_aspect_ratio;
4793
4794 // Resolution and tiling
4795 params.camera_resolution = tile_resolution;
4796 params.camera_resolution_full = camera.resolution;
4797 params.camera_pixel_offset = tile_offset;
4798 params.antialiasing_samples = antialiasing_samples;
4799 params.camera_id = camera_id;
4800
4801 // Compute effective HFOV with zoom
4802 float effective_HFOV = camera.HFOV_degrees / camera.camera_zoom;
4803 params.camera_HFOV = effective_HFOV * M_PI / 180.0f;
4804 params.camera_viewplane_length = 0.5f / tanf(0.5f * effective_HFOV * M_PI / 180.f);
4805
4806 // Compute pixel solid angle
4807 float HFOV_rad = effective_HFOV * M_PI / 180.f;
4808 float VFOV_rad = HFOV_rad / camera.FOV_aspect_ratio;
4809 float pixel_angle_h = HFOV_rad / float(camera.resolution.x);
4810 float pixel_angle_v = VFOV_rad / float(camera.resolution.y);
4811 params.camera_pixel_solid_angle = pixel_angle_h * pixel_angle_v;
4812
4813 // Explicitly set scattering iteration for cameras (always iteration 0 for specular)
4814 params.scattering_iteration = 0;
4815
4816 // Set specular reflection mode from auto-detection
4817 params.specular_reflection_enabled = specular_reflection_mode;
4818
4819 return params;
4820}
4821
4822std::vector<CameraTile> RadiationModel::computeCameraTiles(const RadiationCamera &camera, size_t maxRays) {
4823
4824 std::vector<CameraTile> tiles;
4825
4826 size_t total_rays = size_t(camera.antialiasing_samples) * size_t(camera.resolution.x) * size_t(camera.resolution.y);
4827
4828 // No tiling needed
4829 if (total_rays <= maxRays) {
4830 tiles.push_back({camera.resolution, helios::make_int2(0, 0)});
4831 return tiles;
4832 }
4833
4834 // Calculate tile dimensions
4835 size_t rays_per_row = size_t(camera.antialiasing_samples) * size_t(camera.resolution.x);
4836 size_t max_rows_per_tile = floor(float(maxRays) / float(rays_per_row));
4837
4838 if (max_rows_per_tile == 0) {
4839 // 2D tiling - even one row is too large
4840 size_t max_pixels_per_tile = floor(float(maxRays) / float(camera.antialiasing_samples));
4841
4842 float aspect = float(camera.resolution.x) / float(camera.resolution.y);
4843 size_t tile_width = round(sqrt(max_pixels_per_tile * aspect));
4844 size_t tile_height = floor(float(max_pixels_per_tile) / float(tile_width));
4845
4846 tile_width = std::min(tile_width, size_t(camera.resolution.x));
4847 tile_height = std::min(tile_height, size_t(camera.resolution.y));
4848
4849 int Ntiles_x = ceil(float(camera.resolution.x) / float(tile_width));
4850 int Ntiles_y = ceil(float(camera.resolution.y) / float(tile_height));
4851
4852 for (int ty = 0; ty < Ntiles_y; ty++) {
4853 for (int tx = 0; tx < Ntiles_x; tx++) {
4854 size_t offset_x = tx * tile_width;
4855 size_t offset_y = ty * tile_height;
4856 size_t width_this = std::min(tile_width, camera.resolution.x - offset_x);
4857 size_t height_this = std::min(tile_height, camera.resolution.y - offset_y);
4858
4859 tiles.push_back({helios::make_int2(width_this, height_this), helios::make_int2(offset_x, offset_y)});
4860 }
4861 }
4862 } else {
4863 // 1D tiling - tile along height only
4864 size_t rows_per_tile = std::min(max_rows_per_tile, size_t(camera.resolution.y));
4865 int Ntiles = ceil(float(camera.resolution.y) / float(rows_per_tile));
4866
4867 for (int t = 0; t < Ntiles; t++) {
4868 size_t offset_y = t * rows_per_tile;
4869 size_t height_this = std::min(rows_per_tile, camera.resolution.y - offset_y);
4870
4871 tiles.push_back({helios::make_int2(camera.resolution.x, height_this), helios::make_int2(0, offset_y)});
4872 }
4873 }
4874
4875 return tiles;
4876}
4877
4878void RadiationModel::buildGeometryData() {
4879 // Build backend-agnostic geometry data from Context primitives
4880 // This extracts all geometry information needed by the ray tracing backend
4881
4882 std::vector<uint> UUIDs = context->getAllUUIDs();
4883
4884 // Filter out invalid/zero-area primitives (same as old updateGeometry)
4885 std::vector<uint> valid_UUIDs;
4886 for (uint UUID: UUIDs) {
4887 if (!context->doesPrimitiveExist(UUID))
4888 continue;
4889
4890 float area = context->getPrimitiveArea(UUID);
4891 uint parentID = context->getPrimitiveParentObjectID(UUID);
4892 if ((area == 0 || std::isnan(area)) && context->getObjectType(parentID) != helios::OBJECT_TYPE_TILE) {
4893 continue;
4894 }
4895 valid_UUIDs.push_back(UUID);
4896 }
4897
4898 if (valid_UUIDs.empty()) {
4899 geometry_data = helios::RayTracingGeometry(); // Empty geometry
4900 return;
4901 }
4902
4903 // Reorder primitives by parent object (same ordering as old code)
4904 std::vector<uint> objID_all = context->getUniquePrimitiveParentObjectIDs(valid_UUIDs, true);
4905 std::vector<uint> primitive_UUIDs_ordered;
4906 std::unordered_set<uint> valid_set(valid_UUIDs.begin(), valid_UUIDs.end());
4907
4908 for (uint objID: objID_all) {
4909 const std::vector<uint> &prim_UUIDs = context->getObjectPrimitiveUUIDs(objID);
4910 for (uint UUID: prim_UUIDs) {
4911 if (context->doesPrimitiveExist(UUID) && valid_set.find(UUID) != valid_set.end()) {
4912 primitive_UUIDs_ordered.push_back(UUID);
4913 }
4914 }
4915 }
4916
4917 size_t Nprimitives = primitive_UUIDs_ordered.size();
4918 geometry_data.primitive_count = Nprimitives;
4919
4920 // Clear and allocate per-primitive arrays (important when updateGeometry is called multiple times)
4921 geometry_data.transform_matrices.clear();
4922 geometry_data.transform_matrices.resize(Nprimitives * 16);
4923 geometry_data.primitive_types.clear();
4924 // Initialize to UINT_MAX as sentinel - prevents uninitialized entries from matching type==0 (patch)
4925 geometry_data.primitive_types.resize(Nprimitives, UINT_MAX);
4926 geometry_data.primitive_UUIDs = primitive_UUIDs_ordered;
4927 geometry_data.primitive_IDs.clear();
4928 geometry_data.primitive_IDs.resize(Nprimitives); // Will be populated after primitiveID_indices is built
4929 geometry_data.object_IDs.clear();
4930 geometry_data.object_IDs.resize(Nprimitives);
4931 geometry_data.object_subdivisions.clear();
4932 geometry_data.object_subdivisions.resize(Nprimitives);
4933 geometry_data.twosided_flags.clear();
4934 geometry_data.twosided_flags.resize(Nprimitives);
4935 geometry_data.solid_fractions.clear();
4936 geometry_data.solid_fractions.resize(Nprimitives);
4937
4938 // Clear type-specific arrays
4939 geometry_data.patches.vertices.clear();
4940 geometry_data.patches.UUIDs.clear();
4941 geometry_data.triangles.vertices.clear();
4942 geometry_data.triangles.UUIDs.clear();
4943 geometry_data.disk_centers.clear();
4944 geometry_data.disk_radii.clear();
4945 geometry_data.disk_normals.clear();
4946 geometry_data.disk_UUIDs.clear();
4947 geometry_data.tiles.vertices.clear();
4948 geometry_data.tiles.UUIDs.clear();
4949 geometry_data.voxels.vertices.clear();
4950 geometry_data.voxels.UUIDs.clear();
4951 geometry_data.bboxes.vertices.clear();
4952 geometry_data.bboxes.UUIDs.clear();
4953
4954 // Track object IDs for compound objects
4955 uint current_objID = 0;
4956 uint last_parentID = 99999;
4957
4958 std::vector<uint> primitiveID_indices; // Maps primitives to their "object" index
4959
4960 for (size_t u = 0; u < Nprimitives; u++) {
4961 uint UUID = primitive_UUIDs_ordered[u];
4962 uint parentID = context->getPrimitiveParentObjectID(UUID);
4963
4964 if (last_parentID != parentID || parentID == 0 || context->getObjectType(parentID) != helios::OBJECT_TYPE_TILE) {
4965 primitiveID_indices.push_back(u);
4966 last_parentID = parentID;
4967 current_objID++;
4968 } else {
4969 last_parentID = parentID;
4970 }
4971
4972 geometry_data.object_IDs[u] = current_objID - 1;
4973 }
4974
4975 size_t Nobjects = primitiveID_indices.size();
4976
4977 // Populate primitiveID for runBand() compatibility
4978 primitiveID = primitiveID_indices;
4979
4980 // For backend: primitiveID[position] must return the UUID for that primitive
4981 // Sized by Nprimitives (all primitives including subpatches), not Nobjects (object entries only)
4982 std::vector<uint> primitiveID_for_backend(Nprimitives);
4983 for (size_t i = 0; i < Nprimitives; i++) {
4984 primitiveID_for_backend[i] = primitive_UUIDs_ordered[i];
4985 }
4986
4987 // Copy corrected primitiveID mapping to geometry_data for backend upload
4988 geometry_data.primitive_IDs = primitiveID_for_backend;
4989
4990 // Populate geometry for each primitive
4991 size_t patch_idx = 0, tri_idx = 0, disk_idx = 0, tile_idx = 0, voxel_idx = 0, bbox_idx = 0;
4992
4993 // Track which parent tile objects have been added to tile geometry
4994 // (tiles should have ONE geometry entry per parent object, not per subpatch)
4995 std::set<uint> processed_tile_parents;
4996
4997 // Iterate over ALL primitives to set per-primitive data
4998 // (not just Nobjects, which only has one entry per object group)
4999 for (size_t prim_idx = 0; prim_idx < Nprimitives; prim_idx++) {
5000 uint UUID = primitive_UUIDs_ordered[prim_idx];
5001
5002 // Transform matrix
5003 float m[16];
5004 uint parentID = context->getPrimitiveParentObjectID(UUID);
5005 helios::PrimitiveType type = context->getPrimitiveType(UUID);
5006
5007 // Solid fraction
5008 geometry_data.solid_fractions[prim_idx] = context->getPrimitiveSolidFraction(UUID);
5009
5010 // Two-sided flag
5011 geometry_data.twosided_flags[prim_idx] = context->getPrimitiveTwosidedFlag(UUID, 1) ? 1 : 0;
5012
5013 if (parentID > 0 && context->getObjectType(parentID) == helios::OBJECT_TYPE_TILE) {
5014 // Tile object - set per-primitive data for ALL subpatches
5015 geometry_data.primitive_types[prim_idx] = 3; // tile
5016
5017 context->getObjectTransformationMatrix(parentID, m);
5018 memcpy(&geometry_data.transform_matrices[prim_idx * 16], m, 16 * sizeof(float));
5019
5020 // Individual tile subpatches should NOT be subdivided (they're already the result of subdivision)
5021 // Only the parent tile geometry entry uses the subdivision count
5022 geometry_data.object_subdivisions[prim_idx] = helios::make_int2(1, 1);
5023
5024 // Only add ONE tile geometry entry per parent tile object (not per subpatch)
5025 // The tile intersection program handles subpatch selection internally
5026 if (processed_tile_parents.find(parentID) == processed_tile_parents.end()) {
5027 processed_tile_parents.insert(parentID);
5028
5029 std::vector<vec3> verts = context->getTileObjectVertices(parentID);
5030 for (const auto &v: verts) {
5031 geometry_data.tiles.vertices.push_back(v);
5032 }
5033
5034 // Use the first subpatch's UUID as the tile geometry UUID
5035 geometry_data.tiles.UUIDs.push_back(UUID);
5036 tile_idx++;
5037 }
5038
5039 } else if (type == helios::PRIMITIVE_TYPE_PATCH) {
5040 geometry_data.primitive_types[prim_idx] = 0; // patch
5041
5042 context->getPrimitiveTransformationMatrix(UUID, m);
5043 memcpy(&geometry_data.transform_matrices[prim_idx * 16], m, 16 * sizeof(float));
5044
5045 std::vector<vec3> verts = context->getPrimitiveVertices(UUID);
5046 for (const auto &v: verts) {
5047 geometry_data.patches.vertices.push_back(v);
5048 }
5049
5050 geometry_data.object_subdivisions[prim_idx] = helios::make_int2(1, 1);
5051
5052 // FIX: Add UUID inline to ensure consistent ordering with vertices
5053 geometry_data.patches.UUIDs.push_back(UUID);
5054
5055 patch_idx++;
5056
5057 } else if (type == helios::PRIMITIVE_TYPE_TRIANGLE) {
5058 geometry_data.primitive_types[prim_idx] = 1; // triangle
5059
5060 context->getPrimitiveTransformationMatrix(UUID, m);
5061 memcpy(&geometry_data.transform_matrices[prim_idx * 16], m, 16 * sizeof(float));
5062
5063 std::vector<vec3> verts = context->getPrimitiveVertices(UUID);
5064 for (const auto &v: verts) {
5065 geometry_data.triangles.vertices.push_back(v);
5066 }
5067
5068 geometry_data.object_subdivisions[prim_idx] = helios::make_int2(1, 1);
5069 geometry_data.triangles.UUIDs.push_back(UUID); // Store actual UUID, not position
5070 tri_idx++;
5071
5072 } else if (type == helios::PRIMITIVE_TYPE_VOXEL) {
5073 geometry_data.primitive_types[prim_idx] = 4; // voxel
5074
5075 context->getPrimitiveTransformationMatrix(UUID, m);
5076 memcpy(&geometry_data.transform_matrices[prim_idx * 16], m, 16 * sizeof(float));
5077
5078 std::vector<vec3> verts = context->getPrimitiveVertices(UUID);
5079 for (const auto &v: verts) {
5080 geometry_data.voxels.vertices.push_back(v);
5081 }
5082
5083 geometry_data.object_subdivisions[prim_idx] = helios::make_int2(1, 1);
5084 geometry_data.voxels.UUIDs.push_back(UUID); // Store actual UUID, not position
5085 voxel_idx++;
5086 }
5087 }
5088
5089 // Set counts
5090 geometry_data.patch_count = patch_idx;
5091 geometry_data.triangle_count = tri_idx;
5092 geometry_data.disk_count = disk_idx;
5093 geometry_data.tile_count = tile_idx;
5094 geometry_data.voxel_count = voxel_idx;
5095
5096 // ========== Periodic Boundary Bboxes ==========
5097 // Create bbox geometry for periodic boundary conditions
5098 // Each bbox face is a rectangular boundary at domain edge
5099
5100 // Get domain bounding box
5101 vec2 xbounds, ybounds, zbounds;
5102 context->getDomainBoundingBox(xbounds, ybounds, zbounds);
5103
5104 // Validate camera positions if periodic boundaries enabled
5105 if (periodic_flag.x == 1 || periodic_flag.y == 1) {
5106 if (!cameras.empty()) {
5107 for (auto &camera: cameras) {
5108 vec3 camerapos = camera.second.position;
5109 if (camerapos.x < xbounds.x || camerapos.x > xbounds.y || camerapos.y < ybounds.x || camerapos.y > ybounds.y) {
5110 std::cout << "WARNING (RadiationModel::buildGeometryData): camera position is outside of the domain bounding box. Disabling periodic boundary conditions." << std::endl;
5111 periodic_flag.x = 0;
5112 periodic_flag.y = 0;
5113 break;
5114 }
5115 // Extend z-bounds to include camera
5116 if (camerapos.z < zbounds.x) {
5117 zbounds.x = camerapos.z;
5118 }
5119 if (camerapos.z > zbounds.y) {
5120 zbounds.y = camerapos.z;
5121 }
5122 }
5123 }
5124 }
5125
5126 // Expand bounds slightly to ensure bbox faces are outside geometry
5127 xbounds.x -= 1e-5;
5128 xbounds.y += 1e-5;
5129 ybounds.x -= 1e-5;
5130 ybounds.y += 1e-5;
5131 zbounds.x -= 1e-5;
5132 zbounds.y += 1e-5;
5133
5134 // Bbox UUIDs must not collide with real primitive UUIDs
5135 // Use max_UUID + 1 as base (not Nprimitives, which can cause collisions with sparse UUIDs)
5136 uint max_UUID = geometry_data.primitive_UUIDs.empty() ? 0 : *std::max_element(geometry_data.primitive_UUIDs.begin(), geometry_data.primitive_UUIDs.end());
5137 uint bbox_UUID_base = max_UUID + 1;
5138
5139 // Create bbox faces based on periodic flags
5140 if (periodic_flag.x == 1) {
5141 // -x facing boundary (4 vertices: counter-clockwise from bottom-left)
5142 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.x, zbounds.x));
5143 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.y, zbounds.x));
5144 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.y, zbounds.y));
5145 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.x, zbounds.y));
5146 geometry_data.bboxes.UUIDs.push_back(bbox_UUID_base + bbox_idx);
5147 bbox_idx++;
5148
5149 // +x facing boundary
5150 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.x, zbounds.x));
5151 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.y, zbounds.x));
5152 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.y, zbounds.y));
5153 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.x, zbounds.y));
5154 geometry_data.bboxes.UUIDs.push_back(bbox_UUID_base + bbox_idx);
5155 bbox_idx++;
5156 }
5157
5158 if (periodic_flag.y == 1) {
5159 // -y facing boundary
5160 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.x, zbounds.x));
5161 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.x, zbounds.x));
5162 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.x, zbounds.y));
5163 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.x, zbounds.y));
5164 geometry_data.bboxes.UUIDs.push_back(bbox_UUID_base + bbox_idx);
5165 bbox_idx++;
5166
5167 // +y facing boundary
5168 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.y, zbounds.x));
5169 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.y, zbounds.x));
5170 geometry_data.bboxes.vertices.push_back(vec3(xbounds.y, ybounds.y, zbounds.y));
5171 geometry_data.bboxes.vertices.push_back(vec3(xbounds.x, ybounds.y, zbounds.y));
5172 geometry_data.bboxes.UUIDs.push_back(bbox_UUID_base + bbox_idx);
5173 bbox_idx++;
5174 }
5175
5176 // Update bbox count and UUID base
5177 geometry_data.bbox_count = bbox_idx;
5178 if (bbox_idx > 0) {
5179 geometry_data.bbox_UUID_base = bbox_UUID_base;
5180 } else {
5181 // No bboxes: set sentinel value so GPU knows all UUIDs are real primitives
5182 geometry_data.bbox_UUID_base = UINT_MAX;
5183 }
5184
5185 // Add bbox primitive data so hit/intersection shaders can access them
5186 // Bbox positions are after real primitives: primitive_count + bbox_idx
5187 if (bbox_idx > 0) {
5188 geometry_data.primitive_types.resize(geometry_data.primitive_count + bbox_idx, UINT_MAX);
5189 geometry_data.twosided_flags.resize(geometry_data.primitive_count + bbox_idx, 0);
5190 geometry_data.solid_fractions.resize(geometry_data.primitive_count + bbox_idx, 1.0f);
5191 geometry_data.object_IDs.resize(geometry_data.primitive_count + bbox_idx, UINT_MAX);
5192 geometry_data.object_subdivisions.resize(geometry_data.primitive_count + bbox_idx, make_int2(1, 1));
5193 geometry_data.transform_matrices.resize((geometry_data.primitive_count + bbox_idx) * 16, 0.0f);
5194 geometry_data.primitive_IDs.resize(geometry_data.primitive_count + bbox_idx, UINT_MAX);
5195
5196 for (size_t i = 0; i < bbox_idx; i++) {
5197 size_t bbox_pos = geometry_data.primitive_count + i;
5198 geometry_data.primitive_types[bbox_pos] = 5; // type=5 for bbox
5199 geometry_data.twosided_flags[bbox_pos] = 1; // bboxes are two-sided
5200 geometry_data.solid_fractions[bbox_pos] = 1.0f; // bboxes are fully solid
5201 geometry_data.object_IDs[bbox_pos] = geometry_data.primitive_count + i; // Match master: Nobjects + i
5202 geometry_data.object_subdivisions[bbox_pos] = make_int2(1, 1); // no subdivisions
5203 geometry_data.primitive_IDs[bbox_pos] = bbox_UUID_base + i; // bbox UUID
5204
5205 // Set identity transform matrix for bbox
5206 geometry_data.transform_matrices[bbox_pos * 16 + 0] = 1.0f; // m00
5207 geometry_data.transform_matrices[bbox_pos * 16 + 5] = 1.0f; // m11
5208 geometry_data.transform_matrices[bbox_pos * 16 + 10] = 1.0f; // m22
5209 geometry_data.transform_matrices[bbox_pos * 16 + 15] = 1.0f; // m33
5210 }
5211 }
5212
5213 // Periodic boundary condition
5214 geometry_data.periodic_flag = periodic_flag;
5215
5216 // Extract texture mask and UV data for primitives with transparency textures
5217 buildTextureData();
5218
5219 // Build primitive_positions lookup table for GPU UUID→position conversion
5220 // Size by max UUID to create sparse lookup table (includes bbox UUIDs now that they don't collide)
5221 // Clear first to remove stale mappings from deleted primitives
5222 geometry_data.primitive_positions.clear();
5223 if (!geometry_data.primitive_UUIDs.empty()) {
5224 uint max_UUID = *std::max_element(geometry_data.primitive_UUIDs.begin(), geometry_data.primitive_UUIDs.end());
5225
5226 // Expand to include bbox UUIDs if present (they now use max_UUID+1 base, so no collisions)
5227 uint bbox_max_UUID = max_UUID;
5228 if (geometry_data.bbox_count > 0) {
5229 bbox_max_UUID = geometry_data.bbox_UUID_base + geometry_data.bbox_count - 1;
5230 }
5231
5232 geometry_data.primitive_positions.resize(bbox_max_UUID + 1, UINT_MAX); // UINT_MAX = invalid/unused
5233
5234 // Map real primitive UUIDs
5235 for (size_t i = 0; i < geometry_data.primitive_count; i++) {
5236 uint UUID = geometry_data.primitive_UUIDs[i];
5237 geometry_data.primitive_positions[UUID] = i; // Map UUID → array position
5238 }
5239
5240 // Map bbox UUIDs to their positions (after real primitives)
5241 // Now safe because bbox_UUID_base = max_UUID + 1 (no collisions)
5242 if (geometry_data.bbox_count > 0) {
5243 for (size_t i = 0; i < geometry_data.bbox_count; i++) {
5244 uint bbox_UUID = geometry_data.bbox_UUID_base + i;
5245 geometry_data.primitive_positions[bbox_UUID] = geometry_data.primitive_count + i;
5246 }
5247 }
5248 }
5249}
5250
5251void RadiationModel::buildTextureData() {
5252 // Extract texture mask and UV data for all primitives with transparency textures
5253
5254 size_t Nobjects = geometry_data.primitive_count;
5255
5256 // Clear any previous texture data (important when updateGeometry is called multiple times)
5257 geometry_data.mask_data.clear();
5258 geometry_data.mask_sizes.clear();
5259 geometry_data.uv_data.clear();
5260
5261 // Initialize with -1 (no texture)
5262 geometry_data.mask_IDs.clear();
5263 geometry_data.mask_IDs.resize(Nobjects, -1);
5264 geometry_data.uv_IDs.clear();
5265 geometry_data.uv_IDs.resize(Nobjects, -1);
5266
5267 // Cache to avoid duplicate mask data for primitives using the same texture file
5268 std::map<std::string, int> texture_to_mask_idx;
5269
5270 for (size_t prim_idx = 0; prim_idx < Nobjects; prim_idx++) {
5271 uint UUID = geometry_data.primitive_UUIDs[prim_idx];
5272
5273 // Check if primitive has a texture file
5274 std::string texture_file = context->getPrimitiveTextureFile(UUID);
5275 if (texture_file.empty()) {
5276 continue; // No texture - mask_ID stays -1
5277 }
5278
5279 // Check if texture has transparency channel (alpha)
5280 if (!context->primitiveTextureHasTransparencyChannel(UUID)) {
5281 continue; // No transparency - mask_ID stays -1 (e.g., JPEG files)
5282 }
5283
5284 // Check cache for existing mask from same texture file
5285 int mask_idx;
5286 auto cache_it = texture_to_mask_idx.find(texture_file);
5287 if (cache_it != texture_to_mask_idx.end()) {
5288 // Reuse existing mask
5289 mask_idx = cache_it->second;
5290 } else {
5291 // New texture - extract mask data
5292 const std::vector<std::vector<bool>> *trans_data = context->getPrimitiveTextureTransparencyData(UUID);
5293 helios::int2 tex_size = context->getPrimitiveTextureSize(UUID);
5294
5295 mask_idx = static_cast<int>(geometry_data.mask_sizes.size());
5296 texture_to_mask_idx[texture_file] = mask_idx;
5297
5298 // Flatten 2D bool array to 1D (row-major: [y][x])
5299 // Backend expects: for each mask m, iterate [y][x] order
5300 for (int y = 0; y < tex_size.y; y++) {
5301 for (int x = 0; x < tex_size.x; x++) {
5302 geometry_data.mask_data.push_back((*trans_data)[y][x]);
5303 }
5304 }
5305 geometry_data.mask_sizes.push_back(tex_size);
5306 }
5307
5308 geometry_data.mask_IDs[prim_idx] = mask_idx;
5309
5310 // Extract UV coordinates for this primitive
5311 // uv_IDs stores the position index (not offset), used to access uvdata[vertex][position] in CUDA
5312 std::vector<helios::vec2> uvs = context->getPrimitiveTextureUV(UUID);
5313 if (!uvs.empty()) {
5314 geometry_data.uv_IDs[prim_idx] = static_cast<int>(prim_idx); // Store position index for CUDA 2D buffer access
5315 for (const auto &uv: uvs) {
5316 geometry_data.uv_data.push_back(uv);
5317 }
5318 // Pad to 4 vertices if needed (CUDA expects max 4 vertices per primitive)
5319 size_t start_idx = geometry_data.uv_data.size() - uvs.size();
5320 while (geometry_data.uv_data.size() - start_idx < 4) {
5321 geometry_data.uv_data.push_back(uvs.back());
5322 }
5323 }
5324 // If uvs is empty, uv_ID stays -1 and CUDA will use default UV mapping
5325 }
5326}
5327
5329 buildGeometryData();
5330 return geometry_data.primitive_count;
5331}
5332
5333void RadiationModel::buildUUIDMapping() {
5334 // Build bidirectional UUID ↔ array position mapping
5335 // This enables efficient conversion between UUID values and array indices
5336
5337 uuid_to_position.clear();
5338 position_to_uuid.clear();
5339
5340 // geometry_data.primitive_UUIDs is already ordered by object
5341 // Build mapping from this ordered list
5342 for (size_t i = 0; i < geometry_data.primitive_count; i++) {
5343 uint UUID = geometry_data.primitive_UUIDs[i];
5344 uuid_to_position[UUID] = i;
5345 position_to_uuid.push_back(UUID);
5346 }
5347
5348 // Build type-safe mapper (new indexing system)
5349 // Provides compile-time safety for UUID/position conversions
5350 geometry_data.mapper.build(geometry_data.primitive_UUIDs);
5351}
5352
5353static void validateAndCorrectMaterialProperties(float &rho, float &tau, float eps, bool emission_enabled, uint scattering_depth, const std::string &band_label, uint UUID, helios::WarningAggregator *warnings = nullptr) {
5354 // Helper function to enforce energy conservation constraints on material properties
5355 // Mirrors the validation logic from updateRadiativeProperties() (lines 2672-2686)
5356
5357 // 1. Clamp rho and tau to [0,1] with warnings for out-of-range values
5358 if (rho < 0.f || rho > 1.f) {
5359 if (warnings) {
5360 warnings->addWarning("material_property_clamping", "Reflectivity out of range [0,1] for band " + band_label + ", primitive #" + std::to_string(UUID) + ": rho=" + std::to_string(rho) + ". Clamping to valid range.");
5361 }
5362 rho = std::max(0.f, std::min(1.f, rho));
5363 }
5364
5365 if (tau < 0.f || tau > 1.f) {
5366 if (warnings) {
5367 warnings->addWarning("material_property_clamping", "Transmissivity out of range [0,1] for band " + band_label + ", primitive #" + std::to_string(UUID) + ": tau=" + std::to_string(tau) + ". Clamping to valid range.");
5368 }
5369 tau = std::max(0.f, std::min(1.f, tau));
5370 }
5371
5372 // 2. Apply emission-specific constraints
5373 if (emission_enabled) {
5374 // Special case: blackbody emission (scatteringDepth=0 requires eps=1, rho=0, tau=0)
5375 if (scattering_depth == 0 && eps != 1.f) {
5376 if (warnings && (rho != 0.f || tau != 0.f)) {
5377 warnings->addWarning("blackbody_override", "Band " + band_label + " has emission with scatteringDepth=0, " + "enforcing blackbody behavior (eps=1, rho=0, tau=0) for primitive #" + std::to_string(UUID));
5378 }
5379 rho = 0.f;
5380 tau = 0.f;
5381 }
5382 // General emission case: check energy conservation (eps + rho + tau = 1)
5383 else if (eps != 1.f && rho == 0 && tau == 0) {
5384 // Auto-correct: set rho = 1 - eps
5385 rho = 1.f - eps;
5386 } else if (std::abs(eps + rho + tau - 1.f) > 1e-5f && eps > 0.f) {
5387 // Cannot auto-correct, throw error
5388 helios_runtime_error(std::string("ERROR (RadiationModel): emissivity, transmissivity, and reflectivity ") + "must sum to 1 to ensure energy conservation. Band " + band_label + ", Primitive #" + std::to_string(UUID) +
5389 ": eps=" + std::to_string(eps) + ", tau=" + std::to_string(tau) + ", rho=" + std::to_string(rho) + ". It is also possible that you forgot to disable emission for this band.");
5390 }
5391 } else {
5392 // 3. Non-emission case: rho + tau must be ≤ 1
5393 if (rho + tau > 1.f) {
5394 helios_runtime_error(std::string("ERROR (RadiationModel): transmissivity and reflectivity cannot sum to ") + "greater than 1 to ensure energy conservation. Band " + band_label + ", Primitive #" + std::to_string(UUID) +
5395 ": eps=" + std::to_string(eps) + ", tau=" + std::to_string(tau) + ", rho=" + std::to_string(rho) + ". It is also possible that you forgot to disable emission for this band.");
5396 }
5397 }
5398}
5399
5400void RadiationModel::buildMaterialData() {
5401 // Build backend-agnostic material data from Context primitive data
5402
5403 // Warning aggregator for energy conservation issues
5405
5406 size_t Nprims = geometry_data.primitive_count;
5407 size_t Nbands = radiation_bands.size();
5408 size_t Nsources = radiation_sources.size();
5409
5410 material_data.num_primitives = Nprims;
5411 material_data.num_bands = Nbands;
5412 material_data.num_sources = Nsources;
5413 material_data.num_cameras = cameras.size();
5414
5415 // Allocate arrays (indexed as [source][primitive][band] using MaterialPropertyIndexer)
5416 // NOTE: Bboxes don't need material properties (they only wrap rays for periodic boundaries)
5417 size_t total_size = Nsources * Nbands * Nprims;
5418 material_data.reflectivity.resize(total_size, 0.0f);
5419 material_data.transmissivity.resize(total_size, 0.0f);
5420 material_data.specular_exponent.resize(Nprims, -1.0f); // Default -1 means disabled
5421 material_data.specular_scale.resize(Nprims, 0.0f);
5422
5423 // Create indexer for material properties: [source][primitive][band]
5424 MaterialPropertyIndexer mat_indexer(Nsources, Nprims, Nbands);
5425
5426 // Cache unique spectral data to avoid redundant loads
5427 std::map<std::string, std::vector<helios::vec2>> unique_rho_spectra;
5428 std::map<std::string, std::vector<helios::vec2>> unique_tau_spectra;
5429
5430 for (size_t p = 0; p < Nprims; p++) {
5431 uint UUID = geometry_data.primitive_UUIDs[p];
5432
5433 // Cache reflectivity spectra
5434 if (context->doesPrimitiveDataExist(UUID, "reflectivity_spectrum")) {
5435 std::string spectrum_label;
5436 context->getPrimitiveData(UUID, "reflectivity_spectrum", spectrum_label);
5437 if (unique_rho_spectra.find(spectrum_label) == unique_rho_spectra.end()) {
5438 // Only load if spectrum exists in global data
5439 if (context->doesGlobalDataExist(spectrum_label.c_str())) {
5440 unique_rho_spectra[spectrum_label] = loadSpectralData(spectrum_label);
5441 }
5442 }
5443 }
5444
5445 // Cache transmissivity spectra
5446 if (context->doesPrimitiveDataExist(UUID, "transmissivity_spectrum")) {
5447 std::string spectrum_label;
5448 context->getPrimitiveData(UUID, "transmissivity_spectrum", spectrum_label);
5449 if (unique_tau_spectra.find(spectrum_label) == unique_tau_spectra.end()) {
5450 // Only load if spectrum exists in global data
5451 if (context->doesGlobalDataExist(spectrum_label.c_str())) {
5452 unique_tau_spectra[spectrum_label] = loadSpectralData(spectrum_label);
5453 }
5454 }
5455 }
5456 }
5457
5458 // Extract material properties from Context primitives
5459 size_t b_idx = 0;
5460 for (const auto &band_pair: radiation_bands) {
5461 std::string band_label = band_pair.second.label;
5462
5463 for (size_t s = 0; s < Nsources; s++) {
5464 for (size_t p = 0; p < Nprims; p++) {
5465 uint UUID = geometry_data.primitive_UUIDs[p];
5466
5467 // Use BufferIndexer for safe, verifiable indexing
5468 // Note: p is already the array position, so we use p directly (not UUID)
5469 size_t idx = mat_indexer(s, p, b_idx);
5470
5471 // Get reflectivity - try spectrum first, then per-band label
5472 float rho = rho_default;
5473
5474 if (context->doesPrimitiveDataExist(UUID, "reflectivity_spectrum")) {
5475 // Spectrum-based reflectivity
5476 std::string spectrum_label;
5477 context->getPrimitiveData(UUID, "reflectivity_spectrum", spectrum_label);
5478
5479 // Get spectrum from cache
5480 if (unique_rho_spectra.find(spectrum_label) != unique_rho_spectra.end()) {
5481 const std::vector<helios::vec2> &spectrum = unique_rho_spectra.at(spectrum_label);
5482
5483 // Get band wavelength bounds
5484 helios::vec2 wavebounds = band_pair.second.wavebandBounds;
5485
5486 // Only require wavelength bounds if band performs scattering/absorption
5487 // Emission-only bands (scatteringDepth==0) use Stefan-Boltzmann and don't need spectral integration
5488 // Ray launches for emission don't require wavelength bounds since emission properties are wavelength-independent
5489 bool needs_spectral_integration = (band_pair.second.scatteringDepth > 0);
5490
5491 if (needs_spectral_integration && wavebounds.x == 0 && wavebounds.y == 0) {
5492 helios_runtime_error("ERROR (RadiationModel::buildMaterialData): Band '" + band_label + "' has no wavelength bounds - required for spectral integration");
5493 }
5494
5495 // Integrate spectrum over band wavelength range (only if bounds are defined)
5496 if (wavebounds.x != 0 || wavebounds.y != 0) {
5497 if (!radiation_sources[s].source_spectrum.empty()) {
5498 // Weight by source spectrum
5499 rho = integrateSpectrum(s, spectrum, wavebounds.x, wavebounds.y);
5500 } else {
5501 // Uniform integration (divide by wavelength range to normalize)
5502 rho = integrateSpectrum(spectrum, wavebounds.x, wavebounds.y) / (wavebounds.y - wavebounds.x);
5503 }
5504 }
5505 // else: emission-only band, rho remains at default value (should be 0 for blackbody)
5506 }
5507 } else {
5508 // Per-band reflectivity (backward compatibility)
5509 std::string rho_label = "reflectivity_" + band_label;
5510 if (context->doesPrimitiveDataExist(UUID, rho_label.c_str())) {
5511 context->getPrimitiveData(UUID, rho_label.c_str(), rho);
5512 }
5513 }
5514
5515 // Get transmissivity - try spectrum first, then per-band label
5516 float tau = tau_default;
5517
5518 if (context->doesPrimitiveDataExist(UUID, "transmissivity_spectrum")) {
5519 // Spectrum-based transmissivity
5520 std::string spectrum_label;
5521 context->getPrimitiveData(UUID, "transmissivity_spectrum", spectrum_label);
5522
5523 // Get spectrum from cache
5524 if (unique_tau_spectra.find(spectrum_label) != unique_tau_spectra.end()) {
5525 const std::vector<helios::vec2> &spectrum = unique_tau_spectra.at(spectrum_label);
5526
5527 // Get band wavelength bounds
5528 helios::vec2 wavebounds = band_pair.second.wavebandBounds;
5529
5530 // Only require wavelength bounds if band performs scattering/absorption
5531 // Emission-only bands (scatteringDepth==0) use Stefan-Boltzmann and don't need spectral integration
5532 // Ray launches for emission don't require wavelength bounds since emission properties are wavelength-independent
5533 bool needs_spectral_integration = (band_pair.second.scatteringDepth > 0);
5534
5535 if (needs_spectral_integration && wavebounds.x == 0 && wavebounds.y == 0) {
5536 helios_runtime_error("ERROR (RadiationModel::buildMaterialData): Band '" + band_label + "' has no wavelength bounds - required for spectral integration");
5537 }
5538
5539 // Integrate spectrum over band wavelength range (only if bounds are defined)
5540 if (wavebounds.x != 0 || wavebounds.y != 0) {
5541 if (!radiation_sources[s].source_spectrum.empty()) {
5542 // Weight by source spectrum
5543 tau = integrateSpectrum(s, spectrum, wavebounds.x, wavebounds.y);
5544 } else {
5545 // Uniform integration
5546 tau = integrateSpectrum(spectrum, wavebounds.x, wavebounds.y) / (wavebounds.y - wavebounds.x);
5547 }
5548 }
5549 // else: emission-only band, tau remains at default value (should be 0 for blackbody)
5550 }
5551 } else {
5552 // Per-band transmissivity (backward compatibility)
5553 std::string tau_label = "transmissivity_" + band_label;
5554 if (context->doesPrimitiveDataExist(UUID, tau_label.c_str())) {
5555 context->getPrimitiveData(UUID, tau_label.c_str(), tau);
5556 }
5557 }
5558
5559 // Get emissivity for validation
5560 float eps = eps_default;
5561 std::string eps_label = "emissivity_" + band_label;
5562 if (context->doesPrimitiveDataExist(UUID, eps_label.c_str())) {
5563 context->getPrimitiveData(UUID, eps_label.c_str(), eps);
5564 }
5565
5566 // Validate and correct material properties to ensure energy conservation
5567 const RadiationBand &band = band_pair.second;
5568 validateAndCorrectMaterialProperties(rho, tau, eps, band.emissionFlag, band.scatteringDepth, band_label, UUID, &warnings);
5569
5570 // Store validated properties
5571 material_data.reflectivity[idx] = rho;
5572 material_data.transmissivity[idx] = tau;
5573 }
5574 }
5575 b_idx++;
5576 }
5577
5578 // NOTE: Bboxes don't need material properties - they only wrap rays for periodic boundaries
5579 // Material buffers are sized for real primitives only (Nprims), not including bboxes
5580
5581 // Load specular reflection properties from primitive data
5582 bool specular_exponent_specified = false;
5583 bool specular_scale_specified = false;
5584
5585 for (size_t p = 0; p < Nprims; p++) {
5586 uint UUID = geometry_data.primitive_UUIDs[p];
5587
5588 if (context->doesPrimitiveDataExist(UUID, "specular_exponent") && context->getPrimitiveDataType("specular_exponent") == helios::HELIOS_TYPE_FLOAT) {
5589 context->getPrimitiveData(UUID, "specular_exponent", material_data.specular_exponent.at(p));
5590 if (material_data.specular_exponent.at(p) >= 0.f) {
5591 specular_exponent_specified = true;
5592 }
5593 }
5594
5595 if (context->doesPrimitiveDataExist(UUID, "specular_scale") && context->getPrimitiveDataType("specular_scale") == helios::HELIOS_TYPE_FLOAT) {
5596 context->getPrimitiveData(UUID, "specular_scale", material_data.specular_scale.at(p));
5597 if (material_data.specular_scale.at(p) > 0.f) {
5598 specular_scale_specified = true;
5599 }
5600 }
5601 }
5602
5603 // Auto-enable specular reflection if specular properties are specified on any primitive
5604 if (specular_exponent_specified) {
5605 if (specular_scale_specified) {
5606 specular_reflection_mode = 2; // Mode 2: use primitive specular_scale
5607 } else {
5608 specular_reflection_mode = 1; // Mode 1: use default 0.25 scale
5609 }
5610 } else {
5611 specular_reflection_mode = 0; // Disabled
5612 }
5613
5614 // Report any accumulated warnings
5615 warnings.report();
5616}
5617
5618void RadiationModel::buildSourceData() {
5619 // Build backend-agnostic source data from radiation_sources
5620
5621 source_data.clear();
5622 source_data.reserve(radiation_sources.size());
5623
5624 for (size_t s = 0; s < radiation_sources.size(); s++) {
5625 const auto &src = radiation_sources[s];
5626 helios::RayTracingSource backend_src;
5627 backend_src.position = src.source_position;
5628 backend_src.rotation = src.source_rotation;
5629 backend_src.width = src.source_width;
5630 backend_src.type = src.source_type;
5631
5632 // Flatten flux arrays - use getSourceFlux() to handle -1.f sentinel values
5633 backend_src.fluxes.clear();
5634 backend_src.fluxes_cam.clear();
5635 for (const auto &band_pair: radiation_bands) {
5636 std::string band_label = band_pair.second.label;
5637 // Use getSourceFlux() which properly handles -1.f sentinel (returns 0 or integrates spectrum)
5638 float flux = getSourceFlux(s, band_label);
5639 backend_src.fluxes.push_back(flux);
5640 backend_src.fluxes_cam.push_back(flux); // Same for now
5641 }
5642
5643 source_data.push_back(backend_src);
5644 }
5645}
5646
5647
5649 return backend.get();
5650}
5651
5655
5659
5660std::vector<helios::RayTracingSource> &RadiationModel::getSourceData() {
5661 return source_data;
5662}
5663
5664
5666 buildGeometryData();
5667 buildMaterialData();
5668 buildSourceData();
5669}