reze-engine 0.25.1 → 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/dist/engine.d.ts +20 -18
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +113 -100
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +18 -3
- 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 +119 -102
- package/src/shaders/passes/composite.ts +23 -3
- package/src/shaders/passes/outline.ts +50 -23
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
// MMD-style
|
|
2
|
-
//
|
|
1
|
+
// MMD-style inverted-hull outline, ported from babylon-mmd's mmdOutline shader
|
|
2
|
+
// (the reference implementation whose output matches MMD):
|
|
3
|
+
// 1. Extrude along the VIEW-SPACE normal's XY, normalized — a pure screen
|
|
4
|
+
// direction, so rims never smear toward the camera at grazing angles.
|
|
5
|
+
// 2. Offset in clip space by edgeSize · 4/viewport · w. The ×w cancels the
|
|
6
|
+
// perspective divide → CONSTANT screen thickness of exactly
|
|
7
|
+
// 2·edgeSize device pixels (babylon: `screenNormal / (viewport*0.25) *
|
|
8
|
+
// offset * projectedPosition.w`). PMX edgeSize ~0.3–1.0 ⇒ fine 0.6–2px
|
|
9
|
+
// rims, matching MMD instead of our former chunky constants.
|
|
10
|
+
// 3. The FRAGMENT stage samples the material's own diffuse texture and
|
|
11
|
+
// MODULATES the rim's alpha by it (discarding only near-zero cut-out
|
|
12
|
+
// margins) — sheer fabric gets a proportional rim, never a solid black
|
|
13
|
+
// hull, without dropping the author's edge flag.
|
|
3
14
|
export const OUTLINE_SHADER_WGSL = /* wgsl */ `
|
|
4
15
|
struct CameraUniforms {
|
|
5
16
|
view: mat4x4f,
|
|
6
17
|
projection: mat4x4f,
|
|
7
18
|
viewPos: vec3f,
|
|
8
|
-
|
|
19
|
+
// Render-target height in device pixels (engine writes it each frame);
|
|
20
|
+
// width is recovered via the projection matrix's aspect.
|
|
21
|
+
viewportHeight: f32,
|
|
9
22
|
};
|
|
10
23
|
|
|
11
24
|
struct MaterialUniforms {
|
|
@@ -17,16 +30,20 @@ struct MaterialUniforms {
|
|
|
17
30
|
};
|
|
18
31
|
|
|
19
32
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
33
|
+
@group(0) @binding(1) var edgeSampler: sampler;
|
|
20
34
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
21
35
|
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
|
|
36
|
+
@group(2) @binding(1) var diffuseTexture: texture_2d<f32>;
|
|
22
37
|
|
|
23
38
|
struct VertexOutput {
|
|
24
39
|
@builtin(position) position: vec4f,
|
|
40
|
+
@location(0) uv: vec2f,
|
|
25
41
|
};
|
|
26
42
|
|
|
27
43
|
@vertex fn vs(
|
|
28
44
|
@location(0) position: vec3f,
|
|
29
45
|
@location(1) normal: vec3f,
|
|
46
|
+
@location(2) uv: vec2f,
|
|
30
47
|
@location(3) joints0: vec4<u32>,
|
|
31
48
|
@location(4) weights0: vec4<f32>
|
|
32
49
|
) -> VertexOutput {
|
|
@@ -50,33 +67,43 @@ struct VertexOutput {
|
|
|
50
67
|
let worldPos = skinnedPos.xyz;
|
|
51
68
|
let worldNormal = normalize(skinnedNrm);
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// projection is column-major: proj[0][0] = 1/(aspect·tan(fov/2)), proj[1][1] = 1/tan(fov/2).
|
|
66
|
-
// Ratio proj[1][1]/proj[0][0] recovers the viewport aspect (width/height).
|
|
70
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
71
|
+
|
|
72
|
+
// babylon-mmd: screenNormal = normalize((view * worldNormal).xy)
|
|
73
|
+
let viewNormal = (camera.view * vec4f(worldNormal, 0.0)).xyz;
|
|
74
|
+
let snLen = length(viewNormal.xy);
|
|
75
|
+
let screenNormal = select(vec2f(0.0, 0.0), viewNormal.xy / snLen, snLen > 1e-5);
|
|
76
|
+
|
|
77
|
+
// Reference-height normalization (babylon-mmd ships this variant commented
|
|
78
|
+
// out as \`renderHeight = 1080\`): thickness is a constant FRACTION of the
|
|
79
|
+
// frame — 2·edgeSize px at 1080p — so retina DPR and 4K export don't thin
|
|
80
|
+
// the rims to sub-pixel. Width follows the projection aspect.
|
|
81
|
+
// projection[1][1]/projection[0][0] = width/height for a symmetric frustum.
|
|
67
82
|
let aspect = camera.projection[1][1] / camera.projection[0][0];
|
|
68
|
-
let
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
let viewport = vec2f(1080.0 * aspect, 1080.0);
|
|
84
|
+
|
|
85
|
+
// NDC offset = edgeSize · 4/viewport, ×w so the perspective divide cancels:
|
|
86
|
+
// constant screen thickness at any distance (babylon-mmd parity).
|
|
87
|
+
let offset = screenNormal * (material.edgeSize * 4.0 / viewport) * clipPos.w;
|
|
72
88
|
output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
|
|
89
|
+
output.uv = uv;
|
|
73
90
|
return output;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
77
|
-
@fragment fn fs() -> FSOut {
|
|
94
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
95
|
+
// Rim alpha FOLLOWS the fabric's texture alpha instead of a hard alpha test:
|
|
96
|
+
// MMD draws blend-material edges solid (only cutout materials alpha-test), so
|
|
97
|
+
// a 0.4 discard erased the whole hull on semi-transparent cloth — stockinged
|
|
98
|
+
// legs crossing lost their outline entirely. Modulating instead keeps a
|
|
99
|
+
// proportional rim on sheer weave (never a solid black hull) and still
|
|
100
|
+
// discards true cut-out margins like hair-card borders.
|
|
101
|
+
let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
|
|
102
|
+
if (texA < 0.05) {
|
|
103
|
+
discard;
|
|
104
|
+
}
|
|
78
105
|
var out: FSOut;
|
|
79
|
-
out.color = material.edgeColor;
|
|
106
|
+
out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);
|
|
80
107
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
81
108
|
return out;
|
|
82
109
|
}
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -173,7 +173,7 @@ type GroupInstall = {
|
|
|
173
173
|
renderClass: RenderClass
|
|
174
174
|
alphaMode: AlphaMode
|
|
175
175
|
pipeline: GPURenderPipeline
|
|
176
|
-
/** Depth-write-off twin
|
|
176
|
+
/** Depth-write-off twin — dormant, kept for a future OIT path. */
|
|
177
177
|
pipelineNoDepthWrite: GPURenderPipeline
|
|
178
178
|
/** hair render-class only: the stencil-matched IS_OVER_EYES=true variant. */
|
|
179
179
|
overEyesPipeline?: GPURenderPipeline
|
|
@@ -359,6 +359,10 @@ interface DrawCall {
|
|
|
359
359
|
baseBindGroupEntries?: GPUBindGroupEntry[]
|
|
360
360
|
/** Material draws only: false = excluded from the shadow map (fully sheer). */
|
|
361
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 }
|
|
362
366
|
}
|
|
363
367
|
|
|
364
368
|
interface PickDrawCall {
|
|
@@ -456,14 +460,10 @@ function buildAlphaSampler(
|
|
|
456
460
|
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
457
461
|
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
458
462
|
* neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
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. */
|
|
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. */
|
|
465
466
|
const SHEER_ALPHA_THRESHOLD = 0.7
|
|
466
|
-
const PARTIAL_TRANSLUCENT_FRAC = 0.15
|
|
467
467
|
function materialAlphaStats(
|
|
468
468
|
verts: Float32Array,
|
|
469
469
|
indices: Uint32Array,
|
|
@@ -917,7 +917,12 @@ export class Engine {
|
|
|
917
917
|
}
|
|
918
918
|
|
|
919
919
|
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
920
|
-
|
|
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
|
|
921
926
|
setOutlineEnabled(on: boolean): void {
|
|
922
927
|
this.outlineEnabled = on
|
|
923
928
|
}
|
|
@@ -1493,6 +1498,8 @@ export class Engine {
|
|
|
1493
1498
|
attributes: [
|
|
1494
1499
|
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
1495
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 },
|
|
1496
1503
|
],
|
|
1497
1504
|
},
|
|
1498
1505
|
{
|
|
@@ -1758,6 +1765,7 @@ export class Engine {
|
|
|
1758
1765
|
label: "outline per-frame bind group layout",
|
|
1759
1766
|
entries: [
|
|
1760
1767
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1768
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
1761
1769
|
],
|
|
1762
1770
|
})
|
|
1763
1771
|
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
@@ -1765,6 +1773,7 @@ export class Engine {
|
|
|
1765
1773
|
label: "outline per-material bind group layout",
|
|
1766
1774
|
entries: [
|
|
1767
1775
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1776
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1768
1777
|
],
|
|
1769
1778
|
})
|
|
1770
1779
|
|
|
@@ -1780,7 +1789,10 @@ export class Engine {
|
|
|
1780
1789
|
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
1781
1790
|
label: "outline per-frame bind group",
|
|
1782
1791
|
layout: this.outlinePerFrameBindGroupLayout,
|
|
1783
|
-
entries: [
|
|
1792
|
+
entries: [
|
|
1793
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1794
|
+
{ binding: 1, resource: this.materialSampler },
|
|
1795
|
+
],
|
|
1784
1796
|
})
|
|
1785
1797
|
|
|
1786
1798
|
const outlineShaderModule = this.device.createShaderModule({
|
|
@@ -1797,8 +1809,10 @@ export class Engine {
|
|
|
1797
1809
|
cullMode: "back",
|
|
1798
1810
|
depthStencil: {
|
|
1799
1811
|
format: "depth24plus-stencil8",
|
|
1800
|
-
//
|
|
1801
|
-
|
|
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,
|
|
1802
1816
|
depthCompare: "less-equal",
|
|
1803
1817
|
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1804
1818
|
// carry their surface's exact depth, so against this model's paired
|
|
@@ -3555,28 +3569,26 @@ export class Engine {
|
|
|
3555
3569
|
}
|
|
3556
3570
|
|
|
3557
3571
|
const materialAlpha = mat.diffuse[3]
|
|
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
3572
|
const diffusePath = texLogicalPath(mat.diffuseTextureIndex)
|
|
3566
3573
|
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null
|
|
3567
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.
|
|
3568
3583
|
const sheer = stats.avg < SHEER_ALPHA_THRESHOLD
|
|
3569
|
-
const
|
|
3570
|
-
//
|
|
3571
|
-
//
|
|
3572
|
-
|
|
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
|
|
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
|
|
3576
3588
|
// Load-time classification log — one line per material, cheap and
|
|
3577
|
-
// invaluable when a model renders wrong (bucket/outline/
|
|
3589
|
+
// invaluable when a model renders wrong (bucket/outline/shadow disputes).
|
|
3578
3590
|
console.info(
|
|
3579
|
-
`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)}
|
|
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"}`,
|
|
3580
3592
|
)
|
|
3581
3593
|
|
|
3582
3594
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
@@ -3615,24 +3627,13 @@ export class Engine {
|
|
|
3615
3627
|
this.zeroStyleBuffer,
|
|
3616
3628
|
)
|
|
3617
3629
|
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
groupId: null,
|
|
3626
|
-
baseBindGroupEntries,
|
|
3627
|
-
castsShadow: !sheer,
|
|
3628
|
-
})
|
|
3629
|
-
|
|
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) {
|
|
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"]
|
|
3636
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
3636
3637
|
const materialUniformData = new Float32Array([
|
|
3637
3638
|
mat.edgeColor[0],
|
|
3638
3639
|
mat.edgeColor[1],
|
|
@@ -3648,19 +3649,27 @@ export class Engine {
|
|
|
3648
3649
|
const outlineBindGroup = this.device.createBindGroup({
|
|
3649
3650
|
label: `${prefix}outline: ${mat.name}`,
|
|
3650
3651
|
layout: this.outlinePerMaterialBindGroupLayout,
|
|
3651
|
-
entries: [
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
type: outlineType,
|
|
3656
|
-
count: indexCount,
|
|
3657
|
-
firstIndex: currentIndexOffset,
|
|
3658
|
-
bindGroup: outlineBindGroup,
|
|
3659
|
-
materialName: mat.name,
|
|
3660
|
-
groupId: null,
|
|
3652
|
+
entries: [
|
|
3653
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
3654
|
+
{ binding: 1, resource: textureView },
|
|
3655
|
+
],
|
|
3661
3656
|
})
|
|
3657
|
+
outline = { bindGroup: outlineBindGroup }
|
|
3662
3658
|
}
|
|
3663
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
|
+
|
|
3664
3673
|
if (this.onRaycast) {
|
|
3665
3674
|
const pickIdData = new Float32Array([modelId, materialId, 0, 0])
|
|
3666
3675
|
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData)
|
|
@@ -4509,11 +4518,22 @@ export class Engine {
|
|
|
4509
4518
|
}
|
|
4510
4519
|
|
|
4511
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.
|
|
4512
4528
|
if (hasModels)
|
|
4513
4529
|
this.forEachInstance((inst) => {
|
|
4514
|
-
if (inst.model.visible) this.
|
|
4530
|
+
if (inst.model.visible) this.renderModelOpaquePhase(pass, inst)
|
|
4515
4531
|
})
|
|
4516
4532
|
if (this.hasGround) this.renderGround(pass)
|
|
4533
|
+
if (hasModels)
|
|
4534
|
+
this.forEachInstance((inst) => {
|
|
4535
|
+
if (inst.model.visible) this.renderModelTransparentPhase(pass, inst)
|
|
4536
|
+
})
|
|
4517
4537
|
pass.end()
|
|
4518
4538
|
|
|
4519
4539
|
// Bloom pyramid (EEVEE 3.6):
|
|
@@ -4805,8 +4825,7 @@ export class Engine {
|
|
|
4805
4825
|
let overEyesPipeline: GPURenderPipeline | undefined
|
|
4806
4826
|
try {
|
|
4807
4827
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false)
|
|
4808
|
-
//
|
|
4809
|
-
// patchwork) — same shading, different depth state.
|
|
4828
|
+
// Dormant OIT twin — kept for a future order-independent-transparency path.
|
|
4810
4829
|
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false)
|
|
4811
4830
|
if (renderClass === "hair") overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true)
|
|
4812
4831
|
} catch (e) {
|
|
@@ -4966,14 +4985,10 @@ export class Engine {
|
|
|
4966
4985
|
}
|
|
4967
4986
|
|
|
4968
4987
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4969
|
-
// 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).
|
|
4970
4991
|
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.
|
|
4977
4992
|
if (dc.groupId) {
|
|
4978
4993
|
const install = inst.styleGroups.get(dc.groupId)
|
|
4979
4994
|
if (install) return install.pipeline
|
|
@@ -4983,9 +4998,10 @@ export class Engine {
|
|
|
4983
4998
|
|
|
4984
4999
|
/**
|
|
4985
5000
|
* Draw every material of a given type (`opaque` or `transparent`) using the main
|
|
4986
|
-
* pipeline(s)
|
|
4987
|
-
*
|
|
4988
|
-
*
|
|
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.
|
|
4989
5005
|
*/
|
|
4990
5006
|
private drawMaterials(pass: GPURenderPassEncoder, inst: ModelInstance, type: "opaque" | "transparent"): void {
|
|
4991
5007
|
let currentPipeline: GPURenderPipeline | null = null
|
|
@@ -5004,61 +5020,58 @@ export class Engine {
|
|
|
5004
5020
|
}
|
|
5005
5021
|
pass.setBindGroup(2, draw.bindGroup)
|
|
5006
5022
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
* Draw every outline of a given type (`opaque-outline` or `transparent-outline`).
|
|
5012
|
-
* Uses its own pipeline layout (group 0 = camera-only, group 2 = edge uniforms), so
|
|
5013
|
-
* every batch binds its own groups from scratch — the next drawMaterials call will
|
|
5014
|
-
* rebind group 0/1 correctly if needed.
|
|
5015
|
-
*/
|
|
5016
|
-
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, type: DrawCallType): void {
|
|
5017
|
-
if (!this.outlineEnabled) return
|
|
5018
|
-
let bound = false
|
|
5019
|
-
for (const draw of inst.drawCalls) {
|
|
5020
|
-
if (draw.type !== type || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
5021
|
-
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.
|
|
5022
5027
|
pass.setPipeline(this.outlinePipeline)
|
|
5023
5028
|
pass.setBindGroup(0, this.outlinePerFrameBindGroup)
|
|
5024
|
-
pass.setBindGroup(
|
|
5025
|
-
|
|
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
|
|
5026
5033
|
}
|
|
5027
|
-
pass.setBindGroup(2, draw.bindGroup)
|
|
5028
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5029
5034
|
}
|
|
5030
5035
|
}
|
|
5031
5036
|
|
|
5032
5037
|
/**
|
|
5033
|
-
* Main-pass render sequence for one model instance:
|
|
5034
|
-
*
|
|
5035
|
-
*
|
|
5036
|
-
*
|
|
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.
|
|
5037
5043
|
*/
|
|
5038
|
-
private
|
|
5044
|
+
private setModelDrawState(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5039
5045
|
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
5040
5046
|
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
5041
5047
|
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
5042
5048
|
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
5043
|
-
|
|
5044
5049
|
// Single stencil-reference set covers eye (write), hair (read not-equal),
|
|
5045
5050
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
5046
5051
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
5052
|
+
}
|
|
5047
5053
|
|
|
5048
|
-
|
|
5049
|
-
|
|
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.
|
|
5054
|
+
private renderModelOpaquePhase(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5055
|
+
this.setModelDrawState(pass, inst)
|
|
5052
5056
|
this.drawMaterials(pass, inst, "opaque")
|
|
5053
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.
|
|
5054
5070
|
this.drawMaterials(pass, inst, "transparent")
|
|
5055
|
-
this.drawOutlines(pass, inst, "opaque-outline")
|
|
5056
|
-
this.drawOutlines(pass, inst, "transparent-outline")
|
|
5057
5071
|
}
|
|
5058
5072
|
|
|
5059
5073
|
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
5060
|
-
*
|
|
5061
|
-
* for the dormant no-write/OIT path. */
|
|
5074
|
+
* Dormant — kept for a future order-independent-transparency path. */
|
|
5062
5075
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5063
5076
|
protected drawTransparentDepthPrepass(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5064
5077
|
let bound = false
|
|
@@ -5119,6 +5132,10 @@ export class Engine {
|
|
|
5119
5132
|
this.cameraMatrixData[32] = cameraPos.x
|
|
5120
5133
|
this.cameraMatrixData[33] = cameraPos.y
|
|
5121
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
|
|
5122
5139
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
5123
5140
|
|
|
5124
5141
|
// 360 backdrop: the composite reconstructs each pixel's view ray from the
|
|
@@ -117,7 +117,7 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
117
117
|
var bgA = select(0.0, 1.0, bg.w > 0.5);
|
|
118
118
|
var bgPm = bg.rgb * bgA; // premultiplied accumulator
|
|
119
119
|
let fxOn = viewU[6].y > 0.5;
|
|
120
|
-
if (bg.w > 1.5 || fxOn) {
|
|
120
|
+
if ((bg.w > 1.5 || fxOn) COVERAGE_GATE) {
|
|
121
121
|
// The equirect and any effect both need this pixel's world-space view ray,
|
|
122
122
|
// rebuilt from the camera basis. The dome sits at infinity (no parallax) —
|
|
123
123
|
// PhotoDome-style, display-only.
|
|
@@ -152,8 +152,28 @@ const EFFECT_CALL = /* wgsl */ `
|
|
|
152
152
|
}
|
|
153
153
|
`
|
|
154
154
|
|
|
155
|
+
// Derivative builtins are illegal in non-uniform control flow (WGSL uniformity
|
|
156
|
+
// analysis rejects the pipeline), so the coverage gate below can only wrap
|
|
157
|
+
// effect code that doesn't use them. Checked textually at build time.
|
|
158
|
+
const USES_DERIVATIVES = /\b(?:fwidth|dpdx|dpdy)(?:Fine|Coarse)?\s*\(/
|
|
159
|
+
|
|
160
|
+
/** Skip the whole background block (equirect sample + effect) behind pixels the
|
|
161
|
+
* model fully covers — the composite multiplies the result by (1 - alpha) = 0
|
|
162
|
+
* there anyway, and on a full-screen effect that's a third or more of the frame
|
|
163
|
+
* (the cost Safari feels most). The equirect uses explicit-LOD sampling, which
|
|
164
|
+
* is always legal in non-uniform flow; only derivative-using effects must keep
|
|
165
|
+
* uniform control flow and forgo the gate. */
|
|
166
|
+
function coverageGate(effect?: CompositeEffectSource | null): string {
|
|
167
|
+
const gated = !effect || !USES_DERIVATIVES.test(effect.wgsl)
|
|
168
|
+
return gated ? "&& alpha < 0.999" : ""
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
export function buildCompositeShader(effect?: CompositeEffectSource | null): string {
|
|
156
|
-
if (!effect)
|
|
172
|
+
if (!effect)
|
|
173
|
+
return (
|
|
174
|
+
COMPOSITE_HEAD +
|
|
175
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL).replace("COVERAGE_GATE", coverageGate(null))
|
|
176
|
+
)
|
|
157
177
|
return (
|
|
158
178
|
COMPOSITE_HEAD +
|
|
159
179
|
"\n// ── user background effect (setBackgroundEffect) ──\n" +
|
|
@@ -161,7 +181,7 @@ export function buildCompositeShader(effect?: CompositeEffectSource | null): str
|
|
|
161
181
|
"\n" +
|
|
162
182
|
effect.wgsl +
|
|
163
183
|
"\n" +
|
|
164
|
-
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim())
|
|
184
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim()).replace("COVERAGE_GATE", coverageGate(effect))
|
|
165
185
|
)
|
|
166
186
|
}
|
|
167
187
|
|