Files
test/intern/cycles/kernel/integrator/shade_light.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

103 lines
3.5 KiB
C

/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/film/light_passes.h"
#include "kernel/integrator/surface_shader.h"
#include "kernel/light/light.h"
#include "kernel/light/sample.h"
CCL_NAMESPACE_BEGIN
ccl_device_inline void integrate_light(KernelGlobals kg,
IntegratorState state,
ccl_global float *ccl_restrict render_buffer)
{
/* Setup light sample. */
Intersection isect ccl_optional_struct_init;
integrator_state_read_isect(state, &isect);
guiding_record_light_surface_segment(kg, state, &isect);
float3 ray_P = INTEGRATOR_STATE(state, ray, P);
const float3 ray_D = INTEGRATOR_STATE(state, ray, D);
const float ray_time = INTEGRATOR_STATE(state, ray, time);
const uint32_t path_flag = INTEGRATOR_STATE(state, path, flag);
const float3 N = INTEGRATOR_STATE(state, path, mis_origin_n);
/* Advance ray to new start distance. */
INTEGRATOR_STATE_WRITE(state, ray, tmin) = intersection_t_offset(isect.t);
LightSample ls ccl_optional_struct_init;
const bool use_light_sample = light_sample_from_intersection(
kg, &isect, ray_P, ray_D, N, path_flag, &ls);
if (!use_light_sample) {
return;
}
/* Use visibility flag to skip lights. */
#ifdef __PASSES__
if (!is_light_shader_visible_to_path(ls.shader, path_flag)) {
return;
}
#endif
/* Evaluate light shader. */
/* TODO: does aliasing like this break automatic SoA in CUDA? */
ShaderDataTinyStorage emission_sd_storage;
ccl_private ShaderData *emission_sd = AS_SHADER_DATA(&emission_sd_storage);
Spectrum light_eval = light_sample_shader_eval(kg, state, emission_sd, &ls, ray_time);
if (is_zero(light_eval)) {
return;
}
/* MIS weighting. */
float mis_weight = 1.0f;
if (!(path_flag & PATH_RAY_MIS_SKIP)) {
mis_weight = light_sample_mis_weight_forward_lamp(kg, state, path_flag, &ls, ray_P);
}
/* Write to render buffer. */
guiding_record_surface_emission(kg, state, light_eval, mis_weight);
film_write_surface_emission(kg, state, light_eval, mis_weight, render_buffer, ls.group);
}
ccl_device void integrator_shade_light(KernelGlobals kg,
IntegratorState state,
ccl_global float *ccl_restrict render_buffer)
{
PROFILING_INIT(kg, PROFILING_SHADE_LIGHT_SETUP);
integrate_light(kg, state, render_buffer);
/* TODO: we could get stuck in an infinite loop if there are precision issues
* and the same light is hit again.
*
* As a workaround count this as a transparent bounce. It makes some sense
* to interpret lights as transparent surfaces (and support making them opaque),
* but this needs to be revisited. */
uint32_t transparent_bounce = INTEGRATOR_STATE(state, path, transparent_bounce) + 1;
INTEGRATOR_STATE_WRITE(state, path, transparent_bounce) = transparent_bounce;
if (transparent_bounce >= kernel_data.integrator.transparent_max_bounce) {
integrator_path_terminate(kg, state, DEVICE_KERNEL_INTEGRATOR_SHADE_LIGHT);
return;
}
else {
integrator_path_next(kg,
state,
DEVICE_KERNEL_INTEGRATOR_SHADE_LIGHT,
DEVICE_KERNEL_INTEGRATOR_INTERSECT_CLOSEST);
return;
}
/* TODO: in some cases we could continue directly to SHADE_BACKGROUND, but
* probably that optimization is probably not practical if we add lights to
* scene geometry. */
}
CCL_NAMESPACE_END