@stowkit/three-loader 0.1.35 → 0.1.39

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.
@@ -610,11 +610,19 @@ class StowKitPack {
610
610
  materialData.push({ name, schemaId, propertyCount, properties: [] });
611
611
  metadataOffset += 196;
612
612
  }
613
- // Skip Node array and node_mesh_indices (not needed for rendering)
614
- // Node struct: transform[16 floats] + name[64 bytes] + mesh_index[4 bytes] + parent_index[4 bytes] = 116 bytes
613
+ // Parse Node array to calculate total mesh indices
614
+ // Node struct: name[64] + parent_index[4] + position[12] + rotation[16] + scale[12] + mesh_count[4] + first_mesh_index[4] = 116 bytes
615
615
  const NODE_STRIDE = 116;
616
+ let totalMeshIndices = 0;
617
+ for (let n = 0; n < nodeCount; n++) {
618
+ const nodeOffset = metadataOffset + n * NODE_STRIDE;
619
+ // mesh_count is at offset 64 + 4 + 12 + 16 + 12 = 108 within Node struct
620
+ const meshCount = metaView.getUint32(nodeOffset + 108, true);
621
+ totalMeshIndices += meshCount;
622
+ }
616
623
  metadataOffset += nodeCount * NODE_STRIDE;
617
- metadataOffset += nodeCount * 4;
624
+ // Skip node_mesh_indices array (totalMeshIndices entries, not nodeCount!)
625
+ metadataOffset += totalMeshIndices * 4;
618
626
  // Parse material properties
619
627
  const PROPERTY_STRIDE = 144;
620
628
  for (let i = 0; i < materialCount; i++) {
@@ -692,7 +700,8 @@ class StowKitPack {
692
700
  const bone = bones[i];
693
701
  const bindPose = bindPoses[i];
694
702
  const parentIndex = parentIndices[i];
695
- if (parentIndex < 0) {
703
+ if (parentIndex < 0 || parentIndex >= bindPoses.length) {
704
+ // Root bone or invalid parent - use bind pose directly
696
705
  const pos = new THREE__namespace.Vector3();
697
706
  const quat = new THREE__namespace.Quaternion();
698
707
  const scale = new THREE__namespace.Vector3();
@@ -722,7 +731,9 @@ class StowKitPack {
722
731
  const material = new THREE__namespace.MeshStandardMaterial({
723
732
  side: THREE__namespace.DoubleSide,
724
733
  name: mat.name || `Material_${i}`,
725
- color: new THREE__namespace.Color(0.8, 0.8, 0.8) // Default gray
734
+ color: new THREE__namespace.Color(0.8, 0.8, 0.8), // Default gray
735
+ metalness: 0.5,
736
+ roughness: 0.5
726
737
  });
727
738
  // Apply non-texture properties
728
739
  for (const prop of mat.properties) {
@@ -789,6 +800,8 @@ class StowKitPack {
789
800
  skinnedMesh.bind(skeleton, skinnedMesh.matrixWorld);
790
801
  skinnedMesh.normalizeSkinWeights();
791
802
  skinnedMesh.updateMatrixWorld(true);
803
+ // Ensure frustum culling doesn't hide the mesh if bounds are wrong
804
+ skinnedMesh.frustumCulled = false;
792
805
  const container = new THREE__namespace.Group();
793
806
  container.name = stringId || 'SkinnedMesh';
794
807
  container.add(boneContainer);
@@ -995,22 +1008,99 @@ class StowKitPack {
995
1008
  */
996
1009
  async loadAnimation(skinnedMeshGroup, animationPath) {
997
1010
  const clip = await this.loadAnimationClip(animationPath);
1011
+ const retargetedClip = this.retargetAnimationClip(clip, skinnedMeshGroup);
998
1012
  const mixer = new THREE__namespace.AnimationMixer(skinnedMeshGroup);
999
- const action = mixer.clipAction(clip);
1013
+ const action = mixer.clipAction(retargetedClip);
1000
1014
  action.setLoop(THREE__namespace.LoopRepeat, Infinity);
1001
1015
  action.play();
1002
- return { mixer, action, clip };
1016
+ return { mixer, action, clip: retargetedClip };
1003
1017
  }
1004
1018
  /**
1005
1019
  * Load and play animation by index
1006
1020
  */
1007
1021
  async loadAnimationByIndex(skinnedMeshGroup, animationIndex) {
1008
1022
  const clip = await this.loadAnimationClipByIndex(animationIndex);
1023
+ const retargetedClip = this.retargetAnimationClip(clip, skinnedMeshGroup);
1009
1024
  const mixer = new THREE__namespace.AnimationMixer(skinnedMeshGroup);
1010
- const action = mixer.clipAction(clip);
1025
+ const action = mixer.clipAction(retargetedClip);
1011
1026
  action.setLoop(THREE__namespace.LoopRepeat, Infinity);
1012
1027
  action.play();
1013
- return { mixer, action, clip };
1028
+ return { mixer, action, clip: retargetedClip };
1029
+ }
1030
+ /**
1031
+ * Retarget animation clip to match skeleton bone names
1032
+ */
1033
+ retargetAnimationClip(clip, skinnedMeshGroup) {
1034
+ // Collect all bone names from the skeleton
1035
+ const skeletonBoneNames = new Set();
1036
+ skinnedMeshGroup.traverse((child) => {
1037
+ if (child.isBone) {
1038
+ skeletonBoneNames.add(child.name);
1039
+ }
1040
+ });
1041
+ // Build a mapping from animation bone names to skeleton bone names
1042
+ // Try to match by suffix (e.g., "CharacterRig_MainHipJ" -> "MainHipJ")
1043
+ const boneNameMap = new Map();
1044
+ for (const track of clip.tracks) {
1045
+ const trackParts = track.name.split('.');
1046
+ const animBoneName = trackParts[0];
1047
+ if (skeletonBoneNames.has(animBoneName)) {
1048
+ // Exact match - no remapping needed
1049
+ continue;
1050
+ }
1051
+ // Try to find a match by suffix
1052
+ for (const skelBoneName of skeletonBoneNames) {
1053
+ // Check if animation bone name ends with skeleton bone name
1054
+ // e.g., "CharacterRig_MainHipJ" contains "MainHipJ"
1055
+ if (animBoneName.endsWith(skelBoneName) ||
1056
+ animBoneName.endsWith('_' + skelBoneName) ||
1057
+ skelBoneName.endsWith(animBoneName) ||
1058
+ skelBoneName.endsWith('_' + animBoneName)) {
1059
+ boneNameMap.set(animBoneName, skelBoneName);
1060
+ break;
1061
+ }
1062
+ // Also try matching without common prefixes
1063
+ const animSuffix = animBoneName.replace(/^.*_/, '');
1064
+ const skelSuffix = skelBoneName.replace(/^.*_/, '');
1065
+ if (animSuffix === skelSuffix && animSuffix.length > 2) {
1066
+ boneNameMap.set(animBoneName, skelBoneName);
1067
+ break;
1068
+ }
1069
+ }
1070
+ }
1071
+ // If no remapping needed, return original clip
1072
+ if (boneNameMap.size === 0) {
1073
+ return clip;
1074
+ }
1075
+ console.log(`[StowKit] Retargeting animation: ${boneNameMap.size} bones remapped`);
1076
+ // Create new tracks with remapped names
1077
+ const newTracks = [];
1078
+ for (const track of clip.tracks) {
1079
+ const trackParts = track.name.split('.');
1080
+ const animBoneName = trackParts[0];
1081
+ const property = trackParts.slice(1).join('.');
1082
+ const mappedBoneName = boneNameMap.get(animBoneName) || animBoneName;
1083
+ const newTrackName = `${mappedBoneName}.${property}`;
1084
+ // Clone the track with the new name
1085
+ // Use generic KeyframeTrack constructor to avoid type issues with specific track types
1086
+ const times = track.times;
1087
+ const values = track.values;
1088
+ let newTrack;
1089
+ if (track instanceof THREE__namespace.VectorKeyframeTrack) {
1090
+ newTrack = new THREE__namespace.VectorKeyframeTrack(newTrackName, times, values);
1091
+ }
1092
+ else if (track instanceof THREE__namespace.QuaternionKeyframeTrack) {
1093
+ newTrack = new THREE__namespace.QuaternionKeyframeTrack(newTrackName, times, values);
1094
+ }
1095
+ else if (track instanceof THREE__namespace.NumberKeyframeTrack) {
1096
+ newTrack = new THREE__namespace.NumberKeyframeTrack(newTrackName, times, values);
1097
+ }
1098
+ else {
1099
+ newTrack = new THREE__namespace.KeyframeTrack(newTrackName, times, values);
1100
+ }
1101
+ newTracks.push(newTrack);
1102
+ }
1103
+ return new THREE__namespace.AnimationClip(clip.name, clip.duration, newTracks);
1014
1104
  }
1015
1105
  /**
1016
1106
  * Load animation clip by index
@@ -1192,6 +1282,8 @@ class StowKitPack {
1192
1282
  URL.revokeObjectURL(url);
1193
1283
  reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName}": 0ms (IndexedDB cache)`);
1194
1284
  const texture = new THREE__namespace.CompressedTexture([{ data: cached.data, width: cached.width, height: cached.height }], cached.width, cached.height, cached.format, THREE__namespace.UnsignedByteType);
1285
+ // All textures default to sRGB. Data textures are set to linear in applyTextureToMaterial.
1286
+ texture.colorSpace = THREE__namespace.SRGBColorSpace;
1195
1287
  texture.needsUpdate = true;
1196
1288
  return texture;
1197
1289
  }
@@ -1199,6 +1291,9 @@ class StowKitPack {
1199
1291
  const texture = await new Promise((resolve, reject) => {
1200
1292
  this.ktx2Loader.load(url, (texture) => {
1201
1293
  URL.revokeObjectURL(url);
1294
+ // All textures default to sRGB. Data textures (normal, metallic, roughness)
1295
+ // are set back to linear in applyTextureToMaterial.
1296
+ texture.colorSpace = THREE__namespace.SRGBColorSpace;
1202
1297
  texture.needsUpdate = true;
1203
1298
  resolve(texture);
1204
1299
  }, undefined, (error) => {
@@ -1232,18 +1327,26 @@ class StowKitPack {
1232
1327
  const propNameLower = propertyName.toLowerCase();
1233
1328
  if (propNameLower === 'maintex' || propNameLower.includes('diffuse') ||
1234
1329
  propNameLower.includes('albedo') || propNameLower.includes('base')) {
1330
+ // Color texture - keep sRGB (default)
1235
1331
  material.map = texture;
1236
1332
  }
1237
1333
  else if (propNameLower.includes('normal')) {
1334
+ // Data texture - use linear
1335
+ texture.colorSpace = THREE__namespace.LinearSRGBColorSpace;
1238
1336
  material.normalMap = texture;
1239
1337
  }
1240
1338
  else if (propNameLower.includes('metallic')) {
1339
+ // Data texture - use linear
1340
+ texture.colorSpace = THREE__namespace.LinearSRGBColorSpace;
1241
1341
  material.metalnessMap = texture;
1242
1342
  }
1243
1343
  else if (propNameLower.includes('roughness')) {
1344
+ // Data texture - use linear
1345
+ texture.colorSpace = THREE__namespace.LinearSRGBColorSpace;
1244
1346
  material.roughnessMap = texture;
1245
1347
  }
1246
1348
  else {
1349
+ // Default to color texture - keep sRGB
1247
1350
  material.map = texture;
1248
1351
  }
1249
1352
  material.needsUpdate = true;