reze-engine 0.2.4 → 0.2.5
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 +104 -104
- package/dist/engine.d.ts +11 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +530 -589
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +2414 -2475
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/pmx-loader.ts +1054 -1054
- package/src/vmd-loader.ts +179 -179
package/dist/engine.js
CHANGED
|
@@ -9,7 +9,11 @@ export class Engine {
|
|
|
9
9
|
this.lightData = new Float32Array(64);
|
|
10
10
|
this.lightCount = 0;
|
|
11
11
|
this.resizeObserver = null;
|
|
12
|
-
this.sampleCount = 4;
|
|
12
|
+
this.sampleCount = 4;
|
|
13
|
+
// Constants
|
|
14
|
+
this.STENCIL_EYE_VALUE = 1;
|
|
15
|
+
this.COMPUTE_WORKGROUP_SIZE = 64;
|
|
16
|
+
this.BLOOM_DOWNSCALE_FACTOR = 2;
|
|
13
17
|
// Ambient light settings
|
|
14
18
|
this.ambient = 1.0;
|
|
15
19
|
// Bloom settings
|
|
@@ -22,7 +26,6 @@ export class Engine {
|
|
|
22
26
|
this.modelDir = "";
|
|
23
27
|
this.physics = null;
|
|
24
28
|
this.textureCache = new Map();
|
|
25
|
-
this.textureSizes = new Map();
|
|
26
29
|
this.lastFpsUpdate = performance.now();
|
|
27
30
|
this.framesSinceLastUpdate = 0;
|
|
28
31
|
this.frameTimeSamples = [];
|
|
@@ -38,6 +41,7 @@ export class Engine {
|
|
|
38
41
|
this.renderLoopCallback = null;
|
|
39
42
|
this.animationFrames = [];
|
|
40
43
|
this.animationTimeouts = [];
|
|
44
|
+
this.gpuMemoryMB = 0;
|
|
41
45
|
this.opaqueNonEyeNonHairDraws = [];
|
|
42
46
|
this.eyeDraws = [];
|
|
43
47
|
this.hairDrawsOverEyes = [];
|
|
@@ -90,130 +94,130 @@ export class Engine {
|
|
|
90
94
|
});
|
|
91
95
|
const shaderModule = this.device.createShaderModule({
|
|
92
96
|
label: "model shaders",
|
|
93
|
-
code: /* wgsl */ `
|
|
94
|
-
struct CameraUniforms {
|
|
95
|
-
view: mat4x4f,
|
|
96
|
-
projection: mat4x4f,
|
|
97
|
-
viewPos: vec3f,
|
|
98
|
-
_padding: f32,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
struct Light {
|
|
102
|
-
direction: vec3f,
|
|
103
|
-
_padding1: f32,
|
|
104
|
-
color: vec3f,
|
|
105
|
-
intensity: f32,
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
struct LightUniforms {
|
|
109
|
-
ambient: f32,
|
|
110
|
-
lightCount: f32,
|
|
111
|
-
_padding1: f32,
|
|
112
|
-
_padding2: f32,
|
|
113
|
-
lights: array<Light, 4>,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
struct MaterialUniforms {
|
|
117
|
-
alpha: f32,
|
|
118
|
-
alphaMultiplier: f32,
|
|
119
|
-
rimIntensity: f32,
|
|
120
|
-
rimPower: f32,
|
|
121
|
-
rimColor: vec3f,
|
|
122
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
struct VertexOutput {
|
|
126
|
-
@builtin(position) position: vec4f,
|
|
127
|
-
@location(0) normal: vec3f,
|
|
128
|
-
@location(1) uv: vec2f,
|
|
129
|
-
@location(2) worldPos: vec3f,
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
133
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
134
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
135
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
136
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
137
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
138
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
139
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
140
|
-
|
|
141
|
-
@vertex fn vs(
|
|
142
|
-
@location(0) position: vec3f,
|
|
143
|
-
@location(1) normal: vec3f,
|
|
144
|
-
@location(2) uv: vec2f,
|
|
145
|
-
@location(3) joints0: vec4<u32>,
|
|
146
|
-
@location(4) weights0: vec4<f32>
|
|
147
|
-
) -> VertexOutput {
|
|
148
|
-
var output: VertexOutput;
|
|
149
|
-
let pos4 = vec4f(position, 1.0);
|
|
150
|
-
|
|
151
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
152
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
153
|
-
var normalizedWeights: vec4f;
|
|
154
|
-
if (weightSum > 0.0001) {
|
|
155
|
-
normalizedWeights = weights0 / weightSum;
|
|
156
|
-
} else {
|
|
157
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
161
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
162
|
-
for (var i = 0u; i < 4u; i++) {
|
|
163
|
-
let j = joints0[i];
|
|
164
|
-
let w = normalizedWeights[i];
|
|
165
|
-
let m = skinMats[j];
|
|
166
|
-
skinnedPos += (m * pos4) * w;
|
|
167
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
168
|
-
skinnedNrm += (r3 * normal) * w;
|
|
169
|
-
}
|
|
170
|
-
let worldPos = skinnedPos.xyz;
|
|
171
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
172
|
-
output.normal = normalize(skinnedNrm);
|
|
173
|
-
output.uv = uv;
|
|
174
|
-
output.worldPos = worldPos;
|
|
175
|
-
return output;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
179
|
-
let n = normalize(input.normal);
|
|
180
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
181
|
-
|
|
182
|
-
var lightAccum = vec3f(light.ambient);
|
|
183
|
-
let numLights = u32(light.lightCount);
|
|
184
|
-
for (var i = 0u; i < numLights; i++) {
|
|
185
|
-
let l = -light.lights[i].direction;
|
|
186
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
187
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
188
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
189
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
190
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Rim light calculation
|
|
194
|
-
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
195
|
-
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
196
|
-
rimFactor = pow(rimFactor, material.rimPower);
|
|
197
|
-
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
198
|
-
|
|
199
|
-
let color = albedo * lightAccum + rimLight;
|
|
200
|
-
|
|
201
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
202
|
-
if (material.isOverEyes > 0.5) {
|
|
203
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (finalAlpha < 0.001) {
|
|
207
|
-
discard;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
211
|
-
}
|
|
97
|
+
code: /* wgsl */ `
|
|
98
|
+
struct CameraUniforms {
|
|
99
|
+
view: mat4x4f,
|
|
100
|
+
projection: mat4x4f,
|
|
101
|
+
viewPos: vec3f,
|
|
102
|
+
_padding: f32,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
struct Light {
|
|
106
|
+
direction: vec3f,
|
|
107
|
+
_padding1: f32,
|
|
108
|
+
color: vec3f,
|
|
109
|
+
intensity: f32,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
struct LightUniforms {
|
|
113
|
+
ambient: f32,
|
|
114
|
+
lightCount: f32,
|
|
115
|
+
_padding1: f32,
|
|
116
|
+
_padding2: f32,
|
|
117
|
+
lights: array<Light, 4>,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
struct MaterialUniforms {
|
|
121
|
+
alpha: f32,
|
|
122
|
+
alphaMultiplier: f32,
|
|
123
|
+
rimIntensity: f32,
|
|
124
|
+
rimPower: f32,
|
|
125
|
+
rimColor: vec3f,
|
|
126
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
struct VertexOutput {
|
|
130
|
+
@builtin(position) position: vec4f,
|
|
131
|
+
@location(0) normal: vec3f,
|
|
132
|
+
@location(1) uv: vec2f,
|
|
133
|
+
@location(2) worldPos: vec3f,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
137
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
138
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
139
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
140
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
141
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
142
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
143
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
144
|
+
|
|
145
|
+
@vertex fn vs(
|
|
146
|
+
@location(0) position: vec3f,
|
|
147
|
+
@location(1) normal: vec3f,
|
|
148
|
+
@location(2) uv: vec2f,
|
|
149
|
+
@location(3) joints0: vec4<u32>,
|
|
150
|
+
@location(4) weights0: vec4<f32>
|
|
151
|
+
) -> VertexOutput {
|
|
152
|
+
var output: VertexOutput;
|
|
153
|
+
let pos4 = vec4f(position, 1.0);
|
|
154
|
+
|
|
155
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
156
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
157
|
+
var normalizedWeights: vec4f;
|
|
158
|
+
if (weightSum > 0.0001) {
|
|
159
|
+
normalizedWeights = weights0 / weightSum;
|
|
160
|
+
} else {
|
|
161
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
165
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
166
|
+
for (var i = 0u; i < 4u; i++) {
|
|
167
|
+
let j = joints0[i];
|
|
168
|
+
let w = normalizedWeights[i];
|
|
169
|
+
let m = skinMats[j];
|
|
170
|
+
skinnedPos += (m * pos4) * w;
|
|
171
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
172
|
+
skinnedNrm += (r3 * normal) * w;
|
|
173
|
+
}
|
|
174
|
+
let worldPos = skinnedPos.xyz;
|
|
175
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
176
|
+
output.normal = normalize(skinnedNrm);
|
|
177
|
+
output.uv = uv;
|
|
178
|
+
output.worldPos = worldPos;
|
|
179
|
+
return output;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
183
|
+
let n = normalize(input.normal);
|
|
184
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
185
|
+
|
|
186
|
+
var lightAccum = vec3f(light.ambient);
|
|
187
|
+
let numLights = u32(light.lightCount);
|
|
188
|
+
for (var i = 0u; i < numLights; i++) {
|
|
189
|
+
let l = -light.lights[i].direction;
|
|
190
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
191
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
192
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
193
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
194
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Rim light calculation
|
|
198
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
199
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
200
|
+
rimFactor = pow(rimFactor, material.rimPower);
|
|
201
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
202
|
+
|
|
203
|
+
let color = albedo * lightAccum + rimLight;
|
|
204
|
+
|
|
205
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
206
|
+
if (material.isOverEyes > 0.5) {
|
|
207
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (finalAlpha < 0.001) {
|
|
211
|
+
discard;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
215
|
+
}
|
|
212
216
|
`,
|
|
213
217
|
});
|
|
214
218
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
215
|
-
this.
|
|
216
|
-
label: "
|
|
219
|
+
this.mainBindGroupLayout = this.device.createBindGroupLayout({
|
|
220
|
+
label: "main material bind group layout",
|
|
217
221
|
entries: [
|
|
218
222
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
219
223
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
|
|
@@ -225,14 +229,13 @@ export class Engine {
|
|
|
225
229
|
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
226
230
|
],
|
|
227
231
|
});
|
|
228
|
-
const
|
|
229
|
-
label: "
|
|
230
|
-
bindGroupLayouts: [this.
|
|
232
|
+
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
233
|
+
label: "main pipeline layout",
|
|
234
|
+
bindGroupLayouts: [this.mainBindGroupLayout],
|
|
231
235
|
});
|
|
232
|
-
|
|
233
|
-
this.pipeline = this.device.createRenderPipeline({
|
|
236
|
+
this.modelPipeline = this.device.createRenderPipeline({
|
|
234
237
|
label: "model pipeline",
|
|
235
|
-
layout:
|
|
238
|
+
layout: mainPipelineLayout,
|
|
236
239
|
vertex: {
|
|
237
240
|
module: shaderModule,
|
|
238
241
|
buffers: [
|
|
@@ -299,77 +302,77 @@ export class Engine {
|
|
|
299
302
|
});
|
|
300
303
|
const outlineShaderModule = this.device.createShaderModule({
|
|
301
304
|
label: "outline shaders",
|
|
302
|
-
code: /* wgsl */ `
|
|
303
|
-
struct CameraUniforms {
|
|
304
|
-
view: mat4x4f,
|
|
305
|
-
projection: mat4x4f,
|
|
306
|
-
viewPos: vec3f,
|
|
307
|
-
_padding: f32,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
struct MaterialUniforms {
|
|
311
|
-
edgeColor: vec4f,
|
|
312
|
-
edgeSize: f32,
|
|
313
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
314
|
-
_padding1: f32,
|
|
315
|
-
_padding2: f32,
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
319
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
320
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
321
|
-
|
|
322
|
-
struct VertexOutput {
|
|
323
|
-
@builtin(position) position: vec4f,
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
@vertex fn vs(
|
|
327
|
-
@location(0) position: vec3f,
|
|
328
|
-
@location(1) normal: vec3f,
|
|
329
|
-
@location(3) joints0: vec4<u32>,
|
|
330
|
-
@location(4) weights0: vec4<f32>
|
|
331
|
-
) -> VertexOutput {
|
|
332
|
-
var output: VertexOutput;
|
|
333
|
-
let pos4 = vec4f(position, 1.0);
|
|
334
|
-
|
|
335
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
336
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
337
|
-
var normalizedWeights: vec4f;
|
|
338
|
-
if (weightSum > 0.0001) {
|
|
339
|
-
normalizedWeights = weights0 / weightSum;
|
|
340
|
-
} else {
|
|
341
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
345
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
346
|
-
for (var i = 0u; i < 4u; i++) {
|
|
347
|
-
let j = joints0[i];
|
|
348
|
-
let w = normalizedWeights[i];
|
|
349
|
-
let m = skinMats[j];
|
|
350
|
-
skinnedPos += (m * pos4) * w;
|
|
351
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
352
|
-
skinnedNrm += (r3 * normal) * w;
|
|
353
|
-
}
|
|
354
|
-
let worldPos = skinnedPos.xyz;
|
|
355
|
-
let worldNormal = normalize(skinnedNrm);
|
|
356
|
-
|
|
357
|
-
// MMD invert hull: expand vertices outward along normals
|
|
358
|
-
let scaleFactor = 0.01;
|
|
359
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
360
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
361
|
-
return output;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
365
|
-
var color = material.edgeColor;
|
|
366
|
-
|
|
367
|
-
if (material.isOverEyes > 0.5) {
|
|
368
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return color;
|
|
372
|
-
}
|
|
305
|
+
code: /* wgsl */ `
|
|
306
|
+
struct CameraUniforms {
|
|
307
|
+
view: mat4x4f,
|
|
308
|
+
projection: mat4x4f,
|
|
309
|
+
viewPos: vec3f,
|
|
310
|
+
_padding: f32,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
struct MaterialUniforms {
|
|
314
|
+
edgeColor: vec4f,
|
|
315
|
+
edgeSize: f32,
|
|
316
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
317
|
+
_padding1: f32,
|
|
318
|
+
_padding2: f32,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
322
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
323
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
324
|
+
|
|
325
|
+
struct VertexOutput {
|
|
326
|
+
@builtin(position) position: vec4f,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
@vertex fn vs(
|
|
330
|
+
@location(0) position: vec3f,
|
|
331
|
+
@location(1) normal: vec3f,
|
|
332
|
+
@location(3) joints0: vec4<u32>,
|
|
333
|
+
@location(4) weights0: vec4<f32>
|
|
334
|
+
) -> VertexOutput {
|
|
335
|
+
var output: VertexOutput;
|
|
336
|
+
let pos4 = vec4f(position, 1.0);
|
|
337
|
+
|
|
338
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
339
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
340
|
+
var normalizedWeights: vec4f;
|
|
341
|
+
if (weightSum > 0.0001) {
|
|
342
|
+
normalizedWeights = weights0 / weightSum;
|
|
343
|
+
} else {
|
|
344
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
348
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
349
|
+
for (var i = 0u; i < 4u; i++) {
|
|
350
|
+
let j = joints0[i];
|
|
351
|
+
let w = normalizedWeights[i];
|
|
352
|
+
let m = skinMats[j];
|
|
353
|
+
skinnedPos += (m * pos4) * w;
|
|
354
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
355
|
+
skinnedNrm += (r3 * normal) * w;
|
|
356
|
+
}
|
|
357
|
+
let worldPos = skinnedPos.xyz;
|
|
358
|
+
let worldNormal = normalize(skinnedNrm);
|
|
359
|
+
|
|
360
|
+
// MMD invert hull: expand vertices outward along normals
|
|
361
|
+
let scaleFactor = 0.01;
|
|
362
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
363
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
364
|
+
return output;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
368
|
+
var color = material.edgeColor;
|
|
369
|
+
|
|
370
|
+
if (material.isOverEyes > 0.5) {
|
|
371
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return color;
|
|
375
|
+
}
|
|
373
376
|
`,
|
|
374
377
|
});
|
|
375
378
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -435,9 +438,9 @@ export class Engine {
|
|
|
435
438
|
count: this.sampleCount,
|
|
436
439
|
},
|
|
437
440
|
});
|
|
438
|
-
//
|
|
439
|
-
this.
|
|
440
|
-
label: "
|
|
441
|
+
// Hair outline pipeline
|
|
442
|
+
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
443
|
+
label: "hair outline pipeline",
|
|
441
444
|
layout: outlinePipelineLayout,
|
|
442
445
|
vertex: {
|
|
443
446
|
module: outlineShaderModule,
|
|
@@ -505,7 +508,7 @@ export class Engine {
|
|
|
505
508
|
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
506
509
|
this.eyePipeline = this.device.createRenderPipeline({
|
|
507
510
|
label: "eye overlay pipeline",
|
|
508
|
-
layout:
|
|
511
|
+
layout: mainPipelineLayout,
|
|
509
512
|
vertex: {
|
|
510
513
|
module: shaderModule,
|
|
511
514
|
buffers: [
|
|
@@ -570,55 +573,55 @@ export class Engine {
|
|
|
570
573
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
571
574
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
572
575
|
label: "depth only shader",
|
|
573
|
-
code: /* wgsl */ `
|
|
574
|
-
struct CameraUniforms {
|
|
575
|
-
view: mat4x4f,
|
|
576
|
-
projection: mat4x4f,
|
|
577
|
-
viewPos: vec3f,
|
|
578
|
-
_padding: f32,
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
582
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
583
|
-
|
|
584
|
-
@vertex fn vs(
|
|
585
|
-
@location(0) position: vec3f,
|
|
586
|
-
@location(1) normal: vec3f,
|
|
587
|
-
@location(3) joints0: vec4<u32>,
|
|
588
|
-
@location(4) weights0: vec4<f32>
|
|
589
|
-
) -> @builtin(position) vec4f {
|
|
590
|
-
let pos4 = vec4f(position, 1.0);
|
|
591
|
-
|
|
592
|
-
// Normalize weights
|
|
593
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
594
|
-
var normalizedWeights: vec4f;
|
|
595
|
-
if (weightSum > 0.0001) {
|
|
596
|
-
normalizedWeights = weights0 / weightSum;
|
|
597
|
-
} else {
|
|
598
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
602
|
-
for (var i = 0u; i < 4u; i++) {
|
|
603
|
-
let j = joints0[i];
|
|
604
|
-
let w = normalizedWeights[i];
|
|
605
|
-
let m = skinMats[j];
|
|
606
|
-
skinnedPos += (m * pos4) * w;
|
|
607
|
-
}
|
|
608
|
-
let worldPos = skinnedPos.xyz;
|
|
609
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
610
|
-
return clipPos;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
614
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
615
|
-
}
|
|
576
|
+
code: /* wgsl */ `
|
|
577
|
+
struct CameraUniforms {
|
|
578
|
+
view: mat4x4f,
|
|
579
|
+
projection: mat4x4f,
|
|
580
|
+
viewPos: vec3f,
|
|
581
|
+
_padding: f32,
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
585
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
586
|
+
|
|
587
|
+
@vertex fn vs(
|
|
588
|
+
@location(0) position: vec3f,
|
|
589
|
+
@location(1) normal: vec3f,
|
|
590
|
+
@location(3) joints0: vec4<u32>,
|
|
591
|
+
@location(4) weights0: vec4<f32>
|
|
592
|
+
) -> @builtin(position) vec4f {
|
|
593
|
+
let pos4 = vec4f(position, 1.0);
|
|
594
|
+
|
|
595
|
+
// Normalize weights
|
|
596
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
597
|
+
var normalizedWeights: vec4f;
|
|
598
|
+
if (weightSum > 0.0001) {
|
|
599
|
+
normalizedWeights = weights0 / weightSum;
|
|
600
|
+
} else {
|
|
601
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
605
|
+
for (var i = 0u; i < 4u; i++) {
|
|
606
|
+
let j = joints0[i];
|
|
607
|
+
let w = normalizedWeights[i];
|
|
608
|
+
let m = skinMats[j];
|
|
609
|
+
skinnedPos += (m * pos4) * w;
|
|
610
|
+
}
|
|
611
|
+
let worldPos = skinnedPos.xyz;
|
|
612
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
613
|
+
return clipPos;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
617
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
618
|
+
}
|
|
616
619
|
`,
|
|
617
620
|
});
|
|
618
621
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
619
622
|
this.hairDepthPipeline = this.device.createRenderPipeline({
|
|
620
623
|
label: "hair depth pre-pass",
|
|
621
|
-
layout:
|
|
624
|
+
layout: mainPipelineLayout,
|
|
622
625
|
vertex: {
|
|
623
626
|
module: depthOnlyShaderModule,
|
|
624
627
|
buffers: [
|
|
@@ -657,10 +660,10 @@ export class Engine {
|
|
|
657
660
|
},
|
|
658
661
|
multisample: { count: this.sampleCount },
|
|
659
662
|
});
|
|
660
|
-
//
|
|
661
|
-
this.
|
|
662
|
-
label: "
|
|
663
|
-
layout:
|
|
663
|
+
// Hair pipeline for rendering over eyes (stencil == 1)
|
|
664
|
+
this.hairPipelineOverEyes = this.device.createRenderPipeline({
|
|
665
|
+
label: "hair pipeline (over eyes)",
|
|
666
|
+
layout: mainPipelineLayout,
|
|
664
667
|
vertex: {
|
|
665
668
|
module: shaderModule,
|
|
666
669
|
buffers: [
|
|
@@ -722,10 +725,10 @@ export class Engine {
|
|
|
722
725
|
},
|
|
723
726
|
multisample: { count: this.sampleCount },
|
|
724
727
|
});
|
|
725
|
-
//
|
|
726
|
-
this.
|
|
727
|
-
label: "
|
|
728
|
-
layout:
|
|
728
|
+
// Hair pipeline for rendering over non-eyes (stencil != 1)
|
|
729
|
+
this.hairPipelineOverNonEyes = this.device.createRenderPipeline({
|
|
730
|
+
label: "hair pipeline (over non-eyes)",
|
|
731
|
+
layout: mainPipelineLayout,
|
|
729
732
|
vertex: {
|
|
730
733
|
module: shaderModule,
|
|
731
734
|
buffers: [
|
|
@@ -792,31 +795,30 @@ export class Engine {
|
|
|
792
795
|
createSkinMatrixComputePipeline() {
|
|
793
796
|
const computeShader = this.device.createShaderModule({
|
|
794
797
|
label: "skin matrix compute",
|
|
795
|
-
code: /* wgsl */ `
|
|
796
|
-
struct BoneCountUniform {
|
|
797
|
-
count: u32,
|
|
798
|
-
_padding1: u32,
|
|
799
|
-
_padding2: u32,
|
|
800
|
-
_padding3: u32,
|
|
801
|
-
_padding4: vec4<u32>,
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
805
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
806
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
807
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
808
|
-
|
|
809
|
-
@compute @workgroup_size(64)
|
|
810
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
811
|
-
let boneIndex = globalId.x;
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
let
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
798
|
+
code: /* wgsl */ `
|
|
799
|
+
struct BoneCountUniform {
|
|
800
|
+
count: u32,
|
|
801
|
+
_padding1: u32,
|
|
802
|
+
_padding2: u32,
|
|
803
|
+
_padding3: u32,
|
|
804
|
+
_padding4: vec4<u32>,
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
808
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
809
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
810
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
811
|
+
|
|
812
|
+
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
813
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
814
|
+
let boneIndex = globalId.x;
|
|
815
|
+
if (boneIndex >= boneCount.count) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
let worldMat = worldMatrices[boneIndex];
|
|
819
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
820
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
821
|
+
}
|
|
820
822
|
`,
|
|
821
823
|
});
|
|
822
824
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -870,143 +872,138 @@ export class Engine {
|
|
|
870
872
|
// Bloom extraction shader (extracts bright areas)
|
|
871
873
|
const bloomExtractShader = this.device.createShaderModule({
|
|
872
874
|
label: "bloom extract",
|
|
873
|
-
code: /* wgsl */ `
|
|
874
|
-
struct VertexOutput {
|
|
875
|
-
@builtin(position) position: vec4f,
|
|
876
|
-
@location(0) uv: vec2f,
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
880
|
-
var output: VertexOutput;
|
|
881
|
-
// Generate fullscreen quad from vertex index
|
|
882
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
883
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
884
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
885
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
886
|
-
return output;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
struct BloomExtractUniforms {
|
|
890
|
-
threshold: f32,
|
|
891
|
-
_padding1: f32,
|
|
892
|
-
_padding2: f32,
|
|
893
|
-
_padding3: f32,
|
|
894
|
-
_padding4: f32,
|
|
895
|
-
_padding5: f32,
|
|
896
|
-
_padding6: f32,
|
|
897
|
-
_padding7: f32,
|
|
898
|
-
};
|
|
899
|
-
|
|
900
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
901
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
902
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
903
|
-
|
|
904
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
905
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
906
|
-
// Extract bright areas above threshold
|
|
907
|
-
let threshold = extractUniforms.threshold;
|
|
908
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
909
|
-
return vec4f(bloom, color.a);
|
|
910
|
-
}
|
|
875
|
+
code: /* wgsl */ `
|
|
876
|
+
struct VertexOutput {
|
|
877
|
+
@builtin(position) position: vec4f,
|
|
878
|
+
@location(0) uv: vec2f,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
882
|
+
var output: VertexOutput;
|
|
883
|
+
// Generate fullscreen quad from vertex index
|
|
884
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
885
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
886
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
887
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
888
|
+
return output;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
struct BloomExtractUniforms {
|
|
892
|
+
threshold: f32,
|
|
893
|
+
_padding1: f32,
|
|
894
|
+
_padding2: f32,
|
|
895
|
+
_padding3: f32,
|
|
896
|
+
_padding4: f32,
|
|
897
|
+
_padding5: f32,
|
|
898
|
+
_padding6: f32,
|
|
899
|
+
_padding7: f32,
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
903
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
904
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
905
|
+
|
|
906
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
907
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
908
|
+
// Extract bright areas above threshold
|
|
909
|
+
let threshold = extractUniforms.threshold;
|
|
910
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
911
|
+
return vec4f(bloom, color.a);
|
|
912
|
+
}
|
|
911
913
|
`,
|
|
912
914
|
});
|
|
913
915
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
914
916
|
const bloomBlurShader = this.device.createShaderModule({
|
|
915
917
|
label: "bloom blur",
|
|
916
|
-
code: /* wgsl */ `
|
|
917
|
-
struct VertexOutput {
|
|
918
|
-
@builtin(position) position: vec4f,
|
|
919
|
-
@location(0) uv: vec2f,
|
|
920
|
-
};
|
|
921
|
-
|
|
922
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
923
|
-
var output: VertexOutput;
|
|
924
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
925
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
926
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
927
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
928
|
-
return output;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
struct BlurUniforms {
|
|
932
|
-
direction: vec2f,
|
|
933
|
-
_padding1: f32,
|
|
934
|
-
_padding2: f32,
|
|
935
|
-
_padding3: f32,
|
|
936
|
-
_padding4: f32,
|
|
937
|
-
_padding5: f32,
|
|
938
|
-
_padding6: f32,
|
|
939
|
-
};
|
|
940
|
-
|
|
941
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
942
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
943
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
944
|
-
|
|
945
|
-
//
|
|
946
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
947
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
948
|
-
var result = vec4f(0.0);
|
|
949
|
-
|
|
950
|
-
// Gaussian
|
|
951
|
-
let weights = array<f32,
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
return result;
|
|
965
|
-
}
|
|
918
|
+
code: /* wgsl */ `
|
|
919
|
+
struct VertexOutput {
|
|
920
|
+
@builtin(position) position: vec4f,
|
|
921
|
+
@location(0) uv: vec2f,
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
925
|
+
var output: VertexOutput;
|
|
926
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
927
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
928
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
929
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
930
|
+
return output;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
struct BlurUniforms {
|
|
934
|
+
direction: vec2f,
|
|
935
|
+
_padding1: f32,
|
|
936
|
+
_padding2: f32,
|
|
937
|
+
_padding3: f32,
|
|
938
|
+
_padding4: f32,
|
|
939
|
+
_padding5: f32,
|
|
940
|
+
_padding6: f32,
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
944
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
945
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
946
|
+
|
|
947
|
+
// 5-tap gaussian blur
|
|
948
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
949
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
950
|
+
var result = vec4f(0.0);
|
|
951
|
+
|
|
952
|
+
// Optimized 5-tap Gaussian filter (faster, nearly same quality)
|
|
953
|
+
let weights = array<f32, 5>(0.06136, 0.24477, 0.38774, 0.24477, 0.06136);
|
|
954
|
+
let offsets = array<f32, 5>(-2.0, -1.0, 0.0, 1.0, 2.0);
|
|
955
|
+
|
|
956
|
+
for (var i = 0u; i < 5u; i++) {
|
|
957
|
+
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
958
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
966
963
|
`,
|
|
967
964
|
});
|
|
968
965
|
// Bloom composition shader (combines original scene with bloom)
|
|
969
966
|
const bloomComposeShader = this.device.createShaderModule({
|
|
970
967
|
label: "bloom compose",
|
|
971
|
-
code: /* wgsl */ `
|
|
972
|
-
struct VertexOutput {
|
|
973
|
-
@builtin(position) position: vec4f,
|
|
974
|
-
@location(0) uv: vec2f,
|
|
975
|
-
};
|
|
976
|
-
|
|
977
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
978
|
-
var output: VertexOutput;
|
|
979
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
980
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
981
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
982
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
983
|
-
return output;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
struct BloomComposeUniforms {
|
|
987
|
-
intensity: f32,
|
|
988
|
-
_padding1: f32,
|
|
989
|
-
_padding2: f32,
|
|
990
|
-
_padding3: f32,
|
|
991
|
-
_padding4: f32,
|
|
992
|
-
_padding5: f32,
|
|
993
|
-
_padding6: f32,
|
|
994
|
-
_padding7: f32,
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
998
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
999
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1000
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1001
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1002
|
-
|
|
1003
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1004
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1005
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1006
|
-
// Additive blending with intensity control
|
|
1007
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1008
|
-
return vec4f(result, scene.a);
|
|
1009
|
-
}
|
|
968
|
+
code: /* wgsl */ `
|
|
969
|
+
struct VertexOutput {
|
|
970
|
+
@builtin(position) position: vec4f,
|
|
971
|
+
@location(0) uv: vec2f,
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
975
|
+
var output: VertexOutput;
|
|
976
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
977
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
978
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
979
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
980
|
+
return output;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
struct BloomComposeUniforms {
|
|
984
|
+
intensity: f32,
|
|
985
|
+
_padding1: f32,
|
|
986
|
+
_padding2: f32,
|
|
987
|
+
_padding3: f32,
|
|
988
|
+
_padding4: f32,
|
|
989
|
+
_padding5: f32,
|
|
990
|
+
_padding6: f32,
|
|
991
|
+
_padding7: f32,
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
995
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
996
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
997
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
998
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
999
|
+
|
|
1000
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1001
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1002
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1003
|
+
// Additive blending with intensity control
|
|
1004
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1005
|
+
return vec4f(result, scene.a);
|
|
1006
|
+
}
|
|
1010
1007
|
`,
|
|
1011
1008
|
});
|
|
1012
1009
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1092,11 +1089,9 @@ export class Engine {
|
|
|
1092
1089
|
this.bloomThresholdBuffer = bloomThresholdBuffer;
|
|
1093
1090
|
this.linearSampler = linearSampler;
|
|
1094
1091
|
}
|
|
1095
|
-
// Setup bloom textures and bind groups (called when canvas is resized)
|
|
1096
1092
|
setupBloom(width, height) {
|
|
1097
|
-
|
|
1098
|
-
const
|
|
1099
|
-
const bloomHeight = Math.floor(height / 2);
|
|
1093
|
+
const bloomWidth = Math.floor(width / this.BLOOM_DOWNSCALE_FACTOR);
|
|
1094
|
+
const bloomHeight = Math.floor(height / this.BLOOM_DOWNSCALE_FACTOR);
|
|
1100
1095
|
this.bloomExtractTexture = this.device.createTexture({
|
|
1101
1096
|
label: "bloom extract",
|
|
1102
1097
|
size: [bloomWidth, bloomHeight],
|
|
@@ -1331,7 +1326,9 @@ export class Engine {
|
|
|
1331
1326
|
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1332
1327
|
// Upload matrices immediately so next frame shows correct pose
|
|
1333
1328
|
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1334
|
-
this.
|
|
1329
|
+
const encoder = this.device.createCommandEncoder();
|
|
1330
|
+
this.computeSkinMatrices(encoder);
|
|
1331
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1335
1332
|
}
|
|
1336
1333
|
}
|
|
1337
1334
|
for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
@@ -1540,7 +1537,6 @@ export class Engine {
|
|
|
1540
1537
|
});
|
|
1541
1538
|
this.device.queue.writeTexture({ texture: defaultToonTexture }, defaultToonData, { bytesPerRow: 256 * 4 }, [256, 2]);
|
|
1542
1539
|
this.textureCache.set(defaultToonPath, defaultToonTexture);
|
|
1543
|
-
this.textureSizes.set(defaultToonPath, { width: 256, height: 2 });
|
|
1544
1540
|
return defaultToonTexture;
|
|
1545
1541
|
};
|
|
1546
1542
|
this.opaqueNonEyeNonHairDraws = [];
|
|
@@ -1552,10 +1548,10 @@ export class Engine {
|
|
|
1552
1548
|
this.eyeOutlineDraws = [];
|
|
1553
1549
|
this.hairOutlineDraws = [];
|
|
1554
1550
|
this.transparentNonEyeNonHairOutlineDraws = [];
|
|
1555
|
-
let
|
|
1551
|
+
let currentIndexOffset = 0;
|
|
1556
1552
|
for (const mat of materials) {
|
|
1557
|
-
const
|
|
1558
|
-
if (
|
|
1553
|
+
const indexCount = mat.vertexCount;
|
|
1554
|
+
if (indexCount === 0)
|
|
1559
1555
|
continue;
|
|
1560
1556
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex);
|
|
1561
1557
|
if (!diffuseTexture)
|
|
@@ -1583,7 +1579,7 @@ export class Engine {
|
|
|
1583
1579
|
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1584
1580
|
const bindGroup = this.device.createBindGroup({
|
|
1585
1581
|
label: `material bind group: ${mat.name}`,
|
|
1586
|
-
layout: this.
|
|
1582
|
+
layout: this.mainBindGroupLayout,
|
|
1587
1583
|
entries: [
|
|
1588
1584
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1589
1585
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
@@ -1598,100 +1594,77 @@ export class Engine {
|
|
|
1598
1594
|
// Classify materials into appropriate draw lists
|
|
1599
1595
|
if (mat.isEye) {
|
|
1600
1596
|
this.eyeDraws.push({
|
|
1601
|
-
count:
|
|
1602
|
-
firstIndex:
|
|
1597
|
+
count: indexCount,
|
|
1598
|
+
firstIndex: currentIndexOffset,
|
|
1603
1599
|
bindGroup,
|
|
1604
1600
|
isTransparent,
|
|
1605
1601
|
});
|
|
1606
1602
|
}
|
|
1607
1603
|
else if (mat.isHair) {
|
|
1608
|
-
// Hair materials: create bind groups for
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1643
|
-
{ binding: 3, resource: this.textureSampler },
|
|
1644
|
-
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1645
|
-
{ binding: 5, resource: toonTexture.createView() },
|
|
1646
|
-
{ binding: 6, resource: this.textureSampler },
|
|
1647
|
-
{ binding: 7, resource: { buffer: materialUniformBufferOverEyes } },
|
|
1648
|
-
],
|
|
1649
|
-
});
|
|
1650
|
-
const bindGroupOverNonEyes = this.device.createBindGroup({
|
|
1651
|
-
label: `material bind group (over non-eyes): ${mat.name}`,
|
|
1652
|
-
layout: this.hairBindGroupLayout,
|
|
1653
|
-
entries: [
|
|
1654
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1655
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1656
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1657
|
-
{ binding: 3, resource: this.textureSampler },
|
|
1658
|
-
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1659
|
-
{ binding: 5, resource: toonTexture.createView() },
|
|
1660
|
-
{ binding: 6, resource: this.textureSampler },
|
|
1661
|
-
{ binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
|
|
1662
|
-
],
|
|
1663
|
-
});
|
|
1664
|
-
// Store both bind groups for unified pipeline
|
|
1604
|
+
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
1605
|
+
const createHairBindGroup = (isOverEyes) => {
|
|
1606
|
+
const uniformData = new Float32Array(8);
|
|
1607
|
+
uniformData[0] = materialAlpha;
|
|
1608
|
+
uniformData[1] = 1.0; // alphaMultiplier (shader adjusts based on isOverEyes)
|
|
1609
|
+
uniformData[2] = this.rimLightIntensity;
|
|
1610
|
+
uniformData[3] = this.rimLightPower;
|
|
1611
|
+
uniformData[4] = 1.0; // rimColor.rgb
|
|
1612
|
+
uniformData[5] = 1.0;
|
|
1613
|
+
uniformData[6] = 1.0;
|
|
1614
|
+
uniformData[7] = isOverEyes ? 1.0 : 0.0;
|
|
1615
|
+
const buffer = this.device.createBuffer({
|
|
1616
|
+
label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
1617
|
+
size: uniformData.byteLength,
|
|
1618
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1619
|
+
});
|
|
1620
|
+
this.device.queue.writeBuffer(buffer, 0, uniformData);
|
|
1621
|
+
return this.device.createBindGroup({
|
|
1622
|
+
label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
1623
|
+
layout: this.mainBindGroupLayout,
|
|
1624
|
+
entries: [
|
|
1625
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1626
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1627
|
+
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1628
|
+
{ binding: 3, resource: this.textureSampler },
|
|
1629
|
+
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1630
|
+
{ binding: 5, resource: toonTexture.createView() },
|
|
1631
|
+
{ binding: 6, resource: this.textureSampler },
|
|
1632
|
+
{ binding: 7, resource: { buffer: buffer } },
|
|
1633
|
+
],
|
|
1634
|
+
});
|
|
1635
|
+
};
|
|
1636
|
+
const bindGroupOverEyes = createHairBindGroup(true);
|
|
1637
|
+
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1665
1638
|
this.hairDrawsOverEyes.push({
|
|
1666
|
-
count:
|
|
1667
|
-
firstIndex:
|
|
1639
|
+
count: indexCount,
|
|
1640
|
+
firstIndex: currentIndexOffset,
|
|
1668
1641
|
bindGroup: bindGroupOverEyes,
|
|
1669
1642
|
isTransparent,
|
|
1670
1643
|
});
|
|
1671
1644
|
this.hairDrawsOverNonEyes.push({
|
|
1672
|
-
count:
|
|
1673
|
-
firstIndex:
|
|
1645
|
+
count: indexCount,
|
|
1646
|
+
firstIndex: currentIndexOffset,
|
|
1674
1647
|
bindGroup: bindGroupOverNonEyes,
|
|
1675
1648
|
isTransparent,
|
|
1676
1649
|
});
|
|
1677
1650
|
}
|
|
1678
1651
|
else if (isTransparent) {
|
|
1679
1652
|
this.transparentNonEyeNonHairDraws.push({
|
|
1680
|
-
count:
|
|
1681
|
-
firstIndex:
|
|
1653
|
+
count: indexCount,
|
|
1654
|
+
firstIndex: currentIndexOffset,
|
|
1682
1655
|
bindGroup,
|
|
1683
1656
|
isTransparent,
|
|
1684
1657
|
});
|
|
1685
1658
|
}
|
|
1686
1659
|
else {
|
|
1687
1660
|
this.opaqueNonEyeNonHairDraws.push({
|
|
1688
|
-
count:
|
|
1689
|
-
firstIndex:
|
|
1661
|
+
count: indexCount,
|
|
1662
|
+
firstIndex: currentIndexOffset,
|
|
1690
1663
|
bindGroup,
|
|
1691
1664
|
isTransparent,
|
|
1692
1665
|
});
|
|
1693
1666
|
}
|
|
1694
|
-
//
|
|
1667
|
+
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1695
1668
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1696
1669
|
const materialUniformData = new Float32Array(8);
|
|
1697
1670
|
materialUniformData[0] = mat.edgeColor[0]; // edgeColor.r
|
|
@@ -1699,9 +1672,9 @@ export class Engine {
|
|
|
1699
1672
|
materialUniformData[2] = mat.edgeColor[2]; // edgeColor.b
|
|
1700
1673
|
materialUniformData[3] = mat.edgeColor[3]; // edgeColor.a
|
|
1701
1674
|
materialUniformData[4] = mat.edgeSize;
|
|
1702
|
-
materialUniformData[5] = 0.0; // isOverEyes
|
|
1703
|
-
materialUniformData[6] = 0.0;
|
|
1704
|
-
materialUniformData[7] = 0.0;
|
|
1675
|
+
materialUniformData[5] = 0.0; // isOverEyes
|
|
1676
|
+
materialUniformData[6] = 0.0;
|
|
1677
|
+
materialUniformData[7] = 0.0;
|
|
1705
1678
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1706
1679
|
label: `outline material uniform: ${mat.name}`,
|
|
1707
1680
|
size: materialUniformData.byteLength,
|
|
@@ -1717,45 +1690,44 @@ export class Engine {
|
|
|
1717
1690
|
{ binding: 2, resource: { buffer: this.skinMatrixBuffer } },
|
|
1718
1691
|
],
|
|
1719
1692
|
});
|
|
1720
|
-
// Classify outlines into appropriate draw lists
|
|
1721
1693
|
if (mat.isEye) {
|
|
1722
1694
|
this.eyeOutlineDraws.push({
|
|
1723
|
-
count:
|
|
1724
|
-
firstIndex:
|
|
1695
|
+
count: indexCount,
|
|
1696
|
+
firstIndex: currentIndexOffset,
|
|
1725
1697
|
bindGroup: outlineBindGroup,
|
|
1726
1698
|
isTransparent,
|
|
1727
1699
|
});
|
|
1728
1700
|
}
|
|
1729
1701
|
else if (mat.isHair) {
|
|
1730
1702
|
this.hairOutlineDraws.push({
|
|
1731
|
-
count:
|
|
1732
|
-
firstIndex:
|
|
1703
|
+
count: indexCount,
|
|
1704
|
+
firstIndex: currentIndexOffset,
|
|
1733
1705
|
bindGroup: outlineBindGroup,
|
|
1734
1706
|
isTransparent,
|
|
1735
1707
|
});
|
|
1736
1708
|
}
|
|
1737
1709
|
else if (isTransparent) {
|
|
1738
1710
|
this.transparentNonEyeNonHairOutlineDraws.push({
|
|
1739
|
-
count:
|
|
1740
|
-
firstIndex:
|
|
1711
|
+
count: indexCount,
|
|
1712
|
+
firstIndex: currentIndexOffset,
|
|
1741
1713
|
bindGroup: outlineBindGroup,
|
|
1742
1714
|
isTransparent,
|
|
1743
1715
|
});
|
|
1744
1716
|
}
|
|
1745
1717
|
else {
|
|
1746
1718
|
this.opaqueNonEyeNonHairOutlineDraws.push({
|
|
1747
|
-
count:
|
|
1748
|
-
firstIndex:
|
|
1719
|
+
count: indexCount,
|
|
1720
|
+
firstIndex: currentIndexOffset,
|
|
1749
1721
|
bindGroup: outlineBindGroup,
|
|
1750
1722
|
isTransparent,
|
|
1751
1723
|
});
|
|
1752
1724
|
}
|
|
1753
1725
|
}
|
|
1754
|
-
|
|
1726
|
+
currentIndexOffset += indexCount;
|
|
1755
1727
|
}
|
|
1728
|
+
this.gpuMemoryMB = this.calculateGpuMemory();
|
|
1756
1729
|
}
|
|
1757
|
-
|
|
1758
|
-
async createTextureFromPath(path, maxSize = 2048) {
|
|
1730
|
+
async createTextureFromPath(path) {
|
|
1759
1731
|
const cached = this.textureCache.get(path);
|
|
1760
1732
|
if (cached) {
|
|
1761
1733
|
return cached;
|
|
@@ -1765,41 +1737,28 @@ export class Engine {
|
|
|
1765
1737
|
if (!response.ok) {
|
|
1766
1738
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1767
1739
|
}
|
|
1768
|
-
|
|
1740
|
+
const imageBitmap = await createImageBitmap(await response.blob(), {
|
|
1769
1741
|
premultiplyAlpha: "none",
|
|
1770
1742
|
colorSpaceConversion: "none",
|
|
1771
1743
|
});
|
|
1772
|
-
// Downscale if texture is too large
|
|
1773
|
-
let finalWidth = imageBitmap.width;
|
|
1774
|
-
let finalHeight = imageBitmap.height;
|
|
1775
|
-
if (finalWidth > maxSize || finalHeight > maxSize) {
|
|
1776
|
-
const scale = Math.min(maxSize / finalWidth, maxSize / finalHeight);
|
|
1777
|
-
finalWidth = Math.floor(finalWidth * scale);
|
|
1778
|
-
finalHeight = Math.floor(finalHeight * scale);
|
|
1779
|
-
// Create canvas to downscale
|
|
1780
|
-
const canvas = new OffscreenCanvas(finalWidth, finalHeight);
|
|
1781
|
-
const ctx = canvas.getContext("2d");
|
|
1782
|
-
if (ctx) {
|
|
1783
|
-
ctx.drawImage(imageBitmap, 0, 0, finalWidth, finalHeight);
|
|
1784
|
-
imageBitmap = await createImageBitmap(canvas);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
1744
|
const texture = this.device.createTexture({
|
|
1788
1745
|
label: `texture: ${path}`,
|
|
1789
|
-
size: [
|
|
1746
|
+
size: [imageBitmap.width, imageBitmap.height],
|
|
1790
1747
|
format: "rgba8unorm",
|
|
1791
1748
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1792
1749
|
});
|
|
1793
|
-
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
|
|
1750
|
+
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
|
|
1751
|
+
imageBitmap.width,
|
|
1752
|
+
imageBitmap.height,
|
|
1753
|
+
]);
|
|
1794
1754
|
this.textureCache.set(path, texture);
|
|
1795
|
-
this.textureSizes.set(path, { width: finalWidth, height: finalHeight });
|
|
1796
1755
|
return texture;
|
|
1797
1756
|
}
|
|
1798
1757
|
catch {
|
|
1799
1758
|
return null;
|
|
1800
1759
|
}
|
|
1801
1760
|
}
|
|
1802
|
-
//
|
|
1761
|
+
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
|
|
1803
1762
|
render() {
|
|
1804
1763
|
if (this.multisampleTexture && this.camera && this.device && this.currentModel) {
|
|
1805
1764
|
const currentTime = performance.now();
|
|
@@ -1807,16 +1766,17 @@ export class Engine {
|
|
|
1807
1766
|
this.lastFrameTime = currentTime;
|
|
1808
1767
|
this.updateCameraUniforms();
|
|
1809
1768
|
this.updateRenderTarget();
|
|
1810
|
-
|
|
1769
|
+
// Use single encoder for both compute and render (reduces sync points)
|
|
1811
1770
|
const encoder = this.device.createCommandEncoder();
|
|
1771
|
+
this.updateModelPose(deltaTime, encoder);
|
|
1812
1772
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1813
1773
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1814
1774
|
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
1815
1775
|
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1816
1776
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1817
1777
|
this.drawCallCount = 0;
|
|
1818
|
-
//
|
|
1819
|
-
pass.setPipeline(this.
|
|
1778
|
+
// Pass 1: Opaque non-eye, non-hair
|
|
1779
|
+
pass.setPipeline(this.modelPipeline);
|
|
1820
1780
|
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
1821
1781
|
if (draw.count > 0) {
|
|
1822
1782
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -1824,9 +1784,9 @@ export class Engine {
|
|
|
1824
1784
|
this.drawCallCount++;
|
|
1825
1785
|
}
|
|
1826
1786
|
}
|
|
1827
|
-
//
|
|
1787
|
+
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1828
1788
|
pass.setPipeline(this.eyePipeline);
|
|
1829
|
-
pass.setStencilReference(
|
|
1789
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1830
1790
|
for (const draw of this.eyeDraws) {
|
|
1831
1791
|
if (draw.count > 0) {
|
|
1832
1792
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -1834,9 +1794,9 @@ export class Engine {
|
|
|
1834
1794
|
this.drawCallCount++;
|
|
1835
1795
|
}
|
|
1836
1796
|
}
|
|
1837
|
-
//
|
|
1797
|
+
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1838
1798
|
this.drawOutlines(pass, false);
|
|
1839
|
-
// 3a: Hair depth pre-pass (
|
|
1799
|
+
// 3a: Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1840
1800
|
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
1841
1801
|
pass.setPipeline(this.hairDepthPipeline);
|
|
1842
1802
|
for (const draw of this.hairDrawsOverEyes) {
|
|
@@ -1852,10 +1812,10 @@ export class Engine {
|
|
|
1852
1812
|
}
|
|
1853
1813
|
}
|
|
1854
1814
|
}
|
|
1855
|
-
// 3b: Hair shading
|
|
1815
|
+
// 3b: Hair shading (split by stencil for transparency over eyes)
|
|
1856
1816
|
if (this.hairDrawsOverEyes.length > 0) {
|
|
1857
|
-
pass.setPipeline(this.
|
|
1858
|
-
pass.setStencilReference(
|
|
1817
|
+
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1818
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1859
1819
|
for (const draw of this.hairDrawsOverEyes) {
|
|
1860
1820
|
if (draw.count > 0) {
|
|
1861
1821
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -1865,8 +1825,8 @@ export class Engine {
|
|
|
1865
1825
|
}
|
|
1866
1826
|
}
|
|
1867
1827
|
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1868
|
-
pass.setPipeline(this.
|
|
1869
|
-
pass.setStencilReference(
|
|
1828
|
+
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1829
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1870
1830
|
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1871
1831
|
if (draw.count > 0) {
|
|
1872
1832
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -1875,9 +1835,9 @@ export class Engine {
|
|
|
1875
1835
|
}
|
|
1876
1836
|
}
|
|
1877
1837
|
}
|
|
1878
|
-
// 3c: Hair outlines
|
|
1838
|
+
// 3c: Hair outlines
|
|
1879
1839
|
if (this.hairOutlineDraws.length > 0) {
|
|
1880
|
-
pass.setPipeline(this.
|
|
1840
|
+
pass.setPipeline(this.hairOutlinePipeline);
|
|
1881
1841
|
for (const draw of this.hairOutlineDraws) {
|
|
1882
1842
|
if (draw.count > 0) {
|
|
1883
1843
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -1885,8 +1845,8 @@ export class Engine {
|
|
|
1885
1845
|
}
|
|
1886
1846
|
}
|
|
1887
1847
|
}
|
|
1888
|
-
//
|
|
1889
|
-
pass.setPipeline(this.
|
|
1848
|
+
// Pass 4: Transparent non-eye, non-hair
|
|
1849
|
+
pass.setPipeline(this.modelPipeline);
|
|
1890
1850
|
for (const draw of this.transparentNonEyeNonHairDraws) {
|
|
1891
1851
|
if (draw.count > 0) {
|
|
1892
1852
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -1897,12 +1857,10 @@ export class Engine {
|
|
|
1897
1857
|
this.drawOutlines(pass, true);
|
|
1898
1858
|
pass.end();
|
|
1899
1859
|
this.device.queue.submit([encoder.finish()]);
|
|
1900
|
-
// Apply bloom post-processing
|
|
1901
1860
|
this.applyBloom();
|
|
1902
1861
|
this.updateStats(performance.now() - currentTime);
|
|
1903
1862
|
}
|
|
1904
1863
|
}
|
|
1905
|
-
// Apply bloom post-processing
|
|
1906
1864
|
applyBloom() {
|
|
1907
1865
|
if (!this.sceneRenderTexture || !this.bloomExtractTexture) {
|
|
1908
1866
|
return;
|
|
@@ -1917,9 +1875,9 @@ export class Engine {
|
|
|
1917
1875
|
const encoder = this.device.createCommandEncoder();
|
|
1918
1876
|
const width = this.canvas.width;
|
|
1919
1877
|
const height = this.canvas.height;
|
|
1920
|
-
const bloomWidth = Math.floor(width /
|
|
1921
|
-
const bloomHeight = Math.floor(height /
|
|
1922
|
-
//
|
|
1878
|
+
const bloomWidth = Math.floor(width / this.BLOOM_DOWNSCALE_FACTOR);
|
|
1879
|
+
const bloomHeight = Math.floor(height / this.BLOOM_DOWNSCALE_FACTOR);
|
|
1880
|
+
// Extract bright areas
|
|
1923
1881
|
const extractPass = encoder.beginRenderPass({
|
|
1924
1882
|
label: "bloom extract",
|
|
1925
1883
|
colorAttachments: [
|
|
@@ -1935,8 +1893,8 @@ export class Engine {
|
|
|
1935
1893
|
extractPass.setBindGroup(0, this.bloomExtractBindGroup);
|
|
1936
1894
|
extractPass.draw(6, 1, 0, 0);
|
|
1937
1895
|
extractPass.end();
|
|
1938
|
-
//
|
|
1939
|
-
const hBlurData = new Float32Array(4);
|
|
1896
|
+
// Horizontal blur
|
|
1897
|
+
const hBlurData = new Float32Array(4);
|
|
1940
1898
|
hBlurData[0] = 1.0;
|
|
1941
1899
|
hBlurData[1] = 0.0;
|
|
1942
1900
|
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, hBlurData);
|
|
@@ -1955,8 +1913,8 @@ export class Engine {
|
|
|
1955
1913
|
blurHPass.setBindGroup(0, this.bloomBlurHBindGroup);
|
|
1956
1914
|
blurHPass.draw(6, 1, 0, 0);
|
|
1957
1915
|
blurHPass.end();
|
|
1958
|
-
//
|
|
1959
|
-
const vBlurData = new Float32Array(4);
|
|
1916
|
+
// Vertical blur
|
|
1917
|
+
const vBlurData = new Float32Array(4);
|
|
1960
1918
|
vBlurData[0] = 0.0;
|
|
1961
1919
|
vBlurData[1] = 1.0;
|
|
1962
1920
|
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, vBlurData);
|
|
@@ -1975,7 +1933,7 @@ export class Engine {
|
|
|
1975
1933
|
blurVPass.setBindGroup(0, this.bloomBlurVBindGroup);
|
|
1976
1934
|
blurVPass.draw(6, 1, 0, 0);
|
|
1977
1935
|
blurVPass.end();
|
|
1978
|
-
//
|
|
1936
|
+
// Compose to canvas
|
|
1979
1937
|
const composePass = encoder.beginRenderPass({
|
|
1980
1938
|
label: "bloom compose",
|
|
1981
1939
|
colorAttachments: [
|
|
@@ -1993,7 +1951,6 @@ export class Engine {
|
|
|
1993
1951
|
composePass.end();
|
|
1994
1952
|
this.device.queue.submit([encoder.finish()]);
|
|
1995
1953
|
}
|
|
1996
|
-
// Update camera uniform buffer each frame
|
|
1997
1954
|
updateCameraUniforms() {
|
|
1998
1955
|
const viewMatrix = this.camera.getViewMatrix();
|
|
1999
1956
|
const projectionMatrix = this.camera.getProjectionMatrix();
|
|
@@ -2005,47 +1962,36 @@ export class Engine {
|
|
|
2005
1962
|
this.cameraMatrixData[34] = cameraPos.z;
|
|
2006
1963
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
2007
1964
|
}
|
|
2008
|
-
// Update render target texture view
|
|
2009
1965
|
updateRenderTarget() {
|
|
2010
1966
|
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
2011
1967
|
if (this.sampleCount > 1) {
|
|
2012
|
-
// Resolve to scene render texture for post-processing
|
|
2013
1968
|
colorAttachment.resolveTarget = this.sceneRenderTextureView;
|
|
2014
1969
|
}
|
|
2015
1970
|
else {
|
|
2016
|
-
// Render directly to scene render texture
|
|
2017
1971
|
colorAttachment.view = this.sceneRenderTextureView;
|
|
2018
1972
|
}
|
|
2019
1973
|
}
|
|
2020
|
-
updateModelPose(deltaTime) {
|
|
1974
|
+
updateModelPose(deltaTime, encoder) {
|
|
2021
1975
|
this.currentModel.evaluatePose();
|
|
2022
1976
|
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
2023
1977
|
if (this.physics) {
|
|
2024
1978
|
this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
2025
1979
|
}
|
|
2026
1980
|
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
2027
|
-
this.computeSkinMatrices();
|
|
1981
|
+
this.computeSkinMatrices(encoder);
|
|
2028
1982
|
}
|
|
2029
|
-
|
|
2030
|
-
computeSkinMatrices() {
|
|
1983
|
+
computeSkinMatrices(encoder) {
|
|
2031
1984
|
const boneCount = this.currentModel.getSkeleton().bones.length;
|
|
2032
|
-
const
|
|
2033
|
-
// Dispatch exactly enough threads for all bones (no bounds check needed)
|
|
2034
|
-
const workgroupCount = Math.ceil(boneCount / workgroupSize);
|
|
2035
|
-
// Bone count is written once in setupModelBuffers() and never changes
|
|
2036
|
-
const encoder = this.device.createCommandEncoder();
|
|
1985
|
+
const workgroupCount = Math.ceil(boneCount / this.COMPUTE_WORKGROUP_SIZE);
|
|
2037
1986
|
const pass = encoder.beginComputePass();
|
|
2038
1987
|
pass.setPipeline(this.skinMatrixComputePipeline);
|
|
2039
1988
|
pass.setBindGroup(0, this.skinMatrixComputeBindGroup);
|
|
2040
1989
|
pass.dispatchWorkgroups(workgroupCount);
|
|
2041
1990
|
pass.end();
|
|
2042
|
-
this.device.queue.submit([encoder.finish()]);
|
|
2043
1991
|
}
|
|
2044
|
-
// Draw outlines (opaque or transparent)
|
|
2045
1992
|
drawOutlines(pass, transparent) {
|
|
2046
1993
|
pass.setPipeline(this.outlinePipeline);
|
|
2047
1994
|
if (transparent) {
|
|
2048
|
-
// Draw transparent outlines (if any)
|
|
2049
1995
|
for (const draw of this.transparentNonEyeNonHairOutlineDraws) {
|
|
2050
1996
|
if (draw.count > 0) {
|
|
2051
1997
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -2054,7 +2000,6 @@ export class Engine {
|
|
|
2054
2000
|
}
|
|
2055
2001
|
}
|
|
2056
2002
|
else {
|
|
2057
|
-
// Draw opaque outlines before main geometry
|
|
2058
2003
|
for (const draw of this.opaqueNonEyeNonHairOutlineDraws) {
|
|
2059
2004
|
if (draw.count > 0) {
|
|
2060
2005
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -2081,12 +2026,12 @@ export class Engine {
|
|
|
2081
2026
|
this.framesSinceLastUpdate = 0;
|
|
2082
2027
|
this.lastFpsUpdate = now;
|
|
2083
2028
|
}
|
|
2084
|
-
|
|
2029
|
+
this.stats.gpuMemory = this.gpuMemoryMB;
|
|
2030
|
+
}
|
|
2031
|
+
calculateGpuMemory() {
|
|
2085
2032
|
let textureMemoryBytes = 0;
|
|
2086
|
-
for (const
|
|
2087
|
-
|
|
2088
|
-
textureMemoryBytes += size.width * size.height * 4; // RGBA8 = 4 bytes per pixel
|
|
2089
|
-
}
|
|
2033
|
+
for (const texture of this.textureCache.values()) {
|
|
2034
|
+
textureMemoryBytes += texture.width * texture.height * 4;
|
|
2090
2035
|
}
|
|
2091
2036
|
let bufferMemoryBytes = 0;
|
|
2092
2037
|
if (this.vertexBuffer) {
|
|
@@ -2124,48 +2069,44 @@ export class Engine {
|
|
|
2124
2069
|
if (skeleton)
|
|
2125
2070
|
bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4);
|
|
2126
2071
|
}
|
|
2127
|
-
bufferMemoryBytes += 40 * 4;
|
|
2128
|
-
bufferMemoryBytes += 64 * 4;
|
|
2129
|
-
bufferMemoryBytes += 32;
|
|
2130
|
-
bufferMemoryBytes += 32;
|
|
2131
|
-
bufferMemoryBytes += 32;
|
|
2132
|
-
bufferMemoryBytes += 32;
|
|
2072
|
+
bufferMemoryBytes += 40 * 4;
|
|
2073
|
+
bufferMemoryBytes += 64 * 4;
|
|
2074
|
+
bufferMemoryBytes += 32;
|
|
2075
|
+
bufferMemoryBytes += 32;
|
|
2076
|
+
bufferMemoryBytes += 32;
|
|
2077
|
+
bufferMemoryBytes += 32;
|
|
2133
2078
|
if (this.fullscreenQuadBuffer) {
|
|
2134
|
-
bufferMemoryBytes += 24 * 4;
|
|
2079
|
+
bufferMemoryBytes += 24 * 4;
|
|
2135
2080
|
}
|
|
2136
|
-
// Material uniform buffers: Float32Array(8) = 32 bytes each
|
|
2137
2081
|
const totalMaterialDraws = this.opaqueNonEyeNonHairDraws.length +
|
|
2138
2082
|
this.eyeDraws.length +
|
|
2139
2083
|
this.hairDrawsOverEyes.length +
|
|
2140
2084
|
this.hairDrawsOverNonEyes.length +
|
|
2141
2085
|
this.transparentNonEyeNonHairDraws.length;
|
|
2142
|
-
bufferMemoryBytes += totalMaterialDraws * 32;
|
|
2143
|
-
// Outline material uniform buffers: Float32Array(8) = 32 bytes each
|
|
2086
|
+
bufferMemoryBytes += totalMaterialDraws * 32;
|
|
2144
2087
|
const totalOutlineDraws = this.opaqueNonEyeNonHairOutlineDraws.length +
|
|
2145
2088
|
this.eyeOutlineDraws.length +
|
|
2146
2089
|
this.hairOutlineDraws.length +
|
|
2147
2090
|
this.transparentNonEyeNonHairOutlineDraws.length;
|
|
2148
|
-
bufferMemoryBytes += totalOutlineDraws * 32;
|
|
2091
|
+
bufferMemoryBytes += totalOutlineDraws * 32;
|
|
2149
2092
|
let renderTargetMemoryBytes = 0;
|
|
2150
2093
|
if (this.multisampleTexture) {
|
|
2151
2094
|
const width = this.canvas.width;
|
|
2152
2095
|
const height = this.canvas.height;
|
|
2153
|
-
renderTargetMemoryBytes += width * height * 4 * this.sampleCount;
|
|
2154
|
-
renderTargetMemoryBytes += width * height * 4;
|
|
2096
|
+
renderTargetMemoryBytes += width * height * 4 * this.sampleCount;
|
|
2097
|
+
renderTargetMemoryBytes += width * height * 4;
|
|
2155
2098
|
}
|
|
2156
2099
|
if (this.sceneRenderTexture) {
|
|
2157
2100
|
const width = this.canvas.width;
|
|
2158
2101
|
const height = this.canvas.height;
|
|
2159
|
-
renderTargetMemoryBytes += width * height * 4;
|
|
2102
|
+
renderTargetMemoryBytes += width * height * 4;
|
|
2160
2103
|
}
|
|
2161
2104
|
if (this.bloomExtractTexture) {
|
|
2162
|
-
const width = Math.floor(this.canvas.width /
|
|
2163
|
-
const height = Math.floor(this.canvas.height /
|
|
2164
|
-
renderTargetMemoryBytes += width * height * 4
|
|
2165
|
-
renderTargetMemoryBytes += width * height * 4; // bloomBlurTexture1
|
|
2166
|
-
renderTargetMemoryBytes += width * height * 4; // bloomBlurTexture2
|
|
2105
|
+
const width = Math.floor(this.canvas.width / this.BLOOM_DOWNSCALE_FACTOR);
|
|
2106
|
+
const height = Math.floor(this.canvas.height / this.BLOOM_DOWNSCALE_FACTOR);
|
|
2107
|
+
renderTargetMemoryBytes += width * height * 4 * 3;
|
|
2167
2108
|
}
|
|
2168
2109
|
const totalGPUMemoryBytes = textureMemoryBytes + bufferMemoryBytes + renderTargetMemoryBytes;
|
|
2169
|
-
|
|
2110
|
+
return Math.round((totalGPUMemoryBytes / 1024 / 1024) * 100) / 100;
|
|
2170
2111
|
}
|
|
2171
2112
|
}
|