UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

156 lines (138 loc) 6.18 kB
/** * LittleJS Three.js Plugin * - Renders a three.js scene on a canvas behind the LittleJS canvases * - The three.js module is passed in by the user, nothing is bundled * - Keep canvasClearColor transparent so the 3D scene shows through * - Aligned camera mode locks the 3D camera to the LittleJS 2D camera * - ThreeJSObject lets LittleJS physics drive a three.js mesh * - Call new ThreeJSPlugin(THREE) in gameInit to set up * @namespace ThreeJS */ 'use strict'; /////////////////////////////////////////////////////////////////////////////// /** Global ThreeJS plugin object * @type {ThreeJSPlugin} * @memberof ThreeJS */ let threeJS; /////////////////////////////////////////////////////////////////////////////// /** * ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas * @example * // in gameInit, with three.js loaded by the user * new ThreeJSPlugin(THREE); * threeJS.scene.add(new THREE.AmbientLight); * @memberof ThreeJS */ class ThreeJSPlugin { /** Set up the three.js rendering layer, call in gameInit * @param {Object} THREE - The three.js module, supplied by the user * @param {number} [cameraFOV] - Vertical field of view in degrees */ constructor(THREE, cameraFOV=60) { ASSERT(!threeJS, 'ThreeJS plugin already initialized'); threeJS = this; if (headlessMode) return; ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit'); ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in'); /** @property {Object} - The three.js module passed into the constructor */ this.THREE = THREE; /** @property {Object} - The three.js renderer */ this.renderer = new THREE.WebGLRenderer({antialias: true}); /** @property {Object} - The three.js scene, add lights and meshes here */ this.scene = new THREE.Scene(); /** @property {Object} - The three.js perspective camera */ this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3); /** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */ this.cameraAlign2D = true; // insert the canvas below the engine canvases and match the layout const threeCanvas = this.renderer.domElement; const rootElement = mainCanvas.parentElement; rootElement.insertBefore(threeCanvas, rootElement.firstChild); threeCanvas.style.cssText = mainCanvas.style.cssText; // render automatically each frame after the engine renders engineAddPlugin(undefined, ()=> this.render()); } /** Position the camera so the z=0 plane exactly matches LittleJS world space, * called automatically when cameraAlign2D is set */ alignCamera2D() { const halfHeight = mainCanvasSize.y / 2 / cameraScale; // half visible height in world units const distance = halfHeight / tan(this.camera.fov/2 * PI/180); this.camera.position.set(cameraPos.x, cameraPos.y, distance); // reset all axes in case a free camera was used, littlejs angles are clockwise this.camera.rotation.set(0, 0, -cameraAngle); } /** Sync the canvas layout and render the scene, called automatically each frame */ render() { if (!this.renderer) return; // headless mode // keep renderer size and css in sync with the LittleJS canvas const threeCanvas = this.renderer.domElement; if (threeCanvas.width != mainCanvasSize.x || threeCanvas.height != mainCanvasSize.y) { this.renderer.setSize(mainCanvasSize.x, mainCanvasSize.y, false); this.camera.aspect = mainCanvasSize.x / mainCanvasSize.y; this.camera.updateProjectionMatrix(); } if (threeCanvas.style.cssText != mainCanvas.style.cssText) threeCanvas.style.cssText = mainCanvas.style.cssText; if (this.cameraAlign2D) this.alignCamera2D(); this.renderer.render(this.scene, this.camera); } } /////////////////////////////////////////////////////////////////////////////// /** * ThreeJS Object - EngineObject that drives a three.js mesh * - LittleJS physics moves the object and the mesh follows automatically * - Destroying the object removes the mesh from the scene * @extends EngineObject * @memberof ThreeJS */ class ThreeJSObject extends EngineObject { /** Create an engine object that drives a three.js mesh * @param {Vector2} [pos] - World space position * @param {Vector2} [size] - World space size * @param {Object} [mesh] - The three.js object3d to drive * @param {number} [z] - Mesh height above the 2D plane */ constructor(pos, size, mesh, z=0) { super(pos, size); ASSERT(threeJS, 'ThreeJS plugin must be initialized first'); /** @property {Object} - The three.js object3d this object drives */ this.mesh = mesh; /** @property {number} - Mesh height above the 2D plane */ this.z = z; if (mesh) { threeJS.scene.add(mesh); this.syncMesh(); } } /** Update the object and sync the mesh to its transform */ update() { super.update(); this.syncMesh(); } /** Copy this object's transform to the mesh */ syncMesh() { if (!this.mesh) return; this.mesh.position.set(this.pos.x, this.pos.y, this.z); this.mesh.rotation.z = -this.angle; // littlejs angles are clockwise } /** The mesh is this object's visual, the default 2D rendering is skipped */ render() {} /** Destroy this object and remove its mesh from the scene * @param {boolean} [immediate] */ destroy(immediate) { if (this.destroyed) return; // note: frequently destroyed objects should also dispose geometry and material this.mesh && threeJS.scene.remove(this.mesh); super.destroy(immediate); } }