@genex-ai/cli-demo 0.71.0 → 0.74.0-dev.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +354 -8
- package/package.json +2 -1
- package/templates/controllers/character/follow-camera.ts +15 -4
- package/templates/controllers/character/vrm/vrm-loader.ts +74 -11
- package/templates/controllers/quality/governor.ts +147 -0
- package/templates/controllers/quality/pick-asset.ts +57 -0
- package/templates/controllers/quality/tier.ts +170 -0
- package/templates/skills/genex-ai-hud/SKILL.md +11 -2
- package/templates/skills/genex-ai-menu/SKILL.md +18 -2
- package/templates/skills/genex-ai-skybox/SKILL.md +15 -4
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-explore/SKILL.md +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +141 -0
- package/templates/skills/genex-threejs-adaptive-quality/references/adaptive-quality.md +105 -0
- package/templates/skills/genex-threejs-bloom/SKILL.md +4 -1
- package/templates/skills/genex-threejs-bloom/references/bloom.md +1 -1
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +62 -12
- package/templates/skills/genex-threejs-camera-direction/references/camera-rigs.md +62 -0
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +9 -3
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +4 -1
- package/templates/skills/genex-threejs-game-feel/SKILL.md +4 -1
- package/templates/skills/genex-threejs-game-ui/SKILL.md +113 -30
- package/templates/skills/genex-threejs-game-ui/references/style-capsules.md +4 -1
- package/templates/skills/genex-threejs-image-pipeline/SKILL.md +5 -0
- package/templates/skills/genex-threejs-image-pipeline/references/image-pipeline.md +1 -1
- package/templates/skills/genex-threejs-lighting-design/SKILL.md +5 -1
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +7 -1
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +6 -3
- package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +1 -0
- package/templates/skills/genex-threejs-screen-space-ambient-occlusion/references/ambient-occlusion.md +1 -1
- package/templates/skills/genex-threejs-shadow-systems/SKILL.md +6 -0
- package/templates/skills/genex-threejs-shadow-systems/references/shadow-systems.md +1 -1
- package/templates/skills/genex-threejs-skill-router/SKILL.md +26 -5
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +25 -9
- package/templates/skills/genex-threejs-spectral-ocean/references/spectral-ocean.md +1 -1
- package/templates/skills/genex-threejs-touch-controls/SKILL.md +11 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +7 -0
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +36 -12
- package/templates/skills/genex-threejs-water-optics/references/water-optics.md +1 -1
- package/templates/skills/genex-updates/SKILL.md +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-threejs-adaptive-quality
|
|
3
|
+
description: Make a Genex Three.js game phone-survivable with the vendored adaptive-quality kit — device-tier detection, tier-budgeted renderer settings, a runtime governor that steps quality down before phones run out of memory, per-tier asset rungs for generated skyboxes/textures, and a Quality picker in settings. Load for every game at boot wiring time, and whenever a game is heavy, crashes on phones, or gets flagged desktop-only.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Three.js Adaptive Quality
|
|
7
|
+
|
|
8
|
+
Phones enforce a hard GPU-memory ceiling desktops don't have: iOS silently
|
|
9
|
+
kills the page when a game allocates too much, and the kill arrives at BOOT —
|
|
10
|
+
exactly when a skybox, models, and the post stack all decode at once. This
|
|
11
|
+
skill wires the vendored quality kit so the game boots conservatively on
|
|
12
|
+
phones, steps quality UP when the device proves smooth, and never gets uglier
|
|
13
|
+
on desktop. **You can recover from ugly; you cannot recover from a killed
|
|
14
|
+
page.**
|
|
15
|
+
|
|
16
|
+
This is a completion gate like the post stack: every game wires the tier at
|
|
17
|
+
boot before it is called done. It costs three lines, not a testing burden —
|
|
18
|
+
you still verify on desktop only.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx genex controller quality
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Installs `src/controllers/quality/{tier.ts, governor.ts, pick-asset.ts}` —
|
|
27
|
+
game-owned code, edit freely.
|
|
28
|
+
|
|
29
|
+
## Wire the tier at boot (before renderer construction)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { detectTier } from "./controllers/quality/tier.ts";
|
|
33
|
+
import { QualityGovernor } from "./controllers/quality/governor.ts";
|
|
34
|
+
|
|
35
|
+
const tier = detectTier(); // phone-low | phone | desktop; manual Quality setting wins
|
|
36
|
+
const renderer = new THREE.WebGLRenderer({ antialias: tier.antialias });
|
|
37
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, tier.dprCap));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The tier owns every budget decision: `dprCap` (1.5 on phones — the single
|
|
41
|
+
biggest framebuffer lever), `antialias` (off on phones; it is fixed at context
|
|
42
|
+
creation and can never change live), `shadowMapSize` (1024 phone / 2048
|
|
43
|
+
desktop), `postLevel` (`'light'` = tone mapping + cheap passes on phones;
|
|
44
|
+
`'full'` = the named stack on desktop), `particleScale`, `drawDistanceScale`,
|
|
45
|
+
`frameCap`, and `remoteAvatarCap` for multiplayer. The exact ladder and which
|
|
46
|
+
knobs may change at runtime vs load time vs never: [references/adaptive-quality.md](references/adaptive-quality.md).
|
|
47
|
+
|
|
48
|
+
## Wire the governor (runtime — phones throttle over minutes)
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const governor = new QualityGovernor(tier, {
|
|
52
|
+
setDprScale: (m) => renderer.setPixelRatio(Math.min(window.devicePixelRatio, tier.dprCap * m)),
|
|
53
|
+
setPostEnabled: (on) => (composerEnabled = on),
|
|
54
|
+
setDrawDistanceScale: (m) => (scene.fog!.far = baseFogFar * tier.drawDistanceScale * m),
|
|
55
|
+
}, renderer);
|
|
56
|
+
|
|
57
|
+
// In the render loop, with the same performance.now() delta the loop already computes:
|
|
58
|
+
governor.frame(deltaMs);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Sustained slow frames step down (DPR ×0.8 → post off → draw distance →
|
|
62
|
+
30 fps cap); twenty smooth seconds step back up; a knob that failed twice
|
|
63
|
+
stays down for the session. It keeps governing forever — thermal throttling
|
|
64
|
+
arrives at minute eight, not second thirty. It also pauses judgment when the
|
|
65
|
+
tab is hidden and publishes memory counts the platform's crash telemetry
|
|
66
|
+
reads; pause your own loop and audio on `visibilitychange` too.
|
|
67
|
+
|
|
68
|
+
## Generated assets: load through the rungs
|
|
69
|
+
|
|
70
|
+
Generated skyboxes are 8192×4096 — about 178 MB decoded, over half a phone's
|
|
71
|
+
whole budget in one texture. Every generated image asset ships with downscale
|
|
72
|
+
rungs; phones must load through them:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { detectTier } from "./controllers/quality/tier.ts";
|
|
76
|
+
import { loadTextureWithFallback } from "./controllers/quality/pick-asset.ts";
|
|
77
|
+
|
|
78
|
+
const texture = await loadTextureWithFallback(SKYBOX_URL, tier, (u) =>
|
|
79
|
+
new THREE.TextureLoader().loadAsync(u),
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Desktop loads the original, phones the `@2048` rung (~11 MB), and a missing
|
|
84
|
+
rung falls back to the original — never a broken boot. The `$genex-ai-skybox`
|
|
85
|
+
and `$genex-ai-texture` skills show the wiring in place.
|
|
86
|
+
|
|
87
|
+
## Quality picker in settings
|
|
88
|
+
|
|
89
|
+
The pause/settings screen (see `$genex-threejs-game-ui`) always carries a
|
|
90
|
+
Quality entry: **Auto / Low / Medium / High**, wired to
|
|
91
|
+
`setQualitySetting(...)` from `tier.ts` + a reload or re-tier. Persisted
|
|
92
|
+
per-device in localStorage on purpose — quality is a property of the phone,
|
|
93
|
+
not the player's account. Default Auto.
|
|
94
|
+
|
|
95
|
+
## Budgets that ride the tier (not separate rules)
|
|
96
|
+
|
|
97
|
+
- Shadows: `tier.shadowMapSize`, `shadow.autoUpdate = false` for static scenes,
|
|
98
|
+
at most 2 cascades on phones (`$genex-threejs-shadow-systems`).
|
|
99
|
+
- Post: phone floor is a BUILT tone-mapping/output pass (`postLevel: 'light'`
|
|
100
|
+
adds FXAA/vignette); SSAO, volumetrics, and DoF are desktop-tier only
|
|
101
|
+
(`$genex-threejs-skill-router` owns the floor wording).
|
|
102
|
+
- Particles/scatter: multiply counts by `tier.particleScale`; render heavy
|
|
103
|
+
transparency at half resolution and upsample.
|
|
104
|
+
- Animation: distant mixers update at 1/2–1/4 rate; multiplayer remotes above
|
|
105
|
+
`tier.remoteAvatarCap` billboard instead of animating
|
|
106
|
+
(`$genex-threejs-multiplayer`).
|
|
107
|
+
- Physics: phone tiers prefer hull/cuboid colliders for props — trimesh only
|
|
108
|
+
where gameplay demands it (`$genex-threejs-physics-rapier`).
|
|
109
|
+
- Shaders: default `mediump` (iOS exception: float-texture sampling needs
|
|
110
|
+
`highp sampler2D`); precompile with `renderer.compileAsync(scene, camera)`
|
|
111
|
+
during the loader screen so first-frame jank doesn't read as a stall; skip
|
|
112
|
+
max anisotropy on phones.
|
|
113
|
+
- Disposal: level swaps traverse the outgoing scene and dispose geometry,
|
|
114
|
+
materials, AND textures (three never frees them for you); watch
|
|
115
|
+
`renderer.info.memory` while testing — if textures/geometries climb across
|
|
116
|
+
swaps, you leak toward the kill.
|
|
117
|
+
|
|
118
|
+
## WebGPU games
|
|
119
|
+
|
|
120
|
+
The scaffold ships WebGL and stays the default; if this project already uses
|
|
121
|
+
`WebGPURenderer`, keep it (never switch renderers mid-project). Context loss
|
|
122
|
+
differs: WebGL fires `webglcontextlost` events; WebGPU exposes a
|
|
123
|
+
`device.lost` promise — attach a handler that pauses the loop and rebuilds,
|
|
124
|
+
mirroring the scaffold's WebGL pattern. All tier knobs apply identically
|
|
125
|
+
except `antialias` (WebGPU MSAA is per-render-target and CAN change at
|
|
126
|
+
runtime).
|
|
127
|
+
|
|
128
|
+
## Failure conditions
|
|
129
|
+
|
|
130
|
+
- Renderer constructed before `detectTier()` → context-creation knobs
|
|
131
|
+
(antialias) are locked wrong for the session. Tier first, renderer second.
|
|
132
|
+
- Governor wired to context-creation flags → no-op at best. Runtime knobs
|
|
133
|
+
only: DPR, post toggles, distances, frame cap.
|
|
134
|
+
- Skybox loaded with a bare `TextureLoader.loadAsync(SKYBOX_URL)` on a game
|
|
135
|
+
that targets phones → ~178 MB decoded; route it through
|
|
136
|
+
`loadTextureWithFallback`.
|
|
137
|
+
- Quality stepping on every spike → shader compiles read as slowness. The
|
|
138
|
+
governor requires SUSTAINED slow windows; do not shorten them.
|
|
139
|
+
- Testing quality tiers by resizing the desktop window → tiers key off touch +
|
|
140
|
+
OS, not viewport. Trust desktop verification plus the preflight report
|
|
141
|
+
`genex preview` prints.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Genex adaptive quality — tiers, knobs, and the governor in depth
|
|
2
|
+
|
|
3
|
+
Use this reference when tuning the tier ladder, deciding which knob may change
|
|
4
|
+
when, or teaching the governor a game-specific step.
|
|
5
|
+
|
|
6
|
+
## Why boot-conservative
|
|
7
|
+
|
|
8
|
+
The phone budget is a hard ceiling that includes GPU memory (textures,
|
|
9
|
+
framebuffers), and the OS kill arrives with no catchable event. Boot is the
|
|
10
|
+
danger window: skybox + models + post targets decode together. So phone tiers
|
|
11
|
+
START one notch below what the heuristics suggest and the governor steps UP
|
|
12
|
+
after ~20 smooth seconds. The cost of guessing low is moments of softness; the
|
|
13
|
+
cost of guessing high is a dead page.
|
|
14
|
+
|
|
15
|
+
## The tier ladder
|
|
16
|
+
|
|
17
|
+
| Knob | phone-low | phone | desktop |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| DPR cap | 1.0 | 1.5 | 2 |
|
|
20
|
+
| antialias (context) | off | off | on |
|
|
21
|
+
| Shadow map | 512 (static-cached) | 1024 | 2048 |
|
|
22
|
+
| Post level | tone map only | + FXAA/vignette | full named stack |
|
|
23
|
+
| Skybox rung | @2048 (~11 MB) | @4096 (~45 MB) | original |
|
|
24
|
+
| Texture rung (props) | @1024 | @2048 | original |
|
|
25
|
+
| Particles/scatter | 0.25× | 0.5× | 1× |
|
|
26
|
+
| Draw distance | 0.5× | 0.75× | 1× |
|
|
27
|
+
| Frame target | stable 30 | 60 | 60 |
|
|
28
|
+
| Remote avatars animated | 4 | 8 | all |
|
|
29
|
+
| Prop colliders | hull/cuboid | hull | as designed |
|
|
30
|
+
|
|
31
|
+
A DPR drop from 3 (raw iPhone) to 1.5 cuts every full-screen surface — color,
|
|
32
|
+
depth, and each post target — to a quarter of the bytes. It is the single
|
|
33
|
+
strongest lever the tier owns.
|
|
34
|
+
|
|
35
|
+
## The knob split — what may change when
|
|
36
|
+
|
|
37
|
+
Getting this wrong produces silent no-ops or a session stuck ugly:
|
|
38
|
+
|
|
39
|
+
- **Context-creation-fixed (never changes live):** `antialias`, `alpha`,
|
|
40
|
+
`stencil`, `powerPreference` on WebGL. Changing them means a new context and
|
|
41
|
+
a full re-init — the tier must decide them BEFORE the renderer exists.
|
|
42
|
+
(WebGPU differs: MSAA is per-render-target sample count and is runtime-
|
|
43
|
+
changeable.)
|
|
44
|
+
- **Load-time (fixed for the session once fetched):** asset rungs (skybox and
|
|
45
|
+
texture resolutions), model LOD sets. `pickAsset` decides them from the tier
|
|
46
|
+
at load; switching later means a re-fetch — treat as fixed.
|
|
47
|
+
- **Runtime-free (the governor's domain):** `setPixelRatio`, post passes on/off
|
|
48
|
+
and their target resolutions, shadow map size (realloc), draw distance and
|
|
49
|
+
fog, LOD bias, particle counts, mixer update rates, frame cap, remote-avatar
|
|
50
|
+
animation count.
|
|
51
|
+
|
|
52
|
+
## Governor mechanics
|
|
53
|
+
|
|
54
|
+
- Slow = frame delta over budget for a SUSTAINED window (4 s) — never single
|
|
55
|
+
spikes, which are usually shader compiles or GC. Precompiling with
|
|
56
|
+
`renderer.compileAsync` during the loader screen removes most spikes at the
|
|
57
|
+
source (and keeps first-frame jank from reading as a stall to the platform's
|
|
58
|
+
telemetry).
|
|
59
|
+
- Step-down order: DPR ×0.8 → post off → draw distance ×0.6 → 30 fps cap.
|
|
60
|
+
Each step is the cheapest remaining lever with the biggest headroom return.
|
|
61
|
+
- Step-up needs 20 smooth seconds (hysteresis), and a step that had to be
|
|
62
|
+
re-applied twice is pinned for the session — oscillating quality reads worse
|
|
63
|
+
than stable-low.
|
|
64
|
+
- The governor never stops: thermal throttling degrades phones after minutes
|
|
65
|
+
of play, so a boot-time benchmark alone always ends up wrong.
|
|
66
|
+
- A stable 30 fps cap beats a stuttery 40–50: consistent frame pacing reads
|
|
67
|
+
smoother and halves GPU work per second (heat, battery, memory bandwidth).
|
|
68
|
+
- Backgrounded tab (`visibilitychange`): pause the render loop and audio, not
|
|
69
|
+
just the governor — a hidden game burning GPU is pure thermal debt on the
|
|
70
|
+
device class that can least afford it.
|
|
71
|
+
|
|
72
|
+
## Detection honesty
|
|
73
|
+
|
|
74
|
+
- Apple devices mask the GPU renderer string ("Apple GPU") — screen dims + DPR
|
|
75
|
+
+ iOS major version are the usable signals there, and the governor corrects
|
|
76
|
+
the rest from measured frames.
|
|
77
|
+
- Android exposes real renderer strings (Adreno/Mali/Xclipse); the vendored
|
|
78
|
+
lookup in `tier.ts` promotes strong GPUs to the `phone` tier. It is a
|
|
79
|
+
heuristic on purpose — extend the regex when field data shows a
|
|
80
|
+
misclassified family, and let the governor absorb the rest.
|
|
81
|
+
- Never burn a probe context on a memory-strapped phone at play time; the one
|
|
82
|
+
probe in `tier.ts` runs at boot and frees its context immediately.
|
|
83
|
+
|
|
84
|
+
## Memory discipline that rides the tier
|
|
85
|
+
|
|
86
|
+
- Dispose on every level swap: traverse the outgoing scene and call
|
|
87
|
+
`.dispose()` on geometry, material, AND each material's textures — material
|
|
88
|
+
dispose does not free textures, and three frees nothing automatically.
|
|
89
|
+
- Watch `renderer.info.memory.{textures,geometries}` across swaps in dev; a
|
|
90
|
+
monotonic climb is a leak marching toward the OS kill. The governor
|
|
91
|
+
publishes these counts for the platform's field telemetry.
|
|
92
|
+
- Prefer meshopt/instanced geometry for repeats; `BatchedMesh` batches
|
|
93
|
+
HETEROGENEOUS static meshes into one draw where instancing (identical
|
|
94
|
+
meshes only) can't.
|
|
95
|
+
- Half-resolution transparency: render heavy particle/transparency passes to a
|
|
96
|
+
half-size target and composite up — fill-rate is the phone bottleneck.
|
|
97
|
+
|
|
98
|
+
## Multiplayer at tier
|
|
99
|
+
|
|
100
|
+
Remotes are visual-only; with the shared avatar file, `loadVrmClone` gives N
|
|
101
|
+
remotes one set of GPU geometry/textures. Animate and fully draw only the
|
|
102
|
+
nearest `tier.remoteAvatarCap`; beyond it, freeze the mixer and billboard or
|
|
103
|
+
hide. Matchmade games can also declare a lower `maxPlayers` in
|
|
104
|
+
`genex.matchmaking` for phone-heavy audiences — capacity is a server-owned
|
|
105
|
+
knob.
|
|
@@ -32,7 +32,10 @@ reference before adding selective bloom to a composed scene.
|
|
|
32
32
|
- selective bloom requires mutating scene materials every frame without restoration guarantees;
|
|
33
33
|
- transparent particles disappear from extraction because pass ownership is unclear;
|
|
34
34
|
- bloom radius changes wildly with resolution;
|
|
35
|
-
- highlights become gray because energy is clamped too early
|
|
35
|
+
- highlights become gray because energy is clamped too early;
|
|
36
|
+
- bloom ships un-tiered: phone tiers run the light post level
|
|
37
|
+
(`$genex-threejs-adaptive-quality`) — bloom is a desktop-tier pass, and its
|
|
38
|
+
full-res HDR target is exactly the allocation phones get killed for.
|
|
36
39
|
|
|
37
40
|
## Routing boundary
|
|
38
41
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use this reference to choose bloom ownership, signal order, selective contribution, and scene-relative emissive ranges without making bloom responsible for the underlying form.
|
|
4
4
|
|
|
5
|
-
> **Renderer note:** this reference assumes `WebGPURenderer` + TSL node materials. Check the project's actual renderer first — the Genex scaffold ships vanilla WebGL three.js. On WebGL, adapt the technique with standard materials / `EffectComposer` passes or pick a simpler alternative; never switch renderers mid-project.
|
|
5
|
+
> **Renderer note:** this reference assumes `WebGPURenderer` + TSL node materials. Check the project's actual renderer first — the Genex scaffold ships vanilla WebGL three.js. On WebGL, adapt the technique with standard materials / `EffectComposer` passes or pick a simpler alternative; never switch renderers mid-project. Either way the effect obeys the device tier (`$genex-threejs-adaptive-quality`): expensive passes are desktop-tier, and on WebGPU the per-target MSAA sample count is a runtime knob the governor may drive.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
@@ -35,9 +35,12 @@ rules, floating-origin shot, pointer controls, and implementation limits.
|
|
|
35
35
|
|
|
36
36
|
## Aiming and pointer lock
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
The rule is binary: during play, **the cursor is either a gameplay tool or it is
|
|
39
|
+
locked away**. An OS arrow parked over the action in a game that never uses it is
|
|
40
|
+
a shipped defect, not a default. On the bundled `FollowCamera`, pointer-lock aim
|
|
41
|
+
is ON by default on desktop — a click locks the pointer and raw mouse movement
|
|
42
|
+
drives the view — so mostly you decide whether to turn it OFF. Name the bucket in
|
|
43
|
+
the build plan:
|
|
41
44
|
|
|
42
45
|
- **MANDATORY** — first-person of any kind (FPS, walking sim, horror) and any
|
|
43
46
|
mouse-aimed action (third-person shooter, turret/range). Lock is on by default;
|
|
@@ -47,11 +50,16 @@ it on — you decide whether to turn it OFF. Name the bucket in the build plan:
|
|
|
47
50
|
`genex controller character` game). On by default; leave it on. Opt out with
|
|
48
51
|
`pointerLockAim: false` only for a stated reason (a cursor-heavy UI at the core
|
|
49
52
|
of play).
|
|
50
|
-
- **
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
- **Keyboard-driven games lock too** — a racer, platformer, or runner that never
|
|
54
|
+
reads the mouse still locks the pointer on the play/Start click: the cursor is
|
|
55
|
+
not a tool there, so lock it away (hidden cursor, no stray clicks, Esc = pause
|
|
56
|
+
as usual). Bundled-controller games get this for free; a hand-rolled game uses
|
|
57
|
+
the minimal lock recipe in `$genex-threejs-game-ui` (~6 lines).
|
|
58
|
+
- **NEVER** — cursor-core games where the pointer IS the gameplay tool (top-down
|
|
59
|
+
click-to-move, tower defense, builders, card/board/puzzle) and spectator/orbit
|
|
60
|
+
showcases. These **must pass `pointerLockAim: false`** — otherwise the bundled
|
|
61
|
+
camera grabs the cursor on the first click. (Touch needs nothing: pointer lock
|
|
62
|
+
doesn't exist there and the mode no-ops on coarse pointers.)
|
|
55
63
|
|
|
56
64
|
**Mechanism — games on the bundled controller (most games):** do NOT hand-roll
|
|
57
65
|
lock handling. Aim is already enabled; the kit ships a ready-made cue overlay —
|
|
@@ -65,6 +73,7 @@ character`) to `onAimChange` and you get the reticle + "click to aim" cue for fr
|
|
|
65
73
|
domElement: renderer.domElement,
|
|
66
74
|
onAimChange: cue.onAimChange,
|
|
67
75
|
});
|
|
76
|
+
followCam.setPaused(getPhase() !== "playing"); // park aim if the game boots on a menu/loader
|
|
68
77
|
|
|
69
78
|
Cursor-core game? Pass `pointerLockAim: false` instead and skip the cue. Want a
|
|
70
79
|
custom HUD? Read `onAimChange` yourself — `{ state }` is one of `locked` /
|
|
@@ -86,12 +95,17 @@ grants the permission.
|
|
|
86
95
|
|
|
87
96
|
1. Two states only: locked = playing, unlocked = menu/paused. "Unlocked but
|
|
88
97
|
gameplay continues" is the imprecise-aim defect in disguise.
|
|
89
|
-
2. Opening any menu
|
|
98
|
+
2. Opening any menu — the BOOT/main menu included — parks aim: exit the lock
|
|
90
99
|
(`followCam.setPaused(true)`), cursor returns, gameplay input pauses. Menu
|
|
91
|
-
keys are Tab/I/E — never Esc.
|
|
100
|
+
keys are Tab/I/E — never Esc. The one true wiring is the phase binding
|
|
101
|
+
`followCam.setPaused(phase !== "playing")` applied on phase transitions,
|
|
102
|
+
never per frame: an unguarded per-frame resume once re-requested the lock
|
|
103
|
+
every frame, and any menu click's ~5s of transient activation then locked
|
|
104
|
+
the pointer over the open menu.
|
|
92
105
|
3. Closing a menu re-locks INSIDE the close click/keypress handler
|
|
93
|
-
(`followCam.setPaused(false)`
|
|
94
|
-
|
|
106
|
+
(`followCam.setPaused(false)` — the phase binding's `setPhase("playing")`
|
|
107
|
+
in the Play/Resume click does exactly this) — the browser requires a user
|
|
108
|
+
gesture, so menus close by click/keypress, never by timeout.
|
|
95
109
|
4. Esc is the browser's release valve (you can't intercept it; Chrome enforces
|
|
96
110
|
a re-lock cooldown) → treat Esc as pause: show the overlay with a
|
|
97
111
|
"click to resume" button.
|
|
@@ -108,11 +122,45 @@ grants the permission.
|
|
|
108
122
|
Chrome's post-Esc cooldown) fires `onAimChange` with reason `needs-gesture` and
|
|
109
123
|
keeps the "click to aim" cue up; the next click or keypress re-locks. Never
|
|
110
124
|
retry on a timer.
|
|
125
|
+
10. The bundled camera OWNS the lock: never call `document.exitPointerLock()` or
|
|
126
|
+
`canvas.requestPointerLock()` yourself alongside it — raw calls desync
|
|
127
|
+
`aimState` and the cue. Everything routes through `setPaused` (which is
|
|
128
|
+
idempotent — a same-value call is a no-op).
|
|
111
129
|
|
|
112
130
|
**Not an OS setting:** pointer `movementX/movementY` is never inverted by the OS
|
|
113
131
|
(trackpad "natural scrolling" only flips the wheel). If look feels inverted it's a
|
|
114
132
|
sign bug in the rig, not a device quirk — fix the sign, don't sniff the trackpad.
|
|
115
133
|
|
|
134
|
+
## The screen-direction contract
|
|
135
|
+
|
|
136
|
+
Every input axis has one correct on-screen direction, for every rig, bundled or
|
|
137
|
+
hand-rolled. These four invariants are testable and non-negotiable:
|
|
138
|
+
|
|
139
|
+
1. Mouse/touchpad RIGHT turns the view right; mouse UP looks up (down only behind
|
|
140
|
+
an explicit invert option the player chose).
|
|
141
|
+
2. `KeyD`/ArrowRight moves or turns the player toward screen-RIGHT; `KeyA`/
|
|
142
|
+
ArrowLeft toward screen-left. Same for a touch stick's +x.
|
|
143
|
+
3. Drag-pan picks ONE convention — grab-the-world (terrain follows the pointer)
|
|
144
|
+
or move-the-camera — and BOTH axes obey it. One axis each is the
|
|
145
|
+
"diagonals feel twisted" bug.
|
|
146
|
+
4. See the OS-setting note above: inversion is always your sign, never the device.
|
|
147
|
+
|
|
148
|
+
The formula that settles every sign argument: `screenRight = cross(cameraForward, worldUp)`.
|
|
149
|
+
For a Y-up world and forward `(sin yaw, 0, cos yaw)`, screen-right is
|
|
150
|
+
`(-cos yaw, 0, sin yaw)`. **Warning — `(cos yaw, 0, -sin yaw)` is the LEFT
|
|
151
|
+
vector** (that's `cross(worldUp, cameraForward)`), and writing it as "right" is
|
|
152
|
+
the single most-shipped direction bug in generated games: two independent
|
|
153
|
+
projects inverted their A/D exactly this way. Related trap: positive
|
|
154
|
+
`rotation.y` turns a +Z-facing object toward +X, which is screen-LEFT from a
|
|
155
|
+
chase camera behind it — so "positive yaw = turn right" is false in this basis.
|
|
156
|
+
|
|
157
|
+
Never derive signs by intuition — intuition about right-handed frames is wrong
|
|
158
|
+
about half the time and has been wrong in every shipped instance. Copy a
|
|
159
|
+
verified pair (sign AND basis together) from
|
|
160
|
+
[references/camera-rigs.md](references/camera-rigs.md), then confirm with the
|
|
161
|
+
input-direction part of the smoke check: hold D and watch which way the world
|
|
162
|
+
answers.
|
|
163
|
+
|
|
116
164
|
## Non-negotiable rules
|
|
117
165
|
|
|
118
166
|
- Use subject dimensions to derive offsets; do not tune one fixed distance for
|
|
@@ -122,6 +170,8 @@ sign bug in the rig, not a device quirk — fix the sign, don't sniff the trackp
|
|
|
122
170
|
- During an explicit handoff, use one interpolation stage. Do not stack a
|
|
123
171
|
transition blend and a second follow smoother over the same interval.
|
|
124
172
|
- Re-sync yaw/pitch from the camera when pointer lock is acquired.
|
|
173
|
+
- Hand-rolled steering/pan/look math copies a verified basis from the reference
|
|
174
|
+
and passes the input-direction check — signs are never derived by intuition.
|
|
125
175
|
- Update the projection matrix whenever FOV, near, far, or aspect changes.
|
|
126
176
|
- Keep stars or infinite backgrounds camera-relative when large translation
|
|
127
177
|
would create false parallax or precision loss.
|
|
@@ -11,6 +11,7 @@ Use this reference for scale-aware chase, side, orbit, authored-shot, pointer-lo
|
|
|
11
11
|
- Explicit camera handoffs
|
|
12
12
|
- cinematic implementation shot ownership
|
|
13
13
|
- Pointer-look and movement constraints
|
|
14
|
+
- Verified screen-direction bases
|
|
14
15
|
- Floating origin and background handling
|
|
15
16
|
- Projection and lifecycle ownership
|
|
16
17
|
- Failure modes and diagnostics
|
|
@@ -166,6 +167,9 @@ yaw -= mouseDeltaX * 0.0022
|
|
|
166
167
|
pitch -= mouseDeltaY * 0.0018
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
Expected on screen: mouse-right orbits the view right, mouse-up tilts it up —
|
|
171
|
+
verify both axes against the screen-direction contract before retuning the scales.
|
|
172
|
+
|
|
169
173
|
Pitch bounds vary by flight mode. The implementation also enforces camera height above
|
|
170
174
|
the ship:
|
|
171
175
|
|
|
@@ -281,6 +285,9 @@ distance = movementSpeed * dt
|
|
|
281
285
|
|
|
282
286
|
Default speed is `9`, sensitivity `0.0023`.
|
|
283
287
|
|
|
288
|
+
Expected on screen: mouse-right turns the view right, mouse-up looks up — assert
|
|
289
|
+
both axes (yaw-only evidence has let inverted pitch ship).
|
|
290
|
+
|
|
284
291
|
Keys are cleared on:
|
|
285
292
|
|
|
286
293
|
- pointer-lock exit;
|
|
@@ -295,6 +302,61 @@ Scene-specific constraints then run after controls:
|
|
|
295
302
|
|
|
296
303
|
Input control and spatial constraint are separate layers.
|
|
297
304
|
|
|
305
|
+
## Verified screen-direction bases
|
|
306
|
+
|
|
307
|
+
Copy these pairs whole — the sign and the basis are only correct TOGETHER. Each
|
|
308
|
+
was derived from `screenRight = cross(cameraForward, worldUp)` and verified
|
|
309
|
+
against the on-screen result; if you change one half, re-verify with the
|
|
310
|
+
input-direction check instead of reasoning about it.
|
|
311
|
+
|
|
312
|
+
**Chase-cam steering** (vehicle/character heading, camera behind):
|
|
313
|
+
|
|
314
|
+
```text
|
|
315
|
+
heading = (sin yaw, 0, cos yaw) // matches rotation.y for a +Z-front model
|
|
316
|
+
yaw -= steer * rate * dt // steer: D/right = +1, A/left = -1
|
|
317
|
+
position += heading * speed * dt
|
|
318
|
+
camera at position - heading * dist, lookAt(position)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Why the minus: the camera looks along `heading`, so screen-right is
|
|
322
|
+
`cross(heading, up) = (-cos yaw, 0, sin yaw)`, while `d(heading)/d(yaw) =
|
|
323
|
+
(cos yaw, 0, -sin yaw)` — exactly screen-LEFT. Increasing yaw always veers the
|
|
324
|
+
nose left on screen, so "D turns right" needs `yaw -=`. (Equivalently
|
|
325
|
+
`yaw += steer` is correct only with heading `(-sin yaw, 0, cos yaw)` — a pair,
|
|
326
|
+
never a lone sign.)
|
|
327
|
+
|
|
328
|
+
**RTS / overhead pan camera** (fixed pitch, yaw-orbiting):
|
|
329
|
+
|
|
330
|
+
```text
|
|
331
|
+
right = (-cos yaw, 0, sin yaw) // pitch-independent screen-right on the ground
|
|
332
|
+
forwardGround = (sin yaw, 0, cos yaw) // into the screen along the ground
|
|
333
|
+
D / ArrowRight: target += right * pan A / ArrowLeft: target -= right * pan
|
|
334
|
+
W / ArrowUp: target += forwardGround * pan S: target -= forwardGround * pan
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Drag-pan, grab-the-world (terrain follows the pointer; both axes, one
|
|
338
|
+
convention — `movementY` is positive DOWNWARD):
|
|
339
|
+
|
|
340
|
+
```text
|
|
341
|
+
target -= right * movementX * k
|
|
342
|
+
target += forwardGround * movementY * k
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Move-the-camera convention = flip BOTH signs, never one. The classic shipped bug
|
|
346
|
+
writes `right = (cos yaw, 0, -sin yaw)` — that is `cross(up, forward)`, the LEFT
|
|
347
|
+
vector — inverting A/D and the horizontal drag while W/S stay correct.
|
|
348
|
+
|
|
349
|
+
**Pointer-look** (locked mouse driving yaw/pitch, Euler order `YXZ`):
|
|
350
|
+
|
|
351
|
+
```text
|
|
352
|
+
yaw -= movementX * sensitivity // mouse-right -> view turns RIGHT
|
|
353
|
+
pitch -= movementY * sensitivity // mouse-up -> view tilts UP
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Expected on screen, both axes, before any tuning: a straight-ahead landmark
|
|
357
|
+
slides LEFT when the mouse moves right (the view pans right) and slides DOWN
|
|
358
|
+
when the mouse moves up (the view tilts up).
|
|
359
|
+
|
|
298
360
|
## Floating origin and background handling
|
|
299
361
|
|
|
300
362
|
The Saturn scene first computes a virtual camera pose, stores its orientation
|
|
@@ -149,9 +149,15 @@ Want a custom HUD? Read `onAimChange: ({ state }) => …` yourself instead of th
|
|
|
149
149
|
- **Opt out:** cursor-core games (RTS, tower defense, card, builder) pass
|
|
150
150
|
`pointerLockAim: false` — the camera then stays drag-orbit and never grabs the
|
|
151
151
|
cursor.
|
|
152
|
-
- **Menus / vehicles:**
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
- **Menus / vehicles:** the BOOT/main menu counts as a menu — park aim from the
|
|
153
|
+
first frame. The one true wiring is the phase binding
|
|
154
|
+
`followCam.setPaused(phase !== "playing")` applied on phase TRANSITIONS
|
|
155
|
+
(inside `setPhase()`), never driven from the render loop: `setPaused` is
|
|
156
|
+
idempotent, but a per-frame resume once locked the pointer over an open menu
|
|
157
|
+
the moment a menu click granted transient activation. The Play/Resume click's
|
|
158
|
+
transition doubles as the required user gesture for the re-lock. Vehicles:
|
|
159
|
+
combine conditions (`phase !== "playing" || activeId !== CHARACTER_ID`) — aim
|
|
160
|
+
is on-foot only.
|
|
155
161
|
- **First-person:** the same mode plus `minDistance`/`maxDistance` ≈ 0.1, an
|
|
156
162
|
eye-height target fed to `moveTo`, `avatar.visible = false`, and
|
|
157
163
|
`controller.setLockForward(true)`.
|
|
@@ -162,7 +162,10 @@ From `@genex-ai/embed-sdk/sentry` (crash reporting; exactly these two):
|
|
|
162
162
|
- `sentryCanvasSnapshot(canvas)` — session replay records the DOM, not the 3D
|
|
163
163
|
canvas; call this once per frame at the END of the render loop so replays
|
|
164
164
|
show actual gameplay. Works for BOTH WebGL and WebGPU renderers; internally
|
|
165
|
-
throttled, so calling at 60fps is fine
|
|
165
|
+
throttled, so calling at 60fps is fine. On TOUCH devices it is a deliberate
|
|
166
|
+
no-op (and session replay/tracing sample down): each capture is a full-canvas
|
|
167
|
+
GPU readback, exactly the overhead phones get memory-killed for — mobile
|
|
168
|
+
replays are DOM-only by design, on-error replays still record everywhere:
|
|
166
169
|
|
|
167
170
|
```ts
|
|
168
171
|
function animate() {
|
|
@@ -37,7 +37,10 @@ no matter how good it looks.
|
|
|
37
37
|
unlocked drag-to-turn camera feels imprecise no matter how tight the numbers
|
|
38
38
|
are. The bucket rule + the bundled `FollowCamera` aim mode live in
|
|
39
39
|
`$genex-threejs-camera-direction`; on the bundled controller it's ON by default
|
|
40
|
-
(with a ready-made cue), not hand-rolled events.
|
|
40
|
+
(with a ready-made cue), not hand-rolled events. Direction is half of it: a
|
|
41
|
+
movement key or look axis whose on-screen direction contradicts its label is a
|
|
42
|
+
defect, not a tuning issue — the screen-direction contract and verified bases
|
|
43
|
+
are in `$genex-threejs-camera-direction`.
|
|
41
44
|
|
|
42
45
|
## Movement: snappy beats realistic
|
|
43
46
|
|