reze-engine 0.25.0 → 0.25.2
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 +95 -16
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +205 -72
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +10 -2
- 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.map +1 -1
- package/dist/shaders/passes/composite.js +18 -3
- 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/outline.d.ts +1 -1
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +50 -23
- package/package.json +1 -1
- package/src/engine.ts +220 -75
- package/src/graph/slots.ts +11 -2
- package/src/shaders/materials/common.ts +207 -205
- package/src/shaders/passes/composite.ts +23 -3
- package/src/shaders/passes/depth-prepass.ts +57 -0
- package/src/shaders/passes/outline.ts +50 -23
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 {
|
|
@@ -172,6 +173,8 @@ type GroupInstall = {
|
|
|
172
173
|
renderClass: RenderClass
|
|
173
174
|
alphaMode: AlphaMode
|
|
174
175
|
pipeline: GPURenderPipeline
|
|
176
|
+
/** Depth-write-off twin — dormant, kept for a future OIT path. */
|
|
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
|
|
@@ -354,6 +357,12 @@ interface DrawCall {
|
|
|
354
357
|
// Present only for material draw calls (opaque/transparent) — the grouping walk skips
|
|
355
358
|
// any draw call without it.
|
|
356
359
|
baseBindGroupEntries?: GPUBindGroupEntry[]
|
|
360
|
+
/** Material draws only: false = excluded from the shadow map (fully sheer). */
|
|
361
|
+
castsShadow?: boolean
|
|
362
|
+
/** Edge-flagged materials: interleaved inverted-hull outline drawn right after
|
|
363
|
+
* this material with the outline pipeline. Shares this call's index range;
|
|
364
|
+
* own bind group (edge uniforms + diffuse texture for the alpha test). */
|
|
365
|
+
outline?: { bindGroup: GPUBindGroup }
|
|
357
366
|
}
|
|
358
367
|
|
|
359
368
|
interface PickDrawCall {
|
|
@@ -448,21 +457,26 @@ function buildAlphaSampler(
|
|
|
448
457
|
}
|
|
449
458
|
}
|
|
450
459
|
|
|
451
|
-
/**
|
|
452
|
-
*
|
|
460
|
+
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
461
|
+
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
462
|
+
* neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
|
|
463
|
+
* Bucketing itself is binary (babylon-mmd parity): ANY translucent coverage
|
|
464
|
+
* routes to the alpha-blend bucket. `avg` below this threshold additionally
|
|
465
|
+
* marks a material as fully sheer (a veil), which vetoes shadow casting. */
|
|
453
466
|
const SHEER_ALPHA_THRESHOLD = 0.7
|
|
454
|
-
function
|
|
467
|
+
function materialAlphaStats(
|
|
455
468
|
verts: Float32Array,
|
|
456
469
|
indices: Uint32Array,
|
|
457
470
|
firstIndex: number,
|
|
458
471
|
count: number,
|
|
459
472
|
sampler: { a: Uint8ClampedArray; w: number; h: number } | null | undefined,
|
|
460
|
-
):
|
|
461
|
-
if (!sampler) return
|
|
473
|
+
): { avg: number; translucentFrac: number } {
|
|
474
|
+
if (!sampler) return { avg: 1, translucentFrac: 0 }
|
|
462
475
|
const triCount = Math.floor(count / 3)
|
|
463
|
-
if (triCount === 0) return
|
|
476
|
+
if (triCount === 0) return { avg: 1, translucentFrac: 0 }
|
|
464
477
|
const step = Math.max(1, Math.floor(triCount / 400))
|
|
465
478
|
let sum = 0
|
|
479
|
+
let translucent = 0
|
|
466
480
|
let n = 0
|
|
467
481
|
for (let t = 0; t < triCount; t += step) {
|
|
468
482
|
const i0 = indices[firstIndex + t * 3]
|
|
@@ -473,10 +487,13 @@ function materialIsSheer(
|
|
|
473
487
|
// Wrap (MMD UVs may tile), then nearest-sample the downsampled plane.
|
|
474
488
|
const x = Math.min(sampler.w - 1, Math.max(0, Math.floor((u - Math.floor(u)) * sampler.w)))
|
|
475
489
|
const y = Math.min(sampler.h - 1, Math.max(0, Math.floor((v - Math.floor(v)) * sampler.h)))
|
|
476
|
-
|
|
490
|
+
const a = sampler.a[y * sampler.w + x]
|
|
491
|
+
sum += a
|
|
492
|
+
if (a > 8 && a < 247) translucent++
|
|
477
493
|
n++
|
|
478
494
|
}
|
|
479
|
-
|
|
495
|
+
if (n === 0) return { avg: 1, translucentFrac: 0 }
|
|
496
|
+
return { avg: sum / n / 255, translucentFrac: translucent / n }
|
|
480
497
|
}
|
|
481
498
|
|
|
482
499
|
export class Engine {
|
|
@@ -509,6 +526,8 @@ export class Engine {
|
|
|
509
526
|
// The one base shading model: ungrouped materials render this (compiled DEFAULT_GRAPH).
|
|
510
527
|
// Grouped materials use their group's own compiled pipeline.
|
|
511
528
|
private neutralPipeline!: GPURenderPipeline
|
|
529
|
+
private neutralPipelineNoDepthWrite!: GPURenderPipeline
|
|
530
|
+
private transparentDepthPrepassPipeline!: GPURenderPipeline
|
|
512
531
|
// ── Style group runtime ──
|
|
513
532
|
// Shared 256 B zero StyleUniforms buffer (group(2) binding(4)) bound by every ungrouped
|
|
514
533
|
// material; grouped materials rebind to their group's own buffer (per-model, in the
|
|
@@ -897,6 +916,17 @@ export class Engine {
|
|
|
897
916
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
898
917
|
}
|
|
899
918
|
|
|
919
|
+
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
920
|
+
// OFF by default — the product aesthetic. Modern high-detail models read
|
|
921
|
+
// better without hulls (babylon-mmd's own demos disable its outline renderer
|
|
922
|
+
// too), and no hull pass means no depth-tie edge cases against near-coplanar
|
|
923
|
+
// cloth. The full MMD-faithful machinery (interleaved per-material hulls,
|
|
924
|
+
// texture-alpha-modulated rims) stays in place behind setOutlineEnabled(true).
|
|
925
|
+
private outlineEnabled = false
|
|
926
|
+
setOutlineEnabled(on: boolean): void {
|
|
927
|
+
this.outlineEnabled = on
|
|
928
|
+
}
|
|
929
|
+
|
|
900
930
|
private rebuildCompositeBindGroup(): void {
|
|
901
931
|
if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView) return
|
|
902
932
|
this.compositeBindGroup = this.device.createBindGroup({
|
|
@@ -1468,6 +1498,8 @@ export class Engine {
|
|
|
1468
1498
|
attributes: [
|
|
1469
1499
|
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
1470
1500
|
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
1501
|
+
// uv — the outline FS alpha-tests the diffuse texture (babylon-mmd parity)
|
|
1502
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
1471
1503
|
],
|
|
1472
1504
|
},
|
|
1473
1505
|
{
|
|
@@ -1600,6 +1632,44 @@ export class Engine {
|
|
|
1600
1632
|
depthCompare: "less-equal",
|
|
1601
1633
|
},
|
|
1602
1634
|
})
|
|
1635
|
+
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
1636
|
+
this.neutralPipelineNoDepthWrite = this.createRenderPipeline({
|
|
1637
|
+
label: "neutral base pipeline (no depth write)",
|
|
1638
|
+
layout: mainPipelineLayout,
|
|
1639
|
+
shaderModule: neutralModule,
|
|
1640
|
+
vertexBuffers: fullVertexBuffers,
|
|
1641
|
+
fragmentTargets: sceneTargets,
|
|
1642
|
+
cullMode: "none",
|
|
1643
|
+
depthStencil: {
|
|
1644
|
+
format: "depth24plus-stencil8",
|
|
1645
|
+
depthWriteEnabled: false,
|
|
1646
|
+
depthCompare: "less-equal",
|
|
1647
|
+
},
|
|
1648
|
+
})
|
|
1649
|
+
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
1650
|
+
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
1651
|
+
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
1652
|
+
const prepassModule = this.device.createShaderModule({
|
|
1653
|
+
label: "transparent depth prepass",
|
|
1654
|
+
code: TRANSPARENT_DEPTH_PREPASS_WGSL,
|
|
1655
|
+
})
|
|
1656
|
+
this.transparentDepthPrepassPipeline = this.device.createRenderPipeline({
|
|
1657
|
+
label: "transparent depth prepass",
|
|
1658
|
+
layout: mainPipelineLayout,
|
|
1659
|
+
vertex: { module: prepassModule, entryPoint: "vs", buffers: fullVertexBuffers as GPUVertexBufferLayout[] },
|
|
1660
|
+
fragment: {
|
|
1661
|
+
module: prepassModule,
|
|
1662
|
+
entryPoint: "fs",
|
|
1663
|
+
targets: sceneTargets.map((t) => ({ format: (t as GPUColorTargetState).format, writeMask: 0 })),
|
|
1664
|
+
},
|
|
1665
|
+
primitive: { cullMode: "none" },
|
|
1666
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
1667
|
+
depthStencil: {
|
|
1668
|
+
format: "depth24plus-stencil8",
|
|
1669
|
+
depthWriteEnabled: true,
|
|
1670
|
+
depthCompare: "less-equal",
|
|
1671
|
+
},
|
|
1672
|
+
})
|
|
1603
1673
|
|
|
1604
1674
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
1605
1675
|
size: 64,
|
|
@@ -1695,6 +1765,7 @@ export class Engine {
|
|
|
1695
1765
|
label: "outline per-frame bind group layout",
|
|
1696
1766
|
entries: [
|
|
1697
1767
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1768
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
1698
1769
|
],
|
|
1699
1770
|
})
|
|
1700
1771
|
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
@@ -1702,6 +1773,7 @@ export class Engine {
|
|
|
1702
1773
|
label: "outline per-material bind group layout",
|
|
1703
1774
|
entries: [
|
|
1704
1775
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1776
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1705
1777
|
],
|
|
1706
1778
|
})
|
|
1707
1779
|
|
|
@@ -1717,7 +1789,10 @@ export class Engine {
|
|
|
1717
1789
|
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
1718
1790
|
label: "outline per-frame bind group",
|
|
1719
1791
|
layout: this.outlinePerFrameBindGroupLayout,
|
|
1720
|
-
entries: [
|
|
1792
|
+
entries: [
|
|
1793
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1794
|
+
{ binding: 1, resource: this.materialSampler },
|
|
1795
|
+
],
|
|
1721
1796
|
})
|
|
1722
1797
|
|
|
1723
1798
|
const outlineShaderModule = this.device.createShaderModule({
|
|
@@ -1734,9 +1809,21 @@ export class Engine {
|
|
|
1734
1809
|
cullMode: "back",
|
|
1735
1810
|
depthStencil: {
|
|
1736
1811
|
format: "depth24plus-stencil8",
|
|
1737
|
-
//
|
|
1738
|
-
|
|
1812
|
+
// babylon-mmd draws outlines WITH depth write (its _afterRenderingMesh
|
|
1813
|
+
// forces setDepthWrite(true)); the constant bias below still makes
|
|
1814
|
+
// hulls lose depth ties against their own near-coplanar geometry.
|
|
1815
|
+
depthWriteEnabled: true,
|
|
1739
1816
|
depthCompare: "less-equal",
|
|
1817
|
+
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1818
|
+
// carry their surface's exact depth, so against this model's paired
|
|
1819
|
+
// near-coplanar skirt layers the hulls WON depth ties in patches —
|
|
1820
|
+
// the black shapes on the dress. A small constant bias makes hulls lose
|
|
1821
|
+
// every tie; silhouette rims compare against the far background and are
|
|
1822
|
+
// unaffected. No slope term — slope explodes at silhouettes and would
|
|
1823
|
+
// erase the rims themselves (previous regression).
|
|
1824
|
+
depthBias: 4,
|
|
1825
|
+
depthBiasSlopeScale: 0,
|
|
1826
|
+
depthBiasClamp: 0,
|
|
1740
1827
|
// Skip fragments where the eye stamped stencil=EYE_VALUE. Those pixels are owned by
|
|
1741
1828
|
// the see-through-hair blend (hair-over-eyes), so letting the outline's near-black
|
|
1742
1829
|
// edge color overwrite them would re-introduce the dark almond we just killed.
|
|
@@ -3482,18 +3569,27 @@ export class Engine {
|
|
|
3482
3569
|
}
|
|
3483
3570
|
|
|
3484
3571
|
const materialAlpha = mat.diffuse[3]
|
|
3485
|
-
// Transparent bucket when the MATERIAL says so — or when the TEXTURE does
|
|
3486
|
-
// (sheer cloth almost always ships with diffuse alpha 1.0 and carries its
|
|
3487
|
-
// translucency in texture alpha). Transparent-bucket draws happen after
|
|
3488
|
-
// the opaque bucket (and after the late-drawn hair render-class), so a
|
|
3489
|
-
// veil composites over the hair behind it instead of depth-rejecting it;
|
|
3490
|
-
// they are also excluded from the shadow map, so sheer cloth stops
|
|
3491
|
-
// casting the solid shadow of an opaque sheet.
|
|
3492
3572
|
const diffusePath = texLogicalPath(mat.diffuseTextureIndex)
|
|
3493
3573
|
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3574
|
+
const stats = materialAlphaStats(meshVertices, meshIndices, currentIndexOffset, indexCount, alphaSampler)
|
|
3575
|
+
// babylon-mmd parity (its default DepthWriteAlphaBlendingWithEvaluation
|
|
3576
|
+
// method): the bucket decision is BINARY. A material with ANY translucent
|
|
3577
|
+
// texels on its geometry is alpha-blend — drawn in PMX author order with
|
|
3578
|
+
// depth write ON (forceDepthWrite); everything else is opaque. The old
|
|
3579
|
+
// avg/frac tier system left mostly-opaque lace (translucentFrac 0.09) in
|
|
3580
|
+
// the opaque bucket while its sibling panels went transparent, breaking
|
|
3581
|
+
// the author's compositing order — the gray fold patches. The 2% floor
|
|
3582
|
+
// only guards against centroid-sampling noise on genuinely solid cloth.
|
|
3583
|
+
const sheer = stats.avg < SHEER_ALPHA_THRESHOLD
|
|
3584
|
+
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || stats.translucentFrac > 0.02
|
|
3585
|
+
// Shadow casting: the PMX author's own flag (bit 0x04, cast self-shadow),
|
|
3586
|
+
// still vetoed for fully sheer cloth — a veil must not cast a solid sheet.
|
|
3587
|
+
const castsShadow = (mat.edgeFlag & 0x04) !== 0 && !sheer
|
|
3588
|
+
// Load-time classification log — one line per material, cheap and
|
|
3589
|
+
// invaluable when a model renders wrong (bucket/outline/shadow disputes).
|
|
3590
|
+
console.info(
|
|
3591
|
+
`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)} translucentFrac=${stats.translucentFrac.toFixed(2)} bucket=${isTransparent ? "transparent" : "opaque"} castsShadow=${castsShadow} edge=${(mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 ? "on" : "off"}`,
|
|
3592
|
+
)
|
|
3497
3593
|
|
|
3498
3594
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
3499
3595
|
// rare and not implemented — treated as none, like a failed load.
|
|
@@ -3531,17 +3627,12 @@ export class Engine {
|
|
|
3531
3627
|
this.zeroStyleBuffer,
|
|
3532
3628
|
)
|
|
3533
3629
|
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
materialName: mat.name,
|
|
3541
|
-
groupId: null,
|
|
3542
|
-
baseBindGroupEntries,
|
|
3543
|
-
})
|
|
3544
|
-
|
|
3630
|
+
// Inverted-hull outline for EVERY edge-flagged material (PMX bit 0x10) —
|
|
3631
|
+
// the outline FS alpha-tests the diffuse texture, so sheer fabric masks
|
|
3632
|
+
// its own hull where it is see-through instead of us skipping it here.
|
|
3633
|
+
// Drawn interleaved right after this material's color draw (babylon-mmd's
|
|
3634
|
+
// per-mesh afterRender outline stage) — see drawMaterials.
|
|
3635
|
+
let outline: DrawCall["outline"]
|
|
3545
3636
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
3546
3637
|
const materialUniformData = new Float32Array([
|
|
3547
3638
|
mat.edgeColor[0],
|
|
@@ -3558,19 +3649,27 @@ export class Engine {
|
|
|
3558
3649
|
const outlineBindGroup = this.device.createBindGroup({
|
|
3559
3650
|
label: `${prefix}outline: ${mat.name}`,
|
|
3560
3651
|
layout: this.outlinePerMaterialBindGroupLayout,
|
|
3561
|
-
entries: [
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
type: outlineType,
|
|
3566
|
-
count: indexCount,
|
|
3567
|
-
firstIndex: currentIndexOffset,
|
|
3568
|
-
bindGroup: outlineBindGroup,
|
|
3569
|
-
materialName: mat.name,
|
|
3570
|
-
groupId: null,
|
|
3652
|
+
entries: [
|
|
3653
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
3654
|
+
{ binding: 1, resource: textureView },
|
|
3655
|
+
],
|
|
3571
3656
|
})
|
|
3657
|
+
outline = { bindGroup: outlineBindGroup }
|
|
3572
3658
|
}
|
|
3573
3659
|
|
|
3660
|
+
const type: DrawCallType = isTransparent ? "transparent" : "opaque"
|
|
3661
|
+
inst.drawCalls.push({
|
|
3662
|
+
type,
|
|
3663
|
+
count: indexCount,
|
|
3664
|
+
firstIndex: currentIndexOffset,
|
|
3665
|
+
bindGroup,
|
|
3666
|
+
materialName: mat.name,
|
|
3667
|
+
groupId: null,
|
|
3668
|
+
baseBindGroupEntries,
|
|
3669
|
+
castsShadow,
|
|
3670
|
+
outline,
|
|
3671
|
+
})
|
|
3672
|
+
|
|
3574
3673
|
if (this.onRaycast) {
|
|
3575
3674
|
const pickIdData = new Float32Array([modelId, materialId, 0, 0])
|
|
3576
3675
|
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData)
|
|
@@ -4419,11 +4518,22 @@ export class Engine {
|
|
|
4419
4518
|
}
|
|
4420
4519
|
|
|
4421
4520
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
4521
|
+
// Phase order: opaque models → ground → transparent fabric.
|
|
4522
|
+
// The ground shader is the most expensive full-coverage draw in the frame
|
|
4523
|
+
// (9-tap PCF on the 4096² shadow map per pixel), so it draws AFTER the
|
|
4524
|
+
// opaque phase to get early-z rejected behind the body — drawing it first
|
|
4525
|
+
// shaded every covered pixel and measurably dropped Safari fps. It still
|
|
4526
|
+
// draws BEFORE the transparent phase so sheer fabric blends over the floor
|
|
4527
|
+
// instead of over the background with the floor depth-rejected behind it.
|
|
4422
4528
|
if (hasModels)
|
|
4423
4529
|
this.forEachInstance((inst) => {
|
|
4424
|
-
if (inst.model.visible) this.
|
|
4530
|
+
if (inst.model.visible) this.renderModelOpaquePhase(pass, inst)
|
|
4425
4531
|
})
|
|
4426
4532
|
if (this.hasGround) this.renderGround(pass)
|
|
4533
|
+
if (hasModels)
|
|
4534
|
+
this.forEachInstance((inst) => {
|
|
4535
|
+
if (inst.model.visible) this.renderModelTransparentPhase(pass, inst)
|
|
4536
|
+
})
|
|
4427
4537
|
pass.end()
|
|
4428
4538
|
|
|
4429
4539
|
// Bloom pyramid (EEVEE 3.6):
|
|
@@ -4711,9 +4821,12 @@ export class Engine {
|
|
|
4711
4821
|
}
|
|
4712
4822
|
|
|
4713
4823
|
let pipeline: GPURenderPipeline
|
|
4824
|
+
let pipelineNoDepthWrite: GPURenderPipeline
|
|
4714
4825
|
let overEyesPipeline: GPURenderPipeline | undefined
|
|
4715
4826
|
try {
|
|
4716
4827
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false)
|
|
4828
|
+
// Dormant OIT twin — kept for a future order-independent-transparency path.
|
|
4829
|
+
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false)
|
|
4717
4830
|
if (renderClass === "hair") overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true)
|
|
4718
4831
|
} catch (e) {
|
|
4719
4832
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${(e as Error).message}` })
|
|
@@ -4739,6 +4852,7 @@ export class Engine {
|
|
|
4739
4852
|
renderClass,
|
|
4740
4853
|
alphaMode,
|
|
4741
4854
|
pipeline,
|
|
4855
|
+
pipelineNoDepthWrite,
|
|
4742
4856
|
overEyesPipeline,
|
|
4743
4857
|
uniformBuffer,
|
|
4744
4858
|
slotMap: result.slotMap,
|
|
@@ -4802,7 +4916,9 @@ export class Engine {
|
|
|
4802
4916
|
inst.drawCalls.sort(
|
|
4803
4917
|
(a, b) => typeOrder[a.type] - typeOrder[b.type] || this.drawCallRank(inst, a) - this.drawCallRank(inst, b),
|
|
4804
4918
|
)
|
|
4805
|
-
inst.shadowDrawCalls = inst.drawCalls.filter(
|
|
4919
|
+
inst.shadowDrawCalls = inst.drawCalls.filter(
|
|
4920
|
+
(d) => (d.type === "opaque" || d.type === "transparent") && d.castsShadow === true,
|
|
4921
|
+
)
|
|
4806
4922
|
}
|
|
4807
4923
|
|
|
4808
4924
|
/**
|
|
@@ -4814,6 +4930,7 @@ export class Engine {
|
|
|
4814
4930
|
renderClass: RenderClass,
|
|
4815
4931
|
module: GPUShaderModule,
|
|
4816
4932
|
overEyes: boolean,
|
|
4933
|
+
depthWrite = true,
|
|
4817
4934
|
): Promise<GPURenderPipeline> {
|
|
4818
4935
|
const base = {
|
|
4819
4936
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
@@ -4824,7 +4941,7 @@ export class Engine {
|
|
|
4824
4941
|
}
|
|
4825
4942
|
const plainDepth: GPUDepthStencilState = {
|
|
4826
4943
|
format: "depth24plus-stencil8",
|
|
4827
|
-
depthWriteEnabled:
|
|
4944
|
+
depthWriteEnabled: depthWrite,
|
|
4828
4945
|
depthCompare: "less-equal",
|
|
4829
4946
|
}
|
|
4830
4947
|
let depthStencil: GPUDepthStencilState = plainDepth
|
|
@@ -4868,7 +4985,9 @@ export class Engine {
|
|
|
4868
4985
|
}
|
|
4869
4986
|
|
|
4870
4987
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4871
|
-
// the neutral base (ungrouped materials render the default graph).
|
|
4988
|
+
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
4989
|
+
// draws use the SAME depth-write-on pipeline — babylon-mmd's forceDepthWrite
|
|
4990
|
+
// blending (see renderModelTransparentPhase for the trade-off record).
|
|
4872
4991
|
private pipelineForDrawCall(inst: ModelInstance, dc: DrawCall): GPURenderPipeline {
|
|
4873
4992
|
if (dc.groupId) {
|
|
4874
4993
|
const install = inst.styleGroups.get(dc.groupId)
|
|
@@ -4879,9 +4998,10 @@ export class Engine {
|
|
|
4879
4998
|
|
|
4880
4999
|
/**
|
|
4881
5000
|
* Draw every material of a given type (`opaque` or `transparent`) using the main
|
|
4882
|
-
* pipeline(s)
|
|
4883
|
-
*
|
|
4884
|
-
*
|
|
5001
|
+
* pipeline(s), and — babylon-mmd's per-mesh outline stage — each edge-flagged
|
|
5002
|
+
* material's inverted hull IMMEDIATELY after its color draw. Interleaving is what
|
|
5003
|
+
* makes outlines compose like MMD: every material drawn later in the author's
|
|
5004
|
+
* order covers earlier hulls, and each hull sits over everything drawn before it.
|
|
4885
5005
|
*/
|
|
4886
5006
|
private drawMaterials(pass: GPURenderPassEncoder, inst: ModelInstance, type: "opaque" | "transparent"): void {
|
|
4887
5007
|
let currentPipeline: GPURenderPipeline | null = null
|
|
@@ -4900,51 +5020,72 @@ export class Engine {
|
|
|
4900
5020
|
}
|
|
4901
5021
|
pass.setBindGroup(2, draw.bindGroup)
|
|
4902
5022
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
* Draw every outline of a given type (`opaque-outline` or `transparent-outline`).
|
|
4908
|
-
* Uses its own pipeline layout (group 0 = camera-only, group 2 = edge uniforms), so
|
|
4909
|
-
* every batch binds its own groups from scratch — the next drawMaterials call will
|
|
4910
|
-
* rebind group 0/1 correctly if needed.
|
|
4911
|
-
*/
|
|
4912
|
-
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, type: DrawCallType): void {
|
|
4913
|
-
let bound = false
|
|
4914
|
-
for (const draw of inst.drawCalls) {
|
|
4915
|
-
if (draw.type !== type || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
4916
|
-
if (!bound) {
|
|
5023
|
+
if (draw.outline && this.outlineEnabled) {
|
|
5024
|
+
// Same index range; own pipeline + groups 0/2. Group 1 (skinMats) is
|
|
5025
|
+
// layout-identical between the main and outline pipelines and stays
|
|
5026
|
+
// bound. Restore group 0 afterwards and force a pipeline re-set.
|
|
4917
5027
|
pass.setPipeline(this.outlinePipeline)
|
|
4918
5028
|
pass.setBindGroup(0, this.outlinePerFrameBindGroup)
|
|
4919
|
-
pass.setBindGroup(
|
|
4920
|
-
|
|
5029
|
+
pass.setBindGroup(2, draw.outline.bindGroup)
|
|
5030
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5031
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
5032
|
+
currentPipeline = null
|
|
4921
5033
|
}
|
|
4922
|
-
pass.setBindGroup(2, draw.bindGroup)
|
|
4923
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
4924
5034
|
}
|
|
4925
5035
|
}
|
|
4926
5036
|
|
|
4927
5037
|
/**
|
|
4928
|
-
* Main-pass render sequence for one model instance:
|
|
4929
|
-
*
|
|
4930
|
-
*
|
|
4931
|
-
*
|
|
5038
|
+
* Main-pass render sequence for one model instance — babylon-mmd parity:
|
|
5039
|
+
* opaque bucket, the hair-over-eyes stencil pass, then alpha-blend materials
|
|
5040
|
+
* in PMX author order with depth write ON (forceDepthWrite). Outlines are not
|
|
5041
|
+
* a separate phase: drawMaterials draws each edge-flagged material's hull
|
|
5042
|
+
* right after the material itself, like MMD's per-mesh outline stage.
|
|
4932
5043
|
*/
|
|
4933
|
-
private
|
|
5044
|
+
private setModelDrawState(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
4934
5045
|
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
4935
5046
|
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
4936
5047
|
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
4937
5048
|
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
4938
|
-
|
|
4939
5049
|
// Single stencil-reference set covers eye (write), hair (read not-equal),
|
|
4940
5050
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
4941
5051
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
5052
|
+
}
|
|
4942
5053
|
|
|
5054
|
+
private renderModelOpaquePhase(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5055
|
+
this.setModelDrawState(pass, inst)
|
|
4943
5056
|
this.drawMaterials(pass, inst, "opaque")
|
|
4944
|
-
this.drawOutlines(pass, inst, "opaque-outline")
|
|
4945
5057
|
this.drawHairOverEyes(pass, inst)
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
private renderModelTransparentPhase(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5061
|
+
this.setModelDrawState(pass, inst)
|
|
5062
|
+
// Transparent: babylon-mmd's forceDepthWrite blending — PMX author order
|
|
5063
|
+
// with depth write ON. The accepted trade-off after trying every variant:
|
|
5064
|
+
// · depth-write ON (this): a fold hides its far side; rare view-dependent
|
|
5065
|
+
// double-blend seams at some angles. MMD's own known behavior.
|
|
5066
|
+
// · nearest-surface prepass: view-independent, but punched see-through
|
|
5067
|
+
// holes to whatever sat far behind a fold.
|
|
5068
|
+
// · depth-write OFF layering: every overlap visible everywhere — MORE
|
|
5069
|
+
// gray patches and texture artifacts in practice.
|
|
4946
5070
|
this.drawMaterials(pass, inst, "transparent")
|
|
4947
|
-
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
5074
|
+
* Dormant — kept for a future order-independent-transparency path. */
|
|
5075
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5076
|
+
protected drawTransparentDepthPrepass(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5077
|
+
let bound = false
|
|
5078
|
+
for (const draw of inst.drawCalls) {
|
|
5079
|
+
if (draw.type !== "transparent" || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
5080
|
+
if (!bound) {
|
|
5081
|
+
pass.setPipeline(this.transparentDepthPrepassPipeline)
|
|
5082
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
5083
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
5084
|
+
bound = true
|
|
5085
|
+
}
|
|
5086
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
5087
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5088
|
+
}
|
|
4948
5089
|
}
|
|
4949
5090
|
|
|
4950
5091
|
/**
|
|
@@ -4991,6 +5132,10 @@ export class Engine {
|
|
|
4991
5132
|
this.cameraMatrixData[32] = cameraPos.x
|
|
4992
5133
|
this.cameraMatrixData[33] = cameraPos.y
|
|
4993
5134
|
this.cameraMatrixData[34] = cameraPos.z
|
|
5135
|
+
// Spare float after viewPos: render-target height in device px — the outline
|
|
5136
|
+
// shader derives the full viewport (width via projection aspect) for its
|
|
5137
|
+
// babylon-mmd constant-pixel edge extrusion.
|
|
5138
|
+
this.cameraMatrixData[35] = this.canvas.height
|
|
4994
5139
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
4995
5140
|
|
|
4996
5141
|
// 360 backdrop: the composite reconstructs each pixel's view ray from the
|
package/src/graph/slots.ts
CHANGED
|
@@ -76,14 +76,23 @@ function prelude(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
|
76
76
|
? " if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }"
|
|
77
77
|
: " if (alpha < 0.001) { discard; }"
|
|
78
78
|
const gate = renderClass === "eye" ? EYE_REAR_GATE : ""
|
|
79
|
+
// Double-sided shading, winding-independent: a normal pointing away from the
|
|
80
|
+
// camera means we're seeing the surface's other side — flip it. Only genuinely
|
|
81
|
+
// camera-averted fragments change (front surfaces have dot(n,v) > 0 and are
|
|
82
|
+
// untouched), unlike a front_facing test, which PMX's clockwise winding
|
|
83
|
+
// inverts. Fixes inner skirt linings (裙子白1-style flipped-normal copies)
|
|
84
|
+
// shading near-black through sheer outer layers. Eye is exempt: it FRONT-culls
|
|
85
|
+
// by design, so its visible fragments are back faces with intended normals.
|
|
86
|
+
const flip =
|
|
87
|
+
renderClass === "eye" ? "" : "\n n = select(-n, n, dot(n, v) >= 0.0);"
|
|
79
88
|
return `@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
80
89
|
let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
81
90
|
// MMD alpha semantics: material alpha × texture alpha.
|
|
82
91
|
let alpha = material.alpha * tex_s.a;
|
|
83
92
|
${discard}
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
let v = normalize(camera.viewPos - input.worldPos)
|
|
94
|
+
var n = safe_normal(input.normal);
|
|
95
|
+
let v = normalize(camera.viewPos - input.worldPos);${flip}
|
|
87
96
|
${gate}
|
|
88
97
|
let l = -light.lights[0].direction.xyz;
|
|
89
98
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|