Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(viz): foliage wind sway on billboard trees#19

Merged
WomB0ComB0 merged 1 commit into
mainfrom
feat/foliage-wind
Apr 20, 2026
Merged

feat(viz): foliage wind sway on billboard trees#19
WomB0ComB0 merged 1 commit into
mainfrom
feat/foliage-wind

Conversation

@WomB0ComB0

Copy link
Copy Markdown
Member

Summary

First slice of PR 4 in the visual upgrade roadmap. 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) * 0.65 + sin(t·3.1 + z·f) * 0.35, scaled by uv.y so trunks stay still. A per-tree phase offset keyed on world XZ prevents patches of trees from moving in lockstep.
  • Amplitude is deliberately small (0.15 world units) so alpha-test seams between adjacent instances don't crack during motion.
  • 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 render-loop tick callback that calls tickWind(dt) to advance uTime.

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

File Change
client/treeSprites.ts WIND_UNIFORMS + tickWind(dt) + shader-injection onBeforeCompile
client/app.ts One import + one viz.addTickCallback((dt) => tickWind(dt))

Bundle + perf

No bundle size impact (~15 lines of embedded GLSL). Vertex-shader cost is one sin + one cos per vertex per billboard; negligible on WebGL.

Local verification

tsc --noEmit                                   → clean
vite build (via Release MSBuild)               → green
dotnet build ResQ.Viz.sln -c Release           → 0 / 0
dotnet format --verify-no-changes --severity warn → exit 0
dotnet test -c Release                         → 82 / 82 passing

Test plan

  • Load alpine scenario — tree crowns visibly sway in a low-frequency pattern
  • Zoom in on a cluster of trees — neighbours don't move in perfect sync (per-tree XZ phase)
  • Switch to coastal — same wind applies to the deciduous billboards
  • Check shadows — tree shadows should still render (the MeshStandardMaterial preserves the depth pass)

🤖 Generated with Claude Code

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]>
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@WomB0ComB0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 27 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e25f42e-e6bf-4afc-8db9-37004ea174d0

📥 Commits

Reviewing files that changed from the base of the PR and between 0354a67 and b060039.

📒 Files selected for processing (2)
  • src/ResQ.Viz.Web/client/app.ts
  • src/ResQ.Viz.Web/client/treeSprites.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/foliage-wind

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@WomB0ComB0 WomB0ComB0 merged commit d0a1f59 into main Apr 20, 2026
35 checks passed
@WomB0ComB0 WomB0ComB0 deleted the feat/foliage-wind branch April 20, 2026 21:53

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +74 to +78
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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:

  1. 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".
  2. 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;

WomB0ComB0 added a commit that referenced this pull request Apr 23, 2026
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant