Skip to content

Commit 33820f6

Browse files
committed
Add PRD for 3D asset optimization pipeline (GLB-only)
- Defines comprehensive GLB-only optimization pipeline - Integrates meshopt/Draco compression and KTX2 texture compression - Includes LOD generation and power-of-two texture resizing - Extends manifest to prevent reprocessing with config-aware hashing - Covers all automation steps: geometry, compression, textures, LODs
1 parent e4420b1 commit 33820f6

1 file changed

Lines changed: 325 additions & 0 deletions

File tree

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
# 3D Asset Optimization Pipeline (GLB-only)
2+
3+
## Overview
4+
5+
- **Context & Goals**
6+
7+
- Optimize runtime performance and load times by reducing GLB sizes and texture bandwidth for assets served from `/assets/models`.
8+
- Standardize a repeatable, fully-automated pipeline that runs in CI and locally via Yarn scripts.
9+
- Produce optional LOD variants for scalable rendering quality/perf across devices.
10+
- Ensure idempotency via a manifest that prevents reprocessing of unchanged inputs or config.
11+
12+
- **Current Pain Points**
13+
- Inconsistent model optimization; some models are large and uncompressed.
14+
- Missing texture compression to KTX2 and lack of power-of-two sizing.
15+
- No LOD variants for distance-based rendering.
16+
- Reprocessing wastes CPU; manifest lacks config-awareness.
17+
18+
## Proposed Solution
19+
20+
- **High‑level Summary**
21+
22+
- Accept GLB inputs only (export upstream handled elsewhere), operate on `public/assets/models/**`.
23+
- Apply geometry transforms: prune, dedup, weld, quantize, optional simplify (LOD).
24+
- Apply mesh compression (Draco and/or Meshopt) and compress textures to KTX2 with KHR_texture_basisu.
25+
- Add texture resizing to power-of-two with max dimension caps.
26+
- Extend manifest to include input hash and pipeline config hash; skip reprocessing when unchanged.
27+
28+
- **Architecture & Directory Structure**
29+
```
30+
vibe-coder-3d/
31+
├── scripts/
32+
│ └── optimize-models.js # Pipeline entrypoint (extended)
33+
├── public/
34+
│ └── assets/
35+
│ └── models/ # GLB inputs + outputs served by app
36+
│ └── <ModelName>/
37+
│ ├── glb/ # canonical GLB(s)
38+
│ ├── lod/ # optional LOD variants (e.g., LOD0/1/2)
39+
│ └── textures/ # external textures (if not embedded)
40+
├── docs/
41+
│ └── PRDs/
42+
│ └── 3d-asset-optimization-pipeline.md
43+
├── .model-optimization-manifest.json # Tracks inputs + config hash + outputs
44+
└── .model-optimization.config.json # Declarative pipeline config (versioned)
45+
```
46+
47+
## Implementation Plan
48+
49+
1. **Phase 1: Baseline + Config (0.5 day)**
50+
51+
1. Introduce `.model-optimization.config.json` (quantization, compression, texture policy, LOD policy, pipelineVersion).
52+
2. Extend manifest schema to include `fileHash`, `configHash`, `outputs`, `variants`.
53+
3. Wire config hashing into the existing script; skip when both hashes match.
54+
55+
2. **Phase 2: Geometry Optimizations (0.5 day)**
56+
57+
1. Keep `prune`, `dedup`, `weld`, `quantize` (already present).
58+
2. Add option for `simplify` (MeshoptSimplifier) gated by config.
59+
3. Add LOD generation loop with per-LOD simplify ratios; place outputs in `lod/`.
60+
61+
3. **Phase 3: Mesh Compression (0.5–1 day)**
62+
63+
1. Add Draco compression path (continue to use `draco3dgltf`).
64+
2. Add Meshopt compression path via either:
65+
- glTF-Transform Meshopt extension + `meshoptimizer` dependency, or
66+
- `gltfpack` CLI (if we choose CLI path; configurable).
67+
3. Choose method per-config: `compression: "draco" | "meshopt" | "both"`.
68+
69+
4. **Phase 4: Texture Pipeline (0.5–1 day)**
70+
71+
1. Enforce power-of-two and max-dimension resizing (external or embedded textures) using `sharp` or `imagemagick`.
72+
2. Convert textures to **KTX2** using `textureCompress` with `toktx` encoder (prefer ETC1S for general, UASTC for high-quality normals).
73+
3. Add/ensure `KHR_texture_basisu` on output; preserve sRGB/linear spaces.
74+
75+
5. **Phase 5: CI Integration & DX (0.5 day)**
76+
77+
1. Update `package.json` scripts and build to run optimization silently.
78+
2. Add logging summary + stats (size before/after, triangle count deltas).
79+
3. Document usage and troubleshooting.
80+
81+
6. **Phase 6: Tests & Validation (0.5 day)**
82+
1. Unit test manifest hashing + skip logic.
83+
2. Integration test end-to-end on sample assets (size reduction assertions, presence of KTX2/Draco/Meshopt).
84+
85+
## File and Directory Structures
86+
87+
```
88+
public/assets/models/<ModelName>/
89+
├── glb/
90+
│ └── <ModelName>.glb # canonical optimized glb (LOD0)
91+
├── lod/
92+
│ ├── <ModelName>.lod1.glb # optional (~50% tris)
93+
│ └── <ModelName>.lod2.glb # optional (~25% tris)
94+
└── textures/
95+
├── albedo.ktx2
96+
├── normal.ktx2
97+
└── orm.ktx2
98+
```
99+
100+
## Technical Details
101+
102+
- **Config file**: `.model-optimization.config.json`
103+
104+
```json
105+
{
106+
"pipelineVersion": 1,
107+
"geometry": {
108+
"quantize": { "position": 14, "normal": 10, "texcoord": 12, "color": 8, "generic": 12 },
109+
"simplify": { "enabled": true, "ratio": 0.6, "error": 0.001 }
110+
},
111+
"compression": {
112+
"method": "draco", // "meshopt" | "both"
113+
"draco": { "encodeSpeed": 5, "decodeSpeed": 5 },
114+
"meshopt": { "level": "medium" }
115+
},
116+
"textures": {
117+
"resize": { "enabled": true, "max": 2048, "powerOfTwo": true },
118+
"ktx2": { "enabled": true, "mode": "ETC1S", "quality": 128, "uastcZstandard": 18 }
119+
},
120+
"lod": { "enabled": true, "ratios": [1.0, 0.5, 0.25] }
121+
}
122+
```
123+
124+
- **Manifest schema**: `.model-optimization-manifest.json`
125+
126+
```json
127+
{
128+
"optimized": {
129+
"public/assets/models/FarmHouse/glb/farm_house_basic_shaded.glb": {
130+
"fileHash": "sha256:...",
131+
"configHash": "sha256:...",
132+
"outputs": {
133+
"lod0": { "path": ".../glb/farm_house_basic_shaded.glb", "size": 123456 },
134+
"lod1": { "path": ".../lod/farm_house_basic_shaded.lod1.glb", "size": 65432 },
135+
"lod2": { "path": ".../lod/farm_house_basic_shaded.lod2.glb", "size": 32100 }
136+
},
137+
"timestamp": 1730000000000
138+
}
139+
}
140+
}
141+
```
142+
143+
- **Script extension sketch** (ESM, Node):
144+
145+
```ts
146+
// scripts/optimize-models.js (sketch only)
147+
import { NodeIO } from '@gltf-transform/core';
148+
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
149+
import {
150+
prune,
151+
dedup,
152+
weld,
153+
quantize,
154+
simplify,
155+
textureCompress,
156+
} from '@gltf-transform/functions';
157+
import draco3d from 'draco3dgltf';
158+
// Optional Meshopt path A: gltf-transform + meshoptimizer
159+
// import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
160+
// Optional Meshopt path B: run `gltfpack` via child_process for EXT_meshopt
161+
162+
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({
163+
'draco3d.decoder': await draco3d.createDecoderModule(),
164+
'draco3d.encoder': await draco3d.createEncoderModule(),
165+
// 'meshopt.decoder': MeshoptDecoder,
166+
// 'meshopt.encoder': MeshoptEncoder,
167+
});
168+
169+
// 1) Geometry transforms
170+
await document.transform(
171+
prune(),
172+
dedup(),
173+
weld(),
174+
quantize({
175+
/* from config */
176+
}),
177+
);
178+
179+
// 2) LOD generation (if enabled): clone document, apply simplify() per ratio
180+
// 3) Mesh compression per config: Draco and/or Meshopt
181+
// 4) Textures: resize (sharp/imagemagick) then textureCompress({encoder: 'toktx', ...})
182+
// 5) Write outputs + update manifest entry with fileHash + configHash
183+
```
184+
185+
- **Texture resizing**:
186+
187+
- Prefer `sharp` (Node) for in-process resizing of external textures; for embedded textures, export via IO to buffers, resize, reattach.
188+
- Alternatively, call `magick`/`convert` (ImageMagick) as a child process if preferred.
189+
190+
- **KTX2 compression**:
191+
192+
- Use `textureCompress({ encoder: 'toktx', slots: ['baseColorTexture','normalTexture', ...], quality, uastc, etc })` from `@gltf-transform/functions`.
193+
- Ensure `toktx` binary availability in CI image and local dev PATH.
194+
195+
- **Mesh compression**:
196+
- Draco: keep current dependency `draco3dgltf` and add encode step.
197+
- Meshopt: choose one:
198+
- Transform path: register meshopt encoder/decoder and emit `EXT_meshopt_compression`.
199+
- CLI path: `gltfpack -cc -tc -kn -ke -km -si <ratio> -o out.glb in.glb`.
200+
201+
## Usage Examples
202+
203+
```bash
204+
# Local dev (silent):
205+
yarn optimize
206+
207+
# Force re-run (ignore manifest):
208+
yarn optimize:force
209+
210+
# CI (build): automatically runs optimize silently
211+
```
212+
213+
```json
214+
// package.json (scripts excerpt)
215+
{
216+
"scripts": {
217+
"optimize": "node scripts/optimize-models.js",
218+
"optimize:force": "node scripts/optimize-models.js --force",
219+
"build": "node scripts/sync-assets.js --once --silent && node scripts/optimize-models.js --silent && tsc && vite build"
220+
}
221+
}
222+
```
223+
224+
## Testing Strategy
225+
226+
- **Unit Tests**
227+
228+
- Manifest hashing and skip logic (`fileHash`, `configHash`).
229+
- Config parsing + defaults; invalid config surfaces actionable errors.
230+
- Texture policy: power-of-two enforcement decisions.
231+
232+
- **Integration Tests**
233+
- End-to-end run on sample GLB with embedded + external textures.
234+
- Assert presence of KHR_texture_basisu and KTX2 outputs.
235+
- Assert Draco/Meshopt compression presence per-config and size reductions.
236+
- Validate LOD variants exist and triangle counts decrease monotonically.
237+
238+
## Edge Cases
239+
240+
| Edge Case | Remediation |
241+
| ---------------------------------- | ------------------------------------------------------------------------ |
242+
| Embedded non-POT textures | Extract, resize to POT, re-embed, then KTX2 compress. |
243+
| Unsupported image formats | Convert to PNG/JPEG before KTX2; log warning. |
244+
| Animation data precision loss | Loosen quantization for joints/morph weights via config. |
245+
| Normal map quality loss with ETC1S | Use UASTC for normals; per-slot config. |
246+
| Draco vs Meshopt incompatibilities | Make compression mutually exclusive per-output; prefer one per asset. |
247+
| Missing `toktx` binary | Skip KTX2 with warning; exit non-zero in CI if `strictTextures` enabled. |
248+
| LOD artifacts | Tune simplify error/ratio; allow model-level overrides. |
249+
250+
## Sequence Diagram
251+
252+
```mermaid
253+
sequenceDiagram
254+
participant Dev as Developer
255+
participant Script as optimize-models.js
256+
participant FS as FileSystem
257+
participant KTX2 as toktx
258+
participant CI as CI/CD
259+
260+
Dev->>Script: yarn optimize
261+
Script->>FS: Scan public/assets/models/**.glb
262+
Script->>FS: Read .model-optimization.config.json
263+
Script->>FS: Read .model-optimization-manifest.json
264+
Script->>Script: Compute fileHash + configHash
265+
alt Already optimized (hashes match)
266+
Script-->>Dev: Skip
267+
else Needs optimization
268+
Script->>Script: prune/dedup/weld/quantize
269+
opt LOD
270+
Script->>Script: simplify per ratio
271+
end
272+
opt Mesh compression
273+
Script->>Script: Draco and/or Meshopt
274+
end
275+
opt Textures
276+
Script->>KTX2: Compress to KTX2 (ETC1S/UASTC)
277+
end
278+
Script->>FS: Write outputs (glb, lod/*, textures/*.ktx2)
279+
Script->>FS: Update manifest entry
280+
end
281+
CI->>Script: run on build (silent)
282+
Script-->>CI: summary (sizes, counts)
283+
```
284+
285+
## Risks & Mitigations
286+
287+
| Risk | Mitigation |
288+
| -------------------------------- | ---------------------------------------------------------------------------------- |
289+
| Increased complexity in pipeline | Centralize config; document presets; add telemetry summary. |
290+
| Binary tool availability (toktx) | Provide dev container/CI image with preinstalled tools; verify on start. |
291+
| Quality regressions | Start conservative; allow per-model overrides; snapshot testing of visuals. |
292+
| Decode perf tradeoffs | Prefer Meshopt for web if chosen; test on target devices; allow fallback to Draco. |
293+
294+
## Timeline
295+
296+
- Total: ~2.5–3.5 days
297+
- Phase 1: 0.5 day
298+
- Phase 2: 0.5 day
299+
- Phase 3: 0.5–1 day
300+
- Phase 4: 0.5–1 day
301+
- Phase 5: 0.5 day
302+
- Phase 6: 0.5 day
303+
304+
## Acceptance Criteria
305+
306+
- Pipeline operates on GLB-only inputs in `public/assets/models/**`.
307+
- Manifest prevents reprocessing when `fileHash` and `configHash` match.
308+
- Geometry passes: prune, dedup, weld, quantize applied.
309+
- Optional LOD variants produced when enabled; triangle counts reduced.
310+
- Mesh compression path(s) configurable: Draco and/or Meshopt.
311+
- Textures resized to POT and compressed to KTX2 with KHR_texture_basisu.
312+
- Build and local scripts run optimization silently with clear summary.
313+
- Tests pass validating size reduction and metadata presence.
314+
315+
## Conclusion
316+
317+
This pipeline makes model optimization consistent, fast, and safe by standardizing geometry, mesh, and texture compression steps with robust skip logic. It scales across devices via LODs and balances quality/performance through a single declarative config.
318+
319+
## Assumptions & Dependencies
320+
321+
- Inputs are GLB; export from DCC tools is out of scope here.
322+
- Node 18+, Yarn, `@gltf-transform/*` (core, extensions, functions), `draco3dgltf` available.
323+
- KTX2 requires `toktx` binary; install in CI image and advise local installation.
324+
- Meshopt requires either `meshoptimizer` (Node) or `gltfpack` CLI; choose per-config.
325+
- Image processing requires `sharp` or ImageMagick available in environment.

0 commit comments

Comments
 (0)