Files
test/intern/cycles/kernel/light/sample.h
Weizhen Huang a4d792a3ad Cycles/EEVEE: change point light to double-sided sphere light
for energy preservation and better compatibility with other renderes. Ref: #108505

Point light now behaves the same as a spherical mesh light with the same overall energy (scaling from emission strength to power is \(4\pi^2R^2\)).
# Cycles
## Comparison
| Mesh Light | This patch | Previous behavior |
| -------- | -------- | -------- |
| ![mesh_1024](attachments/2900954c-57f8-49c2-b6f3-8fb559b820ac)     | ![sphere_1024](attachments/148241ca-9350-48b6-be04-3933e015424c)     | ![point_1024](attachments/d9b19d54-2b00-4986-ba8c-c4b28f687f09)  |

The behavior stays the same when `radius = 0`.

| This patch | Previous behavior |
| -------- | -------- |
| ![sphere_64](attachments/aa05d59a-146a-4f69-b257-5d09a7f41d4e)     | ![point_64](attachments/69a743be-bc15-454b-92d8-af02f4e8ab07)    |

No obvious performance change observed.

## Sampling
When shading point lies outside the sphere, sample the spanned solid angle uniformly.
When shading point lies inside the sphere, sample spherical direction uniformly when inside volume or the surface is transmissive, otherwise sample cosine-weighted upper hemisphere.
## Light Tree
When shading point lies outside the sphere, treat as a disk light spanning the same solid angle.
When shading point lies inside the sphere, it behaves like a background light, with estimated outgoing radiance
\[L_o=\int f_aL_i\cos\theta_i\mathrm{d}\omega_i=\int f_a\frac{E}{\pi r^2}\cos\theta_i\mathrm{d}\omega_i\approx f_a \frac{E}{r^2}\],
with \(f_a\) being the BSDF and \(E\) `measure.energy` in `light_tree.cpp`.
The importance calculation for `LIGHT_POINT` is
\[L_o=f_a E\cos\theta_i\frac{\cos\theta}{d^2}\].
Consider `min_importance = 0` because maximal incidence angle is \(\pi\), we could substitute \(d^2\) with \(\frac{r^2}{2}\) so the averaged outgoing radiance is \(f_a \frac{E}{r^2}\).
This only holds for non-transmissive surface, but should be fine to use in volume.
# EEVEE
When shading point lies outside the sphere, the sphere light is equivalent to a disk light spanning the same solid angle. The sine of the new half-angle is the tangent of the previous half-angle.
When shading point lies inside the sphere, integrating over the cosine-weighted hemisphere gives 1.0.
## Comparison with Cycles
The plane is diffuse, the blue sphere has specular component.
| Before | |After ||
|---|--|--|--|
|Cycles|EEVEE|Cycles|EEVEE|
|![](attachments/5824c494-0645-461a-b193-d74e02f353b8)|![](attachments/d2e85b53-3c2a-4a9f-a3b2-6e11c6083ce0)|![](attachments/a8dcdd8b-c13c-4fdc-808c-2563624549be)|![](attachments/8c3618ef-1ab4-4210-9535-c85e873f1e45)|

Pull Request: https://projects.blender.org/blender/blender/pulls/108506
2023-06-20 12:23:05 +02:00

502 lines
18 KiB
C

/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/integrator/path_state.h"
#include "kernel/integrator/surface_shader.h"
#include "kernel/light/distribution.h"
#include "kernel/light/light.h"
#ifdef __LIGHT_TREE__
# include "kernel/light/tree.h"
#endif
#include "kernel/sample/mapping.h"
#include "kernel/sample/mis.h"
CCL_NAMESPACE_BEGIN
/* Evaluate shader on light. */
ccl_device_noinline_cpu Spectrum
light_sample_shader_eval(KernelGlobals kg,
IntegratorState state,
ccl_private ShaderData *ccl_restrict emission_sd,
ccl_private LightSample *ccl_restrict ls,
float time)
{
/* setup shading at emitter */
Spectrum eval = zero_spectrum();
if (surface_shader_constant_emission(kg, ls->shader, &eval)) {
if ((ls->prim != PRIM_NONE) && dot(ls->Ng, ls->D) > 0.0f) {
ls->Ng = -ls->Ng;
}
}
else {
/* Setup shader data and call surface_shader_eval once, better
* for GPU coherence and compile times. */
PROFILING_INIT_FOR_SHADER(kg, PROFILING_SHADE_LIGHT_SETUP);
if (ls->type == LIGHT_BACKGROUND) {
shader_setup_from_background(kg, emission_sd, ls->P, ls->D, time);
}
else {
shader_setup_from_sample(kg,
emission_sd,
ls->P,
ls->Ng,
-ls->D,
ls->shader,
ls->object,
ls->prim,
ls->u,
ls->v,
ls->t,
time,
false,
ls->lamp);
ls->Ng = emission_sd->Ng;
}
PROFILING_SHADER(emission_sd->object, emission_sd->shader);
PROFILING_EVENT(PROFILING_SHADE_LIGHT_EVAL);
/* No proper path flag, we're evaluating this for all closures. that's
* weak but we'd have to do multiple evaluations otherwise. */
surface_shader_eval<KERNEL_FEATURE_NODE_MASK_SURFACE_LIGHT>(
kg, state, emission_sd, NULL, PATH_RAY_EMISSION);
/* Evaluate closures. */
if (ls->type == LIGHT_BACKGROUND) {
eval = surface_shader_background(emission_sd);
}
else {
eval = surface_shader_emission(emission_sd);
}
}
eval *= ls->eval_fac;
if (ls->lamp != LAMP_NONE) {
ccl_global const KernelLight *klight = &kernel_data_fetch(lights, ls->lamp);
eval *= rgb_to_spectrum(
make_float3(klight->strength[0], klight->strength[1], klight->strength[2]));
}
return eval;
}
/* Early path termination of shadow rays. */
ccl_device_inline bool light_sample_terminate(KernelGlobals kg,
ccl_private const LightSample *ccl_restrict ls,
ccl_private BsdfEval *ccl_restrict eval,
const float rand_terminate)
{
if (bsdf_eval_is_zero(eval)) {
return true;
}
if (kernel_data.integrator.light_inv_rr_threshold > 0.0f) {
float probability = reduce_max(fabs(bsdf_eval_sum(eval))) *
kernel_data.integrator.light_inv_rr_threshold;
if (probability < 1.0f) {
if (rand_terminate >= probability) {
return true;
}
bsdf_eval_mul(eval, 1.0f / probability);
}
}
return false;
}
/* This function should be used to compute a modified ray start position for
* rays leaving from a surface. The algorithm slightly distorts flat surface
* of a triangle. Surface is lifted by amount h along normal n in the incident
* point. */
ccl_device_inline float3 shadow_ray_smooth_surface_offset(
KernelGlobals kg, ccl_private const ShaderData *ccl_restrict sd, float3 Ng)
{
float3 V[3], N[3];
if (sd->type == PRIMITIVE_MOTION_TRIANGLE) {
motion_triangle_vertices_and_normals(kg, sd->object, sd->prim, sd->time, V, N);
}
else {
kernel_assert(sd->type == PRIMITIVE_TRIANGLE);
triangle_vertices_and_normals(kg, sd->prim, V, N);
}
const float u = 1.0f - sd->u - sd->v;
const float v = sd->u;
const float w = sd->v;
float3 P = V[0] * u + V[1] * v + V[2] * w; /* Local space */
float3 n = N[0] * u + N[1] * v + N[2] * w; /* We get away without normalization */
if (!(sd->object_flag & SD_OBJECT_TRANSFORM_APPLIED)) {
object_dir_transform(kg, sd, &n); /* Normal x scale, to world space */
}
/* Parabolic approximation */
float a = dot(N[2] - N[0], V[0] - V[2]);
float b = dot(N[2] - N[1], V[1] - V[2]);
float c = dot(N[1] - N[0], V[1] - V[0]);
float h = a * u * (u - 1) + (a + b + c) * u * v + b * v * (v - 1);
/* Check flipped normals */
if (dot(n, Ng) > 0) {
/* Local linear envelope */
float h0 = max(max(dot(V[1] - V[0], N[0]), dot(V[2] - V[0], N[0])), 0.0f);
float h1 = max(max(dot(V[0] - V[1], N[1]), dot(V[2] - V[1], N[1])), 0.0f);
float h2 = max(max(dot(V[0] - V[2], N[2]), dot(V[1] - V[2], N[2])), 0.0f);
h0 = max(dot(V[0] - P, N[0]) + h0, 0.0f);
h1 = max(dot(V[1] - P, N[1]) + h1, 0.0f);
h2 = max(dot(V[2] - P, N[2]) + h2, 0.0f);
h = max(min(min(h0, h1), h2), h * 0.5f);
}
else {
float h0 = max(max(dot(V[0] - V[1], N[0]), dot(V[0] - V[2], N[0])), 0.0f);
float h1 = max(max(dot(V[1] - V[0], N[1]), dot(V[1] - V[2], N[1])), 0.0f);
float h2 = max(max(dot(V[2] - V[0], N[2]), dot(V[2] - V[1], N[2])), 0.0f);
h0 = max(dot(P - V[0], N[0]) + h0, 0.0f);
h1 = max(dot(P - V[1], N[1]) + h1, 0.0f);
h2 = max(dot(P - V[2], N[2]) + h2, 0.0f);
h = min(-min(min(h0, h1), h2), h * 0.5f);
}
return n * h;
}
/* Ray offset to avoid shadow terminator artifact. */
ccl_device_inline float3 shadow_ray_offset(KernelGlobals kg,
ccl_private const ShaderData *ccl_restrict sd,
float3 L,
ccl_private bool *r_skip_self)
{
float3 P = sd->P;
if ((sd->type & PRIMITIVE_TRIANGLE) && (sd->shader & SHADER_SMOOTH_NORMAL)) {
const float offset_cutoff =
kernel_data_fetch(objects, sd->object).shadow_terminator_geometry_offset;
/* Do ray offset (heavy stuff) only for close to be terminated triangles:
* offset_cutoff = 0.1f means that 10-20% of rays will be affected. Also
* make a smooth transition near the threshold. */
if (offset_cutoff > 0.0f) {
float NL = dot(sd->N, L);
const bool transmit = (NL < 0.0f);
if (NL < 0) {
NL = -NL;
}
const float3 Ng = (transmit ? -sd->Ng : sd->Ng);
const float NgL = dot(Ng, L);
const float offset_amount = (NL < offset_cutoff) ?
clamp(2.0f - (NgL + NL) / offset_cutoff, 0.0f, 1.0f) :
clamp(1.0f - NgL / offset_cutoff, 0.0f, 1.0f);
if (offset_amount > 0.0f) {
P += shadow_ray_smooth_surface_offset(kg, sd, Ng) * offset_amount;
/* Only skip self intersections if light direction and geometric normal point in the same
* direction, otherwise we're meant to hit this surface. */
*r_skip_self = (NgL > 0.0f);
}
}
}
return P;
}
ccl_device_inline void shadow_ray_setup(ccl_private const ShaderData *ccl_restrict sd,
ccl_private const LightSample *ccl_restrict ls,
const float3 P,
ccl_private Ray *ray,
const bool skip_self)
{
if (ls->shader & SHADER_CAST_SHADOW) {
/* setup ray */
ray->P = P;
ray->tmin = 0.0f;
if (ls->t == FLT_MAX) {
/* distant light */
ray->D = ls->D;
ray->tmax = ls->t;
}
else {
/* other lights, avoid self-intersection */
ray->D = ls->P - P;
ray->D = normalize_len(ray->D, &ray->tmax);
}
}
else {
/* signal to not cast shadow ray */
ray->P = zero_float3();
ray->D = zero_float3();
ray->tmax = 0.0f;
}
ray->dP = differential_make_compact(sd->dP);
ray->dD = differential_zero_compact();
ray->time = sd->time;
/* Fill in intersection surface and light details. */
ray->self.object = (skip_self) ? sd->object : OBJECT_NONE;
ray->self.prim = (skip_self) ? sd->prim : PRIM_NONE;
ray->self.light_object = ls->object;
ray->self.light_prim = ls->prim;
ray->self.light = ls->lamp;
}
/* Create shadow ray towards light sample. */
ccl_device_inline void light_sample_to_surface_shadow_ray(
KernelGlobals kg,
ccl_private const ShaderData *ccl_restrict sd,
ccl_private const LightSample *ccl_restrict ls,
ccl_private Ray *ray)
{
bool skip_self = true;
const float3 P = shadow_ray_offset(kg, sd, ls->D, &skip_self);
shadow_ray_setup(sd, ls, P, ray, skip_self);
}
/* Create shadow ray towards light sample. */
ccl_device_inline void light_sample_to_volume_shadow_ray(
KernelGlobals kg,
ccl_private const ShaderData *ccl_restrict sd,
ccl_private const LightSample *ccl_restrict ls,
const float3 P,
ccl_private Ray *ray)
{
shadow_ray_setup(sd, ls, P, ray, false);
}
/* Multiple importance sampling weights. */
ccl_device_inline float light_sample_mis_weight_forward(KernelGlobals kg,
const float forward_pdf,
const float nee_pdf)
{
#ifdef WITH_CYCLES_DEBUG
if (kernel_data.integrator.direct_light_sampling_type == DIRECT_LIGHT_SAMPLING_FORWARD) {
return 1.0f;
}
else if (kernel_data.integrator.direct_light_sampling_type == DIRECT_LIGHT_SAMPLING_NEE) {
return 0.0f;
}
else
#endif
return power_heuristic(forward_pdf, nee_pdf);
}
ccl_device_inline float light_sample_mis_weight_nee(KernelGlobals kg,
const float nee_pdf,
const float forward_pdf)
{
#ifdef WITH_CYCLES_DEBUG
if (kernel_data.integrator.direct_light_sampling_type == DIRECT_LIGHT_SAMPLING_FORWARD) {
return 0.0f;
}
else if (kernel_data.integrator.direct_light_sampling_type == DIRECT_LIGHT_SAMPLING_NEE) {
return 1.0f;
}
else
#endif
return power_heuristic(nee_pdf, forward_pdf);
}
/* Next event estimation sampling.
*
* Sample a position on a light in the scene, from a position on a surface or
* from a volume segment.
*
* Uses either a flat distribution or light tree. */
ccl_device_inline bool light_sample_from_volume_segment(KernelGlobals kg,
const float3 rand,
const float time,
const float3 P,
const float3 D,
const float t,
const int object_receiver,
const int bounce,
const uint32_t path_flag,
ccl_private LightSample *ls)
{
#ifdef __LIGHT_TREE__
if (kernel_data.integrator.use_light_tree) {
return light_tree_sample<true>(
kg, rand, time, P, D, t, object_receiver, SD_BSDF_HAS_TRANSMISSION, bounce, path_flag, ls);
}
else
#endif
{
return light_distribution_sample<true>(
kg, rand, time, P, D, object_receiver, SD_BSDF_HAS_TRANSMISSION, bounce, path_flag, ls);
}
}
ccl_device bool light_sample_from_position(KernelGlobals kg,
ccl_private const RNGState *rng_state,
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)
{
#ifdef __LIGHT_TREE__
if (kernel_data.integrator.use_light_tree) {
return light_tree_sample<false>(
kg, rand, time, P, N, 0.0f, object_receiver, shader_flags, bounce, path_flag, ls);
}
else
#endif
{
return light_distribution_sample<false>(
kg, rand, time, P, N, object_receiver, shader_flags, bounce, path_flag, ls);
}
}
/* Update light sample with new shading point position for MNEE. The position on the light is fixed
* except for directional light. */
ccl_device_forceinline void light_sample_update(KernelGlobals kg,
ccl_private LightSample *ls,
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, N, path_flag);
}
else if (ls->type == LIGHT_SPOT) {
spot_light_mnee_sample_update(klight, ls, P);
}
else if (ls->type == LIGHT_AREA) {
area_light_mnee_sample_update(klight, ls, P);
}
else {
/* Keep previous values. */
}
/* Re-apply already computed selection pdf. */
ls->pdf *= ls->pdf_selection;
}
/* Forward sampling.
*
* Multiple importance sampling weights for hitting surface, light or background
* through indirect light ray.
*
* The BSDF or phase pdf from the previous bounce was stored in mis_ray_pdf and
* is used for balancing with the light sampling pdf. */
ccl_device_inline float light_sample_mis_weight_forward_surface(KernelGlobals kg,
IntegratorState state,
const uint32_t path_flag,
const ccl_private ShaderData *sd)
{
const float bsdf_pdf = INTEGRATOR_STATE(state, path, mis_ray_pdf);
const float t = sd->ray_length;
float pdf = triangle_light_pdf(kg, sd, t);
/* Light selection pdf. */
#ifdef __LIGHT_TREE__
if (kernel_data.integrator.use_light_tree) {
float3 ray_P = INTEGRATOR_STATE(state, ray, P);
const float3 N = INTEGRATOR_STATE(state, path, mis_origin_n);
uint lookup_offset = kernel_data_fetch(object_lookup_offset, sd->object);
uint prim_offset = kernel_data_fetch(object_prim_offset, sd->object);
uint triangle = kernel_data_fetch(triangle_to_tree, sd->prim - prim_offset + lookup_offset);
pdf *= light_tree_pdf(
kg, ray_P, N, path_flag, sd->object, triangle, light_link_receiver_forward(kg, state));
}
else
#endif
{
/* Handled in triangle_light_pdf for efficiency. */
}
return light_sample_mis_weight_forward(kg, bsdf_pdf, pdf);
}
ccl_device_inline float light_sample_mis_weight_forward_lamp(KernelGlobals kg,
IntegratorState state,
const uint32_t path_flag,
const ccl_private LightSample *ls,
const float3 P)
{
const float mis_ray_pdf = INTEGRATOR_STATE(state, path, mis_ray_pdf);
float pdf = ls->pdf;
/* Light selection pdf. */
#ifdef __LIGHT_TREE__
if (kernel_data.integrator.use_light_tree) {
const float3 N = INTEGRATOR_STATE(state, path, mis_origin_n);
pdf *= light_tree_pdf(kg,
P,
N,
path_flag,
0,
kernel_data_fetch(light_to_tree, ls->lamp),
light_link_receiver_forward(kg, state));
}
else
#endif
{
pdf *= light_distribution_pdf_lamp(kg);
}
return light_sample_mis_weight_forward(kg, mis_ray_pdf, pdf);
}
ccl_device_inline float light_sample_mis_weight_forward_distant(KernelGlobals kg,
IntegratorState state,
const uint32_t path_flag,
const ccl_private LightSample *ls)
{
const float3 ray_P = INTEGRATOR_STATE(state, ray, P);
return light_sample_mis_weight_forward_lamp(kg, state, path_flag, ls, ray_P);
}
ccl_device_inline float light_sample_mis_weight_forward_background(KernelGlobals kg,
IntegratorState state,
const uint32_t path_flag)
{
const float3 ray_P = INTEGRATOR_STATE(state, ray, P);
const float3 ray_D = INTEGRATOR_STATE(state, ray, D);
const float mis_ray_pdf = INTEGRATOR_STATE(state, path, mis_ray_pdf);
float pdf = background_light_pdf(kg, ray_P, ray_D);
/* Light selection pdf. */
#ifdef __LIGHT_TREE__
if (kernel_data.integrator.use_light_tree) {
const float3 N = INTEGRATOR_STATE(state, path, mis_origin_n);
uint light = kernel_data_fetch(light_to_tree, kernel_data.background.light_index);
pdf *= light_tree_pdf(
kg, ray_P, N, path_flag, 0, light, light_link_receiver_forward(kg, state));
}
else
#endif
{
pdf *= light_distribution_pdf_lamp(kg);
}
return light_sample_mis_weight_forward(kg, mis_ray_pdf, pdf);
}
CCL_NAMESPACE_END