From facebe5aa1b10ae8c0829d375c63b0764df0870e Mon Sep 17 00:00:00 2001 From: Andrew Kaufman Date: Wed, 4 Mar 2026 12:14:52 -0800 Subject: [PATCH 01/15] Build : Fetch newton-usd-schemas from GitHub --- src/experimental/usd/CMakeLists.txt | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/experimental/usd/CMakeLists.txt b/src/experimental/usd/CMakeLists.txt index c2763ef3..6db73264 100644 --- a/src/experimental/usd/CMakeLists.txt +++ b/src/experimental/usd/CMakeLists.txt @@ -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} From 792f53c8239ad87137e768be21290376221ace70 Mon Sep 17 00:00:00 2001 From: Andrew Kaufman Date: Wed, 4 Mar 2026 12:27:04 -0800 Subject: [PATCH 02/15] USD : Adopt newton-usd-schemas & deprecate MjcPhysics equivalents --- plugin/usd_decoder/usd_decoder.cc | 265 +++++++++++++++--- .../usd/mjcPhysics/generatedSchema.usda | 82 ++++-- src/experimental/usd/mjcPhysics/schema.usda | 94 +++++-- .../usd/plugins/mjcf/mujoco_to_usd.cc | 66 +++-- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 55 +++- 5 files changed, 457 insertions(+), 105 deletions(-) diff --git a/plugin/usd_decoder/usd_decoder.cc b/plugin/usd_decoder/usd_decoder.cc index 58a24590..87fdfe87 100644 --- a/plugin/usd_decoder/usd_decoder.cc +++ b/plugin/usd_decoder/usd_decoder.cc @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +83,27 @@ using pxr::MjcPhysicsTokens; using pxr::TfToken; +template +using TfStaticData = pxr::TfStaticData; + +// 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 +485,48 @@ void ParseUsdPhysicsScene(mjSpec* spec, SetGravityAttributes(spec, stage, gravity_direction, gravity_magnitude); - // Early exit if theres no MjcPhysicsSceneAPI applied. - if (!physics_scene.GetPrim().HasAPI()) { + // 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()) { 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 +641,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 +699,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 +985,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 + geom->gap; + } } } @@ -942,8 +1039,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 +1776,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 +1924,13 @@ void ParseUsdPhysicsCollider(mjSpec* spec, if (bound_material) { pxr::UsdPrim bound_material_prim = bound_material.GetPrim(); if (bound_material_prim.HasAPI() || - bound_material_prim.HasAPI()) { + bound_material_prim.HasAPI() || + 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 +1963,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()) { + if (mesh != nullptr && + (prim.HasAPI() || + prim.HasAPI(kNewtonTokens->NewtonMeshCollisionAPI))) { ParseMjcPhysicsMeshCollisionAPI(mesh, pxr::MjcPhysicsMeshCollisionAPI(prim)); } @@ -1877,8 +2016,8 @@ void ParseMjcEqualityAPISolverParams( void ParseConstraint(mjSpec* spec, const pxr::UsdPrim& prim, mjsBody* body, pxr::UsdGeomXformCache& xform_cache) { - if (prim.HasAPI()) { - // Handle MjcPhysicsEqualityJointAPI on revolute/prismatic joints. + if (prim.HasAPI() || + prim.HasAPI(kNewtonTokens->NewtonMimicAPI)) { pxr::MjcPhysicsEqualityJointAPI eq_joint_api(prim); mjsEquality* eq = mjs_addEquality(spec, nullptr); eq->type = mjEQ_JOINT; @@ -1889,20 +2028,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 +2077,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() || prim.IsA()) { // Handle fixed joints as weld constraints, spherical joints as connect constraints diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index 633ec10d..dec480b0 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -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 = "This attribute is used to enable the generation of inactive contacts, i.e., contacts that are ignored by the constraint solver but are included in mjData.contact for the purpose of custom computations. When this value is positive, geom distances between margin and margin-gap correspond to such inactive contacts." + doc = """DEPRECATED: Use newton:contactGap instead. + + This attribute is used to enable the generation of inactive contacts, i.e., contacts that are ignored by the constraint solver but are included in mjData.contact for the purpose of custom computations. When this value is positive, geom distances between margin and margin-gap correspond to such inactive contacts.""" ) uniform int mjc:group = 0 ( displayName = "Group" @@ -320,7 +330,9 @@ class "MjcCollisionAPI" ( ) uniform double mjc:margin = 0 ( displayName = "Margin" - doc = "Distance threshold below which contacts are detected and included in the global array mjData.contact." + doc = """DEPRECATED: Use newton:contactMargin and newton:contactGap instead. + + Distance threshold below which contacts are detected and included in the global array mjData.contact.""" ) 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" ( diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index 62c1d993..dd705a6e 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -100,6 +100,7 @@ class "MjcSceneAPI" } doc = """API providing global simulation options for MuJoCo.""" + prepend apiSchemas = ["NewtonSceneAPI"] inherits = ) { @@ -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 = ) { @@ -738,7 +746,9 @@ class "MjcCollisionAPI" string apiName = "Margin" } displayName = "Margin" - doc = """Distance threshold below which contacts are detected and included in the global array mjData.contact.""" + doc = """DEPRECATED: Use newton:contactMargin and newton:contactGap instead. + + Distance threshold below which contacts are detected and included in the global array mjData.contact.""" ) uniform double mjc:gap = 0.0 ( @@ -746,7 +756,9 @@ class "MjcCollisionAPI" string apiName = "Gap" } displayName = "Gap" - doc = """This attribute is used to enable the generation of inactive contacts, i.e., contacts that are ignored by the constraint solver but are included in mjData.contact for the purpose of custom computations. When this value is positive, geom distances between margin and margin-gap correspond to such inactive contacts.""" + doc = """DEPRECATED: Use newton:contactGap instead. + + This attribute is used to enable the generation of inactive contacts, i.e., contacts that are ignored by the constraint solver but are included in mjData.contact for the purpose of custom computations. When this value is positive, geom distances between margin and margin-gap correspond to such inactive contacts.""" ) } @@ -757,6 +769,7 @@ class "MjcMeshCollisionAPI" } doc = """API describing a MuJoCo mesh collider.""" + prepend apiSchemas = ["NewtonMeshCollisionAPI"] inherits = ) { @@ -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 = ) { @@ -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 = ) { + 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 ( diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 2820a3aa..71612246 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -15,6 +15,7 @@ #include "mjcf/mujoco_to_usd.h" #include +#include #include #include #include @@ -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> 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> 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}, @@ -666,7 +680,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(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(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(geom->margin - geom->gap)); + WriteUniformAttribute(geom_spec, pxr::SdfValueTypeNames->Float, + kTokens->newtonContactGap, + static_cast(geom->gap)); if (geom->mass >= mjMINVAL || geom->density >= mjMINVAL) { ApplyApiSchema(layer_, geom_spec, pxr::UsdPhysicsTokens->PhysicsMassAPI); diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index b74ebcff..d77ae851 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -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) { )"); + // 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,12 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsCollisionAPI) { pxr::VtArray({0.1, 0.2})); ExpectAttributeEqual(stage, "/test/body/box.mjc:solimp", pxr::VtArray({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"); + // newton:contactMargin = margin - gap = 0.8 - 0.9 = -0.1 + ExpectAttributeEqual(stage, "/test/body/box.newton:contactMargin", -0.1f); + ExpectAttributeEqual(stage, "/test/body/box.newton:contactGap", 0.9f); ExpectAttributeEqual(stage, "/test/body/box.mjc:shellinertia", true); } @@ -1508,8 +1536,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) { From ad23db5942e0bae416730ab221abc8e639a4014b Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Mon, 1 Jun 2026 12:16:59 -0700 Subject: [PATCH 03/15] Guard againsts NaNs in engine_collision_gjk_test.cc. PiperOrigin-RevId: 924844839 Change-Id: I98d5acd8ee97881ebd17f7ba7a40fb378ca1fb2d --- test/engine/engine_collision_gjk_test.cc | 763 +++++++++-------------- 1 file changed, 279 insertions(+), 484 deletions(-) diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index f46d0c0f..71eca6b1 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -17,6 +17,9 @@ #include "src/engine/engine_collision_gjk.h" #include +#include +#include +#include #include #include // IWYU pragma: keep @@ -35,11 +38,13 @@ namespace mujoco { namespace { -using ::testing::NotNull; using ::testing::ElementsAre; using ::testing::Pointwise; using ::testing::DoubleNear; +using TestModel = std::unique_ptr; +using TestData = std::unique_ptr; + constexpr mjtNum kTolerance = 1e-6; constexpr int kMaxIterations = 1000; constexpr char kEllipsoidXml[] = R"( @@ -61,8 +66,19 @@ constexpr char kEllipsoidXml[] = R"( )"; -mjtNum GeomDist(mjModel* m, mjData* d, int g1, int g2, mjtNum x1[3], - mjtNum x2[3], mjtNum cutoff = mjMAX_LIMIT) { +TestModel LoadModel(std::string_view xml) { + char error[1024]; + mjModel* model = LoadModelFromString(xml, error, sizeof(error)); + EXPECT_NE(model, nullptr) << "Failed to load model: " << error; + return TestModel(model, mj_deleteModel); +} + +TestData MakeData(const mjModel* model) { + return TestData(mj_makeData(model), mj_deleteData); +} + +mjtNum GeomDist(const TestModel& m, const TestData& d, int g1, int g2, + mjtNum x1[3], mjtNum x2[3], mjtNum cutoff = mjMAX_LIMIT) { mjCCDConfig config; mjCCDStatus status; @@ -74,8 +90,8 @@ mjtNum GeomDist(mjModel* m, mjData* d, int g1, int g2, mjtNum x1[3], config.buffer = nullptr; mjCCDObj obj1, obj2; - mjc_initCCDObj(&obj1, m, d, g1, 0); - mjc_initCCDObj(&obj2, m, d, g2, 0); + mjc_initCCDObj(&obj1, m.get(), d.get(), g1, 0); + mjc_initCCDObj(&obj2, m.get(), d.get(), g2, 0); mjtNum dist = mjc_ccd(&config, &status, &obj1, &obj2); if (status.nx > 0) { @@ -86,11 +102,12 @@ mjtNum GeomDist(mjModel* m, mjData* d, int g1, int g2, mjtNum x1[3], } int Penetration(mjCCDStatus& status, mjtNum& depth, std::vector& dir, - std::vector& pos, mjModel* model, mjData* data, - int g1, int g2, mjtNum margin = 0, int max_contacts = 1) { + std::vector& pos, const TestModel& model, + const TestData& data, int g1, int g2, mjtNum margin = 0, + int max_contacts = 1) { mjCCDObj obj1, obj2; - mjc_initCCDObj(&obj1, model, data, g1, margin); - mjc_initCCDObj(&obj2, model, data, g2, margin); + mjc_initCCDObj(&obj1, model.get(), data.get(), g1, margin); + mjc_initCCDObj(&obj2, model.get(), data.get(), g2, margin); #if defined(TEST_WITH_LIBCCD) if (max_contacts == 1) { @@ -163,23 +180,18 @@ TEST_F(MjGjkTest, SphereSphereDist) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjtNum x1[3], x2[3]; mjtNum dist = GeomDist(model, data, geom1, geom2, x1, x2); EXPECT_EQ(dist, 1); EXPECT_THAT(x1, ElementsAre(-.5, 0, 0)); EXPECT_THAT(x2, ElementsAre(.5, 0, 0)); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, SphereSphereDistCutoff) { @@ -191,20 +203,15 @@ TEST_F(MjGjkTest, SphereSphereDistCutoff) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjtNum dist = GeomDist(model, data, geom1, geom2, nullptr, nullptr, .999999); EXPECT_EQ(dist, mjMAX_LIMIT); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, SphereSphereNoDist) { @@ -216,24 +223,19 @@ TEST_F(MjGjkTest, SphereSphereNoDist) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - EXPECT_EQ(ncons, 0); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 0); } TEST_F(MjGjkTest, SphereSphereIntersect) { @@ -245,22 +247,19 @@ TEST_F(MjGjkTest, SphereSphereIntersect) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - EXPECT_EQ(ncons, 1); + ASSERT_EQ(ncons, 1); // penetration depth EXPECT_NEAR(dist, -2, kTolerance); @@ -274,9 +273,6 @@ TEST_F(MjGjkTest, SphereSphereIntersect) { EXPECT_NEAR(pos[0], 1, kTolerance); EXPECT_NEAR(pos[1], 0, kTolerance); EXPECT_NEAR(pos[2], 0, kTolerance); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxDepth) { @@ -288,30 +284,24 @@ TEST_F(MjGjkTest, BoxBoxDepth) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - EXPECT_EQ(ncons, 1); + ASSERT_EQ(ncons, 1); EXPECT_NEAR(dist, -1, kTolerance); EXPECT_NEAR(dir[0], 1, kTolerance); EXPECT_NEAR(dir[1], 0, kTolerance); EXPECT_NEAR(dir[2], 0, kTolerance); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxDepth2) { @@ -323,12 +313,9 @@ TEST_F(MjGjkTest, BoxBoxDepth2) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat + 9; mjtNum* xpos = data->geom_xpos + 3; @@ -347,23 +334,19 @@ TEST_F(MjGjkTest, BoxBoxDepth2) { xmat[7] = 0.000260616790777321797722282382; xmat[8] = 0.999999932078886044628518448008; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - if (ncons == 1) { - EXPECT_NEAR(dist, -0.033401579411886845, kTolerance); - EXPECT_NEAR(dir[0], 0, kTolerance); - EXPECT_NEAR(dir[1], 0, kTolerance); - EXPECT_NEAR(dir[2], 1, kTolerance); - } - - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 1); + EXPECT_NEAR(dist, -0.033401579411886845, kTolerance); + EXPECT_NEAR(dir[0], 0, kTolerance); + EXPECT_NEAR(dir[1], 0, kTolerance); + EXPECT_NEAR(dir[2], 1, kTolerance); } TEST_F(MjGjkTest, BoxBoxDepth3) { @@ -375,12 +358,9 @@ TEST_F(MjGjkTest, BoxBoxDepth3) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -416,22 +396,19 @@ TEST_F(MjGjkTest, BoxBoxDepth3) { xpos[1] = -0.023505499999999998617106200527; xpos[2] = -4.659230360891631228525966434972; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - EXPECT_EQ(ncons, 1); + ASSERT_EQ(ncons, 1); EXPECT_NEAR(dist, -0.003066, kTolerance); EXPECT_NEAR(dir[0], 0, kTolerance); EXPECT_NEAR(dir[1], 0, kTolerance); EXPECT_NEAR(dir[2], -1, kTolerance); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxTouching) { @@ -443,26 +420,21 @@ TEST_F(MjGjkTest, BoxBoxTouching) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - EXPECT_EQ(ncons, 0); + ASSERT_EQ(ncons, 0); EXPECT_EQ(status.epa_status, -1); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD) { @@ -474,22 +446,19 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); EXPECT_NEAR(dist, -.1, kTolerance); EXPECT_NEAR(dir[0], 0, kTolerance); @@ -500,9 +469,6 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD) { 1.0, 1.0, 0.95, 1.0, -1.0, 0.95, -1.0, -1.0, 0.95})); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD2) { @@ -514,22 +480,19 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD2) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); EXPECT_NEAR(dist, -.1, kTolerance); EXPECT_NEAR(dir[0], 0, kTolerance); @@ -540,9 +503,6 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD2) { 10.0, 10.0, 0.95, 10.0, 8.5, 0.95, 8.5, 8.5, 0.95})); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD3) { @@ -554,12 +514,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD3) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat + 9; mjtNum* xpos = data->geom_xpos + 3; @@ -579,17 +536,15 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD3) { xpos[2] = 1.095456702630382306296041861060; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 4); } TEST_F(MjGjkTest, BoxBoxMultiCCD4) { @@ -601,12 +556,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD4) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -642,22 +594,20 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD4) { xpos[1] = -0.023500601273213628239489025873; xpos[2] = -4.958782854594746325460619118530; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 8); - EXPECT_NEAR(dist, -0.00060425119242707459, kTolerance); + ASSERT_EQ(ncons, 8); + EXPECT_NEAR(dist, -0.00060425119242707459, kTolerance); - EXPECT_NEAR(dir[0], 0, kTolerance); - EXPECT_NEAR(dir[1], 0, kTolerance); - EXPECT_NEAR(dir[2], -1, kTolerance); - mj_deleteData(data); - mj_deleteModel(model); + EXPECT_NEAR(dir[0], 0, kTolerance); + EXPECT_NEAR(dir[1], 0, kTolerance); + EXPECT_NEAR(dir[2], -1, kTolerance); } TEST_F(MjGjkTest, BoxBoxMultiCCD5) { @@ -669,12 +619,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD5) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -711,22 +658,20 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD5) { xpos[2] = -4.659108354876987156956147373421; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 8); + ASSERT_EQ(ncons, 8); EXPECT_NEAR(dist, -0.0001077858631973211, kTolerance); EXPECT_NEAR(dir[0], 0.00019065, kTolerance); EXPECT_NEAR(dir[1], -8.6494189274575805e-05, kTolerance); EXPECT_NEAR(dir[2], -1, kTolerance); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD6) { @@ -738,12 +683,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD6) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat + 9; mjtNum* xpos = data->geom_xpos + 3; @@ -762,22 +704,20 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD6) { xpos[1] = 0.190777715293135141649827346555; xpos[2] = 0.100006658017411736993906856696; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 5); + ASSERT_EQ(ncons, 5); EXPECT_NEAR(dist, -0.00009843, kTolerance); EXPECT_NEAR(dir[0], -0.0008879306751646528, kTolerance); EXPECT_NEAR(dir[1], -0.00046014397575771832, kTolerance); EXPECT_NEAR(dir[2], 1, kTolerance); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD7) { @@ -789,12 +729,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD7) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -831,17 +768,15 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD7) { xpos[2] = -4.958375812037025376355359185254; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 8); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 8); } TEST_F(MjGjkTest, BoxBoxMultiCCD8) { @@ -853,12 +788,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD8) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -894,17 +826,15 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD8) { xpos[1] = -0.023505499999999998617106200527; xpos[2] = -4.958574289672835533338002278470; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 4); } TEST_F(MjGjkTest, BoxBoxMultiCCD9) { @@ -916,12 +846,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD9) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -958,17 +885,15 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD9) { xpos[2] = 0.2156259187793853615566774806211469694972; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 4); } TEST_F(MjGjkTest, BoxBoxMultiCCD10) { @@ -980,12 +905,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD10) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xpos = data->geom_xpos; @@ -999,18 +921,16 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD10) { xpos[1] = -0.0765140000000000264357424839545274153352; xpos[2] = 0.1751399999999999623767621415026951581240; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD11) { @@ -1022,12 +942,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD11) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xpos = data->geom_xpos; mjtNum* xmat = data->geom_xmat; @@ -1065,18 +982,16 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD11) { xpos[2] = 0.1745248497897437800485676007156143896282; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD12) { @@ -1088,12 +1003,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD12) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xpos = data->geom_xpos; mjtNum* xmat = data->geom_xmat; @@ -1129,18 +1041,16 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD12) { xpos[1] = -0.0764300000000000256950016819246229715645; xpos[2] = 0.1748374248948718623353215662064030766487; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD13) { @@ -1152,12 +1062,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD13) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xpos = data->geom_xpos; mjtNum* xmat = data->geom_xmat; @@ -1193,22 +1100,19 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD13) { xpos[1] = -0.2000000000000000111022302462515654042363; xpos[2] = -0.0418396695286432432348000531874276930466; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); EXPECT_NEAR(dir[0], 0, kTolerance); EXPECT_NEAR(dir[1], 0, kTolerance); EXPECT_NEAR(dir[2], 1, kTolerance); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBoxMultiCCD14) { @@ -1220,12 +1124,9 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD14) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xpos = data->geom_xpos; mjtNum* xmat = data->geom_xmat; @@ -1261,18 +1162,16 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD14) { xpos[1] = -0.0000051338999751368759734112059978095033; xpos[2] = -0.0400059009625639144802633495601185131818; - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, SmallBoxMesh) { @@ -1305,22 +1204,19 @@ TEST_F(MjGjkTest, SmallBoxMesh) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - EXPECT_EQ(ncons, 1); + ASSERT_EQ(ncons, 1); EXPECT_NEAR(dist, 0, kTolerance); // direction @@ -1332,9 +1228,6 @@ TEST_F(MjGjkTest, SmallBoxMesh) { EXPECT_NEAR(pos[0], 0, kTolerance); EXPECT_NEAR(pos[1], 0, kTolerance); EXPECT_NEAR(pos[2], 0, kTolerance); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxMesh) { static constexpr char xml[] = R"( @@ -1351,24 +1244,19 @@ TEST_F(MjGjkTest, BoxMesh) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g2, g1, 0, 1000); EXPECT_EQ(model->nmeshpoly, 7); - EXPECT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 4); } TEST_F(MjGjkTest, BoxMesh2) { @@ -1386,24 +1274,19 @@ TEST_F(MjGjkTest, BoxMesh2) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g2, g1, 0, 1000); - EXPECT_EQ(ncons, 5); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 5); } TEST_F(MjGjkTest, BoxMeshPrune) { @@ -1421,24 +1304,19 @@ TEST_F(MjGjkTest, BoxMeshPrune) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g2, g1, 0, 4); - EXPECT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 4); } TEST_F(MjGjkTest, MeshMesh) { @@ -1458,24 +1336,19 @@ TEST_F(MjGjkTest, MeshMesh) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 5); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 5); } TEST_F(MjGjkTest, MeshMeshPrune) { @@ -1495,24 +1368,19 @@ TEST_F(MjGjkTest, MeshMeshPrune) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); - EXPECT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 4); } TEST_F(MjGjkTest, BoxEdge) { @@ -1524,24 +1392,19 @@ TEST_F(MjGjkTest, BoxEdge) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "box1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "box1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "box2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); - EXPECT_EQ(ncons, 2); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 2); } TEST_F(MjGjkTest, BoxEdge2) { @@ -1553,12 +1416,9 @@ TEST_F(MjGjkTest, BoxEdge2) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -1594,17 +1454,15 @@ TEST_F(MjGjkTest, BoxEdge2) { xpos[1] = 0.9828851949225971829093850828940048813820; xpos[2] = 3.0930077345364814789263618877157568931580; - int g1 = mj_name2id(model, mjOBJ_GEOM, "box1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "box1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "box2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); - EXPECT_EQ(ncons, 2); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 2); } TEST_F(MjGjkTest, BoxEdgeEdge) { @@ -1616,12 +1474,9 @@ TEST_F(MjGjkTest, BoxEdgeEdge) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -1657,17 +1512,15 @@ TEST_F(MjGjkTest, BoxEdgeEdge) { xpos[1] = -0.0000000000000000008679606505055748997840; xpos[2] = 2.8141526153588731773425024584867060184479; - int g1 = mj_name2id(model, mjOBJ_GEOM, "box1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "box1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "box2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); - EXPECT_EQ(ncons, 2); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 2); } TEST_F(MjGjkTest, MeshEdge) { @@ -1685,24 +1538,19 @@ TEST_F(MjGjkTest, MeshEdge) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "box1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "box1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "box2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); - EXPECT_EQ(ncons, 2); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 2); } TEST_F(MjGjkTest, MeshEdge2) { @@ -1730,47 +1578,37 @@ TEST_F(MjGjkTest, MeshEdge2) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "floor"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "meshbox"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "floor"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "meshbox"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); - EXPECT_EQ(ncons, 2); - mj_deleteData(data); - mj_deleteModel(model); + ASSERT_EQ(ncons, 2); } TEST_F(MjGjkTest, EllipsoidEllipsoidPenetrating) { - char error[1024]; - mjModel* model = LoadModelFromString(kEllipsoidXml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(kEllipsoidXml); + TestData data = MakeData(model.get()); + mj_resetDataKeyframe(model.get(), data.get(), 0); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_resetDataKeyframe(model, data, 0); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); - EXPECT_EQ(ncons, 1); + ASSERT_EQ(ncons, 1); EXPECT_NEAR(dist, -0.00022548856248122027, kTolerance); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, EllipsoidEllipsoid) { @@ -1782,20 +1620,15 @@ TEST_F(MjGjkTest, EllipsoidEllipsoid) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjtNum dist = GeomDist(model, data, geom1, geom2, nullptr, nullptr); EXPECT_NEAR(dist, 0.7542, .0001); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, EllipsoidEllipsoidSlowConvergence) { @@ -1810,12 +1643,9 @@ TEST_F(MjGjkTest, EllipsoidEllipsoidSlowConvergence) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); mjtNum* xmat = data->geom_xmat; mjtNum* xpos = data->geom_xpos; @@ -1851,8 +1681,8 @@ TEST_F(MjGjkTest, EllipsoidEllipsoidSlowConvergence) { xpos[1] = 0.00961542646741688108; xpos[2] = 0.29832742817753182818; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; @@ -1861,8 +1691,6 @@ TEST_F(MjGjkTest, EllipsoidEllipsoidSlowConvergence) { EXPECT_LT(dist, 0.0); EXPECT_NEAR(dist, 0.0, kTolerance); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxBox) { @@ -1874,20 +1702,15 @@ TEST_F(MjGjkTest, BoxBox) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjtNum dist = GeomDist(model, data, geom1, geom2, nullptr, nullptr); EXPECT_EQ(dist, 1); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, LongBox) { @@ -1904,22 +1727,19 @@ static constexpr char xml[] = R"( )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2); - EXPECT_EQ(ncons, 1); + ASSERT_EQ(ncons, 1); EXPECT_NEAR(dist, -0.01, kTolerance); EXPECT_NEAR(dir[0], 0, kTolerance); @@ -1932,10 +1752,8 @@ static constexpr char xml[] = R"( // multicontact ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 4); + ASSERT_EQ(ncons, 4); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, EllipsoidEllipsoidIntersect) { @@ -1947,25 +1765,20 @@ TEST_F(MjGjkTest, EllipsoidEllipsoidIntersect) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 15); - EXPECT_EQ(ncons, 1); + ASSERT_EQ(ncons, 1); EXPECT_NEAR(dist, -14.245732934582151, kTolerance); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, CapsuleCapsule) { @@ -1977,20 +1790,15 @@ TEST_F(MjGjkTest, CapsuleCapsule) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjtNum dist = GeomDist(model, data, geom1, geom2, nullptr, nullptr); EXPECT_NEAR(dist, 0.4711, .0001); - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, CylinderBoxMargin) { @@ -2012,20 +1820,14 @@ TEST_F(MjGjkTest, CylinderBoxMargin) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; - - mjData* data = mj_makeData(model); - mj_forward(model, data); + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); // margin=0.1 means forces generated when dist<0.1 // the contact at dist~0.015 is within margin, so forces are generated EXPECT_EQ(data->ncon, 1); EXPECT_GE(data->contact[0].efc_address, 0); - - mj_deleteData(data); - mj_deleteModel(model); } TEST_F(MjGjkTest, BoxEdgeFlipped) { @@ -2038,32 +1840,25 @@ TEST_F(MjGjkTest, BoxEdgeFlipped) { )"; - char error[1024]; - mjModel* model = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error; + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); - mjData* data = mj_makeData(model); - mj_forward(model, data); - - int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); - EXPECT_EQ(ncons, 2); - + ASSERT_EQ(ncons, 2); EXPECT_NEAR(status.x1[0], 1.907368, kTolerance); EXPECT_NEAR(status.x1[1], -0.052973, kTolerance); EXPECT_NEAR(status.x1[2], 0.700000, kTolerance); EXPECT_NEAR(status.x2[0], 1.30000, kTolerance); EXPECT_NEAR(status.x2[1], -0.052973, kTolerance); EXPECT_NEAR(status.x2[2], 0.700000, kTolerance); - - mj_deleteData(data); - mj_deleteModel(model); } } // namespace From 062b0f1ea69fa7df8eb77124a6adfb2301c82138 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 2 Jun 2026 01:58:19 -0700 Subject: [PATCH 04/15] Remove deprecated `mju_{error,warning}_{i,s}` functions. PiperOrigin-RevId: 925204136 Change-Id: Ia877d08a135092db8037d04e6a237e81325a1d7c --- doc/APIreference/functions.rst | 36 ------------ doc/changelog.rst | 8 +-- doc/includes/references.h | 4 -- include/mujoco/mujoco.h | 12 ---- python/mujoco/introspect/functions.py | 76 -------------------------- src/engine/engine_util_errmem.c | 24 -------- src/engine/engine_util_errmem.h | 4 -- test/engine/engine_util_errmem_test.cc | 64 ---------------------- unity/Runtime/Bindings/MjBindings.cs | 12 ---- wasm/codegen/generators/constants.py | 4 -- 10 files changed, 3 insertions(+), 241 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 7a409cc2..10dd03da 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -1980,24 +1980,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 +1989,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>`__ diff --git a/doc/changelog.rst b/doc/changelog.rst index 2eaddeb0..4ef058ea 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -21,16 +21,14 @@ 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` 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. Bug fixes ^^^^^^^^^ @@ -2145,7 +2143,7 @@ General `__ 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%. diff --git a/doc/includes/references.h b/doc/includes/references.h index 32828541..520785a7 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -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); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 1af40764..10c0a6e6 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -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); diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 5deefe3c..5fc5cf92 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -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', diff --git a/src/engine/engine_util_errmem.c b/src/engine/engine_util_errmem.c index f44e2a24..07a6aed0 100644 --- a/src/engine/engine_util_errmem.c +++ b/src/engine/engine_util_errmem.c @@ -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 diff --git a/src/engine/engine_util_errmem.h b/src/engine/engine_util_errmem.h index 10623c93..a1b791d2 100644 --- a/src/engine/engine_util_errmem.h +++ b/src/engine/engine_util_errmem.h @@ -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); diff --git a/test/engine/engine_util_errmem_test.cc b/test/engine/engine_util_errmem_test.cc index 1b398f5d..41754952 100644 --- a/test/engine/engine_util_errmem_test.cc +++ b/test/engine/engine_util_errmem_test.cc @@ -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); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 69877044..49b99f3c 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -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(); diff --git a/wasm/codegen/generators/constants.py b/wasm/codegen/generators/constants.py index 9a20444d..7a826e09 100644 --- a/wasm/codegen/generators/constants.py +++ b/wasm/codegen/generators/constants.py @@ -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 ) From 4cf4a5665d5fce57a77217ecc88f1410c77f3da6 Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Tue, 2 Jun 2026 05:31:48 -0700 Subject: [PATCH 05/15] Configure Copybara export for Dear ImGui and ImPlot Python bindings Following the export declarations in Dear ImGui and ImPlot METADATA, this change updates MuJoCo's Copybara configuration (copy.bara.sky) to export and transform the Python bindings. `//third_party/dear_imgui/google/py` exports to `python/mujoco/experimental/dear_imgui` and `//third_party/implot/google/py` exports to `python/mujoco/experimental/implot`. PiperOrigin-RevId: 925293624 Change-Id: Ie6e32d247a6f7fc24bb36ae7060f2075d8efeb26 --- .../experimental/dear_imgui/dear_imgui.cc | 1233 +++++++++++++++++ .../dear_imgui/dear_imgui_macros.h | 326 +++++ python/mujoco/experimental/implot/implot.cc | 438 ++++++ .../experimental/studio/native_viewer.cc | 189 +++ .../experimental/studio/native_viewer.py | 171 +++ python/mujoco/experimental/studio/parser.cc | 46 + python/mujoco/experimental/studio/renderer.cc | 78 ++ .../experimental/studio/sample/async.py | 231 +++ .../experimental/studio/sample/implot.py | 199 +++ .../experimental/studio/sample/render.py | 73 + python/mujoco/experimental/studio/sim.cc | 65 + python/mujoco/experimental/studio/studio.py | 50 + .../mujoco/experimental/studio/studio_app.py | 440 ++++++ .../experimental/studio/studio_app_events.py | 592 ++++++++ python/mujoco/experimental/studio/ux.cc | 398 ++++++ .../experimental/studio/viewer_protocol.py | 43 + 16 files changed, 4572 insertions(+) create mode 100644 python/mujoco/experimental/dear_imgui/dear_imgui.cc create mode 100644 python/mujoco/experimental/dear_imgui/dear_imgui_macros.h create mode 100644 python/mujoco/experimental/implot/implot.cc create mode 100644 python/mujoco/experimental/studio/native_viewer.cc create mode 100644 python/mujoco/experimental/studio/native_viewer.py create mode 100644 python/mujoco/experimental/studio/parser.cc create mode 100644 python/mujoco/experimental/studio/renderer.cc create mode 100644 python/mujoco/experimental/studio/sample/async.py create mode 100644 python/mujoco/experimental/studio/sample/implot.py create mode 100644 python/mujoco/experimental/studio/sample/render.py create mode 100644 python/mujoco/experimental/studio/sim.cc create mode 100644 python/mujoco/experimental/studio/studio.py create mode 100644 python/mujoco/experimental/studio/studio_app.py create mode 100644 python/mujoco/experimental/studio/studio_app_events.py create mode 100644 python/mujoco/experimental/studio/ux.cc create mode 100644 python/mujoco/experimental/studio/viewer_protocol.py diff --git a/python/mujoco/experimental/dear_imgui/dear_imgui.cc b/python/mujoco/experimental/dear_imgui/dear_imgui.cc new file mode 100644 index 00000000..c8f41fb0 --- /dev/null +++ b/python/mujoco/experimental/dear_imgui/dear_imgui.cc @@ -0,0 +1,1233 @@ +// 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 +#include +#include +#define NAMESPACE ImGui +#include "dear_imgui_macros.h" +#include +#include +#include +#include +#include +#include + +// 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_Min_Zero = ImVec2(-FLT_MIN, 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); + + +PYBIND11_MODULE(dear_imgui, m) { + // Types. + + py::class_(m, "Vec2") + .def(py::init<>()) + .def(py::init(), py::arg("_x"), py::arg("_y")) + .def_readwrite("x", &ImVec2::x) + .def_readwrite("y", &ImVec2::y); + + py::class_(m, "Vec4") + .def(py::init<>()) + .def(py::init(), py::arg("_x"), py::arg("_y"), py::arg("_z"), py::arg("_w")) + .def_readwrite("x", &ImVec4::x) + .def_readwrite("y", &ImVec4::y) + .def_readwrite("z", &ImVec4::z) + .def_readwrite("w", &ImVec4::w); + + py::class_(m, "Style") + .def_readwrite("FramePadding", &ImGuiStyle::FramePadding) + .def_readwrite("ItemSpacing", &ImGuiStyle::ItemSpacing) + .def_readwrite("WindowPadding", &ImGuiStyle::WindowPadding); + + m.def("GetStyle", &ImGui::GetStyle, py::return_value_policy::reference); + + py::class_(m, "IO") + .def_readonly("DisplaySize", &ImGuiIO::DisplaySize) + .def_readonly("DeltaTime", &ImGuiIO::DeltaTime) + .def_readonly("Framerate", &ImGuiIO::Framerate) + .def_readonly("WantCaptureMouse", &ImGuiIO::WantCaptureMouse) + .def_readonly("WantCaptureKeyboard", &ImGuiIO::WantCaptureKeyboard) + .def_readonly("KeyShift", &ImGuiIO::KeyShift) + .def_readonly("KeyCtrl", &ImGuiIO::KeyCtrl) + .def_readonly("KeyAlt", &ImGuiIO::KeyAlt) + .def_readonly("KeySuper", &ImGuiIO::KeySuper) + .def_readonly("MousePos", &ImGuiIO::MousePos) + .def_readonly("MouseWheel", &ImGuiIO::MouseWheel) + .def_readonly("MouseDelta", &ImGuiIO::MouseDelta); + + m.def("GetIO", &ImGui::GetIO, py::return_value_policy::reference); + + m.def("GetCurrentContext", []() { + return reinterpret_cast(ImGui::GetCurrentContext()); + }); + m.def("SetCurrentContext", [](uintptr_t ptr) { + ImGui::SetCurrentContext(reinterpret_cast(ptr)); + }); + + py::class_(m, "ListClipper") + .def(py::init<>()) + .def("Begin", &ImGuiListClipper::Begin, py::arg("items_count"), py::arg("items_height") = -1.0f) + .def("End", &ImGuiListClipper::End) + .def("Step", &ImGuiListClipper::Step) + .def("IncludeItemsByIndex", &ImGuiListClipper::IncludeItemsByIndex, py::arg("item_begin"), py::arg("item_end")) + .def("IncludeItemByIndex", &ImGuiListClipper::IncludeItemByIndex, py::arg("item_index")) + .def("SeekCursorForItem", &ImGuiListClipper::SeekCursorForItem, py::arg("item_index")) + .def_readonly("DisplayStart", &ImGuiListClipper::DisplayStart) + .def_readonly("DisplayEnd", &ImGuiListClipper::DisplayEnd); + + // Enumerations. + + py::enum_(m, "WindowFlags") + .value("None", ImGuiWindowFlags_None) + .value("NoTitleBar", ImGuiWindowFlags_NoTitleBar) + .value("NoResize", ImGuiWindowFlags_NoResize) + .value("NoMove", ImGuiWindowFlags_NoMove) + .value("NoScrollbar", ImGuiWindowFlags_NoScrollbar) + .value("NoScrollWithMouse", ImGuiWindowFlags_NoScrollWithMouse) + .value("NoCollapse", ImGuiWindowFlags_NoCollapse) + .value("AlwaysAutoResize", ImGuiWindowFlags_AlwaysAutoResize) + .value("NoBackground", ImGuiWindowFlags_NoBackground) + .value("NoSavedSettings", ImGuiWindowFlags_NoSavedSettings) + .value("NoMouseInputs", ImGuiWindowFlags_NoMouseInputs) + .value("MenuBar", ImGuiWindowFlags_MenuBar) + .value("HorizontalScrollbar", ImGuiWindowFlags_HorizontalScrollbar) + .value("NoFocusOnAppearing", ImGuiWindowFlags_NoFocusOnAppearing) + .value("NoBringToFrontOnFocus", ImGuiWindowFlags_NoBringToFrontOnFocus) + .value("AlwaysVerticalScrollbar", ImGuiWindowFlags_AlwaysVerticalScrollbar) + .value("AlwaysHorizontalScrollbar", ImGuiWindowFlags_AlwaysHorizontalScrollbar) + .value("NoNavInputs", ImGuiWindowFlags_NoNavInputs) + .value("NoNavFocus", ImGuiWindowFlags_NoNavFocus) + .value("UnsavedDocument", ImGuiWindowFlags_UnsavedDocument) + .value("NoDocking", ImGuiWindowFlags_NoDocking) + .value("NoNav", ImGuiWindowFlags_NoNav) + .value("NoDecoration", ImGuiWindowFlags_NoDecoration) + .value("NoInputs", ImGuiWindowFlags_NoInputs) + .value("ChildWindow", ImGuiWindowFlags_ChildWindow) + .value("Tooltip", ImGuiWindowFlags_Tooltip) + .value("Popup", ImGuiWindowFlags_Popup) + .value("Modal", ImGuiWindowFlags_Modal) + .value("ChildMenu", ImGuiWindowFlags_ChildMenu) + .value("DockNodeHost", ImGuiWindowFlags_DockNodeHost); + + py::enum_(m, "ChildFlags") + .value("None", ImGuiChildFlags_None) + .value("Borders", ImGuiChildFlags_Borders) + .value("AlwaysUseWindowPadding", ImGuiChildFlags_AlwaysUseWindowPadding) + .value("ResizeX", ImGuiChildFlags_ResizeX) + .value("ResizeY", ImGuiChildFlags_ResizeY) + .value("AutoResizeX", ImGuiChildFlags_AutoResizeX) + .value("AutoResizeY", ImGuiChildFlags_AutoResizeY) + .value("AlwaysAutoResize", ImGuiChildFlags_AlwaysAutoResize) + .value("FrameStyle", ImGuiChildFlags_FrameStyle) + .value("NavFlattened", ImGuiChildFlags_NavFlattened); + + py::enum_(m, "InputTextFlags") + .value("None", ImGuiInputTextFlags_None) + .value("CharsDecimal", ImGuiInputTextFlags_CharsDecimal) + .value("CharsHexadecimal", ImGuiInputTextFlags_CharsHexadecimal) + .value("CharsScientific", ImGuiInputTextFlags_CharsScientific) + .value("CharsUppercase", ImGuiInputTextFlags_CharsUppercase) + .value("CharsNoBlank", ImGuiInputTextFlags_CharsNoBlank) + .value("AllowTabInput", ImGuiInputTextFlags_AllowTabInput) + .value("EnterReturnsTrue", ImGuiInputTextFlags_EnterReturnsTrue) + .value("EscapeClearsAll", ImGuiInputTextFlags_EscapeClearsAll) + .value("CtrlEnterForNewLine", ImGuiInputTextFlags_CtrlEnterForNewLine) + .value("ReadOnly", ImGuiInputTextFlags_ReadOnly) + .value("Password", ImGuiInputTextFlags_Password) + .value("AlwaysOverwrite", ImGuiInputTextFlags_AlwaysOverwrite) + .value("AutoSelectAll", ImGuiInputTextFlags_AutoSelectAll) + .value("ParseEmptyRefVal", ImGuiInputTextFlags_ParseEmptyRefVal) + .value("DisplayEmptyRefVal", ImGuiInputTextFlags_DisplayEmptyRefVal) + .value("NoHorizontalScroll", ImGuiInputTextFlags_NoHorizontalScroll) + .value("NoUndoRedo", ImGuiInputTextFlags_NoUndoRedo) + .value("CallbackCompletion", ImGuiInputTextFlags_CallbackCompletion) + .value("CallbackHistory", ImGuiInputTextFlags_CallbackHistory) + .value("CallbackAlways", ImGuiInputTextFlags_CallbackAlways) + .value("CallbackCharFilter", ImGuiInputTextFlags_CallbackCharFilter) + .value("CallbackResize", ImGuiInputTextFlags_CallbackResize) + .value("CallbackEdit", ImGuiInputTextFlags_CallbackEdit); + + py::enum_(m, "TreeNodeFlags") + .value("None", ImGuiTreeNodeFlags_None) + .value("Selected", ImGuiTreeNodeFlags_Selected) + .value("Framed", ImGuiTreeNodeFlags_Framed) + .value("AllowOverlap", ImGuiTreeNodeFlags_AllowOverlap) + .value("NoTreePushOnOpen", ImGuiTreeNodeFlags_NoTreePushOnOpen) + .value("NoAutoOpenOnLog", ImGuiTreeNodeFlags_NoAutoOpenOnLog) + .value("DefaultOpen", ImGuiTreeNodeFlags_DefaultOpen) + .value("OpenOnDoubleClick", ImGuiTreeNodeFlags_OpenOnDoubleClick) + .value("OpenOnArrow", ImGuiTreeNodeFlags_OpenOnArrow) + .value("Leaf", ImGuiTreeNodeFlags_Leaf) + .value("Bullet", ImGuiTreeNodeFlags_Bullet) + .value("FramePadding", ImGuiTreeNodeFlags_FramePadding) + .value("SpanAvailWidth", ImGuiTreeNodeFlags_SpanAvailWidth) + .value("SpanFullWidth", ImGuiTreeNodeFlags_SpanFullWidth) + .value("SpanTextWidth", ImGuiTreeNodeFlags_SpanTextWidth) + .value("SpanAllColumns", ImGuiTreeNodeFlags_SpanAllColumns) + .value("NavLeftJumpsBackHere", ImGuiTreeNodeFlags_NavLeftJumpsBackHere) + .value("CollapsingHeader", ImGuiTreeNodeFlags_CollapsingHeader); + + py::enum_(m, "PopupFlags") + .value("None", ImGuiPopupFlags_None) + .value("MouseButtonLeft", ImGuiPopupFlags_MouseButtonLeft) + .value("MouseButtonRight", ImGuiPopupFlags_MouseButtonRight) + .value("MouseButtonMiddle", ImGuiPopupFlags_MouseButtonMiddle) + .value("MouseButtonMask", ImGuiPopupFlags_MouseButtonMask_) + .value("NoReopen", ImGuiPopupFlags_NoReopen) + .value("NoOpenOverExistingPopup", ImGuiPopupFlags_NoOpenOverExistingPopup) + .value("NoOpenOverItems", ImGuiPopupFlags_NoOpenOverItems) + .value("AnyPopupId", ImGuiPopupFlags_AnyPopupId) + .value("AnyPopupLevel", ImGuiPopupFlags_AnyPopupLevel) + .value("AnyPopup", ImGuiPopupFlags_AnyPopup); + + py::enum_(m, "SelectableFlags") + .value("None", ImGuiSelectableFlags_None) + .value("DontClosePopups", ImGuiSelectableFlags_DontClosePopups) + .value("SpanAllColumns", ImGuiSelectableFlags_SpanAllColumns) + .value("AllowDoubleClick", ImGuiSelectableFlags_AllowDoubleClick) + .value("Disabled", ImGuiSelectableFlags_Disabled) + .value("AllowOverlap", ImGuiSelectableFlags_AllowOverlap); + + py::enum_(m, "ComboFlags") + .value("None", ImGuiComboFlags_None) + .value("PopupAlignLeft", ImGuiComboFlags_PopupAlignLeft) + .value("HeightSmall", ImGuiComboFlags_HeightSmall) + .value("HeightRegular", ImGuiComboFlags_HeightRegular) + .value("HeightLarge", ImGuiComboFlags_HeightLarge) + .value("HeightLargest", ImGuiComboFlags_HeightLargest) + .value("NoArrowButton", ImGuiComboFlags_NoArrowButton) + .value("NoPreview", ImGuiComboFlags_NoPreview) + .value("WidthFitPreview", ImGuiComboFlags_WidthFitPreview) + .value("HeightMask", ImGuiComboFlags_HeightMask_); + + py::enum_(m, "TabBarFlags") + .value("None", ImGuiTabBarFlags_None) + .value("Reorderable", ImGuiTabBarFlags_Reorderable) + .value("AutoSelectNewTabs", ImGuiTabBarFlags_AutoSelectNewTabs) + .value("TabListPopupButton", ImGuiTabBarFlags_TabListPopupButton) + .value("NoCloseWithMiddleMouseButton", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) + .value("NoTabListScrollingButtons", ImGuiTabBarFlags_NoTabListScrollingButtons) + .value("NoTooltip", ImGuiTabBarFlags_NoTooltip) + .value("DrawSelectedOverline", ImGuiTabBarFlags_DrawSelectedOverline) + .value("FittingPolicyResizeDown", ImGuiTabBarFlags_FittingPolicyResizeDown) + .value("FittingPolicyScroll", ImGuiTabBarFlags_FittingPolicyScroll) + .value("FittingPolicyMask", ImGuiTabBarFlags_FittingPolicyMask_) + .value("FittingPolicyDefault", ImGuiTabBarFlags_FittingPolicyDefault_); + + py::enum_(m, "TabItemFlags") + .value("None", ImGuiTabItemFlags_None) + .value("UnsavedDocument", ImGuiTabItemFlags_UnsavedDocument) + .value("SetSelected", ImGuiTabItemFlags_SetSelected) + .value("NoCloseWithMiddleMouseButton", ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) + .value("NoPushId", ImGuiTabItemFlags_NoPushId) + .value("NoTooltip", ImGuiTabItemFlags_NoTooltip) + .value("NoReorder", ImGuiTabItemFlags_NoReorder) + .value("Leading", ImGuiTabItemFlags_Leading) + .value("Trailing", ImGuiTabItemFlags_Trailing) + .value("NoAssumedClosure", ImGuiTabItemFlags_NoAssumedClosure); + + py::enum_(m, "FocusedFlags") + .value("None", ImGuiFocusedFlags_None) + .value("ChildWindows", ImGuiFocusedFlags_ChildWindows) + .value("RootWindow", ImGuiFocusedFlags_RootWindow) + .value("AnyWindow", ImGuiFocusedFlags_AnyWindow) + .value("NoPopupHierarchy", ImGuiFocusedFlags_NoPopupHierarchy) + .value("DockHierarchy", ImGuiFocusedFlags_DockHierarchy) + .value("RootAndChildWindows", ImGuiFocusedFlags_RootAndChildWindows); + + py::enum_(m, "HoveredFlags") + .value("None", ImGuiHoveredFlags_None) + .value("ChildWindows", ImGuiHoveredFlags_ChildWindows) + .value("RootWindow", ImGuiHoveredFlags_RootWindow) + .value("AnyWindow", ImGuiHoveredFlags_AnyWindow) + .value("NoPopupHierarchy", ImGuiHoveredFlags_NoPopupHierarchy) + .value("DockHierarchy", ImGuiHoveredFlags_DockHierarchy) + .value("AllowWhenBlockedByPopup", ImGuiHoveredFlags_AllowWhenBlockedByPopup) + .value("AllowWhenBlockedByActiveItem", ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) + .value("AllowWhenOverlappedByItem", ImGuiHoveredFlags_AllowWhenOverlappedByItem) + .value("AllowWhenOverlappedByWindow", ImGuiHoveredFlags_AllowWhenOverlappedByWindow) + .value("AllowWhenDisabled", ImGuiHoveredFlags_AllowWhenDisabled) + .value("NoNavOverride", ImGuiHoveredFlags_NoNavOverride) + .value("AllowWhenOverlapped", ImGuiHoveredFlags_AllowWhenOverlapped) + .value("RectOnly", ImGuiHoveredFlags_RectOnly) + .value("RootAndChildWindows", ImGuiHoveredFlags_RootAndChildWindows) + .value("ForTooltip", ImGuiHoveredFlags_ForTooltip) + .value("Stationary", ImGuiHoveredFlags_Stationary) + .value("DelayNone", ImGuiHoveredFlags_DelayNone) + .value("DelayShort", ImGuiHoveredFlags_DelayShort) + .value("DelayNormal", ImGuiHoveredFlags_DelayNormal) + .value("NoSharedDelay", ImGuiHoveredFlags_NoSharedDelay); + + py::enum_(m, "DockNodeFlags") + .value("None", ImGuiDockNodeFlags_None) + .value("KeepAliveOnly", ImGuiDockNodeFlags_KeepAliveOnly) + .value("NoDockingOverCentralNode", ImGuiDockNodeFlags_NoDockingOverCentralNode) + .value("PassthruCentralNode", ImGuiDockNodeFlags_PassthruCentralNode) + .value("NoDockingSplit", ImGuiDockNodeFlags_NoDockingSplit) + .value("NoResize", ImGuiDockNodeFlags_NoResize) + .value("AutoHideTabBar", ImGuiDockNodeFlags_AutoHideTabBar) + .value("NoUndocking", ImGuiDockNodeFlags_NoUndocking); + + py::enum_(m, "DragDropFlags") + .value("None", ImGuiDragDropFlags_None) + .value("SourceNoPreviewTooltip", ImGuiDragDropFlags_SourceNoPreviewTooltip) + .value("SourceNoDisableHover", ImGuiDragDropFlags_SourceNoDisableHover) + .value("SourceNoHoldToOpenOthers", ImGuiDragDropFlags_SourceNoHoldToOpenOthers) + .value("SourceAllowNullID", ImGuiDragDropFlags_SourceAllowNullID) + .value("SourceExtern", ImGuiDragDropFlags_SourceExtern) + .value("PayloadAutoExpire", ImGuiDragDropFlags_PayloadAutoExpire) + .value("PayloadNoCrossContext", ImGuiDragDropFlags_PayloadNoCrossContext) + .value("PayloadNoCrossProcess", ImGuiDragDropFlags_PayloadNoCrossProcess) + .value("AcceptBeforeDelivery", ImGuiDragDropFlags_AcceptBeforeDelivery) + .value("AcceptNoDrawDefaultRect", ImGuiDragDropFlags_AcceptNoDrawDefaultRect) + .value("AcceptNoPreviewTooltip", ImGuiDragDropFlags_AcceptNoPreviewTooltip) + .value("AcceptPeekOnly", ImGuiDragDropFlags_AcceptPeekOnly); + + py::enum_(m, "DataType") + .value("S8", ImGuiDataType_S8) + .value("U8", ImGuiDataType_U8) + .value("S16", ImGuiDataType_S16) + .value("U16", ImGuiDataType_U16) + .value("S32", ImGuiDataType_S32) + .value("U32", ImGuiDataType_U32) + .value("S64", ImGuiDataType_S64) + .value("U64", ImGuiDataType_U64) + .value("Float", ImGuiDataType_Float) + .value("Double", ImGuiDataType_Double); + + py::enum_(m, "Dir") + .value("None", ImGuiDir_None) + .value("Left", ImGuiDir_Left) + .value("Right", ImGuiDir_Right) + .value("Up", ImGuiDir_Up) + .value("Down", ImGuiDir_Down); + + py::enum_(m, "SortDirection") + .value("None", ImGuiSortDirection_None) + .value("Ascending", ImGuiSortDirection_Ascending) + .value("Descending", ImGuiSortDirection_Descending); + + py::enum_(m, "InputFlags") + .value("None", ImGuiInputFlags_None) + .value("Repeat", ImGuiInputFlags_Repeat) + .value("RouteActive", ImGuiInputFlags_RouteActive) + .value("RouteFocused", ImGuiInputFlags_RouteFocused) + .value("RouteGlobal", ImGuiInputFlags_RouteGlobal) + .value("RouteAlways", ImGuiInputFlags_RouteAlways) + .value("RouteOverFocused", ImGuiInputFlags_RouteOverFocused) + .value("RouteOverActive", ImGuiInputFlags_RouteOverActive) + .value("RouteUnlessBgFocused", ImGuiInputFlags_RouteUnlessBgFocused) + .value("RouteFromRootWindow", ImGuiInputFlags_RouteFromRootWindow) + .value("Tooltip", ImGuiInputFlags_Tooltip); + + py::enum_(m, "ConfigFlags") + .value("None", ImGuiConfigFlags_None) + .value("NavEnableKeyboard", ImGuiConfigFlags_NavEnableKeyboard) + .value("NavEnableGamepad", ImGuiConfigFlags_NavEnableGamepad) + .value("NavEnableSetMousePos", ImGuiConfigFlags_NavEnableSetMousePos) + .value("NavNoCaptureKeyboard", ImGuiConfigFlags_NavNoCaptureKeyboard) + .value("NoMouse", ImGuiConfigFlags_NoMouse) + .value("NoMouseCursorChange", ImGuiConfigFlags_NoMouseCursorChange) + .value("NoKeyboard", ImGuiConfigFlags_NoKeyboard) + .value("DockingEnable", ImGuiConfigFlags_DockingEnable) + .value("ViewportsEnable", ImGuiConfigFlags_ViewportsEnable) + .value("DpiEnableScaleViewports", ImGuiConfigFlags_DpiEnableScaleViewports) + .value("DpiEnableScaleFonts", ImGuiConfigFlags_DpiEnableScaleFonts) + .value("IsSRGB", ImGuiConfigFlags_IsSRGB) + .value("IsTouchScreen", ImGuiConfigFlags_IsTouchScreen); + + py::enum_(m, "BackendFlags") + .value("None", ImGuiBackendFlags_None) + .value("HasGamepad", ImGuiBackendFlags_HasGamepad) + .value("HasMouseCursors", ImGuiBackendFlags_HasMouseCursors) + .value("HasSetMousePos", ImGuiBackendFlags_HasSetMousePos) + .value("RendererHasVtxOffset", ImGuiBackendFlags_RendererHasVtxOffset) + .value("PlatformHasViewports", ImGuiBackendFlags_PlatformHasViewports) + .value("HasMouseHoveredViewport", ImGuiBackendFlags_HasMouseHoveredViewport) + .value("RendererHasViewports", ImGuiBackendFlags_RendererHasViewports); + + py::enum_(m, "Col") + .value("Text", ImGuiCol_Text) + .value("TextDisabled", ImGuiCol_TextDisabled) + .value("WindowBg", ImGuiCol_WindowBg) + .value("ChildBg", ImGuiCol_ChildBg) + .value("PopupBg", ImGuiCol_PopupBg) + .value("Border", ImGuiCol_Border) + .value("BorderShadow", ImGuiCol_BorderShadow) + .value("FrameBg", ImGuiCol_FrameBg) + .value("FrameBgHovered", ImGuiCol_FrameBgHovered) + .value("FrameBgActive", ImGuiCol_FrameBgActive) + .value("TitleBg", ImGuiCol_TitleBg) + .value("TitleBgActive", ImGuiCol_TitleBgActive) + .value("TitleBgCollapsed", ImGuiCol_TitleBgCollapsed) + .value("MenuBarBg", ImGuiCol_MenuBarBg) + .value("ScrollbarBg", ImGuiCol_ScrollbarBg) + .value("ScrollbarGrab", ImGuiCol_ScrollbarGrab) + .value("ScrollbarGrabHovered", ImGuiCol_ScrollbarGrabHovered) + .value("ScrollbarGrabActive", ImGuiCol_ScrollbarGrabActive) + .value("CheckMark", ImGuiCol_CheckMark) + .value("SliderGrab", ImGuiCol_SliderGrab) + .value("SliderGrabActive", ImGuiCol_SliderGrabActive) + .value("Button", ImGuiCol_Button) + .value("ButtonHovered", ImGuiCol_ButtonHovered) + .value("ButtonActive", ImGuiCol_ButtonActive) + .value("Header", ImGuiCol_Header) + .value("HeaderHovered", ImGuiCol_HeaderHovered) + .value("HeaderActive", ImGuiCol_HeaderActive) + .value("Separator", ImGuiCol_Separator) + .value("SeparatorHovered", ImGuiCol_SeparatorHovered) + .value("SeparatorActive", ImGuiCol_SeparatorActive) + .value("ResizeGrip", ImGuiCol_ResizeGrip) + .value("ResizeGripHovered", ImGuiCol_ResizeGripHovered) + .value("ResizeGripActive", ImGuiCol_ResizeGripActive) + .value("TabHovered", ImGuiCol_TabHovered) + .value("Tab", ImGuiCol_Tab) + .value("TabSelected", ImGuiCol_TabSelected) + .value("TabSelectedOverline", ImGuiCol_TabSelectedOverline) + .value("TabDimmed", ImGuiCol_TabDimmed) + .value("TabDimmedSelected", ImGuiCol_TabDimmedSelected) + .value("TabDimmedSelectedOverline", ImGuiCol_TabDimmedSelectedOverline) + .value("DockingPreview", ImGuiCol_DockingPreview) + .value("DockingEmptyBg", ImGuiCol_DockingEmptyBg) + .value("PlotLines", ImGuiCol_PlotLines) + .value("PlotLinesHovered", ImGuiCol_PlotLinesHovered) + .value("PlotHistogram", ImGuiCol_PlotHistogram) + .value("PlotHistogramHovered", ImGuiCol_PlotHistogramHovered) + .value("TableHeaderBg", ImGuiCol_TableHeaderBg) + .value("TableBorderStrong", ImGuiCol_TableBorderStrong) + .value("TableBorderLight", ImGuiCol_TableBorderLight) + .value("TableRowBg", ImGuiCol_TableRowBg) + .value("TableRowBgAlt", ImGuiCol_TableRowBgAlt) + .value("TextLink", ImGuiCol_TextLink) + .value("TextSelectedBg", ImGuiCol_TextSelectedBg) + .value("DragDropTarget", ImGuiCol_DragDropTarget) + .value("NavHighlight", ImGuiCol_NavHighlight) + .value("NavWindowingHighlight", ImGuiCol_NavWindowingHighlight) + .value("NavWindowingDimBg", ImGuiCol_NavWindowingDimBg) + .value("ModalWindowDimBg", ImGuiCol_ModalWindowDimBg); + + py::enum_(m, "StyleVar") + .value("Alpha", ImGuiStyleVar_Alpha) + .value("DisabledAlpha", ImGuiStyleVar_DisabledAlpha) + .value("WindowPadding", ImGuiStyleVar_WindowPadding) + .value("WindowRounding", ImGuiStyleVar_WindowRounding) + .value("WindowBorderSize", ImGuiStyleVar_WindowBorderSize) + .value("WindowMinSize", ImGuiStyleVar_WindowMinSize) + .value("WindowTitleAlign", ImGuiStyleVar_WindowTitleAlign) + .value("ChildRounding", ImGuiStyleVar_ChildRounding) + .value("ChildBorderSize", ImGuiStyleVar_ChildBorderSize) + .value("PopupRounding", ImGuiStyleVar_PopupRounding) + .value("PopupBorderSize", ImGuiStyleVar_PopupBorderSize) + .value("FramePadding", ImGuiStyleVar_FramePadding) + .value("FrameRounding", ImGuiStyleVar_FrameRounding) + .value("FrameBorderSize", ImGuiStyleVar_FrameBorderSize) + .value("ItemSpacing", ImGuiStyleVar_ItemSpacing) + .value("ItemInnerSpacing", ImGuiStyleVar_ItemInnerSpacing) + .value("IndentSpacing", ImGuiStyleVar_IndentSpacing) + .value("CellPadding", ImGuiStyleVar_CellPadding) + .value("ScrollbarSize", ImGuiStyleVar_ScrollbarSize) + .value("ScrollbarRounding", ImGuiStyleVar_ScrollbarRounding) + .value("GrabMinSize", ImGuiStyleVar_GrabMinSize) + .value("GrabRounding", ImGuiStyleVar_GrabRounding) + .value("TabRounding", ImGuiStyleVar_TabRounding) + .value("TabBorderSize", ImGuiStyleVar_TabBorderSize) + .value("TabBarBorderSize", ImGuiStyleVar_TabBarBorderSize) + .value("TableAngledHeadersAngle", ImGuiStyleVar_TableAngledHeadersAngle) + .value("TableAngledHeadersTextAlign", ImGuiStyleVar_TableAngledHeadersTextAlign) + .value("ButtonTextAlign", ImGuiStyleVar_ButtonTextAlign) + .value("SelectableTextAlign", ImGuiStyleVar_SelectableTextAlign) + .value("SeparatorTextBorderSize", ImGuiStyleVar_SeparatorTextBorderSize) + .value("SeparatorTextAlign", ImGuiStyleVar_SeparatorTextAlign) + .value("SeparatorTextPadding", ImGuiStyleVar_SeparatorTextPadding) + .value("DockingSeparatorSize", ImGuiStyleVar_DockingSeparatorSize); + + py::enum_(m, "ButtonFlags") + .value("None", ImGuiButtonFlags_None) + .value("MouseButtonLeft", ImGuiButtonFlags_MouseButtonLeft) + .value("MouseButtonRight", ImGuiButtonFlags_MouseButtonRight) + .value("MouseButtonMiddle", ImGuiButtonFlags_MouseButtonMiddle) + .value("MouseButtonMask", ImGuiButtonFlags_MouseButtonMask_); + + py::enum_(m, "ColorEditFlags") + .value("None", ImGuiColorEditFlags_None) + .value("NoAlpha", ImGuiColorEditFlags_NoAlpha) + .value("NoPicker", ImGuiColorEditFlags_NoPicker) + .value("NoOptions", ImGuiColorEditFlags_NoOptions) + .value("NoSmallPreview", ImGuiColorEditFlags_NoSmallPreview) + .value("NoInputs", ImGuiColorEditFlags_NoInputs) + .value("NoTooltip", ImGuiColorEditFlags_NoTooltip) + .value("NoLabel", ImGuiColorEditFlags_NoLabel) + .value("NoSidePreview", ImGuiColorEditFlags_NoSidePreview) + .value("NoDragDrop", ImGuiColorEditFlags_NoDragDrop) + .value("NoBorder", ImGuiColorEditFlags_NoBorder) + .value("AlphaBar", ImGuiColorEditFlags_AlphaBar) + .value("AlphaPreview", ImGuiColorEditFlags_AlphaPreview) + .value("AlphaPreviewHalf", ImGuiColorEditFlags_AlphaPreviewHalf) + .value("HDR", ImGuiColorEditFlags_HDR) + .value("DisplayRGB", ImGuiColorEditFlags_DisplayRGB) + .value("DisplayHSV", ImGuiColorEditFlags_DisplayHSV) + .value("DisplayHex", ImGuiColorEditFlags_DisplayHex) + .value("Uint8", ImGuiColorEditFlags_Uint8) + .value("Float", ImGuiColorEditFlags_Float) + .value("PickerHueBar", ImGuiColorEditFlags_PickerHueBar) + .value("PickerHueWheel", ImGuiColorEditFlags_PickerHueWheel) + .value("InputRGB", ImGuiColorEditFlags_InputRGB) + .value("InputHSV", ImGuiColorEditFlags_InputHSV) + .value("DefaultOptions", ImGuiColorEditFlags_DefaultOptions_) + .value("DisplayMask", ImGuiColorEditFlags_DisplayMask_) + .value("DataTypeMask", ImGuiColorEditFlags_DataTypeMask_) + .value("PickerMask", ImGuiColorEditFlags_PickerMask_) + .value("InputMask", ImGuiColorEditFlags_InputMask_); + + py::enum_(m, "SliderFlags") + .value("None", ImGuiSliderFlags_None) + .value("AlwaysClamp", ImGuiSliderFlags_AlwaysClamp) + .value("Logarithmic", ImGuiSliderFlags_Logarithmic) + .value("NoRoundToFormat", ImGuiSliderFlags_NoRoundToFormat) + .value("NoInput", ImGuiSliderFlags_NoInput) + .value("WrapAround", ImGuiSliderFlags_WrapAround) + .value("InvalidMask", ImGuiSliderFlags_InvalidMask_); + + py::enum_(m, "MouseButton") + .value("Left", ImGuiMouseButton_Left) + .value("Right", ImGuiMouseButton_Right) + .value("Middle", ImGuiMouseButton_Middle); + + py::enum_(m, "MouseCursor") + .value("None", ImGuiMouseCursor_None) + .value("Arrow", ImGuiMouseCursor_Arrow) + .value("TextInput", ImGuiMouseCursor_TextInput) + .value("ResizeAll", ImGuiMouseCursor_ResizeAll) + .value("ResizeNS", ImGuiMouseCursor_ResizeNS) + .value("ResizeEW", ImGuiMouseCursor_ResizeEW) + .value("ResizeNESW", ImGuiMouseCursor_ResizeNESW) + .value("ResizeNWSE", ImGuiMouseCursor_ResizeNWSE) + .value("Hand", ImGuiMouseCursor_Hand) + .value("NotAllowed", ImGuiMouseCursor_NotAllowed); + + py::enum_(m, "MouseSource") + .value("Mouse", ImGuiMouseSource_Mouse) + .value("TouchScreen", ImGuiMouseSource_TouchScreen) + .value("Pen", ImGuiMouseSource_Pen); + + py::enum_(m, "Cond") + .value("None", ImGuiCond_None) + .value("Always", ImGuiCond_Always) + .value("Once", ImGuiCond_Once) + .value("FirstUseEver", ImGuiCond_FirstUseEver) + .value("Appearing", ImGuiCond_Appearing); + + py::enum_(m, "TableFlags") + .value("None", ImGuiTableFlags_None) + .value("Resizable", ImGuiTableFlags_Resizable) + .value("Reorderable", ImGuiTableFlags_Reorderable) + .value("Hideable", ImGuiTableFlags_Hideable) + .value("Sortable", ImGuiTableFlags_Sortable) + .value("NoSavedSettings", ImGuiTableFlags_NoSavedSettings) + .value("ContextMenuInBody", ImGuiTableFlags_ContextMenuInBody) + .value("RowBg", ImGuiTableFlags_RowBg) + .value("BordersInnerH", ImGuiTableFlags_BordersInnerH) + .value("BordersOuterH", ImGuiTableFlags_BordersOuterH) + .value("BordersInnerV", ImGuiTableFlags_BordersInnerV) + .value("BordersOuterV", ImGuiTableFlags_BordersOuterV) + .value("BordersH", ImGuiTableFlags_BordersH) + .value("BordersV", ImGuiTableFlags_BordersV) + .value("BordersInner", ImGuiTableFlags_BordersInner) + .value("BordersOuter", ImGuiTableFlags_BordersOuter) + .value("Borders", ImGuiTableFlags_Borders) + .value("NoBordersInBody", ImGuiTableFlags_NoBordersInBody) + .value("NoBordersInBodyUntilResize", ImGuiTableFlags_NoBordersInBodyUntilResize) + .value("SizingFixedFit", ImGuiTableFlags_SizingFixedFit) + .value("SizingFixedSame", ImGuiTableFlags_SizingFixedSame) + .value("SizingStretchProp", ImGuiTableFlags_SizingStretchProp) + .value("SizingStretchSame", ImGuiTableFlags_SizingStretchSame) + .value("NoHostExtendX", ImGuiTableFlags_NoHostExtendX) + .value("NoHostExtendY", ImGuiTableFlags_NoHostExtendY) + .value("NoKeepColumnsVisible", ImGuiTableFlags_NoKeepColumnsVisible) + .value("PreciseWidths", ImGuiTableFlags_PreciseWidths) + .value("NoClip", ImGuiTableFlags_NoClip) + .value("PadOuterX", ImGuiTableFlags_PadOuterX) + .value("NoPadOuterX", ImGuiTableFlags_NoPadOuterX) + .value("NoPadInnerX", ImGuiTableFlags_NoPadInnerX) + .value("ScrollX", ImGuiTableFlags_ScrollX) + .value("ScrollY", ImGuiTableFlags_ScrollY) + .value("SortMulti", ImGuiTableFlags_SortMulti) + .value("SortTristate", ImGuiTableFlags_SortTristate) + .value("HighlightHoveredColumn", ImGuiTableFlags_HighlightHoveredColumn) + .value("SizingMask", ImGuiTableFlags_SizingMask_); + + py::enum_(m, "TableColumnFlags") + .value("None", ImGuiTableColumnFlags_None) + .value("Disabled", ImGuiTableColumnFlags_Disabled) + .value("DefaultHide", ImGuiTableColumnFlags_DefaultHide) + .value("DefaultSort", ImGuiTableColumnFlags_DefaultSort) + .value("WidthStretch", ImGuiTableColumnFlags_WidthStretch) + .value("WidthFixed", ImGuiTableColumnFlags_WidthFixed) + .value("NoResize", ImGuiTableColumnFlags_NoResize) + .value("NoReorder", ImGuiTableColumnFlags_NoReorder) + .value("NoHide", ImGuiTableColumnFlags_NoHide) + .value("NoClip", ImGuiTableColumnFlags_NoClip) + .value("NoSort", ImGuiTableColumnFlags_NoSort) + .value("NoSortAscending", ImGuiTableColumnFlags_NoSortAscending) + .value("NoSortDescending", ImGuiTableColumnFlags_NoSortDescending) + .value("NoHeaderLabel", ImGuiTableColumnFlags_NoHeaderLabel) + .value("NoHeaderWidth", ImGuiTableColumnFlags_NoHeaderWidth) + .value("PreferSortAscending", ImGuiTableColumnFlags_PreferSortAscending) + .value("PreferSortDescending", ImGuiTableColumnFlags_PreferSortDescending) + .value("IndentEnable", ImGuiTableColumnFlags_IndentEnable) + .value("IndentDisable", ImGuiTableColumnFlags_IndentDisable) + .value("AngledHeader", ImGuiTableColumnFlags_AngledHeader) + .value("IsEnabled", ImGuiTableColumnFlags_IsEnabled) + .value("IsVisible", ImGuiTableColumnFlags_IsVisible) + .value("IsSorted", ImGuiTableColumnFlags_IsSorted) + .value("IsHovered", ImGuiTableColumnFlags_IsHovered) + .value("WidthMask", ImGuiTableColumnFlags_WidthMask_) + .value("IndentMask", ImGuiTableColumnFlags_IndentMask_) + .value("StatusMask", ImGuiTableColumnFlags_StatusMask_) + .value("NoDirectResize", ImGuiTableColumnFlags_NoDirectResize_); + + py::enum_(m, "TableRowFlags") + .value("None", ImGuiTableRowFlags_None) + .value("Headers", ImGuiTableRowFlags_Headers); + + py::enum_(m, "TableBgTarget") + .value("None", ImGuiTableBgTarget_None) + .value("RowBg0", ImGuiTableBgTarget_RowBg0) + .value("RowBg1", ImGuiTableBgTarget_RowBg1) + .value("CellBg", ImGuiTableBgTarget_CellBg); + + py::enum_(m, "Key") + .value("None", ImGuiKey_None) + .value("Tab", ImGuiKey_Tab) + .value("LeftArrow", ImGuiKey_LeftArrow) + .value("RightArrow", ImGuiKey_RightArrow) + .value("UpArrow", ImGuiKey_UpArrow) + .value("DownArrow", ImGuiKey_DownArrow) + .value("PageUp", ImGuiKey_PageUp) + .value("PageDown", ImGuiKey_PageDown) + .value("Home", ImGuiKey_Home) + .value("End", ImGuiKey_End) + .value("Insert", ImGuiKey_Insert) + .value("Delete", ImGuiKey_Delete) + .value("Backspace", ImGuiKey_Backspace) + .value("Space", ImGuiKey_Space) + .value("Enter", ImGuiKey_Enter) + .value("Escape", ImGuiKey_Escape) + .value("LeftCtrl", ImGuiKey_LeftCtrl) + .value("LeftShift", ImGuiKey_LeftShift) + .value("LeftAlt", ImGuiKey_LeftAlt) + .value("LeftSuper", ImGuiKey_LeftSuper) + .value("RightCtrl", ImGuiKey_RightCtrl) + .value("RightShift", ImGuiKey_RightShift) + .value("RightAlt", ImGuiKey_RightAlt) + .value("RightSuper", ImGuiKey_RightSuper) + .value("Menu", ImGuiKey_Menu) + // "N" prefix ensures "imgui.Key.N0" parses correctly in Python + // (numbers are not valid identifier prefixes). + .value("N0", ImGuiKey_0) + .value("N1", ImGuiKey_1) + .value("N2", ImGuiKey_2) + .value("N3", ImGuiKey_3) + .value("N4", ImGuiKey_4) + .value("N5", ImGuiKey_5) + .value("N6", ImGuiKey_6) + .value("N7", ImGuiKey_7) + .value("N8", ImGuiKey_8) + .value("N9", ImGuiKey_9) + .value("A", ImGuiKey_A) + .value("B", ImGuiKey_B) + .value("C", ImGuiKey_C) + .value("D", ImGuiKey_D) + .value("E", ImGuiKey_E) + .value("F", ImGuiKey_F) + .value("G", ImGuiKey_G) + .value("H", ImGuiKey_H) + .value("I", ImGuiKey_I) + .value("J", ImGuiKey_J) + .value("K", ImGuiKey_K) + .value("L", ImGuiKey_L) + .value("M", ImGuiKey_M) + .value("N", ImGuiKey_N) + .value("O", ImGuiKey_O) + .value("P", ImGuiKey_P) + .value("Q", ImGuiKey_Q) + .value("R", ImGuiKey_R) + .value("S", ImGuiKey_S) + .value("T", ImGuiKey_T) + .value("U", ImGuiKey_U) + .value("V", ImGuiKey_V) + .value("W", ImGuiKey_W) + .value("X", ImGuiKey_X) + .value("Y", ImGuiKey_Y) + .value("Z", ImGuiKey_Z) + .value("F1", ImGuiKey_F1) + .value("F2", ImGuiKey_F2) + .value("F3", ImGuiKey_F3) + .value("F4", ImGuiKey_F4) + .value("F5", ImGuiKey_F5) + .value("F6", ImGuiKey_F6) + .value("F7", ImGuiKey_F7) + .value("F8", ImGuiKey_F8) + .value("F9", ImGuiKey_F9) + .value("F10", ImGuiKey_F10) + .value("F11", ImGuiKey_F11) + .value("F12", ImGuiKey_F12) + .value("F13", ImGuiKey_F13) + .value("F14", ImGuiKey_F14) + .value("F15", ImGuiKey_F15) + .value("F16", ImGuiKey_F16) + .value("F17", ImGuiKey_F17) + .value("F18", ImGuiKey_F18) + .value("F19", ImGuiKey_F19) + .value("F20", ImGuiKey_F20) + .value("F21", ImGuiKey_F21) + .value("F22", ImGuiKey_F22) + .value("F23", ImGuiKey_F23) + .value("F24", ImGuiKey_F24) + .value("Apostrophe", ImGuiKey_Apostrophe) + .value("Comma", ImGuiKey_Comma) + .value("Minus", ImGuiKey_Minus) + .value("Period", ImGuiKey_Period) + .value("Slash", ImGuiKey_Slash) + .value("Semicolon", ImGuiKey_Semicolon) + .value("Equal", ImGuiKey_Equal) + .value("LeftBracket", ImGuiKey_LeftBracket) + .value("Backslash", ImGuiKey_Backslash) + .value("RightBracket", ImGuiKey_RightBracket) + .value("GraveAccent", ImGuiKey_GraveAccent) + .value("CapsLock", ImGuiKey_CapsLock) + .value("ScrollLock", ImGuiKey_ScrollLock) + .value("NumLock", ImGuiKey_NumLock) + .value("PrintScreen", ImGuiKey_PrintScreen) + .value("Pause", ImGuiKey_Pause) + .value("Keypad0", ImGuiKey_Keypad0) + .value("Keypad1", ImGuiKey_Keypad1) + .value("Keypad2", ImGuiKey_Keypad2) + .value("Keypad3", ImGuiKey_Keypad3) + .value("Keypad4", ImGuiKey_Keypad4) + .value("Keypad5", ImGuiKey_Keypad5) + .value("Keypad6", ImGuiKey_Keypad6) + .value("Keypad7", ImGuiKey_Keypad7) + .value("Keypad8", ImGuiKey_Keypad8) + .value("Keypad9", ImGuiKey_Keypad9) + .value("KeypadDecimal", ImGuiKey_KeypadDecimal) + .value("KeypadDivide", ImGuiKey_KeypadDivide) + .value("KeypadMultiply", ImGuiKey_KeypadMultiply) + .value("KeypadSubtract", ImGuiKey_KeypadSubtract) + .value("KeypadAdd", ImGuiKey_KeypadAdd) + .value("KeypadEnter", ImGuiKey_KeypadEnter) + .value("KeypadEqual", ImGuiKey_KeypadEqual) + .value("AppBack", ImGuiKey_AppBack) + .value("AppForward", ImGuiKey_AppForward) + .value("GamepadStart", ImGuiKey_GamepadStart) + .value("GamepadBack", ImGuiKey_GamepadBack) + .value("GamepadFaceLeft", ImGuiKey_GamepadFaceLeft) + .value("GamepadFaceRight", ImGuiKey_GamepadFaceRight) + .value("GamepadFaceUp", ImGuiKey_GamepadFaceUp) + .value("GamepadFaceDown", ImGuiKey_GamepadFaceDown) + .value("GamepadDpadLeft", ImGuiKey_GamepadDpadLeft) + .value("GamepadDpadRight", ImGuiKey_GamepadDpadRight) + .value("GamepadDpadUp", ImGuiKey_GamepadDpadUp) + .value("GamepadDpadDown", ImGuiKey_GamepadDpadDown) + .value("GamepadL1", ImGuiKey_GamepadL1) + .value("GamepadR1", ImGuiKey_GamepadR1) + .value("GamepadL2", ImGuiKey_GamepadL2) + .value("GamepadR2", ImGuiKey_GamepadR2) + .value("GamepadL3", ImGuiKey_GamepadL3) + .value("GamepadR3", ImGuiKey_GamepadR3) + .value("GamepadLStickLeft", ImGuiKey_GamepadLStickLeft) + .value("GamepadLStickRight", ImGuiKey_GamepadLStickRight) + .value("GamepadLStickUp", ImGuiKey_GamepadLStickUp) + .value("GamepadLStickDown", ImGuiKey_GamepadLStickDown) + .value("GamepadRStickLeft", ImGuiKey_GamepadRStickLeft) + .value("GamepadRStickRight", ImGuiKey_GamepadRStickRight) + .value("GamepadRStickUp", ImGuiKey_GamepadRStickUp) + .value("GamepadRStickDown", ImGuiKey_GamepadRStickDown) + .value("MouseLeft", ImGuiKey_MouseLeft) + .value("MouseRight", ImGuiKey_MouseRight) + .value("MouseMiddle", ImGuiKey_MouseMiddle) + .value("MouseX1", ImGuiKey_MouseX1) + .value("MouseX2", ImGuiKey_MouseX2) + .value("MouseWheelX", ImGuiKey_MouseWheelX) + .value("MouseWheelY", ImGuiKey_MouseWheelY) + .value("ReservedForModCtrl", ImGuiKey_ReservedForModCtrl) + .value("ReservedForModShift", ImGuiKey_ReservedForModShift) + .value("ReservedForModAlt", ImGuiKey_ReservedForModAlt) + .value("ReservedForModSuper", ImGuiKey_ReservedForModSuper) + .value("Ctrl", ImGuiMod_Ctrl) + .value("Shift", ImGuiMod_Shift) + .value("Alt", ImGuiMod_Alt) + .value("Super", ImGuiMod_Super) + .value("Mask", ImGuiMod_Mask_); + + // Functions. + // + // Most functions can be bound directly with no custom implementation. A few + // overloaded functions are bound with unique names to prevent ambiguities + // on the python side. Which function is given an alternative name is more + // or less arbitrary. + // + // Functions that take pointers require custom implementations. Pointers are + // usually used as either in/out parameters or lists. For in/out parameters, + // we return a tuple containing the return value of the function and the + // updated value stored in the pointer. For lists, we accept a std::vector + // of the corresponding type and pass the raw array into the ImGui functions. + // + // Any printf-like function (that took a format + va_list) instead simply + // takes a string argument, on the assumption that the formatting will be done + // in python. + // + // Note: this list of functions isn't guaranteed to be complete. We ignore all + // rendering specific functions as well as debug functions and anything else + // that we feel isn't worth the effort to support. + + DEF0_F(ShowDemoWindow, { + bool open = true; + ImGui::ShowDemoWindow(&open); + return open; + }); + + DEF3_F(Begin, (ImString, name, ), (bool*, p_open, = nullptr), (ImGuiWindowFlags, flags, = 0), { + const auto result = ImGui::Begin(name, p_open, flags); + return std::make_tuple(result, *p_open); + }); + DEF0(End); + DEF4(BeginChild, (ImString, str_id, ), (const ImVec2&, size, = ImVec2_Zero), (ImGuiChildFlags, child_flags, = 0), (ImGuiWindowFlags, window_flags, = 0)); + DEF4_AS(BeginChild, BeginChildId, (ImGuiID, id, ), (const ImVec2&, size, = ImVec2_Zero), (ImGuiChildFlags, child_flags, = 0), (ImGuiWindowFlags, window_flags, = 0)); + DEF0(EndChild); + DEF0(IsWindowAppearing); + DEF0(IsWindowCollapsed); + DEF1(IsWindowFocused, (ImGuiFocusedFlags, flags, = 0)); + DEF1(IsWindowHovered, (ImGuiHoveredFlags, flags, = 0)); + DEF0(GetWindowDpiScale); + DEF0(GetWindowPos); + DEF0(GetWindowSize); + DEF0(GetWindowWidth); + DEF0(GetWindowHeight); + DEF3(SetNextWindowPos, (const ImVec2&, pos, ), (ImGuiCond, cond, = 0), (const ImVec2&, pivot, = ImVec2_Zero)); + DEF2(SetNextWindowSize, (const ImVec2&, size, ), (ImGuiCond, cond, = 0)); + DEF1(SetNextWindowContentSize, (const ImVec2&, size, )); + DEF2(SetNextWindowCollapsed, (bool, collapsed, ), (ImGuiCond, cond, = 0)); + DEF0(SetNextWindowFocus); + DEF1(SetNextWindowScroll, (const ImVec2&, scroll, )); + DEF1(SetNextWindowBgAlpha, (float, alpha, )); + DEF1(SetNextWindowViewport, (ImGuiID, viewport_id, )); + DEF2(SetWindowPos, (const ImVec2&, pos, ), (ImGuiCond, cond, = 0)); + DEF2(SetWindowSize, (const ImVec2&, size, ), (ImGuiCond, cond, = 0)); + DEF2(SetWindowCollapsed, (bool, collapsed, ), (ImGuiCond, cond, = 0)); + DEF0(SetWindowFocus); + DEF1(SetWindowFontScale, (float, scale, )); + DEF3(SetWindowPos, (ImString, name, ), (const ImVec2&, pos, ), (ImGuiCond, cond, = 0)); + DEF3(SetWindowSize, (ImString, name, ), (const ImVec2&, size, ), (ImGuiCond, cond, = 0)); + DEF3(SetWindowCollapsed, (ImString, name, ), (bool, collapsed, ), (ImGuiCond, cond, = 0)); + DEF1(SetWindowFocus, (ImString, name, )); + DEF0(GetContentRegionAvail); + DEF0(GetContentRegionMax); + DEF0(GetWindowContentRegionMin); + DEF0(GetWindowContentRegionMax); + DEF0(GetScrollX); + DEF0(GetScrollY); + DEF1(SetScrollX, (float, scroll_x, )); + DEF1(SetScrollY, (float, scroll_y, )); + DEF0(GetScrollMaxX); + DEF0(GetScrollMaxY); + DEF1(SetScrollHereX, (float, center_x_ratio, = 0.5f)); + DEF1(SetScrollHereY, (float, center_y_ratio, = 0.5f)); + DEF2(SetScrollFromPosX, (float, local_x, ), (float, center_x_ratio, = 0.5f)); + DEF2(SetScrollFromPosY, (float, local_y, ), (float, center_y_ratio, = 0.5f)); + DEF0(PopFont); + DEF2(PushStyleColor, (ImGuiCol, idx, ), (ImU32, col, )); + DEF2(PushStyleColor, (ImGuiCol, idx, ), (const ImVec4&, col, )); + DEF1(PopStyleColor, (int, count, = 1)); + DEF2(PushStyleVar, (ImGuiStyleVar, idx, ), (float, val, )); + DEF2(PushStyleVar, (ImGuiStyleVar, idx, ), (const ImVec2&, val, )); + DEF1(PopStyleVar, (int, count, = 1)); + DEF1(PushTabStop, (bool, tab_stop, )); + DEF0(PopTabStop); + DEF1(PushButtonRepeat, (bool, repeat, )); + DEF0(PopButtonRepeat); + DEF1(PushItemWidth, (float, item_width, )); + DEF0(PopItemWidth); + DEF1(SetNextItemWidth, (float, item_width, )); + DEF0(CalcItemWidth); + DEF1(PushTextWrapPos, (float, wrap_local_pos_x, = 0.0f)); + DEF0(PopTextWrapPos); + DEF0(GetFontSize); + DEF0(GetFontTexUvWhitePixel); + DEF2(GetColorU32, (ImGuiCol, idx, ), (float, alpha_mul, = 1.0f)); + DEF1(GetColorU32, (const ImVec4&, col, )); + DEF2(GetColorU32, (ImU32, col, ), (float, alpha_mul, = 1.0f)); + DEF1(GetStyleColorVec4, (ImGuiCol, idx, )); + DEF0(GetCursorScreenPos); + DEF1(SetCursorScreenPos, (const ImVec2&, pos, )); + DEF0(GetCursorPos); + DEF0(GetCursorPosX); + DEF0(GetCursorPosY); + DEF1(SetCursorPos, (const ImVec2&, local_pos, )); + DEF1(SetCursorPosX, (float, local_x, )); + DEF1(SetCursorPosY, (float, local_y, )); + DEF0(GetCursorStartPos); + DEF0(Separator); + DEF2(SameLine, (float, offset_from_start_x, = 0.0f), (float, spacing, = -1.0f)); + DEF0(NewLine); + DEF0(Spacing); + DEF1(Dummy, (const ImVec2&, size, )); + DEF1(Indent, (float, indent_w, = 0.0f)); + DEF1(Unindent, (float, indent_w, = 0.0f)); + DEF0(BeginGroup); + DEF0(EndGroup); + DEF0(AlignTextToFramePadding); + DEF0(GetTextLineHeight); + DEF0(GetTextLineHeightWithSpacing); + DEF0(GetFrameHeight); + DEF0(GetFrameHeightWithSpacing); + DEF1(PushID, (ImString, str_id, )); + DEF2(PushID, (ImString, str_id_begin, ), (ImString, str_id_end, )); + DEF1(PushID, (int, int_id, )); + DEF0(PopID); + DEF1(GetID, (ImString, str_id, )); + DEF2(GetID, (ImString, str_id_begin, ), (ImString, str_id_end, )); + DEF1_F(TextUnformatted, (ImString, txt, ), { + ImGui::TextUnformatted(txt); + }); + DEF1_F(Text, (ImString, txt, ), { + return ImGui::Text("%s", txt); + }); + DEF2_F(TextColored, (const ImVec4&, col, ), (ImString, txt, ), { + return ImGui::TextColored(col, "%s", txt); + }); + DEF1_F(TextDisabled, (ImString, txt, ), { + return ImGui::TextDisabled("%s", txt); + }); + DEF1_F(TextWrapped, (ImString, txt, ), { + return ImGui::TextWrapped("%s", txt); + }); + DEF2_F(LabelText, (ImString, label, ), (ImString, txt, ), { + return ImGui::LabelText(label, "%s", txt); + }); + DEF1_F(BulletText, (ImString, txt, ), { + return ImGui::BulletText("%s", txt); + }); + DEF1(SeparatorText, (ImString, label, )); + DEF2(Button, (ImString, label, ), (const ImVec2&, size, = ImVec2_Zero)); + DEF1(SmallButton, (ImString, label, )); + DEF3(InvisibleButton, (ImString, str_id, ), (const ImVec2&, size, ), (ImGuiButtonFlags, flags, = 0)); + DEF2(ArrowButton, (ImString, str_id, ), (ImGuiDir, dir, )); + DEF2_F(Checkbox, (ImString, label, ), (bool*, v, ), { + auto result = ImGui::Checkbox(label, v); + return std::make_tuple(result, *v); + }); + DEF2(RadioButton, (ImString, label, ), (bool, active, )); + DEF3(ProgressBar, (float, fraction, ), (const ImVec2&, size_arg, = ImVec2_Min_Zero), (ImString, overlay, = nullptr)); + DEF0(Bullet); + DEF1(TextLink, (ImString, label, )); + DEF2(TextLinkOpenURL, (ImString, label, ), (ImString, url, = nullptr)); + DEF6_F(Image, (long, user_texture_id, ), (const ImVec2&, image_size, ), (const ImVec2&, uv0, = ImVec2_Zero), (const ImVec2&, uv1, = ImVec2_One), (const ImVec4&, tint_col, = ImVec4_One), (const ImVec4&, border_col, = ImVec4_Zero), { + return ImGui::Image(reinterpret_cast(user_texture_id), image_size, uv0, uv1, tint_col, border_col); + }); + + DEF7_F(ImageButton, (ImString, str_id, ), (long, user_texture_id, ), (const ImVec2&, image_size, ), (const ImVec2&, uv0, = ImVec2_Zero), (const ImVec2&, uv1, = ImVec2_One), (const ImVec4&, bg_col, = ImVec4_Zero), (const ImVec4&, tint_col, = ImVec4_One), { + return ImGui::ImageButton(str_id, reinterpret_cast(user_texture_id), image_size, uv0, uv1, bg_col, tint_col); + }); + DEF3(BeginCombo, (ImString, label, ), (ImString, preview_value, ), (ImGuiComboFlags, flags, = 0)); + DEF0(EndCombo); + m.def( + "Combo", + [](const char* label, int current_item, std::vector items, + int popup_max_height_in_items) { + std::vector items_ptr; + items_ptr.reserve(items.size()); + for (const auto& item : items) { + items_ptr.push_back(item.c_str()); + } + const auto result = + ImGui::Combo(label, ¤t_item, items_ptr.data(), + items_ptr.size(), popup_max_height_in_items); + return std::make_tuple(result, current_item); + }, + py::arg("label"), py::arg("current_item"), py::arg("items"), + py::arg("popup_max_height_in_items") = -1); + m.def( + "ComboStr", + [](const char* label, int current_item, + const char* items_separated_by_zeros, int popup_max_height_in_items) { + const auto result = + ImGui::Combo(label, ¤t_item, items_separated_by_zeros, + popup_max_height_in_items); + return std::make_tuple(result, current_item); + }, + py::arg("label"), py::arg("current_item"), + py::arg("items_separated_by_zeros"), + py::arg("popup_max_height_in_items") = -1); + DEF7_F(DragFloat, (ImString, label, ), (float*, v, ), (float, v_speed, = 1.0f), (float, v_min, = 0.0f), (float, v_max, = 0.0f), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragFloat(label, v, v_speed, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF7_F(DragFloatN, (ImString, label, ), (std::vector, v, ), (float, v_speed, = 1.0f), (float, v_min, = 0.0f), (float, v_max, = 0.0f), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragScalarN(label, ImGuiDataType_Float, v.data(), v.size(), v_speed, &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF7_F(DragInt, (ImString, label, ), (int*, v, ), (float, v_speed, = 1.0f), (int, v_min, = 0), (int, v_max, = 0), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragInt(label, v, v_speed, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF7_F(DragIntN, (ImString, label, ), (std::vector, v, ), (float, v_speed, = 1.0f), (int, v_min, = 0), (int, v_max, = 0), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragScalarN(label, ImGuiDataType_S32, v.data(), v.size(), v_speed, &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF6_F(SliderFloat, (ImString, label, ), (float*, v, ), (float, v_min, ), (float, v_max, ), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderFloat(label, v, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF6_F(SliderFloatN, (ImString, label, ), (std::vector, v, ), (float, v_min, ), (float, v_max, ), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderScalarN(label, ImGuiDataType_Float, v.data(), v.size(), &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF6_F(SliderAngle, (ImString, label, ), (float*, v_rad, ), (float, v_degrees_min, = -360.0f), (float, v_degrees_max, = +360.0f), (ImString, format, = "%.0f deg"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderAngle(label, v_rad, v_degrees_min, v_degrees_max, format, flags); + return std::make_tuple(result, *v_rad); + }); + DEF6_F(SliderInt, (ImString, label, ), (int*, v, ), (int, v_min, ), (int, v_max, ), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderInt(label, v, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF6_F(SliderIntN, (ImString, label, ), (std::vector, v, ), (int, v_min, ), (int, v_max, ), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderScalarN(label, ImGuiDataType_S32, v.data(), v.size(), &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF6_F(InputFloat, (ImString, label, ), (float*, v, ), (float, step, = 0.0f), (float, step_fast, = 0.0f), (ImString, format, = "%.3f"), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputFloat(label, v, step, step_fast, format, flags); + return std::make_tuple(result, *v); + }); + DEF4_F(InputFloatN, (ImString, label, ), (std::vector, v, ), (ImString, format, = "%.3f"), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputScalarN(label, ImGuiDataType_Float, v.data(), v.size(), NULL, NULL, format, flags); + return std::make_tuple(result, v); + }); + DEF5_F(InputInt, (ImString, label, ), (int*, v, ), (int, step, = 1), (int, step_fast, = 100), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputInt(label, v, step, step_fast, flags); + return std::make_tuple(result, *v); + }); + DEF3_F(InputIntN, (ImString, label, ), (std::vector, v, ), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputScalarN(label, ImGuiDataType_S32, v.data(), v.size(), NULL, NULL, "%d", flags); + return std::make_tuple(result, v); + }); + DEF6_F(InputDouble, (ImString, label, ), (double*, v, ), (double, step, = 0.0), (double, step_fast, = 0.0), (ImString, format, = "%.6f"), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputDouble(label, v, step, step_fast, format, flags); + return std::make_tuple(result, *v); + }); + DEF3_F(InputText, (ImString, label, ), (std::string, text, ), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputText(label, &text, flags); + return std::make_tuple(result, text); + }); + DEF4_F(InputTextMultiline, (ImString, label, ), (std::string, text, ), (const ImVec2&, size, = ImVec2_Zero), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputTextMultiline(label, &text, size, flags); + return std::make_tuple(result, text); + }); + DEF4_F(InputTextWithHint, (ImString, label, ), (ImString, hint, ), (std::string, text, ), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputTextWithHint(label, hint, &text, flags); + return std::make_tuple(result, text); + }); + DEF3_F(ColorEdit3, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), { + const auto result = ImGui::ColorEdit3(label, col.data(), flags); + return std::make_tuple(result, col); + }); + DEF3_F(ColorEdit4, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), { + const auto result = ImGui::ColorEdit4(label, col.data(), flags); + return std::make_tuple(result, col); + }); + DEF3_F(ColorPicker3, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), { + const auto result = ImGui::ColorPicker3(label, col.data(), flags); + return std::make_tuple(result, col); + }); + DEF4_F(ColorPicker4, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), (const float*, ref_col, = nullptr), { + const auto result = ImGui::ColorPicker4(label, col.data(), flags, ref_col); + return std::make_tuple(result, col); + }); + DEF4(ColorButton, (ImString, desc_id, ), (const ImVec4&, col, ), (ImGuiColorEditFlags, flags, = 0), (const ImVec2&, size, = ImVec2_Zero)); + DEF1(SetColorEditOptions, (ImGuiColorEditFlags, flags, )); + DEF1(TreeNode, (ImString, label, )); + DEF2_F(TreeNode, (ImString, str_id, ), (ImString, txt, ), { + return ImGui::TreeNode(str_id, "%s", txt); + }); + DEF2(TreeNodeEx, (ImString, label, ), (ImGuiTreeNodeFlags, flags, = 0)); + DEF3_F(TreeNodeEx, (ImString, str_id, ), (ImGuiTreeNodeFlags, flags, ), (ImString, txt, ), { + return ImGui::TreeNodeEx(str_id, flags, "%s", txt); + }); + DEF1(TreePush, (ImString, str_id, )); + DEF0(TreePop); + DEF0(GetTreeNodeToLabelSpacing); + DEF2(CollapsingHeader, (ImString, label, ), (ImGuiTreeNodeFlags, flags, = 0)); + DEF3_F(CollapsingHeader2, (ImString, label, ), (bool*, p_visible, ), (ImGuiTreeNodeFlags, flags, = 0), { + const auto result = ImGui::CollapsingHeader(label, p_visible, flags); + return std::make_tuple(result, *p_visible); + }); + DEF2(SetNextItemOpen, (bool, is_open, ), (ImGuiCond, cond, = 0)); + DEF4(Selectable, (ImString, label, ), (bool, selected, = false), (ImGuiSelectableFlags, flags, = 0), (const ImVec2&, size, = ImVec2_Zero)); + DEF4_F(Selectable2, (ImString, label, ), (bool*, p_selected, ), (ImGuiSelectableFlags, flags, = 0), (const ImVec2&, size, = ImVec2_Zero), { + const auto result = ImGui::Selectable(label, p_selected, flags, size); + return std::make_tuple(result, *p_selected); + }); + DEF2(BeginListBox, (ImString, label, ), (const ImVec2&, size, = ImVec2_Zero)); + DEF0(EndListBox); + DEF2(Value, (ImString, prefix, ), (bool, b, )); + DEF2(Value, (ImString, prefix, ), (int, v, )); + DEF2(Value, (ImString, prefix, ), (unsigned int, v, )); + DEF3(Value, (ImString, prefix, ), (float, v, ), (ImString, float_format, = nullptr)); + DEF0(BeginMenuBar); + DEF0(EndMenuBar); + DEF0(BeginMainMenuBar); + DEF0(EndMainMenuBar); + DEF2(BeginMenu, (ImString, label, ), (bool, enabled, = true)); + DEF0(EndMenu); + DEF4(MenuItem, (ImString, label, ), (ImString, shortcut, = nullptr), (bool, selected, = false), (bool, enabled, = true)); + DEF4_F(MenuItem, (ImString, label, ), (ImString, shortcut, ), (bool*, p_selected, ), (bool, enabled, = true), { + const auto result = ImGui::MenuItem(label, shortcut, p_selected, enabled); + return std::make_tuple(result, *p_selected); + }); + DEF0(BeginTooltip); + DEF0(EndTooltip); + DEF1_F(SetTooltip, (ImString, txt, ), { + return ImGui::SetTooltip("%s", txt); + }); + DEF0(BeginItemTooltip); + DEF1_F(SetItemTooltip, (ImString, txt, ), { + return ImGui::SetItemTooltip("%s", txt); + }); + DEF2(BeginPopup, (ImString, str_id, ), (ImGuiWindowFlags, flags, = 0)); + DEF3_F(BeginPopupModal, (ImString, name, ), (bool*, p_open, = nullptr), (ImGuiWindowFlags, flags, = 0), { + const auto result = ImGui::BeginPopupModal(name, p_open, flags); + return std::make_tuple(result, *p_open); + }); + DEF0(EndPopup); + DEF2(OpenPopup, (ImString, str_id, ), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(OpenPopup, (ImGuiID, id, ), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(OpenPopupOnItemClick, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF0(CloseCurrentPopup); + DEF2(BeginPopupContextItem, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(BeginPopupContextWindow, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(BeginPopupContextVoid, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(IsPopupOpen, (ImString, str_id, ), (ImGuiPopupFlags, flags, = 0)); + DEF5(BeginTable, (ImString, str_id, ), (int, columns, ), (ImGuiTableFlags, flags, = 0), (const ImVec2&, outer_size, = ImVec2_Zero), (float, inner_width, = 0.0f)); + DEF0(EndTable); + DEF2(TableNextRow, (ImGuiTableRowFlags, row_flags, = 0), (float, min_row_height, = 0.0f)); + DEF0(TableNextColumn); + DEF1(TableSetColumnIndex, (int, column_n, )); + DEF4(TableSetupColumn, (ImString, label, ), (ImGuiTableColumnFlags, flags, = 0), (float, init_width_or_weight, = 0.0f), (ImGuiID, user_id, = 0)); + DEF2(TableSetupScrollFreeze, (int, cols, ), (int, rows, )); + DEF1(TableHeader, (ImString, label, )); + DEF0(TableHeadersRow); + DEF0(TableAngledHeadersRow); + DEF0(TableGetColumnCount); + DEF0(TableGetColumnIndex); + DEF0(TableGetRowIndex); + DEF1(TableGetColumnName, (int, column_n, = -1)); + DEF1(TableGetColumnFlags, (int, column_n, = -1)); + DEF2(TableSetColumnEnabled, (int, column_n, ), (bool, v, )); + DEF0(TableGetHoveredColumn); + DEF3(TableSetBgColor, (ImGuiTableBgTarget, target, ), (ImU32, color, ), (int, column_n, = -1)); + DEF3(Columns, (int, count, = 1), (ImString, id, = nullptr), (bool, border, = true)); + DEF0(NextColumn); + DEF0(GetColumnIndex); + DEF1(GetColumnWidth, (int, column_index, = -1)); + DEF2(SetColumnWidth, (int, column_index, ), (float, width, )); + DEF1(GetColumnOffset, (int, column_index, = -1)); + DEF2(SetColumnOffset, (int, column_index, ), (float, offset_x, )); + DEF0(GetColumnsCount); + DEF2(BeginTabBar, (ImString, str_id, ), (ImGuiTabBarFlags, flags, = 0)); + DEF0(EndTabBar); + DEF3_F(BeginTabItem, (ImString, label, ), (bool*, p_open, ), (ImGuiTabItemFlags, flags, = 0), { + const auto result = ImGui::BeginTabItem(label, p_open, flags); + return std::make_tuple(result, p_open ? *p_open : true); + }); + // Convenience overload for non-closable tab items. + // This function returns a boolean so you can call it directly in an if condition. + DEF2_F(BeginTabItem, (ImString, label, ), (ImGuiTabItemFlags, flags, = 0), { + return ImGui::BeginTabItem(label, nullptr, flags); + }); + DEF0(EndTabItem); + DEF2(TabItemButton, (ImString, label, ), (ImGuiTabItemFlags, flags, = 0)); + DEF1(SetTabItemClosed, (ImString, tab_or_docked_window_label, )); + DEF2(SetNextWindowDockID, (ImGuiID, dock_id, ), (ImGuiCond, cond, = 0)); + DEF0(GetWindowDockID); + DEF0(IsWindowDocked); + DEF1(BeginDisabled, (bool, disabled, = true)); + DEF0(EndDisabled); + DEF3(PushClipRect, (const ImVec2&, clip_rect_min, ), (const ImVec2&, clip_rect_max, ), (bool, intersect_with_current_clip_rect, )); + DEF0(PopClipRect); + DEF0(SetItemDefaultFocus); + DEF1(SetKeyboardFocusHere, (int, offset, = 0)); + DEF0(SetNextItemAllowOverlap); + DEF1(IsItemHovered, (ImGuiHoveredFlags, flags, = 0)); + DEF0(IsItemActive); + DEF0(IsItemFocused); + DEF1(IsItemClicked, (ImGuiMouseButton, mouse_button, = 0)); + DEF0(IsItemVisible); + DEF0(IsItemEdited); + DEF0(IsItemActivated); + DEF0(IsItemDeactivated); + DEF0(IsItemDeactivatedAfterEdit); + DEF0(IsItemToggledOpen); + DEF0(IsAnyItemHovered); + DEF0(IsAnyItemActive); + DEF0(IsAnyItemFocused); + DEF0(GetItemID); + DEF0(GetItemRectMin); + DEF0(GetItemRectMax); + DEF0(GetItemRectSize); + DEF1(IsRectVisible, (const ImVec2&, size, )); + DEF2(IsRectVisible, (const ImVec2&, rect_min, ), (const ImVec2&, rect_max, )); + DEF0(GetTime); + DEF0(GetFrameCount); + DEF4(CalcTextSize, (ImString, text, ), (ImString, text_end, = nullptr), (bool, hide_text_after_double_hash, = false), (float, wrap_width, = -1.0f)); + DEF1(ColorConvertU32ToFloat4, (ImU32, in, )); + DEF1(ColorConvertFloat4ToU32, (const ImVec4&, in, )); + DEF6(ColorConvertRGBtoHSV, (float, r, ), (float, g, ), (float, b, ), (float&, out_h, ), (float&, out_s, ), (float&, out_v, )); + DEF6(ColorConvertHSVtoRGB, (float, h, ), (float, s, ), (float, v, ), (float&, out_r, ), (float&, out_g, ), (float&, out_b, )); + DEF1(IsKeyDown, (ImGuiKey, key, )); + DEF2(IsKeyPressed, (ImGuiKey, key, ), (bool, repeat, = true)); + DEF1(IsKeyReleased, (ImGuiKey, key, )); + DEF1(IsKeyChordPressed, (ImGuiKeyChord, key_chord, )); + DEF3(GetKeyPressedAmount, (ImGuiKey, key, ), (float, repeat_delay, ), (float, rate, )); + DEF1(GetKeyName, (ImGuiKey, key, )); + DEF1(SetNextFrameWantCaptureKeyboard, (bool, want_capture_keyboard, )); + DEF2(Shortcut, (ImGuiKeyChord, key_chord, ), (ImGuiInputFlags, flags, = 0)); + DEF2(SetNextItemShortcut, (ImGuiKeyChord, key_chord, ), (ImGuiInputFlags, flags, = 0)); + DEF1(IsMouseDown, (ImGuiMouseButton, button, )); + DEF2(IsMouseClicked, (ImGuiMouseButton, button, ), (bool, repeat, = false)); + DEF1(IsMouseReleased, (ImGuiMouseButton, button, )); + DEF1(IsMouseDoubleClicked, (ImGuiMouseButton, button, )); + DEF1(GetMouseClickedCount, (ImGuiMouseButton, button, )); + DEF3(IsMouseHoveringRect, (const ImVec2&, r_min, ), (const ImVec2&, r_max, ), (bool, clip, = true)); + DEF1(IsMousePosValid, (const ImVec2*, mouse_pos, = nullptr)); + DEF0(IsAnyMouseDown); + DEF0(GetMousePos); + DEF0(GetMousePosOnOpeningCurrentPopup); + DEF2(IsMouseDragging, (ImGuiMouseButton, button, ), (float, lock_threshold, = -1.0f)); + DEF2(GetMouseDragDelta, (ImGuiMouseButton, button, = 0), (float, lock_threshold, = -1.0f)); + DEF1(ResetMouseDragDelta, (ImGuiMouseButton, button, = 0)); + DEF0(GetMouseCursor); + DEF1(SetMouseCursor, (ImGuiMouseCursor, cursor_type, )); + DEF1(SetNextFrameWantCaptureMouse, (bool, want_capture_mouse, )); + DEF0(GetClipboardText); + DEF1(SetClipboardText, (ImString, text, )); +} + +// NOLINTEND(whitespace/line_length) diff --git a/python/mujoco/experimental/dear_imgui/dear_imgui_macros.h b/python/mujoco/experimental/dear_imgui/dear_imgui_macros.h new file mode 100644 index 00000000..159b81e5 --- /dev/null +++ b/python/mujoco/experimental/dear_imgui/dear_imgui_macros.h @@ -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(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_ diff --git a/python/mujoco/experimental/implot/implot.cc b/python/mujoco/experimental/implot/implot.cc new file mode 100644 index 00000000..a174ea8c --- /dev/null +++ b/python/mujoco/experimental/implot/implot.cc @@ -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 +#include +#include +#include +#include + +// 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_(m, "Point") + .def(py::init<>()) + .def(py::init(), py::arg("_x"), py::arg("_y")) + .def_readwrite("x", &ImPlotPoint::x) + .def_readwrite("y", &ImPlotPoint::y); + + py::class_(m, "Range") + .def(py::init<>()) + .def(py::init(), py::arg("_min"), py::arg("_max")) + .def_readwrite("min", &ImPlotRange::Min) + .def_readwrite("max", &ImPlotRange::Max); + +// py::class_(m, "Rect") +// .def(py::init<>()) +// .def(py::init(), py::arg("_x"), py::arg("_y")) +// .def_readwrite("x", &ImPlotRect::X) +// .def_readwrite("y", &ImPlotRect::Y); + + // Enumerations. + + py::enum_(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_(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_(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_(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_(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_(m, "MouseTextFlags") + .value("None", ImPlotMouseTextFlags_None) + .value("NoAuxAxes", ImPlotMouseTextFlags_NoAuxAxes) + .value("NoFormat", ImPlotMouseTextFlags_NoFormat) + .value("ShowAlways", ImPlotMouseTextFlags_ShowAlways); + + py::enum_(m, "DragToolFlags") + .value("None", ImPlotDragToolFlags_None) + .value("NoCursors", ImPlotDragToolFlags_NoCursors) + .value("NoFit", ImPlotDragToolFlags_NoFit) + .value("NoInputs", ImPlotDragToolFlags_NoInputs) + .value("Delayed", ImPlotDragToolFlags_Delayed); + + py::enum_(m, "ColormapScaleFlags") + .value("None", ImPlotColormapScaleFlags_None) + .value("NoLabel", ImPlotColormapScaleFlags_NoLabel) + .value("Opposite", ImPlotColormapScaleFlags_Opposite) + .value("Invert", ImPlotColormapScaleFlags_Invert); + + py::enum_(m, "ItemFlags") + .value("None", ImPlotItemFlags_None) + .value("NoLegend", ImPlotItemFlags_NoLegend) + .value("NoFit", ImPlotItemFlags_NoFit); + + py::enum_(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_(m, "ScatterFlags") + .value("None", ImPlotScatterFlags_None) + .value("NoClip", ImPlotScatterFlags_NoClip); + + py::enum_(m, "StairsFlags") + .value("None", ImPlotStairsFlags_None) + .value("PreStep", ImPlotStairsFlags_PreStep) + .value("Shaded", ImPlotStairsFlags_Shaded); + + py::enum_(m, "ShadedFlags") + .value("None", ImPlotShadedFlags_None); + + py::enum_(m, "BarsFlags") + .value("None", ImPlotBarsFlags_None) + .value("Horizontal", ImPlotBarsFlags_Horizontal); + + py::enum_(m, "BarGroupsFlags") + .value("None", ImPlotBarGroupsFlags_None) + .value("Horizontal", ImPlotBarGroupsFlags_Horizontal) + .value("Stacked", ImPlotBarGroupsFlags_Stacked); + + py::enum_(m, "ErrorBarsFlags") + .value("None", ImPlotErrorBarsFlags_None) + .value("Horizontal", ImPlotErrorBarsFlags_Horizontal); + + py::enum_(m, "StemsFlags") + .value("None", ImPlotStemsFlags_None) + .value("Horizontal", ImPlotStemsFlags_Horizontal); + + py::enum_(m, "InfLinesFlags") + .value("None", ImPlotInfLinesFlags_None) + .value("Horizontal", ImPlotInfLinesFlags_Horizontal); + + py::enum_(m, "PieChartFlags") + .value("None", ImPlotPieChartFlags_None) + .value("Normalize", ImPlotPieChartFlags_Normalize) + .value("IgnoreHidden", ImPlotPieChartFlags_IgnoreHidden) + .value("Exploding", ImPlotPieChartFlags_Exploding); + + py::enum_(m, "HeatmapFlags") + .value("None", ImPlotHeatmapFlags_None) + .value("ColMajor", ImPlotHeatmapFlags_ColMajor); + + py::enum_(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_(m, "DigitalFlags") + .value("ImPlotNone", ImPlotDigitalFlags_None); + + py::enum_(m, "ImageFlags") + .value("None", ImPlotImageFlags_None); + + py::enum_(m, "TextFlags") + .value("None", ImPlotTextFlags_None) + .value("Vertical", ImPlotTextFlags_Vertical); + + py::enum_(m, "DummyFlags") + .value("None", ImPlotDummyFlags_None); + + py::enum_(m, "Cond") + .value("None", ImPlotCond_None) + .value("Always", ImPlotCond_Always) + .value("Once", ImPlotCond_Once); + + py::enum_(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_(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_(m, "Scale") + .value("ImPlotScale_Linear", ImPlotScale_Linear) + .value("ImPlotScale_Time", ImPlotScale_Time) + .value("ImPlotScale_Log10", ImPlotScale_Log10) + .value("ImPlotScale_SymLog", ImPlotScale_SymLog); + + py::enum_(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_(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, values, ), (std::vector, labels, ), (bool, keep_default, = false), { + std::vector 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, xs, ), (std::vector, 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, xs, ), (std::vector, 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, xs, ), (std::vector, 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, xs, ), (std::vector, 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, xs, ), (std::vector, ys1, ), (std::vector, 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, xs, ), (std::vector, 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, xs, ), (std::vector, ys, ), (std::vector, 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, xs, ), (std::vector, ys, ), (std::vector, neg, ), (std::vector, 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, xs, ), (std::vector, 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, xs, ), (std::vector, 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) diff --git a/python/mujoco/experimental/studio/native_viewer.cc b/python/mujoco/experimental/studio/native_viewer.cc new file mode 100644 index 00000000..e4379862 --- /dev/null +++ b/python/mujoco/experimental/studio/native_viewer.cc @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#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 +#include +#include +#include + +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 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 buffer(file_size); + if (!file.read(reinterpret_cast(buffer.data()), file_size)) { + return {}; + } + return buffer; +} + +// Holds loaded resource data for the MuJoCo resource provider. +struct ResourceData { + std::vector 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(data->bytes.size()); + }; + resource_provider.read = [](mjResource* resource, const void** buffer) { + auto* data = static_cast(resource->data); + *buffer = data->bytes.data(); + return static_cast(data->bytes.size()); + }; + resource_provider.close = [](mjResource* resource) { + delete static_cast(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("PyStudio " + title, + width, height, config); + ImPlot::CreateContext(); + + renderer_ = std::make_unique( + 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 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& 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 window_; + std::unique_ptr renderer_; + std::vector pixels_; +}; + +PYBIND11_MODULE(native_viewer_cc, m) { + pybind11::class_(m, "Viewer") + .def(pybind11::init()) + .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); +} diff --git a/python/mujoco/experimental/studio/native_viewer.py b/python/mujoco/experimental/studio/native_viewer.py new file mode 100644 index 00000000..dd9afb37 --- /dev/null +++ b/python/mujoco/experimental/studio/native_viewer.py @@ -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, + ) diff --git a/python/mujoco/experimental/studio/parser.cc b/python/mujoco/experimental/studio/parser.cc new file mode 100644 index 00000000..c1e191ce --- /dev/null +++ b/python/mujoco/experimental/studio/parser.cc @@ -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 + +#include +#include "third_party/mujoco/src/experimental/platform/sim/model_holder.h" +#include "structs.h" +#include + +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(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); +} diff --git a/python/mujoco/experimental/studio/renderer.cc b/python/mujoco/experimental/studio/renderer.cc new file mode 100644 index 00000000..b121e1e9 --- /dev/null +++ b/python/mujoco/experimental/studio/renderer.cc @@ -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 +#include +#include +#include +#include + +#include +#include "third_party/mujoco/src/experimental/platform/hal/graphics_mode.h" +#include "structs.h" +#include +#include +#include +#include + +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(nullptr, mode); + } + + void Init(const MjModelWrapper& model) { impl_->Init(model.get()); } + + pybind11::bytes Render(const MjModelWrapper& model, MjDataWrapper& data, + std::optional& perturb, + std::optional& camera, + std::optional& vis_option, int width, + int height) { + std::vector 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(mjNRNDFLAG)}, + {sizeof(mjtByte)}); + } + + private: + std::unique_ptr impl_; +}; + +} // namespace mujoco::python + +PYBIND11_MODULE(renderer, m) { + pybind11::class_(m, "Renderer") + .def(pybind11::init()) + .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>()); +} diff --git a/python/mujoco/experimental/studio/sample/async.py b/python/mujoco/experimental/studio/sample/async.py new file mode 100644 index 00000000..33f6d255 --- /dev/null +++ b/python/mujoco/experimental/studio/sample/async.py @@ -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) diff --git a/python/mujoco/experimental/studio/sample/implot.py b/python/mujoco/experimental/studio/sample/implot.py new file mode 100644 index 00000000..a801fccb --- /dev/null +++ b/python/mujoco/experimental/studio/sample/implot.py @@ -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) diff --git a/python/mujoco/experimental/studio/sample/render.py b/python/mujoco/experimental/studio/sample/render.py new file mode 100644 index 00000000..c373537c --- /dev/null +++ b/python/mujoco/experimental/studio/sample/render.py @@ -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) diff --git a/python/mujoco/experimental/studio/sim.cc b/python/mujoco/experimental/studio/sim.cc new file mode 100644 index 00000000..ae75612b --- /dev/null +++ b/python/mujoco/experimental/studio/sim.cc @@ -0,0 +1,65 @@ +// 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 +#include "third_party/mujoco/src/experimental/platform/sim/step_control.h" +#include "structs.h" +#include + +namespace py = pybind11; + +using StepControl = mujoco::platform::StepControl; + +PYBIND11_MODULE(sim, m) { + m.doc() = "MuJoCo platform simulation bindings for Link."; + + py::enum_(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_(m, "PauseState") + .value("UNPAUSED", StepControl::PauseState::kUnpaused) + .value("NORMAL_PAUSED", StepControl::PauseState::kNormalPaused) + .value("VISCOUS_PAUSED", StepControl::PauseState::kViscousPaused); + + py::class_(m, "StepControl") + .def(py::init<>()) + .def( + "advance", + [](StepControl& self, mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data) { + return self.Advance(model.get(), data.get()); + }, + py::arg("model"), py::arg("data"), + "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."); +} diff --git a/python/mujoco/experimental/studio/studio.py b/python/mujoco/experimental/studio/studio.py new file mode 100644 index 00000000..b21af916 --- /dev/null +++ b/python/mujoco/experimental/studio/studio.py @@ -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) diff --git a/python/mujoco/experimental/studio/studio_app.py b/python/mujoco/experimental/studio/studio_app.py new file mode 100644 index 00000000..4b50ea25 --- /dev/null +++ b/python/mujoco/experimental/studio/studio_app.py @@ -0,0 +1,440 @@ +# 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 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 + + +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) -> None: + """Applies the purturbations and advances the physics.""" + self.apply_perturb(perturb) + + 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 = '', + ) -> 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 purturbations 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. + + 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) + return self.is_running() + + def update_from_viewer(self, viewer: viewer_protocol.Viewer) -> bool: + """Convenience wrapper around update() that unpacks viewer attributes.""" + return self.update( + viewer.camera, + viewer.vis_options, + viewer.perturb, + drop_file=viewer.get_drop_file(), + ) + + 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) diff --git a/python/mujoco/experimental/studio/studio_app_events.py b/python/mujoco/experimental/studio/studio_app_events.py new file mode 100644 index 00000000..45397c0e --- /dev/null +++ b/python/mujoco/experimental/studio/studio_app_events.py @@ -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 + ) diff --git a/python/mujoco/experimental/studio/ux.cc b/python/mujoco/experimental/studio/ux.cc new file mode 100644 index 00000000..8827a70f --- /dev/null +++ b/python/mujoco/experimental/studio/ux.cc @@ -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 +#include +#include +#include +#include + +#include +#include +#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 +#include + +namespace py = pybind11; + +struct UxState { + // Read/edited by step_control_gui + int speed_index = 0; + + // Read/edited by state_gui + std::vector 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 flags = {0}; +}; + +PYBIND11_MODULE(ux, m) { + py::class_(m, "RenderFlags") + .def(py::init<>()) + .def_readwrite("flags", &RenderFlags::flags); + + m.doc() = "MuJoCo platform UX components."; + + py::enum_(m, "GuiTheme") + .value("LIGHT", mujoco::platform::GuiTheme::kLight) + .value("DARK", mujoco::platform::GuiTheme::kDark) + .value("CLASSIC", mujoco::platform::GuiTheme::kClassic); + + py::class_(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_(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(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(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_(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(); + res.point[1] = t[1].cast(); + res.point[2] = t[2].cast(); + }); + + 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."); +} diff --git a/python/mujoco/experimental/studio/viewer_protocol.py b/python/mujoco/experimental/studio/viewer_protocol.py new file mode 100644 index 00000000..7138eba8 --- /dev/null +++ b/python/mujoco/experimental/studio/viewer_protocol.py @@ -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: + ... From 3a5626bc279c6168da3a1f879675b71690d7614f Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Tue, 2 Jun 2026 09:14:33 -0700 Subject: [PATCH 06/15] Align GJK internal memory to avoid misalignment segfaults when compiled as single precision. This is effectively a no-op under double precision. PiperOrigin-RevId: 925389982 Change-Id: I7117779669965ebf72cb77ccbece419acf9b1b96 --- src/engine/engine_collision_gjk.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index 8ad8d782..c662529b 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -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 @@ -2217,10 +2222,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 +2319,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; From 0e4749501c1ea670ea53ffa382025e5a189c4a4e Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Tue, 2 Jun 2026 09:19:30 -0700 Subject: [PATCH 07/15] Allow custom step function in StepControl::Advance. The StepControl::Advance method now accepts an optional std::function to be used instead of mj_step. This allows for custom simulation logic to be executed within the stepping loop. The Python bindings for StepControl::advance have been updated to support passing a Python callable as the custom step function. PiperOrigin-RevId: 925393132 Change-Id: Id2820c4b55cbdfee6b01adc2395e99a990d4e7a3 --- python/mujoco/experimental/studio/sim.cc | 18 ++++++++++++++---- src/experimental/platform/sim/step_control.cc | 10 ++++++++-- src/experimental/platform/sim/step_control.h | 4 +++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/python/mujoco/experimental/studio/sim.cc b/python/mujoco/experimental/studio/sim.cc index ae75612b..c694abaa 100644 --- a/python/mujoco/experimental/studio/sim.cc +++ b/python/mujoco/experimental/studio/sim.cc @@ -42,11 +42,21 @@ PYBIND11_MODULE(sim, m) { .def(py::init<>()) .def( "advance", - [](StepControl& self, mujoco::python::MjModelWrapper& model, - mujoco::python::MjDataWrapper& data) { - return self.Advance(model.get(), data.get()); + [](StepControl& self, py::object model_obj, py::object data_obj, + py::object step_fn) { + auto& model = py::cast(model_obj); + auto& data = py::cast(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("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.") diff --git a/src/experimental/platform/sim/step_control.cc b/src/experimental/platform/sim/step_control.cc index 1f5b54c3..a41a6bbe 100644 --- a/src/experimental/platform/sim/step_control.cc +++ b/src/experimental/platform/sim/step_control.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -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) { diff --git a/src/experimental/platform/sim/step_control.h b/src/experimental/platform/sim/step_control.h index 1f34a810..1717c975 100644 --- a/src/experimental/platform/sim/step_control.h +++ b/src/experimental/platform/sim/step_control.h @@ -16,6 +16,7 @@ #define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_SIM_STEP_CONTROL_H_ #include +#include #include #include @@ -24,6 +25,7 @@ namespace mujoco::platform { using Seconds = std::chrono::duration; using Clock = std::chrono::steady_clock; +using StepFn = std::function; // 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(); From 49211a05c58ddaa7af0459d8c30cc5b5c3128a17 Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Tue, 2 Jun 2026 10:27:01 -0700 Subject: [PATCH 08/15] Add support for custom physics step functions in StudioApp. This change allows users to provide an optional `step_fn` callable to `StudioApp.update` and `StudioApp.update_from_viewer`. When provided, this function is called to advance the physics simulation instead of the default `step_control.advance`. PiperOrigin-RevId: 925439890 Change-Id: Ia833503f6dd0c22fb8d75af6c5f06757496853e5 --- .../mujoco/experimental/studio/studio_app.py | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/python/mujoco/experimental/studio/studio_app.py b/python/mujoco/experimental/studio/studio_app.py index 4b50ea25..fe030122 100644 --- a/python/mujoco/experimental/studio/studio_app.py +++ b/python/mujoco/experimental/studio/studio_app.py @@ -34,6 +34,7 @@ See the sample/ folder for usage examples. import os import sys +import typing import mujoco from mujoco.experimental.studio import parser @@ -45,6 +46,9 @@ 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, @@ -246,13 +250,28 @@ class StudioApp: else: mujoco.mjv_applyPerturbPose(self.model, self.data, perturb, 1) - def update_physics(self, perturb: mujoco.MjvPerturb) -> None: - """Applies the purturbations and advances the physics.""" + 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) - advance_status = self.step_control.advance(self.model, self.data) - if advance_status == sim.StepStatus.AUTO_RESET: - self.reset_physics() + 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.""" @@ -272,13 +291,15 @@ class StudioApp: 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 purturbations and advances the physics. + Applies the perturbations and advances the physics. The argument objects are provided by the viewer. Args: @@ -287,6 +308,8 @@ class StudioApp: 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 @@ -297,16 +320,31 @@ class StudioApp: self.handle_mouse_events(camera, vis_options, perturb) self.handle_keyboard_events(camera, vis_options) - self.update_physics(perturb) + self.update_physics(perturb, step_fn=step_fn) return self.is_running() - def update_from_viewer(self, viewer: viewer_protocol.Viewer) -> bool: - """Convenience wrapper around update() that unpacks viewer attributes.""" + 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( From 7b9b88060e5e7f27f62373d60f7895b1eafcc587 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 2 Jun 2026 17:22:15 -0700 Subject: [PATCH 09/15] Refactor `mj_fullM`. This change is part of the deprecation of `mjData.qM`. PiperOrigin-RevId: 925669464 Change-Id: I4889c66591bc1df4c31135a13776052aad491f7a --- doc/APIreference/functions.rst | 9 +-------- doc/APIreference/functions_override.rst | 11 ----------- doc/changelog.rst | 6 ++++++ doc/includes/references.h | 2 +- include/mujoco/mujoco.h | 4 ++-- python/mujoco/functions.cc | 9 +++------ python/mujoco/introspect/functions.py | 14 +++++++------- src/engine/engine_support.c | 15 ++------------- src/engine/engine_support.h | 2 +- test/engine/engine_core_smooth_test.cc | 23 +++++++++++++++++++---- test/engine/engine_derivative_test.cc | 2 +- test/engine/engine_support_test.cc | 6 +++--- unity/Runtime/Bindings/MjBindings.cs | 2 +- wasm/codegen/generated/bindings.cc | 6 ++---- wasm/codegen/generators/constants.py | 1 - 15 files changed, 49 insertions(+), 63 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 10dd03da..0a9e1dd7 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -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 `__ for -pedagogical examples. +Convert sparse inertia matrix into full (i.e. dense) matrix. .. _mj_mulM: diff --git a/doc/APIreference/functions_override.rst b/doc/APIreference/functions_override.rst index 88e90085..1ca383fe 100644 --- a/doc/APIreference/functions_override.rst +++ b/doc/APIreference/functions_override.rst @@ -342,17 +342,6 @@ found, the function will return ``distmax`` and ``fromto``, if given, will be se As explained in :ref:`Collision Detection`, distances are inaccurate when using the :ref:`legacy CCD pipeline`, 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 `__ for -pedagogical examples. - .. _mj_mulM: This function multiplies the joint-space inertia matrix stored in ``mjData.M`` by a vector. diff --git a/doc/changelog.rst b/doc/changelog.rst index 4ef058ea..82259ed9 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -30,6 +30,12 @@ General 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 ^^^^^^^^^ - Fixed a bug in the ``mjz`` :ref:`decoder ` where unnormalized paths would fail to be read. diff --git a/doc/includes/references.h b/doc/includes/references.h index 520785a7..97c61cd9 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -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); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 10c0a6e6..91d59856 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -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); diff --git a/python/mujoco/functions.cc b/python/mujoco/functions.cc index 05dfa362..97c7fb79 100644 --- a/python/mujoco/functions.cc +++ b/python/mujoco/functions.cc @@ -591,15 +591,12 @@ PYBIND11_MODULE(_functions, pymodule) { Def(pymodule); Def( pymodule, - [](const raw::MjModel* m, Eigen::Ref dst, - Eigen::Ref 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 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( pymodule, diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 5fc5cf92..800ed67c 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -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( diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 73e62190..1c1874e6 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -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); } diff --git a/src/engine/engine_support.h b/src/engine/engine_support.h index cf112cb0..0f447608 100644 --- a/src/engine/engine_support.h +++ b/src/engine/engine_support.h @@ -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); diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index df97cd02..f689105a 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -233,13 +233,13 @@ TEST_F(CoreSmoothTest, TendonArmature) { // get full M, includes both CRB and tendon inertia vector 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 M2(nv*nv); - mj_fullM(m, M2.data(), d->qM); + mj_fullM(m, d, M2.data()); vector ten_J(nv); // tendon Jacobian vector ten_M(nv*nv); // tendon inertia @@ -681,7 +681,7 @@ TEST_F(CoreSmoothTest, FactorI) { // dense M matrix vector 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 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++) { diff --git a/test/engine/engine_derivative_test.cc b/test/engine/engine_derivative_test.cc index 471c4540..25221e0d 100644 --- a/test/engine/engine_derivative_test.cc +++ b/test/engine/engine_derivative_test.cc @@ -848,7 +848,7 @@ TEST_F(DerivativeTest, LinearSystemInverse) { // expect that acceleration derivatives are the mass matrix vector 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 diff --git a/test/engine/engine_support_test.cc b/test/engine/engine_support_test.cc index 30bb5dfe..cd753e47 100644 --- a/test/engine/engine_support_test.cc +++ b/test/engine/engine_support_test.cc @@ -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 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 M_CSR(nv * nv); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 49b99f3c..c05b5c4d 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -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); diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index 36cc5dfa..723092d3 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -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) { diff --git a/wasm/codegen/generators/constants.py b/wasm/codegen/generators/constants.py index 7a826e09..6a6ba85a 100644 --- a/wasm/codegen/generators/constants.py +++ b/wasm/codegen/generators/constants.py @@ -565,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": """ From 0fe20648473eaf7cfc04dc5b34e67b40f428b001 Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Wed, 3 Jun 2026 01:05:04 -0700 Subject: [PATCH 10/15] Cleanup Renderable API. Add function for getting the material. Remove unused/unneeded functions. PiperOrigin-RevId: 925847769 Change-Id: Iefaf9112b7419830eb2dc4d1323e9c27725c4b16 --- .../filament/compat/scene_geom_util.cc | 3 +- .../filament/render_context_filament.cc | 20 +++--------- .../filament/render_context_filament.h | 32 ++++++++++++------- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/experimental/filament/compat/scene_geom_util.cc b/src/experimental/filament/compat/scene_geom_util.cc index 17ad8e6d..99255717 100644 --- a/src/experimental/filament/compat/scene_geom_util.cc +++ b/src/experimental/filament/compat/scene_geom_util.cc @@ -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 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); diff --git a/src/experimental/filament/render_context_filament.cc b/src/experimental/filament/render_context_filament.cc index f122110b..19ccee6f 100644 --- a/src/experimental/filament/render_context_filament.cc +++ b/src/experimental/filament/render_context_filament.cc @@ -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)); diff --git a/src/experimental/filament/render_context_filament.h b/src/experimental/filament/render_context_filament.h index 98204f75..3d5e545f 100644 --- a/src/experimental/filament/render_context_filament.h +++ b/src/experimental/filament/render_context_filament.h @@ -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; }; From 4cfdf5f34b54ba0f20b1e0b0d0235fcdfec81a2c Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Wed, 3 Jun 2026 03:30:24 -0700 Subject: [PATCH 11/15] Bind materials before rendering reflection passes. PiperOrigin-RevId: 925911124 Change-Id: I134d29424233ccbf57f56e7379e8830662a57836 --- src/experimental/filament/filament/scene_view.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/experimental/filament/filament/scene_view.cc b/src/experimental/filament/filament/scene_view.cc index f76f7e55..bc4ebf75 100644 --- a/src/experimental/filament/filament/scene_view.cc +++ b/src/experimental/filament/filament/scene_view.cc @@ -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; From 5d231c64cd04716f858a6e35df02f3a7eeae1f53 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Wed, 3 Jun 2026 04:40:22 -0700 Subject: [PATCH 12/15] Import google-deepmind/mujoco_warp from GitHub. PiperOrigin-RevId: 925939529 Change-Id: Idb92c2d50d5be9df7b6e91113b576e9c706847ca --- .../mjx/third_party/mujoco_warp/_src/io.py | 12 +- .../third_party/mujoco_warp/_src/render.py | 770 ++++++++---------- .../third_party/mujoco_warp/_src/support.py | 6 +- .../mjx/third_party/mujoco_warp/_src/types.py | 6 + mjx/mujoco/mjx/warp/bvh.py | 11 +- mjx/mujoco/mjx/warp/collision_driver.py | 8 +- mjx/mujoco/mjx/warp/forward.py | 8 +- mjx/mujoco/mjx/warp/render.py | 13 +- mjx/mujoco/mjx/warp/smooth.py | 8 +- mjx/mujoco/mjx/warp/types.py | 21 +- 10 files changed, 379 insertions(+), 484 deletions(-) diff --git a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/io.py b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/io.py index ffd9f8f3..2810bc79 100644 --- a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/io.py +++ b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/io.py @@ -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) diff --git a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/render.py b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/render.py index 7afb140b..05780e5b 100644 --- a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/render.py +++ b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/render.py @@ -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, ) diff --git a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/support.py b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/support.py index 61e5b219..97fb1f39 100644 --- a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/support.py +++ b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/support.py @@ -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 diff --git a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/types.py b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/types.py index 77c6cca2..4ce5a31c 100644 --- a/mjx/mujoco/mjx/third_party/mujoco_warp/_src/types.py +++ b/mjx/mujoco/mjx/third_party/mujoco_warp/_src/types.py @@ -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 = () diff --git a/mjx/mujoco/mjx/warp/bvh.py b/mjx/mujoco/mjx/warp/bvh.py index 1301aa88..7f648694 100644 --- a/mjx/mujoco/mjx/warp/bvh.py +++ b/mjx/mujoco/mjx/warp/bvh.py @@ -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 diff --git a/mjx/mujoco/mjx/warp/collision_driver.py b/mjx/mujoco/mjx/warp/collision_driver.py index e3d6b595..d3cfd111 100644 --- a/mjx/mujoco/mjx/warp/collision_driver.py +++ b/mjx/mujoco/mjx/warp/collision_driver.py @@ -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 diff --git a/mjx/mujoco/mjx/warp/forward.py b/mjx/mujoco/mjx/warp/forward.py index 8bae7621..5242732d 100644 --- a/mjx/mujoco/mjx/warp/forward.py +++ b/mjx/mujoco/mjx/warp/forward.py @@ -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 diff --git a/mjx/mujoco/mjx/warp/render.py b/mjx/mujoco/mjx/warp/render.py index 92311b58..ae82028f 100644 --- a/mjx/mujoco/mjx/warp/render.py +++ b/mjx/mujoco/mjx/warp/render.py @@ -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, diff --git a/mjx/mujoco/mjx/warp/smooth.py b/mjx/mujoco/mjx/warp/smooth.py index 862b6d07..bf26a123 100644 --- a/mjx/mujoco/mjx/warp/smooth.py +++ b/mjx/mujoco/mjx/warp/smooth.py @@ -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 diff --git a/mjx/mujoco/mjx/warp/types.py b/mjx/mujoco/mjx/warp/types.py index 8f952897..cc8f089d 100644 --- a/mjx/mujoco/mjx/warp/types.py +++ b/mjx/mujoco/mjx/warp/types.py @@ -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,7 +42,6 @@ else: Callback = None PyTreeNode = mjx_dataclasses.PyTreeNode - @dataclasses.dataclass(frozen=True) @tree_util.register_pytree_node_class class TileSet: @@ -58,7 +53,6 @@ class TileSet: adr: address of each tile in the set size: size of all the tiles in this set """ - adr: np.ndarray size: int @@ -101,8 +95,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 @@ -114,6 +108,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 @@ -133,13 +128,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 @@ -154,7 +146,6 @@ class OptionWarp(PyTreeNode): sdf_initpoints: int sdf_iterations: int - class ModelWarp(PyTreeNode): """Derived fields from Model.""" D_colind: np.ndarray @@ -336,7 +327,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 @@ -448,8 +438,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', @@ -478,7 +466,6 @@ DATA_NON_VMAP = { 'nworld', } - def _to_elt(cont, _, d, axis): return DataWarp(**{ f.name: ( @@ -712,6 +699,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, @@ -1347,6 +1335,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, From 558366f3643fd2161ccf1ef6bc71adcc954da375 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Wed, 3 Jun 2026 07:03:29 -0700 Subject: [PATCH 13/15] Always normalize in planeNormal to reduce rounding errors in single precision. PiperOrigin-RevId: 926002329 Change-Id: I682f3b90249e60838c2432231b3181e57c565c28 --- src/engine/engine_collision_gjk.c | 3 ++ test/engine/engine_collision_gjk_test.cc | 65 ++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index c662529b..dac58cfb 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -1538,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); } diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index 71eca6b1..64c51937 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -411,6 +411,65 @@ TEST_F(MjGjkTest, BoxBoxDepth3) { EXPECT_NEAR(dir[2], -1, kTolerance); } + +TEST_F(MjGjkTest, BoxBoxSize05) { + static constexpr char xml[] = R"( + + + + + + )"; + + TestModel model = LoadModel(xml); + TestData data = MakeData(model.get()); + mj_forward(model.get(), data.get()); + + mjtNum* xmat = data->geom_xmat; + mjtNum* xpos = data->geom_xpos; + + xmat[0] = 1.000000000000000; + xmat[1] = 0.000000047289880; + xmat[2] = -0.000000050905665; + xmat[3] = -0.000000047289880; + xmat[4] = 1.000000000000000; + xmat[5] = 0.000000017136196; + xmat[6] = 0.000000050905665; + xmat[7] = -0.000000017136193; + xmat[8] = 1.000000000000000; + + xpos[0] = 0.000000009724202; + xpos[1] = -0.000000014139289; + xpos[2] = 7.369161128997803; + + xmat = data->geom_xmat + 9; + xpos = data->geom_xpos + 3; + + xmat[0] = 1.000000000000000; + xmat[1] = -0.000000013726950; + xmat[2] = 0.000000008946020; + xmat[3] = 0.000000013726950; + xmat[4] = 1.000000000000000; + xmat[5] = -0.000000012039017; + xmat[6] = -0.000000008946020; + xmat[7] = 0.000000012039017; + xmat[8] = 1.000000000000000; + + xpos[0] = 0.000000013445962; + xpos[1] = -0.000000019194527; + xpos[2] = 8.264492034912109; + + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; + std::vector dir, pos; + mjtNum dist; + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); + + ASSERT_EQ(ncons, 4); +} + TEST_F(MjGjkTest, BoxBoxTouching) { static constexpr char xml[] = R"( @@ -424,13 +483,13 @@ TEST_F(MjGjkTest, BoxBoxTouching) { TestData data = MakeData(model.get()); mj_forward(model.get(), data.get()); - int geom1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model.get(), mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model.get(), mjOBJ_GEOM, "geom2"); mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2); ASSERT_EQ(ncons, 0); EXPECT_EQ(status.epa_status, -1); From 45d5f2ed5fd2a00ebf08737c83d728400d6bb6e3 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 3 Jun 2026 07:11:27 -0700 Subject: [PATCH 14/15] Improve link in light/directional docs PiperOrigin-RevId: 926006386 Change-Id: Idfd3eae6c127f29919587ae88aa4291c290e2944 --- doc/XMLreference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 87a6d928..bcf33364 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -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 ` type instead. If set to "true", and + This is a deprecated legacy attribute. Please use light :ref:`type ` instead. If set to "true", and no type is specified, this will change the light type to be directional. .. _body-light-castshadow: From 0897e3aec1dcc66925e30cc58ff83fea97e2289b Mon Sep 17 00:00:00 2001 From: Kevin Zakka Date: Wed, 3 Jun 2026 08:22:49 -0700 Subject: [PATCH 15/15] Skip the prefetcher when the URL scheme isn't fetchable. Drag-and-drop uploads go through Module.loadFile and never reached the prefetcher. This is a guard for future provider schemes (e.g., vfs) that shouldn't be passed to fetch(). PiperOrigin-RevId: 926042158 Change-Id: I119d5b1a7512f8e84bb4c752414797593a1e98cd --- src/experimental/studio/live.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/experimental/studio/live.js b/src/experimental/studio/live.js index 13bd874f..e5f51fff 100644 --- a/src/experimental/studio/live.js +++ b/src/experimental/studio/live.js @@ -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 };