reze-engine 0.17.0 → 0.18.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 +252 -250
- package/dist/engine.d.ts +36 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +258 -15
- package/dist/gpu-profile.d.ts +19 -0
- package/dist/gpu-profile.d.ts.map +1 -0
- package/dist/gpu-profile.js +120 -0
- package/dist/graph/compile.d.ts +34 -0
- package/dist/graph/compile.d.ts.map +1 -0
- package/dist/graph/compile.js +299 -0
- package/dist/graph/presets/body.d.ts +3 -0
- package/dist/graph/presets/body.d.ts.map +1 -0
- package/dist/graph/presets/body.js +100 -0
- package/dist/graph/presets/cloth_rough.d.ts +3 -0
- package/dist/graph/presets/cloth_rough.d.ts.map +1 -0
- package/dist/graph/presets/cloth_rough.js +61 -0
- package/dist/graph/presets/cloth_smooth.d.ts +3 -0
- package/dist/graph/presets/cloth_smooth.d.ts.map +1 -0
- package/dist/graph/presets/cloth_smooth.js +53 -0
- package/dist/graph/presets/default.d.ts +3 -0
- package/dist/graph/presets/default.d.ts.map +1 -0
- package/dist/graph/presets/default.js +20 -0
- package/dist/graph/presets/hair.d.ts +3 -0
- package/dist/graph/presets/hair.d.ts.map +1 -0
- package/dist/graph/presets/hair.js +79 -0
- package/dist/graph/presets/metal.d.ts +3 -0
- package/dist/graph/presets/metal.d.ts.map +1 -0
- package/dist/graph/presets/metal.js +58 -0
- package/dist/graph/presets/stockings.d.ts +3 -0
- package/dist/graph/presets/stockings.d.ts.map +1 -0
- package/dist/graph/presets/stockings.js +54 -0
- package/dist/graph/registry.d.ts +28 -0
- package/dist/graph/registry.d.ts.map +1 -0
- package/dist/graph/registry.js +322 -0
- package/dist/graph/schema.d.ts +66 -0
- package/dist/graph/schema.d.ts.map +1 -0
- package/dist/graph/schema.js +6 -0
- package/dist/graph/slots.d.ts +14 -0
- package/dist/graph/slots.d.ts.map +1 -0
- package/dist/graph/slots.js +123 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/physics/profile.d.ts +18 -0
- package/dist/physics/profile.d.ts.map +1 -0
- package/dist/physics/profile.js +44 -0
- package/package.json +43 -42
- package/src/engine.ts +301 -20
- package/src/graph/compile.ts +342 -0
- package/src/graph/presets/body.ts +103 -0
- package/src/graph/presets/cloth_rough.ts +64 -0
- package/src/graph/presets/cloth_smooth.ts +56 -0
- package/src/graph/presets/default.ts +23 -0
- package/src/graph/presets/hair.ts +82 -0
- package/src/graph/presets/metal.ts +61 -0
- package/src/graph/presets/stockings.ts +57 -0
- package/src/graph/registry.ts +351 -0
- package/src/graph/schema.ts +60 -0
- package/src/graph/slots.ts +145 -0
- package/src/index.ts +54 -28
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
package/src/engine.ts
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
import { COMPOSITE_SHADER_WGSL } from "./shaders/passes/composite"
|
|
40
40
|
import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
|
|
41
41
|
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
|
|
42
|
+
import { compileGraph, type CompileOptions, type StyleSlot } from "./graph/compile"
|
|
43
|
+
import type { Diagnostic, StyleGraph } from "./graph/schema"
|
|
42
44
|
|
|
43
45
|
// Material preset dispatch. Consumers supply a MaterialPresetMap assigning material names
|
|
44
46
|
// to presets; unmapped materials fall back to "default" (Principled BSDF).
|
|
@@ -52,7 +54,13 @@ export type MaterialPreset =
|
|
|
52
54
|
| "metal"
|
|
53
55
|
| "cloth_smooth"
|
|
54
56
|
| "cloth_rough"
|
|
55
|
-
|
|
57
|
+
|
|
58
|
+
// What a draw call actually resolves to. "mmd_classic" is the automatic
|
|
59
|
+
// authentic-MMD fallback for materials no preset map or name heuristic
|
|
60
|
+
// covers — deliberately NOT part of MaterialPreset, so consumer-side
|
|
61
|
+
// exhaustive Record<MaterialPreset, …> maps and switches don't have to
|
|
62
|
+
// carry an option users never assign.
|
|
63
|
+
export type ResolvedMaterialPreset = MaterialPreset | "mmd_classic"
|
|
56
64
|
|
|
57
65
|
export type MaterialPresetMap = Partial<Record<MaterialPreset, string[]>>
|
|
58
66
|
|
|
@@ -102,7 +110,7 @@ const PRESET_NAME_HINTS: Array<[MaterialPreset, string[]]> = [
|
|
|
102
110
|
],
|
|
103
111
|
]
|
|
104
112
|
|
|
105
|
-
function resolvePreset(materialName: string, map: MaterialPresetMap | undefined):
|
|
113
|
+
function resolvePreset(materialName: string, map: MaterialPresetMap | undefined): ResolvedMaterialPreset {
|
|
106
114
|
if (map) {
|
|
107
115
|
for (const [preset, names] of Object.entries(map)) {
|
|
108
116
|
if (names && names.includes(materialName)) return preset as MaterialPreset
|
|
@@ -117,6 +125,33 @@ function resolvePreset(materialName: string, map: MaterialPresetMap | undefined)
|
|
|
117
125
|
return "mmd_classic"
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
// Map a WGSL compile-error line back to the graph node whose `let` produced it —
|
|
129
|
+
// the compiler tags every generated line with a trailing `// @node:<id>` marker.
|
|
130
|
+
function nodeIdForWgslLine(wgsl: string, lineNum: number): string | undefined {
|
|
131
|
+
const lines = wgsl.split("\n")
|
|
132
|
+
for (let i = Math.min(lineNum - 1, lines.length - 1); i >= 0; i--) {
|
|
133
|
+
const m = lines[i].match(/\/\/ @node:([a-z0-9_]+)/)
|
|
134
|
+
if (m) return m[1]
|
|
135
|
+
}
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// An applied style graph on a preset slot: the swapped pipeline(s) plus the
|
|
140
|
+
// slider→UBO map that setStyleParam consults for instant uniform writes.
|
|
141
|
+
type StyleOverride = {
|
|
142
|
+
pipeline: GPURenderPipeline
|
|
143
|
+
/** Hair slot only: the stencil-matched IS_OVER_EYES=true variant. */
|
|
144
|
+
overEyesPipeline?: GPURenderPipeline
|
|
145
|
+
slotMap: StyleSlot[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Result of Engine.applyStyleGraph — compile/pipeline diagnostics + slider slot map. */
|
|
149
|
+
export type ApplyStyleResult = {
|
|
150
|
+
ok: boolean
|
|
151
|
+
diagnostics: Diagnostic[]
|
|
152
|
+
slotMap: StyleSlot[]
|
|
153
|
+
}
|
|
154
|
+
|
|
120
155
|
export type RaycastCallback = (
|
|
121
156
|
modelName: string,
|
|
122
157
|
material: string | null,
|
|
@@ -259,7 +294,10 @@ interface DrawCall {
|
|
|
259
294
|
firstIndex: number
|
|
260
295
|
bindGroup: GPUBindGroup
|
|
261
296
|
materialName: string
|
|
262
|
-
preset:
|
|
297
|
+
preset: ResolvedMaterialPreset
|
|
298
|
+
// Bindings 0–3 kept so the bind group can be rebuilt when setMaterialPresets moves
|
|
299
|
+
// this material to a different slot (binding 4 must follow the slot's style buffer).
|
|
300
|
+
baseBindGroupEntries?: GPUBindGroupEntry[]
|
|
263
301
|
}
|
|
264
302
|
|
|
265
303
|
interface PickDrawCall {
|
|
@@ -327,6 +365,7 @@ export class Engine {
|
|
|
327
365
|
private lightData = new Float32Array(64)
|
|
328
366
|
private lightCount = 0
|
|
329
367
|
private resizeObserver: ResizeObserver | null = null
|
|
368
|
+
private resizePending = false
|
|
330
369
|
private depthTexture!: GPUTexture
|
|
331
370
|
private modelPipeline!: GPURenderPipeline
|
|
332
371
|
private facePipeline!: GPURenderPipeline
|
|
@@ -339,6 +378,21 @@ export class Engine {
|
|
|
339
378
|
private hairOverEyesPipeline!: GPURenderPipeline
|
|
340
379
|
private stockingsPipeline!: GPURenderPipeline
|
|
341
380
|
private mmdClassicPipeline!: GPURenderPipeline
|
|
381
|
+
// ── Style graph runtime (graph/ compiler output applied to preset slots) ──
|
|
382
|
+
// Per-slot 256 B StyleUniforms buffers (group(2) binding(4)). Created eagerly so
|
|
383
|
+
// material bind groups can reference them at model-load time regardless of whether
|
|
384
|
+
// a graph is ever applied; mmd_classic materials bind the shared zero buffer.
|
|
385
|
+
private styleBuffers = new Map<MaterialPreset, GPUBuffer>()
|
|
386
|
+
private zeroStyleBuffer!: GPUBuffer
|
|
387
|
+
// Applied graphs: slot → swapped pipeline(s) + the slider→UBO map for setStyleParam.
|
|
388
|
+
private styleOverrides = new Map<MaterialPreset, StyleOverride>()
|
|
389
|
+
// Per-slot generation counter — an async compile that finishes after a newer
|
|
390
|
+
// applyStyleGraph/resetStyleSlot call on the same slot is discarded (stale guard).
|
|
391
|
+
private styleGenerations = new Map<MaterialPreset, number>()
|
|
392
|
+
// Stashed at createPipelines so applyStyleGraph can rebuild slot pipelines later.
|
|
393
|
+
private mainPipelineLayout!: GPUPipelineLayout
|
|
394
|
+
private sceneTargets!: GPUColorTargetState[]
|
|
395
|
+
private fullVertexBufferLayouts!: GPUVertexBufferLayout[]
|
|
342
396
|
// 1×64 vertical ramp for shared-toon materials: lit (top) → soft shadow
|
|
343
397
|
// tone (bottom). Stand-in for MMD's toon01–10.bmp, which we can't ship.
|
|
344
398
|
private defaultToonRampTexture!: GPUTexture
|
|
@@ -1049,6 +1103,8 @@ export class Engine {
|
|
|
1049
1103
|
},
|
|
1050
1104
|
}
|
|
1051
1105
|
const sceneTargets: GPUColorTargetState[] = [standardBlend, maskBlend]
|
|
1106
|
+
this.sceneTargets = sceneTargets
|
|
1107
|
+
this.fullVertexBufferLayouts = fullVertexBuffers
|
|
1052
1108
|
|
|
1053
1109
|
const shaderModule = this.device.createShaderModule({
|
|
1054
1110
|
label: "default model shader",
|
|
@@ -1135,9 +1191,40 @@ export class Engine {
|
|
|
1135
1191
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1136
1192
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1137
1193
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1194
|
+
// StyleUniforms for compiled graph shaders (adjust-tier sliders). Hand-written
|
|
1195
|
+
// presets simply don't declare it — a layout may carry bindings a shader ignores.
|
|
1196
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1138
1197
|
],
|
|
1139
1198
|
})
|
|
1140
1199
|
|
|
1200
|
+
// Per-slot style buffers + shared zero buffer (mmd_classic / never-styled slots).
|
|
1201
|
+
this.zeroStyleBuffer = this.device.createBuffer({
|
|
1202
|
+
label: "style uniforms (zero)",
|
|
1203
|
+
size: 256,
|
|
1204
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1205
|
+
})
|
|
1206
|
+
const ALL_PRESETS: MaterialPreset[] = [
|
|
1207
|
+
"default",
|
|
1208
|
+
"face",
|
|
1209
|
+
"hair",
|
|
1210
|
+
"body",
|
|
1211
|
+
"eye",
|
|
1212
|
+
"stockings",
|
|
1213
|
+
"metal",
|
|
1214
|
+
"cloth_smooth",
|
|
1215
|
+
"cloth_rough",
|
|
1216
|
+
]
|
|
1217
|
+
for (const preset of ALL_PRESETS) {
|
|
1218
|
+
this.styleBuffers.set(
|
|
1219
|
+
preset,
|
|
1220
|
+
this.device.createBuffer({
|
|
1221
|
+
label: `style uniforms (${preset})`,
|
|
1222
|
+
size: 256,
|
|
1223
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1224
|
+
}),
|
|
1225
|
+
)
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1141
1228
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
1142
1229
|
label: "main pipeline layout",
|
|
1143
1230
|
bindGroupLayouts: [
|
|
@@ -1146,6 +1233,7 @@ export class Engine {
|
|
|
1146
1233
|
this.mainPerMaterialBindGroupLayout,
|
|
1147
1234
|
],
|
|
1148
1235
|
})
|
|
1236
|
+
this.mainPipelineLayout = mainPipelineLayout
|
|
1149
1237
|
|
|
1150
1238
|
// perFrameBindGroup is created after shadow resources below
|
|
1151
1239
|
|
|
@@ -1794,9 +1882,16 @@ export class Engine {
|
|
|
1794
1882
|
})
|
|
1795
1883
|
}
|
|
1796
1884
|
|
|
1797
|
-
// Step 3: Setup canvas resize handling
|
|
1885
|
+
// Step 3: Setup canvas resize handling.
|
|
1886
|
+
// The observer only flags the resize; render() applies it at the top of the next
|
|
1887
|
+
// frame. Resizing inside the RO callback (post-layout) clears the canvas buffer
|
|
1888
|
+
// after the frame's rAF draw already ran, so during continuous drags (resizable
|
|
1889
|
+
// panels) every paint showed a stale-aspect or cleared buffer — one frame behind,
|
|
1890
|
+
// reading as laggy/stretchy. Flag-and-apply keeps resize + redraw in one frame.
|
|
1798
1891
|
private setupResize() {
|
|
1799
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
1892
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
1893
|
+
this.resizePending = true
|
|
1894
|
+
})
|
|
1800
1895
|
this.resizeObserver.observe(this.canvas)
|
|
1801
1896
|
this.handleResize()
|
|
1802
1897
|
|
|
@@ -2470,6 +2565,13 @@ export class Engine {
|
|
|
2470
2565
|
this.resizeObserver.disconnect()
|
|
2471
2566
|
this.resizeObserver = null
|
|
2472
2567
|
}
|
|
2568
|
+
|
|
2569
|
+
// Style graph runtime: per-slot uniform buffers + swapped pipelines (pipelines
|
|
2570
|
+
// are GC'd; buffers need explicit destroy).
|
|
2571
|
+
this.styleOverrides.clear()
|
|
2572
|
+
for (const buf of this.styleBuffers.values()) buf.destroy()
|
|
2573
|
+
this.styleBuffers.clear()
|
|
2574
|
+
this.zeroStyleBuffer?.destroy()
|
|
2473
2575
|
}
|
|
2474
2576
|
|
|
2475
2577
|
async loadModel(path: string): Promise<Model>
|
|
@@ -2576,10 +2678,28 @@ export class Engine {
|
|
|
2576
2678
|
if (!inst) return
|
|
2577
2679
|
inst.materialPresets = presets
|
|
2578
2680
|
for (const dc of inst.drawCalls) {
|
|
2579
|
-
|
|
2681
|
+
const preset = resolvePreset(dc.materialName, presets)
|
|
2682
|
+
if (preset === dc.preset) continue
|
|
2683
|
+
dc.preset = preset
|
|
2684
|
+
// Rebind so binding(4) follows the new slot's style buffer.
|
|
2685
|
+
if (dc.baseBindGroupEntries)
|
|
2686
|
+
dc.bindGroup = this.createMaterialBindGroup(`material: ${dc.materialName}`, dc.baseBindGroupEntries, preset)
|
|
2580
2687
|
}
|
|
2581
2688
|
}
|
|
2582
2689
|
|
|
2690
|
+
private createMaterialBindGroup(
|
|
2691
|
+
label: string,
|
|
2692
|
+
baseEntries: GPUBindGroupEntry[],
|
|
2693
|
+
preset: ResolvedMaterialPreset,
|
|
2694
|
+
): GPUBindGroup {
|
|
2695
|
+
const styleBuffer = preset === "mmd_classic" ? this.zeroStyleBuffer : this.styleBuffers.get(preset)!
|
|
2696
|
+
return this.device.createBindGroup({
|
|
2697
|
+
label,
|
|
2698
|
+
layout: this.mainPerMaterialBindGroupLayout,
|
|
2699
|
+
entries: [...baseEntries, { binding: 4, resource: { buffer: styleBuffer } }],
|
|
2700
|
+
})
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2583
2703
|
setMaterialVisible(modelName: string, materialName: string, visible: boolean): void {
|
|
2584
2704
|
const inst = this.modelInstances.get(modelName)
|
|
2585
2705
|
if (!inst) return
|
|
@@ -3100,20 +3220,17 @@ export class Engine {
|
|
|
3100
3220
|
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, mat, sphereMode, headBoneIndex)
|
|
3101
3221
|
inst.gpuBuffers.push(materialUniformBuffer)
|
|
3102
3222
|
|
|
3223
|
+
const preset = resolvePreset(mat.name, inst.materialPresets)
|
|
3103
3224
|
const textureView = diffuseTexture.createView()
|
|
3104
|
-
const
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
{ binding: 3, resource: (sphereTexture ?? this.fallbackMaterialTexture).createView() },
|
|
3112
|
-
],
|
|
3113
|
-
})
|
|
3225
|
+
const baseBindGroupEntries: GPUBindGroupEntry[] = [
|
|
3226
|
+
{ binding: 0, resource: textureView },
|
|
3227
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
3228
|
+
{ binding: 2, resource: (toonTexture ?? this.fallbackMaterialTexture).createView() },
|
|
3229
|
+
{ binding: 3, resource: (sphereTexture ?? this.fallbackMaterialTexture).createView() },
|
|
3230
|
+
]
|
|
3231
|
+
const bindGroup = this.createMaterialBindGroup(`${prefix}material: ${mat.name}`, baseBindGroupEntries, preset)
|
|
3114
3232
|
|
|
3115
3233
|
const type: DrawCallType = isTransparent ? "transparent" : "opaque"
|
|
3116
|
-
const preset = resolvePreset(mat.name, inst.materialPresets)
|
|
3117
3234
|
inst.drawCalls.push({
|
|
3118
3235
|
type,
|
|
3119
3236
|
count: indexCount,
|
|
@@ -3121,6 +3238,7 @@ export class Engine {
|
|
|
3121
3238
|
bindGroup,
|
|
3122
3239
|
materialName: mat.name,
|
|
3123
3240
|
preset,
|
|
3241
|
+
baseBindGroupEntries,
|
|
3124
3242
|
})
|
|
3125
3243
|
|
|
3126
3244
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
@@ -3180,7 +3298,7 @@ export class Engine {
|
|
|
3180
3298
|
"transparent-outline": 3,
|
|
3181
3299
|
ground: 4,
|
|
3182
3300
|
}
|
|
3183
|
-
const presetRank = (p:
|
|
3301
|
+
const presetRank = (p: ResolvedMaterialPreset): number => (p === "hair" ? 2 : p === "eye" ? 1 : 0)
|
|
3184
3302
|
inst.drawCalls.sort((a, b) => {
|
|
3185
3303
|
const ta = typeOrder[a.type] - typeOrder[b.type]
|
|
3186
3304
|
if (ta !== 0) return ta
|
|
@@ -3889,6 +4007,11 @@ export class Engine {
|
|
|
3889
4007
|
render() {
|
|
3890
4008
|
if (!this.multisampleTexture || !this.camera || !this.device) return
|
|
3891
4009
|
|
|
4010
|
+
if (this.resizePending) {
|
|
4011
|
+
this.resizePending = false
|
|
4012
|
+
this.handleResize()
|
|
4013
|
+
}
|
|
4014
|
+
|
|
3892
4015
|
const currentTime = performance.now()
|
|
3893
4016
|
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
3894
4017
|
this.lastFrameTime = currentTime
|
|
@@ -4021,7 +4144,165 @@ export class Engine {
|
|
|
4021
4144
|
}
|
|
4022
4145
|
}
|
|
4023
4146
|
|
|
4024
|
-
|
|
4147
|
+
// ─── Style graph API ──────────────────────────────────────────────
|
|
4148
|
+
// Two-tier edit model: applyStyleGraph = topology tier (async compile + pipeline
|
|
4149
|
+
// swap, previous look keeps rendering until the new pipeline is ready or on any
|
|
4150
|
+
// error); setStyleParam = adjust tier (uniform write, instant, no pipeline touch).
|
|
4151
|
+
|
|
4152
|
+
/**
|
|
4153
|
+
* Compile a style graph and swap it onto its preset slot. Fallback-on-error: the
|
|
4154
|
+
* slot's current pipeline keeps rendering unless compilation fully succeeds.
|
|
4155
|
+
* Slider defaults are written to the slot's StyleUniforms buffer on success.
|
|
4156
|
+
*/
|
|
4157
|
+
async applyStyleGraph(graph: StyleGraph, opts?: CompileOptions): Promise<ApplyStyleResult> {
|
|
4158
|
+
const result = compileGraph(graph, opts)
|
|
4159
|
+
if (!result.ok) return { ok: false, diagnostics: result.diagnostics, slotMap: result.slotMap }
|
|
4160
|
+
|
|
4161
|
+
const slot = graph.slot
|
|
4162
|
+
const generation = (this.styleGenerations.get(slot) ?? 0) + 1
|
|
4163
|
+
this.styleGenerations.set(slot, generation)
|
|
4164
|
+
|
|
4165
|
+
this.device.pushErrorScope("validation")
|
|
4166
|
+
const module = this.device.createShaderModule({ label: `style graph: ${graph.name} (${slot})`, code: result.wgsl })
|
|
4167
|
+
const info = await module.getCompilationInfo()
|
|
4168
|
+
const scopeError = await this.device.popErrorScope()
|
|
4169
|
+
const diagnostics = [...result.diagnostics]
|
|
4170
|
+
for (const msg of info.messages) {
|
|
4171
|
+
if (msg.type !== "error") continue
|
|
4172
|
+
diagnostics.push({
|
|
4173
|
+
severity: "error",
|
|
4174
|
+
nodeId: nodeIdForWgslLine(result.wgsl, msg.lineNum),
|
|
4175
|
+
message: `WGSL: ${msg.message}`,
|
|
4176
|
+
})
|
|
4177
|
+
}
|
|
4178
|
+
if (diagnostics.some((d) => d.severity === "error") || scopeError) {
|
|
4179
|
+
if (scopeError && !diagnostics.some((d) => d.severity === "error"))
|
|
4180
|
+
diagnostics.push({ severity: "error", message: `WGSL: ${scopeError.message}` })
|
|
4181
|
+
return { ok: false, diagnostics, slotMap: result.slotMap }
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
let pipeline: GPURenderPipeline
|
|
4185
|
+
let overEyesPipeline: GPURenderPipeline | undefined
|
|
4186
|
+
try {
|
|
4187
|
+
pipeline = await this.createSlotPipeline(slot, module, false)
|
|
4188
|
+
if (slot === "hair") overEyesPipeline = await this.createSlotPipeline(slot, module, true)
|
|
4189
|
+
} catch (e) {
|
|
4190
|
+
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${(e as Error).message}` })
|
|
4191
|
+
return { ok: false, diagnostics, slotMap: result.slotMap }
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
// A newer apply/reset on this slot finished (or started) while we compiled.
|
|
4195
|
+
if (this.styleGenerations.get(slot) !== generation) {
|
|
4196
|
+
diagnostics.push({ severity: "warning", message: "superseded by a newer edit — result discarded" })
|
|
4197
|
+
return { ok: false, diagnostics, slotMap: result.slotMap }
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
this.styleOverrides.set(slot, { pipeline, overEyesPipeline, slotMap: result.slotMap })
|
|
4201
|
+
this.writeStyleDefaults(slot, graph, result.slotMap)
|
|
4202
|
+
return { ok: true, diagnostics, slotMap: result.slotMap }
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
/** Instant adjust-tier write: set one exposed slider on the slot's applied graph. */
|
|
4206
|
+
setStyleParam(slot: MaterialPreset, paramId: string, value: number | [number, number, number]): boolean {
|
|
4207
|
+
const override = this.styleOverrides.get(slot)
|
|
4208
|
+
const styleSlot = override?.slotMap.find((s) => s.id === paramId)
|
|
4209
|
+
const buffer = this.styleBuffers.get(slot)
|
|
4210
|
+
if (!styleSlot || !buffer) return false
|
|
4211
|
+
if (styleSlot.kind === "float") {
|
|
4212
|
+
if (typeof value !== "number") return false
|
|
4213
|
+
const offset = styleSlot.vec4Index * 16 + ["x", "y", "z", "w"].indexOf(styleSlot.component!) * 4
|
|
4214
|
+
this.device.queue.writeBuffer(buffer, offset, new Float32Array([value]))
|
|
4215
|
+
} else {
|
|
4216
|
+
if (typeof value === "number") return false
|
|
4217
|
+
this.device.queue.writeBuffer(buffer, styleSlot.vec4Index * 16, new Float32Array(value))
|
|
4218
|
+
}
|
|
4219
|
+
return true
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
/** Remove an applied style graph — the slot returns to its built-in preset shader. */
|
|
4223
|
+
resetStyleSlot(slot: MaterialPreset): void {
|
|
4224
|
+
this.styleGenerations.set(slot, (this.styleGenerations.get(slot) ?? 0) + 1)
|
|
4225
|
+
this.styleOverrides.delete(slot)
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
private writeStyleDefaults(slot: MaterialPreset, graph: StyleGraph, slotMap: StyleSlot[]): void {
|
|
4229
|
+
const data = new Float32Array(64) // 16 vec4f
|
|
4230
|
+
for (const styleSlot of slotMap) {
|
|
4231
|
+
const param = graph.params?.find((p) => p.id === styleSlot.id)
|
|
4232
|
+
if (!param) continue
|
|
4233
|
+
const base = styleSlot.vec4Index * 4
|
|
4234
|
+
if (styleSlot.kind === "float" && typeof param.default === "number") {
|
|
4235
|
+
data[base + ["x", "y", "z", "w"].indexOf(styleSlot.component!)] = param.default
|
|
4236
|
+
} else if (styleSlot.kind === "color" && typeof param.default !== "number") {
|
|
4237
|
+
data.set(param.default.slice(0, 3), base)
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
this.device.queue.writeBuffer(this.styleBuffers.get(slot)!, 0, data)
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
/**
|
|
4244
|
+
* Slot pipeline states mirror createPipelines exactly — a compiled graph swaps the
|
|
4245
|
+
* fragment shading of a slot, never its pass integration (stencil interplay, depth
|
|
4246
|
+
* bias, cull mode stay slot-owned; see docs/graph-compiler-spec.md §6).
|
|
4247
|
+
*/
|
|
4248
|
+
private createSlotPipeline(slot: MaterialPreset, module: GPUShaderModule, overEyes: boolean): Promise<GPURenderPipeline> {
|
|
4249
|
+
const base = {
|
|
4250
|
+
label: `style ${slot}${overEyes ? " (over eyes)" : ""}`,
|
|
4251
|
+
layout: this.mainPipelineLayout,
|
|
4252
|
+
vertex: { module, buffers: this.fullVertexBufferLayouts },
|
|
4253
|
+
primitive: { cullMode: (slot === "eye" ? "front" : "none") as GPUCullMode },
|
|
4254
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4255
|
+
}
|
|
4256
|
+
const plainDepth: GPUDepthStencilState = {
|
|
4257
|
+
format: "depth24plus-stencil8",
|
|
4258
|
+
depthWriteEnabled: true,
|
|
4259
|
+
depthCompare: "less-equal",
|
|
4260
|
+
}
|
|
4261
|
+
let depthStencil: GPUDepthStencilState = plainDepth
|
|
4262
|
+
let constants: Record<string, number> | undefined
|
|
4263
|
+
if (slot === "hair" && !overEyes) {
|
|
4264
|
+
depthStencil = {
|
|
4265
|
+
...plainDepth,
|
|
4266
|
+
stencilFront: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
|
|
4267
|
+
stencilBack: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
|
|
4268
|
+
stencilReadMask: 0xff,
|
|
4269
|
+
stencilWriteMask: 0,
|
|
4270
|
+
}
|
|
4271
|
+
} else if (slot === "hair" && overEyes) {
|
|
4272
|
+
constants = { IS_OVER_EYES: 1 }
|
|
4273
|
+
depthStencil = {
|
|
4274
|
+
format: "depth24plus-stencil8",
|
|
4275
|
+
depthWriteEnabled: false,
|
|
4276
|
+
depthCompare: "less-equal",
|
|
4277
|
+
stencilFront: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
|
|
4278
|
+
stencilBack: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
|
|
4279
|
+
stencilReadMask: 0xff,
|
|
4280
|
+
stencilWriteMask: 0,
|
|
4281
|
+
}
|
|
4282
|
+
} else if (slot === "eye") {
|
|
4283
|
+
depthStencil = {
|
|
4284
|
+
...plainDepth,
|
|
4285
|
+
depthBias: -0.00005,
|
|
4286
|
+
depthBiasSlopeScale: 0.0,
|
|
4287
|
+
depthBiasClamp: 0.0,
|
|
4288
|
+
stencilFront: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
|
|
4289
|
+
stencilBack: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
|
|
4290
|
+
stencilReadMask: 0xff,
|
|
4291
|
+
stencilWriteMask: 0xff,
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
return this.device.createRenderPipelineAsync({
|
|
4295
|
+
...base,
|
|
4296
|
+
fragment: { module, constants, targets: this.sceneTargets },
|
|
4297
|
+
depthStencil,
|
|
4298
|
+
})
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
private pipelineForPreset(preset: ResolvedMaterialPreset): GPURenderPipeline {
|
|
4302
|
+
// Applied style graph wins over the built-in preset shader (mmd_classic is never
|
|
4303
|
+
// in the override map — it's not a stylable slot).
|
|
4304
|
+
const override = this.styleOverrides.get(preset as MaterialPreset)
|
|
4305
|
+
if (override) return override.pipeline
|
|
4025
4306
|
if (preset === "face") return this.facePipeline
|
|
4026
4307
|
if (preset === "hair") return this.hairPipeline
|
|
4027
4308
|
if (preset === "cloth_smooth") return this.clothSmoothPipeline
|
|
@@ -4115,7 +4396,7 @@ export class Engine {
|
|
|
4115
4396
|
for (const draw of inst.drawCalls) {
|
|
4116
4397
|
if (draw.type !== "opaque" || draw.preset !== "hair" || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
4117
4398
|
if (!bound) {
|
|
4118
|
-
pass.setPipeline(this.hairOverEyesPipeline)
|
|
4399
|
+
pass.setPipeline(this.styleOverrides.get("hair")?.overEyesPipeline ?? this.hairOverEyesPipeline)
|
|
4119
4400
|
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
4120
4401
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
4121
4402
|
bound = true
|