reze-engine 0.2.17 → 0.2.19
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 +1 -2
- package/dist/engine.d.ts +16 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +604 -722
- package/dist/model.d.ts +36 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +156 -1
- package/dist/pmx-loader.d.ts +3 -1
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +161 -94
- package/package.json +1 -1
- package/src/engine.ts +2267 -2392
- package/src/model.ts +649 -421
- package/src/pmx-loader.ts +180 -89
package/dist/engine.js
CHANGED
|
@@ -18,14 +18,15 @@ 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;
|
|
28
28
|
this.textureCache = new Map();
|
|
29
|
+
this.vertexBufferNeedsUpdate = false;
|
|
29
30
|
// Draw lists
|
|
30
31
|
this.opaqueDraws = [];
|
|
31
32
|
this.eyeDraws = [];
|
|
@@ -38,10 +39,9 @@ export class Engine {
|
|
|
38
39
|
this.transparentOutlineDraws = [];
|
|
39
40
|
this.lastFpsUpdate = performance.now();
|
|
40
41
|
this.framesSinceLastUpdate = 0;
|
|
41
|
-
this.frameTimeSamples = [];
|
|
42
|
-
this.frameTimeSum = 0;
|
|
43
|
-
this.drawCallCount = 0;
|
|
44
42
|
this.lastFrameTime = performance.now();
|
|
43
|
+
this.frameTimeSum = 0;
|
|
44
|
+
this.frameTimeCount = 0;
|
|
45
45
|
this.stats = {
|
|
46
46
|
fps: 0,
|
|
47
47
|
frameTime: 0,
|
|
@@ -57,10 +57,10 @@ export class Engine {
|
|
|
57
57
|
this.canvas = canvas;
|
|
58
58
|
if (options) {
|
|
59
59
|
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 ??
|
|
60
|
+
this.bloomIntensity = options.bloomIntensity ?? Engine.DEFAULT_BLOOM_INTENSITY;
|
|
61
|
+
this.rimLightIntensity = options.rimLightIntensity ?? Engine.DEFAULT_RIM_LIGHT_INTENSITY;
|
|
62
|
+
this.cameraDistance = options.cameraDistance ?? Engine.DEFAULT_CAMERA_DISTANCE;
|
|
63
|
+
this.cameraTarget = options.cameraTarget ?? Engine.DEFAULT_CAMERA_TARGET;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
// Step 1: Get WebGPU device and context
|
|
@@ -85,7 +85,6 @@ export class Engine {
|
|
|
85
85
|
this.setupCamera();
|
|
86
86
|
this.setupLighting();
|
|
87
87
|
this.createPipelines();
|
|
88
|
-
this.createFullscreenQuad();
|
|
89
88
|
this.createBloomPipelines();
|
|
90
89
|
this.setupResize();
|
|
91
90
|
}
|
|
@@ -98,101 +97,99 @@ export class Engine {
|
|
|
98
97
|
});
|
|
99
98
|
const shaderModule = this.device.createShaderModule({
|
|
100
99
|
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
|
-
}
|
|
100
|
+
code: /* wgsl */ `
|
|
101
|
+
struct CameraUniforms {
|
|
102
|
+
view: mat4x4f,
|
|
103
|
+
projection: mat4x4f,
|
|
104
|
+
viewPos: vec3f,
|
|
105
|
+
_padding: f32,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
struct LightUniforms {
|
|
109
|
+
ambientColor: vec3f,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
struct MaterialUniforms {
|
|
113
|
+
alpha: f32,
|
|
114
|
+
alphaMultiplier: f32,
|
|
115
|
+
rimIntensity: f32,
|
|
116
|
+
_padding1: f32,
|
|
117
|
+
rimColor: vec3f,
|
|
118
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
struct VertexOutput {
|
|
122
|
+
@builtin(position) position: vec4f,
|
|
123
|
+
@location(0) normal: vec3f,
|
|
124
|
+
@location(1) uv: vec2f,
|
|
125
|
+
@location(2) worldPos: vec3f,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
129
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
130
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
131
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
132
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
133
|
+
@group(0) @binding(5) var<uniform> material: MaterialUniforms;
|
|
134
|
+
|
|
135
|
+
@vertex fn vs(
|
|
136
|
+
@location(0) position: vec3f,
|
|
137
|
+
@location(1) normal: vec3f,
|
|
138
|
+
@location(2) uv: vec2f,
|
|
139
|
+
@location(3) joints0: vec4<u32>,
|
|
140
|
+
@location(4) weights0: vec4<f32>
|
|
141
|
+
) -> VertexOutput {
|
|
142
|
+
var output: VertexOutput;
|
|
143
|
+
let pos4 = vec4f(position, 1.0);
|
|
144
|
+
|
|
145
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
146
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
147
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
148
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
149
|
+
|
|
150
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
151
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
152
|
+
for (var i = 0u; i < 4u; i++) {
|
|
153
|
+
let j = joints0[i];
|
|
154
|
+
let w = normalizedWeights[i];
|
|
155
|
+
let m = skinMats[j];
|
|
156
|
+
skinnedPos += (m * pos4) * w;
|
|
157
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
158
|
+
skinnedNrm += (r3 * normal) * w;
|
|
159
|
+
}
|
|
160
|
+
let worldPos = skinnedPos.xyz;
|
|
161
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
162
|
+
output.normal = normalize(skinnedNrm);
|
|
163
|
+
output.uv = uv;
|
|
164
|
+
output.worldPos = worldPos;
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
169
|
+
// Early alpha test - discard before expensive calculations
|
|
170
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
171
|
+
if (material.isOverEyes > 0.5) {
|
|
172
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
173
|
+
}
|
|
174
|
+
if (finalAlpha < 0.001) {
|
|
175
|
+
discard;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let n = normalize(input.normal);
|
|
179
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
180
|
+
|
|
181
|
+
let lightAccum = light.ambientColor;
|
|
182
|
+
|
|
183
|
+
// Rim light calculation
|
|
184
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
185
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
186
|
+
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
187
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
188
|
+
|
|
189
|
+
let color = albedo * lightAccum + rimLight;
|
|
190
|
+
|
|
191
|
+
return vec4f(color, finalAlpha);
|
|
192
|
+
}
|
|
196
193
|
`,
|
|
197
194
|
});
|
|
198
195
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -204,9 +201,7 @@ export class Engine {
|
|
|
204
201
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // diffuseTexture
|
|
205
202
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // diffuseSampler
|
|
206
203
|
{ 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
|
|
204
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
210
205
|
],
|
|
211
206
|
});
|
|
212
207
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -282,73 +277,73 @@ export class Engine {
|
|
|
282
277
|
});
|
|
283
278
|
const outlineShaderModule = this.device.createShaderModule({
|
|
284
279
|
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
|
-
}
|
|
280
|
+
code: /* wgsl */ `
|
|
281
|
+
struct CameraUniforms {
|
|
282
|
+
view: mat4x4f,
|
|
283
|
+
projection: mat4x4f,
|
|
284
|
+
viewPos: vec3f,
|
|
285
|
+
_padding: f32,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
struct MaterialUniforms {
|
|
289
|
+
edgeColor: vec4f,
|
|
290
|
+
edgeSize: f32,
|
|
291
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
292
|
+
_padding1: f32,
|
|
293
|
+
_padding2: f32,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
297
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
298
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
299
|
+
|
|
300
|
+
struct VertexOutput {
|
|
301
|
+
@builtin(position) position: vec4f,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
@vertex fn vs(
|
|
305
|
+
@location(0) position: vec3f,
|
|
306
|
+
@location(1) normal: vec3f,
|
|
307
|
+
@location(3) joints0: vec4<u32>,
|
|
308
|
+
@location(4) weights0: vec4<f32>
|
|
309
|
+
) -> VertexOutput {
|
|
310
|
+
var output: VertexOutput;
|
|
311
|
+
let pos4 = vec4f(position, 1.0);
|
|
312
|
+
|
|
313
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
314
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
315
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
316
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
317
|
+
|
|
318
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
319
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
320
|
+
for (var i = 0u; i < 4u; i++) {
|
|
321
|
+
let j = joints0[i];
|
|
322
|
+
let w = normalizedWeights[i];
|
|
323
|
+
let m = skinMats[j];
|
|
324
|
+
skinnedPos += (m * pos4) * w;
|
|
325
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
326
|
+
skinnedNrm += (r3 * normal) * w;
|
|
327
|
+
}
|
|
328
|
+
let worldPos = skinnedPos.xyz;
|
|
329
|
+
let worldNormal = normalize(skinnedNrm);
|
|
330
|
+
|
|
331
|
+
// MMD invert hull: expand vertices outward along normals
|
|
332
|
+
let scaleFactor = 0.01;
|
|
333
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
334
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
335
|
+
return output;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
339
|
+
var color = material.edgeColor;
|
|
340
|
+
|
|
341
|
+
if (material.isOverEyes > 0.5) {
|
|
342
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return color;
|
|
346
|
+
}
|
|
352
347
|
`,
|
|
353
348
|
});
|
|
354
349
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -552,45 +547,45 @@ export class Engine {
|
|
|
552
547
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
553
548
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
554
549
|
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
|
-
}
|
|
550
|
+
code: /* wgsl */ `
|
|
551
|
+
struct CameraUniforms {
|
|
552
|
+
view: mat4x4f,
|
|
553
|
+
projection: mat4x4f,
|
|
554
|
+
viewPos: vec3f,
|
|
555
|
+
_padding: f32,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
559
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
560
|
+
|
|
561
|
+
@vertex fn vs(
|
|
562
|
+
@location(0) position: vec3f,
|
|
563
|
+
@location(1) normal: vec3f,
|
|
564
|
+
@location(3) joints0: vec4<u32>,
|
|
565
|
+
@location(4) weights0: vec4<f32>
|
|
566
|
+
) -> @builtin(position) vec4f {
|
|
567
|
+
let pos4 = vec4f(position, 1.0);
|
|
568
|
+
|
|
569
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
570
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
571
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
572
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
573
|
+
|
|
574
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
575
|
+
for (var i = 0u; i < 4u; i++) {
|
|
576
|
+
let j = joints0[i];
|
|
577
|
+
let w = normalizedWeights[i];
|
|
578
|
+
let m = skinMats[j];
|
|
579
|
+
skinnedPos += (m * pos4) * w;
|
|
580
|
+
}
|
|
581
|
+
let worldPos = skinnedPos.xyz;
|
|
582
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
583
|
+
return clipPos;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
587
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
588
|
+
}
|
|
594
589
|
`,
|
|
595
590
|
});
|
|
596
591
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
@@ -638,165 +633,104 @@ export class Engine {
|
|
|
638
633
|
},
|
|
639
634
|
multisample: { count: this.sampleCount },
|
|
640
635
|
});
|
|
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
|
-
},
|
|
636
|
+
// Hair pipelines for rendering over eyes vs non-eyes (only differ in stencil compare mode)
|
|
637
|
+
const createHairPipeline = (isOverEyes) => {
|
|
638
|
+
return this.device.createRenderPipeline({
|
|
639
|
+
label: `hair pipeline (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
640
|
+
layout: mainPipelineLayout,
|
|
641
|
+
vertex: {
|
|
642
|
+
module: shaderModule,
|
|
643
|
+
buffers: [
|
|
644
|
+
{
|
|
645
|
+
arrayStride: 8 * 4,
|
|
646
|
+
attributes: [
|
|
647
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
648
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
649
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
650
|
+
],
|
|
682
651
|
},
|
|
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",
|
|
652
|
+
{
|
|
653
|
+
arrayStride: 4 * 2,
|
|
654
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
arrayStride: 4,
|
|
658
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
659
|
+
},
|
|
660
|
+
],
|
|
702
661
|
},
|
|
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",
|
|
662
|
+
fragment: {
|
|
663
|
+
module: shaderModule,
|
|
664
|
+
targets: [
|
|
665
|
+
{
|
|
666
|
+
format: this.presentationFormat,
|
|
667
|
+
blend: {
|
|
668
|
+
color: {
|
|
669
|
+
srcFactor: "src-alpha",
|
|
670
|
+
dstFactor: "one-minus-src-alpha",
|
|
671
|
+
operation: "add",
|
|
672
|
+
},
|
|
673
|
+
alpha: {
|
|
674
|
+
srcFactor: "one",
|
|
675
|
+
dstFactor: "one-minus-src-alpha",
|
|
676
|
+
operation: "add",
|
|
677
|
+
},
|
|
746
678
|
},
|
|
747
679
|
},
|
|
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",
|
|
680
|
+
],
|
|
761
681
|
},
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
682
|
+
primitive: { cullMode: "front" },
|
|
683
|
+
depthStencil: {
|
|
684
|
+
format: "depth24plus-stencil8",
|
|
685
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
686
|
+
depthCompare: "less-equal", // More lenient than "equal" to avoid precision issues with MSAA
|
|
687
|
+
stencilFront: {
|
|
688
|
+
compare: isOverEyes ? "equal" : "not-equal", // Over eyes: stencil == 1, over non-eyes: stencil != 1
|
|
689
|
+
failOp: "keep",
|
|
690
|
+
depthFailOp: "keep",
|
|
691
|
+
passOp: "keep",
|
|
692
|
+
},
|
|
693
|
+
stencilBack: {
|
|
694
|
+
compare: isOverEyes ? "equal" : "not-equal",
|
|
695
|
+
failOp: "keep",
|
|
696
|
+
depthFailOp: "keep",
|
|
697
|
+
passOp: "keep",
|
|
698
|
+
},
|
|
767
699
|
},
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
700
|
+
multisample: { count: this.sampleCount },
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
this.hairPipelineOverEyes = createHairPipeline(true);
|
|
704
|
+
this.hairPipelineOverNonEyes = createHairPipeline(false);
|
|
771
705
|
}
|
|
772
706
|
// Create compute shader for skin matrix computation
|
|
773
707
|
createSkinMatrixComputePipeline() {
|
|
774
708
|
const computeShader = this.device.createShaderModule({
|
|
775
709
|
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
|
-
}
|
|
710
|
+
code: /* wgsl */ `
|
|
711
|
+
struct BoneCountUniform {
|
|
712
|
+
count: u32,
|
|
713
|
+
_padding1: u32,
|
|
714
|
+
_padding2: u32,
|
|
715
|
+
_padding3: u32,
|
|
716
|
+
_padding4: vec4<u32>,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
720
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
721
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
722
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
723
|
+
|
|
724
|
+
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
725
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
726
|
+
let boneIndex = globalId.x;
|
|
727
|
+
if (boneIndex >= boneCount.count) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
let worldMat = worldMatrices[boneIndex];
|
|
731
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
732
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
733
|
+
}
|
|
800
734
|
`,
|
|
801
735
|
});
|
|
802
736
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -807,183 +741,145 @@ export class Engine {
|
|
|
807
741
|
},
|
|
808
742
|
});
|
|
809
743
|
}
|
|
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
744
|
// Create bloom post-processing pipelines
|
|
849
745
|
createBloomPipelines() {
|
|
850
746
|
// Bloom extraction shader (extracts bright areas)
|
|
851
747
|
const bloomExtractShader = this.device.createShaderModule({
|
|
852
748
|
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
|
-
}
|
|
749
|
+
code: /* wgsl */ `
|
|
750
|
+
struct VertexOutput {
|
|
751
|
+
@builtin(position) position: vec4f,
|
|
752
|
+
@location(0) uv: vec2f,
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
756
|
+
var output: VertexOutput;
|
|
757
|
+
// Generate fullscreen quad from vertex index
|
|
758
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
759
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
760
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
761
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
762
|
+
return output;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
struct BloomExtractUniforms {
|
|
766
|
+
threshold: f32,
|
|
767
|
+
_padding1: f32,
|
|
768
|
+
_padding2: f32,
|
|
769
|
+
_padding3: f32,
|
|
770
|
+
_padding4: f32,
|
|
771
|
+
_padding5: f32,
|
|
772
|
+
_padding6: f32,
|
|
773
|
+
_padding7: f32,
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
777
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
778
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
779
|
+
|
|
780
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
781
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
782
|
+
// Extract bright areas above threshold
|
|
783
|
+
let threshold = extractUniforms.threshold;
|
|
784
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
785
|
+
return vec4f(bloom, color.a);
|
|
786
|
+
}
|
|
891
787
|
`,
|
|
892
788
|
});
|
|
893
789
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
894
790
|
const bloomBlurShader = this.device.createShaderModule({
|
|
895
791
|
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
|
-
}
|
|
792
|
+
code: /* wgsl */ `
|
|
793
|
+
struct VertexOutput {
|
|
794
|
+
@builtin(position) position: vec4f,
|
|
795
|
+
@location(0) uv: vec2f,
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
799
|
+
var output: VertexOutput;
|
|
800
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
801
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
802
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
803
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
804
|
+
return output;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
struct BlurUniforms {
|
|
808
|
+
direction: vec2f,
|
|
809
|
+
_padding1: f32,
|
|
810
|
+
_padding2: f32,
|
|
811
|
+
_padding3: f32,
|
|
812
|
+
_padding4: f32,
|
|
813
|
+
_padding5: f32,
|
|
814
|
+
_padding6: f32,
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
818
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
819
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
820
|
+
|
|
821
|
+
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
822
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
823
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
824
|
+
|
|
825
|
+
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
826
|
+
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
827
|
+
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
828
|
+
let weight0 = 0.38774; // Center sample
|
|
829
|
+
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
830
|
+
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
831
|
+
|
|
832
|
+
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
833
|
+
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
834
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
835
|
+
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
836
|
+
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
943
839
|
`,
|
|
944
840
|
});
|
|
945
841
|
// Bloom composition shader (combines original scene with bloom)
|
|
946
842
|
const bloomComposeShader = this.device.createShaderModule({
|
|
947
843
|
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
|
-
}
|
|
844
|
+
code: /* wgsl */ `
|
|
845
|
+
struct VertexOutput {
|
|
846
|
+
@builtin(position) position: vec4f,
|
|
847
|
+
@location(0) uv: vec2f,
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
851
|
+
var output: VertexOutput;
|
|
852
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
853
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
854
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
855
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
856
|
+
return output;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
struct BloomComposeUniforms {
|
|
860
|
+
intensity: f32,
|
|
861
|
+
_padding1: f32,
|
|
862
|
+
_padding2: f32,
|
|
863
|
+
_padding3: f32,
|
|
864
|
+
_padding4: f32,
|
|
865
|
+
_padding5: f32,
|
|
866
|
+
_padding6: f32,
|
|
867
|
+
_padding7: f32,
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
871
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
872
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
873
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
874
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
875
|
+
|
|
876
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
877
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
878
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
879
|
+
// Additive blending with intensity control
|
|
880
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
881
|
+
return vec4f(result, scene.a);
|
|
882
|
+
}
|
|
987
883
|
`,
|
|
988
884
|
});
|
|
989
885
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1162,10 +1058,11 @@ export class Engine {
|
|
|
1162
1058
|
format: this.presentationFormat,
|
|
1163
1059
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1164
1060
|
});
|
|
1165
|
-
this.sceneRenderTextureView = this.sceneRenderTexture.createView();
|
|
1166
1061
|
// Setup bloom textures and bind groups
|
|
1167
1062
|
this.setupBloom(width, height);
|
|
1168
1063
|
const depthTextureView = this.depthTexture.createView();
|
|
1064
|
+
// Cache the scene render texture view (only recreate on resize)
|
|
1065
|
+
this.sceneRenderTextureView = this.sceneRenderTexture.createView();
|
|
1169
1066
|
// Render scene to texture instead of directly to canvas
|
|
1170
1067
|
const colorAttachment = this.sampleCount > 1
|
|
1171
1068
|
? {
|
|
@@ -1472,6 +1369,22 @@ export class Engine {
|
|
|
1472
1369
|
rotateBones(bones, rotations, durationMs) {
|
|
1473
1370
|
this.currentModel?.rotateBones(bones, rotations, durationMs);
|
|
1474
1371
|
}
|
|
1372
|
+
setMorphWeight(name, weight, durationMs) {
|
|
1373
|
+
if (!this.currentModel)
|
|
1374
|
+
return;
|
|
1375
|
+
this.currentModel.setMorphWeight(name, weight, durationMs);
|
|
1376
|
+
if (!durationMs || durationMs === 0) {
|
|
1377
|
+
this.vertexBufferNeedsUpdate = true;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
updateVertexBuffer() {
|
|
1381
|
+
if (!this.currentModel || !this.vertexBuffer)
|
|
1382
|
+
return;
|
|
1383
|
+
const vertices = this.currentModel.getVertices();
|
|
1384
|
+
if (!vertices || vertices.length === 0)
|
|
1385
|
+
return;
|
|
1386
|
+
this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices);
|
|
1387
|
+
}
|
|
1475
1388
|
// Step 7: Create vertex, index, and joint buffers
|
|
1476
1389
|
async setupModelBuffers(model) {
|
|
1477
1390
|
this.currentModel = model;
|
|
@@ -1562,38 +1475,6 @@ export class Engine {
|
|
|
1562
1475
|
const texture = await this.createTextureFromPath(path);
|
|
1563
1476
|
return texture;
|
|
1564
1477
|
};
|
|
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
1478
|
this.opaqueDraws = [];
|
|
1598
1479
|
this.eyeDraws = [];
|
|
1599
1480
|
this.hairDrawsOverEyes = [];
|
|
@@ -1611,10 +1492,8 @@ export class Engine {
|
|
|
1611
1492
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex);
|
|
1612
1493
|
if (!diffuseTexture)
|
|
1613
1494
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1614
|
-
const toonTexture = await loadToonTexture(mat.toonTextureIndex);
|
|
1615
1495
|
const materialAlpha = mat.diffuse[3];
|
|
1616
|
-
const
|
|
1617
|
-
const isTransparent = materialAlpha < 1.0 - EPSILON;
|
|
1496
|
+
const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON;
|
|
1618
1497
|
// Create material uniform data
|
|
1619
1498
|
const materialUniformData = new Float32Array(8);
|
|
1620
1499
|
materialUniformData[0] = materialAlpha;
|
|
@@ -1641,18 +1520,17 @@ export class Engine {
|
|
|
1641
1520
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1642
1521
|
{ binding: 3, resource: this.materialSampler },
|
|
1643
1522
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1644
|
-
{ binding: 5, resource:
|
|
1645
|
-
{ binding: 6, resource: this.materialSampler },
|
|
1646
|
-
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
1523
|
+
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1647
1524
|
],
|
|
1648
1525
|
});
|
|
1649
1526
|
if (mat.isEye) {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1527
|
+
if (indexCount > 0) {
|
|
1528
|
+
this.eyeDraws.push({
|
|
1529
|
+
count: indexCount,
|
|
1530
|
+
firstIndex: currentIndexOffset,
|
|
1531
|
+
bindGroup,
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1656
1534
|
}
|
|
1657
1535
|
else if (mat.isHair) {
|
|
1658
1536
|
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
@@ -1681,42 +1559,42 @@ export class Engine {
|
|
|
1681
1559
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1682
1560
|
{ binding: 3, resource: this.materialSampler },
|
|
1683
1561
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1684
|
-
{ binding: 5, resource:
|
|
1685
|
-
{ binding: 6, resource: this.materialSampler },
|
|
1686
|
-
{ binding: 7, resource: { buffer: buffer } },
|
|
1562
|
+
{ binding: 5, resource: { buffer: buffer } },
|
|
1687
1563
|
],
|
|
1688
1564
|
});
|
|
1689
1565
|
};
|
|
1690
1566
|
const bindGroupOverEyes = createHairBindGroup(true);
|
|
1691
1567
|
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
}
|
|
1568
|
+
if (indexCount > 0) {
|
|
1569
|
+
this.hairDrawsOverEyes.push({
|
|
1570
|
+
count: indexCount,
|
|
1571
|
+
firstIndex: currentIndexOffset,
|
|
1572
|
+
bindGroup: bindGroupOverEyes,
|
|
1573
|
+
});
|
|
1574
|
+
this.hairDrawsOverNonEyes.push({
|
|
1575
|
+
count: indexCount,
|
|
1576
|
+
firstIndex: currentIndexOffset,
|
|
1577
|
+
bindGroup: bindGroupOverNonEyes,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1704
1580
|
}
|
|
1705
1581
|
else if (isTransparent) {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1582
|
+
if (indexCount > 0) {
|
|
1583
|
+
this.transparentDraws.push({
|
|
1584
|
+
count: indexCount,
|
|
1585
|
+
firstIndex: currentIndexOffset,
|
|
1586
|
+
bindGroup,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1712
1589
|
}
|
|
1713
1590
|
else {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1591
|
+
if (indexCount > 0) {
|
|
1592
|
+
this.opaqueDraws.push({
|
|
1593
|
+
count: indexCount,
|
|
1594
|
+
firstIndex: currentIndexOffset,
|
|
1595
|
+
bindGroup,
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1720
1598
|
}
|
|
1721
1599
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1722
1600
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
@@ -1744,37 +1622,35 @@ export class Engine {
|
|
|
1744
1622
|
{ binding: 2, resource: { buffer: this.skinMatrixBuffer } },
|
|
1745
1623
|
],
|
|
1746
1624
|
});
|
|
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
|
-
});
|
|
1625
|
+
if (indexCount > 0) {
|
|
1626
|
+
if (mat.isEye) {
|
|
1627
|
+
this.eyeOutlineDraws.push({
|
|
1628
|
+
count: indexCount,
|
|
1629
|
+
firstIndex: currentIndexOffset,
|
|
1630
|
+
bindGroup: outlineBindGroup,
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
else if (mat.isHair) {
|
|
1634
|
+
this.hairOutlineDraws.push({
|
|
1635
|
+
count: indexCount,
|
|
1636
|
+
firstIndex: currentIndexOffset,
|
|
1637
|
+
bindGroup: outlineBindGroup,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
else if (isTransparent) {
|
|
1641
|
+
this.transparentOutlineDraws.push({
|
|
1642
|
+
count: indexCount,
|
|
1643
|
+
firstIndex: currentIndexOffset,
|
|
1644
|
+
bindGroup: outlineBindGroup,
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
this.opaqueOutlineDraws.push({
|
|
1649
|
+
count: indexCount,
|
|
1650
|
+
firstIndex: currentIndexOffset,
|
|
1651
|
+
bindGroup: outlineBindGroup,
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1778
1654
|
}
|
|
1779
1655
|
}
|
|
1780
1656
|
currentIndexOffset += indexCount;
|
|
@@ -1811,6 +1687,56 @@ export class Engine {
|
|
|
1811
1687
|
return null;
|
|
1812
1688
|
}
|
|
1813
1689
|
}
|
|
1690
|
+
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1691
|
+
renderEyes(pass) {
|
|
1692
|
+
pass.setPipeline(this.eyePipeline);
|
|
1693
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1694
|
+
for (const draw of this.eyeDraws) {
|
|
1695
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1696
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1700
|
+
renderHair(pass) {
|
|
1701
|
+
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1702
|
+
const hasHair = this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0;
|
|
1703
|
+
if (hasHair) {
|
|
1704
|
+
pass.setPipeline(this.hairDepthPipeline);
|
|
1705
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1706
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1707
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1708
|
+
}
|
|
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 shading (split by stencil for transparency over eyes)
|
|
1715
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
1716
|
+
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1717
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1718
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1719
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1720
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1724
|
+
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1725
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1726
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1727
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1728
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
// Hair outlines
|
|
1732
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
1733
|
+
pass.setPipeline(this.hairOutlinePipeline);
|
|
1734
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1735
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1736
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1814
1740
|
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
|
|
1815
1741
|
render() {
|
|
1816
1742
|
if (this.multisampleTexture && this.camera && this.device) {
|
|
@@ -1819,6 +1745,19 @@ export class Engine {
|
|
|
1819
1745
|
this.lastFrameTime = currentTime;
|
|
1820
1746
|
this.updateCameraUniforms();
|
|
1821
1747
|
this.updateRenderTarget();
|
|
1748
|
+
// Update model pose first (this may update morph weights via tweens)
|
|
1749
|
+
// We need to do this before creating the encoder to ensure vertex buffer is ready
|
|
1750
|
+
if (this.currentModel) {
|
|
1751
|
+
const hasActiveMorphTweens = this.currentModel.evaluatePose();
|
|
1752
|
+
if (hasActiveMorphTweens) {
|
|
1753
|
+
this.vertexBufferNeedsUpdate = true;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
// Update vertex buffer if morphs changed
|
|
1757
|
+
if (this.vertexBufferNeedsUpdate) {
|
|
1758
|
+
this.updateVertexBuffer();
|
|
1759
|
+
this.vertexBufferNeedsUpdate = false;
|
|
1760
|
+
}
|
|
1822
1761
|
// Use single encoder for both compute and render (reduces sync points)
|
|
1823
1762
|
const encoder = this.device.createCommandEncoder();
|
|
1824
1763
|
this.updateModelPose(deltaTime, encoder);
|
|
@@ -1830,7 +1769,6 @@ export class Engine {
|
|
|
1830
1769
|
return;
|
|
1831
1770
|
}
|
|
1832
1771
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1833
|
-
this.drawCallCount = 0;
|
|
1834
1772
|
if (this.currentModel) {
|
|
1835
1773
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1836
1774
|
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
@@ -1839,81 +1777,19 @@ export class Engine {
|
|
|
1839
1777
|
// Pass 1: Opaque
|
|
1840
1778
|
pass.setPipeline(this.modelPipeline);
|
|
1841
1779
|
for (const draw of this.opaqueDraws) {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1845
|
-
this.drawCallCount++;
|
|
1846
|
-
}
|
|
1780
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1781
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1847
1782
|
}
|
|
1848
1783
|
// 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)
|
|
1784
|
+
this.renderEyes(pass);
|
|
1859
1785
|
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
|
-
}
|
|
1786
|
+
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1787
|
+
this.renderHair(pass);
|
|
1909
1788
|
// Pass 4: Transparent
|
|
1910
1789
|
pass.setPipeline(this.modelPipeline);
|
|
1911
1790
|
for (const draw of this.transparentDraws) {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1915
|
-
this.drawCallCount++;
|
|
1916
|
-
}
|
|
1791
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1792
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1917
1793
|
}
|
|
1918
1794
|
this.drawOutlines(pass, true);
|
|
1919
1795
|
}
|
|
@@ -2021,6 +1897,7 @@ export class Engine {
|
|
|
2021
1897
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
2022
1898
|
}
|
|
2023
1899
|
updateRenderTarget() {
|
|
1900
|
+
// Use cached view (only recreated on resize in handleResize)
|
|
2024
1901
|
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
2025
1902
|
if (this.sampleCount > 1) {
|
|
2026
1903
|
colorAttachment.resolveTarget = this.sceneRenderTextureView;
|
|
@@ -2030,7 +1907,8 @@ export class Engine {
|
|
|
2030
1907
|
}
|
|
2031
1908
|
}
|
|
2032
1909
|
updateModelPose(deltaTime, encoder) {
|
|
2033
|
-
|
|
1910
|
+
// Note: evaluatePose is called earlier in render() to update vertex buffer before encoder creation
|
|
1911
|
+
// Here we just get the matrices and update physics/compute
|
|
2034
1912
|
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
2035
1913
|
if (this.physics) {
|
|
2036
1914
|
this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
@@ -2049,40 +1927,44 @@ export class Engine {
|
|
|
2049
1927
|
}
|
|
2050
1928
|
drawOutlines(pass, transparent) {
|
|
2051
1929
|
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
|
-
}
|
|
1930
|
+
const draws = transparent ? this.transparentOutlineDraws : this.opaqueOutlineDraws;
|
|
1931
|
+
for (const draw of draws) {
|
|
1932
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1933
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2067
1934
|
}
|
|
2068
1935
|
}
|
|
2069
1936
|
updateStats(frameTime) {
|
|
1937
|
+
// Simplified frame time tracking - rolling average with fixed window
|
|
2070
1938
|
const maxSamples = 60;
|
|
2071
|
-
this.frameTimeSamples.push(frameTime);
|
|
2072
1939
|
this.frameTimeSum += frameTime;
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
1940
|
+
this.frameTimeCount++;
|
|
1941
|
+
if (this.frameTimeCount > maxSamples) {
|
|
1942
|
+
// Maintain rolling window by subtracting oldest sample estimate
|
|
1943
|
+
const avg = this.frameTimeSum / maxSamples;
|
|
1944
|
+
this.frameTimeSum -= avg;
|
|
1945
|
+
this.frameTimeCount = maxSamples;
|
|
2076
1946
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
1947
|
+
this.stats.frameTime =
|
|
1948
|
+
Math.round((this.frameTimeSum / this.frameTimeCount) * Engine.STATS_FRAME_TIME_ROUNDING) /
|
|
1949
|
+
Engine.STATS_FRAME_TIME_ROUNDING;
|
|
1950
|
+
// FPS tracking
|
|
2079
1951
|
const now = performance.now();
|
|
2080
1952
|
this.framesSinceLastUpdate++;
|
|
2081
1953
|
const elapsed = now - this.lastFpsUpdate;
|
|
2082
|
-
if (elapsed >=
|
|
2083
|
-
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) *
|
|
1954
|
+
if (elapsed >= Engine.STATS_FPS_UPDATE_INTERVAL_MS) {
|
|
1955
|
+
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * Engine.STATS_FPS_UPDATE_INTERVAL_MS);
|
|
2084
1956
|
this.framesSinceLastUpdate = 0;
|
|
2085
1957
|
this.lastFpsUpdate = now;
|
|
2086
1958
|
}
|
|
2087
1959
|
}
|
|
2088
1960
|
}
|
|
1961
|
+
// Default values
|
|
1962
|
+
Engine.DEFAULT_BLOOM_THRESHOLD = 0.01;
|
|
1963
|
+
Engine.DEFAULT_BLOOM_INTENSITY = 0.12;
|
|
1964
|
+
Engine.DEFAULT_RIM_LIGHT_INTENSITY = 0.45;
|
|
1965
|
+
Engine.DEFAULT_CAMERA_DISTANCE = 26.6;
|
|
1966
|
+
Engine.DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0);
|
|
1967
|
+
Engine.HAIR_OVER_EYES_ALPHA = 0.5;
|
|
1968
|
+
Engine.TRANSPARENCY_EPSILON = 0.001;
|
|
1969
|
+
Engine.STATS_FPS_UPDATE_INTERVAL_MS = 1000;
|
|
1970
|
+
Engine.STATS_FRAME_TIME_ROUNDING = 100;
|