1.3.64
 
Loading...
Searching...
No Matches
LensFlare.cpp
Go to the documentation of this file.
1
16#include "RadiationModel.h" // Must be included first for LensFlareProperties definition
17#include "LensFlare.h"
18#include <algorithm>
19#include <cmath>
20
21using namespace helios;
22
23// Define static constexpr arrays
24constexpr float LensFlare::ghost_scales_[];
25constexpr float LensFlare::ghost_sizes_[];
26
27LensFlare::LensFlare(const LensFlareProperties &lens_flare_props, int2 resolution) : props_(lens_flare_props), resolution_(resolution) {
28 // Determine kernel size based on image resolution (use power of 2)
29 int max_dim = std::max(resolution.x, resolution.y);
30 kernel_size_ = 64; // Minimum kernel size
31 while (kernel_size_ < max_dim / 4 && kernel_size_ < 256) {
32 kernel_size_ *= 2;
33 }
34
35 // Pre-compute the starburst kernel
36 generateStarburstKernel();
37}
38
39void LensFlare::apply(std::map<std::string, std::vector<float>> &pixel_data, int2 resolution) {
40 if (pixel_data.empty()) {
41 return;
42 }
43
44 // Find bright pixels that will generate lens flare
45 auto bright_pixels = findBrightPixels(pixel_data, resolution, props_.intensity_threshold);
46
47 if (bright_pixels.empty()) {
48 return; // No bright pixels, nothing to do
49 }
50
51 // Apply starburst diffraction pattern
52 if (props_.starburst_intensity > 0.0f) {
53 applyStarburst(pixel_data, resolution, bright_pixels);
54 }
55
56 // Apply ghost reflections
57 if (props_.ghost_intensity > 0.0f) {
58 applyGhosts(pixel_data, resolution, bright_pixels);
59 }
60}
61
62std::vector<std::tuple<int, int, float>> LensFlare::findBrightPixels(const std::map<std::string, std::vector<float>> &pixel_data, int2 resolution, float threshold) const {
63 std::vector<std::tuple<int, int, float>> bright_pixels;
64
65 if (pixel_data.empty()) {
66 return bright_pixels;
67 }
68
69 int num_pixels = resolution.x * resolution.y;
70
71 // Get the first band to determine pixel count
72 const auto &first_band = pixel_data.begin()->second;
73 if (static_cast<int>(first_band.size()) != num_pixels) {
74 return bright_pixels;
75 }
76
77 // Calculate per-pixel maximum intensity across all bands
78 std::vector<float> max_intensity(num_pixels, 0.0f);
79 for (const auto &band_pair: pixel_data) {
80 const auto &band_data = band_pair.second;
81 for (int i = 0; i < num_pixels; ++i) {
82 max_intensity[i] = std::max(max_intensity[i], band_data[i]);
83 }
84 }
85
86 // Find pixels above threshold
87 for (int j = 0; j < resolution.y; ++j) {
88 for (int i = 0; i < resolution.x; ++i) {
89 int idx = j * resolution.x + i;
90 if (max_intensity[idx] >= threshold) {
91 bright_pixels.emplace_back(i, j, max_intensity[idx]);
92 }
93 }
94 }
95
96 // Sort by intensity (brightest first) and limit count for performance
97 std::sort(bright_pixels.begin(), bright_pixels.end(), [](const auto &a, const auto &b) { return std::get<2>(a) > std::get<2>(b); });
98
99 // Limit to top 100 bright pixels to maintain performance
100 constexpr size_t max_bright_pixels = 100;
101 if (bright_pixels.size() > max_bright_pixels) {
102 bright_pixels.resize(max_bright_pixels);
103 }
104
105 return bright_pixels;
106}
107
108void LensFlare::generateStarburstKernel() {
109 // Generate aperture mask
110 std::vector<float> aperture_mask;
111 generateApertureMask(aperture_mask);
112
113 // Compute 2D FFT
114 std::vector<std::complex<float>> fft_result;
115 fft2D(aperture_mask, fft_result, kernel_size_);
116
117 // Extract magnitude and normalize
118 starburst_kernel_.resize(kernel_size_ * kernel_size_);
119 float max_magnitude = 0.0f;
120 for (size_t i = 0; i < fft_result.size(); ++i) {
121 float mag = std::abs(fft_result[i]);
122 starburst_kernel_[i] = mag;
123 max_magnitude = std::max(max_magnitude, mag);
124 }
125
126 // Normalize kernel to [0, 1]
127 if (max_magnitude > 0.0f) {
128 for (float &val: starburst_kernel_) {
129 val /= max_magnitude;
130 }
131 }
132
133 // Apply radial window to fade at edges (eliminates square box artifact)
134 float center = static_cast<float>(kernel_size_) / 2.0f;
135 float max_dist = center * 1.414f; // sqrt(2) * center = corner distance
136 for (int y = 0; y < kernel_size_; ++y) {
137 for (int x = 0; x < kernel_size_; ++x) {
138 size_t i = y * kernel_size_ + x;
139 float dx = x - center;
140 float dy = y - center;
141 float dist = std::sqrt(dx * dx + dy * dy);
142 float normalized_dist = std::min(dist / center, 1.0f); // Normalize to aperture radius
143
144 // Smooth cosine window: 1 at center, 0 at radius
145 float window = (normalized_dist < 1.0f) ? (0.5f + 0.5f * std::cos(normalized_dist * M_PI)) : 0.0f;
146 starburst_kernel_[i] *= window;
147 }
148 }
149
150 // Apply power falloff to make spikes more pronounced
151 for (float &val: starburst_kernel_) {
152 val = std::pow(val, 0.5f); // Square root to enhance spikes
153 }
154}
155
156void LensFlare::generateApertureMask(std::vector<float> &mask) const {
157 mask.resize(kernel_size_ * kernel_size_, 0.0f);
158
159 int blade_count = props_.aperture_blade_count;
160 float center = static_cast<float>(kernel_size_) / 2.0f;
161 float radius = center * 0.8f; // Aperture radius (80% of half-size)
162
163 // Generate N-gon vertices
164 std::vector<std::pair<float, float>> vertices(blade_count);
165 for (int i = 0; i < blade_count; ++i) {
166 float angle = 2.0f * static_cast<float>(M_PI) * static_cast<float>(i) / static_cast<float>(blade_count);
167 // Rotate by 90 degrees so one point is at top
168 angle += static_cast<float>(M_PI) / 2.0f;
169 vertices[i] = {center + radius * std::cos(angle), center + radius * std::sin(angle)};
170 }
171
172 // Fill polygon using scanline algorithm
173 for (int y = 0; y < kernel_size_; ++y) {
174 for (int x = 0; x < kernel_size_; ++x) {
175 // Point-in-polygon test using ray casting
176 float px = static_cast<float>(x) + 0.5f;
177 float py = static_cast<float>(y) + 0.5f;
178
179 int crossings = 0;
180 for (int i = 0; i < blade_count; ++i) {
181 int j = (i + 1) % blade_count;
182 float x1 = vertices[i].first, y1 = vertices[i].second;
183 float x2 = vertices[j].first, y2 = vertices[j].second;
184
185 // Check if ray from (px, py) going right crosses edge
186 if ((y1 <= py && y2 > py) || (y2 <= py && y1 > py)) {
187 // Compute x coordinate of intersection
188 float t = (py - y1) / (y2 - y1);
189 float x_intersect = x1 + t * (x2 - x1);
190 if (px < x_intersect) {
191 crossings++;
192 }
193 }
194 }
195
196 // Odd number of crossings = inside polygon
197 if (crossings % 2 == 1) {
198 mask[y * kernel_size_ + x] = 1.0f;
199 }
200 }
201 }
202
203 // Apply soft edge anti-aliasing (optional smoothing)
204 // Not implemented for simplicity - the FFT handles most aliasing
205}
206
207void LensFlare::applyStarburst(std::map<std::string, std::vector<float>> &pixel_data, int2 resolution, const std::vector<std::tuple<int, int, float>> &bright_pixels) {
208 if (starburst_kernel_.empty() || bright_pixels.empty()) {
209 return;
210 }
211
212 float intensity_scale = props_.starburst_intensity * 0.05f; // Reduced for subtler effect
213 int half_kernel = kernel_size_ / 2;
214
215 // Apply starburst kernel centered on each bright pixel
216 for (const auto &bright_pixel: bright_pixels) {
217 int bx = std::get<0>(bright_pixel);
218 int by = std::get<1>(bright_pixel);
219 float pixel_intensity = std::get<2>(bright_pixel);
220
221 // Extract color ratios from the source pixel
222 int pixel_idx = by * resolution.x + bx;
223 std::map<std::string, float> color_ratios;
224 float total = 0.0f;
225 for (const auto &[band_label, band_data]: pixel_data) {
226 float value = band_data[pixel_idx];
227 color_ratios[band_label] = value;
228 total += value;
229 }
230 // Normalize color ratios
231 if (total > 0.0f) {
232 for (auto &[band_label, ratio]: color_ratios) {
233 ratio /= total;
234 }
235 }
236
237 // Scale starburst by pixel brightness relative to threshold
238 float brightness_factor = (pixel_intensity - props_.intensity_threshold) / (1.0f - props_.intensity_threshold + 0.001f);
239 brightness_factor = std::clamp(brightness_factor, 0.0f, 1.0f);
240 float scaled_intensity = intensity_scale * brightness_factor * pixel_intensity;
241
242 // Add starburst contribution to each band with source color
243 for (auto &[band_label, band_data]: pixel_data) {
244 float band_scale = color_ratios[band_label] * scaled_intensity;
245
246 for (int ky = 0; ky < kernel_size_; ++ky) {
247 for (int kx = 0; kx < kernel_size_; ++kx) {
248 // Map kernel coordinate to image coordinate (centered on bright pixel)
249 int ix = bx + kx - half_kernel;
250 int iy = by + ky - half_kernel;
251
252 // Skip pixels outside image bounds
253 if (ix < 0 || ix >= resolution.x || iy < 0 || iy >= resolution.y) {
254 continue;
255 }
256
257 int img_idx = iy * resolution.x + ix;
258 int kern_idx = ky * kernel_size_ + kx;
259
260 // Add kernel contribution with color (additive blending)
261 band_data[img_idx] += starburst_kernel_[kern_idx] * band_scale;
262 }
263 }
264 }
265 }
266}
267
268void LensFlare::applyGhosts(std::map<std::string, std::vector<float>> &pixel_data, int2 resolution, const std::vector<std::tuple<int, int, float>> &bright_pixels) {
269 if (bright_pixels.empty()) {
270 return;
271 }
272
273 float center_x = static_cast<float>(resolution.x) / 2.0f;
274 float center_y = static_cast<float>(resolution.y) / 2.0f;
275
276 // Base reflectance from coating efficiency
277 // Each ghost involves 2 reflections, so intensity = (1 - coating_efficiency)^2
278 float coating_reflectance = 1.0f - props_.coating_efficiency;
279 float base_reflectance = coating_reflectance * coating_reflectance * props_.ghost_intensity;
280
281 int num_ghosts = std::min(props_.ghost_count, static_cast<int>(sizeof(ghost_scales_) / sizeof(ghost_scales_[0])));
282
283 for (const auto &bright_pixel: bright_pixels) {
284 int bx = std::get<0>(bright_pixel);
285 int by = std::get<1>(bright_pixel);
286 float pixel_intensity = std::get<2>(bright_pixel);
287
288 // Extract color from source pixel
289 int pixel_idx = by * resolution.x + bx;
290 std::map<std::string, float> source_color;
291 for (const auto &[band_label, band_data]: pixel_data) {
292 source_color[band_label] = band_data[pixel_idx];
293 }
294
295 // Vector from center to bright pixel
296 float dx = static_cast<float>(bx) - center_x;
297 float dy = static_cast<float>(by) - center_y;
298
299 // Distance from center (for Fresnel calculation)
300 float dist_from_center = std::sqrt(dx * dx + dy * dy);
301 float max_dist = std::sqrt(center_x * center_x + center_y * center_y);
302 float normalized_dist = dist_from_center / max_dist;
303
304 // Approximate incident angle for Fresnel (0 at center, increases toward edges)
305 float cos_theta = std::sqrt(1.0f - normalized_dist * normalized_dist * 0.25f);
306 float fresnel = fresnelReflectance(cos_theta);
307
308 // Render each ghost
309 for (int g = 0; g < num_ghosts; ++g) {
310 // Ghost position is reflected through center with scaling
311 float ghost_x = center_x - ghost_scales_[g] * dx;
312 float ghost_y = center_y - ghost_scales_[g] * dy;
313
314 // Ghost radius based on image diagonal
315 float image_diagonal = std::sqrt(static_cast<float>(resolution.x * resolution.x + resolution.y * resolution.y));
316 float ghost_radius = image_diagonal * ghost_sizes_[g];
317
318 // Ghost base intensity (physical attenuation only)
319 float ghost_base = base_reflectance * fresnel * std::exp(-ghost_scales_[g] * 0.5f);
320
321 // Separate luminance and chrominance (production approach)
322 // Step 1: Find max channel as luminance proxy
323 float luminance = 0.0f;
324 for (const auto &[band_label, val]: source_color) {
325 luminance = std::max(luminance, val);
326 }
327
328 // Step 2: Extract normalized chrominance (color ratios)
329 std::map<std::string, float> chrominance;
330 for (const auto &[band_label, val]: source_color) {
331 chrominance[band_label] = val / std::max(luminance, 1e-6f);
332 }
333
334 // Step 3: Compress luminance only with Reinhard
335 float compressed_luminance = luminance / (1.0f + luminance);
336
337 // Step 4: Apply ghost physics attenuation and visibility scaling
338 // compressed_luminance ≈ 1.0 for bright sources, ghost_base ≈ 5e-5
339 // Reduced to 20% of previous strength for subtler effect
340 float ghost_luminance = compressed_luminance * ghost_base * 40.0f;
341
342 // Apply to each band: chrominance × compressed luminance
343 for (auto &[band_label, band_data]: pixel_data) {
344 // Recombine: color ratio × compressed brightness
345 float band_intensity = chrominance[band_label] * ghost_luminance;
346
347 // Render without chromatic aberration for now (can add back later as optional)
348 renderSoftDisc(band_data, resolution, ghost_x, ghost_y, ghost_radius, band_intensity);
349 }
350 }
351 }
352}
353
354float LensFlare::fresnelReflectance(float cos_theta, float n1, float n2) {
355 // Schlick's approximation for unpolarized light
356 float r0 = ((n1 - n2) / (n1 + n2));
357 r0 = r0 * r0;
358 float one_minus_cos = 1.0f - cos_theta;
359 float one_minus_cos_5 = one_minus_cos * one_minus_cos * one_minus_cos * one_minus_cos * one_minus_cos;
360 return r0 + (1.0f - r0) * one_minus_cos_5;
361}
362
363void LensFlare::renderSoftDisc(std::vector<float> &channel, int2 resolution, float center_x, float center_y, float radius, float intensity) const {
364 if (intensity <= 0.0f || radius <= 0.0f) {
365 return;
366 }
367
368 // Bounding box for the disc
369 int x_min = std::max(0, static_cast<int>(center_x - radius - 1.0f));
370 int x_max = std::min(resolution.x - 1, static_cast<int>(center_x + radius + 1.0f));
371 int y_min = std::max(0, static_cast<int>(center_y - radius - 1.0f));
372 int y_max = std::min(resolution.y - 1, static_cast<int>(center_y + radius + 1.0f));
373
374 float radius_sq = radius * radius;
375
376 // Solid disc with smooth falloff from bright center to transparent edge
377 for (int y = y_min; y <= y_max; ++y) {
378 for (int x = x_min; x <= x_max; ++x) {
379 float dx = static_cast<float>(x) + 0.5f - center_x;
380 float dy = static_cast<float>(y) + 0.5f - center_y;
381 float dist_sq = dx * dx + dy * dy;
382
383 if (dist_sq <= radius_sq) {
384 float dist_ratio = std::sqrt(dist_sq) / radius;
385
386 // Smooth falloff: 1.0 at center, 0.0 at edge
387 // Using 1 - r² for gradual falloff
388 float falloff = 1.0f - dist_ratio * dist_ratio;
389 falloff = std::max(0.0f, falloff);
390
391 int idx = y * resolution.x + x;
392 channel[idx] += intensity * falloff;
393 }
394 }
395 }
396}
397
398void LensFlare::fft2D(const std::vector<float> &input, std::vector<std::complex<float>> &output, int size) {
399 // Initialize output with input values
400 output.resize(size * size);
401 for (int i = 0; i < size * size; ++i) {
402 output[i] = std::complex<float>(input[i], 0.0f);
403 }
404
405 // Apply FFT to each row
406 std::vector<std::complex<float>> row(size);
407 for (int y = 0; y < size; ++y) {
408 for (int x = 0; x < size; ++x) {
409 row[x] = output[y * size + x];
410 }
411 fft1D(row, size);
412 for (int x = 0; x < size; ++x) {
413 output[y * size + x] = row[x];
414 }
415 }
416
417 // Apply FFT to each column
418 std::vector<std::complex<float>> col(size);
419 for (int x = 0; x < size; ++x) {
420 for (int y = 0; y < size; ++y) {
421 col[y] = output[y * size + x];
422 }
423 fft1D(col, size);
424 for (int y = 0; y < size; ++y) {
425 output[y * size + x] = col[y];
426 }
427 }
428
429 // FFT shift to center the zero-frequency component
430 int half = size / 2;
431 for (int y = 0; y < half; ++y) {
432 for (int x = 0; x < half; ++x) {
433 // Swap quadrants
434 std::swap(output[y * size + x], output[(y + half) * size + (x + half)]);
435 std::swap(output[y * size + (x + half)], output[(y + half) * size + x]);
436 }
437 }
438}
439
440void LensFlare::fft1D(std::vector<std::complex<float>> &data, int size, bool inverse) {
441 // Bit-reversal permutation
442 int n = size;
443 int j = 0;
444 for (int i = 0; i < n - 1; ++i) {
445 if (i < j) {
446 std::swap(data[i], data[j]);
447 }
448 int k = n / 2;
449 while (k <= j) {
450 j -= k;
451 k /= 2;
452 }
453 j += k;
454 }
455
456 // Cooley-Tukey FFT
457 for (int len = 2; len <= n; len *= 2) {
458 float angle = 2.0f * static_cast<float>(M_PI) / static_cast<float>(len);
459 if (inverse) {
460 angle = -angle;
461 }
462 std::complex<float> wn(std::cos(angle), std::sin(angle));
463
464 for (int i = 0; i < n; i += len) {
465 std::complex<float> w(1.0f, 0.0f);
466 for (int jj = 0; jj < len / 2; ++jj) {
467 std::complex<float> u = data[i + jj];
468 std::complex<float> t = w * data[i + jj + len / 2];
469 data[i + jj] = u + t;
470 data[i + jj + len / 2] = u - t;
471 w *= wn;
472 }
473 }
474 }
475
476 // Scale for inverse FFT
477 if (inverse) {
478 for (auto &val: data) {
479 val /= static_cast<float>(n);
480 }
481 }
482}