Files
test/intern/cycles/kernel/integrator/subsurface.h
Lukas Stockner d7aee5a580 Cycles: Tweak Principled BSDF Subsurface parameters
Previously, the Principled BSDF used the Subsurface input to scale the radius.
When it was zero, it used a diffuse closure, otherwise a subsurface closure.
This sort of scaling input makes sense, but it should be specified in distance
units, rather than a 0..1 factor, so this commit changes the unit and renames
the input to Subsurface Scale.

Additionally, it adds support for mixing diffuse and subsurface components.
This is part of e.g. the OpenPBR spec, and the logic behind it is to support
modeling e.g. dirt or paint on top of skin. Before, materials would be either
fully diffuse (radius=0) or fully subsurface.

For typical materials, this mixing factor will be either zero or one
(just like metallic or transmission), but supporting fractional inputs makes
sense for e.g. smooth transitions at boundaries.

Another change is that there is no separate Subsurface Color anymore - before,
this was mixed with the Base Color using the Subsurface input as the factor,
but this was not really useful since that input was generally very small.

And finally, the handling of how the path enters the material for random walk
subsurface scattering is changed. Before, this always used lambertian (diffuse)
transmission, but this caused some problems, like overly white edges.

Instead, two different methods are now used, depending on the selected mode.
In Fixed Radius mode, the code assumes a simple medium boundary, and performs
refraction into the material using the main Roughness and IOR inputs.

Meanwhile, when not using Fixed Radius, the code assumes a more complex
boundary (as typically found on organic materials, e.g. skin), so the entry
bounce has a 50/50 chance of being either diffuse transmission or refraction
using the separate Subsurface IOR input and a fixed roughness of 1.
Credit for this method goes to Christophe Hery.

Pull Request: https://projects.blender.org/blender/blender/pulls/110989
2023-09-13 02:45:33 +02:00

257 lines
9.1 KiB
C

/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/camera/projection.h"
#include "kernel/bvh/bvh.h"
#include "kernel/closure/alloc.h"
#include "kernel/closure/bsdf_diffuse.h"
#include "kernel/closure/bssrdf.h"
#include "kernel/closure/volume.h"
#include "kernel/integrator/intersect_volume_stack.h"
#include "kernel/integrator/path_state.h"
#include "kernel/integrator/subsurface_disk.h"
#include "kernel/integrator/subsurface_random_walk.h"
#include "kernel/integrator/surface_shader.h"
CCL_NAMESPACE_BEGIN
#ifdef __SUBSURFACE__
ccl_device_inline bool subsurface_entry_bounce(KernelGlobals kg,
ccl_private const Bssrdf *bssrdf,
ccl_private ShaderData *sd,
ccl_private RNGState *rng_state,
ccl_private float3 *wo)
{
float2 rand_bsdf = path_state_rng_2D(kg, rng_state, PRNG_SUBSURFACE_BSDF);
if (bssrdf->type == CLOSURE_BSSRDF_RANDOM_WALK_ID) {
/* CLOSURE_BSSRDF_RANDOM_WALK_ID has a 50% chance to sample a diffuse entry bounce.
* Also, for the refractive entry, it uses a fixed roughness of 1.0. */
if (rand_bsdf.x < 0.5f) {
rand_bsdf.x *= 2.0f;
float pdf;
sample_cos_hemisphere(-bssrdf->N, rand_bsdf, wo, &pdf);
return true;
}
rand_bsdf.x = 2.0f * (rand_bsdf.x - 0.5f);
}
const float cos_NI = dot(bssrdf->N, sd->wi);
if (cos_NI <= 0.0f) {
return false;
}
float3 X, Y, Z = bssrdf->N;
make_orthonormals(Z, &X, &Y);
const float alpha = bssrdf->alpha;
const float neta = 1.0f / bssrdf->ior;
/* Sample microfacet normal by transforming to/from local coordinates. */
const float3 local_I = make_float3(dot(X, sd->wi), dot(Y, sd->wi), cos_NI);
const float3 local_H = microfacet_ggx_sample_vndf(local_I, alpha, alpha, rand_bsdf);
const float3 H = X * local_H.x + Y * local_H.y + Z * local_H.z;
const float cos_HI = dot(H, sd->wi);
const float arg = 1.0f - (sqr(neta) * (1.0f - sqr(cos_HI)));
/* We clamp subsurface IOR to be above 1, so there should never be TIR. */
kernel_assert(arg >= 0.0f);
const float dnp = max(sqrtf(arg), 1e-7f);
const float nK = (neta * cos_HI) - dnp;
*wo = -(neta * sd->wi) + (nK * H);
return true;
/* Note: For a proper refractive GGX interface, we should be computing lambdaI and lambdaO
* and multiplying the throughput by BSDF/pdf, which for VNDF sampling works out to
* (1 + lambdaI) / (1 + lambdaI + lambdaO).
* However, this causes darkening due to the single-scattering approximation, which we'd
* then have to correct with a lookup table.
* Since we only really care about the directional distribution here, it's much easier to
* just skip all that instead. */
}
ccl_device int subsurface_bounce(KernelGlobals kg,
IntegratorState state,
ccl_private ShaderData *sd,
ccl_private const ShaderClosure *sc)
{
/* We should never have two consecutive BSSRDF bounces, the second one should
* be converted to a diffuse BSDF to avoid this. */
kernel_assert(!(INTEGRATOR_STATE(state, path, flag) & PATH_RAY_DIFFUSE_ANCESTOR));
/* Setup path state for intersect_subsurface kernel. */
ccl_private const Bssrdf *bssrdf = (ccl_private const Bssrdf *)sc;
/* Setup ray into surface. */
INTEGRATOR_STATE_WRITE(state, ray, P) = sd->P;
INTEGRATOR_STATE_WRITE(state, ray, tmin) = 0.0f;
INTEGRATOR_STATE_WRITE(state, ray, tmax) = FLT_MAX;
INTEGRATOR_STATE_WRITE(state, ray, dP) = differential_make_compact(sd->dP);
INTEGRATOR_STATE_WRITE(state, ray, dD) = differential_zero_compact();
/* Advance random number offset for bounce. */
INTEGRATOR_STATE_WRITE(state, path, rng_offset) += PRNG_BOUNCE_NUM;
/* Compute weight, optionally including Fresnel from entry point. */
Spectrum weight = surface_shader_bssrdf_sample_weight(sd, sc);
INTEGRATOR_STATE_WRITE(state, path, throughput) *= weight;
uint32_t path_flag = (INTEGRATOR_STATE(state, path, flag) & ~PATH_RAY_CAMERA);
if (sc->type == CLOSURE_BSSRDF_BURLEY_ID) {
path_flag |= PATH_RAY_SUBSURFACE_DISK;
INTEGRATOR_STATE_WRITE(state, subsurface, N) = sd->Ng;
}
else {
path_flag |= PATH_RAY_SUBSURFACE_RANDOM_WALK;
/* Sample entry bounce into the material. */
RNGState rng_state;
path_state_rng_load(state, &rng_state);
float3 wo;
if (!subsurface_entry_bounce(kg, bssrdf, sd, &rng_state, &wo) || dot(sd->Ng, wo) >= 0.0f) {
/* Sampling failed, give up on this bounce. */
return LABEL_NONE;
}
INTEGRATOR_STATE_WRITE(state, ray, D) = wo;
INTEGRATOR_STATE_WRITE(state, subsurface, N) = sd->N;
}
if (sd->flag & SD_BACKFACING) {
path_flag |= PATH_RAY_SUBSURFACE_BACKFACING;
}
INTEGRATOR_STATE_WRITE(state, path, flag) = path_flag;
if (kernel_data.kernel_features & KERNEL_FEATURE_LIGHT_PASSES) {
if (INTEGRATOR_STATE(state, path, bounce) == 0) {
INTEGRATOR_STATE_WRITE(state, path, pass_diffuse_weight) = one_spectrum();
INTEGRATOR_STATE_WRITE(state, path, pass_glossy_weight) = zero_spectrum();
}
}
/* Pass BSSRDF parameters. */
INTEGRATOR_STATE_WRITE(state, subsurface, albedo) = bssrdf->albedo;
INTEGRATOR_STATE_WRITE(state, subsurface, radius) = bssrdf->radius;
INTEGRATOR_STATE_WRITE(state, subsurface, anisotropy) = bssrdf->anisotropy;
/* Path guiding. */
guiding_record_bssrdf_weight(kg, state, weight, bssrdf->albedo);
return LABEL_SUBSURFACE_SCATTER;
}
ccl_device void subsurface_shader_data_setup(KernelGlobals kg,
IntegratorState state,
ccl_private ShaderData *sd,
const uint32_t path_flag)
{
/* Get bump mapped normal from shader evaluation at exit point. */
float3 N = sd->N;
if (sd->flag & SD_HAS_BSSRDF_BUMP) {
N = surface_shader_bssrdf_normal(sd);
}
/* Setup diffuse BSDF at the exit point. This replaces shader_eval_surface. */
sd->flag &= ~SD_CLOSURE_FLAGS;
sd->num_closure = 0;
sd->num_closure_left = kernel_data.max_closures;
const Spectrum weight = one_spectrum();
ccl_private DiffuseBsdf *bsdf = (ccl_private DiffuseBsdf *)bsdf_alloc(
sd, sizeof(DiffuseBsdf), weight);
if (bsdf) {
bsdf->N = N;
sd->flag |= bsdf_diffuse_setup(bsdf);
}
}
ccl_device_inline bool subsurface_scatter(KernelGlobals kg, IntegratorState state)
{
RNGState rng_state;
path_state_rng_load(state, &rng_state);
Ray ray ccl_optional_struct_init;
LocalIntersection ss_isect ccl_optional_struct_init;
if (INTEGRATOR_STATE(state, path, flag) & PATH_RAY_SUBSURFACE_RANDOM_WALK) {
if (!subsurface_random_walk(kg, state, rng_state, ray, ss_isect)) {
return false;
}
}
else {
if (!subsurface_disk(kg, state, rng_state, ray, ss_isect)) {
return false;
}
}
# ifdef __VOLUME__
/* Update volume stack if needed. */
if (kernel_data.integrator.use_volumes) {
const int object = ss_isect.hits[0].object;
const int object_flag = kernel_data_fetch(object_flag, object);
if (object_flag & SD_OBJECT_INTERSECTS_VOLUME) {
float3 P = INTEGRATOR_STATE(state, ray, P);
integrator_volume_stack_update_for_subsurface(kg, state, P, ray.P);
}
}
# endif /* __VOLUME__ */
/* Pretend ray is coming from the outside towards the exit point. This ensures
* correct front/back facing normals.
* TODO: find a more elegant solution? */
ray.P += ray.D * ray.tmax * 2.0f;
ray.D = -ray.D;
integrator_state_write_isect(state, &ss_isect.hits[0]);
integrator_state_write_ray(state, &ray);
/* Advance random number offset for bounce. */
INTEGRATOR_STATE_WRITE(state, path, rng_offset) += PRNG_BOUNCE_NUM;
const int shader = intersection_get_shader(kg, &ss_isect.hits[0]);
const int shader_flags = kernel_data_fetch(shaders, shader).flags;
const int object_flags = intersection_get_object_flags(kg, &ss_isect.hits[0]);
const bool use_caustics = kernel_data.integrator.use_caustics &&
(object_flags & SD_OBJECT_CAUSTICS);
const bool use_raytrace_kernel = (shader_flags & SD_HAS_RAYTRACE);
if (use_caustics) {
integrator_path_next_sorted(kg,
state,
DEVICE_KERNEL_INTEGRATOR_INTERSECT_SUBSURFACE,
DEVICE_KERNEL_INTEGRATOR_SHADE_SURFACE_MNEE,
shader);
}
else if (use_raytrace_kernel) {
integrator_path_next_sorted(kg,
state,
DEVICE_KERNEL_INTEGRATOR_INTERSECT_SUBSURFACE,
DEVICE_KERNEL_INTEGRATOR_SHADE_SURFACE_RAYTRACE,
shader);
}
else {
integrator_path_next_sorted(kg,
state,
DEVICE_KERNEL_INTEGRATOR_INTERSECT_SUBSURFACE,
DEVICE_KERNEL_INTEGRATOR_SHADE_SURFACE,
shader);
}
return true;
}
#endif /* __SUBSURFACE__ */
CCL_NAMESPACE_END