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