reze-engine 0.24.1 → 0.25.1
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 +5 -1
- package/dist/engine.d.ts +127 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +422 -34
- 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/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- 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/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +143 -141
- package/dist/shaders/passes/composite.d.ts +23 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +62 -16
- package/dist/shaders/passes/depth-prepass.d.ts +2 -0
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -0
- package/dist/shaders/passes/depth-prepass.js +56 -0
- package/dist/shaders/passes/ground.d.ts +1 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +8 -0
- package/package.json +1 -1
- package/src/engine.ts +459 -34
- package/src/graph/slots.ts +11 -2
- package/src/index.ts +2 -0
- package/src/shaders/materials/common.ts +207 -205
- package/src/shaders/passes/composite.ts +89 -16
- package/src/shaders/passes/depth-prepass.ts +57 -0
- package/src/shaders/passes/ground.ts +8 -0
- 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
|
@@ -22,6 +22,7 @@ import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
|
|
|
22
22
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
|
|
23
23
|
import { GROUND_SHADOW_SHADER_WGSL } from "./shaders/passes/ground"
|
|
24
24
|
import { OUTLINE_SHADER_WGSL } from "./shaders/passes/outline"
|
|
25
|
+
import { TRANSPARENT_DEPTH_PREPASS_WGSL } from "./shaders/passes/depth-prepass"
|
|
25
26
|
import { SELECTION_MASK_SHADER_WGSL, SELECTION_EDGE_SHADER_WGSL } from "./shaders/passes/selection"
|
|
26
27
|
import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo"
|
|
27
28
|
import {
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
BLOOM_DOWNSAMPLE_SHADER_WGSL,
|
|
30
31
|
BLOOM_UPSAMPLE_SHADER_WGSL,
|
|
31
32
|
} from "./shaders/passes/bloom"
|
|
32
|
-
import {
|
|
33
|
+
import { buildCompositeShader } from "./shaders/passes/composite"
|
|
33
34
|
import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
|
|
34
35
|
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
|
|
35
36
|
import { compileGraph, type CompileOptions, type StyleSlot } from "./graph/compile"
|
|
@@ -172,6 +173,8 @@ type GroupInstall = {
|
|
|
172
173
|
renderClass: RenderClass
|
|
173
174
|
alphaMode: AlphaMode
|
|
174
175
|
pipeline: GPURenderPipeline
|
|
176
|
+
/** Depth-write-off twin, used when this group's material draws in the transparent bucket. */
|
|
177
|
+
pipelineNoDepthWrite: GPURenderPipeline
|
|
175
178
|
/** hair render-class only: the stencil-matched IS_OVER_EYES=true variant. */
|
|
176
179
|
overEyesPipeline?: GPURenderPipeline
|
|
177
180
|
uniformBuffer: GPUBuffer
|
|
@@ -223,6 +226,16 @@ export type SunOptions = {
|
|
|
223
226
|
direction?: Vec3
|
|
224
227
|
}
|
|
225
228
|
|
|
229
|
+
/** A background-effect param: number → f32, vector-like → vec3f (see
|
|
230
|
+
* setBackgroundEffect). Structural {x,y,z} rather than the Vec3 class so
|
|
231
|
+
* JSON-derived values (a shared scene document's params) pass straight in. */
|
|
232
|
+
export type BackgroundEffectParamValue = number | { x: number; y: number; z: number }
|
|
233
|
+
export type BackgroundEffectResult = {
|
|
234
|
+
ok: boolean
|
|
235
|
+
/** Compile/validation errors, line:col relative to the USER's WGSL. */
|
|
236
|
+
diagnostics: string[]
|
|
237
|
+
}
|
|
238
|
+
|
|
226
239
|
export type CameraOptions = {
|
|
227
240
|
/** Orbit distance from target. */
|
|
228
241
|
distance?: number
|
|
@@ -344,6 +357,8 @@ interface DrawCall {
|
|
|
344
357
|
// Present only for material draw calls (opaque/transparent) — the grouping walk skips
|
|
345
358
|
// any draw call without it.
|
|
346
359
|
baseBindGroupEntries?: GPUBindGroupEntry[]
|
|
360
|
+
/** Material draws only: false = excluded from the shadow map (fully sheer). */
|
|
361
|
+
castsShadow?: boolean
|
|
347
362
|
}
|
|
348
363
|
|
|
349
364
|
interface PickDrawCall {
|
|
@@ -392,6 +407,95 @@ interface GpuMorph {
|
|
|
392
407
|
dispatchNeeded: boolean
|
|
393
408
|
}
|
|
394
409
|
|
|
410
|
+
// ── Sheer-material detection ──────────────────────────────────────────────────
|
|
411
|
+
// PMX carries no "translucent" flag: a see-through veil usually has diffuse
|
|
412
|
+
// alpha 1.0 and does its transparency entirely in the TEXTURE's alpha channel.
|
|
413
|
+
// Classifying by material alpha alone put such cloth in the opaque bucket,
|
|
414
|
+
// where it draws in PMX order with depth writes — anything the engine draws
|
|
415
|
+
// after it (the hair render-class draws LAST for the eye-stencil effect) got
|
|
416
|
+
// depth-rejected behind the veil, so you saw the body through it but not the
|
|
417
|
+
// hair. These helpers measure a material's real coverage by sampling the
|
|
418
|
+
// texture's alpha at the material's own triangle CENTROIDS — centroids, not
|
|
419
|
+
// vertices, because hair-card corners sit in transparent texture margins and
|
|
420
|
+
// vertex sampling would misclassify hair (which must stay opaque-bucket for
|
|
421
|
+
// stencil interplay and shadows).
|
|
422
|
+
|
|
423
|
+
/** Downsampled alpha plane of a decoded texture (≤128², nearest-sampled). */
|
|
424
|
+
function buildAlphaSampler(
|
|
425
|
+
source: ImageBitmap | null,
|
|
426
|
+
rgba: Uint8Array | null,
|
|
427
|
+
width: number,
|
|
428
|
+
height: number,
|
|
429
|
+
): { a: Uint8ClampedArray; w: number; h: number } | null {
|
|
430
|
+
try {
|
|
431
|
+
const w = Math.max(1, Math.min(128, width))
|
|
432
|
+
const h = Math.max(1, Math.min(128, height))
|
|
433
|
+
const canvas = new OffscreenCanvas(w, h)
|
|
434
|
+
const cx = canvas.getContext("2d", { willReadFrequently: true })
|
|
435
|
+
if (!cx) return null
|
|
436
|
+
if (source) {
|
|
437
|
+
cx.drawImage(source, 0, 0, w, h)
|
|
438
|
+
} else if (rgba) {
|
|
439
|
+
const tmp = new OffscreenCanvas(width, height)
|
|
440
|
+
const tcx = tmp.getContext("2d")
|
|
441
|
+
if (!tcx) return null
|
|
442
|
+
tcx.putImageData(new ImageData(new Uint8ClampedArray(rgba), width, height), 0, 0)
|
|
443
|
+
cx.drawImage(tmp, 0, 0, w, h)
|
|
444
|
+
} else {
|
|
445
|
+
return null
|
|
446
|
+
}
|
|
447
|
+
const img = cx.getImageData(0, 0, w, h).data
|
|
448
|
+
const a = new Uint8ClampedArray(w * h)
|
|
449
|
+
for (let i = 0; i < w * h; i++) a[i] = img[i * 4 + 3]
|
|
450
|
+
return { a, w, h }
|
|
451
|
+
} catch {
|
|
452
|
+
return null
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
457
|
+
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
458
|
+
* neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
|
|
459
|
+
* they classify two distinct things:
|
|
460
|
+
* sheer (avg low) — a veil: mostly see-through everywhere
|
|
461
|
+
* partial (translucentFrac up) — mostly-opaque cloth with sheer REGIONS,
|
|
462
|
+
* e.g. a lace skirt panel
|
|
463
|
+
* Both route to the transparent bucket; only `sheer` is excluded from the
|
|
464
|
+
* shadow map and outline pass. */
|
|
465
|
+
const SHEER_ALPHA_THRESHOLD = 0.7
|
|
466
|
+
const PARTIAL_TRANSLUCENT_FRAC = 0.15
|
|
467
|
+
function materialAlphaStats(
|
|
468
|
+
verts: Float32Array,
|
|
469
|
+
indices: Uint32Array,
|
|
470
|
+
firstIndex: number,
|
|
471
|
+
count: number,
|
|
472
|
+
sampler: { a: Uint8ClampedArray; w: number; h: number } | null | undefined,
|
|
473
|
+
): { avg: number; translucentFrac: number } {
|
|
474
|
+
if (!sampler) return { avg: 1, translucentFrac: 0 }
|
|
475
|
+
const triCount = Math.floor(count / 3)
|
|
476
|
+
if (triCount === 0) return { avg: 1, translucentFrac: 0 }
|
|
477
|
+
const step = Math.max(1, Math.floor(triCount / 400))
|
|
478
|
+
let sum = 0
|
|
479
|
+
let translucent = 0
|
|
480
|
+
let n = 0
|
|
481
|
+
for (let t = 0; t < triCount; t += step) {
|
|
482
|
+
const i0 = indices[firstIndex + t * 3]
|
|
483
|
+
const i1 = indices[firstIndex + t * 3 + 1]
|
|
484
|
+
const i2 = indices[firstIndex + t * 3 + 2]
|
|
485
|
+
const u = (verts[i0 * 8 + 6] + verts[i1 * 8 + 6] + verts[i2 * 8 + 6]) / 3
|
|
486
|
+
const v = (verts[i0 * 8 + 7] + verts[i1 * 8 + 7] + verts[i2 * 8 + 7]) / 3
|
|
487
|
+
// Wrap (MMD UVs may tile), then nearest-sample the downsampled plane.
|
|
488
|
+
const x = Math.min(sampler.w - 1, Math.max(0, Math.floor((u - Math.floor(u)) * sampler.w)))
|
|
489
|
+
const y = Math.min(sampler.h - 1, Math.max(0, Math.floor((v - Math.floor(v)) * sampler.h)))
|
|
490
|
+
const a = sampler.a[y * sampler.w + x]
|
|
491
|
+
sum += a
|
|
492
|
+
if (a > 8 && a < 247) translucent++
|
|
493
|
+
n++
|
|
494
|
+
}
|
|
495
|
+
if (n === 0) return { avg: 1, translucentFrac: 0 }
|
|
496
|
+
return { avg: sum / n / 255, translucentFrac: translucent / n }
|
|
497
|
+
}
|
|
498
|
+
|
|
395
499
|
export class Engine {
|
|
396
500
|
private static instance: Engine | null = null
|
|
397
501
|
|
|
@@ -422,6 +526,8 @@ export class Engine {
|
|
|
422
526
|
// The one base shading model: ungrouped materials render this (compiled DEFAULT_GRAPH).
|
|
423
527
|
// Grouped materials use their group's own compiled pipeline.
|
|
424
528
|
private neutralPipeline!: GPURenderPipeline
|
|
529
|
+
private neutralPipelineNoDepthWrite!: GPURenderPipeline
|
|
530
|
+
private transparentDepthPrepassPipeline!: GPURenderPipeline
|
|
425
531
|
// ── Style group runtime ──
|
|
426
532
|
// Shared 256 B zero StyleUniforms buffer (group(2) binding(4)) bound by every ungrouped
|
|
427
533
|
// material; grouped materials rebind to their group's own buffer (per-model, in the
|
|
@@ -545,7 +651,7 @@ export class Engine {
|
|
|
545
651
|
private compositeBindGroup!: GPUBindGroup
|
|
546
652
|
private compositeUniformBuffer!: GPUBuffer
|
|
547
653
|
// [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
|
|
548
|
-
private readonly compositeUniformData = new Float32Array(
|
|
654
|
+
private readonly compositeUniformData = new Float32Array(28)
|
|
549
655
|
/** Composite background (display-space sRGB 0–1) — null = transparent canvas. */
|
|
550
656
|
private backgroundColor: Vec3 | null = null
|
|
551
657
|
// 360 backdrop (equirectangular skybox, sampled by view ray in composite).
|
|
@@ -553,6 +659,21 @@ export class Engine {
|
|
|
553
659
|
private backdropEquirectView: GPUTextureView | null = null
|
|
554
660
|
private fallbackEquirectTexture!: GPUTexture
|
|
555
661
|
private fallbackEquirectView!: GPUTextureView
|
|
662
|
+
// User WGSL background effect (background mode 3, setBackgroundEffect). The
|
|
663
|
+
// composite pipelines are REBUILT with the user code injected; params live in
|
|
664
|
+
// their own uniform buffer so setBackgroundEffectParam is a write, not a
|
|
665
|
+
// recompile (the same instant tier as setStyleParam).
|
|
666
|
+
private backgroundEffect: {
|
|
667
|
+
wgsl: string
|
|
668
|
+
paramLayout: Map<string, { offset: number; comps: 1 | 3 }>
|
|
669
|
+
paramsBuffer: GPUBuffer | null
|
|
670
|
+
paramsData: Float32Array<ArrayBuffer>
|
|
671
|
+
} | null = null
|
|
672
|
+
/** Bound at composite binding 7 when no effect (or a param-less one) is set. */
|
|
673
|
+
private bgParamsDummyBuffer!: GPUBuffer
|
|
674
|
+
private compositePipelineLayout!: GPUPipelineLayout
|
|
675
|
+
/** time=0 origin for the active effect — reset each setBackgroundEffect. */
|
|
676
|
+
private bgEffectEpochMs = 0
|
|
556
677
|
private compositeBloomView: GPUTextureView | null = null
|
|
557
678
|
|
|
558
679
|
// EEVEE-style bloom pyramid (mirrors Blender 3.6 effect_bloom_frag.glsl):
|
|
@@ -627,6 +748,11 @@ export class Engine {
|
|
|
627
748
|
private materialSampler!: GPUSampler
|
|
628
749
|
private fallbackMaterialTexture!: GPUTexture
|
|
629
750
|
private textureCache = new Map<string, GPUTexture>()
|
|
751
|
+
// Downsampled CPU alpha channel per texture (≤128², ~16KB) — kept so materials
|
|
752
|
+
// can be classified as SHEER at load by sampling alpha at their own UVs (the
|
|
753
|
+
// GPU texture can't be read back cheaply, and PMX diffuse alpha is usually 1.0
|
|
754
|
+
// even for see-through cloth: the translucency lives in the texture).
|
|
755
|
+
private textureAlphaCache = new Map<string, { a: Uint8ClampedArray; w: number; h: number } | null>()
|
|
630
756
|
private mipBlitPipeline: GPURenderPipeline | null = null
|
|
631
757
|
private mipBlitSampler: GPUSampler | null = null
|
|
632
758
|
private _nextDefaultModelId = 0
|
|
@@ -769,7 +895,12 @@ export class Engine {
|
|
|
769
895
|
u[8] = bg?.x ?? 0
|
|
770
896
|
u[9] = bg?.y ?? 0
|
|
771
897
|
u[10] = bg?.z ?? 0
|
|
898
|
+
// Base-layer mode; a user effect is a separate LAYER flagged at u[25] and
|
|
899
|
+
// over-composited onto whichever base is active.
|
|
772
900
|
u[11] = this.backdropEquirectView ? 2 : bg ? 1 : 0
|
|
901
|
+
u[25] = this.backgroundEffect ? 1 : 0
|
|
902
|
+
u[26] = this.canvas.width
|
|
903
|
+
u[27] = this.canvas.height
|
|
773
904
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u)
|
|
774
905
|
}
|
|
775
906
|
|
|
@@ -785,6 +916,12 @@ export class Engine {
|
|
|
785
916
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
786
917
|
}
|
|
787
918
|
|
|
919
|
+
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
920
|
+
private outlineEnabled = true
|
|
921
|
+
setOutlineEnabled(on: boolean): void {
|
|
922
|
+
this.outlineEnabled = on
|
|
923
|
+
}
|
|
924
|
+
|
|
788
925
|
private rebuildCompositeBindGroup(): void {
|
|
789
926
|
if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView) return
|
|
790
927
|
this.compositeBindGroup = this.device.createBindGroup({
|
|
@@ -798,6 +935,7 @@ export class Engine {
|
|
|
798
935
|
{ binding: 4, resource: this.maskResolveView },
|
|
799
936
|
{ binding: 5, resource: this.filmicLutView },
|
|
800
937
|
{ binding: 6, resource: this.backdropEquirectView ?? this.fallbackEquirectView },
|
|
938
|
+
{ binding: 7, resource: { buffer: this.backgroundEffect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
|
|
801
939
|
],
|
|
802
940
|
})
|
|
803
941
|
}
|
|
@@ -846,6 +984,168 @@ export class Engine {
|
|
|
846
984
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
847
985
|
}
|
|
848
986
|
|
|
987
|
+
private makeCompositePipeline(module: GPUShaderModule, applyGamma: boolean, label: string): GPURenderPipeline {
|
|
988
|
+
return this.device.createRenderPipeline({
|
|
989
|
+
label,
|
|
990
|
+
layout: this.compositePipelineLayout,
|
|
991
|
+
vertex: { module, entryPoint: "vs" },
|
|
992
|
+
fragment: {
|
|
993
|
+
module,
|
|
994
|
+
entryPoint: "fs",
|
|
995
|
+
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
996
|
+
targets: [{ format: this.presentationFormat }],
|
|
997
|
+
},
|
|
998
|
+
primitive: { topology: "triangle-list" },
|
|
999
|
+
})
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Install a WGSL background effect (shadertoy-style) as a LAYER between the
|
|
1004
|
+
* base background and the scene: rendered per-pixel in the composite pass and
|
|
1005
|
+
* over-composited onto whichever base is active (solid color, 360 equirect,
|
|
1006
|
+
* or transparency) — its alpha lets the base show through, so a starfield is
|
|
1007
|
+
* stars over the user's background color. Display-space: never affects
|
|
1008
|
+
* lighting, bloom, or tonemapping, and is captured by offline export like any
|
|
1009
|
+
* background.
|
|
1010
|
+
*
|
|
1011
|
+
* `wgsl` must define:
|
|
1012
|
+
*
|
|
1013
|
+
* fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
|
|
1014
|
+
*
|
|
1015
|
+
* where `ray` is the pixel's normalized world-space view direction (LH, +Z
|
|
1016
|
+
* forward — what the skybox samples by), `uv` is 0..1 bottom-left origin,
|
|
1017
|
+
* `time` is seconds since apply, and `bgResolution()` gives the canvas size.
|
|
1018
|
+
* Return sRGB + alpha. Declared `params` arrive as `params.<name>` (number →
|
|
1019
|
+
* f32, Vec3 → vec3f) and are later tweaked without recompiling via
|
|
1020
|
+
* setBackgroundEffectParam.
|
|
1021
|
+
*
|
|
1022
|
+
* Compiles off the hot path (async pipelines): on failure the previous
|
|
1023
|
+
* background is KEPT and diagnostics are returned with line numbers relative
|
|
1024
|
+
* to the user's WGSL. Pass null to remove the effect.
|
|
1025
|
+
*/
|
|
1026
|
+
async setBackgroundEffect(
|
|
1027
|
+
wgsl: string | null,
|
|
1028
|
+
params?: Record<string, BackgroundEffectParamValue>,
|
|
1029
|
+
): Promise<BackgroundEffectResult> {
|
|
1030
|
+
if (!this.device) return { ok: false, diagnostics: ["setBackgroundEffect requires init() to have run"] }
|
|
1031
|
+
|
|
1032
|
+
if (wgsl === null) {
|
|
1033
|
+
this.backgroundEffect?.paramsBuffer?.destroy()
|
|
1034
|
+
this.backgroundEffect = null
|
|
1035
|
+
const module = this.device.createShaderModule({ label: "composite shader", code: buildCompositeShader(null) })
|
|
1036
|
+
this.compositePipelineIdentity = this.makeCompositePipeline(module, false, "composite pipeline (gamma=1)")
|
|
1037
|
+
this.compositePipelineGamma = this.makeCompositePipeline(module, true, "composite pipeline (gamma!=1)")
|
|
1038
|
+
this.rebuildCompositeBindGroup()
|
|
1039
|
+
this.writeCompositeViewUniforms()
|
|
1040
|
+
return { ok: true, diagnostics: [] }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ── Params: codegen a WGSL struct and mirror its uniform layout on the CPU.
|
|
1044
|
+
// Fields are emitted in declaration order; offsets follow WGSL's natural
|
|
1045
|
+
// uniform rules (f32 align 4, vec3f align 16 size 12), computed identically
|
|
1046
|
+
// on both sides so no reordering is needed.
|
|
1047
|
+
const entries = Object.entries(params ?? {})
|
|
1048
|
+
const layout = new Map<string, { offset: number; comps: 1 | 3 }>()
|
|
1049
|
+
const fields: string[] = []
|
|
1050
|
+
let cursor = 0
|
|
1051
|
+
for (const [name, value] of entries) {
|
|
1052
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
1053
|
+
return { ok: false, diagnostics: [`invalid param name "${name}" (must be a WGSL identifier)`] }
|
|
1054
|
+
}
|
|
1055
|
+
const isVec = typeof value !== "number"
|
|
1056
|
+
const align = isVec ? 16 : 4
|
|
1057
|
+
const offset = Math.ceil(cursor / align) * align
|
|
1058
|
+
layout.set(name, { offset: offset / 4, comps: isVec ? 3 : 1 })
|
|
1059
|
+
fields.push(` ${name}: ${isVec ? "vec3f" : "f32"},`)
|
|
1060
|
+
cursor = offset + (isVec ? 12 : 4)
|
|
1061
|
+
}
|
|
1062
|
+
const paramsData = new Float32Array(Math.max(4, Math.ceil(cursor / 16) * 4))
|
|
1063
|
+
for (const [name, value] of entries) {
|
|
1064
|
+
const slot = layout.get(name)!
|
|
1065
|
+
if (typeof value === "number") paramsData[slot.offset] = value
|
|
1066
|
+
else {
|
|
1067
|
+
paramsData[slot.offset] = value.x
|
|
1068
|
+
paramsData[slot.offset + 1] = value.y
|
|
1069
|
+
paramsData[slot.offset + 2] = value.z
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const paramsDecl = entries.length
|
|
1073
|
+
? `struct BgParams {\n${fields.join("\n")}\n}\n@group(0) @binding(7) var<uniform> params: BgParams;\n`
|
|
1074
|
+
: ""
|
|
1075
|
+
|
|
1076
|
+
// ── Compile with validation captured, not thrown at the console. Line
|
|
1077
|
+
// numbers in diagnostics are rebased to the USER's source.
|
|
1078
|
+
const source = buildCompositeShader({ wgsl, paramsDecl })
|
|
1079
|
+
const userLineOffset = source.slice(0, source.indexOf(wgsl)).split("\n").length - 1
|
|
1080
|
+
this.device.pushErrorScope("validation")
|
|
1081
|
+
const module = this.device.createShaderModule({ label: "composite shader (bg effect)", code: source })
|
|
1082
|
+
const info = await module.getCompilationInfo()
|
|
1083
|
+
const scopeErr = await this.device.popErrorScope()
|
|
1084
|
+
const diagnostics = info.messages
|
|
1085
|
+
.filter((m) => m.type === "error")
|
|
1086
|
+
.map((m) => `${Math.max(0, m.lineNum - userLineOffset)}:${m.linePos} ${m.message}`)
|
|
1087
|
+
if (diagnostics.length === 0 && scopeErr) diagnostics.push(scopeErr.message)
|
|
1088
|
+
if (diagnostics.length > 0) return { ok: false, diagnostics }
|
|
1089
|
+
let identity: GPURenderPipeline
|
|
1090
|
+
let gamma: GPURenderPipeline
|
|
1091
|
+
try {
|
|
1092
|
+
const make = (applyGamma: boolean, label: string) =>
|
|
1093
|
+
this.device.createRenderPipelineAsync({
|
|
1094
|
+
label,
|
|
1095
|
+
layout: this.compositePipelineLayout,
|
|
1096
|
+
vertex: { module, entryPoint: "vs" },
|
|
1097
|
+
fragment: {
|
|
1098
|
+
module,
|
|
1099
|
+
entryPoint: "fs",
|
|
1100
|
+
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
1101
|
+
targets: [{ format: this.presentationFormat }],
|
|
1102
|
+
},
|
|
1103
|
+
primitive: { topology: "triangle-list" },
|
|
1104
|
+
})
|
|
1105
|
+
;[identity, gamma] = await Promise.all([
|
|
1106
|
+
make(false, "composite pipeline (bg effect, gamma=1)"),
|
|
1107
|
+
make(true, "composite pipeline (bg effect, gamma!=1)"),
|
|
1108
|
+
])
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// ── Swap — only now does the old effect (and its params buffer) go away.
|
|
1114
|
+
this.backgroundEffect?.paramsBuffer?.destroy()
|
|
1115
|
+
let paramsBuffer: GPUBuffer | null = null
|
|
1116
|
+
if (entries.length) {
|
|
1117
|
+
paramsBuffer = this.device.createBuffer({
|
|
1118
|
+
label: "bg effect params",
|
|
1119
|
+
size: paramsData.byteLength,
|
|
1120
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1121
|
+
})
|
|
1122
|
+
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData)
|
|
1123
|
+
}
|
|
1124
|
+
this.backgroundEffect = { wgsl, paramLayout: layout, paramsBuffer, paramsData }
|
|
1125
|
+
this.compositePipelineIdentity = identity
|
|
1126
|
+
this.compositePipelineGamma = gamma
|
|
1127
|
+
this.bgEffectEpochMs = performance.now()
|
|
1128
|
+
this.rebuildCompositeBindGroup()
|
|
1129
|
+
this.writeCompositeViewUniforms()
|
|
1130
|
+
return { ok: true, diagnostics: [] }
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/** Write one background-effect param (declared at setBackgroundEffect) — a
|
|
1134
|
+
* uniform write, no recompile; the instant tier, like setStyleParam. */
|
|
1135
|
+
setBackgroundEffectParam(name: string, value: BackgroundEffectParamValue): void {
|
|
1136
|
+
const fx = this.backgroundEffect
|
|
1137
|
+
if (!fx || !fx.paramsBuffer) return
|
|
1138
|
+
const slot = fx.paramLayout.get(name)
|
|
1139
|
+
if (!slot) return
|
|
1140
|
+
if (typeof value === "number") fx.paramsData[slot.offset] = value
|
|
1141
|
+
else {
|
|
1142
|
+
fx.paramsData[slot.offset] = value.x
|
|
1143
|
+
fx.paramsData[slot.offset + 1] = value.y
|
|
1144
|
+
fx.paramsData[slot.offset + 2] = value.z
|
|
1145
|
+
}
|
|
1146
|
+
this.device.queue.writeBuffer(fx.paramsBuffer, 0, fx.paramsData)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
849
1149
|
/** Patch bloom; GPU uniforms update immediately if `init()` has run. */
|
|
850
1150
|
setBloomOptions(patch: Partial<BloomOptions>): void {
|
|
851
1151
|
const b = this.bloomSettings
|
|
@@ -1325,6 +1625,44 @@ export class Engine {
|
|
|
1325
1625
|
depthCompare: "less-equal",
|
|
1326
1626
|
},
|
|
1327
1627
|
})
|
|
1628
|
+
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
1629
|
+
this.neutralPipelineNoDepthWrite = this.createRenderPipeline({
|
|
1630
|
+
label: "neutral base pipeline (no depth write)",
|
|
1631
|
+
layout: mainPipelineLayout,
|
|
1632
|
+
shaderModule: neutralModule,
|
|
1633
|
+
vertexBuffers: fullVertexBuffers,
|
|
1634
|
+
fragmentTargets: sceneTargets,
|
|
1635
|
+
cullMode: "none",
|
|
1636
|
+
depthStencil: {
|
|
1637
|
+
format: "depth24plus-stencil8",
|
|
1638
|
+
depthWriteEnabled: false,
|
|
1639
|
+
depthCompare: "less-equal",
|
|
1640
|
+
},
|
|
1641
|
+
})
|
|
1642
|
+
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
1643
|
+
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
1644
|
+
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
1645
|
+
const prepassModule = this.device.createShaderModule({
|
|
1646
|
+
label: "transparent depth prepass",
|
|
1647
|
+
code: TRANSPARENT_DEPTH_PREPASS_WGSL,
|
|
1648
|
+
})
|
|
1649
|
+
this.transparentDepthPrepassPipeline = this.device.createRenderPipeline({
|
|
1650
|
+
label: "transparent depth prepass",
|
|
1651
|
+
layout: mainPipelineLayout,
|
|
1652
|
+
vertex: { module: prepassModule, entryPoint: "vs", buffers: fullVertexBuffers as GPUVertexBufferLayout[] },
|
|
1653
|
+
fragment: {
|
|
1654
|
+
module: prepassModule,
|
|
1655
|
+
entryPoint: "fs",
|
|
1656
|
+
targets: sceneTargets.map((t) => ({ format: (t as GPUColorTargetState).format, writeMask: 0 })),
|
|
1657
|
+
},
|
|
1658
|
+
primitive: { cullMode: "none" },
|
|
1659
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
1660
|
+
depthStencil: {
|
|
1661
|
+
format: "depth24plus-stencil8",
|
|
1662
|
+
depthWriteEnabled: true,
|
|
1663
|
+
depthCompare: "less-equal",
|
|
1664
|
+
},
|
|
1665
|
+
})
|
|
1328
1666
|
|
|
1329
1667
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
1330
1668
|
size: 64,
|
|
@@ -1462,6 +1800,16 @@ export class Engine {
|
|
|
1462
1800
|
// Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
|
|
1463
1801
|
depthWriteEnabled: false,
|
|
1464
1802
|
depthCompare: "less-equal",
|
|
1803
|
+
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1804
|
+
// carry their surface's exact depth, so against this model's paired
|
|
1805
|
+
// near-coplanar skirt layers the hulls WON depth ties in patches —
|
|
1806
|
+
// the black shapes on the dress. A small constant bias makes hulls lose
|
|
1807
|
+
// every tie; silhouette rims compare against the far background and are
|
|
1808
|
+
// unaffected. No slope term — slope explodes at silhouettes and would
|
|
1809
|
+
// erase the rims themselves (previous regression).
|
|
1810
|
+
depthBias: 4,
|
|
1811
|
+
depthBiasSlopeScale: 0,
|
|
1812
|
+
depthBiasClamp: 0,
|
|
1465
1813
|
// Skip fragments where the eye stamped stencil=EYE_VALUE. Those pixels are owned by
|
|
1466
1814
|
// the see-through-hair blend (hair-over-eyes), so letting the outline's near-black
|
|
1467
1815
|
// edge color overwrite them would re-introduce the dark almond we just killed.
|
|
@@ -1643,9 +1991,15 @@ export class Engine {
|
|
|
1643
1991
|
// mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
|
|
1644
1992
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
1645
1993
|
label: "composite view uniforms",
|
|
1646
|
-
//
|
|
1647
|
-
// (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray
|
|
1648
|
-
|
|
1994
|
+
// 7 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
1995
|
+
// (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray ·
|
|
1996
|
+
// (time, _, canvas width, canvas height) for user background effects.
|
|
1997
|
+
size: 112,
|
|
1998
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1999
|
+
})
|
|
2000
|
+
this.bgParamsDummyBuffer = this.device.createBuffer({
|
|
2001
|
+
label: "bg effect params (dummy)",
|
|
2002
|
+
size: 16,
|
|
1649
2003
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1650
2004
|
})
|
|
1651
2005
|
this.compositeBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -1662,6 +2016,9 @@ export class Engine {
|
|
|
1662
2016
|
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1663
2017
|
// 360 backdrop equirect (PhotoDome-style skybox) — 1×1 fallback when unset.
|
|
1664
2018
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
2019
|
+
// User background-effect params — dummy buffer when no effect is set. The
|
|
2020
|
+
// layout is explicit, so the base shader legally ignores the binding.
|
|
2021
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1665
2022
|
],
|
|
1666
2023
|
})
|
|
1667
2024
|
this.fallbackEquirectTexture = this.device.createTexture({
|
|
@@ -1672,29 +2029,15 @@ export class Engine {
|
|
|
1672
2029
|
})
|
|
1673
2030
|
this.fallbackEquirectView = this.fallbackEquirectTexture.createView()
|
|
1674
2031
|
|
|
2032
|
+
this.compositePipelineLayout = this.device.createPipelineLayout({
|
|
2033
|
+
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
2034
|
+
})
|
|
1675
2035
|
const compositeShader = this.device.createShaderModule({
|
|
1676
2036
|
label: "composite shader",
|
|
1677
|
-
code:
|
|
1678
|
-
})
|
|
1679
|
-
|
|
1680
|
-
const compositePipelineLayout = this.device.createPipelineLayout({
|
|
1681
|
-
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
2037
|
+
code: buildCompositeShader(null),
|
|
1682
2038
|
})
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
label,
|
|
1686
|
-
layout: compositePipelineLayout,
|
|
1687
|
-
vertex: { module: compositeShader, entryPoint: "vs" },
|
|
1688
|
-
fragment: {
|
|
1689
|
-
module: compositeShader,
|
|
1690
|
-
entryPoint: "fs",
|
|
1691
|
-
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
1692
|
-
targets: [{ format: this.presentationFormat }],
|
|
1693
|
-
},
|
|
1694
|
-
primitive: { topology: "triangle-list" },
|
|
1695
|
-
})
|
|
1696
|
-
this.compositePipelineIdentity = makeCompositePipeline(false, "composite pipeline (gamma=1)")
|
|
1697
|
-
this.compositePipelineGamma = makeCompositePipeline(true, "composite pipeline (gamma!=1)")
|
|
2039
|
+
this.compositePipelineIdentity = this.makeCompositePipeline(compositeShader, false, "composite pipeline (gamma=1)")
|
|
2040
|
+
this.compositePipelineGamma = this.makeCompositePipeline(compositeShader, true, "composite pipeline (gamma!=1)")
|
|
1698
2041
|
|
|
1699
2042
|
// GPU vertex-morph compute pipeline (shared by all models; per-model bind groups).
|
|
1700
2043
|
// Bindings: 0-4 read-only storage (base pos, CSR rowStart/colMorph/colOffset, weights),
|
|
@@ -2613,6 +2956,7 @@ export class Engine {
|
|
|
2613
2956
|
if (!shared) {
|
|
2614
2957
|
tex.destroy()
|
|
2615
2958
|
this.textureCache.delete(path)
|
|
2959
|
+
this.textureAlphaCache.delete(path)
|
|
2616
2960
|
}
|
|
2617
2961
|
}
|
|
2618
2962
|
for (const buf of inst.gpuBuffers) {
|
|
@@ -3181,11 +3525,18 @@ export class Engine {
|
|
|
3181
3525
|
// 1-based so that (0,0) = clear color = "no hit"
|
|
3182
3526
|
const modelId = this.modelInstances.size + 1
|
|
3183
3527
|
|
|
3528
|
+
const texLogicalPath = (texIndex: number): string | null =>
|
|
3529
|
+
texIndex < 0 || texIndex >= textures.length
|
|
3530
|
+
? null
|
|
3531
|
+
: joinAssetPath(inst.basePath, normalizeAssetPath(textures[texIndex].path))
|
|
3184
3532
|
const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
return this.createTextureFromLogicalPath(inst, logicalPath)
|
|
3533
|
+
const logicalPath = texLogicalPath(texIndex)
|
|
3534
|
+
return logicalPath ? this.createTextureFromLogicalPath(inst, logicalPath) : null
|
|
3188
3535
|
}
|
|
3536
|
+
// Mesh data for sheerness sampling (8 floats/vertex; uv at +6). See
|
|
3537
|
+
// materialIsSheer — classification happens per material below.
|
|
3538
|
+
const meshVertices = model.getVertices()
|
|
3539
|
+
const meshIndices = model.getIndices()
|
|
3189
3540
|
|
|
3190
3541
|
// 頭 bone index for the eye shader's rear-view gate (-1 when absent).
|
|
3191
3542
|
const headBoneIndex = model.getSkeleton().bones.findIndex((b) => b.name === "頭")
|
|
@@ -3204,7 +3555,29 @@ export class Engine {
|
|
|
3204
3555
|
}
|
|
3205
3556
|
|
|
3206
3557
|
const materialAlpha = mat.diffuse[3]
|
|
3207
|
-
|
|
3558
|
+
// Transparent bucket when the MATERIAL says so — or when the TEXTURE does
|
|
3559
|
+
// (sheer cloth almost always ships with diffuse alpha 1.0 and carries its
|
|
3560
|
+
// translucency in texture alpha). Transparent-bucket draws happen after
|
|
3561
|
+
// the opaque bucket (and after the late-drawn hair render-class), so a
|
|
3562
|
+
// veil composites over the hair behind it instead of depth-rejecting it;
|
|
3563
|
+
// they are also excluded from the shadow map, so sheer cloth stops
|
|
3564
|
+
// casting the solid shadow of an opaque sheet.
|
|
3565
|
+
const diffusePath = texLogicalPath(mat.diffuseTextureIndex)
|
|
3566
|
+
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null
|
|
3567
|
+
const stats = materialAlphaStats(meshVertices, meshIndices, currentIndexOffset, indexCount, alphaSampler)
|
|
3568
|
+
const sheer = stats.avg < SHEER_ALPHA_THRESHOLD
|
|
3569
|
+
const partial = !sheer && stats.translucentFrac > PARTIAL_TRANSLUCENT_FRAC
|
|
3570
|
+
// Transparent bucket: drawn after the opaque bucket (and hair) with depth
|
|
3571
|
+
// WRITE OFF, so a lace panel folded in front of itself blends both layers
|
|
3572
|
+
// consistently instead of a triangle-order patchwork of single/double
|
|
3573
|
+
// coverage. Partial-sheer cloth still casts shadows and keeps its outline;
|
|
3574
|
+
// fully sheer cloth (veil) does neither.
|
|
3575
|
+
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || partial
|
|
3576
|
+
// Load-time classification log — one line per material, cheap and
|
|
3577
|
+
// invaluable when a model renders wrong (bucket/outline/sheer disputes).
|
|
3578
|
+
console.info(
|
|
3579
|
+
`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)} sheerFrac=${stats.translucentFrac.toFixed(2)} ${sheer ? "SHEER" : partial ? "PARTIAL" : "opaque"} bucket=${isTransparent ? "transparent" : "opaque"} edge=${(mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 ? (sheer ? "skipped(sheer)" : "on") : "off"}`,
|
|
3580
|
+
)
|
|
3208
3581
|
|
|
3209
3582
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
3210
3583
|
// rare and not implemented — treated as none, like a failed load.
|
|
@@ -3251,9 +3624,15 @@ export class Engine {
|
|
|
3251
3624
|
materialName: mat.name,
|
|
3252
3625
|
groupId: null,
|
|
3253
3626
|
baseBindGroupEntries,
|
|
3627
|
+
castsShadow: !sheer,
|
|
3254
3628
|
})
|
|
3255
3629
|
|
|
3256
|
-
|
|
3630
|
+
// No inverted-hull outline for SHEER materials: the outline shader draws a
|
|
3631
|
+
// solid edgeColor silhouette (it never samples texture alpha), so a
|
|
3632
|
+
// see-through veil dragged a near-black hull over the cloth behind it —
|
|
3633
|
+
// broken black shapes that waved with physics and flickered with camera
|
|
3634
|
+
// angle. A solid rim on see-through fabric is wrong in principle.
|
|
3635
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 && !sheer) {
|
|
3257
3636
|
const materialUniformData = new Float32Array([
|
|
3258
3637
|
mat.edgeColor[0],
|
|
3259
3638
|
mat.edgeColor[1],
|
|
@@ -3403,6 +3782,10 @@ export class Engine {
|
|
|
3403
3782
|
}
|
|
3404
3783
|
}
|
|
3405
3784
|
|
|
3785
|
+
// CPU alpha sampler for sheerness classification (see textureAlphaCache).
|
|
3786
|
+
// Canvas 2D premultiplies RGB on readback, but the ALPHA channel is exact.
|
|
3787
|
+
this.textureAlphaCache.set(cacheKey, buildAlphaSampler(source, rgba, width, height))
|
|
3788
|
+
|
|
3406
3789
|
const mipLevelCount = Math.floor(Math.log2(Math.max(width, height))) + 1
|
|
3407
3790
|
const texture = this.device.createTexture({
|
|
3408
3791
|
label: `texture: ${cacheKey}`,
|
|
@@ -4418,9 +4801,13 @@ export class Engine {
|
|
|
4418
4801
|
}
|
|
4419
4802
|
|
|
4420
4803
|
let pipeline: GPURenderPipeline
|
|
4804
|
+
let pipelineNoDepthWrite: GPURenderPipeline
|
|
4421
4805
|
let overEyesPipeline: GPURenderPipeline | undefined
|
|
4422
4806
|
try {
|
|
4423
4807
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false)
|
|
4808
|
+
// Transparent-bucket draws don't write depth (self-overlap must blend, not
|
|
4809
|
+
// patchwork) — same shading, different depth state.
|
|
4810
|
+
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false)
|
|
4424
4811
|
if (renderClass === "hair") overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true)
|
|
4425
4812
|
} catch (e) {
|
|
4426
4813
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${(e as Error).message}` })
|
|
@@ -4446,6 +4833,7 @@ export class Engine {
|
|
|
4446
4833
|
renderClass,
|
|
4447
4834
|
alphaMode,
|
|
4448
4835
|
pipeline,
|
|
4836
|
+
pipelineNoDepthWrite,
|
|
4449
4837
|
overEyesPipeline,
|
|
4450
4838
|
uniformBuffer,
|
|
4451
4839
|
slotMap: result.slotMap,
|
|
@@ -4509,7 +4897,9 @@ export class Engine {
|
|
|
4509
4897
|
inst.drawCalls.sort(
|
|
4510
4898
|
(a, b) => typeOrder[a.type] - typeOrder[b.type] || this.drawCallRank(inst, a) - this.drawCallRank(inst, b),
|
|
4511
4899
|
)
|
|
4512
|
-
inst.shadowDrawCalls = inst.drawCalls.filter(
|
|
4900
|
+
inst.shadowDrawCalls = inst.drawCalls.filter(
|
|
4901
|
+
(d) => (d.type === "opaque" || d.type === "transparent") && d.castsShadow === true,
|
|
4902
|
+
)
|
|
4513
4903
|
}
|
|
4514
4904
|
|
|
4515
4905
|
/**
|
|
@@ -4521,6 +4911,7 @@ export class Engine {
|
|
|
4521
4911
|
renderClass: RenderClass,
|
|
4522
4912
|
module: GPUShaderModule,
|
|
4523
4913
|
overEyes: boolean,
|
|
4914
|
+
depthWrite = true,
|
|
4524
4915
|
): Promise<GPURenderPipeline> {
|
|
4525
4916
|
const base = {
|
|
4526
4917
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
@@ -4531,7 +4922,7 @@ export class Engine {
|
|
|
4531
4922
|
}
|
|
4532
4923
|
const plainDepth: GPUDepthStencilState = {
|
|
4533
4924
|
format: "depth24plus-stencil8",
|
|
4534
|
-
depthWriteEnabled:
|
|
4925
|
+
depthWriteEnabled: depthWrite,
|
|
4535
4926
|
depthCompare: "less-equal",
|
|
4536
4927
|
}
|
|
4537
4928
|
let depthStencil: GPUDepthStencilState = plainDepth
|
|
@@ -4577,6 +4968,12 @@ export class Engine {
|
|
|
4577
4968
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4578
4969
|
// the neutral base (ungrouped materials render the default graph).
|
|
4579
4970
|
private pipelineForDrawCall(inst: ModelInstance, dc: DrawCall): GPURenderPipeline {
|
|
4971
|
+
// Transparent draws WRITE depth — MMD semantics: PMX triangle/material order
|
|
4972
|
+
// is the author's compositing order, and a fold HIDES its far side rather
|
|
4973
|
+
// than blending it (the far side shades dark — light-averted — so letting it
|
|
4974
|
+
// show through read as gray fold-shaped stains; depth-write-off made every
|
|
4975
|
+
// fold do that). The no-write twins stay available for a future true-OIT
|
|
4976
|
+
// path but are deliberately unused.
|
|
4580
4977
|
if (dc.groupId) {
|
|
4581
4978
|
const install = inst.styleGroups.get(dc.groupId)
|
|
4582
4979
|
if (install) return install.pipeline
|
|
@@ -4617,6 +5014,7 @@ export class Engine {
|
|
|
4617
5014
|
* rebind group 0/1 correctly if needed.
|
|
4618
5015
|
*/
|
|
4619
5016
|
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, type: DrawCallType): void {
|
|
5017
|
+
if (!this.outlineEnabled) return
|
|
4620
5018
|
let bound = false
|
|
4621
5019
|
for (const draw of inst.drawCalls) {
|
|
4622
5020
|
if (draw.type !== type || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
@@ -4647,13 +5045,36 @@ export class Engine {
|
|
|
4647
5045
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
4648
5046
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
4649
5047
|
|
|
5048
|
+
// Order matters (the black-hull saga): transparent color draws don't write
|
|
5049
|
+
// depth (self-overlap must double-blend, not patchwork), so ALL outlines
|
|
5050
|
+
// draw after the transparent depth PREPASS has recorded the fabric's depth
|
|
5051
|
+
// — otherwise every hull behind a sheer skirt shows straight through it.
|
|
4650
5052
|
this.drawMaterials(pass, inst, "opaque")
|
|
4651
|
-
this.drawOutlines(pass, inst, "opaque-outline")
|
|
4652
5053
|
this.drawHairOverEyes(pass, inst)
|
|
4653
5054
|
this.drawMaterials(pass, inst, "transparent")
|
|
5055
|
+
this.drawOutlines(pass, inst, "opaque-outline")
|
|
4654
5056
|
this.drawOutlines(pass, inst, "transparent-outline")
|
|
4655
5057
|
}
|
|
4656
5058
|
|
|
5059
|
+
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
5060
|
+
* Unused while transparent draws write depth themselves (MMD parity) — kept
|
|
5061
|
+
* for the dormant no-write/OIT path. */
|
|
5062
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5063
|
+
protected drawTransparentDepthPrepass(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5064
|
+
let bound = false
|
|
5065
|
+
for (const draw of inst.drawCalls) {
|
|
5066
|
+
if (draw.type !== "transparent" || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
5067
|
+
if (!bound) {
|
|
5068
|
+
pass.setPipeline(this.transparentDepthPrepassPipeline)
|
|
5069
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
5070
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
5071
|
+
bound = true
|
|
5072
|
+
}
|
|
5073
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
5074
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
|
|
4657
5078
|
/**
|
|
4658
5079
|
* Second hair pass for the see-through-hair effect. Re-draws every hair-class grouped
|
|
4659
5080
|
* opaque draw with its compiled over-eyes pipeline — stencil-matched to `EYE_VALUE`,
|
|
@@ -4705,7 +5126,7 @@ export class Engine {
|
|
|
4705
5126
|
// is LEFT-HANDED (+Z forward, see Mat4.lookAtInto), so the world-space
|
|
4706
5127
|
// right/up/FORWARD vectors are rows 0/1/2 of its rotation block directly
|
|
4707
5128
|
// (column-major storage: row i = values[i], values[i+4], values[i+8]).
|
|
4708
|
-
if (this.backdropEquirectView && this.compositeUniformBuffer) {
|
|
5129
|
+
if ((this.backdropEquirectView || this.backgroundEffect) && this.compositeUniformBuffer) {
|
|
4709
5130
|
const v = viewMatrix.values
|
|
4710
5131
|
const u = this.compositeUniformData
|
|
4711
5132
|
const tanHalf = Math.tan((this.camera.fov ?? Math.PI / 4) / 2)
|
|
@@ -4722,6 +5143,10 @@ export class Engine {
|
|
|
4722
5143
|
u[21] = v[6]
|
|
4723
5144
|
u[22] = v[10]
|
|
4724
5145
|
u[23] = 0
|
|
5146
|
+
// Effect clock + canvas size (viewU[6]) — written on the same refresh.
|
|
5147
|
+
u[24] = (performance.now() - this.bgEffectEpochMs) / 1000
|
|
5148
|
+
u[26] = this.canvas.width
|
|
5149
|
+
u[27] = this.canvas.height
|
|
4725
5150
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u)
|
|
4726
5151
|
}
|
|
4727
5152
|
}
|