reze-engine 0.8.3 → 0.8.4

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,6 +1,7 @@
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"
4
5
  import { Physics } from "./physics"
5
6
 
6
7
  export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
@@ -14,7 +15,6 @@ export type EngineOptions = {
14
15
  cameraTarget?: Vec3
15
16
  cameraFov?: number
16
17
  onRaycast?: RaycastCallback
17
- multisampleCount?: 1 | 4
18
18
  }
19
19
 
20
20
  export type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">
@@ -28,7 +28,6 @@ export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
28
28
  cameraTarget: new Vec3(0, 12.5, 0),
29
29
  cameraFov: Math.PI / 4,
30
30
  onRaycast: undefined,
31
- multisampleCount: 4,
32
31
  }
33
32
 
34
33
  export interface EngineStats {
@@ -78,7 +77,7 @@ export class Engine {
78
77
 
79
78
  public static getInstance(): Engine {
80
79
  if (!Engine.instance) {
81
- throw new Error("Engine not ready: create Engine, await init(), then load models via Model.loadFrom().")
80
+ throw new Error("Engine not ready: create Engine, await init(), then load models via engine.loadModel().")
82
81
  }
83
82
  return Engine.instance
84
83
  }
@@ -104,19 +103,18 @@ export class Engine {
104
103
  private hairPipelineOverEyes!: GPURenderPipeline
105
104
  private hairPipelineOverNonEyes!: GPURenderPipeline
106
105
  private hairDepthPipeline!: GPURenderPipeline
107
- // Ground/reflection pipeline
108
- private groundPipeline!: GPURenderPipeline
109
- private groundBindGroupLayout!: GPUBindGroupLayout
110
- private reflectionPipeline!: GPURenderPipeline
106
+ // Ground (shadow only)
107
+ private groundShadowPipeline!: GPURenderPipeline
108
+ private groundShadowBindGroupLayout!: GPUBindGroupLayout
111
109
  // Outline pipelines
112
110
  private outlinePipeline!: GPURenderPipeline
113
111
  private hairOutlinePipeline!: GPURenderPipeline
114
112
  private mainBindGroupLayout!: GPUBindGroupLayout
115
113
  private outlineBindGroupLayout!: GPUBindGroupLayout
116
114
  private multisampleTexture!: GPUTexture
117
- private sampleCount: 1 | 4 = 4
115
+ private static readonly MULTISAMPLE_COUNT = 4
118
116
  private renderPassDescriptor!: GPURenderPassDescriptor
119
- // Constants
117
+ // Post-alpha eye: eyes write stencil, hair-over-eyes reads it for see-through bangs (MMD-style).
120
118
  private readonly STENCIL_EYE_VALUE = 1
121
119
 
122
120
  // Ambient light settings
@@ -126,23 +124,15 @@ export class Engine {
126
124
  // Rim light settings
127
125
  private rimLightIntensity!: number
128
126
 
129
- // Ground/reflection properties
127
+ // Ground properties (shadow only)
130
128
  private groundVertexBuffer?: GPUBuffer
131
129
  private groundIndexBuffer?: GPUBuffer
132
- private groundReflectionTexture?: GPUTexture
133
- private groundReflectionResolveTexture?: GPUTexture // Resolve target for multisampled texture
134
- private groundReflectionDepthTexture?: GPUTexture
135
- private groundReflectionBindGroup?: GPUBindGroup
136
- private groundMaterialUniformBuffer?: GPUBuffer
137
- private groundHasReflections = false
138
- private groundMode: "reflection" | "shadow" = "reflection"
130
+ private hasGround = false
139
131
  private shadowMapTexture?: GPUTexture
140
132
  private shadowMapDepthView?: GPUTextureView
141
133
  private shadowDepthPipeline!: GPURenderPipeline
142
134
  private shadowLightVPBuffer!: GPUBuffer
143
135
  private shadowLightVPMatrix = new Float32Array(16)
144
- private groundShadowPipeline!: GPURenderPipeline
145
- private groundShadowBindGroupLayout!: GPUBindGroupLayout
146
136
  private groundShadowBindGroup?: GPUBindGroup
147
137
  private shadowComparisonSampler!: GPUSampler
148
138
  private groundShadowMaterialBuffer?: GPUBuffer
@@ -159,7 +149,16 @@ export class Engine {
159
149
  private modelInstances = new Map<string, ModelInstance>()
160
150
  private materialSampler!: GPUSampler
161
151
  private textureCache = new Map<string, GPUTexture>()
162
- private raycastVertexBuffer: Float32Array | null = null
152
+ private _nextDefaultModelId = 0
153
+
154
+ // IK and physics enabled at engine level (same for all models)
155
+ private ikEnabled = true
156
+ private physicsEnabled = true
157
+
158
+ // Camera target binding (Babylon/Three style: camera follows model)
159
+ private cameraTargetModel: Model | null = null
160
+ private cameraTargetBoneName = "全ての親"
161
+ private cameraTargetOffset: Vec3 = new Vec3(0, 0, 0)
163
162
 
164
163
  private lastFpsUpdate = performance.now()
165
164
  private framesSinceLastUpdate = 0
@@ -175,7 +174,6 @@ export class Engine {
175
174
 
176
175
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
177
176
  this.canvas = canvas
178
- this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount
179
177
  if (options) {
180
178
  this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor!
181
179
  this.directionalLightIntensity =
@@ -246,7 +244,7 @@ export class Engine {
246
244
  : undefined,
247
245
  primitive: { cullMode: config.cullMode ?? "none" },
248
246
  depthStencil: config.depthStencil,
249
- multisample: config.multisample ?? { count: this.sampleCount },
247
+ multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
250
248
  })
251
249
  }
252
250
 
@@ -496,71 +494,6 @@ export class Engine {
496
494
  },
497
495
  })
498
496
 
499
- this.groundBindGroupLayout = this.device.createBindGroupLayout({
500
- label: "ground bind group layout",
501
- entries: [
502
- { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
503
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
504
- { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
505
- { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
506
- { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
507
- ],
508
- })
509
- const groundPipelineLayout = this.device.createPipelineLayout({
510
- label: "ground pipeline layout",
511
- bindGroupLayouts: [this.groundBindGroupLayout],
512
- })
513
- const groundShaderModule = this.device.createShaderModule({
514
- label: "ground shaders",
515
- code: /* wgsl */ `
516
- struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
517
- struct Light { direction: vec4f, color: vec4f, };
518
- struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
519
- struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
520
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
521
- @group(0) @binding(1) var<uniform> light: LightUniforms;
522
- @group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
523
- @group(0) @binding(3) var reflectionSampler: sampler;
524
- @group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
525
- struct VertexOutput {
526
- @builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
527
- };
528
- @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
529
- var o: VertexOutput;
530
- o.worldPos = position;
531
- o.position = camera.projection * camera.view * vec4f(position, 1.0);
532
- o.normal = normal; o.uv = uv; return o;
533
- }
534
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
535
- let n = normalize(input.normal);
536
- let centerDist = length(input.worldPos.xz);
537
- let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
538
- let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
539
- let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
540
- let ndcPos = clipPos.xyz / clipPos.w;
541
- let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
542
- let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
543
- let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
544
- let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
545
- let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
546
- let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
547
- var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
548
- let l = -light.lights[0].direction.xyz;
549
- let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
550
- return vec4f(finalColor * lightAccum, edgeFade);
551
- }
552
- `,
553
- })
554
- this.groundPipeline = this.createRenderPipeline({
555
- label: "ground pipeline",
556
- layout: groundPipelineLayout,
557
- shaderModule: groundShaderModule,
558
- vertexBuffers: fullVertexBuffers,
559
- fragmentTarget: standardBlend,
560
- cullMode: "back",
561
- depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
562
- })
563
-
564
497
  this.shadowLightVPBuffer = this.device.createBuffer({
565
498
  size: 64,
566
499
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
@@ -677,25 +610,6 @@ export class Engine {
677
610
  depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
678
611
  })
679
612
 
680
- // Create reflection pipeline (multisampled version for higher quality)
681
- this.reflectionPipeline = this.createRenderPipeline({
682
- label: "reflection pipeline",
683
- layout: mainPipelineLayout,
684
- shaderModule,
685
- vertexBuffers: fullVertexBuffers,
686
- fragmentTarget: {
687
- format: this.presentationFormat,
688
- blend: standardBlend.blend,
689
- },
690
- multisample: { count: this.sampleCount }, // Use same multisampling as main render
691
- cullMode: "none",
692
- depthStencil: {
693
- format: "depth24plus-stencil8",
694
- depthWriteEnabled: true,
695
- depthCompare: "less-equal",
696
- },
697
- })
698
-
699
613
  // Create bind group layout for outline pipelines
700
614
  this.outlineBindGroupLayout = this.device.createBindGroupLayout({
701
615
  label: "outline bind group layout",
@@ -974,7 +888,7 @@ export class Engine {
974
888
  this.multisampleTexture = this.device.createTexture({
975
889
  label: "multisample render target",
976
890
  size: [width, height],
977
- sampleCount: this.sampleCount,
891
+ sampleCount: Engine.MULTISAMPLE_COUNT,
978
892
  format: this.presentationFormat,
979
893
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
980
894
  })
@@ -982,29 +896,20 @@ export class Engine {
982
896
  this.depthTexture = this.device.createTexture({
983
897
  label: "depth texture",
984
898
  size: [width, height],
985
- sampleCount: this.sampleCount,
899
+ sampleCount: Engine.MULTISAMPLE_COUNT,
986
900
  format: "depth24plus-stencil8",
987
901
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
988
902
  })
989
903
 
990
904
  const depthTextureView = this.depthTexture.createView()
991
905
 
992
- // Render directly to canvas
993
- const colorAttachment: GPURenderPassColorAttachment =
994
- this.sampleCount > 1
995
- ? {
996
- view: this.multisampleTexture.createView(),
997
- resolveTarget: this.context.getCurrentTexture().createView(),
998
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
999
- loadOp: "clear",
1000
- storeOp: "store",
1001
- }
1002
- : {
1003
- view: this.context.getCurrentTexture().createView(),
1004
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
1005
- loadOp: "clear",
1006
- storeOp: "store",
1007
- }
906
+ const colorAttachment: GPURenderPassColorAttachment = {
907
+ view: this.multisampleTexture.createView(),
908
+ resolveTarget: this.context.getCurrentTexture().createView(),
909
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
910
+ loadOp: "clear",
911
+ storeOp: "store",
912
+ }
1008
913
 
1009
914
  this.renderPassDescriptor = {
1010
915
  label: "renderPass",
@@ -1038,6 +943,29 @@ export class Engine {
1038
943
  this.camera.attachControl(this.canvas)
1039
944
  }
1040
945
 
946
+ /** Set camera look-at target to a point. Clears any model binding. */
947
+ public setCameraTarget(v: Vec3): void
948
+ /** Bind camera target to a model's bone. Engine updates target each frame. Bone not found → (0,0,0) + offset. Pass null to unbind. */
949
+ public setCameraTarget(model: Model | null, boneName: string, offset?: Vec3): void
950
+ public setCameraTarget(modelOrVec: Model | Vec3 | null, boneName?: string, offset?: Vec3): void {
951
+ if (modelOrVec === null) {
952
+ this.cameraTargetModel = null
953
+ return
954
+ }
955
+ if ("x" in modelOrVec && "y" in modelOrVec && "z" in modelOrVec) {
956
+ this.cameraTargetModel = null
957
+ this.camera.target.x = modelOrVec.x
958
+ this.camera.target.y = modelOrVec.y
959
+ this.camera.target.z = modelOrVec.z
960
+ return
961
+ }
962
+ this.cameraTargetModel = modelOrVec
963
+ this.cameraTargetBoneName = boneName ?? ""
964
+ this.cameraTargetOffset.x = offset?.x ?? 0
965
+ this.cameraTargetOffset.y = offset?.y ?? 0
966
+ this.cameraTargetOffset.z = offset?.z ?? 0
967
+ }
968
+
1041
969
  // Step 5: Create lighting buffers
1042
970
  private setupLighting() {
1043
971
  this.lightUniformBuffer = this.device.createBuffer({
@@ -1096,11 +1024,8 @@ export class Engine {
1096
1024
  width?: number
1097
1025
  height?: number
1098
1026
  diffuseColor?: Vec3
1099
- reflectionLevel?: number
1100
- reflectionTextureSize?: number
1101
1027
  fadeStart?: number
1102
1028
  fadeEnd?: number
1103
- mode?: "reflection" | "shadow"
1104
1029
  shadowMapSize?: number
1105
1030
  shadowStrength?: number
1106
1031
  }): void {
@@ -1108,29 +1033,20 @@ export class Engine {
1108
1033
  width: 100,
1109
1034
  height: 100,
1110
1035
  diffuseColor: new Vec3(1, 1, 1),
1111
- reflectionLevel: 0.5,
1112
- reflectionTextureSize: 1024,
1113
1036
  fadeStart: 5.0,
1114
1037
  fadeEnd: 60.0,
1115
- mode: "reflection" as const,
1116
1038
  shadowMapSize: 4096,
1117
1039
  shadowStrength: 1.0,
1118
1040
  ...options,
1119
1041
  }
1120
- this.groundMode = opts.mode
1121
1042
  this.createGroundGeometry(opts.width, opts.height)
1122
- if (opts.mode === "reflection") {
1123
- this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd)
1124
- this.createReflectionTexture(opts.reflectionTextureSize)
1125
- } else {
1126
- this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
1127
- }
1128
- this.groundHasReflections = true
1043
+ this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
1044
+ this.hasGround = true
1129
1045
  this.groundDrawCall = {
1130
1046
  type: "ground",
1131
1047
  count: 6,
1132
1048
  firstIndex: 0,
1133
- bindGroup: (opts.mode === "reflection" ? this.groundReflectionBindGroup : this.groundShadowBindGroup)!,
1049
+ bindGroup: this.groundShadowBindGroup!,
1134
1050
  materialName: "Ground",
1135
1051
  }
1136
1052
  }
@@ -1185,6 +1101,17 @@ export class Engine {
1185
1101
  }
1186
1102
  }
1187
1103
 
1104
+ public async loadModel(path: string): Promise<Model>
1105
+ public async loadModel(name: string, path: string): Promise<Model>
1106
+ public async loadModel(nameOrPath: string, path?: string): Promise<Model> {
1107
+ const pmxPath = path === undefined ? nameOrPath : path
1108
+ const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath
1109
+ const model = await PmxLoader.load(pmxPath)
1110
+ model.setName(name)
1111
+ await this.addModel(model, pmxPath, name)
1112
+ return model
1113
+ }
1114
+
1188
1115
  public async addModel(model: Model, pmxPath: string, name?: string): Promise<string> {
1189
1116
  const requested = name ?? model.name
1190
1117
  let key = requested
@@ -1249,12 +1176,20 @@ export class Engine {
1249
1176
  return inst ? !inst.hiddenMaterials.has(materialName) : false
1250
1177
  }
1251
1178
 
1252
- public setModelIKEnabled(modelName: string, enabled: boolean): void {
1253
- this.modelInstances.get(modelName)?.model.setIKEnabled(enabled)
1179
+ public setIKEnabled(enabled: boolean): void {
1180
+ this.ikEnabled = enabled
1254
1181
  }
1255
1182
 
1256
- public setModelPhysicsEnabled(modelName: string, enabled: boolean): void {
1257
- this.modelInstances.get(modelName)?.model.setPhysicsEnabled(enabled)
1183
+ public getIKEnabled(): boolean {
1184
+ return this.ikEnabled
1185
+ }
1186
+
1187
+ public setPhysicsEnabled(enabled: boolean): void {
1188
+ this.physicsEnabled = enabled
1189
+ }
1190
+
1191
+ public getPhysicsEnabled(): boolean {
1192
+ return this.physicsEnabled
1258
1193
  }
1259
1194
 
1260
1195
  public resetPhysics(): void {
@@ -1275,9 +1210,9 @@ export class Engine {
1275
1210
 
1276
1211
  private updateInstances(deltaTime: number): void {
1277
1212
  this.forEachInstance((inst) => {
1278
- const verticesChanged = inst.model.update(deltaTime)
1213
+ const verticesChanged = inst.model.update(deltaTime, this.ikEnabled)
1279
1214
  if (verticesChanged) inst.vertexBufferNeedsUpdate = true
1280
- if (inst.physics && inst.model.getPhysicsEnabled()) {
1215
+ if (inst.physics && this.physicsEnabled) {
1281
1216
  inst.physics.step(
1282
1217
  deltaTime,
1283
1218
  inst.model.getWorldMatrices(),
@@ -1454,58 +1389,6 @@ export class Engine {
1454
1389
  this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices)
1455
1390
  }
1456
1391
 
1457
- private createGroundMaterialBuffer(diffuseColor: Vec3, reflectionLevel: number, fadeStart: number, fadeEnd: number) {
1458
- const u = new Float32Array(8)
1459
- u[0] = diffuseColor.x
1460
- u[1] = diffuseColor.y
1461
- u[2] = diffuseColor.z
1462
- u[3] = reflectionLevel
1463
- u[4] = fadeStart
1464
- u[5] = fadeEnd
1465
- u[6] = 0
1466
- u[7] = 0
1467
- this.groundMaterialUniformBuffer = this.device.createBuffer({
1468
- label: "ground material uniform buffer",
1469
- size: 64,
1470
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1471
- })
1472
- this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u)
1473
- }
1474
-
1475
- private createReflectionTexture(size: number) {
1476
- this.groundReflectionTexture = this.device.createTexture({
1477
- label: "ground reflection texture",
1478
- size: [size, size],
1479
- sampleCount: this.sampleCount,
1480
- format: this.presentationFormat,
1481
- usage: GPUTextureUsage.RENDER_ATTACHMENT,
1482
- })
1483
- this.groundReflectionResolveTexture = this.device.createTexture({
1484
- label: "ground reflection resolve texture",
1485
- size: [size, size],
1486
- format: this.presentationFormat,
1487
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1488
- })
1489
- this.groundReflectionDepthTexture = this.device.createTexture({
1490
- label: "ground reflection depth texture",
1491
- size: [size, size],
1492
- sampleCount: this.sampleCount,
1493
- format: "depth24plus-stencil8",
1494
- usage: GPUTextureUsage.RENDER_ATTACHMENT,
1495
- })
1496
- this.groundReflectionBindGroup = this.device.createBindGroup({
1497
- label: "ground reflection bind group",
1498
- layout: this.groundBindGroupLayout,
1499
- entries: [
1500
- { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1501
- { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1502
- { binding: 2, resource: this.groundReflectionResolveTexture.createView() },
1503
- { binding: 3, resource: this.materialSampler },
1504
- { binding: 4, resource: { buffer: this.groundMaterialUniformBuffer! } },
1505
- ],
1506
- })
1507
- }
1508
-
1509
1392
  private createShadowGroundResources(
1510
1393
  shadowMapSize: number,
1511
1394
  diffuseColor: Vec3,
@@ -1776,85 +1659,29 @@ export class Engine {
1776
1659
  }
1777
1660
  }
1778
1661
 
1779
- // Helper: Render eyes with stencil writing (for post-alpha-eye effect)
1780
- private renderEyes(pass: GPURenderPassEncoder, inst: ModelInstance, useReflectionPipeline = false) {
1781
- if (useReflectionPipeline) {
1782
- pass.setPipeline(this.reflectionPipeline)
1783
- for (const draw of inst.drawCalls) {
1784
- if (draw.type === "eye") {
1785
- pass.setBindGroup(0, draw.bindGroup)
1786
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1787
- }
1788
- }
1789
- } else {
1790
- pass.setPipeline(this.eyePipeline)
1791
- pass.setStencilReference(this.STENCIL_EYE_VALUE)
1792
- for (const draw of inst.drawCalls) {
1793
- if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
1794
- pass.setBindGroup(0, draw.bindGroup)
1795
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1796
- }
1662
+ // Post-alpha eye: render eye draws; main pass writes stencil so hair-over-eyes can use it for see-through bangs.
1663
+ private renderEyes(pass: GPURenderPassEncoder, inst: ModelInstance) {
1664
+ pass.setPipeline(this.eyePipeline)
1665
+ pass.setStencilReference(this.STENCIL_EYE_VALUE)
1666
+ for (const draw of inst.drawCalls) {
1667
+ if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
1668
+ pass.setBindGroup(0, draw.bindGroup)
1669
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1797
1670
  }
1798
1671
  }
1799
1672
  }
1800
1673
 
1801
1674
  private renderGround(pass: GPURenderPassEncoder) {
1802
- if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
1803
- if (this.groundMode === "reflection" && this.groundReflectionTexture) this.renderReflectionTexture()
1804
- pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline)
1675
+ if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
1676
+ pass.setPipeline(this.groundShadowPipeline)
1805
1677
  pass.setVertexBuffer(0, this.groundVertexBuffer)
1806
1678
  pass.setIndexBuffer(this.groundIndexBuffer, "uint16")
1807
1679
  pass.setBindGroup(0, this.groundDrawCall.bindGroup)
1808
1680
  pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0)
1809
1681
  }
1810
1682
 
1811
- private renderReflectionTexture() {
1812
- if (!this.groundReflectionTexture) return
1813
-
1814
- const mirrorMatrix = this.createMirrorMatrix(new Vec3(0, 1, 0), 0)
1815
- this.updateCameraUniforms()
1816
-
1817
- const reflectionEncoder = this.device.createCommandEncoder()
1818
- const reflectionPassDescriptor: GPURenderPassDescriptor = {
1819
- label: "reflection render pass",
1820
- colorAttachments: [
1821
- {
1822
- view: this.groundReflectionTexture!.createView(),
1823
- resolveTarget: this.groundReflectionResolveTexture!.createView(),
1824
- clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // White
1825
- loadOp: "clear",
1826
- storeOp: "store",
1827
- },
1828
- ],
1829
- depthStencilAttachment: {
1830
- view: this.groundReflectionDepthTexture!.createView(),
1831
- depthClearValue: 1.0,
1832
- depthLoadOp: "clear",
1833
- depthStoreOp: "store",
1834
- stencilClearValue: 0,
1835
- stencilLoadOp: "clear",
1836
- stencilStoreOp: "discard",
1837
- },
1838
- }
1839
-
1840
- const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor)
1841
- this.forEachInstance((inst) => this.renderOneModel(reflectionPass, inst, true, mirrorMatrix))
1842
- reflectionPass.end()
1843
- this.device.queue.submit([reflectionEncoder.finish()])
1844
- this.updateSkinMatrices()
1845
- }
1846
-
1847
- private renderHair(pass: GPURenderPassEncoder, inst: ModelInstance, useReflectionPipeline = false) {
1848
- if (useReflectionPipeline) {
1849
- pass.setPipeline(this.reflectionPipeline)
1850
- for (const draw of inst.drawCalls) {
1851
- if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
1852
- pass.setBindGroup(0, draw.bindGroup)
1853
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1854
- }
1855
- }
1856
- return
1857
- }
1683
+ // Post-alpha eye: hair-over-eyes uses stencil (from renderEyes) for 50% alpha; hair-over-non-eyes uses inverse stencil.
1684
+ private renderHair(pass: GPURenderPassEncoder, inst: ModelInstance) {
1858
1685
 
1859
1686
  const hasHair = inst.drawCalls.some(
1860
1687
  (d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d)
@@ -2052,18 +1879,29 @@ export class Engine {
2052
1879
  const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
2053
1880
  this.lastFrameTime = currentTime
2054
1881
 
2055
- this.updateCameraUniforms()
2056
1882
  this.updateRenderTarget()
2057
1883
 
2058
1884
  const hasModels = this.modelInstances.size > 0
2059
1885
  if (hasModels) {
2060
1886
  this.updateInstances(deltaTime)
2061
1887
  this.updateSkinMatrices()
1888
+ // Update camera target from bound model (bone not found → 0,0,0 + offset)
1889
+ if (this.cameraTargetModel) {
1890
+ const pos = this.cameraTargetModel.getBoneWorldPosition(this.cameraTargetBoneName)
1891
+ const px = pos?.x ?? 0
1892
+ const py = pos?.y ?? 0
1893
+ const pz = pos?.z ?? 0
1894
+ this.camera.target.x = px + this.cameraTargetOffset.x
1895
+ this.camera.target.y = py + this.cameraTargetOffset.y
1896
+ this.camera.target.z = pz + this.cameraTargetOffset.z
1897
+ }
2062
1898
  }
2063
- if (this.groundMode === "shadow") this.updateShadowLightVP()
1899
+
1900
+ this.updateCameraUniforms()
1901
+ if (this.hasGround) this.updateShadowLightVP()
2064
1902
 
2065
1903
  const encoder = this.device.createCommandEncoder()
2066
- if (hasModels && this.groundMode === "shadow" && this.shadowMapDepthView) {
1904
+ if (hasModels && this.hasGround && this.shadowMapDepthView) {
2067
1905
  const sp = encoder.beginRenderPass({
2068
1906
  colorAttachments: [],
2069
1907
  depthStencilAttachment: {
@@ -2079,8 +1917,8 @@ export class Engine {
2079
1917
  }
2080
1918
 
2081
1919
  const pass = encoder.beginRenderPass(this.renderPassDescriptor)
2082
- if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst, false))
2083
- if (this.groundHasReflections) this.renderGround(pass)
1920
+ if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst))
1921
+ if (this.hasGround) this.renderGround(pass)
2084
1922
 
2085
1923
  pass.end()
2086
1924
  this.device.queue.submit([encoder.finish()])
@@ -2098,50 +1936,37 @@ export class Engine {
2098
1936
  }
2099
1937
  }
2100
1938
 
2101
- private renderOneModel(pass: GPURenderPassEncoder, inst: ModelInstance, useReflection: boolean, mirrorMatrix?: Mat4): void {
2102
- pass.setVertexBuffer(0, inst.vertexBuffer)
2103
- pass.setVertexBuffer(1, inst.jointsBuffer)
2104
- pass.setVertexBuffer(2, inst.weightsBuffer)
2105
- pass.setIndexBuffer(inst.indexBuffer, "uint32")
2106
-
2107
- if (useReflection && mirrorMatrix) {
2108
- this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix)
2109
- pass.setPipeline(this.reflectionPipeline)
2110
- for (const draw of inst.drawCalls) {
2111
- if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
2112
- pass.setBindGroup(0, draw.bindGroup)
2113
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2114
- }
2115
- }
2116
- this.renderEyes(pass, inst, true)
2117
- this.renderHair(pass, inst, true)
2118
- for (const draw of inst.drawCalls) {
2119
- if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
2120
- pass.setBindGroup(0, draw.bindGroup)
2121
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2122
- }
2123
- }
2124
- this.drawOutlines(pass, inst, true, true)
2125
- return
2126
- }
2127
-
2128
- pass.setPipeline(this.modelPipeline)
1939
+ private drawOpaque(pass: GPURenderPassEncoder, inst: ModelInstance, pipeline: GPURenderPipeline): void {
1940
+ pass.setPipeline(pipeline)
2129
1941
  for (const draw of inst.drawCalls) {
2130
1942
  if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
2131
1943
  pass.setBindGroup(0, draw.bindGroup)
2132
1944
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2133
1945
  }
2134
1946
  }
2135
- this.renderEyes(pass, inst, false)
2136
- this.drawOutlines(pass, inst, false)
2137
- this.renderHair(pass, inst, false)
2138
- pass.setPipeline(this.modelPipeline)
1947
+ }
1948
+
1949
+ private drawTransparent(pass: GPURenderPassEncoder, inst: ModelInstance, pipeline: GPURenderPipeline): void {
1950
+ pass.setPipeline(pipeline)
2139
1951
  for (const draw of inst.drawCalls) {
2140
1952
  if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
2141
1953
  pass.setBindGroup(0, draw.bindGroup)
2142
1954
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2143
1955
  }
2144
1956
  }
1957
+ }
1958
+
1959
+ private renderOneModel(pass: GPURenderPassEncoder, inst: ModelInstance): void {
1960
+ pass.setVertexBuffer(0, inst.vertexBuffer)
1961
+ pass.setVertexBuffer(1, inst.jointsBuffer)
1962
+ pass.setVertexBuffer(2, inst.weightsBuffer)
1963
+ pass.setIndexBuffer(inst.indexBuffer, "uint32")
1964
+
1965
+ this.drawOpaque(pass, inst, this.modelPipeline)
1966
+ this.renderEyes(pass, inst)
1967
+ this.drawOutlines(pass, inst, false)
1968
+ this.renderHair(pass, inst)
1969
+ this.drawTransparent(pass, inst, this.modelPipeline)
2145
1970
  this.drawOutlines(pass, inst, true)
2146
1971
  }
2147
1972
 
@@ -2159,13 +1984,8 @@ export class Engine {
2159
1984
  }
2160
1985
 
2161
1986
  private updateRenderTarget() {
2162
- // Update render target to use current canvas texture
2163
1987
  const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
2164
- if (this.sampleCount > 1) {
2165
- colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
2166
- } else {
2167
- colorAttachment.view = this.context.getCurrentTexture().createView()
2168
- }
1988
+ colorAttachment.resolveTarget = this.context.getCurrentTexture().createView()
2169
1989
  }
2170
1990
 
2171
1991
  private updateSkinMatrices() {
@@ -2181,8 +2001,7 @@ export class Engine {
2181
2001
  })
2182
2002
  }
2183
2003
 
2184
- private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean, useReflectionPipeline = false) {
2185
- if (useReflectionPipeline) return
2004
+ private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean) {
2186
2005
  pass.setPipeline(this.outlinePipeline)
2187
2006
  const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
2188
2007
  for (const draw of inst.drawCalls) {
@@ -2218,42 +2037,4 @@ export class Engine {
2218
2037
  }
2219
2038
  }
2220
2039
 
2221
- private createMirrorMatrix(planeNormal: Vec3, planeDistance: number): Mat4 {
2222
- // Create reflection matrix across a plane
2223
- const n = planeNormal.normalize()
2224
-
2225
- return new Mat4(
2226
- new Float32Array([
2227
- 1 - 2 * n.x * n.x,
2228
- -2 * n.x * n.y,
2229
- -2 * n.x * n.z,
2230
- 0,
2231
- -2 * n.y * n.x,
2232
- 1 - 2 * n.y * n.y,
2233
- -2 * n.y * n.z,
2234
- 0,
2235
- -2 * n.z * n.x,
2236
- -2 * n.z * n.y,
2237
- 1 - 2 * n.z * n.z,
2238
- 0,
2239
- -2 * planeDistance * n.x,
2240
- -2 * planeDistance * n.y,
2241
- -2 * planeDistance * n.z,
2242
- 1,
2243
- ])
2244
- )
2245
- }
2246
-
2247
- private writeMirrorTransformedSkinMatrices(inst: ModelInstance, mirrorMatrix: Mat4) {
2248
- const originalMatrices = inst.model.getSkinMatrices()
2249
- const transformedMatrices = new Float32Array(originalMatrices.length)
2250
- for (let i = 0; i < originalMatrices.length; i += 16) {
2251
- const boneMatrixValues = new Float32Array(16)
2252
- for (let j = 0; j < 16; j++) boneMatrixValues[j] = originalMatrices[i + j]
2253
- const boneMatrix = new Mat4(boneMatrixValues)
2254
- const transformed = mirrorMatrix.multiply(boneMatrix)
2255
- for (let j = 0; j < 16; j++) transformedMatrices[i + j] = transformed.values[j]
2256
- }
2257
- this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices)
2258
- }
2259
2040
  }