From d43c3ed42682c3332259c0b4c2904d24d5610dc4 Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Thu, 9 Jul 2026 11:08:43 -0400 Subject: [PATCH 1/3] Move texture block copy out of render context so it can be modified online via mjmodel --- doc/APIreference/APIglobals.rst | 3 --- doc/changelog.rst | 9 +++++++ include/mujoco/mjrender.h | 6 ----- include/mujoco/mjvisualize.h | 3 +++ python/mujoco/introspect/structs.py | 42 +++++++++++++---------------- python/mujoco/render.cc | 12 --------- python/mujoco/structs.cc | 3 +++ python/mujoco/structs.h | 1 + python/mujoco/structs_wrappers.cc | 6 +++-- src/engine/engine_vis_visualize.c | 8 ++++++ src/render/classic/render_context.c | 40 --------------------------- src/render/classic/render_gl3.c | 16 +++++------ 12 files changed, 54 insertions(+), 95 deletions(-) diff --git a/doc/APIreference/APIglobals.rst b/doc/APIreference/APIglobals.rst index b25cb5fd..fec3dc6b 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -583,9 +583,6 @@ Defined in `mjrender.h : public WrapperBase { X(auxFBO_r); X(auxColor); X(auxColor_r); - X(mat_texid); - X(mat_texuniform); - X(mat_texrepeat); X(textureType); X(texture); X(skinvertVBO); @@ -98,9 +95,6 @@ MjrContextWrapper::MjWrapper() X(auxFBO_r), X(auxColor), X(auxColor_r), - X(mat_texid), - X(mat_texuniform), - X(mat_texrepeat), X(textureType), X(texture), X_SKIN(skinvertVBO), @@ -125,9 +119,6 @@ MjrContextWrapper::MjWrapper(const MjModelWrapper& model, int fontscale) X(auxFBO_r), X(auxColor), X(auxColor_r), - X(mat_texid), - X(mat_texuniform), - X(mat_texrepeat), X(textureType), X(texture), X_SKIN(skinvertVBO), @@ -228,9 +219,6 @@ PYBIND11_MODULE(_render, pymodule, pybind11::mod_gil_not_used()) { X(auxFBO_r); X(auxColor); X(auxColor_r); - X(mat_texid); - X(mat_texuniform); - X(mat_texrepeat); X(textureType); X(texture); X(skinvertVBO); diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 359b14eb..5971ab77 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1185,6 +1185,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(objid); X(category); X(matid); + X(texid); + X(texuniform); X(texcoord); X(segid); X(emission); @@ -1201,6 +1203,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(pos); X(mat); X(rgba); + X(texrepeat); #undef X DefinePyStr(mjvGeom, "label", &raw::MjvGeom::label); diff --git a/python/mujoco/structs.h b/python/mujoco/structs.h index f8b72049..450c7dbf 100644 --- a/python/mujoco/structs.h +++ b/python/mujoco/structs.h @@ -832,6 +832,7 @@ class MjWrapper : public WrapperBase { X(pos); X(mat); X(rgba); + X(texrepeat); #undef X }; diff --git a/python/mujoco/structs_wrappers.cc b/python/mujoco/structs_wrappers.cc index 60bd3bff..4e47446b 100644 --- a/python/mujoco/structs_wrappers.cc +++ b/python/mujoco/structs_wrappers.cc @@ -1193,7 +1193,8 @@ MjvGeomWrapper::MjWrapper() static_assert(sizeof(ptr_->mat) == sizeof(ptr_->mat[0]) * 9); return InitPyArray(std::array{3, 3}, ptr_->mat, owner_); }()), - X(rgba) { + X(rgba), + X(texrepeat) { mjv_initGeom(ptr_, mjGEOM_NONE, nullptr, nullptr, nullptr, nullptr); } @@ -1205,7 +1206,8 @@ MjvGeomWrapper::MjWrapper(raw::MjvGeom* ptr, py::handle owner) static_assert(sizeof(ptr_->mat) == sizeof(ptr_->mat[0]) * 9); return InitPyArray(std::array{3, 3}, ptr_->mat, owner_); }()), - X(rgba) {} + X(rgba), + X(texrepeat) {} #undef X MjvGeomWrapper::MjWrapper(const MjvGeomWrapper& other) : MjvGeomWrapper() { diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index 6350da14..1eda8563 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -241,6 +241,10 @@ static void setMaterial(const mjModel* m, mjvGeom* geom, int matid, const float* // set texture if (flags[mjVIS_TEXTURE] && matid >= 0) { geom->matid = matid; + geom->texid = m->mat_texid[matid*mjNTEXROLE + mjTEXROLE_RGB]; + geom->texuniform = m->mat_texuniform[matid]; + geom->texrepeat[0] = m->mat_texrepeat[2*matid]; + geom->texrepeat[1] = m->mat_texrepeat[2*matid+1]; } // scale alpha for dynamic geoms only @@ -379,6 +383,10 @@ void mjv_initGeom(mjvGeom* geom, int type, const mjtNum* size, // set defaults that cannot be assigned via this function geom->dataid = -1; geom->matid = -1; + geom->texid = -1; + geom->texuniform = 0; + geom->texrepeat[0] = 0; + geom->texrepeat[1] = 0; geom->texcoord = 0; geom->emission = 0; geom->specular = 0.5; diff --git a/src/render/classic/render_context.c b/src/render/classic/render_context.c index c7a5dd0b..52402c7f 100644 --- a/src/render/classic/render_context.c +++ b/src/render/classic/render_context.c @@ -14,7 +14,6 @@ #include "render/classic/render_context.h" -#include // IWYU pragma: keep #include #include #include @@ -1299,44 +1298,6 @@ static void makeFont(mjrContext* con, int fontscale) { } } -// make materials, just for those that have textures -static void makeMaterial(const mjModel* m, mjrContext* con) { - memset(con->mat_texid, -1, sizeof(con->mat_texid)); - memset(con->mat_texuniform, 0, sizeof(con->mat_texuniform)); - memset(con->mat_texrepeat, 0, sizeof(con->mat_texrepeat)); - - // find skybox texture - for (int i=0; i < m->ntex; i++) { - if (m->tex_type[i] == mjTEXTURE_SKYBOX) { - if (m->nmat >= mjMAXMATERIAL-2) { - mju_error("With skybox, maximum number of materials is %d, got %" PRId64, - mjMAXMATERIAL-1, m->nmat); - } - for (int j=0; j < mjNTEXROLE; j++) { - con->mat_texid[mjNTEXROLE * (mjMAXMATERIAL-1) + j] = -1; - } - con->mat_texid[mjNTEXROLE * (mjMAXMATERIAL-1) + mjTEXROLE_RGB] = i; - - break; - } - } - - if (m->nmat >= mjMAXMATERIAL-1) { - mju_error("Maximum number of materials is %d, got %" PRId64, mjMAXMATERIAL, m->nmat); - } - for (int i=0; i < m->nmat; i++) { - if (m->mat_texid[i*mjNTEXROLE + mjTEXROLE_RGB] >= 0) { - for (int j=0; j < mjNTEXROLE; j++) { - con->mat_texid[i*mjNTEXROLE + j] = m->mat_texid[i*mjNTEXROLE + j]; - } - con->mat_texuniform[i] = m->mat_texuniform[i]; - con->mat_texrepeat[2*i] = m->mat_texrepeat[2*i]; - con->mat_texrepeat[2*i+1] = m->mat_texrepeat[2*i+1]; - } - } -} - - // make textures static void makeTexture(const mjModel* m, mjrContext* con) { // checks size @@ -1639,7 +1600,6 @@ void mjr_makeContext_offSize(const mjModel* m, mjrContext* con, int fontscale, // make everything makeOff(con); makeShadow(m, con); - makeMaterial(m, con); makeTexture(m, con); makePlane(m, con); makeMesh(m, con); diff --git a/src/render/classic/render_gl3.c b/src/render/classic/render_gl3.c index 30d246ff..402fcc6a 100644 --- a/src/render/classic/render_gl3.c +++ b/src/render/classic/render_gl3.c @@ -64,7 +64,7 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom int texid = -1; if (geom) { if (geom->matid >= 0) { - texid = con->mat_texid[mjNTEXROLE * geom->matid + mjTEXROLE_RGB]; + texid = geom->texid; } } @@ -119,8 +119,8 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom glBindTexture(GL_TEXTURE_2D, con->texture[texid]); // determine scaling, adjust for pre-scaled geoms - scl[0] = con->mat_texrepeat[geom->matid*2]; - scl[1] = con->mat_texrepeat[geom->matid*2+1]; + scl[0] = geom->texrepeat[0]; + scl[1] = geom->texrepeat[1]; if (geom->dataid >= 0) { if (geom->size[0] > 0) { scl[0] = scl[0] / mju_max(mjMINVAL, geom->size[0]); @@ -132,7 +132,7 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom } // uniform: repeat relative to spatial units rather than object - if (con->mat_texuniform[geom->matid]) { + if (geom->texuniform) { if (geom->size[0] > 0) { scl[0] = scl[0] * geom->size[0]; } @@ -171,11 +171,11 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom // set mapping : cube if (type == mjtexREGULAR) { - mjr_setf4(plane, con->mat_texuniform[geom->matid] ? geom->size[0] : 1, 0, 0, 0); + mjr_setf4(plane, geom->texuniform ? geom->size[0] : 1, 0, 0, 0); glTexGenfv(GL_S, GL_OBJECT_PLANE, plane); - mjr_setf4(plane, 0, con->mat_texuniform[geom->matid] ? geom->size[1] : 1, 0, 0); + mjr_setf4(plane, 0, geom->texuniform ? geom->size[1] : 1, 0, 0); glTexGenfv(GL_T, GL_OBJECT_PLANE, plane); - mjr_setf4(plane, 0, 0, con->mat_texuniform[geom->matid] ? geom->size[2] : 1, 0); + mjr_setf4(plane, 0, 0, geom->texuniform ? geom->size[2] : 1, 0); glTexGenfv(GL_R, GL_OBJECT_PLANE, plane); } @@ -1317,7 +1317,7 @@ void mjr_render(mjrRect viewport, mjvScene* scn, const mjrContext* con) { if (con->textureType[i] == mjTEXTURE_SKYBOX) { // save first skybox texture id in tempgeom memset(&tempgeom, 0, sizeof(mjvGeom)); - tempgeom.matid = mjMAXMATERIAL - 1; + tempgeom.texid = i; // modify settings glDisable(GL_LIGHTING); From 0a6b631d4b0f460faddcaef198e57d43a41a79d9 Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Wed, 15 Jul 2026 11:53:22 -0400 Subject: [PATCH 2/3] Make change non-breaking per haroonq's feedback --- doc/APIreference/APIglobals.rst | 3 +++ doc/changelog.rst | 6 +---- include/mujoco/mjrender.h | 6 +++++ python/mujoco/introspect/structs.py | 24 +++++++++++++++++ python/mujoco/render.cc | 12 +++++++++ src/render/classic/render_context.c | 40 +++++++++++++++++++++++++++++ src/render/classic/render_gl3.c | 25 ++++++++++++------ 7 files changed, 103 insertions(+), 13 deletions(-) diff --git a/doc/APIreference/APIglobals.rst b/doc/APIreference/APIglobals.rst index fec3dc6b..b25cb5fd 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -583,6 +583,9 @@ Defined in `mjrender.h : public WrapperBase { X(auxFBO_r); X(auxColor); X(auxColor_r); + X(mat_texid); + X(mat_texuniform); + X(mat_texrepeat); X(textureType); X(texture); X(skinvertVBO); @@ -95,6 +98,9 @@ MjrContextWrapper::MjWrapper() X(auxFBO_r), X(auxColor), X(auxColor_r), + X(mat_texid), + X(mat_texuniform), + X(mat_texrepeat), X(textureType), X(texture), X_SKIN(skinvertVBO), @@ -119,6 +125,9 @@ MjrContextWrapper::MjWrapper(const MjModelWrapper& model, int fontscale) X(auxFBO_r), X(auxColor), X(auxColor_r), + X(mat_texid), + X(mat_texuniform), + X(mat_texrepeat), X(textureType), X(texture), X_SKIN(skinvertVBO), @@ -219,6 +228,9 @@ PYBIND11_MODULE(_render, pymodule, pybind11::mod_gil_not_used()) { X(auxFBO_r); X(auxColor); X(auxColor_r); + X(mat_texid); + X(mat_texuniform); + X(mat_texrepeat); X(textureType); X(texture); X(skinvertVBO); diff --git a/src/render/classic/render_context.c b/src/render/classic/render_context.c index 52402c7f..c7a5dd0b 100644 --- a/src/render/classic/render_context.c +++ b/src/render/classic/render_context.c @@ -14,6 +14,7 @@ #include "render/classic/render_context.h" +#include // IWYU pragma: keep #include #include #include @@ -1298,6 +1299,44 @@ static void makeFont(mjrContext* con, int fontscale) { } } +// make materials, just for those that have textures +static void makeMaterial(const mjModel* m, mjrContext* con) { + memset(con->mat_texid, -1, sizeof(con->mat_texid)); + memset(con->mat_texuniform, 0, sizeof(con->mat_texuniform)); + memset(con->mat_texrepeat, 0, sizeof(con->mat_texrepeat)); + + // find skybox texture + for (int i=0; i < m->ntex; i++) { + if (m->tex_type[i] == mjTEXTURE_SKYBOX) { + if (m->nmat >= mjMAXMATERIAL-2) { + mju_error("With skybox, maximum number of materials is %d, got %" PRId64, + mjMAXMATERIAL-1, m->nmat); + } + for (int j=0; j < mjNTEXROLE; j++) { + con->mat_texid[mjNTEXROLE * (mjMAXMATERIAL-1) + j] = -1; + } + con->mat_texid[mjNTEXROLE * (mjMAXMATERIAL-1) + mjTEXROLE_RGB] = i; + + break; + } + } + + if (m->nmat >= mjMAXMATERIAL-1) { + mju_error("Maximum number of materials is %d, got %" PRId64, mjMAXMATERIAL, m->nmat); + } + for (int i=0; i < m->nmat; i++) { + if (m->mat_texid[i*mjNTEXROLE + mjTEXROLE_RGB] >= 0) { + for (int j=0; j < mjNTEXROLE; j++) { + con->mat_texid[i*mjNTEXROLE + j] = m->mat_texid[i*mjNTEXROLE + j]; + } + con->mat_texuniform[i] = m->mat_texuniform[i]; + con->mat_texrepeat[2*i] = m->mat_texrepeat[2*i]; + con->mat_texrepeat[2*i+1] = m->mat_texrepeat[2*i+1]; + } + } +} + + // make textures static void makeTexture(const mjModel* m, mjrContext* con) { // checks size @@ -1600,6 +1639,7 @@ void mjr_makeContext_offSize(const mjModel* m, mjrContext* con, int fontscale, // make everything makeOff(con); makeShadow(m, con); + makeMaterial(m, con); makeTexture(m, con); makePlane(m, con); makeMesh(m, con); diff --git a/src/render/classic/render_gl3.c b/src/render/classic/render_gl3.c index 402fcc6a..f986c0ff 100644 --- a/src/render/classic/render_gl3.c +++ b/src/render/classic/render_gl3.c @@ -61,10 +61,19 @@ enum { // enable/disable texture mapping static void settexture(int type, int state, const mjrContext* con, const mjvGeom* geom) { float plane[4], scl[2]; - int texid = -1; + int texid = -1, texuniform = 0; + float texrepeat[2] = {0, 0}; if (geom) { - if (geom->matid >= 0) { + if (geom->texid >= 0) { texid = geom->texid; + texuniform = geom->texuniform; + texrepeat[0] = geom->texrepeat[0]; + texrepeat[1] = geom->texrepeat[1]; + } else if (geom->matid >= 0) { + texid = con->mat_texid[mjNTEXROLE * geom->matid + mjTEXROLE_RGB]; + texuniform = con->mat_texuniform[geom->matid]; + texrepeat[0] = con->mat_texrepeat[geom->matid*2]; + texrepeat[1] = con->mat_texrepeat[geom->matid*2+1]; } } @@ -119,8 +128,8 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom glBindTexture(GL_TEXTURE_2D, con->texture[texid]); // determine scaling, adjust for pre-scaled geoms - scl[0] = geom->texrepeat[0]; - scl[1] = geom->texrepeat[1]; + scl[0] = texrepeat[0]; + scl[1] = texrepeat[1]; if (geom->dataid >= 0) { if (geom->size[0] > 0) { scl[0] = scl[0] / mju_max(mjMINVAL, geom->size[0]); @@ -132,7 +141,7 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom } // uniform: repeat relative to spatial units rather than object - if (geom->texuniform) { + if (texuniform) { if (geom->size[0] > 0) { scl[0] = scl[0] * geom->size[0]; } @@ -171,11 +180,11 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom // set mapping : cube if (type == mjtexREGULAR) { - mjr_setf4(plane, geom->texuniform ? geom->size[0] : 1, 0, 0, 0); + mjr_setf4(plane, texuniform ? geom->size[0] : 1, 0, 0, 0); glTexGenfv(GL_S, GL_OBJECT_PLANE, plane); - mjr_setf4(plane, 0, geom->texuniform ? geom->size[1] : 1, 0, 0); + mjr_setf4(plane, 0, texuniform ? geom->size[1] : 1, 0, 0); glTexGenfv(GL_T, GL_OBJECT_PLANE, plane); - mjr_setf4(plane, 0, 0, geom->texuniform ? geom->size[2] : 1, 0); + mjr_setf4(plane, 0, 0, texuniform ? geom->size[2] : 1, 0); glTexGenfv(GL_R, GL_OBJECT_PLANE, plane); } From 03e484116d8f25c8f431842845959fde7ffb2dca Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Thu, 16 Jul 2026 10:29:00 -0400 Subject: [PATCH 3/3] Reflect mjvgeom texture field changes in the filament renderer --- .../filament/compat/scene_geom_util.cc | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/experimental/filament/compat/scene_geom_util.cc b/src/experimental/filament/compat/scene_geom_util.cc index 997ee92d..90f9d455 100644 --- a/src/experimental/filament/compat/scene_geom_util.cc +++ b/src/experimental/filament/compat/scene_geom_util.cc @@ -174,6 +174,9 @@ static void UpdateGeomMaterial(mjrfRenderable* renderable, const mjvGeom& geom, material.roughness_texture = get_texture(mjTEXROLE_ROUGHNESS); material.occlusion_texture = get_texture(mjTEXROLE_OCCLUSION); } + if (geom.texid >= 0) { + material.color_texture = model_objs->GetTexture(geom.texid); + } material.reflectance = geom.reflectance; material.emissive = geom.emission; @@ -200,14 +203,21 @@ static void UpdateGeomMaterial(mjrfRenderable* renderable, const mjvGeom& geom, // the programmatic UVs. if (material.color_texture) { - const bool tex_uniform = model->mat_texuniform[geom.matid]; + bool tex_uniform; + float tex_repeat[2]; + if (geom.texid >= 0) { + tex_uniform = geom.texuniform; + tex_repeat[0] = geom.texrepeat[0]; + tex_repeat[1] = geom.texrepeat[1]; + } else { + tex_uniform = model->mat_texuniform[geom.matid]; + tex_repeat[0] = model->mat_texrepeat[(geom.matid * 2) + 0]; + tex_repeat[1] = model->mat_texrepeat[(geom.matid * 2) + 1]; + } if (mjrf_getTextureSamplerType(material.color_texture) == mjTEXTURE_2D) { // For 2D textures, `tex_repeat` specifies how many times the texture // image is repeated. The `tex_uniform` flag determines if the repetition // is applied at in object space (false) or in world space (true). - float tex_repeat[2]; - tex_repeat[0] = model->mat_texrepeat[(geom.matid * 2) + 0]; - tex_repeat[1] = model->mat_texrepeat[(geom.matid * 2) + 1]; material.uv_scale[0] = tex_repeat[0]; material.uv_scale[1] = tex_repeat[1];