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/dist/engine.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
|
-
import {
|
|
2
|
+
import { Vec3 } from "./math";
|
|
3
3
|
import { PmxLoader } from "./pmx-loader";
|
|
4
|
-
import { Physics } from "./physics";
|
|
5
|
-
import { Player } from "./player";
|
|
6
4
|
export class Engine {
|
|
7
5
|
constructor(canvas, options) {
|
|
8
6
|
this.cameraMatrixData = new Float32Array(36);
|
|
@@ -13,7 +11,6 @@ export class Engine {
|
|
|
13
11
|
this.sampleCount = 4;
|
|
14
12
|
// Constants
|
|
15
13
|
this.STENCIL_EYE_VALUE = 1;
|
|
16
|
-
this.COMPUTE_WORKGROUP_SIZE = 64;
|
|
17
14
|
this.BLOOM_DOWNSCALE_FACTOR = 2;
|
|
18
15
|
// Ambient light settings
|
|
19
16
|
this.ambientColor = new Vec3(1.0, 1.0, 1.0);
|
|
@@ -24,19 +21,10 @@ export class Engine {
|
|
|
24
21
|
this.rimLightIntensity = Engine.DEFAULT_RIM_LIGHT_INTENSITY;
|
|
25
22
|
this.currentModel = null;
|
|
26
23
|
this.modelDir = "";
|
|
27
|
-
this.physics = null;
|
|
28
24
|
this.textureCache = new Map();
|
|
29
25
|
this.vertexBufferNeedsUpdate = false;
|
|
30
|
-
//
|
|
31
|
-
this.
|
|
32
|
-
this.eyeDraws = [];
|
|
33
|
-
this.hairDrawsOverEyes = [];
|
|
34
|
-
this.hairDrawsOverNonEyes = [];
|
|
35
|
-
this.transparentDraws = [];
|
|
36
|
-
this.opaqueOutlineDraws = [];
|
|
37
|
-
this.eyeOutlineDraws = [];
|
|
38
|
-
this.hairOutlineDraws = [];
|
|
39
|
-
this.transparentOutlineDraws = [];
|
|
26
|
+
// Unified draw call list
|
|
27
|
+
this.drawCalls = [];
|
|
40
28
|
this.lastFpsUpdate = performance.now();
|
|
41
29
|
this.framesSinceLastUpdate = 0;
|
|
42
30
|
this.lastFrameTime = performance.now();
|
|
@@ -48,8 +36,6 @@ export class Engine {
|
|
|
48
36
|
};
|
|
49
37
|
this.animationFrameId = null;
|
|
50
38
|
this.renderLoopCallback = null;
|
|
51
|
-
this.player = new Player();
|
|
52
|
-
this.hasAnimation = false; // Set to true when loadAnimation is called
|
|
53
39
|
this.canvas = canvas;
|
|
54
40
|
if (options) {
|
|
55
41
|
this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
|
|
@@ -84,6 +70,26 @@ export class Engine {
|
|
|
84
70
|
this.createBloomPipelines();
|
|
85
71
|
this.setupResize();
|
|
86
72
|
}
|
|
73
|
+
createRenderPipeline(config) {
|
|
74
|
+
return this.device.createRenderPipeline({
|
|
75
|
+
label: config.label,
|
|
76
|
+
layout: config.layout,
|
|
77
|
+
vertex: {
|
|
78
|
+
module: config.shaderModule,
|
|
79
|
+
buffers: config.vertexBuffers,
|
|
80
|
+
},
|
|
81
|
+
fragment: config.fragmentTarget
|
|
82
|
+
? {
|
|
83
|
+
module: config.shaderModule,
|
|
84
|
+
entryPoint: config.fragmentEntryPoint,
|
|
85
|
+
targets: [config.fragmentTarget],
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
primitive: { cullMode: config.cullMode ?? "none" },
|
|
89
|
+
depthStencil: config.depthStencil,
|
|
90
|
+
multisample: config.multisample ?? { count: this.sampleCount },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
87
93
|
createPipelines() {
|
|
88
94
|
this.materialSampler = this.device.createSampler({
|
|
89
95
|
magFilter: "linear",
|
|
@@ -91,6 +97,74 @@ export class Engine {
|
|
|
91
97
|
addressModeU: "repeat",
|
|
92
98
|
addressModeV: "repeat",
|
|
93
99
|
});
|
|
100
|
+
// Shared vertex buffer layouts
|
|
101
|
+
const fullVertexBuffers = [
|
|
102
|
+
{
|
|
103
|
+
arrayStride: 8 * 4,
|
|
104
|
+
attributes: [
|
|
105
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
106
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
107
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
arrayStride: 4 * 2,
|
|
112
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
arrayStride: 4,
|
|
116
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
const outlineVertexBuffers = [
|
|
120
|
+
{
|
|
121
|
+
arrayStride: 8 * 4,
|
|
122
|
+
attributes: [
|
|
123
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
124
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
arrayStride: 4 * 2,
|
|
129
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
arrayStride: 4,
|
|
133
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
const depthOnlyVertexBuffers = [
|
|
137
|
+
{
|
|
138
|
+
arrayStride: 8 * 4,
|
|
139
|
+
attributes: [
|
|
140
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
141
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
arrayStride: 4 * 2,
|
|
146
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
arrayStride: 4,
|
|
150
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
const standardBlend = {
|
|
154
|
+
format: this.presentationFormat,
|
|
155
|
+
blend: {
|
|
156
|
+
color: {
|
|
157
|
+
srcFactor: "src-alpha",
|
|
158
|
+
dstFactor: "one-minus-src-alpha",
|
|
159
|
+
operation: "add",
|
|
160
|
+
},
|
|
161
|
+
alpha: {
|
|
162
|
+
srcFactor: "one",
|
|
163
|
+
dstFactor: "one-minus-src-alpha",
|
|
164
|
+
operation: "add",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
94
168
|
const shaderModule = this.device.createShaderModule({
|
|
95
169
|
label: "model shaders",
|
|
96
170
|
code: /* wgsl */ `
|
|
@@ -204,59 +278,18 @@ export class Engine {
|
|
|
204
278
|
label: "main pipeline layout",
|
|
205
279
|
bindGroupLayouts: [this.mainBindGroupLayout],
|
|
206
280
|
});
|
|
207
|
-
this.modelPipeline = this.
|
|
281
|
+
this.modelPipeline = this.createRenderPipeline({
|
|
208
282
|
label: "model pipeline",
|
|
209
283
|
layout: mainPipelineLayout,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
arrayStride: 8 * 4,
|
|
215
|
-
attributes: [
|
|
216
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
217
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
218
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
219
|
-
],
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
arrayStride: 4 * 2,
|
|
223
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
arrayStride: 4,
|
|
227
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
},
|
|
231
|
-
fragment: {
|
|
232
|
-
module: shaderModule,
|
|
233
|
-
targets: [
|
|
234
|
-
{
|
|
235
|
-
format: this.presentationFormat,
|
|
236
|
-
blend: {
|
|
237
|
-
color: {
|
|
238
|
-
srcFactor: "src-alpha",
|
|
239
|
-
dstFactor: "one-minus-src-alpha",
|
|
240
|
-
operation: "add",
|
|
241
|
-
},
|
|
242
|
-
alpha: {
|
|
243
|
-
srcFactor: "one",
|
|
244
|
-
dstFactor: "one-minus-src-alpha",
|
|
245
|
-
operation: "add",
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
],
|
|
250
|
-
},
|
|
251
|
-
primitive: { cullMode: "none" },
|
|
284
|
+
shaderModule,
|
|
285
|
+
vertexBuffers: fullVertexBuffers,
|
|
286
|
+
fragmentTarget: standardBlend,
|
|
287
|
+
cullMode: "none",
|
|
252
288
|
depthStencil: {
|
|
253
289
|
format: "depth24plus-stencil8",
|
|
254
290
|
depthWriteEnabled: true,
|
|
255
291
|
depthCompare: "less-equal",
|
|
256
292
|
},
|
|
257
|
-
multisample: {
|
|
258
|
-
count: this.sampleCount,
|
|
259
|
-
},
|
|
260
293
|
});
|
|
261
294
|
// Create bind group layout for outline pipelines
|
|
262
295
|
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -342,194 +375,56 @@ export class Engine {
|
|
|
342
375
|
}
|
|
343
376
|
`,
|
|
344
377
|
});
|
|
345
|
-
this.outlinePipeline = this.
|
|
378
|
+
this.outlinePipeline = this.createRenderPipeline({
|
|
346
379
|
label: "outline pipeline",
|
|
347
380
|
layout: outlinePipelineLayout,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
arrayStride: 8 * 4,
|
|
353
|
-
attributes: [
|
|
354
|
-
{
|
|
355
|
-
shaderLocation: 0,
|
|
356
|
-
offset: 0,
|
|
357
|
-
format: "float32x3",
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
shaderLocation: 1,
|
|
361
|
-
offset: 3 * 4,
|
|
362
|
-
format: "float32x3",
|
|
363
|
-
},
|
|
364
|
-
],
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
arrayStride: 4 * 2,
|
|
368
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
arrayStride: 4,
|
|
372
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
373
|
-
},
|
|
374
|
-
],
|
|
375
|
-
},
|
|
376
|
-
fragment: {
|
|
377
|
-
module: outlineShaderModule,
|
|
378
|
-
targets: [
|
|
379
|
-
{
|
|
380
|
-
format: this.presentationFormat,
|
|
381
|
-
blend: {
|
|
382
|
-
color: {
|
|
383
|
-
srcFactor: "src-alpha",
|
|
384
|
-
dstFactor: "one-minus-src-alpha",
|
|
385
|
-
operation: "add",
|
|
386
|
-
},
|
|
387
|
-
alpha: {
|
|
388
|
-
srcFactor: "one",
|
|
389
|
-
dstFactor: "one-minus-src-alpha",
|
|
390
|
-
operation: "add",
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
],
|
|
395
|
-
},
|
|
396
|
-
primitive: {
|
|
397
|
-
cullMode: "back",
|
|
398
|
-
},
|
|
381
|
+
shaderModule: outlineShaderModule,
|
|
382
|
+
vertexBuffers: outlineVertexBuffers,
|
|
383
|
+
fragmentTarget: standardBlend,
|
|
384
|
+
cullMode: "back",
|
|
399
385
|
depthStencil: {
|
|
400
386
|
format: "depth24plus-stencil8",
|
|
401
387
|
depthWriteEnabled: true,
|
|
402
388
|
depthCompare: "less-equal",
|
|
403
389
|
},
|
|
404
|
-
multisample: {
|
|
405
|
-
count: this.sampleCount,
|
|
406
|
-
},
|
|
407
390
|
});
|
|
408
391
|
// Hair outline pipeline
|
|
409
|
-
this.hairOutlinePipeline = this.
|
|
392
|
+
this.hairOutlinePipeline = this.createRenderPipeline({
|
|
410
393
|
label: "hair outline pipeline",
|
|
411
394
|
layout: outlinePipelineLayout,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
arrayStride: 8 * 4,
|
|
417
|
-
attributes: [
|
|
418
|
-
{
|
|
419
|
-
shaderLocation: 0,
|
|
420
|
-
offset: 0,
|
|
421
|
-
format: "float32x3",
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
shaderLocation: 1,
|
|
425
|
-
offset: 3 * 4,
|
|
426
|
-
format: "float32x3",
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
arrayStride: 4 * 2,
|
|
432
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
433
|
-
},
|
|
434
|
-
{
|
|
435
|
-
arrayStride: 4,
|
|
436
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
},
|
|
440
|
-
fragment: {
|
|
441
|
-
module: outlineShaderModule,
|
|
442
|
-
targets: [
|
|
443
|
-
{
|
|
444
|
-
format: this.presentationFormat,
|
|
445
|
-
blend: {
|
|
446
|
-
color: {
|
|
447
|
-
srcFactor: "src-alpha",
|
|
448
|
-
dstFactor: "one-minus-src-alpha",
|
|
449
|
-
operation: "add",
|
|
450
|
-
},
|
|
451
|
-
alpha: {
|
|
452
|
-
srcFactor: "one",
|
|
453
|
-
dstFactor: "one-minus-src-alpha",
|
|
454
|
-
operation: "add",
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
],
|
|
459
|
-
},
|
|
460
|
-
primitive: {
|
|
461
|
-
cullMode: "back",
|
|
462
|
-
},
|
|
395
|
+
shaderModule: outlineShaderModule,
|
|
396
|
+
vertexBuffers: outlineVertexBuffers,
|
|
397
|
+
fragmentTarget: standardBlend,
|
|
398
|
+
cullMode: "back",
|
|
463
399
|
depthStencil: {
|
|
464
400
|
format: "depth24plus-stencil8",
|
|
465
|
-
depthWriteEnabled: false,
|
|
466
|
-
depthCompare: "less-equal",
|
|
467
|
-
depthBias: -0.0001,
|
|
401
|
+
depthWriteEnabled: false,
|
|
402
|
+
depthCompare: "less-equal",
|
|
403
|
+
depthBias: -0.0001,
|
|
468
404
|
depthBiasSlopeScale: 0.0,
|
|
469
405
|
depthBiasClamp: 0.0,
|
|
470
406
|
},
|
|
471
|
-
multisample: {
|
|
472
|
-
count: this.sampleCount,
|
|
473
|
-
},
|
|
474
407
|
});
|
|
475
408
|
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
476
|
-
this.eyePipeline = this.
|
|
409
|
+
this.eyePipeline = this.createRenderPipeline({
|
|
477
410
|
label: "eye overlay pipeline",
|
|
478
411
|
layout: mainPipelineLayout,
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
arrayStride: 8 * 4,
|
|
484
|
-
attributes: [
|
|
485
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
486
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
487
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
488
|
-
],
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
arrayStride: 4 * 2,
|
|
492
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
arrayStride: 4,
|
|
496
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
497
|
-
},
|
|
498
|
-
],
|
|
499
|
-
},
|
|
500
|
-
fragment: {
|
|
501
|
-
module: shaderModule,
|
|
502
|
-
targets: [
|
|
503
|
-
{
|
|
504
|
-
format: this.presentationFormat,
|
|
505
|
-
blend: {
|
|
506
|
-
color: {
|
|
507
|
-
srcFactor: "src-alpha",
|
|
508
|
-
dstFactor: "one-minus-src-alpha",
|
|
509
|
-
operation: "add",
|
|
510
|
-
},
|
|
511
|
-
alpha: {
|
|
512
|
-
srcFactor: "one",
|
|
513
|
-
dstFactor: "one-minus-src-alpha",
|
|
514
|
-
operation: "add",
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
],
|
|
519
|
-
},
|
|
520
|
-
primitive: { cullMode: "front" },
|
|
412
|
+
shaderModule,
|
|
413
|
+
vertexBuffers: fullVertexBuffers,
|
|
414
|
+
fragmentTarget: standardBlend,
|
|
415
|
+
cullMode: "front",
|
|
521
416
|
depthStencil: {
|
|
522
417
|
format: "depth24plus-stencil8",
|
|
523
|
-
depthWriteEnabled: true,
|
|
524
|
-
depthCompare: "less-equal",
|
|
525
|
-
depthBias: -0.00005,
|
|
418
|
+
depthWriteEnabled: true,
|
|
419
|
+
depthCompare: "less-equal",
|
|
420
|
+
depthBias: -0.00005,
|
|
526
421
|
depthBiasSlopeScale: 0.0,
|
|
527
422
|
depthBiasClamp: 0.0,
|
|
528
423
|
stencilFront: {
|
|
529
424
|
compare: "always",
|
|
530
425
|
failOp: "keep",
|
|
531
426
|
depthFailOp: "keep",
|
|
532
|
-
passOp: "replace",
|
|
427
|
+
passOp: "replace",
|
|
533
428
|
},
|
|
534
429
|
stencilBack: {
|
|
535
430
|
compare: "always",
|
|
@@ -538,7 +433,6 @@ export class Engine {
|
|
|
538
433
|
passOp: "replace",
|
|
539
434
|
},
|
|
540
435
|
},
|
|
541
|
-
multisample: { count: this.sampleCount },
|
|
542
436
|
});
|
|
543
437
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
544
438
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
@@ -585,103 +479,41 @@ export class Engine {
|
|
|
585
479
|
`,
|
|
586
480
|
});
|
|
587
481
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
588
|
-
this.hairDepthPipeline = this.
|
|
482
|
+
this.hairDepthPipeline = this.createRenderPipeline({
|
|
589
483
|
label: "hair depth pre-pass",
|
|
590
484
|
layout: mainPipelineLayout,
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
attributes: [
|
|
597
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
598
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
599
|
-
],
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
arrayStride: 4 * 2,
|
|
603
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
604
|
-
},
|
|
605
|
-
{
|
|
606
|
-
arrayStride: 4,
|
|
607
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
608
|
-
},
|
|
609
|
-
],
|
|
610
|
-
},
|
|
611
|
-
fragment: {
|
|
612
|
-
module: depthOnlyShaderModule,
|
|
613
|
-
entryPoint: "fs",
|
|
614
|
-
targets: [
|
|
615
|
-
{
|
|
616
|
-
format: this.presentationFormat,
|
|
617
|
-
writeMask: 0, // Disable all color writes - we only care about depth
|
|
618
|
-
},
|
|
619
|
-
],
|
|
485
|
+
shaderModule: depthOnlyShaderModule,
|
|
486
|
+
vertexBuffers: depthOnlyVertexBuffers,
|
|
487
|
+
fragmentTarget: {
|
|
488
|
+
format: this.presentationFormat,
|
|
489
|
+
writeMask: 0,
|
|
620
490
|
},
|
|
621
|
-
|
|
491
|
+
fragmentEntryPoint: "fs",
|
|
492
|
+
cullMode: "front",
|
|
622
493
|
depthStencil: {
|
|
623
494
|
format: "depth24plus-stencil8",
|
|
624
495
|
depthWriteEnabled: true,
|
|
625
|
-
depthCompare: "less-equal",
|
|
496
|
+
depthCompare: "less-equal",
|
|
626
497
|
depthBias: 0.0,
|
|
627
498
|
depthBiasSlopeScale: 0.0,
|
|
628
499
|
depthBiasClamp: 0.0,
|
|
629
500
|
},
|
|
630
|
-
multisample: { count: this.sampleCount },
|
|
631
501
|
});
|
|
632
502
|
// Hair pipelines for rendering over eyes vs non-eyes (only differ in stencil compare mode)
|
|
633
503
|
const createHairPipeline = (isOverEyes) => {
|
|
634
|
-
return this.
|
|
504
|
+
return this.createRenderPipeline({
|
|
635
505
|
label: `hair pipeline (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
636
506
|
layout: mainPipelineLayout,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
arrayStride: 8 * 4,
|
|
642
|
-
attributes: [
|
|
643
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
644
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
645
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
646
|
-
],
|
|
647
|
-
},
|
|
648
|
-
{
|
|
649
|
-
arrayStride: 4 * 2,
|
|
650
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
arrayStride: 4,
|
|
654
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
655
|
-
},
|
|
656
|
-
],
|
|
657
|
-
},
|
|
658
|
-
fragment: {
|
|
659
|
-
module: shaderModule,
|
|
660
|
-
targets: [
|
|
661
|
-
{
|
|
662
|
-
format: this.presentationFormat,
|
|
663
|
-
blend: {
|
|
664
|
-
color: {
|
|
665
|
-
srcFactor: "src-alpha",
|
|
666
|
-
dstFactor: "one-minus-src-alpha",
|
|
667
|
-
operation: "add",
|
|
668
|
-
},
|
|
669
|
-
alpha: {
|
|
670
|
-
srcFactor: "one",
|
|
671
|
-
dstFactor: "one-minus-src-alpha",
|
|
672
|
-
operation: "add",
|
|
673
|
-
},
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
|
-
],
|
|
677
|
-
},
|
|
678
|
-
primitive: { cullMode: "front" },
|
|
507
|
+
shaderModule,
|
|
508
|
+
vertexBuffers: fullVertexBuffers,
|
|
509
|
+
fragmentTarget: standardBlend,
|
|
510
|
+
cullMode: "front",
|
|
679
511
|
depthStencil: {
|
|
680
512
|
format: "depth24plus-stencil8",
|
|
681
|
-
depthWriteEnabled: false,
|
|
682
|
-
depthCompare: "less-equal",
|
|
513
|
+
depthWriteEnabled: false,
|
|
514
|
+
depthCompare: "less-equal",
|
|
683
515
|
stencilFront: {
|
|
684
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
516
|
+
compare: isOverEyes ? "equal" : "not-equal",
|
|
685
517
|
failOp: "keep",
|
|
686
518
|
depthFailOp: "keep",
|
|
687
519
|
passOp: "keep",
|
|
@@ -693,50 +525,11 @@ export class Engine {
|
|
|
693
525
|
passOp: "keep",
|
|
694
526
|
},
|
|
695
527
|
},
|
|
696
|
-
multisample: { count: this.sampleCount },
|
|
697
528
|
});
|
|
698
529
|
};
|
|
699
530
|
this.hairPipelineOverEyes = createHairPipeline(true);
|
|
700
531
|
this.hairPipelineOverNonEyes = createHairPipeline(false);
|
|
701
532
|
}
|
|
702
|
-
// Create compute shader for skin matrix computation
|
|
703
|
-
createSkinMatrixComputePipeline() {
|
|
704
|
-
const computeShader = this.device.createShaderModule({
|
|
705
|
-
label: "skin matrix compute",
|
|
706
|
-
code: /* wgsl */ `
|
|
707
|
-
struct BoneCountUniform {
|
|
708
|
-
count: u32,
|
|
709
|
-
_padding1: u32,
|
|
710
|
-
_padding2: u32,
|
|
711
|
-
_padding3: u32,
|
|
712
|
-
_padding4: vec4<u32>,
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
716
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
717
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
718
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
719
|
-
|
|
720
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
721
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
722
|
-
let boneIndex = globalId.x;
|
|
723
|
-
if (boneIndex >= boneCount.count) {
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
let worldMat = worldMatrices[boneIndex];
|
|
727
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
728
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
729
|
-
}
|
|
730
|
-
`,
|
|
731
|
-
});
|
|
732
|
-
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
733
|
-
label: "skin matrix compute pipeline",
|
|
734
|
-
layout: "auto",
|
|
735
|
-
compute: {
|
|
736
|
-
module: computeShader,
|
|
737
|
-
},
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
533
|
// Create bloom post-processing pipelines
|
|
741
534
|
createBloomPipelines() {
|
|
742
535
|
// Bloom extraction shader (extracts bright areas)
|
|
@@ -1119,128 +912,24 @@ export class Engine {
|
|
|
1119
912
|
this.lightData[3] = 0.0; // Padding for vec3f alignment
|
|
1120
913
|
}
|
|
1121
914
|
async loadAnimation(url) {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (this.currentModel) {
|
|
1126
|
-
const initialPose = this.player.getPoseAtTime(0);
|
|
1127
|
-
this.applyPose(initialPose);
|
|
1128
|
-
// Reset bones without time 0 keyframes
|
|
1129
|
-
const skeleton = this.currentModel.getSkeleton();
|
|
1130
|
-
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1131
|
-
const bonesToReset = [];
|
|
1132
|
-
for (const bone of skeleton.bones) {
|
|
1133
|
-
if (!bonesWithPose.has(bone.name)) {
|
|
1134
|
-
bonesToReset.push(bone.name);
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
if (bonesToReset.length > 0) {
|
|
1138
|
-
const identityQuat = new Quat(0, 0, 0, 1);
|
|
1139
|
-
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1140
|
-
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1141
|
-
}
|
|
1142
|
-
// Update model pose and physics
|
|
1143
|
-
this.currentModel.evaluatePose();
|
|
1144
|
-
if (this.physics) {
|
|
1145
|
-
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1146
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1147
|
-
// Upload matrices immediately
|
|
1148
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1149
|
-
const encoder = this.device.createCommandEncoder();
|
|
1150
|
-
this.computeSkinMatrices(encoder);
|
|
1151
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
915
|
+
if (!this.currentModel)
|
|
916
|
+
return;
|
|
917
|
+
await this.currentModel.loadVmd(url);
|
|
1154
918
|
}
|
|
1155
919
|
playAnimation() {
|
|
1156
|
-
|
|
1157
|
-
return;
|
|
1158
|
-
const wasPaused = this.player.isPausedState;
|
|
1159
|
-
const wasPlaying = this.player.isPlayingState;
|
|
1160
|
-
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1161
|
-
if (!wasPlaying && !wasPaused) {
|
|
1162
|
-
// Get initial pose at time 0
|
|
1163
|
-
const initialPose = this.player.getPoseAtTime(0);
|
|
1164
|
-
this.applyPose(initialPose);
|
|
1165
|
-
// Reset bones without time 0 keyframes
|
|
1166
|
-
const skeleton = this.currentModel.getSkeleton();
|
|
1167
|
-
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1168
|
-
const bonesToReset = [];
|
|
1169
|
-
for (const bone of skeleton.bones) {
|
|
1170
|
-
if (!bonesWithPose.has(bone.name)) {
|
|
1171
|
-
bonesToReset.push(bone.name);
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
if (bonesToReset.length > 0) {
|
|
1175
|
-
const identityQuat = new Quat(0, 0, 0, 1);
|
|
1176
|
-
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1177
|
-
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1178
|
-
}
|
|
1179
|
-
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1180
|
-
if (this.physics) {
|
|
1181
|
-
this.currentModel.evaluatePose();
|
|
1182
|
-
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1183
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1184
|
-
// Upload matrices immediately so next frame shows correct pose
|
|
1185
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1186
|
-
const encoder = this.device.createCommandEncoder();
|
|
1187
|
-
this.computeSkinMatrices(encoder);
|
|
1188
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
// Start playback (or resume if paused)
|
|
1192
|
-
this.player.play();
|
|
920
|
+
this.currentModel?.playAnimation();
|
|
1193
921
|
}
|
|
1194
922
|
stopAnimation() {
|
|
1195
|
-
this.
|
|
923
|
+
this.currentModel?.stopAnimation();
|
|
1196
924
|
}
|
|
1197
925
|
pauseAnimation() {
|
|
1198
|
-
this.
|
|
926
|
+
this.currentModel?.pauseAnimation();
|
|
1199
927
|
}
|
|
1200
928
|
seekAnimation(time) {
|
|
1201
|
-
|
|
1202
|
-
return;
|
|
1203
|
-
this.player.seek(time);
|
|
1204
|
-
// Immediately apply pose at seeked time
|
|
1205
|
-
const pose = this.player.getPoseAtTime(time);
|
|
1206
|
-
this.applyPose(pose);
|
|
1207
|
-
// Update model pose and physics
|
|
1208
|
-
this.currentModel.evaluatePose();
|
|
1209
|
-
if (this.physics) {
|
|
1210
|
-
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1211
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1212
|
-
// Upload matrices immediately
|
|
1213
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1214
|
-
const encoder = this.device.createCommandEncoder();
|
|
1215
|
-
this.computeSkinMatrices(encoder);
|
|
1216
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1217
|
-
}
|
|
929
|
+
this.currentModel?.seekAnimation(time);
|
|
1218
930
|
}
|
|
1219
931
|
getAnimationProgress() {
|
|
1220
|
-
return this.
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Apply animation pose to model
|
|
1224
|
-
*/
|
|
1225
|
-
applyPose(pose) {
|
|
1226
|
-
if (!this.currentModel)
|
|
1227
|
-
return;
|
|
1228
|
-
// Apply bone rotations
|
|
1229
|
-
if (pose.boneRotations.size > 0) {
|
|
1230
|
-
const boneNames = Array.from(pose.boneRotations.keys());
|
|
1231
|
-
const rotations = Array.from(pose.boneRotations.values());
|
|
1232
|
-
this.rotateBones(boneNames, rotations, 0);
|
|
1233
|
-
}
|
|
1234
|
-
// Apply bone translations
|
|
1235
|
-
if (pose.boneTranslations.size > 0) {
|
|
1236
|
-
const boneNames = Array.from(pose.boneTranslations.keys());
|
|
1237
|
-
const translations = Array.from(pose.boneTranslations.values());
|
|
1238
|
-
this.moveBones(boneNames, translations, 0);
|
|
1239
|
-
}
|
|
1240
|
-
// Apply morph weights
|
|
1241
|
-
for (const [morphName, weight] of pose.morphWeights.entries()) {
|
|
1242
|
-
this.setMorphWeight(morphName, weight, 0);
|
|
1243
|
-
}
|
|
932
|
+
return this.currentModel?.getAnimationProgress() ?? { current: 0, duration: 0, percentage: 0 };
|
|
1244
933
|
}
|
|
1245
934
|
getStats() {
|
|
1246
935
|
return { ...this.stats };
|
|
@@ -1280,7 +969,6 @@ export class Engine {
|
|
|
1280
969
|
const dir = pathParts.join("/") + "/";
|
|
1281
970
|
this.modelDir = dir;
|
|
1282
971
|
const model = await PmxLoader.load(path);
|
|
1283
|
-
this.physics = new Physics(model.getRigidbodies(), model.getJoints());
|
|
1284
972
|
await this.setupModelBuffers(model);
|
|
1285
973
|
}
|
|
1286
974
|
rotateBones(bones, rotations, durationMs) {
|
|
@@ -1335,12 +1023,7 @@ export class Engine {
|
|
|
1335
1023
|
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1336
1024
|
label: "skin matrices",
|
|
1337
1025
|
size: Math.max(256, matrixSize),
|
|
1338
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
|
|
1339
|
-
});
|
|
1340
|
-
this.worldMatrixBuffer = this.device.createBuffer({
|
|
1341
|
-
label: "world matrices",
|
|
1342
|
-
size: Math.max(256, matrixSize),
|
|
1343
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1026
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1344
1027
|
});
|
|
1345
1028
|
this.inverseBindMatrixBuffer = this.device.createBuffer({
|
|
1346
1029
|
label: "inverse bind matrices",
|
|
@@ -1349,25 +1032,6 @@ export class Engine {
|
|
|
1349
1032
|
});
|
|
1350
1033
|
const invBindMatrices = skeleton.inverseBindMatrices;
|
|
1351
1034
|
this.device.queue.writeBuffer(this.inverseBindMatrixBuffer, 0, invBindMatrices.buffer, invBindMatrices.byteOffset, invBindMatrices.byteLength);
|
|
1352
|
-
this.boneCountBuffer = this.device.createBuffer({
|
|
1353
|
-
label: "bone count uniform",
|
|
1354
|
-
size: 32, // Minimum uniform buffer size is 32 bytes
|
|
1355
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1356
|
-
});
|
|
1357
|
-
const boneCountData = new Uint32Array(8); // 32 bytes total
|
|
1358
|
-
boneCountData[0] = boneCount;
|
|
1359
|
-
this.device.queue.writeBuffer(this.boneCountBuffer, 0, boneCountData);
|
|
1360
|
-
this.createSkinMatrixComputePipeline();
|
|
1361
|
-
// Create compute bind group once (reused every frame)
|
|
1362
|
-
this.skinMatrixComputeBindGroup = this.device.createBindGroup({
|
|
1363
|
-
layout: this.skinMatrixComputePipeline.getBindGroupLayout(0),
|
|
1364
|
-
entries: [
|
|
1365
|
-
{ binding: 0, resource: { buffer: this.boneCountBuffer } },
|
|
1366
|
-
{ binding: 1, resource: { buffer: this.worldMatrixBuffer } },
|
|
1367
|
-
{ binding: 2, resource: { buffer: this.inverseBindMatrixBuffer } },
|
|
1368
|
-
{ binding: 3, resource: { buffer: this.skinMatrixBuffer } },
|
|
1369
|
-
],
|
|
1370
|
-
});
|
|
1371
1035
|
const indices = model.getIndices();
|
|
1372
1036
|
if (indices) {
|
|
1373
1037
|
this.indexBuffer = this.device.createBuffer({
|
|
@@ -1396,15 +1060,7 @@ export class Engine {
|
|
|
1396
1060
|
const texture = await this.createTextureFromPath(path);
|
|
1397
1061
|
return texture;
|
|
1398
1062
|
};
|
|
1399
|
-
this.
|
|
1400
|
-
this.eyeDraws = [];
|
|
1401
|
-
this.hairDrawsOverEyes = [];
|
|
1402
|
-
this.hairDrawsOverNonEyes = [];
|
|
1403
|
-
this.transparentDraws = [];
|
|
1404
|
-
this.opaqueOutlineDraws = [];
|
|
1405
|
-
this.eyeOutlineDraws = [];
|
|
1406
|
-
this.hairOutlineDraws = [];
|
|
1407
|
-
this.transparentOutlineDraws = [];
|
|
1063
|
+
this.drawCalls = [];
|
|
1408
1064
|
let currentIndexOffset = 0;
|
|
1409
1065
|
for (const mat of materials) {
|
|
1410
1066
|
const indexCount = mat.vertexCount;
|
|
@@ -1429,51 +1085,48 @@ export class Engine {
|
|
|
1429
1085
|
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1430
1086
|
],
|
|
1431
1087
|
});
|
|
1432
|
-
|
|
1433
|
-
if (
|
|
1434
|
-
|
|
1088
|
+
if (indexCount > 0) {
|
|
1089
|
+
if (mat.isEye) {
|
|
1090
|
+
this.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup });
|
|
1435
1091
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const bindGroupOverEyes = createHairBindGroup(true);
|
|
1458
|
-
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1459
|
-
if (indexCount > 0) {
|
|
1460
|
-
this.hairDrawsOverEyes.push({
|
|
1092
|
+
else if (mat.isHair) {
|
|
1093
|
+
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
1094
|
+
const createHairBindGroup = (isOverEyes) => {
|
|
1095
|
+
const buffer = this.createMaterialUniformBuffer(`${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`, materialAlpha, isOverEyes ? 1.0 : 0.0);
|
|
1096
|
+
return this.device.createBindGroup({
|
|
1097
|
+
label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
1098
|
+
layout: this.mainBindGroupLayout,
|
|
1099
|
+
entries: [
|
|
1100
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1101
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1102
|
+
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1103
|
+
{ binding: 3, resource: this.materialSampler },
|
|
1104
|
+
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1105
|
+
{ binding: 5, resource: { buffer: buffer } },
|
|
1106
|
+
],
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
const bindGroupOverEyes = createHairBindGroup(true);
|
|
1110
|
+
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1111
|
+
this.drawCalls.push({
|
|
1112
|
+
type: "hair-over-eyes",
|
|
1461
1113
|
count: indexCount,
|
|
1462
1114
|
firstIndex: currentIndexOffset,
|
|
1463
1115
|
bindGroup: bindGroupOverEyes,
|
|
1464
1116
|
});
|
|
1465
|
-
this.
|
|
1117
|
+
this.drawCalls.push({
|
|
1118
|
+
type: "hair-over-non-eyes",
|
|
1466
1119
|
count: indexCount,
|
|
1467
1120
|
firstIndex: currentIndexOffset,
|
|
1468
1121
|
bindGroup: bindGroupOverNonEyes,
|
|
1469
1122
|
});
|
|
1470
1123
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1124
|
+
else if (isTransparent) {
|
|
1125
|
+
this.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup });
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
this.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup });
|
|
1129
|
+
}
|
|
1477
1130
|
}
|
|
1478
1131
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1479
1132
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
@@ -1498,14 +1151,19 @@ export class Engine {
|
|
|
1498
1151
|
],
|
|
1499
1152
|
});
|
|
1500
1153
|
if (indexCount > 0) {
|
|
1501
|
-
const
|
|
1502
|
-
?
|
|
1154
|
+
const outlineType = mat.isEye
|
|
1155
|
+
? "eye-outline"
|
|
1503
1156
|
: mat.isHair
|
|
1504
|
-
?
|
|
1157
|
+
? "hair-outline"
|
|
1505
1158
|
: isTransparent
|
|
1506
|
-
?
|
|
1507
|
-
:
|
|
1508
|
-
|
|
1159
|
+
? "transparent-outline"
|
|
1160
|
+
: "opaque-outline";
|
|
1161
|
+
this.drawCalls.push({
|
|
1162
|
+
type: outlineType,
|
|
1163
|
+
count: indexCount,
|
|
1164
|
+
firstIndex: currentIndexOffset,
|
|
1165
|
+
bindGroup: outlineBindGroup,
|
|
1166
|
+
});
|
|
1509
1167
|
}
|
|
1510
1168
|
}
|
|
1511
1169
|
currentIndexOffset += indexCount;
|
|
@@ -1560,47 +1218,50 @@ export class Engine {
|
|
|
1560
1218
|
renderEyes(pass) {
|
|
1561
1219
|
pass.setPipeline(this.eyePipeline);
|
|
1562
1220
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1563
|
-
for (const draw of this.
|
|
1564
|
-
|
|
1565
|
-
|
|
1221
|
+
for (const draw of this.drawCalls) {
|
|
1222
|
+
if (draw.type === "eye") {
|
|
1223
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1224
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1225
|
+
}
|
|
1566
1226
|
}
|
|
1567
1227
|
}
|
|
1568
1228
|
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1569
1229
|
renderHair(pass) {
|
|
1570
1230
|
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1571
|
-
const hasHair = this.
|
|
1231
|
+
const hasHair = this.drawCalls.some((d) => d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes");
|
|
1572
1232
|
if (hasHair) {
|
|
1573
1233
|
pass.setPipeline(this.hairDepthPipeline);
|
|
1574
|
-
for (const draw of this.
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1580
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1234
|
+
for (const draw of this.drawCalls) {
|
|
1235
|
+
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
1236
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1237
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1238
|
+
}
|
|
1581
1239
|
}
|
|
1582
1240
|
}
|
|
1583
1241
|
// Hair shading (split by stencil for transparency over eyes)
|
|
1584
|
-
|
|
1242
|
+
const hairOverEyes = this.drawCalls.filter((d) => d.type === "hair-over-eyes");
|
|
1243
|
+
if (hairOverEyes.length > 0) {
|
|
1585
1244
|
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1586
1245
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1587
|
-
for (const draw of
|
|
1246
|
+
for (const draw of hairOverEyes) {
|
|
1588
1247
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1589
1248
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1590
1249
|
}
|
|
1591
1250
|
}
|
|
1592
|
-
|
|
1251
|
+
const hairOverNonEyes = this.drawCalls.filter((d) => d.type === "hair-over-non-eyes");
|
|
1252
|
+
if (hairOverNonEyes.length > 0) {
|
|
1593
1253
|
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1594
1254
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1595
|
-
for (const draw of
|
|
1255
|
+
for (const draw of hairOverNonEyes) {
|
|
1596
1256
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1597
1257
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1598
1258
|
}
|
|
1599
1259
|
}
|
|
1600
1260
|
// Hair outlines
|
|
1601
|
-
|
|
1261
|
+
const hairOutlines = this.drawCalls.filter((d) => d.type === "hair-outline");
|
|
1262
|
+
if (hairOutlines.length > 0) {
|
|
1602
1263
|
pass.setPipeline(this.hairOutlinePipeline);
|
|
1603
|
-
for (const draw of
|
|
1264
|
+
for (const draw of hairOutlines) {
|
|
1604
1265
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1605
1266
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1606
1267
|
}
|
|
@@ -1614,18 +1275,10 @@ export class Engine {
|
|
|
1614
1275
|
this.lastFrameTime = currentTime;
|
|
1615
1276
|
this.updateCameraUniforms();
|
|
1616
1277
|
this.updateRenderTarget();
|
|
1617
|
-
//
|
|
1618
|
-
if (this.hasAnimation && this.currentModel) {
|
|
1619
|
-
const pose = this.player.update(currentTime);
|
|
1620
|
-
if (pose) {
|
|
1621
|
-
this.applyPose(pose);
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
// Update model pose first (this may update morph weights via tweens)
|
|
1625
|
-
// We need to do this before creating the encoder to ensure vertex buffer is ready
|
|
1278
|
+
// Update model (handles tweens, animation, physics, IK, and skin matrices)
|
|
1626
1279
|
if (this.currentModel) {
|
|
1627
|
-
const
|
|
1628
|
-
if (
|
|
1280
|
+
const verticesChanged = this.currentModel.update(deltaTime);
|
|
1281
|
+
if (verticesChanged) {
|
|
1629
1282
|
this.vertexBufferNeedsUpdate = true;
|
|
1630
1283
|
}
|
|
1631
1284
|
}
|
|
@@ -1634,9 +1287,10 @@ export class Engine {
|
|
|
1634
1287
|
this.updateVertexBuffer();
|
|
1635
1288
|
this.vertexBufferNeedsUpdate = false;
|
|
1636
1289
|
}
|
|
1637
|
-
//
|
|
1290
|
+
// Update skin matrices buffer
|
|
1291
|
+
this.updateSkinMatrices();
|
|
1292
|
+
// Use single encoder for render
|
|
1638
1293
|
const encoder = this.device.createCommandEncoder();
|
|
1639
|
-
this.updateModelPose(deltaTime, encoder);
|
|
1640
1294
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1641
1295
|
if (this.currentModel) {
|
|
1642
1296
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
@@ -1645,9 +1299,11 @@ export class Engine {
|
|
|
1645
1299
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1646
1300
|
// Pass 1: Opaque
|
|
1647
1301
|
pass.setPipeline(this.modelPipeline);
|
|
1648
|
-
for (const draw of this.
|
|
1649
|
-
|
|
1650
|
-
|
|
1302
|
+
for (const draw of this.drawCalls) {
|
|
1303
|
+
if (draw.type === "opaque") {
|
|
1304
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1305
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1306
|
+
}
|
|
1651
1307
|
}
|
|
1652
1308
|
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1653
1309
|
this.renderEyes(pass);
|
|
@@ -1656,9 +1312,11 @@ export class Engine {
|
|
|
1656
1312
|
this.renderHair(pass);
|
|
1657
1313
|
// Pass 4: Transparent
|
|
1658
1314
|
pass.setPipeline(this.modelPipeline);
|
|
1659
|
-
for (const draw of this.
|
|
1660
|
-
|
|
1661
|
-
|
|
1315
|
+
for (const draw of this.drawCalls) {
|
|
1316
|
+
if (draw.type === "transparent") {
|
|
1317
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1318
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1319
|
+
}
|
|
1662
1320
|
}
|
|
1663
1321
|
this.drawOutlines(pass, true);
|
|
1664
1322
|
}
|
|
@@ -1775,31 +1433,20 @@ export class Engine {
|
|
|
1775
1433
|
colorAttachment.view = this.sceneRenderTextureView;
|
|
1776
1434
|
}
|
|
1777
1435
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1784
|
-
}
|
|
1785
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1786
|
-
this.computeSkinMatrices(encoder);
|
|
1787
|
-
}
|
|
1788
|
-
computeSkinMatrices(encoder) {
|
|
1789
|
-
const boneCount = this.currentModel.getSkeleton().bones.length;
|
|
1790
|
-
const workgroupCount = Math.ceil(boneCount / this.COMPUTE_WORKGROUP_SIZE);
|
|
1791
|
-
const pass = encoder.beginComputePass();
|
|
1792
|
-
pass.setPipeline(this.skinMatrixComputePipeline);
|
|
1793
|
-
pass.setBindGroup(0, this.skinMatrixComputeBindGroup);
|
|
1794
|
-
pass.dispatchWorkgroups(workgroupCount);
|
|
1795
|
-
pass.end();
|
|
1436
|
+
updateSkinMatrices() {
|
|
1437
|
+
if (!this.currentModel || !this.skinMatrixBuffer)
|
|
1438
|
+
return;
|
|
1439
|
+
const skinMatrices = this.currentModel.getSkinMatrices();
|
|
1440
|
+
this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, skinMatrices.buffer, skinMatrices.byteOffset, skinMatrices.byteLength);
|
|
1796
1441
|
}
|
|
1797
1442
|
drawOutlines(pass, transparent) {
|
|
1798
1443
|
pass.setPipeline(this.outlinePipeline);
|
|
1799
|
-
const
|
|
1800
|
-
for (const draw of
|
|
1801
|
-
|
|
1802
|
-
|
|
1444
|
+
const outlineType = transparent ? "transparent-outline" : "opaque-outline";
|
|
1445
|
+
for (const draw of this.drawCalls) {
|
|
1446
|
+
if (draw.type === outlineType) {
|
|
1447
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1448
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1449
|
+
}
|
|
1803
1450
|
}
|
|
1804
1451
|
}
|
|
1805
1452
|
updateStats(frameTime) {
|