Working with Heightfield Terrain Collisions in Newton Simulations

Heightfield terrain collisions in Newton simulations are handled by the Heightfield class, which normalizes elevation data to the [0, 1] range and uses GPU-accelerated signed-distance field (SDF) sampling for accurate contact detection between terrain and dynamic bodies.

Newton Physics represents terrain-like static surfaces through the Heightfield abstraction, providing an efficient way to simulate complex landscapes without explicit mesh generation. This article explains how to create, configure, and optimize heightfield terrain collisions in Newton simulations using the actual source implementation from the newton-physics/newton repository.

Understanding the Heightfield Abstraction in Newton

The core implementation resides in newton/_src/geometry/types.py, where the Heightfield class encapsulates 2-D grid elevation data. Internally, Newton normalizes all heightfield data to the range [0, 1] regardless of input scale, then maps these normalized values to world-space heights using the user-provided min_z and max_z parameters.

Key characteristics of the Newton heightfield implementation include:

  • Automatic normalization: Input arrays are reshaped to (nrow, ncol) and scaled to [0, 1] based on their intrinsic min/max values (Heightfield.__init__ lines 15-25).
  • Static geometry: Heightfields are always immovable with mass = 0, zero inertia matrix, and has_inertia = False (lines 38-41).
  • Geometric footprint: Half-extents hx and hy define the XY bounding box as [-hx, +hx] × [-hy, +hy].

Creating and Configuring Heightfield Terrain

To instantiate terrain, pass a NumPy array of elevation values to the Heightfield constructor along with grid dimensions and spatial extents. The following example from newton/examples/basic/example_basic_heightfield.py generates a sinusoidal landscape:

import numpy as np
import warp as wp
import newton

# Build a sinusoidal heightfield

nrow, ncol = 50, 50
hx, hy = 5.0, 5.0
x = np.linspace(-hx, hx, ncol)
y = np.linspace(-hy, hy, nrow)
xx, yy = np.meshgrid(x, y)
elevation = np.sin(xx) * np.cos(yy) * 0.5

hfield = newton.Heightfield(
    data=elevation, 
    nrow=nrow, 
    ncol=ncol, 
    hx=hx, 
    hy=hy
)

builder = newton.ModelBuilder()
builder.add_shape_heightfield(heightfield=hfield)

The constructor automatically derives min_z and max_z from the data if omitted, or you may specify them explicitly to control the world-space vertical range independently of the source array's scale.

Heightfield Collision Detection Architecture

Newton employs a hybrid collision strategy for heightfields, combining signed-distance field (SDF) queries for fast broad-phase rejection with exact triangle extraction for narrow-phase contact solving. The implementation in newton/_src/utils/heightfield.py provides the GPU kernels that drive this pipeline.

SDF Sampling Kernels

For rapid distance queries, Newton uses sample_sdf_heightfield (lines 171-187), which computes the signed distance from a query point to the terrain surface. Positive values indicate points above the terrain, while negative values indicate penetration. The companion function sample_sdf_grad_heightfield (lines 190-212) additionally returns the surface gradient, yielding the contact normal required for impulse resolution.

Triangle Extraction for Narrow-Phase

When a convex shape intersects the heightfield's bounding region, heightfield_vs_convex_midphase (lines 310-362) performs a broad-phase grid traversal to emit candidate triangle indices. The narrow-phase solver then invokes get_triangle_shape_from_heightfield (lines 215-286) to extract the specific terrain triangle as a GenericShapeData object, enabling standard GJK/MPR contact algorithms to resolve the collision.

Integrating Heightfields into Simulation Models

To incorporate terrain into a simulation, use the ModelBuilder API, which handles buffer management and GPU memory allocation automatically. The workflow follows three stages: construction, finalization, and execution.

First, add the heightfield to your builder using add_shape_heightfield:

builder = newton.ModelBuilder()
builder.add_shape_heightfield(heightfield=hfield)

This method stores a compact HeightfieldData struct (defined in newton/_src/utils/heightfield.py lines 77-90) and appends the elevation data to a concatenated GPU buffer.

Next, add dynamic bodies and finalize the model:


# Add a sphere above the terrain

sphere = builder.add_body(
    xform=wp.transform(p=wp.vec3(0.0, 0.0, 0.5), q=wp.quat_identity())
)
builder.add_shape_sphere(body=sphere, radius=0.1)

model = builder.finalize()

During finalization, Heightfield.finalize() (lines 61-74) allocates the Warp array on the target device and records the buffer offset in HeightfieldData.

The following test snippet from newton/tests/test_heightfield.py verifies that a sphere resting on a flat heightfield does not fall through:

builder = newton.ModelBuilder()
hfield = newton.Heightfield(
    data=np.zeros((10, 10), dtype=np.float32),
    nrow=10, ncol=10, hx=5.0, hy=5.0,
    min_z=0.0, max_z=1.0
)
builder.add_shape_heightfield(heightfield=hfield)

sphere = builder.add_body(xform=wp.transform((0,0,0.5), wp.quat_identity()))
builder.add_shape_sphere(body=sphere, radius=0.1)

model = builder.finalize()
solver = newton.solvers.SolverMuJoCo(model)

state_in, state_out = model.state(), model.state()
control = model.control()
dt = 1.0/240.0

# Step for a few hundred frames

for _ in range(200):
    solver.step(state_in, state_out, control, None, dt)
    state_in, state_out = state_out, state_in

final_z = float(state_in.body_q.numpy()[sphere, 2])
assert final_z > -0.1   # sphere stays above the terrain

Advanced: Direct SDF Queries for Custom Forces

For simulations requiring custom soft-contact models or sensor queries, you can access the heightfield SDF directly without invoking the full narrow-phase pipeline. The sample_sdf_grad_heightfield function in newton/_src/utils/heightfield.py returns both the signed distance and surface normal for any world-space point.


# Assume hfd is a HeightfieldData struct and elev is the concatenated elevation buffer

pos = wp.vec3(1.0, -0.5, 0.2)
dist, normal = newton._src.utils.heightfield.sample_sdf_grad_heightfield(hfd, elev, pos)

# dist: signed distance (positive above terrain, negative inside)

# normal: surface gradient for impulse direction

This low-level access enables implementation of specialized effects such as granular terrain scarring, tire deformation, or heightfield-based fluid coupling while leveraging Newton's optimized GPU kernels.

Summary

  • Heightfield terrain collisions in Newton simulations rely on the Heightfield class in newton/_src/geometry/types.py, which normalizes elevation data to [0, 1] and scales it to world-space using min_z and max_z.
  • Heightfields are strictly static geometry (zero mass and inertia) defined by half-extents hx, hy that bound the XY footprint.
  • Collision detection uses a hybrid approach: GPU-accelerated SDF sampling (sample_sdf_heightfield, sample_sdf_grad_heightfield) for broad-phase queries, and exact triangle extraction (get_triangle_shape_from_heightfield) for narrow-phase GJK/MPR contact solving.
  • Integration requires adding the heightfield to a ModelBuilder via add_shape_heightfield, which manages the HeightfieldData struct and GPU buffer concatenation automatically.
  • Advanced users can query the SDF directly for custom contact models or sensor logic without invoking the full collision pipeline.

Frequently Asked Questions

How does Newton normalize heightfield elevation data?

Newton automatically rescales any input 2-D array to the range [0, 1] during Heightfield initialization (newton/_src/geometry/types.py lines 15-25). The class computes the intrinsic minimum and maximum of the source data, then stores normalized values internally. When the simulation runs, these normalized heights are mapped to world-space coordinates using the user-provided min_z and max_z parameters, allowing you to define terrain extents independently of the source data scale.

Can heightfields be used with dynamic bodies in Newton?

Yes, but heightfields themselves are always static. According to the source code in newton/_src/geometry/types.py (lines 38-41), heightfields have mass = 0, zero inertia matrices, and has_inertia = False. They serve as immovable terrain surfaces that dynamic bodies—such as spheres, boxes, or arbitrary convex shapes—can collide against. The collision pipeline in newton/_src/utils/heightfield.py handles interactions between moving bodies and the static heightfield grid automatically.

What collision algorithms does Newton use for heightfield terrain?

Newton employs a hybrid collision strategy optimized for GPU execution. For broad-phase queries, the engine uses signed-distance field (SDF) sampling via sample_sdf_heightfield and sample_sdf_grad_heightfield (lines 171-212 in newton/_src/utils/heightfield.py) to quickly determine penetration depth and contact normals. For narrow-phase resolution, the system extracts exact terrain triangles using get_triangle_shape_from_heightfield (lines 215-286) and heightfield_vs_convex_midphase (lines 310-362), feeding these into standard GJK/MPR contact algorithms for precise collision response.

How do I prevent objects from falling through heightfield terrain?

Ensure that your dynamic bodies are initialized above the terrain surface and that the heightfield's min_z and max_z parameters correctly map to your world's vertical extents. As demonstrated in newton/tests/test_heightfield.py, you should add the heightfield to a ModelBuilder using add_shape_heightfield, then finalize the model to allocate GPU buffers. The collision pipeline automatically queries the heightfield SDF during each simulation step; if objects still fall through, verify that your solver time step (dt) is sufficiently small (typically 1/240s or less) to capture contact events accurately.

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 →