import Screen from './screen' import { Node } from './components' import { glMatrix, mat4, vec2, vec3, vec4 } from 'gl-matrix' const SPEED = 100.0 const SENSITIVTY = 100.0 const FOV = 45.0 const ZNEAR = 0.1 const ZFAR = 10000.0 class Frustum { constructor () { this.planes = [] } construct (projectionMatrix) { const me = projectionMatrix this.planes = [ vec4.normalize([], [me[3] - me[0], me[7] - me[4], me[11] - me[8], me[15] - me[12]]), vec4.normalize([], [me[3] + me[0], me[7] + me[4], me[11] + me[8], me[15] + me[12]]), vec4.normalize([], [me[3] + me[1], me[7] + me[5], me[11] + me[9], me[15] + me[13]]), vec4.normalize([], [me[3] - me[1], me[7] - me[5], me[11] - me[9], me[15] - me[13]]), vec4.normalize([], [me[3] - me[2], me[7] - me[6], me[11] - me[10], me[15] - me[14]]), vec4.normalize([], [me[3] + me[2], me[7] + me[6], me[11] + me[10], me[15] + me[14]]) ] } // Calculates the closest distance from a given point to a given clipping plane distanceToPlane (plane, point) { return vec3.dot(this.planes[plane], point) + this.planes[plane][3] } containsPoint (point) { // For each plane; return outside if the point is behind the plane for (let i = 0; i < 6; i++) { if (this.distanceToPlane(i, point) <= 0.0) return false } // Return inside return true } containsSphere (position, radius) { // Plane counter let planeCount = 0 // Use the point-to-plane distance to calculate the number of planes the sphere is in front of for (let i = 0; i < 6; i++) { const distance = this.distanceToPlane(i, position) if (distance <= -radius) { return 0 } else if (distance > radius) { planeCount++ } } // Return inside if in front of all planes; otherwise intersecting return planeCount === 6 ? 1 : 2 } containsBox (min, max) { // Build a vector holding all box corners const points = [] for (let i = 0; i < 8; i++) { points.push([i < 4 ? max[0] : min[0], i % 4 < 2 ? max[1] : min[1], i % 2 ? max[2] : min[2]]) } // Test the box as a polygon return this.containsPolygon(points) } containsPolygon (points) { // Plane counter let planeCount = 0 // Use the point-to-plane distance to calculate the number of planes the polygon is in front of for (let i = 0; i < 6; i++) { let pointCount = 0 for (let j = 0; j < points.length; j++) { if (this.distanceToPlane(i, points[j]) > 0.0) { pointCount++ } } if (pointCount === 0) { return 0 } else if (pointCount === points.length) { planeCount++ } } // Return inside if in front of all planes; otherwise intersecting return planeCount === 6 ? 1 : 2 } } class Camera extends Node { constructor (pos, rotation) { super(pos, null, rotation) this.fov = FOV this.speed = SPEED this.sensitivity = SENSITIVTY // Create an empty projection matrix this.projection = mat4.create() // Helping vectors for calculating the view matrix this.up = vec3.create() this.front = vec3.fromValues(0.0, 0.0, -1.0) this.right = vec3.create() this.worldUp = vec3.fromValues(0.0, 1.0, 0.0) this.nearPlane = ZNEAR this.farPlane = ZFAR // Frustum planes this.frustum = new Frustum() this.updateTransform() } processKeyboard (direction, delta) { const newSpeed = this.speed * delta const velocity = vec3.fromValues(newSpeed, newSpeed, newSpeed) const vec = vec3.create() if (direction === 0) { vec3.multiply(vec, this.front, velocity) vec3.add(this.pos, this.pos, vec) } if (direction === 1) { vec3.multiply(vec, this.front, velocity) vec3.sub(this.pos, this.pos, vec) } if (direction === 2) { vec3.multiply(vec, this.right, velocity) vec3.sub(this.pos, this.pos, vec) } if (direction === 3) { vec3.multiply(vec, this.right, velocity) vec3.add(this.pos, this.pos, vec) } this.frustum.construct(mat4.multiply([], this.projection, this.view)) } processMouseMove (offset, constrain = true) { const fst = vec2.fromValues(offset.x * -this.sensitivity, offset.y * this.sensitivity) this.rotation[0] += glMatrix.toRadian(fst[0]) this.rotation[1] += glMatrix.toRadian(fst[1]) // Make sure that when pitch is out of bounds, screen doesn't get flipped if (constrain) { if (this.rotation[1] > glMatrix.toRadian(89.0)) { this.rotation[1] = glMatrix.toRadian(89.0) } if (this.rotation[1] < -glMatrix.toRadian(89.0)) { this.rotation[1] = -glMatrix.toRadian(89.0) } } this.updateTransform() } // Calculate the vertices required for the view matrix updateTransform () { // Prevent premature call (from super class) if (!this.front || !this.worldUp) return // Calculate the new Front vector const front = vec3.create() front[0] = Math.cos(this.rotation[0]) * Math.cos(this.rotation[1]) front[1] = Math.sin(this.rotation[1]) front[2] = Math.sin(this.rotation[0]) * Math.cos(this.rotation[1]) vec3.normalize(this.front, front) // Also re-calculate the Right and Up vector // Normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement. const rightCross = vec3.create() const upCross = vec3.create() vec3.cross(rightCross, this.front, this.worldUp) vec3.normalize(this.right, rightCross) vec3.cross(upCross, this.right, this.front) vec3.normalize(this.up, upCross) this.frustum.construct(mat4.multiply([], this.projection, this.view)) } updateProjection (gl) { mat4.perspective(this.projection, this.fov, Screen.aspectRatio, this.nearPlane, this.farPlane) this.updateTransform() } // Calculate the view matrix on-the-go // Really no advantage in storing this get view () { const mat = mat4.create() const center = vec3.create() vec3.add(center, this.pos, this.front) mat4.lookAt(mat, this.pos, center, this.up) return mat } // Override the default draw method because we don't need to draw the camera, // instead set the projection and view matrices draw (gl, shader) { const projloc = shader.getUniformLocation(gl, 'uProjectionMatrix') const viewloc = shader.getUniformLocation(gl, 'uViewMatrix') gl.uniformMatrix4fv(projloc, false, this.projection) gl.uniformMatrix4fv(viewloc, false, this.view) } } export default Camera