happynood's picture
Add artwork and tighten the card
368d239 verified
Raw
History Blame Contribute Delete
6.47 kB
// three.js rendering driven entirely by the compiled MuJoCo model.
//
// Geometry is read out of the model — types, sizes, mesh vertices and faces — and
// posed each frame from `data.geom_xpos` and `data.geom_xmat`. Nothing is
// re-authored, so what is on screen is the scene the policy was trained in.
import * as THREE from '../vendor/three/three.module.js';
import { at } from './sim.js';
// mjtGeom values used by this scene.
const GEOM_PLANE = 0;
const GEOM_SPHERE = 2;
const GEOM_CAPSULE = 3;
const GEOM_ELLIPSOID = 4;
const GEOM_CYLINDER = 5;
const GEOM_BOX = 6;
const GEOM_MESH = 7;
function meshGeometry(model, dataId) {
const vertAdr = at(model.mesh_vertadr, dataId);
const vertNum = at(model.mesh_vertnum, dataId);
const faceAdr = at(model.mesh_faceadr, dataId);
const faceNum = at(model.mesh_facenum, dataId);
const positions = new Float32Array(vertNum * 3);
for (let i = 0; i < vertNum * 3; i++) positions[i] = at(model.mesh_vert, vertAdr * 3 + i);
const indices = new Uint32Array(faceNum * 3);
for (let i = 0; i < faceNum * 3; i++) indices[i] = at(model.mesh_face, faceAdr * 3 + i);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.computeVertexNormals();
return geometry;
}
/**
* A checkerboard for the ground plane.
*
* Not decoration. The camera tracks the torso and MuJoCo's floor is a featureless
* infinite plane, so a robot walking at 1 m/s renders as a robot standing still —
* nothing in frame moves relative to it. The texture is what makes the gait read
* as locomotion.
*/
function floorTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#1b222d';
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#232c3a';
ctx.fillRect(0, 0, size / 2, size / 2);
ctx.fillRect(size / 2, size / 2, size / 2, size / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
// One tile per 0.5 m of a 50 m plane.
texture.repeat.set(100, 100);
texture.anisotropy = 8;
return texture;
}
function primitiveGeometry(type, sx, sy, sz) {
switch (type) {
case GEOM_PLANE:
// MuJoCo planes are infinite; size is a rendering hint. 0 means "large".
return new THREE.PlaneGeometry(2 * (sx || 25), 2 * (sy || 25));
case GEOM_SPHERE:
return new THREE.SphereGeometry(sx, 16, 12);
case GEOM_CAPSULE:
return new THREE.CapsuleGeometry(sx, 2 * sy, 8, 8);
case GEOM_ELLIPSOID: {
const g = new THREE.SphereGeometry(1, 16, 12);
g.scale(sx, sy, sz);
return g;
}
case GEOM_CYLINDER:
return new THREE.CylinderGeometry(sx, sx, 2 * sy, 16);
case GEOM_BOX:
return new THREE.BoxGeometry(2 * sx, 2 * sy, 2 * sz);
default:
return null;
}
}
export class Viewport {
constructor(canvas, sim, label) {
this.sim = sim;
this.label = label;
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x11151c);
this.camera = new THREE.PerspectiveCamera(45, 1, 0.05, 100);
this.camera.up.set(0, 0, 1); // MuJoCo is z-up
this.camera.position.set(1.05, -1.35, 0.78);
this.scene.add(new THREE.HemisphereLight(0xbfd4ff, 0x30302f, 1.1));
const sun = new THREE.DirectionalLight(0xffffff, 1.4);
sun.position.set(2, -3, 5);
this.scene.add(sun);
this.#buildGeoms();
this.resize();
}
#buildGeoms() {
const model = this.sim.model;
this.meshes = [];
for (let g = 0; g < model.ngeom; g++) {
const type = at(model.geom_type, g);
const sx = at(model.geom_size, g * 3 + 0);
const sy = at(model.geom_size, g * 3 + 1);
const sz = at(model.geom_size, g * 3 + 2);
let geometry = null;
if (type === GEOM_MESH) {
const dataId = at(model.geom_dataid, g);
if (dataId >= 0) geometry = meshGeometry(model, dataId);
} else {
geometry = primitiveGeometry(type, sx, sy, sz);
}
if (geometry === null) {
this.meshes.push(null);
continue;
}
const r = at(model.geom_rgba, g * 4 + 0);
const gg = at(model.geom_rgba, g * 4 + 1);
const b = at(model.geom_rgba, g * 4 + 2);
const a = at(model.geom_rgba, g * 4 + 3);
const material =
type === GEOM_PLANE
? new THREE.MeshStandardMaterial({
map: floorTexture(),
roughness: 0.95,
metalness: 0.0,
})
: new THREE.MeshStandardMaterial({
color: new THREE.Color(r, gg, b),
transparent: a < 1,
opacity: a,
roughness: 0.6,
metalness: 0.15,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.matrixAutoUpdate = false;
this.scene.add(mesh);
this.meshes.push(mesh);
}
}
/** Pose every geom from the current simulation state. */
sync() {
const d = this.sim.data;
const m = new THREE.Matrix4();
for (let g = 0; g < this.meshes.length; g++) {
const mesh = this.meshes[g];
if (!mesh) continue;
const px = at(d.geom_xpos, g * 3 + 0);
const py = at(d.geom_xpos, g * 3 + 1);
const pz = at(d.geom_xpos, g * 3 + 2);
// geom_xmat is row-major 3x3; three.js `set` takes row-major too.
const r = (i) => at(d.geom_xmat, g * 9 + i);
m.set(
r(0), r(1), r(2), px,
r(3), r(4), r(5), py,
r(6), r(7), r(8), pz,
0, 0, 0, 1
);
mesh.matrix.copy(m);
}
// Follow the torso so the robot does not walk out of frame.
const tx = at(d.xpos, 3);
const ty = at(d.xpos, 4);
const tz = at(d.xpos, 5);
this.camera.position.set(tx + 1.05, ty - 1.35, tz + 0.55);
this.camera.lookAt(tx, ty, tz - 0.05);
}
resize() {
const canvas = this.renderer.domElement;
const w = canvas.clientWidth || 320;
const h = canvas.clientHeight || 240;
this.renderer.setSize(w, h, false);
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
}
draw() {
this.renderer.render(this.scene, this.camera);
}
}