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/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);
|
|
@@ -677,22 +696,17 @@ export class Engine {
|
|
|
677
696
|
}
|
|
678
697
|
let worldPos = skinnedPos.xyz;
|
|
679
698
|
let worldNormal = normalize(skinnedNrm);
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
let
|
|
683
|
-
let
|
|
699
|
+
// Screen-stable edgeline: extrusion ∝ camera distance (same idea as MMD viewers / babylon-mmd-style scaling)
|
|
700
|
+
let camDist = max(length(camera.viewPos - worldPos), 0.25);
|
|
701
|
+
let refDist = 30.0;
|
|
702
|
+
let edgeScale = 0.03;
|
|
703
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * edgeScale * (camDist / refDist);
|
|
684
704
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
685
705
|
return output;
|
|
686
706
|
}
|
|
687
707
|
|
|
688
708
|
@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;
|
|
709
|
+
return material.edgeColor;
|
|
696
710
|
}
|
|
697
711
|
`,
|
|
698
712
|
})
|
|
@@ -706,62 +720,15 @@ export class Engine {
|
|
|
706
720
|
cullMode: "back",
|
|
707
721
|
depthStencil: {
|
|
708
722
|
format: "depth24plus-stencil8",
|
|
709
|
-
|
|
710
|
-
depthCompare: "less-equal",
|
|
711
|
-
},
|
|
712
|
-
})
|
|
713
|
-
|
|
714
|
-
// Hair outline pipeline
|
|
715
|
-
this.hairOutlinePipeline = this.createRenderPipeline({
|
|
716
|
-
label: "hair outline pipeline",
|
|
717
|
-
layout: outlinePipelineLayout,
|
|
718
|
-
shaderModule: outlineShaderModule,
|
|
719
|
-
vertexBuffers: outlineVertexBuffers,
|
|
720
|
-
fragmentTarget: standardBlend,
|
|
721
|
-
cullMode: "back",
|
|
722
|
-
depthStencil: {
|
|
723
|
-
format: "depth24plus-stencil8",
|
|
723
|
+
// Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
|
|
724
724
|
depthWriteEnabled: false,
|
|
725
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
726
|
},
|
|
760
727
|
})
|
|
761
728
|
|
|
762
|
-
//
|
|
763
|
-
const
|
|
764
|
-
label: "
|
|
729
|
+
// GPU picking: encode (modelIndex, materialIndex) as color
|
|
730
|
+
const pickShaderModule = this.device.createShaderModule({
|
|
731
|
+
label: "pick shader",
|
|
765
732
|
code: /* wgsl */ `
|
|
766
733
|
struct CameraUniforms {
|
|
767
734
|
view: mat4x4f,
|
|
@@ -769,94 +736,92 @@ export class Engine {
|
|
|
769
736
|
viewPos: vec3f,
|
|
770
737
|
_padding: f32,
|
|
771
738
|
};
|
|
739
|
+
struct PickId {
|
|
740
|
+
modelId: f32,
|
|
741
|
+
materialId: f32,
|
|
742
|
+
_p1: f32,
|
|
743
|
+
_p2: f32,
|
|
744
|
+
};
|
|
772
745
|
|
|
773
746
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
774
|
-
@group(
|
|
747
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
748
|
+
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
775
749
|
|
|
776
750
|
@vertex fn vs(
|
|
777
751
|
@location(0) position: vec3f,
|
|
778
752
|
@location(1) normal: vec3f,
|
|
753
|
+
@location(2) uv: vec2f,
|
|
779
754
|
@location(3) joints0: vec4<u32>,
|
|
780
755
|
@location(4) weights0: vec4<f32>
|
|
781
756
|
) -> @builtin(position) vec4f {
|
|
782
757
|
let pos4 = vec4f(position, 1.0);
|
|
783
|
-
|
|
784
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
785
758
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
786
759
|
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;
|
|
760
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
761
|
+
var sp = vec4f(0.0);
|
|
762
|
+
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
763
|
+
return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
799
764
|
}
|
|
800
765
|
|
|
801
766
|
@fragment fn fs() -> @location(0) vec4f {
|
|
802
|
-
return vec4f(
|
|
767
|
+
return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
|
|
803
768
|
}
|
|
804
769
|
`,
|
|
805
770
|
})
|
|
806
771
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
772
|
+
this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
773
|
+
label: "pick per-frame layout",
|
|
774
|
+
entries: [
|
|
775
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
776
|
+
],
|
|
777
|
+
})
|
|
778
|
+
this.pickPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
779
|
+
label: "pick per-instance layout",
|
|
780
|
+
entries: [
|
|
781
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
782
|
+
],
|
|
783
|
+
})
|
|
784
|
+
this.pickPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
785
|
+
label: "pick per-material layout",
|
|
786
|
+
entries: [
|
|
787
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
788
|
+
],
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const pickPipelineLayout = this.device.createPipelineLayout({
|
|
792
|
+
label: "pick pipeline layout",
|
|
793
|
+
bindGroupLayouts: [this.pickPerFrameBindGroupLayout, this.pickPerInstanceBindGroupLayout, this.pickPerMaterialBindGroupLayout],
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
this.pickPerFrameBindGroup = this.device.createBindGroup({
|
|
797
|
+
label: "pick per-frame bind group",
|
|
798
|
+
layout: this.pickPerFrameBindGroupLayout,
|
|
799
|
+
entries: [
|
|
800
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
801
|
+
],
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
this.pickPipeline = this.device.createRenderPipeline({
|
|
805
|
+
label: "pick pipeline",
|
|
806
|
+
layout: pickPipelineLayout,
|
|
807
|
+
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
808
|
+
fragment: {
|
|
809
|
+
module: pickShaderModule,
|
|
810
|
+
targets: [{ format: "rgba8unorm" }],
|
|
816
811
|
},
|
|
817
|
-
|
|
818
|
-
cullMode: "none",
|
|
812
|
+
primitive: { cullMode: "none" },
|
|
819
813
|
depthStencil: {
|
|
820
|
-
format: "depth24plus
|
|
814
|
+
format: "depth24plus",
|
|
821
815
|
depthWriteEnabled: true,
|
|
822
816
|
depthCompare: "less-equal",
|
|
823
|
-
depthBias: 0.0,
|
|
824
|
-
depthBiasSlopeScale: 0.0,
|
|
825
|
-
depthBiasClamp: 0.0,
|
|
826
817
|
},
|
|
827
818
|
})
|
|
828
819
|
|
|
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)
|
|
820
|
+
this.pickReadbackBuffer = this.device.createBuffer({
|
|
821
|
+
label: "pick readback",
|
|
822
|
+
size: 256,
|
|
823
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
824
|
+
})
|
|
860
825
|
}
|
|
861
826
|
|
|
862
827
|
|
|
@@ -921,11 +886,26 @@ export class Engine {
|
|
|
921
886
|
depthStoreOp: "store",
|
|
922
887
|
stencilClearValue: 0,
|
|
923
888
|
stencilLoadOp: "clear",
|
|
924
|
-
stencilStoreOp: "discard",
|
|
889
|
+
stencilStoreOp: "discard",
|
|
925
890
|
},
|
|
926
891
|
}
|
|
927
892
|
|
|
928
893
|
this.camera.aspect = width / height
|
|
894
|
+
|
|
895
|
+
if (this.onRaycast) {
|
|
896
|
+
this.pickTexture = this.device.createTexture({
|
|
897
|
+
label: "pick render target",
|
|
898
|
+
size: [width, height],
|
|
899
|
+
format: "rgba8unorm",
|
|
900
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
901
|
+
})
|
|
902
|
+
this.pickDepthTexture = this.device.createTexture({
|
|
903
|
+
label: "pick depth",
|
|
904
|
+
size: [width, height],
|
|
905
|
+
format: "depth24plus",
|
|
906
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
907
|
+
})
|
|
908
|
+
}
|
|
929
909
|
}
|
|
930
910
|
}
|
|
931
911
|
|
|
@@ -943,9 +923,9 @@ export class Engine {
|
|
|
943
923
|
this.camera.attachControl(this.canvas)
|
|
944
924
|
}
|
|
945
925
|
|
|
946
|
-
/** Set camera look-at
|
|
926
|
+
/** Set static camera look-at / orbit center. Clears any model follow binding. */
|
|
947
927
|
public setCameraTarget(v: Vec3): void
|
|
948
|
-
/** Bind camera
|
|
928
|
+
/** Bind camera orbit center to a model's bone (Souls-style follow cam). Pass null to unbind. */
|
|
949
929
|
public setCameraTarget(model: Model | null, boneName: string, offset?: Vec3): void
|
|
950
930
|
public setCameraTarget(modelOrVec: Model | Vec3 | null, boneName?: string, offset?: Vec3): void {
|
|
951
931
|
if (modelOrVec === null) {
|
|
@@ -966,6 +946,26 @@ export class Engine {
|
|
|
966
946
|
this.cameraTargetOffset.z = offset?.z ?? 0
|
|
967
947
|
}
|
|
968
948
|
|
|
949
|
+
/** Souls-style follow cam: orbit center tracks a model bone each frame. Shorthand for setCameraTarget(model, boneName, offset). */
|
|
950
|
+
public setCameraFollow(model: Model | null, boneName?: string, offset?: Vec3): void {
|
|
951
|
+
if (model === null) {
|
|
952
|
+
this.cameraTargetModel = null
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
this.cameraTargetModel = model
|
|
956
|
+
this.cameraTargetBoneName = boneName ?? "全ての親"
|
|
957
|
+
this.cameraTargetOffset.x = offset?.x ?? 0
|
|
958
|
+
this.cameraTargetOffset.y = offset?.y ?? 0
|
|
959
|
+
this.cameraTargetOffset.z = offset?.z ?? 0
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
public getCameraDistance(): number { return this.camera.radius }
|
|
963
|
+
public setCameraDistance(d: number): void { this.camera.radius = d }
|
|
964
|
+
public getCameraAlpha(): number { return this.camera.alpha }
|
|
965
|
+
public setCameraAlpha(a: number): void { this.camera.alpha = a }
|
|
966
|
+
public getCameraBeta(): number { return this.camera.beta }
|
|
967
|
+
public setCameraBeta(b: number): void { this.camera.beta = b }
|
|
968
|
+
|
|
969
969
|
// Step 5: Create lighting buffers
|
|
970
970
|
private setupLighting() {
|
|
971
971
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -1126,10 +1126,6 @@ export class Engine {
|
|
|
1126
1126
|
return key
|
|
1127
1127
|
}
|
|
1128
1128
|
|
|
1129
|
-
public async registerModel(model: Model, pmxPath: string): Promise<string> {
|
|
1130
|
-
return this.addModel(model, pmxPath)
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
1129
|
public removeModel(name: string): void {
|
|
1134
1130
|
this.modelInstances.delete(name)
|
|
1135
1131
|
}
|
|
@@ -1200,12 +1196,8 @@ export class Engine {
|
|
|
1200
1196
|
})
|
|
1201
1197
|
}
|
|
1202
1198
|
|
|
1203
|
-
private instances(): IterableIterator<ModelInstance> {
|
|
1204
|
-
return this.modelInstances.values()
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
1199
|
private forEachInstance(fn: (inst: ModelInstance) => void): void {
|
|
1208
|
-
for (const inst of this.
|
|
1200
|
+
for (const inst of this.modelInstances.values()) fn(inst)
|
|
1209
1201
|
}
|
|
1210
1202
|
|
|
1211
1203
|
private updateInstances(deltaTime: number): void {
|
|
@@ -1297,6 +1289,22 @@ export class Engine {
|
|
|
1297
1289
|
],
|
|
1298
1290
|
})
|
|
1299
1291
|
|
|
1292
|
+
const mainPerInstanceBindGroup = this.device.createBindGroup({
|
|
1293
|
+
label: `${name}: main per-instance bind group`,
|
|
1294
|
+
layout: this.mainPerInstanceBindGroupLayout,
|
|
1295
|
+
entries: [
|
|
1296
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1297
|
+
],
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
1301
|
+
label: `${name}: pick per-instance bind group`,
|
|
1302
|
+
layout: this.pickPerInstanceBindGroupLayout,
|
|
1303
|
+
entries: [
|
|
1304
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1305
|
+
],
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1300
1308
|
const inst: ModelInstance = {
|
|
1301
1309
|
name,
|
|
1302
1310
|
model,
|
|
@@ -1309,6 +1317,9 @@ export class Engine {
|
|
|
1309
1317
|
drawCalls: [],
|
|
1310
1318
|
shadowDrawCalls: [],
|
|
1311
1319
|
shadowBindGroup,
|
|
1320
|
+
mainPerInstanceBindGroup,
|
|
1321
|
+
pickPerInstanceBindGroup,
|
|
1322
|
+
pickDrawCalls: [],
|
|
1312
1323
|
hiddenMaterials: new Set(),
|
|
1313
1324
|
physics,
|
|
1314
1325
|
vertexBufferNeedsUpdate: false,
|
|
@@ -1457,6 +1468,8 @@ export class Engine {
|
|
|
1457
1468
|
if (materials.length === 0) throw new Error("Model has no materials")
|
|
1458
1469
|
const textures = model.getTextures()
|
|
1459
1470
|
const prefix = `${inst.name}: `
|
|
1471
|
+
// 1-based so that (0,0) = clear color = "no hit"
|
|
1472
|
+
const modelId = this.modelInstances.size + 1
|
|
1460
1473
|
|
|
1461
1474
|
const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
|
|
1462
1475
|
if (texIndex < 0 || texIndex >= textures.length) return null
|
|
@@ -1465,9 +1478,11 @@ export class Engine {
|
|
|
1465
1478
|
}
|
|
1466
1479
|
|
|
1467
1480
|
let currentIndexOffset = 0
|
|
1481
|
+
let materialId = 0
|
|
1468
1482
|
for (const mat of materials) {
|
|
1469
1483
|
const indexCount = mat.vertexCount
|
|
1470
1484
|
if (indexCount === 0) continue
|
|
1485
|
+
materialId++
|
|
1471
1486
|
|
|
1472
1487
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex)
|
|
1473
1488
|
if (!diffuseTexture) throw new Error(`Material "${mat.name}" has no diffuse texture`)
|
|
@@ -1478,73 +1493,24 @@ export class Engine {
|
|
|
1478
1493
|
const materialUniformBuffer = this.createMaterialUniformBuffer(
|
|
1479
1494
|
prefix + mat.name,
|
|
1480
1495
|
materialAlpha,
|
|
1481
|
-
0.0,
|
|
1482
1496
|
[mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]],
|
|
1483
1497
|
mat.ambient,
|
|
1484
1498
|
mat.specular,
|
|
1485
1499
|
mat.shininess
|
|
1486
1500
|
)
|
|
1487
1501
|
|
|
1502
|
+
const textureView = diffuseTexture.createView()
|
|
1488
1503
|
const bindGroup = this.device.createBindGroup({
|
|
1489
1504
|
label: `${prefix}material: ${mat.name}`,
|
|
1490
|
-
layout: this.
|
|
1505
|
+
layout: this.mainPerMaterialBindGroupLayout,
|
|
1491
1506
|
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 } },
|
|
1507
|
+
{ binding: 0, resource: textureView },
|
|
1508
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
1498
1509
|
],
|
|
1499
1510
|
})
|
|
1500
1511
|
|
|
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
|
-
}
|
|
1512
|
+
const type: DrawCallType = isTransparent ? "transparent" : "opaque"
|
|
1513
|
+
inst.drawCalls.push({ type, count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1548
1514
|
|
|
1549
1515
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1550
1516
|
const materialUniformData = new Float32Array([
|
|
@@ -1554,32 +1520,37 @@ export class Engine {
|
|
|
1554
1520
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData)
|
|
1555
1521
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1556
1522
|
label: `${prefix}outline: ${mat.name}`,
|
|
1557
|
-
layout: this.
|
|
1523
|
+
layout: this.outlinePerMaterialBindGroupLayout,
|
|
1558
1524
|
entries: [
|
|
1559
|
-
{ binding: 0, resource: { buffer:
|
|
1560
|
-
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1561
|
-
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1525
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
1562
1526
|
],
|
|
1563
1527
|
})
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1528
|
+
const outlineType: DrawCallType = isTransparent ? "transparent-outline" : "opaque-outline"
|
|
1529
|
+
inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name })
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
if (this.onRaycast) {
|
|
1533
|
+
const pickIdData = new Float32Array([modelId, materialId, 0, 0])
|
|
1534
|
+
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData)
|
|
1535
|
+
const pickBindGroup = this.device.createBindGroup({
|
|
1536
|
+
label: `${prefix}pick: ${mat.name}`,
|
|
1537
|
+
layout: this.pickPerMaterialBindGroupLayout,
|
|
1538
|
+
entries: [{ binding: 0, resource: { buffer: pickIdBuffer } }],
|
|
1539
|
+
})
|
|
1540
|
+
inst.pickDrawCalls.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: pickBindGroup })
|
|
1568
1541
|
}
|
|
1569
1542
|
|
|
1570
1543
|
currentIndexOffset += indexCount
|
|
1571
1544
|
}
|
|
1572
1545
|
|
|
1573
1546
|
for (const d of inst.drawCalls) {
|
|
1574
|
-
if (d.type === "opaque"
|
|
1575
|
-
inst.shadowDrawCalls.push(d)
|
|
1547
|
+
if (d.type === "opaque") inst.shadowDrawCalls.push(d)
|
|
1576
1548
|
}
|
|
1577
1549
|
}
|
|
1578
1550
|
|
|
1579
1551
|
private createMaterialUniformBuffer(
|
|
1580
1552
|
label: string,
|
|
1581
1553
|
alpha: number,
|
|
1582
|
-
isOverEyes: number,
|
|
1583
1554
|
diffuseColor: [number, number, number],
|
|
1584
1555
|
ambientColor: [number, number, number],
|
|
1585
1556
|
specularColor: [number, number, number],
|
|
@@ -1588,25 +1559,13 @@ export class Engine {
|
|
|
1588
1559
|
const data = new Float32Array(20)
|
|
1589
1560
|
data.set([
|
|
1590
1561
|
alpha,
|
|
1591
|
-
1.0,
|
|
1592
1562
|
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
|
|
1563
|
+
shininess,
|
|
1564
|
+
0.0,
|
|
1565
|
+
1.0, 1.0, 1.0, 0.0, // rimColor (vec3), _padding2
|
|
1566
|
+
diffuseColor[0], diffuseColor[1], diffuseColor[2], 0.0,
|
|
1567
|
+
ambientColor[0], ambientColor[1], ambientColor[2], 0.0,
|
|
1568
|
+
specularColor[0], specularColor[1], specularColor[2], 0.0,
|
|
1610
1569
|
])
|
|
1611
1570
|
return this.createUniformBuffer(`material uniform: ${label}`, data)
|
|
1612
1571
|
}
|
|
@@ -1659,17 +1618,6 @@ export class Engine {
|
|
|
1659
1618
|
}
|
|
1660
1619
|
}
|
|
1661
1620
|
|
|
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
1621
|
|
|
1674
1622
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
1675
1623
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
@@ -1680,51 +1628,6 @@ export class Engine {
|
|
|
1680
1628
|
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0)
|
|
1681
1629
|
}
|
|
1682
1630
|
|
|
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
1631
|
|
|
1729
1632
|
private handleCanvasDoubleClick = (event: MouseEvent) => {
|
|
1730
1633
|
if (!this.onRaycast || this.modelInstances.size === 0) return
|
|
@@ -1765,111 +1668,92 @@ export class Engine {
|
|
|
1765
1668
|
this.onRaycast?.("", null, screenX, screenY)
|
|
1766
1669
|
return
|
|
1767
1670
|
}
|
|
1671
|
+
const dpr = window.devicePixelRatio || 1
|
|
1672
|
+
this.pendingPick = { x: Math.floor(screenX * dpr), y: Math.floor(screenY * dpr) }
|
|
1673
|
+
}
|
|
1768
1674
|
|
|
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
|
-
}
|
|
1675
|
+
private renderPickPass(encoder: GPUCommandEncoder): void {
|
|
1676
|
+
if (!this.pendingPick || !this.pickTexture || !this.pickDepthTexture) return
|
|
1800
1677
|
|
|
1801
|
-
|
|
1802
|
-
|
|
1678
|
+
const pass = encoder.beginRenderPass({
|
|
1679
|
+
colorAttachments: [{
|
|
1680
|
+
view: this.pickTexture.createView(),
|
|
1681
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1682
|
+
loadOp: "clear",
|
|
1683
|
+
storeOp: "store",
|
|
1684
|
+
}],
|
|
1685
|
+
depthStencilAttachment: {
|
|
1686
|
+
view: this.pickDepthTexture.createView(),
|
|
1687
|
+
depthClearValue: 1.0,
|
|
1688
|
+
depthLoadOp: "clear",
|
|
1689
|
+
depthStoreOp: "store",
|
|
1690
|
+
},
|
|
1691
|
+
})
|
|
1692
|
+
|
|
1693
|
+
pass.setPipeline(this.pickPipeline)
|
|
1694
|
+
pass.setBindGroup(0, this.pickPerFrameBindGroup)
|
|
1803
1695
|
|
|
1804
1696
|
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]
|
|
1697
|
+
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
1698
|
+
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
1699
|
+
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
1700
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
1701
|
+
pass.setBindGroup(1, inst.pickPerInstanceBindGroup)
|
|
1702
|
+
for (const draw of inst.pickDrawCalls) {
|
|
1703
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
1704
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1836
1705
|
}
|
|
1706
|
+
})
|
|
1837
1707
|
|
|
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
|
-
|
|
1708
|
+
pass.end()
|
|
1709
|
+
|
|
1710
|
+
// Copy the single pixel under cursor to readback buffer
|
|
1711
|
+
const px = Math.min(this.pendingPick.x, this.pickTexture.width - 1)
|
|
1712
|
+
const py = Math.min(this.pendingPick.y, this.pickTexture.height - 1)
|
|
1713
|
+
encoder.copyTextureToBuffer(
|
|
1714
|
+
{ texture: this.pickTexture, origin: { x: Math.max(0, px), y: Math.max(0, py) } },
|
|
1715
|
+
{ buffer: this.pickReadbackBuffer, bytesPerRow: 256 },
|
|
1716
|
+
{ width: 1, height: 1 }
|
|
1717
|
+
)
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
private async resolvePickResult(screenX: number, screenY: number): Promise<void> {
|
|
1721
|
+
if (!this.onRaycast) return
|
|
1722
|
+
await this.pickReadbackBuffer.mapAsync(GPUMapMode.READ)
|
|
1723
|
+
const data = new Uint8Array(this.pickReadbackBuffer.getMappedRange())
|
|
1724
|
+
const modelId = data[0]
|
|
1725
|
+
const materialId = data[1]
|
|
1726
|
+
this.pickReadbackBuffer.unmap()
|
|
1727
|
+
|
|
1728
|
+
if (modelId === 0) {
|
|
1729
|
+
this.onRaycast("", null, screenX, screenY)
|
|
1730
|
+
return
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Find model by 1-based index
|
|
1734
|
+
let idx = 1
|
|
1735
|
+
let hitModel = ""
|
|
1736
|
+
for (const [name] of this.modelInstances) {
|
|
1737
|
+
if (idx === modelId) { hitModel = name; break }
|
|
1738
|
+
idx++
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Find material by 1-based index (skipping zero-vertex materials)
|
|
1742
|
+
let hitMaterial: string | null = null
|
|
1743
|
+
if (hitModel) {
|
|
1744
|
+
const inst = this.modelInstances.get(hitModel)
|
|
1745
|
+
if (inst) {
|
|
1746
|
+
const materials = inst.model.getMaterials()
|
|
1747
|
+
let matIdx = 0
|
|
1748
|
+
for (const mat of materials) {
|
|
1749
|
+
if (mat.vertexCount === 0) continue
|
|
1750
|
+
matIdx++
|
|
1751
|
+
if (matIdx === materialId) { hitMaterial = mat.name; break }
|
|
1865
1752
|
}
|
|
1866
1753
|
}
|
|
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
1754
|
}
|
|
1755
|
+
|
|
1756
|
+
this.onRaycast(hitModel, hitMaterial, screenX, screenY)
|
|
1873
1757
|
}
|
|
1874
1758
|
|
|
1875
1759
|
public render() {
|
|
@@ -1919,12 +1803,27 @@ export class Engine {
|
|
|
1919
1803
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
1920
1804
|
if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst))
|
|
1921
1805
|
if (this.hasGround) this.renderGround(pass)
|
|
1922
|
-
|
|
1923
1806
|
pass.end()
|
|
1807
|
+
|
|
1808
|
+
const pick = this.pendingPick
|
|
1809
|
+
if (pick && hasModels) this.renderPickPass(encoder)
|
|
1810
|
+
|
|
1924
1811
|
this.device.queue.submit([encoder.finish()])
|
|
1812
|
+
|
|
1813
|
+
if (pick) {
|
|
1814
|
+
this.pendingPick = null
|
|
1815
|
+
const dpr = window.devicePixelRatio || 1
|
|
1816
|
+
this.resolvePickResult(pick.x / dpr, pick.y / dpr)
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1925
1819
|
this.updateStats(performance.now() - currentTime)
|
|
1926
1820
|
}
|
|
1927
1821
|
|
|
1822
|
+
private updateRenderTarget() {
|
|
1823
|
+
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
1824
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1928
1827
|
private drawInstanceShadow(sp: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1929
1828
|
sp.setBindGroup(0, inst.shadowBindGroup)
|
|
1930
1829
|
sp.setVertexBuffer(0, inst.vertexBuffer)
|
|
@@ -1940,7 +1839,7 @@ export class Engine {
|
|
|
1940
1839
|
pass.setPipeline(pipeline)
|
|
1941
1840
|
for (const draw of inst.drawCalls) {
|
|
1942
1841
|
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1943
|
-
pass.setBindGroup(
|
|
1842
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
1944
1843
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1945
1844
|
}
|
|
1946
1845
|
}
|
|
@@ -1950,22 +1849,27 @@ export class Engine {
|
|
|
1950
1849
|
pass.setPipeline(pipeline)
|
|
1951
1850
|
for (const draw of inst.drawCalls) {
|
|
1952
1851
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1953
|
-
pass.setBindGroup(
|
|
1852
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
1954
1853
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1955
1854
|
}
|
|
1956
1855
|
}
|
|
1957
1856
|
}
|
|
1958
1857
|
|
|
1858
|
+
private bindMainGroups(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1859
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
1860
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1959
1863
|
private renderOneModel(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1960
1864
|
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
1961
1865
|
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
1962
1866
|
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
1963
1867
|
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
1964
1868
|
|
|
1869
|
+
this.bindMainGroups(pass, inst)
|
|
1965
1870
|
this.drawOpaque(pass, inst, this.modelPipeline)
|
|
1966
|
-
this.renderEyes(pass, inst)
|
|
1967
1871
|
this.drawOutlines(pass, inst, false)
|
|
1968
|
-
this.
|
|
1872
|
+
this.bindMainGroups(pass, inst)
|
|
1969
1873
|
this.drawTransparent(pass, inst, this.modelPipeline)
|
|
1970
1874
|
this.drawOutlines(pass, inst, true)
|
|
1971
1875
|
}
|
|
@@ -1983,10 +1887,6 @@ export class Engine {
|
|
|
1983
1887
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
1984
1888
|
}
|
|
1985
1889
|
|
|
1986
|
-
private updateRenderTarget() {
|
|
1987
|
-
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
1988
|
-
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
1989
|
-
}
|
|
1990
1890
|
|
|
1991
1891
|
private updateSkinMatrices() {
|
|
1992
1892
|
this.forEachInstance((inst) => {
|
|
@@ -2003,10 +1903,12 @@ export class Engine {
|
|
|
2003
1903
|
|
|
2004
1904
|
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean) {
|
|
2005
1905
|
pass.setPipeline(this.outlinePipeline)
|
|
1906
|
+
pass.setBindGroup(0, this.outlinePerFrameBindGroup)
|
|
1907
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
2006
1908
|
const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
|
|
2007
1909
|
for (const draw of inst.drawCalls) {
|
|
2008
1910
|
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
2009
|
-
pass.setBindGroup(
|
|
1911
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
2010
1912
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2011
1913
|
}
|
|
2012
1914
|
}
|