Cycles: Change sun lamp to have uniform intensity at high angles
This fixes the issue described in https://projects.blender.org/blender/blender/issues/108957. Instead of modeling distant lights like a disk light at infinity, it models them as cones. This way, the radiance is constant across the entire range of directions that it covers. For smaller angles, the difference is very subtle, but for very large angles it becomes obvious (here's the file from #108957, the angle is 179°): | Old | New | | - | - | |  |  | One notable detail is the sampling method: Using `sample_uniform_cone` can increase noise, since the sampling method no longer preserves the stratification of the samples. This is visible in the "light tree multi distant" test scene. Turns out we can do better, and after a bit of testing I found a way to adapt the concentric Shirley mapping to uniform cone sampling. I hope the comment explains the logic behind it reasonably well. Here's the result, note that even the noise distribution is the same when using the new sampling: | Method | Old | New, basic sampling | New, concentric sampling | | - | - |- | - | | Image |  |  |  | | Render time (at higher spp)| 9.03sec | 8.79sec | 8.96sec | I'm not sure if I got the `light->normalized` handling right, since I don't really know what the expectation from Hydra is here. Co-authored-by: Weizhen Huang <weizhen@blender.org> Pull Request: https://projects.blender.org/blender/blender/pulls/108996
This commit is contained in:
committed by
Weizhen Huang
parent
9fe87646d5
commit
213204c229
@@ -14,24 +14,15 @@ ccl_device_inline bool distant_light_sample(const ccl_global KernelLight *klight
|
||||
const float2 rand,
|
||||
ccl_private LightSample *ls)
|
||||
{
|
||||
/* distant light */
|
||||
float3 lightD = klight->co;
|
||||
float3 D = lightD;
|
||||
float radius = klight->distant.radius;
|
||||
float invarea = klight->distant.invarea;
|
||||
float unused;
|
||||
sample_uniform_cone_concentric(
|
||||
klight->co, klight->distant.one_minus_cosangle, rand, &unused, &ls->Ng, &ls->pdf);
|
||||
|
||||
if (radius > 0.0f) {
|
||||
D = normalize(D + disk_light_sample(D, rand) * radius);
|
||||
}
|
||||
|
||||
ls->P = D;
|
||||
ls->Ng = D;
|
||||
ls->D = -D;
|
||||
ls->P = ls->Ng;
|
||||
ls->D = -ls->Ng;
|
||||
ls->t = FLT_MAX;
|
||||
|
||||
float costheta = dot(lightD, D);
|
||||
ls->pdf = invarea / (costheta * costheta * costheta);
|
||||
ls->eval_fac = ls->pdf;
|
||||
ls->eval_fac = klight->distant.eval_fac;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -50,15 +41,11 @@ ccl_device bool distant_light_intersect(const ccl_global KernelLight *klight,
|
||||
{
|
||||
kernel_assert(klight->type == LIGHT_DISTANT);
|
||||
|
||||
if (klight->distant.radius == 0.0f) {
|
||||
if (klight->distant.angle == 0.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float3 lightD = klight->co;
|
||||
const float costheta = dot(-lightD, ray->D);
|
||||
const float cosangle = klight->distant.cosangle;
|
||||
|
||||
if (costheta < cosangle) {
|
||||
if (vector_angle(-klight->co, ray->D) > klight->distant.angle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -76,7 +63,6 @@ ccl_device bool distant_light_sample_from_intersection(KernelGlobals kg,
|
||||
{
|
||||
ccl_global const KernelLight *klight = &kernel_data_fetch(lights, lamp);
|
||||
const int shader = klight->shader_id;
|
||||
const float radius = klight->distant.radius;
|
||||
const LightType type = (LightType)klight->type;
|
||||
|
||||
if (type != LIGHT_DISTANT) {
|
||||
@@ -85,37 +71,19 @@ ccl_device bool distant_light_sample_from_intersection(KernelGlobals kg,
|
||||
if (!(shader & SHADER_USE_MIS)) {
|
||||
return false;
|
||||
}
|
||||
if (radius == 0.0f) {
|
||||
if (klight->distant.angle == 0.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* a distant light is infinitely far away, but equivalent to a disk
|
||||
* shaped light exactly 1 unit away from the current shading point.
|
||||
*
|
||||
* radius t^2/cos(theta)
|
||||
* <----------> t = sqrt(1^2 + tan(theta)^2)
|
||||
* tan(th) area = radius*radius*pi
|
||||
* <----->
|
||||
* \ | (1 + tan(theta)^2)/cos(theta)
|
||||
* \ | (1 + tan(acos(cos(theta)))^2)/cos(theta)
|
||||
* t \th| 1 simplifies to
|
||||
* \-| 1/(cos(theta)^3)
|
||||
* \| magic!
|
||||
* P
|
||||
*/
|
||||
|
||||
float3 lightD = klight->co;
|
||||
float costheta = dot(-lightD, ray_D);
|
||||
float cosangle = klight->distant.cosangle;
|
||||
|
||||
/* Workaround to prevent a hang in the classroom scene with AMD HIP drivers 22.10,
|
||||
* Remove when a compiler fix is available. */
|
||||
#ifdef __HIP__
|
||||
ls->shader = klight->shader_id;
|
||||
#endif
|
||||
|
||||
if (costheta < cosangle)
|
||||
if (vector_angle(-klight->co, ray_D) > klight->distant.angle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ls->type = type;
|
||||
#ifndef __HIP__
|
||||
@@ -133,10 +101,8 @@ ccl_device bool distant_light_sample_from_intersection(KernelGlobals kg,
|
||||
ls->D = ray_D;
|
||||
ls->group = lamp_lightgroup(kg, lamp);
|
||||
|
||||
/* compute pdf */
|
||||
float invarea = klight->distant.invarea;
|
||||
ls->pdf = invarea / (costheta * costheta * costheta);
|
||||
ls->eval_fac = ls->pdf;
|
||||
ls->pdf = klight->distant.pdf;
|
||||
ls->eval_fac = klight->distant.eval_fac;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -133,14 +133,35 @@ ccl_device_inline void sample_uniform_cone_concentric(const float3 N,
|
||||
ccl_private float *pdf)
|
||||
{
|
||||
if (one_minus_cos_angle > 0) {
|
||||
/* Map random number from 2D to 1D. */
|
||||
/* Remap radius to get a uniform distribution w.r.t. solid angle on the cone.
|
||||
* The logic to derive this mapping is as follows:
|
||||
*
|
||||
* Sampling a cone is comparable to sampling the hemisphere, we just restrict theta. Therefore,
|
||||
* the same trick of first sampling the unit disk and the projecting the result up towards the
|
||||
* hemisphere by calculating the appropriate z coordinate still works.
|
||||
*
|
||||
* However, by itself this results in cosine-weighted hemisphere sampling, so we need some kind
|
||||
* of remapping. Cosine-weighted hemisphere and uniform cone sampling have the same conditional
|
||||
* PDF for phi (both are constant), so we only need to think about theta, which corresponds
|
||||
* directly to the radius.
|
||||
*
|
||||
* To find this mapping, we consider the simplest sampling strategies for cosine-weighted
|
||||
* hemispheres and uniform cones. In both, phi is chosen as 2pi * random(). For the former,
|
||||
* r_disk(rand) = sqrt(rand). This is just naive disk sampling, since the projection to the
|
||||
* hemisphere doesn't change the radius. For the latter, r_cone(rand) =
|
||||
* sin_from_cos(mix(cos_angle, 1, rand)).
|
||||
*
|
||||
* So, to remap, we just invert r_disk (-> rand(r_disk) = r_disk^2) and insert it into r_cone:
|
||||
* r_cone(r_disk) = r_cone(rand(r_disk)) = sin_from_cos(mix(cos_angle, 1, r_disk^2)). In
|
||||
* practise, we need to replace `rand` with `1 - rand` to preserve the stratification, but
|
||||
* since it's uniform, that's fine. */
|
||||
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); */
|
||||
/* Remap disk radius to cone radius, equivalent to `xy *= sin_theta / sqrt(r2); */
|
||||
xy *= safe_sqrtf(one_minus_cos_angle * (2.0f - one_minus_cos_angle * r2));
|
||||
|
||||
float3 T, B;
|
||||
|
||||
@@ -1377,10 +1377,10 @@ typedef struct KernelAreaLight {
|
||||
} KernelAreaLight;
|
||||
|
||||
typedef struct KernelDistantLight {
|
||||
float radius;
|
||||
float cosangle;
|
||||
float invarea;
|
||||
float pad;
|
||||
float angle;
|
||||
float one_minus_cosangle;
|
||||
float pdf;
|
||||
float eval_fac;
|
||||
} KernelDistantLight;
|
||||
|
||||
typedef struct KernelLight {
|
||||
|
||||
@@ -1230,22 +1230,23 @@ void LightManager::device_update_lights(Device *device, DeviceScene *dscene, Sce
|
||||
else if (light->light_type == LIGHT_DISTANT) {
|
||||
shader_id &= ~SHADER_AREA_LIGHT;
|
||||
|
||||
float3 dir = safe_normalize(light->dir);
|
||||
float angle = light->angle / 2.0f;
|
||||
float radius = tanf(angle);
|
||||
float cosangle = cosf(angle);
|
||||
float area = M_PI_F * radius * radius;
|
||||
float invarea = (light->normalize && area > 0.0f) ? 1.0f / area : 1.0f;
|
||||
float3 dir = light->dir;
|
||||
|
||||
dir = safe_normalize(dir);
|
||||
|
||||
if (light->use_mis && area > 0.0f)
|
||||
if (light->use_mis && angle > 0.0f) {
|
||||
shader_id |= SHADER_USE_MIS;
|
||||
}
|
||||
|
||||
const float one_minus_cosangle = 2.0f * sqr(sinf(0.5f * angle));
|
||||
const float pdf = (angle > 0.0f) ? (M_1_2PI_F / one_minus_cosangle) : 1.0f;
|
||||
|
||||
klights[light_index].co = dir;
|
||||
klights[light_index].distant.invarea = invarea;
|
||||
klights[light_index].distant.radius = radius;
|
||||
klights[light_index].distant.cosangle = cosangle;
|
||||
klights[light_index].distant.angle = angle;
|
||||
klights[light_index].distant.one_minus_cosangle = one_minus_cosangle;
|
||||
klights[light_index].distant.pdf = pdf;
|
||||
klights[light_index].distant.eval_fac = (light->normalize && angle > 0) ?
|
||||
M_1_PI_F / sqr(sinf(angle)) :
|
||||
1.0f;
|
||||
}
|
||||
else if (light->light_type == LIGHT_BACKGROUND) {
|
||||
uint visibility = scene->background->get_visibility();
|
||||
|
||||
@@ -336,6 +336,12 @@ ccl_device float fast_atan2f(float y, float x)
|
||||
return copysignf(r, y);
|
||||
}
|
||||
|
||||
/* Same as precise_angle, but using fast_atan2f. Still much better that acos(dot(a, b)). */
|
||||
ccl_device_inline float vector_angle(float3 a, float3 b)
|
||||
{
|
||||
return 2.0f * fast_atan2f(len(a - b), len(a + b));
|
||||
}
|
||||
|
||||
/* Based on:
|
||||
*
|
||||
* https://github.com/LiraNuna/glsl-sse2/blob/master/source/vec4.h
|
||||
|
||||
Reference in New Issue
Block a user