Files
Mujoco_WASM/src/engine/engine_vis_visualize.c
T
Yuval Tassa 4787c8094c Add geom surfacevel: zero-dof conveyors, treadmills and turntables.
https://www.youtube.com/watch?v=PdSdrqhSiZA

The new geom attribute surfacevel (6 numbers: linear and angular velocity in the geom's local frame, angular about the geom frame origin) specifies the velocity of the geom's surface material relative to the geom frame. The relative surface velocity of the two geoms is added to the tangential contact rows of efc_vel in mj_referenceConstraint, so friction drives touching bodies toward the motion of the surface: objects on a conveyor are transported at belt speed, turntables impart omega x r with torsional spin-up for condim >= 4, and surface velocities compose with each other and with body motion. The component along the contact normal is projected out: probe experiments showed that velocity-space emission chatters mass-independently and ingestion merely deepens penetration; normal-direction effects belong to force-space features.

surfacevel is interpreted in the geom frame as authored: for mesh geoms, whose compiled frame absorbs the mesh centering and principal-axes transform, the compiler re-expresses the authored value in the compiled frame.

No special interaction with sleeping: objects being transported do not fall asleep because they are moving; objects at rest on an active surface may sleep like any other resting object.

Includes showcase models (model/surfacevel/): a luggage carousel whose ring is a spinning square-profile supertorus fed by a cascade of belts with matched spinning end rollers, bags dropping in and circulating indefinitely; and a treadmill with a passive humanoid.

PiperOrigin-RevId: 948647785
Change-Id: I0c6559a91cc7ece1237eb8ac2e51986e7342d962
2026-07-15 17:58:17 -07:00

3722 lines
114 KiB
C

// Copyright 2021 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 "engine/engine_vis_visualize.h"
#include <math.h>
#include <stddef.h>
#include <string.h>
#include <mujoco/mjdata.h>
#include <mujoco/mjmacro.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjsan.h> // IWYU pragma: keep
#include <mujoco/mjvisualize.h>
#include "engine/engine_array_safety.h"
#include "engine/engine_core_util.h"
#include "engine/engine_memory.h"
#include "engine/engine_name.h"
#include "engine/engine_plugin.h"
#include "engine/engine_sleep.h"
#include "engine/engine_support.h"
#include "engine/engine_util_blas.h"
#include "engine/engine_util_errmem.h"
#include "engine/engine_util_misc.h"
#include "engine/engine_util_spatial.h"
#include "engine/engine_vis_init.h"
#include "engine/engine_vis_interact.h"
//----------------------------- utility functions and macros ---------------------------------------
static const mjtNum IDENTITY[9] = {1, 0, 0,
0, 1, 0,
0, 0, 1};
// copy float array
static void f2f(float* dest, const float* src, int n) {
memcpy(dest, src, n*sizeof(float));
}
// make text label
static void makeLabel(const mjModel* m, mjtObj type, int id, char* label) {
const char* typestr = mju_type2Str(type);
const char* namestr = mj_id2name(m, type, id);
char txt[100];
// copy existing name or make numeric name
if (namestr) {
mjSNPRINTF(txt, "%s", namestr);
} else if (typestr) {
mjSNPRINTF(txt, "%s %d", typestr, id);
} else {
mjSNPRINTF(txt, "%d", id);
}
// copy result into label
strncpy(label, txt, 100);
label[99] = '\0';
}
// convert HSV to RGB
void hsv2rgb(float *RGB, float H, float S, float V) {
float R, G, B;
if (S <= 0) {
R = G = B = V;
} else {
float hh = H * 6;
int i = (int)hh;
float ff = hh - i;
float p = V * (1 - S);
float q = V * (1 - (S * ff));
float t = V * (1 - (S * (1 - ff)));
if (i == 0) {
R=V; G=t; B=p;
} else if (i == 1) {
R=q; G=V; B=p;
} else if (i == 2) {
R=p; G=V; B=t;
} else if (i == 3) {
R=p; G=q; B=V;
} else if (i == 4) {
R=t; G=p; B=V;
} else {
R=V; G=p; B=q;
}
}
RGB[0] = R;
RGB[1] = G;
RGB[2] = B;
}
// assign pseudo-random rgba to constraint island using Halton sequence
static void islandColor(float rgba[4], int h, int awake) {
// default to gray R = G = B = 0.7;
float hue = 1.0f;
float saturation = 0.0f;
float value = 0.7f;
// island index given, use Halton sequence to generate pseudo-random color
if (h >= 0) {
// hue in [0, 1]
hue = mju_Halton(h + 1, 7);
// saturation in [0.5, 1.0]
saturation = .5 + .5*mju_Halton(h + 1, 3);
// value in [0.6, 1.0]
value = .6 + .4*mju_Halton(h + 1, 5);
}
// if asleep, decrease saturation and value
if (!awake) {
value *= 0.6;
saturation *= 0.7;
}
hsv2rgb(rgba, hue, saturation, value);
rgba[3] = 1;
}
// mix colors for perturbation object
static void mixcolor(float rgba[4], const float ref[4], int flg1, int flg2) {
rgba[0] = flg1 ? ref[0] : 0;
if (flg2) {
rgba[0] = mjMAX(rgba[0], ref[1]);
}
rgba[1] = flg1 ? ref[1] : 0;
if (flg2) {
rgba[1] = mjMAX(rgba[1], ref[0]);
}
rgba[2] = ref[2];
rgba[3] = ref[3];
}
// a body is static if it is welded to the world and is not a mocap body or descendant thereof
static int bodycategory(const mjModel* m, int bodyid) {
if (m->body_weldid[bodyid] == 0 && m->body_mocapid[m->body_rootid[bodyid]] == -1) {
return mjCAT_STATIC;
} else {
return mjCAT_DYNAMIC;
}
}
//----------------------------- geom functions -----------------------------------------------------
// acquires and initializes the next available geom in the scene
mjvGeom* acquireGeom(mjvScene* scn, int objid, int category, int objtype) {
// check for overflow, SHOULD NOT OCCUR
if (scn->ngeom >= scn->maxgeom) {
if (!scn->status) {
mju_warning("Pre-allocated visual geom buffer is full. "
"Increase maxgeom above %d.", scn->maxgeom);
scn->status = 1;
}
return NULL;
}
mjvGeom* thisgeom = scn->geoms + scn->ngeom;
memset(thisgeom, 0, sizeof(mjvGeom));
mjv_initGeom(thisgeom, mjGEOM_NONE, NULL, NULL, NULL, NULL);
thisgeom->objtype = objtype;
thisgeom->objid = objid;
thisgeom->category = category;
thisgeom->segid = scn->ngeom;
return thisgeom;
}
// mark geom as used, set its pointer to NULL, increment scn->ngeom
void releaseGeom(mjvGeom** geom, mjvScene* scn) {
// check geom being released was most recently acquired, SHOULD NOT OCCUR
if (*geom != scn->geoms + scn->ngeom) {
mju_error("Unexpected geom pointer; did you call acquireGeom?");
}
scn->ngeom++;
*geom = NULL;
}
// add a triangle to the scene
static void addTriangle(mjvScene* scn, const mjtNum v0[3], const mjtNum v1[3],
const mjtNum v2[3], const float rgba[4],
int objid, int category, int objtype) {
mjvGeom* thisgeom = acquireGeom(scn, objid, category, objtype);
if (!thisgeom) {
return;
}
mjtNum e1[3] = {v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]};
mjtNum e2[3] = {v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]};
mjtNum normal[3];
mju_cross(normal, e1, e2);
mjtNum lengths[3] = {mju_normalize3(e1), mju_normalize3(e2), mju_normalize3(normal)};
mjtNum xmat[9] = {e1[0], e2[0], normal[0],
e1[1], e2[1], normal[1],
e1[2], e2[2], normal[2]};
mjv_initGeom(thisgeom, mjGEOM_TRIANGLE, lengths, v0, xmat, rgba);
releaseGeom(&thisgeom, scn);
}
// copy material fields from model to visual geom
static void setMaterial(const mjModel* m, mjvGeom* geom, int matid, const float* rgba,
const mjtByte* flags) {
// set material properties if given
if (matid >= 0) {
f2f(geom->rgba, m->mat_rgba + 4*matid, 4);
geom->emission = m->mat_emission[matid];
geom->specular = m->mat_specular[matid];
geom->shininess = m->mat_shininess[matid];
geom->reflectance = m->mat_reflectance[matid];
}
// use rgba if different from default, or no material given
if (rgba[0] != 0.5f || rgba[1] != 0.5f || rgba[2] != 0.5f || rgba[3] != 1.0f || matid < 0) {
f2f(geom->rgba, rgba, 4);
}
// set texture
if (flags[mjVIS_TEXTURE] && matid >= 0) {
geom->matid = matid;
}
// scale alpha for dynamic geoms only
if (flags[mjVIS_TRANSPARENT] && (geom->category == mjCAT_DYNAMIC)) {
geom->rgba[3] *= m->vis.map.alpha;
}
}
// set (type, size, pos, mat) connector-type geom between given points
// assume that mjv_initGeom was already called to set all other properties
void mjv_connector(mjvGeom* geom, int type, mjtNum width,
const mjtNum from[3], const mjtNum to[3]) {
mjtNum quat[4], mat[9], dif[3] = {to[0]-from[0], to[1]-from[1], to[2]-from[2]};
// require connector-compatible type
if (type != mjGEOM_CAPSULE && type != mjGEOM_CYLINDER &&
type != mjGEOM_ARROW && type != mjGEOM_ARROW1 && type != mjGEOM_ARROW2
&& type != mjGEOM_LINE) {
mjERROR("invalid geom type %d for connector", type);
}
// assign type
geom->type = type;
// compute size for XYZ scaling
geom->size[0] = geom->size[1] = (float)width;
geom->size[2] = (float)mju_norm3(dif);
// cylinder and capsule are centered, and size[0] is "radius"
if (type == mjGEOM_CAPSULE || type == mjGEOM_CYLINDER) {
geom->pos[0] = 0.5*(from[0] + to[0]);
geom->pos[1] = 0.5*(from[1] + to[1]);
geom->pos[2] = 0.5*(from[2] + to[2]);
geom->size[2] *= 0.5;
}
// arrow is not centered
else {
geom->pos[0] = from[0];
geom->pos[1] = from[1];
geom->pos[2] = from[2];
}
// set mat to minimal rotation aligning b-a with z axis
mju_quatZ2Vec(quat, dif);
mju_quat2Mat(mat, quat);
mju_n2f(geom->mat, mat, 9);
}
// add a connector to the scene
static void addConnector(mjvScene* scn, int type, mjtNum width,
const mjtNum from[3], const mjtNum to[3],
const float rgba[4], int objid, int category, int objtype) {
mjvGeom* thisgeom = acquireGeom(scn, objid, category, objtype);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, type, width, from, to);
if (rgba) f2f(thisgeom->rgba, rgba, 4);
releaseGeom(&thisgeom, scn);
}
// initialize given fields when not NULL, set the rest to their default values
void mjv_initGeom(mjvGeom* geom, int type, const mjtNum* size,
const mjtNum* pos, const mjtNum* mat, const float* rgba) {
// assign type
geom->type = type;
// set size (for XYZ scaling)
if (size) {
switch ((mjtGeom) type) {
case mjGEOM_SPHERE:
geom->size[0] = (float)size[0];
geom->size[1] = (float)size[0];
geom->size[2] = (float)size[0];
break;
case mjGEOM_CAPSULE:
geom->size[0] = (float)size[0];
geom->size[1] = (float)size[0];
geom->size[2] = (float)size[1];
break;
case mjGEOM_CYLINDER:
geom->size[0] = (float)size[0];
geom->size[1] = (float)size[0];
geom->size[2] = (float)size[1];
break;
default:
mju_n2f(geom->size, size, 3);
}
} else {
geom->size[0] = 0.1f;
geom->size[1] = 0.1f;
geom->size[2] = 0.1f;
}
// set pos
if (pos) {
mju_n2f(geom->pos, pos, 3);
} else {
geom->pos[0] = 0;
geom->pos[1] = 0;
geom->pos[2] = 0;
}
// set mat
if (mat) {
mju_n2f(geom->mat, mat, 9);
} else {
geom->mat[0] = 1;
geom->mat[1] = 0;
geom->mat[2] = 0;
geom->mat[3] = 0;
geom->mat[4] = 1;
geom->mat[5] = 0;
geom->mat[6] = 0;
geom->mat[7] = 0;
geom->mat[8] = 1;
}
// set rgba
if (rgba) {
f2f(geom->rgba, rgba, 4);
} else {
geom->rgba[0] = 0.5;
geom->rgba[1] = 0.5;
geom->rgba[2] = 0.5;
geom->rgba[3] = 1;
}
// set defaults that cannot be assigned via this function
geom->dataid = -1;
geom->matid = -1;
geom->texcoord = 0;
geom->emission = 0;
geom->specular = 0.5;
geom->shininess = 0.5;
geom->reflectance = 0;
geom->label[0] = 0;
geom->modelrbound = 0;
}
// mark geom as selected
static void markselected(const mjVisual* vis, mjvGeom* geom) {
// add emission
geom->emission += vis->global.glow;
}
// draw 3 cylinders representing a "frame" decor element
void addFrame(mjvScene* scn, int objid, const mjtNum pos[3], const mjtNum rot[9], float length,
float width) {
// draw separate geoms for each axis
for (int j=0; j < 3; j++) {
mjtNum axis[3];
for (int k=0; k < 3; k++) {
axis[k] = (j == k ? length : 0);
}
mjtNum vec[3];
mju_mulMatVec3(vec, rot, axis);
// create a cylinder
mjtNum to[3];
mju_add3(to, pos, vec);
mjvGeom* thisgeom = acquireGeom(scn, objid, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, mjGEOM_CYLINDER, width, pos, to);
for (int k=0; k < 3; k++) {
thisgeom->rgba[k] = (j == k ? 0.9 : 0);
}
thisgeom->rgba[3] = 1;
releaseGeom(&thisgeom, scn);
}
}
//----------------------------- camera functions --------------------------------------------------
// computes the camera frustum
static void getFrustum(float zver[2], float zhor[2], float znear,
const float intrinsic[4], const float sensorsize[2]) {
if (zhor) {
zhor[0] = znear / intrinsic[0] * (sensorsize[0]/2.f - intrinsic[2]);
zhor[1] = znear / intrinsic[0] * (sensorsize[0]/2.f + intrinsic[2]);
}
if (zver) {
zver[0] = znear / intrinsic[1] * (sensorsize[1]/2.f - intrinsic[3]);
zver[1] = znear / intrinsic[1] * (sensorsize[1]/2.f + intrinsic[3]);
}
}
void mjv_cameraFrame(mjtNum headpos[3], mjtNum forward[3], mjtNum up[3], mjtNum right[3],
const mjData* d, const mjvCamera* cam) {
switch (cam->type) {
case mjCAMERA_FREE:
case mjCAMERA_TRACKING: {
const mjtNum ca = mju_cos(cam->azimuth/180.0*mjPI);
const mjtNum sa = mju_sin(cam->azimuth/180.0*mjPI);
const mjtNum ce = mju_cos(cam->elevation/180.0*mjPI);
const mjtNum se = mju_sin(cam->elevation/180.0*mjPI);
if (forward) {
forward[0] = ce*ca;
forward[1] = ce*sa;
forward[2] = se;
}
if (up) {
up[0] = -se*ca;
up[1] = -se*sa;
up[2] = ce;
}
if (right) {
right[0] = sa;
right[1] = -ca;
right[2] = 0;
}
if (headpos) {
mju_addScl3(headpos, cam->lookat, forward, -cam->distance);
}
break;
}
case mjCAMERA_FIXED: {
const int cid = cam->fixedcamid;
const mjtNum* mat = d->cam_xmat + 9*cid;
if (forward) {
forward[0] = -mat[2];
forward[1] = -mat[5];
forward[2] = -mat[8];
}
if (up) {
up[0] = mat[1];
up[1] = mat[4];
up[2] = mat[7];
}
if (right) {
right[0] = mat[0];
right[1] = mat[3];
right[2] = mat[6];
}
if (headpos) {
mju_copy3(headpos, d->cam_xpos + 3*cid);
}
break;
}
default: {
mjERROR("unknown camera type");
}
}
}
void mjv_cameraFrustum(float zver[2], float zhor[2], float zclip[2], const mjModel* m,
const mjvCamera* cam) {
mjtNum fovy;
int orthographic = 0, cid = 0;
float* intrinsic = NULL;
float* sensorsize = NULL;
// get ipd, fovy, orthographic, intrinsic
switch (cam->type) {
case mjCAMERA_FREE:
case mjCAMERA_TRACKING:
orthographic = m->vis.global.orthographic;
fovy = m->vis.global.fovy;
break;
case mjCAMERA_FIXED:
// get id, check range
cid = cam->fixedcamid;
if (cid < 0 || cid >= m->ncam) {
mjERROR("fixed camera id is outside valid range");
}
orthographic = m->cam_projection[cid] == mjPROJ_ORTHOGRAPHIC;
fovy = m->cam_fovy[cid];
// if positive sensorsize, get sensorsize and intrinsic
if (m->cam_sensorsize[2*cid+1]) {
sensorsize = m->cam_sensorsize + 2*cid;
intrinsic = m->cam_intrinsic + 4*cid;
}
break;
default:
mjERROR("unknown camera type");
}
const float znear = m->vis.map.znear * m->stat.extent;
if (orthographic) {
if (zver) zver[0] = zver[1] = fovy / 2;
if (zhor) zhor[0] = zhor[1] = 0.0f;
} else if (intrinsic) {
getFrustum(zver, zhor, znear, intrinsic, sensorsize);
} else {
if (zver) zver[0] = zver[1] = znear * mju_tan(fovy * mjPI/360.0);
if (zhor) zhor[0] = zhor[1] = 0.0f;
}
if (zclip) {
zclip[0] = znear;
zclip[1] = m->vis.map.zfar * m->stat.extent;
}
}
//----------------------------- main API functions -------------------------------------------------
// add contact-related geoms in mjvObject
static void addContactGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn,
int catmask) {
if (!vopt->flags[mjVIS_CONTACTPOINT] && !vopt->flags[mjVIS_CONTACTFORCE] &&
vopt->frame != mjFRAME_CONTACT) {
return;
}
int objtype = mjOBJ_UNKNOWN, category = mjCAT_DECOR;
mjtNum mat[9], tmp[9], vec[3], frc[3], confrc[6];
mjtNum framewidth, framelength, scl = m->stat.meansize;
mjContact* con;
mjvGeom* thisgeom;
mjtByte split;
// loop over contacts
for (int i=0; i < d->ncon; i++) {
// get pointer
con = d->contact + i;
// mat = contact rotation matrix (normal along z)
mju_copy(tmp, con->frame+3, 6);
mju_copy(tmp+6, con->frame, 3);
mju_transpose(mat, tmp, 3, 3);
// contact point
if (vopt->flags[mjVIS_CONTACTPOINT]) {
thisgeom = acquireGeom(scn, i, category, objtype);
if (!thisgeom) {
return;
}
thisgeom->type = mjGEOM_CYLINDER;
thisgeom->size[0] = thisgeom->size[1] = m->vis.scale.contactwidth * scl;
float halfheight = m->vis.scale.contactheight * scl;
float halfdepth = -con->dist / 2;
thisgeom->size[2] = mjMAX(halfheight, halfdepth);
mju_n2f(thisgeom->pos, con->pos, 3);
mju_n2f(thisgeom->mat, mat, 9);
int efc_adr = d->contact[i].efc_address;
// override standard colors if visualizing islands
if (vopt->flags[mjVIS_ISLAND] && efc_adr >= 0) {
// set hue using island's first dof
int h = d->nisland > 0 ? d->island_dofadr[d->efc_island[efc_adr]] : -1;
islandColor(thisgeom->rgba, h, /*awake*/1);
}
// otherwise regular colors (different for included and excluded contacts)
else {
if (efc_adr >= 0) {
f2f(thisgeom->rgba, m->vis.rgba.contactpoint, 4);
} else {
f2f(thisgeom->rgba, m->vis.rgba.contactgap, 4);
}
}
// label contacting geom names or ids
if (vopt->label == mjLABEL_CONTACTPOINT) {
char contactlabel[2][48];
for (int k=0; k < 2; k++) {
// make geom label
if (con->geom[k] >= 0) {
const char* geomname = mj_id2name(m, mjOBJ_GEOM, con->geom[k]);
if (geomname) {
mjSNPRINTF(contactlabel[k], "%s", geomname);
} else {
mjSNPRINTF(contactlabel[k], "g%d", con->geom[k]);
}
}
// make flex elem or vert label
else {
const char* flexname = mj_id2name(m, mjOBJ_FLEX, con->flex[k]);
if (flexname) {
if (con->elem[k] >= 0) {
mjSNPRINTF(contactlabel[k], "%s.e%d", flexname, con->elem[k]);
} else {
mjSNPRINTF(contactlabel[k], "%s.v%d", flexname, con->vert[k]);
}
}
else {
if (con->elem[k] >= 0) {
mjSNPRINTF(contactlabel[k], "f%d.e%d", con->flex[k], con->elem[k]);
} else {
mjSNPRINTF(contactlabel[k], "f%d.v%d", con->flex[k], con->vert[k]);
}
}
}
}
mjSNPRINTF(thisgeom->label, "%s | %s", contactlabel[0], contactlabel[1]);
}
float contactrgba[4];
f2f(contactrgba, thisgeom->rgba, 4);
releaseGeom(&thisgeom, scn);
// surface velocity: one arrow per moving surface, pointing along the
// tangential material velocity at the contact point
const mjtNum kVelocityMap = 0.5; // units of time: arrow length = velocity * kVelocityMap
for (int side=0; side < 2; side++) {
int g = con->geom[side];
if (g < 0) {
// TODO(team): support flex
continue;
}
const mjtNum* sv = m->geom_surfacevel + 6*g;
if (!sv[0] && !sv[1] && !sv[2] && !sv[3] && !sv[4] && !sv[5]) {
continue;
}
// material velocity at the contact point, world frame
mjtNum vw[3], ww[3];
mj_geomSurfaceVelocity(m, d, g, con->pos, vw, ww);
// project out the normal component: only the tangential part acts
mjtNum vn = mju_dot3(vw, con->frame);
mju_addToScl3(vw, con->frame, -vn);
if (mju_norm3(vw) < mjMINVAL) {
continue;
}
// anchor slightly off the contact point on the owning geom's side
mjtNum from[3], to[3];
mjtNum offset = (side ? 1 : -1) * 0.5 * m->vis.scale.forcewidth * scl;
mju_addScl3(from, con->pos, con->frame, offset);
mju_addScl3(to, from, vw, kVelocityMap);
thisgeom = acquireGeom(scn, i, category, objtype);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, mjGEOM_ARROW, m->vis.scale.forcewidth * scl, from, to);
f2f(thisgeom->rgba, contactrgba, 4);
releaseGeom(&thisgeom, scn);
}
}
// mat = contact frame rotation matrix (normal along x)
mju_transpose(mat, con->frame, 3, 3);
// contact frame
if (vopt->frame == mjFRAME_CONTACT) {
// set length and width of axis cylinders using half regular frame scaling
framelength = m->vis.scale.framelength * scl / 2;
framewidth = m->vis.scale.framewidth * scl / 2;
addFrame(scn, i, con->pos, mat, framelength, framewidth);
}
// nothing else to do for excluded contacts
if (d->contact[i].efc_address < 0) {
continue;
}
// get contact force:torque in contact frame
mj_contactForce(m, d, i, confrc);
// contact force
if (vopt->flags[mjVIS_CONTACTFORCE]) {
// get force, fill zeros if only normal
mju_zero3(frc);
mju_copy(frc, confrc, mjMIN(3, con->dim));
if (mju_norm3(frc) < mjMINVAL) {
continue;
}
// render combined or split
split = (vopt->flags[mjVIS_CONTACTSPLIT] && con->dim > 1);
for (int j = (split ? 1 : 0); j < (split ? 3 : 1); j++) {
// set vec to combined, normal or friction force, in world frame
switch (j) {
case 0: // combined
mju_mulMatVec(vec, mat, frc, 3, 3);
break;
case 1: // normal
vec[0] = mat[0]*frc[0];
vec[1] = mat[3]*frc[0];
vec[2] = mat[6]*frc[0];
break;
case 2: // friction
vec[0] = mat[1]*frc[1] + mat[2]*frc[2];
vec[1] = mat[4]*frc[1] + mat[5]*frc[2];
vec[2] = mat[7]*frc[1] + mat[8]*frc[2];
break;
}
// scale vector
mju_scl3(vec, vec, m->vis.map.force/m->stat.meanmass);
// get bodyflex ids
int bf[2];
for (int k=0; k < 2; k++) {
bf[k] = (con->geom[k] >= 0) ? m->geom_bodyid[con->geom[k]] :
m->nbody + con->flex[k];
}
// make sure arrow points towards bodyflex with higher id
if (bf[0] > bf[1]) {
mju_scl3(vec, vec, -1);
}
// one-directional arrow for friction and world, symmetric otherwise
thisgeom = acquireGeom(scn, i, category, objtype);
if (!thisgeom) {
return;
}
mjtNum* from = con->pos;
mjtNum to[3];
mju_add3(to, from, vec);
mjv_connector(thisgeom,
bf[0] > 0 && bf[1] > 0 && !split ? mjGEOM_ARROW2 : mjGEOM_ARROW,
m->vis.scale.forcewidth * scl, from, to);
f2f(thisgeom->rgba, j == 2 ? m->vis.rgba.contactfriction : m->vis.rgba.contactforce, 4);
if (vopt->label == mjLABEL_CONTACTFORCE && j == (split ? 1 : 0)) {
mjSNPRINTF(thisgeom->label, "%-.3g", mju_norm3(frc));
}
releaseGeom(&thisgeom, scn);
}
}
}
}
static void addFlexGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, int catmask, mjvScene* scn) {
const int category = mjCAT_DYNAMIC;
if (!(category & catmask)) {
return;
}
if (!vopt->flags[mjVIS_FLEXVERT] && !vopt->flags[mjVIS_FLEXEDGE] &&
!vopt->flags[mjVIS_FLEXFACE] && !vopt->flags[mjVIS_FLEXSKIN]) {
return;
}
for (int i=0; i < m->nflex; i++) {
if (!vopt->flexgroup[mjMAX(0, mjMIN(mjNGROUP-1, m->flex_group[i]))]) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, category, mjOBJ_FLEX);
if (!thisgeom) {
return;
}
// construct geom, pos = first vertex
mjv_initGeom(thisgeom, mjGEOM_FLEX, NULL,
d->flexvert_xpos + 3*m->flex_vertadr[i], NULL, NULL);
thisgeom->size[0] = m->flex_radius[i];
setMaterial(m, thisgeom, m->flex_matid[i], m->flex_rgba+4*i, vopt->flags);
// override if visualizing islands
if (vopt->flags[mjVIS_ISLAND]) {
// find first dynamic body in flex
int bodyid = -1;
if (m->flex_interp[i]) {
int nodeadr = m->flex_nodeadr[i];
for (int j=0; j < m->flex_nodenum[i] && bodyid < 0; j++) {
int b = m->flex_nodebodyid[nodeadr+j];
if (m->body_treeid[b] >= 0) bodyid = b;
}
} else {
int vertadr = m->flex_vertadr[i];
for (int j=0; j < m->flex_vertnum[i] && bodyid < 0; j++) {
int b = m->flex_vertbodyid[vertadr+j];
if (m->body_treeid[b] >= 0) bodyid = b;
}
}
if (bodyid >= 0) {
// strip material
thisgeom->matid = -1;
int weld_id = m->body_weldid[bodyid];
int dof = m->body_dofadr[weld_id];
int island = d->nisland ? d->dof_island[dof] : -1;
int h = island >= 0 ? d->island_dofadr[island] : -1;
int awake = d->body_awake[bodyid];
// if sleep is enabled, color by first tree dof
if (h == -1 && mjENABLED(mjENBL_SLEEP)) {
int tree = m->dof_treeid[dof];
if (!awake) tree = mj_sleepCycle(d->tree_asleep, m->ntree, tree);
h = m->tree_dofadr[tree];
}
islandColor(thisgeom->rgba, h, awake);
}
}
// set texcoord
if (m->flex_texcoordadr[i] >= 0) {
thisgeom->texcoord = 1;
} else {
thisgeom->matid = -1;
}
// glow flex if selected
if (pert->flexselect == i) {
markselected(&m->vis, thisgeom);
}
// skip if alpha is 0
if (thisgeom->rgba[3] == 0) {
continue;
}
// vopt->label
if (vopt->label == mjLABEL_FLEX) {
makeLabel(m, mjOBJ_FLEX, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
static void addSkinGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, int catmask, mjvScene* scn) {
const int category = mjCAT_DYNAMIC;
if (!(category & catmask)) {
return;
}
if (!vopt->flags[mjVIS_SKIN]) {
return;
}
for (int i=0; i < m->nskin; i++) {
if (!vopt->skingroup[mjMAX(0, mjMIN(mjNGROUP-1, m->skin_group[i]))]) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, category, mjOBJ_SKIN);
if (!thisgeom) {
return;
}
// construct geom, pos = first bone
mjv_initGeom(thisgeom, mjGEOM_SKIN, NULL,
d->xpos + 3*m->skin_bonebodyid[m->skin_boneadr[i]], NULL, NULL);
// set material properties
setMaterial(m, thisgeom, m->skin_matid[i], m->skin_rgba+4*i, vopt->flags);
// glow skin if selected
if (pert->skinselect == i) {
markselected(&m->vis, thisgeom);
}
// set texcoord
if (m->skin_texcoordadr[i] >= 0) {
thisgeom->texcoord = 1;
}
// skip if alpha is 0
if (thisgeom->rgba[3] == 0) {
continue;
}
// vopt->label
if (vopt->label == mjLABEL_SKIN) {
makeLabel(m, mjOBJ_SKIN, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
static void addGeomGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, int catmask, mjvScene* scn) {
const int objtype = mjOBJ_GEOM;
int planeid = -1;
for (int i=0; i < m->ngeom; i++) {
// count planes, put current plane number in geom->dataid
if (m->geom_type[i] == mjGEOM_PLANE) {
planeid++;
}
// skip if category is masked
int category = bodycategory(m, m->geom_bodyid[i]);
if (!(category & catmask)) {
continue;
}
// skip if group is disabled
if (!vopt->geomgroup[mjMAX(0, mjMIN(mjNGROUP-1, m->geom_group[i]))]) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, category, objtype);
if (!thisgeom) {
return;
}
// construct geom
mjv_initGeom(thisgeom, m->geom_type[i], m->geom_size+3*i,
d->geom_xpos+3*i, d->geom_xmat+9*i, NULL);
thisgeom->dataid = m->geom_dataid[i];
// copy rbound from model
thisgeom->modelrbound = (float)m->geom_rbound[i];
// set material properties
float* rgba = m->geom_rgba+4*i;
int geom_matid = m->geom_matid[i];
setMaterial(m, thisgeom, geom_matid, rgba, vopt->flags);
// override if visualizing islands
if (vopt->flags[mjVIS_ISLAND]) {
int weld_id = m->body_weldid[m->geom_bodyid[i]];
if (m->body_dofnum[weld_id]) {
// strip materials off moving geom
thisgeom->matid = -1;
// set hue using first island dof, -1 if no island
int dof = m->body_dofadr[weld_id];
int island = d->nisland ? d->dof_island[dof] : -1;
int h = island >= 0 ? d->island_dofadr[island] : -1;
int awake = d->body_awake[m->geom_bodyid[i]];
// if sleep is enabled, color by first tree dof
if (h == -1 && mjENABLED(mjENBL_SLEEP)) {
int tree = m->dof_treeid[dof];
if (!awake) tree = mj_sleepCycle(d->tree_asleep, m->ntree, tree);
h = m->tree_dofadr[tree];
}
islandColor(thisgeom->rgba, h, awake);
}
}
// set texcoord
if ((m->geom_type[i] == mjGEOM_MESH || m->geom_type[i] == mjGEOM_SDF) &&
m->geom_dataid[i] >= 0 &&
m->mesh_texcoordadr[m->geom_dataid[i]] >= 0) {
thisgeom->texcoord = 1;
}
// skip if alpha is 0
if (thisgeom->rgba[3] == 0) {
continue;
}
// glow geoms of selected body
if (pert->select > 0 && pert->select == m->geom_bodyid[i]) {
markselected(&m->vis, thisgeom);
}
// vopt->label
if (vopt->label == mjLABEL_GEOM) {
makeLabel(m, mjOBJ_GEOM, i, thisgeom->label);
}
// mesh: 2*i is original, 2*i+1 is convex hull
if (m->geom_type[i] == mjGEOM_MESH || m->geom_type[i] == mjGEOM_SDF) {
thisgeom->dataid *= 2;
if (m->mesh_graphadr[m->geom_dataid[i]] >= 0 && vopt->flags[mjVIS_CONVEXHULL] &&
(m->geom_contype[i] || m->geom_conaffinity[i])) {
thisgeom->dataid += 1;
}
}
// plane
else if (m->geom_type[i] == mjGEOM_PLANE) {
// use current planeid
thisgeom->dataid = planeid;
// save initial pos
mjtNum tmp[9];
mju_copy3(tmp, d->geom_xpos+3*i);
// re-center infinite plane
if (m->geom_size[3*i] <= 0 || m->geom_size[3*i+1] <= 0) {
// vec = headpos - geompos
mjtNum vec[3];
for (int j=0; j < 3; j++) {
vec[j] = 0.5*(scn->camera[0].pos[j] + scn->camera[1].pos[j]) - d->geom_xpos[3*i+j];
}
// construct axes
mjtNum ax[9];
mju_transpose(ax, d->geom_xmat+9*i, 3, 3);
// loop over (x,y)
for (int k=0; k < 2; k++) {
if (m->geom_size[3*i+k] <= 0) {
// compute zfar
mjtNum zfar = m->vis.map.zfar * m->stat.extent;
// get size increment
mjtNum sX;
int matid = m->geom_matid[i];
if (matid >= 0 && m->mat_texrepeat[2*matid+k] > 0) {
sX = 2/m->mat_texrepeat[2*matid+k];
} else {
sX = 2.1*zfar/(mjMAXPLANEGRID-2);
}
// project on frame, round to integer increment of size
mjtNum dX = mju_dot3(vec, ax+3*k);
dX = 2*sX*mju_round(0.5*dX/sX);
// translate
mju_addToScl3(tmp, ax+3*k, dX);
}
}
}
// set final pos
mju_n2f(thisgeom->pos, tmp, 3);
}
releaseGeom(&thisgeom, scn);
}
}
static void addSiteGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, int catmask, mjvScene* scn) {
for (int i=0; i < m->nsite; i++) {
// skip if category is masked
int category = bodycategory(m, m->site_bodyid[i]);
if (!(category & catmask)) {
continue;
}
// skip if group disabled
if (!vopt->sitegroup[mjMAX(0, mjMIN(mjNGROUP-1, m->site_group[i]))]) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, category, mjOBJ_SITE);
if (!thisgeom) {
return;
}
// construct geom
mjv_initGeom(thisgeom, m->site_type[i], m->site_size+3*i,
d->site_xpos+3*i, d->site_xmat+9*i, NULL);
// set material if given
setMaterial(m, thisgeom, m->site_matid[i], m->site_rgba+4*i, vopt->flags);
// skip if alpha is 0
if (thisgeom->rgba[3] == 0) {
continue;
}
// glow
if (pert->select > 0 && pert->select == m->site_bodyid[i]) {
markselected(&m->vis, thisgeom);
}
// vopt->label
if (vopt->label == mjLABEL_SITE) {
makeLabel(m, mjOBJ_SITE, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
int mjv_isCatenary(const mjModel* m, const mjData* d, int i, mjtNum* length) {
int has_stiffness = m->tendon_stiffness[i] ||
!mju_isZero(m->tendon_stiffnesspoly+mjNPOLY*i, mjNPOLY);
// tendon has a deadband spring
int limitedspring =
has_stiffness && // positive stiffness
m->tendon_lengthspring[2*i] == 0 && // range lower-bound is 0
m->tendon_lengthspring[2*i+1] > 0; // range upper-bound is positive
// tendon has a simple length constraint, but is currently not limited
mjtNum ten_length = d->ten_length[i];
mjtNum lower = m->tendon_range[2*i];
mjtNum upper = m->tendon_range[2*i + 1];
int limitedconstraint =
!has_stiffness && // zero stiffness
m->tendon_limited[i] == 1 && // limited length range
lower == 0 && // range lower-bound is 0
ten_length < upper; // current length is smaller than upper bound
int has_damping = m->tendon_damping[i] || !mju_isZero(m->tendon_dampingpoly+mjNPOLY*i, mjNPOLY);
// conditions for drawing a catenary
int draw_catenary =
!mjDISABLED(mjDSBL_GRAVITY) && // gravity enabled
mju_norm3(m->opt.gravity) > mjMINVAL && // gravity strictly nonzero
m->tendon_num[i] == 2 && // only two sites on the tendon
(limitedspring != limitedconstraint) && // either spring or constraint length limits
!has_damping && // no damping
m->tendon_frictionloss[i] == 0; // no frictionloss
// no actuator
if (draw_catenary) {
for (int j=0; j < m->nactuator; j++) {
if (m->actuator_trntype[j] == mjTRN_TENDON && m->actuator_trnid[2*j] == i) {
draw_catenary = 0;
break;
}
}
}
if (draw_catenary) {
// length of the tendon
if (limitedconstraint) {
*length = m->tendon_range[2*i+1];
} else {
*length = m->tendon_lengthspring[2*i+1];
}
}
return draw_catenary;
}
static void addSpatialTendonGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, int catmask,
mjvScene* scn) {
const int category = mjCAT_DYNAMIC;
if (!(category & catmask)) {
return;
}
if (!vopt->flags[mjVIS_TENDON]) {
return;
}
for (int i=0; i < m->ntendon; i++) {
if (!vopt->tendongroup[mjMAX(0, mjMIN(mjNGROUP-1, m->tendon_group[i]))]) {
continue;
}
mjtNum length;
int draw_catenary = mjv_isCatenary(m, d, i, &length);
// conditions not met: draw straight lines
if (!draw_catenary) {
for (int j=d->ten_wrapadr[i]; j < d->ten_wrapadr[i]+d->ten_wrapnum[i]-1; j++) {
if (d->wrap_obj[j] != -2 && d->wrap_obj[j+1] != -2) {
mjvGeom* thisgeom = acquireGeom(scn, i, category, mjOBJ_TENDON);
if (!thisgeom) {
return;
}
// determine width: smaller for segments inside wrapping objects
mjtNum width;
if (d->wrap_obj[j] >= 0 && d->wrap_obj[j+1] >= 0) {
width = 0.5 * m->tendon_width[i];
} else {
width = m->tendon_width[i];
}
// construct geom
mjv_connector(thisgeom, mjGEOM_CAPSULE, width, d->wrap_xpos+3*j, d->wrap_xpos+3*j+3);
// set material properties
int tendon_matid = m->tendon_matid[i];
float rgba[4];
f2f(rgba, m->tendon_rgba+4*i, 4);
// if tendon has no material and the color is the default gray, re-color it using limit impedance
if (tendon_matid == -1 && rgba[0] == 0.5 && rgba[1] == 0.5 && rgba[2] == 0.5 && rgba[3] == 1) {
// loop over limit constraints, get impedance if this tendon is limited
mjtNum imp = 0;
int efc_start = d->ne + d->nf;
int efc_end = efc_start + d->nl;
for (int k=efc_start; k < efc_end; k++) {
if (d->efc_type[k] == mjCNSTR_LIMIT_TENDON && d->efc_id[k] == i) {
imp = d->efc_KBIP[4*k + 2];
}
}
// use impedance to mix tendon and constraint colors
rgba[0] = (1-imp) * rgba[0] + imp * m->vis.rgba.constraint[0];
rgba[1] = (1-imp) * rgba[1] + imp * m->vis.rgba.constraint[1];
rgba[2] = (1-imp) * rgba[2] + imp * m->vis.rgba.constraint[2];
}
setMaterial(m, thisgeom, tendon_matid, rgba, vopt->flags);
// override if visualizing islands
if (vopt->flags[mjVIS_ISLAND]) {
// strip material
thisgeom->matid = -1;
// set hue with first island dof, if constrained
int h = -1;
if (d->nisland && d->tendon_efcadr[i] >= 0) {
h = d->island_dofadr[d->efc_island[d->tendon_efcadr[i]]];
}
islandColor(thisgeom->rgba, h, 1);
}
// vopt->label: only the first segment
if (vopt->label == mjLABEL_TENDON && j == d->ten_wrapadr[i]) {
makeLabel(m, mjOBJ_TENDON, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
}
// special case handling of string-like tendons under gravity
else {
// two hanging points: x0, x1
mjtNum x0[3], x1[3];
mju_copy3(x0, d->wrap_xpos + 3*d->ten_wrapadr[i]);
mju_copy3(x1, d->wrap_xpos + 3*d->ten_wrapadr[i] + 3);
// get number of points along catenary path (capped at 100)
int ncatenary = mjMIN(m->vis.quality.numslices + 1, 100);
mjtNum catenary[300];
// points along catenary path
int npoints = mjv_catenary(x0, x1, m->opt.gravity, length, catenary, ncatenary);
// draw npoints-1 segments
for (int j=0; j < npoints-1; j++) {
mjvGeom* thisgeom = acquireGeom(scn, i, category, mjOBJ_TENDON);
if (!thisgeom) {
return;
}
// construct geom
mjv_connector(thisgeom, mjGEOM_CAPSULE, m->tendon_width[i], catenary+3*j, catenary+3*j+3);
// set material if given
setMaterial(m, thisgeom, m->tendon_matid[i], m->tendon_rgba+4*i, vopt->flags);
// vopt->label: only the first segment
if (vopt->label == mjLABEL_TENDON && npoints/2) {
makeLabel(m, mjOBJ_TENDON, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
}
}
static void addSliderCrankGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, int catmask,
mjvScene* scn) {
const int category = mjCAT_DYNAMIC;
if (!(category & catmask)) {
return;
}
const float scl = m->stat.meansize;
for (int i=0; i < m->nactuator; i++) {
if (m->actuator_trntype[i] == mjTRN_SLIDERCRANK) {
// get data
int j = m->actuator_trnid[2*i]; // crank
int k = m->actuator_trnid[2*i+1]; // slider
mjtNum rod = m->actuator_cranklength[i];
mjtNum axis[3];
axis[0] = d->site_xmat[9*k+2];
axis[1] = d->site_xmat[9*k+5];
axis[2] = d->site_xmat[9*k+8];
// compute crank length
mjtNum vec[3];
mju_sub(vec, d->site_xpos+3*j, d->site_xpos+3*k, 3);
mjtNum len = mju_dot3(vec, axis);
mjtNum det = len*len + rod*rod - mju_dot3(vec, vec);
mjtByte broken = 0;
if (det < 0) {
det = 0;
broken = 1;
}
len = len - mju_sqrt(det);
// compute slider endpoint
mjtNum end[3];
mju_scl3(end, axis, len);
mju_addTo3(end, d->site_xpos+3*k);
// render slider
mjvGeom* thisgeom = acquireGeom(scn, i, category, mjOBJ_ACTUATOR);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, mjGEOM_CYLINDER, scl * m->vis.scale.slidercrank,
d->site_xpos+3*k, end);
f2f(thisgeom->rgba, m->vis.rgba.slidercrank, 4);
if (vopt->label == mjLABEL_ACTUATOR) {
makeLabel(m, mjOBJ_ACTUATOR, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
thisgeom = acquireGeom(scn, i, category, mjOBJ_ACTUATOR);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, mjGEOM_CAPSULE, scl * m->vis.scale.slidercrank/2.0,
end, d->site_xpos+3*j);
if (broken) {
f2f(thisgeom->rgba, m->vis.rgba.crankbroken, 4);
} else {
f2f(thisgeom->rgba, m->vis.rgba.slidercrank, 4);
}
releaseGeom(&thisgeom, scn);
}
}
}
static void addGeomFrameGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, int catmask,
mjvScene* scn) {
if (vopt->frame != mjFRAME_GEOM) {
return;
}
const float scl = m->stat.meansize;
for (int i=0; i < m->ngeom; i++) {
if (!(bodycategory(m, m->geom_bodyid[i]) & catmask)) {
continue;
}
if (!vopt->geomgroup[mjMAX(0, mjMIN(mjNGROUP-1, m->geom_group[i]))]) {
continue;
}
// base element is invisible; don't show decors
int matid = m->geom_matid[i];
float* rgba = (matid >= 0) ? (m->mat_rgba + 4*matid) : (m->geom_rgba + 4*i);
if (rgba[3] == 0) {
continue;
}
// construct geom frame
mjtNum width = m->vis.scale.framewidth * scl;
mjtNum length = m->vis.scale.framelength * scl;
addFrame(scn, i, d->geom_xpos+3*i, d->geom_xmat+9*i, length, width);
}
}
static void addSiteFrameGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, int catmask,
mjvScene* scn) {
if (vopt->frame != mjFRAME_SITE) {
return;
}
const float scl = m->stat.meansize;
for (int i=0; i < m->nsite; i++) {
if (!(bodycategory(m, m->site_bodyid[i]) & catmask)) {
continue;
}
if (!vopt->sitegroup[mjMAX(0, mjMIN(mjNGROUP-1, m->site_group[i]))]) {
continue;
}
// base element is invisible; don't show decors
int matid = m->site_matid[i];
float* rgba = (matid >= 0) ? (m->mat_rgba + 4*matid) : (m->site_rgba + 4*i);
if (rgba[3] == 0) {
continue;
}
// construct site frame
mjtNum width = m->vis.scale.framewidth * scl;
mjtNum length = m->vis.scale.framelength * scl;
addFrame(scn, i, d->site_xpos+3*i, d->site_xmat+9*i, length, width);
}
}
static void addBodyBvhGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_BODYBVH]) {
return;
}
for (int i = 0; i < m->nbvhstatic; i++) {
int isleaf = m->bvh_child[2*i] == -1 && m->bvh_child[2*i+1] == -1;
if (m->bvh_depth[i] != vopt->bvh_depth) {
if (!isleaf || m->bvh_depth[i] > vopt->bvh_depth) {
continue;
}
}
// find geom number
int bodyid = 0;
int geomid = m->bvh_nodeid[i];
while (i >= m->body_bvhadr[bodyid] + m->body_bvhnum[bodyid]) {
if (++bodyid >= m->nbody) {
break;
}
}
// stop after body bvh are finished
if (bodyid >= m->nbody) {
break;
}
// get xpos, xmat, size
const mjtNum* xpos = isleaf ? d->geom_xpos + 3 * geomid : d->xipos + 3 * bodyid;
const mjtNum* xmat = isleaf ? d->geom_xmat + 9 * geomid : d->ximat + 9 * bodyid;
const mjtNum* size = isleaf ? m->geom_aabb + 6*geomid + 3 : m->bvh_aabb + 6*i + 3;
// offset xpos with aabb center (not always at frame origin)
const mjtNum* center = isleaf ? m->geom_aabb + 6*geomid : m->bvh_aabb + 6*i;
mjtNum pos[3];
mju_mulMatVec3(pos, xmat, center);
mju_addTo3(pos, xpos);
// set box color
const float* rgba = m->vis.rgba.bv;
if (m->vis.global.bvactive && d->bvh_active[i]) {
rgba = m->vis.rgba.bvactive;
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
mjv_initGeom(thisgeom, mjGEOM_LINEBOX, size, pos, xmat, rgba);
releaseGeom(&thisgeom, scn);
}
}
static void addFlexBvhGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_MESHBVH]) {
return;
}
for (int f=0; f < m->nflex; f++) {
if (m->flex_bvhnum[f] && vopt->flexgroup[mjMAX(0, mjMIN(mjNGROUP-1, m->flex_group[f]))]) {
for (int i=m->flex_bvhadr[f]; i < m->flex_bvhadr[f]+m->flex_bvhnum[f]; i++) {
int isleaf = m->bvh_child[2*i] == -1 && m->bvh_child[2*i+1] == -1;
if (m->bvh_depth[i] != vopt->bvh_depth) {
if (!isleaf || m->bvh_depth[i] > vopt->bvh_depth) {
continue;
}
}
// get box data
mjtNum* aabb = d->bvh_aabb_dyn + 6*(i - m->nbvhstatic);
// set box color
const float* rgba = m->vis.rgba.bv;
if (m->vis.global.bvactive && d->bvh_active[i]) {
rgba = m->vis.rgba.bvactive;
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
mjv_initGeom(thisgeom, mjGEOM_LINEBOX, aabb+3, aabb, NULL, rgba);
releaseGeom(&thisgeom, scn);
}
}
if (!m->flex_interp[f]) {
continue;
}
// control points box
mjtNum* xpos = mjSTACKALLOC(d, 3*m->flex_nodenum[f], mjtNum);
int nstart = m->flex_nodeadr[f];
int* bodyid = m->flex_nodebodyid + m->flex_nodeadr[f];
if (m->flex_centered[f]) {
for (int i=0; i < m->flex_nodenum[f]; i++) {
mju_copy3(xpos + 3*i, d->xpos + 3*bodyid[i]);
}
} else {
for (int i=0; i < m->flex_nodenum[f]; i++) {
mju_mulMatVec3(xpos + 3*i, d->xmat + 9*bodyid[i], m->flex_node + 3*(i+nstart));
mju_addTo3(xpos + 3*i, d->xpos + 3*bodyid[i]);
}
}
int cx = m->flex_cellnum[3*f+0];
int cy = m->flex_cellnum[3*f+1];
int cz = m->flex_cellnum[3*f+2];
int order = m->flex_interp[f];
order = order < 0 ? -order : order;
int NX = cx * order + 1;
int NY = cy * order + 1;
int NZ = cz * order + 1;
int shell_mode = m->flex_interp[f] < 0;
for (int i=0; i < NX; i++) {
for (int j=0; j < NY; j++) {
for (int k=0; k < NZ; k++) {
int n0 = i*NY*NZ + j*NZ + k;
// skip if this node is pinned (no joints on its body)
if (m->body_jntnum[bodyid[n0]] == 0) {
continue;
}
// shell mode: skip interior nodes entirely
int is_boundary = (i == 0 || i == NX-1 ||
j == 0 || j == NY-1 ||
k == 0 || k == NZ-1);
if (shell_mode && !is_boundary) {
continue;
}
int offset = 3*n0;
int offset1 = 3*((i+1)*NY*NZ + j*NZ + k);
int offset2 = 3*(i*NY*NZ + (j+1)*NZ + k);
int offset3 = 3*(i*NY*NZ + j*NZ + (k+1));
// edge along i: draw if neighbor is also on boundary (shell) or has joints
if (i < NX-1 && m->body_jntnum[bodyid[(i+1)*NY*NZ + j*NZ + k]] > 0) {
int nb_boundary = ((i+1) == 0 || (i+1) == NX-1 ||
j == 0 || j == NY-1 ||
k == 0 || k == NZ-1);
if (!shell_mode || nb_boundary) {
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, mjGEOM_LINE, 3, xpos+offset, xpos+offset1);
releaseGeom(&thisgeom, scn);
}
}
if (j < NY-1 && m->body_jntnum[bodyid[i*NY*NZ + (j+1)*NZ + k]] > 0) {
int nb_boundary = (i == 0 || i == NX-1 ||
(j+1) == 0 || (j+1) == NY-1 ||
k == 0 || k == NZ-1);
if (!shell_mode || nb_boundary) {
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, mjGEOM_LINE, 3, xpos+offset, xpos+offset2);
releaseGeom(&thisgeom, scn);
}
}
if (k < NZ-1 && m->body_jntnum[bodyid[i*NY*NZ + j*NZ + (k+1)]] > 0) {
int nb_boundary = (i == 0 || i == NX-1 ||
j == 0 || j == NY-1 ||
(k+1) == 0 || (k+1) == NZ-1);
if (!shell_mode || nb_boundary) {
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
mjv_connector(thisgeom, mjGEOM_LINE, 3, xpos+offset, xpos+offset3);
releaseGeom(&thisgeom, scn);
}
}
}
}
}
}
}
static void addMeshBvhGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_MESHBVH]) {
return;
}
for (int geomid = 0; geomid < m->ngeom; geomid++) {
int meshid = m->geom_dataid[geomid];
// skip if not a mesh or if there is an octree
if (meshid == -1 || m->geom_type[geomid] == mjGEOM_SDF || m->mesh_octadr[meshid] >= 0) {
continue;
}
for (int b = 0; b < m->mesh_bvhnum[meshid]; b++) {
int i = b + m->mesh_bvhadr[meshid];
int isleaf = m->bvh_child[2*i] == -1 && m->bvh_child[2*i+1] == -1;
if (m->bvh_depth[i] != vopt->bvh_depth) {
if (!isleaf || m->bvh_depth[i] > vopt->bvh_depth) {
continue;
}
}
// box color
const float* rgba = m->vis.rgba.bv;
if (m->vis.global.bvactive) {
if (d->bvh_active[i]) {
rgba = m->vis.rgba.bvactive;
} else {
// when marking active bvs, skip inactive volumes
continue;
}
}
// get xpos, xmat, size
const mjtNum* xpos = d->geom_xpos + 3 * geomid;
const mjtNum* xmat = d->geom_xmat + 9 * geomid;
const mjtNum* size = m->bvh_aabb + 6*i + 3;
// offset xpos with aabb center (not always at geom origin)
const mjtNum* center = m->bvh_aabb + 6*i;
mjtNum pos[3];
mju_mulMatVec3(pos, xmat, center);
mju_addTo3(pos, xpos);
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
mjv_initGeom(thisgeom, mjGEOM_LINEBOX, size, pos, xmat, rgba);
releaseGeom(&thisgeom, scn);
}
}
}
static void addMeshOctreeGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_MESHBVH]) {
return;
}
for (int geomid = 0; geomid < m->ngeom; geomid++) {
int meshid = m->geom_dataid[geomid];
if (meshid == -1 || m->geom_type[geomid] == mjGEOM_HFIELD || m->mesh_octadr[meshid] == -1) {
continue;
}
for (int b = 0; b < m->mesh_octnum[meshid]; b++) {
int i = b + m->mesh_octadr[meshid];
if (m->oct_depth[i] != vopt->bvh_depth) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
const float* rgba = m->vis.rgba.bv;
const mjtNum* xpos = d->geom_xpos + 3 * geomid;
const mjtNum* xmat = d->geom_xmat + 9 * geomid;
const mjtNum* size = m->oct_aabb + 6*i + 3;
// offset xpos with aabb center (not always at geom origin)
const mjtNum* center = m->oct_aabb + 6*i;
mjtNum pos[3];
mju_mulMatVec3(pos, xmat, center);
mju_addTo3(pos, xpos);
mjv_initGeom(thisgeom, mjGEOM_LINEBOX, size, pos, xmat, rgba);
releaseGeom(&thisgeom, scn);
}
}
}
static void addTactileSensorGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
mjvScene* scn) {
if (!vopt->flags[mjVIS_CONTACTPOINT]) {
return;
}
for (int id = 0; id < m->nsensor; id++) {
if (m->sensor_type[id] == mjSENS_TACTILE) {
// get site id and frame
int mesh_id = m->sensor_objid[id];
int geom_id = m->sensor_refid[id];
mjtNum* geom_pos = d->geom_xpos + 3*geom_id;
mjtNum* geom_mat = d->geom_xmat + 9*geom_id;
mjtNum geom_quat[4];
mju_mat2Quat(geom_quat, geom_mat);
// get sensor data
mjtNum* sensordata = d->sensordata + m->sensor_adr[id];
int nchannel = m->sensor_dim[id] / m->mesh_vertnum[mesh_id];
// get maximum absolute normal force
mjtNum maxval = 0;
for (int j=0; j < m->mesh_vertnum[mesh_id]; j++) {
maxval = mju_max(maxval, mju_abs(sensordata[j]));
}
// if no normal force readings, quick return
if (!maxval || m->geom_rbound[geom_id] < mjMINVAL) {
continue;
}
// draw geoms
float* mesh_vert = m->mesh_vert + 3*m->mesh_vertadr[mesh_id];
int* face = m->mesh_face + 3*m->mesh_faceadr[mesh_id];
for (int i=0; i < m->mesh_facenum[mesh_id]; i++) {
// triangle in global frame
mjtNum pos[3][3];
for (int j = 0; j < 3; j++) {
mjtNum v[3] = {mesh_vert[3 * face[3 * i + j] + 0],
mesh_vert[3 * face[3 * i + j] + 1],
mesh_vert[3 * face[3 * i + j] + 2]};
mju_mulMatVec3(pos[j], geom_mat, v);
mju_addTo3(pos[j], geom_pos);
}
// color
float rgba[4] = {0, 0, 0, 1.0};
mjtNum nval[3] = {0, 0, 0};
for (int r = 0; r < mjMIN(nchannel, 3); r++) {
for (int j = 0; j < 3; j++) {
mjtNum val = sensordata[r*m->mesh_vertnum[mesh_id] + face[3*i+j]];
rgba[r] += mju_abs(val) / maxval;
if (val) {
nval[r] += 1;
}
}
if (nval[r]) {
rgba[r] /= nval[r];
}
}
if (rgba[0]==0 && rgba[1]==0 && rgba[2]==0) {
rgba[3] = .1;
}
// draw triangles, one per side
addTriangle(scn, pos[0], pos[1], pos[2], rgba, id, mjCAT_DECOR, mjOBJ_SENSOR);
}
}
}
}
static void addInertiaGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, int catmask, mjvScene* scn) {
if (!vopt->flags[mjVIS_INERTIA]) {
return;
}
int ellipsoid = m->vis.global.ellipsoidinertia == 1;
for (int i=1; i < m->nbody; i++) {
if (m->body_mass[i] <= mjMINVAL) {
continue;
}
if (!(bodycategory(m, i) & catmask)) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_BODY);
if (!thisgeom) {
return;
}
mjtNum Ixx = m->body_inertia[3*i+0];
mjtNum Iyy = m->body_inertia[3*i+1];
mjtNum Izz = m->body_inertia[3*i+2];
mjtNum mass = m->body_mass[i];
mjtNum scale_inertia = ellipsoid ? mju_sqrt(5) : mju_sqrt(3);
mjtNum sz[3];
sz[0] = mju_sqrt((Iyy + Izz - Ixx) / (2 * mass)) * scale_inertia;
sz[1] = mju_sqrt((Ixx + Izz - Iyy) / (2 * mass)) * scale_inertia;
sz[2] = mju_sqrt((Ixx + Iyy - Izz) / (2 * mass)) * scale_inertia;
// scale with mass if enabled
if (vopt->flags[mjVIS_SCLINERTIA]) {
// density = mass / volume
mjtNum scale_volume = ellipsoid ? 4.0/3.0*mjPI : 8.0;
mjtNum volume = scale_volume * sz[0]*sz[1]*sz[2];
mjtNum density = mass / mju_max(mjMINVAL, volume);
// scale = root3(density)
mjtNum scale = mju_pow(density*0.001, 1.0/3.0);
// scale sizes, so that box/ellipsoid with density of 1000 has same mass
sz[0] *= scale;
sz[1] *= scale;
sz[2] *= scale;
}
// construct geom
mjtGeom type = ellipsoid ? mjGEOM_ELLIPSOID : mjGEOM_BOX;
mjv_initGeom(thisgeom, type, sz, d->xipos+3*i, d->ximat+9*i, m->vis.rgba.inertia);
// glow
if (pert->select == i) {
markselected(&m->vis, thisgeom);
}
// vopt->label
if (vopt->label == mjLABEL_BODY ||
(vopt->label == mjLABEL_SELECTION && pert->select == i)) {
makeLabel(m, mjOBJ_BODY, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
static void addPerturbGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, mjvScene* scn) {
if (!vopt->flags[mjVIS_PERTOBJ]) {
return;
}
if (pert->select <= 0) {
return;
}
const float scl = m->stat.meansize;
if ((pert->active | pert->active2) & mjPERT_TRANSLATE) {
mjvGeom* thisgeom = acquireGeom(scn, pert->select, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
// compute selection point in world coordinates
mjtNum selpos[3];
mju_mulMatVec3(selpos, d->xmat+9*pert->select, pert->localpos);
mju_addTo3(selpos, d->xpos+3*pert->select);
// construct geom
mjtNum sz[3];
sz[0] = scl * m->vis.scale.constraint;
mjv_connector(thisgeom, mjGEOM_CAPSULE, sz[0], selpos, pert->refselpos);
// prepare color
float rgba[4];
mixcolor(rgba, m->vis.rgba.constraint,
(pert->active & mjPERT_TRANSLATE) > 0,
(pert->active2 & mjPERT_TRANSLATE) > 0);
f2f(thisgeom->rgba, rgba, 4);
releaseGeom(&thisgeom, scn);
// add small sphere at end-effector
thisgeom = acquireGeom(scn, pert->select, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
// construct geom
sz[0] = 2*sz[0];
sz[1] = sz[2] = sz[0];
mjtNum mat[9];
mju_quat2Mat(mat, pert->refquat);
mjv_initGeom(thisgeom, mjGEOM_SPHERE, sz, pert->refselpos, mat, rgba);
releaseGeom(&thisgeom, scn);
}
if ((pert->active | pert->active2) & mjPERT_ROTATE) {
mjvGeom* thisgeom = acquireGeom(scn, pert->select, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
// prepare color, use inertia color
float rgba[4];
mixcolor(rgba, m->vis.rgba.inertia,
(pert->active & mjPERT_ROTATE) > 0,
(pert->active2 & mjPERT_ROTATE) > 0);
// construct geom: if body i has a collision aabb, use that
mjtNum pos[3] = {0};
mjtNum sz[3];
if (m->body_bvhnum[pert->select]) {
mjtNum* aabb = m->bvh_aabb+6*m->body_bvhadr[pert->select];
mju_copy3(sz, aabb+3);
mju_mulMatVec3(pos, d->ximat+9*pert->select, aabb);
}
// otherwise box of size meansize
else {
sz[0] = sz[1] = sz[2] = scl;
}
mjtNum mat[9];
mju_quat2Mat(mat, pert->refquat);
mju_addTo3(pos, d->xipos+3*pert->select);
mjv_initGeom(thisgeom, mjGEOM_LINEBOX, sz, pos, mat, rgba);
releaseGeom(&thisgeom, scn);
}
}
static void addWorldBodyFrameGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, int catmask,
mjvScene* scn) {
const float scl = m->stat.meansize;
for (int i = (vopt->frame == mjFRAME_WORLD ? 0 : 1);
i < (vopt->frame == mjFRAME_BODY ? m->nbody : 1);
i++) {
// skip if body is static and static bodies are masked
if (i > 0 && bodycategory(m, i) & ~catmask) {
continue;
}
// set length(1) and width(0) of the axis cylinders
mjtNum sz[2];
if (i == 0) {
sz[1] = m->vis.scale.framelength * scl * 2;
sz[0] = m->vis.scale.framewidth * scl * 2;
} else {
sz[1] = m->vis.scale.framelength * scl;
sz[0] = m->vis.scale.framewidth * scl;
}
mjtNum* xmat = vopt->flags[mjVIS_INERTIA] ? d->ximat+9*i : d->xmat+9*i;
mjtNum* xpos = vopt->flags[mjVIS_INERTIA] ? d->xipos+3*i : d->xpos+3*i;
addFrame(scn, i, xpos, xmat, sz[1], sz[0]);
}
}
static void addSelectionPointGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, mjvScene* scn) {
if (pert->select <= 0) {
return;
}
if (!vopt->flags[mjVIS_SELECT]) {
return;
}
const float scl = m->stat.meansize;
// compute selection point in world coordinates
mjtNum selpos[3];
mju_mulMatVec3(selpos, d->xmat+9*pert->select, pert->localpos);
mju_addTo3(selpos, d->xpos+3*pert->select);
mjvGeom* thisgeom = acquireGeom(scn, pert->select, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
thisgeom->type = mjGEOM_SPHERE;
thisgeom->size[0] = thisgeom->size[1] = thisgeom->size[2] = scl * m->vis.scale.selectpoint;
mju_n2f(thisgeom->pos, selpos, 3);
mju_n2f(thisgeom->mat, IDENTITY, 9);
f2f(thisgeom->rgba, m->vis.rgba.selectpoint, 4);
if (vopt->label == mjLABEL_SELPNT) {
mjSNPRINTF(
thisgeom->label, "%.3f %.3f %.3f (local %.3f %.3f %.3f)",
selpos[0], selpos[1], selpos[2],
pert->localpos[0], pert->localpos[1], pert->localpos[2]);
}
releaseGeom(&thisgeom, scn);
}
static void addBodyLabelGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, int catmask, mjvScene* scn) {
if (vopt->flags[mjVIS_INERTIA]) {
return;
}
if (vopt->label != mjLABEL_SELECTION && vopt->label != mjLABEL_BODY) {
return;
}
for (int i=1; i < m->nbody; i++) {
if (vopt->label == mjLABEL_SELECTION && pert->select != i) {
continue;
}
if (bodycategory(m, i) & ~catmask) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
thisgeom->type = mjGEOM_LABEL;
mju_n2f(thisgeom->pos, d->xpos+3*i, 3);
mju_n2f(thisgeom->mat, d->xmat+9*i, 9);
makeLabel(m, mjOBJ_BODY, i, thisgeom->label);
releaseGeom(&thisgeom, scn);
}
}
static void addJointGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_JOINT]) {
return;
}
const float scl = m->stat.meansize;
for (int i=0; i < m->njnt; i++) {
if (!vopt->jointgroup[mjMAX(0, mjMIN(mjNGROUP-1, m->jnt_group[i]))]) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_JOINT);
if (!thisgeom) {
return;
}
// set sz = {width, length} of the connectors
mjtNum sz[2];
sz[1] = m->vis.scale.jointlength * scl;
sz[0] = m->vis.scale.jointwidth * scl;
// set type, size, pos, mat depending on joint type
int j = m->jnt_bodyid[i];
mjtNum* from;
mjtNum to[3];
switch ((mjtJoint) m->jnt_type[i]) {
case mjJNT_FREE:
thisgeom->type = mjGEOM_BOX;
thisgeom->size[0] = thisgeom->size[1] = thisgeom->size[2] = 0.3*sz[1];
mju_n2f(thisgeom->pos, d->xanchor+3*i, 3);
mju_n2f(thisgeom->mat, d->xmat+9*j, 9);
break;
case mjJNT_BALL:
thisgeom->type = mjGEOM_SPHERE;
thisgeom->size[0] = thisgeom->size[1] = thisgeom->size[2] = 0.3*sz[1];
mju_n2f(thisgeom->pos, d->xanchor+3*i, 3);
mju_n2f(thisgeom->mat, d->xmat+9*j, 9);
break;
case mjJNT_SLIDE:
case mjJNT_HINGE:
from = d->xanchor+3*i;
mju_addScl3(to, from, d->xaxis+3*i, sz[1]);
mjv_connector(thisgeom, m->jnt_type[i] == mjJNT_SLIDE ? mjGEOM_ARROW : mjGEOM_ARROW1,
sz[0], from, to);
break;
default:
mjERROR("unknown joint type %d", m->jnt_type[i]);
}
// loop over limit constraints, get impedance if this joint is limited
mjtNum imp = 0;
int efc_start = d->ne + d->nf;
int efc_end = efc_start + d->nl;
for (int k=efc_start; k < efc_end; k++) {
if (d->efc_type[k] == mjCNSTR_LIMIT_JOINT && d->efc_id[k] == i) {
imp = d->efc_KBIP[4*k + 2];
}
}
// use impedance to mix joint and constraint colors
float rgba[4];
rgba[0] = (1-imp) * m->vis.rgba.joint[0] + imp * m->vis.rgba.constraint[0];
rgba[1] = (1-imp) * m->vis.rgba.joint[1] + imp * m->vis.rgba.constraint[1];
rgba[2] = (1-imp) * m->vis.rgba.joint[2] + imp * m->vis.rgba.constraint[2];
rgba[3] = 1;
f2f(thisgeom->rgba, rgba, 4);
// vopt->label
if (vopt->label == mjLABEL_JOINT) {
makeLabel(m, mjOBJ_JOINT, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
static void addActuatorGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_ACTUATOR]) {
return;
}
const float scl = m->stat.meansize;
for (int i=0; i < m->nactuator; i++) {
if (!vopt->actuatorgroup[mjMAX(0, mjMIN(mjNGROUP-1, m->actuator_group[i]))]) {
continue;
}
if (mj_actuatorDisabled(m, i)) {
continue;
}
// determine extended range
mjtNum rng[3] = {-1, 0, +1};
mjtNum rmin = -1, rmax = 1, act = 0;
if (m->actuator_ctrllimited[m->actuator_ctrladr[i]]) {
rmin = m->actuator_ctrlrange[2*m->actuator_ctrladr[i]];
rmax = m->actuator_ctrlrange[2*m->actuator_ctrladr[i]+1];
} else if (vopt->flags[mjVIS_ACTIVATION] && m->actuator_actlimited[i]) {
rmin = m->actuator_actrange[2*i];
rmax = m->actuator_actrange[2*i+1];
}
if (rmin >= 0) {
rng[0] = -1;
rng[1] = rmin;
rng[2] = rmax;
} else if (rmax <= 0) {
rng[0] = rmin;
rng[1] = rmax;
rng[2] = +1;
} else {
rng[0] = rmin;
rng[1] = 0;
rng[2] = rmax;
}
// adjust small ranges
if (rng[1]-rng[0] < mjMINVAL) {
rng[0] = rng[1] - mjMINVAL;
}
if (rng[2]-rng[1] < mjMINVAL) {
rng[2] = rng[1] + mjMINVAL;
}
// clamp act to extended range
if (vopt->flags[mjVIS_ACTIVATION] && m->actuator_dyntype[i]) {
act = mju_clip(d->act[m->actuator_actadr[i] + m->actuator_actnum[i] - 1], rng[0], rng[2]);
} else {
act = mju_clip(d->ctrl[m->actuator_ctrladr[i]], rng[0], rng[2]);
}
// compute interpolants
float amin, amean, amax;
if (act <= rng[1]) {
amin = (rng[1]-act) / mjMAX(mjMINVAL, rng[1]-rng[0]);
amean = 1 - amin;
amax = 0;
} else {
amax = (act-rng[1]) / mjMAX(mjMINVAL, rng[2]-rng[1]);
amean = 1 - amax;
amin = 0;
}
// interpolated color
float rgba[4];
for (int j=0; j < 4; j++) {
rgba[j] = amin*m->vis.rgba.actuatornegative[j] +
amean*m->vis.rgba.actuator[j] +
amax*m->vis.rgba.actuatorpositive[j];
}
// get transmission object id
int j = m->actuator_trnid[2*i];
// slide and hinge joint actuators
if (m->actuator_trntype[i] == mjTRN_JOINT ||
m->actuator_trntype[i] == mjTRN_JOINTINPARENT ||
m->actuator_trntype[i] == mjTRN_SITE) {
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_ACTUATOR);
if (!thisgeom) {
return;
}
// site actuators
mjtNum sz[3];
if (m->actuator_trntype[i] == mjTRN_SITE) {
// inflate sizes by 5%
mju_scl3(sz, m->site_size+3*j, 1.05);
// make geom
mjv_initGeom(thisgeom,
m->site_type[j], sz,
d->site_xpos + 3*j,
d->site_xmat + 9*j,
thisgeom->rgba);
} else if (m->jnt_type[j] == mjJNT_HINGE || m->jnt_type[j] == mjJNT_SLIDE) {
// set length(1) and width(0) of the connectors
sz[1] = m->vis.scale.actuatorlength * scl;
sz[0] = m->vis.scale.actuatorwidth * scl;
// make geom
mjtNum* from = d->xanchor + 3*j;
mjtNum to[3];
mju_addScl3(to, from, d->xaxis+3*j, sz[1]);
mjv_connector(thisgeom, m->jnt_type[j] == mjJNT_SLIDE ? mjGEOM_ARROW : mjGEOM_ARROW1,
sz[0], from, to);
}
// ball or free joint
else if (m->jnt_type[j] == mjJNT_BALL || m->jnt_type[j] == mjJNT_FREE) {
sz[0] = sz[1] = sz[2] = m->vis.scale.jointlength * scl * 0.33;
// make geom
mjv_initGeom(thisgeom,
m->jnt_type[j] == mjJNT_BALL ? mjGEOM_SPHERE : mjGEOM_BOX, sz,
d->xanchor + 3*j,
d->xmat + 9*m->jnt_bodyid[j],
thisgeom->rgba);
}
// set interpolated color
f2f(thisgeom->rgba, rgba, 4);
// vopt->label
if (vopt->label == mjLABEL_ACTUATOR) {
makeLabel(m, mjOBJ_ACTUATOR, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
// body actuators
else if (m->actuator_trntype[i] == mjTRN_BODY) {
// iterate over body's geoms
int geomnum = m->body_geomnum[j];
int geomadr = m->body_geomadr[j];
for (int k=geomadr; k < geomadr+geomnum; k++) {
int geomtype = m->geom_type[k];
// add inflated geom if it is a regular primitive
if (geomtype != mjGEOM_PLANE && geomtype != mjGEOM_HFIELD &&
geomtype != mjGEOM_MESH && geomtype != mjGEOM_SDF) {
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_ACTUATOR);
if (!thisgeom) {
return;
}
// inflate sizes by 5%
mjtNum sz[3];
mju_scl3(sz, m->geom_size+3*k, 1.05);
// make geom
mjv_initGeom(thisgeom,
m->geom_type[k], sz,
d->geom_xpos + 3*k,
d->geom_xmat + 9*k,
thisgeom->rgba);
// set interpolated color
f2f(thisgeom->rgba, rgba, 4);
releaseGeom(&thisgeom, scn);
}
}
}
// spatial tendon actuators
else if (m->actuator_trntype[i] == mjTRN_TENDON && d->ten_wrapnum[j]) {
for (int k=d->ten_wrapadr[j]; k < d->ten_wrapadr[j]+d->ten_wrapnum[j]-1; k++) {
if (d->wrap_obj[k] != -2 && d->wrap_obj[k+1] != -2) {
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_ACTUATOR);
if (!thisgeom) {
return;
}
// determine width: smaller for segments inside wrapping objects
mjtNum width;
if (d->wrap_obj[k] >= 0 && d->wrap_obj[k+1] >= 0) {
width = 0.5 * m->tendon_width[j];
} else {
width = m->tendon_width[j];
}
// increase width for actuator
width *= m->vis.map.actuatortendon;
// construct geom
mjv_connector(thisgeom, mjGEOM_CAPSULE, width, d->wrap_xpos+3*k, d->wrap_xpos+3*k+3);
// set material if given
setMaterial(m, thisgeom, m->tendon_matid[j], m->tendon_rgba+4*j, vopt->flags);
// set interpolated color
f2f(thisgeom->rgba, rgba, 4);
// vopt->label: only the first segment
if (vopt->label == mjLABEL_ACTUATOR && k == d->ten_wrapadr[j]) {
makeLabel(m, mjOBJ_ACTUATOR, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
}
}
}
static void addIslandLabelGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
mjvScene* scn) {
if (vopt->label != mjLABEL_ISLAND || !d->nisland) {
return;
}
for (int i=1; i < m->nbody; i++) {
int weld_id = m->body_weldid[i];
if (!m->body_dofnum[weld_id]) {
continue;
}
int islandid = d->dof_island[m->body_dofadr[weld_id]];
if (islandid <= -1) {
continue;
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
thisgeom->type = mjGEOM_LABEL;
mju_n2f(thisgeom->pos, d->xipos+3*i, 3);
mju_n2f(thisgeom->mat, d->ximat+9*i, 9);
mjSNPRINTF(thisgeom->label, "%d", islandid);
releaseGeom(&thisgeom, scn);
}
}
static void addCameraGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_CAMERA]) {
return;
}
const float scl = m->stat.meansize;
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 resolution larger than (1, 1)
if (m->cam_resolution[2*i] > 1 || m->cam_resolution[2*i+1] > 1) {
// 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];
int orthographic = m->cam_projection[i] == mjPROJ_ORTHOGRAPHIC;
// get frustum
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
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: scale for perspective, average(width, height) for orthographic
if (!orthographic) {
zhor[0] *= zfar / znear;
zhor[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(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++) {
addTriangle(scn, vnear[e], vfar[e], vnear[(e+1)%4], rgba,
i, mjCAT_DECOR, mjOBJ_CAMERA);
addTriangle(scn, vfar[e], vfar[(e+1)%4], vnear[(e+1)%4], rgba,
i, mjCAT_DECOR, mjOBJ_CAMERA);
addConnector(scn, mjGEOM_LINE, 3, vnear[e], vnear[(e+1)%4], rgba,
i, mjCAT_DECOR, mjOBJ_CAMERA);
addConnector(scn, mjGEOM_LINE, 3, vfar[e], vfar[(e+1)%4], rgba,
i, mjCAT_DECOR, mjOBJ_CAMERA);
addConnector(scn, mjGEOM_LINE, 3, vnear[e], vfar[e], rgba,
i, mjCAT_DECOR, mjOBJ_CAMERA);
}
}
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_CAMERA);
if (!thisgeom) {
return;
}
// construct geom: camera body
thisgeom->type = mjGEOM_BOX;
thisgeom->size[0] = scl * m->vis.scale.camera * 1.0;
thisgeom->size[1] = scl * m->vis.scale.camera * 0.8;
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, cam_rgba, 4);
// vopt->label
if (vopt->label == mjLABEL_CAMERA) {
makeLabel(m, mjOBJ_CAMERA, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_CAMERA);
if (!thisgeom) {
return;
}
// construct geom: lens
thisgeom->pos[0] = (float)(d->cam_xpos[3*i] -
scl*m->vis.scale.camera*0.6 * d->cam_xmat[9*i+2]);
thisgeom->pos[1] = (float)(d->cam_xpos[3*i+1] -
scl*m->vis.scale.camera*0.6 * d->cam_xmat[9*i+5]);
thisgeom->pos[2] = (float)(d->cam_xpos[3*i+2] -
scl*m->vis.scale.camera*0.6 * d->cam_xmat[9*i+8]);
thisgeom->type = mjGEOM_CYLINDER;
thisgeom->size[0] = scl * m->vis.scale.camera * 0.4;
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, cam_rgba, 4);
for (int k=0; k < 3; k++) {
thisgeom->rgba[k] *= 0.5; // make lens body darker
}
releaseGeom(&thisgeom, scn);
if (vopt->frame != mjFRAME_CAMERA) {
continue;
}
mjtNum width = m->vis.scale.framewidth * scl;
mjtNum length = m->vis.scale.framelength * scl;
addFrame(scn, i, d->cam_xpos+3*i, d->cam_xmat+9*i, length, width);
}
}
static void addLightGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_LIGHT]) {
return;
}
const float scl = m->stat.meansize;
for (int i=0; i < m->nlight; i++) {
// make light frame
mjtNum quat[4];
mju_quatZ2Vec(quat, d->light_xdir+3*i);
mjtNum mat[9];
mju_quat2Mat(mat, quat);
// make light position: offset backward, to avoid casting shadow
mjtNum vec[3];
mju_addScl3(vec, d->light_xpos+3*i, d->light_xdir+3*i, -scl * m->vis.scale.light -0.0001);
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_LIGHT);
if (!thisgeom) {
return;
}
// construct geom
thisgeom->type = mjGEOM_CYLINDER;
thisgeom->size[0] = scl * m->vis.scale.light * 0.8;
thisgeom->size[1] = scl * m->vis.scale.light * 0.8;
thisgeom->size[2] = scl * m->vis.scale.light * 1.0;
mju_n2f(thisgeom->pos, vec, 3);
mju_n2f(thisgeom->mat, mat, 9);
f2f(thisgeom->rgba, m->vis.rgba.light, 4);
// vopt->label
if (vopt->label == mjLABEL_LIGHT) {
makeLabel(m, mjOBJ_LIGHT, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
if (vopt->frame != mjFRAME_LIGHT) {
continue;
}
mjtNum width = m->vis.scale.framewidth * scl;
mjtNum length = m->vis.scale.framelength * scl;
addFrame(scn, i, d->light_xpos+3*i, mat, length, width);
}
}
static void addCenterOfMassGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
mjvScene* scn) {
// center of mass for root bodies
if (!vopt->flags[mjVIS_COM]) {
return;
}
const float scl = m->stat.meansize;
for (int i=1; i < m->nbody; i++) {
if (m->body_rootid[i] == i) {
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
if (!thisgeom) {
return;
}
thisgeom->type = mjGEOM_SPHERE;
thisgeom->size[0] = thisgeom->size[1] = thisgeom->size[2] = scl * m->vis.scale.com;
mju_n2f(thisgeom->pos, d->subtree_com+3*i, 3);
mju_n2f(thisgeom->mat, IDENTITY, 9);
f2f(thisgeom->rgba, m->vis.rgba.com, 4);
releaseGeom(&thisgeom, scn);
}
}
}
static void addAutoConnectGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
mjvScene* scn) {
if (!vopt->flags[mjVIS_AUTOCONNECT]) {
return;
}
const float scl = m->stat.meansize;
for (int i=1; i < m->nbody; i++) {
// do not connect to world
if (m->body_parentid[i] == 0) {
continue;
}
// start at body com, connect joint centers in reverse order
mjtNum* cur = d->xipos+3*i;
if (m->body_jntnum[i]) {
for (int j=m->body_jntadr[i]+m->body_jntnum[i]-1; j >= m->body_jntadr[i]; j--) {
mjtNum* nxt = d->xanchor+3*j;
// construct geom
addConnector(scn, mjGEOM_CAPSULE, scl * m->vis.scale.connect, cur, nxt,
m->vis.rgba.connect, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
cur = nxt;
}
}
// connect first joint (or com) to parent com
mjtNum* first = d->xipos+3*m->body_parentid[i];
addConnector(scn, mjGEOM_CAPSULE, scl * m->vis.scale.connect, cur, first,
m->vis.rgba.connect, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
}
}
static void addRangefinderGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
mjvScene* scn) {
if (!vopt->flags[mjVIS_RANGEFINDER]) {
return;
}
const float scl = m->stat.meansize;
mjtNum framewidth = m->vis.scale.framewidth * scl;
mjtNum framelength = m->vis.scale.framelength * scl;
for (int i=0; i < m->nsensor; i++) {
if (m->sensor_type[i] == mjSENS_RANGEFINDER) {
int objid = m->sensor_objid[i];
int adr = m->sensor_adr[i];
// get dataspec and compute field offsets
int dataspec = m->sensor_intprm[i*mjNSENS];
int size = mju_raydataSize(dataspec);
int offset[mjNRAYDATA] = {0};
int increment = 0;
for (int j=0; j < mjNRAYDATA; j++) {
offset[j] = increment;
if (dataspec & (1 << j)) {
increment += mjRAYDATA_SIZE[j];
}
}
// site-attached rangefinder
if (m->sensor_objtype[i] == mjOBJ_SITE) {
const mjtNum* ptr = d->sensordata + adr;
// get distance (if present)
mjtNum dist = -1;
if (dataspec & (1 << mjRAYDATA_DIST)) {
dist = ptr[offset[mjRAYDATA_DIST]];
}
// get point and draw line if dist is valid
mjtNum point[3] = {0};
if (dist >= 0) {
mjtNum* origin = d->site_xpos + 3*objid;
point[0] = origin[0] + d->site_xmat[9*objid+2]*dist;
point[1] = origin[1] + d->site_xmat[9*objid+5]*dist;
point[2] = origin[2] + d->site_xmat[9*objid+8]*dist;
addConnector(scn, mjGEOM_LINE, 3, origin, point, m->vis.rgba.rangefinder,
i, mjCAT_DECOR, mjOBJ_SENSOR);
}
// draw point if present and non-zero
if (dataspec & (1 << mjRAYDATA_POINT)) {
const mjtNum* point_data = ptr + offset[mjRAYDATA_POINT];
if (point_data[0] || point_data[1] || point_data[2]) {
mju_copy3(point, point_data);
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_SENSOR);
if (thisgeom) {
thisgeom->type = mjGEOM_SPHERE;
thisgeom->size[0] = thisgeom->size[1] = thisgeom->size[2] = 1.5 * framewidth;
mju_n2f(thisgeom->pos, point, 3);
mju_n2f(thisgeom->mat, IDENTITY, 9);
f2f(thisgeom->rgba, m->vis.rgba.rangefinder, 4);
releaseGeom(&thisgeom, scn);
}
}
}
// draw normal if present and point is valid
int valid_point = dist >= 0 || point[0] || point[1] || point[2];
if (valid_point && (dataspec & (1 << mjRAYDATA_NORMAL))) {
const mjtNum* normal = ptr + offset[mjRAYDATA_NORMAL];
mjtNum to[3];
mju_addScl3(to, point, normal, 2*framelength);
addConnector(scn, mjGEOM_ARROW1, framewidth, point, to,
m->vis.rgba.rangefinder, i, mjCAT_DECOR, mjOBJ_SENSOR);
}
}
// camera-attached rangefinder
else if (m->sensor_objtype[i] == mjOBJ_CAMERA) {
const int width = m->cam_resolution[2*objid];
const int height = m->cam_resolution[2*objid+1];
const mjtNum* cam_xpos = d->cam_xpos + 3*objid;
const mjtNum* cam_xmat = d->cam_xmat + 9*objid;
const int projection = m->cam_projection[objid];
// compute focal length in pixels using helper
mjtNum fx, fy, cx, cy, ortho_extent;
mju_camIntrinsics(m, objid, &fx, &fy, &cx, &cy, &ortho_extent);
// draw for each pixel
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int idx = row*width + col;
const mjtNum* ptr = d->sensordata + adr + idx*size;
// get distance (if present)
mjtNum dist = -1;
if (dataspec & (1 << mjRAYDATA_DIST)) {
dist = ptr[offset[mjRAYDATA_DIST]];
}
// compute ray origin and direction
mjtNum origin[3], direction[3];
mju_camPixelRay(origin, direction, cam_xpos, cam_xmat,
col, row, fx, fy, cx, cy, projection, ortho_extent);
// get point and draw line if dist is valid
mjtNum point[3] = {0};
if (dist >= 0) {
mju_addScl3(point, origin, direction, dist);
addConnector(scn, mjGEOM_LINE, 3, origin, point, m->vis.rgba.rangefinder,
i, mjCAT_DECOR, mjOBJ_SENSOR);
}
// draw point if present and non-zero
if (dataspec & (1 << mjRAYDATA_POINT)) {
const mjtNum* point_data = ptr + offset[mjRAYDATA_POINT];
if (point_data[0] || point_data[1] || point_data[2]) {
mju_copy3(point, point_data);
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_SENSOR);
if (thisgeom) {
thisgeom->type = mjGEOM_SPHERE;
thisgeom->size[0] = thisgeom->size[1] = thisgeom->size[2] = 1.3 * framewidth;
mju_n2f(thisgeom->pos, point, 3);
mju_n2f(thisgeom->mat, IDENTITY, 9);
f2f(thisgeom->rgba, m->vis.rgba.rangefinder, 4);
releaseGeom(&thisgeom, scn);
}
}
}
// draw normal if present and point is valid
int valid_point = dist >= 0 || point[0] || point[1] || point[2];
if (valid_point && (dataspec & (1 << mjRAYDATA_NORMAL))) {
const mjtNum* normal = ptr + offset[mjRAYDATA_NORMAL];
mjtNum to[3];
mju_addScl3(to, point, normal, 2*framelength);
addConnector(scn, mjGEOM_ARROW1, framewidth, point, to,
m->vis.rgba.rangefinder, i, mjCAT_DECOR, mjOBJ_SENSOR);
}
}
}
}
} else if (m->sensor_type[i] == mjSENS_GEOMFROMTO) {
// sensor data
mjtNum* fromto = d->sensordata + m->sensor_adr[i];
// null output: nothing to render
if (mju_isZero(fromto, 6)) {
continue;
}
// make ray
addConnector(scn, mjGEOM_LINE, 3, fromto, fromto+3, m->vis.rgba.rangefinder,
i, mjCAT_DECOR, mjOBJ_SENSOR);
}
}
}
static void addExternalPerturbGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
mjvScene* scn) {
if (!vopt->flags[mjVIS_PERTFORCE]) {
return;
}
const float scl = m->stat.meansize;
for (int i=1; i < m->nbody; i++) {
if (mju_isZero(d->xfrc_applied+6*i, 6)) {
continue;
}
// force perturbation
mjtNum* xfrc = d->xfrc_applied+6*i;
if (mju_norm3(xfrc) <= mjMINVAL) {
continue;
}
mjtNum* from = d->xipos+3*i;
// map force to spatial vector in world frame
mjtNum vec[3];
mju_scl3(vec, xfrc, m->vis.map.force/m->stat.meanmass);
mjtNum to[3];
mju_add3(to, from, vec);
addConnector(scn, mjGEOM_ARROW, m->vis.scale.forcewidth * scl, from, to,
m->vis.rgba.force, i, mjCAT_DECOR, mjOBJ_UNKNOWN);
}
}
static void addConstraintGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, mjvScene* scn) {
if (!vopt->flags[mjVIS_CONSTRAINT]) {
return;
}
// connect or weld
const float scl = m->stat.meansize;
for (int i=0; i < m->neq; i++) {
int is_weld = m->eq_type[i] == mjEQ_WELD;
int is_connect = m->eq_type[i] == mjEQ_CONNECT;
if (d->eq_active[i] && (is_connect || is_weld)) {
// compute endpoints in global coordinates
mjtNum vec[3], end[3];
mjtNum *xmat_j, *xmat_k;
int j = m->eq_obj1id[i], k = m->eq_obj2id[i];
if (m->eq_objtype[i] == mjOBJ_SITE) {
mju_copy3(vec, d->site_xpos+3*j);
mju_copy3(end, d->site_xpos+3*k);
xmat_j = d->site_xmat+9*j;
xmat_k = d->site_xmat+9*k;
} else {
mju_mulMatVec3(vec, d->xmat+9*j, m->eq_data+mjNEQDATA*i+3*is_weld);
mju_addTo3(vec, d->xpos+3*j);
mju_mulMatVec3(end, d->xmat+9*k, m->eq_data+mjNEQDATA*i+3*is_connect);
mju_addTo3(end, d->xpos+3*k);
xmat_j = d->xmat+9*j;
xmat_k = d->xmat+9*k;
}
// construct geom
mjtNum sz[3];
sz[0] = scl * m->vis.scale.constraint;
mjvGeom* thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_EQUALITY);
if (!thisgeom) {
return;
}
mjv_initGeom(thisgeom, mjGEOM_SPHERE, sz, vec, xmat_j, m->vis.rgba.connect);
if (vopt->label == mjLABEL_CONSTRAINT) {
makeLabel(m, mjOBJ_EQUALITY, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
thisgeom = acquireGeom(scn, i, mjCAT_DECOR, mjOBJ_EQUALITY);
if (!thisgeom) {
return;
}
mjv_initGeom(thisgeom, mjGEOM_SPHERE, sz, end, xmat_k, m->vis.rgba.constraint);
if (vopt->label == mjLABEL_CONSTRAINT) {
makeLabel(m, mjOBJ_EQUALITY, i, thisgeom->label);
}
releaseGeom(&thisgeom, scn);
}
}
}
// add abstract geoms
void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
const mjvPerturb* pert, int catmask, mjvScene* scn) {
// make default pert if missing
mjvPerturb localpert;
if (!pert) {
mjv_defaultPerturb(&localpert);
pert = &localpert;
}
// clear mjCAT_STATIC bit if mjVIS_STATIC is not set
if (!vopt->flags[mjVIS_STATIC]) {
catmask &= (~mjCAT_STATIC);
}
addFlexGeoms(m, d, vopt, pert, catmask, scn);
addSkinGeoms(m, d, vopt, pert, catmask, scn);
addGeomGeoms(m, d, vopt, pert, catmask, scn);
addSiteGeoms(m, d, vopt, pert, catmask, scn);
addSpatialTendonGeoms(m, d, vopt, catmask, scn);
addSliderCrankGeoms(m, d, vopt, catmask, scn);
// remaining functions only add decor elements
if (!(catmask & mjCAT_DECOR)) {
return;
}
addGeomFrameGeoms(m, d, vopt, catmask, scn);
addSiteFrameGeoms(m, d, vopt, catmask, scn);
addBodyBvhGeoms(m, d, vopt, scn);
addFlexBvhGeoms(m, d, vopt, scn);
addMeshBvhGeoms(m, d, vopt, scn);
addMeshOctreeGeoms(m, d, vopt, scn);
addTactileSensorGeoms(m, d, vopt, scn);
addInertiaGeoms(m, d, vopt, pert, catmask, scn);
addPerturbGeoms(m, d, vopt, pert, scn);
addWorldBodyFrameGeoms(m, d, vopt, catmask, scn);
addSelectionPointGeoms(m, d, vopt, pert, scn);
addBodyLabelGeoms(m, d, vopt, pert, catmask, scn);
addJointGeoms(m, d, vopt, scn);
addActuatorGeoms(m, d, vopt, scn);
addIslandLabelGeoms(m, d, vopt, scn);
addCameraGeoms(m, d, vopt, scn);
addLightGeoms(m, d, vopt, scn);
addCenterOfMassGeoms(m, d, vopt, scn);
addAutoConnectGeoms(m, d, vopt, scn);
addRangefinderGeoms(m, d, vopt, scn);
addExternalPerturbGeoms(m, d, vopt, scn);
addConstraintGeoms(m, d, vopt, scn);
addContactGeoms(m, d, vopt, scn, catmask);
}
// make list of lights only
void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) {
mjvLight* thislight;
// clear counter
scn->nlight = 0;
// headlight
if (m->vis.headlight.active) {
// get pointer
thislight = scn->lights;
// set default properties
memset(thislight, 0, sizeof(mjvLight));
thislight->id = -1;
thislight->headlight = 1;
thislight->texid = -1;
thislight->type = mjLIGHT_DIRECTIONAL;
thislight->castshadow = 0;
thislight->bulbradius = 0.02;
thislight->intensity = 0;
thislight->range = 10;
// compute head position and gaze direction in model space
mjtNum hpos[3], hfwd[3];
mjv_cameraInModel(hpos, hfwd, NULL, scn);
mju_n2f(thislight->pos, hpos, 3);
mju_n2f(thislight->dir, hfwd, 3);
// copy colors
f2f(thislight->ambient, m->vis.headlight.ambient, 3);
f2f(thislight->diffuse, m->vis.headlight.diffuse, 3);
f2f(thislight->specular, m->vis.headlight.specular, 3);
// advance counter
scn->nlight++;
}
// remaining lights
for (int i=0; i < m->nlight && scn->nlight < mjMAXLIGHT; i++) {
if (m->light_active[i]) {
// get pointer
thislight = scn->lights + scn->nlight;
// copy properties
memset(thislight, 0, sizeof(mjvLight));
thislight->id = i;
thislight->type = m->light_type[i];
thislight->texid = m->light_texid[i];
thislight->castshadow = m->light_castshadow[i];
thislight->bulbradius = m->light_bulbradius[i];
thislight->intensity = m->light_intensity[i];
thislight->range = m->light_range[i];
if (thislight->type == mjLIGHT_SPOT) {
f2f(thislight->attenuation, m->light_attenuation+3*i, 3);
thislight->exponent = m->light_exponent[i];
thislight->cutoff = m->light_cutoff[i];
}
// copy colors
f2f(thislight->ambient, m->light_ambient+3*i, 3);
f2f(thislight->diffuse, m->light_diffuse+3*i, 3);
f2f(thislight->specular, m->light_specular+3*i, 3);
// copy position and direction
mju_n2f(thislight->pos, d->light_xpos+3*i, 3);
mju_n2f(thislight->dir, d->light_xdir+3*i, 3);
// advance counter
scn->nlight++;
}
}
}
// update camera only
void mjv_updateCamera(const mjModel* m, const mjData* d, mjvCamera* cam, mjvScene* scn) {
// return if nothing to do
if (!m || !cam || cam->type == mjCAMERA_USER) {
return;
}
// move lookat for tracking
if (cam->type == mjCAMERA_TRACKING) {
// get id and check
int bid = cam->trackbodyid;
if (bid < 0 || bid >= m->nbody) {
mjERROR("track body id is outside valid range");
}
mju_copy3(cam->lookat, d->subtree_com + 3*bid);
}
// get camera frame
mjtNum headpos[3], forward[3], up[3], right[3];
mjv_cameraFrame(headpos, forward, up, right, d, cam);
// get camera frustum
float zver[2], zhor[2], zclip[2] = {0, 0};
mjv_cameraFrustum(zver, zhor, zclip, m, cam);
// get ipd, orthographic
int cid, orthographic = 0;
mjtNum ipd;
switch (cam->type) {
case mjCAMERA_FREE:
case mjCAMERA_TRACKING:
ipd = m->vis.global.ipd;
orthographic = m->vis.global.orthographic;
break;
case mjCAMERA_FIXED:
// get id, check range
cid = cam->fixedcamid;
if (cid < 0 || cid >= m->ncam) {
mjERROR("fixed camera id is outside valid range");
}
ipd = m->cam_ipd[cid];
orthographic = m->cam_projection[cid] == mjPROJ_ORTHOGRAPHIC;
break;
default:
mjERROR("unknown camera type");
}
// compute GL cameras
for (int view=0; view < 2; view++) {
// set frame
for (int i=0; i < 3; i++) {
scn->camera[view].pos[i] = (float)(headpos[i] + (view ? ipd : -ipd)*0.5*right[i]);
scn->camera[view].forward[i] = (float)forward[i];
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[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];
scn->camera[view].frustum_far = zclip[1];
}
// disable model transformation (do not clear float data; user may need it later)
scn->enabletransform = 0;
}
// construct face, flat normals
static void makeFace(float* _face, float* _normal, mjtNum radius, const mjtNum* vertxpos,
int nface, int i0, int i1, int i2) {
float* face = _face + 9*nface;
float* normal = _normal + 9*nface;
const mjtNum* v0 = vertxpos + 3*i0;
const mjtNum* v1 = vertxpos + 3*i1;
const mjtNum* v2 = vertxpos + 3*i2;
// compute normal
mjtNum v01[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
mjtNum v02[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
mjtNum nrm[3];
mju_cross(nrm, v01, v02);
mju_normalize3(nrm);
// set vertices: offset by radius*normal
mjtNum temp[3];
mju_addScl3(temp, v0, nrm, radius);
mju_n2f(face, temp, 3);
mju_addScl3(temp, v1, nrm, radius);
mju_n2f(face+3, temp, 3);
mju_addScl3(temp, v2, nrm, radius);
mju_n2f(face+6, temp, 3);
// set normals
mju_n2f(normal, nrm, 3);
mju_n2f(normal+3, nrm, 3);
mju_n2f(normal+6, nrm, 3);
}
// add face normal to vertices
static void addNormal(mjtNum* vertnorm, const mjtNum* vertxpos,
int i0, int i1, int i2) {
// compute normal*area
const mjtNum* v0 = vertxpos + 3*i0;
const mjtNum* v1 = vertxpos + 3*i1;
const mjtNum* v2 = vertxpos + 3*i2;
mjtNum v01[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
mjtNum v02[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
mjtNum nrm[3];
mju_cross(nrm, v01, v02);
mju_normalize3(nrm);
// accumulate at each vertex
mju_addTo3(vertnorm + 3*i0, nrm);
mju_addTo3(vertnorm + 3*i1, nrm);
mju_addTo3(vertnorm + 3*i2, nrm);
}
// construct face, smooth normals
static void makeSmooth(float* _face, float* _normal, mjtNum radius, mjtByte flg_flat,
const mjtNum* vertnorm, const mjtNum* vertxpos,
int nface, int i0, int i1, int i2) {
float* face = _face + 9*nface;
float* normal = _normal + 9*nface;
int ind[3] = {i0, i1, i2};
int sign = radius > 0 ? 1 : -1;
// flat shading
if (flg_flat) {
// compute face normal
const mjtNum* v0 = vertxpos + 3*i0;
const mjtNum* v1 = vertxpos + 3*i1;
const mjtNum* v2 = vertxpos + 3*i2;
mjtNum v01[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
mjtNum v02[3] = {v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]};
mjtNum nrm[3];
mju_cross(nrm, v01, v02);
mju_normalize3(nrm);
// set all vertex normals equal to face normal
for (int k=0; k < 3; k++){
normal[3*k+0] = (float) (sign*nrm[0]);
normal[3*k+1] = (float) (sign*nrm[1]);
normal[3*k+2] = (float) (sign*nrm[2]);
}
}
// smooth shading
else {
for (int k=0; k < 3; k++){
normal[3*k+0] = (float) (sign*vertnorm[3*ind[k]+0]);
normal[3*k+1] = (float) (sign*vertnorm[3*ind[k]+1]);
normal[3*k+2] = (float) (sign*vertnorm[3*ind[k]+2]);
}
}
// set positions: vertices offset by radius*normal
for (int k=0; k < 3; k++){
face[3*k+0] = (float) (vertxpos[3*ind[k]+0] + radius*vertnorm[3*ind[k]+0]);
face[3*k+1] = (float) (vertxpos[3*ind[k]+1] + radius*vertnorm[3*ind[k]+1]);
face[3*k+2] = (float) (vertxpos[3*ind[k]+2] + radius*vertnorm[3*ind[k]+2]);
}
}
// construct side in 2D face
static void makeSide(float* _face, float* _normal, mjtNum radius,
const mjtNum* vertnorm, const mjtNum* vertxpos,
int nface, int i0, int i1) {
float* face = _face + 9*nface;
float* normal = _normal + 9*nface;
// compute normal
const mjtNum* v0 = vertxpos + 3*i0;
const mjtNum* v1 = vertxpos + 3*i1;
mjtNum v01[3] = {v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]};
mjtNum nrm[3];
mju_cross(nrm, v01, vertnorm+3*i1);
if (radius < 0) {
mju_scl3(nrm, nrm, -1);
}
mju_normalize3(nrm);
// set normals
for (int k=0; k < 3; k++){
normal[3*k+0] = (float) nrm[0];
normal[3*k+1] = (float) nrm[1];
normal[3*k+2] = (float) nrm[2];
}
// set positions
int ind[3] = {i0, i1, i1};
for (int k=0; k < 3; k++){
mjtNum sign = (k == 1 ? -1 : +1);
face[3*k+0] = (float) (vertxpos[3*ind[k]+0] + sign*radius*vertnorm[3*ind[k]+0]);
face[3*k+1] = (float) (vertxpos[3*ind[k]+1] + sign*radius*vertnorm[3*ind[k]+1]);
face[3*k+2] = (float) (vertxpos[3*ind[k]+2] + sign*radius*vertnorm[3*ind[k]+2]);
}
}
// copy texcoord for face
static void copyTex(float* dst, const float* src, int nface, int i0, int i1, int i2) {
if (!dst || !src) {
return;
}
dst[6*nface+0] = src[2*i0];
dst[6*nface+1] = src[2*i0+1];
dst[6*nface+2] = src[2*i1];
dst[6*nface+3] = src[2*i1+1];
dst[6*nface+4] = src[2*i2];
dst[6*nface+5] = src[2*i2+1];
}
// update visible flexes only
void mjv_updateActiveFlex(const mjModel* m, mjData* d, mjvScene* scn, const mjvOption* opt) {
// save flex visualization flags in scene (needed by renderer)
scn->flexvertopt = opt->flags[mjVIS_FLEXVERT];
scn->flexedgeopt = opt->flags[mjVIS_FLEXEDGE];
scn->flexfaceopt = opt->flags[mjVIS_FLEXFACE];
scn->flexskinopt = opt->flags[mjVIS_FLEXSKIN];
// convert vertex positions from mjtNum to float
for (int v=0; v < 3*m->nflexvert; v++) {
scn->flexvert[v] = (float) d->flexvert_xpos[v];
}
// construct faces
for (int f=0; f < m->nflex; f++) {
int dim = m->flex_dim[f];
mjtNum radius = m->flex_radius[f];
mjtByte flg_flat = m->flex_flatskin[f];
const mjtNum* vertxpos = d->flexvert_xpos + 3*m->flex_vertadr[f];
float* face = scn->flexface + 9*scn->flexfaceadr[f];
float* normal = scn->flexnormal + 9*scn->flexfaceadr[f];
float* texdst = m->flex_texcoordadr[f] >= 0 ?
scn->flextexcoord + 6*scn->flexfaceadr[f] : NULL;
const float* texsrc = m->flex_texcoordadr[f] >= 0 ?
m->flex_texcoord + 2*m->flex_texcoordadr[f] : NULL;
// 1D, or face and skin disabled: no faces
if (dim == 1 || (!opt->flags[mjVIS_FLEXFACE] && !opt->flags[mjVIS_FLEXSKIN])) {
scn->flexfaceused[f] = 0;
}
// 2D or 3D face: faces from elements, flat normals, texture
else if (!opt->flags[mjVIS_FLEXSKIN]) {
int nface = 0;
for (int e=0; e < m->flex_elemnum[f]; e++) {
// in 3D, show only elements in selected layer
if (dim == 2 || m->flex_elemlayer[m->flex_elemadr[f]+e] == opt->flex_layer) {
// get element data
const int* edata = m->flex_elem + m->flex_elemdataadr[f] + e*(dim+1);
const int* tdata = m->flex_elemtexcoord + m->flex_elemdataadr[f] + e*(dim+1);
// triangles: two faces per element
if (dim == 2) {
makeFace(face, normal, radius, vertxpos, nface, edata[0], edata[1], edata[2]);
copyTex(texdst, texsrc, nface, tdata[0], tdata[1], tdata[2]);
nface++;
makeFace(face, normal, radius, vertxpos, nface, edata[0], edata[2], edata[1]);
copyTex(texdst, texsrc, nface, tdata[0], tdata[2], tdata[1]);
nface++;
}
// tetrahedra: four faces per element
else {
makeFace(face, normal, radius, vertxpos,
nface, edata[0], edata[1], edata[2]);
copyTex(texdst, texsrc, nface, tdata[0], tdata[1], tdata[2]);
nface++;
makeFace(face, normal, radius, vertxpos,
nface, edata[0], edata[2], edata[3]);
copyTex(texdst, texsrc, nface, tdata[0], tdata[2], tdata[3]);
nface++;
makeFace(face, normal, radius, vertxpos,
nface, edata[0], edata[3], edata[1]);
copyTex(texdst, texsrc, nface, tdata[0], tdata[3], tdata[1]);
nface++;
makeFace(face, normal, radius, vertxpos,
nface, edata[1], edata[3], edata[2]);
copyTex(texdst, texsrc, nface, tdata[1], tdata[3], tdata[2]);
nface++;
}
}
}
// save face count
scn->flexfaceused[f] = nface;
}
// 2D or 3D skin: faces from elements (2D) or shells (3D), smooth normals, texture
else {
// allocate and clear vertex normals for smoothing
mj_markStack(d);
mjtNum* vertnorm = mjSTACKALLOC(d, 3*m->flex_vertnum[f], mjtNum);
mju_zero(vertnorm, 3*m->flex_vertnum[f]);
// add vertex normals: top element sides in 2D, shell fragments in 3D
if (dim == 2) {
for (int e=0; e < m->flex_elemnum[f]; e++) {
const int* edata = m->flex_elem + m->flex_elemdataadr[f] + e*(dim+1);
addNormal(vertnorm, vertxpos, edata[0], edata[1], edata[2]);
}
} else {
for (int s=0; s < m->flex_shellnum[f]; s++) {
const int* sdata = m->flex_shell + m->flex_shelldataadr[f] + s*dim;
addNormal(vertnorm, vertxpos, sdata[0], sdata[1], sdata[2]);
}
}
// normalize vertex normals
for (int i=0; i < m->flex_vertnum[f]; i++) {
mju_normalize3(vertnorm+3*i);
}
// create faces, offset along smoothed vertex normals, and texcoord
int nface = 0;
if (dim == 2) {
for (int e=0; e < m->flex_elemnum[f]; e++) {
const int* edata = m->flex_elem + m->flex_elemdataadr[f] + e*(dim+1);
const int* tdata = m->flex_elemtexcoord + m->flex_elemdataadr[f] + e*(dim+1);
makeSmooth(face, normal, radius, flg_flat, vertnorm, vertxpos,
nface, edata[0], edata[1], edata[2]);
copyTex(texdst, texsrc, nface, tdata[0], tdata[1], tdata[2]);
nface++;
makeSmooth(face, normal, -radius, flg_flat, vertnorm, vertxpos,
nface, edata[0], edata[2], edata[1]);
copyTex(texdst, texsrc, nface, tdata[0], tdata[2], tdata[1]);
nface++;
}
} else {
for (int s=0; s < m->flex_shellnum[f]; s++) {
const int* sdata = m->flex_shell + m->flex_shelldataadr[f] + s*dim;
makeSmooth(face, normal, radius, flg_flat, vertnorm, vertxpos,
nface, sdata[0], sdata[1], sdata[2]);
copyTex(texdst, texsrc, nface, sdata[0], sdata[1], sdata[2]);
nface++;
}
}
// 2D: close sides using shell fragments
if (dim == 2) {
for (int s=0; s < m->flex_shellnum[f]; s++) {
const int* sdata = m->flex_shell + m->flex_shelldataadr[f] + s*dim;
makeSide(face, normal, radius, vertnorm, vertxpos,
nface, sdata[0], sdata[1]);
copyTex(texdst, texsrc, nface, sdata[0], sdata[1], sdata[1]);
nface++;
makeSide(face, normal, -radius, vertnorm, vertxpos,
nface, sdata[1], sdata[0]);
copyTex(texdst, texsrc, nface, sdata[1], sdata[0], sdata[0]);
nface++;
}
}
// save face count
scn->flexfaceused[f] = nface;
mj_freeStack(d);
}
// check face count, SHOULD NOT OCCUR
if (scn->flexfaceused[f] > scn->flexfacenum[f]) {
mju_error("too many flex faces in mjv_updateActiveFlex");
}
}
}
// update all skins, here for backward API compatibility
void mjv_updateSkin(const mjModel* m, const mjData* d, mjvScene* scn) {
mjvOption opt;
mjv_defaultOption(&opt);
mjv_updateActiveSkin(m, d, scn, &opt);
mju_warning("mjv_updateSkin is deprecated, please use mjv_updateActiveSkin.");
}
// update visible skins only
void mjv_updateActiveSkin(const mjModel* m, const mjData* d, mjvScene* scn, const mjvOption* opt) {
// process skins
for (int i=0; i < m->nskin; i++) {
// get info
int vertadr = m->skin_vertadr[i];
int vertnum = m->skin_vertnum[i];
int faceadr = m->skin_faceadr[i];
int facenum = m->skin_facenum[i];
// clear positions and normals
memset(scn->skinvert + 3*vertadr, 0, 3*vertnum*sizeof(float));
memset(scn->skinnormal + 3*vertadr, 0, 3*vertnum*sizeof(float));
// update only if visible
if (opt->skingroup[mjMAX(0, mjMIN(mjNGROUP-1, m->skin_group[i]))]) {
// accumulate positions from all bones
for (int j=m->skin_boneadr[i];
j < m->skin_boneadr[i]+m->skin_bonenum[i];
j++) {
// get bind pose
mjtNum bindpos[3] = {
(mjtNum) m->skin_bonebindpos[3*j],
(mjtNum) m->skin_bonebindpos[3*j+1],
(mjtNum) m->skin_bonebindpos[3*j+2]
};
mjtNum bindquat[4] = {
(mjtNum) m->skin_bonebindquat[4*j],
(mjtNum) m->skin_bonebindquat[4*j+1],
(mjtNum) m->skin_bonebindquat[4*j+2],
(mjtNum) m->skin_bonebindquat[4*j+3]
};
// compute rotation
int bodyid = m->skin_bonebodyid[j];
mjtNum quat[4], quatneg[4], rotate[9];
mju_negQuat(quatneg, bindquat);
mju_mulQuat(quat, d->xquat+4*bodyid, quatneg);
mju_quat2Mat(rotate, quat);
// compute translation
mjtNum translate[3];
mju_mulMatVec3(translate, rotate, bindpos);
mju_sub3(translate, d->xpos+3*bodyid, translate);
// process all bone vertices
for (int k=m->skin_bonevertadr[j];
k < m->skin_bonevertadr[j]+m->skin_bonevertnum[j];
k++) {
// vertex id and weight
int vid = m->skin_bonevertid[k];
float vweight = m->skin_bonevertweight[k];
// get original position
mjtNum pos[3] = {
(mjtNum) m->skin_vert[3*(vertadr+vid)],
(mjtNum) m->skin_vert[3*(vertadr+vid)+1],
(mjtNum) m->skin_vert[3*(vertadr+vid)+2],
};
// transform
mjtNum pos1[3];
mju_mulMatVec3(pos1, rotate, pos);
mju_addTo3(pos1, translate);
// accumulate position
scn->skinvert[3*(vertadr+vid)] += vweight*(float)pos1[0];
scn->skinvert[3*(vertadr+vid)+1] += vweight*(float)pos1[1];
scn->skinvert[3*(vertadr+vid)+2] += vweight*(float)pos1[2];
}
}
// compute vertex normals from face normals
for (int k=faceadr; k < faceadr+facenum; k++) {
// get face vertex indices
int vid[3] = {
m->skin_face[3*k],
m->skin_face[3*k+1],
m->skin_face[3*k+2]
};
// get triangle edges
mjtNum vec01[3], vec02[3];
for (int r=0; r < 3; r++) {
vec01[r] = scn->skinvert[3*(vertadr+vid[1])+r] - scn->skinvert[3*(vertadr+vid[0])+r];
vec02[r] = scn->skinvert[3*(vertadr+vid[2])+r] - scn->skinvert[3*(vertadr+vid[0])+r];
}
// compute face normal
mjtNum nrm[3];
mju_cross(nrm, vec01, vec02);
// add normal to each vertex with weight = area
for (int r=0; r < 3; r++) {
for (int t=0; t < 3; t++) {
scn->skinnormal[3*(vertadr+vid[r])+t] += nrm[t];
}
}
}
// normalize normals
for (int k=vertadr; k < vertadr+vertnum; k++) {
float s = sqrtf(
scn->skinnormal[3*k+0]*scn->skinnormal[3*k+0] +
scn->skinnormal[3*k+1]*scn->skinnormal[3*k+1] +
scn->skinnormal[3*k+2]*scn->skinnormal[3*k+2]
);
float scl = 1/mjMAX(mjMINVAL, s);
scn->skinnormal[3*k] *= scl;
scn->skinnormal[3*k+1] *= scl;
scn->skinnormal[3*k+2] *= scl;
}
// inflate
if (m->skin_inflate[i]) {
float inflate = m->skin_inflate[i];
for (int k=vertadr; k < vertadr+vertnum; k++) {
scn->skinvert[3*k] += inflate*scn->skinnormal[3*k];
scn->skinvert[3*k+1] += inflate*scn->skinnormal[3*k+1];
scn->skinvert[3*k+2] += inflate*scn->skinnormal[3*k+2];
}
}
}
}
}
// update entire scene
void mjv_updateScene(const mjModel* m, mjData* d, const mjvOption* opt,
const mjvPerturb* pert, mjvCamera* cam, int catmask, mjvScene* scn) {
// clear geoms
scn->ngeom = 0;
// trigger plugin visualization hooks
if (m->nplugin) {
const int nslot = mjp_pluginCount();
// iterate over plugins, call visualize if defined
for (int i=0; i < m->nplugin; i++) {
const int slot = m->plugin[i];
const mjpPlugin* plugin = mjp_getPluginAtSlotUnsafe(slot, nslot);
if (!plugin) {
mjERROR("invalid plugin slot: %d", slot);
}
if (plugin->visualize) {
plugin->visualize(m, d, opt, scn, i);
}
}
}
// add all categories
mjv_addGeoms(m, d, opt, pert, catmask, scn);
// update camera
mjv_updateCamera(m, d, cam, scn);
// add lights
mjv_makeLights(m, d, scn);
// update flexes
if (opt->flags[mjVIS_FLEXVERT] || opt->flags[mjVIS_FLEXEDGE] ||
opt->flags[mjVIS_FLEXFACE] || opt->flags[mjVIS_FLEXSKIN]) {
mjv_updateActiveFlex(m, d, scn, opt);
}
// update skins
if (opt->flags[mjVIS_SKIN]) {
mjv_updateActiveSkin(m, d, scn, opt);
}
}
//----------------------------------- catenary functions -------------------------------------------
// returns hyperbolic cosine and optionally computes hyperbolic sine
static inline mjtNum cosh_sinh(mjtNum x, mjtNum* sinh) {
mjtNum expx = mju_exp(x);
if (sinh) {
*sinh = 0.5 * (expx - 1/expx);
}
return 0.5 * (expx + 1/expx);
}
// returns intercept of the catenary equation
static inline mjtNum catenary_intercept(mjtNum v, mjtNum h, mjtNum length) {
return 1/mju_sqrt(mju_sqrt(length*length - v*v)/h - 1);
}
// returns residual of catenary equation and optionally computes its gradient w.r.t b
static inline mjtNum catenary_residual(mjtNum b, mjtNum intercept, mjtNum* grad) {
mjtNum a = 0.5 / b;
mjtNum sinh, cosh = cosh_sinh(a, &sinh);
if (grad) {
*grad = (a*cosh - sinh) * mju_pow(2*b*sinh - 1, -1.5);
}
return 1/mju_sqrt(2*b*sinh - 1) - intercept;
}
// convergence tolerance for catenary solver
static const mjtNum tolerance = 1e-9;
// solve transcendental catenary equation using change of variables proposed in
// https://math.stackexchange.com/a/1002996
static inline mjtNum solve_catenary(mjtNum v, mjtNum h, mjtNum length) {
mjtNum intercept = catenary_intercept(v, h, length);
// initial guess using linear approximation to catenary_residual
mjtNum b = intercept / mju_sqrt(24);
// Newton steps to convergence (usually ~ 5 steps)
for (int i=0; i < 50; i++) {
// get value and gradient
mjtNum grad;
mjtNum res = catenary_residual(b, intercept, &grad);
if (mju_abs(res) < tolerance) {
break;
}
// Newton step
mjtNum step = -res / grad;
// backtracking line-search is not essential but can reduce number of iterations
for (int j=0; j < 10; j++) {
mjtNum new_res = catenary_residual(b + step, intercept, NULL);
if (mju_abs(new_res) < mju_abs(res)) {
break;
} else {
step *= 0.5;
}
}
// take step
b += step;
}
return b;
}
// points along catenary of given length between x0 and x1, returns number of points
int mjv_catenary(const mjtNum x0[3], const mjtNum x1[3], const mjtNum gravity[3], mjtNum length,
mjtNum* catenary, int ncatenary) {
mjtNum dist = mju_dist3(x0, x1);
// tendon is stretched longer than length: draw straight line
if (dist > length) {
// copy start and end points
mju_copy3(catenary+0, x0);
mju_copy3(catenary+3, x1);
return 2;
}
// tendon is shorter than length
else {
// normalized up vector
mjtNum up[3];
mju_scl3(up, gravity, -1);
mju_normalize3(up);
// x0 to x1
mjtNum x01[3];
mju_sub3(x01, x1, x0);
// make across orthonormal to up, points from x0 to x1
mjtNum across[3];
mju_copy3(across, x01);
mjtNum tmp[3];
mju_scl3(tmp, up, mju_dot3(up, across));
mju_subFrom3(across, tmp);
mjtNum norm = mju_normalize3(across);
// if across is numerically tiny, just set to 0
if (norm < mjMINVAL) {
mju_zero3(across);
}
// extents in the suspension plane
mjtNum h = mju_dot3(x01, across); // horizontal suspension extent
mjtNum v = mju_dot3(x01, up); // vertical height difference of x1 and x0
// near vertical tendon, use hanging bead approximation: 3 points
if (length > 100*h) {
// solve for location of bead hanging on tendon
mjtNum d_up = -0.5*(mju_sqrt(length*length - h*h) - v); // down from x0
mjtNum d_across = h*d_up / (2*d_up - v); // across from x0
// start point
mju_copy3(catenary+0, x0);
// midpoint: bead location
mju_copy3(catenary+3, x0);
mju_addToScl3(catenary+3, up, d_up);
mju_addToScl3(catenary+3, across, d_across);
// end point
mju_copy3(catenary+6, x1);
return 3;
}
// compute full catenary: ncatenary points
else {
// b*h: scaled catenary flatness
mjtNum bh = solve_catenary(v, h, length) * h;
// horizontal and vertical offsets
mjtNum h_offset = -0.5 * (mju_log((length+v) / (length-v)) * bh - h);
mjtNum v_offset = -cosh_sinh(h_offset / bh, NULL) * bh;
// start point
mju_copy3(catenary+0, x0);
// hanging points
for (int i=1; i < ncatenary-1; i++) {
// linearly spaced horizontal offset
mjtNum horizontal = i*h/ncatenary;
mju_addScl3(catenary+3*i, x0, across, horizontal);
// vertical offset, evaluate catenary values
mjtNum vertical = bh * cosh_sinh((horizontal - h_offset) / bh, NULL) + v_offset;
mju_addToScl3(catenary+3*i, up, vertical);
}
// end point
mju_copy3(catenary+3*(ncatenary-1), x1);
return ncatenary;
}
}
return 0; // SHOULD NOT OCCUR
}