reze-engine 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -45
- package/dist/animation.d.ts +0 -27
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +3 -17
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +2 -2
- package/dist/engine.d.ts +49 -52
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +613 -614
- package/dist/ik-solver.d.ts +0 -11
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +1 -11
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/math.d.ts +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +24 -0
- package/dist/model.d.ts +10 -50
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +60 -93
- package/dist/pmx-loader.js +1 -1
- package/package.json +1 -1
- package/src/animation.ts +3 -37
- package/src/camera.ts +2 -2
- package/src/engine.ts +646 -750
- package/src/ik-solver.ts +1 -11
- package/src/index.ts +3 -4
- package/src/math.ts +27 -0
- package/src/model.ts +68 -99
- package/src/pmx-loader.ts +1 -1
package/dist/engine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { Mat4, Vec3 } from "./math";
|
|
3
|
-
import {
|
|
3
|
+
import { Physics } from "./physics";
|
|
4
4
|
export const DEFAULT_ENGINE_OPTIONS = {
|
|
5
5
|
ambientColor: new Vec3(0.88, 0.88, 0.88),
|
|
6
6
|
directionalLightIntensity: 0.24,
|
|
@@ -10,10 +10,15 @@ export const DEFAULT_ENGINE_OPTIONS = {
|
|
|
10
10
|
cameraTarget: new Vec3(0, 12.5, 0),
|
|
11
11
|
cameraFov: Math.PI / 4,
|
|
12
12
|
onRaycast: undefined,
|
|
13
|
-
|
|
14
|
-
disablePhysics: false,
|
|
13
|
+
multisampleCount: 4,
|
|
15
14
|
};
|
|
16
15
|
export class Engine {
|
|
16
|
+
static getInstance() {
|
|
17
|
+
if (!Engine.instance) {
|
|
18
|
+
throw new Error("Engine not ready: create Engine, await init(), then load models via Model.loadPmx().");
|
|
19
|
+
}
|
|
20
|
+
return Engine.instance;
|
|
21
|
+
}
|
|
17
22
|
constructor(canvas, options) {
|
|
18
23
|
this.cameraMatrixData = new Float32Array(36);
|
|
19
24
|
this.lightData = new Float32Array(64);
|
|
@@ -23,22 +28,19 @@ export class Engine {
|
|
|
23
28
|
// Constants
|
|
24
29
|
this.STENCIL_EYE_VALUE = 1;
|
|
25
30
|
this.groundHasReflections = false;
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
31
|
+
this.groundMode = "reflection";
|
|
32
|
+
this.shadowLightVPMatrix = new Float32Array(16);
|
|
33
|
+
this.groundDrawCall = null;
|
|
34
|
+
this.shadowVPLightX = Number.NaN;
|
|
35
|
+
this.shadowVPLightY = Number.NaN;
|
|
36
|
+
this.shadowVPLightZ = Number.NaN;
|
|
28
37
|
// Double-tap detection
|
|
29
38
|
this.lastTouchTime = 0;
|
|
30
39
|
this.DOUBLE_TAP_DELAY = 300; // ms
|
|
31
|
-
|
|
32
|
-
this._disableIK = false;
|
|
33
|
-
this._disablePhysics = false;
|
|
34
|
-
this.currentModel = null;
|
|
35
|
-
this.modelDir = "";
|
|
40
|
+
this.modelInstances = new Map();
|
|
36
41
|
this.textureCache = new Map();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.drawCalls = [];
|
|
40
|
-
// Material visibility tracking
|
|
41
|
-
this.hiddenMaterials = new Set();
|
|
42
|
+
/** Reusable buffer for raycast skinning to avoid per-instance allocations (Three.js/Babylon.js style). */
|
|
43
|
+
this.raycastVertexBuffer = null;
|
|
42
44
|
this.lastFpsUpdate = performance.now();
|
|
43
45
|
this.framesSinceLastUpdate = 0;
|
|
44
46
|
this.lastFrameTime = performance.now();
|
|
@@ -51,15 +53,13 @@ export class Engine {
|
|
|
51
53
|
this.animationFrameId = null;
|
|
52
54
|
this.renderLoopCallback = null;
|
|
53
55
|
this.handleCanvasDoubleClick = (event) => {
|
|
54
|
-
if (!this.onRaycast ||
|
|
56
|
+
if (!this.onRaycast || this.modelInstances.size === 0)
|
|
55
57
|
return;
|
|
56
58
|
const rect = this.canvas.getBoundingClientRect();
|
|
57
|
-
|
|
58
|
-
const y = event.clientY - rect.top;
|
|
59
|
-
this.performRaycast(x, y);
|
|
59
|
+
this.performRaycast(event.clientX - rect.left, event.clientY - rect.top);
|
|
60
60
|
};
|
|
61
61
|
this.handleCanvasTouch = (event) => {
|
|
62
|
-
if (!this.onRaycast ||
|
|
62
|
+
if (!this.onRaycast || this.modelInstances.size === 0)
|
|
63
63
|
return;
|
|
64
64
|
// Prevent default to avoid triggering mouse events
|
|
65
65
|
event.preventDefault();
|
|
@@ -84,6 +84,7 @@ export class Engine {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
this.canvas = canvas;
|
|
87
|
+
this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount;
|
|
87
88
|
if (options) {
|
|
88
89
|
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor;
|
|
89
90
|
this.directionalLightIntensity =
|
|
@@ -94,8 +95,6 @@ export class Engine {
|
|
|
94
95
|
this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget;
|
|
95
96
|
this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov;
|
|
96
97
|
this.onRaycast = options.onRaycast;
|
|
97
|
-
this._disableIK = options.disableIK ?? DEFAULT_ENGINE_OPTIONS.disableIK;
|
|
98
|
-
this._disablePhysics = options.disablePhysics ?? DEFAULT_ENGINE_OPTIONS.disablePhysics;
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
// Step 1: Get WebGPU device and context
|
|
@@ -121,6 +120,7 @@ export class Engine {
|
|
|
121
120
|
this.setupLighting();
|
|
122
121
|
this.createPipelines();
|
|
123
122
|
this.setupResize();
|
|
123
|
+
Engine.instance = this;
|
|
124
124
|
}
|
|
125
125
|
createRenderPipeline(config) {
|
|
126
126
|
return this.device.createRenderPipeline({
|
|
@@ -379,15 +379,14 @@ export class Engine {
|
|
|
379
379
|
depthCompare: "less-equal",
|
|
380
380
|
},
|
|
381
381
|
});
|
|
382
|
-
// Create ground/reflection pipeline with reflection texture support
|
|
383
382
|
this.groundBindGroupLayout = this.device.createBindGroupLayout({
|
|
384
383
|
label: "ground bind group layout",
|
|
385
384
|
entries: [
|
|
386
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
387
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
388
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
389
|
-
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
390
|
-
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
385
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
386
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
387
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
388
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
389
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
391
390
|
],
|
|
392
391
|
});
|
|
393
392
|
const groundPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -397,97 +396,41 @@ export class Engine {
|
|
|
397
396
|
const groundShaderModule = this.device.createShaderModule({
|
|
398
397
|
label: "ground shaders",
|
|
399
398
|
code: /* wgsl */ `
|
|
400
|
-
struct CameraUniforms {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
_padding: f32,
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
struct LightUniforms {
|
|
408
|
-
ambientColor: vec4f,
|
|
409
|
-
lights: array<Light, 4>,
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
struct Light {
|
|
413
|
-
direction: vec4f,
|
|
414
|
-
color: vec4f,
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
struct GroundMaterialUniforms {
|
|
418
|
-
diffuseColor: vec3f,
|
|
419
|
-
reflectionLevel: f32,
|
|
420
|
-
fadeStart: f32,
|
|
421
|
-
fadeEnd: f32,
|
|
422
|
-
_padding1: f32,
|
|
423
|
-
_padding2: f32,
|
|
424
|
-
};
|
|
425
|
-
|
|
399
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
400
|
+
struct Light { direction: vec4f, color: vec4f, };
|
|
401
|
+
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
402
|
+
struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
|
|
426
403
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
427
404
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
428
405
|
@group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
|
|
429
406
|
@group(0) @binding(3) var reflectionSampler: sampler;
|
|
430
407
|
@group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
|
|
431
|
-
|
|
432
408
|
struct VertexOutput {
|
|
433
|
-
@builtin(position) position: vec4f,
|
|
434
|
-
@location(0) normal: vec3f,
|
|
435
|
-
@location(1) uv: vec2f,
|
|
436
|
-
@location(2) worldPos: vec3f,
|
|
409
|
+
@builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
|
|
437
410
|
};
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
) -> VertexOutput {
|
|
444
|
-
var output: VertexOutput;
|
|
445
|
-
let worldPos = position;
|
|
446
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
447
|
-
output.normal = normal;
|
|
448
|
-
output.uv = uv;
|
|
449
|
-
output.worldPos = worldPos;
|
|
450
|
-
return output;
|
|
411
|
+
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
|
|
412
|
+
var o: VertexOutput;
|
|
413
|
+
o.worldPos = position;
|
|
414
|
+
o.position = camera.projection * camera.view * vec4f(position, 1.0);
|
|
415
|
+
o.normal = normal; o.uv = uv; return o;
|
|
451
416
|
}
|
|
452
|
-
|
|
453
417
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
454
418
|
let n = normalize(input.normal);
|
|
455
|
-
|
|
419
|
+
let centerDist = length(input.worldPos.xz);
|
|
420
|
+
let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
|
|
421
|
+
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
456
422
|
let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
|
|
457
423
|
let ndcPos = clipPos.xyz / clipPos.w;
|
|
458
|
-
|
|
459
|
-
|
|
424
|
+
let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
|
|
460
425
|
let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
|
|
461
|
-
let isValidReflection = clipPos.w > 0.0 &&
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
let fadeFactor = clamp((distanceFromCamera - 15.0) / 20.0, 0.0, 1.0);
|
|
467
|
-
reflectionColor *= (1.0 - fadeFactor * 0.3);
|
|
468
|
-
|
|
469
|
-
let diffuseColor = material.diffuseColor;
|
|
470
|
-
var finalColor = mix(diffuseColor, reflectionColor, material.reflectionLevel);
|
|
471
|
-
|
|
472
|
-
// Ground edge fade effect - smooth fade out at edges based on distance from center
|
|
473
|
-
let centerDist = length(input.worldPos.xz); // Distance from ground center in XZ plane
|
|
474
|
-
|
|
475
|
-
// Smoothstep for much smoother gradient transition
|
|
476
|
-
let t = clamp((centerDist - material.fadeStart) / (material.fadeEnd - material.fadeStart), 0.0, 1.0);
|
|
477
|
-
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
478
|
-
finalColor *= edgeFade;
|
|
479
|
-
|
|
480
|
-
// Single directional light
|
|
426
|
+
let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
|
|
427
|
+
let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
|
|
428
|
+
let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
|
|
429
|
+
let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
|
|
430
|
+
var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
|
|
481
431
|
let l = -light.lights[0].direction.xyz;
|
|
482
|
-
let
|
|
483
|
-
|
|
484
|
-
let radiance = light.lights[0].color.xyz * intensity;
|
|
485
|
-
let lightAccum = light.ambientColor.xyz + radiance * nDotL;
|
|
486
|
-
|
|
487
|
-
// Apply lighting to the blended color
|
|
488
|
-
let litColor = finalColor * lightAccum;
|
|
489
|
-
|
|
490
|
-
return vec4f(litColor, edgeFade);
|
|
432
|
+
let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
|
|
433
|
+
return vec4f(finalColor * lightAccum, edgeFade);
|
|
491
434
|
}
|
|
492
435
|
`,
|
|
493
436
|
});
|
|
@@ -498,12 +441,123 @@ export class Engine {
|
|
|
498
441
|
vertexBuffers: fullVertexBuffers,
|
|
499
442
|
fragmentTarget: standardBlend,
|
|
500
443
|
cullMode: "back",
|
|
444
|
+
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
445
|
+
});
|
|
446
|
+
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
447
|
+
size: 64,
|
|
448
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
449
|
+
});
|
|
450
|
+
const shadowBindGroupLayout = this.device.createBindGroupLayout({
|
|
451
|
+
label: "shadow depth bind layout",
|
|
452
|
+
entries: [
|
|
453
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
454
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
455
|
+
],
|
|
456
|
+
});
|
|
457
|
+
const shadowShader = this.device.createShaderModule({
|
|
458
|
+
label: "shadow depth",
|
|
459
|
+
code: /* wgsl */ `
|
|
460
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
461
|
+
@group(0) @binding(0) var<uniform> lp: LightVP;
|
|
462
|
+
@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
|
|
463
|
+
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
|
|
464
|
+
@location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
|
|
465
|
+
let pos4 = vec4f(position, 1.0);
|
|
466
|
+
let ws = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
467
|
+
let inv = select(1.0, 1.0 / ws, ws > 0.0001);
|
|
468
|
+
let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
|
|
469
|
+
var sp = vec4f(0.0);
|
|
470
|
+
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
471
|
+
return lp.viewProj * vec4f(sp.xyz, 1.0);
|
|
472
|
+
}
|
|
473
|
+
`,
|
|
474
|
+
});
|
|
475
|
+
this.shadowDepthPipeline = this.device.createRenderPipeline({
|
|
476
|
+
label: "shadow depth pipeline",
|
|
477
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [shadowBindGroupLayout] }),
|
|
478
|
+
vertex: { module: shadowShader, entryPoint: "vs", buffers: fullVertexBuffers },
|
|
479
|
+
primitive: { cullMode: "none" },
|
|
501
480
|
depthStencil: {
|
|
502
|
-
format: "
|
|
481
|
+
format: "depth32float",
|
|
503
482
|
depthWriteEnabled: true,
|
|
504
483
|
depthCompare: "less-equal",
|
|
484
|
+
depthBias: 2,
|
|
485
|
+
depthBiasSlopeScale: 1.5,
|
|
486
|
+
depthBiasClamp: 0,
|
|
505
487
|
},
|
|
506
488
|
});
|
|
489
|
+
this.shadowComparisonSampler = this.device.createSampler({
|
|
490
|
+
compare: "less",
|
|
491
|
+
magFilter: "linear",
|
|
492
|
+
minFilter: "linear",
|
|
493
|
+
});
|
|
494
|
+
this.groundShadowBindGroupLayout = this.device.createBindGroupLayout({
|
|
495
|
+
label: "ground shadow layout",
|
|
496
|
+
entries: [
|
|
497
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
498
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
499
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
500
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
501
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
502
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
503
|
+
],
|
|
504
|
+
});
|
|
505
|
+
const groundShadowShader = this.device.createShaderModule({
|
|
506
|
+
label: "ground shadow",
|
|
507
|
+
code: /* wgsl */ `
|
|
508
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
509
|
+
struct Light { direction: vec4f, color: vec4f, };
|
|
510
|
+
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
511
|
+
struct GroundShadowMat { diffuseColor: vec3f, fadeStart: f32, fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, _y: f32, };
|
|
512
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
513
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
514
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
515
|
+
@group(0) @binding(2) var shadowMap: texture_depth_2d;
|
|
516
|
+
@group(0) @binding(3) var shadowSampler: sampler_comparison;
|
|
517
|
+
@group(0) @binding(4) var<uniform> material: GroundShadowMat;
|
|
518
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
519
|
+
struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
|
|
520
|
+
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
|
|
521
|
+
var o: VO; o.worldPos = position; o.normal = normal;
|
|
522
|
+
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
523
|
+
}
|
|
524
|
+
@fragment fn fs(i: VO) -> @location(0) vec4f {
|
|
525
|
+
let n = normalize(i.normal);
|
|
526
|
+
let centerDist = length(i.worldPos.xz);
|
|
527
|
+
let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));
|
|
528
|
+
let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);
|
|
529
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
530
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
531
|
+
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
532
|
+
let st = material.pcfTexel;
|
|
533
|
+
let compareZ = ndc.z - 0.0035;
|
|
534
|
+
var vis = 0.0;
|
|
535
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, -st), compareZ);
|
|
536
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(0.0, -st), compareZ);
|
|
537
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, -st), compareZ);
|
|
538
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, 0.0), compareZ);
|
|
539
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c, compareZ);
|
|
540
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, 0.0), compareZ);
|
|
541
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, st), compareZ);
|
|
542
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(0.0, st), compareZ);
|
|
543
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, st), compareZ);
|
|
544
|
+
vis *= 0.1111111;
|
|
545
|
+
let sun = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);
|
|
546
|
+
let dark = (1.0 - vis) * material.shadowStrength;
|
|
547
|
+
let lit = material.diffuseColor * sun * (1.0 - dark * 0.65);
|
|
548
|
+
return vec4f(lit * edgeFade, edgeFade);
|
|
549
|
+
}
|
|
550
|
+
`,
|
|
551
|
+
});
|
|
552
|
+
this.groundShadowPipeline = this.createRenderPipeline({
|
|
553
|
+
label: "ground shadow pipeline",
|
|
554
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
|
|
555
|
+
shaderModule: groundShadowShader,
|
|
556
|
+
vertexBuffers: fullVertexBuffers,
|
|
557
|
+
fragmentTarget: standardBlend,
|
|
558
|
+
cullMode: "back",
|
|
559
|
+
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
560
|
+
});
|
|
507
561
|
// Create reflection pipeline (multisampled version for higher quality)
|
|
508
562
|
this.reflectionPipeline = this.createRenderPipeline({
|
|
509
563
|
label: "reflection pipeline",
|
|
@@ -894,50 +948,32 @@ export class Engine {
|
|
|
894
948
|
reflectionTextureSize: 1024,
|
|
895
949
|
fadeStart: 5.0,
|
|
896
950
|
fadeEnd: 60.0,
|
|
951
|
+
mode: "reflection",
|
|
952
|
+
shadowMapSize: 4096,
|
|
953
|
+
shadowStrength: 1.0,
|
|
897
954
|
...options,
|
|
898
955
|
};
|
|
899
|
-
|
|
956
|
+
this.groundMode = opts.mode;
|
|
900
957
|
this.createGroundGeometry(opts.width, opts.height);
|
|
901
|
-
|
|
902
|
-
|
|
958
|
+
if (opts.mode === "reflection") {
|
|
959
|
+
this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd);
|
|
960
|
+
this.createReflectionTexture(opts.reflectionTextureSize);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength);
|
|
964
|
+
}
|
|
903
965
|
this.groundHasReflections = true;
|
|
904
|
-
this.
|
|
966
|
+
this.groundDrawCall = {
|
|
905
967
|
type: "ground",
|
|
906
|
-
count: 6,
|
|
968
|
+
count: 6,
|
|
907
969
|
firstIndex: 0,
|
|
908
|
-
bindGroup: this.groundReflectionBindGroup,
|
|
970
|
+
bindGroup: (opts.mode === "reflection" ? this.groundReflectionBindGroup : this.groundShadowBindGroup),
|
|
909
971
|
materialName: "Ground",
|
|
910
|
-
}
|
|
972
|
+
};
|
|
911
973
|
}
|
|
912
974
|
updateLightBuffer() {
|
|
913
975
|
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
|
|
914
976
|
}
|
|
915
|
-
async loadAnimation(url) {
|
|
916
|
-
if (!this.currentModel)
|
|
917
|
-
return;
|
|
918
|
-
await this.currentModel.loadVmd(url);
|
|
919
|
-
}
|
|
920
|
-
loadAnimationData(data) {
|
|
921
|
-
this.currentModel?.loadAnimationData(data);
|
|
922
|
-
}
|
|
923
|
-
getAnimationData() {
|
|
924
|
-
return this.currentModel?.getAnimationData() ?? null;
|
|
925
|
-
}
|
|
926
|
-
playAnimation() {
|
|
927
|
-
this.currentModel?.playAnimation();
|
|
928
|
-
}
|
|
929
|
-
stopAnimation() {
|
|
930
|
-
this.currentModel?.stopAnimation();
|
|
931
|
-
}
|
|
932
|
-
pauseAnimation() {
|
|
933
|
-
this.currentModel?.pauseAnimation();
|
|
934
|
-
}
|
|
935
|
-
seekAnimation(time) {
|
|
936
|
-
this.currentModel?.seekAnimation(time);
|
|
937
|
-
}
|
|
938
|
-
getAnimationProgress() {
|
|
939
|
-
return this.currentModel?.getAnimationProgress() ?? { current: 0, duration: 0, percentage: 0 };
|
|
940
|
-
}
|
|
941
977
|
getStats() {
|
|
942
978
|
return { ...this.stats };
|
|
943
979
|
}
|
|
@@ -961,7 +997,9 @@ export class Engine {
|
|
|
961
997
|
}
|
|
962
998
|
dispose() {
|
|
963
999
|
this.stopRenderLoop();
|
|
964
|
-
this.stopAnimation();
|
|
1000
|
+
this.forEachInstance((inst) => inst.model.stopAnimation());
|
|
1001
|
+
if (Engine.instance === this)
|
|
1002
|
+
Engine.instance = null;
|
|
965
1003
|
if (this.camera)
|
|
966
1004
|
this.camera.detachControl();
|
|
967
1005
|
// Remove raycasting event listeners
|
|
@@ -974,145 +1012,175 @@ export class Engine {
|
|
|
974
1012
|
this.resizeObserver = null;
|
|
975
1013
|
}
|
|
976
1014
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1015
|
+
async addModel(model, pmxPath, name) {
|
|
1016
|
+
const requested = name ?? model.name;
|
|
1017
|
+
let key = requested;
|
|
1018
|
+
let n = 1;
|
|
1019
|
+
while (this.modelInstances.has(key)) {
|
|
1020
|
+
key = `${requested}_${n++}`;
|
|
1021
|
+
}
|
|
1022
|
+
const pathParts = pmxPath.split("/");
|
|
980
1023
|
pathParts.pop();
|
|
981
|
-
const
|
|
982
|
-
this.
|
|
983
|
-
|
|
984
|
-
// Clear cached skinned vertices when loading a new model
|
|
985
|
-
this.cachedSkinnedVertices = undefined;
|
|
986
|
-
this.cachedSkinMatricesVersion = -1;
|
|
987
|
-
await this.setupModelBuffers(model);
|
|
1024
|
+
const basePath = pathParts.join("/") + "/";
|
|
1025
|
+
await this.setupModelInstance(key, model, basePath);
|
|
1026
|
+
return key;
|
|
988
1027
|
}
|
|
989
|
-
|
|
990
|
-
this.
|
|
1028
|
+
async registerModel(model, pmxPath) {
|
|
1029
|
+
return this.addModel(model, pmxPath);
|
|
991
1030
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
this.currentModel?.moveBones(boneTranslations, durationMs);
|
|
1031
|
+
removeModel(name) {
|
|
1032
|
+
this.modelInstances.delete(name);
|
|
995
1033
|
}
|
|
996
|
-
|
|
997
|
-
this.
|
|
1034
|
+
getModelNames() {
|
|
1035
|
+
return Array.from(this.modelInstances.keys());
|
|
998
1036
|
}
|
|
999
|
-
|
|
1000
|
-
this.
|
|
1037
|
+
getModel(name) {
|
|
1038
|
+
return this.modelInstances.get(name)?.model ?? null;
|
|
1001
1039
|
}
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1040
|
+
markVertexBufferDirty(modelNameOrModel) {
|
|
1041
|
+
if (modelNameOrModel === undefined)
|
|
1042
|
+
return;
|
|
1043
|
+
if (typeof modelNameOrModel === "string") {
|
|
1044
|
+
const inst = this.modelInstances.get(modelNameOrModel);
|
|
1045
|
+
if (inst)
|
|
1046
|
+
inst.vertexBufferNeedsUpdate = true;
|
|
1004
1047
|
return;
|
|
1005
|
-
this.currentModel.setMorphWeight(name, weight, durationMs);
|
|
1006
|
-
if (!durationMs || durationMs === 0) {
|
|
1007
|
-
this.vertexBufferNeedsUpdate = true;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
setMaterialVisible(name, visible) {
|
|
1011
|
-
if (visible) {
|
|
1012
|
-
this.hiddenMaterials.delete(name);
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
this.hiddenMaterials.add(name);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
toggleMaterialVisible(name) {
|
|
1019
|
-
if (this.hiddenMaterials.has(name)) {
|
|
1020
|
-
this.hiddenMaterials.delete(name);
|
|
1021
1048
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1049
|
+
for (const inst of this.modelInstances.values()) {
|
|
1050
|
+
if (inst.model === modelNameOrModel) {
|
|
1051
|
+
inst.vertexBufferNeedsUpdate = true;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1024
1054
|
}
|
|
1025
1055
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1056
|
+
setMaterialVisible(modelName, materialName, visible) {
|
|
1057
|
+
const inst = this.modelInstances.get(modelName);
|
|
1058
|
+
if (!inst)
|
|
1059
|
+
return;
|
|
1060
|
+
if (visible)
|
|
1061
|
+
inst.hiddenMaterials.delete(materialName);
|
|
1062
|
+
else
|
|
1063
|
+
inst.hiddenMaterials.add(materialName);
|
|
1064
|
+
}
|
|
1065
|
+
toggleMaterialVisible(modelName, materialName) {
|
|
1066
|
+
const inst = this.modelInstances.get(modelName);
|
|
1067
|
+
if (!inst)
|
|
1068
|
+
return;
|
|
1069
|
+
if (inst.hiddenMaterials.has(materialName))
|
|
1070
|
+
inst.hiddenMaterials.delete(materialName);
|
|
1071
|
+
else
|
|
1072
|
+
inst.hiddenMaterials.add(materialName);
|
|
1028
1073
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1074
|
+
isMaterialVisible(modelName, materialName) {
|
|
1075
|
+
const inst = this.modelInstances.get(modelName);
|
|
1076
|
+
return inst ? !inst.hiddenMaterials.has(materialName) : false;
|
|
1031
1077
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1078
|
+
setModelIKEnabled(modelName, enabled) {
|
|
1079
|
+
this.modelInstances.get(modelName)?.model.setIKEnabled(enabled);
|
|
1034
1080
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1081
|
+
setModelPhysicsEnabled(modelName, enabled) {
|
|
1082
|
+
this.modelInstances.get(modelName)?.model.setPhysicsEnabled(enabled);
|
|
1037
1083
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1084
|
+
resetPhysics() {
|
|
1085
|
+
this.forEachInstance((inst) => {
|
|
1086
|
+
if (!inst.physics)
|
|
1087
|
+
return;
|
|
1088
|
+
inst.model.computeWorldMatrices();
|
|
1089
|
+
inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
|
|
1090
|
+
});
|
|
1041
1091
|
}
|
|
1042
|
-
|
|
1043
|
-
this.
|
|
1044
|
-
this.currentModel?.setIKEnabled(!value);
|
|
1092
|
+
instances() {
|
|
1093
|
+
return this.modelInstances.values();
|
|
1045
1094
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1095
|
+
forEachInstance(fn) {
|
|
1096
|
+
for (const inst of this.instances())
|
|
1097
|
+
fn(inst);
|
|
1049
1098
|
}
|
|
1050
|
-
|
|
1051
|
-
this.
|
|
1052
|
-
|
|
1099
|
+
updateInstances(deltaTime) {
|
|
1100
|
+
this.forEachInstance((inst) => {
|
|
1101
|
+
const verticesChanged = inst.model.update(deltaTime);
|
|
1102
|
+
if (verticesChanged)
|
|
1103
|
+
inst.vertexBufferNeedsUpdate = true;
|
|
1104
|
+
if (inst.physics && inst.model.getPhysicsEnabled()) {
|
|
1105
|
+
inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
|
|
1106
|
+
}
|
|
1107
|
+
if (inst.vertexBufferNeedsUpdate)
|
|
1108
|
+
this.updateVertexBuffer(inst);
|
|
1109
|
+
});
|
|
1053
1110
|
}
|
|
1054
|
-
updateVertexBuffer() {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const vertices = this.currentModel.getVertices();
|
|
1058
|
-
if (!vertices || vertices.length === 0)
|
|
1111
|
+
updateVertexBuffer(inst) {
|
|
1112
|
+
const vertices = inst.model.getVertices();
|
|
1113
|
+
if (!vertices?.length)
|
|
1059
1114
|
return;
|
|
1060
|
-
this.device.queue.writeBuffer(
|
|
1115
|
+
this.device.queue.writeBuffer(inst.vertexBuffer, 0, vertices);
|
|
1116
|
+
inst.vertexBufferNeedsUpdate = false;
|
|
1061
1117
|
}
|
|
1062
|
-
|
|
1063
|
-
async setupModelBuffers(model) {
|
|
1064
|
-
this.currentModel = model;
|
|
1065
|
-
// Apply IK and Physics flags from engine options
|
|
1066
|
-
model.setIKEnabled(!this._disableIK);
|
|
1067
|
-
model.setPhysicsEnabled(!this._disablePhysics);
|
|
1118
|
+
async setupModelInstance(name, model, basePath) {
|
|
1068
1119
|
const vertices = model.getVertices();
|
|
1069
1120
|
const skinning = model.getSkinning();
|
|
1070
1121
|
const skeleton = model.getSkeleton();
|
|
1071
|
-
|
|
1072
|
-
|
|
1122
|
+
const boneCount = skeleton.bones.length;
|
|
1123
|
+
const matrixSize = boneCount * 16 * 4;
|
|
1124
|
+
const vertexBuffer = this.device.createBuffer({
|
|
1125
|
+
label: `${name}: vertex buffer`,
|
|
1073
1126
|
size: vertices.byteLength,
|
|
1074
1127
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1075
1128
|
});
|
|
1076
|
-
this.device.queue.writeBuffer(
|
|
1077
|
-
|
|
1078
|
-
label:
|
|
1129
|
+
this.device.queue.writeBuffer(vertexBuffer, 0, vertices);
|
|
1130
|
+
const jointsBuffer = this.device.createBuffer({
|
|
1131
|
+
label: `${name}: joints buffer`,
|
|
1079
1132
|
size: skinning.joints.byteLength,
|
|
1080
1133
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1081
1134
|
});
|
|
1082
|
-
this.device.queue.writeBuffer(
|
|
1083
|
-
|
|
1084
|
-
label:
|
|
1135
|
+
this.device.queue.writeBuffer(jointsBuffer, 0, skinning.joints.buffer, skinning.joints.byteOffset, skinning.joints.byteLength);
|
|
1136
|
+
const weightsBuffer = this.device.createBuffer({
|
|
1137
|
+
label: `${name}: weights buffer`,
|
|
1085
1138
|
size: skinning.weights.byteLength,
|
|
1086
1139
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1087
1140
|
});
|
|
1088
|
-
this.device.queue.writeBuffer(
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1092
|
-
label: "skin matrices",
|
|
1141
|
+
this.device.queue.writeBuffer(weightsBuffer, 0, skinning.weights.buffer, skinning.weights.byteOffset, skinning.weights.byteLength);
|
|
1142
|
+
const skinMatrixBuffer = this.device.createBuffer({
|
|
1143
|
+
label: `${name}: skin matrices`,
|
|
1093
1144
|
size: Math.max(256, matrixSize),
|
|
1094
1145
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1095
1146
|
});
|
|
1096
|
-
this.inverseBindMatrixBuffer = this.device.createBuffer({
|
|
1097
|
-
label: "inverse bind matrices",
|
|
1098
|
-
size: Math.max(256, matrixSize),
|
|
1099
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
1100
|
-
});
|
|
1101
|
-
const invBindMatrices = skeleton.inverseBindMatrices;
|
|
1102
|
-
this.device.queue.writeBuffer(this.inverseBindMatrixBuffer, 0, invBindMatrices.buffer, invBindMatrices.byteOffset, invBindMatrices.byteLength);
|
|
1103
1147
|
const indices = model.getIndices();
|
|
1104
|
-
if (indices)
|
|
1105
|
-
this.indexBuffer = this.device.createBuffer({
|
|
1106
|
-
label: "model index buffer",
|
|
1107
|
-
size: indices.byteLength,
|
|
1108
|
-
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1109
|
-
});
|
|
1110
|
-
this.device.queue.writeBuffer(this.indexBuffer, 0, indices);
|
|
1111
|
-
}
|
|
1112
|
-
else {
|
|
1148
|
+
if (!indices)
|
|
1113
1149
|
throw new Error("Model has no index buffer");
|
|
1114
|
-
|
|
1115
|
-
|
|
1150
|
+
const indexBuffer = this.device.createBuffer({
|
|
1151
|
+
label: `${name}: index buffer`,
|
|
1152
|
+
size: indices.byteLength,
|
|
1153
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1154
|
+
});
|
|
1155
|
+
this.device.queue.writeBuffer(indexBuffer, 0, indices);
|
|
1156
|
+
const rbs = model.getRigidbodies();
|
|
1157
|
+
const physics = rbs.length > 0 ? new Physics(rbs, model.getJoints()) : null;
|
|
1158
|
+
const shadowBindGroup = this.device.createBindGroup({
|
|
1159
|
+
label: `${name}: shadow bind`,
|
|
1160
|
+
layout: this.shadowDepthPipeline.getBindGroupLayout(0),
|
|
1161
|
+
entries: [
|
|
1162
|
+
{ binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
|
|
1163
|
+
{ binding: 1, resource: { buffer: skinMatrixBuffer } },
|
|
1164
|
+
],
|
|
1165
|
+
});
|
|
1166
|
+
const inst = {
|
|
1167
|
+
name,
|
|
1168
|
+
model,
|
|
1169
|
+
basePath,
|
|
1170
|
+
vertexBuffer,
|
|
1171
|
+
indexBuffer,
|
|
1172
|
+
jointsBuffer,
|
|
1173
|
+
weightsBuffer,
|
|
1174
|
+
skinMatrixBuffer,
|
|
1175
|
+
drawCalls: [],
|
|
1176
|
+
shadowDrawCalls: [],
|
|
1177
|
+
shadowBindGroup,
|
|
1178
|
+
hiddenMaterials: new Set(),
|
|
1179
|
+
physics,
|
|
1180
|
+
vertexBufferNeedsUpdate: false,
|
|
1181
|
+
};
|
|
1182
|
+
await this.setupMaterialsForInstance(inst);
|
|
1183
|
+
this.modelInstances.set(name, inst);
|
|
1116
1184
|
}
|
|
1117
1185
|
createGroundGeometry(width = 100, height = 100) {
|
|
1118
1186
|
const halfWidth = width / 2;
|
|
@@ -1178,27 +1246,24 @@ export class Engine {
|
|
|
1178
1246
|
});
|
|
1179
1247
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
1180
1248
|
}
|
|
1181
|
-
createGroundMaterialBuffer(diffuseColor
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
0, // padding (4 bytes)
|
|
1192
|
-
0, // padding (4 bytes)
|
|
1193
|
-
]);
|
|
1249
|
+
createGroundMaterialBuffer(diffuseColor, reflectionLevel, fadeStart, fadeEnd) {
|
|
1250
|
+
const u = new Float32Array(8);
|
|
1251
|
+
u[0] = diffuseColor.x;
|
|
1252
|
+
u[1] = diffuseColor.y;
|
|
1253
|
+
u[2] = diffuseColor.z;
|
|
1254
|
+
u[3] = reflectionLevel;
|
|
1255
|
+
u[4] = fadeStart;
|
|
1256
|
+
u[5] = fadeEnd;
|
|
1257
|
+
u[6] = 0;
|
|
1258
|
+
u[7] = 0;
|
|
1194
1259
|
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1195
1260
|
label: "ground material uniform buffer",
|
|
1196
|
-
size:
|
|
1261
|
+
size: 64,
|
|
1197
1262
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1198
1263
|
});
|
|
1199
|
-
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0,
|
|
1264
|
+
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u);
|
|
1200
1265
|
}
|
|
1201
|
-
createReflectionTexture(size
|
|
1266
|
+
createReflectionTexture(size) {
|
|
1202
1267
|
this.groundReflectionTexture = this.device.createTexture({
|
|
1203
1268
|
label: "ground reflection texture",
|
|
1204
1269
|
size: [size, size],
|
|
@@ -1219,34 +1284,88 @@ export class Engine {
|
|
|
1219
1284
|
format: "depth24plus-stencil8",
|
|
1220
1285
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1221
1286
|
});
|
|
1222
|
-
// Create a bind group for the reflection texture that can be used in the ground material
|
|
1223
1287
|
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1224
1288
|
label: "ground reflection bind group",
|
|
1225
1289
|
layout: this.groundBindGroupLayout,
|
|
1226
1290
|
entries: [
|
|
1227
1291
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1228
1292
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1229
|
-
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1293
|
+
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1230
1294
|
{ binding: 3, resource: this.materialSampler },
|
|
1231
1295
|
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer } },
|
|
1232
1296
|
],
|
|
1233
1297
|
});
|
|
1234
1298
|
}
|
|
1235
|
-
|
|
1299
|
+
createShadowGroundResources(shadowMapSize, diffuseColor, fadeStart, fadeEnd, shadowStrength) {
|
|
1300
|
+
this.shadowMapTexture = this.device.createTexture({
|
|
1301
|
+
label: "shadow map",
|
|
1302
|
+
size: [shadowMapSize, shadowMapSize],
|
|
1303
|
+
format: "depth32float",
|
|
1304
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1305
|
+
});
|
|
1306
|
+
this.shadowMapDepthView = this.shadowMapTexture.createView();
|
|
1307
|
+
const gb = new Float32Array(8);
|
|
1308
|
+
gb[0] = diffuseColor.x;
|
|
1309
|
+
gb[1] = diffuseColor.y;
|
|
1310
|
+
gb[2] = diffuseColor.z;
|
|
1311
|
+
gb[3] = fadeStart;
|
|
1312
|
+
gb[4] = fadeEnd;
|
|
1313
|
+
gb[5] = shadowStrength;
|
|
1314
|
+
gb[6] = 1.2 / shadowMapSize;
|
|
1315
|
+
gb[7] = 0;
|
|
1316
|
+
this.groundShadowMaterialBuffer = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
1317
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 0, gb);
|
|
1318
|
+
this.groundShadowBindGroup = this.device.createBindGroup({
|
|
1319
|
+
label: "ground shadow bind",
|
|
1320
|
+
layout: this.groundShadowBindGroupLayout,
|
|
1321
|
+
entries: [
|
|
1322
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1323
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1324
|
+
{ binding: 2, resource: this.shadowMapDepthView },
|
|
1325
|
+
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
1326
|
+
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
1327
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
1328
|
+
],
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
updateShadowLightVP() {
|
|
1332
|
+
const lx = this.lightData[4];
|
|
1333
|
+
const ly = this.lightData[5];
|
|
1334
|
+
const lz = this.lightData[6];
|
|
1335
|
+
if (lx === this.shadowVPLightX && ly === this.shadowVPLightY && lz === this.shadowVPLightZ)
|
|
1336
|
+
return;
|
|
1337
|
+
this.shadowVPLightX = lx;
|
|
1338
|
+
this.shadowVPLightY = ly;
|
|
1339
|
+
this.shadowVPLightZ = lz;
|
|
1340
|
+
const dir = new Vec3(lx, ly, lz);
|
|
1341
|
+
if (dir.length() < 1e-6) {
|
|
1342
|
+
dir.x = 0.35;
|
|
1343
|
+
dir.y = -1;
|
|
1344
|
+
dir.z = 0.2;
|
|
1345
|
+
}
|
|
1346
|
+
else
|
|
1347
|
+
dir.normalize();
|
|
1348
|
+
const target = new Vec3(0, 11, 0);
|
|
1349
|
+
const eye = new Vec3(target.x - dir.x * 72, target.y - dir.y * 72, target.z - dir.z * 72);
|
|
1350
|
+
const view = Mat4.lookAt(eye, target, new Vec3(0, 1, 0));
|
|
1351
|
+
const proj = Mat4.orthographicLh(-72, 72, -72, 72, 1, 140);
|
|
1352
|
+
const vp = proj.multiply(view);
|
|
1353
|
+
this.shadowLightVPMatrix.set(vp.values);
|
|
1354
|
+
this.device.queue.writeBuffer(this.shadowLightVPBuffer, 0, this.shadowLightVPMatrix);
|
|
1355
|
+
}
|
|
1356
|
+
async setupMaterialsForInstance(inst) {
|
|
1357
|
+
const model = inst.model;
|
|
1236
1358
|
const materials = model.getMaterials();
|
|
1237
|
-
if (materials.length === 0)
|
|
1359
|
+
if (materials.length === 0)
|
|
1238
1360
|
throw new Error("Model has no materials");
|
|
1239
|
-
}
|
|
1240
1361
|
const textures = model.getTextures();
|
|
1362
|
+
const prefix = `${inst.name}: `;
|
|
1241
1363
|
const loadTextureByIndex = async (texIndex) => {
|
|
1242
|
-
if (texIndex < 0 || texIndex >= textures.length)
|
|
1364
|
+
if (texIndex < 0 || texIndex >= textures.length)
|
|
1243
1365
|
return null;
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const texture = await this.createTextureFromPath(path);
|
|
1247
|
-
return texture;
|
|
1366
|
+
const path = inst.basePath + textures[texIndex].path;
|
|
1367
|
+
return this.createTextureFromPath(path);
|
|
1248
1368
|
};
|
|
1249
|
-
this.drawCalls = [];
|
|
1250
1369
|
let currentIndexOffset = 0;
|
|
1251
1370
|
for (const mat of materials) {
|
|
1252
1371
|
const indexCount = mat.vertexCount;
|
|
@@ -1257,124 +1376,87 @@ export class Engine {
|
|
|
1257
1376
|
throw new Error(`Material "${mat.name}" has no diffuse texture`);
|
|
1258
1377
|
const materialAlpha = mat.diffuse[3];
|
|
1259
1378
|
const isTransparent = materialAlpha < 1.0 - 0.001;
|
|
1260
|
-
const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1261
|
-
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1379
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha, 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1262
1380
|
const bindGroup = this.device.createBindGroup({
|
|
1263
|
-
label:
|
|
1381
|
+
label: `${prefix}material: ${mat.name}`,
|
|
1264
1382
|
layout: this.mainBindGroupLayout,
|
|
1265
1383
|
entries: [
|
|
1266
1384
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1267
1385
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1268
1386
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1269
1387
|
{ binding: 3, resource: this.materialSampler },
|
|
1270
|
-
{ binding: 4, resource: { buffer:
|
|
1388
|
+
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1271
1389
|
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1272
1390
|
],
|
|
1273
1391
|
});
|
|
1274
1392
|
if (indexCount > 0) {
|
|
1275
1393
|
if (mat.isEye) {
|
|
1276
|
-
|
|
1277
|
-
type: "eye",
|
|
1278
|
-
count: indexCount,
|
|
1279
|
-
firstIndex: currentIndexOffset,
|
|
1280
|
-
bindGroup,
|
|
1281
|
-
materialName: mat.name,
|
|
1282
|
-
});
|
|
1394
|
+
inst.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1283
1395
|
}
|
|
1284
1396
|
else if (mat.isHair) {
|
|
1285
|
-
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
1286
1397
|
const createHairBindGroup = (isOverEyes) => {
|
|
1287
|
-
const
|
|
1398
|
+
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);
|
|
1288
1399
|
return this.device.createBindGroup({
|
|
1289
|
-
label:
|
|
1400
|
+
label: `${prefix}hair ${isOverEyes ? "over eyes" : "over non-eyes"}: ${mat.name}`,
|
|
1290
1401
|
layout: this.mainBindGroupLayout,
|
|
1291
1402
|
entries: [
|
|
1292
1403
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1293
1404
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1294
1405
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1295
1406
|
{ binding: 3, resource: this.materialSampler },
|
|
1296
|
-
{ binding: 4, resource: { buffer:
|
|
1297
|
-
{ binding: 5, resource: { buffer:
|
|
1407
|
+
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1408
|
+
{ binding: 5, resource: { buffer: buf } },
|
|
1298
1409
|
],
|
|
1299
1410
|
});
|
|
1300
1411
|
};
|
|
1301
|
-
|
|
1302
|
-
const bindGroupOverNonEyes = createHairBindGroup(false);
|
|
1303
|
-
this.drawCalls.push({
|
|
1412
|
+
inst.drawCalls.push({
|
|
1304
1413
|
type: "hair-over-eyes",
|
|
1305
1414
|
count: indexCount,
|
|
1306
1415
|
firstIndex: currentIndexOffset,
|
|
1307
|
-
bindGroup:
|
|
1416
|
+
bindGroup: createHairBindGroup(true),
|
|
1308
1417
|
materialName: mat.name,
|
|
1309
1418
|
});
|
|
1310
|
-
|
|
1419
|
+
inst.drawCalls.push({
|
|
1311
1420
|
type: "hair-over-non-eyes",
|
|
1312
1421
|
count: indexCount,
|
|
1313
1422
|
firstIndex: currentIndexOffset,
|
|
1314
|
-
bindGroup:
|
|
1423
|
+
bindGroup: createHairBindGroup(false),
|
|
1315
1424
|
materialName: mat.name,
|
|
1316
1425
|
});
|
|
1317
1426
|
}
|
|
1318
1427
|
else if (isTransparent) {
|
|
1319
|
-
|
|
1320
|
-
type: "transparent",
|
|
1321
|
-
count: indexCount,
|
|
1322
|
-
firstIndex: currentIndexOffset,
|
|
1323
|
-
bindGroup,
|
|
1324
|
-
materialName: mat.name,
|
|
1325
|
-
});
|
|
1428
|
+
inst.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1326
1429
|
}
|
|
1327
1430
|
else {
|
|
1328
|
-
|
|
1329
|
-
type: "opaque",
|
|
1330
|
-
count: indexCount,
|
|
1331
|
-
firstIndex: currentIndexOffset,
|
|
1332
|
-
bindGroup,
|
|
1333
|
-
materialName: mat.name,
|
|
1334
|
-
});
|
|
1431
|
+
inst.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
|
|
1335
1432
|
}
|
|
1336
1433
|
}
|
|
1337
|
-
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1338
1434
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1339
1435
|
const materialUniformData = new Float32Array([
|
|
1340
|
-
mat.edgeColor[0],
|
|
1341
|
-
mat.
|
|
1342
|
-
mat.edgeColor[2],
|
|
1343
|
-
mat.edgeColor[3],
|
|
1344
|
-
mat.edgeSize,
|
|
1345
|
-
0,
|
|
1346
|
-
0,
|
|
1347
|
-
0,
|
|
1436
|
+
mat.edgeColor[0], mat.edgeColor[1], mat.edgeColor[2], mat.edgeColor[3],
|
|
1437
|
+
mat.edgeSize, 0, 0, 0,
|
|
1348
1438
|
]);
|
|
1349
|
-
const
|
|
1439
|
+
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData);
|
|
1350
1440
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1351
|
-
label:
|
|
1441
|
+
label: `${prefix}outline: ${mat.name}`,
|
|
1352
1442
|
layout: this.outlineBindGroupLayout,
|
|
1353
1443
|
entries: [
|
|
1354
1444
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1355
|
-
{ binding: 1, resource: { buffer:
|
|
1356
|
-
{ binding: 2, resource: { buffer:
|
|
1445
|
+
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1446
|
+
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1357
1447
|
],
|
|
1358
1448
|
});
|
|
1359
1449
|
if (indexCount > 0) {
|
|
1360
|
-
const outlineType = mat.isEye
|
|
1361
|
-
|
|
1362
|
-
: mat.isHair
|
|
1363
|
-
? "hair-outline"
|
|
1364
|
-
: isTransparent
|
|
1365
|
-
? "transparent-outline"
|
|
1366
|
-
: "opaque-outline";
|
|
1367
|
-
this.drawCalls.push({
|
|
1368
|
-
type: outlineType,
|
|
1369
|
-
count: indexCount,
|
|
1370
|
-
firstIndex: currentIndexOffset,
|
|
1371
|
-
bindGroup: outlineBindGroup,
|
|
1372
|
-
materialName: mat.name,
|
|
1373
|
-
});
|
|
1450
|
+
const outlineType = mat.isEye ? "eye-outline" : mat.isHair ? "hair-outline" : isTransparent ? "transparent-outline" : "opaque-outline";
|
|
1451
|
+
inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name });
|
|
1374
1452
|
}
|
|
1375
1453
|
}
|
|
1376
1454
|
currentIndexOffset += indexCount;
|
|
1377
1455
|
}
|
|
1456
|
+
for (const d of inst.drawCalls) {
|
|
1457
|
+
if (d.type === "opaque" || d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes")
|
|
1458
|
+
inst.shadowDrawCalls.push(d);
|
|
1459
|
+
}
|
|
1378
1460
|
}
|
|
1379
1461
|
createMaterialUniformBuffer(label, alpha, isOverEyes, diffuseColor, ambientColor, specularColor, shininess) {
|
|
1380
1462
|
const data = new Float32Array(20);
|
|
@@ -1411,8 +1493,8 @@ export class Engine {
|
|
|
1411
1493
|
this.device.queue.writeBuffer(buffer, 0, data);
|
|
1412
1494
|
return buffer;
|
|
1413
1495
|
}
|
|
1414
|
-
shouldRenderDrawCall(drawCall) {
|
|
1415
|
-
return !
|
|
1496
|
+
shouldRenderDrawCall(inst, drawCall) {
|
|
1497
|
+
return !inst.hiddenMaterials.has(drawCall.materialName);
|
|
1416
1498
|
}
|
|
1417
1499
|
async createTextureFromPath(path) {
|
|
1418
1500
|
const cached = this.textureCache.get(path);
|
|
@@ -1446,11 +1528,10 @@ export class Engine {
|
|
|
1446
1528
|
}
|
|
1447
1529
|
}
|
|
1448
1530
|
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1449
|
-
renderEyes(pass, useReflectionPipeline = false) {
|
|
1531
|
+
renderEyes(pass, inst, useReflectionPipeline = false) {
|
|
1450
1532
|
if (useReflectionPipeline) {
|
|
1451
|
-
// For reflections, use the basic reflection pipeline instead of specialized eye pipeline
|
|
1452
1533
|
pass.setPipeline(this.reflectionPipeline);
|
|
1453
|
-
for (const draw of
|
|
1534
|
+
for (const draw of inst.drawCalls) {
|
|
1454
1535
|
if (draw.type === "eye") {
|
|
1455
1536
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1456
1537
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -1460,8 +1541,8 @@ export class Engine {
|
|
|
1460
1541
|
else {
|
|
1461
1542
|
pass.setPipeline(this.eyePipeline);
|
|
1462
1543
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1463
|
-
for (const draw of
|
|
1464
|
-
if (draw.type === "eye" && this.shouldRenderDrawCall(draw)) {
|
|
1544
|
+
for (const draw of inst.drawCalls) {
|
|
1545
|
+
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1465
1546
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1466
1547
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1467
1548
|
}
|
|
@@ -1469,23 +1550,15 @@ export class Engine {
|
|
|
1469
1550
|
}
|
|
1470
1551
|
}
|
|
1471
1552
|
renderGround(pass) {
|
|
1472
|
-
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer)
|
|
1553
|
+
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
1473
1554
|
return;
|
|
1474
|
-
|
|
1475
|
-
if (this.groundReflectionTexture) {
|
|
1555
|
+
if (this.groundMode === "reflection" && this.groundReflectionTexture)
|
|
1476
1556
|
this.renderReflectionTexture();
|
|
1477
|
-
|
|
1478
|
-
pass.setPipeline(this.groundPipeline);
|
|
1557
|
+
pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline);
|
|
1479
1558
|
pass.setVertexBuffer(0, this.groundVertexBuffer);
|
|
1480
1559
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1484
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
// // Restore model index buffer for subsequent rendering
|
|
1488
|
-
// pass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
1560
|
+
pass.setBindGroup(0, this.groundDrawCall.bindGroup);
|
|
1561
|
+
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
|
|
1489
1562
|
}
|
|
1490
1563
|
renderReflectionTexture() {
|
|
1491
1564
|
if (!this.groundReflectionTexture)
|
|
@@ -1515,45 +1588,15 @@ export class Engine {
|
|
|
1515
1588
|
},
|
|
1516
1589
|
};
|
|
1517
1590
|
const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor);
|
|
1518
|
-
|
|
1519
|
-
reflectionPass.setVertexBuffer(0, this.vertexBuffer);
|
|
1520
|
-
reflectionPass.setVertexBuffer(1, this.jointsBuffer);
|
|
1521
|
-
reflectionPass.setVertexBuffer(2, this.weightsBuffer);
|
|
1522
|
-
reflectionPass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1523
|
-
this.writeMirrorTransformedSkinMatrices(mirrorMatrix);
|
|
1524
|
-
reflectionPass.setPipeline(this.reflectionPipeline);
|
|
1525
|
-
for (const draw of this.drawCalls) {
|
|
1526
|
-
if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
|
|
1527
|
-
reflectionPass.setBindGroup(0, draw.bindGroup);
|
|
1528
|
-
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
// Render eyes (using reflection pipeline)
|
|
1532
|
-
this.renderEyes(reflectionPass, true);
|
|
1533
|
-
// Render hair (using reflection pipeline)
|
|
1534
|
-
this.renderHair(reflectionPass, true);
|
|
1535
|
-
// Render transparent objects
|
|
1536
|
-
for (const draw of this.drawCalls) {
|
|
1537
|
-
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
1538
|
-
reflectionPass.setBindGroup(0, draw.bindGroup);
|
|
1539
|
-
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
this.drawOutlines(reflectionPass, true, true);
|
|
1543
|
-
}
|
|
1591
|
+
this.forEachInstance((inst) => this.renderOneModel(reflectionPass, inst, true, mirrorMatrix));
|
|
1544
1592
|
reflectionPass.end();
|
|
1545
|
-
|
|
1546
|
-
const reflectionCommandBuffer = reflectionEncoder.finish();
|
|
1547
|
-
this.device.queue.submit([reflectionCommandBuffer]);
|
|
1548
|
-
// Restore original skin matrices
|
|
1593
|
+
this.device.queue.submit([reflectionEncoder.finish()]);
|
|
1549
1594
|
this.updateSkinMatrices();
|
|
1550
1595
|
}
|
|
1551
|
-
|
|
1552
|
-
renderHair(pass, useReflectionPipeline = false) {
|
|
1596
|
+
renderHair(pass, inst, useReflectionPipeline = false) {
|
|
1553
1597
|
if (useReflectionPipeline) {
|
|
1554
|
-
// For reflections, use the basic reflection pipeline for all hair
|
|
1555
1598
|
pass.setPipeline(this.reflectionPipeline);
|
|
1556
|
-
for (const draw of
|
|
1599
|
+
for (const draw of inst.drawCalls) {
|
|
1557
1600
|
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
1558
1601
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1559
1602
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -1561,19 +1604,17 @@ export class Engine {
|
|
|
1561
1604
|
}
|
|
1562
1605
|
return;
|
|
1563
1606
|
}
|
|
1564
|
-
|
|
1565
|
-
const hasHair = this.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(d));
|
|
1607
|
+
const hasHair = inst.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d));
|
|
1566
1608
|
if (hasHair) {
|
|
1567
1609
|
pass.setPipeline(this.hairDepthPipeline);
|
|
1568
|
-
for (const draw of
|
|
1569
|
-
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(draw)) {
|
|
1610
|
+
for (const draw of inst.drawCalls) {
|
|
1611
|
+
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, draw)) {
|
|
1570
1612
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1571
1613
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1572
1614
|
}
|
|
1573
1615
|
}
|
|
1574
1616
|
}
|
|
1575
|
-
|
|
1576
|
-
const hairOverEyes = this.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(d));
|
|
1617
|
+
const hairOverEyes = inst.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(inst, d));
|
|
1577
1618
|
if (hairOverEyes.length > 0) {
|
|
1578
1619
|
pass.setPipeline(this.hairPipelineOverEyes);
|
|
1579
1620
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
@@ -1582,7 +1623,7 @@ export class Engine {
|
|
|
1582
1623
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1583
1624
|
}
|
|
1584
1625
|
}
|
|
1585
|
-
const hairOverNonEyes =
|
|
1626
|
+
const hairOverNonEyes = inst.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(inst, d));
|
|
1586
1627
|
if (hairOverNonEyes.length > 0) {
|
|
1587
1628
|
pass.setPipeline(this.hairPipelineOverNonEyes);
|
|
1588
1629
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
@@ -1591,8 +1632,7 @@ export class Engine {
|
|
|
1591
1632
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1592
1633
|
}
|
|
1593
1634
|
}
|
|
1594
|
-
|
|
1595
|
-
const hairOutlines = this.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(d));
|
|
1635
|
+
const hairOutlines = inst.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(inst, d));
|
|
1596
1636
|
if (hairOutlines.length > 0) {
|
|
1597
1637
|
pass.setPipeline(this.hairOutlinePipeline);
|
|
1598
1638
|
for (const draw of hairOutlines) {
|
|
@@ -1602,237 +1642,203 @@ export class Engine {
|
|
|
1602
1642
|
}
|
|
1603
1643
|
}
|
|
1604
1644
|
performRaycast(screenX, screenY) {
|
|
1605
|
-
if (!this.
|
|
1645
|
+
if (!this.onRaycast || this.modelInstances.size === 0) {
|
|
1646
|
+
this.onRaycast?.("", null, screenX, screenY);
|
|
1606
1647
|
return;
|
|
1607
|
-
|
|
1608
|
-
if (materials.length === 0)
|
|
1609
|
-
return;
|
|
1610
|
-
// Get camera matrices
|
|
1648
|
+
}
|
|
1611
1649
|
const viewMatrix = this.camera.getViewMatrix();
|
|
1612
1650
|
const projectionMatrix = this.camera.getProjectionMatrix();
|
|
1613
|
-
|
|
1614
|
-
const canvas = this.canvas;
|
|
1615
|
-
const rect = canvas.getBoundingClientRect();
|
|
1616
|
-
// Convert to clip space (-1 to 1)
|
|
1651
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1617
1652
|
const clipX = (screenX / rect.width) * 2 - 1;
|
|
1618
|
-
const clipY = 1 - (screenY / rect.height) * 2;
|
|
1619
|
-
// Create ray in clip space at near and far planes
|
|
1620
|
-
const clipNear = new Vec3(clipX, clipY, -1); // Near plane
|
|
1621
|
-
const clipFar = new Vec3(clipX, clipY, 1); // Far plane
|
|
1622
|
-
// Transform to world space using inverse view-projection matrix
|
|
1653
|
+
const clipY = 1 - (screenY / rect.height) * 2;
|
|
1623
1654
|
const viewProjMatrix = projectionMatrix.multiply(viewMatrix);
|
|
1624
1655
|
const inverseViewProj = viewProjMatrix.inverse();
|
|
1625
|
-
// Transform point through 4x4 matrix with perspective division
|
|
1626
1656
|
const transformPoint = (matrix, point) => {
|
|
1627
1657
|
const m = matrix.values;
|
|
1628
1658
|
const x = point.x, y = point.y, z = point.z;
|
|
1629
|
-
// Compute transformed point (matrix * vec4(point, 1.0))
|
|
1630
1659
|
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]);
|
|
1631
|
-
// Perspective division
|
|
1632
1660
|
const w = m[3] * x + m[7] * y + m[11] * z + m[15];
|
|
1633
|
-
|
|
1634
|
-
return result.scale(invW);
|
|
1661
|
+
return result.scale(w !== 0 ? 1 / w : 1);
|
|
1635
1662
|
};
|
|
1636
|
-
const worldNear = transformPoint(inverseViewProj,
|
|
1637
|
-
const worldFar = transformPoint(inverseViewProj,
|
|
1638
|
-
// Create ray from camera position through the clicked point
|
|
1663
|
+
const worldNear = transformPoint(inverseViewProj, new Vec3(clipX, clipY, -1));
|
|
1664
|
+
const worldFar = transformPoint(inverseViewProj, new Vec3(clipX, clipY, 1));
|
|
1639
1665
|
const rayOrigin = this.camera.getPosition();
|
|
1640
1666
|
const rayDirection = worldFar.subtract(worldNear).normalize();
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const skinMatrices = this.currentModel.getSkinMatrices();
|
|
1660
|
-
// Helper function to transform point by 4x4 matrix
|
|
1661
|
-
const transformByMatrix = (matrix, offset, point) => {
|
|
1662
|
-
const m = matrix;
|
|
1663
|
-
const x = point.x, y = point.y, z = point.z;
|
|
1664
|
-
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]);
|
|
1665
|
-
};
|
|
1667
|
+
const transformByMatrix = (matrix, offset, point) => {
|
|
1668
|
+
const m = matrix, x = point.x, y = point.y, z = point.z;
|
|
1669
|
+
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]);
|
|
1670
|
+
};
|
|
1671
|
+
let closest = null;
|
|
1672
|
+
const maxDistance = 1000;
|
|
1673
|
+
this.forEachInstance((inst) => {
|
|
1674
|
+
const model = inst.model;
|
|
1675
|
+
const materials = model.getMaterials();
|
|
1676
|
+
if (materials.length === 0)
|
|
1677
|
+
return;
|
|
1678
|
+
const baseVertices = model.getVertices();
|
|
1679
|
+
const indices = model.getIndices();
|
|
1680
|
+
const skinning = model.getSkinning();
|
|
1681
|
+
if (!baseVertices?.length || !indices || !skinning)
|
|
1682
|
+
return;
|
|
1683
|
+
const vertices = new Float32Array(baseVertices.length);
|
|
1684
|
+
const skinMatrices = model.getSkinMatrices();
|
|
1666
1685
|
for (let i = 0; i < baseVertices.length; i += 8) {
|
|
1667
|
-
const vertexIndex =
|
|
1686
|
+
const vertexIndex = i / 8;
|
|
1668
1687
|
const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2]);
|
|
1669
|
-
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
skinning.joints[vertexIndex * 4 + 3],
|
|
1675
|
-
];
|
|
1676
|
-
const weights = [
|
|
1677
|
-
skinning.weights[vertexIndex * 4],
|
|
1678
|
-
skinning.weights[vertexIndex * 4 + 1],
|
|
1679
|
-
skinning.weights[vertexIndex * 4 + 2],
|
|
1680
|
-
skinning.weights[vertexIndex * 4 + 3],
|
|
1681
|
-
];
|
|
1682
|
-
// Normalize weights (same as shader)
|
|
1683
|
-
const weightSum = weights[0] + weights[1] + weights[2] + weights[3];
|
|
1684
|
-
const invWeightSum = weightSum > 0.0001 ? 1.0 / weightSum : 1.0;
|
|
1685
|
-
const normalizedWeights = weightSum > 0.0001 ? weights.map((w) => w * invWeightSum) : [1.0, 0.0, 0.0, 0.0];
|
|
1686
|
-
// Apply skinning transformation (same as shader)
|
|
1687
|
-
let skinnedPosition = new Vec3(0, 0, 0);
|
|
1688
|
+
const j0 = skinning.joints[vertexIndex * 4], j1 = skinning.joints[vertexIndex * 4 + 1], j2 = skinning.joints[vertexIndex * 4 + 2], j3 = skinning.joints[vertexIndex * 4 + 3];
|
|
1689
|
+
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;
|
|
1690
|
+
const ws = w0 + w1 + w2 + w3;
|
|
1691
|
+
const nw = ws > 0.0001 ? [w0 / ws, w1 / ws, w2 / ws, w3 / ws] : [1, 0, 0, 0];
|
|
1692
|
+
let sp = new Vec3(0, 0, 0);
|
|
1688
1693
|
for (let j = 0; j < 4; j++) {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
skinnedPosition = skinnedPosition.add(transformed.scale(weight));
|
|
1694
|
-
}
|
|
1694
|
+
if (nw[j] <= 0)
|
|
1695
|
+
continue;
|
|
1696
|
+
const transformed = transformByMatrix(skinMatrices, [j0, j1, j2, j3][j] * 16, position);
|
|
1697
|
+
sp = sp.add(transformed.scale(nw[j]));
|
|
1695
1698
|
}
|
|
1696
|
-
|
|
1697
|
-
vertices[i] =
|
|
1698
|
-
vertices[i +
|
|
1699
|
-
vertices[i +
|
|
1700
|
-
vertices[i +
|
|
1701
|
-
vertices[i +
|
|
1702
|
-
vertices[i +
|
|
1703
|
-
vertices[i +
|
|
1704
|
-
vertices[i + 7] = baseVertices[i + 7]; // UV Y
|
|
1699
|
+
vertices[i] = sp.x;
|
|
1700
|
+
vertices[i + 1] = sp.y;
|
|
1701
|
+
vertices[i + 2] = sp.z;
|
|
1702
|
+
vertices[i + 3] = baseVertices[i + 3];
|
|
1703
|
+
vertices[i + 4] = baseVertices[i + 4];
|
|
1704
|
+
vertices[i + 5] = baseVertices[i + 5];
|
|
1705
|
+
vertices[i + 6] = baseVertices[i + 6];
|
|
1706
|
+
vertices[i + 7] = baseVertices[i + 7];
|
|
1705
1707
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2]);
|
|
1720
|
-
const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2]);
|
|
1721
|
-
// Find which material this triangle belongs to
|
|
1722
|
-
// Each material has mat.vertexCount indices (3 per triangle)
|
|
1723
|
-
let triangleMaterialIndex = -1;
|
|
1724
|
-
let indexOffset = 0;
|
|
1725
|
-
for (let matIdx = 0; matIdx < materials.length; matIdx++) {
|
|
1726
|
-
const mat = materials[matIdx];
|
|
1727
|
-
if (i >= indexOffset && i < indexOffset + mat.vertexCount) {
|
|
1728
|
-
triangleMaterialIndex = matIdx;
|
|
1729
|
-
break;
|
|
1708
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
1709
|
+
const idx0 = indices[i] * 8, idx1 = indices[i + 1] * 8, idx2 = indices[i + 2] * 8;
|
|
1710
|
+
const v0 = new Vec3(vertices[idx0], vertices[idx0 + 1], vertices[idx0 + 2]);
|
|
1711
|
+
const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2]);
|
|
1712
|
+
const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2]);
|
|
1713
|
+
let triangleMaterialIndex = -1;
|
|
1714
|
+
let indexOffset = 0;
|
|
1715
|
+
for (let matIdx = 0; matIdx < materials.length; matIdx++) {
|
|
1716
|
+
if (i >= indexOffset && i < indexOffset + materials[matIdx].vertexCount) {
|
|
1717
|
+
triangleMaterialIndex = matIdx;
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
indexOffset += materials[matIdx].vertexCount;
|
|
1730
1721
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const f = 1.0 / a;
|
|
1746
|
-
const s = rayOrigin.subtract(v0);
|
|
1747
|
-
const u = f * s.dot(h);
|
|
1748
|
-
if (u < 0.0 || u > 1.0)
|
|
1749
|
-
continue;
|
|
1750
|
-
const q = s.cross(edge1);
|
|
1751
|
-
const v = f * rayDirection.dot(q);
|
|
1752
|
-
if (v < 0.0 || u + v > 1.0)
|
|
1753
|
-
continue;
|
|
1754
|
-
// At this point we have a hit
|
|
1755
|
-
const t = f * edge2.dot(q);
|
|
1756
|
-
if (t > 0.0001 && t < maxDistance) {
|
|
1757
|
-
// Backface culling: only consider front-facing triangles
|
|
1722
|
+
if (triangleMaterialIndex === -1)
|
|
1723
|
+
continue;
|
|
1724
|
+
const edge1 = v1.subtract(v0), edge2 = v2.subtract(v0), h = rayDirection.cross(edge2), a = edge1.dot(h);
|
|
1725
|
+
if (Math.abs(a) < 0.0001)
|
|
1726
|
+
continue;
|
|
1727
|
+
const f = 1 / a, s = rayOrigin.subtract(v0), u = f * s.dot(h);
|
|
1728
|
+
if (u < 0 || u > 1)
|
|
1729
|
+
continue;
|
|
1730
|
+
const q = s.cross(edge1), v = f * rayDirection.dot(q);
|
|
1731
|
+
if (v < 0 || u + v > 1)
|
|
1732
|
+
continue;
|
|
1733
|
+
const t = f * edge2.dot(q);
|
|
1734
|
+
if (t <= 0.0001 || t >= maxDistance)
|
|
1735
|
+
continue;
|
|
1758
1736
|
const triangleNormal = edge1.cross(edge2).normalize();
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
materialName: materials[triangleMaterialIndex].name,
|
|
1764
|
-
distance: t,
|
|
1765
|
-
};
|
|
1766
|
-
}
|
|
1737
|
+
if (triangleNormal.dot(rayDirection) >= 0)
|
|
1738
|
+
continue;
|
|
1739
|
+
if (!closest || t < closest.distance) {
|
|
1740
|
+
closest = { modelName: inst.name, materialName: materials[triangleMaterialIndex].name, distance: t };
|
|
1767
1741
|
}
|
|
1768
1742
|
}
|
|
1769
|
-
}
|
|
1770
|
-
// Call the callback with the result
|
|
1743
|
+
});
|
|
1771
1744
|
if (this.onRaycast) {
|
|
1772
|
-
|
|
1745
|
+
const hit = closest;
|
|
1746
|
+
this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY);
|
|
1773
1747
|
}
|
|
1774
1748
|
}
|
|
1775
|
-
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent
|
|
1776
1749
|
render() {
|
|
1777
|
-
if (this.multisampleTexture
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
this.vertexBufferNeedsUpdate = true;
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
// Update vertex buffer if morphs changed
|
|
1791
|
-
if (this.vertexBufferNeedsUpdate) {
|
|
1792
|
-
this.updateVertexBuffer();
|
|
1793
|
-
this.vertexBufferNeedsUpdate = false;
|
|
1794
|
-
}
|
|
1795
|
-
// Update skin matrices buffer
|
|
1750
|
+
if (!this.multisampleTexture || !this.camera || !this.device)
|
|
1751
|
+
return;
|
|
1752
|
+
const currentTime = performance.now();
|
|
1753
|
+
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016;
|
|
1754
|
+
this.lastFrameTime = currentTime;
|
|
1755
|
+
this.updateCameraUniforms();
|
|
1756
|
+
this.updateRenderTarget();
|
|
1757
|
+
const hasModels = this.modelInstances.size > 0;
|
|
1758
|
+
if (hasModels) {
|
|
1759
|
+
this.updateInstances(deltaTime);
|
|
1796
1760
|
this.updateSkinMatrices();
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1761
|
+
}
|
|
1762
|
+
if (this.groundMode === "shadow")
|
|
1763
|
+
this.updateShadowLightVP();
|
|
1764
|
+
const encoder = this.device.createCommandEncoder();
|
|
1765
|
+
if (hasModels && this.groundMode === "shadow" && this.shadowMapDepthView) {
|
|
1766
|
+
const sp = encoder.beginRenderPass({
|
|
1767
|
+
colorAttachments: [],
|
|
1768
|
+
depthStencilAttachment: {
|
|
1769
|
+
view: this.shadowMapDepthView,
|
|
1770
|
+
depthClearValue: 1.0,
|
|
1771
|
+
depthLoadOp: "clear",
|
|
1772
|
+
depthStoreOp: "store",
|
|
1773
|
+
},
|
|
1774
|
+
});
|
|
1775
|
+
sp.setPipeline(this.shadowDepthPipeline);
|
|
1776
|
+
this.forEachInstance((inst) => this.drawInstanceShadow(sp, inst));
|
|
1777
|
+
sp.end();
|
|
1778
|
+
}
|
|
1779
|
+
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1780
|
+
if (hasModels)
|
|
1781
|
+
this.forEachInstance((inst) => this.renderOneModel(pass, inst, false));
|
|
1782
|
+
if (this.groundHasReflections)
|
|
1783
|
+
this.renderGround(pass);
|
|
1784
|
+
pass.end();
|
|
1785
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1786
|
+
this.updateStats(performance.now() - currentTime);
|
|
1787
|
+
}
|
|
1788
|
+
drawInstanceShadow(sp, inst) {
|
|
1789
|
+
sp.setBindGroup(0, inst.shadowBindGroup);
|
|
1790
|
+
sp.setVertexBuffer(0, inst.vertexBuffer);
|
|
1791
|
+
sp.setVertexBuffer(1, inst.jointsBuffer);
|
|
1792
|
+
sp.setVertexBuffer(2, inst.weightsBuffer);
|
|
1793
|
+
sp.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1794
|
+
for (const draw of inst.shadowDrawCalls) {
|
|
1795
|
+
if (this.shouldRenderDrawCall(inst, draw))
|
|
1796
|
+
sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
renderOneModel(pass, inst, useReflection, mirrorMatrix) {
|
|
1800
|
+
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
1801
|
+
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
1802
|
+
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
1803
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
1804
|
+
if (useReflection && mirrorMatrix) {
|
|
1805
|
+
this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix);
|
|
1806
|
+
pass.setPipeline(this.reflectionPipeline);
|
|
1807
|
+
for (const draw of inst.drawCalls) {
|
|
1808
|
+
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1809
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1810
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1812
1811
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
this.
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
for (const draw of this.drawCalls) {
|
|
1821
|
-
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
1822
|
-
pass.setBindGroup(0, draw.bindGroup);
|
|
1823
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1824
|
-
}
|
|
1812
|
+
}
|
|
1813
|
+
this.renderEyes(pass, inst, true);
|
|
1814
|
+
this.renderHair(pass, inst, true);
|
|
1815
|
+
for (const draw of inst.drawCalls) {
|
|
1816
|
+
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1817
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1818
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1825
1819
|
}
|
|
1826
|
-
this.drawOutlines(pass, true);
|
|
1827
1820
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1821
|
+
this.drawOutlines(pass, inst, true, true);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
pass.setPipeline(this.modelPipeline);
|
|
1825
|
+
for (const draw of inst.drawCalls) {
|
|
1826
|
+
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1827
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1828
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
this.renderEyes(pass, inst, false);
|
|
1832
|
+
this.drawOutlines(pass, inst, false);
|
|
1833
|
+
this.renderHair(pass, inst, false);
|
|
1834
|
+
pass.setPipeline(this.modelPipeline);
|
|
1835
|
+
for (const draw of inst.drawCalls) {
|
|
1836
|
+
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1837
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1838
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1831
1839
|
}
|
|
1832
|
-
pass.end();
|
|
1833
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1834
|
-
this.updateStats(performance.now() - currentTime);
|
|
1835
1840
|
}
|
|
1841
|
+
this.drawOutlines(pass, inst, true);
|
|
1836
1842
|
}
|
|
1837
1843
|
updateCameraUniforms() {
|
|
1838
1844
|
const viewMatrix = this.camera.getViewMatrix();
|
|
@@ -1856,22 +1862,18 @@ export class Engine {
|
|
|
1856
1862
|
}
|
|
1857
1863
|
}
|
|
1858
1864
|
updateSkinMatrices() {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
// Increment version to invalidate cached skinned vertices
|
|
1864
|
-
this.skinMatricesVersion++;
|
|
1865
|
+
this.forEachInstance((inst) => {
|
|
1866
|
+
const skinMatrices = inst.model.getSkinMatrices();
|
|
1867
|
+
this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, skinMatrices.buffer, skinMatrices.byteOffset, skinMatrices.byteLength);
|
|
1868
|
+
});
|
|
1865
1869
|
}
|
|
1866
|
-
drawOutlines(pass, transparent, useReflectionPipeline = false) {
|
|
1867
|
-
if (useReflectionPipeline)
|
|
1868
|
-
// Skip outlines for reflections - not critical for the effect
|
|
1870
|
+
drawOutlines(pass, inst, transparent, useReflectionPipeline = false) {
|
|
1871
|
+
if (useReflectionPipeline)
|
|
1869
1872
|
return;
|
|
1870
|
-
}
|
|
1871
1873
|
pass.setPipeline(this.outlinePipeline);
|
|
1872
1874
|
const outlineType = transparent ? "transparent-outline" : "opaque-outline";
|
|
1873
|
-
for (const draw of
|
|
1874
|
-
if (draw.type === outlineType && this.shouldRenderDrawCall(draw)) {
|
|
1875
|
+
for (const draw of inst.drawCalls) {
|
|
1876
|
+
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
1875
1877
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1876
1878
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1877
1879
|
}
|
|
@@ -1921,22 +1923,19 @@ export class Engine {
|
|
|
1921
1923
|
1,
|
|
1922
1924
|
]));
|
|
1923
1925
|
}
|
|
1924
|
-
writeMirrorTransformedSkinMatrices(mirrorMatrix) {
|
|
1925
|
-
|
|
1926
|
-
return;
|
|
1927
|
-
const originalMatrices = this.currentModel.getSkinMatrices();
|
|
1926
|
+
writeMirrorTransformedSkinMatrices(inst, mirrorMatrix) {
|
|
1927
|
+
const originalMatrices = inst.model.getSkinMatrices();
|
|
1928
1928
|
const transformedMatrices = new Float32Array(originalMatrices.length);
|
|
1929
1929
|
for (let i = 0; i < originalMatrices.length; i += 16) {
|
|
1930
1930
|
const boneMatrixValues = new Float32Array(16);
|
|
1931
|
-
for (let j = 0; j < 16; j++)
|
|
1931
|
+
for (let j = 0; j < 16; j++)
|
|
1932
1932
|
boneMatrixValues[j] = originalMatrices[i + j];
|
|
1933
|
-
}
|
|
1934
1933
|
const boneMatrix = new Mat4(boneMatrixValues);
|
|
1935
1934
|
const transformed = mirrorMatrix.multiply(boneMatrix);
|
|
1936
|
-
for (let j = 0; j < 16; j++)
|
|
1935
|
+
for (let j = 0; j < 16; j++)
|
|
1937
1936
|
transformedMatrices[i + j] = transformed.values[j];
|
|
1938
|
-
}
|
|
1939
1937
|
}
|
|
1940
|
-
this.device.queue.writeBuffer(
|
|
1938
|
+
this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices);
|
|
1941
1939
|
}
|
|
1942
1940
|
}
|
|
1941
|
+
Engine.instance = null;
|