Merge remote-tracking branch 'origin/main' into tarik/mjx-warp-nested-dataclass-methods
# Conflicts: # mjx/mujoco/mjx/warp/types.py
This commit is contained in:
@@ -601,14 +601,7 @@ Get name of object with the specified :ref:`mjtObj` type and id, returns ``NULL`
|
||||
|
||||
.. mujoco-include:: mj_fullM
|
||||
|
||||
Convert sparse inertia matrix ``M`` into full (i.e. dense) matrix.
|
||||
|br| ``dst`` must be of size ``nv x nv``, ``M`` must be of the same structure as ``mjData.qM``.
|
||||
|
||||
The ``mjData`` members ``qM`` and ``M`` represent the same matrix in different formats; the former is unique to
|
||||
MuJoCo, the latter is standard Compressed Sparse Row (lower triangle only). The :math:`L^T D L` factor of the inertia
|
||||
matrix ``mjData.qLD`` uses the same CSR format as ``mjData.M``. See
|
||||
`engine_support_test <https://github.com/google-deepmind/mujoco/blob/main/test/engine/engine_support_test.cc>`__ for
|
||||
pedagogical examples.
|
||||
Convert sparse inertia matrix into full (i.e. dense) matrix.
|
||||
|
||||
.. _mj_mulM:
|
||||
|
||||
@@ -1980,24 +1973,6 @@ Error and memory
|
||||
|
||||
Main error function; does not return to caller.
|
||||
|
||||
.. _mju_error_i:
|
||||
|
||||
`mju_error_i <#mju_error_i>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mju_error_i
|
||||
|
||||
Deprecated: use mju_error.
|
||||
|
||||
.. _mju_error_s:
|
||||
|
||||
`mju_error_s <#mju_error_s>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mju_error_s
|
||||
|
||||
Deprecated: use mju_error.
|
||||
|
||||
.. _mju_warning:
|
||||
|
||||
`mju_warning <#mju_warning>`__
|
||||
@@ -2007,24 +1982,6 @@ Deprecated: use mju_error.
|
||||
|
||||
Main warning function; returns to caller.
|
||||
|
||||
.. _mju_warning_i:
|
||||
|
||||
`mju_warning_i <#mju_warning_i>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mju_warning_i
|
||||
|
||||
Deprecated: use mju_warning.
|
||||
|
||||
.. _mju_warning_s:
|
||||
|
||||
`mju_warning_s <#mju_warning_s>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mju_warning_s
|
||||
|
||||
Deprecated: use mju_warning.
|
||||
|
||||
.. _mju_clearHandlers:
|
||||
|
||||
`mju_clearHandlers <#mju_clearHandlers>`__
|
||||
|
||||
@@ -342,17 +342,6 @@ found, the function will return ``distmax`` and ``fromto``, if given, will be se
|
||||
As explained in :ref:`Collision Detection<coDistance>`, distances are inaccurate when using the
|
||||
:ref:`legacy CCD pipeline<coCCD>`, and its use is discouraged.
|
||||
|
||||
.. _mj_fullM:
|
||||
|
||||
Convert sparse inertia matrix ``M`` into full (i.e. dense) matrix.
|
||||
|br| ``dst`` must be of size ``nv x nv``, ``M`` must be of the same structure as ``mjData.qM``.
|
||||
|
||||
The ``mjData`` members ``qM`` and ``M`` represent the same matrix in different formats; the former is unique to
|
||||
MuJoCo, the latter is standard Compressed Sparse Row (lower triangle only). The :math:`L^T D L` factor of the inertia
|
||||
matrix ``mjData.qLD`` uses the same CSR format as ``mjData.M``. See
|
||||
`engine_support_test <https://github.com/google-deepmind/mujoco/blob/main/test/engine/engine_support_test.cc>`__ for
|
||||
pedagogical examples.
|
||||
|
||||
.. _mj_mulM:
|
||||
|
||||
This function multiplies the joint-space inertia matrix stored in ``mjData.M`` by a vector.
|
||||
|
||||
@@ -3137,7 +3137,7 @@ Attributes may be applied or ignored depending on the lighting model being used.
|
||||
.. _body-light-directional:
|
||||
|
||||
:at:`directional`: :at-val:`[false, true], "false"`
|
||||
This is a deprecated legacy attribute. Please use :ref:`light <body-light-type>` type instead. If set to "true", and
|
||||
This is a deprecated legacy attribute. Please use light :ref:`type <body-light-type>` instead. If set to "true", and
|
||||
no type is specified, this will change the light type to be directional.
|
||||
|
||||
.. _body-light-castshadow:
|
||||
|
||||
+9
-5
@@ -21,16 +21,20 @@ General
|
||||
:class: attention
|
||||
|
||||
- The header file ``mjthread.h`` was removed along with the old engine threading API.
|
||||
|
||||
**Migration:** Use :ref:`mju_threadpool` to set number of worker threads for the engine.
|
||||
|
||||
|br| **Migration:** Use :ref:`mju_threadpool` to set number of worker threads for the engine.
|
||||
- Moved island sparse matrix construction from :ref:`mj_island` (single threaded) into :ref:`mj_fwdConstraint`
|
||||
(multi-threaded). The island-specific matrices ``iM, iLD, iefc_J`` were removed from the arena and are now
|
||||
allocated on the stack.
|
||||
|
||||
- Following the introduction of the :ref:`diagexact<option-flag-diagexact>` flag, the ``mjData`` field
|
||||
``efc_diagApprox`` was renamed to ``efc_diagA``, as it can now be either the exact or approximate diagonal of
|
||||
the :math:`A` ("Delassus") matrix.
|
||||
- The deprecated functions ``mju_{error,warning}_{i,s}`` have been removed.
|
||||
|
||||
- Changed the signature of :ref:`mj_fullM` from ``mj_fullM(m, dst, M)`` to ``mj_fullM(m, d, dst)`` as part of the
|
||||
planned deprecation of ``mjData.qM`` in favor of the CSR-format ``mjData.M``.
|
||||
|
||||
**Migration:** For inertia matrix conversion, replace ``mj_fullM(m, dst, d->qM)`` with ``mj_fullM(m, d, dst)`` or
|
||||
``mju_sym2dense(dst, d->M, m->nv, m->M_rownnz, m->M_rowadr, m->M_colind)``.
|
||||
|
||||
Bug fixes
|
||||
^^^^^^^^^
|
||||
@@ -2145,7 +2149,7 @@ General
|
||||
<https://github.com/google-deepmind/mujoco/blob/main/model/humanoid/humanoid100.xml>`__ model, which previously
|
||||
required ~500,000 ``mjtNum``'s, now only requires ~6000. Very large models can now load and run with the CG solver.
|
||||
#. Modified :ref:`mju_error` and :ref:`mju_warning` to be variadic functions (support for printf-like arguments). The
|
||||
functions :ref:`mju_error_i`, :ref:`mju_error_s`, :ref:`mju_warning_i`, and :ref:`mju_warning_s` are now deprecated.
|
||||
functions ``mju_error_i``, ``mju_error_s``, ``mju_warning_i``, and ``mju_warning_s`` are now deprecated.
|
||||
#. Implemented a performant ``mju_sqrMatTDSparse`` function that doesn't require dense memory allocation.
|
||||
#. Added ``mj_stackAllocInt`` to get correct size for allocating ints on mjData stack. Reducing stack memory usage
|
||||
by 10% - 15%.
|
||||
|
||||
@@ -3320,7 +3320,7 @@ void mj_jacDot(const mjModel* m, const mjData* d, mjtNum* jacp, mjtNum* jacr,
|
||||
void mj_angmomMat(const mjModel* m, mjData* d, mjtNum* mat, int body);
|
||||
int mj_name2id(const mjModel* m, int type, const char* name);
|
||||
const char* mj_id2name(const mjModel* m, int type, int id);
|
||||
void mj_fullM(const mjModel* m, mjtNum* dst, const mjtNum* M);
|
||||
void mj_fullM(const mjModel* m, const mjData* d, mjtNum* dst);
|
||||
void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec);
|
||||
void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec);
|
||||
void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, int* rownnz, int* rowadr, int* colind);
|
||||
@@ -3457,11 +3457,7 @@ void mjui_update(int section, int item, const mjUI* ui,
|
||||
mjuiItem* mjui_event(mjUI* ui, mjuiState* state, const mjrContext* con);
|
||||
void mjui_render(mjUI* ui, const mjuiState* state, const mjrContext* con);
|
||||
void mju_error(const char* msg, ...) mjPRINTFLIKE(1, 2);
|
||||
void mju_error_i(const char* msg, int i);
|
||||
void mju_error_s(const char* msg, const char* text);
|
||||
void mju_warning(const char* msg, ...) mjPRINTFLIKE(1, 2);
|
||||
void mju_warning_i(const char* msg, int i);
|
||||
void mju_warning_s(const char* msg, const char* text);
|
||||
void mju_clearHandlers(void);
|
||||
void* mju_malloc(size_t size);
|
||||
void mju_free(void* ptr);
|
||||
|
||||
@@ -323,6 +323,8 @@ class MjcPhysicsCollisionAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// MARGIN
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:contactMargin and newton:contactGap instead.
|
||||
///
|
||||
/// Geometric inflation of the geom surface for the purpose of contact force
|
||||
/// generation.
|
||||
///
|
||||
@@ -348,6 +350,8 @@ class MjcPhysicsCollisionAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// GAP
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:contactGap instead.
|
||||
///
|
||||
/// Additional contact detection buffer beyond margin. Contacts are detected
|
||||
/// at distance margin + gap but forces are only generated at distance margin.
|
||||
///
|
||||
|
||||
@@ -43,12 +43,20 @@ class SdfAssetPath;
|
||||
/// \class MjcPhysicsEqualityJointAPI
|
||||
///
|
||||
/// API providing extension attributes to represent equality/joint constraints.
|
||||
/// This API is applied to a joint prim which acts as the constrained joint
|
||||
/// (joint1 in MuJoCo terminology). The target relationship points to another
|
||||
/// joint prim which is the reference joint (joint2 in MuJoCo terminology). The
|
||||
/// constrained joint's position or angle is constrained to be a quartic
|
||||
/// polynomial of the reference joint's position or angle. Only scalar joint
|
||||
/// types (slide and hinge) can be used.
|
||||
///
|
||||
/// This API is applied to a joint prim which acts as the follower (joint0). The
|
||||
/// leader joint (joint1) is specified via the newton:mimicJoint relationship
|
||||
/// inherited from NewtonMimicAPI.
|
||||
///
|
||||
/// The follower's position or angle is constrained to be a quartic polynomial
|
||||
/// of the leader's position or angle: joint0 = coef0 + coef1*(joint1) +
|
||||
/// coef2*(joint1)^2 + coef3*(joint1)^3 + coef4*(joint1)^4
|
||||
///
|
||||
/// The constant (coef0) and linear (coef1) coefficients are provided by
|
||||
/// NewtonMimicAPI as newton:mimicCoef0 and newton:mimicCoef1. The higher-order
|
||||
/// coefficients (coef2-coef4) are provided by this API.
|
||||
///
|
||||
/// Only scalar joint types (slide and hinge) can be used.
|
||||
///
|
||||
class MjcPhysicsEqualityJointAPI : public UsdAPISchemaBase {
|
||||
public:
|
||||
@@ -150,10 +158,60 @@ class MjcPhysicsEqualityJointAPI : public UsdAPISchemaBase {
|
||||
MJCPHYSICS_API
|
||||
const TfType& _GetTfType() const override;
|
||||
|
||||
public:
|
||||
// --------------------------------------------------------------------- //
|
||||
// SOLIMP
|
||||
// --------------------------------------------------------------------- //
|
||||
/// Constraint solver parameter for equality constraint simulation.
|
||||
///
|
||||
/// | ||
|
||||
/// | -- | -- |
|
||||
/// | Declaration | `uniform double[] mjc:solimp = [0.9, 0.95, 0.001, 0.5, 2]`
|
||||
/// | | C++ Type | VtArray<double> | | \ref Usd_Datatypes "Usd Type" |
|
||||
/// SdfValueTypeNames->DoubleArray | | \ref SdfVariability "Variability" |
|
||||
/// SdfVariabilityUniform |
|
||||
MJCPHYSICS_API
|
||||
UsdAttribute GetSolImpAttr() const;
|
||||
|
||||
/// See GetSolImpAttr(), and also
|
||||
/// \ref Usd_Create_Or_Get_Property for when to use Get vs Create.
|
||||
/// If specified, author \p defaultValue as the attribute's default,
|
||||
/// sparsely (when it makes sense to do so) if \p writeSparsely is \c true -
|
||||
/// the default for \p writeSparsely is \c false.
|
||||
MJCPHYSICS_API
|
||||
UsdAttribute CreateSolImpAttr(VtValue const& defaultValue = VtValue(),
|
||||
bool writeSparsely = false) const;
|
||||
|
||||
public:
|
||||
// --------------------------------------------------------------------- //
|
||||
// SOLREF
|
||||
// --------------------------------------------------------------------- //
|
||||
/// Constraint solver parameter for equality constraint simulation.
|
||||
///
|
||||
/// | ||
|
||||
/// | -- | -- |
|
||||
/// | Declaration | `uniform double[] mjc:solref = [0.02, 1]` |
|
||||
/// | C++ Type | VtArray<double> |
|
||||
/// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->DoubleArray |
|
||||
/// | \ref SdfVariability "Variability" | SdfVariabilityUniform |
|
||||
MJCPHYSICS_API
|
||||
UsdAttribute GetSolRefAttr() const;
|
||||
|
||||
/// See GetSolRefAttr(), and also
|
||||
/// \ref Usd_Create_Or_Get_Property for when to use Get vs Create.
|
||||
/// If specified, author \p defaultValue as the attribute's default,
|
||||
/// sparsely (when it makes sense to do so) if \p writeSparsely is \c true -
|
||||
/// the default for \p writeSparsely is \c false.
|
||||
MJCPHYSICS_API
|
||||
UsdAttribute CreateSolRefAttr(VtValue const& defaultValue = VtValue(),
|
||||
bool writeSparsely = false) const;
|
||||
|
||||
public:
|
||||
// --------------------------------------------------------------------- //
|
||||
// COEF0
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:mimicCoef0 instead.
|
||||
///
|
||||
/// Constant coefficient a0 of the quartic polynomial. The constraint is:
|
||||
/// y = y0 + a0 + a1*(x-x0) + a2*(x-x0)^2 + a3*(x-x0)^3 + a4*(x-x0)^4.
|
||||
///
|
||||
@@ -179,6 +237,8 @@ class MjcPhysicsEqualityJointAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// COEF1
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:mimicCoef1 instead.
|
||||
///
|
||||
/// Linear coefficient a1 of the quartic polynomial.
|
||||
///
|
||||
/// | ||
|
||||
@@ -271,6 +331,22 @@ class MjcPhysicsEqualityJointAPI : public UsdAPISchemaBase {
|
||||
UsdAttribute CreateCoef4Attr(VtValue const& defaultValue = VtValue(),
|
||||
bool writeSparsely = false) const;
|
||||
|
||||
public:
|
||||
// --------------------------------------------------------------------- //
|
||||
// MJCTARGET
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:mimicJoint instead.
|
||||
///
|
||||
/// Secondary target of the equality constraint (the leader/reference joint).
|
||||
///
|
||||
MJCPHYSICS_API
|
||||
UsdRelationship GetMjcTargetRel() const;
|
||||
|
||||
/// See GetMjcTargetRel(), and also
|
||||
/// \ref Usd_Create_Or_Get_Property for when to use Get vs Create
|
||||
MJCPHYSICS_API
|
||||
UsdRelationship CreateMjcTargetRel() const;
|
||||
|
||||
public:
|
||||
// ===================================================================== //
|
||||
// Feel free to add custom code below this line, it will be preserved by
|
||||
|
||||
@@ -42,6 +42,9 @@ class SdfAssetPath;
|
||||
|
||||
/// \class MjcPhysicsMaterialAPI
|
||||
///
|
||||
/// DEPRECATED: Use NewtonMaterialAPI instead. All attributes on this API have
|
||||
/// been superseded by Newton equivalents.
|
||||
///
|
||||
/// API providing extension attributes to represent physical MuJoCo materials.
|
||||
///
|
||||
class MjcPhysicsMaterialAPI : public UsdAPISchemaBase {
|
||||
@@ -148,6 +151,8 @@ class MjcPhysicsMaterialAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// TORSIONALFRICTION
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:torsionalFriction instead.
|
||||
///
|
||||
/// Friction value acting around contact normal.
|
||||
///
|
||||
/// | ||
|
||||
@@ -173,6 +178,8 @@ class MjcPhysicsMaterialAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// ROLLINGFRICTION
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:rollingFriction instead.
|
||||
///
|
||||
/// Friction value acting around both axes on the contact tangent plane.
|
||||
///
|
||||
/// | ||
|
||||
|
||||
@@ -179,6 +179,8 @@ class MjcPhysicsMeshCollisionAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// MAXHULLVERT
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:maxHullVertices instead.
|
||||
///
|
||||
/// Sets an upper limit on the number of vertices in the meshes convex hull.
|
||||
/// The default value of -1 means unlimited.
|
||||
///
|
||||
|
||||
@@ -152,6 +152,8 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// TIMESTEP
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:timeStepsPerSecond instead.
|
||||
///
|
||||
/// Controls the timestep in seconds used by MuJoCo.
|
||||
///
|
||||
/// | ||
|
||||
@@ -498,6 +500,8 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// ITERATIONS
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:maxSolverIterations instead.
|
||||
///
|
||||
/// Maximum number of iterations of the constraint solver.
|
||||
///
|
||||
/// | ||
|
||||
@@ -942,6 +946,8 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase {
|
||||
// --------------------------------------------------------------------- //
|
||||
// GRAVITYFLAG
|
||||
// --------------------------------------------------------------------- //
|
||||
/// DEPRECATED: Use newton:gravityEnabled instead.
|
||||
///
|
||||
/// Enables the application of gravitational acceleration as defined in
|
||||
/// mjOption.
|
||||
///
|
||||
|
||||
@@ -656,7 +656,7 @@ struct MjcPhysicsTokensType {
|
||||
const TfToken mjcSliderSite;
|
||||
/// \brief "mjc:solimp"
|
||||
///
|
||||
/// MjcPhysicsCollisionAPI, MjcPhysicsEqualityAPI
|
||||
/// MjcPhysicsCollisionAPI, MjcPhysicsEqualityAPI, MjcPhysicsEqualityJointAPI
|
||||
const TfToken mjcSolimp;
|
||||
/// \brief "mjc:solimpfriction"
|
||||
///
|
||||
@@ -672,7 +672,7 @@ struct MjcPhysicsTokensType {
|
||||
const TfToken mjcSolmix;
|
||||
/// \brief "mjc:solref"
|
||||
///
|
||||
/// MjcPhysicsCollisionAPI, MjcPhysicsEqualityAPI
|
||||
/// MjcPhysicsCollisionAPI, MjcPhysicsEqualityAPI, MjcPhysicsEqualityJointAPI
|
||||
const TfToken mjcSolref;
|
||||
/// \brief "mjc:solreffriction"
|
||||
///
|
||||
@@ -700,7 +700,7 @@ struct MjcPhysicsTokensType {
|
||||
const TfToken mjcStiffness;
|
||||
/// \brief "mjc:target"
|
||||
///
|
||||
/// MjcPhysicsActuator, MjcPhysicsEqualityAPI
|
||||
/// MjcPhysicsActuator, MjcPhysicsEqualityAPI, MjcPhysicsEqualityJointAPI
|
||||
const TfToken mjcTarget;
|
||||
/// \brief "mjc:torqueScale"
|
||||
///
|
||||
|
||||
+2
-14
@@ -598,8 +598,8 @@ MJAPI int mj_name2id(const mjModel* m, int type, const char* name);
|
||||
// Get name of object with the specified mjtObj type and id; return NULL if name not found.
|
||||
MJAPI const char* mj_id2name(const mjModel* m, int type, int id);
|
||||
|
||||
// Convert sparse inertia matrix M into full (i.e. dense) matrix.
|
||||
MJAPI void mj_fullM(const mjModel* m, mjtNum* dst, const mjtNum* M);
|
||||
// Convert sparse inertia matrix into full (i.e. dense) matrix.
|
||||
MJAPI void mj_fullM(const mjModel* m, const mjData* d, mjtNum* dst);
|
||||
|
||||
// Multiply vector by inertia matrix.
|
||||
MJAPI void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec);
|
||||
@@ -972,21 +972,9 @@ MJAPI void mjui_render(mjUI* ui, const mjuiState* state, const mjrContext* con);
|
||||
// Main error function; does not return to caller.
|
||||
MJAPI void mju_error(const char* msg, ...) mjPRINTFLIKE(1, 2);
|
||||
|
||||
// Deprecated: use mju_error.
|
||||
MJAPI void mju_error_i(const char* msg, int i);
|
||||
|
||||
// Deprecated: use mju_error.
|
||||
MJAPI void mju_error_s(const char* msg, const char* text);
|
||||
|
||||
// Main warning function; returns to caller.
|
||||
MJAPI void mju_warning(const char* msg, ...) mjPRINTFLIKE(1, 2);
|
||||
|
||||
// Deprecated: use mju_warning.
|
||||
MJAPI void mju_warning_i(const char* msg, int i);
|
||||
|
||||
// Deprecated: use mju_warning.
|
||||
MJAPI void mju_warning_s(const char* msg, const char* text);
|
||||
|
||||
// Clear user error and memory handlers.
|
||||
MJAPI void mju_clearHandlers(void);
|
||||
|
||||
|
||||
@@ -17,23 +17,21 @@
|
||||
import ast
|
||||
import dataclasses
|
||||
import enum
|
||||
import inspect
|
||||
import logging
|
||||
import textwrap
|
||||
import typing
|
||||
from typing import Any, Callable, Dict, List, Optional, Set
|
||||
|
||||
from absl import app
|
||||
from absl import flags
|
||||
from etils import epath
|
||||
import numpy as np
|
||||
import warp as wp
|
||||
|
||||
import mujoco
|
||||
from mujoco.mjx.codegen import file
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
import numpy as np
|
||||
import warp as wp
|
||||
from mujoco.mjx.third_party.warp._src.jax_experimental import ffi
|
||||
|
||||
|
||||
_MJX_WARP_TYPES_OUT_FPATH = flags.DEFINE_string(
|
||||
'mjx_warp_types_out_path',
|
||||
'third_party/py/mujoco/mjx/warp/types.py',
|
||||
@@ -47,7 +45,6 @@ _MJX_TYPES_PATH = flags.DEFINE_string(
|
||||
)
|
||||
|
||||
_DATA_SHAPE_PROPERTY_FIELD = 'cacc'
|
||||
_DOCSTRING_LINE_LENGTH = 80
|
||||
_DUMMY_XML = """
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
@@ -64,29 +61,6 @@ _DUMMY_XML = """
|
||||
"""
|
||||
|
||||
|
||||
def _format_docstring(docstring: str) -> str:
|
||||
"""Wraps docstring body lines to match checked-in generated files."""
|
||||
formatted_lines = []
|
||||
for line in docstring.splitlines():
|
||||
if not line.strip():
|
||||
formatted_lines.append(line)
|
||||
continue
|
||||
|
||||
indent = line[: len(line) - len(line.lstrip())]
|
||||
continuation_indent = indent + (' ' if indent else '')
|
||||
formatted_lines.append(
|
||||
textwrap.fill(
|
||||
line,
|
||||
width=_DOCSTRING_LINE_LENGTH,
|
||||
subsequent_indent=continuation_indent,
|
||||
break_long_words=False,
|
||||
break_on_hyphens=False,
|
||||
)
|
||||
)
|
||||
|
||||
return '\n'.join(formatted_lines)
|
||||
|
||||
|
||||
def _to_py_string(value, indent=0):
|
||||
"""Converts a dictionary/set/tuple/type to a Python code string."""
|
||||
indent_str = ' ' * indent
|
||||
@@ -135,10 +109,8 @@ def _get_target_annotation_node(
|
||||
if annotation == np.ndarray:
|
||||
return _ast_parse_type('np.ndarray')
|
||||
|
||||
if (
|
||||
isinstance(annotation, wp.array)
|
||||
or type(annotation).__name__ == '_ArrayAnnotation'
|
||||
):
|
||||
if (isinstance(annotation, wp.array) or
|
||||
type(annotation).__name__ == '_ArrayAnnotation'):
|
||||
return _ast_parse_type('jax.Array')
|
||||
|
||||
if annotation in (int, float, bool):
|
||||
@@ -194,10 +166,9 @@ def _get_annotations_recursive(
|
||||
flattened = {}
|
||||
for key, annotation in annotations.items():
|
||||
full_key = f'{prefix}{key}'
|
||||
if (
|
||||
hasattr(annotation, '__annotations__')
|
||||
and 'mujoco_warp' in annotation.__module__
|
||||
):
|
||||
if hasattr(
|
||||
annotation, '__annotations__'
|
||||
) and 'mujoco_warp' in annotation.__module__:
|
||||
nested = _get_annotations_recursive(
|
||||
dict(annotation.__annotations__), prefix=f'{full_key}__'
|
||||
)
|
||||
@@ -310,12 +281,6 @@ else:
|
||||
Callback = None
|
||||
|
||||
PyTreeNode = mjx_dataclasses.PyTreeNode
|
||||
|
||||
|
||||
def _as_numpy_array(value):
|
||||
if hasattr(value, 'numpy'):
|
||||
return value.numpy()
|
||||
return np.asarray(value)
|
||||
'''
|
||||
target_fpath.write_text(header)
|
||||
|
||||
@@ -333,68 +298,6 @@ _FLATTEN_UNFLATTEN = """
|
||||
"""
|
||||
|
||||
|
||||
class _NumpyMethodAdapter(ast.NodeTransformer):
|
||||
"""Adapts copied Warp array methods to MJX numpy-backed fields."""
|
||||
|
||||
def _is_asarray_call(self, node: ast.Call) -> bool:
|
||||
return (
|
||||
isinstance(node.func, ast.Attribute)
|
||||
and isinstance(node.func.value, ast.Name)
|
||||
and node.func.value.id == 'np'
|
||||
and node.func.attr == 'asarray'
|
||||
and len(node.args) == 1
|
||||
and not node.keywords
|
||||
)
|
||||
|
||||
def _is_as_numpy_array_call(self, node: ast.AST) -> bool:
|
||||
return (
|
||||
isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id == '_as_numpy_array'
|
||||
)
|
||||
|
||||
def visit_Call(self, node: ast.Call) -> ast.AST: # pylint: disable=invalid-name
|
||||
self.generic_visit(node)
|
||||
if (
|
||||
isinstance(node.func, ast.Attribute)
|
||||
and node.func.attr == 'numpy'
|
||||
and not node.args
|
||||
and not node.keywords
|
||||
):
|
||||
return ast.copy_location(
|
||||
ast.Call(
|
||||
func=ast.Name(id='_as_numpy_array', ctx=ast.Load()),
|
||||
args=[node.func.value],
|
||||
keywords=[],
|
||||
),
|
||||
node,
|
||||
)
|
||||
if self._is_asarray_call(node) and self._is_as_numpy_array_call(
|
||||
node.args[0]
|
||||
):
|
||||
return ast.copy_location(node.args[0], node)
|
||||
return node
|
||||
|
||||
|
||||
def _get_explicit_method_nodes(cls: Any) -> List[ast.FunctionDef]:
|
||||
"""Returns explicit methods from a source class, adapted for MJX fields."""
|
||||
source = textwrap.dedent(inspect.getsource(cls))
|
||||
tree = ast.parse(source)
|
||||
class_def = next(node for node in tree.body if isinstance(node, ast.ClassDef))
|
||||
|
||||
methods: List[ast.FunctionDef] = []
|
||||
for node in class_def.body:
|
||||
if not isinstance(node, ast.FunctionDef):
|
||||
continue
|
||||
if node.name in {'tree_flatten', 'tree_unflatten'}:
|
||||
continue
|
||||
methods.append(
|
||||
typing.cast(ast.FunctionDef, _NumpyMethodAdapter().visit(node))
|
||||
)
|
||||
|
||||
return methods
|
||||
|
||||
|
||||
def write_nested_dataclass(target_fpath: epath.Path, cls: Any):
|
||||
new_class_body = _build_new_class_body_ast(
|
||||
set(cls.__annotations__.keys()),
|
||||
@@ -402,89 +305,19 @@ def write_nested_dataclass(target_fpath: epath.Path, cls: Any):
|
||||
dict(cls.__annotations__),
|
||||
add_docstring=False,
|
||||
)
|
||||
new_class_body.extend(_get_explicit_method_nodes(cls))
|
||||
cls_str = '\n'.join(
|
||||
textwrap.indent(ast.unparse(node), ' ') for node in new_class_body
|
||||
)
|
||||
cls_str = '\n'.join([' ' + ast.unparse(node) for node in new_class_body])
|
||||
cls_str = cls_str.replace('jax.Array', 'np.ndarray')
|
||||
docstring = _format_docstring(cls.__doc__ or '')
|
||||
with target_fpath.open('a') as f:
|
||||
f.write(f'''
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@tree_util.register_pytree_node_class
|
||||
class {cls.__name__}:
|
||||
"""{docstring}"""
|
||||
"""{cls.__doc__}"""
|
||||
{cls_str}
|
||||
{_FLATTEN_UNFLATTEN}
|
||||
''')
|
||||
|
||||
|
||||
def _get_class_name(line: str) -> str | None:
|
||||
"""Returns the class name from a generated class definition line."""
|
||||
if not line.startswith('class '):
|
||||
return None
|
||||
return line.removeprefix('class ').split('(', maxsplit=1)[0]
|
||||
|
||||
|
||||
def _get_compact_derived_docstring_classes(
|
||||
target_fpath: epath.Path,
|
||||
) -> Set[str]:
|
||||
"""Finds generated classes with no blank line after their docstring."""
|
||||
compact_classes = set()
|
||||
if not target_fpath.exists():
|
||||
return compact_classes
|
||||
|
||||
lines = target_fpath.read_text().splitlines()
|
||||
for i, line in enumerate(lines[:-2]):
|
||||
cls_name = _get_class_name(line)
|
||||
if cls_name is None:
|
||||
continue
|
||||
if (
|
||||
'(PyTreeNode)' in line
|
||||
and lines[i + 1].startswith(' """Derived fields from ')
|
||||
and lines[i + 1].rstrip().endswith('."""')
|
||||
and lines[i + 2].startswith(' ')
|
||||
and ':' in lines[i + 2]
|
||||
):
|
||||
compact_classes.add(cls_name)
|
||||
|
||||
return compact_classes
|
||||
|
||||
|
||||
def _restore_compact_derived_docstring_spacing(
|
||||
target_fpath: epath.Path,
|
||||
compact_classes: Set[str],
|
||||
):
|
||||
"""Restores compact generated PyTreeNode field class spacing."""
|
||||
if not compact_classes:
|
||||
return
|
||||
|
||||
lines = target_fpath.read_text().splitlines(keepends=True)
|
||||
result = []
|
||||
i = 0
|
||||
current_class = None
|
||||
while i < len(lines):
|
||||
cls_name = _get_class_name(lines[i])
|
||||
if cls_name is not None:
|
||||
current_class = cls_name
|
||||
|
||||
result.append(lines[i])
|
||||
if (
|
||||
current_class in compact_classes
|
||||
and lines[i].startswith(' """Derived fields from ')
|
||||
and lines[i].rstrip().endswith('."""')
|
||||
and i + 2 < len(lines)
|
||||
and lines[i + 1].strip() == ''
|
||||
and lines[i + 2].startswith(' ')
|
||||
and ':' in lines[i + 2]
|
||||
):
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
|
||||
target_fpath.write_text(''.join(result))
|
||||
|
||||
|
||||
def _get_meta_fields(cls_name: str) -> Set[str]:
|
||||
"""Returns the set of fields that should be meta-fields in the pytree."""
|
||||
m = mujoco.MjModel.from_xml_string(_DUMMY_XML)
|
||||
@@ -597,10 +430,8 @@ def _get_fields_with_cond(
|
||||
if f.type in (int, float, bool) and add_static:
|
||||
s.add(prefix + f.name)
|
||||
continue
|
||||
if not (
|
||||
isinstance(f.type, wp.array)
|
||||
or type(f.type).__name__ == '_ArrayAnnotation'
|
||||
):
|
||||
if not (isinstance(f.type, wp.array) or
|
||||
type(f.type).__name__ == '_ArrayAnnotation'):
|
||||
continue
|
||||
if cond_fn(attr):
|
||||
s.add(prefix + f.name)
|
||||
@@ -639,10 +470,8 @@ batching.register_vmappable(DataWarp, int, int, _to_elt, _from_elt, None)
|
||||
|
||||
def _is_ffi_compatible(wp_type: Any) -> bool:
|
||||
"""Returns True if the type is an array, scalar, or variadic tuple."""
|
||||
if (
|
||||
isinstance(wp_type, wp.array)
|
||||
or type(wp_type).__name__ == '_ArrayAnnotation'
|
||||
):
|
||||
if (isinstance(wp_type, wp.array) or
|
||||
type(wp_type).__name__ == '_ArrayAnnotation'):
|
||||
return True
|
||||
if wp_type in wp._src.types.value_types:
|
||||
return True
|
||||
@@ -719,9 +548,6 @@ def main(argv):
|
||||
base_path = file.get_base_path()
|
||||
target_fpath = base_path / _MJX_WARP_TYPES_OUT_FPATH.value
|
||||
mjx_types_fpath = base_path / _MJX_TYPES_PATH.value
|
||||
compact_derived_docstring_classes = _get_compact_derived_docstring_classes(
|
||||
target_fpath
|
||||
)
|
||||
|
||||
write_header(target_fpath)
|
||||
# TODO(btaba): consider automated grabbing of nested dataclasses from mjwarp.
|
||||
@@ -730,9 +556,7 @@ def main(argv):
|
||||
|
||||
write_core_cls('Statistic', target_fpath, mjx_types_fpath, set_diff=False)
|
||||
write_core_cls(
|
||||
'Option',
|
||||
target_fpath,
|
||||
mjx_types_fpath,
|
||||
'Option', target_fpath, mjx_types_fpath,
|
||||
extra_annotations={'graph_mode': ffi.GraphMode},
|
||||
)
|
||||
write_core_cls('Model', target_fpath, mjx_types_fpath)
|
||||
@@ -743,9 +567,6 @@ def main(argv):
|
||||
|
||||
file.write_license(target_fpath)
|
||||
file.format_file(target_fpath)
|
||||
_restore_compact_derived_docstring_spacing(
|
||||
target_fpath, compact_derived_docstring_classes
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+10
-2
@@ -3058,11 +3058,11 @@ def create_render_context(
|
||||
|
||||
# Locate skybox texture
|
||||
skybox_tex_ids = np.nonzero(mjm.tex_type == mujoco.mjtTexture.mjTEXTURE_SKYBOX)[0] if mjm.ntex else np.array([], dtype=int)
|
||||
if render_skybox:
|
||||
assert skybox_tex_ids.size > 0, "render_skybox=True but the model has no texture with type mjTEXTURE_SKYBOX"
|
||||
if render_skybox and skybox_tex_ids.size > 0:
|
||||
skybox_tex_id = int(skybox_tex_ids[0])
|
||||
skybox_face_width = int(mjm.tex_width[skybox_tex_id])
|
||||
else:
|
||||
render_skybox = False
|
||||
skybox_tex_id = -1
|
||||
skybox_face_width = 1
|
||||
|
||||
@@ -3157,6 +3157,13 @@ def create_render_context(
|
||||
|
||||
bvh_ngeom = len(geom_enabled_idx)
|
||||
|
||||
# Geom types present among enabled geoms, plus FLEX when flex primitives exist.
|
||||
# Used to statically eliminate unused intersection branches in the ray-cast kernels.
|
||||
geom_ray_types = set(int(t) for t in mjm.geom_type[geom_enabled_idx])
|
||||
if len(flex_geom_flexid) > 0:
|
||||
geom_ray_types.add(int(types.GeomType.FLEX))
|
||||
geom_ray_types = tuple(sorted(geom_ray_types))
|
||||
|
||||
rc = types.RenderContext(
|
||||
nrender=ncam,
|
||||
cam_res=cam_res_arr,
|
||||
@@ -3210,6 +3217,7 @@ def create_render_context(
|
||||
znear=znear,
|
||||
total_rays=int(total),
|
||||
enable_backface_culling=enable_backface_culling,
|
||||
geom_ray_types=geom_ray_types,
|
||||
)
|
||||
|
||||
bvh.build_scene_bvh(mjm, mjd, rc, nworld)
|
||||
|
||||
+338
-432
@@ -151,449 +151,346 @@ def sample_skybox(
|
||||
return wp.vec3(color[0], color[1], color[2])
|
||||
|
||||
|
||||
# TODO: Investigate combining cast_ray and cast_ray_first_hit
|
||||
@wp.func
|
||||
def cast_ray(
|
||||
# Model:
|
||||
geom_type: wp.array[int],
|
||||
geom_dataid: wp.array2d[int],
|
||||
geom_size: wp.array2d[wp.vec3],
|
||||
flex_vertadr: wp.array[int],
|
||||
flex_edge: wp.array[wp.vec2i],
|
||||
flex_radius: wp.array[float],
|
||||
# Data in:
|
||||
geom_xpos_in: wp.array2d[wp.vec3],
|
||||
geom_xmat_in: wp.array2d[wp.mat33],
|
||||
flexvert_xpos_in: wp.array2d[wp.vec3],
|
||||
# In:
|
||||
bvh_id: wp.uint64,
|
||||
group_root: int,
|
||||
worldid: int,
|
||||
bvh_ngeom: int,
|
||||
flex_bvh_ngeom: int,
|
||||
enabled_geom_ids: wp.array[int],
|
||||
mesh_bvh_id: wp.array[wp.uint64],
|
||||
hfield_bvh_id: wp.array[wp.uint64],
|
||||
flex_geom_flexid: wp.array[int],
|
||||
flex_geom_edgeid: wp.array[int],
|
||||
flex_bvh_id: wp.array[wp.uint64],
|
||||
flex_group_root: wp.array2d[int],
|
||||
ray_origin_world: wp.vec3,
|
||||
ray_dir_world: wp.vec3,
|
||||
cull_backfaces: bool,
|
||||
) -> Tuple[int, float, wp.vec3, float, float, int, int]:
|
||||
dist = float(MJ_MAXVAL)
|
||||
normal = wp.vec3(0.0, 0.0, 0.0)
|
||||
geom_id = int(-1)
|
||||
bary_u = float(0.0)
|
||||
bary_v = float(0.0)
|
||||
face_idx = int(-1)
|
||||
geom_mesh_id = int(-1)
|
||||
def _make_cast_ray(geom_ray_types: Tuple[int], first_hit: bool = False) -> wp.Function:
|
||||
"""Build a ray-cast func specialized to the geom types present in the scene.
|
||||
|
||||
query = wp.bvh_query_ray(bvh_id, ray_origin_world, ray_dir_world, group_root)
|
||||
bounds_nr = int(0)
|
||||
ngeom = bvh_ngeom + flex_bvh_ngeom
|
||||
geom_ray_types is the set of GeomType int values that actually occur, so the
|
||||
per-type intersection branches for absent types are eliminated at compile time
|
||||
via wp.static, avoiding the register pressure of unreachable code paths.
|
||||
|
||||
while wp.bvh_query_next(query, bounds_nr, dist):
|
||||
gi_global = bounds_nr
|
||||
local_id = gi_global - (worldid * ngeom)
|
||||
first_hit selects the variant (also resolved at compile time via wp.static):
|
||||
- False: full closest-hit cast. Returns the closest hit's full surface data.
|
||||
- True: any-hit cast (shadow rays). Uses the cheaper any-hit mesh/flex
|
||||
intersections and returns on the first hit within max_dist. The result is
|
||||
still the full tuple; callers test geom_id != -1 to detect a hit.
|
||||
"""
|
||||
|
||||
d = float(-1.0)
|
||||
hit_mesh_id = int(-1)
|
||||
u = float(0.0)
|
||||
v = float(0.0)
|
||||
f = int(-1)
|
||||
n = wp.vec3(0.0, 0.0, 0.0)
|
||||
hit_geom_id = int(-1)
|
||||
@wp.func
|
||||
def cast_ray(
|
||||
# Model:
|
||||
geom_type: wp.array[int],
|
||||
geom_dataid: wp.array2d[int],
|
||||
geom_size: wp.array2d[wp.vec3],
|
||||
flex_vertadr: wp.array[int],
|
||||
flex_edge: wp.array[wp.vec2i],
|
||||
flex_radius: wp.array[float],
|
||||
# Data in:
|
||||
geom_xpos_in: wp.array2d[wp.vec3],
|
||||
geom_xmat_in: wp.array2d[wp.mat33],
|
||||
flexvert_xpos_in: wp.array2d[wp.vec3],
|
||||
# In:
|
||||
bvh_id: wp.uint64,
|
||||
group_root: int,
|
||||
worldid: int,
|
||||
bvh_ngeom: int,
|
||||
flex_bvh_ngeom: int,
|
||||
enabled_geom_ids: wp.array[int],
|
||||
mesh_bvh_id: wp.array[wp.uint64],
|
||||
hfield_bvh_id: wp.array[wp.uint64],
|
||||
flex_geom_flexid: wp.array[int],
|
||||
flex_geom_edgeid: wp.array[int],
|
||||
flex_bvh_id: wp.array[wp.uint64],
|
||||
flex_group_root: wp.array2d[int],
|
||||
ray_origin_world: wp.vec3,
|
||||
ray_dir_world: wp.vec3,
|
||||
max_dist: float,
|
||||
cull_backfaces: bool,
|
||||
) -> Tuple[int, float, wp.vec3, float, float, int, int]:
|
||||
dist = max_dist
|
||||
normal = wp.vec3(0.0, 0.0, 0.0)
|
||||
geom_id = int(-1)
|
||||
bary_u = float(0.0)
|
||||
bary_v = float(0.0)
|
||||
face_idx = int(-1)
|
||||
geom_mesh_id = int(-1)
|
||||
|
||||
if local_id < bvh_ngeom:
|
||||
gi = enabled_geom_ids[local_id]
|
||||
gtype = geom_type[gi]
|
||||
else:
|
||||
gi = local_id - bvh_ngeom
|
||||
gtype = GeomType.FLEX
|
||||
query = wp.bvh_query_ray(bvh_id, ray_origin_world, ray_dir_world, group_root)
|
||||
bounds_nr = int(0)
|
||||
ngeom = bvh_ngeom + flex_bvh_ngeom
|
||||
|
||||
hit_geom_id = gi
|
||||
while wp.bvh_query_next(query, bounds_nr, dist):
|
||||
gi_global = bounds_nr
|
||||
local_id = gi_global - (worldid * ngeom)
|
||||
|
||||
# TODO: Investigate branch elimination with static loop unrolling
|
||||
if gtype == GeomType.PLANE:
|
||||
d, n = ray_plane(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.HFIELD:
|
||||
d, n, u, v, f, geom_hfield_id = ray_mesh_with_bvh(
|
||||
hfield_bvh_id,
|
||||
geom_dataid[worldid % geom_dataid.shape[0], gi],
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
dist,
|
||||
cull_backfaces,
|
||||
)
|
||||
if gtype == GeomType.SPHERE:
|
||||
d, n = ray_sphere(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi][0] * geom_size[worldid % geom_size.shape[0], gi][0],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.ELLIPSOID:
|
||||
d, n = ray_ellipsoid(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.CAPSULE:
|
||||
d, n = ray_capsule(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.CYLINDER:
|
||||
d, n = ray_cylinder(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.BOX:
|
||||
d, all, n = ray_box(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.MESH:
|
||||
d, n, u, v, f, hit_mesh_id = ray_mesh_with_bvh(
|
||||
mesh_bvh_id,
|
||||
geom_dataid[worldid % geom_dataid.shape[0], gi],
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
dist,
|
||||
cull_backfaces,
|
||||
)
|
||||
if gtype == GeomType.FLEX:
|
||||
hit_geom_id = -2
|
||||
flexid = flex_geom_flexid[gi]
|
||||
edge_id = flex_geom_edgeid[gi]
|
||||
d = float(-1.0)
|
||||
hit_mesh_id = int(-1)
|
||||
u = float(0.0)
|
||||
v = float(0.0)
|
||||
f = int(-1)
|
||||
n = wp.vec3(0.0, 0.0, 0.0)
|
||||
hit_geom_id = int(-1)
|
||||
|
||||
if edge_id >= 0:
|
||||
edge = flex_edge[edge_id]
|
||||
vert_adr = flex_vertadr[flexid]
|
||||
v0 = flexvert_xpos_in[worldid, vert_adr + edge[0]]
|
||||
v1 = flexvert_xpos_in[worldid, vert_adr + edge[1]]
|
||||
pos = 0.5 * (v0 + v1)
|
||||
vec = v1 - v0
|
||||
|
||||
length = wp.length(vec)
|
||||
edgeq = math.quat_z2vec(vec)
|
||||
mat = math.quat_to_mat(edgeq)
|
||||
size = wp.vec3(flex_radius[flexid], 0.5 * length, 0.0)
|
||||
|
||||
d, n = ray_capsule(pos, mat, size, ray_origin_world, ray_dir_world)
|
||||
hit_mesh_id = flexid
|
||||
if local_id < bvh_ngeom:
|
||||
gi = enabled_geom_ids[local_id]
|
||||
gtype = geom_type[gi]
|
||||
else:
|
||||
flex_gr = flex_group_root[worldid, flexid]
|
||||
d, n, u, v, f = ray_flex_with_bvh(flex_bvh_id, flexid, flex_gr, ray_origin_world, ray_dir_world, dist)
|
||||
if d >= 0.0:
|
||||
hit_mesh_id = flexid
|
||||
gi = local_id - bvh_ngeom
|
||||
gtype = GeomType.FLEX
|
||||
|
||||
# Backface cull: drop exit-face hits when the ray origin is inside the geom,
|
||||
# matching ray_mesh_with_bvh's `dot(lvec, n) < 0` rule.
|
||||
if cull_backfaces and d >= 0.0 and wp.dot(ray_dir_world, n) > 0.0:
|
||||
d = -1.0
|
||||
hit_geom_id = gi
|
||||
|
||||
if d >= 0.0 and d < dist:
|
||||
dist = d
|
||||
normal = n
|
||||
geom_id = hit_geom_id
|
||||
bary_u = u
|
||||
bary_v = v
|
||||
face_idx = f
|
||||
geom_mesh_id = hit_mesh_id
|
||||
if wp.static(int(GeomType.PLANE) in geom_ray_types):
|
||||
if gtype == GeomType.PLANE:
|
||||
d, n = ray_plane(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if wp.static(int(GeomType.HFIELD) in geom_ray_types):
|
||||
if gtype == GeomType.HFIELD:
|
||||
d, n, u, v, f, geom_hfield_id = ray_mesh_with_bvh(
|
||||
hfield_bvh_id,
|
||||
geom_dataid[worldid % geom_dataid.shape[0], gi],
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
dist,
|
||||
cull_backfaces,
|
||||
)
|
||||
if wp.static(int(GeomType.SPHERE) in geom_ray_types):
|
||||
if gtype == GeomType.SPHERE:
|
||||
d, n = ray_sphere(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi][0] * geom_size[worldid % geom_size.shape[0], gi][0],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if wp.static(int(GeomType.ELLIPSOID) in geom_ray_types):
|
||||
if gtype == GeomType.ELLIPSOID:
|
||||
d, n = ray_ellipsoid(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if wp.static(int(GeomType.CAPSULE) in geom_ray_types):
|
||||
if gtype == GeomType.CAPSULE:
|
||||
d, n = ray_capsule(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if wp.static(int(GeomType.CYLINDER) in geom_ray_types):
|
||||
if gtype == GeomType.CYLINDER:
|
||||
d, n = ray_cylinder(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if wp.static(int(GeomType.BOX) in geom_ray_types):
|
||||
if gtype == GeomType.BOX:
|
||||
d, all, n = ray_box(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if wp.static(int(GeomType.MESH) in geom_ray_types):
|
||||
if gtype == GeomType.MESH:
|
||||
if wp.static(first_hit):
|
||||
hit = ray_mesh_with_bvh_anyhit(
|
||||
mesh_bvh_id,
|
||||
geom_dataid[worldid % geom_dataid.shape[0], gi],
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
dist,
|
||||
)
|
||||
d = 0.0 if hit else -1.0
|
||||
else:
|
||||
d, n, u, v, f, hit_mesh_id = ray_mesh_with_bvh(
|
||||
mesh_bvh_id,
|
||||
geom_dataid[worldid % geom_dataid.shape[0], gi],
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
dist,
|
||||
cull_backfaces,
|
||||
)
|
||||
if wp.static(int(GeomType.FLEX) in geom_ray_types):
|
||||
if gtype == GeomType.FLEX:
|
||||
hit_geom_id = -2
|
||||
flexid = flex_geom_flexid[gi]
|
||||
edge_id = flex_geom_edgeid[gi]
|
||||
|
||||
return geom_id, dist, normal, bary_u, bary_v, face_idx, geom_mesh_id
|
||||
if edge_id >= 0:
|
||||
edge = flex_edge[edge_id]
|
||||
vert_adr = flex_vertadr[flexid]
|
||||
v0 = flexvert_xpos_in[worldid, vert_adr + edge[0]]
|
||||
v1 = flexvert_xpos_in[worldid, vert_adr + edge[1]]
|
||||
pos = 0.5 * (v0 + v1)
|
||||
vec = v1 - v0
|
||||
|
||||
length = wp.length(vec)
|
||||
edgeq = math.quat_z2vec(vec)
|
||||
mat = math.quat_to_mat(edgeq)
|
||||
size = wp.vec3(flex_radius[flexid], 0.5 * length, 0.0)
|
||||
|
||||
@wp.func
|
||||
def cast_ray_first_hit(
|
||||
# Model:
|
||||
geom_type: wp.array[int],
|
||||
geom_dataid: wp.array2d[int],
|
||||
geom_size: wp.array2d[wp.vec3],
|
||||
flex_vertadr: wp.array[int],
|
||||
flex_edge: wp.array[wp.vec2i],
|
||||
flex_radius: wp.array[float],
|
||||
# Data in:
|
||||
geom_xpos_in: wp.array2d[wp.vec3],
|
||||
geom_xmat_in: wp.array2d[wp.mat33],
|
||||
flexvert_xpos_in: wp.array2d[wp.vec3],
|
||||
# In:
|
||||
bvh_id: wp.uint64,
|
||||
group_root: int,
|
||||
worldid: int,
|
||||
bvh_ngeom: int,
|
||||
bvh_nflexgeom: int,
|
||||
enabled_geom_ids: wp.array[int],
|
||||
mesh_bvh_id: wp.array[wp.uint64],
|
||||
hfield_bvh_id: wp.array[wp.uint64],
|
||||
flex_geom_flexid: wp.array[int],
|
||||
flex_geom_edgeid: wp.array[int],
|
||||
flex_bvh_id: wp.array[wp.uint64],
|
||||
flex_group_root: wp.array2d[int],
|
||||
ray_origin_world: wp.vec3,
|
||||
ray_dir_world: wp.vec3,
|
||||
max_dist: float,
|
||||
cull_backfaces: bool,
|
||||
) -> bool:
|
||||
"""A simpler version of casting rays that only checks for the first hit."""
|
||||
query = wp.bvh_query_ray(bvh_id, ray_origin_world, ray_dir_world, group_root)
|
||||
bounds_nr = int(0)
|
||||
ngeom = bvh_ngeom + bvh_nflexgeom
|
||||
d, n = ray_capsule(pos, mat, size, ray_origin_world, ray_dir_world)
|
||||
hit_mesh_id = flexid
|
||||
else:
|
||||
if wp.static(first_hit):
|
||||
hit = ray_flex_with_bvh_anyhit(
|
||||
flex_bvh_id,
|
||||
flexid,
|
||||
flex_group_root[worldid, flexid],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
dist,
|
||||
)
|
||||
d = 0.0 if hit else -1.0
|
||||
else:
|
||||
flex_gr = flex_group_root[worldid, flexid]
|
||||
d, n, u, v, f = ray_flex_with_bvh(flex_bvh_id, flexid, flex_gr, ray_origin_world, ray_dir_world, dist)
|
||||
if d >= 0.0:
|
||||
hit_mesh_id = flexid
|
||||
|
||||
while wp.bvh_query_next(query, bounds_nr, max_dist):
|
||||
gi_global = bounds_nr
|
||||
local_id = gi_global - (worldid * ngeom)
|
||||
# Backface cull: drop exit-face hits when the ray origin is inside the geom,
|
||||
# matching ray_mesh_with_bvh's `dot(lvec, n) < 0` rule. Strict `> 0` keeps
|
||||
# tangent hits and skips branches with a zero-vector normal (any-hit).
|
||||
if cull_backfaces and d >= 0.0 and wp.dot(ray_dir_world, n) > 0.0:
|
||||
d = -1.0
|
||||
|
||||
d = float(-1.0)
|
||||
n = wp.vec3(0.0, 0.0, 0.0)
|
||||
|
||||
if local_id < bvh_ngeom:
|
||||
gi = enabled_geom_ids[local_id]
|
||||
gtype = geom_type[gi]
|
||||
else:
|
||||
gi = local_id - bvh_ngeom
|
||||
gtype = GeomType.FLEX
|
||||
|
||||
# TODO: Investigate branch elimination with static loop unrolling
|
||||
if gtype == GeomType.PLANE:
|
||||
d, n = ray_plane(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.HFIELD:
|
||||
d, n, u, v, f, geom_hfield_id = ray_mesh_with_bvh(
|
||||
hfield_bvh_id,
|
||||
geom_dataid[worldid % geom_dataid.shape[0], gi],
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
max_dist,
|
||||
cull_backfaces,
|
||||
)
|
||||
if gtype == GeomType.SPHERE:
|
||||
d, n = ray_sphere(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi][0] * geom_size[worldid % geom_size.shape[0], gi][0],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.ELLIPSOID:
|
||||
d, n = ray_ellipsoid(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.CAPSULE:
|
||||
d, n = ray_capsule(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.CYLINDER:
|
||||
d, n = ray_cylinder(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.BOX:
|
||||
d, all, n = ray_box(
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
geom_size[worldid % geom_size.shape[0], gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
)
|
||||
if gtype == GeomType.MESH:
|
||||
hit = ray_mesh_with_bvh_anyhit(
|
||||
mesh_bvh_id,
|
||||
geom_dataid[worldid % geom_dataid.shape[0], gi],
|
||||
geom_xpos_in[worldid, gi],
|
||||
geom_xmat_in[worldid, gi],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
max_dist,
|
||||
)
|
||||
d = 0.0 if hit else -1.0
|
||||
if gtype == GeomType.FLEX:
|
||||
flexid = flex_geom_flexid[gi]
|
||||
edge_id = flex_geom_edgeid[gi]
|
||||
|
||||
if edge_id >= 0:
|
||||
edge = flex_edge[edge_id]
|
||||
vert_adr = flex_vertadr[flexid]
|
||||
v0 = flexvert_xpos_in[worldid, vert_adr + edge[0]]
|
||||
v1 = flexvert_xpos_in[worldid, vert_adr + edge[1]]
|
||||
pos = 0.5 * (v0 + v1)
|
||||
vec = v1 - v0
|
||||
|
||||
length = wp.length(vec)
|
||||
edgeq = math.quat_z2vec(vec)
|
||||
mat = math.quat_to_mat(edgeq)
|
||||
size = wp.vec3(flex_radius[flexid], 0.5 * length, 0.0)
|
||||
|
||||
d, n = ray_capsule(pos, mat, size, ray_origin_world, ray_dir_world)
|
||||
if wp.static(first_hit):
|
||||
# Any-hit: return as soon as anything is in range; surface data is unused.
|
||||
if d >= 0.0 and d < dist:
|
||||
return hit_geom_id, d, n, u, v, f, hit_mesh_id
|
||||
else:
|
||||
hit = ray_flex_with_bvh_anyhit(
|
||||
flex_bvh_id,
|
||||
flexid,
|
||||
flex_group_root[worldid, flexid],
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
max_dist,
|
||||
)
|
||||
d = 0.0 if hit else -1.0
|
||||
if d >= 0.0 and d < dist:
|
||||
dist = d
|
||||
normal = n
|
||||
geom_id = hit_geom_id
|
||||
bary_u = u
|
||||
bary_v = v
|
||||
face_idx = f
|
||||
geom_mesh_id = hit_mesh_id
|
||||
|
||||
# Backface cull: see cast_ray for rationale. Strict `> 0` keeps tangent
|
||||
# hits and skips branches with a zero-vector normal (mesh/flex anyhit).
|
||||
if cull_backfaces and d >= 0.0 and wp.dot(ray_dir_world, n) > 0.0:
|
||||
d = -1.0
|
||||
return geom_id, dist, normal, bary_u, bary_v, face_idx, geom_mesh_id
|
||||
|
||||
if d >= 0.0 and d < max_dist:
|
||||
return True
|
||||
|
||||
return False
|
||||
return cast_ray
|
||||
|
||||
|
||||
@wp.func
|
||||
def compute_lighting(
|
||||
# Model:
|
||||
geom_type: wp.array[int],
|
||||
geom_dataid: wp.array2d[int],
|
||||
geom_size: wp.array2d[wp.vec3],
|
||||
flex_vertadr: wp.array[int],
|
||||
flex_edge: wp.array[wp.vec2i],
|
||||
flex_radius: wp.array[float],
|
||||
# Data in:
|
||||
geom_xpos_in: wp.array2d[wp.vec3],
|
||||
geom_xmat_in: wp.array2d[wp.mat33],
|
||||
flexvert_xpos_in: wp.array2d[wp.vec3],
|
||||
# In:
|
||||
use_shadows: bool,
|
||||
bvh_id: wp.uint64,
|
||||
group_root: int,
|
||||
bvh_ngeom: int,
|
||||
bvh_nflexgeom: int,
|
||||
enabled_geom_ids: wp.array[int],
|
||||
worldid: int,
|
||||
mesh_bvh_id: wp.array[wp.uint64],
|
||||
hfield_bvh_id: wp.array[wp.uint64],
|
||||
flex_geom_flexid: wp.array[int],
|
||||
flex_geom_edgeid: wp.array[int],
|
||||
flex_bvh_id: wp.array[wp.uint64],
|
||||
flex_group_root: wp.array2d[int],
|
||||
lightactive: bool,
|
||||
lighttype: int,
|
||||
lightcastshadow: bool,
|
||||
lightpos: wp.vec3,
|
||||
lightdir: wp.vec3,
|
||||
normal: wp.vec3,
|
||||
hitpoint: wp.vec3,
|
||||
cull_backfaces: bool,
|
||||
) -> float:
|
||||
light_contribution = float(0.0)
|
||||
def _make_compute_lighting(cast_ray_first_hit: wp.Function) -> wp.Function:
|
||||
"""Build specialized compute_lighting."""
|
||||
|
||||
# TODO: We should probably only be looping over active lights
|
||||
# in the first place with a static loop of enabled light idx?
|
||||
if not lightactive:
|
||||
return light_contribution
|
||||
@wp.func
|
||||
def compute_lighting(
|
||||
# Model:
|
||||
geom_type: wp.array[int],
|
||||
geom_dataid: wp.array2d[int],
|
||||
geom_size: wp.array2d[wp.vec3],
|
||||
flex_vertadr: wp.array[int],
|
||||
flex_edge: wp.array[wp.vec2i],
|
||||
flex_radius: wp.array[float],
|
||||
# Data in:
|
||||
geom_xpos_in: wp.array2d[wp.vec3],
|
||||
geom_xmat_in: wp.array2d[wp.mat33],
|
||||
flexvert_xpos_in: wp.array2d[wp.vec3],
|
||||
# In:
|
||||
use_shadows: bool,
|
||||
bvh_id: wp.uint64,
|
||||
group_root: int,
|
||||
bvh_ngeom: int,
|
||||
bvh_nflexgeom: int,
|
||||
enabled_geom_ids: wp.array[int],
|
||||
worldid: int,
|
||||
mesh_bvh_id: wp.array[wp.uint64],
|
||||
hfield_bvh_id: wp.array[wp.uint64],
|
||||
flex_geom_flexid: wp.array[int],
|
||||
flex_geom_edgeid: wp.array[int],
|
||||
flex_bvh_id: wp.array[wp.uint64],
|
||||
flex_group_root: wp.array2d[int],
|
||||
lightactive: bool,
|
||||
lighttype: int,
|
||||
lightcastshadow: bool,
|
||||
lightpos: wp.vec3,
|
||||
lightdir: wp.vec3,
|
||||
normal: wp.vec3,
|
||||
hitpoint: wp.vec3,
|
||||
cull_backfaces: bool,
|
||||
) -> float:
|
||||
light_contribution = float(0.0)
|
||||
|
||||
L = wp.vec3(0.0, 0.0, 0.0)
|
||||
dist_to_light = float(MJ_MAXVAL)
|
||||
attenuation = float(1.0)
|
||||
# TODO: We should probably only be looping over active lights
|
||||
# in the first place with a static loop of enabled light idx?
|
||||
if not lightactive:
|
||||
return light_contribution
|
||||
|
||||
if lighttype == 1: # directional light
|
||||
L = wp.normalize(-lightdir)
|
||||
else:
|
||||
L, dist_to_light = math.normalize_with_norm(lightpos - hitpoint)
|
||||
attenuation = 1.0 / (1.0 + 0.02 * dist_to_light * dist_to_light)
|
||||
if lighttype == 0: # spot light
|
||||
spot_dir = wp.normalize(lightdir)
|
||||
cos_theta = wp.dot(-L, spot_dir)
|
||||
spot_factor = wp.min(1.0, wp.max(0.0, (cos_theta - 0.85) / (0.95 - 0.85)))
|
||||
attenuation = attenuation * spot_factor
|
||||
L = wp.vec3(0.0, 0.0, 0.0)
|
||||
dist_to_light = float(MJ_MAXVAL)
|
||||
attenuation = float(1.0)
|
||||
|
||||
ndotl = wp.max(0.0, wp.dot(normal, L))
|
||||
if ndotl == 0.0:
|
||||
return light_contribution
|
||||
|
||||
visible = float(1.0)
|
||||
|
||||
if use_shadows and lightcastshadow:
|
||||
# Nudge the origin slightly along the surface normal to avoid
|
||||
# self-intersection when casting shadow rays
|
||||
eps = 1.0e-4
|
||||
shadow_origin = hitpoint + normal * eps
|
||||
# Distance-limited shadows: cap by dist_to_light (for non-directional)
|
||||
max_t = float(dist_to_light - 1.0e-3)
|
||||
if lighttype == 1: # directional light
|
||||
max_t = float(1.0e8)
|
||||
L = wp.normalize(-lightdir)
|
||||
else:
|
||||
L, dist_to_light = math.normalize_with_norm(lightpos - hitpoint)
|
||||
attenuation = 1.0 / (1.0 + 0.02 * dist_to_light * dist_to_light)
|
||||
if lighttype == 0: # spot light
|
||||
spot_dir = wp.normalize(lightdir)
|
||||
cos_theta = wp.dot(-L, spot_dir)
|
||||
spot_factor = wp.min(1.0, wp.max(0.0, (cos_theta - 0.85) * 10.0))
|
||||
attenuation = attenuation * spot_factor
|
||||
|
||||
shadow_hit = cast_ray_first_hit(
|
||||
geom_type,
|
||||
geom_dataid,
|
||||
geom_size,
|
||||
flex_vertadr,
|
||||
flex_edge,
|
||||
flex_radius,
|
||||
geom_xpos_in,
|
||||
geom_xmat_in,
|
||||
flexvert_xpos_in,
|
||||
bvh_id,
|
||||
group_root,
|
||||
worldid,
|
||||
bvh_ngeom,
|
||||
bvh_nflexgeom,
|
||||
enabled_geom_ids,
|
||||
mesh_bvh_id,
|
||||
hfield_bvh_id,
|
||||
flex_geom_flexid,
|
||||
flex_geom_edgeid,
|
||||
flex_bvh_id,
|
||||
flex_group_root,
|
||||
shadow_origin,
|
||||
L,
|
||||
max_t,
|
||||
cull_backfaces,
|
||||
)
|
||||
ndotl = wp.max(0.0, wp.dot(normal, L))
|
||||
if ndotl == 0.0:
|
||||
return light_contribution
|
||||
|
||||
if shadow_hit:
|
||||
visible = 0.3
|
||||
visible = float(1.0)
|
||||
|
||||
return ndotl * attenuation * visible
|
||||
if use_shadows and lightcastshadow:
|
||||
# Nudge the origin slightly along the surface normal to avoid
|
||||
# self-intersection when casting shadow rays
|
||||
shadow_origin = hitpoint + normal * 1.0e-4
|
||||
# Distance-limited shadows: cap by dist_to_light (for non-directional)
|
||||
max_t = dist_to_light - 1.0e-3
|
||||
if lighttype == 1: # directional light
|
||||
max_t = 1.0e8
|
||||
|
||||
shadow_geom_id, shadow_d, shadow_n, shadow_u, shadow_v, shadow_f, shadow_mesh_id = cast_ray_first_hit(
|
||||
geom_type,
|
||||
geom_dataid,
|
||||
geom_size,
|
||||
flex_vertadr,
|
||||
flex_edge,
|
||||
flex_radius,
|
||||
geom_xpos_in,
|
||||
geom_xmat_in,
|
||||
flexvert_xpos_in,
|
||||
bvh_id,
|
||||
group_root,
|
||||
worldid,
|
||||
bvh_ngeom,
|
||||
bvh_nflexgeom,
|
||||
enabled_geom_ids,
|
||||
mesh_bvh_id,
|
||||
hfield_bvh_id,
|
||||
flex_geom_flexid,
|
||||
flex_geom_edgeid,
|
||||
flex_bvh_id,
|
||||
flex_group_root,
|
||||
shadow_origin,
|
||||
L,
|
||||
max_t,
|
||||
cull_backfaces,
|
||||
)
|
||||
|
||||
if shadow_geom_id != -1:
|
||||
visible = 0.3
|
||||
|
||||
return ndotl * attenuation * visible
|
||||
|
||||
return compute_lighting
|
||||
|
||||
|
||||
@event_scope
|
||||
@@ -611,6 +508,13 @@ def render(m: Model, d: Data, rc: RenderContext):
|
||||
rc.depth_data.fill_(0.0)
|
||||
rc.seg_data.fill_(wp.vec2i(-1, -1))
|
||||
|
||||
# Specialize the ray-cast helpers to the geom types present in the scene so the
|
||||
# compiler eliminates intersection branches for absent types.
|
||||
geom_ray_types = rc.geom_ray_types
|
||||
cast_ray = _make_cast_ray(geom_ray_types, first_hit=False)
|
||||
cast_ray_first_hit = _make_cast_ray(geom_ray_types, first_hit=True)
|
||||
compute_lighting = _make_compute_lighting(cast_ray_first_hit)
|
||||
|
||||
@wp.kernel(module="unique", enable_backward=False)
|
||||
def _render_megakernel(
|
||||
# Model:
|
||||
@@ -676,31 +580,31 @@ def render(m: Model, d: Data, rc: RenderContext):
|
||||
):
|
||||
worldid, rayid = wp.tid()
|
||||
|
||||
# Map global rayid -> (cam_idx, rayid_local) using cumulative sizes
|
||||
cam_idx = int(-1)
|
||||
# Map global rayid -> (camid, rayid_local) using cumulative sizes
|
||||
camid = int(-1)
|
||||
rayid_local = int(-1)
|
||||
accum = int(0)
|
||||
for i in range(nrender):
|
||||
num_i = cam_res[i][0] * cam_res[i][1]
|
||||
if rayid < accum + num_i:
|
||||
cam_idx = i
|
||||
camid = i
|
||||
rayid_local = rayid - accum
|
||||
break
|
||||
accum += num_i
|
||||
if cam_idx == -1 or rayid_local < 0:
|
||||
if camid == -1 or rayid_local < 0:
|
||||
return
|
||||
|
||||
if not render_rgb[cam_idx] and not render_depth[cam_idx] and not render_seg[cam_idx]:
|
||||
if not render_rgb[camid] and not render_depth[camid] and not render_seg[camid]:
|
||||
return
|
||||
|
||||
# Map active camera index to MuJoCo camera ID
|
||||
mujoco_cam_id = cam_id_map[cam_idx]
|
||||
mujoco_cam_id = cam_id_map[camid]
|
||||
|
||||
if wp.static(rc.use_precomputed_rays):
|
||||
ray_dir_local_cam = ray[rayid]
|
||||
else:
|
||||
img_w = cam_res[cam_idx][0]
|
||||
img_h = cam_res[cam_idx][1]
|
||||
img_w = cam_res[camid][0]
|
||||
img_h = cam_res[camid][1]
|
||||
px = rayid_local % img_w
|
||||
py = rayid_local // img_w
|
||||
ray_dir_local_cam = compute_ray(
|
||||
@@ -742,24 +646,25 @@ def render(m: Model, d: Data, rc: RenderContext):
|
||||
flex_group_root,
|
||||
ray_origin_world,
|
||||
ray_dir_world,
|
||||
float(MJ_MAXVAL),
|
||||
wp.static(rc.enable_backface_culling),
|
||||
)
|
||||
|
||||
if render_seg[cam_idx] and geom_id != -1:
|
||||
if render_seg[camid] and geom_id != -1:
|
||||
if geom_id == -2:
|
||||
seg_out[worldid, seg_adr[cam_idx] + rayid_local] = wp.vec2i(mesh_id, int(ObjType.FLEX))
|
||||
seg_out[worldid, seg_adr[camid] + rayid_local] = wp.vec2i(mesh_id, int(ObjType.FLEX))
|
||||
else:
|
||||
seg_out[worldid, seg_adr[cam_idx] + rayid_local] = wp.vec2i(geom_id, int(ObjType.GEOM))
|
||||
seg_out[worldid, seg_adr[camid] + rayid_local] = wp.vec2i(geom_id, int(ObjType.GEOM))
|
||||
|
||||
# Early Out
|
||||
if geom_id == -1:
|
||||
if wp.static(rc.render_skybox) and render_rgb[cam_idx]:
|
||||
if wp.static(rc.render_skybox) and render_rgb[camid]:
|
||||
skybox_color = sample_skybox(
|
||||
textures[wp.static(rc.skybox_tex_id)],
|
||||
wp.static(1.0 / float(rc.skybox_face_width)),
|
||||
ray_dir_world,
|
||||
)
|
||||
rgb_out[worldid, rgb_adr[cam_idx] + rayid_local] = pack_rgba_to_uint32(
|
||||
rgb_out[worldid, rgb_adr[camid] + rayid_local] = pack_rgba_to_uint32(
|
||||
skybox_color[0] * 255.0,
|
||||
skybox_color[1] * 255.0,
|
||||
skybox_color[2] * 255.0,
|
||||
@@ -767,14 +672,14 @@ def render(m: Model, d: Data, rc: RenderContext):
|
||||
)
|
||||
return
|
||||
|
||||
if render_depth[cam_idx]:
|
||||
if render_depth[camid]:
|
||||
# Planar depth: project Euclidean distance onto the camera's optical axis.
|
||||
# In camera-local coordinates, the optical axis is -Z. The Z-component of the
|
||||
# normalized ray direction is negative, so -ray_dir_local_cam[2] gives cos(θ)
|
||||
# between the ray and the optical axis.
|
||||
depth_out[worldid, depth_adr[cam_idx] + rayid_local] = dist * (-ray_dir_local_cam[2])
|
||||
depth_out[worldid, depth_adr[camid] + rayid_local] = dist * (-ray_dir_local_cam[2])
|
||||
|
||||
if not render_rgb[cam_idx]:
|
||||
if not render_rgb[camid]:
|
||||
return
|
||||
|
||||
# Shade the pixel
|
||||
@@ -864,7 +769,7 @@ def render(m: Model, d: Data, rc: RenderContext):
|
||||
hit_color = wp.min(result, wp.vec3(1.0, 1.0, 1.0))
|
||||
hit_color = wp.max(hit_color, wp.vec3(0.0, 0.0, 0.0))
|
||||
|
||||
rgb_out[worldid, rgb_adr[cam_idx] + rayid_local] = pack_rgba_to_uint32(
|
||||
rgb_out[worldid, rgb_adr[camid] + rayid_local] = pack_rgba_to_uint32(
|
||||
hit_color[0] * 255.0,
|
||||
hit_color[1] * 255.0,
|
||||
hit_color[2] * 255.0,
|
||||
@@ -934,4 +839,5 @@ def render(m: Model, d: Data, rc: RenderContext):
|
||||
rc.depth_data,
|
||||
rc.seg_data,
|
||||
],
|
||||
block_dim=m.block_dim.render,
|
||||
)
|
||||
|
||||
+3
-3
@@ -1012,7 +1012,7 @@ def get_state(m: Model, d: Data, state: wp.array2d[float], sig: int, active: Opt
|
||||
elif element == State.EQ_ACTIVE:
|
||||
for j in range(neq):
|
||||
state_out[worldid, adr + j] = float(eq_active_in[worldid, j])
|
||||
adr += j
|
||||
adr += neq
|
||||
elif element == State.MOCAP_POS:
|
||||
for j in range(nmocap):
|
||||
pos = mocap_pos_in[worldid, j]
|
||||
@@ -1160,12 +1160,12 @@ def set_state(m: Model, d: Data, state: wp.array2d[float], sig: int, active: Opt
|
||||
elif element == State.EQ_ACTIVE:
|
||||
for j in range(neq):
|
||||
eq_active_out[worldid, j] = bool(state_in[worldid, adr + j])
|
||||
adr += j
|
||||
adr += neq
|
||||
elif element == State.MOCAP_POS:
|
||||
for j in range(nmocap):
|
||||
pos = wp.vec3(
|
||||
state_in[worldid, adr + 1],
|
||||
state_in[worldid, adr + 0],
|
||||
state_in[worldid, adr + 1],
|
||||
state_in[worldid, adr + 2],
|
||||
)
|
||||
mocap_pos_out[worldid, j] = pos
|
||||
|
||||
@@ -67,6 +67,7 @@ class BlockDim:
|
||||
linesearch_iterative: linesearch iterative block dimension (solver)
|
||||
contact_jac_tiled: contact Jacobian tiled block dimension (solver)
|
||||
qderiv_actuator_dense: qderiv actuator dense block dimension (derivative)
|
||||
render: render block dimension (render)
|
||||
"""
|
||||
|
||||
# collision_driver
|
||||
@@ -93,6 +94,8 @@ class BlockDim:
|
||||
contact_jac_tiled: int = 32
|
||||
# derivative
|
||||
qderiv_actuator_dense: int = 32
|
||||
# render
|
||||
render: int = 64
|
||||
|
||||
|
||||
class BroadphaseType(enum.IntEnum):
|
||||
@@ -2206,6 +2209,8 @@ class RenderContext:
|
||||
mesh-ray rule. When False, the renderer reports inner-surface hits, which
|
||||
is faster but causes a camera placed inside a geom to render that geom's
|
||||
back wall.
|
||||
geom_ray_types: tuple of GeomType int values present in the scene, used to
|
||||
statically eliminate unused intersection branches in the ray-cast kernels.
|
||||
"""
|
||||
|
||||
nrender: int
|
||||
@@ -2260,3 +2265,4 @@ class RenderContext:
|
||||
znear: float
|
||||
total_rays: int
|
||||
enable_backface_culling: bool
|
||||
geom_ray_types: tuple = ()
|
||||
|
||||
@@ -14,19 +14,17 @@
|
||||
# ==============================================================================
|
||||
|
||||
"""DO NOT EDIT. This file is auto-generated."""
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
|
||||
import jax
|
||||
import warp as wp
|
||||
|
||||
from mujoco.mjx._src import types
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
from mujoco.mjx.warp import ffi
|
||||
from mujoco.mjx.warp.render_context import _MJX_RENDER_CONTEXT_BUFFERS
|
||||
from mujoco.mjx.warp.render_context import RenderContextPytree
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
import warp as wp
|
||||
|
||||
|
||||
_m = mjwarp.Model(
|
||||
**{f.name: None for f in dataclasses.fields(mjwarp.Model) if f.init}
|
||||
@@ -50,7 +48,6 @@ _cb = mjwp_types.Callback(
|
||||
**{f.name: None for f in dataclasses.fields(mjwp_types.Callback) if f.init}
|
||||
)
|
||||
|
||||
|
||||
@ffi.format_args_for_warp
|
||||
def _refit_bvh_shim(
|
||||
# Model
|
||||
|
||||
@@ -14,17 +14,14 @@
|
||||
# ==============================================================================
|
||||
|
||||
"""DO NOT EDIT. This file is auto-generated."""
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
|
||||
import jax
|
||||
import warp as wp
|
||||
|
||||
from mujoco.mjx._src import types
|
||||
from mujoco.mjx.warp import ffi
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
from mujoco.mjx.warp import ffi
|
||||
import warp as wp
|
||||
|
||||
_m = mjwarp.Model(
|
||||
**{f.name: None for f in dataclasses.fields(mjwarp.Model) if f.init}
|
||||
@@ -48,7 +45,6 @@ _cb = mjwp_types.Callback(
|
||||
**{f.name: None for f in dataclasses.fields(mjwp_types.Callback) if f.init}
|
||||
)
|
||||
|
||||
|
||||
@ffi.format_args_for_warp
|
||||
def _collision_shim(
|
||||
# Model
|
||||
|
||||
@@ -14,17 +14,14 @@
|
||||
# ==============================================================================
|
||||
|
||||
"""DO NOT EDIT. This file is auto-generated."""
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
|
||||
import jax
|
||||
import warp as wp
|
||||
|
||||
from mujoco.mjx._src import types
|
||||
from mujoco.mjx.warp import ffi
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
from mujoco.mjx.warp import ffi
|
||||
import warp as wp
|
||||
|
||||
_m = mjwarp.Model(
|
||||
**{f.name: None for f in dataclasses.fields(mjwarp.Model) if f.init}
|
||||
@@ -48,7 +45,6 @@ _cb = mjwp_types.Callback(
|
||||
**{f.name: None for f in dataclasses.fields(mjwp_types.Callback) if f.init}
|
||||
)
|
||||
|
||||
|
||||
@ffi.format_args_for_warp
|
||||
def _forward_shim(
|
||||
# Model
|
||||
|
||||
@@ -14,19 +14,17 @@
|
||||
# ==============================================================================
|
||||
|
||||
"""DO NOT EDIT. This file is auto-generated."""
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
|
||||
import jax
|
||||
import warp as wp
|
||||
|
||||
from mujoco.mjx._src import types
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
from mujoco.mjx.warp import ffi
|
||||
from mujoco.mjx.warp.render_context import _MJX_RENDER_CONTEXT_BUFFERS
|
||||
from mujoco.mjx.warp.render_context import RenderContextPytree
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
import warp as wp
|
||||
|
||||
|
||||
_m = mjwarp.Model(
|
||||
**{f.name: None for f in dataclasses.fields(mjwarp.Model) if f.init}
|
||||
@@ -55,6 +53,7 @@ _cb = mjwp_types.Callback(
|
||||
def _render_shim(
|
||||
# Model
|
||||
nworld: int,
|
||||
block_dim: mjwp_types.BlockDim,
|
||||
cam_fovy: wp.array2d[float],
|
||||
cam_intrinsic: wp.array2d[wp.vec4],
|
||||
cam_projection: wp.array[int],
|
||||
@@ -94,6 +93,7 @@ def _render_shim(
|
||||
_m.callback = _cb
|
||||
_d.efc = _e
|
||||
_d.contact = _c
|
||||
_m.block_dim = block_dim
|
||||
_m.cam_fovy = cam_fovy
|
||||
_m.cam_intrinsic = cam_intrinsic
|
||||
_m.cam_projection = cam_projection
|
||||
@@ -164,6 +164,7 @@ def _render_jax_impl(m: types.Model, d: types.Data, ctx: RenderContextPytree):
|
||||
)
|
||||
out = jf(
|
||||
render_ctx.nworld,
|
||||
m._impl.block_dim,
|
||||
m.cam_fovy,
|
||||
m.cam_intrinsic,
|
||||
m._impl.cam_projection,
|
||||
|
||||
@@ -14,17 +14,14 @@
|
||||
# ==============================================================================
|
||||
|
||||
"""DO NOT EDIT. This file is auto-generated."""
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
|
||||
import jax
|
||||
import warp as wp
|
||||
|
||||
from mujoco.mjx._src import types
|
||||
from mujoco.mjx.warp import ffi
|
||||
import mujoco.mjx.third_party.mujoco_warp as mjwarp
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
from mujoco.mjx.warp import ffi
|
||||
import warp as wp
|
||||
|
||||
_m = mjwarp.Model(
|
||||
**{f.name: None for f in dataclasses.fields(mjwarp.Model) if f.init}
|
||||
@@ -48,7 +45,6 @@ _cb = mjwp_types.Callback(
|
||||
**{f.name: None for f in dataclasses.fields(mjwp_types.Callback) if f.init}
|
||||
)
|
||||
|
||||
|
||||
@ffi.format_args_for_warp
|
||||
def _kinematics_shim(
|
||||
# Model
|
||||
|
||||
@@ -15,17 +15,14 @@
|
||||
"""MJX Warp types.
|
||||
DO NOT EDIT. This file is auto-generated.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import typing
|
||||
from typing import Tuple
|
||||
|
||||
import jax
|
||||
from jax import tree_util
|
||||
from jax.interpreters import batching
|
||||
import numpy as np
|
||||
|
||||
from mujoco.mjx._src import dataclasses as mjx_dataclasses
|
||||
import numpy as np
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
GraphMode = int
|
||||
@@ -37,7 +34,6 @@ if typing.TYPE_CHECKING:
|
||||
else:
|
||||
try:
|
||||
from warp._src.jax_experimental.ffi import GraphMode
|
||||
|
||||
from mujoco.mjx.third_party.mujoco_warp._src import types as mjwp_types
|
||||
|
||||
Callback = mjwp_types.Callback
|
||||
@@ -46,13 +42,6 @@ else:
|
||||
Callback = None
|
||||
PyTreeNode = mjx_dataclasses.PyTreeNode
|
||||
|
||||
|
||||
def _as_numpy_array(value):
|
||||
if hasattr(value, 'numpy'):
|
||||
return value.numpy()
|
||||
return np.asarray(value)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@tree_util.register_pytree_node_class
|
||||
class TileSet:
|
||||
@@ -64,19 +53,20 @@ class TileSet:
|
||||
adr: address of each tile in the set
|
||||
size: size of all the tiles in this set
|
||||
"""
|
||||
|
||||
adr: np.ndarray
|
||||
size: int
|
||||
|
||||
# Manually kept in this generated shim until TileSet method generation is
|
||||
# needed more broadly. Keep this in sync with mujoco_warp._src.types.TileSet.
|
||||
def __eq__(self, other) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return NotImplemented
|
||||
return self.size == other.size and np.array_equal(
|
||||
_as_numpy_array(self.adr), _as_numpy_array(other.adr)
|
||||
np.asarray(self.adr), np.asarray(other.adr)
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
adr = _as_numpy_array(self.adr)
|
||||
adr = np.asarray(self.adr)
|
||||
return hash((self.size, adr.dtype.str, adr.shape, adr.tobytes()))
|
||||
|
||||
def tree_flatten(self):
|
||||
@@ -118,8 +108,8 @@ class BlockDim:
|
||||
linesearch_iterative: linesearch iterative block dimension (solver)
|
||||
contact_jac_tiled: contact Jacobian tiled block dimension (solver)
|
||||
qderiv_actuator_dense: qderiv actuator dense block dimension (derivative)
|
||||
render: render block dimension (render)
|
||||
"""
|
||||
|
||||
actuator_velocity: int
|
||||
cholesky_factorize: int
|
||||
cholesky_factorize_solve: int
|
||||
@@ -131,6 +121,7 @@ class BlockDim:
|
||||
linesearch_iterative: int
|
||||
qderiv_actuator_dense: int
|
||||
ray: int
|
||||
render: int
|
||||
segmented_sort: int
|
||||
solve_LD_sparse_fused: int
|
||||
update_gradient_JTDAJ_dense: int
|
||||
@@ -150,13 +141,10 @@ class BlockDim:
|
||||
|
||||
class StatisticWarp(PyTreeNode):
|
||||
"""Derived fields from Statistic."""
|
||||
|
||||
meaninertia: jax.Array
|
||||
|
||||
|
||||
class OptionWarp(PyTreeNode):
|
||||
"""Derived fields from Option."""
|
||||
|
||||
broadphase: int
|
||||
broadphase_filter: int
|
||||
ccd_iterations: int
|
||||
@@ -171,7 +159,6 @@ class OptionWarp(PyTreeNode):
|
||||
sdf_initpoints: int
|
||||
sdf_iterations: int
|
||||
|
||||
|
||||
class ModelWarp(PyTreeNode):
|
||||
"""Derived fields from Model."""
|
||||
D_colind: np.ndarray
|
||||
@@ -353,7 +340,6 @@ class ModelWarp(PyTreeNode):
|
||||
wrap_site_adr: np.ndarray
|
||||
wrap_site_pair_adr: np.ndarray
|
||||
|
||||
|
||||
class DataWarp(PyTreeNode):
|
||||
"""Derived fields from Data."""
|
||||
M: jax.Array
|
||||
@@ -465,8 +451,6 @@ class DataWarp(PyTreeNode):
|
||||
wrap_obj: jax.Array
|
||||
wrap_xpos: jax.Array
|
||||
shape = property(lambda self: self.cacc.shape)
|
||||
|
||||
|
||||
DATA_NON_VMAP = {
|
||||
'contact__dim',
|
||||
'contact__dist',
|
||||
@@ -495,7 +479,6 @@ DATA_NON_VMAP = {
|
||||
'nworld',
|
||||
}
|
||||
|
||||
|
||||
def _to_elt(cont, _, d, axis):
|
||||
return DataWarp(**{
|
||||
f.name: (
|
||||
@@ -729,6 +712,7 @@ _NDIM = {
|
||||
'block_dim__linesearch_iterative': 0,
|
||||
'block_dim__qderiv_actuator_dense': 0,
|
||||
'block_dim__ray': 0,
|
||||
'block_dim__render': 0,
|
||||
'block_dim__segmented_sort': 0,
|
||||
'block_dim__solve_LD_sparse_fused': 0,
|
||||
'block_dim__update_gradient_JTDAJ_dense': 0,
|
||||
@@ -1364,6 +1348,7 @@ _BATCH_DIM = {
|
||||
'block_dim__linesearch_iterative': False,
|
||||
'block_dim__qderiv_actuator_dense': False,
|
||||
'block_dim__ray': False,
|
||||
'block_dim__render': False,
|
||||
'block_dim__segmented_sort': False,
|
||||
'block_dim__solve_LD_sparse_fused': False,
|
||||
'block_dim__update_gradient_JTDAJ_dense': False,
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
#include <pxr/base/gf/matrix4f.h>
|
||||
#include <pxr/base/gf/rotation.h>
|
||||
#include <pxr/base/gf/vec3d.h>
|
||||
#include <pxr/base/tf/staticData.h>
|
||||
#include <pxr/base/tf/staticTokens.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
#include <pxr/base/vt/types.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
@@ -82,6 +84,27 @@
|
||||
|
||||
using pxr::MjcPhysicsTokens;
|
||||
using pxr::TfToken;
|
||||
template <typename T>
|
||||
using TfStaticData = pxr::TfStaticData<T>;
|
||||
|
||||
// clang-format off
|
||||
TF_DEFINE_PRIVATE_TOKENS(kNewtonTokens,
|
||||
((NewtonMaterialAPI, "NewtonMaterialAPI"))
|
||||
((NewtonMeshCollisionAPI, "NewtonMeshCollisionAPI"))
|
||||
((newtonMaxSolverIterations, "newton:maxSolverIterations"))
|
||||
((newtonTimeStepsPerSecond, "newton:timeStepsPerSecond"))
|
||||
((newtonGravityEnabled, "newton:gravityEnabled"))
|
||||
((newtonContactMargin, "newton:contactMargin"))
|
||||
((newtonContactGap, "newton:contactGap"))
|
||||
((newtonMaxHullVertices, "newton:maxHullVertices"))
|
||||
((newtonTorsionalFriction, "newton:torsionalFriction"))
|
||||
((newtonRollingFriction, "newton:rollingFriction"))
|
||||
((newtonMimicJoint, "newton:mimicJoint"))
|
||||
((newtonMimicCoef0, "newton:mimicCoef0"))
|
||||
((newtonMimicCoef1, "newton:mimicCoef1"))
|
||||
((NewtonMimicAPI, "NewtonMimicAPI"))
|
||||
);
|
||||
// clang-format on
|
||||
|
||||
struct UsdCaches {
|
||||
pxr::UsdGeomXformCache xform_cache;
|
||||
@@ -463,15 +486,48 @@ void ParseUsdPhysicsScene(mjSpec* spec,
|
||||
|
||||
SetGravityAttributes(spec, stage, gravity_direction, gravity_magnitude);
|
||||
|
||||
// Early exit if theres no MjcPhysicsSceneAPI applied.
|
||||
if (!physics_scene.GetPrim().HasAPI<pxr::MjcPhysicsSceneAPI>()) {
|
||||
// Parse Newton scene attributes if present (works for Newton-only files)
|
||||
pxr::UsdPrim scene_prim = physics_scene.GetPrim();
|
||||
auto newton_iterations = scene_prim.GetAttribute(
|
||||
kNewtonTokens->newtonMaxSolverIterations);
|
||||
if (newton_iterations && newton_iterations.HasAuthoredValue()) {
|
||||
int val;
|
||||
newton_iterations.Get(&val);
|
||||
if (val >= 0) spec->option.iterations = val;
|
||||
}
|
||||
auto newton_timesteps = scene_prim.GetAttribute(
|
||||
kNewtonTokens->newtonTimeStepsPerSecond);
|
||||
if (newton_timesteps && newton_timesteps.HasAuthoredValue()) {
|
||||
int val;
|
||||
newton_timesteps.Get(&val);
|
||||
if (val > 0) spec->option.timestep = 1.0 / val;
|
||||
}
|
||||
auto newton_gravity = scene_prim.GetAttribute(
|
||||
kNewtonTokens->newtonGravityEnabled);
|
||||
if (newton_gravity && newton_gravity.HasAuthoredValue()) {
|
||||
bool enabled;
|
||||
newton_gravity.Get(&enabled);
|
||||
if (!enabled) {
|
||||
spec->option.disableflags |= mjDSBL_GRAVITY;
|
||||
}
|
||||
}
|
||||
|
||||
// Early exit if there's no MjcPhysicsSceneAPI applied.
|
||||
if (!scene_prim.HasAPI<pxr::MjcPhysicsSceneAPI>()) {
|
||||
return;
|
||||
}
|
||||
auto mjc_physics_scene = pxr::MjcPhysicsSceneAPI(physics_scene.GetPrim());
|
||||
auto mjc_physics_scene = pxr::MjcPhysicsSceneAPI(scene_prim);
|
||||
|
||||
double timestep;
|
||||
mjc_physics_scene.GetTimestepAttr().Get(×tep);
|
||||
spec->option.timestep = timestep;
|
||||
// MJC values override Newton values only when explicitly authored.
|
||||
auto timestep_attr = mjc_physics_scene.GetTimestepAttr();
|
||||
if (timestep_attr.HasAuthoredValue()) {
|
||||
double timestep;
|
||||
timestep_attr.Get(×tep);
|
||||
spec->option.timestep = timestep;
|
||||
mju_warning("Scene '%s' uses deprecated mjc:option:timestep. "
|
||||
"Please migrate to newton:timeStepsPerSecond.",
|
||||
scene_prim.GetPath().GetText());
|
||||
}
|
||||
|
||||
double impratio;
|
||||
mjc_physics_scene.GetImpRatioAttr().Get(&impratio);
|
||||
@@ -586,9 +642,15 @@ void ParseUsdPhysicsScene(mjSpec* spec,
|
||||
spec->option.solver = mjSOL_PGS;
|
||||
}
|
||||
|
||||
int iterations;
|
||||
mjc_physics_scene.GetIterationsAttr().Get(&iterations);
|
||||
spec->option.iterations = iterations;
|
||||
auto iterations_attr = mjc_physics_scene.GetIterationsAttr();
|
||||
if (iterations_attr.HasAuthoredValue()) {
|
||||
int iterations;
|
||||
iterations_attr.Get(&iterations);
|
||||
spec->option.iterations = iterations;
|
||||
mju_warning("Scene '%s' uses deprecated mjc:option:iterations. "
|
||||
"Please migrate to newton:maxSolverIterations.",
|
||||
scene_prim.GetPath().GetText());
|
||||
}
|
||||
|
||||
int ls_iterations;
|
||||
mjc_physics_scene.GetLSIterationsAttr().Get(&ls_iterations);
|
||||
@@ -638,9 +700,19 @@ void ParseUsdPhysicsScene(mjSpec* spec,
|
||||
mjc_physics_scene.GetDamperFlagAttr().Get(&damper_flag);
|
||||
spec->option.disableflags |= (!damper_flag ? mjDSBL_DAMPER : 0);
|
||||
|
||||
bool gravity_flag;
|
||||
mjc_physics_scene.GetGravityFlagAttr().Get(&gravity_flag);
|
||||
spec->option.disableflags |= (!gravity_flag ? mjDSBL_GRAVITY : 0);
|
||||
auto gravity_flag_attr = mjc_physics_scene.GetGravityFlagAttr();
|
||||
if (gravity_flag_attr.HasAuthoredValue()) {
|
||||
bool gravity_flag;
|
||||
gravity_flag_attr.Get(&gravity_flag);
|
||||
if (!gravity_flag) {
|
||||
spec->option.disableflags |= mjDSBL_GRAVITY;
|
||||
} else {
|
||||
spec->option.disableflags &= ~mjDSBL_GRAVITY;
|
||||
}
|
||||
mju_warning("Scene '%s' uses deprecated mjc:flag:gravity. "
|
||||
"Please migrate to newton:gravityEnabled.",
|
||||
scene_prim.GetPath().GetText());
|
||||
}
|
||||
|
||||
bool clampctrl_flag;
|
||||
mjc_physics_scene.GetClampCtrlFlagAttr().Get(&clampctrl_flag);
|
||||
@@ -914,13 +986,39 @@ void ParseMjcPhysicsCollisionAPI(
|
||||
}
|
||||
|
||||
auto margin_attr = collision_api.GetMarginAttr();
|
||||
if (margin_attr.HasAuthoredValue()) {
|
||||
auto gap_attr = collision_api.GetGapAttr();
|
||||
bool mjc_margin_authored = margin_attr.HasAuthoredValue();
|
||||
bool mjc_gap_authored = gap_attr.HasAuthoredValue();
|
||||
|
||||
if (mjc_margin_authored) {
|
||||
margin_attr.Get(&geom->margin);
|
||||
mju_warning("Prim '%s' uses deprecated mjc:margin. "
|
||||
"Please migrate to newton:contactMargin and newton:contactGap.",
|
||||
collision_api.GetPrim().GetPath().GetText());
|
||||
}
|
||||
if (mjc_gap_authored) {
|
||||
gap_attr.Get(&geom->gap);
|
||||
mju_warning("Prim '%s' uses deprecated mjc:gap. "
|
||||
"Please migrate to newton:contactGap.",
|
||||
collision_api.GetPrim().GetPath().GetText());
|
||||
}
|
||||
|
||||
auto gap_attr = collision_api.GetGapAttr();
|
||||
if (gap_attr.HasAuthoredValue()) {
|
||||
gap_attr.Get(&geom->gap);
|
||||
// Newton collision fallback: newton:contactMargin + newton:contactGap -> margin, gap
|
||||
if (!mjc_margin_authored || !mjc_gap_authored) {
|
||||
pxr::UsdPrim prim = collision_api.GetPrim();
|
||||
auto newton_margin = prim.GetAttribute(kNewtonTokens->newtonContactMargin);
|
||||
auto newton_gap = prim.GetAttribute(kNewtonTokens->newtonContactGap);
|
||||
float n_margin = 0, n_gap = 0;
|
||||
bool has_newton_margin = newton_margin && newton_margin.HasAuthoredValue();
|
||||
bool has_newton_gap = newton_gap && newton_gap.HasAuthoredValue();
|
||||
if (has_newton_margin) newton_margin.Get(&n_margin);
|
||||
if (has_newton_gap) newton_gap.Get(&n_gap);
|
||||
if (!mjc_gap_authored && has_newton_gap) {
|
||||
geom->gap = n_gap;
|
||||
}
|
||||
if (!mjc_margin_authored && has_newton_margin) {
|
||||
geom->margin = n_margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -942,8 +1040,19 @@ void ParseMjcPhysicsMeshCollisionAPI(
|
||||
}
|
||||
|
||||
auto maxhullvert_attr = mesh_collision_api.GetMaxHullVertAttr();
|
||||
auto newton_maxhull = mesh_collision_api.GetPrim().GetAttribute(
|
||||
kNewtonTokens->newtonMaxHullVertices);
|
||||
if (maxhullvert_attr.HasAuthoredValue()) {
|
||||
maxhullvert_attr.Get(&mesh->maxhullvert);
|
||||
if (!newton_maxhull || !newton_maxhull.HasAuthoredValue()) {
|
||||
mju_warning("Prim '%s' uses deprecated mjc:maxhullvert. "
|
||||
"Please migrate to newton:maxHullVertices.",
|
||||
mesh_collision_api.GetPrim().GetPath().GetText());
|
||||
}
|
||||
} else if (newton_maxhull && newton_maxhull.HasAuthoredValue()) {
|
||||
int val;
|
||||
newton_maxhull.Get(&val);
|
||||
mesh->maxhullvert = val;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,15 +1777,42 @@ void ParseUsdPhysicsMaterialAPI(
|
||||
}
|
||||
|
||||
void ParseMjcPhysicsMaterialAPI(
|
||||
mjsGeom* geom, const pxr::MjcPhysicsMaterialAPI& material_api) {
|
||||
auto torsional_friction_attr = material_api.GetTorsionalFrictionAttr();
|
||||
if (torsional_friction_attr.HasAuthoredValue()) {
|
||||
torsional_friction_attr.Get(&geom->friction[1]);
|
||||
mjsGeom* geom, const pxr::UsdPrim& material_prim,
|
||||
const pxr::MjcPhysicsMaterialAPI& material_api) {
|
||||
// Torsional friction: prefer newton:torsionalFriction, fall back to
|
||||
// mjc:torsionalfriction with deprecation warning. If both are authored,
|
||||
// mjc takes precedence for backwards compatibility.
|
||||
auto mjc_torsional = material_api.GetTorsionalFrictionAttr();
|
||||
auto newton_torsional = material_prim.GetAttribute(
|
||||
kNewtonTokens->newtonTorsionalFriction);
|
||||
if (mjc_torsional.HasAuthoredValue()) {
|
||||
mjc_torsional.Get(&geom->friction[1]);
|
||||
if (!newton_torsional || !newton_torsional.HasAuthoredValue()) {
|
||||
mju_warning("Prim '%s' uses deprecated mjc:torsionalfriction. "
|
||||
"Please migrate to newton:torsionalFriction.",
|
||||
material_prim.GetPath().GetText());
|
||||
}
|
||||
} else if (newton_torsional && newton_torsional.HasAuthoredValue()) {
|
||||
float val;
|
||||
newton_torsional.Get(&val);
|
||||
geom->friction[1] = val;
|
||||
}
|
||||
|
||||
auto rolling_friction_attr = material_api.GetRollingFrictionAttr();
|
||||
if (rolling_friction_attr.HasAuthoredValue()) {
|
||||
rolling_friction_attr.Get(&geom->friction[2]);
|
||||
// Rolling friction: same deprecation/fallback pattern.
|
||||
auto mjc_rolling = material_api.GetRollingFrictionAttr();
|
||||
auto newton_rolling = material_prim.GetAttribute(
|
||||
kNewtonTokens->newtonRollingFriction);
|
||||
if (mjc_rolling.HasAuthoredValue()) {
|
||||
mjc_rolling.Get(&geom->friction[2]);
|
||||
if (!newton_rolling || !newton_rolling.HasAuthoredValue()) {
|
||||
mju_warning("Prim '%s' uses deprecated mjc:rollingfriction. "
|
||||
"Please migrate to newton:rollingFriction.",
|
||||
material_prim.GetPath().GetText());
|
||||
}
|
||||
} else if (newton_rolling && newton_rolling.HasAuthoredValue()) {
|
||||
float val;
|
||||
newton_rolling.Get(&val);
|
||||
geom->friction[2] = val;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1789,11 +1925,13 @@ void ParseUsdPhysicsCollider(mjSpec* spec,
|
||||
if (bound_material) {
|
||||
pxr::UsdPrim bound_material_prim = bound_material.GetPrim();
|
||||
if (bound_material_prim.HasAPI<pxr::UsdPhysicsMaterialAPI>() ||
|
||||
bound_material_prim.HasAPI<pxr::MjcPhysicsMaterialAPI>()) {
|
||||
bound_material_prim.HasAPI<pxr::MjcPhysicsMaterialAPI>() ||
|
||||
bound_material_prim.HasAPI(kNewtonTokens->NewtonMaterialAPI)) {
|
||||
ParseUsdPhysicsMaterialAPI(
|
||||
geom, pxr::UsdPhysicsMaterialAPI(bound_material_prim));
|
||||
ParseMjcPhysicsMaterialAPI(
|
||||
geom, pxr::MjcPhysicsMaterialAPI(bound_material_prim));
|
||||
geom, bound_material_prim,
|
||||
pxr::MjcPhysicsMaterialAPI(bound_material_prim));
|
||||
}
|
||||
pxr::SdfPath material_path = bound_material_prim.GetPath();
|
||||
mjsMaterial* material = nullptr;
|
||||
@@ -1826,7 +1964,9 @@ void ParseUsdPhysicsCollider(mjSpec* spec,
|
||||
|
||||
if (!MaybeParseGeomPrimitive(prim, geom, caches.xform_cache)) {
|
||||
mjsMesh* mesh = ParseUsdMesh(spec, prim, geom, caches.xform_cache);
|
||||
if (mesh != nullptr && prim.HasAPI<pxr::MjcPhysicsMeshCollisionAPI>()) {
|
||||
if (mesh != nullptr &&
|
||||
(prim.HasAPI<pxr::MjcPhysicsMeshCollisionAPI>() ||
|
||||
prim.HasAPI(kNewtonTokens->NewtonMeshCollisionAPI))) {
|
||||
ParseMjcPhysicsMeshCollisionAPI(mesh,
|
||||
pxr::MjcPhysicsMeshCollisionAPI(prim));
|
||||
}
|
||||
@@ -1877,8 +2017,8 @@ void ParseMjcEqualityAPISolverParams(
|
||||
|
||||
void ParseConstraint(mjSpec* spec, const pxr::UsdPrim& prim, mjsBody* body,
|
||||
pxr::UsdGeomXformCache& xform_cache) {
|
||||
if (prim.HasAPI<pxr::MjcPhysicsEqualityJointAPI>()) {
|
||||
// Handle MjcPhysicsEqualityJointAPI on revolute/prismatic joints.
|
||||
if (prim.HasAPI<pxr::MjcPhysicsEqualityJointAPI>() ||
|
||||
prim.HasAPI(kNewtonTokens->NewtonMimicAPI)) {
|
||||
pxr::MjcPhysicsEqualityJointAPI eq_joint_api(prim);
|
||||
mjsEquality* eq = mjs_addEquality(spec, nullptr);
|
||||
eq->type = mjEQ_JOINT;
|
||||
@@ -1889,20 +2029,48 @@ void ParseConstraint(mjSpec* spec, const pxr::UsdPrim& prim, mjsBody* body,
|
||||
eq->objtype = mjOBJ_JOINT;
|
||||
mjs_setString(eq->name1, prim.GetPath().GetAsString().c_str());
|
||||
|
||||
// Get the target joint (joint2) from the MjcEqualityAPI target
|
||||
// relationship.
|
||||
pxr::MjcPhysicsEqualityAPI equality_api(prim);
|
||||
pxr::UsdRelationship target_rel = equality_api.GetMjcTargetRel();
|
||||
// Target joint: prefer newton:mimicJoint, fall back to deprecated mjc:target
|
||||
pxr::SdfPathVector targets;
|
||||
target_rel.GetTargets(&targets);
|
||||
if (!targets.empty()) {
|
||||
auto newton_mimic_rel = prim.GetRelationship(kNewtonTokens->newtonMimicJoint);
|
||||
if (newton_mimic_rel && newton_mimic_rel.GetTargets(&targets) && !targets.empty()) {
|
||||
mjs_setString(eq->name2, targets[0].GetAsString().c_str());
|
||||
} else {
|
||||
auto mjc_target_rel = prim.GetRelationship(MjcPhysicsTokens->mjcTarget);
|
||||
if (mjc_target_rel && mjc_target_rel.GetTargets(&targets) && !targets.empty()) {
|
||||
mjs_setString(eq->name2, targets[0].GetAsString().c_str());
|
||||
mju_warning("Prim '%s' uses deprecated mjc:target. "
|
||||
"Please migrate to newton:mimicJoint.",
|
||||
prim.GetPath().GetText());
|
||||
}
|
||||
}
|
||||
// If no target, name2 remains empty, meaning joint1 is fixed to a constant.
|
||||
|
||||
// Parse individual coefficient attributes for the quartic polynomial.
|
||||
eq_joint_api.GetCoef0Attr().Get(&eq->data[0]);
|
||||
eq_joint_api.GetCoef1Attr().Get(&eq->data[1]);
|
||||
// Coefficients: prefer Newton, fall back to deprecated MJC
|
||||
auto newton_coef0 = prim.GetAttribute(kNewtonTokens->newtonMimicCoef0);
|
||||
auto newton_coef1 = prim.GetAttribute(kNewtonTokens->newtonMimicCoef1);
|
||||
if (newton_coef0 && newton_coef0.HasAuthoredValue()) {
|
||||
float val;
|
||||
newton_coef0.Get(&val);
|
||||
eq->data[0] = val;
|
||||
} else {
|
||||
eq_joint_api.GetCoef0Attr().Get(&eq->data[0]);
|
||||
if (eq_joint_api.GetCoef0Attr().HasAuthoredValue()) {
|
||||
mju_warning("Prim '%s' uses deprecated mjc:coef0. "
|
||||
"Please migrate to newton:mimicCoef0.",
|
||||
prim.GetPath().GetText());
|
||||
}
|
||||
}
|
||||
if (newton_coef1 && newton_coef1.HasAuthoredValue()) {
|
||||
float val;
|
||||
newton_coef1.Get(&val);
|
||||
eq->data[1] = val;
|
||||
} else {
|
||||
eq_joint_api.GetCoef1Attr().Get(&eq->data[1]);
|
||||
if (eq_joint_api.GetCoef1Attr().HasAuthoredValue()) {
|
||||
mju_warning("Prim '%s' uses deprecated mjc:coef1. "
|
||||
"Please migrate to newton:mimicCoef1.",
|
||||
prim.GetPath().GetText());
|
||||
}
|
||||
}
|
||||
eq_joint_api.GetCoef2Attr().Get(&eq->data[2]);
|
||||
eq_joint_api.GetCoef3Attr().Get(&eq->data[3]);
|
||||
eq_joint_api.GetCoef4Attr().Get(&eq->data[4]);
|
||||
@@ -1910,7 +2078,27 @@ void ParseConstraint(mjSpec* spec, const pxr::UsdPrim& prim, mjsBody* body,
|
||||
pxr::UsdPhysicsJoint joint(prim);
|
||||
ParseJointEnabled(eq, joint);
|
||||
|
||||
ParseMjcEqualityAPISolverParams(eq, equality_api, prim);
|
||||
// Solver params are now inline on MjcEqualityJointAPI
|
||||
auto solref_attr = prim.GetAttribute(MjcPhysicsTokens->mjcSolref);
|
||||
if (solref_attr.HasAuthoredValue()) {
|
||||
pxr::VtDoubleArray solref;
|
||||
solref_attr.Get(&solref);
|
||||
if (solref.size() == mjNREF) {
|
||||
for (int i = 0; i < mjNREF; ++i) {
|
||||
eq->solref[i] = solref[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
auto solimp_attr = prim.GetAttribute(MjcPhysicsTokens->mjcSolimp);
|
||||
if (solimp_attr.HasAuthoredValue()) {
|
||||
pxr::VtDoubleArray solimp;
|
||||
solimp_attr.Get(&solimp);
|
||||
if (solimp.size() == mjNIMP) {
|
||||
for (int i = 0; i < mjNIMP; ++i) {
|
||||
eq->solimp[i] = solimp[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (prim.IsA<pxr::UsdPhysicsFixedJoint>() ||
|
||||
prim.IsA<pxr::UsdPhysicsSphericalJoint>()) {
|
||||
// Handle fixed joints as weld constraints, spherical joints as connect constraints
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
||||
// Copyright 2026 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
#ifndef MUJOCO_PYTHON_EXPERIMENTAL_DEAR_IMGUI_DEAR_IMGUI_MACROS_H_
|
||||
#define MUJOCO_PYTHON_EXPERIMENTAL_DEAR_IMGUI_DEAR_IMGUI_MACROS_H_
|
||||
|
||||
// WARNING: This file is intended for internal use by dear_imgui libraries ONLY!
|
||||
//
|
||||
// The macros defined here use short, generic names (DEF0, ARG_ID, etc.) and
|
||||
// are NOT #undef'd. Including this header elsewhere may cause naming conflicts.
|
||||
//
|
||||
// Define NAMESPACE to be the ImGui library you're binding before including this
|
||||
// header, e.g. #define NAMESPACE ImGui
|
||||
//
|
||||
// ============================================================================
|
||||
// Quick Reference
|
||||
// ============================================================================
|
||||
//
|
||||
// DEFn(Name, Args...) // Binds NAMESPACE::Name as Name in Python
|
||||
// DEFn_AS(CppName, PyName, Args...) // Binds NAMESPACE::CppName as PyName
|
||||
// DEFn_F(PyName, Args..., { CppBody }) // Custom implementation
|
||||
//
|
||||
// Where 'n' is the number of arguments (0-9).
|
||||
//
|
||||
// ============================================================================
|
||||
// Argument Format
|
||||
// ============================================================================
|
||||
//
|
||||
// Each argument is a tuple: (Type, name, DefaultValue)
|
||||
//
|
||||
// - No default value: (ImString, label, ) // trailing comma required
|
||||
// - With default value: (int, flags, = 0) // include the '='
|
||||
// - Complex defaults: (const ImVec2&, size, = ImVec2_Zero)
|
||||
//
|
||||
// NOTE: Default values cannot contain commas. Use predefined constants like
|
||||
// ImVec2_Zero, ImVec4_One, etc.
|
||||
//
|
||||
// ============================================================================
|
||||
// Examples (from dear_imgui.cc)
|
||||
// ============================================================================
|
||||
//
|
||||
// Simple binding - NAMESPACE::End() exposed as End():
|
||||
// DEF0(End);
|
||||
//
|
||||
// Binding with arguments:
|
||||
// DEF4(BeginChild,
|
||||
// (ImString, str_id, ),
|
||||
// (const ImVec2&, size, = ImVec2_Zero),
|
||||
// (ImGuiChildFlags, child_flags, = 0),
|
||||
// (ImGuiWindowFlags, window_flags, = 0));
|
||||
//
|
||||
// Overloaded function - NAMESPACE::BeginChild(ImGuiID) exposed as BeginChildId():
|
||||
// DEF4_AS(BeginChild, BeginChildId,
|
||||
// (ImGuiID, id, ),
|
||||
// (const ImVec2&, size, = ImVec2_Zero),
|
||||
// (ImGuiChildFlags, child_flags, = 0),
|
||||
// (ImGuiWindowFlags, window_flags, = 0));
|
||||
//
|
||||
// Custom implementation using DEFn_F is needed when:
|
||||
//
|
||||
// 1. Variadic functions (e.g., Text, TextColored)
|
||||
//
|
||||
// C++ variadic functions (those with "...") cannot be bound directly
|
||||
// because the type/count of arguments is unknown at compile time. Use a
|
||||
// wrapper that calls the function with a fixed format. Also note that
|
||||
// user-controlled format strings are a security risk (format string
|
||||
// attacks). Always use "%s":
|
||||
//
|
||||
// DEF1_F(Text, (ImString, txt, ), {
|
||||
// return NAMESPACE::Text("%s", txt);
|
||||
// });
|
||||
//
|
||||
// 2. Output pointer parameters (e.g., Checkbox, SliderFloat)
|
||||
//
|
||||
// Python doesn't have output pointers, so return modified values as a
|
||||
// tuple:
|
||||
//
|
||||
// DEF2_F(Checkbox, (ImString, label, ), (bool*, v, ), {
|
||||
// auto result = NAMESPACE::Checkbox(label, v);
|
||||
// return std::make_tuple(result, *v);
|
||||
// });
|
||||
//
|
||||
// 3. Type conversions (e.g., Image, ImageButton)
|
||||
//
|
||||
// Some C++ types don't have Python equivalents. For example, ImTextureID
|
||||
// is a void* (opaque pointer), which pybind11 can't automatically convert.
|
||||
// Accept a Python-friendly type (like long) and cast it:
|
||||
//
|
||||
// DEF2_F(Image, (long, tex_id, ), (const ImVec2&, size, ), {
|
||||
// return NAMESPACE::Image(reinterpret_cast<void*>(tex_id), size);
|
||||
// });
|
||||
|
||||
// ============================================================================
|
||||
// Internal helper macros (not intended to be called directly by binding code)
|
||||
// ============================================================================
|
||||
|
||||
// Extracts the type and name of an argument tuple.
|
||||
// Example: ARG_DECL((float, alpha, = 1.0f)) -> float alpha
|
||||
#define ARG_DECL_X(T_, N_, V_) T_ N_
|
||||
#define ARG_DECL(A_) ARG_DECL_X A_
|
||||
|
||||
// Extracts the identifier of an argument tuple.
|
||||
// Example: ARG_ID((float, alpha, = 1.0f)) -> alpha
|
||||
#define ARG_ID_X(T_, N_, V_) N_
|
||||
#define ARG_ID(A_) ARG_ID_X A_
|
||||
|
||||
// Extracts the name of an argument tuple as a quoted string literal.
|
||||
// Example: ARG_NAME((float, alpha, = 1.0f)) -> "alpha"
|
||||
#define ARG_NAME_X(T_, N_, V_) #N_
|
||||
#define ARG_NAME(A_) ARG_NAME_X A_
|
||||
|
||||
// Extracts the default value of an argument tuple.
|
||||
// Example: ARG_DEFVAL((float, alpha, = 1.0f)) -> = 1.0f
|
||||
#define ARG_DEFVAL_X(T_, N_, V_) V_
|
||||
#define ARG_DEFVAL(A_) ARG_DEFVAL_X A_
|
||||
|
||||
// ============================================================================
|
||||
// Public macros for binding code
|
||||
// ============================================================================
|
||||
|
||||
//
|
||||
#define DEF0_F(N, FN) \
|
||||
m.def(#N, []( \
|
||||
) FN \
|
||||
);
|
||||
|
||||
#define DEF1_F(N, A1, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1) \
|
||||
);
|
||||
|
||||
#define DEF2_F(N, A1, A2, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2) \
|
||||
);
|
||||
|
||||
#define DEF3_F(N, A1, A2, A3, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3) \
|
||||
);
|
||||
|
||||
#define DEF4_F(N, A1, A2, A3, A4, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3), \
|
||||
ARG_DECL(A4) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \
|
||||
py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4) \
|
||||
);
|
||||
|
||||
#define DEF5_F(N, A1, A2, A3, A4, A5, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3), \
|
||||
ARG_DECL(A4), \
|
||||
ARG_DECL(A5) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \
|
||||
py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \
|
||||
py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5) \
|
||||
);
|
||||
|
||||
#define DEF6_F(N, A1, A2, A3, A4, A5, A6, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3), \
|
||||
ARG_DECL(A4), \
|
||||
ARG_DECL(A5), \
|
||||
ARG_DECL(A6) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \
|
||||
py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \
|
||||
py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \
|
||||
py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6) \
|
||||
);
|
||||
|
||||
#define DEF7_F(N, A1, A2, A3, A4, A5, A6, A7, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3), \
|
||||
ARG_DECL(A4), \
|
||||
ARG_DECL(A5), \
|
||||
ARG_DECL(A6), \
|
||||
ARG_DECL(A7) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \
|
||||
py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \
|
||||
py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \
|
||||
py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \
|
||||
py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7) \
|
||||
);
|
||||
|
||||
#define DEF8_F(N, A1, A2, A3, A4, A5, A6, A7, A8, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3), \
|
||||
ARG_DECL(A4), \
|
||||
ARG_DECL(A5), \
|
||||
ARG_DECL(A6), \
|
||||
ARG_DECL(A7), \
|
||||
ARG_DECL(A8) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \
|
||||
py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \
|
||||
py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \
|
||||
py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \
|
||||
py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7), \
|
||||
py::arg(ARG_NAME(A8)) ARG_DEFVAL(A8) \
|
||||
);
|
||||
|
||||
#define DEF9_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3), \
|
||||
ARG_DECL(A4), \
|
||||
ARG_DECL(A5), \
|
||||
ARG_DECL(A6), \
|
||||
ARG_DECL(A7), \
|
||||
ARG_DECL(A8), \
|
||||
ARG_DECL(A9) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \
|
||||
py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \
|
||||
py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \
|
||||
py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \
|
||||
py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7), \
|
||||
py::arg(ARG_NAME(A8)) ARG_DEFVAL(A8), \
|
||||
py::arg(ARG_NAME(A9)) ARG_DEFVAL(A9) \
|
||||
);
|
||||
|
||||
#define DEF10_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, FN) \
|
||||
m.def(#N, []( \
|
||||
ARG_DECL(A1), \
|
||||
ARG_DECL(A2), \
|
||||
ARG_DECL(A3), \
|
||||
ARG_DECL(A4), \
|
||||
ARG_DECL(A5), \
|
||||
ARG_DECL(A6), \
|
||||
ARG_DECL(A7), \
|
||||
ARG_DECL(A8), \
|
||||
ARG_DECL(A9), \
|
||||
ARG_DECL(A10) \
|
||||
) FN, \
|
||||
py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \
|
||||
py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \
|
||||
py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \
|
||||
py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \
|
||||
py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \
|
||||
py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \
|
||||
py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7), \
|
||||
py::arg(ARG_NAME(A8)) ARG_DEFVAL(A8), \
|
||||
py::arg(ARG_NAME(A9)) ARG_DEFVAL(A9), \
|
||||
py::arg(ARG_NAME(A10)) ARG_DEFVAL(A10) \
|
||||
);
|
||||
|
||||
// NOLINTBEGIN(whitespace/line_length)
|
||||
|
||||
#define DEF0_AS(N, AS) DEF0_F(AS, { return NAMESPACE::N(); } )
|
||||
#define DEF1_AS(N, AS, A1) DEF1_F(AS, A1, { return NAMESPACE::N(ARG_ID(A1)); } )
|
||||
#define DEF2_AS(N, AS, A1, A2) DEF2_F(AS, A1, A2, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2)); } )
|
||||
#define DEF3_AS(N, AS, A1, A2, A3) DEF3_F(AS, A1, A2, A3, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3)); } )
|
||||
#define DEF4_AS(N, AS, A1, A2, A3, A4) DEF4_F(AS, A1, A2, A3, A4, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4)); } )
|
||||
#define DEF5_AS(N, AS, A1, A2, A3, A4, A5) DEF5_F(AS, A1, A2, A3, A4, A5, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5)); } )
|
||||
#define DEF6_AS(N, AS, A1, A2, A3, A4, A5, A6) DEF6_F(AS, A1, A2, A3, A4, A5, A6, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6)); } )
|
||||
#define DEF7_AS(N, AS, A1, A2, A3, A4, A5, A6, A7) DEF7_F(AS, A1, A2, A3, A4, A5, A6, A7, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7)); } )
|
||||
#define DEF8_AS(N, AS, A1, A2, A3, A4, A5, A6, A7, A8) DEF8_F(AS, A1, A2, A3, A4, A5, A6, A7, A8, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8)); } )
|
||||
#define DEF9_AS(N, AS, A1, A2, A3, A4, A5, A6, A7, A8, A9) DEF9_F(AS, A1, A2, A3, A4, A5, A6, A7, A8, A9, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9)); } )
|
||||
#define DEF10_AS(N, AS, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) DEF10_F(AS, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9), ARG_ID(A10)); } )
|
||||
|
||||
#define DEF0(N) DEF0_F(N, { return NAMESPACE::N(); } )
|
||||
#define DEF1(N, A1) DEF1_F(N, A1, { return NAMESPACE::N(ARG_ID(A1)); } )
|
||||
#define DEF2(N, A1, A2) DEF2_F(N, A1, A2, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2)); } )
|
||||
#define DEF3(N, A1, A2, A3) DEF3_F(N, A1, A2, A3, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3)); } )
|
||||
#define DEF4(N, A1, A2, A3, A4) DEF4_F(N, A1, A2, A3, A4, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4)); } )
|
||||
#define DEF5(N, A1, A2, A3, A4, A5) DEF5_F(N, A1, A2, A3, A4, A5, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5)); } )
|
||||
#define DEF6(N, A1, A2, A3, A4, A5, A6) DEF6_F(N, A1, A2, A3, A4, A5, A6, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6)); } )
|
||||
#define DEF7(N, A1, A2, A3, A4, A5, A6, A7) DEF7_F(N, A1, A2, A3, A4, A5, A6, A7, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7)); } )
|
||||
#define DEF8(N, A1, A2, A3, A4, A5, A6, A7, A8) DEF8_F(N, A1, A2, A3, A4, A5, A6, A7, A8, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8)); } )
|
||||
#define DEF9(N, A1, A2, A3, A4, A5, A6, A7, A8, A9) DEF9_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9)); } )
|
||||
#define DEF10(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) DEF10_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9), ARG_ID(A10)); } )
|
||||
|
||||
// NOLINTEND(whitespace/line_length)
|
||||
|
||||
#endif // MUJOCO_PYTHON_EXPERIMENTAL_DEAR_IMGUI_DEAR_IMGUI_MACROS_H_
|
||||
@@ -0,0 +1,438 @@
|
||||
// Copyright 2026 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
#define NAMESPACE ImPlot
|
||||
#include "dear_imgui_macros.h"
|
||||
#include <implot.h>
|
||||
#include <pybind11/eval.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
// NOLINTBEGIN(whitespace/line_length)
|
||||
|
||||
namespace py = pybind11;
|
||||
using ImString = const char*;
|
||||
static constexpr const ImVec2 ImVec2_Zero = ImVec2(0.0f, 0.0f);
|
||||
static constexpr const ImVec2 ImVec2_One = ImVec2(1.0f, 1.0f);
|
||||
static constexpr const ImVec2 ImVec2_NegOne_Zero = ImVec2(-1.0f, 0.0f);
|
||||
static constexpr const ImVec4 ImVec4_Zero = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
static constexpr const ImVec4 ImVec4_One = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
static constexpr const ImPlotRect ImPlotRect_Default{};
|
||||
static constexpr const ImPlotRange ImPlotRange_Default{};
|
||||
|
||||
PYBIND11_MODULE(implot, m) {
|
||||
// Import dear_imgui to make types like ImVec2 available.
|
||||
py::module_::import("mujoco.experimental.dear_imgui.dear_imgui");
|
||||
|
||||
// Types.
|
||||
py::class_<ImPlotPoint>(m, "Point")
|
||||
.def(py::init<>())
|
||||
.def(py::init<double, double>(), py::arg("_x"), py::arg("_y"))
|
||||
.def_readwrite("x", &ImPlotPoint::x)
|
||||
.def_readwrite("y", &ImPlotPoint::y);
|
||||
|
||||
py::class_<ImPlotRange>(m, "Range")
|
||||
.def(py::init<>())
|
||||
.def(py::init<double, double>(), py::arg("_min"), py::arg("_max"))
|
||||
.def_readwrite("min", &ImPlotRange::Min)
|
||||
.def_readwrite("max", &ImPlotRange::Max);
|
||||
|
||||
// py::class_<ImPlotRect>(m, "Rect")
|
||||
// .def(py::init<>())
|
||||
// .def(py::init<ImPlotRange, ImPlotRange>(), py::arg("_x"), py::arg("_y"))
|
||||
// .def_readwrite("x", &ImPlotRect::X)
|
||||
// .def_readwrite("y", &ImPlotRect::Y);
|
||||
|
||||
// Enumerations.
|
||||
|
||||
py::enum_<ImAxis_>(m, "Axis")
|
||||
.value("X1", ImAxis_X1)
|
||||
.value("X2", ImAxis_X2)
|
||||
.value("X3", ImAxis_X3)
|
||||
.value("Y1", ImAxis_Y1)
|
||||
.value("Y2", ImAxis_Y2)
|
||||
.value("Y3", ImAxis_Y3);
|
||||
|
||||
py::enum_<ImPlotFlags_>(m, "Flags")
|
||||
.value("None", ImPlotFlags_None)
|
||||
.value("NoTitle", ImPlotFlags_NoTitle)
|
||||
.value("NoLegend", ImPlotFlags_NoLegend)
|
||||
.value("NoMouseText", ImPlotFlags_NoMouseText)
|
||||
.value("NoInputs", ImPlotFlags_NoInputs)
|
||||
.value("NoMenus", ImPlotFlags_NoMenus)
|
||||
.value("NoBoxSelect", ImPlotFlags_NoBoxSelect)
|
||||
.value("NoFrame", ImPlotFlags_NoFrame)
|
||||
.value("Equal", ImPlotFlags_Equal)
|
||||
.value("Crosshairs", ImPlotFlags_Crosshairs)
|
||||
.value("CanvasOnly", ImPlotFlags_CanvasOnly);
|
||||
|
||||
py::enum_<ImPlotAxisFlags_>(m, "AxisFlags")
|
||||
.value("None", ImPlotAxisFlags_None)
|
||||
.value("NoLabel", ImPlotAxisFlags_NoLabel)
|
||||
.value("NoGridLines", ImPlotAxisFlags_NoGridLines)
|
||||
.value("NoTickMarks", ImPlotAxisFlags_NoTickMarks)
|
||||
.value("NoTickLabels", ImPlotAxisFlags_NoTickLabels)
|
||||
.value("NoInitialFit", ImPlotAxisFlags_NoInitialFit)
|
||||
.value("NoMenus", ImPlotAxisFlags_NoMenus)
|
||||
.value("NoSideSwitch", ImPlotAxisFlags_NoSideSwitch)
|
||||
.value("NoHighlight", ImPlotAxisFlags_NoHighlight)
|
||||
.value("Opposite", ImPlotAxisFlags_Opposite)
|
||||
.value("Foreground", ImPlotAxisFlags_Foreground)
|
||||
.value("Invert", ImPlotAxisFlags_Invert)
|
||||
.value("AutoFit", ImPlotAxisFlags_AutoFit)
|
||||
.value("RangeFit", ImPlotAxisFlags_RangeFit)
|
||||
.value("PanStretch", ImPlotAxisFlags_PanStretch)
|
||||
.value("LockMin", ImPlotAxisFlags_LockMin)
|
||||
.value("LockMax", ImPlotAxisFlags_LockMax)
|
||||
.value("Lock", ImPlotAxisFlags_Lock)
|
||||
.value("NoDecorations", ImPlotAxisFlags_NoDecorations)
|
||||
.value("AuxDefault", ImPlotAxisFlags_AuxDefault);
|
||||
|
||||
py::enum_<ImPlotSubplotFlags_>(m, "SubplotFlags")
|
||||
.value("None", ImPlotSubplotFlags_None)
|
||||
.value("NoTitle", ImPlotSubplotFlags_NoTitle)
|
||||
.value("NoLegend", ImPlotSubplotFlags_NoLegend)
|
||||
.value("NoMenus", ImPlotSubplotFlags_NoMenus)
|
||||
.value("NoResize", ImPlotSubplotFlags_NoResize)
|
||||
.value("NoAlign", ImPlotSubplotFlags_NoAlign)
|
||||
.value("ShareItems", ImPlotSubplotFlags_ShareItems)
|
||||
.value("LinkRows", ImPlotSubplotFlags_LinkRows)
|
||||
.value("LinkCols", ImPlotSubplotFlags_LinkCols)
|
||||
.value("LinkAllX", ImPlotSubplotFlags_LinkAllX)
|
||||
.value("LinkAllY", ImPlotSubplotFlags_LinkAllY)
|
||||
.value("ColMajor", ImPlotSubplotFlags_ColMajor);
|
||||
|
||||
py::enum_<ImPlotLegendFlags_>(m, "LegendFlags")
|
||||
.value("None", ImPlotLegendFlags_None)
|
||||
.value("NoButtons", ImPlotLegendFlags_NoButtons)
|
||||
.value("NoHighlightItem", ImPlotLegendFlags_NoHighlightItem)
|
||||
.value("NoHighlightAxis", ImPlotLegendFlags_NoHighlightAxis)
|
||||
.value("NoMenus", ImPlotLegendFlags_NoMenus)
|
||||
.value("Outside", ImPlotLegendFlags_Outside)
|
||||
.value("Horizontal", ImPlotLegendFlags_Horizontal)
|
||||
.value("Sort", ImPlotLegendFlags_Sort)
|
||||
.value("Reverse", ImPlotLegendFlags_Reverse);
|
||||
|
||||
py::enum_<ImPlotMouseTextFlags_>(m, "MouseTextFlags")
|
||||
.value("None", ImPlotMouseTextFlags_None)
|
||||
.value("NoAuxAxes", ImPlotMouseTextFlags_NoAuxAxes)
|
||||
.value("NoFormat", ImPlotMouseTextFlags_NoFormat)
|
||||
.value("ShowAlways", ImPlotMouseTextFlags_ShowAlways);
|
||||
|
||||
py::enum_<ImPlotDragToolFlags_>(m, "DragToolFlags")
|
||||
.value("None", ImPlotDragToolFlags_None)
|
||||
.value("NoCursors", ImPlotDragToolFlags_NoCursors)
|
||||
.value("NoFit", ImPlotDragToolFlags_NoFit)
|
||||
.value("NoInputs", ImPlotDragToolFlags_NoInputs)
|
||||
.value("Delayed", ImPlotDragToolFlags_Delayed);
|
||||
|
||||
py::enum_<ImPlotColormapScaleFlags_>(m, "ColormapScaleFlags")
|
||||
.value("None", ImPlotColormapScaleFlags_None)
|
||||
.value("NoLabel", ImPlotColormapScaleFlags_NoLabel)
|
||||
.value("Opposite", ImPlotColormapScaleFlags_Opposite)
|
||||
.value("Invert", ImPlotColormapScaleFlags_Invert);
|
||||
|
||||
py::enum_<ImPlotItemFlags_>(m, "ItemFlags")
|
||||
.value("None", ImPlotItemFlags_None)
|
||||
.value("NoLegend", ImPlotItemFlags_NoLegend)
|
||||
.value("NoFit", ImPlotItemFlags_NoFit);
|
||||
|
||||
py::enum_<ImPlotLineFlags_>(m, "LineFlags")
|
||||
.value("None", ImPlotLineFlags_None)
|
||||
.value("Segments", ImPlotLineFlags_Segments)
|
||||
.value("Loop", ImPlotLineFlags_Loop)
|
||||
.value("SkipNaN", ImPlotLineFlags_SkipNaN)
|
||||
.value("NoClip", ImPlotLineFlags_NoClip)
|
||||
.value("Shaded", ImPlotLineFlags_Shaded);
|
||||
|
||||
py::enum_<ImPlotScatterFlags_>(m, "ScatterFlags")
|
||||
.value("None", ImPlotScatterFlags_None)
|
||||
.value("NoClip", ImPlotScatterFlags_NoClip);
|
||||
|
||||
py::enum_<ImPlotStairsFlags_>(m, "StairsFlags")
|
||||
.value("None", ImPlotStairsFlags_None)
|
||||
.value("PreStep", ImPlotStairsFlags_PreStep)
|
||||
.value("Shaded", ImPlotStairsFlags_Shaded);
|
||||
|
||||
py::enum_<ImPlotShadedFlags_>(m, "ShadedFlags")
|
||||
.value("None", ImPlotShadedFlags_None);
|
||||
|
||||
py::enum_<ImPlotBarsFlags_>(m, "BarsFlags")
|
||||
.value("None", ImPlotBarsFlags_None)
|
||||
.value("Horizontal", ImPlotBarsFlags_Horizontal);
|
||||
|
||||
py::enum_<ImPlotBarGroupsFlags_>(m, "BarGroupsFlags")
|
||||
.value("None", ImPlotBarGroupsFlags_None)
|
||||
.value("Horizontal", ImPlotBarGroupsFlags_Horizontal)
|
||||
.value("Stacked", ImPlotBarGroupsFlags_Stacked);
|
||||
|
||||
py::enum_<ImPlotErrorBarsFlags_>(m, "ErrorBarsFlags")
|
||||
.value("None", ImPlotErrorBarsFlags_None)
|
||||
.value("Horizontal", ImPlotErrorBarsFlags_Horizontal);
|
||||
|
||||
py::enum_<ImPlotStemsFlags_>(m, "StemsFlags")
|
||||
.value("None", ImPlotStemsFlags_None)
|
||||
.value("Horizontal", ImPlotStemsFlags_Horizontal);
|
||||
|
||||
py::enum_<ImPlotInfLinesFlags_>(m, "InfLinesFlags")
|
||||
.value("None", ImPlotInfLinesFlags_None)
|
||||
.value("Horizontal", ImPlotInfLinesFlags_Horizontal);
|
||||
|
||||
py::enum_<ImPlotPieChartFlags_>(m, "PieChartFlags")
|
||||
.value("None", ImPlotPieChartFlags_None)
|
||||
.value("Normalize", ImPlotPieChartFlags_Normalize)
|
||||
.value("IgnoreHidden", ImPlotPieChartFlags_IgnoreHidden)
|
||||
.value("Exploding", ImPlotPieChartFlags_Exploding);
|
||||
|
||||
py::enum_<ImPlotHeatmapFlags_>(m, "HeatmapFlags")
|
||||
.value("None", ImPlotHeatmapFlags_None)
|
||||
.value("ColMajor", ImPlotHeatmapFlags_ColMajor);
|
||||
|
||||
py::enum_<ImPlotHistogramFlags_>(m, "HistogramFlags")
|
||||
.value("None", ImPlotHistogramFlags_None)
|
||||
.value("Horizontal", ImPlotHistogramFlags_Horizontal)
|
||||
.value("Cumulative", ImPlotHistogramFlags_Cumulative)
|
||||
.value("Density", ImPlotHistogramFlags_Density)
|
||||
.value("NoOutliers", ImPlotHistogramFlags_NoOutliers)
|
||||
.value("ColMajor", ImPlotHistogramFlags_ColMajor);
|
||||
|
||||
py::enum_<ImPlotDigitalFlags_>(m, "DigitalFlags")
|
||||
.value("ImPlotNone", ImPlotDigitalFlags_None);
|
||||
|
||||
py::enum_<ImPlotImageFlags_>(m, "ImageFlags")
|
||||
.value("None", ImPlotImageFlags_None);
|
||||
|
||||
py::enum_<ImPlotTextFlags_>(m, "TextFlags")
|
||||
.value("None", ImPlotTextFlags_None)
|
||||
.value("Vertical", ImPlotTextFlags_Vertical);
|
||||
|
||||
py::enum_<ImPlotDummyFlags_>(m, "DummyFlags")
|
||||
.value("None", ImPlotDummyFlags_None);
|
||||
|
||||
py::enum_<ImPlotCond_>(m, "Cond")
|
||||
.value("None", ImPlotCond_None)
|
||||
.value("Always", ImPlotCond_Always)
|
||||
.value("Once", ImPlotCond_Once);
|
||||
|
||||
py::enum_<ImPlotCol_>(m, "Col")
|
||||
.value("Line", ImPlotCol_Line)
|
||||
.value("Fill", ImPlotCol_Fill)
|
||||
.value("MarkerOutline", ImPlotCol_MarkerOutline)
|
||||
.value("MarkerFill", ImPlotCol_MarkerFill)
|
||||
.value("ErrorBar", ImPlotCol_ErrorBar)
|
||||
.value("FrameBg", ImPlotCol_FrameBg)
|
||||
.value("PlotBg", ImPlotCol_PlotBg)
|
||||
.value("PlotBorder", ImPlotCol_PlotBorder)
|
||||
.value("LegendBg", ImPlotCol_LegendBg)
|
||||
.value("LegendBorder", ImPlotCol_LegendBorder)
|
||||
.value("LegendText", ImPlotCol_LegendText)
|
||||
.value("TitleText", ImPlotCol_TitleText)
|
||||
.value("InlayText", ImPlotCol_InlayText)
|
||||
.value("AxisText", ImPlotCol_AxisText)
|
||||
.value("AxisGrid", ImPlotCol_AxisGrid)
|
||||
.value("AxisTick", ImPlotCol_AxisTick)
|
||||
.value("AxisBg", ImPlotCol_AxisBg)
|
||||
.value("AxisBgHovered", ImPlotCol_AxisBgHovered)
|
||||
.value("AxisBgActive", ImPlotCol_AxisBgActive)
|
||||
.value("Selection", ImPlotCol_Selection)
|
||||
.value("Crosshairs", ImPlotCol_Crosshairs);
|
||||
|
||||
py::enum_<ImPlotStyleVar_>(m, "StyleVar")
|
||||
.value("LineWeight", ImPlotStyleVar_LineWeight)
|
||||
.value("Marker", ImPlotStyleVar_Marker)
|
||||
.value("MarkerSize", ImPlotStyleVar_MarkerSize)
|
||||
.value("MarkerWeight", ImPlotStyleVar_MarkerWeight)
|
||||
.value("FillAlpha", ImPlotStyleVar_FillAlpha)
|
||||
.value("ErrorBarSize", ImPlotStyleVar_ErrorBarSize)
|
||||
.value("ErrorBarWeight", ImPlotStyleVar_ErrorBarWeight)
|
||||
.value("DigitalBitHeight", ImPlotStyleVar_DigitalBitHeight)
|
||||
.value("DigitalBitGap", ImPlotStyleVar_DigitalBitGap)
|
||||
.value("PlotBorderSize", ImPlotStyleVar_PlotBorderSize)
|
||||
.value("MinorAlpha", ImPlotStyleVar_MinorAlpha)
|
||||
.value("MajorTickLen", ImPlotStyleVar_MajorTickLen)
|
||||
.value("MinorTickLen", ImPlotStyleVar_MinorTickLen)
|
||||
.value("MajorTickSize", ImPlotStyleVar_MajorTickSize)
|
||||
.value("MinorTickSize", ImPlotStyleVar_MinorTickSize)
|
||||
.value("MajorGridSize", ImPlotStyleVar_MajorGridSize)
|
||||
.value("MinorGridSize", ImPlotStyleVar_MinorGridSize)
|
||||
.value("PlotPadding", ImPlotStyleVar_PlotPadding)
|
||||
.value("LabelPadding", ImPlotStyleVar_LabelPadding)
|
||||
.value("LegendPadding", ImPlotStyleVar_LegendPadding)
|
||||
.value("LegendInnerPadding", ImPlotStyleVar_LegendInnerPadding)
|
||||
.value("LegendSpacing", ImPlotStyleVar_LegendSpacing)
|
||||
.value("MousePosPadding", ImPlotStyleVar_MousePosPadding)
|
||||
.value("AnnotationPadding", ImPlotStyleVar_AnnotationPadding)
|
||||
.value("FitPadding", ImPlotStyleVar_FitPadding)
|
||||
.value("PlotDefaultSize", ImPlotStyleVar_PlotDefaultSize)
|
||||
.value("PlotMinSize", ImPlotStyleVar_PlotMinSize);
|
||||
|
||||
py::enum_<ImPlotScale_>(m, "Scale")
|
||||
.value("ImPlotScale_Linear", ImPlotScale_Linear)
|
||||
.value("ImPlotScale_Time", ImPlotScale_Time)
|
||||
.value("ImPlotScale_Log10", ImPlotScale_Log10)
|
||||
.value("ImPlotScale_SymLog", ImPlotScale_SymLog);
|
||||
|
||||
py::enum_<ImPlotMarker_>(m, "Marker")
|
||||
.value("None", ImPlotMarker_None)
|
||||
.value("Circle", ImPlotMarker_Circle)
|
||||
.value("Square", ImPlotMarker_Square)
|
||||
.value("Diamond", ImPlotMarker_Diamond)
|
||||
.value("Up", ImPlotMarker_Up)
|
||||
.value("Down", ImPlotMarker_Down)
|
||||
.value("Left", ImPlotMarker_Left)
|
||||
.value("Right", ImPlotMarker_Right)
|
||||
.value("Cross", ImPlotMarker_Cross)
|
||||
.value("Plus", ImPlotMarker_Plus)
|
||||
.value("Asterisk", ImPlotMarker_Asterisk);
|
||||
|
||||
py::enum_<ImPlotLocation_>(m, "Location")
|
||||
.value("Center", ImPlotLocation_Center)
|
||||
.value("North", ImPlotLocation_North)
|
||||
.value("South", ImPlotLocation_South)
|
||||
.value("West", ImPlotLocation_West)
|
||||
.value("East", ImPlotLocation_East)
|
||||
.value("NorthWest", ImPlotLocation_NorthWest)
|
||||
.value("NorthEast", ImPlotLocation_NorthEast)
|
||||
.value("SouthWest", ImPlotLocation_SouthWest)
|
||||
.value("SouthEast", ImPlotLocation_SouthEast);
|
||||
|
||||
// Functions.
|
||||
|
||||
DEF3(BeginPlot, (ImString, title_id, ), (const ImVec2&, size, = ImVec2_NegOne_Zero), (ImPlotFlags, flags, = 0));
|
||||
DEF0(EndPlot);
|
||||
DEF7(BeginSubplots, (ImString, title_id, ), (int, rows, ), (int, cols, ), (const ImVec2&, size, ), (ImPlotSubplotFlags, flags, = 0), (float*, row_ratios, = nullptr), (float*, col_ratios, = nullptr));
|
||||
DEF0(EndSubplots);
|
||||
DEF3(SetupAxis, (ImAxis, axis, ), (ImString, label, = nullptr), (ImPlotAxisFlags, flags, = 0));
|
||||
DEF4(SetupAxisLimits, (ImAxis, axis, ), (double, v_min, ), (double, v_max, ), (ImPlotCond, cond, = ImPlotCond_Once));
|
||||
DEF4_F(SetupAxisTicks, (ImAxis, axis, ), (std::vector<double>, values, ), (std::vector<std::string>, labels, ), (bool, keep_default, = false), {
|
||||
std::vector<const char*> c_labels;
|
||||
c_labels.reserve(labels.size());
|
||||
for (const auto& l : labels) {
|
||||
c_labels.push_back(l.c_str());
|
||||
}
|
||||
ImPlot::SetupAxisTicks(axis, values.data(), values.size(), c_labels.empty() ? nullptr : c_labels.data(), keep_default);
|
||||
});
|
||||
DEF3(SetupAxisLinks, (ImAxis, axis, ), (double*, link_min, ), (double*, link_max, ));
|
||||
DEF2(SetupAxisFormat, (ImAxis, axis, ), (ImString, fmt, ));
|
||||
DEF2(SetupAxisScale, (ImAxis, axis, ), (ImPlotScale, scale, ));
|
||||
DEF3(SetupAxisLimitsConstraints, (ImAxis, axis, ), (double, v_min, ), (double, v_max, ));
|
||||
DEF3(SetupAxisZoomConstraints, (ImAxis, axis, ), (double, z_min, ), (double, z_max, ));
|
||||
DEF4(SetupAxes, (ImString, x_label, ), (ImString, y_label, ), (ImPlotAxisFlags, x_flags, = 0), (ImPlotAxisFlags, y_flags, = 0));
|
||||
DEF5(SetupAxesLimits, (double, x_min, ), (double, x_max, ), (double, y_min, ), (double, y_max, ), (ImPlotCond, cond, = ImPlotCond_Once));
|
||||
DEF2(SetupLegend, (ImPlotLocation, location, ), (ImPlotLegendFlags, flags, = 0));
|
||||
DEF2(SetupMouseText, (ImPlotLocation, location, ), (ImPlotMouseTextFlags, flags, = 0));
|
||||
DEF0(SetupFinish);
|
||||
DEF4(SetNextAxisLimits, (ImAxis, axis, ), (double, v_min, ), (double, v_max, ), (ImPlotCond, cond, = ImPlotCond_Once));
|
||||
DEF3(SetNextAxisLinks, (ImAxis, axis, ), (double*, link_min, ), (double*, link_max, ));
|
||||
DEF1(SetNextAxisToFit, (ImAxis, axis, ));
|
||||
DEF5(SetNextAxesLimits, (double, x_min, ), (double, x_max, ), (double, y_min, ), (double, y_max, ), (ImPlotCond, cond, = ImPlotCond_Once));
|
||||
DEF0_F(SetNextAxesToFit, {
|
||||
return ImPlot::SetNextAxesToFit();
|
||||
});
|
||||
DEF6_F(PlotLine, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (ImPlotLineFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotLine(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride);
|
||||
});
|
||||
DEF6_F(PlotScatter, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (ImPlotScatterFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotScatter(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride);
|
||||
});
|
||||
DEF6_F(PlotStairs, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (ImPlotStairsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotStairs(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride);
|
||||
});
|
||||
DEF7_F(PlotShaded, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (double, yref, = 0), (ImPlotShadedFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotShaded(label_id, xs.data(), ys.data(), xs.size(), yref, flags, offset, stride);
|
||||
});
|
||||
DEF7_F(PlotShaded, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys1, ), (std::vector<double>, ys2, ), (ImPlotShadedFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotShaded(label_id, xs.data(), ys1.data(), ys2.data(), xs.size(), flags, offset, stride);
|
||||
});
|
||||
DEF7_F(PlotBars, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (double, bar_size, ), (ImPlotBarsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotBars(label_id, xs.data(), ys.data(), xs.size(), bar_size, flags, offset, stride);
|
||||
});
|
||||
DEF7_F(PlotErrorBars, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (std::vector<double>, err, ), (ImPlotErrorBarsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotErrorBars(label_id, xs.data(), ys.data(), err.data(), xs.size(), flags, offset, stride);
|
||||
});
|
||||
DEF8_F(PlotErrorBars, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (std::vector<double>, neg, ), (std::vector<double>, pos, ), (ImPlotErrorBarsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotErrorBars(label_id, xs.data(), ys.data(), neg.data(), pos.data(), xs.size(), flags, offset, stride);
|
||||
});
|
||||
DEF7_F(PlotStems, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (double, ref, = 0), (ImPlotStemsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotStems(label_id, xs.data(), ys.data(), xs.size(), ref, flags, offset, stride);
|
||||
});
|
||||
DEF6_F(PlotDigital, (ImString, label_id, ), (std::vector<double>, xs, ), (std::vector<double>, ys, ), (ImPlotDigitalFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), {
|
||||
return ImPlot::PlotDigital(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride);
|
||||
});
|
||||
DEF8(PlotImage, (ImString, label_id, ), (ImTextureRef, tex_ref, ), (const ImPlotPoint&, bounds_min, ), (const ImPlotPoint&, bounds_max, ), (const ImVec2&, uv0, = ImVec2_Zero), (const ImVec2&, uv1, = ImVec2_One), (const ImVec4&, tint_col, = ImVec4_One), (ImPlotImageFlags, flags, = 0));
|
||||
DEF5(PlotText, (ImString, text, ), (double, x, ), (double, y, ), (const ImVec2&, pix_offset, = ImVec2_Zero), (ImPlotTextFlags, flags, = 0));
|
||||
DEF2(PlotDummy, (ImString, label_id, ), (ImPlotDummyFlags, flags, = 0));
|
||||
DEF9(DragPoint, (int, id, ), (double*, x, ), (double*, y, ), (const ImVec4&, col, ), (float, size, = 4), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr));
|
||||
DEF8(DragLineX, (int, id, ), (double*, x, ), (const ImVec4&, col, ), (float, thickness, = 1), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr));
|
||||
DEF8(DragLineY, (int, id, ), (double*, y, ), (const ImVec4&, col, ), (float, thickness, = 1), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr));
|
||||
DEF10(DragRect, (int, id, ), (double*, x1, ), (double*, y1, ), (double*, x2, ), (double*, y2, ), (const ImVec4&, col, ), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr));
|
||||
DEF6(Annotation, (double, x, ), (double, y, ), (const ImVec4&, col, ), (const ImVec2&, pix_offset, ), (bool, clamp, ), (bool, round, = false));
|
||||
DEF6_F(Annotation, (double, x, ), (double, y, ), (const ImVec4&, col, ), (const ImVec2&, pix_offset, ), (bool, clamp, ), (ImString, txt, ), {
|
||||
return ImPlot::Annotation(x, y, col, pix_offset, clamp, "%s", txt);
|
||||
});
|
||||
DEF3(TagX, (double, x, ), (const ImVec4&, col, ), (bool, round, = false));
|
||||
DEF3_F(TagX, (double, x, ), (const ImVec4&, col, ), (ImString, txt, ), {
|
||||
return ImPlot::TagX(x, col, "%s", txt);
|
||||
});
|
||||
DEF3(TagY, (double, y, ), (const ImVec4&, col, ), (bool, round, = false));
|
||||
DEF3_F(TagY, (double, y, ), (const ImVec4&, col, ), (ImString, txt, ), {
|
||||
return ImPlot::TagY(y, col, "%s", txt);
|
||||
});
|
||||
DEF1(SetAxis, (ImAxis, axis, ));
|
||||
DEF2(SetAxes, (ImAxis, x_axis, ), (ImAxis, y_axis, ));
|
||||
DEF3(PixelsToPlot, (const ImVec2&, pix, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO));
|
||||
DEF4(PixelsToPlot, (float, x, ), (float, y, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO));
|
||||
DEF3(PlotToPixels, (const ImPlotPoint&, plt, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO));
|
||||
DEF4(PlotToPixels, (double, x, ), (double, y, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO));
|
||||
DEF0(GetPlotPos);
|
||||
DEF0(GetPlotSize);
|
||||
DEF2(GetPlotMousePos, (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO));
|
||||
DEF2(GetPlotLimits, (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO));
|
||||
DEF0(IsPlotHovered);
|
||||
DEF1(IsAxisHovered, (ImAxis, axis, ));
|
||||
DEF0(IsSubplotsHovered);
|
||||
DEF0(IsPlotSelected);
|
||||
DEF2(GetPlotSelection, (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO));
|
||||
DEF0(CancelPlotSelection);
|
||||
DEF2(HideNextItem, (bool, hidden, = true), (ImPlotCond, cond, = ImPlotCond_Once));
|
||||
DEF2(BeginAlignedPlots, (ImString, group_id, ), (bool, vertical, = true));
|
||||
DEF0(EndAlignedPlots);
|
||||
DEF2(BeginLegendPopup, (ImString, label_id, ), (ImGuiMouseButton, mouse_button, = 1));
|
||||
DEF0(EndLegendPopup);
|
||||
DEF1(IsLegendEntryHovered, (ImString, label_id, ));
|
||||
DEF0(BeginDragDropTargetPlot);
|
||||
DEF1(BeginDragDropTargetAxis, (ImAxis, axis, ));
|
||||
DEF0(BeginDragDropTargetLegend);
|
||||
DEF0(EndDragDropTarget);
|
||||
DEF1(BeginDragDropSourcePlot, (ImGuiDragDropFlags, flags, = 0));
|
||||
DEF2(BeginDragDropSourceAxis, (ImAxis, axis, ), (ImGuiDragDropFlags, flags, = 0));
|
||||
DEF2(BeginDragDropSourceItem, (ImString, label_id, ), (ImGuiDragDropFlags, flags, = 0));
|
||||
DEF0(EndDragDropSource);
|
||||
DEF2(PushStyleColor, (ImPlotCol, idx, ), (ImU32, col, ));
|
||||
DEF2(PushStyleColor, (ImPlotCol, idx, ), (const ImVec4&, col, ));
|
||||
DEF1(PopStyleColor, (int, count, = 1));
|
||||
DEF2(PushStyleVar, (ImPlotStyleVar, idx, ), (float, val, ));
|
||||
DEF2(PushStyleVar, (ImPlotStyleVar, idx, ), (int, val, ));
|
||||
DEF2(PushStyleVar, (ImPlotStyleVar, idx, ), (const ImVec2&, val, ));
|
||||
DEF1(PopStyleVar, (int, count, = 1));
|
||||
DEF2(SetNextLineStyle, (const ImVec4&, col, = IMPLOT_AUTO_COL), (float, weight, = IMPLOT_AUTO));
|
||||
DEF2(SetNextFillStyle, (const ImVec4&, col, = IMPLOT_AUTO_COL), (float, alpha_mod, = IMPLOT_AUTO));
|
||||
DEF5(SetNextMarkerStyle, (ImPlotMarker, marker, = IMPLOT_AUTO), (float, size, = IMPLOT_AUTO), (const ImVec4&, fill, = IMPLOT_AUTO_COL), (float, weight, = IMPLOT_AUTO), (const ImVec4&, outline, = IMPLOT_AUTO_COL));
|
||||
DEF3(SetNextErrorBarStyle, (const ImVec4&, col, = IMPLOT_AUTO_COL), (float, size, = IMPLOT_AUTO), (float, weight, = IMPLOT_AUTO));
|
||||
DEF1(PushPlotClipRect, (float, expand, = 0));
|
||||
DEF0(PopPlotClipRect);
|
||||
}
|
||||
|
||||
// NOLINTEND(whitespace/line_length)
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright 2026 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <fstream>
|
||||
#include <implot.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "third_party/mujoco/src/experimental/platform/hal/graphics_mode.h"
|
||||
#include "third_party/mujoco/src/experimental/platform/hal/renderer.h"
|
||||
#include "third_party/mujoco/src/experimental/platform/hal/window.h"
|
||||
#include "structs.h"
|
||||
#include <pybind11/eval.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
static bool IsCuda() {
|
||||
#ifdef CUDA
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool IsCrd() {
|
||||
const char* display = getenv("DISPLAY");
|
||||
return display ? strcmp(display, ":20") == 0 : false;
|
||||
}
|
||||
|
||||
static std::vector<std::byte> LoadAsset(std::string_view path) {
|
||||
std::string file_path = "assets/" +
|
||||
std::string(path.substr(path.find(':') + 1));
|
||||
|
||||
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open()) {
|
||||
return {};
|
||||
}
|
||||
auto file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<std::byte> buffer(file_size);
|
||||
if (!file.read(reinterpret_cast<char*>(buffer.data()), file_size)) {
|
||||
return {};
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Holds loaded resource data for the MuJoCo resource provider.
|
||||
struct ResourceData {
|
||||
std::vector<std::byte> bytes;
|
||||
};
|
||||
|
||||
class Viewer {
|
||||
public:
|
||||
Viewer(const std::string& title, int width, int height,
|
||||
std::string graphics_mode_str) {
|
||||
// Register resource providers for font and filament assets.
|
||||
mjpResourceProvider resource_provider;
|
||||
mjp_defaultResourceProvider(&resource_provider);
|
||||
|
||||
resource_provider.open = [](mjResource* resource) {
|
||||
auto* data = new ResourceData();
|
||||
data->bytes = LoadAsset(resource->name);
|
||||
resource->data = data;
|
||||
return static_cast<int>(data->bytes.size());
|
||||
};
|
||||
resource_provider.read = [](mjResource* resource, const void** buffer) {
|
||||
auto* data = static_cast<ResourceData*>(resource->data);
|
||||
*buffer = data->bytes.data();
|
||||
return static_cast<int>(data->bytes.size());
|
||||
};
|
||||
resource_provider.close = [](mjResource* resource) {
|
||||
delete static_cast<ResourceData*>(resource->data);
|
||||
resource->data = nullptr;
|
||||
};
|
||||
resource_provider.prefix = "font";
|
||||
mjp_registerResourceProvider(&resource_provider);
|
||||
resource_provider.prefix = "filament";
|
||||
mjp_registerResourceProvider(&resource_provider);
|
||||
|
||||
mujoco::platform::Window::Config config;
|
||||
using GraphicsMode = mujoco::platform::GraphicsMode;
|
||||
config.gfx_mode = mujoco::platform::GraphicsModeFromString(
|
||||
graphics_mode_str, GraphicsMode::FilamentOpenGl);
|
||||
window_ = std::make_unique<mujoco::platform::Window>("PyStudio " + title,
|
||||
width, height, config);
|
||||
ImPlot::CreateContext();
|
||||
|
||||
renderer_ = std::make_unique<mujoco::platform::Renderer>(
|
||||
window_->GetNativeWindowHandle(), config.gfx_mode);
|
||||
}
|
||||
|
||||
void InitRenderer(const mujoco::python::MjModelWrapper& model) {
|
||||
renderer_->Init(model.get());
|
||||
}
|
||||
|
||||
bool NewFrame() {
|
||||
const mujoco::platform::Window::Status status = window_->NewFrame();
|
||||
return status == mujoco::platform::Window::Status::kRunning;
|
||||
}
|
||||
|
||||
intptr_t UploadImage(intptr_t tex_id, const std::string img, int width,
|
||||
int height, int bpp) {
|
||||
return renderer_->UploadImage(tex_id, (const std::byte*)img.data(), width,
|
||||
height, bpp);
|
||||
}
|
||||
|
||||
int RenderToTexture(const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data,
|
||||
mujoco::python::MjvCameraWrapper& cam, int width,
|
||||
int height, int tex_id) {
|
||||
const int bytes_per_pixel = 3;
|
||||
std::vector<std::byte> bytes(width * height * bytes_per_pixel);
|
||||
renderer_->RenderToTexture(model.get(), data.get(), cam.get(), width,
|
||||
height, bytes.data());
|
||||
return renderer_->UploadImage(tex_id, bytes.data(), width, height,
|
||||
bytes_per_pixel);
|
||||
}
|
||||
|
||||
std::string GetDropFile() {
|
||||
return window_->GetDropFile();
|
||||
}
|
||||
|
||||
void Present(const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data,
|
||||
mujoco::python::MjvPerturbWrapper& perturb,
|
||||
mujoco::python::MjvCameraWrapper& camera,
|
||||
mujoco::python::MjvOptionWrapper& vis_options,
|
||||
const std::vector<uint8_t>& render_flags) {
|
||||
const float width = window_->GetWidth();
|
||||
const float height = window_->GetHeight();
|
||||
const float scale = window_->GetScale();
|
||||
|
||||
if (mujoco::platform::IsHeadless(window_->GetGraphicsMode())) {
|
||||
pixels_.resize(width * height * 3);
|
||||
} else {
|
||||
pixels_.clear();
|
||||
}
|
||||
|
||||
// Update render flags before rendering.
|
||||
mjtByte* flags = renderer_->GetRenderFlags();
|
||||
for (size_t i = 0; i < mjNRNDFLAG && i < render_flags.size(); ++i) {
|
||||
flags[i] = render_flags[i];
|
||||
}
|
||||
|
||||
renderer_->Render(model.get(), data.get(), perturb.get(), camera.get(),
|
||||
vis_options.get(), width * scale, height * scale,
|
||||
pixels_);
|
||||
|
||||
window_->EndFrame();
|
||||
window_->Present(pixels_);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<mujoco::platform::Window> window_;
|
||||
std::unique_ptr<mujoco::platform::Renderer> renderer_;
|
||||
std::vector<std::byte> pixels_;
|
||||
};
|
||||
|
||||
PYBIND11_MODULE(native_viewer_cc, m) {
|
||||
pybind11::class_<Viewer>(m, "Viewer")
|
||||
.def(pybind11::init<const std::string&, int, int, const std::string&>())
|
||||
.def("InitRenderer", &Viewer::InitRenderer)
|
||||
.def("NewFrame", &Viewer::NewFrame)
|
||||
.def("Present", &Viewer::Present)
|
||||
.def("UploadImage", &Viewer::UploadImage)
|
||||
.def("RenderToTexture", &Viewer::RenderToTexture)
|
||||
.def("GetDropFile", &Viewer::GetDropFile);
|
||||
m.def("IsCrd", &IsCrd);
|
||||
m.def("IsCuda", &IsCuda);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""Simulation-agnostic native viewer for MuJoCo models.
|
||||
|
||||
This class is simulation-agnostic and as such it does not own the model or data.
|
||||
|
||||
See the documentation for studio_app.py for more details on the architecture
|
||||
separating the viewer and simulation. See the sample/ folder for examples of how
|
||||
to use these classes.
|
||||
"""
|
||||
|
||||
import mujoco
|
||||
|
||||
from mujoco.experimental.studio import native_viewer_cc as _viewer
|
||||
from mujoco.experimental.studio import ux
|
||||
|
||||
|
||||
class NativeViewer:
|
||||
"""Simulation-agnostic native viewer for MuJoCo models."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: mujoco.MjModel,
|
||||
camera: mujoco.MjvCamera | None = None,
|
||||
vis_options: mujoco.MjvOption | None = None,
|
||||
perturb: mujoco.MjvPerturb | None = None,
|
||||
render_flags: ux.RenderFlags | None = None,
|
||||
title: str = '',
|
||||
width: int = 1200,
|
||||
height: int = 800,
|
||||
gfx: str = '',
|
||||
) -> None:
|
||||
"""Initializes the NativeViewer.
|
||||
|
||||
The viewer creates and modifies its own camera, perturbation, and
|
||||
visualization option objects unless they are provided.
|
||||
|
||||
Args:
|
||||
model: The MuJoCo model, used to initialize the renderer.
|
||||
camera: Camera parameters. Internal object is created if None.
|
||||
vis_options: Visualization options. Internal object is created if None.
|
||||
perturb: Perturbation parameters. Internal object is created if None.
|
||||
render_flags: Render flags. Internal object is created if None.
|
||||
title: Title of the viewer window.
|
||||
width: Initial width of the viewer window.
|
||||
height: Initial height of the viewer window.
|
||||
gfx: Graphics mode.
|
||||
"""
|
||||
self.camera = camera or mujoco.MjvCamera()
|
||||
self.perturb = perturb or mujoco.MjvPerturb()
|
||||
self.vis_options = vis_options or mujoco.MjvOption()
|
||||
self._viewer = _viewer.Viewer(title, width, height, gfx)
|
||||
self._viewer.InitRenderer(model)
|
||||
# This class does not own the model but we need to know if the model being
|
||||
# rendered has changed, so we store the unique python object id here so we
|
||||
# can use it to detect model changes.
|
||||
self._renderer_model_id = id(model)
|
||||
self._is_running = True
|
||||
if render_flags is not None:
|
||||
self.render_flags = render_flags
|
||||
else:
|
||||
self.render_flags = ux.RenderFlags()
|
||||
# Initted to match mujoco/src/engine/engine_vis_init.c
|
||||
self.render_flags.flags = [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1]
|
||||
|
||||
def _sync_renderer(self, model: mujoco.MjModel) -> None:
|
||||
"""Re-initializes the renderer if the model object has changed."""
|
||||
if id(model) != self._renderer_model_id:
|
||||
self._viewer.InitRenderer(model)
|
||||
self._renderer_model_id = id(model)
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Poll for a new frame; returns ``False`` when the window is closed."""
|
||||
if not self._is_running:
|
||||
return False
|
||||
self._is_running = self._viewer.NewFrame()
|
||||
return self._is_running
|
||||
|
||||
def sync(
|
||||
self,
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
) -> None:
|
||||
"""Render the scene and present it to the window.
|
||||
|
||||
Args:
|
||||
model: The MuJoCo model provided by the simulation.
|
||||
data: The MuJoCo data provided by the simulation.
|
||||
"""
|
||||
self._sync_renderer(model)
|
||||
self._viewer.Present(
|
||||
model,
|
||||
data,
|
||||
self.perturb,
|
||||
self.camera,
|
||||
self.vis_options,
|
||||
self.render_flags.flags,
|
||||
)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the viewer."""
|
||||
self._is_running = False
|
||||
|
||||
def get_drop_file(self) -> str:
|
||||
"""Returns the path of the file dropped into the window, or empty string."""
|
||||
return self._viewer.GetDropFile()
|
||||
|
||||
def upload_image(
|
||||
self, tex_id: int, img: str | bytes, width: int, height: int, bpp: int
|
||||
) -> int:
|
||||
"""Uploads an image to the backend for GUI rendering.
|
||||
|
||||
The ID can be used in subsequent calls to update the texture data. An empty
|
||||
`img` argument will free the texture if it exists. A `tex_id` of 0 will
|
||||
create a new texture.
|
||||
|
||||
Args:
|
||||
tex_id: The texture ID.
|
||||
img: The image data as string or bytes.
|
||||
width: Width of the image.
|
||||
height: Height of the image.
|
||||
bpp: Bytes per pixel.
|
||||
|
||||
Returns:
|
||||
The texture ID.
|
||||
"""
|
||||
return self._viewer.UploadImage(tex_id, img, width, height, bpp)
|
||||
|
||||
def render_to_texture(
|
||||
self,
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
tex_id: int,
|
||||
width: int,
|
||||
height: int,
|
||||
) -> int:
|
||||
"""Renders the scene to a texture.
|
||||
|
||||
This function renders the scene from the current camera view into a texture.
|
||||
It handles buffer allocation internally.
|
||||
|
||||
Args:
|
||||
model: The MuJoCo model provided by the simulation.
|
||||
data: The MuJoCo data provided by the simulation.
|
||||
tex_id: The texture ID to render into (0 to create a new one).
|
||||
width: Width of the texture.
|
||||
height: Height of the texture.
|
||||
|
||||
Returns:
|
||||
The texture ID.
|
||||
"""
|
||||
self._sync_renderer(model)
|
||||
return self._viewer.RenderToTexture(
|
||||
model,
|
||||
data,
|
||||
self.camera,
|
||||
width,
|
||||
height,
|
||||
tex_id,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "third_party/mujoco/src/experimental/platform/sim/model_holder.h"
|
||||
#include "structs.h"
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace mujoco::python {
|
||||
|
||||
// Loads, parses, and compiles a MuJoCo model from the given file. Returns the
|
||||
// python mjData object (which also contains the compiled mjModel).
|
||||
py::object Parse(std::string_view filepath) {
|
||||
auto holder = platform::ModelHolder::FromFile(filepath);
|
||||
if (!holder->ok()) {
|
||||
throw py::value_error(
|
||||
std::string("Failed to load model from '") +
|
||||
std::string(filepath) + "': " + std::string(holder->error()));
|
||||
}
|
||||
mjModel* model = holder->ReleaseModel();
|
||||
mjData* data = holder->ReleaseData();
|
||||
py::object py_model = py::cast(MjModelWrapper(model));
|
||||
py::object py_data =
|
||||
py::cast(MjDataWrapper(py::cast<MjModelWrapper*>(py_model), data));
|
||||
return py_data;
|
||||
}
|
||||
|
||||
} // namespace mujoco::python
|
||||
|
||||
PYBIND11_MODULE(parser, m) {
|
||||
m.def("parse", &mujoco::python::Parse,
|
||||
pybind11::return_value_policy::take_ownership);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
#include "third_party/mujoco/src/experimental/platform/hal/renderer.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "third_party/mujoco/src/experimental/platform/hal/graphics_mode.h"
|
||||
#include "structs.h"
|
||||
#include <pybind11/eval.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace mujoco::python {
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
using RendererImpl = mujoco::platform::Renderer;
|
||||
using GraphicsMode = mujoco::platform::GraphicsMode;
|
||||
|
||||
Renderer(const std::string& graphics_mode_str) {
|
||||
const GraphicsMode mode = mujoco::platform::GraphicsModeFromString(
|
||||
graphics_mode_str, GraphicsMode::FilamentOpenGl);
|
||||
impl_ = std::make_unique<RendererImpl>(nullptr, mode);
|
||||
}
|
||||
|
||||
void Init(const MjModelWrapper& model) { impl_->Init(model.get()); }
|
||||
|
||||
pybind11::bytes Render(const MjModelWrapper& model, MjDataWrapper& data,
|
||||
std::optional<MjvPerturbWrapper>& perturb,
|
||||
std::optional<MjvCameraWrapper>& camera,
|
||||
std::optional<MjvOptionWrapper>& vis_option, int width,
|
||||
int height) {
|
||||
std::vector<std::byte> pixels(width * height * 3);
|
||||
impl_->Render(
|
||||
model.get(), data.get(), perturb ? perturb.value().get() : nullptr,
|
||||
camera ? camera.value().get() : nullptr,
|
||||
vis_option ? vis_option.value().get() : nullptr, width, height, pixels);
|
||||
return pybind11::bytes((const char*)pixels.data(), pixels.size());
|
||||
}
|
||||
|
||||
pybind11::memoryview GetRenderFlags() {
|
||||
return pybind11::memoryview::from_buffer(
|
||||
impl_->GetRenderFlags(), {static_cast<pybind11::ssize_t>(mjNRNDFLAG)},
|
||||
{sizeof(mjtByte)});
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<RendererImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace mujoco::python
|
||||
|
||||
PYBIND11_MODULE(renderer, m) {
|
||||
pybind11::class_<mujoco::python::Renderer>(m, "Renderer")
|
||||
.def(pybind11::init<const std::string&>())
|
||||
.def("Init", &mujoco::python::Renderer::Init)
|
||||
.def("Render", &mujoco::python::Renderer::Render)
|
||||
.def("get_render_flags", &mujoco::python::Renderer::GetRenderFlags,
|
||||
pybind11::keep_alive<0, 1>());
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""This script runs a simulation and viewer in separate processes communicating asynchronously.
|
||||
|
||||
In this example, we will run the viewer in an independent process communicating
|
||||
via multiprocessing queues. Controls are provided to simulate network transit
|
||||
latency and adjust the communication rates.
|
||||
|
||||
You must provide a mjcf model file via the first command-line argument.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from absl import app as absl_app
|
||||
from absl import flags as absl_flags
|
||||
import mujoco
|
||||
from mujoco.experimental.studio import native_viewer as _viewer
|
||||
from mujoco.experimental.studio import sim as _sim
|
||||
from mujoco.experimental.studio import studio_app
|
||||
from mujoco.experimental.studio import ux
|
||||
import numpy as np
|
||||
|
||||
from mujoco.experimental.dear_imgui import dear_imgui as imgui
|
||||
|
||||
_GFX = absl_flags.DEFINE_string('gfx', '', 'Rendering graphics mode.')
|
||||
_WIDTH = absl_flags.DEFINE_integer('width', 1200, 'Width of the output image.')
|
||||
_HEIGHT = absl_flags.DEFINE_integer('height', 800, 'Height of the output image')
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SimToView:
|
||||
"""A message sent from the simulation process to the viewer process."""
|
||||
|
||||
model: mujoco.MjModel | None = None
|
||||
data: mujoco.MjData | None = None
|
||||
state: np.ndarray | None = None
|
||||
state_sig: int = 0
|
||||
send_time: float = 0.0
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ViewToSim:
|
||||
"""A message sent from the viewer process to the simulation process."""
|
||||
|
||||
state: np.ndarray | None = None
|
||||
state_sig: int = 0
|
||||
reset: bool = False
|
||||
send_rate: float = 60.0
|
||||
|
||||
|
||||
class Network:
|
||||
"""Simulated networking parameters."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.transit_buffer = []
|
||||
self.send_rate = 60.0
|
||||
self.network_delay = 0.2
|
||||
|
||||
def get_arrived(self, q: multiprocessing.Queue) -> SimToView | None:
|
||||
now = time.time()
|
||||
while not q.empty():
|
||||
self.transit_buffer.append(q.get())
|
||||
arrived = None
|
||||
while (
|
||||
self.transit_buffer
|
||||
and now >= self.transit_buffer[0].send_time + self.network_delay
|
||||
):
|
||||
arrived = self.transit_buffer.pop(0)
|
||||
return arrived
|
||||
|
||||
|
||||
def view(
|
||||
sim_to_view: multiprocessing.Queue,
|
||||
view_to_sim: multiprocessing.Queue,
|
||||
) -> None:
|
||||
"""Entry-point for process that renders the simulation."""
|
||||
# Block until the first message (containing the model) arrives.
|
||||
msg = sim_to_view.get()
|
||||
assert msg.model is not None, 'First message must contain the MuJoCo model.'
|
||||
|
||||
title = os.path.basename(sys.argv[0])
|
||||
xfrc_sig = int(mujoco.mjtState.mjSTATE_XFRC_APPLIED)
|
||||
xfrc_size = mujoco.mj_stateSize(msg.model, xfrc_sig)
|
||||
xfrc_state = np.zeros(xfrc_size, np.float64)
|
||||
|
||||
app = studio_app.StudioApp(msg.model, msg.data)
|
||||
network = Network()
|
||||
viewer = _viewer.NativeViewer(
|
||||
app.model,
|
||||
title=title,
|
||||
width=_WIDTH.value,
|
||||
height=_HEIGHT.value,
|
||||
gfx=_GFX.value,
|
||||
)
|
||||
|
||||
while viewer.is_running() and app.is_running():
|
||||
|
||||
# Determine which messages have arrived through the simulated network.
|
||||
arrived = network.get_arrived(sim_to_view)
|
||||
|
||||
# Update the camera and compute the perturbation.
|
||||
app.handle_mouse_events(viewer.camera, viewer.vis_options, viewer.perturb)
|
||||
|
||||
# Sync state from the backend if a new payload actually arrived.
|
||||
if arrived is not None and arrived.state is not None:
|
||||
mujoco.mj_setState(app.model, app.data, arrived.state, arrived.state_sig)
|
||||
mujoco.mj_forward(app.model, app.data)
|
||||
|
||||
# Always apply the perturbation forces from the viewer.
|
||||
app.apply_perturb(viewer.perturb)
|
||||
|
||||
# Transmit user interaction when we get a new state
|
||||
if arrived is not None:
|
||||
mujoco.mj_getState(app.model, app.data, xfrc_state, xfrc_sig)
|
||||
view_to_sim.put(
|
||||
ViewToSim(
|
||||
send_rate=network.send_rate, state=xfrc_state, state_sig=xfrc_sig
|
||||
)
|
||||
)
|
||||
|
||||
# Build the UI.
|
||||
ux.setup_theme(app.theme)
|
||||
if imgui.Begin(
|
||||
'Settings',
|
||||
flags=int(imgui.WindowFlags.AlwaysAutoResize)
|
||||
| int(imgui.WindowFlags.NoTitleBar)
|
||||
| int(imgui.WindowFlags.NoCollapse),
|
||||
):
|
||||
imgui.PushItemWidth(200.0)
|
||||
_, network.network_delay = imgui.SliderFloat(
|
||||
'Network Latency (s)', network.network_delay, 0.0, 2.0
|
||||
)
|
||||
updated, network.send_rate = imgui.SliderFloat(
|
||||
'Send Rate (Hz)', network.send_rate, 1.0, 120.0
|
||||
)
|
||||
if updated:
|
||||
view_to_sim.put(ViewToSim(send_rate=network.send_rate))
|
||||
|
||||
imgui.SetNextItemWidth(-1)
|
||||
if imgui.Button('Reset Simulation'):
|
||||
view_to_sim.put(ViewToSim(reset=True, send_rate=network.send_rate))
|
||||
|
||||
imgui.PopItemWidth()
|
||||
imgui.End()
|
||||
|
||||
viewer.sync(app.model, app.data)
|
||||
|
||||
|
||||
def sim(
|
||||
data: mujoco.MjData,
|
||||
model: mujoco.MjModel,
|
||||
sim_to_view: multiprocessing.Queue,
|
||||
view_to_sim: multiprocessing.Queue,
|
||||
view_process: multiprocessing.Process,
|
||||
) -> None:
|
||||
"""Entry-point for process that runs the simulation."""
|
||||
|
||||
sim_to_view.put(SimToView(model=model, data=data))
|
||||
|
||||
step_control = _sim.StepControl()
|
||||
integration_sig = int(mujoco.mjtState.mjSTATE_INTEGRATION)
|
||||
integration_size = mujoco.mj_stateSize(model, integration_sig)
|
||||
integration_state = np.empty(integration_size, np.float64)
|
||||
|
||||
msg = ViewToSim()
|
||||
last_send_time = time.time()
|
||||
|
||||
while view_process.is_alive():
|
||||
while not view_to_sim.empty():
|
||||
msg = view_to_sim.get()
|
||||
|
||||
if msg.reset:
|
||||
mujoco.mj_resetData(model, data)
|
||||
mujoco.mj_forward(model, data)
|
||||
msg.reset = False
|
||||
|
||||
# Apply perturbation forces received from the viewer process.
|
||||
if msg.state is not None:
|
||||
mujoco.mj_setState(model, data, msg.state, msg.state_sig)
|
||||
|
||||
# Advance the simulation keeping up with real-time.
|
||||
step_control.advance(model, data)
|
||||
|
||||
# Send the simulation state paced by the requested send_rate.
|
||||
now = time.time()
|
||||
if now - last_send_time >= 1.0 / max(1.0, msg.send_rate):
|
||||
mujoco.mj_getState(model, data, integration_state, integration_sig)
|
||||
sim_to_view.put(
|
||||
SimToView(
|
||||
state=integration_state,
|
||||
state_sig=integration_sig,
|
||||
send_time=now,
|
||||
)
|
||||
)
|
||||
last_send_time = now
|
||||
|
||||
|
||||
def main(argv: list[str]) -> None:
|
||||
app = studio_app.StudioApp.from_argv(argv)
|
||||
|
||||
# Queues for communication between the simulation and viewer processes.
|
||||
sim_to_view = multiprocessing.Queue()
|
||||
view_to_sim = multiprocessing.Queue()
|
||||
|
||||
# Start the viewer process.
|
||||
view_process = multiprocessing.Process(
|
||||
target=view, args=(sim_to_view, view_to_sim)
|
||||
)
|
||||
view_process.start()
|
||||
|
||||
# Start the simulation in the main process.
|
||||
sim(app.data, app.model, sim_to_view, view_to_sim, view_process)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absl_app.run(main)
|
||||
@@ -0,0 +1,199 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""Example to run studio in the native viewer with responsive ImPlot UI.
|
||||
|
||||
This script runs a Studio viewer in-process and adds an 'Inspect Body' window
|
||||
using ImGui and ImPlot bindings to visualize selected body data. The example
|
||||
demonstrates how responsive UI layout rules are easily implemented.
|
||||
|
||||
Provide an MJCF model file via the first command-line argument to launch.
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
from absl import app as absl_app
|
||||
from absl import flags as absl_flags
|
||||
import mujoco
|
||||
from mujoco.experimental.studio import native_viewer as _viewer
|
||||
from mujoco.experimental.studio import studio_app
|
||||
import numpy as np
|
||||
|
||||
from mujoco.experimental.dear_imgui import dear_imgui as imgui
|
||||
from mujoco.experimental.implot import implot
|
||||
|
||||
_GFX = absl_flags.DEFINE_string('gfx', '', 'Rendering graphics mode.')
|
||||
_WIDTH = absl_flags.DEFINE_integer('width', 1200, 'Width of the output image.')
|
||||
_HEIGHT = absl_flags.DEFINE_integer('height', 800, 'Height of the output image')
|
||||
|
||||
|
||||
_N_HISTORY = 100
|
||||
|
||||
_PLOT_FLAGS = (
|
||||
implot.Flags.NoInputs.value # Disable pan/zoom mouse interaction.
|
||||
| implot.Flags.NoMenus.value # Disable right-click context menu.
|
||||
| implot.Flags.NoBoxSelect.value # Disable drag-to-select regions.
|
||||
)
|
||||
|
||||
_AXIS_FLAGS = (
|
||||
implot.AxisFlags.NoGridLines.value # Hide background grid lines.
|
||||
| implot.AxisFlags.NoTickMarks.value # Hide small tick marks on the axis.
|
||||
)
|
||||
|
||||
|
||||
def _setup_plot_flags(plot_size: imgui.Vec2) -> int:
|
||||
flags = _PLOT_FLAGS
|
||||
if min(plot_size.x, plot_size.y) < 300:
|
||||
flags |= implot.Flags.NoTitle.value
|
||||
if min(plot_size.x, plot_size.y) < 200:
|
||||
flags |= implot.Flags.NoLegend.value
|
||||
return flags
|
||||
|
||||
|
||||
def _setup_time_axis(plot_size: imgui.Vec2) -> None:
|
||||
flags = _AXIS_FLAGS
|
||||
if plot_size.x < 300:
|
||||
flags |= implot.AxisFlags.NoTickLabels.value
|
||||
implot.SetupAxis(implot.Axis.X1, '', flags)
|
||||
implot.SetupAxisLimits(implot.Axis.X1, 0, _N_HISTORY)
|
||||
|
||||
|
||||
def _setup_xpos_axis(centroid: list[np.ndarray], plot_size: imgui.Vec2) -> None:
|
||||
flags = _AXIS_FLAGS
|
||||
if plot_size.y < 300:
|
||||
flags |= implot.AxisFlags.NoTickLabels.value
|
||||
implot.SetupAxis(implot.Axis.Y1, '', flags)
|
||||
min_y = min(c[1] for c in centroid)
|
||||
max_y = max(c[1] for c in centroid)
|
||||
margin = max((max_y - min_y) * 0.1, 0.05)
|
||||
implot.SetupAxisLimits(
|
||||
implot.Axis.Y1,
|
||||
min_y - margin,
|
||||
max_y + margin,
|
||||
cond=implot.Cond.Always,
|
||||
)
|
||||
|
||||
|
||||
def _setup_angle_axis(plot_size: imgui.Vec2) -> None:
|
||||
flags = _AXIS_FLAGS
|
||||
if plot_size.y < 300:
|
||||
flags |= implot.AxisFlags.NoTickLabels.value
|
||||
implot.SetupAxis(implot.Axis.Y1, '', flags)
|
||||
implot.SetupAxisLimits(implot.Axis.Y1, -185.0, 185.0)
|
||||
implot.SetupAxisTicks(
|
||||
implot.Axis.Y1,
|
||||
[-180.0, -90.0, 0.0, 90.0, 180.0],
|
||||
['-180', '-90', '0', '90', '180'],
|
||||
)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> None:
|
||||
app = studio_app.StudioApp.from_argv(argv)
|
||||
title = os.path.basename(sys.argv[0])
|
||||
|
||||
# Initialize the viewer.
|
||||
viewer = _viewer.NativeViewer(
|
||||
app.model,
|
||||
title=title,
|
||||
width=_WIDTH.value,
|
||||
height=_HEIGHT.value,
|
||||
gfx=_GFX.value,
|
||||
)
|
||||
|
||||
# Variables for the custom UI.
|
||||
centroid = [np.zeros(3) for _ in range(_N_HISTORY)]
|
||||
euler = [np.zeros(3) for _ in range(_N_HISTORY)]
|
||||
body_id = -1
|
||||
|
||||
# Main viewer loop.
|
||||
while viewer.is_running():
|
||||
if not app.update(viewer.camera, viewer.vis_options, viewer.perturb):
|
||||
break
|
||||
|
||||
# Build standard Studio UI.
|
||||
app.build_gui(viewer.camera, viewer.vis_options, viewer.render_flags)
|
||||
|
||||
# Inspect the perturb.select body
|
||||
if viewer.perturb.select > 0:
|
||||
body_id = viewer.perturb.select
|
||||
|
||||
# Display selected body information.
|
||||
if body_id > 0:
|
||||
body_name = mujoco.mj_id2name(
|
||||
app.model, int(mujoco.mjtObj.mjOBJ_BODY), body_id
|
||||
)
|
||||
|
||||
imgui.SetNextWindowSize(imgui.Vec2(1200, 600), imgui.Cond.FirstUseEver)
|
||||
|
||||
# Note: The window title uses the special "###" markup to ensure the imgui
|
||||
# ID for the window is constant for all body names. This is needed for
|
||||
# the window to retain its state for all bodies.
|
||||
window_title = f'Inspect Body {body_name or "(???)"!r} ({body_id})###Plot'
|
||||
if imgui.Begin(window_title):
|
||||
avail = imgui.GetContentRegionAvail()
|
||||
wide = avail.x > avail.y
|
||||
|
||||
# Add a small padding factor to prevent scrollbars.
|
||||
plot_size = imgui.Vec2(
|
||||
avail.x * 0.5 - 4 if wide else avail.x,
|
||||
avail.y if wide else avail.y * 0.5 - 4,
|
||||
)
|
||||
|
||||
plot_flags = _setup_plot_flags(plot_size)
|
||||
if implot.BeginPlot('Centroid vs Time', plot_size, flags=plot_flags):
|
||||
_setup_time_axis(plot_size)
|
||||
_setup_xpos_axis(centroid, plot_size)
|
||||
implot.PlotLine('x', range(_N_HISTORY), [c[0] for c in centroid])
|
||||
implot.PlotLine('y', range(_N_HISTORY), [c[1] for c in centroid])
|
||||
implot.PlotLine('z', range(_N_HISTORY), [c[2] for c in centroid])
|
||||
implot.EndPlot()
|
||||
|
||||
if wide:
|
||||
imgui.SameLine()
|
||||
|
||||
if implot.BeginPlot('Euler Angle vs Time', plot_size, flags=plot_flags):
|
||||
_setup_time_axis(plot_size)
|
||||
_setup_angle_axis(plot_size)
|
||||
implot.PlotLine('roll', range(_N_HISTORY), [e[0] for e in euler])
|
||||
implot.PlotLine('pitch', range(_N_HISTORY), [e[1] for e in euler])
|
||||
implot.PlotLine('yaw', range(_N_HISTORY), [e[2] for e in euler])
|
||||
implot.EndPlot()
|
||||
imgui.End()
|
||||
|
||||
# Update plot data
|
||||
centroid.pop(0)
|
||||
euler.pop(0)
|
||||
if body_id > 0:
|
||||
centroid.append(app.data.xpos[body_id].copy())
|
||||
# Convert quaternion to Euler angles via rotation matrix.
|
||||
quat = app.data.xquat[body_id]
|
||||
mat = np.zeros(9)
|
||||
mujoco.mju_quat2Mat(mat, quat)
|
||||
# mat is row-major 3x3: R[i,j] = mat[3*i + j].
|
||||
roll = math.atan2(mat[7], mat[8])
|
||||
pitch = math.atan2(-mat[6], math.sqrt(mat[7] ** 2 + mat[8] ** 2))
|
||||
yaw = math.atan2(mat[3], mat[0])
|
||||
euler.append(np.degrees(np.array([roll, pitch, yaw])))
|
||||
else:
|
||||
centroid.append(np.zeros(3))
|
||||
euler.append(np.zeros(3))
|
||||
|
||||
viewer.sync(app.model, app.data)
|
||||
|
||||
viewer.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absl_app.run(main)
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""Render a MuJoCo model to an image."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from absl import app
|
||||
from absl import flags
|
||||
import mujoco
|
||||
from mujoco.experimental.studio import parser
|
||||
from mujoco.experimental.studio import renderer
|
||||
from PIL import Image
|
||||
|
||||
_MODEL = flags.DEFINE_string('model', '', 'Model file to load.')
|
||||
_OUTPUT = flags.DEFINE_string('output', '', 'Output file to save.')
|
||||
_GFX = flags.DEFINE_string('gfx', '', 'Renderer to use.')
|
||||
_WIDTH = flags.DEFINE_integer('width', 320, 'Width of the output image.')
|
||||
_HEIGHT = flags.DEFINE_integer('height', 240, 'Height of the output image.')
|
||||
_STEPS = flags.DEFINE_integer('steps', 1, 'Number of steps before render.')
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) > 1:
|
||||
raise app.UsageError('Too many command-line arguments.')
|
||||
if not _MODEL.value:
|
||||
raise ValueError('`model` flag is required.')
|
||||
if not _OUTPUT.value:
|
||||
raise ValueError('`output flag is required.')
|
||||
|
||||
try:
|
||||
data = parser.parse(_MODEL.value)
|
||||
model = data.model
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
print(f'Error loading model from `{_MODEL.value}`: {ex}')
|
||||
sys.exit(-1)
|
||||
|
||||
for _ in range(_STEPS.value):
|
||||
mujoco.mj_step(model, data)
|
||||
|
||||
try:
|
||||
r = renderer.Renderer(_GFX.value)
|
||||
r.Init(model)
|
||||
pixels = r.Render(
|
||||
model, data, None, None, None, _WIDTH.value, _HEIGHT.value
|
||||
)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
print(f'Error rendering model: {ex}')
|
||||
sys.exit(-2)
|
||||
|
||||
try:
|
||||
img = Image.frombytes('RGB', (_WIDTH.value, _HEIGHT.value), pixels)
|
||||
img.save(_OUTPUT.value, format=os.path.splitext(_OUTPUT.value)[1][1:])
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
print(f'Error saving image to `{_OUTPUT.value}`: {ex}')
|
||||
sys.exit(-3)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(main)
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2026 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
// Python bindings for MuJoCo platform simulation components.
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "third_party/mujoco/src/experimental/platform/sim/step_control.h"
|
||||
#include "structs.h"
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
using StepControl = mujoco::platform::StepControl;
|
||||
|
||||
PYBIND11_MODULE(sim, m) {
|
||||
m.doc() = "MuJoCo platform simulation bindings for Link.";
|
||||
|
||||
py::enum_<StepControl::Status>(m, "StepStatus")
|
||||
.value("OK", StepControl::Status::kOk)
|
||||
.value("PAUSED", StepControl::Status::kPaused)
|
||||
.value("VISCOUS_PAUSED", StepControl::Status::kViscousPaused)
|
||||
.value("AUTO_RESET", StepControl::Status::kAutoReset)
|
||||
.value("DIVERGED", StepControl::Status::kDiverged);
|
||||
|
||||
py::enum_<StepControl::PauseState>(m, "PauseState")
|
||||
.value("UNPAUSED", StepControl::PauseState::kUnpaused)
|
||||
.value("NORMAL_PAUSED", StepControl::PauseState::kNormalPaused)
|
||||
.value("VISCOUS_PAUSED", StepControl::PauseState::kViscousPaused);
|
||||
|
||||
py::class_<StepControl>(m, "StepControl")
|
||||
.def(py::init<>())
|
||||
.def(
|
||||
"advance",
|
||||
[](StepControl& self, py::object model_obj, py::object data_obj,
|
||||
py::object step_fn) {
|
||||
auto& model = py::cast<mujoco::python::MjModelWrapper&>(model_obj);
|
||||
auto& data = py::cast<mujoco::python::MjDataWrapper&>(data_obj);
|
||||
if (step_fn.is_none()) {
|
||||
return self.Advance(model.get(), data.get());
|
||||
} else {
|
||||
return self.Advance(
|
||||
model.get(), data.get(),
|
||||
[step_fn, model_obj, data_obj](mjModel*, mjData*) {
|
||||
step_fn(model_obj, data_obj);
|
||||
});
|
||||
}
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("step_fn") = py::none(),
|
||||
"Step physics forward, respecting speed settings and refresh budget.")
|
||||
.def("force_sync", &StepControl::ForceSync,
|
||||
"Ensures the next Advance() will synchronize time and step once.")
|
||||
.def("get_speed", &StepControl::GetSpeed,
|
||||
"Returns the desired simulation speed as a percentage of real time.")
|
||||
.def("get_speed_measured", &StepControl::GetSpeedMeasured,
|
||||
"Returns the measured simulation speed.")
|
||||
.def("set_speed", &StepControl::SetSpeed, py::arg("speed"),
|
||||
"Sets the desired speed (clamped to [0.1%, 100%]).")
|
||||
.def("set_pause_state", &StepControl::SetPauseState, py::arg("state"),
|
||||
"Sets the pause state of the simulation.")
|
||||
.def("get_pause_state", &StepControl::GetPauseState,
|
||||
"Returns the current pause state.")
|
||||
.def("request_single_step", &StepControl::RequestSingleStep,
|
||||
"Request a single step if paused.");
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""This script runs Studio from Python, visualized in a native viewer."""
|
||||
|
||||
from absl import app as absl_app
|
||||
from absl import flags as absl_flags
|
||||
from mujoco.experimental.studio import native_viewer
|
||||
from mujoco.experimental.studio import studio_app
|
||||
|
||||
_GFX = absl_flags.DEFINE_string('gfx', '', 'Rendering graphics mode.')
|
||||
_WIDTH = absl_flags.DEFINE_integer('width', 1200, 'Width of the output image.')
|
||||
_HEIGHT = absl_flags.DEFINE_integer('height', 800, 'Height of the output image')
|
||||
|
||||
|
||||
def main(argv: list[str]) -> None:
|
||||
app = studio_app.StudioApp.from_argv(argv)
|
||||
|
||||
# Initialize the viewer.
|
||||
viewer = native_viewer.NativeViewer(
|
||||
app.model,
|
||||
width=_WIDTH.value,
|
||||
height=_HEIGHT.value,
|
||||
gfx=_GFX.value,
|
||||
)
|
||||
|
||||
# Main viewer loop.
|
||||
while viewer.is_running():
|
||||
if not app.update_from_viewer(viewer):
|
||||
break
|
||||
|
||||
app.build_gui(viewer.camera, viewer.vis_options, viewer.render_flags)
|
||||
|
||||
viewer.sync(app.model, app.data)
|
||||
|
||||
viewer.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absl_app.run(main)
|
||||
@@ -0,0 +1,478 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""Viewer-agnostic Python implementation of Studio.
|
||||
|
||||
Architecture:
|
||||
StudioApp owns the simulation state (model, data) and the UI logic.
|
||||
Viewer classes (e.g., NativeViewer) own the window (if required), renderer,
|
||||
camera, and visualization options. The viewer never stores references to
|
||||
model or data. Instead, the caller passes them each frame via
|
||||
viewer.sync(model, data). This ensures the viewer always renders the current
|
||||
model, even if StudioApp.load_model_from_file() swaps it.
|
||||
|
||||
The class can be used to implement the full Studio application in Python. By
|
||||
using the more granular member functions it can also build simple apps that only
|
||||
use a subset of the Studio UI. This configuration is fully dynamic, there is
|
||||
nothing to configure in advance, you can change your app by changing the
|
||||
functions that get called each frame. This class is also viewer-agnostic and as
|
||||
such does not own camera, vis_options or perturb objects (these are provided by
|
||||
the viewer).
|
||||
|
||||
See the sample/ folder for usage examples.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import typing
|
||||
|
||||
import mujoco
|
||||
from mujoco.experimental.studio import parser
|
||||
from mujoco.experimental.studio import sim
|
||||
from mujoco.experimental.studio import studio_app_events as events
|
||||
from mujoco.experimental.studio import ux
|
||||
from mujoco.experimental.studio import viewer_protocol
|
||||
import numpy as np
|
||||
|
||||
from mujoco.experimental.dear_imgui import dear_imgui as imgui
|
||||
|
||||
# Type alias for a custom physics step function.
|
||||
StepFn = typing.Callable[[mujoco.MjModel, mujoco.MjData], None]
|
||||
|
||||
|
||||
def load_model_from_file(
|
||||
model_path: str,
|
||||
) -> tuple[mujoco.MjModel, mujoco.MjData] | None:
|
||||
"""Loads a model and data from a file path."""
|
||||
try:
|
||||
data = parser.parse(model_path)
|
||||
return data.model, data
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
print(f'Error loading model from {model_path!r}: {ex}')
|
||||
return None
|
||||
|
||||
|
||||
class StudioApp:
|
||||
"""Viewer-agnostic Python implementation of Studio."""
|
||||
|
||||
@classmethod
|
||||
def from_argv(cls, argv: list[str]) -> 'StudioApp':
|
||||
"""Constructs a StudioApp by parsing a model path from command-line args."""
|
||||
if len(argv) < 2:
|
||||
model = mujoco.MjSpec().compile()
|
||||
data = mujoco.MjData(model)
|
||||
app = cls(model, data)
|
||||
app.step_control.set_pause_state(sim.PauseState.NORMAL_PAUSED)
|
||||
return app
|
||||
|
||||
model_path = argv[1]
|
||||
|
||||
res = load_model_from_file(model_path)
|
||||
if res is None:
|
||||
sys.exit(-1)
|
||||
model, data = res
|
||||
|
||||
app = cls(model, data)
|
||||
app.model_path = model_path
|
||||
return app
|
||||
|
||||
def load_model_from_file(
|
||||
self, model_path: str
|
||||
) -> tuple[mujoco.MjModel, mujoco.MjData] | None:
|
||||
"""Loads a new model from a file, replacing the current model and data."""
|
||||
res = load_model_from_file(model_path)
|
||||
if res is None:
|
||||
self.status = f'Error loading model from {model_path!r}'
|
||||
return None
|
||||
|
||||
model, data = res
|
||||
self.model = model
|
||||
self.data = data
|
||||
self.model_path = model_path
|
||||
self.step_control = sim.StepControl()
|
||||
self.ux_state = ux.UxState()
|
||||
self.status = f'Loaded: {os.path.basename(model_path)!r}'
|
||||
|
||||
return model, data
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
):
|
||||
"""Initializes the Studio application."""
|
||||
self.model = model
|
||||
self.data = data
|
||||
self.model_path = ''
|
||||
|
||||
self.step_control = sim.StepControl()
|
||||
self.ux_state = ux.UxState()
|
||||
self.theme = ux.GuiTheme.LIGHT
|
||||
self.show_stats = False
|
||||
self.show_solver = False
|
||||
self.should_quit = False
|
||||
self.status = 'Ready'
|
||||
# TODO(matijak): This should be part of the viewer, also making a struct to
|
||||
# pass it around with the camera would be convenient.
|
||||
self._cam_speed = 0.001
|
||||
|
||||
def handle_vis_options_keyboard_events(
|
||||
self,
|
||||
vis_options: mujoco.MjvOption,
|
||||
is_freecam_wasd: bool,
|
||||
) -> bool:
|
||||
"""Toggles visualization flags based on keyboard shortcuts.
|
||||
|
||||
Args:
|
||||
vis_options: The visualization options to modify.
|
||||
is_freecam_wasd: If True, keys Q/E/A/D are reserved for camera movement
|
||||
and will not toggle visualization flags.
|
||||
|
||||
Returns:
|
||||
True if a key was handled, False otherwise.
|
||||
"""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False
|
||||
|
||||
return events.handle_vis_options_keyboard_events(
|
||||
vis_options, is_freecam_wasd
|
||||
)
|
||||
|
||||
def handle_step_control_keyboard_events(self) -> bool:
|
||||
"""Handles keyboard shortcuts for simulation stepping control.
|
||||
|
||||
Returns:
|
||||
True if a key was handled, False otherwise.
|
||||
"""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False
|
||||
|
||||
return events.handle_step_control_keyboard_events(
|
||||
self.model, self.data, self.step_control, self.ux_state
|
||||
)
|
||||
|
||||
def handle_freecam_wasd_keyboard_events(
|
||||
self,
|
||||
camera: mujoco.MjvCamera,
|
||||
) -> bool:
|
||||
"""Handles keyboard shortcuts for free camera movement."""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False
|
||||
|
||||
handled, self._cam_speed = events.handle_freecam_wasd_keyboard_events(
|
||||
self.model, self.data, camera, self._cam_speed
|
||||
)
|
||||
return handled
|
||||
|
||||
def handle_keyboard_events(
|
||||
self,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
) -> bool:
|
||||
"""Handle keyboard events according to Studio's bindings."""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False
|
||||
|
||||
is_freecam_wasd = self.ux_state.camera_index == ux.FREE_CAMERA_IDX
|
||||
if events.handle_step_control_keyboard_events(
|
||||
self.model, self.data, self.step_control, self.ux_state
|
||||
):
|
||||
return True
|
||||
|
||||
if events.handle_camera_select_keyboard_events(
|
||||
self.model, camera, self.ux_state
|
||||
):
|
||||
return True
|
||||
|
||||
if events.handle_vis_options_keyboard_events(vis_options, is_freecam_wasd):
|
||||
return True
|
||||
|
||||
if is_freecam_wasd:
|
||||
handled, self._cam_speed = events.handle_freecam_wasd_keyboard_events(
|
||||
self.model, self.data, camera, self._cam_speed
|
||||
)
|
||||
if handled:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def handle_camera_tracking_mouse_events(
|
||||
self,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
) -> None:
|
||||
"""Handles mouse events for camera tracking."""
|
||||
if imgui.GetIO().WantCaptureMouse:
|
||||
return
|
||||
|
||||
events.handle_camera_tracking_mouse_events(
|
||||
self.model, self.data, camera, vis_options, self.ux_state
|
||||
)
|
||||
|
||||
def handle_mouse_events(
|
||||
self,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
perturb: mujoco.MjvPerturb,
|
||||
) -> None:
|
||||
"""Handles mouse events."""
|
||||
if imgui.GetIO().WantCaptureMouse:
|
||||
return
|
||||
|
||||
events.handle_mouse_events(
|
||||
self.model, self.data, camera, vis_options, perturb, self.ux_state
|
||||
)
|
||||
|
||||
def reset_physics(self) -> None:
|
||||
"""Reset the physics."""
|
||||
mujoco.mj_resetData(self.model, self.data)
|
||||
mujoco.mj_forward(self.model, self.data)
|
||||
|
||||
def apply_perturb(self, perturb: mujoco.MjvPerturb) -> None:
|
||||
"""Apply perturbation the model."""
|
||||
if self.step_control.get_pause_state() != sim.PauseState.NORMAL_PAUSED:
|
||||
sig = mujoco.mjtState.mjSTATE_XFRC_APPLIED.value
|
||||
size = mujoco.mj_stateSize(self.model, sig)
|
||||
zero_state = np.zeros(size, np.float64)
|
||||
mujoco.mj_setState(self.model, self.data, zero_state, sig)
|
||||
mujoco.mjv_applyPerturbPose(self.model, self.data, perturb, 0)
|
||||
mujoco.mjv_applyPerturbForce(self.model, self.data, perturb)
|
||||
else:
|
||||
mujoco.mjv_applyPerturbPose(self.model, self.data, perturb, 1)
|
||||
|
||||
def update_physics(
|
||||
self,
|
||||
perturb: mujoco.MjvPerturb,
|
||||
*,
|
||||
step_fn: StepFn | None = None,
|
||||
) -> None:
|
||||
"""Applies the perturbations and advances the physics.
|
||||
|
||||
Args:
|
||||
perturb: The MuJoCo perturbation object.
|
||||
step_fn: Optional custom physics step function. When provided, it is
|
||||
called instead of ``step_control.advance``. The function receives
|
||||
``(model, data)`` and should step the simulation in-place.
|
||||
"""
|
||||
self.apply_perturb(perturb)
|
||||
|
||||
if step_fn is not None:
|
||||
step_fn(self.model, self.data)
|
||||
else:
|
||||
advance_status = self.step_control.advance(self.model, self.data)
|
||||
if advance_status == sim.StepStatus.AUTO_RESET:
|
||||
self.reset_physics()
|
||||
|
||||
def reset_physics_gui(self) -> None:
|
||||
"""GUI to Reset the physics i.e., the reset button."""
|
||||
button_size = imgui.GetFrameHeight()
|
||||
square_size = imgui.Vec2(button_size, button_size)
|
||||
icon_reset_model = '\uf0e2' # FontAwesome "undo" icon.
|
||||
if imgui.Button(icon_reset_model, square_size):
|
||||
self.reset_physics()
|
||||
imgui.SetItemTooltip('Reset')
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Returns True if the application should continue running (called by update())."""
|
||||
return not self.should_quit
|
||||
|
||||
def update(
|
||||
self,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
perturb: mujoco.MjvPerturb,
|
||||
*,
|
||||
drop_file: str = '',
|
||||
step_fn: StepFn | None = None,
|
||||
) -> bool:
|
||||
"""Update the simulation and handle user input.
|
||||
|
||||
Handles mouse input to compute perturbations or camera motion.
|
||||
Handles keyboard input e.g., for keybindings or camera motion.
|
||||
Applies the perturbations and advances the physics.
|
||||
The argument objects are provided by the viewer.
|
||||
|
||||
Args:
|
||||
camera: The MuJoCo camera object.
|
||||
vis_options: The MuJoCo visualization options.
|
||||
perturb: The MuJoCo perturbation object.
|
||||
drop_file: Path of a file dropped into the viewer window. If non-empty the
|
||||
current model is replaced with the dropped file.
|
||||
step_fn: Optional custom physics step function. When provided, it is
|
||||
called instead of ``step_control.advance``.
|
||||
|
||||
Returns:
|
||||
Whether the application should continue running, this is a
|
||||
convenience to allow this function to be used in a while loop.
|
||||
"""
|
||||
if drop_file:
|
||||
self.load_model_from_file(drop_file)
|
||||
|
||||
self.handle_mouse_events(camera, vis_options, perturb)
|
||||
self.handle_keyboard_events(camera, vis_options)
|
||||
self.update_physics(perturb, step_fn=step_fn)
|
||||
return self.is_running()
|
||||
|
||||
def update_from_viewer(
|
||||
self,
|
||||
viewer: viewer_protocol.Viewer,
|
||||
*,
|
||||
step_fn: StepFn | None = None,
|
||||
) -> bool:
|
||||
"""Convenience wrapper around update() that unpacks viewer attributes.
|
||||
|
||||
Args:
|
||||
viewer: A viewer conforming to the Viewer protocol.
|
||||
step_fn: Optional custom physics step function. When provided, it is
|
||||
called instead of ``step_control.advance``.
|
||||
|
||||
Returns:
|
||||
Whether the application should continue running.
|
||||
"""
|
||||
return self.update(
|
||||
viewer.camera,
|
||||
viewer.vis_options,
|
||||
viewer.perturb,
|
||||
drop_file=viewer.get_drop_file(),
|
||||
step_fn=step_fn,
|
||||
)
|
||||
|
||||
def build_gui(
|
||||
self,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
render_flags: ux.RenderFlags,
|
||||
) -> None:
|
||||
"""Emit full Studio UI."""
|
||||
ux.setup_theme(self.theme)
|
||||
ux.configure_docking_layout()
|
||||
|
||||
# -- Main menu bar --------------------------------------------------------
|
||||
if imgui.BeginMainMenuBar():
|
||||
if imgui.BeginMenu('File'):
|
||||
if imgui.MenuItem('Quit'):
|
||||
self.should_quit = True
|
||||
imgui.EndMenu()
|
||||
if imgui.BeginMenu('Simulation'):
|
||||
imgui.EndMenu()
|
||||
if imgui.BeginMenu('Charts'):
|
||||
if imgui.MenuItem('Solver', '', self.show_solver):
|
||||
self.show_solver = not self.show_solver
|
||||
if imgui.MenuItem('Stats', '', self.show_stats):
|
||||
self.show_stats = not self.show_stats
|
||||
imgui.EndMenu()
|
||||
if imgui.BeginMenu('Help'):
|
||||
if imgui.MenuItem('Stats', '', self.show_stats):
|
||||
self.show_stats = not self.show_stats
|
||||
imgui.Separator()
|
||||
version = f'Version {mujoco.mj_versionString()}'
|
||||
imgui.MenuItem(version)
|
||||
imgui.EndMenu()
|
||||
imgui.EndMainMenuBar()
|
||||
|
||||
# -- Tool Bar -------------------------------------------------------------
|
||||
if imgui.Begin('ToolBar'):
|
||||
imgui.PushStyleVar(imgui.StyleVar.CellPadding, imgui.Vec2(0, 0))
|
||||
if imgui.BeginTable('##ToolBarTable', 2):
|
||||
imgui.TableSetupColumn('', int(imgui.TableColumnFlags.WidthStretch))
|
||||
imgui.TableSetupColumn('', int(imgui.TableColumnFlags.WidthFixed))
|
||||
|
||||
imgui.TableNextColumn()
|
||||
self.reset_physics_gui()
|
||||
|
||||
imgui.SameLine()
|
||||
ux.step_control_gui(self.model, self.step_control, self.ux_state)
|
||||
|
||||
imgui.TableNextColumn()
|
||||
ux.camera_selection_gui(self.model, self.data, camera, self.ux_state)
|
||||
|
||||
imgui.SameLine()
|
||||
ux.label_selection_gui(vis_options)
|
||||
|
||||
imgui.SameLine()
|
||||
ux.frame_selection_gui(vis_options)
|
||||
|
||||
imgui.SameLine()
|
||||
changed, self.theme = ux.theme_select_gui(self.theme)
|
||||
if changed:
|
||||
ux.setup_theme(self.theme)
|
||||
|
||||
imgui.EndTable()
|
||||
imgui.PopStyleVar()
|
||||
imgui.End()
|
||||
|
||||
# -- Left pane: Options ---------------------------------------------------
|
||||
node_flags = int(imgui.TreeNodeFlags.SpanAvailWidth) | int(
|
||||
imgui.TreeNodeFlags.Framed
|
||||
)
|
||||
|
||||
imgui.Begin('Options')
|
||||
if imgui.TreeNodeEx('Physics Settings', node_flags):
|
||||
ux.physics_gui(self.model)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx('Rendering Settings', node_flags):
|
||||
ux.rendering_gui(self.model, vis_options, render_flags)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx('Visibility Groups', node_flags):
|
||||
ux.groups_gui(self.model, vis_options)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx('Visualization', node_flags):
|
||||
ux.visualization_gui(self.model, vis_options, camera)
|
||||
imgui.TreePop()
|
||||
imgui.End()
|
||||
|
||||
# -- Right pane: Inspector ------------------------------------------------
|
||||
imgui.Begin('Inspector')
|
||||
if imgui.TreeNodeEx('Noise', node_flags):
|
||||
ux.noise_gui(self.model, self.data, self.ux_state)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx('Joints', node_flags):
|
||||
ux.joints_gui(self.model, self.data, vis_options)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx('Controls', node_flags):
|
||||
ux.controls_gui(self.model, self.data, vis_options)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx(
|
||||
'Sensors', node_flags | int(imgui.TreeNodeFlags.DefaultOpen)
|
||||
):
|
||||
ux.sensor_gui(self.model, self.data)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx('Watch', node_flags):
|
||||
ux.watch_gui(self.model, self.data, self.ux_state)
|
||||
imgui.TreePop()
|
||||
if imgui.TreeNodeEx('State', node_flags):
|
||||
ux.state_gui(self.model, self.data, self.ux_state)
|
||||
imgui.TreePop()
|
||||
imgui.End()
|
||||
|
||||
# -- Floating windows -----------------------------------------------------
|
||||
if self.show_solver:
|
||||
_, self.show_solver = imgui.Begin('Solver', self.show_solver)
|
||||
ux.counts_gui(self.model, self.data)
|
||||
ux.convergence_gui(self.model, self.data)
|
||||
imgui.End()
|
||||
|
||||
if self.show_stats:
|
||||
_, self.show_stats = imgui.Begin('Stats', self.show_stats)
|
||||
paused = self.step_control.get_pause_state() != sim.PauseState.UNPAUSED
|
||||
ux.stats_gui(self.model, self.data, paused, 0.0)
|
||||
imgui.End()
|
||||
|
||||
# -- Status bar -----------------------------------------------------------
|
||||
imgui.PushStyleVar(imgui.StyleVar.CellPadding, imgui.Vec2(0, 0))
|
||||
imgui.PushStyleVar(imgui.StyleVar.FramePadding, imgui.Vec2(0, 0))
|
||||
imgui.PushStyleVar(imgui.StyleVar.WindowPadding, imgui.Vec2(0, 0))
|
||||
if imgui.Begin('StatusBar'):
|
||||
imgui.Text(self.status)
|
||||
imgui.End()
|
||||
imgui.PopStyleVar(3)
|
||||
@@ -0,0 +1,592 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""Temporary event handling functions for StudioApp."""
|
||||
|
||||
# TODO(matijak): These free functions implement the keyboard and mouse event
|
||||
# handling for Studio. They are separated from the main StudioApp class to keep
|
||||
# clarify the long-term API and avoid cluttering it with a large amount of
|
||||
# temporary code. When studio/platform has a proper API for registering key
|
||||
# bindings and mouse behaviour, the event handling functions will delegate to
|
||||
# code shared with the C++ studio application.
|
||||
|
||||
import mujoco
|
||||
from mujoco.experimental.studio import sim
|
||||
from mujoco.experimental.studio import ux
|
||||
import numpy as np
|
||||
|
||||
from mujoco.experimental.dear_imgui import dear_imgui as imgui
|
||||
|
||||
|
||||
def handle_vis_options_keyboard_events(
|
||||
vis_options: mujoco.MjvOption,
|
||||
is_freecam_wasd: bool,
|
||||
) -> bool:
|
||||
"""Toggles visualization flags based on keyboard shortcuts.
|
||||
|
||||
Args:
|
||||
vis_options: The visualization options to modify.
|
||||
is_freecam_wasd: If True, keys Q/E/A/D are reserved for camera movement and
|
||||
will not toggle visualization flags.
|
||||
|
||||
Returns:
|
||||
True if a key was handled, False otherwise.
|
||||
"""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False
|
||||
|
||||
pressed = imgui.IsKeyChordPressed
|
||||
|
||||
# Frame and label cycling.
|
||||
if pressed(imgui.Key.F6):
|
||||
vis_options.frame = (vis_options.frame + 1) % mujoco.mjtFrame.mjNFRAME.value
|
||||
elif pressed(imgui.Key.F7):
|
||||
vis_options.label = (vis_options.label + 1) % mujoco.mjtLabel.mjNLABEL.value
|
||||
|
||||
# Visualization flag toggles (single-key shortcuts).
|
||||
elif pressed(imgui.Key.H):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONVEXHULL] ^= 1
|
||||
elif pressed(imgui.Key.X):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_TEXTURE] ^= 1
|
||||
elif pressed(imgui.Key.J):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_JOINT] ^= 1
|
||||
elif not is_freecam_wasd and pressed(imgui.Key.Q):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CAMERA] ^= 1
|
||||
elif pressed(imgui.Key.U):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTUATOR] ^= 1
|
||||
elif pressed(imgui.Key.Comma):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTIVATION] ^= 1
|
||||
elif pressed(imgui.Key.Z):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_LIGHT] ^= 1
|
||||
elif pressed(imgui.Key.V):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_TENDON] ^= 1
|
||||
elif pressed(imgui.Key.Y):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_RANGEFINDER] ^= 1
|
||||
elif not is_freecam_wasd and pressed(imgui.Key.E):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONSTRAINT] ^= 1
|
||||
elif pressed(imgui.Key.I):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_INERTIA] ^= 1
|
||||
elif pressed(imgui.Key.Apostrophe):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_SCLINERTIA] ^= 1
|
||||
elif pressed(imgui.Key.B):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] ^= 1
|
||||
elif pressed(imgui.Key.O):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTOBJ] ^= 1
|
||||
elif pressed(imgui.Key.C):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] ^= 1
|
||||
elif pressed(imgui.Key.N):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_ISLAND] ^= 1
|
||||
elif pressed(imgui.Key.F):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] ^= 1
|
||||
elif pressed(imgui.Key.P):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTSPLIT] ^= 1
|
||||
elif pressed(imgui.Key.T):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] ^= 1
|
||||
elif not is_freecam_wasd and pressed(imgui.Key.A):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_AUTOCONNECT] ^= 1
|
||||
elif pressed(imgui.Key.M):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_COM] ^= 1
|
||||
elif not is_freecam_wasd and pressed(imgui.Key.D):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_STATIC] ^= 1
|
||||
elif pressed(imgui.Key.Semicolon):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_SKIN] ^= 1
|
||||
elif pressed(imgui.Key.GraveAccent):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_BODYBVH] ^= 1
|
||||
elif pressed(imgui.Key.Backslash):
|
||||
vis_options.flags[mujoco.mjtVisFlag.mjVIS_MESHBVH] ^= 1
|
||||
|
||||
# Site group toggles (Shift + 0-5).
|
||||
elif pressed(int(imgui.Key.N0) | int(imgui.Key.Shift)):
|
||||
vis_options.sitegroup[0] ^= 1
|
||||
elif pressed(int(imgui.Key.N1) | int(imgui.Key.Shift)):
|
||||
vis_options.sitegroup[1] ^= 1
|
||||
elif pressed(int(imgui.Key.N2) | int(imgui.Key.Shift)):
|
||||
vis_options.sitegroup[2] ^= 1
|
||||
elif pressed(int(imgui.Key.N3) | int(imgui.Key.Shift)):
|
||||
vis_options.sitegroup[3] ^= 1
|
||||
elif pressed(int(imgui.Key.N4) | int(imgui.Key.Shift)):
|
||||
vis_options.sitegroup[4] ^= 1
|
||||
elif pressed(int(imgui.Key.N5) | int(imgui.Key.Shift)):
|
||||
vis_options.sitegroup[5] ^= 1
|
||||
|
||||
# Geom group toggles (0-5).
|
||||
elif pressed(imgui.Key.N0):
|
||||
vis_options.geomgroup[0] ^= 1
|
||||
elif pressed(imgui.Key.N1):
|
||||
vis_options.geomgroup[1] ^= 1
|
||||
elif pressed(imgui.Key.N2):
|
||||
vis_options.geomgroup[2] ^= 1
|
||||
elif pressed(imgui.Key.N3):
|
||||
vis_options.geomgroup[3] ^= 1
|
||||
elif pressed(imgui.Key.N4):
|
||||
vis_options.geomgroup[4] ^= 1
|
||||
elif pressed(imgui.Key.N5):
|
||||
vis_options.geomgroup[5] ^= 1
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def handle_step_control_keyboard_events(
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
step_control: sim.StepControl,
|
||||
ux_state: ux.UxState,
|
||||
) -> bool:
|
||||
"""Handles keyboard shortcuts for simulation stepping control.
|
||||
|
||||
Args:
|
||||
model: The MuJoCo model.
|
||||
data: The MuJoCo data.
|
||||
step_control: The simulation step control object.
|
||||
ux_state: The UX state object.
|
||||
|
||||
Returns:
|
||||
True if a key was handled, False otherwise.
|
||||
"""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False
|
||||
|
||||
pressed = imgui.IsKeyChordPressed
|
||||
|
||||
if pressed(int(imgui.Key.Ctrl) | int(imgui.Key.Space)):
|
||||
if step_control.get_pause_state() == sim.PauseState.VISCOUS_PAUSED:
|
||||
step_control.set_pause_state(sim.PauseState.UNPAUSED)
|
||||
else:
|
||||
step_control.set_pause_state(sim.PauseState.VISCOUS_PAUSED)
|
||||
return True
|
||||
elif pressed(imgui.Key.Space):
|
||||
pause = step_control.get_pause_state()
|
||||
if pause in (sim.PauseState.VISCOUS_PAUSED, sim.PauseState.UNPAUSED):
|
||||
step_control.set_pause_state(sim.PauseState.NORMAL_PAUSED)
|
||||
else:
|
||||
step_control.set_pause_state(sim.PauseState.UNPAUSED)
|
||||
return True
|
||||
elif pressed(imgui.Key.Backspace):
|
||||
mujoco.mj_resetData(model, data)
|
||||
mujoco.mj_forward(model, data)
|
||||
return True
|
||||
elif pressed(imgui.Key.Minus):
|
||||
ux.set_speed_index(step_control, ux_state, ux_state.speed_index + 1)
|
||||
return True
|
||||
elif pressed(imgui.Key.Equal):
|
||||
ux.set_speed_index(step_control, ux_state, ux_state.speed_index - 1)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def handle_camera_select_keyboard_events(
|
||||
model: mujoco.MjModel,
|
||||
camera: mujoco.MjvCamera,
|
||||
ux_state: ux.UxState,
|
||||
) -> bool:
|
||||
"""Handles keyboard shortcuts for camera selection.
|
||||
|
||||
Args:
|
||||
model: The MuJoCo model.
|
||||
camera: The MuJoCo camera object.
|
||||
ux_state: The UX state object.
|
||||
|
||||
Returns:
|
||||
True if a key was handled, False otherwise.
|
||||
"""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False
|
||||
|
||||
pressed = imgui.IsKeyChordPressed
|
||||
|
||||
if pressed(imgui.Key.Escape):
|
||||
ux_state.camera_index = ux.set_camera(model, camera, ux.TUMBLE_CAMERA_IDX)
|
||||
return True
|
||||
elif pressed(imgui.Key.LeftBracket):
|
||||
ux_state.camera_index = ux.set_camera(
|
||||
model, camera, ux_state.camera_index - 1
|
||||
)
|
||||
return True
|
||||
elif pressed(imgui.Key.RightBracket):
|
||||
ux_state.camera_index = ux.set_camera(
|
||||
model, camera, ux_state.camera_index + 1
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def handle_freecam_wasd_keyboard_events(
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
camera: mujoco.MjvCamera,
|
||||
cam_speed: float,
|
||||
) -> tuple[bool, float]:
|
||||
"""Handles keyboard shortcuts for free camera movement.
|
||||
|
||||
Args:
|
||||
model: The MuJoCo model.
|
||||
data: The MuJoCo data.
|
||||
camera: The MuJoCo camera object.
|
||||
cam_speed: The current camera speed.
|
||||
|
||||
Returns:
|
||||
A tuple of (handled, updated_cam_speed).
|
||||
"""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False, cam_speed
|
||||
|
||||
if (
|
||||
imgui.IsKeyDown(imgui.Key.W)
|
||||
or imgui.IsKeyDown(imgui.Key.S)
|
||||
or imgui.IsKeyDown(imgui.Key.A)
|
||||
or imgui.IsKeyDown(imgui.Key.D)
|
||||
or imgui.IsKeyDown(imgui.Key.Q)
|
||||
or imgui.IsKeyDown(imgui.Key.E)
|
||||
):
|
||||
moved = False
|
||||
|
||||
# Move (dolly) forward/backward using W and S keys.
|
||||
if imgui.IsKeyDown(imgui.Key.W):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.TRUCK_DOLLY,
|
||||
0,
|
||||
cam_speed,
|
||||
)
|
||||
moved = True
|
||||
elif imgui.IsKeyDown(imgui.Key.S):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.TRUCK_DOLLY,
|
||||
0,
|
||||
-cam_speed,
|
||||
)
|
||||
moved = True
|
||||
|
||||
# Strafe (truck) left/right using A and D keys.
|
||||
if imgui.IsKeyDown(imgui.Key.A):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.TRUCK_DOLLY,
|
||||
-cam_speed,
|
||||
0,
|
||||
)
|
||||
moved = True
|
||||
elif imgui.IsKeyDown(imgui.Key.D):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.TRUCK_DOLLY,
|
||||
cam_speed,
|
||||
0,
|
||||
)
|
||||
moved = True
|
||||
|
||||
# Move (pedestal) up/down using Q and E keys.
|
||||
if imgui.IsKeyDown(imgui.Key.Q):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.TRUCK_PEDESTAL,
|
||||
0,
|
||||
cam_speed,
|
||||
)
|
||||
moved = True
|
||||
elif imgui.IsKeyDown(imgui.Key.E):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.TRUCK_PEDESTAL,
|
||||
0,
|
||||
-cam_speed,
|
||||
)
|
||||
moved = True
|
||||
|
||||
if moved:
|
||||
cam_speed += 0.001
|
||||
max_speed = 0.1 if imgui.GetIO().KeyShift else 0.01
|
||||
if cam_speed > max_speed:
|
||||
cam_speed = max_speed
|
||||
else:
|
||||
cam_speed = 0.001
|
||||
|
||||
return True, cam_speed
|
||||
|
||||
return False, cam_speed
|
||||
|
||||
|
||||
def handle_keyboard_events(
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
step_control: sim.StepControl,
|
||||
ux_state: ux.UxState,
|
||||
cam_speed: float,
|
||||
) -> tuple[bool, float]:
|
||||
"""Handle keyboard events according to Studio's bindings.
|
||||
|
||||
Args:
|
||||
model: The MuJoCo model.
|
||||
data: The MuJoCo data.
|
||||
camera: The MuJoCo camera object.
|
||||
vis_options: The MuJoCo visualization options.
|
||||
step_control: The simulation step control object.
|
||||
ux_state: The UX state object.
|
||||
cam_speed: The current camera speed.
|
||||
|
||||
Returns:
|
||||
A tuple of (handled, updated_cam_speed).
|
||||
"""
|
||||
if imgui.GetIO().WantCaptureKeyboard:
|
||||
return False, cam_speed
|
||||
|
||||
is_freecam_wasd = ux_state.camera_index == ux.FREE_CAMERA_IDX
|
||||
if handle_step_control_keyboard_events(model, data, step_control, ux_state):
|
||||
return True, cam_speed
|
||||
|
||||
if handle_camera_select_keyboard_events(model, camera, ux_state):
|
||||
return True, cam_speed
|
||||
|
||||
if handle_vis_options_keyboard_events(vis_options, is_freecam_wasd):
|
||||
return True, cam_speed
|
||||
|
||||
if is_freecam_wasd:
|
||||
handled, cam_speed = handle_freecam_wasd_keyboard_events(
|
||||
model, data, camera, cam_speed
|
||||
)
|
||||
if handled:
|
||||
return True, cam_speed
|
||||
|
||||
return False, cam_speed
|
||||
|
||||
|
||||
def handle_camera_tracking_mouse_events(
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
ux_state: ux.UxState,
|
||||
) -> None:
|
||||
"""Handles mouse events for camera tracking."""
|
||||
io = imgui.GetIO()
|
||||
if imgui.GetIO().WantCaptureMouse:
|
||||
return
|
||||
|
||||
if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0:
|
||||
return
|
||||
|
||||
mouse_x = io.MousePos.x / io.DisplaySize.x
|
||||
mouse_y = io.MousePos.y / io.DisplaySize.y
|
||||
aspect_ratio = io.DisplaySize.x / io.DisplaySize.y
|
||||
|
||||
# Right double click.
|
||||
if imgui.IsMouseDoubleClicked(imgui.MouseButton.Right):
|
||||
picked = ux.Pick(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
mouse_x,
|
||||
mouse_y,
|
||||
aspect_ratio,
|
||||
vis_options,
|
||||
)
|
||||
if picked.body > 0 and io.KeyCtrl:
|
||||
# Switch camera to tracking mode and track the selected body.
|
||||
camera.type = int(mujoco.mjtCamera.mjCAMERA_TRACKING)
|
||||
camera.trackbodyid = picked.body
|
||||
camera.fixedcamid = -1
|
||||
ux_state.camera_index = ux.TRACKING_CAMERA_IDX
|
||||
|
||||
|
||||
def handle_mouse_events(
|
||||
model: mujoco.MjModel,
|
||||
data: mujoco.MjData,
|
||||
camera: mujoco.MjvCamera,
|
||||
vis_options: mujoco.MjvOption,
|
||||
perturb: mujoco.MjvPerturb,
|
||||
ux_state: ux.UxState,
|
||||
) -> None:
|
||||
"""Handles mouse events."""
|
||||
io = imgui.GetIO()
|
||||
if io.WantCaptureMouse:
|
||||
return
|
||||
|
||||
if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0:
|
||||
return
|
||||
|
||||
mouse_x = io.MousePos.x / io.DisplaySize.x
|
||||
mouse_y = io.MousePos.y / io.DisplaySize.y
|
||||
mouse_dx = io.MouseDelta.x / io.DisplaySize.x
|
||||
mouse_dy = io.MouseDelta.y / io.DisplaySize.y
|
||||
mouse_scroll = io.MouseWheel / 50.0
|
||||
|
||||
is_mouse_moving = mouse_dx != 0.0 or mouse_dy != 0.0
|
||||
is_any_mouse_down = (
|
||||
imgui.IsMouseDown(imgui.MouseButton.Left)
|
||||
or imgui.IsMouseDown(imgui.MouseButton.Right)
|
||||
or imgui.IsMouseDown(imgui.MouseButton.Middle)
|
||||
)
|
||||
is_mouse_dragging = is_mouse_moving and is_any_mouse_down
|
||||
|
||||
# If no mouse buttons are down, end any active perturbations.
|
||||
if not is_any_mouse_down:
|
||||
perturb.active = 0
|
||||
|
||||
# Handle perturbation mouse actions.
|
||||
if is_mouse_dragging and io.KeyCtrl:
|
||||
if perturb.select > 0:
|
||||
action = int(mujoco.mjtMouse.mjMOUSE_NONE)
|
||||
if imgui.IsMouseDown(imgui.MouseButton.Left):
|
||||
if io.KeyAlt:
|
||||
action = int(
|
||||
mujoco.mjtMouse.mjMOUSE_MOVE_H
|
||||
if io.KeyShift
|
||||
else mujoco.mjtMouse.mjMOUSE_MOVE_V
|
||||
)
|
||||
else:
|
||||
action = int(
|
||||
mujoco.mjtMouse.mjMOUSE_ROTATE_H
|
||||
if io.KeyShift
|
||||
else mujoco.mjtMouse.mjMOUSE_ROTATE_V
|
||||
)
|
||||
elif imgui.IsMouseDown(imgui.MouseButton.Right):
|
||||
action = int(
|
||||
mujoco.mjtMouse.mjMOUSE_MOVE_H
|
||||
if io.KeyShift
|
||||
else mujoco.mjtMouse.mjMOUSE_MOVE_V
|
||||
)
|
||||
elif imgui.IsMouseDown(imgui.MouseButton.Middle):
|
||||
action = int(mujoco.mjtMouse.mjMOUSE_ZOOM)
|
||||
|
||||
active = int(
|
||||
mujoco.mjtPertBit.mjPERT_TRANSLATE
|
||||
if action
|
||||
in (
|
||||
int(mujoco.mjtMouse.mjMOUSE_MOVE_V),
|
||||
int(mujoco.mjtMouse.mjMOUSE_MOVE_H),
|
||||
)
|
||||
else mujoco.mjtPertBit.mjPERT_ROTATE
|
||||
)
|
||||
if active != perturb.active:
|
||||
ux.InitPerturb(model, data, camera, perturb, active)
|
||||
ux.MovePerturb(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
perturb,
|
||||
action,
|
||||
mouse_dx,
|
||||
mouse_dy,
|
||||
)
|
||||
elif is_mouse_dragging:
|
||||
if ux_state.camera_index == ux.FREE_CAMERA_IDX:
|
||||
if imgui.IsMouseDown(imgui.MouseButton.Left):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.PAN_TILT,
|
||||
mouse_dx,
|
||||
mouse_dy,
|
||||
)
|
||||
else:
|
||||
if imgui.IsMouseDown(imgui.MouseButton.Left):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.ORBIT,
|
||||
mouse_dx,
|
||||
mouse_dy,
|
||||
)
|
||||
elif imgui.IsMouseDown(imgui.MouseButton.Middle):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.ZOOM,
|
||||
mouse_dx,
|
||||
mouse_dy,
|
||||
)
|
||||
|
||||
# Right mouse movement is relative to the horizontal and vertical planes.
|
||||
if imgui.IsMouseDown(imgui.MouseButton.Right) and io.KeyShift:
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.PLANAR_MOVE_H,
|
||||
mouse_dx,
|
||||
mouse_dy,
|
||||
)
|
||||
elif imgui.IsMouseDown(imgui.MouseButton.Right):
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.PLANAR_MOVE_V,
|
||||
mouse_dx,
|
||||
mouse_dy,
|
||||
)
|
||||
|
||||
# Mouse scroll.
|
||||
if mouse_scroll != 0.0 and ux_state.camera_index != ux.FREE_CAMERA_IDX:
|
||||
ux.MoveCamera(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
ux.CameraMotion.ZOOM,
|
||||
0,
|
||||
-mouse_scroll,
|
||||
)
|
||||
|
||||
aspect_ratio = io.DisplaySize.x / io.DisplaySize.y
|
||||
|
||||
# Left double click.
|
||||
if imgui.IsMouseDoubleClicked(imgui.MouseButton.Left):
|
||||
picked = ux.Pick(
|
||||
model,
|
||||
data,
|
||||
camera,
|
||||
mouse_x,
|
||||
mouse_y,
|
||||
aspect_ratio,
|
||||
vis_options,
|
||||
)
|
||||
if picked.body >= 0:
|
||||
perturb.select = picked.body
|
||||
perturb.flexselect = picked.flex
|
||||
perturb.skinselect = picked.skin
|
||||
|
||||
# Compute the local position of the selected object in the world.
|
||||
tmp = np.array(picked.point, dtype=np.float64) - data.xpos[picked.body]
|
||||
xmat = np.array(data.xmat[picked.body], dtype=np.float64).reshape(3, 3)
|
||||
perturb.localpos = xmat.T @ tmp
|
||||
else:
|
||||
perturb.select = 0
|
||||
perturb.flexselect = -1
|
||||
perturb.skinselect = -1
|
||||
|
||||
handle_camera_tracking_mouse_events(
|
||||
model, data, camera, vis_options, ux_state
|
||||
)
|
||||
@@ -0,0 +1,398 @@
|
||||
// Copyright 2026 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
// Python bindings for MuJoCo platform UX components.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "third_party/mujoco/src/experimental/platform/helpers.h"
|
||||
#include "third_party/mujoco/src/experimental/platform/sim/step_control.h"
|
||||
#include "third_party/mujoco/src/experimental/platform/ux/gui.h"
|
||||
#include "third_party/mujoco/src/experimental/platform/ux/interaction.h"
|
||||
#include "structs.h"
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
struct UxState {
|
||||
// Read/edited by step_control_gui
|
||||
int speed_index = 0;
|
||||
|
||||
// Read/edited by state_gui
|
||||
std::vector<mjtNum> state;
|
||||
int state_sig = 0;
|
||||
|
||||
// Read/edited by watch_gui
|
||||
char watch_field_name[256] = {0};
|
||||
int watch_field_index = 0;
|
||||
|
||||
// Read/edited by noise_gui
|
||||
float noise_scale = 0.0f;
|
||||
float noise_rate = 0.0f;
|
||||
|
||||
// Read/edited by camera_selection_gui
|
||||
int camera_index = mujoco::platform::kTumbleCameraIdx;
|
||||
};
|
||||
|
||||
struct RenderFlags {
|
||||
std::array<uint8_t, mjNRNDFLAG> flags = {0};
|
||||
};
|
||||
|
||||
PYBIND11_MODULE(ux, m) {
|
||||
py::class_<RenderFlags>(m, "RenderFlags")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("flags", &RenderFlags::flags);
|
||||
|
||||
m.doc() = "MuJoCo platform UX components.";
|
||||
|
||||
py::enum_<mujoco::platform::GuiTheme>(m, "GuiTheme")
|
||||
.value("LIGHT", mujoco::platform::GuiTheme::kLight)
|
||||
.value("DARK", mujoco::platform::GuiTheme::kDark)
|
||||
.value("CLASSIC", mujoco::platform::GuiTheme::kClassic);
|
||||
|
||||
py::class_<UxState>(m, "UxState")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("speed_index", &UxState::speed_index)
|
||||
.def_readwrite("state", &UxState::state)
|
||||
.def_readwrite("state_sig", &UxState::state_sig)
|
||||
.def_readwrite("watch_field_index", &UxState::watch_field_index)
|
||||
.def_readwrite("noise_scale", &UxState::noise_scale)
|
||||
.def_readwrite("noise_rate", &UxState::noise_rate)
|
||||
.def_readwrite("camera_index", &UxState::camera_index)
|
||||
.def_property(
|
||||
"watch_field_name",
|
||||
[](const UxState& self) {
|
||||
return std::string(self.watch_field_name);
|
||||
},
|
||||
[](UxState& self, const std::string& val) {
|
||||
std::snprintf(self.watch_field_name, sizeof(self.watch_field_name),
|
||||
"%s", val.c_str());
|
||||
});
|
||||
|
||||
m.def(
|
||||
"setup_theme",
|
||||
[](mujoco::platform::GuiTheme theme) {
|
||||
mujoco::platform::SetupTheme(theme);
|
||||
},
|
||||
py::arg("theme"), "Set up Dear ImGui visual theme.");
|
||||
|
||||
m.def(
|
||||
"configure_docking_layout",
|
||||
[]() {
|
||||
ImVec4 r = mujoco::platform::ConfigureDockingLayout();
|
||||
return std::make_tuple(r.x, r.y, r.z, r.w);
|
||||
},
|
||||
"Configure the docking layout with Options (left) and Inspector (right) "
|
||||
"panes. Returns (x, y, w, h) of the central workspace area.");
|
||||
|
||||
m.def(
|
||||
"step_control_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::platform::StepControl* step_control, UxState& ux_state) {
|
||||
mujoco::platform::StepControlGui(model.get(), step_control,
|
||||
ux_state.speed_index);
|
||||
},
|
||||
py::arg("model"), py::arg("step_control"), py::arg("ux_state"),
|
||||
"Render the simulation stepping control GUI. Modifies "
|
||||
"ux_state.speed_index.");
|
||||
|
||||
m.def(
|
||||
"theme_select_gui",
|
||||
[](mujoco::platform::GuiTheme theme) {
|
||||
bool changed = mujoco::platform::ThemeSelectGui(&theme);
|
||||
return std::make_tuple(changed, theme);
|
||||
},
|
||||
py::arg("theme"),
|
||||
"Render the GUI theme selector. Returns (changed, theme).");
|
||||
|
||||
m.def(
|
||||
"label_selection_gui",
|
||||
[](mujoco::python::MjvOptionWrapper& vis_options) {
|
||||
return mujoco::platform::LabelSelectionGui(vis_options.get());
|
||||
},
|
||||
py::arg("vis_options"), "Render the visualization label selection GUI.");
|
||||
|
||||
m.def(
|
||||
"frame_selection_gui",
|
||||
[](mujoco::python::MjvOptionWrapper& vis_options) {
|
||||
return mujoco::platform::FrameSelectionGui(vis_options.get());
|
||||
},
|
||||
py::arg("vis_options"), "Render the visualization frame selection GUI.");
|
||||
|
||||
m.def(
|
||||
"camera_selection_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data,
|
||||
mujoco::python::MjvCameraWrapper& camera, UxState& ux_state) {
|
||||
bool changed = mujoco::platform::CameraSelectionGui(
|
||||
model.get(), data.get(), *camera.get(), ux_state.camera_index);
|
||||
return changed;
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("camera"), py::arg("ux_state"),
|
||||
"Render the camera selection GUI. Modifies ux_state.camera_index. "
|
||||
"Returns true if camera changed.");
|
||||
|
||||
m.def(
|
||||
"set_camera",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjvCameraWrapper& camera, int request_idx) {
|
||||
return mujoco::platform::SetCamera(model.get(), camera.get(), request_idx);
|
||||
},
|
||||
py::arg("model"), py::arg("camera"), py::arg("request_idx"),
|
||||
"Set the camera index and update the camera object.");
|
||||
|
||||
m.def(
|
||||
"set_speed_index",
|
||||
[](mujoco::platform::StepControl* step_control, UxState& ux_state, int idx) {
|
||||
mujoco::platform::SetSpeedIndex(step_control, ux_state.speed_index, idx);
|
||||
},
|
||||
py::arg("step_control"), py::arg("ux_state"), py::arg("idx"),
|
||||
"Set the simulation speed index.");
|
||||
|
||||
m.def(
|
||||
"physics_gui",
|
||||
[](mujoco::python::MjModelWrapper& model, float min_width) {
|
||||
mujoco::platform::PhysicsGui(model.get(), min_width);
|
||||
},
|
||||
py::arg("model"), py::arg("min_width") = 150.0f,
|
||||
"Render the physics settings UI.");
|
||||
|
||||
m.def(
|
||||
"rendering_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjvOptionWrapper& vis_options,
|
||||
RenderFlags& render_flags) {
|
||||
mjtByte flags[mjNRNDFLAG] = {0};
|
||||
for (int i = 0; i < mjNRNDFLAG; ++i) {
|
||||
flags[i] = render_flags.flags[i];
|
||||
}
|
||||
mujoco::platform::RenderingGui(model.get(), vis_options.get(), flags,
|
||||
150.0f);
|
||||
for (int i = 0; i < mjNRNDFLAG; ++i) {
|
||||
render_flags.flags[i] = flags[i];
|
||||
}
|
||||
},
|
||||
py::arg("model"), py::arg("vis_options"), py::arg("render_flags"),
|
||||
"Render the rendering settings UI. Modifies render_flags in place.");
|
||||
|
||||
m.def(
|
||||
"groups_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjvOptionWrapper& vis_options, float min_width) {
|
||||
mujoco::platform::GroupsGui(model.get(), vis_options.get(), min_width);
|
||||
},
|
||||
py::arg("model"), py::arg("vis_options"), py::arg("min_width") = 150.0f,
|
||||
"Render the visibility groups UI.");
|
||||
|
||||
m.def(
|
||||
"visualization_gui",
|
||||
[](mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjvOptionWrapper& vis_options,
|
||||
mujoco::python::MjvCameraWrapper& camera, float min_width) {
|
||||
mujoco::platform::VisualizationGui(model.get(), vis_options.get(),
|
||||
camera.get(), min_width);
|
||||
},
|
||||
py::arg("model"), py::arg("vis_options"), py::arg("camera"),
|
||||
py::arg("min_width") = 150.0f, "Render the visualization settings UI.");
|
||||
|
||||
m.def(
|
||||
"controls_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data,
|
||||
mujoco::python::MjvOptionWrapper& vis_options) {
|
||||
mujoco::platform::ControlsGui(model.get(), data.get(),
|
||||
vis_options.get());
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("vis_options"),
|
||||
"Render the actuator controls UI.");
|
||||
|
||||
m.def(
|
||||
"joints_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data,
|
||||
mujoco::python::MjvOptionWrapper& vis_options) {
|
||||
mujoco::platform::JointsGui(model.get(), data.get(), vis_options.get());
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("vis_options"),
|
||||
"Render the joints UI.");
|
||||
|
||||
m.def(
|
||||
"sensor_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data) {
|
||||
mujoco::platform::SensorGui(model.get(), data.get());
|
||||
},
|
||||
py::arg("model"), py::arg("data"), "Render the sensor data plot.");
|
||||
|
||||
m.def(
|
||||
"state_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data, UxState& ux_state,
|
||||
float min_width) {
|
||||
mujoco::platform::StateGui(model.get(), data.get(), ux_state.state,
|
||||
ux_state.state_sig, min_width);
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("ux_state"),
|
||||
py::arg("min_width") = 150.0f,
|
||||
"Render the state UI. Modifies ux_state.state and ux_state.state_sig.");
|
||||
|
||||
m.def(
|
||||
"watch_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data, UxState& ux_state) {
|
||||
mujoco::platform::WatchGui(
|
||||
model.get(), data.get(), ux_state.watch_field_name,
|
||||
sizeof(ux_state.watch_field_name), ux_state.watch_field_index);
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("ux_state"),
|
||||
"Render the watch UI. Modifies ux_state.watch_field_name and "
|
||||
"ux_state.watch_field_index.");
|
||||
|
||||
m.def(
|
||||
"noise_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data, UxState& ux_state) {
|
||||
mujoco::platform::NoiseGui(model.get(), data.get(),
|
||||
ux_state.noise_scale, ux_state.noise_rate);
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("ux_state"),
|
||||
"Render the noise UI. Modifies ux_state.noise_scale and "
|
||||
"ux_state.noise_rate.");
|
||||
|
||||
m.def(
|
||||
"convergence_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data) {
|
||||
mujoco::platform::ConvergenceGui(model.get(), data.get());
|
||||
},
|
||||
py::arg("model"), py::arg("data"),
|
||||
"Render the solver convergence chart.");
|
||||
|
||||
m.def(
|
||||
"counts_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data) {
|
||||
mujoco::platform::CountsGui(model.get(), data.get());
|
||||
},
|
||||
py::arg("model"), py::arg("data"), "Render the solver counts chart.");
|
||||
|
||||
m.def(
|
||||
"stats_gui",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
mujoco::python::MjDataWrapper& data, bool paused, float fps) {
|
||||
mujoco::platform::StatsGui(model.get(), data.get(), paused, fps);
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("paused"), py::arg("fps"),
|
||||
"Render the simulation statistics UI.");
|
||||
|
||||
m.attr("FREE_CAMERA_IDX") = mujoco::platform::kFreeCameraIdx;
|
||||
m.attr("TUMBLE_CAMERA_IDX") = mujoco::platform::kTumbleCameraIdx;
|
||||
m.attr("TRACKING_CAMERA_IDX") = mujoco::platform::kTrackingCameraIdx;
|
||||
|
||||
py::enum_<mujoco::platform::CameraMotion>(m, "CameraMotion")
|
||||
.value("ZOOM", mujoco::platform::CameraMotion::ZOOM)
|
||||
.value("ORBIT", mujoco::platform::CameraMotion::ORBIT)
|
||||
.value("TRUCK_PEDESTAL", mujoco::platform::CameraMotion::TRUCK_PEDESTAL)
|
||||
.value("TRUCK_DOLLY", mujoco::platform::CameraMotion::TRUCK_DOLLY)
|
||||
.value("PAN_TILT", mujoco::platform::CameraMotion::PAN_TILT)
|
||||
.value("PLANAR_MOVE_H", mujoco::platform::CameraMotion::PLANAR_MOVE_H)
|
||||
.value("PLANAR_MOVE_V", mujoco::platform::CameraMotion::PLANAR_MOVE_V)
|
||||
.export_values();
|
||||
|
||||
m.def(
|
||||
"MoveCamera",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
const mujoco::python::MjDataWrapper& data,
|
||||
mujoco::python::MjvCameraWrapper& cam,
|
||||
mujoco::platform::CameraMotion motion, mjtNum dx, mjtNum dy) {
|
||||
mujoco::platform::MoveCamera(model.get(), data.get(), cam.get(), motion,
|
||||
dx, dy);
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("motion"),
|
||||
py::arg("dx"), py::arg("dy"), "Moves the given camera.");
|
||||
|
||||
m.def(
|
||||
"InitPerturb",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
const mujoco::python::MjDataWrapper& data,
|
||||
const mujoco::python::MjvCameraWrapper& cam,
|
||||
mujoco::python::MjvPerturbWrapper& pert, int active) {
|
||||
mujoco::platform::InitPerturb(model.get(), data.get(), cam.get(),
|
||||
pert.get(),
|
||||
static_cast<mjtPertBit>(active));
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("pert"),
|
||||
py::arg("active"), "Initializes mouse perturbation.");
|
||||
|
||||
m.def(
|
||||
"MovePerturb",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
const mujoco::python::MjDataWrapper& data,
|
||||
const mujoco::python::MjvCameraWrapper& cam,
|
||||
mujoco::python::MjvPerturbWrapper& pert, int action, mjtNum reldx,
|
||||
mjtNum reldy) {
|
||||
mujoco::platform::MovePerturb(model.get(), data.get(), cam.get(),
|
||||
pert.get(), static_cast<mjtMouse>(action),
|
||||
reldx, reldy);
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("pert"),
|
||||
py::arg("action"), py::arg("reldx"), py::arg("reldy"),
|
||||
"Moves mouse perturbation.");
|
||||
|
||||
py::class_<mujoco::platform::PickResult>(m, "PickResult")
|
||||
.def_readwrite("dist", &mujoco::platform::PickResult::dist)
|
||||
.def_readwrite("body", &mujoco::platform::PickResult::body)
|
||||
.def_readwrite("geom", &mujoco::platform::PickResult::geom)
|
||||
.def_readwrite("flex", &mujoco::platform::PickResult::flex)
|
||||
.def_readwrite("skin", &mujoco::platform::PickResult::skin)
|
||||
.def_property(
|
||||
"point",
|
||||
[](const mujoco::platform::PickResult& res) {
|
||||
return py::make_tuple(res.point[0], res.point[1], res.point[2]);
|
||||
},
|
||||
[](mujoco::platform::PickResult& res, const py::tuple& t) {
|
||||
res.point[0] = t[0].cast<mjtNum>();
|
||||
res.point[1] = t[1].cast<mjtNum>();
|
||||
res.point[2] = t[2].cast<mjtNum>();
|
||||
});
|
||||
|
||||
m.def(
|
||||
"Pick",
|
||||
[](const mujoco::python::MjModelWrapper& model,
|
||||
const mujoco::python::MjDataWrapper& data,
|
||||
const mujoco::python::MjvCameraWrapper& cam, float x, float y,
|
||||
float aspect_ratio, const mujoco::python::MjvOptionWrapper& opt) {
|
||||
return mujoco::platform::Pick(model.get(), data.get(), cam.get(), x, y,
|
||||
aspect_ratio, opt.get());
|
||||
},
|
||||
py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("x"),
|
||||
py::arg("y"), py::arg("aspect_ratio"), py::arg("opt"),
|
||||
"Picks object under cursor.");
|
||||
|
||||
m.def(
|
||||
"camera_to_string",
|
||||
[](const mujoco::python::MjDataWrapper& data,
|
||||
const mujoco::python::MjvCameraWrapper& camera) {
|
||||
return mujoco::platform::CameraToString(data.get(), camera.get());
|
||||
},
|
||||
py::arg("data"), py::arg("camera"),
|
||||
"Returns an XML string representation of the camera.");
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright 2026 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
|
||||
#
|
||||
# https://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.
|
||||
"""Structural protocol defining the common viewer interface.
|
||||
|
||||
StudioApp uses the protocol for convenience methods that accept any viewer.
|
||||
"""
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
import mujoco
|
||||
from mujoco.experimental.studio import ux
|
||||
|
||||
|
||||
class Viewer(Protocol):
|
||||
"""Structural interface for any viewer."""
|
||||
|
||||
camera: mujoco.MjvCamera
|
||||
perturb: mujoco.MjvPerturb
|
||||
vis_options: mujoco.MjvOption
|
||||
render_flags: ux.RenderFlags
|
||||
|
||||
def is_running(self) -> bool:
|
||||
...
|
||||
|
||||
def sync(self, model: mujoco.MjModel, data: mujoco.MjData) -> None:
|
||||
...
|
||||
|
||||
def stop(self) -> None:
|
||||
...
|
||||
|
||||
def get_drop_file(self) -> str:
|
||||
...
|
||||
@@ -591,15 +591,12 @@ PYBIND11_MODULE(_functions, pymodule) {
|
||||
Def<traits::mj_id2name>(pymodule);
|
||||
Def<traits::mj_fullM>(
|
||||
pymodule,
|
||||
[](const raw::MjModel* m, Eigen::Ref<EigenArrayXX> dst,
|
||||
Eigen::Ref<const EigenVectorX> M) {
|
||||
if (M.size() != m->nM) {
|
||||
throw py::type_error("M should be of size nM");
|
||||
}
|
||||
[](const raw::MjModel* m, const raw::MjData* d,
|
||||
Eigen::Ref<EigenArrayXX> dst) {
|
||||
if (dst.cols() != m->nv || dst.rows() != m->nv) {
|
||||
throw py::type_error("dst should be of shape (nv, nv)");
|
||||
}
|
||||
return ::mj_fullM(m, dst.data(), M.data());
|
||||
return ::mj_fullM(m, d, dst.data());
|
||||
});
|
||||
Def<traits::mj_mulM>(
|
||||
pymodule,
|
||||
|
||||
@@ -3478,20 +3478,20 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
inner_type=ValueType(name='mjModel', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='d',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjData', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='dst',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtNum'),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='M',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtNum', is_const=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Convert sparse inertia matrix M into full (i.e. dense) matrix.',
|
||||
doc='Convert sparse inertia matrix into full (i.e. dense) matrix.',
|
||||
)),
|
||||
('mj_mulM',
|
||||
FunctionDecl(
|
||||
@@ -6288,44 +6288,6 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Main error function; does not return to caller.',
|
||||
)),
|
||||
('mju_error_i',
|
||||
FunctionDecl(
|
||||
name='mju_error_i',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='msg',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='i',
|
||||
type=ValueType(name='int'),
|
||||
),
|
||||
),
|
||||
doc='Deprecated: use mju_error.',
|
||||
)),
|
||||
('mju_error_s',
|
||||
FunctionDecl(
|
||||
name='mju_error_s',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='msg',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='text',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Deprecated: use mju_error.',
|
||||
)),
|
||||
('mju_warning',
|
||||
FunctionDecl(
|
||||
name='mju_warning',
|
||||
@@ -6340,44 +6302,6 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Main warning function; returns to caller.',
|
||||
)),
|
||||
('mju_warning_i',
|
||||
FunctionDecl(
|
||||
name='mju_warning_i',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='msg',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='i',
|
||||
type=ValueType(name='int'),
|
||||
),
|
||||
),
|
||||
doc='Deprecated: use mju_warning.',
|
||||
)),
|
||||
('mju_warning_s',
|
||||
FunctionDecl(
|
||||
name='mju_warning_s',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='msg',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='text',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Deprecated: use mju_warning.',
|
||||
)),
|
||||
('mju_clearHandlers',
|
||||
FunctionDecl(
|
||||
name='mju_clearHandlers',
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
#define mjMINVAL2 (mjMINVAL * mjMINVAL)
|
||||
#define mjMAXVAL2 (mjMAXVAL * mjMAXVAL)
|
||||
|
||||
// align memory size on 8-byte boundary; needed for single precision
|
||||
static inline size_t align8(size_t size) {
|
||||
return ((size + 7) / 8) * 8;
|
||||
}
|
||||
|
||||
// subdistance algorithm for GJK that computes the barycentric coordinates of the point in a
|
||||
// simplex closest to the origin
|
||||
// implementation adapted from Montanari et al, ToG 2017
|
||||
@@ -1533,6 +1538,9 @@ static mjtNum planeNormal(mjtNum res[3], const mjtNum v1[3], const mjtNum v2[3],
|
||||
sub3(diff1, v2, v1);
|
||||
sub3(diff2, v3, v1);
|
||||
cross3(res, diff1, diff2);
|
||||
|
||||
// normalize isn't needed (cancelled out), but done to avoid asymmetric rounding later on
|
||||
mju_normalize3(res);
|
||||
return dot3(res, v1);
|
||||
}
|
||||
|
||||
@@ -2217,10 +2225,11 @@ static inline void inflate(mjCCDStatus* status, mjtNum margin1, mjtNum margin2)
|
||||
|
||||
// return size in bytes of the buffer needed for mjc_ccd for a given number of iterations
|
||||
size_t mjc_ccdSize(int iterations) {
|
||||
return (sizeof(Face) * 6 * iterations) // faces in polytope
|
||||
+ (sizeof(Face*) * 6 * iterations) // map in polytope
|
||||
+ (sizeof(Vertex) * (5 + iterations)) // vertices in polytope
|
||||
+ 2 * (24 * sizeof(int)); // horizon data
|
||||
return align8(sizeof(Vertex) * (5 + iterations)) // vertices in polytope
|
||||
+ align8(sizeof(Face) * 6 * iterations) // faces in polytope
|
||||
+ align8(sizeof(Face*) * 6 * iterations) // map in polytope
|
||||
+ align8(sizeof(int) * 24) // horizon indices
|
||||
+ align8(sizeof(int) * 24); // horizon edges
|
||||
}
|
||||
|
||||
|
||||
@@ -2313,13 +2322,13 @@ mjtNum mjc_ccd(const mjCCDConfig* config, mjCCDStatus* status, mjCCDObj* obj1, m
|
||||
pt.maxfaces = 6 * N;
|
||||
uint8_t* buffer = config->buffer;
|
||||
pt.verts = (Vertex*)buffer;
|
||||
buffer += sizeof(Vertex) * (5 + N);
|
||||
buffer += align8(sizeof(Vertex) * (5 + N));
|
||||
pt.faces = (Face*)buffer;
|
||||
buffer += sizeof(Face) * (6 * N);
|
||||
buffer += align8(sizeof(Face) * (6 * N));
|
||||
pt.map = (Face**)buffer;
|
||||
buffer += sizeof(Face*) * (6 * N);
|
||||
buffer += align8(sizeof(Face*) * (6 * N));
|
||||
pt.horizon.indices = (int*)buffer;
|
||||
buffer += sizeof(int) * 24;
|
||||
buffer += align8(sizeof(int) * 24);
|
||||
pt.horizon.edges = (int*)buffer;
|
||||
|
||||
int ret;
|
||||
|
||||
@@ -367,19 +367,8 @@ void mj_setKeyframe(mjModel* m, const mjData* d, int k) {
|
||||
//-------------------------- inertia functions -----------------------------------------------------
|
||||
|
||||
// convert sparse inertia matrix M into full matrix
|
||||
void mj_fullM(const mjModel* m, mjtNum* dst, const mjtNum* M) {
|
||||
int adr = 0, nv = m->nv;
|
||||
mju_zero(dst, nv*nv);
|
||||
|
||||
for (int i=0; i < nv; i++) {
|
||||
int j = i;
|
||||
while (j >= 0) {
|
||||
dst[i*nv+j] = M[adr];
|
||||
dst[j*nv+i] = M[adr];
|
||||
j = m->dof_parentid[j];
|
||||
adr++;
|
||||
}
|
||||
}
|
||||
void mj_fullM(const mjModel* m, const mjData* d, mjtNum* dst) {
|
||||
mju_sym2dense(dst, d->M, m->nv, m->M_rownnz, m->M_rowadr, m->M_colind);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ MJAPI void mj_setKeyframe(mjModel* m, const mjData* d, int k);
|
||||
//-------------------------- inertia functions -----------------------------------------------------
|
||||
|
||||
// convert sparse inertia matrix M into full matrix
|
||||
MJAPI void mj_fullM(const mjModel* m, mjtNum* dst, const mjtNum* M);
|
||||
MJAPI void mj_fullM(const mjModel* m, const mjData* d, mjtNum* dst);
|
||||
|
||||
// multiply vector by inertia matrix
|
||||
MJAPI void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec);
|
||||
|
||||
@@ -169,30 +169,6 @@ void mju_warning(const char* msg, ...) {
|
||||
}
|
||||
|
||||
|
||||
// error with int argument
|
||||
void mju_error_i(const char* msg, int i) {
|
||||
mju_error(msg, i);
|
||||
}
|
||||
|
||||
|
||||
// warning with int argument
|
||||
void mju_warning_i(const char* msg, int i) {
|
||||
mju_warning(msg, i);
|
||||
}
|
||||
|
||||
|
||||
// error string argument
|
||||
void mju_error_s(const char* msg, const char* text) {
|
||||
mju_error(msg, text);
|
||||
}
|
||||
|
||||
|
||||
// warning string argument
|
||||
void mju_warning_s(const char* msg, const char* text) {
|
||||
mju_warning(msg, text);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------ malloc and free ---------------------------------------------------
|
||||
|
||||
// allocate memory; byte-align on 64; pad size to multiple of 64
|
||||
|
||||
@@ -58,13 +58,9 @@ MJAPI void _mjPRIVATE__set_tls_warning_fn(void (*h)(const char*));
|
||||
MJAPI void mju_error_raw(const char* msg);
|
||||
MJAPI void mju_error(const char* msg, ...) mjPRINTFLIKE(1, 2);
|
||||
MJAPI void mju_error_v(const char* msg, va_list args);
|
||||
MJAPI void mju_error_i(const char* msg, int i);
|
||||
MJAPI void mju_error_s(const char* msg, const char* text);
|
||||
|
||||
// warnings
|
||||
MJAPI void mju_warning(const char* msg, ...) mjPRINTFLIKE(1, 2);
|
||||
MJAPI void mju_warning_i(const char* msg, int i);
|
||||
MJAPI void mju_warning_s(const char* msg, const char* text);
|
||||
|
||||
// write [datetime, type: message] to MUJOCO_LOG.TXT
|
||||
MJAPI void mju_writeLog(const char* type, const char* msg);
|
||||
|
||||
@@ -159,8 +159,6 @@ static void UpdateGeomMaterial(mjrRenderable* renderable, const mjvGeom& geom,
|
||||
material.color[2] = geom.rgba[2];
|
||||
material.color[3] = geom.rgba[3];
|
||||
|
||||
mjrf_setRenderableLayerMask(renderable, geom.category);
|
||||
|
||||
if (geom.matid >= 0 && geom.matid < model->nmat) {
|
||||
auto get_texture = [&](int role) -> const mjrTexture* {
|
||||
const int tex_id = model->mat_texid[geom.matid * mjNTEXROLE + role];
|
||||
@@ -283,6 +281,7 @@ UniquePtr<mjrRenderable> CreateGeomRenderable(
|
||||
const mjtByte render_flags[mjNRNDFLAG]) {
|
||||
mjrRenderableParams params;
|
||||
mjr_defaultRenderableParams(¶ms);
|
||||
params.layer_mask = geom.category;
|
||||
auto renderable = CreateRenderable(ctx, params);
|
||||
PrepareGeomMeshes(renderable.get(), geom, model_objs);
|
||||
UpdateGeomMaterial(renderable.get(), geom, model_objs, render_flags);
|
||||
|
||||
@@ -255,7 +255,9 @@ void SceneView::Render(filament::Renderer* renderer, const mjrRenderRequest& req
|
||||
|
||||
for (auto& iter : renderables_) {
|
||||
iter->BindMaterialInstance(request);
|
||||
}
|
||||
|
||||
for (auto& iter : renderables_) {
|
||||
if (RenderTarget* target = iter->GetReflectionTarget()) {
|
||||
viewport.left = 0;
|
||||
viewport.bottom = 0;
|
||||
|
||||
@@ -245,6 +245,11 @@ void mjrf_setRenderableMaterial(mjrRenderable* renderable,
|
||||
mujoco::Renderable::downcast(renderable)->UpdateMaterial(*material);
|
||||
}
|
||||
|
||||
void mjrf_getRenderableMaterial(mjrRenderable* renderable,
|
||||
mjrMaterial* material) {
|
||||
*material = mujoco::Renderable::downcast(renderable)->GetMaterial();
|
||||
}
|
||||
|
||||
void mjrf_setRenderableTransform(mjrRenderable* renderable,
|
||||
const float position[3],
|
||||
const float rotation[9]) {
|
||||
@@ -261,21 +266,6 @@ void mjrf_setRenderableSize(mjrRenderable* renderable, const float size[3]) {
|
||||
mujoco::Renderable::downcast(renderable)->SetSize(fsize);
|
||||
}
|
||||
|
||||
void mjrf_setRenderableLayerMask(mjrRenderable* renderable,
|
||||
uint8_t layer_mask) {
|
||||
mujoco::Renderable::downcast(renderable)->SetLayerMask(layer_mask);
|
||||
}
|
||||
|
||||
void mjrf_setRenderableCastShadows(mjrRenderable* renderable,
|
||||
mjtByte cast_shadows) {
|
||||
mujoco::Renderable::downcast(renderable)->SetCastShadows(cast_shadows);
|
||||
}
|
||||
|
||||
void mjrf_setRenderableReceiveShadows(mjrRenderable* renderable,
|
||||
mjtByte receive_shadows) {
|
||||
mujoco::Renderable::downcast(renderable)->SetReceiveShadows(receive_shadows);
|
||||
}
|
||||
|
||||
void mjrf_addLightToScene(mjrScene* scene, mjrLight* light) {
|
||||
mujoco::SceneView::downcast(scene)->AddToScene(
|
||||
mujoco::Light::downcast(light));
|
||||
|
||||
@@ -67,7 +67,6 @@ struct mjrLight {};
|
||||
struct mjrRenderable {};
|
||||
struct mjrRenderTarget {};
|
||||
|
||||
|
||||
// ## Rendering Context (mjrfContext)
|
||||
//
|
||||
// The Context is the main entry point for the library. It manages all the
|
||||
@@ -520,22 +519,31 @@ typedef mjtLightType mjrLightType;
|
||||
struct mjrLightParams {
|
||||
// The type of light (e.g. spot, point, directional, etc.)
|
||||
mjrLightType type;
|
||||
|
||||
// The texture to use for image lights.
|
||||
const mjrTexture* texture;
|
||||
|
||||
// The color of the light.
|
||||
float color[3];
|
||||
|
||||
// The intensity of the light, in candela.
|
||||
float intensity;
|
||||
|
||||
// Whether or not the light casts shadows.
|
||||
mjtByte cast_shadows;
|
||||
|
||||
// The range/distance in which the light is effective, in meters.
|
||||
float range;
|
||||
|
||||
// The angle of the spot light cone, in degrees.
|
||||
float spot_cone_angle;
|
||||
|
||||
// The radius of the bulb used for soft shadows.
|
||||
float bulb_radius;
|
||||
|
||||
// The size of the shadow map.
|
||||
int shadow_map_size;
|
||||
|
||||
// Blur width for EL VSM.
|
||||
float vsm_blur_width;
|
||||
};
|
||||
@@ -656,15 +664,19 @@ void mjr_defaultMaterial(mjrMaterial* material);
|
||||
struct mjrRenderableParams {
|
||||
// Whether or not the Renderable casts shadows.
|
||||
mjtByte cast_shadows;
|
||||
|
||||
// Whether or not the Renderable receives shadows.
|
||||
mjtByte receive_shadows;
|
||||
|
||||
// The layers to which the Renderable belongs. This mask is used in
|
||||
// conjunction with the layer mask in the Scene to determine which
|
||||
// Renderables to render. Defaults to 0xff.
|
||||
uint8_t layer_mask;
|
||||
|
||||
// Controls the order in which the Renderable is drawn relative to other
|
||||
// Renderables; defaults to 4.
|
||||
uint8_t priority;
|
||||
|
||||
// Similar to priority, but provides finer-grained control for Renderables
|
||||
// with transparency; defaults to 0.
|
||||
uint16_t blend_order;
|
||||
@@ -694,6 +706,10 @@ void mjrf_setRenderableGeomMesh(mjrRenderable* renderable, mjtGeom type,
|
||||
void mjrf_setRenderableMaterial(mjrRenderable* renderable,
|
||||
const mjrMaterial* material);
|
||||
|
||||
// Copies the material properties of the renderable into the given mjrMaterial.
|
||||
void mjrf_getRenderableMaterial(mjrRenderable* renderable,
|
||||
mjrMaterial* material);
|
||||
|
||||
// Sets the transform position and rotation of the renderable.
|
||||
void mjrf_setRenderableTransform(mjrRenderable* renderable,
|
||||
const float position[3],
|
||||
@@ -705,17 +721,6 @@ void mjrf_setRenderableTransform(mjrRenderable* renderable,
|
||||
// capsule are scaled such that they always remain spherical).
|
||||
void mjrf_setRenderableSize(mjrRenderable* renderable, const float size[3]);
|
||||
|
||||
// Sets whether the renderable casts shadows or not.
|
||||
void mjrf_setRenderableCastShadows(mjrRenderable* renderable,
|
||||
mjtByte cast_shadows);
|
||||
|
||||
// Sets whether the renderable receives shadows or not.
|
||||
void mjrf_setRenderableReceiveShadows(mjrRenderable* renderable,
|
||||
mjtByte receive_shadows);
|
||||
|
||||
// Sets the layer mask of the renderable. See mjrRenderableParams for details.
|
||||
void mjrf_setRenderableLayerMask(mjrRenderable* renderable, uint8_t layer_mask);
|
||||
|
||||
// ## Render Targets (mjrRenderTarget)
|
||||
//
|
||||
// A render target is a memory buffer that holds the results of a rendering
|
||||
@@ -726,10 +731,13 @@ void mjrf_setRenderableLayerMask(mjrRenderable* renderable, uint8_t layer_mask);
|
||||
struct mjrRenderTargetConfig {
|
||||
// The width of the render target.
|
||||
int width;
|
||||
|
||||
// The height of the render target.
|
||||
int height;
|
||||
|
||||
// The format of the color buffer in the render target.
|
||||
mjrPixelFormat color_format;
|
||||
|
||||
// The format of the depth buffer in the render target.
|
||||
mjrPixelFormat depth_format;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <ratio>
|
||||
|
||||
@@ -91,7 +92,8 @@ StepControl::PauseState StepControl::GetPauseState() const {
|
||||
return pause_state_;
|
||||
}
|
||||
|
||||
StepControl::Status StepControl::Advance(mjModel* m, mjData* d) {
|
||||
StepControl::Status StepControl::Advance(mjModel* m, mjData* d,
|
||||
StepFn step_fn) {
|
||||
if (!m) {
|
||||
return Status::kOk;
|
||||
}
|
||||
@@ -182,7 +184,11 @@ StepControl::Status StepControl::Advance(mjModel* m, mjData* d) {
|
||||
|
||||
mjtNum prev_time = d->time;
|
||||
InjectNoise(m, d);
|
||||
mj_step(m, d);
|
||||
if (step_fn) {
|
||||
step_fn(m, d);
|
||||
} else {
|
||||
mj_step(m, d);
|
||||
}
|
||||
|
||||
if (mjDISABLED(mjDSBL_AUTORESET)) {
|
||||
for (mjtWarning w : kDivergedWarnings) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_SIM_STEP_CONTROL_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
@@ -24,6 +25,7 @@ namespace mujoco::platform {
|
||||
|
||||
using Seconds = std::chrono::duration<double>;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using StepFn = std::function<void(mjModel*, mjData*)>;
|
||||
|
||||
// State and logic for physics synchronization and stepping.
|
||||
class StepControl {
|
||||
@@ -53,7 +55,7 @@ class StepControl {
|
||||
mjWARN_BADQACC, mjWARN_BADQVEL, mjWARN_BADQPOS};
|
||||
|
||||
// Steps physics forward, respecting speed settings and refresh budget.
|
||||
Status Advance(mjModel* m, mjData* d);
|
||||
Status Advance(mjModel* m, mjData* d, StepFn step_fn = nullptr);
|
||||
|
||||
// Ensures the next call to Advance() will synchronize time and step once.
|
||||
void ForceSync();
|
||||
|
||||
@@ -50,6 +50,14 @@ function resolveScheme(url) {
|
||||
}
|
||||
|
||||
async function prefetchModelAssets(rootUrl, onProgress) {
|
||||
// Only prefetch when the root URL is one we know how to fetch from
|
||||
// Javascript. Other resource-provider-backed schemes (e.g., uploaded files
|
||||
// via the drag-and-drop path) skip the prefetcher entirely and let
|
||||
// Module.loadUrl handle take the existing slow path.
|
||||
if (!/^(https?:|github:)/.test(rootUrl)) {
|
||||
return { files: 0, bytes: 0, errors: 0 };
|
||||
}
|
||||
|
||||
const primed = new Set(); // URLs we've already pushed into FetchCache
|
||||
const stats = { files: 0, bytes: 0, errors: 0 };
|
||||
|
||||
|
||||
@@ -199,6 +199,42 @@ target_link_libraries(mujoco PUBLIC
|
||||
${MJC_PHYSICS_PLUGIN_TARGET_NAME}
|
||||
)
|
||||
|
||||
## ----- Newton USD Schemas (codeless plugin) -----
|
||||
|
||||
function(install_newton_usd_plugin install_base_dir)
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
newton-usd-schemas
|
||||
GIT_REPOSITORY https://github.com/newton-physics/newton-usd-schemas.git
|
||||
GIT_TAG v0.1.0rc3
|
||||
GIT_SHALLOW TRUE
|
||||
UPDATE_DISCONNECTED TRUE
|
||||
)
|
||||
|
||||
FetchContent_GetProperties(newton-usd-schemas)
|
||||
if(NOT newton-usd-schemas_POPULATED)
|
||||
FetchContent_Populate(newton-usd-schemas)
|
||||
endif()
|
||||
|
||||
set(NEWTON_USD_DIR "${newton-usd-schemas_SOURCE_DIR}/newton_usd_schemas")
|
||||
if(NOT EXISTS "${NEWTON_USD_DIR}")
|
||||
message(FATAL_ERROR "newton_usd_schemas directory not found in fetched repository")
|
||||
endif()
|
||||
|
||||
set(NEWTON_BUILD_DIR "${CMAKE_BINARY_DIR}/${install_base_dir}/newton")
|
||||
file(MAKE_DIRECTORY "${NEWTON_BUILD_DIR}")
|
||||
configure_file("${NEWTON_USD_DIR}/plugInfo.json" "${NEWTON_BUILD_DIR}/plugInfo.json" COPYONLY)
|
||||
configure_file("${NEWTON_USD_DIR}/generatedSchema.usda" "${NEWTON_BUILD_DIR}/generatedSchema.usda" COPYONLY)
|
||||
|
||||
install(FILES "${NEWTON_BUILD_DIR}/plugInfo.json"
|
||||
DESTINATION "${install_base_dir}/newton"
|
||||
)
|
||||
install(FILES "${NEWTON_BUILD_DIR}/generatedSchema.usda"
|
||||
DESTINATION "${install_base_dir}/newton"
|
||||
)
|
||||
endfunction()
|
||||
|
||||
## Installation
|
||||
|
||||
# Generate and install plugInfo.json for each plugin
|
||||
@@ -219,6 +255,10 @@ install(FILES
|
||||
DESTINATION ${MJ_USD_INSTALL_DIR_LIB}/mjcPhysics
|
||||
)
|
||||
|
||||
install_newton_usd_plugin(
|
||||
${MJ_USD_INSTALL_DIR_LIB}
|
||||
)
|
||||
|
||||
# Install shared libraries
|
||||
install(TARGETS
|
||||
${MJCF_PLUGIN_TARGET_NAME}
|
||||
|
||||
@@ -79,6 +79,28 @@ const TfType& MjcPhysicsEqualityJointAPI::_GetTfType() const {
|
||||
return _GetStaticTfType();
|
||||
}
|
||||
|
||||
UsdAttribute MjcPhysicsEqualityJointAPI::GetSolImpAttr() const {
|
||||
return GetPrim().GetAttribute(MjcPhysicsTokens->mjcSolimp);
|
||||
}
|
||||
|
||||
UsdAttribute MjcPhysicsEqualityJointAPI::CreateSolImpAttr(
|
||||
VtValue const& defaultValue, bool writeSparsely) const {
|
||||
return UsdSchemaBase::_CreateAttr(
|
||||
MjcPhysicsTokens->mjcSolimp, SdfValueTypeNames->DoubleArray,
|
||||
/* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely);
|
||||
}
|
||||
|
||||
UsdAttribute MjcPhysicsEqualityJointAPI::GetSolRefAttr() const {
|
||||
return GetPrim().GetAttribute(MjcPhysicsTokens->mjcSolref);
|
||||
}
|
||||
|
||||
UsdAttribute MjcPhysicsEqualityJointAPI::CreateSolRefAttr(
|
||||
VtValue const& defaultValue, bool writeSparsely) const {
|
||||
return UsdSchemaBase::_CreateAttr(
|
||||
MjcPhysicsTokens->mjcSolref, SdfValueTypeNames->DoubleArray,
|
||||
/* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely);
|
||||
}
|
||||
|
||||
UsdAttribute MjcPhysicsEqualityJointAPI::GetCoef0Attr() const {
|
||||
return GetPrim().GetAttribute(MjcPhysicsTokens->mjcCoef0);
|
||||
}
|
||||
@@ -134,6 +156,15 @@ UsdAttribute MjcPhysicsEqualityJointAPI::CreateCoef4Attr(
|
||||
/* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely);
|
||||
}
|
||||
|
||||
UsdRelationship MjcPhysicsEqualityJointAPI::GetMjcTargetRel() const {
|
||||
return GetPrim().GetRelationship(MjcPhysicsTokens->mjcTarget);
|
||||
}
|
||||
|
||||
UsdRelationship MjcPhysicsEqualityJointAPI::CreateMjcTargetRel() const {
|
||||
return GetPrim().CreateRelationship(MjcPhysicsTokens->mjcTarget,
|
||||
/* custom = */ false);
|
||||
}
|
||||
|
||||
namespace {
|
||||
static inline TfTokenVector _ConcatenateAttributeNames(
|
||||
const TfTokenVector& left, const TfTokenVector& right) {
|
||||
@@ -149,8 +180,9 @@ static inline TfTokenVector _ConcatenateAttributeNames(
|
||||
const TfTokenVector& MjcPhysicsEqualityJointAPI::GetSchemaAttributeNames(
|
||||
bool includeInherited) {
|
||||
static TfTokenVector localNames = {
|
||||
MjcPhysicsTokens->mjcCoef0, MjcPhysicsTokens->mjcCoef1,
|
||||
MjcPhysicsTokens->mjcCoef2, MjcPhysicsTokens->mjcCoef3,
|
||||
MjcPhysicsTokens->mjcSolimp, MjcPhysicsTokens->mjcSolref,
|
||||
MjcPhysicsTokens->mjcCoef0, MjcPhysicsTokens->mjcCoef1,
|
||||
MjcPhysicsTokens->mjcCoef2, MjcPhysicsTokens->mjcCoef3,
|
||||
MjcPhysicsTokens->mjcCoef4,
|
||||
};
|
||||
static TfTokenVector allNames = _ConcatenateAttributeNames(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
)
|
||||
|
||||
class "MjcSceneAPI" (
|
||||
apiSchemas = ["NewtonSceneAPI"]
|
||||
doc = "API providing global simulation options for MuJoCo."
|
||||
)
|
||||
{
|
||||
@@ -120,7 +121,9 @@ class "MjcSceneAPI" (
|
||||
)
|
||||
uniform bool mjc:flag:gravity = 1 (
|
||||
displayName = "Gravity Toggle"
|
||||
doc = "Enables the application of gravitational acceleration as defined in mjOption."
|
||||
doc = """DEPRECATED: Use newton:gravityEnabled instead.
|
||||
|
||||
Enables the application of gravitational acceleration as defined in mjOption."""
|
||||
)
|
||||
uniform bool mjc:flag:invdiscrete = 0 (
|
||||
displayName = "Discrete-Time Inverse Dynamics Toggle"
|
||||
@@ -200,7 +203,9 @@ class "MjcSceneAPI" (
|
||||
)
|
||||
uniform int mjc:option:iterations = 100 (
|
||||
displayName = "Solver Iterations"
|
||||
doc = "Maximum number of iterations of the constraint solver."
|
||||
doc = """DEPRECATED: Use newton:maxSolverIterations instead.
|
||||
|
||||
Maximum number of iterations of the constraint solver."""
|
||||
)
|
||||
uniform token mjc:option:jacobian = "auto" (
|
||||
allowedTokens = ["auto", "dense", "sparse"]
|
||||
@@ -265,7 +270,9 @@ class "MjcSceneAPI" (
|
||||
)
|
||||
uniform double mjc:option:timestep = 0.002 (
|
||||
displayName = "Timestep"
|
||||
doc = "Controls the timestep in seconds used by MuJoCo."
|
||||
doc = """DEPRECATED: Use newton:timeStepsPerSecond instead.
|
||||
|
||||
Controls the timestep in seconds used by MuJoCo."""
|
||||
)
|
||||
uniform double mjc:option:tolerance = 1e-8 (
|
||||
displayName = "Solver Tolerance"
|
||||
@@ -303,6 +310,7 @@ class "MjcImageableAPI" (
|
||||
}
|
||||
|
||||
class "MjcCollisionAPI" (
|
||||
apiSchemas = ["NewtonCollisionAPI"]
|
||||
doc = "API describing a MuJoCo collider."
|
||||
)
|
||||
{
|
||||
@@ -312,7 +320,9 @@ class "MjcCollisionAPI" (
|
||||
)
|
||||
uniform double mjc:gap = 0 (
|
||||
displayName = "Gap"
|
||||
doc = "Additional contact detection buffer beyond margin. Contacts are detected at distance margin + gap but forces are only generated at distance margin."
|
||||
doc = """DEPRECATED: Use newton:contactGap instead.
|
||||
|
||||
Additional contact detection buffer beyond margin. Contacts are detected at distance margin + gap but forces are only generated at distance margin."""
|
||||
)
|
||||
uniform int mjc:group = 0 (
|
||||
displayName = "Group"
|
||||
@@ -320,7 +330,9 @@ class "MjcCollisionAPI" (
|
||||
)
|
||||
uniform double mjc:margin = 0 (
|
||||
displayName = "Margin"
|
||||
doc = "Geometric inflation of the geom surface for the purpose of contact force generation."
|
||||
doc = """DEPRECATED: Use newton:contactMargin and newton:contactGap instead.
|
||||
|
||||
Geometric inflation of the geom surface for the purpose of contact force generation."""
|
||||
)
|
||||
uniform int mjc:priority = 0 (
|
||||
displayName = "Priority"
|
||||
@@ -345,6 +357,7 @@ class "MjcCollisionAPI" (
|
||||
}
|
||||
|
||||
class "MjcMeshCollisionAPI" (
|
||||
apiSchemas = ["NewtonMeshCollisionAPI"]
|
||||
doc = "API describing a MuJoCo mesh collider."
|
||||
)
|
||||
{
|
||||
@@ -355,7 +368,9 @@ class "MjcMeshCollisionAPI" (
|
||||
)
|
||||
uniform int mjc:maxhullvert = -1 (
|
||||
displayName = "Maximum Hull Vertices"
|
||||
doc = "Sets an upper limit on the number of vertices in the meshes convex hull. The default value of -1 means unlimited."
|
||||
doc = """DEPRECATED: Use newton:maxHullVertices instead.
|
||||
|
||||
Sets an upper limit on the number of vertices in the meshes convex hull. The default value of -1 means unlimited."""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -537,16 +552,23 @@ class "MjcJointAPI" (
|
||||
}
|
||||
|
||||
class "MjcMaterialAPI" (
|
||||
doc = "API providing extension attributes to represent physical MuJoCo materials."
|
||||
apiSchemas = ["NewtonMaterialAPI"]
|
||||
doc = """DEPRECATED: Use NewtonMaterialAPI instead. All attributes on this API have been superseded by Newton equivalents.
|
||||
|
||||
API providing extension attributes to represent physical MuJoCo materials."""
|
||||
)
|
||||
{
|
||||
uniform double mjc:rollingfriction = 0.0001 (
|
||||
displayName = "Rolling Friction"
|
||||
doc = "Friction value acting around both axes on the contact tangent plane."
|
||||
doc = """DEPRECATED: Use newton:rollingFriction instead.
|
||||
|
||||
Friction value acting around both axes on the contact tangent plane."""
|
||||
)
|
||||
uniform double mjc:torsionalfriction = 0.005 (
|
||||
displayName = "Torsional Friction"
|
||||
doc = "Friction value acting around contact normal."
|
||||
doc = """DEPRECATED: Use newton:torsionalFriction instead.
|
||||
|
||||
Friction value acting around contact normal."""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -586,23 +608,36 @@ class "MjcEqualityWeldAPI" (
|
||||
}
|
||||
|
||||
class "MjcEqualityJointAPI" (
|
||||
apiSchemas = ["MjcEqualityAPI"]
|
||||
apiSchemas = ["NewtonMimicAPI"]
|
||||
doc = """API providing extension attributes to represent equality/joint constraints.
|
||||
This API is applied to a joint prim which acts as the constrained joint (joint1 in
|
||||
MuJoCo terminology). The target relationship points to another joint prim which is
|
||||
the reference joint (joint2 in MuJoCo terminology). The constrained joint's position
|
||||
or angle is constrained to be a quartic polynomial of the reference joint's position
|
||||
or angle. Only scalar joint types (slide and hinge) can be used."""
|
||||
|
||||
This API is applied to a joint prim which acts as the follower (joint0). The leader
|
||||
joint (joint1) is specified via the newton:mimicJoint relationship inherited from
|
||||
NewtonMimicAPI.
|
||||
|
||||
The follower's position or angle is constrained to be a quartic polynomial of the
|
||||
leader's position or angle:
|
||||
joint0 = coef0 + coef1*(joint1) + coef2*(joint1)^2 + coef3*(joint1)^3 + coef4*(joint1)^4
|
||||
|
||||
The constant (coef0) and linear (coef1) coefficients are provided by NewtonMimicAPI
|
||||
as newton:mimicCoef0 and newton:mimicCoef1. The higher-order coefficients (coef2-coef4)
|
||||
are provided by this API.
|
||||
|
||||
Only scalar joint types (slide and hinge) can be used."""
|
||||
)
|
||||
{
|
||||
uniform double mjc:coef0 = 0 (
|
||||
displayName = "Coefficient 0"
|
||||
doc = """Constant coefficient a0 of the quartic polynomial. The constraint is:
|
||||
doc = """DEPRECATED: Use newton:mimicCoef0 instead.
|
||||
|
||||
Constant coefficient a0 of the quartic polynomial. The constraint is:
|
||||
y = y0 + a0 + a1*(x-x0) + a2*(x-x0)^2 + a3*(x-x0)^3 + a4*(x-x0)^4."""
|
||||
)
|
||||
uniform double mjc:coef1 = 1 (
|
||||
displayName = "Coefficient 1"
|
||||
doc = "Linear coefficient a1 of the quartic polynomial."
|
||||
doc = """DEPRECATED: Use newton:mimicCoef1 instead.
|
||||
|
||||
Linear coefficient a1 of the quartic polynomial."""
|
||||
)
|
||||
uniform double mjc:coef2 = 0 (
|
||||
displayName = "Coefficient 2"
|
||||
@@ -616,6 +651,19 @@ class "MjcEqualityJointAPI" (
|
||||
displayName = "Coefficient 4"
|
||||
doc = "Quartic coefficient a4 of the quartic polynomial."
|
||||
)
|
||||
uniform double[] mjc:solimp = [0.9, 0.95, 0.001, 0.5, 2] (
|
||||
displayName = "SolImp"
|
||||
doc = "Constraint solver parameter for equality constraint simulation."
|
||||
)
|
||||
uniform double[] mjc:solref = [0.02, 1] (
|
||||
displayName = "SolRef"
|
||||
doc = "Constraint solver parameter for equality constraint simulation."
|
||||
)
|
||||
rel mjc:target (
|
||||
doc = """DEPRECATED: Use newton:mimicJoint instead.
|
||||
|
||||
Secondary target of the equality constraint (the leader/reference joint)."""
|
||||
)
|
||||
}
|
||||
|
||||
class MjcTendon "MjcTendon" (
|
||||
|
||||
@@ -100,6 +100,7 @@ class "MjcSceneAPI"
|
||||
}
|
||||
doc = """API providing global simulation options for MuJoCo."""
|
||||
|
||||
prepend apiSchemas = ["NewtonSceneAPI"]
|
||||
inherits = </APISchemaBase>
|
||||
)
|
||||
{
|
||||
@@ -108,7 +109,9 @@ class "MjcSceneAPI"
|
||||
string apiName = "Timestep"
|
||||
}
|
||||
displayName = "Timestep"
|
||||
doc = """Controls the timestep in seconds used by MuJoCo."""
|
||||
doc = """DEPRECATED: Use newton:timeStepsPerSecond instead.
|
||||
|
||||
Controls the timestep in seconds used by MuJoCo."""
|
||||
)
|
||||
|
||||
uniform double mjc:option:impratio = 1.0 (
|
||||
@@ -229,7 +232,9 @@ class "MjcSceneAPI"
|
||||
string apiName = "Iterations"
|
||||
}
|
||||
displayName = "Solver Iterations"
|
||||
doc = """Maximum number of iterations of the constraint solver."""
|
||||
doc = """DEPRECATED: Use newton:maxSolverIterations instead.
|
||||
|
||||
Maximum number of iterations of the constraint solver."""
|
||||
)
|
||||
|
||||
uniform double mjc:option:tolerance = 1e-08 (
|
||||
@@ -378,7 +383,9 @@ class "MjcSceneAPI"
|
||||
string apiName = "GravityFlag"
|
||||
}
|
||||
displayName = "Gravity Toggle"
|
||||
doc = """Enables the application of gravitational acceleration as defined in mjOption."""
|
||||
doc = """DEPRECATED: Use newton:gravityEnabled instead.
|
||||
|
||||
Enables the application of gravitational acceleration as defined in mjOption."""
|
||||
)
|
||||
|
||||
uniform bool mjc:flag:clampctrl = True (
|
||||
@@ -674,6 +681,7 @@ class "MjcCollisionAPI"
|
||||
}
|
||||
doc = """API describing a MuJoCo collider."""
|
||||
|
||||
prepend apiSchemas = ["NewtonCollisionAPI"]
|
||||
inherits = </APISchemaBase>
|
||||
)
|
||||
{
|
||||
@@ -738,7 +746,9 @@ class "MjcCollisionAPI"
|
||||
string apiName = "Margin"
|
||||
}
|
||||
displayName = "Margin"
|
||||
doc = """Geometric inflation of the geom surface for the purpose of contact force generation."""
|
||||
doc = """DEPRECATED: Use newton:contactMargin and newton:contactGap instead.
|
||||
|
||||
Geometric inflation of the geom surface for the purpose of contact force generation."""
|
||||
)
|
||||
|
||||
uniform double mjc:gap = 0.0 (
|
||||
@@ -746,7 +756,9 @@ class "MjcCollisionAPI"
|
||||
string apiName = "Gap"
|
||||
}
|
||||
displayName = "Gap"
|
||||
doc = """Additional contact detection buffer beyond margin. Contacts are detected at distance margin + gap but forces are only generated at distance margin."""
|
||||
doc = """DEPRECATED: Use newton:contactGap instead.
|
||||
|
||||
Additional contact detection buffer beyond margin. Contacts are detected at distance margin + gap but forces are only generated at distance margin."""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -757,6 +769,7 @@ class "MjcMeshCollisionAPI"
|
||||
}
|
||||
doc = """API describing a MuJoCo mesh collider."""
|
||||
|
||||
prepend apiSchemas = ["NewtonMeshCollisionAPI"]
|
||||
inherits = </APISchemaBase>
|
||||
)
|
||||
{
|
||||
@@ -774,7 +787,9 @@ class "MjcMeshCollisionAPI"
|
||||
string apiName = "MaxHullVert"
|
||||
}
|
||||
displayName = "Maximum Hull Vertices"
|
||||
doc = """Sets an upper limit on the number of vertices in the meshes convex hull. The default value of -1 means unlimited."""
|
||||
doc = """DEPRECATED: Use newton:maxHullVertices instead.
|
||||
|
||||
Sets an upper limit on the number of vertices in the meshes convex hull. The default value of -1 means unlimited."""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1035,8 +1050,11 @@ class "MjcMaterialAPI"
|
||||
customData = {
|
||||
string className = "MaterialAPI"
|
||||
}
|
||||
doc = """API providing extension attributes to represent physical MuJoCo materials."""
|
||||
doc = """DEPRECATED: Use NewtonMaterialAPI instead. All attributes on this API have been superseded by Newton equivalents.
|
||||
|
||||
API providing extension attributes to represent physical MuJoCo materials."""
|
||||
|
||||
prepend apiSchemas = ["NewtonMaterialAPI"]
|
||||
inherits = </APISchemaBase>
|
||||
)
|
||||
{
|
||||
@@ -1045,7 +1063,9 @@ class "MjcMaterialAPI"
|
||||
string apiName = "TorsionalFriction"
|
||||
}
|
||||
displayName = "Torsional Friction"
|
||||
doc = """Friction value acting around contact normal."""
|
||||
doc = """DEPRECATED: Use newton:torsionalFriction instead.
|
||||
|
||||
Friction value acting around contact normal."""
|
||||
)
|
||||
|
||||
uniform double mjc:rollingfriction = 0.0001 (
|
||||
@@ -1053,7 +1073,9 @@ class "MjcMaterialAPI"
|
||||
string apiName = "RollingFriction"
|
||||
}
|
||||
displayName = "Rolling Friction"
|
||||
doc = """Friction value acting around both axes on the contact tangent plane."""
|
||||
doc = """DEPRECATED: Use newton:rollingFriction instead.
|
||||
|
||||
Friction value acting around both axes on the contact tangent plane."""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1121,22 +1143,58 @@ class "MjcEqualityJointAPI" (
|
||||
string className = "EqualityJointAPI"
|
||||
}
|
||||
doc = """API providing extension attributes to represent equality/joint constraints.
|
||||
This API is applied to a joint prim which acts as the constrained joint (joint1 in
|
||||
MuJoCo terminology). The target relationship points to another joint prim which is
|
||||
the reference joint (joint2 in MuJoCo terminology). The constrained joint's position
|
||||
or angle is constrained to be a quartic polynomial of the reference joint's position
|
||||
or angle. Only scalar joint types (slide and hinge) can be used."""
|
||||
|
||||
prepend apiSchemas = ["MjcEqualityAPI"]
|
||||
This API is applied to a joint prim which acts as the follower (joint0). The leader
|
||||
joint (joint1) is specified via the newton:mimicJoint relationship inherited from
|
||||
NewtonMimicAPI.
|
||||
|
||||
The follower's position or angle is constrained to be a quartic polynomial of the
|
||||
leader's position or angle:
|
||||
joint0 = coef0 + coef1*(joint1) + coef2*(joint1)^2 + coef3*(joint1)^3 + coef4*(joint1)^4
|
||||
|
||||
The constant (coef0) and linear (coef1) coefficients are provided by NewtonMimicAPI
|
||||
as newton:mimicCoef0 and newton:mimicCoef1. The higher-order coefficients (coef2-coef4)
|
||||
are provided by this API.
|
||||
|
||||
Only scalar joint types (slide and hinge) can be used."""
|
||||
|
||||
prepend apiSchemas = ["NewtonMimicAPI"]
|
||||
inherits = </APISchemaBase>
|
||||
)
|
||||
{
|
||||
uniform double[] mjc:solimp = [0.9, 0.95, 0.001, 0.5, 2] (
|
||||
customData = {
|
||||
string apiName = "SolImp"
|
||||
}
|
||||
displayName = "SolImp"
|
||||
doc = """Constraint solver parameter for equality constraint simulation."""
|
||||
)
|
||||
|
||||
uniform double[] mjc:solref = [0.02, 1] (
|
||||
customData = {
|
||||
string apiName = "SolRef"
|
||||
}
|
||||
displayName = "SolRef"
|
||||
doc = """Constraint solver parameter for equality constraint simulation."""
|
||||
)
|
||||
|
||||
rel mjc:target (
|
||||
customData = {
|
||||
string apiName = "MjcTarget"
|
||||
}
|
||||
doc = """DEPRECATED: Use newton:mimicJoint instead.
|
||||
|
||||
Secondary target of the equality constraint (the leader/reference joint)."""
|
||||
)
|
||||
|
||||
uniform double mjc:coef0 = 0 (
|
||||
customData = {
|
||||
string apiName = "Coef0"
|
||||
}
|
||||
displayName = "Coefficient 0"
|
||||
doc = """Constant coefficient a0 of the quartic polynomial. The constraint is:
|
||||
doc = """DEPRECATED: Use newton:mimicCoef0 instead.
|
||||
|
||||
Constant coefficient a0 of the quartic polynomial. The constraint is:
|
||||
y = y0 + a0 + a1*(x-x0) + a2*(x-x0)^2 + a3*(x-x0)^3 + a4*(x-x0)^4."""
|
||||
)
|
||||
|
||||
@@ -1145,7 +1203,9 @@ class "MjcEqualityJointAPI" (
|
||||
string apiName = "Coef1"
|
||||
}
|
||||
displayName = "Coefficient 1"
|
||||
doc = """Linear coefficient a1 of the quartic polynomial."""
|
||||
doc = """DEPRECATED: Use newton:mimicCoef1 instead.
|
||||
|
||||
Linear coefficient a1 of the quartic polynomial."""
|
||||
)
|
||||
|
||||
uniform double mjc:coef2 = 0 (
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "mjcf/mujoco_to_usd.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <numbers>
|
||||
#include <string>
|
||||
@@ -119,6 +120,16 @@ TF_DEFINE_PRIVATE_TOKENS(kTokens,
|
||||
(UsdPrimvarReader_float2)
|
||||
(UsdUVTexture)
|
||||
(UsdPreviewSurface)
|
||||
((NewtonMaterialAPI, "NewtonMaterialAPI"))
|
||||
((NewtonMeshCollisionAPI, "NewtonMeshCollisionAPI"))
|
||||
((newtonTorsionalFriction, "newton:torsionalFriction"))
|
||||
((newtonRollingFriction, "newton:rollingFriction"))
|
||||
((newtonMaxHullVertices, "newton:maxHullVertices"))
|
||||
((newtonMaxSolverIterations, "newton:maxSolverIterations"))
|
||||
((newtonTimeStepsPerSecond, "newton:timeStepsPerSecond"))
|
||||
((newtonGravityEnabled, "newton:gravityEnabled"))
|
||||
((newtonContactMargin, "newton:contactMargin"))
|
||||
((newtonContactGap, "newton:contactGap"))
|
||||
);
|
||||
|
||||
// Using to satisfy TF_REGISTRY_FUNCTION macro below and avoid operating in PXR_NS.
|
||||
@@ -393,8 +404,11 @@ class ModelWriter {
|
||||
WriteUniformAttribute(mesh_spec, pxr::SdfValueTypeNames->Token,
|
||||
MjcPhysicsTokens->mjcInertia, inertia);
|
||||
|
||||
WriteUniformAttribute(mesh_spec, pxr::SdfValueTypeNames->Int,
|
||||
MjcPhysicsTokens->mjcMaxhullvert, mesh->maxhullvert);
|
||||
// Newton mesh attribute (replaces deprecated mjc:maxhullvert)
|
||||
if (mesh->maxhullvert != -1) {
|
||||
WriteUniformAttribute(mesh_spec, pxr::SdfValueTypeNames->Int,
|
||||
kTokens->newtonMaxHullVertices, mesh->maxhullvert);
|
||||
}
|
||||
|
||||
// NOTE: The geometry data taken from the spec is the post-compilation
|
||||
// data after it has been mjCMesh::Compile'd. So don't be surprised if
|
||||
@@ -500,7 +514,7 @@ class ModelWriter {
|
||||
|
||||
const std::vector<std::pair<pxr::TfToken, double>>
|
||||
option_double_attributes = {
|
||||
{MjcPhysicsTokens->mjcOptionTimestep, spec_->option.timestep},
|
||||
// mjc:option:timestep deprecated in favor of newton:timeStepsPerSecond
|
||||
{MjcPhysicsTokens->mjcOptionTolerance, spec_->option.tolerance},
|
||||
{MjcPhysicsTokens->mjcOptionLs_tolerance,
|
||||
spec_->option.ls_tolerance},
|
||||
@@ -519,7 +533,7 @@ class ModelWriter {
|
||||
}
|
||||
|
||||
const std::vector<std::pair<pxr::TfToken, int>> option_int_attributes = {
|
||||
{MjcPhysicsTokens->mjcOptionIterations, spec_->option.iterations},
|
||||
// mjc:option:iterations deprecated in favor of newton:maxSolverIterations
|
||||
{MjcPhysicsTokens->mjcOptionLs_iterations, spec_->option.ls_iterations},
|
||||
{MjcPhysicsTokens->mjcOptionNoslip_iterations,
|
||||
spec_->option.noslip_iterations},
|
||||
@@ -665,7 +679,7 @@ class ModelWriter {
|
||||
{MjcPhysicsTokens->mjcFlagContact, mjDSBL_CONTACT},
|
||||
{MjcPhysicsTokens->mjcFlagSpring, mjDSBL_SPRING},
|
||||
{MjcPhysicsTokens->mjcFlagDamper, mjDSBL_DAMPER},
|
||||
{MjcPhysicsTokens->mjcFlagGravity, mjDSBL_GRAVITY},
|
||||
// mjc:flag:gravity deprecated in favor of newton:gravityEnabled
|
||||
{MjcPhysicsTokens->mjcFlagClampctrl, mjDSBL_CLAMPCTRL},
|
||||
{MjcPhysicsTokens->mjcFlagWarmstart, mjDSBL_WARMSTART},
|
||||
{MjcPhysicsTokens->mjcFlagFilterparent, mjDSBL_FILTERPARENT},
|
||||
@@ -747,6 +761,19 @@ class ModelWriter {
|
||||
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
|
||||
MjcPhysicsTokens->mjcCompilerSaveInertial,
|
||||
(bool)spec_->compiler.saveinertial);
|
||||
|
||||
// Newton scene attributes (auto-applied via MjcSceneAPI -> NewtonSceneAPI)
|
||||
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Int,
|
||||
kTokens->newtonMaxSolverIterations,
|
||||
spec_->option.iterations);
|
||||
if (spec_->option.timestep > 0) {
|
||||
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Int,
|
||||
kTokens->newtonTimeStepsPerSecond,
|
||||
static_cast<int>(std::round(1.0 / spec_->option.timestep)));
|
||||
}
|
||||
bool gravity_disabled = spec_->option.disableflags & mjDSBL_GRAVITY;
|
||||
WriteUniformAttribute(physics_scene_spec, pxr::SdfValueTypeNames->Bool,
|
||||
kTokens->newtonGravityEnabled, !gravity_disabled);
|
||||
}
|
||||
|
||||
void WriteMeshes() {
|
||||
@@ -868,16 +895,13 @@ class ModelWriter {
|
||||
pxr::UsdPhysicsTokens->physicsDynamicFriction,
|
||||
(float)geom->friction[0]);
|
||||
}
|
||||
if (geom->friction[1] != geom_default->friction[1]) {
|
||||
WriteUniformAttribute(material_spec, pxr::SdfValueTypeNames->Double,
|
||||
MjcPhysicsTokens->mjcTorsionalfriction,
|
||||
geom->friction[1]);
|
||||
}
|
||||
if (geom->friction[2] != geom_default->friction[2]) {
|
||||
WriteUniformAttribute(material_spec, pxr::SdfValueTypeNames->Double,
|
||||
MjcPhysicsTokens->mjcRollingfriction,
|
||||
geom->friction[2]);
|
||||
}
|
||||
// Newton material attributes (replaces deprecated mjc:torsionalfriction / mjc:rollingfriction)
|
||||
WriteUniformAttribute(material_spec, pxr::SdfValueTypeNames->Float,
|
||||
kTokens->newtonTorsionalFriction,
|
||||
(float)geom->friction[1]);
|
||||
WriteUniformAttribute(material_spec, pxr::SdfValueTypeNames->Float,
|
||||
kTokens->newtonRollingFriction,
|
||||
(float)geom->friction[2]);
|
||||
|
||||
return material_spec;
|
||||
}
|
||||
@@ -1706,11 +1730,13 @@ class ModelWriter {
|
||||
MjcPhysicsTokens->mjcSolimp,
|
||||
pxr::VtArray<double>(geom->solimp, geom->solimp + mjNIMP));
|
||||
|
||||
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Double,
|
||||
MjcPhysicsTokens->mjcMargin, geom->margin);
|
||||
|
||||
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Double,
|
||||
MjcPhysicsTokens->mjcGap, geom->gap);
|
||||
// Newton collision attributes (replaces deprecated mjc:margin / mjc:gap)
|
||||
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Float,
|
||||
kTokens->newtonContactMargin,
|
||||
static_cast<float>(geom->margin));
|
||||
WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Float,
|
||||
kTokens->newtonContactGap,
|
||||
static_cast<float>(geom->gap));
|
||||
|
||||
if (geom->mass >= mjMINVAL || geom->density >= mjMINVAL) {
|
||||
ApplyApiSchema(layer_, geom_spec, pxr::UsdPhysicsTokens->PhysicsMassAPI);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -233,13 +233,13 @@ TEST_F(CoreSmoothTest, TendonArmature) {
|
||||
|
||||
// get full M, includes both CRB and tendon inertia
|
||||
vector<mjtNum> M(nv*nv);
|
||||
mj_fullM(m, M.data(), d->qM);
|
||||
mj_fullM(m, d, M.data());
|
||||
|
||||
// put only CRB inertia in M2
|
||||
mj_crb(m, d);
|
||||
mju_scatter(d->qM, d->M, m->mapM2M, m->nC);
|
||||
vector<mjtNum> M2(nv*nv);
|
||||
mj_fullM(m, M2.data(), d->qM);
|
||||
mj_fullM(m, d, M2.data());
|
||||
|
||||
vector<mjtNum> ten_J(nv); // tendon Jacobian
|
||||
vector<mjtNum> ten_M(nv*nv); // tendon inertia
|
||||
@@ -681,7 +681,7 @@ TEST_F(CoreSmoothTest, FactorI) {
|
||||
|
||||
// dense M matrix
|
||||
vector<mjtNum> Mexpected(nv*nv);
|
||||
mj_fullM(model, Mexpected.data(), data->qM);
|
||||
mj_fullM(model, data, Mexpected.data());
|
||||
|
||||
// expect matrices to match to floating point precision
|
||||
EXPECT_THAT(M, Pointwise(MjNear(1e-12, 1e-5), Mexpected));
|
||||
@@ -690,6 +690,21 @@ TEST_F(CoreSmoothTest, FactorI) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
// Convert legacy-format symmetric matrix to dense (local helper for tests).
|
||||
static void legacyToDense(const mjModel* m, mjtNum* dst, const mjtNum* M) {
|
||||
int adr = 0, nv = m->nv;
|
||||
mju_zero(dst, nv*nv);
|
||||
for (int i = 0; i < nv; i++) {
|
||||
int j = i;
|
||||
while (j >= 0) {
|
||||
dst[i*nv+j] = M[adr];
|
||||
dst[j*nv+i] = M[adr];
|
||||
j = m->dof_parentid[j];
|
||||
adr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CoreSmoothTest, SolveLDs) {
|
||||
const std::string xml_path = GetTestDataFilePath(kInertiaPath);
|
||||
char error[1024];
|
||||
@@ -712,7 +727,7 @@ TEST_F(CoreSmoothTest, SolveLDs) {
|
||||
mju_sparse2dense(LDdense.data(), d->qLD, nv, nv,
|
||||
m->M_rownnz, m->M_rowadr, m->M_colind);
|
||||
vector<mjtNum> LDdense2(nv*nv);
|
||||
mj_fullM(m, LDdense2.data(), LDlegacy.data());
|
||||
legacyToDense(m, LDdense2.data(), LDlegacy.data());
|
||||
|
||||
// expect lower triangles to match exactly
|
||||
for (int i=0; i < nv; i++) {
|
||||
|
||||
@@ -848,7 +848,7 @@ TEST_F(DerivativeTest, LinearSystemInverse) {
|
||||
|
||||
// expect that acceleration derivatives are the mass matrix
|
||||
vector<mjtNum> DfDa_expect(nv*nv, 0);
|
||||
mj_fullM(model, DfDa_expect.data(), data->qM);
|
||||
mj_fullM(model, data, DfDa_expect.data());
|
||||
EXPECT_THAT(DfDa, Pointwise(DoubleNear(eps), DfDa_expect));
|
||||
|
||||
// expect that sensor derivatives w.r.t position only see sensor 1 at dof 0
|
||||
|
||||
@@ -608,13 +608,13 @@ TEST_F(InertiaTest, FullM) {
|
||||
ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error;
|
||||
int nv = m->nv;
|
||||
|
||||
// forward dynamics, populate qM and qLD
|
||||
// forward dynamics, populate M and qLD
|
||||
mjData* d = mj_makeData(m);
|
||||
mj_forward(m, d);
|
||||
|
||||
// get dense mass matrix from M using mju_sym2dense
|
||||
// get dense mass matrix from M using mj_fullM
|
||||
vector<mjtNum> M(nv * nv);
|
||||
mju_sym2dense(M.data(), d->M, nv, m->M_rownnz, m->M_rowadr, m->M_colind);
|
||||
mj_fullM(m, d, M.data());
|
||||
|
||||
// get dense mass matrix from M using mju_sparse2dense
|
||||
vector<mjtNum> M_CSR(nv * nv);
|
||||
|
||||
@@ -68,70 +68,6 @@ class MujocoErrorAndWarningTest : public ::testing::Test {
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MujocoErrorAndWarningTest, MjuErrorI) {
|
||||
std::string format_string = "%010d";
|
||||
while (format_string.length() < 2 * kBufferSize) {
|
||||
format_string += 'x';
|
||||
}
|
||||
|
||||
std::string expected_message = "0123456789";
|
||||
while (expected_message.length() < kBufferSize - 1) {
|
||||
expected_message += 'x';
|
||||
}
|
||||
|
||||
ClearErrorMessage();
|
||||
mju_error_i(format_string.c_str(), 123456789);
|
||||
EXPECT_EQ(std::string(ErrorMessageBuffer()), expected_message);
|
||||
}
|
||||
|
||||
TEST_F(MujocoErrorAndWarningTest, MjuWarningI) {
|
||||
std::string format_string = "%010d";
|
||||
while (format_string.length() < 2 * kBufferSize) {
|
||||
format_string += 'x';
|
||||
}
|
||||
|
||||
std::string expected_message = "0123456789";
|
||||
while (expected_message.length() < kBufferSize - 1) {
|
||||
expected_message += 'x';
|
||||
}
|
||||
|
||||
ClearWarningMessage();
|
||||
mju_warning_i(format_string.c_str(), 123456789);
|
||||
EXPECT_EQ(std::string(WarningMessageBuffer()), expected_message);
|
||||
}
|
||||
|
||||
TEST_F(MujocoErrorAndWarningTest, MjuErrorS) {
|
||||
std::string format_string = "% 9s";
|
||||
while (format_string.length() < 2 * kBufferSize) {
|
||||
format_string += 'z';
|
||||
}
|
||||
|
||||
std::string expected_message = " foobar";
|
||||
while (expected_message.length() < kBufferSize - 1) {
|
||||
expected_message += 'z';
|
||||
}
|
||||
|
||||
ClearErrorMessage();
|
||||
mju_error_s(format_string.c_str(), "foobar");
|
||||
EXPECT_EQ(std::string(ErrorMessageBuffer()), expected_message);
|
||||
}
|
||||
|
||||
TEST_F(MujocoErrorAndWarningTest, MjuWarningS) {
|
||||
std::string format_string = "% 9s";
|
||||
while (format_string.length() < 2 * kBufferSize) {
|
||||
format_string += 'z';
|
||||
}
|
||||
|
||||
std::string expected_message = " foobar";
|
||||
while (expected_message.length() < kBufferSize - 1) {
|
||||
expected_message += 'z';
|
||||
}
|
||||
|
||||
ClearWarningMessage();
|
||||
mju_warning_s(format_string.c_str(), "foobar");
|
||||
EXPECT_EQ(std::string(WarningMessageBuffer()), expected_message);
|
||||
}
|
||||
|
||||
TEST_F(MujocoErrorAndWarningTest, MjuErrorInternal) {
|
||||
ClearErrorMessage();
|
||||
mjERROR("foobar %d", 123);
|
||||
|
||||
@@ -78,6 +78,9 @@ PXR_NAMESPACE_OPEN_SCOPE
|
||||
// clang-format off
|
||||
TF_DEFINE_PRIVATE_TOKENS(_tokens,
|
||||
(st)
|
||||
((newtonTimeStepsPerSecond, "newton:timeStepsPerSecond"))
|
||||
((newtonMaxSolverIterations, "newton:maxSolverIterations"))
|
||||
((newtonGravityEnabled, "newton:gravityEnabled"))
|
||||
);
|
||||
// clang-format on
|
||||
PXR_NAMESPACE_CLOSE_SCOPE
|
||||
@@ -159,12 +162,20 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsMaterials) {
|
||||
4.0f);
|
||||
ExpectAttributeEqual(stage,
|
||||
"/physics_materials_test/PhysicsMaterials/"
|
||||
"geom_with_friction.mjc:torsionalfriction",
|
||||
5.0);
|
||||
"geom_with_friction.newton:torsionalFriction",
|
||||
5.0f);
|
||||
ExpectAttributeEqual(stage,
|
||||
"/physics_materials_test/PhysicsMaterials/"
|
||||
"geom_with_friction.mjc:rollingfriction",
|
||||
6.0);
|
||||
"geom_with_friction.newton:rollingFriction",
|
||||
6.0f);
|
||||
EXPECT_ATTRIBUTE_HAS_NO_AUTHORED_VALUE(
|
||||
stage,
|
||||
"/physics_materials_test/PhysicsMaterials/"
|
||||
"geom_with_friction.mjc:torsionalfriction");
|
||||
EXPECT_ATTRIBUTE_HAS_NO_AUTHORED_VALUE(
|
||||
stage,
|
||||
"/physics_materials_test/PhysicsMaterials/"
|
||||
"geom_with_friction.mjc:rollingfriction");
|
||||
}
|
||||
|
||||
TEST_F(MjcfSdfFileFormatPluginTest, TestMaterials) {
|
||||
@@ -692,10 +703,15 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimTimestep) {
|
||||
</mujoco>
|
||||
)");
|
||||
|
||||
// newton:timeStepsPerSecond = round(1/0.005) = 200
|
||||
ExpectAttributeEqual(
|
||||
stage,
|
||||
kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionTimestep),
|
||||
0.005);
|
||||
kPhysicsScenePrimPath.AppendProperty(pxr::_tokens->newtonTimeStepsPerSecond),
|
||||
200);
|
||||
// deprecated mjc:option:timestep should not be authored
|
||||
EXPECT_ATTRIBUTE_HAS_NO_AUTHORED_VALUE(
|
||||
stage,
|
||||
kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionTimestep));
|
||||
}
|
||||
|
||||
TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimCone) {
|
||||
@@ -928,8 +944,12 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimIterations) {
|
||||
|
||||
ExpectAttributeEqual(stage,
|
||||
kPhysicsScenePrimPath.AppendProperty(
|
||||
MjcPhysicsTokens->mjcOptionIterations),
|
||||
pxr::_tokens->newtonMaxSolverIterations),
|
||||
10);
|
||||
EXPECT_ATTRIBUTE_HAS_NO_AUTHORED_VALUE(
|
||||
stage,
|
||||
kPhysicsScenePrimPath.AppendProperty(
|
||||
MjcPhysicsTokens->mjcOptionIterations));
|
||||
}
|
||||
|
||||
TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimLSIterations) {
|
||||
@@ -1066,7 +1086,7 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimDisableFlags) {
|
||||
MjcPhysicsTokens->mjcFlagContact,
|
||||
MjcPhysicsTokens->mjcFlagSpring,
|
||||
MjcPhysicsTokens->mjcFlagDamper,
|
||||
MjcPhysicsTokens->mjcFlagGravity,
|
||||
// mjc:flag:gravity is deprecated, now exported as newton:gravityEnabled
|
||||
MjcPhysicsTokens->mjcFlagClampctrl,
|
||||
MjcPhysicsTokens->mjcFlagWarmstart,
|
||||
MjcPhysicsTokens->mjcFlagFilterparent,
|
||||
@@ -1083,6 +1103,10 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimDisableFlags) {
|
||||
ExpectAttributeEqual(stage, kPhysicsScenePrimPath.AppendProperty(flag),
|
||||
false);
|
||||
}
|
||||
ExpectAttributeEqual(
|
||||
stage,
|
||||
kPhysicsScenePrimPath.AppendProperty(pxr::_tokens->newtonGravityEnabled),
|
||||
false);
|
||||
}
|
||||
|
||||
TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimEnableFlags) {
|
||||
@@ -1472,8 +1496,11 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsCollisionAPI) {
|
||||
pxr::VtArray<double>({0.1, 0.2}));
|
||||
ExpectAttributeEqual(stage, "/test/body/box.mjc:solimp",
|
||||
pxr::VtArray<double>({0.3, 0.4, 0.5, 0.6, 0.7}));
|
||||
ExpectAttributeEqual(stage, "/test/body/box.mjc:margin", 0.8);
|
||||
ExpectAttributeEqual(stage, "/test/body/box.mjc:gap", 0.9);
|
||||
// margin and gap are now exported as Newton attributes
|
||||
EXPECT_ATTRIBUTE_HAS_NO_AUTHORED_VALUE(stage, "/test/body/box.mjc:margin");
|
||||
EXPECT_ATTRIBUTE_HAS_NO_AUTHORED_VALUE(stage, "/test/body/box.mjc:gap");
|
||||
ExpectAttributeEqual(stage, "/test/body/box.newton:contactMargin", 0.8f);
|
||||
ExpectAttributeEqual(stage, "/test/body/box.newton:contactGap", 0.9f);
|
||||
ExpectAttributeEqual(stage, "/test/body/box.mjc:shellinertia", true);
|
||||
}
|
||||
|
||||
@@ -1508,8 +1535,11 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsMeshCollisionAPI) {
|
||||
MjcPhysicsTokens->convex);
|
||||
ExpectAttributeEqual(stage, "/test/body/tet_shell/Mesh.mjc:inertia",
|
||||
MjcPhysicsTokens->shell);
|
||||
ExpectAttributeEqual(stage, "/test/body/tet_max_vert/Mesh.mjc:maxhullvert",
|
||||
12);
|
||||
// mjc:maxhullvert deprecated, newton:maxHullVertices used instead
|
||||
EXPECT_ATTRIBUTE_HAS_NO_AUTHORED_VALUE(
|
||||
stage, "/test/body/tet_max_vert/Mesh.mjc:maxhullvert");
|
||||
ExpectAttributeEqual(
|
||||
stage, "/test/body/tet_max_vert/Mesh.newton:maxHullVertices", 12);
|
||||
}
|
||||
|
||||
TEST_F(MjcfSdfFileFormatPluginTest, TestMassAPIApplied) {
|
||||
|
||||
@@ -6946,7 +6946,7 @@ public static unsafe extern int mj_name2id(mjModel_* m, int type, [MarshalAs(Unm
|
||||
public static unsafe extern IntPtr mj_id2name(mjModel_* m, int type, int id);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mj_fullM(mjModel_* m, double* dst, double* M);
|
||||
public static unsafe extern void mj_fullM(mjModel_* m, mjData_* d, double* dst);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mj_mulM(mjModel_* m, mjData_* d, double* res, double* vec);
|
||||
@@ -7224,21 +7224,9 @@ public static unsafe extern void mjui_render(mjUI_* ui, mjuiState_* state, mjrCo
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_error([MarshalAs(UnmanagedType.LPStr)]string msg);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_error_i([MarshalAs(UnmanagedType.LPStr)]string msg, int i);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_error_s([MarshalAs(UnmanagedType.LPStr)]string msg, [MarshalAs(UnmanagedType.LPStr)]string text);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_warning([MarshalAs(UnmanagedType.LPStr)]string msg);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_warning_i([MarshalAs(UnmanagedType.LPStr)]string msg, int i);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_warning_s([MarshalAs(UnmanagedType.LPStr)]string msg, [MarshalAs(UnmanagedType.LPStr)]string text);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_clearHandlers();
|
||||
|
||||
|
||||
@@ -8709,12 +8709,10 @@ void mj_forwardSkip_wrapper(const MjModel& m, MjData& d, int skipstage, int skip
|
||||
mj_forwardSkip(m.get(), d.get(), skipstage, skipsensor);
|
||||
}
|
||||
|
||||
void mj_fullM_wrapper(const MjModel& m, const val& dst, const NumberArray& M) {
|
||||
void mj_fullM_wrapper(const MjModel& m, const MjData& d, const val& dst) {
|
||||
UNPACK_VALUE(mjtNum, dst);
|
||||
UNPACK_ARRAY(mjtNum, M);
|
||||
CHECK_SIZE(M, m.nM());
|
||||
CHECK_SIZE(dst, m.nv() * m.nv());
|
||||
mj_fullM(m.get(), dst_.data(), M_.data());
|
||||
mj_fullM(m.get(), d.get(), dst_.data());
|
||||
}
|
||||
|
||||
void mj_fwdAcceleration_wrapper(const MjModel& m, MjData& d) {
|
||||
|
||||
@@ -147,14 +147,10 @@ _SKIPPED_MEMORY_FUNCTIONS: tuple[str, ...] = (
|
||||
"mju_boxQPmalloc",
|
||||
"mju_clearHandlers",
|
||||
"mju_error",
|
||||
"mju_error_i",
|
||||
"mju_error_s",
|
||||
"mju_free",
|
||||
"mju_malloc",
|
||||
"mju_strncpy",
|
||||
"mju_warning",
|
||||
"mju_warning_i",
|
||||
"mju_warning_s",
|
||||
# go/keep-sorted end
|
||||
)
|
||||
|
||||
@@ -569,7 +565,6 @@ FUNCTION_BOUNDS_CHECKS: Dict[str, str] = {
|
||||
CHECK_SIZE(qpos2, m.nq());
|
||||
""".strip(),
|
||||
"mj_fullM": """
|
||||
CHECK_SIZE(M, m.nM());
|
||||
CHECK_SIZE(dst, m.nv() * m.nv());
|
||||
""".strip(),
|
||||
"mj_geomDistance": """
|
||||
|
||||
Reference in New Issue
Block a user