<!DOCTYPE html>
<html>
<head>
<title>CS 86 example: Robot, with animation and picking</title>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.181.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.181.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", 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", 1 * 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.1 * 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)) //*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>