--- name: threejs-rain-snow-and-wet-surfaces description: Couple Three.js WebGPU/TSL rain, snow, and wet surfaces. Use for airborne precipitation motion, physical surface deposition, persistent snow or wetness state, weather-driven ripple normals, or impact splashes. --- # Rain, Snow, and Wet Surfaces Couple appearance to causes: one time source and wind field drive airborne precipitation, while one owner per receiver integrates deposited rain or snow. Visual particle count samples the weather; it never sets the deposited mass. Use Three.js r185 `WebGPURenderer`, TSL, storage nodes, node materials, and the node post stack. Initialize the renderer before allocating compute or storage: ```js await renderer.init(); if (renderer.backend.isWebGPUBackend !== true) { throw new Error('This weather path requires the WebGPU backend.'); } ``` For cross-system precipitation or receiver state, declare its units, frame, time interval, authority/version, support, validity, and reset semantics; invoke `$threejs-choose-skills` when ownership spans skills. ## Build sequence ### 1. Name the owners and units Declare: - one monotonically sampled time source and update interval in seconds; - one air-velocity field in world metres per second, including its frame, support, cadence, and validity; - temperature in kelvin and a named humidity convention when phase, melt, or evaporation depends on them; - precipitation forcing as liquid/ice mass-area flux in `kg m^-2 s^-1` over a physical receiver support; - one receiver-state owner for each liquid or snow inventory; - one owner for HDR presentation, tone mapping, and output conversion. Treat cloud appearance and causal precipitation as separate branches. An appearance-only cloud may coordinate art direction. A causal cloud source publishes a mass flux or airborne inventory with a fall-delay/transport model; rain transports it to receivers on a later ordered stage. This step is complete when every cause and persistent state has exactly one owner, every exchanged quantity has units and a frame, and every producer is sampled within its stated validity. ### 2. Select motion before allocating state Use immutable seeds and analytic vertex motion when position is an exact function of seed, time, and integrated wind. Use recurrent GPU-resident state when turbulence, collisions, feedback, or path history affects the next state. Authored time-varying wind remains analytic only when its displacement integral is available; multiplying the current wind by total elapsed time makes all particles jump when wind changes. For unbounded visual weather, stream camera-centred cells whose identities, spawn phases, and trajectories are hashed in stable world space. For localized weather, use a world-anchored bounded volume with an intentional boundary. Impacts and accumulation always use world-stable receiver cells independent of the visual pool. Read [precipitation motion](references/precipitation-motion.md) when choosing analytic versus recurrent motion, implementing compute updates, generating physical impacts, or budgeting precipitation work. This step is complete when each requested force or collision maps to an exact analytic term or recurrent state field, and a camera-translation test changes only the visible cell set—not particle phase, impact position, or accumulated receiver state. ### 3. Separate deposition from visual sampling Integrate an intensive flux with physical-area quadrature: ```text sum_i A_i = represented receiver area deltaMass = deltaTime * sum_i(massFlux_i * A_i) ``` Each `A_i` includes the receiver chart Jacobian and has units of square metres. When sparse impacts represent the already-integrated transfer, partition that extensive mass and momentum across impacts so their sum closes the parent transfer exactly once. Keep rendered streak/flake density, sprite size, and visual LOD outside this calculation. When implementing or independently auditing this physical-deposition branch, read or copy the [physical-area deposition oracle](examples/physical-area-deposition.mjs). It accepts varying quadrature areas, never normalizes impact partitions, and checks the receiver owner's extensive-mass ledger. It is a CPU oracle, not a runtime state owner. This step is complete when changing visual particle count and visual LOD under the same forcing trace leaves deposited mass and momentum unchanged, and changing receiver cadence leaves the time integral unchanged within the named numerical tolerance. ### 4. Order whole-grid updates Use this causal order: ```text latch time, wind, and precipitation forcing -> advance analytic or recurrent airborne precipitation -> resolve and bin impacts -> publish deposition/impact transfer -> integrate receiver liquid and snow state -> commit receiver state -> build render projection ``` Split solver, collision/binning, scan/compaction, indirect-argument, and receiver passes into ordered dispatches when later stages consume earlier whole-grid results. A workgroup barrier orders one workgroup; an explicit pass or queue dependency orders the grid. Treat r185 `computeAsync()` as initialization/enqueue convenience rather than a GPU-completion fence. Keep steady-frame state on the GPU; use host readback only outside frame-critical execution. This step is complete when every read names the dispatch or committed snapshot that produced it, the receiver integrates after deposition is resolved, and a fixed forcing replay produces the same committed inventory at every supported update cadence. ### 5. Project one committed receiver state Read [receiver weathering](references/receiver-weathering.md) when implementing snow accumulation, wetness, puddles, ripple normals, splashes, or their material response. Build all surface effects from the committed receiver snapshot: - derive snow displacement and snow normals from the same height field; - sample object snow in stable model space and gate it with the transformed world-space support normal; - apply the early wetness response before enabling heavy-rain ripple normals; - use one wetness/puddle mask for roughness, absorption, ripple eligibility, splash intensity, and mask diagnostics; - orient splashes from world-space receiver normals and reject candidates that are downward-facing, unsupported, or hidden under the selected visibility policy. This step is complete when the snow-position and snow-normal diagnostics agree, wetness remains visible with ripples disabled, and transformed or occluded receiver tests accept only supported world-space splash candidates. ### 6. Present and falsify Use `MeshStandardNodeMaterial` or `MeshPhysicalNodeMaterial` slots for color, roughness, normal, opacity, and displacement. Present through one `RenderPipeline`. Tag encoded base-color textures as `SRGBColorSpace`; treat normal, roughness, mask, noise, LUT, ripple, and weather fields as data. Keep HDR buffers linear until the single tone-map/output conversion owner. Expose diagnostics for forcing revision and age, motion/cells, impacts, deposited mass, receiver terms, snow height/normals, wetness/ripples, splash orientation/visibility, data-texture interpretation, and final output. Measure full-frame and paired weather-on/off CPU/GPU p50/p95, transparent overdraw, hot bytes per frame, active impact tiles, and peak live storage on the named target. This step is complete when the final and diagnostic views pass the observable checks below, disabling weather restores the baseline image, resize/rebuild preserves owners and resets transient history, and disposal releases every weather-owned buffer, texture, material, and pass. ## Observable checks | Observable | Failure signature and likely cause | | --- | --- | | Camera translation preserves precipitation phase and impacts | Cell identity or impact support is camera-relative. | | Wind changes bend future motion without translating the whole field | Current wind was multiplied by elapsed time instead of integrated. | | Streak length follows fall speed/exposure, and drift follows the shared wind | Sprite geometry or a surface branch uses a separate clock or wind. | | Unbounded weather has no emitter seam; localized weather has an intentional edge | The selected visual-domain contract is incomplete. | | Snow silhouette and lighting move together | Displacement and normals come from different fields. | | Animated objects keep snow attached to their surfaces | Coverage is sampled in world rather than stable model space. | | Wet surfaces change before heavy-rain ripples appear | Roughness is gated by the ripple mask instead of receiver wetness. | | Splashes stay on supported visible faces after transforms | Candidate normals or visibility are evaluated in local/unstable space. | | Dense recurrent weather has no per-drop object loop or full hot-buffer upload | State or presentation left the GPU-resident branch. | | Data fields preserve values and final color has one display transform | A data texture is decoded as color, or output conversion runs twice. | ## Routing boundary Use `$threejs-water-optics` for bounded water bodies, refraction, caustics, and Beer-Lambert thickness. Use `$threejs-particles-trails-and-effects` for non-weather particle systems. Use `$threejs-dynamic-surface-effects` for screen-space touch or clearing histories. Use `$threejs-image-pipeline` for scene-wide HDR/post ownership and `$threejs-scalable-real-time-shadows` for large-scene shadow allocation. This skill owns precipitation transport and weather-specific receiver projections; the route-selected receiver owner owns the persistent liquid or snow inventory.