reze-engine 0.18.1 → 0.19.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 +45 -28
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +339 -144
- package/dist/graph/compile.d.ts +6 -0
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +1 -1
- package/dist/graph/presets/body.js +1 -1
- package/dist/graph/presets/cloth_rough.js +1 -1
- package/dist/graph/presets/cloth_smooth.js +1 -1
- package/dist/graph/presets/default.js +1 -1
- package/dist/graph/presets/eye.js +1 -1
- package/dist/graph/presets/face.js +1 -1
- package/dist/graph/presets/hair.js +1 -1
- package/dist/graph/presets/metal.js +1 -1
- package/dist/graph/presets/stockings.js +1 -1
- package/dist/graph/render-class.d.ts +16 -0
- package/dist/graph/render-class.d.ts.map +1 -0
- package/dist/graph/render-class.js +15 -0
- package/dist/graph/schema.d.ts +4 -5
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts +7 -12
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +68 -100
- package/dist/graph/style-group.d.ts +38 -0
- package/dist/graph/style-group.d.ts.map +1 -0
- package/dist/graph/style-group.js +6 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/engine.ts +395 -156
- package/src/graph/compile.ts +7 -1
- package/src/graph/presets/body.ts +1 -1
- package/src/graph/presets/cloth_rough.ts +1 -1
- package/src/graph/presets/cloth_smooth.ts +1 -1
- package/src/graph/presets/default.ts +1 -1
- package/src/graph/presets/eye.ts +1 -1
- package/src/graph/presets/face.ts +1 -1
- package/src/graph/presets/hair.ts +1 -1
- package/src/graph/presets/metal.ts +1 -1
- package/src/graph/presets/stockings.ts +1 -1
- package/src/graph/render-class.ts +33 -0
- package/src/graph/schema.ts +4 -6
- package/src/graph/slots.ts +76 -113
- package/src/graph/style-group.ts +39 -0
- package/src/index.ts +7 -1
package/src/engine.ts
CHANGED
|
@@ -41,6 +41,22 @@ import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
|
|
|
41
41
|
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
|
|
42
42
|
import { compileGraph, type CompileOptions, type StyleSlot } from "./graph/compile"
|
|
43
43
|
import type { Diagnostic, StyleGraph } from "./graph/schema"
|
|
44
|
+
import type { AlphaMode, RenderClass } from "./graph/render-class"
|
|
45
|
+
import type {
|
|
46
|
+
ApplyStyleGroupResult,
|
|
47
|
+
ApplyStyleGroupsResult,
|
|
48
|
+
GroupDiagnostic,
|
|
49
|
+
StyleGroup,
|
|
50
|
+
} from "./graph/style-group"
|
|
51
|
+
import { DEFAULT_GRAPH } from "./graph/presets/default"
|
|
52
|
+
import { FACE_GRAPH } from "./graph/presets/face"
|
|
53
|
+
import { HAIR_GRAPH } from "./graph/presets/hair"
|
|
54
|
+
import { BODY_GRAPH } from "./graph/presets/body"
|
|
55
|
+
import { EYE_GRAPH } from "./graph/presets/eye"
|
|
56
|
+
import { STOCKINGS_GRAPH } from "./graph/presets/stockings"
|
|
57
|
+
import { METAL_GRAPH } from "./graph/presets/metal"
|
|
58
|
+
import { CLOTH_SMOOTH_GRAPH } from "./graph/presets/cloth_smooth"
|
|
59
|
+
import { CLOTH_ROUGH_GRAPH } from "./graph/presets/cloth_rough"
|
|
44
60
|
|
|
45
61
|
// Material preset dispatch. Consumers supply a MaterialPresetMap assigning material names
|
|
46
62
|
// to presets; unmapped materials fall back to "default" (Principled BSDF).
|
|
@@ -125,6 +141,22 @@ function resolvePreset(materialName: string, map: MaterialPresetMap | undefined)
|
|
|
125
141
|
return "mmd_classic"
|
|
126
142
|
}
|
|
127
143
|
|
|
144
|
+
// Default-group recipe per preset label: the shipped graph + its natural pass-integration
|
|
145
|
+
// (renderClass, alphaMode). This is the auto-default-groups mapping (docs §8) — the same
|
|
146
|
+
// label→integration knowledge the old fixed slots encoded, now producing editable groups.
|
|
147
|
+
// mmd_classic has no entry — those materials stay ungrouped (hand-shader path).
|
|
148
|
+
const PRESET_GROUP_INFO: Partial<Record<MaterialPreset, { graph: StyleGraph; renderClass: RenderClass; alphaMode: AlphaMode }>> = {
|
|
149
|
+
default: { graph: DEFAULT_GRAPH, renderClass: "auto", alphaMode: "opaque" },
|
|
150
|
+
face: { graph: FACE_GRAPH, renderClass: "auto", alphaMode: "opaque" },
|
|
151
|
+
hair: { graph: HAIR_GRAPH, renderClass: "hair", alphaMode: "opaque" },
|
|
152
|
+
body: { graph: BODY_GRAPH, renderClass: "auto", alphaMode: "opaque" },
|
|
153
|
+
eye: { graph: EYE_GRAPH, renderClass: "eye", alphaMode: "opaque" },
|
|
154
|
+
stockings: { graph: STOCKINGS_GRAPH, renderClass: "auto", alphaMode: "hashed" },
|
|
155
|
+
metal: { graph: METAL_GRAPH, renderClass: "auto", alphaMode: "opaque" },
|
|
156
|
+
cloth_smooth: { graph: CLOTH_SMOOTH_GRAPH, renderClass: "auto", alphaMode: "opaque" },
|
|
157
|
+
cloth_rough: { graph: CLOTH_ROUGH_GRAPH, renderClass: "auto", alphaMode: "opaque" },
|
|
158
|
+
}
|
|
159
|
+
|
|
128
160
|
// Map a WGSL compile-error line back to the graph node whose `let` produced it —
|
|
129
161
|
// the compiler tags every generated line with a trailing `// @node:<id>` marker.
|
|
130
162
|
function nodeIdForWgslLine(wgsl: string, lineNum: number): string | undefined {
|
|
@@ -136,20 +168,21 @@ function nodeIdForWgslLine(wgsl: string, lineNum: number): string | undefined {
|
|
|
136
168
|
return undefined
|
|
137
169
|
}
|
|
138
170
|
|
|
139
|
-
//
|
|
140
|
-
// slider→UBO map
|
|
141
|
-
|
|
171
|
+
// A compiled + installed style group on a model: the swapped pipeline(s), the group's
|
|
172
|
+
// StyleUniforms buffer, the slider→UBO map setStyleParam consults, and the resolved
|
|
173
|
+
// render-class (draw-order + over-eyes participation). Keyed per-model by group id.
|
|
174
|
+
type GroupInstall = {
|
|
175
|
+
group: StyleGroup
|
|
176
|
+
renderClass: RenderClass
|
|
177
|
+
alphaMode: AlphaMode
|
|
142
178
|
pipeline: GPURenderPipeline
|
|
143
|
-
/**
|
|
179
|
+
/** hair render-class only: the stencil-matched IS_OVER_EYES=true variant. */
|
|
144
180
|
overEyesPipeline?: GPURenderPipeline
|
|
181
|
+
uniformBuffer: GPUBuffer
|
|
145
182
|
slotMap: StyleSlot[]
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
export type ApplyStyleResult = {
|
|
150
|
-
ok: boolean
|
|
151
|
-
diagnostics: Diagnostic[]
|
|
152
|
-
slotMap: StyleSlot[]
|
|
183
|
+
/** Serialized (graph + renderClass + alphaMode) — lets applyStyleGroups skip recompiling
|
|
184
|
+
* an unchanged group. */
|
|
185
|
+
signature: string
|
|
153
186
|
}
|
|
154
187
|
|
|
155
188
|
export type RaycastCallback = (
|
|
@@ -295,8 +328,13 @@ interface DrawCall {
|
|
|
295
328
|
bindGroup: GPUBindGroup
|
|
296
329
|
materialName: string
|
|
297
330
|
preset: ResolvedMaterialPreset
|
|
298
|
-
//
|
|
299
|
-
//
|
|
331
|
+
// Style group this material belongs to, or null (ungrouped → hand-shader path).
|
|
332
|
+
// Outline/ground draw calls are never grouped and leave this null.
|
|
333
|
+
groupId: string | null
|
|
334
|
+
// Bindings 0–3 kept so the bind group can be rebuilt when the material's group changes
|
|
335
|
+
// (binding 4 must follow the group's style buffer, or the zero buffer when ungrouped).
|
|
336
|
+
// Present only for material draw calls (opaque/transparent) — the grouping walk skips
|
|
337
|
+
// any draw call without it.
|
|
300
338
|
baseBindGroupEntries?: GPUBindGroupEntry[]
|
|
301
339
|
}
|
|
302
340
|
|
|
@@ -329,6 +367,13 @@ interface ModelInstance {
|
|
|
329
367
|
physics: RezePhysics | null
|
|
330
368
|
vertexBufferNeedsUpdate: boolean
|
|
331
369
|
gpuMorph: GpuMorph | null
|
|
370
|
+
// Style groups applied to this model: group id → compiled install.
|
|
371
|
+
styleGroups: Map<string, GroupInstall>
|
|
372
|
+
// Material name → group id (each material in ≤1 group). Drives draw-call assignment.
|
|
373
|
+
materialToGroup: Map<string, string>
|
|
374
|
+
// Per-group compile generation — an async compile finishing after a newer edit/remove
|
|
375
|
+
// on the same id is discarded (stale-write guard).
|
|
376
|
+
styleGroupGen: Map<string, number>
|
|
332
377
|
}
|
|
333
378
|
|
|
334
379
|
// Per-model GPU vertex-morph compute state. Present only for models with vertex morphs.
|
|
@@ -378,18 +423,12 @@ export class Engine {
|
|
|
378
423
|
private hairOverEyesPipeline!: GPURenderPipeline
|
|
379
424
|
private stockingsPipeline!: GPURenderPipeline
|
|
380
425
|
private mmdClassicPipeline!: GPURenderPipeline
|
|
381
|
-
// ── Style
|
|
382
|
-
//
|
|
383
|
-
// material
|
|
384
|
-
//
|
|
385
|
-
private styleBuffers = new Map<MaterialPreset, GPUBuffer>()
|
|
426
|
+
// ── Style group runtime ──
|
|
427
|
+
// Shared 256 B zero StyleUniforms buffer (group(2) binding(4)) bound by every ungrouped
|
|
428
|
+
// material; grouped materials rebind to their group's own buffer (per-model, in the
|
|
429
|
+
// ModelInstance's styleGroups map). See docs/style-groups-spec.md §6.
|
|
386
430
|
private zeroStyleBuffer!: GPUBuffer
|
|
387
|
-
//
|
|
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.
|
|
431
|
+
// Stashed at createPipelines so group pipelines can be compiled later.
|
|
393
432
|
private mainPipelineLayout!: GPUPipelineLayout
|
|
394
433
|
private sceneTargets!: GPUColorTargetState[]
|
|
395
434
|
private fullVertexBufferLayouts!: GPUVertexBufferLayout[]
|
|
@@ -1197,33 +1236,13 @@ export class Engine {
|
|
|
1197
1236
|
],
|
|
1198
1237
|
})
|
|
1199
1238
|
|
|
1200
|
-
//
|
|
1239
|
+
// Shared zero StyleUniforms buffer — bound by every ungrouped material; grouped
|
|
1240
|
+
// materials rebind binding(4) to their group's own buffer (per model, per group).
|
|
1201
1241
|
this.zeroStyleBuffer = this.device.createBuffer({
|
|
1202
1242
|
label: "style uniforms (zero)",
|
|
1203
1243
|
size: 256,
|
|
1204
1244
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1205
1245
|
})
|
|
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
1246
|
|
|
1228
1247
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
1229
1248
|
label: "main pipeline layout",
|
|
@@ -2509,6 +2528,7 @@ export class Engine {
|
|
|
2509
2528
|
bindGroup: this.groundShadowBindGroup!,
|
|
2510
2529
|
materialName: "Ground",
|
|
2511
2530
|
preset: "cloth_rough",
|
|
2531
|
+
groupId: null,
|
|
2512
2532
|
}
|
|
2513
2533
|
}
|
|
2514
2534
|
|
|
@@ -2566,11 +2586,13 @@ export class Engine {
|
|
|
2566
2586
|
this.resizeObserver = null
|
|
2567
2587
|
}
|
|
2568
2588
|
|
|
2569
|
-
// Style
|
|
2570
|
-
//
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2589
|
+
// Style group runtime: per-group uniform buffers (pipelines are GC'd; buffers need
|
|
2590
|
+
// explicit destroy). Per-model group buffers are torn down in removeModel; the shared
|
|
2591
|
+
// zero buffer is engine-owned.
|
|
2592
|
+
this.forEachInstance((inst) => {
|
|
2593
|
+
for (const install of inst.styleGroups.values()) install.uniformBuffer.destroy()
|
|
2594
|
+
inst.styleGroups.clear()
|
|
2595
|
+
})
|
|
2574
2596
|
this.zeroStyleBuffer?.destroy()
|
|
2575
2597
|
}
|
|
2576
2598
|
|
|
@@ -2629,6 +2651,8 @@ export class Engine {
|
|
|
2629
2651
|
for (const buf of inst.gpuBuffers) {
|
|
2630
2652
|
buf.destroy()
|
|
2631
2653
|
}
|
|
2654
|
+
// Per-group StyleUniforms buffers aren't in gpuBuffers (allocated post-load).
|
|
2655
|
+
for (const install of inst.styleGroups.values()) install.uniformBuffer.destroy()
|
|
2632
2656
|
this.modelInstances.delete(name)
|
|
2633
2657
|
}
|
|
2634
2658
|
|
|
@@ -2677,22 +2701,15 @@ export class Engine {
|
|
|
2677
2701
|
const inst = this.modelInstances.get(modelName)
|
|
2678
2702
|
if (!inst) return
|
|
2679
2703
|
inst.materialPresets = presets
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
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)
|
|
2687
|
-
}
|
|
2704
|
+
// Only updates the ungrouped fallback preset; grouped materials render via their
|
|
2705
|
+
// group regardless. binding(4) is unaffected (ungrouped always binds the zero
|
|
2706
|
+
// buffer; grouped bindings are owned by applyStyleGroups).
|
|
2707
|
+
for (const dc of inst.drawCalls) dc.preset = resolvePreset(dc.materialName, presets)
|
|
2688
2708
|
}
|
|
2689
2709
|
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
preset: ResolvedMaterialPreset,
|
|
2694
|
-
): GPUBindGroup {
|
|
2695
|
-
const styleBuffer = preset === "mmd_classic" ? this.zeroStyleBuffer : this.styleBuffers.get(preset)!
|
|
2710
|
+
// Build a material's bind group with binding(4) pointing at a given StyleUniforms buffer
|
|
2711
|
+
// (the group's buffer when grouped, or the shared zero buffer when ungrouped).
|
|
2712
|
+
private createMaterialBindGroup(label: string, baseEntries: GPUBindGroupEntry[], styleBuffer: GPUBuffer): GPUBindGroup {
|
|
2696
2713
|
return this.device.createBindGroup({
|
|
2697
2714
|
label,
|
|
2698
2715
|
layout: this.mainPerMaterialBindGroupLayout,
|
|
@@ -2941,6 +2958,9 @@ export class Engine {
|
|
|
2941
2958
|
physics,
|
|
2942
2959
|
vertexBufferNeedsUpdate: false,
|
|
2943
2960
|
gpuMorph,
|
|
2961
|
+
styleGroups: new Map(),
|
|
2962
|
+
materialToGroup: new Map(),
|
|
2963
|
+
styleGroupGen: new Map(),
|
|
2944
2964
|
}
|
|
2945
2965
|
await this.setupMaterialsForInstance(inst)
|
|
2946
2966
|
this.modelInstances.set(name, inst)
|
|
@@ -3228,7 +3248,12 @@ export class Engine {
|
|
|
3228
3248
|
{ binding: 2, resource: (toonTexture ?? this.fallbackMaterialTexture).createView() },
|
|
3229
3249
|
{ binding: 3, resource: (sphereTexture ?? this.fallbackMaterialTexture).createView() },
|
|
3230
3250
|
]
|
|
3231
|
-
|
|
3251
|
+
// Ungrouped at load — binding(4) = zero buffer. applyStyleGroups rebinds grouped ones.
|
|
3252
|
+
const bindGroup = this.createMaterialBindGroup(
|
|
3253
|
+
`${prefix}material: ${mat.name}`,
|
|
3254
|
+
baseBindGroupEntries,
|
|
3255
|
+
this.zeroStyleBuffer,
|
|
3256
|
+
)
|
|
3232
3257
|
|
|
3233
3258
|
const type: DrawCallType = isTransparent ? "transparent" : "opaque"
|
|
3234
3259
|
inst.drawCalls.push({
|
|
@@ -3238,6 +3263,7 @@ export class Engine {
|
|
|
3238
3263
|
bindGroup,
|
|
3239
3264
|
materialName: mat.name,
|
|
3240
3265
|
preset,
|
|
3266
|
+
groupId: null,
|
|
3241
3267
|
baseBindGroupEntries,
|
|
3242
3268
|
})
|
|
3243
3269
|
|
|
@@ -3267,6 +3293,7 @@ export class Engine {
|
|
|
3267
3293
|
bindGroup: outlineBindGroup,
|
|
3268
3294
|
materialName: mat.name,
|
|
3269
3295
|
preset,
|
|
3296
|
+
groupId: null,
|
|
3270
3297
|
})
|
|
3271
3298
|
}
|
|
3272
3299
|
|
|
@@ -3285,29 +3312,14 @@ export class Engine {
|
|
|
3285
3312
|
currentIndexOffset += indexCount
|
|
3286
3313
|
}
|
|
3287
3314
|
|
|
3288
|
-
// Sort so the opaque bucket is emitted in the order the stencil-based
|
|
3289
|
-
//
|
|
3290
|
-
//
|
|
3291
|
-
//
|
|
3292
|
-
//
|
|
3293
|
-
//
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
"opaque-outline": 1,
|
|
3297
|
-
transparent: 2,
|
|
3298
|
-
"transparent-outline": 3,
|
|
3299
|
-
ground: 4,
|
|
3300
|
-
}
|
|
3301
|
-
const presetRank = (p: ResolvedMaterialPreset): number => (p === "hair" ? 2 : p === "eye" ? 1 : 0)
|
|
3302
|
-
inst.drawCalls.sort((a, b) => {
|
|
3303
|
-
const ta = typeOrder[a.type] - typeOrder[b.type]
|
|
3304
|
-
if (ta !== 0) return ta
|
|
3305
|
-
return presetRank(a.preset) - presetRank(b.preset)
|
|
3306
|
-
})
|
|
3307
|
-
|
|
3308
|
-
for (const d of inst.drawCalls) {
|
|
3309
|
-
if (d.type === "opaque") inst.shadowDrawCalls.push(d)
|
|
3310
|
-
}
|
|
3315
|
+
// Sort so the opaque bucket is emitted in the order the stencil-based see-through-hair
|
|
3316
|
+
// effect requires: {non-hair, non-eye} → {eye} → {hair}. Eye writes stencil=EYE_VALUE;
|
|
3317
|
+
// hair stencil-tests "not equal" and skips eye pixels; the follow-up hairOverEyes pass
|
|
3318
|
+
// re-fills them alpha-blended. sortDrawCalls also (re)builds shadowDrawCalls. All draws
|
|
3319
|
+
// are ungrouped at setup, so the rank comes from the preset; applyStyleGroups re-sorts
|
|
3320
|
+
// by render-class when groups are assigned. Array.sort is stable → PMX order preserved
|
|
3321
|
+
// within a bucket.
|
|
3322
|
+
this.sortDrawCalls(inst)
|
|
3311
3323
|
}
|
|
3312
3324
|
|
|
3313
3325
|
private createMaterialUniformBuffer(
|
|
@@ -4144,36 +4156,201 @@ export class Engine {
|
|
|
4144
4156
|
}
|
|
4145
4157
|
}
|
|
4146
4158
|
|
|
4147
|
-
// ─── Style
|
|
4148
|
-
// Two-tier
|
|
4149
|
-
//
|
|
4150
|
-
//
|
|
4159
|
+
// ─── Style group API ──────────────────────────────────────────────
|
|
4160
|
+
// Two-tier edits: applyStyleGroups/upsert = topology (async compile + pipeline swap,
|
|
4161
|
+
// fallback-on-error, per-group stale guard); setStyleParam = adjust (instant uniform
|
|
4162
|
+
// write). Overlay-first: grouped materials render via their group's compiled graph;
|
|
4163
|
+
// ungrouped ones keep the hand-written preset path. See docs/style-groups-spec.md.
|
|
4164
|
+
|
|
4165
|
+
/** Read a model's current style groups (including auto-created defaults) for editor
|
|
4166
|
+
* round-trip. The host owns group state; this is bootstrap/read, not a second store. */
|
|
4167
|
+
getStyleGroups(modelName: string): StyleGroup[] {
|
|
4168
|
+
const inst = this.modelInstances.get(modelName)
|
|
4169
|
+
if (!inst) return []
|
|
4170
|
+
return [...inst.styleGroups.values()].map((g) => g.group)
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
/**
|
|
4174
|
+
* Create default style groups from the model's material→preset resolution
|
|
4175
|
+
* (`setMaterialPresets` map first, then name hints), so a freshly loaded model comes up
|
|
4176
|
+
* editably grouped with zero interaction. Materials resolving to `mmd_classic` (no map
|
|
4177
|
+
* entry, no hint) stay ungrouped. The returned promise resolves after grouping AND the
|
|
4178
|
+
* async graph compiles, so `getStyleGroups` is populated the moment it resolves. Opt-in:
|
|
4179
|
+
* call it explicitly (typically after `setMaterialPresets`); a bare load stays ungrouped
|
|
4180
|
+
* / byte-identical to before.
|
|
4181
|
+
*/
|
|
4182
|
+
async autoStyleGroups(modelName: string): Promise<ApplyStyleGroupsResult> {
|
|
4183
|
+
const inst = this.modelInstances.get(modelName)
|
|
4184
|
+
if (!inst) return { ok: false, groups: [], unknownMaterials: [], conflicts: [] }
|
|
4185
|
+
|
|
4186
|
+
const buckets = new Map<MaterialPreset, string[]>()
|
|
4187
|
+
for (const dc of inst.drawCalls) {
|
|
4188
|
+
if (!dc.baseBindGroupEntries) continue // material draw calls only (skip outlines)
|
|
4189
|
+
const preset = resolvePreset(dc.materialName, inst.materialPresets)
|
|
4190
|
+
if (preset === "mmd_classic") continue // unmatched → ungrouped
|
|
4191
|
+
const arr = buckets.get(preset) ?? []
|
|
4192
|
+
if (!arr.includes(dc.materialName)) arr.push(dc.materialName)
|
|
4193
|
+
buckets.set(preset, arr)
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
const groups: StyleGroup[] = []
|
|
4197
|
+
for (const [preset, materials] of buckets) {
|
|
4198
|
+
const info = PRESET_GROUP_INFO[preset]
|
|
4199
|
+
if (!info) continue
|
|
4200
|
+
groups.push({
|
|
4201
|
+
id: preset,
|
|
4202
|
+
label: info.graph.name,
|
|
4203
|
+
materials,
|
|
4204
|
+
graph: info.graph,
|
|
4205
|
+
renderClass: info.renderClass,
|
|
4206
|
+
alphaMode: info.alphaMode,
|
|
4207
|
+
})
|
|
4208
|
+
}
|
|
4209
|
+
return this.applyStyleGroups(modelName, groups)
|
|
4210
|
+
}
|
|
4151
4211
|
|
|
4152
4212
|
/**
|
|
4153
|
-
*
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
4213
|
+
* Replace a model's full style-group set. Unchanged groups (same graph + renderClass +
|
|
4214
|
+
* alphaMode) keep their pipeline; new/changed ones compile and swap; removed ones are
|
|
4215
|
+
* torn down and their materials revert to the ungrouped hand-shader path. A group whose
|
|
4216
|
+
* compile fails is not installed and its materials stay ungrouped (fallback-on-error).
|
|
4156
4217
|
*/
|
|
4157
|
-
async
|
|
4158
|
-
const
|
|
4218
|
+
async applyStyleGroups(modelName: string, groups: StyleGroup[]): Promise<ApplyStyleGroupsResult> {
|
|
4219
|
+
const inst = this.modelInstances.get(modelName)
|
|
4220
|
+
if (!inst) return { ok: false, groups: [], unknownMaterials: [], conflicts: [] }
|
|
4221
|
+
|
|
4222
|
+
// Whole-set validation: material claims (last group in array order wins), unknowns.
|
|
4223
|
+
const modelMaterials = new Set(inst.drawCalls.map((d) => d.materialName))
|
|
4224
|
+
const claimed = new Map<string, string>()
|
|
4225
|
+
const conflicts = new Set<string>()
|
|
4226
|
+
const unknownMaterials = new Set<string>()
|
|
4227
|
+
for (const g of groups) {
|
|
4228
|
+
for (const m of g.materials) {
|
|
4229
|
+
if (!modelMaterials.has(m)) unknownMaterials.add(m)
|
|
4230
|
+
if (claimed.has(m)) conflicts.add(m)
|
|
4231
|
+
claimed.set(m, g.id)
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
// Tear down installs no longer present.
|
|
4236
|
+
const nextIds = new Set(groups.map((g) => g.id))
|
|
4237
|
+
for (const [id, install] of inst.styleGroups) {
|
|
4238
|
+
if (!nextIds.has(id)) {
|
|
4239
|
+
inst.styleGroupGen.set(id, (inst.styleGroupGen.get(id) ?? 0) + 1)
|
|
4240
|
+
install.uniformBuffer.destroy()
|
|
4241
|
+
inst.styleGroups.delete(id)
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
const groupResults: GroupDiagnostic[] = []
|
|
4246
|
+
for (const g of groups) {
|
|
4247
|
+
const r = await this.compileAndInstallGroup(inst, g)
|
|
4248
|
+
groupResults.push({ groupId: g.id, diagnostics: r.diagnostics, ok: r.ok })
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
this.assignDrawCallGroups(inst, claimed)
|
|
4252
|
+
return {
|
|
4253
|
+
ok: groupResults.every((r) => r.ok),
|
|
4254
|
+
groups: groupResults,
|
|
4255
|
+
unknownMaterials: [...unknownMaterials],
|
|
4256
|
+
conflicts: [...conflicts],
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
/** Add or replace a single style group by id. `opts` may carry a `previewNode` for the
|
|
4261
|
+
* editor's node-output preview workflow. */
|
|
4262
|
+
async upsertStyleGroup(modelName: string, group: StyleGroup, opts?: CompileOptions): Promise<ApplyStyleGroupResult> {
|
|
4263
|
+
const inst = this.modelInstances.get(modelName)
|
|
4264
|
+
if (!inst)
|
|
4265
|
+
return { ok: false, diagnostics: [{ severity: "error", message: `unknown model "${modelName}"` }], slotMap: [] }
|
|
4266
|
+
const r = await this.compileAndInstallGroup(inst, group, opts)
|
|
4267
|
+
this.assignDrawCallGroups(inst, this.currentClaims(inst))
|
|
4268
|
+
return r
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
/** Remove a style group; its materials revert to the ungrouped hand-shader path. */
|
|
4272
|
+
removeStyleGroup(modelName: string, groupId: string): void {
|
|
4273
|
+
const inst = this.modelInstances.get(modelName)
|
|
4274
|
+
const install = inst?.styleGroups.get(groupId)
|
|
4275
|
+
if (!inst || !install) return
|
|
4276
|
+
inst.styleGroupGen.set(groupId, (inst.styleGroupGen.get(groupId) ?? 0) + 1) // discard in-flight compile
|
|
4277
|
+
install.uniformBuffer.destroy()
|
|
4278
|
+
inst.styleGroups.delete(groupId)
|
|
4279
|
+
this.assignDrawCallGroups(inst, this.currentClaims(inst))
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
/** Clear all style groups on a model — every material returns to the hand-shader path. */
|
|
4283
|
+
resetStyleGroups(modelName: string): void {
|
|
4284
|
+
const inst = this.modelInstances.get(modelName)
|
|
4285
|
+
if (!inst) return
|
|
4286
|
+
for (const [id, install] of inst.styleGroups) {
|
|
4287
|
+
inst.styleGroupGen.set(id, (inst.styleGroupGen.get(id) ?? 0) + 1)
|
|
4288
|
+
install.uniformBuffer.destroy()
|
|
4289
|
+
}
|
|
4290
|
+
inst.styleGroups.clear()
|
|
4291
|
+
this.assignDrawCallGroups(inst, new Map())
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
/** Instant adjust-tier write: set one exposed slider on a group's applied graph. */
|
|
4295
|
+
setStyleParam(
|
|
4296
|
+
modelName: string,
|
|
4297
|
+
groupId: string,
|
|
4298
|
+
paramId: string,
|
|
4299
|
+
value: number | [number, number, number],
|
|
4300
|
+
): boolean {
|
|
4301
|
+
const install = this.modelInstances.get(modelName)?.styleGroups.get(groupId)
|
|
4302
|
+
const styleSlot = install?.slotMap.find((s) => s.id === paramId)
|
|
4303
|
+
if (!install || !styleSlot) return false
|
|
4304
|
+
if (styleSlot.kind === "float") {
|
|
4305
|
+
if (typeof value !== "number") return false
|
|
4306
|
+
const offset = styleSlot.vec4Index * 16 + ["x", "y", "z", "w"].indexOf(styleSlot.component!) * 4
|
|
4307
|
+
this.device.queue.writeBuffer(install.uniformBuffer, offset, new Float32Array([value]))
|
|
4308
|
+
} else {
|
|
4309
|
+
if (typeof value === "number") return false
|
|
4310
|
+
this.device.queue.writeBuffer(install.uniformBuffer, styleSlot.vec4Index * 16, new Float32Array(value))
|
|
4311
|
+
}
|
|
4312
|
+
return true
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
// Materials claimed by the model's currently-installed groups (for upsert/remove paths;
|
|
4316
|
+
// applyStyleGroups derives claims from its input array instead).
|
|
4317
|
+
private currentClaims(inst: ModelInstance): Map<string, string> {
|
|
4318
|
+
const claimed = new Map<string, string>()
|
|
4319
|
+
for (const install of inst.styleGroups.values())
|
|
4320
|
+
for (const m of install.group.materials) claimed.set(m, install.group.id)
|
|
4321
|
+
return claimed
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
// Compile a group's graph → WGSL → pipeline(s), install keyed by group id. Reuses the
|
|
4325
|
+
// install (pipeline + uniform buffer) when the graph/integration is byte-unchanged.
|
|
4326
|
+
private async compileAndInstallGroup(
|
|
4327
|
+
inst: ModelInstance,
|
|
4328
|
+
group: StyleGroup,
|
|
4329
|
+
opts?: CompileOptions,
|
|
4330
|
+
): Promise<ApplyStyleGroupResult> {
|
|
4331
|
+
const renderClass = group.renderClass ?? "auto"
|
|
4332
|
+
const alphaMode = group.alphaMode ?? "opaque"
|
|
4333
|
+
const signature = JSON.stringify({ g: group.graph, rc: renderClass, am: alphaMode, o: opts?.previewNode ?? null })
|
|
4334
|
+
const existing = inst.styleGroups.get(group.id)
|
|
4335
|
+
if (existing && existing.signature === signature) {
|
|
4336
|
+
existing.group = group // refresh def (label/materials) without recompiling
|
|
4337
|
+
return { ok: true, diagnostics: [], slotMap: existing.slotMap }
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
const result = compileGraph(group.graph, { ...opts, renderClass, alphaMode })
|
|
4159
4341
|
if (!result.ok) return { ok: false, diagnostics: result.diagnostics, slotMap: result.slotMap }
|
|
4160
4342
|
|
|
4161
|
-
const
|
|
4162
|
-
|
|
4163
|
-
this.styleGenerations.set(slot, generation)
|
|
4343
|
+
const generation = (inst.styleGroupGen.get(group.id) ?? 0) + 1
|
|
4344
|
+
inst.styleGroupGen.set(group.id, generation)
|
|
4164
4345
|
|
|
4165
4346
|
this.device.pushErrorScope("validation")
|
|
4166
|
-
const module = this.device.createShaderModule({ label: `style
|
|
4347
|
+
const module = this.device.createShaderModule({ label: `style group: ${group.id} (${renderClass})`, code: result.wgsl })
|
|
4167
4348
|
const info = await module.getCompilationInfo()
|
|
4168
4349
|
const scopeError = await this.device.popErrorScope()
|
|
4169
4350
|
const diagnostics = [...result.diagnostics]
|
|
4170
4351
|
for (const msg of info.messages) {
|
|
4171
4352
|
if (msg.type !== "error") continue
|
|
4172
|
-
diagnostics.push({
|
|
4173
|
-
severity: "error",
|
|
4174
|
-
nodeId: nodeIdForWgslLine(result.wgsl, msg.lineNum),
|
|
4175
|
-
message: `WGSL: ${msg.message}`,
|
|
4176
|
-
})
|
|
4353
|
+
diagnostics.push({ severity: "error", nodeId: nodeIdForWgslLine(result.wgsl, msg.lineNum), message: `WGSL: ${msg.message}` })
|
|
4177
4354
|
}
|
|
4178
4355
|
if (diagnostics.some((d) => d.severity === "error") || scopeError) {
|
|
4179
4356
|
if (scopeError && !diagnostics.some((d) => d.severity === "error"))
|
|
@@ -4184,51 +4361,66 @@ export class Engine {
|
|
|
4184
4361
|
let pipeline: GPURenderPipeline
|
|
4185
4362
|
let overEyesPipeline: GPURenderPipeline | undefined
|
|
4186
4363
|
try {
|
|
4187
|
-
pipeline = await this.
|
|
4188
|
-
if (
|
|
4364
|
+
pipeline = await this.createRenderClassPipeline(renderClass, module, false)
|
|
4365
|
+
if (renderClass === "hair") overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true)
|
|
4189
4366
|
} catch (e) {
|
|
4190
4367
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${(e as Error).message}` })
|
|
4191
4368
|
return { ok: false, diagnostics, slotMap: result.slotMap }
|
|
4192
4369
|
}
|
|
4193
4370
|
|
|
4194
|
-
//
|
|
4195
|
-
if (
|
|
4371
|
+
// Stale guard: a newer compile/remove for this id happened while we awaited.
|
|
4372
|
+
if (inst.styleGroupGen.get(group.id) !== generation) {
|
|
4196
4373
|
diagnostics.push({ severity: "warning", message: "superseded by a newer edit — result discarded" })
|
|
4197
4374
|
return { ok: false, diagnostics, slotMap: result.slotMap }
|
|
4198
4375
|
}
|
|
4199
4376
|
|
|
4200
|
-
|
|
4201
|
-
|
|
4377
|
+
const uniformBuffer =
|
|
4378
|
+
existing?.uniformBuffer ??
|
|
4379
|
+
this.device.createBuffer({
|
|
4380
|
+
label: `style uniforms: ${group.id}`,
|
|
4381
|
+
size: 256,
|
|
4382
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
4383
|
+
})
|
|
4384
|
+
|
|
4385
|
+
inst.styleGroups.set(group.id, {
|
|
4386
|
+
group,
|
|
4387
|
+
renderClass,
|
|
4388
|
+
alphaMode,
|
|
4389
|
+
pipeline,
|
|
4390
|
+
overEyesPipeline,
|
|
4391
|
+
uniformBuffer,
|
|
4392
|
+
slotMap: result.slotMap,
|
|
4393
|
+
signature,
|
|
4394
|
+
})
|
|
4395
|
+
this.writeGroupDefaults(uniformBuffer, group, result.slotMap)
|
|
4202
4396
|
return { ok: true, diagnostics, slotMap: result.slotMap }
|
|
4203
4397
|
}
|
|
4204
4398
|
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
const
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
const
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4399
|
+
// Rebind each material draw call to its (successfully-installed) group's uniform buffer,
|
|
4400
|
+
// or the zero buffer when ungrouped, then re-sort by render-class draw order.
|
|
4401
|
+
private assignDrawCallGroups(inst: ModelInstance, claimed: Map<string, string>): void {
|
|
4402
|
+
inst.materialToGroup.clear()
|
|
4403
|
+
for (const dc of inst.drawCalls) {
|
|
4404
|
+
if (!dc.baseBindGroupEntries) continue // outlines/ground are never grouped
|
|
4405
|
+
const wantId = claimed.get(dc.materialName)
|
|
4406
|
+
const install = wantId ? inst.styleGroups.get(wantId) : undefined
|
|
4407
|
+
const groupId = install ? wantId! : null
|
|
4408
|
+
if (groupId) inst.materialToGroup.set(dc.materialName, groupId)
|
|
4409
|
+
if (dc.groupId === groupId) continue
|
|
4410
|
+
dc.groupId = groupId
|
|
4411
|
+
dc.bindGroup = this.createMaterialBindGroup(
|
|
4412
|
+
`material: ${dc.materialName}`,
|
|
4413
|
+
dc.baseBindGroupEntries,
|
|
4414
|
+
install ? install.uniformBuffer : this.zeroStyleBuffer,
|
|
4415
|
+
)
|
|
4218
4416
|
}
|
|
4219
|
-
|
|
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)
|
|
4417
|
+
this.sortDrawCalls(inst)
|
|
4226
4418
|
}
|
|
4227
4419
|
|
|
4228
|
-
private
|
|
4420
|
+
private writeGroupDefaults(buffer: GPUBuffer, group: StyleGroup, slotMap: StyleSlot[]): void {
|
|
4229
4421
|
const data = new Float32Array(64) // 16 vec4f
|
|
4230
4422
|
for (const styleSlot of slotMap) {
|
|
4231
|
-
const param = graph.params?.find((p) => p.id === styleSlot.id)
|
|
4423
|
+
const param = group.graph.params?.find((p) => p.id === styleSlot.id)
|
|
4232
4424
|
if (!param) continue
|
|
4233
4425
|
const base = styleSlot.vec4Index * 4
|
|
4234
4426
|
if (styleSlot.kind === "float" && typeof param.default === "number") {
|
|
@@ -4237,20 +4429,46 @@ export class Engine {
|
|
|
4237
4429
|
data.set(param.default.slice(0, 3), base)
|
|
4238
4430
|
}
|
|
4239
4431
|
}
|
|
4240
|
-
this.device.queue.writeBuffer(
|
|
4432
|
+
this.device.queue.writeBuffer(buffer, 0, data)
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
// Draw-order rank within a bucket: eye stamps before hair reads. A grouped call ranks by
|
|
4436
|
+
// its group's render-class; an ungrouped call by its preset (the hand-shader path).
|
|
4437
|
+
private drawCallRank(inst: ModelInstance, dc: DrawCall): number {
|
|
4438
|
+
const rc = dc.groupId ? (inst.styleGroups.get(dc.groupId)?.renderClass ?? "auto") : undefined
|
|
4439
|
+
if (rc) return rc === "hair" ? 2 : rc === "eye" ? 1 : 0
|
|
4440
|
+
return dc.preset === "hair" ? 2 : dc.preset === "eye" ? 1 : 0
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
private sortDrawCalls(inst: ModelInstance): void {
|
|
4444
|
+
const typeOrder: Record<DrawCallType, number> = {
|
|
4445
|
+
opaque: 0,
|
|
4446
|
+
"opaque-outline": 1,
|
|
4447
|
+
transparent: 2,
|
|
4448
|
+
"transparent-outline": 3,
|
|
4449
|
+
ground: 4,
|
|
4450
|
+
}
|
|
4451
|
+
inst.drawCalls.sort(
|
|
4452
|
+
(a, b) => typeOrder[a.type] - typeOrder[b.type] || this.drawCallRank(inst, a) - this.drawCallRank(inst, b),
|
|
4453
|
+
)
|
|
4454
|
+
inst.shadowDrawCalls = inst.drawCalls.filter((d) => d.type === "opaque")
|
|
4241
4455
|
}
|
|
4242
4456
|
|
|
4243
4457
|
/**
|
|
4244
|
-
*
|
|
4245
|
-
*
|
|
4246
|
-
*
|
|
4458
|
+
* Render-class pipeline state. A group's compiled graph swaps the fragment shading; the
|
|
4459
|
+
* render-class owns pass integration (stencil interplay, depth bias, cull). auto = plain;
|
|
4460
|
+
* eye = stamp + front cull + bias; hair = stencil-test (+ the over-eyes variant).
|
|
4247
4461
|
*/
|
|
4248
|
-
private
|
|
4462
|
+
private createRenderClassPipeline(
|
|
4463
|
+
renderClass: RenderClass,
|
|
4464
|
+
module: GPUShaderModule,
|
|
4465
|
+
overEyes: boolean,
|
|
4466
|
+
): Promise<GPURenderPipeline> {
|
|
4249
4467
|
const base = {
|
|
4250
|
-
label: `style ${
|
|
4468
|
+
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
4251
4469
|
layout: this.mainPipelineLayout,
|
|
4252
4470
|
vertex: { module, buffers: this.fullVertexBufferLayouts },
|
|
4253
|
-
primitive: { cullMode: (
|
|
4471
|
+
primitive: { cullMode: (renderClass === "eye" ? "front" : "none") as GPUCullMode },
|
|
4254
4472
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4255
4473
|
}
|
|
4256
4474
|
const plainDepth: GPUDepthStencilState = {
|
|
@@ -4260,7 +4478,7 @@ export class Engine {
|
|
|
4260
4478
|
}
|
|
4261
4479
|
let depthStencil: GPUDepthStencilState = plainDepth
|
|
4262
4480
|
let constants: Record<string, number> | undefined
|
|
4263
|
-
if (
|
|
4481
|
+
if (renderClass === "hair" && !overEyes) {
|
|
4264
4482
|
depthStencil = {
|
|
4265
4483
|
...plainDepth,
|
|
4266
4484
|
stencilFront: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
|
|
@@ -4268,7 +4486,7 @@ export class Engine {
|
|
|
4268
4486
|
stencilReadMask: 0xff,
|
|
4269
4487
|
stencilWriteMask: 0,
|
|
4270
4488
|
}
|
|
4271
|
-
} else if (
|
|
4489
|
+
} else if (renderClass === "hair" && overEyes) {
|
|
4272
4490
|
constants = { IS_OVER_EYES: 1 }
|
|
4273
4491
|
depthStencil = {
|
|
4274
4492
|
format: "depth24plus-stencil8",
|
|
@@ -4279,7 +4497,7 @@ export class Engine {
|
|
|
4279
4497
|
stencilReadMask: 0xff,
|
|
4280
4498
|
stencilWriteMask: 0,
|
|
4281
4499
|
}
|
|
4282
|
-
} else if (
|
|
4500
|
+
} else if (renderClass === "eye") {
|
|
4283
4501
|
depthStencil = {
|
|
4284
4502
|
...plainDepth,
|
|
4285
4503
|
depthBias: -0.00005,
|
|
@@ -4298,11 +4516,17 @@ export class Engine {
|
|
|
4298
4516
|
})
|
|
4299
4517
|
}
|
|
4300
4518
|
|
|
4519
|
+
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4520
|
+
// the built-in preset (hand-shader) pipeline.
|
|
4521
|
+
private pipelineForDrawCall(inst: ModelInstance, dc: DrawCall): GPURenderPipeline {
|
|
4522
|
+
if (dc.groupId) {
|
|
4523
|
+
const install = inst.styleGroups.get(dc.groupId)
|
|
4524
|
+
if (install) return install.pipeline
|
|
4525
|
+
}
|
|
4526
|
+
return this.pipelineForPreset(dc.preset)
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4301
4529
|
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
|
|
4306
4530
|
if (preset === "face") return this.facePipeline
|
|
4307
4531
|
if (preset === "hair") return this.hairPipeline
|
|
4308
4532
|
if (preset === "cloth_smooth") return this.clothSmoothPipeline
|
|
@@ -4331,7 +4555,7 @@ export class Engine {
|
|
|
4331
4555
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
4332
4556
|
bound = true
|
|
4333
4557
|
}
|
|
4334
|
-
const pipeline = this.
|
|
4558
|
+
const pipeline = this.pipelineForDrawCall(inst, draw)
|
|
4335
4559
|
if (pipeline !== currentPipeline) {
|
|
4336
4560
|
pass.setPipeline(pipeline)
|
|
4337
4561
|
currentPipeline = pipeline
|
|
@@ -4386,26 +4610,41 @@ export class Engine {
|
|
|
4386
4610
|
}
|
|
4387
4611
|
|
|
4388
4612
|
/**
|
|
4389
|
-
* Second hair pass for the see-through-hair effect. Re-draws every hair opaque
|
|
4390
|
-
* draw
|
|
4391
|
-
*
|
|
4392
|
-
*
|
|
4613
|
+
* Second hair pass for the see-through-hair effect. Re-draws every hair-class opaque
|
|
4614
|
+
* draw with its over-eyes pipeline — stencil-matched to `EYE_VALUE`, `IS_OVER_EYES=true`
|
|
4615
|
+
* (25% alpha), depth-write off. Covers both grouped hair-class draws (their compiled
|
|
4616
|
+
* over-eyes pipeline) and ungrouped hair-preset draws (the hand `hairOverEyesPipeline`).
|
|
4393
4617
|
*/
|
|
4394
4618
|
private drawHairOverEyes(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
4395
4619
|
let bound = false
|
|
4620
|
+
let currentPipeline: GPURenderPipeline | null = null
|
|
4396
4621
|
for (const draw of inst.drawCalls) {
|
|
4397
|
-
if (draw.type !== "opaque" ||
|
|
4622
|
+
if (draw.type !== "opaque" || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
4623
|
+
const overEyes = this.overEyesPipelineFor(inst, draw)
|
|
4624
|
+
if (!overEyes) continue
|
|
4398
4625
|
if (!bound) {
|
|
4399
|
-
pass.setPipeline(this.styleOverrides.get("hair")?.overEyesPipeline ?? this.hairOverEyesPipeline)
|
|
4400
4626
|
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
4401
4627
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
4402
4628
|
bound = true
|
|
4403
4629
|
}
|
|
4630
|
+
if (overEyes !== currentPipeline) {
|
|
4631
|
+
pass.setPipeline(overEyes)
|
|
4632
|
+
currentPipeline = overEyes
|
|
4633
|
+
}
|
|
4404
4634
|
pass.setBindGroup(2, draw.bindGroup)
|
|
4405
4635
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
4406
4636
|
}
|
|
4407
4637
|
}
|
|
4408
4638
|
|
|
4639
|
+
// The over-eyes pipeline for a hair draw call, or null if it isn't hair-class.
|
|
4640
|
+
private overEyesPipelineFor(inst: ModelInstance, dc: DrawCall): GPURenderPipeline | null {
|
|
4641
|
+
if (dc.groupId) {
|
|
4642
|
+
const install = inst.styleGroups.get(dc.groupId)
|
|
4643
|
+
return install?.renderClass === "hair" ? (install.overEyesPipeline ?? null) : null
|
|
4644
|
+
}
|
|
4645
|
+
return dc.preset === "hair" ? this.hairOverEyesPipeline : null
|
|
4646
|
+
}
|
|
4647
|
+
|
|
4409
4648
|
private updateCameraUniforms() {
|
|
4410
4649
|
const viewMatrix = this.camera.getViewMatrix()
|
|
4411
4650
|
const projectionMatrix = this.camera.getProjectionMatrix()
|