From 568620dd2f0f661dae2baafe3c5dee6d1d9f88ae Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Tue, 27 May 2025 05:03:04 -0700 Subject: [PATCH] Add `texture` attribute to lights. PiperOrigin-RevId: 763739805 Change-Id: I333ed2ac68f30f79f688ca01d7b5fd6bf2d836c9 --- doc/XMLreference.rst | 5 +++++ doc/XMLschema.rst | 2 +- doc/includes/references.h | 3 +++ include/mujoco/mjmodel.h | 1 + include/mujoco/mjspec.h | 1 + include/mujoco/mjvisualize.h | 1 + include/mujoco/mjxmacro.h | 1 + python/mujoco/introspect/structs.py | 20 ++++++++++++++++++++ python/mujoco/structs.cc | 1 + src/engine/engine_vis_visualize.c | 5 +++++ src/user/user_model.cc | 1 + src/user/user_objects.cc | 18 ++++++++++++++++++ src/user/user_objects.h | 4 ++++ src/xml/xml_native_reader.cc | 9 ++++++--- src/xml/xml_native_writer.cc | 1 + test/user/user_objects_test.cc | 23 +++++++++++++++++++++++ test/xml/testdata/lights.xml | 12 ++++++++++++ unity/Runtime/Bindings/MjBindings.cs | 2 ++ 18 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 test/xml/testdata/lights.xml diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index e276c831..2e716ca5 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2961,6 +2961,11 @@ Attributes may be applied or ignored depending on the lighting model being used. The color of the light. For the Phong (default) lighting model, this defines the diffuse color of the light. +.. _body-light-texture: + +:at:`texture`: :at-val:`string, optional` + The texture to use for image-based lighting. This is unused by the default Phong lighting model. + .. _body-light-intensity: :at:`intensity`: :at-val:`real, "1000.0"` diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index a95e5211..21272c90 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -321,7 +321,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`cutoff` | :ref:`exponent` | :ref:`ambient` | :ref:`diffuse` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`specular` | :ref:`mode` | :ref:`target` | | | +| | | | :ref:`specular` | :ref:`mode` | :ref:`target` | :ref:`texture` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| body |br| |_| |L| | | .. table:: | diff --git a/doc/includes/references.h b/doc/includes/references.h index fc9943c1..92aae862 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1186,6 +1186,7 @@ struct mjModel_ { int* light_bodyid; // id of light's body (nlight x 1) int* light_targetbodyid; // id of targeted body; -1: none (nlight x 1) int* light_type; // spot, directional, etc. (mjtLightType) (nlight x 1) + int* light_texid; // texture id for image lights (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) float* light_intensity; // intensity, in candela (nlight x 1) @@ -2043,6 +2044,7 @@ typedef struct mjsLight_ { // light specification // intrinsics mjtByte active; // is light active mjtLightType type; // type of light + mjString* texture; // texture name for image lights mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius, for soft shadows float intensity; // intensity, in candelas @@ -2847,6 +2849,7 @@ struct mjvLight_ { // OpenGL light float pos[3]; // position rel. to body frame float dir[3]; // direction rel. to body frame int type; // type (mjtLightType) + int texid; // texture id for image lights float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 8c50af50..75eb3cd0 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -858,6 +858,7 @@ struct mjModel_ { int* light_bodyid; // id of light's body (nlight x 1) int* light_targetbodyid; // id of targeted body; -1: none (nlight x 1) int* light_type; // spot, directional, etc. (mjtLightType) (nlight x 1) + int* light_texid; // texture id for image lights (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) float* light_intensity; // intensity, in candela (nlight x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index c422eedd..3c00f478 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -395,6 +395,7 @@ typedef struct mjsLight_ { // light specification // intrinsics mjtByte active; // is light active mjtLightType type; // type of light + mjString* texture; // texture name for image lights mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius, for soft shadows float intensity; // intensity, in candelas diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index 5903ba59..efff4d03 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -262,6 +262,7 @@ struct mjvLight_ { // OpenGL light float pos[3]; // position rel. to body frame float dir[3]; // direction rel. to body frame int type; // type (mjtLightType) + int texid; // texture id for image lights float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 3a8a6fcf..93542a88 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -300,6 +300,7 @@ X ( int, light_bodyid, nlight, 1 ) \ X ( int, light_targetbodyid, nlight, 1 ) \ X ( int, light_type, nlight, 1 ) \ + X ( int, light_texid, nlight, 1 ) \ X ( mjtByte, light_castshadow, nlight, 1 ) \ X ( float, light_bulbradius, nlight, 1 ) \ X ( float, light_intensity, nlight, 1 ) \ diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 283a158a..49b8752b 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -2227,6 +2227,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='spot, directional, etc. (mjtLightType)', array_extent=('nlight',), ), + StructFieldDecl( + name='light_texid', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='texture id for image lights', + array_extent=('nlight',), + ), StructFieldDecl( name='light_castshadow', type=PointerType( @@ -6702,6 +6710,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='type (mjtLightType)', ), + StructFieldDecl( + name='texid', + type=ValueType(name='int'), + doc='texture id for image lights', + ), StructFieldDecl( name='attenuation', type=ArrayType( @@ -8996,6 +9009,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjtLightType'), doc='type of light', ), + StructFieldDecl( + name='texture', + type=PointerType( + inner_type=ValueType(name='mjString'), + ), + doc='texture name for image lights', + ), StructFieldDecl( name='castshadow', type=ValueType(name='mjtByte'), diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index dc98e3dd..1b92c61a 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1074,6 +1074,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(exponent); X(headlight); X(type); + X(texid); X(castshadow); X(bulbradius); X(intensity); diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index 6f9364f1..d0d57b3f 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -2143,8 +2143,12 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { // set default properties memset(thislight, 0, sizeof(mjvLight)); thislight->headlight = 1; + thislight->texid = -1; thislight->type = mjLIGHT_DIRECTIONAL; thislight->castshadow = 0; + thislight->bulbradius = 0.02; + thislight->intensity = 1000; + thislight->range = 10; // compute head position and gaze direction in model space mjtNum hpos[3], hfwd[3]; @@ -2170,6 +2174,7 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { // copy properties memset(thislight, 0, sizeof(mjvLight)); thislight->type = m->light_type[i]; + thislight->texid = m->light_texid[i]; thislight->castshadow = m->light_castshadow[i]; thislight->bulbradius = m->light_bulbradius[i]; thislight->intensity = m->light_intensity[i]; diff --git a/src/user/user_model.cc b/src/user/user_model.cc index d7f862eb..e3d4544a 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2670,6 +2670,7 @@ void mjCModel::CopyTree(mjModel* m) { m->light_mode[lid] = (int)pl->mode; m->light_targetbodyid[lid] = pl->targetbodyid; m->light_type[lid] = pl->type; + m->light_texid[lid] = pl->texid; m->light_castshadow[lid] = (mjtByte)pl->castshadow; m->light_active[lid] = (mjtByte)pl->active; mjuu_copyvec(m->light_pos+3*lid, pl->pos, 3); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 688c9729..80d57a8d 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -3553,7 +3553,10 @@ mjCLight::mjCLight(mjCModel* _model, mjCDef* _def) { // clear private variables body = 0; targetbodyid = -1; + texid = -1; spec_targetbody_.clear(); + spec_texture_.clear(); + // reset to default if given if (_def) { @@ -3593,6 +3596,7 @@ void mjCLight::PointToLocal() { spec.element = static_cast(this); spec.name = &name; spec.targetbody = &spec_targetbody_; + spec.texture = &spec_texture_; spec.info = &info; targetbody = nullptr; } @@ -3604,6 +3608,9 @@ void mjCLight::NameSpace(const mjCModel* m) { if (!spec_targetbody_.empty()) { spec_targetbody_ = m->prefix + spec_targetbody_ + m->suffix; } + if (!spec_texture_.empty()) { + spec_texture_ = m->prefix + spec_texture_ + m->suffix; + } } @@ -3611,6 +3618,7 @@ void mjCLight::NameSpace(const mjCModel* m) { void mjCLight::CopyFromSpec() { *static_cast(this) = spec; targetbody_ = spec_targetbody_; + texture_ = spec_texture_; } @@ -3643,6 +3651,16 @@ void mjCLight::Compile(void) { throw mjCError(this, "unknown target body in light"); } } + + // get texture + if (!texture_.empty()) { + mjCTexture* tex = (mjCTexture*)model->FindObject(mjOBJ_TEXTURE, texture_); + if (tex) { + texid = tex->id; + } else { + throw mjCError(this, "unknown target body in light"); + } + } } diff --git a/src/user/user_objects.h b/src/user/user_objects.h index db01349a..9c150646 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -793,6 +793,9 @@ class mjCLight_ : public mjCBase { protected: mjCBody* body; // light's body int targetbodyid; // id of target body; -1: none + int texid; // id of texture; -1: none + std::string texture_; + std::string spec_texture_; std::string targetbody_; std::string spec_targetbody_; }; @@ -814,6 +817,7 @@ class mjCLight : public mjCLight_, private mjsLight { // used by mjXWriter and mjCModel const std::string& get_targetbody() const { return targetbody_; } + const std::string& get_texture() const { return texture_; } void SetParent(mjCBody* _body) { body = _body; } mjCBody* GetParent() const { return body; } diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 02908ed2..c5ad9c8f 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -281,9 +281,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"camera", "*", "20", "name", "class", "orthographic", "fovy", "ipd", "resolution", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, - {"light", "*", "19", "name", "class", "directional", "type", "castshadow", "active", + {"light", "*", "20", "name", "class", "directional", "type", "castshadow", "active", "pos", "dir", "bulbradius", "intensity", "range", "attenuation", "cutoff", - "exponent", "ambient", "diffuse", "specular", "mode", "target"}, + "exponent", "ambient", "diffuse", "specular", "mode", "target", "texture"}, {"plugin", "*", "2", "plugin", "instance"}, {"<"}, {"config", "*", "2", "key", "value"}, @@ -1864,12 +1864,15 @@ void mjXReader::OneCamera(XMLElement* elem, mjsCamera* camera) { void mjXReader::OneLight(XMLElement* elem, mjsLight* light) { int n; bool has_directional = false; - string text, name, targetbody; + string text, name, texture, targetbody; // read attributes if (ReadAttrTxt(elem, "name", name)) { mjs_setString(light->name, name.c_str()); } + if (ReadAttrTxt(elem, "texture", texture)) { + mjs_setString(light->texture, texture.c_str()); + } if (ReadAttrTxt(elem, "target", targetbody)) { mjs_setString(light->targetbody, targetbody.c_str()); } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 9f9a1b9d..f28041a8 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -608,6 +608,7 @@ void mjXWriter::OneLight(XMLElement* elem, const mjCLight* light, mjCDef* def, WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity); WriteAttr(elem, "range", 1, &light->range, &def->Light().range); WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type); + WriteAttrTxt(elem, "texture", light->get_texture()); WriteAttrKey(elem, "castshadow", bool_map, 2, light->castshadow, def->Light().castshadow); WriteAttrKey(elem, "active", bool_map, 2, light->active, def->Light().active); WriteAttr(elem, "attenuation", 3, light->attenuation, def->Light().attenuation); diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index 37bec917..396b2ea0 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -2294,6 +2294,29 @@ TEST_F(UserObjectsTest, FrameTransformsLight) { mj_deleteModel(m); } +TEST_F(ContentTypeTest, ImageLightsReferenceTexture) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + + std::array error; + mjModel* m = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(m, NotNull()); + EXPECT_EQ(m->ntex, 1); + EXPECT_EQ(m->nlight, 1); + EXPECT_THAT(m->light_texid[0], 0); + mj_deleteModel(m); +} + // ------------- test bvh ------------------------------------------------------ TEST_F(UserObjectsTest, RobustBVH) { diff --git a/test/xml/testdata/lights.xml b/test/xml/testdata/lights.xml new file mode 100644 index 00000000..7937e1e7 --- /dev/null +++ b/test/xml/testdata/lights.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d15803a7..11698ed6 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5441,6 +5441,7 @@ public unsafe struct mjModel_ { public int* light_bodyid; public int* light_targetbodyid; public int* light_type; + public int* light_texid; public byte* light_castshadow; public float* light_bulbradius; public float* light_intensity; @@ -6082,6 +6083,7 @@ public unsafe struct mjvLight_ { public fixed float pos[3]; public fixed float dir[3]; public int type; + public int texid; public fixed float attenuation[3]; public float cutoff; public float exponent;