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)" + } + ] } }