Files
zzg 4dddb62958
ci / test (push) Canceled after 0s
initial commit
2026-09-25 21:31:54 -07:00

3.1 KiB

tracer

A path tracer. Ray tracing renders an image by following the path of light as pixels in an image plane and simulating its encounters with virtual objects.

Supports spheres, planes and triangles; Lambertian, metal, dielectric and emissive materials; a positionable camera with depth of field; and a BVH to keep large scenes tractable. Rendering is parallel across CPUs and reproducible from a seed.

Install

go install github.com/zg/tracer/cmd/tracer@latest

Use

tracer -w 1200 -h 800 -samples 256 -o render.png
tracer -scene cornell -samples 512 -o cornell.png
flag meaning
-w, -h image dimensions
-samples rays per pixel; noise falls as 1/√samples
-depth maximum bounces per ray
-scene spheres or cornell
-seed same seed ⇒ same image
-workers render goroutines (0 = one per CPU)

.ppm output is chosen by the file extension; anything else writes PNG. Ctrl-C stops the render and still writes the partial image.

Library

world := scene.NewBVH([]scene.Surface{
    scene.Sphere{Center: vec.New(0, 0, -2), Radius: 0.5,
        Material: scene.Lambertian{Albedo: vec.New(0.7, 0.3, 0.3)}},
})
cam := scene.NewCamera(scene.CameraOptions{
    LookFrom: vec.New(0, 0, 0), LookAt: vec.New(0, 0, -1), AspectRatio: 16.0 / 9.0,
})
img, err := render.Render(ctx, world, cam, render.Options{Width: 800, Height: 450, Samples: 128})

Design notes

Renders are reproducible. Each row is seeded from its own index rather than sharing one generator. Sharing would serialise every worker on a mutex; seeding from the clock would make output vary run to run. Because the seed follows the row rather than the worker, the image is identical no matter how rows are distributed — a property the tests check by rendering at 1, 2, 4 and 8 workers and comparing bytes.

Rays start slightly off the surface. A scattered ray begins exactly on the surface it just hit, and floating point error lets it immediately re-hit the same point. Tracing from t=1e-3 rather than t=0 is what prevents the resulting "shadow acne".

Light accumulation is iterative. Rather than recursing per bounce, rayColour carries a running throughput product. Equivalent result, but the bounce limit cannot overflow the stack.

Degenerate cases are handled explicitly. Normalising a zero vector returns zero rather than NaN; a diffuse scatter direction that exactly cancels the normal falls back to the normal; metal fuzz that pushes a reflection below the surface absorbs the ray instead of letting it escape through the object; and the AABB slab test skips NaN intervals produced by rays lying exactly on a slab boundary. Each of these is a silent image-corrupting bug if missed, so each has a test.

The BVH is verified against brute force. An acceleration structure is only correct if it returns exactly what testing every surface would. The suite builds a 200-sphere scene and compares 5,000 rays against a linear scan, asserting both the hit/miss decision and the exact t.

Tests

go test -race ./...
go test -bench=. ./...