reze-engine 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/engine.d.ts +49 -18
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +190 -104
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +46 -4
- 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 +214 -106
- package/src/index.ts +2 -0
- package/src/shaders/passes/composite.ts +51 -4
- 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
|
|
@@ -283,6 +283,34 @@ export const DEFAULT_VIEW_TRANSFORM: ViewTransformOptions = {
|
|
|
283
283
|
look: "medium_high_contrast",
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
/** Color grading applied to the tonemapped scene (ASC CDL — see grade() in
|
|
287
|
+
* composite.ts). The three tonal controls are expressed as COLORS with
|
|
288
|
+
* mid-gray (0.5, 0.5, 0.5) as neutral: the direction from neutral is the hue
|
|
289
|
+
* you push toward, and the distance from neutral is the amount — so no
|
|
290
|
+
* separate strength slider is needed. Display-space sRGB, since grading runs
|
|
291
|
+
* after the view transform. */
|
|
292
|
+
export type ColorGradingOptions = {
|
|
293
|
+
/** Lifts/tints the dark end (CDL offset). */
|
|
294
|
+
shadows: Vec3
|
|
295
|
+
/** Bends the midtones (CDL power) — brighter above neutral, darker below. */
|
|
296
|
+
midtones: Vec3
|
|
297
|
+
/** Scales/tints the bright end (CDL slope). */
|
|
298
|
+
highlights: Vec3
|
|
299
|
+
/** Contrast about the 0.5 display pivot. 1 = neutral. */
|
|
300
|
+
contrast: number
|
|
301
|
+
/** 1 = neutral, 0 = grayscale, >1 = punchier. */
|
|
302
|
+
saturation: number
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const NEUTRAL_GRADE_CHANNEL = 0.5
|
|
306
|
+
export const DEFAULT_COLOR_GRADING: ColorGradingOptions = {
|
|
307
|
+
shadows: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
308
|
+
midtones: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
309
|
+
highlights: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
310
|
+
contrast: 1,
|
|
311
|
+
saturation: 1,
|
|
312
|
+
}
|
|
313
|
+
|
|
286
314
|
export type GizmoDragKind = "rotate" | "translate"
|
|
287
315
|
|
|
288
316
|
export interface GizmoDragEvent {
|
|
@@ -359,6 +387,10 @@ interface DrawCall {
|
|
|
359
387
|
baseBindGroupEntries?: GPUBindGroupEntry[]
|
|
360
388
|
/** Material draws only: false = excluded from the shadow map (fully sheer). */
|
|
361
389
|
castsShadow?: boolean
|
|
390
|
+
/** Edge-flagged materials: interleaved inverted-hull outline drawn right after
|
|
391
|
+
* this material with the outline pipeline. Shares this call's index range;
|
|
392
|
+
* own bind group (edge uniforms + diffuse texture for the alpha test). */
|
|
393
|
+
outline?: { bindGroup: GPUBindGroup }
|
|
362
394
|
}
|
|
363
395
|
|
|
364
396
|
interface PickDrawCall {
|
|
@@ -456,14 +488,10 @@ function buildAlphaSampler(
|
|
|
456
488
|
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
457
489
|
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
458
490
|
* 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. */
|
|
491
|
+
* Bucketing itself is binary (babylon-mmd parity): ANY translucent coverage
|
|
492
|
+
* routes to the alpha-blend bucket. `avg` below this threshold additionally
|
|
493
|
+
* marks a material as fully sheer (a veil), which vetoes shadow casting. */
|
|
465
494
|
const SHEER_ALPHA_THRESHOLD = 0.7
|
|
466
|
-
const PARTIAL_TRANSLUCENT_FRAC = 0.15
|
|
467
495
|
function materialAlphaStats(
|
|
468
496
|
verts: Float32Array,
|
|
469
497
|
indices: Uint32Array,
|
|
@@ -651,7 +679,7 @@ export class Engine {
|
|
|
651
679
|
private compositeBindGroup!: GPUBindGroup
|
|
652
680
|
private compositeUniformBuffer!: GPUBuffer
|
|
653
681
|
// [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
|
|
654
|
-
private readonly compositeUniformData = new Float32Array(
|
|
682
|
+
private readonly compositeUniformData = new Float32Array(40)
|
|
655
683
|
/** Composite background (display-space sRGB 0–1) — null = transparent canvas. */
|
|
656
684
|
private backgroundColor: Vec3 | null = null
|
|
657
685
|
// 360 backdrop (equirectangular skybox, sampled by view ray in composite).
|
|
@@ -861,6 +889,42 @@ export class Engine {
|
|
|
861
889
|
return { exposure: v.exposure, gamma: v.gamma, look: v.look }
|
|
862
890
|
}
|
|
863
891
|
|
|
892
|
+
private colorGrading: ColorGradingOptions = {
|
|
893
|
+
shadows: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
894
|
+
midtones: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
895
|
+
highlights: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
896
|
+
contrast: DEFAULT_COLOR_GRADING.contrast,
|
|
897
|
+
saturation: DEFAULT_COLOR_GRADING.saturation,
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Color-grade the tonemapped scene (ASC CDL slope/offset/power + saturation).
|
|
902
|
+
* The background layer is deliberately left ungraded — see the call site in
|
|
903
|
+
* composite.ts. Uniforms-only: no pipeline rebuild, safe to call per frame
|
|
904
|
+
* (e.g. from a slider drag).
|
|
905
|
+
*/
|
|
906
|
+
setColorGrading(patch: Partial<ColorGradingOptions>): void {
|
|
907
|
+
const g = this.colorGrading
|
|
908
|
+
if (patch.shadows) g.shadows = new Vec3(patch.shadows.x, patch.shadows.y, patch.shadows.z)
|
|
909
|
+
if (patch.midtones) g.midtones = new Vec3(patch.midtones.x, patch.midtones.y, patch.midtones.z)
|
|
910
|
+
if (patch.highlights) g.highlights = new Vec3(patch.highlights.x, patch.highlights.y, patch.highlights.z)
|
|
911
|
+
if (patch.contrast !== undefined) g.contrast = patch.contrast
|
|
912
|
+
if (patch.saturation !== undefined) g.saturation = patch.saturation
|
|
913
|
+
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Current grade (for serialization into a scene descriptor). */
|
|
917
|
+
getColorGrading(): ColorGradingOptions {
|
|
918
|
+
const g = this.colorGrading
|
|
919
|
+
return {
|
|
920
|
+
shadows: new Vec3(g.shadows.x, g.shadows.y, g.shadows.z),
|
|
921
|
+
midtones: new Vec3(g.midtones.x, g.midtones.y, g.midtones.z),
|
|
922
|
+
highlights: new Vec3(g.highlights.x, g.highlights.y, g.highlights.z),
|
|
923
|
+
contrast: g.contrast,
|
|
924
|
+
saturation: g.saturation,
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
864
928
|
setViewTransformOptions(patch: Partial<ViewTransformOptions>): void {
|
|
865
929
|
const v = this.viewTransform
|
|
866
930
|
if (patch.exposure !== undefined) v.exposure = patch.exposure
|
|
@@ -901,6 +965,32 @@ export class Engine {
|
|
|
901
965
|
u[25] = this.backgroundEffect ? 1 : 0
|
|
902
966
|
u[26] = this.canvas.width
|
|
903
967
|
u[27] = this.canvas.height
|
|
968
|
+
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
969
|
+
// on the CPU, so the shader only ever sees slope/offset/power. Mid-gray is
|
|
970
|
+
// neutral in all three; the signed distance from it is the amount.
|
|
971
|
+
const g = this.colorGrading
|
|
972
|
+
const off = (c: number) => (c - NEUTRAL_GRADE_CHANNEL) * 0.5 // ±0.25 lift
|
|
973
|
+
// power < 1 brightens, so midtones ABOVE neutral must lower the exponent.
|
|
974
|
+
const pow_ = (c: number) => Math.max(0.05, 1 - (c - NEUTRAL_GRADE_CHANNEL) * 1.5)
|
|
975
|
+
const slope = (c: number) => Math.max(0, 1 + (c - NEUTRAL_GRADE_CHANNEL) * 1.5)
|
|
976
|
+
u[28] = off(g.shadows.x)
|
|
977
|
+
u[29] = off(g.shadows.y)
|
|
978
|
+
u[30] = off(g.shadows.z)
|
|
979
|
+
u[31] = g.contrast
|
|
980
|
+
u[32] = pow_(g.midtones.x)
|
|
981
|
+
u[33] = pow_(g.midtones.y)
|
|
982
|
+
u[34] = pow_(g.midtones.z)
|
|
983
|
+
u[35] = g.saturation
|
|
984
|
+
u[36] = slope(g.highlights.x)
|
|
985
|
+
u[37] = slope(g.highlights.y)
|
|
986
|
+
u[38] = slope(g.highlights.z)
|
|
987
|
+
// Neutral grade → flag off, so the default pipeline pays nothing per pixel.
|
|
988
|
+
const neutral =
|
|
989
|
+
u[28] === 0 && u[29] === 0 && u[30] === 0 &&
|
|
990
|
+
u[32] === 1 && u[33] === 1 && u[34] === 1 &&
|
|
991
|
+
u[36] === 1 && u[37] === 1 && u[38] === 1 &&
|
|
992
|
+
g.contrast === 1 && g.saturation === 1
|
|
993
|
+
u[39] = neutral ? 0 : 1
|
|
904
994
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u)
|
|
905
995
|
}
|
|
906
996
|
|
|
@@ -917,7 +1007,12 @@ export class Engine {
|
|
|
917
1007
|
}
|
|
918
1008
|
|
|
919
1009
|
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
920
|
-
|
|
1010
|
+
// OFF by default — the product aesthetic. Modern high-detail models read
|
|
1011
|
+
// better without hulls (babylon-mmd's own demos disable its outline renderer
|
|
1012
|
+
// too), and no hull pass means no depth-tie edge cases against near-coplanar
|
|
1013
|
+
// cloth. The full MMD-faithful machinery (interleaved per-material hulls,
|
|
1014
|
+
// texture-alpha-modulated rims) stays in place behind setOutlineEnabled(true).
|
|
1015
|
+
private outlineEnabled = false
|
|
921
1016
|
setOutlineEnabled(on: boolean): void {
|
|
922
1017
|
this.outlineEnabled = on
|
|
923
1018
|
}
|
|
@@ -1493,6 +1588,8 @@ export class Engine {
|
|
|
1493
1588
|
attributes: [
|
|
1494
1589
|
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
1495
1590
|
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
1591
|
+
// uv — the outline FS alpha-tests the diffuse texture (babylon-mmd parity)
|
|
1592
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
1496
1593
|
],
|
|
1497
1594
|
},
|
|
1498
1595
|
{
|
|
@@ -1758,6 +1855,7 @@ export class Engine {
|
|
|
1758
1855
|
label: "outline per-frame bind group layout",
|
|
1759
1856
|
entries: [
|
|
1760
1857
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1858
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
1761
1859
|
],
|
|
1762
1860
|
})
|
|
1763
1861
|
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
@@ -1765,6 +1863,7 @@ export class Engine {
|
|
|
1765
1863
|
label: "outline per-material bind group layout",
|
|
1766
1864
|
entries: [
|
|
1767
1865
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1866
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1768
1867
|
],
|
|
1769
1868
|
})
|
|
1770
1869
|
|
|
@@ -1780,7 +1879,10 @@ export class Engine {
|
|
|
1780
1879
|
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
1781
1880
|
label: "outline per-frame bind group",
|
|
1782
1881
|
layout: this.outlinePerFrameBindGroupLayout,
|
|
1783
|
-
entries: [
|
|
1882
|
+
entries: [
|
|
1883
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1884
|
+
{ binding: 1, resource: this.materialSampler },
|
|
1885
|
+
],
|
|
1784
1886
|
})
|
|
1785
1887
|
|
|
1786
1888
|
const outlineShaderModule = this.device.createShaderModule({
|
|
@@ -1797,8 +1899,10 @@ export class Engine {
|
|
|
1797
1899
|
cullMode: "back",
|
|
1798
1900
|
depthStencil: {
|
|
1799
1901
|
format: "depth24plus-stencil8",
|
|
1800
|
-
//
|
|
1801
|
-
|
|
1902
|
+
// babylon-mmd draws outlines WITH depth write (its _afterRenderingMesh
|
|
1903
|
+
// forces setDepthWrite(true)); the constant bias below still makes
|
|
1904
|
+
// hulls lose depth ties against their own near-coplanar geometry.
|
|
1905
|
+
depthWriteEnabled: true,
|
|
1802
1906
|
depthCompare: "less-equal",
|
|
1803
1907
|
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1804
1908
|
// carry their surface's exact depth, so against this model's paired
|
|
@@ -1991,10 +2095,11 @@ export class Engine {
|
|
|
1991
2095
|
// mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
|
|
1992
2096
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
1993
2097
|
label: "composite view uniforms",
|
|
1994
|
-
//
|
|
2098
|
+
// 10 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
1995
2099
|
// (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray ·
|
|
1996
|
-
// (time, _, canvas width, canvas height) for user background effects
|
|
1997
|
-
|
|
2100
|
+
// (time, _, canvas width, canvas height) for user background effects ·
|
|
2101
|
+
// three grade vectors (CDL offset+contrast, power+saturation, slope+flag).
|
|
2102
|
+
size: 160,
|
|
1998
2103
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1999
2104
|
})
|
|
2000
2105
|
this.bgParamsDummyBuffer = this.device.createBuffer({
|
|
@@ -3555,28 +3660,26 @@ export class Engine {
|
|
|
3555
3660
|
}
|
|
3556
3661
|
|
|
3557
3662
|
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
3663
|
const diffusePath = texLogicalPath(mat.diffuseTextureIndex)
|
|
3566
3664
|
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null
|
|
3567
3665
|
const stats = materialAlphaStats(meshVertices, meshIndices, currentIndexOffset, indexCount, alphaSampler)
|
|
3666
|
+
// babylon-mmd parity (its default DepthWriteAlphaBlendingWithEvaluation
|
|
3667
|
+
// method): the bucket decision is BINARY. A material with ANY translucent
|
|
3668
|
+
// texels on its geometry is alpha-blend — drawn in PMX author order with
|
|
3669
|
+
// depth write ON (forceDepthWrite); everything else is opaque. The old
|
|
3670
|
+
// avg/frac tier system left mostly-opaque lace (translucentFrac 0.09) in
|
|
3671
|
+
// the opaque bucket while its sibling panels went transparent, breaking
|
|
3672
|
+
// the author's compositing order — the gray fold patches. The 2% floor
|
|
3673
|
+
// only guards against centroid-sampling noise on genuinely solid cloth.
|
|
3568
3674
|
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
|
|
3675
|
+
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || stats.translucentFrac > 0.02
|
|
3676
|
+
// Shadow casting: the PMX author's own flag (bit 0x04, cast self-shadow),
|
|
3677
|
+
// still vetoed for fully sheer cloth — a veil must not cast a solid sheet.
|
|
3678
|
+
const castsShadow = (mat.edgeFlag & 0x04) !== 0 && !sheer
|
|
3576
3679
|
// Load-time classification log — one line per material, cheap and
|
|
3577
|
-
// invaluable when a model renders wrong (bucket/outline/
|
|
3680
|
+
// invaluable when a model renders wrong (bucket/outline/shadow disputes).
|
|
3578
3681
|
console.info(
|
|
3579
|
-
`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)}
|
|
3682
|
+
`[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
3683
|
)
|
|
3581
3684
|
|
|
3582
3685
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
@@ -3615,24 +3718,13 @@ export class Engine {
|
|
|
3615
3718
|
this.zeroStyleBuffer,
|
|
3616
3719
|
)
|
|
3617
3720
|
|
|
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) {
|
|
3721
|
+
// Inverted-hull outline for EVERY edge-flagged material (PMX bit 0x10) —
|
|
3722
|
+
// the outline FS alpha-tests the diffuse texture, so sheer fabric masks
|
|
3723
|
+
// its own hull where it is see-through instead of us skipping it here.
|
|
3724
|
+
// Drawn interleaved right after this material's color draw (babylon-mmd's
|
|
3725
|
+
// per-mesh afterRender outline stage) — see drawMaterials.
|
|
3726
|
+
let outline: DrawCall["outline"]
|
|
3727
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
3636
3728
|
const materialUniformData = new Float32Array([
|
|
3637
3729
|
mat.edgeColor[0],
|
|
3638
3730
|
mat.edgeColor[1],
|
|
@@ -3648,19 +3740,27 @@ export class Engine {
|
|
|
3648
3740
|
const outlineBindGroup = this.device.createBindGroup({
|
|
3649
3741
|
label: `${prefix}outline: ${mat.name}`,
|
|
3650
3742
|
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,
|
|
3743
|
+
entries: [
|
|
3744
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
3745
|
+
{ binding: 1, resource: textureView },
|
|
3746
|
+
],
|
|
3661
3747
|
})
|
|
3748
|
+
outline = { bindGroup: outlineBindGroup }
|
|
3662
3749
|
}
|
|
3663
3750
|
|
|
3751
|
+
const type: DrawCallType = isTransparent ? "transparent" : "opaque"
|
|
3752
|
+
inst.drawCalls.push({
|
|
3753
|
+
type,
|
|
3754
|
+
count: indexCount,
|
|
3755
|
+
firstIndex: currentIndexOffset,
|
|
3756
|
+
bindGroup,
|
|
3757
|
+
materialName: mat.name,
|
|
3758
|
+
groupId: null,
|
|
3759
|
+
baseBindGroupEntries,
|
|
3760
|
+
castsShadow,
|
|
3761
|
+
outline,
|
|
3762
|
+
})
|
|
3763
|
+
|
|
3664
3764
|
if (this.onRaycast) {
|
|
3665
3765
|
const pickIdData = new Float32Array([modelId, materialId, 0, 0])
|
|
3666
3766
|
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData)
|
|
@@ -4509,11 +4609,22 @@ export class Engine {
|
|
|
4509
4609
|
}
|
|
4510
4610
|
|
|
4511
4611
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
4612
|
+
// Phase order: opaque models → ground → transparent fabric.
|
|
4613
|
+
// The ground shader is the most expensive full-coverage draw in the frame
|
|
4614
|
+
// (9-tap PCF on the 4096² shadow map per pixel), so it draws AFTER the
|
|
4615
|
+
// opaque phase to get early-z rejected behind the body — drawing it first
|
|
4616
|
+
// shaded every covered pixel and measurably dropped Safari fps. It still
|
|
4617
|
+
// draws BEFORE the transparent phase so sheer fabric blends over the floor
|
|
4618
|
+
// instead of over the background with the floor depth-rejected behind it.
|
|
4512
4619
|
if (hasModels)
|
|
4513
4620
|
this.forEachInstance((inst) => {
|
|
4514
|
-
if (inst.model.visible) this.
|
|
4621
|
+
if (inst.model.visible) this.renderModelOpaquePhase(pass, inst)
|
|
4515
4622
|
})
|
|
4516
4623
|
if (this.hasGround) this.renderGround(pass)
|
|
4624
|
+
if (hasModels)
|
|
4625
|
+
this.forEachInstance((inst) => {
|
|
4626
|
+
if (inst.model.visible) this.renderModelTransparentPhase(pass, inst)
|
|
4627
|
+
})
|
|
4517
4628
|
pass.end()
|
|
4518
4629
|
|
|
4519
4630
|
// Bloom pyramid (EEVEE 3.6):
|
|
@@ -4805,8 +4916,7 @@ export class Engine {
|
|
|
4805
4916
|
let overEyesPipeline: GPURenderPipeline | undefined
|
|
4806
4917
|
try {
|
|
4807
4918
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false)
|
|
4808
|
-
//
|
|
4809
|
-
// patchwork) — same shading, different depth state.
|
|
4919
|
+
// Dormant OIT twin — kept for a future order-independent-transparency path.
|
|
4810
4920
|
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false)
|
|
4811
4921
|
if (renderClass === "hair") overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true)
|
|
4812
4922
|
} catch (e) {
|
|
@@ -4966,14 +5076,10 @@ export class Engine {
|
|
|
4966
5076
|
}
|
|
4967
5077
|
|
|
4968
5078
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4969
|
-
// the neutral base (ungrouped materials render the default graph).
|
|
5079
|
+
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
5080
|
+
// draws use the SAME depth-write-on pipeline — babylon-mmd's forceDepthWrite
|
|
5081
|
+
// blending (see renderModelTransparentPhase for the trade-off record).
|
|
4970
5082
|
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
5083
|
if (dc.groupId) {
|
|
4978
5084
|
const install = inst.styleGroups.get(dc.groupId)
|
|
4979
5085
|
if (install) return install.pipeline
|
|
@@ -4983,9 +5089,10 @@ export class Engine {
|
|
|
4983
5089
|
|
|
4984
5090
|
/**
|
|
4985
5091
|
* Draw every material of a given type (`opaque` or `transparent`) using the main
|
|
4986
|
-
* pipeline(s)
|
|
4987
|
-
*
|
|
4988
|
-
*
|
|
5092
|
+
* pipeline(s), and — babylon-mmd's per-mesh outline stage — each edge-flagged
|
|
5093
|
+
* material's inverted hull IMMEDIATELY after its color draw. Interleaving is what
|
|
5094
|
+
* makes outlines compose like MMD: every material drawn later in the author's
|
|
5095
|
+
* order covers earlier hulls, and each hull sits over everything drawn before it.
|
|
4989
5096
|
*/
|
|
4990
5097
|
private drawMaterials(pass: GPURenderPassEncoder, inst: ModelInstance, type: "opaque" | "transparent"): void {
|
|
4991
5098
|
let currentPipeline: GPURenderPipeline | null = null
|
|
@@ -5004,61 +5111,58 @@ export class Engine {
|
|
|
5004
5111
|
}
|
|
5005
5112
|
pass.setBindGroup(2, draw.bindGroup)
|
|
5006
5113
|
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) {
|
|
5114
|
+
if (draw.outline && this.outlineEnabled) {
|
|
5115
|
+
// Same index range; own pipeline + groups 0/2. Group 1 (skinMats) is
|
|
5116
|
+
// layout-identical between the main and outline pipelines and stays
|
|
5117
|
+
// bound. Restore group 0 afterwards and force a pipeline re-set.
|
|
5022
5118
|
pass.setPipeline(this.outlinePipeline)
|
|
5023
5119
|
pass.setBindGroup(0, this.outlinePerFrameBindGroup)
|
|
5024
|
-
pass.setBindGroup(
|
|
5025
|
-
|
|
5120
|
+
pass.setBindGroup(2, draw.outline.bindGroup)
|
|
5121
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5122
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
5123
|
+
currentPipeline = null
|
|
5026
5124
|
}
|
|
5027
|
-
pass.setBindGroup(2, draw.bindGroup)
|
|
5028
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5029
5125
|
}
|
|
5030
5126
|
}
|
|
5031
5127
|
|
|
5032
5128
|
/**
|
|
5033
|
-
* Main-pass render sequence for one model instance:
|
|
5034
|
-
*
|
|
5035
|
-
*
|
|
5036
|
-
*
|
|
5129
|
+
* Main-pass render sequence for one model instance — babylon-mmd parity:
|
|
5130
|
+
* opaque bucket, the hair-over-eyes stencil pass, then alpha-blend materials
|
|
5131
|
+
* in PMX author order with depth write ON (forceDepthWrite). Outlines are not
|
|
5132
|
+
* a separate phase: drawMaterials draws each edge-flagged material's hull
|
|
5133
|
+
* right after the material itself, like MMD's per-mesh outline stage.
|
|
5037
5134
|
*/
|
|
5038
|
-
private
|
|
5135
|
+
private setModelDrawState(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5039
5136
|
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
5040
5137
|
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
5041
5138
|
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
5042
5139
|
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
5043
|
-
|
|
5044
5140
|
// Single stencil-reference set covers eye (write), hair (read not-equal),
|
|
5045
5141
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
5046
5142
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
5143
|
+
}
|
|
5047
5144
|
|
|
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.
|
|
5145
|
+
private renderModelOpaquePhase(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5146
|
+
this.setModelDrawState(pass, inst)
|
|
5052
5147
|
this.drawMaterials(pass, inst, "opaque")
|
|
5053
5148
|
this.drawHairOverEyes(pass, inst)
|
|
5149
|
+
}
|
|
5150
|
+
|
|
5151
|
+
private renderModelTransparentPhase(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5152
|
+
this.setModelDrawState(pass, inst)
|
|
5153
|
+
// Transparent: babylon-mmd's forceDepthWrite blending — PMX author order
|
|
5154
|
+
// with depth write ON. The accepted trade-off after trying every variant:
|
|
5155
|
+
// · depth-write ON (this): a fold hides its far side; rare view-dependent
|
|
5156
|
+
// double-blend seams at some angles. MMD's own known behavior.
|
|
5157
|
+
// · nearest-surface prepass: view-independent, but punched see-through
|
|
5158
|
+
// holes to whatever sat far behind a fold.
|
|
5159
|
+
// · depth-write OFF layering: every overlap visible everywhere — MORE
|
|
5160
|
+
// gray patches and texture artifacts in practice.
|
|
5054
5161
|
this.drawMaterials(pass, inst, "transparent")
|
|
5055
|
-
this.drawOutlines(pass, inst, "opaque-outline")
|
|
5056
|
-
this.drawOutlines(pass, inst, "transparent-outline")
|
|
5057
5162
|
}
|
|
5058
5163
|
|
|
5059
5164
|
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
5060
|
-
*
|
|
5061
|
-
* for the dormant no-write/OIT path. */
|
|
5165
|
+
* Dormant — kept for a future order-independent-transparency path. */
|
|
5062
5166
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5063
5167
|
protected drawTransparentDepthPrepass(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5064
5168
|
let bound = false
|
|
@@ -5119,6 +5223,10 @@ export class Engine {
|
|
|
5119
5223
|
this.cameraMatrixData[32] = cameraPos.x
|
|
5120
5224
|
this.cameraMatrixData[33] = cameraPos.y
|
|
5121
5225
|
this.cameraMatrixData[34] = cameraPos.z
|
|
5226
|
+
// Spare float after viewPos: render-target height in device px — the outline
|
|
5227
|
+
// shader derives the full viewport (width via projection aspect) for its
|
|
5228
|
+
// babylon-mmd constant-pixel edge extrusion.
|
|
5229
|
+
this.cameraMatrixData[35] = this.canvas.height
|
|
5122
5230
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
5123
5231
|
|
|
5124
5232
|
// 360 backdrop: the composite reconstructs each pixel's view ray from the
|