@genex-ai/cli-demo 0.21.0 → 0.22.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 +18 -18
- package/dist/index.js +37 -80
- package/package.json +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +17 -17
- package/templates/skills/genex-ai-sfx/SKILL.md +10 -6
- package/templates/skills/genex-ai-skybox/SKILL.md +11 -10
- package/templates/skills/genex-ai-texture/SKILL.md +10 -10
- package/templates/skills/genex-getting-started/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ genex init <name> # authorize + create the draft project
|
|
|
8
8
|
genex link <slug> # re-link this folder to an existing game of yours (recovery)
|
|
9
9
|
genex preview # build + push to the hosted draft URL (unlisted)
|
|
10
10
|
genex publish # build + push, then list the game in the gallery
|
|
11
|
-
genex model "<prompt>" # generate a 3D model →
|
|
12
|
-
genex skybox "<prompt>" # generate a 360° sky →
|
|
13
|
-
genex sfx "<prompt>" # generate a sound fx →
|
|
14
|
-
genex texture "<prompt>" # generate a texture →
|
|
11
|
+
genex model "<prompt>" # generate a 3D model → prints an asset URL
|
|
12
|
+
genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
|
|
13
|
+
genex sfx "<prompt>" # generate a sound fx → prints an asset URL
|
|
14
|
+
genex texture "<prompt>" # generate a texture → prints an asset URL
|
|
15
15
|
genex controller <type> # install a tuned character|car|drone controller → src/controllers/
|
|
16
16
|
```
|
|
17
17
|
|
|
@@ -70,22 +70,22 @@ Defaults: API `https://demo-api.glotech.world`, auth site
|
|
|
70
70
|
## Generating assets
|
|
71
71
|
|
|
72
72
|
`genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a real,
|
|
73
|
-
game-ready asset
|
|
74
|
-
|
|
73
|
+
game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
|
|
74
|
+
stream), then prints its public URL — the game loads it directly, nothing is
|
|
75
|
+
downloaded or committed:
|
|
75
76
|
|
|
76
|
-
| Command | Provider |
|
|
77
|
+
| Command | Provider | Prints (role) |
|
|
77
78
|
| --- | --- | --- |
|
|
78
|
-
| `genex model "<prompt>"` | Tripo |
|
|
79
|
-
| `genex skybox "<prompt>"` | Blockade Labs |
|
|
80
|
-
| `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs |
|
|
81
|
-
| `genex texture "<prompt>" [--terrain]` | Gemini |
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`genex-ai-<kind>` skill with the exact Three.js loader code.
|
|
79
|
+
| `genex model "<prompt>"` | Tripo | `…/generations/<id>/model-glb` |
|
|
80
|
+
| `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
|
|
81
|
+
| `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
|
|
82
|
+
| `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
|
|
83
|
+
|
|
84
|
+
Each URL is a permanent `https://assets.genex.technology/...` address served straight
|
|
85
|
+
from R2 (with CORS, so three.js loads it cross-origin without tainting). It resolves
|
|
86
|
+
the same in local dev, the published game, and remixes, and the bytes never pass back
|
|
87
|
+
through this API. Each kind has a scaffolded `genex-ai-<kind>` skill with the exact
|
|
88
|
+
Three.js loader code.
|
|
89
89
|
|
|
90
90
|
Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
|
|
91
91
|
provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path12 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -1468,25 +1468,6 @@ async function runPreview(opts) {
|
|
|
1468
1468
|
log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
|
|
1469
1469
|
}
|
|
1470
1470
|
|
|
1471
|
-
// src/commands/generate.ts
|
|
1472
|
-
import path12 from "path";
|
|
1473
|
-
|
|
1474
|
-
// src/lib/assets.ts
|
|
1475
|
-
import fs10 from "fs/promises";
|
|
1476
|
-
import path11 from "path";
|
|
1477
|
-
function slugify(input) {
|
|
1478
|
-
const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
|
|
1479
|
-
return s || "asset";
|
|
1480
|
-
}
|
|
1481
|
-
async function downloadToFile(url, dest, headers) {
|
|
1482
|
-
const res = await fetch(url, { headers });
|
|
1483
|
-
if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
|
|
1484
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
1485
|
-
await fs10.mkdir(path11.dirname(dest), { recursive: true });
|
|
1486
|
-
await fs10.writeFile(dest, buf);
|
|
1487
|
-
return buf.byteLength;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
1471
|
// src/lib/sse.ts
|
|
1491
1472
|
async function* readSSE(body) {
|
|
1492
1473
|
const decoder = new TextDecoder();
|
|
@@ -1527,24 +1508,7 @@ async function* readSSE(body) {
|
|
|
1527
1508
|
}
|
|
1528
1509
|
|
|
1529
1510
|
// src/commands/generate.ts
|
|
1530
|
-
var PUBLIC_PREFIX = "public/";
|
|
1531
|
-
var KIND_DIR = {
|
|
1532
|
-
model: "public/assets/models",
|
|
1533
|
-
skybox: "public/assets/skybox",
|
|
1534
|
-
sfx: "public/assets/sfx",
|
|
1535
|
-
texture: "public/assets/textures"
|
|
1536
|
-
};
|
|
1537
|
-
function runtimePath(rel) {
|
|
1538
|
-
return `./${rel.replace(PUBLIC_PREFIX, "")}`;
|
|
1539
|
-
}
|
|
1540
1511
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1541
|
-
function fileDest(kind, slug, f) {
|
|
1542
|
-
if (kind === "texture") {
|
|
1543
|
-
const name = f.role.replace(/^texture-/, "");
|
|
1544
|
-
return `public/assets/textures/${slug}/${name}.${f.ext}`;
|
|
1545
|
-
}
|
|
1546
|
-
return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
|
|
1547
|
-
}
|
|
1548
1512
|
async function runGenerate(kind, opts) {
|
|
1549
1513
|
const log = createLogger({ quiet: opts.quiet });
|
|
1550
1514
|
const prompt = opts.prompt?.trim();
|
|
@@ -1591,7 +1555,7 @@ async function runGenerate(kind, opts) {
|
|
|
1591
1555
|
return;
|
|
1592
1556
|
}
|
|
1593
1557
|
if (opts.noWait) {
|
|
1594
|
-
log.success(`Queued (${id}). Re-run without --no-wait to
|
|
1558
|
+
log.success(`Queued (${id}). Re-run without --no-wait to get the finished asset URL.`);
|
|
1595
1559
|
return;
|
|
1596
1560
|
}
|
|
1597
1561
|
log.step("Generating\u2026 (this can take up to a minute)");
|
|
@@ -1609,25 +1573,20 @@ async function runGenerate(kind, opts) {
|
|
|
1609
1573
|
process.exitCode = 1;
|
|
1610
1574
|
return;
|
|
1611
1575
|
}
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
for (const f of view.files ?? []) {
|
|
1616
|
-
const rel = fileDest(kind, slug, f);
|
|
1617
|
-
const url = `${apiUrl}/api/generations/${id}/asset/${f.role}`;
|
|
1618
|
-
const bytes = await downloadToFile(url, path12.join(process.cwd(), rel), {
|
|
1619
|
-
Authorization: `Bearer ${token}`
|
|
1620
|
-
});
|
|
1621
|
-
written.push(rel);
|
|
1622
|
-
log.success(`${rel} ${c.dim(`(${(bytes / 1024).toFixed(0)} KB)`)}`);
|
|
1623
|
-
}
|
|
1624
|
-
} catch (err) {
|
|
1625
|
-
log.error(`Download failed: ${String(err)}`);
|
|
1576
|
+
const files = view.files ?? [];
|
|
1577
|
+
if (files.length === 0) {
|
|
1578
|
+
log.error("Generation completed but produced no files.");
|
|
1626
1579
|
process.exitCode = 1;
|
|
1627
1580
|
return;
|
|
1628
1581
|
}
|
|
1629
1582
|
log.plain("");
|
|
1630
|
-
|
|
1583
|
+
log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
|
|
1584
|
+
for (const f of files) {
|
|
1585
|
+
const label = files.length > 1 ? `${c.dim(`${f.role.replace(/^texture-/, "")}: `)}` : "";
|
|
1586
|
+
log.plain(` ${label}${f.url}`);
|
|
1587
|
+
}
|
|
1588
|
+
log.plain("");
|
|
1589
|
+
printHint(kind, files, log);
|
|
1631
1590
|
}
|
|
1632
1591
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1633
1592
|
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
@@ -1706,23 +1665,21 @@ async function poll(apiUrl, token, id, onProgress) {
|
|
|
1706
1665
|
}
|
|
1707
1666
|
return null;
|
|
1708
1667
|
}
|
|
1709
|
-
function printHint(kind,
|
|
1710
|
-
const
|
|
1668
|
+
function printHint(kind, files, log) {
|
|
1669
|
+
const url = files[0]?.url ?? "";
|
|
1711
1670
|
const hint = {
|
|
1712
|
-
model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill.
|
|
1713
|
-
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox.
|
|
1714
|
-
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx.
|
|
1715
|
-
texture: `Load with TextureLoader
|
|
1671
|
+
model: `Load with GLTFLoader from the URL and add it to the scene \u2014 see the genex-ai-model skill. url = "${url}"`,
|
|
1672
|
+
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
|
|
1673
|
+
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
|
|
1674
|
+
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`
|
|
1716
1675
|
};
|
|
1717
|
-
log.success(
|
|
1718
|
-
"Done. Asset saved into public/assets (so Vite ships it; load it as ./assets/\u2026 \u2014 `genex publish` commits and ships it with the game)."
|
|
1719
|
-
);
|
|
1720
1676
|
log.dim(` ${hint[kind]}`);
|
|
1677
|
+
log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
|
|
1721
1678
|
}
|
|
1722
1679
|
|
|
1723
1680
|
// src/commands/controller.ts
|
|
1724
|
-
import
|
|
1725
|
-
import
|
|
1681
|
+
import fs10 from "fs/promises";
|
|
1682
|
+
import path11 from "path";
|
|
1726
1683
|
var CONTROLLER_KINDS = ["character", "car", "drone"];
|
|
1727
1684
|
var SHARED = [
|
|
1728
1685
|
"shared/math.ts",
|
|
@@ -1801,8 +1758,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1801
1758
|
]
|
|
1802
1759
|
}
|
|
1803
1760
|
};
|
|
1804
|
-
var CODE_DEST =
|
|
1805
|
-
var ASSETS_DEST =
|
|
1761
|
+
var CODE_DEST = path11.join("src", "controllers");
|
|
1762
|
+
var ASSETS_DEST = path11.join("public", "assets");
|
|
1806
1763
|
async function runController(opts) {
|
|
1807
1764
|
const log = createLogger({ quiet: opts.quiet });
|
|
1808
1765
|
const kind = opts.kind?.trim();
|
|
@@ -1815,31 +1772,31 @@ async function runController(opts) {
|
|
|
1815
1772
|
process.exitCode = 1;
|
|
1816
1773
|
return;
|
|
1817
1774
|
}
|
|
1818
|
-
const srcDir =
|
|
1775
|
+
const srcDir = path11.join(getTemplatesDir(), "controllers");
|
|
1819
1776
|
const root = opts.cwd ?? process.cwd();
|
|
1820
1777
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
1821
1778
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
1822
1779
|
log.plain("");
|
|
1823
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
1780
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path11.sep)}`);
|
|
1824
1781
|
const plan = [
|
|
1825
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
1782
|
+
...set.code.map((rel) => ({ from: rel, rel: path11.join(CODE_DEST, rel) })),
|
|
1826
1783
|
...set.assets.map((rel) => ({
|
|
1827
1784
|
from: rel,
|
|
1828
|
-
rel:
|
|
1785
|
+
rel: path11.join(ASSETS_DEST, path11.basename(rel))
|
|
1829
1786
|
}))
|
|
1830
1787
|
];
|
|
1831
1788
|
let copied = 0;
|
|
1832
1789
|
let skipped = 0;
|
|
1833
1790
|
try {
|
|
1834
1791
|
for (const file of plan) {
|
|
1835
|
-
const dest =
|
|
1792
|
+
const dest = path11.join(root, file.rel);
|
|
1836
1793
|
if (!opts.force && await exists2(dest)) {
|
|
1837
1794
|
skipped++;
|
|
1838
1795
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
1839
1796
|
continue;
|
|
1840
1797
|
}
|
|
1841
|
-
await
|
|
1842
|
-
await
|
|
1798
|
+
await fs10.mkdir(path11.dirname(dest), { recursive: true });
|
|
1799
|
+
await fs10.copyFile(path11.join(srcDir, file.from), dest);
|
|
1843
1800
|
copied++;
|
|
1844
1801
|
log.dim(` ${file.rel}`);
|
|
1845
1802
|
}
|
|
@@ -1871,8 +1828,8 @@ async function runController(opts) {
|
|
|
1871
1828
|
}
|
|
1872
1829
|
async function installOwnerAvatar(args) {
|
|
1873
1830
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
1874
|
-
const dest =
|
|
1875
|
-
await
|
|
1831
|
+
const dest = path11.join(root, ASSETS_DEST, "avatar.vrm");
|
|
1832
|
+
await fs10.mkdir(path11.dirname(dest), { recursive: true });
|
|
1876
1833
|
if (token) {
|
|
1877
1834
|
try {
|
|
1878
1835
|
const res = await fetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -1884,7 +1841,7 @@ async function installOwnerAvatar(args) {
|
|
|
1884
1841
|
const vrmRes = await fetch(me.vrmUrl);
|
|
1885
1842
|
if (vrmRes.ok) {
|
|
1886
1843
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
1887
|
-
await
|
|
1844
|
+
await fs10.writeFile(dest, buf);
|
|
1888
1845
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
1889
1846
|
return;
|
|
1890
1847
|
}
|
|
@@ -1895,14 +1852,14 @@ async function installOwnerAvatar(args) {
|
|
|
1895
1852
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
1896
1853
|
}
|
|
1897
1854
|
}
|
|
1898
|
-
await
|
|
1855
|
+
await fs10.copyFile(path11.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
1899
1856
|
log.dim(
|
|
1900
1857
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
1901
1858
|
);
|
|
1902
1859
|
}
|
|
1903
1860
|
async function exists2(p) {
|
|
1904
1861
|
try {
|
|
1905
|
-
await
|
|
1862
|
+
await fs10.access(p);
|
|
1906
1863
|
return true;
|
|
1907
1864
|
} catch {
|
|
1908
1865
|
return false;
|
|
@@ -1979,9 +1936,9 @@ function rank(items, query) {
|
|
|
1979
1936
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
|
|
1980
1937
|
function getVersion() {
|
|
1981
1938
|
try {
|
|
1982
|
-
const here =
|
|
1939
|
+
const here = path12.dirname(fileURLToPath2(import.meta.url));
|
|
1983
1940
|
const pkg = JSON.parse(
|
|
1984
|
-
readFileSync(
|
|
1941
|
+
readFileSync(path12.resolve(here, "..", "package.json"), "utf8")
|
|
1985
1942
|
);
|
|
1986
1943
|
return pkg.version ?? "0.0.0";
|
|
1987
1944
|
} catch {
|
package/package.json
CHANGED
|
@@ -21,16 +21,17 @@ npx genex model "<prompt>"
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Write a specific prompt — "weathered wooden barrel with rusted iron bands" beats
|
|
24
|
-
"barrel". The command blocks until the mesh is ready (up to ~a minute), then
|
|
24
|
+
"barrel". The command blocks until the mesh is ready (up to ~a minute), then prints
|
|
25
|
+
its public URL:
|
|
25
26
|
|
|
26
27
|
```
|
|
27
|
-
|
|
28
|
+
https://assets.genex.technology/generations/<id>/model-glb
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
The GLB lives in Genex storage (R2) and loads straight from that URL. You **don't
|
|
32
|
+
download it** and **nothing is committed to your repo** — copy the URL the command
|
|
33
|
+
prints into your loader. The URL is permanent, so it works the same in local dev, your
|
|
34
|
+
published game, and remixes.
|
|
34
35
|
|
|
35
36
|
## Models that point somewhere must face forward
|
|
36
37
|
|
|
@@ -62,14 +63,16 @@ side — reads as broken at a glance.
|
|
|
62
63
|
|
|
63
64
|
## Load it into the scene
|
|
64
65
|
|
|
65
|
-
Use Three.js `GLTFLoader`
|
|
66
|
-
|
|
66
|
+
Use Three.js `GLTFLoader` with the URL the command printed (R2 sends the right CORS
|
|
67
|
+
headers, so cross-origin loading just works):
|
|
67
68
|
|
|
68
69
|
```ts
|
|
69
70
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
70
71
|
|
|
71
72
|
const loader = new GLTFLoader();
|
|
72
|
-
|
|
73
|
+
// the URL `npx genex model` printed:
|
|
74
|
+
const MODEL_URL = "https://assets.genex.technology/generations/<id>/model-glb";
|
|
75
|
+
const gltf = await loader.loadAsync(MODEL_URL);
|
|
73
76
|
const model = gltf.scene;
|
|
74
77
|
model.scale.setScalar(1); // tune to taste
|
|
75
78
|
model.position.set(0, 0, 0);
|
|
@@ -86,17 +89,14 @@ scene is a ghost: players and objects pass straight through it.
|
|
|
86
89
|
|
|
87
90
|
## Publish checklist (so players see the model)
|
|
88
91
|
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
prompt's publish step).
|
|
93
|
-
- Keep generated assets under `public/assets/` — Vite only ships files under `public/`
|
|
94
|
-
(or files you `import`); `npx genex publish` ships that folder with the game.
|
|
92
|
+
- Load it from the **URL** the command printed — it's absolute and permanent, so it
|
|
93
|
+
resolves the same in local dev, the published game, and remixes. Nothing to commit.
|
|
94
|
+
- Don't copy the GLB into `public/assets/` — generated assets live in R2, not the repo.
|
|
95
95
|
|
|
96
96
|
## Options
|
|
97
97
|
|
|
98
|
-
- `--no-wait` — enqueue and return immediately (
|
|
99
|
-
|
|
98
|
+
- `--no-wait` — enqueue and return immediately (won't print the URL; re-run without
|
|
99
|
+
`--no-wait` to get it).
|
|
100
100
|
- `--api-url <url>` — override the API base (local dev).
|
|
101
101
|
|
|
102
102
|
## Troubleshooting
|
|
@@ -15,13 +15,15 @@ npx genex sfx "punchy laser zap, short and dry" --duration 2
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
`--duration <sec>` (0.5–22) sets a target length; omit it to let the model pick.
|
|
18
|
-
Blocks until ready, then
|
|
18
|
+
Blocks until ready, then prints its public URL:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
-
|
|
21
|
+
https://assets.genex.technology/generations/<id>/audio-sfx
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
The mp3 lives in Genex storage (R2) and loads straight from that URL — you don't download
|
|
25
|
+
it and nothing is committed to your repo. The URL is permanent (local dev, published game,
|
|
26
|
+
and remixes alike).
|
|
25
27
|
|
|
26
28
|
## Play it in Three.js
|
|
27
29
|
|
|
@@ -36,7 +38,9 @@ import * as THREE from "three";
|
|
|
36
38
|
const listener = new THREE.AudioListener();
|
|
37
39
|
camera.add(listener);
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
// the URL `npx genex sfx` printed (R2 sends CORS headers, so cross-origin works):
|
|
42
|
+
const SFX_URL = "https://assets.genex.technology/generations/<id>/audio-sfx";
|
|
43
|
+
const buffer = await new THREE.AudioLoader().loadAsync(SFX_URL);
|
|
40
44
|
|
|
41
45
|
const sound = new THREE.PositionalAudio(listener);
|
|
42
46
|
sound.setBuffer(buffer);
|
|
@@ -55,8 +59,8 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
55
59
|
|
|
56
60
|
## Publish checklist
|
|
57
61
|
|
|
58
|
-
-
|
|
59
|
-
|
|
62
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
63
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
60
64
|
|
|
61
65
|
## Options
|
|
62
66
|
|
|
@@ -23,13 +23,15 @@ also lights it (image-based lighting).
|
|
|
23
23
|
npx genex skybox "<prompt>"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Blocks until ready, then
|
|
26
|
+
Blocks until ready, then prints its public URL:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
|
|
29
|
+
https://assets.genex.technology/generations/<id>/skybox-equirect
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The
|
|
32
|
+
The image lives in Genex storage (R2) and loads straight from that URL — you don't
|
|
33
|
+
download it and nothing is committed to your repo. The URL is permanent (works the same
|
|
34
|
+
in local dev, the published game, and remixes).
|
|
33
35
|
|
|
34
36
|
## Load it as background + environment
|
|
35
37
|
|
|
@@ -39,9 +41,9 @@ background and the lighting:
|
|
|
39
41
|
```ts
|
|
40
42
|
import * as THREE from "three";
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
44
|
+
// the URL `npx genex skybox` printed (R2 sends CORS headers, so cross-origin works):
|
|
45
|
+
const SKYBOX_URL = "https://assets.genex.technology/generations/<id>/skybox-equirect";
|
|
46
|
+
const texture = await new THREE.TextureLoader().loadAsync(SKYBOX_URL);
|
|
45
47
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
46
48
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
47
49
|
|
|
@@ -62,10 +64,9 @@ scene.background = texture; // keep the raw texture for the visible sky
|
|
|
62
64
|
|
|
63
65
|
## Publish checklist
|
|
64
66
|
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
ships that folder with the game.
|
|
67
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
68
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
69
|
+
- Don't copy the image into `public/assets/` — generated assets live in R2, not the repo.
|
|
69
70
|
|
|
70
71
|
## Options
|
|
71
72
|
|
|
@@ -24,13 +24,15 @@ npx genex texture "<prompt>"
|
|
|
24
24
|
npx genex texture "lush green grass" --terrain # seamless tiling for ground/terrain
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Blocks until ready, then
|
|
27
|
+
Blocks until ready, then prints its public URL:
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
|
|
30
|
+
https://assets.genex.technology/generations/<id>/texture-basecolor
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
The image lives in Genex storage (R2) and loads straight from that URL — you don't
|
|
34
|
+
download it and nothing is committed to your repo. The URL is permanent (local dev,
|
|
35
|
+
published game, and remixes alike).
|
|
34
36
|
|
|
35
37
|
> **Scope:** v1 generates the **base-color (albedo)** map only. Normal / roughness
|
|
36
38
|
> / AO are a planned follow-up — for now set sensible `roughness`/`metalness`
|
|
@@ -41,11 +43,9 @@ Committed by `npx genex publish`, so it ships with your game.
|
|
|
41
43
|
```ts
|
|
42
44
|
import * as THREE from "three";
|
|
43
45
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
const map = await new THREE.TextureLoader().loadAsync(
|
|
47
|
-
"./assets/textures/lush-green-grass/basecolor.png",
|
|
48
|
-
);
|
|
46
|
+
// the URL `npx genex texture` printed (R2 sends CORS headers, so cross-origin works):
|
|
47
|
+
const TEXTURE_URL = "https://assets.genex.technology/generations/<id>/texture-basecolor";
|
|
48
|
+
const map = await new THREE.TextureLoader().loadAsync(TEXTURE_URL);
|
|
49
49
|
map.colorSpace = THREE.SRGBColorSpace;
|
|
50
50
|
map.wrapS = map.wrapT = THREE.RepeatWrapping;
|
|
51
51
|
map.repeat.set(8, 8); // tile count — raise for large surfaces
|
|
@@ -69,8 +69,8 @@ scene.add(ground);
|
|
|
69
69
|
|
|
70
70
|
## Publish checklist
|
|
71
71
|
|
|
72
|
-
-
|
|
73
|
-
|
|
72
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
73
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
74
74
|
|
|
75
75
|
## Options
|
|
76
76
|
|
|
@@ -28,9 +28,9 @@ validation.
|
|
|
28
28
|
|
|
29
29
|
## Generating real assets
|
|
30
30
|
|
|
31
|
-
Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt.
|
|
32
|
+
Each command prints a permanent `assets.genex.technology` URL you load directly —
|
|
33
|
+
the asset lives in Genex storage (R2), not your repo, so there's nothing to commit:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
|