<!DOCTYPE html> <html> <head> <title>Comp 86 example: Robot, with animation and picking</title> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.156.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.156.0/examples/jsm/" } } </script> <script type="module"> class SceneGraph extends THREE.Scene { constructor () { super() // Keep our robots in a list so timer can access this.robots = [] //*2 Propagate tick() // First robot, position = WC origin to robot local origin this.robots[0] = new Robot () this.robots[0].position.set (0, -5, -10) this.add (this.robots[0]) // A second robot, different local origin this.robots[1] = new Robot () this.robots[1].position.set (-5, -5, -20) this.robots[1].rotation.set (0, -20 * Math.PI/180, 0) this.add (this.robots[1]) } /* * Receive animation tick, pass it to objects that need it */ tick () { //*2 for (let r of this.robots) { //*2 r.tick() //*2 } } } /* * We make a Robot out of other library objects */ class Robot extends THREE.Object3D { constructor () { super() /* * Initialize some Materials we will be using */ let platform = new THREE.MeshPhongMaterial ({ color: 0x4c4c33 }) let pants = new THREE.MeshPhongMaterial ({ color: 0x0c0c4c }) let shirt = new THREE.MeshPhongMaterial ({ color: 0xffcc80 }) let skin = new THREE.MeshPhongMaterial ({ color: 0xffd9b3, shininess: 60 }) /* * The base */ // The bottom flat part of base let base = new THREE.Mesh ( new THREE.BoxGeometry (1.5, 1, 1.25), platform) // (transform = local offset for center of box, ie 0.5 radius) base.position.set (0, 1/2., 0) this.add (base) // The column let column = new THREE.Mesh ( new THREE.CylinderGeometry (.4, .4, 2.5, 32, 32, false), pants) column.position.set (0, 1.0 + 2.5/2.0, 0) this.add (column) /* * Upper body */ // First make root node for the rest of the upper body, // and then work from its local origin let upperbody = new THREE.Object3D () upperbody.position.set (0, 1+2.5, 0) this.add (upperbody) // Trunk let trunk = new THREE.Mesh ( new THREE.BoxGeometry (2, 3, 2), shirt) trunk.position.set (0, 1.5, 0) upperbody.add (trunk) /* * Head animation: * Insert an extra transform node, Identity for now, * which we will modify on each tick. * Things the timer needs to access are stored in ivars */ this.headMove = new THREE.Object3D () //*3 Insert animation nodes upperbody.add (this.headMove) //*3 // Head let head = new THREE.Mesh ( new THREE.BoxGeometry (1, 1, 1), skin) head.position.set (0, 3 + 1/2., 0) this.headMove.add (head) //*3 // Right armhand: first we insert a transform node, from upperbody // local origin of armhand, so the rotation will happen at the // right point let rightArmHandTrans = new THREE.Object3D () rightArmHandTrans.position.set (-1.25, 3, 0) upperbody.add (rightArmHandTrans) //*3 // Animation for right armhand: again, insert transform and remember it // Must also remember current direction (+1 for increasing, -1 for decreasing) // we stash that in the userData field in scene graph node this.rightMove = new THREE.Object3D () //*3 // userData is our own data field this.rightMove.userData = 1 //*5 Stash our data in node rightArmHandTrans.add (this.rightMove) //*3 // Right armhand itself let rightArmHand = new ArmHand (shirt, skin) this.rightMove.add (rightArmHand) //*3 // Animation for left armhand: similarly let leftArmHandTrans = new THREE.Object3D() leftArmHandTrans.position.set (1.25, 3, 0) leftArmHandTrans.rotation.set (0, Math.PI, 0) upperbody.add (leftArmHandTrans) //*3 this.leftMove = new THREE.Object3D () //*3 this.leftMove.userData = 1 //*5 leftArmHandTrans.add (this.leftMove) //*3 // Left armhand let leftArmHand = new ArmHand (shirt, skin) this.leftMove.add (leftArmHand) //*3 } /* * Received whenever timer tick's, * we should update everything we need in scene graph, * redraw is automatic. */ tick () { //*4 Change each transform /* * Head: * rotates 1 revolution (2*PI) in 9 seconds (540 1/60 sec ticks) * goes round and round, rather than oscillate back and forth. * We retrieve the old rotation angle out of the scene graph, * modify it, and put it back in */ let a = this.headMove.rotation.y //*4 a += (2 * Math.PI) / (9*60) //*4 if (a > 2*Math.PI) { //*4 a -= 2 * Math.PI //*4 } this.headMove.rotation.set (0, a, 0) //*4 /* * Right armhand: * rotates about (local) X axis, * oscillates back and forth from -50 to +50 degrees, * 5 seconds (5*60 ticks) total for the 100 (ie 2*50) degree travel. * Must also remember current direction (increasing/decreasing). */ a = this.rightMove.rotation.x //*4 a += (this.rightMove.userData * 2*50 * Math.PI/180) / (5*60) //*5 if (Math.abs(a) > 50 * Math.PI/180) { //*5 this.rightMove.userData *= -1; //*5 } this.rightMove.rotation.set (a, 0, 0) //*4 /* * Left armhand: * oscillates -30 to +30 degrees in 1 second */ a = this.leftMove.rotation.x //*4 a += (this.leftMove.userData * 2*30 * Math.PI/180) / (1*60) //*5 if (Math.abs(a) > 30 * Math.PI/180) { //*5 this.leftMove.userData *= -1; //*5 } this.leftMove.rotation.set (a, 0, 0) //*4 } } /* * Separate object that makes the Arm + Hand assembly */ class ArmHand extends THREE.Object3D { constructor (shirt, skin) { super() // Arm let arm = new THREE.Mesh ( new THREE.BoxGeometry (0.5, 2.5, 0.5), shirt) arm.position.set (0, -1.25, 0) this.add (arm) // Finger let finger = new THREE.Mesh ( new THREE.BoxGeometry (0.1, .5, .5), skin) finger.position.set (-0.25+0.05, -2.5-0.25, 0) this.add (finger) // Thumb let thumb = new THREE.Mesh ( new THREE.BoxGeometry (0.1, .5, .5), skin) thumb.position.set (0.25-0.1-0.05, -2.5-0.25, 0.) this.add (thumb) } } /* * An object to hold the lights, same as previous */ class Lights { constructor (scene) { // Uses typical lighting setup, like portrait or TV studio... // Main (key) light, directional, // from 45 deg. user's right, above, bright white this.mainLight = new THREE.DirectionalLight ("white", 1 * 2*Math.PI) this.mainLight.position.set (1, 0.5, 1) scene.add (this.mainLight) // Fill light, directional, from 45deg. user's left, // white, half as bright this.fillLight = new THREE.DirectionalLight ("white", 0.5 * 2*Math.PI) this.fillLight.position.set (-1, 0, 1 ) scene.add (this.fillLight) // Ambient light, white, still less bright this.ambientLight = new THREE.AmbientLight ("white", 0.05 * 2*Math.PI) scene.add (this.ambientLight); } } /* * The rest of this is almost the same boilerplate as previously */ import * as THREE from "three"; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; class Main { constructor () { // Set up window for 3D this.renderer = new THREE.WebGLRenderer( { antialias: true } ); this.renderer.setClearColor( new THREE.Color ("lightgrey")) this.renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( this.renderer.domElement ); // Create our scene this.scene = new SceneGraph(); // Add our lights to the scene, we keep in a separate class new Lights(this.scene); // Create the camera this.camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 1000 ); this.camera.position.z = 5; // Create a camera contol new OrbitControls( this.camera, this.renderer.domElement ); // Start animation loop this.animate() // In case window is resized window.onresize = () => this.onResize() // Other initialization: when the mouse clicks, call our function // Call it with => syntax cause needs "this" // and with (event) arg cause needs "event" too document.addEventListener ("mousedown", //*6 Pick with mouse (event) => this.doMouse(event), false) //*6 } /* * Render the scene */ render() { this.renderer.render( this.scene, this.camera ); } /* * Animation loop: slightly different from previous boilerplate */ animate () { //*1 Animation loop requestAnimationFrame (() => {this.animate()}); //*1 this.scene.tick() //*1 this.render() //*1 } /* * Handle mouse picking */ doMouse (event) { //*6 // Take mouse coords, flip y, convert to (-1..+1) // Also note in HTML: <body style="margin: 0px"> let mouse = { //*6 x: ( event.clientX / window.innerWidth ) * 2 - 1, //*6 y: - ( event.clientY / window.innerHeight ) * 2 + 1 } //*6 // Set up for picking let raycaster = new THREE.Raycaster () //*6 raycaster.setFromCamera (mouse, this.camera) //*6 // Returns array of all objects in scene with which the ray intersects let intersects = raycaster.intersectObjects (this.scene.children, true); //*6 // If there were any intersections, take the first (ie nearest) if (intersects.length > 0) { //*6 intersects[0].object.material = //*6 new THREE.MeshPhongMaterial ({ color: "red" }) //*6 } } // In case window is resized onResize () { this.renderer.setSize( window.innerWidth, window.innerHeight ); this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.render() } } window.onload = () => new Main () </script> </head> </html>