reze-engine 0.8.3 → 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/README.md +3 -3
- package/dist/engine.d.ts +50 -38
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +400 -722
- package/dist/model.d.ts +1 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +3 -28
- 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 +470 -790
- package/src/model.ts +3 -38
- package/src/pmx-loader.ts +0 -21
package/src/engine.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Camera } from "./camera"
|
|
2
|
-
import { Mat4,
|
|
2
|
+
import { Mat4, Vec3 } from "./math"
|
|
3
3
|
import { Model } from "./model"
|
|
4
|
+
import { PmxLoader } from "./pmx-loader"
|
|
4
5
|
import { Physics } from "./physics"
|
|
5
6
|
|
|
6
7
|
export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
|
|
@@ -14,7 +15,6 @@ export type EngineOptions = {
|
|
|
14
15
|
cameraTarget?: Vec3
|
|
15
16
|
cameraFov?: number
|
|
16
17
|
onRaycast?: RaycastCallback
|
|
17
|
-
multisampleCount?: 1 | 4
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">
|
|
@@ -28,7 +28,6 @@ export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
|
|
|
28
28
|
cameraTarget: new Vec3(0, 12.5, 0),
|
|
29
29
|
cameraFov: Math.PI / 4,
|
|
30
30
|
onRaycast: undefined,
|
|
31
|
-
multisampleCount: 4,
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export interface EngineStats {
|
|
@@ -38,14 +37,9 @@ export interface EngineStats {
|
|
|
38
37
|
|
|
39
38
|
type DrawCallType =
|
|
40
39
|
| "opaque"
|
|
41
|
-
| "eye"
|
|
42
|
-
| "hair-over-eyes"
|
|
43
|
-
| "hair-over-non-eyes"
|
|
44
40
|
| "transparent"
|
|
45
41
|
| "ground"
|
|
46
42
|
| "opaque-outline"
|
|
47
|
-
| "eye-outline"
|
|
48
|
-
| "hair-outline"
|
|
49
43
|
| "transparent-outline"
|
|
50
44
|
|
|
51
45
|
interface DrawCall {
|
|
@@ -56,6 +50,12 @@ interface DrawCall {
|
|
|
56
50
|
materialName: string
|
|
57
51
|
}
|
|
58
52
|
|
|
53
|
+
interface PickDrawCall {
|
|
54
|
+
count: number
|
|
55
|
+
firstIndex: number
|
|
56
|
+
bindGroup: GPUBindGroup
|
|
57
|
+
}
|
|
58
|
+
|
|
59
59
|
interface ModelInstance {
|
|
60
60
|
name: string
|
|
61
61
|
model: Model
|
|
@@ -68,6 +68,9 @@ interface ModelInstance {
|
|
|
68
68
|
drawCalls: DrawCall[]
|
|
69
69
|
shadowDrawCalls: DrawCall[]
|
|
70
70
|
shadowBindGroup: GPUBindGroup
|
|
71
|
+
mainPerInstanceBindGroup: GPUBindGroup
|
|
72
|
+
pickPerInstanceBindGroup: GPUBindGroup
|
|
73
|
+
pickDrawCalls: PickDrawCall[]
|
|
71
74
|
hiddenMaterials: Set<string>
|
|
72
75
|
physics: Physics | null
|
|
73
76
|
vertexBufferNeedsUpdate: boolean
|
|
@@ -78,7 +81,7 @@ export class Engine {
|
|
|
78
81
|
|
|
79
82
|
public static getInstance(): Engine {
|
|
80
83
|
if (!Engine.instance) {
|
|
81
|
-
throw new Error("Engine not ready: create Engine, await init(), then load models via
|
|
84
|
+
throw new Error("Engine not ready: create Engine, await init(), then load models via engine.loadModel().")
|
|
82
85
|
}
|
|
83
86
|
return Engine.instance
|
|
84
87
|
}
|
|
@@ -98,26 +101,20 @@ export class Engine {
|
|
|
98
101
|
private lightCount = 0
|
|
99
102
|
private resizeObserver: ResizeObserver | null = null
|
|
100
103
|
private depthTexture!: GPUTexture
|
|
101
|
-
// Material rendering pipelines
|
|
102
104
|
private modelPipeline!: GPURenderPipeline
|
|
103
|
-
private
|
|
104
|
-
private
|
|
105
|
-
private hairPipelineOverNonEyes!: GPURenderPipeline
|
|
106
|
-
private hairDepthPipeline!: GPURenderPipeline
|
|
107
|
-
// Ground/reflection pipeline
|
|
108
|
-
private groundPipeline!: GPURenderPipeline
|
|
109
|
-
private groundBindGroupLayout!: GPUBindGroupLayout
|
|
110
|
-
private reflectionPipeline!: GPURenderPipeline
|
|
111
|
-
// Outline pipelines
|
|
105
|
+
private groundShadowPipeline!: GPURenderPipeline
|
|
106
|
+
private groundShadowBindGroupLayout!: GPUBindGroupLayout
|
|
112
107
|
private outlinePipeline!: GPURenderPipeline
|
|
113
|
-
private
|
|
114
|
-
private
|
|
115
|
-
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
|
|
116
115
|
private multisampleTexture!: GPUTexture
|
|
117
|
-
private
|
|
116
|
+
private static readonly MULTISAMPLE_COUNT = 4
|
|
118
117
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
119
|
-
// Constants
|
|
120
|
-
private readonly STENCIL_EYE_VALUE = 1
|
|
121
118
|
|
|
122
119
|
// Ambient light settings
|
|
123
120
|
private ambientColor!: Vec3
|
|
@@ -126,23 +123,15 @@ export class Engine {
|
|
|
126
123
|
// Rim light settings
|
|
127
124
|
private rimLightIntensity!: number
|
|
128
125
|
|
|
129
|
-
// Ground
|
|
126
|
+
// Ground properties (shadow only)
|
|
130
127
|
private groundVertexBuffer?: GPUBuffer
|
|
131
128
|
private groundIndexBuffer?: GPUBuffer
|
|
132
|
-
private
|
|
133
|
-
private groundReflectionResolveTexture?: GPUTexture // Resolve target for multisampled texture
|
|
134
|
-
private groundReflectionDepthTexture?: GPUTexture
|
|
135
|
-
private groundReflectionBindGroup?: GPUBindGroup
|
|
136
|
-
private groundMaterialUniformBuffer?: GPUBuffer
|
|
137
|
-
private groundHasReflections = false
|
|
138
|
-
private groundMode: "reflection" | "shadow" = "reflection"
|
|
129
|
+
private hasGround = false
|
|
139
130
|
private shadowMapTexture?: GPUTexture
|
|
140
131
|
private shadowMapDepthView?: GPUTextureView
|
|
141
132
|
private shadowDepthPipeline!: GPURenderPipeline
|
|
142
133
|
private shadowLightVPBuffer!: GPUBuffer
|
|
143
134
|
private shadowLightVPMatrix = new Float32Array(16)
|
|
144
|
-
private groundShadowPipeline!: GPURenderPipeline
|
|
145
|
-
private groundShadowBindGroupLayout!: GPUBindGroupLayout
|
|
146
135
|
private groundShadowBindGroup?: GPUBindGroup
|
|
147
136
|
private shadowComparisonSampler!: GPUSampler
|
|
148
137
|
private groundShadowMaterialBuffer?: GPUBuffer
|
|
@@ -152,14 +141,32 @@ export class Engine {
|
|
|
152
141
|
private shadowVPLightZ = Number.NaN
|
|
153
142
|
|
|
154
143
|
private onRaycast?: RaycastCallback
|
|
155
|
-
// Double-tap detection
|
|
156
144
|
private lastTouchTime = 0
|
|
157
|
-
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
|
|
158
156
|
|
|
159
157
|
private modelInstances = new Map<string, ModelInstance>()
|
|
160
158
|
private materialSampler!: GPUSampler
|
|
161
159
|
private textureCache = new Map<string, GPUTexture>()
|
|
162
|
-
private
|
|
160
|
+
private _nextDefaultModelId = 0
|
|
161
|
+
|
|
162
|
+
// IK and physics enabled at engine level (same for all models)
|
|
163
|
+
private ikEnabled = true
|
|
164
|
+
private physicsEnabled = true
|
|
165
|
+
|
|
166
|
+
// Camera target binding (Babylon/Three style: camera follows model)
|
|
167
|
+
private cameraTargetModel: Model | null = null
|
|
168
|
+
private cameraTargetBoneName = "全ての親"
|
|
169
|
+
private cameraTargetOffset: Vec3 = new Vec3(0, 0, 0)
|
|
163
170
|
|
|
164
171
|
private lastFpsUpdate = performance.now()
|
|
165
172
|
private framesSinceLastUpdate = 0
|
|
@@ -175,7 +182,6 @@ export class Engine {
|
|
|
175
182
|
|
|
176
183
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
177
184
|
this.canvas = canvas
|
|
178
|
-
this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount
|
|
179
185
|
if (options) {
|
|
180
186
|
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor!
|
|
181
187
|
this.directionalLightIntensity =
|
|
@@ -246,7 +252,7 @@ export class Engine {
|
|
|
246
252
|
: undefined,
|
|
247
253
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
248
254
|
depthStencil: config.depthStencil,
|
|
249
|
-
multisample: config.multisample ?? { count:
|
|
255
|
+
multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
|
|
250
256
|
})
|
|
251
257
|
}
|
|
252
258
|
|
|
@@ -296,24 +302,6 @@ export class Engine {
|
|
|
296
302
|
},
|
|
297
303
|
]
|
|
298
304
|
|
|
299
|
-
const depthOnlyVertexBuffers: GPUVertexBufferLayout[] = [
|
|
300
|
-
{
|
|
301
|
-
arrayStride: 8 * 4,
|
|
302
|
-
attributes: [
|
|
303
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
304
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
305
|
-
],
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
arrayStride: 4 * 2,
|
|
309
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
arrayStride: 4,
|
|
313
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
314
|
-
},
|
|
315
|
-
]
|
|
316
|
-
|
|
317
305
|
const standardBlend: GPUColorTargetState = {
|
|
318
306
|
format: this.presentationFormat,
|
|
319
307
|
blend: {
|
|
@@ -352,17 +340,17 @@ export class Engine {
|
|
|
352
340
|
|
|
353
341
|
struct MaterialUniforms {
|
|
354
342
|
alpha: f32,
|
|
355
|
-
alphaMultiplier: f32,
|
|
356
343
|
rimIntensity: f32,
|
|
357
344
|
shininess: f32,
|
|
345
|
+
_padding1: f32,
|
|
358
346
|
rimColor: vec3f,
|
|
359
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
360
|
-
diffuseColor: vec3f,
|
|
361
347
|
_padding2: f32,
|
|
362
|
-
|
|
348
|
+
diffuseColor: vec3f,
|
|
363
349
|
_padding3: f32,
|
|
364
|
-
|
|
350
|
+
ambientColor: vec3f,
|
|
365
351
|
_padding4: f32,
|
|
352
|
+
specularColor: vec3f,
|
|
353
|
+
_padding5: f32,
|
|
366
354
|
};
|
|
367
355
|
|
|
368
356
|
struct VertexOutput {
|
|
@@ -372,12 +360,15 @@ export class Engine {
|
|
|
372
360
|
@location(2) worldPos: vec3f,
|
|
373
361
|
};
|
|
374
362
|
|
|
363
|
+
// group 0: per-frame (bound once per pass)
|
|
375
364
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
376
365
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
377
|
-
@group(0) @binding(2) var
|
|
378
|
-
|
|
379
|
-
@group(
|
|
380
|
-
|
|
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;
|
|
381
372
|
|
|
382
373
|
@vertex fn vs(
|
|
383
374
|
@location(0) position: vec3f,
|
|
@@ -389,7 +380,6 @@ export class Engine {
|
|
|
389
380
|
var output: VertexOutput;
|
|
390
381
|
let pos4 = vec4f(position, 1.0);
|
|
391
382
|
|
|
392
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
393
383
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
394
384
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
395
385
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -413,11 +403,7 @@ export class Engine {
|
|
|
413
403
|
}
|
|
414
404
|
|
|
415
405
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
416
|
-
|
|
417
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
418
|
-
if (material.isOverEyes > 0.5) {
|
|
419
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
420
|
-
}
|
|
406
|
+
let finalAlpha = material.alpha;
|
|
421
407
|
if (finalAlpha < 0.001) {
|
|
422
408
|
discard;
|
|
423
409
|
}
|
|
@@ -425,18 +411,14 @@ export class Engine {
|
|
|
425
411
|
let n = normalize(input.normal);
|
|
426
412
|
let textureColor = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
427
413
|
|
|
428
|
-
// View direction for specular and rim
|
|
429
414
|
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
430
415
|
|
|
431
|
-
// Simple lighting: global ambient + diffuse lighting
|
|
432
416
|
let albedo = textureColor * material.diffuseColor;
|
|
433
417
|
|
|
434
|
-
// Precompute material values
|
|
435
418
|
let minSpec = light.ambientColor.w;
|
|
436
419
|
let effectiveSpecular = max(material.specularColor, vec3f(minSpec));
|
|
437
420
|
let specPower = max(material.shininess, 1.0);
|
|
438
421
|
|
|
439
|
-
// Single directional light
|
|
440
422
|
let l = -light.lights[0].direction.xyz;
|
|
441
423
|
let nDotL = max(dot(n, l), 0.0);
|
|
442
424
|
let intensity = light.lights[0].color.w;
|
|
@@ -444,7 +426,6 @@ export class Engine {
|
|
|
444
426
|
|
|
445
427
|
let lightAccum = light.ambientColor.xyz + radiance * nDotL;
|
|
446
428
|
|
|
447
|
-
// Blinn-Phong specular
|
|
448
429
|
let h = normalize(l + viewDir);
|
|
449
430
|
let nDotH = max(dot(n, h), 0.0);
|
|
450
431
|
let specFactor = pow(nDotH, specPower);
|
|
@@ -452,9 +433,8 @@ export class Engine {
|
|
|
452
433
|
|
|
453
434
|
let litColor = albedo * lightAccum;
|
|
454
435
|
|
|
455
|
-
// Rim light calculation - proper Fresnel for edge-only highlights
|
|
456
436
|
let fresnel = 1.0 - abs(dot(n, viewDir));
|
|
457
|
-
let rimFactor = pow(fresnel, 4.0);
|
|
437
|
+
let rimFactor = pow(fresnel, 4.0);
|
|
458
438
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
459
439
|
|
|
460
440
|
let color = litColor + specularAccum + rimLight;
|
|
@@ -464,22 +444,44 @@ export class Engine {
|
|
|
464
444
|
`,
|
|
465
445
|
})
|
|
466
446
|
|
|
467
|
-
//
|
|
468
|
-
this.
|
|
469
|
-
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",
|
|
450
|
+
entries: [
|
|
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",
|
|
470
459
|
entries: [
|
|
471
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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" } },
|
|
477
469
|
],
|
|
478
470
|
})
|
|
479
471
|
|
|
480
472
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
481
473
|
label: "main pipeline layout",
|
|
482
|
-
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
|
+
],
|
|
483
485
|
})
|
|
484
486
|
|
|
485
487
|
this.modelPipeline = this.createRenderPipeline({
|
|
@@ -496,71 +498,6 @@ export class Engine {
|
|
|
496
498
|
},
|
|
497
499
|
})
|
|
498
500
|
|
|
499
|
-
this.groundBindGroupLayout = this.device.createBindGroupLayout({
|
|
500
|
-
label: "ground bind group layout",
|
|
501
|
-
entries: [
|
|
502
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
503
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
504
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
505
|
-
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
506
|
-
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
507
|
-
],
|
|
508
|
-
})
|
|
509
|
-
const groundPipelineLayout = this.device.createPipelineLayout({
|
|
510
|
-
label: "ground pipeline layout",
|
|
511
|
-
bindGroupLayouts: [this.groundBindGroupLayout],
|
|
512
|
-
})
|
|
513
|
-
const groundShaderModule = this.device.createShaderModule({
|
|
514
|
-
label: "ground shaders",
|
|
515
|
-
code: /* wgsl */ `
|
|
516
|
-
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
517
|
-
struct Light { direction: vec4f, color: vec4f, };
|
|
518
|
-
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
519
|
-
struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
|
|
520
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
521
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
522
|
-
@group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
|
|
523
|
-
@group(0) @binding(3) var reflectionSampler: sampler;
|
|
524
|
-
@group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
|
|
525
|
-
struct VertexOutput {
|
|
526
|
-
@builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
|
|
527
|
-
};
|
|
528
|
-
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
|
|
529
|
-
var o: VertexOutput;
|
|
530
|
-
o.worldPos = position;
|
|
531
|
-
o.position = camera.projection * camera.view * vec4f(position, 1.0);
|
|
532
|
-
o.normal = normal; o.uv = uv; return o;
|
|
533
|
-
}
|
|
534
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
535
|
-
let n = normalize(input.normal);
|
|
536
|
-
let centerDist = length(input.worldPos.xz);
|
|
537
|
-
let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
|
|
538
|
-
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
539
|
-
let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
|
|
540
|
-
let ndcPos = clipPos.xyz / clipPos.w;
|
|
541
|
-
let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
|
|
542
|
-
let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
|
|
543
|
-
let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
|
|
544
|
-
let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
|
|
545
|
-
let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
|
|
546
|
-
let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
|
|
547
|
-
var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
|
|
548
|
-
let l = -light.lights[0].direction.xyz;
|
|
549
|
-
let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
|
|
550
|
-
return vec4f(finalColor * lightAccum, edgeFade);
|
|
551
|
-
}
|
|
552
|
-
`,
|
|
553
|
-
})
|
|
554
|
-
this.groundPipeline = this.createRenderPipeline({
|
|
555
|
-
label: "ground pipeline",
|
|
556
|
-
layout: groundPipelineLayout,
|
|
557
|
-
shaderModule: groundShaderModule,
|
|
558
|
-
vertexBuffers: fullVertexBuffers,
|
|
559
|
-
fragmentTarget: standardBlend,
|
|
560
|
-
cullMode: "back",
|
|
561
|
-
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
562
|
-
})
|
|
563
|
-
|
|
564
501
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
565
502
|
size: 64,
|
|
566
503
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -677,38 +614,32 @@ export class Engine {
|
|
|
677
614
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
678
615
|
})
|
|
679
616
|
|
|
680
|
-
//
|
|
681
|
-
this.
|
|
682
|
-
label: "
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
fragmentTarget: {
|
|
687
|
-
format: this.presentationFormat,
|
|
688
|
-
blend: standardBlend.blend,
|
|
689
|
-
},
|
|
690
|
-
multisample: { count: this.sampleCount }, // Use same multisampling as main render
|
|
691
|
-
cullMode: "none",
|
|
692
|
-
depthStencil: {
|
|
693
|
-
format: "depth24plus-stencil8",
|
|
694
|
-
depthWriteEnabled: true,
|
|
695
|
-
depthCompare: "less-equal",
|
|
696
|
-
},
|
|
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",
|
|
620
|
+
entries: [
|
|
621
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
622
|
+
],
|
|
697
623
|
})
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
label: "outline bind group layout",
|
|
624
|
+
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
625
|
+
this.outlinePerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
626
|
+
label: "outline per-material bind group layout",
|
|
702
627
|
entries: [
|
|
703
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
704
|
-
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
705
|
-
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
628
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
706
629
|
],
|
|
707
630
|
})
|
|
708
631
|
|
|
709
632
|
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
710
633
|
label: "outline pipeline layout",
|
|
711
|
-
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
|
+
],
|
|
712
643
|
})
|
|
713
644
|
|
|
714
645
|
const outlineShaderModule = this.device.createShaderModule({
|
|
@@ -724,14 +655,17 @@ export class Engine {
|
|
|
724
655
|
struct MaterialUniforms {
|
|
725
656
|
edgeColor: vec4f,
|
|
726
657
|
edgeSize: f32,
|
|
727
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
728
658
|
_padding1: f32,
|
|
729
659
|
_padding2: f32,
|
|
660
|
+
_padding3: f32,
|
|
730
661
|
};
|
|
731
662
|
|
|
663
|
+
// group 0: per-frame
|
|
732
664
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
733
|
-
|
|
734
|
-
@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;
|
|
735
669
|
|
|
736
670
|
struct VertexOutput {
|
|
737
671
|
@builtin(position) position: vec4f,
|
|
@@ -746,7 +680,6 @@ export class Engine {
|
|
|
746
680
|
var output: VertexOutput;
|
|
747
681
|
let pos4 = vec4f(position, 1.0);
|
|
748
682
|
|
|
749
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
750
683
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
751
684
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
752
685
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -764,7 +697,6 @@ export class Engine {
|
|
|
764
697
|
let worldPos = skinnedPos.xyz;
|
|
765
698
|
let worldNormal = normalize(skinnedNrm);
|
|
766
699
|
|
|
767
|
-
// MMD invert hull: expand vertices outward along normals
|
|
768
700
|
let scaleFactor = 0.01;
|
|
769
701
|
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
770
702
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
@@ -772,13 +704,7 @@ export class Engine {
|
|
|
772
704
|
}
|
|
773
705
|
|
|
774
706
|
@fragment fn fs() -> @location(0) vec4f {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (material.isOverEyes > 0.5) {
|
|
778
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
return color;
|
|
707
|
+
return material.edgeColor;
|
|
782
708
|
}
|
|
783
709
|
`,
|
|
784
710
|
})
|
|
@@ -797,57 +723,9 @@ export class Engine {
|
|
|
797
723
|
},
|
|
798
724
|
})
|
|
799
725
|
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
label: "
|
|
803
|
-
layout: outlinePipelineLayout,
|
|
804
|
-
shaderModule: outlineShaderModule,
|
|
805
|
-
vertexBuffers: outlineVertexBuffers,
|
|
806
|
-
fragmentTarget: standardBlend,
|
|
807
|
-
cullMode: "back",
|
|
808
|
-
depthStencil: {
|
|
809
|
-
format: "depth24plus-stencil8",
|
|
810
|
-
depthWriteEnabled: false,
|
|
811
|
-
depthCompare: "less-equal",
|
|
812
|
-
depthBias: -0.0001,
|
|
813
|
-
depthBiasSlopeScale: 0.0,
|
|
814
|
-
depthBiasClamp: 0.0,
|
|
815
|
-
},
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
819
|
-
this.eyePipeline = this.createRenderPipeline({
|
|
820
|
-
label: "eye overlay pipeline",
|
|
821
|
-
layout: mainPipelineLayout,
|
|
822
|
-
shaderModule,
|
|
823
|
-
vertexBuffers: fullVertexBuffers,
|
|
824
|
-
fragmentTarget: standardBlend,
|
|
825
|
-
cullMode: "front",
|
|
826
|
-
depthStencil: {
|
|
827
|
-
format: "depth24plus-stencil8",
|
|
828
|
-
depthWriteEnabled: true,
|
|
829
|
-
depthCompare: "less-equal",
|
|
830
|
-
depthBias: -0.00005,
|
|
831
|
-
depthBiasSlopeScale: 0.0,
|
|
832
|
-
depthBiasClamp: 0.0,
|
|
833
|
-
stencilFront: {
|
|
834
|
-
compare: "always",
|
|
835
|
-
failOp: "keep",
|
|
836
|
-
depthFailOp: "keep",
|
|
837
|
-
passOp: "replace",
|
|
838
|
-
},
|
|
839
|
-
stencilBack: {
|
|
840
|
-
compare: "always",
|
|
841
|
-
failOp: "keep",
|
|
842
|
-
depthFailOp: "keep",
|
|
843
|
-
passOp: "replace",
|
|
844
|
-
},
|
|
845
|
-
},
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
849
|
-
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
850
|
-
label: "depth only shader",
|
|
726
|
+
// GPU picking: encode (modelIndex, materialIndex) as color
|
|
727
|
+
const pickShaderModule = this.device.createShaderModule({
|
|
728
|
+
label: "pick shader",
|
|
851
729
|
code: /* wgsl */ `
|
|
852
730
|
struct CameraUniforms {
|
|
853
731
|
view: mat4x4f,
|
|
@@ -855,94 +733,92 @@ export class Engine {
|
|
|
855
733
|
viewPos: vec3f,
|
|
856
734
|
_padding: f32,
|
|
857
735
|
};
|
|
736
|
+
struct PickId {
|
|
737
|
+
modelId: f32,
|
|
738
|
+
materialId: f32,
|
|
739
|
+
_p1: f32,
|
|
740
|
+
_p2: f32,
|
|
741
|
+
};
|
|
858
742
|
|
|
859
743
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
860
|
-
@group(
|
|
744
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
745
|
+
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
861
746
|
|
|
862
747
|
@vertex fn vs(
|
|
863
748
|
@location(0) position: vec3f,
|
|
864
749
|
@location(1) normal: vec3f,
|
|
750
|
+
@location(2) uv: vec2f,
|
|
865
751
|
@location(3) joints0: vec4<u32>,
|
|
866
752
|
@location(4) weights0: vec4<f32>
|
|
867
753
|
) -> @builtin(position) vec4f {
|
|
868
754
|
let pos4 = vec4f(position, 1.0);
|
|
869
|
-
|
|
870
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
871
755
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
872
756
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
873
|
-
let
|
|
874
|
-
|
|
875
|
-
var
|
|
876
|
-
|
|
877
|
-
let j = joints0[i];
|
|
878
|
-
let w = normalizedWeights[i];
|
|
879
|
-
let m = skinMats[j];
|
|
880
|
-
skinnedPos += (m * pos4) * w;
|
|
881
|
-
}
|
|
882
|
-
let worldPos = skinnedPos.xyz;
|
|
883
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
884
|
-
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);
|
|
885
761
|
}
|
|
886
762
|
|
|
887
763
|
@fragment fn fs() -> @location(0) vec4f {
|
|
888
|
-
return vec4f(
|
|
764
|
+
return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
|
|
889
765
|
}
|
|
890
766
|
`,
|
|
891
767
|
})
|
|
892
768
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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" }],
|
|
902
808
|
},
|
|
903
|
-
|
|
904
|
-
cullMode: "none",
|
|
809
|
+
primitive: { cullMode: "none" },
|
|
905
810
|
depthStencil: {
|
|
906
|
-
format: "depth24plus
|
|
811
|
+
format: "depth24plus",
|
|
907
812
|
depthWriteEnabled: true,
|
|
908
813
|
depthCompare: "less-equal",
|
|
909
|
-
depthBias: 0.0,
|
|
910
|
-
depthBiasSlopeScale: 0.0,
|
|
911
|
-
depthBiasClamp: 0.0,
|
|
912
814
|
},
|
|
913
815
|
})
|
|
914
816
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
shaderModule,
|
|
921
|
-
vertexBuffers: fullVertexBuffers,
|
|
922
|
-
fragmentTarget: standardBlend,
|
|
923
|
-
cullMode: "none",
|
|
924
|
-
depthStencil: {
|
|
925
|
-
format: "depth24plus-stencil8",
|
|
926
|
-
depthWriteEnabled: false,
|
|
927
|
-
depthCompare: "less-equal",
|
|
928
|
-
stencilFront: {
|
|
929
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
930
|
-
failOp: "keep",
|
|
931
|
-
depthFailOp: "keep",
|
|
932
|
-
passOp: "keep",
|
|
933
|
-
},
|
|
934
|
-
stencilBack: {
|
|
935
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
936
|
-
failOp: "keep",
|
|
937
|
-
depthFailOp: "keep",
|
|
938
|
-
passOp: "keep",
|
|
939
|
-
},
|
|
940
|
-
},
|
|
941
|
-
})
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
this.hairPipelineOverEyes = createHairPipeline(true)
|
|
945
|
-
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
|
+
})
|
|
946
822
|
}
|
|
947
823
|
|
|
948
824
|
|
|
@@ -974,7 +850,7 @@ export class Engine {
|
|
|
974
850
|
this.multisampleTexture = this.device.createTexture({
|
|
975
851
|
label: "multisample render target",
|
|
976
852
|
size: [width, height],
|
|
977
|
-
sampleCount:
|
|
853
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
978
854
|
format: this.presentationFormat,
|
|
979
855
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
980
856
|
})
|
|
@@ -982,29 +858,20 @@ export class Engine {
|
|
|
982
858
|
this.depthTexture = this.device.createTexture({
|
|
983
859
|
label: "depth texture",
|
|
984
860
|
size: [width, height],
|
|
985
|
-
sampleCount:
|
|
861
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
986
862
|
format: "depth24plus-stencil8",
|
|
987
863
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
988
864
|
})
|
|
989
865
|
|
|
990
866
|
const depthTextureView = this.depthTexture.createView()
|
|
991
867
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
this.
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
loadOp: "clear",
|
|
1000
|
-
storeOp: "store",
|
|
1001
|
-
}
|
|
1002
|
-
: {
|
|
1003
|
-
view: this.context.getCurrentTexture().createView(),
|
|
1004
|
-
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1005
|
-
loadOp: "clear",
|
|
1006
|
-
storeOp: "store",
|
|
1007
|
-
}
|
|
868
|
+
const colorAttachment: GPURenderPassColorAttachment = {
|
|
869
|
+
view: this.multisampleTexture.createView(),
|
|
870
|
+
resolveTarget: this.context.getCurrentTexture().createView(),
|
|
871
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
872
|
+
loadOp: "clear",
|
|
873
|
+
storeOp: "store",
|
|
874
|
+
}
|
|
1008
875
|
|
|
1009
876
|
this.renderPassDescriptor = {
|
|
1010
877
|
label: "renderPass",
|
|
@@ -1016,11 +883,26 @@ export class Engine {
|
|
|
1016
883
|
depthStoreOp: "store",
|
|
1017
884
|
stencilClearValue: 0,
|
|
1018
885
|
stencilLoadOp: "clear",
|
|
1019
|
-
stencilStoreOp: "discard",
|
|
886
|
+
stencilStoreOp: "discard",
|
|
1020
887
|
},
|
|
1021
888
|
}
|
|
1022
889
|
|
|
1023
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
|
+
}
|
|
1024
906
|
}
|
|
1025
907
|
}
|
|
1026
908
|
|
|
@@ -1038,6 +920,49 @@ export class Engine {
|
|
|
1038
920
|
this.camera.attachControl(this.canvas)
|
|
1039
921
|
}
|
|
1040
922
|
|
|
923
|
+
/** Set static camera look-at / orbit center. Clears any model follow binding. */
|
|
924
|
+
public setCameraTarget(v: Vec3): void
|
|
925
|
+
/** Bind camera orbit center to a model's bone (Souls-style follow cam). Pass null to unbind. */
|
|
926
|
+
public setCameraTarget(model: Model | null, boneName: string, offset?: Vec3): void
|
|
927
|
+
public setCameraTarget(modelOrVec: Model | Vec3 | null, boneName?: string, offset?: Vec3): void {
|
|
928
|
+
if (modelOrVec === null) {
|
|
929
|
+
this.cameraTargetModel = null
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
if ("x" in modelOrVec && "y" in modelOrVec && "z" in modelOrVec) {
|
|
933
|
+
this.cameraTargetModel = null
|
|
934
|
+
this.camera.target.x = modelOrVec.x
|
|
935
|
+
this.camera.target.y = modelOrVec.y
|
|
936
|
+
this.camera.target.z = modelOrVec.z
|
|
937
|
+
return
|
|
938
|
+
}
|
|
939
|
+
this.cameraTargetModel = modelOrVec
|
|
940
|
+
this.cameraTargetBoneName = boneName ?? ""
|
|
941
|
+
this.cameraTargetOffset.x = offset?.x ?? 0
|
|
942
|
+
this.cameraTargetOffset.y = offset?.y ?? 0
|
|
943
|
+
this.cameraTargetOffset.z = offset?.z ?? 0
|
|
944
|
+
}
|
|
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
|
+
|
|
1041
966
|
// Step 5: Create lighting buffers
|
|
1042
967
|
private setupLighting() {
|
|
1043
968
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -1096,11 +1021,8 @@ export class Engine {
|
|
|
1096
1021
|
width?: number
|
|
1097
1022
|
height?: number
|
|
1098
1023
|
diffuseColor?: Vec3
|
|
1099
|
-
reflectionLevel?: number
|
|
1100
|
-
reflectionTextureSize?: number
|
|
1101
1024
|
fadeStart?: number
|
|
1102
1025
|
fadeEnd?: number
|
|
1103
|
-
mode?: "reflection" | "shadow"
|
|
1104
1026
|
shadowMapSize?: number
|
|
1105
1027
|
shadowStrength?: number
|
|
1106
1028
|
}): void {
|
|
@@ -1108,29 +1030,20 @@ export class Engine {
|
|
|
1108
1030
|
width: 100,
|
|
1109
1031
|
height: 100,
|
|
1110
1032
|
diffuseColor: new Vec3(1, 1, 1),
|
|
1111
|
-
reflectionLevel: 0.5,
|
|
1112
|
-
reflectionTextureSize: 1024,
|
|
1113
1033
|
fadeStart: 5.0,
|
|
1114
1034
|
fadeEnd: 60.0,
|
|
1115
|
-
mode: "reflection" as const,
|
|
1116
1035
|
shadowMapSize: 4096,
|
|
1117
1036
|
shadowStrength: 1.0,
|
|
1118
1037
|
...options,
|
|
1119
1038
|
}
|
|
1120
|
-
this.groundMode = opts.mode
|
|
1121
1039
|
this.createGroundGeometry(opts.width, opts.height)
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
this.createReflectionTexture(opts.reflectionTextureSize)
|
|
1125
|
-
} else {
|
|
1126
|
-
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
|
|
1127
|
-
}
|
|
1128
|
-
this.groundHasReflections = true
|
|
1040
|
+
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
|
|
1041
|
+
this.hasGround = true
|
|
1129
1042
|
this.groundDrawCall = {
|
|
1130
1043
|
type: "ground",
|
|
1131
1044
|
count: 6,
|
|
1132
1045
|
firstIndex: 0,
|
|
1133
|
-
bindGroup:
|
|
1046
|
+
bindGroup: this.groundShadowBindGroup!,
|
|
1134
1047
|
materialName: "Ground",
|
|
1135
1048
|
}
|
|
1136
1049
|
}
|
|
@@ -1185,6 +1098,17 @@ export class Engine {
|
|
|
1185
1098
|
}
|
|
1186
1099
|
}
|
|
1187
1100
|
|
|
1101
|
+
public async loadModel(path: string): Promise<Model>
|
|
1102
|
+
public async loadModel(name: string, path: string): Promise<Model>
|
|
1103
|
+
public async loadModel(nameOrPath: string, path?: string): Promise<Model> {
|
|
1104
|
+
const pmxPath = path === undefined ? nameOrPath : path
|
|
1105
|
+
const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath
|
|
1106
|
+
const model = await PmxLoader.load(pmxPath)
|
|
1107
|
+
model.setName(name)
|
|
1108
|
+
await this.addModel(model, pmxPath, name)
|
|
1109
|
+
return model
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1188
1112
|
public async addModel(model: Model, pmxPath: string, name?: string): Promise<string> {
|
|
1189
1113
|
const requested = name ?? model.name
|
|
1190
1114
|
let key = requested
|
|
@@ -1199,10 +1123,6 @@ export class Engine {
|
|
|
1199
1123
|
return key
|
|
1200
1124
|
}
|
|
1201
1125
|
|
|
1202
|
-
public async registerModel(model: Model, pmxPath: string): Promise<string> {
|
|
1203
|
-
return this.addModel(model, pmxPath)
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
1126
|
public removeModel(name: string): void {
|
|
1207
1127
|
this.modelInstances.delete(name)
|
|
1208
1128
|
}
|
|
@@ -1249,12 +1169,20 @@ export class Engine {
|
|
|
1249
1169
|
return inst ? !inst.hiddenMaterials.has(materialName) : false
|
|
1250
1170
|
}
|
|
1251
1171
|
|
|
1252
|
-
public
|
|
1253
|
-
this.
|
|
1172
|
+
public setIKEnabled(enabled: boolean): void {
|
|
1173
|
+
this.ikEnabled = enabled
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
public getIKEnabled(): boolean {
|
|
1177
|
+
return this.ikEnabled
|
|
1254
1178
|
}
|
|
1255
1179
|
|
|
1256
|
-
public
|
|
1257
|
-
this.
|
|
1180
|
+
public setPhysicsEnabled(enabled: boolean): void {
|
|
1181
|
+
this.physicsEnabled = enabled
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
public getPhysicsEnabled(): boolean {
|
|
1185
|
+
return this.physicsEnabled
|
|
1258
1186
|
}
|
|
1259
1187
|
|
|
1260
1188
|
public resetPhysics(): void {
|
|
@@ -1265,19 +1193,15 @@ export class Engine {
|
|
|
1265
1193
|
})
|
|
1266
1194
|
}
|
|
1267
1195
|
|
|
1268
|
-
private instances(): IterableIterator<ModelInstance> {
|
|
1269
|
-
return this.modelInstances.values()
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
1196
|
private forEachInstance(fn: (inst: ModelInstance) => void): void {
|
|
1273
|
-
for (const inst of this.
|
|
1197
|
+
for (const inst of this.modelInstances.values()) fn(inst)
|
|
1274
1198
|
}
|
|
1275
1199
|
|
|
1276
1200
|
private updateInstances(deltaTime: number): void {
|
|
1277
1201
|
this.forEachInstance((inst) => {
|
|
1278
|
-
const verticesChanged = inst.model.update(deltaTime)
|
|
1202
|
+
const verticesChanged = inst.model.update(deltaTime, this.ikEnabled)
|
|
1279
1203
|
if (verticesChanged) inst.vertexBufferNeedsUpdate = true
|
|
1280
|
-
if (inst.physics &&
|
|
1204
|
+
if (inst.physics && this.physicsEnabled) {
|
|
1281
1205
|
inst.physics.step(
|
|
1282
1206
|
deltaTime,
|
|
1283
1207
|
inst.model.getWorldMatrices(),
|
|
@@ -1362,6 +1286,22 @@ export class Engine {
|
|
|
1362
1286
|
],
|
|
1363
1287
|
})
|
|
1364
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
|
+
|
|
1365
1305
|
const inst: ModelInstance = {
|
|
1366
1306
|
name,
|
|
1367
1307
|
model,
|
|
@@ -1374,6 +1314,9 @@ export class Engine {
|
|
|
1374
1314
|
drawCalls: [],
|
|
1375
1315
|
shadowDrawCalls: [],
|
|
1376
1316
|
shadowBindGroup,
|
|
1317
|
+
mainPerInstanceBindGroup,
|
|
1318
|
+
pickPerInstanceBindGroup,
|
|
1319
|
+
pickDrawCalls: [],
|
|
1377
1320
|
hiddenMaterials: new Set(),
|
|
1378
1321
|
physics,
|
|
1379
1322
|
vertexBufferNeedsUpdate: false,
|
|
@@ -1454,58 +1397,6 @@ export class Engine {
|
|
|
1454
1397
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices)
|
|
1455
1398
|
}
|
|
1456
1399
|
|
|
1457
|
-
private createGroundMaterialBuffer(diffuseColor: Vec3, reflectionLevel: number, fadeStart: number, fadeEnd: number) {
|
|
1458
|
-
const u = new Float32Array(8)
|
|
1459
|
-
u[0] = diffuseColor.x
|
|
1460
|
-
u[1] = diffuseColor.y
|
|
1461
|
-
u[2] = diffuseColor.z
|
|
1462
|
-
u[3] = reflectionLevel
|
|
1463
|
-
u[4] = fadeStart
|
|
1464
|
-
u[5] = fadeEnd
|
|
1465
|
-
u[6] = 0
|
|
1466
|
-
u[7] = 0
|
|
1467
|
-
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1468
|
-
label: "ground material uniform buffer",
|
|
1469
|
-
size: 64,
|
|
1470
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1471
|
-
})
|
|
1472
|
-
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u)
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
private createReflectionTexture(size: number) {
|
|
1476
|
-
this.groundReflectionTexture = this.device.createTexture({
|
|
1477
|
-
label: "ground reflection texture",
|
|
1478
|
-
size: [size, size],
|
|
1479
|
-
sampleCount: this.sampleCount,
|
|
1480
|
-
format: this.presentationFormat,
|
|
1481
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1482
|
-
})
|
|
1483
|
-
this.groundReflectionResolveTexture = this.device.createTexture({
|
|
1484
|
-
label: "ground reflection resolve texture",
|
|
1485
|
-
size: [size, size],
|
|
1486
|
-
format: this.presentationFormat,
|
|
1487
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1488
|
-
})
|
|
1489
|
-
this.groundReflectionDepthTexture = this.device.createTexture({
|
|
1490
|
-
label: "ground reflection depth texture",
|
|
1491
|
-
size: [size, size],
|
|
1492
|
-
sampleCount: this.sampleCount,
|
|
1493
|
-
format: "depth24plus-stencil8",
|
|
1494
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1495
|
-
})
|
|
1496
|
-
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1497
|
-
label: "ground reflection bind group",
|
|
1498
|
-
layout: this.groundBindGroupLayout,
|
|
1499
|
-
entries: [
|
|
1500
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1501
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1502
|
-
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1503
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1504
|
-
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer! } },
|
|
1505
|
-
],
|
|
1506
|
-
})
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
1400
|
private createShadowGroundResources(
|
|
1510
1401
|
shadowMapSize: number,
|
|
1511
1402
|
diffuseColor: Vec3,
|
|
@@ -1574,6 +1465,8 @@ export class Engine {
|
|
|
1574
1465
|
if (materials.length === 0) throw new Error("Model has no materials")
|
|
1575
1466
|
const textures = model.getTextures()
|
|
1576
1467
|
const prefix = `${inst.name}: `
|
|
1468
|
+
// 1-based so that (0,0) = clear color = "no hit"
|
|
1469
|
+
const modelId = this.modelInstances.size + 1
|
|
1577
1470
|
|
|
1578
1471
|
const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
|
|
1579
1472
|
if (texIndex < 0 || texIndex >= textures.length) return null
|
|
@@ -1582,9 +1475,11 @@ export class Engine {
|
|
|
1582
1475
|
}
|
|
1583
1476
|
|
|
1584
1477
|
let currentIndexOffset = 0
|
|
1478
|
+
let materialId = 0
|
|
1585
1479
|
for (const mat of materials) {
|
|
1586
1480
|
const indexCount = mat.vertexCount
|
|
1587
1481
|
if (indexCount === 0) continue
|
|
1482
|
+
materialId++
|
|
1588
1483
|
|
|
1589
1484
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex)
|
|
1590
1485
|
if (!diffuseTexture) throw new Error(`Material "${mat.name}" has no diffuse texture`)
|
|
@@ -1595,73 +1490,24 @@ export class Engine {
|
|
|
1595
1490
|
const materialUniformBuffer = this.createMaterialUniformBuffer(
|
|
1596
1491
|
prefix + mat.name,
|
|
1597
1492
|
materialAlpha,
|
|
1598
|
-
0.0,
|
|
1599
1493
|
[mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]],
|
|
1600
1494
|
mat.ambient,
|
|
1601
1495
|
mat.specular,
|
|
1602
1496
|
mat.shininess
|
|
1603
1497
|
)
|
|
1604
1498
|
|
|
1499
|
+
const textureView = diffuseTexture.createView()
|
|
1605
1500
|
const bindGroup = this.device.createBindGroup({
|
|
1606
1501
|
label: `${prefix}material: ${mat.name}`,
|
|
1607
|
-
layout: this.
|
|
1502
|
+
layout: this.mainPerMaterialBindGroupLayout,
|
|
1608
1503
|
entries: [
|
|
1609
|
-
{ binding: 0, resource:
|
|
1610
|
-
{ binding: 1, resource: { buffer:
|
|
1611
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1612
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1613
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1614
|
-
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1504
|
+
{ binding: 0, resource: textureView },
|
|
1505
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
1615
1506
|
],
|
|
1616
1507
|
})
|
|
1617
1508
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
inst.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1621
|
-
} else if (mat.isHair) {
|
|
1622
|
-
const createHairBindGroup = (isOverEyes: boolean) => {
|
|
1623
|
-
const buf = this.createMaterialUniformBuffer(
|
|
1624
|
-
`${prefix}${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
1625
|
-
materialAlpha,
|
|
1626
|
-
isOverEyes ? 1.0 : 0.0,
|
|
1627
|
-
[mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]],
|
|
1628
|
-
mat.ambient,
|
|
1629
|
-
mat.specular,
|
|
1630
|
-
mat.shininess
|
|
1631
|
-
)
|
|
1632
|
-
return this.device.createBindGroup({
|
|
1633
|
-
label: `${prefix}hair ${isOverEyes ? "over eyes" : "over non-eyes"}: ${mat.name}`,
|
|
1634
|
-
layout: this.mainBindGroupLayout,
|
|
1635
|
-
entries: [
|
|
1636
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1637
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1638
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1639
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1640
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1641
|
-
{ binding: 5, resource: { buffer: buf } },
|
|
1642
|
-
],
|
|
1643
|
-
})
|
|
1644
|
-
}
|
|
1645
|
-
inst.drawCalls.push({
|
|
1646
|
-
type: "hair-over-eyes",
|
|
1647
|
-
count: indexCount,
|
|
1648
|
-
firstIndex: currentIndexOffset,
|
|
1649
|
-
bindGroup: createHairBindGroup(true),
|
|
1650
|
-
materialName: mat.name,
|
|
1651
|
-
})
|
|
1652
|
-
inst.drawCalls.push({
|
|
1653
|
-
type: "hair-over-non-eyes",
|
|
1654
|
-
count: indexCount,
|
|
1655
|
-
firstIndex: currentIndexOffset,
|
|
1656
|
-
bindGroup: createHairBindGroup(false),
|
|
1657
|
-
materialName: mat.name,
|
|
1658
|
-
})
|
|
1659
|
-
} else if (isTransparent) {
|
|
1660
|
-
inst.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1661
|
-
} else {
|
|
1662
|
-
inst.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1509
|
+
const type: DrawCallType = isTransparent ? "transparent" : "opaque"
|
|
1510
|
+
inst.drawCalls.push({ type, count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1665
1511
|
|
|
1666
1512
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1667
1513
|
const materialUniformData = new Float32Array([
|
|
@@ -1671,32 +1517,37 @@ export class Engine {
|
|
|
1671
1517
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData)
|
|
1672
1518
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1673
1519
|
label: `${prefix}outline: ${mat.name}`,
|
|
1674
|
-
layout: this.
|
|
1520
|
+
layout: this.outlinePerMaterialBindGroupLayout,
|
|
1675
1521
|
entries: [
|
|
1676
|
-
{ binding: 0, resource: { buffer:
|
|
1677
|
-
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1678
|
-
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1522
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
1679
1523
|
],
|
|
1680
1524
|
})
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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 })
|
|
1685
1538
|
}
|
|
1686
1539
|
|
|
1687
1540
|
currentIndexOffset += indexCount
|
|
1688
1541
|
}
|
|
1689
1542
|
|
|
1690
1543
|
for (const d of inst.drawCalls) {
|
|
1691
|
-
if (d.type === "opaque"
|
|
1692
|
-
inst.shadowDrawCalls.push(d)
|
|
1544
|
+
if (d.type === "opaque") inst.shadowDrawCalls.push(d)
|
|
1693
1545
|
}
|
|
1694
1546
|
}
|
|
1695
1547
|
|
|
1696
1548
|
private createMaterialUniformBuffer(
|
|
1697
1549
|
label: string,
|
|
1698
1550
|
alpha: number,
|
|
1699
|
-
isOverEyes: number,
|
|
1700
1551
|
diffuseColor: [number, number, number],
|
|
1701
1552
|
ambientColor: [number, number, number],
|
|
1702
1553
|
specularColor: [number, number, number],
|
|
@@ -1705,25 +1556,13 @@ export class Engine {
|
|
|
1705
1556
|
const data = new Float32Array(20)
|
|
1706
1557
|
data.set([
|
|
1707
1558
|
alpha,
|
|
1708
|
-
1.0,
|
|
1709
1559
|
this.rimLightIntensity,
|
|
1710
|
-
shininess,
|
|
1711
|
-
|
|
1712
|
-
1.0,
|
|
1713
|
-
1.0,
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
diffuseColor[1],
|
|
1717
|
-
diffuseColor[2],
|
|
1718
|
-
0.0, // diffuseColor (vec3), _padding2
|
|
1719
|
-
ambientColor[0],
|
|
1720
|
-
ambientColor[1],
|
|
1721
|
-
ambientColor[2],
|
|
1722
|
-
0.0, // ambientColor (vec3), _padding3
|
|
1723
|
-
specularColor[0],
|
|
1724
|
-
specularColor[1],
|
|
1725
|
-
specularColor[2],
|
|
1726
|
-
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,
|
|
1727
1566
|
])
|
|
1728
1567
|
return this.createUniformBuffer(`material uniform: ${label}`, data)
|
|
1729
1568
|
}
|
|
@@ -1776,128 +1615,16 @@ export class Engine {
|
|
|
1776
1615
|
}
|
|
1777
1616
|
}
|
|
1778
1617
|
|
|
1779
|
-
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1780
|
-
private renderEyes(pass: GPURenderPassEncoder, inst: ModelInstance, useReflectionPipeline = false) {
|
|
1781
|
-
if (useReflectionPipeline) {
|
|
1782
|
-
pass.setPipeline(this.reflectionPipeline)
|
|
1783
|
-
for (const draw of inst.drawCalls) {
|
|
1784
|
-
if (draw.type === "eye") {
|
|
1785
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1786
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
} else {
|
|
1790
|
-
pass.setPipeline(this.eyePipeline)
|
|
1791
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1792
|
-
for (const draw of inst.drawCalls) {
|
|
1793
|
-
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1794
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1795
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
1618
|
|
|
1801
1619
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
1802
|
-
if (!this.
|
|
1803
|
-
|
|
1804
|
-
pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline)
|
|
1620
|
+
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
1621
|
+
pass.setPipeline(this.groundShadowPipeline)
|
|
1805
1622
|
pass.setVertexBuffer(0, this.groundVertexBuffer)
|
|
1806
1623
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16")
|
|
1807
1624
|
pass.setBindGroup(0, this.groundDrawCall.bindGroup)
|
|
1808
1625
|
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0)
|
|
1809
1626
|
}
|
|
1810
1627
|
|
|
1811
|
-
private renderReflectionTexture() {
|
|
1812
|
-
if (!this.groundReflectionTexture) return
|
|
1813
|
-
|
|
1814
|
-
const mirrorMatrix = this.createMirrorMatrix(new Vec3(0, 1, 0), 0)
|
|
1815
|
-
this.updateCameraUniforms()
|
|
1816
|
-
|
|
1817
|
-
const reflectionEncoder = this.device.createCommandEncoder()
|
|
1818
|
-
const reflectionPassDescriptor: GPURenderPassDescriptor = {
|
|
1819
|
-
label: "reflection render pass",
|
|
1820
|
-
colorAttachments: [
|
|
1821
|
-
{
|
|
1822
|
-
view: this.groundReflectionTexture!.createView(),
|
|
1823
|
-
resolveTarget: this.groundReflectionResolveTexture!.createView(),
|
|
1824
|
-
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // White
|
|
1825
|
-
loadOp: "clear",
|
|
1826
|
-
storeOp: "store",
|
|
1827
|
-
},
|
|
1828
|
-
],
|
|
1829
|
-
depthStencilAttachment: {
|
|
1830
|
-
view: this.groundReflectionDepthTexture!.createView(),
|
|
1831
|
-
depthClearValue: 1.0,
|
|
1832
|
-
depthLoadOp: "clear",
|
|
1833
|
-
depthStoreOp: "store",
|
|
1834
|
-
stencilClearValue: 0,
|
|
1835
|
-
stencilLoadOp: "clear",
|
|
1836
|
-
stencilStoreOp: "discard",
|
|
1837
|
-
},
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor)
|
|
1841
|
-
this.forEachInstance((inst) => this.renderOneModel(reflectionPass, inst, true, mirrorMatrix))
|
|
1842
|
-
reflectionPass.end()
|
|
1843
|
-
this.device.queue.submit([reflectionEncoder.finish()])
|
|
1844
|
-
this.updateSkinMatrices()
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
private renderHair(pass: GPURenderPassEncoder, inst: ModelInstance, useReflectionPipeline = false) {
|
|
1848
|
-
if (useReflectionPipeline) {
|
|
1849
|
-
pass.setPipeline(this.reflectionPipeline)
|
|
1850
|
-
for (const draw of inst.drawCalls) {
|
|
1851
|
-
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
1852
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1853
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
return
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
const hasHair = inst.drawCalls.some(
|
|
1860
|
-
(d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d)
|
|
1861
|
-
)
|
|
1862
|
-
if (hasHair) {
|
|
1863
|
-
pass.setPipeline(this.hairDepthPipeline)
|
|
1864
|
-
for (const draw of inst.drawCalls) {
|
|
1865
|
-
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, draw)) {
|
|
1866
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1867
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
const hairOverEyes = inst.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(inst, d))
|
|
1873
|
-
if (hairOverEyes.length > 0) {
|
|
1874
|
-
pass.setPipeline(this.hairPipelineOverEyes)
|
|
1875
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1876
|
-
for (const draw of hairOverEyes) {
|
|
1877
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1878
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
const hairOverNonEyes = inst.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(inst, d))
|
|
1883
|
-
if (hairOverNonEyes.length > 0) {
|
|
1884
|
-
pass.setPipeline(this.hairPipelineOverNonEyes)
|
|
1885
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1886
|
-
for (const draw of hairOverNonEyes) {
|
|
1887
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1888
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
const hairOutlines = inst.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(inst, d))
|
|
1893
|
-
if (hairOutlines.length > 0) {
|
|
1894
|
-
pass.setPipeline(this.hairOutlinePipeline)
|
|
1895
|
-
for (const draw of hairOutlines) {
|
|
1896
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1897
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
1628
|
|
|
1902
1629
|
private handleCanvasDoubleClick = (event: MouseEvent) => {
|
|
1903
1630
|
if (!this.onRaycast || this.modelInstances.size === 0) return
|
|
@@ -1938,111 +1665,92 @@ export class Engine {
|
|
|
1938
1665
|
this.onRaycast?.("", null, screenX, screenY)
|
|
1939
1666
|
return
|
|
1940
1667
|
}
|
|
1668
|
+
const dpr = window.devicePixelRatio || 1
|
|
1669
|
+
this.pendingPick = { x: Math.floor(screenX * dpr), y: Math.floor(screenY * dpr) }
|
|
1670
|
+
}
|
|
1941
1671
|
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
const rect = this.canvas.getBoundingClientRect()
|
|
1945
|
-
const clipX = (screenX / rect.width) * 2 - 1
|
|
1946
|
-
const clipY = 1 - (screenY / rect.height) * 2
|
|
1947
|
-
const viewProjMatrix = projectionMatrix.multiply(viewMatrix)
|
|
1948
|
-
const inverseViewProj = viewProjMatrix.inverse()
|
|
1949
|
-
const transformPoint = (matrix: Mat4, point: Vec3): Vec3 => {
|
|
1950
|
-
const m = matrix.values
|
|
1951
|
-
const x = point.x, y = point.y, z = point.z
|
|
1952
|
-
const result = new Vec3(
|
|
1953
|
-
m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
1954
|
-
m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
1955
|
-
m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
1956
|
-
)
|
|
1957
|
-
const w = m[3] * x + m[7] * y + m[11] * z + m[15]
|
|
1958
|
-
return result.scale(w !== 0 ? 1 / w : 1)
|
|
1959
|
-
}
|
|
1960
|
-
const worldNear = transformPoint(inverseViewProj, new Vec3(clipX, clipY, -1))
|
|
1961
|
-
const worldFar = transformPoint(inverseViewProj, new Vec3(clipX, clipY, 1))
|
|
1962
|
-
const rayOrigin = this.camera.getPosition()
|
|
1963
|
-
const rayDirection = worldFar.subtract(worldNear).normalize()
|
|
1964
|
-
|
|
1965
|
-
const transformByMatrix = (matrix: Float32Array, offset: number, point: Vec3): Vec3 => {
|
|
1966
|
-
const m = matrix, x = point.x, y = point.y, z = point.z
|
|
1967
|
-
return new Vec3(
|
|
1968
|
-
m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12],
|
|
1969
|
-
m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13],
|
|
1970
|
-
m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]
|
|
1971
|
-
)
|
|
1972
|
-
}
|
|
1672
|
+
private renderPickPass(encoder: GPUCommandEncoder): void {
|
|
1673
|
+
if (!this.pendingPick || !this.pickTexture || !this.pickDepthTexture) return
|
|
1973
1674
|
|
|
1974
|
-
|
|
1975
|
-
|
|
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)
|
|
1976
1692
|
|
|
1977
1693
|
this.forEachInstance((inst) => {
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
const
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
const vertices = new Float32Array(baseVertices.length)
|
|
1987
|
-
const skinMatrices = model.getSkinMatrices()
|
|
1988
|
-
for (let i = 0; i < baseVertices.length; i += 8) {
|
|
1989
|
-
const vertexIndex = i / 8
|
|
1990
|
-
const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2])
|
|
1991
|
-
const j0 = skinning.joints[vertexIndex * 4], j1 = skinning.joints[vertexIndex * 4 + 1], j2 = skinning.joints[vertexIndex * 4 + 2], j3 = skinning.joints[vertexIndex * 4 + 3]
|
|
1992
|
-
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
|
|
1993
|
-
const ws = w0 + w1 + w2 + w3
|
|
1994
|
-
const nw = ws > 0.0001 ? [w0 / ws, w1 / ws, w2 / ws, w3 / ws] : [1, 0, 0, 0]
|
|
1995
|
-
let sp = new Vec3(0, 0, 0)
|
|
1996
|
-
for (let j = 0; j < 4; j++) {
|
|
1997
|
-
if (nw[j] <= 0) continue
|
|
1998
|
-
const transformed = transformByMatrix(skinMatrices, [j0, j1, j2, j3][j] * 16, position)
|
|
1999
|
-
sp = sp.add(transformed.scale(nw[j]))
|
|
2000
|
-
}
|
|
2001
|
-
vertices[i] = sp.x
|
|
2002
|
-
vertices[i + 1] = sp.y
|
|
2003
|
-
vertices[i + 2] = sp.z
|
|
2004
|
-
vertices[i + 3] = baseVertices[i + 3]
|
|
2005
|
-
vertices[i + 4] = baseVertices[i + 4]
|
|
2006
|
-
vertices[i + 5] = baseVertices[i + 5]
|
|
2007
|
-
vertices[i + 6] = baseVertices[i + 6]
|
|
2008
|
-
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)
|
|
2009
1702
|
}
|
|
1703
|
+
})
|
|
2010
1704
|
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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 }
|
|
2038
1749
|
}
|
|
2039
1750
|
}
|
|
2040
|
-
})
|
|
2041
|
-
|
|
2042
|
-
if (this.onRaycast) {
|
|
2043
|
-
const hit = closest as { modelName: string; materialName: string; distance: number } | null
|
|
2044
|
-
this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY)
|
|
2045
1751
|
}
|
|
1752
|
+
|
|
1753
|
+
this.onRaycast(hitModel, hitMaterial, screenX, screenY)
|
|
2046
1754
|
}
|
|
2047
1755
|
|
|
2048
1756
|
public render() {
|
|
@@ -2052,18 +1760,29 @@ export class Engine {
|
|
|
2052
1760
|
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
2053
1761
|
this.lastFrameTime = currentTime
|
|
2054
1762
|
|
|
2055
|
-
this.updateCameraUniforms()
|
|
2056
1763
|
this.updateRenderTarget()
|
|
2057
1764
|
|
|
2058
1765
|
const hasModels = this.modelInstances.size > 0
|
|
2059
1766
|
if (hasModels) {
|
|
2060
1767
|
this.updateInstances(deltaTime)
|
|
2061
1768
|
this.updateSkinMatrices()
|
|
1769
|
+
// Update camera target from bound model (bone not found → 0,0,0 + offset)
|
|
1770
|
+
if (this.cameraTargetModel) {
|
|
1771
|
+
const pos = this.cameraTargetModel.getBoneWorldPosition(this.cameraTargetBoneName)
|
|
1772
|
+
const px = pos?.x ?? 0
|
|
1773
|
+
const py = pos?.y ?? 0
|
|
1774
|
+
const pz = pos?.z ?? 0
|
|
1775
|
+
this.camera.target.x = px + this.cameraTargetOffset.x
|
|
1776
|
+
this.camera.target.y = py + this.cameraTargetOffset.y
|
|
1777
|
+
this.camera.target.z = pz + this.cameraTargetOffset.z
|
|
1778
|
+
}
|
|
2062
1779
|
}
|
|
2063
|
-
|
|
1780
|
+
|
|
1781
|
+
this.updateCameraUniforms()
|
|
1782
|
+
if (this.hasGround) this.updateShadowLightVP()
|
|
2064
1783
|
|
|
2065
1784
|
const encoder = this.device.createCommandEncoder()
|
|
2066
|
-
if (hasModels && this.
|
|
1785
|
+
if (hasModels && this.hasGround && this.shadowMapDepthView) {
|
|
2067
1786
|
const sp = encoder.beginRenderPass({
|
|
2068
1787
|
colorAttachments: [],
|
|
2069
1788
|
depthStencilAttachment: {
|
|
@@ -2079,14 +1798,29 @@ export class Engine {
|
|
|
2079
1798
|
}
|
|
2080
1799
|
|
|
2081
1800
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
2082
|
-
if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst
|
|
2083
|
-
if (this.
|
|
2084
|
-
|
|
1801
|
+
if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst))
|
|
1802
|
+
if (this.hasGround) this.renderGround(pass)
|
|
2085
1803
|
pass.end()
|
|
1804
|
+
|
|
1805
|
+
const pick = this.pendingPick
|
|
1806
|
+
if (pick && hasModels) this.renderPickPass(encoder)
|
|
1807
|
+
|
|
2086
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
|
+
|
|
2087
1816
|
this.updateStats(performance.now() - currentTime)
|
|
2088
1817
|
}
|
|
2089
1818
|
|
|
1819
|
+
private updateRenderTarget() {
|
|
1820
|
+
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
1821
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
1822
|
+
}
|
|
1823
|
+
|
|
2090
1824
|
private drawInstanceShadow(sp: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
2091
1825
|
sp.setBindGroup(0, inst.shadowBindGroup)
|
|
2092
1826
|
sp.setVertexBuffer(0, inst.vertexBuffer)
|
|
@@ -2098,50 +1832,42 @@ export class Engine {
|
|
|
2098
1832
|
}
|
|
2099
1833
|
}
|
|
2100
1834
|
|
|
2101
|
-
private
|
|
2102
|
-
pass.
|
|
2103
|
-
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
2104
|
-
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
2105
|
-
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
2106
|
-
|
|
2107
|
-
if (useReflection && mirrorMatrix) {
|
|
2108
|
-
this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix)
|
|
2109
|
-
pass.setPipeline(this.reflectionPipeline)
|
|
2110
|
-
for (const draw of inst.drawCalls) {
|
|
2111
|
-
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2112
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
2113
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
this.renderEyes(pass, inst, true)
|
|
2117
|
-
this.renderHair(pass, inst, true)
|
|
2118
|
-
for (const draw of inst.drawCalls) {
|
|
2119
|
-
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2120
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
2121
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
this.drawOutlines(pass, inst, true, true)
|
|
2125
|
-
return
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
pass.setPipeline(this.modelPipeline)
|
|
1835
|
+
private drawOpaque(pass: GPURenderPassEncoder, inst: ModelInstance, pipeline: GPURenderPipeline): void {
|
|
1836
|
+
pass.setPipeline(pipeline)
|
|
2129
1837
|
for (const draw of inst.drawCalls) {
|
|
2130
1838
|
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2131
|
-
pass.setBindGroup(
|
|
1839
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
2132
1840
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2133
1841
|
}
|
|
2134
1842
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
pass.setPipeline(
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
private drawTransparent(pass: GPURenderPassEncoder, inst: ModelInstance, pipeline: GPURenderPipeline): void {
|
|
1846
|
+
pass.setPipeline(pipeline)
|
|
2139
1847
|
for (const draw of inst.drawCalls) {
|
|
2140
1848
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2141
|
-
pass.setBindGroup(
|
|
1849
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
2142
1850
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2143
1851
|
}
|
|
2144
1852
|
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
private bindMainGroups(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1856
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
1857
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
private renderOneModel(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
1861
|
+
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
1862
|
+
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
1863
|
+
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
1864
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
1865
|
+
|
|
1866
|
+
this.bindMainGroups(pass, inst)
|
|
1867
|
+
this.drawOpaque(pass, inst, this.modelPipeline)
|
|
1868
|
+
this.drawOutlines(pass, inst, false)
|
|
1869
|
+
this.bindMainGroups(pass, inst)
|
|
1870
|
+
this.drawTransparent(pass, inst, this.modelPipeline)
|
|
2145
1871
|
this.drawOutlines(pass, inst, true)
|
|
2146
1872
|
}
|
|
2147
1873
|
|
|
@@ -2158,15 +1884,6 @@ export class Engine {
|
|
|
2158
1884
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData)
|
|
2159
1885
|
}
|
|
2160
1886
|
|
|
2161
|
-
private updateRenderTarget() {
|
|
2162
|
-
// Update render target to use current canvas texture
|
|
2163
|
-
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
2164
|
-
if (this.sampleCount > 1) {
|
|
2165
|
-
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
|
|
2166
|
-
} else {
|
|
2167
|
-
colorAttachment.view = this.context.getCurrentTexture().createView()
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
1887
|
|
|
2171
1888
|
private updateSkinMatrices() {
|
|
2172
1889
|
this.forEachInstance((inst) => {
|
|
@@ -2181,13 +1898,14 @@ export class Engine {
|
|
|
2181
1898
|
})
|
|
2182
1899
|
}
|
|
2183
1900
|
|
|
2184
|
-
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean
|
|
2185
|
-
if (useReflectionPipeline) return
|
|
1901
|
+
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean) {
|
|
2186
1902
|
pass.setPipeline(this.outlinePipeline)
|
|
1903
|
+
pass.setBindGroup(0, this.outlinePerFrameBindGroup)
|
|
1904
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
2187
1905
|
const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
|
|
2188
1906
|
for (const draw of inst.drawCalls) {
|
|
2189
1907
|
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
2190
|
-
pass.setBindGroup(
|
|
1908
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
2191
1909
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2192
1910
|
}
|
|
2193
1911
|
}
|
|
@@ -2218,42 +1936,4 @@ export class Engine {
|
|
|
2218
1936
|
}
|
|
2219
1937
|
}
|
|
2220
1938
|
|
|
2221
|
-
private createMirrorMatrix(planeNormal: Vec3, planeDistance: number): Mat4 {
|
|
2222
|
-
// Create reflection matrix across a plane
|
|
2223
|
-
const n = planeNormal.normalize()
|
|
2224
|
-
|
|
2225
|
-
return new Mat4(
|
|
2226
|
-
new Float32Array([
|
|
2227
|
-
1 - 2 * n.x * n.x,
|
|
2228
|
-
-2 * n.x * n.y,
|
|
2229
|
-
-2 * n.x * n.z,
|
|
2230
|
-
0,
|
|
2231
|
-
-2 * n.y * n.x,
|
|
2232
|
-
1 - 2 * n.y * n.y,
|
|
2233
|
-
-2 * n.y * n.z,
|
|
2234
|
-
0,
|
|
2235
|
-
-2 * n.z * n.x,
|
|
2236
|
-
-2 * n.z * n.y,
|
|
2237
|
-
1 - 2 * n.z * n.z,
|
|
2238
|
-
0,
|
|
2239
|
-
-2 * planeDistance * n.x,
|
|
2240
|
-
-2 * planeDistance * n.y,
|
|
2241
|
-
-2 * planeDistance * n.z,
|
|
2242
|
-
1,
|
|
2243
|
-
])
|
|
2244
|
-
)
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
private writeMirrorTransformedSkinMatrices(inst: ModelInstance, mirrorMatrix: Mat4) {
|
|
2248
|
-
const originalMatrices = inst.model.getSkinMatrices()
|
|
2249
|
-
const transformedMatrices = new Float32Array(originalMatrices.length)
|
|
2250
|
-
for (let i = 0; i < originalMatrices.length; i += 16) {
|
|
2251
|
-
const boneMatrixValues = new Float32Array(16)
|
|
2252
|
-
for (let j = 0; j < 16; j++) boneMatrixValues[j] = originalMatrices[i + j]
|
|
2253
|
-
const boneMatrix = new Mat4(boneMatrixValues)
|
|
2254
|
-
const transformed = mirrorMatrix.multiply(boneMatrix)
|
|
2255
|
-
for (let j = 0; j < 16; j++) transformedMatrices[i + j] = transformed.values[j]
|
|
2256
|
-
}
|
|
2257
|
-
this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices)
|
|
2258
|
-
}
|
|
2259
1939
|
}
|