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/dist/engine.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Camera } from "./camera";
2
2
  import { Mat4, Vec3 } from "./math";
3
+ import { PmxLoader } from "./pmx-loader";
3
4
  import { Physics } from "./physics";
4
5
  export const DEFAULT_ENGINE_OPTIONS = {
5
6
  ambientColor: new Vec3(0.88, 0.88, 0.88),
@@ -10,12 +11,11 @@ export const DEFAULT_ENGINE_OPTIONS = {
10
11
  cameraTarget: new Vec3(0, 12.5, 0),
11
12
  cameraFov: Math.PI / 4,
12
13
  onRaycast: undefined,
13
- multisampleCount: 4,
14
14
  };
15
15
  export class Engine {
16
16
  static getInstance() {
17
17
  if (!Engine.instance) {
18
- throw new Error("Engine not ready: create Engine, await init(), then load models via Model.loadFrom().");
18
+ throw new Error("Engine not ready: create Engine, await init(), then load models via engine.loadModel().");
19
19
  }
20
20
  return Engine.instance;
21
21
  }
@@ -24,11 +24,9 @@ export class Engine {
24
24
  this.lightData = new Float32Array(64);
25
25
  this.lightCount = 0;
26
26
  this.resizeObserver = null;
27
- this.sampleCount = 4;
28
- // Constants
27
+ // Post-alpha eye: eyes write stencil, hair-over-eyes reads it for see-through bangs (MMD-style).
29
28
  this.STENCIL_EYE_VALUE = 1;
30
- this.groundHasReflections = false;
31
- this.groundMode = "reflection";
29
+ this.hasGround = false;
32
30
  this.shadowLightVPMatrix = new Float32Array(16);
33
31
  this.groundDrawCall = null;
34
32
  this.shadowVPLightX = Number.NaN;
@@ -39,7 +37,14 @@ export class Engine {
39
37
  this.DOUBLE_TAP_DELAY = 300; // ms
40
38
  this.modelInstances = new Map();
41
39
  this.textureCache = new Map();
42
- this.raycastVertexBuffer = null;
40
+ this._nextDefaultModelId = 0;
41
+ // IK and physics enabled at engine level (same for all models)
42
+ this.ikEnabled = true;
43
+ this.physicsEnabled = true;
44
+ // Camera target binding (Babylon/Three style: camera follows model)
45
+ this.cameraTargetModel = null;
46
+ this.cameraTargetBoneName = "全ての親";
47
+ this.cameraTargetOffset = new Vec3(0, 0, 0);
43
48
  this.lastFpsUpdate = performance.now();
44
49
  this.framesSinceLastUpdate = 0;
45
50
  this.lastFrameTime = performance.now();
@@ -83,7 +88,6 @@ export class Engine {
83
88
  }
84
89
  };
85
90
  this.canvas = canvas;
86
- this.sampleCount = options?.multisampleCount ?? DEFAULT_ENGINE_OPTIONS.multisampleCount;
87
91
  if (options) {
88
92
  this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor;
89
93
  this.directionalLightIntensity =
@@ -138,7 +142,7 @@ export class Engine {
138
142
  : undefined,
139
143
  primitive: { cullMode: config.cullMode ?? "none" },
140
144
  depthStencil: config.depthStencil,
141
- multisample: config.multisample ?? { count: this.sampleCount },
145
+ multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
142
146
  });
143
147
  }
144
148
  createPipelines() {
@@ -378,70 +382,6 @@ export class Engine {
378
382
  depthCompare: "less-equal",
379
383
  },
380
384
  });
381
- this.groundBindGroupLayout = this.device.createBindGroupLayout({
382
- label: "ground bind group layout",
383
- entries: [
384
- { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
385
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
386
- { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
387
- { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
388
- { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
389
- ],
390
- });
391
- const groundPipelineLayout = this.device.createPipelineLayout({
392
- label: "ground pipeline layout",
393
- bindGroupLayouts: [this.groundBindGroupLayout],
394
- });
395
- const groundShaderModule = this.device.createShaderModule({
396
- label: "ground shaders",
397
- code: /* wgsl */ `
398
- struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
399
- struct Light { direction: vec4f, color: vec4f, };
400
- struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
401
- struct GroundMaterialUniforms { diffuseColor: vec3f, reflectionLevel: f32, fadeStart: f32, fadeEnd: f32, _a: f32, _b: f32, };
402
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
403
- @group(0) @binding(1) var<uniform> light: LightUniforms;
404
- @group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
405
- @group(0) @binding(3) var reflectionSampler: sampler;
406
- @group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
407
- struct VertexOutput {
408
- @builtin(position) position: vec4f, @location(0) normal: vec3f, @location(1) uv: vec2f, @location(2) worldPos: vec3f,
409
- };
410
- @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VertexOutput {
411
- var o: VertexOutput;
412
- o.worldPos = position;
413
- o.position = camera.projection * camera.view * vec4f(position, 1.0);
414
- o.normal = normal; o.uv = uv; return o;
415
- }
416
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
417
- let n = normalize(input.normal);
418
- let centerDist = length(input.worldPos.xz);
419
- let t = clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0);
420
- let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
421
- let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
422
- let ndcPos = clipPos.xyz / clipPos.w;
423
- let reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
424
- let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
425
- let isValidReflection = clipPos.w > 0.0 && all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
426
- let reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
427
- let fadeFactor = clamp((length(input.worldPos - camera.viewPos) - 15.0) / 20.0, 0.0, 1.0);
428
- let refl = reflectionColor * (1.0 - fadeFactor * 0.3);
429
- var finalColor = mix(material.diffuseColor, refl, material.reflectionLevel) * edgeFade;
430
- let l = -light.lights[0].direction.xyz;
431
- let lightAccum = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, l), 0.0);
432
- return vec4f(finalColor * lightAccum, edgeFade);
433
- }
434
- `,
435
- });
436
- this.groundPipeline = this.createRenderPipeline({
437
- label: "ground pipeline",
438
- layout: groundPipelineLayout,
439
- shaderModule: groundShaderModule,
440
- vertexBuffers: fullVertexBuffers,
441
- fragmentTarget: standardBlend,
442
- cullMode: "back",
443
- depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
444
- });
445
385
  this.shadowLightVPBuffer = this.device.createBuffer({
446
386
  size: 64,
447
387
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
@@ -557,24 +497,6 @@ export class Engine {
557
497
  cullMode: "back",
558
498
  depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
559
499
  });
560
- // Create reflection pipeline (multisampled version for higher quality)
561
- this.reflectionPipeline = this.createRenderPipeline({
562
- label: "reflection pipeline",
563
- layout: mainPipelineLayout,
564
- shaderModule,
565
- vertexBuffers: fullVertexBuffers,
566
- fragmentTarget: {
567
- format: this.presentationFormat,
568
- blend: standardBlend.blend,
569
- },
570
- multisample: { count: this.sampleCount }, // Use same multisampling as main render
571
- cullMode: "none",
572
- depthStencil: {
573
- format: "depth24plus-stencil8",
574
- depthWriteEnabled: true,
575
- depthCompare: "less-equal",
576
- },
577
- });
578
500
  // Create bind group layout for outline pipelines
579
501
  this.outlineBindGroupLayout = this.device.createBindGroupLayout({
580
502
  label: "outline bind group layout",
@@ -837,33 +759,25 @@ export class Engine {
837
759
  this.multisampleTexture = this.device.createTexture({
838
760
  label: "multisample render target",
839
761
  size: [width, height],
840
- sampleCount: this.sampleCount,
762
+ sampleCount: Engine.MULTISAMPLE_COUNT,
841
763
  format: this.presentationFormat,
842
764
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
843
765
  });
844
766
  this.depthTexture = this.device.createTexture({
845
767
  label: "depth texture",
846
768
  size: [width, height],
847
- sampleCount: this.sampleCount,
769
+ sampleCount: Engine.MULTISAMPLE_COUNT,
848
770
  format: "depth24plus-stencil8",
849
771
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
850
772
  });
851
773
  const depthTextureView = this.depthTexture.createView();
852
- // Render directly to canvas
853
- const colorAttachment = this.sampleCount > 1
854
- ? {
855
- view: this.multisampleTexture.createView(),
856
- resolveTarget: this.context.getCurrentTexture().createView(),
857
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
858
- loadOp: "clear",
859
- storeOp: "store",
860
- }
861
- : {
862
- view: this.context.getCurrentTexture().createView(),
863
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
864
- loadOp: "clear",
865
- storeOp: "store",
866
- };
774
+ const colorAttachment = {
775
+ view: this.multisampleTexture.createView(),
776
+ resolveTarget: this.context.getCurrentTexture().createView(),
777
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
778
+ loadOp: "clear",
779
+ storeOp: "store",
780
+ };
867
781
  this.renderPassDescriptor = {
868
782
  label: "renderPass",
869
783
  colorAttachments: [colorAttachment],
@@ -891,6 +805,24 @@ export class Engine {
891
805
  this.camera.aspect = this.canvas.width / this.canvas.height;
892
806
  this.camera.attachControl(this.canvas);
893
807
  }
808
+ setCameraTarget(modelOrVec, boneName, offset) {
809
+ if (modelOrVec === null) {
810
+ this.cameraTargetModel = null;
811
+ return;
812
+ }
813
+ if ("x" in modelOrVec && "y" in modelOrVec && "z" in modelOrVec) {
814
+ this.cameraTargetModel = null;
815
+ this.camera.target.x = modelOrVec.x;
816
+ this.camera.target.y = modelOrVec.y;
817
+ this.camera.target.z = modelOrVec.z;
818
+ return;
819
+ }
820
+ this.cameraTargetModel = modelOrVec;
821
+ this.cameraTargetBoneName = boneName ?? "";
822
+ this.cameraTargetOffset.x = offset?.x ?? 0;
823
+ this.cameraTargetOffset.y = offset?.y ?? 0;
824
+ this.cameraTargetOffset.z = offset?.z ?? 0;
825
+ }
894
826
  // Step 5: Create lighting buffers
895
827
  setupLighting() {
896
828
  this.lightUniformBuffer = this.device.createBuffer({
@@ -943,30 +875,20 @@ export class Engine {
943
875
  width: 100,
944
876
  height: 100,
945
877
  diffuseColor: new Vec3(1, 1, 1),
946
- reflectionLevel: 0.5,
947
- reflectionTextureSize: 1024,
948
878
  fadeStart: 5.0,
949
879
  fadeEnd: 60.0,
950
- mode: "reflection",
951
880
  shadowMapSize: 4096,
952
881
  shadowStrength: 1.0,
953
882
  ...options,
954
883
  };
955
- this.groundMode = opts.mode;
956
884
  this.createGroundGeometry(opts.width, opts.height);
957
- if (opts.mode === "reflection") {
958
- this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd);
959
- this.createReflectionTexture(opts.reflectionTextureSize);
960
- }
961
- else {
962
- this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength);
963
- }
964
- this.groundHasReflections = true;
885
+ this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength);
886
+ this.hasGround = true;
965
887
  this.groundDrawCall = {
966
888
  type: "ground",
967
889
  count: 6,
968
890
  firstIndex: 0,
969
- bindGroup: (opts.mode === "reflection" ? this.groundReflectionBindGroup : this.groundShadowBindGroup),
891
+ bindGroup: this.groundShadowBindGroup,
970
892
  materialName: "Ground",
971
893
  };
972
894
  }
@@ -1011,6 +933,14 @@ export class Engine {
1011
933
  this.resizeObserver = null;
1012
934
  }
1013
935
  }
936
+ async loadModel(nameOrPath, path) {
937
+ const pmxPath = path === undefined ? nameOrPath : path;
938
+ const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath;
939
+ const model = await PmxLoader.load(pmxPath);
940
+ model.setName(name);
941
+ await this.addModel(model, pmxPath, name);
942
+ return model;
943
+ }
1014
944
  async addModel(model, pmxPath, name) {
1015
945
  const requested = name ?? model.name;
1016
946
  let key = requested;
@@ -1074,11 +1004,17 @@ export class Engine {
1074
1004
  const inst = this.modelInstances.get(modelName);
1075
1005
  return inst ? !inst.hiddenMaterials.has(materialName) : false;
1076
1006
  }
1077
- setModelIKEnabled(modelName, enabled) {
1078
- this.modelInstances.get(modelName)?.model.setIKEnabled(enabled);
1007
+ setIKEnabled(enabled) {
1008
+ this.ikEnabled = enabled;
1079
1009
  }
1080
- setModelPhysicsEnabled(modelName, enabled) {
1081
- this.modelInstances.get(modelName)?.model.setPhysicsEnabled(enabled);
1010
+ getIKEnabled() {
1011
+ return this.ikEnabled;
1012
+ }
1013
+ setPhysicsEnabled(enabled) {
1014
+ this.physicsEnabled = enabled;
1015
+ }
1016
+ getPhysicsEnabled() {
1017
+ return this.physicsEnabled;
1082
1018
  }
1083
1019
  resetPhysics() {
1084
1020
  this.forEachInstance((inst) => {
@@ -1097,10 +1033,10 @@ export class Engine {
1097
1033
  }
1098
1034
  updateInstances(deltaTime) {
1099
1035
  this.forEachInstance((inst) => {
1100
- const verticesChanged = inst.model.update(deltaTime);
1036
+ const verticesChanged = inst.model.update(deltaTime, this.ikEnabled);
1101
1037
  if (verticesChanged)
1102
1038
  inst.vertexBufferNeedsUpdate = true;
1103
- if (inst.physics && inst.model.getPhysicsEnabled()) {
1039
+ if (inst.physics && this.physicsEnabled) {
1104
1040
  inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
1105
1041
  }
1106
1042
  if (inst.vertexBufferNeedsUpdate)
@@ -1245,56 +1181,6 @@ export class Engine {
1245
1181
  });
1246
1182
  this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
1247
1183
  }
1248
- createGroundMaterialBuffer(diffuseColor, reflectionLevel, fadeStart, fadeEnd) {
1249
- const u = new Float32Array(8);
1250
- u[0] = diffuseColor.x;
1251
- u[1] = diffuseColor.y;
1252
- u[2] = diffuseColor.z;
1253
- u[3] = reflectionLevel;
1254
- u[4] = fadeStart;
1255
- u[5] = fadeEnd;
1256
- u[6] = 0;
1257
- u[7] = 0;
1258
- this.groundMaterialUniformBuffer = this.device.createBuffer({
1259
- label: "ground material uniform buffer",
1260
- size: 64,
1261
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1262
- });
1263
- this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, u);
1264
- }
1265
- createReflectionTexture(size) {
1266
- this.groundReflectionTexture = this.device.createTexture({
1267
- label: "ground reflection texture",
1268
- size: [size, size],
1269
- sampleCount: this.sampleCount,
1270
- format: this.presentationFormat,
1271
- usage: GPUTextureUsage.RENDER_ATTACHMENT,
1272
- });
1273
- this.groundReflectionResolveTexture = this.device.createTexture({
1274
- label: "ground reflection resolve texture",
1275
- size: [size, size],
1276
- format: this.presentationFormat,
1277
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1278
- });
1279
- this.groundReflectionDepthTexture = this.device.createTexture({
1280
- label: "ground reflection depth texture",
1281
- size: [size, size],
1282
- sampleCount: this.sampleCount,
1283
- format: "depth24plus-stencil8",
1284
- usage: GPUTextureUsage.RENDER_ATTACHMENT,
1285
- });
1286
- this.groundReflectionBindGroup = this.device.createBindGroup({
1287
- label: "ground reflection bind group",
1288
- layout: this.groundBindGroupLayout,
1289
- entries: [
1290
- { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1291
- { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1292
- { binding: 2, resource: this.groundReflectionResolveTexture.createView() },
1293
- { binding: 3, resource: this.materialSampler },
1294
- { binding: 4, resource: { buffer: this.groundMaterialUniformBuffer } },
1295
- ],
1296
- });
1297
- }
1298
1184
  createShadowGroundResources(shadowMapSize, diffuseColor, fadeStart, fadeEnd, shadowStrength) {
1299
1185
  this.shadowMapTexture = this.device.createTexture({
1300
1186
  label: "shadow map",
@@ -1526,83 +1412,28 @@ export class Engine {
1526
1412
  return null;
1527
1413
  }
1528
1414
  }
1529
- // Helper: Render eyes with stencil writing (for post-alpha-eye effect)
1530
- renderEyes(pass, inst, useReflectionPipeline = false) {
1531
- if (useReflectionPipeline) {
1532
- pass.setPipeline(this.reflectionPipeline);
1533
- for (const draw of inst.drawCalls) {
1534
- if (draw.type === "eye") {
1535
- pass.setBindGroup(0, draw.bindGroup);
1536
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1537
- }
1538
- }
1539
- }
1540
- else {
1541
- pass.setPipeline(this.eyePipeline);
1542
- pass.setStencilReference(this.STENCIL_EYE_VALUE);
1543
- for (const draw of inst.drawCalls) {
1544
- if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
1545
- pass.setBindGroup(0, draw.bindGroup);
1546
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1547
- }
1415
+ // Post-alpha eye: render eye draws; main pass writes stencil so hair-over-eyes can use it for see-through bangs.
1416
+ renderEyes(pass, inst) {
1417
+ pass.setPipeline(this.eyePipeline);
1418
+ pass.setStencilReference(this.STENCIL_EYE_VALUE);
1419
+ for (const draw of inst.drawCalls) {
1420
+ if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
1421
+ pass.setBindGroup(0, draw.bindGroup);
1422
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1548
1423
  }
1549
1424
  }
1550
1425
  }
1551
1426
  renderGround(pass) {
1552
- if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
1427
+ if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
1553
1428
  return;
1554
- if (this.groundMode === "reflection" && this.groundReflectionTexture)
1555
- this.renderReflectionTexture();
1556
- pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline);
1429
+ pass.setPipeline(this.groundShadowPipeline);
1557
1430
  pass.setVertexBuffer(0, this.groundVertexBuffer);
1558
1431
  pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
1559
1432
  pass.setBindGroup(0, this.groundDrawCall.bindGroup);
1560
1433
  pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
1561
1434
  }
1562
- renderReflectionTexture() {
1563
- if (!this.groundReflectionTexture)
1564
- return;
1565
- const mirrorMatrix = this.createMirrorMatrix(new Vec3(0, 1, 0), 0);
1566
- this.updateCameraUniforms();
1567
- const reflectionEncoder = this.device.createCommandEncoder();
1568
- const reflectionPassDescriptor = {
1569
- label: "reflection render pass",
1570
- colorAttachments: [
1571
- {
1572
- view: this.groundReflectionTexture.createView(),
1573
- resolveTarget: this.groundReflectionResolveTexture.createView(),
1574
- clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // White
1575
- loadOp: "clear",
1576
- storeOp: "store",
1577
- },
1578
- ],
1579
- depthStencilAttachment: {
1580
- view: this.groundReflectionDepthTexture.createView(),
1581
- depthClearValue: 1.0,
1582
- depthLoadOp: "clear",
1583
- depthStoreOp: "store",
1584
- stencilClearValue: 0,
1585
- stencilLoadOp: "clear",
1586
- stencilStoreOp: "discard",
1587
- },
1588
- };
1589
- const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor);
1590
- this.forEachInstance((inst) => this.renderOneModel(reflectionPass, inst, true, mirrorMatrix));
1591
- reflectionPass.end();
1592
- this.device.queue.submit([reflectionEncoder.finish()]);
1593
- this.updateSkinMatrices();
1594
- }
1595
- renderHair(pass, inst, useReflectionPipeline = false) {
1596
- if (useReflectionPipeline) {
1597
- pass.setPipeline(this.reflectionPipeline);
1598
- for (const draw of inst.drawCalls) {
1599
- if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
1600
- pass.setBindGroup(0, draw.bindGroup);
1601
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1602
- }
1603
- }
1604
- return;
1605
- }
1435
+ // Post-alpha eye: hair-over-eyes uses stencil (from renderEyes) for 50% alpha; hair-over-non-eyes uses inverse stencil.
1436
+ renderHair(pass, inst) {
1606
1437
  const hasHair = inst.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d));
1607
1438
  if (hasHair) {
1608
1439
  pass.setPipeline(this.hairDepthPipeline);
@@ -1751,17 +1582,27 @@ export class Engine {
1751
1582
  const currentTime = performance.now();
1752
1583
  const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016;
1753
1584
  this.lastFrameTime = currentTime;
1754
- this.updateCameraUniforms();
1755
1585
  this.updateRenderTarget();
1756
1586
  const hasModels = this.modelInstances.size > 0;
1757
1587
  if (hasModels) {
1758
1588
  this.updateInstances(deltaTime);
1759
1589
  this.updateSkinMatrices();
1590
+ // Update camera target from bound model (bone not found → 0,0,0 + offset)
1591
+ if (this.cameraTargetModel) {
1592
+ const pos = this.cameraTargetModel.getBoneWorldPosition(this.cameraTargetBoneName);
1593
+ const px = pos?.x ?? 0;
1594
+ const py = pos?.y ?? 0;
1595
+ const pz = pos?.z ?? 0;
1596
+ this.camera.target.x = px + this.cameraTargetOffset.x;
1597
+ this.camera.target.y = py + this.cameraTargetOffset.y;
1598
+ this.camera.target.z = pz + this.cameraTargetOffset.z;
1599
+ }
1760
1600
  }
1761
- if (this.groundMode === "shadow")
1601
+ this.updateCameraUniforms();
1602
+ if (this.hasGround)
1762
1603
  this.updateShadowLightVP();
1763
1604
  const encoder = this.device.createCommandEncoder();
1764
- if (hasModels && this.groundMode === "shadow" && this.shadowMapDepthView) {
1605
+ if (hasModels && this.hasGround && this.shadowMapDepthView) {
1765
1606
  const sp = encoder.beginRenderPass({
1766
1607
  colorAttachments: [],
1767
1608
  depthStencilAttachment: {
@@ -1777,8 +1618,8 @@ export class Engine {
1777
1618
  }
1778
1619
  const pass = encoder.beginRenderPass(this.renderPassDescriptor);
1779
1620
  if (hasModels)
1780
- this.forEachInstance((inst) => this.renderOneModel(pass, inst, false));
1781
- if (this.groundHasReflections)
1621
+ this.forEachInstance((inst) => this.renderOneModel(pass, inst));
1622
+ if (this.hasGround)
1782
1623
  this.renderGround(pass);
1783
1624
  pass.end();
1784
1625
  this.device.queue.submit([encoder.finish()]);
@@ -1795,48 +1636,34 @@ export class Engine {
1795
1636
  sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1796
1637
  }
1797
1638
  }
1798
- renderOneModel(pass, inst, useReflection, mirrorMatrix) {
1799
- pass.setVertexBuffer(0, inst.vertexBuffer);
1800
- pass.setVertexBuffer(1, inst.jointsBuffer);
1801
- pass.setVertexBuffer(2, inst.weightsBuffer);
1802
- pass.setIndexBuffer(inst.indexBuffer, "uint32");
1803
- if (useReflection && mirrorMatrix) {
1804
- this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix);
1805
- pass.setPipeline(this.reflectionPipeline);
1806
- for (const draw of inst.drawCalls) {
1807
- if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
1808
- pass.setBindGroup(0, draw.bindGroup);
1809
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1810
- }
1811
- }
1812
- this.renderEyes(pass, inst, true);
1813
- this.renderHair(pass, inst, true);
1814
- for (const draw of inst.drawCalls) {
1815
- if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
1816
- pass.setBindGroup(0, draw.bindGroup);
1817
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1818
- }
1819
- }
1820
- this.drawOutlines(pass, inst, true, true);
1821
- return;
1822
- }
1823
- pass.setPipeline(this.modelPipeline);
1639
+ drawOpaque(pass, inst, pipeline) {
1640
+ pass.setPipeline(pipeline);
1824
1641
  for (const draw of inst.drawCalls) {
1825
1642
  if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
1826
1643
  pass.setBindGroup(0, draw.bindGroup);
1827
1644
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1828
1645
  }
1829
1646
  }
1830
- this.renderEyes(pass, inst, false);
1831
- this.drawOutlines(pass, inst, false);
1832
- this.renderHair(pass, inst, false);
1833
- pass.setPipeline(this.modelPipeline);
1647
+ }
1648
+ drawTransparent(pass, inst, pipeline) {
1649
+ pass.setPipeline(pipeline);
1834
1650
  for (const draw of inst.drawCalls) {
1835
1651
  if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
1836
1652
  pass.setBindGroup(0, draw.bindGroup);
1837
1653
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1838
1654
  }
1839
1655
  }
1656
+ }
1657
+ renderOneModel(pass, inst) {
1658
+ pass.setVertexBuffer(0, inst.vertexBuffer);
1659
+ pass.setVertexBuffer(1, inst.jointsBuffer);
1660
+ pass.setVertexBuffer(2, inst.weightsBuffer);
1661
+ pass.setIndexBuffer(inst.indexBuffer, "uint32");
1662
+ this.drawOpaque(pass, inst, this.modelPipeline);
1663
+ this.renderEyes(pass, inst);
1664
+ this.drawOutlines(pass, inst, false);
1665
+ this.renderHair(pass, inst);
1666
+ this.drawTransparent(pass, inst, this.modelPipeline);
1840
1667
  this.drawOutlines(pass, inst, true);
1841
1668
  }
1842
1669
  updateCameraUniforms() {
@@ -1851,14 +1678,8 @@ export class Engine {
1851
1678
  this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
1852
1679
  }
1853
1680
  updateRenderTarget() {
1854
- // Update render target to use current canvas texture
1855
1681
  const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
1856
- if (this.sampleCount > 1) {
1857
- colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
1858
- }
1859
- else {
1860
- colorAttachment.view = this.context.getCurrentTexture().createView();
1861
- }
1682
+ colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
1862
1683
  }
1863
1684
  updateSkinMatrices() {
1864
1685
  this.forEachInstance((inst) => {
@@ -1866,9 +1687,7 @@ export class Engine {
1866
1687
  this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, skinMatrices.buffer, skinMatrices.byteOffset, skinMatrices.byteLength);
1867
1688
  });
1868
1689
  }
1869
- drawOutlines(pass, inst, transparent, useReflectionPipeline = false) {
1870
- if (useReflectionPipeline)
1871
- return;
1690
+ drawOutlines(pass, inst, transparent) {
1872
1691
  pass.setPipeline(this.outlinePipeline);
1873
1692
  const outlineType = transparent ? "transparent-outline" : "opaque-outline";
1874
1693
  for (const draw of inst.drawCalls) {
@@ -1900,41 +1719,6 @@ export class Engine {
1900
1719
  this.lastFpsUpdate = now;
1901
1720
  }
1902
1721
  }
1903
- createMirrorMatrix(planeNormal, planeDistance) {
1904
- // Create reflection matrix across a plane
1905
- const n = planeNormal.normalize();
1906
- return new Mat4(new Float32Array([
1907
- 1 - 2 * n.x * n.x,
1908
- -2 * n.x * n.y,
1909
- -2 * n.x * n.z,
1910
- 0,
1911
- -2 * n.y * n.x,
1912
- 1 - 2 * n.y * n.y,
1913
- -2 * n.y * n.z,
1914
- 0,
1915
- -2 * n.z * n.x,
1916
- -2 * n.z * n.y,
1917
- 1 - 2 * n.z * n.z,
1918
- 0,
1919
- -2 * planeDistance * n.x,
1920
- -2 * planeDistance * n.y,
1921
- -2 * planeDistance * n.z,
1922
- 1,
1923
- ]));
1924
- }
1925
- writeMirrorTransformedSkinMatrices(inst, mirrorMatrix) {
1926
- const originalMatrices = inst.model.getSkinMatrices();
1927
- const transformedMatrices = new Float32Array(originalMatrices.length);
1928
- for (let i = 0; i < originalMatrices.length; i += 16) {
1929
- const boneMatrixValues = new Float32Array(16);
1930
- for (let j = 0; j < 16; j++)
1931
- boneMatrixValues[j] = originalMatrices[i + j];
1932
- const boneMatrix = new Mat4(boneMatrixValues);
1933
- const transformed = mirrorMatrix.multiply(boneMatrix);
1934
- for (let j = 0; j < 16; j++)
1935
- transformedMatrices[i + j] = transformed.values[j];
1936
- }
1937
- this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices);
1938
- }
1939
1722
  }
1940
1723
  Engine.instance = null;
1724
+ Engine.MULTISAMPLE_COUNT = 4;
package/dist/model.d.ts CHANGED
@@ -95,10 +95,6 @@ export interface MorphRuntime {
95
95
  weights: Float32Array;
96
96
  }
97
97
  export declare class Model {
98
- private static _nextId;
99
- private static nextDefaultName;
100
- static loadFrom(path: string): Promise<Model>;
101
- static loadFrom(name: string, path: string): Promise<Model>;
102
98
  private _name;
103
99
  get name(): string;
104
100
  setName(value: string): void;
@@ -125,8 +121,6 @@ export declare class Model {
125
121
  private boneTrackIndices;
126
122
  private morphTrackIndices;
127
123
  private lastAppliedClip;
128
- private ikEnabled;
129
- private physicsEnabled;
130
124
  constructor(vertexData: Float32Array<ArrayBuffer>, indexData: Uint32Array<ArrayBuffer>, textures: Texture[], materials: Material[], skeleton: Skeleton, skinning: Skinning, morphing: Morphing, rigidbodies?: Rigidbody[], joints?: Joint[]);
131
125
  private initializeRuntimeSkeleton;
132
126
  private initializeIKRuntime;
@@ -157,9 +151,6 @@ export declare class Model {
157
151
  loadAnimation(animationName: string, vmdUrl: string): Promise<void>;
158
152
  resetAllBones(): void;
159
153
  resetAllMorphs(): void;
160
- setIKEnabled(enabled: boolean): void;
161
- setPhysicsEnabled(enabled: boolean): void;
162
- getPhysicsEnabled(): boolean;
163
154
  getAnimationState(): AnimationState;
164
155
  play(): void;
165
156
  play(name: string): boolean;
@@ -180,7 +171,7 @@ export declare class Model {
180
171
  private static upperBound;
181
172
  private findKeyframeIndex;
182
173
  private applyPoseFromClip;
183
- update(deltaTime: number): boolean;
174
+ update(deltaTime: number, ikEnabled: boolean): boolean;
184
175
  private solveIKChains;
185
176
  private ikComputedSet;
186
177
  private computeSingleBoneWorldMatrix;