reze-engine 0.3.6 → 0.3.7

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/src/engine.ts CHANGED
@@ -2,8 +2,6 @@ import { Camera } from "./camera"
2
2
  import { Quat, Vec3 } from "./math"
3
3
  import { Model } from "./model"
4
4
  import { PmxLoader } from "./pmx-loader"
5
- import { Physics } from "./physics"
6
- import { AnimationPose, Player } from "./player"
7
5
 
8
6
  export type EngineOptions = {
9
7
  ambientColor?: Vec3
@@ -18,7 +16,19 @@ export interface EngineStats {
18
16
  frameTime: number // ms
19
17
  }
20
18
 
19
+ type DrawCallType =
20
+ | "opaque"
21
+ | "eye"
22
+ | "hair-over-eyes"
23
+ | "hair-over-non-eyes"
24
+ | "transparent"
25
+ | "opaque-outline"
26
+ | "eye-outline"
27
+ | "hair-outline"
28
+ | "transparent-outline"
29
+
21
30
  interface DrawCall {
31
+ type: DrawCallType
22
32
  count: number
23
33
  firstIndex: number
24
34
  bindGroup: GPUBindGroup
@@ -54,17 +64,12 @@ export class Engine {
54
64
  private jointsBuffer!: GPUBuffer
55
65
  private weightsBuffer!: GPUBuffer
56
66
  private skinMatrixBuffer?: GPUBuffer
57
- private worldMatrixBuffer?: GPUBuffer
58
67
  private inverseBindMatrixBuffer?: GPUBuffer
59
- private skinMatrixComputePipeline?: GPUComputePipeline
60
- private skinMatrixComputeBindGroup?: GPUBindGroup
61
- private boneCountBuffer?: GPUBuffer
62
68
  private multisampleTexture!: GPUTexture
63
69
  private readonly sampleCount = 4
64
70
  private renderPassDescriptor!: GPURenderPassDescriptor
65
71
  // Constants
66
72
  private readonly STENCIL_EYE_VALUE = 1
67
- private readonly COMPUTE_WORKGROUP_SIZE = 64
68
73
  private readonly BLOOM_DOWNSCALE_FACTOR = 2
69
74
 
70
75
  // Default values
@@ -106,20 +111,11 @@ export class Engine {
106
111
 
107
112
  private currentModel: Model | null = null
108
113
  private modelDir: string = ""
109
- private physics: Physics | null = null
110
114
  private materialSampler!: GPUSampler
111
115
  private textureCache = new Map<string, GPUTexture>()
112
116
  private vertexBufferNeedsUpdate = false
113
- // Draw lists
114
- private opaqueDraws: DrawCall[] = []
115
- private eyeDraws: DrawCall[] = []
116
- private hairDrawsOverEyes: DrawCall[] = []
117
- private hairDrawsOverNonEyes: DrawCall[] = []
118
- private transparentDraws: DrawCall[] = []
119
- private opaqueOutlineDraws: DrawCall[] = []
120
- private eyeOutlineDraws: DrawCall[] = []
121
- private hairOutlineDraws: DrawCall[] = []
122
- private transparentOutlineDraws: DrawCall[] = []
117
+ // Unified draw call list
118
+ private drawCalls: DrawCall[] = []
123
119
 
124
120
  private lastFpsUpdate = performance.now()
125
121
  private framesSinceLastUpdate = 0
@@ -133,9 +129,6 @@ export class Engine {
133
129
  private animationFrameId: number | null = null
134
130
  private renderLoopCallback: (() => void) | null = null
135
131
 
136
- private player: Player = new Player()
137
- private hasAnimation = false // Set to true when loadAnimation is called
138
-
139
132
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
140
133
  this.canvas = canvas
141
134
  if (options) {
@@ -177,6 +170,37 @@ export class Engine {
177
170
  this.setupResize()
178
171
  }
179
172
 
173
+ private createRenderPipeline(config: {
174
+ label: string
175
+ layout: GPUPipelineLayout
176
+ shaderModule: GPUShaderModule
177
+ vertexBuffers: GPUVertexBufferLayout[]
178
+ fragmentTarget?: GPUColorTargetState
179
+ fragmentEntryPoint?: string
180
+ cullMode?: GPUCullMode
181
+ depthStencil?: GPUDepthStencilState
182
+ multisample?: GPUMultisampleState
183
+ }): GPURenderPipeline {
184
+ return this.device.createRenderPipeline({
185
+ label: config.label,
186
+ layout: config.layout,
187
+ vertex: {
188
+ module: config.shaderModule,
189
+ buffers: config.vertexBuffers,
190
+ },
191
+ fragment: config.fragmentTarget
192
+ ? {
193
+ module: config.shaderModule,
194
+ entryPoint: config.fragmentEntryPoint,
195
+ targets: [config.fragmentTarget],
196
+ }
197
+ : undefined,
198
+ primitive: { cullMode: config.cullMode ?? "none" },
199
+ depthStencil: config.depthStencil,
200
+ multisample: config.multisample ?? { count: this.sampleCount },
201
+ })
202
+ }
203
+
180
204
  private createPipelines() {
181
205
  this.materialSampler = this.device.createSampler({
182
206
  magFilter: "linear",
@@ -185,6 +209,78 @@ export class Engine {
185
209
  addressModeV: "repeat",
186
210
  })
187
211
 
212
+ // Shared vertex buffer layouts
213
+ const fullVertexBuffers: GPUVertexBufferLayout[] = [
214
+ {
215
+ arrayStride: 8 * 4,
216
+ attributes: [
217
+ { shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
218
+ { shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
219
+ { shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
220
+ ],
221
+ },
222
+ {
223
+ arrayStride: 4 * 2,
224
+ attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
225
+ },
226
+ {
227
+ arrayStride: 4,
228
+ attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
229
+ },
230
+ ]
231
+
232
+ const outlineVertexBuffers: GPUVertexBufferLayout[] = [
233
+ {
234
+ arrayStride: 8 * 4,
235
+ attributes: [
236
+ { shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
237
+ { shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
238
+ ],
239
+ },
240
+ {
241
+ arrayStride: 4 * 2,
242
+ attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
243
+ },
244
+ {
245
+ arrayStride: 4,
246
+ attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
247
+ },
248
+ ]
249
+
250
+ const depthOnlyVertexBuffers: GPUVertexBufferLayout[] = [
251
+ {
252
+ arrayStride: 8 * 4,
253
+ attributes: [
254
+ { shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
255
+ { shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
256
+ ],
257
+ },
258
+ {
259
+ arrayStride: 4 * 2,
260
+ attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
261
+ },
262
+ {
263
+ arrayStride: 4,
264
+ attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
265
+ },
266
+ ]
267
+
268
+ const standardBlend: GPUColorTargetState = {
269
+ format: this.presentationFormat,
270
+ blend: {
271
+ color: {
272
+ srcFactor: "src-alpha",
273
+ dstFactor: "one-minus-src-alpha",
274
+ operation: "add",
275
+ },
276
+ alpha: {
277
+ srcFactor: "one",
278
+ dstFactor: "one-minus-src-alpha",
279
+ operation: "add",
280
+ },
281
+ },
282
+ }
283
+
188
284
  const shaderModule = this.device.createShaderModule({
189
285
  label: "model shaders",
190
286
  code: /* wgsl */ `
@@ -301,59 +397,18 @@ export class Engine {
301
397
  bindGroupLayouts: [this.mainBindGroupLayout],
302
398
  })
303
399
 
304
- this.modelPipeline = this.device.createRenderPipeline({
400
+ this.modelPipeline = this.createRenderPipeline({
305
401
  label: "model pipeline",
306
402
  layout: mainPipelineLayout,
307
- vertex: {
308
- module: shaderModule,
309
- buffers: [
310
- {
311
- arrayStride: 8 * 4,
312
- attributes: [
313
- { shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
314
- { shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
315
- { shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
316
- ],
317
- },
318
- {
319
- arrayStride: 4 * 2,
320
- attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
321
- },
322
- {
323
- arrayStride: 4,
324
- attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
325
- },
326
- ],
327
- },
328
- fragment: {
329
- module: shaderModule,
330
- targets: [
331
- {
332
- format: this.presentationFormat,
333
- blend: {
334
- color: {
335
- srcFactor: "src-alpha",
336
- dstFactor: "one-minus-src-alpha",
337
- operation: "add",
338
- },
339
- alpha: {
340
- srcFactor: "one",
341
- dstFactor: "one-minus-src-alpha",
342
- operation: "add",
343
- },
344
- },
345
- },
346
- ],
347
- },
348
- primitive: { cullMode: "none" },
403
+ shaderModule,
404
+ vertexBuffers: fullVertexBuffers,
405
+ fragmentTarget: standardBlend,
406
+ cullMode: "none",
349
407
  depthStencil: {
350
408
  format: "depth24plus-stencil8",
351
409
  depthWriteEnabled: true,
352
410
  depthCompare: "less-equal",
353
411
  },
354
- multisample: {
355
- count: this.sampleCount,
356
- },
357
412
  })
358
413
 
359
414
  // Create bind group layout for outline pipelines
@@ -443,196 +498,58 @@ export class Engine {
443
498
  `,
444
499
  })
445
500
 
446
- this.outlinePipeline = this.device.createRenderPipeline({
501
+ this.outlinePipeline = this.createRenderPipeline({
447
502
  label: "outline pipeline",
448
503
  layout: outlinePipelineLayout,
449
- vertex: {
450
- module: outlineShaderModule,
451
- buffers: [
452
- {
453
- arrayStride: 8 * 4,
454
- attributes: [
455
- {
456
- shaderLocation: 0,
457
- offset: 0,
458
- format: "float32x3" as GPUVertexFormat,
459
- },
460
- {
461
- shaderLocation: 1,
462
- offset: 3 * 4,
463
- format: "float32x3" as GPUVertexFormat,
464
- },
465
- ],
466
- },
467
- {
468
- arrayStride: 4 * 2,
469
- attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
470
- },
471
- {
472
- arrayStride: 4,
473
- attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
474
- },
475
- ],
476
- },
477
- fragment: {
478
- module: outlineShaderModule,
479
- targets: [
480
- {
481
- format: this.presentationFormat,
482
- blend: {
483
- color: {
484
- srcFactor: "src-alpha",
485
- dstFactor: "one-minus-src-alpha",
486
- operation: "add",
487
- },
488
- alpha: {
489
- srcFactor: "one",
490
- dstFactor: "one-minus-src-alpha",
491
- operation: "add",
492
- },
493
- },
494
- },
495
- ],
496
- },
497
- primitive: {
498
- cullMode: "back",
499
- },
504
+ shaderModule: outlineShaderModule,
505
+ vertexBuffers: outlineVertexBuffers,
506
+ fragmentTarget: standardBlend,
507
+ cullMode: "back",
500
508
  depthStencil: {
501
509
  format: "depth24plus-stencil8",
502
510
  depthWriteEnabled: true,
503
511
  depthCompare: "less-equal",
504
512
  },
505
- multisample: {
506
- count: this.sampleCount,
507
- },
508
513
  })
509
514
 
510
515
  // Hair outline pipeline
511
- this.hairOutlinePipeline = this.device.createRenderPipeline({
516
+ this.hairOutlinePipeline = this.createRenderPipeline({
512
517
  label: "hair outline pipeline",
513
518
  layout: outlinePipelineLayout,
514
- vertex: {
515
- module: outlineShaderModule,
516
- buffers: [
517
- {
518
- arrayStride: 8 * 4,
519
- attributes: [
520
- {
521
- shaderLocation: 0,
522
- offset: 0,
523
- format: "float32x3" as GPUVertexFormat,
524
- },
525
- {
526
- shaderLocation: 1,
527
- offset: 3 * 4,
528
- format: "float32x3" as GPUVertexFormat,
529
- },
530
- ],
531
- },
532
- {
533
- arrayStride: 4 * 2,
534
- attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
535
- },
536
- {
537
- arrayStride: 4,
538
- attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
539
- },
540
- ],
541
- },
542
- fragment: {
543
- module: outlineShaderModule,
544
- targets: [
545
- {
546
- format: this.presentationFormat,
547
- blend: {
548
- color: {
549
- srcFactor: "src-alpha",
550
- dstFactor: "one-minus-src-alpha",
551
- operation: "add",
552
- },
553
- alpha: {
554
- srcFactor: "one",
555
- dstFactor: "one-minus-src-alpha",
556
- operation: "add",
557
- },
558
- },
559
- },
560
- ],
561
- },
562
- primitive: {
563
- cullMode: "back",
564
- },
519
+ shaderModule: outlineShaderModule,
520
+ vertexBuffers: outlineVertexBuffers,
521
+ fragmentTarget: standardBlend,
522
+ cullMode: "back",
565
523
  depthStencil: {
566
524
  format: "depth24plus-stencil8",
567
- depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
568
- depthCompare: "less-equal", // Only draw where hair depth exists (no stencil test needed)
569
- depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
525
+ depthWriteEnabled: false,
526
+ depthCompare: "less-equal",
527
+ depthBias: -0.0001,
570
528
  depthBiasSlopeScale: 0.0,
571
529
  depthBiasClamp: 0.0,
572
530
  },
573
- multisample: {
574
- count: this.sampleCount,
575
- },
576
531
  })
577
532
 
578
533
  // Eye overlay pipeline (renders after opaque, writes stencil)
579
- this.eyePipeline = this.device.createRenderPipeline({
534
+ this.eyePipeline = this.createRenderPipeline({
580
535
  label: "eye overlay pipeline",
581
536
  layout: mainPipelineLayout,
582
- vertex: {
583
- module: shaderModule,
584
- buffers: [
585
- {
586
- arrayStride: 8 * 4,
587
- attributes: [
588
- { shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
589
- { shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
590
- { shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
591
- ],
592
- },
593
- {
594
- arrayStride: 4 * 2,
595
- attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
596
- },
597
- {
598
- arrayStride: 4,
599
- attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
600
- },
601
- ],
602
- },
603
- fragment: {
604
- module: shaderModule,
605
- targets: [
606
- {
607
- format: this.presentationFormat,
608
- blend: {
609
- color: {
610
- srcFactor: "src-alpha",
611
- dstFactor: "one-minus-src-alpha",
612
- operation: "add",
613
- },
614
- alpha: {
615
- srcFactor: "one",
616
- dstFactor: "one-minus-src-alpha",
617
- operation: "add",
618
- },
619
- },
620
- },
621
- ],
622
- },
623
- primitive: { cullMode: "front" },
537
+ shaderModule,
538
+ vertexBuffers: fullVertexBuffers,
539
+ fragmentTarget: standardBlend,
540
+ cullMode: "front",
624
541
  depthStencil: {
625
542
  format: "depth24plus-stencil8",
626
- depthWriteEnabled: true, // Write depth to occlude back of head
627
- depthCompare: "less-equal", // More lenient to reduce precision conflicts
628
- depthBias: -0.00005, // Reduced bias to minimize conflicts while still occluding back face
543
+ depthWriteEnabled: true,
544
+ depthCompare: "less-equal",
545
+ depthBias: -0.00005,
629
546
  depthBiasSlopeScale: 0.0,
630
547
  depthBiasClamp: 0.0,
631
548
  stencilFront: {
632
549
  compare: "always",
633
550
  failOp: "keep",
634
551
  depthFailOp: "keep",
635
- passOp: "replace", // Write stencil value 1
552
+ passOp: "replace",
636
553
  },
637
554
  stencilBack: {
638
555
  compare: "always",
@@ -641,7 +558,6 @@ export class Engine {
641
558
  passOp: "replace",
642
559
  },
643
560
  },
644
- multisample: { count: this.sampleCount },
645
561
  })
646
562
 
647
563
  // Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
@@ -690,104 +606,42 @@ export class Engine {
690
606
  })
691
607
 
692
608
  // Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
693
- this.hairDepthPipeline = this.device.createRenderPipeline({
609
+ this.hairDepthPipeline = this.createRenderPipeline({
694
610
  label: "hair depth pre-pass",
695
611
  layout: mainPipelineLayout,
696
- vertex: {
697
- module: depthOnlyShaderModule,
698
- buffers: [
699
- {
700
- arrayStride: 8 * 4,
701
- attributes: [
702
- { shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
703
- { shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
704
- ],
705
- },
706
- {
707
- arrayStride: 4 * 2,
708
- attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
709
- },
710
- {
711
- arrayStride: 4,
712
- attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
713
- },
714
- ],
715
- },
716
- fragment: {
717
- module: depthOnlyShaderModule,
718
- entryPoint: "fs",
719
- targets: [
720
- {
721
- format: this.presentationFormat,
722
- writeMask: 0, // Disable all color writes - we only care about depth
723
- },
724
- ],
612
+ shaderModule: depthOnlyShaderModule,
613
+ vertexBuffers: depthOnlyVertexBuffers,
614
+ fragmentTarget: {
615
+ format: this.presentationFormat,
616
+ writeMask: 0,
725
617
  },
726
- primitive: { cullMode: "front" },
618
+ fragmentEntryPoint: "fs",
619
+ cullMode: "front",
727
620
  depthStencil: {
728
621
  format: "depth24plus-stencil8",
729
622
  depthWriteEnabled: true,
730
- depthCompare: "less-equal", // Match the color pass compare mode for consistency
623
+ depthCompare: "less-equal",
731
624
  depthBias: 0.0,
732
625
  depthBiasSlopeScale: 0.0,
733
626
  depthBiasClamp: 0.0,
734
627
  },
735
- multisample: { count: this.sampleCount },
736
628
  })
737
629
 
738
630
  // Hair pipelines for rendering over eyes vs non-eyes (only differ in stencil compare mode)
739
631
  const createHairPipeline = (isOverEyes: boolean): GPURenderPipeline => {
740
- return this.device.createRenderPipeline({
632
+ return this.createRenderPipeline({
741
633
  label: `hair pipeline (${isOverEyes ? "over eyes" : "over non-eyes"})`,
742
634
  layout: mainPipelineLayout,
743
- vertex: {
744
- module: shaderModule,
745
- buffers: [
746
- {
747
- arrayStride: 8 * 4,
748
- attributes: [
749
- { shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
750
- { shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
751
- { shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
752
- ],
753
- },
754
- {
755
- arrayStride: 4 * 2,
756
- attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
757
- },
758
- {
759
- arrayStride: 4,
760
- attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
761
- },
762
- ],
763
- },
764
- fragment: {
765
- module: shaderModule,
766
- targets: [
767
- {
768
- format: this.presentationFormat,
769
- blend: {
770
- color: {
771
- srcFactor: "src-alpha",
772
- dstFactor: "one-minus-src-alpha",
773
- operation: "add",
774
- },
775
- alpha: {
776
- srcFactor: "one",
777
- dstFactor: "one-minus-src-alpha",
778
- operation: "add",
779
- },
780
- },
781
- },
782
- ],
783
- },
784
- primitive: { cullMode: "front" },
635
+ shaderModule,
636
+ vertexBuffers: fullVertexBuffers,
637
+ fragmentTarget: standardBlend,
638
+ cullMode: "front",
785
639
  depthStencil: {
786
640
  format: "depth24plus-stencil8",
787
- depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
788
- depthCompare: "less-equal", // More lenient than "equal" to avoid precision issues with MSAA
641
+ depthWriteEnabled: false,
642
+ depthCompare: "less-equal",
789
643
  stencilFront: {
790
- compare: isOverEyes ? "equal" : "not-equal", // Over eyes: stencil == 1, over non-eyes: stencil != 1
644
+ compare: isOverEyes ? "equal" : "not-equal",
791
645
  failOp: "keep",
792
646
  depthFailOp: "keep",
793
647
  passOp: "keep",
@@ -799,7 +653,6 @@ export class Engine {
799
653
  passOp: "keep",
800
654
  },
801
655
  },
802
- multisample: { count: this.sampleCount },
803
656
  })
804
657
  }
805
658
 
@@ -807,46 +660,6 @@ export class Engine {
807
660
  this.hairPipelineOverNonEyes = createHairPipeline(false)
808
661
  }
809
662
 
810
- // Create compute shader for skin matrix computation
811
- private createSkinMatrixComputePipeline() {
812
- const computeShader = this.device.createShaderModule({
813
- label: "skin matrix compute",
814
- code: /* wgsl */ `
815
- struct BoneCountUniform {
816
- count: u32,
817
- _padding1: u32,
818
- _padding2: u32,
819
- _padding3: u32,
820
- _padding4: vec4<u32>,
821
- };
822
-
823
- @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
824
- @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
825
- @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
826
- @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
827
-
828
- @compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
829
- fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
830
- let boneIndex = globalId.x;
831
- if (boneIndex >= boneCount.count) {
832
- return;
833
- }
834
- let worldMat = worldMatrices[boneIndex];
835
- let invBindMat = inverseBindMatrices[boneIndex];
836
- skinMatrices[boneIndex] = worldMat * invBindMat;
837
- }
838
- `,
839
- })
840
-
841
- this.skinMatrixComputePipeline = this.device.createComputePipeline({
842
- label: "skin matrix compute pipeline",
843
- layout: "auto",
844
- compute: {
845
- module: computeShader,
846
- },
847
- })
848
- }
849
-
850
663
  // Create bloom post-processing pipelines
851
664
  private createBloomPipelines() {
852
665
  // Bloom extraction shader (extracts bright areas)
@@ -1267,171 +1080,28 @@ export class Engine {
1267
1080
  }
1268
1081
 
1269
1082
  public async loadAnimation(url: string) {
1270
- await this.player.loadVmd(url)
1271
- this.hasAnimation = true
1272
-
1273
- // Show first frame (time 0) immediately
1274
- if (this.currentModel) {
1275
- const initialPose = this.player.getPoseAtTime(0)
1276
- this.applyPose(initialPose)
1277
-
1278
- // Reset bones without time 0 keyframes
1279
- const skeleton = this.currentModel.getSkeleton()
1280
- const bonesWithPose = new Set(initialPose.boneRotations.keys())
1281
- const bonesToReset: string[] = []
1282
- for (const bone of skeleton.bones) {
1283
- if (!bonesWithPose.has(bone.name)) {
1284
- bonesToReset.push(bone.name)
1285
- }
1286
- }
1287
-
1288
- if (bonesToReset.length > 0) {
1289
- const identityQuat = new Quat(0, 0, 0, 1)
1290
- const identityQuats = new Array(bonesToReset.length).fill(identityQuat)
1291
- this.rotateBones(bonesToReset, identityQuats, 0)
1292
- }
1293
-
1294
- // Update model pose and physics
1295
- this.currentModel.evaluatePose()
1296
-
1297
- if (this.physics) {
1298
- const worldMats = this.currentModel.getBoneWorldMatrices()
1299
- this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
1300
-
1301
- // Upload matrices immediately
1302
- this.device.queue.writeBuffer(
1303
- this.worldMatrixBuffer!,
1304
- 0,
1305
- worldMats.buffer,
1306
- worldMats.byteOffset,
1307
- worldMats.byteLength
1308
- )
1309
- const encoder = this.device.createCommandEncoder()
1310
- this.computeSkinMatrices(encoder)
1311
- this.device.queue.submit([encoder.finish()])
1312
- }
1313
- }
1083
+ if (!this.currentModel) return
1084
+ await this.currentModel.loadVmd(url)
1314
1085
  }
1315
1086
 
1316
1087
  public playAnimation() {
1317
- if (!this.hasAnimation || !this.currentModel) return
1318
-
1319
- const wasPaused = this.player.isPausedState
1320
- const wasPlaying = this.player.isPlayingState
1321
-
1322
- // Only reset pose and physics if starting from beginning (not resuming)
1323
- if (!wasPlaying && !wasPaused) {
1324
- // Get initial pose at time 0
1325
- const initialPose = this.player.getPoseAtTime(0)
1326
- this.applyPose(initialPose)
1327
-
1328
- // Reset bones without time 0 keyframes
1329
- const skeleton = this.currentModel.getSkeleton()
1330
- const bonesWithPose = new Set(initialPose.boneRotations.keys())
1331
- const bonesToReset: string[] = []
1332
- for (const bone of skeleton.bones) {
1333
- if (!bonesWithPose.has(bone.name)) {
1334
- bonesToReset.push(bone.name)
1335
- }
1336
- }
1337
-
1338
- if (bonesToReset.length > 0) {
1339
- const identityQuat = new Quat(0, 0, 0, 1)
1340
- const identityQuats = new Array(bonesToReset.length).fill(identityQuat)
1341
- this.rotateBones(bonesToReset, identityQuats, 0)
1342
- }
1343
-
1344
- // Reset physics immediately and upload matrices to prevent A-pose flash
1345
- if (this.physics) {
1346
- this.currentModel.evaluatePose()
1347
-
1348
- const worldMats = this.currentModel.getBoneWorldMatrices()
1349
- this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
1350
-
1351
- // Upload matrices immediately so next frame shows correct pose
1352
- this.device.queue.writeBuffer(
1353
- this.worldMatrixBuffer!,
1354
- 0,
1355
- worldMats.buffer,
1356
- worldMats.byteOffset,
1357
- worldMats.byteLength
1358
- )
1359
- const encoder = this.device.createCommandEncoder()
1360
- this.computeSkinMatrices(encoder)
1361
- this.device.queue.submit([encoder.finish()])
1362
- }
1363
- }
1364
-
1365
- // Start playback (or resume if paused)
1366
- this.player.play()
1088
+ this.currentModel?.playAnimation()
1367
1089
  }
1368
1090
 
1369
1091
  public stopAnimation() {
1370
- this.player.stop()
1092
+ this.currentModel?.stopAnimation()
1371
1093
  }
1372
1094
 
1373
1095
  public pauseAnimation() {
1374
- this.player.pause()
1096
+ this.currentModel?.pauseAnimation()
1375
1097
  }
1376
1098
 
1377
1099
  public seekAnimation(time: number) {
1378
- if (!this.currentModel || !this.hasAnimation) return
1379
-
1380
- this.player.seek(time)
1381
-
1382
- // Immediately apply pose at seeked time
1383
- const pose = this.player.getPoseAtTime(time)
1384
- this.applyPose(pose)
1385
-
1386
- // Update model pose and physics
1387
- this.currentModel.evaluatePose()
1388
-
1389
- if (this.physics) {
1390
- const worldMats = this.currentModel.getBoneWorldMatrices()
1391
- this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
1392
-
1393
- // Upload matrices immediately
1394
- this.device.queue.writeBuffer(
1395
- this.worldMatrixBuffer!,
1396
- 0,
1397
- worldMats.buffer,
1398
- worldMats.byteOffset,
1399
- worldMats.byteLength
1400
- )
1401
- const encoder = this.device.createCommandEncoder()
1402
- this.computeSkinMatrices(encoder)
1403
- this.device.queue.submit([encoder.finish()])
1404
- }
1100
+ this.currentModel?.seekAnimation(time)
1405
1101
  }
1406
1102
 
1407
1103
  public getAnimationProgress() {
1408
- return this.player.getProgress()
1409
- }
1410
-
1411
- /**
1412
- * Apply animation pose to model
1413
- */
1414
- private applyPose(pose: AnimationPose): void {
1415
- if (!this.currentModel) return
1416
-
1417
- // Apply bone rotations
1418
- if (pose.boneRotations.size > 0) {
1419
- const boneNames = Array.from(pose.boneRotations.keys())
1420
- const rotations = Array.from(pose.boneRotations.values())
1421
- this.rotateBones(boneNames, rotations, 0)
1422
- }
1423
-
1424
- // Apply bone translations
1425
- if (pose.boneTranslations.size > 0) {
1426
- const boneNames = Array.from(pose.boneTranslations.keys())
1427
- const translations = Array.from(pose.boneTranslations.values())
1428
- this.moveBones(boneNames, translations, 0)
1429
- }
1430
-
1431
- // Apply morph weights
1432
- for (const [morphName, weight] of pose.morphWeights.entries()) {
1433
- this.setMorphWeight(morphName, weight, 0)
1434
- }
1104
+ return this.currentModel?.getAnimationProgress() ?? { current: 0, duration: 0, percentage: 0 }
1435
1105
  }
1436
1106
 
1437
1107
  public getStats(): EngineStats {
@@ -1480,8 +1150,6 @@ export class Engine {
1480
1150
  this.modelDir = dir
1481
1151
 
1482
1152
  const model = await PmxLoader.load(path)
1483
-
1484
- this.physics = new Physics(model.getRigidbodies(), model.getJoints())
1485
1153
  await this.setupModelBuffers(model)
1486
1154
  }
1487
1155
 
@@ -1555,13 +1223,7 @@ export class Engine {
1555
1223
  this.skinMatrixBuffer = this.device.createBuffer({
1556
1224
  label: "skin matrices",
1557
1225
  size: Math.max(256, matrixSize),
1558
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
1559
- })
1560
-
1561
- this.worldMatrixBuffer = this.device.createBuffer({
1562
- label: "world matrices",
1563
- size: Math.max(256, matrixSize),
1564
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1226
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1565
1227
  })
1566
1228
 
1567
1229
  this.inverseBindMatrixBuffer = this.device.createBuffer({
@@ -1579,28 +1241,6 @@ export class Engine {
1579
1241
  invBindMatrices.byteLength
1580
1242
  )
1581
1243
 
1582
- this.boneCountBuffer = this.device.createBuffer({
1583
- label: "bone count uniform",
1584
- size: 32, // Minimum uniform buffer size is 32 bytes
1585
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1586
- })
1587
- const boneCountData = new Uint32Array(8) // 32 bytes total
1588
- boneCountData[0] = boneCount
1589
- this.device.queue.writeBuffer(this.boneCountBuffer, 0, boneCountData)
1590
-
1591
- this.createSkinMatrixComputePipeline()
1592
-
1593
- // Create compute bind group once (reused every frame)
1594
- this.skinMatrixComputeBindGroup = this.device.createBindGroup({
1595
- layout: this.skinMatrixComputePipeline!.getBindGroupLayout(0),
1596
- entries: [
1597
- { binding: 0, resource: { buffer: this.boneCountBuffer } },
1598
- { binding: 1, resource: { buffer: this.worldMatrixBuffer } },
1599
- { binding: 2, resource: { buffer: this.inverseBindMatrixBuffer } },
1600
- { binding: 3, resource: { buffer: this.skinMatrixBuffer } },
1601
- ],
1602
- })
1603
-
1604
1244
  const indices = model.getIndices()
1605
1245
  if (indices) {
1606
1246
  this.indexBuffer = this.device.createBuffer({
@@ -1634,15 +1274,7 @@ export class Engine {
1634
1274
  return texture
1635
1275
  }
1636
1276
 
1637
- this.opaqueDraws = []
1638
- this.eyeDraws = []
1639
- this.hairDrawsOverEyes = []
1640
- this.hairDrawsOverNonEyes = []
1641
- this.transparentDraws = []
1642
- this.opaqueOutlineDraws = []
1643
- this.eyeOutlineDraws = []
1644
- this.hairOutlineDraws = []
1645
- this.transparentOutlineDraws = []
1277
+ this.drawCalls = []
1646
1278
  let currentIndexOffset = 0
1647
1279
 
1648
1280
  for (const mat of materials) {
@@ -1671,56 +1303,52 @@ export class Engine {
1671
1303
  ],
1672
1304
  })
1673
1305
 
1674
- const addDrawCall = (draws: DrawCall[]) => {
1675
- if (indexCount > 0) {
1676
- draws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup })
1677
- }
1678
- }
1679
-
1680
- if (mat.isEye) {
1681
- addDrawCall(this.eyeDraws)
1682
- } else if (mat.isHair) {
1683
- // Hair materials: create separate bind groups for over-eyes vs over-non-eyes
1684
- const createHairBindGroup = (isOverEyes: boolean) => {
1685
- const buffer = this.createMaterialUniformBuffer(
1686
- `${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
1687
- materialAlpha,
1688
- isOverEyes ? 1.0 : 0.0
1689
- )
1690
-
1691
- return this.device.createBindGroup({
1692
- label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
1693
- layout: this.mainBindGroupLayout,
1694
- entries: [
1695
- { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1696
- { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1697
- { binding: 2, resource: diffuseTexture.createView() },
1698
- { binding: 3, resource: this.materialSampler },
1699
- { binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
1700
- { binding: 5, resource: { buffer: buffer } },
1701
- ],
1702
- })
1703
- }
1306
+ if (indexCount > 0) {
1307
+ if (mat.isEye) {
1308
+ this.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup })
1309
+ } else if (mat.isHair) {
1310
+ // Hair materials: create separate bind groups for over-eyes vs over-non-eyes
1311
+ const createHairBindGroup = (isOverEyes: boolean) => {
1312
+ const buffer = this.createMaterialUniformBuffer(
1313
+ `${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
1314
+ materialAlpha,
1315
+ isOverEyes ? 1.0 : 0.0
1316
+ )
1317
+
1318
+ return this.device.createBindGroup({
1319
+ label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
1320
+ layout: this.mainBindGroupLayout,
1321
+ entries: [
1322
+ { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1323
+ { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1324
+ { binding: 2, resource: diffuseTexture.createView() },
1325
+ { binding: 3, resource: this.materialSampler },
1326
+ { binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
1327
+ { binding: 5, resource: { buffer: buffer } },
1328
+ ],
1329
+ })
1330
+ }
1704
1331
 
1705
- const bindGroupOverEyes = createHairBindGroup(true)
1706
- const bindGroupOverNonEyes = createHairBindGroup(false)
1332
+ const bindGroupOverEyes = createHairBindGroup(true)
1333
+ const bindGroupOverNonEyes = createHairBindGroup(false)
1707
1334
 
1708
- if (indexCount > 0) {
1709
- this.hairDrawsOverEyes.push({
1335
+ this.drawCalls.push({
1336
+ type: "hair-over-eyes",
1710
1337
  count: indexCount,
1711
1338
  firstIndex: currentIndexOffset,
1712
1339
  bindGroup: bindGroupOverEyes,
1713
1340
  })
1714
- this.hairDrawsOverNonEyes.push({
1341
+ this.drawCalls.push({
1342
+ type: "hair-over-non-eyes",
1715
1343
  count: indexCount,
1716
1344
  firstIndex: currentIndexOffset,
1717
1345
  bindGroup: bindGroupOverNonEyes,
1718
1346
  })
1347
+ } else if (isTransparent) {
1348
+ this.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup })
1349
+ } else {
1350
+ this.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup })
1719
1351
  }
1720
- } else if (isTransparent) {
1721
- addDrawCall(this.transparentDraws)
1722
- } else {
1723
- addDrawCall(this.opaqueDraws)
1724
1352
  }
1725
1353
 
1726
1354
  // Edge flag is at bit 4 (0x10) in PMX format
@@ -1751,14 +1379,19 @@ export class Engine {
1751
1379
  })
1752
1380
 
1753
1381
  if (indexCount > 0) {
1754
- const outlineDraws = mat.isEye
1755
- ? this.eyeOutlineDraws
1382
+ const outlineType: DrawCallType = mat.isEye
1383
+ ? "eye-outline"
1756
1384
  : mat.isHair
1757
- ? this.hairOutlineDraws
1385
+ ? "hair-outline"
1758
1386
  : isTransparent
1759
- ? this.transparentOutlineDraws
1760
- : this.opaqueOutlineDraws
1761
- outlineDraws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup })
1387
+ ? "transparent-outline"
1388
+ : "opaque-outline"
1389
+ this.drawCalls.push({
1390
+ type: outlineType,
1391
+ count: indexCount,
1392
+ firstIndex: currentIndexOffset,
1393
+ bindGroup: outlineBindGroup,
1394
+ })
1762
1395
  }
1763
1396
  }
1764
1397
 
@@ -1820,51 +1453,54 @@ export class Engine {
1820
1453
  private renderEyes(pass: GPURenderPassEncoder) {
1821
1454
  pass.setPipeline(this.eyePipeline)
1822
1455
  pass.setStencilReference(this.STENCIL_EYE_VALUE)
1823
- for (const draw of this.eyeDraws) {
1824
- pass.setBindGroup(0, draw.bindGroup)
1825
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1456
+ for (const draw of this.drawCalls) {
1457
+ if (draw.type === "eye") {
1458
+ pass.setBindGroup(0, draw.bindGroup)
1459
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1460
+ }
1826
1461
  }
1827
1462
  }
1828
1463
 
1829
1464
  // Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
1830
1465
  private renderHair(pass: GPURenderPassEncoder) {
1831
1466
  // Hair depth pre-pass (reduces overdraw via early depth rejection)
1832
- const hasHair = this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0
1467
+ const hasHair = this.drawCalls.some((d) => d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes")
1833
1468
  if (hasHair) {
1834
1469
  pass.setPipeline(this.hairDepthPipeline)
1835
- for (const draw of this.hairDrawsOverEyes) {
1836
- pass.setBindGroup(0, draw.bindGroup)
1837
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1838
- }
1839
- for (const draw of this.hairDrawsOverNonEyes) {
1840
- pass.setBindGroup(0, draw.bindGroup)
1841
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1470
+ for (const draw of this.drawCalls) {
1471
+ if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
1472
+ pass.setBindGroup(0, draw.bindGroup)
1473
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1474
+ }
1842
1475
  }
1843
1476
  }
1844
1477
 
1845
1478
  // Hair shading (split by stencil for transparency over eyes)
1846
- if (this.hairDrawsOverEyes.length > 0) {
1479
+ const hairOverEyes = this.drawCalls.filter((d) => d.type === "hair-over-eyes")
1480
+ if (hairOverEyes.length > 0) {
1847
1481
  pass.setPipeline(this.hairPipelineOverEyes)
1848
1482
  pass.setStencilReference(this.STENCIL_EYE_VALUE)
1849
- for (const draw of this.hairDrawsOverEyes) {
1483
+ for (const draw of hairOverEyes) {
1850
1484
  pass.setBindGroup(0, draw.bindGroup)
1851
1485
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1852
1486
  }
1853
1487
  }
1854
1488
 
1855
- if (this.hairDrawsOverNonEyes.length > 0) {
1489
+ const hairOverNonEyes = this.drawCalls.filter((d) => d.type === "hair-over-non-eyes")
1490
+ if (hairOverNonEyes.length > 0) {
1856
1491
  pass.setPipeline(this.hairPipelineOverNonEyes)
1857
1492
  pass.setStencilReference(this.STENCIL_EYE_VALUE)
1858
- for (const draw of this.hairDrawsOverNonEyes) {
1493
+ for (const draw of hairOverNonEyes) {
1859
1494
  pass.setBindGroup(0, draw.bindGroup)
1860
1495
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1861
1496
  }
1862
1497
  }
1863
1498
 
1864
1499
  // Hair outlines
1865
- if (this.hairOutlineDraws.length > 0) {
1500
+ const hairOutlines = this.drawCalls.filter((d) => d.type === "hair-outline")
1501
+ if (hairOutlines.length > 0) {
1866
1502
  pass.setPipeline(this.hairOutlinePipeline)
1867
- for (const draw of this.hairOutlineDraws) {
1503
+ for (const draw of hairOutlines) {
1868
1504
  pass.setBindGroup(0, draw.bindGroup)
1869
1505
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1870
1506
  }
@@ -1881,19 +1517,10 @@ export class Engine {
1881
1517
  this.updateCameraUniforms()
1882
1518
  this.updateRenderTarget()
1883
1519
 
1884
- // Animate VMD animation if playing
1885
- if (this.hasAnimation && this.currentModel) {
1886
- const pose = this.player.update(currentTime)
1887
- if (pose) {
1888
- this.applyPose(pose)
1889
- }
1890
- }
1891
-
1892
- // Update model pose first (this may update morph weights via tweens)
1893
- // We need to do this before creating the encoder to ensure vertex buffer is ready
1520
+ // Update model (handles tweens, animation, physics, IK, and skin matrices)
1894
1521
  if (this.currentModel) {
1895
- const hasActiveMorphTweens = this.currentModel.evaluatePose()
1896
- if (hasActiveMorphTweens) {
1522
+ const verticesChanged = this.currentModel.update(deltaTime)
1523
+ if (verticesChanged) {
1897
1524
  this.vertexBufferNeedsUpdate = true
1898
1525
  }
1899
1526
  }
@@ -1904,10 +1531,11 @@ export class Engine {
1904
1531
  this.vertexBufferNeedsUpdate = false
1905
1532
  }
1906
1533
 
1907
- // Use single encoder for both compute and render (reduces sync points)
1908
- const encoder = this.device.createCommandEncoder()
1534
+ // Update skin matrices buffer
1535
+ this.updateSkinMatrices()
1909
1536
 
1910
- this.updateModelPose(deltaTime, encoder)
1537
+ // Use single encoder for render
1538
+ const encoder = this.device.createCommandEncoder()
1911
1539
 
1912
1540
  const pass = encoder.beginRenderPass(this.renderPassDescriptor)
1913
1541
 
@@ -1919,9 +1547,11 @@ export class Engine {
1919
1547
 
1920
1548
  // Pass 1: Opaque
1921
1549
  pass.setPipeline(this.modelPipeline)
1922
- for (const draw of this.opaqueDraws) {
1923
- pass.setBindGroup(0, draw.bindGroup)
1924
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1550
+ for (const draw of this.drawCalls) {
1551
+ if (draw.type === "opaque") {
1552
+ pass.setBindGroup(0, draw.bindGroup)
1553
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1554
+ }
1925
1555
  }
1926
1556
 
1927
1557
  // Pass 2: Eyes (writes stencil value for hair to test against)
@@ -1934,9 +1564,11 @@ export class Engine {
1934
1564
 
1935
1565
  // Pass 4: Transparent
1936
1566
  pass.setPipeline(this.modelPipeline)
1937
- for (const draw of this.transparentDraws) {
1938
- pass.setBindGroup(0, draw.bindGroup)
1939
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1567
+ for (const draw of this.drawCalls) {
1568
+ if (draw.type === "transparent") {
1569
+ pass.setBindGroup(0, draw.bindGroup)
1570
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1571
+ }
1940
1572
  }
1941
1573
 
1942
1574
  this.drawOutlines(pass, true)
@@ -2072,42 +1704,27 @@ export class Engine {
2072
1704
  }
2073
1705
  }
2074
1706
 
2075
- private updateModelPose(deltaTime: number, encoder: GPUCommandEncoder) {
2076
- // Note: evaluatePose is called earlier in render() to update vertex buffer before encoder creation
2077
- // Here we just get the matrices and update physics/compute
2078
- const worldMats = this.currentModel!.getBoneWorldMatrices()
2079
-
2080
- if (this.physics) {
2081
- this.physics.step(deltaTime, worldMats, this.currentModel!.getBoneInverseBindMatrices())
2082
- }
1707
+ private updateSkinMatrices() {
1708
+ if (!this.currentModel || !this.skinMatrixBuffer) return
2083
1709
 
1710
+ const skinMatrices = this.currentModel.getSkinMatrices()
2084
1711
  this.device.queue.writeBuffer(
2085
- this.worldMatrixBuffer!,
1712
+ this.skinMatrixBuffer,
2086
1713
  0,
2087
- worldMats.buffer,
2088
- worldMats.byteOffset,
2089
- worldMats.byteLength
1714
+ skinMatrices.buffer,
1715
+ skinMatrices.byteOffset,
1716
+ skinMatrices.byteLength
2090
1717
  )
2091
- this.computeSkinMatrices(encoder)
2092
- }
2093
-
2094
- private computeSkinMatrices(encoder: GPUCommandEncoder) {
2095
- const boneCount = this.currentModel!.getSkeleton().bones.length
2096
- const workgroupCount = Math.ceil(boneCount / this.COMPUTE_WORKGROUP_SIZE)
2097
-
2098
- const pass = encoder.beginComputePass()
2099
- pass.setPipeline(this.skinMatrixComputePipeline!)
2100
- pass.setBindGroup(0, this.skinMatrixComputeBindGroup!)
2101
- pass.dispatchWorkgroups(workgroupCount)
2102
- pass.end()
2103
1718
  }
2104
1719
 
2105
1720
  private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
2106
1721
  pass.setPipeline(this.outlinePipeline)
2107
- const draws = transparent ? this.transparentOutlineDraws : this.opaqueOutlineDraws
2108
- for (const draw of draws) {
2109
- pass.setBindGroup(0, draw.bindGroup)
2110
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1722
+ const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
1723
+ for (const draw of this.drawCalls) {
1724
+ if (draw.type === outlineType) {
1725
+ pass.setBindGroup(0, draw.bindGroup)
1726
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1727
+ }
2111
1728
  }
2112
1729
  }
2113
1730