reze-engine 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -99
- package/dist/engine.d.ts +1 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +239 -252
- package/dist/math.d.ts +2 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +33 -0
- package/dist/model.d.ts +5 -25
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +49 -195
- package/package.json +2 -2
- package/src/camera.ts +358 -358
- package/src/engine.ts +1136 -1158
- package/src/math.ts +546 -505
- package/src/model.ts +418 -586
- package/src/physics.ts +680 -680
- package/src/pmx-loader.ts +1031 -1031
package/src/engine.ts
CHANGED
|
@@ -1,1158 +1,1136 @@
|
|
|
1
|
-
import { Camera } from "./camera"
|
|
2
|
-
import { Quat, Vec3 } from "./math"
|
|
3
|
-
import { Model } from "./model"
|
|
4
|
-
import { PmxLoader } from "./pmx-loader"
|
|
5
|
-
import { Physics } from "./physics"
|
|
6
|
-
|
|
7
|
-
export interface EngineStats {
|
|
8
|
-
fps: number
|
|
9
|
-
frameTime: number // ms
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
31
|
-
private
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private
|
|
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
|
-
|
|
48
|
-
private
|
|
49
|
-
private
|
|
50
|
-
private
|
|
51
|
-
private
|
|
52
|
-
private
|
|
53
|
-
private
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
@
|
|
154
|
-
@location(
|
|
155
|
-
@location(
|
|
156
|
-
@location(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
count
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
this.
|
|
546
|
-
|
|
547
|
-
this.
|
|
548
|
-
this.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
this.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
this.
|
|
560
|
-
|
|
561
|
-
this.
|
|
562
|
-
this.
|
|
563
|
-
this.
|
|
564
|
-
this.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
this.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
this.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
public
|
|
596
|
-
this.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
this.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Step
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
this.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
this.device.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
this.
|
|
673
|
-
label: "
|
|
674
|
-
size:
|
|
675
|
-
usage: GPUBufferUsage.
|
|
676
|
-
})
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
this.
|
|
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
|
-
if (
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
defaultToonData
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
if (
|
|
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
|
-
this.
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
pass.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
this.
|
|
947
|
-
|
|
948
|
-
this.
|
|
949
|
-
|
|
950
|
-
this.
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
this.
|
|
954
|
-
this.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
this.
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
this.cameraMatrixData
|
|
968
|
-
this.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
this.
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
pass.
|
|
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
|
-
this.
|
|
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
|
-
if (this.jointsBuffer) {
|
|
1138
|
-
const skinning = this.currentModel?.getSkinning()
|
|
1139
|
-
if (skinning) bufferMemoryBytes += skinning.joints.byteLength
|
|
1140
|
-
}
|
|
1141
|
-
if (this.weightsBuffer) {
|
|
1142
|
-
const skinning = this.currentModel?.getSkinning()
|
|
1143
|
-
if (skinning) bufferMemoryBytes += skinning.weights.byteLength
|
|
1144
|
-
}
|
|
1145
|
-
if (this.skinMatrixBuffer) {
|
|
1146
|
-
const skeleton = this.currentModel?.getSkeleton()
|
|
1147
|
-
if (skeleton) bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4)
|
|
1148
|
-
}
|
|
1149
|
-
bufferMemoryBytes += 40 * 4 // cameraUniformBuffer
|
|
1150
|
-
bufferMemoryBytes += 64 * 4 // lightUniformBuffer
|
|
1151
|
-
// Material uniform buffers (estimate: 4 bytes per material)
|
|
1152
|
-
bufferMemoryBytes += this.materialDraws.length * 4
|
|
1153
|
-
this.stats.bufferMemory = Math.round((bufferMemoryBytes / 1024 / 1024) * 100) / 100
|
|
1154
|
-
|
|
1155
|
-
// Total GPU memory estimate
|
|
1156
|
-
this.stats.gpuMemory = Math.round((this.stats.textureMemory + this.stats.bufferMemory) * 100) / 100
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1
|
+
import { Camera } from "./camera"
|
|
2
|
+
import { Quat, Vec3 } from "./math"
|
|
3
|
+
import { Model } from "./model"
|
|
4
|
+
import { PmxLoader } from "./pmx-loader"
|
|
5
|
+
import { Physics } from "./physics"
|
|
6
|
+
|
|
7
|
+
export interface EngineStats {
|
|
8
|
+
fps: number
|
|
9
|
+
frameTime: number // ms
|
|
10
|
+
gpuMemory: number // MB (estimated total GPU memory)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Engine {
|
|
14
|
+
private canvas: HTMLCanvasElement
|
|
15
|
+
private device!: GPUDevice
|
|
16
|
+
private context!: GPUCanvasContext
|
|
17
|
+
private presentationFormat!: GPUTextureFormat
|
|
18
|
+
public camera!: Camera
|
|
19
|
+
private cameraUniformBuffer!: GPUBuffer
|
|
20
|
+
private cameraMatrixData = new Float32Array(36)
|
|
21
|
+
private lightUniformBuffer!: GPUBuffer
|
|
22
|
+
private lightData = new Float32Array(64)
|
|
23
|
+
private lightCount = 0
|
|
24
|
+
private vertexBuffer!: GPUBuffer
|
|
25
|
+
private vertexCount: number = 0
|
|
26
|
+
private indexBuffer?: GPUBuffer
|
|
27
|
+
private resizeObserver: ResizeObserver | null = null
|
|
28
|
+
private depthTexture!: GPUTexture
|
|
29
|
+
private pipeline!: GPURenderPipeline
|
|
30
|
+
private outlinePipeline!: GPURenderPipeline
|
|
31
|
+
private jointsBuffer!: GPUBuffer
|
|
32
|
+
private weightsBuffer!: GPUBuffer
|
|
33
|
+
private skinMatrixBuffer?: GPUBuffer
|
|
34
|
+
private worldMatrixBuffer?: GPUBuffer
|
|
35
|
+
private inverseBindMatrixBuffer?: GPUBuffer
|
|
36
|
+
private skinMatrixComputePipeline?: GPUComputePipeline
|
|
37
|
+
private boneCountBuffer?: GPUBuffer
|
|
38
|
+
private multisampleTexture!: GPUTexture
|
|
39
|
+
private readonly sampleCount = 4 // MSAA 4x
|
|
40
|
+
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
41
|
+
private currentModel: Model | null = null
|
|
42
|
+
private modelDir: string = ""
|
|
43
|
+
private physics: Physics | null = null
|
|
44
|
+
private textureSampler!: GPUSampler
|
|
45
|
+
private textureCache = new Map<string, GPUTexture>()
|
|
46
|
+
private textureSizes = new Map<string, { width: number; height: number }>()
|
|
47
|
+
|
|
48
|
+
private lastFpsUpdate = performance.now()
|
|
49
|
+
private framesSinceLastUpdate = 0
|
|
50
|
+
private frameTimeSamples: number[] = []
|
|
51
|
+
private frameTimeSum: number = 0
|
|
52
|
+
private drawCallCount: number = 0
|
|
53
|
+
private lastFrameTime = performance.now()
|
|
54
|
+
private stats: EngineStats = {
|
|
55
|
+
fps: 0,
|
|
56
|
+
frameTime: 0,
|
|
57
|
+
gpuMemory: 0,
|
|
58
|
+
}
|
|
59
|
+
private animationFrameId: number | null = null
|
|
60
|
+
private renderLoopCallback: (() => void) | null = null
|
|
61
|
+
|
|
62
|
+
constructor(canvas: HTMLCanvasElement) {
|
|
63
|
+
this.canvas = canvas
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Step 1: Get WebGPU device and context
|
|
67
|
+
public async init() {
|
|
68
|
+
const adapter = await navigator.gpu?.requestAdapter()
|
|
69
|
+
const device = await adapter?.requestDevice()
|
|
70
|
+
if (!device) {
|
|
71
|
+
throw new Error("WebGPU is not supported in this browser.")
|
|
72
|
+
}
|
|
73
|
+
this.device = device
|
|
74
|
+
|
|
75
|
+
const context = this.canvas.getContext("webgpu")
|
|
76
|
+
if (!context) {
|
|
77
|
+
throw new Error("Failed to get WebGPU context.")
|
|
78
|
+
}
|
|
79
|
+
this.context = context
|
|
80
|
+
|
|
81
|
+
this.presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
|
82
|
+
|
|
83
|
+
this.context.configure({
|
|
84
|
+
device: this.device,
|
|
85
|
+
format: this.presentationFormat,
|
|
86
|
+
alphaMode: "premultiplied",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
this.setupCamera()
|
|
90
|
+
this.setupLighting()
|
|
91
|
+
this.createPipelines()
|
|
92
|
+
this.setupResize()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 2: Create shaders and render pipelines
|
|
96
|
+
private createPipelines() {
|
|
97
|
+
this.textureSampler = this.device.createSampler({
|
|
98
|
+
magFilter: "linear",
|
|
99
|
+
minFilter: "linear",
|
|
100
|
+
addressModeU: "repeat",
|
|
101
|
+
addressModeV: "repeat",
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const shaderModule = this.device.createShaderModule({
|
|
105
|
+
label: "model shaders",
|
|
106
|
+
code: /* wgsl */ `
|
|
107
|
+
struct CameraUniforms {
|
|
108
|
+
view: mat4x4f,
|
|
109
|
+
projection: mat4x4f,
|
|
110
|
+
viewPos: vec3f,
|
|
111
|
+
_padding: f32,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
struct Light {
|
|
115
|
+
direction: vec3f,
|
|
116
|
+
_padding1: f32,
|
|
117
|
+
color: vec3f,
|
|
118
|
+
intensity: f32,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
struct LightUniforms {
|
|
122
|
+
ambient: f32,
|
|
123
|
+
lightCount: f32,
|
|
124
|
+
_padding1: f32,
|
|
125
|
+
_padding2: f32,
|
|
126
|
+
lights: array<Light, 4>,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
struct MaterialUniforms {
|
|
130
|
+
alpha: f32,
|
|
131
|
+
_padding1: f32,
|
|
132
|
+
_padding2: f32,
|
|
133
|
+
_padding3: f32,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
struct VertexOutput {
|
|
137
|
+
@builtin(position) position: vec4f,
|
|
138
|
+
@location(0) normal: vec3f,
|
|
139
|
+
@location(1) uv: vec2f,
|
|
140
|
+
@location(2) worldPos: vec3f,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
144
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
145
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
146
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
147
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
148
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
149
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
150
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
151
|
+
|
|
152
|
+
@vertex fn vs(
|
|
153
|
+
@location(0) position: vec3f,
|
|
154
|
+
@location(1) normal: vec3f,
|
|
155
|
+
@location(2) uv: vec2f,
|
|
156
|
+
@location(3) joints0: vec4<u32>,
|
|
157
|
+
@location(4) weights0: vec4<f32>
|
|
158
|
+
) -> VertexOutput {
|
|
159
|
+
var output: VertexOutput;
|
|
160
|
+
let pos4 = vec4f(position, 1.0);
|
|
161
|
+
|
|
162
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
163
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
164
|
+
var normalizedWeights: vec4f;
|
|
165
|
+
if (weightSum > 0.0001) {
|
|
166
|
+
normalizedWeights = weights0 / weightSum;
|
|
167
|
+
} else {
|
|
168
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
172
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
173
|
+
for (var i = 0u; i < 4u; i++) {
|
|
174
|
+
let j = joints0[i];
|
|
175
|
+
let w = normalizedWeights[i];
|
|
176
|
+
let m = skinMats[j];
|
|
177
|
+
skinnedPos += (m * pos4) * w;
|
|
178
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
179
|
+
skinnedNrm += (r3 * normal) * w;
|
|
180
|
+
}
|
|
181
|
+
let worldPos = skinnedPos.xyz;
|
|
182
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
183
|
+
output.normal = normalize(skinnedNrm);
|
|
184
|
+
output.uv = uv;
|
|
185
|
+
output.worldPos = worldPos;
|
|
186
|
+
return output;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
190
|
+
let n = normalize(input.normal);
|
|
191
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
192
|
+
|
|
193
|
+
var lightAccum = vec3f(light.ambient);
|
|
194
|
+
let numLights = u32(light.lightCount);
|
|
195
|
+
for (var i = 0u; i < numLights; i++) {
|
|
196
|
+
let l = -light.lights[i].direction;
|
|
197
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
198
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
199
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
200
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
201
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let color = albedo * lightAccum;
|
|
205
|
+
let finalAlpha = material.alpha;
|
|
206
|
+
if (finalAlpha < 0.001) {
|
|
207
|
+
discard;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
211
|
+
}
|
|
212
|
+
`,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Single pipeline for all materials with alpha blending
|
|
216
|
+
this.pipeline = this.device.createRenderPipeline({
|
|
217
|
+
label: "model pipeline",
|
|
218
|
+
layout: "auto",
|
|
219
|
+
vertex: {
|
|
220
|
+
module: shaderModule,
|
|
221
|
+
buffers: [
|
|
222
|
+
{
|
|
223
|
+
arrayStride: 8 * 4,
|
|
224
|
+
attributes: [
|
|
225
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
226
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
227
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
arrayStride: 4 * 2,
|
|
232
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
arrayStride: 4,
|
|
236
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
fragment: {
|
|
241
|
+
module: shaderModule,
|
|
242
|
+
targets: [
|
|
243
|
+
{
|
|
244
|
+
format: this.presentationFormat,
|
|
245
|
+
blend: {
|
|
246
|
+
color: {
|
|
247
|
+
srcFactor: "src-alpha",
|
|
248
|
+
dstFactor: "one-minus-src-alpha",
|
|
249
|
+
operation: "add",
|
|
250
|
+
},
|
|
251
|
+
alpha: {
|
|
252
|
+
srcFactor: "one",
|
|
253
|
+
dstFactor: "one-minus-src-alpha",
|
|
254
|
+
operation: "add",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
primitive: { cullMode: "none" },
|
|
261
|
+
depthStencil: {
|
|
262
|
+
format: "depth24plus",
|
|
263
|
+
depthWriteEnabled: true,
|
|
264
|
+
depthCompare: "less",
|
|
265
|
+
},
|
|
266
|
+
multisample: {
|
|
267
|
+
count: this.sampleCount,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
const outlineShaderModule = this.device.createShaderModule({
|
|
272
|
+
label: "outline shaders",
|
|
273
|
+
code: /* wgsl */ `
|
|
274
|
+
struct CameraUniforms {
|
|
275
|
+
view: mat4x4f,
|
|
276
|
+
projection: mat4x4f,
|
|
277
|
+
viewPos: vec3f,
|
|
278
|
+
_padding: f32,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
struct MaterialUniforms {
|
|
282
|
+
edgeColor: vec4f,
|
|
283
|
+
edgeSize: f32,
|
|
284
|
+
_padding1: f32,
|
|
285
|
+
_padding2: f32,
|
|
286
|
+
_padding3: f32,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
290
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
291
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
292
|
+
|
|
293
|
+
struct VertexOutput {
|
|
294
|
+
@builtin(position) position: vec4f,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
@vertex fn vs(
|
|
298
|
+
@location(0) position: vec3f,
|
|
299
|
+
@location(1) normal: vec3f,
|
|
300
|
+
@location(2) uv: vec2f,
|
|
301
|
+
@location(3) joints0: vec4<u32>,
|
|
302
|
+
@location(4) weights0: vec4<f32>
|
|
303
|
+
) -> VertexOutput {
|
|
304
|
+
var output: VertexOutput;
|
|
305
|
+
let pos4 = vec4f(position, 1.0);
|
|
306
|
+
|
|
307
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
308
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
309
|
+
var normalizedWeights: vec4f;
|
|
310
|
+
if (weightSum > 0.0001) {
|
|
311
|
+
normalizedWeights = weights0 / weightSum;
|
|
312
|
+
} else {
|
|
313
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
317
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
318
|
+
for (var i = 0u; i < 4u; i++) {
|
|
319
|
+
let j = joints0[i];
|
|
320
|
+
let w = normalizedWeights[i];
|
|
321
|
+
let m = skinMats[j];
|
|
322
|
+
skinnedPos += (m * pos4) * w;
|
|
323
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
324
|
+
skinnedNrm += (r3 * normal) * w;
|
|
325
|
+
}
|
|
326
|
+
let worldPos = skinnedPos.xyz;
|
|
327
|
+
let worldNormal = normalize(skinnedNrm);
|
|
328
|
+
|
|
329
|
+
// MMD invert hull: expand vertices outward along normals
|
|
330
|
+
let scaleFactor = 0.01;
|
|
331
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
332
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
333
|
+
return output;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
337
|
+
return material.edgeColor;
|
|
338
|
+
}
|
|
339
|
+
`,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
this.outlinePipeline = this.device.createRenderPipeline({
|
|
343
|
+
label: "outline pipeline",
|
|
344
|
+
layout: "auto",
|
|
345
|
+
vertex: {
|
|
346
|
+
module: outlineShaderModule,
|
|
347
|
+
buffers: [
|
|
348
|
+
{
|
|
349
|
+
arrayStride: 8 * 4,
|
|
350
|
+
attributes: [
|
|
351
|
+
{
|
|
352
|
+
shaderLocation: 0,
|
|
353
|
+
offset: 0,
|
|
354
|
+
format: "float32x3" as GPUVertexFormat,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
shaderLocation: 1,
|
|
358
|
+
offset: 3 * 4,
|
|
359
|
+
format: "float32x3" as GPUVertexFormat,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
shaderLocation: 2,
|
|
363
|
+
offset: 6 * 4,
|
|
364
|
+
format: "float32x2" as GPUVertexFormat,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
arrayStride: 4 * 2,
|
|
370
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
arrayStride: 4,
|
|
374
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
fragment: {
|
|
379
|
+
module: outlineShaderModule,
|
|
380
|
+
targets: [
|
|
381
|
+
{
|
|
382
|
+
format: this.presentationFormat,
|
|
383
|
+
blend: {
|
|
384
|
+
color: {
|
|
385
|
+
srcFactor: "src-alpha",
|
|
386
|
+
dstFactor: "one-minus-src-alpha",
|
|
387
|
+
operation: "add",
|
|
388
|
+
},
|
|
389
|
+
alpha: {
|
|
390
|
+
srcFactor: "one",
|
|
391
|
+
dstFactor: "one-minus-src-alpha",
|
|
392
|
+
operation: "add",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
primitive: {
|
|
399
|
+
cullMode: "back",
|
|
400
|
+
},
|
|
401
|
+
depthStencil: {
|
|
402
|
+
format: "depth24plus",
|
|
403
|
+
depthWriteEnabled: true,
|
|
404
|
+
depthCompare: "less",
|
|
405
|
+
},
|
|
406
|
+
multisample: {
|
|
407
|
+
count: this.sampleCount,
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Create compute shader for skin matrix computation
|
|
413
|
+
private createSkinMatrixComputePipeline() {
|
|
414
|
+
const computeShader = this.device.createShaderModule({
|
|
415
|
+
label: "skin matrix compute",
|
|
416
|
+
code: /* wgsl */ `
|
|
417
|
+
struct BoneCountUniform {
|
|
418
|
+
count: u32,
|
|
419
|
+
_padding1: u32,
|
|
420
|
+
_padding2: u32,
|
|
421
|
+
_padding3: u32,
|
|
422
|
+
_padding4: vec4<u32>,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
426
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
427
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
428
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
429
|
+
|
|
430
|
+
@compute @workgroup_size(64)
|
|
431
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
432
|
+
let boneIndex = globalId.x;
|
|
433
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
434
|
+
if (boneIndex >= boneCount.count) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
let worldMat = worldMatrices[boneIndex];
|
|
438
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
439
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
440
|
+
}
|
|
441
|
+
`,
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
445
|
+
label: "skin matrix compute pipeline",
|
|
446
|
+
layout: "auto",
|
|
447
|
+
compute: {
|
|
448
|
+
module: computeShader,
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Step 3: Setup canvas resize handling
|
|
454
|
+
private setupResize() {
|
|
455
|
+
this.resizeObserver = new ResizeObserver(() => this.handleResize())
|
|
456
|
+
this.resizeObserver.observe(this.canvas)
|
|
457
|
+
this.handleResize()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private handleResize() {
|
|
461
|
+
const displayWidth = this.canvas.clientWidth
|
|
462
|
+
const displayHeight = this.canvas.clientHeight
|
|
463
|
+
|
|
464
|
+
const dpr = window.devicePixelRatio || 1
|
|
465
|
+
const width = Math.floor(displayWidth * dpr)
|
|
466
|
+
const height = Math.floor(displayHeight * dpr)
|
|
467
|
+
|
|
468
|
+
if (!this.multisampleTexture || this.canvas.width !== width || this.canvas.height !== height) {
|
|
469
|
+
this.canvas.width = width
|
|
470
|
+
this.canvas.height = height
|
|
471
|
+
|
|
472
|
+
this.multisampleTexture = this.device.createTexture({
|
|
473
|
+
label: "multisample render target",
|
|
474
|
+
size: [width, height],
|
|
475
|
+
sampleCount: this.sampleCount,
|
|
476
|
+
format: this.presentationFormat,
|
|
477
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
this.depthTexture = this.device.createTexture({
|
|
481
|
+
label: "depth texture",
|
|
482
|
+
size: [width, height],
|
|
483
|
+
sampleCount: this.sampleCount,
|
|
484
|
+
format: "depth24plus",
|
|
485
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
const depthTextureView = this.depthTexture.createView()
|
|
489
|
+
|
|
490
|
+
const colorAttachment: GPURenderPassColorAttachment =
|
|
491
|
+
this.sampleCount > 1
|
|
492
|
+
? {
|
|
493
|
+
view: this.multisampleTexture.createView(),
|
|
494
|
+
resolveTarget: this.context.getCurrentTexture().createView(),
|
|
495
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
496
|
+
loadOp: "clear",
|
|
497
|
+
storeOp: "store",
|
|
498
|
+
}
|
|
499
|
+
: {
|
|
500
|
+
view: this.context.getCurrentTexture().createView(),
|
|
501
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
502
|
+
loadOp: "clear",
|
|
503
|
+
storeOp: "store",
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this.renderPassDescriptor = {
|
|
507
|
+
label: "renderPass",
|
|
508
|
+
colorAttachments: [colorAttachment],
|
|
509
|
+
depthStencilAttachment: {
|
|
510
|
+
view: depthTextureView,
|
|
511
|
+
depthClearValue: 1.0,
|
|
512
|
+
depthLoadOp: "clear",
|
|
513
|
+
depthStoreOp: "store",
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.camera.aspect = width / height
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Step 4: Create camera and uniform buffer
|
|
522
|
+
private setupCamera() {
|
|
523
|
+
this.cameraUniformBuffer = this.device.createBuffer({
|
|
524
|
+
label: "camera uniforms",
|
|
525
|
+
size: 40 * 4,
|
|
526
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
this.camera = new Camera(Math.PI, Math.PI / 2.5, 26.6, new Vec3(0, 12.5, 0))
|
|
530
|
+
|
|
531
|
+
this.camera.aspect = this.canvas.width / this.canvas.height
|
|
532
|
+
this.camera.attachControl(this.canvas)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Step 5: Create lighting buffers
|
|
536
|
+
private setupLighting() {
|
|
537
|
+
this.lightUniformBuffer = this.device.createBuffer({
|
|
538
|
+
label: "light uniforms",
|
|
539
|
+
size: 64 * 4,
|
|
540
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
this.lightCount = 0
|
|
544
|
+
|
|
545
|
+
this.setAmbient(0.96)
|
|
546
|
+
this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.12)
|
|
547
|
+
this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.1)
|
|
548
|
+
this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.08)
|
|
549
|
+
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
public addLight(direction: Vec3, color: Vec3, intensity: number = 1.0): boolean {
|
|
553
|
+
if (this.lightCount >= 4) return false
|
|
554
|
+
|
|
555
|
+
const normalized = direction.normalize()
|
|
556
|
+
const baseIndex = 4 + this.lightCount * 8
|
|
557
|
+
this.lightData[baseIndex] = normalized.x
|
|
558
|
+
this.lightData[baseIndex + 1] = normalized.y
|
|
559
|
+
this.lightData[baseIndex + 2] = normalized.z
|
|
560
|
+
this.lightData[baseIndex + 3] = 0
|
|
561
|
+
this.lightData[baseIndex + 4] = color.x
|
|
562
|
+
this.lightData[baseIndex + 5] = color.y
|
|
563
|
+
this.lightData[baseIndex + 6] = color.z
|
|
564
|
+
this.lightData[baseIndex + 7] = intensity
|
|
565
|
+
|
|
566
|
+
this.lightCount++
|
|
567
|
+
this.lightData[1] = this.lightCount
|
|
568
|
+
return true
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
public setAmbient(intensity: number) {
|
|
572
|
+
this.lightData[0] = intensity
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
public getStats(): EngineStats {
|
|
576
|
+
return { ...this.stats }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
public runRenderLoop(callback?: () => void) {
|
|
580
|
+
this.renderLoopCallback = callback || null
|
|
581
|
+
|
|
582
|
+
const loop = () => {
|
|
583
|
+
this.render()
|
|
584
|
+
|
|
585
|
+
if (this.renderLoopCallback) {
|
|
586
|
+
this.renderLoopCallback()
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
this.animationFrameId = requestAnimationFrame(loop)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
this.animationFrameId = requestAnimationFrame(loop)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public stopRenderLoop() {
|
|
596
|
+
if (this.animationFrameId !== null) {
|
|
597
|
+
cancelAnimationFrame(this.animationFrameId)
|
|
598
|
+
this.animationFrameId = null
|
|
599
|
+
}
|
|
600
|
+
this.renderLoopCallback = null
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
public dispose() {
|
|
604
|
+
this.stopRenderLoop()
|
|
605
|
+
if (this.camera) this.camera.detachControl()
|
|
606
|
+
if (this.resizeObserver) {
|
|
607
|
+
this.resizeObserver.disconnect()
|
|
608
|
+
this.resizeObserver = null
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Step 6: Load PMX model file
|
|
613
|
+
public async loadModel(path: string) {
|
|
614
|
+
const pathParts = path.split("/")
|
|
615
|
+
pathParts.pop()
|
|
616
|
+
const dir = pathParts.join("/") + "/"
|
|
617
|
+
this.modelDir = dir
|
|
618
|
+
|
|
619
|
+
const model = await PmxLoader.load(path)
|
|
620
|
+
this.physics = new Physics(model.getRigidbodies(), model.getJoints())
|
|
621
|
+
await this.setupModelBuffers(model)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
public rotateBones(bones: string[], rotations: Quat[], durationMs?: number) {
|
|
625
|
+
this.currentModel?.rotateBones(bones, rotations, durationMs)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Step 7: Create vertex, index, and joint buffers
|
|
629
|
+
private async setupModelBuffers(model: Model) {
|
|
630
|
+
this.currentModel = model
|
|
631
|
+
const vertices = model.getVertices()
|
|
632
|
+
const skinning = model.getSkinning()
|
|
633
|
+
const skeleton = model.getSkeleton()
|
|
634
|
+
|
|
635
|
+
this.vertexBuffer = this.device.createBuffer({
|
|
636
|
+
label: "model vertex buffer",
|
|
637
|
+
size: vertices.byteLength,
|
|
638
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
639
|
+
})
|
|
640
|
+
this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices)
|
|
641
|
+
this.vertexCount = model.getVertexCount()
|
|
642
|
+
|
|
643
|
+
this.jointsBuffer = this.device.createBuffer({
|
|
644
|
+
label: "joints buffer",
|
|
645
|
+
size: skinning.joints.byteLength,
|
|
646
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
647
|
+
})
|
|
648
|
+
this.device.queue.writeBuffer(
|
|
649
|
+
this.jointsBuffer,
|
|
650
|
+
0,
|
|
651
|
+
skinning.joints.buffer,
|
|
652
|
+
skinning.joints.byteOffset,
|
|
653
|
+
skinning.joints.byteLength
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
this.weightsBuffer = this.device.createBuffer({
|
|
657
|
+
label: "weights buffer",
|
|
658
|
+
size: skinning.weights.byteLength,
|
|
659
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
660
|
+
})
|
|
661
|
+
this.device.queue.writeBuffer(
|
|
662
|
+
this.weightsBuffer,
|
|
663
|
+
0,
|
|
664
|
+
skinning.weights.buffer,
|
|
665
|
+
skinning.weights.byteOffset,
|
|
666
|
+
skinning.weights.byteLength
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
const boneCount = skeleton.bones.length
|
|
670
|
+
const matrixSize = boneCount * 16 * 4
|
|
671
|
+
|
|
672
|
+
this.skinMatrixBuffer = this.device.createBuffer({
|
|
673
|
+
label: "skin matrices",
|
|
674
|
+
size: Math.max(256, matrixSize),
|
|
675
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
this.worldMatrixBuffer = this.device.createBuffer({
|
|
679
|
+
label: "world matrices",
|
|
680
|
+
size: Math.max(256, matrixSize),
|
|
681
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
this.inverseBindMatrixBuffer = this.device.createBuffer({
|
|
685
|
+
label: "inverse bind matrices",
|
|
686
|
+
size: Math.max(256, matrixSize),
|
|
687
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
const invBindMatrices = skeleton.inverseBindMatrices
|
|
691
|
+
this.device.queue.writeBuffer(
|
|
692
|
+
this.inverseBindMatrixBuffer,
|
|
693
|
+
0,
|
|
694
|
+
invBindMatrices.buffer,
|
|
695
|
+
invBindMatrices.byteOffset,
|
|
696
|
+
invBindMatrices.byteLength
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
this.boneCountBuffer = this.device.createBuffer({
|
|
700
|
+
label: "bone count uniform",
|
|
701
|
+
size: 32, // Minimum uniform buffer size is 32 bytes
|
|
702
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
703
|
+
})
|
|
704
|
+
const boneCountData = new Uint32Array(8) // 32 bytes total
|
|
705
|
+
boneCountData[0] = boneCount
|
|
706
|
+
this.device.queue.writeBuffer(this.boneCountBuffer, 0, boneCountData)
|
|
707
|
+
|
|
708
|
+
this.createSkinMatrixComputePipeline()
|
|
709
|
+
|
|
710
|
+
const indices = model.getIndices()
|
|
711
|
+
if (indices) {
|
|
712
|
+
this.indexBuffer = this.device.createBuffer({
|
|
713
|
+
label: "model index buffer",
|
|
714
|
+
size: indices.byteLength,
|
|
715
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
716
|
+
})
|
|
717
|
+
this.device.queue.writeBuffer(this.indexBuffer, 0, indices)
|
|
718
|
+
} else {
|
|
719
|
+
throw new Error("Model has no index buffer")
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
await this.setupMaterials(model)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private materialDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
726
|
+
private outlineDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
727
|
+
|
|
728
|
+
// Step 8: Load textures and create material bind groups
|
|
729
|
+
private async setupMaterials(model: Model) {
|
|
730
|
+
const materials = model.getMaterials()
|
|
731
|
+
if (materials.length === 0) {
|
|
732
|
+
throw new Error("Model has no materials")
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const textures = model.getTextures()
|
|
736
|
+
|
|
737
|
+
const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
|
|
738
|
+
if (texIndex < 0 || texIndex >= textures.length) {
|
|
739
|
+
return null
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const path = this.modelDir + textures[texIndex].path
|
|
743
|
+
const texture = await this.createTextureFromPath(path)
|
|
744
|
+
return texture
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const loadToonTexture = async (toonTextureIndex: number): Promise<GPUTexture> => {
|
|
748
|
+
const texture = await loadTextureByIndex(toonTextureIndex)
|
|
749
|
+
if (texture) return texture
|
|
750
|
+
|
|
751
|
+
// Default toon texture fallback - cache it
|
|
752
|
+
const defaultToonPath = "__default_toon__"
|
|
753
|
+
const cached = this.textureCache.get(defaultToonPath)
|
|
754
|
+
if (cached) return cached
|
|
755
|
+
|
|
756
|
+
const defaultToonData = new Uint8Array(256 * 2 * 4)
|
|
757
|
+
for (let i = 0; i < 256; i++) {
|
|
758
|
+
const factor = i / 255.0
|
|
759
|
+
const gray = Math.floor(128 + factor * 127)
|
|
760
|
+
defaultToonData[i * 4] = gray
|
|
761
|
+
defaultToonData[i * 4 + 1] = gray
|
|
762
|
+
defaultToonData[i * 4 + 2] = gray
|
|
763
|
+
defaultToonData[i * 4 + 3] = 255
|
|
764
|
+
defaultToonData[(256 + i) * 4] = gray
|
|
765
|
+
defaultToonData[(256 + i) * 4 + 1] = gray
|
|
766
|
+
defaultToonData[(256 + i) * 4 + 2] = gray
|
|
767
|
+
defaultToonData[(256 + i) * 4 + 3] = 255
|
|
768
|
+
}
|
|
769
|
+
const defaultToonTexture = this.device.createTexture({
|
|
770
|
+
label: "default toon texture",
|
|
771
|
+
size: [256, 2],
|
|
772
|
+
format: "rgba8unorm",
|
|
773
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
774
|
+
})
|
|
775
|
+
this.device.queue.writeTexture(
|
|
776
|
+
{ texture: defaultToonTexture },
|
|
777
|
+
defaultToonData,
|
|
778
|
+
{ bytesPerRow: 256 * 4 },
|
|
779
|
+
[256, 2]
|
|
780
|
+
)
|
|
781
|
+
this.textureCache.set(defaultToonPath, defaultToonTexture)
|
|
782
|
+
this.textureSizes.set(defaultToonPath, { width: 256, height: 2 })
|
|
783
|
+
return defaultToonTexture
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
this.materialDraws = []
|
|
787
|
+
this.outlineDraws = []
|
|
788
|
+
const outlineBindGroupLayout = this.outlinePipeline.getBindGroupLayout(0)
|
|
789
|
+
let runningFirstIndex = 0
|
|
790
|
+
|
|
791
|
+
for (const mat of materials) {
|
|
792
|
+
const matCount = mat.vertexCount | 0
|
|
793
|
+
if (matCount === 0) continue
|
|
794
|
+
|
|
795
|
+
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex)
|
|
796
|
+
if (!diffuseTexture) throw new Error(`Material "${mat.name}" has no diffuse texture`)
|
|
797
|
+
|
|
798
|
+
const toonTexture = await loadToonTexture(mat.toonTextureIndex)
|
|
799
|
+
|
|
800
|
+
const materialAlpha = mat.diffuse[3]
|
|
801
|
+
const EPSILON = 0.001
|
|
802
|
+
const isTransparent = materialAlpha < 1.0 - EPSILON
|
|
803
|
+
|
|
804
|
+
const materialUniformData = new Float32Array(4)
|
|
805
|
+
materialUniformData[0] = materialAlpha
|
|
806
|
+
materialUniformData[1] = 0.0
|
|
807
|
+
materialUniformData[2] = 0.0
|
|
808
|
+
materialUniformData[3] = 0.0
|
|
809
|
+
|
|
810
|
+
const materialUniformBuffer = this.device.createBuffer({
|
|
811
|
+
label: `material uniform: ${mat.name}`,
|
|
812
|
+
size: materialUniformData.byteLength,
|
|
813
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
814
|
+
})
|
|
815
|
+
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
816
|
+
|
|
817
|
+
const bindGroup = this.device.createBindGroup({
|
|
818
|
+
label: `material bind group: ${mat.name}`,
|
|
819
|
+
layout: this.pipeline.getBindGroupLayout(0),
|
|
820
|
+
entries: [
|
|
821
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
822
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
823
|
+
{ binding: 2, resource: diffuseTexture.createView() },
|
|
824
|
+
{ binding: 3, resource: this.textureSampler },
|
|
825
|
+
{ binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
|
|
826
|
+
{ binding: 5, resource: toonTexture.createView() },
|
|
827
|
+
{ binding: 6, resource: this.textureSampler },
|
|
828
|
+
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
829
|
+
],
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
// All materials use the same pipeline
|
|
833
|
+
this.materialDraws.push({
|
|
834
|
+
count: matCount,
|
|
835
|
+
firstIndex: runningFirstIndex,
|
|
836
|
+
bindGroup,
|
|
837
|
+
isTransparent,
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
// Outline for all materials (including transparent)
|
|
841
|
+
// Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
842
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
843
|
+
const materialUniformData = new Float32Array(8)
|
|
844
|
+
materialUniformData[0] = mat.edgeColor[0]
|
|
845
|
+
materialUniformData[1] = mat.edgeColor[1]
|
|
846
|
+
materialUniformData[2] = mat.edgeColor[2]
|
|
847
|
+
materialUniformData[3] = mat.edgeColor[3]
|
|
848
|
+
materialUniformData[4] = mat.edgeSize
|
|
849
|
+
|
|
850
|
+
const materialUniformBuffer = this.device.createBuffer({
|
|
851
|
+
label: `outline material uniform: ${mat.name}`,
|
|
852
|
+
size: materialUniformData.byteLength,
|
|
853
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
854
|
+
})
|
|
855
|
+
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
856
|
+
|
|
857
|
+
const outlineBindGroup = this.device.createBindGroup({
|
|
858
|
+
label: `outline bind group: ${mat.name}`,
|
|
859
|
+
layout: outlineBindGroupLayout,
|
|
860
|
+
entries: [
|
|
861
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
862
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
863
|
+
{ binding: 2, resource: { buffer: this.skinMatrixBuffer! } },
|
|
864
|
+
],
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
// All outlines use the same pipeline
|
|
868
|
+
this.outlineDraws.push({
|
|
869
|
+
count: matCount,
|
|
870
|
+
firstIndex: runningFirstIndex,
|
|
871
|
+
bindGroup: outlineBindGroup,
|
|
872
|
+
isTransparent,
|
|
873
|
+
})
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
runningFirstIndex += matCount
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Helper: Load texture from file path with optional max size limit
|
|
881
|
+
private async createTextureFromPath(path: string, maxSize: number = 2048): Promise<GPUTexture | null> {
|
|
882
|
+
const cached = this.textureCache.get(path)
|
|
883
|
+
if (cached) {
|
|
884
|
+
return cached
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
const response = await fetch(path)
|
|
889
|
+
if (!response.ok) {
|
|
890
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
891
|
+
}
|
|
892
|
+
let imageBitmap = await createImageBitmap(await response.blob(), {
|
|
893
|
+
premultiplyAlpha: "none",
|
|
894
|
+
colorSpaceConversion: "none",
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
// Downscale if texture is too large
|
|
898
|
+
let finalWidth = imageBitmap.width
|
|
899
|
+
let finalHeight = imageBitmap.height
|
|
900
|
+
if (finalWidth > maxSize || finalHeight > maxSize) {
|
|
901
|
+
const scale = Math.min(maxSize / finalWidth, maxSize / finalHeight)
|
|
902
|
+
finalWidth = Math.floor(finalWidth * scale)
|
|
903
|
+
finalHeight = Math.floor(finalHeight * scale)
|
|
904
|
+
|
|
905
|
+
// Create canvas to downscale
|
|
906
|
+
const canvas = new OffscreenCanvas(finalWidth, finalHeight)
|
|
907
|
+
const ctx = canvas.getContext("2d")
|
|
908
|
+
if (ctx) {
|
|
909
|
+
ctx.drawImage(imageBitmap, 0, 0, finalWidth, finalHeight)
|
|
910
|
+
imageBitmap = await createImageBitmap(canvas)
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const texture = this.device.createTexture({
|
|
915
|
+
label: `texture: ${path}`,
|
|
916
|
+
size: [finalWidth, finalHeight],
|
|
917
|
+
format: "rgba8unorm",
|
|
918
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
919
|
+
})
|
|
920
|
+
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [finalWidth, finalHeight])
|
|
921
|
+
|
|
922
|
+
this.textureCache.set(path, texture)
|
|
923
|
+
this.textureSizes.set(path, { width: finalWidth, height: finalHeight })
|
|
924
|
+
return texture
|
|
925
|
+
} catch {
|
|
926
|
+
return null
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Step 9: Render one frame
|
|
931
|
+
public render() {
|
|
932
|
+
if (this.multisampleTexture && this.camera && this.device && this.currentModel) {
|
|
933
|
+
const currentTime = performance.now()
|
|
934
|
+
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
935
|
+
this.lastFrameTime = currentTime
|
|
936
|
+
|
|
937
|
+
this.updateCameraUniforms()
|
|
938
|
+
this.updateRenderTarget()
|
|
939
|
+
|
|
940
|
+
this.updateModelPose(deltaTime)
|
|
941
|
+
|
|
942
|
+
const encoder = this.device.createCommandEncoder()
|
|
943
|
+
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
944
|
+
|
|
945
|
+
pass.setVertexBuffer(0, this.vertexBuffer)
|
|
946
|
+
pass.setVertexBuffer(1, this.jointsBuffer)
|
|
947
|
+
pass.setVertexBuffer(2, this.weightsBuffer)
|
|
948
|
+
pass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
949
|
+
|
|
950
|
+
this.drawCallCount = 0
|
|
951
|
+
this.drawOutlines(pass, false)
|
|
952
|
+
this.drawModel(pass, false)
|
|
953
|
+
this.drawModel(pass, true)
|
|
954
|
+
this.drawOutlines(pass, true)
|
|
955
|
+
|
|
956
|
+
pass.end()
|
|
957
|
+
this.device.queue.submit([encoder.finish()])
|
|
958
|
+
this.updateStats(performance.now() - currentTime)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Update camera uniform buffer each frame
|
|
963
|
+
private updateCameraUniforms() {
|
|
964
|
+
const viewMatrix = this.camera.getViewMatrix()
|
|
965
|
+
const projectionMatrix = this.camera.getProjectionMatrix()
|
|
966
|
+
const cameraPos = this.camera.getPosition()
|
|
967
|
+
this.cameraMatrixData.set(viewMatrix.values, 0)
|
|
968
|
+
this.cameraMatrixData.set(projectionMatrix.values, 16)
|
|
969
|
+
this.cameraMatrixData[32] = cameraPos.x
|
|
970
|
+
this.cameraMatrixData[33] = cameraPos.y
|
|
971
|
+
this.cameraMatrixData[34] = cameraPos.z
|
|
972
|
+
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Update render target texture view
|
|
976
|
+
private updateRenderTarget() {
|
|
977
|
+
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
978
|
+
if (this.sampleCount > 1) {
|
|
979
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
980
|
+
} else {
|
|
981
|
+
colorAttachment.view = this.context.getCurrentTexture().createView()
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Update model pose and physics
|
|
986
|
+
private updateModelPose(deltaTime: number) {
|
|
987
|
+
this.currentModel!.evaluatePose()
|
|
988
|
+
|
|
989
|
+
// Upload world matrices to GPU
|
|
990
|
+
const worldMats = this.currentModel!.getBoneWorldMatrices()
|
|
991
|
+
this.device.queue.writeBuffer(
|
|
992
|
+
this.worldMatrixBuffer!,
|
|
993
|
+
0,
|
|
994
|
+
worldMats.buffer,
|
|
995
|
+
worldMats.byteOffset,
|
|
996
|
+
worldMats.byteLength
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
if (this.physics) {
|
|
1000
|
+
this.physics.step(deltaTime, worldMats, this.currentModel!.getBoneInverseBindMatrices())
|
|
1001
|
+
// Re-upload world matrices after physics (physics may have updated bones)
|
|
1002
|
+
this.device.queue.writeBuffer(
|
|
1003
|
+
this.worldMatrixBuffer!,
|
|
1004
|
+
0,
|
|
1005
|
+
worldMats.buffer,
|
|
1006
|
+
worldMats.byteOffset,
|
|
1007
|
+
worldMats.byteLength
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Compute skin matrices on GPU
|
|
1012
|
+
this.computeSkinMatrices()
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Compute skin matrices on GPU
|
|
1016
|
+
private computeSkinMatrices() {
|
|
1017
|
+
const boneCount = this.currentModel!.getSkeleton().bones.length
|
|
1018
|
+
const workgroupSize = 64
|
|
1019
|
+
// Dispatch exactly enough threads for all bones (no bounds check needed)
|
|
1020
|
+
const workgroupCount = Math.ceil(boneCount / workgroupSize)
|
|
1021
|
+
|
|
1022
|
+
// Update bone count uniform
|
|
1023
|
+
const boneCountData = new Uint32Array(8) // 32 bytes total
|
|
1024
|
+
boneCountData[0] = boneCount
|
|
1025
|
+
this.device.queue.writeBuffer(this.boneCountBuffer!, 0, boneCountData)
|
|
1026
|
+
|
|
1027
|
+
const bindGroup = this.device.createBindGroup({
|
|
1028
|
+
label: "skin matrix compute bind group",
|
|
1029
|
+
layout: this.skinMatrixComputePipeline!.getBindGroupLayout(0),
|
|
1030
|
+
entries: [
|
|
1031
|
+
{ binding: 0, resource: { buffer: this.boneCountBuffer! } },
|
|
1032
|
+
{ binding: 1, resource: { buffer: this.worldMatrixBuffer! } },
|
|
1033
|
+
{ binding: 2, resource: { buffer: this.inverseBindMatrixBuffer! } },
|
|
1034
|
+
{ binding: 3, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1035
|
+
],
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
const encoder = this.device.createCommandEncoder()
|
|
1039
|
+
const pass = encoder.beginComputePass()
|
|
1040
|
+
pass.setPipeline(this.skinMatrixComputePipeline!)
|
|
1041
|
+
pass.setBindGroup(0, bindGroup)
|
|
1042
|
+
pass.dispatchWorkgroups(workgroupCount)
|
|
1043
|
+
pass.end()
|
|
1044
|
+
this.device.queue.submit([encoder.finish()])
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Draw outlines (opaque or transparent)
|
|
1048
|
+
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
1049
|
+
if (this.outlineDraws.length === 0) return
|
|
1050
|
+
pass.setPipeline(this.outlinePipeline)
|
|
1051
|
+
for (const draw of this.outlineDraws) {
|
|
1052
|
+
if (draw.count > 0 && draw.isTransparent === transparent) {
|
|
1053
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1054
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Draw model materials (opaque or transparent)
|
|
1060
|
+
private drawModel(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
1061
|
+
pass.setPipeline(this.pipeline)
|
|
1062
|
+
for (const draw of this.materialDraws) {
|
|
1063
|
+
if (draw.count > 0 && draw.isTransparent === transparent) {
|
|
1064
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1065
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1066
|
+
this.drawCallCount++
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private updateStats(frameTime: number) {
|
|
1072
|
+
const maxSamples = 60
|
|
1073
|
+
this.frameTimeSamples.push(frameTime)
|
|
1074
|
+
this.frameTimeSum += frameTime
|
|
1075
|
+
if (this.frameTimeSamples.length > maxSamples) {
|
|
1076
|
+
const removed = this.frameTimeSamples.shift()!
|
|
1077
|
+
this.frameTimeSum -= removed
|
|
1078
|
+
}
|
|
1079
|
+
const avgFrameTime = this.frameTimeSum / this.frameTimeSamples.length
|
|
1080
|
+
this.stats.frameTime = Math.round(avgFrameTime * 100) / 100
|
|
1081
|
+
|
|
1082
|
+
const now = performance.now()
|
|
1083
|
+
this.framesSinceLastUpdate++
|
|
1084
|
+
const elapsed = now - this.lastFpsUpdate
|
|
1085
|
+
|
|
1086
|
+
if (elapsed >= 1000) {
|
|
1087
|
+
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * 1000)
|
|
1088
|
+
this.framesSinceLastUpdate = 0
|
|
1089
|
+
this.lastFpsUpdate = now
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Calculate GPU memory: textures + buffers + render targets
|
|
1093
|
+
let textureMemoryBytes = 0
|
|
1094
|
+
for (const [path, size] of this.textureSizes.entries()) {
|
|
1095
|
+
if (this.textureCache.has(path)) {
|
|
1096
|
+
textureMemoryBytes += size.width * size.height * 4 // RGBA8 = 4 bytes per pixel
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
let bufferMemoryBytes = 0
|
|
1101
|
+
if (this.vertexBuffer) {
|
|
1102
|
+
const vertices = this.currentModel?.getVertices()
|
|
1103
|
+
if (vertices) bufferMemoryBytes += vertices.byteLength
|
|
1104
|
+
}
|
|
1105
|
+
if (this.indexBuffer) {
|
|
1106
|
+
const indices = this.currentModel?.getIndices()
|
|
1107
|
+
if (indices) bufferMemoryBytes += indices.byteLength
|
|
1108
|
+
}
|
|
1109
|
+
if (this.jointsBuffer) {
|
|
1110
|
+
const skinning = this.currentModel?.getSkinning()
|
|
1111
|
+
if (skinning) bufferMemoryBytes += skinning.joints.byteLength
|
|
1112
|
+
}
|
|
1113
|
+
if (this.weightsBuffer) {
|
|
1114
|
+
const skinning = this.currentModel?.getSkinning()
|
|
1115
|
+
if (skinning) bufferMemoryBytes += skinning.weights.byteLength
|
|
1116
|
+
}
|
|
1117
|
+
if (this.skinMatrixBuffer) {
|
|
1118
|
+
const skeleton = this.currentModel?.getSkeleton()
|
|
1119
|
+
if (skeleton) bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4)
|
|
1120
|
+
}
|
|
1121
|
+
bufferMemoryBytes += 40 * 4 // cameraUniformBuffer
|
|
1122
|
+
bufferMemoryBytes += 64 * 4 // lightUniformBuffer
|
|
1123
|
+
bufferMemoryBytes += this.materialDraws.length * 4 // Material uniform buffers
|
|
1124
|
+
|
|
1125
|
+
let renderTargetMemoryBytes = 0
|
|
1126
|
+
if (this.multisampleTexture) {
|
|
1127
|
+
const width = this.canvas.width
|
|
1128
|
+
const height = this.canvas.height
|
|
1129
|
+
renderTargetMemoryBytes += width * height * 4 * this.sampleCount // multisample color
|
|
1130
|
+
renderTargetMemoryBytes += width * height * 4 // depth
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const totalGPUMemoryBytes = textureMemoryBytes + bufferMemoryBytes + renderTargetMemoryBytes
|
|
1134
|
+
this.stats.gpuMemory = Math.round((totalGPUMemoryBytes / 1024 / 1024) * 100) / 100
|
|
1135
|
+
}
|
|
1136
|
+
}
|