reze-engine 0.2.11 → 0.2.13
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 +71 -71
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +436 -434
- package/dist/pool.d.ts +38 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +422 -0
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +2404 -2402
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1054 -1054
- package/src/vmd-loader.ts +179 -179
package/dist/engine.js
CHANGED
|
@@ -99,121 +99,121 @@ export class Engine {
|
|
|
99
99
|
});
|
|
100
100
|
const shaderModule = this.device.createShaderModule({
|
|
101
101
|
label: "model shaders",
|
|
102
|
-
code: /* wgsl */ `
|
|
103
|
-
struct CameraUniforms {
|
|
104
|
-
view: mat4x4f,
|
|
105
|
-
projection: mat4x4f,
|
|
106
|
-
viewPos: vec3f,
|
|
107
|
-
_padding: f32,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
struct Light {
|
|
111
|
-
direction: vec3f,
|
|
112
|
-
_padding1: f32,
|
|
113
|
-
color: vec3f,
|
|
114
|
-
intensity: f32,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
struct LightUniforms {
|
|
118
|
-
ambient: f32,
|
|
119
|
-
lightCount: f32,
|
|
120
|
-
_padding1: f32,
|
|
121
|
-
_padding2: f32,
|
|
122
|
-
lights: array<Light, 4>,
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
struct MaterialUniforms {
|
|
126
|
-
alpha: f32,
|
|
127
|
-
alphaMultiplier: f32,
|
|
128
|
-
rimIntensity: f32,
|
|
129
|
-
_padding1: f32,
|
|
130
|
-
rimColor: vec3f,
|
|
131
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
struct VertexOutput {
|
|
135
|
-
@builtin(position) position: vec4f,
|
|
136
|
-
@location(0) normal: vec3f,
|
|
137
|
-
@location(1) uv: vec2f,
|
|
138
|
-
@location(2) worldPos: vec3f,
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
142
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
143
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
144
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
145
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
146
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
147
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
148
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
149
|
-
|
|
150
|
-
@vertex fn vs(
|
|
151
|
-
@location(0) position: vec3f,
|
|
152
|
-
@location(1) normal: vec3f,
|
|
153
|
-
@location(2) uv: vec2f,
|
|
154
|
-
@location(3) joints0: vec4<u32>,
|
|
155
|
-
@location(4) weights0: vec4<f32>
|
|
156
|
-
) -> VertexOutput {
|
|
157
|
-
var output: VertexOutput;
|
|
158
|
-
let pos4 = vec4f(position, 1.0);
|
|
159
|
-
|
|
160
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
161
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
162
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
163
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
164
|
-
|
|
165
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
166
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
167
|
-
for (var i = 0u; i < 4u; i++) {
|
|
168
|
-
let j = joints0[i];
|
|
169
|
-
let w = normalizedWeights[i];
|
|
170
|
-
let m = skinMats[j];
|
|
171
|
-
skinnedPos += (m * pos4) * w;
|
|
172
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
173
|
-
skinnedNrm += (r3 * normal) * w;
|
|
174
|
-
}
|
|
175
|
-
let worldPos = skinnedPos.xyz;
|
|
176
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
177
|
-
output.normal = normalize(skinnedNrm);
|
|
178
|
-
output.uv = uv;
|
|
179
|
-
output.worldPos = worldPos;
|
|
180
|
-
return output;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
184
|
-
// Early alpha test - discard before expensive calculations
|
|
185
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
186
|
-
if (material.isOverEyes > 0.5) {
|
|
187
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
188
|
-
}
|
|
189
|
-
if (finalAlpha < 0.001) {
|
|
190
|
-
discard;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let n = normalize(input.normal);
|
|
194
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
195
|
-
|
|
196
|
-
var lightAccum = vec3f(light.ambient);
|
|
197
|
-
let numLights = u32(light.lightCount);
|
|
198
|
-
for (var i = 0u; i < numLights; i++) {
|
|
199
|
-
let l = -light.lights[i].direction;
|
|
200
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
201
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
202
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
203
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
204
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Rim light calculation
|
|
208
|
-
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
209
|
-
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
210
|
-
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
211
|
-
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
212
|
-
|
|
213
|
-
let color = albedo * lightAccum + rimLight;
|
|
214
|
-
|
|
215
|
-
return vec4f(color, finalAlpha);
|
|
216
|
-
}
|
|
102
|
+
code: /* wgsl */ `
|
|
103
|
+
struct CameraUniforms {
|
|
104
|
+
view: mat4x4f,
|
|
105
|
+
projection: mat4x4f,
|
|
106
|
+
viewPos: vec3f,
|
|
107
|
+
_padding: f32,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
struct Light {
|
|
111
|
+
direction: vec3f,
|
|
112
|
+
_padding1: f32,
|
|
113
|
+
color: vec3f,
|
|
114
|
+
intensity: f32,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
struct LightUniforms {
|
|
118
|
+
ambient: f32,
|
|
119
|
+
lightCount: f32,
|
|
120
|
+
_padding1: f32,
|
|
121
|
+
_padding2: f32,
|
|
122
|
+
lights: array<Light, 4>,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
struct MaterialUniforms {
|
|
126
|
+
alpha: f32,
|
|
127
|
+
alphaMultiplier: f32,
|
|
128
|
+
rimIntensity: f32,
|
|
129
|
+
_padding1: f32,
|
|
130
|
+
rimColor: vec3f,
|
|
131
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
struct VertexOutput {
|
|
135
|
+
@builtin(position) position: vec4f,
|
|
136
|
+
@location(0) normal: vec3f,
|
|
137
|
+
@location(1) uv: vec2f,
|
|
138
|
+
@location(2) worldPos: vec3f,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
142
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
143
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
144
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
145
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
146
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
147
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
148
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
149
|
+
|
|
150
|
+
@vertex fn vs(
|
|
151
|
+
@location(0) position: vec3f,
|
|
152
|
+
@location(1) normal: vec3f,
|
|
153
|
+
@location(2) uv: vec2f,
|
|
154
|
+
@location(3) joints0: vec4<u32>,
|
|
155
|
+
@location(4) weights0: vec4<f32>
|
|
156
|
+
) -> VertexOutput {
|
|
157
|
+
var output: VertexOutput;
|
|
158
|
+
let pos4 = vec4f(position, 1.0);
|
|
159
|
+
|
|
160
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
161
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
162
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
163
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
164
|
+
|
|
165
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
166
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
167
|
+
for (var i = 0u; i < 4u; i++) {
|
|
168
|
+
let j = joints0[i];
|
|
169
|
+
let w = normalizedWeights[i];
|
|
170
|
+
let m = skinMats[j];
|
|
171
|
+
skinnedPos += (m * pos4) * w;
|
|
172
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
173
|
+
skinnedNrm += (r3 * normal) * w;
|
|
174
|
+
}
|
|
175
|
+
let worldPos = skinnedPos.xyz;
|
|
176
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
177
|
+
output.normal = normalize(skinnedNrm);
|
|
178
|
+
output.uv = uv;
|
|
179
|
+
output.worldPos = worldPos;
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
184
|
+
// Early alpha test - discard before expensive calculations
|
|
185
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
186
|
+
if (material.isOverEyes > 0.5) {
|
|
187
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
188
|
+
}
|
|
189
|
+
if (finalAlpha < 0.001) {
|
|
190
|
+
discard;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let n = normalize(input.normal);
|
|
194
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
195
|
+
|
|
196
|
+
var lightAccum = vec3f(light.ambient);
|
|
197
|
+
let numLights = u32(light.lightCount);
|
|
198
|
+
for (var i = 0u; i < numLights; i++) {
|
|
199
|
+
let l = -light.lights[i].direction;
|
|
200
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
201
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
202
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
203
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
204
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Rim light calculation
|
|
208
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
209
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
210
|
+
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
211
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
212
|
+
|
|
213
|
+
let color = albedo * lightAccum + rimLight;
|
|
214
|
+
|
|
215
|
+
return vec4f(color, finalAlpha);
|
|
216
|
+
}
|
|
217
217
|
`,
|
|
218
218
|
});
|
|
219
219
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -303,73 +303,73 @@ export class Engine {
|
|
|
303
303
|
});
|
|
304
304
|
const outlineShaderModule = this.device.createShaderModule({
|
|
305
305
|
label: "outline shaders",
|
|
306
|
-
code: /* wgsl */ `
|
|
307
|
-
struct CameraUniforms {
|
|
308
|
-
view: mat4x4f,
|
|
309
|
-
projection: mat4x4f,
|
|
310
|
-
viewPos: vec3f,
|
|
311
|
-
_padding: f32,
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
struct MaterialUniforms {
|
|
315
|
-
edgeColor: vec4f,
|
|
316
|
-
edgeSize: f32,
|
|
317
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
318
|
-
_padding1: f32,
|
|
319
|
-
_padding2: f32,
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
323
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
324
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
325
|
-
|
|
326
|
-
struct VertexOutput {
|
|
327
|
-
@builtin(position) position: vec4f,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
@vertex fn vs(
|
|
331
|
-
@location(0) position: vec3f,
|
|
332
|
-
@location(1) normal: vec3f,
|
|
333
|
-
@location(3) joints0: vec4<u32>,
|
|
334
|
-
@location(4) weights0: vec4<f32>
|
|
335
|
-
) -> VertexOutput {
|
|
336
|
-
var output: VertexOutput;
|
|
337
|
-
let pos4 = vec4f(position, 1.0);
|
|
338
|
-
|
|
339
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
340
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
341
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
342
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
343
|
-
|
|
344
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
345
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
346
|
-
for (var i = 0u; i < 4u; i++) {
|
|
347
|
-
let j = joints0[i];
|
|
348
|
-
let w = normalizedWeights[i];
|
|
349
|
-
let m = skinMats[j];
|
|
350
|
-
skinnedPos += (m * pos4) * w;
|
|
351
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
352
|
-
skinnedNrm += (r3 * normal) * w;
|
|
353
|
-
}
|
|
354
|
-
let worldPos = skinnedPos.xyz;
|
|
355
|
-
let worldNormal = normalize(skinnedNrm);
|
|
356
|
-
|
|
357
|
-
// MMD invert hull: expand vertices outward along normals
|
|
358
|
-
let scaleFactor = 0.01;
|
|
359
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
360
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
361
|
-
return output;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
365
|
-
var color = material.edgeColor;
|
|
366
|
-
|
|
367
|
-
if (material.isOverEyes > 0.5) {
|
|
368
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return color;
|
|
372
|
-
}
|
|
306
|
+
code: /* wgsl */ `
|
|
307
|
+
struct CameraUniforms {
|
|
308
|
+
view: mat4x4f,
|
|
309
|
+
projection: mat4x4f,
|
|
310
|
+
viewPos: vec3f,
|
|
311
|
+
_padding: f32,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
struct MaterialUniforms {
|
|
315
|
+
edgeColor: vec4f,
|
|
316
|
+
edgeSize: f32,
|
|
317
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
318
|
+
_padding1: f32,
|
|
319
|
+
_padding2: f32,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
323
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
324
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
325
|
+
|
|
326
|
+
struct VertexOutput {
|
|
327
|
+
@builtin(position) position: vec4f,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
@vertex fn vs(
|
|
331
|
+
@location(0) position: vec3f,
|
|
332
|
+
@location(1) normal: vec3f,
|
|
333
|
+
@location(3) joints0: vec4<u32>,
|
|
334
|
+
@location(4) weights0: vec4<f32>
|
|
335
|
+
) -> VertexOutput {
|
|
336
|
+
var output: VertexOutput;
|
|
337
|
+
let pos4 = vec4f(position, 1.0);
|
|
338
|
+
|
|
339
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
340
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
341
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
342
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
343
|
+
|
|
344
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
345
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
346
|
+
for (var i = 0u; i < 4u; i++) {
|
|
347
|
+
let j = joints0[i];
|
|
348
|
+
let w = normalizedWeights[i];
|
|
349
|
+
let m = skinMats[j];
|
|
350
|
+
skinnedPos += (m * pos4) * w;
|
|
351
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
352
|
+
skinnedNrm += (r3 * normal) * w;
|
|
353
|
+
}
|
|
354
|
+
let worldPos = skinnedPos.xyz;
|
|
355
|
+
let worldNormal = normalize(skinnedNrm);
|
|
356
|
+
|
|
357
|
+
// MMD invert hull: expand vertices outward along normals
|
|
358
|
+
let scaleFactor = 0.01;
|
|
359
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
360
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
361
|
+
return output;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
365
|
+
var color = material.edgeColor;
|
|
366
|
+
|
|
367
|
+
if (material.isOverEyes > 0.5) {
|
|
368
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return color;
|
|
372
|
+
}
|
|
373
373
|
`,
|
|
374
374
|
});
|
|
375
375
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -573,45 +573,45 @@ export class Engine {
|
|
|
573
573
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
574
574
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
575
575
|
label: "depth only shader",
|
|
576
|
-
code: /* wgsl */ `
|
|
577
|
-
struct CameraUniforms {
|
|
578
|
-
view: mat4x4f,
|
|
579
|
-
projection: mat4x4f,
|
|
580
|
-
viewPos: vec3f,
|
|
581
|
-
_padding: f32,
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
585
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
586
|
-
|
|
587
|
-
@vertex fn vs(
|
|
588
|
-
@location(0) position: vec3f,
|
|
589
|
-
@location(1) normal: vec3f,
|
|
590
|
-
@location(3) joints0: vec4<u32>,
|
|
591
|
-
@location(4) weights0: vec4<f32>
|
|
592
|
-
) -> @builtin(position) vec4f {
|
|
593
|
-
let pos4 = vec4f(position, 1.0);
|
|
594
|
-
|
|
595
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
596
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
597
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
598
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
599
|
-
|
|
600
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
601
|
-
for (var i = 0u; i < 4u; i++) {
|
|
602
|
-
let j = joints0[i];
|
|
603
|
-
let w = normalizedWeights[i];
|
|
604
|
-
let m = skinMats[j];
|
|
605
|
-
skinnedPos += (m * pos4) * w;
|
|
606
|
-
}
|
|
607
|
-
let worldPos = skinnedPos.xyz;
|
|
608
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
609
|
-
return clipPos;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
613
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
614
|
-
}
|
|
576
|
+
code: /* wgsl */ `
|
|
577
|
+
struct CameraUniforms {
|
|
578
|
+
view: mat4x4f,
|
|
579
|
+
projection: mat4x4f,
|
|
580
|
+
viewPos: vec3f,
|
|
581
|
+
_padding: f32,
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
585
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
586
|
+
|
|
587
|
+
@vertex fn vs(
|
|
588
|
+
@location(0) position: vec3f,
|
|
589
|
+
@location(1) normal: vec3f,
|
|
590
|
+
@location(3) joints0: vec4<u32>,
|
|
591
|
+
@location(4) weights0: vec4<f32>
|
|
592
|
+
) -> @builtin(position) vec4f {
|
|
593
|
+
let pos4 = vec4f(position, 1.0);
|
|
594
|
+
|
|
595
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
596
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
597
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
598
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
599
|
+
|
|
600
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
601
|
+
for (var i = 0u; i < 4u; i++) {
|
|
602
|
+
let j = joints0[i];
|
|
603
|
+
let w = normalizedWeights[i];
|
|
604
|
+
let m = skinMats[j];
|
|
605
|
+
skinnedPos += (m * pos4) * w;
|
|
606
|
+
}
|
|
607
|
+
let worldPos = skinnedPos.xyz;
|
|
608
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
609
|
+
return clipPos;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
613
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
614
|
+
}
|
|
615
615
|
`,
|
|
616
616
|
});
|
|
617
617
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
@@ -794,30 +794,30 @@ export class Engine {
|
|
|
794
794
|
createSkinMatrixComputePipeline() {
|
|
795
795
|
const computeShader = this.device.createShaderModule({
|
|
796
796
|
label: "skin matrix compute",
|
|
797
|
-
code: /* wgsl */ `
|
|
798
|
-
struct BoneCountUniform {
|
|
799
|
-
count: u32,
|
|
800
|
-
_padding1: u32,
|
|
801
|
-
_padding2: u32,
|
|
802
|
-
_padding3: u32,
|
|
803
|
-
_padding4: vec4<u32>,
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
807
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
808
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
809
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
810
|
-
|
|
811
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
812
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
813
|
-
let boneIndex = globalId.x;
|
|
814
|
-
if (boneIndex >= boneCount.count) {
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
let worldMat = worldMatrices[boneIndex];
|
|
818
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
819
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
820
|
-
}
|
|
797
|
+
code: /* wgsl */ `
|
|
798
|
+
struct BoneCountUniform {
|
|
799
|
+
count: u32,
|
|
800
|
+
_padding1: u32,
|
|
801
|
+
_padding2: u32,
|
|
802
|
+
_padding3: u32,
|
|
803
|
+
_padding4: vec4<u32>,
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
807
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
808
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
809
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
810
|
+
|
|
811
|
+
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
812
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
813
|
+
let boneIndex = globalId.x;
|
|
814
|
+
if (boneIndex >= boneCount.count) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
let worldMat = worldMatrices[boneIndex];
|
|
818
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
819
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
820
|
+
}
|
|
821
821
|
`,
|
|
822
822
|
});
|
|
823
823
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -871,140 +871,140 @@ export class Engine {
|
|
|
871
871
|
// Bloom extraction shader (extracts bright areas)
|
|
872
872
|
const bloomExtractShader = this.device.createShaderModule({
|
|
873
873
|
label: "bloom extract",
|
|
874
|
-
code: /* wgsl */ `
|
|
875
|
-
struct VertexOutput {
|
|
876
|
-
@builtin(position) position: vec4f,
|
|
877
|
-
@location(0) uv: vec2f,
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
881
|
-
var output: VertexOutput;
|
|
882
|
-
// Generate fullscreen quad from vertex index
|
|
883
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
884
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
885
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
886
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
887
|
-
return output;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
struct BloomExtractUniforms {
|
|
891
|
-
threshold: f32,
|
|
892
|
-
_padding1: f32,
|
|
893
|
-
_padding2: f32,
|
|
894
|
-
_padding3: f32,
|
|
895
|
-
_padding4: f32,
|
|
896
|
-
_padding5: f32,
|
|
897
|
-
_padding6: f32,
|
|
898
|
-
_padding7: f32,
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
902
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
903
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
904
|
-
|
|
905
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
906
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
907
|
-
// Extract bright areas above threshold
|
|
908
|
-
let threshold = extractUniforms.threshold;
|
|
909
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
910
|
-
return vec4f(bloom, color.a);
|
|
911
|
-
}
|
|
874
|
+
code: /* wgsl */ `
|
|
875
|
+
struct VertexOutput {
|
|
876
|
+
@builtin(position) position: vec4f,
|
|
877
|
+
@location(0) uv: vec2f,
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
881
|
+
var output: VertexOutput;
|
|
882
|
+
// Generate fullscreen quad from vertex index
|
|
883
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
884
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
885
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
886
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
887
|
+
return output;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
struct BloomExtractUniforms {
|
|
891
|
+
threshold: f32,
|
|
892
|
+
_padding1: f32,
|
|
893
|
+
_padding2: f32,
|
|
894
|
+
_padding3: f32,
|
|
895
|
+
_padding4: f32,
|
|
896
|
+
_padding5: f32,
|
|
897
|
+
_padding6: f32,
|
|
898
|
+
_padding7: f32,
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
902
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
903
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
904
|
+
|
|
905
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
906
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
907
|
+
// Extract bright areas above threshold
|
|
908
|
+
let threshold = extractUniforms.threshold;
|
|
909
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
910
|
+
return vec4f(bloom, color.a);
|
|
911
|
+
}
|
|
912
912
|
`,
|
|
913
913
|
});
|
|
914
914
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
915
915
|
const bloomBlurShader = this.device.createShaderModule({
|
|
916
916
|
label: "bloom blur",
|
|
917
|
-
code: /* wgsl */ `
|
|
918
|
-
struct VertexOutput {
|
|
919
|
-
@builtin(position) position: vec4f,
|
|
920
|
-
@location(0) uv: vec2f,
|
|
921
|
-
};
|
|
922
|
-
|
|
923
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
924
|
-
var output: VertexOutput;
|
|
925
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
926
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
927
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
928
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
929
|
-
return output;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
struct BlurUniforms {
|
|
933
|
-
direction: vec2f,
|
|
934
|
-
_padding1: f32,
|
|
935
|
-
_padding2: f32,
|
|
936
|
-
_padding3: f32,
|
|
937
|
-
_padding4: f32,
|
|
938
|
-
_padding5: f32,
|
|
939
|
-
_padding6: f32,
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
943
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
944
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
945
|
-
|
|
946
|
-
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
947
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
948
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
949
|
-
|
|
950
|
-
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
951
|
-
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
952
|
-
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
953
|
-
let weight0 = 0.38774; // Center sample
|
|
954
|
-
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
955
|
-
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
956
|
-
|
|
957
|
-
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
958
|
-
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
959
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
960
|
-
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
961
|
-
|
|
962
|
-
return result;
|
|
963
|
-
}
|
|
917
|
+
code: /* wgsl */ `
|
|
918
|
+
struct VertexOutput {
|
|
919
|
+
@builtin(position) position: vec4f,
|
|
920
|
+
@location(0) uv: vec2f,
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
924
|
+
var output: VertexOutput;
|
|
925
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
926
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
927
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
928
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
929
|
+
return output;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
struct BlurUniforms {
|
|
933
|
+
direction: vec2f,
|
|
934
|
+
_padding1: f32,
|
|
935
|
+
_padding2: f32,
|
|
936
|
+
_padding3: f32,
|
|
937
|
+
_padding4: f32,
|
|
938
|
+
_padding5: f32,
|
|
939
|
+
_padding6: f32,
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
943
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
944
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
945
|
+
|
|
946
|
+
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
947
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
948
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
949
|
+
|
|
950
|
+
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
951
|
+
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
952
|
+
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
953
|
+
let weight0 = 0.38774; // Center sample
|
|
954
|
+
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
955
|
+
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
956
|
+
|
|
957
|
+
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
958
|
+
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
959
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
960
|
+
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
961
|
+
|
|
962
|
+
return result;
|
|
963
|
+
}
|
|
964
964
|
`,
|
|
965
965
|
});
|
|
966
966
|
// Bloom composition shader (combines original scene with bloom)
|
|
967
967
|
const bloomComposeShader = this.device.createShaderModule({
|
|
968
968
|
label: "bloom compose",
|
|
969
|
-
code: /* wgsl */ `
|
|
970
|
-
struct VertexOutput {
|
|
971
|
-
@builtin(position) position: vec4f,
|
|
972
|
-
@location(0) uv: vec2f,
|
|
973
|
-
};
|
|
974
|
-
|
|
975
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
976
|
-
var output: VertexOutput;
|
|
977
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
978
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
979
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
980
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
981
|
-
return output;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
struct BloomComposeUniforms {
|
|
985
|
-
intensity: f32,
|
|
986
|
-
_padding1: f32,
|
|
987
|
-
_padding2: f32,
|
|
988
|
-
_padding3: f32,
|
|
989
|
-
_padding4: f32,
|
|
990
|
-
_padding5: f32,
|
|
991
|
-
_padding6: f32,
|
|
992
|
-
_padding7: f32,
|
|
993
|
-
};
|
|
994
|
-
|
|
995
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
996
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
997
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
998
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
999
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1000
|
-
|
|
1001
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1002
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1003
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1004
|
-
// Additive blending with intensity control
|
|
1005
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1006
|
-
return vec4f(result, scene.a);
|
|
1007
|
-
}
|
|
969
|
+
code: /* wgsl */ `
|
|
970
|
+
struct VertexOutput {
|
|
971
|
+
@builtin(position) position: vec4f,
|
|
972
|
+
@location(0) uv: vec2f,
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
976
|
+
var output: VertexOutput;
|
|
977
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
978
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
979
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
980
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
981
|
+
return output;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
struct BloomComposeUniforms {
|
|
985
|
+
intensity: f32,
|
|
986
|
+
_padding1: f32,
|
|
987
|
+
_padding2: f32,
|
|
988
|
+
_padding3: f32,
|
|
989
|
+
_padding4: f32,
|
|
990
|
+
_padding5: f32,
|
|
991
|
+
_padding6: f32,
|
|
992
|
+
_padding7: f32,
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
996
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
997
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
998
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
999
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1000
|
+
|
|
1001
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1002
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1003
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1004
|
+
// Additive blending with intensity control
|
|
1005
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1006
|
+
return vec4f(result, scene.a);
|
|
1007
|
+
}
|
|
1008
1008
|
`,
|
|
1009
1009
|
});
|
|
1010
1010
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1762,7 +1762,7 @@ export class Engine {
|
|
|
1762
1762
|
}
|
|
1763
1763
|
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
|
|
1764
1764
|
render() {
|
|
1765
|
-
if (this.multisampleTexture && this.camera && this.device
|
|
1765
|
+
if (this.multisampleTexture && this.camera && this.device) {
|
|
1766
1766
|
const currentTime = performance.now();
|
|
1767
1767
|
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016;
|
|
1768
1768
|
this.lastFrameTime = currentTime;
|
|
@@ -1779,91 +1779,93 @@ export class Engine {
|
|
|
1779
1779
|
return;
|
|
1780
1780
|
}
|
|
1781
1781
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1782
|
-
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1783
|
-
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
1784
|
-
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1785
|
-
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1786
1782
|
this.drawCallCount = 0;
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1796
|
-
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1797
|
-
pass.setPipeline(this.eyePipeline);
|
|
1798
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1799
|
-
for (const draw of this.eyeDraws) {
|
|
1800
|
-
if (draw.count > 0) {
|
|
1801
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1802
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1803
|
-
this.drawCallCount++;
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1807
|
-
this.drawOutlines(pass, false);
|
|
1808
|
-
// 3a: Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1809
|
-
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
1810
|
-
pass.setPipeline(this.hairDepthPipeline);
|
|
1811
|
-
for (const draw of this.hairDrawsOverEyes) {
|
|
1812
|
-
if (draw.count > 0) {
|
|
1813
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1814
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1783
|
+
if (this.currentModel) {
|
|
1784
|
+
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1785
|
+
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
1786
|
+
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1787
|
+
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1788
|
+
// Pass 1: Opaque
|
|
1789
|
+
pass.setPipeline(this.modelPipeline);
|
|
1790
|
+
for (const draw of this.opaqueDraws) {
|
|
1818
1791
|
if (draw.count > 0) {
|
|
1819
1792
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1820
1793
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1794
|
+
this.drawCallCount++;
|
|
1821
1795
|
}
|
|
1822
1796
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (this.hairDrawsOverEyes.length > 0) {
|
|
1826
|
-
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1797
|
+
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1798
|
+
pass.setPipeline(this.eyePipeline);
|
|
1827
1799
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1828
|
-
for (const draw of this.
|
|
1800
|
+
for (const draw of this.eyeDraws) {
|
|
1829
1801
|
if (draw.count > 0) {
|
|
1830
1802
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1831
1803
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1832
1804
|
this.drawCallCount++;
|
|
1833
1805
|
}
|
|
1834
1806
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
pass
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1807
|
+
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1808
|
+
this.drawOutlines(pass, false);
|
|
1809
|
+
// 3a: Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1810
|
+
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
1811
|
+
pass.setPipeline(this.hairDepthPipeline);
|
|
1812
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1813
|
+
if (draw.count > 0) {
|
|
1814
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1815
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1819
|
+
if (draw.count > 0) {
|
|
1820
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1821
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1822
|
+
}
|
|
1844
1823
|
}
|
|
1845
1824
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1825
|
+
// 3b: Hair shading (split by stencil for transparency over eyes)
|
|
1826
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
1827
|
+
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1828
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1829
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1830
|
+
if (draw.count > 0) {
|
|
1831
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1832
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1833
|
+
this.drawCallCount++;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1838
|
+
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1839
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1840
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1841
|
+
if (draw.count > 0) {
|
|
1842
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1843
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1844
|
+
this.drawCallCount++;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
// 3c: Hair outlines
|
|
1849
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
1850
|
+
pass.setPipeline(this.hairOutlinePipeline);
|
|
1851
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1852
|
+
if (draw.count > 0) {
|
|
1853
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1854
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
// Pass 4: Transparent
|
|
1859
|
+
pass.setPipeline(this.modelPipeline);
|
|
1860
|
+
for (const draw of this.transparentDraws) {
|
|
1851
1861
|
if (draw.count > 0) {
|
|
1852
1862
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1853
1863
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1864
|
+
this.drawCallCount++;
|
|
1854
1865
|
}
|
|
1855
1866
|
}
|
|
1867
|
+
this.drawOutlines(pass, true);
|
|
1856
1868
|
}
|
|
1857
|
-
// Pass 4: Transparent
|
|
1858
|
-
pass.setPipeline(this.modelPipeline);
|
|
1859
|
-
for (const draw of this.transparentDraws) {
|
|
1860
|
-
if (draw.count > 0) {
|
|
1861
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1862
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1863
|
-
this.drawCallCount++;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
this.drawOutlines(pass, true);
|
|
1867
1869
|
pass.end();
|
|
1868
1870
|
this.device.queue.submit([encoder.finish()]);
|
|
1869
1871
|
this.applyBloom();
|