Browser-based 3D editor for creating low-poly N64/PS1-style objects, exportable as .glb.
- 9 editor primitives/shapes: cube, sphere, cylinder, cone, plane, capsule, torus, wedge, pyramid
- Importable custom meshes: freeform triangle geometry via
custom/meshwithvertices+faces - 37 templates organized by category (Furniture, Nature, Architecture, Props, Characters)
- Multiple selection with Ctrl+Click and object grouping/ungrouping
- 4 material types: Basic, Lambert, Phong, Standard
- Textures: drag-and-drop import, UV controls (offset, repeat, rotation)
- Texture editor: integrated paint canvas with brushes, eraser, undo, PNG download
- Per-face UVs: independent UV editing for each cube face with interactive UV MAP mode
- Texture persistence: painted and imported textures are saved with the scene
- PNG to Flat Model v2: turn a transparent PNG/WebP silhouette into an editable 2.5D shell with coverage-aware contours, controlled edge taper, connected-part cleanup, sampled or solid side walls, and local depth painting
- GLB export with embedded textures and animations
- Selective export: export only selected objects or the full scene
- JSON import for objects generated by external LLMs
- Animation system: JSON keyframes (position, rotation, scale, visible), playback timeline, GLB export with
AnimationClip - Animation mode: dedicated panel to manage, play, and delete animation clips
- Video motion capture: capture humanoid motion from a shared YouTube/browser video with MediaPipe and import it directly into the selected rigged model
- Local video Motion Ripper: load a video file, step frames, change capture speed, and keep source timing when importing
- Avatar Forge: generated head molds, facial controls, starter heroes, section dice, turntable, and reusable recipe JSON
- Local-first agent and MCP bridge: validated scene tools for the in-app assistant, Codex, Grok, and other MCP clients
- Bone system: skeleton visualization, attach/detach bones between
PivotGroups - Object list: collapsible panel with a hierarchical scene tree and selection from the list
- Name overlay: shows the selected object's name over the viewport
- Undo/Redo: Ctrl+Z / Ctrl+Shift+Z with a 50-action history
- Color picker: swatch palette plus native HTML color picker
- Persistence: localStorage save/load plus full-scene JSON export/import
- Retro palette of saturated N64-style colors
- Configurable snap for position, rotation, and scale
- Render toggles: flat shading, wireframe, pixelated texture mode
- Bilingual: instant English/Spanish UI switching
- Keyboard shortcuts for all common operations
npm install
npm run dev # development server
npm run build # production build in dist/
npm run verify # unit, data, visual and production-build checks
npm run test:e2e # Playwright smoke suiteRetrovisor includes a local-first agent bridge for Codex, Grok, and other MCP
clients, plus an AGENT panel inside the editor. It uses the same validated
scene-command registry and undo/redo path for both surfaces. Provider keys stay
in the local Node companion and are never bundled into the browser.
Quick start in PowerShell:
$env:RETROVISOR_AGENT_TOKEN = ([guid]::NewGuid().ToString("N") + [guid]::NewGuid().ToString("N"))
npm run agent:devSee README-MCP.md for the Spanish quick-start with Codex, Grok, and Kimi, token/cost accounting, current limitations, and evaluation prompts. See docs/retrovisor-agent.md for the architecture and trust model.
The project uses Vite + Three.js with ES modules:
src/
|-- main.js # Entry point, event wiring, and window bindings
`-- modules/
|-- state.js # Shared state (scene, camera, selectedMesh, config)
|-- scene.js # Scene setup, lights, grid, render loop, bones
|-- primitives.js # Primitive geometry creation
|-- template-registry.js # Declarative template registry
|-- templates.js # Generic builder + template UI generation
|-- selection.js # Click, Ctrl+Click, double-click, highlight
|-- materials.js # Material creation and editing, flat shading, wireframe
|-- textures.js # Texture loading, configuration, drag-and-drop, toggle
|-- texture-editor.js # Texture editor: paint canvas, per-face UVs, 3D preview
|-- actions.js # Duplicate, delete, group, ungroup, attach/detach bones
|-- ui.js # Properties panel, UV controls, toasts, color sync
|-- object-list.js # Object list panel with hierarchical tree
|-- undo.js # Undo/redo system with command pattern (50 actions max)
|-- animation.js # Animation compilation, playback, and control
|-- animation-import.js # JSON animation import and validation
|-- export.js # Selective GLB export with materials and animations
|-- persistence.js # Scene serialization with textures and animations
|-- json-import.js # Import of objects and animations from external JSON
|-- shortcuts.js # Keyboard shortcuts (including Ctrl+Z, Space)
|-- snap.js # Snap system for TransformControls
`-- i18n.js # Internationalization system (EN/ES)
- Left panel: click a primitive or template
- IMPORT OBJECT: paste JSON generated by an LLM (see
ask.md)
- Click: select object
- Ctrl+Click: add/remove from multi-selection
- Double-click: select the full group
- W/E/R: move/rotate/scale mode
- Delete: remove selected object
- Ctrl+Z: undo
- Ctrl+Shift+Z: redo
- Ctrl+D: duplicate
- Ctrl+G: group selection
- Ctrl+Shift+G: ungroup
- Space: play/pause animation
- Esc: exit animation mode
- OBJECTS button in the viewport to view the scene tree
- Click an item to select it
- Double-click to select the full root group
- Groups and
PivotGroups with children can be expanded/collapsed
- Select an object
- Drag an image to the "LOAD TEXTURE" area in the right panel, or use the Texture Editor button (
T) - In the editor, paint directly on the canvas, adjust brushes, and use the eraser
- For cubes, use UV MAP mode to assign canvas regions to each face independently
- Adjust UVs: offset, repeat, rotation (global or per face)
- Textures persist when saving the scene and are embedded when exporting GLB
- In the left panel, click PNG TO FLAT MODEL.
- Load a transparent PNG/WebP, or start from TRY REEF FISH.
- Adjust density and the true Max depth ceiling, then shape the rim with Edge depth and Edge falloff.
- Use Cell coverage to balance thin details against alpha noise, and choose Main shape or All parts for disconnected silhouette components.
- Choose a Sampled rim or Solid color side finish; Side tint multiplies the sampled source color or supplies the solid color.
- Paint local Inflate, Deflate, Smooth, or Erase corrections. Depth strokes have their own Undo/Redo history inside the workbench.
- Inspect the result from Front, 3/4, or Side, orbit freely, and optionally overlay polygons or vertices before clicking CREATE MODEL.
- Select the derived root group and use EDIT PNG MODEL to reopen its source, settings, and paint map.
The result has textured front/back surfaces and boundary walls, but remains a 2.5D reconstruction from one view rather than inferred hidden anatomy. V2 uses alpha-weighted cell coverage and projects boundary vertices within their cells; it does not use Marching Squares. GLB export welds the aligned editor meshes into one indexed manifold shell with separate surface/rim material groups.
Create/Update waits for the latest valid generation, and closing a dirty
workbench asks before discarding changes. Scene saves store one compact editable
recipe instead of duplicating generated geometry, then regenerate the mesh and
texture on load. Compact object JSON also supports the recipe, transform,
animations, and non-generated child objects. GLB export embeds the texture,
emits the cutout surface as alpha MASK, and removes editor-only recipes/data
URLs from node extras.
Uploads are limited to 5 MB. PNG/WebP headers are checked before decoding (maximum 8192 px per dimension and 32 Mi pixels), then accepted images are normalized locally to PNG with a longest side of at most 1024 px. See docs/png-to-flat-model.md for the complete controls, persistence behavior, migrations, limits, and troubleshooting.
- Enable BONES in the top bar to visualize the skeleton
- Click a bone sphere to select the
PivotGroup - Shift+Click another bone to reparent it (attach)
- Use Detach in the properties panel to separate it
- Generate an animation with the prompt in
ask-animation.md, or include it in the object's JSON - Select a group and use Space or the timeline controls to play it
- Enter Animation Mode to manage clips: play, delete, import new ones, or capture a new take from video
- When exporting GLB, animations are included as
AnimationClips
- Select a humanoid group already prepared for animation.
- Open Animation Mode.
- Click CAPTURE FROM YOUTUBE / VIDEO.
- Share the browser tab or window where the reference video is playing.
- Wait until tracking locks, click SET NEUTRAL, then START RECORD.
- Play the reference move, stop the recording, and click IMPORT INTO CURRENT MODEL.
This capture flow now targets the internal humanoid rig (HUMANOID_DEFAULT) across torso, head, clavicles, arms, hands, legs, and feet, then translates the result into the selected model through its current rig bindings. The current pass also applies extra stabilization to shoulders and feet so noisy frames do not snap the imported take.
The browser-side motion capture flow was adapted from the Motion Ripper idea in Animateur, created by ilatroce. In LowPoly64, that approach is integrated into the existing animation mode and retargeted to the project's own models and skeleton bindings.
- Export GLB: if objects are selected, export only those; otherwise export the full scene
- Export JSON: save the scene as reusable JSON, including animations and textures
- Copy/Download object JSON: export the selected object in importable format
Templates use a declarative format in src/data/templates/:
{
"id": "my-object",
"name": "My Object",
"category": "Props",
"pieces": [
{
"name": "BODY",
"geometry": { "type": "cube", "params": { "width": 2, "height": 3, "depth": 2 } },
"color": "#ffcc00",
"position": [0, 1.5, 0]
},
{
"name": "HEAD",
"geometry": { "type": "sphere", "params": { "radius": 1, "widthSegments": 8, "heightSegments": 6 } },
"color": "#ff6600",
"position": [0, 4, 0],
"parent": "BODY",
"pivot": [0, 1.5, 0]
}
],
"animations": [
{
"name": "idle",
"duration": 2,
"loop": true,
"tracks": [
{
"target": "BODY",
"property": "position",
"interpolation": "smooth",
"keyframes": [
{ "time": 0, "value": [0, 1.5, 0] },
{ "time": 1, "value": [0, 1.8, 0] },
{ "time": 2, "value": [0, 1.5, 0] }
]
}
]
}
]
}| Type | Parameters |
|---|---|
cube |
width, height, depth |
sphere |
radius, widthSegments, heightSegments |
cylinder |
radiusTop, radiusBottom, height, radialSegments |
cone |
radius, height, radialSegments |
plane |
width, height |
capsule |
radius, length, capSegments, radialSegments |
torus |
radius, tube, radialSegments, tubularSegments |
wedge |
width, height, depth |
pyramid |
width, height |
custom |
vertices, faces (triangles only) |
| Field | Description | Default |
|---|---|---|
name |
Piece name (used as the target in animations) | PIECE_N |
geometry |
{ type, params } |
required |
color |
Hex color | #ffcc00 |
position |
[x, y, z] |
[0, 0, 0] |
rotation |
[rx, ry, rz] in radians |
[0, 0, 0] |
scale |
[sx, sy, sz] |
[1, 1, 1] |
pivot |
[x, y, z] pivot point for rotation |
same as position |
parent |
Parent piece name (hierarchy) | none |
| Field | Description |
|---|---|
name |
Clip name |
duration |
Duration in seconds |
loop |
true/false |
tracks[].target |
Name of the piece to animate |
tracks[].property |
position, rotation, scale, or visible |
tracks[].interpolation |
linear, smooth, or step |
tracks[].keyframes[] |
{ time, value: [x, y, z] } |
You can generate 3D objects using any external LLM:
- Copy the prompt from
ask.md - Append your object description at the end
- Paste the JSON response into the IMPORT OBJECT modal
For animations, use the prompt in ask-animation.md, or include them directly in the object's JSON under the animations field.
npm run render -- <file.json> (or --template <id>) headlessly imports a
model exactly like the UI would, renders front/profile/three-quarter PNGs
with the editor chrome hidden, and writes a report.json with bounds and a
retro style-budget evaluation (triangles, texture size, flat-color count).
Feed the screenshots back to an LLM to close the "generate JSON -> look ->
fix" loop without opening the app:
npm run render -- my-character.json
npm run render -- --template n64_elf_hero_cm --views front,profile --json- Three.js - 3D engine
- Vite - bundler
- Tailwind CSS - styling (via CDN)
- Vanilla JS - no frameworks