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/dist/engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { Mat4, Vec3 } from "./math";
|
|
3
|
+
import { PmxLoader } from "./pmx-loader";
|
|
3
4
|
import { Physics } from "./physics";
|
|
4
5
|
export const DEFAULT_ENGINE_OPTIONS = {
|
|
5
6
|
ambientColor: new Vec3(0.88, 0.88, 0.88),
|
|
@@ -10,12 +11,11 @@ export const DEFAULT_ENGINE_OPTIONS = {
|
|
|
10
11
|
cameraTarget: new Vec3(0, 12.5, 0),
|
|
11
12
|
cameraFov: Math.PI / 4,
|
|
12
13
|
onRaycast: undefined,
|
|
13
|
-
multisampleCount: 4,
|
|
14
14
|
};
|
|
15
15
|
export class Engine {
|
|
16
16
|
static getInstance() {
|
|
17
17
|
if (!Engine.instance) {
|
|
18
|
-
throw new Error("Engine not ready: create Engine, await init(), then load models via
|
|
18
|
+
throw new Error("Engine not ready: create Engine, await init(), then load models via engine.loadModel().");
|
|
19
19
|
}
|
|
20
20
|
return Engine.instance;
|
|
21
21
|
}
|
|
@@ -24,22 +24,25 @@ export class Engine {
|
|
|
24
24
|
this.lightData = new Float32Array(64);
|
|
25
25
|
this.lightCount = 0;
|
|
26
26
|
this.resizeObserver = null;
|
|
27
|
-
this.
|
|
28
|
-
// Constants
|
|
29
|
-
this.STENCIL_EYE_VALUE = 1;
|
|
30
|
-
this.groundHasReflections = false;
|
|
31
|
-
this.groundMode = "reflection";
|
|
27
|
+
this.hasGround = false;
|
|
32
28
|
this.shadowLightVPMatrix = new Float32Array(16);
|
|
33
29
|
this.groundDrawCall = null;
|
|
34
30
|
this.shadowVPLightX = Number.NaN;
|
|
35
31
|
this.shadowVPLightY = Number.NaN;
|
|
36
32
|
this.shadowVPLightZ = Number.NaN;
|
|
37
|
-
// Double-tap detection
|
|
38
33
|
this.lastTouchTime = 0;
|
|
39
|
-
this.DOUBLE_TAP_DELAY = 300;
|
|
34
|
+
this.DOUBLE_TAP_DELAY = 300;
|
|
35
|
+
this.pendingPick = null;
|
|
40
36
|
this.modelInstances = new Map();
|
|
41
37
|
this.textureCache = new Map();
|
|
42
|
-
this.
|
|
38
|
+
this._nextDefaultModelId = 0;
|
|
39
|
+
// IK and physics enabled at engine level (same for all models)
|
|
40
|
+
this.ikEnabled = true;
|
|
41
|
+
this.physicsEnabled = true;
|
|
42
|
+
// Camera target binding (Babylon/Three style: camera follows model)
|
|
43
|
+
this.cameraTargetModel = null;
|
|
44
|
+
this.cameraTargetBoneName = "全ての親";
|
|
45
|
+
this.cameraTargetOffset = new Vec3(0, 0, 0);
|
|
43
46
|
this.lastFpsUpdate = performance.now();
|
|
44
47
|
this.framesSinceLastUpdate = 0;
|
|
45
48
|
this.lastFrameTime = performance.now();
|
|
@@ -83,7 +86,6 @@ export class Engine {
|
|
|
83
86
|
}
|
|
84
87
|
};
|
|
85
88
|
this.canvas = canvas;
|
|
86
|
-
this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount;
|
|
87
89
|
if (options) {
|
|
88
90
|
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor;
|
|
89
91
|
this.directionalLightIntensity =
|
|
@@ -138,7 +140,7 @@ export class Engine {
|
|
|
138
140
|
: undefined,
|
|
139
141
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
140
142
|
depthStencil: config.depthStencil,
|
|
141
|
-
multisample: config.multisample ?? { count:
|
|
143
|
+
multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
|
|
142
144
|
});
|
|
143
145
|
}
|
|
144
146
|
createPipelines() {
|
|
@@ -184,23 +186,6 @@ export class Engine {
|
|
|
184
186
|
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
185
187
|
},
|
|
186
188
|
];
|
|
187
|
-
const depthOnlyVertexBuffers = [
|
|
188
|
-
{
|
|
189
|
-
arrayStride: 8 * 4,
|
|
190
|
-
attributes: [
|
|
191
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
192
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
arrayStride: 4 * 2,
|
|
197
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
arrayStride: 4,
|
|
201
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
202
|
-
},
|
|
203
|
-
];
|
|
204
189
|
const standardBlend = {
|
|
205
190
|
format: this.presentationFormat,
|
|
206
191
|
blend: {
|
|
@@ -238,17 +223,17 @@ export class Engine {
|
|
|
238
223
|
|
|
239
224
|
struct MaterialUniforms {
|
|
240
225
|
alpha: f32,
|
|
241
|
-
alphaMultiplier: f32,
|
|
242
226
|
rimIntensity: f32,
|
|
243
227
|
shininess: f32,
|
|
228
|
+
_padding1: f32,
|
|
244
229
|
rimColor: vec3f,
|
|
245
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
246
|
-
diffuseColor: vec3f,
|
|
247
230
|
_padding2: f32,
|
|
248
|
-
|
|
231
|
+
diffuseColor: vec3f,
|
|
249
232
|
_padding3: f32,
|
|
250
|
-
|
|
233
|
+
ambientColor: vec3f,
|
|
251
234
|
_padding4: f32,
|
|
235
|
+
specularColor: vec3f,
|
|
236
|
+
_padding5: f32,
|
|
252
237
|
};
|
|
253
238
|
|
|
254
239
|
struct VertexOutput {
|
|
@@ -258,12 +243,15 @@ export class Engine {
|
|
|
258
243
|
@location(2) worldPos: vec3f,
|
|
259
244
|
};
|
|
260
245
|
|
|
246
|
+
// group 0: per-frame (bound once per pass)
|
|
261
247
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
262
248
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
263
|
-
@group(0) @binding(2) var
|
|
264
|
-
|
|
265
|
-
@group(
|
|
266
|
-
|
|
249
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
250
|
+
// group 1: per-instance (bound once per model)
|
|
251
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
252
|
+
// group 2: per-material (bound per draw call)
|
|
253
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
254
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
267
255
|
|
|
268
256
|
@vertex fn vs(
|
|
269
257
|
@location(0) position: vec3f,
|
|
@@ -275,7 +263,6 @@ export class Engine {
|
|
|
275
263
|
var output: VertexOutput;
|
|
276
264
|
let pos4 = vec4f(position, 1.0);
|
|
277
265
|
|
|
278
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
279
266
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
280
267
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
281
268
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -299,11 +286,7 @@ export class Engine {
|
|
|
299
286
|
}
|
|
300
287
|
|
|
301
288
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
302
|
-
|
|
303
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
304
|
-
if (material.isOverEyes > 0.5) {
|
|
305
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
306
|
-
}
|
|
289
|
+
let finalAlpha = material.alpha;
|
|
307
290
|
if (finalAlpha < 0.001) {
|
|
308
291
|
discard;
|
|
309
292
|
}
|
|
@@ -311,18 +294,14 @@ export class Engine {
|
|
|
311
294
|
let n = normalize(input.normal);
|
|
312
295
|
let textureColor = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
313
296
|
|
|
314
|
-
// View direction for specular and rim
|
|
315
297
|
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
316
298
|
|
|
317
|
-
// Simple lighting: global ambient + diffuse lighting
|
|
318
299
|
let albedo = textureColor * material.diffuseColor;
|
|
319
300
|
|
|
320
|
-
// Precompute material values
|
|
321
301
|
let minSpec = light.ambientColor.w;
|
|
322
302
|
let effectiveSpecular = max(material.specularColor, vec3f(minSpec));
|
|
323
303
|
let specPower = max(material.shininess, 1.0);
|
|
324
304
|
|
|
325
|
-
// Single directional light
|
|
326
305
|
let l = -light.lights[0].direction.xyz;
|
|
327
306
|
let nDotL = max(dot(n, l), 0.0);
|
|
328
307
|
let intensity = light.lights[0].color.w;
|
|
@@ -330,7 +309,6 @@ export class Engine {
|
|
|
330
309
|
|
|
331
310
|
let lightAccum = light.ambientColor.xyz + radiance * nDotL;
|
|
332
311
|
|
|
333
|
-
// Blinn-Phong specular
|
|
334
312
|
let h = normalize(l + viewDir);
|
|
335
313
|
let nDotH = max(dot(n, h), 0.0);
|
|
336
314
|
let specFactor = pow(nDotH, specPower);
|
|
@@ -338,9 +316,8 @@ export class Engine {
|
|
|
338
316
|
|
|
339
317
|
let litColor = albedo * lightAccum;
|
|
340
318
|
|
|
341
|
-
// Rim light calculation - proper Fresnel for edge-only highlights
|
|
342
319
|
let fresnel = 1.0 - abs(dot(n, viewDir));
|
|
343
|
-
let rimFactor = pow(fresnel, 4.0);
|
|
320
|
+
let rimFactor = pow(fresnel, 4.0);
|
|
344
321
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
345
322
|
|
|
346
323
|
let color = litColor + specularAccum + rimLight;
|
|
@@ -349,21 +326,42 @@ export class Engine {
|
|
|
349
326
|
}
|
|
350
327
|
`,
|
|
351
328
|
});
|
|
352
|
-
//
|
|
353
|
-
this.
|
|
354
|
-
label: "main
|
|
329
|
+
// group 0: per-frame (camera + light + sampler) — bound once per pass
|
|
330
|
+
this.mainPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
331
|
+
label: "main per-frame bind group layout",
|
|
332
|
+
entries: [
|
|
333
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
334
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
335
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
// group 1: per-instance (skinMats) — bound once per model
|
|
339
|
+
this.mainPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
340
|
+
label: "main per-instance bind group layout",
|
|
355
341
|
entries: [
|
|
356
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
342
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
// group 2: per-material (texture + material uniforms) — bound per draw call
|
|
346
|
+
this.mainPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
347
|
+
label: "main per-material bind group layout",
|
|
348
|
+
entries: [
|
|
349
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
350
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
362
351
|
],
|
|
363
352
|
});
|
|
364
353
|
const mainPipelineLayout = this.device.createPipelineLayout({
|
|
365
354
|
label: "main pipeline layout",
|
|
366
|
-
bindGroupLayouts: [this.
|
|
355
|
+
bindGroupLayouts: [this.mainPerFrameBindGroupLayout, this.mainPerInstanceBindGroupLayout, this.mainPerMaterialBindGroupLayout],
|
|
356
|
+
});
|
|
357
|
+
this.perFrameBindGroup = this.device.createBindGroup({
|
|
358
|
+
label: "main per-frame bind group",
|
|
359
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
360
|
+
entries: [
|
|
361
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
362
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
363
|
+
{ binding: 2, resource: this.materialSampler },
|
|
364
|
+
],
|
|
367
365
|
});
|
|
368
366
|
this.modelPipeline = this.createRenderPipeline({
|
|
369
367
|
label: "model pipeline",
|
|
@@ -378,70 +376,6 @@ export class Engine {
|
|
|
378
376
|
depthCompare: "less-equal",
|
|
379
377
|
},
|
|
380
378
|
});
|
|
381
|
-
this.groundBindGroupLayout = this.device.createBindGroupLayout({
|
|
382
|
-
label: "ground bind group layout",
|
|
383
|
-
entries: [
|
|
384
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
385
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
386
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
387
|
-
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
388
|
-
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
389
|
-
],
|
|
390
|
-
});
|
|
391
|
-
const groundPipelineLayout = this.device.createPipelineLayout({
|
|
392
|
-
label: "ground pipeline layout",
|
|
393
|
-
bindGroupLayouts: [this.groundBindGroupLayout],
|
|
394
|
-
});
|
|
395
|
-
const groundShaderModule = this.device.createShaderModule({
|
|
396
|
-
label: "ground shaders",
|
|
397
|
-
code: /* wgsl */ `
|
|
398
|
-
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
399
|
-
struct Light { direction: vec4f, color: vec4f, };
|
|
400
|
-
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
401
|
-
struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
|
|
402
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
403
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
404
|
-
@group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
|
|
405
|
-
@group(0) @binding(3) var reflectionSampler: sampler;
|
|
406
|
-
@group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
|
|
407
|
-
struct VertexOutput {
|
|
408
|
-
@builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
|
|
409
|
-
};
|
|
410
|
-
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
|
|
411
|
-
var o: VertexOutput;
|
|
412
|
-
o.worldPos = position;
|
|
413
|
-
o.position = camera.projection * camera.view * vec4f(position, 1.0);
|
|
414
|
-
o.normal = normal; o.uv = uv; return o;
|
|
415
|
-
}
|
|
416
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
417
|
-
let n = normalize(input.normal);
|
|
418
|
-
let centerDist = length(input.worldPos.xz);
|
|
419
|
-
let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
|
|
420
|
-
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
421
|
-
let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
|
|
422
|
-
let ndcPos = clipPos.xyz / clipPos.w;
|
|
423
|
-
let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
|
|
424
|
-
let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
|
|
425
|
-
let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
|
|
426
|
-
let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
|
|
427
|
-
let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
|
|
428
|
-
let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
|
|
429
|
-
var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
|
|
430
|
-
let l = -light.lights[0].direction.xyz;
|
|
431
|
-
let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
|
|
432
|
-
return vec4f(finalColor * lightAccum, edgeFade);
|
|
433
|
-
}
|
|
434
|
-
`,
|
|
435
|
-
});
|
|
436
|
-
this.groundPipeline = this.createRenderPipeline({
|
|
437
|
-
label: "ground pipeline",
|
|
438
|
-
layout: groundPipelineLayout,
|
|
439
|
-
shaderModule: groundShaderModule,
|
|
440
|
-
vertexBuffers: fullVertexBuffers,
|
|
441
|
-
fragmentTarget: standardBlend,
|
|
442
|
-
cullMode: "back",
|
|
443
|
-
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
444
|
-
});
|
|
445
379
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
446
380
|
size: 64,
|
|
447
381
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -557,36 +491,30 @@ export class Engine {
|
|
|
557
491
|
cullMode: "back",
|
|
558
492
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
559
493
|
});
|
|
560
|
-
//
|
|
561
|
-
this.
|
|
562
|
-
label: "
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
fragmentTarget: {
|
|
567
|
-
format: this.presentationFormat,
|
|
568
|
-
blend: standardBlend.blend,
|
|
569
|
-
},
|
|
570
|
-
multisample: { count: this.sampleCount }, // Use same multisampling as main render
|
|
571
|
-
cullMode: "none",
|
|
572
|
-
depthStencil: {
|
|
573
|
-
format: "depth24plus-stencil8",
|
|
574
|
-
depthWriteEnabled: true,
|
|
575
|
-
depthCompare: "less-equal",
|
|
576
|
-
},
|
|
494
|
+
// Outline: group 0 = per-frame (camera), group 1 = per-instance (skinMats), group 2 = per-material (edge uniforms)
|
|
495
|
+
this.outlinePerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
496
|
+
label: "outline per-frame bind group layout",
|
|
497
|
+
entries: [
|
|
498
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
499
|
+
],
|
|
577
500
|
});
|
|
578
|
-
//
|
|
579
|
-
this.
|
|
580
|
-
label: "outline bind group layout",
|
|
501
|
+
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
502
|
+
this.outlinePerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
503
|
+
label: "outline per-material bind group layout",
|
|
581
504
|
entries: [
|
|
582
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
583
|
-
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
584
|
-
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
505
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
585
506
|
],
|
|
586
507
|
});
|
|
587
508
|
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
588
509
|
label: "outline pipeline layout",
|
|
589
|
-
bindGroupLayouts: [this.
|
|
510
|
+
bindGroupLayouts: [this.outlinePerFrameBindGroupLayout, this.mainPerInstanceBindGroupLayout, this.outlinePerMaterialBindGroupLayout],
|
|
511
|
+
});
|
|
512
|
+
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
513
|
+
label: "outline per-frame bind group",
|
|
514
|
+
layout: this.outlinePerFrameBindGroupLayout,
|
|
515
|
+
entries: [
|
|
516
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
517
|
+
],
|
|
590
518
|
});
|
|
591
519
|
const outlineShaderModule = this.device.createShaderModule({
|
|
592
520
|
label: "outline shaders",
|
|
@@ -601,14 +529,17 @@ export class Engine {
|
|
|
601
529
|
struct MaterialUniforms {
|
|
602
530
|
edgeColor: vec4f,
|
|
603
531
|
edgeSize: f32,
|
|
604
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
605
532
|
_padding1: f32,
|
|
606
533
|
_padding2: f32,
|
|
534
|
+
_padding3: f32,
|
|
607
535
|
};
|
|
608
536
|
|
|
537
|
+
// group 0: per-frame
|
|
609
538
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
610
|
-
|
|
611
|
-
@group(
|
|
539
|
+
// group 1: per-instance
|
|
540
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
541
|
+
// group 2: per-material
|
|
542
|
+
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
|
|
612
543
|
|
|
613
544
|
struct VertexOutput {
|
|
614
545
|
@builtin(position) position: vec4f,
|
|
@@ -623,7 +554,6 @@ export class Engine {
|
|
|
623
554
|
var output: VertexOutput;
|
|
624
555
|
let pos4 = vec4f(position, 1.0);
|
|
625
556
|
|
|
626
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
627
557
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
628
558
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
629
559
|
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
@@ -641,7 +571,6 @@ export class Engine {
|
|
|
641
571
|
let worldPos = skinnedPos.xyz;
|
|
642
572
|
let worldNormal = normalize(skinnedNrm);
|
|
643
573
|
|
|
644
|
-
// MMD invert hull: expand vertices outward along normals
|
|
645
574
|
let scaleFactor = 0.01;
|
|
646
575
|
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
647
576
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
@@ -649,13 +578,7 @@ export class Engine {
|
|
|
649
578
|
}
|
|
650
579
|
|
|
651
580
|
@fragment fn fs() -> @location(0) vec4f {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (material.isOverEyes > 0.5) {
|
|
655
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return color;
|
|
581
|
+
return material.edgeColor;
|
|
659
582
|
}
|
|
660
583
|
`,
|
|
661
584
|
});
|
|
@@ -672,55 +595,9 @@ export class Engine {
|
|
|
672
595
|
depthCompare: "less-equal",
|
|
673
596
|
},
|
|
674
597
|
});
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
label: "
|
|
678
|
-
layout: outlinePipelineLayout,
|
|
679
|
-
shaderModule: outlineShaderModule,
|
|
680
|
-
vertexBuffers: outlineVertexBuffers,
|
|
681
|
-
fragmentTarget: standardBlend,
|
|
682
|
-
cullMode: "back",
|
|
683
|
-
depthStencil: {
|
|
684
|
-
format: "depth24plus-stencil8",
|
|
685
|
-
depthWriteEnabled: false,
|
|
686
|
-
depthCompare: "less-equal",
|
|
687
|
-
depthBias: -0.0001,
|
|
688
|
-
depthBiasSlopeScale: 0.0,
|
|
689
|
-
depthBiasClamp: 0.0,
|
|
690
|
-
},
|
|
691
|
-
});
|
|
692
|
-
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
693
|
-
this.eyePipeline = this.createRenderPipeline({
|
|
694
|
-
label: "eye overlay pipeline",
|
|
695
|
-
layout: mainPipelineLayout,
|
|
696
|
-
shaderModule,
|
|
697
|
-
vertexBuffers: fullVertexBuffers,
|
|
698
|
-
fragmentTarget: standardBlend,
|
|
699
|
-
cullMode: "front",
|
|
700
|
-
depthStencil: {
|
|
701
|
-
format: "depth24plus-stencil8",
|
|
702
|
-
depthWriteEnabled: true,
|
|
703
|
-
depthCompare: "less-equal",
|
|
704
|
-
depthBias: -0.00005,
|
|
705
|
-
depthBiasSlopeScale: 0.0,
|
|
706
|
-
depthBiasClamp: 0.0,
|
|
707
|
-
stencilFront: {
|
|
708
|
-
compare: "always",
|
|
709
|
-
failOp: "keep",
|
|
710
|
-
depthFailOp: "keep",
|
|
711
|
-
passOp: "replace",
|
|
712
|
-
},
|
|
713
|
-
stencilBack: {
|
|
714
|
-
compare: "always",
|
|
715
|
-
failOp: "keep",
|
|
716
|
-
depthFailOp: "keep",
|
|
717
|
-
passOp: "replace",
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
});
|
|
721
|
-
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
722
|
-
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
723
|
-
label: "depth only shader",
|
|
598
|
+
// GPU picking: encode (modelIndex, materialIndex) as color
|
|
599
|
+
const pickShaderModule = this.device.createShaderModule({
|
|
600
|
+
label: "pick shader",
|
|
724
601
|
code: /* wgsl */ `
|
|
725
602
|
struct CameraUniforms {
|
|
726
603
|
view: mat4x4f,
|
|
@@ -728,91 +605,87 @@ export class Engine {
|
|
|
728
605
|
viewPos: vec3f,
|
|
729
606
|
_padding: f32,
|
|
730
607
|
};
|
|
608
|
+
struct PickId {
|
|
609
|
+
modelId: f32,
|
|
610
|
+
materialId: f32,
|
|
611
|
+
_p1: f32,
|
|
612
|
+
_p2: f32,
|
|
613
|
+
};
|
|
731
614
|
|
|
732
615
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
733
|
-
@group(
|
|
616
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
617
|
+
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
734
618
|
|
|
735
619
|
@vertex fn vs(
|
|
736
620
|
@location(0) position: vec3f,
|
|
737
621
|
@location(1) normal: vec3f,
|
|
622
|
+
@location(2) uv: vec2f,
|
|
738
623
|
@location(3) joints0: vec4<u32>,
|
|
739
624
|
@location(4) weights0: vec4<f32>
|
|
740
625
|
) -> @builtin(position) vec4f {
|
|
741
626
|
let pos4 = vec4f(position, 1.0);
|
|
742
|
-
|
|
743
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
744
627
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
745
628
|
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
746
|
-
let
|
|
747
|
-
|
|
748
|
-
var
|
|
749
|
-
|
|
750
|
-
let j = joints0[i];
|
|
751
|
-
let w = normalizedWeights[i];
|
|
752
|
-
let m = skinMats[j];
|
|
753
|
-
skinnedPos += (m * pos4) * w;
|
|
754
|
-
}
|
|
755
|
-
let worldPos = skinnedPos.xyz;
|
|
756
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
757
|
-
return clipPos;
|
|
629
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
630
|
+
var sp = vec4f(0.0);
|
|
631
|
+
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
632
|
+
return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
758
633
|
}
|
|
759
634
|
|
|
760
635
|
@fragment fn fs() -> @location(0) vec4f {
|
|
761
|
-
return vec4f(
|
|
636
|
+
return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
|
|
762
637
|
}
|
|
763
638
|
`,
|
|
764
639
|
});
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
640
|
+
this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
641
|
+
label: "pick per-frame layout",
|
|
642
|
+
entries: [
|
|
643
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
644
|
+
],
|
|
645
|
+
});
|
|
646
|
+
this.pickPerInstanceBindGroupLayout = this.device.createBindGroupLayout({
|
|
647
|
+
label: "pick per-instance layout",
|
|
648
|
+
entries: [
|
|
649
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
650
|
+
],
|
|
651
|
+
});
|
|
652
|
+
this.pickPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
|
|
653
|
+
label: "pick per-material layout",
|
|
654
|
+
entries: [
|
|
655
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
656
|
+
],
|
|
657
|
+
});
|
|
658
|
+
const pickPipelineLayout = this.device.createPipelineLayout({
|
|
659
|
+
label: "pick pipeline layout",
|
|
660
|
+
bindGroupLayouts: [this.pickPerFrameBindGroupLayout, this.pickPerInstanceBindGroupLayout, this.pickPerMaterialBindGroupLayout],
|
|
661
|
+
});
|
|
662
|
+
this.pickPerFrameBindGroup = this.device.createBindGroup({
|
|
663
|
+
label: "pick per-frame bind group",
|
|
664
|
+
layout: this.pickPerFrameBindGroupLayout,
|
|
665
|
+
entries: [
|
|
666
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
667
|
+
],
|
|
668
|
+
});
|
|
669
|
+
this.pickPipeline = this.device.createRenderPipeline({
|
|
670
|
+
label: "pick pipeline",
|
|
671
|
+
layout: pickPipelineLayout,
|
|
672
|
+
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
673
|
+
fragment: {
|
|
674
|
+
module: pickShaderModule,
|
|
675
|
+
targets: [{ format: "rgba8unorm" }],
|
|
774
676
|
},
|
|
775
|
-
|
|
776
|
-
cullMode: "none",
|
|
677
|
+
primitive: { cullMode: "none" },
|
|
777
678
|
depthStencil: {
|
|
778
|
-
format: "depth24plus
|
|
679
|
+
format: "depth24plus",
|
|
779
680
|
depthWriteEnabled: true,
|
|
780
681
|
depthCompare: "less-equal",
|
|
781
|
-
depthBias: 0.0,
|
|
782
|
-
depthBiasSlopeScale: 0.0,
|
|
783
|
-
depthBiasClamp: 0.0,
|
|
784
682
|
},
|
|
785
683
|
});
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
shaderModule,
|
|
792
|
-
vertexBuffers: fullVertexBuffers,
|
|
793
|
-
fragmentTarget: standardBlend,
|
|
794
|
-
cullMode: "none",
|
|
795
|
-
depthStencil: {
|
|
796
|
-
format: "depth24plus-stencil8",
|
|
797
|
-
depthWriteEnabled: false,
|
|
798
|
-
depthCompare: "less-equal",
|
|
799
|
-
stencilFront: {
|
|
800
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
801
|
-
failOp: "keep",
|
|
802
|
-
depthFailOp: "keep",
|
|
803
|
-
passOp: "keep",
|
|
804
|
-
},
|
|
805
|
-
stencilBack: {
|
|
806
|
-
compare: isOverEyes ? "equal" : "not-equal",
|
|
807
|
-
failOp: "keep",
|
|
808
|
-
depthFailOp: "keep",
|
|
809
|
-
passOp: "keep",
|
|
810
|
-
},
|
|
811
|
-
},
|
|
812
|
-
});
|
|
813
|
-
};
|
|
814
|
-
this.hairPipelineOverEyes = createHairPipeline(true);
|
|
815
|
-
this.hairPipelineOverNonEyes = createHairPipeline(false);
|
|
684
|
+
this.pickReadbackBuffer = this.device.createBuffer({
|
|
685
|
+
label: "pick readback",
|
|
686
|
+
size: 256,
|
|
687
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
688
|
+
});
|
|
816
689
|
}
|
|
817
690
|
// Step 3: Setup canvas resize handling
|
|
818
691
|
setupResize() {
|
|
@@ -837,33 +710,25 @@ export class Engine {
|
|
|
837
710
|
this.multisampleTexture = this.device.createTexture({
|
|
838
711
|
label: "multisample render target",
|
|
839
712
|
size: [width, height],
|
|
840
|
-
sampleCount:
|
|
713
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
841
714
|
format: this.presentationFormat,
|
|
842
715
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
843
716
|
});
|
|
844
717
|
this.depthTexture = this.device.createTexture({
|
|
845
718
|
label: "depth texture",
|
|
846
719
|
size: [width, height],
|
|
847
|
-
sampleCount:
|
|
720
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
848
721
|
format: "depth24plus-stencil8",
|
|
849
722
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
850
723
|
});
|
|
851
724
|
const depthTextureView = this.depthTexture.createView();
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
storeOp: "store",
|
|
860
|
-
}
|
|
861
|
-
: {
|
|
862
|
-
view: this.context.getCurrentTexture().createView(),
|
|
863
|
-
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
864
|
-
loadOp: "clear",
|
|
865
|
-
storeOp: "store",
|
|
866
|
-
};
|
|
725
|
+
const colorAttachment = {
|
|
726
|
+
view: this.multisampleTexture.createView(),
|
|
727
|
+
resolveTarget: this.context.getCurrentTexture().createView(),
|
|
728
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
729
|
+
loadOp: "clear",
|
|
730
|
+
storeOp: "store",
|
|
731
|
+
};
|
|
867
732
|
this.renderPassDescriptor = {
|
|
868
733
|
label: "renderPass",
|
|
869
734
|
colorAttachments: [colorAttachment],
|
|
@@ -874,10 +739,24 @@ export class Engine {
|
|
|
874
739
|
depthStoreOp: "store",
|
|
875
740
|
stencilClearValue: 0,
|
|
876
741
|
stencilLoadOp: "clear",
|
|
877
|
-
stencilStoreOp: "discard",
|
|
742
|
+
stencilStoreOp: "discard",
|
|
878
743
|
},
|
|
879
744
|
};
|
|
880
745
|
this.camera.aspect = width / height;
|
|
746
|
+
if (this.onRaycast) {
|
|
747
|
+
this.pickTexture = this.device.createTexture({
|
|
748
|
+
label: "pick render target",
|
|
749
|
+
size: [width, height],
|
|
750
|
+
format: "rgba8unorm",
|
|
751
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
752
|
+
});
|
|
753
|
+
this.pickDepthTexture = this.device.createTexture({
|
|
754
|
+
label: "pick depth",
|
|
755
|
+
size: [width, height],
|
|
756
|
+
format: "depth24plus",
|
|
757
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
881
760
|
}
|
|
882
761
|
}
|
|
883
762
|
// Step 4: Create camera and uniform buffer
|
|
@@ -891,6 +770,42 @@ export class Engine {
|
|
|
891
770
|
this.camera.aspect = this.canvas.width / this.canvas.height;
|
|
892
771
|
this.camera.attachControl(this.canvas);
|
|
893
772
|
}
|
|
773
|
+
setCameraTarget(modelOrVec, boneName, offset) {
|
|
774
|
+
if (modelOrVec === null) {
|
|
775
|
+
this.cameraTargetModel = null;
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if ("x" in modelOrVec && "y" in modelOrVec && "z" in modelOrVec) {
|
|
779
|
+
this.cameraTargetModel = null;
|
|
780
|
+
this.camera.target.x = modelOrVec.x;
|
|
781
|
+
this.camera.target.y = modelOrVec.y;
|
|
782
|
+
this.camera.target.z = modelOrVec.z;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.cameraTargetModel = modelOrVec;
|
|
786
|
+
this.cameraTargetBoneName = boneName ?? "";
|
|
787
|
+
this.cameraTargetOffset.x = offset?.x ?? 0;
|
|
788
|
+
this.cameraTargetOffset.y = offset?.y ?? 0;
|
|
789
|
+
this.cameraTargetOffset.z = offset?.z ?? 0;
|
|
790
|
+
}
|
|
791
|
+
/** Souls-style follow cam: orbit center tracks a model bone each frame. Shorthand for setCameraTarget(model, boneName, offset). */
|
|
792
|
+
setCameraFollow(model, boneName, offset) {
|
|
793
|
+
if (model === null) {
|
|
794
|
+
this.cameraTargetModel = null;
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
this.cameraTargetModel = model;
|
|
798
|
+
this.cameraTargetBoneName = boneName ?? "全ての親";
|
|
799
|
+
this.cameraTargetOffset.x = offset?.x ?? 0;
|
|
800
|
+
this.cameraTargetOffset.y = offset?.y ?? 0;
|
|
801
|
+
this.cameraTargetOffset.z = offset?.z ?? 0;
|
|
802
|
+
}
|
|
803
|
+
getCameraDistance() { return this.camera.radius; }
|
|
804
|
+
setCameraDistance(d) { this.camera.radius = d; }
|
|
805
|
+
getCameraAlpha() { return this.camera.alpha; }
|
|
806
|
+
setCameraAlpha(a) { this.camera.alpha = a; }
|
|
807
|
+
getCameraBeta() { return this.camera.beta; }
|
|
808
|
+
setCameraBeta(b) { this.camera.beta = b; }
|
|
894
809
|
// Step 5: Create lighting buffers
|
|
895
810
|
setupLighting() {
|
|
896
811
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -943,30 +858,20 @@ export class Engine {
|
|
|
943
858
|
width: 100,
|
|
944
859
|
height: 100,
|
|
945
860
|
diffuseColor: new Vec3(1, 1, 1),
|
|
946
|
-
reflectionLevel: 0.5,
|
|
947
|
-
reflectionTextureSize: 1024,
|
|
948
861
|
fadeStart: 5.0,
|
|
949
862
|
fadeEnd: 60.0,
|
|
950
|
-
mode: "reflection",
|
|
951
863
|
shadowMapSize: 4096,
|
|
952
864
|
shadowStrength: 1.0,
|
|
953
865
|
...options,
|
|
954
866
|
};
|
|
955
|
-
this.groundMode = opts.mode;
|
|
956
867
|
this.createGroundGeometry(opts.width, opts.height);
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
this.createReflectionTexture(opts.reflectionTextureSize);
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength);
|
|
963
|
-
}
|
|
964
|
-
this.groundHasReflections = true;
|
|
868
|
+
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength);
|
|
869
|
+
this.hasGround = true;
|
|
965
870
|
this.groundDrawCall = {
|
|
966
871
|
type: "ground",
|
|
967
872
|
count: 6,
|
|
968
873
|
firstIndex: 0,
|
|
969
|
-
bindGroup:
|
|
874
|
+
bindGroup: this.groundShadowBindGroup,
|
|
970
875
|
materialName: "Ground",
|
|
971
876
|
};
|
|
972
877
|
}
|
|
@@ -1011,6 +916,14 @@ export class Engine {
|
|
|
1011
916
|
this.resizeObserver = null;
|
|
1012
917
|
}
|
|
1013
918
|
}
|
|
919
|
+
async loadModel(nameOrPath, path) {
|
|
920
|
+
const pmxPath = path === undefined ? nameOrPath : path;
|
|
921
|
+
const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath;
|
|
922
|
+
const model = await PmxLoader.load(pmxPath);
|
|
923
|
+
model.setName(name);
|
|
924
|
+
await this.addModel(model, pmxPath, name);
|
|
925
|
+
return model;
|
|
926
|
+
}
|
|
1014
927
|
async addModel(model, pmxPath, name) {
|
|
1015
928
|
const requested = name ?? model.name;
|
|
1016
929
|
let key = requested;
|
|
@@ -1024,9 +937,6 @@ export class Engine {
|
|
|
1024
937
|
await this.setupModelInstance(key, model, basePath);
|
|
1025
938
|
return key;
|
|
1026
939
|
}
|
|
1027
|
-
async registerModel(model, pmxPath) {
|
|
1028
|
-
return this.addModel(model, pmxPath);
|
|
1029
|
-
}
|
|
1030
940
|
removeModel(name) {
|
|
1031
941
|
this.modelInstances.delete(name);
|
|
1032
942
|
}
|
|
@@ -1074,11 +984,17 @@ export class Engine {
|
|
|
1074
984
|
const inst = this.modelInstances.get(modelName);
|
|
1075
985
|
return inst ? !inst.hiddenMaterials.has(materialName) : false;
|
|
1076
986
|
}
|
|
1077
|
-
|
|
1078
|
-
this.
|
|
987
|
+
setIKEnabled(enabled) {
|
|
988
|
+
this.ikEnabled = enabled;
|
|
989
|
+
}
|
|
990
|
+
getIKEnabled() {
|
|
991
|
+
return this.ikEnabled;
|
|
992
|
+
}
|
|
993
|
+
setPhysicsEnabled(enabled) {
|
|
994
|
+
this.physicsEnabled = enabled;
|
|
1079
995
|
}
|
|
1080
|
-
|
|
1081
|
-
this.
|
|
996
|
+
getPhysicsEnabled() {
|
|
997
|
+
return this.physicsEnabled;
|
|
1082
998
|
}
|
|
1083
999
|
resetPhysics() {
|
|
1084
1000
|
this.forEachInstance((inst) => {
|
|
@@ -1088,19 +1004,16 @@ export class Engine {
|
|
|
1088
1004
|
inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
|
|
1089
1005
|
});
|
|
1090
1006
|
}
|
|
1091
|
-
instances() {
|
|
1092
|
-
return this.modelInstances.values();
|
|
1093
|
-
}
|
|
1094
1007
|
forEachInstance(fn) {
|
|
1095
|
-
for (const inst of this.
|
|
1008
|
+
for (const inst of this.modelInstances.values())
|
|
1096
1009
|
fn(inst);
|
|
1097
1010
|
}
|
|
1098
1011
|
updateInstances(deltaTime) {
|
|
1099
1012
|
this.forEachInstance((inst) => {
|
|
1100
|
-
const verticesChanged = inst.model.update(deltaTime);
|
|
1013
|
+
const verticesChanged = inst.model.update(deltaTime, this.ikEnabled);
|
|
1101
1014
|
if (verticesChanged)
|
|
1102
1015
|
inst.vertexBufferNeedsUpdate = true;
|
|
1103
|
-
if (inst.physics &&
|
|
1016
|
+
if (inst.physics && this.physicsEnabled) {
|
|
1104
1017
|
inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
|
|
1105
1018
|
}
|
|
1106
1019
|
if (inst.vertexBufferNeedsUpdate)
|
|
@@ -1162,6 +1075,20 @@ export class Engine {
|
|
|
1162
1075
|
{ binding: 1, resource: { buffer: skinMatrixBuffer } },
|
|
1163
1076
|
],
|
|
1164
1077
|
});
|
|
1078
|
+
const mainPerInstanceBindGroup = this.device.createBindGroup({
|
|
1079
|
+
label: `${name}: main per-instance bind group`,
|
|
1080
|
+
layout: this.mainPerInstanceBindGroupLayout,
|
|
1081
|
+
entries: [
|
|
1082
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1083
|
+
],
|
|
1084
|
+
});
|
|
1085
|
+
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
1086
|
+
label: `${name}: pick per-instance bind group`,
|
|
1087
|
+
layout: this.pickPerInstanceBindGroupLayout,
|
|
1088
|
+
entries: [
|
|
1089
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1090
|
+
],
|
|
1091
|
+
});
|
|
1165
1092
|
const inst = {
|
|
1166
1093
|
name,
|
|
1167
1094
|
model,
|
|
@@ -1174,6 +1101,9 @@ export class Engine {
|
|
|
1174
1101
|
drawCalls: [],
|
|
1175
1102
|
shadowDrawCalls: [],
|
|
1176
1103
|
shadowBindGroup,
|
|
1104
|
+
mainPerInstanceBindGroup,
|
|
1105
|
+
pickPerInstanceBindGroup,
|
|
1106
|
+
pickDrawCalls: [],
|
|
1177
1107
|
hiddenMaterials: new Set(),
|
|
1178
1108
|
physics,
|
|
1179
1109
|
vertexBufferNeedsUpdate: false,
|
|
@@ -1245,56 +1175,6 @@ export class Engine {
|
|
|
1245
1175
|
});
|
|
1246
1176
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
1247
1177
|
}
|
|
1248
|
-
createGroundMaterialBuffer(diffuseColor, reflectionLevel, fadeStart, fadeEnd) {
|
|
1249
|
-
const u = new Float32Array(8);
|
|
1250
|
-
u[0] = diffuseColor.x;
|
|
1251
|
-
u[1] = diffuseColor.y;
|
|
1252
|
-
u[2] = diffuseColor.z;
|
|
1253
|
-
u[3] = reflectionLevel;
|
|
1254
|
-
u[4] = fadeStart;
|
|
1255
|
-
u[5] = fadeEnd;
|
|
1256
|
-
u[6] = 0;
|
|
1257
|
-
u[7] = 0;
|
|
1258
|
-
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1259
|
-
label: "ground material uniform buffer",
|
|
1260
|
-
size: 64,
|
|
1261
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1262
|
-
});
|
|
1263
|
-
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u);
|
|
1264
|
-
}
|
|
1265
|
-
createReflectionTexture(size) {
|
|
1266
|
-
this.groundReflectionTexture = this.device.createTexture({
|
|
1267
|
-
label: "ground reflection texture",
|
|
1268
|
-
size: [size, size],
|
|
1269
|
-
sampleCount: this.sampleCount,
|
|
1270
|
-
format: this.presentationFormat,
|
|
1271
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1272
|
-
});
|
|
1273
|
-
this.groundReflectionResolveTexture = this.device.createTexture({
|
|
1274
|
-
label: "ground reflection resolve texture",
|
|
1275
|
-
size: [size, size],
|
|
1276
|
-
format: this.presentationFormat,
|
|
1277
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1278
|
-
});
|
|
1279
|
-
this.groundReflectionDepthTexture = this.device.createTexture({
|
|
1280
|
-
label: "ground reflection depth texture",
|
|
1281
|
-
size: [size, size],
|
|
1282
|
-
sampleCount: this.sampleCount,
|
|
1283
|
-
format: "depth24plus-stencil8",
|
|
1284
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1285
|
-
});
|
|
1286
|
-
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1287
|
-
label: "ground reflection bind group",
|
|
1288
|
-
layout: this.groundBindGroupLayout,
|
|
1289
|
-
entries: [
|
|
1290
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1291
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1292
|
-
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1293
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1294
|
-
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer } },
|
|
1295
|
-
],
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
1178
|
createShadowGroundResources(shadowMapSize, diffuseColor, fadeStart, fadeEnd, shadowStrength) {
|
|
1299
1179
|
this.shadowMapTexture = this.device.createTexture({
|
|
1300
1180
|
label: "shadow map",
|
|
@@ -1359,6 +1239,8 @@ export class Engine {
|
|
|
1359
1239
|
throw new Error("Model has no materials");
|
|
1360
1240
|
const textures = model.getTextures();
|
|
1361
1241
|
const prefix = `${inst.name}: `;
|
|
1242
|
+
// 1-based so that (0,0) = clear color = "no hit"
|
|
1243
|
+
const modelId = this.modelInstances.size + 1;
|
|
1362
1244
|
const loadTextureByIndex = async (texIndex) => {
|
|
1363
1245
|
if (texIndex < 0 || texIndex >= textures.length)
|
|
1364
1246
|
return null;
|
|
@@ -1366,70 +1248,29 @@ export class Engine {
|
|
|
1366
1248
|
return this.createTextureFromPath(path);
|
|
1367
1249
|
};
|
|
1368
1250
|
let currentIndexOffset = 0;
|
|
1251
|
+
let materialId = 0;
|
|
1369
1252
|
for (const mat of materials) {
|
|
1370
1253
|
const indexCount = mat.vertexCount;
|
|
1371
1254
|
if (indexCount === 0)
|
|
1372
1255
|
continue;
|
|
1256
|
+
materialId++;
|
|
1373
1257
|
const diffuseTexture = await loadTextureByIndex(mat.diffuseTextureIndex);
|
|
1374
1258
|
if (!diffuseTexture)
|
|
1375
1259
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1376
1260
|
const materialAlpha = mat.diffuse[3];
|
|
1377
1261
|
const isTransparent = materialAlpha < 1.0 - 0.001;
|
|
1378
|
-
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha,
|
|
1262
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1263
|
+
const textureView = diffuseTexture.createView();
|
|
1379
1264
|
const bindGroup = this.device.createBindGroup({
|
|
1380
1265
|
label: `${prefix}material: ${mat.name}`,
|
|
1381
|
-
layout: this.
|
|
1266
|
+
layout: this.mainPerMaterialBindGroupLayout,
|
|
1382
1267
|
entries: [
|
|
1383
|
-
{ binding: 0, resource:
|
|
1384
|
-
{ binding: 1, resource: { buffer:
|
|
1385
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1386
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1387
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1388
|
-
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1268
|
+
{ binding: 0, resource: textureView },
|
|
1269
|
+
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
1389
1270
|
],
|
|
1390
1271
|
});
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
inst.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1394
|
-
}
|
|
1395
|
-
else if (mat.isHair) {
|
|
1396
|
-
const createHairBindGroup = (isOverEyes) => {
|
|
1397
|
-
const buf = this.createMaterialUniformBuffer(`${prefix}${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`, materialAlpha, isOverEyes ? 1.0 : 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1398
|
-
return this.device.createBindGroup({
|
|
1399
|
-
label: `${prefix}hair ${isOverEyes ? "over eyes" : "over non-eyes"}: ${mat.name}`,
|
|
1400
|
-
layout: this.mainBindGroupLayout,
|
|
1401
|
-
entries: [
|
|
1402
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1403
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1404
|
-
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1405
|
-
{ binding: 3, resource: this.materialSampler },
|
|
1406
|
-
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1407
|
-
{ binding: 5, resource: { buffer: buf } },
|
|
1408
|
-
],
|
|
1409
|
-
});
|
|
1410
|
-
};
|
|
1411
|
-
inst.drawCalls.push({
|
|
1412
|
-
type: "hair-over-eyes",
|
|
1413
|
-
count: indexCount,
|
|
1414
|
-
firstIndex: currentIndexOffset,
|
|
1415
|
-
bindGroup: createHairBindGroup(true),
|
|
1416
|
-
materialName: mat.name,
|
|
1417
|
-
});
|
|
1418
|
-
inst.drawCalls.push({
|
|
1419
|
-
type: "hair-over-non-eyes",
|
|
1420
|
-
count: indexCount,
|
|
1421
|
-
firstIndex: currentIndexOffset,
|
|
1422
|
-
bindGroup: createHairBindGroup(false),
|
|
1423
|
-
materialName: mat.name,
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
else if (isTransparent) {
|
|
1427
|
-
inst.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1428
|
-
}
|
|
1429
|
-
else {
|
|
1430
|
-
inst.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1272
|
+
const type = isTransparent ? "transparent" : "opaque";
|
|
1273
|
+
inst.drawCalls.push({ type, count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1433
1274
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1434
1275
|
const materialUniformData = new Float32Array([
|
|
1435
1276
|
mat.edgeColor[0], mat.edgeColor[1], mat.edgeColor[2], mat.edgeColor[3],
|
|
@@ -1438,48 +1279,42 @@ export class Engine {
|
|
|
1438
1279
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData);
|
|
1439
1280
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1440
1281
|
label: `${prefix}outline: ${mat.name}`,
|
|
1441
|
-
layout: this.
|
|
1282
|
+
layout: this.outlinePerMaterialBindGroupLayout,
|
|
1442
1283
|
entries: [
|
|
1443
|
-
{ binding: 0, resource: { buffer:
|
|
1444
|
-
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1445
|
-
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1284
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
1446
1285
|
],
|
|
1447
1286
|
});
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1287
|
+
const outlineType = isTransparent ? "transparent-outline" : "opaque-outline";
|
|
1288
|
+
inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name });
|
|
1289
|
+
}
|
|
1290
|
+
if (this.onRaycast) {
|
|
1291
|
+
const pickIdData = new Float32Array([modelId, materialId, 0, 0]);
|
|
1292
|
+
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData);
|
|
1293
|
+
const pickBindGroup = this.device.createBindGroup({
|
|
1294
|
+
label: `${prefix}pick: ${mat.name}`,
|
|
1295
|
+
layout: this.pickPerMaterialBindGroupLayout,
|
|
1296
|
+
entries: [{ binding: 0, resource: { buffer: pickIdBuffer } }],
|
|
1297
|
+
});
|
|
1298
|
+
inst.pickDrawCalls.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: pickBindGroup });
|
|
1452
1299
|
}
|
|
1453
1300
|
currentIndexOffset += indexCount;
|
|
1454
1301
|
}
|
|
1455
1302
|
for (const d of inst.drawCalls) {
|
|
1456
|
-
if (d.type === "opaque"
|
|
1303
|
+
if (d.type === "opaque")
|
|
1457
1304
|
inst.shadowDrawCalls.push(d);
|
|
1458
1305
|
}
|
|
1459
1306
|
}
|
|
1460
|
-
createMaterialUniformBuffer(label, alpha,
|
|
1307
|
+
createMaterialUniformBuffer(label, alpha, diffuseColor, ambientColor, specularColor, shininess) {
|
|
1461
1308
|
const data = new Float32Array(20);
|
|
1462
1309
|
data.set([
|
|
1463
1310
|
alpha,
|
|
1464
|
-
1.0,
|
|
1465
1311
|
this.rimLightIntensity,
|
|
1466
|
-
shininess,
|
|
1467
|
-
|
|
1468
|
-
1.0,
|
|
1469
|
-
1.0,
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
diffuseColor[1],
|
|
1473
|
-
diffuseColor[2],
|
|
1474
|
-
0.0, // diffuseColor (vec3), _padding2
|
|
1475
|
-
ambientColor[0],
|
|
1476
|
-
ambientColor[1],
|
|
1477
|
-
ambientColor[2],
|
|
1478
|
-
0.0, // ambientColor (vec3), _padding3
|
|
1479
|
-
specularColor[0],
|
|
1480
|
-
specularColor[1],
|
|
1481
|
-
specularColor[2],
|
|
1482
|
-
0.0, // specularColor (vec3), _padding4
|
|
1312
|
+
shininess,
|
|
1313
|
+
0.0,
|
|
1314
|
+
1.0, 1.0, 1.0, 0.0, // rimColor (vec3), _padding2
|
|
1315
|
+
diffuseColor[0], diffuseColor[1], diffuseColor[2], 0.0,
|
|
1316
|
+
ambientColor[0], ambientColor[1], ambientColor[2], 0.0,
|
|
1317
|
+
specularColor[0], specularColor[1], specularColor[2], 0.0,
|
|
1483
1318
|
]);
|
|
1484
1319
|
return this.createUniformBuffer(`material uniform: ${label}`, data);
|
|
1485
1320
|
}
|
|
@@ -1526,224 +1361,100 @@ export class Engine {
|
|
|
1526
1361
|
return null;
|
|
1527
1362
|
}
|
|
1528
1363
|
}
|
|
1529
|
-
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1530
|
-
renderEyes(pass, inst, useReflectionPipeline = false) {
|
|
1531
|
-
if (useReflectionPipeline) {
|
|
1532
|
-
pass.setPipeline(this.reflectionPipeline);
|
|
1533
|
-
for (const draw of inst.drawCalls) {
|
|
1534
|
-
if (draw.type === "eye") {
|
|
1535
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1536
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
else {
|
|
1541
|
-
pass.setPipeline(this.eyePipeline);
|
|
1542
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1543
|
-
for (const draw of inst.drawCalls) {
|
|
1544
|
-
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1545
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1546
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
1364
|
renderGround(pass) {
|
|
1552
|
-
if (!this.
|
|
1365
|
+
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
1553
1366
|
return;
|
|
1554
|
-
|
|
1555
|
-
this.renderReflectionTexture();
|
|
1556
|
-
pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline);
|
|
1367
|
+
pass.setPipeline(this.groundShadowPipeline);
|
|
1557
1368
|
pass.setVertexBuffer(0, this.groundVertexBuffer);
|
|
1558
1369
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
|
|
1559
1370
|
pass.setBindGroup(0, this.groundDrawCall.bindGroup);
|
|
1560
1371
|
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
|
|
1561
1372
|
}
|
|
1562
|
-
|
|
1563
|
-
if (!this.
|
|
1373
|
+
performRaycast(screenX, screenY) {
|
|
1374
|
+
if (!this.onRaycast || this.modelInstances.size === 0) {
|
|
1375
|
+
this.onRaycast?.("", null, screenX, screenY);
|
|
1564
1376
|
return;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1377
|
+
}
|
|
1378
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1379
|
+
this.pendingPick = { x: Math.floor(screenX * dpr), y: Math.floor(screenY * dpr) };
|
|
1380
|
+
}
|
|
1381
|
+
renderPickPass(encoder) {
|
|
1382
|
+
if (!this.pendingPick || !this.pickTexture || !this.pickDepthTexture)
|
|
1383
|
+
return;
|
|
1384
|
+
const pass = encoder.beginRenderPass({
|
|
1385
|
+
colorAttachments: [{
|
|
1386
|
+
view: this.pickTexture.createView(),
|
|
1387
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1575
1388
|
loadOp: "clear",
|
|
1576
1389
|
storeOp: "store",
|
|
1577
|
-
},
|
|
1578
|
-
],
|
|
1390
|
+
}],
|
|
1579
1391
|
depthStencilAttachment: {
|
|
1580
|
-
view: this.
|
|
1392
|
+
view: this.pickDepthTexture.createView(),
|
|
1581
1393
|
depthClearValue: 1.0,
|
|
1582
1394
|
depthLoadOp: "clear",
|
|
1583
1395
|
depthStoreOp: "store",
|
|
1584
|
-
stencilClearValue: 0,
|
|
1585
|
-
stencilLoadOp: "clear",
|
|
1586
|
-
stencilStoreOp: "discard",
|
|
1587
1396
|
},
|
|
1588
|
-
};
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
1600
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1601
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
const hasHair = inst.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d));
|
|
1607
|
-
if (hasHair) {
|
|
1608
|
-
pass.setPipeline(this.hairDepthPipeline);
|
|
1609
|
-
for (const draw of inst.drawCalls) {
|
|
1610
|
-
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, draw)) {
|
|
1611
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1612
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
const hairOverEyes = inst.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(inst, d));
|
|
1617
|
-
if (hairOverEyes.length > 0) {
|
|
1618
|
-
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1619
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1620
|
-
for (const draw of hairOverEyes) {
|
|
1621
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1622
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
const hairOverNonEyes = inst.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(inst, d));
|
|
1626
|
-
if (hairOverNonEyes.length > 0) {
|
|
1627
|
-
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1628
|
-
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1629
|
-
for (const draw of hairOverNonEyes) {
|
|
1630
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1631
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
const hairOutlines = inst.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(inst, d));
|
|
1635
|
-
if (hairOutlines.length > 0) {
|
|
1636
|
-
pass.setPipeline(this.hairOutlinePipeline);
|
|
1637
|
-
for (const draw of hairOutlines) {
|
|
1638
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1397
|
+
});
|
|
1398
|
+
pass.setPipeline(this.pickPipeline);
|
|
1399
|
+
pass.setBindGroup(0, this.pickPerFrameBindGroup);
|
|
1400
|
+
this.forEachInstance((inst) => {
|
|
1401
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
1402
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1403
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1404
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1405
|
+
pass.setBindGroup(1, inst.pickPerInstanceBindGroup);
|
|
1406
|
+
for (const draw of inst.pickDrawCalls) {
|
|
1407
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1639
1408
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1640
1409
|
}
|
|
1641
|
-
}
|
|
1410
|
+
});
|
|
1411
|
+
pass.end();
|
|
1412
|
+
// Copy the single pixel under cursor to readback buffer
|
|
1413
|
+
const px = Math.min(this.pendingPick.x, this.pickTexture.width - 1);
|
|
1414
|
+
const py = Math.min(this.pendingPick.y, this.pickTexture.height - 1);
|
|
1415
|
+
encoder.copyTextureToBuffer({ texture: this.pickTexture, origin: { x: Math.max(0, px), y: Math.max(0, py) } }, { buffer: this.pickReadbackBuffer, bytesPerRow: 256 }, { width: 1, height: 1 });
|
|
1642
1416
|
}
|
|
1643
|
-
|
|
1644
|
-
if (!this.onRaycast
|
|
1645
|
-
|
|
1417
|
+
async resolvePickResult(screenX, screenY) {
|
|
1418
|
+
if (!this.onRaycast)
|
|
1419
|
+
return;
|
|
1420
|
+
await this.pickReadbackBuffer.mapAsync(GPUMapMode.READ);
|
|
1421
|
+
const data = new Uint8Array(this.pickReadbackBuffer.getMappedRange());
|
|
1422
|
+
const modelId = data[0];
|
|
1423
|
+
const materialId = data[1];
|
|
1424
|
+
this.pickReadbackBuffer.unmap();
|
|
1425
|
+
if (modelId === 0) {
|
|
1426
|
+
this.onRaycast("", null, screenX, screenY);
|
|
1646
1427
|
return;
|
|
1647
1428
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
const transformPoint = (matrix, point) => {
|
|
1656
|
-
const m = matrix.values;
|
|
1657
|
-
const x = point.x, y = point.y, z = point.z;
|
|
1658
|
-
const result = new Vec3(m[0] * x + m[4] * y + m[8] * z + m[12], m[1] * x + m[5] * y + m[9] * z + m[13], m[2] * x + m[6] * y + m[10] * z + m[14]);
|
|
1659
|
-
const w = m[3] * x + m[7] * y + m[11] * z + m[15];
|
|
1660
|
-
return result.scale(w !== 0 ? 1 / w : 1);
|
|
1661
|
-
};
|
|
1662
|
-
const worldNear = transformPoint(inverseViewProj, new Vec3(clipX, clipY, -1));
|
|
1663
|
-
const worldFar = transformPoint(inverseViewProj, new Vec3(clipX, clipY, 1));
|
|
1664
|
-
const rayOrigin = this.camera.getPosition();
|
|
1665
|
-
const rayDirection = worldFar.subtract(worldNear).normalize();
|
|
1666
|
-
const transformByMatrix = (matrix, offset, point) => {
|
|
1667
|
-
const m = matrix, x = point.x, y = point.y, z = point.z;
|
|
1668
|
-
return new Vec3(m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12], m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13], m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]);
|
|
1669
|
-
};
|
|
1670
|
-
let closest = null;
|
|
1671
|
-
const maxDistance = 1000;
|
|
1672
|
-
this.forEachInstance((inst) => {
|
|
1673
|
-
const model = inst.model;
|
|
1674
|
-
const materials = model.getMaterials();
|
|
1675
|
-
if (materials.length === 0)
|
|
1676
|
-
return;
|
|
1677
|
-
const baseVertices = model.getVertices();
|
|
1678
|
-
const indices = model.getIndices();
|
|
1679
|
-
const skinning = model.getSkinning();
|
|
1680
|
-
if (!baseVertices?.length || !indices || !skinning)
|
|
1681
|
-
return;
|
|
1682
|
-
const vertices = new Float32Array(baseVertices.length);
|
|
1683
|
-
const skinMatrices = model.getSkinMatrices();
|
|
1684
|
-
for (let i = 0; i < baseVertices.length; i += 8) {
|
|
1685
|
-
const vertexIndex = i / 8;
|
|
1686
|
-
const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2]);
|
|
1687
|
-
const j0 = skinning.joints[vertexIndex * 4], j1 = skinning.joints[vertexIndex * 4 + 1], j2 = skinning.joints[vertexIndex * 4 + 2], j3 = skinning.joints[vertexIndex * 4 + 3];
|
|
1688
|
-
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;
|
|
1689
|
-
const ws = w0 + w1 + w2 + w3;
|
|
1690
|
-
const nw = ws > 0.0001 ? [w0 / ws, w1 / ws, w2 / ws, w3 / ws] : [1, 0, 0, 0];
|
|
1691
|
-
let sp = new Vec3(0, 0, 0);
|
|
1692
|
-
for (let j = 0; j < 4; j++) {
|
|
1693
|
-
if (nw[j] <= 0)
|
|
1694
|
-
continue;
|
|
1695
|
-
const transformed = transformByMatrix(skinMatrices, [j0, j1, j2, j3][j] * 16, position);
|
|
1696
|
-
sp = sp.add(transformed.scale(nw[j]));
|
|
1697
|
-
}
|
|
1698
|
-
vertices[i] = sp.x;
|
|
1699
|
-
vertices[i + 1] = sp.y;
|
|
1700
|
-
vertices[i + 2] = sp.z;
|
|
1701
|
-
vertices[i + 3] = baseVertices[i + 3];
|
|
1702
|
-
vertices[i + 4] = baseVertices[i + 4];
|
|
1703
|
-
vertices[i + 5] = baseVertices[i + 5];
|
|
1704
|
-
vertices[i + 6] = baseVertices[i + 6];
|
|
1705
|
-
vertices[i + 7] = baseVertices[i + 7];
|
|
1429
|
+
// Find model by 1-based index
|
|
1430
|
+
let idx = 1;
|
|
1431
|
+
let hitModel = "";
|
|
1432
|
+
for (const [name] of this.modelInstances) {
|
|
1433
|
+
if (idx === modelId) {
|
|
1434
|
+
hitModel = name;
|
|
1435
|
+
break;
|
|
1706
1436
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1437
|
+
idx++;
|
|
1438
|
+
}
|
|
1439
|
+
// Find material by 1-based index (skipping zero-vertex materials)
|
|
1440
|
+
let hitMaterial = null;
|
|
1441
|
+
if (hitModel) {
|
|
1442
|
+
const inst = this.modelInstances.get(hitModel);
|
|
1443
|
+
if (inst) {
|
|
1444
|
+
const materials = inst.model.getMaterials();
|
|
1445
|
+
let matIdx = 0;
|
|
1446
|
+
for (const mat of materials) {
|
|
1447
|
+
if (mat.vertexCount === 0)
|
|
1448
|
+
continue;
|
|
1449
|
+
matIdx++;
|
|
1450
|
+
if (matIdx === materialId) {
|
|
1451
|
+
hitMaterial = mat.name;
|
|
1717
1452
|
break;
|
|
1718
1453
|
}
|
|
1719
|
-
indexOffset += materials[matIdx].vertexCount;
|
|
1720
|
-
}
|
|
1721
|
-
if (triangleMaterialIndex === -1)
|
|
1722
|
-
continue;
|
|
1723
|
-
const edge1 = v1.subtract(v0), edge2 = v2.subtract(v0), h = rayDirection.cross(edge2), a = edge1.dot(h);
|
|
1724
|
-
if (Math.abs(a) < 0.0001)
|
|
1725
|
-
continue;
|
|
1726
|
-
const f = 1 / a, s = rayOrigin.subtract(v0), u = f * s.dot(h);
|
|
1727
|
-
if (u < 0 || u > 1)
|
|
1728
|
-
continue;
|
|
1729
|
-
const q = s.cross(edge1), v = f * rayDirection.dot(q);
|
|
1730
|
-
if (v < 0 || u + v > 1)
|
|
1731
|
-
continue;
|
|
1732
|
-
const t = f * edge2.dot(q);
|
|
1733
|
-
if (t <= 0.0001 || t >= maxDistance)
|
|
1734
|
-
continue;
|
|
1735
|
-
const triangleNormal = edge1.cross(edge2).normalize();
|
|
1736
|
-
if (triangleNormal.dot(rayDirection) >= 0)
|
|
1737
|
-
continue;
|
|
1738
|
-
if (!closest || t < closest.distance) {
|
|
1739
|
-
closest = { modelName: inst.name, materialName: materials[triangleMaterialIndex].name, distance: t };
|
|
1740
1454
|
}
|
|
1741
1455
|
}
|
|
1742
|
-
});
|
|
1743
|
-
if (this.onRaycast) {
|
|
1744
|
-
const hit = closest;
|
|
1745
|
-
this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY);
|
|
1746
1456
|
}
|
|
1457
|
+
this.onRaycast(hitModel, hitMaterial, screenX, screenY);
|
|
1747
1458
|
}
|
|
1748
1459
|
render() {
|
|
1749
1460
|
if (!this.multisampleTexture || !this.camera || !this.device)
|
|
@@ -1751,17 +1462,27 @@ export class Engine {
|
|
|
1751
1462
|
const currentTime = performance.now();
|
|
1752
1463
|
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016;
|
|
1753
1464
|
this.lastFrameTime = currentTime;
|
|
1754
|
-
this.updateCameraUniforms();
|
|
1755
1465
|
this.updateRenderTarget();
|
|
1756
1466
|
const hasModels = this.modelInstances.size > 0;
|
|
1757
1467
|
if (hasModels) {
|
|
1758
1468
|
this.updateInstances(deltaTime);
|
|
1759
1469
|
this.updateSkinMatrices();
|
|
1470
|
+
// Update camera target from bound model (bone not found → 0,0,0 + offset)
|
|
1471
|
+
if (this.cameraTargetModel) {
|
|
1472
|
+
const pos = this.cameraTargetModel.getBoneWorldPosition(this.cameraTargetBoneName);
|
|
1473
|
+
const px = pos?.x ?? 0;
|
|
1474
|
+
const py = pos?.y ?? 0;
|
|
1475
|
+
const pz = pos?.z ?? 0;
|
|
1476
|
+
this.camera.target.x = px + this.cameraTargetOffset.x;
|
|
1477
|
+
this.camera.target.y = py + this.cameraTargetOffset.y;
|
|
1478
|
+
this.camera.target.z = pz + this.cameraTargetOffset.z;
|
|
1479
|
+
}
|
|
1760
1480
|
}
|
|
1761
|
-
|
|
1481
|
+
this.updateCameraUniforms();
|
|
1482
|
+
if (this.hasGround)
|
|
1762
1483
|
this.updateShadowLightVP();
|
|
1763
1484
|
const encoder = this.device.createCommandEncoder();
|
|
1764
|
-
if (hasModels && this.
|
|
1485
|
+
if (hasModels && this.hasGround && this.shadowMapDepthView) {
|
|
1765
1486
|
const sp = encoder.beginRenderPass({
|
|
1766
1487
|
colorAttachments: [],
|
|
1767
1488
|
depthStencilAttachment: {
|
|
@@ -1777,13 +1498,25 @@ export class Engine {
|
|
|
1777
1498
|
}
|
|
1778
1499
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1779
1500
|
if (hasModels)
|
|
1780
|
-
this.forEachInstance((inst) => this.renderOneModel(pass, inst
|
|
1781
|
-
if (this.
|
|
1501
|
+
this.forEachInstance((inst) => this.renderOneModel(pass, inst));
|
|
1502
|
+
if (this.hasGround)
|
|
1782
1503
|
this.renderGround(pass);
|
|
1783
1504
|
pass.end();
|
|
1505
|
+
const pick = this.pendingPick;
|
|
1506
|
+
if (pick && hasModels)
|
|
1507
|
+
this.renderPickPass(encoder);
|
|
1784
1508
|
this.device.queue.submit([encoder.finish()]);
|
|
1509
|
+
if (pick) {
|
|
1510
|
+
this.pendingPick = null;
|
|
1511
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1512
|
+
this.resolvePickResult(pick.x / dpr, pick.y / dpr);
|
|
1513
|
+
}
|
|
1785
1514
|
this.updateStats(performance.now() - currentTime);
|
|
1786
1515
|
}
|
|
1516
|
+
updateRenderTarget() {
|
|
1517
|
+
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
1518
|
+
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
|
|
1519
|
+
}
|
|
1787
1520
|
drawInstanceShadow(sp, inst) {
|
|
1788
1521
|
sp.setBindGroup(0, inst.shadowBindGroup);
|
|
1789
1522
|
sp.setVertexBuffer(0, inst.vertexBuffer);
|
|
@@ -1795,48 +1528,38 @@ export class Engine {
|
|
|
1795
1528
|
sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1796
1529
|
}
|
|
1797
1530
|
}
|
|
1798
|
-
|
|
1799
|
-
pass.
|
|
1800
|
-
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1801
|
-
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1802
|
-
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1803
|
-
if (useReflection && mirrorMatrix) {
|
|
1804
|
-
this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix);
|
|
1805
|
-
pass.setPipeline(this.reflectionPipeline);
|
|
1806
|
-
for (const draw of inst.drawCalls) {
|
|
1807
|
-
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1808
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1809
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
this.renderEyes(pass, inst, true);
|
|
1813
|
-
this.renderHair(pass, inst, true);
|
|
1814
|
-
for (const draw of inst.drawCalls) {
|
|
1815
|
-
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1816
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1817
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
this.drawOutlines(pass, inst, true, true);
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
pass.setPipeline(this.modelPipeline);
|
|
1531
|
+
drawOpaque(pass, inst, pipeline) {
|
|
1532
|
+
pass.setPipeline(pipeline);
|
|
1824
1533
|
for (const draw of inst.drawCalls) {
|
|
1825
1534
|
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1826
|
-
pass.setBindGroup(
|
|
1535
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1827
1536
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1828
1537
|
}
|
|
1829
1538
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
pass.setPipeline(this.modelPipeline);
|
|
1539
|
+
}
|
|
1540
|
+
drawTransparent(pass, inst, pipeline) {
|
|
1541
|
+
pass.setPipeline(pipeline);
|
|
1834
1542
|
for (const draw of inst.drawCalls) {
|
|
1835
1543
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1836
|
-
pass.setBindGroup(
|
|
1544
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1837
1545
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1838
1546
|
}
|
|
1839
1547
|
}
|
|
1548
|
+
}
|
|
1549
|
+
bindMainGroups(pass, inst) {
|
|
1550
|
+
pass.setBindGroup(0, this.perFrameBindGroup);
|
|
1551
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
1552
|
+
}
|
|
1553
|
+
renderOneModel(pass, inst) {
|
|
1554
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
1555
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1556
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1557
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1558
|
+
this.bindMainGroups(pass, inst);
|
|
1559
|
+
this.drawOpaque(pass, inst, this.modelPipeline);
|
|
1560
|
+
this.drawOutlines(pass, inst, false);
|
|
1561
|
+
this.bindMainGroups(pass, inst);
|
|
1562
|
+
this.drawTransparent(pass, inst, this.modelPipeline);
|
|
1840
1563
|
this.drawOutlines(pass, inst, true);
|
|
1841
1564
|
}
|
|
1842
1565
|
updateCameraUniforms() {
|
|
@@ -1850,30 +1573,20 @@ export class Engine {
|
|
|
1850
1573
|
this.cameraMatrixData[34] = cameraPos.z;
|
|
1851
1574
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
1852
1575
|
}
|
|
1853
|
-
updateRenderTarget() {
|
|
1854
|
-
// Update render target to use current canvas texture
|
|
1855
|
-
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
1856
|
-
if (this.sampleCount > 1) {
|
|
1857
|
-
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
|
|
1858
|
-
}
|
|
1859
|
-
else {
|
|
1860
|
-
colorAttachment.view = this.context.getCurrentTexture().createView();
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
1576
|
updateSkinMatrices() {
|
|
1864
1577
|
this.forEachInstance((inst) => {
|
|
1865
1578
|
const skinMatrices = inst.model.getSkinMatrices();
|
|
1866
1579
|
this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, skinMatrices.buffer, skinMatrices.byteOffset, skinMatrices.byteLength);
|
|
1867
1580
|
});
|
|
1868
1581
|
}
|
|
1869
|
-
drawOutlines(pass, inst, transparent
|
|
1870
|
-
if (useReflectionPipeline)
|
|
1871
|
-
return;
|
|
1582
|
+
drawOutlines(pass, inst, transparent) {
|
|
1872
1583
|
pass.setPipeline(this.outlinePipeline);
|
|
1584
|
+
pass.setBindGroup(0, this.outlinePerFrameBindGroup);
|
|
1585
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
1873
1586
|
const outlineType = transparent ? "transparent-outline" : "opaque-outline";
|
|
1874
1587
|
for (const draw of inst.drawCalls) {
|
|
1875
1588
|
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
1876
|
-
pass.setBindGroup(
|
|
1589
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
1877
1590
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1878
1591
|
}
|
|
1879
1592
|
}
|
|
@@ -1900,41 +1613,6 @@ export class Engine {
|
|
|
1900
1613
|
this.lastFpsUpdate = now;
|
|
1901
1614
|
}
|
|
1902
1615
|
}
|
|
1903
|
-
createMirrorMatrix(planeNormal, planeDistance) {
|
|
1904
|
-
// Create reflection matrix across a plane
|
|
1905
|
-
const n = planeNormal.normalize();
|
|
1906
|
-
return new Mat4(new Float32Array([
|
|
1907
|
-
1 - 2 * n.x * n.x,
|
|
1908
|
-
-2 * n.x * n.y,
|
|
1909
|
-
-2 * n.x * n.z,
|
|
1910
|
-
0,
|
|
1911
|
-
-2 * n.y * n.x,
|
|
1912
|
-
1 - 2 * n.y * n.y,
|
|
1913
|
-
-2 * n.y * n.z,
|
|
1914
|
-
0,
|
|
1915
|
-
-2 * n.z * n.x,
|
|
1916
|
-
-2 * n.z * n.y,
|
|
1917
|
-
1 - 2 * n.z * n.z,
|
|
1918
|
-
0,
|
|
1919
|
-
-2 * planeDistance * n.x,
|
|
1920
|
-
-2 * planeDistance * n.y,
|
|
1921
|
-
-2 * planeDistance * n.z,
|
|
1922
|
-
1,
|
|
1923
|
-
]));
|
|
1924
|
-
}
|
|
1925
|
-
writeMirrorTransformedSkinMatrices(inst, mirrorMatrix) {
|
|
1926
|
-
const originalMatrices = inst.model.getSkinMatrices();
|
|
1927
|
-
const transformedMatrices = new Float32Array(originalMatrices.length);
|
|
1928
|
-
for (let i = 0; i < originalMatrices.length; i += 16) {
|
|
1929
|
-
const boneMatrixValues = new Float32Array(16);
|
|
1930
|
-
for (let j = 0; j < 16; j++)
|
|
1931
|
-
boneMatrixValues[j] = originalMatrices[i + j];
|
|
1932
|
-
const boneMatrix = new Mat4(boneMatrixValues);
|
|
1933
|
-
const transformed = mirrorMatrix.multiply(boneMatrix);
|
|
1934
|
-
for (let j = 0; j < 16; j++)
|
|
1935
|
-
transformedMatrices[i + j] = transformed.values[j];
|
|
1936
|
-
}
|
|
1937
|
-
this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices);
|
|
1938
|
-
}
|
|
1939
1616
|
}
|
|
1940
1617
|
Engine.instance = null;
|
|
1618
|
+
Engine.MULTISAMPLE_COUNT = 4;
|