Configure Hydroelastic Contact for a Nut-and-Bolt Assembly#
Hydroelastic contact represents interaction over a contact patch rather than only at a small set of points. This contact model is useful for conforming geometry, where the shape of the interface affects the forces, torques, and relative motion between bodies.
This walkthrough uses a threaded nut-and-bolt assembly to demonstrate how to prepare existing USD collision meshes for Newton hydroelastic contact in the Isaac Sim UI. It also distinguishes the contact properties authored on the USD assets from the collision-pipeline settings configured at runtime.
By the end of this walkthrough you will know how to:
Prepare existing collision meshes for hydroelastic contact from the Property panel.
Configure SDF detail and contact properties for conforming geometry.
Distinguish asset-level USD properties from runtime
NewtonConfigsettings.Validate the authored contact pair in simulation.
For the contact model and parameter reference, see Hydroelastic Contact.
Prerequisites#
Isaac Sim with the Newton backend available (
isaacsim.physics.newton).
Start with Newton#
Hydroelastic contact is available only with the Newton physics engine. Launch
Isaac Sim with Newton enabled (for example isaacsim.newton.sh /
isaacsim.newton.bat, or set the default engine to Newton), then confirm that
the viewport indicates Newton is active.
The Newton schema entries under Add > Physics > Newton appear only
when Newton is the active engine. Without them, you cannot apply
NewtonSDFCollisionAPI from the UI.
Load the nut and bolt#
The Factory assets provide collision meshes for the example nut and bolt. Open Window > Script Editor, paste the following script, and run it. The script creates a new stage, loads both assets, and places the nut on the bolt tip with a small initial thread engagement.
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Load and position the Factory nut-and-bolt assets."""
import asyncio
import math
import isaacsim.core.experimental.utils.stage as stage_utils
import omni.kit.actions.core
import omni.kit.app
from isaacsim.core.experimental.prims import XformPrim
from isaacsim.storage.native import get_assets_root_path_async
from pxr import UsdPhysics
BOLT_PATH = "/World/bolt"
NUT_PATH = "/World/nut"
BOLT_BODY_PATH = f"{BOLT_PATH}/factory_bolt_loose"
BOLT_ROOT_JOINT_PATH = f"{BOLT_PATH}/root_joint"
BOLT_TIP_HEIGHT = 0.035
NUT_MESH_BASE_OFFSET = 0.010
NUT_ENGAGEMENT = 0.0005
NUT_YAW = math.pi / 8.0
async def load_factory_fasteners_async() -> None:
"""Create a stage and place the Factory nut on the bolt tip."""
assets_root_path = await get_assets_root_path_async()
if assets_root_path is None:
raise RuntimeError("Could not find the Isaac Sim assets root.")
await stage_utils.create_new_stage_async()
action_registry = omni.kit.actions.core.get_action_registry()
action_registry.get_action("omni.kit.viewport.menubar.lighting", "set_lighting_mode_camera").execute()
factory_directory = f"{assets_root_path}/Isaac/IsaacLab/Factory"
stage_utils.add_reference_to_stage(
usd_path=f"{factory_directory}/factory_bolt_m16.usd",
path=BOLT_PATH,
)
XformPrim(BOLT_PATH, reset_xform_op_properties=True).set_local_poses(
translations=[[0.0, 0.0, 0.0]],
orientations=[[1.0, 0.0, 0.0, 0.0]],
)
stage = stage_utils.get_current_stage(backend="usd")
bolt_body = stage.GetPrimAtPath(BOLT_BODY_PATH)
# Keep the bolt fixed as a static collider instead of a one-joint articulation.
bolt_body.RemoveAPI(UsdPhysics.RigidBodyAPI)
bolt_body.RemoveAPI(UsdPhysics.ArticulationRootAPI)
stage.GetPrimAtPath(BOLT_ROOT_JOINT_PATH).SetActive(False)
stage_utils.add_reference_to_stage(
usd_path=f"{factory_directory}/factory_nut_m16.usd",
path=NUT_PATH,
)
XformPrim(NUT_PATH, reset_xform_op_properties=True).set_local_poses(
translations=[[0.0, 0.0, BOLT_TIP_HEIGHT - NUT_MESH_BASE_OFFSET - NUT_ENGAGEMENT]],
orientations=[[math.cos(NUT_YAW * 0.5), 0.0, 0.0, math.sin(NUT_YAW * 0.5)]],
)
await omni.kit.app.get_app().next_update_async()
asyncio.ensure_future(load_factory_fasteners_async())
This setup uses an asynchronous function because asset-root lookup, stage creation, and the final Kit update must be awaited. The remaining Script Editor snippets on this page perform synchronous USD authoring and do not require an asynchronous wrapper.
The script also converts the bolt from a fixed-base articulation to a static collider and enables Camera Light. The nut remains dynamic so gravity can engage the threads. To enable the same lighting mode manually, open the viewport Lighting menu and select Camera Light.
Apply Newton SDF Collider#
Hydroelastic contact needs a signed distance field (SDF) on each participating
collider. Applying Newton SDF Collider is the UI step that opts a mesh into
that path: it adds NewtonSDFCollisionAPI, enables SDF generation for the
shape, and inherits NewtonCollisionAPI and PhysicsCollisionAPI.
In the Stage panel, select the bolt collision mesh at
/World/bolt/factory_bolt_loose/collisions.In the Property panel, click Add > Physics > Newton > Newton SDF Collider.
Repeat for the nut collision mesh at
/World/nut/factory_nut_loose/collisions.
Both shapes in a pair must carry this API. Applying it on only one mesh does not produce hydroelastic contacts.
Enable hydroelastic contact#
NewtonSDFCollisionAPI alone prepares the SDF. You still have to enable
hydroelastic contact on each shape and size the SDF so the helical thread is
resolved. With each collider selected after applying Newton SDF Collider,
set the attributes in the Property panel:
Set Hydroelastic Enabled (
newton:hydroelasticEnabled) totrueon both the nut and the bolt colliders.Optionally set Hydroelastic Stiffness (
newton:hydroelasticStiffness). Higher values produce stiffer contacts. The default is1e10.Set SDF Max Resolution (
newton:sdfMaxResolution) high enough for the thread detail.128is a reasonable starting value for these Factory meshes.Optionally tighten SDF Narrow Band Inner / Outer for fine features (for example
-0.005and0.005).Set Contact Margin (
newton:contactMargin) to0.0and Contact Gap (newton:contactGap) to0.005so contacts are detected before the threads interpenetrate without inflating the collision surface.
Both shapes must have newton:hydroelasticEnabled = true. Enabling the
attribute on only one shape of a pair does not produce hydroelastic contacts.
The Factory assets also carry the PhysX SDF collider setup. NewtonSDFCollisionAPI supersedes it, and Newton warns that physics:approximation is ignored. Setting Approximation to none on the collider clears the warning.
After completing the UI steps, you can perform the same authoring through the
Script Editor. The following script applies NewtonSDFCollisionAPI and the
hydroelastic settings described above to both Factory collision meshes:
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Configure the Factory colliders for Newton hydroelastic contact."""
import omni.usd
from pxr import Sdf
COLLIDER_PATHS = (
"/World/bolt/factory_bolt_loose/collisions",
"/World/nut/factory_nut_loose/collisions",
)
def configure_hydroelastic_colliders() -> None:
"""Configure both fastener colliders for hydroelastic contact."""
stage = omni.usd.get_context().get_stage()
for collider_path in COLLIDER_PATHS:
prim = stage.GetPrimAtPath(collider_path)
if not prim.IsValid():
raise RuntimeError(f"Collider prim does not exist: {collider_path}")
if not prim.HasAPI("NewtonSDFCollisionAPI"):
prim.ApplyAPI("NewtonSDFCollisionAPI")
prim.GetAttribute("newton:hydroelasticEnabled").Set(True)
prim.GetAttribute("newton:hydroelasticStiffness").Set(1.0e10)
prim.GetAttribute("newton:sdfMaxResolution").Set(128)
prim.GetAttribute("newton:sdfNarrowBandInner").Set(-0.005)
prim.GetAttribute("newton:sdfNarrowBandOuter").Set(0.005)
prim.GetAttribute("newton:contactMargin").Set(0.0)
prim.GetAttribute("newton:contactGap").Set(0.005)
approximation_attr = prim.GetAttribute("physics:approximation")
if not approximation_attr.IsValid():
approximation_attr = prim.CreateAttribute("physics:approximation", Sdf.ValueTypeNames.Token)
approximation_attr.Set("none")
configure_hydroelastic_colliders()
Bind a contact material#
NewtonSDFCollisionAPI configures the collision representation, but it does
not define friction or other material response. In this example, friction
determines whether the nut turns along the thread or locks on the first
engagement. With the default friction of 1.0, the nut wedges after a few
millimeters.
Select Create > Physics > Physics Material, choose Rigid Body Material, and create the material at
/World/PhysicsMaterials/fastener.On the material, set Static Friction and Dynamic Friction to
0.01and Restitution to0.0.With the material selected, choose Add > Physics > Newton > Newton Material. Set Torsional Friction and Rolling Friction to
0.0, Contact Stiffness to1e7, and Contact Damping to1e4.Select the bolt collision mesh. In Physics Materials on Selected Models, bind
/World/PhysicsMaterials/fastener.Repeat the material binding for the nut collision mesh.
The following Script Editor snippet performs the same setup. It creates the
material, applies NewtonMaterialAPI, authors the USD Physics and Newton
material properties, and binds the material to both collision meshes for the
physics purpose.
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Author and bind a low-friction Newton contact material."""
import omni.usd
from pxr import UsdPhysics, UsdShade
COLLIDER_PATHS = (
"/World/bolt/factory_bolt_loose/collisions",
"/World/nut/factory_nut_loose/collisions",
)
def bind_fastener_material() -> None:
"""Create a Newton material and bind it to both fastener colliders."""
stage = omni.usd.get_context().get_stage()
material = UsdShade.Material.Define(stage, "/World/PhysicsMaterials/fastener")
material_api = UsdPhysics.MaterialAPI.Apply(material.GetPrim())
material_api.CreateStaticFrictionAttr().Set(0.01)
material_api.CreateDynamicFrictionAttr().Set(0.01)
material_api.CreateRestitutionAttr().Set(0.0)
material_prim = material.GetPrim()
if not material_prim.HasAPI("NewtonMaterialAPI"):
material_prim.ApplyAPI("NewtonMaterialAPI")
material_prim.GetAttribute("newton:torsionalFriction").Set(0.0)
material_prim.GetAttribute("newton:rollingFriction").Set(0.0)
material_prim.GetAttribute("newton:contactStiffness").Set(1.0e7)
material_prim.GetAttribute("newton:contactDamping").Set(1.0e4)
for collider_path in COLLIDER_PATHS:
collider_prim = stage.GetPrimAtPath(collider_path)
if not collider_prim.IsValid():
raise RuntimeError(f"Collider prim does not exist: {collider_path}")
UsdShade.MaterialBindingAPI.Apply(collider_prim).Bind(
material,
UsdShade.Tokens.weakerThanDescendants,
materialPurpose="physics",
)
bind_fastener_material()
Configure solver and pipeline settings#
The USD work above makes the shapes hydroelastic-ready. Threaded contact also
depends on the simulation rate, solver settings, and runtime collision-pipeline
capacity. NewtonConfig is not stored in USD, so apply this configuration
after loading the stage and before pressing Play:
A physics rate of 240 Hz with
num_substepsset to2, so contact is resolved at 480 Hz for the 2 mm thread pitch.rigid_contact_maxand the MuJoConconmax/njmaxraised well above their defaults. A hydroelastic patch on a helical thread produces hundreds of contacts per step, and the defaults (1000 pipeline contacts, 200 solver contacts) drop them, which lets the nut pass through the bolt.MuJoCo
iterationsset to15andls_iterationsset to100to match the solver configuration used by the standalone example.mc_edge_clamp_minof0.0. The marching-cubes edge clamp biases contact surface vertices, and threading is sensitive to that bias.buffer_mult_isoof2. The iso-surface buffers must hold the full thread patch; the default size overflows and drops contacts.
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Configure solver and pipeline settings that have no USD attribute equivalent."""
import omni.kit.app
from isaacsim.core.simulation_manager import SimulationManager
ext_mgr = omni.kit.app.get_app().get_extension_manager()
ext_mgr.set_extension_enabled_immediate("isaacsim.physics.newton", True)
from isaacsim.physics.newton import (
CollisionConfig,
HydroelasticConfig,
MuJoCoSolverConfig,
NewtonConfig,
configure_newton,
)
MAX_CONTACTS = 40_000
SimulationManager.setup_simulation(dt=1.0 / 240.0, device="cuda")
configure_newton(
NewtonConfig(
num_substeps=2,
solver_cfg=MuJoCoSolverConfig(
njmax=MAX_CONTACTS,
nconmax=MAX_CONTACTS,
),
collision_cfg=CollisionConfig(
rigid_contact_max=MAX_CONTACTS,
hydroelastic=HydroelasticConfig(mc_edge_clamp_min=0.0, buffer_mult_iso=2),
),
)
)
The snippet sets the 240 Hz physics timestep with
SimulationManager.setup_simulation() and then calls configure_newton().
Rerun it whenever you open or reload the stage. Defaults for other fields
(enabled, reduce_contacts, and so on) are listed in
Hydroelastic Contact and the isaacsim.physics.newton Python API.
The resulting motion is shown below.
Standalone example#
If you prefer not to author in the UI, the same scene can be built and run from a standalone script:
./python.sh standalone_examples/api/isaacsim.physics.newton/nut_bolt_hydroelastic.py
That script loads the Factory USDs, applies NewtonSDFCollisionAPI and the
contact material on the colliders, configures the solver and pipeline, and then
waits for you to press Play.
Additional resources#
Hydroelastic Contact — contact model and parameter reference.
Newton Physics Backend — Newton backend overview.