diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index a9985c26..f42cf455 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -2910,8 +2910,8 @@ void mjv_updateCamera(const mjModel* m, const mjData* d, mjvCamera* cam, mjvScen 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]; + scn->camera[view].frustum_top = zver[0]; + scn->camera[view].frustum_bottom = -zver[1]; scn->camera[view].frustum_center = (zhor[1] - zhor[0]) / 2; scn->camera[view].frustum_width = (zhor[1] + zhor[0]) / 2; scn->camera[view].frustum_near = zclip[0]; diff --git a/test/engine/engine_vis_visualize_test.cc b/test/engine/engine_vis_visualize_test.cc index ce2e4022..90ec776e 100644 --- a/test/engine/engine_vis_visualize_test.cc +++ b/test/engine/engine_vis_visualize_test.cc @@ -108,5 +108,51 @@ TEST_F(MjvSceneTest, UpdateSceneGeomsExhausted) { mj_deleteModel(model); } +TEST_F(MjvSceneTest, PrincipalPointFrustumSign) { + constexpr char xml[] = R"( + + + + + + )"; + + mjModel* model = LoadModelFromString(xml); + ASSERT_THAT(model, NotNull()); + mjData* data = mj_makeData(model); + mj_forward(model, data); + + InitSceneObjects(model); + + // point camera at the fixed cam + cam_.type = mjCAMERA_FIXED; + cam_.fixedcamid = 0; + mjv_updateCamera(model, data, &cam_, &scn_); + + float top = scn_.camera[0].frustum_top; + float bottom = scn_.camera[0].frustum_bottom; + + // with cy > 0 the principal point is above center, so the frustum should + // extend further downward than upward: |bottom| > top + EXPECT_GT(-bottom, top); + + // verify exact values against the pinhole model + float znear = model->vis.map.znear * model->stat.extent; + float cy = model->cam_intrinsic[3]; + float fy = model->cam_intrinsic[1]; + float sh = model->cam_sensorsize[1]; + float half = znear / fy * (sh / 2); + float offset = znear / fy * cy; + + EXPECT_FLOAT_EQ(top, half - offset); + EXPECT_FLOAT_EQ(bottom, -(half + offset)); + + mj_deleteData(data); + FreeSceneObjects(); + mj_deleteModel(model); +} + } // namespace } // namespace mujoco