diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index a57f1a1f..b2ce9370 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -1526,6 +1526,12 @@ still be specified here but this functionality is now deprecated and will be rem with the texrepeat attribute of :ref:`material `. The data can be loaded from a single file or created procedurally. +.. _asset-texture-colorspace: + +:at:`colorspace`: :at-val:`[auto, linear, sRGB], "auto"` + This attribute determines the color space of the texture. The default value ``auto`` means that the color space will + be determined from the image file itself. If no color space is defined in the file, then ``linear`` is assumed. + .. _asset-texture-content_type: :at:`content_type`: :at-val:`string, optional` diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 1306115e..bd71743b 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -153,17 +153,17 @@ | :ref:`texture | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`name` | :ref:`type` | :ref:`content_type` | :ref:`file` | | +| | | | :ref:`name` | :ref:`type` | :ref:`colorspace` | :ref:`content_type` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`gridsize` | :ref:`gridlayout` | :ref:`fileright` | :ref:`fileleft` | | +| | | | :ref:`file` | :ref:`gridsize` | :ref:`gridlayout` | :ref:`fileright` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`fileup` | :ref:`filedown` | :ref:`filefront` | :ref:`fileback` | | +| | | | :ref:`fileleft` | :ref:`fileup` | :ref:`filedown` | :ref:`filefront` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`builtin` | :ref:`rgb1` | :ref:`rgb2` | :ref:`mark` | | +| | | | :ref:`fileback` | :ref:`builtin` | :ref:`rgb1` | :ref:`rgb2` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`markrgb` | :ref:`random` | :ref:`width` | :ref:`height` | | +| | | | :ref:`mark` | :ref:`markrgb` | :ref:`random` | :ref:`width` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`hflip` | :ref:`vflip` | :ref:`nchannel` | | | +| | | | :ref:`height` | :ref:`hflip` | :ref:`vflip` | :ref:`nchannel` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| asset |br| |_| |L| | | .. table:: | diff --git a/doc/includes/references.h b/doc/includes/references.h index 9a973092..b9adc621 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -544,6 +544,11 @@ typedef enum mjtTextureRole_ { // role of texture map in rendering mjTEXROLE_ORM, // occlusion, roughness, metallic mjNTEXROLE } mjtTextureRole; +typedef enum mjtColorSpace_ { // type of color space encoding + mjCOLORSPACE_AUTO = 0, // attempts to autodetect color space, defaults to linear + mjCOLORSPACE_LINEAR, // linear color space + mjCOLORSPACE_SRGB // standard RGB color space +} mjtColorSpace; typedef enum mjtIntegrator_ { // integrator mode mjINT_EULER = 0, // semi-implicit Euler mjINT_RK4, // 4th-order Runge Kutta @@ -1326,6 +1331,7 @@ struct mjModel_ { // textures int* tex_type; // texture type (mjtTexture) (ntex x 1) + int* tex_colorspace; // texture colorspace (mjtColorSpace) (ntex x 1) int* tex_height; // number of rows in texture image (ntex x 1) int* tex_width; // number of columns in texture image (ntex x 1) int* tex_nchannel; // number of channels in texture image (ntex x 1) @@ -2150,6 +2156,7 @@ typedef struct mjsTexture_ { // texture specification mjsElement* element; // element type mjString* name; // name mjtTexture type; // texture type + mjtColorSpace colorspace; // colorspace // method 1: builtin int builtin; // builtin type (mjtBuiltin) diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 0c531664..05b3d832 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -151,6 +151,13 @@ typedef enum mjtTextureRole_ { // role of texture map in rendering } mjtTextureRole; +typedef enum mjtColorSpace_ { // type of color space encoding + mjCOLORSPACE_AUTO = 0, // attempts to autodetect color space, defaults to linear + mjCOLORSPACE_LINEAR, // linear color space + mjCOLORSPACE_SRGB // standard RGB color space +} mjtColorSpace; + + typedef enum mjtIntegrator_ { // integrator mode mjINT_EULER = 0, // semi-implicit Euler mjINT_RK4, // 4th-order Runge Kutta @@ -994,6 +1001,7 @@ struct mjModel_ { // textures int* tex_type; // texture type (mjtTexture) (ntex x 1) + int* tex_colorspace; // texture colorspace (mjtColorSpace) (ntex x 1) int* tex_height; // number of rows in texture image (ntex x 1) int* tex_width; // number of columns in texture image (ntex x 1) int* tex_nchannel; // number of channels in texture image (ntex x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index d1b89d8e..20568f2f 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -525,6 +525,7 @@ typedef struct mjsTexture_ { // texture specification mjsElement* element; // element type mjString* name; // name mjtTexture type; // texture type + mjtColorSpace colorspace; // colorspace // method 1: builtin int builtin; // builtin type (mjtBuiltin) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 5ffe5764..57673285 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -439,6 +439,7 @@ XNV ( float, hfield_data, nhfielddata, 1 ) \ X ( int, hfield_pathadr, nhfield, 1 ) \ X ( int, tex_type, ntex, 1 ) \ + X ( int, tex_colorspace, ntex, 1 ) \ X ( int, tex_height, ntex, 1 ) \ X ( int, tex_width, ntex, 1 ) \ X ( int, tex_nchannel, ntex, 1 ) \ diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index a1cab44f..dc828a57 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -139,6 +139,16 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjNTEXROLE', 10), ]), )), + ('mjtColorSpace', + EnumDecl( + name='mjtColorSpace', + declname='enum mjtColorSpace_', + values=dict([ + ('mjCOLORSPACE_AUTO', 0), + ('mjCOLORSPACE_LINEAR', 1), + ('mjCOLORSPACE_SRGB', 2), + ]), + )), ('mjtIntegrator', EnumDecl( name='mjtIntegrator', diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 379cdc84..5685ac1c 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -3339,6 +3339,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='texture type (mjtTexture)', array_extent=('ntex',), ), + StructFieldDecl( + name='tex_colorspace', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='texture colorspace (mjtColorSpace)', + array_extent=('ntex',), + ), StructFieldDecl( name='tex_height', type=PointerType( @@ -9613,6 +9621,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjtTexture'), doc='texture type', ), + StructFieldDecl( + name='colorspace', + type=ValueType(name='mjtColorSpace'), + doc='colorspace', + ), StructFieldDecl( name='builtin', type=ValueType(name='int'), diff --git a/src/render/render_context.c b/src/render/render_context.c index b305f2dd..b1f279bb 100644 --- a/src/render/render_context.c +++ b/src/render/render_context.c @@ -1387,15 +1387,20 @@ void mjr_uploadTexture(const mjModel* m, const mjrContext* con, int texid) { // assign data int type = 0; + int internaltype = 0; if (m->tex_nchannel[texid] == 3) { type = GL_RGB; + internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_EXT : GL_RGB; } else if (m->tex_nchannel[texid] == 4) { type = GL_RGBA; + internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_ALPHA8_EXT : GL_RGBA; } else { mju_error("Number of channels not supported: %d", m->tex_nchannel[texid]); } - glTexImage2D(GL_TEXTURE_2D, 0, type, m->tex_width[texid], m->tex_height[texid], 0, - type, GL_UNSIGNED_BYTE, m->tex_data + m->tex_adr[texid]); + + glTexImage2D(GL_TEXTURE_2D, 0, internaltype, m->tex_width[texid], + m->tex_height[texid], 0, type, GL_UNSIGNED_BYTE, + m->tex_data + m->tex_adr[texid]); // generate mipmaps glGenerateMipmap(GL_TEXTURE_2D); diff --git a/src/user/user_init.c b/src/user/user_init.c index 01f4a8b9..fd8a7132 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -273,6 +273,7 @@ void mjs_defaultSkin(mjsSkin* skin) { void mjs_defaultTexture(mjsTexture* texture) { memset(texture, 0, sizeof(mjsTexture)); texture->type = mjTEXTURE_CUBE; + texture->colorspace = mjCOLORSPACE_AUTO; texture->rgb1[0] = texture->rgb1[1] = texture->rgb1[2] = 0.8; texture->rgb2[0] = texture->rgb2[1] = texture->rgb2[2] = 0.5; texture->random = 0.01; diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 49f79ead..bd3eb0ac 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3285,6 +3285,7 @@ void mjCModel::CopyObjects(mjModel* m) { // set fields m->tex_type[i] = ptex->type; + m->tex_colorspace[i] = ptex->colorspace; m->tex_height[i] = ptex->height; m->tex_width[i] = ptex->width; m->tex_nchannel[i] = ptex->nchannel; diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 51c0f63a..2e2c63b2 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -55,6 +55,8 @@ class PNGImage { LodePNGColorType color_type); int Width() const { return width_; } int Height() const { return height_; } + bool IsSRGB() const { return is_srgb_; } + uint8_t operator[] (int i) const { return data_[i]; } std::vector& MoveData() { return data_; } @@ -65,6 +67,7 @@ class PNGImage { int width_; int height_; + bool is_srgb_; LodePNGColorType color_type_; std::vector data_; }; @@ -104,8 +107,11 @@ PNGImage PNGImage::Load(const mjCBase* obj, mjResource* resource, // decode PNG from buffer unsigned int w, h; - unsigned err = lodepng::decode(image.data_, w, h, - buffer, nbuffer, image.color_type_, 8); + + lodepng::State state; + state.info_raw.colortype = image.color_type_; + state.info_raw.bitdepth = 8; + unsigned err = lodepng::decode(image.data_, w, h, state, buffer, nbuffer); // check for errors if (err) { @@ -116,6 +122,7 @@ PNGImage PNGImage::Load(const mjCBase* obj, mjResource* resource, image.width_ = w; image.height_ = h; + image.is_srgb_ = (state.info_png.srgb_defined == 1); if (image.width_ <= 0 || image.height_ < 0) { std::stringstream ss; @@ -4257,7 +4264,7 @@ void mjCTexture::BuiltinCube(void) { // load PNG file void mjCTexture::LoadPNG(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h) { + unsigned int& w, unsigned int& h, bool& is_srgb) { LodePNGColorType color_type; if (nchannel == 4) { color_type = LCT_RGBA; @@ -4272,13 +4279,14 @@ void mjCTexture::LoadPNG(mjResource* resource, PNGImage png_image = PNGImage::Load(this, resource, color_type); w = png_image.Width(); h = png_image.Height(); + is_srgb = png_image.IsSRGB(); image = png_image.MoveData(); } // load custom file void mjCTexture::LoadCustom(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h) { + unsigned int& w, unsigned int& h, bool& is_srgb) { const void* buffer = 0; int buffer_sz = mju_readResource(resource, &buffer); @@ -4295,6 +4303,9 @@ void mjCTexture::LoadCustom(mjResource* resource, w = pint[0]; h = pint[1]; + // assume linear color space + is_srgb = false; + // check dimensions if (w < 1 || h < 1) { throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n" @@ -4317,7 +4328,7 @@ void mjCTexture::LoadCustom(mjResource* resource, // load from PNG or custom file, flip if specified void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, std::vector& image, - unsigned int& w, unsigned int& h) { + unsigned int& w, unsigned int& h, bool& is_srgb) { std::string asset_type = GetAssetContentType(filename, content_type_); // fallback to custom @@ -4333,9 +4344,9 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, try { if (asset_type == "image/png") { - LoadPNG(resource, image, w, h); + LoadPNG(resource, image, w, h, is_srgb); } else { - LoadCustom(resource, image, w, h); + LoadCustom(resource, image, w, h, is_srgb); } mju_closeResource(resource); } catch(mjCError err) { @@ -4402,12 +4413,16 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, void mjCTexture::Load2D(std::string filename, const mjVFS* vfs) { // load PNG or custom unsigned int w, h; + bool is_srgb; std::vector image; - LoadFlip(filename, vfs, image, w, h); + LoadFlip(filename, vfs, image, w, h, is_srgb); // assign size width = w; height = h; + if (colorspace == mjCOLORSPACE_AUTO) { + colorspace = is_srgb ? mjCOLORSPACE_SRGB : mjCOLORSPACE_LINEAR; + } // allocate and copy data std::int64_t size = static_cast(width)*height; @@ -4435,8 +4450,13 @@ void mjCTexture::LoadCubeSingle(std::string filename, const mjVFS* vfs) { // load PNG or custom unsigned int w, h; + bool is_srgb; std::vector image; - LoadFlip(filename, vfs, image, w, h); + LoadFlip(filename, vfs, image, w, h, is_srgb); + + if (colorspace == mjCOLORSPACE_AUTO) { + colorspace = is_srgb ? mjCOLORSPACE_SRGB : mjCOLORSPACE_LINEAR; + } // check gridsize for compatibility if (w/gridsize[1] != h/gridsize[0] || (w%gridsize[1]) || (h%gridsize[0])) { @@ -4549,8 +4569,14 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { // load PNG or custom unsigned int w, h; + bool is_srgb; std::vector image; - LoadFlip(filename.Str(), vfs, image, w, h); + LoadFlip(filename.Str(), vfs, image, w, h, is_srgb); + + // assume all faces have the same colorspace + if (colorspace == mjCOLORSPACE_AUTO) { + colorspace = is_srgb ? mjCOLORSPACE_SRGB : mjCOLORSPACE_LINEAR; + } // PNG must be square if (w != h) { diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 46df52e6..994cb542 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1328,14 +1328,14 @@ class mjCTexture : public mjCTexture_, private mjsTexture { void LoadFlip(std::string filename, const mjVFS* vfs, // load and flip std::vector& image, - unsigned int& w, unsigned int& h); + unsigned int& w, unsigned int& h, bool& is_srgb); void LoadPNG(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h); + unsigned int& w, unsigned int& h, bool& is_srgb); void LoadCustom(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h); + unsigned int& w, unsigned int& h, bool& is_srgb); bool clear_data_; // if true, data_ is empty and should be filled by Compile }; diff --git a/src/xml/xml_base.h b/src/xml/xml_base.h index 3651d31c..4134f46f 100644 --- a/src/xml/xml_base.h +++ b/src/xml/xml_base.h @@ -35,6 +35,7 @@ extern const int jac_sz; extern const int solver_sz; extern const int equality_sz; extern const int texture_sz; +extern const int colorspace_sz; extern const int builtin_sz; extern const int mark_sz; extern const int dyn_sz; @@ -60,6 +61,7 @@ extern const mjMap jac_map[]; extern const mjMap solver_map[]; extern const mjMap equality_map[]; extern const mjMap texture_map[]; +extern const mjMap colorspace_map[]; extern const mjMap texrole_map[]; extern const mjMap builtin_map[]; extern const mjMap mark_map[]; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index f2b5f1e9..52d3181a 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -241,8 +241,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"<"}, {"bone", "*", "5", "body", "bindpos", "bindquat", "vertid", "vertweight"}, {">"}, - {"texture", "*", "23", "name", "type", "content_type", "file", "gridsize", "gridlayout", - "fileright", "fileleft", "fileup", "filedown", "filefront", "fileback", + {"texture", "*", "24", "name", "type", "colorspace", "content_type", "file", "gridsize", + "gridlayout", "fileright", "fileleft", "fileup", "filedown", "filefront", "fileback", "builtin", "rgb1", "rgb2", "mark", "markrgb", "random", "width", "height", "hflip", "vflip", "nchannel"}, {"material", "*", "12", "name", "class", "texture", "texrepeat", "texuniform", @@ -650,6 +650,15 @@ const mjMap texture_map[texture_sz] = { }; +// colorspace for texture +const int colorspace_sz = 3; +const mjMap colorspace_map[colorspace_sz] = { + {"auto", mjCOLORSPACE_AUTO}, + {"linear", mjCOLORSPACE_LINEAR}, + {"sRGB", mjCOLORSPACE_SRGB} +}; + + // builtin type for texture const int builtin_sz = 4; const mjMap builtin_map[builtin_sz] = { @@ -3194,6 +3203,9 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { if (MapValue(elem, "type", &n, texture_map, texture_sz)) { texture->type = (mjtTexture)n; } + if (MapValue(elem, "colorspace", &n, colorspace_map, colorspace_sz)) { + texture->colorspace = (mjtColorSpace)n; + } if (ReadAttrTxt(elem, "name", texname)) { mjs_setString(texture->name, texname.c_str()); } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index b6464cd1..f850c82b 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1488,6 +1488,7 @@ void mjXWriter::Asset(XMLElement* root) { // write common attributes WriteAttrKey(elem, "type", texture_map, texture_sz, texture->type); + WriteAttrKey(elem, "colorspace", colorspace_map, colorspace_sz, texture->colorspace); WriteAttrTxt(elem, "name", texture->name); // write builtin diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index dc7b8078..1172e758 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -225,6 +225,11 @@ public enum mjtTextureRole : int{ mjTEXROLE_ORM = 9, mjNTEXROLE = 10, } +public enum mjtColorSpace : int{ + mjCOLORSPACE_AUTO = 0, + mjCOLORSPACE_LINEAR = 1, + mjCOLORSPACE_SRGB = 2, +} public enum mjtIntegrator : int{ mjINT_EULER = 0, mjINT_RK4 = 1, @@ -5569,6 +5574,7 @@ public unsafe struct mjModel_ { public float* hfield_data; public int* hfield_pathadr; public int* tex_type; + public int* tex_colorspace; public int* tex_height; public int* tex_width; public int* tex_nchannel;