diff --git a/src/main.ts b/src/main.ts index 831f58e..d25fc72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -60,6 +60,31 @@ function getSpeciesUrl(sp: FishSpecies): string { }; return urls[sp]; } +// Per-species correction factor between Box3.setFromObject's measured extent +// and the model's true rendered extent (see FishSpec.baseScale). Box3 measures +// skinned geometry in skeleton space (and may use a stale cached bounding box), +// which does not match the GPU's actual output (mesh.matrixWorld * skinning * +// position). baseScale = rendered / box3, so `targetLength / (box3 * baseScale)` +// normalizes each fish to its real rendered size. +// +// Measured with three.js's exact skinning formula per GLB. Most models are 1.0. +// The skinned blendswap oscar/knifefish deviate and are corrected here. The +// clownfish GLB is a full *scene* (water "Surface" 45.3, "TopLight" 15, "Bubbles" +// 3.4) plus a small skinned body (~8.3); box3 is dominated by the water plane, so +// a baseScale can't fix it without blowing up the scene geometry — left at 1.0 +// until the GLB is re-exported as a clean fish. +const SPECIES_SCALE: Record = { + angelfish: 1.0, + goldfish: 1.0, + guppy: 1.0, + betta: 1.0, + blue_tang: 1.0, + flower_horn: 1.0, + neon_tetra: 1.0, + oscar: 1.35, + knifefish: 2.58, + clownfish: 1.0, +}; function fishToSpec(f: FishData): FishSpec { return { url: getSpeciesUrl(f.species), @@ -67,6 +92,7 @@ function fishToSpec(f: FishData): FishSpec { speed: [0.3 * f.genetics.speedMult, 0.7 * f.genetics.speedMult], orient: isQuaternius(f.species) ? { y: Math.PI / 2 } : undefined, genetics: f.genetics, + baseScale: SPECIES_SCALE[f.species], }; } diff --git a/src/scene/GLTFish.ts b/src/scene/GLTFish.ts index 2b4481d..2641bad 100644 --- a/src/scene/GLTFish.ts +++ b/src/scene/GLTFish.ts @@ -20,6 +20,16 @@ export interface FishSpec { orient?: { x?: number; y?: number; z?: number }; speed?: Range; genetics?: Genetics; + /** + * Correction factor between the extent reported by `Box3.setFromObject` + * and the fish's true rendered extent. `Box3.setFromObject` measures + * skinned geometry in skeleton space (and may use a stale cached + * bounding box), which does not match what the GPU actually renders + * (`mesh.matrixWorld * skinning * position`). baseScale = rendered / box3, + * so the final scale becomes `targetLength / (box3 * baseScale) = + * targetLength / rendered`, normalizing every fish to its real size. + */ + baseScale?: number; } // ---- Helpers ---- @@ -132,7 +142,8 @@ function buildFish(gltf: GLTF, spec: FishSpec): Fish { const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const longest = Math.max(size.x, size.y, size.z); - const scale = (spec.targetLength / longest) * rand(0.92, 1.08); + const baseScale = spec.baseScale ?? 1.0; + const scale = (spec.targetLength / (longest * baseScale)) * rand(0.92, 1.08); orient.scale.setScalar(scale); orient.add(model);