Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

In place switch to reversed Z rendering and offscreen 32 bit float depth buffers for improved camera depth accuracy #978

3 changes: 3 additions & 0 deletions doc/changelog.rst
Expand Up @@ -115,6 +115,9 @@ General
attributes are specified. See the following
`example model <https://github.com/deepmind/mujoco/blob/main/test/engine/testdata/vis_visualize/frustum.xml>`__.
- Note that these attributes only take effect for offline rendering and do not affect interactive visualisation.
21. Implemented reversed Z rendering and added enum ``mjtDepthMap`` with elements ``mjDEPTH_ZERONEAR`` and
``mjDEPTH_ZEROFAR`` which can be used to set ``readDepthMap`` in ``mjrContext`` to control how the depth returned by
``mjr_readPixels`` is mapped from ``znear`` to ``zfar``.

Python bindings
^^^^^^^^^^^^^^^
Expand Down
7 changes: 7 additions & 0 deletions include/mujoco/mjrender.h
Expand Up @@ -39,6 +39,10 @@ typedef enum mjtFramebuffer_ { // OpenGL framebuffer option
mjFB_OFFSCREEN // offscreen buffer
} mjtFramebuffer;

typedef enum mjtDepthMap_ { // depth mapping for `mjr_readPixels`
mjDEPTH_ZERONEAR = 0, // standard depth map; 0: znear, 1: zfar
mjDEPTH_ZEROFAR = 1 // reversed depth map; 1: znear, 0: zfar
} mjtDepthMap;

typedef enum mjtFontScale_ { // font scale, used at context creation
mjFONTSCALE_50 = 50, // 50% scale, suitable for low-res rendering
Expand Down Expand Up @@ -151,6 +155,9 @@ struct mjrContext_ { // custom OpenGL context

// pixel output format
int readPixelFormat; // default color pixel format for mjr_readPixels

// depth output format
int readDepthMap; // depth mapping: mjDEPTH_ZERONEAR or mjDEPTH_ZEROFAR
};
typedef struct mjrContext_ mjrContext;

Expand Down
9 changes: 9 additions & 0 deletions introspect/enums.py
Expand Up @@ -621,6 +621,15 @@
('mjFB_OFFSCREEN', 1),
]),
)),
('mjtDepthMap',
EnumDecl(
name='mjtDepthMap',
declname='enum mjtDepthMap_',
values=dict([
('mjDEPTH_ZERONEAR', 0),
('mjDEPTH_ZEROFAR', 1),
]),
)),
('mjtFontScale',
EnumDecl(
name='mjtFontScale',
Expand Down
5 changes: 5 additions & 0 deletions introspect/structs.py
Expand Up @@ -7126,6 +7126,11 @@
type=ValueType(name='int'),
doc='default color pixel format for mjr_readPixels',
),
StructFieldDecl(
name='readDepthMap',
type=ValueType(name='int'),
doc='depth mapping: mjDEPTH_ZERONEAR or mjDEPTH_ZEROFAR',
),
),
)),
('mjuiState',
Expand Down
1 change: 1 addition & 0 deletions python/mujoco/render.cc
Expand Up @@ -215,6 +215,7 @@ PYBIND11_MODULE(_render, pymodule) {
X(windowDoublebuffer);
X(currentBuffer);
X(readPixelFormat);
X(readDepthMap);
#undef X

#define X(var) \
Expand Down
34 changes: 29 additions & 5 deletions python/mujoco/renderer.py
Expand Up @@ -86,6 +86,7 @@ def __init__(
_render.mjr_setBuffer(
_enums.mjtFramebuffer.mjFB_OFFSCREEN.value, self._mjr_context
)
self._mjr_context.readDepthMap = _enums.mjtDepthMap.mjDEPTH_ZEROFAR

# Default render flags.
self._depth_rendering = False
Expand Down Expand Up @@ -138,7 +139,8 @@ def render(self, *, out: Optional[np.ndarray] = None) -> np.ndarray:
"""
original_flags = self._scene.flags.copy()

if self._segmentation_rendering:
# Using segmented rendering for depth makes the calculated depth more accurate at far distances
if self._depth_rendering or self._segmentation_rendering:
self._scene.flags[_enums.mjtRndFlag.mjRND_SEGMENT] = True
self._scene.flags[_enums.mjtRndFlag.mjRND_IDCOLOR] = True

Expand Down Expand Up @@ -173,11 +175,33 @@ def render(self, *, out: Optional[np.ndarray] = None) -> np.ndarray:
near = self._model.vis.map.znear * extent
far = self._model.vis.map.zfar * extent

# Convert from [0 1] to depth in units of length, see links below:
# http://stackoverflow.com/a/6657284/1461210
# https://www.khronos.org/opengl/wiki/Depth_Buffer_Precision
out = near / (1 - out * (1 - near / far))
# Calculate OpenGL perspective matrix values in float32 precision
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused.. what's this trying to do? Is the intention to break or preserve compatibility for existing users of this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose is to improve the accuracy of the returned linear depth values and does not break compatibility.

The changes to renderer.py can be summarized is as: "Improve accuracy of linear depth by using segmented, reverse Z rendering and more carefully implement perspective transformation inversion to preserve numerical accuracy".

Without doing the calculations in this way, most of the accuracy improvements are lost due to numerical issues. Again, this should not break backwards compatibility since in the end renderer.py just returns linear depth.

# so they are close to what glFrustum returns
# https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
zfar = np.float32(far)
znear = np.float32(near)
C = -(zfar + znear)/(zfar - znear)
D = -(np.float32(2)*zfar*znear)/(zfar - znear)

# In reverse Z mode the perspective matrix is transformed by the following
C = np.float32(-0.5)*C - np.float32(0.5)
D = np.float32(-0.5)*D

# We need 64 bits to convert Z from ndc to metric depth without noticeable losses in precision
out_64 = out.astype(np.float64)

# Undo OpenGL projection
# Note: We do not need to take action to convert from window coordinates to
# normalized device coordinates because in reversed Z mode the mapping
# is identity
out_64 = D / (out_64 + C)

# Cast result back to float32 for backwards compatibility
# This has a small accuracy cost
out[:] = out_64.astype(np.float32)

# Reset scene flags.
np.copyto(self._scene.flags, original_flags)
elif self._segmentation_rendering:
_render.mjr_readPixels(out, None, self._rect, self._mjr_context)

Expand Down
18 changes: 15 additions & 3 deletions src/render/glad/glad.c
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Based on the OpenGL loader generated by glad 0.1.35 on Mon Mar 21 12:42:33 2022.
// Based on the OpenGL loader generated by glad 0.1.34 on Thu Jun 22 14:40:03 2023.
yuvaltassa marked this conversation as resolved.
Show resolved Hide resolved
//
// The original generated code is distributed under CC0.
// https://creativecommons.org/publicdomain/zero/1.0/
Expand All @@ -22,6 +22,8 @@
// APIs: gl=1.5
// Profile: compatibility
// Extensions:
// GL_ARB_clip_control,
// GL_ARB_depth_buffer_float,
// GL_ARB_framebuffer_object,
// GL_ARB_seamless_cube_map,
// GL_ARB_vertex_buffer_object,
Expand All @@ -32,9 +34,9 @@
// Reproducible: False
//
// Commandline:
// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_KHR_debug"
// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_clip_control,GL_ARB_depth_buffer_float,GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_KHR_debug"
// Online:
// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_KHR_debug
// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_clip_control&extensions=GL_ARB_depth_buffer_float&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_KHR_debug

#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
Expand Down Expand Up @@ -829,10 +831,13 @@ PFNGLWINDOWPOS3IPROC mjGlad_glWindowPos3i = NULL;
PFNGLWINDOWPOS3IVPROC mjGlad_glWindowPos3iv = NULL;
PFNGLWINDOWPOS3SPROC mjGlad_glWindowPos3s = NULL;
PFNGLWINDOWPOS3SVPROC mjGlad_glWindowPos3sv = NULL;
int mjGLAD_GL_ARB_clip_control = 0;
int mjGLAD_GL_ARB_depth_buffer_float = 0;
int mjGLAD_GL_ARB_framebuffer_object = 0;
int mjGLAD_GL_ARB_seamless_cube_map = 0;
int mjGLAD_GL_ARB_vertex_buffer_object = 0;
int mjGLAD_GL_KHR_debug = 0;
PFNGLCLIPCONTROLPROC mjGlad_glClipControl = NULL;
PFNGLISRENDERBUFFERPROC mjGlad_glIsRenderbuffer = NULL;
PFNGLBINDRENDERBUFFERPROC mjGlad_glBindRenderbuffer = NULL;
PFNGLDELETERENDERBUFFERSPROC mjGlad_glDeleteRenderbuffers = NULL;
Expand Down Expand Up @@ -1355,6 +1360,10 @@ static void mjGlad_load_GL_VERSION_1_5(GLADloadproc load) {
mjGlad_glGetBufferParameteriv = (PFNGLGETBUFFERPARAMETERIVPROC)load("glGetBufferParameteriv");
mjGlad_glGetBufferPointerv = (PFNGLGETBUFFERPOINTERVPROC)load("glGetBufferPointerv");
}
static void mjGlad_load_GL_ARB_clip_control(GLADloadproc load) {
if(!mjGLAD_GL_ARB_clip_control) return;
mjGlad_glClipControl = (PFNGLCLIPCONTROLPROC)load("glClipControl");
}
static void mjGlad_load_GL_ARB_framebuffer_object(GLADloadproc load) {
if(!mjGLAD_GL_ARB_framebuffer_object) return;
mjGlad_glIsRenderbuffer = (PFNGLISRENDERBUFFERPROC)load("glIsRenderbuffer");
Expand Down Expand Up @@ -1419,6 +1428,8 @@ static void mjGlad_load_GL_KHR_debug(GLADloadproc load) {
}
static int mjGlad_find_extensionsGL(void) {
if (!mjGlad_get_exts()) return 0;
mjGLAD_GL_ARB_clip_control = mjGlad_has_ext("GL_ARB_clip_control");
mjGLAD_GL_ARB_depth_buffer_float = mjGlad_has_ext("GL_ARB_depth_buffer_float");
mjGLAD_GL_ARB_framebuffer_object = mjGlad_has_ext("GL_ARB_framebuffer_object");
mjGLAD_GL_ARB_seamless_cube_map = mjGlad_has_ext("GL_ARB_seamless_cube_map");
mjGLAD_GL_ARB_vertex_buffer_object = mjGlad_has_ext("GL_ARB_vertex_buffer_object");
Expand Down Expand Up @@ -1491,6 +1502,7 @@ int mjGladLoadGLUnsafe(void) {
mjGlad_load_GL_VERSION_1_5(mjGlad_get_proc);

if (!mjGlad_find_extensionsGL()) return 0;
mjGlad_load_GL_ARB_clip_control(mjGlad_get_proc);
mjGlad_load_GL_ARB_framebuffer_object(mjGlad_get_proc);
mjGlad_load_GL_ARB_vertex_buffer_object(mjGlad_get_proc);
mjGlad_load_GL_KHR_debug(mjGlad_get_proc);
Expand Down
28 changes: 25 additions & 3 deletions src/render/glad/glad.h
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Based on the OpenGL loader generated by glad 0.1.35 on Mon Mar 21 12:42:33 2022.
// Based on the OpenGL loader generated by glad 0.1.34 on Thu Jun 22 14:40:03 2023.
//
// The original generated code is distributed under CC0.
// https://creativecommons.org/publicdomain/zero/1.0/
Expand All @@ -22,6 +22,8 @@
// APIs: gl=1.5
// Profile: compatibility
// Extensions:
// GL_ARB_clip_control,
// GL_ARB_depth_buffer_float,
// GL_ARB_framebuffer_object,
// GL_ARB_seamless_cube_map,
// GL_ARB_vertex_buffer_object,
Expand All @@ -32,9 +34,9 @@
// Reproducible: False
//
// Commandline:
// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_KHR_debug"
// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_clip_control,GL_ARB_depth_buffer_float,GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_KHR_debug"
// Online:
// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_KHR_debug
// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_clip_control&extensions=GL_ARB_depth_buffer_float&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_KHR_debug

#ifndef MUJOCO_SRC_RENDER_GLAD_GLAD_H_
#define MUJOCO_SRC_RENDER_GLAD_GLAD_H_
Expand Down Expand Up @@ -2304,6 +2306,15 @@ typedef void (APIENTRYP PFNGLGETBUFFERPOINTERVPROC)(GLenum target, GLenum pname,
GLAPI PFNGLGETBUFFERPOINTERVPROC mjGlad_glGetBufferPointerv;
#define glGetBufferPointerv mjGlad_glGetBufferPointerv
#endif
#define GL_LOWER_LEFT 0x8CA1
#define GL_UPPER_LEFT 0x8CA2
#define GL_NEGATIVE_ONE_TO_ONE 0x935E
#define GL_ZERO_TO_ONE 0x935F
#define GL_CLIP_ORIGIN 0x935C
#define GL_CLIP_DEPTH_MODE 0x935D
#define GL_DEPTH_COMPONENT32F 0x8CAC
#define GL_DEPTH32F_STENCIL8 0x8CAD
#define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD
#define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506
#define GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING 0x8210
#define GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE 0x8211
Expand Down Expand Up @@ -2487,6 +2498,17 @@ GLAPI PFNGLGETBUFFERPOINTERVPROC mjGlad_glGetBufferPointerv;
#define GL_STACK_OVERFLOW_KHR 0x0503
#define GL_STACK_UNDERFLOW_KHR 0x0504
#define GL_DISPLAY_LIST 0x82E7
#ifndef GL_ARB_clip_control
#define GL_ARB_clip_control 1
GLAPI int mjGLAD_GL_ARB_clip_control;
typedef void (APIENTRYP PFNGLCLIPCONTROLPROC)(GLenum origin, GLenum depth);
GLAPI PFNGLCLIPCONTROLPROC mjGlad_glClipControl;
#define glClipControl mjGlad_glClipControl
#endif
#ifndef GL_ARB_depth_buffer_float
#define GL_ARB_depth_buffer_float 1
GLAPI int mjGLAD_GL_ARB_depth_buffer_float;
#endif
#ifndef GL_ARB_framebuffer_object
#define GL_ARB_framebuffer_object 1
GLAPI int mjGLAD_GL_ARB_framebuffer_object;
Expand Down
21 changes: 13 additions & 8 deletions src/render/render_context.c
Expand Up @@ -1062,7 +1062,7 @@ static void makeShadow(const mjModel* m, mjrContext* con) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_GEQUAL);
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
Expand Down Expand Up @@ -1127,10 +1127,10 @@ static void makeOff(mjrContext* con) {
}
glBindRenderbuffer(GL_RENDERBUFFER, con->offDepthStencil);
if (con->offSamples) {
glRenderbufferStorageMultisample(GL_RENDERBUFFER, con->offSamples, GL_DEPTH24_STENCIL8,
glRenderbufferStorageMultisample(GL_RENDERBUFFER, con->offSamples, GL_DEPTH32F_STENCIL8,
con->offWidth, con->offHeight);
} else {
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, con->offWidth, con->offHeight);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH32F_STENCIL8, con->offWidth, con->offHeight);
}
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, con->offDepthStencil);
Expand Down Expand Up @@ -1169,7 +1169,7 @@ static void makeOff(mjrContext* con) {
mju_error("Could not allocate offscreen depth and stencil buffer_r");
}
glBindRenderbuffer(GL_RENDERBUFFER, con->offDepthStencil_r);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, con->offWidth, con->offHeight);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH32F_STENCIL8, con->offWidth, con->offHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, con->offDepthStencil_r);

Expand Down Expand Up @@ -1489,6 +1489,9 @@ void mjr_makeContext_offSize(const mjModel* m, mjrContext* con, int fontscale,
if (!mjGLAD_GL_ARB_vertex_buffer_object) {
mju_error("OpenGL ARB_vertex_buffer_object required");
}
if (!mjGLAD_GL_ARB_depth_buffer_float) {
mju_error("OpenGL ARB_depth_buffer_float required");
}
con->glInitialized = 1;

// determine window availability (could be EGL-headless)
Expand Down Expand Up @@ -1535,7 +1538,6 @@ void mjr_makeContext_offSize(const mjModel* m, mjrContext* con, int fontscale,
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

// free previous context
mjr_freeContext(con);

// no model: offscreen and font only
Expand Down Expand Up @@ -1607,6 +1609,9 @@ void mjr_makeContext_offSize(const mjModel* m, mjrContext* con, int fontscale,

// set default color pixel format for mjr_readPixels
con->readPixelFormat = GL_RGB;

// set default depth mapping for mjr_readPixels
con->readDepthMap = mjDEPTH_ZERONEAR;
}


Expand Down Expand Up @@ -1830,17 +1835,17 @@ MJAPI void mjr_resizeOffscreen(int width, int height, mjrContext* con) {

glBindRenderbuffer(GL_RENDERBUFFER, con->offDepthStencil);
if (con->offSamples) {
glRenderbufferStorageMultisample(GL_RENDERBUFFER, con->offSamples, GL_DEPTH24_STENCIL8,
glRenderbufferStorageMultisample(GL_RENDERBUFFER, con->offSamples, GL_DEPTH32F_STENCIL8,
con->offWidth, con->offHeight);
} else {
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, con->offWidth, con->offHeight);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH32F_STENCIL8, con->offWidth, con->offHeight);
}

if (con->offSamples) {
glBindRenderbuffer(GL_RENDERBUFFER, con->offColor_r);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, con->offWidth, con->offHeight);

glBindRenderbuffer(GL_RENDERBUFFER, con->offDepthStencil_r);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, con->offWidth, con->offHeight);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH32F_STENCIL8, con->offWidth, con->offHeight);
}
}
14 changes: 14 additions & 0 deletions src/render/render_gl2.c
Expand Up @@ -115,6 +115,13 @@ void mjr_readPixels(unsigned char* rgb, float* depth,
if (depth) {
glReadPixels(viewport.left, viewport.bottom, viewport.width, viewport.height,
GL_DEPTH_COMPONENT, GL_FLOAT, depth);
if (con->readDepthMap == mjDEPTH_ZERONEAR) {
int N_pixels = viewport.width * viewport.height;
for (int i = 0; i < N_pixels; i++) depth[i] = 1.0 - depth[i]; // Reverse the reversed Z buffer
}
else if (!mjGLAD_GL_ARB_clip_control) {
mju_warning("ARB_clip_control unavailable while mjDEPTH_ZEROFAR requested, depth accuracy will be limited");
}
}
}

Expand Down Expand Up @@ -159,6 +166,13 @@ void mjr_readPixels(unsigned char* rgb, float* depth,
if (depth) {
glReadPixels(viewport.left, viewport.bottom, viewport.width, viewport.height,
GL_DEPTH_COMPONENT, GL_FLOAT, depth);
if (con->readDepthMap == mjDEPTH_ZERONEAR) {
int N_pixels = viewport.width * viewport.height;
for (int i = 0; i < N_pixels; i++) depth[i] = 1.0 - depth[i]; // Reverse the reversed Z buffer
}
else if (!mjGLAD_GL_ARB_clip_control) {
mju_warning("ARB_clip_control unavailable while mjDEPTH_ZEROFAR requested, depth accuracy will be limited");
}
}

// restore currentBuffer
Expand Down