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:

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_mesh in newton/_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_sdf and texture_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.py for soft-body contacts or sdf_contact.py for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →