#include "Lighting/SkyLight.h" #include "Common.h" #include // Constant Sky sky_light_t sky_create_constant_sky(const constant_sky_data_t* data) { sky_light_t light = { .data_size = sizeof(constant_sky_data_t), .evaluate_bsdf_sky = evaluate_bsdf_const_sky, }; light.data = malloc(sizeof(constant_sky_data_t)); if (light.data == NULL) { return light; } memcpy(light.data, data, sizeof(constant_sky_data_t)); return light; } path_output evaluate_bsdf_const_sky(const void* data, const light_shading_context_t* context, vec3s throughput, uint32_t sample_index) { const constant_sky_data_t* sky_data = (const constant_sky_data_t*)data; path_output output = {0.0f}; output.state = PS_TERMINATE; if (sky_data->intensity <= 0.0f) { return output; } vec3s sky_light = glms_vec3_scale(sky_data->color, sky_data->intensity); float pdf = 0.25f * INV_PI; output.pdf = pdf; if (context->bvh_tree == NULL) { output.direct_lighting = glms_vec3_mul(sky_light, throughput); return output; } uint32_t scramble = hash_position(context->position); uint16_t d1 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_U); uint16_t d2 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_V); vec3s wi = random_uniform_cdf_direction(context->normal, sample_index, d1, d2, scramble); ray_t shadow_ray = ray_create(offset_ray_origin(context->position, context->normal, context->wo), wi, 0.0f, 0.0f); hit_result_t shadow_hit = {0}; ray_intersect_bvh_any(&shadow_ray, context->bvh_tree->nodes, context->bvh_tree->primitive_indices, context->bvh_tree->triangles, 0, &shadow_hit); if (shadow_hit.hit) { return output; } float cos_theta = fmaxf(glms_vec3_dot(wi, context->normal), 0.0f); output.direct_lighting = glms_vec3_scale(glms_vec3_mul(sky_light, throughput), cos_theta / pdf); output.wi = wi; output.state = PS_SUCCESS; return output; } static uint32_t lower_bound_float(const float* arr, uint32_t n, float target) { uint32_t low = 0; uint32_t high = n; // Search range [low, high) while (low < high) { uint32_t mid = low + (high - low) / 2; if (arr[mid] < target) { low = mid + 1; } else { high = mid; } } return low; // This is the index where target would be inserted to maintain order } // HDR Sky sky_light_t sky_create_hdr_sky(const texture_collection_t* textures, texture_handle_t hdri_entity, float intensity) { sky_light_t light = { .data_size = sizeof(hdr_sky_data_t), .evaluate_bsdf_sky = evaluate_bsdf_hdr_sky, .free_sky_data = (sky_free_f)hdr_sky_free, }; const texture_t* hdri = get_texture(textures, hdri_entity); if (hdri == NULL) { return light; } hdr_sky_data_t data = { .width = hdri->width, .height = hdri->height, .texture = hdri_entity, .intensity = intensity, .pdf_uv_mass = NULL, .cdf_x = NULL, .cdf_y_transposed = NULL, }; size_t size_hw = (size_t)data.height * data.width; // height * width elements size_t size_w = (size_t)data.width; // width elements size_t size_wh = (size_t)data.width * data.height; // width * height elements (same as hw) // Total size for intermediate buffers: p(x,y)/cond_cdf + p(x) + cdf_x + cond_transposed_cdf size_t intermediate_buffer_size = size_hw + size_w + size_w + size_hw + size_wh; float* intermediate_buffer = (float*)malloc(sizeof(float) * intermediate_buffer_size); if (intermediate_buffer == NULL) { return light; } // These pointers partition the single allocated intermediate_buffer float* p_xy_original_pdf = intermediate_buffer; // size hw (Stores the normalized p(x,y) values) float* p_x_buffer = p_xy_original_pdf + size_hw; // size w (Stores p(x)) float* cdf_x_buffer = p_x_buffer + size_w; // size w (Stores cdf_x_margin) float* cdf_y_conditional = cdf_x_buffer + size_w; // size hw (Stores cdf(y|x)) float* cdf_y_transposed_buffer = cdf_y_conditional + size_hw; // size wh (Stores transposed cdf(y|x)) // --- Calculate Initial Luminance (PDF) --- float lumSum = 0.0f; for (uint32_t i = 0; i < data.height; ++i) { // Calculate the sine of the polar angle theta // Map row i to V [0,1], then to theta [0, PI] float v = (i + 0.5f) / (float)data.height; float theta = v * PI; float sin_theta = sinf(theta); for (uint32_t j = 0; j < data.width; ++j) { vec2s uv = {((float)j + 0.5f) / (float)data.width, ((float)i + 0.5f) / (float)data.height}; vec4s pixel = texture_get_pixel(hdri, uv, 0); float lum = luminance(glms_vec3(pixel)) * sin_theta; p_xy_original_pdf[i * data.width + j] = lum; lumSum += lum; // Sum of luminances (or lum * area_element) } } float inv_lumSum = (lumSum > FLT_EPSILON) ? (1.0f / lumSum) : 0.0f; for (size_t i = 0; i < size_hw; ++i) { p_xy_original_pdf[i] *= inv_lumSum; } // Keep a copy of the normalized discrete probability mass per texel for MIS queries by direction. float* pdf_uv_mass = (float*)malloc(sizeof(float) * size_hw); if (pdf_uv_mass == NULL) { free(intermediate_buffer); return light; } memcpy(pdf_uv_mass, p_xy_original_pdf, sizeof(float) * size_hw); // --- Calculate Marginal PDF p(x) --- for (uint32_t j = 0; j < data.width; ++j) { p_x_buffer[j] = 0.0f; for (uint32_t i = 0; i < data.height; ++i) { p_x_buffer[j] += p_xy_original_pdf[i * data.width + j]; } } // --- Calculate Marginal CDF (prefix sum on marginal PDF) --- memcpy(cdf_x_buffer, p_x_buffer, sizeof(float) * size_w); if (data.width > 0) { for (uint32_t i = 1; i < data.width; ++i) { cdf_x_buffer[i] += cdf_x_buffer[i - 1]; // Prefix sum on cdf_x_buffer } // The last element cdf_x_buffer[width-1] should be close to 1.0 (total probability) } // --- Calculate Conditional PDF and CDF --- // p_xy_buffer currently holds p(x,y) // It will be modified in place to hold cdf(y|x) memcpy(cdf_y_conditional, p_xy_original_pdf, sizeof(float) * size_hw); for (uint32_t j = 0; j < data.width; ++j) { float marginal_x_val = p_x_buffer[j]; // Get p(x=j) from the stored p_x_buffer float inv_marginal_x_val = (marginal_x_val > 1e-6f) ? (1.0f / marginal_x_val) : 0.0f; // Normalize p(x,y) column to get conditional PDF p(y|x) and calculate conditional CDF cdf(y|x) if (data.height > 0) { // First element is p(y=0|x) = p(x,y=0) / p(x), then becomes cdf(y=0|x) cdf_y_conditional[0 * data.width + j] *= inv_marginal_x_val; // Calculate prefix sum (conditional CDF) in place for (uint32_t i = 1; i < data.height; ++i) { // p(y=i|x) = p(x,y=i) / p(x) cdf_y_conditional[i * data.width + j] *= inv_marginal_x_val; // cdf(y=i|x) = cdf(y=i-1|x) + p(y=i|x) cdf_y_conditional[i * data.width + j] += cdf_y_conditional[(i - 1) * data.width + j]; } // The last element of this column (at row height-1) should be close to 1.0 } } // --- Transpose Conditional CDF --- // Transpose cdf_y_conditional (size hw) into cdf_y_transposed_buffer (size wh) for (uint32_t i = 0; i < data.height; ++i) { for (uint32_t j = 0; j < data.width; ++j) { cdf_y_transposed_buffer[j * data.height + i] = cdf_y_conditional[i * data.width + j]; } } // Persist CDF tables for correct runtime sampling (keeps wi/pdf consistent) float* cdf_x_persist = (float*)malloc(sizeof(float) * size_w); float* cdf_y_transposed_persist = (float*)malloc(sizeof(float) * size_wh); if (cdf_x_persist == NULL || cdf_y_transposed_persist == NULL) { free(cdf_x_persist); free(cdf_y_transposed_persist); free(pdf_uv_mass); free(intermediate_buffer); return light; } memcpy(cdf_x_persist, cdf_x_buffer, sizeof(float) * size_w); memcpy(cdf_y_transposed_persist, cdf_y_transposed_buffer, sizeof(float) * size_wh); free(intermediate_buffer); light.data = malloc(sizeof(hdr_sky_data_t)); if (light.data == NULL) { free(pdf_uv_mass); free(cdf_x_persist); free(cdf_y_transposed_persist); return light; } data.total_weight = lumSum; data.pdf_uv_mass = pdf_uv_mass; data.cdf_x = cdf_x_persist; data.cdf_y_transposed = cdf_y_transposed_persist; memcpy(light.data, &data, sizeof(hdr_sky_data_t)); return light; } static inline float hdr_pdf_from_uv_mass(float uv_mass, float v, uint32_t height, uint32_t width) { // v in [0,1] maps to theta in [0, PI] float theta = v * PI; float sin_theta = fmaxf(sinf(theta), 1e-4f); // Δω per texel for equirectangular map: // Δω = (2π/width) * (π/height) * sinθ = (2π^2 sinθ)/(W H) // pdf(ω) = uv_mass / Δω = uv_mass * (W H)/(2π^2 sinθ) float convert = (float)(height * width) / (2.0f * PI_TWO * sin_theta); float pdf = uv_mass * convert; return fminf(pdf, 1e6f); } static inline float hdr_sky_pdf_direction(const hdr_sky_data_t* sky_data, vec3s wi) { vec2s uv = direction_to_equirectangular(wi); uint32_t x, y; uv_to_index(uv, sky_data->width, sky_data->height, &x, &y); float mass = 0.0f; if (sky_data->pdf_uv_mass != NULL) { mass = sky_data->pdf_uv_mass[y * sky_data->width + x]; } return hdr_pdf_from_uv_mass(mass, uv.y, sky_data->height, sky_data->width); } path_output evaluate_bsdf_hdr_sky(const void* data, const light_shading_context_t* context, vec3s throughput, uint32_t sample_index) { const hdr_sky_data_t* sky_data = (const hdr_sky_data_t*)data; path_output output = {0.0f}; output.state = PS_TERMINATE; if (sky_data->intensity <= 0.0f) { return output; } // Last hit was no geometry, directly sample the sky if (context->bvh_tree == NULL) { vec2s uv = direction_to_equirectangular(context->wo); // Calculate LOD based on ray spread angle float lod = 0.0f; if (context->spread_angle > 0.0f) { // Approximate texel footprint: spread_angle * resolution / (2*PI) // This is a rough estimation for equirectangular mapping float texels = context->spread_angle * (float)fmax(sky_data->width, sky_data->height) * INV_PI; lod = log2f(fmaxf(texels, 1.0f)); } vec4s sky_light = texture_sample_lod(get_texture(context->textures, sky_data->texture), uv, lod); output.direct_lighting = glms_vec3_scale(glms_vec3_mul(glms_vec3(sky_light), throughput), sky_data->intensity); // Return the correct environment PDF for MIS when the BSDF-sampled ray escapes to the sky. output.pdf = hdr_sky_pdf_direction(sky_data, context->wo); return output; } uint32_t x_idx, y_idx; uint16_t d1 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_U); uint16_t d2 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_V); float r1 = sobol_sample(sample_index, d1); float r2 = sobol_sample(sample_index, d2); if (sky_data->cdf_x == NULL || sky_data->cdf_y_transposed == NULL || sky_data->pdf_uv_mass == NULL) { return output; } x_idx = lower_bound_float(sky_data->cdf_x, sky_data->width, r1); x_idx = (x_idx == sky_data->width) ? (sky_data->width - 1) : x_idx; y_idx = lower_bound_float(sky_data->cdf_y_transposed + x_idx * sky_data->height, sky_data->height, r2); y_idx = (y_idx == sky_data->height) ? (sky_data->height - 1) : y_idx; float u = ((float)x_idx + 0.5f) / (float)sky_data->width; float v = ((float)y_idx + 0.5f) / (float)sky_data->height; vec3s wi = equirectangular_to_direction(u, v); float n_dot_l = glms_vec3_dot(wi, context->normal); if (n_dot_l <= 0.0f) { return output; } ray_t shadow_ray = ray_create(offset_ray_origin(context->position, context->normal, context->wo), wi, 0.0f, 0.0f); hit_result_t shadow_hit = {0}; ray_intersect_bvh_any(&shadow_ray, context->bvh_tree->nodes, context->bvh_tree->primitive_indices, context->bvh_tree->triangles, 0, &shadow_hit); if (shadow_hit.hit) { return output; } vec2s uv = (vec2s){u, v}; const texture_t* hdri = get_texture(context->textures, sky_data->texture); vec4s pixel = texture_get_pixel(hdri, uv, 0); vec3s sky_light = glms_vec3_scale(glms_vec3(pixel), sky_data->intensity); float mass = sky_data->pdf_uv_mass[y_idx * sky_data->width + x_idx]; float pdf = hdr_pdf_from_uv_mass(mass, v, sky_data->height, sky_data->width); if (pdf < 1e-12f) { return output; } output.direct_lighting = glms_vec3_scale(glms_vec3_mul(sky_light, throughput), n_dot_l / pdf); output.wi = wi; output.pdf = pdf; output.state = PS_SUCCESS; return output; } void hdr_sky_free(hdr_sky_data_t* data) { free(data->pdf_uv_mass); free(data->cdf_x); free(data->cdf_y_transposed); data->pdf_uv_mass = NULL; data->cdf_x = NULL; data->cdf_y_transposed = NULL; }