<h1>UFO with Controls</h1>
<p> Current time: <span id="time"></span></p>
<ul>
<li>0: reset animation to initial state
<li>1: take one step
<li>g: "go" — start the animation <q>loop</q>
<li>SPC: stop the animation
</ul>
body {
max-width: 100%;
}
/* feel free to style the canvas any way you want. If you want it to
use the entire window, set width: 100% and height: 100%. */
canvas {
width: 80%;
height: 500px;
display: block;
margin: 10px auto;
}
TW.randomBell = function (center, range) {
// returns a sample from an approximately bell-shaped distribution with
// the input center and range of values
var i,
sum = 0;
for (i = 0; i < range; i++) {
sum += 2 * Math.random() - 1;
}
return center + sum;
};
var halfSize = 50; // half the extent of the scene
// global parameters (only a few are controlled in the gui)
var guiParams = {
ufoSize: 1, // used to scale UFO size - really 10x5x10
gunLength: 20,
torpedoVelocity: 100, // speed of torpedoes, set in gui
initX: -48, // initial position of the UFO
initY: 80, // height does not change in animation
initZ: -25,
initVx: 1.2, // initial velocity of UFO, set in gui
initVz: -0.3,
later: 20, // time when UFO velocity changes, set in gui
laterVx: 2.1, // later velocity, set in gui
laterVz: 1,
deltaT: 0.05 // time between steps, arbitrary time units, set in gui
};
// ================================================================
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
TW.mainInit(renderer, scene);
TW.cameraSetup(renderer, scene, {
minx: -halfSize,
maxx: halfSize,
miny: 0,
maxy: 90,
minz: -halfSize,
maxz: halfSize
});
var ufoObj; // container for the UFO (flattened sphere) and the gun
var gun; // just a THREE.Line object with two vertices
var photonTorpedo; // a tiny sphere
// changes the "to" vertex of the line to define the direction of aim of the gun
function aimGun(gun, dx, dz) {
var geom = gun.geometry;
var v1 = geom.vertices[1];
v1.set(dx, -guiParams.gunLength, dz);
geom.verticesNeedUpdate = true;
}
// creates a THREE.Line object from two vertices "from" and "to"
function line(from, to, color) {
var geom = new THREE.Geometry();
geom.vertices = [from, to];
var mat = new THREE.LineBasicMaterial({ color: color, linewidth: 2 });
var lineObj = new THREE.Line(geom, mat);
return lineObj;
}
// creates the scene, with barn, ground, UFO object with gun, and photon torpedo
function makeScene() {
// create barn and ground, and add to scene
var barn = new TW.createMesh(TW.createBarn(30, 40, 50));
scene.add(barn);
var ground = new THREE.Mesh(
new THREE.PlaneGeometry(2 * halfSize, 2 * halfSize),
new THREE.MeshBasicMaterial({ color: THREE.ColorKeywords.darkgreen })
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// create UFO object with gun, and add to scene
ufoObj = new THREE.Object3D();
var ufo = new THREE.Mesh(
new THREE.SphereGeometry(guiParams.ufoSize),
new THREE.MeshBasicMaterial({ color: THREE.ColorKeywords.purple })
);
ufo.scale.set(10, 5, 10); // real size of UFO
gun = line(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, -guiParams.gunLength, 0),
THREE.ColorKeywords.purple
);
ufoObj.add(ufo);
ufoObj.add(gun);
ufoObj.position.set(guiParams.initX, guiParams.initY, guiParams.initZ);
scene.add(ufoObj);
// create photon torpedo and add to scene
photonTorpedo = new THREE.Mesh(
new THREE.SphereGeometry(1),
new THREE.MeshBasicMaterial({ color: THREE.ColorKeywords.yellow })
);
photonTorpedo.visible = false;
scene.add(photonTorpedo);
}
makeScene();
// state variables of the animation
var animationState;
// reset animation to initial state
function resetAnimationState() {
animationState = {
time: 0,
x: guiParams.initX, // current position of UFO
z: guiParams.initZ,
vx: guiParams.initVx, // current velocity of UFO
vz: guiParams.initVz
};
ufoObj.position.x = animationState.x;
ufoObj.position.z = animationState.z;
document.getElementById("time").innerHTML = animationState.time;
}
// resets the animation to the initial state and renders the scene
function firstState() {
resetAnimationState();
TW.render();
}
// updates the state of the animation
function updateState() {
// changes the time and everything that depends on time
var dt = guiParams.deltaT;
animationState.time += dt;
document.getElementById("time").innerHTML = animationState.time.toFixed(3);
// change velocity after the time given by guiParams.later
// (actually sets them multiple times, but no harm)
if (animationState.time > guiParams.later) {
animationState.vx = guiParams.laterVx;
animationState.vz = guiParams.laterVz;
}
// change UFO location
animationState.x += animationState.vx * dt;
animationState.z += animationState.vz * dt;
ufoObj.position.x = animationState.x;
ufoObj.position.z = animationState.z;
// stop the animation when UFO reaches outer bounds of scene (generalizes to
// movement in +/- X and Z directions)
if (
animationState.x < -halfSize ||
animationState.x > +halfSize ||
animationState.z < -halfSize ||
animationState.z > +halfSize
) {
console.log("stop because out of bounds");
stopAnimation();
}
var time = animationState.time;
if (Math.floor(time) <= time && time <= Math.floor(time) + dt) {
// re-aim and fire a new photon torpedo every second - the code above tests
// for the beginning (first animation frame) of a second, assuming dt < 1
var torpedoDir = new THREE.Vector3(0, -guiParams.gunLength, 0);
torpedoDir.x = TW.randomBell(0, guiParams.gunLength);
torpedoDir.z = TW.randomBell(0, guiParams.gunLength);
// update where the gun is aiming
aimGun(gun, torpedoDir.x, torpedoDir.z);
animationState.photonSequence = 0;
var start = new THREE.Vector3();
start.copy(ufoObj.position);
animationState.torpedoStart = start;
animationState.torpedoStartTime = time;
animationState.torpedoDir = torpedoDir;
} else {
// draw a photon torpedo in the scene at Q = P + V * t
animationState.photonSequence++;
var P = animationState.torpedoStart;
// absolute time
var sinceStart = time - animationState.torpedoStartTime;
var Q = new THREE.Vector3();
var V = new THREE.Vector3();
V.copy(animationState.torpedoDir);
V.normalize();
V.multiplyScalar(guiParams.torpedoVelocity * sinceStart);
Q.addVectors(P, V);
photonTorpedo.position.copy(Q);
photonTorpedo.visible = true;
}
}
// performs one step of the animation
function oneStep() {
updateState();
TW.render();
}
var stopRequested = false; // set to true to have the animation stop itself
var animationId = null; // so we can cancel the animation if we want
// starts continuous animation loop
function animate() {
oneStep();
if (!stopRequested) {
animationId = requestAnimationFrame(animate);
}
}
function startAnimation() {
stopRequested = false;
if (animationId == null) {
animate();
}
}
// halts the animation
function stopAnimation() {
if (animationId != null) {
cancelAnimationFrame(animationId);
stopRequested = true;
animationId = null;
}
}
// setup keyboard callbacks
TW.setKeyboardCallback("0", firstState, "reset animation");
TW.setKeyboardCallback("1", oneStep, "advance by one step");
TW.setKeyboardCallback("g", startAnimation, "go: start animation");
TW.setKeyboardCallback(" ", stopAnimation, "stop animation");
// setup gui controls
// listen() method enables gui controller to be adjusted automatically if
// the code execution results in a change to the parameter
var gui = new dat.GUI();
gui.add(guiParams, "deltaT", 0.01, 0.99).step(0.01).listen();
gui.add(guiParams, "later", 1, 40).step(0.1).listen();
gui.add(guiParams, "torpedoVelocity", 50, 200).step(1).listen();
gui.add(guiParams, "initVx", 1, 10).step(0.1).listen();
gui.add(guiParams, "laterVx", 1, 10).step(0.1).listen();
firstState();
This Pen doesn't use any external CSS resources.