render: skip textures the classic renderer can't upload
KTX textures crash the classic OpenGL renderer and the Python viewer with "Number of channels not supported: 1": mjCTexture::LoadKTX stores the file as an opaque single-channel blob for the Filament renderer, and mjr_uploadTexture aborts via mju_error because the classic path only handles 3- or 4-channel textures. Skip textures the classic renderer can't upload (nchannel not 3 or 4) with a warning instead of aborting, so the model still loads and the other textures render. LoadKTX and the Filament path are unchanged. The guard also covers the cube/skybox path, which previously over-read a single-channel blob as RGB. Add a render_test.py regression test that builds a model with a KTX texture and constructs an MjrContext; it aborts before this change and passes after. Addresses #3343.
This commit is contained in:
@@ -14,6 +14,8 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
"""Tests for MuJoCo Python rendering."""
|
"""Tests for MuJoCo Python rendering."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from absl.testing import absltest
|
from absl.testing import absltest
|
||||||
import mujoco
|
import mujoco
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -104,6 +106,34 @@ class MuJoCoRenderTest(absltest.TestCase):
|
|||||||
context.free()
|
context.free()
|
||||||
context.free()
|
context.free()
|
||||||
|
|
||||||
|
def test_ktx_texture_does_not_crash_classic_renderer(self):
|
||||||
|
# KTX textures are stored as opaque single-channel blobs for the Filament
|
||||||
|
# renderer; mjCTexture::LoadKTX does not decode them. The classic renderer
|
||||||
|
# must skip such textures with a warning rather than aborting in
|
||||||
|
# mjr_uploadTexture (https://github.com/google-deepmind/mujoco/issues/3343).
|
||||||
|
ktx_path = os.path.join(self.create_tempdir().full_path, 'texture.ktx')
|
||||||
|
with open(ktx_path, 'wb') as f:
|
||||||
|
# LoadKTX stores the raw payload as a 1-channel blob without decoding, so
|
||||||
|
# the contents do not matter; start with the KTX 2.0 identifier for realism.
|
||||||
|
f.write(b'\xabKTX 20\xbb\r\n\x1a\n' + bytes(64))
|
||||||
|
|
||||||
|
model = mujoco.MjModel.from_xml_string(f"""
|
||||||
|
<mujoco>
|
||||||
|
<asset>
|
||||||
|
<texture name="ktx" type="2d" file="{ktx_path}"/>
|
||||||
|
<material name="mat" texture="ktx"/>
|
||||||
|
</asset>
|
||||||
|
<worldbody>
|
||||||
|
<geom type="plane" size="1 1 .1" material="mat"/>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>""")
|
||||||
|
self.assertEqual(model.tex_nchannel[0], 1)
|
||||||
|
|
||||||
|
# Building the render context uploads all textures; before the fix this
|
||||||
|
# aborted with "Number of channels not supported: 1". It must not raise now.
|
||||||
|
context = mujoco.MjrContext(model, mujoco.mjtFontScale.mjFONTSCALE_150)
|
||||||
|
context.free()
|
||||||
|
|
||||||
def test_mjrrect_repr(self):
|
def test_mjrrect_repr(self):
|
||||||
rect = mujoco.MjrRect(1, 2, 3, 4)
|
rect = mujoco.MjrRect(1, 2, 3, 4)
|
||||||
rect_repr = repr(rect)
|
rect_repr = repr(rect)
|
||||||
|
|||||||
@@ -1365,6 +1365,16 @@ void mjr_uploadTexture(const mjModel* m, const mjrContext* con, int texid) {
|
|||||||
int w = m->tex_width[texid];
|
int w = m->tex_width[texid];
|
||||||
float plane[4];
|
float plane[4];
|
||||||
|
|
||||||
|
// the classic renderer only uploads 3- (RGB) or 4-channel (RGBA) textures. KTX
|
||||||
|
// and other encoded textures are single-channel blobs for the Filament renderer
|
||||||
|
// (see LoadKTX); skip them with a warning instead of aborting (issue #3343).
|
||||||
|
if (m->tex_nchannel[texid] != 3 && m->tex_nchannel[texid] != 4) {
|
||||||
|
mju_warning("texture %d: classic renderer can't upload %d-channel data, "
|
||||||
|
"skipping (KTX/encoded textures need the Filament renderer)",
|
||||||
|
texid, m->tex_nchannel[texid]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 2D texture
|
// 2D texture
|
||||||
if (m->tex_type[texid] == mjTEXTURE_2D) {
|
if (m->tex_type[texid] == mjTEXTURE_2D) {
|
||||||
// OpenGL settings
|
// OpenGL settings
|
||||||
@@ -1390,11 +1400,9 @@ void mjr_uploadTexture(const mjModel* m, const mjrContext* con, int texid) {
|
|||||||
if (m->tex_nchannel[texid] == 3) {
|
if (m->tex_nchannel[texid] == 3) {
|
||||||
type = GL_RGB;
|
type = GL_RGB;
|
||||||
internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_EXT : GL_RGB;
|
internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_EXT : GL_RGB;
|
||||||
} else if (m->tex_nchannel[texid] == 4) {
|
} else { // 4 channels, guaranteed by the channel check at the top of the function
|
||||||
type = GL_RGBA;
|
type = GL_RGBA;
|
||||||
internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_ALPHA8_EXT : 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, internaltype, m->tex_width[texid],
|
glTexImage2D(GL_TEXTURE_2D, 0, internaltype, m->tex_width[texid],
|
||||||
|
|||||||
Reference in New Issue
Block a user