reze-engine 0.1.11 → 0.1.13
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/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +385 -485
- package/package.json +1 -1
- package/src/engine.ts +2311 -2416
package/src/engine.ts
CHANGED
|
@@ -1,2416 +1,2311 @@
|
|
|
1
|
-
import { Camera } from "./camera"
|
|
2
|
-
import { Quat, Vec3 } from "./math"
|
|
3
|
-
import { Model } from "./model"
|
|
4
|
-
import { PmxLoader } from "./pmx-loader"
|
|
5
|
-
import { Physics } from "./physics"
|
|
6
|
-
|
|
7
|
-
export interface EngineStats {
|
|
8
|
-
fps: number
|
|
9
|
-
frameTime: number // ms
|
|
10
|
-
gpuMemory: number // MB (estimated total GPU memory)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class Engine {
|
|
14
|
-
private canvas: HTMLCanvasElement
|
|
15
|
-
private device!: GPUDevice
|
|
16
|
-
private context!: GPUCanvasContext
|
|
17
|
-
private presentationFormat!: GPUTextureFormat
|
|
18
|
-
public camera!: Camera
|
|
19
|
-
private cameraUniformBuffer!: GPUBuffer
|
|
20
|
-
private cameraMatrixData = new Float32Array(36)
|
|
21
|
-
private lightUniformBuffer!: GPUBuffer
|
|
22
|
-
private lightData = new Float32Array(64)
|
|
23
|
-
private lightCount = 0
|
|
24
|
-
private vertexBuffer!: GPUBuffer
|
|
25
|
-
private indexBuffer?: GPUBuffer
|
|
26
|
-
private resizeObserver: ResizeObserver | null = null
|
|
27
|
-
private depthTexture!: GPUTexture
|
|
28
|
-
private pipeline!: GPURenderPipeline
|
|
29
|
-
private outlinePipeline!: GPURenderPipeline
|
|
30
|
-
private hairOutlinePipeline!: GPURenderPipeline
|
|
31
|
-
private hairOutlineOverEyesPipeline!: GPURenderPipeline
|
|
32
|
-
private hairMultiplyPipeline!: GPURenderPipeline
|
|
33
|
-
private hairOpaquePipeline!: GPURenderPipeline
|
|
34
|
-
private eyePipeline!: GPURenderPipeline
|
|
35
|
-
private hairBindGroupLayout!: GPUBindGroupLayout
|
|
36
|
-
private outlineBindGroupLayout!: GPUBindGroupLayout
|
|
37
|
-
private jointsBuffer!: GPUBuffer
|
|
38
|
-
private weightsBuffer!: GPUBuffer
|
|
39
|
-
private skinMatrixBuffer?: GPUBuffer
|
|
40
|
-
private worldMatrixBuffer?: GPUBuffer
|
|
41
|
-
private inverseBindMatrixBuffer?: GPUBuffer
|
|
42
|
-
private skinMatrixComputePipeline?: GPUComputePipeline
|
|
43
|
-
private boneCountBuffer?: GPUBuffer
|
|
44
|
-
private multisampleTexture!: GPUTexture
|
|
45
|
-
private readonly sampleCount = 4 // MSAA 4x
|
|
46
|
-
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
47
|
-
// Bloom post-processing textures
|
|
48
|
-
private sceneRenderTexture!: GPUTexture
|
|
49
|
-
private sceneRenderTextureView!: GPUTextureView
|
|
50
|
-
private bloomExtractTexture!: GPUTexture
|
|
51
|
-
private bloomBlurTexture1!: GPUTexture
|
|
52
|
-
private bloomBlurTexture2!: GPUTexture
|
|
53
|
-
// Bloom post-processing pipelines
|
|
54
|
-
private bloomExtractPipeline!: GPURenderPipeline
|
|
55
|
-
private bloomBlurPipeline!: GPURenderPipeline
|
|
56
|
-
private bloomComposePipeline!: GPURenderPipeline
|
|
57
|
-
// Fullscreen quad for post-processing
|
|
58
|
-
private fullscreenQuadBuffer!: GPUBuffer
|
|
59
|
-
private blurDirectionBuffer!: GPUBuffer
|
|
60
|
-
private bloomIntensityBuffer!: GPUBuffer
|
|
61
|
-
private bloomThresholdBuffer!: GPUBuffer
|
|
62
|
-
private linearSampler!: GPUSampler
|
|
63
|
-
// Bloom settings
|
|
64
|
-
public bloomThreshold: number = 0.3
|
|
65
|
-
public bloomIntensity: number = 0.
|
|
66
|
-
// Rim light settings
|
|
67
|
-
private rimLightIntensity: number = 0.35
|
|
68
|
-
private rimLightPower: number = 2.0
|
|
69
|
-
private rimLightColor: [number, number, number] = [1.0, 1.0, 1.0]
|
|
70
|
-
private currentModel: Model | null = null
|
|
71
|
-
private modelDir: string = ""
|
|
72
|
-
private physics: Physics | null = null
|
|
73
|
-
private textureSampler!: GPUSampler
|
|
74
|
-
private textureCache = new Map<string, GPUTexture>()
|
|
75
|
-
private textureSizes = new Map<string, { width: number; height: number }>()
|
|
76
|
-
|
|
77
|
-
private lastFpsUpdate = performance.now()
|
|
78
|
-
private framesSinceLastUpdate = 0
|
|
79
|
-
private frameTimeSamples: number[] = []
|
|
80
|
-
private frameTimeSum: number = 0
|
|
81
|
-
private drawCallCount: number = 0
|
|
82
|
-
private lastFrameTime = performance.now()
|
|
83
|
-
private stats: EngineStats = {
|
|
84
|
-
fps: 0,
|
|
85
|
-
frameTime: 0,
|
|
86
|
-
gpuMemory: 0,
|
|
87
|
-
}
|
|
88
|
-
private animationFrameId: number | null = null
|
|
89
|
-
private renderLoopCallback: (() => void) | null = null
|
|
90
|
-
|
|
91
|
-
constructor(canvas: HTMLCanvasElement) {
|
|
92
|
-
this.canvas = canvas
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Step 1: Get WebGPU device and context
|
|
96
|
-
public async init() {
|
|
97
|
-
const adapter = await navigator.gpu?.requestAdapter()
|
|
98
|
-
const device = await adapter?.requestDevice()
|
|
99
|
-
if (!device) {
|
|
100
|
-
throw new Error("WebGPU is not supported in this browser.")
|
|
101
|
-
}
|
|
102
|
-
this.device = device
|
|
103
|
-
|
|
104
|
-
const context = this.canvas.getContext("webgpu")
|
|
105
|
-
if (!context) {
|
|
106
|
-
throw new Error("Failed to get WebGPU context.")
|
|
107
|
-
}
|
|
108
|
-
this.context = context
|
|
109
|
-
|
|
110
|
-
this.presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
|
111
|
-
|
|
112
|
-
this.context.configure({
|
|
113
|
-
device: this.device,
|
|
114
|
-
format: this.presentationFormat,
|
|
115
|
-
alphaMode: "premultiplied",
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
this.setupCamera()
|
|
119
|
-
this.setupLighting()
|
|
120
|
-
this.createPipelines()
|
|
121
|
-
this.createFullscreenQuad()
|
|
122
|
-
this.createBloomPipelines()
|
|
123
|
-
this.setupResize()
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Step 2: Create shaders and render pipelines
|
|
127
|
-
private createPipelines() {
|
|
128
|
-
this.textureSampler = this.device.createSampler({
|
|
129
|
-
magFilter: "linear",
|
|
130
|
-
minFilter: "linear",
|
|
131
|
-
addressModeU: "repeat",
|
|
132
|
-
addressModeV: "repeat",
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const shaderModule = this.device.createShaderModule({
|
|
136
|
-
label: "model shaders",
|
|
137
|
-
code: /* wgsl */ `
|
|
138
|
-
struct CameraUniforms {
|
|
139
|
-
view: mat4x4f,
|
|
140
|
-
projection: mat4x4f,
|
|
141
|
-
viewPos: vec3f,
|
|
142
|
-
_padding: f32,
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
struct Light {
|
|
146
|
-
direction: vec3f,
|
|
147
|
-
_padding1: f32,
|
|
148
|
-
color: vec3f,
|
|
149
|
-
intensity: f32,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
struct LightUniforms {
|
|
153
|
-
ambient: f32,
|
|
154
|
-
lightCount: f32,
|
|
155
|
-
_padding1: f32,
|
|
156
|
-
_padding2: f32,
|
|
157
|
-
lights: array<Light, 4>,
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
struct MaterialUniforms {
|
|
161
|
-
alpha: f32,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
rimColor: vec3f,
|
|
166
|
-
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
struct VertexOutput {
|
|
170
|
-
@builtin(position) position: vec4f,
|
|
171
|
-
@location(0) normal: vec3f,
|
|
172
|
-
@location(1) uv: vec2f,
|
|
173
|
-
@location(2) worldPos: vec3f,
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
177
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
178
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
179
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
180
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
181
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
182
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
183
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
184
|
-
|
|
185
|
-
@vertex fn vs(
|
|
186
|
-
@location(0) position: vec3f,
|
|
187
|
-
@location(1) normal: vec3f,
|
|
188
|
-
@location(2) uv: vec2f,
|
|
189
|
-
@location(3) joints0: vec4<u32>,
|
|
190
|
-
@location(4) weights0: vec4<f32>
|
|
191
|
-
) -> VertexOutput {
|
|
192
|
-
var output: VertexOutput;
|
|
193
|
-
let pos4 = vec4f(position, 1.0);
|
|
194
|
-
|
|
195
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
196
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
197
|
-
var normalizedWeights: vec4f;
|
|
198
|
-
if (weightSum > 0.0001) {
|
|
199
|
-
normalizedWeights = weights0 / weightSum;
|
|
200
|
-
} else {
|
|
201
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
205
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
206
|
-
for (var i = 0u; i < 4u; i++) {
|
|
207
|
-
let j = joints0[i];
|
|
208
|
-
let w = normalizedWeights[i];
|
|
209
|
-
let m = skinMats[j];
|
|
210
|
-
skinnedPos += (m * pos4) * w;
|
|
211
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
212
|
-
skinnedNrm += (r3 * normal) * w;
|
|
213
|
-
}
|
|
214
|
-
let worldPos = skinnedPos.xyz;
|
|
215
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
216
|
-
output.normal = normalize(skinnedNrm);
|
|
217
|
-
output.uv = uv;
|
|
218
|
-
output.worldPos = worldPos;
|
|
219
|
-
return output;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
223
|
-
let n = normalize(input.normal);
|
|
224
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
225
|
-
|
|
226
|
-
var lightAccum = vec3f(light.ambient);
|
|
227
|
-
let numLights = u32(light.lightCount);
|
|
228
|
-
for (var i = 0u; i < numLights; i++) {
|
|
229
|
-
let l = -light.lights[i].direction;
|
|
230
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
231
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
232
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
233
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
234
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Rim light calculation
|
|
238
|
-
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
239
|
-
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
240
|
-
rimFactor = pow(rimFactor, material.rimPower);
|
|
241
|
-
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
242
|
-
|
|
243
|
-
let color = albedo * lightAccum + rimLight;
|
|
244
|
-
let finalAlpha = material.alpha;
|
|
245
|
-
if (finalAlpha < 0.001) {
|
|
246
|
-
discard;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
250
|
-
}
|
|
251
|
-
`,
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
label: "
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
module:
|
|
645
|
-
|
|
646
|
-
{
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
srcFactor: "
|
|
738
|
-
dstFactor: "one-minus-src-alpha",
|
|
739
|
-
operation: "add",
|
|
740
|
-
},
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
},
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
],
|
|
789
|
-
},
|
|
790
|
-
{
|
|
791
|
-
arrayStride: 4
|
|
792
|
-
attributes: [{ shaderLocation:
|
|
793
|
-
},
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
{
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
let
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
this.
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
this.
|
|
1290
|
-
this.
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
this.
|
|
1299
|
-
this.
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
this.
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
this.
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
this.
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
this.
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
this.
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
bindGroup
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
this.
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
this.
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
this.
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
const
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
this.
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
this.
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
this.
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
this.
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
this.
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
this.
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
})
|
|
2313
|
-
|
|
2314
|
-
const encoder = this.device.createCommandEncoder()
|
|
2315
|
-
const pass = encoder.beginComputePass()
|
|
2316
|
-
pass.setPipeline(this.skinMatrixComputePipeline!)
|
|
2317
|
-
pass.setBindGroup(0, bindGroup)
|
|
2318
|
-
pass.dispatchWorkgroups(workgroupCount)
|
|
2319
|
-
pass.end()
|
|
2320
|
-
this.device.queue.submit([encoder.finish()])
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
// Draw outlines (opaque or transparent)
|
|
2324
|
-
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
2325
|
-
pass.setPipeline(this.outlinePipeline)
|
|
2326
|
-
if (transparent) {
|
|
2327
|
-
// Draw transparent outlines (if any)
|
|
2328
|
-
for (const draw of this.transparentNonEyeNonHairOutlineDraws) {
|
|
2329
|
-
if (draw.count > 0) {
|
|
2330
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
2331
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
} else {
|
|
2335
|
-
// Draw opaque outlines before main geometry
|
|
2336
|
-
for (const draw of this.opaqueNonEyeNonHairOutlineDraws) {
|
|
2337
|
-
if (draw.count > 0) {
|
|
2338
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
2339
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
private updateStats(frameTime: number) {
|
|
2346
|
-
const maxSamples = 60
|
|
2347
|
-
this.frameTimeSamples.push(frameTime)
|
|
2348
|
-
this.frameTimeSum += frameTime
|
|
2349
|
-
if (this.frameTimeSamples.length > maxSamples) {
|
|
2350
|
-
const removed = this.frameTimeSamples.shift()!
|
|
2351
|
-
this.frameTimeSum -= removed
|
|
2352
|
-
}
|
|
2353
|
-
const avgFrameTime = this.frameTimeSum / this.frameTimeSamples.length
|
|
2354
|
-
this.stats.frameTime = Math.round(avgFrameTime * 100) / 100
|
|
2355
|
-
|
|
2356
|
-
const now = performance.now()
|
|
2357
|
-
this.framesSinceLastUpdate++
|
|
2358
|
-
const elapsed = now - this.lastFpsUpdate
|
|
2359
|
-
|
|
2360
|
-
if (elapsed >= 1000) {
|
|
2361
|
-
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * 1000)
|
|
2362
|
-
this.framesSinceLastUpdate = 0
|
|
2363
|
-
this.lastFpsUpdate = now
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
// Calculate GPU memory: textures + buffers + render targets
|
|
2367
|
-
let textureMemoryBytes = 0
|
|
2368
|
-
for (const [path, size] of this.textureSizes.entries()) {
|
|
2369
|
-
if (this.textureCache.has(path)) {
|
|
2370
|
-
textureMemoryBytes += size.width * size.height * 4 // RGBA8 = 4 bytes per pixel
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
let bufferMemoryBytes = 0
|
|
2375
|
-
if (this.vertexBuffer) {
|
|
2376
|
-
const vertices = this.currentModel?.getVertices()
|
|
2377
|
-
if (vertices) bufferMemoryBytes += vertices.byteLength
|
|
2378
|
-
}
|
|
2379
|
-
if (this.indexBuffer) {
|
|
2380
|
-
const indices = this.currentModel?.getIndices()
|
|
2381
|
-
if (indices) bufferMemoryBytes += indices.byteLength
|
|
2382
|
-
}
|
|
2383
|
-
if (this.jointsBuffer) {
|
|
2384
|
-
const skinning = this.currentModel?.getSkinning()
|
|
2385
|
-
if (skinning) bufferMemoryBytes += skinning.joints.byteLength
|
|
2386
|
-
}
|
|
2387
|
-
if (this.weightsBuffer) {
|
|
2388
|
-
const skinning = this.currentModel?.getSkinning()
|
|
2389
|
-
if (skinning) bufferMemoryBytes += skinning.weights.byteLength
|
|
2390
|
-
}
|
|
2391
|
-
if (this.skinMatrixBuffer) {
|
|
2392
|
-
const skeleton = this.currentModel?.getSkeleton()
|
|
2393
|
-
if (skeleton) bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4)
|
|
2394
|
-
}
|
|
2395
|
-
bufferMemoryBytes += 40 * 4 // cameraUniformBuffer
|
|
2396
|
-
bufferMemoryBytes += 64 * 4 // lightUniformBuffer
|
|
2397
|
-
const totalMaterialDraws =
|
|
2398
|
-
this.opaqueNonEyeNonHairDraws.length +
|
|
2399
|
-
this.eyeDraws.length +
|
|
2400
|
-
this.hairDrawsOverEyes.length +
|
|
2401
|
-
this.hairDrawsOverNonEyes.length +
|
|
2402
|
-
this.transparentNonEyeNonHairDraws.length
|
|
2403
|
-
bufferMemoryBytes += totalMaterialDraws * 4 // Material uniform buffers
|
|
2404
|
-
|
|
2405
|
-
let renderTargetMemoryBytes = 0
|
|
2406
|
-
if (this.multisampleTexture) {
|
|
2407
|
-
const width = this.canvas.width
|
|
2408
|
-
const height = this.canvas.height
|
|
2409
|
-
renderTargetMemoryBytes += width * height * 4 * this.sampleCount // multisample color
|
|
2410
|
-
renderTargetMemoryBytes += width * height * 4 // depth
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
const totalGPUMemoryBytes = textureMemoryBytes + bufferMemoryBytes + renderTargetMemoryBytes
|
|
2414
|
-
this.stats.gpuMemory = Math.round((totalGPUMemoryBytes / 1024 / 1024) * 100) / 100
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
1
|
+
import { Camera } from "./camera"
|
|
2
|
+
import { Quat, Vec3 } from "./math"
|
|
3
|
+
import { Model } from "./model"
|
|
4
|
+
import { PmxLoader } from "./pmx-loader"
|
|
5
|
+
import { Physics } from "./physics"
|
|
6
|
+
|
|
7
|
+
export interface EngineStats {
|
|
8
|
+
fps: number
|
|
9
|
+
frameTime: number // ms
|
|
10
|
+
gpuMemory: number // MB (estimated total GPU memory)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Engine {
|
|
14
|
+
private canvas: HTMLCanvasElement
|
|
15
|
+
private device!: GPUDevice
|
|
16
|
+
private context!: GPUCanvasContext
|
|
17
|
+
private presentationFormat!: GPUTextureFormat
|
|
18
|
+
public camera!: Camera
|
|
19
|
+
private cameraUniformBuffer!: GPUBuffer
|
|
20
|
+
private cameraMatrixData = new Float32Array(36)
|
|
21
|
+
private lightUniformBuffer!: GPUBuffer
|
|
22
|
+
private lightData = new Float32Array(64)
|
|
23
|
+
private lightCount = 0
|
|
24
|
+
private vertexBuffer!: GPUBuffer
|
|
25
|
+
private indexBuffer?: GPUBuffer
|
|
26
|
+
private resizeObserver: ResizeObserver | null = null
|
|
27
|
+
private depthTexture!: GPUTexture
|
|
28
|
+
private pipeline!: GPURenderPipeline
|
|
29
|
+
private outlinePipeline!: GPURenderPipeline
|
|
30
|
+
private hairOutlinePipeline!: GPURenderPipeline
|
|
31
|
+
private hairOutlineOverEyesPipeline!: GPURenderPipeline
|
|
32
|
+
private hairMultiplyPipeline!: GPURenderPipeline
|
|
33
|
+
private hairOpaquePipeline!: GPURenderPipeline
|
|
34
|
+
private eyePipeline!: GPURenderPipeline
|
|
35
|
+
private hairBindGroupLayout!: GPUBindGroupLayout
|
|
36
|
+
private outlineBindGroupLayout!: GPUBindGroupLayout
|
|
37
|
+
private jointsBuffer!: GPUBuffer
|
|
38
|
+
private weightsBuffer!: GPUBuffer
|
|
39
|
+
private skinMatrixBuffer?: GPUBuffer
|
|
40
|
+
private worldMatrixBuffer?: GPUBuffer
|
|
41
|
+
private inverseBindMatrixBuffer?: GPUBuffer
|
|
42
|
+
private skinMatrixComputePipeline?: GPUComputePipeline
|
|
43
|
+
private boneCountBuffer?: GPUBuffer
|
|
44
|
+
private multisampleTexture!: GPUTexture
|
|
45
|
+
private readonly sampleCount = 4 // MSAA 4x
|
|
46
|
+
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
47
|
+
// Bloom post-processing textures
|
|
48
|
+
private sceneRenderTexture!: GPUTexture
|
|
49
|
+
private sceneRenderTextureView!: GPUTextureView
|
|
50
|
+
private bloomExtractTexture!: GPUTexture
|
|
51
|
+
private bloomBlurTexture1!: GPUTexture
|
|
52
|
+
private bloomBlurTexture2!: GPUTexture
|
|
53
|
+
// Bloom post-processing pipelines
|
|
54
|
+
private bloomExtractPipeline!: GPURenderPipeline
|
|
55
|
+
private bloomBlurPipeline!: GPURenderPipeline
|
|
56
|
+
private bloomComposePipeline!: GPURenderPipeline
|
|
57
|
+
// Fullscreen quad for post-processing
|
|
58
|
+
private fullscreenQuadBuffer!: GPUBuffer
|
|
59
|
+
private blurDirectionBuffer!: GPUBuffer
|
|
60
|
+
private bloomIntensityBuffer!: GPUBuffer
|
|
61
|
+
private bloomThresholdBuffer!: GPUBuffer
|
|
62
|
+
private linearSampler!: GPUSampler
|
|
63
|
+
// Bloom settings
|
|
64
|
+
public bloomThreshold: number = 0.3
|
|
65
|
+
public bloomIntensity: number = 0.1
|
|
66
|
+
// Rim light settings
|
|
67
|
+
private rimLightIntensity: number = 0.35
|
|
68
|
+
private rimLightPower: number = 2.0
|
|
69
|
+
private rimLightColor: [number, number, number] = [1.0, 1.0, 1.0]
|
|
70
|
+
private currentModel: Model | null = null
|
|
71
|
+
private modelDir: string = ""
|
|
72
|
+
private physics: Physics | null = null
|
|
73
|
+
private textureSampler!: GPUSampler
|
|
74
|
+
private textureCache = new Map<string, GPUTexture>()
|
|
75
|
+
private textureSizes = new Map<string, { width: number; height: number }>()
|
|
76
|
+
|
|
77
|
+
private lastFpsUpdate = performance.now()
|
|
78
|
+
private framesSinceLastUpdate = 0
|
|
79
|
+
private frameTimeSamples: number[] = []
|
|
80
|
+
private frameTimeSum: number = 0
|
|
81
|
+
private drawCallCount: number = 0
|
|
82
|
+
private lastFrameTime = performance.now()
|
|
83
|
+
private stats: EngineStats = {
|
|
84
|
+
fps: 0,
|
|
85
|
+
frameTime: 0,
|
|
86
|
+
gpuMemory: 0,
|
|
87
|
+
}
|
|
88
|
+
private animationFrameId: number | null = null
|
|
89
|
+
private renderLoopCallback: (() => void) | null = null
|
|
90
|
+
|
|
91
|
+
constructor(canvas: HTMLCanvasElement) {
|
|
92
|
+
this.canvas = canvas
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 1: Get WebGPU device and context
|
|
96
|
+
public async init() {
|
|
97
|
+
const adapter = await navigator.gpu?.requestAdapter()
|
|
98
|
+
const device = await adapter?.requestDevice()
|
|
99
|
+
if (!device) {
|
|
100
|
+
throw new Error("WebGPU is not supported in this browser.")
|
|
101
|
+
}
|
|
102
|
+
this.device = device
|
|
103
|
+
|
|
104
|
+
const context = this.canvas.getContext("webgpu")
|
|
105
|
+
if (!context) {
|
|
106
|
+
throw new Error("Failed to get WebGPU context.")
|
|
107
|
+
}
|
|
108
|
+
this.context = context
|
|
109
|
+
|
|
110
|
+
this.presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
|
111
|
+
|
|
112
|
+
this.context.configure({
|
|
113
|
+
device: this.device,
|
|
114
|
+
format: this.presentationFormat,
|
|
115
|
+
alphaMode: "premultiplied",
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
this.setupCamera()
|
|
119
|
+
this.setupLighting()
|
|
120
|
+
this.createPipelines()
|
|
121
|
+
this.createFullscreenQuad()
|
|
122
|
+
this.createBloomPipelines()
|
|
123
|
+
this.setupResize()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Step 2: Create shaders and render pipelines
|
|
127
|
+
private createPipelines() {
|
|
128
|
+
this.textureSampler = this.device.createSampler({
|
|
129
|
+
magFilter: "linear",
|
|
130
|
+
minFilter: "linear",
|
|
131
|
+
addressModeU: "repeat",
|
|
132
|
+
addressModeV: "repeat",
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const shaderModule = this.device.createShaderModule({
|
|
136
|
+
label: "model shaders",
|
|
137
|
+
code: /* wgsl */ `
|
|
138
|
+
struct CameraUniforms {
|
|
139
|
+
view: mat4x4f,
|
|
140
|
+
projection: mat4x4f,
|
|
141
|
+
viewPos: vec3f,
|
|
142
|
+
_padding: f32,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
struct Light {
|
|
146
|
+
direction: vec3f,
|
|
147
|
+
_padding1: f32,
|
|
148
|
+
color: vec3f,
|
|
149
|
+
intensity: f32,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
struct LightUniforms {
|
|
153
|
+
ambient: f32,
|
|
154
|
+
lightCount: f32,
|
|
155
|
+
_padding1: f32,
|
|
156
|
+
_padding2: f32,
|
|
157
|
+
lights: array<Light, 4>,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
struct MaterialUniforms {
|
|
161
|
+
alpha: f32,
|
|
162
|
+
alphaMultiplier: f32,
|
|
163
|
+
rimIntensity: f32,
|
|
164
|
+
rimPower: f32,
|
|
165
|
+
rimColor: vec3f,
|
|
166
|
+
_padding1: f32,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
struct VertexOutput {
|
|
170
|
+
@builtin(position) position: vec4f,
|
|
171
|
+
@location(0) normal: vec3f,
|
|
172
|
+
@location(1) uv: vec2f,
|
|
173
|
+
@location(2) worldPos: vec3f,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
177
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
178
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
179
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
180
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
181
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
182
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
183
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
184
|
+
|
|
185
|
+
@vertex fn vs(
|
|
186
|
+
@location(0) position: vec3f,
|
|
187
|
+
@location(1) normal: vec3f,
|
|
188
|
+
@location(2) uv: vec2f,
|
|
189
|
+
@location(3) joints0: vec4<u32>,
|
|
190
|
+
@location(4) weights0: vec4<f32>
|
|
191
|
+
) -> VertexOutput {
|
|
192
|
+
var output: VertexOutput;
|
|
193
|
+
let pos4 = vec4f(position, 1.0);
|
|
194
|
+
|
|
195
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
196
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
197
|
+
var normalizedWeights: vec4f;
|
|
198
|
+
if (weightSum > 0.0001) {
|
|
199
|
+
normalizedWeights = weights0 / weightSum;
|
|
200
|
+
} else {
|
|
201
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
205
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
206
|
+
for (var i = 0u; i < 4u; i++) {
|
|
207
|
+
let j = joints0[i];
|
|
208
|
+
let w = normalizedWeights[i];
|
|
209
|
+
let m = skinMats[j];
|
|
210
|
+
skinnedPos += (m * pos4) * w;
|
|
211
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
212
|
+
skinnedNrm += (r3 * normal) * w;
|
|
213
|
+
}
|
|
214
|
+
let worldPos = skinnedPos.xyz;
|
|
215
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
216
|
+
output.normal = normalize(skinnedNrm);
|
|
217
|
+
output.uv = uv;
|
|
218
|
+
output.worldPos = worldPos;
|
|
219
|
+
return output;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
223
|
+
let n = normalize(input.normal);
|
|
224
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
225
|
+
|
|
226
|
+
var lightAccum = vec3f(light.ambient);
|
|
227
|
+
let numLights = u32(light.lightCount);
|
|
228
|
+
for (var i = 0u; i < numLights; i++) {
|
|
229
|
+
let l = -light.lights[i].direction;
|
|
230
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
231
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
232
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
233
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
234
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Rim light calculation
|
|
238
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
239
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
240
|
+
rimFactor = pow(rimFactor, material.rimPower);
|
|
241
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
242
|
+
|
|
243
|
+
let color = albedo * lightAccum + rimLight;
|
|
244
|
+
let finalAlpha = material.alpha * material.alphaMultiplier;
|
|
245
|
+
if (finalAlpha < 0.001) {
|
|
246
|
+
discard;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
250
|
+
}
|
|
251
|
+
`,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Create explicit bind group layout for all pipelines using the main shader
|
|
255
|
+
// This ensures compatibility across all pipelines (main, eye, hair multiply, hair opaque)
|
|
256
|
+
this.hairBindGroupLayout = this.device.createBindGroupLayout({
|
|
257
|
+
label: "shared material bind group layout",
|
|
258
|
+
entries: [
|
|
259
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
260
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
|
|
261
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // diffuseTexture
|
|
262
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // diffuseSampler
|
|
263
|
+
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
264
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // toonTexture
|
|
265
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // toonSampler
|
|
266
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
267
|
+
],
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const sharedPipelineLayout = this.device.createPipelineLayout({
|
|
271
|
+
label: "shared pipeline layout",
|
|
272
|
+
bindGroupLayouts: [this.hairBindGroupLayout],
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// Single pipeline for all materials with alpha blending
|
|
276
|
+
this.pipeline = this.device.createRenderPipeline({
|
|
277
|
+
label: "model pipeline",
|
|
278
|
+
layout: sharedPipelineLayout,
|
|
279
|
+
vertex: {
|
|
280
|
+
module: shaderModule,
|
|
281
|
+
buffers: [
|
|
282
|
+
{
|
|
283
|
+
arrayStride: 8 * 4,
|
|
284
|
+
attributes: [
|
|
285
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
286
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
287
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
arrayStride: 4 * 2,
|
|
292
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
arrayStride: 4,
|
|
296
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
fragment: {
|
|
301
|
+
module: shaderModule,
|
|
302
|
+
targets: [
|
|
303
|
+
{
|
|
304
|
+
format: this.presentationFormat,
|
|
305
|
+
blend: {
|
|
306
|
+
color: {
|
|
307
|
+
srcFactor: "src-alpha",
|
|
308
|
+
dstFactor: "one-minus-src-alpha",
|
|
309
|
+
operation: "add",
|
|
310
|
+
},
|
|
311
|
+
alpha: {
|
|
312
|
+
srcFactor: "one",
|
|
313
|
+
dstFactor: "one-minus-src-alpha",
|
|
314
|
+
operation: "add",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
primitive: { cullMode: "none" },
|
|
321
|
+
depthStencil: {
|
|
322
|
+
format: "depth24plus-stencil8",
|
|
323
|
+
depthWriteEnabled: true,
|
|
324
|
+
depthCompare: "less",
|
|
325
|
+
},
|
|
326
|
+
multisample: {
|
|
327
|
+
count: this.sampleCount,
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Create bind group layout for outline pipelines
|
|
332
|
+
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
333
|
+
label: "outline bind group layout",
|
|
334
|
+
entries: [
|
|
335
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
336
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
337
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
338
|
+
],
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
342
|
+
label: "outline pipeline layout",
|
|
343
|
+
bindGroupLayouts: [this.outlineBindGroupLayout],
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
const outlineShaderModule = this.device.createShaderModule({
|
|
347
|
+
label: "outline shaders",
|
|
348
|
+
code: /* wgsl */ `
|
|
349
|
+
struct CameraUniforms {
|
|
350
|
+
view: mat4x4f,
|
|
351
|
+
projection: mat4x4f,
|
|
352
|
+
viewPos: vec3f,
|
|
353
|
+
_padding: f32,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
struct MaterialUniforms {
|
|
357
|
+
edgeColor: vec4f,
|
|
358
|
+
edgeSize: f32,
|
|
359
|
+
_padding1: f32,
|
|
360
|
+
_padding2: f32,
|
|
361
|
+
_padding3: f32,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
365
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
366
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
367
|
+
|
|
368
|
+
struct VertexOutput {
|
|
369
|
+
@builtin(position) position: vec4f,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
@vertex fn vs(
|
|
373
|
+
@location(0) position: vec3f,
|
|
374
|
+
@location(1) normal: vec3f,
|
|
375
|
+
@location(3) joints0: vec4<u32>,
|
|
376
|
+
@location(4) weights0: vec4<f32>
|
|
377
|
+
) -> VertexOutput {
|
|
378
|
+
var output: VertexOutput;
|
|
379
|
+
let pos4 = vec4f(position, 1.0);
|
|
380
|
+
|
|
381
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
382
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
383
|
+
var normalizedWeights: vec4f;
|
|
384
|
+
if (weightSum > 0.0001) {
|
|
385
|
+
normalizedWeights = weights0 / weightSum;
|
|
386
|
+
} else {
|
|
387
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
391
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
392
|
+
for (var i = 0u; i < 4u; i++) {
|
|
393
|
+
let j = joints0[i];
|
|
394
|
+
let w = normalizedWeights[i];
|
|
395
|
+
let m = skinMats[j];
|
|
396
|
+
skinnedPos += (m * pos4) * w;
|
|
397
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
398
|
+
skinnedNrm += (r3 * normal) * w;
|
|
399
|
+
}
|
|
400
|
+
let worldPos = skinnedPos.xyz;
|
|
401
|
+
let worldNormal = normalize(skinnedNrm);
|
|
402
|
+
|
|
403
|
+
// MMD invert hull: expand vertices outward along normals
|
|
404
|
+
let scaleFactor = 0.01;
|
|
405
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
406
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
407
|
+
return output;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
411
|
+
return material.edgeColor;
|
|
412
|
+
}
|
|
413
|
+
`,
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
this.outlinePipeline = this.device.createRenderPipeline({
|
|
417
|
+
label: "outline pipeline",
|
|
418
|
+
layout: outlinePipelineLayout,
|
|
419
|
+
vertex: {
|
|
420
|
+
module: outlineShaderModule,
|
|
421
|
+
buffers: [
|
|
422
|
+
{
|
|
423
|
+
arrayStride: 8 * 4,
|
|
424
|
+
attributes: [
|
|
425
|
+
{
|
|
426
|
+
shaderLocation: 0,
|
|
427
|
+
offset: 0,
|
|
428
|
+
format: "float32x3" as GPUVertexFormat,
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
shaderLocation: 1,
|
|
432
|
+
offset: 3 * 4,
|
|
433
|
+
format: "float32x3" as GPUVertexFormat,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
arrayStride: 4 * 2,
|
|
439
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
arrayStride: 4,
|
|
443
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
fragment: {
|
|
448
|
+
module: outlineShaderModule,
|
|
449
|
+
targets: [
|
|
450
|
+
{
|
|
451
|
+
format: this.presentationFormat,
|
|
452
|
+
blend: {
|
|
453
|
+
color: {
|
|
454
|
+
srcFactor: "src-alpha",
|
|
455
|
+
dstFactor: "one-minus-src-alpha",
|
|
456
|
+
operation: "add",
|
|
457
|
+
},
|
|
458
|
+
alpha: {
|
|
459
|
+
srcFactor: "one",
|
|
460
|
+
dstFactor: "one-minus-src-alpha",
|
|
461
|
+
operation: "add",
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
primitive: {
|
|
468
|
+
cullMode: "back",
|
|
469
|
+
},
|
|
470
|
+
depthStencil: {
|
|
471
|
+
format: "depth24plus-stencil8",
|
|
472
|
+
depthWriteEnabled: true,
|
|
473
|
+
depthCompare: "less",
|
|
474
|
+
},
|
|
475
|
+
multisample: {
|
|
476
|
+
count: this.sampleCount,
|
|
477
|
+
},
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1) - Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
|
|
481
|
+
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
482
|
+
label: "hair outline pipeline",
|
|
483
|
+
layout: outlinePipelineLayout,
|
|
484
|
+
vertex: {
|
|
485
|
+
module: outlineShaderModule,
|
|
486
|
+
buffers: [
|
|
487
|
+
{
|
|
488
|
+
arrayStride: 8 * 4,
|
|
489
|
+
attributes: [
|
|
490
|
+
{
|
|
491
|
+
shaderLocation: 0,
|
|
492
|
+
offset: 0,
|
|
493
|
+
format: "float32x3" as GPUVertexFormat,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
shaderLocation: 1,
|
|
497
|
+
offset: 3 * 4,
|
|
498
|
+
format: "float32x3" as GPUVertexFormat,
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
arrayStride: 4 * 2,
|
|
504
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
arrayStride: 4,
|
|
508
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
fragment: {
|
|
513
|
+
module: outlineShaderModule,
|
|
514
|
+
targets: [
|
|
515
|
+
{
|
|
516
|
+
format: this.presentationFormat,
|
|
517
|
+
blend: {
|
|
518
|
+
color: {
|
|
519
|
+
srcFactor: "src-alpha",
|
|
520
|
+
dstFactor: "one-minus-src-alpha",
|
|
521
|
+
operation: "add",
|
|
522
|
+
},
|
|
523
|
+
alpha: {
|
|
524
|
+
srcFactor: "one",
|
|
525
|
+
dstFactor: "one-minus-src-alpha",
|
|
526
|
+
operation: "add",
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
primitive: {
|
|
533
|
+
cullMode: "back",
|
|
534
|
+
},
|
|
535
|
+
depthStencil: {
|
|
536
|
+
format: "depth24plus-stencil8",
|
|
537
|
+
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
538
|
+
depthCompare: "less-equal", // Only draw where hair depth exists
|
|
539
|
+
stencilFront: {
|
|
540
|
+
compare: "not-equal", // Only render where stencil != 1 (not over eyes)
|
|
541
|
+
failOp: "keep",
|
|
542
|
+
depthFailOp: "keep",
|
|
543
|
+
passOp: "keep",
|
|
544
|
+
},
|
|
545
|
+
stencilBack: {
|
|
546
|
+
compare: "not-equal",
|
|
547
|
+
failOp: "keep",
|
|
548
|
+
depthFailOp: "keep",
|
|
549
|
+
passOp: "keep",
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
multisample: {
|
|
553
|
+
count: this.sampleCount,
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
// Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists - Uses depth compare "equal" with a small bias to only appear where hair geometry exists
|
|
558
|
+
this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
|
|
559
|
+
label: "hair outline over eyes pipeline",
|
|
560
|
+
layout: outlinePipelineLayout,
|
|
561
|
+
vertex: {
|
|
562
|
+
module: outlineShaderModule,
|
|
563
|
+
buffers: [
|
|
564
|
+
{
|
|
565
|
+
arrayStride: 8 * 4,
|
|
566
|
+
attributes: [
|
|
567
|
+
{
|
|
568
|
+
shaderLocation: 0,
|
|
569
|
+
offset: 0,
|
|
570
|
+
format: "float32x3" as GPUVertexFormat,
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
shaderLocation: 1,
|
|
574
|
+
offset: 3 * 4,
|
|
575
|
+
format: "float32x3" as GPUVertexFormat,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
arrayStride: 4 * 2,
|
|
581
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
arrayStride: 4,
|
|
585
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
},
|
|
589
|
+
fragment: {
|
|
590
|
+
module: outlineShaderModule,
|
|
591
|
+
targets: [
|
|
592
|
+
{
|
|
593
|
+
format: this.presentationFormat,
|
|
594
|
+
blend: {
|
|
595
|
+
color: {
|
|
596
|
+
srcFactor: "src-alpha",
|
|
597
|
+
dstFactor: "one-minus-src-alpha",
|
|
598
|
+
operation: "add",
|
|
599
|
+
},
|
|
600
|
+
alpha: {
|
|
601
|
+
srcFactor: "one",
|
|
602
|
+
dstFactor: "one-minus-src-alpha",
|
|
603
|
+
operation: "add",
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
},
|
|
609
|
+
primitive: {
|
|
610
|
+
cullMode: "back",
|
|
611
|
+
},
|
|
612
|
+
depthStencil: {
|
|
613
|
+
format: "depth24plus-stencil8",
|
|
614
|
+
depthWriteEnabled: false, // Don't write depth
|
|
615
|
+
|
|
616
|
+
depthCompare: "less-equal", // Draw where outline depth <= existing depth (hair depth)
|
|
617
|
+
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
618
|
+
depthBiasSlopeScale: 0.0,
|
|
619
|
+
depthBiasClamp: 0.0,
|
|
620
|
+
stencilFront: {
|
|
621
|
+
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
622
|
+
failOp: "keep",
|
|
623
|
+
depthFailOp: "keep",
|
|
624
|
+
passOp: "keep",
|
|
625
|
+
},
|
|
626
|
+
stencilBack: {
|
|
627
|
+
compare: "equal",
|
|
628
|
+
failOp: "keep",
|
|
629
|
+
depthFailOp: "keep",
|
|
630
|
+
passOp: "keep",
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
multisample: {
|
|
634
|
+
count: this.sampleCount,
|
|
635
|
+
},
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
// Unified hair pipeline - can be used for both over-eyes and over-non-eyes
|
|
639
|
+
// The difference is controlled by stencil state and alpha multiplier in material uniform
|
|
640
|
+
this.hairMultiplyPipeline = this.device.createRenderPipeline({
|
|
641
|
+
label: "hair pipeline (over eyes)",
|
|
642
|
+
layout: sharedPipelineLayout,
|
|
643
|
+
vertex: {
|
|
644
|
+
module: shaderModule,
|
|
645
|
+
buffers: [
|
|
646
|
+
{
|
|
647
|
+
arrayStride: 8 * 4,
|
|
648
|
+
attributes: [
|
|
649
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
650
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
651
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
652
|
+
],
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
arrayStride: 4 * 2,
|
|
656
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
arrayStride: 4,
|
|
660
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
fragment: {
|
|
665
|
+
module: shaderModule,
|
|
666
|
+
targets: [
|
|
667
|
+
{
|
|
668
|
+
format: this.presentationFormat,
|
|
669
|
+
blend: {
|
|
670
|
+
color: {
|
|
671
|
+
srcFactor: "src-alpha",
|
|
672
|
+
dstFactor: "one-minus-src-alpha",
|
|
673
|
+
operation: "add",
|
|
674
|
+
},
|
|
675
|
+
alpha: {
|
|
676
|
+
srcFactor: "one",
|
|
677
|
+
dstFactor: "one-minus-src-alpha",
|
|
678
|
+
operation: "add",
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
primitive: { cullMode: "none" },
|
|
685
|
+
depthStencil: {
|
|
686
|
+
format: "depth24plus-stencil8",
|
|
687
|
+
depthWriteEnabled: true, // Write depth so outlines can test against it
|
|
688
|
+
depthCompare: "less",
|
|
689
|
+
stencilFront: {
|
|
690
|
+
compare: "equal", // Only render where stencil == 1
|
|
691
|
+
failOp: "keep",
|
|
692
|
+
depthFailOp: "keep",
|
|
693
|
+
passOp: "keep",
|
|
694
|
+
},
|
|
695
|
+
stencilBack: {
|
|
696
|
+
compare: "equal",
|
|
697
|
+
failOp: "keep",
|
|
698
|
+
depthFailOp: "keep",
|
|
699
|
+
passOp: "keep",
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
multisample: { count: this.sampleCount },
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
// Hair pipeline for opaque rendering (hair over non-eyes) - uses same shader, different stencil state
|
|
706
|
+
this.hairOpaquePipeline = this.device.createRenderPipeline({
|
|
707
|
+
label: "hair pipeline (over non-eyes)",
|
|
708
|
+
layout: sharedPipelineLayout,
|
|
709
|
+
vertex: {
|
|
710
|
+
module: shaderModule,
|
|
711
|
+
buffers: [
|
|
712
|
+
{
|
|
713
|
+
arrayStride: 8 * 4,
|
|
714
|
+
attributes: [
|
|
715
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
716
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
717
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
718
|
+
],
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
arrayStride: 4 * 2,
|
|
722
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
arrayStride: 4,
|
|
726
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
},
|
|
730
|
+
fragment: {
|
|
731
|
+
module: shaderModule,
|
|
732
|
+
targets: [
|
|
733
|
+
{
|
|
734
|
+
format: this.presentationFormat,
|
|
735
|
+
blend: {
|
|
736
|
+
color: {
|
|
737
|
+
srcFactor: "src-alpha",
|
|
738
|
+
dstFactor: "one-minus-src-alpha",
|
|
739
|
+
operation: "add",
|
|
740
|
+
},
|
|
741
|
+
alpha: {
|
|
742
|
+
srcFactor: "one",
|
|
743
|
+
dstFactor: "one-minus-src-alpha",
|
|
744
|
+
operation: "add",
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
},
|
|
750
|
+
primitive: { cullMode: "none" },
|
|
751
|
+
depthStencil: {
|
|
752
|
+
format: "depth24plus-stencil8",
|
|
753
|
+
depthWriteEnabled: true,
|
|
754
|
+
depthCompare: "less",
|
|
755
|
+
stencilFront: {
|
|
756
|
+
compare: "not-equal", // Only render where stencil != 1
|
|
757
|
+
failOp: "keep",
|
|
758
|
+
depthFailOp: "keep",
|
|
759
|
+
passOp: "keep",
|
|
760
|
+
},
|
|
761
|
+
stencilBack: {
|
|
762
|
+
compare: "not-equal",
|
|
763
|
+
failOp: "keep",
|
|
764
|
+
depthFailOp: "keep",
|
|
765
|
+
passOp: "keep",
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
multisample: { count: this.sampleCount },
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
772
|
+
this.eyePipeline = this.device.createRenderPipeline({
|
|
773
|
+
label: "eye overlay pipeline",
|
|
774
|
+
layout: sharedPipelineLayout,
|
|
775
|
+
vertex: {
|
|
776
|
+
module: shaderModule,
|
|
777
|
+
buffers: [
|
|
778
|
+
{
|
|
779
|
+
arrayStride: 8 * 4,
|
|
780
|
+
attributes: [
|
|
781
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
782
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
783
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
784
|
+
],
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
arrayStride: 4 * 2,
|
|
788
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
arrayStride: 4,
|
|
792
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
},
|
|
796
|
+
fragment: {
|
|
797
|
+
module: shaderModule,
|
|
798
|
+
targets: [
|
|
799
|
+
{
|
|
800
|
+
format: this.presentationFormat,
|
|
801
|
+
blend: {
|
|
802
|
+
color: {
|
|
803
|
+
srcFactor: "src-alpha",
|
|
804
|
+
dstFactor: "one-minus-src-alpha",
|
|
805
|
+
operation: "add",
|
|
806
|
+
},
|
|
807
|
+
alpha: {
|
|
808
|
+
srcFactor: "one",
|
|
809
|
+
dstFactor: "one-minus-src-alpha",
|
|
810
|
+
operation: "add",
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
],
|
|
815
|
+
},
|
|
816
|
+
primitive: { cullMode: "none" },
|
|
817
|
+
depthStencil: {
|
|
818
|
+
format: "depth24plus-stencil8",
|
|
819
|
+
depthWriteEnabled: false, // Don't write depth
|
|
820
|
+
depthCompare: "less", // Respect existing depth
|
|
821
|
+
stencilFront: {
|
|
822
|
+
compare: "always",
|
|
823
|
+
failOp: "keep",
|
|
824
|
+
depthFailOp: "keep",
|
|
825
|
+
passOp: "replace", // Write stencil value 1
|
|
826
|
+
},
|
|
827
|
+
stencilBack: {
|
|
828
|
+
compare: "always",
|
|
829
|
+
failOp: "keep",
|
|
830
|
+
depthFailOp: "keep",
|
|
831
|
+
passOp: "replace",
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
multisample: { count: this.sampleCount },
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Create compute shader for skin matrix computation
|
|
839
|
+
private createSkinMatrixComputePipeline() {
|
|
840
|
+
const computeShader = this.device.createShaderModule({
|
|
841
|
+
label: "skin matrix compute",
|
|
842
|
+
code: /* wgsl */ `
|
|
843
|
+
struct BoneCountUniform {
|
|
844
|
+
count: u32,
|
|
845
|
+
_padding1: u32,
|
|
846
|
+
_padding2: u32,
|
|
847
|
+
_padding3: u32,
|
|
848
|
+
_padding4: vec4<u32>,
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
852
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
853
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
854
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
855
|
+
|
|
856
|
+
@compute @workgroup_size(64)
|
|
857
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
858
|
+
let boneIndex = globalId.x;
|
|
859
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
860
|
+
if (boneIndex >= boneCount.count) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
let worldMat = worldMatrices[boneIndex];
|
|
864
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
865
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
866
|
+
}
|
|
867
|
+
`,
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
871
|
+
label: "skin matrix compute pipeline",
|
|
872
|
+
layout: "auto",
|
|
873
|
+
compute: {
|
|
874
|
+
module: computeShader,
|
|
875
|
+
},
|
|
876
|
+
})
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Create fullscreen quad for post-processing
|
|
880
|
+
private createFullscreenQuad() {
|
|
881
|
+
// Fullscreen quad vertices: two triangles covering the entire screen - Format: position (x, y), uv (u, v)
|
|
882
|
+
const quadVertices = new Float32Array([
|
|
883
|
+
// Triangle 1
|
|
884
|
+
-1.0,
|
|
885
|
+
-1.0,
|
|
886
|
+
0.0,
|
|
887
|
+
0.0, // bottom-left
|
|
888
|
+
1.0,
|
|
889
|
+
-1.0,
|
|
890
|
+
1.0,
|
|
891
|
+
0.0, // bottom-right
|
|
892
|
+
-1.0,
|
|
893
|
+
1.0,
|
|
894
|
+
0.0,
|
|
895
|
+
1.0, // top-left
|
|
896
|
+
// Triangle 2
|
|
897
|
+
-1.0,
|
|
898
|
+
1.0,
|
|
899
|
+
0.0,
|
|
900
|
+
1.0, // top-left
|
|
901
|
+
1.0,
|
|
902
|
+
-1.0,
|
|
903
|
+
1.0,
|
|
904
|
+
0.0, // bottom-right
|
|
905
|
+
1.0,
|
|
906
|
+
1.0,
|
|
907
|
+
1.0,
|
|
908
|
+
1.0, // top-right
|
|
909
|
+
])
|
|
910
|
+
|
|
911
|
+
this.fullscreenQuadBuffer = this.device.createBuffer({
|
|
912
|
+
label: "fullscreen quad",
|
|
913
|
+
size: quadVertices.byteLength,
|
|
914
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
915
|
+
})
|
|
916
|
+
this.device.queue.writeBuffer(this.fullscreenQuadBuffer, 0, quadVertices)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Create bloom post-processing pipelines
|
|
920
|
+
private createBloomPipelines() {
|
|
921
|
+
// Bloom extraction shader (extracts bright areas)
|
|
922
|
+
const bloomExtractShader = this.device.createShaderModule({
|
|
923
|
+
label: "bloom extract",
|
|
924
|
+
code: /* wgsl */ `
|
|
925
|
+
struct VertexOutput {
|
|
926
|
+
@builtin(position) position: vec4f,
|
|
927
|
+
@location(0) uv: vec2f,
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
931
|
+
var output: VertexOutput;
|
|
932
|
+
// Generate fullscreen quad from vertex index
|
|
933
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
934
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
935
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
936
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
937
|
+
return output;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
struct BloomExtractUniforms {
|
|
941
|
+
threshold: f32,
|
|
942
|
+
_padding1: f32,
|
|
943
|
+
_padding2: f32,
|
|
944
|
+
_padding3: f32,
|
|
945
|
+
_padding4: f32,
|
|
946
|
+
_padding5: f32,
|
|
947
|
+
_padding6: f32,
|
|
948
|
+
_padding7: f32,
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
952
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
953
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
954
|
+
|
|
955
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
956
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
957
|
+
// Extract bright areas above threshold
|
|
958
|
+
let threshold = extractUniforms.threshold;
|
|
959
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
960
|
+
return vec4f(bloom, color.a);
|
|
961
|
+
}
|
|
962
|
+
`,
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
966
|
+
const bloomBlurShader = this.device.createShaderModule({
|
|
967
|
+
label: "bloom blur",
|
|
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 BlurUniforms {
|
|
984
|
+
direction: vec2f,
|
|
985
|
+
_padding1: f32,
|
|
986
|
+
_padding2: f32,
|
|
987
|
+
_padding3: f32,
|
|
988
|
+
_padding4: f32,
|
|
989
|
+
_padding5: f32,
|
|
990
|
+
_padding6: f32,
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
994
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
995
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
996
|
+
|
|
997
|
+
// 9-tap gaussian blur
|
|
998
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
999
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
1000
|
+
var result = vec4f(0.0);
|
|
1001
|
+
|
|
1002
|
+
// Gaussian weights for 9-tap filter
|
|
1003
|
+
let weights = array<f32, 9>(
|
|
1004
|
+
0.01621622, 0.05405405, 0.12162162,
|
|
1005
|
+
0.19459459, 0.22702703,
|
|
1006
|
+
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
1010
|
+
|
|
1011
|
+
for (var i = 0u; i < 9u; i++) {
|
|
1012
|
+
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
1013
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1018
|
+
`,
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
// Bloom composition shader (combines original scene with bloom)
|
|
1022
|
+
const bloomComposeShader = this.device.createShaderModule({
|
|
1023
|
+
label: "bloom compose",
|
|
1024
|
+
code: /* wgsl */ `
|
|
1025
|
+
struct VertexOutput {
|
|
1026
|
+
@builtin(position) position: vec4f,
|
|
1027
|
+
@location(0) uv: vec2f,
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1031
|
+
var output: VertexOutput;
|
|
1032
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1033
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1034
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1035
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1036
|
+
return output;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
struct BloomComposeUniforms {
|
|
1040
|
+
intensity: f32,
|
|
1041
|
+
_padding1: f32,
|
|
1042
|
+
_padding2: f32,
|
|
1043
|
+
_padding3: f32,
|
|
1044
|
+
_padding4: f32,
|
|
1045
|
+
_padding5: f32,
|
|
1046
|
+
_padding6: f32,
|
|
1047
|
+
_padding7: f32,
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
1051
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
1052
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1053
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1054
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1055
|
+
|
|
1056
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1057
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1058
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1059
|
+
// Additive blending with intensity control
|
|
1060
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1061
|
+
return vec4f(result, scene.a);
|
|
1062
|
+
}
|
|
1063
|
+
`,
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
1067
|
+
const blurDirectionBuffer = this.device.createBuffer({
|
|
1068
|
+
label: "blur direction",
|
|
1069
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1070
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
// Create uniform buffer for bloom intensity (minimum 32 bytes for WebGPU)
|
|
1074
|
+
const bloomIntensityBuffer = this.device.createBuffer({
|
|
1075
|
+
label: "bloom intensity",
|
|
1076
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1077
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
// Create uniform buffer for bloom threshold (minimum 32 bytes for WebGPU)
|
|
1081
|
+
const bloomThresholdBuffer = this.device.createBuffer({
|
|
1082
|
+
label: "bloom threshold",
|
|
1083
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1084
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
// Set default bloom values
|
|
1088
|
+
const intensityData = new Float32Array(8) // f32 + 7 padding floats = 8 floats = 32 bytes
|
|
1089
|
+
intensityData[0] = this.bloomIntensity
|
|
1090
|
+
this.device.queue.writeBuffer(bloomIntensityBuffer, 0, intensityData)
|
|
1091
|
+
|
|
1092
|
+
const thresholdData = new Float32Array(8) // f32 + 7 padding floats = 8 floats = 32 bytes
|
|
1093
|
+
thresholdData[0] = this.bloomThreshold
|
|
1094
|
+
this.device.queue.writeBuffer(bloomThresholdBuffer, 0, thresholdData)
|
|
1095
|
+
|
|
1096
|
+
// Create linear sampler for post-processing
|
|
1097
|
+
const linearSampler = this.device.createSampler({
|
|
1098
|
+
magFilter: "linear",
|
|
1099
|
+
minFilter: "linear",
|
|
1100
|
+
addressModeU: "clamp-to-edge",
|
|
1101
|
+
addressModeV: "clamp-to-edge",
|
|
1102
|
+
})
|
|
1103
|
+
|
|
1104
|
+
// Bloom extraction pipeline
|
|
1105
|
+
this.bloomExtractPipeline = this.device.createRenderPipeline({
|
|
1106
|
+
label: "bloom extract",
|
|
1107
|
+
layout: "auto",
|
|
1108
|
+
vertex: {
|
|
1109
|
+
module: bloomExtractShader,
|
|
1110
|
+
entryPoint: "vs",
|
|
1111
|
+
},
|
|
1112
|
+
fragment: {
|
|
1113
|
+
module: bloomExtractShader,
|
|
1114
|
+
entryPoint: "fs",
|
|
1115
|
+
targets: [{ format: this.presentationFormat }],
|
|
1116
|
+
},
|
|
1117
|
+
primitive: { topology: "triangle-list" },
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
// Bloom blur pipeline
|
|
1121
|
+
this.bloomBlurPipeline = this.device.createRenderPipeline({
|
|
1122
|
+
label: "bloom blur",
|
|
1123
|
+
layout: "auto",
|
|
1124
|
+
vertex: {
|
|
1125
|
+
module: bloomBlurShader,
|
|
1126
|
+
entryPoint: "vs",
|
|
1127
|
+
},
|
|
1128
|
+
fragment: {
|
|
1129
|
+
module: bloomBlurShader,
|
|
1130
|
+
entryPoint: "fs",
|
|
1131
|
+
targets: [{ format: this.presentationFormat }],
|
|
1132
|
+
},
|
|
1133
|
+
primitive: { topology: "triangle-list" },
|
|
1134
|
+
})
|
|
1135
|
+
|
|
1136
|
+
// Bloom composition pipeline
|
|
1137
|
+
this.bloomComposePipeline = this.device.createRenderPipeline({
|
|
1138
|
+
label: "bloom compose",
|
|
1139
|
+
layout: "auto",
|
|
1140
|
+
vertex: {
|
|
1141
|
+
module: bloomComposeShader,
|
|
1142
|
+
entryPoint: "vs",
|
|
1143
|
+
},
|
|
1144
|
+
fragment: {
|
|
1145
|
+
module: bloomComposeShader,
|
|
1146
|
+
entryPoint: "fs",
|
|
1147
|
+
targets: [{ format: this.presentationFormat }],
|
|
1148
|
+
},
|
|
1149
|
+
primitive: { topology: "triangle-list" },
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
// Store buffers and sampler for later use
|
|
1153
|
+
this.blurDirectionBuffer = blurDirectionBuffer
|
|
1154
|
+
this.bloomIntensityBuffer = bloomIntensityBuffer
|
|
1155
|
+
this.bloomThresholdBuffer = bloomThresholdBuffer
|
|
1156
|
+
this.linearSampler = linearSampler
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Step 3: Setup canvas resize handling
|
|
1160
|
+
private setupResize() {
|
|
1161
|
+
this.resizeObserver = new ResizeObserver(() => this.handleResize())
|
|
1162
|
+
this.resizeObserver.observe(this.canvas)
|
|
1163
|
+
this.handleResize()
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private handleResize() {
|
|
1167
|
+
const displayWidth = this.canvas.clientWidth
|
|
1168
|
+
const displayHeight = this.canvas.clientHeight
|
|
1169
|
+
|
|
1170
|
+
const dpr = window.devicePixelRatio || 1
|
|
1171
|
+
const width = Math.floor(displayWidth * dpr)
|
|
1172
|
+
const height = Math.floor(displayHeight * dpr)
|
|
1173
|
+
|
|
1174
|
+
if (!this.multisampleTexture || this.canvas.width !== width || this.canvas.height !== height) {
|
|
1175
|
+
this.canvas.width = width
|
|
1176
|
+
this.canvas.height = height
|
|
1177
|
+
|
|
1178
|
+
this.multisampleTexture = this.device.createTexture({
|
|
1179
|
+
label: "multisample render target",
|
|
1180
|
+
size: [width, height],
|
|
1181
|
+
sampleCount: this.sampleCount,
|
|
1182
|
+
format: this.presentationFormat,
|
|
1183
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
this.depthTexture = this.device.createTexture({
|
|
1187
|
+
label: "depth texture",
|
|
1188
|
+
size: [width, height],
|
|
1189
|
+
sampleCount: this.sampleCount,
|
|
1190
|
+
format: "depth24plus-stencil8",
|
|
1191
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
// Create scene render texture (non-multisampled for post-processing)
|
|
1195
|
+
this.sceneRenderTexture = this.device.createTexture({
|
|
1196
|
+
label: "scene render texture",
|
|
1197
|
+
size: [width, height],
|
|
1198
|
+
format: this.presentationFormat,
|
|
1199
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1200
|
+
})
|
|
1201
|
+
this.sceneRenderTextureView = this.sceneRenderTexture.createView()
|
|
1202
|
+
|
|
1203
|
+
// Create bloom textures (half resolution for performance)
|
|
1204
|
+
const bloomWidth = Math.floor(width / 2)
|
|
1205
|
+
const bloomHeight = Math.floor(height / 2)
|
|
1206
|
+
this.bloomExtractTexture = this.device.createTexture({
|
|
1207
|
+
label: "bloom extract",
|
|
1208
|
+
size: [bloomWidth, bloomHeight],
|
|
1209
|
+
format: this.presentationFormat,
|
|
1210
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1211
|
+
})
|
|
1212
|
+
this.bloomBlurTexture1 = this.device.createTexture({
|
|
1213
|
+
label: "bloom blur 1",
|
|
1214
|
+
size: [bloomWidth, bloomHeight],
|
|
1215
|
+
format: this.presentationFormat,
|
|
1216
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1217
|
+
})
|
|
1218
|
+
this.bloomBlurTexture2 = this.device.createTexture({
|
|
1219
|
+
label: "bloom blur 2",
|
|
1220
|
+
size: [bloomWidth, bloomHeight],
|
|
1221
|
+
format: this.presentationFormat,
|
|
1222
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1223
|
+
})
|
|
1224
|
+
|
|
1225
|
+
const depthTextureView = this.depthTexture.createView()
|
|
1226
|
+
|
|
1227
|
+
// Render scene to texture instead of directly to canvas
|
|
1228
|
+
const colorAttachment: GPURenderPassColorAttachment =
|
|
1229
|
+
this.sampleCount > 1
|
|
1230
|
+
? {
|
|
1231
|
+
view: this.multisampleTexture.createView(),
|
|
1232
|
+
resolveTarget: this.sceneRenderTextureView,
|
|
1233
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1234
|
+
loadOp: "clear",
|
|
1235
|
+
storeOp: "store",
|
|
1236
|
+
}
|
|
1237
|
+
: {
|
|
1238
|
+
view: this.sceneRenderTextureView,
|
|
1239
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1240
|
+
loadOp: "clear",
|
|
1241
|
+
storeOp: "store",
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
this.renderPassDescriptor = {
|
|
1245
|
+
label: "renderPass",
|
|
1246
|
+
colorAttachments: [colorAttachment],
|
|
1247
|
+
depthStencilAttachment: {
|
|
1248
|
+
view: depthTextureView,
|
|
1249
|
+
depthClearValue: 1.0,
|
|
1250
|
+
depthLoadOp: "clear",
|
|
1251
|
+
depthStoreOp: "store",
|
|
1252
|
+
stencilClearValue: 0, // New: clear stencil to 0
|
|
1253
|
+
stencilLoadOp: "clear", // New: clear stencil each frame
|
|
1254
|
+
stencilStoreOp: "store", // New: store stencil
|
|
1255
|
+
},
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
this.camera.aspect = width / height
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Step 4: Create camera and uniform buffer
|
|
1263
|
+
private setupCamera() {
|
|
1264
|
+
this.cameraUniformBuffer = this.device.createBuffer({
|
|
1265
|
+
label: "camera uniforms",
|
|
1266
|
+
size: 40 * 4,
|
|
1267
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
this.camera = new Camera(Math.PI, Math.PI / 2.5, 26.6, new Vec3(0, 12.5, 0))
|
|
1271
|
+
|
|
1272
|
+
this.camera.aspect = this.canvas.width / this.canvas.height
|
|
1273
|
+
this.camera.attachControl(this.canvas)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Step 5: Create lighting buffers
|
|
1277
|
+
private setupLighting() {
|
|
1278
|
+
this.lightUniformBuffer = this.device.createBuffer({
|
|
1279
|
+
label: "light uniforms",
|
|
1280
|
+
size: 64 * 4,
|
|
1281
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
this.lightCount = 0
|
|
1285
|
+
|
|
1286
|
+
this.setAmbient(0.96)
|
|
1287
|
+
this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.12)
|
|
1288
|
+
this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.1)
|
|
1289
|
+
this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.08)
|
|
1290
|
+
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
public addLight(direction: Vec3, color: Vec3, intensity: number = 1.0): boolean {
|
|
1294
|
+
if (this.lightCount >= 4) return false
|
|
1295
|
+
|
|
1296
|
+
const normalized = direction.normalize()
|
|
1297
|
+
const baseIndex = 4 + this.lightCount * 8
|
|
1298
|
+
this.lightData[baseIndex] = normalized.x
|
|
1299
|
+
this.lightData[baseIndex + 1] = normalized.y
|
|
1300
|
+
this.lightData[baseIndex + 2] = normalized.z
|
|
1301
|
+
this.lightData[baseIndex + 3] = 0
|
|
1302
|
+
this.lightData[baseIndex + 4] = color.x
|
|
1303
|
+
this.lightData[baseIndex + 5] = color.y
|
|
1304
|
+
this.lightData[baseIndex + 6] = color.z
|
|
1305
|
+
this.lightData[baseIndex + 7] = intensity
|
|
1306
|
+
|
|
1307
|
+
this.lightCount++
|
|
1308
|
+
this.lightData[1] = this.lightCount
|
|
1309
|
+
return true
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
public setAmbient(intensity: number) {
|
|
1313
|
+
this.lightData[0] = intensity
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
public getStats(): EngineStats {
|
|
1317
|
+
return { ...this.stats }
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
public runRenderLoop(callback?: () => void) {
|
|
1321
|
+
this.renderLoopCallback = callback || null
|
|
1322
|
+
|
|
1323
|
+
const loop = () => {
|
|
1324
|
+
this.render()
|
|
1325
|
+
|
|
1326
|
+
if (this.renderLoopCallback) {
|
|
1327
|
+
this.renderLoopCallback()
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
this.animationFrameId = requestAnimationFrame(loop)
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
this.animationFrameId = requestAnimationFrame(loop)
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
public stopRenderLoop() {
|
|
1337
|
+
if (this.animationFrameId !== null) {
|
|
1338
|
+
cancelAnimationFrame(this.animationFrameId)
|
|
1339
|
+
this.animationFrameId = null
|
|
1340
|
+
}
|
|
1341
|
+
this.renderLoopCallback = null
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
public dispose() {
|
|
1345
|
+
this.stopRenderLoop()
|
|
1346
|
+
if (this.camera) this.camera.detachControl()
|
|
1347
|
+
if (this.resizeObserver) {
|
|
1348
|
+
this.resizeObserver.disconnect()
|
|
1349
|
+
this.resizeObserver = null
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Step 6: Load PMX model file
|
|
1354
|
+
public async loadModel(path: string) {
|
|
1355
|
+
const pathParts = path.split("/")
|
|
1356
|
+
pathParts.pop()
|
|
1357
|
+
const dir = pathParts.join("/") + "/"
|
|
1358
|
+
this.modelDir = dir
|
|
1359
|
+
|
|
1360
|
+
const model = await PmxLoader.load(path)
|
|
1361
|
+
this.physics = new Physics(model.getRigidbodies(), model.getJoints())
|
|
1362
|
+
await this.setupModelBuffers(model)
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
public rotateBones(bones: string[], rotations: Quat[], durationMs?: number) {
|
|
1366
|
+
this.currentModel?.rotateBones(bones, rotations, durationMs)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Step 7: Create vertex, index, and joint buffers
|
|
1370
|
+
private async setupModelBuffers(model: Model) {
|
|
1371
|
+
this.currentModel = model
|
|
1372
|
+
const vertices = model.getVertices()
|
|
1373
|
+
const skinning = model.getSkinning()
|
|
1374
|
+
const skeleton = model.getSkeleton()
|
|
1375
|
+
|
|
1376
|
+
this.vertexBuffer = this.device.createBuffer({
|
|
1377
|
+
label: "model vertex buffer",
|
|
1378
|
+
size: vertices.byteLength,
|
|
1379
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1380
|
+
})
|
|
1381
|
+
this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices)
|
|
1382
|
+
|
|
1383
|
+
this.jointsBuffer = this.device.createBuffer({
|
|
1384
|
+
label: "joints buffer",
|
|
1385
|
+
size: skinning.joints.byteLength,
|
|
1386
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1387
|
+
})
|
|
1388
|
+
this.device.queue.writeBuffer(
|
|
1389
|
+
this.jointsBuffer,
|
|
1390
|
+
0,
|
|
1391
|
+
skinning.joints.buffer,
|
|
1392
|
+
skinning.joints.byteOffset,
|
|
1393
|
+
skinning.joints.byteLength
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
this.weightsBuffer = this.device.createBuffer({
|
|
1397
|
+
label: "weights buffer",
|
|
1398
|
+
size: skinning.weights.byteLength,
|
|
1399
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1400
|
+
})
|
|
1401
|
+
this.device.queue.writeBuffer(
|
|
1402
|
+
this.weightsBuffer,
|
|
1403
|
+
0,
|
|
1404
|
+
skinning.weights.buffer,
|
|
1405
|
+
skinning.weights.byteOffset,
|
|
1406
|
+
skinning.weights.byteLength
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
const boneCount = skeleton.bones.length
|
|
1410
|
+
const matrixSize = boneCount * 16 * 4
|
|
1411
|
+
|
|
1412
|
+
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1413
|
+
label: "skin matrices",
|
|
1414
|
+
size: Math.max(256, matrixSize),
|
|
1415
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
|
|
1416
|
+
})
|
|
1417
|
+
|
|
1418
|
+
this.worldMatrixBuffer = this.device.createBuffer({
|
|
1419
|
+
label: "world matrices",
|
|
1420
|
+
size: Math.max(256, matrixSize),
|
|
1421
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1422
|
+
})
|
|
1423
|
+
|
|
1424
|
+
this.inverseBindMatrixBuffer = this.device.createBuffer({
|
|
1425
|
+
label: "inverse bind matrices",
|
|
1426
|
+
size: Math.max(256, matrixSize),
|
|
1427
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1428
|
+
})
|
|
1429
|
+
|
|
1430
|
+
const invBindMatrices = skeleton.inverseBindMatrices
|
|
1431
|
+
this.device.queue.writeBuffer(
|
|
1432
|
+
this.inverseBindMatrixBuffer,
|
|
1433
|
+
0,
|
|
1434
|
+
invBindMatrices.buffer,
|
|
1435
|
+
invBindMatrices.byteOffset,
|
|
1436
|
+
invBindMatrices.byteLength
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
this.boneCountBuffer = this.device.createBuffer({
|
|
1440
|
+
label: "bone count uniform",
|
|
1441
|
+
size: 32, // Minimum uniform buffer size is 32 bytes
|
|
1442
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1443
|
+
})
|
|
1444
|
+
const boneCountData = new Uint32Array(8) // 32 bytes total
|
|
1445
|
+
boneCountData[0] = boneCount
|
|
1446
|
+
this.device.queue.writeBuffer(this.boneCountBuffer, 0, boneCountData)
|
|
1447
|
+
|
|
1448
|
+
this.createSkinMatrixComputePipeline()
|
|
1449
|
+
|
|
1450
|
+
const indices = model.getIndices()
|
|
1451
|
+
if (indices) {
|
|
1452
|
+
this.indexBuffer = this.device.createBuffer({
|
|
1453
|
+
label: "model index buffer",
|
|
1454
|
+
size: indices.byteLength,
|
|
1455
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1456
|
+
})
|
|
1457
|
+
this.device.queue.writeBuffer(this.indexBuffer, 0, indices)
|
|
1458
|
+
} else {
|
|
1459
|
+
throw new Error("Model has no index buffer")
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
await this.setupMaterials(model)
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private opaqueNonEyeNonHairDraws: {
|
|
1466
|
+
count: number
|
|
1467
|
+
firstIndex: number
|
|
1468
|
+
bindGroup: GPUBindGroup
|
|
1469
|
+
isTransparent: boolean
|
|
1470
|
+
}[] = []
|
|
1471
|
+
private eyeDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
1472
|
+
private hairDrawsOverEyes: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] =
|
|
1473
|
+
[]
|
|
1474
|
+
private hairDrawsOverNonEyes: {
|
|
1475
|
+
count: number
|
|
1476
|
+
firstIndex: number
|
|
1477
|
+
bindGroup: GPUBindGroup
|
|
1478
|
+
isTransparent: boolean
|
|
1479
|
+
}[] = []
|
|
1480
|
+
private transparentNonEyeNonHairDraws: {
|
|
1481
|
+
count: number
|
|
1482
|
+
firstIndex: number
|
|
1483
|
+
bindGroup: GPUBindGroup
|
|
1484
|
+
isTransparent: boolean
|
|
1485
|
+
}[] = []
|
|
1486
|
+
private opaqueNonEyeNonHairOutlineDraws: {
|
|
1487
|
+
count: number
|
|
1488
|
+
firstIndex: number
|
|
1489
|
+
bindGroup: GPUBindGroup
|
|
1490
|
+
isTransparent: boolean
|
|
1491
|
+
}[] = []
|
|
1492
|
+
private eyeOutlineDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
1493
|
+
private hairOutlineDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] =
|
|
1494
|
+
[]
|
|
1495
|
+
private transparentNonEyeNonHairOutlineDraws: {
|
|
1496
|
+
count: number
|
|
1497
|
+
firstIndex: number
|
|
1498
|
+
bindGroup: GPUBindGroup
|
|
1499
|
+
isTransparent: boolean
|
|
1500
|
+
}[] = []
|
|
1501
|
+
|
|
1502
|
+
// Step 8: Load textures and create material bind groups
|
|
1503
|
+
private async setupMaterials(model: Model) {
|
|
1504
|
+
const materials = model.getMaterials()
|
|
1505
|
+
if (materials.length === 0) {
|
|
1506
|
+
throw new Error("Model has no materials")
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
const textures = model.getTextures()
|
|
1510
|
+
|
|
1511
|
+
const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
|
|
1512
|
+
if (texIndex < 0 || texIndex >= textures.length) {
|
|
1513
|
+
return null
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const path = this.modelDir + textures[texIndex].path
|
|
1517
|
+
const texture = await this.createTextureFromPath(path)
|
|
1518
|
+
return texture
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const loadToonTexture = async (toonTextureIndex: number): Promise<GPUTexture> => {
|
|
1522
|
+
const texture = await loadTextureByIndex(toonTextureIndex)
|
|
1523
|
+
if (texture) return texture
|
|
1524
|
+
|
|
1525
|
+
// Default toon texture fallback - cache it
|
|
1526
|
+
const defaultToonPath = "__default_toon__"
|
|
1527
|
+
const cached = this.textureCache.get(defaultToonPath)
|
|
1528
|
+
if (cached) return cached
|
|
1529
|
+
|
|
1530
|
+
const defaultToonData = new Uint8Array(256 * 2 * 4)
|
|
1531
|
+
for (let i = 0; i < 256; i++) {
|
|
1532
|
+
const factor = i / 255.0
|
|
1533
|
+
const gray = Math.floor(128 + factor * 127)
|
|
1534
|
+
defaultToonData[i * 4] = gray
|
|
1535
|
+
defaultToonData[i * 4 + 1] = gray
|
|
1536
|
+
defaultToonData[i * 4 + 2] = gray
|
|
1537
|
+
defaultToonData[i * 4 + 3] = 255
|
|
1538
|
+
defaultToonData[(256 + i) * 4] = gray
|
|
1539
|
+
defaultToonData[(256 + i) * 4 + 1] = gray
|
|
1540
|
+
defaultToonData[(256 + i) * 4 + 2] = gray
|
|
1541
|
+
defaultToonData[(256 + i) * 4 + 3] = 255
|
|
1542
|
+
}
|
|
1543
|
+
const defaultToonTexture = this.device.createTexture({
|
|
1544
|
+
label: "default toon texture",
|
|
1545
|
+
size: [256, 2],
|
|
1546
|
+
format: "rgba8unorm",
|
|
1547
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
1548
|
+
})
|
|
1549
|
+
this.device.queue.writeTexture(
|
|
1550
|
+
{ texture: defaultToonTexture },
|
|
1551
|
+
defaultToonData,
|
|
1552
|
+
{ bytesPerRow: 256 * 4 },
|
|
1553
|
+
[256, 2]
|
|
1554
|
+
)
|
|
1555
|
+
this.textureCache.set(defaultToonPath, defaultToonTexture)
|
|
1556
|
+
this.textureSizes.set(defaultToonPath, { width: 256, height: 2 })
|
|
1557
|
+
return defaultToonTexture
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
this.opaqueNonEyeNonHairDraws = []
|
|
1561
|
+
this.eyeDraws = []
|
|
1562
|
+
this.hairDrawsOverEyes = []
|
|
1563
|
+
this.hairDrawsOverNonEyes = []
|
|
1564
|
+
this.transparentNonEyeNonHairDraws = []
|
|
1565
|
+
this.opaqueNonEyeNonHairOutlineDraws = []
|
|
1566
|
+
this.eyeOutlineDraws = []
|
|
1567
|
+
this.hairOutlineDraws = []
|
|
1568
|
+
this.transparentNonEyeNonHairOutlineDraws = []
|
|
1569
|
+
let runningFirstIndex = 0
|
|
1570
|
+
|
|
1571
|
+
for (const mat of materials) {
|
|
1572
|
+
const matCount = mat.vertexCount | 0
|
|
1573
|
+
if (matCount === 0) continue
|
|
1574
|
+
|
|
1575
|
+
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex)
|
|
1576
|
+
if (!diffuseTexture) throw new Error(`Material "${mat.name}" has no diffuse texture`)
|
|
1577
|
+
|
|
1578
|
+
const toonTexture = await loadToonTexture(mat.toonTextureIndex)
|
|
1579
|
+
|
|
1580
|
+
const materialAlpha = mat.diffuse[3]
|
|
1581
|
+
const EPSILON = 0.001
|
|
1582
|
+
const isTransparent = materialAlpha < 1.0 - EPSILON
|
|
1583
|
+
|
|
1584
|
+
// Create material uniform data
|
|
1585
|
+
const materialUniformData = new Float32Array(8)
|
|
1586
|
+
materialUniformData[0] = materialAlpha
|
|
1587
|
+
materialUniformData[1] = 1.0 // alphaMultiplier: 1.0 for non-hair materials
|
|
1588
|
+
materialUniformData[2] = this.rimLightIntensity
|
|
1589
|
+
materialUniformData[3] = this.rimLightPower
|
|
1590
|
+
materialUniformData[4] = this.rimLightColor[0] // rimColor.r
|
|
1591
|
+
materialUniformData[5] = this.rimLightColor[1] // rimColor.g
|
|
1592
|
+
materialUniformData[6] = this.rimLightColor[2] // rimColor.b
|
|
1593
|
+
materialUniformData[7] = 0.0 // _padding1
|
|
1594
|
+
|
|
1595
|
+
const materialUniformBuffer = this.device.createBuffer({
|
|
1596
|
+
label: `material uniform: ${mat.name}`,
|
|
1597
|
+
size: materialUniformData.byteLength,
|
|
1598
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1599
|
+
})
|
|
1600
|
+
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1601
|
+
|
|
1602
|
+
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1603
|
+
const bindGroup = this.device.createBindGroup({
|
|
1604
|
+
label: `material bind group: ${mat.name}`,
|
|
1605
|
+
layout: this.hairBindGroupLayout,
|
|
1606
|
+
entries: [
|
|
1607
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1608
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1609
|
+
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1610
|
+
{ binding: 3, resource: this.textureSampler },
|
|
1611
|
+
{ binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1612
|
+
{ binding: 5, resource: toonTexture.createView() },
|
|
1613
|
+
{ binding: 6, resource: this.textureSampler },
|
|
1614
|
+
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
1615
|
+
],
|
|
1616
|
+
})
|
|
1617
|
+
|
|
1618
|
+
// Classify materials into appropriate draw lists
|
|
1619
|
+
if (mat.isEye) {
|
|
1620
|
+
this.eyeDraws.push({
|
|
1621
|
+
count: matCount,
|
|
1622
|
+
firstIndex: runningFirstIndex,
|
|
1623
|
+
bindGroup,
|
|
1624
|
+
isTransparent,
|
|
1625
|
+
})
|
|
1626
|
+
} else if (mat.isHair) {
|
|
1627
|
+
// For hair materials, create two bind groups: one for over-eyes (alphaMultiplier = 0.5) and one for over-non-eyes (alphaMultiplier = 1.0)
|
|
1628
|
+
const materialUniformDataOverEyes = new Float32Array(8)
|
|
1629
|
+
materialUniformDataOverEyes[0] = materialAlpha
|
|
1630
|
+
materialUniformDataOverEyes[1] = 0.5 // alphaMultiplier: 0.5 for over-eyes
|
|
1631
|
+
materialUniformDataOverEyes[2] = this.rimLightIntensity
|
|
1632
|
+
materialUniformDataOverEyes[3] = this.rimLightPower
|
|
1633
|
+
materialUniformDataOverEyes[4] = this.rimLightColor[0] // rimColor.r
|
|
1634
|
+
materialUniformDataOverEyes[5] = this.rimLightColor[1] // rimColor.g
|
|
1635
|
+
materialUniformDataOverEyes[6] = this.rimLightColor[2] // rimColor.b
|
|
1636
|
+
materialUniformDataOverEyes[7] = 0.0 // _padding1
|
|
1637
|
+
|
|
1638
|
+
const materialUniformBufferOverEyes = this.device.createBuffer({
|
|
1639
|
+
label: `material uniform (over eyes): ${mat.name}`,
|
|
1640
|
+
size: materialUniformDataOverEyes.byteLength,
|
|
1641
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1642
|
+
})
|
|
1643
|
+
this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes)
|
|
1644
|
+
|
|
1645
|
+
const bindGroupOverEyes = this.device.createBindGroup({
|
|
1646
|
+
label: `material bind group (over eyes): ${mat.name}`,
|
|
1647
|
+
layout: this.hairBindGroupLayout,
|
|
1648
|
+
entries: [
|
|
1649
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1650
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1651
|
+
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1652
|
+
{ binding: 3, resource: this.textureSampler },
|
|
1653
|
+
{ binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1654
|
+
{ binding: 5, resource: toonTexture.createView() },
|
|
1655
|
+
{ binding: 6, resource: this.textureSampler },
|
|
1656
|
+
{ binding: 7, resource: { buffer: materialUniformBufferOverEyes } },
|
|
1657
|
+
],
|
|
1658
|
+
})
|
|
1659
|
+
|
|
1660
|
+
this.hairDrawsOverEyes.push({
|
|
1661
|
+
count: matCount,
|
|
1662
|
+
firstIndex: runningFirstIndex,
|
|
1663
|
+
bindGroup: bindGroupOverEyes,
|
|
1664
|
+
isTransparent,
|
|
1665
|
+
})
|
|
1666
|
+
|
|
1667
|
+
// Create material uniform for hair over non-eyes (alphaMultiplier = 1.0)
|
|
1668
|
+
const materialUniformDataOverNonEyes = new Float32Array(8)
|
|
1669
|
+
materialUniformDataOverNonEyes[0] = materialAlpha
|
|
1670
|
+
materialUniformDataOverNonEyes[1] = 1.0 // alphaMultiplier: 1.0 for over-non-eyes
|
|
1671
|
+
materialUniformDataOverNonEyes[2] = this.rimLightIntensity
|
|
1672
|
+
materialUniformDataOverNonEyes[3] = this.rimLightPower
|
|
1673
|
+
materialUniformDataOverNonEyes[4] = this.rimLightColor[0] // rimColor.r
|
|
1674
|
+
materialUniformDataOverNonEyes[5] = this.rimLightColor[1] // rimColor.g
|
|
1675
|
+
materialUniformDataOverNonEyes[6] = this.rimLightColor[2] // rimColor.b
|
|
1676
|
+
materialUniformDataOverNonEyes[7] = 0.0 // _padding1
|
|
1677
|
+
|
|
1678
|
+
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1679
|
+
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
1680
|
+
size: materialUniformDataOverNonEyes.byteLength,
|
|
1681
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1682
|
+
})
|
|
1683
|
+
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes)
|
|
1684
|
+
|
|
1685
|
+
const bindGroupOverNonEyes = this.device.createBindGroup({
|
|
1686
|
+
label: `material bind group (over non-eyes): ${mat.name}`,
|
|
1687
|
+
layout: this.hairBindGroupLayout,
|
|
1688
|
+
entries: [
|
|
1689
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1690
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1691
|
+
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1692
|
+
{ binding: 3, resource: this.textureSampler },
|
|
1693
|
+
{ binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1694
|
+
{ binding: 5, resource: toonTexture.createView() },
|
|
1695
|
+
{ binding: 6, resource: this.textureSampler },
|
|
1696
|
+
{ binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
|
|
1697
|
+
],
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
this.hairDrawsOverNonEyes.push({
|
|
1701
|
+
count: matCount,
|
|
1702
|
+
firstIndex: runningFirstIndex,
|
|
1703
|
+
bindGroup: bindGroupOverNonEyes,
|
|
1704
|
+
isTransparent,
|
|
1705
|
+
})
|
|
1706
|
+
} else if (isTransparent) {
|
|
1707
|
+
this.transparentNonEyeNonHairDraws.push({
|
|
1708
|
+
count: matCount,
|
|
1709
|
+
firstIndex: runningFirstIndex,
|
|
1710
|
+
bindGroup,
|
|
1711
|
+
isTransparent,
|
|
1712
|
+
})
|
|
1713
|
+
} else {
|
|
1714
|
+
this.opaqueNonEyeNonHairDraws.push({
|
|
1715
|
+
count: matCount,
|
|
1716
|
+
firstIndex: runningFirstIndex,
|
|
1717
|
+
bindGroup,
|
|
1718
|
+
isTransparent,
|
|
1719
|
+
})
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Outline for all materials (including transparent) - Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1723
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1724
|
+
const materialUniformData = new Float32Array(8)
|
|
1725
|
+
materialUniformData[0] = mat.edgeColor[0]
|
|
1726
|
+
materialUniformData[1] = mat.edgeColor[1]
|
|
1727
|
+
materialUniformData[2] = mat.edgeColor[2]
|
|
1728
|
+
materialUniformData[3] = mat.edgeColor[3]
|
|
1729
|
+
materialUniformData[4] = mat.edgeSize
|
|
1730
|
+
|
|
1731
|
+
const materialUniformBuffer = this.device.createBuffer({
|
|
1732
|
+
label: `outline material uniform: ${mat.name}`,
|
|
1733
|
+
size: materialUniformData.byteLength,
|
|
1734
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1735
|
+
})
|
|
1736
|
+
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1737
|
+
|
|
1738
|
+
const outlineBindGroup = this.device.createBindGroup({
|
|
1739
|
+
label: `outline bind group: ${mat.name}`,
|
|
1740
|
+
layout: this.outlineBindGroupLayout,
|
|
1741
|
+
entries: [
|
|
1742
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1743
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
1744
|
+
{ binding: 2, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1745
|
+
],
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
// Classify outlines into appropriate draw lists
|
|
1749
|
+
if (mat.isEye) {
|
|
1750
|
+
this.eyeOutlineDraws.push({
|
|
1751
|
+
count: matCount,
|
|
1752
|
+
firstIndex: runningFirstIndex,
|
|
1753
|
+
bindGroup: outlineBindGroup,
|
|
1754
|
+
isTransparent,
|
|
1755
|
+
})
|
|
1756
|
+
} else if (mat.isHair) {
|
|
1757
|
+
this.hairOutlineDraws.push({
|
|
1758
|
+
count: matCount,
|
|
1759
|
+
firstIndex: runningFirstIndex,
|
|
1760
|
+
bindGroup: outlineBindGroup,
|
|
1761
|
+
isTransparent,
|
|
1762
|
+
})
|
|
1763
|
+
} else if (isTransparent) {
|
|
1764
|
+
this.transparentNonEyeNonHairOutlineDraws.push({
|
|
1765
|
+
count: matCount,
|
|
1766
|
+
firstIndex: runningFirstIndex,
|
|
1767
|
+
bindGroup: outlineBindGroup,
|
|
1768
|
+
isTransparent,
|
|
1769
|
+
})
|
|
1770
|
+
} else {
|
|
1771
|
+
this.opaqueNonEyeNonHairOutlineDraws.push({
|
|
1772
|
+
count: matCount,
|
|
1773
|
+
firstIndex: runningFirstIndex,
|
|
1774
|
+
bindGroup: outlineBindGroup,
|
|
1775
|
+
isTransparent,
|
|
1776
|
+
})
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
runningFirstIndex += matCount
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Helper: Load texture from file path with optional max size limit
|
|
1785
|
+
private async createTextureFromPath(path: string, maxSize: number = 2048): Promise<GPUTexture | null> {
|
|
1786
|
+
const cached = this.textureCache.get(path)
|
|
1787
|
+
if (cached) {
|
|
1788
|
+
return cached
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
try {
|
|
1792
|
+
const response = await fetch(path)
|
|
1793
|
+
if (!response.ok) {
|
|
1794
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
1795
|
+
}
|
|
1796
|
+
let imageBitmap = await createImageBitmap(await response.blob(), {
|
|
1797
|
+
premultiplyAlpha: "none",
|
|
1798
|
+
colorSpaceConversion: "none",
|
|
1799
|
+
})
|
|
1800
|
+
|
|
1801
|
+
// Downscale if texture is too large
|
|
1802
|
+
let finalWidth = imageBitmap.width
|
|
1803
|
+
let finalHeight = imageBitmap.height
|
|
1804
|
+
if (finalWidth > maxSize || finalHeight > maxSize) {
|
|
1805
|
+
const scale = Math.min(maxSize / finalWidth, maxSize / finalHeight)
|
|
1806
|
+
finalWidth = Math.floor(finalWidth * scale)
|
|
1807
|
+
finalHeight = Math.floor(finalHeight * scale)
|
|
1808
|
+
|
|
1809
|
+
// Create canvas to downscale
|
|
1810
|
+
const canvas = new OffscreenCanvas(finalWidth, finalHeight)
|
|
1811
|
+
const ctx = canvas.getContext("2d")
|
|
1812
|
+
if (ctx) {
|
|
1813
|
+
ctx.drawImage(imageBitmap, 0, 0, finalWidth, finalHeight)
|
|
1814
|
+
imageBitmap = await createImageBitmap(canvas)
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
const texture = this.device.createTexture({
|
|
1819
|
+
label: `texture: ${path}`,
|
|
1820
|
+
size: [finalWidth, finalHeight],
|
|
1821
|
+
format: "rgba8unorm",
|
|
1822
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1823
|
+
})
|
|
1824
|
+
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [finalWidth, finalHeight])
|
|
1825
|
+
|
|
1826
|
+
this.textureCache.set(path, texture)
|
|
1827
|
+
this.textureSizes.set(path, { width: finalWidth, height: finalHeight })
|
|
1828
|
+
return texture
|
|
1829
|
+
} catch {
|
|
1830
|
+
return null
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Step 9: Render one frame
|
|
1835
|
+
public render() {
|
|
1836
|
+
if (this.multisampleTexture && this.camera && this.device && this.currentModel) {
|
|
1837
|
+
const currentTime = performance.now()
|
|
1838
|
+
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
1839
|
+
this.lastFrameTime = currentTime
|
|
1840
|
+
|
|
1841
|
+
this.updateCameraUniforms()
|
|
1842
|
+
this.updateRenderTarget()
|
|
1843
|
+
|
|
1844
|
+
this.updateModelPose(deltaTime)
|
|
1845
|
+
|
|
1846
|
+
const encoder = this.device.createCommandEncoder()
|
|
1847
|
+
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
1848
|
+
|
|
1849
|
+
pass.setVertexBuffer(0, this.vertexBuffer)
|
|
1850
|
+
pass.setVertexBuffer(1, this.jointsBuffer)
|
|
1851
|
+
pass.setVertexBuffer(2, this.weightsBuffer)
|
|
1852
|
+
pass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
1853
|
+
|
|
1854
|
+
this.drawCallCount = 0
|
|
1855
|
+
|
|
1856
|
+
// PASS 1: Opaque non-eye, non-hair (face, body, etc)
|
|
1857
|
+
// this.drawOutlines(pass, false) // Opaque outlines
|
|
1858
|
+
|
|
1859
|
+
pass.setPipeline(this.pipeline)
|
|
1860
|
+
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
1861
|
+
if (draw.count > 0) {
|
|
1862
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1863
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1864
|
+
this.drawCallCount++
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// PASS 2: Eyes (writes stencil = 1)
|
|
1869
|
+
pass.setPipeline(this.eyePipeline)
|
|
1870
|
+
pass.setStencilReference(1) // Set stencil reference value to 1
|
|
1871
|
+
for (const draw of this.eyeDraws) {
|
|
1872
|
+
if (draw.count > 0) {
|
|
1873
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1874
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1875
|
+
this.drawCallCount++
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// PASS 3: Hair rendering - optimized single pass approach
|
|
1880
|
+
// Since both hair passes use the same shader, we batch them together
|
|
1881
|
+
// but still need separate passes due to stencil requirements (equal vs not-equal)
|
|
1882
|
+
|
|
1883
|
+
this.drawOutlines(pass, false) // Opaque outlines
|
|
1884
|
+
|
|
1885
|
+
// 3a: Hair over eyes (stencil == 1, alphaMultiplier = 0.5)
|
|
1886
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
1887
|
+
pass.setPipeline(this.hairMultiplyPipeline)
|
|
1888
|
+
pass.setStencilReference(1)
|
|
1889
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1890
|
+
if (draw.count > 0) {
|
|
1891
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1892
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1893
|
+
this.drawCallCount++
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// 3b: Hair over non-eyes (stencil != 1, alphaMultiplier = 1.0)
|
|
1899
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1900
|
+
pass.setPipeline(this.hairOpaquePipeline)
|
|
1901
|
+
pass.setStencilReference(1)
|
|
1902
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1903
|
+
if (draw.count > 0) {
|
|
1904
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1905
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1906
|
+
this.drawCallCount++
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// 3c: Hair outlines - batched together, only draw if outlines exist
|
|
1912
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
1913
|
+
// Over eyes
|
|
1914
|
+
pass.setPipeline(this.hairOutlineOverEyesPipeline)
|
|
1915
|
+
pass.setStencilReference(1)
|
|
1916
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1917
|
+
if (draw.count > 0) {
|
|
1918
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1919
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Over non-eyes
|
|
1924
|
+
pass.setPipeline(this.hairOutlinePipeline)
|
|
1925
|
+
pass.setStencilReference(1)
|
|
1926
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1927
|
+
if (draw.count > 0) {
|
|
1928
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1929
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// PASS 4: Transparent non-eye, non-hair
|
|
1935
|
+
pass.setPipeline(this.pipeline)
|
|
1936
|
+
for (const draw of this.transparentNonEyeNonHairDraws) {
|
|
1937
|
+
if (draw.count > 0) {
|
|
1938
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1939
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1940
|
+
this.drawCallCount++
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
this.drawOutlines(pass, true) // Transparent outlines
|
|
1945
|
+
|
|
1946
|
+
pass.end()
|
|
1947
|
+
this.device.queue.submit([encoder.finish()])
|
|
1948
|
+
|
|
1949
|
+
// Apply bloom post-processing
|
|
1950
|
+
this.applyBloom()
|
|
1951
|
+
|
|
1952
|
+
this.updateStats(performance.now() - currentTime)
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Apply bloom post-processing
|
|
1957
|
+
private applyBloom() {
|
|
1958
|
+
if (!this.sceneRenderTexture || !this.bloomExtractTexture) {
|
|
1959
|
+
return
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Update bloom parameters
|
|
1963
|
+
const thresholdData = new Float32Array(8)
|
|
1964
|
+
thresholdData[0] = this.bloomThreshold
|
|
1965
|
+
this.device.queue.writeBuffer(this.bloomThresholdBuffer, 0, thresholdData)
|
|
1966
|
+
|
|
1967
|
+
const intensityData = new Float32Array(8)
|
|
1968
|
+
intensityData[0] = this.bloomIntensity
|
|
1969
|
+
this.device.queue.writeBuffer(this.bloomIntensityBuffer, 0, intensityData)
|
|
1970
|
+
|
|
1971
|
+
const encoder = this.device.createCommandEncoder()
|
|
1972
|
+
const width = this.canvas.width
|
|
1973
|
+
const height = this.canvas.height
|
|
1974
|
+
const bloomWidth = Math.floor(width / 2)
|
|
1975
|
+
const bloomHeight = Math.floor(height / 2)
|
|
1976
|
+
|
|
1977
|
+
// Pass 1: Extract bright areas (downsample to half resolution)
|
|
1978
|
+
const extractPass = encoder.beginRenderPass({
|
|
1979
|
+
label: "bloom extract",
|
|
1980
|
+
colorAttachments: [
|
|
1981
|
+
{
|
|
1982
|
+
view: this.bloomExtractTexture.createView(),
|
|
1983
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1984
|
+
loadOp: "clear",
|
|
1985
|
+
storeOp: "store",
|
|
1986
|
+
},
|
|
1987
|
+
],
|
|
1988
|
+
})
|
|
1989
|
+
|
|
1990
|
+
const extractBindGroup = this.device.createBindGroup({
|
|
1991
|
+
layout: this.bloomExtractPipeline.getBindGroupLayout(0),
|
|
1992
|
+
entries: [
|
|
1993
|
+
{ binding: 0, resource: this.sceneRenderTexture.createView() },
|
|
1994
|
+
{ binding: 1, resource: this.linearSampler },
|
|
1995
|
+
{ binding: 2, resource: { buffer: this.bloomThresholdBuffer } },
|
|
1996
|
+
],
|
|
1997
|
+
})
|
|
1998
|
+
|
|
1999
|
+
extractPass.setPipeline(this.bloomExtractPipeline)
|
|
2000
|
+
extractPass.setBindGroup(0, extractBindGroup)
|
|
2001
|
+
extractPass.draw(6, 1, 0, 0)
|
|
2002
|
+
extractPass.end()
|
|
2003
|
+
|
|
2004
|
+
// Pass 2: Horizontal blur
|
|
2005
|
+
const hBlurData = new Float32Array(4) // vec2f + padding = 4 floats
|
|
2006
|
+
hBlurData[0] = 1.0
|
|
2007
|
+
hBlurData[1] = 0.0
|
|
2008
|
+
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, hBlurData)
|
|
2009
|
+
const blurHPass = encoder.beginRenderPass({
|
|
2010
|
+
label: "bloom blur horizontal",
|
|
2011
|
+
colorAttachments: [
|
|
2012
|
+
{
|
|
2013
|
+
view: this.bloomBlurTexture1.createView(),
|
|
2014
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
2015
|
+
loadOp: "clear",
|
|
2016
|
+
storeOp: "store",
|
|
2017
|
+
},
|
|
2018
|
+
],
|
|
2019
|
+
})
|
|
2020
|
+
|
|
2021
|
+
const blurHBindGroup = this.device.createBindGroup({
|
|
2022
|
+
layout: this.bloomBlurPipeline.getBindGroupLayout(0),
|
|
2023
|
+
entries: [
|
|
2024
|
+
{ binding: 0, resource: this.bloomExtractTexture.createView() },
|
|
2025
|
+
{ binding: 1, resource: this.linearSampler },
|
|
2026
|
+
{ binding: 2, resource: { buffer: this.blurDirectionBuffer } },
|
|
2027
|
+
],
|
|
2028
|
+
})
|
|
2029
|
+
|
|
2030
|
+
blurHPass.setPipeline(this.bloomBlurPipeline)
|
|
2031
|
+
blurHPass.setBindGroup(0, blurHBindGroup)
|
|
2032
|
+
blurHPass.draw(6, 1, 0, 0)
|
|
2033
|
+
blurHPass.end()
|
|
2034
|
+
|
|
2035
|
+
// Pass 3: Vertical blur
|
|
2036
|
+
const vBlurData = new Float32Array(4) // vec2f + padding = 4 floats
|
|
2037
|
+
vBlurData[0] = 0.0
|
|
2038
|
+
vBlurData[1] = 1.0
|
|
2039
|
+
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, vBlurData)
|
|
2040
|
+
const blurVPass = encoder.beginRenderPass({
|
|
2041
|
+
label: "bloom blur vertical",
|
|
2042
|
+
colorAttachments: [
|
|
2043
|
+
{
|
|
2044
|
+
view: this.bloomBlurTexture2.createView(),
|
|
2045
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
2046
|
+
loadOp: "clear",
|
|
2047
|
+
storeOp: "store",
|
|
2048
|
+
},
|
|
2049
|
+
],
|
|
2050
|
+
})
|
|
2051
|
+
|
|
2052
|
+
const blurVBindGroup = this.device.createBindGroup({
|
|
2053
|
+
layout: this.bloomBlurPipeline.getBindGroupLayout(0),
|
|
2054
|
+
entries: [
|
|
2055
|
+
{ binding: 0, resource: this.bloomBlurTexture1.createView() },
|
|
2056
|
+
{ binding: 1, resource: this.linearSampler },
|
|
2057
|
+
{ binding: 2, resource: { buffer: this.blurDirectionBuffer } },
|
|
2058
|
+
],
|
|
2059
|
+
})
|
|
2060
|
+
|
|
2061
|
+
blurVPass.setPipeline(this.bloomBlurPipeline)
|
|
2062
|
+
blurVPass.setBindGroup(0, blurVBindGroup)
|
|
2063
|
+
blurVPass.draw(6, 1, 0, 0)
|
|
2064
|
+
blurVPass.end()
|
|
2065
|
+
|
|
2066
|
+
// Pass 4: Compose scene + bloom to canvas
|
|
2067
|
+
const composePass = encoder.beginRenderPass({
|
|
2068
|
+
label: "bloom compose",
|
|
2069
|
+
colorAttachments: [
|
|
2070
|
+
{
|
|
2071
|
+
view: this.context.getCurrentTexture().createView(),
|
|
2072
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
2073
|
+
loadOp: "clear",
|
|
2074
|
+
storeOp: "store",
|
|
2075
|
+
},
|
|
2076
|
+
],
|
|
2077
|
+
})
|
|
2078
|
+
|
|
2079
|
+
const composeBindGroup = this.device.createBindGroup({
|
|
2080
|
+
layout: this.bloomComposePipeline.getBindGroupLayout(0),
|
|
2081
|
+
entries: [
|
|
2082
|
+
{ binding: 0, resource: this.sceneRenderTexture.createView() },
|
|
2083
|
+
{ binding: 1, resource: this.linearSampler },
|
|
2084
|
+
{ binding: 2, resource: this.bloomBlurTexture2.createView() },
|
|
2085
|
+
{ binding: 3, resource: this.linearSampler },
|
|
2086
|
+
{ binding: 4, resource: { buffer: this.bloomIntensityBuffer } },
|
|
2087
|
+
],
|
|
2088
|
+
})
|
|
2089
|
+
|
|
2090
|
+
composePass.setPipeline(this.bloomComposePipeline)
|
|
2091
|
+
composePass.setBindGroup(0, composeBindGroup)
|
|
2092
|
+
composePass.draw(6, 1, 0, 0)
|
|
2093
|
+
composePass.end()
|
|
2094
|
+
|
|
2095
|
+
this.device.queue.submit([encoder.finish()])
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// Update camera uniform buffer each frame
|
|
2099
|
+
private updateCameraUniforms() {
|
|
2100
|
+
const viewMatrix = this.camera.getViewMatrix()
|
|
2101
|
+
const projectionMatrix = this.camera.getProjectionMatrix()
|
|
2102
|
+
const cameraPos = this.camera.getPosition()
|
|
2103
|
+
this.cameraMatrixData.set(viewMatrix.values, 0)
|
|
2104
|
+
this.cameraMatrixData.set(projectionMatrix.values, 16)
|
|
2105
|
+
this.cameraMatrixData[32] = cameraPos.x
|
|
2106
|
+
this.cameraMatrixData[33] = cameraPos.y
|
|
2107
|
+
this.cameraMatrixData[34] = cameraPos.z
|
|
2108
|
+
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Update render target texture view
|
|
2112
|
+
private updateRenderTarget() {
|
|
2113
|
+
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
2114
|
+
if (this.sampleCount > 1) {
|
|
2115
|
+
// Resolve to scene render texture for post-processing
|
|
2116
|
+
colorAttachment.resolveTarget = this.sceneRenderTextureView
|
|
2117
|
+
} else {
|
|
2118
|
+
// Render directly to scene render texture
|
|
2119
|
+
colorAttachment.view = this.sceneRenderTextureView
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// Update model pose and physics
|
|
2124
|
+
private updateModelPose(deltaTime: number) {
|
|
2125
|
+
// Step 1: Animation evaluation (computes matrices to CPU memory, no upload yet)
|
|
2126
|
+
this.currentModel!.evaluatePose()
|
|
2127
|
+
|
|
2128
|
+
// Step 2: Get world matrices (still in CPU memory)
|
|
2129
|
+
const worldMats = this.currentModel!.getBoneWorldMatrices()
|
|
2130
|
+
|
|
2131
|
+
// Step 3: Physics modifies matrices in-place
|
|
2132
|
+
if (this.physics) {
|
|
2133
|
+
this.physics.step(deltaTime, worldMats, this.currentModel!.getBoneInverseBindMatrices())
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// Step 4: Upload ONCE with final result (animation + physics)
|
|
2137
|
+
this.device.queue.writeBuffer(
|
|
2138
|
+
this.worldMatrixBuffer!,
|
|
2139
|
+
0,
|
|
2140
|
+
worldMats.buffer,
|
|
2141
|
+
worldMats.byteOffset,
|
|
2142
|
+
worldMats.byteLength
|
|
2143
|
+
)
|
|
2144
|
+
|
|
2145
|
+
// Step 5: GPU skinning
|
|
2146
|
+
this.computeSkinMatrices()
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// Compute skin matrices on GPU
|
|
2150
|
+
private computeSkinMatrices() {
|
|
2151
|
+
const boneCount = this.currentModel!.getSkeleton().bones.length
|
|
2152
|
+
const workgroupSize = 64
|
|
2153
|
+
// Dispatch exactly enough threads for all bones (no bounds check needed)
|
|
2154
|
+
const workgroupCount = Math.ceil(boneCount / workgroupSize)
|
|
2155
|
+
|
|
2156
|
+
// Update bone count uniform
|
|
2157
|
+
const boneCountData = new Uint32Array(8) // 32 bytes total
|
|
2158
|
+
boneCountData[0] = boneCount
|
|
2159
|
+
this.device.queue.writeBuffer(this.boneCountBuffer!, 0, boneCountData)
|
|
2160
|
+
|
|
2161
|
+
const bindGroup = this.device.createBindGroup({
|
|
2162
|
+
label: "skin matrix compute bind group",
|
|
2163
|
+
layout: this.skinMatrixComputePipeline!.getBindGroupLayout(0),
|
|
2164
|
+
entries: [
|
|
2165
|
+
{ binding: 0, resource: { buffer: this.boneCountBuffer! } },
|
|
2166
|
+
{ binding: 1, resource: { buffer: this.worldMatrixBuffer! } },
|
|
2167
|
+
{ binding: 2, resource: { buffer: this.inverseBindMatrixBuffer! } },
|
|
2168
|
+
{ binding: 3, resource: { buffer: this.skinMatrixBuffer! } },
|
|
2169
|
+
],
|
|
2170
|
+
})
|
|
2171
|
+
|
|
2172
|
+
const encoder = this.device.createCommandEncoder()
|
|
2173
|
+
const pass = encoder.beginComputePass()
|
|
2174
|
+
pass.setPipeline(this.skinMatrixComputePipeline!)
|
|
2175
|
+
pass.setBindGroup(0, bindGroup)
|
|
2176
|
+
pass.dispatchWorkgroups(workgroupCount)
|
|
2177
|
+
pass.end()
|
|
2178
|
+
this.device.queue.submit([encoder.finish()])
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Draw outlines (opaque or transparent)
|
|
2182
|
+
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
2183
|
+
pass.setPipeline(this.outlinePipeline)
|
|
2184
|
+
if (transparent) {
|
|
2185
|
+
// Draw transparent outlines (if any)
|
|
2186
|
+
for (const draw of this.transparentNonEyeNonHairOutlineDraws) {
|
|
2187
|
+
if (draw.count > 0) {
|
|
2188
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2189
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
} else {
|
|
2193
|
+
// Draw opaque outlines before main geometry
|
|
2194
|
+
for (const draw of this.opaqueNonEyeNonHairOutlineDraws) {
|
|
2195
|
+
if (draw.count > 0) {
|
|
2196
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2197
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
private updateStats(frameTime: number) {
|
|
2204
|
+
const maxSamples = 60
|
|
2205
|
+
this.frameTimeSamples.push(frameTime)
|
|
2206
|
+
this.frameTimeSum += frameTime
|
|
2207
|
+
if (this.frameTimeSamples.length > maxSamples) {
|
|
2208
|
+
const removed = this.frameTimeSamples.shift()!
|
|
2209
|
+
this.frameTimeSum -= removed
|
|
2210
|
+
}
|
|
2211
|
+
const avgFrameTime = this.frameTimeSum / this.frameTimeSamples.length
|
|
2212
|
+
this.stats.frameTime = Math.round(avgFrameTime * 100) / 100
|
|
2213
|
+
|
|
2214
|
+
const now = performance.now()
|
|
2215
|
+
this.framesSinceLastUpdate++
|
|
2216
|
+
const elapsed = now - this.lastFpsUpdate
|
|
2217
|
+
|
|
2218
|
+
if (elapsed >= 1000) {
|
|
2219
|
+
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * 1000)
|
|
2220
|
+
this.framesSinceLastUpdate = 0
|
|
2221
|
+
this.lastFpsUpdate = now
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// Calculate GPU memory: textures + buffers + render targets
|
|
2225
|
+
let textureMemoryBytes = 0
|
|
2226
|
+
for (const [path, size] of this.textureSizes.entries()) {
|
|
2227
|
+
if (this.textureCache.has(path)) {
|
|
2228
|
+
textureMemoryBytes += size.width * size.height * 4 // RGBA8 = 4 bytes per pixel
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
let bufferMemoryBytes = 0
|
|
2233
|
+
if (this.vertexBuffer) {
|
|
2234
|
+
const vertices = this.currentModel?.getVertices()
|
|
2235
|
+
if (vertices) bufferMemoryBytes += vertices.byteLength
|
|
2236
|
+
}
|
|
2237
|
+
if (this.indexBuffer) {
|
|
2238
|
+
const indices = this.currentModel?.getIndices()
|
|
2239
|
+
if (indices) bufferMemoryBytes += indices.byteLength
|
|
2240
|
+
}
|
|
2241
|
+
if (this.jointsBuffer) {
|
|
2242
|
+
const skinning = this.currentModel?.getSkinning()
|
|
2243
|
+
if (skinning) bufferMemoryBytes += skinning.joints.byteLength
|
|
2244
|
+
}
|
|
2245
|
+
if (this.weightsBuffer) {
|
|
2246
|
+
const skinning = this.currentModel?.getSkinning()
|
|
2247
|
+
if (skinning) bufferMemoryBytes += skinning.weights.byteLength
|
|
2248
|
+
}
|
|
2249
|
+
if (this.skinMatrixBuffer) {
|
|
2250
|
+
const skeleton = this.currentModel?.getSkeleton()
|
|
2251
|
+
if (skeleton) bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4)
|
|
2252
|
+
}
|
|
2253
|
+
if (this.worldMatrixBuffer) {
|
|
2254
|
+
const skeleton = this.currentModel?.getSkeleton()
|
|
2255
|
+
if (skeleton) bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4)
|
|
2256
|
+
}
|
|
2257
|
+
if (this.inverseBindMatrixBuffer) {
|
|
2258
|
+
const skeleton = this.currentModel?.getSkeleton()
|
|
2259
|
+
if (skeleton) bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4)
|
|
2260
|
+
}
|
|
2261
|
+
bufferMemoryBytes += 40 * 4 // cameraUniformBuffer
|
|
2262
|
+
bufferMemoryBytes += 64 * 4 // lightUniformBuffer
|
|
2263
|
+
bufferMemoryBytes += 32 // boneCountBuffer
|
|
2264
|
+
bufferMemoryBytes += 32 // blurDirectionBuffer
|
|
2265
|
+
bufferMemoryBytes += 32 // bloomIntensityBuffer
|
|
2266
|
+
bufferMemoryBytes += 32 // bloomThresholdBuffer
|
|
2267
|
+
if (this.fullscreenQuadBuffer) {
|
|
2268
|
+
bufferMemoryBytes += 24 * 4 // fullscreenQuadBuffer (6 vertices * 4 floats)
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// Material uniform buffers: Float32Array(8) = 32 bytes each
|
|
2272
|
+
const totalMaterialDraws =
|
|
2273
|
+
this.opaqueNonEyeNonHairDraws.length +
|
|
2274
|
+
this.eyeDraws.length +
|
|
2275
|
+
this.hairDrawsOverEyes.length +
|
|
2276
|
+
this.hairDrawsOverNonEyes.length +
|
|
2277
|
+
this.transparentNonEyeNonHairDraws.length
|
|
2278
|
+
bufferMemoryBytes += totalMaterialDraws * 32 // Material uniform buffers (8 floats = 32 bytes)
|
|
2279
|
+
|
|
2280
|
+
// Outline material uniform buffers: Float32Array(8) = 32 bytes each
|
|
2281
|
+
const totalOutlineDraws =
|
|
2282
|
+
this.opaqueNonEyeNonHairOutlineDraws.length +
|
|
2283
|
+
this.eyeOutlineDraws.length +
|
|
2284
|
+
this.hairOutlineDraws.length +
|
|
2285
|
+
this.transparentNonEyeNonHairOutlineDraws.length
|
|
2286
|
+
bufferMemoryBytes += totalOutlineDraws * 32 // Outline material uniform buffers
|
|
2287
|
+
|
|
2288
|
+
let renderTargetMemoryBytes = 0
|
|
2289
|
+
if (this.multisampleTexture) {
|
|
2290
|
+
const width = this.canvas.width
|
|
2291
|
+
const height = this.canvas.height
|
|
2292
|
+
renderTargetMemoryBytes += width * height * 4 * this.sampleCount // multisample color
|
|
2293
|
+
renderTargetMemoryBytes += width * height * 4 // depth (depth24plus-stencil8 = 4 bytes)
|
|
2294
|
+
}
|
|
2295
|
+
if (this.sceneRenderTexture) {
|
|
2296
|
+
const width = this.canvas.width
|
|
2297
|
+
const height = this.canvas.height
|
|
2298
|
+
renderTargetMemoryBytes += width * height * 4 // sceneRenderTexture (non-multisampled)
|
|
2299
|
+
}
|
|
2300
|
+
if (this.bloomExtractTexture) {
|
|
2301
|
+
const width = Math.floor(this.canvas.width / 2)
|
|
2302
|
+
const height = Math.floor(this.canvas.height / 2)
|
|
2303
|
+
renderTargetMemoryBytes += width * height * 4 // bloomExtractTexture
|
|
2304
|
+
renderTargetMemoryBytes += width * height * 4 // bloomBlurTexture1
|
|
2305
|
+
renderTargetMemoryBytes += width * height * 4 // bloomBlurTexture2
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const totalGPUMemoryBytes = textureMemoryBytes + bufferMemoryBytes + renderTargetMemoryBytes
|
|
2309
|
+
this.stats.gpuMemory = Math.round((totalGPUMemoryBytes / 1024 / 1024) * 100) / 100
|
|
2310
|
+
}
|
|
2311
|
+
}
|