Raymarching
Camera Ray:
1# Camera ray setup for raymarching2import numpy as np34def generate_ray(pixel_x, pixel_y, width, height, fov):5 """Convert pixel coordinates to a 3D ray direction."""6 aspect = width / height7 # Normalize pixel to [-1, 1]8 ndc_x = (2.0 * pixel_x / width - 1.0) * aspect9 ndc_y = 1.0 - 2.0 * pixel_y / height1011 # Perspective projection12 import math13 z = -1.0 / math.tan(math.radians(fov) / 2.0)14 direction = np.array([ndc_x, ndc_y, z])15 direction /= np.linalg.norm(direction)16 return direction
Raymarching starts by generating a ray for each pixel. The camera origin and pixel-to-ray mapping define the viewing frustum.
Sphere Tracing:
1# Single sphere tracing step2def sphere_trace_step(origin, direction, sdf_fn, current_t):3 """Advance ray by the SDF distance at current position."""4 position = origin + current_t * direction5 distance = sdf_fn(position)6 # Safe to advance by 'distance' without missing surface7 new_t = current_t + distance8 return new_t, distance
Each step advances the ray by exactly the SDF distance at the current position. This is safe because the SDF guarantees no surface exists within that radius.
Raymarch Loop:
1# Full sphere tracing loop2def raymarch(origin, direction, sdf_fn, max_steps=128, max_dist=100.0, eps=1e-4):3 t = 0.04 for i in range(max_steps):5 pos = origin + t * direction6 d = sdf_fn(pos)7 if d < eps: # Hit surface8 return t, pos, True9 if t > max_dist: # Escaped scene10 return t, pos, False11 t += d12 return t, origin + t * direction, False
The full solver loops until convergence (surface hit) or escape (exceeding max distance or iteration count).
Gradient Normal:
1# Gradient-based normal estimation at hit point2def estimate_normal(sdf_fn, pos, eps=1e-4):3 """Central differences gradient = surface normal."""4 nx = sdf_fn([pos[0]+eps, pos[1], pos[2]]) - sdf_fn([pos[0]-eps, pos[1], pos[2]])5 ny = sdf_fn([pos[0], pos[1]+eps, pos[2]]) - sdf_fn([pos[0], pos[1]-eps, pos[2]])6 nz = sdf_fn([pos[0], pos[1], pos[2]+eps]) - sdf_fn([pos[0], pos[1], pos[2]-eps])7 import math8 length = math.sqrt(nx*nx + ny*ny + nz*nz) or 1.09 return [nx/length, ny/length, nz/length]
After the ray hits a surface, the normal is estimated via the SDF gradient using central finite differences.
Diffuse Shade:
1# Lambertian diffuse shading2def shade_diffuse(normal, light_dir, base_color):3 """N dot L diffuse lighting."""4 import numpy as np5 n_dot_l = max(np.dot(normal, light_dir), 0.0)6 return [c * n_dot_l for c in base_color]78# After computing the hit normal and light direction,9# the diffuse intensity is simply clamp(N . L, 0, 1).
The simplest lighting model uses the dot product between the surface normal and light direction (Lambertian reflectance).
Soft Shadows:
1# Soft shadow via secondary sphere trace2def soft_shadow(origin, light_dir, sdf_fn, k=8.0, max_t=50.0):3 """Raymarch toward light; closest approach gives penumbra."""4 result = 1.05 t = 0.016 while t < max_t:7 d = sdf_fn(origin + t * light_dir)8 if d < 1e-4:9 return 0.0 # Full shadow10 result = min(result, k * d / t)11 t += d12 return clamp(result, 0.0, 1.0)1314# k controls penumbra softness.15# Higher k = sharper shadows, lower k = softer.
Soft shadows trace a secondary ray toward the light source. The closest approach to any surface during the march determines the penumbra softness.
SDF AO:
1# SDF-based ambient occlusion2def ambient_occlusion(pos, normal, sdf_fn, steps=5, step_size=0.1):3 """Sample SDF along normal to estimate occlusion."""4 ao = 0.05 weight = 1.06 for i in range(1, steps + 1):7 sample_pos = pos + normal * (i * step_size)8 expected_dist = i * step_size9 actual_dist = sdf_fn(sample_pos)10 # Difference between expected and actual indicates occlusion11 ao += weight * (expected_dist - actual_dist)12 weight *= 0.5 # Diminishing influence13 return max(1.0 - ao, 0.0)
SDF-based AO samples the field along the surface normal. If actual SDF values are less than expected (indicating nearby geometry), the point is occluded.
Fog:
1# Distance-based fog for atmospheric effects2def apply_fog(color, distance, fog_color, fog_density):3 """Exponential fog blending."""4 import math5 fog_factor = 1.0 - math.exp(-fog_density * distance)6 fog_factor = max(0.0, min(1.0, fog_factor))7 # Blend between object color and fog color8 result = [c * (1 - fog_factor) + f * fog_factor9 for c, f in zip(color, fog_color)]10 return result1112# Fog uses the ray's travel distance (t parameter)13# to blend toward a background fog color.
Distance-based exponential fog blends the surface color toward a fog color based on how far the ray traveled before hitting the surface.
Related Concepts
Signed ray crossings, Even-Odd parity, Non-Zero, Positive, and Negative polygon containment rules.
Binary Space Partitioning, polygon slicing, and CSG boundary classification rules.
Implicit geometry and mathematical shapes.