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.
@@ -18,148 +18,150 @@
18
18
  // group(0): per-frame scene (camera, lights, shadow map, BRDF LUT via nodes.ts)
19
19
  // group(1): per-model skinning
20
20
  // group(2): per-material (diffuse texture + material uniforms)
21
- export const COMMON_BINDINGS_WGSL = /* wgsl */ `
22
-
23
- struct CameraUniforms {
24
- view: mat4x4f,
25
- projection: mat4x4f,
26
- viewPos: vec3f,
27
- _padding: f32,
28
- };
29
-
30
- struct Light {
31
- direction: vec4f,
32
- color: vec4f,
33
- };
34
-
35
- struct LightUniforms {
36
- ambientColor: vec4f,
37
- lights: array<Light, 4>,
38
- };
39
-
40
- // Per-material uniforms. Every material binds this layout even if it ignores fields;
41
- // the engine keeps one bind group layout across all material pipelines. The PMX
42
- // classic-material fields (ambient/specular/shininess) are carried for graph nodes that
43
- // want them; most graphs read only diffuseColor + alpha.
44
- struct MaterialUniforms {
45
- diffuseColor: vec3f, // PMX diffuse rgb — the material_diffuse node reads this
46
- alpha: f32, // 0 → discard; <1 → transparent draw call
47
- ambient: vec3f, // PMX ambient rgb
48
- shininess: f32, // PMX specular power
49
- specular: vec3f, // PMX specular rgb
50
- sphereMode: f32, // 0 none · 1 multiply (sph) · 2 add (spa)
51
- // Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
52
- // the post-alpha-eye stencil by camera-vs-face hemisphere.
53
- headBoneIndex: f32,
54
- _pad0: f32,
55
- _pad1: f32,
56
- _pad2: f32,
57
- };
58
-
59
- struct VertexOutput {
60
- @builtin(position) position: vec4f,
61
- @location(0) normal: vec3f,
62
- @location(1) uv: vec2f,
63
- @location(2) worldPos: vec3f,
64
- // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
65
- // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
66
- // worldPos so the pattern rides with the surface — otherwise the mesh swims through a
67
- // world-static noise field under any skinning deformation or root (センター) motion.
68
- // At rest skinMats are identity so restPos == worldPos, which is why existing noise-
69
- // scale constants stay valid without retuning.
70
- @location(3) restPos: vec3f,
71
- };
72
-
73
- struct LightVP { viewProj: mat4x4f, };
74
-
75
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
76
- @group(0) @binding(1) var<uniform> light: LightUniforms;
77
- @group(0) @binding(2) var diffuseSampler: sampler;
78
- @group(0) @binding(3) var shadowMap: texture_depth_2d;
79
- @group(0) @binding(4) var shadowSampler: sampler_comparison;
80
- @group(0) @binding(5) var<uniform> lightVP: LightVP;
81
- // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
82
- @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
83
- @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
84
- @group(2) @binding(1) var<uniform> material: MaterialUniforms;
85
- // Reserved for future sphere/toon graph nodes; graphs that don't read them get the
86
- // 1×1 white fallback bound here.
87
- @group(2) @binding(2) var toonTexture: texture_2d<f32>;
88
- @group(2) @binding(3) var sphereTexture: texture_2d<f32>;
89
-
90
- // Four-bone blended normals can cancel to ~zero on physics-driven parts
91
- // (opposing bone rotations at 50/50 weights) — normalize(0) is 0/0 = NaN,
92
- // which poisons the whole shading stack and flashes through bloom. Fall
93
- // back to up for degenerate normals instead.
94
- fn safe_normal(nIn: vec3f) -> vec3f {
95
- let l2 = dot(nIn, nIn);
96
- if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }
97
- return nIn * inverseSqrt(l2);
98
- }
99
-
21
+ export const COMMON_BINDINGS_WGSL = /* wgsl */ `
22
+
23
+ struct CameraUniforms {
24
+ view: mat4x4f,
25
+ projection: mat4x4f,
26
+ viewPos: vec3f,
27
+ _padding: f32,
28
+ };
29
+
30
+ struct Light {
31
+ direction: vec4f,
32
+ color: vec4f,
33
+ };
34
+
35
+ struct LightUniforms {
36
+ ambientColor: vec4f,
37
+ lights: array<Light, 4>,
38
+ };
39
+
40
+ // Per-material uniforms. Every material binds this layout even if it ignores fields;
41
+ // the engine keeps one bind group layout across all material pipelines. The PMX
42
+ // classic-material fields (ambient/specular/shininess) are carried for graph nodes that
43
+ // want them; most graphs read only diffuseColor + alpha.
44
+ struct MaterialUniforms {
45
+ diffuseColor: vec3f, // PMX diffuse rgb — the material_diffuse node reads this
46
+ alpha: f32, // 0 → discard; <1 → transparent draw call
47
+ ambient: vec3f, // PMX ambient rgb
48
+ shininess: f32, // PMX specular power
49
+ specular: vec3f, // PMX specular rgb
50
+ sphereMode: f32, // 0 none · 1 multiply (sph) · 2 add (spa)
51
+ // Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
52
+ // the post-alpha-eye stencil by camera-vs-face hemisphere.
53
+ headBoneIndex: f32,
54
+ _pad0: f32,
55
+ _pad1: f32,
56
+ _pad2: f32,
57
+ };
58
+
59
+ struct VertexOutput {
60
+ @builtin(position) position: vec4f,
61
+ @location(0) normal: vec3f,
62
+ @location(1) uv: vec2f,
63
+ @location(2) worldPos: vec3f,
64
+ // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
65
+ // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
66
+ // worldPos so the pattern rides with the surface — otherwise the mesh swims through a
67
+ // world-static noise field under any skinning deformation or root (センター) motion.
68
+ // At rest skinMats are identity so restPos == worldPos, which is why existing noise-
69
+ // scale constants stay valid without retuning.
70
+ @location(3) restPos: vec3f,
71
+ };
72
+
73
+ struct LightVP { viewProj: mat4x4f, };
74
+
75
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
76
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
77
+ @group(0) @binding(2) var diffuseSampler: sampler;
78
+ @group(0) @binding(3) var shadowMap: texture_depth_2d;
79
+ @group(0) @binding(4) var shadowSampler: sampler_comparison;
80
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
81
+ // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
82
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
83
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
84
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
85
+ // Reserved for future sphere/toon graph nodes; graphs that don't read them get the
86
+ // 1×1 white fallback bound here.
87
+ @group(2) @binding(2) var toonTexture: texture_2d<f32>;
88
+ @group(2) @binding(3) var sphereTexture: texture_2d<f32>;
89
+
90
+ // Four-bone blended normals can cancel to ~zero on physics-driven parts
91
+ // (opposing bone rotations at 50/50 weights) — normalize(0) is 0/0 = NaN,
92
+ // which poisons the whole shading stack and flashes through bloom. Fall
93
+ // back to up for degenerate normals instead.
94
+ fn safe_normal(nIn: vec3f) -> vec3f {
95
+ let l2 = dot(nIn, nIn);
96
+ if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }
97
+ return nIn * inverseSqrt(l2);
98
+ }
99
+
100
100
  `;
101
101
  // ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
102
- // 2048-map, normal-bias 0.08, depth-bias 0.001. Unrolled — Safari's Metal backend
103
- // doesn't unroll nested shadow loops reliably, and the early out on back-facing
104
- // fragments saves 9 texture taps per skipped pixel.
105
- export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
106
-
107
- fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
108
- if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
109
- let biasedPos = worldPos + n * 0.08;
110
- let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
111
- let ndc = lclip.xyz / max(lclip.w, 1e-6);
112
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
113
- let cmpZ = ndc.z - 0.001;
114
- let ts = 1.0 / 2048.0;
115
- let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
116
- let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
117
- let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
118
- let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
119
- let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
120
- let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
121
- let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
122
- let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
123
- let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
124
- return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
125
- }
126
-
102
+ // 4096-map (MUST match Engine.SHADOW_MAP_SIZE), normal-bias 0.08, depth-bias
103
+ // 0.001. Unrolled — Safari's Metal backend doesn't unroll nested shadow loops
104
+ // reliably. The texel size was stale at 1/2048 after the map grew to 4096:
105
+ // PCF taps landed TWO texels apart, quantizing self-shadow edges into
106
+ // texel-sized gray/black squares that crawled with the animation.
107
+ export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
108
+
109
+ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
110
+ if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
111
+ let biasedPos = worldPos + n * 0.08;
112
+ let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
113
+ let ndc = lclip.xyz / max(lclip.w, 1e-6);
114
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
115
+ let cmpZ = ndc.z - 0.001;
116
+ let ts = 1.0 / 4096.0;
117
+ let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
118
+ let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
119
+ let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
120
+ let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
121
+ let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
122
+ let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
123
+ let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
124
+ let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
125
+ let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
126
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
127
+ }
128
+
127
129
  `;
128
130
  // ─── Skinning vertex shader ─────────────────────────────────────────
129
131
  // Four-bone linear blend skinning. Renormalizes weights when they don't sum to 1
130
132
  // (PMX models occasionally ship with unnormalized weights on extras like hair tips).
131
133
  // VS normalize on the outgoing normal is skipped — interpolation denormalizes it
132
134
  // anyway and every fragment shader does `normalize(input.normal)` as its first line.
133
- export const COMMON_VS_WGSL = /* wgsl */ `
134
-
135
- @vertex fn vs(
136
- @location(0) position: vec3f,
137
- @location(1) normal: vec3f,
138
- @location(2) uv: vec2f,
139
- @location(3) joints0: vec4<u32>,
140
- @location(4) weights0: vec4<f32>
141
- ) -> VertexOutput {
142
- var output: VertexOutput;
143
- let pos4 = vec4f(position, 1.0);
144
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
145
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
146
- let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
147
- var skinnedPos = vec4f(0.0);
148
- var skinnedNrm = vec3f(0.0);
149
- for (var i = 0u; i < 4u; i++) {
150
- let m = skinMats[joints0[i]];
151
- let w = nw[i];
152
- skinnedPos += (m * pos4) * w;
153
- skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
154
- }
155
- output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
156
- output.normal = skinnedNrm;
157
- output.uv = uv;
158
- output.worldPos = skinnedPos.xyz;
159
- output.restPos = position;
160
- return output;
161
- }
162
-
135
+ export const COMMON_VS_WGSL = /* wgsl */ `
136
+
137
+ @vertex fn vs(
138
+ @location(0) position: vec3f,
139
+ @location(1) normal: vec3f,
140
+ @location(2) uv: vec2f,
141
+ @location(3) joints0: vec4<u32>,
142
+ @location(4) weights0: vec4<f32>
143
+ ) -> VertexOutput {
144
+ var output: VertexOutput;
145
+ let pos4 = vec4f(position, 1.0);
146
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
147
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
148
+ let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
149
+ var skinnedPos = vec4f(0.0);
150
+ var skinnedNrm = vec3f(0.0);
151
+ for (var i = 0u; i < 4u; i++) {
152
+ let m = skinMats[joints0[i]];
153
+ let w = nw[i];
154
+ skinnedPos += (m * pos4) * w;
155
+ skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
156
+ }
157
+ output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
158
+ output.normal = skinnedNrm;
159
+ output.uv = uv;
160
+ output.worldPos = skinnedPos.xyz;
161
+ output.restPos = position;
162
+ return output;
163
+ }
164
+
163
165
  `;
164
166
  // ─── FS output struct ───────────────────────────────────────────────
165
167
  // Location 0: final radiance+alpha (blended into rg11b10ufloat; the HDR target
@@ -180,13 +182,13 @@ export const COMMON_VS_WGSL = /* wgsl */ `
180
182
  // src-alpha blending enabled:
181
183
  // out.r = mask_r · src.a + dst.r · (1-src.a) (bloom mask, weighted by alpha)
182
184
  // out.g = 1.0 · src.a + dst.g · (1-src.a) (canonical premultiplied alpha-over)
183
- export const COMMON_FS_OUT_WGSL = /* wgsl */ `
184
-
185
- struct FSOut {
186
- @location(0) color: vec4f,
187
- @location(1) mask: vec4f,
188
- };
189
-
185
+ export const COMMON_FS_OUT_WGSL = /* wgsl */ `
186
+
187
+ struct FSOut {
188
+ @location(0) color: vec4f,
189
+ @location(1) mask: vec4f,
190
+ };
191
+
190
192
  `;
191
193
  // ─── Convenience: full shared prelude ───────────────────────────────
192
194
  // Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;yEAayE;AACzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AA8HD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAWlF;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;yEAayE;AACzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AA8ID,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAelF;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
@@ -94,7 +94,7 @@ const COMPOSITE_BODY = /* wgsl */ `
94
94
  var bgA = select(0.0, 1.0, bg.w > 0.5);
95
95
  var bgPm = bg.rgb * bgA; // premultiplied accumulator
96
96
  let fxOn = viewU[6].y > 0.5;
97
- if (bg.w > 1.5 || fxOn) {
97
+ if ((bg.w > 1.5 || fxOn) COVERAGE_GATE) {
98
98
  // The equirect and any effect both need this pixel's world-space view ray,
99
99
  // rebuilt from the camera basis. The dome sits at infinity (no parallax) —
100
100
  // PhotoDome-style, display-only.
@@ -126,16 +126,31 @@ const EFFECT_CALL = /* wgsl */ `
126
126
  bgA = fx.a + bgA * (1.0 - fx.a);
127
127
  }
128
128
  `;
129
+ // Derivative builtins are illegal in non-uniform control flow (WGSL uniformity
130
+ // analysis rejects the pipeline), so the coverage gate below can only wrap
131
+ // effect code that doesn't use them. Checked textually at build time.
132
+ const USES_DERIVATIVES = /\b(?:fwidth|dpdx|dpdy)(?:Fine|Coarse)?\s*\(/;
133
+ /** Skip the whole background block (equirect sample + effect) behind pixels the
134
+ * model fully covers — the composite multiplies the result by (1 - alpha) = 0
135
+ * there anyway, and on a full-screen effect that's a third or more of the frame
136
+ * (the cost Safari feels most). The equirect uses explicit-LOD sampling, which
137
+ * is always legal in non-uniform flow; only derivative-using effects must keep
138
+ * uniform control flow and forgo the gate. */
139
+ function coverageGate(effect) {
140
+ const gated = !effect || !USES_DERIVATIVES.test(effect.wgsl);
141
+ return gated ? "&& alpha < 0.999" : "";
142
+ }
129
143
  export function buildCompositeShader(effect) {
130
144
  if (!effect)
131
- return COMPOSITE_HEAD + COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL);
145
+ return (COMPOSITE_HEAD +
146
+ COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL).replace("COVERAGE_GATE", coverageGate(null)));
132
147
  return (COMPOSITE_HEAD +
133
148
  "\n// ── user background effect (setBackgroundEffect) ──\n" +
134
149
  effect.paramsDecl +
135
150
  "\n" +
136
151
  effect.wgsl +
137
152
  "\n" +
138
- COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim()));
153
+ COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim()).replace("COVERAGE_GATE", coverageGate(effect)));
139
154
  }
140
155
  /** Kept for compatibility with existing imports (the base, no-effect shader). */
141
156
  export const COMPOSITE_SHADER_WGSL = buildCompositeShader(null);
@@ -0,0 +1,2 @@
1
+ export declare const TRANSPARENT_DEPTH_PREPASS_WGSL = "\nstruct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };\nstruct MaterialUniforms {\n diffuseColor: vec3f,\n alpha: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n};\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>,\n) -> VSOut {\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let w = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinned = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) {\n skinned += (skinMats[joints0[i]] * pos4) * w[i];\n }\n var o: VSOut;\n o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);\n o.uv = uv;\n return o;\n}\n\n@fragment fn fs(in: VSOut) {\n let a = material.alpha * textureSample(diffuseTexture, diffuseSampler, in.uv).a;\n if (a < 0.5) { discard; }\n}\n";
2
+ //# sourceMappingURL=depth-prepass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,8BAA8B,04CA2C1C,CAAA"}
@@ -0,0 +1,56 @@
1
+ // Depth-only prepass for the TRANSPARENT bucket. Transparent color draws keep
2
+ // depth write OFF so self-overlapping sheer cloth blends both layers instead of
3
+ // a triangle-order patchwork — but that leaves no depth record, so anything
4
+ // drawn later (the outline hulls) shows straight through the fabric as black
5
+ // shapes. This pass re-draws the transparent geometry depth-only AFTER its
6
+ // color pass: solid-enough texels (alpha ≥ 0.5) write depth, so outlines get
7
+ // occluded behind fabric exactly like they are behind opaque cloth, while
8
+ // truly sheer texels (a veil) stay non-occluding.
9
+ //
10
+ // Reuses mainPipelineLayout: camera g0b0, diffuseSampler g0b2, skinMats g1b0,
11
+ // diffuse texture g2b0, material uniforms g2b1 — the same bind groups the
12
+ // color draws already set, so drawing it costs no extra binding work.
13
+ export const TRANSPARENT_DEPTH_PREPASS_WGSL = /* wgsl */ `
14
+ struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
15
+ struct MaterialUniforms {
16
+ diffuseColor: vec3f,
17
+ alpha: f32,
18
+ };
19
+
20
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
21
+ @group(0) @binding(2) var diffuseSampler: sampler;
22
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
23
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
24
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
25
+
26
+ struct VSOut {
27
+ @builtin(position) position: vec4f,
28
+ @location(0) uv: vec2f,
29
+ };
30
+
31
+ @vertex fn vs(
32
+ @location(0) position: vec3f,
33
+ @location(1) normal: vec3f,
34
+ @location(2) uv: vec2f,
35
+ @location(3) joints0: vec4<u32>,
36
+ @location(4) weights0: vec4<f32>,
37
+ ) -> VSOut {
38
+ let pos4 = vec4f(position, 1.0);
39
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
40
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
41
+ let w = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
42
+ var skinned = vec4f(0.0);
43
+ for (var i = 0u; i < 4u; i++) {
44
+ skinned += (skinMats[joints0[i]] * pos4) * w[i];
45
+ }
46
+ var o: VSOut;
47
+ o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);
48
+ o.uv = uv;
49
+ return o;
50
+ }
51
+
52
+ @fragment fn fs(in: VSOut) {
53
+ let a = material.alpha * textureSample(diffuseTexture, diffuseSampler, in.uv).a;
54
+ if (a < 0.5) { discard; }
55
+ }
56
+ `;
@@ -1,2 +1,2 @@
1
- export declare const OUTLINE_SHADER_WGSL = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct MaterialUniforms {\n edgeColor: vec4f,\n edgeSize: f32,\n _padding1: f32,\n _padding2: f32,\n _padding3: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var<uniform> material: MaterialUniforms;\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n};\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n\n var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);\n var skinnedNrm = vec3f(0.0, 0.0, 0.0);\n for (var i = 0u; i < 4u; i++) {\n let j = joints0[i];\n let w = normalizedWeights[i];\n let m = skinMats[j];\n skinnedPos += (m * pos4) * w;\n let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);\n skinnedNrm += (r3 * normal) * w;\n }\n let worldPos = skinnedPos.xyz;\n let worldNormal = normalize(skinnedNrm);\n\n // Screen-space outline extrusion \u2014 MMD-style pixel-stable edge line.\n // 1. Project position and normal-as-direction to clip space.\n // 2. Normalize the 2D clip-space normal, aspect-compensated so \"one pixel horizontally\"\n // matches \"one pixel vertically\" (otherwise wide viewports squash the outline in X).\n // 3. Offset clip-space xy by (normal * edgeSize * edgeScale), then multiply by w\n // so the perspective divide cancels out \u2192 offset stays constant in NDC regardless\n // of depth, matching how MMD / babylon-mmd style outlines look identical when zooming.\n // 4. edgeScale is in NDC-y units per PMX edgeSize. \u2248 0.006 gives ~3px at 1080p; it's\n // tied to viewport HEIGHT so resizing the window keeps pixel thickness stable.\n let viewProj = camera.projection * camera.view;\n let clipPos = viewProj * vec4f(worldPos, 1.0);\n let clipNormal = (viewProj * vec4f(worldNormal, 0.0)).xy;\n // projection is column-major: proj[0][0] = 1/(aspect\u00B7tan(fov/2)), proj[1][1] = 1/tan(fov/2).\n // Ratio proj[1][1]/proj[0][0] recovers the viewport aspect (width/height).\n let aspect = camera.projection[1][1] / camera.projection[0][0];\n let pixelDir = normalize(vec2f(clipNormal.x * aspect, clipNormal.y));\n let ndcDir = vec2f(pixelDir.x / aspect, pixelDir.y);\n let edgeScale = 0.0016;\n let offset = ndcDir * material.edgeSize * edgeScale * clipPos.w;\n output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);\n return output;\n}\n\nstruct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };\n@fragment fn fs() -> FSOut {\n var out: FSOut;\n out.color = material.edgeColor;\n out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);\n return out;\n}\n";
1
+ export declare const OUTLINE_SHADER_WGSL = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n // Render-target height in device pixels (engine writes it each frame);\n // width is recovered via the projection matrix's aspect.\n viewportHeight: f32,\n};\n\nstruct MaterialUniforms {\n edgeColor: vec4f,\n edgeSize: f32,\n _padding1: f32,\n _padding2: f32,\n _padding3: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var edgeSampler: sampler;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var<uniform> material: MaterialUniforms;\n@group(2) @binding(1) var diffuseTexture: texture_2d<f32>;\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n};\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n\n var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);\n var skinnedNrm = vec3f(0.0, 0.0, 0.0);\n for (var i = 0u; i < 4u; i++) {\n let j = joints0[i];\n let w = normalizedWeights[i];\n let m = skinMats[j];\n skinnedPos += (m * pos4) * w;\n let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);\n skinnedNrm += (r3 * normal) * w;\n }\n let worldPos = skinnedPos.xyz;\n let worldNormal = normalize(skinnedNrm);\n\n let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);\n\n // babylon-mmd: screenNormal = normalize((view * worldNormal).xy)\n let viewNormal = (camera.view * vec4f(worldNormal, 0.0)).xyz;\n let snLen = length(viewNormal.xy);\n let screenNormal = select(vec2f(0.0, 0.0), viewNormal.xy / snLen, snLen > 1e-5);\n\n // Reference-height normalization (babylon-mmd ships this variant commented\n // out as `renderHeight = 1080`): thickness is a constant FRACTION of the\n // frame \u2014 2\u00B7edgeSize px at 1080p \u2014 so retina DPR and 4K export don't thin\n // the rims to sub-pixel. Width follows the projection aspect.\n // projection[1][1]/projection[0][0] = width/height for a symmetric frustum.\n let aspect = camera.projection[1][1] / camera.projection[0][0];\n let viewport = vec2f(1080.0 * aspect, 1080.0);\n\n // NDC offset = edgeSize \u00B7 4/viewport, \u00D7w so the perspective divide cancels:\n // constant screen thickness at any distance (babylon-mmd parity).\n let offset = screenNormal * (material.edgeSize * 4.0 / viewport) * clipPos.w;\n output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);\n output.uv = uv;\n return output;\n}\n\nstruct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };\n@fragment fn fs(input: VertexOutput) -> FSOut {\n // Rim alpha FOLLOWS the fabric's texture alpha instead of a hard alpha test:\n // MMD draws blend-material edges solid (only cutout materials alpha-test), so\n // a 0.4 discard erased the whole hull on semi-transparent cloth \u2014 stockinged\n // legs crossing lost their outline entirely. Modulating instead keeps a\n // proportional rim on sheer weave (never a solid black hull) and still\n // discards true cut-out margins like hair-card borders.\n let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;\n if (texA < 0.05) {\n discard;\n }\n var out: FSOut;\n out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);\n out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);\n return out;\n}\n";
2
2
  //# sourceMappingURL=outline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,klGAgF/B,CAAA"}
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,mBAAmB,ksHAgG/B,CAAA"}
@@ -1,11 +1,24 @@
1
- // MMD-style screen-space outline via normal-extrusion in clip space.
2
- // Aspect-compensated so pixel thickness stays stable across viewport sizes.
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
- _padding: f32,
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
- // Screen-space outline extrusion MMD-style pixel-stable edge line.
54
- // 1. Project position and normal-as-direction to clip space.
55
- // 2. Normalize the 2D clip-space normal, aspect-compensated so "one pixel horizontally"
56
- // matches "one pixel vertically" (otherwise wide viewports squash the outline in X).
57
- // 3. Offset clip-space xy by (normal * edgeSize * edgeScale), then multiply by w
58
- // so the perspective divide cancels out offset stays constant in NDC regardless
59
- // of depth, matching how MMD / babylon-mmd style outlines look identical when zooming.
60
- // 4. edgeScale is in NDC-y units per PMX edgeSize. ≈ 0.006 gives ~3px at 1080p; it's
61
- // tied to viewport HEIGHT so resizing the window keeps pixel thickness stable.
62
- let viewProj = camera.projection * camera.view;
63
- let clipPos = viewProj * vec4f(worldPos, 1.0);
64
- let clipNormal = (viewProj * vec4f(worldNormal, 0.0)).xy;
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 pixelDir = normalize(vec2f(clipNormal.x * aspect, clipNormal.y));
69
- let ndcDir = vec2f(pixelDir.x / aspect, pixelDir.y);
70
- let edgeScale = 0.0016;
71
- let offset = ndcDir * material.edgeSize * edgeScale * clipPos.w;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",