Cycles: Add more scattering phase functions

Previously, Cycles only supported the Henyey-Greenstein phase function for volume scattering.
While HG is flexible and works for a wide range of effects, sometimes a more physically accurate
phase function may be needed for realism.

Therefore, this adds three new phase functions to the code:
Rayleigh: For particles with a size below the wavelength of light, mostly athmospheric scattering.
Fournier-Forand: For realistic underwater scattering.
Draine: Fairly specific on its own (mostly for interstellar dust), but useful for the next entry.
Mie: Approximates Mie scattering in water droplets using a mix of Draine and HG phase functions.

These phase functions can be combined using Mix nodes as usual.

Co-authored-by: Lukas Stockner <lukas@lukasstockner.de>
Pull Request: https://projects.blender.org/blender/blender/pulls/123532
This commit is contained in:
Alexandre Cardaillac
2024-10-02 11:05:28 +02:00
committed by Lukas Stockner
parent db490e90fe
commit 0315eae536
27 changed files with 1027 additions and 172 deletions

View File

@@ -721,7 +721,26 @@ static ShaderNode *add_node(Scene *scene,
node = ao;
}
else if (b_node.is_a(&RNA_ShaderNodeVolumeScatter)) {
node = graph->create_node<ScatterVolumeNode>();
BL::ShaderNodeVolumeScatter b_scatter_node(b_node);
ScatterVolumeNode *scatter = graph->create_node<ScatterVolumeNode>();
switch (b_scatter_node.phase()) {
case BL::ShaderNodeVolumeScatter::phase_HENYEY_GREENSTEIN:
scatter->set_phase(CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID);
break;
case BL::ShaderNodeVolumeScatter::phase_FOURNIER_FORAND:
scatter->set_phase(CLOSURE_VOLUME_FOURNIER_FORAND_ID);
break;
case BL::ShaderNodeVolumeScatter::phase_DRAINE:
scatter->set_phase(CLOSURE_VOLUME_DRAINE_ID);
break;
case BL::ShaderNodeVolumeScatter::phase_RAYLEIGH:
scatter->set_phase(CLOSURE_VOLUME_RAYLEIGH_ID);
break;
case BL::ShaderNodeVolumeScatter::phase_MIE:
scatter->set_phase(CLOSURE_VOLUME_MIE_ID);
break;
}
node = scatter;
}
else if (b_node.is_a(&RNA_ShaderNodeVolumeAbsorption)) {
node = graph->create_node<AbsorptionVolumeNode>();

View File

@@ -161,6 +161,11 @@ set(SRC_KERNEL_CLOSURE_HEADERS
closure/bssrdf.h
closure/emissive.h
closure/volume.h
closure/volume_util.h
closure/volume_henyey_greenstein.h
closure/volume_rayleigh.h
closure/volume_fournier_forand.h
closure/volume_draine.h
closure/bsdf_principled_hair_chiang.h
closure/bsdf_principled_hair_huang.h
)

View File

@@ -4,6 +4,13 @@
#pragma once
#include "kernel/closure/volume_util.h"
#include "kernel/closure/volume_draine.h"
#include "kernel/closure/volume_fournier_forand.h"
#include "kernel/closure/volume_henyey_greenstein.h"
#include "kernel/closure/volume_rayleigh.h"
CCL_NAMESPACE_BEGIN
/* VOLUME EXTINCTION */
@@ -19,110 +26,27 @@ ccl_device void volume_extinction_setup(ccl_private ShaderData *sd, Spectrum wei
}
}
/* HENYEY-GREENSTEIN CLOSURE */
typedef struct HenyeyGreensteinVolume {
SHADER_CLOSURE_BASE;
float g;
} HenyeyGreensteinVolume;
static_assert(sizeof(ShaderClosure) >= sizeof(HenyeyGreensteinVolume),
"HenyeyGreensteinVolume is too large!");
/* Given cosine between rays, return probability density that a photon bounces
* to that direction. The g parameter controls how different it is from the
* uniform sphere. g=0 uniform diffuse-like, g=1 close to sharp single ray. */
ccl_device float single_peaked_henyey_greenstein(float cos_theta, float g)
{
return ((1.0f - g * g) / safe_powf(1.0f + g * g - 2.0f * g * cos_theta, 1.5f)) *
(M_1_PI_F * 0.25f);
};
ccl_device int volume_henyey_greenstein_setup(ccl_private HenyeyGreensteinVolume *volume)
{
volume->type = CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID;
/* clamp anisotropy to avoid delta function */
volume->g = signf(volume->g) * min(fabsf(volume->g), 1.0f - 1e-3f);
return SD_SCATTER;
}
ccl_device Spectrum volume_henyey_greenstein_eval_phase(ccl_private const ShaderVolumeClosure *svc,
const float3 wi,
float3 wo,
ccl_private float *pdf)
{
float g = svc->g;
/* note that wi points towards the viewer */
if (fabsf(g) < 1e-3f) {
*pdf = M_1_PI_F * 0.25f;
}
else {
float cos_theta = dot(-wi, wo);
*pdf = single_peaked_henyey_greenstein(cos_theta, g);
}
return make_spectrum(*pdf);
}
ccl_device float3 henyey_greenstrein_sample(float3 D, float g, float2 rand, ccl_private float *pdf)
{
/* match pdf for small g */
float cos_theta;
bool isotropic = fabsf(g) < 1e-3f;
if (isotropic) {
cos_theta = (1.0f - 2.0f * rand.x);
if (pdf) {
*pdf = M_1_PI_F * 0.25f;
}
}
else {
float k = (1.0f - g * g) / (1.0f - g + 2.0f * g * rand.x);
cos_theta = (1.0f + g * g - k * k) / (2.0f * g);
if (pdf) {
*pdf = single_peaked_henyey_greenstein(cos_theta, g);
}
}
float sin_theta = sin_from_cos(cos_theta);
float phi = M_2PI_F * rand.y;
float3 dir = make_float3(sin_theta * cosf(phi), sin_theta * sinf(phi), cos_theta);
float3 T, B;
make_orthonormals(D, &T, &B);
dir = dir.x * T + dir.y * B + dir.z * D;
return dir;
}
ccl_device int volume_henyey_greenstein_sample(ccl_private const ShaderVolumeClosure *svc,
float3 wi,
float2 rand,
ccl_private Spectrum *eval,
ccl_private float3 *wo,
ccl_private float *pdf)
{
float g = svc->g;
/* note that wi points towards the viewer and so is used negated */
*wo = henyey_greenstrein_sample(-wi, g, rand, pdf);
*eval = make_spectrum(*pdf); /* perfect importance sampling */
return LABEL_VOLUME_SCATTER;
}
/* VOLUME CLOSURE */
/* VOLUME SCATTERING */
ccl_device Spectrum volume_phase_eval(ccl_private const ShaderData *sd,
ccl_private const ShaderVolumeClosure *svc,
float3 wo,
ccl_private float *pdf)
{
return volume_henyey_greenstein_eval_phase(svc, sd->wi, wo, pdf);
switch (svc->type) {
case CLOSURE_VOLUME_FOURNIER_FORAND_ID:
return volume_fournier_forand_eval(sd, svc, wo, pdf);
case CLOSURE_VOLUME_RAYLEIGH_ID:
return volume_rayleigh_eval(sd, wo, pdf);
case CLOSURE_VOLUME_DRAINE_ID:
return volume_draine_eval(sd, svc, wo, pdf);
case CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID:
return volume_henyey_greenstein_eval(sd, svc, wo, pdf);
default:
kernel_assert(false);
*pdf = 0.0f;
return zero_spectrum();
}
}
ccl_device int volume_phase_sample(ccl_private const ShaderData *sd,
@@ -132,7 +56,71 @@ ccl_device int volume_phase_sample(ccl_private const ShaderData *sd,
ccl_private float3 *wo,
ccl_private float *pdf)
{
return volume_henyey_greenstein_sample(svc, sd->wi, rand, eval, wo, pdf);
switch (svc->type) {
case CLOSURE_VOLUME_FOURNIER_FORAND_ID:
return volume_fournier_forand_sample(sd, svc, rand, eval, wo, pdf);
case CLOSURE_VOLUME_RAYLEIGH_ID:
return volume_rayleigh_sample(sd, rand, eval, wo, pdf);
case CLOSURE_VOLUME_DRAINE_ID:
return volume_draine_sample(sd, svc, rand, eval, wo, pdf);
case CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID:
return volume_henyey_greenstein_sample(sd, svc, rand, eval, wo, pdf);
default:
kernel_assert(false);
*pdf = 0.0f;
return 0;
}
}
ccl_device bool volume_phase_equal(ccl_private const ShaderClosure *c1,
ccl_private const ShaderClosure *c2)
{
if (c1->type != c2->type) {
return false;
}
switch (c1->type) {
case CLOSURE_VOLUME_FOURNIER_FORAND_ID: {
ccl_private FournierForandVolume *v1 = (ccl_private FournierForandVolume *)c1;
ccl_private FournierForandVolume *v2 = (ccl_private FournierForandVolume *)c2;
return v1->c1 == v2->c1 && v1->c2 == v2->c2 && v1->c3 == v2->c3;
}
case CLOSURE_VOLUME_RAYLEIGH_ID:
return true;
case CLOSURE_VOLUME_DRAINE_ID: {
ccl_private DraineVolume *v1 = (ccl_private DraineVolume *)c1;
ccl_private DraineVolume *v2 = (ccl_private DraineVolume *)c2;
return v1->g == v2->g && v1->alpha == v2->alpha;
}
case CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID: {
ccl_private HenyeyGreensteinVolume *v1 = (ccl_private HenyeyGreensteinVolume *)c1;
ccl_private HenyeyGreensteinVolume *v2 = (ccl_private HenyeyGreensteinVolume *)c2;
return v1->g == v2->g;
}
default:
return false;
}
return false;
}
/* Approximate phase functions as Henyey-Greenstein for volume guiding.
* TODO: This is not ideal, we should use RIS guiding for non-HG phase functions. */
ccl_device float volume_phase_get_g(ccl_private const ShaderVolumeClosure *svc)
{
switch (svc->type) {
case CLOSURE_VOLUME_FOURNIER_FORAND_ID:
/* TODO */
return 1.0f;
case CLOSURE_VOLUME_RAYLEIGH_ID:
/* Approximate as isotropic */
return 0.0f;
case CLOSURE_VOLUME_DRAINE_ID:
/* Approximate as HG, TODO */
return ((ccl_private DraineVolume *)svc)->g;
case CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID:
return ((ccl_private HenyeyGreensteinVolume *)svc)->g;
default:
return 0.0f;
}
}
/* Volume sampling utilities. */

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2011-2024 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/closure/volume_util.h"
CCL_NAMESPACE_BEGIN
/* DRAINE CLOSURE */
typedef struct DraineVolume {
SHADER_CLOSURE_VOLUME_BASE;
float g;
float alpha;
} DraineVolume;
static_assert(sizeof(ShaderVolumeClosure) >= sizeof(DraineVolume), "DraineVolume is too large!");
ccl_device int volume_draine_setup(ccl_private DraineVolume *volume)
{
volume->type = CLOSURE_VOLUME_DRAINE_ID;
/* clamp anisotropy */
volume->g = signf(volume->g) * min(fabsf(volume->g), 1.0f - 1e-3f);
return SD_SCATTER;
}
ccl_device Spectrum volume_draine_eval(ccl_private const ShaderData *sd,
ccl_private const ShaderVolumeClosure *svc,
float3 wo,
ccl_private float *pdf)
{
ccl_private const DraineVolume *volume = (ccl_private const DraineVolume *)svc;
/* note that wi points towards the viewer */
float cos_theta = dot(-sd->wi, wo);
*pdf = phase_draine(cos_theta, volume->g, volume->alpha);
return make_spectrum(*pdf);
}
ccl_device int volume_draine_sample(ccl_private const ShaderData *sd,
ccl_private const ShaderVolumeClosure *svc,
float2 rand,
ccl_private Spectrum *eval,
ccl_private float3 *wo,
ccl_private float *pdf)
{
ccl_private const DraineVolume *volume = (ccl_private const DraineVolume *)svc;
/* note that wi points towards the viewer and so is used negated */
*wo = phase_draine_sample(-sd->wi, volume->g, volume->alpha, rand, pdf);
*eval = make_spectrum(*pdf); /* perfect importance sampling */
return LABEL_VOLUME_SCATTER;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,71 @@
/* SPDX-FileCopyrightText: 2011-2024 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/closure/volume_util.h"
CCL_NAMESPACE_BEGIN
/* FOURNIER-FORAND CLOSURE */
typedef struct FournierForandVolume {
SHADER_CLOSURE_VOLUME_BASE;
/* Precomputed coefficients, based on B and IOR */
float c1, c2, c3;
} FournierForandVolume;
static_assert(sizeof(ShaderVolumeClosure) >= sizeof(FournierForandVolume),
"FournierForandVolume is too large!");
ccl_device int volume_fournier_forand_setup(ccl_private FournierForandVolume *volume,
float B,
float IOR)
{
volume->type = CLOSURE_VOLUME_FOURNIER_FORAND_ID;
/* clamp backscatter fraction to avoid delta function */
B = min(fabsf(B), 0.5f - 1e-3f);
IOR = max(IOR, 1.0f + 1e-3f);
float3 coeffs = phase_fournier_forand_coeffs(B, IOR);
volume->c1 = coeffs.x;
volume->c2 = coeffs.y;
volume->c3 = coeffs.z;
return SD_SCATTER;
}
ccl_device Spectrum volume_fournier_forand_eval(ccl_private const ShaderData *sd,
ccl_private const ShaderVolumeClosure *svc,
float3 wo,
ccl_private float *pdf)
{
ccl_private const FournierForandVolume *volume = (ccl_private const FournierForandVolume *)svc;
const float3 coeffs = make_float3(volume->c1, volume->c2, volume->c3);
/* note that wi points towards the viewer */
float cos_theta = dot(-sd->wi, wo);
*pdf = phase_fournier_forand(cos_theta, coeffs);
return make_spectrum(*pdf);
}
ccl_device int volume_fournier_forand_sample(ccl_private const ShaderData *sd,
ccl_private const ShaderVolumeClosure *svc,
float2 rand,
ccl_private Spectrum *eval,
ccl_private float3 *wo,
ccl_private float *pdf)
{
ccl_private const FournierForandVolume *volume = (ccl_private const FournierForandVolume *)svc;
const float3 coeffs = make_float3(volume->c1, volume->c2, volume->c3);
/* note that wi points towards the viewer and so is used negated */
*wo = phase_fournier_forand_sample(-sd->wi, coeffs, rand, pdf);
*eval = make_spectrum(*pdf); /* perfect importance sampling */
return LABEL_VOLUME_SCATTER;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,62 @@
/* SPDX-FileCopyrightText: 2011-2024 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/closure/volume_util.h"
CCL_NAMESPACE_BEGIN
/* HENYEY-GREENSTEIN CLOSURE */
typedef struct HenyeyGreensteinVolume {
SHADER_CLOSURE_VOLUME_BASE;
float g;
} HenyeyGreensteinVolume;
static_assert(sizeof(ShaderVolumeClosure) >= sizeof(HenyeyGreensteinVolume),
"HenyeyGreensteinVolume is too large!");
ccl_device int volume_henyey_greenstein_setup(ccl_private HenyeyGreensteinVolume *volume)
{
volume->type = CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID;
/* clamp anisotropy to avoid delta function */
volume->g = signf(volume->g) * min(fabsf(volume->g), 1.0f - 1e-3f);
return SD_SCATTER;
}
ccl_device Spectrum volume_henyey_greenstein_eval(ccl_private const ShaderData *sd,
ccl_private const ShaderVolumeClosure *svc,
float3 wo,
ccl_private float *pdf)
{
ccl_private const HenyeyGreensteinVolume *volume = (ccl_private const HenyeyGreensteinVolume *)
svc;
/* note that wi points towards the viewer */
float cos_theta = dot(-sd->wi, wo);
*pdf = phase_henyey_greenstein(cos_theta, volume->g);
return make_spectrum(*pdf);
}
ccl_device int volume_henyey_greenstein_sample(ccl_private const ShaderData *sd,
ccl_private const ShaderVolumeClosure *svc,
float2 rand,
ccl_private Spectrum *eval,
ccl_private float3 *wo,
ccl_private float *pdf)
{
ccl_private const HenyeyGreensteinVolume *volume = (ccl_private const HenyeyGreensteinVolume *)
svc;
/* note that wi points towards the viewer and so is used negated */
*wo = phase_henyey_greenstein_sample(-sd->wi, volume->g, rand, pdf);
*eval = make_spectrum(*pdf); /* perfect importance sampling */
return LABEL_VOLUME_SCATTER;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,49 @@
/* SPDX-FileCopyrightText: 2011-2024 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/closure/volume_util.h"
CCL_NAMESPACE_BEGIN
/* RAYLEIGH CLOSURE */
typedef struct RayleighVolume {
SHADER_CLOSURE_VOLUME_BASE;
} RayleighVolume;
static_assert(sizeof(ShaderVolumeClosure) >= sizeof(RayleighVolume),
"RayleighVolume is too large!");
ccl_device int volume_rayleigh_setup(ccl_private RayleighVolume *volume)
{
volume->type = CLOSURE_VOLUME_RAYLEIGH_ID;
return SD_SCATTER;
}
ccl_device Spectrum volume_rayleigh_eval(ccl_private const ShaderData *sd,
float3 wo,
ccl_private float *pdf)
{
/* note that wi points towards the viewer */
float cos_theta = dot(-sd->wi, wo);
*pdf = phase_rayleigh(cos_theta);
return make_spectrum(*pdf);
}
ccl_device int volume_rayleigh_sample(ccl_private const ShaderData *sd,
float2 rand,
ccl_private Spectrum *eval,
ccl_private float3 *wo,
ccl_private float *pdf)
{
/* note that wi points towards the viewer and so is used negated */
*wo = phase_rayleigh_sample(-sd->wi, rand, pdf);
*eval = make_spectrum(*pdf); /* perfect importance sampling */
return LABEL_VOLUME_SCATTER;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,238 @@
/* SPDX-FileCopyrightText: 2011-2024 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
CCL_NAMESPACE_BEGIN
/* Given a random number, sample a direction that makes an angle of theta with direction D. */
ccl_device float3 phase_sample_direction(float3 D, float cos_theta, float rand)
{
float sin_theta = sin_from_cos(cos_theta);
float phi = M_2PI_F * rand;
float3 dir = make_float3(sin_theta * cosf(phi), sin_theta * sinf(phi), cos_theta);
float3 T, B;
make_orthonormals(D, &T, &B);
dir = dir.x * T + dir.y * B + dir.z * D;
return dir;
}
/* Given cosine between rays, return probability density that a photon bounces
* to that direction. The g parameter controls how different it is from the
* uniform sphere. g=0 uniform diffuse-like, g=1 close to sharp single ray. */
ccl_device float phase_henyey_greenstein(float cos_theta, float g)
{
if (fabsf(g) < 1e-3f) {
return M_1_4PI_F;
}
const float fac = 1 + g * (g - 2 * cos_theta);
return (1 - sqr(g)) / (M_4PI_F * fac * safe_sqrtf(fac));
}
ccl_device float3 phase_henyey_greenstein_sample(float3 D,
float g,
float2 rand,
ccl_private float *pdf)
{
float cos_theta = 1 - 2 * rand.x;
if (fabsf(g) >= 1e-3f) {
const float k = (1 - sqr(g)) / (1 - g * cos_theta);
cos_theta = (1 + sqr(g) - sqr(k)) / (2 * g);
}
*pdf = phase_henyey_greenstein(cos_theta, g);
return phase_sample_direction(D, cos_theta, rand.y);
}
/* Given cosine between rays, return probability density that a photon bounces to that direction
* according to the constant Rayleigh phase function.
* See https://doi.org/10.1364/JOSAA.28.002436 for details. */
ccl_device float phase_rayleigh(float cos_theta)
{
return (0.1875f * M_1_PI_F) * (1.0f + sqr(cos_theta));
}
ccl_device float3 phase_rayleigh_sample(float3 D, float2 rand, ccl_private float *pdf)
{
const float a = 2 - 4 * rand.x;
/* Metal doesn't have cbrtf, but since we compute u - 1/u anyways, we can just as well
* use the inverse cube root for which there is a simple Quake-style fast implementation.*/
const float inv_u = -fast_inv_cbrtf(sqrtf(1 + sqr(a)) - a);
const float cos_theta = 1 / inv_u - inv_u;
*pdf = phase_rayleigh(cos_theta);
return phase_sample_direction(D, cos_theta, rand.y);
}
/* Given cosine between rays, return probability density that a photon bounces to that direction
* according to the Draine phase function. This is a generalisation of the Henyey-Greenstein
* function which bridges the cases of HG and Rayleigh scattering. The parameter g mainly controls
* the first moment <cos theta>, and alpha the second moment <cos2 theta> of the exact phase
* function. alpha=0 reduces to HG function, g=0, alpha=1 reduces to Rayleigh function, alpha=1
* reduces to Cornette-Shanks function.
* See https://doi.org/10.1086/379118 for details. */
ccl_device float phase_draine(float cos_theta, float g, float alpha)
{
/* Check special cases. */
if (fabsf(g) < 1e-3f && alpha > 0.999f) {
return phase_rayleigh(cos_theta);
}
else if (fabsf(alpha) < 1e-3f) {
return phase_henyey_greenstein(cos_theta, g);
}
const float g2 = sqr(g);
const float fac = 1 + g2 - 2 * g * cos_theta;
return ((1 - g2) * (1 + alpha * sqr(cos_theta))) /
((1 + (alpha * (1 + 2 * g2)) * (1 / 3.0f)) * M_4PI_F * fac * sqrtf(fac));
}
/* Adapted from the hlsl code provided in https://research.nvidia.com/labs/rtr/approximate-mie/ */
ccl_device float phase_draine_sample_cos(float g, float alpha, float rand)
{
const float g2 = sqr(g), g3 = g * g2, g4 = sqr(g2), g6 = g2 * g4;
const float pgp1_2 = sqr(1 + g2);
const float T1a = alpha * (g4 - 1), T1a3 = sqr(T1a) * T1a;
const float T2 = -1296 * (g2 - 1) * (alpha - alpha * g2) * T1a * (4 * g2 + alpha * pgp1_2);
const float T9 = 2 + g2 + g3 * (1 + 2 * g2) * (2 * rand - 1);
const float T3 = 3 * g2 * (1 + g * (2 * rand - 1)) + alpha * T9;
const float T4a = 432 * T1a3 + T2 + 432 * (alpha * (1 - g2)) * sqr(T3);
const float T10 = alpha * (2 * g4 - g2 - g6);
const float T4b = 144 * T10, T4b3 = sqr(T4b) * T4b;
const float T4 = T4a + sqrtf(-4 * T4b3 + sqr(T4a));
const float inv_T4p3 = fast_inv_cbrtf(T4);
const float T8 = 48 * M_CBRT2_F * T10;
const float T6 = (2 * T1a + T8 * inv_T4p3 + 1 / (3 * M_CBRT2_F * inv_T4p3)) / (alpha * (1 - g2));
const float T5 = 6 * (1 + g2) + T6;
const float T7 = 6 * (1 + g2) - (8 * T3) / (alpha * (g2 - 1) * sqrtf(T5)) - T6;
return (1 + g2 - 0.25f * sqr(sqrtf(T7) - sqrtf(T5))) / (2 * g);
}
ccl_device float3
phase_draine_sample(float3 D, float g, float alpha, float2 rand, ccl_private float *pdf)
{
/* Check special cases. */
if (fabsf(g) < 1e-3f && alpha > 0.999f) {
return phase_rayleigh_sample(D, rand, pdf);
}
else if (fabsf(alpha) < 1e-3f) {
return phase_henyey_greenstein_sample(D, g, rand, pdf);
}
const float cos_theta = phase_draine_sample_cos(g, alpha, rand.x);
*pdf = phase_draine(cos_theta, g, alpha);
return phase_sample_direction(D, cos_theta, rand.y);
}
ccl_device float phase_fournier_forand_delta(float n, float sin_htheta_sqr)
{
float u = 4 * sin_htheta_sqr;
return u / (3 * sqr(n - 1));
}
ccl_device_inline float3 phase_fournier_forand_coeffs(float B, float IOR)
{
const float d90 = phase_fournier_forand_delta(IOR, 0.5f);
const float d180 = phase_fournier_forand_delta(IOR, 1.0f);
const float v = -logf(2 * B * (d90 - 1) + 1) / logf(d90);
return make_float3(IOR, v, (powf(d180, -v) - 1) / (d180 - 1));
}
/* Given cosine between rays, return probability density that a photon bounces to that direction
* according to the Fournier-Forand phase function. The n parameter is the particle index of
* refraction and controls how much of the light is refracted. B is the particle backscatter
* fraction, B = b_b / b.
* See https://doi.org/10.1117/12.366488 for details. */
ccl_device_inline float phase_fournier_forand_impl(
float cos_theta, float delta, float pow_delta_v, float v, float sin_htheta_sqr, float pf_coeff)
{
const float m_delta = 1 - delta, m_pow_delta_v = 1 - pow_delta_v;
float pf;
if (fabsf(m_delta) < 1e-3f) {
/* Special case (first-order Taylor expansion) to avoid singularity at delta near 1.0 */
pf = v * ((v - 1) - (v + 1) / sin_htheta_sqr) * (1 / (8 * M_PI_F));
pf += v * (v + 1) * m_delta * (2 * (v - 1) - (2 * v + 1) / sin_htheta_sqr) *
(1 / (24 * M_PI_F));
}
else {
pf = (v * m_delta - m_pow_delta_v + (delta * m_pow_delta_v - v * m_delta) / sin_htheta_sqr) /
(M_4PI_F * sqr(m_delta) * pow_delta_v);
}
pf += pf_coeff * (3 * sqr(cos_theta) - 1);
return pf;
}
ccl_device float phase_fournier_forand(float cos_theta, float3 coeffs)
{
if (fabsf(cos_theta) >= 1.0f) {
return 0.0f;
}
const float n = coeffs.x, v = coeffs.y;
const float pf_coeff = coeffs.z * (1.0f / (16.0f * M_PI_F));
const float sin_htheta_sqr = 0.5f * (1 - cos_theta); /* sin^2(theta / 2)*/
const float delta = phase_fournier_forand_delta(n, sin_htheta_sqr);
return phase_fournier_forand_impl(cos_theta, delta, powf(delta, v), v, sin_htheta_sqr, pf_coeff);
}
ccl_device float phase_fournier_forand_newton(float rand, float3 coeffs)
{
const float n = coeffs.x, v = coeffs.y;
const float cdf_coeff = coeffs.z * (1.0f / 8.0f);
const float pf_coeff = coeffs.z * (1.0f / (16.0f * M_PI_F));
float cos_theta = 0.64278760968f; /* Initial guess: 50 degrees */
for (int it = 0; it < 20; it++) {
const float sin_htheta_sqr = 0.5f * (1 - cos_theta); /* sin^2(theta / 2)*/
const float delta = phase_fournier_forand_delta(n, sin_htheta_sqr);
const float pow_delta_v = powf(delta, v);
const float m_delta = 1 - delta, m_pow_delta_v = 1 - pow_delta_v;
/* Evaluate CDF and phase functions */
float cdf;
if (fabsf(m_delta) < 1e-3f) {
/* Special case (first-order Taylor expansion) to avoid singularity at delta near 1.0 */
cdf = 1 + v * (1 - sin_htheta_sqr) * (1 - 0.5f * (v + 1) * m_delta);
}
else {
cdf = (1 - pow_delta_v * delta - m_pow_delta_v * sin_htheta_sqr) / (m_delta * pow_delta_v);
}
cdf += cdf_coeff * cos_theta * (1 - sqr(cos_theta));
const float pf = phase_fournier_forand_impl(
cos_theta, delta, pow_delta_v, v, sin_htheta_sqr, pf_coeff);
/* Perform Newton iteration step */
float new_cos_theta = cos_theta + M_1_2PI_F * (cdf - rand) / pf;
/* Don't step off past 1.0, approach the peak slowly */
if (new_cos_theta >= 1.0f) {
new_cos_theta = max(mix(cos_theta, 1.0f, 0.5f), 0.99f);
}
if (fabsf(cos_theta - new_cos_theta) < 1e-6f || new_cos_theta == 1.0f) {
return new_cos_theta;
}
cos_theta = new_cos_theta;
}
/* Reached iteration limit, so give up and use what we have. */
return cos_theta;
}
ccl_device float3 phase_fournier_forand_sample(float3 D,
float3 coeffs,
float2 rand,
ccl_private float *pdf)
{
float cos_theta = phase_fournier_forand_newton(rand.x, coeffs);
*pdf = phase_fournier_forand(cos_theta, coeffs);
return phase_sample_direction(D, cos_theta, rand.y);
}
CCL_NAMESPACE_END

View File

@@ -315,11 +315,11 @@ ccl_device_inline bool subsurface_random_walk(KernelGlobals kg,
cos_theta = -cos_theta;
}
float3 newD = direction_from_cosine(N, cos_theta, rand_scatter.y);
hg_pdf = single_peaked_henyey_greenstein(dot(ray.D, newD), anisotropy);
hg_pdf = phase_henyey_greenstein(dot(ray.D, newD), anisotropy);
ray.D = newD;
}
else {
float3 newD = henyey_greenstrein_sample(ray.D, anisotropy, rand_scatter, &hg_pdf);
float3 newD = phase_henyey_greenstein_sample(ray.D, anisotropy, rand_scatter, &hg_pdf);
cos_theta = dot(newD, N);
ray.D = newD;
}

View File

@@ -30,21 +30,13 @@ ccl_device_inline void volume_shader_merge_closures(ccl_private ShaderData *sd)
for (int i = 0; i < sd->num_closure; i++) {
ccl_private ShaderClosure *sci = &sd->closure[i];
if (sci->type != CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID) {
if (!CLOSURE_IS_VOLUME_SCATTER(sci->type)) {
continue;
}
for (int j = i + 1; j < sd->num_closure; j++) {
ccl_private ShaderClosure *scj = &sd->closure[j];
if (sci->type != scj->type) {
continue;
}
ccl_private const HenyeyGreensteinVolume *hgi = (ccl_private const HenyeyGreensteinVolume *)
sci;
ccl_private const HenyeyGreensteinVolume *hgj = (ccl_private const HenyeyGreensteinVolume *)
scj;
if (!(hgi->g == hgj->g)) {
if (!volume_phase_equal(sci, scj)) {
continue;
}
@@ -73,16 +65,10 @@ ccl_device_inline void volume_shader_copy_phases(ccl_private ShaderVolumePhases
for (int i = 0; i < sd->num_closure; i++) {
ccl_private const ShaderClosure *from_sc = &sd->closure[i];
ccl_private const HenyeyGreensteinVolume *from_hg =
(ccl_private const HenyeyGreensteinVolume *)from_sc;
if (from_sc->type == CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID) {
ccl_private ShaderVolumeClosure *to_sc = &phases->closure[phases->num_closure];
to_sc->weight = from_sc->weight;
to_sc->sample_weight = from_sc->sample_weight;
to_sc->g = from_hg->g;
phases->num_closure++;
if (CLOSURE_IS_VOLUME_SCATTER(from_sc->type)) {
/* ShaderVolumeClosure is a subset of ShaderClosure, so this is fine for all volume scatter
* closures. */
phases->closure[phases->num_closure++] = *((ccl_private const ShaderVolumeClosure *)from_sc);
if (phases->num_closure >= MAX_VOLUME_CLOSURE) {
break;
}
@@ -147,7 +133,8 @@ ccl_device_inline void volume_shader_prepare_guiding(KernelGlobals kg,
/* Init guiding for selected phase function. */
ccl_private const ShaderVolumeClosure *svc = &phases->closure[phase_id];
if (!guiding_phase_init(kg, state, P, D, svc->g, rand_phase_guiding)) {
const float phase_g = volume_phase_get_g(svc);
if (!guiding_phase_init(kg, state, P, D, phase_g, rand_phase_guiding)) {
state->guiding.use_volume_guiding = false;
return;
}
@@ -301,7 +288,7 @@ ccl_device int volume_shader_phase_guided_sample(KernelGlobals kg,
*unguided_phase_pdf = 0.0f;
float guide_pdf = 0.0f;
*sampled_roughness = 1.0f - fabsf(svc->g);
*sampled_roughness = 1.0f - fabsf(volume_phase_get_g(svc));
bsdf_eval_init(phase_eval, zero_spectrum());
@@ -355,7 +342,7 @@ ccl_device int volume_shader_phase_sample(KernelGlobals kg,
ccl_private float *pdf,
ccl_private float *sampled_roughness)
{
*sampled_roughness = 1.0f - fabsf(svc->g);
*sampled_roughness = 1.0f - fabsf(volume_phase_get_g(svc));
Spectrum eval = zero_spectrum();
*pdf = 0.0f;

View File

@@ -1086,4 +1086,62 @@ ccl_device void osl_closure_henyey_greenstein_setup(
sd->flag |= volume_henyey_greenstein_setup(volume);
}
ccl_device void osl_closure_fournier_forand_setup(
KernelGlobals kg,
ccl_private ShaderData *sd,
uint32_t path_flag,
float3 weight,
ccl_private const VolumeFournierForandClosure *closure,
float3 *layer_albedo)
{
volume_extinction_setup(sd, rgb_to_spectrum(weight));
ccl_private FournierForandVolume *volume = (ccl_private FournierForandVolume *)bsdf_alloc(
sd, sizeof(FournierForandVolume), rgb_to_spectrum(weight));
if (!volume) {
return;
}
sd->flag |= volume_fournier_forand_setup(volume, closure->B, closure->IOR);
}
ccl_device void osl_closure_draine_setup(KernelGlobals kg,
ccl_private ShaderData *sd,
uint32_t path_flag,
float3 weight,
ccl_private const VolumeDraineClosure *closure,
float3 *layer_albedo)
{
volume_extinction_setup(sd, rgb_to_spectrum(weight));
ccl_private DraineVolume *volume = (ccl_private DraineVolume *)bsdf_alloc(
sd, sizeof(DraineVolume), rgb_to_spectrum(weight));
if (!volume) {
return;
}
volume->g = closure->g;
volume->alpha = closure->alpha;
sd->flag |= volume_draine_setup(volume);
}
ccl_device void osl_closure_rayleigh_setup(KernelGlobals kg,
ccl_private ShaderData *sd,
uint32_t path_flag,
float3 weight,
ccl_private const VolumeRayleighClosure *closure,
float3 *layer_albedo)
{
volume_extinction_setup(sd, rgb_to_spectrum(weight));
ccl_private RayleighVolume *volume = (ccl_private RayleighVolume *)bsdf_alloc(
sd, sizeof(RayleighVolume), rgb_to_spectrum(weight));
if (!volume) {
return;
}
sd->flag |= volume_rayleigh_setup(volume);
}
CCL_NAMESPACE_END

View File

@@ -223,6 +223,19 @@ OSL_CLOSURE_STRUCT_BEGIN(VolumeHenyeyGreenstein, henyey_greenstein)
OSL_CLOSURE_STRUCT_MEMBER(VolumeHenyeyGreenstein, FLOAT, float, g, NULL)
OSL_CLOSURE_STRUCT_END(VolumeHenyeyGreenstein, henyey_greenstein)
OSL_CLOSURE_STRUCT_BEGIN(VolumeFournierForand, fournier_forand)
OSL_CLOSURE_STRUCT_MEMBER(VolumeFournierForand, FLOAT, float, B, NULL)
OSL_CLOSURE_STRUCT_MEMBER(VolumeFournierForand, FLOAT, float, IOR, NULL)
OSL_CLOSURE_STRUCT_END(VolumeFournierForand, fournier_forand)
OSL_CLOSURE_STRUCT_BEGIN(VolumeDraine, draine)
OSL_CLOSURE_STRUCT_MEMBER(VolumeDraine, FLOAT, float, g, NULL)
OSL_CLOSURE_STRUCT_MEMBER(VolumeDraine, FLOAT, float, alpha, NULL)
OSL_CLOSURE_STRUCT_END(VolumeDraine, draine)
OSL_CLOSURE_STRUCT_BEGIN(VolumeRayleigh, rayleigh)
OSL_CLOSURE_STRUCT_END(VolumeRayleigh, rayleigh)
#undef OSL_CLOSURE_STRUCT_BEGIN
#undef OSL_CLOSURE_STRUCT_END
#undef OSL_CLOSURE_STRUCT_MEMBER

View File

@@ -4,10 +4,39 @@
#include "stdcycles.h"
shader node_scatter_volume(color Color = color(0.8, 0.8, 0.8),
shader node_scatter_volume(string phase = "Henyey-Greenstein",
color Color = color(0.8, 0.8, 0.8),
float Density = 1.0,
float Anisotropy = 0.0,
float IOR = 1.33,
float Backscatter = 0.1,
float Alpha = 0.5,
float Diameter = 20.0,
output closure color Volume = 0)
{
Volume = (Color * max(Density, 0.0)) * henyey_greenstein(Anisotropy);
closure color scatter = 0;
if (phase == "Fournier-Forand") {
scatter = fournier_forand(Backscatter, IOR);
}
else if (phase == "Draine") {
scatter = draine(Anisotropy, Alpha);
}
else if (phase == "Rayleigh") {
scatter = rayleigh();
}
else if (phase == "Mie") {
/* Approxmiation of Mie phase function for water droplets using a mix of Draine and H-G.
* See kernel/svm/closure.h for details. */
float d = max(Diameter, 2.0);
float aniso_hg = exp(-0.0990567 / (d - 1.67154));
float aniso_d = exp(-2.20679 / (d + 3.91029) - 0.428934);
float alpha = exp(3.62489 - 8.29288 / (d + 5.52825));
float mixture = exp(-0.599085 / (d - 0.641583) - 0.665888);
scatter = mix(henyey_greenstein(aniso_hg), draine(aniso_d, alpha), mixture);
}
else {
scatter = henyey_greenstein(Anisotropy);
}
Volume = (Color * max(Density, 0.0)) * scatter;
}

View File

@@ -61,6 +61,9 @@ closure color hair_huang(normal N,
// Volume
closure color henyey_greenstein(float g) BUILTIN;
closure color fournier_forand(float B, float IOR) BUILTIN;
closure color draine(float g, float alpha) BUILTIN;
closure color rayleigh() BUILTIN;
closure color absorption() BUILTIN;
// Ray Portal

View File

@@ -1006,13 +1006,10 @@ ccl_device_noinline void svm_node_closure_volume(KernelGlobals kg,
return;
}
uint type, density_offset, anisotropy_offset;
uint mix_weight_offset;
svm_unpack_node_uchar4(node.y, &type, &density_offset, &anisotropy_offset, &mix_weight_offset);
uint type, density_offset, param1_offset, mix_weight_offset;
svm_unpack_node_uchar4(node.y, &type, &density_offset, &param1_offset, &mix_weight_offset);
float mix_weight = (stack_valid(mix_weight_offset) ? stack_load_float(stack, mix_weight_offset) :
1.0f);
if (mix_weight == 0.0f) {
return;
}
@@ -1031,16 +1028,70 @@ ccl_device_noinline void svm_node_closure_volume(KernelGlobals kg,
weight *= density;
/* Add closure for volume scattering. */
if (type == CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID) {
ccl_private HenyeyGreensteinVolume *volume = (ccl_private HenyeyGreensteinVolume *)bsdf_alloc(
sd, sizeof(HenyeyGreensteinVolume), weight);
if (volume) {
float anisotropy = (stack_valid(anisotropy_offset)) ?
stack_load_float(stack, anisotropy_offset) :
__uint_as_float(node.w);
volume->g = anisotropy; /* g */
sd->flag |= volume_henyey_greenstein_setup(volume);
if (CLOSURE_IS_VOLUME_SCATTER(type)) {
switch (type) {
case CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID: {
ccl_private HenyeyGreensteinVolume *volume = (ccl_private HenyeyGreensteinVolume *)
bsdf_alloc(sd, sizeof(HenyeyGreensteinVolume), weight);
if (volume) {
volume->g = stack_valid(param1_offset) ? stack_load_float(stack, param1_offset) :
__uint_as_float(node.w);
sd->flag |= volume_henyey_greenstein_setup(volume);
}
} break;
case CLOSURE_VOLUME_FOURNIER_FORAND_ID: {
ccl_private FournierForandVolume *volume = (ccl_private FournierForandVolume *)bsdf_alloc(
sd, sizeof(FournierForandVolume), weight);
if (volume) {
const float IOR = stack_load_float(stack, param1_offset);
const float B = stack_load_float(stack, node.w);
sd->flag |= volume_fournier_forand_setup(volume, B, IOR);
}
} break;
case CLOSURE_VOLUME_RAYLEIGH_ID: {
ccl_private RayleighVolume *volume = (ccl_private RayleighVolume *)bsdf_alloc(
sd, sizeof(RayleighVolume), weight);
if (volume) {
sd->flag |= volume_rayleigh_setup(volume);
}
break;
}
case CLOSURE_VOLUME_DRAINE_ID: {
ccl_private DraineVolume *volume = (ccl_private DraineVolume *)bsdf_alloc(
sd, sizeof(DraineVolume), weight);
if (volume) {
volume->g = stack_load_float(stack, param1_offset);
volume->alpha = stack_load_float(stack, node.w);
sd->flag |= volume_draine_setup(volume);
}
} break;
case CLOSURE_VOLUME_MIE_ID: {
/* We approximate the Mie phase function for water droplets using a mix of Draine and H-G
* following "An Approximate Mie Scattering Function for Fog and Cloud Rendering", Johannes
* Jendersie and Eugene d'Eon, https://research.nvidia.com/labs/rtr/approximate-mie.
*
* The numerical fit here (eq. 4-7 in the paper) is intended for 5<d<50.
* Generally, we try to allow exceeding the soft limits when reasonable. Here, the only
* real limit is that the values need to stay within -1<g<1, 0<a and 0<mixture<1.
* This results in the condition d > 1.67154, so we clamp it to 2 to be safe. */
const float d = max(2.0f,
stack_valid(param1_offset) ? stack_load_float(stack, param1_offset) :
__uint_as_float(node.w));
const float mixture = fast_expf(-0.599085f / (d - 0.641583f) - 0.665888f);
ccl_private HenyeyGreensteinVolume *hg = (ccl_private HenyeyGreensteinVolume *)bsdf_alloc(
sd, sizeof(HenyeyGreensteinVolume), weight * (1.0f - mixture));
if (hg) {
hg->g = fast_expf(-0.0990567f / (d - 1.67154f));
sd->flag |= volume_henyey_greenstein_setup(hg);
}
ccl_private DraineVolume *draine = (ccl_private DraineVolume *)bsdf_alloc(
sd, sizeof(DraineVolume), weight * mixture);
if (draine) {
draine->g = fast_expf(-2.20679f / (d + 3.91029f) - 0.428934f);
draine->alpha = fast_expf(3.62489f - 8.29288f / (d + 5.52825f));
sd->flag |= volume_draine_setup(draine);
}
} break;
}
}

View File

@@ -470,6 +470,10 @@ typedef enum ClosureType {
CLOSURE_VOLUME_ID,
CLOSURE_VOLUME_ABSORPTION_ID,
CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID,
CLOSURE_VOLUME_MIE_ID, /* virtual closure */
CLOSURE_VOLUME_FOURNIER_FORAND_ID,
CLOSURE_VOLUME_RAYLEIGH_ID,
CLOSURE_VOLUME_DRAINE_ID,
CLOSURE_BSDF_PRINCIPLED_ID,
@@ -502,12 +506,13 @@ typedef enum ClosureType {
(type != CLOSURE_NONE_ID && type <= CLOSURE_BSSRDF_RANDOM_WALK_SKIN_ID)
#define CLOSURE_IS_BSSRDF(type) \
(type >= CLOSURE_BSSRDF_BURLEY_ID && type <= CLOSURE_BSSRDF_RANDOM_WALK_SKIN_ID)
#define CLOSURE_IS_VOLUME(type) \
(type >= CLOSURE_VOLUME_ID && type <= CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID)
#define CLOSURE_IS_VOLUME_SCATTER(type) (type == CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID)
#define CLOSURE_IS_VOLUME(type) (type >= CLOSURE_VOLUME_ID && type <= CLOSURE_VOLUME_DRAINE_ID)
#define CLOSURE_IS_VOLUME_SCATTER(type) \
(type >= CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID && type <= CLOSURE_VOLUME_DRAINE_ID)
#define CLOSURE_IS_VOLUME_ABSORPTION(type) (type == CLOSURE_VOLUME_ABSORPTION_ID)
#define CLOSURE_IS_HOLDOUT(type) (type == CLOSURE_HOLDOUT_ID)
#define CLOSURE_IS_PHASE(type) (type == CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID)
#define CLOSURE_IS_PHASE(type) \
(type >= CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID && type <= CLOSURE_VOLUME_DRAINE_ID)
#define CLOSURE_IS_REFRACTION(type) \
(type >= CLOSURE_BSDF_MICROFACET_BECKMANN_REFRACTION_ID && \
type <= CLOSURE_BSDF_MICROFACET_GGX_REFRACTION_ID)

View File

@@ -977,6 +977,15 @@ typedef struct AttributeMap {
float sample_weight; \
float3 N
/* To save some space, volume closures (phase functions) don't store a normal.
* They are still allocated as ShaderClosures first, but get assigned to
* slots of type ShaderVolumeClosure later, so make sure to keep the layout
* in sync. */
#define SHADER_CLOSURE_VOLUME_BASE \
Spectrum weight; \
ClosureType type; \
float sample_weight
typedef struct ccl_align(16) ShaderClosure
{
SHADER_CLOSURE_BASE;
@@ -1221,12 +1230,18 @@ ShaderDataCausticsStorage;
/* Compact volume closures storage.
*
* Used for decoupled direct/indirect light closure storage. */
* Used for decoupled direct/indirect light closure storage.
*
* This shares its basic layout with SHADER_CLOSURE_VOLUME_BASE and ShaderClosure,
* just without the normal and with less space for closure-specific parameters.
* That way, we can just cast ShaderClosure* to ShaderVolumeClosure* and assign it.
*/
typedef struct ShaderVolumeClosure {
Spectrum weight;
ClosureType type;
float sample_weight;
float g;
/* Space for closure-specific parameters. */
float param[3];
} ShaderVolumeClosure;
typedef struct ShaderVolumePhases {

View File

@@ -3400,7 +3400,10 @@ VolumeNode::VolumeNode(const NodeType *node_type) : ShaderNode(node_type)
closure = CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID;
}
void VolumeNode::compile(SVMCompiler &compiler, ShaderInput *param1, ShaderInput *param2)
void VolumeNode::compile(SVMCompiler &compiler,
ShaderInput *density,
ShaderInput *param1,
ShaderInput *param2)
{
ShaderInput *color_in = input("Color");
@@ -3411,19 +3414,32 @@ void VolumeNode::compile(SVMCompiler &compiler, ShaderInput *param1, ShaderInput
compiler.add_node(NODE_CLOSURE_SET_WEIGHT, color);
}
compiler.add_node(
NODE_CLOSURE_VOLUME,
compiler.encode_uchar4(closure,
(param1) ? compiler.stack_assign(param1) : SVM_STACK_INVALID,
(param2) ? compiler.stack_assign(param2) : SVM_STACK_INVALID,
compiler.closure_mix_weight_offset()),
__float_as_int((param1) ? get_float(param1->socket_type) : 0.0f),
__float_as_int((param2) ? get_float(param2->socket_type) : 0.0f));
/* Density and mix weight need to be stored the same way for all volume closures since there's
* a shortcut code path if we only need the extinction value. */
uint density_ofs = (density) ? compiler.stack_assign_if_linked(density) : SVM_STACK_INVALID;
uint mix_weight_ofs = compiler.closure_mix_weight_offset();
if (param2 == nullptr) {
/* More efficient packing if we don't need the second parameter. */
uint param1_ofs = (param1) ? compiler.stack_assign_if_linked(param1) : SVM_STACK_INVALID;
compiler.add_node(NODE_CLOSURE_VOLUME,
compiler.encode_uchar4(closure, density_ofs, param1_ofs, mix_weight_ofs),
__float_as_int((density) ? get_float(density->socket_type) : 0.0f),
__float_as_int((param1) ? get_float(param1->socket_type) : 0.0f));
}
else {
uint param1_ofs = (param1) ? compiler.stack_assign(param1) : SVM_STACK_INVALID;
uint param2_ofs = (param2) ? compiler.stack_assign(param2) : SVM_STACK_INVALID;
compiler.add_node(NODE_CLOSURE_VOLUME,
compiler.encode_uchar4(closure, density_ofs, param1_ofs, mix_weight_ofs),
__float_as_int((density) ? get_float(density->socket_type) : 0.0f),
param2_ofs);
}
}
void VolumeNode::compile(SVMCompiler &compiler)
{
compile(compiler, NULL, NULL);
compile(compiler, nullptr, nullptr, nullptr);
}
void VolumeNode::compile(OSLCompiler & /*compiler*/)
@@ -3453,7 +3469,7 @@ AbsorptionVolumeNode::AbsorptionVolumeNode() : VolumeNode(get_node_type())
void AbsorptionVolumeNode::compile(SVMCompiler &compiler)
{
VolumeNode::compile(compiler, input("Density"), NULL);
VolumeNode::compile(compiler, input("Density"));
}
void AbsorptionVolumeNode::compile(OSLCompiler &compiler)
@@ -3470,6 +3486,19 @@ NODE_DEFINE(ScatterVolumeNode)
SOCKET_IN_COLOR(color, "Color", make_float3(0.8f, 0.8f, 0.8f));
SOCKET_IN_FLOAT(density, "Density", 1.0f);
SOCKET_IN_FLOAT(anisotropy, "Anisotropy", 0.0f);
SOCKET_IN_FLOAT(IOR, "IOR", 1.33f);
SOCKET_IN_FLOAT(backscatter, "Backscatter", 0.1f);
SOCKET_IN_FLOAT(alpha, "Alpha", 0.5f);
SOCKET_IN_FLOAT(diameter, "Diameter", 20.0f);
static NodeEnum phase_enum;
phase_enum.insert("Henyey-Greenstein", CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID);
phase_enum.insert("Fournier-Forand", CLOSURE_VOLUME_FOURNIER_FORAND_ID);
phase_enum.insert("Draine", CLOSURE_VOLUME_DRAINE_ID);
phase_enum.insert("Rayleigh", CLOSURE_VOLUME_RAYLEIGH_ID);
phase_enum.insert("Mie", CLOSURE_VOLUME_MIE_ID);
SOCKET_ENUM(phase, "Phase", phase_enum, CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID);
SOCKET_IN_FLOAT(volume_mix_weight, "VolumeMixWeight", 0.0f, SocketType::SVM_INTERNAL);
SOCKET_OUT_CLOSURE(volume, "Volume");
@@ -3484,11 +3513,33 @@ ScatterVolumeNode::ScatterVolumeNode() : VolumeNode(get_node_type())
void ScatterVolumeNode::compile(SVMCompiler &compiler)
{
VolumeNode::compile(compiler, input("Density"), input("Anisotropy"));
closure = phase;
switch (phase) {
case CLOSURE_VOLUME_HENYEY_GREENSTEIN_ID:
VolumeNode::compile(compiler, input("Density"), input("Anisotropy"));
break;
case CLOSURE_VOLUME_FOURNIER_FORAND_ID:
VolumeNode::compile(compiler, input("Density"), input("IOR"), input("Backscatter"));
break;
case CLOSURE_VOLUME_RAYLEIGH_ID:
VolumeNode::compile(compiler, input("Density"));
break;
case CLOSURE_VOLUME_DRAINE_ID:
VolumeNode::compile(compiler, input("Density"), input("Anisotropy"), input("Alpha"));
break;
case CLOSURE_VOLUME_MIE_ID:
VolumeNode::compile(compiler, input("Density"), input("Diameter"));
break;
default:
assert(false);
break;
}
}
void ScatterVolumeNode::compile(OSLCompiler &compiler)
{
compiler.parameter(this, "phase");
compiler.add(this, "node_scatter_volume");
}

View File

@@ -796,7 +796,10 @@ class VolumeNode : public ShaderNode {
VolumeNode(const NodeType *node_type);
SHADER_NODE_BASE_CLASS(VolumeNode)
void compile(SVMCompiler &compiler, ShaderInput *param1, ShaderInput *param2);
void compile(SVMCompiler &compiler,
ShaderInput *density,
ShaderInput *param1 = nullptr,
ShaderInput *param2 = nullptr);
virtual int get_feature()
{
return ShaderNode::get_feature() | KERNEL_FEATURE_NODE_VOLUME;
@@ -835,6 +838,11 @@ class ScatterVolumeNode : public VolumeNode {
SHADER_NODE_CLASS(ScatterVolumeNode)
NODE_SOCKET_API(float, anisotropy)
NODE_SOCKET_API(float, IOR)
NODE_SOCKET_API(float, backscatter)
NODE_SOCKET_API(float, alpha)
NODE_SOCKET_API(float, diameter)
NODE_SOCKET_API(ClosureType, phase)
};
class PrincipledVolumeNode : public VolumeNode {

View File

@@ -49,6 +49,9 @@ CCL_NAMESPACE_BEGIN
#ifndef M_1_2PI_F
# define M_1_2PI_F (0.1591549430918953f) /* 1/(2*pi) */
#endif
#ifndef M_1_4PI_F
# define M_1_4PI_F (0.0795774715459476f) /* 1/(4*pi) */
#endif
#ifndef M_SQRT_PI_8_F
# define M_SQRT_PI_8_F (0.6266570686577501f) /* sqrt(pi/8) */
#endif
@@ -71,6 +74,9 @@ CCL_NAMESPACE_BEGIN
#ifndef M_SQRT2_F
# define M_SQRT2_F (1.4142135623730950f) /* sqrt(2) */
#endif
#ifndef M_CBRT2_F
# define M_CBRT2_F 1.2599210498948732f /* cbrt(2) */
#endif
#ifndef M_SQRT1_2F
# define M_SQRT1_2F 0.70710678118654752440f /* sqrt(1/2) */
#endif

View File

@@ -634,6 +634,19 @@ ccl_device_inline float fast_ierff(float x)
return p * x;
}
/* Fast inverse cube root for positive x, with two Newton iterations to improve accuracy. */
ccl_device_inline float fast_inv_cbrtf(float x)
{
util_assert(x >= 0.0f);
/* Constant is roughly cbrt(2^127), but tweaked a bit to balance the error across the entire
* range. The exact value is not critical. */
float y = __int_as_float(0x54a24242 - __float_as_int(x) / 3);
y = (2.0f / 3) * y + 1 / (3 * y * y * x);
y = (2.0f / 3) * y + 1 / (3 * y * y * x);
return y;
}
CCL_NAMESPACE_END
#endif /* __UTIL_FAST_MATH__ */

View File

@@ -445,6 +445,11 @@ static void node_buts_output_shader(uiLayout *layout, bContext * /*C*/, PointerR
uiItemR(layout, ptr, "target", DEFAULT_FLAGS, "", ICON_NONE);
}
static void node_shader_buts_scatter(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
uiItemR(layout, ptr, "phase", DEFAULT_FLAGS, "", ICON_NONE);
}
/* only once called */
static void node_shader_set_butfunc(blender::bke::bNodeType *ntype)
{
@@ -501,6 +506,9 @@ static void node_shader_set_butfunc(blender::bke::bNodeType *ntype)
case SH_NODE_OUTPUT_WORLD:
ntype->draw_buttons = node_buts_output_shader;
break;
case SH_NODE_VOLUME_SCATTER:
ntype->draw_buttons = node_shader_buts_scatter;
break;
}
}

View File

@@ -2,8 +2,15 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
void node_volume_scatter(
vec4 color, float density, float anisotropy, float weight, out Closure result)
void node_volume_scatter(vec4 color,
float density,
float anisotropy,
float IOR,
float Backscatter,
float alpha,
float diameter,
float weight,
out Closure result)
{
color = max(color, vec4(0.0));
density = max(density, 0.0);

View File

@@ -2849,6 +2849,15 @@ enum {
SHD_POINTDENSITY_COLOR_VERTNOR = 2,
};
/* Scattering phase functions */
enum {
SHD_PHASE_HENYEY_GREENSTEIN = 0,
SHD_PHASE_FOURNIER_FORAND = 1,
SHD_PHASE_DRAINE = 2,
SHD_PHASE_RAYLEIGH = 3,
SHD_PHASE_MIE = 4,
};
/* Output shader node */
typedef enum NodeShaderOutputTarget {

View File

@@ -4476,6 +4476,37 @@ static const EnumPropertyItem prop_image_extension[] = {
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem node_scatter_phase_items[] = {
{SHD_PHASE_HENYEY_GREENSTEIN,
"HENYEY_GREENSTEIN",
0,
"Henyey-Greenstein",
"Henyey-Greenstein, default phase function for the scattering of light"},
{SHD_PHASE_FOURNIER_FORAND,
"FOURNIER_FORAND",
0,
"Fournier-Forand",
"Fournier-Forand phase function, used for the scattering of light in underwater "
"environments"},
{SHD_PHASE_DRAINE,
"DRAINE",
0,
"Draine",
"Draine phase functions, mostly used for the scattering of light in interstellar dust"},
{SHD_PHASE_RAYLEIGH,
"RAYLEIGH",
0,
"Rayleigh",
"Rayleigh phase function, mostly used for particles smaller than the wavelength of light, "
"such as scattering of sunlight in earth's atmosphere"},
{SHD_PHASE_MIE,
"MIE",
0,
"Mie",
"Approximation of Mie scattering in water droplets, used for scattering in clouds and fog"},
{0, nullptr, 0, nullptr, nullptr},
};
/* -- Common nodes ---------------------------------------------------------- */
static void def_group_input(StructRNA * /*srna*/) {}
@@ -5927,6 +5958,17 @@ static void def_refraction(StructRNA *srna)
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_scatter(StructRNA *srna)
{
PropertyRNA *prop;
prop = RNA_def_property(srna, "phase", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "custom1");
RNA_def_property_enum_items(prop, node_scatter_phase_items);
RNA_def_property_ui_text(prop, "Phase", "Phase function for the scattered light");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_toon(StructRNA *srna)
{
PropertyRNA *prop;

View File

@@ -76,7 +76,7 @@ DefNode(ShaderNode, SH_NODE_BSDF_HAIR, def_hair, "BSD
DefNode(ShaderNode, SH_NODE_BSDF_HAIR_PRINCIPLED, def_hair_principled, "BSDF_HAIR_PRINCIPLED", BsdfHairPrincipled, "Principled Hair BSDF", "Physically-based, easy-to-use shader for rendering hair and fur")
DefNode(ShaderNode, SH_NODE_SUBSURFACE_SCATTERING, def_sh_subsurface, "SUBSURFACE_SCATTERING",SubsurfaceScattering,"Subsurface Scattering","Subsurface multiple scattering shader to simulate light entering the surface and bouncing internally.\nTypically used for materials such as skin, wax, marble or milk")
DefNode(ShaderNode, SH_NODE_VOLUME_ABSORPTION, 0, "VOLUME_ABSORPTION", VolumeAbsorption, "Volume Absorption", "Absorb light as it passes through the volume")
DefNode(ShaderNode, SH_NODE_VOLUME_SCATTER, 0, "VOLUME_SCATTER", VolumeScatter, "Volume Scatter", "Scatter light as it passes through the volume, often used to add fog to a scene")
DefNode(ShaderNode, SH_NODE_VOLUME_SCATTER, def_scatter, "VOLUME_SCATTER", VolumeScatter, "Volume Scatter", "Scatter light as it passes through the volume, often used to add fog to a scene")
DefNode(ShaderNode, SH_NODE_VOLUME_PRINCIPLED, 0, "PRINCIPLED_VOLUME", VolumePrincipled, "Principled Volume", "Combine all volume shading components into a single easy to use node")
DefNode(ShaderNode, SH_NODE_EMISSION, 0, "EMISSION", Emission, "Emission", "Lambertian emission shader")
DefNode(ShaderNode, SH_NODE_NEW_GEOMETRY, 0, "NEW_GEOMETRY", NewGeometry, "Geometry", "Retrieve geometric information about the current shading point")

View File

@@ -4,6 +4,13 @@
#include "node_shader_util.hh"
#include "BLI_string.h"
#include "UI_interface.hh"
#include "UI_resources.hh"
#include "BKE_node_runtime.hh"
namespace blender::nodes::node_shader_volume_scatter_cc {
static void node_declare(NodeDeclarationBuilder &b)
@@ -16,16 +23,62 @@ static void node_declare(NodeDeclarationBuilder &b)
.default_value(0.0f)
.min(-1.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
.subtype(PROP_FACTOR)
.description(
"Directionality of the scattering. Zero is isotropic, negative is backward, "
"positive is forward");
b.add_input<decl::Float>("IOR")
.default_value(1.33f)
.min(1.0f)
.max(2.0f)
.subtype(PROP_FACTOR)
.description("Index Of Refraction of the scattering particles");
b.add_input<decl::Float>("Backscatter")
.default_value(0.1f)
.min(0.0f)
.max(0.5f)
.subtype(PROP_FACTOR)
.description("Fraction of light that is scattered backwards");
b.add_input<decl::Float>("Alpha").default_value(0.5f).min(0.0f).max(500.0f);
b.add_input<decl::Float>("Diameter")
.default_value(20.0f)
.min(5.0f)
.max(50.0f)
.description("Diameter of the water droplets, in micrometers");
b.add_input<decl::Float>("Weight").available(false);
b.add_output<decl::Shader>("Volume").translation_context(BLT_I18NCONTEXT_ID_ID);
}
#define socket_not_zero(sock) (in[sock].link || (clamp_f(in[sock].vec[0], 0.0f, 1.0f) > 1e-5f))
#define socket_not_black(sock) \
(in[sock].link || (clamp_f(in[sock].vec[0], 0.0f, 1.0f) > 1e-5f && \
clamp_f(in[sock].vec[1], 0.0f, 1.0f) > 1e-5f && \
clamp_f(in[sock].vec[2], 0.0f, 1.0f) > 1e-5f))
static void node_shader_buts_scatter(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
uiItemR(layout, ptr, "phase", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE);
}
static void node_shader_init_scatter(bNodeTree * /*ntree*/, bNode *node)
{
node->custom1 = SHD_PHASE_HENYEY_GREENSTEIN;
}
static void node_shader_update_scatter(bNodeTree *ntree, bNode *node)
{
const int phase_function = node->custom1;
LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) {
if (STR_ELEM(sock->name, "IOR", "Backscatter")) {
bke::node_set_socket_availability(ntree, sock, phase_function == SHD_PHASE_FOURNIER_FORAND);
}
else if (STR_ELEM(sock->name, "Anisotropy")) {
bke::node_set_socket_availability(
ntree, sock, ELEM(phase_function, SHD_PHASE_HENYEY_GREENSTEIN, SHD_PHASE_DRAINE));
}
else if (STR_ELEM(sock->name, "Alpha")) {
bke::node_set_socket_availability(ntree, sock, phase_function == SHD_PHASE_DRAINE);
}
else if (STR_ELEM(sock->name, "Diameter")) {
bke::node_set_socket_availability(ntree, sock, phase_function == SHD_PHASE_MIE);
}
}
}
static int node_shader_gpu_volume_scatter(GPUMaterial *mat,
bNode *node,
@@ -33,7 +86,7 @@ static int node_shader_gpu_volume_scatter(GPUMaterial *mat,
GPUNodeStack *in,
GPUNodeStack *out)
{
if (socket_not_zero(SOCK_DENSITY_ID) && socket_not_black(SOCK_COLOR_ID)) {
if (node_socket_not_zero(in[SOCK_DENSITY_ID]) && node_socket_not_black(in[SOCK_COLOR_ID])) {
/* Consider there is absorption phenomenon when there is scattering since
* `extinction = scattering + absorption`. */
GPU_material_flag_set(mat, GPU_MATFLAG_VOLUME_SCATTER | GPU_MATFLAG_VOLUME_ABSORPTION);
@@ -55,7 +108,12 @@ void register_node_type_sh_volume_scatter()
sh_node_type_base(&ntype, SH_NODE_VOLUME_SCATTER, "Volume Scatter", NODE_CLASS_SHADER);
ntype.declare = file_ns::node_declare;
ntype.add_ui_poll = object_shader_nodes_poll;
ntype.draw_buttons = file_ns::node_shader_buts_scatter;
blender::bke::node_type_size_preset(&ntype, blender::bke::eNodeSizePreset::Middle);
ntype.initfunc = file_ns::node_shader_init_scatter;
ntype.gpu_fn = file_ns::node_shader_gpu_volume_scatter;
ntype.updatefunc = file_ns::node_shader_update_scatter;
blender::bke::node_register_type(&ntype);
}