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