From 851bb6eef4aee39063e97ad8c1d70d2de4edfec2 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 12 Aug 2024 12:26:39 -0700 Subject: [PATCH] Add option to mjspec for creating a texture directly from a buffer. PiperOrigin-RevId: 662186527 Change-Id: I469c876681d52964ac971c0bb2ec3517224d3a3d --- doc/APIreference/functions.rst | 9 ++ doc/changelog.rst | 9 +- doc/includes/references.h | 4 + include/mujoco/mjspec.h | 6 + include/mujoco/mujoco.h | 3 + introspect/functions.py | 24 +++ introspect/structs.py | 7 + .../mujoco/codegen/generate_spec_bindings.py | 18 +++ src/user/user_api.cc | 12 ++ src/user/user_api.h | 3 + src/user/user_model.cc | 3 +- src/user/user_objects.cc | 147 ++++++++++-------- src/user/user_objects.h | 5 +- src/xml/xml.cc | 8 +- src/xml/xml_native_writer.cc | 10 +- test/user/user_api_test.cc | 94 ++++++++--- 16 files changed, 267 insertions(+), 95 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 7accaa2a..23561a19 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -4417,6 +4417,15 @@ Safely cast an element as mjsMaterial, or return NULL if the element is not an m Attribute setters ^^^^^^^^^^^^^^^^^ +.. _mjs_setBuffer: + +mjs_setBuffer +~~~~~~~~~~~~~ + +.. mujoco-include:: mjs_setBuffer + +Copy buffer. + .. _mjs_setString: mjs_setString diff --git a/doc/changelog.rst b/doc/changelog.rst index b49bf217..d8d2236c 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -5,6 +5,13 @@ Changelog Upcoming version (not yet released) ----------------------------------- +General +^^^^^^^ +1. Add :ref:`mjSpec` option for creating a texture from a buffer. + +Upcoming version (not yet released) +----------------------------------- + General ^^^^^^^ - :ref:`shellinertia ` is now supported by all geom types. @@ -22,7 +29,7 @@ Version 3.2.1 (Aug 5, 2024) General ^^^^^^^ -1. Renamed ``mjModel.tex_rbg`` to ``mjModel.tex_data``. +1. Renamed ``mjModel.tex_rgb`` to ``mjModel.tex_data``. 2. Added a new :ref:`autoreset` flag to disable automatic reset when NaNs or infinities are detected. 3. Added sub-elements to the MJCF :ref:`material` element, to allow specification of multiple textures diff --git a/doc/includes/references.h b/doc/includes/references.h index 8ff4f43c..267059b8 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2026,6 +2026,9 @@ typedef struct mjsTexture_ { // texture specification // method 3: separate files mjStringVec* cubefiles; // different file for each side of the cube + // method 4: from buffer read by user + mjBuffer* data; // texture data + // flip options mjtByte hflip; // horizontal flip mjtByte vflip; // vertical flip @@ -3584,6 +3587,7 @@ mjsHField* mjs_asHField(mjsElement* element); mjsSkin* mjs_asSkin(mjsElement* element); mjsTexture* mjs_asTexture(mjsElement* element); mjsMaterial* mjs_asMaterial(mjsElement* element); +void mjs_setBuffer(mjBuffer* dest, const void* array, int size); void mjs_setString(mjString* dest, const char* text); void mjs_setStringVec(mjStringVec* dest, const char* text); mjtByte mjs_setInStringVec(mjStringVec* dest, int i, const char* text); diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index db3c53b9..1057e559 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -22,6 +22,7 @@ // this is a C-API #ifdef __cplusplus +#include #include #include @@ -32,6 +33,7 @@ extern "C" { #ifdef __cplusplus // C++: defined to be compatible with corresponding std types + using mjBuffer = std::vector; using mjString = std::string; using mjStringVec = std::vector; using mjIntVec = std::vector; @@ -41,6 +43,7 @@ extern "C" { using mjDoubleVec = std::vector; #else // C: opaque types + typedef void mjBuffer; typedef void mjString; typedef void mjStringVec; typedef void mjIntVec; @@ -511,6 +514,9 @@ typedef struct mjsTexture_ { // texture specification // method 3: separate files mjStringVec* cubefiles; // different file for each side of the cube + // method 4: from buffer read by user + mjBuffer* data; // texture data + // flip options mjtByte hflip; // horizontal flip mjtByte vflip; // vertical flip diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index e9b875bb..ac8a5507 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1629,6 +1629,9 @@ MJAPI mjsMaterial* mjs_asMaterial(mjsElement* element); //---------------------------------- Attribute setters --------------------------------------------- +// Copy buffer. +MJAPI void mjs_setBuffer(mjBuffer* dest, const void* array, int size); + // Copy text to string. MJAPI void mjs_setString(mjString* dest, const char* text); diff --git a/introspect/functions.py b/introspect/functions.py index 3141750f..bf2860ba 100644 --- a/introspect/functions.py +++ b/introspect/functions.py @@ -10245,6 +10245,30 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Safely cast an element as mjsMaterial, or return NULL if the element is not an mjsMaterial.', # pylint: disable=line-too-long )), + ('mjs_setBuffer', + FunctionDecl( + name='mjs_setBuffer', + return_type=ValueType(name='void'), + parameters=( + FunctionParameterDecl( + name='dest', + type=PointerType( + inner_type=ValueType(name='mjBuffer'), + ), + ), + FunctionParameterDecl( + name='array', + type=PointerType( + inner_type=ValueType(name='void', is_const=True), + ), + ), + FunctionParameterDecl( + name='size', + type=ValueType(name='int'), + ), + ), + doc='Copy buffer.', + )), ('mjs_setString', FunctionDecl( name='mjs_setString', diff --git a/introspect/structs.py b/introspect/structs.py index 1348f5a1..12d2af2b 100644 --- a/introspect/structs.py +++ b/introspect/structs.py @@ -10011,6 +10011,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='different file for each side of the cube', ), + StructFieldDecl( + name='data', + type=PointerType( + inner_type=ValueType(name='mjBuffer'), + ), + doc='texture data', + ), StructFieldDecl( name='hflip', type=ValueType(name='mjtByte'), diff --git a/python/mujoco/codegen/generate_spec_bindings.py b/python/mujoco/codegen/generate_spec_bindings.py index 1b4c33cb..ef4aedf4 100644 --- a/python/mujoco/codegen/generate_spec_bindings.py +++ b/python/mujoco/codegen/generate_spec_bindings.py @@ -146,6 +146,24 @@ def _ptr_binding_code( self.{fullvarname}->push_back(py::cast<{vartype}>(val)); }} }}, py::return_value_policy::reference_internal);""" + elif vartype == 'mjBuffer': # C++ buffer -> Python list + return f"""\ + {classname}.def_property( + "{varname}", + []({rawclassname}& self) -> py::list {{ + py::list list; + for (auto val : *self.{fullvarname}) {{ + list.append(val); + }} + return list; + }}, + []({rawclassname}& self, py::object rhs) {{ + self.{fullvarname}->clear(); + self.{fullvarname}->reserve(py::len(rhs)); + for (auto val : rhs) {{ + self.{fullvarname}->push_back(py::cast(val)); + }} + }}, py::return_value_policy::reference_internal);""" elif vartype == 'mjStringVec': # C++ vector of strings -> Python list return f"""\ {classname}.def_property( diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 83b1ae5e..e5ff263b 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -14,8 +14,10 @@ #include "user/user_api.h" +#include #include #include +#include #include #include #include @@ -877,6 +879,16 @@ mjsMaterial* mjs_asMaterial(mjsElement* element) { +// copy buffer to destination buffer +void mjs_setBuffer(mjBuffer* dest, const void* array, int size) { + const std::byte* buffer = static_cast(array); + dest->clear(); + dest->reserve(size); + std::copy_n(buffer, size, std::back_inserter(*dest)); +} + + + // set string void mjs_setString(mjString* dest, const char* text) { std::string* str = static_cast(dest); diff --git a/src/user/user_api.h b/src/user/user_api.h index d5022610..967320f5 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -302,6 +302,9 @@ MJAPI mjsMaterial* mjs_asMaterial(mjsElement* element); //---------------------------------- Attribute setters --------------------------------------------- +// Copy buffer. +MJAPI void mjs_setBuffer(mjBuffer* dest, const void* array, int size); + // Copy text to string. MJAPI void mjs_setString(mjString* dest, const char* text); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index f08f8fc1..e39e432b 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -2573,7 +2574,7 @@ void mjCModel::CopyObjects(mjModel* m) { m->tex_adr[i] = data_adr; // copy rgb data - memcpy(m->tex_data + data_adr, ptex->data.data(), + memcpy(m->tex_data + data_adr, ptex->data_.data(), ptex->nchannel * ptex->width * ptex->height); // advance counter diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index beed497f..496e2aa7 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -3459,7 +3459,8 @@ mjCTexture::mjCTexture(mjCModel* _model) { spec_cubefiles_.assign(6, ""); // clear internal variables - data.clear(); + data_.clear(); + clear_data_ = false; // point to local PointToLocal(); @@ -3480,6 +3481,7 @@ mjCTexture& mjCTexture::operator=(const mjCTexture& other) { if (this != &other) { this->spec = other.spec; *static_cast(this) = static_cast(other); + clear_data_ = other.clear_data_; } PointToLocal(); return *this; @@ -3491,6 +3493,7 @@ void mjCTexture::PointToLocal() { spec.element = static_cast(this); spec.name = &name; spec.file = &spec_file_; + spec.data = &data_; spec.content_type = &spec_content_type_; spec.cubefiles = &spec_cubefiles_; spec.info = &info; @@ -3507,21 +3510,23 @@ void mjCTexture::CopyFromSpec() { content_type_ = spec_content_type_; cubefiles_ = spec_cubefiles_; - // clear precompiled asset. TODO: use asset cache - data.clear(); + if (clear_data_) { + // clear precompiled asset. TODO: use asset cache + data_.clear(); + } } // free data storage allocated by lodepng mjCTexture::~mjCTexture() { - data.clear(); + data_.clear(); } // insert random dots -static void randomdot(unsigned char* rgb, const double* markrgb, +static void randomdot(std::byte* rgb, const double* markrgb, int width, int height, double probability) { // make distribution using fixed seed std::mt19937_64 rng; @@ -3533,7 +3538,7 @@ static void randomdot(unsigned char* rgb, const double* markrgb, for (int c=0; c0) { - randomdot(data.data(), markrgb, width, height, random); + randomdot(data_.data(), markrgb, width, height, random); } } @@ -3661,7 +3666,7 @@ void mjCTexture::Builtin2D(void) { // make builtin: Cube void mjCTexture::BuiltinCube(void) { - unsigned char RGB1[3], RGB2[3], RGBm[3], RGBi[3]; + std::byte RGB1[3], RGB2[3], RGBm[3], RGBi[3]; int w = width; if (w > std::numeric_limits::max() / w) { throw mjCError(this, "Cube texture width is too large."); @@ -3670,9 +3675,9 @@ void mjCTexture::BuiltinCube(void) { // convert fixed colors for (int j = 0; j < 3; j++) { - RGB1[j] = (mjtByte)(255 * rgb1[j]); - RGB2[j] = (mjtByte)(255 * rgb2[j]); - RGBm[j] = (mjtByte)(255 * markrgb[j]); + RGB1[j] = (std::byte)(255 * rgb1[j]); + RGB2[j] = (std::byte)(255 * rgb2[j]); + RGBm[j] = (std::byte)(255 * markrgb[j]); } //------------------ faces @@ -3694,26 +3699,26 @@ void mjCTexture::BuiltinCube(void) { // set sides interp(RGBi, rgb1, rgb2, elside); - memcpy(data.data() + 0 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 0: right - memcpy(data.data() + 1 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 1: left - memcpy(data.data() + 4 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 4: front - memcpy(data.data() + 5 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 5: back + memcpy(data_.data() + 0 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 0: right + memcpy(data_.data() + 1 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 1: left + memcpy(data_.data() + 4 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 4: front + memcpy(data_.data() + 5 * 3 * ww + 3 * (r * w + c), RGBi, 3); // 5: back // set up and down - interp(data.data() + 2 * 3 * ww + 3 * (r * w + c), rgb1, rgb2, elup); // 2: up - interp(data.data() + 3 * 3 * ww + 3 * (r * w + c), rgb1, rgb2, -elup); // 3: down + interp(data_.data() + 2 * 3 * ww + 3 * (r * w + c), rgb1, rgb2, elup); // 2: up + interp(data_.data() + 3 * 3 * ww + 3 * (r * w + c), rgb1, rgb2, -elup); // 3: down } } } // checker else if (builtin == mjBUILTIN_CHECKER) { - checker(data.data() + 0 * 3 * ww, RGB1, RGB2, w, w); - checker(data.data() + 1 * 3 * ww, RGB1, RGB2, w, w); - checker(data.data() + 2 * 3 * ww, RGB1, RGB2, w, w); - checker(data.data() + 3 * 3 * ww, RGB1, RGB2, w, w); - checker(data.data() + 4 * 3 * ww, RGB2, RGB1, w, w); - checker(data.data() + 5 * 3 * ww, RGB2, RGB1, w, w); + checker(data_.data() + 0 * 3 * ww, RGB1, RGB2, w, w); + checker(data_.data() + 1 * 3 * ww, RGB1, RGB2, w, w); + checker(data_.data() + 2 * 3 * ww, RGB1, RGB2, w, w); + checker(data_.data() + 3 * 3 * ww, RGB1, RGB2, w, w); + checker(data_.data() + 4 * 3 * ww, RGB2, RGB1, w, w); + checker(data_.data() + 5 * 3 * ww, RGB2, RGB1, w, w); } // flat @@ -3721,14 +3726,14 @@ void mjCTexture::BuiltinCube(void) { for (int r = 0; r < w; r++) { for (int c = 0; c < w; c++) { // set sides and up - memcpy(data.data() + 0 * 3 * ww + 3 * (r * w + c), RGB1, 3); - memcpy(data.data() + 1 * 3 * ww + 3 * (r * w + c), RGB1, 3); - memcpy(data.data() + 2 * 3 * ww + 3 * (r * w + c), RGB1, 3); - memcpy(data.data() + 4 * 3 * ww + 3 * (r * w + c), RGB1, 3); - memcpy(data.data() + 5 * 3 * ww + 3 * (r * w + c), RGB1, 3); + memcpy(data_.data() + 0 * 3 * ww + 3 * (r * w + c), RGB1, 3); + memcpy(data_.data() + 1 * 3 * ww + 3 * (r * w + c), RGB1, 3); + memcpy(data_.data() + 2 * 3 * ww + 3 * (r * w + c), RGB1, 3); + memcpy(data_.data() + 4 * 3 * ww + 3 * (r * w + c), RGB1, 3); + memcpy(data_.data() + 5 * 3 * ww + 3 * (r * w + c), RGB1, 3); // set down - memcpy(data.data() + 3 * 3 * ww + 3 * (r * w + c), RGB2, 3); + memcpy(data_.data() + 3 * 3 * ww + 3 * (r * w + c), RGB2, 3); } } } @@ -3739,12 +3744,12 @@ void mjCTexture::BuiltinCube(void) { if (mark == mjMARK_EDGE) { for (int j = 0; j < 6; j++) { for (int r = 0; r < w; r++) { - memcpy(data.data() + j * 3 * ww + 3 * (r * w + 0), RGBm, 3); - memcpy(data.data() + j * 3 * ww + 3 * (r * w + w - 1), RGBm, 3); + memcpy(data_.data() + j * 3 * ww + 3 * (r * w + 0), RGBm, 3); + memcpy(data_.data() + j * 3 * ww + 3 * (r * w + w - 1), RGBm, 3); } for (int c = 0; c < w; c++) { - memcpy(data.data() + j * 3 * ww + 3 * (0 * w + c), RGBm, 3); - memcpy(data.data() + j * 3 * ww + 3 * ((w - 1) * w + c), RGBm, 3); + memcpy(data_.data() + j * 3 * ww + 3 * (0 * w + c), RGBm, 3); + memcpy(data_.data() + j * 3 * ww + 3 * ((w - 1) * w + c), RGBm, 3); } } } @@ -3753,17 +3758,17 @@ void mjCTexture::BuiltinCube(void) { else if (mark == mjMARK_CROSS) { for (int j = 0; j < 6; j++) { for (int r = 0; r < w; r++) { - memcpy(data.data() + j * 3 * ww + 3 * (r * w + w / 2), RGBm, 3); + memcpy(data_.data() + j * 3 * ww + 3 * (r * w + w / 2), RGBm, 3); } for (int c = 0; c < w; c++) { - memcpy(data.data() + j * 3 * ww + 3 * (w / 2 * w + c), RGBm, 3); + memcpy(data_.data() + j * 3 * ww + 3 * (w / 2 * w + c), RGBm, 3); } } } // random dots else if (mark == mjMARK_RANDOM && random > 0) { - randomdot(data.data(), markrgb, w, height, random); + randomdot(data_.data(), markrgb, w, height, random); } } @@ -3928,12 +3933,12 @@ void mjCTexture::Load2D(std::string filename, const mjVFS* vfs) { throw mjCError(this, "Texture too large"); } try { - data.assign(nchannel*size, 0); + data_.assign(nchannel*size, std::byte(0)); } catch (const std::bad_alloc& e) { throw mjCError(this, "Could not allocate memory for texture '%s' (id %d)", (const char*)file_.c_str(), id); } - memcpy(data.data(), image.data(), nchannel*size); + memcpy(data_.data(), image.data(), nchannel*size); image.clear(); } @@ -3975,7 +3980,7 @@ void mjCTexture::LoadCubeSingle(std::string filename, const mjVFS* vfs) { throw mjCError(this, "Cube texture too large"); } try { - data.assign(3*size, 0); + data_.assign(3*size, std::byte(0)); } catch (const std::bad_alloc& e) { throw mjCError(this, "Could not allocate memory for texture '%s' (id %d)", @@ -3984,7 +3989,7 @@ void mjCTexture::LoadCubeSingle(std::string filename, const mjVFS* vfs) { // copy: repeated if (gridsize[0]==1 && gridsize[1]==1) { - memcpy(data.data(), image.data(), 3*width*width); + memcpy(data_.data(), image.data(), 3*width*width); } // copy: grid @@ -4017,7 +4022,7 @@ void mjCTexture::LoadCubeSingle(std::string filename, const mjVFS* vfs) { int rstart = width*(k/gridsize[1]); int cstart = width*(k%gridsize[1]); for (int j=0; j= std::numeric_limits::max()/6) { throw mjCError(this, "Invalid width of builtin texture"); @@ -4084,7 +4089,7 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { throw mjCError(this, "PNG texture too large"); } try { - data.assign(3*size, 0); + data_.assign(3*size, std::byte(0)); } catch (const std::bad_alloc& e) { throw mjCError(this, "Could not allocate memory for texture"); } @@ -4098,7 +4103,7 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { } // copy data - memcpy(data.data()+i*3*width*width, image.data(), 3*width*width); + memcpy(data_.data()+i*3*width*width, image.data(), 3*width*width); image.clear(); // mark as defined @@ -4112,7 +4117,7 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { for (int k=0; k #include #include #include @@ -1015,7 +1016,7 @@ class mjCHField : public mjCHField_, private mjsHField { class mjCTexture_ : public mjCBase { protected: - std::vector data; // texture data (rgb, roughness, etc.) + std::vector data_; // texture data (rgb, roughness, etc.) std::string file_; std::string content_type_; @@ -1066,6 +1067,8 @@ class mjCTexture : public mjCTexture_, private mjsTexture { void LoadCustom(mjResource* resource, std::vector& image, unsigned int& w, unsigned int& h); + + bool clear_data_; // if true, data_ is empty and should be filled by Compile }; diff --git a/src/xml/xml.cc b/src/xml/xml.cc index e299cea2..24196f8a 100644 --- a/src/xml/xml.cc +++ b/src/xml/xml.cc @@ -406,5 +406,11 @@ std::string WriteXML(const mjSpec* spec, char* error, int nerror) { mjXWriter writer; writer.SetModel(spec); - return writer.Write(error, nerror); + + try { + return writer.Write(error, nerror); + } catch (mjXError err) { + mjCopyError(error, err.message, nerror); + return ""; + } } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index ceb5d836..48274dfb 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -318,7 +318,7 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* pmat, mjCDef* d } if (has_non_rgb) { // // TODO elem = InsertEnd(section, "role"); - mju_error("mjXWriter: no support for non-RGB textures."); + throw mjXError(0, "no support for non-RGB textures."); } WriteAttrKey(elem, "texuniform", bool_map, 2, pmat->texuniform, def->Material().texuniform); WriteAttr(elem, "texrepeat", 2, pmat->texrepeat, def->Material().texrepeat); @@ -1452,6 +1452,14 @@ void mjXWriter::Asset(XMLElement* root) { WriteAttrInt(elem, "height", ptex->height); } + // write buffer + else if (ptex->get_cubefiles()[0].empty() && ptex->get_cubefiles()[1].empty() && + ptex->get_cubefiles()[2].empty() && ptex->get_cubefiles()[3].empty() && + ptex->get_cubefiles()[4].empty() && ptex->get_cubefiles()[5].empty() && + ptex->get_file().empty() && ptex->gridsize[0] == 1 && ptex->gridsize[1] == 1) { + throw mjXError(0, "no support for buffer textures."); + } + // write textures loaded from files else { // write single file diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index e51d0935..ae4fa924 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -15,6 +15,8 @@ // Tests for user/user_api.cc. #include +#include +#include #include #include #include @@ -272,6 +274,30 @@ TEST_F(PluginTest, RecompileCompareObjCache) { mj_deleteVFS(vfs.get()); } +// tiny RGB 2 x 3 PNG file +static constexpr uint8_t tex1[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x12, 0x16, 0xf1, 0x4d, 0x00, 0x00, 0x00, + 0x1c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x78, 0xc1, 0xc0, 0xc0, + 0xc0, 0xf0, 0xbf, 0xb8, 0xb8, 0x98, 0x81, 0xe1, 0x3f, 0xc3, 0xff, 0xff, + 0xff, 0xc5, 0xc4, 0xc4, 0x00, 0x46, 0xd7, 0x07, 0x7f, 0xd2, 0x52, 0xa1, + 0x41, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, + 0x82 +}; + +// previous PNG file, but rotated by 180 degrees +static constexpr uint8_t tex2[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x12, 0x16, 0xf1, 0x4d, 0x00, 0x00, 0x00, + 0x1c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x10, 0x13, 0x13, 0xfb, + 0xff, 0xff, 0x3f, 0xc3, 0x7f, 0x06, 0x96, 0xd8, 0xd8, 0x58, 0x46, 0x46, + 0x86, 0x17, 0x0c, 0x0c, 0x00, 0x49, 0x22, 0x06, 0x44, 0xe4, 0x91, 0xb8, + 0x83, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, + 0x82 +}; + TEST_F(PluginTest, RecompileComparePngCache) { static constexpr char xml[] = R"( @@ -286,31 +312,6 @@ TEST_F(PluginTest, RecompileComparePngCache) { )"; - // tiny RGB 2 x 3 PNG file - static constexpr unsigned char tex1[] = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x12, 0x16, 0xf1, 0x4d, 0x00, 0x00, 0x00, - 0x1c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x78, 0xc1, 0xc0, 0xc0, - 0xc0, 0xf0, 0xbf, 0xb8, 0xb8, 0x98, 0x81, 0xe1, 0x3f, 0xc3, 0xff, 0xff, - 0xff, 0xc5, 0xc4, 0xc4, 0x00, 0x46, 0xd7, 0x07, 0x7f, 0xd2, 0x52, 0xa1, - 0x41, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, - 0x82 - }; - - // previous PNG file, but rotated by 180 degrees - static constexpr unsigned char tex2[] = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x12, 0x16, 0xf1, 0x4d, 0x00, 0x00, 0x00, - 0x1c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x10, 0x13, 0x13, 0xfb, - 0xff, 0xff, 0x3f, 0xc3, 0x7f, 0x06, 0x96, 0xd8, 0xd8, 0x58, 0x46, 0x46, - 0x86, 0x17, 0x0c, 0x0c, 0x00, 0x49, 0x22, 0x06, 0x44, 0xe4, 0x91, 0xb8, - 0x83, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, - 0x82 - }; - - auto vfs = std::make_unique(); mj_defaultVFS(vfs.get()); mj_addBufferVFS(vfs.get(), "tex.png", tex1, sizeof(tex1)); @@ -334,6 +335,49 @@ TEST_F(PluginTest, RecompileComparePngCache) { mj_deleteVFS(vfs.get()); } +// -------------------------------- test textures ------------------------------ + +TEST_F(PluginTest, TextureFromBuffer) { + mjSpec* spec = mj_makeSpec(); + + mjsTexture* t1 = mjs_addTexture(spec); + mjs_setString(t1->name, "tex1"); + t1->type = mjTEXTURE_2D; + t1->width = 3; + t1->height = 2; + t1->nchannel = 3; + mjs_setBuffer(t1->data, (std::byte*)tex1, 18); + + mjsTexture* t2 = mjs_addTexture(spec); + mjs_setString(t2->name, "tex2"); + t2->type = mjTEXTURE_2D; + t2->width = 3; + t2->height = 2; + t2->nchannel = 3; + mjs_setBuffer(t2->data, (std::byte*)tex2, 18); + + mjsMaterial* mat = mjs_addMaterial(spec, nullptr); + mjs_setString(mat->name, "mat"); + mjs_setInStringVec(mat->textures, mjTEXROLE_RGB, "tex1"); + mjs_setInStringVec(mat->textures, mjTEXROLE_ORM, "tex2"); + + mjsGeom* geom = mjs_addGeom(mjs_findBody(spec, "world"), nullptr); + mjs_setString(geom->material, "mat"); + geom->size[0] = 1; + + mjModel* m = mj_compile(spec, nullptr); + EXPECT_THAT(m, NotNull()); + EXPECT_EQ(m->ntex, 2); + + std::array err; + std::array str; + mj_saveXMLString(spec, str.data(), str.size(), err.data(), err.size()); + EXPECT_STREQ(err.data(), "XML Error: no support for buffer textures."); + + mj_deleteModel(m); + mj_deleteSpec(spec); +} + // -------------------------------- test attach -------------------------------- static constexpr char xml_child[] = R"(