@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.
- package/README.md +13 -12
- package/dist/index.js +410 -154
- package/package.json +10 -11
- package/templates/README.md +8 -7
- package/templates/skills/genex-ai-model/SKILL.md +5 -5
- package/templates/skills/genex-ai-sfx/SKILL.md +1 -1
- package/templates/skills/genex-ai-skybox/SKILL.md +2 -2
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +10 -6
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +174 -0
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +193 -100
- package/templates/skills/genex-threejs-multiplayer/references/genre-recipes.md +115 -0
- package/templates/skills/genex-threejs-multiplayer/references/realtime-patterns.md +138 -123
- package/templates/skills/genex-threejs-precipitation-surfaces/SKILL.md +59 -0
- package/templates/skills/genex-threejs-precipitation-surfaces/references/precipitation-surfaces.md +181 -0
- package/templates/skills/genex-threejs-procedural-architecture/SKILL.md +2 -1
- package/templates/skills/genex-threejs-procedural-architecture/references/architecture-systems.md +1 -1
- package/templates/skills/genex-threejs-procedural-fields/SKILL.md +0 -5
- package/templates/skills/genex-threejs-procedural-geometry/SKILL.md +0 -5
- package/templates/skills/genex-threejs-procedural-materials/SKILL.md +2 -17
- package/templates/skills/genex-threejs-procedural-vegetation/SKILL.md +11 -1
- package/templates/skills/genex-threejs-procedural-vfx/SKILL.md +3 -1
- package/templates/skills/genex-threejs-shadow-systems/SKILL.md +0 -5
- package/templates/skills/genex-threejs-skill-router/SKILL.md +10 -5
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +9 -6
- package/templates/skills/genex-threejs-spectral-ocean/SKILL.md +11 -1
- package/templates/skills/genex-threejs-temporal-surfaces/SKILL.md +3 -1
- package/templates/skills/genex-threejs-water-optics/SKILL.md +14 -2
|
@@ -1,113 +1,56 @@
|
|
|
1
|
-
# Realtime patterns:
|
|
1
|
+
# Realtime patterns: rendering, objects, host, events, persistence
|
|
2
2
|
|
|
3
|
-
The `@genex-ai/multiplayer`
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## The golden rule
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
102
|
-
|
|
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
|
-
//
|
|
110
|
-
const
|
|
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
|
|
127
|
-
meshFor(room.id)
|
|
128
|
-
|
|
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:
|
|
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(
|
|
138
|
-
m.
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
//
|
|
156
|
-
|
|
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
|
-
//
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
Read remote positions for hit-testing from `stateRaw` (raw latest), not `state` (rendered in the
|
|
175
|
+
past). Guard the lookup — a 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
|
|
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
|
|
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: {
|
|
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
|
-
|
|
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.
|
package/templates/skills/genex-threejs-precipitation-surfaces/references/precipitation-surfaces.md
ADDED
|
@@ -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,
|
|
32
|
+
- Provide topology, façade ownership, material/geometry, and shadow diagnostics
|
|
33
|
+
appropriate to the renderer path.
|
|
33
34
|
|
|
34
35
|
## Acceptance
|
|
35
36
|
|
|
@@ -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
|
|