layer_flutter(l, v) gives a scatter layer a per-leaf tremble. The vertex stage offsets
each leaf by a phase taken from its own place on the card, so neighbouring leaves are
never in step, and writes the result out as a varying the fragment stage uses to flash
the leaf's pale underside as it turns - which is the part that reads, since a still
frame of a tremble is a still frame of nothing. One uniform, one varying, no extra pass.
And the sway itself was measured in METRES: hgt * hgt * 0.35 is right for a 40 cm
flower and puts ten metres of sideways into a 14 m trunk, so every tall tree in the
valley stood bent over like a fishing rod. It is a fraction of the model's own height
now - the tip moves a few per cent of the tree whatever the tree is, and the base does
not move at all.
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>
Foliage roughness was pinned to 1.0 and grazing Fresnel switched off, so nothing green
in the game had a highlight anywhere. The glint off waxy leaves and wet needles is most
of what makes a stand look alive rather than painted.
The reason it was off is real: a crown is card quads, and at a grazing angle the card's
normal is a lie, so a specular lobe frosted whole crowns white against the sky. The sheen
returns as its own term gated on exactly that - it fades as the card turns edge-on, which
is where its normal stops meaning anything. A tight lobe for the glint, a weak wide one
for the waxy rim, nothing at the angles that frosted.
Translucency reaches 260 m rather than 140, and a dense crown passes 0.45 of it rather
than 0.3, so a backlit stand glows for as far as you can see it.
400 frames: GL 7.1 s, VK 7.4 s. Backends agree to 0.14/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>
The chunked grass path draws each chunk as one mesh-shader dispatch when the
setting asks and the card has VK_EXT_mesh_shader: work group y is a tile, x a
batch of 16 of its blades, and a blade the placement, density, frustum, water
or slope tests reject emits nothing - the instanced path still runs its eight
vertices to a degenerate position. grass.mesh generates the same blades as
grass.vert (grass_blade_mesh(4)'s rows, the same hashes, sway and lighting
normal), capped at the instanced path's 65535 a tile.
Vulkan: VK_EXT_mesh_shader with meshShader, and maintenance4 (glslang's mesh
stages declare LocalSizeId); vkCmdDrawMeshTasksEXT looked up per device, as the
Streamline interposer exports none; a *.mesh program's pipeline takes the mesh
stage and no vertex input, its bindings the mesh stage bit. gpu_has_mesh,
gpu_draw_mesh_tasks; r3d_mesh_grass and R3D_MESH_GRASS / R3D_NO_MESH.
bin/ludic-dev rebuilt: the committed binary predated the shader tool's mesh
support and compiled grass.mesh as a vertex stage.
PC (RTX 3070 Ti): the camp matches the chunked path; validation only the
no-window present-id message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>