robot2: robot2.html

<!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>
[download file]