reze-engine 0.6.7 → 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 +23 -35
- package/dist/animation.d.ts +14 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/animation.js +48 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +2 -2
- package/dist/engine.d.ts +29 -22
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +292 -178
- 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 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/math.d.ts +1 -10
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +24 -36
- package/dist/model.d.ts +3 -52
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +103 -170
- package/dist/pmx-loader.js +1 -1
- package/package.json +1 -1
- package/src/animation.ts +75 -0
- package/src/camera.ts +2 -2
- package/src/engine.ts +315 -218
- package/src/ik-solver.ts +1 -11
- package/src/index.ts +3 -2
- package/src/math.ts +27 -42
- package/src/model.ts +125 -220
- 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,44 +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
|
-
playAnimation() {
|
|
921
|
-
this.currentModel?.playAnimation();
|
|
922
|
-
}
|
|
923
|
-
stopAnimation() {
|
|
924
|
-
this.currentModel?.stopAnimation();
|
|
925
|
-
}
|
|
926
|
-
pauseAnimation() {
|
|
927
|
-
this.currentModel?.pauseAnimation();
|
|
928
|
-
}
|
|
929
|
-
seekAnimation(time) {
|
|
930
|
-
this.currentModel?.seekAnimation(time);
|
|
931
|
-
}
|
|
932
|
-
getAnimationProgress() {
|
|
933
|
-
return this.currentModel?.getAnimationProgress() ?? { current: 0, duration: 0, percentage: 0 };
|
|
934
|
-
}
|
|
935
992
|
getStats() {
|
|
936
993
|
return { ...this.stats };
|
|
937
994
|
}
|
|
@@ -955,7 +1012,9 @@ export class Engine {
|
|
|
955
1012
|
}
|
|
956
1013
|
dispose() {
|
|
957
1014
|
this.stopRenderLoop();
|
|
958
|
-
this.stopAnimation();
|
|
1015
|
+
this.currentModel?.stopAnimation();
|
|
1016
|
+
if (Engine.instance === this)
|
|
1017
|
+
Engine.instance = null;
|
|
959
1018
|
if (this.camera)
|
|
960
1019
|
this.camera.detachControl();
|
|
961
1020
|
// Remove raycasting event listeners
|
|
@@ -968,41 +1027,18 @@ export class Engine {
|
|
|
968
1027
|
this.resizeObserver = null;
|
|
969
1028
|
}
|
|
970
1029
|
}
|
|
971
|
-
//
|
|
972
|
-
async
|
|
973
|
-
const pathParts =
|
|
1030
|
+
// Single active model; prefer Model.loadPmx() so load + register stay paired
|
|
1031
|
+
async registerModel(model, pmxPath) {
|
|
1032
|
+
const pathParts = pmxPath.split("/");
|
|
974
1033
|
pathParts.pop();
|
|
975
|
-
|
|
976
|
-
this.modelDir = dir;
|
|
977
|
-
const model = await PmxLoader.load(path);
|
|
978
|
-
// Clear cached skinned vertices when loading a new model
|
|
1034
|
+
this.modelDir = pathParts.join("/") + "/";
|
|
979
1035
|
this.cachedSkinnedVertices = undefined;
|
|
980
1036
|
this.cachedSkinMatricesVersion = -1;
|
|
981
1037
|
await this.setupModelBuffers(model);
|
|
982
1038
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
// moveBones now takes relative translations (VMD-style) by default
|
|
987
|
-
moveBones(boneTranslations, durationMs) {
|
|
988
|
-
this.currentModel?.moveBones(boneTranslations, durationMs);
|
|
989
|
-
}
|
|
990
|
-
setPose(rotations, translations, morphs) {
|
|
991
|
-
this.currentModel?.setPose(rotations, translations, morphs);
|
|
992
|
-
}
|
|
993
|
-
resetAllBones() {
|
|
994
|
-
this.currentModel?.resetAllBones();
|
|
995
|
-
}
|
|
996
|
-
resetAllMorphs() {
|
|
997
|
-
this.currentModel?.resetAllMorphs();
|
|
998
|
-
}
|
|
999
|
-
setMorphWeight(name, weight, durationMs) {
|
|
1000
|
-
if (!this.currentModel)
|
|
1001
|
-
return;
|
|
1002
|
-
this.currentModel.setMorphWeight(name, weight, durationMs);
|
|
1003
|
-
if (!durationMs || durationMs === 0) {
|
|
1004
|
-
this.vertexBufferNeedsUpdate = true;
|
|
1005
|
-
}
|
|
1039
|
+
// After morph/vertex edits, queues GPU vertex upload on next frame
|
|
1040
|
+
markVertexBufferDirty() {
|
|
1041
|
+
this.vertexBufferNeedsUpdate = true;
|
|
1006
1042
|
}
|
|
1007
1043
|
setMaterialVisible(name, visible) {
|
|
1008
1044
|
if (visible) {
|
|
@@ -1023,15 +1059,6 @@ export class Engine {
|
|
|
1023
1059
|
isMaterialVisible(name) {
|
|
1024
1060
|
return !this.hiddenMaterials.has(name);
|
|
1025
1061
|
}
|
|
1026
|
-
getBones() {
|
|
1027
|
-
return this.currentModel?.getSkeleton().bones.map((bone) => bone.name) ?? [];
|
|
1028
|
-
}
|
|
1029
|
-
getMorphs() {
|
|
1030
|
-
return this.currentModel?.getMorphing().morphs.map((morph) => morph.name) ?? [];
|
|
1031
|
-
}
|
|
1032
|
-
getMaterials() {
|
|
1033
|
-
return this.currentModel?.getMaterials().map((material) => material.name) ?? [];
|
|
1034
|
-
}
|
|
1035
1062
|
// IK control
|
|
1036
1063
|
get disableIK() {
|
|
1037
1064
|
return this._disableIK;
|
|
@@ -1175,27 +1202,24 @@ export class Engine {
|
|
|
1175
1202
|
});
|
|
1176
1203
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
1177
1204
|
}
|
|
1178
|
-
createGroundMaterialBuffer(diffuseColor
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
0, // padding (4 bytes)
|
|
1189
|
-
0, // padding (4 bytes)
|
|
1190
|
-
]);
|
|
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;
|
|
1191
1215
|
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1192
1216
|
label: "ground material uniform buffer",
|
|
1193
|
-
size:
|
|
1217
|
+
size: 64,
|
|
1194
1218
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1195
1219
|
});
|
|
1196
|
-
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0,
|
|
1220
|
+
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u);
|
|
1197
1221
|
}
|
|
1198
|
-
createReflectionTexture(size
|
|
1222
|
+
createReflectionTexture(size) {
|
|
1199
1223
|
this.groundReflectionTexture = this.device.createTexture({
|
|
1200
1224
|
label: "ground reflection texture",
|
|
1201
1225
|
size: [size, size],
|
|
@@ -1216,19 +1240,83 @@ export class Engine {
|
|
|
1216
1240
|
format: "depth24plus-stencil8",
|
|
1217
1241
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1218
1242
|
});
|
|
1219
|
-
// Create a bind group for the reflection texture that can be used in the ground material
|
|
1220
1243
|
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1221
1244
|
label: "ground reflection bind group",
|
|
1222
1245
|
layout: this.groundBindGroupLayout,
|
|
1223
1246
|
entries: [
|
|
1224
1247
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1225
1248
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1226
|
-
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1249
|
+
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() },
|
|
1227
1250
|
{ binding: 3, resource: this.materialSampler },
|
|
1228
1251
|
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer } },
|
|
1229
1252
|
],
|
|
1230
1253
|
});
|
|
1231
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
|
+
}
|
|
1232
1320
|
async setupMaterials(model) {
|
|
1233
1321
|
const materials = model.getMaterials();
|
|
1234
1322
|
if (materials.length === 0) {
|
|
@@ -1372,6 +1460,11 @@ export class Engine {
|
|
|
1372
1460
|
}
|
|
1373
1461
|
currentIndexOffset += indexCount;
|
|
1374
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
|
+
}
|
|
1375
1468
|
}
|
|
1376
1469
|
createMaterialUniformBuffer(label, alpha, isOverEyes, diffuseColor, ambientColor, specularColor, shininess) {
|
|
1377
1470
|
const data = new Float32Array(20);
|
|
@@ -1466,13 +1559,11 @@ export class Engine {
|
|
|
1466
1559
|
}
|
|
1467
1560
|
}
|
|
1468
1561
|
renderGround(pass) {
|
|
1469
|
-
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer)
|
|
1562
|
+
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer)
|
|
1470
1563
|
return;
|
|
1471
|
-
|
|
1472
|
-
if (this.groundReflectionTexture) {
|
|
1564
|
+
if (this.groundMode === "reflection" && this.groundReflectionTexture)
|
|
1473
1565
|
this.renderReflectionTexture();
|
|
1474
|
-
|
|
1475
|
-
pass.setPipeline(this.groundPipeline);
|
|
1566
|
+
pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline);
|
|
1476
1567
|
pass.setVertexBuffer(0, this.groundVertexBuffer);
|
|
1477
1568
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
|
|
1478
1569
|
for (const draw of this.drawCalls) {
|
|
@@ -1481,8 +1572,6 @@ export class Engine {
|
|
|
1481
1572
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1482
1573
|
}
|
|
1483
1574
|
}
|
|
1484
|
-
// // Restore model index buffer for subsequent rendering
|
|
1485
|
-
// pass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
1486
1575
|
}
|
|
1487
1576
|
renderReflectionTexture() {
|
|
1488
1577
|
if (!this.groundReflectionTexture)
|
|
@@ -1789,10 +1878,35 @@ export class Engine {
|
|
|
1789
1878
|
this.updateVertexBuffer();
|
|
1790
1879
|
this.vertexBufferNeedsUpdate = false;
|
|
1791
1880
|
}
|
|
1792
|
-
// Update skin matrices buffer
|
|
1793
1881
|
this.updateSkinMatrices();
|
|
1794
|
-
|
|
1882
|
+
if (this.groundMode === "shadow")
|
|
1883
|
+
this.updateShadowLightVP();
|
|
1795
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
|
+
}
|
|
1796
1910
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1797
1911
|
if (this.currentModel) {
|
|
1798
1912
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
@@ -1822,7 +1936,6 @@ export class Engine {
|
|
|
1822
1936
|
}
|
|
1823
1937
|
this.drawOutlines(pass, true);
|
|
1824
1938
|
}
|
|
1825
|
-
// Pass 4: Ground (with reflections)
|
|
1826
1939
|
if (this.groundHasReflections) {
|
|
1827
1940
|
this.renderGround(pass);
|
|
1828
1941
|
}
|
|
@@ -1937,3 +2050,4 @@ export class Engine {
|
|
|
1937
2050
|
this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, transformedMatrices);
|
|
1938
2051
|
}
|
|
1939
2052
|
}
|
|
2053
|
+
Engine.instance = null;
|