reze-engine 0.8.4 → 0.9.0
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/dist/engine.d.ts +30 -15
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +313 -419
- package/dist/model.d.ts +0 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +0 -17
- package/package.json +2 -2
- package/src/engine.ts +359 -460
- package/src/model.ts +0 -3
- package/src/pmx-loader.ts +0 -21
package/dist/engine.js
CHANGED
|
@@ -24,17 +24,15 @@ export class Engine {
|
|
|
24
24
|
this.lightData = new Float32Array(64);
|
|
25
25
|
this.lightCount = 0;
|
|
26
26
|
this.resizeObserver = null;
|
|
27
|
-
// Post-alpha eye: eyes write stencil, hair-over-eyes reads it for see-through bangs (MMD-style).
|
|
28
|
-
this.STENCIL_EYE_VALUE = 1;
|
|
29
27
|
this.hasGround = false;
|
|
30
28
|
this.shadowLightVPMatrix = new Float32Array(16);
|
|
31
29
|
this.groundDrawCall = null;
|
|
32
30
|
this.shadowVPLightX = Number.NaN;
|
|
33
31
|
this.shadowVPLightY = Number.NaN;
|
|
34
32
|
this.shadowVPLightZ = Number.NaN;
|
|
35
|
-
// Double-tap detection
|
|
36
33
|
this.lastTouchTime = 0;
|
|
37
|
-
this.DOUBLE_TAP_DELAY = 300;
|
|
34
|
+
this.DOUBLE_TAP_DELAY = 300;
|
|
35
|
+
this.pendingPick = null;
|
|
38
36
|
this.modelInstances = new Map();
|
|
39
37
|
this.textureCache = new Map();
|
|
40
38
|
this._nextDefaultModelId = 0;
|
|
@@ -188,23 +186,6 @@ export class Engine {
|
|
|
188
186
|
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
189
187
|
},
|
|
190
188
|
];
|
|
191
|
-
const depthOnlyVertexBuffers = [
|
|
192
|
-
{
|
|
193
|
-
arrayStride: 8 * 4,
|
|
194
|
-
attributes: [
|
|
195
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
196
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
197
|
-
],
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
arrayStride: 4 * 2,
|
|
201
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
arrayStride: 4,
|
|
205
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
206
|
-
},
|
|
207
|
-
];
|
|
208
189
|
const standardBlend = {
|
|
209
190
|
format: this.presentationFormat,
|
|
210
191
|
blend: {
|
|
@@ -242,17 +223,17 @@ export class Engine {
|
|
|
242
223
|
|
|
243
224
|
struct MaterialUniforms {
|
|
244
225
|
alpha: f32,
|
|
245
|
-
alphaMultiplier: f32,
|
|
246
226
|
rimIntensity: f32,
|
|
247
227
|
shininess: f32,
|
|
228
|
+
_padding1: f32,
|
|
248
229
|
rimColor: vec3f,
|
|
249
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
250
|
-
diffuseColor: vec3f,
|
|
251
230
|
_padding2: f32,
|
|
252
|
-
|
|
231
|
+
diffuseColor: vec3f,
|
|
253
232
|
_padding3: f32,
|
|
254
|
-
|
|
233
|
+
ambientColor: vec3f,
|
|
255
234
|
_padding4: f32,
|
|
235
|
+
specularColor: vec3f,
|
|
236
|
+
_padding5: f32,
|
|
256
237
|
};
|
|
257
238
|
|
|
258
239
|
struct VertexOutput {
|
|
@@ -262,12 +243,15 @@ export class Engine {
|
|
|
262
243
|
@location(2) worldPos: vec3f,
|
|
263
244
|
};
|
|
264
245
|
|
|
246
|
+
// group 0: per-frame (bound once per pass)
|
|
265
247
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
266
248
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
267
|
-
@group(0) @binding(2) var
|
|
268
|
-
|
|
269
|
-
@group(
|
|
270
|
-
|
|
249
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
250
|
+
// group 1: per-instance (bound once per model)
|
|
251
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
252
|
+
// group 2: per-material (bound per draw call)
|
|
253
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
254
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
271
255
|
|
|
272
256
|
@vertex fn vs(
|
|
273
257
|
@location(0) position: vec3f,
|
|
@@ -279,7 +263,6 @@ export class Engine {
|
|
|
279
263
|
var output: VertexOutput;
|
|
280
264
|
let pos4 = vec4f(position, 1.0);
|
|
281
265
|
|
|
282
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
283
266
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
284
267
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
285
268
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -303,11 +286,7 @@ export class Engine {
|
|
|
303
286
|
}
|
|
304
287
|
|
|
305
288
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
306
|
-
|
|
307
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
308
|
-
if (material.isOverEyes > 0.5) {
|
|
309
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
310
|
-
}
|
|
289
|
+
let finalAlpha = material.alpha;
|
|
311
290
|
if (finalAlpha < 0.001) {
|
|
312
291
|
discard;
|
|
313
292
|
}
|
|
@@ -315,18 +294,14 @@ export class Engine {
|
|
|
315
294
|
let n = normalize(input.normal);
|
|
316
295
|
let textureColor = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
317
296
|
|
|
318
|
-
// View direction for specular and rim
|
|
319
297
|
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
320
298
|
|
|
321
|
-
// Simple lighting: global ambient + diffuse lighting
|
|
322
299
|
let albedo = textureColor * material.diffuseColor;
|
|
323
300
|
|
|
324
|
-
// Precompute material values
|
|
325
301
|
let minSpec = light.ambientColor.w;
|
|
326
302
|
let effectiveSpecular = max(material.specularColor, vec3f(minSpec));
|
|
327
303
|
let specPower = max(material.shininess, 1.0);
|
|
328
304
|
|
|
329
|
-
// Single directional light
|
|
330
305
|
let l = -light.lights[0].direction.xyz;
|
|
331
306
|
let nDotL = max(dot(n, l), 0.0);
|
|
332
307
|
let intensity = light.lights[0].color.w;
|
|
@@ -334,7 +309,6 @@ export class Engine {
|
|
|
334
309
|
|
|
335
310
|
let lightAccum = light.ambientColor.xyz + radiance * nDotL;
|
|
336
311
|
|
|
337
|
-
// Blinn-Phong specular
|
|
338
312
|
let h = normalize(l + viewDir);
|
|
339
313
|
let nDotH = max(dot(n, h), 0.0);
|
|
340
314
|
let specFactor = pow(nDotH, specPower);
|
|
@@ -342,9 +316,8 @@ export class Engine {
|
|
|
342
316
|
|
|
343
317
|
let litColor = albedo * lightAccum;
|
|
344
318
|
|
|
345
|
-
// Rim light calculation - proper Fresnel for edge-only highlights
|
|
346
319
|
let fresnel = 1.0 - abs(dot(n, viewDir));
|
|
347
|
-
let rimFactor = pow(fresnel, 4.0);
|
|
320
|
+
let rimFactor = pow(fresnel, 4.0);
|
|
348
321
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
349
322
|
|
|
350
323
|
let color = litColor + specularAccum + rimLight;
|
|
@@ -353,21 +326,42 @@ export class Engine {
|
|
|
353
326
|
}
|
|
354
327
|
`,
|
|
355
328
|
});
|
|
356
|
-
//
|
|
357
|
-
this.
|
|
358
|
-
label: "main
|
|
329
|
+
// group 0: per-frame (camera + light + sampler) — bound once per pass
|
|
330
|
+
this.mainPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
331
|
+
label: "main per-frame bind group layout",
|
|
359
332
|
entries: [
|
|
360
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
361
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
362
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
333
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
334
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
335
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
// group 1: per-instance (skinMats) — bound once per model
|
|
339
|
+
this.mainPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
340
|
+
label: "main per-instance bind group layout",
|
|
341
|
+
entries: [
|
|
342
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
// group 2: per-material (texture + material uniforms) — bound per draw call
|
|
346
|
+
this.mainPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
347
|
+
label: "main per-material bind group layout",
|
|
348
|
+
entries: [
|
|
349
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
350
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
366
351
|
],
|
|
367
352
|
});
|
|
368
353
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
369
354
|
label: "main pipeline layout",
|
|
370
|
-
bindGroupLayouts: [this.
|
|
355
|
+
bindGroupLayouts: [this.mainPerFrameBindGroupLayout, this.mainPerInstanceBindGroupLayout, this.mainPerMaterialBindGroupLayout],
|
|
356
|
+
});
|
|
357
|
+
this.perFrameBindGroup = this.device.createBindGroup({
|
|
358
|
+
label: "main per-frame bind group",
|
|
359
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
360
|
+
entries: [
|
|
361
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
362
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
363
|
+
{ binding: 2, resource: this.materialSampler },
|
|
364
|
+
],
|
|
371
365
|
});
|
|
372
366
|
this.modelPipeline = this.createRenderPipeline({
|
|
373
367
|
label: "model pipeline",
|
|
@@ -497,18 +491,30 @@ export class Engine {
|
|
|
497
491
|
cullMode: "back",
|
|
498
492
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
499
493
|
});
|
|
500
|
-
//
|
|
501
|
-
this.
|
|
502
|
-
label: "outline bind group layout",
|
|
494
|
+
// Outline: group 0 = per-frame (camera), group 1 = per-instance (skinMats), group 2 = per-material (edge uniforms)
|
|
495
|
+
this.outlinePerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
496
|
+
label: "outline per-frame bind group layout",
|
|
497
|
+
entries: [
|
|
498
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
502
|
+
this.outlinePerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
503
|
+
label: "outline per-material bind group layout",
|
|
503
504
|
entries: [
|
|
504
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
505
|
-
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
506
|
-
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
505
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
507
506
|
],
|
|
508
507
|
});
|
|
509
508
|
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
510
509
|
label: "outline pipeline layout",
|
|
511
|
-
bindGroupLayouts: [this.
|
|
510
|
+
bindGroupLayouts: [this.outlinePerFrameBindGroupLayout, this.mainPerInstanceBindGroupLayout, this.outlinePerMaterialBindGroupLayout],
|
|
511
|
+
});
|
|
512
|
+
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
513
|
+
label: "outline per-frame bind group",
|
|
514
|
+
layout: this.outlinePerFrameBindGroupLayout,
|
|
515
|
+
entries: [
|
|
516
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
517
|
+
],
|
|
512
518
|
});
|
|
513
519
|
const outlineShaderModule = this.device.createShaderModule({
|
|
514
520
|
label: "outline shaders",
|
|
@@ -523,14 +529,17 @@ export class Engine {
|
|
|
523
529
|
struct MaterialUniforms {
|
|
524
530
|
edgeColor: vec4f,
|
|
525
531
|
edgeSize: f32,
|
|
526
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
527
532
|
_padding1: f32,
|
|
528
533
|
_padding2: f32,
|
|
534
|
+
_padding3: f32,
|
|
529
535
|
};
|
|
530
536
|
|
|
537
|
+
// group 0: per-frame
|
|
531
538
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
532
|
-
|
|
533
|
-
@group(
|
|
539
|
+
// group 1: per-instance
|
|
540
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
541
|
+
// group 2: per-material
|
|
542
|
+
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
|
|
534
543
|
|
|
535
544
|
struct VertexOutput {
|
|
536
545
|
@builtin(position) position: vec4f,
|
|
@@ -545,7 +554,6 @@ export class Engine {
|
|
|
545
554
|
var output: VertexOutput;
|
|
546
555
|
let pos4 = vec4f(position, 1.0);
|
|
547
556
|
|
|
548
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
549
557
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
550
558
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
551
559
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -563,7 +571,6 @@ export class Engine {
|
|
|
563
571
|
let worldPos = skinnedPos.xyz;
|
|
564
572
|
let worldNormal = normalize(skinnedNrm);
|
|
565
573
|
|
|
566
|
-
// MMD invert hull: expand vertices outward along normals
|
|
567
574
|
let scaleFactor = 0.01;
|
|
568
575
|
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
569
576
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
@@ -571,13 +578,7 @@ export class Engine {
|
|
|
571
578
|
}
|
|
572
579
|
|
|
573
580
|
@fragment fn fs() -> @location(0) vec4f {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (material.isOverEyes > 0.5) {
|
|
577
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return color;
|
|
581
|
+
return material.edgeColor;
|
|
581
582
|
}
|
|
582
583
|
`,
|
|
583
584
|
});
|
|
@@ -594,55 +595,9 @@ export class Engine {
|
|
|
594
595
|
depthCompare: "less-equal",
|
|
595
596
|
},
|
|
596
597
|
});
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
label: "
|
|
600
|
-
layout: outlinePipelineLayout,
|
|
601
|
-
shaderModule: outlineShaderModule,
|
|
602
|
-
vertexBuffers: outlineVertexBuffers,
|
|
603
|
-
fragmentTarget: standardBlend,
|
|
604
|
-
cullMode: "back",
|
|
605
|
-
depthStencil: {
|
|
606
|
-
format: "depth24plus-stencil8",
|
|
607
|
-
depthWriteEnabled: false,
|
|
608
|
-
depthCompare: "less-equal",
|
|
609
|
-
depthBias: -0.0001,
|
|
610
|
-
depthBiasSlopeScale: 0.0,
|
|
611
|
-
depthBiasClamp: 0.0,
|
|
612
|
-
},
|
|
613
|
-
});
|
|
614
|
-
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
615
|
-
this.eyePipeline = this.createRenderPipeline({
|
|
616
|
-
label: "eye overlay pipeline",
|
|
617
|
-
layout: mainPipelineLayout,
|
|
618
|
-
shaderModule,
|
|
619
|
-
vertexBuffers: fullVertexBuffers,
|
|
620
|
-
fragmentTarget: standardBlend,
|
|
621
|
-
cullMode: "front",
|
|
622
|
-
depthStencil: {
|
|
623
|
-
format: "depth24plus-stencil8",
|
|
624
|
-
depthWriteEnabled: true,
|
|
625
|
-
depthCompare: "less-equal",
|
|
626
|
-
depthBias: -0.00005,
|
|
627
|
-
depthBiasSlopeScale: 0.0,
|
|
628
|
-
depthBiasClamp: 0.0,
|
|
629
|
-
stencilFront: {
|
|
630
|
-
compare: "always",
|
|
631
|
-
failOp: "keep",
|
|
632
|
-
depthFailOp: "keep",
|
|
633
|
-
passOp: "replace",
|
|
634
|
-
},
|
|
635
|
-
stencilBack: {
|
|
636
|
-
compare: "always",
|
|
637
|
-
failOp: "keep",
|
|
638
|
-
depthFailOp: "keep",
|
|
639
|
-
passOp: "replace",
|
|
640
|
-
},
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
644
|
-
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
645
|
-
label: "depth only shader",
|
|
598
|
+
// GPU picking: encode (modelIndex, materialIndex) as color
|
|
599
|
+
const pickShaderModule = this.device.createShaderModule({
|
|
600
|
+
label: "pick shader",
|
|
646
601
|
code: /* wgsl */ `
|
|
647
602
|
struct CameraUniforms {
|
|
648
603
|
view: mat4x4f,
|
|
@@ -650,91 +605,87 @@ export class Engine {
|
|
|
650
605
|
viewPos: vec3f,
|
|
651
606
|
_padding: f32,
|
|
652
607
|
};
|
|
608
|
+
struct PickId {
|
|
609
|
+
modelId: f32,
|
|
610
|
+
materialId: f32,
|
|
611
|
+
_p1: f32,
|
|
612
|
+
_p2: f32,
|
|
613
|
+
};
|
|
653
614
|
|
|
654
615
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
655
|
-
@group(
|
|
616
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
617
|
+
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
656
618
|
|
|
657
619
|
@vertex fn vs(
|
|
658
620
|
@location(0) position: vec3f,
|
|
659
621
|
@location(1) normal: vec3f,
|
|
622
|
+
@location(2) uv: vec2f,
|
|
660
623
|
@location(3) joints0: vec4<u32>,
|
|
661
624
|
@location(4) weights0: vec4<f32>
|
|
662
625
|
) -> @builtin(position) vec4f {
|
|
663
626
|
let pos4 = vec4f(position, 1.0);
|
|
664
|
-
|
|
665
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
666
627
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
667
628
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
668
|
-
let
|
|
669
|
-
|
|
670
|
-
var
|
|
671
|
-
|
|
672
|
-
let j = joints0[i];
|
|
673
|
-
let w = normalizedWeights[i];
|
|
674
|
-
let m = skinMats[j];
|
|
675
|
-
skinnedPos += (m * pos4) * w;
|
|
676
|
-
}
|
|
677
|
-
let worldPos = skinnedPos.xyz;
|
|
678
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
679
|
-
return clipPos;
|
|
629
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
630
|
+
var sp = vec4f(0.0);
|
|
631
|
+
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
632
|
+
return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
680
633
|
}
|
|
681
634
|
|
|
682
635
|
@fragment fn fs() -> @location(0) vec4f {
|
|
683
|
-
return vec4f(
|
|
636
|
+
return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
|
|
684
637
|
}
|
|
685
638
|
`,
|
|
686
639
|
});
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
640
|
+
this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
641
|
+
label: "pick per-frame layout",
|
|
642
|
+
entries: [
|
|
643
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
644
|
+
],
|
|
645
|
+
});
|
|
646
|
+
this.pickPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
647
|
+
label: "pick per-instance layout",
|
|
648
|
+
entries: [
|
|
649
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
650
|
+
],
|
|
651
|
+
});
|
|
652
|
+
this.pickPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
653
|
+
label: "pick per-material layout",
|
|
654
|
+
entries: [
|
|
655
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
656
|
+
],
|
|
657
|
+
});
|
|
658
|
+
const pickPipelineLayout = this.device.createPipelineLayout({
|
|
659
|
+
label: "pick pipeline layout",
|
|
660
|
+
bindGroupLayouts: [this.pickPerFrameBindGroupLayout, this.pickPerInstanceBindGroupLayout, this.pickPerMaterialBindGroupLayout],
|
|
661
|
+
});
|
|
662
|
+
this.pickPerFrameBindGroup = this.device.createBindGroup({
|
|
663
|
+
label: "pick per-frame bind group",
|
|
664
|
+
layout: this.pickPerFrameBindGroupLayout,
|
|
665
|
+
entries: [
|
|
666
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
667
|
+
],
|
|
668
|
+
});
|
|
669
|
+
this.pickPipeline = this.device.createRenderPipeline({
|
|
670
|
+
label: "pick pipeline",
|
|
671
|
+
layout: pickPipelineLayout,
|
|
672
|
+
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
673
|
+
fragment: {
|
|
674
|
+
module: pickShaderModule,
|
|
675
|
+
targets: [{ format: "rgba8unorm" }],
|
|
696
676
|
},
|
|
697
|
-
|
|
698
|
-
cullMode: "none",
|
|
677
|
+
primitive: { cullMode: "none" },
|
|
699
678
|
depthStencil: {
|
|
700
|
-
format: "depth24plus
|
|
679
|
+
format: "depth24plus",
|
|
701
680
|
depthWriteEnabled: true,
|
|
702
681
|
depthCompare: "less-equal",
|
|
703
|
-
depthBias: 0.0,
|
|
704
|
-
depthBiasSlopeScale: 0.0,
|
|
705
|
-
depthBiasClamp: 0.0,
|
|
706
682
|
},
|
|
707
683
|
});
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
shaderModule,
|
|
714
|
-
vertexBuffers: fullVertexBuffers,
|
|
715
|
-
fragmentTarget: standardBlend,
|
|
716
|
-
cullMode: "none",
|
|
717
|
-
depthStencil: {
|
|
718
|
-
format: "depth24plus-stencil8",
|
|
719
|
-
depthWriteEnabled: false,
|
|
720
|
-
depthCompare: "less-equal",
|
|
721
|
-
stencilFront: {
|
|
722
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
723
|
-
failOp: "keep",
|
|
724
|
-
depthFailOp: "keep",
|
|
725
|
-
passOp: "keep",
|
|
726
|
-
},
|
|
727
|
-
stencilBack: {
|
|
728
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
729
|
-
failOp: "keep",
|
|
730
|
-
depthFailOp: "keep",
|
|
731
|
-
passOp: "keep",
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
|
-
});
|
|
735
|
-
};
|
|
736
|
-
this.hairPipelineOverEyes = createHairPipeline(true);
|
|
737
|
-
this.hairPipelineOverNonEyes = createHairPipeline(false);
|
|
684
|
+
this.pickReadbackBuffer = this.device.createBuffer({
|
|
685
|
+
label: "pick readback",
|
|
686
|
+
size: 256,
|
|
687
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
688
|
+
});
|
|
738
689
|
}
|
|
739
690
|
// Step 3: Setup canvas resize handling
|
|
740
691
|
setupResize() {
|
|
@@ -788,10 +739,24 @@ export class Engine {
|
|
|
788
739
|
depthStoreOp: "store",
|
|
789
740
|
stencilClearValue: 0,
|
|
790
741
|
stencilLoadOp: "clear",
|
|
791
|
-
stencilStoreOp: "discard",
|
|
742
|
+
stencilStoreOp: "discard",
|
|
792
743
|
},
|
|
793
744
|
};
|
|
794
745
|
this.camera.aspect = width / height;
|
|
746
|
+
if (this.onRaycast) {
|
|
747
|
+
this.pickTexture = this.device.createTexture({
|
|
748
|
+
label: "pick render target",
|
|
749
|
+
size: [width, height],
|
|
750
|
+
format: "rgba8unorm",
|
|
751
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
752
|
+
});
|
|
753
|
+
this.pickDepthTexture = this.device.createTexture({
|
|
754
|
+
label: "pick depth",
|
|
755
|
+
size: [width, height],
|
|
756
|
+
format: "depth24plus",
|
|
757
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
795
760
|
}
|
|
796
761
|
}
|
|
797
762
|
// Step 4: Create camera and uniform buffer
|
|
@@ -823,6 +788,24 @@ export class Engine {
|
|
|
823
788
|
this.cameraTargetOffset.y = offset?.y ?? 0;
|
|
824
789
|
this.cameraTargetOffset.z = offset?.z ?? 0;
|
|
825
790
|
}
|
|
791
|
+
/** Souls-style follow cam: orbit center tracks a model bone each frame. Shorthand for setCameraTarget(model, boneName, offset). */
|
|
792
|
+
setCameraFollow(model, boneName, offset) {
|
|
793
|
+
if (model === null) {
|
|
794
|
+
this.cameraTargetModel = null;
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
this.cameraTargetModel = model;
|
|
798
|
+
this.cameraTargetBoneName = boneName ?? "全ての親";
|
|
799
|
+
this.cameraTargetOffset.x = offset?.x ?? 0;
|
|
800
|
+
this.cameraTargetOffset.y = offset?.y ?? 0;
|
|
801
|
+
this.cameraTargetOffset.z = offset?.z ?? 0;
|
|
802
|
+
}
|
|
803
|
+
getCameraDistance() { return this.camera.radius; }
|
|
804
|
+
setCameraDistance(d) { this.camera.radius = d; }
|
|
805
|
+
getCameraAlpha() { return this.camera.alpha; }
|
|
806
|
+
setCameraAlpha(a) { this.camera.alpha = a; }
|
|
807
|
+
getCameraBeta() { return this.camera.beta; }
|
|
808
|
+
setCameraBeta(b) { this.camera.beta = b; }
|
|
826
809
|
// Step 5: Create lighting buffers
|
|
827
810
|
setupLighting() {
|
|
828
811
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -954,9 +937,6 @@ export class Engine {
|
|
|
954
937
|
await this.setupModelInstance(key, model, basePath);
|
|
955
938
|
return key;
|
|
956
939
|
}
|
|
957
|
-
async registerModel(model, pmxPath) {
|
|
958
|
-
return this.addModel(model, pmxPath);
|
|
959
|
-
}
|
|
960
940
|
removeModel(name) {
|
|
961
941
|
this.modelInstances.delete(name);
|
|
962
942
|
}
|
|
@@ -1024,11 +1004,8 @@ export class Engine {
|
|
|
1024
1004
|
inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
|
|
1025
1005
|
});
|
|
1026
1006
|
}
|
|
1027
|
-
instances() {
|
|
1028
|
-
return this.modelInstances.values();
|
|
1029
|
-
}
|
|
1030
1007
|
forEachInstance(fn) {
|
|
1031
|
-
for (const inst of this.
|
|
1008
|
+
for (const inst of this.modelInstances.values())
|
|
1032
1009
|
fn(inst);
|
|
1033
1010
|
}
|
|
1034
1011
|
updateInstances(deltaTime) {
|
|
@@ -1098,6 +1075,20 @@ export class Engine {
|
|
|
1098
1075
|
{ binding: 1, resource: { buffer: skinMatrixBuffer } },
|
|
1099
1076
|
],
|
|
1100
1077
|
});
|
|
1078
|
+
const mainPerInstanceBindGroup = this.device.createBindGroup({
|
|
1079
|
+
label: `${name}: main per-instance bind group`,
|
|
1080
|
+
layout: this.mainPerInstanceBindGroupLayout,
|
|
1081
|
+
entries: [
|
|
1082
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1083
|
+
],
|
|
1084
|
+
});
|
|
1085
|
+
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
1086
|
+
label: `${name}: pick per-instance bind group`,
|
|
1087
|
+
layout: this.pickPerInstanceBindGroupLayout,
|
|
1088
|
+
entries: [
|
|
1089
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1090
|
+
],
|
|
1091
|
+
});
|
|
1101
1092
|
const inst = {
|
|
1102
1093
|
name,
|
|
1103
1094
|
model,
|
|
@@ -1110,6 +1101,9 @@ export class Engine {
|
|
|
1110
1101
|
drawCalls: [],
|
|
1111
1102
|
shadowDrawCalls: [],
|
|
1112
1103
|
shadowBindGroup,
|
|
1104
|
+
mainPerInstanceBindGroup,
|
|
1105
|
+
pickPerInstanceBindGroup,
|
|
1106
|
+
pickDrawCalls: [],
|
|
1113
1107
|
hiddenMaterials: new Set(),
|
|
1114
1108
|
physics,
|
|
1115
1109
|
vertexBufferNeedsUpdate: false,
|
|
@@ -1245,6 +1239,8 @@ export class Engine {
|
|
|
1245
1239
|
throw new Error("Model has no materials");
|
|
1246
1240
|
const textures = model.getTextures();
|
|
1247
1241
|
const prefix = `${inst.name}: `;
|
|
1242
|
+
// 1-based so that (0,0) = clear color = "no hit"
|
|
1243
|
+
const modelId = this.modelInstances.size + 1;
|
|
1248
1244
|
const loadTextureByIndex = async (texIndex) => {
|
|
1249
1245
|
if (texIndex < 0 || texIndex >= textures.length)
|
|
1250
1246
|
return null;
|
|
@@ -1252,70 +1248,29 @@ export class Engine {
|
|
|
1252
1248
|
return this.createTextureFromPath(path);
|
|
1253
1249
|
};
|
|
1254
1250
|
let currentIndexOffset = 0;
|
|
1251
|
+
let materialId = 0;
|
|
1255
1252
|
for (const mat of materials) {
|
|
1256
1253
|
const indexCount = mat.vertexCount;
|
|
1257
1254
|
if (indexCount === 0)
|
|
1258
1255
|
continue;
|
|
1256
|
+
materialId++;
|
|
1259
1257
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex);
|
|
1260
1258
|
if (!diffuseTexture)
|
|
1261
1259
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1262
1260
|
const materialAlpha = mat.diffuse[3];
|
|
1263
1261
|
const isTransparent = materialAlpha < 1.0 - 0.001;
|
|
1264
|
-
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha,
|
|
1262
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1263
|
+
const textureView = diffuseTexture.createView();
|
|
1265
1264
|
const bindGroup = this.device.createBindGroup({
|
|
1266
1265
|
label: `${prefix}material: ${mat.name}`,
|
|
1267
|
-
layout: this.
|
|
1266
|
+
layout: this.mainPerMaterialBindGroupLayout,
|
|
1268
1267
|
entries: [
|
|
1269
|
-
{ binding: 0, resource:
|
|
1270
|
-
{ binding: 1, resource: { buffer:
|
|
1271
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1272
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1273
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1274
|
-
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1268
|
+
{ binding: 0, resource: textureView },
|
|
1269
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
1275
1270
|
],
|
|
1276
1271
|
});
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
inst.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1280
|
-
}
|
|
1281
|
-
else if (mat.isHair) {
|
|
1282
|
-
const createHairBindGroup = (isOverEyes) => {
|
|
1283
|
-
const buf = this.createMaterialUniformBuffer(`${prefix}${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`, materialAlpha, isOverEyes ? 1.0 : 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1284
|
-
return this.device.createBindGroup({
|
|
1285
|
-
label: `${prefix}hair ${isOverEyes ? "over eyes" : "over non-eyes"}: ${mat.name}`,
|
|
1286
|
-
layout: this.mainBindGroupLayout,
|
|
1287
|
-
entries: [
|
|
1288
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1289
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1290
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1291
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1292
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1293
|
-
{ binding: 5, resource: { buffer: buf } },
|
|
1294
|
-
],
|
|
1295
|
-
});
|
|
1296
|
-
};
|
|
1297
|
-
inst.drawCalls.push({
|
|
1298
|
-
type: "hair-over-eyes",
|
|
1299
|
-
count: indexCount,
|
|
1300
|
-
firstIndex: currentIndexOffset,
|
|
1301
|
-
bindGroup: createHairBindGroup(true),
|
|
1302
|
-
materialName: mat.name,
|
|
1303
|
-
});
|
|
1304
|
-
inst.drawCalls.push({
|
|
1305
|
-
type: "hair-over-non-eyes",
|
|
1306
|
-
count: indexCount,
|
|
1307
|
-
firstIndex: currentIndexOffset,
|
|
1308
|
-
bindGroup: createHairBindGroup(false),
|
|
1309
|
-
materialName: mat.name,
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
else if (isTransparent) {
|
|
1313
|
-
inst.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1314
|
-
}
|
|
1315
|
-
else {
|
|
1316
|
-
inst.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1272
|
+
const type = isTransparent ? "transparent" : "opaque";
|
|
1273
|
+
inst.drawCalls.push({ type, count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1319
1274
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1320
1275
|
const materialUniformData = new Float32Array([
|
|
1321
1276
|
mat.edgeColor[0], mat.edgeColor[1], mat.edgeColor[2], mat.edgeColor[3],
|
|
@@ -1324,48 +1279,42 @@ export class Engine {
|
|
|
1324
1279
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData);
|
|
1325
1280
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1326
1281
|
label: `${prefix}outline: ${mat.name}`,
|
|
1327
|
-
layout: this.
|
|
1282
|
+
layout: this.outlinePerMaterialBindGroupLayout,
|
|
1328
1283
|
entries: [
|
|
1329
|
-
{ binding: 0, resource: { buffer:
|
|
1330
|
-
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1331
|
-
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1284
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
1332
1285
|
],
|
|
1333
1286
|
});
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1287
|
+
const outlineType = isTransparent ? "transparent-outline" : "opaque-outline";
|
|
1288
|
+
inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name });
|
|
1289
|
+
}
|
|
1290
|
+
if (this.onRaycast) {
|
|
1291
|
+
const pickIdData = new Float32Array([modelId, materialId, 0, 0]);
|
|
1292
|
+
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData);
|
|
1293
|
+
const pickBindGroup = this.device.createBindGroup({
|
|
1294
|
+
label: `${prefix}pick: ${mat.name}`,
|
|
1295
|
+
layout: this.pickPerMaterialBindGroupLayout,
|
|
1296
|
+
entries: [{ binding: 0, resource: { buffer: pickIdBuffer } }],
|
|
1297
|
+
});
|
|
1298
|
+
inst.pickDrawCalls.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: pickBindGroup });
|
|
1338
1299
|
}
|
|
1339
1300
|
currentIndexOffset += indexCount;
|
|
1340
1301
|
}
|
|
1341
1302
|
for (const d of inst.drawCalls) {
|
|
1342
|
-
if (d.type === "opaque"
|
|
1303
|
+
if (d.type === "opaque")
|
|
1343
1304
|
inst.shadowDrawCalls.push(d);
|
|
1344
1305
|
}
|
|
1345
1306
|
}
|
|
1346
|
-
createMaterialUniformBuffer(label, alpha,
|
|
1307
|
+
createMaterialUniformBuffer(label, alpha, diffuseColor, ambientColor, specularColor, shininess) {
|
|
1347
1308
|
const data = new Float32Array(20);
|
|
1348
1309
|
data.set([
|
|
1349
1310
|
alpha,
|
|
1350
|
-
1.0,
|
|
1351
1311
|
this.rimLightIntensity,
|
|
1352
|
-
shininess,
|
|
1353
|
-
|
|
1354
|
-
1.0,
|
|
1355
|
-
1.0,
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
diffuseColor[1],
|
|
1359
|
-
diffuseColor[2],
|
|
1360
|
-
0.0, // diffuseColor (vec3), _padding2
|
|
1361
|
-
ambientColor[0],
|
|
1362
|
-
ambientColor[1],
|
|
1363
|
-
ambientColor[2],
|
|
1364
|
-
0.0, // ambientColor (vec3), _padding3
|
|
1365
|
-
specularColor[0],
|
|
1366
|
-
specularColor[1],
|
|
1367
|
-
specularColor[2],
|
|
1368
|
-
0.0, // specularColor (vec3), _padding4
|
|
1312
|
+
shininess,
|
|
1313
|
+
0.0,
|
|
1314
|
+
1.0, 1.0, 1.0, 0.0, // rimColor (vec3), _padding2
|
|
1315
|
+
diffuseColor[0], diffuseColor[1], diffuseColor[2], 0.0,
|
|
1316
|
+
ambientColor[0], ambientColor[1], ambientColor[2], 0.0,
|
|
1317
|
+
specularColor[0], specularColor[1], specularColor[2], 0.0,
|
|
1369
1318
|
]);
|
|
1370
1319
|
return this.createUniformBuffer(`material uniform: ${label}`, data);
|
|
1371
1320
|
}
|
|
@@ -1412,17 +1361,6 @@ export class Engine {
|
|
|
1412
1361
|
return null;
|
|
1413
1362
|
}
|
|
1414
1363
|
}
|
|
1415
|
-
// Post-alpha eye: render eye draws; main pass writes stencil so hair-over-eyes can use it for see-through bangs.
|
|
1416
|
-
renderEyes(pass, inst) {
|
|
1417
|
-
pass.setPipeline(this.eyePipeline);
|
|
1418
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1419
|
-
for (const draw of inst.drawCalls) {
|
|
1420
|
-
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1421
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1422
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
1364
|
renderGround(pass) {
|
|
1427
1365
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
1428
1366
|
return;
|
|
@@ -1432,149 +1370,91 @@ export class Engine {
|
|
|
1432
1370
|
pass.setBindGroup(0, this.groundDrawCall.bindGroup);
|
|
1433
1371
|
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
|
|
1434
1372
|
}
|
|
1435
|
-
// Post-alpha eye: hair-over-eyes uses stencil (from renderEyes) for 50% alpha; hair-over-non-eyes uses inverse stencil.
|
|
1436
|
-
renderHair(pass, inst) {
|
|
1437
|
-
const hasHair = inst.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d));
|
|
1438
|
-
if (hasHair) {
|
|
1439
|
-
pass.setPipeline(this.hairDepthPipeline);
|
|
1440
|
-
for (const draw of inst.drawCalls) {
|
|
1441
|
-
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, draw)) {
|
|
1442
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1443
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
const hairOverEyes = inst.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(inst, d));
|
|
1448
|
-
if (hairOverEyes.length > 0) {
|
|
1449
|
-
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1450
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1451
|
-
for (const draw of hairOverEyes) {
|
|
1452
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1453
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
const hairOverNonEyes = inst.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(inst, d));
|
|
1457
|
-
if (hairOverNonEyes.length > 0) {
|
|
1458
|
-
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1459
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1460
|
-
for (const draw of hairOverNonEyes) {
|
|
1461
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1462
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
const hairOutlines = inst.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(inst, d));
|
|
1466
|
-
if (hairOutlines.length > 0) {
|
|
1467
|
-
pass.setPipeline(this.hairOutlinePipeline);
|
|
1468
|
-
for (const draw of hairOutlines) {
|
|
1469
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1470
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
1373
|
performRaycast(screenX, screenY) {
|
|
1475
1374
|
if (!this.onRaycast || this.modelInstances.size === 0) {
|
|
1476
1375
|
this.onRaycast?.("", null, screenX, screenY);
|
|
1477
1376
|
return;
|
|
1478
1377
|
}
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
let closest = null;
|
|
1502
|
-
const maxDistance = 1000;
|
|
1378
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1379
|
+
this.pendingPick = { x: Math.floor(screenX * dpr), y: Math.floor(screenY * dpr) };
|
|
1380
|
+
}
|
|
1381
|
+
renderPickPass(encoder) {
|
|
1382
|
+
if (!this.pendingPick || !this.pickTexture || !this.pickDepthTexture)
|
|
1383
|
+
return;
|
|
1384
|
+
const pass = encoder.beginRenderPass({
|
|
1385
|
+
colorAttachments: [{
|
|
1386
|
+
view: this.pickTexture.createView(),
|
|
1387
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1388
|
+
loadOp: "clear",
|
|
1389
|
+
storeOp: "store",
|
|
1390
|
+
}],
|
|
1391
|
+
depthStencilAttachment: {
|
|
1392
|
+
view: this.pickDepthTexture.createView(),
|
|
1393
|
+
depthClearValue: 1.0,
|
|
1394
|
+
depthLoadOp: "clear",
|
|
1395
|
+
depthStoreOp: "store",
|
|
1396
|
+
},
|
|
1397
|
+
});
|
|
1398
|
+
pass.setPipeline(this.pickPipeline);
|
|
1399
|
+
pass.setBindGroup(0, this.pickPerFrameBindGroup);
|
|
1503
1400
|
this.forEachInstance((inst) => {
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1401
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
1402
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1403
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1404
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1405
|
+
pass.setBindGroup(1, inst.pickPerInstanceBindGroup);
|
|
1406
|
+
for (const draw of inst.pickDrawCalls) {
|
|
1407
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1408
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
pass.end();
|
|
1412
|
+
// Copy the single pixel under cursor to readback buffer
|
|
1413
|
+
const px = Math.min(this.pendingPick.x, this.pickTexture.width - 1);
|
|
1414
|
+
const py = Math.min(this.pendingPick.y, this.pickTexture.height - 1);
|
|
1415
|
+
encoder.copyTextureToBuffer({ texture: this.pickTexture, origin: { x: Math.max(0, px), y: Math.max(0, py) } }, { buffer: this.pickReadbackBuffer, bytesPerRow: 256 }, { width: 1, height: 1 });
|
|
1416
|
+
}
|
|
1417
|
+
async resolvePickResult(screenX, screenY) {
|
|
1418
|
+
if (!this.onRaycast)
|
|
1419
|
+
return;
|
|
1420
|
+
await this.pickReadbackBuffer.mapAsync(GPUMapMode.READ);
|
|
1421
|
+
const data = new Uint8Array(this.pickReadbackBuffer.getMappedRange());
|
|
1422
|
+
const modelId = data[0];
|
|
1423
|
+
const materialId = data[1];
|
|
1424
|
+
this.pickReadbackBuffer.unmap();
|
|
1425
|
+
if (modelId === 0) {
|
|
1426
|
+
this.onRaycast("", null, screenX, screenY);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
// Find model by 1-based index
|
|
1430
|
+
let idx = 1;
|
|
1431
|
+
let hitModel = "";
|
|
1432
|
+
for (const [name] of this.modelInstances) {
|
|
1433
|
+
if (idx === modelId) {
|
|
1434
|
+
hitModel = name;
|
|
1435
|
+
break;
|
|
1537
1436
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1437
|
+
idx++;
|
|
1438
|
+
}
|
|
1439
|
+
// Find material by 1-based index (skipping zero-vertex materials)
|
|
1440
|
+
let hitMaterial = null;
|
|
1441
|
+
if (hitModel) {
|
|
1442
|
+
const inst = this.modelInstances.get(hitModel);
|
|
1443
|
+
if (inst) {
|
|
1444
|
+
const materials = inst.model.getMaterials();
|
|
1445
|
+
let matIdx = 0;
|
|
1446
|
+
for (const mat of materials) {
|
|
1447
|
+
if (mat.vertexCount === 0)
|
|
1448
|
+
continue;
|
|
1449
|
+
matIdx++;
|
|
1450
|
+
if (matIdx === materialId) {
|
|
1451
|
+
hitMaterial = mat.name;
|
|
1548
1452
|
break;
|
|
1549
1453
|
}
|
|
1550
|
-
indexOffset += materials[matIdx].vertexCount;
|
|
1551
|
-
}
|
|
1552
|
-
if (triangleMaterialIndex === -1)
|
|
1553
|
-
continue;
|
|
1554
|
-
const edge1 = v1.subtract(v0), edge2 = v2.subtract(v0), h = rayDirection.cross(edge2), a = edge1.dot(h);
|
|
1555
|
-
if (Math.abs(a) < 0.0001)
|
|
1556
|
-
continue;
|
|
1557
|
-
const f = 1 / a, s = rayOrigin.subtract(v0), u = f * s.dot(h);
|
|
1558
|
-
if (u < 0 || u > 1)
|
|
1559
|
-
continue;
|
|
1560
|
-
const q = s.cross(edge1), v = f * rayDirection.dot(q);
|
|
1561
|
-
if (v < 0 || u + v > 1)
|
|
1562
|
-
continue;
|
|
1563
|
-
const t = f * edge2.dot(q);
|
|
1564
|
-
if (t <= 0.0001 || t >= maxDistance)
|
|
1565
|
-
continue;
|
|
1566
|
-
const triangleNormal = edge1.cross(edge2).normalize();
|
|
1567
|
-
if (triangleNormal.dot(rayDirection) >= 0)
|
|
1568
|
-
continue;
|
|
1569
|
-
if (!closest || t < closest.distance) {
|
|
1570
|
-
closest = { modelName: inst.name, materialName: materials[triangleMaterialIndex].name, distance: t };
|
|
1571
1454
|
}
|
|
1572
1455
|
}
|
|
1573
|
-
});
|
|
1574
|
-
if (this.onRaycast) {
|
|
1575
|
-
const hit = closest;
|
|
1576
|
-
this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY);
|
|
1577
1456
|
}
|
|
1457
|
+
this.onRaycast(hitModel, hitMaterial, screenX, screenY);
|
|
1578
1458
|
}
|
|
1579
1459
|
render() {
|
|
1580
1460
|
if (!this.multisampleTexture || !this.camera || !this.device)
|
|
@@ -1622,9 +1502,21 @@ export class Engine {
|
|
|
1622
1502
|
if (this.hasGround)
|
|
1623
1503
|
this.renderGround(pass);
|
|
1624
1504
|
pass.end();
|
|
1505
|
+
const pick = this.pendingPick;
|
|
1506
|
+
if (pick && hasModels)
|
|
1507
|
+
this.renderPickPass(encoder);
|
|
1625
1508
|
this.device.queue.submit([encoder.finish()]);
|
|
1509
|
+
if (pick) {
|
|
1510
|
+
this.pendingPick = null;
|
|
1511
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1512
|
+
this.resolvePickResult(pick.x / dpr, pick.y / dpr);
|
|
1513
|
+
}
|
|
1626
1514
|
this.updateStats(performance.now() - currentTime);
|
|
1627
1515
|
}
|
|
1516
|
+
updateRenderTarget() {
|
|
1517
|
+
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
1518
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
|
|
1519
|
+
}
|
|
1628
1520
|
drawInstanceShadow(sp, inst) {
|
|
1629
1521
|
sp.setBindGroup(0, inst.shadowBindGroup);
|
|
1630
1522
|
sp.setVertexBuffer(0, inst.vertexBuffer);
|
|
@@ -1640,7 +1532,7 @@ export class Engine {
|
|
|
1640
1532
|
pass.setPipeline(pipeline);
|
|
1641
1533
|
for (const draw of inst.drawCalls) {
|
|
1642
1534
|
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1643
|
-
pass.setBindGroup(
|
|
1535
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1644
1536
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1645
1537
|
}
|
|
1646
1538
|
}
|
|
@@ -1649,20 +1541,24 @@ export class Engine {
|
|
|
1649
1541
|
pass.setPipeline(pipeline);
|
|
1650
1542
|
for (const draw of inst.drawCalls) {
|
|
1651
1543
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1652
|
-
pass.setBindGroup(
|
|
1544
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1653
1545
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1654
1546
|
}
|
|
1655
1547
|
}
|
|
1656
1548
|
}
|
|
1549
|
+
bindMainGroups(pass, inst) {
|
|
1550
|
+
pass.setBindGroup(0, this.perFrameBindGroup);
|
|
1551
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
1552
|
+
}
|
|
1657
1553
|
renderOneModel(pass, inst) {
|
|
1658
1554
|
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
1659
1555
|
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1660
1556
|
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1661
1557
|
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1558
|
+
this.bindMainGroups(pass, inst);
|
|
1662
1559
|
this.drawOpaque(pass, inst, this.modelPipeline);
|
|
1663
|
-
this.renderEyes(pass, inst);
|
|
1664
1560
|
this.drawOutlines(pass, inst, false);
|
|
1665
|
-
this.
|
|
1561
|
+
this.bindMainGroups(pass, inst);
|
|
1666
1562
|
this.drawTransparent(pass, inst, this.modelPipeline);
|
|
1667
1563
|
this.drawOutlines(pass, inst, true);
|
|
1668
1564
|
}
|
|
@@ -1677,10 +1573,6 @@ export class Engine {
|
|
|
1677
1573
|
this.cameraMatrixData[34] = cameraPos.z;
|
|
1678
1574
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
1679
1575
|
}
|
|
1680
|
-
updateRenderTarget() {
|
|
1681
|
-
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
1682
|
-
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
|
|
1683
|
-
}
|
|
1684
1576
|
updateSkinMatrices() {
|
|
1685
1577
|
this.forEachInstance((inst) => {
|
|
1686
1578
|
const skinMatrices = inst.model.getSkinMatrices();
|
|
@@ -1689,10 +1581,12 @@ export class Engine {
|
|
|
1689
1581
|
}
|
|
1690
1582
|
drawOutlines(pass, inst, transparent) {
|
|
1691
1583
|
pass.setPipeline(this.outlinePipeline);
|
|
1584
|
+
pass.setBindGroup(0, this.outlinePerFrameBindGroup);
|
|
1585
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
1692
1586
|
const outlineType = transparent ? "transparent-outline" : "opaque-outline";
|
|
1693
1587
|
for (const draw of inst.drawCalls) {
|
|
1694
1588
|
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
1695
|
-
pass.setBindGroup(
|
|
1589
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1696
1590
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1697
1591
|
}
|
|
1698
1592
|
}
|