reze-engine 0.1.5 → 0.1.6
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 +99 -99
- package/dist/engine.d.ts +15 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +892 -243
- package/dist/model.d.ts +3 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +26 -2
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +1826 -1136
- package/src/math.ts +546 -546
- package/src/model.ts +421 -418
- package/src/physics.ts +680 -680
- package/src/pmx-loader.ts +1060 -1031
package/dist/engine.js
CHANGED
|
@@ -28,8 +28,14 @@ export class Engine {
|
|
|
28
28
|
};
|
|
29
29
|
this.animationFrameId = null;
|
|
30
30
|
this.renderLoopCallback = null;
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
31
|
+
this.opaqueNonEyeNonHairDraws = [];
|
|
32
|
+
this.eyeDraws = [];
|
|
33
|
+
this.hairDraws = [];
|
|
34
|
+
this.transparentNonEyeNonHairDraws = [];
|
|
35
|
+
this.opaqueNonEyeNonHairOutlineDraws = [];
|
|
36
|
+
this.eyeOutlineDraws = [];
|
|
37
|
+
this.hairOutlineDraws = [];
|
|
38
|
+
this.transparentNonEyeNonHairOutlineDraws = [];
|
|
33
39
|
this.canvas = canvas;
|
|
34
40
|
}
|
|
35
41
|
// Step 1: Get WebGPU device and context
|
|
@@ -66,118 +72,251 @@ export class Engine {
|
|
|
66
72
|
});
|
|
67
73
|
const shaderModule = this.device.createShaderModule({
|
|
68
74
|
label: "model shaders",
|
|
69
|
-
code: /* wgsl */ `
|
|
70
|
-
struct CameraUniforms {
|
|
71
|
-
view: mat4x4f,
|
|
72
|
-
projection: mat4x4f,
|
|
73
|
-
viewPos: vec3f,
|
|
74
|
-
_padding: f32,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
struct Light {
|
|
78
|
-
direction: vec3f,
|
|
79
|
-
_padding1: f32,
|
|
80
|
-
color: vec3f,
|
|
81
|
-
intensity: f32,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
struct LightUniforms {
|
|
85
|
-
ambient: f32,
|
|
86
|
-
lightCount: f32,
|
|
87
|
-
_padding1: f32,
|
|
88
|
-
_padding2: f32,
|
|
89
|
-
lights: array<Light, 4>,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
struct MaterialUniforms {
|
|
93
|
-
alpha: f32,
|
|
94
|
-
_padding1: f32,
|
|
95
|
-
_padding2: f32,
|
|
96
|
-
_padding3: f32,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
struct VertexOutput {
|
|
100
|
-
@builtin(position) position: vec4f,
|
|
101
|
-
@location(0) normal: vec3f,
|
|
102
|
-
@location(1) uv: vec2f,
|
|
103
|
-
@location(2) worldPos: vec3f,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
107
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
108
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
109
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
110
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
111
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
112
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
113
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
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
|
-
|
|
125
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
126
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
127
|
-
var normalizedWeights: vec4f;
|
|
128
|
-
if (weightSum > 0.0001) {
|
|
129
|
-
normalizedWeights = weights0 / weightSum;
|
|
130
|
-
} else {
|
|
131
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
135
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
136
|
-
for (var i = 0u; i < 4u; i++) {
|
|
137
|
-
let j = joints0[i];
|
|
138
|
-
let w = normalizedWeights[i];
|
|
139
|
-
let m = skinMats[j];
|
|
140
|
-
skinnedPos += (m * pos4) * w;
|
|
141
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
142
|
-
skinnedNrm += (r3 * normal) * w;
|
|
143
|
-
}
|
|
144
|
-
let worldPos = skinnedPos.xyz;
|
|
145
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
146
|
-
output.normal = normalize(skinnedNrm);
|
|
147
|
-
output.uv = uv;
|
|
148
|
-
output.worldPos = worldPos;
|
|
149
|
-
return output;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
153
|
-
let n = normalize(input.normal);
|
|
154
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
155
|
-
|
|
156
|
-
var lightAccum = vec3f(light.ambient);
|
|
157
|
-
let numLights = u32(light.lightCount);
|
|
158
|
-
for (var i = 0u; i < numLights; i++) {
|
|
159
|
-
let l = -light.lights[i].direction;
|
|
160
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
161
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
162
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
163
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
164
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let color = albedo * lightAccum;
|
|
168
|
-
let finalAlpha = material.alpha;
|
|
169
|
-
if (finalAlpha < 0.001) {
|
|
170
|
-
discard;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
174
|
-
}
|
|
75
|
+
code: /* wgsl */ `
|
|
76
|
+
struct CameraUniforms {
|
|
77
|
+
view: mat4x4f,
|
|
78
|
+
projection: mat4x4f,
|
|
79
|
+
viewPos: vec3f,
|
|
80
|
+
_padding: f32,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
struct Light {
|
|
84
|
+
direction: vec3f,
|
|
85
|
+
_padding1: f32,
|
|
86
|
+
color: vec3f,
|
|
87
|
+
intensity: f32,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
struct LightUniforms {
|
|
91
|
+
ambient: f32,
|
|
92
|
+
lightCount: f32,
|
|
93
|
+
_padding1: f32,
|
|
94
|
+
_padding2: f32,
|
|
95
|
+
lights: array<Light, 4>,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
struct MaterialUniforms {
|
|
99
|
+
alpha: f32,
|
|
100
|
+
_padding1: f32,
|
|
101
|
+
_padding2: f32,
|
|
102
|
+
_padding3: f32,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
struct VertexOutput {
|
|
106
|
+
@builtin(position) position: vec4f,
|
|
107
|
+
@location(0) normal: vec3f,
|
|
108
|
+
@location(1) uv: vec2f,
|
|
109
|
+
@location(2) worldPos: vec3f,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
113
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
114
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
115
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
116
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
117
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
118
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
119
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
120
|
+
|
|
121
|
+
@vertex fn vs(
|
|
122
|
+
@location(0) position: vec3f,
|
|
123
|
+
@location(1) normal: vec3f,
|
|
124
|
+
@location(2) uv: vec2f,
|
|
125
|
+
@location(3) joints0: vec4<u32>,
|
|
126
|
+
@location(4) weights0: vec4<f32>
|
|
127
|
+
) -> VertexOutput {
|
|
128
|
+
var output: VertexOutput;
|
|
129
|
+
let pos4 = vec4f(position, 1.0);
|
|
130
|
+
|
|
131
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
132
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
133
|
+
var normalizedWeights: vec4f;
|
|
134
|
+
if (weightSum > 0.0001) {
|
|
135
|
+
normalizedWeights = weights0 / weightSum;
|
|
136
|
+
} else {
|
|
137
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
141
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
142
|
+
for (var i = 0u; i < 4u; i++) {
|
|
143
|
+
let j = joints0[i];
|
|
144
|
+
let w = normalizedWeights[i];
|
|
145
|
+
let m = skinMats[j];
|
|
146
|
+
skinnedPos += (m * pos4) * w;
|
|
147
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
148
|
+
skinnedNrm += (r3 * normal) * w;
|
|
149
|
+
}
|
|
150
|
+
let worldPos = skinnedPos.xyz;
|
|
151
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
152
|
+
output.normal = normalize(skinnedNrm);
|
|
153
|
+
output.uv = uv;
|
|
154
|
+
output.worldPos = worldPos;
|
|
155
|
+
return output;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
159
|
+
let n = normalize(input.normal);
|
|
160
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
161
|
+
|
|
162
|
+
var lightAccum = vec3f(light.ambient);
|
|
163
|
+
let numLights = u32(light.lightCount);
|
|
164
|
+
for (var i = 0u; i < numLights; i++) {
|
|
165
|
+
let l = -light.lights[i].direction;
|
|
166
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
167
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
168
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
169
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
170
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let color = albedo * lightAccum;
|
|
174
|
+
let finalAlpha = material.alpha;
|
|
175
|
+
if (finalAlpha < 0.001) {
|
|
176
|
+
discard;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
180
|
+
}
|
|
181
|
+
`,
|
|
182
|
+
});
|
|
183
|
+
// Create a separate shader for hair-over-eyes that outputs pre-multiplied color for darkening effect
|
|
184
|
+
const hairMultiplyShaderModule = this.device.createShaderModule({
|
|
185
|
+
label: "hair multiply shaders",
|
|
186
|
+
code: /* wgsl */ `
|
|
187
|
+
struct CameraUniforms {
|
|
188
|
+
view: mat4x4f,
|
|
189
|
+
projection: mat4x4f,
|
|
190
|
+
viewPos: vec3f,
|
|
191
|
+
_padding: f32,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
struct Light {
|
|
195
|
+
direction: vec3f,
|
|
196
|
+
_padding1: f32,
|
|
197
|
+
color: vec3f,
|
|
198
|
+
intensity: f32,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
struct LightUniforms {
|
|
202
|
+
ambient: f32,
|
|
203
|
+
lightCount: f32,
|
|
204
|
+
_padding1: f32,
|
|
205
|
+
_padding2: f32,
|
|
206
|
+
lights: array<Light, 4>,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
struct MaterialUniforms {
|
|
210
|
+
alpha: f32,
|
|
211
|
+
_padding1: f32,
|
|
212
|
+
_padding2: f32,
|
|
213
|
+
_padding3: f32,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
struct VertexOutput {
|
|
217
|
+
@builtin(position) position: vec4f,
|
|
218
|
+
@location(0) normal: vec3f,
|
|
219
|
+
@location(1) uv: vec2f,
|
|
220
|
+
@location(2) worldPos: vec3f,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
224
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
225
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
226
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
227
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
228
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
229
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
230
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
231
|
+
|
|
232
|
+
@vertex fn vs(
|
|
233
|
+
@location(0) position: vec3f,
|
|
234
|
+
@location(1) normal: vec3f,
|
|
235
|
+
@location(2) uv: vec2f,
|
|
236
|
+
@location(3) joints0: vec4<u32>,
|
|
237
|
+
@location(4) weights0: vec4<f32>
|
|
238
|
+
) -> VertexOutput {
|
|
239
|
+
var output: VertexOutput;
|
|
240
|
+
let pos4 = vec4f(position, 1.0);
|
|
241
|
+
|
|
242
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
243
|
+
var normalizedWeights: vec4f;
|
|
244
|
+
if (weightSum > 0.0001) {
|
|
245
|
+
normalizedWeights = weights0 / weightSum;
|
|
246
|
+
} else {
|
|
247
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
251
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
252
|
+
for (var i = 0u; i < 4u; i++) {
|
|
253
|
+
let j = joints0[i];
|
|
254
|
+
let w = normalizedWeights[i];
|
|
255
|
+
let m = skinMats[j];
|
|
256
|
+
skinnedPos += (m * pos4) * w;
|
|
257
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
258
|
+
skinnedNrm += (r3 * normal) * w;
|
|
259
|
+
}
|
|
260
|
+
let worldPos = skinnedPos.xyz;
|
|
261
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
262
|
+
output.normal = normalize(skinnedNrm);
|
|
263
|
+
output.uv = uv;
|
|
264
|
+
output.worldPos = worldPos;
|
|
265
|
+
return output;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
269
|
+
let n = normalize(input.normal);
|
|
270
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
271
|
+
|
|
272
|
+
var lightAccum = vec3f(light.ambient);
|
|
273
|
+
let numLights = u32(light.lightCount);
|
|
274
|
+
for (var i = 0u; i < numLights; i++) {
|
|
275
|
+
let l = -light.lights[i].direction;
|
|
276
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
277
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
278
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
279
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
280
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let color = albedo * lightAccum;
|
|
284
|
+
let finalAlpha = material.alpha;
|
|
285
|
+
if (finalAlpha < 0.001) {
|
|
286
|
+
discard;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// For hair-over-eyes effect: simple half-transparent overlay
|
|
290
|
+
// Use 60% opacity to create a semi-transparent hair color overlay
|
|
291
|
+
let overlayAlpha = finalAlpha * 0.6;
|
|
292
|
+
|
|
293
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), overlayAlpha);
|
|
294
|
+
}
|
|
175
295
|
`,
|
|
176
296
|
});
|
|
297
|
+
// Create explicit bind group layout for all pipelines using the main shader
|
|
298
|
+
// This ensures compatibility across all pipelines (main, eye, hair multiply, hair opaque)
|
|
299
|
+
this.hairBindGroupLayout = this.device.createBindGroupLayout({
|
|
300
|
+
label: "shared material bind group layout",
|
|
301
|
+
entries: [
|
|
302
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
303
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
|
|
304
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // diffuseTexture
|
|
305
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // diffuseSampler
|
|
306
|
+
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
307
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // toonTexture
|
|
308
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // toonSampler
|
|
309
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
const sharedPipelineLayout = this.device.createPipelineLayout({
|
|
313
|
+
label: "shared pipeline layout",
|
|
314
|
+
bindGroupLayouts: [this.hairBindGroupLayout],
|
|
315
|
+
});
|
|
177
316
|
// Single pipeline for all materials with alpha blending
|
|
178
317
|
this.pipeline = this.device.createRenderPipeline({
|
|
179
318
|
label: "model pipeline",
|
|
180
|
-
layout:
|
|
319
|
+
layout: sharedPipelineLayout,
|
|
181
320
|
vertex: {
|
|
182
321
|
module: shaderModule,
|
|
183
322
|
buffers: [
|
|
@@ -221,7 +360,7 @@ export class Engine {
|
|
|
221
360
|
},
|
|
222
361
|
primitive: { cullMode: "none" },
|
|
223
362
|
depthStencil: {
|
|
224
|
-
format: "depth24plus",
|
|
363
|
+
format: "depth24plus-stencil8",
|
|
225
364
|
depthWriteEnabled: true,
|
|
226
365
|
depthCompare: "less",
|
|
227
366
|
},
|
|
@@ -229,79 +368,92 @@ export class Engine {
|
|
|
229
368
|
count: this.sampleCount,
|
|
230
369
|
},
|
|
231
370
|
});
|
|
371
|
+
// Create bind group layout for outline pipelines
|
|
372
|
+
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
373
|
+
label: "outline bind group layout",
|
|
374
|
+
entries: [
|
|
375
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
376
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
377
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
381
|
+
label: "outline pipeline layout",
|
|
382
|
+
bindGroupLayouts: [this.outlineBindGroupLayout],
|
|
383
|
+
});
|
|
232
384
|
const outlineShaderModule = this.device.createShaderModule({
|
|
233
385
|
label: "outline shaders",
|
|
234
|
-
code: /* wgsl */ `
|
|
235
|
-
struct CameraUniforms {
|
|
236
|
-
view: mat4x4f,
|
|
237
|
-
projection: mat4x4f,
|
|
238
|
-
viewPos: vec3f,
|
|
239
|
-
_padding: f32,
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
struct MaterialUniforms {
|
|
243
|
-
edgeColor: vec4f,
|
|
244
|
-
edgeSize: f32,
|
|
245
|
-
_padding1: f32,
|
|
246
|
-
_padding2: f32,
|
|
247
|
-
_padding3: f32,
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
251
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
252
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
253
|
-
|
|
254
|
-
struct VertexOutput {
|
|
255
|
-
@builtin(position) position: vec4f,
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
@vertex fn vs(
|
|
259
|
-
@location(0) position: vec3f,
|
|
260
|
-
@location(1) normal: vec3f,
|
|
261
|
-
@location(2) uv: vec2f,
|
|
262
|
-
@location(3) joints0: vec4<u32>,
|
|
263
|
-
@location(4) weights0: vec4<f32>
|
|
264
|
-
) -> VertexOutput {
|
|
265
|
-
var output: VertexOutput;
|
|
266
|
-
let pos4 = vec4f(position, 1.0);
|
|
267
|
-
|
|
268
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
269
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
270
|
-
var normalizedWeights: vec4f;
|
|
271
|
-
if (weightSum > 0.0001) {
|
|
272
|
-
normalizedWeights = weights0 / weightSum;
|
|
273
|
-
} else {
|
|
274
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
278
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
279
|
-
for (var i = 0u; i < 4u; i++) {
|
|
280
|
-
let j = joints0[i];
|
|
281
|
-
let w = normalizedWeights[i];
|
|
282
|
-
let m = skinMats[j];
|
|
283
|
-
skinnedPos += (m * pos4) * w;
|
|
284
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
285
|
-
skinnedNrm += (r3 * normal) * w;
|
|
286
|
-
}
|
|
287
|
-
let worldPos = skinnedPos.xyz;
|
|
288
|
-
let worldNormal = normalize(skinnedNrm);
|
|
289
|
-
|
|
290
|
-
// MMD invert hull: expand vertices outward along normals
|
|
291
|
-
let scaleFactor = 0.01;
|
|
292
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
293
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
294
|
-
return output;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
298
|
-
return material.edgeColor;
|
|
299
|
-
}
|
|
386
|
+
code: /* wgsl */ `
|
|
387
|
+
struct CameraUniforms {
|
|
388
|
+
view: mat4x4f,
|
|
389
|
+
projection: mat4x4f,
|
|
390
|
+
viewPos: vec3f,
|
|
391
|
+
_padding: f32,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
struct MaterialUniforms {
|
|
395
|
+
edgeColor: vec4f,
|
|
396
|
+
edgeSize: f32,
|
|
397
|
+
_padding1: f32,
|
|
398
|
+
_padding2: f32,
|
|
399
|
+
_padding3: f32,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
403
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
404
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
405
|
+
|
|
406
|
+
struct VertexOutput {
|
|
407
|
+
@builtin(position) position: vec4f,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
@vertex fn vs(
|
|
411
|
+
@location(0) position: vec3f,
|
|
412
|
+
@location(1) normal: vec3f,
|
|
413
|
+
@location(2) uv: vec2f,
|
|
414
|
+
@location(3) joints0: vec4<u32>,
|
|
415
|
+
@location(4) weights0: vec4<f32>
|
|
416
|
+
) -> VertexOutput {
|
|
417
|
+
var output: VertexOutput;
|
|
418
|
+
let pos4 = vec4f(position, 1.0);
|
|
419
|
+
|
|
420
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
421
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
422
|
+
var normalizedWeights: vec4f;
|
|
423
|
+
if (weightSum > 0.0001) {
|
|
424
|
+
normalizedWeights = weights0 / weightSum;
|
|
425
|
+
} else {
|
|
426
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
430
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
431
|
+
for (var i = 0u; i < 4u; i++) {
|
|
432
|
+
let j = joints0[i];
|
|
433
|
+
let w = normalizedWeights[i];
|
|
434
|
+
let m = skinMats[j];
|
|
435
|
+
skinnedPos += (m * pos4) * w;
|
|
436
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
437
|
+
skinnedNrm += (r3 * normal) * w;
|
|
438
|
+
}
|
|
439
|
+
let worldPos = skinnedPos.xyz;
|
|
440
|
+
let worldNormal = normalize(skinnedNrm);
|
|
441
|
+
|
|
442
|
+
// MMD invert hull: expand vertices outward along normals
|
|
443
|
+
let scaleFactor = 0.01;
|
|
444
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
445
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
446
|
+
return output;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
450
|
+
return material.edgeColor;
|
|
451
|
+
}
|
|
300
452
|
`,
|
|
301
453
|
});
|
|
302
454
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
303
455
|
label: "outline pipeline",
|
|
304
|
-
layout:
|
|
456
|
+
layout: outlinePipelineLayout,
|
|
305
457
|
vertex: {
|
|
306
458
|
module: outlineShaderModule,
|
|
307
459
|
buffers: [
|
|
@@ -359,7 +511,7 @@ export class Engine {
|
|
|
359
511
|
cullMode: "back",
|
|
360
512
|
},
|
|
361
513
|
depthStencil: {
|
|
362
|
-
format: "depth24plus",
|
|
514
|
+
format: "depth24plus-stencil8",
|
|
363
515
|
depthWriteEnabled: true,
|
|
364
516
|
depthCompare: "less",
|
|
365
517
|
},
|
|
@@ -367,36 +519,400 @@ export class Engine {
|
|
|
367
519
|
count: this.sampleCount,
|
|
368
520
|
},
|
|
369
521
|
});
|
|
522
|
+
// Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1)
|
|
523
|
+
// Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
|
|
524
|
+
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
525
|
+
label: "hair outline pipeline",
|
|
526
|
+
layout: outlinePipelineLayout,
|
|
527
|
+
vertex: {
|
|
528
|
+
module: outlineShaderModule,
|
|
529
|
+
buffers: [
|
|
530
|
+
{
|
|
531
|
+
arrayStride: 8 * 4,
|
|
532
|
+
attributes: [
|
|
533
|
+
{
|
|
534
|
+
shaderLocation: 0,
|
|
535
|
+
offset: 0,
|
|
536
|
+
format: "float32x3",
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
shaderLocation: 1,
|
|
540
|
+
offset: 3 * 4,
|
|
541
|
+
format: "float32x3",
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
shaderLocation: 2,
|
|
545
|
+
offset: 6 * 4,
|
|
546
|
+
format: "float32x2",
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
arrayStride: 4 * 2,
|
|
552
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
arrayStride: 4,
|
|
556
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
fragment: {
|
|
561
|
+
module: outlineShaderModule,
|
|
562
|
+
targets: [
|
|
563
|
+
{
|
|
564
|
+
format: this.presentationFormat,
|
|
565
|
+
blend: {
|
|
566
|
+
color: {
|
|
567
|
+
srcFactor: "src-alpha",
|
|
568
|
+
dstFactor: "one-minus-src-alpha",
|
|
569
|
+
operation: "add",
|
|
570
|
+
},
|
|
571
|
+
alpha: {
|
|
572
|
+
srcFactor: "one",
|
|
573
|
+
dstFactor: "one-minus-src-alpha",
|
|
574
|
+
operation: "add",
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
primitive: {
|
|
581
|
+
cullMode: "back",
|
|
582
|
+
},
|
|
583
|
+
depthStencil: {
|
|
584
|
+
format: "depth24plus-stencil8",
|
|
585
|
+
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
586
|
+
depthCompare: "less-equal", // Only draw where hair depth exists
|
|
587
|
+
stencilFront: {
|
|
588
|
+
compare: "not-equal", // Only render where stencil != 1 (not over eyes)
|
|
589
|
+
failOp: "keep",
|
|
590
|
+
depthFailOp: "keep",
|
|
591
|
+
passOp: "keep",
|
|
592
|
+
},
|
|
593
|
+
stencilBack: {
|
|
594
|
+
compare: "not-equal",
|
|
595
|
+
failOp: "keep",
|
|
596
|
+
depthFailOp: "keep",
|
|
597
|
+
passOp: "keep",
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
multisample: {
|
|
601
|
+
count: this.sampleCount,
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
// Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists
|
|
605
|
+
// Uses depth compare "equal" with a small bias to only appear where hair geometry exists
|
|
606
|
+
this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
|
|
607
|
+
label: "hair outline over eyes pipeline",
|
|
608
|
+
layout: outlinePipelineLayout,
|
|
609
|
+
vertex: {
|
|
610
|
+
module: outlineShaderModule,
|
|
611
|
+
buffers: [
|
|
612
|
+
{
|
|
613
|
+
arrayStride: 8 * 4,
|
|
614
|
+
attributes: [
|
|
615
|
+
{
|
|
616
|
+
shaderLocation: 0,
|
|
617
|
+
offset: 0,
|
|
618
|
+
format: "float32x3",
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
shaderLocation: 1,
|
|
622
|
+
offset: 3 * 4,
|
|
623
|
+
format: "float32x3",
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
shaderLocation: 2,
|
|
627
|
+
offset: 6 * 4,
|
|
628
|
+
format: "float32x2",
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
arrayStride: 4 * 2,
|
|
634
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
arrayStride: 4,
|
|
638
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
fragment: {
|
|
643
|
+
module: outlineShaderModule,
|
|
644
|
+
targets: [
|
|
645
|
+
{
|
|
646
|
+
format: this.presentationFormat,
|
|
647
|
+
blend: {
|
|
648
|
+
color: {
|
|
649
|
+
srcFactor: "src-alpha",
|
|
650
|
+
dstFactor: "one-minus-src-alpha",
|
|
651
|
+
operation: "add",
|
|
652
|
+
},
|
|
653
|
+
alpha: {
|
|
654
|
+
srcFactor: "one",
|
|
655
|
+
dstFactor: "one-minus-src-alpha",
|
|
656
|
+
operation: "add",
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
},
|
|
662
|
+
primitive: {
|
|
663
|
+
cullMode: "back",
|
|
664
|
+
},
|
|
665
|
+
depthStencil: {
|
|
666
|
+
format: "depth24plus-stencil8",
|
|
667
|
+
depthWriteEnabled: false, // Don't write depth
|
|
668
|
+
depthCompare: "less-equal", // Draw where outline depth <= existing depth (hair depth)
|
|
669
|
+
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
670
|
+
depthBiasSlopeScale: 0.0,
|
|
671
|
+
depthBiasClamp: 0.0,
|
|
672
|
+
stencilFront: {
|
|
673
|
+
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
674
|
+
failOp: "keep",
|
|
675
|
+
depthFailOp: "keep",
|
|
676
|
+
passOp: "keep",
|
|
677
|
+
},
|
|
678
|
+
stencilBack: {
|
|
679
|
+
compare: "equal",
|
|
680
|
+
failOp: "keep",
|
|
681
|
+
depthFailOp: "keep",
|
|
682
|
+
passOp: "keep",
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
multisample: {
|
|
686
|
+
count: this.sampleCount,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
// Hair pipeline with multiplicative blending (for hair over eyes)
|
|
690
|
+
this.hairMultiplyPipeline = this.device.createRenderPipeline({
|
|
691
|
+
label: "hair multiply pipeline",
|
|
692
|
+
layout: sharedPipelineLayout,
|
|
693
|
+
vertex: {
|
|
694
|
+
module: hairMultiplyShaderModule,
|
|
695
|
+
buffers: [
|
|
696
|
+
{
|
|
697
|
+
arrayStride: 8 * 4,
|
|
698
|
+
attributes: [
|
|
699
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
700
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
701
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
702
|
+
],
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
arrayStride: 4 * 2,
|
|
706
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
arrayStride: 4,
|
|
710
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
},
|
|
714
|
+
fragment: {
|
|
715
|
+
module: hairMultiplyShaderModule,
|
|
716
|
+
targets: [
|
|
717
|
+
{
|
|
718
|
+
format: this.presentationFormat,
|
|
719
|
+
blend: {
|
|
720
|
+
color: {
|
|
721
|
+
// Simple half-transparent overlay effect
|
|
722
|
+
// Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
|
|
723
|
+
srcFactor: "src-alpha",
|
|
724
|
+
dstFactor: "one-minus-src-alpha",
|
|
725
|
+
operation: "add",
|
|
726
|
+
},
|
|
727
|
+
alpha: {
|
|
728
|
+
srcFactor: "one",
|
|
729
|
+
dstFactor: "one-minus-src-alpha",
|
|
730
|
+
operation: "add",
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
},
|
|
736
|
+
primitive: { cullMode: "none" },
|
|
737
|
+
depthStencil: {
|
|
738
|
+
format: "depth24plus-stencil8",
|
|
739
|
+
depthWriteEnabled: true, // Write depth so outlines can test against it
|
|
740
|
+
depthCompare: "less",
|
|
741
|
+
stencilFront: {
|
|
742
|
+
compare: "equal", // Only render where stencil == 1
|
|
743
|
+
failOp: "keep",
|
|
744
|
+
depthFailOp: "keep",
|
|
745
|
+
passOp: "keep",
|
|
746
|
+
},
|
|
747
|
+
stencilBack: {
|
|
748
|
+
compare: "equal",
|
|
749
|
+
failOp: "keep",
|
|
750
|
+
depthFailOp: "keep",
|
|
751
|
+
passOp: "keep",
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
multisample: { count: this.sampleCount },
|
|
755
|
+
});
|
|
756
|
+
// Hair pipeline for opaque rendering (hair over non-eyes)
|
|
757
|
+
this.hairOpaquePipeline = this.device.createRenderPipeline({
|
|
758
|
+
label: "hair opaque pipeline",
|
|
759
|
+
layout: sharedPipelineLayout,
|
|
760
|
+
vertex: {
|
|
761
|
+
module: shaderModule,
|
|
762
|
+
buffers: [
|
|
763
|
+
{
|
|
764
|
+
arrayStride: 8 * 4,
|
|
765
|
+
attributes: [
|
|
766
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
767
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
768
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
769
|
+
],
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
arrayStride: 4 * 2,
|
|
773
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
arrayStride: 4,
|
|
777
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
778
|
+
},
|
|
779
|
+
],
|
|
780
|
+
},
|
|
781
|
+
fragment: {
|
|
782
|
+
module: shaderModule,
|
|
783
|
+
targets: [
|
|
784
|
+
{
|
|
785
|
+
format: this.presentationFormat,
|
|
786
|
+
blend: {
|
|
787
|
+
color: {
|
|
788
|
+
srcFactor: "src-alpha",
|
|
789
|
+
dstFactor: "one-minus-src-alpha",
|
|
790
|
+
operation: "add",
|
|
791
|
+
},
|
|
792
|
+
alpha: {
|
|
793
|
+
srcFactor: "one",
|
|
794
|
+
dstFactor: "one-minus-src-alpha",
|
|
795
|
+
operation: "add",
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
},
|
|
801
|
+
primitive: { cullMode: "none" },
|
|
802
|
+
depthStencil: {
|
|
803
|
+
format: "depth24plus-stencil8",
|
|
804
|
+
depthWriteEnabled: true,
|
|
805
|
+
depthCompare: "less",
|
|
806
|
+
stencilFront: {
|
|
807
|
+
compare: "not-equal", // Only render where stencil != 1
|
|
808
|
+
failOp: "keep",
|
|
809
|
+
depthFailOp: "keep",
|
|
810
|
+
passOp: "keep",
|
|
811
|
+
},
|
|
812
|
+
stencilBack: {
|
|
813
|
+
compare: "not-equal",
|
|
814
|
+
failOp: "keep",
|
|
815
|
+
depthFailOp: "keep",
|
|
816
|
+
passOp: "keep",
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
multisample: { count: this.sampleCount },
|
|
820
|
+
});
|
|
821
|
+
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
822
|
+
this.eyePipeline = this.device.createRenderPipeline({
|
|
823
|
+
label: "eye overlay pipeline",
|
|
824
|
+
layout: sharedPipelineLayout,
|
|
825
|
+
vertex: {
|
|
826
|
+
module: shaderModule,
|
|
827
|
+
buffers: [
|
|
828
|
+
{
|
|
829
|
+
arrayStride: 8 * 4,
|
|
830
|
+
attributes: [
|
|
831
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
832
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
833
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
834
|
+
],
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
arrayStride: 4 * 2,
|
|
838
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
arrayStride: 4,
|
|
842
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
},
|
|
846
|
+
fragment: {
|
|
847
|
+
module: shaderModule,
|
|
848
|
+
targets: [
|
|
849
|
+
{
|
|
850
|
+
format: this.presentationFormat,
|
|
851
|
+
blend: {
|
|
852
|
+
color: {
|
|
853
|
+
srcFactor: "src-alpha",
|
|
854
|
+
dstFactor: "one-minus-src-alpha",
|
|
855
|
+
operation: "add",
|
|
856
|
+
},
|
|
857
|
+
alpha: {
|
|
858
|
+
srcFactor: "one",
|
|
859
|
+
dstFactor: "one-minus-src-alpha",
|
|
860
|
+
operation: "add",
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
primitive: { cullMode: "none" },
|
|
867
|
+
depthStencil: {
|
|
868
|
+
format: "depth24plus-stencil8",
|
|
869
|
+
depthWriteEnabled: false, // Don't write depth
|
|
870
|
+
depthCompare: "less", // Respect existing depth
|
|
871
|
+
stencilFront: {
|
|
872
|
+
compare: "always",
|
|
873
|
+
failOp: "keep",
|
|
874
|
+
depthFailOp: "keep",
|
|
875
|
+
passOp: "replace", // Write stencil value 1
|
|
876
|
+
},
|
|
877
|
+
stencilBack: {
|
|
878
|
+
compare: "always",
|
|
879
|
+
failOp: "keep",
|
|
880
|
+
depthFailOp: "keep",
|
|
881
|
+
passOp: "replace",
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
multisample: { count: this.sampleCount },
|
|
885
|
+
});
|
|
370
886
|
}
|
|
371
887
|
// Create compute shader for skin matrix computation
|
|
372
888
|
createSkinMatrixComputePipeline() {
|
|
373
889
|
const computeShader = this.device.createShaderModule({
|
|
374
890
|
label: "skin matrix compute",
|
|
375
|
-
code: /* wgsl */ `
|
|
376
|
-
struct BoneCountUniform {
|
|
377
|
-
count: u32,
|
|
378
|
-
_padding1: u32,
|
|
379
|
-
_padding2: u32,
|
|
380
|
-
_padding3: u32,
|
|
381
|
-
_padding4: vec4<u32>,
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
385
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
386
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
387
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
388
|
-
|
|
389
|
-
@compute @workgroup_size(64)
|
|
390
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
391
|
-
let boneIndex = globalId.x;
|
|
392
|
-
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
393
|
-
if (boneIndex >= boneCount.count) {
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
let worldMat = worldMatrices[boneIndex];
|
|
397
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
398
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
399
|
-
}
|
|
891
|
+
code: /* wgsl */ `
|
|
892
|
+
struct BoneCountUniform {
|
|
893
|
+
count: u32,
|
|
894
|
+
_padding1: u32,
|
|
895
|
+
_padding2: u32,
|
|
896
|
+
_padding3: u32,
|
|
897
|
+
_padding4: vec4<u32>,
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
901
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
902
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
903
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
904
|
+
|
|
905
|
+
@compute @workgroup_size(64)
|
|
906
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
907
|
+
let boneIndex = globalId.x;
|
|
908
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
909
|
+
if (boneIndex >= boneCount.count) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
let worldMat = worldMatrices[boneIndex];
|
|
913
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
914
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
915
|
+
}
|
|
400
916
|
`,
|
|
401
917
|
});
|
|
402
918
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -433,7 +949,7 @@ export class Engine {
|
|
|
433
949
|
label: "depth texture",
|
|
434
950
|
size: [width, height],
|
|
435
951
|
sampleCount: this.sampleCount,
|
|
436
|
-
format: "depth24plus",
|
|
952
|
+
format: "depth24plus-stencil8",
|
|
437
953
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
438
954
|
});
|
|
439
955
|
const depthTextureView = this.depthTexture.createView();
|
|
@@ -459,6 +975,9 @@ export class Engine {
|
|
|
459
975
|
depthClearValue: 1.0,
|
|
460
976
|
depthLoadOp: "clear",
|
|
461
977
|
depthStoreOp: "store",
|
|
978
|
+
stencilClearValue: 0, // New: clear stencil to 0
|
|
979
|
+
stencilLoadOp: "clear", // New: clear stencil each frame
|
|
980
|
+
stencilStoreOp: "store", // New: store stencil
|
|
462
981
|
},
|
|
463
982
|
};
|
|
464
983
|
this.camera.aspect = width / height;
|
|
@@ -667,9 +1186,14 @@ export class Engine {
|
|
|
667
1186
|
this.textureSizes.set(defaultToonPath, { width: 256, height: 2 });
|
|
668
1187
|
return defaultToonTexture;
|
|
669
1188
|
};
|
|
670
|
-
this.
|
|
671
|
-
this.
|
|
672
|
-
|
|
1189
|
+
this.opaqueNonEyeNonHairDraws = [];
|
|
1190
|
+
this.eyeDraws = [];
|
|
1191
|
+
this.hairDraws = [];
|
|
1192
|
+
this.transparentNonEyeNonHairDraws = [];
|
|
1193
|
+
this.opaqueNonEyeNonHairOutlineDraws = [];
|
|
1194
|
+
this.eyeOutlineDraws = [];
|
|
1195
|
+
this.hairOutlineDraws = [];
|
|
1196
|
+
this.transparentNonEyeNonHairOutlineDraws = [];
|
|
673
1197
|
let runningFirstIndex = 0;
|
|
674
1198
|
for (const mat of materials) {
|
|
675
1199
|
const matCount = mat.vertexCount | 0;
|
|
@@ -693,9 +1217,11 @@ export class Engine {
|
|
|
693
1217
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
694
1218
|
});
|
|
695
1219
|
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData);
|
|
1220
|
+
// Create bind groups using the shared bind group layout
|
|
1221
|
+
// All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
696
1222
|
const bindGroup = this.device.createBindGroup({
|
|
697
1223
|
label: `material bind group: ${mat.name}`,
|
|
698
|
-
layout: this.
|
|
1224
|
+
layout: this.hairBindGroupLayout,
|
|
699
1225
|
entries: [
|
|
700
1226
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
701
1227
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
@@ -707,13 +1233,39 @@ export class Engine {
|
|
|
707
1233
|
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
708
1234
|
],
|
|
709
1235
|
});
|
|
710
|
-
//
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1236
|
+
// Classify materials into appropriate draw lists
|
|
1237
|
+
if (mat.isEye) {
|
|
1238
|
+
this.eyeDraws.push({
|
|
1239
|
+
count: matCount,
|
|
1240
|
+
firstIndex: runningFirstIndex,
|
|
1241
|
+
bindGroup,
|
|
1242
|
+
isTransparent,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
else if (mat.isHair) {
|
|
1246
|
+
this.hairDraws.push({
|
|
1247
|
+
count: matCount,
|
|
1248
|
+
firstIndex: runningFirstIndex,
|
|
1249
|
+
bindGroup,
|
|
1250
|
+
isTransparent,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
else if (isTransparent) {
|
|
1254
|
+
this.transparentNonEyeNonHairDraws.push({
|
|
1255
|
+
count: matCount,
|
|
1256
|
+
firstIndex: runningFirstIndex,
|
|
1257
|
+
bindGroup,
|
|
1258
|
+
isTransparent,
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
this.opaqueNonEyeNonHairDraws.push({
|
|
1263
|
+
count: matCount,
|
|
1264
|
+
firstIndex: runningFirstIndex,
|
|
1265
|
+
bindGroup,
|
|
1266
|
+
isTransparent,
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
717
1269
|
// Outline for all materials (including transparent)
|
|
718
1270
|
// Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
719
1271
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
@@ -731,20 +1283,46 @@ export class Engine {
|
|
|
731
1283
|
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData);
|
|
732
1284
|
const outlineBindGroup = this.device.createBindGroup({
|
|
733
1285
|
label: `outline bind group: ${mat.name}`,
|
|
734
|
-
layout: outlineBindGroupLayout,
|
|
1286
|
+
layout: this.outlineBindGroupLayout,
|
|
735
1287
|
entries: [
|
|
736
1288
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
737
1289
|
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
738
1290
|
{ binding: 2, resource: { buffer: this.skinMatrixBuffer } },
|
|
739
1291
|
],
|
|
740
1292
|
});
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1293
|
+
// Classify outlines into appropriate draw lists
|
|
1294
|
+
if (mat.isEye) {
|
|
1295
|
+
this.eyeOutlineDraws.push({
|
|
1296
|
+
count: matCount,
|
|
1297
|
+
firstIndex: runningFirstIndex,
|
|
1298
|
+
bindGroup: outlineBindGroup,
|
|
1299
|
+
isTransparent,
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
else if (mat.isHair) {
|
|
1303
|
+
this.hairOutlineDraws.push({
|
|
1304
|
+
count: matCount,
|
|
1305
|
+
firstIndex: runningFirstIndex,
|
|
1306
|
+
bindGroup: outlineBindGroup,
|
|
1307
|
+
isTransparent,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
else if (isTransparent) {
|
|
1311
|
+
this.transparentNonEyeNonHairOutlineDraws.push({
|
|
1312
|
+
count: matCount,
|
|
1313
|
+
firstIndex: runningFirstIndex,
|
|
1314
|
+
bindGroup: outlineBindGroup,
|
|
1315
|
+
isTransparent,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
this.opaqueNonEyeNonHairOutlineDraws.push({
|
|
1320
|
+
count: matCount,
|
|
1321
|
+
firstIndex: runningFirstIndex,
|
|
1322
|
+
bindGroup: outlineBindGroup,
|
|
1323
|
+
isTransparent,
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
748
1326
|
}
|
|
749
1327
|
runningFirstIndex += matCount;
|
|
750
1328
|
}
|
|
@@ -810,10 +1388,78 @@ export class Engine {
|
|
|
810
1388
|
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
811
1389
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
812
1390
|
this.drawCallCount = 0;
|
|
813
|
-
|
|
814
|
-
this.
|
|
815
|
-
this.
|
|
816
|
-
this.
|
|
1391
|
+
// === PASS 1: Opaque non-eye, non-hair (face, body, etc) ===
|
|
1392
|
+
this.drawOutlines(pass, false); // Opaque outlines
|
|
1393
|
+
pass.setPipeline(this.pipeline);
|
|
1394
|
+
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
1395
|
+
if (draw.count > 0) {
|
|
1396
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1397
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1398
|
+
this.drawCallCount++;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
// === PASS 2: Eyes (writes stencil = 1) ===
|
|
1402
|
+
pass.setPipeline(this.eyePipeline);
|
|
1403
|
+
pass.setStencilReference(1); // Set stencil reference value to 1
|
|
1404
|
+
for (const draw of this.eyeDraws) {
|
|
1405
|
+
if (draw.count > 0) {
|
|
1406
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1407
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1408
|
+
this.drawCallCount++;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
// === PASS 3a: Hair over eyes (stencil == 1, multiply blend) ===
|
|
1412
|
+
// Draw hair geometry first to establish depth
|
|
1413
|
+
pass.setPipeline(this.hairMultiplyPipeline);
|
|
1414
|
+
pass.setStencilReference(1); // Check against stencil value 1
|
|
1415
|
+
for (const draw of this.hairDraws) {
|
|
1416
|
+
if (draw.count > 0) {
|
|
1417
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1418
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1419
|
+
this.drawCallCount++;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
// === PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair) ===
|
|
1423
|
+
// Use depth compare "less-equal" with the hair depth to only draw outline where hair exists
|
|
1424
|
+
// The outline is expanded outward, so we need to ensure it only appears near the hair edge
|
|
1425
|
+
pass.setPipeline(this.hairOutlineOverEyesPipeline);
|
|
1426
|
+
pass.setStencilReference(1); // Check against stencil value 1 (with equal test)
|
|
1427
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1428
|
+
if (draw.count > 0) {
|
|
1429
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1430
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// === PASS 3b: Hair over non-eyes (stencil != 1, opaque) ===
|
|
1434
|
+
pass.setPipeline(this.hairOpaquePipeline);
|
|
1435
|
+
pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
|
|
1436
|
+
for (const draw of this.hairDraws) {
|
|
1437
|
+
if (draw.count > 0) {
|
|
1438
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1439
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1440
|
+
this.drawCallCount++;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
// === PASS 3b.5: Hair outlines over non-eyes (stencil != 1) ===
|
|
1444
|
+
// Draw hair outlines after hair geometry, so they only appear where hair exists
|
|
1445
|
+
pass.setPipeline(this.hairOutlinePipeline);
|
|
1446
|
+
pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
|
|
1447
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1448
|
+
if (draw.count > 0) {
|
|
1449
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1450
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
// === PASS 4: Transparent non-eye, non-hair ===
|
|
1454
|
+
pass.setPipeline(this.pipeline);
|
|
1455
|
+
for (const draw of this.transparentNonEyeNonHairDraws) {
|
|
1456
|
+
if (draw.count > 0) {
|
|
1457
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1458
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1459
|
+
this.drawCallCount++;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
this.drawOutlines(pass, true); // Transparent outlines
|
|
817
1463
|
pass.end();
|
|
818
1464
|
this.device.queue.submit([encoder.finish()]);
|
|
819
1465
|
this.updateStats(performance.now() - currentTime);
|
|
@@ -885,24 +1531,23 @@ export class Engine {
|
|
|
885
1531
|
}
|
|
886
1532
|
// Draw outlines (opaque or transparent)
|
|
887
1533
|
drawOutlines(pass, transparent) {
|
|
888
|
-
if (this.outlineDraws.length === 0)
|
|
889
|
-
return;
|
|
890
1534
|
pass.setPipeline(this.outlinePipeline);
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1535
|
+
if (transparent) {
|
|
1536
|
+
// Draw transparent outlines (if any)
|
|
1537
|
+
for (const draw of this.transparentNonEyeNonHairOutlineDraws) {
|
|
1538
|
+
if (draw.count > 0) {
|
|
1539
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1540
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1541
|
+
}
|
|
895
1542
|
}
|
|
896
1543
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
905
|
-
this.drawCallCount++;
|
|
1544
|
+
else {
|
|
1545
|
+
// Draw opaque outlines before main geometry
|
|
1546
|
+
for (const draw of this.opaqueNonEyeNonHairOutlineDraws) {
|
|
1547
|
+
if (draw.count > 0) {
|
|
1548
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1549
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1550
|
+
}
|
|
906
1551
|
}
|
|
907
1552
|
}
|
|
908
1553
|
}
|
|
@@ -959,7 +1604,11 @@ export class Engine {
|
|
|
959
1604
|
}
|
|
960
1605
|
bufferMemoryBytes += 40 * 4; // cameraUniformBuffer
|
|
961
1606
|
bufferMemoryBytes += 64 * 4; // lightUniformBuffer
|
|
962
|
-
|
|
1607
|
+
const totalMaterialDraws = this.opaqueNonEyeNonHairDraws.length +
|
|
1608
|
+
this.eyeDraws.length +
|
|
1609
|
+
this.hairDraws.length +
|
|
1610
|
+
this.transparentNonEyeNonHairDraws.length;
|
|
1611
|
+
bufferMemoryBytes += totalMaterialDraws * 4; // Material uniform buffers
|
|
963
1612
|
let renderTargetMemoryBytes = 0;
|
|
964
1613
|
if (this.multisampleTexture) {
|
|
965
1614
|
const width = this.canvas.width;
|