Fix #161678: Cycles: Correct background lighting for mixed volume phases

Mixing multiple volume phase closures can make world background
lighting slightly too bright. The phase-sampled path stores the PDF of
only the selected closure, while the background-sampled path evaluates
the marginal mixture PDF. The resulting MIS weights are inconsistent.

Store the marginal PDF over all phase closures for forward MIS. When
volume path guiding is active, combine it with the guiding PDF so the
stored PDF remains equal to the actual sampling distribution.

Pull Request: https://projects.blender.org/blender/blender/pulls/161690
This commit is contained in:
Cielluloid 2026-08-23 13:58:13 +02:00 • committed by Brecht Van Lommel
parent 94a7fd639e
commit bfa0bfac3e
4 changed files with 60 additions and 50 deletions

View file

@ -2635,6 +2635,7 @@ ccl_device_forceinline bool integrate_volume_phase_scatter(
label = volume_shader_phase_guided_sample(kg,
state,
sd,
phases,
svc,
rand_phase,
&phase_eval,
@ -2653,7 +2654,7 @@ ccl_device_forceinline bool integrate_volume_phase_scatter(
# endif
{
label = volume_shader_phase_sample(
sd, svc, rand_phase, &phase_eval, &phase_wo, &phase_pdf, &sampled_roughness);
sd, phases, svc, rand_phase, &phase_eval, &phase_wo, &phase_pdf, &sampled_roughness);
if (phase_pdf == 0.0f || bsdf_eval_is_zero(&phase_eval)) {
return false;

View file

@ -187,15 +187,22 @@ const ccl_device_inline ccl_private ShaderVolumeClosure *volume_shader_phase_pic
return &phases->closure[sampled];
}
ccl_device_inline float _volume_shader_phase_eval_mis(const ccl_private ShaderData *sd,
const ccl_private ShaderVolumePhases *phases,
const float3 wo,
ccl_private BsdfEval *result_eval,
float sum_pdf,
float sum_sample_weight)
ccl_device_inline float _volume_shader_phase_eval_mis(
const ccl_private ShaderData *sd,
const ccl_private ShaderVolumePhases *phases,
const float3 wo,
const ccl_private ShaderVolumeClosure *skip_svc,
ccl_private BsdfEval *result_eval,
float sum_pdf,
float sum_sample_weight)
{
for (int i = 0; i < phases->num_closure; i++) {
const ccl_private ShaderVolumeClosure *svc = &phases->closure[i];
if (svc == skip_svc) {
continue;
}
float phase_pdf = 0.0f;
const Spectrum eval = volume_phase_eval(sd, svc, wo, &phase_pdf);
@ -211,21 +218,6 @@ ccl_device_inline float _volume_shader_phase_eval_mis(const ccl_private ShaderDa
return (sum_sample_weight > 0.0f) ? sum_pdf / sum_sample_weight : 0.0f;
}
ccl_device float volume_shader_phase_eval(const ccl_private ShaderData *sd,
const ccl_private ShaderVolumeClosure *svc,
const float3 wo,
ccl_private BsdfEval *phase_eval)
{
float phase_pdf = 0.0f;
const Spectrum eval = volume_phase_eval(sd, svc, wo, &phase_pdf);
if (phase_pdf != 0.0f) {
bsdf_eval_accum(phase_eval, eval);
}
return phase_pdf;
}
ccl_device float volume_shader_phase_eval(ccl_attr_maybe_unused KernelGlobals kg,
ccl_attr_maybe_unused IntegratorState state,
const ccl_private ShaderData *sd,
@ -236,7 +228,7 @@ ccl_device float volume_shader_phase_eval(ccl_attr_maybe_unused KernelGlobals kg
{
bsdf_eval_init(phase_eval, zero_spectrum());
float pdf = _volume_shader_phase_eval_mis(sd, phases, wo, phase_eval, 0.0f, 0.0f);
float pdf = _volume_shader_phase_eval_mis(sd, phases, wo, nullptr, phase_eval, 0.0f, 0.0f);
# if defined(__PATH_GUIDING__) && PATH_GUIDING_LEVEL >= 4
if ((kernel_data.kernel_features & KERNEL_FEATURE_PATH_GUIDING)) {
@ -258,10 +250,43 @@ ccl_device float volume_shader_phase_eval(ccl_attr_maybe_unused KernelGlobals kg
return pdf;
}
/* Sample direction for a picked phase function, and return evaluation and PDF for all phase
* functions combined using MIS. */
ccl_device int volume_shader_phase_sample(const ccl_private ShaderData *sd,
const ccl_private ShaderVolumePhases *phases,
const ccl_private ShaderVolumeClosure *svc,
const float2 rand_phase,
ccl_private BsdfEval *phase_eval,
ccl_private float3 *wo,
ccl_private float *pdf,
ccl_private float *sampled_roughness)
{
*sampled_roughness = 1.0f - fabsf(volume_phase_get_g(svc));
Spectrum eval = zero_spectrum();
*pdf = 0.0f;
const int label = volume_phase_sample(sd, svc, rand_phase, &eval, wo, pdf);
if (*pdf != 0.0f) {
if (phases->num_closure > 1) {
const float sample_weight = svc->sample_weight;
bsdf_eval_init(phase_eval, eval * sample_weight);
*pdf = _volume_shader_phase_eval_mis(
sd, phases, *wo, svc, phase_eval, *pdf * sample_weight, sample_weight);
}
else {
bsdf_eval_init(phase_eval, eval);
}
}
return label;
}
# if defined(__PATH_GUIDING__)
ccl_device int volume_shader_phase_guided_sample(KernelGlobals kg,
IntegratorState state,
const ccl_private ShaderData *sd,
const ccl_private ShaderVolumePhases *phases,
const ccl_private ShaderVolumeClosure *svc,
const float2 rand_phase,
ccl_private BsdfEval *phase_eval,
@ -288,21 +313,21 @@ ccl_device int volume_shader_phase_guided_sample(KernelGlobals kg,
/* Initialize to zero. */
int label = LABEL_NONE;
Spectrum eval = zero_spectrum();
*unguided_phase_pdf = 0.0f;
float guide_pdf = 0.0f;
*sampled_roughness = 1.0f - fabsf(volume_phase_get_g(svc));
bsdf_eval_init(phase_eval, zero_spectrum());
if (sample_guiding) {
/* Sample guiding distribution. */
*sampled_roughness = 1.0f - fabsf(volume_phase_get_g(svc));
guide_pdf = guiding_phase_sample(kg, rand_phase, wo);
*phase_pdf = 0.0f;
if (guide_pdf != 0.0f) {
*unguided_phase_pdf = volume_shader_phase_eval(sd, svc, *wo, phase_eval);
*unguided_phase_pdf = _volume_shader_phase_eval_mis(
sd, phases, *wo, nullptr, phase_eval, 0.0f, 0.0f);
*phase_pdf = (guiding_sampling_prob * guide_pdf) +
((1.0f - guiding_sampling_prob) * (*unguided_phase_pdf));
label = LABEL_VOLUME_SCATTER;
@ -311,11 +336,10 @@ ccl_device int volume_shader_phase_guided_sample(KernelGlobals kg,
else {
/* Sample phase. */
*phase_pdf = 0.0f;
label = volume_phase_sample(sd, svc, rand_phase, &eval, wo, unguided_phase_pdf);
label = volume_shader_phase_sample(
sd, phases, svc, rand_phase, phase_eval, wo, unguided_phase_pdf, sampled_roughness);
if (*unguided_phase_pdf != 0.0f) {
bsdf_eval_init(phase_eval, eval);
*phase_pdf = *unguided_phase_pdf;
if (use_volume_guiding) {
guide_pdf = guiding_phase_pdf(kg, *wo);
@ -336,27 +360,6 @@ ccl_device int volume_shader_phase_guided_sample(KernelGlobals kg,
}
# endif
ccl_device int volume_shader_phase_sample(const ccl_private ShaderData *sd,
const ccl_private ShaderVolumeClosure *svc,
const float2 rand_phase,
ccl_private BsdfEval *phase_eval,
ccl_private float3 *wo,
ccl_private float *pdf,
ccl_private float *sampled_roughness)
{
*sampled_roughness = 1.0f - fabsf(volume_phase_get_g(svc));
Spectrum eval = zero_spectrum();
*pdf = 0.0f;
const int label = volume_phase_sample(sd, svc, rand_phase, &eval, wo, pdf);
if (*pdf != 0.0f) {
bsdf_eval_init(phase_eval, eval);
}
return label;
}
/* Motion Blur */
# ifdef __OBJECT_MOTION__

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26ac69260928f622d77908057d1e9ecc8781f8865894c77045e4b48ba009d748
size 1051

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cd823182fc8f6d0a269f0ee72fa321941fdb5f1c8020c9778732d3859b8ba395
size 156813