@genex-ai/cli-demo 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +13 -12
  2. package/dist/index.js +410 -154
  3. package/package.json +10 -11
  4. package/templates/README.md +8 -7
  5. package/templates/skills/genex-ai-model/SKILL.md +5 -5
  6. package/templates/skills/genex-ai-sfx/SKILL.md +1 -1
  7. package/templates/skills/genex-ai-skybox/SKILL.md +2 -2
  8. package/templates/skills/genex-ai-texture/SKILL.md +1 -1
  9. package/templates/skills/genex-getting-started/SKILL.md +10 -6
  10. package/templates/skills/genex-threejs-embed-auth/SKILL.md +174 -0
  11. package/templates/skills/genex-threejs-multiplayer/SKILL.md +193 -100
  12. package/templates/skills/genex-threejs-multiplayer/references/genre-recipes.md +115 -0
  13. package/templates/skills/genex-threejs-multiplayer/references/realtime-patterns.md +138 -123
  14. package/templates/skills/genex-threejs-precipitation-surfaces/SKILL.md +59 -0
  15. package/templates/skills/genex-threejs-precipitation-surfaces/references/precipitation-surfaces.md +181 -0
  16. package/templates/skills/genex-threejs-procedural-architecture/SKILL.md +2 -1
  17. package/templates/skills/genex-threejs-procedural-architecture/references/architecture-systems.md +1 -1
  18. package/templates/skills/genex-threejs-procedural-fields/SKILL.md +0 -5
  19. package/templates/skills/genex-threejs-procedural-geometry/SKILL.md +0 -5
  20. package/templates/skills/genex-threejs-procedural-materials/SKILL.md +2 -17
  21. package/templates/skills/genex-threejs-procedural-vegetation/SKILL.md +11 -1
  22. package/templates/skills/genex-threejs-procedural-vfx/SKILL.md +3 -1
  23. package/templates/skills/genex-threejs-shadow-systems/SKILL.md +0 -5
  24. package/templates/skills/genex-threejs-skill-router/SKILL.md +10 -5
  25. package/templates/skills/genex-threejs-skill-router/references/routing-map.md +9 -6
  26. package/templates/skills/genex-threejs-spectral-ocean/SKILL.md +11 -1
  27. package/templates/skills/genex-threejs-temporal-surfaces/SKILL.md +3 -1
  28. package/templates/skills/genex-threejs-water-optics/SKILL.md +14 -2
@@ -1,113 +1,56 @@
1
- # Realtime patterns: interpolation, prediction, persistence
1
+ # Realtime patterns: rendering, objects, host, events, persistence
2
2
 
3
- The `@genex-ai/multiplayer` relay gives you the **latest** state of each player and
4
- nothing else no timestamps, no smoothing. These are the pieces you add on top so the
5
- game feels good. All code is plain TypeScript; nothing here is provided by the package.
3
+ The `@genex-ai/multiplayer` SDK smooths remote players **and shared objects** for you (client-side
4
+ entity interpolation on a shared clock). These are the patterns you build on top none of them
5
+ re-implement smoothing, because doing so is the mistake that makes games lag.
6
6
 
7
- ## Why you need this
7
+ ## The golden rule
8
8
 
9
- - You publish `me.set` at ~15 Hz but render at 60 fps. If you snap each remote player to
10
- their last received position, they move in visible 15 Hz steps and jump on packet
11
- jitter. **Fix: interpolation** render each remote player slightly in the past and
12
- lerp between their two surrounding snapshots.
13
- - Your *own* avatar would feel laggy if you waited for the server to echo your position
14
- back. **Fix: prediction** — apply input locally and render yourself from that
15
- immediately; the tick still publishes it for everyone else.
9
+ - **Remote players & objects:** draw `players.get(id).state` / `objects.get(id).state` directly.
10
+ Already smoothed.
11
+ - **Yourself & objects you own:** draw from your own local object. Never from the network echo.
16
12
 
17
- ## RemoteInterpolator (render-delay interpolation)
13
+ If you ever buffer `state` and lerp between snapshots, stop — you're rebuilding smoothing the SDK
14
+ already did, and stacking two smoothers adds visible lag.
18
15
 
19
- Buffers timestamped snapshots per remote player and samples a smoothed value ~100 ms in
20
- the past. Stamp snapshots with the local clock on arrival (we don't get server time).
21
-
22
- ```ts
23
- // interpolation.ts
24
- const RENDER_DELAY_MS = 100; // render remotes this far in the past; raise if jittery
25
- const BUFFER_MS = 1000; // keep ~1s of history
26
-
27
- type Vec = { x: number; y: number; z: number; yaw: number };
28
- type Snap = { t: number; v: Vec };
29
-
30
- function lerp(a: number, b: number, k: number) {
31
- return a + (b - a) * k;
32
- }
33
- // shortest-arc angle lerp (radians) — avoids spinning the wrong way across ±π
34
- function lerpAngle(a: number, b: number, k: number) {
35
- let d = (b - a) % (Math.PI * 2);
36
- if (d > Math.PI) d -= Math.PI * 2;
37
- if (d < -Math.PI) d += Math.PI * 2;
38
- return a + d * k;
39
- }
40
-
41
- export class RemoteInterpolator {
42
- private buffers = new Map<string, Snap[]>();
43
-
44
- /** Call whenever you read a remote player's latest state (e.g. each frame, or on 'change'). */
45
- push(id: string, v: Vec, now = performance.now()) {
46
- let buf = this.buffers.get(id);
47
- if (!buf) this.buffers.set(id, (buf = []));
48
- const last = buf[buf.length - 1];
49
- // de-dupe identical repeats (players is re-read every frame)
50
- if (last && last.v.x === v.x && last.v.z === v.z && last.v.yaw === v.yaw && last.v.y === v.y) return;
51
- buf.push({ t: now, v });
52
- const cutoff = now - BUFFER_MS;
53
- while (buf.length > 2 && buf[0].t < cutoff) buf.shift();
54
- }
55
-
56
- /** Smoothed value to render this frame, or null if we have nothing yet. */
57
- sample(id: string, now = performance.now()): Vec | null {
58
- const buf = this.buffers.get(id);
59
- if (!buf || buf.length === 0) return null;
60
- const target = now - RENDER_DELAY_MS;
61
- if (buf.length === 1 || target <= buf[0].t) return buf[0].v;
62
- if (target >= buf[buf.length - 1].t) return buf[buf.length - 1].v; // clamp (no extrapolation)
63
- for (let i = 0; i < buf.length - 1; i++) {
64
- const a = buf[i], b = buf[i + 1];
65
- if (target >= a.t && target <= b.t) {
66
- const k = (target - a.t) / (b.t - a.t || 1);
67
- return {
68
- x: lerp(a.v.x, b.v.x, k),
69
- y: lerp(a.v.y, b.v.y, k),
70
- z: lerp(a.v.z, b.v.z, k),
71
- yaw: lerpAngle(a.v.yaw, b.v.yaw, k),
72
- };
73
- }
74
- }
75
- return buf[buf.length - 1].v;
76
- }
77
-
78
- remove(id: string) {
79
- this.buffers.delete(id);
80
- }
81
- }
82
- ```
83
-
84
- **Tuning:** `RENDER_DELAY_MS` ≈ one tick interval + jitter (100 ms is safe for a 15 Hz
85
- tick). Lower = more responsive but more stutter on jitter. For sudden teleports (respawn,
86
- warp) clear that player's buffer and snap, so you don't lerp across the whole map.
87
-
88
- ## Complete movement example
16
+ ## Complete movement example (players)
89
17
 
90
18
  ```ts
91
19
  import * as THREE from "three";
92
20
  import { connect } from "@genex-ai/multiplayer";
93
21
  import { GENEX } from "./genex.config";
94
- import { RemoteInterpolator } from "./interpolation";
95
22
 
96
- type S = { x: number; z: number; yaw: number };
23
+ // Your published state EVERY synced field, including discrete ones like hp. `me.set`
24
+ // replaces your state wholesale, so the tick must send all of these every time (below).
25
+ type S = { x: number; z: number; q: number[]; hp: number };
97
26
 
98
27
  const room = await connect<S>({ url: GENEX.colyseusUrl, room: GENEX.slug });
99
- const interp = new RemoteInterpolator();
100
28
 
101
- // --- local player: prediction. Input mutates this; we render yourself from it. ---
102
- const me = { x: 0, z: 0, yaw: 0 };
29
+ // --- local player: input mutates this; we render yourself from it (zero latency) ---
30
+ // This ONE object holds everything you sync. Keep hp/ammo/etc. here too — see the warning below.
31
+ const me = { x: 0, z: 0, yaw: 0, hp: 100 };
103
32
  addEventListener("keydown", (e) => {
104
33
  if (e.key === "ArrowLeft") me.yaw += 0.1;
105
34
  if (e.key === "ArrowRight") me.yaw -= 0.1;
106
35
  if (e.key === "ArrowUp") { me.x += Math.sin(me.yaw); me.z += Math.cos(me.yaw); }
107
36
  });
108
37
 
109
- // --- publish at ~15 Hz (NOT per frame) ---
110
- const tick = setInterval(() => room.me.set(me), 66);
38
+ // rotation helper turn our yaw into the quaternion we publish (and send with shots, below)
39
+ const _q = new THREE.Quaternion();
40
+ const myQuat = (): number[] => _q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), me.yaw).toArray();
41
+
42
+ // --- publish at ~15 Hz (NOT per frame). Send the WHOLE state — a field you omit is deleted. ---
43
+ const tick = setInterval(() => {
44
+ room.me.set({ x: me.x, z: me.z, q: myQuat(), hp: me.hp });
45
+ }, 66);
46
+ ```
47
+
48
+ > **`me.set` replaces your state wholesale.** Every tick must include EVERY field — position,
49
+ > rotation, AND discrete values like `hp`/`ammo`. A field you leave out is gone on the wire, so
50
+ > keep one local `me` object with all of them and publish it whole each tick. (Same for
51
+ > `objects.set`.)
52
+
53
+ ```ts
111
54
 
112
55
  // --- meshes, one per id ---
113
56
  const meshes = new Map<string, THREE.Object3D>();
@@ -119,84 +62,156 @@ function meshFor(id: string) {
119
62
  room.on("leave", (id) => {
120
63
  const m = meshes.get(id);
121
64
  if (m) { scene.remove(m); meshes.delete(id); }
122
- interp.remove(id);
123
65
  });
124
66
 
125
67
  function frame() {
126
- // yourself: render from local predicted state (zero latency)
127
- meshFor(room.id).position.set(me.x, 0, me.z);
128
- meshFor(room.id).rotation.y = me.yaw;
68
+ // yourself: render from the local object (zero latency)
69
+ const self = meshFor(room.id);
70
+ self.position.set(me.x, 0, me.z);
71
+ self.rotation.y = me.yaw;
129
72
 
130
- // everyone else: feed the interpolator, render the smoothed sample
73
+ // everyone else: draw state DIRECTLY it is already smoothed
131
74
  for (const [id, p] of room.players) {
132
75
  if (id === room.id) continue;
133
- interp.push(id, { x: p.state.x ?? 0, y: 0, z: p.state.z ?? 0, yaw: p.state.yaw ?? 0 });
134
- const v = interp.sample(id);
135
- if (!v) continue;
136
76
  const m = meshFor(id);
137
- m.position.set(v.x, v.y, v.z);
138
- m.rotation.y = v.yaw;
77
+ m.position.set(p.state.x ?? 0, 0, p.state.z ?? 0);
78
+ if (p.state.q) m.quaternion.fromArray(p.state.q); // slerped for you by the SDK
139
79
  }
140
80
 
141
81
  renderer.render(scene, camera);
142
82
  requestAnimationFrame(frame);
143
83
  }
144
84
  frame();
145
-
146
- // cleanup if you ever tear down: clearInterval(tick); room.leave();
147
85
  ```
148
86
 
149
- ## Custom events (shots, emotes, chat)
87
+ No interpolation buffer, no render-delay constant, no lerp of remote state — the SDK owns all of it.
88
+
89
+ ## A shared object (the ball) — `objects`, not `shared`
150
90
 
151
- For one-off actions that aren't part of continuous state, use `send` it doesn't belong
152
- in `me.set` (which is your *current* state, not events):
91
+ A ball belongs to no player. On `objects`, exactly one client **owns** it at a time (server-
92
+ enforced), the owner simulates it, and everyone else reads it auto-smoothed — on the same
93
+ interpolation as a player. The stream is keyed by the object id, so it stays continuous when
94
+ ownership changes (no snap), and the object survives its owner leaving (reassigned to the host).
153
95
 
154
96
  ```ts
155
- // shooter:
156
- room.send("shot", { from: room.id, x: me.x, z: me.z, yaw: me.yaw });
97
+ // ball.ts
98
+ const ball = { x: 0, y: 0.5, z: 0, vx: 0, vy: 0, vz: 0 }; // the owner's local sim
157
99
 
158
- // everyone else:
159
- room.on("shot", (msg: any) => spawnTracer(msg.x, msg.z, msg.yaw));
100
+ // claim on contact (you kicked it) — NOT every frame
101
+ function kick(dir: THREE.Vector3, power: number) {
102
+ room.objects.claim("ball");
103
+ ball.vx += dir.x * power; ball.vz += dir.z * power;
104
+ }
105
+
106
+ // in your fixed tick: if you own it, simulate + publish it (flat fields so it smooths)
107
+ function publishBall() {
108
+ const view = room.objects.get("ball");
109
+ if (view?.isMine) {
110
+ stepBallPhysics(ball); // your own integrator or Rapier
111
+ room.objects.set("ball", { x: ball.x, y: ball.y, z: ball.z });
112
+ }
113
+ }
114
+
115
+ // render: owner draws its local sim (zero latency); everyone else draws the smoothed state
116
+ function drawBall() {
117
+ const view = room.objects.get<{ x: number; y: number; z: number }>("ball");
118
+ if (!view) return;
119
+ ballMesh.position.set(view.state.x ?? 0, view.state.y ?? 0, view.state.z ?? 0);
120
+ }
121
+ ```
122
+
123
+ Rules that keep it correct:
124
+
125
+ - **Claim on interaction, not continuously.** Whoever last kicked owns and simulates it.
126
+ - **Keep object fields flat** (`x/y/z`, a 4-number quaternion). Nested objects snap instead of glide.
127
+ - **Only the owner's `set` lands** — the relay drops writes from non-owners, so you never get two
128
+ clients fighting over the ball. You don't need to check "am I owner" before drawing, only before
129
+ simulating.
130
+ - **Transient objects** (bullets, pickups): `room.objects.remove(id)` when they expire, so they
131
+ don't pile up.
132
+
133
+ ## Host authority (scores, rounds, world)
134
+
135
+ One client is `room.host` (the first joiner, re-elected on leave). Let only the host write agreed
136
+ state, so there's a single source of truth — no "who increments the score" races:
137
+
138
+ ```ts
139
+ function updateHud() {
140
+ scoreEl.textContent = String(room.shared.get("score") ?? 0); // everyone reads
141
+ }
142
+ function goal(team: "a" | "b") {
143
+ if (!room.isHost) return; // only the host writes
144
+ const key = `score_${team}`;
145
+ room.shared.set(key, (Number(room.shared.get(key)) || 0) + 1);
146
+ }
147
+ room.on("host", () => {/* host migrated — the new host takes over writing */});
160
148
  ```
161
149
 
162
- ## Shared room state (scores, round, world)
150
+ `room.on('shared', …)` fires when a key is **first set**, not on every change — for a live value,
151
+ read `room.shared.get(...)` in your loop (as `updateHud` does) rather than relying on the event.
152
+
153
+ ## Custom events (shots, emotes, chat)
154
+
155
+ For one-off actions that aren't continuous state, use `send`. The shooter judges the hit **locally**
156
+ against what it sees (favor-the-shooter) and announces it; the victim applies its own damage.
163
157
 
164
- `shared` is for state everyone agrees on, not per-player position:
158
+ **`send` reaches only OTHER clients it never echoes back to you.** So apply your own action's
159
+ visible effect *directly* at fire time; use `on(...)` only to render everyone else's:
165
160
 
166
161
  ```ts
167
- room.shared.set("round", (Number(room.shared.get("round")) || 0) + 1);
168
- room.on("shared", (key, value) => { if (key === "score") updateScoreboard(value); });
162
+ function fire() {
163
+ const hit = raycastAgainstWhatISee(); // favor-the-shooter: judge locally
164
+ spawnTracer(me.x, me.z, myQuat()); // MY tracer — right here, not via on("shot")
165
+ room.send("shot", { from: room.id, x: me.x, z: me.z, q: myQuat(), hit: hit ?? null });
166
+ }
167
+
168
+ room.on("shot", (m: any) => {
169
+ spawnTracer(m.x, m.z, m.q); // OTHER players' tracers
170
+ if (m.hit === room.id) me.hp -= 10; // I was hit → my next me.set publishes the new hp
171
+ });
169
172
  ```
170
173
 
171
- Any client can write any key (demo relay has no auth). For authority (who may change the
172
- score / advance the round), pick one client e.g. the first/lowest session id present —
173
- and let only it write.
174
+ Read remote positions for hit-testing from `stateRaw` (raw latest), not `state` (rendered in the
175
+ past). Guard the lookupa player may have just left:
176
+
177
+ ```ts
178
+ const t = room.players.get(id);
179
+ if (!t) return;
180
+ const px = t.stateRaw.x, pz = t.stateRaw.z; // test against the raw latest
181
+ ```
174
182
 
175
183
  ## Persistence helper
176
184
 
177
185
  ```ts
178
- // persistence.ts
186
+ // persistence.ts — /state requires the embed identity: call only after
187
+ // `await waitForAuth()` has resolved (see the genex-threejs-embed-auth skill).
188
+ import { getEmbedToken } from "@genex-ai/embed-sdk";
189
+
179
190
  export async function loadWorld<T>(fallback: T): Promise<T> {
180
191
  try {
181
- const { data } = await fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`).then(r => r.json());
192
+ const { data } = await fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`, {
193
+ headers: { Authorization: `Bearer ${getEmbedToken()}` },
194
+ }).then(r => r.json());
182
195
  return (data as T) ?? fallback;
183
196
  } catch { return fallback; }
184
197
  }
185
198
 
186
199
  let saveTimer: ReturnType<typeof setTimeout> | null = null;
187
200
  export function saveWorld(world: unknown) {
188
- // debounce: at most one PUT per second, from ONE authority client
189
- if (saveTimer) return;
201
+ if (saveTimer) return; // debounce: at most one PUT/sec, from the host only
190
202
  saveTimer = setTimeout(() => {
191
203
  saveTimer = null;
192
204
  fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`, {
193
205
  method: "PUT",
194
- headers: { "Content-Type": "application/json" },
206
+ headers: {
207
+ "Content-Type": "application/json",
208
+ Authorization: `Bearer ${getEmbedToken()}`,
209
+ },
195
210
  body: JSON.stringify(world),
196
211
  }).catch(() => {});
197
212
  }, 1000);
198
213
  }
199
214
  ```
200
215
 
201
- State is one JSON blob per project, max 1 MB, last-write-wins. Save from a single
202
- authority (host) so concurrent writers don't clobber each other.
216
+ State is one JSON blob per project, max 1 MB, last-write-wins. Save from a single authority
217
+ (`room.isHost`) so concurrent writers don't clobber each other.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: genex-threejs-precipitation-surfaces
3
+ description: Build coupled precipitation and weather-affected surfaces for Genex Three.js games. Use for falling snow, snow accumulation, model snow caps, rain, wet asphalt puddles, procedural ripple normals, splash flipbooks, rain streaks, shared weather envelopes, and surface wetness or coverage transitions.
4
+ ---
5
+
6
+ # Genex Three.js Precipitation Surfaces
7
+
8
+ Treat weather as a coupled event, particle, and surface-response system. Do not
9
+ add rain or snow particles that are visually disconnected from the ground.
10
+
11
+ ## Build order
12
+
13
+ ```text
14
+ weather envelope
15
+ -> falling precipitation volume
16
+ -> world/object surface mask
17
+ -> displaced or optical surface response
18
+ -> impact residue and splashes
19
+ -> shared lighting/post presentation
20
+ ```
21
+
22
+ Read [references/precipitation-surfaces.md](references/precipitation-surfaces.md)
23
+ for snow accumulation, object capping, wrapped precipitation volumes, wet
24
+ puddle masks, procedural ripple normals, splash placement, and debug outputs.
25
+
26
+ For the base ground surface under the wetness or snow (asphalt, dirt, stone),
27
+ generate a real texture with `npx genex texture` and load it via
28
+ `$genex-ai-texture`, then build the precipitation response on top of it.
29
+ Splash flipbook atlases and ripple normals stay procedural (for example a
30
+ canvas-drawn expanding-ring atlas) — they are not generated assets.
31
+
32
+ ## Required controls
33
+
34
+ - precipitation density and speed;
35
+ - wind direction and strength;
36
+ - shared weather progress or coverage;
37
+ - wetness, snow, or puddle mask threshold and softness;
38
+ - ripple or drift normal strength;
39
+ - surface roughness response;
40
+ - particle/splash opacity;
41
+ - debug modes for masks, normals, particles, and event progress.
42
+
43
+ ## Failure conditions
44
+
45
+ - falling precipitation ignores the wind or timing used by surface response;
46
+ - snow height and snow normals come from different fields;
47
+ - model snow sticks to vertical faces without an upward-facing filter;
48
+ - puddles only lower roughness without a mask, normal response, or ripples;
49
+ - splashes appear on downward or hidden faces;
50
+ - rain streaks allocate per drop or fail to wrap around the camera;
51
+ - temporal wetness is faked with unrelated time noise.
52
+
53
+ ## Routing boundary
54
+
55
+ Use `$genex-threejs-water-optics` for bounded pool simulation, caustics, Fresnel,
56
+ refraction, and Beer-Lambert water volumes. Use `$genex-threejs-procedural-vfx` for
57
+ general sparks, plasma, trails, and non-weather particles. Use
58
+ `$genex-threejs-temporal-surfaces` for screen-space touch history or frost clearing.
59
+ This skill owns precipitation events and the surfaces they visibly alter.
@@ -0,0 +1,181 @@
1
+ # Precipitation Surface Systems
2
+
3
+ Precipitation reads as real only when particles, surface masks, normals,
4
+ roughness, and impact residue share the same event state. The following
5
+ contracts describe two reusable families: snow accumulation and wet rain
6
+ puddles.
7
+
8
+ ## Contents
9
+
10
+ - Weather state contract
11
+ - Wrapped precipitation volume
12
+ - Snow accumulation contract
13
+ - Object snow capping
14
+ - Wet puddle contract
15
+ - Rain streaks and splashes
16
+ - Debug outputs
17
+ - Boundaries and failure modes
18
+
19
+ ## Weather state contract
20
+
21
+ Use a small shared state object for weather systems. The state is passed by
22
+ reference into both particles and surfaces.
23
+
24
+ ```js
25
+ const weather = {
26
+ uTime: { value: 0 },
27
+ uWind: { value: new THREE.Vector3(1.2, 0, 0.5) },
28
+ uProgress: { value: 0 },
29
+ };
30
+
31
+ function updateWeather(delta, target) {
32
+ weather.uTime.value += delta;
33
+ weather.uProgress.value = THREE.MathUtils.damp(
34
+ weather.uProgress.value,
35
+ target,
36
+ 0.9,
37
+ delta,
38
+ );
39
+ }
40
+ ```
41
+
42
+ Do not give rain particles one clock and puddle ripples another. Do not sample
43
+ wind in screen space for particles and world space for surfaces. The wind vector
44
+ is horizontal and is interpreted as world units per second for moving
45
+ precipitation, while scalar progress controls wetness or coverage.
46
+
47
+ ## Wrapped precipitation volume
48
+
49
+ A camera-centered volume avoids finite emitter edges. Each instance stores a
50
+ normalized spawn point and a random seed. The vertex shader turns that into a
51
+ world position and wraps all axes with `mod`.
52
+
53
+ ```glsl
54
+ vec3 origin = uCameraPos - vec3(vol.x * 0.5, vol.y * 0.4, vol.z * 0.5);
55
+ float speed = uSpeed * (0.6 + 0.7 * aRand);
56
+ vec3 base = aSeed * vol;
57
+ vec3 disp = vec3(uWind.x, -speed, uWind.z) * uTime + sway;
58
+ vec3 pos = mod(base + disp - origin, vol) + origin;
59
+ ```
60
+
61
+ For snow, use soft round camera-facing billboards with opacity around `0.9`,
62
+ flake radius around `0.07`, speed around `3.2`, and a horizontal sway near
63
+ `0.5`. For rain, use narrow vertical or uneven-capsule billboards and a faster
64
+ fall speed, commonly around `5` world units per second in an inspection-scale
65
+ scene.
66
+
67
+ ## Snow accumulation contract
68
+
69
+ Ground snow needs one height function. The same function displaces vertices and
70
+ feeds finite-difference normals.
71
+
72
+ ```glsl
73
+ float snowMaskAt(vec2 worldXZ) {
74
+ vec2 p = worldXZ * uSnowScale + uSnowSeed;
75
+ float n = fbm(p) * 0.5 + 0.5;
76
+ float threshold = 1.0 - uSnowCoverage;
77
+ return smoothstep(threshold - uSnowEdge, threshold + uSnowEdge, n);
78
+ }
79
+
80
+ float snowHeightAt(vec2 worldXZ) {
81
+ float mask = snowMaskAt(worldXZ);
82
+ float drift = fbm(worldXZ * uSnowBumpScale) * 0.5 + 0.5;
83
+ float h = mask * (1.0 - 0.4 * uSnowBumpStrength +
84
+ 0.4 * uSnowBumpStrength * drift);
85
+ vec2 edge = smoothstep(10.0, 8.0, abs(worldXZ));
86
+ return uSnowDepth * h * edge.x * edge.y;
87
+ }
88
+
89
+ vec3 groundSurfaceNormal(vec2 worldXZ) {
90
+ float e = 0.08;
91
+ float h0 = snowHeightAt(worldXZ);
92
+ float hx = snowHeightAt(worldXZ + vec2(e, 0.0));
93
+ float hz = snowHeightAt(worldXZ + vec2(0.0, e));
94
+ vec2 grad = vec2(hx - h0, hz - h0) / e;
95
+ return normalize(vec3(-grad.x, 1.0, -grad.y));
96
+ }
97
+ ```
98
+
99
+ The snow material response should override albedo toward a cool white, push
100
+ roughness to roughly `0.82`, and add sparse sparkle only inside the snow mask.
101
+ The sparkle is a material response, not a separate particle layer.
102
+
103
+ ## Object snow capping
104
+
105
+ Object snow must be model-locked. Compute a world-to-model matrix for the host
106
+ object and sample coverage in that coordinate space so moving or rotating the
107
+ object does not slide the snow pattern.
108
+
109
+ ```glsl
110
+ float snowAccumAt(vec3 worldNormal, vec2 modelXZ) {
111
+ float up = clamp(worldNormal.y, 0.0, 1.0);
112
+ float top = smoothstep(uSnowFlatThreshold, 1.0, up);
113
+ return top * snowCoverageMask(modelXZ);
114
+ }
115
+ ```
116
+
117
+ Typical controls are `uSnowFlatThreshold = 0.35`, `uSnowThickness = 0.06`,
118
+ `uSnowCoverage = 0.7`, and `uSnowEdge = 0.15`. Displace along the object normal
119
+ but convert from world units to local units using the mapped normal length.
120
+
121
+ ## Wet puddle contract
122
+
123
+ Wet asphalt is a material transition driven by rain progress. Use separate
124
+ progress bands: roughness changes early, ripple normals arrive as the rain
125
+ becomes heavy.
126
+
127
+ ```glsl
128
+ float roughnessProgress = smoothstep(0.0, 0.75, uRainFactor);
129
+ float normalProgress = smoothstep(0.75, 1.0, uRainFactor);
130
+ float puddleNoise = getPuddle(vPosition.xy * 15.0);
131
+ float puddleMask = smoothstep(0.0, 1.0, puddleNoise) * normalProgress;
132
+ ```
133
+
134
+ The puddle roughness is intentionally collapsed toward the `0.0..0.1` range
135
+ inside the mask. Ripple normals are analytic: every local cell emits expanding
136
+ rings with finite-difference slope estimation. Keep the ripple normal separate
137
+ from the static asphalt normal until the final normal handoff.
138
+
139
+ ## Rain streaks and splashes
140
+
141
+ Rain streaks can be instanced quads. Their fragment shape may use an uneven
142
+ capsule SDF and alpha around `0.1 * rainProgress`. Splash placement should use
143
+ surface sampling weighted by upward normals.
144
+
145
+ ```js
146
+ const skyWeight = normal.dot(new THREE.Vector3(0, 1, 0)) >= 0 ? 1 : 0;
147
+ geometry.setAttribute("skyWeight", new THREE.BufferAttribute(weights, 1));
148
+ sampler.setWeightAttribute("skyWeight");
149
+ ```
150
+
151
+ Each splash instance owns a progress attribute. A flipbook shader maps progress
152
+ to a tile in a `4 x 5` atlas, fades by rain progress, and uses additive
153
+ blending. The splash mesh should face the camera around Y.
154
+
155
+ ## Debug outputs
156
+
157
+ Expose at least:
158
+
159
+ - `final`: complete weather and surface response;
160
+ - `mask`: snow or puddle coverage only;
161
+ - `normals`: accumulated snow normal or ripple normal;
162
+ - `particles`: precipitation density and fall volume;
163
+ - `progress`: shared rain or snow envelope.
164
+
165
+ Diagnostics should report active instance count, coverage, and whether the
166
+ surface response is reading the same time/wind uniforms as particles.
167
+
168
+ ## Boundaries and failure modes
169
+
170
+ Use a water-volume skill when the system needs refraction through a bounded
171
+ water body, caustics, or Beer-Lambert thickness. Use a general VFX skill for
172
+ non-weather particles. Use a screen-space temporal-surface skill for touch
173
+ history, not for world-space wetness.
174
+
175
+ Known failure modes:
176
+
177
+ - snow silhouettes rise but normals stay flat;
178
+ - object snow uses world coordinates and slides under animation;
179
+ - puddle masks are independent of roughness and normal changes;
180
+ - splashes sample all triangles and appear under objects;
181
+ - rain progress affects particles but not the material, or the reverse.
@@ -29,7 +29,8 @@ Read [references/architecture-systems.md](references/architecture-systems.md) be
29
29
  - Compile by material slot to reduce draw calls without destroying material separation.
30
30
  - Preserve real dimensions for floor height, bay width, trim projection, and texture density.
31
31
  - Randomness may select among valid designs; it must not repair invalid geometry.
32
- - Provide topology, placement, material-slot, and UV-density debug modes.
32
+ - Provide topology, façade ownership, material/geometry, and shadow diagnostics
33
+ appropriate to the renderer path.
33
34
 
34
35
  ## Acceptance
35
36
 
@@ -73,7 +73,7 @@ Seeded randomness perturbs constrained decisions:
73
73
 
74
74
  ```text
75
75
  towerScale = clamp(
76
- authoredTowerScale + random(-0.05, 0.04),
76
+ settings.towerScale + random(-0.05, 0.04),
77
77
  0.62,
78
78
  0.96
79
79
  )
@@ -43,11 +43,6 @@ Read [references/field-systems.md](references/field-systems.md)
43
43
  before implementation. It records sphere, terrain, water, and
44
44
  structured-placement field contracts plus common parity defects.
45
45
 
46
- Read the
47
- [procedural planet surface](../threejs-procedural-planets/examples/procedural-planet-surface/planet-system.js)
48
- for a shared CPU/GLSL field bundle whose height, continents, climate, biomes,
49
- roughness, and normals remain independently inspectable.
50
-
51
46
  ## Non-negotiable rules
52
47
 
53
48
  - Independent noise per channel produces visual soup. Share structure.
@@ -21,11 +21,6 @@ Read [references/mesh-systems.md](references/mesh-systems.md)
21
21
  for the exact sculpted-frame profile, rail emission, tree rings, semantic mesh
22
22
  writer, and their observed scaling limits.
23
23
 
24
- Read the
25
- [authored financial tower compiler](../threejs-procedural-architecture/examples/authored-financial-tower/building-system.js)
26
- for semantic placement compilation and material-slot instancing at building
27
- scale.
28
-
29
24
  ## Failure conditions
30
25
 
31
26
  - profile orientation flips along a curve;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: genex-threejs-procedural-materials
3
- description: Author production procedural materials for Genex Three.js games. Use for PBR identity, terrain materials, atlas filtering, specular anti-aliasing, wetness, biome surfaces, dissolves, procedural normals, roughness variation, and readable materials across gameplay distances.
3
+ description: Author production procedural materials for Genex Three.js games. Use for PBR identity, terrain materials, atlas filtering, specular anti-aliasing, wetness, lava and hot emissive surfaces, raymarched material fields, biome surfaces, dissolves, procedural normals, roughness variation, and readable materials across gameplay distances.
4
4
  ---
5
5
 
6
6
  # Genex Three.js Procedural Materials
@@ -23,21 +23,6 @@ Read [references/material-systems.md](references/material-systems.md)
23
23
  for atlas filtering, specular AA, planetary coordinates,
24
24
  world-height wetness, per-instance dissolve, and authored PBR response bundles.
25
25
 
26
- Read the
27
- [sculpted gallery frame geometry](../threejs-procedural-geometry/examples/sculpted-gallery-frame/frame-geometry.js)
28
- for walnut, antique-gold, and ebony texture/roughness/metalness/clearcoat
29
- bundles under a grazing-light setup.
30
-
31
- Read the
32
- [procedural planet surface](../threejs-procedural-planets/examples/procedural-planet-surface/planet-system.js)
33
- for shared geological, climate, water, biome, roughness, and derivative-normal
34
- causes on a procedural planetary surface.
35
-
36
- Read the
37
- [analytic wave optics](../threejs-water-optics/examples/analytic-wave-optics/water-system.js)
38
- for coupled reflection, refraction, absorption, filtered microstructure,
39
- resolved crest response, and their diagnostic channels.
40
-
41
26
  ## Required controls
42
27
 
43
28
  - real or perceptual texture scale;
@@ -46,7 +31,7 @@ resolved crest response, and their diagnostic channels.
46
31
  - the causal fields required by the selected material pattern;
47
32
  - distance/derivative filtering;
48
33
  - specular antialiasing;
49
- - channel and mask debug modes.
34
+ - channel and mask debug modes;
50
35
  - emissive-material debug modes when the material owns glow or volumetric
51
36
  accumulation.
52
37