@genex-ai/cli-demo 0.36.0 → 0.37.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 +10 -5
- package/dist/index.js +38 -8
- package/package.json +1 -1
- package/templates/skills/genex-ai-image/SKILL.md +181 -0
- package/templates/skills/genex-ai-video/SKILL.md +148 -0
- package/templates/skills/genex-getting-started/SKILL.md +4 -1
- package/templates/skills/genex-threejs-skill-router/SKILL.md +8 -7
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ genex model "<prompt>" # generate a 3D model → prints an asset URL
|
|
|
13
13
|
genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
|
|
14
14
|
genex sfx "<prompt>" # generate a sound fx → prints an asset URL
|
|
15
15
|
genex texture "<prompt>" # generate a texture → prints an asset URL
|
|
16
|
+
genex image "<prompt>" # generate an image → prints an asset URL
|
|
17
|
+
genex video "<prompt>" # generate a video → prints an asset URL
|
|
16
18
|
genex controller <type> # install a tuned character|car|drone controller → src/controllers/
|
|
17
19
|
```
|
|
18
20
|
|
|
@@ -72,8 +74,8 @@ Defaults: API `https://demo-api.glotech.world`, auth site
|
|
|
72
74
|
|
|
73
75
|
## Generating assets
|
|
74
76
|
|
|
75
|
-
`genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a
|
|
76
|
-
game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
|
|
77
|
+
`genex model | skybox | sfx | texture | image | video "<prompt>"` turn a prompt into a
|
|
78
|
+
real, game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
|
|
77
79
|
stream), then prints its public URL — the game loads it directly, nothing is
|
|
78
80
|
downloaded or committed:
|
|
79
81
|
|
|
@@ -83,6 +85,8 @@ downloaded or committed:
|
|
|
83
85
|
| `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
|
|
84
86
|
| `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
|
|
85
87
|
| `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
|
|
88
|
+
| `genex image "<prompt>" [--transparent] [--aspect <ratio>]` | fal.ai | `…/generations/<id>/image-main` |
|
|
89
|
+
| `genex video "<prompt>" [--duration <s>] [--loop]` | fal.ai | `…/generations/<id>/video-mp4` |
|
|
86
90
|
|
|
87
91
|
Each URL is a permanent `https://assets.genex.technology/...` address served straight
|
|
88
92
|
from R2 (with CORS, so three.js loads it cross-origin without tainting). It resolves
|
|
@@ -92,9 +96,10 @@ Three.js loader code.
|
|
|
92
96
|
|
|
93
97
|
Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
|
|
94
98
|
provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
|
|
95
|
-
`ELEVENLABS_API_KEY`, `GEMINI_API_KEY`); when a key
|
|
96
|
-
a built-in **mock** provider that returns sample
|
|
97
|
-
`--no-wait` enqueues without downloading
|
|
99
|
+
`ELEVENLABS_API_KEY`, `GEMINI_API_KEY`, and `FAL_KEY` for image + video); when a key
|
|
100
|
+
is unset that kind falls back to a built-in **mock** provider that returns sample
|
|
101
|
+
assets, so the flow runs keyless. `--no-wait` enqueues without downloading — useful
|
|
102
|
+
for video, which is minutes-class end-to-end.
|
|
98
103
|
|
|
99
104
|
## Install / run
|
|
100
105
|
|
package/dist/index.js
CHANGED
|
@@ -1790,6 +1790,10 @@ async function runGenerate(kind, opts) {
|
|
|
1790
1790
|
const options = {};
|
|
1791
1791
|
if (kind === "texture" && opts.terrain) options.terrain = true;
|
|
1792
1792
|
if (kind === "sfx" && opts.duration) options.durationSeconds = opts.duration;
|
|
1793
|
+
if (kind === "image" && opts.transparent) options.transparent = true;
|
|
1794
|
+
if (kind === "image" && opts.aspect) options.aspect = opts.aspect;
|
|
1795
|
+
if (kind === "video" && opts.duration) options.durationSeconds = opts.duration;
|
|
1796
|
+
if (kind === "video" && opts.loop) options.loop = true;
|
|
1793
1797
|
log.plain(c.bold(`genex ${kind}`));
|
|
1794
1798
|
log.dim(` ${prompt}`);
|
|
1795
1799
|
log.plain("");
|
|
@@ -1826,9 +1830,10 @@ async function runGenerate(kind, opts) {
|
|
|
1826
1830
|
}
|
|
1827
1831
|
log.step("Generating\u2026 (this can take up to a minute)");
|
|
1828
1832
|
const onProgress = (p) => log.dim(` ${p}%`);
|
|
1829
|
-
const
|
|
1833
|
+
const timeoutMs = waitTimeoutFor(kind);
|
|
1834
|
+
const deadline = Date.now() + timeoutMs;
|
|
1830
1835
|
const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
|
|
1831
|
-
const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress) : streamed;
|
|
1836
|
+
const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
|
|
1832
1837
|
if (!view) {
|
|
1833
1838
|
log.error("Timed out waiting for the generation.");
|
|
1834
1839
|
process.exitCode = 1;
|
|
@@ -1855,6 +1860,11 @@ async function runGenerate(kind, opts) {
|
|
|
1855
1860
|
printHint(kind, files, log);
|
|
1856
1861
|
}
|
|
1857
1862
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1863
|
+
var KIND_WAIT_TIMEOUT_MS = {
|
|
1864
|
+
video: 15 * 60 * 1e3
|
|
1865
|
+
// 15 min
|
|
1866
|
+
};
|
|
1867
|
+
var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
|
|
1858
1868
|
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
1859
1869
|
async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
1860
1870
|
let connectFailures = 0;
|
|
@@ -1907,8 +1917,8 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
|
1907
1917
|
}
|
|
1908
1918
|
return null;
|
|
1909
1919
|
}
|
|
1910
|
-
async function poll(apiUrl, token, id, onProgress) {
|
|
1911
|
-
const deadline = Date.now() +
|
|
1920
|
+
async function poll(apiUrl, token, id, onProgress, timeoutMs) {
|
|
1921
|
+
const deadline = Date.now() + timeoutMs;
|
|
1912
1922
|
let last = -1;
|
|
1913
1923
|
while (Date.now() < deadline) {
|
|
1914
1924
|
try {
|
|
@@ -1937,7 +1947,9 @@ function printHint(kind, files, log) {
|
|
|
1937
1947
|
model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
|
|
1938
1948
|
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
|
|
1939
1949
|
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
|
|
1940
|
-
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role
|
|
1950
|
+
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
|
|
1951
|
+
image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
|
|
1952
|
+
video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`
|
|
1941
1953
|
};
|
|
1942
1954
|
log.dim(` ${hint[kind]}`);
|
|
1943
1955
|
log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
|
|
@@ -2198,7 +2210,7 @@ function rank(items, query) {
|
|
|
2198
2210
|
}
|
|
2199
2211
|
|
|
2200
2212
|
// src/index.ts
|
|
2201
|
-
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
|
|
2213
|
+
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "image", "video"]);
|
|
2202
2214
|
var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize, and publish 3D games.
|
|
2203
2215
|
|
|
2204
2216
|
${c.bold("Usage")}
|
|
@@ -2214,15 +2226,20 @@ ${c.bold("Usage")}
|
|
|
2214
2226
|
genex skybox "<prompt>" [options] Generate a skybox (equirect) into public/assets/skybox.
|
|
2215
2227
|
genex sfx "<prompt>" [options] Generate a sound effect (mp3) into public/assets/sfx.
|
|
2216
2228
|
genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
|
|
2229
|
+
genex image "<prompt>" [options] Generate an image (PNG); prints a public asset URL.
|
|
2230
|
+
genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
|
|
2217
2231
|
genex controller <type> [--force] Install a physics controller (character|car|drone)
|
|
2218
2232
|
into src/controllers (+ assets into public/assets).
|
|
2219
2233
|
genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
|
|
2220
2234
|
Three.js systems you can clone or borrow parts
|
|
2221
2235
|
from. No query lists the whole catalog.
|
|
2222
2236
|
|
|
2223
|
-
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
2237
|
+
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `video`)")}
|
|
2224
2238
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
2225
|
-
--duration <sec> (sfx) target clip length in seconds.
|
|
2239
|
+
--duration <sec> (sfx, video) target clip length in seconds.
|
|
2240
|
+
--transparent (image) transparent background (alpha) \u2014 for decals/stickers.
|
|
2241
|
+
--aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
|
|
2242
|
+
--loop (video) generate a seamless loop.
|
|
2226
2243
|
--no-wait Enqueue only; don't block waiting for the asset.
|
|
2227
2244
|
--api-url <url> Override the API base URL.
|
|
2228
2245
|
--env <path> Token env file (default: ~/.genex/env).
|
|
@@ -2300,6 +2317,9 @@ ${c.bold("Examples")}
|
|
|
2300
2317
|
genex skybox "golden hour over a misty mountain range"
|
|
2301
2318
|
genex sfx "punchy laser zap" --duration 2
|
|
2302
2319
|
genex texture "mossy cracked cobblestone" --terrain
|
|
2320
|
+
genex image "retro arcade poster art" --aspect portrait
|
|
2321
|
+
genex image "neon graffiti tag, spray-paint style" --transparent
|
|
2322
|
+
genex video "swirling neon plasma, seamless loop" --loop
|
|
2303
2323
|
genex controller character
|
|
2304
2324
|
genex explore "grass"
|
|
2305
2325
|
genex explore
|
|
@@ -2326,6 +2346,7 @@ function parseArgs(argv) {
|
|
|
2326
2346
|
"--categories",
|
|
2327
2347
|
"--timeout",
|
|
2328
2348
|
"--duration",
|
|
2349
|
+
"--aspect",
|
|
2329
2350
|
"--source-repo-url",
|
|
2330
2351
|
"--source-author",
|
|
2331
2352
|
"--license",
|
|
@@ -2360,6 +2381,12 @@ function parseArgs(argv) {
|
|
|
2360
2381
|
case "--terrain":
|
|
2361
2382
|
parsed.options.terrain = true;
|
|
2362
2383
|
break;
|
|
2384
|
+
case "--transparent":
|
|
2385
|
+
parsed.options.transparent = true;
|
|
2386
|
+
break;
|
|
2387
|
+
case "--loop":
|
|
2388
|
+
parsed.options.loop = true;
|
|
2389
|
+
break;
|
|
2363
2390
|
case "--force":
|
|
2364
2391
|
parsed.options.force = true;
|
|
2365
2392
|
break;
|
|
@@ -2471,6 +2498,9 @@ function applyValueFlag(options, flag, value) {
|
|
|
2471
2498
|
options.duration = n;
|
|
2472
2499
|
break;
|
|
2473
2500
|
}
|
|
2501
|
+
case "--aspect":
|
|
2502
|
+
options.aspect = value;
|
|
2503
|
+
break;
|
|
2474
2504
|
}
|
|
2475
2505
|
}
|
|
2476
2506
|
async function main() {
|
package/package.json
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-ai-image
|
|
3
|
+
description: Generate a real image (PNG/JPEG) from a text prompt with `npx genex image`, then load it into Three.js on any mesh, plane, or sprite. Use for posters, paintings, billboards, signs, logos, sprites, card/item art, loading screens, textures for in-game screens, and decals/stickers ("wanted poster", "arcade cabinet marquee", "neon graffiti tag") rather than a procedural/shader look. Pass `--transparent` for anything with an alpha channel.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex AI · Image
|
|
7
|
+
|
|
8
|
+
Turn a prompt into a real raster image and put it anywhere in the game — a poster,
|
|
9
|
+
a painting, a billboard, a sign, a logo, a sprite, card/item art, a loading screen,
|
|
10
|
+
art on an in-game screen, or a decal/sticker.
|
|
11
|
+
|
|
12
|
+
## When to use this vs. procedural materials
|
|
13
|
+
|
|
14
|
+
- **Use `npx genex image`** for a specific, recognizable picture you can describe —
|
|
15
|
+
"vintage travel poster of Mars", "guild crest with crossed swords", "arcade
|
|
16
|
+
marquee art". You get a real image.
|
|
17
|
+
- **Use `$genex-threejs-procedural-materials`** for stylized/abstract or fully
|
|
18
|
+
parametric surfaces authored in shaders. Use `$genex-ai-texture` for a *tiling*
|
|
19
|
+
PBR surface (floors, ground, walls) — this skill is for a single flat picture.
|
|
20
|
+
|
|
21
|
+
## Run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx genex image "<prompt>"
|
|
25
|
+
npx genex image "neon graffiti tag, spray-paint style" --transparent # PNG with alpha (decals/stickers/logos)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Blocks until ready, then prints its public URL:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
https://assets.genex.technology/generations/<id>/image-main
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The image lives in Genex storage (R2) and loads straight from that URL — you don't
|
|
35
|
+
download it and nothing is committed to your repo. The URL is permanent (local dev,
|
|
36
|
+
published game, and remixes alike).
|
|
37
|
+
|
|
38
|
+
## Load it into the scene
|
|
39
|
+
|
|
40
|
+
Load the image and apply it to any mesh, plane, or sprite:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import * as THREE from "three";
|
|
44
|
+
|
|
45
|
+
// the URL `npx genex image` printed (R2 sends CORS headers, so cross-origin works):
|
|
46
|
+
const IMAGE_URL = "https://assets.genex.technology/generations/<id>/image-main";
|
|
47
|
+
const map = await new THREE.TextureLoader().loadAsync(IMAGE_URL);
|
|
48
|
+
map.colorSpace = THREE.SRGBColorSpace; // pictures are sRGB — without this they look washed/dark
|
|
49
|
+
map.anisotropy = renderer.capabilities.getMaxAnisotropy(); // stays sharp at grazing angles
|
|
50
|
+
|
|
51
|
+
// a poster/painting/sign on a wall — a flat plane:
|
|
52
|
+
const poster = new THREE.Mesh(
|
|
53
|
+
new THREE.PlaneGeometry(2, 3), // match the image aspect (w:h)
|
|
54
|
+
new THREE.MeshStandardMaterial({ map, roughness: 0.9, metalness: 0 }),
|
|
55
|
+
);
|
|
56
|
+
scene.add(poster);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For a `--transparent` PNG, set `transparent: true` on the material so the alpha shows.
|
|
60
|
+
For a screen-space sprite (HUD art, an icon) use `new THREE.Sprite(new THREE.SpriteMaterial({ map }))`.
|
|
61
|
+
For art that must glow (a lit sign, a screen) use `MeshBasicMaterial` (unlit) so scene lighting doesn't darken it.
|
|
62
|
+
|
|
63
|
+
## Decals & stickers
|
|
64
|
+
|
|
65
|
+
**Anything applied *on top* of a surface — a decal, sticker, spray tag, logo, or
|
|
66
|
+
bullet hole — needs an alpha channel, so always generate it with `--transparent`.**
|
|
67
|
+
Without alpha you get an opaque rectangle instead of a shaped mark.
|
|
68
|
+
|
|
69
|
+
Project it onto the target mesh with `DecalGeometry` (the canonical spray-paint look —
|
|
70
|
+
clips to the surface and wraps around corners):
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import * as THREE from "three";
|
|
74
|
+
import { DecalGeometry } from "three/addons/geometries/DecalGeometry.js";
|
|
75
|
+
|
|
76
|
+
// ONE shared texture + material for all sprays (generated with --transparent):
|
|
77
|
+
const map = await new THREE.TextureLoader().loadAsync(IMAGE_URL);
|
|
78
|
+
map.colorSpace = THREE.SRGBColorSpace;
|
|
79
|
+
map.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
|
80
|
+
const sprayMat = new THREE.MeshStandardMaterial({
|
|
81
|
+
map, transparent: true, depthTest: true, depthWrite: false,
|
|
82
|
+
polygonOffset: true, polygonOffsetFactor: -4, // pulls the decal forward — kills z-fighting
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const raycaster = new THREE.Raycaster();
|
|
86
|
+
const helper = new THREE.Object3D(); // orientation scratch — never added to the scene
|
|
87
|
+
const decals: THREE.Mesh[] = [];
|
|
88
|
+
const MAX_DECALS = 20;
|
|
89
|
+
|
|
90
|
+
function spray(): void {
|
|
91
|
+
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); // screen centre
|
|
92
|
+
const hit = raycaster.intersectObjects(sprayables, false)[0]; // sprayables = your wall meshes
|
|
93
|
+
if (!hit || !hit.face) return;
|
|
94
|
+
const n = hit.face.normal.clone() // face.normal is LOCAL space —
|
|
95
|
+
.applyNormalMatrix(new THREE.Matrix3().getNormalMatrix(hit.object.matrixWorld)); // must transform it
|
|
96
|
+
helper.position.copy(hit.point);
|
|
97
|
+
helper.lookAt(hit.point.clone().add(n));
|
|
98
|
+
const S = 1; // metres; for non-square art size.y = S * imgH/imgW
|
|
99
|
+
const geom = new DecalGeometry(hit.object as THREE.Mesh, hit.point,
|
|
100
|
+
helper.rotation.clone(), new THREE.Vector3(S, S, S * 0.5)); // size.z = wrap depth
|
|
101
|
+
const decal = new THREE.Mesh(geom, sprayMat);
|
|
102
|
+
decal.renderOrder = 100 + decals.length; // newer sprays draw on top
|
|
103
|
+
(hit.object as THREE.Mesh).attach(decal); // verts are world-space; attach keeps them correct
|
|
104
|
+
decals.push(decal);
|
|
105
|
+
if (decals.length > MAX_DECALS) { // FIFO cap — dispose the oldest
|
|
106
|
+
const old = decals.shift()!;
|
|
107
|
+
old.removeFromParent();
|
|
108
|
+
old.geometry.dispose(); // geometry is per-spray — MUST dispose; the shared material/map are never disposed here
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.addEventListener("keydown", (e) => {
|
|
113
|
+
if (e.code === "KeyT" && !e.repeat) spray();
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Gotchas (all bite in practice):
|
|
118
|
+
|
|
119
|
+
- **`hit.face.normal` is LOCAL space** — apply the normal-matrix from `matrixWorld`.
|
|
120
|
+
Skipping it "works by accident" only on an unrotated, unscaled wall; any rotated
|
|
121
|
+
wall gets the decal facing the wrong way.
|
|
122
|
+
- **`depthWrite: false` means overlap order is draw order** — increment `renderOrder`
|
|
123
|
+
per spray, or stacked decals flicker and sort wrongly.
|
|
124
|
+
- **One decal clips against ONE mesh** — a spray straddling two wall meshes gets cut
|
|
125
|
+
at the boundary. Pass the single `hit.object`, or make walls separate meshes.
|
|
126
|
+
- **`size.z` too large on a thin wall wraps the decal onto the back face** (visible
|
|
127
|
+
from behind). Keep `size.z` below the wall thickness.
|
|
128
|
+
- **`DecalGeometry` is static-mesh only** — no `SkinnedMesh`/morph targets. For a
|
|
129
|
+
moving or skinned target, use a small `PlaneGeometry` quad offset along the normal
|
|
130
|
+
(`hit.point + n * 0.01`, `quad.lookAt(hit.point.clone().add(n))`) instead.
|
|
131
|
+
|
|
132
|
+
## Multiplayer
|
|
133
|
+
|
|
134
|
+
The asset URL is public, permanent, and CORS-open, so it is safe to broadcast the
|
|
135
|
+
string to every player. A placed-at-runtime mark (a decal included) is static shared
|
|
136
|
+
world state — put it on `room.shared`, **not** `objects` (no movement to smooth) and
|
|
137
|
+
**not** `send` (late joiners would see a bare wall; `shared` keys replay on connect).
|
|
138
|
+
|
|
139
|
+
Use a **fixed ring of slots** and overwrite the oldest — **never a fresh key per
|
|
140
|
+
spray**. The relay caps game-writable `shared` keys at **256 per room, keys are
|
|
141
|
+
permanent and undeletable, and new keys past the cap are silently dropped forever**,
|
|
142
|
+
so a new key per spray eventually breaks the game:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
let next = 0;
|
|
146
|
+
const RING = 32; // decal:0 .. decal:31
|
|
147
|
+
function placeShared(url: string, hit: { point: THREE.Vector3; normal: THREE.Vector3 }) {
|
|
148
|
+
room.shared.set(`decal:${next % RING}`, { url, p: hit.point.toArray(), n: hit.normal.toArray() });
|
|
149
|
+
next++;
|
|
150
|
+
}
|
|
151
|
+
room.on("shared", (key, value) => { // every player (incl. late joiners) rebuilds the decal
|
|
152
|
+
if (key.startsWith("decal:") && value) spawnDecalFromShared(value);
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
See `$genex-threejs-multiplayer` for the `shared` channel rules and the room API.
|
|
157
|
+
|
|
158
|
+
## Publish checklist
|
|
159
|
+
|
|
160
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
161
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
162
|
+
- Don't copy the image into `public/assets/` — generated assets live in R2, not the repo.
|
|
163
|
+
|
|
164
|
+
## Options
|
|
165
|
+
|
|
166
|
+
- `--transparent` — PNG with an alpha channel (mandatory for decals/stickers/logos —
|
|
167
|
+
anything laid on top of a surface).
|
|
168
|
+
- `--aspect <ratio>` — image shape (e.g. `square`, `16:9`, `9:16`); default is square.
|
|
169
|
+
- `--no-wait` — enqueue and return immediately (the file won't be downloaded;
|
|
170
|
+
re-run without `--no-wait` to fetch it).
|
|
171
|
+
- `--api-url <url>` — override the API base (local dev).
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
|
|
175
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
176
|
+
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
177
|
+
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
178
|
+
- **Decal is an opaque rectangle** — the image has no alpha. Regenerate with
|
|
179
|
+
`--transparent`.
|
|
180
|
+
- **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
|
|
181
|
+
- **Decal blurs at oblique angles** — set `map.anisotropy = renderer.capabilities.getMaxAnisotropy()`.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-ai-video
|
|
3
|
+
description: Generate a real video clip (H.264 mp4) from a text prompt with `npx genex video`, then play it in Three.js on any surface via VideoTexture. Use for in-game TVs/screens/monitors, animated billboards, cutscene clips, ambient backdrops, portals, and video decals ("news broadcast on a TV", "swirling portal", "animated arcade attract screen") rather than a static image or shader effect. Pass `--loop` for a seamless loop.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex AI · Video
|
|
7
|
+
|
|
8
|
+
Turn a prompt into a real mp4 clip and play it anywhere in the game — an in-game
|
|
9
|
+
TV/screen/monitor, an animated billboard, a cutscene, an ambient backdrop, a portal,
|
|
10
|
+
or a video decal.
|
|
11
|
+
|
|
12
|
+
## When to use this vs. a static image or shader
|
|
13
|
+
|
|
14
|
+
- **Use `npx genex video`** for moving footage you can describe — "static-y CRT
|
|
15
|
+
news broadcast", "swirling neon portal", "rain running down glass". You get a real
|
|
16
|
+
mp4.
|
|
17
|
+
- **Use `$genex-ai-image`** for a single still picture, or
|
|
18
|
+
`$genex-threejs-procedural-vfx` for parametric real-time effects (particles,
|
|
19
|
+
trails, shockwaves) authored in code.
|
|
20
|
+
|
|
21
|
+
## Run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx genex video "<prompt>"
|
|
25
|
+
npx genex video "swirling neon plasma, seamless loop" --loop # seamless loop for screens/backdrops
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Video takes a **minute or two** end to end (queue + generation). If you're building
|
|
29
|
+
other things meanwhile, add `--no-wait` to enqueue and come back for it. Blocks until
|
|
30
|
+
ready, then prints its public URL:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
https://assets.genex.technology/generations/<id>/video-mp4
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The clip lives in Genex storage (R2) and loads straight from that URL — you don't
|
|
37
|
+
download it and nothing is committed to your repo. The URL is permanent (local dev,
|
|
38
|
+
published game, and remixes alike).
|
|
39
|
+
|
|
40
|
+
> **Cost & length:** the default is a **5-second, 720p** clip, and that IS the right
|
|
41
|
+
> default for a looping screen or backdrop — only pass `--duration` when the content
|
|
42
|
+
> genuinely needs to be longer (a cutscene). Longer, higher-res clips cost more and
|
|
43
|
+
> take longer. mp4 has **no alpha channel**, so a video is always a full rectangle
|
|
44
|
+
> (there are no transparent video decals).
|
|
45
|
+
|
|
46
|
+
## Play it in Three.js
|
|
47
|
+
|
|
48
|
+
Video needs an `HTMLVideoElement`, and browsers block autoplay — the first
|
|
49
|
+
`video.play()` must run inside a user gesture (any click or keypress). Wrap it in a
|
|
50
|
+
`VideoTexture` and put it on any surface:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import * as THREE from "three";
|
|
54
|
+
|
|
55
|
+
// the URL `npx genex video` printed (R2 sends CORS headers, so cross-origin works):
|
|
56
|
+
const VIDEO_URL = "https://assets.genex.technology/generations/<id>/video-mp4";
|
|
57
|
+
const video = document.createElement("video");
|
|
58
|
+
video.src = VIDEO_URL;
|
|
59
|
+
video.crossOrigin = "anonymous"; // REQUIRED to upload cross-origin video to WebGL (else a tainted-source error)
|
|
60
|
+
video.muted = true; // muted is what lets it play under autoplay policy
|
|
61
|
+
video.loop = true;
|
|
62
|
+
video.playsInline = true; // iOS: no fullscreen takeover
|
|
63
|
+
|
|
64
|
+
const texture = new THREE.VideoTexture(video); // auto-updates every frame — no per-frame code
|
|
65
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
66
|
+
|
|
67
|
+
// an in-game TV / animated billboard — MeshBasicMaterial so it glows regardless of scene lighting:
|
|
68
|
+
const screen = new THREE.Mesh(
|
|
69
|
+
new THREE.PlaneGeometry(3.2, 1.8), // 16:9
|
|
70
|
+
new THREE.MeshBasicMaterial({ map: texture }),
|
|
71
|
+
);
|
|
72
|
+
scene.add(screen);
|
|
73
|
+
|
|
74
|
+
// start playback on a user gesture (never swallow the rejection):
|
|
75
|
+
window.addEventListener("keydown", () => {
|
|
76
|
+
video.play().catch((e) => console.warn("video play", e));
|
|
77
|
+
}, { once: true });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
One `VideoTexture` can feed **many** surfaces — a bank of monitors, N sprays. Cost is
|
|
81
|
+
per distinct `<video>` element (media decode + one GPU upload per frame), **not** per
|
|
82
|
+
surface, so sharing one element for every screen is nearly free (all play in sync).
|
|
83
|
+
Keep clips **≤720p**. Pause the element (`video.pause()`) when no video surface is
|
|
84
|
+
visible.
|
|
85
|
+
|
|
86
|
+
## Video decals
|
|
87
|
+
|
|
88
|
+
A video decal is the same `DecalGeometry` projection as an image decal — read the
|
|
89
|
+
**Decals & stickers** section of `$genex-ai-image` for the full recipe and gotchas —
|
|
90
|
+
with one change: swap the `TextureLoader` map for the `VideoTexture` above, and call
|
|
91
|
+
`video.play().catch(...)` inside the spray handler (the keypress is the user gesture).
|
|
92
|
+
All decals can share the one `VideoTexture`. Because mp4 has no alpha, a video decal
|
|
93
|
+
is a full rectangle (fine for a screen-shaped mark; use `$genex-ai-image --transparent`
|
|
94
|
+
for a shaped sticker). **Layering gotcha:** an opaque video decal draws in the opaque
|
|
95
|
+
pass — *before* any `transparent: true` image decal — so image decals always land on
|
|
96
|
+
top regardless of spray order or `renderOrder`. If you mix both and need strict
|
|
97
|
+
newest-on-top, set `transparent: true` on the video decal material too.
|
|
98
|
+
|
|
99
|
+
## Multiplayer
|
|
100
|
+
|
|
101
|
+
The asset URL is public, permanent, and CORS-open, so it is safe to broadcast the
|
|
102
|
+
string to every player. A placed-at-runtime surface (a video decal included) is static
|
|
103
|
+
shared world state — put it on `room.shared`, **not** `objects` (no movement to smooth)
|
|
104
|
+
and **not** `send` (late joiners would see a bare wall; `shared` keys replay on connect).
|
|
105
|
+
|
|
106
|
+
Use a **fixed ring of slots** and overwrite the oldest — **never a fresh key per
|
|
107
|
+
placement**. The relay caps game-writable `shared` keys at **256 per room, keys are
|
|
108
|
+
permanent and undeletable, and new keys past the cap are silently dropped forever**:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
let next = 0;
|
|
112
|
+
const RING = 32; // decal:0 .. decal:31
|
|
113
|
+
function placeShared(url: string, hit: { point: THREE.Vector3; normal: THREE.Vector3 }) {
|
|
114
|
+
room.shared.set(`decal:${next % RING}`, { url, p: hit.point.toArray(), n: hit.normal.toArray() });
|
|
115
|
+
next++;
|
|
116
|
+
}
|
|
117
|
+
room.on("shared", (key, value) => { // every player (incl. late joiners) rebuilds it
|
|
118
|
+
if (key.startsWith("decal:") && value) spawnVideoDecalFromShared(value);
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
See `$genex-threejs-multiplayer` for the `shared` channel rules and the room API.
|
|
123
|
+
|
|
124
|
+
## Publish checklist
|
|
125
|
+
|
|
126
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
127
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
128
|
+
- Don't copy the mp4 into `public/assets/` — generated assets live in R2, not the repo.
|
|
129
|
+
|
|
130
|
+
## Options
|
|
131
|
+
|
|
132
|
+
- `--loop` — a seamless loop (for screens, ambient backdrops, video decals).
|
|
133
|
+
- `--duration <sec>` — clip length 1–15; default 5. Only raise it when the content
|
|
134
|
+
genuinely needs more — longer clips cost more and take longer.
|
|
135
|
+
- `--no-wait` — enqueue and return immediately (the file won't be downloaded;
|
|
136
|
+
re-run without `--no-wait` to fetch it). Handy for video since it takes a minute or two.
|
|
137
|
+
- `--api-url <url>` — override the API base (local dev).
|
|
138
|
+
|
|
139
|
+
## Troubleshooting
|
|
140
|
+
|
|
141
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
142
|
+
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
143
|
+
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
144
|
+
- **Nothing plays / black surface** — the first `video.play()` must run inside a user
|
|
145
|
+
gesture (click/keydown); confirm it's called and its promise rejection is logged.
|
|
146
|
+
- **Tainted-source / security error** — set `video.crossOrigin = "anonymous"` before
|
|
147
|
+
`video.src`.
|
|
148
|
+
- **Colors look washed/dark** — ensure `texture.colorSpace = THREE.SRGBColorSpace`.
|
|
@@ -37,13 +37,16 @@ npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
|
|
|
37
37
|
npx genex skybox "golden hour over mountains" # a 360° sky + lighting
|
|
38
38
|
npx genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
|
|
39
39
|
npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
|
|
40
|
+
npx genex image "vintage travel poster" # a picture (poster/sign/sprite/decal)
|
|
41
|
+
npx genex video "swirling neon portal" --loop # a video clip (screen/backdrop)
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
(Run them inside your project — the `@genex-ai/cli-demo` dev dependency makes
|
|
43
45
|
`npx genex` resolve to the right CLI.)
|
|
44
46
|
|
|
45
47
|
Each has a focused skill with the exact loader code — `$genex-ai-model`,
|
|
46
|
-
`$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture
|
|
48
|
+
`$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`, `$genex-ai-image`,
|
|
49
|
+
`$genex-ai-video`.
|
|
47
50
|
|
|
48
51
|
## Identity & saves (every game)
|
|
49
52
|
|
|
@@ -60,13 +60,12 @@ recipes (sports/ball, shooter, co-op).
|
|
|
60
60
|
## Real (AI-generated) assets — `npx genex` commands
|
|
61
61
|
|
|
62
62
|
When the user wants a **specific, recognizable asset** (a named object, a described
|
|
63
|
-
sky, a particular sound or
|
|
64
|
-
it with an `npx genex` command (run inside the project, where the
|
|
65
|
-
dev dependency makes `genex` resolve to the right CLI). Each
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
loader code.
|
|
63
|
+
sky, a particular sound, surface, image, or clip) rather than something authored in
|
|
64
|
+
code, generate it with an `npx genex` command (run inside the project, where the
|
|
65
|
+
`@genex-ai/cli-demo` dev dependency makes `genex` resolve to the right CLI). Each
|
|
66
|
+
prints a permanent public `assets.genex.technology` URL you load straight from at
|
|
67
|
+
runtime — the asset lives in Genex storage (R2), not your repo, so there's nothing to
|
|
68
|
+
commit. Each skill has the exact Three.js loader code.
|
|
70
69
|
|
|
71
70
|
| Work needed | Generate with | Skill |
|
|
72
71
|
| --- | --- | --- |
|
|
@@ -74,6 +73,8 @@ loader code.
|
|
|
74
73
|
| a described 360° sky / backdrop + image-based lighting | `npx genex skybox "<prompt>"` | `$genex-ai-skybox` |
|
|
75
74
|
| a specific sound effect tied to an event | `npx genex sfx "<prompt>"` | `$genex-ai-sfx` |
|
|
76
75
|
| a photoreal surface/material on a mesh or terrain | `npx genex texture "<prompt>" [--terrain]` | `$genex-ai-texture` |
|
|
76
|
+
| a picture on a plane/sprite — poster, sign, sprite, card art, decal/sticker | `npx genex image "<prompt>" [--transparent]` | `$genex-ai-image` |
|
|
77
|
+
| a video clip on a surface — in-game screen, billboard, cutscene, backdrop | `npx genex video "<prompt>" [--loop]` | `$genex-ai-video` |
|
|
77
78
|
|
|
78
79
|
Prefer the **procedural** skills above for abstract/parametric/animated systems
|
|
79
80
|
(geometry, materials, sky, water, VFX) — no files, infinite variation. Prefer the
|