@genex-ai/cli-demo 1.13.2-dev.559 → 1.14.0-dev.560
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/{blender-mcp-LLN4FPA4.js → blender-mcp-3U5AG7WS.js} +3 -3
- package/dist/{chunk-WXHTNSFB.js → chunk-2UJK5OCB.js} +17 -9
- package/dist/index.js +43 -11
- package/package.json +1 -1
- package/templates/skills/genex-blender-scene/SKILL.md +222 -0
- package/templates/skills/genex-game-director/SKILL.md +5 -2
- package/templates/skills/genex-game-director/references/routing-map.md +4 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BLENDER_SETUP_HINT,
|
|
3
2
|
SHEET_FORMATS,
|
|
4
3
|
acquireSeat,
|
|
5
4
|
blenderCall,
|
|
6
5
|
blenderEndpoint,
|
|
6
|
+
blenderSetupHint,
|
|
7
7
|
isRenderMode,
|
|
8
8
|
sceneSummary,
|
|
9
9
|
sheetOf
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2UJK5OCB.js";
|
|
11
11
|
import {
|
|
12
12
|
getCliVersion
|
|
13
13
|
} from "./chunk-HYCSNWYX.js";
|
|
@@ -178,7 +178,7 @@ async function handle(msg) {
|
|
|
178
178
|
if (!tool) return fail(id, -32602, `Unknown tool: ${name}`);
|
|
179
179
|
const base = await mcpEndpoint();
|
|
180
180
|
if (!base) {
|
|
181
|
-
return reply(id, { content: [{ type: "text", text:
|
|
181
|
+
return reply(id, { content: [{ type: "text", text: blenderSetupHint() }], isError: true });
|
|
182
182
|
}
|
|
183
183
|
try {
|
|
184
184
|
return reply(id, await tool.run(args, base));
|
|
@@ -124,13 +124,20 @@ function blenderEndpoint() {
|
|
|
124
124
|
const raw = process.env.GENEX_BLENDER_URL?.trim();
|
|
125
125
|
return raw ? raw.replace(/\/+$/, "") : void 0;
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
function blenderSetupHint() {
|
|
128
|
+
if (process.env.GENEX_HOSTED_SESSION === "1") {
|
|
129
|
+
return [
|
|
130
|
+
"The Blender lane is off on this stand \u2014 there is no seat to acquire.",
|
|
131
|
+
" Build the space in code instead ($genex-threejs-procedural-assets); do not wait for it."
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
return [
|
|
135
|
+
"No Blender endpoint. Run the service against your own Blender in another terminal:",
|
|
136
|
+
" npx genex blender serve",
|
|
137
|
+
" export GENEX_BLENDER_URL=http://localhost:8088",
|
|
138
|
+
" GENEX_BLENDER_URL may also point at any running genex-blender service."
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
134
141
|
var SHEET_FORMATS = ["webp", "png"];
|
|
135
142
|
function sheetOf(r) {
|
|
136
143
|
if (r.contactSheet?.b64) return { b64: r.contactSheet.b64, mime: r.contactSheet.mime };
|
|
@@ -198,7 +205,8 @@ async function blenderCall(base, route, body) {
|
|
|
198
205
|
seat ? `${route} refused the seat token (401) \u2014 the seat may have been closed; run the command again to acquire a new one` : `${route} refused the request (401) \u2014 set GENEX_BLENDER_SECRET to the service's secret`
|
|
199
206
|
);
|
|
200
207
|
}
|
|
201
|
-
|
|
208
|
+
const detail = typeof json.detail === "string" ? ` \u2014 ${json.detail}` : "";
|
|
209
|
+
throw new Error(`${route} failed (${res.status}): ${json.error ?? text.slice(0, 200)}${detail}`);
|
|
202
210
|
}
|
|
203
211
|
return json;
|
|
204
212
|
} catch (err) {
|
|
@@ -431,7 +439,7 @@ export {
|
|
|
431
439
|
RENDER_MODES,
|
|
432
440
|
isRenderMode,
|
|
433
441
|
blenderEndpoint,
|
|
434
|
-
|
|
442
|
+
blenderSetupHint,
|
|
435
443
|
SHEET_FORMATS,
|
|
436
444
|
sheetOf,
|
|
437
445
|
sheetExt,
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
BLENDER_SETUP_HINT,
|
|
4
3
|
RENDER_MODES,
|
|
5
4
|
SHEET_FORMATS,
|
|
6
5
|
acquireSeat,
|
|
7
6
|
apiFetch,
|
|
8
7
|
blenderCall,
|
|
9
8
|
blenderEndpoint,
|
|
9
|
+
blenderSetupHint,
|
|
10
10
|
fetchSignedInEmail,
|
|
11
11
|
getWorkspacePath,
|
|
12
12
|
isRenderMode,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
writeProject,
|
|
23
23
|
writeUserToken,
|
|
24
24
|
writeWorkspace
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-2UJK5OCB.js";
|
|
26
26
|
import {
|
|
27
27
|
CLI_CHANNEL,
|
|
28
28
|
DEFAULT_API_URL,
|
|
@@ -700,7 +700,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
|
|
|
700
700
|
var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
701
701
|
# Genex build contract (always in effect for this game)
|
|
702
702
|
|
|
703
|
-
Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 load the owning row before the first file of each subsystem, and re-load the map after any compaction.
|
|
703
|
+
Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 a live Blender for composing levels and interiors \u2014 \`blender scene|exec|snap|import|export\` (hosted sessions with the GPU lane on, or \`blender serve\` on your own machine; the \`genex-blender-scene\` skill owns it) \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 load the owning row before the first file of each subsystem, and re-load the map after any compaction.
|
|
704
704
|
|
|
705
705
|
Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real. Reach for genex capabilities by default \u2014 they are how the world gets real assets (law 22) \u2014 but never limit your imagination \u2014 you are a powerful agent. Use your built-in sub-agents and iteration loops (\`/loop\` or your platform's equivalent) where you need them to reach outstanding results. Verify yourself. Anything you put in front of the player should already feel alive and impressive \u2014 never a bare scene waiting for "later". Build what's still missing before polishing what already works.
|
|
706
706
|
|
|
@@ -20613,7 +20613,7 @@ async function runMotion(opts) {
|
|
|
20613
20613
|
// src/commands/blender.ts
|
|
20614
20614
|
import fs23 from "fs/promises";
|
|
20615
20615
|
import path22 from "path";
|
|
20616
|
-
var SUBS2 = ["demo", "exec", "snap", "scene", "export", "reset", "mcp", "serve"];
|
|
20616
|
+
var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve"];
|
|
20617
20617
|
var DEFAULT_OUT_DIR = "assets/blender";
|
|
20618
20618
|
async function writeB64(dir, name, b64) {
|
|
20619
20619
|
await fs23.mkdir(dir, { recursive: true });
|
|
@@ -20644,7 +20644,7 @@ async function runBlender(opts) {
|
|
|
20644
20644
|
return serveLocalBlender({ port, log });
|
|
20645
20645
|
}
|
|
20646
20646
|
if (sub === "mcp") {
|
|
20647
|
-
const { runBlenderMcp } = await import("./blender-mcp-
|
|
20647
|
+
const { runBlenderMcp } = await import("./blender-mcp-3U5AG7WS.js");
|
|
20648
20648
|
return runBlenderMcp();
|
|
20649
20649
|
}
|
|
20650
20650
|
let base = blenderEndpoint();
|
|
@@ -20659,8 +20659,8 @@ async function runBlender(opts) {
|
|
|
20659
20659
|
return 1;
|
|
20660
20660
|
}
|
|
20661
20661
|
if (!base) {
|
|
20662
|
-
const [first, ...rest] =
|
|
20663
|
-
log.error(first ?? "
|
|
20662
|
+
const [first, ...rest] = blenderSetupHint().split("\n");
|
|
20663
|
+
log.error(first ?? "No Blender endpoint");
|
|
20664
20664
|
log.plain(rest.join("\n"));
|
|
20665
20665
|
return 1;
|
|
20666
20666
|
}
|
|
@@ -20706,12 +20706,31 @@ async function runBlender(opts) {
|
|
|
20706
20706
|
case "export": {
|
|
20707
20707
|
const target = opts.out ?? path22.join(outDir, "scene.glb");
|
|
20708
20708
|
const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
|
|
20709
|
+
if (!r.glbBase64) {
|
|
20710
|
+
log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
|
|
20711
|
+
return 1;
|
|
20712
|
+
}
|
|
20709
20713
|
await fs23.mkdir(path22.dirname(target), { recursive: true });
|
|
20710
|
-
await fs23.writeFile(target, Buffer.from(r.glbBase64
|
|
20714
|
+
await fs23.writeFile(target, Buffer.from(r.glbBase64, "base64"));
|
|
20711
20715
|
log.success(`Exported ${r.bytes ?? 0} bytes`);
|
|
20712
20716
|
log.plain(` ${c.cyan(target)}`);
|
|
20713
20717
|
return 0;
|
|
20714
20718
|
}
|
|
20719
|
+
case "import": {
|
|
20720
|
+
const url = opts.importUrl?.trim();
|
|
20721
|
+
if (!url || !/^https?:\/\//.test(url)) {
|
|
20722
|
+
log.error("genex blender import needs a GLB URL: genex blender import https://\u2026/model-glb");
|
|
20723
|
+
return 1;
|
|
20724
|
+
}
|
|
20725
|
+
const r = await blenderCall(base, "/import", { url });
|
|
20726
|
+
log.success(`imported ${r.count ?? 0}: ${(r.imported ?? []).join(", ") || "(nothing)"}`);
|
|
20727
|
+
const snap = await blenderCall(base, "/render", { ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
|
|
20728
|
+
const sheet = sheetOf(snap);
|
|
20729
|
+
if (sheet) log.plain(` ${c.cyan(await writeB64(outDir, `contact-sheet.${sheetExt(sheet.mime)}`, sheet.b64))}`);
|
|
20730
|
+
const info = await blenderCall(base, "/scene");
|
|
20731
|
+
reportScene(log, info);
|
|
20732
|
+
return 0;
|
|
20733
|
+
}
|
|
20715
20734
|
case "exec":
|
|
20716
20735
|
case "demo": {
|
|
20717
20736
|
let script;
|
|
@@ -21734,6 +21753,13 @@ ${c.bold("Usage")}
|
|
|
21734
21753
|
signature move. --lean keeps movement to forward
|
|
21735
21754
|
walk + run (enemies follow a path).
|
|
21736
21755
|
genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
|
|
21756
|
+
genex blender <verb> Compose a level or interior in a live Blender \u2014
|
|
21757
|
+
scene | exec --in <file.py> | snap [--mode \u2026] |
|
|
21758
|
+
import <glb-url> | export --out <file.glb> | reset.
|
|
21759
|
+
Hosted sessions get a GPU seat on first use; at a
|
|
21760
|
+
terminal run 'genex blender serve' against your
|
|
21761
|
+
own Blender. Every scene change writes
|
|
21762
|
+
./assets/blender/contact-sheet.webp \u2014 look at it.
|
|
21737
21763
|
genex wait <id> Attach to a generation enqueued with --no-wait and
|
|
21738
21764
|
print its asset URL(s) when it finishes. Safe to
|
|
21739
21765
|
re-run \u2014 it never creates a new generation.
|
|
@@ -22430,6 +22456,13 @@ function parseArgs(argv) {
|
|
|
22430
22456
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
22431
22457
|
return parsed;
|
|
22432
22458
|
}
|
|
22459
|
+
} else if (parsed.command === "blender") {
|
|
22460
|
+
if (parsed.options.name === "import" && parsed.options.importUrl === void 0) {
|
|
22461
|
+
parsed.options.importUrl = arg;
|
|
22462
|
+
} else {
|
|
22463
|
+
parsed.error = `Unexpected argument: ${arg}`;
|
|
22464
|
+
return parsed;
|
|
22465
|
+
}
|
|
22433
22466
|
} else {
|
|
22434
22467
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
22435
22468
|
return parsed;
|
|
@@ -22950,9 +22983,8 @@ async function main() {
|
|
|
22950
22983
|
case "ui":
|
|
22951
22984
|
await runUi({ ...parsed.options, sub: parsed.options.name });
|
|
22952
22985
|
break;
|
|
22953
|
-
//
|
|
22954
|
-
//
|
|
22955
|
-
// stand sets, so this is a local-only verb until that service is real.
|
|
22986
|
+
// The Blender surface (apps/blender-service): a hosted seat, an explicit
|
|
22987
|
+
// GENEX_BLENDER_URL, or `blender serve` — see commands/blender.ts.
|
|
22956
22988
|
case "blender":
|
|
22957
22989
|
process.exitCode = await runBlender({ ...parsed.options, sub: parsed.options.name });
|
|
22958
22990
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0-dev.560",
|
|
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": {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-blender-scene
|
|
3
|
+
description: Compose a level, arena, interior, building or map — a SPACE of rooms, lanes, walls and sites that must relate — in a live Blender with `npx genex blender`, then export one GLB the game walks through. Use when the request is a place to move through (from a description or a floor plan), not a single prop; probe the lane first and build the same space in code if it is off.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Blender · Scene
|
|
7
|
+
|
|
8
|
+
Build the game's SPACE in a real Blender that answers over HTTP: write a Python
|
|
9
|
+
script, run it, read the numbers, look at the four-view sheet, repeat — then
|
|
10
|
+
export one GLB and load it like any model. The scene persists between steps, so a
|
|
11
|
+
level grows the way a level is actually built: shell, then zones, then dressing.
|
|
12
|
+
|
|
13
|
+
## When this lane owns the work
|
|
14
|
+
|
|
15
|
+
- A **space**: a level, arena, interior, building, map, dungeon, station — several
|
|
16
|
+
rooms, lanes, walls or sites that must relate to each other, and a player who
|
|
17
|
+
moves through them.
|
|
18
|
+
- A **floor plan or reference** to trace (a sketch, an ASCII plan, a photo of a room).
|
|
19
|
+
- A **blockout**: the playable shape first, dressing later.
|
|
20
|
+
|
|
21
|
+
Not this lane:
|
|
22
|
+
|
|
23
|
+
- One recognisable object (a barrel, a cart, a statue) → `$genex-ai-model`.
|
|
24
|
+
- One parameterized object placed many times with variation, or a stylized shader
|
|
25
|
+
material → `$genex-threejs-procedural-assets`.
|
|
26
|
+
- The player's body, creatures, animation → `$genex-ai-character`,
|
|
27
|
+
`$genex-threejs-creatures`. Terrain from a heightmap → code.
|
|
28
|
+
|
|
29
|
+
Mixing all three in one scene is normal: the Blender level is the architecture, a
|
|
30
|
+
generated GLB is the hero piece, code is the repeated dressing.
|
|
31
|
+
|
|
32
|
+
## Is the lane on? Probe by doing
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx genex blender scene
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Read the answer literally — there are three:
|
|
39
|
+
|
|
40
|
+
1. **A scene JSON prints** → the lane is on. Continue.
|
|
41
|
+
2. `Warming up a Blender seat…` and then `The Blender seat is still warming up. Run
|
|
42
|
+
the same command again in a minute.` → **not done yet**, even though the command
|
|
43
|
+
exited cleanly. A GPU is booting for you (about 20 s on a warm pod, a few
|
|
44
|
+
minutes on a cold one). Write your first script meanwhile, then run the command
|
|
45
|
+
again. Never read this line as success, and never read it as failure.
|
|
46
|
+
3. `The Blender lane is off on this stand` or `No Blender seat: fleet_full` → build
|
|
47
|
+
the same space **in code** with `$genex-threejs-procedural-assets`, say so in one
|
|
48
|
+
plain line to the player, and keep going. Do not wait, do not ask.
|
|
49
|
+
|
|
50
|
+
The seat, once granted, is remembered in `.genex/blender-seat.json` — later
|
|
51
|
+
commands are silent about it.
|
|
52
|
+
|
|
53
|
+
## The loop
|
|
54
|
+
|
|
55
|
+
Scripts are the level's source. Keep them in the project, one per zone:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
blender/01-shell.py walls, floor, towers
|
|
59
|
+
blender/02-gatehouse.py
|
|
60
|
+
blender/03-courtyard.py
|
|
61
|
+
…
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run one:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx genex blender exec --in blender/01-shell.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Every run prints two things and writes one file:
|
|
71
|
+
|
|
72
|
+
- **The numbers** — `objects N meshes N tris N materials N radius N`. This is
|
|
73
|
+
the gate. Before you run, know what the script should produce (twelve objects,
|
|
74
|
+
about 4k tris, radius ≈ 40); after, compare. A render judge is unreliable for
|
|
75
|
+
pass/fail; the numbers are not.
|
|
76
|
+
- **The sheet** — `assets/blender/contact-sheet.webp`, four fixed views (front,
|
|
77
|
+
side, TOP, hero) from cameras derived from the scene bounds, so two steps compare
|
|
78
|
+
pixel for pixel. **Open it with your file-reading tool after every step.** The
|
|
79
|
+
TOP view is the floor-plan check; the hero view is the mood check. Nothing shows
|
|
80
|
+
it to you unless you look.
|
|
81
|
+
- Script `print()` output comes back as stdout; a raised exception comes back with
|
|
82
|
+
its traceback AND the sheet of the half-built scene, which is usually where the
|
|
83
|
+
bug is visible.
|
|
84
|
+
|
|
85
|
+
The Python namespace **persists between runs** (`bpy`, `bmesh`, `mathutils`,
|
|
86
|
+
`math`, `random` are preloaded): define `mat()` and `box()` helpers once in the
|
|
87
|
+
first script and use them in every later one. If a run says the service restarted
|
|
88
|
+
and the scene was restored from a checkpoint, re-read the numbers — your last step
|
|
89
|
+
may be missing; re-run that one script.
|
|
90
|
+
|
|
91
|
+
Other verbs:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx genex blender snap --mode wireframe # topology; also normals | solid | lit
|
|
95
|
+
npx genex blender scene # the full graph as JSON, when a number surprises you
|
|
96
|
+
npx genex blender reset # empty scene (the namespace resets too)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Keep each step under about 90 seconds of Blender time: a script that loops
|
|
100
|
+
hundreds of boolean cuts is killed at the deadline and the scene comes back from
|
|
101
|
+
the last checkpoint. Split heavy work across scripts. A level is 8–15 steps, not 50.
|
|
102
|
+
|
|
103
|
+
## Craft rules that decide whether the export works
|
|
104
|
+
|
|
105
|
+
- **Metres.** Floor at `z = 0`, the map's centre at the origin. Blender is Z-up;
|
|
106
|
+
the export converts to Y-up, so the floor is the XZ plane in Three.js and a
|
|
107
|
+
Blender `(x, y, z)` lands at `(x, z, -y)`.
|
|
108
|
+
- **Name everything** `<zone>_<thing>` (`gate_portcullis`, `hall_table_1`). Names
|
|
109
|
+
survive the export and are how the game finds parts.
|
|
110
|
+
- **Materials: few and flat.** Six to eight for a whole level, one per surface
|
|
111
|
+
family (stone, dark stone, wood, roof, iron, glow). Set the Principled BSDF base
|
|
112
|
+
colour **and** `material.diffuse_color` — the sheet's fallback renderer reads the
|
|
113
|
+
second, and a scene that sets only the first exports with correct colours and
|
|
114
|
+
renders entirely grey. No image textures: they do not fit the export ceiling, and
|
|
115
|
+
an asset URL does not export.
|
|
116
|
+
- **Repetition by loop, never by an unapplied modifier.** Crenellations, columns,
|
|
117
|
+
pews: a Python `for`. An Array modifier that is not applied is not in the GLB.
|
|
118
|
+
- **Tri budget.** Keep the whole level under ~150k triangles and use 12–16 segments
|
|
119
|
+
on cylinders — phones load this file too. The export refuses above 6 MB; treat
|
|
120
|
+
that as "no longer a game asset", not as a limit to work around.
|
|
121
|
+
- **Markers.** Empties export as named nodes: `bpy.data.objects.new("spawn", None)`
|
|
122
|
+
placed and linked into the scene collection, likewise `key_1`, `door_crypt`,
|
|
123
|
+
`patrol_1`. The level tells the game where things go; the game reads them with
|
|
124
|
+
`scene.getObjectByName("spawn")`.
|
|
125
|
+
- **Lights stay in the game.** Blender lights do not export usefully; light the
|
|
126
|
+
level in Three.js (a hemisphere + a sun, point lights at torches and hearths).
|
|
127
|
+
- **Collision is the game's.** Static level geometry gets Rapier trimesh colliders
|
|
128
|
+
per mesh — `$genex-threejs-physics-rapier`. If a mesh is too dense to collide
|
|
129
|
+
against, add a simplified `col_<zone>` box in Blender and keep the visual one out
|
|
130
|
+
of physics.
|
|
131
|
+
|
|
132
|
+
A minimal first script, the shape every later one follows:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import bpy
|
|
136
|
+
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
137
|
+
|
|
138
|
+
def mat(name, rgb, rough=0.85):
|
|
139
|
+
m = bpy.data.materials.new(name)
|
|
140
|
+
m.use_nodes = True
|
|
141
|
+
b = m.node_tree.nodes["Principled BSDF"]
|
|
142
|
+
b.inputs["Base Color"].default_value = (*rgb, 1.0)
|
|
143
|
+
b.inputs["Roughness"].default_value = rough
|
|
144
|
+
m.diffuse_color = (*rgb, 1.0) # BOTH, or the sheet renders grey
|
|
145
|
+
return m
|
|
146
|
+
|
|
147
|
+
def box(name, loc, half, m):
|
|
148
|
+
bpy.ops.mesh.primitive_cube_add(size=2.0, location=loc)
|
|
149
|
+
o = bpy.context.active_object
|
|
150
|
+
o.name, o.scale = name, half
|
|
151
|
+
o.data.materials.append(m)
|
|
152
|
+
return o
|
|
153
|
+
|
|
154
|
+
STONE = mat("Stone", (0.46, 0.44, 0.41))
|
|
155
|
+
box("yard_floor", (0, 0, -0.25), (35, 35, 0.25), mat("Ground", (0.30, 0.28, 0.24)))
|
|
156
|
+
for sx, sy in ((1, 1), (1, -1), (-1, 1), (-1, -1)):
|
|
157
|
+
bpy.ops.mesh.primitive_cylinder_add(radius=3, depth=14, vertices=16,
|
|
158
|
+
location=(sx * 35, sy * 35, 7))
|
|
159
|
+
t = bpy.context.active_object
|
|
160
|
+
t.name = f"tower_{sx}_{sy}"
|
|
161
|
+
t.data.materials.append(STONE)
|
|
162
|
+
print("shell:", len(bpy.context.scene.objects), "objects")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Hero pieces from `genex model`
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npx genex model "weathered stone knight effigy on a tomb, medieval" # prints a URL
|
|
169
|
+
npx genex blender import https://assets.genex.technology/generations/<id>/model-glb
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`import` prints the object names it added and renders a fresh sheet; place and
|
|
173
|
+
scale them in the next `exec` (`bpy.data.objects["…"].location = (…)`). Import
|
|
174
|
+
when the piece must be composed WITH the architecture (a gate in a wall, a statue
|
|
175
|
+
in a niche). When it is dressing, skip the import: drop a marker in Blender and
|
|
176
|
+
load the GLB in Three.js at that marker — it keeps the level file small.
|
|
177
|
+
|
|
178
|
+
## Export and load
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npx genex blender export --out public/assets/level.glb
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Load it like any GLB. The quality kit's rung ladder applies only to generated
|
|
185
|
+
`/generations/` URLs, so this file loads as-is — the tri and material budgets
|
|
186
|
+
above are what keep it phone-safe.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { createGltfLoader } from "./controllers/quality/gltf-loader.ts";
|
|
190
|
+
|
|
191
|
+
const gltfLoader = createGltfLoader(renderer);
|
|
192
|
+
const level = await gltfLoader.loader.loadAsync("/assets/level.glb");
|
|
193
|
+
level.scene.traverse((o) => {
|
|
194
|
+
if ((o as THREE.Mesh).isMesh) { o.castShadow = true; o.receiveShadow = true; }
|
|
195
|
+
});
|
|
196
|
+
scene.add(level.scene);
|
|
197
|
+
const spawn = level.scene.getObjectByName("spawn");
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Then colliders (`$genex-threejs-physics-rapier`), the player
|
|
201
|
+
(`$genex-threejs-character-controller`), lights, and a real screenshot check
|
|
202
|
+
(`$genex-threejs-visual-validation`). Record the level in `DESIGN.md`'s Assets as
|
|
203
|
+
`local Blender build — scripts under blender/`, never a provider generation id.
|
|
204
|
+
|
|
205
|
+
## Money
|
|
206
|
+
|
|
207
|
+
The seat bills a few credits **per minute while it is held, idle or not**, and it
|
|
208
|
+
ends with the session — there is no close command. So build the level in one
|
|
209
|
+
sitting: shell → zones → hero pieces → export, then move on to the game. Batch
|
|
210
|
+
changes into fewer, larger scripts; every step renders one sheet.
|
|
211
|
+
|
|
212
|
+
## Troubleshooting
|
|
213
|
+
|
|
214
|
+
- `payload_too_large` on export → fewer cylinder segments, delete faces the player
|
|
215
|
+
never sees, or export two GLBs (`--out public/assets/level-a.glb`).
|
|
216
|
+
- The sheet is grey → `material.diffuse_color` was not set (see above).
|
|
217
|
+
- `refused the seat token (401)` → the seat was closed (the session rested, or the
|
|
218
|
+
pod was replaced); run the same command again — it re-acquires, and a replaced
|
|
219
|
+
pod restores your scene from its checkpoint.
|
|
220
|
+
- `seat_not_ready` → the pod is still booting; run again shortly.
|
|
221
|
+
- The numbers are right but the top view is wrong → coordinates: `y` in Blender is
|
|
222
|
+
depth, and `scale` on a 2-unit cube is the half-size.
|
|
@@ -132,6 +132,7 @@ vendored code from memory of another engine.
|
|
|
132
132
|
| phone-survivable rendering: device tiers, DPR/shadow/post budgets, runtime governor, generated-asset rungs, Quality picker, dispose-on-swap | `$genex-threejs-adaptive-quality` |
|
|
133
133
|
| anything falls, collides, gets pushed, or needs colliders/events | `$genex-threejs-physics-rapier` |
|
|
134
134
|
| launch/docking timelines, authored transform phases, springs, convergence, deterministic prop/debris motion | `$genex-threejs-procedural-animation` |
|
|
135
|
+
| a level, arena, interior, building or map to walk through — a composed space of rooms, lanes, walls and sites, from a description or a floor plan; a blockout | `$genex-blender-scene` — probe the lane first (`npx genex blender scene`); when it is off, the same space is built as code via `$genex-threejs-procedural-assets` |
|
|
135
136
|
| rebuild a reference prop, hard-surface object, modular decoration, or structural environment piece (kits, fences, walls, paving) as editable parameterized Three.js code — held to the asset bar at the end of this section | `$genex-threejs-procedural-assets` |
|
|
136
137
|
| stylized/abstract shader-authored materials, the procedural/PBR material boundary | `$genex-threejs-procedural-assets` |
|
|
137
138
|
| particles, trails, plasma, shockwaves, pooled bursts, and event effects | `$genex-threejs-procedural-vfx` |
|
|
@@ -164,8 +165,10 @@ honestly fit: a generated GLB built from the picture itself
|
|
|
164
165
|
(`npx genex model --image <path>`), or editable parameterized Three.js code.
|
|
165
166
|
If the request says procedural, parametric, code-built, reusable variations,
|
|
166
167
|
or names a GLB, start the corresponding route directly. An image alone never
|
|
167
|
-
auto-triggers the procedural lane.
|
|
168
|
-
|
|
168
|
+
auto-triggers the procedural lane. A floor plan, a room photo or a map sketch
|
|
169
|
+
is a SPACE, not an object: it goes to `$genex-blender-scene` with no route
|
|
170
|
+
question. Player bodies, rigged characters/creatures, and animation stay with
|
|
171
|
+
their protected owners.
|
|
169
172
|
|
|
170
173
|
If a procedural request has a private reference, use it locally and do not
|
|
171
174
|
upload, publish, or commit it without explicit permission. If no reference was
|
|
@@ -107,7 +107,10 @@ parametric, or code-built work. It is a first-class lane, not a fallback, held
|
|
|
107
107
|
to the same bar as a generated GLB, and mixing it with generated GLBs in one
|
|
108
108
|
scene is the normal way to build a detailed world. An image alone still doesn't decide the route (ask the one
|
|
109
109
|
route question when both honestly fit), and never use it for player bodies,
|
|
110
|
-
rigged characters/creatures, or animation.
|
|
110
|
+
rigged characters/creatures, or animation. An OBJECT is this lane's unit; a
|
|
111
|
+
SPACE the player moves through — a level, an interior, a whole building — is
|
|
112
|
+
`$genex-blender-scene`'s, which falls back to this lane when the Blender lane
|
|
113
|
+
is off on the stand.
|
|
111
114
|
|
|
112
115
|
- Prefer a supplied reference. A private attachment stays local unless the
|
|
113
116
|
player explicitly permits upload, publication, or commit.
|