feat(render3d): light you can see - sun shafts and valley mist
Everything before this made the air a COLOUR APPLIED TO A SURFACE. Nothing put light in the space between surfaces, so the basin had no shafts, no pooled mist and no rays off a ridge at any hour, whatever was done to the fog. A half-resolution march from the camera to the depth buffer, asking the same shadow the rest of the frame asks - the cascades, the baked height-field shadow and the cloud mask - so a shaft is cast by the actual trees and the actual ridge and a passing cloud dims its own rays. Henyey-Greenstein scattering, because real air throws light forward. Density and a separate ground-hugging mist layer ride the sun's elevation, so mist forms in the cold at either end of the day and burns off by mid-morning. Composited with the bloom pyramid's own tent upsample under ONE/ONE - what was wanted and already there - and before bloom, so a shaft blooms. Into post_hdr, not post_scene: post_scene is what the water refracts and shafts added there would sit under the lake. The tuning that mattered was the sky term, which is added at every step: at 0.06 it accumulated into a flat grey wash lifting lit and shadowed air equally, which is the contrast a shaft is made of, and the valley came out one pale sheet. At 0.012 the sun dominates and there is light rather than fog. I cut the density and mist three times before the frame looked like air instead of paint. R3D_NOVOL=1 for an A/B; Off in Settings skips the pass whole. 400 frames at 07:00: GL 7.1 -> 7.3 s, VK 7.2 -> 7.4 s. Backends agree to 0.08/255. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d34fb5bc63
commit
b12228c662
15 changed files with 252 additions and 1 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/74258189.frag.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/74258189.frag.spv
Normal file
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/74258189.vert.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/74258189.vert.spv
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -3117,3 +3117,54 @@ T 46e2325c u_scene 7
|
|||
T 46e2325c u_shadow 8
|
||||
T 46e2325c u_tershadow 9
|
||||
T 46e2325c u_ts_height 10
|
||||
P 74258189 fullscreen.vert volumetric.frag
|
||||
B 74258189 frag 1 924
|
||||
U 74258189 frag u_cascade_vp 0 mat4 5 64
|
||||
U 74258189 frag u_cascade_split 320 float 5 16
|
||||
U 74258189 frag u_cascade_range 400 float 5 16
|
||||
U 74258189 frag u_cascade_texel 480 float 5 16
|
||||
U 74258189 frag u_sun_dir 560 vec3 1 0
|
||||
U 74258189 frag u_sun_color 576 vec3 1 0
|
||||
U 74258189 frag u_cam_pos 592 vec3 1 0
|
||||
U 74258189 frag u_prefilter_levels 604 float 1 0
|
||||
U 74258189 frag u_fog_density 608 float 1 0
|
||||
U 74258189 frag u_fog_height_falloff 612 float 1 0
|
||||
U 74258189 frag u_fog_base 616 float 1 0
|
||||
U 74258189 frag u_clip_y 620 float 1 0
|
||||
U 74258189 frag u_spec_scale 624 float 1 0
|
||||
U 74258189 frag u_sky_rot 632 vec2 1 0
|
||||
U 74258189 frag u_ground_alb 640 vec3 1 0
|
||||
U 74258189 frag u_ground_alb_hi 656 vec3 1 0
|
||||
U 74258189 frag u_ground_hi_y 668 float 1 0
|
||||
U 74258189 frag u_ground_hi_w 672 float 1 0
|
||||
U 74258189 frag u_ibl_scale 688 vec3 1 0
|
||||
U 74258189 frag u_daylight 700 float 1 0
|
||||
U 74258189 frag u_fire_pos 704 vec3 1 0
|
||||
U 74258189 frag u_fire_color 720 vec3 1 0
|
||||
U 74258189 frag u_hand_pos 736 vec3 1 0
|
||||
U 74258189 frag u_hand_color 752 vec3 1 0
|
||||
U 74258189 frag u_hand_dir 768 vec3 1 0
|
||||
U 74258189 frag u_hand_cone 780 float 1 0
|
||||
U 74258189 frag u_ts_origin 784 vec2 1 0
|
||||
U 74258189 frag u_ts_half 792 float 1 0
|
||||
U 74258189 frag u_ts_on 796 float 1 0
|
||||
U 74258189 frag u_force_cascade 800 int 1 0
|
||||
U 74258189 frag u_cloud_shadow 804 float 1 0
|
||||
U 74258189 frag u_time 808 float 1 0
|
||||
U 74258189 frag u_fog_inscatter 812 float 1 0
|
||||
U 74258189 frag u_fog_desat 816 float 1 0
|
||||
U 74258189 frag u_inv_vp 832 mat4 1 0
|
||||
U 74258189 frag u_vol_steps 896 float 1 0
|
||||
U 74258189 frag u_vol_density 900 float 1 0
|
||||
U 74258189 frag u_vol_falloff 904 float 1 0
|
||||
U 74258189 frag u_vol_far 908 float 1 0
|
||||
U 74258189 frag u_vol_g 912 float 1 0
|
||||
U 74258189 frag u_vol_mist 916 float 1 0
|
||||
U 74258189 frag u_vol_mist_h 920 float 1 0
|
||||
T 74258189 u_brdf 2
|
||||
T 74258189 u_depth 3
|
||||
T 74258189 u_irradiance 4
|
||||
T 74258189 u_prefilter 5
|
||||
T 74258189 u_shadow 6
|
||||
T 74258189 u_tershadow 7
|
||||
T 74258189 u_ts_height 8
|
||||
|
|
|
|||
|
|
@ -54,3 +54,4 @@ terrain.vert|terrain.frag|#define SUN_INLINE;#define FAR_ONLY;
|
|||
terrain.vert|terrain.frag|#define SUN_INLINE;#define NEAR_ONLY;
|
||||
terrain.vert|tersun.frag|
|
||||
water.vert|water.frag|
|
||||
fullscreen.vert|volumetric.frag|
|
||||
|
|
|
|||
95
packages/ludic.render3d/shaders/volumetric.frag
Normal file
95
packages/ludic.render3d/shaders/volumetric.frag
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Light you can see: sun shafts through the canopy, mist lying in the valley, and a ridge at
|
||||
// dusk standing in glowing air. Everything before this made the AIR a colour applied to a
|
||||
// surface; nothing put light in the space between surfaces, so the basin had no shafts, no
|
||||
// pooled mist and no rays off the peaks at any hour.
|
||||
//
|
||||
// This is a half-resolution march from the camera to whatever the depth buffer says is in
|
||||
// front of it. At each step it asks the SAME shadow the rest of the frame asks - the cascades,
|
||||
// the baked height-field shadow and the cloud mask - so a shaft is cast by the actual trees
|
||||
// and the actual ridge, and a cloud passing over dims its own rays.
|
||||
in vec2 v_uv;
|
||||
out vec4 o_color;
|
||||
uniform sampler2D u_depth;
|
||||
uniform mat4 u_inv_vp;
|
||||
uniform float u_vol_steps;
|
||||
uniform float u_vol_density; // how much light the air scatters back at you
|
||||
uniform float u_vol_falloff; // how fast the haze thins with height above the fog datum
|
||||
uniform float u_vol_far; // stop marching here (m): past it the analytic fog carries on
|
||||
uniform float u_vol_g; // Henyey-Greenstein anisotropy: how forward-throwing the air is
|
||||
uniform float u_vol_mist; // a denser layer lying ON the ground at dawn and dusk
|
||||
uniform float u_vol_mist_h; // how deep that layer is (m)
|
||||
|
||||
// One shadow lookup for a point in mid-air. It wants no normal and no slope bias - there is no
|
||||
// surface here to shadow-acne - so it is the cheap tap rather than the receiver's rotated disc.
|
||||
float volShadow(vec3 wpos, float viewDepth) {
|
||||
int c = CASCADES - 1;
|
||||
for (int i = 0; i < CASCADES - 1; i++) { if (viewDepth < u_cascade_split[i]) { c = i; break; } }
|
||||
vec4 lp = u_cascade_vp[c] * vec4(wpos, 1.0);
|
||||
vec3 p = lp.xyz / lp.w * 0.5 + 0.5;
|
||||
float s = 1.0;
|
||||
if (p.x >= 0.0 && p.x <= 1.0 && p.y >= 0.0 && p.y <= 1.0 && p.z <= 1.0) {
|
||||
s = shadowTap(p.xy, c, p.z - 0.0016);
|
||||
}
|
||||
return min(s, terrainShadow(wpos)) * cloudShadow(wpos);
|
||||
}
|
||||
|
||||
// how much air there is at a height: the basin's own haze, plus a shallow mist that lies on
|
||||
// the ground rather than filling the valley
|
||||
float volDensity(vec3 wpos) {
|
||||
float y = wpos.y - u_fog_base;
|
||||
float air = exp(-max(y, 0.0) * u_vol_falloff);
|
||||
float mist = u_vol_mist * exp(-max(y, 0.0) / max(u_vol_mist_h, 1.0));
|
||||
return u_vol_density * (air + mist);
|
||||
}
|
||||
|
||||
// Henyey-Greenstein: real air throws light FORWARD, which is why a low sun makes the whole
|
||||
// valley glow when you look into it and almost nothing when you look away from it.
|
||||
float phaseHG(float c, float g) {
|
||||
float g2 = g * g;
|
||||
float d = 1.0 + g2 - 2.0 * g * c;
|
||||
return (1.0 - g2) / (4.0 * PI * max(d * sqrt(max(d, 1e-4)), 1e-4));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float d = texture(u_depth, v_uv).r;
|
||||
// the world point this pixel looks at, and the ray to it
|
||||
vec4 far4 = u_inv_vp * vec4(v_uv * 2.0 - 1.0, 1.0, 1.0);
|
||||
vec3 farP = far4.xyz / far4.w;
|
||||
vec3 dir = normalize(farP - u_cam_pos);
|
||||
float march;
|
||||
if (d >= 1.0) {
|
||||
march = u_vol_far; // sky: march the whole way
|
||||
} else {
|
||||
vec4 h4 = u_inv_vp * vec4(v_uv * 2.0 - 1.0, d * 2.0 - 1.0, 1.0);
|
||||
march = min(length(h4.xyz / h4.w - u_cam_pos), u_vol_far);
|
||||
}
|
||||
int steps = int(u_vol_steps);
|
||||
float dt = march / float(steps);
|
||||
// A dither per pixel, not per frame. Advancing it every frame only helps if something
|
||||
// averages the frames, and there is no temporal resolve here (post.ludic); it would just
|
||||
// be noise that changes, which is what the old TAA attempt was removed for.
|
||||
float jitter = ign(gl_FragCoord.xy);
|
||||
float ph = phaseHG(dot(dir, u_sun_dir), u_vol_g);
|
||||
vec3 acc = vec3(0.0);
|
||||
float trans = 1.0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (i >= steps) break;
|
||||
float t = (float(i) + jitter) * dt;
|
||||
vec3 p = u_cam_pos + dir * t;
|
||||
float dens = volDensity(p);
|
||||
if (dens > 1e-5) {
|
||||
float vis = volShadow(p, t);
|
||||
// in-scattering from the sun, and a little from the sky so shadowed air is not black
|
||||
// The sky's own contribution is kept SMALL and deliberately so. It is added at every
|
||||
// step, so it accumulates into a flat grey wash that lifts lit and shadowed air by the
|
||||
// same amount - which is exactly the contrast a shaft is made of. At 0.06 the valley
|
||||
// came out as one pale sheet with no rays in it at all; the sun's term has to dominate
|
||||
// or there is no light to see, only fog.
|
||||
vec3 inscat = u_sun_color * vis * ph + skyIrradiance(vec3(0.0, 1.0, 0.0)) * 0.012;
|
||||
float a = dens * dt;
|
||||
acc += inscat * a * trans;
|
||||
trans *= exp(-a);
|
||||
}
|
||||
}
|
||||
o_color = vec4(sane(acc), 1.0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue