A modular 3D asset assembler for game prototyping. Compose characters, creatures, props, and environment pieces from a library of swappable parts, adjust alignment, swap textures, and export GLB assets ready for any modern engine.
Runs as a web app in any modern browser and as a native desktop app via Tauri on Windows, Mac, and Linux — from one codebase.
pnpm install
pnpm devOpen http://localhost:1420. Click Use Sample Library on the welcome screen.
pnpm install
pnpm tauri devRequires Rust and platform-specific dependencies. See the Tauri prerequisites.
-
Open a library — Click "Open Library Folder" to point at a folder with templates, parts, and textures. Or click "Use Sample Library" to start with the bundled example.
-
Create a new asset — Click "New Asset" in the topbar. Pick a template (e.g. Humanoid, Chest). The tool loads default parts into each slot and renders the assembled asset.
-
Select a slot — Click a part in the 3D viewport or click a slot name in the left sidebar. The selected slot highlights in the viewport.
-
Swap a part — With a slot selected, the library panel on the left filters to compatible parts. Click a part to swap it in. The change is immediate.
-
Adjust alignment — With a slot selected, the right sidebar shows transform controls. Use the gizmo in the viewport to drag, or type numeric values for position, rotation, and scale. Toggle between Translate/Rotate/Scale modes.
-
Swap a texture — Click a texture channel in the properties panel to open the texture drawer. Browse or search textures, click to assign. Import new PNGs by clicking "Import PNG".
-
Paint a texture — In the texture drawer, click "Edit" to open the pixel painter. Brush, eraser, fill bucket, color picker, and brush size. UV overlay shows where paint maps on the model. The viewport updates live as you paint. Save creates a new texture in the library.
-
Save the project — Click "Save" or "Save As" in the topbar. The project is a JSON file referencing the library by relative path.
-
Export GLB — Click "Export GLB" in the topbar. The assembled asset exports as a single GLB file that loads in Blender, Unity, Unreal, Godot, three.js, Babylon.js, or any GLB-compatible tool.
| Shortcut | Action |
|---|---|
| Ctrl+Z | Undo |
| Ctrl+Y / Ctrl+Shift+Z | Redo |
A library is a folder on disk:
my-library/
templates/
humanoid.template.json
chest.template.json
parts/
head/
head_male_base.glb
head_male_base.json
torso/
torso_male_base.glb
torso_male_base.json
textures/
skin_pale.png
skin_pale.json
A template defines the structure of an asset type — its slots, their anchors (position/rotation/scale in template space), default parts, and optional paired slots for mirroring.
{
"id": "humanoid",
"name": "Humanoid",
"description": "Basic biped with paired limbs",
"version": 1,
"slots": [
{
"tag": "head",
"name": "Head",
"anchor": { "position": [0, 1.6, 0], "rotation": [0, 0, 0], "scale": 1 },
"defaultPartId": "head_male_base",
"pairedSlot": null,
"required": true
}
]
}Each part is a GLB mesh file with a JSON sidecar describing its metadata:
{
"id": "head_male_base",
"name": "Male Head (Base)",
"tags": ["head"],
"meshFile": "parts/head/head_male_base.glb",
"defaultOffset": { "position": [0, 0, 0], "rotation": [0, 0, 0], "scale": 1 },
"textureSlots": [
{ "channel": "diffuse", "defaultTextureId": "skin_pale", "variants": [] }
],
"thumbnailFile": null
}- tags — which template slots this part can fill
- defaultOffset — applied on top of the slot anchor whenever this part is used
- textureSlots — declares which texture channels the part uses, with optional named variants
{
"id": "skin_pale",
"name": "Pale Skin",
"file": "textures/skin_pale.png",
"width": 128,
"height": 128,
"tags": ["skin", "human"]
}A saved project is a JSON file referencing a template and recording which part fills each slot, per-slot transform overrides, and texture assignments:
{
"id": "elf_warrior_v1",
"name": "Elf Warrior",
"templateId": "humanoid",
"version": 1,
"slots": {
"head": {
"partId": "head_elf",
"offset": { "position": [0, 0.02, 0], "rotation": [0, 0, 0], "scale": 1 },
"textures": { "diffuse": "head_elf_pale" }
}
}
}- Mirror — Toggle in the properties panel. When enabled, adjustments to a paired slot (e.g. left arm) automatically apply the X-mirrored transform to the opposite slot (right arm).
- Snap to grid — Toggle in the properties panel. Constrains position, rotation, and scale values to configurable increments (default: 0.05 translation, 5° rotation, 0.1 scale).
- Save as part default — Promotes the current per-instance offset into the part's library metadata, so every future use of that part starts from the corrected position.
Toggle in the viewport to render with PS1-era visual effects:
- Affine texture mapping (no perspective correction)
- Vertex snapping to configurable screen resolution (default 320×240)
- Low-resolution render target with point-filtered upscale
- Vertex color baking for the no-texture PS1 look
- Indexed-color (256-color) PNG export option
Create new templates without editing JSON. Load a base GLB, map each mesh to a named slot with a tag and anchor point, and save.
Built-in 2D pixel painter with brush, eraser, color picker, fill bucket, and per-canvas undo/redo. UV wireframe overlay shows how the 2D canvas maps to the 3D model. The viewport updates live as you paint.
- Preact — UI framework (~3KB gzipped)
- Zustand — global state management
- three.js — 3D viewport (imperative, no React Three Fiber)
- TypeScript — strict mode, all flags enabled
- Vite — dev server and build
- Tauri 2 — desktop packaging
- Rust — backend file I/O, schema validation,
ts-rsfor type sync - Vitest — unit and integration tests
pnpm install # Install dependencies
pnpm dev # Start Vite dev server
pnpm tauri dev # Start desktop app with hot reload
pnpm build # Production web build
pnpm test # Run tests
pnpm lint # ESLint
pnpm tsc --noEmit # Type check
pnpm generate:schema # Regenerate TypeScript types from Rustcd src-tauri
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --check
cargo testMIT



