Skip to content

MJCF import of Steel attributes

02 · Getting started

The same MJCF scene loaded twice: a box under a ceiling and a ball rolling on the floor. SolverSteel.register_custom_attributes(builder) before add_mjcf makes the importer read a geom's adhesion attribute into steel:adhesion, and MJCF's friction="slide torsional rolling" fills shape_material_mu, _torsional and _rolling. The importer does not read a geom's condim into steel:condim, so the script sets it after import.

0.00 s

What to look at

  • Without adhesion the box falls 0.9 m to the floor; with adhesion="20" (4 corner contacts, 80 N against 9.8 N) it hangs.
  • With condim 6 set after import, the ball's imported rolling coefficient stops it in 2.0 s; with condim 3 it keeps rolling at 0.68 m/s.

Key parameter

SolverSteel.register_custom_attributes(builder)

Call it before add_mjcf or add_usd so the importers fill the steel: attributes.

Try changing

  • Add adhesion to a <default> class and import a gripper's finger pads.
  • Import the same file into SolverMuJoCo and compare the hang.

Concept

Steel attributes in imported assets

What it is

Steel's per-shape and per-body inputs live in the model's steel namespace. Registering them on the builder before importing lets MJCF and USD files author them: MJCF adhesion, USD physxRigidBody:* damping and velocity limits, physxCollision:minTorsionalPatchRadius and newton:steel:* attributes.

In this scene

The imported adhesion holds the box (0 mm drop vs 900 mm), and the imported rolling coefficient stops the ball once condim 6 is set.

Considerations

Interactions
MJCF condim fills only SolverMuJoCo's mujoco:condim. steel:condim cannot be written in MJCF either: a prefixed XML attribute needs a namespace declaration, and the importer does not read the namespaced form. Set it in Python or through USD newton:steel:condim.
Failure modes
Registering after add_mjcf silently drops the attributes.
Accuracy
The USD route was not recorded here: the recording environment has no USD runtime.
Use it when
Assets authored for MuJoCo or PhysX should carry their contact behaviour into Steel.

The script

cd tutorials && python 02_mjcf_import.py prints each variant's final metric; --record out.json also writes the replay. Every tutorial imports replay.py from the same folder.

tutorials/02_mjcf_import.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Steel attributes from an MJCF file: geom adhesion and the friction triple carry into Steel."""
import newton
import numpy as np
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
# A box under a ceiling and a ball rolling on the floor. friction="slide torsional rolling".
MJCF = """<mujoco>
<worldbody>
<geom name="floor" type="plane" size="2 2 0.1" friction="0.6 0.01 0.004"/>
<geom name="ceiling" type="box" size="0.2 0.2 0.05" pos="0 0 1.05" {adhesion}/>
<body name="box" pos="0 0 0.95"><freejoint/>
<geom type="box" size="0.05 0.05 0.05" density="1000" gap="0.004"/>
</body>
<body name="ball" pos="0.5 0 0.08"><freejoint/>
<geom type="sphere" size="0.08" density="800" friction="0.6 0.01 0.004"/>
</body>
</worldbody>
</mujoco>"""
def build(adhesion, condim):
builder = newton.ModelBuilder()
# register first, so the importer fills model.steel.adhesion from the geom's adhesion attribute
SolverSteel.register_custom_attributes(builder)
builder.add_mjcf(
MJCF.format(adhesion=f'adhesion="{adhesion}" gap="0.004"' if adhesion else 'gap="0.004"')
)
model = builder.finalize()
# steel:condim cannot be authored in MJCF; set it after import to use the friction triple
model.steel.condim.fill_(condim)
qd = model.joint_qd.numpy()
qd[6] = 1.0 # roll the ball at 1 m/s
model.joint_qd.assign(qd)
return model
def simulate(rec, label, adhesion, condim):
model = build(adhesion, condim)
solver = SolverSteel(model)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
newton.eval_fk(model, model.joint_q, model.joint_qd, state_0)
run = rec.variant(label, model)
for _ in range(rec.frames(3.0)):
q, qd = state_0.body_q.numpy(), state_0.body_qd.numpy()
run.frame(
state_0, box_drop_mm=1000.0 * (0.95 - q[0, 2]), ball_speed=np.linalg.norm(qd[1, :3])
)
for _ in range(SUBSTEPS):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, DT)
state_0, state_1 = state_1, state_0
run.check(solver)
if __name__ == "__main__":
rec = Recorder()
simulate(rec, "MJCF without adhesion, condim 3", adhesion=0, condim=3)
# 4 corner contacts x 20 N = 80 N against the box's 9.8 N weight
simulate(rec, 'MJCF adhesion="20", condim 6 after import', adhesion=20, condim=6)
rec.save()

Recorded with SolverSteel on Newton 1.9.0.dev0, Warp 1.19.0, NVIDIA RTX PRO 6000 Blackwell Server Edition. Replays read the state back every frame, so they are not a speed benchmark. Script · page text