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/src/engine.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Camera } from "./camera"
|
|
2
|
-
import { Mat4,
|
|
2
|
+
import { Mat4, Vec3 } from "./math"
|
|
3
3
|
import { Model } from "./model"
|
|
4
4
|
import { PmxLoader } from "./pmx-loader"
|
|
5
5
|
import { Physics } from "./physics"
|
|
@@ -37,14 +37,9 @@ export interface EngineStats {
|
|
|
37
37
|
|
|
38
38
|
type DrawCallType =
|
|
39
39
|
| "opaque"
|
|
40
|
-
| "eye"
|
|
41
|
-
| "hair-over-eyes"
|
|
42
|
-
| "hair-over-non-eyes"
|
|
43
40
|
| "transparent"
|
|
44
41
|
| "ground"
|
|
45
42
|
| "opaque-outline"
|
|
46
|
-
| "eye-outline"
|
|
47
|
-
| "hair-outline"
|
|
48
43
|
| "transparent-outline"
|
|
49
44
|
|
|
50
45
|
interface DrawCall {
|
|
@@ -55,6 +50,12 @@ interface DrawCall {
|
|
|
55
50
|
materialName: string
|
|
56
51
|
}
|
|
57
52
|
|
|
53
|
+
interface PickDrawCall {
|
|
54
|
+
count: number
|
|
55
|
+
firstIndex: number
|
|
56
|
+
bindGroup: GPUBindGroup
|
|
57
|
+
}
|
|
58
|
+
|
|
58
59
|
interface ModelInstance {
|
|
59
60
|
name: string
|
|
60
61
|
model: Model
|
|
@@ -67,6 +68,9 @@ interface ModelInstance {
|
|
|
67
68
|
drawCalls: DrawCall[]
|
|
68
69
|
shadowDrawCalls: DrawCall[]
|
|
69
70
|
shadowBindGroup: GPUBindGroup
|
|
71
|
+
mainPerInstanceBindGroup: GPUBindGroup
|
|
72
|
+
pickPerInstanceBindGroup: GPUBindGroup
|
|
73
|
+
pickDrawCalls: PickDrawCall[]
|
|
70
74
|
hiddenMaterials: Set<string>
|
|
71
75
|
physics: Physics | null
|
|
72
76
|
vertexBufferNeedsUpdate: boolean
|
|
@@ -97,25 +101,20 @@ export class Engine {
|
|
|
97
101
|
private lightCount = 0
|
|
98
102
|
private resizeObserver: ResizeObserver | null = null
|
|
99
103
|
private depthTexture!: GPUTexture
|
|
100
|
-
// Material rendering pipelines
|
|
101
104
|
private modelPipeline!: GPURenderPipeline
|
|
102
|
-
private eyePipeline!: GPURenderPipeline
|
|
103
|
-
private hairPipelineOverEyes!: GPURenderPipeline
|
|
104
|
-
private hairPipelineOverNonEyes!: GPURenderPipeline
|
|
105
|
-
private hairDepthPipeline!: GPURenderPipeline
|
|
106
|
-
// Ground (shadow only)
|
|
107
105
|
private groundShadowPipeline!: GPURenderPipeline
|
|
108
106
|
private groundShadowBindGroupLayout!: GPUBindGroupLayout
|
|
109
|
-
// Outline pipelines
|
|
110
107
|
private outlinePipeline!: GPURenderPipeline
|
|
111
|
-
private
|
|
112
|
-
private
|
|
113
|
-
private
|
|
108
|
+
private mainPerFrameBindGroupLayout!: GPUBindGroupLayout
|
|
109
|
+
private mainPerInstanceBindGroupLayout!: GPUBindGroupLayout
|
|
110
|
+
private mainPerMaterialBindGroupLayout!: GPUBindGroupLayout
|
|
111
|
+
private outlinePerFrameBindGroupLayout!: GPUBindGroupLayout
|
|
112
|
+
private outlinePerMaterialBindGroupLayout!: GPUBindGroupLayout
|
|
113
|
+
private perFrameBindGroup!: GPUBindGroup
|
|
114
|
+
private outlinePerFrameBindGroup!: GPUBindGroup
|
|
114
115
|
private multisampleTexture!: GPUTexture
|
|
115
116
|
private static readonly MULTISAMPLE_COUNT = 4
|
|
116
117
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
117
|
-
// Post-alpha eye: eyes write stencil, hair-over-eyes reads it for see-through bangs (MMD-style).
|
|
118
|
-
private readonly STENCIL_EYE_VALUE = 1
|
|
119
118
|
|
|
120
119
|
// Ambient light settings
|
|
121
120
|
private ambientColor!: Vec3
|
|
@@ -142,9 +141,18 @@ export class Engine {
|
|
|
142
141
|
private shadowVPLightZ = Number.NaN
|
|
143
142
|
|
|
144
143
|
private onRaycast?: RaycastCallback
|
|
145
|
-
// Double-tap detection
|
|
146
144
|
private lastTouchTime = 0
|
|
147
|
-
private readonly DOUBLE_TAP_DELAY = 300
|
|
145
|
+
private readonly DOUBLE_TAP_DELAY = 300
|
|
146
|
+
// GPU picking
|
|
147
|
+
private pickPipeline!: GPURenderPipeline
|
|
148
|
+
private pickPerFrameBindGroupLayout!: GPUBindGroupLayout
|
|
149
|
+
private pickPerInstanceBindGroupLayout!: GPUBindGroupLayout
|
|
150
|
+
private pickPerMaterialBindGroupLayout!: GPUBindGroupLayout
|
|
151
|
+
private pickPerFrameBindGroup!: GPUBindGroup
|
|
152
|
+
private pickTexture!: GPUTexture
|
|
153
|
+
private pickDepthTexture!: GPUTexture
|
|
154
|
+
private pickReadbackBuffer!: GPUBuffer
|
|
155
|
+
private pendingPick: { x: number; y: number } | null = null
|
|
148
156
|
|
|
149
157
|
private modelInstances = new Map<string, ModelInstance>()
|
|
150
158
|
private materialSampler!: GPUSampler
|
|
@@ -294,24 +302,6 @@ export class Engine {
|
|
|
294
302
|
},
|
|
295
303
|
]
|
|
296
304
|
|
|
297
|
-
const depthOnlyVertexBuffers: GPUVertexBufferLayout[] = [
|
|
298
|
-
{
|
|
299
|
-
arrayStride: 8 * 4,
|
|
300
|
-
attributes: [
|
|
301
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
302
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
303
|
-
],
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
arrayStride: 4 * 2,
|
|
307
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
arrayStride: 4,
|
|
311
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
312
|
-
},
|
|
313
|
-
]
|
|
314
|
-
|
|
315
305
|
const standardBlend: GPUColorTargetState = {
|
|
316
306
|
format: this.presentationFormat,
|
|
317
307
|
blend: {
|
|
@@ -350,17 +340,17 @@ export class Engine {
|
|
|
350
340
|
|
|
351
341
|
struct MaterialUniforms {
|
|
352
342
|
alpha: f32,
|
|
353
|
-
alphaMultiplier: f32,
|
|
354
343
|
rimIntensity: f32,
|
|
355
344
|
shininess: f32,
|
|
345
|
+
_padding1: f32,
|
|
356
346
|
rimColor: vec3f,
|
|
357
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
358
|
-
diffuseColor: vec3f,
|
|
359
347
|
_padding2: f32,
|
|
360
|
-
|
|
348
|
+
diffuseColor: vec3f,
|
|
361
349
|
_padding3: f32,
|
|
362
|
-
|
|
350
|
+
ambientColor: vec3f,
|
|
363
351
|
_padding4: f32,
|
|
352
|
+
specularColor: vec3f,
|
|
353
|
+
_padding5: f32,
|
|
364
354
|
};
|
|
365
355
|
|
|
366
356
|
struct VertexOutput {
|
|
@@ -370,12 +360,15 @@ export class Engine {
|
|
|
370
360
|
@location(2) worldPos: vec3f,
|
|
371
361
|
};
|
|
372
362
|
|
|
363
|
+
// group 0: per-frame (bound once per pass)
|
|
373
364
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
374
365
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
375
|
-
@group(0) @binding(2) var
|
|
376
|
-
|
|
377
|
-
@group(
|
|
378
|
-
|
|
366
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
367
|
+
// group 1: per-instance (bound once per model)
|
|
368
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
369
|
+
// group 2: per-material (bound per draw call)
|
|
370
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
371
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
379
372
|
|
|
380
373
|
@vertex fn vs(
|
|
381
374
|
@location(0) position: vec3f,
|
|
@@ -387,7 +380,6 @@ export class Engine {
|
|
|
387
380
|
var output: VertexOutput;
|
|
388
381
|
let pos4 = vec4f(position, 1.0);
|
|
389
382
|
|
|
390
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
391
383
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
392
384
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
393
385
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -411,11 +403,7 @@ export class Engine {
|
|
|
411
403
|
}
|
|
412
404
|
|
|
413
405
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
414
|
-
|
|
415
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
416
|
-
if (material.isOverEyes > 0.5) {
|
|
417
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
418
|
-
}
|
|
406
|
+
let finalAlpha = material.alpha;
|
|
419
407
|
if (finalAlpha < 0.001) {
|
|
420
408
|
discard;
|
|
421
409
|
}
|
|
@@ -423,18 +411,14 @@ export class Engine {
|
|
|
423
411
|
let n = normalize(input.normal);
|
|
424
412
|
let textureColor = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
425
413
|
|
|
426
|
-
// View direction for specular and rim
|
|
427
414
|
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
428
415
|
|
|
429
|
-
// Simple lighting: global ambient + diffuse lighting
|
|
430
416
|
let albedo = textureColor * material.diffuseColor;
|
|
431
417
|
|
|
432
|
-
// Precompute material values
|
|
433
418
|
let minSpec = light.ambientColor.w;
|
|
434
419
|
let effectiveSpecular = max(material.specularColor, vec3f(minSpec));
|
|
435
420
|
let specPower = max(material.shininess, 1.0);
|
|
436
421
|
|
|
437
|
-
// Single directional light
|
|
438
422
|
let l = -light.lights[0].direction.xyz;
|
|
439
423
|
let nDotL = max(dot(n, l), 0.0);
|
|
440
424
|
let intensity = light.lights[0].color.w;
|
|
@@ -442,7 +426,6 @@ export class Engine {
|
|
|
442
426
|
|
|
443
427
|
let lightAccum = light.ambientColor.xyz + radiance * nDotL;
|
|
444
428
|
|
|
445
|
-
// Blinn-Phong specular
|
|
446
429
|
let h = normalize(l + viewDir);
|
|
447
430
|
let nDotH = max(dot(n, h), 0.0);
|
|
448
431
|
let specFactor = pow(nDotH, specPower);
|
|
@@ -450,9 +433,8 @@ export class Engine {
|
|
|
450
433
|
|
|
451
434
|
let litColor = albedo * lightAccum;
|
|
452
435
|
|
|
453
|
-
// Rim light calculation - proper Fresnel for edge-only highlights
|
|
454
436
|
let fresnel = 1.0 - abs(dot(n, viewDir));
|
|
455
|
-
let rimFactor = pow(fresnel, 4.0);
|
|
437
|
+
let rimFactor = pow(fresnel, 4.0);
|
|
456
438
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
457
439
|
|
|
458
440
|
let color = litColor + specularAccum + rimLight;
|
|
@@ -462,22 +444,44 @@ export class Engine {
|
|
|
462
444
|
`,
|
|
463
445
|
})
|
|
464
446
|
|
|
465
|
-
//
|
|
466
|
-
this.
|
|
467
|
-
label: "main
|
|
447
|
+
// group 0: per-frame (camera + light + sampler) — bound once per pass
|
|
448
|
+
this.mainPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
449
|
+
label: "main per-frame bind group layout",
|
|
468
450
|
entries: [
|
|
469
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
470
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
471
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
451
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
452
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
453
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
454
|
+
],
|
|
455
|
+
})
|
|
456
|
+
// group 1: per-instance (skinMats) — bound once per model
|
|
457
|
+
this.mainPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
458
|
+
label: "main per-instance bind group layout",
|
|
459
|
+
entries: [
|
|
460
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
461
|
+
],
|
|
462
|
+
})
|
|
463
|
+
// group 2: per-material (texture + material uniforms) — bound per draw call
|
|
464
|
+
this.mainPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
465
|
+
label: "main per-material bind group layout",
|
|
466
|
+
entries: [
|
|
467
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
468
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
475
469
|
],
|
|
476
470
|
})
|
|
477
471
|
|
|
478
472
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
479
473
|
label: "main pipeline layout",
|
|
480
|
-
bindGroupLayouts: [this.
|
|
474
|
+
bindGroupLayouts: [this.mainPerFrameBindGroupLayout, this.mainPerInstanceBindGroupLayout, this.mainPerMaterialBindGroupLayout],
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
this.perFrameBindGroup = this.device.createBindGroup({
|
|
478
|
+
label: "main per-frame bind group",
|
|
479
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
480
|
+
entries: [
|
|
481
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
482
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
483
|
+
{ binding: 2, resource: this.materialSampler },
|
|
484
|
+
],
|
|
481
485
|
})
|
|
482
486
|
|
|
483
487
|
this.modelPipeline = this.createRenderPipeline({
|
|
@@ -610,19 +614,32 @@ export class Engine {
|
|
|
610
614
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
611
615
|
})
|
|
612
616
|
|
|
613
|
-
//
|
|
614
|
-
this.
|
|
615
|
-
label: "outline bind group layout",
|
|
617
|
+
// Outline: group 0 = per-frame (camera), group 1 = per-instance (skinMats), group 2 = per-material (edge uniforms)
|
|
618
|
+
this.outlinePerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
619
|
+
label: "outline per-frame bind group layout",
|
|
616
620
|
entries: [
|
|
617
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
618
|
-
|
|
619
|
-
|
|
621
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
622
|
+
],
|
|
623
|
+
})
|
|
624
|
+
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
625
|
+
this.outlinePerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
626
|
+
label: "outline per-material bind group layout",
|
|
627
|
+
entries: [
|
|
628
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
620
629
|
],
|
|
621
630
|
})
|
|
622
631
|
|
|
623
632
|
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
624
633
|
label: "outline pipeline layout",
|
|
625
|
-
bindGroupLayouts: [this.
|
|
634
|
+
bindGroupLayouts: [this.outlinePerFrameBindGroupLayout, this.mainPerInstanceBindGroupLayout, this.outlinePerMaterialBindGroupLayout],
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
638
|
+
label: "outline per-frame bind group",
|
|
639
|
+
layout: this.outlinePerFrameBindGroupLayout,
|
|
640
|
+
entries: [
|
|
641
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
642
|
+
],
|
|
626
643
|
})
|
|
627
644
|
|
|
628
645
|
const outlineShaderModule = this.device.createShaderModule({
|
|
@@ -638,14 +655,17 @@ export class Engine {
|
|
|
638
655
|
struct MaterialUniforms {
|
|
639
656
|
edgeColor: vec4f,
|
|
640
657
|
edgeSize: f32,
|
|
641
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
642
658
|
_padding1: f32,
|
|
643
659
|
_padding2: f32,
|
|
660
|
+
_padding3: f32,
|
|
644
661
|
};
|
|
645
662
|
|
|
663
|
+
// group 0: per-frame
|
|
646
664
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
647
|
-
|
|
648
|
-
@group(
|
|
665
|
+
// group 1: per-instance
|
|
666
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
667
|
+
// group 2: per-material
|
|
668
|
+
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
|
|
649
669
|
|
|
650
670
|
struct VertexOutput {
|
|
651
671
|
@builtin(position) position: vec4f,
|
|
@@ -660,7 +680,6 @@ export class Engine {
|
|
|
660
680
|
var output: VertexOutput;
|
|
661
681
|
let pos4 = vec4f(position, 1.0);
|
|
662
682
|
|
|
663
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
664
683
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
665
684
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
666
685
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -678,7 +697,6 @@ export class Engine {
|
|
|
678
697
|
let worldPos = skinnedPos.xyz;
|
|
679
698
|
let worldNormal = normalize(skinnedNrm);
|
|
680
699
|
|
|
681
|
-
// MMD invert hull: expand vertices outward along normals
|
|
682
700
|
let scaleFactor = 0.01;
|
|
683
701
|
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
684
702
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
@@ -686,13 +704,7 @@ export class Engine {
|
|
|
686
704
|
}
|
|
687
705
|
|
|
688
706
|
@fragment fn fs() -> @location(0) vec4f {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (material.isOverEyes > 0.5) {
|
|
692
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return color;
|
|
707
|
+
return material.edgeColor;
|
|
696
708
|
}
|
|
697
709
|
`,
|
|
698
710
|
})
|
|
@@ -711,57 +723,9 @@ export class Engine {
|
|
|
711
723
|
},
|
|
712
724
|
})
|
|
713
725
|
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
label: "
|
|
717
|
-
layout: outlinePipelineLayout,
|
|
718
|
-
shaderModule: outlineShaderModule,
|
|
719
|
-
vertexBuffers: outlineVertexBuffers,
|
|
720
|
-
fragmentTarget: standardBlend,
|
|
721
|
-
cullMode: "back",
|
|
722
|
-
depthStencil: {
|
|
723
|
-
format: "depth24plus-stencil8",
|
|
724
|
-
depthWriteEnabled: false,
|
|
725
|
-
depthCompare: "less-equal",
|
|
726
|
-
depthBias: -0.0001,
|
|
727
|
-
depthBiasSlopeScale: 0.0,
|
|
728
|
-
depthBiasClamp: 0.0,
|
|
729
|
-
},
|
|
730
|
-
})
|
|
731
|
-
|
|
732
|
-
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
733
|
-
this.eyePipeline = this.createRenderPipeline({
|
|
734
|
-
label: "eye overlay pipeline",
|
|
735
|
-
layout: mainPipelineLayout,
|
|
736
|
-
shaderModule,
|
|
737
|
-
vertexBuffers: fullVertexBuffers,
|
|
738
|
-
fragmentTarget: standardBlend,
|
|
739
|
-
cullMode: "front",
|
|
740
|
-
depthStencil: {
|
|
741
|
-
format: "depth24plus-stencil8",
|
|
742
|
-
depthWriteEnabled: true,
|
|
743
|
-
depthCompare: "less-equal",
|
|
744
|
-
depthBias: -0.00005,
|
|
745
|
-
depthBiasSlopeScale: 0.0,
|
|
746
|
-
depthBiasClamp: 0.0,
|
|
747
|
-
stencilFront: {
|
|
748
|
-
compare: "always",
|
|
749
|
-
failOp: "keep",
|
|
750
|
-
depthFailOp: "keep",
|
|
751
|
-
passOp: "replace",
|
|
752
|
-
},
|
|
753
|
-
stencilBack: {
|
|
754
|
-
compare: "always",
|
|
755
|
-
failOp: "keep",
|
|
756
|
-
depthFailOp: "keep",
|
|
757
|
-
passOp: "replace",
|
|
758
|
-
},
|
|
759
|
-
},
|
|
760
|
-
})
|
|
761
|
-
|
|
762
|
-
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
763
|
-
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
764
|
-
label: "depth only shader",
|
|
726
|
+
// GPU picking: encode (modelIndex, materialIndex) as color
|
|
727
|
+
const pickShaderModule = this.device.createShaderModule({
|
|
728
|
+
label: "pick shader",
|
|
765
729
|
code: /* wgsl */ `
|
|
766
730
|
struct CameraUniforms {
|
|
767
731
|
view: mat4x4f,
|
|
@@ -769,94 +733,92 @@ export class Engine {
|
|
|
769
733
|
viewPos: vec3f,
|
|
770
734
|
_padding: f32,
|
|
771
735
|
};
|
|
736
|
+
struct PickId {
|
|
737
|
+
modelId: f32,
|
|
738
|
+
materialId: f32,
|
|
739
|
+
_p1: f32,
|
|
740
|
+
_p2: f32,
|
|
741
|
+
};
|
|
772
742
|
|
|
773
743
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
774
|
-
@group(
|
|
744
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
745
|
+
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
775
746
|
|
|
776
747
|
@vertex fn vs(
|
|
777
748
|
@location(0) position: vec3f,
|
|
778
749
|
@location(1) normal: vec3f,
|
|
750
|
+
@location(2) uv: vec2f,
|
|
779
751
|
@location(3) joints0: vec4<u32>,
|
|
780
752
|
@location(4) weights0: vec4<f32>
|
|
781
753
|
) -> @builtin(position) vec4f {
|
|
782
754
|
let pos4 = vec4f(position, 1.0);
|
|
783
|
-
|
|
784
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
785
755
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
786
756
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
787
|
-
let
|
|
788
|
-
|
|
789
|
-
var
|
|
790
|
-
|
|
791
|
-
let j = joints0[i];
|
|
792
|
-
let w = normalizedWeights[i];
|
|
793
|
-
let m = skinMats[j];
|
|
794
|
-
skinnedPos += (m * pos4) * w;
|
|
795
|
-
}
|
|
796
|
-
let worldPos = skinnedPos.xyz;
|
|
797
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
798
|
-
return clipPos;
|
|
757
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
758
|
+
var sp = vec4f(0.0);
|
|
759
|
+
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
760
|
+
return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
799
761
|
}
|
|
800
762
|
|
|
801
763
|
@fragment fn fs() -> @location(0) vec4f {
|
|
802
|
-
return vec4f(
|
|
764
|
+
return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
|
|
803
765
|
}
|
|
804
766
|
`,
|
|
805
767
|
})
|
|
806
768
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
769
|
+
this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
770
|
+
label: "pick per-frame layout",
|
|
771
|
+
entries: [
|
|
772
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
773
|
+
],
|
|
774
|
+
})
|
|
775
|
+
this.pickPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
776
|
+
label: "pick per-instance layout",
|
|
777
|
+
entries: [
|
|
778
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
779
|
+
],
|
|
780
|
+
})
|
|
781
|
+
this.pickPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
782
|
+
label: "pick per-material layout",
|
|
783
|
+
entries: [
|
|
784
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
785
|
+
],
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
const pickPipelineLayout = this.device.createPipelineLayout({
|
|
789
|
+
label: "pick pipeline layout",
|
|
790
|
+
bindGroupLayouts: [this.pickPerFrameBindGroupLayout, this.pickPerInstanceBindGroupLayout, this.pickPerMaterialBindGroupLayout],
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
this.pickPerFrameBindGroup = this.device.createBindGroup({
|
|
794
|
+
label: "pick per-frame bind group",
|
|
795
|
+
layout: this.pickPerFrameBindGroupLayout,
|
|
796
|
+
entries: [
|
|
797
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
798
|
+
],
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
this.pickPipeline = this.device.createRenderPipeline({
|
|
802
|
+
label: "pick pipeline",
|
|
803
|
+
layout: pickPipelineLayout,
|
|
804
|
+
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
805
|
+
fragment: {
|
|
806
|
+
module: pickShaderModule,
|
|
807
|
+
targets: [{ format: "rgba8unorm" }],
|
|
816
808
|
},
|
|
817
|
-
|
|
818
|
-
cullMode: "none",
|
|
809
|
+
primitive: { cullMode: "none" },
|
|
819
810
|
depthStencil: {
|
|
820
|
-
format: "depth24plus
|
|
811
|
+
format: "depth24plus",
|
|
821
812
|
depthWriteEnabled: true,
|
|
822
813
|
depthCompare: "less-equal",
|
|
823
|
-
depthBias: 0.0,
|
|
824
|
-
depthBiasSlopeScale: 0.0,
|
|
825
|
-
depthBiasClamp: 0.0,
|
|
826
814
|
},
|
|
827
815
|
})
|
|
828
816
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
shaderModule,
|
|
835
|
-
vertexBuffers: fullVertexBuffers,
|
|
836
|
-
fragmentTarget: standardBlend,
|
|
837
|
-
cullMode: "none",
|
|
838
|
-
depthStencil: {
|
|
839
|
-
format: "depth24plus-stencil8",
|
|
840
|
-
depthWriteEnabled: false,
|
|
841
|
-
depthCompare: "less-equal",
|
|
842
|
-
stencilFront: {
|
|
843
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
844
|
-
failOp: "keep",
|
|
845
|
-
depthFailOp: "keep",
|
|
846
|
-
passOp: "keep",
|
|
847
|
-
},
|
|
848
|
-
stencilBack: {
|
|
849
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
850
|
-
failOp: "keep",
|
|
851
|
-
depthFailOp: "keep",
|
|
852
|
-
passOp: "keep",
|
|
853
|
-
},
|
|
854
|
-
},
|
|
855
|
-
})
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
this.hairPipelineOverEyes = createHairPipeline(true)
|
|
859
|
-
this.hairPipelineOverNonEyes = createHairPipeline(false)
|
|
817
|
+
this.pickReadbackBuffer = this.device.createBuffer({
|
|
818
|
+
label: "pick readback",
|
|
819
|
+
size: 256,
|
|
820
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
821
|
+
})
|
|
860
822
|
}
|
|
861
823
|
|
|
862
824
|
|
|
@@ -921,11 +883,26 @@ export class Engine {
|
|
|
921
883
|
depthStoreOp: "store",
|
|
922
884
|
stencilClearValue: 0,
|
|
923
885
|
stencilLoadOp: "clear",
|
|
924
|
-
stencilStoreOp: "discard",
|
|
886
|
+
stencilStoreOp: "discard",
|
|
925
887
|
},
|
|
926
888
|
}
|
|
927
889
|
|
|
928
890
|
this.camera.aspect = width / height
|
|
891
|
+
|
|
892
|
+
if (this.onRaycast) {
|
|
893
|
+
this.pickTexture = this.device.createTexture({
|
|
894
|
+
label: "pick render target",
|
|
895
|
+
size: [width, height],
|
|
896
|
+
format: "rgba8unorm",
|
|
897
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
898
|
+
})
|
|
899
|
+
this.pickDepthTexture = this.device.createTexture({
|
|
900
|
+
label: "pick depth",
|
|
901
|
+
size: [width, height],
|
|
902
|
+
format: "depth24plus",
|
|
903
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
904
|
+
})
|
|
905
|
+
}
|
|
929
906
|
}
|
|
930
907
|
}
|
|
931
908
|
|
|
@@ -943,9 +920,9 @@ export class Engine {
|
|
|
943
920
|
this.camera.attachControl(this.canvas)
|
|
944
921
|
}
|
|
945
922
|
|
|
946
|
-
/** Set camera look-at
|
|
923
|
+
/** Set static camera look-at / orbit center. Clears any model follow binding. */
|
|
947
924
|
public setCameraTarget(v: Vec3): void
|
|
948
|
-
/** Bind camera
|
|
925
|
+
/** Bind camera orbit center to a model's bone (Souls-style follow cam). Pass null to unbind. */
|
|
949
926
|
public setCameraTarget(model: Model | null, boneName: string, offset?: Vec3): void
|
|
950
927
|
public setCameraTarget(modelOrVec: Model | Vec3 | null, boneName?: string, offset?: Vec3): void {
|
|
951
928
|
if (modelOrVec === null) {
|
|
@@ -966,6 +943,26 @@ export class Engine {
|
|
|
966
943
|
this.cameraTargetOffset.z = offset?.z ?? 0
|
|
967
944
|
}
|
|
968
945
|
|
|
946
|
+
/** Souls-style follow cam: orbit center tracks a model bone each frame. Shorthand for setCameraTarget(model, boneName, offset). */
|
|
947
|
+
public setCameraFollow(model: Model | null, boneName?: string, offset?: Vec3): void {
|
|
948
|
+
if (model === null) {
|
|
949
|
+
this.cameraTargetModel = null
|
|
950
|
+
return
|
|
951
|
+
}
|
|
952
|
+
this.cameraTargetModel = model
|
|
953
|
+
this.cameraTargetBoneName = boneName ?? "全ての親"
|
|
954
|
+
this.cameraTargetOffset.x = offset?.x ?? 0
|
|
955
|
+
this.cameraTargetOffset.y = offset?.y ?? 0
|
|
956
|
+
this.cameraTargetOffset.z = offset?.z ?? 0
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
public getCameraDistance(): number { return this.camera.radius }
|
|
960
|
+
public setCameraDistance(d: number): void { this.camera.radius = d }
|
|
961
|
+
public getCameraAlpha(): number { return this.camera.alpha }
|
|
962
|
+
public setCameraAlpha(a: number): void { this.camera.alpha = a }
|
|
963
|
+
public getCameraBeta(): number { return this.camera.beta }
|
|
964
|
+
public setCameraBeta(b: number): void { this.camera.beta = b }
|
|
965
|
+
|
|
969
966
|
// Step 5: Create lighting buffers
|
|
970
967
|
private setupLighting() {
|
|
971
968
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -1126,10 +1123,6 @@ export class Engine {
|
|
|
1126
1123
|
return key
|
|
1127
1124
|
}
|
|
1128
1125
|
|
|
1129
|
-
public async registerModel(model: Model, pmxPath: string): Promise<string> {
|
|
1130
|
-
return this.addModel(model, pmxPath)
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
1126
|
public removeModel(name: string): void {
|
|
1134
1127
|
this.modelInstances.delete(name)
|
|
1135
1128
|
}
|
|
@@ -1200,12 +1193,8 @@ export class Engine {
|
|
|
1200
1193
|
})
|
|
1201
1194
|
}
|
|
1202
1195
|
|
|
1203
|
-
private instances(): IterableIterator<ModelInstance> {
|
|
1204
|
-
return this.modelInstances.values()
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
1196
|
private forEachInstance(fn: (inst: ModelInstance) => void): void {
|
|
1208
|
-
for (const inst of this.
|
|
1197
|
+
for (const inst of this.modelInstances.values()) fn(inst)
|
|
1209
1198
|
}
|
|
1210
1199
|
|
|
1211
1200
|
private updateInstances(deltaTime: number): void {
|
|
@@ -1297,6 +1286,22 @@ export class Engine {
|
|
|
1297
1286
|
],
|
|
1298
1287
|
})
|
|
1299
1288
|
|
|
1289
|
+
const mainPerInstanceBindGroup = this.device.createBindGroup({
|
|
1290
|
+
label: `${name}: main per-instance bind group`,
|
|
1291
|
+
layout: this.mainPerInstanceBindGroupLayout,
|
|
1292
|
+
entries: [
|
|
1293
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1294
|
+
],
|
|
1295
|
+
})
|
|
1296
|
+
|
|
1297
|
+
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
1298
|
+
label: `${name}: pick per-instance bind group`,
|
|
1299
|
+
layout: this.pickPerInstanceBindGroupLayout,
|
|
1300
|
+
entries: [
|
|
1301
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1302
|
+
],
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1300
1305
|
const inst: ModelInstance = {
|
|
1301
1306
|
name,
|
|
1302
1307
|
model,
|
|
@@ -1309,6 +1314,9 @@ export class Engine {
|
|
|
1309
1314
|
drawCalls: [],
|
|
1310
1315
|
shadowDrawCalls: [],
|
|
1311
1316
|
shadowBindGroup,
|
|
1317
|
+
mainPerInstanceBindGroup,
|
|
1318
|
+
pickPerInstanceBindGroup,
|
|
1319
|
+
pickDrawCalls: [],
|
|
1312
1320
|
hiddenMaterials: new Set(),
|
|
1313
1321
|
physics,
|
|
1314
1322
|
vertexBufferNeedsUpdate: false,
|
|
@@ -1457,6 +1465,8 @@ export class Engine {
|
|
|
1457
1465
|
if (materials.length === 0) throw new Error("Model has no materials")
|
|
1458
1466
|
const textures = model.getTextures()
|
|
1459
1467
|
const prefix = `${inst.name}: `
|
|
1468
|
+
// 1-based so that (0,0) = clear color = "no hit"
|
|
1469
|
+
const modelId = this.modelInstances.size + 1
|
|
1460
1470
|
|
|
1461
1471
|
const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
|
|
1462
1472
|
if (texIndex < 0 || texIndex >= textures.length) return null
|
|
@@ -1465,9 +1475,11 @@ export class Engine {
|
|
|
1465
1475
|
}
|
|
1466
1476
|
|
|
1467
1477
|
let currentIndexOffset = 0
|
|
1478
|
+
let materialId = 0
|
|
1468
1479
|
for (const mat of materials) {
|
|
1469
1480
|
const indexCount = mat.vertexCount
|
|
1470
1481
|
if (indexCount === 0) continue
|
|
1482
|
+
materialId++
|
|
1471
1483
|
|
|
1472
1484
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex)
|
|
1473
1485
|
if (!diffuseTexture) throw new Error(`Material "${mat.name}" has no diffuse texture`)
|
|
@@ -1478,73 +1490,24 @@ export class Engine {
|
|
|
1478
1490
|
const materialUniformBuffer = this.createMaterialUniformBuffer(
|
|
1479
1491
|
prefix + mat.name,
|
|
1480
1492
|
materialAlpha,
|
|
1481
|
-
0.0,
|
|
1482
1493
|
[mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]],
|
|
1483
1494
|
mat.ambient,
|
|
1484
1495
|
mat.specular,
|
|
1485
1496
|
mat.shininess
|
|
1486
1497
|
)
|
|
1487
1498
|
|
|
1499
|
+
const textureView = diffuseTexture.createView()
|
|
1488
1500
|
const bindGroup = this.device.createBindGroup({
|
|
1489
1501
|
label: `${prefix}material: ${mat.name}`,
|
|
1490
|
-
layout: this.
|
|
1502
|
+
layout: this.mainPerMaterialBindGroupLayout,
|
|
1491
1503
|
entries: [
|
|
1492
|
-
{ binding: 0, resource:
|
|
1493
|
-
{ binding: 1, resource: { buffer:
|
|
1494
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1495
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1496
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1497
|
-
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1504
|
+
{ binding: 0, resource: textureView },
|
|
1505
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
1498
1506
|
],
|
|
1499
1507
|
})
|
|
1500
1508
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
inst.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1504
|
-
} else if (mat.isHair) {
|
|
1505
|
-
const createHairBindGroup = (isOverEyes: boolean) => {
|
|
1506
|
-
const buf = this.createMaterialUniformBuffer(
|
|
1507
|
-
`${prefix}${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
1508
|
-
materialAlpha,
|
|
1509
|
-
isOverEyes ? 1.0 : 0.0,
|
|
1510
|
-
[mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]],
|
|
1511
|
-
mat.ambient,
|
|
1512
|
-
mat.specular,
|
|
1513
|
-
mat.shininess
|
|
1514
|
-
)
|
|
1515
|
-
return this.device.createBindGroup({
|
|
1516
|
-
label: `${prefix}hair ${isOverEyes ? "over eyes" : "over non-eyes"}: ${mat.name}`,
|
|
1517
|
-
layout: this.mainBindGroupLayout,
|
|
1518
|
-
entries: [
|
|
1519
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1520
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1521
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1522
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1523
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1524
|
-
{ binding: 5, resource: { buffer: buf } },
|
|
1525
|
-
],
|
|
1526
|
-
})
|
|
1527
|
-
}
|
|
1528
|
-
inst.drawCalls.push({
|
|
1529
|
-
type: "hair-over-eyes",
|
|
1530
|
-
count: indexCount,
|
|
1531
|
-
firstIndex: currentIndexOffset,
|
|
1532
|
-
bindGroup: createHairBindGroup(true),
|
|
1533
|
-
materialName: mat.name,
|
|
1534
|
-
})
|
|
1535
|
-
inst.drawCalls.push({
|
|
1536
|
-
type: "hair-over-non-eyes",
|
|
1537
|
-
count: indexCount,
|
|
1538
|
-
firstIndex: currentIndexOffset,
|
|
1539
|
-
bindGroup: createHairBindGroup(false),
|
|
1540
|
-
materialName: mat.name,
|
|
1541
|
-
})
|
|
1542
|
-
} else if (isTransparent) {
|
|
1543
|
-
inst.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1544
|
-
} else {
|
|
1545
|
-
inst.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1509
|
+
const type: DrawCallType = isTransparent ? "transparent" : "opaque"
|
|
1510
|
+
inst.drawCalls.push({ type, count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1548
1511
|
|
|
1549
1512
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1550
1513
|
const materialUniformData = new Float32Array([
|
|
@@ -1554,32 +1517,37 @@ export class Engine {
|
|
|
1554
1517
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData)
|
|
1555
1518
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1556
1519
|
label: `${prefix}outline: ${mat.name}`,
|
|
1557
|
-
layout: this.
|
|
1520
|
+
layout: this.outlinePerMaterialBindGroupLayout,
|
|
1558
1521
|
entries: [
|
|
1559
|
-
{ binding: 0, resource: { buffer:
|
|
1560
|
-
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1561
|
-
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1522
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
1562
1523
|
],
|
|
1563
1524
|
})
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1525
|
+
const outlineType: DrawCallType = isTransparent ? "transparent-outline" : "opaque-outline"
|
|
1526
|
+
inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name })
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (this.onRaycast) {
|
|
1530
|
+
const pickIdData = new Float32Array([modelId, materialId, 0, 0])
|
|
1531
|
+
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData)
|
|
1532
|
+
const pickBindGroup = this.device.createBindGroup({
|
|
1533
|
+
label: `${prefix}pick: ${mat.name}`,
|
|
1534
|
+
layout: this.pickPerMaterialBindGroupLayout,
|
|
1535
|
+
entries: [{ binding: 0, resource: { buffer: pickIdBuffer } }],
|
|
1536
|
+
})
|
|
1537
|
+
inst.pickDrawCalls.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: pickBindGroup })
|
|
1568
1538
|
}
|
|
1569
1539
|
|
|
1570
1540
|
currentIndexOffset += indexCount
|
|
1571
1541
|
}
|
|
1572
1542
|
|
|
1573
1543
|
for (const d of inst.drawCalls) {
|
|
1574
|
-
if (d.type === "opaque"
|
|
1575
|
-
inst.shadowDrawCalls.push(d)
|
|
1544
|
+
if (d.type === "opaque") inst.shadowDrawCalls.push(d)
|
|
1576
1545
|
}
|
|
1577
1546
|
}
|
|
1578
1547
|
|
|
1579
1548
|
private createMaterialUniformBuffer(
|
|
1580
1549
|
label: string,
|
|
1581
1550
|
alpha: number,
|
|
1582
|
-
isOverEyes: number,
|
|
1583
1551
|
diffuseColor: [number, number, number],
|
|
1584
1552
|
ambientColor: [number, number, number],
|
|
1585
1553
|
specularColor: [number, number, number],
|
|
@@ -1588,25 +1556,13 @@ export class Engine {
|
|
|
1588
1556
|
const data = new Float32Array(20)
|
|
1589
1557
|
data.set([
|
|
1590
1558
|
alpha,
|
|
1591
|
-
1.0,
|
|
1592
1559
|
this.rimLightIntensity,
|
|
1593
|
-
shininess,
|
|
1594
|
-
|
|
1595
|
-
1.0,
|
|
1596
|
-
1.0,
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
diffuseColor[1],
|
|
1600
|
-
diffuseColor[2],
|
|
1601
|
-
0.0, // diffuseColor (vec3), _padding2
|
|
1602
|
-
ambientColor[0],
|
|
1603
|
-
ambientColor[1],
|
|
1604
|
-
ambientColor[2],
|
|
1605
|
-
0.0, // ambientColor (vec3), _padding3
|
|
1606
|
-
specularColor[0],
|
|
1607
|
-
specularColor[1],
|
|
1608
|
-
specularColor[2],
|
|
1609
|
-
0.0, // specularColor (vec3), _padding4
|
|
1560
|
+
shininess,
|
|
1561
|
+
0.0,
|
|
1562
|
+
1.0, 1.0, 1.0, 0.0, // rimColor (vec3), _padding2
|
|
1563
|
+
diffuseColor[0], diffuseColor[1], diffuseColor[2], 0.0,
|
|
1564
|
+
ambientColor[0], ambientColor[1], ambientColor[2], 0.0,
|
|
1565
|
+
specularColor[0], specularColor[1], specularColor[2], 0.0,
|
|
1610
1566
|
])
|
|
1611
1567
|
return this.createUniformBuffer(`material uniform: ${label}`, data)
|
|
1612
1568
|
}
|
|
@@ -1659,17 +1615,6 @@ export class Engine {
|
|
|
1659
1615
|
}
|
|
1660
1616
|
}
|
|
1661
1617
|
|
|
1662
|
-
// Post-alpha eye: render eye draws; main pass writes stencil so hair-over-eyes can use it for see-through bangs.
|
|
1663
|
-
private renderEyes(pass: GPURenderPassEncoder, inst: ModelInstance) {
|
|
1664
|
-
pass.setPipeline(this.eyePipeline)
|
|
1665
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1666
|
-
for (const draw of inst.drawCalls) {
|
|
1667
|
-
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1668
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1669
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
1618
|
|
|
1674
1619
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
1675
1620
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
@@ -1680,51 +1625,6 @@ export class Engine {
|
|
|
1680
1625
|
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0)
|
|
1681
1626
|
}
|
|
1682
1627
|
|
|
1683
|
-
// Post-alpha eye: hair-over-eyes uses stencil (from renderEyes) for 50% alpha; hair-over-non-eyes uses inverse stencil.
|
|
1684
|
-
private renderHair(pass: GPURenderPassEncoder, inst: ModelInstance) {
|
|
1685
|
-
|
|
1686
|
-
const hasHair = inst.drawCalls.some(
|
|
1687
|
-
(d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d)
|
|
1688
|
-
)
|
|
1689
|
-
if (hasHair) {
|
|
1690
|
-
pass.setPipeline(this.hairDepthPipeline)
|
|
1691
|
-
for (const draw of inst.drawCalls) {
|
|
1692
|
-
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, draw)) {
|
|
1693
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1694
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
const hairOverEyes = inst.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(inst, d))
|
|
1700
|
-
if (hairOverEyes.length > 0) {
|
|
1701
|
-
pass.setPipeline(this.hairPipelineOverEyes)
|
|
1702
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1703
|
-
for (const draw of hairOverEyes) {
|
|
1704
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1705
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
const hairOverNonEyes = inst.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(inst, d))
|
|
1710
|
-
if (hairOverNonEyes.length > 0) {
|
|
1711
|
-
pass.setPipeline(this.hairPipelineOverNonEyes)
|
|
1712
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1713
|
-
for (const draw of hairOverNonEyes) {
|
|
1714
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1715
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
const hairOutlines = inst.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(inst, d))
|
|
1720
|
-
if (hairOutlines.length > 0) {
|
|
1721
|
-
pass.setPipeline(this.hairOutlinePipeline)
|
|
1722
|
-
for (const draw of hairOutlines) {
|
|
1723
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1724
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
1628
|
|
|
1729
1629
|
private handleCanvasDoubleClick = (event: MouseEvent) => {
|
|
1730
1630
|
if (!this.onRaycast || this.modelInstances.size === 0) return
|
|
@@ -1765,111 +1665,92 @@ export class Engine {
|
|
|
1765
1665
|
this.onRaycast?.("", null, screenX, screenY)
|
|
1766
1666
|
return
|
|
1767
1667
|
}
|
|
1668
|
+
const dpr = window.devicePixelRatio || 1
|
|
1669
|
+
this.pendingPick = { x: Math.floor(screenX * dpr), y: Math.floor(screenY * dpr) }
|
|
1670
|
+
}
|
|
1768
1671
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
const rect = this.canvas.getBoundingClientRect()
|
|
1772
|
-
const clipX = (screenX / rect.width) * 2 - 1
|
|
1773
|
-
const clipY = 1 - (screenY / rect.height) * 2
|
|
1774
|
-
const viewProjMatrix = projectionMatrix.multiply(viewMatrix)
|
|
1775
|
-
const inverseViewProj = viewProjMatrix.inverse()
|
|
1776
|
-
const transformPoint = (matrix: Mat4, point: Vec3): Vec3 => {
|
|
1777
|
-
const m = matrix.values
|
|
1778
|
-
const x = point.x, y = point.y, z = point.z
|
|
1779
|
-
const result = new Vec3(
|
|
1780
|
-
m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
1781
|
-
m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
1782
|
-
m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
1783
|
-
)
|
|
1784
|
-
const w = m[3] * x + m[7] * y + m[11] * z + m[15]
|
|
1785
|
-
return result.scale(w !== 0 ? 1 / w : 1)
|
|
1786
|
-
}
|
|
1787
|
-
const worldNear = transformPoint(inverseViewProj, new Vec3(clipX, clipY, -1))
|
|
1788
|
-
const worldFar = transformPoint(inverseViewProj, new Vec3(clipX, clipY, 1))
|
|
1789
|
-
const rayOrigin = this.camera.getPosition()
|
|
1790
|
-
const rayDirection = worldFar.subtract(worldNear).normalize()
|
|
1791
|
-
|
|
1792
|
-
const transformByMatrix = (matrix: Float32Array, offset: number, point: Vec3): Vec3 => {
|
|
1793
|
-
const m = matrix, x = point.x, y = point.y, z = point.z
|
|
1794
|
-
return new Vec3(
|
|
1795
|
-
m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12],
|
|
1796
|
-
m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13],
|
|
1797
|
-
m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]
|
|
1798
|
-
)
|
|
1799
|
-
}
|
|
1672
|
+
private renderPickPass(encoder: GPUCommandEncoder): void {
|
|
1673
|
+
if (!this.pendingPick || !this.pickTexture || !this.pickDepthTexture) return
|
|
1800
1674
|
|
|
1801
|
-
|
|
1802
|
-
|
|
1675
|
+
const pass = encoder.beginRenderPass({
|
|
1676
|
+
colorAttachments: [{
|
|
1677
|
+
view: this.pickTexture.createView(),
|
|
1678
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1679
|
+
loadOp: "clear",
|
|
1680
|
+
storeOp: "store",
|
|
1681
|
+
}],
|
|
1682
|
+
depthStencilAttachment: {
|
|
1683
|
+
view: this.pickDepthTexture.createView(),
|
|
1684
|
+
depthClearValue: 1.0,
|
|
1685
|
+
depthLoadOp: "clear",
|
|
1686
|
+
depthStoreOp: "store",
|
|
1687
|
+
},
|
|
1688
|
+
})
|
|
1689
|
+
|
|
1690
|
+
pass.setPipeline(this.pickPipeline)
|
|
1691
|
+
pass.setBindGroup(0, this.pickPerFrameBindGroup)
|
|
1803
1692
|
|
|
1804
1693
|
this.forEachInstance((inst) => {
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
const vertices = new Float32Array(baseVertices.length)
|
|
1814
|
-
const skinMatrices = model.getSkinMatrices()
|
|
1815
|
-
for (let i = 0; i < baseVertices.length; i += 8) {
|
|
1816
|
-
const vertexIndex = i / 8
|
|
1817
|
-
const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2])
|
|
1818
|
-
const j0 = skinning.joints[vertexIndex * 4], j1 = skinning.joints[vertexIndex * 4 + 1], j2 = skinning.joints[vertexIndex * 4 + 2], j3 = skinning.joints[vertexIndex * 4 + 3]
|
|
1819
|
-
const w0 = skinning.weights[vertexIndex * 4] / 255, w1 = skinning.weights[vertexIndex * 4 + 1] / 255, w2 = skinning.weights[vertexIndex * 4 + 2] / 255, w3 = skinning.weights[vertexIndex * 4 + 3] / 255
|
|
1820
|
-
const ws = w0 + w1 + w2 + w3
|
|
1821
|
-
const nw = ws > 0.0001 ? [w0 / ws, w1 / ws, w2 / ws, w3 / ws] : [1, 0, 0, 0]
|
|
1822
|
-
let sp = new Vec3(0, 0, 0)
|
|
1823
|
-
for (let j = 0; j < 4; j++) {
|
|
1824
|
-
if (nw[j] <= 0) continue
|
|
1825
|
-
const transformed = transformByMatrix(skinMatrices, [j0, j1, j2, j3][j] * 16, position)
|
|
1826
|
-
sp = sp.add(transformed.scale(nw[j]))
|
|
1827
|
-
}
|
|
1828
|
-
vertices[i] = sp.x
|
|
1829
|
-
vertices[i + 1] = sp.y
|
|
1830
|
-
vertices[i + 2] = sp.z
|
|
1831
|
-
vertices[i + 3] = baseVertices[i + 3]
|
|
1832
|
-
vertices[i + 4] = baseVertices[i + 4]
|
|
1833
|
-
vertices[i + 5] = baseVertices[i + 5]
|
|
1834
|
-
vertices[i + 6] = baseVertices[i + 6]
|
|
1835
|
-
vertices[i + 7] = baseVertices[i + 7]
|
|
1694
|
+
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
1695
|
+
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
1696
|
+
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
1697
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
1698
|
+
pass.setBindGroup(1, inst.pickPerInstanceBindGroup)
|
|
1699
|
+
for (const draw of inst.pickDrawCalls) {
|
|
1700
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
1701
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1836
1702
|
}
|
|
1703
|
+
})
|
|
1837
1704
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1705
|
+
pass.end()
|
|
1706
|
+
|
|
1707
|
+
// Copy the single pixel under cursor to readback buffer
|
|
1708
|
+
const px = Math.min(this.pendingPick.x, this.pickTexture.width - 1)
|
|
1709
|
+
const py = Math.min(this.pendingPick.y, this.pickTexture.height - 1)
|
|
1710
|
+
encoder.copyTextureToBuffer(
|
|
1711
|
+
{ texture: this.pickTexture, origin: { x: Math.max(0, px), y: Math.max(0, py) } },
|
|
1712
|
+
{ buffer: this.pickReadbackBuffer, bytesPerRow: 256 },
|
|
1713
|
+
{ width: 1, height: 1 }
|
|
1714
|
+
)
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
private async resolvePickResult(screenX: number, screenY: number): Promise<void> {
|
|
1718
|
+
if (!this.onRaycast) return
|
|
1719
|
+
await this.pickReadbackBuffer.mapAsync(GPUMapMode.READ)
|
|
1720
|
+
const data = new Uint8Array(this.pickReadbackBuffer.getMappedRange())
|
|
1721
|
+
const modelId = data[0]
|
|
1722
|
+
const materialId = data[1]
|
|
1723
|
+
this.pickReadbackBuffer.unmap()
|
|
1724
|
+
|
|
1725
|
+
if (modelId === 0) {
|
|
1726
|
+
this.onRaycast("", null, screenX, screenY)
|
|
1727
|
+
return
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// Find model by 1-based index
|
|
1731
|
+
let idx = 1
|
|
1732
|
+
let hitModel = ""
|
|
1733
|
+
for (const [name] of this.modelInstances) {
|
|
1734
|
+
if (idx === modelId) { hitModel = name; break }
|
|
1735
|
+
idx++
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Find material by 1-based index (skipping zero-vertex materials)
|
|
1739
|
+
let hitMaterial: string | null = null
|
|
1740
|
+
if (hitModel) {
|
|
1741
|
+
const inst = this.modelInstances.get(hitModel)
|
|
1742
|
+
if (inst) {
|
|
1743
|
+
const materials = inst.model.getMaterials()
|
|
1744
|
+
let matIdx = 0
|
|
1745
|
+
for (const mat of materials) {
|
|
1746
|
+
if (mat.vertexCount === 0) continue
|
|
1747
|
+
matIdx++
|
|
1748
|
+
if (matIdx === materialId) { hitMaterial = mat.name; break }
|
|
1865
1749
|
}
|
|
1866
1750
|
}
|
|
1867
|
-
})
|
|
1868
|
-
|
|
1869
|
-
if (this.onRaycast) {
|
|
1870
|
-
const hit = closest as { modelName: string; materialName: string; distance: number } | null
|
|
1871
|
-
this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY)
|
|
1872
1751
|
}
|
|
1752
|
+
|
|
1753
|
+
this.onRaycast(hitModel, hitMaterial, screenX, screenY)
|
|
1873
1754
|
}
|
|
1874
1755
|
|
|
1875
1756
|
public render() {
|
|
@@ -1919,12 +1800,27 @@ export class Engine {
|
|
|
1919
1800
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
1920
1801
|
if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst))
|
|
1921
1802
|
if (this.hasGround) this.renderGround(pass)
|
|
1922
|
-
|
|
1923
1803
|
pass.end()
|
|
1804
|
+
|
|
1805
|
+
const pick = this.pendingPick
|
|
1806
|
+
if (pick && hasModels) this.renderPickPass(encoder)
|
|
1807
|
+
|
|
1924
1808
|
this.device.queue.submit([encoder.finish()])
|
|
1809
|
+
|
|
1810
|
+
if (pick) {
|
|
1811
|
+
this.pendingPick = null
|
|
1812
|
+
const dpr = window.devicePixelRatio || 1
|
|
1813
|
+
this.resolvePickResult(pick.x / dpr, pick.y / dpr)
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1925
1816
|
this.updateStats(performance.now() - currentTime)
|
|
1926
1817
|
}
|
|
1927
1818
|
|
|
1819
|
+
private updateRenderTarget() {
|
|
1820
|
+
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
1821
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1928
1824
|
private drawInstanceShadow(sp: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1929
1825
|
sp.setBindGroup(0, inst.shadowBindGroup)
|
|
1930
1826
|
sp.setVertexBuffer(0, inst.vertexBuffer)
|
|
@@ -1940,7 +1836,7 @@ export class Engine {
|
|
|
1940
1836
|
pass.setPipeline(pipeline)
|
|
1941
1837
|
for (const draw of inst.drawCalls) {
|
|
1942
1838
|
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1943
|
-
pass.setBindGroup(
|
|
1839
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
1944
1840
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1945
1841
|
}
|
|
1946
1842
|
}
|
|
@@ -1950,22 +1846,27 @@ export class Engine {
|
|
|
1950
1846
|
pass.setPipeline(pipeline)
|
|
1951
1847
|
for (const draw of inst.drawCalls) {
|
|
1952
1848
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1953
|
-
pass.setBindGroup(
|
|
1849
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
1954
1850
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1955
1851
|
}
|
|
1956
1852
|
}
|
|
1957
1853
|
}
|
|
1958
1854
|
|
|
1855
|
+
private bindMainGroups(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1856
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
1857
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1959
1860
|
private renderOneModel(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1960
1861
|
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
1961
1862
|
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
1962
1863
|
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
1963
1864
|
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
1964
1865
|
|
|
1866
|
+
this.bindMainGroups(pass, inst)
|
|
1965
1867
|
this.drawOpaque(pass, inst, this.modelPipeline)
|
|
1966
|
-
this.renderEyes(pass, inst)
|
|
1967
1868
|
this.drawOutlines(pass, inst, false)
|
|
1968
|
-
this.
|
|
1869
|
+
this.bindMainGroups(pass, inst)
|
|
1969
1870
|
this.drawTransparent(pass, inst, this.modelPipeline)
|
|
1970
1871
|
this.drawOutlines(pass, inst, true)
|
|
1971
1872
|
}
|
|
@@ -1983,10 +1884,6 @@ export class Engine {
|
|
|
1983
1884
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
1984
1885
|
}
|
|
1985
1886
|
|
|
1986
|
-
private updateRenderTarget() {
|
|
1987
|
-
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
1988
|
-
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
1989
|
-
}
|
|
1990
1887
|
|
|
1991
1888
|
private updateSkinMatrices() {
|
|
1992
1889
|
this.forEachInstance((inst) => {
|
|
@@ -2003,10 +1900,12 @@ export class Engine {
|
|
|
2003
1900
|
|
|
2004
1901
|
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean) {
|
|
2005
1902
|
pass.setPipeline(this.outlinePipeline)
|
|
1903
|
+
pass.setBindGroup(0, this.outlinePerFrameBindGroup)
|
|
1904
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
2006
1905
|
const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
|
|
2007
1906
|
for (const draw of inst.drawCalls) {
|
|
2008
1907
|
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
2009
|
-
pass.setBindGroup(
|
|
1908
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
2010
1909
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2011
1910
|
}
|
|
2012
1911
|
}
|