Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

c64-3d-toolkit — Build modern 3D. Fit it in 64K.

c64-3d-toolkit

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.

Requirements

Requires:

  • VICE — Commodore 64 emulator; the toolkit uses x64sc by 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 64tass

Verify that the required tools are available with:

./build.sh doctor

Toolchain configuration and macOS/Windows paths

The 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.ini

config/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.exe

On macOS, the easiest command-line installation is typically:

brew install tass64 vice

For 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 --run

See docs/CONFIGURATION.md for precedence, per-platform sections, C643D_CONFIG, --config/--no-config, and macOS package details.

Quickstart

Build and run one of the included procedural objects:

./build.sh --shape torus --run

Build and run an imported OBJ preset:

./build.sh --object horse_head --run

or:

./build.sh --object sunflower_torus --run

The 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 --run

The 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 --run

Build all included example .prg files:

./build.sh --generate-examples

Build + run the reference torus with the current fastest renderer:

./build.sh --shape torus --run

Make 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 --run

Run the included low-poly horse head or sunflower:

./build.sh --object horse_head --run
./build.sh --object sunflower_torus --run

The 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 --run

Import 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 --run

Or compile a one-off OBJ without importing it:

./build.sh --obj ~/models/my_ship.obj --obj-up z --name MY_SHIP --run

SVG 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 --run

Animation 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 --run

Renderer 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 path

Useful inspection commands:

./build.sh inspect --shape torus --vertices 72
./build.sh inspect --object horse_head
./build.sh list-shapes
./build.sh list-objects

Horse-head visibility note

The 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 --run

The 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.

Examples

The repository has an examples/ manifest. Build all reference PRGs at once:

./build.sh --generate-examples
# equivalent:
./build.sh generate-examples

This 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/.

Dependency checks

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 doctor

On 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.

Current state

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.

What happens on the host vs. the C64?

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-plane crawl)
  • 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.

Shapes and topology

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 count

For 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.

OBJ pipeline

Import a model into objects/

./build.sh import-obj path/to/model.obj --as model_name --up y

This creates:

objects/model_name.obj
objects/model_name.json

The JSON sidecar stores object-specific metadata such as:

  • display name
  • source up-axis (y or z)
  • preferred spin axis (x, y, or z)
  • initial rotation
  • object scale

Then build it with:

./build.sh --object model_name --run

List imported/preset objects:

./build.sh list-objects

Inspect topology before compiling:

./build.sh inspect --object model_name

The inspector reports vertices, edges, faces, n-gon mix, boundary edges, non-manifold edges, and isolated vertices.

Current OBJ support

The parser supports:

  • v vertices
  • polygonal f faces
  • positive and negative OBJ indices
  • v/vt, v//vn, and v/vt/vn tokens
  • 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 --run

If 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.

Current limitation

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 pipeline

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 --run

Useful 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 -> horizon

recede is also available for the front-facing logo-moving-away effect:

./build.sh --object space_horse --animation recede --run

See docs/SVG_PIPELINE.md for the current parser/geometry details and limitations.

Horse head

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 --run

On 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.

Spin axis and pose

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 --run

Initial pose can be changed with:

--rotate-x DEG --rotate-y DEG --rotate-z DEG

The 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 42

Renderers

step

The 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.

bytechunk

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.

yunroll

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.

HUD

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.

Repository layout

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

Roadmap

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.

Status

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.

Credits

By FlyingFathead, with ChaosWhisperer lurking somewhere in the machinery.

About

Host-assisted low-poly wireframe 3D compiler/runtime for the Commodore 64, with OBJ/MTL+SVG import and precomputed hidden-line visibility.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages