terpage.glsl is the reference block (tpSlot, tpUV, terHeight, terHeightSmooth, terNormalXZ,
terOrtho, tpOrthoRes, tpOrthoLod), pasted by section into terrain.vert, terrain.frag,
tersun.frag, model.vert, grass.vert and grass.mesh. u_tp_on = 0 reads the old samplers with the
old coordinates and filtering; on, a resident tile is read at level 0 from u_tp_h / u_tp_nrm /
u_tp_ortho, anything else from the coarse map now bound under the old names. The B-splines use
the FULL map's texel and take every tap through the page, so a tile edge stays one surface;
blurred photograph reads (lod 1-2.5) stay on u_ortho with the level moved down by the coarse
map's ratio. The fragment stages drop their unused u_height. SPIR-V rebuilt: terrain programs
carry 23 samplers (21 in the fragment stage), up from 19.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The volumetric pass marched 800 m in a fixed number of steps whatever the wall: it now stops at the
wall with steps in proportion (at least 8) - 0.57 -> 0.33 ms at 30 m by the pass timers. SSGI skipped
only past 900 m; it now also skips past 0.85 of a wall, where the fog is solid (u_ao_far). applyFog
samples the fog's colour only where its weight is above zero. A card layer whose casters inside the
wall are 80% or more of it (a kilometre's wall) casts from its static list and is not refiltered and
re-uploaded every 4 m of camera motion. Fog off: the frame unchanged (0 pixels over 8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DEM (R16 with mips, 44 MB) was read only by terrain_generate and kept for the map's life; a
world swap loads its own again. The height-field sun shadow was one RGBA32F for three values and an
unused fourth: the lowest lit height stays 32-bit (a receiver 3000 m up compares against it to a
quarter metre), the occluder distance and the cloud mask go beside it in RG16F (64 -> 32 MB), baked
in a pass of their own (TS_AUX): a pipeline takes one colour format for all its targets, and drawn
together into R32F + RG16F the writes went nowhere and nothing was lit.
-75 MB at every fog level, off included (2008 -> 1933 MB at the overlook); the fog-off frame is
unchanged (0 pixels over 8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whatever is cut at the wall is now cut inside full fog: at 0.3-1.0 the last metres before the cut
were 80-95% fogged against fully fogged ground behind, which drew a line of tufts at 70 m and a
dark band of blade tips at 30 m. The blades thin out while the fog is still coming in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every lit program blends toward the horizon colour (the prefiltered sky with the sun's inscatter,
so it carries the day's grade); the sky's horizon band is pulled to the same colour, wider the
closer the wall. The far plane, scatter and stream cull reach and the shadow cascades are clamped
to the wall; the reflection pass shares the shaders and the cull. 0 is off: every branch is gated
on u_fog_wall > 0 and r3d_reach hands back the far it was given. R3D_FOG_WALL=<m> for a shot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first generation pass's R32F height is kept as ter_height_tex (not copied into an RGBA32F),
so every reader of the height - placement, the read-back, physics, selftest16's 9 mm - sees the
same 32 bits. ternormal.frag writes the baked normal's x and z into ter_normal_tex (RG16F), and
the six places that read it (terrain.frag twice, tersun.frag, grass.vert, grass.mesh,
grass_cull.comp, which takes a third texture at binding 6) rebuild y. SPIR-V regenerated.
Maroon Lake's play, headless Vulkan: 2793 -> 2647 MB. The camp's frame: 0.066% of pixels differ by
more than 8 (mean 0.024/255), isolated grass blades at the slope gate. ludic-dev test 307/307.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A slice has carried { ptr, len, cap } since it existed and nothing ever read
the len: every index emitted a bare getelementptr. Running off the end of one
wrote into whatever the allocator had put next, and the program died somewhere
else entirely - a maroon-lake crash took a day to find because the stack named
a texture upload and the write was in a telemetry buffer five frames earlier.
Now each index loads the length and compares unsigned, which rejects a negative
index in the same instruction, and a failure aborts with the location the other
located errors use:
oob.ludic:9: index out of range: 7, len 3
`words` and the other raw buffers are unchanged - they are a bare malloc with no
length to check, which is the argument for moving off them.
The SPIR-V variants are regenerated in the same commit: shaders --check was
failing against the edited GLSL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rain fell and the ground did not change: the weather was a curtain of particles in front
of a dry valley.
u_wet, 0 dry to 1 soaked, does the three things a wet surface does. Darkens the albedo.
Drops the roughness hard - which is what makes a soaked meadow read as soaked rather than
merely dark, because the sky glances off it. And pools: water finds the low places and
flat ground holds what a slope sheds, so the puddle mask is the macro noise the materials
already use gated on slope, and a puddle takes the world's up for its normal rather than
the ground's relief.
It joins the shore's existing wet band rather than fighting it - the same term from a
different cause.
34.1% of the frame changes between dry and soaked. GL 7.1 s either way over 400 frames.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The grass had a gust built from two sines; the trees had no gust term AT ALL, only a
per-instance wobble. So the meadow rippled and the canopy above it swayed to an unrelated
rhythm, and nothing ever crossed the valley.
wind.glsl is prepended to every stage (programs.ludic, and shaders.ludic for the SPIR-V
build) so grass, crowns and water read the same field at the same world position. It
declares no uniforms on purpose: u_time and u_wind already exist in several of those
files and redeclaring them is a compile error in whichever stage includes both.
The wavelength is what made it work. At 0.030 the front was 209 m crest to crest -
longer than the meadow you can see - so the whole frame sat in one phase and the gust
read as everything breathing together. At 0.085 it is 74 m and a front crosses a view in
a couple of seconds.
R3D_DEBUG_WIND=1 paints the field on the ground. It is the only honest way to show a gust
in a still image, and two shots 0.3 s apart move by 39/255.
400 frames: GL 6.9 s, VK 7.2 s. Backends agree to 0.68/255.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correcting the previous commit, which said this was unfixed. It is fixed, and I had the
wrong knob: raising the mudstone tint's magnitude from 0.14 to 0.38 made the face LIGHTER
rather than REDDER and it came out pale tan. The shader's own DEBUG_ALB view measured
red:blue at 1.56 on the peak where maroon mudstone wants nearer 4, which says plainly that
what needed moving was green and blue DOWN, not red up.
The built-in DEBUG_MAT view is what settled it, and I should have reached for it an hour
earlier instead of inventing my own: it shows the Bells as pure red, rockW = 1, so the
face is rock and always was. That also explains why painting the snow albedo bright red
changed nothing and I wrongly read that as "this surface is not drawn by this shader" -
there is no snow on those faces to paint.
The far tier's fallback moves with it, so the cheap tier keeps the near tier's mean.
Backends agree to 0.60/255.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
55% of every ground pixel was the orthophotograph, at every distance and on every
material. A survey image records WHERE the forest and the scree are well and what a
steep rock face is coloured badly - it is shot through kilometres of air at an angle no
player stands at. The photograph keeps the meadow and the forest and gives the rock back
to the rock, by material and by elevation and never by distance: a distance fade used to
live here and was removed because the photograph has the day's own shadows baked in, so
grass grew dark patches as you backed away and they slid as you walked.
The far tier's rock fallback is the maroon mean, not a neutral grey - TFAST_3 skips the
rock sample and leaves that constant standing in for a whole mountain.
Ortho-classified snow needs altitude now. Bright and unsaturated is what snow looks like
from a satellite and also what a sunlit rock face looks like. Gully snow is gated on the
snow line rather than two absolute heights.
Distance desaturation eased 1.9 -> 1.15: tuned on a valley whose rock was grey anyway.
NOT FIXED, and I want it recorded rather than implied: the Bells themselves are still
not maroon. The near and middle rock is warmer and measurably so, but the distant peak
does not respond to ANY of this - not the rock tint, not the ortho weight, not the snow
line, not the gully gate. Painting sA bright red left it unchanged, so whatever surface
that is, it is not this shader's snow and not this shader's material blend. Somebody
should find out what draws it before tuning any of these numbers further.
400 frames: GL 7.0 s, VK 7.2 s. Backends agree to 0.62/255.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aerial perspective is a curve now. A low sun shines through far more air than a
high one and shines ALONG the ground rather than down onto it, so density, height
falloff and forward scatter all ride the sun's elevation; overcast thickens the air
and flattens the scatter, because a grey sky has no disc to scatter from. `lowsun`
falls away BELOW the horizon as well as above it, or the middle of the night gets a
dawn's haze with no dawn to justify it.
The term that was missing entirely is distance DESATURATION. Blending a saturated
green ridge toward a saturated blue noon sky leaves a saturated ridge - which is why
the same valley read as a photograph at dusk, where the fog colour happened to be a
warm grey, and as a toy at one o'clock. A surface is now pulled toward its own
luminance faster than the fog itself arrives. Measured far/near saturation at the
camp: 07:00 1.11 -> 0.89, 09:00 1.04 -> 0.93, 13:00 0.98 -> 0.89.
The grade is the hour's too - nine literals bound at the draw, written by
daylight_set now. Noon is the case worth naming: direct sun is warm-white and the
only thing filling a midday shadow is a blue sky, so noon gets a cool balance over a
blue-lifted shadow with hard contrast, and dawn and dusk the reverse. Ground R-B,
lit vs shadowed: 07:00 +42.8/+14.2 -> +48.9/+15.1, 13:00 +32.2/+14.8 -> +25.2/+2.9.
Gain is left alone deliberately: the grade is `c * gain + lift * (1 - c)`, so warming
it warms the whole frame, and warming it at noon made one o'clock yellower than seven
in the morning - the opposite of the point.
The visible sky is relit. Turning a photograph on its axis does not change what
colour it was taken at, so every sunset had a mid-morning blue overhead. An analytic
sky supplies the chroma and the photograph keeps the luminance: the cloud stays where
it is and goes orange at dusk, the zenith goes deep blue at noon, and no second sky
is shipped. It fades out under the horizon and eases off under cloud.
The ground bounce follows the ground, crossing meadow to rock at the map's treeline
instead of being one green constant everywhere including above the scree.
R3D_NOAIR=1 restores all of it, so a before-and-after comes from one binary at one
hour; it joins R3D_NOCLOUD / R3D_NOSHADOW / R3D_NOGI.
Verified on macOS OpenGL, macOS Vulkan (MoltenVK) and Windows Vulkan (RTX 3070 Ti).
Backends agree: mean difference 0.15-0.88/255 within a machine. Across machines the
ORIGINAL renderer already differed by 5.02/255 at 19:12 and this build differs by
2.80, so cross-platform variance is pre-existing and did not grow. 400 frames: GL
7.4 s before and after, VK 7.0 s before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reflecting body is clipped to its ellipse like every other unless it is an
unbounded sea, so a map whose reflection belongs to its lake (Maroon Lake, once
its sea sits below it) does not draw that lake's level over every hollow in the
survey. The terrain's wet shore and its forest and scree gates read the carved
lake's line inside its outline (u_lake), the rule grass already used. SPIR-V
regenerated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tersun.frag rasterised every terrain patch a second time into a screen buffer, because on OpenGL the
cascade read inside terrain.frag fell off a driver cliff (about 6 ms a frame). Vulkan has no such
cliff: its terrain programs are the SUN_INLINE variant, which evaluates the same two tiers with the
same normal and cross-fade in the ground's own shader, and terrain_draw skips the pass - in the frame
and in the water reflection. OpenGL keeps the pass. R3D_SUN_PASS=1 keeps it on Vulkan, for comparing.
Mac Vulkan town 1759 -> 1692 draws; frames within 2/255 of the pass (15331 px, float rounding). PC camp
1984 -> 1882 draws, 3.9 s for 400 frames either way, self-tests 59/59, validation 0. OpenGL frames
byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>