feat(render3d): one wind, and everything in the valley is moved by it
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>
This commit is contained in:
parent
c5693db0e4
commit
9dff8f21bd
116 changed files with 95 additions and 7 deletions
25
changes/one-wind.md
Normal file
25
changes/one-wind.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
One wind, and everything in the valley is moved by it.
|
||||||
|
|
||||||
|
The grass had a gust built from two sines and the trees had NO gust term at all - just a
|
||||||
|
per-instance wobble - so the meadow rippled and the canopy above it swayed to an unrelated
|
||||||
|
rhythm and nothing ever crossed the valley. A real gust is a WAVE: you watch it come over
|
||||||
|
the grass, it reaches you, and it goes on into the trees behind you.
|
||||||
|
|
||||||
|
wind.glsl is prepended to EVERY stage, so the grass's vertex shader, the crown's vertex
|
||||||
|
shader and the water's fragment shader read the same field at the same world position and
|
||||||
|
cannot drift apart. No uniforms are declared in it 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 - so the caller passes what it already has.
|
||||||
|
|
||||||
|
Two numbers mattered. The gust's WAVELENGTH has to fit inside a view or nothing is ever
|
||||||
|
seen to travel: the first cut was 209 m crest to crest, longer than the meadow you can see,
|
||||||
|
so the whole frame sat in one phase and read as everything breathing together. It is 74 m
|
||||||
|
now and a front crosses a normal view in a couple of seconds. And `stiff` lets one field
|
||||||
|
move a blade a long way and a bole hardly at all.
|
||||||
|
|
||||||
|
R3D_DEBUG_WIND=1 paints the field on the ground, which is the only way to SEE a gust in a
|
||||||
|
still: two shots a moment apart show the fronts and show them moving.
|
||||||
|
|
||||||
|
Measured: no cost. 400 frames, GL 6.9 s, VK 7.2 s.
|
||||||
|
|
@ -17,6 +17,7 @@ var r3d_root_found: bool = false
|
||||||
var r3d_global_defs: string = ""
|
var r3d_global_defs: string = ""
|
||||||
var r3d_noise_src: string = null
|
var r3d_noise_src: string = null
|
||||||
var r3d_lighting_src: string = null
|
var r3d_lighting_src: string = null
|
||||||
|
var r3d_wind_src: string = null
|
||||||
|
|
||||||
function r3d_set_paths(root: string, assets: string) -> void { r3d_root = root; r3d_assets = assets }
|
function r3d_set_paths(root: string, assets: string) -> void { r3d_root = root; r3d_assets = assets }
|
||||||
|
|
||||||
|
|
@ -51,7 +52,11 @@ function r3d_shader_file(name: string) -> string {
|
||||||
function r3d_shader_src(name: string, defines: string, is_frag: bool) -> string {
|
function r3d_shader_src(name: string, defines: string, is_frag: bool) -> string {
|
||||||
if r3d_noise_src == null { r3d_noise_src = r3d_shader_file("noise.glsl") }
|
if r3d_noise_src == null { r3d_noise_src = r3d_shader_file("noise.glsl") }
|
||||||
if r3d_lighting_src == null { r3d_lighting_src = r3d_shader_file("lighting.glsl") }
|
if r3d_lighting_src == null { r3d_lighting_src = r3d_shader_file("lighting.glsl") }
|
||||||
var s = "#version 410 core\n" + r3d_global_defs + defines
|
# the wind goes to BOTH stages: the grass and the crowns move in the vertex shader, the water
|
||||||
|
# ripples in the fragment one, and they have to be reading the same field or the meadow and
|
||||||
|
# the lake disagree about the weather
|
||||||
|
if r3d_wind_src == null { r3d_wind_src = r3d_shader_file("wind.glsl") }
|
||||||
|
var s = "#version 410 core\n" + r3d_global_defs + defines + r3d_wind_src
|
||||||
if is_frag { s = s + r3d_noise_src + r3d_lighting_src }
|
if is_frag { s = s + r3d_noise_src + r3d_lighting_src }
|
||||||
return s + r3d_shader_file(name)
|
return s + r3d_shader_file(name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,11 @@ void main() {
|
||||||
if (far) { bw = max(bw, spacing * 0.35); tall = min(tall, spacing * 0.3); }
|
if (far) { bw = max(bw, spacing * 0.35); tall = min(tall, spacing * 0.3); }
|
||||||
vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (0.6 + 0.8 * h4));
|
vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (0.6 + 0.8 * h4));
|
||||||
vec3 n = vec3(0.0, 0.3, 1.0);
|
vec3 n = vec3(0.0, 0.3, 1.0);
|
||||||
float gust = sin(xz.x * 0.09 + u_time * 1.1) * 0.5 + sin(xz.y * 0.13 - u_time * 0.8 + xz.x * 0.05) * 0.5;
|
// The shared field (wind.glsl), so the gust that crosses this meadow is the same gust that
|
||||||
|
// reaches the trees behind it a moment later. The two sines that used to live here were a
|
||||||
|
// standing wobble: the grass moved, but nothing ever travelled.
|
||||||
float ph = u_time * 1.7 + seed * 6.2831 + xz.x * 0.05 + xz.y * 0.07;
|
float ph = u_time * 1.7 + seed * 6.2831 + xz.x * 0.05 + xz.y * 0.07;
|
||||||
float sway = (sin(ph) * 0.6 + sin(ph * 2.3 + 1.0) * 0.4 + gust) * u_wind;
|
float sway = windSway(xz, u_time, ph, 0.0) * u_wind;
|
||||||
float hgt = max(p.y, 0.0);
|
float hgt = max(p.y, 0.0);
|
||||||
p.x += sway * hgt * hgt * 0.35;
|
p.x += sway * hgt * hgt * 0.35;
|
||||||
p.z += sway * hgt * hgt * 0.15 * cos(ph * 0.7);
|
p.z += sway * hgt * hgt * 0.15 * cos(ph * 0.7);
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,11 @@ void main() {
|
||||||
// sway grows with height above the base; gust phase from the instance seed
|
// sway grows with height above the base; gust phase from the instance seed
|
||||||
float hgt = max(p.y, 0.0);
|
float hgt = max(p.y, 0.0);
|
||||||
float ph = u_time * 1.7 + i_rot.z * 6.2831 + i_pos.x * 0.05 + i_pos.z * 0.07;
|
float ph = u_time * 1.7 + i_rot.z * 6.2831 + i_pos.x * 0.05 + i_pos.z * 0.07;
|
||||||
float sway = (sin(ph) * 0.6 + sin(ph * 2.3 + 1.0) * 0.4) * u_wind * i_rot.w;
|
// A crown had NO gust at all - only this per-instance wobble - so the canopy swayed to a
|
||||||
|
// rhythm of its own while the meadow under it rippled to another. It is the same field now,
|
||||||
|
// read at the tree's own place in the world, so a gust crosses the grass and goes on into
|
||||||
|
// the trees. `stiff` is 0.35 because a bole resists what a blade cannot.
|
||||||
|
float sway = windSway(i_pos.xz, u_time, ph, 0.35) * u_wind * i_rot.w;
|
||||||
p.x += sway * hgt * hgt * 0.35;
|
p.x += sway * hgt * hgt * 0.35;
|
||||||
p.z += sway * hgt * hgt * 0.15 * cos(ph * 0.7);
|
p.z += sway * hgt * hgt * 0.15 * cos(ph * 0.7);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue