Implementing Custom Shape Collision Detection with SDF Functions in Newton
Newton physics engine uses Sparse-Texture Signed Distance Fields (SDFs) to enable GPU-accelerated collision detection for arbitrary custom shapes by storing high-resolution distance data in CUDA textures and sampling them via Warp kernels.
Newton is an open-source physics simulation framework that leverages Sparse-Texture SDFs as its core representation for hydroelastic and high-resolution mesh collision detection. Implementing custom shape collision detection with SDF functions in Newton involves converting arbitrary geometry into a two-level texture representation that enables sub-millisecond distance queries on the GPU through the Warp kernel system.
Understanding Newton's Sparse-Texture SDF Architecture
Newton represents custom shapes using a two-level sparse texture stored in CUDA memory. This architecture separates the signed distance field into:
- Coarse texture: A low-resolution grid (typically 8³ voxels) covering the entire bounding domain.
- Sub-grid texture: High-resolution data stored only for occupied tiles, dramatically reducing memory consumption for sparse geometries.
The indirection array maps coarse cells to sub-grid blocks, allowing the sampling kernels to reconstruct continuous distance values via manual trilinear interpolation. This approach avoids CUDA's 8-bit weight limitation and supports uint16 and uint8 quantization modes for memory efficiency.
Building a Texture-Based SDF from a Custom Mesh
To create a collision-ready SDF for a custom shape, use the create_texture_sdf_from_mesh function in newton/_src/geometry/sdf_texture.py. This function requires a Warp mesh with winding numbers enabled to correctly handle inside/outside classification.
import warp as wp
from newton.geometry import sdf_texture
# Create a Warp mesh with winding number support
mesh = wp.Mesh(
points=wp.array(verts, dtype=wp.vec3),
indices=wp.array(faces, dtype=wp.int32),
support_winding_number=True
)
# Build the sparse texture SDF
tex_sdf, coarse_tex, subgrid_tex, block_coords = sdf_texture.create_texture_sdf_from_mesh(
mesh,
margin=0.05, # Extra AABB padding (meters)
narrow_band_range=(-0.1, 0.1), # Distance range for high-res storage
max_resolution=64, # Coarse grid resolution (must be multiple of 8)
subgrid_size=8, # Cells per sub-grid block
quantization_mode=sdf_texture.QuantizationMode.UINT16,
winding_threshold=0.5,
)
The function returns a TextureSDFData struct (tex_sdf) that serves as a handle for all subsequent sampling operations. The coarse and sub-grid textures remain allocated in GPU memory as long as this handle is referenced.
Sampling Distances and Gradients
Once the SDF is built, query signed distances and surface normals using the sampling functions in newton/_src/geometry/sdf_texture.py. These operations execute entirely on the GPU via Warp kernels.
Distance Queries
Use texture_sample_sdf for single-point queries or launch the _sample_texture_sdf_kernel for batch processing:
from newton.geometry import sdf_texture
# Batch query on GPU
query_pts = wp.array(points, dtype=wp.vec3) # Shape (N, 3)
distances = wp.zeros(len(query_pts), dtype=wp.float32)
wp.launch(
sdf_texture._sample_texture_sdf_kernel,
dim=len(query_pts),
inputs=[tex_sdf, query_pts, distances],
device="cuda",
)
The sampling kernel performs:
- Domain clamping to the SDF bounds
- Coarse cell lookup via the indirection array
- Manual trilinear interpolation of sub-grid values
- De-quantization for uint16/uint8 storage formats
Gradient Queries
For surface normals and penetration directions, use texture_sample_sdf_grad:
# Single-point query with gradient
dist, grad = sdf_texture.texture_sample_sdf_grad(tex_sdf, wp.vec3(0.1, 0.2, 0.3))
The gradient vector grad represents the partial derivatives ∂d/∂x, ∂d/∂y, ∂d/∂z and points in the direction of increasing distance (outward for exterior points).
Integrating with the Collision Pipeline
Newton's collision system consumes SDF samples in two primary contexts: hydroelastic contact and general rigid-body contact.
Hydroelastic Contact
The hydroelastic solver in newton/_src/geometry/sdf_hydroelastic.py uses voxel-exact SDF queries to compute penetration depths:
from newton.geometry import sdf_hydroelastic as hydro
# Query at specific voxel coordinates
depth = hydro.texture_sample_sdf_at_voxel(tex_sdf, ix, iy, iz) - thickness
This pattern appears in the marching-cubes surface extraction and pressure field computation, where exact voxel alignment avoids interpolation artifacts.
General Shape-Shape Contact
For narrow-phase collision detection, newton/_src/geometry/sdf_contact.py samples the SDF at contact candidate points:
# Pattern from sdf_contact.py (lines 498-510)
dist = texture_sample_sdf(texture_sdf, p)
if dist < 0.0: # Penetration detected
_, normal = texture_sample_sdf_grad(texture_sdf, p)
# Build contact manifold with point, normal, and depth
The CollisionPipeline automatically invokes these routines when custom SDF shapes participate in collisions, requiring no additional user code beyond the initial SDF construction.
Complete Example – Procedural Torus
The following self-contained example creates a torus mesh, builds a texture SDF, and queries distances and gradients:
import numpy as np
import warp as wp
from newton.geometry import sdf_texture, sdf_hydroelastic
def make_torus(R=0.5, r=0.1, segments=64, rings=32):
"""Generate a procedural torus centered at origin with Z-axis."""
verts = []
faces = []
for i in range(segments):
theta = 2 * np.pi * i / segments
for j in range(rings):
phi = 2 * np.pi * j / rings
x = (R + r * np.cos(phi)) * np.cos(theta)
y = (R + r * np.cos(phi)) * np.sin(theta)
z = r * np.sin(phi)
verts.append([x, y, z])
verts = np.array(verts, dtype=np.float32)
for i in range(segments):
for j in range(rings):
a = i * rings + j
b = ((i + 1) % segments) * rings + j
c = i * rings + (j + 1) % rings
d = ((i + 1) % segments) * rings + (j + 1) % rings
faces.append([a, b, c])
faces.append([b, d, c])
faces = np.array(faces, dtype=np.int32)
return verts, faces
# Build the mesh
verts, faces = make_torus()
mesh = wp.Mesh(
points=wp.array(verts, dtype=wp.vec3),
indices=wp.array(faces, dtype=wp.int32),
support_winding_number=True
)
# Create the texture SDF
tex_sdf, _, _, _ = sdf_texture.create_texture_sdf_from_mesh(
mesh,
margin=0.02,
narrow_band_range=(-0.05, 0.05),
max_resolution=128,
subgrid_size=8,
quantization_mode=sdf_texture.QuantizationMode.UINT16,
)
# Query a point inside the torus hole
p = wp.vec3(0.0, 0.0, 0.0)
dist, grad = sdf_texture.texture_sample_sdf_grad(tex_sdf, p)
print(f"Signed distance: {dist:.4f}") # ≈ +0.4 (outside solid)
print(f"Gradient: {grad}") # Points outward
# Hydroelastic depth query at specific voxel
depth = sdf_hydroelastic.texture_sample_sdf_at_voxel(tex_sdf, 10, 10, 10) - 0.01
print(f"Depth at voxel (10,10,10): {depth:.4f}")
Executing this script with CUDA ≥ 11 and Warp installed produces a signed distance of approximately 0.4 m (indicating the query point lies outside the solid torus) and a gradient vector pointing radially outward, confirming the custom shape integrates seamlessly with Newton's collision system.
Testing and Validation
Newton includes comprehensive tests to validate SDF construction and sampling accuracy. The primary test suites are located in:
newton/tests/test_sdf_texture.py: Validates texture SDF construction, quantization modes (uint16/uint8), gradient accuracy, and isosurface extraction.newton/tests/test_sdf_compute.py: Tests SDF generation from primitive shapes and verifies narrow-phase collision results against expected penetration depths.
Run the specific texture SDF tests using:
uv run --extra dev -m newton.tests -k test_texture_sdf
These tests ensure that custom shapes produce distance values accurate to within quantization error (typically < 0.1 mm for uint16 mode) and that gradients remain unit-length within the narrow band.
Summary
Newton enables custom shape collision detection with SDF functions through a GPU-accelerated sparse texture architecture that balances memory efficiency with high-resolution accuracy.
- Build custom shape SDFs using
create_texture_sdf_from_meshinnewton/_src/geometry/sdf_texture.py, which generates coarse and sub-grid CUDA textures with configurable quantization. - Sample signed distances and surface normals via
texture_sample_sdfandtexture_sample_sdf_grad, performing manual trilinear interpolation and de-quantization entirely on the GPU. - Integrate sampled values into Newton's collision pipelines by feeding distances and gradients into
sdf_hydroelastic.pyfor soft-body contacts orsdf_contact.pyfor rigid-body narrow-phase detection.
Because the heavy computation occurs inside Warp kernels, custom SDF shapes achieve sub-millisecond query performance identical to built-in primitives.
Frequently Asked Questions
What quantization mode should I use for custom shape SDFs?
Use QuantizationMode.UINT16 for general-purpose collision detection, as it provides a good balance between memory footprint (2 bytes per voxel) and precision (typically sub-millimeter accuracy). Use QuantizationMode.FLOAT32 only when you require exact distances for high-precision engineering simulations or when the narrow band exceeds the representable range of 16-bit integers. The sdf_texture.py module handles de-quantization automatically during sampling.
How do I update an SDF when my mesh deforms at runtime?
You must rebuild the texture SDF from the updated mesh vertices. Unlike analytical primitives, sparse-texture SDFs are precomputed volumetric data. For deforming meshes, call create_texture_sdf_from_mesh each frame with the new vertex positions, or maintain a pool of precomputed SDFs for rigid sub-components. The reconstruction cost is typically 1-5 ms for 128³ resolution meshes on modern GPUs, making it suitable for moderately complex deformable objects.
Can I use custom SDF shapes with Newton's hydroelastic contact model?
Yes, custom SDF shapes integrate directly with the hydroelastic contact pipeline. The sdf_hydroelastic.py module samples the texture SDF using texture_sample_sdf_at_voxel to compute penetration depths and pressure fields. When you provide a custom TextureSDFData handle to the collision pipeline, the hydroelastic solver treats it identically to built-in primitives, computing contact forces based on the SDF gradient and voxel-exact distance values.
What is the maximum resolution supported for the coarse grid?
The coarse grid resolution must be a multiple of 8 and is practically limited by GPU memory and the max_resolution parameter in create_texture_sdf_from_mesh. Typical values range from 64 to 256 for the coarse grid, with sub-grids of size 8³, yielding effective resolutions up to 2048³ in occupied regions. Exceeding 256 for the coarse grid may cause memory pressure due to the indirection array size, while the sub-grid texture grows only with surface complexity, not empty space.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →