reze-engine 0.8.0 → 0.8.1

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 { Physics } from "./physics";
3
4
  export const DEFAULT_ENGINE_OPTIONS = {
4
5
  ambientColor: new Vec3(0.88, 0.88, 0.88),
5
6
  directionalLightIntensity: 0.24,
@@ -9,8 +10,6 @@ export const DEFAULT_ENGINE_OPTIONS = {
9
10
  cameraTarget: new Vec3(0, 12.5, 0),
10
11
  cameraFov: Math.PI / 4,
11
12
  onRaycast: undefined,
12
- disableIK: false,
13
- disablePhysics: false,
14
13
  multisampleCount: 4,
15
14
  };
16
15
  export class Engine {
@@ -31,26 +30,17 @@ export class Engine {
31
30
  this.groundHasReflections = false;
32
31
  this.groundMode = "reflection";
33
32
  this.shadowLightVPMatrix = new Float32Array(16);
34
- this.shadowDrawCalls = [];
33
+ this.groundDrawCall = null;
35
34
  this.shadowVPLightX = Number.NaN;
36
35
  this.shadowVPLightY = Number.NaN;
37
36
  this.shadowVPLightZ = Number.NaN;
38
- this.cachedSkinMatricesVersion = -1;
39
- this.skinMatricesVersion = 0;
40
37
  // Double-tap detection
41
38
  this.lastTouchTime = 0;
42
39
  this.DOUBLE_TAP_DELAY = 300; // ms
43
- // IK and Physics flags
44
- this._disableIK = false;
45
- this._disablePhysics = false;
46
- this.currentModel = null;
47
- this.modelDir = "";
40
+ this.modelInstances = new Map();
48
41
  this.textureCache = new Map();
49
- this.vertexBufferNeedsUpdate = false;
50
- // Unified draw call list
51
- this.drawCalls = [];
52
- // Material visibility tracking
53
- this.hiddenMaterials = new Set();
42
+ /** Reusable buffer for raycast skinning to avoid per-instance allocations (Three.js/Babylon.js style). */
43
+ this.raycastVertexBuffer = null;
54
44
  this.lastFpsUpdate = performance.now();
55
45
  this.framesSinceLastUpdate = 0;
56
46
  this.lastFrameTime = performance.now();
@@ -63,15 +53,13 @@ export class Engine {
63
53
  this.animationFrameId = null;
64
54
  this.renderLoopCallback = null;
65
55
  this.handleCanvasDoubleClick = (event) => {
66
- if (!this.onRaycast || !this.currentModel)
56
+ if (!this.onRaycast || this.modelInstances.size === 0)
67
57
  return;
68
58
  const rect = this.canvas.getBoundingClientRect();
69
- const x = event.clientX - rect.left;
70
- const y = event.clientY - rect.top;
71
- this.performRaycast(x, y);
59
+ this.performRaycast(event.clientX - rect.left, event.clientY - rect.top);
72
60
  };
73
61
  this.handleCanvasTouch = (event) => {
74
- if (!this.onRaycast || !this.currentModel)
62
+ if (!this.onRaycast || this.modelInstances.size === 0)
75
63
  return;
76
64
  // Prevent default to avoid triggering mouse events
77
65
  event.preventDefault();
@@ -107,8 +95,6 @@ export class Engine {
107
95
  this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget;
108
96
  this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov;
109
97
  this.onRaycast = options.onRaycast;
110
- this._disableIK = options.disableIK ?? DEFAULT_ENGINE_OPTIONS.disableIK;
111
- this._disablePhysics = options.disablePhysics ?? DEFAULT_ENGINE_OPTIONS.disablePhysics;
112
98
  }
113
99
  }
114
100
  // Step 1: Get WebGPU device and context
@@ -968,7 +954,6 @@ export class Engine {
968
954
  ...options,
969
955
  };
970
956
  this.groundMode = opts.mode;
971
- this.drawCalls = this.drawCalls.filter((d) => d.type !== "ground");
972
957
  this.createGroundGeometry(opts.width, opts.height);
973
958
  if (opts.mode === "reflection") {
974
959
  this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd);
@@ -978,13 +963,13 @@ export class Engine {
978
963
  this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength);
979
964
  }
980
965
  this.groundHasReflections = true;
981
- this.drawCalls.push({
966
+ this.groundDrawCall = {
982
967
  type: "ground",
983
968
  count: 6,
984
969
  firstIndex: 0,
985
970
  bindGroup: (opts.mode === "reflection" ? this.groundReflectionBindGroup : this.groundShadowBindGroup),
986
971
  materialName: "Ground",
987
- });
972
+ };
988
973
  }
989
974
  updateLightBuffer() {
990
975
  this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
@@ -1012,7 +997,7 @@ export class Engine {
1012
997
  }
1013
998
  dispose() {
1014
999
  this.stopRenderLoop();
1015
- this.currentModel?.stopAnimation();
1000
+ this.forEachInstance((inst) => inst.model.stopAnimation());
1016
1001
  if (Engine.instance === this)
1017
1002
  Engine.instance = null;
1018
1003
  if (this.camera)
@@ -1027,116 +1012,175 @@ export class Engine {
1027
1012
  this.resizeObserver = null;
1028
1013
  }
1029
1014
  }
1030
- // Single active model; prefer Model.loadPmx() so load + register stay paired
1031
- async registerModel(model, pmxPath) {
1015
+ async addModel(model, pmxPath, name) {
1016
+ const requested = name ?? model.name;
1017
+ let key = requested;
1018
+ let n = 1;
1019
+ while (this.modelInstances.has(key)) {
1020
+ key = `${requested}_${n++}`;
1021
+ }
1032
1022
  const pathParts = pmxPath.split("/");
1033
1023
  pathParts.pop();
1034
- this.modelDir = pathParts.join("/") + "/";
1035
- this.cachedSkinnedVertices = undefined;
1036
- this.cachedSkinMatricesVersion = -1;
1037
- await this.setupModelBuffers(model);
1024
+ const basePath = pathParts.join("/") + "/";
1025
+ await this.setupModelInstance(key, model, basePath);
1026
+ return key;
1038
1027
  }
1039
- // After morph/vertex edits, queues GPU vertex upload on next frame
1040
- markVertexBufferDirty() {
1041
- this.vertexBufferNeedsUpdate = true;
1028
+ async registerModel(model, pmxPath) {
1029
+ return this.addModel(model, pmxPath);
1042
1030
  }
1043
- setMaterialVisible(name, visible) {
1044
- if (visible) {
1045
- this.hiddenMaterials.delete(name);
1046
- }
1047
- else {
1048
- this.hiddenMaterials.add(name);
1049
- }
1031
+ removeModel(name) {
1032
+ this.modelInstances.delete(name);
1033
+ }
1034
+ getModelNames() {
1035
+ return Array.from(this.modelInstances.keys());
1050
1036
  }
1051
- toggleMaterialVisible(name) {
1052
- if (this.hiddenMaterials.has(name)) {
1053
- this.hiddenMaterials.delete(name);
1037
+ getModel(name) {
1038
+ return this.modelInstances.get(name)?.model ?? null;
1039
+ }
1040
+ markVertexBufferDirty(modelNameOrModel) {
1041
+ if (modelNameOrModel === undefined)
1042
+ return;
1043
+ if (typeof modelNameOrModel === "string") {
1044
+ const inst = this.modelInstances.get(modelNameOrModel);
1045
+ if (inst)
1046
+ inst.vertexBufferNeedsUpdate = true;
1047
+ return;
1054
1048
  }
1055
- else {
1056
- this.hiddenMaterials.add(name);
1049
+ for (const inst of this.modelInstances.values()) {
1050
+ if (inst.model === modelNameOrModel) {
1051
+ inst.vertexBufferNeedsUpdate = true;
1052
+ return;
1053
+ }
1057
1054
  }
1058
1055
  }
1059
- isMaterialVisible(name) {
1060
- return !this.hiddenMaterials.has(name);
1056
+ setMaterialVisible(modelName, materialName, visible) {
1057
+ const inst = this.modelInstances.get(modelName);
1058
+ if (!inst)
1059
+ return;
1060
+ if (visible)
1061
+ inst.hiddenMaterials.delete(materialName);
1062
+ else
1063
+ inst.hiddenMaterials.add(materialName);
1064
+ }
1065
+ toggleMaterialVisible(modelName, materialName) {
1066
+ const inst = this.modelInstances.get(modelName);
1067
+ if (!inst)
1068
+ return;
1069
+ if (inst.hiddenMaterials.has(materialName))
1070
+ inst.hiddenMaterials.delete(materialName);
1071
+ else
1072
+ inst.hiddenMaterials.add(materialName);
1061
1073
  }
1062
- // IK control
1063
- get disableIK() {
1064
- return this._disableIK;
1074
+ isMaterialVisible(modelName, materialName) {
1075
+ const inst = this.modelInstances.get(modelName);
1076
+ return inst ? !inst.hiddenMaterials.has(materialName) : false;
1065
1077
  }
1066
- set disableIK(value) {
1067
- this._disableIK = value;
1068
- this.currentModel?.setIKEnabled(!value);
1078
+ setModelIKEnabled(modelName, enabled) {
1079
+ this.modelInstances.get(modelName)?.model.setIKEnabled(enabled);
1069
1080
  }
1070
- // Physics control
1071
- get disablePhysics() {
1072
- return this._disablePhysics;
1081
+ setModelPhysicsEnabled(modelName, enabled) {
1082
+ this.modelInstances.get(modelName)?.model.setPhysicsEnabled(enabled);
1073
1083
  }
1074
- set disablePhysics(value) {
1075
- this._disablePhysics = value;
1076
- this.currentModel?.setPhysicsEnabled(!value);
1084
+ resetPhysics() {
1085
+ this.forEachInstance((inst) => {
1086
+ if (!inst.physics)
1087
+ return;
1088
+ inst.model.computeWorldMatrices();
1089
+ inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
1090
+ });
1077
1091
  }
1078
- updateVertexBuffer() {
1079
- if (!this.currentModel || !this.vertexBuffer)
1080
- return;
1081
- const vertices = this.currentModel.getVertices();
1082
- if (!vertices || vertices.length === 0)
1092
+ instances() {
1093
+ return this.modelInstances.values();
1094
+ }
1095
+ forEachInstance(fn) {
1096
+ for (const inst of this.instances())
1097
+ fn(inst);
1098
+ }
1099
+ updateInstances(deltaTime) {
1100
+ this.forEachInstance((inst) => {
1101
+ const verticesChanged = inst.model.update(deltaTime);
1102
+ if (verticesChanged)
1103
+ inst.vertexBufferNeedsUpdate = true;
1104
+ if (inst.physics && inst.model.getPhysicsEnabled()) {
1105
+ inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
1106
+ }
1107
+ if (inst.vertexBufferNeedsUpdate)
1108
+ this.updateVertexBuffer(inst);
1109
+ });
1110
+ }
1111
+ updateVertexBuffer(inst) {
1112
+ const vertices = inst.model.getVertices();
1113
+ if (!vertices?.length)
1083
1114
  return;
1084
- this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices);
1115
+ this.device.queue.writeBuffer(inst.vertexBuffer, 0, vertices);
1116
+ inst.vertexBufferNeedsUpdate = false;
1085
1117
  }
1086
- // Step 7: Create vertex, index, and joint buffers
1087
- async setupModelBuffers(model) {
1088
- this.currentModel = model;
1089
- // Apply IK and Physics flags from engine options
1090
- model.setIKEnabled(!this._disableIK);
1091
- model.setPhysicsEnabled(!this._disablePhysics);
1118
+ async setupModelInstance(name, model, basePath) {
1092
1119
  const vertices = model.getVertices();
1093
1120
  const skinning = model.getSkinning();
1094
1121
  const skeleton = model.getSkeleton();
1095
- this.vertexBuffer = this.device.createBuffer({
1096
- label: "model vertex buffer",
1122
+ const boneCount = skeleton.bones.length;
1123
+ const matrixSize = boneCount * 16 * 4;
1124
+ const vertexBuffer = this.device.createBuffer({
1125
+ label: `${name}: vertex buffer`,
1097
1126
  size: vertices.byteLength,
1098
1127
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1099
1128
  });
1100
- this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices);
1101
- this.jointsBuffer = this.device.createBuffer({
1102
- label: "joints buffer",
1129
+ this.device.queue.writeBuffer(vertexBuffer, 0, vertices);
1130
+ const jointsBuffer = this.device.createBuffer({
1131
+ label: `${name}: joints buffer`,
1103
1132
  size: skinning.joints.byteLength,
1104
1133
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1105
1134
  });
1106
- this.device.queue.writeBuffer(this.jointsBuffer, 0, skinning.joints.buffer, skinning.joints.byteOffset, skinning.joints.byteLength);
1107
- this.weightsBuffer = this.device.createBuffer({
1108
- label: "weights buffer",
1135
+ this.device.queue.writeBuffer(jointsBuffer, 0, skinning.joints.buffer, skinning.joints.byteOffset, skinning.joints.byteLength);
1136
+ const weightsBuffer = this.device.createBuffer({
1137
+ label: `${name}: weights buffer`,
1109
1138
  size: skinning.weights.byteLength,
1110
1139
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1111
1140
  });
1112
- this.device.queue.writeBuffer(this.weightsBuffer, 0, skinning.weights.buffer, skinning.weights.byteOffset, skinning.weights.byteLength);
1113
- const boneCount = skeleton.bones.length;
1114
- const matrixSize = boneCount * 16 * 4;
1115
- this.skinMatrixBuffer = this.device.createBuffer({
1116
- label: "skin matrices",
1141
+ this.device.queue.writeBuffer(weightsBuffer, 0, skinning.weights.buffer, skinning.weights.byteOffset, skinning.weights.byteLength);
1142
+ const skinMatrixBuffer = this.device.createBuffer({
1143
+ label: `${name}: skin matrices`,
1117
1144
  size: Math.max(256, matrixSize),
1118
1145
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1119
1146
  });
1120
- this.inverseBindMatrixBuffer = this.device.createBuffer({
1121
- label: "inverse bind matrices",
1122
- size: Math.max(256, matrixSize),
1123
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1124
- });
1125
- const invBindMatrices = skeleton.inverseBindMatrices;
1126
- this.device.queue.writeBuffer(this.inverseBindMatrixBuffer, 0, invBindMatrices.buffer, invBindMatrices.byteOffset, invBindMatrices.byteLength);
1127
1147
  const indices = model.getIndices();
1128
- if (indices) {
1129
- this.indexBuffer = this.device.createBuffer({
1130
- label: "model index buffer",
1131
- size: indices.byteLength,
1132
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
1133
- });
1134
- this.device.queue.writeBuffer(this.indexBuffer, 0, indices);
1135
- }
1136
- else {
1148
+ if (!indices)
1137
1149
  throw new Error("Model has no index buffer");
1138
- }
1139
- await this.setupMaterials(model);
1150
+ const indexBuffer = this.device.createBuffer({
1151
+ label: `${name}: index buffer`,
1152
+ size: indices.byteLength,
1153
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
1154
+ });
1155
+ this.device.queue.writeBuffer(indexBuffer, 0, indices);
1156
+ const rbs = model.getRigidbodies();
1157
+ const physics = rbs.length > 0 ? new Physics(rbs, model.getJoints()) : null;
1158
+ const shadowBindGroup = this.device.createBindGroup({
1159
+ label: `${name}: shadow bind`,
1160
+ layout: this.shadowDepthPipeline.getBindGroupLayout(0),
1161
+ entries: [
1162
+ { binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
1163
+ { binding: 1, resource: { buffer: skinMatrixBuffer } },
1164
+ ],
1165
+ });
1166
+ const inst = {
1167
+ name,
1168
+ model,
1169
+ basePath,
1170
+ vertexBuffer,
1171
+ indexBuffer,
1172
+ jointsBuffer,
1173
+ weightsBuffer,
1174
+ skinMatrixBuffer,
1175
+ drawCalls: [],
1176
+ shadowDrawCalls: [],
1177
+ shadowBindGroup,
1178
+ hiddenMaterials: new Set(),
1179
+ physics,
1180
+ vertexBufferNeedsUpdate: false,
1181
+ };
1182
+ await this.setupMaterialsForInstance(inst);
1183
+ this.modelInstances.set(name, inst);
1140
1184
  }
1141
1185
  createGroundGeometry(width = 100, height = 100) {
1142
1186
  const halfWidth = width / 2;
@@ -1271,14 +1315,6 @@ export class Engine {
1271
1315
  gb[7] = 0;
1272
1316
  this.groundShadowMaterialBuffer = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
1273
1317
  this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 0, gb);
1274
- this.shadowBindGroup = this.device.createBindGroup({
1275
- label: "shadow bind",
1276
- layout: this.shadowDepthPipeline.getBindGroupLayout(0),
1277
- entries: [
1278
- { binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
1279
- { binding: 1, resource: { buffer: this.skinMatrixBuffer } },
1280
- ],
1281
- });
1282
1318
  this.groundShadowBindGroup = this.device.createBindGroup({
1283
1319
  label: "ground shadow bind",
1284
1320
  layout: this.groundShadowBindGroupLayout,
@@ -1317,21 +1353,19 @@ export class Engine {
1317
1353
  this.shadowLightVPMatrix.set(vp.values);
1318
1354
  this.device.queue.writeBuffer(this.shadowLightVPBuffer, 0, this.shadowLightVPMatrix);
1319
1355
  }
1320
- async setupMaterials(model) {
1356
+ async setupMaterialsForInstance(inst) {
1357
+ const model = inst.model;
1321
1358
  const materials = model.getMaterials();
1322
- if (materials.length === 0) {
1359
+ if (materials.length === 0)
1323
1360
  throw new Error("Model has no materials");
1324
- }
1325
1361
  const textures = model.getTextures();
1362
+ const prefix = `${inst.name}: `;
1326
1363
  const loadTextureByIndex = async (texIndex) => {
1327
- if (texIndex < 0 || texIndex >= textures.length) {
1364
+ if (texIndex < 0 || texIndex >= textures.length)
1328
1365
  return null;
1329
- }
1330
- const path = this.modelDir + textures[texIndex].path;
1331
- const texture = await this.createTextureFromPath(path);
1332
- return texture;
1366
+ const path = inst.basePath + textures[texIndex].path;
1367
+ return this.createTextureFromPath(path);
1333
1368
  };
1334
- this.drawCalls = [];
1335
1369
  let currentIndexOffset = 0;
1336
1370
  for (const mat of materials) {
1337
1371
  const indexCount = mat.vertexCount;
@@ -1342,128 +1376,86 @@ export class Engine {
1342
1376
  throw new Error(`Material "${mat.name}" has no diffuse texture`);
1343
1377
  const materialAlpha = mat.diffuse[3];
1344
1378
  const isTransparent = materialAlpha < 1.0 - 0.001;
1345
- const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
1346
- // Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
1379
+ const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha, 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
1347
1380
  const bindGroup = this.device.createBindGroup({
1348
- label: `material bind group: ${mat.name}`,
1381
+ label: `${prefix}material: ${mat.name}`,
1349
1382
  layout: this.mainBindGroupLayout,
1350
1383
  entries: [
1351
1384
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1352
1385
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1353
1386
  { binding: 2, resource: diffuseTexture.createView() },
1354
1387
  { binding: 3, resource: this.materialSampler },
1355
- { binding: 4, resource: { buffer: this.skinMatrixBuffer } },
1388
+ { binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
1356
1389
  { binding: 5, resource: { buffer: materialUniformBuffer } },
1357
1390
  ],
1358
1391
  });
1359
1392
  if (indexCount > 0) {
1360
1393
  if (mat.isEye) {
1361
- this.drawCalls.push({
1362
- type: "eye",
1363
- count: indexCount,
1364
- firstIndex: currentIndexOffset,
1365
- bindGroup,
1366
- materialName: mat.name,
1367
- });
1394
+ inst.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
1368
1395
  }
1369
1396
  else if (mat.isHair) {
1370
- // Hair materials: create separate bind groups for over-eyes vs over-non-eyes
1371
1397
  const createHairBindGroup = (isOverEyes) => {
1372
- const buffer = this.createMaterialUniformBuffer(`${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`, materialAlpha, isOverEyes ? 1.0 : 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
1398
+ const buf = this.createMaterialUniformBuffer(`${prefix}${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`, materialAlpha, isOverEyes ? 1.0 : 0.0, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
1373
1399
  return this.device.createBindGroup({
1374
- label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
1400
+ label: `${prefix}hair ${isOverEyes ? "over eyes" : "over non-eyes"}: ${mat.name}`,
1375
1401
  layout: this.mainBindGroupLayout,
1376
1402
  entries: [
1377
1403
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1378
1404
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1379
1405
  { binding: 2, resource: diffuseTexture.createView() },
1380
1406
  { binding: 3, resource: this.materialSampler },
1381
- { binding: 4, resource: { buffer: this.skinMatrixBuffer } },
1382
- { binding: 5, resource: { buffer: buffer } },
1407
+ { binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
1408
+ { binding: 5, resource: { buffer: buf } },
1383
1409
  ],
1384
1410
  });
1385
1411
  };
1386
- const bindGroupOverEyes = createHairBindGroup(true);
1387
- const bindGroupOverNonEyes = createHairBindGroup(false);
1388
- this.drawCalls.push({
1412
+ inst.drawCalls.push({
1389
1413
  type: "hair-over-eyes",
1390
1414
  count: indexCount,
1391
1415
  firstIndex: currentIndexOffset,
1392
- bindGroup: bindGroupOverEyes,
1416
+ bindGroup: createHairBindGroup(true),
1393
1417
  materialName: mat.name,
1394
1418
  });
1395
- this.drawCalls.push({
1419
+ inst.drawCalls.push({
1396
1420
  type: "hair-over-non-eyes",
1397
1421
  count: indexCount,
1398
1422
  firstIndex: currentIndexOffset,
1399
- bindGroup: bindGroupOverNonEyes,
1423
+ bindGroup: createHairBindGroup(false),
1400
1424
  materialName: mat.name,
1401
1425
  });
1402
1426
  }
1403
1427
  else if (isTransparent) {
1404
- this.drawCalls.push({
1405
- type: "transparent",
1406
- count: indexCount,
1407
- firstIndex: currentIndexOffset,
1408
- bindGroup,
1409
- materialName: mat.name,
1410
- });
1428
+ inst.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
1411
1429
  }
1412
1430
  else {
1413
- this.drawCalls.push({
1414
- type: "opaque",
1415
- count: indexCount,
1416
- firstIndex: currentIndexOffset,
1417
- bindGroup,
1418
- materialName: mat.name,
1419
- });
1431
+ inst.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name });
1420
1432
  }
1421
1433
  }
1422
- // Edge flag is at bit 4 (0x10) in PMX format
1423
1434
  if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
1424
1435
  const materialUniformData = new Float32Array([
1425
- mat.edgeColor[0],
1426
- mat.edgeColor[1],
1427
- mat.edgeColor[2],
1428
- mat.edgeColor[3],
1429
- mat.edgeSize,
1430
- 0,
1431
- 0,
1432
- 0,
1436
+ mat.edgeColor[0], mat.edgeColor[1], mat.edgeColor[2], mat.edgeColor[3],
1437
+ mat.edgeSize, 0, 0, 0,
1433
1438
  ]);
1434
- const materialUniformBuffer = this.createUniformBuffer(`outline material uniform: ${mat.name}`, materialUniformData);
1439
+ const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData);
1435
1440
  const outlineBindGroup = this.device.createBindGroup({
1436
- label: `outline bind group: ${mat.name}`,
1441
+ label: `${prefix}outline: ${mat.name}`,
1437
1442
  layout: this.outlineBindGroupLayout,
1438
1443
  entries: [
1439
1444
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1440
- { binding: 1, resource: { buffer: materialUniformBuffer } },
1441
- { binding: 2, resource: { buffer: this.skinMatrixBuffer } },
1445
+ { binding: 1, resource: { buffer: outlineUniformBuffer } },
1446
+ { binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
1442
1447
  ],
1443
1448
  });
1444
1449
  if (indexCount > 0) {
1445
- const outlineType = mat.isEye
1446
- ? "eye-outline"
1447
- : mat.isHair
1448
- ? "hair-outline"
1449
- : isTransparent
1450
- ? "transparent-outline"
1451
- : "opaque-outline";
1452
- this.drawCalls.push({
1453
- type: outlineType,
1454
- count: indexCount,
1455
- firstIndex: currentIndexOffset,
1456
- bindGroup: outlineBindGroup,
1457
- materialName: mat.name,
1458
- });
1450
+ const outlineType = mat.isEye ? "eye-outline" : mat.isHair ? "hair-outline" : isTransparent ? "transparent-outline" : "opaque-outline";
1451
+ inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name });
1459
1452
  }
1460
1453
  }
1461
1454
  currentIndexOffset += indexCount;
1462
1455
  }
1463
- this.shadowDrawCalls.length = 0;
1464
- for (const d of this.drawCalls) {
1456
+ for (const d of inst.drawCalls) {
1465
1457
  if (d.type === "opaque" || d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes")
1466
- this.shadowDrawCalls.push(d);
1458
+ inst.shadowDrawCalls.push(d);
1467
1459
  }
1468
1460
  }
1469
1461
  createMaterialUniformBuffer(label, alpha, isOverEyes, diffuseColor, ambientColor, specularColor, shininess) {
@@ -1501,8 +1493,8 @@ export class Engine {
1501
1493
  this.device.queue.writeBuffer(buffer, 0, data);
1502
1494
  return buffer;
1503
1495
  }
1504
- shouldRenderDrawCall(drawCall) {
1505
- return !this.hiddenMaterials.has(drawCall.materialName);
1496
+ shouldRenderDrawCall(inst, drawCall) {
1497
+ return !inst.hiddenMaterials.has(drawCall.materialName);
1506
1498
  }
1507
1499
  async createTextureFromPath(path) {
1508
1500
  const cached = this.textureCache.get(path);
@@ -1536,11 +1528,10 @@ export class Engine {
1536
1528
  }
1537
1529
  }
1538
1530
  // Helper: Render eyes with stencil writing (for post-alpha-eye effect)
1539
- renderEyes(pass, useReflectionPipeline = false) {
1531
+ renderEyes(pass, inst, useReflectionPipeline = false) {
1540
1532
  if (useReflectionPipeline) {
1541
- // For reflections, use the basic reflection pipeline instead of specialized eye pipeline
1542
1533
  pass.setPipeline(this.reflectionPipeline);
1543
- for (const draw of this.drawCalls) {
1534
+ for (const draw of inst.drawCalls) {
1544
1535
  if (draw.type === "eye") {
1545
1536
  pass.setBindGroup(0, draw.bindGroup);
1546
1537
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
@@ -1550,8 +1541,8 @@ export class Engine {
1550
1541
  else {
1551
1542
  pass.setPipeline(this.eyePipeline);
1552
1543
  pass.setStencilReference(this.STENCIL_EYE_VALUE);
1553
- for (const draw of this.drawCalls) {
1554
- if (draw.type === "eye" && this.shouldRenderDrawCall(draw)) {
1544
+ for (const draw of inst.drawCalls) {
1545
+ if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
1555
1546
  pass.setBindGroup(0, draw.bindGroup);
1556
1547
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1557
1548
  }
@@ -1559,19 +1550,15 @@ export class Engine {
1559
1550
  }
1560
1551
  }
1561
1552
  renderGround(pass) {
1562
- if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer)
1553
+ if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
1563
1554
  return;
1564
1555
  if (this.groundMode === "reflection" && this.groundReflectionTexture)
1565
1556
  this.renderReflectionTexture();
1566
1557
  pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline);
1567
1558
  pass.setVertexBuffer(0, this.groundVertexBuffer);
1568
1559
  pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
1569
- for (const draw of this.drawCalls) {
1570
- if (draw.type === "ground" && this.shouldRenderDrawCall(draw)) {
1571
- pass.setBindGroup(0, draw.bindGroup);
1572
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1573
- }
1574
- }
1560
+ pass.setBindGroup(0, this.groundDrawCall.bindGroup);
1561
+ pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0);
1575
1562
  }
1576
1563
  renderReflectionTexture() {
1577
1564
  if (!this.groundReflectionTexture)
@@ -1601,45 +1588,15 @@ export class Engine {
1601
1588
  },
1602
1589
  };
1603
1590
  const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor);
1604
- if (this.currentModel) {
1605
- reflectionPass.setVertexBuffer(0, this.vertexBuffer);
1606
- reflectionPass.setVertexBuffer(1, this.jointsBuffer);
1607
- reflectionPass.setVertexBuffer(2, this.weightsBuffer);
1608
- reflectionPass.setIndexBuffer(this.indexBuffer, "uint32");
1609
- this.writeMirrorTransformedSkinMatrices(mirrorMatrix);
1610
- reflectionPass.setPipeline(this.reflectionPipeline);
1611
- for (const draw of this.drawCalls) {
1612
- if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
1613
- reflectionPass.setBindGroup(0, draw.bindGroup);
1614
- reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1615
- }
1616
- }
1617
- // Render eyes (using reflection pipeline)
1618
- this.renderEyes(reflectionPass, true);
1619
- // Render hair (using reflection pipeline)
1620
- this.renderHair(reflectionPass, true);
1621
- // Render transparent objects
1622
- for (const draw of this.drawCalls) {
1623
- if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
1624
- reflectionPass.setBindGroup(0, draw.bindGroup);
1625
- reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1626
- }
1627
- }
1628
- this.drawOutlines(reflectionPass, true, true);
1629
- }
1591
+ this.forEachInstance((inst) => this.renderOneModel(reflectionPass, inst, true, mirrorMatrix));
1630
1592
  reflectionPass.end();
1631
- // Submit reflection rendering commands
1632
- const reflectionCommandBuffer = reflectionEncoder.finish();
1633
- this.device.queue.submit([reflectionCommandBuffer]);
1634
- // Restore original skin matrices
1593
+ this.device.queue.submit([reflectionEncoder.finish()]);
1635
1594
  this.updateSkinMatrices();
1636
1595
  }
1637
- // Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
1638
- renderHair(pass, useReflectionPipeline = false) {
1596
+ renderHair(pass, inst, useReflectionPipeline = false) {
1639
1597
  if (useReflectionPipeline) {
1640
- // For reflections, use the basic reflection pipeline for all hair
1641
1598
  pass.setPipeline(this.reflectionPipeline);
1642
- for (const draw of this.drawCalls) {
1599
+ for (const draw of inst.drawCalls) {
1643
1600
  if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
1644
1601
  pass.setBindGroup(0, draw.bindGroup);
1645
1602
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
@@ -1647,19 +1604,17 @@ export class Engine {
1647
1604
  }
1648
1605
  return;
1649
1606
  }
1650
- // Hair depth pre-pass (reduces overdraw via early depth rejection)
1651
- const hasHair = this.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(d));
1607
+ const hasHair = inst.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d));
1652
1608
  if (hasHair) {
1653
1609
  pass.setPipeline(this.hairDepthPipeline);
1654
- for (const draw of this.drawCalls) {
1655
- if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(draw)) {
1610
+ for (const draw of inst.drawCalls) {
1611
+ if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, draw)) {
1656
1612
  pass.setBindGroup(0, draw.bindGroup);
1657
1613
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1658
1614
  }
1659
1615
  }
1660
1616
  }
1661
- // Hair shading (split by stencil for transparency over eyes)
1662
- const hairOverEyes = this.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(d));
1617
+ const hairOverEyes = inst.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(inst, d));
1663
1618
  if (hairOverEyes.length > 0) {
1664
1619
  pass.setPipeline(this.hairPipelineOverEyes);
1665
1620
  pass.setStencilReference(this.STENCIL_EYE_VALUE);
@@ -1668,7 +1623,7 @@ export class Engine {
1668
1623
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1669
1624
  }
1670
1625
  }
1671
- const hairOverNonEyes = this.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(d));
1626
+ const hairOverNonEyes = inst.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(inst, d));
1672
1627
  if (hairOverNonEyes.length > 0) {
1673
1628
  pass.setPipeline(this.hairPipelineOverNonEyes);
1674
1629
  pass.setStencilReference(this.STENCIL_EYE_VALUE);
@@ -1677,8 +1632,7 @@ export class Engine {
1677
1632
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1678
1633
  }
1679
1634
  }
1680
- // Hair outlines
1681
- const hairOutlines = this.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(d));
1635
+ const hairOutlines = inst.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(inst, d));
1682
1636
  if (hairOutlines.length > 0) {
1683
1637
  pass.setPipeline(this.hairOutlinePipeline);
1684
1638
  for (const draw of hairOutlines) {
@@ -1688,261 +1642,203 @@ export class Engine {
1688
1642
  }
1689
1643
  }
1690
1644
  performRaycast(screenX, screenY) {
1691
- if (!this.currentModel || !this.onRaycast)
1645
+ if (!this.onRaycast || this.modelInstances.size === 0) {
1646
+ this.onRaycast?.("", null, screenX, screenY);
1692
1647
  return;
1693
- const materials = this.currentModel.getMaterials();
1694
- if (materials.length === 0)
1695
- return;
1696
- // Get camera matrices
1648
+ }
1697
1649
  const viewMatrix = this.camera.getViewMatrix();
1698
1650
  const projectionMatrix = this.camera.getProjectionMatrix();
1699
- // Convert screen coordinates to world space ray
1700
- const canvas = this.canvas;
1701
- const rect = canvas.getBoundingClientRect();
1702
- // Convert to clip space (-1 to 1)
1651
+ const rect = this.canvas.getBoundingClientRect();
1703
1652
  const clipX = (screenX / rect.width) * 2 - 1;
1704
- const clipY = 1 - (screenY / rect.height) * 2; // Flip Y
1705
- // Create ray in clip space at near and far planes
1706
- const clipNear = new Vec3(clipX, clipY, -1); // Near plane
1707
- const clipFar = new Vec3(clipX, clipY, 1); // Far plane
1708
- // Transform to world space using inverse view-projection matrix
1653
+ const clipY = 1 - (screenY / rect.height) * 2;
1709
1654
  const viewProjMatrix = projectionMatrix.multiply(viewMatrix);
1710
1655
  const inverseViewProj = viewProjMatrix.inverse();
1711
- // Transform point through 4x4 matrix with perspective division
1712
1656
  const transformPoint = (matrix, point) => {
1713
1657
  const m = matrix.values;
1714
1658
  const x = point.x, y = point.y, z = point.z;
1715
- // Compute transformed point (matrix * vec4(point, 1.0))
1716
1659
  const result = new Vec3(m[0] * x + m[4] * y + m[8] * z + m[12], m[1] * x + m[5] * y + m[9] * z + m[13], m[2] * x + m[6] * y + m[10] * z + m[14]);
1717
- // Perspective division
1718
1660
  const w = m[3] * x + m[7] * y + m[11] * z + m[15];
1719
- const invW = w !== 0 ? 1 / w : 1;
1720
- return result.scale(invW);
1661
+ return result.scale(w !== 0 ? 1 / w : 1);
1721
1662
  };
1722
- const worldNear = transformPoint(inverseViewProj, clipNear);
1723
- const worldFar = transformPoint(inverseViewProj, clipFar);
1724
- // Create ray from camera position through the clicked point
1663
+ const worldNear = transformPoint(inverseViewProj, new Vec3(clipX, clipY, -1));
1664
+ const worldFar = transformPoint(inverseViewProj, new Vec3(clipX, clipY, 1));
1725
1665
  const rayOrigin = this.camera.getPosition();
1726
1666
  const rayDirection = worldFar.subtract(worldNear).normalize();
1727
- // Get model geometry for ray-triangle intersection
1728
- const baseVertices = this.currentModel.getVertices();
1729
- const indices = this.currentModel.getIndices();
1730
- const skinning = this.currentModel.getSkinning();
1731
- if (!baseVertices || !indices || !skinning) {
1732
- if (this.onRaycast) {
1733
- this.onRaycast(null, screenX, screenY);
1734
- }
1735
- return;
1736
- }
1737
- // Use cached skinned vertices if available and up-to-date
1738
- let vertices;
1739
- if (this.cachedSkinnedVertices && this.cachedSkinMatricesVersion === this.skinMatricesVersion) {
1740
- vertices = this.cachedSkinnedVertices;
1741
- }
1742
- else {
1743
- // Apply current skinning transformations to get animated vertex positions
1744
- vertices = new Float32Array(baseVertices.length);
1745
- const skinMatrices = this.currentModel.getSkinMatrices();
1746
- // Helper function to transform point by 4x4 matrix
1747
- const transformByMatrix = (matrix, offset, point) => {
1748
- const m = matrix;
1749
- const x = point.x, y = point.y, z = point.z;
1750
- return new Vec3(m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12], m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13], m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]);
1751
- };
1667
+ const transformByMatrix = (matrix, offset, point) => {
1668
+ const m = matrix, x = point.x, y = point.y, z = point.z;
1669
+ return new Vec3(m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12], m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13], m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]);
1670
+ };
1671
+ let closest = null;
1672
+ const maxDistance = 1000;
1673
+ this.forEachInstance((inst) => {
1674
+ const model = inst.model;
1675
+ const materials = model.getMaterials();
1676
+ if (materials.length === 0)
1677
+ return;
1678
+ const baseVertices = model.getVertices();
1679
+ const indices = model.getIndices();
1680
+ const skinning = model.getSkinning();
1681
+ if (!baseVertices?.length || !indices || !skinning)
1682
+ return;
1683
+ const vertices = new Float32Array(baseVertices.length);
1684
+ const skinMatrices = model.getSkinMatrices();
1752
1685
  for (let i = 0; i < baseVertices.length; i += 8) {
1753
- const vertexIndex = Math.floor(i / 8);
1686
+ const vertexIndex = i / 8;
1754
1687
  const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2]);
1755
- // Get bone influences for this vertex
1756
- const jointIndices = [
1757
- skinning.joints[vertexIndex * 4],
1758
- skinning.joints[vertexIndex * 4 + 1],
1759
- skinning.joints[vertexIndex * 4 + 2],
1760
- skinning.joints[vertexIndex * 4 + 3],
1761
- ];
1762
- const weights = [
1763
- skinning.weights[vertexIndex * 4],
1764
- skinning.weights[vertexIndex * 4 + 1],
1765
- skinning.weights[vertexIndex * 4 + 2],
1766
- skinning.weights[vertexIndex * 4 + 3],
1767
- ];
1768
- // Normalize weights (same as shader)
1769
- const weightSum = weights[0] + weights[1] + weights[2] + weights[3];
1770
- const invWeightSum = weightSum > 0.0001 ? 1.0 / weightSum : 1.0;
1771
- const normalizedWeights = weightSum > 0.0001 ? weights.map((w) => w * invWeightSum) : [1.0, 0.0, 0.0, 0.0];
1772
- // Apply skinning transformation (same as shader)
1773
- let skinnedPosition = new Vec3(0, 0, 0);
1688
+ const j0 = skinning.joints[vertexIndex * 4], j1 = skinning.joints[vertexIndex * 4 + 1], j2 = skinning.joints[vertexIndex * 4 + 2], j3 = skinning.joints[vertexIndex * 4 + 3];
1689
+ const w0 = skinning.weights[vertexIndex * 4] / 255, w1 = skinning.weights[vertexIndex * 4 + 1] / 255, w2 = skinning.weights[vertexIndex * 4 + 2] / 255, w3 = skinning.weights[vertexIndex * 4 + 3] / 255;
1690
+ const ws = w0 + w1 + w2 + w3;
1691
+ const nw = ws > 0.0001 ? [w0 / ws, w1 / ws, w2 / ws, w3 / ws] : [1, 0, 0, 0];
1692
+ let sp = new Vec3(0, 0, 0);
1774
1693
  for (let j = 0; j < 4; j++) {
1775
- const weight = normalizedWeights[j];
1776
- if (weight > 0) {
1777
- const matrixOffset = jointIndices[j] * 16;
1778
- const transformed = transformByMatrix(skinMatrices, matrixOffset, position);
1779
- skinnedPosition = skinnedPosition.add(transformed.scale(weight));
1780
- }
1694
+ if (nw[j] <= 0)
1695
+ continue;
1696
+ const transformed = transformByMatrix(skinMatrices, [j0, j1, j2, j3][j] * 16, position);
1697
+ sp = sp.add(transformed.scale(nw[j]));
1781
1698
  }
1782
- // Store transformed position, copy other attributes unchanged
1783
- vertices[i] = skinnedPosition.x;
1784
- vertices[i + 1] = skinnedPosition.y;
1785
- vertices[i + 2] = skinnedPosition.z;
1786
- vertices[i + 3] = baseVertices[i + 3]; // normal X
1787
- vertices[i + 4] = baseVertices[i + 4]; // normal Y
1788
- vertices[i + 5] = baseVertices[i + 5]; // normal Z
1789
- vertices[i + 6] = baseVertices[i + 6]; // UV X
1790
- vertices[i + 7] = baseVertices[i + 7]; // UV Y
1699
+ vertices[i] = sp.x;
1700
+ vertices[i + 1] = sp.y;
1701
+ vertices[i + 2] = sp.z;
1702
+ vertices[i + 3] = baseVertices[i + 3];
1703
+ vertices[i + 4] = baseVertices[i + 4];
1704
+ vertices[i + 5] = baseVertices[i + 5];
1705
+ vertices[i + 6] = baseVertices[i + 6];
1706
+ vertices[i + 7] = baseVertices[i + 7];
1791
1707
  }
1792
- // Cache the result
1793
- this.cachedSkinnedVertices = vertices;
1794
- this.cachedSkinMatricesVersion = this.skinMatricesVersion;
1795
- }
1796
- let closestHit = null;
1797
- const maxDistance = 1000; // Reasonable max distance
1798
- // Test ray against all triangles (Möller-Trumbore algorithm)
1799
- for (let i = 0; i < indices.length; i += 3) {
1800
- const idx0 = indices[i] * 8; // Each vertex has 8 floats (pos + normal + uv)
1801
- const idx1 = indices[i + 1] * 8;
1802
- const idx2 = indices[i + 2] * 8;
1803
- // Get triangle vertices in world space (first 3 floats are position)
1804
- const v0 = new Vec3(vertices[idx0], vertices[idx0 + 1], vertices[idx0 + 2]);
1805
- const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2]);
1806
- const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2]);
1807
- // Find which material this triangle belongs to
1808
- // Each material has mat.vertexCount indices (3 per triangle)
1809
- let triangleMaterialIndex = -1;
1810
- let indexOffset = 0;
1811
- for (let matIdx = 0; matIdx < materials.length; matIdx++) {
1812
- const mat = materials[matIdx];
1813
- if (i >= indexOffset && i < indexOffset + mat.vertexCount) {
1814
- triangleMaterialIndex = matIdx;
1815
- break;
1708
+ for (let i = 0; i < indices.length; i += 3) {
1709
+ const idx0 = indices[i] * 8, idx1 = indices[i + 1] * 8, idx2 = indices[i + 2] * 8;
1710
+ const v0 = new Vec3(vertices[idx0], vertices[idx0 + 1], vertices[idx0 + 2]);
1711
+ const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2]);
1712
+ const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2]);
1713
+ let triangleMaterialIndex = -1;
1714
+ let indexOffset = 0;
1715
+ for (let matIdx = 0; matIdx < materials.length; matIdx++) {
1716
+ if (i >= indexOffset && i < indexOffset + materials[matIdx].vertexCount) {
1717
+ triangleMaterialIndex = matIdx;
1718
+ break;
1719
+ }
1720
+ indexOffset += materials[matIdx].vertexCount;
1816
1721
  }
1817
- indexOffset += mat.vertexCount;
1818
- }
1819
- if (triangleMaterialIndex === -1)
1820
- continue;
1821
- // Skip invisible materials
1822
- // const materialName = materials[triangleMaterialIndex].name
1823
- // if (this.hiddenMaterials.has(materialName)) continue
1824
- // Ray-triangle intersection test (Möller-Trumbore algorithm)
1825
- const edge1 = v1.subtract(v0);
1826
- const edge2 = v2.subtract(v0);
1827
- const h = rayDirection.cross(edge2);
1828
- const a = edge1.dot(h);
1829
- if (Math.abs(a) < 0.0001)
1830
- continue; // Ray is parallel to triangle
1831
- const f = 1.0 / a;
1832
- const s = rayOrigin.subtract(v0);
1833
- const u = f * s.dot(h);
1834
- if (u < 0.0 || u > 1.0)
1835
- continue;
1836
- const q = s.cross(edge1);
1837
- const v = f * rayDirection.dot(q);
1838
- if (v < 0.0 || u + v > 1.0)
1839
- continue;
1840
- // At this point we have a hit
1841
- const t = f * edge2.dot(q);
1842
- if (t > 0.0001 && t < maxDistance) {
1843
- // Backface culling: only consider front-facing triangles
1722
+ if (triangleMaterialIndex === -1)
1723
+ continue;
1724
+ const edge1 = v1.subtract(v0), edge2 = v2.subtract(v0), h = rayDirection.cross(edge2), a = edge1.dot(h);
1725
+ if (Math.abs(a) < 0.0001)
1726
+ continue;
1727
+ const f = 1 / a, s = rayOrigin.subtract(v0), u = f * s.dot(h);
1728
+ if (u < 0 || u > 1)
1729
+ continue;
1730
+ const q = s.cross(edge1), v = f * rayDirection.dot(q);
1731
+ if (v < 0 || u + v > 1)
1732
+ continue;
1733
+ const t = f * edge2.dot(q);
1734
+ if (t <= 0.0001 || t >= maxDistance)
1735
+ continue;
1844
1736
  const triangleNormal = edge1.cross(edge2).normalize();
1845
- const isFrontFace = triangleNormal.dot(rayDirection) < 0;
1846
- if (isFrontFace) {
1847
- if (!closestHit || t < closestHit.distance) {
1848
- closestHit = {
1849
- materialName: materials[triangleMaterialIndex].name,
1850
- distance: t,
1851
- };
1852
- }
1737
+ if (triangleNormal.dot(rayDirection) >= 0)
1738
+ continue;
1739
+ if (!closest || t < closest.distance) {
1740
+ closest = { modelName: inst.name, materialName: materials[triangleMaterialIndex].name, distance: t };
1853
1741
  }
1854
1742
  }
1855
- }
1856
- // Call the callback with the result
1743
+ });
1857
1744
  if (this.onRaycast) {
1858
- this.onRaycast(closestHit?.materialName || null, screenX, screenY);
1745
+ const hit = closest;
1746
+ this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY);
1859
1747
  }
1860
1748
  }
1861
- // Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent
1862
1749
  render() {
1863
- if (this.multisampleTexture && this.camera && this.device) {
1864
- const currentTime = performance.now();
1865
- const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016;
1866
- this.lastFrameTime = currentTime;
1867
- this.updateCameraUniforms();
1868
- this.updateRenderTarget();
1869
- // Update model (handles tweens, animation, physics, IK, and skin matrices)
1870
- if (this.currentModel) {
1871
- const verticesChanged = this.currentModel.update(deltaTime);
1872
- if (verticesChanged) {
1873
- this.vertexBufferNeedsUpdate = true;
1874
- }
1875
- }
1876
- // Update vertex buffer if morphs changed
1877
- if (this.vertexBufferNeedsUpdate) {
1878
- this.updateVertexBuffer();
1879
- this.vertexBufferNeedsUpdate = false;
1880
- }
1750
+ if (!this.multisampleTexture || !this.camera || !this.device)
1751
+ return;
1752
+ const currentTime = performance.now();
1753
+ const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016;
1754
+ this.lastFrameTime = currentTime;
1755
+ this.updateCameraUniforms();
1756
+ this.updateRenderTarget();
1757
+ const hasModels = this.modelInstances.size > 0;
1758
+ if (hasModels) {
1759
+ this.updateInstances(deltaTime);
1881
1760
  this.updateSkinMatrices();
1882
- if (this.groundMode === "shadow")
1883
- this.updateShadowLightVP();
1884
- const encoder = this.device.createCommandEncoder();
1885
- if (this.groundMode === "shadow" &&
1886
- this.currentModel &&
1887
- this.shadowMapDepthView &&
1888
- this.shadowBindGroup) {
1889
- const sp = encoder.beginRenderPass({
1890
- colorAttachments: [],
1891
- depthStencilAttachment: {
1892
- view: this.shadowMapDepthView,
1893
- depthClearValue: 1.0,
1894
- depthLoadOp: "clear",
1895
- depthStoreOp: "store",
1896
- },
1897
- });
1898
- sp.setPipeline(this.shadowDepthPipeline);
1899
- sp.setBindGroup(0, this.shadowBindGroup);
1900
- sp.setVertexBuffer(0, this.vertexBuffer);
1901
- sp.setVertexBuffer(1, this.jointsBuffer);
1902
- sp.setVertexBuffer(2, this.weightsBuffer);
1903
- sp.setIndexBuffer(this.indexBuffer, "uint32");
1904
- for (const draw of this.shadowDrawCalls) {
1905
- if (this.shouldRenderDrawCall(draw))
1906
- sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1761
+ }
1762
+ if (this.groundMode === "shadow")
1763
+ this.updateShadowLightVP();
1764
+ const encoder = this.device.createCommandEncoder();
1765
+ if (hasModels && this.groundMode === "shadow" && this.shadowMapDepthView) {
1766
+ const sp = encoder.beginRenderPass({
1767
+ colorAttachments: [],
1768
+ depthStencilAttachment: {
1769
+ view: this.shadowMapDepthView,
1770
+ depthClearValue: 1.0,
1771
+ depthLoadOp: "clear",
1772
+ depthStoreOp: "store",
1773
+ },
1774
+ });
1775
+ sp.setPipeline(this.shadowDepthPipeline);
1776
+ this.forEachInstance((inst) => this.drawInstanceShadow(sp, inst));
1777
+ sp.end();
1778
+ }
1779
+ const pass = encoder.beginRenderPass(this.renderPassDescriptor);
1780
+ if (hasModels)
1781
+ this.forEachInstance((inst) => this.renderOneModel(pass, inst, false));
1782
+ if (this.groundHasReflections)
1783
+ this.renderGround(pass);
1784
+ pass.end();
1785
+ this.device.queue.submit([encoder.finish()]);
1786
+ this.updateStats(performance.now() - currentTime);
1787
+ }
1788
+ drawInstanceShadow(sp, inst) {
1789
+ sp.setBindGroup(0, inst.shadowBindGroup);
1790
+ sp.setVertexBuffer(0, inst.vertexBuffer);
1791
+ sp.setVertexBuffer(1, inst.jointsBuffer);
1792
+ sp.setVertexBuffer(2, inst.weightsBuffer);
1793
+ sp.setIndexBuffer(inst.indexBuffer, "uint32");
1794
+ for (const draw of inst.shadowDrawCalls) {
1795
+ if (this.shouldRenderDrawCall(inst, draw))
1796
+ sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1797
+ }
1798
+ }
1799
+ renderOneModel(pass, inst, useReflection, mirrorMatrix) {
1800
+ pass.setVertexBuffer(0, inst.vertexBuffer);
1801
+ pass.setVertexBuffer(1, inst.jointsBuffer);
1802
+ pass.setVertexBuffer(2, inst.weightsBuffer);
1803
+ pass.setIndexBuffer(inst.indexBuffer, "uint32");
1804
+ if (useReflection && mirrorMatrix) {
1805
+ this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix);
1806
+ pass.setPipeline(this.reflectionPipeline);
1807
+ for (const draw of inst.drawCalls) {
1808
+ if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
1809
+ pass.setBindGroup(0, draw.bindGroup);
1810
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1907
1811
  }
1908
- sp.end();
1909
1812
  }
1910
- const pass = encoder.beginRenderPass(this.renderPassDescriptor);
1911
- if (this.currentModel) {
1912
- pass.setVertexBuffer(0, this.vertexBuffer);
1913
- pass.setVertexBuffer(1, this.jointsBuffer);
1914
- pass.setVertexBuffer(2, this.weightsBuffer);
1915
- pass.setIndexBuffer(this.indexBuffer, "uint32");
1916
- // Pass 1: Opaque
1917
- pass.setPipeline(this.modelPipeline);
1918
- for (const draw of this.drawCalls) {
1919
- if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
1920
- pass.setBindGroup(0, draw.bindGroup);
1921
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1922
- }
1923
- }
1924
- // Pass 2: Eyes (writes stencil value for hair to test against)
1925
- this.renderEyes(pass);
1926
- this.drawOutlines(pass, false);
1927
- // Pass 3: Hair rendering (depth pre-pass + shading + outlines)
1928
- this.renderHair(pass);
1929
- // Pass 5: Transparent
1930
- pass.setPipeline(this.modelPipeline);
1931
- for (const draw of this.drawCalls) {
1932
- if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
1933
- pass.setBindGroup(0, draw.bindGroup);
1934
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1935
- }
1813
+ this.renderEyes(pass, inst, true);
1814
+ this.renderHair(pass, inst, true);
1815
+ for (const draw of inst.drawCalls) {
1816
+ if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
1817
+ pass.setBindGroup(0, draw.bindGroup);
1818
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1936
1819
  }
1937
- this.drawOutlines(pass, true);
1938
1820
  }
1939
- if (this.groundHasReflections) {
1940
- this.renderGround(pass);
1821
+ this.drawOutlines(pass, inst, true, true);
1822
+ return;
1823
+ }
1824
+ pass.setPipeline(this.modelPipeline);
1825
+ for (const draw of inst.drawCalls) {
1826
+ if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
1827
+ pass.setBindGroup(0, draw.bindGroup);
1828
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1829
+ }
1830
+ }
1831
+ this.renderEyes(pass, inst, false);
1832
+ this.drawOutlines(pass, inst, false);
1833
+ this.renderHair(pass, inst, false);
1834
+ pass.setPipeline(this.modelPipeline);
1835
+ for (const draw of inst.drawCalls) {
1836
+ if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
1837
+ pass.setBindGroup(0, draw.bindGroup);
1838
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1941
1839
  }
1942
- pass.end();
1943
- this.device.queue.submit([encoder.finish()]);
1944
- this.updateStats(performance.now() - currentTime);
1945
1840
  }
1841
+ this.drawOutlines(pass, inst, true);
1946
1842
  }
1947
1843
  updateCameraUniforms() {
1948
1844
  const viewMatrix = this.camera.getViewMatrix();
@@ -1966,22 +1862,18 @@ export class Engine {
1966
1862
  }
1967
1863
  }
1968
1864
  updateSkinMatrices() {
1969
- if (!this.currentModel || !this.skinMatrixBuffer)
1970
- return;
1971
- const skinMatrices = this.currentModel.getSkinMatrices();
1972
- this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, skinMatrices.buffer, skinMatrices.byteOffset, skinMatrices.byteLength);
1973
- // Increment version to invalidate cached skinned vertices
1974
- this.skinMatricesVersion++;
1865
+ this.forEachInstance((inst) => {
1866
+ const skinMatrices = inst.model.getSkinMatrices();
1867
+ this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, skinMatrices.buffer, skinMatrices.byteOffset, skinMatrices.byteLength);
1868
+ });
1975
1869
  }
1976
- drawOutlines(pass, transparent, useReflectionPipeline = false) {
1977
- if (useReflectionPipeline) {
1978
- // Skip outlines for reflections - not critical for the effect
1870
+ drawOutlines(pass, inst, transparent, useReflectionPipeline = false) {
1871
+ if (useReflectionPipeline)
1979
1872
  return;
1980
- }
1981
1873
  pass.setPipeline(this.outlinePipeline);
1982
1874
  const outlineType = transparent ? "transparent-outline" : "opaque-outline";
1983
- for (const draw of this.drawCalls) {
1984
- if (draw.type === outlineType && this.shouldRenderDrawCall(draw)) {
1875
+ for (const draw of inst.drawCalls) {
1876
+ if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
1985
1877
  pass.setBindGroup(0, draw.bindGroup);
1986
1878
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1987
1879
  }
@@ -2031,23 +1923,19 @@ export class Engine {
2031
1923
  1,
2032
1924
  ]));
2033
1925
  }
2034
- writeMirrorTransformedSkinMatrices(mirrorMatrix) {
2035
- if (!this.currentModel || !this.skinMatrixBuffer)
2036
- return;
2037
- const originalMatrices = this.currentModel.getSkinMatrices();
1926
+ writeMirrorTransformedSkinMatrices(inst, mirrorMatrix) {
1927
+ const originalMatrices = inst.model.getSkinMatrices();
2038
1928
  const transformedMatrices = new Float32Array(originalMatrices.length);
2039
1929
  for (let i = 0; i < originalMatrices.length; i += 16) {
2040
1930
  const boneMatrixValues = new Float32Array(16);
2041
- for (let j = 0; j < 16; j++) {
1931
+ for (let j = 0; j < 16; j++)
2042
1932
  boneMatrixValues[j] = originalMatrices[i + j];
2043
- }
2044
1933
  const boneMatrix = new Mat4(boneMatrixValues);
2045
1934
  const transformed = mirrorMatrix.multiply(boneMatrix);
2046
- for (let j = 0; j < 16; j++) {
1935
+ for (let j = 0; j < 16; j++)
2047
1936
  transformedMatrices[i + j] = transformed.values[j];
2048
- }
2049
1937
  }
2050
- this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, transformedMatrices);
1938
+ this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices);
2051
1939
  }
2052
1940
  }
2053
1941
  Engine.instance = null;