@genex-ai/cli-demo 0.78.0-dev.200 → 0.80.0-dev.211
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/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/controllers/quality/pick-asset.ts +12 -7
- package/templates/skills/genex-ai-character/SKILL.md +2 -4
- package/templates/skills/genex-ai-hud/SKILL.md +69 -23
- package/templates/skills/genex-ai-menu/SKILL.md +17 -12
- package/templates/skills/genex-ai-model/SKILL.md +58 -7
- package/templates/skills/genex-game-director/SKILL.md +265 -0
- package/templates/skills/genex-game-director/references/design-contract.md +148 -0
- package/templates/skills/{genex-threejs-skill-router → genex-game-director}/references/routing-map.md +82 -63
- package/templates/skills/genex-getting-started/SKILL.md +3 -2
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +23 -8
- package/templates/skills/genex-threejs-adaptive-quality/references/adaptive-quality.md +16 -4
- package/templates/skills/genex-threejs-character-controller/SKILL.md +2 -4
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +6 -48
- package/templates/skills/genex-threejs-game-content/SKILL.md +3 -4
- package/templates/skills/genex-threejs-game-ui/SKILL.md +95 -76
- package/templates/skills/genex-threejs-game-ui/references/style-capsules.md +9 -5
- package/templates/skills/genex-threejs-lighting-design/SKILL.md +7 -0
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +1 -2
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +2 -4
- package/templates/skills/genex-threejs-skill-router/SKILL.md +0 -263
package/dist/index.js
CHANGED
|
@@ -287,7 +287,7 @@ async function hasGenexSkills(skillsDir) {
|
|
|
287
287
|
return false;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
var REMOVED_SKILLS = ["genex-explore"];
|
|
290
|
+
var REMOVED_SKILLS = ["genex-explore", "genex-threejs-skill-router"];
|
|
291
291
|
async function pruneRemovedSkills(skillsDir, log) {
|
|
292
292
|
const removed = [];
|
|
293
293
|
for (const name of REMOVED_SKILLS) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.0-dev.211",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/templates/README.md
CHANGED
|
@@ -17,5 +17,5 @@ command keeps them in sync with the installed CLI version automatically
|
|
|
17
17
|
- `agents/` - example subagent definitions.
|
|
18
18
|
- `commands/` - example slash commands.
|
|
19
19
|
|
|
20
|
-
Start with `skills/genex-
|
|
20
|
+
Start with `skills/genex-game-director/SKILL.md` when asking your agent
|
|
21
21
|
to build or improve a 3D browser game.
|
|
@@ -80,26 +80,31 @@ export async function loadTextureWithFallback<T>(
|
|
|
80
80
|
// Generated MODELS (mesh-compression lane). GLB rungs use the same numeric
|
|
81
81
|
// suffix — `model-glb@1024` = embedded textures ≤1024 + meshopt (+simplify) —
|
|
82
82
|
// but unlike images there is NO edge rewrite for old games: selection happens
|
|
83
|
-
// ONLY here, in games that wired the decoders (createGltfLoader).
|
|
84
|
-
//
|
|
83
|
+
// ONLY here, in games that wired the decoders (createGltfLoader). EVERY tier
|
|
84
|
+
// loads a game-ready rung — provider-raw originals are archival/remix source,
|
|
85
|
+
// not game assets (a Tripo prop is ~500k tris + 3x4096² textures; a scene of
|
|
86
|
+
// them floored an M4 Max). Desktop gets @2048, phones @1024; the original is
|
|
87
|
+
// only ever the fallback of last resort.
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
/** Per-tier texture budget: desktop tiers @2048, phone tiers @1024. */
|
|
90
|
+
function modelBudgetFor(tier: QualityTier): number {
|
|
91
|
+
return tier.name === "phone" || tier.name === "phone-low" ? 1024 : 2048;
|
|
92
|
+
}
|
|
87
93
|
const MODEL_ROLE_RE = /^(model-glb|character-rigged(-a\d+)?-glb(-r\d+)?)$/;
|
|
88
94
|
|
|
89
95
|
/** Resolve the model URL a THIS-tier device should load. `ktx2: true` (from
|
|
90
|
-
* createGltfLoader) upgrades
|
|
96
|
+
* createGltfLoader) upgrades to the GPU-compressed sibling. */
|
|
91
97
|
export function pickModel(url: string, tier: QualityTier, opts?: { ktx2?: boolean }): string {
|
|
92
98
|
if (!GENEX_GENERATIONS_RE.test(url)) return url;
|
|
93
99
|
const role = url.split("/").pop() ?? "";
|
|
94
100
|
if (!MODEL_ROLE_RE.test(role)) return url;
|
|
95
|
-
|
|
96
|
-
const rung = `${url}@${MODEL_TEXTURE_BUDGET}`;
|
|
101
|
+
const rung = `${url}@${modelBudgetFor(tier)}`;
|
|
97
102
|
return opts?.ktx2 ? `${rung}.ktx2` : rung;
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
106
|
* Load a generated model through the rung ladder with the full fallback chain
|
|
102
|
-
* (`.ktx2` →
|
|
107
|
+
* (`.ktx2` → the tier's rung → original). Use with the decoder-wired loader:
|
|
103
108
|
*
|
|
104
109
|
* const gltf = createGltfLoader(renderer);
|
|
105
110
|
* const model = await loadModelWithFallback(MODEL_URL, tier, (u) => gltf.loader.loadAsync(u), { ktx2: gltf.ktx2 });
|
|
@@ -43,10 +43,8 @@ Meshy Image-to-3D first produces an unremeshed high-detail model. Show its
|
|
|
43
43
|
front, back, left, and right views and report its measured face count. Preserve
|
|
44
44
|
that model in R2. Before rigging, ask the user to approve a separate
|
|
45
45
|
10,000-face triangle remesh. The 10k remesh—not the high-detail source—is
|
|
46
|
-
rigged and animated. (For these approvals, use your
|
|
47
|
-
|
|
48
|
-
`request_user_input`; if it has none, e.g. Cursor, a short numbered list in
|
|
49
|
-
chat.)
|
|
46
|
+
rigged and animated. (For these approvals, use your question tool when you
|
|
47
|
+
have one; if you have none, a short numbered list in chat.)
|
|
50
48
|
|
|
51
49
|
The high-detail pre-rig generation stays in the selected neutral A-pose. There
|
|
52
50
|
is no dynamic-pose concept and no silent T-pose fallback. After the user has
|
|
@@ -35,18 +35,25 @@ ring) that no hand-written CSS can fake.
|
|
|
35
35
|
(`background: rgba(<darkest hue>, 0.35–0.6)` + `backdrop-filter: blur(6–12px)`
|
|
36
36
|
and its `-webkit-` twin) when the style wants translucency, a solid painted
|
|
37
37
|
plate, a subtle gradient — or no plate at all for an outline-led look.
|
|
38
|
+
**A plate exists only where the widget's own art doesn't already back it.**
|
|
39
|
+
Behind an OPAQUE frame sprite there is no plate — the frame IS the backing
|
|
40
|
+
surface, and a dark div stacked behind it only protrudes as a box over the
|
|
41
|
+
scene (the recurring black-box defect). Where a plate IS needed — bare DOM
|
|
42
|
+
readouts, thin-outline widgets — it stays WITHIN the widget's silhouette:
|
|
43
|
+
inset inside the frame, or shaped to it, never a full rectangle spilling
|
|
44
|
+
past the art onto the scene.
|
|
38
45
|
**Glass is a technique, not a default** — do not reach for translucency
|
|
39
46
|
because this skill mentions it; reach for it when THIS game's brief does.
|
|
40
47
|
What is fixed is WHERE plates are built — the DOM — because
|
|
41
48
|
semi-transparency physically cannot survive sprite extraction: a composited
|
|
42
49
|
panel's pixels are a blend of panel and background, and no cutout can
|
|
43
50
|
un-blend them. Plates are NEVER baked into sprites. Give the plate its
|
|
44
|
-
corners with `border-radius`
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
corners with `border-radius` — **never a raw `clip-path`/`mask` chamfer**
|
|
52
|
+
(it shears off borders, shadows, and content near the cut and re-breaks on
|
|
53
|
+
every padding/value change: the recurring "cut corners" defect). A genuinely
|
|
54
|
+
ornamented or angular frame belongs in chrome, generated. The one
|
|
55
|
+
load-bearing `mask`/`clip-path` in this skill is the masked-fill reveal
|
|
56
|
+
below — never corner shaping.
|
|
50
57
|
- **Chrome (sprites — what THIS pipeline generates).** Opaque frames, corner
|
|
51
58
|
brackets, ornaments, emblems, icons, medallions — hard-alpha art laid over
|
|
52
59
|
the glass. The Stage-2 sheet contains ONLY chrome; never a panel with its
|
|
@@ -73,7 +80,7 @@ ring) that no hand-written CSS can fake.
|
|
|
73
80
|
the container is the sprite and the content is DOM — the default split.
|
|
74
81
|
- **Rule B — image vs code (the 5-minute test).** If CSS can build the
|
|
75
82
|
element in under ~5 minutes without losing fidelity — flat plates, simple
|
|
76
|
-
dots/pips, plain strokes, a
|
|
83
|
+
dots/pips, plain strokes, a rounded corner — it is CSS. If it needs
|
|
77
84
|
craft (material, ornament, painterly texture), it is a sprite. One
|
|
78
85
|
exception cuts the other way: a static label engraved/embossed into a
|
|
79
86
|
frame's craft stays BAKED in the sprite (`baked_static` below) even
|
|
@@ -81,9 +88,10 @@ ring) that no hand-written CSS can fake.
|
|
|
81
88
|
- **CSS-chrome recipes — the middle ground.** Between bare CSS and the full
|
|
82
89
|
sprite pipeline sits brief-styled CSS chrome, right for utilitarian panels
|
|
83
90
|
and secondary screens: a two-hue gradient frame (border + inset
|
|
84
|
-
box-shadow in brief hues),
|
|
85
|
-
`$genex-threejs-game-ui`'s corner rules), a
|
|
86
|
-
divs (outer div = border hue, inset div =
|
|
91
|
+
box-shadow in brief hues), rounded with `border-radius` (never a raw
|
|
92
|
+
`clip-path` chamfer — see `$genex-threejs-game-ui`'s corner rules), a
|
|
93
|
+
9-slice-ish panel from nested divs (outer div = border hue, inset div =
|
|
94
|
+
plate hue, 2–3px reveal). What
|
|
87
95
|
makes these legitimate: **CSS elements are styled FROM THE BRIEF — hue,
|
|
88
96
|
weight, texture. Default-gray CSS anywhere on screen is a defect** (the
|
|
89
97
|
classic failure: score pips shipped as unstyled "○ ●" system glyphs).
|
|
@@ -105,7 +113,10 @@ The HUD overlays a **live 3D scene**. The pixels between widgets show the game.
|
|
|
105
113
|
burns generations.
|
|
106
114
|
- **Every widget needs internal contrast** — a panel fill, outline stroke, or
|
|
107
115
|
semi-opaque backdrop — because the scene behind it might be a snowfield or a
|
|
108
|
-
torchlit dungeon. Pure-outline widgets over arbitrary scenes fail.
|
|
116
|
+
torchlit dungeon. Pure-outline widgets over arbitrary scenes fail. An opaque
|
|
117
|
+
frame sprite already IS that contrast; don't stack a plate behind it. A
|
|
118
|
+
needed backdrop lives inside the widget's silhouette, never as a rectangle
|
|
119
|
+
spilling past the art (see the plate rule under "three layers").
|
|
109
120
|
- **The mockup must be flat and head-on** — a 2D screen-space overlay, never
|
|
110
121
|
tilted, isometric, or in perspective. Angled panels have no clean silhouette
|
|
111
122
|
and deconstruct into skewed slices. If the mockup comes back tilted,
|
|
@@ -203,13 +214,26 @@ and [references/stage2-prompt-template.md](references/stage2-prompt-template.md)
|
|
|
203
214
|
gate — the Stage-1 mockup IS the game concept.** There is no separate
|
|
204
215
|
UI-free concept image before it: this one generation carries scene + HUD,
|
|
205
216
|
serves as the user's style checkpoint, and anchors all later art
|
|
206
|
-
(`$genex-threejs-game-ui` owns the checkpoint choreography).
|
|
217
|
+
(`$genex-threejs-game-ui` owns the checkpoint choreography). **Stage 2 onward
|
|
218
|
+
is frame-anchored, so it waits for the user's keep/change YES on that
|
|
219
|
+
checkpoint — kick it off the instant they approve, not before.** The scene half
|
|
207
220
|
of the prompt is written in TEXT from the game plan — `[GAME_SCENE]` in the
|
|
208
|
-
Stage-1 template: setting, the moment, what the player is doing, lighting
|
|
209
|
-
|
|
221
|
+
Stage-1 template: setting, the moment, what the player is doing, lighting.
|
|
222
|
+
**If a concept/reference image already exists — the user's own concept art, or
|
|
223
|
+
a look frame you generated and they approved — anchor Stage 1 to it with
|
|
224
|
+
`--edit <that-url>` so the HUD inherits its exact palette, materials, and
|
|
225
|
+
lighting; that anchoring is what makes the final HUD actually match the
|
|
226
|
+
concept, and skipping it is why a text-only mockup drifts.** A text-only
|
|
227
|
+
Stage 1 is the fallback for when no reference exists. (To `--edit` a
|
|
228
|
+
user-supplied image it must be reachable as an R2 URL — a bare chat attachment
|
|
229
|
+
is not; reproduce it as a generation first, or say plainly you can't anchor to
|
|
230
|
+
it rather than silently guessing.) Then write the
|
|
210
231
|
widget layout and wiring code (placement, masked-fill scaffolding,
|
|
211
232
|
plain-CSS placeholder bars) while it renders — the CSS HUD keeps
|
|
212
|
-
the game playable until the sprites land.
|
|
233
|
+
the game playable until the sprites land. When a sprite lands it REPLACES
|
|
234
|
+
its placeholder: delete the placeholder's own background/plate as you wire
|
|
235
|
+
the opaque frame in — a placeholder dark div left behind an opaque sprite
|
|
236
|
+
is the black-box defect. The mockup is a STYLE anchor
|
|
213
237
|
only: the widget set and
|
|
214
238
|
layout come from the element inventory and the game contract — a mechanic
|
|
215
239
|
the mockup invented (a lap counter, a stamina orb) does not enter the HUD,
|
|
@@ -221,8 +245,9 @@ re-run from Stage 1 with their notes — cheaper than it sounds (one image
|
|
|
221
245
|
is the whole concept now), so say so in one line and do it.
|
|
222
246
|
|
|
223
247
|
```bash
|
|
224
|
-
# Stage 1 — the game CONCEPT: full HUD composited over the game's own scene
|
|
225
|
-
# described
|
|
248
|
+
# Stage 1 — the game CONCEPT: full HUD composited over the game's own scene.
|
|
249
|
+
# TEXT-described; add `--edit <concept-url>` to anchor it to an existing
|
|
250
|
+
# concept/reference image so the HUD inherits its exact style.
|
|
226
251
|
# Two candidates in one call; pick the better, save its URL:
|
|
227
252
|
npx genex image "<filled stage-1 prompt>" --size 2560x1440 --quality high --candidates 2
|
|
228
253
|
|
|
@@ -311,11 +336,13 @@ To fix ONE sprite, don't re-run the pipeline:
|
|
|
311
336
|
painted ON TOP. Never put the gradient on the scaling fill itself — its `%`
|
|
312
337
|
stops anchor to the fill's own width and the segments visibly compress as
|
|
313
338
|
the value drops.
|
|
314
|
-
- **
|
|
315
|
-
`background: rgba(10, 8, 6, 0.6)` rounded div behind the text
|
|
339
|
+
- **A numeric readout sitting as bare text over the scene gets a backing
|
|
340
|
+
plate** — a `background: rgba(10, 8, 6, 0.6)` rounded div behind the text, an
|
|
316
341
|
extracted plate sprite when the art has one, or a deliberate skip documented
|
|
317
342
|
in a comment (`/* no plate — reads against <reason> */`). Bare digits look
|
|
318
|
-
fine over the dev background and vanish over a bright scene.
|
|
343
|
+
fine over the dev background and vanish over a bright scene. A readout
|
|
344
|
+
already sitting on an opaque frame sprite needs NO separate plate — the frame
|
|
345
|
+
backs it; a second dark div only boxes it (the no-double-backing rule above).
|
|
319
346
|
|
|
320
347
|
## Discrete vs continuous — classify every meter before Stage 2
|
|
321
348
|
|
|
@@ -473,6 +500,13 @@ Two hard rules:
|
|
|
473
500
|
|
|
474
501
|
## Verify before calling it done
|
|
475
502
|
|
|
503
|
+
**This is a gate, not a suggestion — the HUD is not "done", does not ship, and
|
|
504
|
+
is not called a milestone until you have looked at it running.** The field
|
|
505
|
+
failure is an agent that wires the sprites, takes one glance, notices a possible
|
|
506
|
+
clip or cutoff, and ships anyway with "I'll refine if the user reports issues."
|
|
507
|
+
That punt IS the defect. You are the one who verifies, not the user. If you are
|
|
508
|
+
out of budget to check, say the HUD is unverified — never call it done.
|
|
509
|
+
|
|
476
510
|
- **Open every extracted PNG and eyeball it** — right subject, right name.
|
|
477
511
|
The reading-order sort can mismap when rows are uneven; re-run `extract`
|
|
478
512
|
with corrected name order rather than regenerating anything.
|
|
@@ -483,9 +517,11 @@ Two hard rules:
|
|
|
483
517
|
Display fonts run 25–50% wider than a naive estimate; widen the box or drop
|
|
484
518
|
the weight, don't shrink the font.
|
|
485
519
|
- **No widget overlaps** at the reference viewport, and none off-screen.
|
|
486
|
-
- **Screenshot
|
|
487
|
-
`$genex-threejs-visual-validation`'s capture discipline.
|
|
488
|
-
|
|
520
|
+
- **Screenshot the running HUD and inspect it** — bright AND dark scenes —
|
|
521
|
+
with `$genex-threejs-visual-validation`'s capture discipline. **Zoom every
|
|
522
|
+
corner:** no sheared frames, no clipped/cut corners, no text truncated or
|
|
523
|
+
colliding with an edge. Judge the pixels, not your intent — skipping this is
|
|
524
|
+
exactly how the "cut corners" defect ships.
|
|
489
525
|
|
|
490
526
|
## The closing wiring audit — the pipeline's final stage, not optional
|
|
491
527
|
|
|
@@ -517,6 +553,16 @@ does not count. fillBox numbers, `segments`, and real sprite dims do not
|
|
|
517
553
|
survive paraphrase; the canonical version of this failure is a subagent
|
|
518
554
|
reporting "masks validated" while the parent ships width% fills.
|
|
519
555
|
|
|
556
|
+
**Do not fire-and-forget the whole HUD/menu/logo into one background subagent.**
|
|
557
|
+
The field failure: the entire production art pipeline was handed to a single
|
|
558
|
+
background agent that then stalled overnight, so every preview the user played
|
|
559
|
+
showed the CSS placeholder and the real HUD only landed hours later, unseen. If
|
|
560
|
+
art runs in a subagent it is a BOUNDED task you wait on, then wire and verify in
|
|
561
|
+
THIS session — not an overnight handoff. While the art is pending the HUD is
|
|
562
|
+
unverified: do not call the game done or push it as a finished milestone. If a
|
|
563
|
+
subagent stalls or misses its window, wire what landed and say plainly what is
|
|
564
|
+
still placeholder — never present a placeholder HUD as the finished look.
|
|
565
|
+
|
|
520
566
|
## Cost & latency honesty
|
|
521
567
|
|
|
522
568
|
A full HUD is **~9 image generations** (mockup + deconstruct + clean + a few
|
|
@@ -173,8 +173,10 @@ Pause both videos when the menu phase hides (they're decode work), and
|
|
|
173
173
|
resume the front one when it returns. Watch ONE full cycle in the browser
|
|
174
174
|
before calling the menu done — that's the seam check.
|
|
175
175
|
|
|
176
|
-
**Work async — the menu must never block the game.**
|
|
177
|
-
|
|
176
|
+
**Work async — the menu must never block the game.** The still is
|
|
177
|
+
`--edit`-anchored to the concept mockup, so it's style-dependent: generate it
|
|
178
|
+
the moment the user approves the concept (not before), enqueue the video with
|
|
179
|
+
`--no-wait`, ship the CSS menu
|
|
178
180
|
(buttons + title over the frame IMAGE as a static backdrop), and swap the
|
|
179
181
|
`<video>` in when `genex wait` prints the URL. The frame image doubles as the
|
|
180
182
|
**loading screen background** — it exists minutes before the video does (see
|
|
@@ -368,13 +370,14 @@ not):
|
|
|
368
370
|
re-anchored the look) re-opens this rule once — re-edit the still
|
|
369
371
|
(`--edit` against the new concept mockup) and re-run the video from the
|
|
370
372
|
new still. Agent-initiated polish never does.
|
|
371
|
-
- **
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
373
|
+
- **The menu (`--frame`) path renders 720p — don't force 1080p.** The
|
|
374
|
+
frame-conditioned first-last-frame route was rejected by the provider at
|
|
375
|
+
1080p (repeated 422 failures that shipped a dead static menu), so the menu
|
|
376
|
+
video path defaults to the proven-good, seamless-looping 720p — no flag
|
|
377
|
+
needed. The DOM crossfade + full-screen scale read fine at 720p; do NOT pass
|
|
378
|
+
`--resolution 1080p` on a `--frame` menu clip just to sharpen it, or you risk
|
|
379
|
+
re-triggering the 422 and losing the video entirely. (Plain text-to-video,
|
|
380
|
+
without `--frame`, still defaults to 1080p.)
|
|
378
381
|
- **Pause/victory/defeat variants reuse the same video — as GRADES.** Same
|
|
379
382
|
`<video>` element or URL, different emotion via CSS `filter` on the
|
|
380
383
|
background: pause = a plain dark overlay (`rgba(0,0,0,0.55)`); defeat =
|
|
@@ -404,9 +407,11 @@ not):
|
|
|
404
407
|
genuine state change; expect a loop seam.
|
|
405
408
|
- `--duration <sec>` (video) — 4, 6, or 8 for frame-conditioned clips;
|
|
406
409
|
default 8.
|
|
407
|
-
- `--resolution <720p|1080p>` (video) —
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
- `--resolution <720p|1080p>` (video) — plain text-to-video defaults to 1080p;
|
|
411
|
+
the frame-conditioned (`--frame`) menu path defaults to **720p** for
|
|
412
|
+
reliability (1080p was rejected there). `720p` is also the cost fallback
|
|
413
|
+
(~half the credits). Loop clips (`--loop`) ignore it (that model has no
|
|
414
|
+
resolution parameter).
|
|
410
415
|
- `--aspect 16:9 --quality high` (image) — the right settings for a menu frame.
|
|
411
416
|
- `--no-wait` — enqueue and return immediately with the generation id; pick
|
|
412
417
|
the result up later with `npx genex wait <id>` (safe to re-run — it attaches
|
|
@@ -67,11 +67,12 @@ side — reads as broken at a glance.
|
|
|
67
67
|
|
|
68
68
|
Load through the quality kit's decoder-wired loader and the model rung ladder
|
|
69
69
|
(`$genex-threejs-adaptive-quality`; `npx genex controller quality` installs
|
|
70
|
-
it). The bare URL is the provider-raw original —
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
it). The bare URL is the provider-raw original — ~500k triangles + several 4K
|
|
71
|
+
PBR textures, archival/remix source, NOT a game asset (31 raw props once
|
|
72
|
+
floored an M4 Max). Every model ships a game-ready ladder instead: desktop
|
|
73
|
+
tiers load the `@2048` rung, phones the `@1024` rung (both meshopt-compressed
|
|
74
|
+
and simplified), and KTX2-capable games get the GPU-compressed sibling. R2
|
|
75
|
+
sends the right CORS headers, so cross-origin loading just works:
|
|
75
76
|
|
|
76
77
|
```ts
|
|
77
78
|
import { createGltfLoader } from "./controllers/quality/gltf-loader.ts";
|
|
@@ -86,6 +87,22 @@ const gltf = await loadModelWithFallback(
|
|
|
86
87
|
MODEL_URL, tier, (u) => gltfLoader.loader.loadAsync(u), { ktx2: gltfLoader.ktx2 },
|
|
87
88
|
);
|
|
88
89
|
const model = gltf.scene;
|
|
90
|
+
// Tame ONLY the provider's mirror-metal artifact (metalness≈1 + near-zero
|
|
91
|
+
// roughness mirrors the whole sky env and swims with every camera move). Do
|
|
92
|
+
// NOT flatten every material to 0.6 — that was dulling legitimately metallic
|
|
93
|
+
// props (gunmetal, chrome, gold, polished stone). Clamp the extreme case only;
|
|
94
|
+
// leave everything else as authored.
|
|
95
|
+
model.traverse((o) => {
|
|
96
|
+
const m = (o as THREE.Mesh).material as THREE.MeshStandardMaterial;
|
|
97
|
+
if (!m?.isMeshStandardMaterial) return;
|
|
98
|
+
if (m.metalness > 0.85 && m.roughness < 0.2) {
|
|
99
|
+
m.metalness = 0.7; // still reads metallic, no longer a full mirror
|
|
100
|
+
m.roughness = Math.max(m.roughness, 0.3);
|
|
101
|
+
}
|
|
102
|
+
m.envMapIntensity = Math.min(m.envMapIntensity, 0.8);
|
|
103
|
+
});
|
|
104
|
+
// If a model that SHOULD read metallic still looks dull, this clamp is not the
|
|
105
|
+
// cause — check the model against the concept and lift per-material as needed.
|
|
89
106
|
model.scale.setScalar(1); // tune to taste
|
|
90
107
|
model.position.set(0, 0, 0);
|
|
91
108
|
scene.add(model);
|
|
@@ -100,8 +117,42 @@ Never swallow a failed load silently: if you wrap a generated-asset load in a
|
|
|
100
117
|
`catch`, `console.warn` the asset name in it — your own self-check reads the
|
|
101
118
|
console, and a bare `.catch(() => null)` hides a missing model from you too.
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
**Copies**: one or two, `model.clone()` (clones share geometry/textures on the
|
|
121
|
+
GPU). **Three or more of the same model → `InstancedMesh`** — clones still cost
|
|
122
|
+
one draw call per mesh per copy; instancing renders all copies in one draw
|
|
123
|
+
call per source mesh:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// placements: THREE.Matrix4[] — one world transform per copy
|
|
127
|
+
const group = new THREE.Group();
|
|
128
|
+
gltf.scene.updateMatrixWorld(true);
|
|
129
|
+
const tmp = new THREE.Matrix4();
|
|
130
|
+
gltf.scene.traverse((o) => {
|
|
131
|
+
const src = o as THREE.Mesh;
|
|
132
|
+
if (!src.isMesh) return;
|
|
133
|
+
const im = new THREE.InstancedMesh(src.geometry, src.material, placements.length);
|
|
134
|
+
placements.forEach((p, i) => im.setMatrixAt(i, tmp.multiplyMatrices(p, src.matrixWorld)));
|
|
135
|
+
im.instanceMatrix.needsUpdate = true;
|
|
136
|
+
im.castShadow = im.receiveShadow = true;
|
|
137
|
+
group.add(im);
|
|
138
|
+
});
|
|
139
|
+
scene.add(group);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Distance LOD** (open worlds / long sightlines): the rung ladder already ships
|
|
143
|
+
a lighter mesh — use the phone rung as the far level. One extra fetch; worth it
|
|
144
|
+
past ~25 m draw ranges, skip it in small arenas:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const far = await gltfLoader.loader.loadAsync(`${MODEL_URL}@1024`);
|
|
148
|
+
const lod = new THREE.LOD();
|
|
149
|
+
lod.addLevel(gltf.scene, 0); // near: the tier's rung
|
|
150
|
+
lod.addLevel(far.scene, 25); // far: phone rung — three swaps by camera distance
|
|
151
|
+
scene.add(lod);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For animated GLBs, drive `gltf.animations` with a `THREE.AnimationMixer`
|
|
155
|
+
(animated/skinned meshes can't instance — clone those).
|
|
105
156
|
|
|
106
157
|
**If the game has physics, give the model a collider** — a GLB added only to the
|
|
107
158
|
scene is a ghost: players and objects pass straight through it.
|