Add frustum visualization for orthographic cameras.

PiperOrigin-RevId: 847698917
Change-Id: I13633e8ba2d4718b127a5838c404ab74c6180e3d
This commit is contained in:
Yuval Tassa
2025-12-22 03:42:46 -08:00
committed by Copybara-Service
parent 158525269b
commit c7f5766345
4 changed files with 43 additions and 32 deletions
+9 -9
View File
@@ -2976,12 +2976,19 @@ and the +Y axis points up. Thus the frame position and orientation are the key a
for most scenes and should likely be reduced. In either case, the horizontal field of view is computed automatically 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. given the window size and the vertical field of view.
.. _body-camera-resolution:
:at:`resolution`: :at-val:`int(2), "1 1"`
Resolution of the camera in pixels [width height]. Note that these values are not used for rendering since those
dimensions are determined by the size of the rendering context. This attribute serves as a convenient
location to save the required resolution. Setting either value larger than 1
enables frustum visualization when the :ref:`mjVIS_CAMERA<mjtVisFlag>` visualization flag is active.
.. _body-camera-sensorsize: .. _body-camera-sensorsize:
:at:`sensorsize`: :at-val:`real(2), "0 0"` :at:`sensorsize`: :at-val:`real(2), "0 0"`
Size of the camera sensor in length units. When specified, all intrinsic attributes become active and :at:`fovy` Size of the camera sensor in length units. When specified, all intrinsic attributes become active and :at:`fovy`
is ignored. The field-of-view is then computed automatically from the focal length and sensor size. This is ignored. The field-of-view is then computed automatically from the focal length and sensor size.
also enables frustum visualization when the :ref:`mjVIS_CAMERA<mjtVisFlag>` visualization flag is active.
.. _body-camera-focal: .. _body-camera-focal:
.. _body-camera-focalpixel: .. _body-camera-focalpixel:
@@ -2998,13 +3005,6 @@ and the +Y axis points up. Thus the frame position and orientation are the key a
specified, the pixel value is used. At zero offset, the rendered image is centered on the camera's negative Z axis, specified, the pixel value is used. At zero offset, the rendered image is centered on the camera's negative Z axis,
as in a standard pinhole camera model. as in a standard pinhole camera model.
.. _body-camera-resolution:
:at:`resolution`: :at-val:`int(2), "1 1"`
Resolution of the camera in pixels [width height]. Note that these values are not used for rendering since those
dimensions are determined by the size of the rendering context. This attribute serves as a convenient
location to save the required resolution when creating a context.
.. _body-camera-ipd: .. _body-camera-ipd:
:at:`ipd`: :at-val:`real, "0.068"` :at:`ipd`: :at-val:`real, "0.068"`
+2
View File
@@ -18,6 +18,8 @@ Upcoming version (not yet released)
General General
^^^^^^^ ^^^^^^^
- Camera frustum visualization is now triggered by setting :ref:`resolution<body-camera-resolution>` to values larger
than 1. Relatedly, frustum visualization also works for :ref:`orthographic<body-camera-projection>` cameras.
- Non-breaking ABI changes: - Non-breaking ABI changes:
- The type of the ``sig`` (signature) argument of :ref:`mj_stateSize` and related functions has been changed from - The type of the ``sig`` (signature) argument of :ref:`mj_stateSize` and related functions has been changed from
+29 -20
View File
@@ -539,21 +539,13 @@ void mjv_cameraFrustum(float zver[2], float zhor[2], float zclip[2], const mjMod
const float znear = m->vis.map.znear * m->stat.extent; const float znear = m->vis.map.znear * m->stat.extent;
if (orthographic) { if (orthographic) {
if (zver) { if (zver) zver[0] = zver[1] = fovy / 2;
zver[0] = zver[1] = fovy / 2; if (zhor) zhor[0] = zhor[1] = 0.0f;
}
if (zhor) {
zhor[0] = zhor[1] = 0.0f;
}
} else if (intrinsic) { } else if (intrinsic) {
getFrustum(zver, zhor, znear, intrinsic, sensorsize); getFrustum(zver, zhor, znear, intrinsic, sensorsize);
} else { } else {
if (zver) { if (zver) zver[0] = zver[1] = znear * mju_tan(fovy * mjPI/360.0);
zver[0] = zver[1] = znear * mju_tan(fovy * mjPI/360.0); if (zhor) zhor[0] = zhor[1] = 0.0f;
}
if (zhor) {
zhor[0] = zhor[1] = 0.0f;
}
} }
if (zclip) { if (zclip) {
@@ -2232,8 +2224,8 @@ static void addCameraGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, m
float cam_rgba[4]; float cam_rgba[4];
f2f(cam_rgba, m->vis.rgba.camera, 4); f2f(cam_rgba, m->vis.rgba.camera, 4);
// draw frustum if sensorsize is defined // draw frustum if resolution larger than (1, 1)
if (m->cam_sensorsize[2*i+1] > 0) { if (m->cam_resolution[2*i] > 1 || m->cam_resolution[2*i+1] > 1) {
// when drawing frustum, make camera translucent // when drawing frustum, make camera translucent
cam_rgba[3] = 0.3; cam_rgba[3] = 0.3;
@@ -2244,9 +2236,22 @@ static void addCameraGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, m
mjtNum znear = m->vis.map.znear * m->stat.extent; mjtNum znear = m->vis.map.znear * m->stat.extent;
mjtNum zfar = m->vis.scale.frustum * scl; mjtNum zfar = m->vis.scale.frustum * scl;
float zver[2], zhor[2]; float zver[2], zhor[2];
int orthographic = m->cam_projection[i] == mjPROJ_ORTHOGRAPHIC;
// get frustum // get frustum
getFrustum(zver, zhor, znear, m->cam_intrinsic + 4*i, m->cam_sensorsize + 2*i); if (orthographic) {
float aspect = (float)m->cam_resolution[2*i] / m->cam_resolution[2*i+1];
zver[0] = zver[1] = m->cam_fovy[i] / 2;
zhor[0] = zhor[1] = m->cam_fovy[i] * aspect / 2;
} else if (m->cam_sensorsize[2*i] && m->cam_sensorsize[2*i+1]) {
// intrinsic-based perspective camera
getFrustum(zver, zhor, znear, m->cam_intrinsic+4*i, m->cam_sensorsize+2*i);
} else {
// fovy-based perspective camera
float aspect = (float)m->cam_resolution[2*i] / m->cam_resolution[2*i+1];
zver[0] = zver[1] = znear * mju_tan(m->cam_fovy[i] * mjPI / 360.0);
zhor[0] = zhor[1] = zver[0] * aspect;
}
// frustum frame to convert from planes to vertex representation // frustum frame to convert from planes to vertex representation
mjtNum* cam_xpos = d->cam_xpos+3*i; mjtNum* cam_xpos = d->cam_xpos+3*i;
@@ -2266,11 +2271,15 @@ static void addCameraGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, m
mju_addToScl3(vnear[2], y, zver[1]); mju_addToScl3(vnear[2], y, zver[1]);
mju_addToScl3(vnear[3], y, zver[1]); mju_addToScl3(vnear[3], y, zver[1]);
// vertices of the far plane // vertices of the far plane: scale for perspective, average(width, height) for orthographic
zhor[0] *= zfar / znear; if (!orthographic) {
zhor[1] *= zfar / znear; zhor[0] *= zfar / znear;
zver[0] *= zfar / znear; zhor[1] *= zfar / znear;
zver[1] *= zfar / znear; zver[0] *= zfar / znear;
zver[1] *= zfar / znear;
} else {
zfar = (zhor[0] + zver[0]) / 2;
}
mju_addScl3(center, cam_xpos, z, -zfar); mju_addScl3(center, cam_xpos, z, -zfar);
mju_addScl3(vfar[0], center, x, -zhor[0]); mju_addScl3(vfar[0], center, x, -zhor[0]);
mju_addScl3(vfar[1], center, x, zhor[1]); mju_addScl3(vfar[1], center, x, zhor[1]);
+3 -3
View File
@@ -4,7 +4,7 @@
<visual> <visual>
<!-- free camera is orthorgaphic with a 5m vertical field-of-view --> <!-- free camera is orthorgaphic with a 5m vertical field-of-view -->
<global orthographic="true" fovy="5"/> <global orthographic="true" fovy="5"/>
<scale camera="1"/> <scale camera="0.5"/>
</visual> </visual>
<asset> <asset>
@@ -26,9 +26,9 @@
</body> </body>
<!-- box should pefectly fit within field-of-view --> <!-- box should pefectly fit within field-of-view -->
<camera name="fovy = 1" pos=".5 .5 2" projection="orthographic" fovy="1"/> <camera name="fovy = 1" pos=".5 .5 2" projection="orthographic" fovy="1" resolution="640 480"/>
<!-- 2 floor squares should pefectly fit within field-of-view --> <!-- 2 floor squares should pefectly fit within field-of-view -->
<camera name="fovy = 2" pos="0 0 2" fovy="2"/> <!-- inherits projection="orthographic" from defaults --> <camera name="fovy = 2" pos="0 0 3" fovy="2"/> <!-- inherits projection="orthographic" from defaults -->
</worldbody> </worldbody>
</mujoco> </mujoco>