diff --git a/intern/cycles/kernel/integrator/mnee.h b/intern/cycles/kernel/integrator/mnee.h index f6c8d20247d..7af104f6096 100644 --- a/intern/cycles/kernel/integrator/mnee.h +++ b/intern/cycles/kernel/integrator/mnee.h @@ -775,7 +775,9 @@ ccl_device_forceinline bool mnee_path_contribution(KernelGlobals kg, surface_shader_bsdf_eval(kg, state, sd, wo, throughput, ls->shader); /* Update light sample with new position / direction and keep pdf in vertex area measure. */ - light_sample_update(kg, ls, vertices[vertex_count - 1].p); + const uint32_t path_flag = INTEGRATOR_STATE(state, path, flag); + light_sample_update( + kg, ls, vertices[vertex_count - 1].p, vertices[vertex_count - 1].n, path_flag); /* Save state path bounce info in case a light path node is used in the refractive interface or * light shader graph. */ diff --git a/intern/cycles/kernel/integrator/shade_dedicated_light.h b/intern/cycles/kernel/integrator/shade_dedicated_light.h index 9c6b2e3045b..3e91d6e2996 100644 --- a/intern/cycles/kernel/integrator/shade_dedicated_light.h +++ b/intern/cycles/kernel/integrator/shade_dedicated_light.h @@ -20,6 +20,8 @@ ccl_device_inline bool shadow_linking_light_sample_from_intersection( KernelGlobals kg, ccl_private const Intersection &ccl_restrict isect, ccl_private const Ray &ccl_restrict ray, + const float3 N, + const uint32_t path_flag, ccl_private LightSample *ccl_restrict ls) { const int lamp = isect.prim; @@ -31,7 +33,7 @@ ccl_device_inline bool shadow_linking_light_sample_from_intersection( return distant_light_sample_from_intersection(kg, ray.D, lamp, ls); } - return light_sample_from_intersection(kg, &isect, ray.P, ray.D, ls); + return light_sample_from_intersection(kg, &isect, ray.P, ray.D, N, path_flag, ls); } ccl_device_inline float shadow_linking_light_sample_mis_weight(KernelGlobals kg, @@ -88,8 +90,11 @@ ccl_device bool shadow_linking_shade_light(KernelGlobals kg, ccl_private float &mis_weight, ccl_private int &ccl_restrict light_group) { + const uint32_t path_flag = INTEGRATOR_STATE(state, path, flag); + const float3 N = INTEGRATOR_STATE(state, path, mis_origin_n); LightSample ls ccl_optional_struct_init; - const bool use_light_sample = shadow_linking_light_sample_from_intersection(kg, isect, ray, &ls); + const bool use_light_sample = shadow_linking_light_sample_from_intersection( + kg, isect, ray, N, path_flag, &ls); if (!use_light_sample) { /* No light to be sampled, so no direct light contribution either. */ return false; @@ -100,7 +105,6 @@ ccl_device bool shadow_linking_shade_light(KernelGlobals kg, return false; } - const uint32_t path_flag = INTEGRATOR_STATE(state, path, flag); if (!is_light_shader_visible_to_path(ls.shader, path_flag)) { return false; } diff --git a/intern/cycles/kernel/integrator/shade_light.h b/intern/cycles/kernel/integrator/shade_light.h index fb9c53ffdd1..8bcf55a96ff 100644 --- a/intern/cycles/kernel/integrator/shade_light.h +++ b/intern/cycles/kernel/integrator/shade_light.h @@ -24,12 +24,15 @@ ccl_device_inline void integrate_light(KernelGlobals kg, float3 ray_P = INTEGRATOR_STATE(state, ray, P); const float3 ray_D = INTEGRATOR_STATE(state, ray, D); const float ray_time = INTEGRATOR_STATE(state, ray, time); + const uint32_t path_flag = INTEGRATOR_STATE(state, path, flag); + const float3 N = INTEGRATOR_STATE(state, path, mis_origin_n); /* Advance ray to new start distance. */ INTEGRATOR_STATE_WRITE(state, ray, tmin) = intersection_t_offset(isect.t); LightSample ls ccl_optional_struct_init; - const bool use_light_sample = light_sample_from_intersection(kg, &isect, ray_P, ray_D, &ls); + const bool use_light_sample = light_sample_from_intersection( + kg, &isect, ray_P, ray_D, N, path_flag, &ls); if (!use_light_sample) { return; @@ -37,7 +40,6 @@ ccl_device_inline void integrate_light(KernelGlobals kg, /* Use visibility flag to skip lights. */ #ifdef __PASSES__ - const uint32_t path_flag = INTEGRATOR_STATE(state, path, flag); if (!is_light_shader_visible_to_path(ls.shader, path_flag)) { return; } diff --git a/intern/cycles/kernel/light/distribution.h b/intern/cycles/kernel/light/distribution.h index 9ca6856d94d..23cdaa6dff5 100644 --- a/intern/cycles/kernel/light/distribution.h +++ b/intern/cycles/kernel/light/distribution.h @@ -46,7 +46,9 @@ ccl_device_noinline bool light_distribution_sample(KernelGlobals kg, const float3 rand, const float time, const float3 P, + const float3 N, const int object_receiver, + const int shader_flags, const int bounce, const uint32_t path_flag, ccl_private LightSample *ls) @@ -56,8 +58,19 @@ ccl_device_noinline bool light_distribution_sample(KernelGlobals kg, const int index = light_distribution_sample(kg, rand.z); const float pdf_selection = kernel_data.integrator.distribution_pdf_lights; const float2 rand_uv = float3_to_float2(rand); - return light_sample( - kg, rand_uv, time, P, object_receiver, bounce, path_flag, index, 0, pdf_selection, ls); + return light_sample(kg, + rand_uv, + time, + P, + N, + object_receiver, + shader_flags, + bounce, + path_flag, + index, + 0, + pdf_selection, + ls); } ccl_device_inline float light_distribution_pdf_lamp(KernelGlobals kg) diff --git a/intern/cycles/kernel/light/light.h b/intern/cycles/kernel/light/light.h index 22dbdb3a318..87596a0abef 100644 --- a/intern/cycles/kernel/light/light.h +++ b/intern/cycles/kernel/light/light.h @@ -96,6 +96,8 @@ ccl_device_inline bool light_sample(KernelGlobals kg, const int lamp, const float2 rand, const float3 P, + const float3 N, + const int shader_flags, const uint32_t path_flag, ccl_private LightSample *ls) { @@ -150,7 +152,7 @@ ccl_device_inline bool light_sample(KernelGlobals kg, } } else if (type == LIGHT_POINT) { - if (!point_light_sample(klight, rand, P, ls)) { + if (!point_light_sample(klight, rand, P, N, shader_flags, ls)) { return false; } } @@ -171,7 +173,9 @@ ccl_device_noinline bool light_sample(KernelGlobals kg, const float2 rand, const float time, const float3 P, + const float3 N, const int object_receiver, + const int shader_flags, const int bounce, const uint32_t path_flag, const int emitter_index, @@ -233,7 +237,7 @@ ccl_device_noinline bool light_sample(KernelGlobals kg, return false; } - if (!light_sample(kg, light, rand, P, path_flag, ls)) { + if (!light_sample(kg, light, rand, P, N, shader_flags, path_flag, ls)) { return false; } } @@ -446,6 +450,8 @@ ccl_device bool light_sample_from_intersection(KernelGlobals kg, ccl_private const Intersection *ccl_restrict isect, const float3 ray_P, const float3 ray_D, + const float3 N, + const uint32_t path_flag, ccl_private LightSample *ccl_restrict ls) { const int lamp = isect->prim; @@ -468,7 +474,7 @@ ccl_device bool light_sample_from_intersection(KernelGlobals kg, } } else if (type == LIGHT_POINT) { - if (!point_light_sample_from_intersection(klight, isect, ray_P, ray_D, ls)) { + if (!point_light_sample_from_intersection(klight, isect, ray_P, ray_D, N, path_flag, ls)) { return false; } } diff --git a/intern/cycles/kernel/light/point.h b/intern/cycles/kernel/light/point.h index bac53ff58cc..a4dbd1850fe 100644 --- a/intern/cycles/kernel/light/point.h +++ b/intern/cycles/kernel/light/point.h @@ -8,68 +8,123 @@ CCL_NAMESPACE_BEGIN -template ccl_device_inline bool point_light_sample(const ccl_global KernelLight *klight, const float2 rand, const float3 P, + const float3 N, + const int shader_flags, ccl_private LightSample *ls) { - float3 center = klight->co; - float radius = klight->spot.radius; - /* disk oriented normal */ - const float3 lightN = normalize(P - center); - ls->P = center; + float3 lightN = P - klight->co; + const float d_sq = len_squared(lightN); + const float d = sqrtf(d_sq); + lightN /= d; - if (radius > 0.0f) { - ls->P += disk_light_sample(lightN, rand) * radius; + const float r_sq = sqr(klight->spot.radius); + + float cos_theta; + if (d_sq > r_sq) { + const float one_minus_cos = sin_sqr_to_one_minus_cos(r_sq / d_sq); + sample_uniform_cone_concentric(-lightN, one_minus_cos, rand, &cos_theta, &ls->D, &ls->pdf); + } + else { + const bool has_transmission = (shader_flags & SD_BSDF_HAS_TRANSMISSION); + if (has_transmission) { + ls->D = sample_uniform_sphere(rand); + ls->pdf = M_1_2PI_F * 0.5f; + } + else { + sample_cos_hemisphere(N, rand, &ls->D, &ls->pdf); + } + cos_theta = -dot(ls->D, lightN); } - ls->pdf = klight->spot.invarea; - ls->D = normalize_len(ls->P - P, &ls->t); - /* we set the light normal to the outgoing direction to support texturing */ - ls->Ng = -ls->D; + /* Law of cosines. */ + ls->t = d * cos_theta - copysignf(safe_sqrtf(r_sq - d_sq + d_sq * sqr(cos_theta)), d_sq - r_sq); + + ls->P = P + ls->D * ls->t; ls->eval_fac = M_1_PI_F * 0.25f * klight->spot.invarea; + if (r_sq == 0) { + /* Use intensity instead of radiance for point light. */ + ls->eval_fac /= sqr(ls->t); + /* `ls->Ng` is not well-defined for point light, so use the incoming direction instead. */ + ls->Ng = -ls->D; + } + else { + ls->Ng = normalize(ls->P - klight->co); + /* Remap sampled point onto the sphere to prevent precision issues with small radius. */ + ls->P = ls->Ng * klight->spot.radius + klight->co; + } + + const Transform itfm = klight->itfm; + const float2 uv = map_to_sphere(transform_direction(&itfm, ls->Ng)); + /* NOTE: Return barycentric coordinates in the same notation as Embree and OptiX. */ + ls->u = uv.y; + ls->v = 1.0f - uv.x - uv.y; - float2 uv = map_to_sphere(ls->Ng); - ls->u = uv.x; - ls->v = uv.y; - ls->pdf *= lamp_light_pdf(lightN, -ls->D, ls->t); return true; } +ccl_device_forceinline float point_light_pdf( + const float d_sq, const float r_sq, const float3 N, const float3 D, const uint32_t path_flag) +{ + if (d_sq > r_sq) { + return M_1_2PI_F / sin_sqr_to_one_minus_cos(r_sq / d_sq); + } + + const bool has_transmission = (path_flag & PATH_RAY_MIS_HAD_TRANSMISSION); + return has_transmission ? M_1_2PI_F * 0.5f : pdf_cos_hemisphere(N, D); +} + ccl_device_forceinline void point_light_mnee_sample_update(const ccl_global KernelLight *klight, ccl_private LightSample *ls, - const float3 P) + const float3 P, + const float3 N, + const uint32_t path_flag) { ls->D = normalize_len(ls->P - P, &ls->t); - ls->Ng = -ls->D; - float2 uv = map_to_sphere(ls->Ng); - ls->u = uv.x; - ls->v = uv.y; + const float radius = klight->spot.radius; - float invarea = klight->spot.invarea; - ls->eval_fac = (0.25f * M_1_PI_F) * invarea; - /* NOTE : preserve pdf in area measure. */ - ls->pdf = invarea; + if (radius > 0) { + const float d_sq = len_squared(P - klight->co); + const float r_sq = sqr(radius); + const float t_sq = sqr(ls->t); + + ls->pdf = point_light_pdf(d_sq, r_sq, N, ls->D, path_flag); + + /* NOTE : preserve pdf in area measure. */ + ls->pdf *= 0.5f * fabsf(d_sq - r_sq - t_sq) / (radius * ls->t * t_sq); + + ls->Ng = normalize(ls->P - klight->co); + } + else { + ls->eval_fac = M_1_PI_F * 0.25f * klight->spot.invarea; + + ls->Ng = -ls->D; + + /* PDF does not change. */ + } + + const Transform itfm = klight->itfm; + const float2 uv = map_to_sphere(transform_direction(&itfm, ls->Ng)); + /* NOTE: Return barycentric coordinates in the same notation as Embree and OptiX. */ + ls->u = uv.y; + ls->v = 1.0f - uv.x - uv.y; } ccl_device_inline bool point_light_intersect(const ccl_global KernelLight *klight, const ccl_private Ray *ccl_restrict ray, ccl_private float *t) { - /* Sphere light (aka, aligned disk light). */ - const float3 lightP = klight->co; const float radius = klight->spot.radius; if (radius == 0.0f) { return false; } - /* disk oriented normal */ - const float3 lightN = normalize(ray->P - lightP); float3 P; - return ray_disk_intersect(ray->P, ray->D, ray->tmin, ray->tmax, lightP, lightN, radius, &P, t); + return ray_sphere_intersect(ray->P, ray->D, ray->tmin, ray->tmax, klight->co, radius, &P, t); } ccl_device_inline bool point_light_sample_from_intersection( @@ -77,27 +132,27 @@ ccl_device_inline bool point_light_sample_from_intersection( ccl_private const Intersection *ccl_restrict isect, const float3 ray_P, const float3 ray_D, + const float3 N, + const uint32_t path_flag, ccl_private LightSample *ccl_restrict ls) { - const float3 lighN = normalize(ray_P - klight->co); + ls->eval_fac = M_1_PI_F * 0.25f * klight->spot.invarea; - /* We set the light normal to the outgoing direction to support texturing. */ - ls->Ng = -ls->D; + const float radius = klight->spot.radius; - float invarea = klight->spot.invarea; - ls->eval_fac = (0.25f * M_1_PI_F) * invarea; - ls->pdf = invarea; + ls->Ng = radius > 0 ? normalize(ls->P - klight->co) : -ray_D; - float2 uv = map_to_sphere(ls->Ng); - ls->u = uv.x; - ls->v = uv.y; + const Transform itfm = klight->itfm; + const float2 uv = map_to_sphere(transform_direction(&itfm, ls->Ng)); + /* NOTE: Return barycentric coordinates in the same notation as Embree and OptiX. */ + ls->u = uv.y; + ls->v = 1.0f - uv.x - uv.y; - /* compute pdf */ - if (ls->t != FLT_MAX) { - ls->pdf *= lamp_light_pdf(lighN, -ls->D, ls->t); + if (ls->t == FLT_MAX) { + ls->pdf = 0.0f; } else { - ls->pdf = 0.f; + ls->pdf = point_light_pdf(len_squared(ray_P - klight->co), sqr(radius), N, ray_D, path_flag); } return true; @@ -115,14 +170,22 @@ ccl_device_forceinline bool point_light_tree_parameters(const ccl_global KernelL cos_theta_u = 1.0f; /* Any value in [-1, 1], irrelevant since theta = 0 */ return true; } - float min_distance; - point_to_centroid = safe_normalize_len(centroid - P, &min_distance); + + float dist_point_to_centroid; + point_to_centroid = safe_normalize_len(centroid - P, &dist_point_to_centroid); const float radius = klight->spot.radius; - const float hypotenus = sqrtf(sqr(radius) + sqr(min_distance)); - cos_theta_u = min_distance / hypotenus; - - distance = make_float2(hypotenus, min_distance); + if (dist_point_to_centroid > radius) { + /* Equivalent to a disk light with the same angular span. */ + cos_theta_u = cos_from_sin(radius / dist_point_to_centroid); + distance = dist_point_to_centroid * make_float2(1.0f / cos_theta_u, 1.0f); + } + else { + /* Similar to background light. */ + cos_theta_u = -1.0f; + /* HACK: pack radiance scaling in the distance. */ + distance = one_float2() * radius / M_SQRT2_F; + } return true; } diff --git a/intern/cycles/kernel/light/sample.h b/intern/cycles/kernel/light/sample.h index 2a2fce85034..96d6474e7af 100644 --- a/intern/cycles/kernel/light/sample.h +++ b/intern/cycles/kernel/light/sample.h @@ -338,7 +338,7 @@ ccl_device_inline bool light_sample_from_volume_segment(KernelGlobals kg, #endif { return light_distribution_sample( - kg, rand, time, P, object_receiver, bounce, path_flag, ls); + kg, rand, time, P, D, object_receiver, SD_BSDF_HAS_TRANSMISSION, bounce, path_flag, ls); } } @@ -363,7 +363,7 @@ ccl_device bool light_sample_from_position(KernelGlobals kg, #endif { return light_distribution_sample( - kg, rand, time, P, object_receiver, bounce, path_flag, ls); + kg, rand, time, P, N, object_receiver, shader_flags, bounce, path_flag, ls); } } @@ -371,12 +371,14 @@ ccl_device bool light_sample_from_position(KernelGlobals kg, * except for directional light. */ ccl_device_forceinline void light_sample_update(KernelGlobals kg, ccl_private LightSample *ls, - const float3 P) + const float3 P, + const float3 N, + const uint32_t path_flag) { const ccl_global KernelLight *klight = &kernel_data_fetch(lights, ls->lamp); if (ls->type == LIGHT_POINT) { - point_light_mnee_sample_update(klight, ls, P); + point_light_mnee_sample_update(klight, ls, P, N, path_flag); } else if (ls->type == LIGHT_SPOT) { spot_light_mnee_sample_update(klight, ls, P); diff --git a/intern/cycles/kernel/light/tree.h b/intern/cycles/kernel/light/tree.h index 52ff88a3521..55788f064cd 100644 --- a/intern/cycles/kernel/light/tree.h +++ b/intern/cycles/kernel/light/tree.h @@ -761,7 +761,9 @@ ccl_device_noinline bool light_tree_sample(KernelGlobals kg, float3_to_float2(rand), time, P, + N_or_D, object_receiver, + shader_flags, bounce, path_flag, selected_emitter, diff --git a/intern/cycles/kernel/sample/mapping.h b/intern/cycles/kernel/sample/mapping.h index 43c84a40945..6ae8fb9f95a 100644 --- a/intern/cycles/kernel/sample/mapping.h +++ b/intern/cycles/kernel/sample/mapping.h @@ -19,6 +19,29 @@ ccl_device void to_unit_disk(ccl_private float2 *rand) rand->y = r * sinf(phi); } +/* Distribute 2D uniform random samples on [0, 1] over unit disk [-1, 1], with concentric mapping + * to better preserve stratification for some RNG sequences. */ +ccl_device float2 concentric_sample_disk(const float2 rand) +{ + float phi, r; + float a = 2.0f * rand.x - 1.0f; + float b = 2.0f * rand.y - 1.0f; + + if (a == 0.0f && b == 0.0f) { + return zero_float2(); + } + else if (a * a > b * b) { + r = a; + phi = M_PI_4_F * (b / a); + } + else { + r = b; + phi = M_PI_2_F - M_PI_4_F * (a / b); + } + + return make_float2(r * cosf(phi), r * sinf(phi)); +} + /* return an orthogonal tangent and bitangent given a normal and tangent that * may not be exactly orthogonal */ ccl_device void make_orthonormals_tangent(const float3 N, @@ -45,6 +68,12 @@ ccl_device_inline void sample_cos_hemisphere(const float3 N, *pdf = costheta * M_1_PI_F; } +ccl_device_inline float pdf_cos_hemisphere(const float3 N, const float3 D) +{ + const float cos_theta = dot(N, D); + return cos_theta > 0 ? cos_theta * M_1_PI_F : 0.0f; +} + /* sample direction uniformly distributed in hemisphere */ ccl_device_inline void sample_uniform_hemisphere(const float3 N, const float2 rand, @@ -91,6 +120,42 @@ ccl_device_inline float pdf_uniform_cone(const float3 N, float3 D, float angle) return 0.0f; } +/* Uniformly sample a direction in a cone of given angle around `N`. Use concentric mapping to + * better preserve stratification. Return the angle between `N` and the sampled direction as + * `cos_theta`. + * Pass `1 - cos(angle)` as argument instead of `angle` to alleviate precision issues at small + * angles (see sphere light for reference). */ +ccl_device_inline void sample_uniform_cone_concentric(const float3 N, + const float one_minus_cos_angle, + const float2 rand, + ccl_private float *cos_theta, + ccl_private float3 *wo, + ccl_private float *pdf) +{ + if (one_minus_cos_angle > 0) { + /* Map random number from 2D to 1D. */ + float2 xy = concentric_sample_disk(rand); + const float r2 = len_squared(xy); + + /* Equivalent to `mix(cos_angle, 1.0f, 1.0f - r2)` */ + *cos_theta = 1.0f - r2 * one_minus_cos_angle; + + /* Equivalent to `xy *= sin_theta / sqrt(r2); */ + xy *= safe_sqrtf(one_minus_cos_angle * (2.0f - one_minus_cos_angle * r2)); + + float3 T, B; + make_orthonormals(N, &T, &B); + + *wo = xy.x * T + xy.y * B + *cos_theta * N; + *pdf = M_1_2PI_F / one_minus_cos_angle; + } + else { + *cos_theta = 1.0f; + *wo = N; + *pdf = 1.0f; + } +} + /* sample uniform point on the surface of a sphere */ ccl_device float3 sample_uniform_sphere(const float2 rand) { @@ -103,29 +168,6 @@ ccl_device float3 sample_uniform_sphere(const float2 rand) return make_float3(x, y, z); } -/* distribute uniform xy on [0,1] over unit disk [-1,1], with concentric mapping - * to better preserve stratification for some RNG sequences */ -ccl_device float2 concentric_sample_disk(const float2 rand) -{ - float phi, r; - float a = 2.0f * rand.x - 1.0f; - float b = 2.0f * rand.y - 1.0f; - - if (a == 0.0f && b == 0.0f) { - return zero_float2(); - } - else if (a * a > b * b) { - r = a; - phi = M_PI_4_F * (b / a); - } - else { - r = b; - phi = M_PI_2_F - M_PI_4_F * (a / b); - } - - return make_float2(r * cosf(phi), r * sinf(phi)); -} - /* sample point in unit polygon with given number of corners and rotation */ ccl_device float2 regular_polygon_sample(float corners, float rotation, const float2 rand) { diff --git a/intern/cycles/scene/light.cpp b/intern/cycles/scene/light.cpp index bc7a41a5824..f6728f02366 100644 --- a/intern/cycles/scene/light.cpp +++ b/intern/cycles/scene/light.cpp @@ -1214,6 +1214,9 @@ void LightManager::device_update_lights(Device *device, DeviceScene *dscene, Sce shader_id &= ~SHADER_AREA_LIGHT; float radius = light->size; + /* TODO: `invarea` was used for disk sampling, with the current solid angle sampling this + * becomes unnecessary. We could store `eval_fac` instead, but currently it shares the same + * #KernelSpotLight type with #LIGHT_SPOT, so keep it know until refactor for spot light. */ float invarea = (light->normalize && radius > 0.0f) ? 1.0f / (M_PI_F * radius * radius) : 1.0f; diff --git a/intern/cycles/util/math.h b/intern/cycles/util/math.h index a29abe51e7a..394e56ea926 100644 --- a/intern/cycles/util/math.h +++ b/intern/cycles/util/math.h @@ -748,6 +748,12 @@ ccl_device_inline float cos_from_sin(const float s) return safe_sqrtf(1.0f - sqr(s)); } +ccl_device_inline float sin_sqr_to_one_minus_cos(const float s_sq) +{ + /* Using second-order Taylor expansion at small angles for better accuracy. */ + return s_sq > 0.0004f ? 1.0f - safe_sqrtf(1.0f - s_sq) : 0.5f * s_sq; +} + ccl_device_inline float pow20(float a) { return sqr(sqr(sqr(sqr(a)) * a)); diff --git a/intern/cycles/util/math_intersect.h b/intern/cycles/util/math_intersect.h index 0e1ed3de608..c7277f38f25 100644 --- a/intern/cycles/util/math_intersect.h +++ b/intern/cycles/util/math_intersect.h @@ -18,29 +18,32 @@ ccl_device bool ray_sphere_intersect(float3 ray_P, ccl_private float3 *isect_P, ccl_private float *isect_t) { - const float3 d = sphere_P - ray_P; - const float radiussq = sphere_radius * sphere_radius; - const float tsq = dot(d, d); + const float3 d_vec = sphere_P - ray_P; + const float r_sq = sphere_radius * sphere_radius; + const float d_sq = dot(d_vec, d_vec); + const float d_cos_theta = dot(d_vec, ray_D); - if (tsq > radiussq) { - /* Ray origin outside sphere. */ - const float tp = dot(d, ray_D); - if (tp < 0.0f) { - /* Ray points away from sphere. */ - return false; - } - const float dsq = tsq - tp * tp; /* Pythagoras. */ - if (dsq > radiussq) { - /* Closest point on ray outside sphere. */ - return false; - } - const float t = tp - sqrtf(radiussq - dsq); /* pythagoras */ - if (t > ray_tmin && t < ray_tmax) { - *isect_t = t; - *isect_P = ray_P + ray_D * t; - return true; - } + if (d_sq > r_sq && d_cos_theta < 0.0f) { + /* Ray origin outside sphere and points away from sphere. */ + return false; } + + const float d_sin_theta_sq = d_sq - d_cos_theta * d_cos_theta; + + if (d_sin_theta_sq > r_sq) { + /* Closest point on ray outside sphere. */ + return false; + } + + /* Law of cosines. */ + const float t = d_cos_theta - copysignf(sqrtf(r_sq - d_sin_theta_sq), d_sq - r_sq); + + if (t > ray_tmin && t < ray_tmax) { + *isect_t = t; + *isect_P = ray_P + ray_D * t; + return true; + } + return false; } diff --git a/source/blender/draw/engines/eevee/eevee_lights.c b/source/blender/draw/engines/eevee/eevee_lights.c index d05bb85f627..15f194927a5 100644 --- a/source/blender/draw/engines/eevee/eevee_lights.c +++ b/source/blender/draw/engines/eevee/eevee_lights.c @@ -86,8 +86,7 @@ static float light_shape_radiance_get(const Light *la, const EEVEE_Light *evli) case LA_LOCAL: { /* Sphere area. */ float area = 4.0f * (float)M_PI * square_f(evli->radius); - /* NOTE: Presence of a factor of PI here to match Cycles. But it should be missing to be - * consistent with the other cases. */ + /* Convert radiant flux to radiance. */ return 1.0f / (area * (float)M_PI); } default: @@ -128,10 +127,9 @@ static float light_volume_radiance_factor_get(const Light *la, } case LA_SPOT: case LA_LOCAL: { - /* Sphere solid angle. */ - float area = 4.0f * (float)M_PI; - /* NOTE: Missing a factor of PI here to match Cycles. */ - power *= 1.0f / area; + /* Convert radiant flux to intensity. */ + /* Inverse of sphere solid angle. */ + power *= 0.25f * (float)M_1_PI; break; } default: diff --git a/source/blender/draw/engines/eevee/shaders/lights_lib.glsl b/source/blender/draw/engines/eevee/shaders/lights_lib.glsl index d6b4d9b0246..174aceae7e6 100644 --- a/source/blender/draw/engines/eevee/shaders/lights_lib.glsl +++ b/source/blender/draw/engines/eevee/shaders/lights_lib.glsl @@ -325,6 +325,20 @@ float light_diffuse(LightData ld, vec3 N, vec3 V, vec4 l_vector) return ltc_evaluate_disk(N, V, mat3(1.0), points); } + else if (ld.l_type == POINT) { + if (l_vector.w < ld.l_radius) { + /* Inside, treat as hemispherical light. */ + return 1.0; + } + else { + /* Outside, treat as disk light spanning the same solid angle. */ + /* The result is the same as passing the scaled radius to #ltc_evaluate_disk_simple (see + * #light_specular), using simplified math here. */ + float r_sq = sqr(ld.l_radius / l_vector.w); + vec3 L = l_vector.xyz / l_vector.w; + return r_sq * diffuse_sphere_integral(dot(N, L), r_sq); + } + } else { float radius = ld.l_radius; radius /= (ld.l_type == SUN) ? 1.0 : l_vector.w; @@ -347,11 +361,22 @@ float light_specular(LightData ld, vec4 ltc_mat, vec3 N, vec3 V, vec4 l_vector) return ltc_evaluate_quad(corners, vec3(0.0, 0.0, 1.0)); } + else if (ld.l_type == POINT && l_vector.w < ld.l_radius) { + /* Inside the sphere light, integrate over the hemisphere. */ + return 1.0; + } else { bool is_ellipse = (ld.l_type == AREA_ELLIPSE); float radius_x = is_ellipse ? ld.l_sizex : ld.l_radius; float radius_y = is_ellipse ? ld.l_sizey : ld.l_radius; + if (ld.l_type == POINT) { + /* The sine of the half-angle spanned by a sphere light is equal to the tangent of the + * half-angle spanned by a disk light with the same radius. */ + radius_x *= inversesqrt(1.0 - sqr(radius_x / l_vector.w)); + radius_y *= inversesqrt(1.0 - sqr(radius_y / l_vector.w)); + } + vec3 L = (ld.l_type == SUN) ? -ld.l_forward : l_vector.xyz; vec3 Px = ld.l_right; vec3 Py = ld.l_up; diff --git a/source/blender/draw/engines/eevee_next/eevee_light.cc b/source/blender/draw/engines/eevee_next/eevee_light.cc index 239e0e0d387..b85e35a2171 100644 --- a/source/blender/draw/engines/eevee_next/eevee_light.cc +++ b/source/blender/draw/engines/eevee_next/eevee_light.cc @@ -186,8 +186,7 @@ float Light::shape_radiance_get(const ::Light *la) case LA_LOCAL: { /* Sphere area. */ float area = 4.0f * float(M_PI) * square_f(_radius); - /* NOTE: Presence of a factor of PI here to match Cycles. But it should be missing to be - * consistent with the other cases. */ + /* Convert radiant flux to radiance. */ return 1.0f / (area * float(M_PI)); } default: @@ -221,10 +220,9 @@ float Light::point_radiance_get(const ::Light *la) } case LA_SPOT: case LA_LOCAL: { - /* Sphere solid angle. */ - float area = 4.0f * float(M_PI); - /* NOTE: Missing a factor of PI here to match Cycles. */ - return 1.0f / area; + /* Convert radiant flux to intensity. */ + /* Inverse of sphere solid angle. */ + return 0.25f * float(M_1_PI); } default: case LA_SUN: { diff --git a/source/blender/draw/engines/eevee_next/shaders/eevee_light_lib.glsl b/source/blender/draw/engines/eevee_next/shaders/eevee_light_lib.glsl index 761e403b138..8894f632751 100644 --- a/source/blender/draw/engines/eevee_next/shaders/eevee_light_lib.glsl +++ b/source/blender/draw/engines/eevee_next/shaders/eevee_light_lib.glsl @@ -106,7 +106,20 @@ float light_diffuse(sampler2DArray utility_tx, vec3 L, float dist) { - if (is_directional || !is_area_light(ld.type)) { + if (ld.type == LIGHT_POINT) { + if (dist < ld._radius) { + /* Inside, treat as hemispherical light. */ + return 1.0; + } + else { + /* Outside, treat as disk light spanning the same solid angle. */ + /* The result is the same as passing the scaled radius to #ltc_evaluate_disk_simple (see + * #light_ltc), using simplified math here. */ + float r_sq = sqr(ld._radius / dist); + return r_sq * ltc_diffuse_sphere_integral(utility_tx, dot(N, L), r_sq); + } + } + else if (is_directional || ld.type == LIGHT_SPOT) { float radius = ld._radius / dist; return ltc_evaluate_disk_simple(utility_tx, radius, dot(N, L)); } @@ -147,7 +160,11 @@ float light_ltc(sampler2DArray utility_tx, float dist, vec4 ltc_mat) { - if (is_directional || ld.type != LIGHT_RECT) { + if (ld.type == LIGHT_POINT && dist < ld._radius) { + /* Inside the sphere light, integrate over the hemisphere. */ + return 1.0; + } + else if (is_directional || ld.type != LIGHT_RECT) { vec3 Px = ld._right; vec3 Py = ld._up; @@ -156,8 +173,18 @@ float light_ltc(sampler2DArray utility_tx, } vec3 points[3]; - points[0] = Px * -ld._area_size_x + Py * -ld._area_size_y; - points[1] = Px * ld._area_size_x + Py * -ld._area_size_y; + if (ld.type == LIGHT_POINT) { + /* The sine of the half-angle spanned by a sphere light is equal to the tangent of the + * half-angle spanned by a disk light with the same radius. */ + float radius = ld._radius * inversesqrt(1.0 - sqr(ld._radius / dist)); + + points[0] = Px * -radius + Py * -radius; + points[1] = Px * radius + Py * -radius; + } + else { + points[0] = Px * -ld._area_size_x + Py * -ld._area_size_y; + points[1] = Px * ld._area_size_x + Py * -ld._area_size_y; + } points[2] = -points[0]; points[0] += L * dist;