reze-engine 0.19.0 → 0.21.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 +300 -252
- package/dist/engine.d.ts +12 -25
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +97 -312
- package/dist/graph/compile.d.ts +3 -3
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/presets/body.d.ts +2 -2
- package/dist/graph/presets/body.d.ts.map +1 -1
- package/dist/graph/presets/body.js +1 -1
- package/dist/graph/presets/cloth_rough.d.ts +2 -2
- package/dist/graph/presets/cloth_rough.d.ts.map +1 -1
- package/dist/graph/presets/cloth_rough.js +1 -1
- package/dist/graph/presets/cloth_smooth.d.ts +2 -2
- package/dist/graph/presets/cloth_smooth.d.ts.map +1 -1
- package/dist/graph/presets/cloth_smooth.js +1 -1
- package/dist/graph/presets/default.d.ts +2 -2
- package/dist/graph/presets/default.d.ts.map +1 -1
- package/dist/graph/presets/default.js +13 -6
- package/dist/graph/presets/eye.d.ts +2 -2
- package/dist/graph/presets/eye.d.ts.map +1 -1
- package/dist/graph/presets/eye.js +1 -1
- package/dist/graph/presets/face.d.ts +2 -2
- package/dist/graph/presets/face.d.ts.map +1 -1
- package/dist/graph/presets/face.js +1 -1
- package/dist/graph/presets/hair.d.ts +2 -2
- package/dist/graph/presets/hair.d.ts.map +1 -1
- package/dist/graph/presets/hair.js +1 -1
- package/dist/graph/presets/metal.d.ts +2 -2
- package/dist/graph/presets/metal.d.ts.map +1 -1
- package/dist/graph/presets/metal.js +1 -1
- package/dist/graph/presets/stockings.d.ts +2 -2
- package/dist/graph/presets/stockings.d.ts.map +1 -1
- package/dist/graph/presets/stockings.js +1 -1
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +8 -0
- package/dist/graph/render-class.js +1 -1
- package/dist/graph/schema.d.ts +1 -1
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/schema.js +1 -1
- package/dist/graph/style-group.d.ts +3 -3
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/graph/style-group.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/tga-loader.d.ts +7 -0
- package/dist/tga-loader.d.ts.map +1 -0
- package/dist/tga-loader.js +154 -0
- package/package.json +2 -2
- package/src/engine.ts +4488 -4718
- package/src/graph/compile.ts +3 -3
- package/src/graph/presets/body.ts +3 -3
- package/src/graph/presets/cloth_rough.ts +3 -3
- package/src/graph/presets/cloth_smooth.ts +3 -3
- package/src/graph/presets/default.ts +15 -8
- package/src/graph/presets/eye.ts +3 -3
- package/src/graph/presets/face.ts +3 -3
- package/src/graph/presets/hair.ts +3 -3
- package/src/graph/presets/metal.ts +3 -3
- package/src/graph/presets/stockings.ts +3 -3
- package/src/graph/registry.ts +8 -0
- package/src/graph/render-class.ts +1 -1
- package/src/graph/schema.ts +2 -2
- package/src/graph/style-group.ts +4 -4
- package/src/index.ts +62 -62
- package/src/tga-loader.ts +154 -0
package/README.md
CHANGED
|
@@ -1,252 +1,300 @@
|
|
|
1
|
-
# Reze Engine
|
|
2
|
-
|
|
3
|
-
[
|
|
4
|
-
|
|
5
|
-
**Zero-runtime-dependency** WebGPU engine for real-time MMD/PMX rendering — renderer, animation, IK, and physics, all in TypeScript.
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
- **
|
|
17
|
-
- **HDR pipeline** — bloom, Filmic tone mapping, 4× 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
|
-
- **
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
engine.
|
|
88
|
-
|
|
89
|
-
engine.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
engine.
|
|
112
|
-
|
|
113
|
-
engine.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
- **Shader-graph materials + style groups** — every look is a Blender-style **shader graph** (JSON) compiled to WGSL at runtime; **style groups** bind any set of materials to any graph (unlimited, user-defined). 9 NPR graphs ship built-in (`face` / `hair` / `body` / `eye` / `stockings` / `metal` / `cloth_smooth` / `cloth_rough` / `default`); fully customizable — see [reze-design](https://reze.design) for the visual shader-graph editor
|
|
17
|
+
- **HDR pipeline** — bloom, Filmic tone mapping, 4× 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
|
+
- **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
|
|
21
|
+
- **Interactive editing** — GPU picking, transform gizmo, bone/material selection
|
|
22
|
+
- Orbit camera with bone-follow, ground + PCF shadows, multi-model
|
|
23
|
+
|
|
24
|
+
See [Physics](#physics) and [Rendering](#rendering) for the internals.
|
|
25
|
+
|
|
26
|
+
## Used by
|
|
27
|
+
|
|
28
|
+
- [Reze Design](https://reze.design) (web-native scene composer & shader-graph styling)
|
|
29
|
+
- [Reze Studio](https://reze.studio) (MMD animation editor)
|
|
30
|
+
- [MiKaPo](https://mikapo.vercel.app) (motion capture)
|
|
31
|
+
- [Popo](https://popo.love) (LLM-generated poses)
|
|
32
|
+
- [MPL](https://mmd-mpl.vercel.app) (motion language)
|
|
33
|
+
- [Mixamo-MMD](https://mixamo-mmd.vercel.app) (FBX→VMD retarget)
|
|
34
|
+
|
|
35
|
+
## Codebase map
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
engine/src/
|
|
39
|
+
engine.ts Engine: device/context, render loop, all passes & pipelines,
|
|
40
|
+
per-model GPU resources, picking, gizmo (entry point)
|
|
41
|
+
model.ts Model: skeleton, 4-bone skinning, morphs (CPU + GPU compute),
|
|
42
|
+
animation state, drives IK; per-frame update()
|
|
43
|
+
animation.ts AnimationClip, VMD bezier interpolation, playback/priority
|
|
44
|
+
ik-solver.ts MMD-style CCD IK (angle limits, solve-axis specialization)
|
|
45
|
+
camera.ts Orbit camera (alpha/beta/radius), bone-follow, mouse/touch
|
|
46
|
+
math.ts Vec3 / Quat / Mat4 (zero-alloc *Into variants for hot paths)
|
|
47
|
+
pmx-loader.ts PMX parser: mesh, bones, morphs, rigid bodies, joints
|
|
48
|
+
vmd-loader.ts VMD motion parser · vmd-writer.ts VMD export (Shift-JIS)
|
|
49
|
+
asset-reader.ts URL + local-folder asset resolution · folder-upload.ts
|
|
50
|
+
tga-loader.ts TGA decoder (formats createImageBitmap can't read)
|
|
51
|
+
index.ts public exports
|
|
52
|
+
|
|
53
|
+
graph/ shader-graph → WGSL compiler — materials as data
|
|
54
|
+
schema.ts ShaderGraph / StyleGroup / param types + validation
|
|
55
|
+
registry.ts node registry (Blender node → WGSL) + socket conversions
|
|
56
|
+
compile.ts validate → prune → toposort → peephole → emit
|
|
57
|
+
render-class.ts RenderClass / AlphaMode + the RENDER_CLASSES manifest
|
|
58
|
+
slots.ts per-render-class fs() shell (stencil/alpha) around the graph body
|
|
59
|
+
presets/ the 9 built-in shader graphs (hair, face, eye, cloth, …)
|
|
60
|
+
|
|
61
|
+
physics/ in-house rigid-body solver (~2.5k lines)
|
|
62
|
+
physics.ts RezePhysics: bone↔body sync, fixed-step accumulator + interpolation
|
|
63
|
+
solver.ts sequential-impulse PGS (joint + contact rows)
|
|
64
|
+
contact.ts narrowphase (analytical sphere/box/capsule pairs) + contact pool
|
|
65
|
+
constraint.ts 6DOF spring joints · world.ts broadphase + step
|
|
66
|
+
body.ts SoA rigid-body store · types.ts
|
|
67
|
+
|
|
68
|
+
shaders/
|
|
69
|
+
materials/ nodes.ts (Blender-node WGSL library the graph compiler emits into) +
|
|
70
|
+
common.ts (bindings, skinning VS); the per-material .ts files are the
|
|
71
|
+
original hand shaders, kept as the graph-port reference
|
|
72
|
+
passes/ shadow, morph (GPU vertex-morph compute), bloom, composite (Filmic),
|
|
73
|
+
outline, selection, gizmo, pick, ground, mipmap
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
import { Engine, Vec3 } from "reze-engine"
|
|
80
|
+
|
|
81
|
+
const engine = new Engine(canvas, {
|
|
82
|
+
world: { color: new Vec3(0.4, 0.49, 0.65), strength: 1.0 }, // environment light
|
|
83
|
+
sun: { color: new Vec3(1, 1, 1), strength: 2.0, direction: new Vec3(0, -0.5, 1) },
|
|
84
|
+
bloom: { color: new Vec3(0.9, 0.1, 0.8), intensity: 0.05, threshold: 0.5 },
|
|
85
|
+
camera: { distance: 31.5, target: new Vec3(0, 11.5, 0) }, // MMD units (1 unit = 8 cm)
|
|
86
|
+
})
|
|
87
|
+
await engine.init()
|
|
88
|
+
|
|
89
|
+
const model = await engine.loadModel("reze", "/models/reze/reze.pmx")
|
|
90
|
+
|
|
91
|
+
// One-tap styling: bucket materials into the engine's built-in default shader graphs
|
|
92
|
+
// (face / hair / eye / cloth / stockings / metal). These categories are just the shipped
|
|
93
|
+
// starter graphs — NOT fixed slots. `overrides` maps names the built-in JP/CN/EN hints
|
|
94
|
+
// miss; standard-named models need no map. Unmatched materials stay ungrouped (neutral).
|
|
95
|
+
// For arbitrary groups with any graph, use applyStyleGroups (see "Shader graphs & style groups").
|
|
96
|
+
await engine.autoStyleGroups("reze", {
|
|
97
|
+
face: ["face01"],
|
|
98
|
+
body: ["skin"],
|
|
99
|
+
hair: ["hair_f"],
|
|
100
|
+
eye: ["eye"],
|
|
101
|
+
cloth_smooth: ["shirt", "dress", "shoes"],
|
|
102
|
+
cloth_rough: ["jacket"],
|
|
103
|
+
stockings: ["stockings"],
|
|
104
|
+
metal: ["earring"],
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await model.loadVmd("idle", "/animations/idle.vmd")
|
|
108
|
+
model.show("idle")
|
|
109
|
+
model.play()
|
|
110
|
+
|
|
111
|
+
engine.setCameraFollow(model, "センター", new Vec3(0, 3.5, 0))
|
|
112
|
+
engine.addGround({ width: 160, height: 160 })
|
|
113
|
+
engine.runRenderLoop()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
One WebGPU **Engine** per page (singleton after `init()`). Models load by URL **or** from a user-selected folder ([below](#local-folder-uploads-browser)).
|
|
119
|
+
|
|
120
|
+
### Engine
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
engine.init()
|
|
124
|
+
engine.loadModel(name, path) // or ({ files, pmxFile? }) for folder upload
|
|
125
|
+
engine.getModel(name) / getModelNames() / removeModel(name)
|
|
126
|
+
|
|
127
|
+
engine.autoStyleGroups(name, overrides?) // default style groups by material name
|
|
128
|
+
engine.applyStyleGroups(name, groups) / upsertStyleGroup / removeStyleGroup / getStyleGroups
|
|
129
|
+
engine.setMaterialVisible(name, material, visible) / toggleMaterialVisible / isMaterialVisible
|
|
130
|
+
|
|
131
|
+
engine.setIKEnabled(enabled)
|
|
132
|
+
engine.setPhysicsEnabled(enabled)
|
|
133
|
+
engine.resetPhysics() // re-pose bodies from animation + zero velocities (call if physics explodes)
|
|
134
|
+
|
|
135
|
+
engine.setCameraFollow(model, bone?, offset?) / setCameraFollow(null)
|
|
136
|
+
engine.setCameraTarget(vec3) / setCameraDistance(d) / setCameraAlpha(a) / setCameraBeta(b)
|
|
137
|
+
|
|
138
|
+
engine.setWorld({ color?, strength? }) / setSun({ color?, strength?, direction? }) // runtime lighting
|
|
139
|
+
engine.addGround(options?)
|
|
140
|
+
engine.runRenderLoop(callback?) / stopRenderLoop()
|
|
141
|
+
engine.getStats() // fps + smoothness metrics (frameTimeMax, fps1PercentLow, jitter)
|
|
142
|
+
engine.dispose()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**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.
|
|
146
|
+
|
|
147
|
+
### Model
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
await model.loadVmd(name, url) / model.loadClip(name, clip)
|
|
151
|
+
model.show(name)
|
|
152
|
+
model.play(name, { priority?, loop? }) // priority: higher wins when clips compete (0 = default)
|
|
153
|
+
model.pause() / stop() / seek(time)
|
|
154
|
+
model.getAnimationProgress() // { current, duration (s), playing, paused, looping, … }
|
|
155
|
+
model.exportVmd(name) // → ArrayBuffer (Shift-JIS bone/morph names)
|
|
156
|
+
|
|
157
|
+
model.rotateBones({ 首: quat }, ms?) / moveBones({ センター: vec3 }, ms?)
|
|
158
|
+
model.setMorphWeight(name, weight, ms?)
|
|
159
|
+
model.resetAllBones() / resetAllMorphs()
|
|
160
|
+
model.getBoneWorldPosition(name)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`AnimationClip` holds keyframes only (bone/morph tracks keyed by `frame`, plus `frameCount`); time advances at fixed `FPS` (exported, default 30).
|
|
164
|
+
|
|
165
|
+
### Local folder uploads (browser)
|
|
166
|
+
|
|
167
|
+
Feed a `<input type="file" webkitdirectory>` `FileList` (or drag/drop) into the engine; textures resolve relative to the chosen PMX inside that tree.
|
|
168
|
+
|
|
169
|
+
> **Gotcha:** copy `input.files` into an array **before** `input.value = ""` — the `FileList` is live and clearing the input empties it.
|
|
170
|
+
|
|
171
|
+
`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:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
const picked = parsePmxFolderInput(e.target.files)
|
|
175
|
+
e.target.value = ""
|
|
176
|
+
if (picked.status === "single") await engine.loadModel("m", { files: picked.files, pmxFile: picked.pmxFile })
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
VMD and other assets still load by URL when the path starts with `/` or `http(s):`; relative paths resolve against the PMX directory.
|
|
180
|
+
|
|
181
|
+
### Interactive pose editing
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
engine.setSelectedBone(modelName | null, boneName | null) // shows the gizmo
|
|
187
|
+
engine.setSelectedMaterial(modelName | null, materialName | null) // selection outline
|
|
188
|
+
|
|
189
|
+
onRaycast: (modelName, material, bone, screenX, screenY) => { ... } // modelName "" = missed
|
|
190
|
+
|
|
191
|
+
type GizmoDragEvent = {
|
|
192
|
+
boneName: string; boneIndex: number; kind: "rotate" | "translate"
|
|
193
|
+
localRotation: Quat; localTranslation: Vec3 // target absolute local transform
|
|
194
|
+
phase?: "start" | "end" // undefined during drag moves
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
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`:
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
onGizmoDrag: (e) => {
|
|
202
|
+
const model = engine.getModel(e.modelName)
|
|
203
|
+
if (!model) return
|
|
204
|
+
if (e.phase === "start") {
|
|
205
|
+
model.pause()
|
|
206
|
+
model.setClipApplySuspended(true)
|
|
207
|
+
return
|
|
208
|
+
} // stop re-sampling wiping the edit
|
|
209
|
+
if (e.phase === "end") return
|
|
210
|
+
if (e.kind === "rotate")
|
|
211
|
+
model.rotateBones({ [e.boneName]: e.localRotation }, 0) // 0 = instant write
|
|
212
|
+
else model.setBoneLocalTranslation(e.boneIndex, e.localTranslation)
|
|
213
|
+
}
|
|
214
|
+
// play()/seek() auto-clear the suspend flag (edit is lost — runtime-override semantic).
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
## Shader graphs & style groups
|
|
220
|
+
|
|
221
|
+
Materials are styled by **shader graphs** — plain JSON (`ShaderGraph`) 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.
|
|
222
|
+
|
|
223
|
+
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`).
|
|
224
|
+
|
|
225
|
+
Two ways to make groups:
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
import { HAIR_GRAPH, compileGraph } from "reze-engine"
|
|
229
|
+
|
|
230
|
+
// 1. autoStyleGroups — one default group per built-in category (each backed by its shipped
|
|
231
|
+
// graph), bucketed by material-name hints (+ optional overrides). The "just works" path.
|
|
232
|
+
await engine.autoStyleGroups("reze")
|
|
233
|
+
|
|
234
|
+
// 2. applyStyleGroups — define ARBITRARY groups: any id, any materials, any graph.
|
|
235
|
+
await engine.applyStyleGroups("reze", [
|
|
236
|
+
{ id: "hair", materials: ["髪", "前髪"], graph: HAIR_GRAPH, renderClass: "hair" },
|
|
237
|
+
{ id: "visor", materials: ["visor", "hud"], graph: myCustomGraph }, // your own graph
|
|
238
|
+
])
|
|
239
|
+
engine.setStyleParam("reze", "hair", "rim", 0.8) // exposed param → instant uniform write
|
|
240
|
+
engine.removeStyleGroup("reze", "hair") // its materials drop to the neutral default
|
|
241
|
+
|
|
242
|
+
// Headless (no GPU needed):
|
|
243
|
+
const { ok, wgsl, diagnostics } = compileGraph(HAIR_GRAPH, { renderClass: "hair" })
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
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).
|
|
247
|
+
|
|
248
|
+
## Physics
|
|
249
|
+
|
|
250
|
+
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.
|
|
251
|
+
|
|
252
|
+
**Per substep:** `predict velocities → broad + narrowphase → solve constraints (10 iters) → split-impulse position correction → integrate`.
|
|
253
|
+
|
|
254
|
+
- **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".
|
|
255
|
+
- **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.
|
|
256
|
+
- **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.
|
|
257
|
+
- **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.
|
|
258
|
+
- **Split-impulse correction** resolves penetration by a mass-weighted translation *outside* the velocity solver, so joint pulls can't fight separation.
|
|
259
|
+
- **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.
|
|
260
|
+
- **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.
|
|
261
|
+
- Sleeping is off (cloth must always react); resting bodies bleed micro-velocity via per-PMX damping.
|
|
262
|
+
|
|
263
|
+
Engine surface is just `setPhysicsEnabled` / `resetPhysics` — all tuning (mass, damping, friction, restitution, joint stiffness/limits, collision groups) lives on the PMX rig.
|
|
264
|
+
|
|
265
|
+
## Rendering
|
|
266
|
+
|
|
267
|
+
Each built-in shader graph mixes an NPR stack with a Principled-style BSDF, so characters keep a flat illustrated look while highlights and reflections stay grounded. A graph compiles to a fragment shader following one 7-stage layout (node primitives from `nodes.ts`, the fs() shell from `common.ts`):
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
(A) setup → (B) texture + alpha → (C) NPR stack → (D) optional bump
|
|
271
|
+
→ (E) Principled BSDF → (F) NPR↔PBR mix → (G) FSOut
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`default` uses only A/B/E/G; the NPR graphs add C (and sometimes D), with F choosing how NPR-leaning the result is.
|
|
275
|
+
|
|
276
|
+
- **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.
|
|
277
|
+
- **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.
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
| Built-in graph | Notes |
|
|
281
|
+
| -------------- | ----------------------------------------------------------------------- |
|
|
282
|
+
| `default` | Plain Principled, metallic 0, rough 0.5 |
|
|
283
|
+
| `eye` | Plain + post-eval emission ×1.5 |
|
|
284
|
+
| `face` | Toon + warm rim + dual-fresnel rim + bright-tex gate, noise bump |
|
|
285
|
+
| `body` | Toon + warm rim + fresnel + facing rim, noise bump |
|
|
286
|
+
| `hair` | Toon + fresnel + bevel + bright-tex gate, 20% PBR |
|
|
287
|
+
| `cloth_smooth` | Toon + bevel + emission overlay (×18) |
|
|
288
|
+
| `cloth_rough` | Same NPR, live noise bump, rough 0.82 |
|
|
289
|
+
| `metal` | Toon + emission overlay (×8), Voronoi base, metallic 1 |
|
|
290
|
+
| `stockings` | Gradient × facing mask + HSV emission (×5), sheen 0.7, **alpha-hashed** |
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
**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.
|
|
294
|
+
|
|
295
|
+
- **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.
|
|
296
|
+
- **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.
|
|
297
|
+
|
|
298
|
+
## Tutorial
|
|
299
|
+
|
|
300
|
+
[How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
|