Merge branch 'usd-integration' of github.com:awesome-aj0123/mujoco into usd-integration

This commit is contained in:
Abhishek Joshi
2023-10-19 12:25:14 -05:00
244 changed files with 26188 additions and 3990 deletions
+7 -2
View File
@@ -140,7 +140,7 @@ findorfetch(
GIT_REPO
https://github.com/abseil/abseil-cpp
GIT_TAG
c2435f8342c2d0ed8101cb43adfd605fdc52dca2 # LTS 20230125.3
fb3621f4f897824c0dbe0615fa94543df6192f30 # LTS 20230802.1
TARGETS
${MUJOCO_PYTHON_ABSL_TARGETS}
EXCLUDE_FROM_ALL
@@ -173,7 +173,7 @@ findorfetch(
GIT_REPO
https://gitlab.com/libeigen/eigen
GIT_TAG
211c5dfc6741a5570ad007983c113ef4d144f9f3
e8515f78ac098329ab9f8cab21c87caede090a3f
TARGETS
Eigen3::Eigen
EXCLUDE_FROM_ALL
@@ -311,6 +311,11 @@ macro(mujoco_pybind11_module name)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# Only allow fallthrough annotation via __attribute__.
target_compile_options(${name} PRIVATE -Wimplicit-fallthrough=5)
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109561
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 11)
target_compile_options(${name} PRIVATE -Wno-maybe-uninitialized)
endif()
endif()
endif()
set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+1
View File
@@ -829,6 +829,7 @@ Euler integrator, semi-implicit in velocity.
self.assertEqual(mujoco.mjtGeom.mjGEOM_ARROW, 100)
self.assertEqual(mujoco.mjtGeom.mjGEOM_ARROW1, 101)
self.assertEqual(mujoco.mjtGeom.mjGEOM_ARROW2, 102)
self.assertEqual(mujoco.mjtGeom.mjGEOM_TRIANGLE, 107)
self.assertEqual(mujoco.mjtGeom.mjGEOM_NONE, 1001)
def test_enum_from_int(self):
+1 -1
View File
@@ -17,7 +17,7 @@
import ctypes
import enum
_CGL = ctypes.CDLL('/System/Library/OpenGL.framework/OpenGL')
_CGL = ctypes.CDLL('/System/Library/Frameworks/OpenGL.framework/OpenGL')
CGLContextObj = ctypes.c_void_p
CGLPixelFormatObj = ctypes.c_void_p
+11 -8
View File
@@ -126,7 +126,7 @@ PYBIND11_MODULE(_functions, pymodule) {
Def<traits::mj_resetData>(pymodule);
Def<traits::mj_resetDataDebug>(pymodule);
Def<traits::mj_resetDataKeyframe>(pymodule);
// Skipped: mj_stackAlloc (doesn't make sense in Python)
// Skipped: mj_stackAllocByte (doesn't make sense in Python)
// Skipped: mj_deleteData (have MjData.__del__)
Def<traits::mj_resetCallbacks>(pymodule);
Def<traits::mj_setConst>(pymodule);
@@ -199,6 +199,7 @@ PYBIND11_MODULE(_functions, pymodule) {
Def<traits::mj_kinematics>(pymodule);
Def<traits::mj_comPos>(pymodule);
Def<traits::mj_camlight>(pymodule);
Def<traits::mj_flex>(pymodule);
Def<traits::mj_tendon>(pymodule);
Def<traits::mj_transmission>(pymodule);
Def<traits::mj_crb>(pymodule);
@@ -581,6 +582,7 @@ PYBIND11_MODULE(_functions, pymodule) {
Def<traits::mj_rayHfield>(pymodule);
Def<traits::mj_rayMesh>(pymodule);
Def<traits::mju_rayGeom>(pymodule);
Def<traits::mju_rayFlex>(pymodule);
Def<traits::mju_raySkin>(pymodule);
// Interaction
@@ -1343,6 +1345,7 @@ PYBIND11_MODULE(_functions, pymodule) {
static_cast<char*>(data->arena),
data->narena - data->pstack);
#endif
data->parena = 0;
data->ncon = 0;
data->nefc = 0;
data->contact = static_cast<raw::MjContact*>(data->arena);
@@ -1356,7 +1359,7 @@ PYBIND11_MODULE(_functions, pymodule) {
data->ncon = ncon;
data->nefc = nefc;
data->contact =
static_cast<raw::MjContact*>(InterceptMjErrors(::mj_arenaAlloc)(
static_cast<raw::MjContact*>(InterceptMjErrors(::mj_arenaAllocByte)(
data, ncon * sizeof(raw::MjContact), alignof(raw::MjContact)));
if (!data->contact) {
cleanup(data);
@@ -1367,12 +1370,12 @@ PYBIND11_MODULE(_functions, pymodule) {
#define MJ_M(x) d.metadata().x
#undef MJ_D
#define MJ_D(x) data->x
#define X(type, name, nr, nc) \
data->name = static_cast<type*>(InterceptMjErrors(::mj_arenaAlloc)( \
data, sizeof(type) * (nr) * (nc), alignof(type))); \
if (!data->name) { \
cleanup(data); \
throw FatalError("insufficient arena memory available"); \
#define X(type, name, nr, nc) \
data->name = static_cast<type*>(InterceptMjErrors(::mj_arenaAllocByte)( \
data, sizeof(type) * (nr) * (nc), alignof(type))); \
if (!data->name) { \
cleanup(data); \
throw FatalError("insufficient arena memory available"); \
}
MJDATA_ARENA_POINTERS_PRIMAL
+8 -8
View File
@@ -78,14 +78,14 @@
X( mjtNum, cam_, ipd, ncam, 1 ) \
X( mjtNum, cam_, user, ncam, MJ_M(nuser_cam) )
#define MJMODEL_EQUALITY \
X( int, eq_, type, neq, 1 ) \
X( int, eq_, obj1id, neq, 1 ) \
X( int, eq_, obj2id, neq, 1 ) \
X( mjtByte, eq_, active, neq, 1 ) \
X( mjtNum, eq_, solref, neq, mjNREF ) \
X( mjtNum, eq_, solimp, neq, mjNIMP ) \
X( mjtNum, eq_, data, neq, mjNEQDATA )
#define MJMODEL_EQUALITY \
X( int, eq_, type, neq, 1 ) \
X( int, eq_, obj1id, neq, 1 ) \
X( int, eq_, obj2id, neq, 1 ) \
X( mjtByte, eq_, active0, neq, 1 ) \
X( mjtNum, eq_, solref, neq, mjNREF ) \
X( mjtNum, eq_, solimp, neq, mjNIMP ) \
X( mjtNum, eq_, data, neq, mjNEQDATA )
#define MJMODEL_EXCLUDE \
X( int, exclude_, signature, nexclude, 1 )
+26 -5
View File
@@ -115,12 +115,27 @@ class _MjPythonImpl(mujoco.viewer._MjPythonBase):
self._termination = self.__class__.NOT_TERMINATED
self._busy = False
def launch_on_ui_thread(self, model, data, handle_return, key_callback):
def launch_on_ui_thread(
self,
model,
data,
handle_return,
key_callback,
show_left_ui,
show_right_ui,
):
with self._cond:
if self._busy or self._task is not None:
raise RuntimeError('another MuJoCo viewer is already open')
else:
self._task = (model, data, handle_return, key_callback)
self._task = (
model,
data,
handle_return,
key_callback,
show_left_ui,
show_right_ui,
)
self._cond.notify()
def terminate(self):
@@ -294,11 +309,17 @@ while True:
break
# Otherwise, launch the viewer.
model, data, handle_return, key_callback = task
model, data, handle_return, key_callback, show_left_ui, show_right_ui = task
ctypes.CDLL(None).mjpython_show_dock_icon()
mujoco.viewer._launch_internal(
model, data, run_physics_thread=False, handle_return=handle_return,
key_callback=key_callback)
model,
data,
run_physics_thread=False,
handle_return=handle_return,
key_callback=key_callback,
show_left_ui=show_left_ui,
show_right_ui=show_right_ui,
)
ctypes.CDLL(None).mjpython_hide_dock_icon()
finally:
+111
View File
@@ -0,0 +1,111 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""CLI for converting legacy MSH files to Wavefront OBJ files.
Usage:
python -m mujoco.msh2obj -i <msh_file> -o <obj_file>
"""
import argparse
import dataclasses
import io
import pathlib
import numpy as np
@dataclasses.dataclass(frozen=True)
class Msh:
"""MuJoCo legacy binary msh file."""
vertex_positions: np.ndarray
vertex_normals: np.ndarray
vertex_texcoords: np.ndarray
face_vertex_indices: np.ndarray
@staticmethod
def create(file: pathlib.Path) -> "Msh":
"""Create a Msh object from a .msh file."""
if not file.exists():
raise FileNotFoundError(f"{file} does not exist.")
with open(file, "rb") as f:
nvertex = np.fromfile(f, dtype=np.int32, count=1)[0]
nnormal = np.fromfile(f, dtype=np.int32, count=1)[0]
ntexcoord = np.fromfile(f, dtype=np.int32, count=1)[0]
nface = np.fromfile(f, dtype=np.int32, count=1)[0]
vertex_positions = np.fromfile(f, dtype=np.float32, count=3 * nvertex)
vertex_normals = np.fromfile(f, dtype=np.float32, count=3 * nnormal)
vertex_texcoords = np.fromfile(f, dtype=np.float32, count=2 * ntexcoord)
face_vertex_indices = np.fromfile(f, dtype=np.int32, count=3 * nface)
if vertex_positions.size != 3 * nvertex:
raise ValueError(
f"Invalid number of vertices: {vertex_positions.size} != 3*{nvertex}."
)
if vertex_normals.size != 3 * nnormal:
raise ValueError(
f"Invalid number of normals: {vertex_normals.size} != 3*{nnormal}."
)
if vertex_texcoords.size != 2 * ntexcoord:
raise ValueError(
f"Invalid number of texcoords: {vertex_texcoords.size} != "
"2*{ntexcoord}."
)
if face_vertex_indices.size != 3 * nface:
raise ValueError(
f"Invalid number of faces: {face_vertex_indices.size} != 3*{nface}."
)
vertex_positions = vertex_positions.reshape(-1, 3)
vertex_normals = vertex_normals.reshape(-1, 3)
face_vertex_indices = face_vertex_indices.reshape(-1, 3)
# Undo vertical flip done by MuJoCo's OBJ loader.
vertex_texcoords = vertex_texcoords.reshape(-1, 2)
vertex_texcoords[:, 1] = 1 - vertex_texcoords[:, 1]
return Msh(
vertex_positions=vertex_positions,
vertex_normals=vertex_normals,
vertex_texcoords=vertex_texcoords,
face_vertex_indices=face_vertex_indices,
)
def msh_to_obj(msh_file: pathlib.Path) -> str:
"""Convert a legacy .msh file to the .obj format."""
msh = Msh.create(msh_file)
out = io.StringIO()
for x, y, z in msh.vertex_positions:
out.write(f"v {x} {y} {z}\n")
for x, y, z in msh.vertex_normals:
out.write(f"vn {x} {y} {z}\n")
for u, v in msh.vertex_texcoords:
out.write(f"vt {u} {v}\n")
for i, j, k in msh.face_vertex_indices:
out.write(f"f {i+1}/{i+1}/{i+1} {j+1}/{j+1}/{j+1} {k+1}/{k+1}/{k+1}\n")
return out.getvalue()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("-i", "--input", type=str, help="Path to the msh file.")
parser.add_argument("-o", "--output", type=str, help="Path to the obj file.")
args = parser.parse_args()
with open(pathlib.Path(args.output), "w") as f:
f.write(msh_to_obj(pathlib.Path(args.input)))
+77
View File
@@ -0,0 +1,77 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for msh2obj.py."""
from absl.testing import absltest
from etils import epath
import mujoco
from mujoco import msh2obj
import numpy as np
_MESH_FIELDS = (
"mesh_vertadr",
"mesh_vertnum",
"mesh_faceadr",
"mesh_facenum",
"mesh_bvhadr",
"mesh_bvhnum",
"mesh_normaladr",
"mesh_normalnum",
"mesh_texcoordadr",
"mesh_texcoordnum",
"mesh_graphadr",
"mesh_vert",
"mesh_normal",
"mesh_face",
"mesh_facenormal",
"mesh_facetexcoord",
"mesh_graph",
"mesh_texcoord",
)
_XML = """
<mujoco>
<asset>
<mesh name="abdomen_1_body" file="abdomen_1_body.obj"/>
</asset>
</mujoco>
"""
class MshTest(absltest.TestCase):
def test_obj_model_matches_msh_model(self) -> None:
test_path = epath.resource_path("mujoco") / "testdata"
msh_xml = test_path / "msh.xml"
msh_model = mujoco.MjModel.from_xml_path(msh_xml.as_posix())
msh_path = test_path / "abdomen_1_body.msh"
obj = msh2obj.msh_to_obj(msh_path)
obj_model = mujoco.MjModel.from_xml_string(
_XML, {"abdomen_1_body.obj": obj.encode()})
for field in _MESH_FIELDS:
np.testing.assert_allclose(
getattr(msh_model, field),
getattr(obj_model, field),
atol=1e-6,
err_msg=f"Field {field} does not match between msh and obj models.",
)
if __name__ == "__main__":
absltest.main()
+1 -1
View File
@@ -22,7 +22,7 @@
// They should be regarded as part of MuJoCo's internal implementation detail.
extern "C" {
MJAPI void _mjPRIVATE__set_tls_error_fn(void (*h)(const char*));
MJAPI void* mj_arenaAlloc(mjData* d, int bytes, int alignment);
MJAPI void* mj_arenaAllocByte(mjData* d, int bytes, int alignment);
}
#endif // MUJOCO_PYTHON_PRIVATE_H_
+1
View File
@@ -215,6 +215,7 @@ PYBIND11_MODULE(_render, pymodule) {
X(windowDoublebuffer);
X(currentBuffer);
X(readPixelFormat);
X(readDepthMap);
#undef X
#define X(var) \
+31 -5
View File
@@ -86,6 +86,7 @@ the clause:
_render.mjr_setBuffer(
_enums.mjtFramebuffer.mjFB_OFFSCREEN.value, self._mjr_context
)
self._mjr_context.readDepthMap = _enums.mjtDepthMap.mjDEPTH_ZEROFAR
# Default render flags.
self._depth_rendering = False
@@ -138,7 +139,9 @@ the clause:
"""
original_flags = self._scene.flags.copy()
if self._segmentation_rendering:
# Using segmented rendering for depth makes the calculated depth more
# accurate at far distances.
if self._depth_rendering or self._segmentation_rendering:
self._scene.flags[_enums.mjtRndFlag.mjRND_SEGMENT] = True
self._scene.flags[_enums.mjtRndFlag.mjRND_IDCOLOR] = True
@@ -173,11 +176,34 @@ the clause:
near = self._model.vis.map.znear * extent
far = self._model.vis.map.zfar * extent
# Convert from [0 1] to depth in units of length, see links below:
# http://stackoverflow.com/a/6657284/1461210
# https://www.khronos.org/opengl/wiki/Depth_Buffer_Precision
out = near / (1 - out * (1 - near / far))
# Calculate OpenGL perspective matrix values in float32 precision
# so they are close to what glFrustum returns
# https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
zfar = np.float32(far)
znear = np.float32(near)
c_coef = -(zfar + znear) / (zfar - znear)
d_coef = -(np.float32(2) * zfar * znear) / (zfar - znear)
# In reverse Z mode the perspective matrix is transformed by the following
c_coef = np.float32(-0.5) * c_coef - np.float32(0.5)
d_coef = np.float32(-0.5) * d_coef
# We need 64 bits to convert Z from ndc to metric depth without noticeable
# losses in precision
out_64 = out.astype(np.float64)
# Undo OpenGL projection
# Note: We do not need to take action to convert from window coordinates
# to normalized device coordinates because in reversed Z mode the mapping
# is identity
out_64 = d_coef / (out_64 + c_coef)
# Cast result back to float32 for backwards compatibility
# This has a small accuracy cost
out[:] = out_64.astype(np.float32)
# Reset scene flags.
np.copyto(self._scene.flags, original_flags)
elif self._segmentation_rendering:
_render.mjr_readPixels(out, None, self._rect, self._mjr_context)
+36 -17
View File
@@ -69,19 +69,23 @@ class UIAdapterWithPyCallback : public Adapter {
class SimulateWrapper {
public:
SimulateWrapper(std::unique_ptr<PlatformUIAdapter> platform_ui_adapter,
py::object scn, py::object cam, py::object opt,
py::object pert, bool fully_managed)
py::object cam, py::object opt,
py::object pert, py::object user_scn, bool is_passive)
: simulate_(new mujoco::Simulate(
std::move(platform_ui_adapter), scn.cast<MjvSceneWrapper&>().get(),
std::move(platform_ui_adapter),
cam.cast<MjvCameraWrapper&>().get(),
opt.cast<MjvOptionWrapper&>().get(),
pert.cast<MjvPerturbWrapper&>().get(), fully_managed)),
pert.cast<MjvPerturbWrapper&>().get(), is_passive)),
m_(py::none()),
d_(py::none()),
scn_(scn),
cam_(cam),
opt_(opt),
pert_(pert) {}
pert_(pert),
user_scn_(user_scn) {
if (!user_scn.is_none()) {
simulate_->user_scn = user_scn_.cast<MjvSceneWrapper&>().get();
}
}
~SimulateWrapper() { Destroy(); }
@@ -93,9 +97,9 @@ class SimulateWrapper {
}
}
void WaitUntilDestroyed() {
void WaitUntilExit() {
// TODO: replace with atomic wait when we migrate to C++20
while (!destroyed_.load()) {
while (simulate_ && simulate_->exitrequest.load() != 2) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
@@ -127,10 +131,10 @@ class SimulateWrapper {
// simulate object.
py::object m_;
py::object d_;
py::object scn_;
py::object cam_;
py::object opt_;
py::object pert_;
py::object user_scn_;
mjModel* m_raw_ = nullptr;
mjData* d_raw_ = nullptr;
@@ -194,19 +198,25 @@ PYBIND11_MODULE(_simulate, pymodule) {
py::class_<SimulateWrapper>(pymodule, "Simulate")
.def_readonly_static("MAX_GEOM", &mujoco::Simulate::kMaxGeom)
.def(py::init([](py::object scn, py::object cam, py::object opt,
py::object pert, bool fully_managed,
py::object pert, bool run_physics_thread,
py::object key_callback) {
bool is_passive = !run_physics_thread;
return std::make_unique<SimulateWrapper>(
std::make_unique<UIAdapterWithPyCallback<UIAdapter>>(
key_callback),
scn, cam, opt, pert, fully_managed);
scn, cam, opt, pert, is_passive);
}))
.def("destroy", &SimulateWrapper::Destroy,
.def("destroy", &SimulateWrapper::Destroy)
.def("load_message", CallIfNotNull(&mujoco::Simulate::LoadMessage),
py::call_guard<py::gil_scoped_release>())
.def("load", &SimulateWrapper::Load)
.def("load_message_clear",
CallIfNotNull(&mujoco::Simulate::LoadMessageClear),
py::call_guard<py::gil_scoped_release>())
.def("sync", CallIfNotNull(&mujoco::Simulate::Sync),
py::call_guard<py::gil_scoped_release>())
.def("add_to_history", CallIfNotNull(&mujoco::Simulate::AddToHistory),
py::call_guard<py::gil_scoped_release>())
.def("render_loop", CallIfNotNull(&mujoco::Simulate::RenderLoop),
py::call_guard<py::gil_scoped_release>())
.def("lock", GetIfNotNull(&mujoco::Simulate::mtx),
@@ -255,9 +265,8 @@ PYBIND11_MODULE(_simulate, pymodule) {
int value = 0;
sim->exitrequest.compare_exchange_strong(value, 1);
wrapper.WaitUntilDestroyed();
},
py::call_guard<py::gil_scoped_release>())
wrapper.WaitUntilExit();
})
.def_property_readonly("uiloadrequest",
CallIfNotNull(+[](mujoco::Simulate& sim) {
@@ -303,7 +312,17 @@ PYBIND11_MODULE(_simulate, pymodule) {
const auto max_length = sizeof_arr(sim.load_error);
std::strncpy(sim.load_error, error.c_str(), max_length - 1);
sim.load_error[max_length - 1] = '\0';
}));
}))
.def_property("ui0_enable", GetIfNotNull(&mujoco::Simulate::ui0_enable),
CallIfNotNull(+[](mujoco::Simulate& sim, int enabled) {
sim.ui0_enable = enabled;
}),
py::call_guard<py::gil_scoped_release>())
.def_property("ui1_enable", GetIfNotNull(&mujoco::Simulate::ui1_enable),
CallIfNotNull(+[](mujoco::Simulate& sim, int enabled) {
sim.ui1_enable = enabled;
}),
py::call_guard<py::gil_scoped_release>());
pymodule.def("set_glfw_dlhandle", [](std::uintptr_t dlhandle) {
mujoco::Glfw(reinterpret_cast<void*>(dlhandle));
+130 -12
View File
@@ -234,6 +234,7 @@ MjModelWrapper::MjWrapper(raw::MjModel* ptr)
MJMODEL_POINTERS,
text_data_bytes(ptr->text_data, ptr->ntextdata),
names_bytes(ptr->names, ptr->nnames),
paths_bytes(ptr->paths, ptr->npaths),
indexer_(ptr, owner_) {
bool is_newly_inserted = false;
{
@@ -255,6 +256,7 @@ MjModelWrapper::MjWrapper(MjModelWrapper&& other)
MJMODEL_POINTERS,
text_data_bytes(ptr_->text_data, ptr_->ntextdata),
names_bytes(ptr_->names, ptr_->nnames),
paths_bytes(ptr_->paths, ptr_->npaths),
indexer_(ptr_, owner_) {
bool is_newly_inserted = false;
{
@@ -488,7 +490,11 @@ MjContactWrapper::MjWrapper()
X(solref),
X(solreffriction),
X(solimp),
X(H) {}
X(H),
X(geom),
X(flex),
X(elem),
X(vert) {}
MjContactWrapper::MjWrapper(raw::MjContact* ptr, py::handle owner)
: WrapperBase(ptr, owner),
@@ -498,7 +504,11 @@ MjContactWrapper::MjWrapper(raw::MjContact* ptr, py::handle owner)
X(solref),
X(solreffriction),
X(solimp),
X(H) {}
X(H),
X(geom),
X(flex),
X(elem),
X(vert) {}
#undef X
MjContactWrapper::MjWrapper(const MjContactWrapper& other)
@@ -820,11 +830,11 @@ MjDataWrapper MjDataWrapper::Deserialize(std::istream& input) {
#define MJ_M(x) m.x
#undef MJ_D
#define MJ_D(x) d->x
#define X(type, name, nr, nc) \
if ((nr) * (nc)) { \
d->name = static_cast<decltype(d->name)>( \
mj_arenaAlloc(d, sizeof(type) * (nr) * (nc), alignof(type))); \
ReadBytes(input, d->name, sizeof(type) * (nr) * (nc)); \
#define X(type, name, nr, nc) \
if ((nr) * (nc)) { \
d->name = static_cast<decltype(d->name)>( \
mj_arenaAllocByte(d, sizeof(type) * (nr) * (nc), alignof(type))); \
ReadBytes(input, d->name, sizeof(type) * (nr) * (nc)); \
}
MJDATA_ARENA_POINTERS_CONTACT
@@ -1048,7 +1058,9 @@ MjvGeomWrapper::MjWrapper()
static_assert(sizeof(ptr_->mat) == sizeof(ptr_->mat[0])*9);
return InitPyArray(std::array{3, 3}, ptr_->mat, owner_);
}()),
X(rgba) {}
X(rgba) {
mjv_initGeom(ptr_, mjGEOM_NONE, nullptr, nullptr, nullptr, nullptr);
}
MjvGeomWrapper::MjWrapper(raw::MjvGeom* ptr, py::handle owner)
: WrapperBase(ptr, owner),
@@ -1106,6 +1118,7 @@ MjvOptionWrapper::MjWrapper()
X(jointgroup),
X(tendongroup),
X(actuatorgroup),
X(flexgroup),
X(skingroup),
X(flags) {}
#undef X
@@ -1137,6 +1150,18 @@ MjvSceneWrapper::MjWrapper()
nskinvert(0),
XN(geoms, 0),
XN(geomorder, 0),
XN(flexedgeadr, 0),
XN(flexedgenum, 0),
XN(flexvertadr, 0),
XN(flexvertnum, 0),
XN(flexfaceadr, 0),
XN(flexfacenum, 0),
XN(flexfaceused, 0),
XN(flexedge, 0),
XN(flexvert, 0),
XN(flexface, 0),
XN(flexnormal, 0),
XN(flextexcoord, 0),
XN(skinfacenum, 0),
XN(skinvertadr, 0),
XN(skinvertnum, 0),
@@ -1166,8 +1191,54 @@ MjvSceneWrapper::MjWrapper(const MjModelWrapper& model, int maxgeom)
}
return nskinvert;
}(model.get())),
nflexface([](const raw::MjModel* m) {
int nflexface = 0;
int flexfacenum = 0;
for (int f=0; f < m->nflex; f++) {
if (m->flex_dim[f] == 0) {
// 1D : 0
flexfacenum = 0;
} else if (m->flex_dim[f] == 2) {
// 2D: 2*fragments + 2*elements
flexfacenum = 2*m->flex_shellnum[f] + 2*m->flex_elemnum[f];
} else {
// 3D: max(fragments, 4*maxlayer)
// find number of elements in biggest layer
int maxlayer = 0, layer = 0, nlayer = 1;
while (nlayer) {
nlayer = 0;
for (int e=0; e < m->flex_elemnum[f]; e++) {
if (m->flex_elemlayer[m->flex_elemadr[f]+e] == layer) {
nlayer++;
}
}
maxlayer = mjMAX(maxlayer, nlayer);
layer++;
}
flexfacenum = mjMAX(m->flex_shellnum[f], 4*maxlayer);
}
// accumulate over flexes
nflexface += flexfacenum;
}
return nflexface;
}(model.get())),
nflexedge(model.get()->nflexedge),
nflexvert(model.get()->nflexvert),
XN(geoms, ptr_->maxgeom),
XN(geomorder, ptr_->maxgeom),
XN(flexedgeadr, ptr_->nflex),
XN(flexedgenum, ptr_->nflex),
XN(flexvertadr, ptr_->nflex),
XN(flexvertnum, ptr_->nflex),
XN(flexfaceadr, ptr_->nflex),
XN(flexfacenum, ptr_->nflex),
XN(flexfaceused, ptr_->nflex),
XN(flexedge, 2*nflexedge),
XN(flexvert, 3*nflexvert),
XN(flexface, 9*nflexface),
XN(flexnormal, 9*nflexface),
XN(flextexcoord, 6*nflexface),
XN(skinfacenum, ptr_->nskin),
XN(skinvertadr, ptr_->nskin),
XN(skinvertnum, ptr_->nskin),
@@ -1204,6 +1275,18 @@ MjvSceneWrapper::MjWrapper(const MjvSceneWrapper& other)
XN(geoms, ptr_->ngeom);
XN(geomorder, ptr_->ngeom);
XN(flexedgeadr, ptr_->nflex);
XN(flexedgenum, ptr_->nflex);
XN(flexvertadr, ptr_->nflex);
XN(flexvertnum, ptr_->nflex);
XN(flexfaceadr, ptr_->nflex);
XN(flexfacenum, ptr_->nflex);
XN(flexfaceused, ptr_->nflex);
XN(flexedge, 2*nflexedge);
XN(flexvert, 3*nflexvert);
XN(flexface, 9*nflexface);
XN(flexnormal, 9*nflexface);
XN(flextexcoord, 6*nflexface);
XN(skinfacenum, ptr_->nskin);
XN(skinvertadr, ptr_->nskin);
XN(skinvertnum, ptr_->nskin);
@@ -1509,10 +1592,11 @@ This is useful for example when the MJB is not available as a file on disk.)"));
MJMODEL_INTS
#undef X
#define X(dtype, var, dim0, dim1) \
if constexpr (std::string_view(#var) != "text_data" && \
std::string_view(#var) != "names") { \
DefinePyArray(mjModel, #var, &MjModelWrapper::var); \
#define X(dtype, var, dim0, dim1) \
if constexpr (std::string_view(#var) != "text_data" && \
std::string_view(#var) != "names" && \
std::string_view(#var) != "paths") { \
DefinePyArray(mjModel, #var, &MjModelWrapper::var); \
}
MJMODEL_POINTERS
#undef X
@@ -1527,6 +1611,11 @@ This is useful for example when the MJB is not available as a file on disk.)"));
// Return the full bytes array of concatenated names
return m.names_bytes;
});
mjModel.def_property_readonly(
"paths", [](const MjModelWrapper& m) -> const auto& {
// Return the full bytes array of concatenated paths
return m.paths_bytes;
});
#define XGROUP(MjModelGroupedViews, field, nfield, FIELD_XMACROS) \
mjModel.def( \
@@ -1746,6 +1835,10 @@ This is useful for example when the MJB is not available as a file on disk.)"));
X(solreffriction);
X(solimp);
X(H);
X(geom);
X(flex);
X(elem);
X(vert);
#undef X
py::class_<MjContactList> mjContactList(m, "_MjContactList");
@@ -1784,6 +1877,10 @@ This is useful for example when the MJB is not available as a file on disk.)"));
X(int, geom2);
X(int, exclude);
X(int, efc_address);
XN(int, geom);
XN(int, flex);
XN(int, elem);
XN(int, vert);
#undef X
#undef XN
@@ -1975,6 +2072,7 @@ This is useful for example when the MJB is not available as a file on disk.)"));
c.get()->var = rhs; \
})
X(select);
X(flexselect);
X(skinselect);
X(active);
X(active2);
@@ -2036,6 +2134,7 @@ This is useful for example when the MJB is not available as a file on disk.)"));
c.get()->var = rhs; \
})
X(frustum_center);
X(frustum_width);
X(frustum_bottom);
X(frustum_top);
X(frustum_near);
@@ -2143,6 +2242,7 @@ This is useful for example when the MJB is not available as a file on disk.)"));
X(label);
X(frame);
X(bvh_depth);
X(flex_layer);
#undef X
#define X(var) DefinePyArray(mjvOption, #var, &MjvOptionWrapper::var)
@@ -2151,6 +2251,7 @@ This is useful for example when the MJB is not available as a file on disk.)"));
X(jointgroup);
X(tendongroup);
X(actuatorgroup);
X(flexgroup);
X(skingroup);
X(flags);
#undef X
@@ -2174,8 +2275,13 @@ This is useful for example when the MJB is not available as a file on disk.)"));
})
X(maxgeom);
X(ngeom);
X(nflex);
X(nskin);
X(nlight);
X(flexvertopt);
X(flexedgeopt);
X(flexfaceopt);
X(flexskinopt);
X(enabletransform);
X(scale);
X(stereo);
@@ -2185,6 +2291,18 @@ This is useful for example when the MJB is not available as a file on disk.)"));
#define X(var) DefinePyArray(mjvScene, #var, &MjvSceneWrapper::var)
X(geoms);
X(geomorder);
X(flexedgeadr);
X(flexedgenum);
X(flexvertadr);
X(flexvertnum);
X(flexfaceadr);
X(flexfacenum);
X(flexfaceused);
X(flexedge);
X(flexvert);
X(flexface);
X(flexnormal);
X(flextexcoord);
X(skinfacenum);
X(skinvertadr);
X(skinvertnum);
+21
View File
@@ -16,6 +16,7 @@
#define MUJOCO_PYTHON_STRUCTS_H_
#include <array>
#include <cstddef>
#include <istream>
#include <memory>
#include <optional>
@@ -476,6 +477,7 @@ class MjWrapper<raw::MjModel> : public WrapperBase<raw::MjModel> {
// TODO(nimrod): Exclude text_data and names from the MJMODEL_POINTERS macro.
pybind11::bytes text_data_bytes;
pybind11::bytes names_bytes;
pybind11::bytes paths_bytes;
protected:
explicit MjWrapper(raw::MjModel* ptr);
@@ -506,6 +508,10 @@ class MjWrapper<raw::MjContact> : public WrapperBase<raw::MjContact> {
X(solreffriction);
X(solimp);
X(H);
X(geom);
X(flex);
X(elem);
X(vert);
#undef X
};
@@ -579,6 +585,7 @@ class MjWrapper<raw::MjData>: public WrapperBase<raw::MjData> {
py_array_or_tuple_t<mjContact> contact;
py_array_or_tuple_t<size_t> maxuse_threadstack;
py_array_or_tuple_t<raw::MjWarningStat> warning;
py_array_or_tuple_t<raw::MjTimerStat> timer;
py_array_or_tuple_t<raw::MjSolverStat> solver;
@@ -730,6 +737,7 @@ class MjWrapper<raw::MjvOption> : public WrapperBase<raw::MjvOption> {
X(jointgroup);
X(tendongroup);
X(actuatorgroup);
X(flexgroup);
X(skingroup);
X(flags);
#undef X
@@ -751,10 +759,23 @@ class MjWrapper<raw::MjvScene> : public WrapperBase<raw::MjvScene> {
~MjWrapper() = default;
int nskinvert;
int nflexface, nflexedge, nflexvert;
#define X(dtype, var) py_array_or_tuple_t<dtype> var
X(mjvGeom, geoms);
X(int, geomorder);
X(int, flexedgeadr);
X(int, flexedgenum);
X(int, flexvertadr);
X(int, flexvertnum);
X(int, flexfaceadr);
X(int, flexfacenum);
X(int, flexfaceused);
X(int, flexedge);
X(float, flexvert);
X(float, flexface);
X(float, flexnormal);
X(float, flextexcoord);
X(int, skinfacenum);
X(int, skinvertadr);
X(int, skinvertnum);
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
<mujoco>
<asset>
<mesh name="abdomen_1_body" file="abdomen_1_body.msh"/>
</asset>
</mujoco>
+75 -27
View File
@@ -67,20 +67,16 @@ class Handle:
def __init__(
self,
sim: _Simulate,
scn: mujoco.MjvScene,
cam: mujoco.MjvCamera,
opt: mujoco.MjvOption,
pert: mujoco.MjvPerturb,
user_scn: Optional[mujoco.MjvScene],
):
self._sim = weakref.ref(sim)
self._scn = scn
self._cam = cam
self._opt = opt
self._pert = pert
@property
def scn(self):
return self._scn
self._user_scn = user_scn
@property
def cam(self):
@@ -94,40 +90,52 @@ class Handle:
def perturb(self):
return self._pert
@property
def user_scn(self):
return self._user_scn
def close(self):
sim = self._sim()
if sim is not None:
sim.exit()
def is_running(self) -> bool:
def _get_sim(self) -> Optional[_Simulate]:
sim = self._sim()
if sim is not None:
return sim.exitrequest < 2
return False
try:
return sim if sim.exitrequest == 0 else None
except mujoco.UnexpectedError:
# UnexpectedError is raised when accessing `exitrequest` after the
# underlying simulate instance has been deleted in C++.
return None
return None
def is_running(self) -> bool:
return self._get_sim() is not None
def lock(self):
sim = self._sim()
sim = self._get_sim()
if sim is not None:
return sim.lock()
return contextlib.nullcontext()
def sync(self):
sim = self._sim()
sim = self._get_sim()
if sim is not None:
sim.sync() # locks internally
def update_hfield(self, hfieldid: int):
sim = self._sim()
sim = self._get_sim()
if sim is not None:
sim.update_hfield(hfieldid) # locks internally and blocks until done
def update_mesh(self, meshid: int):
sim = self._sim()
sim = self._get_sim()
if sim is not None:
sim.update_mesh(meshid) # locks internally and blocks until done
def update_texture(self, texid: int):
sim = self._sim()
sim = self._get_sim()
if sim is not None:
sim.update_texture(texid) # locks internally and blocks until done
@@ -174,9 +182,11 @@ def _reload(
) -> Optional[Tuple[mujoco.MjModel, mujoco.MjData]]:
"""Internal function for reloading a model in the viewer."""
try:
simulate.load_message('') # path is unknown at this point
load_tuple = loader()
except Exception as e: # pylint: disable=broad-except
simulate.load_error = str(e)
simulate.load_message_clear()
else:
m, d = load_tuple[:2]
@@ -235,6 +245,7 @@ def _physics_loop(simulate: _Simulate, loader: Optional[_InternalLoaderType]):
if m is not None:
assert d is not None
if simulate.run:
stepped = False
# Record CPU time at start of iteration.
startcpu = time.time()
@@ -273,6 +284,7 @@ def _physics_loop(simulate: _Simulate, loader: Optional[_InternalLoaderType]):
# Run single step, let next iteration deal with timing.
mujoco.mj_step(m, d)
stepped = True
# In-sync: step until ahead of cpu.
else:
@@ -290,14 +302,21 @@ def _physics_loop(simulate: _Simulate, loader: Optional[_InternalLoaderType]):
# Call mj_step.
mujoco.mj_step(m, d)
stepped = True
# Break if reset.
if d.time < prevsim:
break
# save current state to history buffer
if (stepped):
simulate.add_to_history()
else: # simulate.run is False: GUI is paused.
# Run mj_forward, to update rendering and joint sliders.
mujoco.mj_forward(m, d)
simulate.speed_changed = True
def _launch_internal(
@@ -308,6 +327,8 @@ def _launch_internal(
loader: Optional[_InternalLoaderType] = None,
handle_return: Optional['queue.Queue[Handle]'] = None,
key_callback: Optional[KeyCallbackType] = None,
show_left_ui: bool = True,
show_right_ui: bool = True,
) -> None:
"""Internal API, so that the public API has more readable type annotations."""
if model is None and data is not None:
@@ -329,14 +350,19 @@ def _launch_internal(
loader = _loader
if model and not run_physics_thread:
scn = mujoco.MjvScene(model, _Simulate.MAX_GEOM)
else:
scn = mujoco.MjvScene()
cam = mujoco.MjvCamera()
opt = mujoco.MjvOption()
pert = mujoco.MjvPerturb()
simulate = _Simulate(scn, cam, opt, pert, run_physics_thread, key_callback)
if model and not run_physics_thread:
user_scn = mujoco.MjvScene(model, _Simulate.MAX_GEOM)
else:
user_scn = None
simulate = _Simulate(
cam, opt, pert, user_scn, run_physics_thread, key_callback
)
simulate.ui0_enable = show_left_ui
simulate.ui1_enable = show_right_ui
# Initialize GLFW if not using mjpython.
if _MJPYTHON is None:
@@ -346,8 +372,9 @@ def _launch_internal(
notify_loaded = None
if handle_return:
notify_loaded = (
lambda: handle_return.put_nowait(Handle(simulate, scn, cam, opt, pert)))
notify_loaded = lambda: handle_return.put_nowait(
Handle(simulate, cam, opt, pert, user_scn)
)
if run_physics_thread:
side_thread = threading.Thread(
@@ -371,13 +398,23 @@ def _launch_internal(
simulate.destroy()
def launch(model: Optional[mujoco.MjModel] = None,
data: Optional[mujoco.MjData] = None,
*,
loader: Optional[LoaderType] = None) -> None:
def launch(
model: Optional[mujoco.MjModel] = None,
data: Optional[mujoco.MjData] = None,
*,
loader: Optional[LoaderType] = None,
show_left_ui: bool = True,
show_right_ui: bool = True,
) -> None:
"""Launches the Simulate GUI."""
_launch_internal(
model, data, run_physics_thread=True, loader=loader)
model,
data,
run_physics_thread=True,
loader=loader,
show_left_ui=show_left_ui,
show_right_ui=show_right_ui,
)
def launch_from_path(path: str) -> None:
@@ -390,6 +427,8 @@ def launch_passive(
data: mujoco.MjData,
*,
key_callback: Optional[KeyCallbackType] = None,
show_left_ui: bool = True,
show_right_ui: bool = True,
) -> Handle:
"""Launches a passive Simulate GUI without blocking the running thread."""
if not isinstance(model, mujoco.MjModel):
@@ -411,6 +450,8 @@ def launch_passive(
run_physics_thread=False,
handle_return=handle_return,
key_callback=key_callback,
show_left_ui=show_left_ui,
show_right_ui=show_right_ui,
),
)
thread.daemon = True
@@ -420,7 +461,14 @@ def launch_passive(
raise RuntimeError(
'`launch_passive` requires that the Python script be run under '
'`mjpython` on macOS')
_MJPYTHON.launch_on_ui_thread(model, data, handle_return, key_callback)
_MJPYTHON.launch_on_ui_thread(
model,
data,
handle_return,
key_callback,
show_left_ui,
show_right_ui,
)
return handle_return.get()