Add content_type attribute that takes a MIME type to specify the file contents for meshes, hfields, and textures.
PiperOrigin-RevId: 549302344 Change-Id: I30a8adfef5b5d87bb20479329f3396892f5137ff
This commit is contained in:
committed by
Copybara-Service
parent
5f327b0c31
commit
5fcdae771b
+29
-5
@@ -2081,7 +2081,10 @@ This adds an element to the tuple.
|
||||
|
||||
This is a grouping element for defining assets. It does not have attributes. Assets are created in the model so that
|
||||
they can be referenced from other model elements; recall the discussion of :ref:`Assets <Assets>` in the Overview
|
||||
chapter.
|
||||
chapter. Assets opened from a file can be identified in two different ways: filename extensions or the ``content_type``
|
||||
attribute. MuJoCo will attempt to open a file specified by the content type provided, and only defaults to the filename
|
||||
extension if no ``content_type`` attribute is specified. The content type is ignored if the asset isn't loaded from a
|
||||
file.
|
||||
|
||||
|
||||
.. _asset-texture:
|
||||
@@ -2097,8 +2100,8 @@ chapter.
|
||||
textures. Alternatively the data can be generated by the compiler as a procedural texture. Because different texture
|
||||
types require different parameters, only a subset of the attributes below are used for any given texture.
|
||||
| MuJoCo 2.0 introduced a second file format for loading textures, in addition to PNG. If the file name extension is
|
||||
different from .png or .PNG, MuJoCo assumes that the texture is in the new format. This is a custom binary file
|
||||
format, containing the following data:
|
||||
different from .png or .PNG, or if the ``content_type`` attribute is set to ``image/vnd.mujoco.texture``, then MuJoCo
|
||||
assumes that the texture is in the new format. This is a custom binary file format, containing the following data:
|
||||
|
||||
.. code:: Text
|
||||
|
||||
@@ -2153,6 +2156,13 @@ chapter.
|
||||
texrepeat attribute of :ref:`material <asset-material>`. The data can be loaded from a singlefile or created
|
||||
procedurally.
|
||||
|
||||
.. _asset-texture-content_type:
|
||||
|
||||
:at:`content_type`: :at-val:`string, optional`
|
||||
If the file attribute is specified, then this sets the `MIME <https://en.wikipedia.org/wiki/MIME>`_ type of the file
|
||||
to be loaded. Any filename extensions will be ignored. Currently ``image/png`` and ``image/vnd.mujoco.texture`` are
|
||||
supported.
|
||||
|
||||
.. _asset-texture-file:
|
||||
|
||||
:at:`file`: :at-val:`string, optional`
|
||||
@@ -2335,6 +2345,13 @@ also known as terrain map, is a 2D matrix of elevation data. The data can be spe
|
||||
Name of the height field, used for referencing. If the name is omitted and a file name is specified, the height field
|
||||
name equals the file name without the path and extension.
|
||||
|
||||
.. _asset-hfield-content_type:
|
||||
|
||||
:at:`content_type`: :at-val: `string, optional`
|
||||
If the file attribute is specified, then this sets the `MIME <https://en.wikipedia.org/wiki/MIME>`_ type of the file
|
||||
to be loaded. Any filename extensions will be overloaded. Currently ``image/png`` and ``image/vnd.mujoco.hfield``
|
||||
are supported.
|
||||
|
||||
.. _asset-hfield-file:
|
||||
|
||||
:at:`file`: :at-val:`string, optional`
|
||||
@@ -2500,12 +2517,19 @@ The full list of processing steps applied by the compiler to each mesh is as fol
|
||||
:at:`class`: :at-val:`string, optional`
|
||||
Defaults class for setting unspecified attributes (only scale in this case).
|
||||
|
||||
.. _asset-mesh-content_type:
|
||||
|
||||
:at:`content_type`: :at-val:`string, optional`
|
||||
If the file attribute is specified, then this sets the `MIME <https://en.wikipedia.org/wiki/MIME>`_ type of the file
|
||||
to be loaded. Any filename extensions will be overloaded. Currently ``model/vnd.mujoco.msh``, ``model/obj``, and
|
||||
``model/stl`` are supported.
|
||||
|
||||
.. _asset-mesh-file:
|
||||
|
||||
:at:`file`: :at-val:`string, optional`
|
||||
The file from which the mesh will be loaded. The path is determined as described in the meshdir attribute of
|
||||
:ref:`compiler <compiler>`. The file extension must be "stl" or "msh" (not case sensitive) specifying the file type.
|
||||
If the file name is omitted, the vertex attribute becomes required.
|
||||
:ref:`compiler <compiler>`. The file extension must be "stl", "msh", or "obj" (not case sensitive) specifying the
|
||||
file type. If the file name is omitted, the vertex attribute becomes required.
|
||||
|
||||
.. _asset-mesh-scale:
|
||||
|
||||
|
||||
+15
-13
@@ -509,41 +509,43 @@
|
||||
| :ref:`texture | \* | :class: mjcf-attributes |
|
||||
| <asset-texture>` | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`name<asset-texture-name>` | :ref:`type<asset-texture-type>` | :ref:`file<asset-texture-file>` | |
|
||||
| | | | :ref:`name<asset-texture-name>` | :ref:`type<asset-texture-type>` | :ref:`content_type<asset-texture-content_type>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`gridsize<asset-texture-gridsize>` | :ref:`gridlayout<asset-texture-gridlayout>` | :ref:`fileright<asset-texture-fileright>` | |
|
||||
| | | | :ref:`file<asset-texture-file>` | :ref:`gridsize<asset-texture-gridsize>` | :ref:`gridlayout<asset-texture-gridlayout>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`fileleft<asset-texture-fileleft>` | :ref:`fileup<asset-texture-fileup>` | :ref:`filedown<asset-texture-filedown>` | |
|
||||
| | | | :ref:`fileright<asset-texture-fileright>` | :ref:`fileleft<asset-texture-fileleft>` | :ref:`fileup<asset-texture-fileup>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`filefront<asset-texture-filefront>` | :ref:`fileback<asset-texture-fileback>` | :ref:`builtin<asset-texture-builtin>` | |
|
||||
| | | | :ref:`filedown<asset-texture-filedown>` | :ref:`filefront<asset-texture-filefront>` | :ref:`fileback<asset-texture-fileback>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`rgb1<asset-texture-rgb1>` | :ref:`rgb2<asset-texture-rgb2>` | :ref:`mark<asset-texture-mark>` | |
|
||||
| | | | :ref:`builtin<asset-texture-builtin>` | :ref:`rgb1<asset-texture-rgb1>` | :ref:`rgb2<asset-texture-rgb2>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`markrgb<asset-texture-markrgb>` | :ref:`random<asset-texture-random>` | :ref:`width<asset-texture-width>` | |
|
||||
| | | | :ref:`mark<asset-texture-mark>` | :ref:`markrgb<asset-texture-markrgb>` | :ref:`random<asset-texture-random>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`height<asset-texture-height>` | :ref:`hflip<asset-texture-hflip>` | :ref:`vflip<asset-texture-vflip>` | |
|
||||
| | | | :ref:`width<asset-texture-width>` | :ref:`height<asset-texture-height>` | :ref:`hflip<asset-texture-hflip>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`vflip<asset-texture-vflip>` | | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| |_| asset |br| |_| |L| | | .. table:: |
|
||||
| :ref:`hfield | \* | :class: mjcf-attributes |
|
||||
| <asset-hfield>` | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`name<asset-hfield-name>` | :ref:`file<asset-hfield-file>` | :ref:`nrow<asset-hfield-nrow>` | |
|
||||
| | | | :ref:`name<asset-hfield-name>` | :ref:`content_type<asset-hfield-content_type>` | :ref:`file<asset-hfield-file>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`ncol<asset-hfield-ncol>` | :ref:`size<asset-hfield-size>` | | |
|
||||
| | | | :ref:`nrow<asset-hfield-nrow>` | :ref:`ncol<asset-hfield-ncol>` | :ref:`size<asset-hfield-size>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| |_| asset |br| |_| |L| | | .. table:: |
|
||||
| :ref:`mesh | \* | :class: mjcf-attributes |
|
||||
| <asset-mesh>` | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`name<asset-mesh-name>` | :ref:`class<asset-mesh-class>` | :ref:`file<asset-mesh-file>` | |
|
||||
| | | | :ref:`name<asset-mesh-name>` | :ref:`class<asset-mesh-class>` | :ref:`content_type<asset-mesh-content_type>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`vertex<asset-mesh-vertex>` | :ref:`normal<asset-mesh-normal>` | :ref:`texcoord<asset-mesh-texcoord>` | |
|
||||
| | | | :ref:`file<asset-mesh-file>` | :ref:`vertex<asset-mesh-vertex>` | :ref:`normal<asset-mesh-normal>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`face<asset-mesh-face>` | :ref:`refpos<asset-mesh-refpos>` | :ref:`refquat<asset-mesh-refquat>` | |
|
||||
| | | | :ref:`texcoord<asset-mesh-texcoord>` | :ref:`face<asset-mesh-face>` | :ref:`refpos<asset-mesh-refpos>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`scale<asset-mesh-scale>` | :ref:`smoothnormal<asset-mesh-smoothnormal>` | | |
|
||||
| | | | :ref:`refquat<asset-mesh-refquat>` | :ref:`scale<asset-mesh-scale>` | :ref:`smoothnormal<asset-mesh-smoothnormal>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| |_| asset |br| |_| |L| | | .. table:: |
|
||||
|
||||
+4
-1
@@ -9,11 +9,14 @@ General
|
||||
^^^^^^^
|
||||
|
||||
- Added primitive collider for sphere-cylinder contacts, previously this pair used the generic convex-convex collider.
|
||||
- Added an optional ``content_type`` attribute to hfield, texture, and mesh assets. This attribute supports a formatted
|
||||
`MIME <https://en.wikipedia.org/wiki/MIME>`_ string used to determine the type of the asset file without resorting to
|
||||
pulling the type from the file extension.
|
||||
- Added analytic derivatives for quaternion :ref:`subtraction<mjd_subQuat>` and :ref:`integration<mjd_quatIntegrate>`
|
||||
(rotation with an angular velocity). Derivatives are in the 3D tangent space.
|
||||
- Added :ref:`mjv_connector` which has identical functionality to :ref:`mjv_makeConnector`, but with more convenient
|
||||
"from-to" argument parametrization. :ref:`mjv_makeConnector` is now deprecated.
|
||||
- Bump oldest supported MacOS from version 10.12 to 11. MacOS 11 is the oldest version still maintained by Apple.
|
||||
- Bumped oldest supported MacOS from version 10.12 to 11. MacOS 11 is the oldest version still maintained by Apple.
|
||||
|
||||
Python bindings
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
+10
-8
@@ -188,21 +188,23 @@ void mjCMesh::Compile(int vfs_provider) {
|
||||
file = mjuu_strippath(file);
|
||||
}
|
||||
|
||||
// load STL, OBJ or MSH
|
||||
string ext = mjuu_getext(file);
|
||||
if (strcasecmp(ext.c_str(), ".stl") &&
|
||||
strcasecmp(ext.c_str(), ".obj") &&
|
||||
strcasecmp(ext.c_str(), ".msh")) {
|
||||
throw mjCError(this, "Unknown mesh file type: %s", file.c_str());
|
||||
std::string asset_type = GetAssetContentType(file, content_type);
|
||||
if (asset_type.empty()) {
|
||||
throw mjCError(this, "unknown mesh content type for file: '%s'", file.c_str());
|
||||
}
|
||||
|
||||
if (asset_type != "model/stl" && asset_type != "model/obj"
|
||||
&& asset_type != "model/vnd.mujoco.msh") {
|
||||
throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str());
|
||||
}
|
||||
|
||||
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
|
||||
mjResource* resource = LoadResource(filename, vfs_provider);
|
||||
|
||||
try {
|
||||
if (!strcasecmp(ext.c_str(), ".stl")) {
|
||||
if (asset_type == "model/stl") {
|
||||
LoadSTL(resource);
|
||||
} else if (!strcasecmp(ext.c_str(), ".obj")) {
|
||||
} else if (asset_type == "model/obj") {
|
||||
LoadOBJ(resource);
|
||||
} else {
|
||||
LoadMSH(resource);
|
||||
|
||||
+43
-10
@@ -507,6 +507,23 @@ mjResource* mjCBase::LoadResource(string filename, int provider) {
|
||||
}
|
||||
|
||||
|
||||
// Get and sanitize content type from raw_text if not empty, otherwise parse
|
||||
// content type from resource_name; throw error on failure
|
||||
std::string mjCBase::GetAssetContentType(std::string_view resource_name,
|
||||
std::string_view raw_text) {
|
||||
if (!raw_text.empty()) {
|
||||
auto type = mjuu_parseContentTypeAttrType(raw_text);
|
||||
auto subtype = mjuu_parseContentTypeAttrSubtype(raw_text);
|
||||
if (!type.has_value() || !subtype.has_value()) {
|
||||
throw mjCError(this, "invalid format for content_type");
|
||||
}
|
||||
return std::string(*type) + "/" + std::string(*subtype);
|
||||
} else {
|
||||
return mjuu_extToContentType(resource_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------------ class mjCBody implementation --------------------------------------------------
|
||||
|
||||
// constructor
|
||||
@@ -2149,15 +2166,22 @@ void mjCHField::Compile(int vfs_provider) {
|
||||
"hfield '%s' (id = %d) specified from file and manually", name.c_str(), id);
|
||||
}
|
||||
|
||||
// make filename
|
||||
std::string asset_type = GetAssetContentType(file, content_type);
|
||||
|
||||
// fallback to custom
|
||||
if (asset_type.empty()) {
|
||||
asset_type = "image/vnd.mujoco.hfield";
|
||||
}
|
||||
|
||||
if (asset_type != "image/png" && asset_type != "image/vnd.mujoco.hfield") {
|
||||
throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str());
|
||||
}
|
||||
|
||||
string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file);
|
||||
mjResource* resource = LoadResource(filename, vfs_provider);
|
||||
|
||||
// load depending on format
|
||||
string ext = mjuu_getext(filename);
|
||||
|
||||
try {
|
||||
if (!strcasecmp(ext.c_str(), ".png")) {
|
||||
if (asset_type == "image/png") {
|
||||
LoadPNG(resource);
|
||||
} else {
|
||||
LoadCustom(resource);
|
||||
@@ -2548,15 +2572,24 @@ void mjCTexture::LoadCustom(mjResource* resource,
|
||||
void mjCTexture::LoadFlip(string filename, int vfs_provider,
|
||||
std::vector<unsigned char>& image,
|
||||
unsigned int& w, unsigned int& h) {
|
||||
// dispatch to PNG or Custom loaded
|
||||
string ext = mjuu_getext(filename);
|
||||
std::string asset_type = GetAssetContentType(filename, content_type);
|
||||
|
||||
// fallback to custom
|
||||
if (asset_type.empty()) {
|
||||
asset_type = "image/vnd.mujoco.texture";
|
||||
}
|
||||
|
||||
if (asset_type != "image/png" && asset_type != "image/vnd.mujoco.texture") {
|
||||
throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str());
|
||||
}
|
||||
|
||||
mjResource* resource = LoadResource(filename, vfs_provider);
|
||||
|
||||
try {
|
||||
if (!strcasecmp(ext.c_str(), ".png")) {
|
||||
LoadPNG(resource, image, w, h);
|
||||
if (asset_type == "image/png") {
|
||||
LoadPNG(resource, image, w, h);
|
||||
} else {
|
||||
LoadCustom(resource, image, w, h);
|
||||
LoadCustom(resource, image, w, h);
|
||||
}
|
||||
mju_closeResource(resource);
|
||||
} catch(mjCError err) {
|
||||
|
||||
@@ -173,6 +173,10 @@ class mjCBase {
|
||||
// load resource if found (fallback to OS filesystem)
|
||||
mjResource* LoadResource(std::string filename, int provider);
|
||||
|
||||
// Get and sanitize content type from raw_text if not empty, otherwise parse
|
||||
// content type from resource_name; throw on failure
|
||||
std::string GetAssetContentType(std::string_view resource_name, std::string_view raw_text);
|
||||
|
||||
std::string name; // object name
|
||||
std::string classname; // defaults class name
|
||||
int id; // object id
|
||||
@@ -529,6 +533,7 @@ class mjCMesh: public mjCBase {
|
||||
// returns a bounding volume given a face
|
||||
mjCBoundingVolume GetBoundingVolume(int faceid);
|
||||
|
||||
std::string content_type; // content type of file
|
||||
std::string file; // mesh file
|
||||
double refpos[3]; // reference position (translate)
|
||||
double refquat[4]; // reference orientation (rotate)
|
||||
@@ -652,6 +657,7 @@ class mjCHField : public mjCBase {
|
||||
friend class mjXWriter;
|
||||
|
||||
public:
|
||||
std::string content_type; // content type of file
|
||||
std::string file; // file: (nrow, ncol, [elevation data])
|
||||
double size[4]; // hfield size (ignore referencing geom size)
|
||||
int nrow; // number of rows
|
||||
@@ -691,6 +697,7 @@ class mjCTexture : public mjCBase {
|
||||
int width; // width in pixels
|
||||
|
||||
// method 2: single file
|
||||
std::string content_type; // content type of file
|
||||
std::string file; // png file to load; use for all sides of cube
|
||||
int gridsize[2]; // size of grid for composite file; (1,1)-repeat
|
||||
char gridlayout[13]; // row-major: L,R,F,B,U,D for faces; . for unused
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <mujoco/mjmacro.h>
|
||||
#include <mujoco/mjtnum.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "engine/engine_crossplatform.h"
|
||||
#include "engine/engine_util_spatial.h"
|
||||
|
||||
using std::isnan;
|
||||
@@ -596,3 +598,82 @@ string mjuu_makefullname(string filedir, string meshdir, string filename) {
|
||||
// default
|
||||
return filedir + meshdir + filename;
|
||||
}
|
||||
|
||||
|
||||
// return true if the text is in a valid content type format:
|
||||
// {type}/{subtype}[;{parameter}={value}]
|
||||
static bool mjuu_isValidContentType(std::string_view text) {
|
||||
// find a forward slash that's not the last character
|
||||
size_t n = text.find('/');
|
||||
if (n == std::string::npos || n == text.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t m = text.find(';');
|
||||
if (m == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m + 1 <= n) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// just check if there's an equal sign; this isn't robust enough for general
|
||||
// validation, but works for our scope, hence this is a private helper
|
||||
// function
|
||||
size_t s = text.find('=');
|
||||
if (s == std::string::npos || s + 1 <= m) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// return type from content_type format {type}/{subtype}[;{parameter}={value}]
|
||||
// return empty string on invalid format
|
||||
std::optional<std::string_view> mjuu_parseContentTypeAttrType(std::string_view text) {
|
||||
if (!mjuu_isValidContentType(text)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return { text.substr(0, text.find('/')) };
|
||||
}
|
||||
|
||||
|
||||
|
||||
// return subtype from content_type format {type}/{subtype}[;{parameter}={value}]
|
||||
// return empty string on invalid format
|
||||
std::optional<std::string_view> mjuu_parseContentTypeAttrSubtype(std::string_view text) {
|
||||
if (!mjuu_isValidContentType(text)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t n = text.find('/');
|
||||
size_t m = text.find(';', n + 1);
|
||||
if (m == std::string::npos) {
|
||||
return { text.substr(n+1) };
|
||||
}
|
||||
|
||||
return { text.substr(n + 1, m - n - 1) };
|
||||
}
|
||||
|
||||
|
||||
|
||||
// convert filename extension to content type; return empty string if not found
|
||||
std::string mjuu_extToContentType(std::string_view filename) {
|
||||
std::string ext = mjuu_getext(filename);
|
||||
|
||||
if (!strcasecmp(ext.c_str(), ".stl")) {
|
||||
return "model/stl";
|
||||
} else if (!strcasecmp(ext.c_str(), ".obj")) {
|
||||
return "model/obj";
|
||||
} else if (!strcasecmp(ext.c_str(), ".msh")) {
|
||||
return "model/vnd.mujoco.msh";
|
||||
} else if (!strcasecmp(ext.c_str(), ".png")) {
|
||||
return "image/png";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -15,6 +15,7 @@
|
||||
#ifndef MUJOCO_SRC_USER_USER_UTIL_H_
|
||||
#define MUJOCO_SRC_USER_USER_UTIL_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -23,7 +24,6 @@ extern const double mjNAN; // used to mark undefined fields
|
||||
const double mjEPS = 1E-14; // minimum value in various calculations
|
||||
const double mjMINMASS = 1E-6; // minimum mass allowed
|
||||
|
||||
|
||||
// check if numeric variable is defined: !_isnan(num)
|
||||
bool mjuu_defined(const double num);
|
||||
|
||||
@@ -152,4 +152,13 @@ bool mjuu_isabspath(std::string path);
|
||||
// assemble full filename
|
||||
std::string mjuu_makefullname(std::string filedir, std::string meshdir, std::string filename);
|
||||
|
||||
// return type from content_type format {type}/{subtype}[;{parameter}={value}]
|
||||
std::optional<std::string_view> mjuu_parseContentTypeAttrType(std::string_view text);
|
||||
|
||||
// return subtype from content_type format {type}/{subtype}[;{parameter}={value}]
|
||||
std::optional<std::string_view> mjuu_parseContentTypeAttrSubtype(std::string_view text);
|
||||
|
||||
// convert filename extension to content type; return empty string if not found
|
||||
std::string mjuu_extToContentType(std::string_view filename);
|
||||
|
||||
#endif // MUJOCO_SRC_USER_USER_UTIL_H_
|
||||
|
||||
@@ -212,12 +212,12 @@ static const char* MJCF[nMJCF][mjXATTRNUM] = {
|
||||
|
||||
{"asset", "*", "0"},
|
||||
{"<"},
|
||||
{"texture", "*", "21", "name", "type", "file", "gridsize", "gridlayout",
|
||||
{"texture", "*", "22", "name", "type", "content_type", "file", "gridsize", "gridlayout",
|
||||
"fileright", "fileleft", "fileup", "filedown", "filefront", "fileback",
|
||||
"builtin", "rgb1", "rgb2", "mark", "markrgb", "random", "width", "height",
|
||||
"hflip", "vflip"},
|
||||
{"hfield", "*", "5", "name", "file", "nrow", "ncol", "size"},
|
||||
{"mesh", "*", "11", "name", "class", "file", "vertex", "normal",
|
||||
{"hfield", "*", "6", "name", "content_type", "file", "nrow", "ncol", "size"},
|
||||
{"mesh", "*", "12", "name", "class", "content_type", "file", "vertex", "normal",
|
||||
"texcoord", "face", "refpos", "refquat", "scale", "smoothnormal"},
|
||||
{"skin", "*", "9", "name", "file", "material", "rgba", "inflate",
|
||||
"vertex", "texcoord", "face", "group"},
|
||||
@@ -1186,6 +1186,7 @@ void mjXReader::OneMesh(XMLElement* elem, mjCMesh* pmesh) {
|
||||
// read attributes
|
||||
ReadAttrTxt(elem, "name", pmesh->name);
|
||||
ReadAttrTxt(elem, "class", pmesh->classname);
|
||||
ReadAttrTxt(elem, "content_type", pmesh->content_type);
|
||||
ReadAttrTxt(elem, "file", pmesh->file);
|
||||
ReadAttr(elem, "refpos", 3, pmesh->refpos, text);
|
||||
ReadAttr(elem, "refquat", 4, pmesh->refquat, text);
|
||||
@@ -2511,6 +2512,7 @@ void mjXReader::Asset(XMLElement* section) {
|
||||
ptex->type = (mjtTexture)n;
|
||||
}
|
||||
ReadAttrTxt(elem, "name", ptex->name);
|
||||
ReadAttrTxt(elem, "content_type", ptex->content_type);
|
||||
ReadAttrTxt(elem, "file", ptex->file);
|
||||
ReadAttrInt(elem, "width", &ptex->width);
|
||||
ReadAttrInt(elem, "height", &ptex->height);
|
||||
@@ -2583,6 +2585,7 @@ void mjXReader::Asset(XMLElement* section) {
|
||||
|
||||
// read attributes
|
||||
ReadAttrTxt(elem, "name", phf->name);
|
||||
ReadAttrTxt(elem, "content_type", phf->content_type);
|
||||
ReadAttrTxt(elem, "file", phf->file);
|
||||
ReadAttrInt(elem, "nrow", &phf->nrow);
|
||||
ReadAttrInt(elem, "ncol", &phf->ncol);
|
||||
|
||||
@@ -90,6 +90,7 @@ void mjXWriter::OneMesh(XMLElement* elem, mjCMesh* pmesh, mjCDef* def) {
|
||||
if (!writingdefaults) {
|
||||
WriteAttrTxt(elem, "name", pmesh->name);
|
||||
WriteAttrTxt(elem, "class", pmesh->classname);
|
||||
WriteAttrTxt(elem, "content_type", pmesh->content_type);
|
||||
WriteAttrTxt(elem, "file", pmesh->file);
|
||||
|
||||
// write vertex data
|
||||
@@ -1255,9 +1256,10 @@ void mjXWriter::Asset(XMLElement* root) {
|
||||
WriteAttrInt(elem, "height", ptex->height);
|
||||
}
|
||||
|
||||
// write texures loaded from files
|
||||
// write textures loaded from files
|
||||
else {
|
||||
// write singe file
|
||||
// write single file
|
||||
WriteAttrTxt(elem, "content_type", ptex->content_type);
|
||||
WriteAttrTxt(elem, "file", ptex->file);
|
||||
|
||||
// write separate files
|
||||
@@ -1317,6 +1319,7 @@ void mjXWriter::Asset(XMLElement* root) {
|
||||
WriteAttrTxt(elem, "name", phf->name);
|
||||
WriteAttr(elem, "size", 4, phf->size);
|
||||
if (!phf->file.empty()) {
|
||||
WriteAttrTxt(elem, "content_type", phf->content_type);
|
||||
WriteAttrTxt(elem, "file", phf->file);
|
||||
} else {
|
||||
WriteAttrInt(elem, "nrow", phf->nrow);
|
||||
|
||||
+128
-1
@@ -88,7 +88,7 @@ TEST_F(MjCMeshTest, UnknownMeshFormat) {
|
||||
LoadModelFromString(xml.c_str(), error.data(), error.size());
|
||||
ASSERT_THAT(model, testing::IsNull())
|
||||
<< "Should fail to load a mesh named: " << name;
|
||||
EXPECT_THAT(error.data(), HasSubstr("Unknown mesh file type"));
|
||||
EXPECT_THAT(error.data(), HasSubstr("unknown mesh content type for file"));
|
||||
EXPECT_THAT(error.data(), HasSubstr(name));
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,133 @@ TEST_F(MjCMeshTest, LoadSTLWithVFS) {
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
// ------------- test content_type attributes ----------------------------------
|
||||
|
||||
TEST_F(MjCMeshTest, LoadMSHWithContentType) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="mesh1" content_type="model/vnd.mujoco.msh" file="some_file"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="mesh1"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try opening the file (not found obviously)
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, LoadOBJWithContentType) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="mesh1" content_type="model/obj" file="some_file"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="mesh1"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try opening the file (not found obviously)
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, LoadSTLWithContentType) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="mesh1" content_type="model/stl" file="some_file"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="mesh1"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try opening the file (not found obviously)
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, LoadMSHWithContentTypeError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="mesh1" content_type="model/unknown" file="some_file"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="mesh1"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should error with unknown file type
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("unsupported content type: 'model/unknown'"));
|
||||
}
|
||||
|
||||
TEST_F(MjCMeshTest, LoadMSHWithContentTypeParam) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<mesh name="mesh1" content_type="model/vnd.mujoco.msh;parameter=value" file="some_file"/>
|
||||
</asset>
|
||||
<worldbody>
|
||||
<geom type="mesh" mesh="mesh1"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try opening the file (not found obviously)
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
// ------------- test vertex de-duplication (STL) ------------------------------
|
||||
|
||||
TEST_F(MjCMeshTest, DeDuplicateSTLVertices) {
|
||||
|
||||
@@ -150,6 +150,224 @@ TEST_F(VfsTest, TextureCustomWithVFS) {
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
// ------------------------ test content_type attribute ------------------------
|
||||
|
||||
using ContentTypeTest = MujocoTest;
|
||||
|
||||
TEST_F(ContentTypeTest, HFieldPngWithContentType) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<hfield name="hfield" content_type="image/png"
|
||||
file="some_file.png" size="0.5 0.5 1 0.1"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<geom type="hfield" hfield="hfield" pos="-.4 .6 .05"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try loading the file
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
TEST_F(ContentTypeTest, HFieldCustomWithContentType) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<hfield name="hfield" content_type="image/vnd.mujoco.hfield"
|
||||
file="some_file" size="0.5 0.5 1 0.1"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<geom type="hfield" hfield="hfield" pos="-.4 .6 .05"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try loading the file
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
TEST_F(ContentTypeTest, HFieldWithContentTypeError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<hfield name="hfield" content_type="image/jpeg"
|
||||
file="some_file" size="0.5 0.5 1 0.1"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<geom type="hfield" hfield="hfield" pos="-.4 .6 .05"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try loading the file
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("unsupported content type: 'image/jpeg'"));
|
||||
}
|
||||
|
||||
TEST_F(ContentTypeTest, TexturePngWithContentType) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture name="texture" content_type="image/png" file="some_file" type="2d"/>
|
||||
<material name="material" texture="texture"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<geom type="plane" material="material" size="4 4 4"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try loading the file
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
TEST_F(ContentTypeTest, TextureCustomWithContentType) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture name="texture" content_type="image/vnd.mujoco.texture"
|
||||
file="some_file" type="2d"/>
|
||||
<material name="material" texture="texture"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<geom type="plane" material="material" size="4 4 4"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try loading the file
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem"));
|
||||
}
|
||||
|
||||
TEST_F(ContentTypeTest, TextureWithContentTypeError) {
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture name="texture" content_type="image/jpeg"
|
||||
file="some_file" type="2d"/>
|
||||
<material name="material" texture="texture"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<geom type="plane" material="material" size="4 4 4"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
|
||||
// should try loading the file
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, IsNull());
|
||||
EXPECT_THAT(error, HasSubstr("unsupported content type: 'image/jpeg'"));
|
||||
}
|
||||
|
||||
TEST_F(ContentTypeTest, TextureLoadPng) {
|
||||
static constexpr char filename[] = "tiny";
|
||||
static constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
<asset>
|
||||
<texture content_type="image/png" file="tiny" type="2d"/>
|
||||
<material name="material" texture="tiny"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<geom type="plane" material="material" size="4 4 4"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
// credit: https://www.mjt.me.uk/posts/smallest-png/
|
||||
static constexpr unsigned char tiny[] =
|
||||
{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00,
|
||||
0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x03, 0x00,
|
||||
0x00, 0x00, 0x66, 0xBC, 0x3A, 0x25, 0x00, 0x00, 0x00,
|
||||
0x03, 0x50, 0x4C, 0x54, 0x45, 0xB5, 0xD0, 0xD0, 0x63,
|
||||
0x04, 0x16, 0xEA, 0x00, 0x00, 0x00, 0x1F, 0x49, 0x44,
|
||||
0x41, 0x54, 0x68, 0x81, 0xED, 0xC1, 0x01, 0x0D, 0x00,
|
||||
0x00, 0x00, 0xC2, 0xA0, 0xF7, 0x4F, 0x6D, 0x0E, 0x37,
|
||||
0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xBE, 0x0D, 0x21, 0x00, 0x00, 0x01, 0x9A, 0x60, 0xE1,
|
||||
0xD5, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
|
||||
0xAE, 0x42, 0x60, 0x82 };
|
||||
size_t tiny_sz = sizeof(tiny);
|
||||
|
||||
char error[1024];
|
||||
size_t error_sz = 1024;
|
||||
|
||||
|
||||
// load VFS on the heap
|
||||
auto vfs = std::make_unique<mjVFS>();
|
||||
mj_defaultVFS(vfs.get());
|
||||
mj_makeEmptyFileVFS(vfs.get(), filename, 105);
|
||||
int i = mj_findFileVFS(vfs.get(), filename);
|
||||
memcpy(vfs->filedata[i], tiny, tiny_sz);
|
||||
|
||||
// loading the file should be successful
|
||||
mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get());
|
||||
EXPECT_THAT(model, NotNull());
|
||||
|
||||
mj_deleteModel(model);
|
||||
mj_deleteFileVFS(vfs.get(), filename);
|
||||
}
|
||||
|
||||
// ------------------------ test keyframes -------------------------------------
|
||||
|
||||
using KeyframeTest = MujocoTest;
|
||||
|
||||
Reference in New Issue
Block a user