reze-engine 0.3.5 → 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/README.md +0 -1
- package/dist/engine.d.ts +10 -26
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +261 -670
- package/dist/engine_r.d.ts +132 -0
- package/dist/engine_r.d.ts.map +1 -0
- package/dist/engine_r.js +1489 -0
- package/dist/engine_ts.d.ts +143 -0
- package/dist/engine_ts.d.ts.map +1 -0
- package/dist/engine_ts.js +1575 -0
- package/dist/ik-solver.d.ts +6 -0
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +98 -101
- package/dist/math.d.ts +0 -5
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +0 -55
- package/dist/model.d.ts +59 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +415 -141
- package/dist/player.d.ts +6 -20
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +88 -191
- package/package.json +1 -1
- package/src/engine.ts +299 -729
- package/src/ik-solver.ts +106 -124
- package/src/math.ts +0 -74
- package/src/model.ts +516 -186
- package/src/player.ts +115 -210
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
|
-
//
|
|
114
|
-
private
|
|
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,10 +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
|
-
private animationStartTime: number = 0 // Track when animation first started (for A-pose prevention)
|
|
139
|
-
|
|
140
132
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
141
133
|
this.canvas = canvas
|
|
142
134
|
if (options) {
|
|
@@ -178,6 +170,37 @@ export class Engine {
|
|
|
178
170
|
this.setupResize()
|
|
179
171
|
}
|
|
180
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
|
+
|
|
181
204
|
private createPipelines() {
|
|
182
205
|
this.materialSampler = this.device.createSampler({
|
|
183
206
|
magFilter: "linear",
|
|
@@ -186,6 +209,78 @@ export class Engine {
|
|
|
186
209
|
addressModeV: "repeat",
|
|
187
210
|
})
|
|
188
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
|
+
|
|
189
284
|
const shaderModule = this.device.createShaderModule({
|
|
190
285
|
label: "model shaders",
|
|
191
286
|
code: /* wgsl */ `
|
|
@@ -302,59 +397,18 @@ export class Engine {
|
|
|
302
397
|
bindGroupLayouts: [this.mainBindGroupLayout],
|
|
303
398
|
})
|
|
304
399
|
|
|
305
|
-
this.modelPipeline = this.
|
|
400
|
+
this.modelPipeline = this.createRenderPipeline({
|
|
306
401
|
label: "model pipeline",
|
|
307
402
|
layout: mainPipelineLayout,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
arrayStride: 8 * 4,
|
|
313
|
-
attributes: [
|
|
314
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
315
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
316
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
317
|
-
],
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
arrayStride: 4 * 2,
|
|
321
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
arrayStride: 4,
|
|
325
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
326
|
-
},
|
|
327
|
-
],
|
|
328
|
-
},
|
|
329
|
-
fragment: {
|
|
330
|
-
module: shaderModule,
|
|
331
|
-
targets: [
|
|
332
|
-
{
|
|
333
|
-
format: this.presentationFormat,
|
|
334
|
-
blend: {
|
|
335
|
-
color: {
|
|
336
|
-
srcFactor: "src-alpha",
|
|
337
|
-
dstFactor: "one-minus-src-alpha",
|
|
338
|
-
operation: "add",
|
|
339
|
-
},
|
|
340
|
-
alpha: {
|
|
341
|
-
srcFactor: "one",
|
|
342
|
-
dstFactor: "one-minus-src-alpha",
|
|
343
|
-
operation: "add",
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
],
|
|
348
|
-
},
|
|
349
|
-
primitive: { cullMode: "none" },
|
|
403
|
+
shaderModule,
|
|
404
|
+
vertexBuffers: fullVertexBuffers,
|
|
405
|
+
fragmentTarget: standardBlend,
|
|
406
|
+
cullMode: "none",
|
|
350
407
|
depthStencil: {
|
|
351
408
|
format: "depth24plus-stencil8",
|
|
352
409
|
depthWriteEnabled: true,
|
|
353
410
|
depthCompare: "less-equal",
|
|
354
411
|
},
|
|
355
|
-
multisample: {
|
|
356
|
-
count: this.sampleCount,
|
|
357
|
-
},
|
|
358
412
|
})
|
|
359
413
|
|
|
360
414
|
// Create bind group layout for outline pipelines
|
|
@@ -444,196 +498,58 @@ export class Engine {
|
|
|
444
498
|
`,
|
|
445
499
|
})
|
|
446
500
|
|
|
447
|
-
this.outlinePipeline = this.
|
|
501
|
+
this.outlinePipeline = this.createRenderPipeline({
|
|
448
502
|
label: "outline pipeline",
|
|
449
503
|
layout: outlinePipelineLayout,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
arrayStride: 8 * 4,
|
|
455
|
-
attributes: [
|
|
456
|
-
{
|
|
457
|
-
shaderLocation: 0,
|
|
458
|
-
offset: 0,
|
|
459
|
-
format: "float32x3" as GPUVertexFormat,
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
shaderLocation: 1,
|
|
463
|
-
offset: 3 * 4,
|
|
464
|
-
format: "float32x3" as GPUVertexFormat,
|
|
465
|
-
},
|
|
466
|
-
],
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
arrayStride: 4 * 2,
|
|
470
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
471
|
-
},
|
|
472
|
-
{
|
|
473
|
-
arrayStride: 4,
|
|
474
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
475
|
-
},
|
|
476
|
-
],
|
|
477
|
-
},
|
|
478
|
-
fragment: {
|
|
479
|
-
module: outlineShaderModule,
|
|
480
|
-
targets: [
|
|
481
|
-
{
|
|
482
|
-
format: this.presentationFormat,
|
|
483
|
-
blend: {
|
|
484
|
-
color: {
|
|
485
|
-
srcFactor: "src-alpha",
|
|
486
|
-
dstFactor: "one-minus-src-alpha",
|
|
487
|
-
operation: "add",
|
|
488
|
-
},
|
|
489
|
-
alpha: {
|
|
490
|
-
srcFactor: "one",
|
|
491
|
-
dstFactor: "one-minus-src-alpha",
|
|
492
|
-
operation: "add",
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
],
|
|
497
|
-
},
|
|
498
|
-
primitive: {
|
|
499
|
-
cullMode: "back",
|
|
500
|
-
},
|
|
504
|
+
shaderModule: outlineShaderModule,
|
|
505
|
+
vertexBuffers: outlineVertexBuffers,
|
|
506
|
+
fragmentTarget: standardBlend,
|
|
507
|
+
cullMode: "back",
|
|
501
508
|
depthStencil: {
|
|
502
509
|
format: "depth24plus-stencil8",
|
|
503
510
|
depthWriteEnabled: true,
|
|
504
511
|
depthCompare: "less-equal",
|
|
505
512
|
},
|
|
506
|
-
multisample: {
|
|
507
|
-
count: this.sampleCount,
|
|
508
|
-
},
|
|
509
513
|
})
|
|
510
514
|
|
|
511
515
|
// Hair outline pipeline
|
|
512
|
-
this.hairOutlinePipeline = this.
|
|
516
|
+
this.hairOutlinePipeline = this.createRenderPipeline({
|
|
513
517
|
label: "hair outline pipeline",
|
|
514
518
|
layout: outlinePipelineLayout,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
arrayStride: 8 * 4,
|
|
520
|
-
attributes: [
|
|
521
|
-
{
|
|
522
|
-
shaderLocation: 0,
|
|
523
|
-
offset: 0,
|
|
524
|
-
format: "float32x3" as GPUVertexFormat,
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
shaderLocation: 1,
|
|
528
|
-
offset: 3 * 4,
|
|
529
|
-
format: "float32x3" as GPUVertexFormat,
|
|
530
|
-
},
|
|
531
|
-
],
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
arrayStride: 4 * 2,
|
|
535
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
arrayStride: 4,
|
|
539
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
540
|
-
},
|
|
541
|
-
],
|
|
542
|
-
},
|
|
543
|
-
fragment: {
|
|
544
|
-
module: outlineShaderModule,
|
|
545
|
-
targets: [
|
|
546
|
-
{
|
|
547
|
-
format: this.presentationFormat,
|
|
548
|
-
blend: {
|
|
549
|
-
color: {
|
|
550
|
-
srcFactor: "src-alpha",
|
|
551
|
-
dstFactor: "one-minus-src-alpha",
|
|
552
|
-
operation: "add",
|
|
553
|
-
},
|
|
554
|
-
alpha: {
|
|
555
|
-
srcFactor: "one",
|
|
556
|
-
dstFactor: "one-minus-src-alpha",
|
|
557
|
-
operation: "add",
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
},
|
|
563
|
-
primitive: {
|
|
564
|
-
cullMode: "back",
|
|
565
|
-
},
|
|
519
|
+
shaderModule: outlineShaderModule,
|
|
520
|
+
vertexBuffers: outlineVertexBuffers,
|
|
521
|
+
fragmentTarget: standardBlend,
|
|
522
|
+
cullMode: "back",
|
|
566
523
|
depthStencil: {
|
|
567
524
|
format: "depth24plus-stencil8",
|
|
568
|
-
depthWriteEnabled: false,
|
|
569
|
-
depthCompare: "less-equal",
|
|
570
|
-
depthBias: -0.0001,
|
|
525
|
+
depthWriteEnabled: false,
|
|
526
|
+
depthCompare: "less-equal",
|
|
527
|
+
depthBias: -0.0001,
|
|
571
528
|
depthBiasSlopeScale: 0.0,
|
|
572
529
|
depthBiasClamp: 0.0,
|
|
573
530
|
},
|
|
574
|
-
multisample: {
|
|
575
|
-
count: this.sampleCount,
|
|
576
|
-
},
|
|
577
531
|
})
|
|
578
532
|
|
|
579
533
|
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
580
|
-
this.eyePipeline = this.
|
|
534
|
+
this.eyePipeline = this.createRenderPipeline({
|
|
581
535
|
label: "eye overlay pipeline",
|
|
582
536
|
layout: mainPipelineLayout,
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
arrayStride: 8 * 4,
|
|
588
|
-
attributes: [
|
|
589
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
590
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
591
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
592
|
-
],
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
arrayStride: 4 * 2,
|
|
596
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
597
|
-
},
|
|
598
|
-
{
|
|
599
|
-
arrayStride: 4,
|
|
600
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
601
|
-
},
|
|
602
|
-
],
|
|
603
|
-
},
|
|
604
|
-
fragment: {
|
|
605
|
-
module: shaderModule,
|
|
606
|
-
targets: [
|
|
607
|
-
{
|
|
608
|
-
format: this.presentationFormat,
|
|
609
|
-
blend: {
|
|
610
|
-
color: {
|
|
611
|
-
srcFactor: "src-alpha",
|
|
612
|
-
dstFactor: "one-minus-src-alpha",
|
|
613
|
-
operation: "add",
|
|
614
|
-
},
|
|
615
|
-
alpha: {
|
|
616
|
-
srcFactor: "one",
|
|
617
|
-
dstFactor: "one-minus-src-alpha",
|
|
618
|
-
operation: "add",
|
|
619
|
-
},
|
|
620
|
-
},
|
|
621
|
-
},
|
|
622
|
-
],
|
|
623
|
-
},
|
|
624
|
-
primitive: { cullMode: "front" },
|
|
537
|
+
shaderModule,
|
|
538
|
+
vertexBuffers: fullVertexBuffers,
|
|
539
|
+
fragmentTarget: standardBlend,
|
|
540
|
+
cullMode: "front",
|
|
625
541
|
depthStencil: {
|
|
626
542
|
format: "depth24plus-stencil8",
|
|
627
|
-
depthWriteEnabled: true,
|
|
628
|
-
depthCompare: "less-equal",
|
|
629
|
-
depthBias: -0.00005,
|
|
543
|
+
depthWriteEnabled: true,
|
|
544
|
+
depthCompare: "less-equal",
|
|
545
|
+
depthBias: -0.00005,
|
|
630
546
|
depthBiasSlopeScale: 0.0,
|
|
631
547
|
depthBiasClamp: 0.0,
|
|
632
548
|
stencilFront: {
|
|
633
549
|
compare: "always",
|
|
634
550
|
failOp: "keep",
|
|
635
551
|
depthFailOp: "keep",
|
|
636
|
-
passOp: "replace",
|
|
552
|
+
passOp: "replace",
|
|
637
553
|
},
|
|
638
554
|
stencilBack: {
|
|
639
555
|
compare: "always",
|
|
@@ -642,7 +558,6 @@ export class Engine {
|
|
|
642
558
|
passOp: "replace",
|
|
643
559
|
},
|
|
644
560
|
},
|
|
645
|
-
multisample: { count: this.sampleCount },
|
|
646
561
|
})
|
|
647
562
|
|
|
648
563
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
@@ -691,104 +606,42 @@ export class Engine {
|
|
|
691
606
|
})
|
|
692
607
|
|
|
693
608
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
694
|
-
this.hairDepthPipeline = this.
|
|
609
|
+
this.hairDepthPipeline = this.createRenderPipeline({
|
|
695
610
|
label: "hair depth pre-pass",
|
|
696
611
|
layout: mainPipelineLayout,
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
attributes: [
|
|
703
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
704
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
705
|
-
],
|
|
706
|
-
},
|
|
707
|
-
{
|
|
708
|
-
arrayStride: 4 * 2,
|
|
709
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
710
|
-
},
|
|
711
|
-
{
|
|
712
|
-
arrayStride: 4,
|
|
713
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
714
|
-
},
|
|
715
|
-
],
|
|
716
|
-
},
|
|
717
|
-
fragment: {
|
|
718
|
-
module: depthOnlyShaderModule,
|
|
719
|
-
entryPoint: "fs",
|
|
720
|
-
targets: [
|
|
721
|
-
{
|
|
722
|
-
format: this.presentationFormat,
|
|
723
|
-
writeMask: 0, // Disable all color writes - we only care about depth
|
|
724
|
-
},
|
|
725
|
-
],
|
|
612
|
+
shaderModule: depthOnlyShaderModule,
|
|
613
|
+
vertexBuffers: depthOnlyVertexBuffers,
|
|
614
|
+
fragmentTarget: {
|
|
615
|
+
format: this.presentationFormat,
|
|
616
|
+
writeMask: 0,
|
|
726
617
|
},
|
|
727
|
-
|
|
618
|
+
fragmentEntryPoint: "fs",
|
|
619
|
+
cullMode: "front",
|
|
728
620
|
depthStencil: {
|
|
729
621
|
format: "depth24plus-stencil8",
|
|
730
622
|
depthWriteEnabled: true,
|
|
731
|
-
depthCompare: "less-equal",
|
|
623
|
+
depthCompare: "less-equal",
|
|
732
624
|
depthBias: 0.0,
|
|
733
625
|
depthBiasSlopeScale: 0.0,
|
|
734
626
|
depthBiasClamp: 0.0,
|
|
735
627
|
},
|
|
736
|
-
multisample: { count: this.sampleCount },
|
|
737
628
|
})
|
|
738
629
|
|
|
739
630
|
// Hair pipelines for rendering over eyes vs non-eyes (only differ in stencil compare mode)
|
|
740
631
|
const createHairPipeline = (isOverEyes: boolean): GPURenderPipeline => {
|
|
741
|
-
return this.
|
|
632
|
+
return this.createRenderPipeline({
|
|
742
633
|
label: `hair pipeline (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
743
634
|
layout: mainPipelineLayout,
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
arrayStride: 8 * 4,
|
|
749
|
-
attributes: [
|
|
750
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
751
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
752
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
753
|
-
],
|
|
754
|
-
},
|
|
755
|
-
{
|
|
756
|
-
arrayStride: 4 * 2,
|
|
757
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
arrayStride: 4,
|
|
761
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
762
|
-
},
|
|
763
|
-
],
|
|
764
|
-
},
|
|
765
|
-
fragment: {
|
|
766
|
-
module: shaderModule,
|
|
767
|
-
targets: [
|
|
768
|
-
{
|
|
769
|
-
format: this.presentationFormat,
|
|
770
|
-
blend: {
|
|
771
|
-
color: {
|
|
772
|
-
srcFactor: "src-alpha",
|
|
773
|
-
dstFactor: "one-minus-src-alpha",
|
|
774
|
-
operation: "add",
|
|
775
|
-
},
|
|
776
|
-
alpha: {
|
|
777
|
-
srcFactor: "one",
|
|
778
|
-
dstFactor: "one-minus-src-alpha",
|
|
779
|
-
operation: "add",
|
|
780
|
-
},
|
|
781
|
-
},
|
|
782
|
-
},
|
|
783
|
-
],
|
|
784
|
-
},
|
|
785
|
-
primitive: { cullMode: "front" },
|
|
635
|
+
shaderModule,
|
|
636
|
+
vertexBuffers: fullVertexBuffers,
|
|
637
|
+
fragmentTarget: standardBlend,
|
|
638
|
+
cullMode: "front",
|
|
786
639
|
depthStencil: {
|
|
787
640
|
format: "depth24plus-stencil8",
|
|
788
|
-
depthWriteEnabled: false,
|
|
789
|
-
depthCompare: "less-equal",
|
|
641
|
+
depthWriteEnabled: false,
|
|
642
|
+
depthCompare: "less-equal",
|
|
790
643
|
stencilFront: {
|
|
791
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
644
|
+
compare: isOverEyes ? "equal" : "not-equal",
|
|
792
645
|
failOp: "keep",
|
|
793
646
|
depthFailOp: "keep",
|
|
794
647
|
passOp: "keep",
|
|
@@ -800,7 +653,6 @@ export class Engine {
|
|
|
800
653
|
passOp: "keep",
|
|
801
654
|
},
|
|
802
655
|
},
|
|
803
|
-
multisample: { count: this.sampleCount },
|
|
804
656
|
})
|
|
805
657
|
}
|
|
806
658
|
|
|
@@ -808,46 +660,6 @@ export class Engine {
|
|
|
808
660
|
this.hairPipelineOverNonEyes = createHairPipeline(false)
|
|
809
661
|
}
|
|
810
662
|
|
|
811
|
-
// Create compute shader for skin matrix computation
|
|
812
|
-
private createSkinMatrixComputePipeline() {
|
|
813
|
-
const computeShader = this.device.createShaderModule({
|
|
814
|
-
label: "skin matrix compute",
|
|
815
|
-
code: /* wgsl */ `
|
|
816
|
-
struct BoneCountUniform {
|
|
817
|
-
count: u32,
|
|
818
|
-
_padding1: u32,
|
|
819
|
-
_padding2: u32,
|
|
820
|
-
_padding3: u32,
|
|
821
|
-
_padding4: vec4<u32>,
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
825
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
826
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
827
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
828
|
-
|
|
829
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
830
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
831
|
-
let boneIndex = globalId.x;
|
|
832
|
-
if (boneIndex >= boneCount.count) {
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
let worldMat = worldMatrices[boneIndex];
|
|
836
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
837
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
838
|
-
}
|
|
839
|
-
`,
|
|
840
|
-
})
|
|
841
|
-
|
|
842
|
-
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
843
|
-
label: "skin matrix compute pipeline",
|
|
844
|
-
layout: "auto",
|
|
845
|
-
compute: {
|
|
846
|
-
module: computeShader,
|
|
847
|
-
},
|
|
848
|
-
})
|
|
849
|
-
}
|
|
850
|
-
|
|
851
663
|
// Create bloom post-processing pipelines
|
|
852
664
|
private createBloomPipelines() {
|
|
853
665
|
// Bloom extraction shader (extracts bright areas)
|
|
@@ -1268,174 +1080,28 @@ export class Engine {
|
|
|
1268
1080
|
}
|
|
1269
1081
|
|
|
1270
1082
|
public async loadAnimation(url: string) {
|
|
1271
|
-
|
|
1272
|
-
this.
|
|
1273
|
-
|
|
1274
|
-
// Show first frame (time 0) immediately
|
|
1275
|
-
if (this.currentModel) {
|
|
1276
|
-
const initialPose = this.player.getPoseAtTime(0)
|
|
1277
|
-
this.applyPose(initialPose)
|
|
1278
|
-
|
|
1279
|
-
// Reset bones without time 0 keyframes
|
|
1280
|
-
const skeleton = this.currentModel.getSkeleton()
|
|
1281
|
-
const bonesWithPose = new Set(initialPose.boneRotations.keys())
|
|
1282
|
-
const bonesToReset: string[] = []
|
|
1283
|
-
for (const bone of skeleton.bones) {
|
|
1284
|
-
if (!bonesWithPose.has(bone.name)) {
|
|
1285
|
-
bonesToReset.push(bone.name)
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
if (bonesToReset.length > 0) {
|
|
1290
|
-
const identityQuat = new Quat(0, 0, 0, 1)
|
|
1291
|
-
const identityQuats = new Array(bonesToReset.length).fill(identityQuat)
|
|
1292
|
-
this.rotateBones(bonesToReset, identityQuats, 0)
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// Update model pose and physics
|
|
1296
|
-
this.currentModel.evaluatePose()
|
|
1297
|
-
|
|
1298
|
-
if (this.physics) {
|
|
1299
|
-
const worldMats = this.currentModel.getBoneWorldMatrices()
|
|
1300
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
|
|
1301
|
-
|
|
1302
|
-
// Upload matrices immediately
|
|
1303
|
-
this.device.queue.writeBuffer(
|
|
1304
|
-
this.worldMatrixBuffer!,
|
|
1305
|
-
0,
|
|
1306
|
-
worldMats.buffer,
|
|
1307
|
-
worldMats.byteOffset,
|
|
1308
|
-
worldMats.byteLength
|
|
1309
|
-
)
|
|
1310
|
-
const encoder = this.device.createCommandEncoder()
|
|
1311
|
-
this.computeSkinMatrices(encoder)
|
|
1312
|
-
this.device.queue.submit([encoder.finish()])
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1083
|
+
if (!this.currentModel) return
|
|
1084
|
+
await this.currentModel.loadVmd(url)
|
|
1315
1085
|
}
|
|
1316
1086
|
|
|
1317
1087
|
public playAnimation() {
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const wasPaused = this.player.isPausedState()
|
|
1321
|
-
const wasPlaying = this.player.isPlayingState()
|
|
1322
|
-
|
|
1323
|
-
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1324
|
-
if (!wasPlaying && !wasPaused) {
|
|
1325
|
-
// Get initial pose at time 0
|
|
1326
|
-
const initialPose = this.player.getPoseAtTime(0)
|
|
1327
|
-
this.applyPose(initialPose)
|
|
1328
|
-
|
|
1329
|
-
// Reset bones without time 0 keyframes
|
|
1330
|
-
const skeleton = this.currentModel.getSkeleton()
|
|
1331
|
-
const bonesWithPose = new Set(initialPose.boneRotations.keys())
|
|
1332
|
-
const bonesToReset: string[] = []
|
|
1333
|
-
for (const bone of skeleton.bones) {
|
|
1334
|
-
if (!bonesWithPose.has(bone.name)) {
|
|
1335
|
-
bonesToReset.push(bone.name)
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
if (bonesToReset.length > 0) {
|
|
1340
|
-
const identityQuat = new Quat(0, 0, 0, 1)
|
|
1341
|
-
const identityQuats = new Array(bonesToReset.length).fill(identityQuat)
|
|
1342
|
-
this.rotateBones(bonesToReset, identityQuats, 0)
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1346
|
-
if (this.physics) {
|
|
1347
|
-
this.currentModel.evaluatePose()
|
|
1348
|
-
|
|
1349
|
-
const worldMats = this.currentModel.getBoneWorldMatrices()
|
|
1350
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
|
|
1351
|
-
|
|
1352
|
-
// Upload matrices immediately so next frame shows correct pose
|
|
1353
|
-
this.device.queue.writeBuffer(
|
|
1354
|
-
this.worldMatrixBuffer!,
|
|
1355
|
-
0,
|
|
1356
|
-
worldMats.buffer,
|
|
1357
|
-
worldMats.byteOffset,
|
|
1358
|
-
worldMats.byteLength
|
|
1359
|
-
)
|
|
1360
|
-
const encoder = this.device.createCommandEncoder()
|
|
1361
|
-
this.computeSkinMatrices(encoder)
|
|
1362
|
-
this.device.queue.submit([encoder.finish()])
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
// Start playback (or resume if paused)
|
|
1367
|
-
this.player.play()
|
|
1368
|
-
if (this.animationStartTime === 0) {
|
|
1369
|
-
this.animationStartTime = performance.now()
|
|
1370
|
-
}
|
|
1088
|
+
this.currentModel?.playAnimation()
|
|
1371
1089
|
}
|
|
1372
1090
|
|
|
1373
1091
|
public stopAnimation() {
|
|
1374
|
-
this.
|
|
1092
|
+
this.currentModel?.stopAnimation()
|
|
1375
1093
|
}
|
|
1376
1094
|
|
|
1377
1095
|
public pauseAnimation() {
|
|
1378
|
-
this.
|
|
1096
|
+
this.currentModel?.pauseAnimation()
|
|
1379
1097
|
}
|
|
1380
1098
|
|
|
1381
1099
|
public seekAnimation(time: number) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
this.player.seek(time)
|
|
1385
|
-
|
|
1386
|
-
// Immediately apply pose at seeked time
|
|
1387
|
-
const pose = this.player.getPoseAtTime(time)
|
|
1388
|
-
this.applyPose(pose)
|
|
1389
|
-
|
|
1390
|
-
// Update model pose and physics
|
|
1391
|
-
this.currentModel.evaluatePose()
|
|
1392
|
-
|
|
1393
|
-
if (this.physics) {
|
|
1394
|
-
const worldMats = this.currentModel.getBoneWorldMatrices()
|
|
1395
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
|
|
1396
|
-
|
|
1397
|
-
// Upload matrices immediately
|
|
1398
|
-
this.device.queue.writeBuffer(
|
|
1399
|
-
this.worldMatrixBuffer!,
|
|
1400
|
-
0,
|
|
1401
|
-
worldMats.buffer,
|
|
1402
|
-
worldMats.byteOffset,
|
|
1403
|
-
worldMats.byteLength
|
|
1404
|
-
)
|
|
1405
|
-
const encoder = this.device.createCommandEncoder()
|
|
1406
|
-
this.computeSkinMatrices(encoder)
|
|
1407
|
-
this.device.queue.submit([encoder.finish()])
|
|
1408
|
-
}
|
|
1100
|
+
this.currentModel?.seekAnimation(time)
|
|
1409
1101
|
}
|
|
1410
1102
|
|
|
1411
1103
|
public getAnimationProgress() {
|
|
1412
|
-
return this.
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
/**
|
|
1416
|
-
* Apply animation pose to model
|
|
1417
|
-
*/
|
|
1418
|
-
private applyPose(pose: AnimationPose): void {
|
|
1419
|
-
if (!this.currentModel) return
|
|
1420
|
-
|
|
1421
|
-
// Apply bone rotations
|
|
1422
|
-
if (pose.boneRotations.size > 0) {
|
|
1423
|
-
const boneNames = Array.from(pose.boneRotations.keys())
|
|
1424
|
-
const rotations = Array.from(pose.boneRotations.values())
|
|
1425
|
-
this.rotateBones(boneNames, rotations, 0)
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
// Apply bone translations
|
|
1429
|
-
if (pose.boneTranslations.size > 0) {
|
|
1430
|
-
const boneNames = Array.from(pose.boneTranslations.keys())
|
|
1431
|
-
const translations = Array.from(pose.boneTranslations.values())
|
|
1432
|
-
this.moveBones(boneNames, translations, 0)
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Apply morph weights
|
|
1436
|
-
for (const [morphName, weight] of pose.morphWeights.entries()) {
|
|
1437
|
-
this.setMorphWeight(morphName, weight, 0)
|
|
1438
|
-
}
|
|
1104
|
+
return this.currentModel?.getAnimationProgress() ?? { current: 0, duration: 0, percentage: 0 }
|
|
1439
1105
|
}
|
|
1440
1106
|
|
|
1441
1107
|
public getStats(): EngineStats {
|
|
@@ -1484,8 +1150,6 @@ export class Engine {
|
|
|
1484
1150
|
this.modelDir = dir
|
|
1485
1151
|
|
|
1486
1152
|
const model = await PmxLoader.load(path)
|
|
1487
|
-
|
|
1488
|
-
this.physics = new Physics(model.getRigidbodies(), model.getJoints())
|
|
1489
1153
|
await this.setupModelBuffers(model)
|
|
1490
1154
|
}
|
|
1491
1155
|
|
|
@@ -1559,13 +1223,7 @@ export class Engine {
|
|
|
1559
1223
|
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1560
1224
|
label: "skin matrices",
|
|
1561
1225
|
size: Math.max(256, matrixSize),
|
|
1562
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
|
|
1563
|
-
})
|
|
1564
|
-
|
|
1565
|
-
this.worldMatrixBuffer = this.device.createBuffer({
|
|
1566
|
-
label: "world matrices",
|
|
1567
|
-
size: Math.max(256, matrixSize),
|
|
1568
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1226
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1569
1227
|
})
|
|
1570
1228
|
|
|
1571
1229
|
this.inverseBindMatrixBuffer = this.device.createBuffer({
|
|
@@ -1583,28 +1241,6 @@ export class Engine {
|
|
|
1583
1241
|
invBindMatrices.byteLength
|
|
1584
1242
|
)
|
|
1585
1243
|
|
|
1586
|
-
this.boneCountBuffer = this.device.createBuffer({
|
|
1587
|
-
label: "bone count uniform",
|
|
1588
|
-
size: 32, // Minimum uniform buffer size is 32 bytes
|
|
1589
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1590
|
-
})
|
|
1591
|
-
const boneCountData = new Uint32Array(8) // 32 bytes total
|
|
1592
|
-
boneCountData[0] = boneCount
|
|
1593
|
-
this.device.queue.writeBuffer(this.boneCountBuffer, 0, boneCountData)
|
|
1594
|
-
|
|
1595
|
-
this.createSkinMatrixComputePipeline()
|
|
1596
|
-
|
|
1597
|
-
// Create compute bind group once (reused every frame)
|
|
1598
|
-
this.skinMatrixComputeBindGroup = this.device.createBindGroup({
|
|
1599
|
-
layout: this.skinMatrixComputePipeline!.getBindGroupLayout(0),
|
|
1600
|
-
entries: [
|
|
1601
|
-
{ binding: 0, resource: { buffer: this.boneCountBuffer } },
|
|
1602
|
-
{ binding: 1, resource: { buffer: this.worldMatrixBuffer } },
|
|
1603
|
-
{ binding: 2, resource: { buffer: this.inverseBindMatrixBuffer } },
|
|
1604
|
-
{ binding: 3, resource: { buffer: this.skinMatrixBuffer } },
|
|
1605
|
-
],
|
|
1606
|
-
})
|
|
1607
|
-
|
|
1608
1244
|
const indices = model.getIndices()
|
|
1609
1245
|
if (indices) {
|
|
1610
1246
|
this.indexBuffer = this.device.createBuffer({
|
|
@@ -1638,15 +1274,7 @@ export class Engine {
|
|
|
1638
1274
|
return texture
|
|
1639
1275
|
}
|
|
1640
1276
|
|
|
1641
|
-
this.
|
|
1642
|
-
this.eyeDraws = []
|
|
1643
|
-
this.hairDrawsOverEyes = []
|
|
1644
|
-
this.hairDrawsOverNonEyes = []
|
|
1645
|
-
this.transparentDraws = []
|
|
1646
|
-
this.opaqueOutlineDraws = []
|
|
1647
|
-
this.eyeOutlineDraws = []
|
|
1648
|
-
this.hairOutlineDraws = []
|
|
1649
|
-
this.transparentOutlineDraws = []
|
|
1277
|
+
this.drawCalls = []
|
|
1650
1278
|
let currentIndexOffset = 0
|
|
1651
1279
|
|
|
1652
1280
|
for (const mat of materials) {
|
|
@@ -1659,23 +1287,7 @@ export class Engine {
|
|
|
1659
1287
|
const materialAlpha = mat.diffuse[3]
|
|
1660
1288
|
const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON
|
|
1661
1289
|
|
|
1662
|
-
|
|
1663
|
-
const materialUniformData = new Float32Array(8)
|
|
1664
|
-
materialUniformData[0] = materialAlpha
|
|
1665
|
-
materialUniformData[1] = 1.0 // alphaMultiplier: 1.0 for non-hair materials
|
|
1666
|
-
materialUniformData[2] = this.rimLightIntensity
|
|
1667
|
-
materialUniformData[3] = 0.0 // _padding1
|
|
1668
|
-
materialUniformData[4] = 1.0 // rimColor.r
|
|
1669
|
-
materialUniformData[5] = 1.0 // rimColor.g
|
|
1670
|
-
materialUniformData[6] = 1.0 // rimColor.b
|
|
1671
|
-
materialUniformData[7] = 0.0 // isOverEyes
|
|
1672
|
-
|
|
1673
|
-
const materialUniformBuffer = this.device.createBuffer({
|
|
1674
|
-
label: `material uniform: ${mat.name}`,
|
|
1675
|
-
size: materialUniformData.byteLength,
|
|
1676
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1677
|
-
})
|
|
1678
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1290
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0)
|
|
1679
1291
|
|
|
1680
1292
|
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1681
1293
|
const bindGroup = this.device.createBindGroup({
|
|
@@ -1691,100 +1303,70 @@ export class Engine {
|
|
|
1691
1303
|
],
|
|
1692
1304
|
})
|
|
1693
1305
|
|
|
1694
|
-
if (
|
|
1695
|
-
if (
|
|
1696
|
-
this.
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
})
|
|
1720
|
-
this.device.queue.writeBuffer(buffer, 0, uniformData)
|
|
1721
|
-
|
|
1722
|
-
return this.device.createBindGroup({
|
|
1723
|
-
label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
1724
|
-
layout: this.mainBindGroupLayout,
|
|
1725
|
-
entries: [
|
|
1726
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1727
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1728
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1729
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1730
|
-
{ binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1731
|
-
{ binding: 5, resource: { buffer: buffer } },
|
|
1732
|
-
],
|
|
1733
|
-
})
|
|
1734
|
-
}
|
|
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
|
+
}
|
|
1735
1331
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1332
|
+
const bindGroupOverEyes = createHairBindGroup(true)
|
|
1333
|
+
const bindGroupOverNonEyes = createHairBindGroup(false)
|
|
1738
1334
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1335
|
+
this.drawCalls.push({
|
|
1336
|
+
type: "hair-over-eyes",
|
|
1741
1337
|
count: indexCount,
|
|
1742
1338
|
firstIndex: currentIndexOffset,
|
|
1743
1339
|
bindGroup: bindGroupOverEyes,
|
|
1744
1340
|
})
|
|
1745
|
-
|
|
1746
|
-
|
|
1341
|
+
this.drawCalls.push({
|
|
1342
|
+
type: "hair-over-non-eyes",
|
|
1747
1343
|
count: indexCount,
|
|
1748
1344
|
firstIndex: currentIndexOffset,
|
|
1749
1345
|
bindGroup: bindGroupOverNonEyes,
|
|
1750
1346
|
})
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
this.
|
|
1755
|
-
count: indexCount,
|
|
1756
|
-
firstIndex: currentIndexOffset,
|
|
1757
|
-
bindGroup,
|
|
1758
|
-
})
|
|
1759
|
-
}
|
|
1760
|
-
} else {
|
|
1761
|
-
if (indexCount > 0) {
|
|
1762
|
-
this.opaqueDraws.push({
|
|
1763
|
-
count: indexCount,
|
|
1764
|
-
firstIndex: currentIndexOffset,
|
|
1765
|
-
bindGroup,
|
|
1766
|
-
})
|
|
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 })
|
|
1767
1351
|
}
|
|
1768
1352
|
}
|
|
1769
1353
|
|
|
1770
1354
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1771
1355
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1772
|
-
const materialUniformData = new Float32Array(
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const materialUniformBuffer = this.
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
})
|
|
1787
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1356
|
+
const materialUniformData = new Float32Array([
|
|
1357
|
+
mat.edgeColor[0],
|
|
1358
|
+
mat.edgeColor[1],
|
|
1359
|
+
mat.edgeColor[2],
|
|
1360
|
+
mat.edgeColor[3],
|
|
1361
|
+
mat.edgeSize,
|
|
1362
|
+
0,
|
|
1363
|
+
0,
|
|
1364
|
+
0,
|
|
1365
|
+
])
|
|
1366
|
+
const materialUniformBuffer = this.createUniformBuffer(
|
|
1367
|
+
`outline material uniform: ${mat.name}`,
|
|
1368
|
+
materialUniformData
|
|
1369
|
+
)
|
|
1788
1370
|
|
|
1789
1371
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1790
1372
|
label: `outline bind group: ${mat.name}`,
|
|
@@ -1797,31 +1379,19 @@ export class Engine {
|
|
|
1797
1379
|
})
|
|
1798
1380
|
|
|
1799
1381
|
if (indexCount > 0) {
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
}
|
|
1813
|
-
this.transparentOutlineDraws.push({
|
|
1814
|
-
count: indexCount,
|
|
1815
|
-
firstIndex: currentIndexOffset,
|
|
1816
|
-
bindGroup: outlineBindGroup,
|
|
1817
|
-
})
|
|
1818
|
-
} else {
|
|
1819
|
-
this.opaqueOutlineDraws.push({
|
|
1820
|
-
count: indexCount,
|
|
1821
|
-
firstIndex: currentIndexOffset,
|
|
1822
|
-
bindGroup: outlineBindGroup,
|
|
1823
|
-
})
|
|
1824
|
-
}
|
|
1382
|
+
const outlineType: DrawCallType = mat.isEye
|
|
1383
|
+
? "eye-outline"
|
|
1384
|
+
: mat.isHair
|
|
1385
|
+
? "hair-outline"
|
|
1386
|
+
: isTransparent
|
|
1387
|
+
? "transparent-outline"
|
|
1388
|
+
: "opaque-outline"
|
|
1389
|
+
this.drawCalls.push({
|
|
1390
|
+
type: outlineType,
|
|
1391
|
+
count: indexCount,
|
|
1392
|
+
firstIndex: currentIndexOffset,
|
|
1393
|
+
bindGroup: outlineBindGroup,
|
|
1394
|
+
})
|
|
1825
1395
|
}
|
|
1826
1396
|
}
|
|
1827
1397
|
|
|
@@ -1829,6 +1399,22 @@ export class Engine {
|
|
|
1829
1399
|
}
|
|
1830
1400
|
}
|
|
1831
1401
|
|
|
1402
|
+
private createMaterialUniformBuffer(label: string, alpha: number, isOverEyes: number): GPUBuffer {
|
|
1403
|
+
const data = new Float32Array(8)
|
|
1404
|
+
data.set([alpha, 1.0, this.rimLightIntensity, 0.0, 1.0, 1.0, 1.0, isOverEyes])
|
|
1405
|
+
return this.createUniformBuffer(`material uniform: ${label}`, data)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private createUniformBuffer(label: string, data: Float32Array | Uint32Array): GPUBuffer {
|
|
1409
|
+
const buffer = this.device.createBuffer({
|
|
1410
|
+
label,
|
|
1411
|
+
size: data.byteLength,
|
|
1412
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1413
|
+
})
|
|
1414
|
+
this.device.queue.writeBuffer(buffer, 0, data as ArrayBufferView<ArrayBuffer>)
|
|
1415
|
+
return buffer
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1832
1418
|
private async createTextureFromPath(path: string): Promise<GPUTexture | null> {
|
|
1833
1419
|
const cached = this.textureCache.get(path)
|
|
1834
1420
|
if (cached) {
|
|
@@ -1867,51 +1453,54 @@ export class Engine {
|
|
|
1867
1453
|
private renderEyes(pass: GPURenderPassEncoder) {
|
|
1868
1454
|
pass.setPipeline(this.eyePipeline)
|
|
1869
1455
|
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1870
|
-
for (const draw of this.
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
+
}
|
|
1873
1461
|
}
|
|
1874
1462
|
}
|
|
1875
1463
|
|
|
1876
1464
|
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1877
1465
|
private renderHair(pass: GPURenderPassEncoder) {
|
|
1878
1466
|
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1879
|
-
const hasHair = this.
|
|
1467
|
+
const hasHair = this.drawCalls.some((d) => d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes")
|
|
1880
1468
|
if (hasHair) {
|
|
1881
1469
|
pass.setPipeline(this.hairDepthPipeline)
|
|
1882
|
-
for (const draw of this.
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1888
|
-
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
|
+
}
|
|
1889
1475
|
}
|
|
1890
1476
|
}
|
|
1891
1477
|
|
|
1892
1478
|
// Hair shading (split by stencil for transparency over eyes)
|
|
1893
|
-
|
|
1479
|
+
const hairOverEyes = this.drawCalls.filter((d) => d.type === "hair-over-eyes")
|
|
1480
|
+
if (hairOverEyes.length > 0) {
|
|
1894
1481
|
pass.setPipeline(this.hairPipelineOverEyes)
|
|
1895
1482
|
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1896
|
-
for (const draw of
|
|
1483
|
+
for (const draw of hairOverEyes) {
|
|
1897
1484
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1898
1485
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1899
1486
|
}
|
|
1900
1487
|
}
|
|
1901
1488
|
|
|
1902
|
-
|
|
1489
|
+
const hairOverNonEyes = this.drawCalls.filter((d) => d.type === "hair-over-non-eyes")
|
|
1490
|
+
if (hairOverNonEyes.length > 0) {
|
|
1903
1491
|
pass.setPipeline(this.hairPipelineOverNonEyes)
|
|
1904
1492
|
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1905
|
-
for (const draw of
|
|
1493
|
+
for (const draw of hairOverNonEyes) {
|
|
1906
1494
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1907
1495
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1908
1496
|
}
|
|
1909
1497
|
}
|
|
1910
1498
|
|
|
1911
1499
|
// Hair outlines
|
|
1912
|
-
|
|
1500
|
+
const hairOutlines = this.drawCalls.filter((d) => d.type === "hair-outline")
|
|
1501
|
+
if (hairOutlines.length > 0) {
|
|
1913
1502
|
pass.setPipeline(this.hairOutlinePipeline)
|
|
1914
|
-
for (const draw of
|
|
1503
|
+
for (const draw of hairOutlines) {
|
|
1915
1504
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1916
1505
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1917
1506
|
}
|
|
@@ -1928,19 +1517,10 @@ export class Engine {
|
|
|
1928
1517
|
this.updateCameraUniforms()
|
|
1929
1518
|
this.updateRenderTarget()
|
|
1930
1519
|
|
|
1931
|
-
//
|
|
1932
|
-
if (this.hasAnimation && this.currentModel) {
|
|
1933
|
-
const pose = this.player.update(currentTime)
|
|
1934
|
-
if (pose) {
|
|
1935
|
-
this.applyPose(pose)
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
// Update model pose first (this may update morph weights via tweens)
|
|
1940
|
-
// 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)
|
|
1941
1521
|
if (this.currentModel) {
|
|
1942
|
-
const
|
|
1943
|
-
if (
|
|
1522
|
+
const verticesChanged = this.currentModel.update(deltaTime)
|
|
1523
|
+
if (verticesChanged) {
|
|
1944
1524
|
this.vertexBufferNeedsUpdate = true
|
|
1945
1525
|
}
|
|
1946
1526
|
}
|
|
@@ -1951,10 +1531,11 @@ export class Engine {
|
|
|
1951
1531
|
this.vertexBufferNeedsUpdate = false
|
|
1952
1532
|
}
|
|
1953
1533
|
|
|
1954
|
-
//
|
|
1955
|
-
|
|
1534
|
+
// Update skin matrices buffer
|
|
1535
|
+
this.updateSkinMatrices()
|
|
1956
1536
|
|
|
1957
|
-
|
|
1537
|
+
// Use single encoder for render
|
|
1538
|
+
const encoder = this.device.createCommandEncoder()
|
|
1958
1539
|
|
|
1959
1540
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
1960
1541
|
|
|
@@ -1966,9 +1547,11 @@ export class Engine {
|
|
|
1966
1547
|
|
|
1967
1548
|
// Pass 1: Opaque
|
|
1968
1549
|
pass.setPipeline(this.modelPipeline)
|
|
1969
|
-
for (const draw of this.
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
+
}
|
|
1972
1555
|
}
|
|
1973
1556
|
|
|
1974
1557
|
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
@@ -1981,9 +1564,11 @@ export class Engine {
|
|
|
1981
1564
|
|
|
1982
1565
|
// Pass 4: Transparent
|
|
1983
1566
|
pass.setPipeline(this.modelPipeline)
|
|
1984
|
-
for (const draw of this.
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
+
}
|
|
1987
1572
|
}
|
|
1988
1573
|
|
|
1989
1574
|
this.drawOutlines(pass, true)
|
|
@@ -2119,42 +1704,27 @@ export class Engine {
|
|
|
2119
1704
|
}
|
|
2120
1705
|
}
|
|
2121
1706
|
|
|
2122
|
-
private
|
|
2123
|
-
|
|
2124
|
-
// Here we just get the matrices and update physics/compute
|
|
2125
|
-
const worldMats = this.currentModel!.getBoneWorldMatrices()
|
|
2126
|
-
|
|
2127
|
-
if (this.physics) {
|
|
2128
|
-
this.physics.step(deltaTime, worldMats, this.currentModel!.getBoneInverseBindMatrices())
|
|
2129
|
-
}
|
|
1707
|
+
private updateSkinMatrices() {
|
|
1708
|
+
if (!this.currentModel || !this.skinMatrixBuffer) return
|
|
2130
1709
|
|
|
1710
|
+
const skinMatrices = this.currentModel.getSkinMatrices()
|
|
2131
1711
|
this.device.queue.writeBuffer(
|
|
2132
|
-
this.
|
|
1712
|
+
this.skinMatrixBuffer,
|
|
2133
1713
|
0,
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
1714
|
+
skinMatrices.buffer,
|
|
1715
|
+
skinMatrices.byteOffset,
|
|
1716
|
+
skinMatrices.byteLength
|
|
2137
1717
|
)
|
|
2138
|
-
this.computeSkinMatrices(encoder)
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
private computeSkinMatrices(encoder: GPUCommandEncoder) {
|
|
2142
|
-
const boneCount = this.currentModel!.getSkeleton().bones.length
|
|
2143
|
-
const workgroupCount = Math.ceil(boneCount / this.COMPUTE_WORKGROUP_SIZE)
|
|
2144
|
-
|
|
2145
|
-
const pass = encoder.beginComputePass()
|
|
2146
|
-
pass.setPipeline(this.skinMatrixComputePipeline!)
|
|
2147
|
-
pass.setBindGroup(0, this.skinMatrixComputeBindGroup!)
|
|
2148
|
-
pass.dispatchWorkgroups(workgroupCount)
|
|
2149
|
-
pass.end()
|
|
2150
1718
|
}
|
|
2151
1719
|
|
|
2152
1720
|
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
2153
1721
|
pass.setPipeline(this.outlinePipeline)
|
|
2154
|
-
const
|
|
2155
|
-
for (const draw of
|
|
2156
|
-
|
|
2157
|
-
|
|
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
|
+
}
|
|
2158
1728
|
}
|
|
2159
1729
|
}
|
|
2160
1730
|
|