Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
2717294d7This file is pinned to the selected release. Download the original to open it in your design tool.
// Copyright 2024 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mujoco/mjvisualize.h>
#include <mujoco/mujoco.h>
#include "test/fixture.h"
namespace mujoco {
namespace {
using ::testing::NotNull;
static const char* const kModelPath = "testdata/model.xml";
class MjvSceneTest : public MujocoTest {
protected:
static constexpr int kMaxGeom = 10000;
void InitSceneObjects(mjModel* model, int maxgeom = kMaxGeom) {
mjv_defaultScene(&scn_);
mjv_makeScene(model, &scn_, maxgeom);
mjv_defaultOption(&opt_);
mjv_defaultPerturb(&pert_);
mjv_defaultFreeCamera(model, &cam_);
// enable flags to exercise additional code paths
for (int i = 0; i < mjNVISFLAG; ++i) {
opt_.flags[i] = 1;
}
}
void FreeSceneObjects() { mjv_freeScene(&scn_); }
mjvScene scn_;
mjvOption opt_;
mjvPerturb pert_;
mjvCamera cam_;
};
TEST_F(MjvSceneTest, UpdateScene) {
const std::string xml_path = GetTestDataFilePath(kModelPath);
char error[1024];
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
ASSERT_THAT(model, NotNull())
<< "Failed to load model from " << kModelPath << ": " << error;
InitSceneObjects(model);
mjData* data = mj_makeData(model);
while (data->time < .2) {
mj_step(model, data);
}
mjv_updateScene(model, data, &opt_, &pert_, &cam_, mjCAT_ALL, &scn_);
EXPECT_EQ(scn_.status, 0);
EXPECT_GT(scn_.ngeom, 0);
EXPECT_GT(scn_.nlight, 0);
if (model->nskin) EXPECT_GT(scn_.nskin, 0);
if (model->nflex) EXPECT_GT(scn_.nflex, 0);
mjv_updateScene(model, data, &opt_, &pert_, &cam_, mjCAT_ALL, &scn_);
// call mj_copyData to expose any memory leaks in mjv_updateScene
mjData* data_copy = mj_copyData(nullptr, model, data);
mj_deleteData(data_copy);
mj_deleteData(data);
FreeSceneObjects();
mj_deleteModel(model);
}
TEST_F(MjvSceneTest, UpdateSceneGeomsExhausted) {
const std::string xml_path = GetTestDataFilePath(kModelPath);
char error[1024];
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
ASSERT_THAT(model, NotNull())
<< "Failed to load model from " << kModelPath << ": " << error;
const int maxgeoms = 1;
InitSceneObjects(model, maxgeoms);
mjData* data = mj_makeData(model);
mj_forward(model, data);
// clear handlers to avoid test failure; we are explicitly expecting a warning
mju_clearHandlers();
mjv_updateScene(model, data, &opt_, &pert_, &cam_, mjCAT_ALL, &scn_);
EXPECT_EQ(scn_.status, 1);
EXPECT_EQ(scn_.ngeom, maxgeoms);
mj_deleteData(data);
FreeSceneObjects();
mj_deleteModel(model);
}
TEST_F(MjvSceneTest, PrincipalPointFrustumSign) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<camera name="cam" pos="0 0 1" zaxis="0 0 1"
sensorsize="0.01 0.01" focal="0.01 0.01"
principal="0 0.002"/>
</worldbody>
</mujoco>
)";
MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull());
MjDataPtr data = MakeData(model);
mj_forward(model.get(), data.get());
InitSceneObjects(model.get());
// point camera at the fixed cam
cam_.type = mjCAMERA_FIXED;
cam_.fixedcamid = 0;
mjv_updateCamera(model.get(), data.get(), &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));
FreeSceneObjects();
}
TEST_F(MjvSceneTest, InvalidFixedCamId) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<camera name="cam" pos="0 0 1"/>
</worldbody>
</mujoco>
)";
MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull());
MjDataPtr data = MakeData(model);
mj_forward(model.get(), data.get());
InitSceneObjects(model.get());
cam_.type = mjCAMERA_FIXED;
cam_.fixedcamid = -1;
EXPECT_THAT(MjuErrorMessageFrom(mjv_updateCamera)(model.get(), data.get(),
&cam_, &scn_),
::testing::HasSubstr("fixed camera id is outside valid range"));
EXPECT_THAT(MjuErrorMessageFrom(mjv_cameraFrame)(nullptr, nullptr, nullptr,
nullptr, data.get(), &cam_),
::testing::HasSubstr("fixed camera id is outside valid range"));
cam_.fixedcamid = model->ncam;
EXPECT_THAT(MjuErrorMessageFrom(mjv_updateCamera)(model.get(), data.get(),
&cam_, &scn_),
::testing::HasSubstr("fixed camera id is outside valid range"));
FreeSceneObjects();
}
static constexpr char kMeshSiteXml[] = R"(
<mujoco>
<asset>
<mesh name="offset_box"
vertex="1 -.5 0 3 -.5 0 3 .5 0 1 .5 0
1 -.5 4 3 -.5 4 3 .5 4 1 .5 4"/>
</asset>
<worldbody>
<body pos=".4 .5 .6" euler="10 20 30">
<freejoint/>
<inertial pos="0 0 0" mass="1" diaginertia="1 2 3"/>
<frame pos=".3 -.2 .1" euler="30 10 20">
<site name="plain" pos=".1 .2 .3" euler="20 30 10" size=".01"/>
<site name="mesh" type="mesh" mesh="offset_box"
pos=".1 .2 .3" euler="20 30 10"/>
<geom name="reference" type="mesh" mesh="offset_box"
pos=".1 .2 .3" euler="20 30 10" contype="0" conaffinity="0"/>
</frame>
</body>
</worldbody>
<sensor>
<framepos objtype="site" objname="plain"/>
<framepos objtype="site" objname="mesh"/>
<framequat objtype="site" objname="plain"/>
<framequat objtype="site" objname="mesh"/>
<gyro site="plain"/>
<gyro site="mesh"/>
<accelerometer site="plain"/>
<accelerometer site="mesh"/>
</sensor>
</mujoco>
)";
TEST_F(MjvSceneTest, MeshSitePreservesAuthoredFrame) {
MjModelPtr model = LoadModelFromString(kMeshSiteXml);
ASSERT_THAT(model.get(), NotNull());
MjDataPtr data = MakeData(model);
int plain = mj_name2id(model.get(), mjOBJ_SITE, "plain");
int mesh = mj_name2id(model.get(), mjOBJ_SITE, "mesh");
for (int j = 0; j < 3; ++j) {
EXPECT_EQ(model->site_pos[3 * plain + j], model->site_pos[3 * mesh + j]);
}
for (int j = 0; j < 4; ++j) {
EXPECT_EQ(model->site_quat[4 * plain + j], model->site_quat[4 * mesh + j]);
}
for (int i = 0; i < model->nv; ++i) {
data->qvel[i] = 0.1 * (i + 1);
data->qfrc_applied[i] = 0.2 * (i + 1);
}
mj_forward(model.get(), data.get());
for (int sensor = 0; sensor < model->nsensor; sensor += 2) {
int adr1 = model->sensor_adr[sensor];
int adr2 = model->sensor_adr[sensor + 1];
for (int j = 0; j < model->sensor_dim[sensor]; ++j) {
EXPECT_NEAR(data->sensordata[adr1 + j], data->sensordata[adr2 + j], 1e-6);
}
}
}
TEST_F(MjvSceneTest, MeshSitePreservesVisualPoseAndVolume) {
MjModelPtr model = LoadModelFromString(kMeshSiteXml);
ASSERT_THAT(model.get(), NotNull());
MjDataPtr data = MakeData(model);
mj_forward(model.get(), data.get());
int site = mj_name2id(model.get(), mjOBJ_SITE, "mesh");
int geom = mj_name2id(model.get(), mjOBJ_GEOM, "reference");
InitSceneObjects(model.get());
mjv_updateScene(model.get(), data.get(), &opt_, &pert_, &cam_, mjCAT_ALL,
&scn_);
const mjvGeom* mesh_site = nullptr;
const mjvGeom* mesh_geom = nullptr;
for (int i = 0; i < scn_.ngeom; ++i) {
const mjvGeom* item = scn_.geoms + i;
if (item->objtype == mjOBJ_SITE && item->objid == site) mesh_site = item;
if (item->objtype == mjOBJ_GEOM && item->objid == geom) mesh_geom = item;
}
ASSERT_THAT(mesh_site, NotNull());
ASSERT_THAT(mesh_geom, NotNull());
for (int j = 0; j < 3; ++j) {
EXPECT_NEAR(mesh_site->pos[j], mesh_geom->pos[j], 1e-6);
}
for (int j = 0; j < 9; ++j) {
EXPECT_NEAR(mesh_site->mat[j], mesh_geom->mat[j], 1e-6);
}
EXPECT_EQ(
mj_insideSite(model.get(), data.get(), site, data->geom_xpos + 3 * geom),
1);
mjtNum outside[3] = {100, 100, 100};
EXPECT_EQ(mj_insideSite(model.get(), data.get(), site, outside), 0);
FreeSceneObjects();
}
TEST_F(MjvSceneTest, TendonWrapGeodesicLength) {
constexpr char kWrapXml[] = R"(
<mujoco>
<worldbody>
<site name="0a" pos="0 0 1"/>
<geom name="0g" type="sphere" size=".1" pos="0 0 0.5"/>
<site name="0b" pos="0 .05 .15"/>
<site name="1a" pos=".4 0 1"/>
<geom name="1g" type="sphere" size=".1" pos=".4 0 0.5"/>
<site name="1s" pos=".52 0 .5"/>
<site name="1b" pos=".25 0 .15"/>
<site name="2a" pos="-.4 0 1"/>
<geom name="2g" type="sphere" size=".1" pos="-.4 0 0.5"/>
<site name="2s" pos="-.49 0 .5"/>
<site name="2b" pos="-.4 0 .15"/>
<site name="3a" pos="0 1 1"/>
<geom name="3g" type="cylinder" size=".05 .2" zaxis="0 1 .2" pos="0 1 0.5"/>
<site name="3s" pos=".12 1 .5"/>
<site name="3b" pos="-.15 1.1 .15"/>
</worldbody>
<tendon>
<spatial>
<site site="0a"/>
<geom geom="0g"/>
<site site="0b"/>
</spatial>
<spatial>
<site site="1a"/>
<geom geom="1g" sidesite="1s"/>
<site site="1b"/>
</spatial>
<spatial>
<site site="2a"/>
<geom geom="2g" sidesite="2s"/>
<site site="2b"/>
</spatial>
<spatial>
<site site="3a"/>
<geom geom="3g" sidesite="3s"/>
<site site="3b"/>
</spatial>
</tendon>
</mujoco>
)";
MjModelPtr model = LoadModelFromString(kWrapXml);
ASSERT_THAT(model.get(), NotNull());
MjDataPtr data = MakeData(model);
mj_forward(model.get(), data.get());
InitSceneObjects(model.get());
mjv_updateScene(model.get(), data.get(), &opt_, &pert_, &cam_, mjCAT_ALL,
&scn_);
for (int i = 0; i < model->ntendon; ++i) {
mjtNum vis_length = 0;
for (int g = 0; g < scn_.ngeom; ++g) {
const mjvGeom* geom = scn_.geoms + g;
if (geom->objtype == mjOBJ_TENDON && geom->objid == i) {
vis_length += 2 * geom->size[2];
}
}
EXPECT_NEAR(vis_length, data->ten_length[i], 1e-3) << "tendon=" << i;
}
FreeSceneObjects();
}
} // namespace
} // namespace mujoco