Use SDF grids and surfels
Trace rays against signed-distance grids and Gaussian surfels, composite surfel opacity, and mix meshes, SDFs, and surfels in one MixedScene.
Besides triangle meshes, RayD traces two kinds of implicit geometry: signed-distance grids (SDFs), sampled on a regular 3D grid, and surfels, flat Gaussian discs with an opacity. Both are differentiable, so you can optimize an SDF or a surfel cloud through ray queries. MixedScene combines them with meshes.
Signed-distance grids#
An SdfGrid is a box of nx * ny * nz distance samples. The values are vertex-centered and negative inside the surface, and they are distances in world units. The box is centered at position, rotated by the scalar-first quaternion rotation = (qw, qx, qy, qz), and has full side lengths scale.
The Dr.Jit grid takes a flat Float in x-major order: sample (i, j, k) is at index (i * ny + j) * nz + k.
import math
import rayd.drjit as rd
from drjit.cuda import Array3f, Array3i, Float
n = 24
lin = [-1.0 + 2.0 * k / (n - 1) for k in range(n)]
values = [math.sqrt(x * x + y * y + z * z) - 0.5 for x in lin for y in lin for z in lin]
sphere = rd.SdfGrid(Float(values), n, n, n,
Array3f(0.0, 0.0, 0.0), # position
Float([1.0, 0.0, 0.0, 0.0]), # rotation (qw, qx, qy, qz)
Array3f(2.0, 2.0, 2.0)) # scale: a 2 x 2 x 2 box
hit = sphere.intersect(rd.Ray(Array3f([0.0], [0.0], [-2.0]), Array3f([0.0], [0.0], [1.0])))
print(hit.t, hit.hit_mask, hit.steps) # [1.50375] [True] [5]
print(hit.normal) # about [[0, 0, -1]]The SDF is traced by sphere tracing, with trilinear interpolation between samples. SdfTraceOptions controls it: max_steps (default 64), relaxation (default 0.9, the fraction of the distance to step), and eps_hit (the hit threshold; the default derives it from the grid). Besides intersect, a grid answers visible(start, end) and trace_reflections(ray, max_bounces).
The hit distance and position are differentiable with respect to the SDF values, the placement, and the ray. Make values an AD array (drjit.cuda.ad.Float) or a tensor with requires_grad=True.
Surfels#
A surfel is a flat Gaussian disc with a center, two tangent axes tangent_u and tangent_v that set its orientation and extent, an opacity, and one scalar value. Build a SurfelCloud, wrap it in a SurfelScene, and call build(). RayD encloses each surfel in a proxy shape (an icosahedron by default, so triangle_count is 20 per surfel) for hardware ray tracing.
cloud = rd.SurfelCloud(
Array3f([0.0, 0.0], [0.0, 0.0], [0.0, -0.5]), # centers
Array3f([1.0, 1.0], [0.0, 0.0], [0.0, 0.0]), # tangent_u
Array3f([0.0, 0.0], [1.0, 1.0], [0.0, 0.0]), # tangent_v
Float([0.6, 0.6])) # opacity
surfels = rd.SurfelScene(cloud)
surfels.build()
print(surfels.surfel_count, surfels.triangle_count) # 2 40
ray = rd.Ray(Array3f([0.0], [0.0], [1.0]), Array3f([0.0], [0.0], [-1.0]))
comp = surfels.composite_alpha(ray)
print(comp.alpha, comp.transmittance) # [0.84] [0.16]: 1 - 0.4 * 0.4intersect returns the first surfel hit, with t, local_uv, gaussian_weight, alpha, and surfel_id. composite_alpha blends every surfel along the ray front to back and returns the accumulated alpha, transmittance, and depth. visible and shadow_test treat the cloud as an occluder. SurfelTraceOptions sets the opacity cutoff (alpha_min, default 1/255), the per-hit cap (alpha_cap, 0.99), the early stop (transmittance_min, 0.03), and the proxy shape (primitive_mode).
Render colors and features (Dr.Jit)#
For appearance fitting, split geometry and appearance: build the scene from a SurfelGeometry, attach a SurfelAppearance, and call render.
geo = rd.SurfelGeometry(Array3f([0.0, 0.0], [0.0, 0.0], [0.0, -0.5]),
Array3f([1.0, 1.0], [0.0, 0.0], [0.0, 0.0]),
Array3f([0.0, 0.0], [1.0, 1.0], [0.0, 0.0]))
scene = rd.SurfelScene(geo)
scene.update_appearance(rd.SurfelAppearance.rgb(Float([0.6, 0.6]),
Array3f([1.0, 0.0], [0.0, 0.0], [0.0, 1.0])))
scene.build()
img = scene.render(ray, rd.SurfelRenderOptions.rgb())
print(img.rgb, img.alpha) # [[0.6, 0, 0.24]] [0.84]SurfelAppearance.sh(opacity, coeffs, sh_degree) uses spherical-harmonic colors, and SurfelAppearance.features(opacity, values, channel_count) renders arbitrary feature channels with SurfelRenderOptions.feature(channel_count). update_geometry and update_appearance replace either half on a built scene.
In Torch, SurfelScene offers intersect, visible, trace_reflections, composite_alpha, and transmittance; rendering is Dr.Jit only.
Mixed scenes#
MixedScene puts meshes, SDF grids, and surfel clouds into one scene. A query returns the nearest hit over all of them.
mixed = rd.MixedScene()
mixed.add_mesh(rd.Mesh(Array3f([-1, 1, 1, -1], [-1, -1, 1, 1], [-1.0] * 4),
Array3i([0, 0], [1, 2], [2, 3])))
mixed.add_sdf(sphere)
mixed.add_surfel(rd.SurfelCloud(Array3f([0.0], [0.0], [1.5]), Array3f([0.3], [0.0], [0.0]),
Array3f([0.0], [0.3], [0.0]), Float([0.8])))
mixed.build()
rays = rd.Ray(Array3f([0.0, 0.9], [0.0, 0.9], [3.0, 3.0]), Array3f([0.0, 0.0], [0.0, 0.0], [-1.0, -1.0]))
hit = mixed.intersect(rays)
print(hit.t, hit.shape_id) # [1.5, 4] [2, 0]: the surfel, then the floor
print(mixed.transmittance(rays)) # [0, 0]MixedScene answers intersect, visible, trace_reflections, and transmittance. It has no edge or diffraction queries: SDFs and surfels have no edges. The Torch MixedScene also has add_sdf_batch.