reze-engine 0.1.15 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +8 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +512 -730
- package/dist/physics.d.ts +1 -0
- package/dist/physics.d.ts.map +1 -1
- package/dist/physics.js +58 -0
- package/dist/vmd-loader.d.ts +25 -0
- package/dist/vmd-loader.d.ts.map +1 -0
- package/dist/vmd-loader.js +141 -0
- package/package.json +1 -1
- package/src/engine.ts +2455 -2659
- package/src/physics.ts +752 -680
- package/src/vmd-loader.ts +179 -0
package/dist/engine.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
|
-
import { Vec3 } from "./math";
|
|
2
|
+
import { Quat, Vec3 } from "./math";
|
|
3
3
|
import { PmxLoader } from "./pmx-loader";
|
|
4
4
|
import { Physics } from "./physics";
|
|
5
|
+
import { VMDLoader } from "./vmd-loader";
|
|
5
6
|
export class Engine {
|
|
6
7
|
constructor(canvas) {
|
|
7
8
|
this.cameraMatrixData = new Float32Array(36);
|
|
@@ -34,6 +35,8 @@ export class Engine {
|
|
|
34
35
|
};
|
|
35
36
|
this.animationFrameId = null;
|
|
36
37
|
this.renderLoopCallback = null;
|
|
38
|
+
this.animationFrames = [];
|
|
39
|
+
this.animationTimeouts = [];
|
|
37
40
|
this.opaqueNonEyeNonHairDraws = [];
|
|
38
41
|
this.eyeDraws = [];
|
|
39
42
|
this.hairDrawsOverEyes = [];
|
|
@@ -81,131 +84,128 @@ export class Engine {
|
|
|
81
84
|
});
|
|
82
85
|
const shaderModule = this.device.createShaderModule({
|
|
83
86
|
label: "model shaders",
|
|
84
|
-
code: /* wgsl */ `
|
|
85
|
-
struct CameraUniforms {
|
|
86
|
-
view: mat4x4f,
|
|
87
|
-
projection: mat4x4f,
|
|
88
|
-
viewPos: vec3f,
|
|
89
|
-
_padding: f32,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
struct Light {
|
|
93
|
-
direction: vec3f,
|
|
94
|
-
_padding1: f32,
|
|
95
|
-
color: vec3f,
|
|
96
|
-
intensity: f32,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
struct LightUniforms {
|
|
100
|
-
ambient: f32,
|
|
101
|
-
lightCount: f32,
|
|
102
|
-
_padding1: f32,
|
|
103
|
-
_padding2: f32,
|
|
104
|
-
lights: array<Light, 4>,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
struct MaterialUniforms {
|
|
108
|
-
alpha: f32,
|
|
109
|
-
alphaMultiplier: f32,
|
|
110
|
-
rimIntensity: f32,
|
|
111
|
-
rimPower: f32,
|
|
112
|
-
rimColor: vec3f,
|
|
113
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
struct VertexOutput {
|
|
117
|
-
@builtin(position) position: vec4f,
|
|
118
|
-
@location(0) normal: vec3f,
|
|
119
|
-
@location(1) uv: vec2f,
|
|
120
|
-
@location(2) worldPos: vec3f,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
124
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
125
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
126
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
127
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
128
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
129
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
130
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
131
|
-
|
|
132
|
-
@vertex fn vs(
|
|
133
|
-
@location(0) position: vec3f,
|
|
134
|
-
@location(1) normal: vec3f,
|
|
135
|
-
@location(2) uv: vec2f,
|
|
136
|
-
@location(3) joints0: vec4<u32>,
|
|
137
|
-
@location(4) weights0: vec4<f32>
|
|
138
|
-
) -> VertexOutput {
|
|
139
|
-
var output: VertexOutput;
|
|
140
|
-
let pos4 = vec4f(position, 1.0);
|
|
141
|
-
|
|
142
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
143
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
144
|
-
var normalizedWeights: vec4f;
|
|
145
|
-
if (weightSum > 0.0001) {
|
|
146
|
-
normalizedWeights = weights0 / weightSum;
|
|
147
|
-
} else {
|
|
148
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
152
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
153
|
-
for (var i = 0u; i < 4u; i++) {
|
|
154
|
-
let j = joints0[i];
|
|
155
|
-
let w = normalizedWeights[i];
|
|
156
|
-
let m = skinMats[j];
|
|
157
|
-
skinnedPos += (m * pos4) * w;
|
|
158
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
159
|
-
skinnedNrm += (r3 * normal) * w;
|
|
160
|
-
}
|
|
161
|
-
let worldPos = skinnedPos.xyz;
|
|
162
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
163
|
-
output.normal = normalize(skinnedNrm);
|
|
164
|
-
output.uv = uv;
|
|
165
|
-
output.worldPos = worldPos;
|
|
166
|
-
return output;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
170
|
-
let n = normalize(input.normal);
|
|
171
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
172
|
-
|
|
173
|
-
var lightAccum = vec3f(light.ambient);
|
|
174
|
-
let numLights = u32(light.lightCount);
|
|
175
|
-
for (var i = 0u; i < numLights; i++) {
|
|
176
|
-
let l = -light.lights[i].direction;
|
|
177
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
178
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
179
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
180
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
181
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Rim light calculation
|
|
185
|
-
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
186
|
-
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
187
|
-
rimFactor = pow(rimFactor, material.rimPower);
|
|
188
|
-
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
189
|
-
|
|
190
|
-
let color = albedo * lightAccum + rimLight;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
204
|
-
}
|
|
87
|
+
code: /* wgsl */ `
|
|
88
|
+
struct CameraUniforms {
|
|
89
|
+
view: mat4x4f,
|
|
90
|
+
projection: mat4x4f,
|
|
91
|
+
viewPos: vec3f,
|
|
92
|
+
_padding: f32,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
struct Light {
|
|
96
|
+
direction: vec3f,
|
|
97
|
+
_padding1: f32,
|
|
98
|
+
color: vec3f,
|
|
99
|
+
intensity: f32,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
struct LightUniforms {
|
|
103
|
+
ambient: f32,
|
|
104
|
+
lightCount: f32,
|
|
105
|
+
_padding1: f32,
|
|
106
|
+
_padding2: f32,
|
|
107
|
+
lights: array<Light, 4>,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
struct MaterialUniforms {
|
|
111
|
+
alpha: f32,
|
|
112
|
+
alphaMultiplier: f32,
|
|
113
|
+
rimIntensity: f32,
|
|
114
|
+
rimPower: f32,
|
|
115
|
+
rimColor: vec3f,
|
|
116
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
struct VertexOutput {
|
|
120
|
+
@builtin(position) position: vec4f,
|
|
121
|
+
@location(0) normal: vec3f,
|
|
122
|
+
@location(1) uv: vec2f,
|
|
123
|
+
@location(2) worldPos: vec3f,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
127
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
128
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
129
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
130
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
131
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
132
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
133
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
134
|
+
|
|
135
|
+
@vertex fn vs(
|
|
136
|
+
@location(0) position: vec3f,
|
|
137
|
+
@location(1) normal: vec3f,
|
|
138
|
+
@location(2) uv: vec2f,
|
|
139
|
+
@location(3) joints0: vec4<u32>,
|
|
140
|
+
@location(4) weights0: vec4<f32>
|
|
141
|
+
) -> VertexOutput {
|
|
142
|
+
var output: VertexOutput;
|
|
143
|
+
let pos4 = vec4f(position, 1.0);
|
|
144
|
+
|
|
145
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
146
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
147
|
+
var normalizedWeights: vec4f;
|
|
148
|
+
if (weightSum > 0.0001) {
|
|
149
|
+
normalizedWeights = weights0 / weightSum;
|
|
150
|
+
} else {
|
|
151
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
155
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
156
|
+
for (var i = 0u; i < 4u; i++) {
|
|
157
|
+
let j = joints0[i];
|
|
158
|
+
let w = normalizedWeights[i];
|
|
159
|
+
let m = skinMats[j];
|
|
160
|
+
skinnedPos += (m * pos4) * w;
|
|
161
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
162
|
+
skinnedNrm += (r3 * normal) * w;
|
|
163
|
+
}
|
|
164
|
+
let worldPos = skinnedPos.xyz;
|
|
165
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
166
|
+
output.normal = normalize(skinnedNrm);
|
|
167
|
+
output.uv = uv;
|
|
168
|
+
output.worldPos = worldPos;
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
173
|
+
let n = normalize(input.normal);
|
|
174
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
175
|
+
|
|
176
|
+
var lightAccum = vec3f(light.ambient);
|
|
177
|
+
let numLights = u32(light.lightCount);
|
|
178
|
+
for (var i = 0u; i < numLights; i++) {
|
|
179
|
+
let l = -light.lights[i].direction;
|
|
180
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
181
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
182
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
183
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
184
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Rim light calculation
|
|
188
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
189
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
190
|
+
rimFactor = pow(rimFactor, material.rimPower);
|
|
191
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
192
|
+
|
|
193
|
+
let color = albedo * lightAccum + rimLight;
|
|
194
|
+
|
|
195
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
196
|
+
if (material.isOverEyes > 0.5) {
|
|
197
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (finalAlpha < 0.001) {
|
|
201
|
+
discard;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
205
|
+
}
|
|
205
206
|
`,
|
|
206
207
|
});
|
|
207
208
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
208
|
-
// This ensures compatibility across all pipelines (main, eye, hair multiply, hair opaque)
|
|
209
209
|
this.hairBindGroupLayout = this.device.createBindGroupLayout({
|
|
210
210
|
label: "shared material bind group layout",
|
|
211
211
|
entries: [
|
|
@@ -293,79 +293,77 @@ export class Engine {
|
|
|
293
293
|
});
|
|
294
294
|
const outlineShaderModule = this.device.createShaderModule({
|
|
295
295
|
label: "outline shaders",
|
|
296
|
-
code: /* wgsl */ `
|
|
297
|
-
struct CameraUniforms {
|
|
298
|
-
view: mat4x4f,
|
|
299
|
-
projection: mat4x4f,
|
|
300
|
-
viewPos: vec3f,
|
|
301
|
-
_padding: f32,
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
struct MaterialUniforms {
|
|
305
|
-
edgeColor: vec4f,
|
|
306
|
-
edgeSize: f32,
|
|
307
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
308
|
-
_padding1: f32,
|
|
309
|
-
_padding2: f32,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
313
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
314
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
315
|
-
|
|
316
|
-
struct VertexOutput {
|
|
317
|
-
@builtin(position) position: vec4f,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
@vertex fn vs(
|
|
321
|
-
@location(0) position: vec3f,
|
|
322
|
-
@location(1) normal: vec3f,
|
|
323
|
-
@location(3) joints0: vec4<u32>,
|
|
324
|
-
@location(4) weights0: vec4<f32>
|
|
325
|
-
) -> VertexOutput {
|
|
326
|
-
var output: VertexOutput;
|
|
327
|
-
let pos4 = vec4f(position, 1.0);
|
|
328
|
-
|
|
329
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
330
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
331
|
-
var normalizedWeights: vec4f;
|
|
332
|
-
if (weightSum > 0.0001) {
|
|
333
|
-
normalizedWeights = weights0 / weightSum;
|
|
334
|
-
} else {
|
|
335
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
339
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
340
|
-
for (var i = 0u; i < 4u; i++) {
|
|
341
|
-
let j = joints0[i];
|
|
342
|
-
let w = normalizedWeights[i];
|
|
343
|
-
let m = skinMats[j];
|
|
344
|
-
skinnedPos += (m * pos4) * w;
|
|
345
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
346
|
-
skinnedNrm += (r3 * normal) * w;
|
|
347
|
-
}
|
|
348
|
-
let worldPos = skinnedPos.xyz;
|
|
349
|
-
let worldNormal = normalize(skinnedNrm);
|
|
350
|
-
|
|
351
|
-
// MMD invert hull: expand vertices outward along normals
|
|
352
|
-
let scaleFactor = 0.01;
|
|
353
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
354
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
355
|
-
return output;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
359
|
-
var color = material.edgeColor;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return color;
|
|
368
|
-
}
|
|
296
|
+
code: /* wgsl */ `
|
|
297
|
+
struct CameraUniforms {
|
|
298
|
+
view: mat4x4f,
|
|
299
|
+
projection: mat4x4f,
|
|
300
|
+
viewPos: vec3f,
|
|
301
|
+
_padding: f32,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
struct MaterialUniforms {
|
|
305
|
+
edgeColor: vec4f,
|
|
306
|
+
edgeSize: f32,
|
|
307
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
308
|
+
_padding1: f32,
|
|
309
|
+
_padding2: f32,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
313
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
314
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
315
|
+
|
|
316
|
+
struct VertexOutput {
|
|
317
|
+
@builtin(position) position: vec4f,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
@vertex fn vs(
|
|
321
|
+
@location(0) position: vec3f,
|
|
322
|
+
@location(1) normal: vec3f,
|
|
323
|
+
@location(3) joints0: vec4<u32>,
|
|
324
|
+
@location(4) weights0: vec4<f32>
|
|
325
|
+
) -> VertexOutput {
|
|
326
|
+
var output: VertexOutput;
|
|
327
|
+
let pos4 = vec4f(position, 1.0);
|
|
328
|
+
|
|
329
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
330
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
331
|
+
var normalizedWeights: vec4f;
|
|
332
|
+
if (weightSum > 0.0001) {
|
|
333
|
+
normalizedWeights = weights0 / weightSum;
|
|
334
|
+
} else {
|
|
335
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
339
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
340
|
+
for (var i = 0u; i < 4u; i++) {
|
|
341
|
+
let j = joints0[i];
|
|
342
|
+
let w = normalizedWeights[i];
|
|
343
|
+
let m = skinMats[j];
|
|
344
|
+
skinnedPos += (m * pos4) * w;
|
|
345
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
346
|
+
skinnedNrm += (r3 * normal) * w;
|
|
347
|
+
}
|
|
348
|
+
let worldPos = skinnedPos.xyz;
|
|
349
|
+
let worldNormal = normalize(skinnedNrm);
|
|
350
|
+
|
|
351
|
+
// MMD invert hull: expand vertices outward along normals
|
|
352
|
+
let scaleFactor = 0.01;
|
|
353
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
354
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
355
|
+
return output;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
359
|
+
var color = material.edgeColor;
|
|
360
|
+
|
|
361
|
+
if (material.isOverEyes > 0.5) {
|
|
362
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return color;
|
|
366
|
+
}
|
|
369
367
|
`,
|
|
370
368
|
});
|
|
371
369
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -431,165 +429,7 @@ export class Engine {
|
|
|
431
429
|
count: this.sampleCount,
|
|
432
430
|
},
|
|
433
431
|
});
|
|
434
|
-
//
|
|
435
|
-
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
436
|
-
label: "hair outline pipeline",
|
|
437
|
-
layout: outlinePipelineLayout,
|
|
438
|
-
vertex: {
|
|
439
|
-
module: outlineShaderModule,
|
|
440
|
-
buffers: [
|
|
441
|
-
{
|
|
442
|
-
arrayStride: 8 * 4,
|
|
443
|
-
attributes: [
|
|
444
|
-
{
|
|
445
|
-
shaderLocation: 0,
|
|
446
|
-
offset: 0,
|
|
447
|
-
format: "float32x3",
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
shaderLocation: 1,
|
|
451
|
-
offset: 3 * 4,
|
|
452
|
-
format: "float32x3",
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
arrayStride: 4 * 2,
|
|
458
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
arrayStride: 4,
|
|
462
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
463
|
-
},
|
|
464
|
-
],
|
|
465
|
-
},
|
|
466
|
-
fragment: {
|
|
467
|
-
module: outlineShaderModule,
|
|
468
|
-
targets: [
|
|
469
|
-
{
|
|
470
|
-
format: this.presentationFormat,
|
|
471
|
-
blend: {
|
|
472
|
-
color: {
|
|
473
|
-
srcFactor: "src-alpha",
|
|
474
|
-
dstFactor: "one-minus-src-alpha",
|
|
475
|
-
operation: "add",
|
|
476
|
-
},
|
|
477
|
-
alpha: {
|
|
478
|
-
srcFactor: "one",
|
|
479
|
-
dstFactor: "one-minus-src-alpha",
|
|
480
|
-
operation: "add",
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
},
|
|
486
|
-
primitive: {
|
|
487
|
-
cullMode: "back",
|
|
488
|
-
},
|
|
489
|
-
depthStencil: {
|
|
490
|
-
format: "depth24plus-stencil8",
|
|
491
|
-
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
492
|
-
depthCompare: "less-equal", // Only draw where hair depth exists
|
|
493
|
-
stencilFront: {
|
|
494
|
-
compare: "not-equal", // Only render where stencil != 1 (not over eyes)
|
|
495
|
-
failOp: "keep",
|
|
496
|
-
depthFailOp: "keep",
|
|
497
|
-
passOp: "keep",
|
|
498
|
-
},
|
|
499
|
-
stencilBack: {
|
|
500
|
-
compare: "not-equal",
|
|
501
|
-
failOp: "keep",
|
|
502
|
-
depthFailOp: "keep",
|
|
503
|
-
passOp: "keep",
|
|
504
|
-
},
|
|
505
|
-
},
|
|
506
|
-
multisample: {
|
|
507
|
-
count: this.sampleCount,
|
|
508
|
-
},
|
|
509
|
-
});
|
|
510
|
-
// Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists - Uses depth compare "equal" with a small bias to only appear where hair geometry exists
|
|
511
|
-
this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
|
|
512
|
-
label: "hair outline over eyes pipeline",
|
|
513
|
-
layout: outlinePipelineLayout,
|
|
514
|
-
vertex: {
|
|
515
|
-
module: outlineShaderModule,
|
|
516
|
-
buffers: [
|
|
517
|
-
{
|
|
518
|
-
arrayStride: 8 * 4,
|
|
519
|
-
attributes: [
|
|
520
|
-
{
|
|
521
|
-
shaderLocation: 0,
|
|
522
|
-
offset: 0,
|
|
523
|
-
format: "float32x3",
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
shaderLocation: 1,
|
|
527
|
-
offset: 3 * 4,
|
|
528
|
-
format: "float32x3",
|
|
529
|
-
},
|
|
530
|
-
],
|
|
531
|
-
},
|
|
532
|
-
{
|
|
533
|
-
arrayStride: 4 * 2,
|
|
534
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
arrayStride: 4,
|
|
538
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
539
|
-
},
|
|
540
|
-
],
|
|
541
|
-
},
|
|
542
|
-
fragment: {
|
|
543
|
-
module: outlineShaderModule,
|
|
544
|
-
targets: [
|
|
545
|
-
{
|
|
546
|
-
format: this.presentationFormat,
|
|
547
|
-
blend: {
|
|
548
|
-
color: {
|
|
549
|
-
srcFactor: "src-alpha",
|
|
550
|
-
dstFactor: "one-minus-src-alpha",
|
|
551
|
-
operation: "add",
|
|
552
|
-
},
|
|
553
|
-
alpha: {
|
|
554
|
-
srcFactor: "one",
|
|
555
|
-
dstFactor: "one-minus-src-alpha",
|
|
556
|
-
operation: "add",
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
],
|
|
561
|
-
},
|
|
562
|
-
primitive: {
|
|
563
|
-
cullMode: "back",
|
|
564
|
-
},
|
|
565
|
-
depthStencil: {
|
|
566
|
-
format: "depth24plus-stencil8",
|
|
567
|
-
depthWriteEnabled: false, // Don't write depth
|
|
568
|
-
depthCompare: "less-equal", // Draw where outline depth <= existing depth (hair depth)
|
|
569
|
-
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
570
|
-
depthBiasSlopeScale: 0.0,
|
|
571
|
-
depthBiasClamp: 0.0,
|
|
572
|
-
stencilFront: {
|
|
573
|
-
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
574
|
-
failOp: "keep",
|
|
575
|
-
depthFailOp: "keep",
|
|
576
|
-
passOp: "keep",
|
|
577
|
-
},
|
|
578
|
-
stencilBack: {
|
|
579
|
-
compare: "equal",
|
|
580
|
-
failOp: "keep",
|
|
581
|
-
depthFailOp: "keep",
|
|
582
|
-
passOp: "keep",
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
multisample: {
|
|
586
|
-
count: this.sampleCount,
|
|
587
|
-
},
|
|
588
|
-
});
|
|
589
|
-
// Unified hair outline pipeline: single pass without stencil testing
|
|
590
|
-
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
591
|
-
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
592
|
-
// This eliminates the need for two separate outline passes
|
|
432
|
+
// Unified hair outline pipeline: single pass without stencil testing, uses depth test "less-equal" to draw everywhere hair exists
|
|
593
433
|
this.hairUnifiedOutlinePipeline = this.device.createRenderPipeline({
|
|
594
434
|
label: "unified hair outline pipeline",
|
|
595
435
|
layout: outlinePipelineLayout,
|
|
@@ -656,137 +496,6 @@ export class Engine {
|
|
|
656
496
|
count: this.sampleCount,
|
|
657
497
|
},
|
|
658
498
|
});
|
|
659
|
-
// Unified hair pipeline - can be used for both over-eyes and over-non-eyes
|
|
660
|
-
// The difference is controlled by stencil state and alpha multiplier in material uniform
|
|
661
|
-
this.hairMultiplyPipeline = this.device.createRenderPipeline({
|
|
662
|
-
label: "hair pipeline (over eyes)",
|
|
663
|
-
layout: sharedPipelineLayout,
|
|
664
|
-
vertex: {
|
|
665
|
-
module: shaderModule,
|
|
666
|
-
buffers: [
|
|
667
|
-
{
|
|
668
|
-
arrayStride: 8 * 4,
|
|
669
|
-
attributes: [
|
|
670
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
671
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
672
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
673
|
-
],
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
arrayStride: 4 * 2,
|
|
677
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
arrayStride: 4,
|
|
681
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
682
|
-
},
|
|
683
|
-
],
|
|
684
|
-
},
|
|
685
|
-
fragment: {
|
|
686
|
-
module: shaderModule,
|
|
687
|
-
targets: [
|
|
688
|
-
{
|
|
689
|
-
format: this.presentationFormat,
|
|
690
|
-
blend: {
|
|
691
|
-
color: {
|
|
692
|
-
srcFactor: "src-alpha",
|
|
693
|
-
dstFactor: "one-minus-src-alpha",
|
|
694
|
-
operation: "add",
|
|
695
|
-
},
|
|
696
|
-
alpha: {
|
|
697
|
-
srcFactor: "one",
|
|
698
|
-
dstFactor: "one-minus-src-alpha",
|
|
699
|
-
operation: "add",
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
},
|
|
703
|
-
],
|
|
704
|
-
},
|
|
705
|
-
primitive: { cullMode: "none" },
|
|
706
|
-
depthStencil: {
|
|
707
|
-
format: "depth24plus-stencil8",
|
|
708
|
-
depthWriteEnabled: true, // Write depth so outlines can test against it
|
|
709
|
-
depthCompare: "less",
|
|
710
|
-
stencilFront: {
|
|
711
|
-
compare: "equal", // Only render where stencil == 1
|
|
712
|
-
failOp: "keep",
|
|
713
|
-
depthFailOp: "keep",
|
|
714
|
-
passOp: "keep",
|
|
715
|
-
},
|
|
716
|
-
stencilBack: {
|
|
717
|
-
compare: "equal",
|
|
718
|
-
failOp: "keep",
|
|
719
|
-
depthFailOp: "keep",
|
|
720
|
-
passOp: "keep",
|
|
721
|
-
},
|
|
722
|
-
},
|
|
723
|
-
multisample: { count: this.sampleCount },
|
|
724
|
-
});
|
|
725
|
-
// Hair pipeline for opaque rendering (hair over non-eyes) - uses same shader, different stencil state
|
|
726
|
-
this.hairOpaquePipeline = this.device.createRenderPipeline({
|
|
727
|
-
label: "hair pipeline (over non-eyes)",
|
|
728
|
-
layout: sharedPipelineLayout,
|
|
729
|
-
vertex: {
|
|
730
|
-
module: shaderModule,
|
|
731
|
-
buffers: [
|
|
732
|
-
{
|
|
733
|
-
arrayStride: 8 * 4,
|
|
734
|
-
attributes: [
|
|
735
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
736
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
737
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
738
|
-
],
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
arrayStride: 4 * 2,
|
|
742
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
743
|
-
},
|
|
744
|
-
{
|
|
745
|
-
arrayStride: 4,
|
|
746
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
747
|
-
},
|
|
748
|
-
],
|
|
749
|
-
},
|
|
750
|
-
fragment: {
|
|
751
|
-
module: shaderModule,
|
|
752
|
-
targets: [
|
|
753
|
-
{
|
|
754
|
-
format: this.presentationFormat,
|
|
755
|
-
blend: {
|
|
756
|
-
color: {
|
|
757
|
-
srcFactor: "src-alpha",
|
|
758
|
-
dstFactor: "one-minus-src-alpha",
|
|
759
|
-
operation: "add",
|
|
760
|
-
},
|
|
761
|
-
alpha: {
|
|
762
|
-
srcFactor: "one",
|
|
763
|
-
dstFactor: "one-minus-src-alpha",
|
|
764
|
-
operation: "add",
|
|
765
|
-
},
|
|
766
|
-
},
|
|
767
|
-
},
|
|
768
|
-
],
|
|
769
|
-
},
|
|
770
|
-
primitive: { cullMode: "none" },
|
|
771
|
-
depthStencil: {
|
|
772
|
-
format: "depth24plus-stencil8",
|
|
773
|
-
depthWriteEnabled: true,
|
|
774
|
-
depthCompare: "less",
|
|
775
|
-
stencilFront: {
|
|
776
|
-
compare: "not-equal", // Only render where stencil != 1
|
|
777
|
-
failOp: "keep",
|
|
778
|
-
depthFailOp: "keep",
|
|
779
|
-
passOp: "keep",
|
|
780
|
-
},
|
|
781
|
-
stencilBack: {
|
|
782
|
-
compare: "not-equal",
|
|
783
|
-
failOp: "keep",
|
|
784
|
-
depthFailOp: "keep",
|
|
785
|
-
passOp: "keep",
|
|
786
|
-
},
|
|
787
|
-
},
|
|
788
|
-
multisample: { count: this.sampleCount },
|
|
789
|
-
});
|
|
790
499
|
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
791
500
|
this.eyePipeline = this.device.createRenderPipeline({
|
|
792
501
|
label: "eye overlay pipeline",
|
|
@@ -855,57 +564,52 @@ export class Engine {
|
|
|
855
564
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
856
565
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
857
566
|
label: "depth only shader",
|
|
858
|
-
code: /* wgsl */ `
|
|
859
|
-
struct CameraUniforms {
|
|
860
|
-
view: mat4x4f,
|
|
861
|
-
projection: mat4x4f,
|
|
862
|
-
viewPos: vec3f,
|
|
863
|
-
_padding: f32,
|
|
864
|
-
};
|
|
865
|
-
|
|
866
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
867
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
868
|
-
|
|
869
|
-
@vertex fn vs(
|
|
870
|
-
@location(0) position: vec3f,
|
|
871
|
-
@location(1) normal: vec3f,
|
|
872
|
-
@location(3) joints0: vec4<u32>,
|
|
873
|
-
@location(4) weights0: vec4<f32>
|
|
874
|
-
) -> @builtin(position) vec4f {
|
|
875
|
-
let pos4 = vec4f(position, 1.0);
|
|
876
|
-
|
|
877
|
-
// Normalize weights
|
|
878
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
879
|
-
var normalizedWeights: vec4f;
|
|
880
|
-
if (weightSum > 0.0001) {
|
|
881
|
-
normalizedWeights = weights0 / weightSum;
|
|
882
|
-
} else {
|
|
883
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
887
|
-
for (var i = 0u; i < 4u; i++) {
|
|
888
|
-
let j = joints0[i];
|
|
889
|
-
let w = normalizedWeights[i];
|
|
890
|
-
let m = skinMats[j];
|
|
891
|
-
skinnedPos += (m * pos4) * w;
|
|
892
|
-
}
|
|
893
|
-
let worldPos = skinnedPos.xyz;
|
|
894
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
895
|
-
return clipPos;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
902
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
903
|
-
}
|
|
567
|
+
code: /* wgsl */ `
|
|
568
|
+
struct CameraUniforms {
|
|
569
|
+
view: mat4x4f,
|
|
570
|
+
projection: mat4x4f,
|
|
571
|
+
viewPos: vec3f,
|
|
572
|
+
_padding: f32,
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
576
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
577
|
+
|
|
578
|
+
@vertex fn vs(
|
|
579
|
+
@location(0) position: vec3f,
|
|
580
|
+
@location(1) normal: vec3f,
|
|
581
|
+
@location(3) joints0: vec4<u32>,
|
|
582
|
+
@location(4) weights0: vec4<f32>
|
|
583
|
+
) -> @builtin(position) vec4f {
|
|
584
|
+
let pos4 = vec4f(position, 1.0);
|
|
585
|
+
|
|
586
|
+
// Normalize weights
|
|
587
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
588
|
+
var normalizedWeights: vec4f;
|
|
589
|
+
if (weightSum > 0.0001) {
|
|
590
|
+
normalizedWeights = weights0 / weightSum;
|
|
591
|
+
} else {
|
|
592
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
596
|
+
for (var i = 0u; i < 4u; i++) {
|
|
597
|
+
let j = joints0[i];
|
|
598
|
+
let w = normalizedWeights[i];
|
|
599
|
+
let m = skinMats[j];
|
|
600
|
+
skinnedPos += (m * pos4) * w;
|
|
601
|
+
}
|
|
602
|
+
let worldPos = skinnedPos.xyz;
|
|
603
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
604
|
+
return clipPos;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
608
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
609
|
+
}
|
|
904
610
|
`,
|
|
905
611
|
});
|
|
906
|
-
// Hair depth pre-pass pipeline
|
|
907
|
-
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
908
|
-
// Note: Must have a color target to match render pass, but we disable all color writes
|
|
612
|
+
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
909
613
|
this.hairDepthPipeline = this.device.createRenderPipeline({
|
|
910
614
|
label: "hair depth pre-pass",
|
|
911
615
|
layout: sharedPipelineLayout,
|
|
@@ -947,11 +651,7 @@ export class Engine {
|
|
|
947
651
|
},
|
|
948
652
|
multisample: { count: this.sampleCount },
|
|
949
653
|
});
|
|
950
|
-
// Unified hair pipeline: single pass with dynamic branching
|
|
951
|
-
// Uses stencil testing to filter fragments, then shader branches on isOverEyes uniform
|
|
952
|
-
// This eliminates the need for separate pipelines - same shader, different stencil states
|
|
953
|
-
// We create two variants: one for over-eyes (stencil == 1) and one for over-non-eyes (stencil != 1)
|
|
954
|
-
// Unified pipeline for hair over eyes (stencil == 1)
|
|
654
|
+
// Unified hair pipeline for over-eyes (stencil == 1): single pass with dynamic branching
|
|
955
655
|
this.hairUnifiedPipelineOverEyes = this.device.createRenderPipeline({
|
|
956
656
|
label: "unified hair pipeline (over eyes)",
|
|
957
657
|
layout: sharedPipelineLayout,
|
|
@@ -1086,31 +786,31 @@ export class Engine {
|
|
|
1086
786
|
createSkinMatrixComputePipeline() {
|
|
1087
787
|
const computeShader = this.device.createShaderModule({
|
|
1088
788
|
label: "skin matrix compute",
|
|
1089
|
-
code: /* wgsl */ `
|
|
1090
|
-
struct BoneCountUniform {
|
|
1091
|
-
count: u32,
|
|
1092
|
-
_padding1: u32,
|
|
1093
|
-
_padding2: u32,
|
|
1094
|
-
_padding3: u32,
|
|
1095
|
-
_padding4: vec4<u32>,
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
1099
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
1100
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
1101
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
1102
|
-
|
|
1103
|
-
@compute @workgroup_size(64)
|
|
1104
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
1105
|
-
let boneIndex = globalId.x;
|
|
1106
|
-
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
1107
|
-
if (boneIndex >= boneCount.count) {
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
let worldMat = worldMatrices[boneIndex];
|
|
1111
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
1112
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
1113
|
-
}
|
|
789
|
+
code: /* wgsl */ `
|
|
790
|
+
struct BoneCountUniform {
|
|
791
|
+
count: u32,
|
|
792
|
+
_padding1: u32,
|
|
793
|
+
_padding2: u32,
|
|
794
|
+
_padding3: u32,
|
|
795
|
+
_padding4: vec4<u32>,
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
799
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
800
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
801
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
802
|
+
|
|
803
|
+
@compute @workgroup_size(64)
|
|
804
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
805
|
+
let boneIndex = globalId.x;
|
|
806
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
807
|
+
if (boneIndex >= boneCount.count) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
let worldMat = worldMatrices[boneIndex];
|
|
811
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
812
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
813
|
+
}
|
|
1114
814
|
`,
|
|
1115
815
|
});
|
|
1116
816
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -1164,143 +864,143 @@ export class Engine {
|
|
|
1164
864
|
// Bloom extraction shader (extracts bright areas)
|
|
1165
865
|
const bloomExtractShader = this.device.createShaderModule({
|
|
1166
866
|
label: "bloom extract",
|
|
1167
|
-
code: /* wgsl */ `
|
|
1168
|
-
struct VertexOutput {
|
|
1169
|
-
@builtin(position) position: vec4f,
|
|
1170
|
-
@location(0) uv: vec2f,
|
|
1171
|
-
};
|
|
1172
|
-
|
|
1173
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1174
|
-
var output: VertexOutput;
|
|
1175
|
-
// Generate fullscreen quad from vertex index
|
|
1176
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1177
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1178
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1179
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1180
|
-
return output;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
struct BloomExtractUniforms {
|
|
1184
|
-
threshold: f32,
|
|
1185
|
-
_padding1: f32,
|
|
1186
|
-
_padding2: f32,
|
|
1187
|
-
_padding3: f32,
|
|
1188
|
-
_padding4: f32,
|
|
1189
|
-
_padding5: f32,
|
|
1190
|
-
_padding6: f32,
|
|
1191
|
-
_padding7: f32,
|
|
1192
|
-
};
|
|
1193
|
-
|
|
1194
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
1195
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
1196
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
1197
|
-
|
|
1198
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1199
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
1200
|
-
// Extract bright areas above threshold
|
|
1201
|
-
let threshold = extractUniforms.threshold;
|
|
1202
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
1203
|
-
return vec4f(bloom, color.a);
|
|
1204
|
-
}
|
|
867
|
+
code: /* wgsl */ `
|
|
868
|
+
struct VertexOutput {
|
|
869
|
+
@builtin(position) position: vec4f,
|
|
870
|
+
@location(0) uv: vec2f,
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
874
|
+
var output: VertexOutput;
|
|
875
|
+
// Generate fullscreen quad from vertex index
|
|
876
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
877
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
878
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
879
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
880
|
+
return output;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
struct BloomExtractUniforms {
|
|
884
|
+
threshold: f32,
|
|
885
|
+
_padding1: f32,
|
|
886
|
+
_padding2: f32,
|
|
887
|
+
_padding3: f32,
|
|
888
|
+
_padding4: f32,
|
|
889
|
+
_padding5: f32,
|
|
890
|
+
_padding6: f32,
|
|
891
|
+
_padding7: f32,
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
895
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
896
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
897
|
+
|
|
898
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
899
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
900
|
+
// Extract bright areas above threshold
|
|
901
|
+
let threshold = extractUniforms.threshold;
|
|
902
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
903
|
+
return vec4f(bloom, color.a);
|
|
904
|
+
}
|
|
1205
905
|
`,
|
|
1206
906
|
});
|
|
1207
907
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
1208
908
|
const bloomBlurShader = this.device.createShaderModule({
|
|
1209
909
|
label: "bloom blur",
|
|
1210
|
-
code: /* wgsl */ `
|
|
1211
|
-
struct VertexOutput {
|
|
1212
|
-
@builtin(position) position: vec4f,
|
|
1213
|
-
@location(0) uv: vec2f,
|
|
1214
|
-
};
|
|
1215
|
-
|
|
1216
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1217
|
-
var output: VertexOutput;
|
|
1218
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1219
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1220
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1221
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1222
|
-
return output;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
struct BlurUniforms {
|
|
1226
|
-
direction: vec2f,
|
|
1227
|
-
_padding1: f32,
|
|
1228
|
-
_padding2: f32,
|
|
1229
|
-
_padding3: f32,
|
|
1230
|
-
_padding4: f32,
|
|
1231
|
-
_padding5: f32,
|
|
1232
|
-
_padding6: f32,
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1235
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
1236
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
1237
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
1238
|
-
|
|
1239
|
-
// 9-tap gaussian blur
|
|
1240
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1241
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
1242
|
-
var result = vec4f(0.0);
|
|
1243
|
-
|
|
1244
|
-
// Gaussian weights for 9-tap filter
|
|
1245
|
-
let weights = array<f32, 9>(
|
|
1246
|
-
0.01621622, 0.05405405, 0.12162162,
|
|
1247
|
-
0.19459459, 0.22702703,
|
|
1248
|
-
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
1249
|
-
);
|
|
1250
|
-
|
|
1251
|
-
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
1252
|
-
|
|
1253
|
-
for (var i = 0u; i < 9u; i++) {
|
|
1254
|
-
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
1255
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
return result;
|
|
1259
|
-
}
|
|
910
|
+
code: /* wgsl */ `
|
|
911
|
+
struct VertexOutput {
|
|
912
|
+
@builtin(position) position: vec4f,
|
|
913
|
+
@location(0) uv: vec2f,
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
917
|
+
var output: VertexOutput;
|
|
918
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
919
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
920
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
921
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
922
|
+
return output;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
struct BlurUniforms {
|
|
926
|
+
direction: vec2f,
|
|
927
|
+
_padding1: f32,
|
|
928
|
+
_padding2: f32,
|
|
929
|
+
_padding3: f32,
|
|
930
|
+
_padding4: f32,
|
|
931
|
+
_padding5: f32,
|
|
932
|
+
_padding6: f32,
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
936
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
937
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
938
|
+
|
|
939
|
+
// 9-tap gaussian blur
|
|
940
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
941
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
942
|
+
var result = vec4f(0.0);
|
|
943
|
+
|
|
944
|
+
// Gaussian weights for 9-tap filter
|
|
945
|
+
let weights = array<f32, 9>(
|
|
946
|
+
0.01621622, 0.05405405, 0.12162162,
|
|
947
|
+
0.19459459, 0.22702703,
|
|
948
|
+
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
952
|
+
|
|
953
|
+
for (var i = 0u; i < 9u; i++) {
|
|
954
|
+
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
955
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
1260
960
|
`,
|
|
1261
961
|
});
|
|
1262
962
|
// Bloom composition shader (combines original scene with bloom)
|
|
1263
963
|
const bloomComposeShader = this.device.createShaderModule({
|
|
1264
964
|
label: "bloom compose",
|
|
1265
|
-
code: /* wgsl */ `
|
|
1266
|
-
struct VertexOutput {
|
|
1267
|
-
@builtin(position) position: vec4f,
|
|
1268
|
-
@location(0) uv: vec2f,
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1272
|
-
var output: VertexOutput;
|
|
1273
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1274
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1275
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1276
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1277
|
-
return output;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
struct BloomComposeUniforms {
|
|
1281
|
-
intensity: f32,
|
|
1282
|
-
_padding1: f32,
|
|
1283
|
-
_padding2: f32,
|
|
1284
|
-
_padding3: f32,
|
|
1285
|
-
_padding4: f32,
|
|
1286
|
-
_padding5: f32,
|
|
1287
|
-
_padding6: f32,
|
|
1288
|
-
_padding7: f32,
|
|
1289
|
-
};
|
|
1290
|
-
|
|
1291
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
1292
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
1293
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1294
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1295
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1296
|
-
|
|
1297
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1298
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1299
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1300
|
-
// Additive blending with intensity control
|
|
1301
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1302
|
-
return vec4f(result, scene.a);
|
|
1303
|
-
}
|
|
965
|
+
code: /* wgsl */ `
|
|
966
|
+
struct VertexOutput {
|
|
967
|
+
@builtin(position) position: vec4f,
|
|
968
|
+
@location(0) uv: vec2f,
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
972
|
+
var output: VertexOutput;
|
|
973
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
974
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
975
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
976
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
977
|
+
return output;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
struct BloomComposeUniforms {
|
|
981
|
+
intensity: f32,
|
|
982
|
+
_padding1: f32,
|
|
983
|
+
_padding2: f32,
|
|
984
|
+
_padding3: f32,
|
|
985
|
+
_padding4: f32,
|
|
986
|
+
_padding5: f32,
|
|
987
|
+
_padding6: f32,
|
|
988
|
+
_padding7: f32,
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
992
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
993
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
994
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
995
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
996
|
+
|
|
997
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
998
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
999
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1000
|
+
// Additive blending with intensity control
|
|
1001
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1002
|
+
return vec4f(result, scene.a);
|
|
1003
|
+
}
|
|
1304
1004
|
`,
|
|
1305
1005
|
});
|
|
1306
1006
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1508,9 +1208,9 @@ export class Engine {
|
|
|
1508
1208
|
depthClearValue: 1.0,
|
|
1509
1209
|
depthLoadOp: "clear",
|
|
1510
1210
|
depthStoreOp: "store",
|
|
1511
|
-
stencilClearValue: 0,
|
|
1512
|
-
stencilLoadOp: "clear",
|
|
1513
|
-
stencilStoreOp: "
|
|
1211
|
+
stencilClearValue: 0,
|
|
1212
|
+
stencilLoadOp: "clear",
|
|
1213
|
+
stencilStoreOp: "discard", // Discard stencil after frame to save bandwidth (we only use it during rendering)
|
|
1514
1214
|
},
|
|
1515
1215
|
};
|
|
1516
1216
|
this.camera.aspect = width / height;
|
|
@@ -1561,6 +1261,107 @@ export class Engine {
|
|
|
1561
1261
|
setAmbient(intensity) {
|
|
1562
1262
|
this.lightData[0] = intensity;
|
|
1563
1263
|
}
|
|
1264
|
+
async loadAnimation(url) {
|
|
1265
|
+
const frames = await VMDLoader.load(url);
|
|
1266
|
+
this.animationFrames = frames;
|
|
1267
|
+
console.log(this.animationFrames);
|
|
1268
|
+
}
|
|
1269
|
+
playAnimation() {
|
|
1270
|
+
if (this.animationFrames.length === 0)
|
|
1271
|
+
return;
|
|
1272
|
+
this.stopAnimation();
|
|
1273
|
+
const allBoneKeyFrames = [];
|
|
1274
|
+
for (const keyFrame of this.animationFrames) {
|
|
1275
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
1276
|
+
allBoneKeyFrames.push({
|
|
1277
|
+
boneName: boneFrame.boneName,
|
|
1278
|
+
time: keyFrame.time,
|
|
1279
|
+
rotation: boneFrame.rotation,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
const boneKeyFramesByBone = new Map();
|
|
1284
|
+
for (const boneKeyFrame of allBoneKeyFrames) {
|
|
1285
|
+
if (!boneKeyFramesByBone.has(boneKeyFrame.boneName)) {
|
|
1286
|
+
boneKeyFramesByBone.set(boneKeyFrame.boneName, []);
|
|
1287
|
+
}
|
|
1288
|
+
boneKeyFramesByBone.get(boneKeyFrame.boneName).push(boneKeyFrame);
|
|
1289
|
+
}
|
|
1290
|
+
for (const keyFrames of boneKeyFramesByBone.values()) {
|
|
1291
|
+
keyFrames.sort((a, b) => a.time - b.time);
|
|
1292
|
+
}
|
|
1293
|
+
const time0Rotations = [];
|
|
1294
|
+
const bonesWithTime0 = new Set();
|
|
1295
|
+
for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
1296
|
+
if (keyFrames.length > 0 && keyFrames[0].time === 0) {
|
|
1297
|
+
time0Rotations.push({
|
|
1298
|
+
boneName: boneName,
|
|
1299
|
+
rotation: keyFrames[0].rotation,
|
|
1300
|
+
});
|
|
1301
|
+
bonesWithTime0.add(boneName);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (this.currentModel) {
|
|
1305
|
+
if (time0Rotations.length > 0) {
|
|
1306
|
+
const boneNames = time0Rotations.map((r) => r.boneName);
|
|
1307
|
+
const rotations = time0Rotations.map((r) => r.rotation);
|
|
1308
|
+
this.rotateBones(boneNames, rotations, 0);
|
|
1309
|
+
}
|
|
1310
|
+
const skeleton = this.currentModel.getSkeleton();
|
|
1311
|
+
const bonesToReset = [];
|
|
1312
|
+
for (const bone of skeleton.bones) {
|
|
1313
|
+
if (!bonesWithTime0.has(bone.name)) {
|
|
1314
|
+
bonesToReset.push(bone.name);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (bonesToReset.length > 0) {
|
|
1318
|
+
const identityQuat = new Quat(0, 0, 0, 1);
|
|
1319
|
+
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1320
|
+
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1321
|
+
}
|
|
1322
|
+
this.currentModel.evaluatePose();
|
|
1323
|
+
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1324
|
+
if (this.physics) {
|
|
1325
|
+
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1326
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1327
|
+
// Upload matrices immediately so next frame shows correct pose
|
|
1328
|
+
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1329
|
+
this.computeSkinMatrices();
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
1333
|
+
for (let i = 0; i < keyFrames.length; i++) {
|
|
1334
|
+
const boneKeyFrame = keyFrames[i];
|
|
1335
|
+
const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null;
|
|
1336
|
+
if (boneKeyFrame.time === 0)
|
|
1337
|
+
continue;
|
|
1338
|
+
let durationMs = 0;
|
|
1339
|
+
if (i === 0) {
|
|
1340
|
+
durationMs = boneKeyFrame.time * 1000;
|
|
1341
|
+
}
|
|
1342
|
+
else if (previousBoneKeyFrame) {
|
|
1343
|
+
durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000;
|
|
1344
|
+
}
|
|
1345
|
+
const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0;
|
|
1346
|
+
const delayMs = scheduleTime * 1000;
|
|
1347
|
+
if (delayMs <= 0) {
|
|
1348
|
+
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
const timeoutId = window.setTimeout(() => {
|
|
1352
|
+
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
|
|
1353
|
+
}, delayMs);
|
|
1354
|
+
this.animationTimeouts.push(timeoutId);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
stopAnimation() {
|
|
1360
|
+
for (const timeoutId of this.animationTimeouts) {
|
|
1361
|
+
clearTimeout(timeoutId);
|
|
1362
|
+
}
|
|
1363
|
+
this.animationTimeouts = [];
|
|
1364
|
+
}
|
|
1564
1365
|
getStats() {
|
|
1565
1366
|
return { ...this.stats };
|
|
1566
1367
|
}
|
|
@@ -1584,6 +1385,7 @@ export class Engine {
|
|
|
1584
1385
|
}
|
|
1585
1386
|
dispose() {
|
|
1586
1387
|
this.stopRenderLoop();
|
|
1388
|
+
this.stopAnimation();
|
|
1587
1389
|
if (this.camera)
|
|
1588
1390
|
this.camera.detachControl();
|
|
1589
1391
|
if (this.resizeObserver) {
|
|
@@ -1758,7 +1560,7 @@ export class Engine {
|
|
|
1758
1560
|
materialUniformData[4] = this.rimLightColor[0]; // rimColor.r
|
|
1759
1561
|
materialUniformData[5] = this.rimLightColor[1]; // rimColor.g
|
|
1760
1562
|
materialUniformData[6] = this.rimLightColor[2]; // rimColor.b
|
|
1761
|
-
materialUniformData[7] = 0.0;
|
|
1563
|
+
materialUniformData[7] = 0.0;
|
|
1762
1564
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1763
1565
|
label: `material uniform: ${mat.name}`,
|
|
1764
1566
|
size: materialUniformData.byteLength,
|
|
@@ -1790,9 +1592,7 @@ export class Engine {
|
|
|
1790
1592
|
});
|
|
1791
1593
|
}
|
|
1792
1594
|
else if (mat.isHair) {
|
|
1793
|
-
//
|
|
1794
|
-
// The shader will dynamically branch based on isOverEyes uniform
|
|
1795
|
-
// We still need two uniform buffers (one for each render mode) but can reuse the same bind group structure
|
|
1595
|
+
// Hair materials: create bind groups for unified pipeline with dynamic branching
|
|
1796
1596
|
const materialUniformDataHair = new Float32Array(8);
|
|
1797
1597
|
materialUniformDataHair[0] = materialAlpha;
|
|
1798
1598
|
materialUniformDataHair[1] = 1.0; // alphaMultiplier: base value, shader will adjust
|
|
@@ -1801,15 +1601,15 @@ export class Engine {
|
|
|
1801
1601
|
materialUniformDataHair[4] = this.rimLightColor[0]; // rimColor.r
|
|
1802
1602
|
materialUniformDataHair[5] = this.rimLightColor[1]; // rimColor.g
|
|
1803
1603
|
materialUniformDataHair[6] = this.rimLightColor[2]; // rimColor.b
|
|
1804
|
-
materialUniformDataHair[7] = 0.0;
|
|
1805
|
-
// Create uniform buffers for both modes
|
|
1604
|
+
materialUniformDataHair[7] = 0.0;
|
|
1605
|
+
// Create uniform buffers for both modes
|
|
1806
1606
|
const materialUniformBufferOverEyes = this.device.createBuffer({
|
|
1807
1607
|
label: `material uniform (over eyes): ${mat.name}`,
|
|
1808
1608
|
size: materialUniformDataHair.byteLength,
|
|
1809
1609
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1810
1610
|
});
|
|
1811
1611
|
const materialUniformDataOverEyes = new Float32Array(materialUniformDataHair);
|
|
1812
|
-
materialUniformDataOverEyes[7] = 1.0;
|
|
1612
|
+
materialUniformDataOverEyes[7] = 1.0;
|
|
1813
1613
|
this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes);
|
|
1814
1614
|
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1815
1615
|
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
@@ -1817,9 +1617,9 @@ export class Engine {
|
|
|
1817
1617
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1818
1618
|
});
|
|
1819
1619
|
const materialUniformDataOverNonEyes = new Float32Array(materialUniformDataHair);
|
|
1820
|
-
materialUniformDataOverNonEyes[7] = 0.0;
|
|
1620
|
+
materialUniformDataOverNonEyes[7] = 0.0;
|
|
1821
1621
|
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes);
|
|
1822
|
-
// Create bind groups for both modes
|
|
1622
|
+
// Create bind groups for both modes
|
|
1823
1623
|
const bindGroupOverEyes = this.device.createBindGroup({
|
|
1824
1624
|
label: `material bind group (over eyes): ${mat.name}`,
|
|
1825
1625
|
layout: this.hairBindGroupLayout,
|
|
@@ -1848,7 +1648,7 @@ export class Engine {
|
|
|
1848
1648
|
{ binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
|
|
1849
1649
|
],
|
|
1850
1650
|
});
|
|
1851
|
-
// Store both bind groups
|
|
1651
|
+
// Store both bind groups for unified pipeline
|
|
1852
1652
|
this.hairDrawsOverEyes.push({
|
|
1853
1653
|
count: matCount,
|
|
1854
1654
|
firstIndex: runningFirstIndex,
|
|
@@ -1886,7 +1686,7 @@ export class Engine {
|
|
|
1886
1686
|
materialUniformData[2] = mat.edgeColor[2]; // edgeColor.b
|
|
1887
1687
|
materialUniformData[3] = mat.edgeColor[3]; // edgeColor.a
|
|
1888
1688
|
materialUniformData[4] = mat.edgeSize;
|
|
1889
|
-
materialUniformData[5] =
|
|
1689
|
+
materialUniformData[5] = 0.0; // isOverEyes: 0.0 for all (unified pipeline doesn't use stencil)
|
|
1890
1690
|
materialUniformData[6] = 0.0; // _padding1
|
|
1891
1691
|
materialUniformData[7] = 0.0; // _padding2
|
|
1892
1692
|
const materialUniformBuffer = this.device.createBuffer({
|
|
@@ -2002,8 +1802,7 @@ export class Engine {
|
|
|
2002
1802
|
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
2003
1803
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
2004
1804
|
this.drawCallCount = 0;
|
|
2005
|
-
// PASS 1: Opaque non-eye, non-hair
|
|
2006
|
-
// this.drawOutlines(pass, false) // Opaque outlines
|
|
1805
|
+
// PASS 1: Opaque non-eye, non-hair
|
|
2007
1806
|
pass.setPipeline(this.pipeline);
|
|
2008
1807
|
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
2009
1808
|
if (draw.count > 0) {
|
|
@@ -2022,18 +1821,13 @@ export class Engine {
|
|
|
2022
1821
|
this.drawCallCount++;
|
|
2023
1822
|
}
|
|
2024
1823
|
}
|
|
2025
|
-
// PASS 3: Hair rendering
|
|
2026
|
-
|
|
2027
|
-
//
|
|
2028
|
-
this.drawOutlines(pass, false); // Opaque outlines
|
|
2029
|
-
// 3a: Hair depth pre-pass (depth-only, no color writes)
|
|
2030
|
-
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
1824
|
+
// PASS 3: Hair rendering with depth pre-pass and unified pipeline
|
|
1825
|
+
this.drawOutlines(pass, false);
|
|
1826
|
+
// 3a: Hair depth pre-pass (eliminates overdraw by rejecting fragments early)
|
|
2031
1827
|
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
2032
1828
|
pass.setPipeline(this.hairDepthPipeline);
|
|
2033
|
-
// Render all hair materials for depth (no stencil test needed for depth pass)
|
|
2034
1829
|
for (const draw of this.hairDrawsOverEyes) {
|
|
2035
1830
|
if (draw.count > 0) {
|
|
2036
|
-
// Use the same bind group structure (camera, skin matrices) for depth pass
|
|
2037
1831
|
pass.setBindGroup(0, draw.bindGroup);
|
|
2038
1832
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2039
1833
|
}
|
|
@@ -2045,10 +1839,7 @@ export class Engine {
|
|
|
2045
1839
|
}
|
|
2046
1840
|
}
|
|
2047
1841
|
}
|
|
2048
|
-
// 3b: Hair shading pass
|
|
2049
|
-
// Uses depth test "equal" to only render where depth was written in pre-pass
|
|
2050
|
-
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
2051
|
-
// This eliminates one full geometry pass compared to the old approach
|
|
1842
|
+
// 3b: Hair shading pass with unified pipeline and dynamic branching
|
|
2052
1843
|
if (this.hairDrawsOverEyes.length > 0) {
|
|
2053
1844
|
pass.setPipeline(this.hairUnifiedPipelineOverEyes);
|
|
2054
1845
|
pass.setStencilReference(1);
|
|
@@ -2072,9 +1863,6 @@ export class Engine {
|
|
|
2072
1863
|
}
|
|
2073
1864
|
}
|
|
2074
1865
|
// 3c: Hair outlines - unified single pass without stencil testing
|
|
2075
|
-
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
2076
|
-
// Shader branches on isOverEyes uniform to adjust alpha dynamically (currently always 0.0)
|
|
2077
|
-
// This eliminates the need for two separate outline passes
|
|
2078
1866
|
if (this.hairOutlineDraws.length > 0) {
|
|
2079
1867
|
pass.setPipeline(this.hairUnifiedOutlinePipeline);
|
|
2080
1868
|
for (const draw of this.hairOutlineDraws) {
|
|
@@ -2093,7 +1881,7 @@ export class Engine {
|
|
|
2093
1881
|
this.drawCallCount++;
|
|
2094
1882
|
}
|
|
2095
1883
|
}
|
|
2096
|
-
this.drawOutlines(pass, true);
|
|
1884
|
+
this.drawOutlines(pass, true);
|
|
2097
1885
|
pass.end();
|
|
2098
1886
|
this.device.queue.submit([encoder.finish()]);
|
|
2099
1887
|
// Apply bloom post-processing
|
|
@@ -2216,19 +2004,13 @@ export class Engine {
|
|
|
2216
2004
|
colorAttachment.view = this.sceneRenderTextureView;
|
|
2217
2005
|
}
|
|
2218
2006
|
}
|
|
2219
|
-
// Update model pose and physics
|
|
2220
2007
|
updateModelPose(deltaTime) {
|
|
2221
|
-
// Step 1: Animation evaluation (computes matrices to CPU memory, no upload yet)
|
|
2222
2008
|
this.currentModel.evaluatePose();
|
|
2223
|
-
// Step 2: Get world matrices (still in CPU memory)
|
|
2224
2009
|
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
2225
|
-
// Step 3: Physics modifies matrices in-place
|
|
2226
2010
|
if (this.physics) {
|
|
2227
2011
|
this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
2228
2012
|
}
|
|
2229
|
-
// Step 4: Upload ONCE with final result (animation + physics)
|
|
2230
2013
|
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
2231
|
-
// Step 5: GPU skinning
|
|
2232
2014
|
this.computeSkinMatrices();
|
|
2233
2015
|
}
|
|
2234
2016
|
// Compute skin matrices on GPU
|