reze-engine 0.2.10 → 0.2.12
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 +7 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +502 -433
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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 +2464 -2389
- package/src/index.ts +1 -0
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/pmx-loader.ts +1054 -1054
- package/src/pool.ts +483 -0
- package/src/vmd-loader.ts +179 -179
package/dist/engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { Quat, Vec3 } from "./math";
|
|
3
|
+
import { Pool } from "./pool";
|
|
3
4
|
import { PmxLoader } from "./pmx-loader";
|
|
4
5
|
import { Physics } from "./physics";
|
|
5
6
|
import { VMDLoader } from "./vmd-loader";
|
|
@@ -26,6 +27,7 @@ export class Engine {
|
|
|
26
27
|
this.currentModel = null;
|
|
27
28
|
this.modelDir = "";
|
|
28
29
|
this.physics = null;
|
|
30
|
+
this.pool = null;
|
|
29
31
|
this.textureCache = new Map();
|
|
30
32
|
// Draw lists
|
|
31
33
|
this.opaqueDraws = [];
|
|
@@ -53,6 +55,8 @@ export class Engine {
|
|
|
53
55
|
this.animationFrames = [];
|
|
54
56
|
this.animationTimeouts = [];
|
|
55
57
|
this.gpuMemoryMB = 0;
|
|
58
|
+
this.hasAnimation = false; // Set to true when loadAnimation is called
|
|
59
|
+
this.playingAnimation = false; // Set to true when playAnimation is called
|
|
56
60
|
this.canvas = canvas;
|
|
57
61
|
if (options) {
|
|
58
62
|
this.ambient = options.ambient ?? 1.0;
|
|
@@ -97,121 +101,121 @@ export class Engine {
|
|
|
97
101
|
});
|
|
98
102
|
const shaderModule = this.device.createShaderModule({
|
|
99
103
|
label: "model shaders",
|
|
100
|
-
code: /* wgsl */ `
|
|
101
|
-
struct CameraUniforms {
|
|
102
|
-
view: mat4x4f,
|
|
103
|
-
projection: mat4x4f,
|
|
104
|
-
viewPos: vec3f,
|
|
105
|
-
_padding: f32,
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
struct Light {
|
|
109
|
-
direction: vec3f,
|
|
110
|
-
_padding1: f32,
|
|
111
|
-
color: vec3f,
|
|
112
|
-
intensity: f32,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
struct LightUniforms {
|
|
116
|
-
ambient: f32,
|
|
117
|
-
lightCount: f32,
|
|
118
|
-
_padding1: f32,
|
|
119
|
-
_padding2: f32,
|
|
120
|
-
lights: array<Light, 4>,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
struct MaterialUniforms {
|
|
124
|
-
alpha: f32,
|
|
125
|
-
alphaMultiplier: f32,
|
|
126
|
-
rimIntensity: f32,
|
|
127
|
-
_padding1: f32,
|
|
128
|
-
rimColor: vec3f,
|
|
129
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
struct VertexOutput {
|
|
133
|
-
@builtin(position) position: vec4f,
|
|
134
|
-
@location(0) normal: vec3f,
|
|
135
|
-
@location(1) uv: vec2f,
|
|
136
|
-
@location(2) worldPos: vec3f,
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
140
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
141
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
142
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
143
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
144
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
145
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
146
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
147
|
-
|
|
148
|
-
@vertex fn vs(
|
|
149
|
-
@location(0) position: vec3f,
|
|
150
|
-
@location(1) normal: vec3f,
|
|
151
|
-
@location(2) uv: vec2f,
|
|
152
|
-
@location(3) joints0: vec4<u32>,
|
|
153
|
-
@location(4) weights0: vec4<f32>
|
|
154
|
-
) -> VertexOutput {
|
|
155
|
-
var output: VertexOutput;
|
|
156
|
-
let pos4 = vec4f(position, 1.0);
|
|
157
|
-
|
|
158
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
159
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
160
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
161
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
162
|
-
|
|
163
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
164
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
165
|
-
for (var i = 0u; i < 4u; i++) {
|
|
166
|
-
let j = joints0[i];
|
|
167
|
-
let w = normalizedWeights[i];
|
|
168
|
-
let m = skinMats[j];
|
|
169
|
-
skinnedPos += (m * pos4) * w;
|
|
170
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
171
|
-
skinnedNrm += (r3 * normal) * w;
|
|
172
|
-
}
|
|
173
|
-
let worldPos = skinnedPos.xyz;
|
|
174
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
175
|
-
output.normal = normalize(skinnedNrm);
|
|
176
|
-
output.uv = uv;
|
|
177
|
-
output.worldPos = worldPos;
|
|
178
|
-
return output;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
182
|
-
// Early alpha test - discard before expensive calculations
|
|
183
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
184
|
-
if (material.isOverEyes > 0.5) {
|
|
185
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
186
|
-
}
|
|
187
|
-
if (finalAlpha < 0.001) {
|
|
188
|
-
discard;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let n = normalize(input.normal);
|
|
192
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
193
|
-
|
|
194
|
-
var lightAccum = vec3f(light.ambient);
|
|
195
|
-
let numLights = u32(light.lightCount);
|
|
196
|
-
for (var i = 0u; i < numLights; i++) {
|
|
197
|
-
let l = -light.lights[i].direction;
|
|
198
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
199
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
200
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
201
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
202
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Rim light calculation
|
|
206
|
-
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
207
|
-
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
208
|
-
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
209
|
-
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
210
|
-
|
|
211
|
-
let color = albedo * lightAccum + rimLight;
|
|
212
|
-
|
|
213
|
-
return vec4f(color, finalAlpha);
|
|
214
|
-
}
|
|
104
|
+
code: /* wgsl */ `
|
|
105
|
+
struct CameraUniforms {
|
|
106
|
+
view: mat4x4f,
|
|
107
|
+
projection: mat4x4f,
|
|
108
|
+
viewPos: vec3f,
|
|
109
|
+
_padding: f32,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
struct Light {
|
|
113
|
+
direction: vec3f,
|
|
114
|
+
_padding1: f32,
|
|
115
|
+
color: vec3f,
|
|
116
|
+
intensity: f32,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
struct LightUniforms {
|
|
120
|
+
ambient: f32,
|
|
121
|
+
lightCount: f32,
|
|
122
|
+
_padding1: f32,
|
|
123
|
+
_padding2: f32,
|
|
124
|
+
lights: array<Light, 4>,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
struct MaterialUniforms {
|
|
128
|
+
alpha: f32,
|
|
129
|
+
alphaMultiplier: f32,
|
|
130
|
+
rimIntensity: f32,
|
|
131
|
+
_padding1: f32,
|
|
132
|
+
rimColor: vec3f,
|
|
133
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
struct VertexOutput {
|
|
137
|
+
@builtin(position) position: vec4f,
|
|
138
|
+
@location(0) normal: vec3f,
|
|
139
|
+
@location(1) uv: vec2f,
|
|
140
|
+
@location(2) worldPos: vec3f,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
144
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
145
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
146
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
147
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
148
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
149
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
150
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
151
|
+
|
|
152
|
+
@vertex fn vs(
|
|
153
|
+
@location(0) position: vec3f,
|
|
154
|
+
@location(1) normal: vec3f,
|
|
155
|
+
@location(2) uv: vec2f,
|
|
156
|
+
@location(3) joints0: vec4<u32>,
|
|
157
|
+
@location(4) weights0: vec4<f32>
|
|
158
|
+
) -> VertexOutput {
|
|
159
|
+
var output: VertexOutput;
|
|
160
|
+
let pos4 = vec4f(position, 1.0);
|
|
161
|
+
|
|
162
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
163
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
164
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
165
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
166
|
+
|
|
167
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
168
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
169
|
+
for (var i = 0u; i < 4u; i++) {
|
|
170
|
+
let j = joints0[i];
|
|
171
|
+
let w = normalizedWeights[i];
|
|
172
|
+
let m = skinMats[j];
|
|
173
|
+
skinnedPos += (m * pos4) * w;
|
|
174
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
175
|
+
skinnedNrm += (r3 * normal) * w;
|
|
176
|
+
}
|
|
177
|
+
let worldPos = skinnedPos.xyz;
|
|
178
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
179
|
+
output.normal = normalize(skinnedNrm);
|
|
180
|
+
output.uv = uv;
|
|
181
|
+
output.worldPos = worldPos;
|
|
182
|
+
return output;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
186
|
+
// Early alpha test - discard before expensive calculations
|
|
187
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
188
|
+
if (material.isOverEyes > 0.5) {
|
|
189
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
190
|
+
}
|
|
191
|
+
if (finalAlpha < 0.001) {
|
|
192
|
+
discard;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let n = normalize(input.normal);
|
|
196
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
197
|
+
|
|
198
|
+
var lightAccum = vec3f(light.ambient);
|
|
199
|
+
let numLights = u32(light.lightCount);
|
|
200
|
+
for (var i = 0u; i < numLights; i++) {
|
|
201
|
+
let l = -light.lights[i].direction;
|
|
202
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
203
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
204
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
205
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
206
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Rim light calculation
|
|
210
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
211
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
212
|
+
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
213
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
214
|
+
|
|
215
|
+
let color = albedo * lightAccum + rimLight;
|
|
216
|
+
|
|
217
|
+
return vec4f(color, finalAlpha);
|
|
218
|
+
}
|
|
215
219
|
`,
|
|
216
220
|
});
|
|
217
221
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -301,73 +305,73 @@ export class Engine {
|
|
|
301
305
|
});
|
|
302
306
|
const outlineShaderModule = this.device.createShaderModule({
|
|
303
307
|
label: "outline shaders",
|
|
304
|
-
code: /* wgsl */ `
|
|
305
|
-
struct CameraUniforms {
|
|
306
|
-
view: mat4x4f,
|
|
307
|
-
projection: mat4x4f,
|
|
308
|
-
viewPos: vec3f,
|
|
309
|
-
_padding: f32,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
struct MaterialUniforms {
|
|
313
|
-
edgeColor: vec4f,
|
|
314
|
-
edgeSize: f32,
|
|
315
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
316
|
-
_padding1: f32,
|
|
317
|
-
_padding2: f32,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
321
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
322
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
323
|
-
|
|
324
|
-
struct VertexOutput {
|
|
325
|
-
@builtin(position) position: vec4f,
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
@vertex fn vs(
|
|
329
|
-
@location(0) position: vec3f,
|
|
330
|
-
@location(1) normal: vec3f,
|
|
331
|
-
@location(3) joints0: vec4<u32>,
|
|
332
|
-
@location(4) weights0: vec4<f32>
|
|
333
|
-
) -> VertexOutput {
|
|
334
|
-
var output: VertexOutput;
|
|
335
|
-
let pos4 = vec4f(position, 1.0);
|
|
336
|
-
|
|
337
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
338
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
339
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
340
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
341
|
-
|
|
342
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
343
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
344
|
-
for (var i = 0u; i < 4u; i++) {
|
|
345
|
-
let j = joints0[i];
|
|
346
|
-
let w = normalizedWeights[i];
|
|
347
|
-
let m = skinMats[j];
|
|
348
|
-
skinnedPos += (m * pos4) * w;
|
|
349
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
350
|
-
skinnedNrm += (r3 * normal) * w;
|
|
351
|
-
}
|
|
352
|
-
let worldPos = skinnedPos.xyz;
|
|
353
|
-
let worldNormal = normalize(skinnedNrm);
|
|
354
|
-
|
|
355
|
-
// MMD invert hull: expand vertices outward along normals
|
|
356
|
-
let scaleFactor = 0.01;
|
|
357
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
358
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
359
|
-
return output;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
363
|
-
var color = material.edgeColor;
|
|
364
|
-
|
|
365
|
-
if (material.isOverEyes > 0.5) {
|
|
366
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return color;
|
|
370
|
-
}
|
|
308
|
+
code: /* wgsl */ `
|
|
309
|
+
struct CameraUniforms {
|
|
310
|
+
view: mat4x4f,
|
|
311
|
+
projection: mat4x4f,
|
|
312
|
+
viewPos: vec3f,
|
|
313
|
+
_padding: f32,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
struct MaterialUniforms {
|
|
317
|
+
edgeColor: vec4f,
|
|
318
|
+
edgeSize: f32,
|
|
319
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
320
|
+
_padding1: f32,
|
|
321
|
+
_padding2: f32,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
325
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
326
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
327
|
+
|
|
328
|
+
struct VertexOutput {
|
|
329
|
+
@builtin(position) position: vec4f,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
@vertex fn vs(
|
|
333
|
+
@location(0) position: vec3f,
|
|
334
|
+
@location(1) normal: vec3f,
|
|
335
|
+
@location(3) joints0: vec4<u32>,
|
|
336
|
+
@location(4) weights0: vec4<f32>
|
|
337
|
+
) -> VertexOutput {
|
|
338
|
+
var output: VertexOutput;
|
|
339
|
+
let pos4 = vec4f(position, 1.0);
|
|
340
|
+
|
|
341
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
342
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
343
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
344
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
345
|
+
|
|
346
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
347
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
348
|
+
for (var i = 0u; i < 4u; i++) {
|
|
349
|
+
let j = joints0[i];
|
|
350
|
+
let w = normalizedWeights[i];
|
|
351
|
+
let m = skinMats[j];
|
|
352
|
+
skinnedPos += (m * pos4) * w;
|
|
353
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
354
|
+
skinnedNrm += (r3 * normal) * w;
|
|
355
|
+
}
|
|
356
|
+
let worldPos = skinnedPos.xyz;
|
|
357
|
+
let worldNormal = normalize(skinnedNrm);
|
|
358
|
+
|
|
359
|
+
// MMD invert hull: expand vertices outward along normals
|
|
360
|
+
let scaleFactor = 0.01;
|
|
361
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
362
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
363
|
+
return output;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
367
|
+
var color = material.edgeColor;
|
|
368
|
+
|
|
369
|
+
if (material.isOverEyes > 0.5) {
|
|
370
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return color;
|
|
374
|
+
}
|
|
371
375
|
`,
|
|
372
376
|
});
|
|
373
377
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -571,45 +575,45 @@ export class Engine {
|
|
|
571
575
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
572
576
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
573
577
|
label: "depth only shader",
|
|
574
|
-
code: /* wgsl */ `
|
|
575
|
-
struct CameraUniforms {
|
|
576
|
-
view: mat4x4f,
|
|
577
|
-
projection: mat4x4f,
|
|
578
|
-
viewPos: vec3f,
|
|
579
|
-
_padding: f32,
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
583
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
584
|
-
|
|
585
|
-
@vertex fn vs(
|
|
586
|
-
@location(0) position: vec3f,
|
|
587
|
-
@location(1) normal: vec3f,
|
|
588
|
-
@location(3) joints0: vec4<u32>,
|
|
589
|
-
@location(4) weights0: vec4<f32>
|
|
590
|
-
) -> @builtin(position) vec4f {
|
|
591
|
-
let pos4 = vec4f(position, 1.0);
|
|
592
|
-
|
|
593
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
594
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
595
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
596
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
597
|
-
|
|
598
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
599
|
-
for (var i = 0u; i < 4u; i++) {
|
|
600
|
-
let j = joints0[i];
|
|
601
|
-
let w = normalizedWeights[i];
|
|
602
|
-
let m = skinMats[j];
|
|
603
|
-
skinnedPos += (m * pos4) * w;
|
|
604
|
-
}
|
|
605
|
-
let worldPos = skinnedPos.xyz;
|
|
606
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
607
|
-
return clipPos;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
611
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
612
|
-
}
|
|
578
|
+
code: /* wgsl */ `
|
|
579
|
+
struct CameraUniforms {
|
|
580
|
+
view: mat4x4f,
|
|
581
|
+
projection: mat4x4f,
|
|
582
|
+
viewPos: vec3f,
|
|
583
|
+
_padding: f32,
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
587
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
588
|
+
|
|
589
|
+
@vertex fn vs(
|
|
590
|
+
@location(0) position: vec3f,
|
|
591
|
+
@location(1) normal: vec3f,
|
|
592
|
+
@location(3) joints0: vec4<u32>,
|
|
593
|
+
@location(4) weights0: vec4<f32>
|
|
594
|
+
) -> @builtin(position) vec4f {
|
|
595
|
+
let pos4 = vec4f(position, 1.0);
|
|
596
|
+
|
|
597
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
598
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
599
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
600
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
601
|
+
|
|
602
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
603
|
+
for (var i = 0u; i < 4u; i++) {
|
|
604
|
+
let j = joints0[i];
|
|
605
|
+
let w = normalizedWeights[i];
|
|
606
|
+
let m = skinMats[j];
|
|
607
|
+
skinnedPos += (m * pos4) * w;
|
|
608
|
+
}
|
|
609
|
+
let worldPos = skinnedPos.xyz;
|
|
610
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
611
|
+
return clipPos;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
615
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
616
|
+
}
|
|
613
617
|
`,
|
|
614
618
|
});
|
|
615
619
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
@@ -792,30 +796,30 @@ export class Engine {
|
|
|
792
796
|
createSkinMatrixComputePipeline() {
|
|
793
797
|
const computeShader = this.device.createShaderModule({
|
|
794
798
|
label: "skin matrix compute",
|
|
795
|
-
code: /* wgsl */ `
|
|
796
|
-
struct BoneCountUniform {
|
|
797
|
-
count: u32,
|
|
798
|
-
_padding1: u32,
|
|
799
|
-
_padding2: u32,
|
|
800
|
-
_padding3: u32,
|
|
801
|
-
_padding4: vec4<u32>,
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
805
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
806
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
807
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
808
|
-
|
|
809
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
810
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
811
|
-
let boneIndex = globalId.x;
|
|
812
|
-
if (boneIndex >= boneCount.count) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
let worldMat = worldMatrices[boneIndex];
|
|
816
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
817
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
818
|
-
}
|
|
799
|
+
code: /* wgsl */ `
|
|
800
|
+
struct BoneCountUniform {
|
|
801
|
+
count: u32,
|
|
802
|
+
_padding1: u32,
|
|
803
|
+
_padding2: u32,
|
|
804
|
+
_padding3: u32,
|
|
805
|
+
_padding4: vec4<u32>,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
809
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
810
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
811
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
812
|
+
|
|
813
|
+
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
814
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
815
|
+
let boneIndex = globalId.x;
|
|
816
|
+
if (boneIndex >= boneCount.count) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
let worldMat = worldMatrices[boneIndex];
|
|
820
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
821
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
822
|
+
}
|
|
819
823
|
`,
|
|
820
824
|
});
|
|
821
825
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -869,140 +873,140 @@ export class Engine {
|
|
|
869
873
|
// Bloom extraction shader (extracts bright areas)
|
|
870
874
|
const bloomExtractShader = this.device.createShaderModule({
|
|
871
875
|
label: "bloom extract",
|
|
872
|
-
code: /* wgsl */ `
|
|
873
|
-
struct VertexOutput {
|
|
874
|
-
@builtin(position) position: vec4f,
|
|
875
|
-
@location(0) uv: vec2f,
|
|
876
|
-
};
|
|
877
|
-
|
|
878
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
879
|
-
var output: VertexOutput;
|
|
880
|
-
// Generate fullscreen quad from vertex index
|
|
881
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
882
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
883
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
884
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
885
|
-
return output;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
struct BloomExtractUniforms {
|
|
889
|
-
threshold: f32,
|
|
890
|
-
_padding1: f32,
|
|
891
|
-
_padding2: f32,
|
|
892
|
-
_padding3: f32,
|
|
893
|
-
_padding4: f32,
|
|
894
|
-
_padding5: f32,
|
|
895
|
-
_padding6: f32,
|
|
896
|
-
_padding7: f32,
|
|
897
|
-
};
|
|
898
|
-
|
|
899
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
900
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
901
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
902
|
-
|
|
903
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
904
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
905
|
-
// Extract bright areas above threshold
|
|
906
|
-
let threshold = extractUniforms.threshold;
|
|
907
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
908
|
-
return vec4f(bloom, color.a);
|
|
909
|
-
}
|
|
876
|
+
code: /* wgsl */ `
|
|
877
|
+
struct VertexOutput {
|
|
878
|
+
@builtin(position) position: vec4f,
|
|
879
|
+
@location(0) uv: vec2f,
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
883
|
+
var output: VertexOutput;
|
|
884
|
+
// Generate fullscreen quad from vertex index
|
|
885
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
886
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
887
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
888
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
889
|
+
return output;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
struct BloomExtractUniforms {
|
|
893
|
+
threshold: f32,
|
|
894
|
+
_padding1: f32,
|
|
895
|
+
_padding2: f32,
|
|
896
|
+
_padding3: f32,
|
|
897
|
+
_padding4: f32,
|
|
898
|
+
_padding5: f32,
|
|
899
|
+
_padding6: f32,
|
|
900
|
+
_padding7: f32,
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
904
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
905
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
906
|
+
|
|
907
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
908
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
909
|
+
// Extract bright areas above threshold
|
|
910
|
+
let threshold = extractUniforms.threshold;
|
|
911
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
912
|
+
return vec4f(bloom, color.a);
|
|
913
|
+
}
|
|
910
914
|
`,
|
|
911
915
|
});
|
|
912
916
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
913
917
|
const bloomBlurShader = this.device.createShaderModule({
|
|
914
918
|
label: "bloom blur",
|
|
915
|
-
code: /* wgsl */ `
|
|
916
|
-
struct VertexOutput {
|
|
917
|
-
@builtin(position) position: vec4f,
|
|
918
|
-
@location(0) uv: vec2f,
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
922
|
-
var output: VertexOutput;
|
|
923
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
924
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
925
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
926
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
927
|
-
return output;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
struct BlurUniforms {
|
|
931
|
-
direction: vec2f,
|
|
932
|
-
_padding1: f32,
|
|
933
|
-
_padding2: f32,
|
|
934
|
-
_padding3: f32,
|
|
935
|
-
_padding4: f32,
|
|
936
|
-
_padding5: f32,
|
|
937
|
-
_padding6: f32,
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
941
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
942
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
943
|
-
|
|
944
|
-
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
945
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
946
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
947
|
-
|
|
948
|
-
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
949
|
-
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
950
|
-
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
951
|
-
let weight0 = 0.38774; // Center sample
|
|
952
|
-
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
953
|
-
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
954
|
-
|
|
955
|
-
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
956
|
-
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
957
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
958
|
-
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
959
|
-
|
|
960
|
-
return result;
|
|
961
|
-
}
|
|
919
|
+
code: /* wgsl */ `
|
|
920
|
+
struct VertexOutput {
|
|
921
|
+
@builtin(position) position: vec4f,
|
|
922
|
+
@location(0) uv: vec2f,
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
926
|
+
var output: VertexOutput;
|
|
927
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
928
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
929
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
930
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
931
|
+
return output;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
struct BlurUniforms {
|
|
935
|
+
direction: vec2f,
|
|
936
|
+
_padding1: f32,
|
|
937
|
+
_padding2: f32,
|
|
938
|
+
_padding3: f32,
|
|
939
|
+
_padding4: f32,
|
|
940
|
+
_padding5: f32,
|
|
941
|
+
_padding6: f32,
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
945
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
946
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
947
|
+
|
|
948
|
+
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
949
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
950
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
951
|
+
|
|
952
|
+
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
953
|
+
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
954
|
+
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
955
|
+
let weight0 = 0.38774; // Center sample
|
|
956
|
+
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
957
|
+
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
958
|
+
|
|
959
|
+
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
960
|
+
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
961
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
962
|
+
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
963
|
+
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
962
966
|
`,
|
|
963
967
|
});
|
|
964
968
|
// Bloom composition shader (combines original scene with bloom)
|
|
965
969
|
const bloomComposeShader = this.device.createShaderModule({
|
|
966
970
|
label: "bloom compose",
|
|
967
|
-
code: /* wgsl */ `
|
|
968
|
-
struct VertexOutput {
|
|
969
|
-
@builtin(position) position: vec4f,
|
|
970
|
-
@location(0) uv: vec2f,
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
974
|
-
var output: VertexOutput;
|
|
975
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
976
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
977
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
978
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
979
|
-
return output;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
struct BloomComposeUniforms {
|
|
983
|
-
intensity: f32,
|
|
984
|
-
_padding1: f32,
|
|
985
|
-
_padding2: f32,
|
|
986
|
-
_padding3: f32,
|
|
987
|
-
_padding4: f32,
|
|
988
|
-
_padding5: f32,
|
|
989
|
-
_padding6: f32,
|
|
990
|
-
_padding7: f32,
|
|
991
|
-
};
|
|
992
|
-
|
|
993
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
994
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
995
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
996
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
997
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
998
|
-
|
|
999
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1000
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1001
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1002
|
-
// Additive blending with intensity control
|
|
1003
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1004
|
-
return vec4f(result, scene.a);
|
|
1005
|
-
}
|
|
971
|
+
code: /* wgsl */ `
|
|
972
|
+
struct VertexOutput {
|
|
973
|
+
@builtin(position) position: vec4f,
|
|
974
|
+
@location(0) uv: vec2f,
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
978
|
+
var output: VertexOutput;
|
|
979
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
980
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
981
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
982
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
983
|
+
return output;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
struct BloomComposeUniforms {
|
|
987
|
+
intensity: f32,
|
|
988
|
+
_padding1: f32,
|
|
989
|
+
_padding2: f32,
|
|
990
|
+
_padding3: f32,
|
|
991
|
+
_padding4: f32,
|
|
992
|
+
_padding5: f32,
|
|
993
|
+
_padding6: f32,
|
|
994
|
+
_padding7: f32,
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
998
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
999
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1000
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1001
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1002
|
+
|
|
1003
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1004
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1005
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1006
|
+
// Additive blending with intensity control
|
|
1007
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1008
|
+
return vec4f(result, scene.a);
|
|
1009
|
+
}
|
|
1006
1010
|
`,
|
|
1007
1011
|
});
|
|
1008
1012
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1227,6 +1231,36 @@ export class Engine {
|
|
|
1227
1231
|
this.camera.aspect = this.canvas.width / this.canvas.height;
|
|
1228
1232
|
this.camera.attachControl(this.canvas);
|
|
1229
1233
|
}
|
|
1234
|
+
// Create camera bind group layout for pool (camera-only)
|
|
1235
|
+
createCameraBindGroupLayout() {
|
|
1236
|
+
return this.device.createBindGroupLayout({
|
|
1237
|
+
label: "camera bind group layout",
|
|
1238
|
+
entries: [
|
|
1239
|
+
{
|
|
1240
|
+
binding: 0,
|
|
1241
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
1242
|
+
buffer: {
|
|
1243
|
+
type: "uniform",
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
],
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
// Create camera bind group for pool
|
|
1250
|
+
createCameraBindGroup(layout) {
|
|
1251
|
+
return this.device.createBindGroup({
|
|
1252
|
+
label: "camera bind group for pool",
|
|
1253
|
+
layout: layout,
|
|
1254
|
+
entries: [
|
|
1255
|
+
{
|
|
1256
|
+
binding: 0,
|
|
1257
|
+
resource: {
|
|
1258
|
+
buffer: this.cameraUniformBuffer,
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
],
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1230
1264
|
// Step 5: Create lighting buffers
|
|
1231
1265
|
setupLighting() {
|
|
1232
1266
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -1264,11 +1298,13 @@ export class Engine {
|
|
|
1264
1298
|
async loadAnimation(url) {
|
|
1265
1299
|
const frames = await VMDLoader.load(url);
|
|
1266
1300
|
this.animationFrames = frames;
|
|
1301
|
+
this.hasAnimation = true;
|
|
1267
1302
|
}
|
|
1268
1303
|
playAnimation() {
|
|
1269
1304
|
if (this.animationFrames.length === 0)
|
|
1270
1305
|
return;
|
|
1271
1306
|
this.stopAnimation();
|
|
1307
|
+
this.playingAnimation = true;
|
|
1272
1308
|
const allBoneKeyFrames = [];
|
|
1273
1309
|
for (const keyFrame of this.animationFrames) {
|
|
1274
1310
|
for (const boneFrame of keyFrame.boneFrames) {
|
|
@@ -1318,9 +1354,9 @@ export class Engine {
|
|
|
1318
1354
|
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1319
1355
|
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1320
1356
|
}
|
|
1321
|
-
this.currentModel.evaluatePose();
|
|
1322
1357
|
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1323
1358
|
if (this.physics) {
|
|
1359
|
+
this.currentModel.evaluatePose();
|
|
1324
1360
|
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1325
1361
|
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1326
1362
|
// Upload matrices immediately so next frame shows correct pose
|
|
@@ -1362,6 +1398,7 @@ export class Engine {
|
|
|
1362
1398
|
clearTimeout(timeoutId);
|
|
1363
1399
|
}
|
|
1364
1400
|
this.animationTimeouts = [];
|
|
1401
|
+
this.playingAnimation = false;
|
|
1365
1402
|
}
|
|
1366
1403
|
getStats() {
|
|
1367
1404
|
return { ...this.stats };
|
|
@@ -1412,6 +1449,14 @@ export class Engine {
|
|
|
1412
1449
|
this.physics = new Physics(model.getRigidbodies(), model.getJoints());
|
|
1413
1450
|
await this.setupModelBuffers(model);
|
|
1414
1451
|
}
|
|
1452
|
+
async addPool(options) {
|
|
1453
|
+
if (!this.device) {
|
|
1454
|
+
throw new Error("Engine must be initialized before adding pool");
|
|
1455
|
+
}
|
|
1456
|
+
const cameraLayout = this.createCameraBindGroupLayout();
|
|
1457
|
+
this.pool = new Pool(this.device, cameraLayout, this.cameraUniformBuffer, options);
|
|
1458
|
+
await this.pool.init();
|
|
1459
|
+
}
|
|
1415
1460
|
rotateBones(bones, rotations, durationMs) {
|
|
1416
1461
|
this.currentModel?.rotateBones(bones, rotations, durationMs);
|
|
1417
1462
|
}
|
|
@@ -1757,7 +1802,7 @@ export class Engine {
|
|
|
1757
1802
|
}
|
|
1758
1803
|
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
|
|
1759
1804
|
render() {
|
|
1760
|
-
if (this.multisampleTexture && this.camera && this.device
|
|
1805
|
+
if (this.multisampleTexture && this.camera && this.device) {
|
|
1761
1806
|
const currentTime = performance.now();
|
|
1762
1807
|
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016;
|
|
1763
1808
|
this.lastFrameTime = currentTime;
|
|
@@ -1766,92 +1811,116 @@ export class Engine {
|
|
|
1766
1811
|
// Use single encoder for both compute and render (reduces sync points)
|
|
1767
1812
|
const encoder = this.device.createCommandEncoder();
|
|
1768
1813
|
this.updateModelPose(deltaTime, encoder);
|
|
1814
|
+
// Hide model if animation is loaded but not playing yet (prevents A-pose flash)
|
|
1815
|
+
// Still update physics and poses, just don't render visually
|
|
1816
|
+
if (this.hasAnimation && !this.playingAnimation) {
|
|
1817
|
+
// Submit encoder to ensure matrices are uploaded and physics initializes
|
|
1818
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1769
1821
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1770
|
-
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1771
|
-
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
1772
|
-
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1773
|
-
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1774
1822
|
this.drawCallCount = 0;
|
|
1775
|
-
//
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1780
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1781
|
-
this.drawCallCount++;
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1785
|
-
pass.setPipeline(this.eyePipeline);
|
|
1786
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1787
|
-
for (const draw of this.eyeDraws) {
|
|
1788
|
-
if (draw.count > 0) {
|
|
1789
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1790
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1791
|
-
this.drawCallCount++;
|
|
1792
|
-
}
|
|
1823
|
+
// Render pool first if no model
|
|
1824
|
+
if (this.pool && !this.currentModel) {
|
|
1825
|
+
this.pool.render(pass);
|
|
1826
|
+
this.drawCallCount++;
|
|
1793
1827
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
pass.
|
|
1799
|
-
|
|
1828
|
+
if (this.currentModel) {
|
|
1829
|
+
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1830
|
+
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
1831
|
+
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1832
|
+
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1833
|
+
// Pass 1: Opaque
|
|
1834
|
+
pass.setPipeline(this.modelPipeline);
|
|
1835
|
+
for (const draw of this.opaqueDraws) {
|
|
1800
1836
|
if (draw.count > 0) {
|
|
1801
1837
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1802
1838
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1839
|
+
this.drawCallCount++;
|
|
1803
1840
|
}
|
|
1804
1841
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1842
|
+
// Pass 1.5: Pool (water plane) - render after opaque, before eyes
|
|
1843
|
+
if (this.pool) {
|
|
1844
|
+
this.pool.render(pass, {
|
|
1845
|
+
vertexBuffer: this.vertexBuffer,
|
|
1846
|
+
jointsBuffer: this.jointsBuffer,
|
|
1847
|
+
weightsBuffer: this.weightsBuffer,
|
|
1848
|
+
indexBuffer: this.indexBuffer,
|
|
1849
|
+
});
|
|
1850
|
+
this.drawCallCount++;
|
|
1810
1851
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
if (this.hairDrawsOverEyes.length > 0) {
|
|
1814
|
-
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1852
|
+
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1853
|
+
pass.setPipeline(this.eyePipeline);
|
|
1815
1854
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1816
|
-
for (const draw of this.
|
|
1855
|
+
for (const draw of this.eyeDraws) {
|
|
1817
1856
|
if (draw.count > 0) {
|
|
1818
1857
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1819
1858
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1820
1859
|
this.drawCallCount++;
|
|
1821
1860
|
}
|
|
1822
1861
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
pass
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1862
|
+
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1863
|
+
this.drawOutlines(pass, false);
|
|
1864
|
+
// 3a: Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1865
|
+
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
1866
|
+
pass.setPipeline(this.hairDepthPipeline);
|
|
1867
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1868
|
+
if (draw.count > 0) {
|
|
1869
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1870
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1874
|
+
if (draw.count > 0) {
|
|
1875
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1876
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1877
|
+
}
|
|
1832
1878
|
}
|
|
1833
1879
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1880
|
+
// 3b: Hair shading (split by stencil for transparency over eyes)
|
|
1881
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
1882
|
+
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1883
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1884
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1885
|
+
if (draw.count > 0) {
|
|
1886
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1887
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1888
|
+
this.drawCallCount++;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1893
|
+
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1894
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1895
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1896
|
+
if (draw.count > 0) {
|
|
1897
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1898
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1899
|
+
this.drawCallCount++;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
// 3c: Hair outlines
|
|
1904
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
1905
|
+
pass.setPipeline(this.hairOutlinePipeline);
|
|
1906
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1907
|
+
if (draw.count > 0) {
|
|
1908
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1909
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
// Pass 4: Transparent
|
|
1914
|
+
pass.setPipeline(this.modelPipeline);
|
|
1915
|
+
for (const draw of this.transparentDraws) {
|
|
1839
1916
|
if (draw.count > 0) {
|
|
1840
1917
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1841
1918
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1919
|
+
this.drawCallCount++;
|
|
1842
1920
|
}
|
|
1843
1921
|
}
|
|
1922
|
+
this.drawOutlines(pass, true);
|
|
1844
1923
|
}
|
|
1845
|
-
// Pass 4: Transparent
|
|
1846
|
-
pass.setPipeline(this.modelPipeline);
|
|
1847
|
-
for (const draw of this.transparentDraws) {
|
|
1848
|
-
if (draw.count > 0) {
|
|
1849
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1850
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1851
|
-
this.drawCallCount++;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
this.drawOutlines(pass, true);
|
|
1855
1924
|
pass.end();
|
|
1856
1925
|
this.device.queue.submit([encoder.finish()]);
|
|
1857
1926
|
this.applyBloom();
|