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/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" } }, // camera
387
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
388
- { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // reflectionTexture
389
- { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // reflectionSampler
390
- { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // groundMaterial
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
- view: mat4x4f,
402
- projection: mat4x4f,
403
- viewPos: vec3f,
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
- @vertex fn vs(
440
- @location(0) position: vec3f,
441
- @location(1) normal: vec3f,
442
- @location(2) uv: vec2f,
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
- var reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
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
- all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
463
- var reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
464
-
465
- let distanceFromCamera = length(input.worldPos - camera.viewPos);
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 nDotL = max(dot(n, l), 0.0);
483
- let intensity = light.lights[0].color.w;
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: "depth24plus-stencil8",
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
- // Create ground geometry
970
+ this.groundMode = opts.mode;
971
+ this.drawCalls = this.drawCalls.filter((d) => d.type !== "ground");
900
972
  this.createGroundGeometry(opts.width, opts.height);
901
- this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd);
902
- this.createReflectionTexture(opts.reflectionTextureSize);
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, // 2 triangles, 3 indices each
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
- // Step 6: Load PMX model file
972
- async loadModel(path) {
973
- const pathParts = path.split("/");
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
- const dir = pathParts.join("/") + "/";
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
- rotateBones(boneRotations, durationMs) {
984
- this.currentModel?.rotateBones(boneRotations, durationMs);
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 = new Vec3(1, 1, 1), reflectionLevel = 0.5, fadeStart = 5.0, fadeEnd = 60.0) {
1179
- const materialData = new Float32Array([
1180
- diffuseColor.x,
1181
- diffuseColor.y,
1182
- diffuseColor.z, // diffuseColor (12 bytes)
1183
- reflectionLevel, // reflectionLevel (4 bytes)
1184
- fadeStart, // fadeStart (4 bytes)
1185
- fadeEnd, // fadeEnd (4 bytes)
1186
- 0, // padding (4 bytes)
1187
- 0, // padding (4 bytes)
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: materialData.byteLength,
1217
+ size: 64,
1194
1218
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1195
1219
  });
1196
- this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, materialData);
1220
+ this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u);
1197
1221
  }
1198
- createReflectionTexture(size = 1024) {
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() }, // Use resolve texture for sampling
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
- // Use single encoder for render
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;