pannellum-react
Version:
Pannellum React Component
1,250 lines (1,136 loc) • 52.1 kB
JavaScript
/*
* libpannellum - A WebGL and CSS 3D transform based Panorama Renderer
* Copyright (c) 2012-2018 Matthew Petroff
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
window.libpannellum = function (window, document, undefined) {
/**
* Creates a new panorama renderer.
* @constructor
* @param {HTMLElement} container - The container element for the renderer.
*/
function Renderer(container) {
var canvas = document.createElement('canvas');
canvas.style.width = canvas.style.height = '100%';
container.appendChild(canvas);
var program, gl, vs, fs;
var fallbackImgSize;
var world;
var vtmps;
var pose;
var image, imageType, dynamic;
var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf;
var globalParams;
/**
* Initialize renderer.
* @memberof Renderer
* @instance
* @param {Image|Array|Object} image - Input image; format varies based on
* `imageType`. For `equirectangular`, this is an image; for
* `cubemap`, this is an array of images for the cube faces in the
* order [+z, +x, -z, -x, +y, -y]; for `multires`, this is a
* configuration object.
* @param {string} imageType - The type of the image: `equirectangular`,
* `cubemap`, or `multires`.
* @param {boolean} dynamic - Whether or not the image is dynamic (e.g. video).
* @param {number} haov - Initial horizontal angle of view.
* @param {number} vaov - Initial vertical angle of view.
* @param {number} voffset - Initial vertical offset angle.
* @param {function} callback - Load callback function.
* @param {Object} [params] - Other configuration parameters (`horizonPitch`, `horizonRoll`, `backgroundColor`).
*/
this.init = function (_image, _imageType, _dynamic, haov, vaov, voffset, callback, params) {
// Default argument for image type
if (_imageType === undefined) {
_imageType = 'equirectangular';
}
if (_imageType != 'equirectangular' && _imageType != 'cubemap' && _imageType != 'multires') {
console.log('Error: invalid image type specified!');
throw { type: 'config error' };
}
imageType = _imageType;
image = _image;
dynamic = _dynamic;
globalParams = params || {};
// Clear old data
if (program) {
if (vs) {
gl.detachShader(program, vs);
gl.deleteShader(vs);
}
if (fs) {
gl.detachShader(program, fs);
gl.deleteShader(fs);
}
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
if (program.texture) {
gl.deleteTexture(program.texture);
}
if (program.nodeCache) {
for (var i = 0; i < program.nodeCache.length; i++) {
gl.deleteTexture(program.nodeCache[i].texture);
}
}
gl.deleteProgram(program);
program = undefined;
}
pose = undefined;
var s;
var faceMissing = false;
var cubeImgWidth;
if (imageType == 'cubemap') {
for (s = 0; s < 6; s++) {
if (image[s].width > 0) {
if (cubeImgWidth === undefined) {
cubeImgWidth = image[s].width;
}
if (cubeImgWidth != image[s].width) {
console.log('Cube faces have inconsistent widths: ' + cubeImgWidth + ' vs. ' + image[s].width);
}
} else {
faceMissing = true;
}
}
}
function fillMissingFaces(imgSize) {
if (faceMissing) {
// Fill any missing fallback/cubemap faces with background
var nbytes = imgSize * imgSize * 4; // RGB, plus non-functional alpha
var imageArray = new Uint8ClampedArray(nbytes);
var rgb = params.backgroundColor ? params.backgroundColor : [0, 0, 0];
rgb[0] *= 255;
rgb[1] *= 255;
rgb[2] *= 255;
// Maybe filling could be done faster, see e.g. https://stackoverflow.com/questions/1295584/most-efficient-way-to-create-a-zero-filled-javascript-array
for (var i = 0; i < nbytes; i++) {
imageArray[i++] = rgb[0];
imageArray[i++] = rgb[1];
imageArray[i++] = rgb[2];
}
var backgroundSquare = new ImageData(imageArray, imgSize, imgSize);
for (s = 0; s < 6; s++) {
if (image[s].width == 0) {
image[s] = backgroundSquare;
}
}
}
}
// This awful browser specific test exists because iOS 8/9 and IE 11
// don't display non-power-of-two cubemap textures but also don't
// throw an error (tested on an iPhone 5c / iOS 8.1.3 / iOS 9.2 /
// iOS 10.3.1).
// Therefore, the WebGL context is never created for these browsers for
// NPOT cubemaps, and the CSS 3D transform fallback renderer is used
// instead.
if (!(imageType == 'cubemap' && (cubeImgWidth & cubeImgWidth - 1) !== 0 && (navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/) || navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 9_/) || navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 10_/) || navigator.userAgent.match(/Trident.*rv[ :]*11\./)))) {
// Enable WebGL on canvas
if (!gl) {
gl = canvas.getContext('experimental-webgl', { alpha: false, depth: false });
}
if (gl && gl.getError() == 1286) {
handleWebGLError1286();
}
}
// If there is no WebGL, fall back to CSS 3D transform renderer.
// This will discard the image loaded so far and load the fallback image.
// While browser specific tests are usually frowned upon, the
// fallback viewer only really works with WebKit/Blink and IE 10/11
// (it doesn't work properly in Firefox).
if (!gl && (imageType == 'multires' && image.hasOwnProperty('fallbackPath') || imageType == 'cubemap') && ('WebkitAppearance' in document.documentElement.style || navigator.userAgent.match(/Trident.*rv[ :]*11\./) || navigator.appVersion.indexOf('MSIE 10') !== -1)) {
// Remove old world if it exists
if (world) {
container.removeChild(world);
}
// Initialize renderer
world = document.createElement('div');
world.className = 'pnlm-world';
// Add images
var path;
if (image.basePath) {
path = image.basePath + image.fallbackPath;
} else {
path = image.fallbackPath;
}
var sides = ['f', 'r', 'b', 'l', 'u', 'd'];
var loaded = 0;
var onLoad = function onLoad() {
// Draw image on canvas
var faceCanvas = document.createElement('canvas');
faceCanvas.className = 'pnlm-face pnlm-' + sides[this.side] + 'face';
world.appendChild(faceCanvas);
var faceContext = faceCanvas.getContext('2d');
faceCanvas.style.width = this.width + 4 + 'px';
faceCanvas.style.height = this.height + 4 + 'px';
faceCanvas.width = this.width + 4;
faceCanvas.height = this.height + 4;
faceContext.drawImage(this, 2, 2);
var imgData = faceContext.getImageData(0, 0, faceCanvas.width, faceCanvas.height);
var data = imgData.data;
// Duplicate edge pixels
var i;
var j;
for (i = 2; i < faceCanvas.width - 2; i++) {
for (j = 0; j < 4; j++) {
data[(i + faceCanvas.width) * 4 + j] = data[(i + faceCanvas.width * 2) * 4 + j];
data[(i + faceCanvas.width * (faceCanvas.height - 2)) * 4 + j] = data[(i + faceCanvas.width * (faceCanvas.height - 3)) * 4 + j];
}
}
for (i = 2; i < faceCanvas.height - 2; i++) {
for (j = 0; j < 4; j++) {
data[(i * faceCanvas.width + 1) * 4 + j] = data[(i * faceCanvas.width + 2) * 4 + j];
data[((i + 1) * faceCanvas.width - 2) * 4 + j] = data[((i + 1) * faceCanvas.width - 3) * 4 + j];
}
}
for (j = 0; j < 4; j++) {
data[(faceCanvas.width + 1) * 4 + j] = data[(faceCanvas.width * 2 + 2) * 4 + j];
data[(faceCanvas.width * 2 - 2) * 4 + j] = data[(faceCanvas.width * 3 - 3) * 4 + j];
data[(faceCanvas.width * (faceCanvas.height - 2) + 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 3) + 2) * 4 + j];
data[(faceCanvas.width * (faceCanvas.height - 1) - 2) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 2) - 3) * 4 + j];
}
for (i = 1; i < faceCanvas.width - 1; i++) {
for (j = 0; j < 4; j++) {
data[i * 4 + j] = data[(i + faceCanvas.width) * 4 + j];
data[(i + faceCanvas.width * (faceCanvas.height - 1)) * 4 + j] = data[(i + faceCanvas.width * (faceCanvas.height - 2)) * 4 + j];
}
}
for (i = 1; i < faceCanvas.height - 1; i++) {
for (j = 0; j < 4; j++) {
data[i * faceCanvas.width * 4 + j] = data[(i * faceCanvas.width + 1) * 4 + j];
data[((i + 1) * faceCanvas.width - 1) * 4 + j] = data[((i + 1) * faceCanvas.width - 2) * 4 + j];
}
}
for (j = 0; j < 4; j++) {
data[j] = data[(faceCanvas.width + 1) * 4 + j];
data[(faceCanvas.width - 1) * 4 + j] = data[(faceCanvas.width * 2 - 2) * 4 + j];
data[faceCanvas.width * (faceCanvas.height - 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 2) + 1) * 4 + j];
data[(faceCanvas.width * faceCanvas.height - 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 1) - 2) * 4 + j];
}
// Draw image width duplicated edge pixels on canvas
faceContext.putImageData(imgData, 0, 0);
incLoaded.call(this);
};
var incLoaded = function incLoaded() {
if (this.width > 0) {
if (fallbackImgSize === undefined) {
fallbackImgSize = this.width;
}
if (fallbackImgSize != this.width) {
console.log('Fallback faces have inconsistent widths: ' + fallbackImgSize + ' vs. ' + this.width);
}
} else {
faceMissing = true;
}
loaded++;
if (loaded == 6) {
fallbackImgSize = this.width;
container.appendChild(world);
callback();
}
};
faceMissing = false;
for (s = 0; s < 6; s++) {
var faceImg = new Image();
faceImg.crossOrigin = globalParams.crossOrigin ? globalParams.crossOrigin : 'anonymous';
faceImg.side = s;
faceImg.onload = onLoad;
faceImg.onerror = incLoaded; // ignore missing face to support partial fallback image
if (imageType == 'multires') {
faceImg.src = encodeURI(path.replace('%s', sides[s]) + '.' + image.extension);
} else {
faceImg.src = encodeURI(image[s].src);
}
}
fillMissingFaces(fallbackImgSize);
return;
} else if (!gl) {
console.log('Error: no WebGL support detected!');
throw { type: 'no webgl' };
}
if (imageType == 'cubemap') {
fillMissingFaces(cubeImgWidth);
}
if (image.basePath) {
image.fullpath = image.basePath + image.path;
} else {
image.fullpath = image.path;
}
image.invTileResolution = 1 / image.tileResolution;
var vertices = createCube();
vtmps = [];
for (s = 0; s < 6; s++) {
vtmps[s] = vertices.slice(s * 12, s * 12 + 12);
vertices = createCube();
}
// Make sure image isn't too big
var width = 0,
maxWidth = 0;
if (imageType == 'equirectangular') {
width = Math.max(image.width, image.height);
maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE);
} else if (imageType == 'cubemap') {
width = cubeImgWidth;
maxWidth = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
}
if (width > maxWidth) {
console.log('Error: The image is too big; it\'s ' + width + 'px wide, ' + 'but this device\'s maximum supported size is ' + maxWidth + 'px.');
throw { type: 'webgl size error', width: width, maxWidth: maxWidth };
}
// Store horizon pitch and roll if applicable
if (params !== undefined && (params.horizonPitch !== undefined || params.horizonRoll !== undefined)) {
pose = [params.horizonPitch == undefined ? 0 : params.horizonPitch, params.horizonRoll == undefined ? 0 : params.horizonRoll];
}
// Set 2d texture binding
var glBindType = gl.TEXTURE_2D;
// Create viewport for entire canvas
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
// Create vertex shader
vs = gl.createShader(gl.VERTEX_SHADER);
var vertexSrc = v;
if (imageType == 'multires') {
vertexSrc = vMulti;
}
gl.shaderSource(vs, vertexSrc);
gl.compileShader(vs);
// Create fragment shader
fs = gl.createShader(gl.FRAGMENT_SHADER);
var fragmentSrc = fragEquirectangular;
if (imageType == 'cubemap') {
glBindType = gl.TEXTURE_CUBE_MAP;
fragmentSrc = fragCube;
} else if (imageType == 'multires') {
fragmentSrc = fragMulti;
}
gl.shaderSource(fs, fragmentSrc);
gl.compileShader(fs);
// Link WebGL program
program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
// Log errors
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
console.log(gl.getShaderInfoLog(vs));
}
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
console.log(gl.getShaderInfoLog(fs));
}
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.log(gl.getProgramInfoLog(program));
}
// Use WebGL program
gl.useProgram(program);
program.drawInProgress = false;
// Set background clear color (does not apply to cubemap/fallback image)
var color = params.backgroundColor ? params.backgroundColor : [0, 0, 0];
gl.clearColor(color[0], color[1], color[2], 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Look up texture coordinates location
program.texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(program.texCoordLocation);
if (imageType != 'multires') {
// Provide texture coordinates for rectangle
if (!texCoordBuffer) {
texCoordBuffer = gl.createBuffer();
}
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, 1, 1, 1, 1, -1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW);
gl.vertexAttribPointer(program.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Pass aspect ratio
program.aspectRatio = gl.getUniformLocation(program, 'u_aspectRatio');
gl.uniform1f(program.aspectRatio, gl.drawingBufferWidth / gl.drawingBufferHeight);
// Locate psi, theta, focal length, horizontal extent, vertical extent, and vertical offset
program.psi = gl.getUniformLocation(program, 'u_psi');
program.theta = gl.getUniformLocation(program, 'u_theta');
program.f = gl.getUniformLocation(program, 'u_f');
program.h = gl.getUniformLocation(program, 'u_h');
program.v = gl.getUniformLocation(program, 'u_v');
program.vo = gl.getUniformLocation(program, 'u_vo');
program.rot = gl.getUniformLocation(program, 'u_rot');
// Pass horizontal extent, vertical extent, and vertical offset
gl.uniform1f(program.h, haov / (Math.PI * 2.0));
gl.uniform1f(program.v, vaov / Math.PI);
gl.uniform1f(program.vo, voffset / Math.PI * 2);
// Set background color
if (imageType == 'equirectangular') {
program.backgroundColor = gl.getUniformLocation(program, 'u_backgroundColor');
gl.uniform4fv(program.backgroundColor, color.concat([1]));
}
// Create texture
program.texture = gl.createTexture();
gl.bindTexture(glBindType, program.texture);
// Upload images to texture depending on type
if (imageType == 'cubemap') {
// Load all six sides of the cube map
gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[1]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[3]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[4]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[5]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[0]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[2]);
} else {
// Upload image to the texture
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
}
// Set parameters for rendering any size
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
} else {
// Look up vertex coordinates location
program.vertPosLocation = gl.getAttribLocation(program, 'a_vertCoord');
gl.enableVertexAttribArray(program.vertPosLocation);
// Create buffers
if (!cubeVertBuf) {
cubeVertBuf = gl.createBuffer();
}
if (!cubeVertTexCoordBuf) {
cubeVertTexCoordBuf = gl.createBuffer();
}
if (!cubeVertIndBuf) {
cubeVertIndBuf = gl.createBuffer();
}
// Bind texture coordinate buffer and pass coordinates to WebGL
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertTexCoordBuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
// Bind square index buffer and pass indicies to WebGL
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertIndBuf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
// Find uniforms
program.perspUniform = gl.getUniformLocation(program, 'u_perspMatrix');
program.cubeUniform = gl.getUniformLocation(program, 'u_cubeMatrix');
//program.colorUniform = gl.getUniformLocation(program, 'u_color');
program.level = -1;
program.currentNodes = [];
program.nodeCache = [];
program.nodeCacheTimestamp = 0;
}
// Check if there was an error
var err = gl.getError();
if (err !== 0) {
console.log('Error: Something went wrong with WebGL!', err);
throw { type: 'webgl error' };
}
callback();
};
/**
* Destroy renderer.
* @memberof Renderer
* @instance
*/
this.destroy = function () {
if (container !== undefined) {
if (canvas !== undefined && container.contains(canvas)) {
container.removeChild(canvas);
}
if (world !== undefined && container.contains(world)) {
container.removeChild(world);
}
}
if (gl) {
// The spec says this is only supposed to simulate losing the WebGL
// context, but in practice it tends to actually free the memory.
var extension = gl.getExtension('WEBGL_lose_context');
if (extension) {
extension.loseContext();
}
}
};
/**
* Resize renderer (call after resizing container).
* @memberof Renderer
* @instance
*/
this.resize = function () {
var pixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * pixelRatio;
canvas.height = canvas.clientHeight * pixelRatio;
if (gl) {
if (gl.getError() == 1286) {
handleWebGLError1286();
}
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
if (imageType != 'multires') {
gl.uniform1f(program.aspectRatio, canvas.clientWidth / canvas.clientHeight);
}
}
};
// Initialize canvas size
this.resize();
/**
* Set renderer horizon pitch and roll.
* @memberof Renderer
* @instance
*/
this.setPose = function (horizonPitch, horizonRoll) {
pose = [horizonPitch, horizonRoll];
};
/**
* Render new view of panorama.
* @memberof Renderer
* @instance
* @param {number} pitch - Pitch to render at (in radians).
* @param {number} yaw - Yaw to render at (in radians).
* @param {number} hfov - Horizontal field of view to render with (in radians).
* @param {Object} [params] - Extra configuration parameters.
* @param {number} [params.roll] - Camera roll (in radians).
* @param {boolean} [params.returnImage] - Return rendered image?
*/
this.render = function (pitch, yaw, hfov, params) {
var focal,
i,
s,
roll = 0;
if (params === undefined) {
params = {};
}
if (params.roll) {
roll = params.roll;
}
// Apply pitch and roll transformation if applicable
if (pose !== undefined) {
var horizonPitch = pose[0],
horizonRoll = pose[1];
// Calculate new pitch and yaw
var orig_pitch = pitch,
orig_yaw = yaw,
x = Math.cos(horizonRoll) * Math.sin(pitch) * Math.sin(horizonPitch) + Math.cos(pitch) * (Math.cos(horizonPitch) * Math.cos(yaw) + Math.sin(horizonRoll) * Math.sin(horizonPitch) * Math.sin(yaw)),
y = -Math.sin(pitch) * Math.sin(horizonRoll) + Math.cos(pitch) * Math.cos(horizonRoll) * Math.sin(yaw),
z = Math.cos(horizonRoll) * Math.cos(horizonPitch) * Math.sin(pitch) + Math.cos(pitch) * (-Math.cos(yaw) * Math.sin(horizonPitch) + Math.cos(horizonPitch) * Math.sin(horizonRoll) * Math.sin(yaw));
pitch = Math.asin(Math.max(Math.min(z, 1), -1));
yaw = Math.atan2(y, x);
// Calculate roll
var v = [Math.cos(orig_pitch) * (Math.sin(horizonRoll) * Math.sin(horizonPitch) * Math.cos(orig_yaw) - Math.cos(horizonPitch) * Math.sin(orig_yaw)), Math.cos(orig_pitch) * Math.cos(horizonRoll) * Math.cos(orig_yaw), Math.cos(orig_pitch) * (Math.cos(horizonPitch) * Math.sin(horizonRoll) * Math.cos(orig_yaw) + Math.sin(orig_yaw) * Math.sin(horizonPitch))],
w = [-Math.cos(pitch) * Math.sin(yaw), Math.cos(pitch) * Math.cos(yaw)];
var roll_adj = Math.acos(Math.max(Math.min((v[0] * w[0] + v[1] * w[1]) / (Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) * Math.sqrt(w[0] * w[0] + w[1] * w[1])), 1), -1));
if (v[2] < 0) {
roll_adj = 2 * Math.PI - roll_adj;
}
roll += roll_adj;
}
// If no WebGL
if (!gl && (imageType == 'multires' || imageType == 'cubemap')) {
// Determine face transforms
s = fallbackImgSize / 2;
var transforms = {
f: 'translate3d(-' + (s + 2) + 'px, -' + (s + 2) + 'px, -' + s + 'px)',
b: 'translate3d(' + (s + 2) + 'px, -' + (s + 2) + 'px, ' + s + 'px) rotateX(180deg) rotateZ(180deg)',
u: 'translate3d(-' + (s + 2) + 'px, -' + s + 'px, ' + (s + 2) + 'px) rotateX(270deg)',
d: 'translate3d(-' + (s + 2) + 'px, ' + s + 'px, -' + (s + 2) + 'px) rotateX(90deg)',
l: 'translate3d(-' + s + 'px, -' + (s + 2) + 'px, ' + (s + 2) + 'px) rotateX(180deg) rotateY(90deg) rotateZ(180deg)',
r: 'translate3d(' + s + 'px, -' + (s + 2) + 'px, -' + (s + 2) + 'px) rotateY(270deg)'
};
focal = 1 / Math.tan(hfov / 2);
var zoom = focal * canvas.clientWidth / 2 + 'px';
var transform = 'perspective(' + zoom + ') translateZ(' + zoom + ') rotateX(' + pitch + 'rad) rotateY(' + yaw + 'rad) ';
// Apply face transforms
var faces = Object.keys(transforms);
for (i = 0; i < 6; i++) {
var face = world.querySelector('.pnlm-' + faces[i] + 'face');
if (!face) {
continue;
} // ignore missing face to support partial cubemap/fallback image
face.style.webkitTransform = transform + transforms[faces[i]];
face.style.transform = transform + transforms[faces[i]];
}
return;
}
if (imageType != 'multires') {
// Calculate focal length from vertical field of view
var vfov = 2 * Math.atan(Math.tan(hfov * 0.5) / (gl.drawingBufferWidth / gl.drawingBufferHeight));
focal = 1 / Math.tan(vfov * 0.5);
// Pass psi, theta, roll, and focal length
gl.uniform1f(program.psi, yaw);
gl.uniform1f(program.theta, pitch);
gl.uniform1f(program.rot, roll);
gl.uniform1f(program.f, focal);
if (dynamic === true) {
// Update texture if dynamic
if (imageType == 'equirectangular') {
gl.bindTexture(gl.TEXTURE_2D, program.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
}
}
// Draw using current buffer
gl.drawArrays(gl.TRIANGLES, 0, 6);
} else {
// Create perspective matrix
var perspMatrix = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 100.0);
// Find correct zoom level
checkZoom(hfov);
// Create rotation matrix
var matrix = identityMatrix3();
matrix = rotateMatrix(matrix, -roll, 'z');
matrix = rotateMatrix(matrix, -pitch, 'x');
matrix = rotateMatrix(matrix, yaw, 'y');
matrix = makeMatrix4(matrix);
// Set matrix uniforms
gl.uniformMatrix4fv(program.perspUniform, false, new Float32Array(transposeMatrix4(perspMatrix)));
gl.uniformMatrix4fv(program.cubeUniform, false, new Float32Array(transposeMatrix4(matrix)));
// Find current nodes
var rotPersp = rotatePersp(perspMatrix, matrix);
program.nodeCache.sort(multiresNodeSort);
if (program.nodeCache.length > 200 && program.nodeCache.length > program.currentNodes.length + 50) {
// Remove older nodes from cache
var removed = program.nodeCache.splice(200, program.nodeCache.length - 200);
for (var i = 0; i < removed.length; i++) {
// Explicitly delete textures
gl.deleteTexture(removed[i].texture);
}
}
program.currentNodes = [];
var sides = ['f', 'b', 'u', 'd', 'l', 'r'];
for (s = 0; s < 6; s++) {
var ntmp = new MultiresNode(vtmps[s], sides[s], 1, 0, 0, image.fullpath);
testMultiresNode(rotPersp, ntmp, pitch, yaw, hfov);
}
program.currentNodes.sort(multiresNodeRenderSort);
// Unqueue any pending requests for nodes that are no longer visible
for (i = pendingTextureRequests.length - 1; i >= 0; i--) {
if (program.currentNodes.indexOf(pendingTextureRequests[i].node) === -1) {
pendingTextureRequests[i].node.textureLoad = false;
pendingTextureRequests.splice(i, 1);
}
}
// Allow one request to be pending, so that we can create a texture buffer for that in advance of loading actually beginning
if (pendingTextureRequests.length === 0) {
for (i = 0; i < program.currentNodes.length; i++) {
var node = program.currentNodes[i];
if (!node.texture && !node.textureLoad) {
node.textureLoad = true;
setTimeout(processNextTile, 0, node);
// Only process one tile per frame to improve responsiveness
break;
}
}
}
// Draw tiles
multiresDraw();
}
if (params.returnImage !== undefined) {
return canvas.toDataURL('image/png');
}
};
/**
* Check if images are loading.
* @memberof Renderer
* @instance
* @returns {boolean} Whether or not images are loading.
*/
this.isLoading = function () {
if (gl && imageType == 'multires') {
for (var i = 0; i < program.currentNodes.length; i++) {
if (!program.currentNodes[i].textureLoaded) {
return true;
}
}
}
return false;
};
/**
* Retrieve renderer's canvas.
* @memberof Renderer
* @instance
* @returns {HTMLElement} Renderer's canvas.
*/
this.getCanvas = function () {
return canvas;
};
/**
* Sorting method for multires nodes.
* @private
* @param {MultiresNode} a - First node.
* @param {MultiresNode} b - Second node.
* @returns {number} Base tiles first, then higher timestamp first.
*/
function multiresNodeSort(a, b) {
// Base tiles are always first
if (a.level == 1 && b.level != 1) {
return -1;
}
if (b.level == 1 && a.level != 1) {
return 1;
}
// Higher timestamp first
return b.timestamp - a.timestamp;
}
/**
* Sorting method for multires node rendering.
* @private
* @param {MultiresNode} a - First node.
* @param {MultiresNode} b - Second node.
* @returns {number} Lower zoom levels first, then closest to center first.
*/
function multiresNodeRenderSort(a, b) {
// Lower zoom levels first
if (a.level != b.level) {
return a.level - b.level;
}
// Lower distance from center first
return a.diff - b.diff;
}
/**
* Draws multires nodes.
* @private
*/
function multiresDraw() {
if (!program.drawInProgress) {
program.drawInProgress = true;
gl.clear(gl.COLOR_BUFFER_BIT);
for (var i = 0; i < program.currentNodes.length; i++) {
if (program.currentNodes[i].textureLoaded > 1) {
//var color = program.currentNodes[i].color;
//gl.uniform4f(program.colorUniform, color[0], color[1], color[2], 1.0);
// Bind vertex buffer and pass vertices to WebGL
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(program.currentNodes[i].vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0);
// Prep for texture
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertTexCoordBuf);
gl.vertexAttribPointer(program.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Bind texture and draw tile
gl.bindTexture(gl.TEXTURE_2D, program.currentNodes[i].texture); // Bind program.currentNodes[i].texture to TEXTURE0
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
}
}
program.drawInProgress = false;
}
}
/**
* Creates new multires node.
* @constructor
* @private
* @param {number[]} vertices - Node's verticies.
* @param {string} side - Node's cube face.
* @param {number} level - Node's zoom level.
* @param {number} x - Node's x position.
* @param {number} y - Node's y position.
* @param {string} path - Node's path.
*/
function MultiresNode(vertices, side, level, x, y, path) {
this.vertices = vertices;
this.side = side;
this.level = level;
this.x = x;
this.y = y;
this.path = path.replace('%s', side).replace('%l', level).replace('%x', x).replace('%y', y);
}
/**
* Test if multires node is visible. If it is, add it to current nodes,
* load its texture, and load appropriate child nodes.
* @private
* @param {number[]} rotPersp - Rotated perspective matrix.
* @param {MultiresNode} node - Multires node to check.
* @param {number} pitch - Pitch to check at.
* @param {number} yaw - Yaw to check at.
* @param {number} hfov - Horizontal field of view to check at.
*/
function testMultiresNode(rotPersp, node, pitch, yaw, hfov) {
if (checkSquareInView(rotPersp, node.vertices)) {
// Calculate central angle between center of view and center of tile
var v = node.vertices;
var x = v[0] + v[3] + v[6] + v[9];
var y = v[1] + v[4] + v[7] + v[10];
var z = v[2] + v[5] + v[8] + v[11];
var r = Math.sqrt(x * x + y * y + z * z);
var theta = Math.asin(z / r);
var phi = Math.atan2(y, x);
var ydiff = phi - yaw;
ydiff += ydiff > Math.PI ? -2 * Math.PI : ydiff < -Math.PI ? 2 * Math.PI : 0;
ydiff = Math.abs(ydiff);
node.diff = Math.acos(Math.sin(pitch) * Math.sin(theta) + Math.cos(pitch) * Math.cos(theta) * Math.cos(ydiff));
// Add node to current nodes and load texture if needed
var inCurrent = false;
for (var k = 0; k < program.nodeCache.length; k++) {
if (program.nodeCache[k].path == node.path) {
inCurrent = true;
program.nodeCache[k].timestamp = program.nodeCacheTimestamp++;
program.nodeCache[k].diff = node.diff;
program.currentNodes.push(program.nodeCache[k]);
break;
}
}
if (!inCurrent) {
//node.color = [Math.random(), Math.random(), Math.random()];
node.timestamp = program.nodeCacheTimestamp++;
program.currentNodes.push(node);
program.nodeCache.push(node);
}
// TODO: Test error
// Create child nodes
if (node.level < program.level) {
var cubeSize = image.cubeResolution * Math.pow(2, node.level - image.maxLevel);
var numTiles = Math.ceil(cubeSize * image.invTileResolution) - 1;
var doubleTileSize = cubeSize % image.tileResolution * 2;
var lastTileSize = cubeSize * 2 % image.tileResolution;
if (lastTileSize === 0) {
lastTileSize = image.tileResolution;
}
if (doubleTileSize === 0) {
doubleTileSize = image.tileResolution * 2;
}
var f = 0.5;
if (node.x == numTiles || node.y == numTiles) {
f = 1.0 - image.tileResolution / (image.tileResolution + lastTileSize);
}
var i = 1.0 - f;
var children = [];
var vtmp, ntmp;
var f1 = f,
f2 = f,
f3 = f,
i1 = i,
i2 = i,
i3 = i;
// Handle non-symmetric tiles
if (lastTileSize < image.tileResolution) {
if (node.x == numTiles && node.y != numTiles) {
f2 = 0.5;
i2 = 0.5;
if (node.side == 'd' || node.side == 'u') {
f3 = 0.5;
i3 = 0.5;
}
} else if (node.x != numTiles && node.y == numTiles) {
f1 = 0.5;
i1 = 0.5;
if (node.side == 'l' || node.side == 'r') {
f3 = 0.5;
i3 = 0.5;
}
}
}
// Handle small tiles that have fewer than four children
if (doubleTileSize <= image.tileResolution) {
if (node.x == numTiles) {
f1 = 0;
i1 = 1;
if (node.side == 'l' || node.side == 'r') {
f3 = 0;
i3 = 1;
}
}
if (node.y == numTiles) {
f2 = 0;
i2 = 1;
if (node.side == 'd' || node.side == 'u') {
f3 = 0;
i3 = 1;
}
}
}
vtmp = [v[0], v[1], v[2], v[0] * f1 + v[3] * i1, v[1] * f + v[4] * i, v[2] * f3 + v[5] * i3, v[0] * f1 + v[6] * i1, v[1] * f2 + v[7] * i2, v[2] * f3 + v[8] * i3, v[0] * f + v[9] * i, v[1] * f2 + v[10] * i2, v[2] * f3 + v[11] * i3];
ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x * 2, node.y * 2, image.fullpath);
children.push(ntmp);
if (!(node.x == numTiles && doubleTileSize <= image.tileResolution)) {
vtmp = [v[0] * f1 + v[3] * i1, v[1] * f + v[4] * i, v[2] * f3 + v[5] * i3, v[3], v[4], v[5], v[3] * f + v[6] * i, v[4] * f2 + v[7] * i2, v[5] * f3 + v[8] * i3, v[0] * f1 + v[6] * i1, v[1] * f2 + v[7] * i2, v[2] * f3 + v[8] * i3];
ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x * 2 + 1, node.y * 2, image.fullpath);
children.push(ntmp);
}
if (!(node.x == numTiles && doubleTileSize <= image.tileResolution) && !(node.y == numTiles && doubleTileSize <= image.tileResolution)) {
vtmp = [v[0] * f1 + v[6] * i1, v[1] * f2 + v[7] * i2, v[2] * f3 + v[8] * i3, v[3] * f + v[6] * i, v[4] * f2 + v[7] * i2, v[5] * f3 + v[8] * i3, v[6], v[7], v[8], v[9] * f1 + v[6] * i1, v[10] * f + v[7] * i, v[11] * f3 + v[8] * i3];
ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x * 2 + 1, node.y * 2 + 1, image.fullpath);
children.push(ntmp);
}
if (!(node.y == numTiles && doubleTileSize <= image.tileResolution)) {
vtmp = [v[0] * f + v[9] * i, v[1] * f2 + v[10] * i2, v[2] * f3 + v[11] * i3, v[0] * f1 + v[6] * i1, v[1] * f2 + v[7] * i2, v[2] * f3 + v[8] * i3, v[9] * f1 + v[6] * i1, v[10] * f + v[7] * i, v[11] * f3 + v[8] * i3, v[9], v[10], v[11]];
ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x * 2, node.y * 2 + 1, image.fullpath);
children.push(ntmp);
}
for (var j = 0; j < children.length; j++) {
testMultiresNode(rotPersp, children[j], pitch, yaw, hfov);
}
}
}
}
/**
* Creates cube vertex array.
* @private
* @returns {number[]} Cube vertex array.
*/
function createCube() {
return [-1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, // Front face
1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, // Back face
-1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, // Up face
-1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // Down face
-1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, // Left face
1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1 // Right face
];
}
/**
* Creates 3x3 identity matrix.
* @private
* @returns {number[]} Identity matrix.
*/
function identityMatrix3() {
return [1, 0, 0, 0, 1, 0, 0, 0, 1];
}
/**
* Rotates a 3x3 matrix.
* @private
* @param {number[]} m - Matrix to rotate.
* @param {number[]} angle - Angle to rotate by in radians.
* @param {string} axis - Axis to rotate about (`x`, `y`, or `z`).
* @returns {number[]} Rotated matrix.
*/
function rotateMatrix(m, angle, axis) {
var s = Math.sin(angle);
var c = Math.cos(angle);
if (axis == 'x') {
return [m[0], c * m[1] + s * m[2], c * m[2] - s * m[1], m[3], c * m[4] + s * m[5], c * m[5] - s * m[4], m[6], c * m[7] + s * m[8], c * m[8] - s * m[7]];
}
if (axis == 'y') {
return [c * m[0] - s * m[2], m[1], c * m[2] + s * m[0], c * m[3] - s * m[5], m[4], c * m[5] + s * m[3], c * m[6] - s * m[8], m[7], c * m[8] + s * m[6]];
}
if (axis == 'z') {
return [c * m[0] + s * m[1], c * m[1] - s * m[0], m[2], c * m[3] + s * m[4], c * m[4] - s * m[3], m[5], c * m[6] + s * m[7], c * m[7] - s * m[6], m[8]];
}
}
/**
* Turns a 3x3 matrix into a 4x4 matrix.
* @private
* @param {number[]} m - Input matrix.
* @returns {number[]} Expanded matrix.
*/
function makeMatrix4(m) {
return [m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, 0, 0, 0, 1];
}
/**
* Transposes a 4x4 matrix.
* @private
* @param {number[]} m - Input matrix.
* @returns {number[]} Transposed matrix.
*/
function transposeMatrix4(m) {
return [m[0], m[4], m[8], m[12], m[1], m[5], m[9], m[13], m[2], m[6], m[10], m[14], m[3], m[7], m[11], m[15]];
}
/**
* Creates a perspective matrix.
* @private
* @param {number} hfov - Desired horizontal field of view.
* @param {number} aspect - Desired aspect ratio.
* @param {number} znear - Near distance.
* @param {number} zfar - Far distance.
* @returns {number[]} Generated perspective matrix.
*/
function makePersp(hfov, aspect, znear, zfar) {
var fovy = 2 * Math.atan(Math.tan(hfov / 2) * gl.drawingBufferHeight / gl.drawingBufferWidth);
var f = 1 / Math.tan(fovy / 2);
return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (zfar + znear) / (znear - zfar), 2 * zfar * znear / (znear - zfar), 0, 0, -1, 0];
}
/**
* Processes a loaded texture image into a WebGL texture.
* @private
* @param {Image} img - Input image.
* @param {WebGLTexture} tex - Texture to bind image to.
*/
function processLoadedTexture(img, tex) {
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null);
}
var pendingTextureRequests = [];
// Based on http://blog.tojicode.com/2012/03/javascript-memory-optimization-and.html
var loadTexture = function () {
var cacheTop = 4; // Maximum number of concurrents loads
var textureImageCache = {};
var crossOrigin;
function TextureImageLoader() {
var self = this;
this.texture = this.callback = null;
this.image = new Image();
this.image.crossOrigin = crossOrigin ? crossOrigin : 'anonymous';
var loadFn = function loadFn() {
if (self.image.width > 0 && self.image.height > 0) {
// ignore missing tile to supporting partial image
processLoadedTexture(self.image, self.texture);
self.callback(self.texture, true);
} else {
self.callback(self.texture, false);
}
releaseTextureImageLoader(self);
};
this.image.addEventListener('load', loadFn);
this.image.addEventListener('error', loadFn); // ignore missing tile file to support partial image, otherwise retry loop causes high CPU load
};
TextureImageLoader.prototype.loadTexture = function (src, texture, callback) {
this.texture = texture;
this.callback = callback;
this.image.src = src;
};
function PendingTextureRequest(node, src, texture, callback) {
this.node = node;
this.src = src;
this.texture = texture;
this.callback = callback;
};
function releaseTextureImageLoader(til) {
if (pendingTextureRequests.length) {
var req = pendingTextureRequests.shift();
til.loadTexture(req.src, req.texture, req.callback);
} else {
textureImageCache[cacheTop++] = til;
}
}
for (var i = 0; i < cacheTop; i++) {
textureImageCache[i] = new TextureImageLoader();
}
return function (node, src, callback, _crossOrigin) {
crossOrigin = _crossOrigin;
var texture = gl.createTexture();
if (cacheTop) {
textureImageCache[--cacheTop].loadTexture(src, texture, callback);
} else {
pendingTextureRequests.push(new PendingTextureRequest(node, src, texture, callback));
}
return texture;
};
}();
/**
* Loads image and creates texture for a multires node / tile.
* @private
* @param {MultiresNode} node - Input node.
*/
function processNextTile(node) {
loadTexture(node, encodeURI(node.path + '.' + image.extension), function (texture, loaded) {
node.texture = texture;
node.textureLoaded = loaded ? 2 : 1;
}, globalParams.crossOrigin);
}
/**
* Finds and applies optimal multires zoom level.
* @private
* @param {number} hfov - Horizontal field of view to check at.
*/
function checkZoom(hfov) {
// Find optimal level
var newLevel = 1;
while (newLevel < image.maxLevel && gl.drawingBufferWidth > image.tileResolution * Math.pow(2, newLevel - 1) * Math.tan(hfov / 2) * 0.707) {
newLevel++;
}
// Apply change
program.level = newLevel;
}
/**
* Rotates perspective matrix.
* @private
* @param {number[]} p - Perspective matrix.
* @param {number[]} r - Rotation matrix.
* @returns {number[]} Rotated matrix.
*/
function rotatePersp(p, r) {
return [p[0] * r[0], p[0] * r[1], p[0] * r[2], 0, p[5] * r[4], p[5] * r[5], p[5] * r[6], 0, p[10] * r[8], p[10] * r[9], p[10] * r[10], p[11], -r[8], -r[9], -r[10], 0];
}
/**
* Applies rotated perspective matrix to a 3-vector
* (last element is inverted).
* @private
* @param {number[]} m - Rotated perspective matrix.
* @param {number[]} v - Input 3-vector.
* @returns {number[]} Resulting 4-vector.
*/
function applyRotPerspToVec(m, v) {
return [m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[4] * v[0] + m[5] * v[1] + m[6] * v[2], m[11] + m[8] * v[0] + m[9] * v[1] + m[10] * v[2], 1 / (m[12] * v[0] + m[13] * v[1] + m[14] * v[2])];
}
/**
* Checks if a vertex is visible.
* @private
* @param {number[]} m - Rotated perspective matrix.
* @param {number[]} v - Input vertex.
* @returns {number} 1 or -1 if the vertex is or is not visible,
* respectively.
*/
function checkInView(m, v) {
var vpp = applyRotPerspToVec(m, v);
var winX = vpp[0] * vpp[3];
var winY = vpp[1] * vpp[3];
var winZ = vpp[2] * vpp[3];
var ret = [0, 0, 0];
if (winX < -1) {
ret[0] = -1;
}
if (winX > 1) {
ret[0] = 1;
}
if (winY < -1) {
ret[1] = -1;
}
if (winY > 1) {
ret[1] = 1;
}
if (winZ < -1 || winZ > 1) {
ret[2] = 1;
}
return ret;
}
/**
* Checks if a square (tile) is visible.
* @private
* @param {number[]} m - Rotated perspective matrix.
* @param {number[]} v - Square's vertex array.
* @returns {boolean} Whether or not the square is visible.
*/
function checkSquareInView(m, v) {
var check1 = checkInView(m, v.slice(0, 3));
var check2 = checkInView(m, v.slice(3, 6));
var check3 = checkInView(m, v.slice(6, 9));
var check4 = checkInView(m, v.slice(9, 12));
var testX = check1[0] + check2[0] + check3[0] + check4[0];
if (testX == -4 || testX == 4) {
return false;
}
var testY = check1[1] + check2[1] + check3[1] + check4[1];
if (testY == -4 || testY == 4) {
return false;
}
var testZ = check1[2] + check2[2] + check3[2] + check4[2];
return testZ != 4;
}
/**
* On iOS (iPhone 5c, iOS 10.3), this WebGL error occurs when the canvas is
* too big. Unfortuately, there's no way to test for this beforehand, so we
* reduce the canvas size if this error is thrown.
* @private
*/
function handleWebGLError1286() {
console.log('Reducing canvas size due to error 1286!');
canvas.width = Math.round(canvas.width / 2);
canvas.height = Math.round(canvas.height / 2);
}
}
// Vertex shader for equirectangular and cube
var v = ['attribute vec2 a_texCoord;', 'varying vec2 v_texCoord;', 'void main() {',
// Set position
'gl_Position = vec4(a_texCoord, 0.0, 1.0);',
// Pass the coordinates to the fragment shader
'v_texCoord = a_texCoord;', '}'].join('');
// Vertex shader for multires
var vMulti = ['attribute vec3 a_vertCoord;', 'attribute vec2 a_texCoord;', 'uniform mat4 u_cubeMatrix;', 'uniform mat4 u_perspMatrix;', 'varying mediump vec2 v_texCoord;', 'void main(void) {',
// Set position
'gl_Position = u_perspMatrix * u_cubeMatrix * vec4(a_vertCoord, 1.0);',
// Pass the coordinates to the fragment shader
'v_texCoord = a_texCoord;', '}'].join('');
// Fragment shader
var fragEquiCubeBase = ['precision mediump float;', 'uniform float u_aspectRatio;', 'uniform float u_psi;', 'uniform float u_theta;', 'uniform float u_f;', 'uniform float u_h;', 'uniform float u_v;', 'uniform float u_vo;', 'uniform float u_rot;', 'const