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/README.md +0 -1
- package/dist/engine.d.ts +8 -25
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +235 -588
- 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/model.d.ts +59 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +416 -112
- package/package.json +1 -1
- package/src/engine.ts +266 -649
- package/src/model.ts +518 -156
- package/dist/ik.d.ts +0 -32
- package/dist/ik.d.ts.map +0 -1
- package/dist/ik.js +0 -337
- package/dist/pool-scene.d.ts +0 -52
- package/dist/pool-scene.d.ts.map +0 -1
- package/dist/pool-scene.js +0 -1122
- package/dist/pool.d.ts +0 -38
- package/dist/pool.d.ts.map +0 -1
- package/dist/pool.js +0 -422
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,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.
|
|
400
|
+
this.modelPipeline = this.createRenderPipeline({
|
|
305
401
|
label: "model pipeline",
|
|
306
402
|
layout: mainPipelineLayout,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
501
|
+
this.outlinePipeline = this.createRenderPipeline({
|
|
447
502
|
label: "outline pipeline",
|
|
448
503
|
layout: outlinePipelineLayout,
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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.
|
|
516
|
+
this.hairOutlinePipeline = this.createRenderPipeline({
|
|
512
517
|
label: "hair outline pipeline",
|
|
513
518
|
layout: outlinePipelineLayout,
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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,
|
|
568
|
-
depthCompare: "less-equal",
|
|
569
|
-
depthBias: -0.0001,
|
|
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.
|
|
534
|
+
this.eyePipeline = this.createRenderPipeline({
|
|
580
535
|
label: "eye overlay pipeline",
|
|
581
536
|
layout: mainPipelineLayout,
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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,
|
|
627
|
-
depthCompare: "less-equal",
|
|
628
|
-
depthBias: -0.00005,
|
|
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",
|
|
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.
|
|
609
|
+
this.hairDepthPipeline = this.createRenderPipeline({
|
|
694
610
|
label: "hair depth pre-pass",
|
|
695
611
|
layout: mainPipelineLayout,
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
618
|
+
fragmentEntryPoint: "fs",
|
|
619
|
+
cullMode: "front",
|
|
727
620
|
depthStencil: {
|
|
728
621
|
format: "depth24plus-stencil8",
|
|
729
622
|
depthWriteEnabled: true,
|
|
730
|
-
depthCompare: "less-equal",
|
|
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.
|
|
632
|
+
return this.createRenderPipeline({
|
|
741
633
|
label: `hair pipeline (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
742
634
|
layout: mainPipelineLayout,
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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,
|
|
788
|
-
depthCompare: "less-equal",
|
|
641
|
+
depthWriteEnabled: false,
|
|
642
|
+
depthCompare: "less-equal",
|
|
789
643
|
stencilFront: {
|
|
790
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
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
|
-
|
|
1271
|
-
this.
|
|
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
|
-
|
|
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.
|
|
1092
|
+
this.currentModel?.stopAnimation()
|
|
1371
1093
|
}
|
|
1372
1094
|
|
|
1373
1095
|
public pauseAnimation() {
|
|
1374
|
-
this.
|
|
1096
|
+
this.currentModel?.pauseAnimation()
|
|
1375
1097
|
}
|
|
1376
1098
|
|
|
1377
1099
|
public seekAnimation(time: number) {
|
|
1378
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1332
|
+
const bindGroupOverEyes = createHairBindGroup(true)
|
|
1333
|
+
const bindGroupOverNonEyes = createHairBindGroup(false)
|
|
1707
1334
|
|
|
1708
|
-
|
|
1709
|
-
|
|
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.
|
|
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
|
|
1755
|
-
?
|
|
1382
|
+
const outlineType: DrawCallType = mat.isEye
|
|
1383
|
+
? "eye-outline"
|
|
1756
1384
|
: mat.isHair
|
|
1757
|
-
?
|
|
1385
|
+
? "hair-outline"
|
|
1758
1386
|
: isTransparent
|
|
1759
|
-
?
|
|
1760
|
-
:
|
|
1761
|
-
|
|
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.
|
|
1824
|
-
|
|
1825
|
-
|
|
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.
|
|
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.
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
1896
|
-
if (
|
|
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
|
-
//
|
|
1908
|
-
|
|
1534
|
+
// Update skin matrices buffer
|
|
1535
|
+
this.updateSkinMatrices()
|
|
1909
1536
|
|
|
1910
|
-
|
|
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.
|
|
1923
|
-
|
|
1924
|
-
|
|
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.
|
|
1938
|
-
|
|
1939
|
-
|
|
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
|
|
2076
|
-
|
|
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.
|
|
1712
|
+
this.skinMatrixBuffer,
|
|
2086
1713
|
0,
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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
|
|
2108
|
-
for (const draw of
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
|