| |
| |
| |
| |
| |
|
|
| import * as THREE from '../vendor/three/three.module.js'; |
| import { at } from './sim.js'; |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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; |
| |
| texture.repeat.set(100, 100); |
| texture.anisotropy = 8; |
| return texture; |
| } |
|
|
| function primitiveGeometry(type, sx, sy, sz) { |
| switch (type) { |
| case GEOM_PLANE: |
| |
| 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); |
| 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); |
| } |
| } |
|
|
| |
| 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); |
| |
| 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); |
| } |
|
|
| |
| 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); |
| } |
| } |
|
|