reze-engine 0.7.0 → 0.8.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 +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 +29 -24
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +292 -181
- 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 +2 -48
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +43 -72
- 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 +315 -220
- package/src/ik-solver.ts +1 -11
- package/src/index.ts +3 -4
- package/src/math.ts +27 -0
- package/src/model.ts +43 -74
- package/src/pmx-loader.ts +1 -1
package/dist/engine.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { Mat4, Vec3 } from "./math";
|
|
3
|
-
import { PmxLoader } from "./pmx-loader";
|
|
4
3
|
export const DEFAULT_ENGINE_OPTIONS = {
|
|
5
4
|
ambientColor: new Vec3(0.88, 0.88, 0.88),
|
|
6
5
|
directionalLightIntensity: 0.24,
|
|
@@ -12,8 +11,15 @@ export const DEFAULT_ENGINE_OPTIONS = {
|
|
|
12
11
|
onRaycast: undefined,
|
|
13
12
|
disableIK: false,
|
|
14
13
|
disablePhysics: false,
|
|
14
|
+
multisampleCount: 4,
|
|
15
15
|
};
|
|
16
16
|
export class Engine {
|
|
17
|
+
static getInstance() {
|
|
18
|
+
if (!Engine.instance) {
|
|
19
|
+
throw new Error("Engine not ready: create Engine, await init(), then load models via Model.loadPmx().");
|
|
20
|
+
}
|
|
21
|
+
return Engine.instance;
|
|
22
|
+
}
|
|
17
23
|
constructor(canvas, options) {
|
|
18
24
|
this.cameraMatrixData = new Float32Array(36);
|
|
19
25
|
this.lightData = new Float32Array(64);
|
|
@@ -23,6 +29,12 @@ export class Engine {
|
|
|
23
29
|
// Constants
|
|
24
30
|
this.STENCIL_EYE_VALUE = 1;
|
|
25
31
|
this.groundHasReflections = false;
|
|
32
|
+
this.groundMode = "reflection";
|
|
33
|
+
this.shadowLightVPMatrix = new Float32Array(16);
|
|
34
|
+
this.shadowDrawCalls = [];
|
|
35
|
+
this.shadowVPLightX = Number.NaN;
|
|
36
|
+
this.shadowVPLightY = Number.NaN;
|
|
37
|
+
this.shadowVPLightZ = Number.NaN;
|
|
26
38
|
this.cachedSkinMatricesVersion = -1;
|
|
27
39
|
this.skinMatricesVersion = 0;
|
|
28
40
|
// Double-tap detection
|
|
@@ -84,6 +96,7 @@ export class Engine {
|
|
|
84
96
|
}
|
|
85
97
|
};
|
|
86
98
|
this.canvas = canvas;
|
|
99
|
+
this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount;
|
|
87
100
|
if (options) {
|
|
88
101
|
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor;
|
|
89
102
|
this.directionalLightIntensity =
|
|
@@ -121,6 +134,7 @@ export class Engine {
|
|
|
121
134
|
this.setupLighting();
|
|
122
135
|
this.createPipelines();
|
|
123
136
|
this.setupResize();
|
|
137
|
+
Engine.instance = this;
|
|
124
138
|
}
|
|
125
139
|
createRenderPipeline(config) {
|
|
126
140
|
return this.device.createRenderPipeline({
|
|
@@ -379,15 +393,14 @@ export class Engine {
|
|
|
379
393
|
depthCompare: "less-equal",
|
|
380
394
|
},
|
|
381
395
|
});
|
|
382
|
-
// Create ground/reflection pipeline with reflection texture support
|
|
383
396
|
this.groundBindGroupLayout = this.device.createBindGroupLayout({
|
|
384
397
|
label: "ground bind group layout",
|
|
385
398
|
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" } },
|
|
399
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
400
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
401
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
402
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
403
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
391
404
|
],
|
|
392
405
|
});
|
|
393
406
|
const groundPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -397,97 +410,41 @@ export class Engine {
|
|
|
397
410
|
const groundShaderModule = this.device.createShaderModule({
|
|
398
411
|
label: "ground shaders",
|
|
399
412
|
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
|
-
|
|
413
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
414
|
+
struct Light { direction: vec4f, color: vec4f, };
|
|
415
|
+
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
416
|
+
struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
|
|
426
417
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
427
418
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
428
419
|
@group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
|
|
429
420
|
@group(0) @binding(3) var reflectionSampler: sampler;
|
|
430
421
|
@group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
|
|
431
|
-
|
|
432
422
|
struct VertexOutput {
|
|
433
|
-
@builtin(position) position: vec4f,
|
|
434
|
-
@location(0) normal: vec3f,
|
|
435
|
-
@location(1) uv: vec2f,
|
|
436
|
-
@location(2) worldPos: vec3f,
|
|
423
|
+
@builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
|
|
437
424
|
};
|
|
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;
|
|
425
|
+
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
|
|
426
|
+
var o: VertexOutput;
|
|
427
|
+
o.worldPos = position;
|
|
428
|
+
o.position = camera.projection * camera.view * vec4f(position, 1.0);
|
|
429
|
+
o.normal = normal; o.uv = uv; return o;
|
|
451
430
|
}
|
|
452
|
-
|
|
453
431
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
454
432
|
let n = normalize(input.normal);
|
|
455
|
-
|
|
433
|
+
let centerDist = length(input.worldPos.xz);
|
|
434
|
+
let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
|
|
435
|
+
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
456
436
|
let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
|
|
457
437
|
let ndcPos = clipPos.xyz / clipPos.w;
|
|
458
|
-
|
|
459
|
-
|
|
438
|
+
let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
|
|
460
439
|
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
|
|
440
|
+
let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
|
|
441
|
+
let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
|
|
442
|
+
let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
|
|
443
|
+
let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
|
|
444
|
+
var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
|
|
481
445
|
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);
|
|
446
|
+
let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
|
|
447
|
+
return vec4f(finalColor * lightAccum, edgeFade);
|
|
491
448
|
}
|
|
492
449
|
`,
|
|
493
450
|
});
|
|
@@ -498,12 +455,123 @@ export class Engine {
|
|
|
498
455
|
vertexBuffers: fullVertexBuffers,
|
|
499
456
|
fragmentTarget: standardBlend,
|
|
500
457
|
cullMode: "back",
|
|
458
|
+
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
459
|
+
});
|
|
460
|
+
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
461
|
+
size: 64,
|
|
462
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
463
|
+
});
|
|
464
|
+
const shadowBindGroupLayout = this.device.createBindGroupLayout({
|
|
465
|
+
label: "shadow depth bind layout",
|
|
466
|
+
entries: [
|
|
467
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
468
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
469
|
+
],
|
|
470
|
+
});
|
|
471
|
+
const shadowShader = this.device.createShaderModule({
|
|
472
|
+
label: "shadow depth",
|
|
473
|
+
code: /* wgsl */ `
|
|
474
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
475
|
+
@group(0) @binding(0) var<uniform> lp: LightVP;
|
|
476
|
+
@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
|
|
477
|
+
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
|
|
478
|
+
@location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
|
|
479
|
+
let pos4 = vec4f(position, 1.0);
|
|
480
|
+
let ws = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
481
|
+
let inv = select(1.0, 1.0 / ws, ws > 0.0001);
|
|
482
|
+
let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
|
|
483
|
+
var sp = vec4f(0.0);
|
|
484
|
+
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
485
|
+
return lp.viewProj * vec4f(sp.xyz, 1.0);
|
|
486
|
+
}
|
|
487
|
+
`,
|
|
488
|
+
});
|
|
489
|
+
this.shadowDepthPipeline = this.device.createRenderPipeline({
|
|
490
|
+
label: "shadow depth pipeline",
|
|
491
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [shadowBindGroupLayout] }),
|
|
492
|
+
vertex: { module: shadowShader, entryPoint: "vs", buffers: fullVertexBuffers },
|
|
493
|
+
primitive: { cullMode: "none" },
|
|
501
494
|
depthStencil: {
|
|
502
|
-
format: "
|
|
495
|
+
format: "depth32float",
|
|
503
496
|
depthWriteEnabled: true,
|
|
504
497
|
depthCompare: "less-equal",
|
|
498
|
+
depthBias: 2,
|
|
499
|
+
depthBiasSlopeScale: 1.5,
|
|
500
|
+
depthBiasClamp: 0,
|
|
505
501
|
},
|
|
506
502
|
});
|
|
503
|
+
this.shadowComparisonSampler = this.device.createSampler({
|
|
504
|
+
compare: "less",
|
|
505
|
+
magFilter: "linear",
|
|
506
|
+
minFilter: "linear",
|
|
507
|
+
});
|
|
508
|
+
this.groundShadowBindGroupLayout = this.device.createBindGroupLayout({
|
|
509
|
+
label: "ground shadow layout",
|
|
510
|
+
entries: [
|
|
511
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
512
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
513
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
514
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
515
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
516
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
517
|
+
],
|
|
518
|
+
});
|
|
519
|
+
const groundShadowShader = this.device.createShaderModule({
|
|
520
|
+
label: "ground shadow",
|
|
521
|
+
code: /* wgsl */ `
|
|
522
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
523
|
+
struct Light { direction: vec4f, color: vec4f, };
|
|
524
|
+
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
525
|
+
struct GroundShadowMat { diffuseColor: vec3f, fadeStart: f32, fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, _y: f32, };
|
|
526
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
527
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
528
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
529
|
+
@group(0) @binding(2) var shadowMap: texture_depth_2d;
|
|
530
|
+
@group(0) @binding(3) var shadowSampler: sampler_comparison;
|
|
531
|
+
@group(0) @binding(4) var<uniform> material: GroundShadowMat;
|
|
532
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
533
|
+
struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
|
|
534
|
+
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
|
|
535
|
+
var o: VO; o.worldPos = position; o.normal = normal;
|
|
536
|
+
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
537
|
+
}
|
|
538
|
+
@fragment fn fs(i: VO) -> @location(0) vec4f {
|
|
539
|
+
let n = normalize(i.normal);
|
|
540
|
+
let centerDist = length(i.worldPos.xz);
|
|
541
|
+
let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));
|
|
542
|
+
let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);
|
|
543
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
544
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
545
|
+
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
546
|
+
let st = material.pcfTexel;
|
|
547
|
+
let compareZ = ndc.z - 0.0035;
|
|
548
|
+
var vis = 0.0;
|
|
549
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, -st), compareZ);
|
|
550
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(0.0, -st), compareZ);
|
|
551
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, -st), compareZ);
|
|
552
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, 0.0), compareZ);
|
|
553
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c, compareZ);
|
|
554
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, 0.0), compareZ);
|
|
555
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, st), compareZ);
|
|
556
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(0.0, st), compareZ);
|
|
557
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, st), compareZ);
|
|
558
|
+
vis *= 0.1111111;
|
|
559
|
+
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);
|
|
560
|
+
let dark = (1.0 - vis) * material.shadowStrength;
|
|
561
|
+
let lit = material.diffuseColor * sun * (1.0 - dark * 0.65);
|
|
562
|
+
return vec4f(lit * edgeFade, edgeFade);
|
|
563
|
+
}
|
|
564
|
+
`,
|
|
565
|
+
});
|
|
566
|
+
this.groundShadowPipeline = this.createRenderPipeline({
|
|
567
|
+
label: "ground shadow pipeline",
|
|
568
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
|
|
569
|
+
shaderModule: groundShadowShader,
|
|
570
|
+
vertexBuffers: fullVertexBuffers,
|
|
571
|
+
fragmentTarget: standardBlend,
|
|
572
|
+
cullMode: "back",
|
|
573
|
+
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
574
|
+
});
|
|
507
575
|
// Create reflection pipeline (multisampled version for higher quality)
|
|
508
576
|
this.reflectionPipeline = this.createRenderPipeline({
|
|
509
577
|
label: "reflection pipeline",
|
|
@@ -894,50 +962,33 @@ export class Engine {
|
|
|
894
962
|
reflectionTextureSize: 1024,
|
|
895
963
|
fadeStart: 5.0,
|
|
896
964
|
fadeEnd: 60.0,
|
|
965
|
+
mode: "reflection",
|
|
966
|
+
shadowMapSize: 4096,
|
|
967
|
+
shadowStrength: 1.0,
|
|
897
968
|
...options,
|
|
898
969
|
};
|
|
899
|
-
|
|
970
|
+
this.groundMode = opts.mode;
|
|
971
|
+
this.drawCalls = this.drawCalls.filter((d) => d.type !== "ground");
|
|
900
972
|
this.createGroundGeometry(opts.width, opts.height);
|
|
901
|
-
|
|
902
|
-
|
|
973
|
+
if (opts.mode === "reflection") {
|
|
974
|
+
this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd);
|
|
975
|
+
this.createReflectionTexture(opts.reflectionTextureSize);
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength);
|
|
979
|
+
}
|
|
903
980
|
this.groundHasReflections = true;
|
|
904
981
|
this.drawCalls.push({
|
|
905
982
|
type: "ground",
|
|
906
|
-
count: 6,
|
|
983
|
+
count: 6,
|
|
907
984
|
firstIndex: 0,
|
|
908
|
-
bindGroup: this.groundReflectionBindGroup,
|
|
985
|
+
bindGroup: (opts.mode === "reflection" ? this.groundReflectionBindGroup : this.groundShadowBindGroup),
|
|
909
986
|
materialName: "Ground",
|
|
910
987
|
});
|
|
911
988
|
}
|
|
912
989
|
updateLightBuffer() {
|
|
913
990
|
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
|
|
914
991
|
}
|
|
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
992
|
getStats() {
|
|
942
993
|
return { ...this.stats };
|
|
943
994
|
}
|
|
@@ -961,7 +1012,9 @@ export class Engine {
|
|
|
961
1012
|
}
|
|
962
1013
|
dispose() {
|
|
963
1014
|
this.stopRenderLoop();
|
|
964
|
-
this.stopAnimation();
|
|
1015
|
+
this.currentModel?.stopAnimation();
|
|
1016
|
+
if (Engine.instance === this)
|
|
1017
|
+
Engine.instance = null;
|
|
965
1018
|
if (this.camera)
|
|
966
1019
|
this.camera.detachControl();
|
|
967
1020
|
// Remove raycasting event listeners
|
|
@@ -974,38 +1027,18 @@ export class Engine {
|
|
|
974
1027
|
this.resizeObserver = null;
|
|
975
1028
|
}
|
|
976
1029
|
}
|
|
977
|
-
//
|
|
978
|
-
async
|
|
979
|
-
const pathParts =
|
|
1030
|
+
// Single active model; prefer Model.loadPmx() so load + register stay paired
|
|
1031
|
+
async registerModel(model, pmxPath) {
|
|
1032
|
+
const pathParts = pmxPath.split("/");
|
|
980
1033
|
pathParts.pop();
|
|
981
|
-
|
|
982
|
-
this.modelDir = dir;
|
|
983
|
-
const model = await PmxLoader.load(path);
|
|
984
|
-
// Clear cached skinned vertices when loading a new model
|
|
1034
|
+
this.modelDir = pathParts.join("/") + "/";
|
|
985
1035
|
this.cachedSkinnedVertices = undefined;
|
|
986
1036
|
this.cachedSkinMatricesVersion = -1;
|
|
987
1037
|
await this.setupModelBuffers(model);
|
|
988
1038
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
// moveBones now takes relative translations (VMD-style) by default
|
|
993
|
-
moveBones(boneTranslations, durationMs) {
|
|
994
|
-
this.currentModel?.moveBones(boneTranslations, durationMs);
|
|
995
|
-
}
|
|
996
|
-
resetAllBones() {
|
|
997
|
-
this.currentModel?.resetAllBones();
|
|
998
|
-
}
|
|
999
|
-
resetAllMorphs() {
|
|
1000
|
-
this.currentModel?.resetAllMorphs();
|
|
1001
|
-
}
|
|
1002
|
-
setMorphWeight(name, weight, durationMs) {
|
|
1003
|
-
if (!this.currentModel)
|
|
1004
|
-
return;
|
|
1005
|
-
this.currentModel.setMorphWeight(name, weight, durationMs);
|
|
1006
|
-
if (!durationMs || durationMs === 0) {
|
|
1007
|
-
this.vertexBufferNeedsUpdate = true;
|
|
1008
|
-
}
|
|
1039
|
+
// After morph/vertex edits, queues GPU vertex upload on next frame
|
|
1040
|
+
markVertexBufferDirty() {
|
|
1041
|
+
this.vertexBufferNeedsUpdate = true;
|
|
1009
1042
|
}
|
|
1010
1043
|
setMaterialVisible(name, visible) {
|
|
1011
1044
|
if (visible) {
|
|
@@ -1026,15 +1059,6 @@ export class Engine {
|
|
|
1026
1059
|
isMaterialVisible(name) {
|
|
1027
1060
|
return !this.hiddenMaterials.has(name);
|
|
1028
1061
|
}
|
|
1029
|
-
getBones() {
|
|
1030
|
-
return this.currentModel?.getSkeleton().bones.map((bone) => bone.name) ?? [];
|
|
1031
|
-
}
|
|
1032
|
-
getMorphs() {
|
|
1033
|
-
return this.currentModel?.getMorphing().morphs.map((morph) => morph.name) ?? [];
|
|
1034
|
-
}
|
|
1035
|
-
getMaterials() {
|
|
1036
|
-
return this.currentModel?.getMaterials().map((material) => material.name) ?? [];
|
|
1037
|
-
}
|
|
1038
1062
|
// IK control
|
|
1039
1063
|
get disableIK() {
|
|
1040
1064
|
return this._disableIK;
|
|
@@ -1178,27 +1202,24 @@ export class Engine {
|
|
|
1178
1202
|
});
|
|
1179
1203
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
1180
1204
|
}
|
|
1181
|
-
createGroundMaterialBuffer(diffuseColor
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
0, // padding (4 bytes)
|
|
1192
|
-
0, // padding (4 bytes)
|
|
1193
|
-
]);
|
|
1205
|
+
createGroundMaterialBuffer(diffuseColor, reflectionLevel, fadeStart, fadeEnd) {
|
|
1206
|
+
const u = new Float32Array(8);
|
|
1207
|
+
u[0] = diffuseColor.x;
|
|
1208
|
+
u[1] = diffuseColor.y;
|
|
1209
|
+
u[2] = diffuseColor.z;
|
|
1210
|
+
u[3] = reflectionLevel;
|
|
1211
|
+
u[4] = fadeStart;
|
|
1212
|
+
u[5] = fadeEnd;
|
|
1213
|
+
u[6] = 0;
|
|
1214
|
+
u[7] = 0;
|
|
1194
1215
|
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1195
1216
|
label: "ground material uniform buffer",
|
|
1196
|
-
size:
|
|
1217
|
+
size: 64,
|
|
1197
1218
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1198
1219
|
});
|
|
1199
|
-
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0,
|
|
1220
|
+
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u);
|
|
1200
1221
|
}
|
|
1201
|
-
createReflectionTexture(size
|
|
1222
|
+
createReflectionTexture(size) {
|
|
1202
1223
|
this.groundReflectionTexture = this.device.createTexture({
|
|
1203
1224
|
label: "ground reflection texture",
|
|
1204
1225
|
size: [size, size],
|
|
@@ -1219,19 +1240,83 @@ export class Engine {
|
|
|
1219
1240
|
format: "depth24plus-stencil8",
|
|
1220
1241
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1221
1242
|
});
|
|
1222
|
-
// Create a bind group for the reflection texture that can be used in the ground material
|
|
1223
1243
|
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1224
1244
|
label: "ground reflection bind group",
|
|
1225
1245
|
layout: this.groundBindGroupLayout,
|
|
1226
1246
|
entries: [
|
|
1227
1247
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1228
1248
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1229
|
-
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1249
|
+
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1230
1250
|
{ binding: 3, resource: this.materialSampler },
|
|
1231
1251
|
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer } },
|
|
1232
1252
|
],
|
|
1233
1253
|
});
|
|
1234
1254
|
}
|
|
1255
|
+
createShadowGroundResources(shadowMapSize, diffuseColor, fadeStart, fadeEnd, shadowStrength) {
|
|
1256
|
+
this.shadowMapTexture = this.device.createTexture({
|
|
1257
|
+
label: "shadow map",
|
|
1258
|
+
size: [shadowMapSize, shadowMapSize],
|
|
1259
|
+
format: "depth32float",
|
|
1260
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1261
|
+
});
|
|
1262
|
+
this.shadowMapDepthView = this.shadowMapTexture.createView();
|
|
1263
|
+
const gb = new Float32Array(8);
|
|
1264
|
+
gb[0] = diffuseColor.x;
|
|
1265
|
+
gb[1] = diffuseColor.y;
|
|
1266
|
+
gb[2] = diffuseColor.z;
|
|
1267
|
+
gb[3] = fadeStart;
|
|
1268
|
+
gb[4] = fadeEnd;
|
|
1269
|
+
gb[5] = shadowStrength;
|
|
1270
|
+
gb[6] = 1.2 / shadowMapSize;
|
|
1271
|
+
gb[7] = 0;
|
|
1272
|
+
this.groundShadowMaterialBuffer = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
1273
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 0, gb);
|
|
1274
|
+
this.shadowBindGroup = this.device.createBindGroup({
|
|
1275
|
+
label: "shadow bind",
|
|
1276
|
+
layout: this.shadowDepthPipeline.getBindGroupLayout(0),
|
|
1277
|
+
entries: [
|
|
1278
|
+
{ binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
|
|
1279
|
+
{ binding: 1, resource: { buffer: this.skinMatrixBuffer } },
|
|
1280
|
+
],
|
|
1281
|
+
});
|
|
1282
|
+
this.groundShadowBindGroup = this.device.createBindGroup({
|
|
1283
|
+
label: "ground shadow bind",
|
|
1284
|
+
layout: this.groundShadowBindGroupLayout,
|
|
1285
|
+
entries: [
|
|
1286
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1287
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1288
|
+
{ binding: 2, resource: this.shadowMapDepthView },
|
|
1289
|
+
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
1290
|
+
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
1291
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
1292
|
+
],
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
updateShadowLightVP() {
|
|
1296
|
+
const lx = this.lightData[4];
|
|
1297
|
+
const ly = this.lightData[5];
|
|
1298
|
+
const lz = this.lightData[6];
|
|
1299
|
+
if (lx === this.shadowVPLightX && ly === this.shadowVPLightY && lz === this.shadowVPLightZ)
|
|
1300
|
+
return;
|
|
1301
|
+
this.shadowVPLightX = lx;
|
|
1302
|
+
this.shadowVPLightY = ly;
|
|
1303
|
+
this.shadowVPLightZ = lz;
|
|
1304
|
+
const dir = new Vec3(lx, ly, lz);
|
|
1305
|
+
if (dir.length() < 1e-6) {
|
|
1306
|
+
dir.x = 0.35;
|
|
1307
|
+
dir.y = -1;
|
|
1308
|
+
dir.z = 0.2;
|
|
1309
|
+
}
|
|
1310
|
+
else
|
|
1311
|
+
dir.normalize();
|
|
1312
|
+
const target = new Vec3(0, 11, 0);
|
|
1313
|
+
const eye = new Vec3(target.x - dir.x * 72, target.y - dir.y * 72, target.z - dir.z * 72);
|
|
1314
|
+
const view = Mat4.lookAt(eye, target, new Vec3(0, 1, 0));
|
|
1315
|
+
const proj = Mat4.orthographicLh(-72, 72, -72, 72, 1, 140);
|
|
1316
|
+
const vp = proj.multiply(view);
|
|
1317
|
+
this.shadowLightVPMatrix.set(vp.values);
|
|
1318
|
+
this.device.queue.writeBuffer(this.shadowLightVPBuffer, 0, this.shadowLightVPMatrix);
|
|
1319
|
+
}
|
|
1235
1320
|
async setupMaterials(model) {
|
|
1236
1321
|
const materials = model.getMaterials();
|
|
1237
1322
|
if (materials.length === 0) {
|
|
@@ -1375,6 +1460,11 @@ export class Engine {
|
|
|
1375
1460
|
}
|
|
1376
1461
|
currentIndexOffset += indexCount;
|
|
1377
1462
|
}
|
|
1463
|
+
this.shadowDrawCalls.length = 0;
|
|
1464
|
+
for (const d of this.drawCalls) {
|
|
1465
|
+
if (d.type === "opaque" || d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes")
|
|
1466
|
+
this.shadowDrawCalls.push(d);
|
|
1467
|
+
}
|
|
1378
1468
|
}
|
|
1379
1469
|
createMaterialUniformBuffer(label, alpha, isOverEyes, diffuseColor, ambientColor, specularColor, shininess) {
|
|
1380
1470
|
const data = new Float32Array(20);
|
|
@@ -1469,13 +1559,11 @@ export class Engine {
|
|
|
1469
1559
|
}
|
|
1470
1560
|
}
|
|
1471
1561
|
renderGround(pass) {
|
|
1472
|
-
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer)
|
|
1562
|
+
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer)
|
|
1473
1563
|
return;
|
|
1474
|
-
|
|
1475
|
-
if (this.groundReflectionTexture) {
|
|
1564
|
+
if (this.groundMode === "reflection" && this.groundReflectionTexture)
|
|
1476
1565
|
this.renderReflectionTexture();
|
|
1477
|
-
|
|
1478
|
-
pass.setPipeline(this.groundPipeline);
|
|
1566
|
+
pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline);
|
|
1479
1567
|
pass.setVertexBuffer(0, this.groundVertexBuffer);
|
|
1480
1568
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
|
|
1481
1569
|
for (const draw of this.drawCalls) {
|
|
@@ -1484,8 +1572,6 @@ export class Engine {
|
|
|
1484
1572
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1485
1573
|
}
|
|
1486
1574
|
}
|
|
1487
|
-
// // Restore model index buffer for subsequent rendering
|
|
1488
|
-
// pass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
1489
1575
|
}
|
|
1490
1576
|
renderReflectionTexture() {
|
|
1491
1577
|
if (!this.groundReflectionTexture)
|
|
@@ -1792,10 +1878,35 @@ export class Engine {
|
|
|
1792
1878
|
this.updateVertexBuffer();
|
|
1793
1879
|
this.vertexBufferNeedsUpdate = false;
|
|
1794
1880
|
}
|
|
1795
|
-
// Update skin matrices buffer
|
|
1796
1881
|
this.updateSkinMatrices();
|
|
1797
|
-
|
|
1882
|
+
if (this.groundMode === "shadow")
|
|
1883
|
+
this.updateShadowLightVP();
|
|
1798
1884
|
const encoder = this.device.createCommandEncoder();
|
|
1885
|
+
if (this.groundMode === "shadow" &&
|
|
1886
|
+
this.currentModel &&
|
|
1887
|
+
this.shadowMapDepthView &&
|
|
1888
|
+
this.shadowBindGroup) {
|
|
1889
|
+
const sp = encoder.beginRenderPass({
|
|
1890
|
+
colorAttachments: [],
|
|
1891
|
+
depthStencilAttachment: {
|
|
1892
|
+
view: this.shadowMapDepthView,
|
|
1893
|
+
depthClearValue: 1.0,
|
|
1894
|
+
depthLoadOp: "clear",
|
|
1895
|
+
depthStoreOp: "store",
|
|
1896
|
+
},
|
|
1897
|
+
});
|
|
1898
|
+
sp.setPipeline(this.shadowDepthPipeline);
|
|
1899
|
+
sp.setBindGroup(0, this.shadowBindGroup);
|
|
1900
|
+
sp.setVertexBuffer(0, this.vertexBuffer);
|
|
1901
|
+
sp.setVertexBuffer(1, this.jointsBuffer);
|
|
1902
|
+
sp.setVertexBuffer(2, this.weightsBuffer);
|
|
1903
|
+
sp.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1904
|
+
for (const draw of this.shadowDrawCalls) {
|
|
1905
|
+
if (this.shouldRenderDrawCall(draw))
|
|
1906
|
+
sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1907
|
+
}
|
|
1908
|
+
sp.end();
|
|
1909
|
+
}
|
|
1799
1910
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1800
1911
|
if (this.currentModel) {
|
|
1801
1912
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
@@ -1825,7 +1936,6 @@ export class Engine {
|
|
|
1825
1936
|
}
|
|
1826
1937
|
this.drawOutlines(pass, true);
|
|
1827
1938
|
}
|
|
1828
|
-
// Pass 4: Ground (with reflections)
|
|
1829
1939
|
if (this.groundHasReflections) {
|
|
1830
1940
|
this.renderGround(pass);
|
|
1831
1941
|
}
|
|
@@ -1940,3 +2050,4 @@ export class Engine {
|
|
|
1940
2050
|
this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, transformedMatrices);
|
|
1941
2051
|
}
|
|
1942
2052
|
}
|
|
2053
|
+
Engine.instance = null;
|