fix: per-species scale correction for skinned mesh fish
Box3.setFromObject measures skinned geometry in skeleton space (and may use a stale cached bounding box), which does not match the GPU's actual rendered extent (mesh.matrixWorld * skinning * position). Add a per-species baseScale correction factor (rendered / box3) to FishSpec and fold it into buildFish's scale calc so each GLB normalizes to its true size. Measured per GLB with three.js's exact skinning formula: most models are 1.0. The skinned blendswap oscar (1.35) and knifefish (2.58) deviate and are corrected. 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 pending a clean re-export.
This commit is contained in:
+26
@@ -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<FishSpecies, number> = {
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user