Introduction to Newton Physics#

The main components for a Newton simulation are:

  1. Model: Encapsulates the physical structure, parameters, and configuration.

  2. State: Represents the dynamic state (positions, velocities, etc.).

  3. Solver: Steps the simulation by integrating physics and resolving contact, joint, and other constraints on the simulated objects.

See also the Overview page.

In this tutorial, we show how to set up the model and state for a simple scene of a few rigid bodies with different collision geometries falling on a ground plane, and how to step the simulation. We’ll also show how to visualize the resulting simulation.

Setup and Imports#

First, let’s import the necessary libraries:

[1]:
# Colab setup: install Newton before importing it.
import importlib.util
import subprocess
import sys

try:
    IN_COLAB = importlib.util.find_spec("google.colab") is not None
except ModuleNotFoundError:
    IN_COLAB = False

if IN_COLAB:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "newton[notebook]"])
[2]:
from pathlib import Path

import warp as wp
from pxr import Usd

import newton

# these dependencies are needed to load example assets and ingest meshes from USD
import newton.examples
import newton.usd

Building a Model with ModelBuilder#

The ModelBuilder is the primary way to construct simulation scenes. Newton provides multiple approaches for building models:

  1. Programmatic construction: Add bodies, shapes, joints, and constraints directly using add_body(), add_shape_*(), add_joint_*() methods

  2. Import from asset files: Load complete models from URDF, MJCF (MuJoCo XML), or USD files using add_urdf(), add_mjcf(), and add_usd()

  3. Parallel environments for RL: Use replicate() to create multiple copies of an environment for parallel training, or combine different models with add_builder() for modular scene construction

In this tutorial, we’ll focus on programmatic construction to understand the fundamentals. For examples showing asset import and parallelization, see newton/examples/basic/example_basic_urdf.py and newton/examples/robot/.

Let’s create a simple scene with various collision shapes falling onto a ground plane.

Step 1: Create the ModelBuilder#

[3]:
# Create a new model builder
builder = newton.ModelBuilder()
Warp CUDA warning: Could not find or load the NVIDIA CUDA driver. GPU execution will not be available.
Warp 1.17.0.dev20260807 initialized:
   CUDA Toolkit 12.9, CUDA driver not available (NVRTC compilation available)
   Devices:
     "cpu"      : "x86_64"
   Kernel cache:
     /home/runner/.cache/warp/1.17.0.dev20260807

Step 2: Add Rigid Bodies with Collision Shapes#

Bodies are the fundamental dynamic entities in Newton. Each body can have one or more collision shapes attached to it.

The workflow is:

  1. Create a body with add_body() - returns a body index

  2. Attach shapes to the body using add_shape_*() methods

  3. Shapes contribute to the body’s mass/inertia

[4]:
# Add a ground plane (infinite static plane at z=0)
builder.add_ground_plane()

# Height from which to drop shapes
drop_z = 2.0

# SPHERE
sphere_pos = wp.vec3(0.0, -4.0, drop_z)
body_sphere = builder.add_body(
    xform=wp.transform(p=sphere_pos, q=wp.quat_identity()),
    label="sphere",  # Optional: human-readable identifier
)
builder.add_shape_sphere(body_sphere, radius=0.5)

# CAPSULE
capsule_pos = wp.vec3(0.0, -2.0, drop_z)
body_capsule = builder.add_body(xform=wp.transform(p=capsule_pos, q=wp.quat_identity()), label="capsule")
builder.add_shape_capsule(body_capsule, radius=0.3, half_height=0.7)

# CYLINDER
cylinder_pos = wp.vec3(0.0, 0.0, drop_z)
body_cylinder = builder.add_body(xform=wp.transform(p=cylinder_pos, q=wp.quat_identity()), label="cylinder")
builder.add_shape_cylinder(body_cylinder, radius=0.4, half_height=0.6)

# Multi-Shape Collider
multi_shape_pos = wp.vec3(0.0, 2.0, drop_z)
body_multi_shape = builder.add_body(xform=wp.transform(p=multi_shape_pos, q=wp.quat_identity()), label="multi_shape")

# Now attach both a sphere and a box to the multi-shape body
# body-local shape offsets, offset sphere in x so the body will topple over
sphere_offset = wp.vec3(0.1, 0.0, -0.3)
box_offset = wp.vec3(0.0, 0.0, 0.3)
builder.add_shape_sphere(body_multi_shape, xform=wp.transform(p=sphere_offset, q=wp.quat_identity()), radius=0.25)
builder.add_shape_box(
    body_multi_shape, xform=wp.transform(p=box_offset, q=wp.quat_identity()), hx=0.25, hy=0.25, hz=0.25
)

print(f"Added {builder.body_count} bodies with collision shapes")
Added 4 bodies with collision shapes

Step 3: Add a Mesh Body#

Newton can also simulate bodies with triangle-mesh collision shapes. Let’s load a mesh from a USD file:

[5]:
# Load a mesh from a USD file
usd_stage = Usd.Stage.Open(newton.examples.get_asset("bunny.usd"))
demo_mesh = newton.usd.get_mesh(usd_stage.GetPrimAtPath("/root/bunny"))

# Add the mesh as a rigid body
mesh_pos = wp.vec3(0.0, 4.0, drop_z - 0.5)
body_mesh = builder.add_body(xform=wp.transform(p=mesh_pos, q=wp.quat(0.5, 0.5, 0.5, 0.5)), label="bunny")
builder.add_shape_mesh(body_mesh, mesh=demo_mesh)

print(f"Added mesh body with {demo_mesh.vertices.shape[0]} vertices")
Module newton._src.geometry.inertia b20cb3b load on device 'cpu' took 5141.00 ms  (compiled)
Added mesh body with 6102 vertices

Step 4: Finalize the Model#

Once all bodies and shapes are added, we finalize the model. This converts the Python data structures into GPU-optimized arrays and makes the model ready for simulation.

Newton runs on GPU by default (if a GPU is available); you may force the compute device to CPU by setting use_cpu to True below.

[6]:
# Optional: Run the simulation on CPU
use_cpu = False
if use_cpu:
    wp.set_device("cpu")  # alternatively, pass device="cpu" to the finalize method

# Finalize the model - this creates the simulation-ready Model object
model = builder.finalize()

print(f"Model finalized for device {model.device}:")
print(f"  Bodies: {model.body_count}")
print(f"  Shapes: {model.shape_count}")
print(f"  Joints: {model.joint_count}")
Module validate_and_correct_inertia_kernel_5e18b3cd 2f0edb1 load on device 'cpu' took 270.51 ms  (compiled)
/tmp/ipykernel_2890/4174647983.py:7: DeprecationWarning: The legacy DOF-shaped joint_target_q layout is deprecated for models whose joint coordinate and DOF counts differ (free/ball/distance joints). In a future release joint_target_q will always use the coordinate layout (matching joint_q) and newton.use_coord_layout_targets will be removed. Set newton.use_coord_layout_targets = True before building models and index targets via Model.joint_target_q_start.
  model = builder.finalize()
Module newton._src.geometry.bvh b70b622 load on device 'cpu' took 611.92 ms  (compiled)
Model finalized for device cpu:
  Bodies: 5
  Shapes: 7
  Joints: 5

Creating States and Control#

After finalizing the model, we can create the objects that hold time-varying data, i.e. the data that is changing with each simulation step.

  • State: Holds positions, velocities, and forces

  • Control: Holds user-set control inputs (joint torques, motor commands, etc.)

  • Contacts: Holds contacts generated by the collision pipeline for processing in the solver

Some solvers rely on input and output state to be separated in memory when running the solver.step() method, including XPBD. The MuJoCo solver, on the other hand, can be run with just a single state passed in as both input and output states.

Note that for differentiable simulations you may need to allocate a new state for every substep of the simulation. To accommodate these different use cases, we leave the memory management of the State and Control objects up to the user.

[7]:
# Create two state objects for time integration
state_0 = model.state()  # Current state
state_1 = model.state()  # Next state

# The control object is not used in this example, but we create it for completeness
control = model.control()

# Allocate Contacts buffer
collision_pipeline = newton.CollisionPipeline(model)
contacts = collision_pipeline.contacts()

print("State, Contacts and Control objects created")
State, Contacts and Control objects created

Setting Up the Solver#

Newton provides multiple solver implementations. For this example, we use XPBD (Extended Position-Based Dynamics).

For other available solvers and their features/strengths, please refer to the Solvers feature overview.

[8]:
# Create the XPBD solver with 10 constraint iterations
solver = newton.solvers.SolverXPBD(model, iterations=10)

print(f"Solver created: {type(solver).__name__}")
Module newton._src.solvers.solver 277ac14 load on device 'cpu' took 709.25 ms  (compiled)
Solver created: SolverXPBD

Configuring the Simulation Loop#

Let’s set up the simulation parameters and create a simulation function:

[9]:
# Simulation parameters
fps = 60  # Frames per second for visualization
frame_dt = 1.0 / fps  # Time step per frame
sim_substeps = 10  # Number of physics substeps per frame
sim_dt = frame_dt / sim_substeps  # Physics time step

print("Simulation configured:")
print(f"  Frame rate: {fps} Hz")
print(f"  Frame dt: {frame_dt:.4f} s")
print(f"  Physics substeps: {sim_substeps}")
print(f"  Physics dt: {sim_dt:.4f} s")
Simulation configured:
  Frame rate: 60 Hz
  Frame dt: 0.0167 s
  Physics substeps: 10
  Physics dt: 0.0017 s

The Simulation Function#

The core simulation loop executed for each substep follows this pattern:

  1. Clear forces in State that may have been set by the solver or user in the previous step

  2. Apply external forces

  3. Detect collisions

  4. Step the solver forward in time

  5. Swap state buffers

[10]:
def simulate():
    """Run multiple physics substeps for one frame."""
    global state_0, state_1

    for _ in range(sim_substeps):
        # 1. Clear forces in input state
        state_0.clear_forces()

        # 2. Apply control targets/forces, and viewer picking forces if using the OpenGL viewer
        # update_control(state_0, control)
        # viewer.apply_forces(state_0)

        # 3. Detect collisions
        collision_pipeline.collide(state_0, contacts)

        # 4. Step the simulation by one physics timestep
        solver.step(state_in=state_0, state_out=state_1, control=control, contacts=contacts, dt=sim_dt)

        # 5. Swap states (next becomes current)
        state_0, state_1 = state_1, state_0

Acceleration with Graph Capture#

For maximum performance, we can capture the simulation loop as a graph. This reduces repeated launch overhead:

[11]:
# Capture the simulation as a graph
with wp.ScopedCapture() as capture:
    simulate()
graph = capture.graph
print("Graph captured for optimized execution")
Module newton._src.sim.collide ed07d9d load on device 'cpu' took 630.16 ms  (compiled)
Module newton._src.geometry.broad_phase_nxn cbef3b7 load on device 'cpu' took 304.49 ms  (compiled)
Module narrow_phase_primitive_write_contact_False 5a341e2 load on device 'cpu' took 1197.77 ms  (compiled)
Module narrow_phase_gjk_mpr_True_False_write_contact_default_default 1d0bed7 load on device 'cpu' took 2726.56 ms  (compiled)
Module newton._src.geometry.narrow_phase c8704b2 load on device 'cpu' took 739.87 ms  (compiled)
Module newton._src.geometry.contact_reduction_global d9c392c load on device 'cpu' took 1073.90 ms  (compiled)
Module newton._src.geometry.sdf_contact 2f70d45 load on device 'cpu' took 167.75 ms  (compiled)
Module narrow_phase_mesh_plane_write_contact_to_reducer_True 7c47a58 load on device 'cpu' took 382.45 ms  (compiled)
Module mesh_triangle_contacts_to_reducer_kernel_81c84625 5bce825 load on device 'cpu' took 2502.75 ms  (compiled)
Module sdf_contact_write_contact_to_reducer_False_True_False_True_False_False 753cde1 load on device 'cpu' took 2642.52 ms  (compiled)
Module export_reduced_contacts_write_contact 9f72d4f load on device 'cpu' took 545.35 ms  (compiled)
Module newton._src.solvers.xpbd.kernels 58c8da7 load on device 'cpu' took 9849.67 ms  (compiled)
Graph captured for optimized execution

Visualization#

There are several viewer types available in Newton that can be used to display and/or debug simulations, see the Visualization section in the docs.

In this example, we use the Viser viewer, which launches a web server and can be embedded in Jupyter notebooks or viewed in a browser.

[12]:
# Create the Viser viewer with a path to save the recording
recording_path = Path("../_static/recordings/00_introduction.viser").resolve()
recording_path.parent.mkdir(parents=True, exist_ok=True)
viewer = newton.viewer.ViewerViser(verbose=False, record_to_viser=str(recording_path))

# Set the model (this logs the static geometry)
viewer.set_model(model)
╭────── viser (listening *:8080) ───────╮
│             ╷                         │
│   HTTP      │ http://localhost:8080   │
│   Websocket │ ws://localhost:8080     │
│             ╵                         │
╰───────────────────────────────────────╯
Module newton._src.viewer.kernels 9d60597 load on device 'cpu' took 2813.35 ms  (compiled)

Running the Simulation#

Now let’s run the simulation and visualize it! We’ll simulate 500 frames (about 8 seconds at 60 fps).

[13]:
# Run the simulation
num_frames = 500
sim_time = 0.0  # Current simulation time in seconds
for _ in range(num_frames):
    # Execute the simulation (replay the captured graph if available)
    if graph:
        wp.capture_launch(graph)
    else:
        simulate()

    # Log the current state to the viewer
    viewer.begin_frame(sim_time)
    viewer.log_state(state_0)

    # Log contacts to the viewer (not supported by the Viser viewer)
    viewer.log_contacts(contacts, state_0)
    viewer.end_frame()

    # Advance simulation time
    sim_time += frame_dt

print(f"\nSimulation complete! Total time: {sim_time:.2f} seconds")

viewer

Simulation complete! Total time: 8.33 seconds

Next Steps#

To learn more about Newton:

  • Examples: Explore the newton/examples/ directory for more complex scenarios

  • Documentation: Visit newton-physics.github.io