145 lines
8.0 KiB
Markdown
145 lines
8.0 KiB
Markdown
# ASCII 3D Renderer — Technical Documentation
|
||
|
||
## Overview
|
||
|
||
This is a software 3D renderer that outputs to a terminal. It takes text input, turns each character into a 3D shape, lights it with a multi-light Blinn-Phong model, and draws the result using ASCII characters (or colored output via ANSI escape sequences). Everything runs in a single-threaded loop at 60 FPS.
|
||
|
||
There are no GPU calls, no windowing system, and no third-party rendering libraries. The only external dependency beyond `libc` is `libm` for math functions (`powf`, `fmaxf`, `sqrtf`, etc.).
|
||
|
||
## How the rendering works
|
||
|
||
The pipeline, roughly, goes like this:
|
||
|
||
1. Look up the glyph bitmap for each character (from `font.c`)
|
||
2. Extrude the 2D bitmap into a 3D volume by adding depth along the Z-axis
|
||
3. Step through the volume at regular intervals (`VOXEL_STEP = 0.15`) and, at each point, check whether we're "inside" geometry
|
||
4. Calculate surface normals by looking at which neighboring cells are empty — a filled voxel next to an empty one means there's a surface edge there, and the normal points toward the gap
|
||
5. Apply rotation matrices (one per axis) based on the current angle
|
||
6. Project the 3D points into 2D screen coordinates using perspective projection (field of view, near/far planes)
|
||
7. For each projected point, run it through the lighting model to get a brightness value
|
||
8. Map that brightness to a character from the current shade palette
|
||
9. Write it to the framebuffer (a 2D array of chars), respecting the Z-buffer so closer surfaces win
|
||
10. Dump the entire framebuffer to stdout in one pass
|
||
|
||
When anti-aliasing is enabled, steps 3–8 are repeated with jittered sample offsets (2×2 pattern by default), and the results are averaged.
|
||
|
||
## Project structure
|
||
|
||
```bash
|
||
src/
|
||
main.c — CLI parsing, signal handling, main loop
|
||
renderer.c — the core rendering engine: projection, voxel traversal,
|
||
framebuffer management, screen output
|
||
lighting.c — Blinn-Phong lighting: diffuse, specular, multi-light
|
||
accumulation, color math
|
||
font.c — 5×7 bitmap font data for A-Z and 0-9, stored as
|
||
bitflag arrays
|
||
tui.c — terminal input handling: puts the TTY into raw mode
|
||
for non-blocking reads, processes keypresses
|
||
timing.c — frame rate limiter using CLOCK_MONOTONIC + nanosleep
|
||
vec3.c — basic 3D vector math (dot, cross, normalize, rotate)
|
||
|
||
include/
|
||
config.h — all the tuneable constants in one place: viewport size,
|
||
camera FOV, lighting params, shade palettes, etc.
|
||
(+ headers for each .c file)
|
||
```
|
||
|
||
## Module details
|
||
|
||
### `font.c` — Bitmap font
|
||
|
||
Each character is a 5-wide, 7-tall bitmap stored as 7 bytes. Each byte represents one row — bits 4 down to 0 tell you which pixels are filled. For example, the letter "A" is:
|
||
|
||
```bash
|
||
0x0E → .###.
|
||
0x11 → #...#
|
||
0x11 → #...#
|
||
0x1F → #####
|
||
0x11 → #...#
|
||
0x11 → #...#
|
||
0x11 → #...#
|
||
```
|
||
|
||
Only A–Z (case-insensitive) and 0–9 are supported. Anything else gets skipped.
|
||
|
||
### `renderer.c` — 3D engine
|
||
|
||
This is the biggest file and does most of the heavy lifting.
|
||
|
||
**Glyph extrusion:** Each 5×7 bitmap gets depth. The renderer walks through the glyph volume at small Z increments. At each (x, y, z) point, if the corresponding 2D pixel is "on" in the bitmap, that point is considered solid geometry.
|
||
|
||
**Normal estimation:** For shading to work, you need surface normals. Since we're working with blocky voxel-like shapes, the normals are estimated by checking adjacent cells. If a filled cell has an empty neighbor to the left, the normal has a leftward component. Same for all six directions. The result is normalized to get a unit vector. This gives you smooth-ish shading on what are otherwise cube-shaped surfaces.
|
||
|
||
**Projection:** Standard perspective projection. A 3D point gets divided by its Z distance (scaled by field-of-view), then offset to the center of the screen. Points behind the near plane or past the far plane are clipped.
|
||
|
||
**Z-buffering:** The renderer keeps a depth buffer (initialized to a large value). When drawing a point, it only updates the framebuffer if the new point is closer than what was already there. This keeps front surfaces in front.
|
||
|
||
**Render modes:**
|
||
|
||
- *Shaded* — full lighting + ASCII shade mapping (the default)
|
||
- *Solid* — everything gets drawn with `@`, no lighting
|
||
- *Wireframe* — only draw points that sit on the boundary between filled and empty cells
|
||
- *Points* — draw all filled voxels with `.`
|
||
|
||
### `lighting.c` — Blinn-Phong model
|
||
|
||
The lighting system supports up to 3 lights (configurable via `MAX_LIGHTS`). By default it sets up a classic 3-point rig:
|
||
|
||
- **Key light** — the main light, slightly above and to the right, warm white
|
||
- **Fill light** — dimmer, coming from the left, slightly blue-tinted, softens shadows
|
||
- **Rim light** — behind and below the subject, adds edge definition
|
||
|
||
Each light can be directional (parallel rays, like the sun) or point (with inverse-square falloff).
|
||
|
||
For each surface point, the shader calculates:
|
||
|
||
- **Diffuse** — Lambert's cosine law: brightness depends on the angle between the surface normal and the light direction
|
||
- **Specular** — Blinn-Phong half-vector method: creates shiny highlights where the surface reflects light toward the camera
|
||
|
||
The final brightness is the sum of ambient + all light contributions, clamped to [0, 1]. In monochrome mode this maps to an index into the shade character palette. In color mode, the full RGB result is computed per-channel and output using ANSI escape sequences.
|
||
|
||
Material properties (ambient/diffuse/specular colors, shininess) are set to sensible defaults — there's no way to change them at runtime yet.
|
||
|
||
### `tui.c` — Terminal input
|
||
|
||
On startup, the terminal gets switched to non-canonical mode with echo disabled (via `termios`). This means keypresses are available immediately without waiting for Enter, and typed characters don't show up on screen.
|
||
|
||
The original terminal settings are saved and restored on exit — this is important because if the program crashes without restoring them, you'd end up with a terminal that doesn't echo your typing or respond to Ctrl+C properly. Signal handlers for SIGINT and SIGTERM are set up specifically to make sure cleanup happens even on interrupt.
|
||
|
||
### `timing.c` — Frame limiter
|
||
|
||
Pretty straightforward. After each frame, it checks how much time has passed since the frame started using `clock_gettime(CLOCK_MONOTONIC)`. If there's time remaining in the frame budget (1/60th of a second), it sleeps for the difference with `nanosleep`. This keeps the animation speed consistent regardless of how fast the machine is.
|
||
|
||
## Build system
|
||
|
||
The Makefile has three build profiles:
|
||
|
||
| Target | Flags | Purpose |
|
||
|--------|-------|---------|
|
||
| `release` | `-O3 -march=native -flto -DNDEBUG` | Production — fast as possible |
|
||
| `debug` | `-O0 -g3 -fsanitize=address,undefined` | Development — catches memory bugs and UB |
|
||
| `profile` | `-O2 -g -pg` | Profiling with gprof |
|
||
|
||
Dependency tracking is handled with `-MMD -MP`, so incremental builds work correctly.
|
||
|
||
The `install` target copies the binary to `/usr/local/bin` (or wherever `PREFIX` points).
|
||
|
||
## Configuration
|
||
|
||
Everything tuneable lives in `include/config.h`. Some notable values:
|
||
|
||
| Constant | Default | What it controls |
|
||
|----------|---------|-----------------|
|
||
| `SCREEN_WIDTH` / `SCREEN_HEIGHT` | 120 × 45 | Framebuffer size in characters |
|
||
| `CAMERA_DISTANCE` | 30.0 | How far back the virtual camera sits |
|
||
| `FIELD_OF_VIEW` | 50.0 | Perspective FOV in degrees |
|
||
| `EXTRUSION_DEPTH` | 4.0 | How thick the 3D letters are |
|
||
| `VOXEL_STEP` | 0.15 | Sampling resolution along Z when building geometry |
|
||
| `AA_SAMPLES` | 2 | Anti-aliasing grid (2 = 2×2 = 4 samples per pixel) |
|
||
| `AMBIENT_INTENSITY` | 0.15 | Base light level in shadows |
|
||
| `DIFFUSE_INTENSITY` | 0.70 | Strength of the diffuse lighting term |
|
||
| `SPECULAR_INTENSITY` | 0.40 | Strength of specular highlights |
|
||
| `SPECULAR_POWER` | 32.0 | Shininess exponent (higher = tighter highlights) |
|
||
| `TARGET_FPS` | 60 | Frame rate cap |
|