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/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,9 +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
|
-
this.animationStartTime = 0; // Track when animation first started (for A-pose prevention)
|
|
54
39
|
this.canvas = canvas;
|
|
55
40
|
if (options) {
|
|
56
41
|
this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
|
|
@@ -85,6 +70,26 @@ export class Engine {
|
|
|
85
70
|
this.createBloomPipelines();
|
|
86
71
|
this.setupResize();
|
|
87
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
|
+
}
|
|
88
93
|
createPipelines() {
|
|
89
94
|
this.materialSampler = this.device.createSampler({
|
|
90
95
|
magFilter: "linear",
|
|
@@ -92,6 +97,74 @@ export class Engine {
|
|
|
92
97
|
addressModeU: "repeat",
|
|
93
98
|
addressModeV: "repeat",
|
|
94
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
|
+
};
|
|
95
168
|
const shaderModule = this.device.createShaderModule({
|
|
96
169
|
label: "model shaders",
|
|
97
170
|
code: /* wgsl */ `
|
|
@@ -205,59 +278,18 @@ export class Engine {
|
|
|
205
278
|
label: "main pipeline layout",
|
|
206
279
|
bindGroupLayouts: [this.mainBindGroupLayout],
|
|
207
280
|
});
|
|
208
|
-
this.modelPipeline = this.
|
|
281
|
+
this.modelPipeline = this.createRenderPipeline({
|
|
209
282
|
label: "model pipeline",
|
|
210
283
|
layout: mainPipelineLayout,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
arrayStride: 8 * 4,
|
|
216
|
-
attributes: [
|
|
217
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
218
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
219
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
220
|
-
],
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
arrayStride: 4 * 2,
|
|
224
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
arrayStride: 4,
|
|
228
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
},
|
|
232
|
-
fragment: {
|
|
233
|
-
module: shaderModule,
|
|
234
|
-
targets: [
|
|
235
|
-
{
|
|
236
|
-
format: this.presentationFormat,
|
|
237
|
-
blend: {
|
|
238
|
-
color: {
|
|
239
|
-
srcFactor: "src-alpha",
|
|
240
|
-
dstFactor: "one-minus-src-alpha",
|
|
241
|
-
operation: "add",
|
|
242
|
-
},
|
|
243
|
-
alpha: {
|
|
244
|
-
srcFactor: "one",
|
|
245
|
-
dstFactor: "one-minus-src-alpha",
|
|
246
|
-
operation: "add",
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
},
|
|
252
|
-
primitive: { cullMode: "none" },
|
|
284
|
+
shaderModule,
|
|
285
|
+
vertexBuffers: fullVertexBuffers,
|
|
286
|
+
fragmentTarget: standardBlend,
|
|
287
|
+
cullMode: "none",
|
|
253
288
|
depthStencil: {
|
|
254
289
|
format: "depth24plus-stencil8",
|
|
255
290
|
depthWriteEnabled: true,
|
|
256
291
|
depthCompare: "less-equal",
|
|
257
292
|
},
|
|
258
|
-
multisample: {
|
|
259
|
-
count: this.sampleCount,
|
|
260
|
-
},
|
|
261
293
|
});
|
|
262
294
|
// Create bind group layout for outline pipelines
|
|
263
295
|
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -343,194 +375,56 @@ export class Engine {
|
|
|
343
375
|
}
|
|
344
376
|
`,
|
|
345
377
|
});
|
|
346
|
-
this.outlinePipeline = this.
|
|
378
|
+
this.outlinePipeline = this.createRenderPipeline({
|
|
347
379
|
label: "outline pipeline",
|
|
348
380
|
layout: outlinePipelineLayout,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
arrayStride: 8 * 4,
|
|
354
|
-
attributes: [
|
|
355
|
-
{
|
|
356
|
-
shaderLocation: 0,
|
|
357
|
-
offset: 0,
|
|
358
|
-
format: "float32x3",
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
shaderLocation: 1,
|
|
362
|
-
offset: 3 * 4,
|
|
363
|
-
format: "float32x3",
|
|
364
|
-
},
|
|
365
|
-
],
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
arrayStride: 4 * 2,
|
|
369
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
arrayStride: 4,
|
|
373
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
},
|
|
377
|
-
fragment: {
|
|
378
|
-
module: outlineShaderModule,
|
|
379
|
-
targets: [
|
|
380
|
-
{
|
|
381
|
-
format: this.presentationFormat,
|
|
382
|
-
blend: {
|
|
383
|
-
color: {
|
|
384
|
-
srcFactor: "src-alpha",
|
|
385
|
-
dstFactor: "one-minus-src-alpha",
|
|
386
|
-
operation: "add",
|
|
387
|
-
},
|
|
388
|
-
alpha: {
|
|
389
|
-
srcFactor: "one",
|
|
390
|
-
dstFactor: "one-minus-src-alpha",
|
|
391
|
-
operation: "add",
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
],
|
|
396
|
-
},
|
|
397
|
-
primitive: {
|
|
398
|
-
cullMode: "back",
|
|
399
|
-
},
|
|
381
|
+
shaderModule: outlineShaderModule,
|
|
382
|
+
vertexBuffers: outlineVertexBuffers,
|
|
383
|
+
fragmentTarget: standardBlend,
|
|
384
|
+
cullMode: "back",
|
|
400
385
|
depthStencil: {
|
|
401
386
|
format: "depth24plus-stencil8",
|
|
402
387
|
depthWriteEnabled: true,
|
|
403
388
|
depthCompare: "less-equal",
|
|
404
389
|
},
|
|
405
|
-
multisample: {
|
|
406
|
-
count: this.sampleCount,
|
|
407
|
-
},
|
|
408
390
|
});
|
|
409
391
|
// Hair outline pipeline
|
|
410
|
-
this.hairOutlinePipeline = this.
|
|
392
|
+
this.hairOutlinePipeline = this.createRenderPipeline({
|
|
411
393
|
label: "hair outline pipeline",
|
|
412
394
|
layout: outlinePipelineLayout,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
arrayStride: 8 * 4,
|
|
418
|
-
attributes: [
|
|
419
|
-
{
|
|
420
|
-
shaderLocation: 0,
|
|
421
|
-
offset: 0,
|
|
422
|
-
format: "float32x3",
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
shaderLocation: 1,
|
|
426
|
-
offset: 3 * 4,
|
|
427
|
-
format: "float32x3",
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
arrayStride: 4 * 2,
|
|
433
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
arrayStride: 4,
|
|
437
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
438
|
-
},
|
|
439
|
-
],
|
|
440
|
-
},
|
|
441
|
-
fragment: {
|
|
442
|
-
module: outlineShaderModule,
|
|
443
|
-
targets: [
|
|
444
|
-
{
|
|
445
|
-
format: this.presentationFormat,
|
|
446
|
-
blend: {
|
|
447
|
-
color: {
|
|
448
|
-
srcFactor: "src-alpha",
|
|
449
|
-
dstFactor: "one-minus-src-alpha",
|
|
450
|
-
operation: "add",
|
|
451
|
-
},
|
|
452
|
-
alpha: {
|
|
453
|
-
srcFactor: "one",
|
|
454
|
-
dstFactor: "one-minus-src-alpha",
|
|
455
|
-
operation: "add",
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
],
|
|
460
|
-
},
|
|
461
|
-
primitive: {
|
|
462
|
-
cullMode: "back",
|
|
463
|
-
},
|
|
395
|
+
shaderModule: outlineShaderModule,
|
|
396
|
+
vertexBuffers: outlineVertexBuffers,
|
|
397
|
+
fragmentTarget: standardBlend,
|
|
398
|
+
cullMode: "back",
|
|
464
399
|
depthStencil: {
|
|
465
400
|
format: "depth24plus-stencil8",
|
|
466
|
-
depthWriteEnabled: false,
|
|
467
|
-
depthCompare: "less-equal",
|
|
468
|
-
depthBias: -0.0001,
|
|
401
|
+
depthWriteEnabled: false,
|
|
402
|
+
depthCompare: "less-equal",
|
|
403
|
+
depthBias: -0.0001,
|
|
469
404
|
depthBiasSlopeScale: 0.0,
|
|
470
405
|
depthBiasClamp: 0.0,
|
|
471
406
|
},
|
|
472
|
-
multisample: {
|
|
473
|
-
count: this.sampleCount,
|
|
474
|
-
},
|
|
475
407
|
});
|
|
476
408
|
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
477
|
-
this.eyePipeline = this.
|
|
409
|
+
this.eyePipeline = this.createRenderPipeline({
|
|
478
410
|
label: "eye overlay pipeline",
|
|
479
411
|
layout: mainPipelineLayout,
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
arrayStride: 8 * 4,
|
|
485
|
-
attributes: [
|
|
486
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
487
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
488
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
489
|
-
],
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
arrayStride: 4 * 2,
|
|
493
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
arrayStride: 4,
|
|
497
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
498
|
-
},
|
|
499
|
-
],
|
|
500
|
-
},
|
|
501
|
-
fragment: {
|
|
502
|
-
module: shaderModule,
|
|
503
|
-
targets: [
|
|
504
|
-
{
|
|
505
|
-
format: this.presentationFormat,
|
|
506
|
-
blend: {
|
|
507
|
-
color: {
|
|
508
|
-
srcFactor: "src-alpha",
|
|
509
|
-
dstFactor: "one-minus-src-alpha",
|
|
510
|
-
operation: "add",
|
|
511
|
-
},
|
|
512
|
-
alpha: {
|
|
513
|
-
srcFactor: "one",
|
|
514
|
-
dstFactor: "one-minus-src-alpha",
|
|
515
|
-
operation: "add",
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
],
|
|
520
|
-
},
|
|
521
|
-
primitive: { cullMode: "front" },
|
|
412
|
+
shaderModule,
|
|
413
|
+
vertexBuffers: fullVertexBuffers,
|
|
414
|
+
fragmentTarget: standardBlend,
|
|
415
|
+
cullMode: "front",
|
|
522
416
|
depthStencil: {
|
|
523
417
|
format: "depth24plus-stencil8",
|
|
524
|
-
depthWriteEnabled: true,
|
|
525
|
-
depthCompare: "less-equal",
|
|
526
|
-
depthBias: -0.00005,
|
|
418
|
+
depthWriteEnabled: true,
|
|
419
|
+
depthCompare: "less-equal",
|
|
420
|
+
depthBias: -0.00005,
|
|
527
421
|
depthBiasSlopeScale: 0.0,
|
|
528
422
|
depthBiasClamp: 0.0,
|
|
529
423
|
stencilFront: {
|
|
530
424
|
compare: "always",
|
|
531
425
|
failOp: "keep",
|
|
532
426
|
depthFailOp: "keep",
|
|
533
|
-
passOp: "replace",
|
|
427
|
+
passOp: "replace",
|
|
534
428
|
},
|
|
535
429
|
stencilBack: {
|
|
536
430
|
compare: "always",
|
|
@@ -539,7 +433,6 @@ export class Engine {
|
|
|
539
433
|
passOp: "replace",
|
|
540
434
|
},
|
|
541
435
|
},
|
|
542
|
-
multisample: { count: this.sampleCount },
|
|
543
436
|
});
|
|
544
437
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
545
438
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
@@ -586,103 +479,41 @@ export class Engine {
|
|
|
586
479
|
`,
|
|
587
480
|
});
|
|
588
481
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
589
|
-
this.hairDepthPipeline = this.
|
|
482
|
+
this.hairDepthPipeline = this.createRenderPipeline({
|
|
590
483
|
label: "hair depth pre-pass",
|
|
591
484
|
layout: mainPipelineLayout,
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
attributes: [
|
|
598
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
599
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
600
|
-
],
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
arrayStride: 4 * 2,
|
|
604
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
arrayStride: 4,
|
|
608
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
609
|
-
},
|
|
610
|
-
],
|
|
611
|
-
},
|
|
612
|
-
fragment: {
|
|
613
|
-
module: depthOnlyShaderModule,
|
|
614
|
-
entryPoint: "fs",
|
|
615
|
-
targets: [
|
|
616
|
-
{
|
|
617
|
-
format: this.presentationFormat,
|
|
618
|
-
writeMask: 0, // Disable all color writes - we only care about depth
|
|
619
|
-
},
|
|
620
|
-
],
|
|
485
|
+
shaderModule: depthOnlyShaderModule,
|
|
486
|
+
vertexBuffers: depthOnlyVertexBuffers,
|
|
487
|
+
fragmentTarget: {
|
|
488
|
+
format: this.presentationFormat,
|
|
489
|
+
writeMask: 0,
|
|
621
490
|
},
|
|
622
|
-
|
|
491
|
+
fragmentEntryPoint: "fs",
|
|
492
|
+
cullMode: "front",
|
|
623
493
|
depthStencil: {
|
|
624
494
|
format: "depth24plus-stencil8",
|
|
625
495
|
depthWriteEnabled: true,
|
|
626
|
-
depthCompare: "less-equal",
|
|
496
|
+
depthCompare: "less-equal",
|
|
627
497
|
depthBias: 0.0,
|
|
628
498
|
depthBiasSlopeScale: 0.0,
|
|
629
499
|
depthBiasClamp: 0.0,
|
|
630
500
|
},
|
|
631
|
-
multisample: { count: this.sampleCount },
|
|
632
501
|
});
|
|
633
502
|
// Hair pipelines for rendering over eyes vs non-eyes (only differ in stencil compare mode)
|
|
634
503
|
const createHairPipeline = (isOverEyes) => {
|
|
635
|
-
return this.
|
|
504
|
+
return this.createRenderPipeline({
|
|
636
505
|
label: `hair pipeline (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
637
506
|
layout: mainPipelineLayout,
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
arrayStride: 8 * 4,
|
|
643
|
-
attributes: [
|
|
644
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
645
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
646
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
647
|
-
],
|
|
648
|
-
},
|
|
649
|
-
{
|
|
650
|
-
arrayStride: 4 * 2,
|
|
651
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
arrayStride: 4,
|
|
655
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
656
|
-
},
|
|
657
|
-
],
|
|
658
|
-
},
|
|
659
|
-
fragment: {
|
|
660
|
-
module: shaderModule,
|
|
661
|
-
targets: [
|
|
662
|
-
{
|
|
663
|
-
format: this.presentationFormat,
|
|
664
|
-
blend: {
|
|
665
|
-
color: {
|
|
666
|
-
srcFactor: "src-alpha",
|
|
667
|
-
dstFactor: "one-minus-src-alpha",
|
|
668
|
-
operation: "add",
|
|
669
|
-
},
|
|
670
|
-
alpha: {
|
|
671
|
-
srcFactor: "one",
|
|
672
|
-
dstFactor: "one-minus-src-alpha",
|
|
673
|
-
operation: "add",
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
|
-
},
|
|
677
|
-
],
|
|
678
|
-
},
|
|
679
|
-
primitive: { cullMode: "front" },
|
|
507
|
+
shaderModule,
|
|
508
|
+
vertexBuffers: fullVertexBuffers,
|
|
509
|
+
fragmentTarget: standardBlend,
|
|
510
|
+
cullMode: "front",
|
|
680
511
|
depthStencil: {
|
|
681
512
|
format: "depth24plus-stencil8",
|
|
682
|
-
depthWriteEnabled: false,
|
|
683
|
-
depthCompare: "less-equal",
|
|
513
|
+
depthWriteEnabled: false,
|
|
514
|
+
depthCompare: "less-equal",
|
|
684
515
|
stencilFront: {
|
|
685
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
516
|
+
compare: isOverEyes ? "equal" : "not-equal",
|
|
686
517
|
failOp: "keep",
|
|
687
518
|
depthFailOp: "keep",
|
|
688
519
|
passOp: "keep",
|
|
@@ -694,50 +525,11 @@ export class Engine {
|
|
|
694
525
|
passOp: "keep",
|
|
695
526
|
},
|
|
696
527
|
},
|
|
697
|
-
multisample: { count: this.sampleCount },
|
|
698
528
|
});
|
|
699
529
|
};
|
|
700
530
|
this.hairPipelineOverEyes = createHairPipeline(true);
|
|
701
531
|
this.hairPipelineOverNonEyes = createHairPipeline(false);
|
|
702
532
|
}
|
|
703
|
-
// Create compute shader for skin matrix computation
|
|
704
|
-
createSkinMatrixComputePipeline() {
|
|
705
|
-
const computeShader = this.device.createShaderModule({
|
|
706
|
-
label: "skin matrix compute",
|
|
707
|
-
code: /* wgsl */ `
|
|
708
|
-
struct BoneCountUniform {
|
|
709
|
-
count: u32,
|
|
710
|
-
_padding1: u32,
|
|
711
|
-
_padding2: u32,
|
|
712
|
-
_padding3: u32,
|
|
713
|
-
_padding4: vec4<u32>,
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
717
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
718
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
719
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
720
|
-
|
|
721
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
722
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
723
|
-
let boneIndex = globalId.x;
|
|
724
|
-
if (boneIndex >= boneCount.count) {
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
let worldMat = worldMatrices[boneIndex];
|
|
728
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
729
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
730
|
-
}
|
|
731
|
-
`,
|
|
732
|
-
});
|
|
733
|
-
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
734
|
-
label: "skin matrix compute pipeline",
|
|
735
|
-
layout: "auto",
|
|
736
|
-
compute: {
|
|
737
|
-
module: computeShader,
|
|
738
|
-
},
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
533
|
// Create bloom post-processing pipelines
|
|
742
534
|
createBloomPipelines() {
|
|
743
535
|
// Bloom extraction shader (extracts bright areas)
|
|
@@ -1120,131 +912,24 @@ export class Engine {
|
|
|
1120
912
|
this.lightData[3] = 0.0; // Padding for vec3f alignment
|
|
1121
913
|
}
|
|
1122
914
|
async loadAnimation(url) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if (this.currentModel) {
|
|
1127
|
-
const initialPose = this.player.getPoseAtTime(0);
|
|
1128
|
-
this.applyPose(initialPose);
|
|
1129
|
-
// Reset bones without time 0 keyframes
|
|
1130
|
-
const skeleton = this.currentModel.getSkeleton();
|
|
1131
|
-
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1132
|
-
const bonesToReset = [];
|
|
1133
|
-
for (const bone of skeleton.bones) {
|
|
1134
|
-
if (!bonesWithPose.has(bone.name)) {
|
|
1135
|
-
bonesToReset.push(bone.name);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if (bonesToReset.length > 0) {
|
|
1139
|
-
const identityQuat = new Quat(0, 0, 0, 1);
|
|
1140
|
-
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1141
|
-
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1142
|
-
}
|
|
1143
|
-
// Update model pose and physics
|
|
1144
|
-
this.currentModel.evaluatePose();
|
|
1145
|
-
if (this.physics) {
|
|
1146
|
-
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1147
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1148
|
-
// Upload matrices immediately
|
|
1149
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1150
|
-
const encoder = this.device.createCommandEncoder();
|
|
1151
|
-
this.computeSkinMatrices(encoder);
|
|
1152
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
915
|
+
if (!this.currentModel)
|
|
916
|
+
return;
|
|
917
|
+
await this.currentModel.loadVmd(url);
|
|
1155
918
|
}
|
|
1156
919
|
playAnimation() {
|
|
1157
|
-
|
|
1158
|
-
return;
|
|
1159
|
-
const wasPaused = this.player.isPausedState();
|
|
1160
|
-
const wasPlaying = this.player.isPlayingState();
|
|
1161
|
-
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1162
|
-
if (!wasPlaying && !wasPaused) {
|
|
1163
|
-
// Get initial pose at time 0
|
|
1164
|
-
const initialPose = this.player.getPoseAtTime(0);
|
|
1165
|
-
this.applyPose(initialPose);
|
|
1166
|
-
// Reset bones without time 0 keyframes
|
|
1167
|
-
const skeleton = this.currentModel.getSkeleton();
|
|
1168
|
-
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1169
|
-
const bonesToReset = [];
|
|
1170
|
-
for (const bone of skeleton.bones) {
|
|
1171
|
-
if (!bonesWithPose.has(bone.name)) {
|
|
1172
|
-
bonesToReset.push(bone.name);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
if (bonesToReset.length > 0) {
|
|
1176
|
-
const identityQuat = new Quat(0, 0, 0, 1);
|
|
1177
|
-
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1178
|
-
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1179
|
-
}
|
|
1180
|
-
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1181
|
-
if (this.physics) {
|
|
1182
|
-
this.currentModel.evaluatePose();
|
|
1183
|
-
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1184
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1185
|
-
// Upload matrices immediately so next frame shows correct pose
|
|
1186
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1187
|
-
const encoder = this.device.createCommandEncoder();
|
|
1188
|
-
this.computeSkinMatrices(encoder);
|
|
1189
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
// Start playback (or resume if paused)
|
|
1193
|
-
this.player.play();
|
|
1194
|
-
if (this.animationStartTime === 0) {
|
|
1195
|
-
this.animationStartTime = performance.now();
|
|
1196
|
-
}
|
|
920
|
+
this.currentModel?.playAnimation();
|
|
1197
921
|
}
|
|
1198
922
|
stopAnimation() {
|
|
1199
|
-
this.
|
|
923
|
+
this.currentModel?.stopAnimation();
|
|
1200
924
|
}
|
|
1201
925
|
pauseAnimation() {
|
|
1202
|
-
this.
|
|
926
|
+
this.currentModel?.pauseAnimation();
|
|
1203
927
|
}
|
|
1204
928
|
seekAnimation(time) {
|
|
1205
|
-
|
|
1206
|
-
return;
|
|
1207
|
-
this.player.seek(time);
|
|
1208
|
-
// Immediately apply pose at seeked time
|
|
1209
|
-
const pose = this.player.getPoseAtTime(time);
|
|
1210
|
-
this.applyPose(pose);
|
|
1211
|
-
// Update model pose and physics
|
|
1212
|
-
this.currentModel.evaluatePose();
|
|
1213
|
-
if (this.physics) {
|
|
1214
|
-
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1215
|
-
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1216
|
-
// Upload matrices immediately
|
|
1217
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1218
|
-
const encoder = this.device.createCommandEncoder();
|
|
1219
|
-
this.computeSkinMatrices(encoder);
|
|
1220
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1221
|
-
}
|
|
929
|
+
this.currentModel?.seekAnimation(time);
|
|
1222
930
|
}
|
|
1223
931
|
getAnimationProgress() {
|
|
1224
|
-
return this.
|
|
1225
|
-
}
|
|
1226
|
-
/**
|
|
1227
|
-
* Apply animation pose to model
|
|
1228
|
-
*/
|
|
1229
|
-
applyPose(pose) {
|
|
1230
|
-
if (!this.currentModel)
|
|
1231
|
-
return;
|
|
1232
|
-
// Apply bone rotations
|
|
1233
|
-
if (pose.boneRotations.size > 0) {
|
|
1234
|
-
const boneNames = Array.from(pose.boneRotations.keys());
|
|
1235
|
-
const rotations = Array.from(pose.boneRotations.values());
|
|
1236
|
-
this.rotateBones(boneNames, rotations, 0);
|
|
1237
|
-
}
|
|
1238
|
-
// Apply bone translations
|
|
1239
|
-
if (pose.boneTranslations.size > 0) {
|
|
1240
|
-
const boneNames = Array.from(pose.boneTranslations.keys());
|
|
1241
|
-
const translations = Array.from(pose.boneTranslations.values());
|
|
1242
|
-
this.moveBones(boneNames, translations, 0);
|
|
1243
|
-
}
|
|
1244
|
-
// Apply morph weights
|
|
1245
|
-
for (const [morphName, weight] of pose.morphWeights.entries()) {
|
|
1246
|
-
this.setMorphWeight(morphName, weight, 0);
|
|
1247
|
-
}
|
|
932
|
+
return this.currentModel?.getAnimationProgress() ?? { current: 0, duration: 0, percentage: 0 };
|
|
1248
933
|
}
|
|
1249
934
|
getStats() {
|
|
1250
935
|
return { ...this.stats };
|
|
@@ -1284,7 +969,6 @@ export class Engine {
|
|
|
1284
969
|
const dir = pathParts.join("/") + "/";
|
|
1285
970
|
this.modelDir = dir;
|
|
1286
971
|
const model = await PmxLoader.load(path);
|
|
1287
|
-
this.physics = new Physics(model.getRigidbodies(), model.getJoints());
|
|
1288
972
|
await this.setupModelBuffers(model);
|
|
1289
973
|
}
|
|
1290
974
|
rotateBones(bones, rotations, durationMs) {
|
|
@@ -1339,12 +1023,7 @@ export class Engine {
|
|
|
1339
1023
|
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1340
1024
|
label: "skin matrices",
|
|
1341
1025
|
size: Math.max(256, matrixSize),
|
|
1342
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
|
|
1343
|
-
});
|
|
1344
|
-
this.worldMatrixBuffer = this.device.createBuffer({
|
|
1345
|
-
label: "world matrices",
|
|
1346
|
-
size: Math.max(256, matrixSize),
|
|
1347
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1026
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1348
1027
|
});
|
|
1349
1028
|
this.inverseBindMatrixBuffer = this.device.createBuffer({
|
|
1350
1029
|
label: "inverse bind matrices",
|
|
@@ -1353,25 +1032,6 @@ export class Engine {
|
|
|
1353
1032
|
});
|
|
1354
1033
|
const invBindMatrices = skeleton.inverseBindMatrices;
|
|
1355
1034
|
this.device.queue.writeBuffer(this.inverseBindMatrixBuffer, 0, invBindMatrices.buffer, invBindMatrices.byteOffset, invBindMatrices.byteLength);
|
|
1356
|
-
this.boneCountBuffer = this.device.createBuffer({
|
|
1357
|
-
label: "bone count uniform",
|
|
1358
|
-
size: 32, // Minimum uniform buffer size is 32 bytes
|
|
1359
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1360
|
-
});
|
|
1361
|
-
const boneCountData = new Uint32Array(8); // 32 bytes total
|
|
1362
|
-
boneCountData[0] = boneCount;
|
|
1363
|
-
this.device.queue.writeBuffer(this.boneCountBuffer, 0, boneCountData);
|
|
1364
|
-
this.createSkinMatrixComputePipeline();
|
|
1365
|
-
// Create compute bind group once (reused every frame)
|
|
1366
|
-
this.skinMatrixComputeBindGroup = this.device.createBindGroup({
|
|
1367
|
-
layout: this.skinMatrixComputePipeline.getBindGroupLayout(0),
|
|
1368
|
-
entries: [
|
|
1369
|
-
{ binding: 0, resource: { buffer: this.boneCountBuffer } },
|
|
1370
|
-
{ binding: 1, resource: { buffer: this.worldMatrixBuffer } },
|
|
1371
|
-
{ binding: 2, resource: { buffer: this.inverseBindMatrixBuffer } },
|
|
1372
|
-
{ binding: 3, resource: { buffer: this.skinMatrixBuffer } },
|
|
1373
|
-
],
|
|
1374
|
-
});
|
|
1375
1035
|
const indices = model.getIndices();
|
|
1376
1036
|
if (indices) {
|
|
1377
1037
|
this.indexBuffer = this.device.createBuffer({
|
|
@@ -1400,15 +1060,7 @@ export class Engine {
|
|
|
1400
1060
|
const texture = await this.createTextureFromPath(path);
|
|
1401
1061
|
return texture;
|
|
1402
1062
|
};
|
|
1403
|
-
this.
|
|
1404
|
-
this.eyeDraws = [];
|
|
1405
|
-
this.hairDrawsOverEyes = [];
|
|
1406
|
-
this.hairDrawsOverNonEyes = [];
|
|
1407
|
-
this.transparentDraws = [];
|
|
1408
|
-
this.opaqueOutlineDraws = [];
|
|
1409
|
-
this.eyeOutlineDraws = [];
|
|
1410
|
-
this.hairOutlineDraws = [];
|
|
1411
|
-
this.transparentOutlineDraws = [];
|
|
1063
|
+
this.drawCalls = [];
|
|
1412
1064
|
let currentIndexOffset = 0;
|
|
1413
1065
|
for (const mat of materials) {
|
|
1414
1066
|
const indexCount = mat.vertexCount;
|
|
@@ -1419,22 +1071,7 @@ export class Engine {
|
|
|
1419
1071
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1420
1072
|
const materialAlpha = mat.diffuse[3];
|
|
1421
1073
|
const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON;
|
|
1422
|
-
|
|
1423
|
-
const materialUniformData = new Float32Array(8);
|
|
1424
|
-
materialUniformData[0] = materialAlpha;
|
|
1425
|
-
materialUniformData[1] = 1.0; // alphaMultiplier: 1.0 for non-hair materials
|
|
1426
|
-
materialUniformData[2] = this.rimLightIntensity;
|
|
1427
|
-
materialUniformData[3] = 0.0; // _padding1
|
|
1428
|
-
materialUniformData[4] = 1.0; // rimColor.r
|
|
1429
|
-
materialUniformData[5] = 1.0; // rimColor.g
|
|
1430
|
-
materialUniformData[6] = 1.0; // rimColor.b
|
|
1431
|
-
materialUniformData[7] = 0.0; // isOverEyes
|
|
1432
|
-
const materialUniformBuffer = this.device.createBuffer({
|
|
1433
|
-
label: `material uniform: ${mat.name}`,
|
|
1434
|
-
size: materialUniformData.byteLength,
|
|
1435
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1436
|
-
});
|
|
1437
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData);
|
|
1074
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0);
|
|
1438
1075
|
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1439
1076
|
const bindGroup = this.device.createBindGroup({
|
|
1440
1077
|
label: `material bind group: ${mat.name}`,
|
|
@@ -1448,96 +1085,62 @@ export class Engine {
|
|
|
1448
1085
|
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1449
1086
|
],
|
|
1450
1087
|
});
|
|
1451
|
-
if (
|
|
1452
|
-
if (
|
|
1453
|
-
this.
|
|
1454
|
-
count: indexCount,
|
|
1455
|
-
firstIndex: currentIndexOffset,
|
|
1456
|
-
bindGroup,
|
|
1457
|
-
});
|
|
1088
|
+
if (indexCount > 0) {
|
|
1089
|
+
if (mat.isEye) {
|
|
1090
|
+
this.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup });
|
|
1458
1091
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
layout: this.mainBindGroupLayout,
|
|
1481
|
-
entries: [
|
|
1482
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1483
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1484
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1485
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1486
|
-
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1487
|
-
{ binding: 5, resource: { buffer: buffer } },
|
|
1488
|
-
],
|
|
1489
|
-
});
|
|
1490
|
-
};
|
|
1491
|
-
const bindGroupOverEyes = createHairBindGroup(true);
|
|
1492
|
-
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1493
|
-
if (indexCount > 0) {
|
|
1494
|
-
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",
|
|
1495
1113
|
count: indexCount,
|
|
1496
1114
|
firstIndex: currentIndexOffset,
|
|
1497
1115
|
bindGroup: bindGroupOverEyes,
|
|
1498
1116
|
});
|
|
1499
|
-
this.
|
|
1117
|
+
this.drawCalls.push({
|
|
1118
|
+
type: "hair-over-non-eyes",
|
|
1500
1119
|
count: indexCount,
|
|
1501
1120
|
firstIndex: currentIndexOffset,
|
|
1502
1121
|
bindGroup: bindGroupOverNonEyes,
|
|
1503
1122
|
});
|
|
1504
1123
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
if (indexCount > 0) {
|
|
1508
|
-
this.transparentDraws.push({
|
|
1509
|
-
count: indexCount,
|
|
1510
|
-
firstIndex: currentIndexOffset,
|
|
1511
|
-
bindGroup,
|
|
1512
|
-
});
|
|
1124
|
+
else if (isTransparent) {
|
|
1125
|
+
this.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup });
|
|
1513
1126
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
if (indexCount > 0) {
|
|
1517
|
-
this.opaqueDraws.push({
|
|
1518
|
-
count: indexCount,
|
|
1519
|
-
firstIndex: currentIndexOffset,
|
|
1520
|
-
bindGroup,
|
|
1521
|
-
});
|
|
1127
|
+
else {
|
|
1128
|
+
this.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup });
|
|
1522
1129
|
}
|
|
1523
1130
|
}
|
|
1524
1131
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1525
1132
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1526
|
-
const materialUniformData = new Float32Array(
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
size: materialUniformData.byteLength,
|
|
1538
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1539
|
-
});
|
|
1540
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData);
|
|
1133
|
+
const materialUniformData = new Float32Array([
|
|
1134
|
+
mat.edgeColor[0],
|
|
1135
|
+
mat.edgeColor[1],
|
|
1136
|
+
mat.edgeColor[2],
|
|
1137
|
+
mat.edgeColor[3],
|
|
1138
|
+
mat.edgeSize,
|
|
1139
|
+
0,
|
|
1140
|
+
0,
|
|
1141
|
+
0,
|
|
1142
|
+
]);
|
|
1143
|
+
const materialUniformBuffer = this.createUniformBuffer(`outline material uniform: ${mat.name}`, materialUniformData);
|
|
1541
1144
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1542
1145
|
label: `outline bind group: ${mat.name}`,
|
|
1543
1146
|
layout: this.outlineBindGroupLayout,
|
|
@@ -1548,39 +1151,38 @@ export class Engine {
|
|
|
1548
1151
|
],
|
|
1549
1152
|
});
|
|
1550
1153
|
if (indexCount > 0) {
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1565
|
-
else if (isTransparent) {
|
|
1566
|
-
this.transparentOutlineDraws.push({
|
|
1567
|
-
count: indexCount,
|
|
1568
|
-
firstIndex: currentIndexOffset,
|
|
1569
|
-
bindGroup: outlineBindGroup,
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
1572
|
-
else {
|
|
1573
|
-
this.opaqueOutlineDraws.push({
|
|
1574
|
-
count: indexCount,
|
|
1575
|
-
firstIndex: currentIndexOffset,
|
|
1576
|
-
bindGroup: outlineBindGroup,
|
|
1577
|
-
});
|
|
1578
|
-
}
|
|
1154
|
+
const outlineType = mat.isEye
|
|
1155
|
+
? "eye-outline"
|
|
1156
|
+
: mat.isHair
|
|
1157
|
+
? "hair-outline"
|
|
1158
|
+
: isTransparent
|
|
1159
|
+
? "transparent-outline"
|
|
1160
|
+
: "opaque-outline";
|
|
1161
|
+
this.drawCalls.push({
|
|
1162
|
+
type: outlineType,
|
|
1163
|
+
count: indexCount,
|
|
1164
|
+
firstIndex: currentIndexOffset,
|
|
1165
|
+
bindGroup: outlineBindGroup,
|
|
1166
|
+
});
|
|
1579
1167
|
}
|
|
1580
1168
|
}
|
|
1581
1169
|
currentIndexOffset += indexCount;
|
|
1582
1170
|
}
|
|
1583
1171
|
}
|
|
1172
|
+
createMaterialUniformBuffer(label, alpha, isOverEyes) {
|
|
1173
|
+
const data = new Float32Array(8);
|
|
1174
|
+
data.set([alpha, 1.0, this.rimLightIntensity, 0.0, 1.0, 1.0, 1.0, isOverEyes]);
|
|
1175
|
+
return this.createUniformBuffer(`material uniform: ${label}`, data);
|
|
1176
|
+
}
|
|
1177
|
+
createUniformBuffer(label, data) {
|
|
1178
|
+
const buffer = this.device.createBuffer({
|
|
1179
|
+
label,
|
|
1180
|
+
size: data.byteLength,
|
|
1181
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1182
|
+
});
|
|
1183
|
+
this.device.queue.writeBuffer(buffer, 0, data);
|
|
1184
|
+
return buffer;
|
|
1185
|
+
}
|
|
1584
1186
|
async createTextureFromPath(path) {
|
|
1585
1187
|
const cached = this.textureCache.get(path);
|
|
1586
1188
|
if (cached) {
|
|
@@ -1616,47 +1218,50 @@ export class Engine {
|
|
|
1616
1218
|
renderEyes(pass) {
|
|
1617
1219
|
pass.setPipeline(this.eyePipeline);
|
|
1618
1220
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1619
|
-
for (const draw of this.
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
+
}
|
|
1622
1226
|
}
|
|
1623
1227
|
}
|
|
1624
1228
|
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1625
1229
|
renderHair(pass) {
|
|
1626
1230
|
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1627
|
-
const hasHair = this.
|
|
1231
|
+
const hasHair = this.drawCalls.some((d) => d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes");
|
|
1628
1232
|
if (hasHair) {
|
|
1629
1233
|
pass.setPipeline(this.hairDepthPipeline);
|
|
1630
|
-
for (const draw of this.
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1636
|
-
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
|
+
}
|
|
1637
1239
|
}
|
|
1638
1240
|
}
|
|
1639
1241
|
// Hair shading (split by stencil for transparency over eyes)
|
|
1640
|
-
|
|
1242
|
+
const hairOverEyes = this.drawCalls.filter((d) => d.type === "hair-over-eyes");
|
|
1243
|
+
if (hairOverEyes.length > 0) {
|
|
1641
1244
|
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1642
1245
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1643
|
-
for (const draw of
|
|
1246
|
+
for (const draw of hairOverEyes) {
|
|
1644
1247
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1645
1248
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1646
1249
|
}
|
|
1647
1250
|
}
|
|
1648
|
-
|
|
1251
|
+
const hairOverNonEyes = this.drawCalls.filter((d) => d.type === "hair-over-non-eyes");
|
|
1252
|
+
if (hairOverNonEyes.length > 0) {
|
|
1649
1253
|
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1650
1254
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1651
|
-
for (const draw of
|
|
1255
|
+
for (const draw of hairOverNonEyes) {
|
|
1652
1256
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1653
1257
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1654
1258
|
}
|
|
1655
1259
|
}
|
|
1656
1260
|
// Hair outlines
|
|
1657
|
-
|
|
1261
|
+
const hairOutlines = this.drawCalls.filter((d) => d.type === "hair-outline");
|
|
1262
|
+
if (hairOutlines.length > 0) {
|
|
1658
1263
|
pass.setPipeline(this.hairOutlinePipeline);
|
|
1659
|
-
for (const draw of
|
|
1264
|
+
for (const draw of hairOutlines) {
|
|
1660
1265
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1661
1266
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1662
1267
|
}
|
|
@@ -1670,18 +1275,10 @@ export class Engine {
|
|
|
1670
1275
|
this.lastFrameTime = currentTime;
|
|
1671
1276
|
this.updateCameraUniforms();
|
|
1672
1277
|
this.updateRenderTarget();
|
|
1673
|
-
//
|
|
1674
|
-
if (this.hasAnimation && this.currentModel) {
|
|
1675
|
-
const pose = this.player.update(currentTime);
|
|
1676
|
-
if (pose) {
|
|
1677
|
-
this.applyPose(pose);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
// Update model pose first (this may update morph weights via tweens)
|
|
1681
|
-
// 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)
|
|
1682
1279
|
if (this.currentModel) {
|
|
1683
|
-
const
|
|
1684
|
-
if (
|
|
1280
|
+
const verticesChanged = this.currentModel.update(deltaTime);
|
|
1281
|
+
if (verticesChanged) {
|
|
1685
1282
|
this.vertexBufferNeedsUpdate = true;
|
|
1686
1283
|
}
|
|
1687
1284
|
}
|
|
@@ -1690,9 +1287,10 @@ export class Engine {
|
|
|
1690
1287
|
this.updateVertexBuffer();
|
|
1691
1288
|
this.vertexBufferNeedsUpdate = false;
|
|
1692
1289
|
}
|
|
1693
|
-
//
|
|
1290
|
+
// Update skin matrices buffer
|
|
1291
|
+
this.updateSkinMatrices();
|
|
1292
|
+
// Use single encoder for render
|
|
1694
1293
|
const encoder = this.device.createCommandEncoder();
|
|
1695
|
-
this.updateModelPose(deltaTime, encoder);
|
|
1696
1294
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1697
1295
|
if (this.currentModel) {
|
|
1698
1296
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
@@ -1701,9 +1299,11 @@ export class Engine {
|
|
|
1701
1299
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1702
1300
|
// Pass 1: Opaque
|
|
1703
1301
|
pass.setPipeline(this.modelPipeline);
|
|
1704
|
-
for (const draw of this.
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
+
}
|
|
1707
1307
|
}
|
|
1708
1308
|
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1709
1309
|
this.renderEyes(pass);
|
|
@@ -1712,9 +1312,11 @@ export class Engine {
|
|
|
1712
1312
|
this.renderHair(pass);
|
|
1713
1313
|
// Pass 4: Transparent
|
|
1714
1314
|
pass.setPipeline(this.modelPipeline);
|
|
1715
|
-
for (const draw of this.
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
+
}
|
|
1718
1320
|
}
|
|
1719
1321
|
this.drawOutlines(pass, true);
|
|
1720
1322
|
}
|
|
@@ -1831,31 +1433,20 @@ export class Engine {
|
|
|
1831
1433
|
colorAttachment.view = this.sceneRenderTextureView;
|
|
1832
1434
|
}
|
|
1833
1435
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1840
|
-
}
|
|
1841
|
-
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1842
|
-
this.computeSkinMatrices(encoder);
|
|
1843
|
-
}
|
|
1844
|
-
computeSkinMatrices(encoder) {
|
|
1845
|
-
const boneCount = this.currentModel.getSkeleton().bones.length;
|
|
1846
|
-
const workgroupCount = Math.ceil(boneCount / this.COMPUTE_WORKGROUP_SIZE);
|
|
1847
|
-
const pass = encoder.beginComputePass();
|
|
1848
|
-
pass.setPipeline(this.skinMatrixComputePipeline);
|
|
1849
|
-
pass.setBindGroup(0, this.skinMatrixComputeBindGroup);
|
|
1850
|
-
pass.dispatchWorkgroups(workgroupCount);
|
|
1851
|
-
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);
|
|
1852
1441
|
}
|
|
1853
1442
|
drawOutlines(pass, transparent) {
|
|
1854
1443
|
pass.setPipeline(this.outlinePipeline);
|
|
1855
|
-
const
|
|
1856
|
-
for (const draw of
|
|
1857
|
-
|
|
1858
|
-
|
|
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
|
+
}
|
|
1859
1450
|
}
|
|
1860
1451
|
}
|
|
1861
1452
|
updateStats(frameTime) {
|