reze-engine 0.2.17 → 0.2.18
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 +0 -1
- package/dist/engine.d.ts +13 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +572 -721
- package/package.json +1 -1
- package/src/engine.ts +2235 -2392
package/dist/engine.js
CHANGED
|
@@ -18,10 +18,10 @@ export class Engine {
|
|
|
18
18
|
// Ambient light settings
|
|
19
19
|
this.ambientColor = new Vec3(1.0, 1.0, 1.0);
|
|
20
20
|
// Bloom settings
|
|
21
|
-
this.bloomThreshold =
|
|
22
|
-
this.bloomIntensity =
|
|
21
|
+
this.bloomThreshold = Engine.DEFAULT_BLOOM_THRESHOLD;
|
|
22
|
+
this.bloomIntensity = Engine.DEFAULT_BLOOM_INTENSITY;
|
|
23
23
|
// Rim light settings
|
|
24
|
-
this.rimLightIntensity =
|
|
24
|
+
this.rimLightIntensity = Engine.DEFAULT_RIM_LIGHT_INTENSITY;
|
|
25
25
|
this.currentModel = null;
|
|
26
26
|
this.modelDir = "";
|
|
27
27
|
this.physics = null;
|
|
@@ -38,10 +38,9 @@ export class Engine {
|
|
|
38
38
|
this.transparentOutlineDraws = [];
|
|
39
39
|
this.lastFpsUpdate = performance.now();
|
|
40
40
|
this.framesSinceLastUpdate = 0;
|
|
41
|
-
this.frameTimeSamples = [];
|
|
42
|
-
this.frameTimeSum = 0;
|
|
43
|
-
this.drawCallCount = 0;
|
|
44
41
|
this.lastFrameTime = performance.now();
|
|
42
|
+
this.frameTimeSum = 0;
|
|
43
|
+
this.frameTimeCount = 0;
|
|
45
44
|
this.stats = {
|
|
46
45
|
fps: 0,
|
|
47
46
|
frameTime: 0,
|
|
@@ -57,10 +56,10 @@ export class Engine {
|
|
|
57
56
|
this.canvas = canvas;
|
|
58
57
|
if (options) {
|
|
59
58
|
this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
|
|
60
|
-
this.bloomIntensity = options.bloomIntensity ??
|
|
61
|
-
this.rimLightIntensity = options.rimLightIntensity ??
|
|
62
|
-
this.cameraDistance = options.cameraDistance ??
|
|
63
|
-
this.cameraTarget = options.cameraTarget ??
|
|
59
|
+
this.bloomIntensity = options.bloomIntensity ?? Engine.DEFAULT_BLOOM_INTENSITY;
|
|
60
|
+
this.rimLightIntensity = options.rimLightIntensity ?? Engine.DEFAULT_RIM_LIGHT_INTENSITY;
|
|
61
|
+
this.cameraDistance = options.cameraDistance ?? Engine.DEFAULT_CAMERA_DISTANCE;
|
|
62
|
+
this.cameraTarget = options.cameraTarget ?? Engine.DEFAULT_CAMERA_TARGET;
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
// Step 1: Get WebGPU device and context
|
|
@@ -85,7 +84,6 @@ export class Engine {
|
|
|
85
84
|
this.setupCamera();
|
|
86
85
|
this.setupLighting();
|
|
87
86
|
this.createPipelines();
|
|
88
|
-
this.createFullscreenQuad();
|
|
89
87
|
this.createBloomPipelines();
|
|
90
88
|
this.setupResize();
|
|
91
89
|
}
|
|
@@ -98,101 +96,99 @@ export class Engine {
|
|
|
98
96
|
});
|
|
99
97
|
const shaderModule = this.device.createShaderModule({
|
|
100
98
|
label: "model shaders",
|
|
101
|
-
code: /* wgsl */ `
|
|
102
|
-
struct CameraUniforms {
|
|
103
|
-
view: mat4x4f,
|
|
104
|
-
projection: mat4x4f,
|
|
105
|
-
viewPos: vec3f,
|
|
106
|
-
_padding: f32,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
struct LightUniforms {
|
|
110
|
-
ambientColor: vec3f,
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
struct MaterialUniforms {
|
|
114
|
-
alpha: f32,
|
|
115
|
-
alphaMultiplier: f32,
|
|
116
|
-
rimIntensity: f32,
|
|
117
|
-
_padding1: f32,
|
|
118
|
-
rimColor: vec3f,
|
|
119
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
struct VertexOutput {
|
|
123
|
-
@builtin(position) position: vec4f,
|
|
124
|
-
@location(0) normal: vec3f,
|
|
125
|
-
@location(1) uv: vec2f,
|
|
126
|
-
@location(2) worldPos: vec3f,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
130
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
131
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
132
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
133
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
134
|
-
@group(0) @binding(5) var
|
|
135
|
-
|
|
136
|
-
@
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@location(
|
|
140
|
-
@location(
|
|
141
|
-
@location(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
let
|
|
157
|
-
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
output.
|
|
165
|
-
output.
|
|
166
|
-
output
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
let
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return vec4f(color, finalAlpha);
|
|
195
|
-
}
|
|
99
|
+
code: /* wgsl */ `
|
|
100
|
+
struct CameraUniforms {
|
|
101
|
+
view: mat4x4f,
|
|
102
|
+
projection: mat4x4f,
|
|
103
|
+
viewPos: vec3f,
|
|
104
|
+
_padding: f32,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
struct LightUniforms {
|
|
108
|
+
ambientColor: vec3f,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
struct MaterialUniforms {
|
|
112
|
+
alpha: f32,
|
|
113
|
+
alphaMultiplier: f32,
|
|
114
|
+
rimIntensity: f32,
|
|
115
|
+
_padding1: f32,
|
|
116
|
+
rimColor: vec3f,
|
|
117
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
struct VertexOutput {
|
|
121
|
+
@builtin(position) position: vec4f,
|
|
122
|
+
@location(0) normal: vec3f,
|
|
123
|
+
@location(1) uv: vec2f,
|
|
124
|
+
@location(2) worldPos: vec3f,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
128
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
129
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
130
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
131
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
132
|
+
@group(0) @binding(5) var<uniform> material: MaterialUniforms;
|
|
133
|
+
|
|
134
|
+
@vertex fn vs(
|
|
135
|
+
@location(0) position: vec3f,
|
|
136
|
+
@location(1) normal: vec3f,
|
|
137
|
+
@location(2) uv: vec2f,
|
|
138
|
+
@location(3) joints0: vec4<u32>,
|
|
139
|
+
@location(4) weights0: vec4<f32>
|
|
140
|
+
) -> VertexOutput {
|
|
141
|
+
var output: VertexOutput;
|
|
142
|
+
let pos4 = vec4f(position, 1.0);
|
|
143
|
+
|
|
144
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
145
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
146
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
147
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
148
|
+
|
|
149
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
150
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
151
|
+
for (var i = 0u; i < 4u; i++) {
|
|
152
|
+
let j = joints0[i];
|
|
153
|
+
let w = normalizedWeights[i];
|
|
154
|
+
let m = skinMats[j];
|
|
155
|
+
skinnedPos += (m * pos4) * w;
|
|
156
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
157
|
+
skinnedNrm += (r3 * normal) * w;
|
|
158
|
+
}
|
|
159
|
+
let worldPos = skinnedPos.xyz;
|
|
160
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
161
|
+
output.normal = normalize(skinnedNrm);
|
|
162
|
+
output.uv = uv;
|
|
163
|
+
output.worldPos = worldPos;
|
|
164
|
+
return output;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
168
|
+
// Early alpha test - discard before expensive calculations
|
|
169
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
170
|
+
if (material.isOverEyes > 0.5) {
|
|
171
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
172
|
+
}
|
|
173
|
+
if (finalAlpha < 0.001) {
|
|
174
|
+
discard;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let n = normalize(input.normal);
|
|
178
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
179
|
+
|
|
180
|
+
let lightAccum = light.ambientColor;
|
|
181
|
+
|
|
182
|
+
// Rim light calculation
|
|
183
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
184
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
185
|
+
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
186
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
187
|
+
|
|
188
|
+
let color = albedo * lightAccum + rimLight;
|
|
189
|
+
|
|
190
|
+
return vec4f(color, finalAlpha);
|
|
191
|
+
}
|
|
196
192
|
`,
|
|
197
193
|
});
|
|
198
194
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -204,9 +200,7 @@ export class Engine {
|
|
|
204
200
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // diffuseTexture
|
|
205
201
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // diffuseSampler
|
|
206
202
|
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
207
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT,
|
|
208
|
-
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // toonSampler
|
|
209
|
-
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
203
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
210
204
|
],
|
|
211
205
|
});
|
|
212
206
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -282,73 +276,73 @@ export class Engine {
|
|
|
282
276
|
});
|
|
283
277
|
const outlineShaderModule = this.device.createShaderModule({
|
|
284
278
|
label: "outline shaders",
|
|
285
|
-
code: /* wgsl */ `
|
|
286
|
-
struct CameraUniforms {
|
|
287
|
-
view: mat4x4f,
|
|
288
|
-
projection: mat4x4f,
|
|
289
|
-
viewPos: vec3f,
|
|
290
|
-
_padding: f32,
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
struct MaterialUniforms {
|
|
294
|
-
edgeColor: vec4f,
|
|
295
|
-
edgeSize: f32,
|
|
296
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
297
|
-
_padding1: f32,
|
|
298
|
-
_padding2: f32,
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
302
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
303
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
304
|
-
|
|
305
|
-
struct VertexOutput {
|
|
306
|
-
@builtin(position) position: vec4f,
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
@vertex fn vs(
|
|
310
|
-
@location(0) position: vec3f,
|
|
311
|
-
@location(1) normal: vec3f,
|
|
312
|
-
@location(3) joints0: vec4<u32>,
|
|
313
|
-
@location(4) weights0: vec4<f32>
|
|
314
|
-
) -> VertexOutput {
|
|
315
|
-
var output: VertexOutput;
|
|
316
|
-
let pos4 = vec4f(position, 1.0);
|
|
317
|
-
|
|
318
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
319
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
320
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
321
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
322
|
-
|
|
323
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
324
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
325
|
-
for (var i = 0u; i < 4u; i++) {
|
|
326
|
-
let j = joints0[i];
|
|
327
|
-
let w = normalizedWeights[i];
|
|
328
|
-
let m = skinMats[j];
|
|
329
|
-
skinnedPos += (m * pos4) * w;
|
|
330
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
331
|
-
skinnedNrm += (r3 * normal) * w;
|
|
332
|
-
}
|
|
333
|
-
let worldPos = skinnedPos.xyz;
|
|
334
|
-
let worldNormal = normalize(skinnedNrm);
|
|
335
|
-
|
|
336
|
-
// MMD invert hull: expand vertices outward along normals
|
|
337
|
-
let scaleFactor = 0.01;
|
|
338
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
339
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
340
|
-
return output;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
344
|
-
var color = material.edgeColor;
|
|
345
|
-
|
|
346
|
-
if (material.isOverEyes > 0.5) {
|
|
347
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return color;
|
|
351
|
-
}
|
|
279
|
+
code: /* wgsl */ `
|
|
280
|
+
struct CameraUniforms {
|
|
281
|
+
view: mat4x4f,
|
|
282
|
+
projection: mat4x4f,
|
|
283
|
+
viewPos: vec3f,
|
|
284
|
+
_padding: f32,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
struct MaterialUniforms {
|
|
288
|
+
edgeColor: vec4f,
|
|
289
|
+
edgeSize: f32,
|
|
290
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
291
|
+
_padding1: f32,
|
|
292
|
+
_padding2: f32,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
296
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
297
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
298
|
+
|
|
299
|
+
struct VertexOutput {
|
|
300
|
+
@builtin(position) position: vec4f,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
@vertex fn vs(
|
|
304
|
+
@location(0) position: vec3f,
|
|
305
|
+
@location(1) normal: vec3f,
|
|
306
|
+
@location(3) joints0: vec4<u32>,
|
|
307
|
+
@location(4) weights0: vec4<f32>
|
|
308
|
+
) -> VertexOutput {
|
|
309
|
+
var output: VertexOutput;
|
|
310
|
+
let pos4 = vec4f(position, 1.0);
|
|
311
|
+
|
|
312
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
313
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
314
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
315
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
316
|
+
|
|
317
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
318
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
319
|
+
for (var i = 0u; i < 4u; i++) {
|
|
320
|
+
let j = joints0[i];
|
|
321
|
+
let w = normalizedWeights[i];
|
|
322
|
+
let m = skinMats[j];
|
|
323
|
+
skinnedPos += (m * pos4) * w;
|
|
324
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
325
|
+
skinnedNrm += (r3 * normal) * w;
|
|
326
|
+
}
|
|
327
|
+
let worldPos = skinnedPos.xyz;
|
|
328
|
+
let worldNormal = normalize(skinnedNrm);
|
|
329
|
+
|
|
330
|
+
// MMD invert hull: expand vertices outward along normals
|
|
331
|
+
let scaleFactor = 0.01;
|
|
332
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
333
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
334
|
+
return output;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
338
|
+
var color = material.edgeColor;
|
|
339
|
+
|
|
340
|
+
if (material.isOverEyes > 0.5) {
|
|
341
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return color;
|
|
345
|
+
}
|
|
352
346
|
`,
|
|
353
347
|
});
|
|
354
348
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -552,45 +546,45 @@ export class Engine {
|
|
|
552
546
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
553
547
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
554
548
|
label: "depth only shader",
|
|
555
|
-
code: /* wgsl */ `
|
|
556
|
-
struct CameraUniforms {
|
|
557
|
-
view: mat4x4f,
|
|
558
|
-
projection: mat4x4f,
|
|
559
|
-
viewPos: vec3f,
|
|
560
|
-
_padding: f32,
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
564
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
565
|
-
|
|
566
|
-
@vertex fn vs(
|
|
567
|
-
@location(0) position: vec3f,
|
|
568
|
-
@location(1) normal: vec3f,
|
|
569
|
-
@location(3) joints0: vec4<u32>,
|
|
570
|
-
@location(4) weights0: vec4<f32>
|
|
571
|
-
) -> @builtin(position) vec4f {
|
|
572
|
-
let pos4 = vec4f(position, 1.0);
|
|
573
|
-
|
|
574
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
575
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
576
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
577
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
578
|
-
|
|
579
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
580
|
-
for (var i = 0u; i < 4u; i++) {
|
|
581
|
-
let j = joints0[i];
|
|
582
|
-
let w = normalizedWeights[i];
|
|
583
|
-
let m = skinMats[j];
|
|
584
|
-
skinnedPos += (m * pos4) * w;
|
|
585
|
-
}
|
|
586
|
-
let worldPos = skinnedPos.xyz;
|
|
587
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
588
|
-
return clipPos;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
592
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
593
|
-
}
|
|
549
|
+
code: /* wgsl */ `
|
|
550
|
+
struct CameraUniforms {
|
|
551
|
+
view: mat4x4f,
|
|
552
|
+
projection: mat4x4f,
|
|
553
|
+
viewPos: vec3f,
|
|
554
|
+
_padding: f32,
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
558
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
559
|
+
|
|
560
|
+
@vertex fn vs(
|
|
561
|
+
@location(0) position: vec3f,
|
|
562
|
+
@location(1) normal: vec3f,
|
|
563
|
+
@location(3) joints0: vec4<u32>,
|
|
564
|
+
@location(4) weights0: vec4<f32>
|
|
565
|
+
) -> @builtin(position) vec4f {
|
|
566
|
+
let pos4 = vec4f(position, 1.0);
|
|
567
|
+
|
|
568
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
569
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
570
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
571
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
572
|
+
|
|
573
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
574
|
+
for (var i = 0u; i < 4u; i++) {
|
|
575
|
+
let j = joints0[i];
|
|
576
|
+
let w = normalizedWeights[i];
|
|
577
|
+
let m = skinMats[j];
|
|
578
|
+
skinnedPos += (m * pos4) * w;
|
|
579
|
+
}
|
|
580
|
+
let worldPos = skinnedPos.xyz;
|
|
581
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
582
|
+
return clipPos;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
586
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
587
|
+
}
|
|
594
588
|
`,
|
|
595
589
|
});
|
|
596
590
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
@@ -638,165 +632,104 @@ export class Engine {
|
|
|
638
632
|
},
|
|
639
633
|
multisample: { count: this.sampleCount },
|
|
640
634
|
});
|
|
641
|
-
// Hair
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
{
|
|
657
|
-
arrayStride: 4 * 2,
|
|
658
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
arrayStride: 4,
|
|
662
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
663
|
-
},
|
|
664
|
-
],
|
|
665
|
-
},
|
|
666
|
-
fragment: {
|
|
667
|
-
module: shaderModule,
|
|
668
|
-
targets: [
|
|
669
|
-
{
|
|
670
|
-
format: this.presentationFormat,
|
|
671
|
-
blend: {
|
|
672
|
-
color: {
|
|
673
|
-
srcFactor: "src-alpha",
|
|
674
|
-
dstFactor: "one-minus-src-alpha",
|
|
675
|
-
operation: "add",
|
|
676
|
-
},
|
|
677
|
-
alpha: {
|
|
678
|
-
srcFactor: "one",
|
|
679
|
-
dstFactor: "one-minus-src-alpha",
|
|
680
|
-
operation: "add",
|
|
681
|
-
},
|
|
635
|
+
// Hair pipelines for rendering over eyes vs non-eyes (only differ in stencil compare mode)
|
|
636
|
+
const createHairPipeline = (isOverEyes) => {
|
|
637
|
+
return this.device.createRenderPipeline({
|
|
638
|
+
label: `hair pipeline (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
639
|
+
layout: mainPipelineLayout,
|
|
640
|
+
vertex: {
|
|
641
|
+
module: shaderModule,
|
|
642
|
+
buffers: [
|
|
643
|
+
{
|
|
644
|
+
arrayStride: 8 * 4,
|
|
645
|
+
attributes: [
|
|
646
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
647
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
648
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
649
|
+
],
|
|
682
650
|
},
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
693
|
-
failOp: "keep",
|
|
694
|
-
depthFailOp: "keep",
|
|
695
|
-
passOp: "keep",
|
|
696
|
-
},
|
|
697
|
-
stencilBack: {
|
|
698
|
-
compare: "equal",
|
|
699
|
-
failOp: "keep",
|
|
700
|
-
depthFailOp: "keep",
|
|
701
|
-
passOp: "keep",
|
|
651
|
+
{
|
|
652
|
+
arrayStride: 4 * 2,
|
|
653
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
arrayStride: 4,
|
|
657
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
658
|
+
},
|
|
659
|
+
],
|
|
702
660
|
},
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
],
|
|
720
|
-
},
|
|
721
|
-
{
|
|
722
|
-
arrayStride: 4 * 2,
|
|
723
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
724
|
-
},
|
|
725
|
-
{
|
|
726
|
-
arrayStride: 4,
|
|
727
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
728
|
-
},
|
|
729
|
-
],
|
|
730
|
-
},
|
|
731
|
-
fragment: {
|
|
732
|
-
module: shaderModule,
|
|
733
|
-
targets: [
|
|
734
|
-
{
|
|
735
|
-
format: this.presentationFormat,
|
|
736
|
-
blend: {
|
|
737
|
-
color: {
|
|
738
|
-
srcFactor: "src-alpha",
|
|
739
|
-
dstFactor: "one-minus-src-alpha",
|
|
740
|
-
operation: "add",
|
|
741
|
-
},
|
|
742
|
-
alpha: {
|
|
743
|
-
srcFactor: "one",
|
|
744
|
-
dstFactor: "one-minus-src-alpha",
|
|
745
|
-
operation: "add",
|
|
661
|
+
fragment: {
|
|
662
|
+
module: shaderModule,
|
|
663
|
+
targets: [
|
|
664
|
+
{
|
|
665
|
+
format: this.presentationFormat,
|
|
666
|
+
blend: {
|
|
667
|
+
color: {
|
|
668
|
+
srcFactor: "src-alpha",
|
|
669
|
+
dstFactor: "one-minus-src-alpha",
|
|
670
|
+
operation: "add",
|
|
671
|
+
},
|
|
672
|
+
alpha: {
|
|
673
|
+
srcFactor: "one",
|
|
674
|
+
dstFactor: "one-minus-src-alpha",
|
|
675
|
+
operation: "add",
|
|
676
|
+
},
|
|
746
677
|
},
|
|
747
678
|
},
|
|
748
|
-
|
|
749
|
-
],
|
|
750
|
-
},
|
|
751
|
-
primitive: { cullMode: "front" },
|
|
752
|
-
depthStencil: {
|
|
753
|
-
format: "depth24plus-stencil8",
|
|
754
|
-
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
755
|
-
depthCompare: "less-equal", // More lenient than "equal" to avoid precision issues with MSAA
|
|
756
|
-
stencilFront: {
|
|
757
|
-
compare: "not-equal", // Only render where stencil != 1 (over non-eyes)
|
|
758
|
-
failOp: "keep",
|
|
759
|
-
depthFailOp: "keep",
|
|
760
|
-
passOp: "keep",
|
|
679
|
+
],
|
|
761
680
|
},
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
681
|
+
primitive: { cullMode: "front" },
|
|
682
|
+
depthStencil: {
|
|
683
|
+
format: "depth24plus-stencil8",
|
|
684
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
685
|
+
depthCompare: "less-equal", // More lenient than "equal" to avoid precision issues with MSAA
|
|
686
|
+
stencilFront: {
|
|
687
|
+
compare: isOverEyes ? "equal" : "not-equal", // Over eyes: stencil == 1, over non-eyes: stencil != 1
|
|
688
|
+
failOp: "keep",
|
|
689
|
+
depthFailOp: "keep",
|
|
690
|
+
passOp: "keep",
|
|
691
|
+
},
|
|
692
|
+
stencilBack: {
|
|
693
|
+
compare: isOverEyes ? "equal" : "not-equal",
|
|
694
|
+
failOp: "keep",
|
|
695
|
+
depthFailOp: "keep",
|
|
696
|
+
passOp: "keep",
|
|
697
|
+
},
|
|
767
698
|
},
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
699
|
+
multisample: { count: this.sampleCount },
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
this.hairPipelineOverEyes = createHairPipeline(true);
|
|
703
|
+
this.hairPipelineOverNonEyes = createHairPipeline(false);
|
|
771
704
|
}
|
|
772
705
|
// Create compute shader for skin matrix computation
|
|
773
706
|
createSkinMatrixComputePipeline() {
|
|
774
707
|
const computeShader = this.device.createShaderModule({
|
|
775
708
|
label: "skin matrix compute",
|
|
776
|
-
code: /* wgsl */ `
|
|
777
|
-
struct BoneCountUniform {
|
|
778
|
-
count: u32,
|
|
779
|
-
_padding1: u32,
|
|
780
|
-
_padding2: u32,
|
|
781
|
-
_padding3: u32,
|
|
782
|
-
_padding4: vec4<u32>,
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
786
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
787
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
788
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
789
|
-
|
|
790
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
791
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
792
|
-
let boneIndex = globalId.x;
|
|
793
|
-
if (boneIndex >= boneCount.count) {
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
let worldMat = worldMatrices[boneIndex];
|
|
797
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
798
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
799
|
-
}
|
|
709
|
+
code: /* wgsl */ `
|
|
710
|
+
struct BoneCountUniform {
|
|
711
|
+
count: u32,
|
|
712
|
+
_padding1: u32,
|
|
713
|
+
_padding2: u32,
|
|
714
|
+
_padding3: u32,
|
|
715
|
+
_padding4: vec4<u32>,
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
719
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
720
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
721
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
722
|
+
|
|
723
|
+
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
724
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
725
|
+
let boneIndex = globalId.x;
|
|
726
|
+
if (boneIndex >= boneCount.count) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
let worldMat = worldMatrices[boneIndex];
|
|
730
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
731
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
732
|
+
}
|
|
800
733
|
`,
|
|
801
734
|
});
|
|
802
735
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -807,183 +740,145 @@ export class Engine {
|
|
|
807
740
|
},
|
|
808
741
|
});
|
|
809
742
|
}
|
|
810
|
-
// Create fullscreen quad for post-processing
|
|
811
|
-
createFullscreenQuad() {
|
|
812
|
-
// Fullscreen quad vertices: two triangles covering the entire screen - Format: position (x, y), uv (u, v)
|
|
813
|
-
const quadVertices = new Float32Array([
|
|
814
|
-
// Triangle 1
|
|
815
|
-
-1.0,
|
|
816
|
-
-1.0,
|
|
817
|
-
0.0,
|
|
818
|
-
0.0, // bottom-left
|
|
819
|
-
1.0,
|
|
820
|
-
-1.0,
|
|
821
|
-
1.0,
|
|
822
|
-
0.0, // bottom-right
|
|
823
|
-
-1.0,
|
|
824
|
-
1.0,
|
|
825
|
-
0.0,
|
|
826
|
-
1.0, // top-left
|
|
827
|
-
// Triangle 2
|
|
828
|
-
-1.0,
|
|
829
|
-
1.0,
|
|
830
|
-
0.0,
|
|
831
|
-
1.0, // top-left
|
|
832
|
-
1.0,
|
|
833
|
-
-1.0,
|
|
834
|
-
1.0,
|
|
835
|
-
0.0, // bottom-right
|
|
836
|
-
1.0,
|
|
837
|
-
1.0,
|
|
838
|
-
1.0,
|
|
839
|
-
1.0, // top-right
|
|
840
|
-
]);
|
|
841
|
-
this.fullscreenQuadBuffer = this.device.createBuffer({
|
|
842
|
-
label: "fullscreen quad",
|
|
843
|
-
size: quadVertices.byteLength,
|
|
844
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
845
|
-
});
|
|
846
|
-
this.device.queue.writeBuffer(this.fullscreenQuadBuffer, 0, quadVertices);
|
|
847
|
-
}
|
|
848
743
|
// Create bloom post-processing pipelines
|
|
849
744
|
createBloomPipelines() {
|
|
850
745
|
// Bloom extraction shader (extracts bright areas)
|
|
851
746
|
const bloomExtractShader = this.device.createShaderModule({
|
|
852
747
|
label: "bloom extract",
|
|
853
|
-
code: /* wgsl */ `
|
|
854
|
-
struct VertexOutput {
|
|
855
|
-
@builtin(position) position: vec4f,
|
|
856
|
-
@location(0) uv: vec2f,
|
|
857
|
-
};
|
|
858
|
-
|
|
859
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
860
|
-
var output: VertexOutput;
|
|
861
|
-
// Generate fullscreen quad from vertex index
|
|
862
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
863
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
864
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
865
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
866
|
-
return output;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
struct BloomExtractUniforms {
|
|
870
|
-
threshold: f32,
|
|
871
|
-
_padding1: f32,
|
|
872
|
-
_padding2: f32,
|
|
873
|
-
_padding3: f32,
|
|
874
|
-
_padding4: f32,
|
|
875
|
-
_padding5: f32,
|
|
876
|
-
_padding6: f32,
|
|
877
|
-
_padding7: f32,
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
881
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
882
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
883
|
-
|
|
884
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
885
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
886
|
-
// Extract bright areas above threshold
|
|
887
|
-
let threshold = extractUniforms.threshold;
|
|
888
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
889
|
-
return vec4f(bloom, color.a);
|
|
890
|
-
}
|
|
748
|
+
code: /* wgsl */ `
|
|
749
|
+
struct VertexOutput {
|
|
750
|
+
@builtin(position) position: vec4f,
|
|
751
|
+
@location(0) uv: vec2f,
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
755
|
+
var output: VertexOutput;
|
|
756
|
+
// Generate fullscreen quad from vertex index
|
|
757
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
758
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
759
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
760
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
761
|
+
return output;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
struct BloomExtractUniforms {
|
|
765
|
+
threshold: f32,
|
|
766
|
+
_padding1: f32,
|
|
767
|
+
_padding2: f32,
|
|
768
|
+
_padding3: f32,
|
|
769
|
+
_padding4: f32,
|
|
770
|
+
_padding5: f32,
|
|
771
|
+
_padding6: f32,
|
|
772
|
+
_padding7: f32,
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
776
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
777
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
778
|
+
|
|
779
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
780
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
781
|
+
// Extract bright areas above threshold
|
|
782
|
+
let threshold = extractUniforms.threshold;
|
|
783
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
784
|
+
return vec4f(bloom, color.a);
|
|
785
|
+
}
|
|
891
786
|
`,
|
|
892
787
|
});
|
|
893
788
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
894
789
|
const bloomBlurShader = this.device.createShaderModule({
|
|
895
790
|
label: "bloom blur",
|
|
896
|
-
code: /* wgsl */ `
|
|
897
|
-
struct VertexOutput {
|
|
898
|
-
@builtin(position) position: vec4f,
|
|
899
|
-
@location(0) uv: vec2f,
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
903
|
-
var output: VertexOutput;
|
|
904
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
905
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
906
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
907
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
908
|
-
return output;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
struct BlurUniforms {
|
|
912
|
-
direction: vec2f,
|
|
913
|
-
_padding1: f32,
|
|
914
|
-
_padding2: f32,
|
|
915
|
-
_padding3: f32,
|
|
916
|
-
_padding4: f32,
|
|
917
|
-
_padding5: f32,
|
|
918
|
-
_padding6: f32,
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
922
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
923
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
924
|
-
|
|
925
|
-
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
926
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
927
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
928
|
-
|
|
929
|
-
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
930
|
-
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
931
|
-
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
932
|
-
let weight0 = 0.38774; // Center sample
|
|
933
|
-
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
934
|
-
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
935
|
-
|
|
936
|
-
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
937
|
-
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
938
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
939
|
-
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
940
|
-
|
|
941
|
-
return result;
|
|
942
|
-
}
|
|
791
|
+
code: /* wgsl */ `
|
|
792
|
+
struct VertexOutput {
|
|
793
|
+
@builtin(position) position: vec4f,
|
|
794
|
+
@location(0) uv: vec2f,
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
798
|
+
var output: VertexOutput;
|
|
799
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
800
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
801
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
802
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
803
|
+
return output;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
struct BlurUniforms {
|
|
807
|
+
direction: vec2f,
|
|
808
|
+
_padding1: f32,
|
|
809
|
+
_padding2: f32,
|
|
810
|
+
_padding3: f32,
|
|
811
|
+
_padding4: f32,
|
|
812
|
+
_padding5: f32,
|
|
813
|
+
_padding6: f32,
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
817
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
818
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
819
|
+
|
|
820
|
+
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
821
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
822
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
823
|
+
|
|
824
|
+
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
825
|
+
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
826
|
+
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
827
|
+
let weight0 = 0.38774; // Center sample
|
|
828
|
+
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
829
|
+
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
830
|
+
|
|
831
|
+
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
832
|
+
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
833
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
834
|
+
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
835
|
+
|
|
836
|
+
return result;
|
|
837
|
+
}
|
|
943
838
|
`,
|
|
944
839
|
});
|
|
945
840
|
// Bloom composition shader (combines original scene with bloom)
|
|
946
841
|
const bloomComposeShader = this.device.createShaderModule({
|
|
947
842
|
label: "bloom compose",
|
|
948
|
-
code: /* wgsl */ `
|
|
949
|
-
struct VertexOutput {
|
|
950
|
-
@builtin(position) position: vec4f,
|
|
951
|
-
@location(0) uv: vec2f,
|
|
952
|
-
};
|
|
953
|
-
|
|
954
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
955
|
-
var output: VertexOutput;
|
|
956
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
957
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
958
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
959
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
960
|
-
return output;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
struct BloomComposeUniforms {
|
|
964
|
-
intensity: f32,
|
|
965
|
-
_padding1: f32,
|
|
966
|
-
_padding2: f32,
|
|
967
|
-
_padding3: f32,
|
|
968
|
-
_padding4: f32,
|
|
969
|
-
_padding5: f32,
|
|
970
|
-
_padding6: f32,
|
|
971
|
-
_padding7: f32,
|
|
972
|
-
};
|
|
973
|
-
|
|
974
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
975
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
976
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
977
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
978
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
979
|
-
|
|
980
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
981
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
982
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
983
|
-
// Additive blending with intensity control
|
|
984
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
985
|
-
return vec4f(result, scene.a);
|
|
986
|
-
}
|
|
843
|
+
code: /* wgsl */ `
|
|
844
|
+
struct VertexOutput {
|
|
845
|
+
@builtin(position) position: vec4f,
|
|
846
|
+
@location(0) uv: vec2f,
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
850
|
+
var output: VertexOutput;
|
|
851
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
852
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
853
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
854
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
855
|
+
return output;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
struct BloomComposeUniforms {
|
|
859
|
+
intensity: f32,
|
|
860
|
+
_padding1: f32,
|
|
861
|
+
_padding2: f32,
|
|
862
|
+
_padding3: f32,
|
|
863
|
+
_padding4: f32,
|
|
864
|
+
_padding5: f32,
|
|
865
|
+
_padding6: f32,
|
|
866
|
+
_padding7: f32,
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
870
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
871
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
872
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
873
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
874
|
+
|
|
875
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
876
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
877
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
878
|
+
// Additive blending with intensity control
|
|
879
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
880
|
+
return vec4f(result, scene.a);
|
|
881
|
+
}
|
|
987
882
|
`,
|
|
988
883
|
});
|
|
989
884
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1162,10 +1057,11 @@ export class Engine {
|
|
|
1162
1057
|
format: this.presentationFormat,
|
|
1163
1058
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1164
1059
|
});
|
|
1165
|
-
this.sceneRenderTextureView = this.sceneRenderTexture.createView();
|
|
1166
1060
|
// Setup bloom textures and bind groups
|
|
1167
1061
|
this.setupBloom(width, height);
|
|
1168
1062
|
const depthTextureView = this.depthTexture.createView();
|
|
1063
|
+
// Cache the scene render texture view (only recreate on resize)
|
|
1064
|
+
this.sceneRenderTextureView = this.sceneRenderTexture.createView();
|
|
1169
1065
|
// Render scene to texture instead of directly to canvas
|
|
1170
1066
|
const colorAttachment = this.sampleCount > 1
|
|
1171
1067
|
? {
|
|
@@ -1562,38 +1458,6 @@ export class Engine {
|
|
|
1562
1458
|
const texture = await this.createTextureFromPath(path);
|
|
1563
1459
|
return texture;
|
|
1564
1460
|
};
|
|
1565
|
-
const loadToonTexture = async (toonTextureIndex) => {
|
|
1566
|
-
const texture = await loadTextureByIndex(toonTextureIndex);
|
|
1567
|
-
if (texture)
|
|
1568
|
-
return texture;
|
|
1569
|
-
// Default toon texture fallback - cache it
|
|
1570
|
-
const defaultToonPath = "__default_toon__";
|
|
1571
|
-
const cached = this.textureCache.get(defaultToonPath);
|
|
1572
|
-
if (cached)
|
|
1573
|
-
return cached;
|
|
1574
|
-
const defaultToonData = new Uint8Array(256 * 2 * 4);
|
|
1575
|
-
for (let i = 0; i < 256; i++) {
|
|
1576
|
-
const factor = i / 255.0;
|
|
1577
|
-
const gray = Math.floor(128 + factor * 127);
|
|
1578
|
-
defaultToonData[i * 4] = gray;
|
|
1579
|
-
defaultToonData[i * 4 + 1] = gray;
|
|
1580
|
-
defaultToonData[i * 4 + 2] = gray;
|
|
1581
|
-
defaultToonData[i * 4 + 3] = 255;
|
|
1582
|
-
defaultToonData[(256 + i) * 4] = gray;
|
|
1583
|
-
defaultToonData[(256 + i) * 4 + 1] = gray;
|
|
1584
|
-
defaultToonData[(256 + i) * 4 + 2] = gray;
|
|
1585
|
-
defaultToonData[(256 + i) * 4 + 3] = 255;
|
|
1586
|
-
}
|
|
1587
|
-
const defaultToonTexture = this.device.createTexture({
|
|
1588
|
-
label: "default toon texture",
|
|
1589
|
-
size: [256, 2],
|
|
1590
|
-
format: "rgba8unorm",
|
|
1591
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
1592
|
-
});
|
|
1593
|
-
this.device.queue.writeTexture({ texture: defaultToonTexture }, defaultToonData, { bytesPerRow: 256 * 4 }, [256, 2]);
|
|
1594
|
-
this.textureCache.set(defaultToonPath, defaultToonTexture);
|
|
1595
|
-
return defaultToonTexture;
|
|
1596
|
-
};
|
|
1597
1461
|
this.opaqueDraws = [];
|
|
1598
1462
|
this.eyeDraws = [];
|
|
1599
1463
|
this.hairDrawsOverEyes = [];
|
|
@@ -1611,10 +1475,8 @@ export class Engine {
|
|
|
1611
1475
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex);
|
|
1612
1476
|
if (!diffuseTexture)
|
|
1613
1477
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1614
|
-
const toonTexture = await loadToonTexture(mat.toonTextureIndex);
|
|
1615
1478
|
const materialAlpha = mat.diffuse[3];
|
|
1616
|
-
const
|
|
1617
|
-
const isTransparent = materialAlpha < 1.0 - EPSILON;
|
|
1479
|
+
const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON;
|
|
1618
1480
|
// Create material uniform data
|
|
1619
1481
|
const materialUniformData = new Float32Array(8);
|
|
1620
1482
|
materialUniformData[0] = materialAlpha;
|
|
@@ -1641,18 +1503,17 @@ export class Engine {
|
|
|
1641
1503
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1642
1504
|
{ binding: 3, resource: this.materialSampler },
|
|
1643
1505
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1644
|
-
{ binding: 5, resource:
|
|
1645
|
-
{ binding: 6, resource: this.materialSampler },
|
|
1646
|
-
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
1506
|
+
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1647
1507
|
],
|
|
1648
1508
|
});
|
|
1649
1509
|
if (mat.isEye) {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1510
|
+
if (indexCount > 0) {
|
|
1511
|
+
this.eyeDraws.push({
|
|
1512
|
+
count: indexCount,
|
|
1513
|
+
firstIndex: currentIndexOffset,
|
|
1514
|
+
bindGroup,
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1656
1517
|
}
|
|
1657
1518
|
else if (mat.isHair) {
|
|
1658
1519
|
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
@@ -1681,42 +1542,42 @@ export class Engine {
|
|
|
1681
1542
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1682
1543
|
{ binding: 3, resource: this.materialSampler },
|
|
1683
1544
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1684
|
-
{ binding: 5, resource:
|
|
1685
|
-
{ binding: 6, resource: this.materialSampler },
|
|
1686
|
-
{ binding: 7, resource: { buffer: buffer } },
|
|
1545
|
+
{ binding: 5, resource: { buffer: buffer } },
|
|
1687
1546
|
],
|
|
1688
1547
|
});
|
|
1689
1548
|
};
|
|
1690
1549
|
const bindGroupOverEyes = createHairBindGroup(true);
|
|
1691
1550
|
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
}
|
|
1551
|
+
if (indexCount > 0) {
|
|
1552
|
+
this.hairDrawsOverEyes.push({
|
|
1553
|
+
count: indexCount,
|
|
1554
|
+
firstIndex: currentIndexOffset,
|
|
1555
|
+
bindGroup: bindGroupOverEyes,
|
|
1556
|
+
});
|
|
1557
|
+
this.hairDrawsOverNonEyes.push({
|
|
1558
|
+
count: indexCount,
|
|
1559
|
+
firstIndex: currentIndexOffset,
|
|
1560
|
+
bindGroup: bindGroupOverNonEyes,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1704
1563
|
}
|
|
1705
1564
|
else if (isTransparent) {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1565
|
+
if (indexCount > 0) {
|
|
1566
|
+
this.transparentDraws.push({
|
|
1567
|
+
count: indexCount,
|
|
1568
|
+
firstIndex: currentIndexOffset,
|
|
1569
|
+
bindGroup,
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1712
1572
|
}
|
|
1713
1573
|
else {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1574
|
+
if (indexCount > 0) {
|
|
1575
|
+
this.opaqueDraws.push({
|
|
1576
|
+
count: indexCount,
|
|
1577
|
+
firstIndex: currentIndexOffset,
|
|
1578
|
+
bindGroup,
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1720
1581
|
}
|
|
1721
1582
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1722
1583
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
@@ -1744,37 +1605,35 @@ export class Engine {
|
|
|
1744
1605
|
{ binding: 2, resource: { buffer: this.skinMatrixBuffer } },
|
|
1745
1606
|
],
|
|
1746
1607
|
});
|
|
1747
|
-
if (
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
isTransparent,
|
|
1777
|
-
});
|
|
1608
|
+
if (indexCount > 0) {
|
|
1609
|
+
if (mat.isEye) {
|
|
1610
|
+
this.eyeOutlineDraws.push({
|
|
1611
|
+
count: indexCount,
|
|
1612
|
+
firstIndex: currentIndexOffset,
|
|
1613
|
+
bindGroup: outlineBindGroup,
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
else if (mat.isHair) {
|
|
1617
|
+
this.hairOutlineDraws.push({
|
|
1618
|
+
count: indexCount,
|
|
1619
|
+
firstIndex: currentIndexOffset,
|
|
1620
|
+
bindGroup: outlineBindGroup,
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
else if (isTransparent) {
|
|
1624
|
+
this.transparentOutlineDraws.push({
|
|
1625
|
+
count: indexCount,
|
|
1626
|
+
firstIndex: currentIndexOffset,
|
|
1627
|
+
bindGroup: outlineBindGroup,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
else {
|
|
1631
|
+
this.opaqueOutlineDraws.push({
|
|
1632
|
+
count: indexCount,
|
|
1633
|
+
firstIndex: currentIndexOffset,
|
|
1634
|
+
bindGroup: outlineBindGroup,
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1778
1637
|
}
|
|
1779
1638
|
}
|
|
1780
1639
|
currentIndexOffset += indexCount;
|
|
@@ -1811,6 +1670,56 @@ export class Engine {
|
|
|
1811
1670
|
return null;
|
|
1812
1671
|
}
|
|
1813
1672
|
}
|
|
1673
|
+
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1674
|
+
renderEyes(pass) {
|
|
1675
|
+
pass.setPipeline(this.eyePipeline);
|
|
1676
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1677
|
+
for (const draw of this.eyeDraws) {
|
|
1678
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1679
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1683
|
+
renderHair(pass) {
|
|
1684
|
+
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1685
|
+
const hasHair = this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0;
|
|
1686
|
+
if (hasHair) {
|
|
1687
|
+
pass.setPipeline(this.hairDepthPipeline);
|
|
1688
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1689
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1690
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1691
|
+
}
|
|
1692
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1693
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1694
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
// Hair shading (split by stencil for transparency over eyes)
|
|
1698
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
1699
|
+
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1700
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1701
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1702
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1703
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1707
|
+
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1708
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1709
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1710
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1711
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
// Hair outlines
|
|
1715
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
1716
|
+
pass.setPipeline(this.hairOutlinePipeline);
|
|
1717
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1718
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1719
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1814
1723
|
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
|
|
1815
1724
|
render() {
|
|
1816
1725
|
if (this.multisampleTexture && this.camera && this.device) {
|
|
@@ -1830,7 +1739,6 @@ export class Engine {
|
|
|
1830
1739
|
return;
|
|
1831
1740
|
}
|
|
1832
1741
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1833
|
-
this.drawCallCount = 0;
|
|
1834
1742
|
if (this.currentModel) {
|
|
1835
1743
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1836
1744
|
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
@@ -1839,81 +1747,19 @@ export class Engine {
|
|
|
1839
1747
|
// Pass 1: Opaque
|
|
1840
1748
|
pass.setPipeline(this.modelPipeline);
|
|
1841
1749
|
for (const draw of this.opaqueDraws) {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1845
|
-
this.drawCallCount++;
|
|
1846
|
-
}
|
|
1750
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1751
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1847
1752
|
}
|
|
1848
1753
|
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1849
|
-
|
|
1850
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1851
|
-
for (const draw of this.eyeDraws) {
|
|
1852
|
-
if (draw.count > 0) {
|
|
1853
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1854
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1855
|
-
this.drawCallCount++;
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1754
|
+
this.renderEyes(pass);
|
|
1859
1755
|
this.drawOutlines(pass, false);
|
|
1860
|
-
//
|
|
1861
|
-
|
|
1862
|
-
pass.setPipeline(this.hairDepthPipeline);
|
|
1863
|
-
for (const draw of this.hairDrawsOverEyes) {
|
|
1864
|
-
if (draw.count > 0) {
|
|
1865
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1866
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1870
|
-
if (draw.count > 0) {
|
|
1871
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1872
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
// 3b: Hair shading (split by stencil for transparency over eyes)
|
|
1877
|
-
if (this.hairDrawsOverEyes.length > 0) {
|
|
1878
|
-
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1879
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1880
|
-
for (const draw of this.hairDrawsOverEyes) {
|
|
1881
|
-
if (draw.count > 0) {
|
|
1882
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1883
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1884
|
-
this.drawCallCount++;
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1889
|
-
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1890
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1891
|
-
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1892
|
-
if (draw.count > 0) {
|
|
1893
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1894
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1895
|
-
this.drawCallCount++;
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
// 3c: Hair outlines
|
|
1900
|
-
if (this.hairOutlineDraws.length > 0) {
|
|
1901
|
-
pass.setPipeline(this.hairOutlinePipeline);
|
|
1902
|
-
for (const draw of this.hairOutlineDraws) {
|
|
1903
|
-
if (draw.count > 0) {
|
|
1904
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1905
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1756
|
+
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1757
|
+
this.renderHair(pass);
|
|
1909
1758
|
// Pass 4: Transparent
|
|
1910
1759
|
pass.setPipeline(this.modelPipeline);
|
|
1911
1760
|
for (const draw of this.transparentDraws) {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1915
|
-
this.drawCallCount++;
|
|
1916
|
-
}
|
|
1761
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1762
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1917
1763
|
}
|
|
1918
1764
|
this.drawOutlines(pass, true);
|
|
1919
1765
|
}
|
|
@@ -2021,6 +1867,7 @@ export class Engine {
|
|
|
2021
1867
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
2022
1868
|
}
|
|
2023
1869
|
updateRenderTarget() {
|
|
1870
|
+
// Use cached view (only recreated on resize in handleResize)
|
|
2024
1871
|
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
2025
1872
|
if (this.sampleCount > 1) {
|
|
2026
1873
|
colorAttachment.resolveTarget = this.sceneRenderTextureView;
|
|
@@ -2049,40 +1896,44 @@ export class Engine {
|
|
|
2049
1896
|
}
|
|
2050
1897
|
drawOutlines(pass, transparent) {
|
|
2051
1898
|
pass.setPipeline(this.outlinePipeline);
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
else {
|
|
2061
|
-
for (const draw of this.opaqueOutlineDraws) {
|
|
2062
|
-
if (draw.count > 0) {
|
|
2063
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
2064
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
1899
|
+
const draws = transparent ? this.transparentOutlineDraws : this.opaqueOutlineDraws;
|
|
1900
|
+
for (const draw of draws) {
|
|
1901
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1902
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2067
1903
|
}
|
|
2068
1904
|
}
|
|
2069
1905
|
updateStats(frameTime) {
|
|
1906
|
+
// Simplified frame time tracking - rolling average with fixed window
|
|
2070
1907
|
const maxSamples = 60;
|
|
2071
|
-
this.frameTimeSamples.push(frameTime);
|
|
2072
1908
|
this.frameTimeSum += frameTime;
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
1909
|
+
this.frameTimeCount++;
|
|
1910
|
+
if (this.frameTimeCount > maxSamples) {
|
|
1911
|
+
// Maintain rolling window by subtracting oldest sample estimate
|
|
1912
|
+
const avg = this.frameTimeSum / maxSamples;
|
|
1913
|
+
this.frameTimeSum -= avg;
|
|
1914
|
+
this.frameTimeCount = maxSamples;
|
|
2076
1915
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
1916
|
+
this.stats.frameTime =
|
|
1917
|
+
Math.round((this.frameTimeSum / this.frameTimeCount) * Engine.STATS_FRAME_TIME_ROUNDING) /
|
|
1918
|
+
Engine.STATS_FRAME_TIME_ROUNDING;
|
|
1919
|
+
// FPS tracking
|
|
2079
1920
|
const now = performance.now();
|
|
2080
1921
|
this.framesSinceLastUpdate++;
|
|
2081
1922
|
const elapsed = now - this.lastFpsUpdate;
|
|
2082
|
-
if (elapsed >=
|
|
2083
|
-
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) *
|
|
1923
|
+
if (elapsed >= Engine.STATS_FPS_UPDATE_INTERVAL_MS) {
|
|
1924
|
+
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * Engine.STATS_FPS_UPDATE_INTERVAL_MS);
|
|
2084
1925
|
this.framesSinceLastUpdate = 0;
|
|
2085
1926
|
this.lastFpsUpdate = now;
|
|
2086
1927
|
}
|
|
2087
1928
|
}
|
|
2088
1929
|
}
|
|
1930
|
+
// Default values
|
|
1931
|
+
Engine.DEFAULT_BLOOM_THRESHOLD = 0.01;
|
|
1932
|
+
Engine.DEFAULT_BLOOM_INTENSITY = 0.12;
|
|
1933
|
+
Engine.DEFAULT_RIM_LIGHT_INTENSITY = 0.45;
|
|
1934
|
+
Engine.DEFAULT_CAMERA_DISTANCE = 26.6;
|
|
1935
|
+
Engine.DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0);
|
|
1936
|
+
Engine.HAIR_OVER_EYES_ALPHA = 0.5;
|
|
1937
|
+
Engine.TRANSPARENCY_EPSILON = 0.001;
|
|
1938
|
+
Engine.STATS_FPS_UPDATE_INTERVAL_MS = 1000;
|
|
1939
|
+
Engine.STATS_FRAME_TIME_ROUNDING = 100;
|