UNPKG

tangram

Version:
303 lines (246 loc) 12.2 kB
import Utils from '../utils/utils'; import ShaderProgram from '../gl/shader_program'; import {mat4, mat3, vec3} from '../utils/gl-matrix'; // Abstract base class export default class Camera { constructor(name, view, options = {}) { this.view = view; this.position = options.position; this.zoom = options.zoom; } // Create a camera by type name, factory-style static create(name, view, config) { switch (config.type) { case 'isometric': return new IsometricCamera(name, view, config); case 'flat': return new FlatCamera(name, view, config); case 'perspective': /* falls through */ default: return new PerspectiveCamera(name, view, config); } } // Update method called once per frame update() { } // Called once per frame per program (e.g. for main render pass, then for each additional pass for feature selection, etc.) setupProgram(/*program*/) { } // Sync camera position/zoom to scene view updateView () { if (this.position || this.zoom) { var view = {}; if (this.position) { view = { lng: this.position[0], lat: this.position[1], zoom: this.position[2] }; } if (this.zoom) { view.zoom = this.zoom; } this.view.setView(view); } } // Set model-view and normal matrices setupMatrices (matrices, program) { // Model view matrix - transform tile space into view space (meters, relative to camera) mat4.multiply(matrices.model_view32, this.view_matrix, matrices.model); program.uniform('Matrix4fv', 'u_modelView', matrices.model_view32); // Normal matrices - transforms surface normals into view space mat3.normalFromMat4(matrices.normal32, matrices.model_view32); mat3.invert(matrices.inverse_normal32, matrices.normal32); program.uniform('Matrix3fv', 'u_normalMatrix', matrices.normal32); program.uniform('Matrix3fv', 'u_inverseNormalMatrix', matrices.inverse_normal32); } } /** Perspective matrix projection This is a specialized perspective camera that, given a desired camera focal length (which can also vary by zoom level), constrains the camera height above the ground plane such that the displayed ground area of the map matches that of a traditional web mercator map. This means you can set the camera location by [lat, lng, zoom] as you would a typical web mercator map, then adjust the focal length as needed. Vanishing point can also be adjusted to achieve different "viewing angles", e.g. instead of looking straight down into the center of the viewport, the camera appears to be tilted at an angle. For example: [0, 0] = looking towards center of viewport [-250, -250] = looking 250 pixels from the viewport center to the lower-left corner [400, 0] = looking 400 pixels to the right of the viewport center */ class PerspectiveCamera extends Camera { constructor(name, view, options = {}) { super(name, view, options); this.type = 'perspective'; // a single scalar, or pairs of stops mapping zoom levels, e.g. [zoom, focal length] this.focal_length = options.focal_length; this.fov = options.fov; if (!this.focal_length && !this.fov) { // Default focal length ranges by zoom this.focal_length = [[16, 2], [17, 2.5], [18, 3], [19, 4], [20, 6]]; } this.vanishing_point = options.vanishing_point || [0, 0]; // [x, y] this.vanishing_point = this.vanishing_point.map(parseFloat); // we implicitly only support px units here this.vanishing_point_skew = []; this.position_meters = null; this.view_matrix = new Float64Array(16); this.projection_matrix = new Float32Array(16); // 'camera' is the name of the shader block, e.g. determines where in the shader this code is injected ShaderProgram.replaceBlock('camera', ` uniform mat4 u_projection; uniform vec3 u_eye; uniform vec2 u_vanishing_point; void cameraProjection (inout vec4 position) { position = u_projection * position; }` ); } // Constrains the camera so that the viewable area matches given the viewport height // (in world space, e.g. meters), given either a camera focal length or field-of-view // (focal length is used if both are passed). constrainCamera({ view_height, height, focal_length, fov }) { // Solve for camera height if (!height) { // We have focal length, calculate FOV if (focal_length) { fov = Math.atan(1 / focal_length) * 2; } // We have FOV, calculate focal length else if (fov) { fov = fov * Math.PI / 180; // convert FOV degrees to radians focal_length = 1 / Math.tan(fov / 2); } // Distance that camera should be from ground such that it fits the field of view expected // for a conventional web mercator map at the current zoom level and camera focal length height = view_height / 2 * focal_length; } // Solve for camera focal length / field-of-view else { focal_length = 2 * height / view_height; fov = Math.atan(1 / focal_length) * 2; } return { view_height, height, focal_length, fov }; } updateMatrices() { // TODO: only re-calculate these vars when necessary // Height of the viewport in meters at current zoom var viewport_height = this.view.size.css.height * this.view.meters_per_pixel; // Compute camera properties to fit desired view var { height, fov } = this.constrainCamera({ view_height: viewport_height, focal_length: Utils.interpolate(this.view.zoom, this.focal_length), fov: Utils.interpolate(this.view.zoom, this.fov) }); // View matrix var position = [this.view.center.meters.x, this.view.center.meters.y, height]; this.position_meters = position; // mat4.lookAt(this.view_matrix, // vec3.fromValues(...position), // vec3.fromValues(position[0], position[1], height - 1), // vec3.fromValues(0, 1, 0)); // Exclude camera height from view matrix mat4.lookAt(this.view_matrix, vec3.fromValues(position[0], position[1], 0), vec3.fromValues(position[0], position[1], -1), vec3.fromValues(0, 1, 0)); // Projection matrix mat4.perspective(this.projection_matrix, fov, this.view.aspect, 1, height * 2); // Convert vanishing point from pixels to viewport space this.vanishing_point_skew[0] = this.vanishing_point[0] / this.view.size.css.width; this.vanishing_point_skew[1] = this.vanishing_point[1] / this.view.size.css.height; // Adjust projection matrix to include vanishing point skew this.projection_matrix[8] = -this.vanishing_point_skew[0] * 2; // z column of x row, e.g. amount z skews x this.projection_matrix[9] = -this.vanishing_point_skew[1] * 2; // z column of y row, e.g. amount z skews y // Translate geometry into the distance so that camera is appropriate height above ground // Additionally, adjust xy to compensate for any vanishing point skew, e.g. move geometry so that the displayed g // plane of the map matches that expected by a traditional web mercator map at this [lat, lng, zoom]. mat4.translate(this.projection_matrix, this.projection_matrix, vec3.fromValues( viewport_height/2 * this.view.aspect * (-this.vanishing_point_skew[0] * 2), viewport_height/2 * (-this.vanishing_point_skew[1] * 2), 0 ) ); // Include camera height in projection matrix mat4.translate(this.projection_matrix, this.projection_matrix, vec3.fromValues(0, 0, -height)); } update() { super.update(); this.updateMatrices(); } setupProgram(program) { program.uniform('Matrix4fv', 'u_projection', this.projection_matrix); program.uniform('3f', 'u_eye', [0, 0, this.position_meters[2]]); program.uniform('2fv', 'u_vanishing_point', this.vanishing_point_skew); } } // Isometric-style projection // Note: this is actually an "axonometric" projection, but I'm using the colloquial term isometric because it is more recognizable. // An isometric projection is a specific subset of axonometric projections. // 'axis' determines the xy skew applied to a vertex based on its z coordinate, e.g. [0, 1] axis causes buildings to be drawn // straight upwards on screen at their true height, [0, .5] would draw them up at half-height, [1, 0] would be sideways, etc. class IsometricCamera extends Camera { constructor(name, view, options = {}) { super(name, view, options); this.type = 'isometric'; this.axis = options.axis || { x: 0, y: 1 }; if (this.axis.length === 2) { this.axis = { x: this.axis[0], y: this.axis[1] }; // allow axis to also be passed as 2-elem array } this.position_meters = null; this.viewport_height = null; this.view_matrix = new Float64Array(16); this.projection_matrix = new Float32Array(16); // 'camera' is the name of the shader block, e.g. determines where in the shader this code is injected ShaderProgram.replaceBlock('camera', ` uniform mat4 u_projection; uniform vec3 u_eye; uniform vec2 u_vanishing_point; void cameraProjection (inout vec4 position) { position = u_projection * position; // position.xy += position.z * u_isometric_axis; // Reverse z for depth buffer so up is negative, // and scale down values so objects higher than one screen height will not get clipped // pull forward slightly to avoid going past far clipping plane position.z = -position.z / 100. + 1. - 0.001; }` ); } update() { super.update(); this.viewport_height = this.view.size.css.height * this.view.meters_per_pixel; var position = [this.view.center.meters.x, this.view.center.meters.y, this.viewport_height]; this.position_meters = position; // View mat4.identity(this.view_matrix); mat4.translate(this.view_matrix, this.view_matrix, vec3.fromValues(-position[0], -position[1], 0)); // Projection mat4.identity(this.projection_matrix); // apply isometric skew this.projection_matrix[8] = this.axis.x / this.view.aspect; // z column of x row, e.g. amount z skews x this.projection_matrix[9] = this.axis.y; // z column of x row, e.g. amount z skews y // convert meters to viewport mat4.scale(this.projection_matrix, this.projection_matrix, vec3.fromValues( 2 / this.view.size.meters.x, 2 / this.view.size.meters.y, 2 / this.view.size.meters.y ) ); } setupProgram(program) { program.uniform('Matrix4fv', 'u_projection', this.projection_matrix); program.uniform('3fv', 'u_eye', [0, 0, this.viewport_height]); // program.uniform('3f', 'u_eye', this.viewport_height * this.axis.x, this.viewport_height * this.axis.y, this.viewport_height); program.uniform('2fv', 'u_vanishing_point', [0, 0]); } } // Flat projection (e.g. just top-down, no perspective) - a degenerate isometric camera class FlatCamera extends IsometricCamera { constructor(name, view, options = {}) { super(name, view, options); this.type = 'flat'; } update() { // Axis is fixed to (0, 0) for flat camera this.axis.x = 0; this.axis.y = 0; super.update(); } }