diff --git a/README.md b/README.md
index 2832340..5846aff 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,8 @@ town-kit/ Town & Market Kit: 27 pieces — street tiles (
tools/town-kit/ its reproducible build + e2e
arcane-kit/ Arcane Study Kit: 7 wizard's-study hero props with LODs (crystal ball,
alchemist's table, lectern, telescope, armillary, potions, rune rug; CC0, Meshy.ai)
+aquarium-kit/ Aquarium Kit: 3 realistic reef fish (centre pivot, nose +Z, thin-film iridescence), a reef rock
+ arch, staghorn coral and 2 aquatic plants (CC0, Meshy.ai; kit.md, build in tools/aquarium-kit/)
avatars/ Adventurers: 9 rigged, animated characters (KayKit by Kay Lousberg, CC0) — the
app's player avatars, placeable as animated models (Idle loops)
tools/avatars/ their build: merge to one skinned mesh + 1 material, shared clips, thumbnails;
diff --git a/aquarium-kit/CoralArch/glTF-Binary/coral-arch.glb b/aquarium-kit/CoralArch/glTF-Binary/coral-arch.glb
new file mode 100644
index 0000000..469b0e0
Binary files /dev/null and b/aquarium-kit/CoralArch/glTF-Binary/coral-arch.glb differ
diff --git a/aquarium-kit/CoralArch/glTF-Binary/coral-arch.lod1.glb b/aquarium-kit/CoralArch/glTF-Binary/coral-arch.lod1.glb
new file mode 100644
index 0000000..870bc69
Binary files /dev/null and b/aquarium-kit/CoralArch/glTF-Binary/coral-arch.lod1.glb differ
diff --git a/aquarium-kit/CoralArch/thumb.webp b/aquarium-kit/CoralArch/thumb.webp
new file mode 100644
index 0000000..49c4592
Binary files /dev/null and b/aquarium-kit/CoralArch/thumb.webp differ
diff --git a/aquarium-kit/CoralBranch/glTF-Binary/coral-branch.glb b/aquarium-kit/CoralBranch/glTF-Binary/coral-branch.glb
new file mode 100644
index 0000000..f7b1b70
Binary files /dev/null and b/aquarium-kit/CoralBranch/glTF-Binary/coral-branch.glb differ
diff --git a/aquarium-kit/CoralBranch/thumb.webp b/aquarium-kit/CoralBranch/thumb.webp
new file mode 100644
index 0000000..8ee523b
Binary files /dev/null and b/aquarium-kit/CoralBranch/thumb.webp differ
diff --git a/aquarium-kit/FishAngel/glTF-Binary/fish-angel.glb b/aquarium-kit/FishAngel/glTF-Binary/fish-angel.glb
new file mode 100644
index 0000000..9ba93cf
Binary files /dev/null and b/aquarium-kit/FishAngel/glTF-Binary/fish-angel.glb differ
diff --git a/aquarium-kit/FishAngel/glTF-Binary/fish-angel.lod1.glb b/aquarium-kit/FishAngel/glTF-Binary/fish-angel.lod1.glb
new file mode 100644
index 0000000..654cc65
Binary files /dev/null and b/aquarium-kit/FishAngel/glTF-Binary/fish-angel.lod1.glb differ
diff --git a/aquarium-kit/FishAngel/thumb.webp b/aquarium-kit/FishAngel/thumb.webp
new file mode 100644
index 0000000..9af7878
Binary files /dev/null and b/aquarium-kit/FishAngel/thumb.webp differ
diff --git a/aquarium-kit/FishClown/glTF-Binary/fish-clown.glb b/aquarium-kit/FishClown/glTF-Binary/fish-clown.glb
new file mode 100644
index 0000000..8cf90ee
Binary files /dev/null and b/aquarium-kit/FishClown/glTF-Binary/fish-clown.glb differ
diff --git a/aquarium-kit/FishClown/glTF-Binary/fish-clown.lod1.glb b/aquarium-kit/FishClown/glTF-Binary/fish-clown.lod1.glb
new file mode 100644
index 0000000..0288f8d
Binary files /dev/null and b/aquarium-kit/FishClown/glTF-Binary/fish-clown.lod1.glb differ
diff --git a/aquarium-kit/FishClown/glTF-Binary/fish-clown.lod2.glb b/aquarium-kit/FishClown/glTF-Binary/fish-clown.lod2.glb
new file mode 100644
index 0000000..0611cca
Binary files /dev/null and b/aquarium-kit/FishClown/glTF-Binary/fish-clown.lod2.glb differ
diff --git a/aquarium-kit/FishClown/thumb.webp b/aquarium-kit/FishClown/thumb.webp
new file mode 100644
index 0000000..1057d1d
Binary files /dev/null and b/aquarium-kit/FishClown/thumb.webp differ
diff --git a/aquarium-kit/FishTang/glTF-Binary/fish-tang.glb b/aquarium-kit/FishTang/glTF-Binary/fish-tang.glb
new file mode 100644
index 0000000..0841a87
Binary files /dev/null and b/aquarium-kit/FishTang/glTF-Binary/fish-tang.glb differ
diff --git a/aquarium-kit/FishTang/glTF-Binary/fish-tang.lod1.glb b/aquarium-kit/FishTang/glTF-Binary/fish-tang.lod1.glb
new file mode 100644
index 0000000..98d3c8a
Binary files /dev/null and b/aquarium-kit/FishTang/glTF-Binary/fish-tang.lod1.glb differ
diff --git a/aquarium-kit/FishTang/thumb.webp b/aquarium-kit/FishTang/thumb.webp
new file mode 100644
index 0000000..e573132
Binary files /dev/null and b/aquarium-kit/FishTang/thumb.webp differ
diff --git a/aquarium-kit/PlantEelgrass/glTF-Binary/plant-eelgrass.glb b/aquarium-kit/PlantEelgrass/glTF-Binary/plant-eelgrass.glb
new file mode 100644
index 0000000..bc079b7
Binary files /dev/null and b/aquarium-kit/PlantEelgrass/glTF-Binary/plant-eelgrass.glb differ
diff --git a/aquarium-kit/PlantEelgrass/thumb.webp b/aquarium-kit/PlantEelgrass/thumb.webp
new file mode 100644
index 0000000..2bebdac
Binary files /dev/null and b/aquarium-kit/PlantEelgrass/thumb.webp differ
diff --git a/aquarium-kit/PlantSword/glTF-Binary/plant-sword.glb b/aquarium-kit/PlantSword/glTF-Binary/plant-sword.glb
new file mode 100644
index 0000000..84f84b4
Binary files /dev/null and b/aquarium-kit/PlantSword/glTF-Binary/plant-sword.glb differ
diff --git a/aquarium-kit/PlantSword/thumb.webp b/aquarium-kit/PlantSword/thumb.webp
new file mode 100644
index 0000000..6ab1a3a
Binary files /dev/null and b/aquarium-kit/PlantSword/thumb.webp differ
diff --git a/aquarium-kit/attribution.html b/aquarium-kit/attribution.html
new file mode 100644
index 0000000..23c9783
--- /dev/null
+++ b/aquarium-kit/attribution.html
@@ -0,0 +1,11 @@
+
Attribution Statement
+
+Aquarium Kit (aquarium-kit) is © theprototype and released under
+ CC0 1.0 — use it for
+ anything, no credit required.
+
+Made with Meshy.ai: every piece (clownfish, regal blue tang,
+ emperor angelfish, reef rock arch, staghorn coral, Amazon sword plant, eelgrass clump) was
+ generated with Meshy's text-to-3D API on a paid plan (Meshy Terms of Use §3.2: paid-plan customers
+ own their output), then post-processed by theprototype (tools/aquarium-kit, tools/kit-build:
+ real metres, pivots, 1024² textures, a thin-film iridescence on the fish, LOD levels).
diff --git a/aquarium-kit/cover.webp b/aquarium-kit/cover.webp
new file mode 100644
index 0000000..fab8bb6
Binary files /dev/null and b/aquarium-kit/cover.webp differ
diff --git a/aquarium-kit/default.json b/aquarium-kit/default.json
new file mode 100644
index 0000000..6d39f6d
--- /dev/null
+++ b/aquarium-kit/default.json
@@ -0,0 +1,114 @@
+[
+ {
+ "name": "FishClown",
+ "screenshot": "thumb.webp",
+ "label": "Clownfish (realistic)",
+ "variants": {
+ "glTF-Binary": "fish-clown.glb"
+ },
+ "lods": [
+ {
+ "file": "fish-clown.lod1.glb",
+ "ratio": 0.5
+ },
+ {
+ "file": "fish-clown.lod2.glb",
+ "ratio": 0.39
+ }
+ ],
+ "size": [0.132, 0.142, 0.32],
+ "box": [-0.066, -0.071, -0.16, 0.066, 0.071, 0.16],
+ "tris": 4000,
+ "bytes": 610416
+ },
+ {
+ "name": "FishTang",
+ "screenshot": "thumb.webp",
+ "label": "Regal blue tang (realistic)",
+ "variants": {
+ "glTF-Binary": "fish-tang.glb"
+ },
+ "lods": [
+ {
+ "file": "fish-tang.lod1.glb",
+ "ratio": 0.5
+ }
+ ],
+ "size": [0.172, 0.218, 0.42],
+ "box": [-0.086, -0.109, -0.21, 0.086, 0.109, 0.21],
+ "tris": 3973,
+ "bytes": 588728
+ },
+ {
+ "name": "FishAngel",
+ "screenshot": "thumb.webp",
+ "label": "Emperor angelfish (realistic)",
+ "variants": {
+ "glTF-Binary": "fish-angel.glb"
+ },
+ "lods": [
+ {
+ "file": "fish-angel.lod1.glb",
+ "ratio": 0.5
+ }
+ ],
+ "size": [0.176, 0.238, 0.42],
+ "box": [-0.088, -0.119, -0.21, 0.088, 0.119, 0.21],
+ "tris": 3908,
+ "bytes": 751488
+ },
+ {
+ "name": "CoralArch",
+ "screenshot": "thumb.webp",
+ "label": "Reef rock arch with coral (swim-through)",
+ "variants": {
+ "glTF-Binary": "coral-arch.glb"
+ },
+ "lods": [
+ {
+ "file": "coral-arch.lod1.glb",
+ "ratio": 0.51
+ }
+ ],
+ "size": [1, 0.692, 0.544],
+ "box": [-0.5, 0, -0.272, 0.5, 0.692, 0.272],
+ "tris": 5209,
+ "bytes": 780648
+ },
+ {
+ "name": "CoralBranch",
+ "screenshot": "thumb.webp",
+ "label": "Staghorn coral on reef rock",
+ "variants": {
+ "glTF-Binary": "coral-branch.glb"
+ },
+ "size": [0.566, 0.5, 0.538],
+ "box": [-0.283, 0, -0.269, 0.283, 0.5, 0.269],
+ "tris": 5234,
+ "bytes": 875996
+ },
+ {
+ "name": "PlantSword",
+ "screenshot": "thumb.webp",
+ "label": "Amazon sword plant",
+ "variants": {
+ "glTF-Binary": "plant-sword.glb"
+ },
+ "size": [0.568, 0.9, 0.614],
+ "box": [-0.284, 0, -0.307, 0.284, 0.9, 0.307],
+ "tris": 4004,
+ "bytes": 928396
+ },
+ {
+ "name": "PlantEelgrass",
+ "screenshot": "thumb.webp",
+ "label": "Eelgrass clump (vallisneria)",
+ "variants": {
+ "glTF-Binary": "plant-eelgrass.glb"
+ },
+ "size": [0.768, 1.3, 0.772],
+ "box": [-0.384, 0, -0.386, 0.384, 1.3, 0.386],
+ "tris": 3509,
+ "bytes": 1033668
+ }
+]
diff --git a/aquarium-kit/kit.md b/aquarium-kit/kit.md
new file mode 100644
index 0000000..578f2f4
--- /dev/null
+++ b/aquarium-kit/kit.md
@@ -0,0 +1,50 @@
+# Aquarium Kit — kit guide
+
+Seven pieces for a reef tank, a pond or an underwater scene: three realistic reef fish, a reef
+rock arch a fish can swim through, a staghorn coral and two aquatic plants. They were made for the
+**Aquarium** example (Templates ▸ Examples), where the fish swim with core's general-purpose motion
+nodes (Follow Path, Wander, Orient to Velocity, Body Wave) and wear the **Fish scales** thin-film
+look.
+
+## Scale and pivots
+
+**1 unit = 1 metre.** The Aquarium's tank is 4 × 2 × 1.6 m, so the fish are sized for it (a real
+clownfish is 0.1 m — scale a piece down for a desk tank).
+
+| Piece | Size x × y × z (m) | Tris | LODs | Pivot | Place it |
+|---|---|---|---|---|---|
+| `FishClown` | 0.13 × 0.14 × 0.32 | 4000 | 2 | **centre**, nose +Z | free in the water |
+| `FishTang` | 0.17 × 0.22 × 0.42 | 3973 | 1 | **centre**, nose +Z | free in the water |
+| `FishAngel` | 0.18 × 0.24 × 0.42 | 3908 | 1 | **centre**, nose +Z | free in the water |
+| `CoralArch` | 1.0 × 0.69 × 0.54 | 5209 | 1 | bottom centre | on the sand; the opening runs along X |
+| `CoralBranch` | 0.57 × 0.5 × 0.54 | 5234 | — | bottom centre | on the sand or a rock |
+| `PlantSword` | 0.57 × 0.9 × 0.61 | 4004 | — | bottom centre | on the sand, back of the tank |
+| `PlantEelgrass` | 0.77 × 1.3 × 0.77 | 3509 | — | bottom centre | in clumps along the back glass |
+
+(`kit-build dims` writes the exact `size` / `box` of every row into `default.json`.)
+
+The fish face **+Z** and turn about their **middle** (the new `center` pivot rule in
+`tools/kit-build`): that is what Follow Path, Orient to Velocity and Body Wave expect (`forward`
++z), and what lets a fish bank and bend about its own body.
+
+## The look
+
+The fish carry **KHR_materials_iridescence** (factor 0.55, IOR 1.45, 140-460 nm): the thin-film
+flash of a turning fish. Core draws it at the device's **look tier** — on a desktop and a phone, not
+in a headset (40 F16). The plants are double-sided (their ribbon leaves have no back faces).
+
+## Levels of detail
+
+`kit-build lod` built levels for the fish and the arch. The branching coral and both plants have
+none: the visual gate refuses every candidate (thin branches and ribbon leaves need impostor
+levels, roadmap-33 F2 — listed in `tools/kit-build/allow.json`); core's automatic runtime LOD still
+applies.
+
+## How it was made
+
+Meshy.ai, preview-then-refine under the `40-aquarium` cap (310): eleven previews (5 credits each,
+`meshy-6-lite`, style none — "realistic"), looked at as a sheet; one rejected (a potted garden plant)
+and one angelfish dropped for a spike; seven refined (10 each); the arch (black inside) and the
+Amazon sword (lime, dark spots) retextured (10 each) — **145 credits**. Then
+`node tools/aquarium-kit/build.mjs` (kit-build's post step + the film + double-sided leaves),
+`kit-build lod`, `thumbs`, `dims --write`, `check`.
diff --git a/index.json b/index.json
index d7a191f..bcf992c 100644
--- a/index.json
+++ b/index.json
@@ -43,8 +43,8 @@
"copyright": "(c) theprototype",
"license": "CC0-1.0",
"source": "https://github.com/theprototype-app/packs"
- },
- {
+ },
+ {
"name": "architecture-kit",
"title": "Modular Architecture Kit",
"value": "architecture-kit/default.json",
@@ -53,8 +53,8 @@
"license": "CC0-1.0",
"source": "https://github.com/theprototype-app/packs/tree/main/architecture-kit",
"cover": "architecture-kit/cover.webp"
- },
- {
+ },
+ {
"name": "props-kit",
"title": "Props & Interiors",
"value": "props-kit/default.json",
@@ -63,8 +63,8 @@
"license": "CC0-1.0",
"source": "https://github.com/theprototype-app/packs/tree/main/props-kit",
"cover": "props-kit/cover.webp"
- },
- {
+ },
+ {
"name": "scifi-kit",
"title": "Sci-fi & Modern Kit",
"value": "scifi-kit/default.json",
@@ -114,6 +114,16 @@
"source": "https://github.com/theprototype-app/packs/tree/main/arcane-kit",
"cover": "arcane-kit/cover.webp"
},
+ {
+ "name": "aquarium-kit",
+ "title": "Aquarium Kit (reef fish, coral, aquatic plants)",
+ "value": "aquarium-kit/default.json",
+ "attribution": "aquarium-kit/attribution.html",
+ "copyright": "(c) theprototype",
+ "license": "CC0-1.0",
+ "source": "https://github.com/theprototype-app/packs/tree/main/aquarium-kit",
+ "cover": "aquarium-kit/cover.webp"
+ },
{
"name": "avatars",
"title": "Adventurers (rigged characters)",
diff --git a/tools/aquarium-kit/build.mjs b/tools/aquarium-kit/build.mjs
new file mode 100644
index 0000000..52948b5
--- /dev/null
+++ b/tools/aquarium-kit/build.mjs
@@ -0,0 +1,89 @@
+// build: kit.json → aquarium-kit/ (the pack), reproducibly, from the staged Meshy outputs (roadmap 40
+// F14). Nothing here spends credits: it reads staging/40-aquarium//-/raw.glb.
+//
+// node tools/aquarium-kit/build.mjs [--only Name,Name]
+// node tools/kit-build/kit-build.mjs lod aquarium-kit && node tools/kit-build/kit-build.mjs thumbs aquarium-kit
+// node tools/kit-build/kit-build.mjs dims aquarium-kit --write && node tools/kit-build/kit-build.mjs check aquarium-kit
+//
+// Per item: kit-build's post step (meshy-post: weld, decimate to targetTris, real metres, pivot,
+// 1024² JPEG; the emissive policy; defight), then two material touches the glTF carries to core:
+// film — KHR_materials_iridescence on every material: the thin-film flash of a turning fish
+// (core's look tier draws it on desktops and phones and drops it in a headset, 40 F16)
+// doubleSided — thin leaves are seen from both sides (a plant's ribbons have no back faces)
+// Output: aquarium-kit//glTF-Binary/ + aquarium-kit/default.json (the model list; the
+// lods / size / box / tris / bytes fields are added by kit-build lod + dims).
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { kitPost } from '../kit-build/lib/post.mjs';
+import { load, makeIO } from '../kit-build/lib/deps.mjs';
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+const REPO = path.resolve(HERE, '../..');
+const kit = JSON.parse(fs.readFileSync(path.join(HERE, 'kit.json'), 'utf8'));
+const STAGING = process.env.MESHY_STAGING || path.join(os.homedir(), '.code/lanes-30/meshy/staging', kit.staging);
+const PACK = path.join(REPO, kit.pack);
+const { KHRMaterialsIridescence } = await load('@gltf-transform/extensions');
+const io = makeIO();
+
+const args = process.argv.slice(2);
+const only = args.includes('--only') ? new Set(args[args.indexOf('--only') + 1].split(',')) : null;
+
+/** newest staged raw for a job/stage @param {{job: string, stage: string}} src */
+function stagedRaw(src) {
+ const dir = path.join(STAGING, src.job);
+ const n = fs
+ .readdirSync(dir)
+ .filter((d) => d.startsWith(src.stage + '-') && fs.existsSync(path.join(dir, d, 'raw.glb')))
+ .map((d) => Number(d.slice(src.stage.length + 1)))
+ .sort((a, b) => b - a)[0];
+ if (!n) throw new Error(`no staged ${src.stage} for ${src.job} under ${dir}`);
+ return path.join(dir, `${src.stage}-${n}`, 'raw.glb');
+}
+
+/** the thin film on every material of a file @param {string} file @param {{factor: number, ior: number, thickness: number[]}} film */
+async function addFilm(file, film) {
+ const doc = await io.read(file);
+ const ext = doc.createExtension(KHRMaterialsIridescence);
+ for (const mat of doc.getRoot().listMaterials()) {
+ const irid = ext
+ .createIridescence()
+ .setIridescenceFactor(film.factor)
+ .setIridescenceIOR(film.ior)
+ .setIridescenceThicknessMinimum(film.thickness[0])
+ .setIridescenceThicknessMaximum(film.thickness[1]);
+ mat.setExtension('KHR_materials_iridescence', irid);
+ }
+ await io.write(file, doc);
+}
+
+/** @param {string} file */
+async function setDoubleSided(file) {
+ const doc = await io.read(file);
+ for (const mat of doc.getRoot().listMaterials()) mat.setDoubleSided(true);
+ await io.write(file, doc);
+}
+
+const rows = [];
+const report = [];
+const existing = fs.existsSync(path.join(PACK, 'default.json')) ? JSON.parse(fs.readFileSync(path.join(PACK, 'default.json'), 'utf8')) : [];
+for (const it of kit.items) {
+ const prior = existing.find((/** @type {any} */ r) => r.name === it.name);
+ if (only && !only.has(it.name)) {
+ if (prior) rows.push(prior);
+ continue;
+ }
+ const raw = stagedRaw(it.src);
+ const out = path.join(PACK, it.name, 'glTF-Binary', it.file);
+ fs.mkdirSync(path.dirname(out), { recursive: true });
+ const r = await kitPost(raw, out, { id: it.name, ...it.post });
+ if (it.film) await addFilm(out, kit.film);
+ if (it.doubleSided) await setDoubleSided(out);
+ report.push({ name: it.name, raw: path.relative(STAGING, raw), tris: r.trisOut ?? r.tris, bytes: fs.statSync(out).size, film: !!it.film });
+ // keep what kit-build measured last time (lods, dims) until it measures again
+ rows.push({ ...(prior ?? {}), name: it.name, screenshot: 'thumb.webp', label: it.label, variants: { 'glTF-Binary': it.file } });
+ console.log(`${it.name}: ${path.relative(REPO, out)} (${fs.statSync(out).size} bytes)`);
+}
+fs.writeFileSync(path.join(PACK, 'default.json'), JSON.stringify(rows, null, 1) + '\n');
+fs.writeFileSync(path.join(HERE, 'report.json'), JSON.stringify(report, null, 1) + '\n');
diff --git a/tools/aquarium-kit/kit.json b/tools/aquarium-kit/kit.json
new file mode 100644
index 0000000..eaf92e6
--- /dev/null
+++ b/tools/aquarium-kit/kit.json
@@ -0,0 +1,21 @@
+{
+ "pack": "aquarium-kit",
+ "staging": "40-aquarium",
+ "items": [
+ {"name": "FishClown", "file": "fish-clown.glb", "label": "Clownfish (realistic)", "src": {"job": "fish-clown-a", "stage": "refine"},
+ "post": {"targetTris": 4000, "dims": {"z": 0.32}, "fit": "contain", "pivot": "center"}, "film": true, "category": "prop", "pivotRule": "center"},
+ {"name": "FishTang", "file": "fish-tang.glb", "label": "Regal blue tang (realistic)", "src": {"job": "fish-tang-a", "stage": "refine"},
+ "post": {"targetTris": 4000, "dims": {"z": 0.42}, "fit": "contain", "pivot": "center"}, "film": true, "category": "prop", "pivotRule": "center"},
+ {"name": "FishAngel", "file": "fish-angel.glb", "label": "Emperor angelfish (realistic)", "src": {"job": "fish-angel-a", "stage": "refine"},
+ "post": {"targetTris": 4000, "dims": {"z": 0.42}, "fit": "contain", "pivot": "center"}, "film": true, "category": "prop", "pivotRule": "center"},
+ {"name": "CoralArch", "file": "coral-arch.glb", "label": "Reef rock arch with coral (swim-through)", "src": {"job": "coral-arch-b", "stage": "retexture"},
+ "post": {"targetTris": 5000, "dims": {"x": 1.0}, "fit": "contain", "pivot": "bottom-center"}},
+ {"name": "CoralBranch", "file": "coral-branch.glb", "label": "Staghorn coral on reef rock", "src": {"job": "coral-branch-a", "stage": "refine"},
+ "post": {"targetTris": 5000, "dims": {"y": 0.5}, "fit": "contain", "pivot": "bottom-center"}},
+ {"name": "PlantSword", "file": "plant-sword.glb", "label": "Amazon sword plant", "src": {"job": "plant-sword-b", "stage": "retexture"},
+ "post": {"targetTris": 4000, "dims": {"y": 0.9}, "fit": "contain", "pivot": "bottom-center"}, "doubleSided": true},
+ {"name": "PlantEelgrass", "file": "plant-eelgrass.glb", "label": "Eelgrass clump (vallisneria)", "src": {"job": "plant-vallis-a", "stage": "refine"},
+ "post": {"targetTris": 4000, "dims": {"y": 1.3}, "fit": "contain", "pivot": "bottom-center"}, "doubleSided": true}
+ ],
+ "film": {"factor": 0.55, "ior": 1.45, "thickness": [140, 460]}
+}
diff --git a/tools/aquarium-kit/report.json b/tools/aquarium-kit/report.json
new file mode 100644
index 0000000..a7d153e
--- /dev/null
+++ b/tools/aquarium-kit/report.json
@@ -0,0 +1,51 @@
+[
+ {
+ "name": "FishClown",
+ "raw": "fish-clown-a/refine-1/raw.glb",
+ "tris": 4000,
+ "bytes": 610416,
+ "film": true
+ },
+ {
+ "name": "FishTang",
+ "raw": "fish-tang-a/refine-1/raw.glb",
+ "tris": 3973,
+ "bytes": 588728,
+ "film": true
+ },
+ {
+ "name": "FishAngel",
+ "raw": "fish-angel-a/refine-1/raw.glb",
+ "tris": 3916,
+ "bytes": 751488,
+ "film": true
+ },
+ {
+ "name": "CoralArch",
+ "raw": "coral-arch-b/retexture-1/raw.glb",
+ "tris": 5209,
+ "bytes": 780648,
+ "film": false
+ },
+ {
+ "name": "CoralBranch",
+ "raw": "coral-branch-a/refine-1/raw.glb",
+ "tris": 5234,
+ "bytes": 875996,
+ "film": false
+ },
+ {
+ "name": "PlantSword",
+ "raw": "plant-sword-b/retexture-1/raw.glb",
+ "tris": 4005,
+ "bytes": 928396,
+ "film": false
+ },
+ {
+ "name": "PlantEelgrass",
+ "raw": "plant-vallis-a/refine-1/raw.glb",
+ "tris": 3512,
+ "bytes": 1033668,
+ "film": false
+ }
+]
diff --git a/tools/kit-build/allow.json b/tools/kit-build/allow.json
index ffa2196..bed682b 100644
--- a/tools/kit-build/allow.json
+++ b/tools/kit-build/allow.json
@@ -103,6 +103,24 @@
"item": "*",
"check": "lods",
"reason": "skinned characters: a decimated level changes the skin weights and the rig, and a scene holds a handful of them (8 peers = ~40k tris); core's runtime LOD skips skinned meshes anyway"
+ },
+ {
+ "pack": "aquarium-kit",
+ "item": "CoralBranch",
+ "check": "lods",
+ "reason": "tools/lod builds no level: no level reaches <= 80 % within the error caps (best 0.86 / 0.86) — the visual gate refuses every candidate (thin coral branches / ribbon leaves need impostor levels, roadmap-33 F2); core's automatic runtime LOD still applies"
+ },
+ {
+ "pack": "aquarium-kit",
+ "item": "PlantSword",
+ "check": "lods",
+ "reason": "tools/lod builds no level: no level reaches <= 80 % within the error caps (best 1 / 1) — the visual gate refuses every candidate (thin coral branches / ribbon leaves need impostor levels, roadmap-33 F2); core's automatic runtime LOD still applies"
+ },
+ {
+ "pack": "aquarium-kit",
+ "item": "PlantEelgrass",
+ "check": "lods",
+ "reason": "tools/lod builds no level: no level reaches <= 80 % within the error caps (best 0.9 / 1) — the visual gate refuses every candidate (thin coral branches / ribbon leaves need impostor levels, roadmap-33 F2); core's automatic runtime LOD still applies"
}
]
}
diff --git a/tools/kit-build/lib/rules.mjs b/tools/kit-build/lib/rules.mjs
index a5d8afe..6cc2ed2 100644
--- a/tools/kit-build/lib/rules.mjs
+++ b/tools/kit-build/lib/rules.mjs
@@ -10,7 +10,7 @@ const BEHAVIOR_KEYS = ['type', 'clip', 'closeClip', 'trigger', 'autoplay', 'soun
export const MAX_LOD_FILES = 5;
const ROW_KEYS = ['name', 'label', 'screenshot', 'variants', 'lods', 'behavior', 'size', 'box', 'tris', 'bytes', 'animated'];
const INDEX_KEYS = ['name', 'title', 'value', 'zip', 'attribution', 'copyright', 'license', 'source', 'cover'];
-export const PIVOTS = ['bottom-center', 'bottom-center-back', 'top-center', 'wall-pivot', 'wall-face', 'foot', 'hinge', 'any'];
+export const PIVOTS = ['bottom-center', 'bottom-center-back', 'top-center', 'wall-pivot', 'wall-face', 'foot', 'hinge', 'center', 'any'];
/** a repo-relative path that stays inside its folder @param {any} p */
export const safeRel = (p) => typeof p === 'string' && p.trim() !== '' && !p.includes('..') && !p.startsWith('/') && !/^[a-z]+:/i.test(p);
@@ -148,6 +148,9 @@ export function pivotProblem(min, max, rule) {
return p.y.min && min[0] <= 0 && max[0] >= 0 && min[2] <= 0 && max[2] >= 0 ? null : `pivot is not under the piece (${at})`;
case 'hinge':
return p.x.inside && p.y.inside && p.z.inside ? null : `the hinge pivot is outside the piece (${at})`;
+ // 40: free-floating pieces (fish, birds, a drone) turn and bend about their middle
+ case 'center':
+ return p.x.centre && p.y.centre && p.z.centre ? null : `pivot is not the bbox centre (${at})`;
}
return `unknown pivot rule "${rule}" (one of ${PIVOTS.join('|')})`;
}
diff --git a/tools/kit-build/packs.json b/tools/kit-build/packs.json
index 3bab59d..abc3095 100644
--- a/tools/kit-build/packs.json
+++ b/tools/kit-build/packs.json
@@ -1,5 +1,5 @@
{
- "note": "kit-build's per-pack policy (roadmap 34 E2). categories: triangle budget of LOD0 (drawn, per node) and the largest bbox extent in metres. packs: textureCap (largest texture side), emissive ('none' = nothing in the pack may glow, 'allowed'), the default category and pivot, and per-item overrides. Pivots: bottom-center | bottom-center-back | top-center | wall-pivot (shares the wall's bottom-centre origin, starts above the floor) | wall-face (origin on the wall line, piece on the wall's face) | foot (stands on its origin, which is under the piece but not its middle: a signpost's post, a pipe's open end) | hinge (origin within 5 cm of the piece: a moving part) | any (legacy packs that keep their source scene's coordinates).",
+ "note": "kit-build's per-pack policy (roadmap 34 E2). categories: triangle budget of LOD0 (drawn, per node) and the largest bbox extent in metres. packs: textureCap (largest texture side), emissive ('none' = nothing in the pack may glow, 'allowed'), the default category and pivot, and per-item overrides. Pivots: bottom-center | bottom-center-back | top-center | wall-pivot (shares the wall's bottom-centre origin, starts above the floor) | wall-face (origin on the wall line, piece on the wall's face) | foot (stands on its origin, which is under the piece but not its middle: a signpost's post, a pipe's open end) | hinge (origin within 5 cm of the piece: a moving part) | center (the bbox middle: a free-floating fish, bird or drone that turns about itself) | any (legacy packs that keep their source scene's coordinates).",
"limits": {
"glbFailBytes": 5242880,
"glbWarnBytes": 2097152,
@@ -392,6 +392,23 @@
"category": "hero",
"pivot": "bottom-center",
"about": "rigged KayKit characters (tools/avatars); one skinned mesh each, the app's avatars"
+ },
+ "aquarium-kit": {
+ "textureCap": 1024,
+ "emissive": "none",
+ "category": "prop",
+ "pivot": "bottom-center",
+ "items": {
+ "FishClown": {
+ "pivot": "center"
+ },
+ "FishTang": {
+ "pivot": "center"
+ },
+ "FishAngel": {
+ "pivot": "center"
+ }
+ }
}
}
}
diff --git a/tools/kit-build/test/rules.test.mjs b/tools/kit-build/test/rules.test.mjs
index 9ab1d67..dae0afc 100644
--- a/tools/kit-build/test/rules.test.mjs
+++ b/tools/kit-build/test/rules.test.mjs
@@ -58,6 +58,8 @@ test('pivot rules: each preset accepts its shape and refuses an off-origin piece
assert.match(String(pivotProblem([0.2, 0, 0.2], [1, 2, 1], 'foot')), /not under the piece/);
assert.equal(pivotProblem([-0.07, -0.03, -0.04], [0.07, 0.51, 0.04], 'hinge'), null);
assert.match(String(pivotProblem([1, 1, 1], [2, 2, 2], 'hinge')), /outside the piece/);
+ assert.equal(pivotProblem([-0.1, -0.08, -0.21], [0.1, 0.08, 0.21], 'center'), null, 'a fish about its middle');
+ assert.match(String(pivotProblem([-0.1, 0, -0.21], [0.1, 0.16, 0.21], 'center')), /not the bbox centre/, 'a fish standing on its belly');
assert.equal(pivotProblem([5, 5, 5], [6, 6, 6], 'any'), null);
assert.match(String(pivotProblem(wall[0], wall[1], 'middle')), /unknown pivot rule/);
});
diff --git a/tools/lod/defight-report.json b/tools/lod/defight-report.json
index f76ca9f..c880838 100644
--- a/tools/lod/defight-report.json
+++ b/tools/lod/defight-report.json
@@ -1036,5 +1036,40 @@
15
],
"left": "no candidate flickers clearly less"
+ },
+ "aquarium-kit/FishClown": {
+ "before": 0,
+ "after": 0,
+ "left": "under 1 cm² of overlap"
+ },
+ "aquarium-kit/FishTang": {
+ "before": 0,
+ "after": 0,
+ "left": "under 1 cm² of overlap"
+ },
+ "aquarium-kit/FishAngel": {
+ "before": 0,
+ "after": 0,
+ "left": "under 1 cm² of overlap"
+ },
+ "aquarium-kit/CoralArch": {
+ "before": 0,
+ "after": 0,
+ "left": "under 1 cm² of overlap"
+ },
+ "aquarium-kit/CoralBranch": {
+ "before": 0,
+ "after": 0,
+ "left": "under 1 cm² of overlap"
+ },
+ "aquarium-kit/PlantSword": {
+ "before": 0,
+ "after": 0,
+ "left": "under 1 cm² of overlap"
+ },
+ "aquarium-kit/PlantEelgrass": {
+ "before": 0,
+ "after": 0,
+ "left": "under 1 cm² of overlap"
}
}
diff --git a/tools/lod/report.json b/tools/lod/report.json
index 027a7e9..39526bc 100644
--- a/tools/lod/report.json
+++ b/tools/lod/report.json
@@ -3063,5 +3063,174 @@
"skipped": "under 2000 triangles"
}
]
+ },
+ "aquarium-kit": {
+ "floor": 500,
+ "items": [
+ {
+ "name": "FishClown",
+ "tris": 4000,
+ "levels": [
+ {
+ "file": "fish-clown.lod1.glb",
+ "tris": 1999,
+ "ratio": 0.5,
+ "bytes": 150488,
+ "pairs": 0,
+ "defight": 55,
+ "grow": 0,
+ "cap": 0.01,
+ "mean": 12
+ },
+ {
+ "file": "fish-clown.lod2.glb",
+ "tris": 1569,
+ "ratio": 0.39,
+ "bytes": 108908,
+ "pairs": 0,
+ "defight": 52,
+ "grow": 0,
+ "cap": 0.04,
+ "mean": 26.3
+ }
+ ]
+ },
+ {
+ "name": "FishTang",
+ "tris": 3973,
+ "levels": [
+ {
+ "file": "fish-tang.lod1.glb",
+ "tris": 1986,
+ "ratio": 0.5,
+ "bytes": 145656,
+ "pairs": 1,
+ "defight": 3,
+ "grow": 0,
+ "cap": 0.01,
+ "mean": 17.1
+ }
+ ],
+ "droppedLevels": [
+ {
+ "level": 2,
+ "tris": 3973,
+ "ratio": 1
+ }
+ ]
+ },
+ {
+ "name": "FishAngel",
+ "tris": 3908,
+ "levels": [
+ {
+ "file": "fish-angel.lod1.glb",
+ "tris": 1952,
+ "ratio": 0.5,
+ "bytes": 160460,
+ "pairs": 2,
+ "defight": 16,
+ "grow": 0,
+ "cap": 0.02,
+ "mean": 22.4
+ }
+ ],
+ "droppedLevels": [
+ {
+ "level": 2,
+ "tris": 1850,
+ "ratio": 0.47,
+ "cap": 0.02,
+ "mean": 27.2
+ }
+ ]
+ },
+ {
+ "name": "CoralArch",
+ "tris": 5209,
+ "levels": [
+ {
+ "file": "coral-arch.lod1.glb",
+ "tris": 2682,
+ "ratio": 0.51,
+ "bytes": 195100,
+ "pairs": 0,
+ "defight": 1,
+ "grow": 0,
+ "cap": 0.03,
+ "mean": 28.2
+ }
+ ],
+ "droppedLevels": [
+ {
+ "level": 2,
+ "tris": 2645,
+ "ratio": 0.51,
+ "cap": 0.04,
+ "mean": 28.6
+ }
+ ]
+ },
+ {
+ "name": "CoralBranch",
+ "tris": 5234,
+ "levels": [],
+ "droppedLevels": [
+ {
+ "level": 1,
+ "tris": 4484,
+ "ratio": 0.86,
+ "cap": 0.04,
+ "mean": 12.3
+ },
+ {
+ "level": 2,
+ "tris": 4484,
+ "ratio": 0.86,
+ "cap": 0.04,
+ "mean": 11.3
+ }
+ ],
+ "skipped": "no level reaches <= 80 % within the error caps (best 0.86 / 0.86)"
+ },
+ {
+ "name": "PlantSword",
+ "tris": 4004,
+ "levels": [],
+ "droppedLevels": [
+ {
+ "level": 1,
+ "tris": 4004,
+ "ratio": 1
+ },
+ {
+ "level": 2,
+ "tris": 4004,
+ "ratio": 1
+ }
+ ],
+ "skipped": "no level reaches <= 80 % within the error caps (best 1 / 1)"
+ },
+ {
+ "name": "PlantEelgrass",
+ "tris": 3509,
+ "levels": [],
+ "droppedLevels": [
+ {
+ "level": 1,
+ "tris": 3157,
+ "ratio": 0.9,
+ "cap": 0.01,
+ "mean": 19.4
+ },
+ {
+ "level": 2,
+ "tris": 3509,
+ "ratio": 1
+ }
+ ],
+ "skipped": "no level reaches <= 80 % within the error caps (best 0.9 / 1)"
+ }
+ ]
}
}