From 8c4a7d45a594c22ed4b30a63e2f19b282682d1c3 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Wed, 7 Oct 2026 14:49:55 +0000 Subject: [PATCH] fix: normalize GLB model scales to prevent oversized fish The three Poly Pizza models (betta, blue_tang, flower_horn) rendered giant (filling the tank) because each GLB had two 100x-scaled nodes (Fish_Armature bone-parent + the skinned mesh node). three.js binds skinned meshes with an identity bindMatrix and uses the GLB's inverseBindMatrices verbatim, so the 100x node scales inflated the rendered skinned extent ~100x relative to what Box3.setFromObject reports. GLTFish's 'scale = targetLength / longest' therefore scaled ~100x too big. Fix: pure GLB JSON-chunk edit (BIN chunk + all 6 animations preserved) setting the bone-parent node scale to S=1/base and the mesh node scale to 1, so Box3 == rendered extent == 1.0 and targetLength directly controls fish size, matching the working models. --- public/models/poly-pizza/betta.glb | Bin 156632 -> 153992 bytes public/models/poly-pizza/blue_tang.glb | Bin 148440 -> 145624 bytes public/models/poly-pizza/flower_horn.glb | Bin 162276 -> 159464 bytes scripts/fix-model-scales.mjs | 160 +++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 scripts/fix-model-scales.mjs diff --git a/public/models/poly-pizza/betta.glb b/public/models/poly-pizza/betta.glb index fc8aa7802f84a082e358a976220addab753e4367..bf0136e35ed8df86f1401b6771978617c8311cfb 100644 GIT binary patch delta 16198 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

!^TdV&tEtO6rseo}mba?9e|G846li zW-wNZ;HXO+xW!RlWP&)2z*I=ot`tgW8A_U{hR{5UqnP9bI9reN9sYqu$@9nfhrKwn}-=%afYE+AuXmb6>L;_Fb8b#43uD!GPLA0VS^!mGnqzH z2+dM(9*7N0T1C8SbsE+4?^g9yCsb#6HLtPdSKL295X96K#W#a(yTBM5sI*3Au671r zOdIE40Xl?^(53>vQFj}XE9tG?rWp z7d2aqD#jbGxuQxm8lB)&z=Io}3T>pycqZ*Yc!YvEU%~TUJcRu&?N{LuGA#J@b{(!f z{p@1c zj}@sjf&}&9;18;(h=_ggz!;XIEodz_#>W#l3khK1rhztQh|BObkh(mrKBJPRZ7s68 zg>`3_mo8rGp$v-0aGr?J2&{~vDZ0pM zrjg5Wc+ixSc2FG2v_?}^SZgvn8m@|?m7_a(ovOI`75Xj1;n1alwE~j!Czn6jd!r2y z%vefG=^xsSs0TylUe5+9!T}URFmX3WW9WO-$m=}~L^1^|YwSC&2>9$fV|15}AuMRC zGBO!~?-|2&BFf{8{$4TREp2(?+ix{XfkMs~)3fPn`b9Zl?PZFSiFYIjHu-9`m_E5! z(V*wahljWCJ$&$(CHpCY?Y+Z~?;hMq+I$6<`U4I8RpG~0@T;U~(4Ki*`mnV-87Wn33 zG5N>AYLy>RTsb<)XR}EedzM9H@(@|KpTWRyAcQ&3>9TKvm@`|R`WX`E-SmbIQnu4A z=Bt9JN(06*4YXU%)j~rFg1^9nQrCKcKyx<{2E-F#&%0?bAf|yaY-wDRM3fzr*NfJR z1MId$YZ_Q^skvgOrnB>lqH=)ko-EGirIPoPJGbxM^xg9L#mbN21bvKmeF^+Eh5-}! z8z{TSyJWp0Zy>t~Hse((L0 z?Wbjuh$6H&*Ndch@nXm%A~d>=Ij)a3&R-x=37j)~_qw~4FIO*T04+d{&`;p+0J5Va zxi^nEbT=TmYGw`S-wEUz_yNwHNNzy?5Fk^tzZJ;1Ggf34#Y-;b{&$jWD8f+Qk0P{| zf&zC|IujldT1&r4LTlMc@k@#rf>Q5o&cNRN?T^ZSOxL52x$NK(Y0CW_Eb`cdd#M1Z`0d3xiuA3^y;f)XyZ2Pi}5Lieplxf9Q`YbE0*h()7=>STcUDtQNHzfwT8Get71vvi(H3D4K z9^n@hn@C+g^7XLgQGSZp{D_b6i$O;x)6=Jm30?N)qoNH}mVwuqGvr4Nqt7g8f}m$g zFW{gyn^|R0l%wDBsbT1=QQ4MG;^i^~A4{vXMW>W!n&Z`qQT)g?JhQU8^rH1%E}Tk~ zytdvw+*(0dkZObI&!Aa5*+XOB_ia!DM;{K$#apU))dC|e)+q&E8sth}{`!NTx~e*f zihj^A6v|Mi7dHv4ExLUT&oP{X8(PUy;FT6e#Y&4Gfx0DHMLe@M&`HDAyi;7U2?P{1 z0Wbge!LJ<6g!(^}?@Of|uW0O<*o*btd_b||$CpD({N<>iK?hW3Xu~08m4A2F81ARjsX!a}A2_dCK z4cSPGiyi3l5@u;{(66PpbRd>Q@vp?IE9|D&JPb;igc_8>fWVH5YsG6UP3H|DZH{NGu%-T02WHMdv+{wM@1+s!)BMi`RHfwG;AHUoMChdZ}n(p zpsRZyy$L9A#!wkYqZ-tVI*eE`gw#?AydlZRhSqji6RFSwWJKHASXg!JEsFMJAf>8G znNtCE%%~YoEm46NjyS|5cK|UV4Q|Z<_#D#OksSM8y4MEw%#uE;auOBZp~Ss22#a?L z(VxG}?~s(t(7WQaS=d+bB2l!(p5s0NuKLJ@5lq+n?R^YprypE)f~g2{K>>=hNYa^p zJefU>g?;N0giwlejs;n?vo-CV9bF#WgSBF%nK1^&Y7KUbU{13Ho`;Wd$HoZ!$1NM3 z5kg?9=@>hV*M1R1APy7$sTnK0{HM{dgr`A!aI%Nj@CYRpnSxcjWZlV(SKSmM;Wvf` kueGB-n=Dc4LwZe31bjfiemni{m4AMH_}{M&U$Ni)AJx$HqW}N^ delta 20223 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 delta 20408 zcmeHP&2AjW5oUA4OeRB84*4EY+uWoGp`EYCNgU5#lzun%QESLFe_uU=*^JPBE7n9X| zfsea)Z~t15$j&EhFWXCY-`U>%BwsA2^BMhC?6KYL&v9*e1R-2u`1kXR<73={Pfxd@ z?)j@#em|eRT%F(w*AY*Z*#3O`qw3BNr}?M#-A1_7E4Wh$HTWI4(Pnz}#nI|-4tvI$|dX2_|v`U?UxvY?88em<;$bAef2~=n=x@h&EXcPm|guogPUu zpCh<790@{#S!yD>?oU_(l7b~8KoAn-apc)md%-6pDbxsGh6I6ue)DL`0tGPEW{HTT zBy+DOi5@#wG}n->9#w+**pX7P2>Hjl zx*|V|-H|G08%#?k+PA#SkTkTXHEemV0;Z3oN<3X|gx5JeR+Qeap9@0DRGM`9vB&nD z1&p7_)c8@a?Z|-jQ<<43gf(?>0F(UR+yYs|O*Yn0Hf(-ufNor~VU5uq)pn(8{#*ym zA5;cAn)ISEv4*msF3J3-C(dcVrip2n-owgb&f8CLNbVElM!l;HP7Lcw-_dRJ3yI{V zt!#w(F$qXDT`3~WtgWlL$YB15EGVEX&7yu^23IMP04}7&vx1qJcA{%@W+7TCll0kipw+}^^rKPu!VM@RW` zIj>I~j^=0Q^I1MyJ$ZFbI`9x~7Ehn>{Js15&aL~m9??<8(fndYrye#LHi-b8MmVOZ zQh9R4*_2K_o^G=}j82~?pB0~Dp=drP(eE4nUA?pW;r<=jKRAc(2C~XMwO6 QAAV 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 parent of the skeleton bones +// - (scale=100) -> the node that owns the mesh + skin +// three.js binds skinned meshes with an identity bindMatrix and uses the GLB's +// inverseBindMatrices verbatim (Skeleton.init() only recomputes them if absent). +// Those inverses were authored for a UNIT-scale bind, but the 100x node scales +// inflate every bone's matrixWorld by 100x, so the shader product +// (boneMatrixWorld * inverseBindMatrix) inflates skinned vertices ~100x relative +// to the rest geometry. Box3.setFromObject (raw geometry) does NOT include that, +// so GLTFish's `scale = targetLength / Box3` ends up rendering ~100x too big. +// +// FIX (pure JSON-chunk edit; BIN chunk + all 6 animations preserved untouched) +// - Fish_Armature scale 100 -> S (bone parent: scales BOTH Box3 and rendered) +// - scale 100 -> 1 (mesh node: only affects rendered; keep 1 so Box3==rendered) +// +// Measured relationship (two 100x nodes: A=Fish_Armature, B=): +// M (Box3.setFromObject) = base * A +// R (true rendered) = base * A * B +// So setting B=1 and A=S=1/base gives M = R = 1.0, i.e. R/M = 1 and longest dim = 1. +// After this, `targetLength` directly controls fish size, matching the working models. +// +// Re-run-safe note: run once. Backups of the originals are in /tmp/opencode/backup-poly-pizza. +import fs from "node:fs"; +import * as THREE from "three"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; + +const MODELS = { + betta: "public/models/poly-pizza/betta.glb", + blue_tang: "public/models/poly-pizza/blue_tang.glb", + flower_horn: "public/models/poly-pizza/flower_horn.glb", +}; +const root = new URL("..", import.meta.url).pathname; + +// ---------------- GLB binary helpers ---------------- +function readGLB(path) { + const bytes = new Uint8Array(fs.readFileSync(path)); + const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + if (dv.getUint32(0, true) !== 0x46546c67) throw new Error("not a GLB (bad magic): " + path); + const jsonLen = dv.getUint32(12, true); + if (String.fromCharCode(...bytes.slice(16, 20)) !== "JSON") throw new Error("bad JSON chunk: " + path); + const jsonBytes = bytes.slice(20, 20 + jsonLen); + const binStart = 20 + jsonLen; + const binLen = dv.getUint32(binStart, true); + if (String.fromCharCode(...bytes.slice(binStart + 4, binStart + 8)) !== "BIN\0") throw new Error("bad BIN chunk: " + path); + const binBytes = bytes.slice(binStart + 8, binStart + 8 + binLen); + return { json: JSON.parse(new TextDecoder().decode(jsonBytes)), binBytes, binLen }; +} + +function buildGLB(jsonObj, binBytes, binLen) { + const jsonRaw = new TextEncoder().encode(JSON.stringify(jsonObj)); + const paddedJsonLen = (jsonRaw.length + 3) & ~3; + const jsonPadded = new Uint8Array(paddedJsonLen).fill(0x20); + jsonPadded.set(jsonRaw, 0); + const total = 12 + 8 + paddedJsonLen + 8 + binLen; + const out = new Uint8Array(total); + const dv = new DataView(out.buffer); + dv.setUint32(0, 0x46546c67, true); // "glTF" + dv.setUint32(4, 2, true); // version 2 + dv.setUint32(8, total, true); // total length + dv.setUint32(12, paddedJsonLen, true); + out.set(new TextEncoder().encode("JSON"), 16); + out.set(jsonPadded, 20); + const binStart = 20 + paddedJsonLen; + dv.setUint32(binStart, binLen, true); + out.set(new TextEncoder().encode("BIN"), binStart + 4); // 4th byte stays 0x00 -> "BIN\0" + out.set(binBytes, binStart + 8); + return out; +} + +// ---------------- measurement (exact three.js skinning formula) ---------------- +function toArrayBuffer(buf) { return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); } +function loadGLB(file) { return new Promise((res, rej) => { new GLTFLoader().parse(toArrayBuffer(fs.readFileSync(file)), "", res, (e) => rej(e)); }); } +function pickClip(clips) { if (!clips.length) return null; return clips.find((x) => /swim/i.test(x.name)) || clips[0]; } +function renderedLongest(model, mixer, frames) { + const skinned = []; model.traverse((o) => { if (o.isSkinnedMesh) skinned.push(o); }); + const box = new THREE.Box3(); const tmp = new THREE.Vector3(); let maxL = 0; + for (let f = 0; f <= frames; f++) { + if (mixer) mixer.update(1 / 30); + model.updateMatrixWorld(true); box.makeEmpty(); + for (const sm of skinned) { + const pos = sm.geometry.attributes.position, joints = sm.geometry.attributes.skinIndex, weights = sm.geometry.attributes.skinWeight; + const bones = sm.skeleton.bones, inverses = sm.skeleton.boneInverses; + if (!bones || !inverses || !pos) continue; + const bm = bones.map((b) => new THREE.Matrix4().multiplyMatrices(b.matrixWorld, inverses[bones.indexOf(b)])); + for (let i = 0; i < pos.count; i++) { + let sx = 0, sy = 0, sz = 0; + for (let k = 0; k < 4; k++) { + const w = weights.array[4 * i + k]; if (!w) continue; + const j = joints.array[4 * i + k]; if (j < 0 || j >= bm.length) continue; + tmp.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(bm[j]); + sx += tmp.x * w; sy += tmp.y * w; sz += tmp.z * w; + } + tmp.set(sx, sy, sz); + if (sm.bindMatrix) tmp.applyMatrix4(sm.bindMatrix); + if (sm.matrixWorld) tmp.applyMatrix4(sm.matrixWorld); + box.expandByPoint(tmp); + } + } + const s = box.getSize(new THREE.Vector3()); + maxL = Math.max(maxL, Math.max(s.x, s.y, s.z)); + } + return maxL; +} + +// ---------------- per-model fix ---------------- +async function fixOne(name, rel) { + const path = root + rel; + + // 1) Compute S: load original, set all 100x nodes to 1, measure the natural rest longest dim. + const gltf = await loadGLB(path); + const model = gltf.scene ?? gltf.scenes[0]; + model.traverse((o) => { if (o.scale && o.scale.x === 100 && o.scale.y === 100 && o.scale.z === 100) o.scale.set(1, 1, 1); }); + model.updateMatrixWorld(true); + const l1 = new THREE.Box3().setFromObject(model).getSize(new THREE.Vector3()); + const l1l = Math.max(l1.x, l1.y, l1.z); + const S = 1 / l1l; + + // 2) Edit the raw GLB JSON chunk. + const { json, binBytes, binLen } = readGLB(path); + let meshNode = null, armatureNode = null; + for (const n of json.nodes || []) { + if (n.scale && n.scale[0] === 100 && n.scale[1] === 100 && n.scale[2] === 100) { + if (n.mesh !== undefined) { n.scale = [1, 1, 1]; meshNode = n; } // mesh node -> 1 + else { n.scale = [S, S, S]; armatureNode = n; } // bone parent -> S + } + } + if (!meshNode || !armatureNode) throw new Error(`expected one mesh-node + one armature-node at scale 100; got mesh=${!!meshNode} armature=${!!armatureNode}`); + + // 3) Write the corrected GLB. + fs.writeFileSync(path, buildGLB(json, binBytes, binLen)); + + // 4) Verify: reload and confirm Box3 ~ 1, rendered ~ 1 (R/M ~ 1), animations intact. + const g2 = await loadGLB(path); + const m2 = g2.scene ?? g2.scenes[0]; + const M2 = new THREE.Box3().setFromObject(m2).getSize(new THREE.Vector3()); + const M2l = Math.max(M2.x, M2.y, M2.z); + const clip2 = pickClip(g2.animations); + const mixer2 = new THREE.AnimationMixer(m2); + let R2 = 0; if (clip2) { mixer2.clipAction(clip2).play(); R2 = renderedLongest(m2, mixer2, 30); } + const clips = (g2.animations || []).length; + const ok = M2l > 0.95 && M2l < 1.05 && R2 / M2l > 0.95 && R2 / M2l < 1.1 && clips > 0; + console.log( + `${name}: rest-longest@1x=${l1l.toFixed(5)} -> S=${S.toFixed(4)} | ` + + `${armatureNode.name}(bones)=${S.toFixed(3)}, ${meshNode.name}(mesh)=1 | ` + + `Box3=${M2l.toFixed(4)} R(swim)=${R2.toFixed(4)} R/M=${(R2 / M2l).toFixed(3)} clips=${clips} ${ok ? "OK" : "** CHECK **"} | ` + + `targetLength 0.5 renders ${((0.5 / M2l) * R2).toFixed(3)}` + ); + return ok; +} + +const results = []; +for (const [name, rel] of Object.entries(MODELS)) { + try { results.push(await fixOne(name, rel)); } + catch (e) { console.log(`${name}: ERROR ${e.message}`); results.push(false); } +} +console.log(results.every(Boolean) ? "\nALL 3 FIXED & VERIFIED" : "\nSOME FAILED"); +process.exit(results.every(Boolean) ? 0 : 1);