From 07fc95ca9a84ccbb616805ad6e0f7a6c777aeae5 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 11 Jun 2024 09:21:39 -0700 Subject: [PATCH] Add orthographic cameras. Orthographic cameras are specified by setting the `orthographic` attribute of the `` element. The `fovy` attribute is still used to specify the field-of-view, but its semantic is different for orthographic cameras. For orthographic cameras, the field-of-view is expressed in units of length, rather than degrees. Other related changes: * Fix bug in the ordering of `cam_xxx` elements in `mjModel`. * Make camera visualization translucent only when the frustum is visualized. * Added a button to `simulate` to toggle between perspective and orthographic free cameras. https://youtu.be/ZXBTEIDWHhs PiperOrigin-RevId: 642293435 Change-Id: Id090a421ad88ab404b5b27ddbbd6bfc81ad49bc5 --- doc/XMLreference.rst | 30 ++- doc/XMLschema.rst | 26 +- doc/changelog.rst | 16 +- doc/includes/references.h | 23 +- include/mujoco/mjmodel.h | 12 +- include/mujoco/mjspec.h | 1 + include/mujoco/mjvisualize.h | 10 +- include/mujoco/mjxmacro.h | 7 +- introspect/structs.py | 81 ++++-- python/mujoco/structs.cc | 3 + simulate/simulate.cc | 5 +- src/engine/engine_io.c | 3 +- src/engine/engine_vis_init.c | 19 +- src/engine/engine_vis_interact.c | 68 +++-- src/engine/engine_vis_visualize.c | 255 ++++++++++-------- src/render/render_gl3.c | 20 +- src/user/user_model.cc | 1 + src/xml/xml_native_reader.cc | 34 ++- src/xml/xml_native_writer.cc | 23 +- .../testdata/vis_visualize/orthographic.xml | 34 +++ test/xml/xml_native_reader_test.cc | 30 +++ test/xml/xml_native_writer_test.cc | 10 + unity/Runtime/Bindings/MjBindings.cs | 14 +- 23 files changed, 490 insertions(+), 235 deletions(-) create mode 100644 test/engine/testdata/vis_visualize/orthographic.xml diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index db20485c..61a9741f 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2615,11 +2615,20 @@ and the +Y axis points up. Thus the frame position and orientation are the key a When the camera mode is "targetbody" or "targetbodycom", this attribute becomes required. It specifies which body should be targeted by the camera. In all other modes this attribute is ignored. +.. _body-camera-orthographic: + +:at:`orthographic`: :at-val:`[false, true], "false"` + Whether the camera uses a perspective projection (the default) or an orthographic projection. Setting this attribute + changes the semantic of the :ref:`fovy` attribute, see below. + .. _body-camera-fovy: :at:`fovy`: :at-val:`real, "45"` - Vertical field of view of the camera, expressed in degrees regardless of the global angle setting. The horizontal - field of view is computed automatically given the window size and the vertical field of view. + Vertical field-of-view of the camera. If the camera uses a perspective projection, the field-of-view is expressed in + degrees, regardless of the global :ref:`compiler/angle ` setting. If the camera uses an orthographic + projection, the field-of-view is expressed in units of length; note that in this case the default of 45 is too large + for most scenes and should likely be reduced. In either case, the horizontal field of view is computed automatically + given the window size and the vertical field of view. .. _body-camera-resolution: @@ -7157,14 +7166,22 @@ coordinated visual settings corresponding to a "theme", and then include this fi While all settings in mjVisual are global, the settings here could not be fit into any of the other subsections. So this is effectively a miscellaneous subsection. +.. _visual-global-orthographic: + +:at:`orthographic`: :at-val:`[false, true], "false"` + Whether the free camera uses a perspective projection (the default) or an orthographic projection. Setting this + attribute changes the semantic of the :ref:`global/fovy` attribute, see below. + .. _visual-global-fovy: :at:`fovy`: :at-val:`real, "45"` This attribute specifies the vertical field of view of the free camera, i.e., the camera that is always available in - the visualizer even if no cameras are explicitly defined in the model. It is always expressed in degrees, regardless - of the setting of the angle attribute of :ref:`compiler `, and is also represented in the low level model - in degrees. This is because we pass it to OpenGL which uses degrees. The same convention applies to the fovy - attribute of the :ref:`camera ` element below. + the visualizer even if no cameras are explicitly defined in the model. If the camera uses a perspective projection, + the field-of-view is expressed in degrees, regardless of the global :ref:`compiler/angle ` setting. + If the camera uses an orthographic projection, the field-of-view is expressed in units of length; note that in this + case the default of 45 is too large for most scenes and should likely be reduced. In either case, the horizontal + field of view is computed automatically given the window size and the vertical field of view. The same convention + applies to the :ref:`camera/fovy ` attribute. .. _visual-global-ipd: @@ -7869,6 +7886,7 @@ if omitted. | This element sets the attributes of the dummy :ref:`site ` element of the defaults class. | All site attributes are available here except: name, class. +.. _default-camera-orthographic: .. _default-camera-fovy: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 044d022b..8f7d96c6 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -277,15 +277,15 @@ | :ref:`camera | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`name` | :ref:`class` | :ref:`fovy` | :ref:`ipd` | | +| | | | :ref:`name` | :ref:`class` | :ref:`orthographic` | :ref:`fovy` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`resolution` | :ref:`pos` | :ref:`quat` | :ref:`axisangle` | | +| | | | :ref:`ipd` | :ref:`resolution` | :ref:`pos` | :ref:`quat` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`xyaxes` | :ref:`zaxis` | :ref:`euler` | :ref:`mode` | | +| | | | :ref:`axisangle` | :ref:`xyaxes` | :ref:`zaxis` | :ref:`euler` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`target` | :ref:`focal` | :ref:`focalpixel` | :ref:`principal` | | +| | | | :ref:`mode` | :ref:`target` | :ref:`focal` | :ref:`focalpixel` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`principalpixel` | :ref:`sensorsize` | :ref:`user` | | | +| | | | :ref:`principal` | :ref:`principalpixel` | :ref:`sensorsize` | :ref:`user` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| body |br| |_| |L| | | .. table:: | @@ -1246,11 +1246,11 @@ | :ref:`global | ? | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`fovy` | :ref:`ipd` | :ref:`azimuth` | :ref:`elevation` | | +| | | | :ref:`orthographic` | :ref:`fovy` | :ref:`ipd` | :ref:`azimuth` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`linewidth` | :ref:`glow` | :ref:`offwidth` | :ref:`offheight` | | +| | | | :ref:`elevation` | :ref:`linewidth` | :ref:`glow` | :ref:`offwidth` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`realtime` | :ref:`ellipsoidinertia` | :ref:`bvactive` | | | +| | | | :ref:`offheight` | :ref:`realtime` | :ref:`ellipsoidinertia` | :ref:`bvactive` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| visual |br| |_| |L| | | .. table:: | @@ -1396,13 +1396,15 @@ | :ref:`camera | ? | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`fovy` | :ref:`ipd` | :ref:`resolution` | :ref:`pos` | | +| | | | :ref:`orthographic` | :ref:`fovy` | :ref:`ipd` | :ref:`resolution` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`quat` | :ref:`axisangle` | :ref:`xyaxes` | :ref:`zaxis` | | +| | | | :ref:`pos` | :ref:`quat` | :ref:`axisangle` | :ref:`xyaxes` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`euler` | :ref:`mode` | :ref:`focal` | :ref:`focalpixel` | | +| | | | :ref:`zaxis` | :ref:`euler` | :ref:`mode` | :ref:`focal` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`principal` | :ref:`principalpixel` | :ref:`sensorsize` | :ref:`user` | | +| | | | :ref:`focalpixel` | :ref:`principal` | :ref:`principalpixel` | :ref:`sensorsize` | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`user` | | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| default |br| |_| |L| | | .. table:: | diff --git a/doc/changelog.rst b/doc/changelog.rst index c265aa27..f27194b8 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -13,15 +13,21 @@ General - Detailed documentation. - Python bindings. -2. Added :ref:`maxhullvert`, the maximum number of vertices in a mesh's convex hull. +.. youtube:: ZXBTEIDWHhs + :align: right + :width: 240px + +2. Added support for orthographic cameras. This is available for both fixed cameras and the free camera, using the + :ref:`camera/orthographic` and :ref:`global/orthographic` + attributes, respectively. +3. Added :ref:`maxhullvert`, the maximum number of vertices in a mesh's convex hull. MJX ~~~ - -3. Added support for :ref:`elliptic friction cones`. -4. Fixed a bug that resulted in less-optimal linesearch solutions for some difficult constraint settings. -5. Fixed a bug in the Newton solver that sometimes resulted in less-optimal gradients. +4. Added support for :ref:`elliptic friction cones`. +5. Fixed a bug that resulted in less-optimal linesearch solutions for some difficult constraint settings. +6. Fixed a bug in the Newton solver that sometimes resulted in less-optimal gradients. Version 3.1.6 (Jun 3, 2024) --------------------------- diff --git a/doc/includes/references.h b/doc/includes/references.h index 253ced07..105b03d2 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -751,7 +751,8 @@ struct mjOption_ { // physics options typedef struct mjOption_ mjOption; struct mjVisual_ { // visualization options struct { // global parameters - float fovy; // y-field of view for free camera (degrees) + int orthographic; // is the free camera orthographic (0: no, 1: yes) + float fovy; // y field-of-view of free camera (orthographic ? length : degree) float ipd; // inter-pupilary distance for free camera float azimuth; // initial azimuth of free camera (degrees) float elevation; // initial elevation of free camera (degrees) @@ -1072,11 +1073,12 @@ struct mjModel_ { mjtNum* cam_poscom0; // global position rel. to sub-com in qpos0 (ncam x 3) mjtNum* cam_pos0; // global position rel. to body in qpos0 (ncam x 3) mjtNum* cam_mat0; // global orientation in qpos0 (ncam x 9) - int* cam_resolution; // [width, height] in pixels (ncam x 2) - mjtNum* cam_fovy; // y-field of view (deg) (ncam x 1) - float* cam_intrinsic; // [focal length; principal point] (ncam x 4) - float* cam_sensorsize; // sensor size (ncam x 2) + int* cam_orthographic; // orthographic camera; 0: no, 1: yes (ncam x 1) + mjtNum* cam_fovy; // y field-of-view (ortho ? len : deg) (ncam x 1) mjtNum* cam_ipd; // inter-pupilary distance (ncam x 1) + int* cam_resolution; // resolution: pixels [width, height] (ncam x 2) + float* cam_sensorsize; // sensor size: length [width, height] (ncam x 2) + float* cam_intrinsic; // [focal length; principal point] (ncam x 4) mjtNum* cam_user; // user data (ncam x nuser_cam) // lights @@ -1858,6 +1860,7 @@ typedef struct mjsCamera_ { // camera specification mjString* targetbody; // target body for tracking/targeting // intrinsics + int orthographic; // is camera orthographic double fovy; // y-field of view double ipd; // inter-pupilary distance float intrinsic[4]; // camera intrinsics (length) @@ -2602,6 +2605,9 @@ struct mjvCamera_ { // abstract camera mjtNum distance; // distance to lookat point or tracked body mjtNum azimuth; // camera azimuth (deg) mjtNum elevation; // camera elevation (deg) + + // orthographic / perspective + int orthographic; // 0: perspective; 1: orthographic }; typedef struct mjvCamera_ mjvCamera; struct mjvGLCamera_ { // OpenGL camera @@ -2617,6 +2623,9 @@ struct mjvGLCamera_ { // OpenGL camera float frustum_top; // top float frustum_near; // near float frustum_far; // far + + // orthographic / perspective + int orthographic; // 0: perspective; 1: orthographic }; typedef struct mjvGLCamera_ mjvGLCamera; struct mjvGeom_ { // abstract geom @@ -2869,10 +2878,12 @@ struct mjvSceneState_ { mjtNum* site_size; float* site_rgba; + int* cam_orthographic; mjtNum* cam_fovy; mjtNum* cam_ipd; - float* cam_intrinsic; + int* cam_resolution; float* cam_sensorsize; + float* cam_intrinsic; mjtByte* light_directional; mjtByte* light_castshadow; diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index f8384c74..72999a31 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -458,7 +458,8 @@ typedef struct mjOption_ mjOption; struct mjVisual_ { // visualization options struct { // global parameters - float fovy; // y-field of view for free camera (degrees) + int orthographic; // is the free camera orthographic (0: no, 1: yes) + float fovy; // y field-of-view of free camera (orthographic ? length : degree) float ipd; // inter-pupilary distance for free camera float azimuth; // initial azimuth of free camera (degrees) float elevation; // initial elevation of free camera (degrees) @@ -787,11 +788,12 @@ struct mjModel_ { mjtNum* cam_poscom0; // global position rel. to sub-com in qpos0 (ncam x 3) mjtNum* cam_pos0; // global position rel. to body in qpos0 (ncam x 3) mjtNum* cam_mat0; // global orientation in qpos0 (ncam x 9) - int* cam_resolution; // [width, height] in pixels (ncam x 2) - mjtNum* cam_fovy; // y-field of view (deg) (ncam x 1) - float* cam_intrinsic; // [focal length; principal point] (ncam x 4) - float* cam_sensorsize; // sensor size (ncam x 2) + int* cam_orthographic; // orthographic camera; 0: no, 1: yes (ncam x 1) + mjtNum* cam_fovy; // y field-of-view (ortho ? len : deg) (ncam x 1) mjtNum* cam_ipd; // inter-pupilary distance (ncam x 1) + int* cam_resolution; // resolution: pixels [width, height] (ncam x 2) + float* cam_sensorsize; // sensor size: length [width, height] (ncam x 2) + float* cam_intrinsic; // [focal length; principal point] (ncam x 4) mjtNum* cam_user; // user data (ncam x nuser_cam) // lights diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index a52f7d7d..91f284c7 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -345,6 +345,7 @@ typedef struct mjsCamera_ { // camera specification mjString* targetbody; // target body for tracking/targeting // intrinsics + int orthographic; // is camera orthographic double fovy; // y-field of view double ipd; // inter-pupilary distance float intrinsic[4]; // camera intrinsics (length) diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index 350f9c1e..0d23e39a 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -192,6 +192,9 @@ struct mjvCamera_ { // abstract camera mjtNum distance; // distance to lookat point or tracked body mjtNum azimuth; // camera azimuth (deg) mjtNum elevation; // camera elevation (deg) + + // orthographic / perspective + int orthographic; // 0: perspective; 1: orthographic }; typedef struct mjvCamera_ mjvCamera; @@ -211,6 +214,9 @@ struct mjvGLCamera_ { // OpenGL camera float frustum_top; // top float frustum_near; // near float frustum_far; // far + + // orthographic / perspective + int orthographic; // 0: perspective; 1: orthographic }; typedef struct mjvGLCamera_ mjvGLCamera; @@ -487,10 +493,12 @@ struct mjvSceneState_ { mjtNum* site_size; float* site_rgba; + int* cam_orthographic; mjtNum* cam_fovy; mjtNum* cam_ipd; - float* cam_intrinsic; + int* cam_resolution; float* cam_sensorsize; + float* cam_intrinsic; mjtByte* light_directional; mjtByte* light_castshadow; diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 9cea2bc6..66a376eb 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -277,16 +277,17 @@ X ( int, cam_mode, ncam, 1 ) \ X ( int, cam_bodyid, ncam, 1 ) \ X ( int, cam_targetbodyid, ncam, 1 ) \ - X ( int, cam_resolution, ncam, 2 ) \ - XMJV( float, cam_sensorsize, ncam, 2 ) \ - XMJV( float, cam_intrinsic, ncam, 4 ) \ X ( mjtNum, cam_pos, ncam, 3 ) \ X ( mjtNum, cam_quat, ncam, 4 ) \ X ( mjtNum, cam_poscom0, ncam, 3 ) \ X ( mjtNum, cam_pos0, ncam, 3 ) \ X ( mjtNum, cam_mat0, ncam, 9 ) \ + XMJV( int, cam_orthographic, ncam, 1 ) \ XMJV( mjtNum, cam_fovy, ncam, 1 ) \ XMJV( mjtNum, cam_ipd, ncam, 1 ) \ + XMJV( int, cam_resolution, ncam, 2 ) \ + XMJV( float, cam_sensorsize, ncam, 2 ) \ + XMJV( float, cam_intrinsic, ncam, 4 ) \ X ( mjtNum, cam_user, ncam, MJ_M(nuser_cam) ) \ X ( int, light_mode, nlight, 1 ) \ X ( int, light_bodyid, nlight, 1 ) \ diff --git a/introspect/structs.py b/introspect/structs.py index cdd5ff1b..b3e92f77 100644 --- a/introspect/structs.py +++ b/introspect/structs.py @@ -310,10 +310,15 @@ STRUCTS: Mapping[str, StructDecl] = dict([ name='global', type=AnonymousStructDecl( fields=( + StructFieldDecl( + name='orthographic', + type=ValueType(name='int'), + doc='is the free camera orthographic (0: no, 1: yes)', # pylint: disable=line-too-long + ), StructFieldDecl( name='fovy', type=ValueType(name='float'), - doc='y-field of view for free camera (degrees)', + doc='y field-of-view of free camera (orthographic ? length : degree)', # pylint: disable=line-too-long ), StructFieldDecl( name='ipd', @@ -2024,32 +2029,18 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='global orientation in qpos0 (ncam x 9)', ), StructFieldDecl( - name='cam_resolution', + name='cam_orthographic', type=PointerType( inner_type=ValueType(name='int'), ), - doc='[width, height] in pixels (ncam x 2)', + doc='orthographic camera; 0: no, 1: yes (ncam x 1)', ), StructFieldDecl( name='cam_fovy', type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='y-field of view (deg) (ncam x 1)', - ), - StructFieldDecl( - name='cam_intrinsic', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='[focal length; principal point] (ncam x 4)', - ), - StructFieldDecl( - name='cam_sensorsize', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='sensor size (ncam x 2)', + doc='y field-of-view (ortho ? len : deg) (ncam x 1)', ), StructFieldDecl( name='cam_ipd', @@ -2058,6 +2049,27 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='inter-pupilary distance (ncam x 1)', ), + StructFieldDecl( + name='cam_resolution', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='resolution: pixels [width, height] (ncam x 2)', + ), + StructFieldDecl( + name='cam_sensorsize', + type=PointerType( + inner_type=ValueType(name='float'), + ), + doc='sensor size: length [width, height] (ncam x 2)', + ), + StructFieldDecl( + name='cam_intrinsic', + type=PointerType( + inner_type=ValueType(name='float'), + ), + doc='[focal length; principal point] (ncam x 4)', + ), StructFieldDecl( name='cam_user', type=PointerType( @@ -5367,6 +5379,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjtNum'), doc='camera elevation (deg)', ), + StructFieldDecl( + name='orthographic', + type=ValueType(name='int'), + doc='0: perspective; 1: orthographic', + ), ), )), ('mjvGLCamera', @@ -5428,6 +5445,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='float'), doc='far', ), + StructFieldDecl( + name='orthographic', + type=ValueType(name='int'), + doc='0: perspective; 1: orthographic', + ), ), )), ('mjvGeom', @@ -6701,6 +6723,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='', ), + StructFieldDecl( + name='cam_orthographic', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='', + ), StructFieldDecl( name='cam_fovy', type=PointerType( @@ -6716,14 +6745,21 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='', ), StructFieldDecl( - name='cam_intrinsic', + name='cam_resolution', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='', + ), + StructFieldDecl( + name='cam_sensorsize', type=PointerType( inner_type=ValueType(name='float'), ), doc='', ), StructFieldDecl( - name='cam_sensorsize', + name='cam_intrinsic', type=PointerType( inner_type=ValueType(name='float'), ), @@ -9216,6 +9252,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='target body for tracking/targeting', ), + StructFieldDecl( + name='orthographic', + type=ValueType(name='int'), + doc='is camera orthographic', + ), StructFieldDecl( name='fovy', type=ValueType(name='double'), diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index ebe134a8..a52c8335 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1390,6 +1390,7 @@ PYBIND11_MODULE(_structs, m) { }); DefineStructFunctions(mjVisualGlobal); #define X(var) mjVisualGlobal.def_readwrite(#var, &raw::MjVisualGlobal::var) + X(orthographic); X(fovy); X(ipd); X(azimuth); @@ -2123,6 +2124,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(distance); X(azimuth); X(elevation); + X(orthographic); #undef X #define X(var) DefinePyArray(mjvCamera, #var, &MjvCameraWrapper::var) @@ -2153,6 +2155,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(frustum_top); X(frustum_near); X(frustum_far); + X(orthographic); #undef X #define X(var) DefinePyArray(mjvGLCamera, #var, &MjvGLCameraWrapper::var) diff --git a/simulate/simulate.cc b/simulate/simulate.cc index f64eaf19..85f8d824 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -887,14 +887,15 @@ void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m, int oldstate) {mjITEM_EDITFLOAT, "Ambient", 2, &(vis->headlight.ambient), "3"}, {mjITEM_EDITFLOAT, "Diffuse", 2, &(vis->headlight.diffuse), "3"}, {mjITEM_EDITFLOAT, "Specular", 2, &(vis->headlight.specular), "3"}, - {mjITEM_SEPARATOR, "Initial Free Camera", 1}, + {mjITEM_SEPARATOR, "Free Camera", 1}, + {mjITEM_RADIO, "Orthographic", 2, &(vis->global.orthographic), "No\nYes"}, + {mjITEM_EDITFLOAT, "Field of view", 2, &(vis->global.fovy), "1"}, {mjITEM_EDITNUM, "Center", 2, &(stat->center), "3"}, {mjITEM_EDITFLOAT, "Azimuth", 2, &(vis->global.azimuth), "1"}, {mjITEM_EDITFLOAT, "Elevation", 2, &(vis->global.elevation), "1"}, {mjITEM_BUTTON, "Align", 2, nullptr, "CA"}, {mjITEM_SEPARATOR, "Global", 1}, {mjITEM_EDITNUM, "Extent", 2, &(stat->extent), "1"}, - {mjITEM_EDITFLOAT, "Field of view", 2, &(vis->global.fovy), "1"}, {mjITEM_RADIO, "Inertia", 5, &(vis->global.ellipsoidinertia), "Box\nEllipsoid"}, {mjITEM_RADIO, "BVH active", 5, &(vis->global.bvactive), "False\nTrue"}, {mjITEM_SEPARATOR, "Map", 1}, diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index c41932eb..5e5af9ae 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -182,6 +182,7 @@ static void setf4(float* rgba, float r, float g, float b, float a) { // set visual options to default values void mj_defaultVisual(mjVisual* vis) { // global + vis->global.orthographic = 0; vis->global.fovy = 45; vis->global.ipd = 0.068; vis->global.azimuth = 90; @@ -257,7 +258,7 @@ void mj_defaultVisual(mjVisual* vis) { setf4(vis->rgba.actuatornegative, .2, .6, .9, 1.); setf4(vis->rgba.actuatorpositive, .9, .4, .2, 1.); setf4(vis->rgba.com, .9, .9, .9, 1.); - setf4(vis->rgba.camera, .6, .9, .6, .3); + setf4(vis->rgba.camera, .6, .9, .6, 1); setf4(vis->rgba.light, .6, .6, .9, 1.); setf4(vis->rgba.selectpoint, .9, .9, .1, 1.); setf4(vis->rgba.connect, .2, .2, .8, 1.); diff --git a/src/engine/engine_vis_init.c b/src/engine/engine_vis_init.c index 3c16bf66..3e0d46a2 100644 --- a/src/engine/engine_vis_init.c +++ b/src/engine/engine_vis_init.c @@ -359,15 +359,16 @@ void mjv_defaultCamera(mjvCamera* cam) { void mjv_defaultFreeCamera(const mjModel* m, mjvCamera* cam) { memset(cam, 0, sizeof(mjvCamera)); - cam->type = mjCAMERA_FREE; - cam->fixedcamid = -1; - cam->trackbodyid = -1; - cam->lookat[0] = m->stat.center[0]; - cam->lookat[1] = m->stat.center[1]; - cam->lookat[2] = m->stat.center[2]; - cam->distance = 1.5 * m->stat.extent; - cam->azimuth = m->vis.global.azimuth; - cam->elevation = m->vis.global.elevation; + cam->type = mjCAMERA_FREE; + cam->fixedcamid = -1; + cam->trackbodyid = -1; + cam->lookat[0] = m->stat.center[0]; + cam->lookat[1] = m->stat.center[1]; + cam->lookat[2] = m->stat.center[2]; + cam->distance = 1.5 * m->stat.extent; + cam->azimuth = m->vis.global.azimuth; + cam->elevation = m->vis.global.elevation; + cam->orthographic = m->vis.global.orthographic; } diff --git a/src/engine/engine_vis_interact.c b/src/engine/engine_vis_interact.c index 62eec2af..604a97b6 100644 --- a/src/engine/engine_vis_interact.c +++ b/src/engine/engine_vis_interact.c @@ -223,16 +223,29 @@ void mjv_cameraInRoom(mjtNum* headpos, mjtNum* forward, mjtNum* up, const mjvSce // get frustum height at unit distance from camera; average left and right OpenGL cameras mjtNum mjv_frustumHeight(const mjvScene* scn) { - mjtNum height; + const mjvGLCamera* cam1 = scn->camera; + const mjvGLCamera* cam2 = scn->camera + 1; - // check znear - if (scn->camera[0].frustum_near < mjMINVAL || scn->camera[1].frustum_near < mjMINVAL) { - mjERROR("mjvScene frustum_near too small"); + if (cam1->orthographic != cam2->orthographic) { + mjERROR("cannot average frustums of perspective and orthographic cameras"); } - // add normalized height for left and right cameras - height = (scn->camera[0].frustum_top-scn->camera[0].frustum_bottom)/scn->camera[0].frustum_near + - (scn->camera[1].frustum_top-scn->camera[1].frustum_bottom)/scn->camera[1].frustum_near; + // get height + mjtNum height; + if (!cam1->orthographic) { + // check znear + if (cam1->frustum_near < mjMINVAL || cam2->frustum_near < mjMINVAL) { + mjERROR("mjvScene frustum_near too small"); + } + + // add normalized height for left and right cameras + height = (cam1->frustum_top - cam1->frustum_bottom) / cam1->frustum_near + + (cam2->frustum_top - cam2->frustum_bottom) / cam2->frustum_near; + } else { + // add height for left and right cameras + height = (cam1->frustum_top - cam1->frustum_bottom) + + (cam2->frustum_top - cam2->frustum_bottom); + } // average return 0.5*height; @@ -337,6 +350,9 @@ void mjv_moveCamera(const mjModel* m, int action, mjtNum reldx, mjtNum reldy, mju_sub3(dif, cam->lookat, headpos); scl = mjv_frustumHeight(scn) * mju_dot3(dif, forward); + // multiply by mystery coefficient TODO: b/346130949 + if (cam->orthographic) scl *= 0.15; + // move lookat point in opposite direction mju_addToScl3(cam->lookat, vec, -scl); break; @@ -563,6 +579,9 @@ void mjv_initPerturb(const mjModel* m, mjData* d, const mjvScene* scn, mjvPertur mju_sub3(dif, pert->refselpos, headpos); pert->scale = mjv_frustumHeight(scn) * mju_dot3(dif, forward); + // multiply by mystery coefficient TODO: b/346130949 + if (scn->camera[0].orthographic) pert->scale *= 0.15; + mj_freeStack(d); } @@ -733,6 +752,12 @@ mjvGLCamera mjv_averageCamera(const mjvGLCamera* cam1, const mjvGLCamera* cam2) cam.frustum_near = 0.5f * (cam1->frustum_near + cam2->frustum_near); cam.frustum_far = 0.5f * (cam1->frustum_far + cam2->frustum_far); + if (cam1->orthographic != cam2->orthographic) { + mjERROR("cannot average perspective and orthographic cameras"); + } else { + cam.orthographic = cam1->orthographic; + } + return cam; } @@ -755,17 +780,31 @@ int mjv_select(const mjModel* m, const mjData* d, const mjvOption* vopt, // compute frustum halfwidth so as to match viewport aspect ratio mjtNum halfwidth = 0.5*aspectratio*(cam.frustum_top - cam.frustum_bottom); - // construct ray + // compute up and left offsets from normalized cursor + mjtNum d_up = cam.frustum_bottom + rely*(cam.frustum_top-cam.frustum_bottom); + mjtNum d_left = -(cam.frustum_center + (2*relx-1)*halfwidth); + + // define ray mjtNum ray[3]; - mju_scl3(ray, forward, cam.frustum_near); - mju_addToScl3(ray, up, cam.frustum_bottom + rely*(cam.frustum_top-cam.frustum_bottom)); - mju_addToScl3(ray, left, -(cam.frustum_center + (2*relx-1)*halfwidth)); - mju_normalize3(ray); + + // construct ray for orthographic camera: fixed direction, modify pos + if (cam.orthographic) { + mju_copy3(ray, forward); + mju_addToScl3(pos, up, d_up); + mju_addToScl3(pos, left, d_left); + } + + // construct ray for perspective camera: fixed pos, modify direction + else { + mju_scl3(ray, forward, cam.frustum_near); + mju_addToScl3(ray, up, d_up); + mju_addToScl3(ray, left, d_left); + mju_normalize3(ray); + } // find intersection with geoms *geomid = -1; - mjtNum geomdist = mj_ray(m, d, pos, ray, vopt->geomgroup, - vopt->flags[mjVIS_STATIC], -1, geomid); + mjtNum geomdist = mj_ray(m, d, pos, ray, vopt->geomgroup, vopt->flags[mjVIS_STATIC], -1, geomid); // find intersection with flexes int flexbodyid = -1; @@ -851,7 +890,6 @@ int mjv_select(const mjModel* m, const mjData* d, const mjvOption* vopt, } } - // geom if (best == 0) { *flexid = -1; diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index f35df109..a38125e9 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -511,11 +511,11 @@ static int bodycategory(const mjModel* m, int bodyid) { // computes the camera frustum static void getFrustum(float zver[2], float zhor[2], float znear, - const float K[4], const float sensorsize[2]) { - zhor[0] = znear / K[0] * (sensorsize[0]/2.f - K[2]); - zhor[1] = znear / K[0] * (sensorsize[0]/2.f + K[2]); - zver[0] = znear / K[1] * (sensorsize[1]/2.f - K[3]); - zver[1] = znear / K[1] * (sensorsize[1]/2.f + K[3]); + const float intrinsic[4], const float sensorsize[2]) { + zhor[0] = znear / intrinsic[0] * (sensorsize[0]/2.f - intrinsic[2]); + zhor[1] = znear / intrinsic[0] * (sensorsize[0]/2.f + intrinsic[2]); + zver[0] = znear / intrinsic[1] * (sensorsize[1]/2.f - intrinsic[3]); + zver[1] = znear / intrinsic[1] * (sensorsize[1]/2.f + intrinsic[3]); } @@ -1502,11 +1502,97 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, } } - // cameras + // cameras and frustums objtype = mjOBJ_CAMERA; category = mjCAT_DECOR; if (vopt->flags[mjVIS_CAMERA] && (category & catmask)) { for (int i=0; i < m->ncam; i++) { + // copy camera rgba + float cam_rgba[4]; + f2f(cam_rgba, m->vis.rgba.camera, 4); + + // draw frustum if sensorsize is defined + if (m->cam_sensorsize[2*i+1] > 0) { + // when drawing frustum, make camera translucent + cam_rgba[3] = 0.3; + + // locals + const float* rgba = m->vis.rgba.frustum; + mjtNum vnear[4][3], vfar[4][3]; + mjtNum center[3]; + mjtNum znear = m->vis.map.znear * m->stat.extent; + mjtNum zfar = m->vis.scale.frustum * scl; + float zver[2], zhor[2]; + + // get frustum + getFrustum(zver, zhor, znear, m->cam_intrinsic + 4*i, m->cam_sensorsize + 2*i); + + // frustum frame to convert from planes to vertex representation + mjtNum *cam_xpos = d->cam_xpos+3*i; + mjtNum *cam_xmat = d->cam_xmat+9*i; + mjtNum x[] = {cam_xmat[0], cam_xmat[3], cam_xmat[6]}; + mjtNum y[] = {cam_xmat[1], cam_xmat[4], cam_xmat[7]}; + mjtNum z[] = {cam_xmat[2], cam_xmat[5], cam_xmat[8]}; + + // vertices of the near plane + mju_addScl3(center, cam_xpos, z, -znear); + mju_addScl3(vnear[0], center, x, -zhor[0]); + mju_addScl3(vnear[1], center, x, zhor[1]); + mju_addScl3(vnear[2], center, x, zhor[1]); + mju_addScl3(vnear[3], center, x, -zhor[0]); + mju_addToScl3(vnear[0], y, -zver[0]); + mju_addToScl3(vnear[1], y, -zver[0]); + mju_addToScl3(vnear[2], y, zver[1]); + mju_addToScl3(vnear[3], y, zver[1]); + + // vertices of the far plane + zhor[0] *= zfar / znear; + zhor[1] *= zfar / znear; + zver[0] *= zfar / znear; + zver[1] *= zfar / znear; + mju_addScl3(center, cam_xpos, z, -zfar); + mju_addScl3(vfar[0], center, x, -zhor[0]); + mju_addScl3(vfar[1], center, x, zhor[1]); + mju_addScl3(vfar[2], center, x, zhor[1]); + mju_addScl3(vfar[3], center, x, -zhor[0]); + mju_addToScl3(vfar[0], y, -zver[0]); + mju_addToScl3(vfar[1], y, -zver[0]); + mju_addToScl3(vfar[2], y, zver[1]); + mju_addToScl3(vfar[3], y, zver[1]); + + // triangulation and wireframe of the frustum + for (int e=0; e < 4; e++) { + START + mju_sub3(x, vfar[e], vnear[e]); + mju_sub3(y, vnear[(e+1)%4], vnear[e]); + mju_cross(z, x, y); + mjtNum tri1[3] = {mju_normalize3(x), mju_normalize3(y), mju_normalize3(z)}; + mjtNum xmat1[9] = {x[0], y[0], z[0], x[1], y[1], z[1], x[2], y[2], z[2]}; + mjv_initGeom(thisgeom, mjGEOM_TRIANGLE, tri1, vnear[e], xmat1, rgba); + FINISH + START + mju_sub3(y, vnear[(e+1)%4], vfar[e]); + mju_sub3(x, vfar[(e+1)%4], vfar[e]); + mju_cross(z, x, y); + mjtNum tri2[3] = {mju_normalize3(x), mju_normalize3(y), mju_normalize3(z)}; + mjtNum xmat2[9] = {x[0], y[0], z[0], x[1], y[1], z[1], x[2], y[2], z[2]}; + mjv_initGeom(thisgeom, mjGEOM_TRIANGLE, tri2, vfar[e], xmat2, rgba); + FINISH + START + mjv_connector(thisgeom, mjGEOM_LINE, 3, vnear[e], vnear[(e+1)%4]); + f2f(thisgeom->rgba, rgba, 4); + FINISH + START + mjv_connector(thisgeom, mjGEOM_LINE, 3, vfar[e], vfar[(e+1)%4]); + f2f(thisgeom->rgba, rgba, 4); + FINISH + START + mjv_connector(thisgeom, mjGEOM_LINE, 3, vnear[e], vfar[e]); + f2f(thisgeom->rgba, rgba, 4); + FINISH + } + } + START // construct geom: camera body @@ -1516,7 +1602,7 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, thisgeom->size[2] = scl * m->vis.scale.camera * 0.4; mju_n2f(thisgeom->pos, d->cam_xpos+3*i, 3); mju_n2f(thisgeom->mat, d->cam_xmat+9*i, 9); - f2f(thisgeom->rgba, m->vis.rgba.camera, 4); + f2f(thisgeom->rgba, cam_rgba, 4); // vopt->label if (vopt->label == mjLABEL_CAMERA) { @@ -1539,7 +1625,7 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, thisgeom->size[1] = scl * m->vis.scale.camera * 0.4; thisgeom->size[2] = scl * m->vis.scale.camera * 0.3; mju_n2f(thisgeom->mat, d->cam_xmat+9*i, 9); - f2f(thisgeom->rgba, m->vis.rgba.camera, 4); + f2f(thisgeom->rgba, cam_rgba, 4); for (int k=0; k < 3; k++) { thisgeom->rgba[k] *= 0.5; // make lens body darker } @@ -1582,88 +1668,6 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, } } - // camera frustum - if (vopt->flags[mjVIS_CAMERA]) { - objtype = mjOBJ_CAMERA; - category = mjCAT_DECOR; - const float* rgba = m->vis.rgba.frustum; - mjtNum vnear[4][3], vfar[4][3]; - mjtNum center[3]; - mjtNum znear = m->vis.map.znear * m->stat.extent; - mjtNum zfar = m->vis.scale.frustum * scl; - float zver[2], zhor[2]; - for (int i=0; i < m->ncam; i++) { - if (m->cam_sensorsize[2*i+1] == 0) { - continue; - } - getFrustum(zver, zhor, znear, m->cam_intrinsic + 4*i, m->cam_sensorsize + 2*i); - - // frustum frame to convert from planes to vertex representation - mjtNum *cam_xpos = d->cam_xpos+3*i; - mjtNum *cam_xmat = d->cam_xmat+9*i; - mjtNum x[] = {cam_xmat[0], cam_xmat[3], cam_xmat[6]}; - mjtNum y[] = {cam_xmat[1], cam_xmat[4], cam_xmat[7]}; - mjtNum z[] = {cam_xmat[2], cam_xmat[5], cam_xmat[8]}; - - // vertices of the near plane - mju_addScl3(center, cam_xpos, z, -znear); - mju_addScl3(vnear[0], center, x, -zhor[0]); - mju_addScl3(vnear[1], center, x, zhor[1]); - mju_addScl3(vnear[2], center, x, zhor[1]); - mju_addScl3(vnear[3], center, x, -zhor[0]); - mju_addToScl3(vnear[0], y, -zver[0]); - mju_addToScl3(vnear[1], y, -zver[0]); - mju_addToScl3(vnear[2], y, zver[1]); - mju_addToScl3(vnear[3], y, zver[1]); - - // vertices of the far plane - zhor[0] *= zfar / znear; - zhor[1] *= zfar / znear; - zver[0] *= zfar / znear; - zver[1] *= zfar / znear; - mju_addScl3(center, cam_xpos, z, -zfar); - mju_addScl3(vfar[0], center, x, -zhor[0]); - mju_addScl3(vfar[1], center, x, zhor[1]); - mju_addScl3(vfar[2], center, x, zhor[1]); - mju_addScl3(vfar[3], center, x, -zhor[0]); - mju_addToScl3(vfar[0], y, -zver[0]); - mju_addToScl3(vfar[1], y, -zver[0]); - mju_addToScl3(vfar[2], y, zver[1]); - mju_addToScl3(vfar[3], y, zver[1]); - - // triangulation and wireframe of the frustum - for (int e=0; e < 4; e++) { - START - mju_sub3(x, vfar[e], vnear[e]); - mju_sub3(y, vnear[(e+1)%4], vnear[e]); - mju_cross(z, x, y); - mjtNum tri1[3] = {mju_normalize3(x), mju_normalize3(y), mju_normalize3(z)}; - mjtNum xmat1[9] = {x[0], y[0], z[0], x[1], y[1], z[1], x[2], y[2], z[2]}; - mjv_initGeom(thisgeom, mjGEOM_TRIANGLE, tri1, vnear[e], xmat1, rgba); - FINISH - START - mju_sub3(y, vnear[(e+1)%4], vfar[e]); - mju_sub3(x, vfar[(e+1)%4], vfar[e]); - mju_cross(z, x, y); - mjtNum tri2[3] = {mju_normalize3(x), mju_normalize3(y), mju_normalize3(z)}; - mjtNum xmat2[9] = {x[0], y[0], z[0], x[1], y[1], z[1], x[2], y[2], z[2]}; - mjv_initGeom(thisgeom, mjGEOM_TRIANGLE, tri2, vfar[e], xmat2, rgba); - FINISH - START - mjv_connector(thisgeom, mjGEOM_LINE, 3, vnear[e], vnear[(e+1)%4]); - f2f(thisgeom->rgba, rgba, 4); - FINISH - START - mjv_connector(thisgeom, mjGEOM_LINE, 3, vfar[e], vfar[(e+1)%4]); - f2f(thisgeom->rgba, rgba, 4); - FINISH - START - mjv_connector(thisgeom, mjGEOM_LINE, 3, vnear[e], vfar[e]); - f2f(thisgeom->rgba, rgba, 4); - FINISH - } - } - } // lights objtype = mjOBJ_LIGHT; @@ -2131,28 +2135,31 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { // update camera only void mjv_updateCamera(const mjModel* m, const mjData* d, mjvCamera* cam, mjvScene* scn) { - mjtNum ca, sa, ce, se, move[3], *mat; - mjtNum headpos[3], forward[3], up[3], right[3], ipd; - // return if nothing to do if (!m || !cam || cam->type == mjCAMERA_USER) { return; } - // initialize frustum - float zver[2], zhor[2] = {0, 0}; - float znear = m->vis.map.znear * m->stat.extent; - float zfar = m->vis.map.zfar * m->stat.extent; + // define extrinsics + mjtNum move[3]; + mjtNum headpos[3], forward[3], up[3], right[3]; - // get headpos, forward[3], up, right, ipd, fovy + // define intrinsics + int cid, orthographic = 0; + mjtNum fovy, ipd; + float* intrinsic = NULL; + float* sensorsize = NULL; + + // get headpos, forward, up, right, ipd, fovy, orthographic, intrinsic switch (cam->type) { case mjCAMERA_FREE: case mjCAMERA_TRACKING: // get global ipd ipd = m->vis.global.ipd; - // compute image size from global fovy - zver[0] = zver[1] = (float)znear * mju_tan(m->vis.global.fovy * (float)(mjPI/360.0)); + // get orthographic, fovy + orthographic = m->vis.global.orthographic; + fovy = m->vis.global.fovy; // move lookat for tracking if (cam->type == mjCAMERA_TRACKING) { @@ -2168,10 +2175,10 @@ void mjv_updateCamera(const mjModel* m, const mjData* d, mjvCamera* cam, mjvScen } // compute frame - ca = mju_cos(cam->azimuth/180.0*mjPI); - sa = mju_sin(cam->azimuth/180.0*mjPI); - ce = mju_cos(cam->elevation/180.0*mjPI); - se = mju_sin(cam->elevation/180.0*mjPI); + mjtNum ca = mju_cos(cam->azimuth/180.0*mjPI); + mjtNum sa = mju_sin(cam->azimuth/180.0*mjPI); + mjtNum ce = mju_cos(cam->elevation/180.0*mjPI); + mjtNum se = mju_sin(cam->elevation/180.0*mjPI); forward[0] = ce*ca; forward[1] = ce*sa; forward[2] = se; @@ -2184,25 +2191,27 @@ void mjv_updateCamera(const mjModel* m, const mjData* d, mjvCamera* cam, mjvScen mju_addScl3(headpos, cam->lookat, forward, -cam->distance); break; - case mjCAMERA_FIXED: { - // get id and check - int cid = cam->fixedcamid; + case mjCAMERA_FIXED: + // get id, check range + cid = cam->fixedcamid; if (cid < 0 || cid >= m->ncam) { mjERROR("fixed camera id is outside valid range"); } - // get camera-specific ipd and fovy + // get camera-specific ipd, orthographic, fovy ipd = m->cam_ipd[cid]; - // get frustum from intrinsics or from fovy + orthographic = m->cam_orthographic[cid]; + fovy = m->cam_fovy[cid]; + + // if positive sensorsize, get sensorsize and intrinsic if (m->cam_sensorsize[2*cid+1]) { - getFrustum(zver, zhor, znear, m->cam_intrinsic + 4*cid, m->cam_sensorsize + 2*cid); - } else { - zver[0] = zver[1] = (float)znear * mju_tan(m->cam_fovy[cid] * (float)(mjPI/360.0)); + sensorsize = m->cam_sensorsize + 2*cid; + intrinsic = m->cam_intrinsic + 4*cid; } // get pointer to camera orientation matrix - mat = d->cam_xmat + 9*cid; + mjtNum* mat = d->cam_xmat + 9*cid; // get frame forward[0] = -mat[2]; @@ -2215,13 +2224,26 @@ void mjv_updateCamera(const mjModel* m, const mjData* d, mjvCamera* cam, mjvScen right[1] = mat[3]; right[2] = mat[6]; mju_copy3(headpos, d->cam_xpos + 3*cid); - } - break; + break; default: mjERROR("unknown camera type"); } + // convert intrinsics to frustum parameters + float znear = m->vis.map.znear * m->stat.extent; + float zfar = m->vis.map.zfar * m->stat.extent; + float zver[2], zhor[2] = {0, 0}; + if (orthographic){ + zver[0] = zver[1] = fovy / 2; + } else { + if (!intrinsic) { + zver[0] = zver[1] = znear * mju_tan(fovy * mjPI/360.0); + } else { + getFrustum(zver, zhor, znear, intrinsic, sensorsize); + } + } + // compute GL cameras for (int view=0; view < 2; view++) { // set frame @@ -2231,6 +2253,9 @@ void mjv_updateCamera(const mjModel* m, const mjData* d, mjvCamera* cam, mjvScen scn->camera[view].up[i] = (float)up[i]; } + // set orthographic + scn->camera[view].orthographic = orthographic; + // set symmetric frustum using intrinsic camera matrix scn->camera[view].frustum_top = zver[1]; scn->camera[view].frustum_bottom = -zver[0]; diff --git a/src/render/render_gl3.c b/src/render/render_gl3.c index 6535874a..4591c4bb 100644 --- a/src/render/render_gl3.c +++ b/src/render/render_gl3.c @@ -694,7 +694,7 @@ static void initLights(mjvScene* scn) { // set projection and modelview static void setView(int view, mjrRect viewport, const mjvScene* scn, const mjrContext* con, - float* camProject, float* camView) { + float camProject[16], float camView[16]) { mjvGLCamera cam; // copy specified camera for stereo, average for mono (view = -1) @@ -709,24 +709,34 @@ static void setView(int view, mjrRect viewport, const mjvScene* scn, const mjrCo : 0.5f * (float)viewport.width / (float)viewport.height * (cam.frustum_top - cam.frustum_bottom); - // set projection + // prepare projection glMatrixMode(GL_PROJECTION); glLoadIdentity(); if (mjGLAD_GL_ARB_clip_control) { // reverse Z rendering mapping [znear, zfar] -> [1, 0] (ndc) glTranslatef(0.0f, 0.0f, 0.5f); glScalef(1.0f, 1.0f, -0.5f); - } - else { + } else { // reverse Z rendering mapping without shift [znear, zfar] -> [1, -1] (ndc) glScalef(1.0f, 1.0f, -1.0f); } - glFrustum(cam.frustum_center - halfwidth, + + // set projection, orthographic or perspective + if (cam.orthographic) { + glOrtho(cam.frustum_center - halfwidth, cam.frustum_center + halfwidth, cam.frustum_bottom, cam.frustum_top, cam.frustum_near, cam.frustum_far); + } else { + glFrustum(cam.frustum_center - halfwidth, + cam.frustum_center + halfwidth, + cam.frustum_bottom, + cam.frustum_top, + cam.frustum_near, + cam.frustum_far); + } // save projection matrix if requested if (camProject) { diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 3a48ace2..a8a0d77c 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2040,6 +2040,7 @@ void mjCModel::CopyTree(mjModel* m) { m->cam_targetbodyid[cid] = pc->targetbodyid; copyvec(m->cam_pos+3*cid, pc->pos, 3); copyvec(m->cam_quat+4*cid, pc->quat, 4); + m->cam_orthographic[cid] = pc->orthographic; m->cam_fovy[cid] = (mjtNum)pc->fovy; m->cam_ipd[cid] = (mjtNum)pc->ipd; copyvec(m->cam_resolution+2*cid, pc->resolution, 2); diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 59aa680e..0e05d98c 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -123,8 +123,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"visual", "*", "0"}, {"<"}, - {"global", "?", "11", "fovy", "ipd", "azimuth", "elevation", "linewidth", "glow", - "offwidth", "offheight", "realtime", "ellipsoidinertia", "bvactive"}, + {"global", "?", "12", "orthographic", "fovy", "ipd", "azimuth", "elevation", "linewidth", + "glow", "offwidth", "offheight", "realtime", "ellipsoidinertia", "bvactive"}, {"quality", "?", "5", "shadowsize", "offsamples", "numslices", "numstacks", "numquads"}, {"headlight", "?", "4", "ambient", "diffuse", "specular", "active"}, @@ -160,9 +160,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { "hfield", "mesh", "fitscale", "rgba", "fluidshape", "fluidcoef", "user"}, {"site", "?", "13", "type", "group", "pos", "quat", "material", "size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"}, - {"camera", "?", "16", "fovy", "ipd", "resolution", "pos", "quat", "axisangle", "xyaxes", - "zaxis", "euler", "mode", "focal", "focalpixel", "principal", "principalpixel", - "sensorsize", "user"}, + {"camera", "?", "17", "orthographic", "fovy", "ipd", "resolution", "pos", "quat", + "axisangle", "xyaxes", "zaxis", "euler", "mode", "focal", "focalpixel", + "principal", "principalpixel", "sensorsize", "user"}, {"light", "?", "13", "pos", "dir", "bulbradius", "directional", "castshadow", "active", "attenuation", "cutoff", "exponent", "ambient", "diffuse", "specular", "mode"}, {"pair", "?", "7", "condim", "friction", "solref", "solreffriction", "solimp", @@ -268,9 +268,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {">"}, {"site", "*", "15", "name", "class", "type", "group", "pos", "quat", "material", "size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"}, - {"camera", "*", "19", "name", "class", "fovy", "ipd", "resolution", "pos", "quat", - "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", "focal", "focalpixel", - "principal", "principalpixel", "sensorsize", "user"}, + {"camera", "*", "20", "name", "class", "orthographic", "fovy", "ipd", "resolution", "pos", + "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", + "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, {"light", "*", "16", "name", "class", "directional", "castshadow", "active", "pos", "dir", "bulbradius", "attenuation", "cutoff", "exponent", "ambient", "diffuse", "specular", "mode", "target"}, @@ -1742,6 +1742,10 @@ void mjXReader::OneCamera(XMLElement* elem, mjsCamera* pcam) { ReadAlternative(elem, pcam->alt); ReadAttr(elem, "ipd", 1, &pcam->ipd, text); + if (MapValue(elem, "orthographic", &n, bool_map, 2)) { + pcam->orthographic = (n==1); + } + bool has_principal = ReadAttr(elem, "principalpixel", 2, pcam->principal_pixel, text) || ReadAttr(elem, "principal", 2, pcam->principal_length, text); bool has_focal = ReadAttr(elem, "focalpixel", 2, pcam->focal_pixel, text) || @@ -2948,6 +2952,7 @@ void mjXReader::Visual(XMLElement* section) { string text, name; XMLElement* elem; mjVisual* vis = &model->visual; + int n; // iterate over child elements elem = FirstChildElement(section); @@ -2957,6 +2962,9 @@ void mjXReader::Visual(XMLElement* section) { // global sub-element if (name=="global") { + if (MapValue(elem, "orthographic", &n, bool_map, 2)) { + vis->global.orthographic = (n==1); + } ReadAttr(elem, "fovy", 1, &vis->global.fovy, text); ReadAttr(elem, "ipd", 1, &vis->global.ipd, text); ReadAttr(elem, "azimuth", 1, &vis->global.azimuth, text); @@ -2970,13 +2978,11 @@ void mjXReader::Visual(XMLElement* section) { throw mjXError(elem, "realtime must be greater than 0"); } } - int ellipsoidinertia; - if (MapValue(elem, "ellipsoidinertia", &ellipsoidinertia, bool_map, 2)) { - vis->global.ellipsoidinertia = (ellipsoidinertia==1); + if (MapValue(elem, "ellipsoidinertia", &n, bool_map, 2)) { + vis->global.ellipsoidinertia = (n==1); } - int bvactive; - if (MapValue(elem, "bvactive", &bvactive, bool_map, 2)) { - vis->global.bvactive = (bvactive==1); + if (MapValue(elem, "bvactive", &n, bool_map, 2)) { + vis->global.bvactive = (n==1); } } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index a9cda59a..c0f07e10 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -474,9 +474,7 @@ void mjXWriter::OneCamera(XMLElement* elem, const mjCCamera* pcam, mjCDef* def) WriteAttr(elem, "ipd", 1, &pcam->ipd, &def->Camera().ipd); WriteAttrKey(elem, "mode", camlight_map, camlight_sz, pcam->mode, def->Camera().mode); WriteAttr(elem, "resolution", 2, pcam->resolution, def->Camera().resolution); - - // resolution if positive - WriteAttr(elem, "resolution", 2, pcam->resolution, def->Camera().resolution); + WriteAttrKey(elem, "orthographic", bool_map, 2, pcam->orthographic, def->Camera().orthographic); // camera intrinsics if specified if (pcam->sensor_size[0]>0 && pcam->sensor_size[1]>0) { @@ -1012,15 +1010,16 @@ void mjXWriter::Visual(XMLElement* root) { // global elem = InsertEnd(section, "global"); - WriteAttr(elem, "fovy", 1, &vis->global.fovy, &visdef.global.fovy); - WriteAttr(elem, "ipd", 1, &vis->global.ipd, &visdef.global.ipd); - WriteAttr(elem, "azimuth", 1, &vis->global.azimuth, &visdef.global.azimuth); - WriteAttr(elem, "elevation", 1, &vis->global.elevation, &visdef.global.elevation); - WriteAttr(elem, "linewidth", 1, &vis->global.linewidth, &visdef.global.linewidth); - WriteAttr(elem, "glow", 1, &vis->global.glow, &visdef.global.glow); - WriteAttr(elem, "realtime", 1, &vis->global.realtime, &visdef.global.realtime); - WriteAttrInt(elem, "offwidth", vis->global.offwidth, visdef.global.offwidth); - WriteAttrInt(elem, "offheight", vis->global.offheight, visdef.global.offheight); + WriteAttrKey(elem, "orthographic", bool_map, 2, vis->global.orthographic, visdef.global.orthographic); + WriteAttr(elem, "fovy", 1, &vis->global.fovy, &visdef.global.fovy); + WriteAttr(elem, "ipd", 1, &vis->global.ipd, &visdef.global.ipd); + WriteAttr(elem, "azimuth", 1, &vis->global.azimuth, &visdef.global.azimuth); + WriteAttr(elem, "elevation", 1, &vis->global.elevation, &visdef.global.elevation); + WriteAttr(elem, "linewidth", 1, &vis->global.linewidth, &visdef.global.linewidth); + WriteAttr(elem, "glow", 1, &vis->global.glow, &visdef.global.glow); + WriteAttr(elem, "realtime", 1, &vis->global.realtime, &visdef.global.realtime); + WriteAttrInt(elem, "offwidth", vis->global.offwidth, visdef.global.offwidth); + WriteAttrInt(elem, "offheight", vis->global.offheight, visdef.global.offheight); WriteAttrKey(elem, "ellipsoidinertia", bool_map, 2, vis->global.ellipsoidinertia, visdef.global.ellipsoidinertia); WriteAttrKey(elem, "bvactive", bool_map, 2, vis->global.bvactive, visdef.global.bvactive); if (!elem->FirstAttribute()) { diff --git a/test/engine/testdata/vis_visualize/orthographic.xml b/test/engine/testdata/vis_visualize/orthographic.xml new file mode 100644 index 00000000..bdff71a3 --- /dev/null +++ b/test/engine/testdata/vis_visualize/orthographic.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 3a12e71c..1d13f7d1 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -1249,6 +1249,36 @@ TEST_F(XMLReaderTest, InvalidSkinGroup) { EXPECT_THAT( error.data(), HasSubstr("skin group must be between 0 and 5\nElement 'skin', line 7")); +} + +TEST_F(XMLReaderTest, Orthographic) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + + + )"; + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(model, NotNull()) << error.data(); + + EXPECT_EQ(model->vis.global.orthographic, 1); + EXPECT_EQ(model->cam_orthographic[0], 1); + EXPECT_EQ(model->cam_orthographic[1], 1); + EXPECT_EQ(model->cam_fovy[0], 1); + EXPECT_EQ(model->cam_fovy[1], 2); + mj_deleteModel(model); } diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 92131025..2c62ff81 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1118,6 +1118,16 @@ TEST_F(XMLWriterTest, TrimsDefaults) { mj_deleteModel(model); } +TEST_F(XMLWriterTest, DoesntSaveGlobal) { + static constexpr char xml[] = ""; + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << error.data(); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, Not(HasSubstr("global"))); + mj_deleteModel(model); +} + TEST_F(XMLWriterTest, InheritrangeSavedAsRange) { static constexpr char xml[] = R"( diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d3c68f8e..53b1ec48 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5013,6 +5013,7 @@ public unsafe struct mjOption_ { [StructLayout(LayoutKind.Sequential)] public unsafe struct global { + public int orthographic; public float fovy; public float ipd; public float azimuth; @@ -5321,11 +5322,12 @@ public unsafe struct mjModel_ { public double* cam_poscom0; public double* cam_pos0; public double* cam_mat0; - public int* cam_resolution; + public int* cam_orthographic; public double* cam_fovy; - public float* cam_intrinsic; - public float* cam_sensorsize; public double* cam_ipd; + public int* cam_resolution; + public float* cam_sensorsize; + public float* cam_intrinsic; public double* cam_user; public int* light_mode; public int* light_bodyid; @@ -5858,6 +5860,7 @@ public unsafe struct mjvCamera_ { public double distance; public double azimuth; public double elevation; + public int orthographic; } [StructLayout(LayoutKind.Sequential)] @@ -5871,6 +5874,7 @@ public unsafe struct mjvGLCamera_ { public float frustum_top; public float frustum_near; public float frustum_far; + public int orthographic; } [StructLayout(LayoutKind.Sequential)] @@ -6184,10 +6188,12 @@ public unsafe struct model { public int* site_group; public double* site_size; public float* site_rgba; + public int* cam_orthographic; public double* cam_fovy; public double* cam_ipd; - public float* cam_intrinsic; + public int* cam_resolution; public float* cam_sensorsize; + public float* cam_intrinsic; public byte* light_directional; public byte* light_castshadow; public float* light_bulbradius;