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/src/engine.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Camera } from "./camera"
2
2
  import { Mat4, Quat, Vec3 } from "./math"
3
3
  import { Model } from "./model"
4
- import { PmxLoader } from "./pmx-loader"
5
4
 
6
5
  export type RaycastCallback = (material: string | null, screenX: number, screenY: number) => void
7
6
 
@@ -16,6 +15,8 @@ export type EngineOptions = {
16
15
  onRaycast?: RaycastCallback
17
16
  disableIK?: boolean
18
17
  disablePhysics?: boolean
18
+ // multisampleCount: 1 | 4 (default 4)
19
+ multisampleCount?: 1 | 4
19
20
  }
20
21
 
21
22
  export type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">
@@ -31,6 +32,7 @@ export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
31
32
  onRaycast: undefined,
32
33
  disableIK: false,
33
34
  disablePhysics: false,
35
+ multisampleCount: 4,
34
36
  }
35
37
 
36
38
  export interface EngineStats {
@@ -59,6 +61,15 @@ interface DrawCall {
59
61
  }
60
62
 
61
63
  export class Engine {
64
+ private static instance: Engine | null = null
65
+
66
+ public static getInstance(): Engine {
67
+ if (!Engine.instance) {
68
+ throw new Error("Engine not ready: create Engine, await init(), then load models via Model.loadPmx().")
69
+ }
70
+ return Engine.instance
71
+ }
72
+
62
73
  private canvas: HTMLCanvasElement
63
74
  private device!: GPUDevice
64
75
  private context!: GPUCanvasContext
@@ -96,7 +107,7 @@ export class Engine {
96
107
  private skinMatrixBuffer?: GPUBuffer
97
108
  private inverseBindMatrixBuffer?: GPUBuffer
98
109
  private multisampleTexture!: GPUTexture
99
- private readonly sampleCount = 4
110
+ private sampleCount: 1 | 4 = 4
100
111
  private renderPassDescriptor!: GPURenderPassDescriptor
101
112
  // Constants
102
113
  private readonly STENCIL_EYE_VALUE = 1
@@ -117,6 +128,22 @@ export class Engine {
117
128
  private groundReflectionBindGroup?: GPUBindGroup
118
129
  private groundMaterialUniformBuffer?: GPUBuffer
119
130
  private groundHasReflections = false
131
+ private groundMode: "reflection" | "shadow" = "reflection"
132
+ private shadowMapTexture?: GPUTexture
133
+ private shadowMapDepthView?: GPUTextureView
134
+ private shadowDepthPipeline!: GPURenderPipeline
135
+ private shadowBindGroup?: GPUBindGroup
136
+ private shadowLightVPBuffer!: GPUBuffer
137
+ private shadowLightVPMatrix = new Float32Array(16)
138
+ private groundShadowPipeline!: GPURenderPipeline
139
+ private groundShadowBindGroupLayout!: GPUBindGroupLayout
140
+ private groundShadowBindGroup?: GPUBindGroup
141
+ private shadowComparisonSampler!: GPUSampler
142
+ private groundShadowMaterialBuffer?: GPUBuffer
143
+ private shadowDrawCalls: DrawCall[] = []
144
+ private shadowVPLightX = Number.NaN
145
+ private shadowVPLightY = Number.NaN
146
+ private shadowVPLightZ = Number.NaN
120
147
 
121
148
  // Raycasting
122
149
  private onRaycast?: RaycastCallback
@@ -155,6 +182,7 @@ export class Engine {
155
182
 
156
183
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
157
184
  this.canvas = canvas
185
+ this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount
158
186
  if (options) {
159
187
  this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor!
160
188
  this.directionalLightIntensity =
@@ -197,6 +225,7 @@ export class Engine {
197
225
  this.setupLighting()
198
226
  this.createPipelines()
199
227
  this.setupResize()
228
+ Engine.instance = this
200
229
  }
201
230
 
202
231
  private createRenderPipeline(config: {
@@ -476,121 +505,61 @@ export class Engine {
476
505
  },
477
506
  })
478
507
 
479
- // Create ground/reflection pipeline with reflection texture support
480
508
  this.groundBindGroupLayout = this.device.createBindGroupLayout({
481
509
  label: "ground bind group layout",
482
510
  entries: [
483
- { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
484
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
485
- { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // reflectionTexture
486
- { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // reflectionSampler
487
- { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // groundMaterial
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: {} },
514
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
515
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
488
516
  ],
489
517
  })
490
-
491
518
  const groundPipelineLayout = this.device.createPipelineLayout({
492
519
  label: "ground pipeline layout",
493
520
  bindGroupLayouts: [this.groundBindGroupLayout],
494
521
  })
495
-
496
522
  const groundShaderModule = this.device.createShaderModule({
497
523
  label: "ground shaders",
498
524
  code: /* wgsl */ `
499
- struct CameraUniforms {
500
- view: mat4x4f,
501
- projection: mat4x4f,
502
- viewPos: vec3f,
503
- _padding: f32,
504
- };
505
-
506
- struct LightUniforms {
507
- ambientColor: vec4f,
508
- lights: array<Light, 4>,
509
- };
510
-
511
- struct Light {
512
- direction: vec4f,
513
- color: vec4f,
514
- };
515
-
516
- struct GroundMaterialUniforms {
517
- diffuseColor: vec3f,
518
- reflectionLevel: f32,
519
- fadeStart: f32,
520
- fadeEnd: f32,
521
- _padding1: f32,
522
- _padding2: f32,
523
- };
524
-
525
+ struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
526
+ struct Light { direction: vec4f, color: vec4f, };
527
+ struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
528
+ struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
525
529
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
526
530
  @group(0) @binding(1) var<uniform> light: LightUniforms;
527
531
  @group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
528
532
  @group(0) @binding(3) var reflectionSampler: sampler;
529
533
  @group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
530
-
531
534
  struct VertexOutput {
532
- @builtin(position) position: vec4f,
533
- @location(0) normal: vec3f,
534
- @location(1) uv: vec2f,
535
- @location(2) worldPos: vec3f,
535
+ @builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
536
536
  };
537
-
538
- @vertex fn vs(
539
- @location(0) position: vec3f,
540
- @location(1) normal: vec3f,
541
- @location(2) uv: vec2f,
542
- ) -> VertexOutput {
543
- var output: VertexOutput;
544
- let worldPos = position;
545
- output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
546
- output.normal = normal;
547
- output.uv = uv;
548
- output.worldPos = worldPos;
549
- return output;
537
+ @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
538
+ var o: VertexOutput;
539
+ o.worldPos = position;
540
+ o.position = camera.projection * camera.view * vec4f(position, 1.0);
541
+ o.normal = normal; o.uv = uv; return o;
550
542
  }
551
-
552
543
  @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
553
544
  let n = normalize(input.normal);
554
-
545
+ let centerDist = length(input.worldPos.xz);
546
+ let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
547
+ let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
555
548
  let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
556
549
  let ndcPos = clipPos.xyz / clipPos.w;
557
- var reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
558
-
550
+ let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
559
551
  let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
560
- let isValidReflection = clipPos.w > 0.0 &&
561
- all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
562
- var reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
563
-
564
- let distanceFromCamera = length(input.worldPos - camera.viewPos);
565
- let fadeFactor = clamp((distanceFromCamera - 15.0) / 20.0, 0.0, 1.0);
566
- reflectionColor *= (1.0 - fadeFactor * 0.3);
567
-
568
- let diffuseColor = material.diffuseColor;
569
- var finalColor = mix(diffuseColor, reflectionColor, material.reflectionLevel);
570
-
571
- // Ground edge fade effect - smooth fade out at edges based on distance from center
572
- let centerDist = length(input.worldPos.xz); // Distance from ground center in XZ plane
573
-
574
- // Smoothstep for much smoother gradient transition
575
- let t = clamp((centerDist - material.fadeStart) / (material.fadeEnd - material.fadeStart), 0.0, 1.0);
576
- let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
577
- finalColor *= edgeFade;
578
-
579
- // Single directional light
552
+ let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
553
+ let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
554
+ let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
555
+ let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
556
+ var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
580
557
  let l = -light.lights[0].direction.xyz;
581
- let nDotL = max(dot(n, l), 0.0);
582
- let intensity = light.lights[0].color.w;
583
- let radiance = light.lights[0].color.xyz * intensity;
584
- let lightAccum = light.ambientColor.xyz + radiance * nDotL;
585
-
586
- // Apply lighting to the blended color
587
- let litColor = finalColor * lightAccum;
588
-
589
- return vec4f(litColor, edgeFade);
558
+ let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
559
+ return vec4f(finalColor * lightAccum, edgeFade);
590
560
  }
591
561
  `,
592
562
  })
593
-
594
563
  this.groundPipeline = this.createRenderPipeline({
595
564
  label: "ground pipeline",
596
565
  layout: groundPipelineLayout,
@@ -598,12 +567,124 @@ export class Engine {
598
567
  vertexBuffers: fullVertexBuffers,
599
568
  fragmentTarget: standardBlend,
600
569
  cullMode: "back",
570
+ depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
571
+ })
572
+
573
+ this.shadowLightVPBuffer = this.device.createBuffer({
574
+ size: 64,
575
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
576
+ })
577
+ const shadowBindGroupLayout = this.device.createBindGroupLayout({
578
+ label: "shadow depth bind layout",
579
+ entries: [
580
+ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
581
+ { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
582
+ ],
583
+ })
584
+ const shadowShader = this.device.createShaderModule({
585
+ label: "shadow depth",
586
+ code: /* wgsl */ `
587
+ struct LightVP { viewProj: mat4x4f, };
588
+ @group(0) @binding(0) var<uniform> lp: LightVP;
589
+ @group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
590
+ @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
591
+ @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
592
+ let pos4 = vec4f(position, 1.0);
593
+ let ws = weights0.x + weights0.y + weights0.z + weights0.w;
594
+ let inv = select(1.0, 1.0 / ws, ws > 0.0001);
595
+ let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
596
+ var sp = vec4f(0.0);
597
+ for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
598
+ return lp.viewProj * vec4f(sp.xyz, 1.0);
599
+ }
600
+ `,
601
+ })
602
+ this.shadowDepthPipeline = this.device.createRenderPipeline({
603
+ label: "shadow depth pipeline",
604
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [shadowBindGroupLayout] }),
605
+ vertex: { module: shadowShader, entryPoint: "vs", buffers: fullVertexBuffers as GPUVertexBufferLayout[] },
606
+ primitive: { cullMode: "none" },
601
607
  depthStencil: {
602
- format: "depth24plus-stencil8",
608
+ format: "depth32float",
603
609
  depthWriteEnabled: true,
604
610
  depthCompare: "less-equal",
611
+ depthBias: 2,
612
+ depthBiasSlopeScale: 1.5,
613
+ depthBiasClamp: 0,
605
614
  },
606
615
  })
616
+ this.shadowComparisonSampler = this.device.createSampler({
617
+ compare: "less",
618
+ magFilter: "linear",
619
+ minFilter: "linear",
620
+ })
621
+ this.groundShadowBindGroupLayout = this.device.createBindGroupLayout({
622
+ label: "ground shadow layout",
623
+ entries: [
624
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
625
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
626
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
627
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
628
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
629
+ { binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
630
+ ],
631
+ })
632
+ const groundShadowShader = this.device.createShaderModule({
633
+ label: "ground shadow",
634
+ code: /* wgsl */ `
635
+ struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
636
+ struct Light { direction: vec4f, color: vec4f, };
637
+ struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
638
+ struct GroundShadowMat { diffuseColor: vec3f, fadeStart: f32, fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, _y: f32, };
639
+ struct LightVP { viewProj: mat4x4f, };
640
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
641
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
642
+ @group(0) @binding(2) var shadowMap: texture_depth_2d;
643
+ @group(0) @binding(3) var shadowSampler: sampler_comparison;
644
+ @group(0) @binding(4) var<uniform> material: GroundShadowMat;
645
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
646
+ struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
647
+ @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
648
+ var o: VO; o.worldPos = position; o.normal = normal;
649
+ o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
650
+ }
651
+ @fragment fn fs(i: VO) -> @location(0) vec4f {
652
+ let n = normalize(i.normal);
653
+ let centerDist = length(i.worldPos.xz);
654
+ let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));
655
+ let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);
656
+ let ndc = lclip.xyz / max(lclip.w, 1e-6);
657
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
658
+ let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
659
+ let st = material.pcfTexel;
660
+ let compareZ = ndc.z - 0.0035;
661
+ var vis = 0.0;
662
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, -st), compareZ);
663
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(0.0, -st), compareZ);
664
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, -st), compareZ);
665
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, 0.0), compareZ);
666
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c, compareZ);
667
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, 0.0), compareZ);
668
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(-st, st), compareZ);
669
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(0.0, st), compareZ);
670
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(st, st), compareZ);
671
+ vis *= 0.1111111;
672
+ 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);
673
+ let dark = (1.0 - vis) * material.shadowStrength;
674
+ let lit = material.diffuseColor * sun * (1.0 - dark * 0.65);
675
+ return vec4f(lit * edgeFade, edgeFade);
676
+ }
677
+ `,
678
+ })
679
+ this.groundShadowPipeline = this.createRenderPipeline({
680
+ label: "ground shadow pipeline",
681
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
682
+ shaderModule: groundShadowShader,
683
+ vertexBuffers: fullVertexBuffers,
684
+ fragmentTarget: standardBlend,
685
+ cullMode: "back",
686
+ depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
687
+ })
607
688
 
608
689
  // Create reflection pipeline (multisampled version for higher quality)
609
690
  this.reflectionPipeline = this.createRenderPipeline({
@@ -1028,6 +1109,9 @@ export class Engine {
1028
1109
  reflectionTextureSize?: number
1029
1110
  fadeStart?: number
1030
1111
  fadeEnd?: number
1112
+ mode?: "reflection" | "shadow"
1113
+ shadowMapSize?: number
1114
+ shadowStrength?: number
1031
1115
  }): void {
1032
1116
  const opts = {
1033
1117
  width: 100,
@@ -1037,21 +1121,26 @@ export class Engine {
1037
1121
  reflectionTextureSize: 1024,
1038
1122
  fadeStart: 5.0,
1039
1123
  fadeEnd: 60.0,
1124
+ mode: "reflection" as const,
1125
+ shadowMapSize: 4096,
1126
+ shadowStrength: 1.0,
1040
1127
  ...options,
1041
1128
  }
1042
-
1043
- // Create ground geometry
1129
+ this.groundMode = opts.mode
1130
+ this.drawCalls = this.drawCalls.filter((d) => d.type !== "ground")
1044
1131
  this.createGroundGeometry(opts.width, opts.height)
1045
-
1046
- this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd)
1047
- this.createReflectionTexture(opts.reflectionTextureSize)
1132
+ if (opts.mode === "reflection") {
1133
+ this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd)
1134
+ this.createReflectionTexture(opts.reflectionTextureSize)
1135
+ } else {
1136
+ this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
1137
+ }
1048
1138
  this.groundHasReflections = true
1049
-
1050
1139
  this.drawCalls.push({
1051
1140
  type: "ground",
1052
- count: 6, // 2 triangles, 3 indices each
1141
+ count: 6,
1053
1142
  firstIndex: 0,
1054
- bindGroup: this.groundReflectionBindGroup!,
1143
+ bindGroup: (opts.mode === "reflection" ? this.groundReflectionBindGroup : this.groundShadowBindGroup)!,
1055
1144
  materialName: "Ground",
1056
1145
  })
1057
1146
  }
@@ -1060,31 +1149,6 @@ export class Engine {
1060
1149
  this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData)
1061
1150
  }
1062
1151
 
1063
- public async loadAnimation(url: string) {
1064
- if (!this.currentModel) return
1065
- await this.currentModel.loadVmd(url)
1066
- }
1067
-
1068
- public playAnimation() {
1069
- this.currentModel?.playAnimation()
1070
- }
1071
-
1072
- public stopAnimation() {
1073
- this.currentModel?.stopAnimation()
1074
- }
1075
-
1076
- public pauseAnimation() {
1077
- this.currentModel?.pauseAnimation()
1078
- }
1079
-
1080
- public seekAnimation(time: number) {
1081
- this.currentModel?.seekAnimation(time)
1082
- }
1083
-
1084
- public getAnimationProgress() {
1085
- return this.currentModel?.getAnimationProgress() ?? { current: 0, duration: 0, percentage: 0 }
1086
- }
1087
-
1088
1152
  public getStats(): EngineStats {
1089
1153
  return { ...this.stats }
1090
1154
  }
@@ -1115,7 +1179,8 @@ export class Engine {
1115
1179
 
1116
1180
  public dispose() {
1117
1181
  this.stopRenderLoop()
1118
- this.stopAnimation()
1182
+ this.currentModel?.stopAnimation()
1183
+ if (Engine.instance === this) Engine.instance = null
1119
1184
  if (this.camera) this.camera.detachControl()
1120
1185
 
1121
1186
  // Remove raycasting event listeners
@@ -1130,53 +1195,19 @@ export class Engine {
1130
1195
  }
1131
1196
  }
1132
1197
 
1133
- // Step 6: Load PMX model file
1134
- public async loadModel(path: string) {
1135
- const pathParts = path.split("/")
1198
+ // Single active model; prefer Model.loadPmx() so load + register stay paired
1199
+ public async registerModel(model: Model, pmxPath: string): Promise<void> {
1200
+ const pathParts = pmxPath.split("/")
1136
1201
  pathParts.pop()
1137
- const dir = pathParts.join("/") + "/"
1138
- this.modelDir = dir
1139
-
1140
- const model = await PmxLoader.load(path)
1141
-
1142
- // Clear cached skinned vertices when loading a new model
1202
+ this.modelDir = pathParts.join("/") + "/"
1143
1203
  this.cachedSkinnedVertices = undefined
1144
1204
  this.cachedSkinMatricesVersion = -1
1145
-
1146
1205
  await this.setupModelBuffers(model)
1147
1206
  }
1148
1207
 
1149
- public rotateBones(boneRotations: Record<string, Quat>, durationMs?: number) {
1150
- this.currentModel?.rotateBones(boneRotations, durationMs)
1151
- }
1152
-
1153
- // moveBones now takes relative translations (VMD-style) by default
1154
- public moveBones(boneTranslations: Record<string, Vec3>, durationMs?: number) {
1155
- this.currentModel?.moveBones(boneTranslations, durationMs)
1156
- }
1157
-
1158
- public setPose(
1159
- rotations?: Record<string, Quat>,
1160
- translations?: Record<string, Vec3>,
1161
- morphs?: Record<string, number>
1162
- ): void {
1163
- this.currentModel?.setPose(rotations, translations, morphs)
1164
- }
1165
-
1166
- public resetAllBones() {
1167
- this.currentModel?.resetAllBones()
1168
- }
1169
-
1170
- public resetAllMorphs(): void {
1171
- this.currentModel?.resetAllMorphs()
1172
- }
1173
-
1174
- public setMorphWeight(name: string, weight: number, durationMs?: number): void {
1175
- if (!this.currentModel) return
1176
- this.currentModel.setMorphWeight(name, weight, durationMs)
1177
- if (!durationMs || durationMs === 0) {
1178
- this.vertexBufferNeedsUpdate = true
1179
- }
1208
+ // After morph/vertex edits, queues GPU vertex upload on next frame
1209
+ public markVertexBufferDirty(): void {
1210
+ this.vertexBufferNeedsUpdate = true
1180
1211
  }
1181
1212
 
1182
1213
  public setMaterialVisible(name: string, visible: boolean): void {
@@ -1199,18 +1230,6 @@ export class Engine {
1199
1230
  return !this.hiddenMaterials.has(name)
1200
1231
  }
1201
1232
 
1202
- public getBones(): string[] {
1203
- return this.currentModel?.getSkeleton().bones.map((bone) => bone.name) ?? []
1204
- }
1205
-
1206
- public getMorphs(): string[] {
1207
- return this.currentModel?.getMorphing().morphs.map((morph) => morph.name) ?? []
1208
- }
1209
-
1210
- public getMaterials(): string[] {
1211
- return this.currentModel?.getMaterials().map((material) => material.name) ?? []
1212
- }
1213
-
1214
1233
  // IK control
1215
1234
  public get disableIK(): boolean {
1216
1235
  return this._disableIK
@@ -1394,34 +1413,25 @@ export class Engine {
1394
1413
  this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices)
1395
1414
  }
1396
1415
 
1397
- private createGroundMaterialBuffer(
1398
- diffuseColor: Vec3 = new Vec3(1, 1, 1),
1399
- reflectionLevel: number = 0.5,
1400
- fadeStart: number = 5.0,
1401
- fadeEnd: number = 60.0
1402
- ) {
1403
- const materialData = new Float32Array([
1404
- diffuseColor.x,
1405
- diffuseColor.y,
1406
- diffuseColor.z, // diffuseColor (12 bytes)
1407
- reflectionLevel, // reflectionLevel (4 bytes)
1408
- fadeStart, // fadeStart (4 bytes)
1409
- fadeEnd, // fadeEnd (4 bytes)
1410
- 0, // padding (4 bytes)
1411
- 0, // padding (4 bytes)
1412
- 0, // padding (4 bytes)
1413
- 0, // padding (4 bytes)
1414
- ])
1415
-
1416
+ private createGroundMaterialBuffer(diffuseColor: Vec3, reflectionLevel: number, fadeStart: number, fadeEnd: number) {
1417
+ const u = new Float32Array(8)
1418
+ u[0] = diffuseColor.x
1419
+ u[1] = diffuseColor.y
1420
+ u[2] = diffuseColor.z
1421
+ u[3] = reflectionLevel
1422
+ u[4] = fadeStart
1423
+ u[5] = fadeEnd
1424
+ u[6] = 0
1425
+ u[7] = 0
1416
1426
  this.groundMaterialUniformBuffer = this.device.createBuffer({
1417
1427
  label: "ground material uniform buffer",
1418
- size: materialData.byteLength,
1428
+ size: 64,
1419
1429
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1420
1430
  })
1421
- this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, materialData)
1431
+ this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u)
1422
1432
  }
1423
1433
 
1424
- private createReflectionTexture(size: number = 1024) {
1434
+ private createReflectionTexture(size: number) {
1425
1435
  this.groundReflectionTexture = this.device.createTexture({
1426
1436
  label: "ground reflection texture",
1427
1437
  size: [size, size],
@@ -1429,14 +1439,12 @@ export class Engine {
1429
1439
  format: this.presentationFormat,
1430
1440
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
1431
1441
  })
1432
-
1433
1442
  this.groundReflectionResolveTexture = this.device.createTexture({
1434
1443
  label: "ground reflection resolve texture",
1435
1444
  size: [size, size],
1436
1445
  format: this.presentationFormat,
1437
1446
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1438
1447
  })
1439
-
1440
1448
  this.groundReflectionDepthTexture = this.device.createTexture({
1441
1449
  label: "ground reflection depth texture",
1442
1450
  size: [size, size],
@@ -1444,21 +1452,89 @@ export class Engine {
1444
1452
  format: "depth24plus-stencil8",
1445
1453
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
1446
1454
  })
1447
-
1448
- // Create a bind group for the reflection texture that can be used in the ground material
1449
1455
  this.groundReflectionBindGroup = this.device.createBindGroup({
1450
1456
  label: "ground reflection bind group",
1451
1457
  layout: this.groundBindGroupLayout,
1452
1458
  entries: [
1453
1459
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1454
1460
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1455
- { binding: 2, resource: this.groundReflectionResolveTexture!.createView() }, // Use resolve texture for sampling
1461
+ { binding: 2, resource: this.groundReflectionResolveTexture.createView() },
1456
1462
  { binding: 3, resource: this.materialSampler },
1457
1463
  { binding: 4, resource: { buffer: this.groundMaterialUniformBuffer! } },
1458
1464
  ],
1459
1465
  })
1460
1466
  }
1461
1467
 
1468
+ private createShadowGroundResources(
1469
+ shadowMapSize: number,
1470
+ diffuseColor: Vec3,
1471
+ fadeStart: number,
1472
+ fadeEnd: number,
1473
+ shadowStrength: number
1474
+ ) {
1475
+ this.shadowMapTexture = this.device.createTexture({
1476
+ label: "shadow map",
1477
+ size: [shadowMapSize, shadowMapSize],
1478
+ format: "depth32float",
1479
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1480
+ })
1481
+ this.shadowMapDepthView = this.shadowMapTexture.createView()
1482
+ const gb = new Float32Array(8)
1483
+ gb[0] = diffuseColor.x
1484
+ gb[1] = diffuseColor.y
1485
+ gb[2] = diffuseColor.z
1486
+ gb[3] = fadeStart
1487
+ gb[4] = fadeEnd
1488
+ gb[5] = shadowStrength
1489
+ gb[6] = 1.2 / shadowMapSize
1490
+ gb[7] = 0
1491
+ this.groundShadowMaterialBuffer = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST })
1492
+ this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 0, gb)
1493
+ this.shadowBindGroup = this.device.createBindGroup({
1494
+ label: "shadow bind",
1495
+ layout: this.shadowDepthPipeline.getBindGroupLayout(0),
1496
+ entries: [
1497
+ { binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
1498
+ { binding: 1, resource: { buffer: this.skinMatrixBuffer! } },
1499
+ ],
1500
+ })
1501
+ this.groundShadowBindGroup = this.device.createBindGroup({
1502
+ label: "ground shadow bind",
1503
+ layout: this.groundShadowBindGroupLayout,
1504
+ entries: [
1505
+ { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1506
+ { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1507
+ { binding: 2, resource: this.shadowMapDepthView },
1508
+ { binding: 3, resource: this.shadowComparisonSampler },
1509
+ { binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
1510
+ { binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
1511
+ ],
1512
+ })
1513
+ }
1514
+
1515
+ private updateShadowLightVP() {
1516
+ const lx = this.lightData[4]
1517
+ const ly = this.lightData[5]
1518
+ const lz = this.lightData[6]
1519
+ if (lx === this.shadowVPLightX && ly === this.shadowVPLightY && lz === this.shadowVPLightZ) return
1520
+ this.shadowVPLightX = lx
1521
+ this.shadowVPLightY = ly
1522
+ this.shadowVPLightZ = lz
1523
+ const dir = new Vec3(lx, ly, lz)
1524
+ if (dir.length() < 1e-6) {
1525
+ dir.x = 0.35
1526
+ dir.y = -1
1527
+ dir.z = 0.2
1528
+ } else dir.normalize()
1529
+ const target = new Vec3(0, 11, 0)
1530
+ const eye = new Vec3(target.x - dir.x * 72, target.y - dir.y * 72, target.z - dir.z * 72)
1531
+ const view = Mat4.lookAt(eye, target, new Vec3(0, 1, 0))
1532
+ const proj = Mat4.orthographicLh(-72, 72, -72, 72, 1, 140)
1533
+ const vp = proj.multiply(view)
1534
+ this.shadowLightVPMatrix.set(vp.values)
1535
+ this.device.queue.writeBuffer(this.shadowLightVPBuffer, 0, this.shadowLightVPMatrix)
1536
+ }
1537
+
1462
1538
  private async setupMaterials(model: Model) {
1463
1539
  const materials = model.getMaterials()
1464
1540
  if (materials.length === 0) {
@@ -1633,6 +1709,11 @@ export class Engine {
1633
1709
 
1634
1710
  currentIndexOffset += indexCount
1635
1711
  }
1712
+ this.shadowDrawCalls.length = 0
1713
+ for (const d of this.drawCalls) {
1714
+ if (d.type === "opaque" || d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes")
1715
+ this.shadowDrawCalls.push(d)
1716
+ }
1636
1717
  }
1637
1718
 
1638
1719
  private createMaterialUniformBuffer(
@@ -1742,26 +1823,17 @@ export class Engine {
1742
1823
  }
1743
1824
 
1744
1825
  private renderGround(pass: GPURenderPassEncoder) {
1745
- if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer) {
1746
- return
1747
- }
1748
-
1749
- if (this.groundReflectionTexture) {
1750
- this.renderReflectionTexture()
1751
- }
1752
- pass.setPipeline(this.groundPipeline)
1826
+ if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer) return
1827
+ if (this.groundMode === "reflection" && this.groundReflectionTexture) this.renderReflectionTexture()
1828
+ pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline)
1753
1829
  pass.setVertexBuffer(0, this.groundVertexBuffer)
1754
1830
  pass.setIndexBuffer(this.groundIndexBuffer, "uint16")
1755
-
1756
1831
  for (const draw of this.drawCalls) {
1757
1832
  if (draw.type === "ground" && this.shouldRenderDrawCall(draw)) {
1758
1833
  pass.setBindGroup(0, draw.bindGroup)
1759
1834
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1760
1835
  }
1761
1836
  }
1762
-
1763
- // // Restore model index buffer for subsequent rendering
1764
- // pass.setIndexBuffer(this.indexBuffer!, "uint32")
1765
1837
  }
1766
1838
 
1767
1839
  private renderReflectionTexture() {
@@ -2178,11 +2250,37 @@ export class Engine {
2178
2250
  this.vertexBufferNeedsUpdate = false
2179
2251
  }
2180
2252
 
2181
- // Update skin matrices buffer
2182
2253
  this.updateSkinMatrices()
2254
+ if (this.groundMode === "shadow") this.updateShadowLightVP()
2183
2255
 
2184
- // Use single encoder for render
2185
2256
  const encoder = this.device.createCommandEncoder()
2257
+ if (
2258
+ this.groundMode === "shadow" &&
2259
+ this.currentModel &&
2260
+ this.shadowMapDepthView &&
2261
+ this.shadowBindGroup
2262
+ ) {
2263
+ const sp = encoder.beginRenderPass({
2264
+ colorAttachments: [],
2265
+ depthStencilAttachment: {
2266
+ view: this.shadowMapDepthView,
2267
+ depthClearValue: 1.0,
2268
+ depthLoadOp: "clear",
2269
+ depthStoreOp: "store",
2270
+ },
2271
+ })
2272
+ sp.setPipeline(this.shadowDepthPipeline)
2273
+ sp.setBindGroup(0, this.shadowBindGroup)
2274
+ sp.setVertexBuffer(0, this.vertexBuffer)
2275
+ sp.setVertexBuffer(1, this.jointsBuffer)
2276
+ sp.setVertexBuffer(2, this.weightsBuffer)
2277
+ sp.setIndexBuffer(this.indexBuffer!, "uint32")
2278
+ for (const draw of this.shadowDrawCalls) {
2279
+ if (this.shouldRenderDrawCall(draw))
2280
+ sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2281
+ }
2282
+ sp.end()
2283
+ }
2186
2284
 
2187
2285
  const pass = encoder.beginRenderPass(this.renderPassDescriptor)
2188
2286
 
@@ -2221,7 +2319,6 @@ export class Engine {
2221
2319
  this.drawOutlines(pass, true)
2222
2320
  }
2223
2321
 
2224
- // Pass 4: Ground (with reflections)
2225
2322
  if (this.groundHasReflections) {
2226
2323
  this.renderGround(pass)
2227
2324
  }