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