reze-engine 0.19.0 → 0.20.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 CHANGED
@@ -1,252 +1,291 @@
1
- # Reze Engine
2
-
3
- [![npm](https://img.shields.io/npm/v/reze-engine)](https://www.npmjs.com/package/reze-engine)
4
-
5
- **Zero-runtime-dependency** WebGPU engine for real-time MMD/PMX rendering — renderer, animation, IK, and physics, all in TypeScript.
6
-
7
- ![screenshot](./screenshot.png)
8
-
9
- ```bash
10
- npm install reze-engine
11
- ```
12
-
13
- ## Features
14
-
15
- - Anime/MMD **hybrid renderer** — toon-ramp NPR over a Principled GGX BSDF, mixed per material
16
- - **9 per-material presets** assigned by material name (`face` / `hair` / `body` / `eye` / `stockings` / `metal` / `cloth_smooth` / `cloth_rough` / `default`)
17
- - **HDR pipeline** — bloom, Filmic tone mapping, MSAA, Apple-TBDR-friendly targets
18
- - **In-house TS physics** — sequential-impulse rigid bodies for PMX rigs, no external dependency
19
- - **VMD animation** with MMD IK, morphs (GPU compute path), and VMD export
20
- - **Interactive editing** GPU picking, transform gizmo, bone/material selection
21
- - Orbit camera with bone-follow, ground + PCF shadows, multi-model
22
-
23
- See [Physics](#physics) and [Rendering](#rendering) for the internals.
24
-
25
- ## Codebase map
26
-
27
- ```
28
- engine/src/
29
- engine.ts Engine: device/context, render loop, all passes & pipelines,
30
- per-model GPU resources, picking, gizmo (entry point)
31
- model.ts Model: skeleton, 4-bone skinning, morphs (CPU + GPU compute),
32
- animation state, drives IK; per-frame update()
33
- animation.ts AnimationClip, VMD bezier interpolation, playback/priority
34
- ik-solver.ts MMD-style CCD IK (angle limits, solve-axis specialization)
35
- camera.ts Orbit camera (alpha/beta/radius), bone-follow, mouse/touch
36
- math.ts Vec3 / Quat / Mat4 (zero-alloc *Into variants for hot paths)
37
- pmx-loader.ts PMX parser: mesh, bones, morphs, rigid bodies, joints
38
- vmd-loader.ts VMD motion parser · vmd-writer.ts VMD export (Shift-JIS)
39
- asset-reader.ts URL + local-folder asset resolution · folder-upload.ts
40
- index.ts public exports
41
-
42
- physics/ in-house rigid-body solver (~2.5k lines)
43
- physics.ts RezePhysics: bone↔body sync, fixed-step accumulator + interpolation
44
- solver.ts sequential-impulse PGS (joint + contact rows)
45
- contact.ts narrowphase (analytical sphere/box/capsule pairs) + contact pool
46
- constraint.ts 6DOF spring joints · world.ts broadphase + step
47
- body.ts SoA rigid-body store · types.ts
48
-
49
- shaders/
50
- materials/ nodes.ts (Blender-node WGSL library) + common.ts (bindings, skinning VS)
51
- + one file per preset (face, hair, body, eye, stockings, …)
52
- passes/ shadow, morph (GPU vertex-morph compute), bloom, composite (Filmic),
53
- outline, selection, gizmo, pick, ground, mipmap
54
- ```
55
-
56
- ## Quick start
57
-
58
- ```javascript
59
- import { Engine, Vec3 } from "reze-engine"
60
-
61
- const engine = new Engine(canvas, {
62
- world: { color: new Vec3(0.4, 0.49, 0.65), strength: 1.0 }, // environment light
63
- sun: { color: new Vec3(1, 1, 1), strength: 2.0, direction: new Vec3(0, -0.5, 1) },
64
- bloom: { color: new Vec3(0.9, 0.1, 0.8), intensity: 0.05, threshold: 0.5 },
65
- camera: { distance: 31.5, target: new Vec3(0, 11.5, 0) }, // MMD units (1 unit = 8 cm)
66
- })
67
- await engine.init()
68
-
69
- const model = await engine.loadModel("hero", "/models/hero/hero.pmx")
70
-
71
- // Map PMX material names to NPR presets (unlisted names fall back to `default`).
72
- engine.setMaterialPresets("hero", {
73
- face: ["face01"],
74
- body: ["skin"],
75
- hair: ["hair_f"],
76
- eye: ["eye"],
77
- cloth_smooth: ["shirt", "dress", "shoes"],
78
- cloth_rough: ["jacket"],
79
- stockings: ["stockings"],
80
- metal: ["earring"],
81
- })
82
-
83
- await model.loadVmd("idle", "/animations/idle.vmd")
84
- model.show("idle")
85
- model.play()
86
-
87
- engine.setCameraFollow(model, "センター", new Vec3(0, 3.5, 0))
88
- engine.addGround({ width: 160, height: 160 })
89
- engine.runRenderLoop()
90
- ```
91
-
92
- ## API
93
-
94
- One WebGPU **Engine** per page (singleton after `init()`). Models load by URL **or** from a user-selected folder ([below](#local-folder-uploads-browser)).
95
-
96
- ### Engine
97
-
98
- ```javascript
99
- engine.init()
100
- engine.loadModel(name, path) // or ({ files, pmxFile? }) for folder upload
101
- engine.getModel(name) / getModelNames() / removeModel(name)
102
-
103
- engine.setMaterialPresets(name, presetMap) // assign NPR presets by material name
104
- engine.setMaterialVisible(name, material, visible) / toggleMaterialVisible / isMaterialVisible
105
-
106
- engine.setIKEnabled(enabled)
107
- engine.setPhysicsEnabled(enabled)
108
- engine.resetPhysics() // re-pose bodies from animation + zero velocities (call if physics explodes)
109
-
110
- engine.setCameraFollow(model, bone?, offset?) / setCameraFollow(null)
111
- engine.setCameraTarget(vec3) / setCameraDistance(d) / setCameraAlpha(a) / setCameraBeta(b)
112
-
113
- engine.setWorld({ color?, strength? }) / setSun({ color?, strength?, direction? }) // runtime lighting
114
- engine.addGround(options?)
115
- engine.runRenderLoop(callback?) / stopRenderLoop()
116
- engine.getStats() // fps + smoothness metrics (frameTimeMax, fps1PercentLow, jitter)
117
- engine.dispose()
118
- ```
119
-
120
- **Options** — Blender-style scene config: `world` = environment lighting, `sun` = directional lamp (`direction` points from sun into the scene), `camera` = framing (`fov` in radians). Callbacks: `onRaycast`, `onGizmoDrag`. The shadow map is cast from `sun.direction` — the same vector the shader lights with — so shading and cast shadows stay coupled.
121
-
122
- ### Model
123
-
124
- ```javascript
125
- await model.loadVmd(name, url) / model.loadClip(name, clip)
126
- model.show(name)
127
- model.play(name, { priority?, loop? }) // priority: higher wins when clips compete (0 = default)
128
- model.pause() / stop() / seek(time)
129
- model.getAnimationProgress() // { current, duration (s), playing, paused, looping, }
130
- model.exportVmd(name) // → ArrayBuffer (Shift-JIS bone/morph names)
131
-
132
- model.rotateBones({ 首: quat }, ms?) / moveBones({ センター: vec3 }, ms?)
133
- model.setMorphWeight(name, weight, ms?)
134
- model.resetAllBones() / resetAllMorphs()
135
- model.getBoneWorldPosition(name)
136
- ```
137
-
138
- `AnimationClip` holds keyframes only (bone/morph tracks keyed by `frame`, plus `frameCount`); time advances at fixed `FPS` (exported, default 30).
139
-
140
- ### Local folder uploads (browser)
141
-
142
- Feed a `<input type="file" webkitdirectory>` `FileList` (or drag/drop) into the engine; textures resolve relative to the chosen PMX inside that tree.
143
-
144
- > **Gotcha:** copy `input.files` into an array **before** `input.value = ""` — the `FileList` is live and clearing the input empties it.
145
-
146
- `parsePmxFolderInput(fileList)` returns a tagged result; for `single` you get `{ files, pmxFile }` directly, for `multiple` show a picker over `pmxRelativePaths` and resolve with `pmxFileAtRelativePath(files, path)`. Then:
147
-
148
- ```javascript
149
- const picked = parsePmxFolderInput(e.target.files)
150
- e.target.value = ""
151
- if (picked.status === "single") await engine.loadModel("m", { files: picked.files, pmxFile: picked.pmxFile })
152
- ```
153
-
154
- VMD and other assets still load by URL when the path starts with `/` or `http(s):`; relative paths resolve against the PMX directory.
155
-
156
- ### Interactive pose editing
157
-
158
- Double-click picks a bone or material (per-triangle dominant-joint from the GPU pick, so one handler serves both modes); a local-axis transform gizmo drags it. **The engine only reports — it never writes the skeleton itself**, so the host chooses the write policy.
159
-
160
- ```typescript
161
- engine.setSelectedBone(modelName | null, boneName | null) // shows the gizmo
162
- engine.setSelectedMaterial(modelName | null, materialName | null) // selection outline
163
-
164
- onRaycast: (modelName, material, bone, screenX, screenY) => { ... } // modelName "" = missed
165
-
166
- type GizmoDragEvent = {
167
- boneName: string; boneIndex: number; kind: "rotate" | "translate"
168
- localRotation: Quat; localTranslation: Vec3 // target absolute local transform
169
- phase?: "start" | "end" // undefined during drag moves
170
- }
171
- ```
172
-
173
- The gizmo consumes mouse input inside its bounding sphere so drags never fight camera orbit. Apply the reported transform — runtime override (below) or keyframe edit into a clip you re-`loadClip`:
174
-
175
- ```javascript
176
- onGizmoDrag: (e) => {
177
- const model = engine.getModel(e.modelName)
178
- if (!model) return
179
- if (e.phase === "start") {
180
- model.pause()
181
- model.setClipApplySuspended(true)
182
- return
183
- } // stop re-sampling wiping the edit
184
- if (e.phase === "end") return
185
- if (e.kind === "rotate")
186
- model.rotateBones({ [e.boneName]: e.localRotation }, 0) // 0 = instant write
187
- else model.setBoneLocalTranslation(e.boneIndex, e.localTranslation)
188
- }
189
- // play()/seek() auto-clear the suspend flag (edit is lost — runtime-override semantic).
190
- ```
191
-
192
- Note the asymmetry: rotation goes through `rotateBones(…, 0)`, but translation uses `setBoneLocalTranslation(idx, v)` — `moveBones` converts VMD-relative→local, and the gizmo output is already local.
193
-
194
- ## Physics
195
-
196
- In-house sequential-impulse rigid-body solver for PMX rigs (sphere / box / capsule colliders, 6DOF spring joints), ~2.5k lines of TypeScript, no external dependency, at quality comparable to Bullet's defaults. A fixed-timestep accumulator runs at a constant **60 Hz** (≤6 substeps/frame) so spring impulse, damping, and integration stay deterministic; dynamic bodies are **render-interpolated** between substeps to stay smooth when the display rate ≠ 60 Hz.
197
-
198
- **Per substep:** `predict velocities → broad + narrowphase → solve constraints (10 iters) → split-impulse position correction → integrate`.
199
-
200
- - **Solver** — projected Gauss-Seidel, joint rows + contact rows in one loop. Joints are 6DOF springs (3 linear + 3 angular) with stop-ERP limit correction and per-axis stiffness×error impulse. Linear rows pivot on each body's own joint-frame origin (Spring2-style, not Bullet 2.7x's shared mid-anchor), so a violated joint pulls itself back together instead of degenerating into torque and "breaking".
201
- - **Angular limits** — hybrid: small violations (< 0.5 rad, the resting-cloth regime) use per-axis Euler stop rows, which converge cleanly and keep resting cloth still; larger violations switch to a single geodesic row toward the Euler-clamped target rotation, because per-axis Euler rows chase phantom errors near the ±90° singularity and pump energy. Ranged stops are unilateral (accumulated impulse clamped to the corrective sign) so a limit pushes back into range but never brakes natural recovery; locked axes stay bilateral equality joints. Spring rows stay per-axis, with stiffness clamped to the `k·dt² ≤ ¼` stability bound.
202
- - **Narrowphase** — analytical sphere-sphere / -capsule / -box and capsule-capsule / -box. Capsule-capsule emits multiple contacts along near-parallel axes so cloth can't pivot around a single closest point.
203
- - **Speculative contacts** (`margin 0.04`) fire at near-touch with a push-only clamp — inert until real overlap, but they stop fast bodies tunneling thin surfaces in one substep.
204
- - **Split-impulse correction** resolves penetration by a mass-weighted translation _outside_ the velocity solver, so joint pulls can't fight separation.
205
- - **Kinematic advancement** — bone-driven bodies move toward the frame's bone pose incrementally per substep, with velocities derived over the fixed step, so the solver never sees more than one 60 Hz step of anchor motion regardless of render dt.
206
- - **Discontinuity guards** — a bone-pose jump beyond continuous motion (timeline scrub, long stall) rigidly carries each dynamic body along with its kinematic root's transform delta and zeroes momentum instead of dragging cloth across the gap; correction velocities are clamped (120 u/s linear, 30 rad/s angular), per-step travel is capped, and any body that still goes non-finite is restored to its previous substep pose.
207
- - Sleeping is off (cloth must always react); resting bodies bleed micro-velocity via per-PMX damping.
208
-
209
- Engine surface is just `setPhysicsEnabled` / `resetPhysics` — all tuning (mass, damping, friction, restitution, joint stiffness/limits, collision groups) lives on the PMX rig.
210
-
211
- ## Rendering
212
-
213
- Each surface mixes an NPR stack with a Principled-style BSDF per material, so characters keep a flat illustrated look while highlights and reflections stay grounded. Shaders live in `engine/src/shaders/materials/`; each fragment shader follows one 7-stage layout (shared stages from `nodes.ts` / `common.ts`):
214
-
215
- ```
216
- (A) setup (B) texture + alpha → (C) NPR stack → (D) optional bump
217
- → (E) Principled BSDF → (F) NPR↔PBR mix → (G) FSOut
218
- ```
219
-
220
- `default` uses only A/B/E/G; NPR presets add C (and sometimes D), with F choosing how NPR-leaning the result is.
221
-
222
- - **PBR core** (`eval_principled`) — GGX + Schlick Fresnel, Walter–Smith G1, Fdez-Agüera 2019 multi-scatter, Karis split-sum DFG LUT, Heitz 2016 LTC direct-spec, optional sheen.
223
- - **NPR toolbox** — toon ramps (constant / fwidth-AA'd), HSV warm-shadow / cool-light remaps, fresnel + layer-weight rims, value-noise bump, Voronoi metallic sparkle, BT.601-gated emission.
224
-
225
- | Preset | Notes |
226
- | -------------- | ----------------------------------------------------------------------- |
227
- | `default` | Plain Principled, metallic 0, rough 0.5 |
228
- | `eye` | Plain + post-eval emission ×1.5 |
229
- | `face` | Toon + warm rim + dual-fresnel rim + bright-tex gate, noise bump |
230
- | `body` | Toon + warm rim + fresnel + facing rim, noise bump |
231
- | `hair` | Toon + fresnel + bevel + bright-tex gate, 20% PBR |
232
- | `cloth_smooth` | Toon + bevel + emission overlay (×18) |
233
- | `cloth_rough` | Same NPR, live noise bump, rough 0.82 |
234
- | `metal` | Toon + emission overlay (×8), Voronoi base, metallic 1 |
235
- | `stockings` | Gradient × facing mask + HSV emission (×5), sheen 0.7, **alpha-hashed** |
236
-
237
- **Post & output.** Directional shadow map (2048², depth32float, PCF) HDR main pass at 4× MSAA (`rg11b10ufloat` color + `rg8unorm` aux MRT for bloom mask + alpha; fits Apple-Silicon TBDR tile memory so MSAA resolves in-tile, `rgba16float` fallback) → bloom mip pyramid → Filmic tone map (Blender 3.6 "Filmic / Medium High Contrast" LUT) → inverted-hull outline.
238
-
239
- - **Alpha-hashed transparency** (`stockings`) — Wyman & McGuire 2017 derivative-aware stochastic discard in object space, so self-overlapping meshes resolve under MSAA with opaque depth writes and the dither doesn't swim.
240
- - **See-through hair over eyes** — stencil-gated extra pass: the eye stamps `EYE_VALUE`, main hair skips it, an extra pass matches it and blends hair at 25% in linear HDR so eyes stay readable.
241
-
242
- ## Used by
243
-
244
- - [Reze Studio](https://reze.studio) (MMD animation editor)
245
- - [MiKaPo](https://mikapo.vercel.app) (motion capture)
246
- - [Popo](https://popo.love) (LLM-generated poses)
247
- - [MPL](https://mmd-mpl.vercel.app) (motion language)
248
- - [Mixamo-MMD](https://mixamo-mmd.vercel.app) (FBX→VMD retarget)
249
-
250
- ## Tutorial
251
-
252
- [How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
1
+ # Reze Engine
2
+
3
+ [npm](https://www.npmjs.com/package/reze-engine)
4
+
5
+ **Zero-runtime-dependency** WebGPU engine for real-time MMD/PMX rendering — renderer, animation, IK, and physics, all in TypeScript.
6
+
7
+ screenshot
8
+
9
+ ```bash
10
+ npm install reze-engine
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - Anime/MMD **hybrid renderer** — toon-ramp NPR over a Principled GGX BSDF, mixed per material
16
+ - **9 per-material presets** assigned by material name (`face` / `hair` / `body` / `eye` / `stockings` / `metal` / `cloth_smooth` / `cloth_rough` / `default`)
17
+ - **Node-graph materials** — Blender-style style graphs (JSON) validated + compiled to WGSL at runtime; instant slider tier, async pipeline swap with fallback-on-error
18
+ - **HDR pipeline** — bloom, Filmic tone mapping, MSAA, Apple-TBDR-friendly targets
19
+ - **In-house TS physics** sequential-impulse rigid bodies for PMX rigs, no external dependency
20
+ - **VMD animation** with MMD IK, morphs (GPU compute path), and VMD export
21
+ - **PMX textures** browser-native formats plus a built-in TGA decoder (common in MMD sphere/eye maps); unsupported files log and fall back, never crash
22
+ - **Interactive editing** — GPU picking, transform gizmo, bone/material selection
23
+ - Orbit camera with bone-follow, ground + PCF shadows, multi-model
24
+
25
+ See [Physics](#physics) and [Rendering](#rendering) for the internals.
26
+
27
+ ## Used by
28
+
29
+ - [Reze Design](https://reze.design) (web-native scene composer & node-graph styling)
30
+ - [Reze Studio](https://reze.studio) (MMD animation editor)
31
+ - [MiKaPo](https://mikapo.vercel.app) (motion capture)
32
+ - [Popo](https://popo.love) (LLM-generated poses)
33
+ - [MPL](https://mmd-mpl.vercel.app) (motion language)
34
+ - [Mixamo-MMD](https://mixamo-mmd.vercel.app) (FBX→VMD retarget)
35
+
36
+ ## Codebase map
37
+
38
+ ```
39
+ engine/src/
40
+ engine.ts Engine: device/context, render loop, all passes & pipelines,
41
+ per-model GPU resources, picking, gizmo (entry point)
42
+ model.ts Model: skeleton, 4-bone skinning, morphs (CPU + GPU compute),
43
+ animation state, drives IK; per-frame update()
44
+ animation.ts AnimationClip, VMD bezier interpolation, playback/priority
45
+ ik-solver.ts MMD-style CCD IK (angle limits, solve-axis specialization)
46
+ camera.ts Orbit camera (alpha/beta/radius), bone-follow, mouse/touch
47
+ math.ts Vec3 / Quat / Mat4 (zero-alloc *Into variants for hot paths)
48
+ pmx-loader.ts PMX parser: mesh, bones, morphs, rigid bodies, joints
49
+ vmd-loader.ts VMD motion parser · vmd-writer.ts VMD export (Shift-JIS)
50
+ asset-reader.ts URL + local-folder asset resolution · folder-upload.ts
51
+ index.ts public exports
52
+
53
+ physics/ in-house rigid-body solver (~2.5k lines)
54
+ physics.ts RezePhysics: bone↔body sync, fixed-step accumulator + interpolation
55
+ solver.ts sequential-impulse PGS (joint + contact rows)
56
+ contact.ts narrowphase (analytical sphere/box/capsule pairs) + contact pool
57
+ constraint.ts 6DOF spring joints · world.ts broadphase + step
58
+ body.ts SoA rigid-body store · types.ts
59
+
60
+ shaders/
61
+ materials/ nodes.ts (Blender-node WGSL library) + common.ts (bindings, skinning VS)
62
+ + one file per preset (face, hair, body, eye, stockings, …)
63
+ passes/ shadow, morph (GPU vertex-morph compute), bloom, composite (Filmic),
64
+ outline, selection, gizmo, pick, ground, mipmap
65
+ ```
66
+
67
+ ## Quick start
68
+
69
+ ```javascript
70
+ import { Engine, Vec3 } from "reze-engine"
71
+
72
+ const engine = new Engine(canvas, {
73
+ world: { color: new Vec3(0.4, 0.49, 0.65), strength: 1.0 }, // environment light
74
+ sun: { color: new Vec3(1, 1, 1), strength: 2.0, direction: new Vec3(0, -0.5, 1) },
75
+ bloom: { color: new Vec3(0.9, 0.1, 0.8), intensity: 0.05, threshold: 0.5 },
76
+ camera: { distance: 31.5, target: new Vec3(0, 11.5, 0) }, // MMD units (1 unit = 8 cm)
77
+ })
78
+ await engine.init()
79
+
80
+ const model = await engine.loadModel("reze", "/models/reze/reze.pmx")
81
+
82
+ // One-tap styling: bucket materials into the engine's built-in default style graphs
83
+ // (face / hair / eye / cloth / stockings / metal). These categories are just the shipped
84
+ // starter graphs — NOT fixed slots. `overrides` maps names the built-in JP/CN/EN hints
85
+ // miss; standard-named models need no map. Unmatched materials stay ungrouped (neutral).
86
+ // For arbitrary groups with any graph, use applyStyleGroups (see "Style graphs & groups").
87
+ await engine.autoStyleGroups("reze", {
88
+ face: ["face01"],
89
+ body: ["skin"],
90
+ hair: ["hair_f"],
91
+ eye: ["eye"],
92
+ cloth_smooth: ["shirt", "dress", "shoes"],
93
+ cloth_rough: ["jacket"],
94
+ stockings: ["stockings"],
95
+ metal: ["earring"],
96
+ })
97
+
98
+ await model.loadVmd("idle", "/animations/idle.vmd")
99
+ model.show("idle")
100
+ model.play()
101
+
102
+ engine.setCameraFollow(model, "センター", new Vec3(0, 3.5, 0))
103
+ engine.addGround({ width: 160, height: 160 })
104
+ engine.runRenderLoop()
105
+ ```
106
+
107
+ ## API
108
+
109
+ One WebGPU **Engine** per page (singleton after `init()`). Models load by URL **or** from a user-selected folder ([below](#local-folder-uploads-browser)).
110
+
111
+ ### Engine
112
+
113
+ ```javascript
114
+ engine.init()
115
+ engine.loadModel(name, path) // or ({ files, pmxFile? }) for folder upload
116
+ engine.getModel(name) / getModelNames() / removeModel(name)
117
+
118
+ engine.autoStyleGroups(name, overrides?) // default style groups by material name
119
+ engine.applyStyleGroups(name, groups) / upsertStyleGroup / removeStyleGroup / getStyleGroups
120
+ engine.setMaterialVisible(name, material, visible) / toggleMaterialVisible / isMaterialVisible
121
+
122
+ engine.setIKEnabled(enabled)
123
+ engine.setPhysicsEnabled(enabled)
124
+ engine.resetPhysics() // re-pose bodies from animation + zero velocities (call if physics explodes)
125
+
126
+ engine.setCameraFollow(model, bone?, offset?) / setCameraFollow(null)
127
+ engine.setCameraTarget(vec3) / setCameraDistance(d) / setCameraAlpha(a) / setCameraBeta(b)
128
+
129
+ engine.setWorld({ color?, strength? }) / setSun({ color?, strength?, direction? }) // runtime lighting
130
+ engine.addGround(options?)
131
+ engine.runRenderLoop(callback?) / stopRenderLoop()
132
+ engine.getStats() // fps + smoothness metrics (frameTimeMax, fps1PercentLow, jitter)
133
+ engine.dispose()
134
+ ```
135
+
136
+ **Options** — Blender-style scene config: `world` = environment lighting, `sun` = directional lamp (`direction` points from sun into the scene), `camera` = framing (`fov` in radians). Callbacks: `onRaycast`, `onGizmoDrag`. The shadow map is cast from `sun.direction` — the same vector the shader lights with — so shading and cast shadows stay coupled.
137
+
138
+ ### Model
139
+
140
+ ```javascript
141
+ await model.loadVmd(name, url) / model.loadClip(name, clip)
142
+ model.show(name)
143
+ model.play(name, { priority?, loop? }) // priority: higher wins when clips compete (0 = default)
144
+ model.pause() / stop() / seek(time)
145
+ model.getAnimationProgress() // { current, duration (s), playing, paused, looping, … }
146
+ model.exportVmd(name) // ArrayBuffer (Shift-JIS bone/morph names)
147
+
148
+ model.rotateBones({ 首: quat }, ms?) / moveBones({ センター: vec3 }, ms?)
149
+ model.setMorphWeight(name, weight, ms?)
150
+ model.resetAllBones() / resetAllMorphs()
151
+ model.getBoneWorldPosition(name)
152
+ ```
153
+
154
+ `AnimationClip` holds keyframes only (bone/morph tracks keyed by `frame`, plus `frameCount`); time advances at fixed `FPS` (exported, default 30).
155
+
156
+ ### Local folder uploads (browser)
157
+
158
+ Feed a `<input type="file" webkitdirectory>` `FileList` (or drag/drop) into the engine; textures resolve relative to the chosen PMX inside that tree.
159
+
160
+ > **Gotcha:** copy `input.files` into an array **before** `input.value = ""` — the `FileList` is live and clearing the input empties it.
161
+
162
+ `parsePmxFolderInput(fileList)` returns a tagged result; for `single` you get `{ files, pmxFile }` directly, for `multiple` show a picker over `pmxRelativePaths` and resolve with `pmxFileAtRelativePath(files, path)`. Then:
163
+
164
+ ```javascript
165
+ const picked = parsePmxFolderInput(e.target.files)
166
+ e.target.value = ""
167
+ if (picked.status === "single") await engine.loadModel("m", { files: picked.files, pmxFile: picked.pmxFile })
168
+ ```
169
+
170
+ VMD and other assets still load by URL when the path starts with `/` or `http(s):`; relative paths resolve against the PMX directory.
171
+
172
+ ### Interactive pose editing
173
+
174
+ Double-click picks a bone or material (per-triangle dominant-joint from the GPU pick, so one handler serves both modes); a local-axis transform gizmo drags it. **The engine only reports — it never writes the skeleton itself**, so the host chooses the write policy.
175
+
176
+ ```typescript
177
+ engine.setSelectedBone(modelName | null, boneName | null) // shows the gizmo
178
+ engine.setSelectedMaterial(modelName | null, materialName | null) // selection outline
179
+
180
+ onRaycast: (modelName, material, bone, screenX, screenY) => { ... } // modelName "" = missed
181
+
182
+ type GizmoDragEvent = {
183
+ boneName: string; boneIndex: number; kind: "rotate" | "translate"
184
+ localRotation: Quat; localTranslation: Vec3 // target absolute local transform
185
+ phase?: "start" | "end" // undefined during drag moves
186
+ }
187
+ ```
188
+
189
+ The gizmo consumes mouse input inside its bounding sphere so drags never fight camera orbit. Apply the reported transform — runtime override (below) or keyframe edit into a clip you re-`loadClip`:
190
+
191
+ ```javascript
192
+ onGizmoDrag: (e) => {
193
+ const model = engine.getModel(e.modelName)
194
+ if (!model) return
195
+ if (e.phase === "start") {
196
+ model.pause()
197
+ model.setClipApplySuspended(true)
198
+ return
199
+ } // stop re-sampling wiping the edit
200
+ if (e.phase === "end") return
201
+ if (e.kind === "rotate")
202
+ model.rotateBones({ [e.boneName]: e.localRotation }, 0) // 0 = instant write
203
+ else model.setBoneLocalTranslation(e.boneIndex, e.localTranslation)
204
+ }
205
+ // play()/seek() auto-clear the suspend flag (edit is lost runtime-override semantic).
206
+ ```
207
+
208
+ Note the asymmetry: rotation goes through `rotateBones(…, 0)`, but translation uses `setBoneLocalTranslation(idx, v)` — `moveBones` converts VMD-relative→local, and the gizmo output is already local.
209
+
210
+ ## Style graphs & groups (node-graph materials)
211
+
212
+ Materials are styled by **node graphs** — plain JSON (`StyleGraph`) validated and compiled to WGSL at runtime. Node semantics are frozen **Blender 3.6 legacy-EEVEE**, so community Blender NPR presets port by transcription. Nine graphs ship built-in (`FACE_GRAPH`, `HAIR_GRAPH`, `BODY_GRAPH`, `EYE_GRAPH`, `METAL_GRAPH`, `STOCKINGS_GRAPH`, `CLOTH_SMOOTH_GRAPH`, `CLOTH_ROUGH_GRAPH`, `DEFAULT_GRAPH` — the neutral base) as a starter library; you can also author or import your own.
213
+
214
+ A **style group** binds `{ materials, graph, renderClass?, alphaMode? }` — a set of materials, the graph that shades them, and the engine's small pass-integration vocabulary (`renderClass`: `auto`/`eye`/`hair` for stencil/cull/draw-order; `alphaMode`: `opaque`/`hashed`). **Groups are user-defined and unlimited** — any materials, any graph. A graph is pure shading; `renderClass` carries the built-in effects (hair's over-eyes stencil, the eye see-through stamp), so any graph in an `eye`/`hair` group inherits them. **Every group needs a valid graph**; a material in no group renders the **neutral default** (`DEFAULT_GRAPH`).
215
+
216
+ Two ways to make groups:
217
+
218
+ ```javascript
219
+ import { HAIR_GRAPH, compileGraph } from "reze-engine"
220
+
221
+ // 1. autoStyleGroups — one default group per built-in category (each backed by its shipped
222
+ // graph), bucketed by material-name hints (+ optional overrides). The "just works" path.
223
+ await engine.autoStyleGroups("reze")
224
+
225
+ // 2. applyStyleGroups — define ARBITRARY groups: any id, any materials, any graph.
226
+ await engine.applyStyleGroups("reze", [
227
+ { id: "hair", materials: ["髪", "前髪"], graph: HAIR_GRAPH, renderClass: "hair" },
228
+ { id: "visor", materials: ["visor", "hud"], graph: myCustomGraph }, // your own graph
229
+ ])
230
+ engine.setStyleParam("reze", "hair", "rim", 0.8) // exposed param instant uniform write
231
+ engine.removeStyleGroup("reze", "hair") // its materials drop to the neutral default
232
+
233
+ // Headless (no GPU needed):
234
+ const { ok, wgsl, diagnostics } = compileGraph(HAIR_GRAPH, { renderClass: "hair" })
235
+ ```
236
+
237
+ Validation catches material conflicts, type mismatches, cycles, and bad links with node-level diagnostics; a failed compile keeps the previous pipeline rendering (fallback-on-error).
238
+
239
+ ## Physics
240
+
241
+ In-house sequential-impulse rigid-body solver for PMX rigs (sphere / box / capsule colliders, 6DOF spring joints), ~2.5k lines of TypeScript, no external dependency, at quality comparable to Bullet's defaults. A fixed-timestep accumulator runs at a constant **60 Hz** (≤6 substeps/frame) so spring impulse, damping, and integration stay deterministic; dynamic bodies are **render-interpolated** between substeps to stay smooth when the display rate ≠ 60 Hz.
242
+
243
+ **Per substep:** `predict velocities → broad + narrowphase → solve constraints (10 iters) → split-impulse position correction → integrate`.
244
+
245
+ - **Solver** — projected Gauss-Seidel, joint rows + contact rows in one loop. Joints are 6DOF springs (3 linear + 3 angular) with stop-ERP limit correction and per-axis stiffness×error impulse. Linear rows pivot on each body's own joint-frame origin (Spring2-style, not Bullet 2.7x's shared mid-anchor), so a violated joint pulls itself back together instead of degenerating into torque and "breaking".
246
+ - **Angular limits** — hybrid: small violations (< 0.5 rad, the resting-cloth regime) use per-axis Euler stop rows, which converge cleanly and keep resting cloth still; larger violations switch to a single geodesic row toward the Euler-clamped target rotation, because per-axis Euler rows chase phantom errors near the ±90° singularity and pump energy. Ranged stops are unilateral (accumulated impulse clamped to the corrective sign) so a limit pushes back into range but never brakes natural recovery; locked axes stay bilateral equality joints. Spring rows stay per-axis, with stiffness clamped to the `k·dt² ≤ ¼` stability bound.
247
+ - **Narrowphase** — analytical sphere-sphere / -capsule / -box and capsule-capsule / -box. Capsule-capsule emits multiple contacts along near-parallel axes so cloth can't pivot around a single closest point.
248
+ - **Speculative contacts** (`margin 0.04`) fire at near-touch with a push-only clamp — inert until real overlap, but they stop fast bodies tunneling thin surfaces in one substep.
249
+ - **Split-impulse correction** resolves penetration by a mass-weighted translation *outside* the velocity solver, so joint pulls can't fight separation.
250
+ - **Kinematic advancement** — bone-driven bodies move toward the frame's bone pose incrementally per substep, with velocities derived over the fixed step, so the solver never sees more than one 60 Hz step of anchor motion regardless of render dt.
251
+ - **Discontinuity guards** — a bone-pose jump beyond continuous motion (timeline scrub, long stall) rigidly carries each dynamic body along with its kinematic root's transform delta and zeroes momentum instead of dragging cloth across the gap; correction velocities are clamped (120 u/s linear, 30 rad/s angular), per-step travel is capped, and any body that still goes non-finite is restored to its previous substep pose.
252
+ - Sleeping is off (cloth must always react); resting bodies bleed micro-velocity via per-PMX damping.
253
+
254
+ Engine surface is just `setPhysicsEnabled` / `resetPhysics` — all tuning (mass, damping, friction, restitution, joint stiffness/limits, collision groups) lives on the PMX rig.
255
+
256
+ ## Rendering
257
+
258
+ Each surface mixes an NPR stack with a Principled-style BSDF per material, so characters keep a flat illustrated look while highlights and reflections stay grounded. Shaders live in `engine/src/shaders/materials/`; each fragment shader follows one 7-stage layout (shared stages from `nodes.ts` / `common.ts`):
259
+
260
+ ```
261
+ (A) setup → (B) texture + alpha → (C) NPR stack → (D) optional bump
262
+ → (E) Principled BSDF → (F) NPR↔PBR mix → (G) FSOut
263
+ ```
264
+
265
+ `default` uses only A/B/E/G; NPR presets add C (and sometimes D), with F choosing how NPR-leaning the result is.
266
+
267
+ - **PBR core** (`eval_principled`) — GGX + Schlick Fresnel, Walter–Smith G1, Fdez-Agüera 2019 multi-scatter, Karis split-sum DFG LUT, Heitz 2016 LTC direct-spec, optional sheen.
268
+ - **NPR toolbox** — toon ramps (constant / fwidth-AA'd), HSV warm-shadow / cool-light remaps, fresnel + layer-weight rims, value-noise bump, Voronoi metallic sparkle, BT.601-gated emission.
269
+
270
+
271
+ | Preset | Notes |
272
+ | -------------- | ----------------------------------------------------------------------- |
273
+ | `default` | Plain Principled, metallic 0, rough 0.5 |
274
+ | `eye` | Plain + post-eval emission ×1.5 |
275
+ | `face` | Toon + warm rim + dual-fresnel rim + bright-tex gate, noise bump |
276
+ | `body` | Toon + warm rim + fresnel + facing rim, noise bump |
277
+ | `hair` | Toon + fresnel + bevel + bright-tex gate, 20% PBR |
278
+ | `cloth_smooth` | Toon + bevel + emission overlay (×18) |
279
+ | `cloth_rough` | Same NPR, live noise bump, rough 0.82 |
280
+ | `metal` | Toon + emission overlay (×8), Voronoi base, metallic 1 |
281
+ | `stockings` | Gradient × facing mask + HSV emission (×5), sheen 0.7, **alpha-hashed** |
282
+
283
+
284
+ **Post & output.** Directional shadow map (2048², depth32float, PCF) → HDR main pass at 4× MSAA (`rg11b10ufloat` color + `rg8unorm` aux MRT for bloom mask + alpha; fits Apple-Silicon TBDR tile memory so MSAA resolves in-tile, `rgba16float` fallback) → bloom mip pyramid → Filmic tone map (Blender 3.6 "Filmic / Medium High Contrast" LUT) → inverted-hull outline.
285
+
286
+ - **Alpha-hashed transparency** (`stockings`) — Wyman & McGuire 2017 derivative-aware stochastic discard in object space, so self-overlapping meshes resolve under MSAA with opaque depth writes and the dither doesn't swim.
287
+ - **See-through hair over eyes** — stencil-gated extra pass: the eye stamps `EYE_VALUE`, main hair skips it, an extra pass matches it and blends hair at 25% in linear HDR so eyes stay readable.
288
+
289
+ ## Tutorial
290
+
291
+ [How to Render an Anime Character with WebGPU](https://reze.one/tutorial)