Raymarching
Raymarching starts by generating a ray for each pixel. The camera origin and pixel-to-ray mapping define the viewing frustum.
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
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.
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
The full solver loops until convergence (surface hit) or escape (exceeding max distance or iteration count).
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
After the ray hits a surface, the normal is estimated via the SDF gradient using central finite differences.
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]
The simplest lighting model uses the dot product between the surface normal and light direction (Lambertian reflectance).
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).
Soft shadows trace a secondary ray toward the light source. The closest approach to any surface during the march determines the penumbra softness.
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.
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.
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)
Distance-based exponential fog blends the surface color toward a fog color based on how far the ray traveled before hitting the surface.
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.