/*jshint esversion: 6 */
// @ts-check
/**
* CS559 3D World Framework Code
*
* GrWorld - bascially a wrapper around scene, except that it uses
* GrObject instead of Object3D (GrObjects have Object3D)
*
* To make things simple, this keeps a renderer and a default camera.
* Of course, that might complicate things if you want to have multiple
* renderers and cameras
*
* Basically, this keeps some of the basic stuff you need when you use
* three.
*/
/** @module GrWorld */
// we need to have the BaseClass definition
import { GrObject } from "./GrObject.js";
import { insertElement } from "../Libs/inputHelpers.js";
import { SimpleGroundPlane } from "./GroundPlane.js";
// these four lines fake out TypeScript into thinking that THREE
// has the same type as the T.js module, so things work for type checking
// type inferencing figures out that THREE has the same type as T
// and then I have to use T (not THREE) to avoid the "UMD Module" warning
/** @type typeof import("./../THREE/threets/index"); */
let T;
// @ts-ignore
T = THREE;
/**
* Document the parameters for making a world - all are optional
* we'll guess at something if you don't give it to us
* @typedef GrWorldProperties
* @property [camera] - use this camera if passed
* @property [fov] - camera property if we make one
* @property [near] - camera property if we make one
* @property [far] - camera property if we make one
* @property [renderer] - if you don't give one, we'll make it
* @property [renderparams] - parameters for making the renderer
* @property [width] - canvas size
* @property [height] - canvas size
* @property [where] - where in the DOM to insert things
* @property [controls] - whether or not to make orbit controls (default is only if we make the camera)
* @property [lights] - a list of lights, or else default ones are made
* @property [groundplane] - can be a groundplane, or False (otherwise, one gets made)
* @property [groundplanecolor="green"] - if we create a ground plane, what color
* @property [groundplanesize=5] - if we create a ground plane, how big
* @property [lookfrom] - where to put the camera (only if we make the camera)
* @property [lookat] - where to have the camera looking (only if we make the camera)
*/
/** @class GrWorld
*
* The GrWorld is basically a wrapper around THREE.js's `scene`,
* except that it keeps a list of `GrObject` rather than `Object3D`.
*
* It contains a `scene` (and it puts things into it for you).
* It also contains a `renderer` and a `camera`.
*
* When this creates a renderer, it places it into the dom (see the `where` option).
*
* By default, the world is created with a reasonable default `renderer`,
* `camera` and `groundplane`. Orbit controls are installed.
*/
export class GrWorld {
/**
* Construct an empty world
* @param {GrWorldProperties} params
*/
constructor(params={}) {
// this keeps a list of objects in the world
/** @type Array<GrObject> */
this.objects = [];
// the GrWorld "has a" of the main things we need in three
this.scene = new T.Scene();
// make a renderer if it isn't given
/** @type THREE.WebGLRenderer */
this.renderer = "renderer" in params ? params.renderer :
new T.WebGLRenderer("renderparams" in params ? params.renderparams : {} );
// width and height are tricky, since they can come from many places
let width = 600;
let height= 400;
// if the renderer was given, get its DOM
if ("renderer" in params) {
width = params.renderer.domElement.width;
height = params.renderer.domElement.height;
} else if (("renderparams" in params) && ("canvas" in params.renderparams)) {
width = params.renderparams.canvas.width;
height = params.renderparams.canvas.height;
}
// specified width/height overrides everything
if ("width" in params) { width = params.width;}
if ("height" in params) { height = params.height;}
// make things be the right size
this.renderer.setSize(width,height);
// make a groundplane (or install a given one)
// we do this before we made the camera, since it's useful for placing the camera
if ("groundplane" in params) {
this.groundplane = params.groundplane;
} else {
// the default is to create a groundplane
this.groundplane = new SimpleGroundPlane(params.groundplanesize || 5, 0.2, params.groundplanecolor || "darkgreen");
this.add(this.groundplane);
}
// make a camera
/** @type THREE.PerspectiveCamera */
this.camera = undefined;
if ("camera" in params) {
this.camera = params.camera;
} else {
this.camera = new T.PerspectiveCamera(
("fov" in params) ? params.fov : 45,
width/height,
("near" in params) ? params.near : 0.1,
("far" in params) ? params.far : 2000
);
/* figure out a default lookat */
let lookat = params.lookat;
if (!("lookat" in params)) {
lookat = new T.Vector3(0,0,0);
}
let lookfrom = params.lookfrom;
if (!("lookat" in params)) {
let gpSize = this.groundplane ? this.groundplane.size : 10;
lookfrom = new T.Vector3(gpSize/2,gpSize,gpSize*2);
}
this.camera.position.copy(lookfrom);
this.camera.lookAt(lookat);
}
// if we either made the camera or controls are specified...
this.controls = (!("camera" in params) || ("controls" in params)) ?
new T.OrbitControls(this.camera,this.renderer.domElement) : undefined;
// if we either specify where things go in the DOM or we made our
// own canvas, install it
if ( ("where" in params) || ( !("renderer" in params) || ("renderparams" in params))) {
insertElement(this.renderer.domElement, ("where" in params) ? params.where : undefined);
}
/**
* Some Lights
*/
if ("lights" in params) {
if (params.lights.length) {
params.lights.forEach(light => this.scene.add(light));
}
} else {
this.scene.add(new T.AmbientLight("white",0.2));
// three lights - all a little off white to give some contrast
let dirLight1 = new T.DirectionalLight(0xF0E0D0,0.6);
dirLight1.position.set(1,1,0);
this.scene.add(dirLight1);
let dirLight2 = new T.DirectionalLight(0xD0E0F0,0.6);
dirLight2.position.set(-1,1,-0.2);
this.scene.add(dirLight2);
let bottomLight = new T.DirectionalLight(0x0806080,0.2);
bottomLight.position.set(0,-1,0.1);
this.scene.add(bottomLight);
}
// Keep track of rendering timings
this.lastRenderTime = 0;
this.lastTimeOfDay = 12;
} // end of constructor
add(grobj) {
this.objects.push(grobj);
// be sure to add all the objects to the scene
grobj.objects.forEach(element => {
this.scene.add(element);
});
}
/**
* draw the default camera to the default renderer
*/
draw() {
this.lastRenderTime = performance.now();
this.renderer.render(this.scene,this.camera);
}
/**
* advance all of the objects
*/
advance(delta, timeOfDay) {
this.objects.forEach(obj => obj.advance(delta,timeOfDay));
}
/**
* perform a cycle of the animation loop - this measures the time since the
* last redraw and advances that much before redrawing
*/
animate() {
let delta = performance.now() - this.lastRenderTime;
this.advance(delta,this.lastTimeOfDay);
this.draw();
}
/**
* start an (endless) animation loop - this just keeps going
*/
go() {
// remember, this gets redefined (it doesn't follow scope rules)
let self=this;
function loop() {
self.animate();
self.draw();
window.requestAnimationFrame(loop);
}
loop();
}
}