Skip to content

Commit 6fef239

Browse files
committed
[misc] multiple lighting fixes and calibrations
1 parent d204e78 commit 6fef239

9 files changed

Lines changed: 298 additions & 134 deletions

File tree

data/shaders/common.hlsl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,47 @@ float3 photometric_to_radiometric(float3 value)
6767
return value / LUMINOUS_EFFICACY_MAX;
6868
}
6969

70+
/*------------------------------------------------------------------------------
71+
SUN RADIANCE
72+
------------------------------------------------------------------------------*/
73+
// single source of truth for every sun energy term in the engine, the directional
74+
// sun is always at slot 0 in the light parameters buffer, light.color holds the
75+
// temperature derived chromaticity (~1.0, 0.95, 0.90 at 5778 K) and light.intensity
76+
// holds the authored lux already converted to radiometric on the cpu via /683
77+
//
78+
// every consumer (direct surface lighting, sky scattering, sun disc, cloud direct
79+
// lighting, fog haze, ibl through the baked panorama) goes through these helpers
80+
// so the entire scene is locked to one calibration, changing the directional light
81+
// color or intensity in the editor propagates to every visual term coherently
82+
float3 get_sun_color()
83+
{
84+
return light_parameters[0].color.rgb;
85+
}
86+
87+
float get_sun_intensity()
88+
{
89+
return light_parameters[0].intensity;
90+
}
91+
92+
float3 get_sun_radiance()
93+
{
94+
return get_sun_color() * get_sun_intensity();
95+
}
96+
97+
// chromaticity preserving hdr clamp, the engine sky panorama is clamped to keep huge sun
98+
// radiance values inside the 16 bit storage range, a channel wise min(color, cap) would
99+
// saturate every channel to the cap whenever any single channel exceeded it which is the
100+
// case for the sun disc at every preset (a warm tinted (146, 136, 127) at the day preset
101+
// for example clips to a flat (100, 100, 100) and looks like a laboratory grade flat white
102+
// even though the source color carries the directional light's temperature), this helper
103+
// scales the whole color by a single factor when its peak channel exceeds the cap so the
104+
// warm or cool tint from the directional light survives the hdr to ldr range reduction
105+
float3 hdr_clamp_chroma(float3 color, float max_value)
106+
{
107+
float peak = max(color.r, max(color.g, color.b));
108+
return (peak > max_value) ? color * (max_value / peak) : color;
109+
}
110+
70111
/*------------------------------------------------------------------------------
71112
SATURATE
72113
------------------------------------------------------------------------------*/

data/shaders/fog.hlsl

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,13 @@ float3 compute_volumetric_fog(Surface surface, Light light, uint2 pixel_pos)
188188
{
189189
// sigma_s is the medium scattering coefficient in 1/m, sigma_t is extinction
190190
// the engine packs the user facing fog density into pass_get_f3_value().y so r.fog scales it linearly
191-
const float sigma_s = pass_get_f3_value().y * 0.0012f;
191+
// the directional sun gets a much smaller scattering coefficient than punctual lights, the sun's
192+
// intensity is several orders of magnitude larger than any spot or point light in the scene so
193+
// sharing one sigma_s would dump unphysical amounts of inscatter into every pixel whose ray
194+
// passes near the sun direction and wash the close geometry, the lower coefficient keeps the
195+
// god rays readable on the sky while letting close surfaces stay tied to their surface lighting
196+
const float sigma_s_base = pass_get_f3_value().y * 0.0012f;
197+
const float sigma_s = light.is_directional() ? (sigma_s_base * 0.25f) : sigma_s_base;
192198
const float sigma_t = sigma_s; // pure scattering, no absorption
193199
const float total_distance = surface.camera_to_pixel_length;
194200

@@ -245,8 +251,11 @@ float3 compute_volumetric_fog(Surface surface, Light light, uint2 pixel_pos)
245251
const float temporal_noise = noise_interleaved_gradient(pixel_pos, true);
246252
float3 ray_pos = ray_origin + ray_direction * (march_start + temporal_noise * step_length);
247253

248-
// moderate forward scattering, dust beams readable for punctual lights without producing a bright sun halo
249-
const float phase_g = 0.6f;
254+
// moderate forward scattering for punctual lights, the directional sun uses a much
255+
// weaker forward bias because its inscatter integrates over the entire camera ray and
256+
// a strong g produces an unphysical bright halo around the sun direction on every
257+
// opaque surface whose ray passes near the sun even when the sun itself is occluded
258+
const float phase_g = light.is_directional() ? 0.25f : 0.6f;
250259
const float min_transmittance = 0.005f;
251260

252261
// hoisted invariants, step transmittance is constant for the whole march
@@ -310,10 +319,13 @@ float3 compute_volumetric_fog(Surface surface, Light light, uint2 pixel_pos)
310319
// the inscatter is largest near the camera and becomes a constant additive haze on every pixel
311320
// whose ray passes through the light volume, without this fade it lights up the entire ground
312321
// plane out to the horizon as a uniform glow that does not match the falling off surface lighting
322+
// the directional sun gets a much stronger fade than punctual lights because its inscatter
323+
// integrates over the entire camera ray with full sun radiance and overwhelms close geometry,
313324
// sky pixels keep the full inscatter so the beams stay visible against the horizon
314325
if (!surface.is_sky())
315326
{
316-
result *= exp(-total_distance * 0.005f);
327+
const float fade_rate = light.is_directional() ? 0.04f : 0.02f;
328+
result *= exp(-total_distance * fade_rate);
317329
}
318330

319331
return result;

data/shaders/light.hlsl

Lines changed: 102 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -216,57 +216,52 @@ float trace_inline_shadow_ray(Light light, Surface surface, float2 pixel_xy)
216216
// Subsurface scattering with wrapped diffuse and thickness estimation
217217
float3 subsurface_scattering(Surface surface, Light light, AngularInfo angular_info)
218218
{
219-
// material-dependent scattering parameters
220-
const float wrap_factor = 0.5f; // wrapped lighting factor (0 = no wrap, 1 = full wrap)
221-
const float sss_exponent = 3.0f; // translucency falloff sharpness
222-
const float thickness_exponent = 1.5f; // edge thickness falloff
223-
const float sss_scale = 0.8f; // overall scattering strength multiplier
224-
const float min_scatter = 0.05f; // minimum ambient scattering
225-
219+
const float wrap_factor = 0.5f;
220+
const float sss_scale = 1.5f; // overall scattering strength
221+
const float min_scatter = 0.05f;
222+
226223
// light.to_pixel and surface.camera_to_pixel are pre-normalized by their builders
227224
float3 L = -light.to_pixel;
228225
float3 V = -surface.camera_to_pixel;
229226
float3 N = surface.normal;
230-
231-
// wrapped diffuse, allows light to wrap around the surface
227+
228+
// wrapped diffuse for the front lit half, lets sun energy bleed past the n_dot_l terminator
232229
float n_dot_l_wrapped = saturate((dot(N, L) + wrap_factor) / (1.0f + wrap_factor));
233230
float wrapped_diffuse = n_dot_l_wrapped * n_dot_l_wrapped;
234-
235-
// back scattering translucency, light passing through from behind using a distorted normal
231+
232+
// back scatter translucency for the back lit half, this is the gdc 2011 penner formulation
233+
// where the LIGHT direction is distorted by the surface normal (not the other way around),
234+
// the previous version had the operands swapped so it produced a vector parallel to the normal
235+
// and dot(V, -that) was always negative on the back hemisphere, leaving back lit translucent
236+
// surfaces like grass blades and leaves at the 5 percent min_scatter floor and looking black
237+
//
238+
// H = normalize(L + N * distortion) // light vector pulled toward the normal
239+
// back_scatter = saturate(dot(V, -H)) // peaks when the camera looks toward the light
240+
//
241+
// for the canonical case (camera looks at the sun through a leaf), L is roughly -V, the
242+
// distortion bends H toward the back face normal but L still dominates, -H points roughly
243+
// toward the camera, and dot(V, -H) approaches 1 which is what we want
236244
const float distortion = 0.4f;
237-
float3 N_distorted = normalize(N + L * distortion);
238-
float back_scatter = saturate(dot(V, -N_distorted));
239-
// pow(x, 3) replaced with mults, sss_exponent is the constant 3
240-
back_scatter = back_scatter * back_scatter * back_scatter;
241-
242-
// combine forward and backward scattering
245+
float3 L_distorted = normalize(L + N * distortion);
246+
float back_scatter = saturate(dot(V, -L_distorted));
247+
back_scatter = back_scatter * back_scatter * back_scatter; // sharpens the back lit lobe
248+
249+
// combine forward and backward scattering, t crosses 0.5 at the terminator
243250
float sss_term = lerp(back_scatter, wrapped_diffuse, saturate(dot(N, L) * 0.5f + 0.5f));
244-
sss_term = max(sss_term, min_scatter);
245-
246-
// thickness modulation, stronger scattering at thin edges
247-
float n_dot_v = saturate(dot(N, V));
248-
// pow(x, 1.5) replaced with x * sqrt(x), thickness_exponent is the constant 1.5
249-
float one_minus_nv = 1.0f - n_dot_v;
250-
float view_thickness = one_minus_nv * sqrt(one_minus_nv);
251-
252-
// light dependent, backlit areas show more scattering
253-
float n_dot_l = saturate(dot(N, L));
254-
float light_thickness = 1.0f - n_dot_l;
255-
256-
// combine thickness terms
251+
sss_term = max(sss_term, min_scatter);
252+
253+
// thickness modulation, thin grazing edges scatter more, back lit surfaces scatter more
254+
float n_dot_v = saturate(dot(N, V));
255+
float one_minus_nv = 1.0f - n_dot_v;
256+
float view_thickness = one_minus_nv * sqrt(one_minus_nv);
257+
float n_dot_l_clamped = saturate(dot(N, L));
258+
float light_thickness = 1.0f - n_dot_l_clamped;
257259
float thickness_modulation = saturate(view_thickness + light_thickness * 0.5f);
258-
259-
// compute light contribution with proper radiance
260-
float3 light_radiance = light.radiance;
261-
262-
// apply material strength and scale
263-
float sss_strength = surface.subsurface_scattering * sss_scale;
264-
265-
// Color tinting: preserve material color for subsurface scattering
266-
float3 sss_color = surface.albedo;
267-
268-
// combine all terms
269-
return light_radiance * sss_term * thickness_modulation * sss_strength * sss_color;
260+
261+
float sss_strength = surface.subsurface_scattering * sss_scale;
262+
float3 sss_color = surface.albedo;
263+
264+
return light.radiance * sss_term * thickness_modulation * sss_strength * sss_color;
270265
}
271266

272267
// evaluates a single light against the surface, accumulates into out parameters
@@ -288,6 +283,14 @@ void evaluate_light(
288283
Light light;
289284
light.Build(light_index, surface);
290285

286+
// raw light energy uncoupled from n_dot_l, used by subsurface scattering which models
287+
// light transmitting through translucent surfaces from the back hemisphere, the canonical
288+
// case (grass blade, leaf, ear) is where the lit side is opposite the viewed side and
289+
// n_dot_l is zero or negative, light.radiance bakes n_dot_l into it so it cannot drive
290+
// sss on its own, the raw form preserves the energy that sss internally redirects through
291+
// its wrap_factor and back_scatter terms
292+
float3 light_radiance_raw = light.color * light.intensity * light.attenuation;
293+
291294
// when restir pt is enabled the initial ris pool now samples every light type
292295
// (directional, point, spot, area) so all direct lighting at the primary is owned by
293296
// restir, skip the analytical surface eval entirely to avoid double counting, volumetric
@@ -301,40 +304,70 @@ void evaluate_light(
301304
float3 L_subsurface = 0.0f;
302305
float3 L_volumetric = 0.0f;
303306

304-
// light.radiance already bakes attenuation and n_dot_l, zero radiance means no surface contribution
305-
bool light_contributes_to_surface = any(light.radiance > 0.0f);
307+
// light can contribute via the standard brdf only when n_dot_l clears (light.radiance > 0)
308+
// light can contribute via subsurface scattering whenever the raw energy is non zero (sss
309+
// fires regardless of n_dot_l, that is its whole purpose), so the gate admits either path
310+
bool light_can_contribute = any(light_radiance_raw > 0.0f);
311+
bool has_brdf = any(light.radiance > 0.0f);
312+
bool has_sss = surface.subsurface_scattering > 0.0f;
306313

307-
if (eval_surface && !surface.is_sky() && !skip_surface_lighting && light_contributes_to_surface)
314+
if (eval_surface && !surface.is_sky() && !skip_surface_lighting && light_can_contribute && (has_brdf || has_sss))
308315
{
309-
// shadow term, ray traced shadows are mutually exclusive with rasterized/screen space shadows
316+
// shadow term is split into a primary and a contact shadow
317+
//
318+
// primary -> ray traced shadow when the cvar and the light enable it, otherwise the
319+
// rasterized cascade / cubemap shadow map, this tells us whether the surface
320+
// is occluded by some OTHER object in the scene (a wall, the terrain, etc)
321+
//
322+
// contact -> screen space contact shadows refine the primary, they capture the small
323+
// features (cascade aliasing, contact under foliage, etc) that neither the
324+
// shadow map nor the ray traced primary resolves well, they used to be
325+
// mutually exclusive with ray traced shadows which lost all contact detail
326+
// whenever ray traced shadows were enabled
327+
//
328+
// primary applies to both brdf and sss, sss is "light reaches this surface at all"
329+
//
330+
// contact applies ONLY to brdf, never to sss, contact shadows march a screen space ray
331+
// from the pixel toward the light and treat every surface (including the leaf itself)
332+
// as opaque, on a translucent material like grass or leaves the ray immediately hits the
333+
// very blade we are shading and reports occluded, that crushed sss to zero on every back
334+
// face and produced the pitch black grass look, the primary shadow already correctly
335+
// handles the "is the blade itself in real shadow" case
310336
bool can_use_rt_shadows = light.has_shadows() && is_ray_traced_shadows_enabled();
311337

338+
float L_shadow_primary = 1.0f;
339+
float L_shadow_contact = 1.0f;
340+
312341
if (can_use_rt_shadows && light.is_directional())
313342
{
314343
// dedicated screen space pass produces high quality multi sample sun shadows
315-
L_shadow = sample_ray_traced_shadow(surface.uv);
316-
light.radiance *= L_shadow;
344+
L_shadow_primary = sample_ray_traced_shadow(surface.uv);
317345
}
318346
#ifdef RAY_TRACING_ENABLED
319347
else if (can_use_rt_shadows)
320348
{
321349
// inline ray traced shadow for point spot and area lights
322-
L_shadow = trace_inline_shadow_ray(light, surface, float2(pixel_xy));
323-
light.radiance *= L_shadow;
350+
L_shadow_primary = trace_inline_shadow_ray(light, surface, float2(pixel_xy));
324351
}
325352
#endif
326353
else if (light.has_shadows())
327354
{
328-
L_shadow = compute_shadow(surface, light);
329-
330-
if (light.has_shadows_screen_space() && surface.is_opaque())
331-
{
332-
L_shadow = min(L_shadow, tex_uav_sss[int3(pixel_xy, light.screen_space_shadows_slice_index)].x);
333-
}
355+
L_shadow_primary = compute_shadow(surface, light);
356+
}
334357

335-
light.radiance *= L_shadow;
358+
if (light.has_shadows() && light.has_shadows_screen_space() && surface.is_opaque())
359+
{
360+
L_shadow_contact = tex_uav_sss[int3(pixel_xy, light.screen_space_shadows_slice_index)].x;
336361
}
337362

363+
// exposed for output composition (the engine still tracks a single L_shadow for now)
364+
L_shadow = min(L_shadow_primary, L_shadow_contact);
365+
366+
// brdf gets the combined shadow (primary plus contact)
367+
// sss gets only the primary, contact shadows must not block translucent transmission
368+
light.radiance *= L_shadow;
369+
light_radiance_raw *= L_shadow_primary;
370+
338371
AngularInfo angular_info;
339372
angular_info.Build(light, surface);
340373

@@ -348,6 +381,7 @@ void evaluate_light(
348381
}
349382

350383
float3 L_specular_lobes = 0.0f;
384+
if (has_brdf)
351385
{
352386
if (surface.anisotropic > 0.0f)
353387
{
@@ -367,11 +401,17 @@ void evaluate_light(
367401
{
368402
L_specular_lobes += BRDF_Specular_Sheen(surface, angular_info);
369403
}
404+
}
370405

371-
if (surface.subsurface_scattering > 0.0f)
372-
{
373-
L_subsurface += subsurface_scattering(surface, light, angular_info);
374-
}
406+
if (has_sss)
407+
{
408+
// sss uses the raw radiance so it can fire on back faced surfaces, the previous
409+
// code passed light.radiance (n_dot_l baked in) which was zero for the entire
410+
// back hemisphere and left translucent geometry pitch black on its shadowed side
411+
// even when the sun was clearly visible through the material from the camera
412+
Light light_sss = light;
413+
light_sss.radiance = light_radiance_raw;
414+
L_subsurface += subsurface_scattering(surface, light_sss, angular_info);
375415
}
376416

377417
L_specular_sum += L_specular_lobes;
@@ -380,7 +420,7 @@ void evaluate_light(
380420
surface.roughness_alpha = original_roughness_alpha;
381421

382422
// diffuse_precomputed is zero for transparents, skip the eval entirely
383-
if (!is_transparent)
423+
if (has_brdf && !is_transparent)
384424
{
385425
L_diffuse_term += BRDF_Diffuse(surface, angular_info);
386426
}

0 commit comments

Comments
 (0)