<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" &mdash; 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();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://s3.amazonaws.com/m.mr-pc.org/t/cisc3620/2020sp/three.min.js
  2. https://s3.amazonaws.com/m.mr-pc.org/t/cisc3620/2020sp/OrbitControls.js
  3. https://s3.amazonaws.com/m.mr-pc.org/t/cisc3620/2020sp/tw.js
  4. https://s3.amazonaws.com/m.mr-pc.org/t/cisc3620/2020sp/dat.gui.min.js