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/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,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
- // 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
- 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
- // Step 6: Load PMX model file
978
- async loadModel(path) {
979
- 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("/");
980
1033
  pathParts.pop();
981
- const dir = pathParts.join("/") + "/";
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
- rotateBones(boneRotations, durationMs) {
990
- this.currentModel?.rotateBones(boneRotations, durationMs);
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 = new Vec3(1, 1, 1), reflectionLevel = 0.5, fadeStart = 5.0, fadeEnd = 60.0) {
1182
- const materialData = new Float32Array([
1183
- diffuseColor.x,
1184
- diffuseColor.y,
1185
- diffuseColor.z, // diffuseColor (12 bytes)
1186
- reflectionLevel, // reflectionLevel (4 bytes)
1187
- fadeStart, // fadeStart (4 bytes)
1188
- fadeEnd, // fadeEnd (4 bytes)
1189
- 0, // padding (4 bytes)
1190
- 0, // padding (4 bytes)
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: materialData.byteLength,
1217
+ size: 64,
1197
1218
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1198
1219
  });
1199
- this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, materialData);
1220
+ this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u);
1200
1221
  }
1201
- createReflectionTexture(size = 1024) {
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() }, // Use resolve texture for sampling
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
- // Use single encoder for render
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;