reze-engine 0.63.0 → 0.65.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/dist/engine.d.ts +226 -96
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1187 -732
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +15 -18
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +10 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +15 -1
- package/dist/outline-normals.d.ts +12 -0
- package/dist/outline-normals.d.ts.map +1 -0
- package/dist/outline-normals.js +100 -0
- package/dist/pmx-loader.d.ts +3 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +7 -2
- package/dist/shaders/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +65 -56
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +210 -0
- package/dist/shaders/passes/bloom.d.ts +4 -3
- package/dist/shaders/passes/bloom.d.ts.map +1 -1
- package/dist/shaders/passes/bloom.js +58 -87
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +122 -64
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- package/dist/shaders/passes/mirror.d.ts +18 -12
- package/dist/shaders/passes/mirror.d.ts.map +1 -1
- package/dist/shaders/passes/mirror.js +59 -13
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +71 -14
- package/dist/shaders/passes/particles.d.ts +1 -1
- package/dist/shaders/passes/particles.js +1 -1
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +152 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +529 -0
- package/dist/unity/looks.d.ts +93 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +312 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +120 -0
- package/dist/unity/stage.d.ts +155 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +410 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +1535 -964
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +15 -18
- package/src/index.ts +12 -0
- package/src/model.ts +18 -1
- package/src/outline-normals.ts +110 -0
- package/src/pmx-loader.ts +8 -2
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +65 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/bloom.ts +59 -87
- package/src/shaders/passes/composite.ts +122 -64
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/mirror.ts +60 -13
- package/src/shaders/passes/outline.ts +71 -14
- package/src/shaders/passes/particles.ts +1 -1
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +630 -0
- package/src/unity/looks.ts +391 -0
- package/src/unity/skin.ts +131 -0
- package/src/unity/stage.ts +604 -0
- package/src/unity/state.ts +172 -0
- package/src/shaders/agx-lut.ts +0 -30
package/src/graph/compile.ts
CHANGED
|
@@ -344,9 +344,11 @@ export function compileGraph(graph: ShaderGraph, opts: CompileOptions = {}): Com
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
const lines: string[] = []
|
|
347
|
+
let nodeTakesLight = false
|
|
347
348
|
for (const id of order) {
|
|
348
349
|
const node = nodes.get(id)!
|
|
349
350
|
const s = spec(node)
|
|
351
|
+
if (s.takesLight) nodeTakesLight = true
|
|
350
352
|
if (s.contextOutputs) continue
|
|
351
353
|
const args: Record<string, Resolved> = {}
|
|
352
354
|
for (const [socket, input] of Object.entries(s.inputs)) args[socket] = resolveInput(node, socket, input.type)
|
|
@@ -360,7 +362,7 @@ export function compileGraph(graph: ShaderGraph, opts: CompileOptions = {}): Com
|
|
|
360
362
|
const fsBody = lines.join("\n")
|
|
361
363
|
// A graph takes light when its body reads any: the four shading nodes all
|
|
362
364
|
// pass `sun, amb`, and a hand-written node reaching for the lamps names them.
|
|
363
|
-
const takesLight = /\bsun\b|\bamb\b|rzLight|rzLamp/.test(fsBody)
|
|
365
|
+
const takesLight = nodeTakesLight || /\bsun\b|\bamb\b|rzLight|rzLamp/.test(fsBody)
|
|
364
366
|
const wgsl = assembleModule(opts.renderClass ?? "auto", opts.alphaMode ?? "opaque", fsBody, usesStyle.current, !!opacity, opts.blend ?? "over", takesLight)
|
|
365
367
|
return { ok: true, wgsl, fsBody, slotMap, diagnostics, prunedNodes }
|
|
366
368
|
}
|
package/src/graph/registry.ts
CHANGED
|
@@ -26,6 +26,9 @@ export type NodeSpec = {
|
|
|
26
26
|
contextOutputs?: Record<string, string>
|
|
27
27
|
/** Swizzle applied to the node's variable per output socket (default: none). */
|
|
28
28
|
outputSelect?: Record<string, string>
|
|
29
|
+
/** The node shades by the scene's light without naming it: its graph takes
|
|
30
|
+
* the lamps and the fog as the four shading nodes' graphs do. */
|
|
31
|
+
takesLight?: boolean
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
// ─── Literal formatting ───────────────────────────────────────────────
|
|
@@ -125,6 +128,54 @@ function foldSpecular(ior: string, level: string): string {
|
|
|
125
128
|
return fmtFloat(Number(((r * r * 2 * Math.max(l, 0)) / 0.08).toPrecision(9)))
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
const uberNode = (slot: number): NodeSpec => ({
|
|
132
|
+
inputs: {
|
|
133
|
+
base: C([1, 1, 1], true),
|
|
134
|
+
metallic: F(0),
|
|
135
|
+
roughness: F(0.6),
|
|
136
|
+
occlusion: F(1),
|
|
137
|
+
normal: { type: "vector", contextDefault: "n" },
|
|
138
|
+
direction: { type: "vector", requiresLink: true },
|
|
139
|
+
// Unity's v into the ramp: the property map's alpha, which picks a
|
|
140
|
+
// surface's band (skin 0.702, hair and pale cloth 0.506).
|
|
141
|
+
row: F(0.702),
|
|
142
|
+
receive_shadow: F(1),
|
|
143
|
+
rim_mask: F(1),
|
|
144
|
+
rim_mid: F(0.448),
|
|
145
|
+
rim_width: F(0.11),
|
|
146
|
+
rim_tint: C([0.9623, 0.9214, 0.9214]),
|
|
147
|
+
rim_intensity: F(1),
|
|
148
|
+
rim_albedo: F(0.9),
|
|
149
|
+
rim_in_light: F(1),
|
|
150
|
+
emission: C([0, 0, 0]),
|
|
151
|
+
reflection: F(1),
|
|
152
|
+
// A face's SDF shade (ag_face_sdf); left at -1, the material reads N·L.
|
|
153
|
+
shade: F(-1),
|
|
154
|
+
// 1: the game's FACE_MODE — no GGX, the environment at a flat 0.0157.
|
|
155
|
+
face: F(0),
|
|
156
|
+
},
|
|
157
|
+
outputs: { color: "color" },
|
|
158
|
+
emit: (a) =>
|
|
159
|
+
`ag_uber(${slot}u, ${a.base}, ${a.metallic}, ${a.roughness}, ${a.occlusion}, ${a.normal}, ${a.direction}, ${a.row}, ${a.receive_shadow}, ` +
|
|
160
|
+
`${a.rim_mask}, ${a.rim_mid}, ${a.rim_width}, ${a.rim_tint}, ${a.rim_intensity}, ${a.rim_albedo}, ${a.rim_in_light}, ${a.emission}, ${a.reflection}, ${a.shade}, ${a.face}, input.worldPos, v)`,
|
|
161
|
+
takesLight: true,
|
|
162
|
+
})
|
|
163
|
+
const UBER_NODES: Record<string, NodeSpec> = Object.fromEntries([0, 1, 2, 3].map((s) => [`ag_uber/${s}`, uberNode(s)]))
|
|
164
|
+
// The face's SDF shade, the image on the slot the type names — 104903's face
|
|
165
|
+
// template (SDF_04_shaonv) by default in the AG pack.
|
|
166
|
+
const faceSdfNode = (slot: number): NodeSpec => ({
|
|
167
|
+
inputs: {
|
|
168
|
+
uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" },
|
|
169
|
+
direction: { type: "vector", requiresLink: true },
|
|
170
|
+
normal: { type: "vector", contextDefault: "n" },
|
|
171
|
+
smoothness: F(0.1),
|
|
172
|
+
invert: F(0),
|
|
173
|
+
},
|
|
174
|
+
outputs: { value: "float" },
|
|
175
|
+
emit: (a) => `ag_face_sdf(${slot}u, ${a.uv}.xy, ${a.direction}, ${a.normal}, ${a.smoothness}, ${a.invert})`,
|
|
176
|
+
})
|
|
177
|
+
const FACE_SDF_NODES: Record<string, NodeSpec> = Object.fromEntries([0, 1, 2, 3].map((s) => [`ag_face_sdf/${s}`, faceSdfNode(s)]))
|
|
178
|
+
|
|
128
179
|
export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
129
180
|
// ── Context inputs (template locals; no emission) ──
|
|
130
181
|
texture: {
|
|
@@ -454,6 +505,97 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
454
505
|
emit: (a) => `group_tex3(${a.uv}.xy)`,
|
|
455
506
|
},
|
|
456
507
|
|
|
508
|
+
// ── Aether Gazer's character shading ──
|
|
509
|
+
// The game's character shader (SimPipeline/Character/Debug, which most of its
|
|
510
|
+
// characters wear), taken apart into the steps it runs — see ag_* in
|
|
511
|
+
// nodes.ts for each step's derivation from the decompiled pass. A graph
|
|
512
|
+
// composes them as the game did: a ramp lookup driven by the key light, a
|
|
513
|
+
// matcap, a reflection, the rim and the fill, with the ramp and matcap
|
|
514
|
+
// images on the style group's slots.
|
|
515
|
+
|
|
516
|
+
// The key light: CharacterEffect's direction, held in a frame that turns
|
|
517
|
+
// with the camera about the vertical (sim_TowardMatrix) — the game lights a
|
|
518
|
+
// character from where the viewer is, not from the scene's sun.
|
|
519
|
+
ag_key_light: {
|
|
520
|
+
inputs: { inclination: F(24), azimuth: F(-28) },
|
|
521
|
+
outputs: { direction: "vector" },
|
|
522
|
+
emit: (a) => `ag_key_light(${a.inclination}, ${a.azimuth})`,
|
|
523
|
+
},
|
|
524
|
+
// Where on the shadow ramp this point falls: N·L under the mask's occlusion,
|
|
525
|
+
// and the sun's cast shadow on top when the character receives it.
|
|
526
|
+
ag_shade_term: {
|
|
527
|
+
inputs: {
|
|
528
|
+
normal: { type: "vector", contextDefault: "n" },
|
|
529
|
+
direction: { type: "vector", requiresLink: true },
|
|
530
|
+
occlusion: F(1),
|
|
531
|
+
occlusion_scale: F(1),
|
|
532
|
+
receive_shadow: F(1),
|
|
533
|
+
},
|
|
534
|
+
outputs: { value: "float" },
|
|
535
|
+
emit: (a) => `ag_shade_term(${a.normal}, ${a.direction}, ${a.occlusion}, ${a.occlusion_scale}, ${a.receive_shadow}, input.worldPos)`,
|
|
536
|
+
takesLight: true,
|
|
537
|
+
},
|
|
538
|
+
// A ramp image's coordinate: u from the shade term, v the ramp's row (the
|
|
539
|
+
// game's property map picks it per texel; a PMX carries one per material).
|
|
540
|
+
ag_ramp_uv: {
|
|
541
|
+
inputs: { value: F(0), row: F(0.5) },
|
|
542
|
+
outputs: { uv: "vector" },
|
|
543
|
+
emit: (a) => `ag_ramp_uv(${a.value}, ${a.row})`,
|
|
544
|
+
},
|
|
545
|
+
// The view-space reflection the matcap, the reflection map, the rim and the
|
|
546
|
+
// fill all read: its uv into a sphere image, and how much it faces the eye.
|
|
547
|
+
ag_view_reflection: {
|
|
548
|
+
inputs: { normal: { type: "vector", contextDefault: "n" } },
|
|
549
|
+
outputs: { uv: "vector", facing: "float" },
|
|
550
|
+
outputSelect: { uv: ".xyz", facing: ".w" },
|
|
551
|
+
emit: (a) => `ag_view_reflection(${a.normal}, v)`,
|
|
552
|
+
},
|
|
553
|
+
// The matcap's lift: a metal's shine, at least 1, where the mask says.
|
|
554
|
+
ag_matcap: {
|
|
555
|
+
inputs: { color: C([1, 1, 1], true), matcap: C([1, 1, 1]), strength: F(1), mask: F(1) },
|
|
556
|
+
outputs: { color: "color" },
|
|
557
|
+
emit: (a) => `ag_matcap(${a.color}, ${a.matcap}, ${a.strength}, ${a.mask})`,
|
|
558
|
+
},
|
|
559
|
+
// The rim: the two fixed screen-side directions CharacterEffect sets, cut to
|
|
560
|
+
// the silhouette, laid on as a colour dodge where the mask allows.
|
|
561
|
+
ag_rim: {
|
|
562
|
+
inputs: {
|
|
563
|
+
color: C([1, 1, 1], true),
|
|
564
|
+
facing: F(1),
|
|
565
|
+
uv: { type: "vector", requiresLink: true },
|
|
566
|
+
rim_color: C([0.8584906, 0.8584906, 0.8584906]),
|
|
567
|
+
threshold: F(0.1),
|
|
568
|
+
fade: F(0.113),
|
|
569
|
+
range: F(0.444),
|
|
570
|
+
inclination: F(0),
|
|
571
|
+
azimuth1: F(0.8499963),
|
|
572
|
+
azimuth2: F(1),
|
|
573
|
+
mask: F(1),
|
|
574
|
+
},
|
|
575
|
+
outputs: { color: "color" },
|
|
576
|
+
emit: (a) =>
|
|
577
|
+
`ag_rim(${a.color}, ${a.facing}, ${a.uv}, ${a.rim_color}, ${a.threshold}, ${a.fade}, ${a.range}, ${a.inclination}, ${a.azimuth1}, ${a.azimuth2}, ${a.mask})`,
|
|
578
|
+
},
|
|
579
|
+
// The fill: a colour laid toward the silhouette, inner to outer.
|
|
580
|
+
ag_fill: {
|
|
581
|
+
inputs: {
|
|
582
|
+
color: C([1, 1, 1], true),
|
|
583
|
+
facing: F(1),
|
|
584
|
+
inner: C([0, 0, 0]),
|
|
585
|
+
outer: C([0, 0, 0]),
|
|
586
|
+
amount: F(0),
|
|
587
|
+
softness: F(0),
|
|
588
|
+
},
|
|
589
|
+
outputs: { color: "color" },
|
|
590
|
+
emit: (a) => `ag_fill(${a.color}, ${a.facing}, ${a.inner}, ${a.outer}, ${a.amount}, ${a.softness})`,
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
// Aether Gazer's current character shading (PBR/Uber) as one closure, the way
|
|
594
|
+
// Principled is one: the ramp image on the group slot named by the type.
|
|
595
|
+
// Defaults are 104903's skin.
|
|
596
|
+
...UBER_NODES,
|
|
597
|
+
...FACE_SDF_NODES,
|
|
598
|
+
|
|
457
599
|
// ── Blender 5.2 colour utilities ──
|
|
458
600
|
separate_color: {
|
|
459
601
|
inputs: { color: C([1, 1, 1], true) },
|
package/src/graph/schema.ts
CHANGED
|
@@ -66,6 +66,15 @@ export type ShaderGraph = {
|
|
|
66
66
|
* matching (e.g. ["hair"]). Ignored by the compiler; round-tripped. A graph is pure
|
|
67
67
|
* shading — pass integration lives on the style group's renderClass, not here. */
|
|
68
68
|
tags?: string[]
|
|
69
|
+
/**
|
|
70
|
+
* The images this look samples on `tex_image/0`…`tex_image/3`, by URL —
|
|
71
|
+
* a ramp, a matcap, the maps a game's look is built on. They belong to the
|
|
72
|
+
* LOOK, not to the model it is put on: they are read at coordinates the
|
|
73
|
+
* shading computes (a light amount, a reflection), never at the mesh's uvs,
|
|
74
|
+
* so one graph carries them to any model. A style group that brings its own
|
|
75
|
+
* images keeps those.
|
|
76
|
+
*/
|
|
77
|
+
images?: ({ url: string; srgb?: boolean } | null)[]
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
export type Diagnostic = {
|
package/src/graph/slots.ts
CHANGED
|
@@ -137,20 +137,17 @@ ${discard}
|
|
|
137
137
|
var n = safe_normal(input.normal);
|
|
138
138
|
let v = normalize(camera.viewPos - input.worldPos);${flip}
|
|
139
139
|
${gate}
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// The key: the directional light this draw's layers take — the sun unless a
|
|
141
|
+
// light on its own layer stands in. The shadow stays the sun's atlas, as the
|
|
142
|
+
// game keeps the scene's shadow under a character's own key.
|
|
143
|
+
let rz_key = rzMainLight();
|
|
144
|
+
let l = -light.lights[rz_key].direction.xyz;
|
|
145
|
+
let sun = light.lights[rz_key].color.xyz * light.lights[rz_key].color.w;
|
|
142
146
|
let shadow = sampleShadow(input.worldPos, n);
|
|
143
|
-
// The
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
// A model given a flat sky (setModelFlatSky - the cast, for an anime look)
|
|
148
|
-
// takes the sky's AVERAGE instead: its colour and brightness from every
|
|
149
|
-
// direction alike. The directional sky shaded a face like PBR - soft
|
|
150
|
-
// realistic gradients the toon ramp never drew - where the game gives its
|
|
151
|
-
// characters one flat base light. A flat World is its own average, so a
|
|
152
|
-
// scene without an HDRI renders exactly as before.
|
|
153
|
-
let amb = select(rzWorldAmbient(n), rzWorldAmbientAvg(), modelLight.opts.x > 0.5);
|
|
147
|
+
// The ambient: this object's own where loaded data gave it one — the game
|
|
148
|
+
// gives every renderer its own — else the world's flat colour or the HDRI's
|
|
149
|
+
// irradiance at this normal.
|
|
150
|
+
let amb = rzObjectAmbient(n);
|
|
154
151
|
let tex_color = tex_s.rgb;
|
|
155
152
|
// The normal the lamps' diffuse layer is lit by, for a principled walk to
|
|
156
153
|
// compute it on the way (rzLightsDiffuseOnce). n is final from here on.
|
|
@@ -196,11 +193,11 @@ function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: bo
|
|
|
196
193
|
// alpha, into a glowing disc under the spot above it.
|
|
197
194
|
//
|
|
198
195
|
// The model's fill (Engine.setModelFill) rides here too, as brightness: the
|
|
199
|
-
// surface colour times the fill, on every pixel of her alike.
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
const LIT = takesLight ? ` + (rzLightsDiffuseOnce(input.worldPos, n) +
|
|
196
|
+
// surface colour times the fill, on every pixel of her alike. In the graph's
|
|
197
|
+
// ambient an NPR ramp would read it as LIGHT — sliding it would move her
|
|
198
|
+
// shadows, and under a hard step flip whole regions at once. After the graph
|
|
199
|
+
// it lifts her evenly and leaves every shadow where it is.
|
|
200
|
+
const LIT = takesLight ? ` + (rzLightsDiffuseOnce(input.worldPos, n) + objectLight.fill.rgb) * albedo` : ""
|
|
204
201
|
|
|
205
202
|
const ALBEDO = ` let albedo = tex_color;
|
|
206
203
|
`
|
package/src/index.ts
CHANGED
|
@@ -14,11 +14,14 @@ export {
|
|
|
14
14
|
type BloomOptions,
|
|
15
15
|
type DepthOfFieldOptions,
|
|
16
16
|
type ViewTransformOptions,
|
|
17
|
+
type ViewTransformName,
|
|
17
18
|
type LoadModelFromFilesOptions,
|
|
18
19
|
type MaterialPreset,
|
|
19
20
|
type MaterialPresetMap,
|
|
20
21
|
type ModelTransform,
|
|
21
22
|
type SceneLight,
|
|
23
|
+
RENDERING_LAYER_DEFAULT,
|
|
24
|
+
RENDERING_LAYER_CHARACTER,
|
|
22
25
|
type ModelAttachment,
|
|
23
26
|
type GizmoDragEvent,
|
|
24
27
|
type GizmoDragCallback,
|
|
@@ -215,3 +218,12 @@ export {
|
|
|
215
218
|
*/
|
|
216
219
|
export { EFFECT_MATH_API, PARTICLE_STRUCT_WGSL } from "./shaders/passes/hosted-api"
|
|
217
220
|
export type { EyeTrackingOptions } from "./model"
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The game's own materials (src/unity): a stage package drawn by its translated
|
|
224
|
+
* shaders (Engine.setNativeStage) and a model dressed in a native look
|
|
225
|
+
* (Engine.setModelNativeLook) — the shapes their files take.
|
|
226
|
+
*/
|
|
227
|
+
export type { NativeStagePackage, NativeStageReader } from "./unity/stage"
|
|
228
|
+
export type { NativeLook, NativeMaterialSpec, NativeTexture } from "./unity/looks"
|
|
229
|
+
export type { NativeShader, NativeShaderInfo } from "./unity/host"
|
package/src/model.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Engine } from "./engine"
|
|
|
3
3
|
import { joinAssetPath, type AssetReader } from "./asset-reader"
|
|
4
4
|
import { Rigidbody, Joint } from "./physics"
|
|
5
5
|
import { IKSolverSystem } from "./ik-solver"
|
|
6
|
+
import { buildOutlineVertices } from "./outline-normals"
|
|
6
7
|
import { VMDLoader, type VMDKeyFrame } from "./vmd-loader"
|
|
7
8
|
import { VMDWriter, type VmdTrackSelection } from "./vmd-writer"
|
|
8
9
|
import {
|
|
@@ -564,8 +565,10 @@ export class Model {
|
|
|
564
565
|
morphing: Morphing,
|
|
565
566
|
rigidbodies: Rigidbody[] = [],
|
|
566
567
|
joints: Joint[] = [],
|
|
567
|
-
loadWarnings: string[] = []
|
|
568
|
+
loadWarnings: string[] = [],
|
|
569
|
+
edgeScales: Float32Array | null = null
|
|
568
570
|
) {
|
|
571
|
+
this.edgeScales = edgeScales
|
|
569
572
|
// Store base vertex data (original positions before morphing)
|
|
570
573
|
this.baseVertexData = new Float32Array(vertexData)
|
|
571
574
|
this.vertexData = vertexData
|
|
@@ -1001,6 +1004,20 @@ export class Model {
|
|
|
1001
1004
|
return this.vertexData
|
|
1002
1005
|
}
|
|
1003
1006
|
|
|
1007
|
+
/** PMX per-vertex edge scale, when the source had one. */
|
|
1008
|
+
private edgeScales: Float32Array | null = null
|
|
1009
|
+
private outlineVertices: Float32Array<ArrayBuffer> | null = null
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* The outline hull's own vertex stream: per vertex, the smoothed rest-pose
|
|
1013
|
+
* normal (xyz) and the PMX edge scale (w). See outline-normals.ts. Built on
|
|
1014
|
+
* first ask — only models with an edge-flagged material ever ask.
|
|
1015
|
+
*/
|
|
1016
|
+
getOutlineVertices(): Float32Array<ArrayBuffer> {
|
|
1017
|
+
this.outlineVertices ??= buildOutlineVertices(this.baseVertexData, VERTEX_STRIDE, this.indexData, this.edgeScales ?? undefined)
|
|
1018
|
+
return this.outlineVertices
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1004
1021
|
getTextures(): Texture[] {
|
|
1005
1022
|
return this.textures
|
|
1006
1023
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The inverted hull's own push direction, one per vertex, built once at load.
|
|
2
|
+
//
|
|
3
|
+
// The hull is the model pushed out along a normal. Pushed along the MESH's
|
|
4
|
+
// normals it tears wherever the mesh carries two vertices at one position —
|
|
5
|
+
// every hard edge and every UV seam — because the two copies go two ways and
|
|
6
|
+
// the gap between them shows the body through the line: the jaw, the finger
|
|
7
|
+
// joints, the parting in the hair. Aether Gazer bakes a smoothed normal into
|
|
8
|
+
// vertex colour for exactly this (Character/Debug.shader, pass "Outline"); a
|
|
9
|
+
// PMX has nowhere to carry one, so it is computed here instead: the normals of
|
|
10
|
+
// every vertex at one position, averaged, so the copies agree and the hull
|
|
11
|
+
// stays closed.
|
|
12
|
+
//
|
|
13
|
+
// Each vertex's vote is weighted by the corner angles it owns, which is what
|
|
14
|
+
// makes a hard corner come out as the corner's true bisector rather than
|
|
15
|
+
// leaning toward whichever side happened to be cut into more triangles.
|
|
16
|
+
//
|
|
17
|
+
// Rest pose only, as the game's is: a morph moves the positions and leaves this
|
|
18
|
+
// alone, and skinning turns it exactly as it turns the normal.
|
|
19
|
+
|
|
20
|
+
/** Positions closer than this (model units) are one position. */
|
|
21
|
+
export const OUTLINE_WELD_EPSILON = 1e-4
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Smoothed outline normals, packed with the PMX per-vertex edge scale:
|
|
25
|
+
* xyz = unit direction, w = edge scale — four floats per vertex.
|
|
26
|
+
*
|
|
27
|
+
* @param vertexData interleaved [pos3, normal3, uv2] per vertex
|
|
28
|
+
* @param indices triangle list
|
|
29
|
+
* @param edgeScales per-vertex PMX edge scale, or omitted for 1
|
|
30
|
+
*/
|
|
31
|
+
export function buildOutlineVertices(
|
|
32
|
+
vertexData: ArrayLike<number>,
|
|
33
|
+
stride: number,
|
|
34
|
+
indices: ArrayLike<number>,
|
|
35
|
+
edgeScales?: ArrayLike<number>,
|
|
36
|
+
): Float32Array<ArrayBuffer> {
|
|
37
|
+
const n = Math.floor(vertexData.length / stride)
|
|
38
|
+
const out = new Float32Array(n * 4)
|
|
39
|
+
|
|
40
|
+
// Corner angles each vertex owns. A vertex no triangle uses keeps a token
|
|
41
|
+
// weight, so it still has a direction of its own.
|
|
42
|
+
const weight = new Float32Array(n).fill(1e-6)
|
|
43
|
+
for (let t = 0; t + 2 < indices.length; t += 3) {
|
|
44
|
+
const a = indices[t], b = indices[t + 1], c = indices[t + 2]
|
|
45
|
+
if (a >= n || b >= n || c >= n) continue
|
|
46
|
+
weight[a] += cornerAngle(vertexData, stride, a, b, c)
|
|
47
|
+
weight[b] += cornerAngle(vertexData, stride, b, c, a)
|
|
48
|
+
weight[c] += cornerAngle(vertexData, stride, c, a, b)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Weld by quantized position.
|
|
52
|
+
const inv = 1 / OUTLINE_WELD_EPSILON
|
|
53
|
+
const group = new Int32Array(n)
|
|
54
|
+
const ids = new Map<string, number>()
|
|
55
|
+
let groups = 0
|
|
56
|
+
for (let i = 0; i < n; i++) {
|
|
57
|
+
const o = i * stride
|
|
58
|
+
const key = `${Math.round(vertexData[o] * inv)},${Math.round(vertexData[o + 1] * inv)},${Math.round(vertexData[o + 2] * inv)}`
|
|
59
|
+
let g = ids.get(key)
|
|
60
|
+
if (g === undefined) {
|
|
61
|
+
g = groups++
|
|
62
|
+
ids.set(key, g)
|
|
63
|
+
}
|
|
64
|
+
group[i] = g
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sum = new Float32Array(groups * 3)
|
|
68
|
+
for (let i = 0; i < n; i++) {
|
|
69
|
+
const o = i * stride
|
|
70
|
+
const w = weight[i]
|
|
71
|
+
const g = group[i] * 3
|
|
72
|
+
sum[g] += vertexData[o + 3] * w
|
|
73
|
+
sum[g + 1] += vertexData[o + 4] * w
|
|
74
|
+
sum[g + 2] += vertexData[o + 5] * w
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < n; i++) {
|
|
78
|
+
const o = i * stride
|
|
79
|
+
const nx = vertexData[o + 3], ny = vertexData[o + 4], nz = vertexData[o + 5]
|
|
80
|
+
const g = group[i] * 3
|
|
81
|
+
let x = sum[g], y = sum[g + 1], z = sum[g + 2]
|
|
82
|
+
const len = Math.hypot(x, y, z)
|
|
83
|
+
// Two sheets back to back at one position (a double-sided skirt, a shell
|
|
84
|
+
// and its lining) cancel to nothing, or to a direction pointing INTO one of
|
|
85
|
+
// them. Either way the welded answer is wrong for this vertex, and its own
|
|
86
|
+
// normal is the honest one.
|
|
87
|
+
if (len < 1e-6 || x * nx + y * ny + z * nz <= 0) {
|
|
88
|
+
x = nx
|
|
89
|
+
y = ny
|
|
90
|
+
z = nz
|
|
91
|
+
}
|
|
92
|
+
const l = Math.hypot(x, y, z) || 1
|
|
93
|
+
out[i * 4] = x / l
|
|
94
|
+
out[i * 4 + 1] = y / l
|
|
95
|
+
out[i * 4 + 2] = z / l
|
|
96
|
+
out[i * 4 + 3] = edgeScales ? edgeScales[i] : 1
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The angle at vertex `a` of triangle (a, b, c), in radians. */
|
|
102
|
+
function cornerAngle(v: ArrayLike<number>, stride: number, a: number, b: number, c: number): number {
|
|
103
|
+
const oa = a * stride, ob = b * stride, oc = c * stride
|
|
104
|
+
const ux = v[ob] - v[oa], uy = v[ob + 1] - v[oa + 1], uz = v[ob + 2] - v[oa + 2]
|
|
105
|
+
const wx = v[oc] - v[oa], wy = v[oc + 1] - v[oa + 1], wz = v[oc + 2] - v[oa + 2]
|
|
106
|
+
const lu = Math.hypot(ux, uy, uz), lw = Math.hypot(wx, wy, wz)
|
|
107
|
+
if (lu < 1e-12 || lw < 1e-12) return 0
|
|
108
|
+
const cos = (ux * wx + uy * wy + uz * wz) / (lu * lw)
|
|
109
|
+
return Math.acos(Math.max(-1, Math.min(1, cos)))
|
|
110
|
+
}
|
package/src/pmx-loader.ts
CHANGED
|
@@ -36,6 +36,9 @@ export class PmxLoader {
|
|
|
36
36
|
private inverseBindMatrices: Float32Array | null = null
|
|
37
37
|
private joints0: Uint16Array | null = null
|
|
38
38
|
private weights0: Uint8Array | null = null
|
|
39
|
+
/** PMX per-vertex edge scale — the outline's width multiplier, as the
|
|
40
|
+
* game's outline takes vertex colour alpha. */
|
|
41
|
+
private edgeScales: Float32Array | null = null
|
|
39
42
|
private morphs: Morph[] = []
|
|
40
43
|
private vertexCount: number = 0
|
|
41
44
|
private rigidbodies: Rigidbody[] = []
|
|
@@ -150,6 +153,7 @@ export class PmxLoader {
|
|
|
150
153
|
// Prepare skinning arrays (4 influences per vertex)
|
|
151
154
|
const joints = new Uint16Array(count * 4)
|
|
152
155
|
const weights = new Uint8Array(count * 4) // UNORM8, will be normalized to 255
|
|
156
|
+
const edgeScales = new Float32Array(count)
|
|
153
157
|
|
|
154
158
|
for (let i = 0; i < count; i++) {
|
|
155
159
|
const px = this.getFloat32()
|
|
@@ -238,11 +242,12 @@ export class PmxLoader {
|
|
|
238
242
|
} else {
|
|
239
243
|
throw new Error(`Invalid bone weight type: ${type}`)
|
|
240
244
|
}
|
|
241
|
-
this.
|
|
245
|
+
edgeScales[i] = this.getFloat32()
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
this.joints0 = joints
|
|
245
249
|
this.weights0 = weights
|
|
250
|
+
this.edgeScales = edgeScales
|
|
246
251
|
return { positions, normals, uvs }
|
|
247
252
|
}
|
|
248
253
|
|
|
@@ -1136,7 +1141,8 @@ export class PmxLoader {
|
|
|
1136
1141
|
morphing,
|
|
1137
1142
|
this.rigidbodies,
|
|
1138
1143
|
this.joints,
|
|
1139
|
-
this.warnings
|
|
1144
|
+
this.warnings,
|
|
1145
|
+
this.edgeScales
|
|
1140
1146
|
)
|
|
1141
1147
|
}
|
|
1142
1148
|
|
package/src/shaders/lights.ts
CHANGED
|
@@ -28,7 +28,8 @@ import { subjectMaskApi } from "./cast-api"
|
|
|
28
28
|
// [0..2] position, world space [3] radius
|
|
29
29
|
// [4..6] colour PREMULTIPLIED by intensity [7] type
|
|
30
30
|
// [8..10] aim, unit, pointing away from the light [11] cos of the outer angle
|
|
31
|
-
// [12] cos of the inner angle
|
|
31
|
+
// [12] cos of the inner angle
|
|
32
|
+
// [13] the rendering layers it does NOT reach (u32 bits) [14..15] spare
|
|
32
33
|
//
|
|
33
34
|
// then the grid: LIGHT_MASK_WORDS words of lamp bits per cell.
|
|
34
35
|
//
|
|
@@ -40,6 +41,12 @@ import { subjectMaskApi } from "./cast-api"
|
|
|
40
41
|
// branchless: it stores aim (0,0,0) and both cosines at -1, which makes the
|
|
41
42
|
// cone term below saturate to exactly 1. `type` says which a light is for
|
|
42
43
|
// anyone reading the buffer; the shading never asks.
|
|
44
|
+
//
|
|
45
|
+
// LAYERS, as the game's pipeline (URP's rendering layers) gates its lights: a
|
|
46
|
+
// draw carries layer bits, and a light reaches it only where its own mask
|
|
47
|
+
// shares one. Stored INVERTED — the layers a light does not reach — so the
|
|
48
|
+
// zero every writer that predates layers leaves in [13] reaches everything,
|
|
49
|
+
// which is what an effect's lamp and a plain document light mean.
|
|
43
50
|
|
|
44
51
|
import { castDistanceStub } from "./passes/cast-distance"
|
|
45
52
|
import { audioApi } from "./audio-api"
|
|
@@ -243,8 +250,9 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
243
250
|
`
|
|
244
251
|
}
|
|
245
252
|
|
|
246
|
-
/** The rz*Light accessors, with the buffer declared at the given binding.
|
|
247
|
-
|
|
253
|
+
/** The rz*Light accessors, with the buffer declared at the given binding.
|
|
254
|
+
* `layers` is the WGSL expression for the drawing's rendering-layer bits. */
|
|
255
|
+
export function lightsApi(group: number, binding: number, layers: string): string {
|
|
248
256
|
const R = LIGHT_HEADER / 4
|
|
249
257
|
const S = LIGHT_STRIDE / 4
|
|
250
258
|
return /* wgsl */ `
|
|
@@ -286,6 +294,10 @@ fn rzLightAim(i: u32) -> vec3f { return _rzLightVec(i, 2u).xyz; }
|
|
|
286
294
|
* light stores (-1, -1), which saturates the cone term to 1. */
|
|
287
295
|
fn rzLightCone(i: u32) -> vec2f { return vec2f(_rzLightVec(i, 2u).w, _rzLightVec(i, 3u).x); }
|
|
288
296
|
|
|
297
|
+
/** Does light i reach this drawing's rendering layers? Read as bits, never as
|
|
298
|
+
* a float — see the layout. */
|
|
299
|
+
fn _rzLightReaches(i: u32) -> bool { return ((${layers}) & ~_rzLights[${R}u + i * ${S}u + 3u].y) != 0u; }
|
|
300
|
+
|
|
289
301
|
/** The document lamps that can reach p: its grid cell's bits, or the outside
|
|
290
302
|
* mask beyond the grid. Written so a NaN position fails the inside test. */
|
|
291
303
|
fn _rzLightCellMask(p: vec3f) -> vec4u {
|
|
@@ -312,6 +324,7 @@ fn _rzLightCellMask(p: vec3f) -> vec4u {
|
|
|
312
324
|
* writes in candela over 683.
|
|
313
325
|
*/
|
|
314
326
|
fn _rzLightOne(i: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
327
|
+
if (!_rzLightReaches(i)) { return vec3f(0.0); }
|
|
315
328
|
let pr = _rzLightVec(i, 0u);
|
|
316
329
|
let d = pr.xyz - p;
|
|
317
330
|
let dist = length(d);
|