Add orthographic cameras.

Orthographic cameras are specified by setting the `orthographic` attribute of the `<camera>` 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
This commit is contained in:
Yuval Tassa
2024-06-11 09:21:39 -07:00
committed by Copybara-Service
parent 171b0d6e06
commit 07fc95ca9a
23 changed files with 490 additions and 235 deletions
+53 -15
View File
@@ -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;