feat(viz): foliage wind sway on billboard trees#19
Conversation
First slice of PR 4 (foliage wind + CSM). Adds a shared `uTime` uniform across every tree billboard and injects a vertex-shader displacement that sways crowns with the wind while trunks stay planted. Technique: * `onBeforeCompile` injection into the existing `MeshStandardMaterial` at `client/treeSprites.ts:buildBillboardMaterial` — preserves the auto-generated shadow depth pass that a `ShaderMaterial` swap would lose. * Displacement is a two-frequency sum (`sin(t*1.3) + sin(t*3.1 + z·f)`), scaled by `uv.y` so trunks stay still. A per-tree phase offset is keyed on world XZ so a patch of trees doesn't move in lockstep. * Amplitude deliberately small (0.15 world units) to keep alpha-test seams tight between neighbouring instances. * `customProgramCacheKey='resq-billboard-wind-v1'` so every billboard shares one compiled program — no shader-cache blowup. Wiring: * `treeSprites.ts` exports `WIND_UNIFORMS` + `tickWind(dt)`. * `app.ts` registers a tick callback on the scene render loop to advance `uTime`. Cascaded shadow maps deferred to a separate PR — they touch every shadow-casting material and want independent validation. Verified: tsc clean, vite build green, `dotnet build -c Release` / format / 82 tests all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 26 minutes and 27 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a wind animation system for foliage billboards by injecting custom vertex shader logic into the billboard materials and managing a global wind clock. A significant issue was identified where the use of local vertex coordinates for wind phase calculations causes all trees to sway in synchronization and leads to visual shearing of the billboard geometry. The feedback recommends using world-space coordinates from the instance or model matrix to ensure varied motion and maintain visual integrity.
| float _resqWindT = uTime + dot(vec2(transformed.x, transformed.z), vec2(0.13, 0.17)); | ||
| float _resqWindSway = sin(_resqWindT * 1.3) * 0.65 | ||
| + sin(_resqWindT * 3.1 + transformed.z * uWindFreq) * 0.35; | ||
| transformed.x += _resqWindSway * uWindAmp * uv.y; | ||
| transformed.z += _resqWindSway * uWindAmp * 0.6 * uv.y; |
There was a problem hiding this comment.
The current implementation uses local vertex coordinates (transformed.x, transformed.z) to calculate the wind phase offset. Since all billboard instances share the same base geometry, this results in two issues:
- Lockstep Motion: Every tree in the scene will sway in perfect synchronization, failing the stated goal of "inter-tree variation keyed on world x+z".
- Visual Shearing: Because the phase varies across the local width of the billboard, the two perpendicular quads of the cross-billboard will move out of sync with each other, causing the tree to "split" or shear during motion.
To fix this, the phase offset should be keyed on the instance's world position (the translation column of the instanceMatrix).
#ifdef USE_INSTANCING
float _resqWindT = uTime + dot(instanceMatrix[3].xz, vec2(0.13, 0.17));
float _resqWindSway = sin(_resqWindT * 1.3) * 0.65
+ sin(_resqWindT * 3.1 + instanceMatrix[3].z * uWindFreq) * 0.35;
#else
float _resqWindT = uTime + dot(modelMatrix[3].xz, vec2(0.13, 0.17));
float _resqWindSway = sin(_resqWindT * 1.3) * 0.65
+ sin(_resqWindT * 3.1 + modelMatrix[3].z * uWindFreq) * 0.35;
#endif
transformed.x += _resqWindSway * uWindAmp * uv.y;
transformed.z += _resqWindSway * uWindAmp * 0.6 * uv.y;* ci: switch client job to node-ci.yml reusable (org PR #19 landed) Swap the hand-rolled frontend install+typecheck+build steps for the org-wide node-ci.yml reusable. The prior adoption attempt failed with "Dependencies lock file is not found" because the reusable hardcoded `cache-dependency-path: package-lock.json` (repo root) — viz's lockfile lives at src/ResQ.Viz.Web/package-lock.json. resq-software/.github#19 parameterised `cache-dependency-path` on the reusable; this PR consumes the merged SHA. Call node-ci.yml directly rather than via required.yml because viz already calls required.yml as `lang: dotnet` for the backend stream; a second `lang: node` dispatch would duplicate the security-scan job. client-budget stays as a companion — the reusable exposes neither bundle outputs nor artifact upload, and the bundle-size ceiling is viz-specific policy. Ref pinned to merge commit 23ce94eabddf963835624451e89baca7ac9db541. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * deps: force uuid to ^14 via override (GHSA-w5hq-g745-h8pq) osv-scanner surfaced GHSA-w5hq-g745-h8pq (medium) against [email protected], pulled transitively through [email protected] → uuid: ^13.0.0. effect hasn't bumped its caret yet. Add a top-level npm `overrides` entry to force uuid@^14.0.0. uuid's public surface (v4()/validate()/parse()) is unchanged across the 13→14 major, so effect's usage is unaffected. Lockfile regenerated; typecheck + vite build both clean. Bundle size unchanged (776 KB JS, within 800 KB budget). This is an unrelated advisory bundled here only because it landed in osv.dev between main's last build and this branch, and the gate now blocks the CI-reusable-adoption signal we're verifying. Remove the override once `effect` upstream bumps its `uuid: ^13.0.0` range. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Summary
First slice of PR 4 in the visual upgrade roadmap. Adds a shared
uTimeuniform across every tree billboard and injects a vertex-shader displacement that sways crowns with the wind while trunks stay planted.Technique
onBeforeCompileinjection into the existingMeshStandardMaterialatclient/treeSprites.ts:buildBillboardMaterial— preserves the auto-generated shadow depth pass that aShaderMaterialswap would lose.sin(t·1.3) * 0.65 + sin(t·3.1 + z·f) * 0.35, scaled byuv.yso trunks stay still. A per-tree phase offset keyed on world XZ prevents patches of trees from moving in lockstep.customProgramCacheKey='resq-billboard-wind-v1'so every billboard shares one compiled program — no shader-cache blowup.Wiring
treeSprites.tsexportsWIND_UNIFORMS+tickWind(dt).app.tsregisters a render-loop tick callback that callstickWind(dt)to advanceuTime.Scope split
Cascaded shadow maps are NOT in this PR. They touch every shadow-casting material (terrain, drones, trees) and want independent validation — one failed registration and shadows silently vanish. Shipping wind alone keeps the blast radius at one file.
Changes
client/treeSprites.tsWIND_UNIFORMS+tickWind(dt)+ shader-injectiononBeforeCompileclient/app.tsviz.addTickCallback((dt) => tickWind(dt))Bundle + perf
No bundle size impact (~15 lines of embedded GLSL). Vertex-shader cost is one
sin+ onecosper vertex per billboard; negligible on WebGL.Local verification
Test plan
MeshStandardMaterialpreserves the depth pass)🤖 Generated with Claude Code