feat(render3d): a bole that does not quake, leaves out of the lens, a light with a reach
layer_flutter's mask was the vertex's height alone, so on a ten metre aspen every part of the trunk above 1.2 m trembled with the leaves and the bole read as cloth. What separates a leaf from a bole is distance from the model's centre line, not height; the mask is radial now, and small plants keep the old one because a flower is all leaf. NEAR_FADE dithers tree foliage out inside arm's length of the camera, in the depth prepass, so the lit pass never sees those pixels and the equal-depth optimisation is untouched. gl_Position.w is the view depth, so it costs one varying and no uniform. Blades, cards and flowers are excluded - they live at the player's feet - and the shadow pass keeps every leaf. daylight_hand takes a reach in metres. The falloff was an inverse square windowed between 26 and 6 with both numbers hard-coded: two per cent of its own near field at ten metres, so a torch lit your boots. It is a gentle power out to the reach now. Measured in Maroon Lake, twice per side: the crown mask moves 1.7% of an aspen frame and nothing at all in the meadow under it; the near fade moves 7.1% of a first-person frame at a conifer and 0% where there is no foliage in the lens; the torch's reach lifts the lit ground 20 to 45% and leaves the sky bit-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a88aecb993
commit
18d23ca20d
8 changed files with 97 additions and 8 deletions
|
|
@ -12,9 +12,17 @@
|
|||
// because without MSAA a fragment is either there or not.
|
||||
in vec3 v_wpos;
|
||||
in vec2 v_uv;
|
||||
in float v_near;
|
||||
uniform sampler2D u_diff;
|
||||
void main() {
|
||||
if (v_wpos.y < u_clip_y) discard;
|
||||
#ifdef NEAR_FADE
|
||||
// Leaves in the camera's face (model.vert's v_near). Dithered out rather than blended:
|
||||
// no sorting, no second pass and no halo, and because it happens in the PREPASS the lit
|
||||
// pass never sees those pixels either - its equal-depth test simply finds nothing to
|
||||
// match. A branch a metre from the lens stops being an opaque wall in front of the body.
|
||||
if (v_near < 0.999 && v_near < fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453)) discard;
|
||||
#endif
|
||||
vec4 d = texture(u_diff, v_uv);
|
||||
float lod = textureQueryLod(u_diff, v_uv).x;
|
||||
float a = min(d.a * (1.0 + 0.45 * max(lod, 0.0)), 1.0);
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ uniform vec3 u_hand_pos; // a torch or flashlight in the hand
|
|||
uniform vec3 u_hand_color;
|
||||
uniform vec3 u_hand_dir;
|
||||
uniform float u_hand_cone; // cos of the half-angle; <= -1: a point light
|
||||
uniform float u_hand_reach; // metres to where it dies out entirely
|
||||
vec3 skyIrradianceRaw(vec3 n) { return texture(u_irradiance, equirectUV(n)).rgb * u_ibl_scale; }
|
||||
vec3 skyIrradiance(vec3 n) {
|
||||
vec3 up = skyIrradianceRaw(vec3(n.x, max(n.y, 0.0), n.z));
|
||||
|
|
@ -88,7 +89,13 @@ vec3 handLight(vec3 wpos, vec3 n, vec3 albedo) {
|
|||
vec3 d = u_hand_pos - wpos;
|
||||
float r2 = max(dot(d, d), 0.04);
|
||||
vec3 l = d * inversesqrt(r2);
|
||||
float att = smoothstep(26.0, 6.0, sqrt(r2)) / (0.5 + r2 * 0.35);
|
||||
// A torch is not a point source in free air, and lighting it as one made it useless.
|
||||
// The old curve was an inverse square windowed off between 26 and 6 metres: two per cent
|
||||
// of its own near field by ten metres out, so carrying fire at night lit your boots and
|
||||
// nothing else. This is a reach and a gentle power falloff instead - about nine tenths at
|
||||
// a metre, half at half the reach, a tenth at nine tenths of it, nothing past it - which
|
||||
// is a pool of light with a gradient in it rather than a hotspot with a cliff.
|
||||
float att = pow(max(1.0 - sqrt(r2) / max(u_hand_reach, 0.001), 0.0), 1.25);
|
||||
if (u_hand_cone > -1.0) {
|
||||
float c = dot(-l, u_hand_dir);
|
||||
att *= smoothstep(u_hand_cone, u_hand_cone + 0.12, c);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ in float v_seed;
|
|||
in vec2 v_rot;
|
||||
in float v_hull;
|
||||
in float v_quake; // an aspen leaf turning its pale side up
|
||||
in float v_near; // 0 in the camera's face, 1 past arm's length (NEAR_FADE)
|
||||
uniform float u_model_h;
|
||||
out vec4 o_color;
|
||||
uniform sampler2D u_diff;
|
||||
|
|
@ -38,6 +39,10 @@ void main() {
|
|||
// early depth rejection off and bring the overdraw back, and the prepass has already
|
||||
// cut both the clip plane and the alpha)
|
||||
if (v_wpos.y < u_clip_y) discard;
|
||||
#ifdef NEAR_FADE
|
||||
// the same dither as the prepass, for the draws that have no prepass in front of them
|
||||
if (v_near < 0.999 && v_near < fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453)) discard;
|
||||
#endif
|
||||
#endif
|
||||
vec3 N = normalize(v_nrm);
|
||||
if (!gl_FrontFacing && v_hull >= 0.0) N = -N;
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ out float v_hull; // 0 at the crown's axis .. 1 at its rim
|
|||
// varying so the fragment stage can flash the leaf's pale underside as it turns - which is
|
||||
// the part you actually SEE. A still frame of a tremble is a still frame of nothing.
|
||||
uniform float u_flutter;
|
||||
out float v_near; // 0 in the camera's face, 1 past arm's length (NEAR_FADE)
|
||||
out float v_quake;
|
||||
void main() {
|
||||
float s = i_rot.x, c = i_rot.y;
|
||||
|
|
@ -104,7 +105,14 @@ void main() {
|
|||
// the whole tree shivering.
|
||||
float qp = u_time * 8.5 + a_pos.x * 11.3 + a_pos.z * 9.7 + a_pos.y * 4.1 + i_rot.z * 6.2831;
|
||||
float q = sin(qp) * 0.65 + sin(qp * 1.73 + 1.1) * 0.35;
|
||||
float amp = u_flutter * (0.40 + 0.60 * windGust(i_pos.xz, u_time)) * smoothstep(0.0, 1.2, hgt);
|
||||
// ...and only out on the CROWN. The mask was height alone, so on an aspen every vertex
|
||||
// of a ten metre bole above 1.2 m trembled along with the leaves and the TRUNK moved
|
||||
// like cloth. What separates a leaf from a bole is not height, it is distance from the
|
||||
// model's own centre line: the leaves are out at the edge of the crown and the trunk is
|
||||
// on the axis. Small plants keep the old mask - a flower is all leaf.
|
||||
float rmask = 1.0;
|
||||
if (u_model_h > 2.0) rmask = smoothstep(0.22, 0.75, length(a_pos.xz) / max(u_model_h * 0.22, 0.05));
|
||||
float amp = u_flutter * (0.40 + 0.60 * windGust(i_pos.xz, u_time)) * smoothstep(0.0, 1.2, hgt) * rmask;
|
||||
p.x += q * amp * 0.10;
|
||||
p.y += q * amp * 0.05;
|
||||
p.z += cos(qp * 0.9) * amp * 0.10;
|
||||
|
|
@ -133,5 +141,25 @@ void main() {
|
|||
gl_Position = u_light_vp * vec4(w, 1.0);
|
||||
#else
|
||||
gl_Position = u_proj * u_view * vec4(w, 1.0);
|
||||
#endif
|
||||
// Leaves in the camera's face. A third-person camera walks through a crown and the
|
||||
// branch between it and the body is an opaque wall a metre from the lens; in first
|
||||
// person it is the whole screen. Tree foliage within arm's length of the CAMERA fades
|
||||
// out - w is the view-space depth for a perspective projection, so this costs nothing
|
||||
// and needs no uniform. Blades, cards and flowers are excluded on purpose: they live at
|
||||
// the player's feet, they are always this close, and fading them opens a hole in the
|
||||
// meadow. The shadow pass keeps every leaf, or a tree would stop shading the ground it
|
||||
// is standing on as you walked up to it.
|
||||
v_near = 1.0;
|
||||
#if defined(NEAR_FADE) && !defined(SHADOW_PASS)
|
||||
// The BOLE is never faded, only what hangs off it. A conifer's trunk and its needle
|
||||
// cards are one mesh drawn by one program, so the first cut dissolved the trunk into a
|
||||
// dither pattern as you walked up to it - a solid tree you can see through reads as a
|
||||
// fault, where a branch getting out of your way reads as the camera being polite. Same
|
||||
// signal as the quake's crown mask: the trunk is on the model's axis and the foliage is
|
||||
// out from it.
|
||||
float ax = 1.0;
|
||||
if (u_model_h > 2.0) ax = smoothstep(0.12, 0.30, length(a_pos.xz) / max(u_model_h * 0.22, 0.05));
|
||||
v_near = mix(1.0, clamp((gl_Position.w - 0.5) / 1.7, 0.0, 1.0), ax);
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue