dim-go2-sim 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. dim_go2_sim/__init__.py +30 -0
  2. dim_go2_sim/camera.py +175 -0
  3. dim_go2_sim/config.py +82 -0
  4. dim_go2_sim/contacts.py +103 -0
  5. dim_go2_sim/data/go2/LICENSE +27 -0
  6. dim_go2_sim/data/go2/README.md +57 -0
  7. dim_go2_sim/data/go2/assets/base_0.obj +36346 -0
  8. dim_go2_sim/data/go2/assets/base_1.obj +23131 -0
  9. dim_go2_sim/data/go2/assets/base_2.obj +8367 -0
  10. dim_go2_sim/data/go2/assets/base_3.obj +10932 -0
  11. dim_go2_sim/data/go2/assets/base_4.obj +199205 -0
  12. dim_go2_sim/data/go2/assets/calf_0.obj +24148 -0
  13. dim_go2_sim/data/go2/assets/calf_1.obj +9243 -0
  14. dim_go2_sim/data/go2/assets/calf_mirror_0.obj +24145 -0
  15. dim_go2_sim/data/go2/assets/calf_mirror_1.obj +9252 -0
  16. dim_go2_sim/data/go2/assets/foot.obj +30786 -0
  17. dim_go2_sim/data/go2/assets/hip_0.obj +76960 -0
  18. dim_go2_sim/data/go2/assets/hip_1.obj +73961 -0
  19. dim_go2_sim/data/go2/assets/thigh_0.obj +75706 -0
  20. dim_go2_sim/data/go2/assets/thigh_1.obj +40483 -0
  21. dim_go2_sim/data/go2/assets/thigh_mirror_0.obj +75572 -0
  22. dim_go2_sim/data/go2/assets/thigh_mirror_1.obj +40450 -0
  23. dim_go2_sim/data/go2/go2.xml +219 -0
  24. dim_go2_sim/data/mid360/NOTICE.md +21 -0
  25. dim_go2_sim/data/mid360/fourier.npz +0 -0
  26. dim_go2_sim/data/policy/LICENSE +201 -0
  27. dim_go2_sim/data/policy/NOTICE +12 -0
  28. dim_go2_sim/data/policy/policy.onnx +0 -0
  29. dim_go2_sim/lidar.py +243 -0
  30. dim_go2_sim/mid360.py +136 -0
  31. dim_go2_sim/module.py +303 -0
  32. dim_go2_sim/policy.py +102 -0
  33. dim_go2_sim/world.py +254 -0
  34. dim_go2_sim-0.1.0.dist-info/METADATA +105 -0
  35. dim_go2_sim-0.1.0.dist-info/RECORD +39 -0
  36. dim_go2_sim-0.1.0.dist-info/WHEEL +4 -0
  37. dim_go2_sim-0.1.0.dist-info/licenses/LICENSE +17 -0
  38. dim_go2_sim-0.1.0.dist-info/licenses/src/dim_go2_sim/data/go2/LICENSE +27 -0
  39. dim_go2_sim-0.1.0.dist-info/licenses/src/dim_go2_sim/data/policy/LICENSE +201 -0
@@ -0,0 +1,30 @@
1
+ # Copyright 2026 Dimensional Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """A simulated Unitree Go2 for dimOS: cmd_vel in; Mid-360, camera, IMU, odometry, contacts out."""
16
+
17
+ from dim_go2_sim.config import CameraConfig, LidarConfig, PhysicsConfig, SceneConfig
18
+ from dim_go2_sim.contacts import Contact, Contacts
19
+ from dim_go2_sim.module import Go2Sim, Go2SimConfig
20
+
21
+ __all__ = [
22
+ "CameraConfig",
23
+ "Contact",
24
+ "Contacts",
25
+ "Go2Sim",
26
+ "Go2SimConfig",
27
+ "LidarConfig",
28
+ "PhysicsConfig",
29
+ "SceneConfig",
30
+ ]
dim_go2_sim/camera.py ADDED
@@ -0,0 +1,175 @@
1
+ # Copyright 2026 Dimensional Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Camera frames drawn from a scene's textured glTF with pygfx (after PimSim's mesh renderer).
16
+
17
+ Cooked scenes' MJCFs are collision-only, so MuJoCo would draw grey boxes. Only the static scene
18
+ is drawn, not the robot: a camera must not see its own body. Rendering runs in a fresh child
19
+ process, since GPU device state cannot cross dimOS's forkserver.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import math
26
+ import pickle
27
+ import subprocess
28
+ import sys
29
+ from typing import Any
30
+
31
+ import numpy as np
32
+ from numpy.typing import NDArray
33
+
34
+ from dim_go2_sim.config import CameraConfig
35
+
36
+ # glTF is Y-up; the world is Z-up.
37
+ Y_UP_TO_Z_UP = np.array([[1, 0, 0, 0], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=np.float64)
38
+
39
+
40
+ def focal_length(config: CameraConfig) -> float:
41
+ return config.height / (2 * math.tan(math.radians(config.fovy_deg) / 2))
42
+
43
+
44
+ class _Renderer:
45
+ def __init__(self, visual: str, y_up: bool, config: CameraConfig) -> None:
46
+ import pygfx as gfx # type: ignore[import-untyped]
47
+ from rendercanvas.offscreen import OffscreenRenderCanvas # type: ignore[import-untyped]
48
+
49
+ root = gfx.Group()
50
+ root.add(gfx.load_gltf(visual, quiet=True, remote_ok=False).scene)
51
+ if y_up:
52
+ root.local.matrix = Y_UP_TO_Z_UP
53
+ self._scene = gfx.Scene()
54
+ background = gfx.Background(None, gfx.BackgroundMaterial(config.background))
55
+ self._scene.add(background, _merged(gfx, root))
56
+ self._scene.add(gfx.AmbientLight("#ffffff", config.ambient))
57
+ sun = gfx.DirectionalLight("#fff4e6", config.sun)
58
+ sun.local.position = (4.0, -6.0, 10.0)
59
+ self._scene.add(sun)
60
+ canvas = OffscreenRenderCanvas(size=(config.width, config.height), pixel_ratio=1.0)
61
+ self._renderer = gfx.WgpuRenderer(canvas, pixel_ratio=1.0)
62
+ # pygfx's fov spans the mean of the view's height and width; match the vertical fovy.
63
+ aspect = config.width / config.height
64
+ fov = math.degrees(2 * math.atan(config.height * (1 + aspect) / (4 * focal_length(config))))
65
+ self._camera = gfx.PerspectiveCamera(
66
+ fov=fov, aspect=aspect, depth_range=(config.near, config.far)
67
+ )
68
+
69
+ def capture(
70
+ self, position: NDArray[np.float64], forward: NDArray[np.float64], up: NDArray[np.float64]
71
+ ) -> NDArray[np.uint8]:
72
+ self._camera.world.position = tuple(position)
73
+ self._camera.world.reference_up = tuple(up)
74
+ self._camera.look_at(tuple(position + forward))
75
+ self._renderer.render(self._scene, self._camera)
76
+ self._renderer.flush()
77
+ return np.ascontiguousarray(np.asarray(self._renderer.snapshot())[:, :, :3])
78
+
79
+
80
+ def _merged(gfx: Any, root: Any) -> Any:
81
+ """One mesh per material, in world coordinates: a store is ~47k nodes, one draw call each."""
82
+ meshes: list[Any] = []
83
+ root.traverse(lambda node: meshes.append(node) if isinstance(node, gfx.Mesh) else None)
84
+ by_material: dict[int, list[Any]] = {}
85
+ for mesh in meshes:
86
+ by_material.setdefault(id(mesh.material), []).append(mesh)
87
+ group = gfx.Group()
88
+ for same in by_material.values():
89
+ positions, normals, texcoords, indices, offset = [], [], [], [], 0
90
+ for mesh in same:
91
+ geometry, matrix = mesh.geometry, mesh.world.matrix
92
+ local = geometry.positions.data.reshape(-1, 3).astype(np.float64)
93
+ positions.append((local @ matrix[:3, :3].T + matrix[:3, 3]).astype(np.float32))
94
+ normal = getattr(geometry, "normals", None)
95
+ if normal is not None:
96
+ rotated = normal.data.reshape(-1, 3) @ np.linalg.inv(matrix[:3, :3])
97
+ rotated /= np.maximum(np.linalg.norm(rotated, axis=1, keepdims=True), 1e-12)
98
+ normals.append(rotated.astype(np.float32))
99
+ uv = getattr(geometry, "texcoords", None)
100
+ if uv is not None:
101
+ texcoords.append(uv.data.reshape(-1, 2).astype(np.float32))
102
+ index = getattr(geometry, "indices", None)
103
+ index = np.arange(len(local)) if index is None else index.data.reshape(-1)
104
+ indices.append(index.astype(np.uint32) + offset)
105
+ offset += len(local)
106
+ attributes: dict[str, NDArray[Any]] = {
107
+ "positions": np.concatenate(positions),
108
+ "indices": np.concatenate(indices).reshape(-1, 3),
109
+ }
110
+ # A partial attribute can't be merged; drop it rather than misalign vertices.
111
+ if len(normals) == len(same):
112
+ attributes["normals"] = np.concatenate(normals)
113
+ if len(texcoords) == len(same):
114
+ attributes["texcoords"] = np.concatenate(texcoords)
115
+ group.add(gfx.Mesh(gfx.Geometry(**attributes), same[0].material))
116
+ return group
117
+
118
+
119
+ def _serve(visual: str, y_up: bool, config: CameraConfig) -> None:
120
+ renderer = _Renderer(visual, y_up, config)
121
+ out = sys.stdout.buffer
122
+ pickle.dump(None, out)
123
+ out.flush()
124
+ while True:
125
+ try:
126
+ request = pickle.load(sys.stdin.buffer)
127
+ except EOFError: # the parent exited without saying goodbye
128
+ break
129
+ if request is None:
130
+ break
131
+ pickle.dump(renderer.capture(*request), out, protocol=pickle.HIGHEST_PROTOCOL)
132
+ out.flush()
133
+
134
+
135
+ class SceneCamera:
136
+ """Renders *visual* in a child process. Loading a large scene there takes a while."""
137
+
138
+ def __init__(self, visual: str, y_up: bool, config: CameraConfig) -> None:
139
+ self._process = subprocess.Popen(
140
+ [sys.executable, "-m", __name__, visual, json.dumps(y_up), config.model_dump_json()],
141
+ stdin=subprocess.PIPE,
142
+ stdout=subprocess.PIPE,
143
+ )
144
+ assert self._process.stdout is not None
145
+ try:
146
+ pickle.load(self._process.stdout)
147
+ except EOFError:
148
+ raise RuntimeError(
149
+ f"the camera renderer exited loading {visual}; see its stderr"
150
+ ) from None
151
+
152
+ def capture(
153
+ self, position: NDArray[np.float64], forward: NDArray[np.float64], up: NDArray[np.float64]
154
+ ) -> NDArray[np.uint8]:
155
+ assert self._process.stdin is not None and self._process.stdout is not None
156
+ pickle.dump((position, forward, up), self._process.stdin)
157
+ self._process.stdin.flush()
158
+ rgb: NDArray[np.uint8] = pickle.load(self._process.stdout)
159
+ return rgb
160
+
161
+ def close(self) -> None:
162
+ try:
163
+ assert self._process.stdin is not None
164
+ pickle.dump(None, self._process.stdin)
165
+ self._process.stdin.close()
166
+ except OSError:
167
+ pass
168
+ try:
169
+ self._process.wait(timeout=5)
170
+ except subprocess.TimeoutExpired:
171
+ self._process.kill()
172
+
173
+
174
+ if __name__ == "__main__":
175
+ _serve(sys.argv[1], json.loads(sys.argv[2]), CameraConfig.model_validate_json(sys.argv[3]))
dim_go2_sim/config.py ADDED
@@ -0,0 +1,82 @@
1
+ # Copyright 2026 Dimensional Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Everything about a Go2Sim run: the scene, the physics, and each sensor (no dimOS imports:
16
+ the camera's child process loads this)."""
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+
22
+ from pydantic import BaseModel
23
+
24
+
25
+ class SceneConfig(BaseModel):
26
+ # A cooked dimOS scene package by name (resolved with dimOS's catalog), or explicit files.
27
+ package: str | None = None
28
+ mjcf: Path | None = None # collision geometry; a robot-free MJCF
29
+ visual: Path | None = None # textured glTF/GLB the camera draws; None draws nothing
30
+ visual_y_up: bool = True # glTF's convention; the world is Z-up
31
+ start: tuple[float, float, float] = (0.0, 0.0, 0.0) # x, y, yaw of the robot
32
+ floor_z: float = 0.0 # height the feet start on
33
+ # Scene geometry whose top is below this counts as floor for contacts; the rest is clutter.
34
+ floor_top_z: float = 0.05
35
+
36
+
37
+ class PhysicsConfig(BaseModel):
38
+ timestep: float = 0.0025 # seconds per MuJoCo step; the policy runs every 0.02 s
39
+ # The physics were fitted to real Go2 runs under the bundled policy (go2-sim, dimOS #4441).
40
+ fitted: bool = True
41
+ actuator_tau: float = 0.0151 # seconds of first-order lag between a torque and the joint
42
+ policy: Path | None = None # an rl_sar-layout ONNX walking policy; None is the bundled one
43
+ # Trim the policy's command closed-loop on the true body velocity, as a Go2's sport mode
44
+ # does: the policy alone ignores slow commands and drifts when told to stand.
45
+ track_velocity: bool = True
46
+ command_timeout: float = 0.5 # seconds a cmd_vel holds before the robot stands still
47
+ max_velocity: tuple[float, float, float] = (1.0, 0.5, 1.5) # vx, vy m/s and wz rad/s
48
+ realtime: bool = True # pace to the wall clock; False runs as fast as it can
49
+ cache_dir: Path | None = None # compiled worlds; None is the user cache directory
50
+
51
+
52
+ class LidarConfig(BaseModel):
53
+ enabled: bool = True
54
+ rate_hz: float = 10.0
55
+ # The Mid-360 on a Go2's head, from the body origin, pitched nose-down.
56
+ xyz: tuple[float, float, float] = (0.29515, -0.00003, 0.16297)
57
+ pitch_deg: float = 60.0
58
+ point_rate_hz: int = 200_000
59
+ downsample: int = 1
60
+ min_range: float = 0.16
61
+ max_range: float = 40.0
62
+ noise: bool = True
63
+ dropout: bool = True
64
+ self_occlusion: bool = True # the robot's own body blocks rays
65
+ motion_bins_hz: float = 200.0 # how often a scan's origin follows the moving body
66
+ seed: int = 0
67
+
68
+
69
+ class CameraConfig(BaseModel):
70
+ enabled: bool = True
71
+ rate_hz: float = 10.0
72
+ width: int = 640
73
+ height: int = 480
74
+ fovy_deg: float = 60.0
75
+ # The Go2's nose camera, from the body origin, looking straight ahead.
76
+ xyz: tuple[float, float, float] = (0.32715, -0.00003, 0.04297)
77
+ pitch_deg: float = 0.0
78
+ near: float = 0.05
79
+ far: float = 100.0
80
+ ambient: float = 1.8
81
+ sun: float = 2.5
82
+ background: str = "#1b2230"
@@ -0,0 +1,103 @@
1
+ # Copyright 2026 Dimensional Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Which scene geometry the robot touches, by robot part and kind of geometry. Ground truth."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ import json
21
+ import time
22
+ from typing import Literal
23
+
24
+ from dimos.types.timestamped import Timestamped
25
+ from dimos_lcm.std_msgs import String as LCMString
26
+ import mujoco
27
+ import numpy as np
28
+
29
+ Part = Literal["trunk", "leg", "foot"]
30
+ Kind = Literal["floor", "clutter"]
31
+
32
+
33
+ @dataclass(frozen=True, order=True)
34
+ class Contact:
35
+ part: Part
36
+ kind: Kind
37
+
38
+
39
+ class Contacts(Timestamped):
40
+ """The set of contacts at one instant, published whenever it changes."""
41
+
42
+ msg_name = "dim_go2_sim.Contacts"
43
+
44
+ def __init__(self, contacts: list[Contact] | None = None, ts: float | None = None) -> None:
45
+ self.contacts = contacts or []
46
+ self.ts = time.time() if ts is None else ts
47
+
48
+ def lcm_encode(self) -> bytes:
49
+ payload = {"ts": self.ts, "contacts": [[c.part, c.kind] for c in self.contacts]}
50
+ return LCMString(data=json.dumps(payload)).lcm_encode() # type: ignore[no-any-return]
51
+
52
+ @classmethod
53
+ def lcm_decode(cls, data: bytes) -> Contacts:
54
+ payload = json.loads(LCMString.lcm_decode(data).data)
55
+ return cls([Contact(part, kind) for part, kind in payload["contacts"]], payload["ts"])
56
+
57
+
58
+ class ContactSensor:
59
+ """Classifies each static scene geom once as floor (its top below floor_top_z) or clutter."""
60
+
61
+ def __init__(
62
+ self,
63
+ model: mujoco.MjModel,
64
+ data: mujoco.MjData,
65
+ robot_bodies: set[int],
66
+ trunk: int,
67
+ feet: tuple[int, ...],
68
+ floor_top_z: float,
69
+ ) -> None:
70
+ self._data = data
71
+ self._trunk = trunk
72
+ self._feet = set(feet)
73
+ self._robot = {g for g in range(model.ngeom) if int(model.geom_bodyid[g]) in robot_bodies}
74
+ self._body = model.geom_bodyid
75
+ self._kind: dict[int, Kind] = {}
76
+ for g in range(model.ngeom):
77
+ if g in self._robot or model.body_weldid[model.geom_bodyid[g]] != 0:
78
+ continue
79
+ if model.geom_type[g] == mujoco.mjtGeom.mjGEOM_PLANE:
80
+ self._kind[g] = "floor"
81
+ continue
82
+ up = data.geom_xmat[g].reshape(3, 3)[2]
83
+ center, half = model.geom_aabb[g, :3], model.geom_aabb[g, 3:]
84
+ top = data.geom_xpos[g, 2] + up @ center + np.abs(up) @ half
85
+ self._kind[g] = "floor" if top < floor_top_z else "clutter"
86
+
87
+ def contacts(self) -> list[Contact]:
88
+ """Every (robot part, scene kind) pair in contact right now, sorted."""
89
+ found: set[Contact] = set()
90
+ for i in range(self._data.ncon):
91
+ c = self._data.contact[i]
92
+ for scene, robot in ((c.geom1, c.geom2), (c.geom2, c.geom1)):
93
+ kind = self._kind.get(int(scene))
94
+ if kind is not None and int(robot) in self._robot:
95
+ found.add(Contact(self._part(int(robot)), kind))
96
+ return sorted(found)
97
+
98
+ def _part(self, geom: int) -> Part:
99
+ if geom in self._feet:
100
+ return "foot"
101
+ if self._body[geom] == self._trunk:
102
+ return "trunk"
103
+ return "leg"
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2016-2022 HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics")
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,57 @@
1
+ # Unitree Go2 Description (MJCF)
2
+
3
+ > [!IMPORTANT]
4
+ > Requires MuJoCo 2.2.2 or later.
5
+
6
+ ## Changelog
7
+
8
+ See [CHANGELOG.md](./CHANGELOG.md) for a full history of changes.
9
+
10
+ ## Overview
11
+
12
+ This package contains a simplified robot description (MJCF) of the [Go2
13
+ Quadruped Robot](https://www.unitree.com/go2/) developed by [Unitree
14
+ Robotics](https://www.unitree.com/). It is derived from the [publicly available
15
+ URDF
16
+ description](https://github.com/unitreerobotics/unitree_ros/tree/master/robots/go2_description).
17
+
18
+ <p float="left">
19
+ <img src="go2.png" width="400">
20
+ </p>
21
+
22
+ ## URDF → MJCF derivation steps
23
+
24
+ 1. Converted the DAE [mesh
25
+ files](https://github.com/unitreerobotics/unitree_ros/tree/master/robots/go2_description/meshes) to OBJ format using [Blender](https://www.blender.org/).
26
+ - When exporting, ensure "up axis" is `+Z`, and "forward axis" is `+Y`.
27
+ 2. Processed `.obj` files with [`obj2mjcf`](https://github.com/kevinzakka/obj2mjcf).
28
+ 3. Added `<mujoco> <compiler discardvisual="false" strippath="false" fusestatic="false"/> </mujoco>` to the URDF's
29
+ `<robot>` clause in order to preserve visual geometries.
30
+ 4. Loaded the URDF into MuJoCo and saved a corresponding MJCF.
31
+ 5. Added a `<freejoint/>` to the base.
32
+ 6. Manually edited the MJCF to extract common properties into the `<default>` section.
33
+ 7. Softened the contacts of the feet to approximate the effect of rubber and
34
+ increased `impratio` to reduce slippage.
35
+ 8. Added `scene.xml` which includes the robot, with a textured groundplane, skybox, and haze.
36
+
37
+ ## MJX
38
+
39
+ A version of the Unitree Go2 model for use in MJX is available in `scene_mjx.xml` with the following changes:
40
+
41
+ 1. The solver `iterations` and `ls_iterations` were modified for performance.
42
+ 2. `frictionloss` was removed.
43
+ 3. The friction cone was changed from `elliptic` to `pyramidal`.
44
+ 4. All contacts other than the sphere geoms were changed to sphere geoms and placed at the joints positions to simulate collisions at critical locations.
45
+ - Note: Currently, MJX cannot deal with collision geoms other than spheres. In this case, we have to put many collision spheres in critical locations of the robot such as joints, torso, and feet as illustrated below.
46
+
47
+ <p float="left">
48
+ <img src="go2_mjx.png" width="800">
49
+ </p>
50
+
51
+ ## License
52
+
53
+ This model is released under a [BSD-3-Clause License](LICENSE).
54
+
55
+ ## Acknowledgments
56
+
57
+ The MJX model of Go2 was contributed by [lonelyfluency](https://github.com/lonelyfluency).