reze-engine 0.2.16 → 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 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +572 -814
- package/package.json +1 -1
- package/src/engine.ts +2235 -2487
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,20 +38,17 @@ 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,
|
|
48
|
-
gpuMemory: 0,
|
|
49
47
|
};
|
|
50
48
|
this.animationFrameId = null;
|
|
51
49
|
this.renderLoopCallback = null;
|
|
52
50
|
this.animationFrames = [];
|
|
53
51
|
this.animationTimeouts = [];
|
|
54
|
-
this.gpuMemoryMB = 0;
|
|
55
52
|
this.hasAnimation = false; // Set to true when loadAnimation is called
|
|
56
53
|
this.playingAnimation = false; // Set to true when playAnimation is called
|
|
57
54
|
this.breathingTimeout = null;
|
|
@@ -59,10 +56,10 @@ export class Engine {
|
|
|
59
56
|
this.canvas = canvas;
|
|
60
57
|
if (options) {
|
|
61
58
|
this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
|
|
62
|
-
this.bloomIntensity = options.bloomIntensity ??
|
|
63
|
-
this.rimLightIntensity = options.rimLightIntensity ??
|
|
64
|
-
this.cameraDistance = options.cameraDistance ??
|
|
65
|
-
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;
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
65
|
// Step 1: Get WebGPU device and context
|
|
@@ -87,7 +84,6 @@ export class Engine {
|
|
|
87
84
|
this.setupCamera();
|
|
88
85
|
this.setupLighting();
|
|
89
86
|
this.createPipelines();
|
|
90
|
-
this.createFullscreenQuad();
|
|
91
87
|
this.createBloomPipelines();
|
|
92
88
|
this.setupResize();
|
|
93
89
|
}
|
|
@@ -100,101 +96,99 @@ export class Engine {
|
|
|
100
96
|
});
|
|
101
97
|
const shaderModule = this.device.createShaderModule({
|
|
102
98
|
label: "model shaders",
|
|
103
|
-
code: /* wgsl */ `
|
|
104
|
-
struct CameraUniforms {
|
|
105
|
-
view: mat4x4f,
|
|
106
|
-
projection: mat4x4f,
|
|
107
|
-
viewPos: vec3f,
|
|
108
|
-
_padding: f32,
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
struct LightUniforms {
|
|
112
|
-
ambientColor: vec3f,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
struct MaterialUniforms {
|
|
116
|
-
alpha: f32,
|
|
117
|
-
alphaMultiplier: f32,
|
|
118
|
-
rimIntensity: f32,
|
|
119
|
-
_padding1: f32,
|
|
120
|
-
rimColor: vec3f,
|
|
121
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
struct VertexOutput {
|
|
125
|
-
@builtin(position) position: vec4f,
|
|
126
|
-
@location(0) normal: vec3f,
|
|
127
|
-
@location(1) uv: vec2f,
|
|
128
|
-
@location(2) worldPos: vec3f,
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
132
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
133
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
134
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
135
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
136
|
-
@group(0) @binding(5) var
|
|
137
|
-
|
|
138
|
-
@
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
@location(
|
|
142
|
-
@location(
|
|
143
|
-
@location(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
var
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
let
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
output.
|
|
167
|
-
output.
|
|
168
|
-
output
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return vec4f(color, finalAlpha);
|
|
197
|
-
}
|
|
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
|
+
}
|
|
198
192
|
`,
|
|
199
193
|
});
|
|
200
194
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -206,9 +200,7 @@ export class Engine {
|
|
|
206
200
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // diffuseTexture
|
|
207
201
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // diffuseSampler
|
|
208
202
|
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
209
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT,
|
|
210
|
-
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // toonSampler
|
|
211
|
-
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
203
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
212
204
|
],
|
|
213
205
|
});
|
|
214
206
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -284,73 +276,73 @@ export class Engine {
|
|
|
284
276
|
});
|
|
285
277
|
const outlineShaderModule = this.device.createShaderModule({
|
|
286
278
|
label: "outline shaders",
|
|
287
|
-
code: /* wgsl */ `
|
|
288
|
-
struct CameraUniforms {
|
|
289
|
-
view: mat4x4f,
|
|
290
|
-
projection: mat4x4f,
|
|
291
|
-
viewPos: vec3f,
|
|
292
|
-
_padding: f32,
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
struct MaterialUniforms {
|
|
296
|
-
edgeColor: vec4f,
|
|
297
|
-
edgeSize: f32,
|
|
298
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
299
|
-
_padding1: f32,
|
|
300
|
-
_padding2: f32,
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
304
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
305
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
306
|
-
|
|
307
|
-
struct VertexOutput {
|
|
308
|
-
@builtin(position) position: vec4f,
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
@vertex fn vs(
|
|
312
|
-
@location(0) position: vec3f,
|
|
313
|
-
@location(1) normal: vec3f,
|
|
314
|
-
@location(3) joints0: vec4<u32>,
|
|
315
|
-
@location(4) weights0: vec4<f32>
|
|
316
|
-
) -> VertexOutput {
|
|
317
|
-
var output: VertexOutput;
|
|
318
|
-
let pos4 = vec4f(position, 1.0);
|
|
319
|
-
|
|
320
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
321
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
322
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
323
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
324
|
-
|
|
325
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
326
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
327
|
-
for (var i = 0u; i < 4u; i++) {
|
|
328
|
-
let j = joints0[i];
|
|
329
|
-
let w = normalizedWeights[i];
|
|
330
|
-
let m = skinMats[j];
|
|
331
|
-
skinnedPos += (m * pos4) * w;
|
|
332
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
333
|
-
skinnedNrm += (r3 * normal) * w;
|
|
334
|
-
}
|
|
335
|
-
let worldPos = skinnedPos.xyz;
|
|
336
|
-
let worldNormal = normalize(skinnedNrm);
|
|
337
|
-
|
|
338
|
-
// MMD invert hull: expand vertices outward along normals
|
|
339
|
-
let scaleFactor = 0.01;
|
|
340
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
341
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
342
|
-
return output;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
346
|
-
var color = material.edgeColor;
|
|
347
|
-
|
|
348
|
-
if (material.isOverEyes > 0.5) {
|
|
349
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return color;
|
|
353
|
-
}
|
|
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
|
+
}
|
|
354
346
|
`,
|
|
355
347
|
});
|
|
356
348
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -554,45 +546,45 @@ export class Engine {
|
|
|
554
546
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
555
547
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
556
548
|
label: "depth only shader",
|
|
557
|
-
code: /* wgsl */ `
|
|
558
|
-
struct CameraUniforms {
|
|
559
|
-
view: mat4x4f,
|
|
560
|
-
projection: mat4x4f,
|
|
561
|
-
viewPos: vec3f,
|
|
562
|
-
_padding: f32,
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
566
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
567
|
-
|
|
568
|
-
@vertex fn vs(
|
|
569
|
-
@location(0) position: vec3f,
|
|
570
|
-
@location(1) normal: vec3f,
|
|
571
|
-
@location(3) joints0: vec4<u32>,
|
|
572
|
-
@location(4) weights0: vec4<f32>
|
|
573
|
-
) -> @builtin(position) vec4f {
|
|
574
|
-
let pos4 = vec4f(position, 1.0);
|
|
575
|
-
|
|
576
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
577
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
578
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
579
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
580
|
-
|
|
581
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
582
|
-
for (var i = 0u; i < 4u; i++) {
|
|
583
|
-
let j = joints0[i];
|
|
584
|
-
let w = normalizedWeights[i];
|
|
585
|
-
let m = skinMats[j];
|
|
586
|
-
skinnedPos += (m * pos4) * w;
|
|
587
|
-
}
|
|
588
|
-
let worldPos = skinnedPos.xyz;
|
|
589
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
590
|
-
return clipPos;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
594
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
595
|
-
}
|
|
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
|
+
}
|
|
596
588
|
`,
|
|
597
589
|
});
|
|
598
590
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
@@ -640,165 +632,104 @@ export class Engine {
|
|
|
640
632
|
},
|
|
641
633
|
multisample: { count: this.sampleCount },
|
|
642
634
|
});
|
|
643
|
-
// Hair
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
{
|
|
659
|
-
arrayStride: 4 * 2,
|
|
660
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
arrayStride: 4,
|
|
664
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
665
|
-
},
|
|
666
|
-
],
|
|
667
|
-
},
|
|
668
|
-
fragment: {
|
|
669
|
-
module: shaderModule,
|
|
670
|
-
targets: [
|
|
671
|
-
{
|
|
672
|
-
format: this.presentationFormat,
|
|
673
|
-
blend: {
|
|
674
|
-
color: {
|
|
675
|
-
srcFactor: "src-alpha",
|
|
676
|
-
dstFactor: "one-minus-src-alpha",
|
|
677
|
-
operation: "add",
|
|
678
|
-
},
|
|
679
|
-
alpha: {
|
|
680
|
-
srcFactor: "one",
|
|
681
|
-
dstFactor: "one-minus-src-alpha",
|
|
682
|
-
operation: "add",
|
|
683
|
-
},
|
|
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
|
+
],
|
|
684
650
|
},
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
695
|
-
failOp: "keep",
|
|
696
|
-
depthFailOp: "keep",
|
|
697
|
-
passOp: "keep",
|
|
698
|
-
},
|
|
699
|
-
stencilBack: {
|
|
700
|
-
compare: "equal",
|
|
701
|
-
failOp: "keep",
|
|
702
|
-
depthFailOp: "keep",
|
|
703
|
-
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
|
+
],
|
|
704
660
|
},
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
],
|
|
722
|
-
},
|
|
723
|
-
{
|
|
724
|
-
arrayStride: 4 * 2,
|
|
725
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
arrayStride: 4,
|
|
729
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
730
|
-
},
|
|
731
|
-
],
|
|
732
|
-
},
|
|
733
|
-
fragment: {
|
|
734
|
-
module: shaderModule,
|
|
735
|
-
targets: [
|
|
736
|
-
{
|
|
737
|
-
format: this.presentationFormat,
|
|
738
|
-
blend: {
|
|
739
|
-
color: {
|
|
740
|
-
srcFactor: "src-alpha",
|
|
741
|
-
dstFactor: "one-minus-src-alpha",
|
|
742
|
-
operation: "add",
|
|
743
|
-
},
|
|
744
|
-
alpha: {
|
|
745
|
-
srcFactor: "one",
|
|
746
|
-
dstFactor: "one-minus-src-alpha",
|
|
747
|
-
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
|
+
},
|
|
748
677
|
},
|
|
749
678
|
},
|
|
750
|
-
|
|
751
|
-
],
|
|
752
|
-
},
|
|
753
|
-
primitive: { cullMode: "front" },
|
|
754
|
-
depthStencil: {
|
|
755
|
-
format: "depth24plus-stencil8",
|
|
756
|
-
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
757
|
-
depthCompare: "less-equal", // More lenient than "equal" to avoid precision issues with MSAA
|
|
758
|
-
stencilFront: {
|
|
759
|
-
compare: "not-equal", // Only render where stencil != 1 (over non-eyes)
|
|
760
|
-
failOp: "keep",
|
|
761
|
-
depthFailOp: "keep",
|
|
762
|
-
passOp: "keep",
|
|
679
|
+
],
|
|
763
680
|
},
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
+
},
|
|
769
698
|
},
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
699
|
+
multisample: { count: this.sampleCount },
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
this.hairPipelineOverEyes = createHairPipeline(true);
|
|
703
|
+
this.hairPipelineOverNonEyes = createHairPipeline(false);
|
|
773
704
|
}
|
|
774
705
|
// Create compute shader for skin matrix computation
|
|
775
706
|
createSkinMatrixComputePipeline() {
|
|
776
707
|
const computeShader = this.device.createShaderModule({
|
|
777
708
|
label: "skin matrix compute",
|
|
778
|
-
code: /* wgsl */ `
|
|
779
|
-
struct BoneCountUniform {
|
|
780
|
-
count: u32,
|
|
781
|
-
_padding1: u32,
|
|
782
|
-
_padding2: u32,
|
|
783
|
-
_padding3: u32,
|
|
784
|
-
_padding4: vec4<u32>,
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
788
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
789
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
790
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
791
|
-
|
|
792
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
793
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
794
|
-
let boneIndex = globalId.x;
|
|
795
|
-
if (boneIndex >= boneCount.count) {
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
let worldMat = worldMatrices[boneIndex];
|
|
799
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
800
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
801
|
-
}
|
|
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
|
+
}
|
|
802
733
|
`,
|
|
803
734
|
});
|
|
804
735
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -809,183 +740,145 @@ export class Engine {
|
|
|
809
740
|
},
|
|
810
741
|
});
|
|
811
742
|
}
|
|
812
|
-
// Create fullscreen quad for post-processing
|
|
813
|
-
createFullscreenQuad() {
|
|
814
|
-
// Fullscreen quad vertices: two triangles covering the entire screen - Format: position (x, y), uv (u, v)
|
|
815
|
-
const quadVertices = new Float32Array([
|
|
816
|
-
// Triangle 1
|
|
817
|
-
-1.0,
|
|
818
|
-
-1.0,
|
|
819
|
-
0.0,
|
|
820
|
-
0.0, // bottom-left
|
|
821
|
-
1.0,
|
|
822
|
-
-1.0,
|
|
823
|
-
1.0,
|
|
824
|
-
0.0, // bottom-right
|
|
825
|
-
-1.0,
|
|
826
|
-
1.0,
|
|
827
|
-
0.0,
|
|
828
|
-
1.0, // top-left
|
|
829
|
-
// Triangle 2
|
|
830
|
-
-1.0,
|
|
831
|
-
1.0,
|
|
832
|
-
0.0,
|
|
833
|
-
1.0, // top-left
|
|
834
|
-
1.0,
|
|
835
|
-
-1.0,
|
|
836
|
-
1.0,
|
|
837
|
-
0.0, // bottom-right
|
|
838
|
-
1.0,
|
|
839
|
-
1.0,
|
|
840
|
-
1.0,
|
|
841
|
-
1.0, // top-right
|
|
842
|
-
]);
|
|
843
|
-
this.fullscreenQuadBuffer = this.device.createBuffer({
|
|
844
|
-
label: "fullscreen quad",
|
|
845
|
-
size: quadVertices.byteLength,
|
|
846
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
847
|
-
});
|
|
848
|
-
this.device.queue.writeBuffer(this.fullscreenQuadBuffer, 0, quadVertices);
|
|
849
|
-
}
|
|
850
743
|
// Create bloom post-processing pipelines
|
|
851
744
|
createBloomPipelines() {
|
|
852
745
|
// Bloom extraction shader (extracts bright areas)
|
|
853
746
|
const bloomExtractShader = this.device.createShaderModule({
|
|
854
747
|
label: "bloom extract",
|
|
855
|
-
code: /* wgsl */ `
|
|
856
|
-
struct VertexOutput {
|
|
857
|
-
@builtin(position) position: vec4f,
|
|
858
|
-
@location(0) uv: vec2f,
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
862
|
-
var output: VertexOutput;
|
|
863
|
-
// Generate fullscreen quad from vertex index
|
|
864
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
865
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
866
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
867
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
868
|
-
return output;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
struct BloomExtractUniforms {
|
|
872
|
-
threshold: f32,
|
|
873
|
-
_padding1: f32,
|
|
874
|
-
_padding2: f32,
|
|
875
|
-
_padding3: f32,
|
|
876
|
-
_padding4: f32,
|
|
877
|
-
_padding5: f32,
|
|
878
|
-
_padding6: f32,
|
|
879
|
-
_padding7: f32,
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
883
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
884
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
885
|
-
|
|
886
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
887
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
888
|
-
// Extract bright areas above threshold
|
|
889
|
-
let threshold = extractUniforms.threshold;
|
|
890
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
891
|
-
return vec4f(bloom, color.a);
|
|
892
|
-
}
|
|
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
|
+
}
|
|
893
786
|
`,
|
|
894
787
|
});
|
|
895
788
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
896
789
|
const bloomBlurShader = this.device.createShaderModule({
|
|
897
790
|
label: "bloom blur",
|
|
898
|
-
code: /* wgsl */ `
|
|
899
|
-
struct VertexOutput {
|
|
900
|
-
@builtin(position) position: vec4f,
|
|
901
|
-
@location(0) uv: vec2f,
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
905
|
-
var output: VertexOutput;
|
|
906
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
907
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
908
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
909
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
910
|
-
return output;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
struct BlurUniforms {
|
|
914
|
-
direction: vec2f,
|
|
915
|
-
_padding1: f32,
|
|
916
|
-
_padding2: f32,
|
|
917
|
-
_padding3: f32,
|
|
918
|
-
_padding4: f32,
|
|
919
|
-
_padding5: f32,
|
|
920
|
-
_padding6: f32,
|
|
921
|
-
};
|
|
922
|
-
|
|
923
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
924
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
925
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
926
|
-
|
|
927
|
-
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
928
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
929
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
930
|
-
|
|
931
|
-
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
932
|
-
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
933
|
-
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
934
|
-
let weight0 = 0.38774; // Center sample
|
|
935
|
-
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
936
|
-
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
937
|
-
|
|
938
|
-
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
939
|
-
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
940
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
941
|
-
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
942
|
-
|
|
943
|
-
return result;
|
|
944
|
-
}
|
|
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
|
+
}
|
|
945
838
|
`,
|
|
946
839
|
});
|
|
947
840
|
// Bloom composition shader (combines original scene with bloom)
|
|
948
841
|
const bloomComposeShader = this.device.createShaderModule({
|
|
949
842
|
label: "bloom compose",
|
|
950
|
-
code: /* wgsl */ `
|
|
951
|
-
struct VertexOutput {
|
|
952
|
-
@builtin(position) position: vec4f,
|
|
953
|
-
@location(0) uv: vec2f,
|
|
954
|
-
};
|
|
955
|
-
|
|
956
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
957
|
-
var output: VertexOutput;
|
|
958
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
959
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
960
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
961
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
962
|
-
return output;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
struct BloomComposeUniforms {
|
|
966
|
-
intensity: f32,
|
|
967
|
-
_padding1: f32,
|
|
968
|
-
_padding2: f32,
|
|
969
|
-
_padding3: f32,
|
|
970
|
-
_padding4: f32,
|
|
971
|
-
_padding5: f32,
|
|
972
|
-
_padding6: f32,
|
|
973
|
-
_padding7: f32,
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
977
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
978
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
979
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
980
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
981
|
-
|
|
982
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
983
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
984
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
985
|
-
// Additive blending with intensity control
|
|
986
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
987
|
-
return vec4f(result, scene.a);
|
|
988
|
-
}
|
|
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
|
+
}
|
|
989
882
|
`,
|
|
990
883
|
});
|
|
991
884
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1164,10 +1057,11 @@ export class Engine {
|
|
|
1164
1057
|
format: this.presentationFormat,
|
|
1165
1058
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1166
1059
|
});
|
|
1167
|
-
this.sceneRenderTextureView = this.sceneRenderTexture.createView();
|
|
1168
1060
|
// Setup bloom textures and bind groups
|
|
1169
1061
|
this.setupBloom(width, height);
|
|
1170
1062
|
const depthTextureView = this.depthTexture.createView();
|
|
1063
|
+
// Cache the scene render texture view (only recreate on resize)
|
|
1064
|
+
this.sceneRenderTextureView = this.sceneRenderTexture.createView();
|
|
1171
1065
|
// Render scene to texture instead of directly to canvas
|
|
1172
1066
|
const colorAttachment = this.sampleCount > 1
|
|
1173
1067
|
? {
|
|
@@ -1468,14 +1362,6 @@ export class Engine {
|
|
|
1468
1362
|
const dir = pathParts.join("/") + "/";
|
|
1469
1363
|
this.modelDir = dir;
|
|
1470
1364
|
const model = await PmxLoader.load(path);
|
|
1471
|
-
// console.log({
|
|
1472
|
-
// vertices: Array.from(model.getVertices()),
|
|
1473
|
-
// indices: Array.from(model.getIndices()),
|
|
1474
|
-
// materials: model.getMaterials(),
|
|
1475
|
-
// textures: model.getTextures(),
|
|
1476
|
-
// bones: model.getSkeleton().bones,
|
|
1477
|
-
// skinning: { joints: Array.from(model.getSkinning().joints), weights: Array.from(model.getSkinning().weights) },
|
|
1478
|
-
// })
|
|
1479
1365
|
this.physics = new Physics(model.getRigidbodies(), model.getJoints());
|
|
1480
1366
|
await this.setupModelBuffers(model);
|
|
1481
1367
|
}
|
|
@@ -1572,38 +1458,6 @@ export class Engine {
|
|
|
1572
1458
|
const texture = await this.createTextureFromPath(path);
|
|
1573
1459
|
return texture;
|
|
1574
1460
|
};
|
|
1575
|
-
const loadToonTexture = async (toonTextureIndex) => {
|
|
1576
|
-
const texture = await loadTextureByIndex(toonTextureIndex);
|
|
1577
|
-
if (texture)
|
|
1578
|
-
return texture;
|
|
1579
|
-
// Default toon texture fallback - cache it
|
|
1580
|
-
const defaultToonPath = "__default_toon__";
|
|
1581
|
-
const cached = this.textureCache.get(defaultToonPath);
|
|
1582
|
-
if (cached)
|
|
1583
|
-
return cached;
|
|
1584
|
-
const defaultToonData = new Uint8Array(256 * 2 * 4);
|
|
1585
|
-
for (let i = 0; i < 256; i++) {
|
|
1586
|
-
const factor = i / 255.0;
|
|
1587
|
-
const gray = Math.floor(128 + factor * 127);
|
|
1588
|
-
defaultToonData[i * 4] = gray;
|
|
1589
|
-
defaultToonData[i * 4 + 1] = gray;
|
|
1590
|
-
defaultToonData[i * 4 + 2] = gray;
|
|
1591
|
-
defaultToonData[i * 4 + 3] = 255;
|
|
1592
|
-
defaultToonData[(256 + i) * 4] = gray;
|
|
1593
|
-
defaultToonData[(256 + i) * 4 + 1] = gray;
|
|
1594
|
-
defaultToonData[(256 + i) * 4 + 2] = gray;
|
|
1595
|
-
defaultToonData[(256 + i) * 4 + 3] = 255;
|
|
1596
|
-
}
|
|
1597
|
-
const defaultToonTexture = this.device.createTexture({
|
|
1598
|
-
label: "default toon texture",
|
|
1599
|
-
size: [256, 2],
|
|
1600
|
-
format: "rgba8unorm",
|
|
1601
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
1602
|
-
});
|
|
1603
|
-
this.device.queue.writeTexture({ texture: defaultToonTexture }, defaultToonData, { bytesPerRow: 256 * 4 }, [256, 2]);
|
|
1604
|
-
this.textureCache.set(defaultToonPath, defaultToonTexture);
|
|
1605
|
-
return defaultToonTexture;
|
|
1606
|
-
};
|
|
1607
1461
|
this.opaqueDraws = [];
|
|
1608
1462
|
this.eyeDraws = [];
|
|
1609
1463
|
this.hairDrawsOverEyes = [];
|
|
@@ -1621,10 +1475,8 @@ export class Engine {
|
|
|
1621
1475
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex);
|
|
1622
1476
|
if (!diffuseTexture)
|
|
1623
1477
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1624
|
-
const toonTexture = await loadToonTexture(mat.toonTextureIndex);
|
|
1625
1478
|
const materialAlpha = mat.diffuse[3];
|
|
1626
|
-
const
|
|
1627
|
-
const isTransparent = materialAlpha < 1.0 - EPSILON;
|
|
1479
|
+
const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON;
|
|
1628
1480
|
// Create material uniform data
|
|
1629
1481
|
const materialUniformData = new Float32Array(8);
|
|
1630
1482
|
materialUniformData[0] = materialAlpha;
|
|
@@ -1651,18 +1503,17 @@ export class Engine {
|
|
|
1651
1503
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1652
1504
|
{ binding: 3, resource: this.materialSampler },
|
|
1653
1505
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1654
|
-
{ binding: 5, resource:
|
|
1655
|
-
{ binding: 6, resource: this.materialSampler },
|
|
1656
|
-
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
1506
|
+
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1657
1507
|
],
|
|
1658
1508
|
});
|
|
1659
1509
|
if (mat.isEye) {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1510
|
+
if (indexCount > 0) {
|
|
1511
|
+
this.eyeDraws.push({
|
|
1512
|
+
count: indexCount,
|
|
1513
|
+
firstIndex: currentIndexOffset,
|
|
1514
|
+
bindGroup,
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1666
1517
|
}
|
|
1667
1518
|
else if (mat.isHair) {
|
|
1668
1519
|
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
@@ -1691,42 +1542,42 @@ export class Engine {
|
|
|
1691
1542
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1692
1543
|
{ binding: 3, resource: this.materialSampler },
|
|
1693
1544
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1694
|
-
{ binding: 5, resource:
|
|
1695
|
-
{ binding: 6, resource: this.materialSampler },
|
|
1696
|
-
{ binding: 7, resource: { buffer: buffer } },
|
|
1545
|
+
{ binding: 5, resource: { buffer: buffer } },
|
|
1697
1546
|
],
|
|
1698
1547
|
});
|
|
1699
1548
|
};
|
|
1700
1549
|
const bindGroupOverEyes = createHairBindGroup(true);
|
|
1701
1550
|
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
}
|
|
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
|
+
}
|
|
1714
1563
|
}
|
|
1715
1564
|
else if (isTransparent) {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1565
|
+
if (indexCount > 0) {
|
|
1566
|
+
this.transparentDraws.push({
|
|
1567
|
+
count: indexCount,
|
|
1568
|
+
firstIndex: currentIndexOffset,
|
|
1569
|
+
bindGroup,
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1722
1572
|
}
|
|
1723
1573
|
else {
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1574
|
+
if (indexCount > 0) {
|
|
1575
|
+
this.opaqueDraws.push({
|
|
1576
|
+
count: indexCount,
|
|
1577
|
+
firstIndex: currentIndexOffset,
|
|
1578
|
+
bindGroup,
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1730
1581
|
}
|
|
1731
1582
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1732
1583
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
@@ -1754,42 +1605,39 @@ export class Engine {
|
|
|
1754
1605
|
{ binding: 2, resource: { buffer: this.skinMatrixBuffer } },
|
|
1755
1606
|
],
|
|
1756
1607
|
});
|
|
1757
|
-
if (
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
isTransparent,
|
|
1787
|
-
});
|
|
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
|
+
}
|
|
1788
1637
|
}
|
|
1789
1638
|
}
|
|
1790
1639
|
currentIndexOffset += indexCount;
|
|
1791
1640
|
}
|
|
1792
|
-
this.gpuMemoryMB = this.calculateGpuMemory();
|
|
1793
1641
|
}
|
|
1794
1642
|
async createTextureFromPath(path) {
|
|
1795
1643
|
const cached = this.textureCache.get(path);
|
|
@@ -1822,6 +1670,56 @@ export class Engine {
|
|
|
1822
1670
|
return null;
|
|
1823
1671
|
}
|
|
1824
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
|
+
}
|
|
1825
1723
|
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
|
|
1826
1724
|
render() {
|
|
1827
1725
|
if (this.multisampleTexture && this.camera && this.device) {
|
|
@@ -1841,7 +1739,6 @@ export class Engine {
|
|
|
1841
1739
|
return;
|
|
1842
1740
|
}
|
|
1843
1741
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1844
|
-
this.drawCallCount = 0;
|
|
1845
1742
|
if (this.currentModel) {
|
|
1846
1743
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1847
1744
|
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
@@ -1850,81 +1747,19 @@ export class Engine {
|
|
|
1850
1747
|
// Pass 1: Opaque
|
|
1851
1748
|
pass.setPipeline(this.modelPipeline);
|
|
1852
1749
|
for (const draw of this.opaqueDraws) {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1856
|
-
this.drawCallCount++;
|
|
1857
|
-
}
|
|
1750
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1751
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1858
1752
|
}
|
|
1859
1753
|
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1860
|
-
|
|
1861
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1862
|
-
for (const draw of this.eyeDraws) {
|
|
1863
|
-
if (draw.count > 0) {
|
|
1864
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1865
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1866
|
-
this.drawCallCount++;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1754
|
+
this.renderEyes(pass);
|
|
1870
1755
|
this.drawOutlines(pass, false);
|
|
1871
|
-
//
|
|
1872
|
-
|
|
1873
|
-
pass.setPipeline(this.hairDepthPipeline);
|
|
1874
|
-
for (const draw of this.hairDrawsOverEyes) {
|
|
1875
|
-
if (draw.count > 0) {
|
|
1876
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1877
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1881
|
-
if (draw.count > 0) {
|
|
1882
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1883
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
// 3b: Hair shading (split by stencil for transparency over eyes)
|
|
1888
|
-
if (this.hairDrawsOverEyes.length > 0) {
|
|
1889
|
-
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1890
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1891
|
-
for (const draw of this.hairDrawsOverEyes) {
|
|
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
|
-
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1900
|
-
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1901
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1902
|
-
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1903
|
-
if (draw.count > 0) {
|
|
1904
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1905
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1906
|
-
this.drawCallCount++;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
// 3c: Hair outlines
|
|
1911
|
-
if (this.hairOutlineDraws.length > 0) {
|
|
1912
|
-
pass.setPipeline(this.hairOutlinePipeline);
|
|
1913
|
-
for (const draw of this.hairOutlineDraws) {
|
|
1914
|
-
if (draw.count > 0) {
|
|
1915
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1916
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1756
|
+
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1757
|
+
this.renderHair(pass);
|
|
1920
1758
|
// Pass 4: Transparent
|
|
1921
1759
|
pass.setPipeline(this.modelPipeline);
|
|
1922
1760
|
for (const draw of this.transparentDraws) {
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1926
|
-
this.drawCallCount++;
|
|
1927
|
-
}
|
|
1761
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1762
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1928
1763
|
}
|
|
1929
1764
|
this.drawOutlines(pass, true);
|
|
1930
1765
|
}
|
|
@@ -2032,6 +1867,7 @@ export class Engine {
|
|
|
2032
1867
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
2033
1868
|
}
|
|
2034
1869
|
updateRenderTarget() {
|
|
1870
|
+
// Use cached view (only recreated on resize in handleResize)
|
|
2035
1871
|
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
2036
1872
|
if (this.sampleCount > 1) {
|
|
2037
1873
|
colorAttachment.resolveTarget = this.sceneRenderTextureView;
|
|
@@ -2060,122 +1896,44 @@ export class Engine {
|
|
|
2060
1896
|
}
|
|
2061
1897
|
drawOutlines(pass, transparent) {
|
|
2062
1898
|
pass.setPipeline(this.outlinePipeline);
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
else {
|
|
2072
|
-
for (const draw of this.opaqueOutlineDraws) {
|
|
2073
|
-
if (draw.count > 0) {
|
|
2074
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
2075
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
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);
|
|
2078
1903
|
}
|
|
2079
1904
|
}
|
|
2080
1905
|
updateStats(frameTime) {
|
|
1906
|
+
// Simplified frame time tracking - rolling average with fixed window
|
|
2081
1907
|
const maxSamples = 60;
|
|
2082
|
-
this.frameTimeSamples.push(frameTime);
|
|
2083
1908
|
this.frameTimeSum += frameTime;
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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;
|
|
2087
1915
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
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
|
|
2090
1920
|
const now = performance.now();
|
|
2091
1921
|
this.framesSinceLastUpdate++;
|
|
2092
1922
|
const elapsed = now - this.lastFpsUpdate;
|
|
2093
|
-
if (elapsed >=
|
|
2094
|
-
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);
|
|
2095
1925
|
this.framesSinceLastUpdate = 0;
|
|
2096
1926
|
this.lastFpsUpdate = now;
|
|
2097
1927
|
}
|
|
2098
|
-
this.stats.gpuMemory = this.gpuMemoryMB;
|
|
2099
|
-
}
|
|
2100
|
-
calculateGpuMemory() {
|
|
2101
|
-
let textureMemoryBytes = 0;
|
|
2102
|
-
for (const texture of this.textureCache.values()) {
|
|
2103
|
-
textureMemoryBytes += texture.width * texture.height * 4;
|
|
2104
|
-
}
|
|
2105
|
-
let bufferMemoryBytes = 0;
|
|
2106
|
-
if (this.vertexBuffer) {
|
|
2107
|
-
const vertices = this.currentModel?.getVertices();
|
|
2108
|
-
if (vertices)
|
|
2109
|
-
bufferMemoryBytes += vertices.byteLength;
|
|
2110
|
-
}
|
|
2111
|
-
if (this.indexBuffer) {
|
|
2112
|
-
const indices = this.currentModel?.getIndices();
|
|
2113
|
-
if (indices)
|
|
2114
|
-
bufferMemoryBytes += indices.byteLength;
|
|
2115
|
-
}
|
|
2116
|
-
if (this.jointsBuffer) {
|
|
2117
|
-
const skinning = this.currentModel?.getSkinning();
|
|
2118
|
-
if (skinning)
|
|
2119
|
-
bufferMemoryBytes += skinning.joints.byteLength;
|
|
2120
|
-
}
|
|
2121
|
-
if (this.weightsBuffer) {
|
|
2122
|
-
const skinning = this.currentModel?.getSkinning();
|
|
2123
|
-
if (skinning)
|
|
2124
|
-
bufferMemoryBytes += skinning.weights.byteLength;
|
|
2125
|
-
}
|
|
2126
|
-
if (this.skinMatrixBuffer) {
|
|
2127
|
-
const skeleton = this.currentModel?.getSkeleton();
|
|
2128
|
-
if (skeleton)
|
|
2129
|
-
bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4);
|
|
2130
|
-
}
|
|
2131
|
-
if (this.worldMatrixBuffer) {
|
|
2132
|
-
const skeleton = this.currentModel?.getSkeleton();
|
|
2133
|
-
if (skeleton)
|
|
2134
|
-
bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4);
|
|
2135
|
-
}
|
|
2136
|
-
if (this.inverseBindMatrixBuffer) {
|
|
2137
|
-
const skeleton = this.currentModel?.getSkeleton();
|
|
2138
|
-
if (skeleton)
|
|
2139
|
-
bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4);
|
|
2140
|
-
}
|
|
2141
|
-
bufferMemoryBytes += 40 * 4;
|
|
2142
|
-
bufferMemoryBytes += 64 * 4;
|
|
2143
|
-
bufferMemoryBytes += 32;
|
|
2144
|
-
bufferMemoryBytes += 32;
|
|
2145
|
-
bufferMemoryBytes += 32;
|
|
2146
|
-
bufferMemoryBytes += 32;
|
|
2147
|
-
if (this.fullscreenQuadBuffer) {
|
|
2148
|
-
bufferMemoryBytes += 24 * 4;
|
|
2149
|
-
}
|
|
2150
|
-
const totalMaterialDraws = this.opaqueDraws.length +
|
|
2151
|
-
this.eyeDraws.length +
|
|
2152
|
-
this.hairDrawsOverEyes.length +
|
|
2153
|
-
this.hairDrawsOverNonEyes.length +
|
|
2154
|
-
this.transparentDraws.length;
|
|
2155
|
-
bufferMemoryBytes += totalMaterialDraws * 32;
|
|
2156
|
-
const totalOutlineDraws = this.opaqueOutlineDraws.length +
|
|
2157
|
-
this.eyeOutlineDraws.length +
|
|
2158
|
-
this.hairOutlineDraws.length +
|
|
2159
|
-
this.transparentOutlineDraws.length;
|
|
2160
|
-
bufferMemoryBytes += totalOutlineDraws * 32;
|
|
2161
|
-
let renderTargetMemoryBytes = 0;
|
|
2162
|
-
if (this.multisampleTexture) {
|
|
2163
|
-
const width = this.canvas.width;
|
|
2164
|
-
const height = this.canvas.height;
|
|
2165
|
-
renderTargetMemoryBytes += width * height * 4 * this.sampleCount;
|
|
2166
|
-
renderTargetMemoryBytes += width * height * 4;
|
|
2167
|
-
}
|
|
2168
|
-
if (this.sceneRenderTexture) {
|
|
2169
|
-
const width = this.canvas.width;
|
|
2170
|
-
const height = this.canvas.height;
|
|
2171
|
-
renderTargetMemoryBytes += width * height * 4;
|
|
2172
|
-
}
|
|
2173
|
-
if (this.bloomExtractTexture) {
|
|
2174
|
-
const width = Math.floor(this.canvas.width / this.BLOOM_DOWNSCALE_FACTOR);
|
|
2175
|
-
const height = Math.floor(this.canvas.height / this.BLOOM_DOWNSCALE_FACTOR);
|
|
2176
|
-
renderTargetMemoryBytes += width * height * 4 * 3;
|
|
2177
|
-
}
|
|
2178
|
-
const totalGPUMemoryBytes = textureMemoryBytes + bufferMemoryBytes + renderTargetMemoryBytes;
|
|
2179
|
-
return Math.round((totalGPUMemoryBytes / 1024 / 1024) * 100) / 100;
|
|
2180
1928
|
}
|
|
2181
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;
|