UNPKG

fury-js

Version:

WebGL Game Engine

199 lines (182 loc) 7.63 kB
const Maths = require('./maths'); const vec3 = Maths.vec3, vec4 = Maths.vec4, mat4 = Maths.mat4, quat = Maths.quat; module.exports = (function() { // NOTE: Camera points in -z direction let exports = {}; let Type = exports.Type = { Perspective: "Perspective", Orthonormal: "Orthonormal" }; // vec3 cache for calculations let localX = vec3.create(); let localY = vec3.create(); let localZ = vec3.create(); let vec3Cache = vec3.create(); let vec4Cache = vec4.create(); let q = quat.create(); let prototype = { // Set Rotation from Euler // Set Position x, y, z // Note do not have enforced copy setters, the user is responsible for this // TODO: Review depth and frustrum to make sure they deal with look in -z correctly calculateFrustum: function() { // TODO: Update to work for orthonormal projection as well Maths.quat.localAxes(this.rotation, localX, localY, localZ); // Calculate Planes // NOTE: Relies on the fact camera looks in -ve z direction // Note Right Handed but facing in negative z, so -x is left, and +x is right. // Planes should point inwards // Near vec3.negate(this.planes[0], localZ); // Set Normal vec3.scaleAndAdd(vec3Cache, this.position, localZ, -this.near); // Calculate mid-point of plane this.planes[0][3] = -vec3.dot(this.planes[0], vec3Cache); // Set [3] to distance from plane to origin along normal (normal is pointing torwards origin) // Far vec3.copy(this.planes[1], localZ); vec3.scaleAndAdd(vec3Cache, this.position, localZ, -this.far); this.planes[1][3] = -vec3.dot(this.planes[1], vec3Cache); // Left quat.identity(q); Maths.quat.rotate(q, q, 0.5 * this.ratio * this.fov, localY); // Rotation is anti-clockwise apparently vec3.transformQuat(this.planes[2], localX, q); this.planes[2][3] = -vec3.dot(this.planes[2], this.position); // Right quat.identity(q); Maths.quat.rotate(q, q, -0.5 * this.ratio * this.fov, localY); vec3.negate(vec3Cache, localX); vec3.transformQuat(this.planes[3], vec3Cache, q); this.planes[3][3] = -vec3.dot(this.planes[3], this.position); // Top quat.identity(q); Maths.quat.rotate(q, q, 0.5 * this.fov, localX); vec3.negate(vec3Cache, localY); vec3.transformQuat(this.planes[4], vec3Cache, q); this.planes[4][3] = -vec3.dot(this.planes[4], this.position); // Bottom quat.identity(q); Maths.quat.rotate(q, q, -0.5 * this.fov, localX); vec3.transformQuat(this.planes[5], localY, q); this.planes[5][3] = -vec3.dot(this.planes[5], this.position); // TODO: The points too please so we can improve culling }, isSphereInFrustum: function(center, radius) { vec4Cache[3] = 1; for (let i = 0; i < 6; i++) { // We want the point center + normal of the plane * radius vec3.scaleAndAdd(vec4Cache, center, this.planes[i], radius); if (vec4.dot(this.planes[i], vec4Cache) < 0) { return false; } } return true; }, isInFrustum: function(bounds) { // https://iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm // Note : https://stackoverflow.com/questions/31788925/correct-frustum-aabb-intersection // TODO: Profile and try different techniques (using continue in the loop, unrolling the lot, etc) vec4Cache[3] = 1; // Consider wrapping this cache in an anon function execution to keep scope minimal, see of it improves performance // i.e. isInFrustum = (function() { let cache = vec4.create(); return function(bounds) { /* implementation */ }; })(); for (let i = 0; i < 6; i++) { let out = 0; vec4Cache[0] = bounds.min[0], vec4Cache[1] = bounds.max[1], vec4Cache[2] = bounds.min[2]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // min max min vec4Cache[1] = bounds.min[1]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // min min min vec4Cache[0] = bounds.max[0]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // max min min vec4Cache[1] = bounds.max[1]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // max max min vec4Cache[2] = bounds.max[2]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // max max max vec4Cache[1] = bounds.min[1]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // max min max vec4Cache[0] = bounds.min[0]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // min min max vec4Cache[1] = bounds.max[1]; out += (vec4.dot(this.planes[i], vec4Cache) < 0) ? 1 : 0; // min max max if (out == 8) { return false; } } // TODO: Add check of points too return true; }, getDepth: function(position) { let p0 = this.position[0], p1 = this.position[1], p2 = this.position[2], q0 = this.rotation[0], q1 = this.rotation[1], q2 = this.rotation[2], q3 = this.rotation[3], l0 = position[0], l1 = position[1], l2 = position[2]; return 2*(q1*q3 + q0*q2)*(l0 - p0) + 2*(q2*q3 - q0*q1)*(l1 - p1) + (1 - 2*q1*q1 - 2*q2*q2)*(l2 - p2); }, getLookDirection: function(out) { vec3.transformQuat(out, Maths.vec3.Z, this.rotation); vec3.negate(out, out); // Camera faces in -z }, getProjectionMatrix: function(out) { if(this.type == Type.Perspective) { mat4.perspective(out, this.fov, this.ratio, this.near, this.far); } else { let left = - (this.height * this.ratio) / 2.0; let right = - left; let top = this.height / 2.0; let bottom = -top; mat4.ortho(out, left, right, bottom, top, this.near, this.far); } return out; }, viewportToWorld: function(out, viewPort, z) { // Viewport measured from top-left if(this.type == Type.Orthonormal) { out[0] = (this.height * this.ratio) * (viewPort[0] - 0.5); out[1] = this.height * (0.5 - viewPort[1]); out[2] = (z || 0); vec3.transformQuat(out, out, this.rotation); vec3.add(out, out, this.position); } else { let zDist = z || this.near; let planeHeight = 2 * zDist * Math.tan(0.5 * this.fov); let planeWidth = planeHeight * this.ratio; out[0] = planeWidth * (viewPort[0] - 0.5); out[1] = planeHeight * (0.5 - viewPort[1]); out[2] = -zDist; vec3.transformQuat(out, out, this.rotation); vec3.add(out, out, this.position); } } }; exports.create = function(config) { let camera = Object.create(prototype); let { type = Type.Perspective, near, far, ratio = 1.0, clear = true, clearColor } = config; camera.type = type; camera.near = near; camera.far = far; camera.ratio = ratio; camera.clear = clear; camera.clearColor = clearColor; switch (type) { case Type.Perspective: // vertical field of view, ratio (aspect) determines horizontal fov camera.fov = config.fov; break; case Type.Orthonormal: camera.height = config.height; break; default: throw new Error("Unrecognised Camera Type '" + type + "'"); } let { position = vec3.create(), rotation = quat.create() } = config; camera.position = position; camera.rotation = rotation; camera.planes = []; // Stored as plane normal, distance from plane to origin in direction of normal for (let i = 0; i < 6; i++) { camera.planes[i] = vec4.create(); } camera.points = []; for (let i = 0; i < 8; i++) { camera.points[i] = vec3.create(); } // TODO: Arguably post-processing effects and target could/should be on the camera, the other option is on the scene return camera; }; return exports; })();