extension.rst 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. .. _exExtension:
  2. Extensions
  3. ----------
  4. This section describes MuJoCo's mechanisms for user-authored extensions. At present, extensibility is provided by
  5. via :ref:`engine plugins<exPlugin>` and :ref:`resource providers<exProvider>`.
  6. .. _exPlugin:
  7. Engine plugins
  8. ~~~~~~~~~~~~~~
  9. Engine plugins, introduced in MuJoCo 2.3.0, allow user-defined logic to be inserted into various parts of MuJoCo's
  10. computational pipeline. For example, custom sensor and actuator types can be implemented as plugins. Plugin features are
  11. referenced in the XML content of an MJCF model, allowing MJCF to remain an abstract physical description of
  12. a system even if the simulation requirements extend beyond MuJoCo's built-in capabilities.
  13. The plugin mechanism was designed to overcome the disadvantages of MuJoCo's :ref:`physics callbacks<glPhysics>`. These
  14. global callbacks (:ref:`usage example<siSimulation>`) are still available and useful for fast prototyping or when
  15. the user wishes to implement functionality in Python, but are generally deprecated as a stable mechanism for extended
  16. functionality. The central features of the plugin mechanism are:
  17. - **Thread safety:** Plugin instances (see below) are thread-local, avoiding collisions.
  18. - **Statefulness:** Plugins can be stateful, and their state will be (de)serialized correctly.
  19. - **Interoperability:** Different plugins can coexist without interference.
  20. Both users and developers of plugins should familiarize themselves with two key concepts:
  21. Plugin
  22. A **plugin** is a collection of functions and static attributes that implement its capabilities, bundled into an
  23. :ref:`mjpPlugin` struct. Plugin functions are **stateless**: they depend only on the
  24. arguments passed to them. When a plugin requires an internal state, it declares this state
  25. and allows MuJoCo to manage it and pass it in. This enables (de)serialization of the full simulation state.
  26. A plugin can therefore be regarded as the "pure logic" part of the functionality and is often bundled as a C library.
  27. A plugin is neither a model element nor is it associated with specific model elements.
  28. Plugin instance
  29. A plugin **instance** represents the self-contained runtime state that is operated on by the
  30. plugin: when the plugin logic is executed, the instance state is passed in by the engine.
  31. A plugin instance is itself a model element of type :ref:`mjOBJ_PLUGIN<mjtObj>`.
  32. There are ``mjModel.nplugin`` instances with id's in ``[0 nplugin-1]``. Like other elements, instances
  33. can have names, with :ref:`mj_name2id` and :ref:`mj_id2name` mapping between id's and names. Unlike the
  34. plugin code which is loaded once into a global table, multiple instances of the same plugin can be defined and have a
  35. one-to-many relationship with other model elements.
  36. **one-to-one:**
  37. In this simplest case, each instance is referenced once in the model. For example,
  38. two sensors may declare that their values are computed by two plugin instances of the same plugin.
  39. In this case, every time the sensor output is computed, the plugin logic will be executed separately.
  40. **one-to-many:**
  41. Alternatively, the behavior of multiple elements can be backed by a single plugin instance. There are
  42. two main scenarios where this is useful:
  43. * The values of different element types are linked to the same physical entity and computation. For example
  44. consider a motor with an internal thermometer. This would manifest as an actuator and sensor, both associated with
  45. the same plugin instance which computes both torque outputs and temperature readings.
  46. * It is advantageous to batch the computation of multiple related elements together, for example where the computed
  47. value is the output of a neural network. The canonical example here is a robot that is equipped with ``N`` motors,
  48. where motor dynamics are modeled as a neural network. In this case, it can be substantially faster to produce the
  49. torque output of all N actuators in a single forward pass than for each motor separately.
  50. Below, we begin by describing plugins from a user perspective:
  51. * Types of plugin capabilities.
  52. * How plugins are declared and configured in an MJCF model.
  53. * How plugin states are incorporated into :ref:`mjData`, and what users need to do to safely duplicate and serialize
  54. :ref:`mjData` structs when plugin instances are present.
  55. Next, we describe the logistics of plugin registration that are relevant to both users and developers of plugins. This
  56. is followed by a section that targets plugin developers.
  57. .. _exCapabilities:
  58. Plugin capabilities
  59. ^^^^^^^^^^^^^^^^^^^
  60. A plugin is described by the contents of its associated :ref:`mjpPlugin` struct. The ``capabilityflags`` member is an
  61. integer bitfield describing the plugin's capabilities, where bit semantics are defined in the enum
  62. :ref:`mjtPluginCapabilityBit`. Using a bitfield allows plugins to support multiple types of computation. The currently
  63. supported plugin capabilities are:
  64. * Actuator plugin
  65. * Sensor plugin
  66. * Passive force plugin
  67. * Signed distance field plugin
  68. Additional capabilities will be added in the future as required.
  69. .. _exDeclaration:
  70. Declaration in MJCF
  71. ^^^^^^^^^^^^^^^^^^^
  72. First, a plugin dependency must be declared through ``<extension><plugin>``. When the model is parsed, if any plugin
  73. is declared but not registered (see below), a model compilation error is raised. If only a single MJCF element is
  74. backed by a plugin, instances can be implicitly created in-place. If multiple elements are backed by the same plugin,
  75. instance declaration must be explicit:
  76. .. code:: xml
  77. <mujoco>
  78. <extension>
  79. <plugin plugin="mujoco.test.simple_sensor_plugin"/>
  80. <plugin plugin="mujoco.test.actuator_sensor_plugin">
  81. <instance name="explicit_instance"/>
  82. </plugin>
  83. </extension>
  84. ...
  85. <sensor>
  86. <plugin name="sensor0" plugin="mujoco.test.simple_sensor_plugin"/>
  87. <plugin name="sensor1" plugin="mujoco.test.simple_sensor_plugin"/>
  88. <plugin name="sensor2" instance="explicit_instance"/>
  89. </sensor>
  90. ...
  91. <actuator>
  92. <plugin name="actuator2" instance="explicit_instance"/>
  93. </actuator>
  94. </mujoco>
  95. In the example above, ``sensor0`` and ``sensor1`` are each backed by a simple plugin that does not share computation
  96. among elements, so an instance is implicitly created for each sensor by directly referencing the plugin identifier.
  97. In contrast, ``sensor2`` and ``actuator2`` are backed by a plugin that shares computation, so they must reference a
  98. shared instance that was explicitly declared.
  99. .. _exConfiguration:
  100. Configuration in MJCF
  101. ^^^^^^^^^^^^^^^^^^^^^
  102. Plugins can declare custom attributes that represent specialized configurable parameters. For example, a DC motor model
  103. may expose the resistance, inductance, and capacitance as configuration attributes. In MJCF, the values of these
  104. attributes can be specified via ``<config>`` elements, where each ``<config>`` has a key and a value. Valid keys and
  105. values are specified by the plugin developers, but are declared to MuJoCo during plugin registration time so that the
  106. MuJoCo model compiler can raise errors for invalid values.
  107. .. code:: xml
  108. <mujoco>
  109. <extension>
  110. <plugin plugin="mujoco.test.simple_actuator_plugin">
  111. <instance name="explicit_instance">
  112. <config key="resistance" value="1.0"/>
  113. <config key="inductance" value="2.0"/>
  114. </instance>
  115. </plugin>
  116. </extension>
  117. ...
  118. <actuator>
  119. <plugin name="actuator0" instance="explicit_instance"/>
  120. <plugin name="actuator1" plugin="mujoco.test.simple_actuator_plugin">
  121. <config key="resistance" value="3.0"/>
  122. <config key="inductance" value="4.0"/>
  123. </plugin>
  124. </actuator>
  125. </mujoco>
  126. In the example above, ``actuator0`` refers to a pre-existing plugin instance that was created and configured via the
  127. ``<instance>`` element, while ``actuator1`` is implicitly creating and configuring a new plugin instance in-place. Note
  128. that it would be an error to add ``<config>`` child elements directly to ``actuator0`` because a new plugin instance is
  129. not being created there.
  130. .. _exPluginState:
  131. Plugin state
  132. ^^^^^^^^^^^^
  133. While plugin code should be stateless, individual plugin instances are permitted to hold time-dependent state that is
  134. intended to evolve alongside MuJoCo physics, for example temperature variables in thermodynamically coupled actuator
  135. models. Separately, it may also be desirable for plugin instances to memoize potentially expensive parts of their
  136. operation. For example, sensor or actuator plugins that are backed by pretrained neural networks will want to preload
  137. their weights at model compilation time. It is important for us to distinguish between these two types of per-instance
  138. plugin payload. The term **plugin state** refers to the time-dependent state of the plugin instance that consists of
  139. *floating point* values, while the term **plugin data** refers to *arbitrary data structures* consisting of memoized
  140. payload that should be considered implementation detail for the plugin's computation.
  141. Crucially, plugin data must be reconstructible only from plugin configuration attributes, the plugin state,
  142. and :ref:`MuJoCo state variables<geState>`. This means that the plugin data is not expected to be serializable, and will
  143. not be serialized by MuJoCo when it copies or stores data. On the other hand, plugin state is considered an integral
  144. part of the physics and must be serialized alongside MuJoCo's other state variables in order for the physics to be
  145. faithfully restored.
  146. Plugins must declare the number of floating point values required for each instance via the ``nstate`` callback of its
  147. :ref:`mjpPlugin` struct. Note that this number can depend on the exact configuration of the instance. During
  148. :ref:`mj_makeData`, MuJoCo allocates the requisite number of slots in the ``plugin_state`` field of :ref:`mjData` for
  149. each plugin instance. The ``plugin_stateadr`` field in :ref:`mjModel` indicates the position within the overall
  150. ``plugin_state`` array at which each plugin instance can find its state values.
  151. Plugin data, however, is entirely opaque from MuJoCo's point of view. During :ref:`mj_makeData`, MuJoCo calls the
  152. ``init`` callback from the relevant :ref:`mjpPlugin`. In this callback, the plugin is permitted to allocate or otherwise
  153. create an arbitrary data structure that it requires to function and stores its pointer in the ``plugin_data`` field of
  154. :ref:`mjData` that is being created. During :ref:`mj_deleteData`, MuJoCo calls the ``destroy`` callback from the same
  155. :ref:`mjpPlugin`, and the plugin is responsible for deallocating its internal resources associated with the instance.
  156. When :ref:`mjData` is being copied via :ref:`mj_copyData`, MuJoCo will copy over the plugin state. However, the plugin
  157. code is responsible for setting up the plugin data for the newly copied :ref:`mjData`. To facilitate this, MuJoCo calls
  158. the ``copy`` callback from :ref:`mjpPlugin` for each plugin instance present.
  159. .. _exActuatorAct:
  160. Actuator activations
  161. """"""""""""""""""""
  162. When writing stateful actuator plugins, there are two choices for where to save the actuator state. One option is using
  163. ``plugin_state`` as described above, and the other is to use ``mjData.act`` by implementing the ``actuator_actdim`` and
  164. ``actuator_act_dot`` callbacks on :ref:`mjpPlugin`.
  165. When using the latter option, the actuator plugin's state will be added to ``mjData.act``, and MuJoCo will
  166. automatically integrate ``mjData.act_dot`` values between timesteps. One advantage of this approach is that
  167. finite-differencing functions like :ref:`mjd_transitionFD` will work as they do for native actuators. The
  168. ``mjpPlugin.advance`` callback will be called after ``act_dot`` is integrated, and actuator plugins may overwrite
  169. the ``act`` values at that point, if Euler integration isn't appropriate.
  170. Users may specify the :ref:`dyntype<actuator-plugin-dyntype>` attribute on actuator plugins, to introduce a filter or
  171. an integrator between user inputs and actuator activations. When they do, the activation variable introduced by
  172. ``dyntype`` will be placed *after* the plugin's activation variables in the ``act`` array.
  173. .. _exRegistration:
  174. Registration
  175. ^^^^^^^^^^^^
  176. Plugins must be registered with MuJoCo before they can be referenced in MJCF models.
  177. One-off plugins that are intended to support a specific application (or throwaway plugins that are implemented to help
  178. troubleshoot issues with a model) can be statically linked into the application. This can be as simple as preparing an
  179. :ref:`mjpPlugin` struct in the ``main`` function, then passing it to :ref:`mjp_registerPlugin` to be registered with
  180. MuJoCo.
  181. Generally, reusable plugins are expected to be packaged as dynamic libraries. A dynamic library containing one or more
  182. MuJoCo plugins should make sure that all plugins are registered when the library is loaded. In GCC-compatible compilers,
  183. this can be achieved by calling :ref:`mjp_registerPlugin` in a function that is declared with
  184. ``__attribute__((constructor))``, while in MSVC this can be done in a DLL entry point (canonically known as
  185. ``DllMain``). MuJoCo provides a convenience macro :ref:`mjPLUGIN_LIB_INIT` that expands to either of these
  186. constructs depending on the compiler used.
  187. Users of plugins that are delivered as dynamic libraries as described above can load the library using the function
  188. :ref:`mj_loadPluginLibrary`. This is the preferred way to load dynamic libraries containing MuJoCo plugins (rather than,
  189. say, calling ``dlopen`` or ``LoadLibraryA`` directly) since the exact way in which MuJoCo expects dynamic libraries to
  190. auto-register plugins may change over time, but :ref:`mj_loadPluginLibrary` is expected to also evolve to reflect the
  191. best practices.
  192. For applications that need to be able to load arbitrary user-provided MJCF models, it may be desirable to automatically
  193. scan and load all dynamic libraries found without a specific directory. Users who bring along an MJCF that requires a
  194. plugin can then be instructed to place the requisite plugin libraries in the relevant directory. For example, this is
  195. what is done in the :ref:`saSimulate` interactive viewer application. The :ref:`mj_loadAllPluginLibraries` function is
  196. provided for this scan-and-load use case.
  197. .. _exWriting:
  198. Writing plugins
  199. ^^^^^^^^^^^^^^^
  200. This section, targeted at developers, is incomplete. We encourage people who wish to write their own plugins
  201. to contact the MuJoCo development team for help. A good starting point for experienced developers is the
  202. `associated tests <https://github.com/google-deepmind/mujoco/blob/main/test/engine/engine_plugin_test.cc>`_ and the
  203. first-party plugins in the `first-party plugin directory <https://github.com/google-deepmind/mujoco/tree/main/plugin>`_.
  204. A future version of this section will include:
  205. * The content of the :ref:`mjpPlugin` struct.
  206. * Which functions and properties need to be provided in order to define a plugin.
  207. * How to declare custom MJCF attributes for a plugin.
  208. * Things that developers need to keep in mind in order to ensure that plugins function correctly when :ref:`mjData` is
  209. copied, stepped, or reset.
  210. There are several first-party plugin directories:
  211. * **actuator:** The plugins in the `actuator/ <https://github.com/google-deepmind/mujoco/tree/main/plugin/actuator>`__
  212. directory implement custom actuators, so far only a PID controller. See the
  213. `README <https://github.com/google-deepmind/mujoco/blob/main/plugin/actuator/README.md>`__ for details.
  214. * **elasticity:** The plugins in the `elasticity/
  215. <https://github.com/google-deepmind/mujoco/tree/main/plugin/elasticity>`__ directory are passive forces based on
  216. continuum mechanics for 1-dimensional and 2-dimensional bodies. The 1D model is invariant under rotations and captures
  217. the large deformation of elastic cables, decoupling twisting and bending strains. The 2D model is a suitable for
  218. computing the bending stiffness of thin elastic plates (i.e. shells having a flat stress-free configuration). In this
  219. case, the elastic energy is quadratic and therefore the stiffness matrix is constant. For more information, please see
  220. the `README <https://github.com/google-deepmind/mujoco/blob/main/plugin/elasticity/README.md>`__.
  221. * **sensor:** The plugins in the `sensor/ <https://github.com/google-deepmind/mujoco/tree/main/plugin/sensor>`__
  222. directory implement custom sensors. Currently the sole sensor plugin is the touch grid sensor, see the
  223. `README <https://github.com/google-deepmind/mujoco/blob/main/plugin/sensor/README.md>`__ for details.
  224. * **sdf:** The plugins in the `sdf/ <https://github.com/google-deepmind/mujoco/tree/main/plugin/sdf>`__ directory
  225. specify custom shapes in a mesh-free manner, by defining methods computing a signed distance field and its gradient at
  226. query points. This shape then acts as a new geom type in the collision table at the top of `engine_collision_driver.c
  227. <https://github.com/google-deepmind/mujoco/blob/main/src/engine/engine_collision_driver.c>`__. For more information
  228. concerning the available SDFs and how to write your own implicit geometry, please see the `README
  229. <https://github.com/google-deepmind/mujoco/blob/main/plugin/sdf/README.md>`__. The rest of this section will give more
  230. detail concerning the collision algorithm and the plugin engine interface.
  231. Collision points are found by minimizing the function A + B + abs(max(A, B)), where A and B are the two colliding
  232. SDFs, via gradient descent. Because SDFs are non-convex, multiple starting points are required in order to converge to
  233. multiple local minima. The number of starting points is set using :ref:`sdf_initpoints<option-sdf_initpoints>`, and
  234. are initialized using the Halton sequence inside the intersection of the axis-aligned bounding boxes. The number of
  235. gradient descent iterations is set using :ref:`sdf_iterations<option-sdf_iterations>`.
  236. While *exact* SDFs---encoding the precise signed distance to the surface---are preferred, collisions are possible with
  237. any function whose value vanishes at the surface and grows monotonically away from it, with a negative sign in the
  238. interior. For such functions, it is still possible to find collisons, albeit with a possibly
  239. increased number of starting points.
  240. The ``sdf_distance`` method is called by the compiler to produce a visual mesh for rendering using the marching cubes
  241. algorithm implemented by `MarchingCubeCpp <https://github.com/aparis69/MarchingCubeCpp>`__.
  242. Future improvement to the gradient descent algorithm, such as a line search which takes advantage of the properties of
  243. SDFs, might reduce the number of iterations and/or starting points.
  244. For the sdf plugin, the following methods need to be specified
  245. ``sdf_distance``:
  246. Returns the signed distance of the query point given in local coordinates.
  247. ``sdf_staticdistance``:
  248. This is the static version of the previous function, taking config attributes as additional inputs. This function is
  249. required because mesh creation occurs during model compilation before the plugin object has been instantiated.
  250. ``sdf_gradient``:
  251. Computes the gradient in local coodinates of the SDF at the query point.
  252. ``sdf_aabb``:
  253. Computes the axis-aligned bounding box in local coordinates. This volume is voxelized uniformly before the call to
  254. the marching cubes algorithm.
  255. .. _exProvider:
  256. Resource providers
  257. ~~~~~~~~~~~~~~~~~~
  258. Resource providers extend MuJoCo to load assets (XML files, meshes, textures, and etc.) that don't necessarily come from
  259. the OS filesystem or the Virtual File System (:ref:`mjVFS`). For example, downloading assets from the Internet could be
  260. implemented as a resource provider. These extensions are handled abstractly in MuJoCo via the :ref:`mjResource` struct.
  261. .. _exProviderStructure:
  262. Overview
  263. ^^^^^^^^
  264. Creating a new resource provider works by registering a :ref:`mjpResourceProvider` struct via
  265. :ref:`mjp_registerResourceProvider` in a global table. Once a resource provider is registered it can be used by all
  266. loading functions. The :ref:`mjpResourceProvider` struct stores three types of fields:
  267. .. _Uniform Resource Identifier: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
  268. Resource prefix
  269. Resources are identified by prefixes in their name. The chosen prefix should have a valid `Uniform Resource
  270. Identifier`_ (URI) scheme syntax. Resource names should also have a valid URI syntax, however this isn't enforced. A
  271. resource name with the syntax ``{prefix}:{filename}`` will match a provider using the scheme ``prefix``. For
  272. instance, a resource provider accessing assets via the Internet might use ``http`` as its scheme. In this case a
  273. resource with the name ``http://www.example.com/myasset.obj`` would match against this resource provider. Schemes are
  274. case-insensitive so that ``HTTP://www.example.com/myasset.obj`` will also match. Note the importance of the colon.
  275. URI syntax requires that a colon follows the prefix in a resource name in order to match against a scheme. For example
  276. ``https://www.example.com/myasset.obj`` would NOT be a match since the scheme is designated as ``https``.
  277. Callbacks
  278. There are three callbacks that a resource provider is required to implement: :ref:`open<mjfOpenResource>`,
  279. :ref:`read<mjfReadResource>`, and :ref:`close<mjfCloseResource>`. The other two callback
  280. :ref:`getdir<mjfGetResourceDir>` and :ref:`modified<mjfResourceModified>` are optional. More details on these callbacks
  281. are given below.
  282. Data Pointer
  283. Lastly, there's an opaque data pointer for the provider to pass data into the callbacks. This data pointer is constant
  284. within a given model.
  285. Resource providers work via callbacks:
  286. - :ref:`mjfOpenResource<mjfOpenResource>`: The open callback takes a single parameter of type :ref:`mjResource`. The
  287. name field of the resource should be used to verify that the resource exists and populate the resource data field with
  288. any extra information needed for the resource. On failure this callback should return 0 (false) or else 1 (true).
  289. - :ref:`mjfReadResource<mjfReadResource>`: The read callback takes as arguments a :ref:`mjResource` and a pointer to a
  290. void pointer called the ``buffer``. The read callback should point the ``buffer`` pointer to the location of where the
  291. bytes of the resource can be read and return the number of bytes pointed to in the ``buffer``. On failure, this
  292. callback should return -1.
  293. - :ref:`mjfCloseResource<mjfCloseResource>`: This callback takes a single parameter of type :ref:`mjResource`, and
  294. should be used to free any memory allocated in the data field in the supplied resource.
  295. - :ref:`mjfGetResourceDir<mjfGetResourceDir>`: This callback is optional and is used to extract the directory from a
  296. resource name. For example, the resource name ``http://www.example.com/myasset.obj`` would have
  297. ``http://www.example.com/`` as its directory.
  298. - :ref:`mjfResourceModified<mjfResourceModified>`: This callback is optional and is used to check if an existing
  299. opened resource has been modifed from its orginal source.
  300. .. _exProviderUsage:
  301. Usage
  302. ^^^^^
  303. When a resource provider is registered, it can be used immediately to open assets. If the asset filename has a prefix
  304. that matches with the prefix of a registered provider, then that provider will be used to load the asset.
  305. .. _exProviderExample:
  306. Example
  307. """""""
  308. .. _data URI scheme: https://en.wikipedia.org/wiki/Data_URI_scheme
  309. This section provides a basic example of a resource provider that reads from a `data URI scheme`_. First we implement
  310. the callbacks:
  311. .. code-block:: C
  312. int str_open_callback(mjResource* resource) {
  313. // call some util function to validate
  314. if (!is_valid_data_uri(resource->name)) {
  315. return 0; // return failure
  316. }
  317. // some upper bound for the data
  318. resource->data = mju_malloc(get_data_uri_size(resource->name));
  319. if (resource->data == NULL) {
  320. return 0; // return failure
  321. }
  322. // fill data from string (some util function)
  323. get_data_uri(resource->name, &data);
  324. }
  325. int str_read_callback(mjResource* resource, const void** buffer) {
  326. *buffer = resource->data;
  327. return get_data_uri_size(resource->name);
  328. }
  329. void str_close_callback(mjResource* resource) {
  330. mju_free(resource->data);
  331. }
  332. Next we create the resource provider and register it with MuJoCo:
  333. .. code-block:: C
  334. mjpResourceProvider resourceProvider = {
  335. .prefix = "data",
  336. .open = str_open_callback,
  337. .read = str_read_callback,
  338. .close = str_close_callback,
  339. .getdir = NULL
  340. };
  341. // return positive number on success
  342. if (!mjp_registerResourceProvider(&resourceProvider)) {
  343. // ...
  344. // return failure
  345. }
  346. Now we can write assets as strings in our MJCF files:
  347. .. code-block:: xml
  348. <asset>
  349. <texture name="grid" file="grid.png" type="2d"/>
  350. <mesh content-type="model/obj" file="data:model/obj;base65,I215IG9iamVjdA0KdiAxIDAgMA0KdiAwIDEgMA0KdiAwIDAgMQ=="/>
  351. ...
  352. </asset>