reze-engine 0.16.2 → 0.17.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.
Files changed (80) hide show
  1. package/dist/engine.d.ts +3 -1
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +165 -26
  4. package/dist/model.d.ts +5 -1
  5. package/dist/model.d.ts.map +1 -1
  6. package/dist/model.js +14 -1
  7. package/dist/physics/body.d.ts +4 -1
  8. package/dist/physics/body.d.ts.map +1 -1
  9. package/dist/physics/body.js +86 -20
  10. package/dist/physics/constraint.d.ts +15 -2
  11. package/dist/physics/constraint.d.ts.map +1 -1
  12. package/dist/physics/constraint.js +49 -2
  13. package/dist/physics/physics.d.ts +3 -0
  14. package/dist/physics/physics.d.ts.map +1 -1
  15. package/dist/physics/physics.js +69 -1
  16. package/dist/physics/solver.d.ts.map +1 -1
  17. package/dist/physics/solver.js +289 -113
  18. package/dist/physics/types.d.ts +2 -1
  19. package/dist/physics/types.d.ts.map +1 -1
  20. package/dist/physics/types.js +5 -0
  21. package/dist/physics/world.d.ts +3 -0
  22. package/dist/physics/world.d.ts.map +1 -1
  23. package/dist/physics/world.js +19 -3
  24. package/dist/pmx-loader.d.ts +2 -0
  25. package/dist/pmx-loader.d.ts.map +1 -1
  26. package/dist/pmx-loader.js +38 -20
  27. package/dist/shaders/materials/body.d.ts +1 -1
  28. package/dist/shaders/materials/body.d.ts.map +1 -1
  29. package/dist/shaders/materials/body.js +90 -85
  30. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  31. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
  32. package/dist/shaders/materials/cloth_rough.js +1 -1
  33. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  34. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
  35. package/dist/shaders/materials/cloth_smooth.js +2 -2
  36. package/dist/shaders/materials/common.d.ts +1 -1
  37. package/dist/shaders/materials/common.d.ts.map +1 -1
  38. package/dist/shaders/materials/common.js +137 -112
  39. package/dist/shaders/materials/default.d.ts +1 -1
  40. package/dist/shaders/materials/default.d.ts.map +1 -1
  41. package/dist/shaders/materials/default.js +37 -32
  42. package/dist/shaders/materials/eye.d.ts +1 -1
  43. package/dist/shaders/materials/eye.d.ts.map +1 -1
  44. package/dist/shaders/materials/eye.js +54 -35
  45. package/dist/shaders/materials/face.d.ts +1 -1
  46. package/dist/shaders/materials/face.d.ts.map +1 -1
  47. package/dist/shaders/materials/face.js +90 -85
  48. package/dist/shaders/materials/hair.d.ts +1 -1
  49. package/dist/shaders/materials/hair.d.ts.map +1 -1
  50. package/dist/shaders/materials/hair.js +85 -75
  51. package/dist/shaders/materials/metal.d.ts +1 -1
  52. package/dist/shaders/materials/metal.d.ts.map +1 -1
  53. package/dist/shaders/materials/metal.js +1 -1
  54. package/dist/shaders/materials/mmd_classic.d.ts +2 -0
  55. package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
  56. package/dist/shaders/materials/mmd_classic.js +66 -0
  57. package/dist/shaders/materials/stockings.d.ts +1 -1
  58. package/dist/shaders/materials/stockings.d.ts.map +1 -1
  59. package/dist/shaders/materials/stockings.js +1 -1
  60. package/package.json +1 -1
  61. package/src/engine.ts +4198 -4030
  62. package/src/model.ts +21 -1
  63. package/src/physics/body.ts +87 -24
  64. package/src/physics/constraint.ts +80 -6
  65. package/src/physics/physics.ts +68 -1
  66. package/src/physics/solver.ts +915 -744
  67. package/src/physics/types.ts +10 -2
  68. package/src/physics/world.ts +20 -3
  69. package/src/pmx-loader.ts +39 -20
  70. package/src/shaders/materials/body.ts +97 -92
  71. package/src/shaders/materials/cloth_rough.ts +1 -1
  72. package/src/shaders/materials/cloth_smooth.ts +2 -2
  73. package/src/shaders/materials/common.ts +204 -179
  74. package/src/shaders/materials/default.ts +45 -40
  75. package/src/shaders/materials/eye.ts +62 -43
  76. package/src/shaders/materials/face.ts +97 -92
  77. package/src/shaders/materials/hair.ts +92 -82
  78. package/src/shaders/materials/metal.ts +1 -1
  79. package/src/shaders/materials/mmd_classic.ts +68 -0
  80. package/src/shaders/materials/stockings.ts +1 -1
@@ -1,179 +1,204 @@
1
- // Shared WGSL blocks concatenated by every material shader.
2
- // Splits the boilerplate (uniform structs, bind group layout, skinning VS, PCF shadow)
3
- // away from the per-material fragment code so each material file only contains what
4
- // makes it visually distinct.
5
- //
6
- // Concat order in every material:
7
- // NODES_WGSL (nodes.ts — math/noise/BSDF helpers)
8
- // COMMON_BINDINGS_WGSL (uniform structs + @group/@binding declarations)
9
- // SAMPLE_SHADOW_WGSL (3×3 PCF shadow sampler; reads bindings above)
10
- // COMMON_VS_WGSL (skinning vertex shader; reads bindings above)
11
- // <material's own constants + @fragment fn fs>
12
- //
13
- // WGSL is a whole-module compile — declaration order at module scope doesn't matter,
14
- // but the readable order is: types → bindings → helpers → entry points.
15
-
16
- // ─── Uniform structs + bind group layout ────────────────────────────
17
- // Every material pipeline uses the same bind group layout, so the same bindings are
18
- // declared here once. Groups:
19
- // group(0): per-frame scene (camera, lights, shadow map, BRDF LUT via nodes.ts)
20
- // group(1): per-model skinning
21
- // group(2): per-material (diffuse texture + material uniforms)
22
-
23
- export const COMMON_BINDINGS_WGSL = /* wgsl */ `
24
-
25
- struct CameraUniforms {
26
- view: mat4x4f,
27
- projection: mat4x4f,
28
- viewPos: vec3f,
29
- _padding: f32,
30
- };
31
-
32
- struct Light {
33
- direction: vec4f,
34
- color: vec4f,
35
- };
36
-
37
- struct LightUniforms {
38
- ambientColor: vec4f,
39
- lights: array<Light, 4>,
40
- };
41
-
42
- // Per-material uniforms. Every material binds this layout even if it ignores fields;
43
- // the engine keeps one bind group layout across all material pipelines.
44
- struct MaterialUniforms {
45
- diffuseColor: vec3f, // tint; reserved (currently unused by all material fs)
46
- alpha: f32, // 0 → discard; <1 → transparent draw call
47
- };
48
-
49
- struct VertexOutput {
50
- @builtin(position) position: vec4f,
51
- @location(0) normal: vec3f,
52
- @location(1) uv: vec2f,
53
- @location(2) worldPos: vec3f,
54
- // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
55
- // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
56
- // worldPos so the pattern rides with the surface — otherwise the mesh swims through a
57
- // world-static noise field under any skinning deformation or root (センター) motion.
58
- // At rest skinMats are identity so restPos == worldPos, which is why existing noise-
59
- // scale constants stay valid without retuning.
60
- @location(3) restPos: vec3f,
61
- };
62
-
63
- struct LightVP { viewProj: mat4x4f, };
64
-
65
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
66
- @group(0) @binding(1) var<uniform> light: LightUniforms;
67
- @group(0) @binding(2) var diffuseSampler: sampler;
68
- @group(0) @binding(3) var shadowMap: texture_depth_2d;
69
- @group(0) @binding(4) var shadowSampler: sampler_comparison;
70
- @group(0) @binding(5) var<uniform> lightVP: LightVP;
71
- // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
72
- @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
73
- @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
74
- @group(2) @binding(1) var<uniform> material: MaterialUniforms;
75
-
76
- `;
77
-
78
- // ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
79
- // 2048-map, normal-bias 0.08, depth-bias 0.001. Unrolled — Safari's Metal backend
80
- // doesn't unroll nested shadow loops reliably, and the early out on back-facing
81
- // fragments saves 9 texture taps per skipped pixel.
82
-
83
- export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
84
-
85
- fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
86
- if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
87
- let biasedPos = worldPos + n * 0.08;
88
- let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
89
- let ndc = lclip.xyz / max(lclip.w, 1e-6);
90
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
91
- let cmpZ = ndc.z - 0.001;
92
- let ts = 1.0 / 2048.0;
93
- let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
94
- let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
95
- let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
96
- let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
97
- let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
98
- let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
99
- let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
100
- let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
101
- let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
102
- return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
103
- }
104
-
105
- `;
106
-
107
- // ─── Skinning vertex shader ─────────────────────────────────────────
108
- // Four-bone linear blend skinning. Renormalizes weights when they don't sum to 1
109
- // (PMX models occasionally ship with unnormalized weights on extras like hair tips).
110
- // VS normalize on the outgoing normal is skipped — interpolation denormalizes it
111
- // anyway and every fragment shader does `normalize(input.normal)` as its first line.
112
-
113
- export const COMMON_VS_WGSL = /* wgsl */ `
114
-
115
- @vertex fn vs(
116
- @location(0) position: vec3f,
117
- @location(1) normal: vec3f,
118
- @location(2) uv: vec2f,
119
- @location(3) joints0: vec4<u32>,
120
- @location(4) weights0: vec4<f32>
121
- ) -> VertexOutput {
122
- var output: VertexOutput;
123
- let pos4 = vec4f(position, 1.0);
124
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
125
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
126
- let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
127
- var skinnedPos = vec4f(0.0);
128
- var skinnedNrm = vec3f(0.0);
129
- for (var i = 0u; i < 4u; i++) {
130
- let m = skinMats[joints0[i]];
131
- let w = nw[i];
132
- skinnedPos += (m * pos4) * w;
133
- skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
134
- }
135
- output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
136
- output.normal = skinnedNrm;
137
- output.uv = uv;
138
- output.worldPos = skinnedPos.xyz;
139
- output.restPos = position;
140
- return output;
141
- }
142
-
143
- `;
144
-
145
- // ─── FS output struct ───────────────────────────────────────────────
146
- // Location 0: final radiance+alpha (blended into rg11b10ufloat; the HDR target
147
- // has no alpha channel, but the blend equation still uses the .a you write here
148
- // as the src-alpha factor that premultiplies rgb into the HDR target).
149
- // Location 1: auxiliary rg8unorm carrying
150
- // .r = bloom mask (1 = contributes to bloom, 0 = skip — e.g. ground).
151
- // .g = accumulated canvas alpha the channel that used to live in hdr.a
152
- // before the switch to rg11b10ufloat. Sampled by composite to
153
- // un-premultiply color for tonemap and to set the final drawable alpha
154
- // (needed for the `premultiplied` canvas alphaMode that blends the
155
- // WebGPU surface over the page background).
156
- // FS output at location 1 must be vec4f — the blend state references src.a, and
157
- // WebGPU requires the fragment output to provide an alpha component even though
158
- // the rg8unorm target only stores .r and .g (extra components are discarded).
159
- // Materials write mask = vec4f(1.0, 1.0, 0.0, color.a); ground writes
160
- // vec4f(0.0, 1.0, 0.0, edgeFade). With src.a coming from the 4th component and
161
- // src-alpha blending enabled:
162
- // out.r = mask_r · src.a + dst.r · (1-src.a) (bloom mask, weighted by alpha)
163
- // out.g = 1.0 · src.a + dst.g · (1-src.a) (canonical premultiplied alpha-over)
164
-
165
- export const COMMON_FS_OUT_WGSL = /* wgsl */ `
166
-
167
- struct FSOut {
168
- @location(0) color: vec4f,
169
- @location(1) mask: vec4f,
170
- };
171
-
172
- `;
173
-
174
- // ─── Convenience: full shared prelude ───────────────────────────────
175
- // Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
176
- // pull in everything structural. Each material then adds its own constants + fs().
177
-
178
- export const COMMON_MATERIAL_PRELUDE_WGSL =
179
- COMMON_BINDINGS_WGSL + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL + COMMON_FS_OUT_WGSL
1
+ // Shared WGSL blocks concatenated by every material shader.
2
+ // Splits the boilerplate (uniform structs, bind group layout, skinning VS, PCF shadow)
3
+ // away from the per-material fragment code so each material file only contains what
4
+ // makes it visually distinct.
5
+ //
6
+ // Concat order in every material:
7
+ // NODES_WGSL (nodes.ts — math/noise/BSDF helpers)
8
+ // COMMON_BINDINGS_WGSL (uniform structs + @group/@binding declarations)
9
+ // SAMPLE_SHADOW_WGSL (3×3 PCF shadow sampler; reads bindings above)
10
+ // COMMON_VS_WGSL (skinning vertex shader; reads bindings above)
11
+ // <material's own constants + @fragment fn fs>
12
+ //
13
+ // WGSL is a whole-module compile — declaration order at module scope doesn't matter,
14
+ // but the readable order is: types → bindings → helpers → entry points.
15
+
16
+ // ─── Uniform structs + bind group layout ────────────────────────────
17
+ // Every material pipeline uses the same bind group layout, so the same bindings are
18
+ // declared here once. Groups:
19
+ // group(0): per-frame scene (camera, lights, shadow map, BRDF LUT via nodes.ts)
20
+ // group(1): per-model skinning
21
+ // group(2): per-material (diffuse texture + material uniforms)
22
+
23
+ export const COMMON_BINDINGS_WGSL = /* wgsl */ `
24
+
25
+ struct CameraUniforms {
26
+ view: mat4x4f,
27
+ projection: mat4x4f,
28
+ viewPos: vec3f,
29
+ _padding: f32,
30
+ };
31
+
32
+ struct Light {
33
+ direction: vec4f,
34
+ color: vec4f,
35
+ };
36
+
37
+ struct LightUniforms {
38
+ ambientColor: vec4f,
39
+ lights: array<Light, 4>,
40
+ };
41
+
42
+ // Per-material uniforms. Every material binds this layout even if it ignores fields;
43
+ // the engine keeps one bind group layout across all material pipelines. The PMX
44
+ // classic-material fields (ambient/specular/toon/sphere) are consumed by the
45
+ // mmd_classic preset; NPR presets ignore them.
46
+ struct MaterialUniforms {
47
+ diffuseColor: vec3f, // PMX diffuse rgb (mmd_classic); reserved for others
48
+ alpha: f32, // 0 → discard; <1 → transparent draw call
49
+ ambient: vec3f, // PMX ambient rgb
50
+ shininess: f32, // PMX specular power
51
+ specular: vec3f, // PMX specular rgb
52
+ sphereMode: f32, // 0 none · 1 multiply (sph) · 2 add (spa)
53
+ // Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
54
+ // the post-alpha-eye stencil by camera-vs-face hemisphere.
55
+ headBoneIndex: f32,
56
+ _pad0: f32,
57
+ _pad1: f32,
58
+ _pad2: f32,
59
+ };
60
+
61
+ struct VertexOutput {
62
+ @builtin(position) position: vec4f,
63
+ @location(0) normal: vec3f,
64
+ @location(1) uv: vec2f,
65
+ @location(2) worldPos: vec3f,
66
+ // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
67
+ // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
68
+ // worldPos so the pattern rides with the surface — otherwise the mesh swims through a
69
+ // world-static noise field under any skinning deformation or root (センター) motion.
70
+ // At rest skinMats are identity so restPos == worldPos, which is why existing noise-
71
+ // scale constants stay valid without retuning.
72
+ @location(3) restPos: vec3f,
73
+ };
74
+
75
+ struct LightVP { viewProj: mat4x4f, };
76
+
77
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
78
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
79
+ @group(0) @binding(2) var diffuseSampler: sampler;
80
+ @group(0) @binding(3) var shadowMap: texture_depth_2d;
81
+ @group(0) @binding(4) var shadowSampler: sampler_comparison;
82
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
83
+ // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
84
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
85
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
86
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
87
+ // mmd_classic inputs; other presets leave them unread (fallback 1×1 whites).
88
+ @group(2) @binding(2) var toonTexture: texture_2d<f32>;
89
+ @group(2) @binding(3) var sphereTexture: texture_2d<f32>;
90
+
91
+ // Four-bone blended normals can cancel to ~zero on physics-driven parts
92
+ // (opposing bone rotations at 50/50 weights) — normalize(0) is 0/0 = NaN,
93
+ // which poisons the whole shading stack and flashes through bloom. Fall
94
+ // back to up for degenerate normals instead.
95
+ fn safe_normal(nIn: vec3f) -> vec3f {
96
+ let l2 = dot(nIn, nIn);
97
+ if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }
98
+ return nIn * inverseSqrt(l2);
99
+ }
100
+
101
+ `;
102
+
103
+ // ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
104
+ // 2048-map, normal-bias 0.08, depth-bias 0.001. Unrolled — Safari's Metal backend
105
+ // doesn't unroll nested shadow loops reliably, and the early out on back-facing
106
+ // fragments saves 9 texture taps per skipped pixel.
107
+
108
+ export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
109
+
110
+ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
111
+ if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
112
+ let biasedPos = worldPos + n * 0.08;
113
+ let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
114
+ let ndc = lclip.xyz / max(lclip.w, 1e-6);
115
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
116
+ let cmpZ = ndc.z - 0.001;
117
+ let ts = 1.0 / 2048.0;
118
+ let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
119
+ let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
120
+ let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
121
+ let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
122
+ let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
123
+ let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
124
+ let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
125
+ let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
126
+ let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
127
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
128
+ }
129
+
130
+ `;
131
+
132
+ // ─── Skinning vertex shader ─────────────────────────────────────────
133
+ // Four-bone linear blend skinning. Renormalizes weights when they don't sum to 1
134
+ // (PMX models occasionally ship with unnormalized weights on extras like hair tips).
135
+ // VS normalize on the outgoing normal is skipped — interpolation denormalizes it
136
+ // anyway and every fragment shader does `normalize(input.normal)` as its first line.
137
+
138
+ export const COMMON_VS_WGSL = /* wgsl */ `
139
+
140
+ @vertex fn vs(
141
+ @location(0) position: vec3f,
142
+ @location(1) normal: vec3f,
143
+ @location(2) uv: vec2f,
144
+ @location(3) joints0: vec4<u32>,
145
+ @location(4) weights0: vec4<f32>
146
+ ) -> VertexOutput {
147
+ var output: VertexOutput;
148
+ let pos4 = vec4f(position, 1.0);
149
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
150
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
151
+ let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
152
+ var skinnedPos = vec4f(0.0);
153
+ var skinnedNrm = vec3f(0.0);
154
+ for (var i = 0u; i < 4u; i++) {
155
+ let m = skinMats[joints0[i]];
156
+ let w = nw[i];
157
+ skinnedPos += (m * pos4) * w;
158
+ skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
159
+ }
160
+ output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
161
+ output.normal = skinnedNrm;
162
+ output.uv = uv;
163
+ output.worldPos = skinnedPos.xyz;
164
+ output.restPos = position;
165
+ return output;
166
+ }
167
+
168
+ `;
169
+
170
+ // ─── FS output struct ───────────────────────────────────────────────
171
+ // Location 0: final radiance+alpha (blended into rg11b10ufloat; the HDR target
172
+ // has no alpha channel, but the blend equation still uses the .a you write here
173
+ // as the src-alpha factor that premultiplies rgb into the HDR target).
174
+ // Location 1: auxiliary rg8unorm carrying
175
+ // .r = bloom mask (1 = contributes to bloom, 0 = skip — e.g. ground).
176
+ // .g = accumulated canvas alpha the channel that used to live in hdr.a
177
+ // before the switch to rg11b10ufloat. Sampled by composite to
178
+ // un-premultiply color for tonemap and to set the final drawable alpha
179
+ // (needed for the `premultiplied` canvas alphaMode that blends the
180
+ // WebGPU surface over the page background).
181
+ // FS output at location 1 must be vec4f — the blend state references src.a, and
182
+ // WebGPU requires the fragment output to provide an alpha component even though
183
+ // the rg8unorm target only stores .r and .g (extra components are discarded).
184
+ // Materials write mask = vec4f(1.0, 1.0, 0.0, color.a); ground writes
185
+ // vec4f(0.0, 1.0, 0.0, edgeFade). With src.a coming from the 4th component and
186
+ // src-alpha blending enabled:
187
+ // out.r = mask_r · src.a + dst.r · (1-src.a) (bloom mask, weighted by alpha)
188
+ // out.g = 1.0 · src.a + dst.g · (1-src.a) (canonical premultiplied alpha-over)
189
+
190
+ export const COMMON_FS_OUT_WGSL = /* wgsl */ `
191
+
192
+ struct FSOut {
193
+ @location(0) color: vec4f,
194
+ @location(1) mask: vec4f,
195
+ };
196
+
197
+ `;
198
+
199
+ // ─── Convenience: full shared prelude ───────────────────────────────
200
+ // Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
201
+ // pull in everything structural. Each material then adds its own constants + fs().
202
+
203
+ export const COMMON_MATERIAL_PRELUDE_WGSL =
204
+ COMMON_BINDINGS_WGSL + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL + COMMON_FS_OUT_WGSL
@@ -1,40 +1,45 @@
1
- // Default material — Blender 3.6 Principled BSDF defaults, no NPR stack.
2
- // Metallic=0, Specular=0.5 (F0=0.04), Roughness=0.5. Serves as the EEVEE reference
3
- // path that every NPR material mixes against in its final stage.
4
-
5
- import { NODES_WGSL } from "./nodes"
6
- import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
7
-
8
- export const DEFAULT_SHADER_WGSL = /* wgsl */ `
9
-
10
- ${NODES_WGSL}
11
- ${COMMON_MATERIAL_PRELUDE_WGSL}
12
-
13
- const DEFAULT_SPECULAR: f32 = 0.5;
14
- const DEFAULT_ROUGHNESS: f32 = 0.5;
15
-
16
- @fragment fn fs(input: VertexOutput) -> FSOut {
17
- let alpha = material.alpha;
18
- if (alpha < 0.001) { discard; }
19
-
20
- let n = normalize(input.normal);
21
- let v = normalize(camera.viewPos - input.worldPos);
22
- let l = -light.lights[0].direction.xyz;
23
- let sun = light.lights[0].color.xyz * light.lights[0].color.w;
24
- let amb = light.ambientColor.xyz;
25
- let shadow = sampleShadow(input.worldPos, n);
26
-
27
- let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
28
-
29
- let color = eval_principled(
30
- PrincipledIn(albedo, 0.0, DEFAULT_SPECULAR, DEFAULT_ROUGHNESS, 1e30, 0.0, 0.0),
31
- n, l, v, sun, amb, shadow
32
- );
33
-
34
- var out: FSOut;
35
- out.color = vec4f(color, alpha);
36
- out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
37
- return out;
38
- }
39
-
40
- `
1
+ // Default material — Blender 3.6 Principled BSDF defaults, no NPR stack.
2
+ // Metallic=0, Specular=0.5 (F0=0.04), Roughness=0.5. Serves as the EEVEE reference
3
+ // path that every NPR material mixes against in its final stage.
4
+
5
+ import { NODES_WGSL } from "./nodes"
6
+ import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
7
+
8
+ export const DEFAULT_SHADER_WGSL = /* wgsl */ `
9
+
10
+ ${NODES_WGSL}
11
+ ${COMMON_MATERIAL_PRELUDE_WGSL}
12
+
13
+ const DEFAULT_SPECULAR: f32 = 0.5;
14
+ const DEFAULT_ROUGHNESS: f32 = 0.5;
15
+
16
+ @fragment fn fs(input: VertexOutput) -> FSOut {
17
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
18
+ // MMD alpha semantics: material alpha × texture alpha. Hair/lace/accessory
19
+ // textures cut their shapes in the alpha channel — ignoring it renders each
20
+ // card's full quad with the texture's padding color (white shimmer on dark
21
+ // hair for models whose textures pad with white).
22
+ let alpha = material.alpha * tex_s.a;
23
+ if (alpha < 0.001) { discard; }
24
+
25
+ let n = safe_normal(input.normal);
26
+ let v = normalize(camera.viewPos - input.worldPos);
27
+ let l = -light.lights[0].direction.xyz;
28
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
29
+ let amb = light.ambientColor.xyz;
30
+ let shadow = sampleShadow(input.worldPos, n);
31
+
32
+ let albedo = tex_s.rgb;
33
+
34
+ let color = eval_principled(
35
+ PrincipledIn(albedo, 0.0, DEFAULT_SPECULAR, DEFAULT_ROUGHNESS, 10.0, 0.0, 0.0),
36
+ n, l, v, sun, amb, shadow
37
+ );
38
+
39
+ var out: FSOut;
40
+ out.color = vec4f(color, alpha);
41
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
42
+ return out;
43
+ }
44
+
45
+ `
@@ -1,43 +1,62 @@
1
- // Eye preset — default Principled BSDF + Emission socket set to albedo × 1.5.
2
- // Matches the published preset author's instruction: "keep eyes in the default
3
- // nodegraph, add emission 1.5". Emission feeds bloom pre-tonemap.
4
-
5
- import { NODES_WGSL } from "./nodes"
6
- import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
7
-
8
- export const EYE_SHADER_WGSL = /* wgsl */ `
9
-
10
- ${NODES_WGSL}
11
- ${COMMON_MATERIAL_PRELUDE_WGSL}
12
-
13
- const EYE_SPECULAR: f32 = 0.5;
14
- const EYE_ROUGHNESS: f32 = 0.5;
15
- const EYE_EMISSION_STRENGTH: f32 = 1.5;
16
-
17
- @fragment fn fs(input: VertexOutput) -> FSOut {
18
- let alpha = material.alpha;
19
- if (alpha < 0.001) { discard; }
20
-
21
- let n = normalize(input.normal);
22
- let v = normalize(camera.viewPos - input.worldPos);
23
- let l = -light.lights[0].direction.xyz;
24
- let sun = light.lights[0].color.xyz * light.lights[0].color.w;
25
- let amb = light.ambientColor.xyz;
26
- let shadow = sampleShadow(input.worldPos, n);
27
-
28
- let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
29
-
30
- let shaded = eval_principled(
31
- PrincipledIn(albedo, 0.0, EYE_SPECULAR, EYE_ROUGHNESS, 1e30, 0.0, 0.0),
32
- n, l, v, sun, amb, shadow
33
- );
34
- // Principled Emission socket: emissive = emission_color × strength, added on top.
35
- let emission = albedo * EYE_EMISSION_STRENGTH;
36
-
37
- var out: FSOut;
38
- out.color = vec4f(shaded + emission, alpha);
39
- out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
40
- return out;
41
- }
42
-
43
- `
1
+ // Eye preset — default Principled BSDF + Emission socket set to albedo × 1.5.
2
+ // Matches the published preset author's instruction: "keep eyes in the default
3
+ // nodegraph, add emission 1.5". Emission feeds bloom pre-tonemap.
4
+
5
+ import { NODES_WGSL } from "./nodes"
6
+ import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
7
+
8
+ export const EYE_SHADER_WGSL = /* wgsl */ `
9
+
10
+ ${NODES_WGSL}
11
+ ${COMMON_MATERIAL_PRELUDE_WGSL}
12
+
13
+ const EYE_SPECULAR: f32 = 0.5;
14
+ const EYE_ROUGHNESS: f32 = 0.5;
15
+ const EYE_EMISSION_STRENGTH: f32 = 1.5;
16
+
17
+ @fragment fn fs(input: VertexOutput) -> FSOut {
18
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
19
+ // MMD alpha semantics: material alpha × texture alpha. Hair/lace/accessory
20
+ // textures cut their shapes in the alpha channel — ignoring it renders each
21
+ // card's full quad with the texture's padding color (white shimmer on dark
22
+ // hair for models whose textures pad with white).
23
+ let alpha = material.alpha * tex_s.a;
24
+ if (alpha < 0.001) { discard; }
25
+
26
+ let n = safe_normal(input.normal);
27
+ let v = normalize(camera.viewPos - input.worldPos);
28
+
29
+ // Rear-view gate for the post-alpha eye. Many PMX heads are open shells
30
+ // (no skull mesh), so from behind nothing occludes the eye — it would draw
31
+ // and stamp the see-through stencil straight through the back hair. Mesh
32
+ // normals/winding are unreliable (double-sided eye geometry), so gate by
33
+ // camera-vs-face hemisphere instead: the 頭 bone's skinning matrix carries
34
+ // the head's world rotation (PMX bind pose is rotationless), and PMX
35
+ // models face −Z locally. Discard drops color, depth, and stencil.
36
+ if (material.headBoneIndex >= 0.0) {
37
+ let hm = skinMats[u32(material.headBoneIndex)];
38
+ let faceDir = -normalize(hm[2].xyz);
39
+ if (dot(faceDir, v) < -0.15) { discard; }
40
+ }
41
+
42
+ let l = -light.lights[0].direction.xyz;
43
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
44
+ let amb = light.ambientColor.xyz;
45
+ let shadow = sampleShadow(input.worldPos, n);
46
+
47
+ let albedo = tex_s.rgb;
48
+
49
+ let shaded = eval_principled(
50
+ PrincipledIn(albedo, 0.0, EYE_SPECULAR, EYE_ROUGHNESS, 1e30, 0.0, 0.0),
51
+ n, l, v, sun, amb, shadow
52
+ );
53
+ // Principled Emission socket: emissive = emission_color × strength, added on top.
54
+ let emission = albedo * EYE_EMISSION_STRENGTH;
55
+
56
+ var out: FSOut;
57
+ out.color = vec4f(shaded + emission, alpha);
58
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
59
+ return out;
60
+ }
61
+
62
+ `