reze-engine 0.8.4 → 0.9.1
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 +91 -21
- package/dist/camera.d.ts +2 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +14 -7
- package/dist/engine.d.ts +30 -15
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +319 -422
- 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 +2 -19
- package/package.json +2 -2
- package/src/camera.ts +15 -7
- package/src/engine.ts +365 -463
- package/src/model.ts +0 -3
- package/src/pmx-loader.ts +2 -23
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);
|
|
@@ -562,22 +570,17 @@ export class Engine {
|
|
|
562
570
|
}
|
|
563
571
|
let worldPos = skinnedPos.xyz;
|
|
564
572
|
let worldNormal = normalize(skinnedNrm);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
let
|
|
568
|
-
let
|
|
573
|
+
// Screen-stable edgeline: extrusion ∝ camera distance (same idea as MMD viewers / babylon-mmd-style scaling)
|
|
574
|
+
let camDist = max(length(camera.viewPos - worldPos), 0.25);
|
|
575
|
+
let refDist = 30.0;
|
|
576
|
+
let edgeScale = 0.03;
|
|
577
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * edgeScale * (camDist / refDist);
|
|
569
578
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
570
579
|
return output;
|
|
571
580
|
}
|
|
572
581
|
|
|
573
582
|
@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;
|
|
583
|
+
return material.edgeColor;
|
|
581
584
|
}
|
|
582
585
|
`,
|
|
583
586
|
});
|
|
@@ -590,59 +593,14 @@ export class Engine {
|
|
|
590
593
|
cullMode: "back",
|
|
591
594
|
depthStencil: {
|
|
592
595
|
format: "depth24plus-stencil8",
|
|
593
|
-
|
|
594
|
-
depthCompare: "less-equal",
|
|
595
|
-
},
|
|
596
|
-
});
|
|
597
|
-
// Hair outline pipeline
|
|
598
|
-
this.hairOutlinePipeline = this.createRenderPipeline({
|
|
599
|
-
label: "hair outline pipeline",
|
|
600
|
-
layout: outlinePipelineLayout,
|
|
601
|
-
shaderModule: outlineShaderModule,
|
|
602
|
-
vertexBuffers: outlineVertexBuffers,
|
|
603
|
-
fragmentTarget: standardBlend,
|
|
604
|
-
cullMode: "back",
|
|
605
|
-
depthStencil: {
|
|
606
|
-
format: "depth24plus-stencil8",
|
|
596
|
+
// Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
|
|
607
597
|
depthWriteEnabled: false,
|
|
608
598
|
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
599
|
},
|
|
642
600
|
});
|
|
643
|
-
//
|
|
644
|
-
const
|
|
645
|
-
label: "
|
|
601
|
+
// GPU picking: encode (modelIndex, materialIndex) as color
|
|
602
|
+
const pickShaderModule = this.device.createShaderModule({
|
|
603
|
+
label: "pick shader",
|
|
646
604
|
code: /* wgsl */ `
|
|
647
605
|
struct CameraUniforms {
|
|
648
606
|
view: mat4x4f,
|
|
@@ -650,91 +608,87 @@ export class Engine {
|
|
|
650
608
|
viewPos: vec3f,
|
|
651
609
|
_padding: f32,
|
|
652
610
|
};
|
|
611
|
+
struct PickId {
|
|
612
|
+
modelId: f32,
|
|
613
|
+
materialId: f32,
|
|
614
|
+
_p1: f32,
|
|
615
|
+
_p2: f32,
|
|
616
|
+
};
|
|
653
617
|
|
|
654
618
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
655
|
-
@group(
|
|
619
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
620
|
+
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
656
621
|
|
|
657
622
|
@vertex fn vs(
|
|
658
623
|
@location(0) position: vec3f,
|
|
659
624
|
@location(1) normal: vec3f,
|
|
625
|
+
@location(2) uv: vec2f,
|
|
660
626
|
@location(3) joints0: vec4<u32>,
|
|
661
627
|
@location(4) weights0: vec4<f32>
|
|
662
628
|
) -> @builtin(position) vec4f {
|
|
663
629
|
let pos4 = vec4f(position, 1.0);
|
|
664
|
-
|
|
665
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
666
630
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
667
631
|
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;
|
|
632
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
633
|
+
var sp = vec4f(0.0);
|
|
634
|
+
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
635
|
+
return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
680
636
|
}
|
|
681
637
|
|
|
682
638
|
@fragment fn fs() -> @location(0) vec4f {
|
|
683
|
-
return vec4f(
|
|
639
|
+
return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
|
|
684
640
|
}
|
|
685
641
|
`,
|
|
686
642
|
});
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
643
|
+
this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
644
|
+
label: "pick per-frame layout",
|
|
645
|
+
entries: [
|
|
646
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
647
|
+
],
|
|
648
|
+
});
|
|
649
|
+
this.pickPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
650
|
+
label: "pick per-instance layout",
|
|
651
|
+
entries: [
|
|
652
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
653
|
+
],
|
|
654
|
+
});
|
|
655
|
+
this.pickPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
656
|
+
label: "pick per-material layout",
|
|
657
|
+
entries: [
|
|
658
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
659
|
+
],
|
|
660
|
+
});
|
|
661
|
+
const pickPipelineLayout = this.device.createPipelineLayout({
|
|
662
|
+
label: "pick pipeline layout",
|
|
663
|
+
bindGroupLayouts: [this.pickPerFrameBindGroupLayout, this.pickPerInstanceBindGroupLayout, this.pickPerMaterialBindGroupLayout],
|
|
664
|
+
});
|
|
665
|
+
this.pickPerFrameBindGroup = this.device.createBindGroup({
|
|
666
|
+
label: "pick per-frame bind group",
|
|
667
|
+
layout: this.pickPerFrameBindGroupLayout,
|
|
668
|
+
entries: [
|
|
669
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
670
|
+
],
|
|
671
|
+
});
|
|
672
|
+
this.pickPipeline = this.device.createRenderPipeline({
|
|
673
|
+
label: "pick pipeline",
|
|
674
|
+
layout: pickPipelineLayout,
|
|
675
|
+
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
676
|
+
fragment: {
|
|
677
|
+
module: pickShaderModule,
|
|
678
|
+
targets: [{ format: "rgba8unorm" }],
|
|
696
679
|
},
|
|
697
|
-
|
|
698
|
-
cullMode: "none",
|
|
680
|
+
primitive: { cullMode: "none" },
|
|
699
681
|
depthStencil: {
|
|
700
|
-
format: "depth24plus
|
|
682
|
+
format: "depth24plus",
|
|
701
683
|
depthWriteEnabled: true,
|
|
702
684
|
depthCompare: "less-equal",
|
|
703
|
-
depthBias: 0.0,
|
|
704
|
-
depthBiasSlopeScale: 0.0,
|
|
705
|
-
depthBiasClamp: 0.0,
|
|
706
685
|
},
|
|
707
686
|
});
|
|
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);
|
|
687
|
+
this.pickReadbackBuffer = this.device.createBuffer({
|
|
688
|
+
label: "pick readback",
|
|
689
|
+
size: 256,
|
|
690
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
691
|
+
});
|
|
738
692
|
}
|
|
739
693
|
// Step 3: Setup canvas resize handling
|
|
740
694
|
setupResize() {
|
|
@@ -788,10 +742,24 @@ export class Engine {
|
|
|
788
742
|
depthStoreOp: "store",
|
|
789
743
|
stencilClearValue: 0,
|
|
790
744
|
stencilLoadOp: "clear",
|
|
791
|
-
stencilStoreOp: "discard",
|
|
745
|
+
stencilStoreOp: "discard",
|
|
792
746
|
},
|
|
793
747
|
};
|
|
794
748
|
this.camera.aspect = width / height;
|
|
749
|
+
if (this.onRaycast) {
|
|
750
|
+
this.pickTexture = this.device.createTexture({
|
|
751
|
+
label: "pick render target",
|
|
752
|
+
size: [width, height],
|
|
753
|
+
format: "rgba8unorm",
|
|
754
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
755
|
+
});
|
|
756
|
+
this.pickDepthTexture = this.device.createTexture({
|
|
757
|
+
label: "pick depth",
|
|
758
|
+
size: [width, height],
|
|
759
|
+
format: "depth24plus",
|
|
760
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
795
763
|
}
|
|
796
764
|
}
|
|
797
765
|
// Step 4: Create camera and uniform buffer
|
|
@@ -823,6 +791,24 @@ export class Engine {
|
|
|
823
791
|
this.cameraTargetOffset.y = offset?.y ?? 0;
|
|
824
792
|
this.cameraTargetOffset.z = offset?.z ?? 0;
|
|
825
793
|
}
|
|
794
|
+
/** Souls-style follow cam: orbit center tracks a model bone each frame. Shorthand for setCameraTarget(model, boneName, offset). */
|
|
795
|
+
setCameraFollow(model, boneName, offset) {
|
|
796
|
+
if (model === null) {
|
|
797
|
+
this.cameraTargetModel = null;
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
this.cameraTargetModel = model;
|
|
801
|
+
this.cameraTargetBoneName = boneName ?? "全ての親";
|
|
802
|
+
this.cameraTargetOffset.x = offset?.x ?? 0;
|
|
803
|
+
this.cameraTargetOffset.y = offset?.y ?? 0;
|
|
804
|
+
this.cameraTargetOffset.z = offset?.z ?? 0;
|
|
805
|
+
}
|
|
806
|
+
getCameraDistance() { return this.camera.radius; }
|
|
807
|
+
setCameraDistance(d) { this.camera.radius = d; }
|
|
808
|
+
getCameraAlpha() { return this.camera.alpha; }
|
|
809
|
+
setCameraAlpha(a) { this.camera.alpha = a; }
|
|
810
|
+
getCameraBeta() { return this.camera.beta; }
|
|
811
|
+
setCameraBeta(b) { this.camera.beta = b; }
|
|
826
812
|
// Step 5: Create lighting buffers
|
|
827
813
|
setupLighting() {
|
|
828
814
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -954,9 +940,6 @@ export class Engine {
|
|
|
954
940
|
await this.setupModelInstance(key, model, basePath);
|
|
955
941
|
return key;
|
|
956
942
|
}
|
|
957
|
-
async registerModel(model, pmxPath) {
|
|
958
|
-
return this.addModel(model, pmxPath);
|
|
959
|
-
}
|
|
960
943
|
removeModel(name) {
|
|
961
944
|
this.modelInstances.delete(name);
|
|
962
945
|
}
|
|
@@ -1024,11 +1007,8 @@ export class Engine {
|
|
|
1024
1007
|
inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
|
|
1025
1008
|
});
|
|
1026
1009
|
}
|
|
1027
|
-
instances() {
|
|
1028
|
-
return this.modelInstances.values();
|
|
1029
|
-
}
|
|
1030
1010
|
forEachInstance(fn) {
|
|
1031
|
-
for (const inst of this.
|
|
1011
|
+
for (const inst of this.modelInstances.values())
|
|
1032
1012
|
fn(inst);
|
|
1033
1013
|
}
|
|
1034
1014
|
updateInstances(deltaTime) {
|
|
@@ -1098,6 +1078,20 @@ export class Engine {
|
|
|
1098
1078
|
{ binding: 1, resource: { buffer: skinMatrixBuffer } },
|
|
1099
1079
|
],
|
|
1100
1080
|
});
|
|
1081
|
+
const mainPerInstanceBindGroup = this.device.createBindGroup({
|
|
1082
|
+
label: `${name}: main per-instance bind group`,
|
|
1083
|
+
layout: this.mainPerInstanceBindGroupLayout,
|
|
1084
|
+
entries: [
|
|
1085
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1086
|
+
],
|
|
1087
|
+
});
|
|
1088
|
+
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
1089
|
+
label: `${name}: pick per-instance bind group`,
|
|
1090
|
+
layout: this.pickPerInstanceBindGroupLayout,
|
|
1091
|
+
entries: [
|
|
1092
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1093
|
+
],
|
|
1094
|
+
});
|
|
1101
1095
|
const inst = {
|
|
1102
1096
|
name,
|
|
1103
1097
|
model,
|
|
@@ -1110,6 +1104,9 @@ export class Engine {
|
|
|
1110
1104
|
drawCalls: [],
|
|
1111
1105
|
shadowDrawCalls: [],
|
|
1112
1106
|
shadowBindGroup,
|
|
1107
|
+
mainPerInstanceBindGroup,
|
|
1108
|
+
pickPerInstanceBindGroup,
|
|
1109
|
+
pickDrawCalls: [],
|
|
1113
1110
|
hiddenMaterials: new Set(),
|
|
1114
1111
|
physics,
|
|
1115
1112
|
vertexBufferNeedsUpdate: false,
|
|
@@ -1245,6 +1242,8 @@ export class Engine {
|
|
|
1245
1242
|
throw new Error("Model has no materials");
|
|
1246
1243
|
const textures = model.getTextures();
|
|
1247
1244
|
const prefix = `${inst.name}: `;
|
|
1245
|
+
// 1-based so that (0,0) = clear color = "no hit"
|
|
1246
|
+
const modelId = this.modelInstances.size + 1;
|
|
1248
1247
|
const loadTextureByIndex = async (texIndex) => {
|
|
1249
1248
|
if (texIndex < 0 || texIndex >= textures.length)
|
|
1250
1249
|
return null;
|
|
@@ -1252,70 +1251,29 @@ export class Engine {
|
|
|
1252
1251
|
return this.createTextureFromPath(path);
|
|
1253
1252
|
};
|
|
1254
1253
|
let currentIndexOffset = 0;
|
|
1254
|
+
let materialId = 0;
|
|
1255
1255
|
for (const mat of materials) {
|
|
1256
1256
|
const indexCount = mat.vertexCount;
|
|
1257
1257
|
if (indexCount === 0)
|
|
1258
1258
|
continue;
|
|
1259
|
+
materialId++;
|
|
1259
1260
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex);
|
|
1260
1261
|
if (!diffuseTexture)
|
|
1261
1262
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1262
1263
|
const materialAlpha = mat.diffuse[3];
|
|
1263
1264
|
const isTransparent = materialAlpha < 1.0 - 0.001;
|
|
1264
|
-
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha,
|
|
1265
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1266
|
+
const textureView = diffuseTexture.createView();
|
|
1265
1267
|
const bindGroup = this.device.createBindGroup({
|
|
1266
1268
|
label: `${prefix}material: ${mat.name}`,
|
|
1267
|
-
layout: this.
|
|
1269
|
+
layout: this.mainPerMaterialBindGroupLayout,
|
|
1268
1270
|
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 } },
|
|
1271
|
+
{ binding: 0, resource: textureView },
|
|
1272
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
1275
1273
|
],
|
|
1276
1274
|
});
|
|
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
|
-
}
|
|
1275
|
+
const type = isTransparent ? "transparent" : "opaque";
|
|
1276
|
+
inst.drawCalls.push({ type, count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1319
1277
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1320
1278
|
const materialUniformData = new Float32Array([
|
|
1321
1279
|
mat.edgeColor[0], mat.edgeColor[1], mat.edgeColor[2], mat.edgeColor[3],
|
|
@@ -1324,48 +1282,42 @@ export class Engine {
|
|
|
1324
1282
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData);
|
|
1325
1283
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1326
1284
|
label: `${prefix}outline: ${mat.name}`,
|
|
1327
|
-
layout: this.
|
|
1285
|
+
layout: this.outlinePerMaterialBindGroupLayout,
|
|
1328
1286
|
entries: [
|
|
1329
|
-
{ binding: 0, resource: { buffer:
|
|
1330
|
-
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1331
|
-
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1287
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
1332
1288
|
],
|
|
1333
1289
|
});
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1290
|
+
const outlineType = isTransparent ? "transparent-outline" : "opaque-outline";
|
|
1291
|
+
inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name });
|
|
1292
|
+
}
|
|
1293
|
+
if (this.onRaycast) {
|
|
1294
|
+
const pickIdData = new Float32Array([modelId, materialId, 0, 0]);
|
|
1295
|
+
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData);
|
|
1296
|
+
const pickBindGroup = this.device.createBindGroup({
|
|
1297
|
+
label: `${prefix}pick: ${mat.name}`,
|
|
1298
|
+
layout: this.pickPerMaterialBindGroupLayout,
|
|
1299
|
+
entries: [{ binding: 0, resource: { buffer: pickIdBuffer } }],
|
|
1300
|
+
});
|
|
1301
|
+
inst.pickDrawCalls.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: pickBindGroup });
|
|
1338
1302
|
}
|
|
1339
1303
|
currentIndexOffset += indexCount;
|
|
1340
1304
|
}
|
|
1341
1305
|
for (const d of inst.drawCalls) {
|
|
1342
|
-
if (d.type === "opaque"
|
|
1306
|
+
if (d.type === "opaque")
|
|
1343
1307
|
inst.shadowDrawCalls.push(d);
|
|
1344
1308
|
}
|
|
1345
1309
|
}
|
|
1346
|
-
createMaterialUniformBuffer(label, alpha,
|
|
1310
|
+
createMaterialUniformBuffer(label, alpha, diffuseColor, ambientColor, specularColor, shininess) {
|
|
1347
1311
|
const data = new Float32Array(20);
|
|
1348
1312
|
data.set([
|
|
1349
1313
|
alpha,
|
|
1350
|
-
1.0,
|
|
1351
1314
|
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
|
|
1315
|
+
shininess,
|
|
1316
|
+
0.0,
|
|
1317
|
+
1.0, 1.0, 1.0, 0.0, // rimColor (vec3), _padding2
|
|
1318
|
+
diffuseColor[0], diffuseColor[1], diffuseColor[2], 0.0,
|
|
1319
|
+
ambientColor[0], ambientColor[1], ambientColor[2], 0.0,
|
|
1320
|
+
specularColor[0], specularColor[1], specularColor[2], 0.0,
|
|
1369
1321
|
]);
|
|
1370
1322
|
return this.createUniformBuffer(`material uniform: ${label}`, data);
|
|
1371
1323
|
}
|
|
@@ -1412,17 +1364,6 @@ export class Engine {
|
|
|
1412
1364
|
return null;
|
|
1413
1365
|
}
|
|
1414
1366
|
}
|
|
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
1367
|
renderGround(pass) {
|
|
1427
1368
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
1428
1369
|
return;
|
|
@@ -1432,149 +1373,91 @@ export class Engine {
|
|
|
1432
1373
|
pass.setBindGroup(0, this.groundDrawCall.bindGroup);
|
|
1433
1374
|
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
|
|
1434
1375
|
}
|
|
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
1376
|
performRaycast(screenX, screenY) {
|
|
1475
1377
|
if (!this.onRaycast || this.modelInstances.size === 0) {
|
|
1476
1378
|
this.onRaycast?.("", null, screenX, screenY);
|
|
1477
1379
|
return;
|
|
1478
1380
|
}
|
|
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;
|
|
1381
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1382
|
+
this.pendingPick = { x: Math.floor(screenX * dpr), y: Math.floor(screenY * dpr) };
|
|
1383
|
+
}
|
|
1384
|
+
renderPickPass(encoder) {
|
|
1385
|
+
if (!this.pendingPick || !this.pickTexture || !this.pickDepthTexture)
|
|
1386
|
+
return;
|
|
1387
|
+
const pass = encoder.beginRenderPass({
|
|
1388
|
+
colorAttachments: [{
|
|
1389
|
+
view: this.pickTexture.createView(),
|
|
1390
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1391
|
+
loadOp: "clear",
|
|
1392
|
+
storeOp: "store",
|
|
1393
|
+
}],
|
|
1394
|
+
depthStencilAttachment: {
|
|
1395
|
+
view: this.pickDepthTexture.createView(),
|
|
1396
|
+
depthClearValue: 1.0,
|
|
1397
|
+
depthLoadOp: "clear",
|
|
1398
|
+
depthStoreOp: "store",
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1401
|
+
pass.setPipeline(this.pickPipeline);
|
|
1402
|
+
pass.setBindGroup(0, this.pickPerFrameBindGroup);
|
|
1503
1403
|
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
|
-
|
|
1404
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
1405
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1406
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1407
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1408
|
+
pass.setBindGroup(1, inst.pickPerInstanceBindGroup);
|
|
1409
|
+
for (const draw of inst.pickDrawCalls) {
|
|
1410
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1411
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
pass.end();
|
|
1415
|
+
// Copy the single pixel under cursor to readback buffer
|
|
1416
|
+
const px = Math.min(this.pendingPick.x, this.pickTexture.width - 1);
|
|
1417
|
+
const py = Math.min(this.pendingPick.y, this.pickTexture.height - 1);
|
|
1418
|
+
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 });
|
|
1419
|
+
}
|
|
1420
|
+
async resolvePickResult(screenX, screenY) {
|
|
1421
|
+
if (!this.onRaycast)
|
|
1422
|
+
return;
|
|
1423
|
+
await this.pickReadbackBuffer.mapAsync(GPUMapMode.READ);
|
|
1424
|
+
const data = new Uint8Array(this.pickReadbackBuffer.getMappedRange());
|
|
1425
|
+
const modelId = data[0];
|
|
1426
|
+
const materialId = data[1];
|
|
1427
|
+
this.pickReadbackBuffer.unmap();
|
|
1428
|
+
if (modelId === 0) {
|
|
1429
|
+
this.onRaycast("", null, screenX, screenY);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
// Find model by 1-based index
|
|
1433
|
+
let idx = 1;
|
|
1434
|
+
let hitModel = "";
|
|
1435
|
+
for (const [name] of this.modelInstances) {
|
|
1436
|
+
if (idx === modelId) {
|
|
1437
|
+
hitModel = name;
|
|
1438
|
+
break;
|
|
1537
1439
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1440
|
+
idx++;
|
|
1441
|
+
}
|
|
1442
|
+
// Find material by 1-based index (skipping zero-vertex materials)
|
|
1443
|
+
let hitMaterial = null;
|
|
1444
|
+
if (hitModel) {
|
|
1445
|
+
const inst = this.modelInstances.get(hitModel);
|
|
1446
|
+
if (inst) {
|
|
1447
|
+
const materials = inst.model.getMaterials();
|
|
1448
|
+
let matIdx = 0;
|
|
1449
|
+
for (const mat of materials) {
|
|
1450
|
+
if (mat.vertexCount === 0)
|
|
1451
|
+
continue;
|
|
1452
|
+
matIdx++;
|
|
1453
|
+
if (matIdx === materialId) {
|
|
1454
|
+
hitMaterial = mat.name;
|
|
1548
1455
|
break;
|
|
1549
1456
|
}
|
|
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
1457
|
}
|
|
1572
1458
|
}
|
|
1573
|
-
});
|
|
1574
|
-
if (this.onRaycast) {
|
|
1575
|
-
const hit = closest;
|
|
1576
|
-
this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY);
|
|
1577
1459
|
}
|
|
1460
|
+
this.onRaycast(hitModel, hitMaterial, screenX, screenY);
|
|
1578
1461
|
}
|
|
1579
1462
|
render() {
|
|
1580
1463
|
if (!this.multisampleTexture || !this.camera || !this.device)
|
|
@@ -1622,9 +1505,21 @@ export class Engine {
|
|
|
1622
1505
|
if (this.hasGround)
|
|
1623
1506
|
this.renderGround(pass);
|
|
1624
1507
|
pass.end();
|
|
1508
|
+
const pick = this.pendingPick;
|
|
1509
|
+
if (pick && hasModels)
|
|
1510
|
+
this.renderPickPass(encoder);
|
|
1625
1511
|
this.device.queue.submit([encoder.finish()]);
|
|
1512
|
+
if (pick) {
|
|
1513
|
+
this.pendingPick = null;
|
|
1514
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1515
|
+
this.resolvePickResult(pick.x / dpr, pick.y / dpr);
|
|
1516
|
+
}
|
|
1626
1517
|
this.updateStats(performance.now() - currentTime);
|
|
1627
1518
|
}
|
|
1519
|
+
updateRenderTarget() {
|
|
1520
|
+
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
1521
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
|
|
1522
|
+
}
|
|
1628
1523
|
drawInstanceShadow(sp, inst) {
|
|
1629
1524
|
sp.setBindGroup(0, inst.shadowBindGroup);
|
|
1630
1525
|
sp.setVertexBuffer(0, inst.vertexBuffer);
|
|
@@ -1640,7 +1535,7 @@ export class Engine {
|
|
|
1640
1535
|
pass.setPipeline(pipeline);
|
|
1641
1536
|
for (const draw of inst.drawCalls) {
|
|
1642
1537
|
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1643
|
-
pass.setBindGroup(
|
|
1538
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1644
1539
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1645
1540
|
}
|
|
1646
1541
|
}
|
|
@@ -1649,20 +1544,24 @@ export class Engine {
|
|
|
1649
1544
|
pass.setPipeline(pipeline);
|
|
1650
1545
|
for (const draw of inst.drawCalls) {
|
|
1651
1546
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1652
|
-
pass.setBindGroup(
|
|
1547
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1653
1548
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1654
1549
|
}
|
|
1655
1550
|
}
|
|
1656
1551
|
}
|
|
1552
|
+
bindMainGroups(pass, inst) {
|
|
1553
|
+
pass.setBindGroup(0, this.perFrameBindGroup);
|
|
1554
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
1555
|
+
}
|
|
1657
1556
|
renderOneModel(pass, inst) {
|
|
1658
1557
|
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
1659
1558
|
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1660
1559
|
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1661
1560
|
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1561
|
+
this.bindMainGroups(pass, inst);
|
|
1662
1562
|
this.drawOpaque(pass, inst, this.modelPipeline);
|
|
1663
|
-
this.renderEyes(pass, inst);
|
|
1664
1563
|
this.drawOutlines(pass, inst, false);
|
|
1665
|
-
this.
|
|
1564
|
+
this.bindMainGroups(pass, inst);
|
|
1666
1565
|
this.drawTransparent(pass, inst, this.modelPipeline);
|
|
1667
1566
|
this.drawOutlines(pass, inst, true);
|
|
1668
1567
|
}
|
|
@@ -1677,10 +1576,6 @@ export class Engine {
|
|
|
1677
1576
|
this.cameraMatrixData[34] = cameraPos.z;
|
|
1678
1577
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
1679
1578
|
}
|
|
1680
|
-
updateRenderTarget() {
|
|
1681
|
-
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
1682
|
-
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
|
|
1683
|
-
}
|
|
1684
1579
|
updateSkinMatrices() {
|
|
1685
1580
|
this.forEachInstance((inst) => {
|
|
1686
1581
|
const skinMatrices = inst.model.getSkinMatrices();
|
|
@@ -1689,10 +1584,12 @@ export class Engine {
|
|
|
1689
1584
|
}
|
|
1690
1585
|
drawOutlines(pass, inst, transparent) {
|
|
1691
1586
|
pass.setPipeline(this.outlinePipeline);
|
|
1587
|
+
pass.setBindGroup(0, this.outlinePerFrameBindGroup);
|
|
1588
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
1692
1589
|
const outlineType = transparent ? "transparent-outline" : "opaque-outline";
|
|
1693
1590
|
for (const draw of inst.drawCalls) {
|
|
1694
1591
|
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
1695
|
-
pass.setBindGroup(
|
|
1592
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1696
1593
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1697
1594
|
}
|
|
1698
1595
|
}
|