Examples | PY

Raycast Rendering

Render a mesh image using batch ray casting, face normals, and matplotlib.

Source: raycast_render.py

Ray-traced rendering of a mesh using an orthographic camera. Generates a grid of rays (one per pixel), batch ray_casts them against the mesh in a single call, and renders Lambertian shading from face normals.

python raycast_render.py [mesh.stl] [--resolution 512]

Features Showcased

  • Batch tf.Ray construction from numpy arrays
  • Batch tf.ray_cast returning (element_ids, t_values) per ray
  • Looking up mesh.normals by hit face ID
  • Two-light Lambertian shading with rim lighting

Loading the Mesh

load.py
import numpy as np
import trueform as tf

faces, points = tf.read_stl("dragon.stl")
mesh = tf.Mesh(faces, points)
mesh.build_tree()

Setting Up the Camera

An orthographic camera looking down the -Z axis. The ray grid spans the mesh bounding box with 10% padding.

camera.py
aabb_min = points.min(axis=0)
aabb_max = points.max(axis=0)
size = aabb_max - aabb_min
pad = size.max() * 0.1
res = 512

x = np.linspace(aabb_min[0] - pad, aabb_max[0] + pad, res).astype(points.dtype)
y = np.linspace(aabb_max[1] + pad, aabb_min[1] - pad, res).astype(points.dtype)
gx, gy = np.meshgrid(x, y)
n_rays = res * res

origins = np.zeros((n_rays, 3), dtype=points.dtype)
origins[:, 0] = gx.ravel()
origins[:, 1] = gy.ravel()
origins[:, 2] = aabb_max[2] + pad

directions = np.zeros((n_rays, 3), dtype=points.dtype)
directions[:, 2] = -1.0

Batch Ray Casting

Construct a batch of rays and cast them all against the mesh in a single call. The result is a tuple of (element_ids, t_values) — one entry per ray. Misses have -1 for the element ID and NaN for t.

raycast.py
rays = tf.Ray(origin=origins, direction=directions)

ids, ts = tf.ray_cast(rays, mesh)

ids = ids.reshape(res, res)
ts = ts.reshape(res, res)
hit = ~np.isnan(ts)
All 262,144 rays (512x512) are cast in a single tf.ray_cast call. The C++ backend parallelizes automatically for batches above 1,000 rays.

Shading

Look up face normals by hit element ID. Two directional lights (key + fill) provide Lambertian diffuse shading, and a Fresnel-like rim term adds bright edges where the surface is nearly perpendicular to the camera.

shading.py
normals = mesh.normals
hit_ids = ids[hit]
hit_normals = normals[hit_ids].copy()

# Flip back-facing normals
hit_normals[hit_normals[:, 2] > 0] *= -1

# Key light + fill light
key_dir = np.array([0.4, 0.6, -0.7], dtype=np.float32)
key_dir /= np.linalg.norm(key_dir)
fill_dir = np.array([-0.5, -0.3, -0.6], dtype=np.float32)
fill_dir /= np.linalg.norm(fill_dir)

key = np.clip(hit_normals @ key_dir, 0.0, 1.0)
fill = np.clip(hit_normals @ fill_dir, 0.0, 1.0)
diffuse = 0.08 + 0.72 * key + 0.20 * fill

# Rim lighting
view_dir = np.array([0.0, 0.0, -1.0], dtype=np.float32)
facing = np.abs(hit_normals @ view_dir)
rim = (1.0 - facing) ** 3
intensity = diffuse + 0.35 * rim

Compositing the Image

Map intensity to a teal color ramp on a dark background and display with matplotlib.

display.py
import matplotlib.pyplot as plt

BG = np.array([27, 43, 52], dtype=np.float32) / 255
TEAL = np.array([0.0, 0.659, 0.604])
TEAL_BRIGHT = np.array([0.0, 0.835, 0.745])

image = np.empty((res, res, 3), dtype=np.float32)
image[:] = BG

color = TEAL + np.clip(intensity, 0, 1)[:, np.newaxis] * (TEAL_BRIGHT - TEAL)
image[hit] = color * intensity[:, np.newaxis]
image = np.clip(image, 0, 1)

fig, ax = plt.subplots(1, 1, figsize=(8, 8), facecolor=BG)
ax.imshow(image)
ax.set_axis_off()
fig.patch.set_facecolor(BG)
plt.show()

Batch Ray Cast Return Values

RaysTargetReturnsMisses
singleprimitivefloat (t) or NoneNone
singleform(element_id, t) or NoneNone
batch(N)primitivendarray(N,)NaN entries
batch(N)form(ndarray[N] ids, ndarray[N] ts)-1 / NaN entries
Single ray cast against a form returns (element_id, t). The batch version returns the same fields as parallel arrays, with -1 and NaN for misses. The config parameter accepts per-ray arrays (min_ts, max_ts) for variable-depth queries — useful for progressive nearest-hit across multiple meshes.