@genex-ai/cli-demo 0.21.0 → 0.23.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 +49 -90
- 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-explore/SKILL.md +10 -3
- 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;
|
|
@@ -1913,11 +1870,6 @@ async function exists2(p) {
|
|
|
1913
1870
|
async function runExplore(opts) {
|
|
1914
1871
|
const log = createLogger({ quiet: opts.quiet });
|
|
1915
1872
|
const query = opts.query?.trim();
|
|
1916
|
-
if (!query) {
|
|
1917
|
-
log.error('Missing query. Usage: genex explore "<what you need>"');
|
|
1918
|
-
process.exitCode = 1;
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
1873
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
1922
1874
|
let res;
|
|
1923
1875
|
try {
|
|
@@ -1933,17 +1885,21 @@ async function runExplore(opts) {
|
|
|
1933
1885
|
return;
|
|
1934
1886
|
}
|
|
1935
1887
|
const { items } = await res.json();
|
|
1936
|
-
const ranked = rank(items, query).slice(0, 5);
|
|
1888
|
+
const ranked = query ? rank(items, query).slice(0, 5) : [...items].sort((a, b) => b.playsCount - a.playsCount);
|
|
1937
1889
|
if (opts.json) {
|
|
1938
1890
|
console.log(JSON.stringify(ranked, null, 2));
|
|
1939
1891
|
return;
|
|
1940
1892
|
}
|
|
1941
1893
|
if (ranked.length === 0) {
|
|
1942
1894
|
log.plain(
|
|
1943
|
-
`No curated projects matched "${query}".
|
|
1895
|
+
query ? `No curated projects matched "${query}". Run \`genex explore\` with no arguments to browse the whole catalog, or try a broader term (terrain, grass, vehicle, water, building, dungeon, fish, shader).` : `The curated catalog is empty right now \u2014 check back soon.`
|
|
1944
1896
|
);
|
|
1945
1897
|
return;
|
|
1946
1898
|
}
|
|
1899
|
+
if (!query) {
|
|
1900
|
+
log.plain(c.bold(`The full curated catalog (${ranked.length} project${ranked.length === 1 ? "" : "s"}):`));
|
|
1901
|
+
log.plain("");
|
|
1902
|
+
}
|
|
1947
1903
|
for (const [i, p] of ranked.entries()) {
|
|
1948
1904
|
const meta = [
|
|
1949
1905
|
p.categories.join(", "),
|
|
@@ -1979,9 +1935,9 @@ function rank(items, query) {
|
|
|
1979
1935
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
|
|
1980
1936
|
function getVersion() {
|
|
1981
1937
|
try {
|
|
1982
|
-
const here =
|
|
1938
|
+
const here = path12.dirname(fileURLToPath2(import.meta.url));
|
|
1983
1939
|
const pkg = JSON.parse(
|
|
1984
|
-
readFileSync(
|
|
1940
|
+
readFileSync(path12.resolve(here, "..", "package.json"), "utf8")
|
|
1985
1941
|
);
|
|
1986
1942
|
return pkg.version ?? "0.0.0";
|
|
1987
1943
|
} catch {
|
|
@@ -2003,8 +1959,9 @@ ${c.bold("Usage")}
|
|
|
2003
1959
|
genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
|
|
2004
1960
|
genex controller <type> [--force] Install a physics controller (character|car|drone)
|
|
2005
1961
|
into src/controllers (+ assets into public/assets).
|
|
2006
|
-
genex explore "<query>" [options]
|
|
2007
|
-
Three.js systems you can clone or borrow parts
|
|
1962
|
+
genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
|
|
1963
|
+
Three.js systems you can clone or borrow parts
|
|
1964
|
+
from. No query lists the whole catalog.
|
|
2008
1965
|
|
|
2009
1966
|
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
2010
1967
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
@@ -2051,8 +2008,9 @@ ${c.bold("Options for `preview` / `publish`")}
|
|
|
2051
2008
|
--env <path> Token env file (default: ~/.genex/env).
|
|
2052
2009
|
|
|
2053
2010
|
${c.bold("Options for `explore`")}
|
|
2054
|
-
--json Print the
|
|
2011
|
+
--json Print the matches as JSON (for tools/agents).
|
|
2055
2012
|
--api-url <url> Override the API base URL.
|
|
2013
|
+
(No query at all lists the entire curated catalog.)
|
|
2056
2014
|
|
|
2057
2015
|
${c.bold("Global")}
|
|
2058
2016
|
--quiet Reduce output.
|
|
@@ -2079,6 +2037,7 @@ ${c.bold("Examples")}
|
|
|
2079
2037
|
genex texture "mossy cracked cobblestone" --terrain
|
|
2080
2038
|
genex controller character
|
|
2081
2039
|
genex explore "grass"
|
|
2040
|
+
genex explore
|
|
2082
2041
|
`;
|
|
2083
2042
|
function parseArgs(argv) {
|
|
2084
2043
|
const parsed = {
|
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
|
|
|
@@ -23,6 +23,13 @@ Examples: `npx genex explore "grass"`, `npx genex explore "vehicle physics"`,
|
|
|
23
23
|
`npx genex explore "procedural building"`. No sign-in needed — this works even
|
|
24
24
|
before `genex init`.
|
|
25
25
|
|
|
26
|
+
**Not sure what exists?** Run it with no arguments to browse the whole curated
|
|
27
|
+
catalog first — it's small and every entry is proven:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx genex explore
|
|
31
|
+
```
|
|
32
|
+
|
|
26
33
|
Each result prints everything you need to act on it:
|
|
27
34
|
|
|
28
35
|
- `play:` — open it in the browser to judge whether it fits.
|
|
@@ -36,8 +43,7 @@ Add `--json` for machine-readable output.
|
|
|
36
43
|
**As a NEW game (start from the whole project):**
|
|
37
44
|
|
|
38
45
|
1. `git clone <clone URL from the output> <name>` — pick a short one-word name.
|
|
39
|
-
2. `cd <name>`, then `npx @genex-ai/cli-demo@latest init <name>` —
|
|
40
|
-
"Ignore files and continue" if it warns the folder isn't empty; never use
|
|
46
|
+
2. `cd <name>`, then `npx @genex-ai/cli-demo@latest init <name>` — never use
|
|
41
47
|
`--force`. This creates your own project; the original is untouched.
|
|
42
48
|
3. `npm install`, keep `base: './'` in `vite.config`, then build your changes
|
|
43
49
|
and ship with `npx genex preview`.
|
|
@@ -64,6 +70,7 @@ build from one:
|
|
|
64
70
|
- keep any in-game credits screens or notices in place;
|
|
65
71
|
- carry the credit into anything you publish or remix from it.
|
|
66
72
|
|
|
67
|
-
If nothing matches your query,
|
|
73
|
+
If nothing matches your query, run `npx genex explore` with no arguments to
|
|
74
|
+
see everything available, or try a broader term (terrain, grass, vehicle,
|
|
68
75
|
water, building, dungeon, fish, shader) — or build it with the regular
|
|
69
76
|
`genex-threejs-*` skills instead.
|
|
@@ -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)
|