Host-assisted low-poly wireframe 3D compiler/runtime for a stock Commodore 64.
The toolkit preprocesses 3D geometry on the host machine and generates 6510/6502 assembly data and runnable C64 .prg files. It includes procedural test meshes, OBJ/MTL and SVG import with nearest-palette wire colours, several visibility modes, multiple animation transforms, multiple renderers, and prebuilt example programs.
Requires:
- VICE — Commodore 64 emulator; the toolkit uses
x64scby default. - 64tass — 6502/6510 cross-assembler.
- Python 3.
On Debian/Ubuntu and derivatives, VICE and 64tass can normally be installed with:
sudo apt install vice 64tassVerify that the required tools are available with:
./build.sh doctorThe toolkit now has an optional local configuration file for tool paths and default arguments. Copy the example if 64tass or x64sc are not directly in PATH, or if your installation needs custom command-line arguments:
cp config/c643d.ini.example config/c643d.iniconfig/c643d.ini is gitignored. If it is absent, built-in defaults are used. Command-line options override the config. The default VICE arguments include +VICIIfull, so --run opens VICE windowed rather than inheriting a saved fullscreen setting.
[toolchain]
tass = 64tass
vice = x64sc
tass_args =
vice_args = +VICIIfull
[macos]
# tass = /opt/homebrew/bin/64tass
# vice = /Applications/vice-arm64-gtk3-3.8/bin/x64sc
[windows]
# tass = C:\Tools\64tass\64tass.exe
# vice = C:\Tools\VICE\bin\x64sc.exeOn macOS, the easiest command-line installation is typically:
brew install tass64 viceFor a VICE package downloaded from the VICE site and moved into /Applications, prefer the package's real command-line binary directly, for example vice = /Applications/vice-arm64-gtk3-3.8/bin/x64sc. The architecture/frontend/version part of the directory name varies by download (for example ARM64 vs. Intel and GTK3 vs. SDL2). The toolkit also probes common package layouts and accepts a VICE distribution directory or .app path, but pointing straight at bin/x64sc is the least ambiguous option.
Existing direct overrides still work:
./build.sh --shape torus --tass /path/to/64tass --vice /path/to/x64sc --run
./build.sh --shape torus --vice-arg=+VICIIfull --run
./build.sh --shape torus --no-vice-default-args --runSee docs/CONFIGURATION.md for precedence, per-platform sections, C643D_CONFIG, --config/--no-config, and macOS package details.
Build and run one of the included procedural objects:
./build.sh --shape torus --runBuild and run an imported OBJ preset:
./build.sh --object horse_head --runor:
./build.sh --object sunflower_torus --runThe sunflower reads usemtl/Kd data from sunflower_torus.mtl and maps it to
C64 brown, yellow, and green. To deliberately keep the historical white-on-black
wireframe path:
./build.sh --object sunflower_torus --no-colors --runThe bundled SVG logo can be spun as a 3-D plane or sent away on a tilted crawl plane:
./build.sh --object space_horse --run
./build.sh --object space_horse_crawl --runBuild all included example .prg files:
./build.sh --generate-examplesBuild + run the reference torus with the current fastest renderer:
./build.sh --shape torus --runMake the torus denser:
# 72 vertices / 72 quad faces = 12 x 6 torus
./build.sh --shape torus --vertices 72 --run
# same topology, explicitly
./build.sh --shape torus --major-segments 12 --minor-segments 6 --run
# target approximate face/poly count
./build.sh --shape torus --polycount 100 --runRun the included low-poly horse head or sunflower:
./build.sh --object horse_head --run
./build.sh --object sunflower_torus --runThe horse OBJ is open/non-manifold in a few places, so its preset uses full surface Z-buffer visibility. This intentionally avoids face-normal pre-culling on unreliable topology; the Z-buffer also tracks face ownership so muzzle edges cannot self-occlude against their own adjacent faces. You can compare the lighter modes or the older front-face-only method with:
./build.sh --object horse_head --visibility surface_features --run
./build.sh --object horse_head --visibility surface_creases --feature-angle 40 --run
./build.sh --object horse_head --visibility frontface --runImport your own OBJ into the project, then build it:
./build.sh import-obj ~/models/my_ship.obj --as my_ship --up z
./build.sh --object my_ship --runOr compile a one-off OBJ without importing it:
./build.sh --obj ~/models/my_ship.obj --obj-up z --name MY_SHIP --runSVG artwork can be imported as wire geometry too. Curves are flattened and simplified on the host; --svg-depth optionally gives the contours a shallow Z extrusion:
./build.sh import-svg ~/art/logo.svg --as logo --animation spin
./build.sh --object logo --run
# one-off, shallow 3-D extrusion
./build.sh --svg ~/art/logo.svg --svg-depth 4 --color yellow --runAnimation modes are spin, recede, and crawl. recede keeps the artwork front-facing while moving it away from the camera; crawl tilts it onto a virtual plane and moves it upward/away toward a horizon:
./build.sh --object space_horse --animation recede --run
./build.sh --object space_horse --animation crawl --animation-tilt 62 --runRenderer comparison:
./build.sh --shape torus --renderer step --run # v0.7-style reference
./build.sh --shape torus --renderer bytechunk --run # v0.8 stable path
./build.sh --shape torus --renderer yunroll --run # current fastest pathUseful inspection commands:
./build.sh inspect --shape torus --vertices 72
./build.sh inspect --object horse_head
./build.sh list-shapes
./build.sh list-objectsThe bundled horse is deliberately compiled with --visibility surface. Its OBJ contains boundary and non-manifold topology, so using adjacent face normals as a pre-cull can make legitimate muzzle/snout edges disappear at some rotations. Full surface mode lets the host-side Z-buffer decide visibility instead.
surface_features retains the cheaper v0.3.1 behavior: ordinary two-face manifold edges are pre-culled when both adjacent faces are back-facing, while boundary/non-manifold edges survive to the surface depth test. surface_creases is the crease-aware variant and preserves sharp manifold edges according to --feature-angle.
./build.sh --object horse_head --visibility surface_features --run
./build.sh --object horse_head --visibility surface_creases --feature-angle 40 --runThe emitter can spill whole per-orientation line blocks into otherwise-unused RAM below bitmap #2, so the full horse surface mode still fits 36 sampled orientations without reducing the mesh.
The repository has an examples/ manifest. Build all reference PRGs at once:
./build.sh --generate-examples
# equivalent:
./build.sh generate-examplesThis currently produces torus.prg, torus_dense.prg, cube.prg, sphere.prg, horse_head.prg, monochrome sunflower_torus.prg, coloured sunflower_torus_color.prg, space_horse_spin_color.prg, and space_horse_crawl_color.prg. Auxiliary labels/listings remain in build/; the runnable PRGs are copied to examples/.
Every build now performs an early preflight. 64tass is required unless --no-assemble is used. VICE is optional for build-only work but required for --run. Override executable names/paths with --tass and --vice.
./build.sh doctorOn Debian/Ubuntu, distro VICE packages can be DFSG-stripped and omit Commodore ROM images. The emulator executable may therefore exist but still fail at machine startup until compatible ROMs are installed/configured. This is separate from the toolkit preflight, which only verifies that the executable is present.
The reference torus now runs around 15-18 FPS with yunroll on stock PAL C64 timing in VICE during development. It is native 320x200 hires, hidden-line clipped, triple-buffered, and does not use pre-rendered bitmap animation frames.
The project grew out of the rotating-torus benchmark, a.k.a. THE WORLD'S MOST DANGEROUS ROTATING DONUT, and is now being generalized into a reusable mesh-to-C64 pipeline.
The repository includes the actual objects/horse_head.obj low-poly model (64 vertices / 124 edges / 65 faces), objects/sunflower_torus.obj + .mtl (76 vertices / 142 edges / 70 faces), and the bundled objects/space_horse.svg vector-logo demo.
The host-side Python compiler performs expensive work that makes sense to precompute for a ~1 MHz target:
- procedural mesh generation, Wavefront OBJ parsing, or SVG contour flattening/simplification
- normalization and coordinate-system conversion
- face-winding repair
- sampled animation transforms (
spin,recede, or tilted-planecrawl) - perspective projection
- face visibility
- host-side Z-buffer hidden-line clipping
- C64-oriented line-step encoding
- dirty-area and hires screen-colour span generation
The C64 still rasterizes the visible wireframe itself into VIC-II hires bitmap RAM. step, bytechunk, and yunroll are vector/line renderers, not bitmap-frame players.
Built-ins:
torus
cube
sphere
Repository objects:
objects/horse_head.obj
objects/horse_head.json
objects/sunflower_torus.obj
objects/sunflower_torus.mtl
objects/sunflower_torus.json
objects/space_horse.svg
objects/space_horse.json
objects/space_horse_crawl.json
For procedural shapes, use either the actual segmentation or an approximate target:
--major-segments N --minor-segments N # torus
--lat-segments N --lon-segments N # sphere
--polycount N # approximate face count
--vertices N # approximate vertex countFor the torus, major_segments * minor_segments equals both the vertex count and quad-face count. Example:
10 x 5 -> 50 verts, 100 edges, 50 faces
12 x 6 -> 72 verts, 144 edges, 72 faces
14 x 7 -> 98 verts, 196 edges, 98 faces
Higher detail consumes both CPU time and generated table RAM. If a requested mesh no longer fits with 48 orientations, the compiler preserves mesh detail and automatically reduces the orientation-table count unless --strict-frames is used.
./build.sh import-obj path/to/model.obj --as model_name --up yThis creates:
objects/model_name.obj
objects/model_name.json
The JSON sidecar stores object-specific metadata such as:
- display name
- source up-axis (
yorz) - preferred spin axis (
x,y, orz) - initial rotation
- object scale
Then build it with:
./build.sh --object model_name --runList imported/preset objects:
./build.sh list-objectsInspect topology before compiling:
./build.sh inspect --object model_nameThe inspector reports vertices, edges, faces, n-gon mix, boundary edges, non-manifold edges, and isolated vertices.
The parser supports:
vvertices- polygonal
ffaces - positive and negative OBJ indices
v/vt,v//vn, andv/vt/vntokens- triangles, quads, and n-gons
Texture/normal indices are currently ignored. Polygon faces are triangulated internally for visibility/Z-buffer work while polygon boundary edges remain the wireframe edges. Direct mtllib references are preserved by import-obj; usemtl assignments and diffuse Kd colours are read and mapped to the nearest C64 palette entries.
OBJ/MTL and SVG source colours are enabled automatically. Disable them with any of the equivalent flags below; this retains the original monochrome table format and hot renderer loop:
./build.sh --object sunflower_torus --no-color --run
./build.sh --object sunflower_torus --no-colors --run
./build.sh --object sunflower_torus --ignore-colors --runIf an OBJ has no usable mtllib/usemtl/Kd data, or an SVG has no explicit
usable stroke/fill colour, the compiler simply uses the single-colour path
(white by default). It does not enable the per-cell colour machinery. Before
frame generation, the build prints which of those paths it selected and names
the source file it inspected.
--color yellow (or --color 7) forces one monochrome foreground colour and
also bypasses per-material/per-contour mapping.
The toolkit does not yet contain a general mesh decimator. Imported meshes should currently already be reasonably low-poly. Automatic simplification to a requested C64 face/edge budget is on the roadmap; it will be implemented as a real topology-aware stage rather than deleting random faces and pretending that is decimation.
SVG artwork is treated as vector contour geometry rather than as a bitmap. The importer understands common SVG path commands and basic vector primitives, flattens Bezier/arc curves to line segments, simplifies them for the C64 budget, flips SVG Y-down coordinates into the toolkit's Y-up space, and stores the result as explicit wire edges. This avoids inventing filled triangles through concave glyphs or letter holes.
Import and build:
./build.sh import-svg path/to/logo.svg --as logo
./build.sh --object logo --runUseful controls:
--svg-tolerance N contour simplification tolerance in source SVG units
--svg-curve-step N curve sampling step before simplification
--svg-depth N shallow wire extrusion depth; 0 keeps a flat plane
--svg-connector-stride N connect every Nth front/back vertex when extruded
--color NAME|0..15 force one C64 foreground colour
--no-colors ignore source colours; classic white-on-black
--animation spin|recede|crawl
--animation-tilt DEG crawl-plane tilt
--animation-travel N distance travelled away from the camera
--animation-rise N upward travel for crawl mode
import-svg inspects each visible contour's stroke/fill colour and maps it to the nearest C64 palette entry. The bundled SPACE HORSE asset uses #FFE81F, which maps to C64 yellow. Multi-colour SVGs retain distinct contour colours.
Native hires bitmap mode selects foreground/background per 8x8 character cell, not per pixel. The host therefore counts the visible coloured line pixels in each touched cell and assigns the dominant colour when several materials/contours share that cell. It emits horizontal screen-colour spans containing ready-to-store VIC-II colour bytes. RGB parsing and nearest-colour searches never run on the C64. Single-colour sources use the existing global hires foreground byte and therefore need no colour table or runtime update pass.
The bundled examples are:
./build.sh --object space_horse --run # Y-axis spinner
./build.sh --object space_horse_crawl --run # tilted plane -> horizonrecede is also available for the front-facing logo-moving-away effect:
./build.sh --object space_horse --animation recede --runSee docs/SVG_PIPELINE.md for the current parser/geometry details and limitations.
The canonical included object is:
objects/horse_head.obj
Topology:
VERTS: 64
EDGES: 124
FACES: 65
Its metadata declares the source as Z-up and the toolkit converts it to internal Y-up coordinates before compiling:
./build.sh --object horse_head --renderer yunroll --runOn the host-side compiler, the full horse currently exceeds the line-table budget at 48 and 40 orientations, so the compiler automatically selects 36 orientations while preserving all 64 vertices / 65 faces.
Table-RAM messages during auto-fit are informational: the compiler retries with fewer precomputed rotation orientations while keeping the mesh itself intact. It now explicitly prints that vertices/edges/faces are preserved. Use --strict-frames if you would rather fail than auto-reduce the orientation count.
Named objects can define a preferred spin axis in their JSON metadata. Override it from the CLI:
./build.sh --object horse_head --spin-axis x --run
./build.sh --object horse_head --spin-axis y --run
./build.sh --object horse_head --spin-axis z --runInitial pose can be changed with:
--rotate-x DEG --rotate-y DEG --rotate-z DEGThe historical spinner is now one animation mode. Named presets may select another mode, and the CLI can override it:
--animation spin
--animation recede
--animation crawl --animation-tilt 62 --animation-travel 105 --animation-rise 42The v0.7-style packed-step renderer. The host precomputes minor-axis decisions and the 6510 rasterizes the lines pixel-by-pixel. Kept as a regression/benchmark reference.
The v0.8 renderer. Full aligned X-major chunks are combined into VIC-II bitmap-byte masks, reducing repeated bitmap read/modify/write operations. Stable reference path.
Current fastest path. Keeps byte-chunk X-major rendering and additionally unrolls Y-major scanline phases. Measured around 15-18 FPS on the default 10x5 torus in the current development setup.
Generated demos show topology at lower left and live guest-side FPS at lower right:
TORUS V:050 E:100 FPS:017
HORSE HEAD V:064 E:124 FPS:...
V means vertices and E means unique mesh edges. --polycount refers to faces, not vertices.
c64-3d-toolkit/
├── README.md
├── build.sh
├── c643d.py
├── config/
│ └── c643d.ini.example
├── c64/
│ ├── renderer-step.asm
│ ├── renderer-bytechunk.asm
│ └── renderer-yunroll.asm
├── tools/c643d/
│ ├── assets.py
│ ├── cli.py
│ ├── colors.py
│ ├── mesh.py
│ ├── shapes.py
│ ├── objio.py
│ ├── svgio.py
│ ├── toolchain.py
│ ├── pipeline.py
│ ├── emit.py
│ └── font.py
├── objects/
│ ├── README.md
│ ├── horse_head.obj
│ ├── horse_head.json
│ ├── sunflower_torus.obj
│ ├── sunflower_torus.mtl
│ ├── sunflower_torus.json
│ ├── space_horse.svg
│ ├── space_horse.json
│ └── space_horse_crawl.json
├── generated/
├── build/
├── tests/
└── docs/
├── ARCHITECTURE.md
├── CONFIGURATION.md
├── OBJ_PIPELINE.md
├── SVG_PIPELINE.md
├── REFERENCES.md
└── ROADMAP.md
The intended eventual workflow is:
Blender / modeller / vector editor / generated asset
|
v
Wavefront OBJ / SVG
|
v
import + inspect
|
v
normalize / repair / simplify
|
v
preview + C64 cost estimate
|
v
hidden-line / vector compilation
|
v
64tass
|
v
PRG -> VICE / real C64
A graphical host-side importer/previewer is planned, but the command-line path will remain first-class.
Version 0.5.0 adds real per-material OBJ/MTL and per-contour SVG colour propagation into VIC-II hires screen cells. The coloured path uses compact native colour codes and is compile-time isolated; monochrome builds retain the previous geometry tables and hot line loop. The torus remains the performance/reference object, while the sunflower is the bundled multi-material acceptance case.
By FlyingFathead, with ChaosWhisperer lurking somewhere in the machinery.
