UNPKG

react-pannellum-next

Version:

The `PanoramaViewer` component is a React component that provides a panoramic image viewer using the Pannellum library. It allows you to display a 360-degree image and add interactive hotspots to it.

899 lines (895 loc) 220 kB
import "./index.css"; import $6PEjo$react, {useRef as $6PEjo$useRef, useEffect as $6PEjo$useEffect} from "react"; /* * libpannellum - A WebGL and CSS 3D transform based Panorama Renderer * Copyright (c) 2012-2022 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(window1, document1, undefined) { "use strict"; /** * Creates a new panorama renderer. * @constructor * @param {HTMLElement} container - The container element for the renderer. * @param {WebGLRenderingContext} [context] - Existing WebGL context (instead of container). */ function Renderer(container, context) { var canvas; if (container) { canvas = document1.createElement("canvas"); canvas.style.width = canvas.style.height = "100%"; container.appendChild(canvas); } var program, gl, vs, fs; var previewProgram, previewVs, previewFs; var fallbackImgSize; var world; var vtmps; var pose; var image, imageType; var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf; var globalParams; var sides = [ "f", "b", "u", "d", "l", "r" ]; var fallbackSides = [ "f", "r", "b", "l", "u", "d" ]; if (context) gl = context; /** * 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 {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, 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; 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); if (program.textureLoads) { pendingTextureRequests = []; while(program.textureLoads.length > 0)program.textureLoads.shift()(false); } gl.deleteProgram(program); program = undefined; } if (previewProgram) { if (previewVs) { gl.detachShader(previewProgram, previewVs); gl.deleteShader(previewVs); } if (previewFs) { gl.detachShader(previewProgram, previewFs); gl.deleteShader(previewFs); } gl.deleteProgram(previewProgram); previewProgram = 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) { 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 document1.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 = document1.createElement("div"); world.className = "pnlm-world"; // Add images var path; if (image.basePath) path = image.basePath + image.fallbackPath; else path = image.fallbackPath; var loaded = 0; var onLoad = function() { // Draw image on canvas var faceCanvas = document1.createElement("canvas"); faceCanvas.className = "pnlm-face pnlm-" + fallbackSides[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() { 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 = path.replace("%s", fallbackSides[s]) + (image.extension ? "." + image.extension : ""); else faceImg.src = 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 maxWidth = 0; if (imageType == "equirectangular") { maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE); if (Math.max(image.width / 2, image.height) > maxWidth) { console.log("Error: The image is too big; it's " + image.width + "px wide, " + "but this device's maximum supported size is " + maxWidth * 2 + "px."); throw { type: "webgl size error", width: image.width, maxWidth: maxWidth * 2 }; } } else if (imageType == "cubemap") { if (cubeImgWidth > gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)) { console.log("Error: The image is too big; it's " + cubeImgWidth + "px wide, " + "but this device's maximum supported size is " + maxWidth + "px."); throw { type: "webgl size error", width: cubeImgWidth, maxWidth: maxWidth }; } } // Store horizon pitch and roll if applicable if (params !== undefined) { var horizonPitch = isNaN(params.horizonPitch) ? 0 : Number(params.horizonPitch), horizonRoll = isNaN(params.horizonRoll) ? 0 : Number(params.horizonRoll); if (horizonPitch != 0 || horizonRoll != 0) pose = [ horizonPitch, horizonRoll ]; } // Set 2d texture binding var glBindType = gl.TEXTURE_2D; // Create viewport for entire canvas gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); // Check precision support if (gl.getShaderPrecisionFormat) { var precision = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); if (precision && precision.precision < 1) // `highp` precision not supported; https://stackoverflow.com/a/33308927 fragEquiCubeBase = fragEquiCubeBase.replace("highp", "mediump"); } // 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) if (params.backgroundColor !== null) { 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 if (image.width <= maxWidth) { gl.uniform1i(gl.getUniformLocation(program, "u_splitImage"), 0); // Upload image to the texture gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); } else { // Image needs to be split into two parts due to texture size limits gl.uniform1i(gl.getUniformLocation(program, "u_splitImage"), 1); // Draw image on canvas var cropCanvas = document1.createElement("canvas"); cropCanvas.width = image.width / 2; cropCanvas.height = image.height; var cropContext = cropCanvas.getContext("2d"); cropContext.drawImage(image, 0, 0); // Upload first half of image to the texture var cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height); gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage); // Create and bind texture for second half of image program.texture2 = gl.createTexture(); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(glBindType, program.texture2); gl.uniform1i(gl.getUniformLocation(program, "u_image1"), 1); // Upload second half of image to the texture cropContext.drawImage(image, -image.width / 2, 0); cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height); gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage); // 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); // Reactivate first texture unit gl.activeTexture(gl.TEXTURE0); } // Set parameters for rendering any size if (imageType != "cubemap" && image.width && image.width <= maxWidth && haov == 2 * Math.PI && (image.width & image.width - 1) == 0) gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.REPEAT); // Only supported for power-of-two images in WebGL 1 else 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); gl.vertexAttribPointer(program.texCoordLocation, 2, gl.FLOAT, false, 0, 0); // Bind square index buffer and pass indices 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); // Bind vertex buffer gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf); gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0); // Find uniforms program.perspUniform = gl.getUniformLocation(program, "u_perspMatrix"); program.cubeUniform = gl.getUniformLocation(program, "u_cubeMatrix"); //program.colorUniform = gl.getUniformLocation(program, 'u_color'); program.currentNodes = []; program.nodeCache = []; program.nodeCacheTimestamp = 0; program.textureLoads = []; if (image.shtHash || image.equirectangularThumbnail) { // Create vertex shader previewVs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(previewVs, v); gl.compileShader(previewVs); // Create fragment shader previewFs = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(previewFs, fragEquirectangular); gl.compileShader(previewFs); // Link WebGL program previewProgram = gl.createProgram(); gl.attachShader(previewProgram, previewVs); gl.attachShader(previewProgram, previewFs); gl.linkProgram(previewProgram); // Log errors if (!gl.getShaderParameter(previewVs, gl.COMPILE_STATUS)) console.log(gl.getShaderInfoLog(previewVs)); if (!gl.getShaderParameter(previewFs, gl.COMPILE_STATUS)) console.log(gl.getShaderInfoLog(previewFs)); if (!gl.getProgramParameter(previewProgram, gl.LINK_STATUS)) console.log(gl.getProgramInfoLog(previewProgram)); // Use WebGL program gl.useProgram(previewProgram); // Look up texture coordinates location previewProgram.texCoordLocation = gl.getAttribLocation(previewProgram, "a_texCoord"); gl.enableVertexAttribArray(previewProgram.texCoordLocation); // 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(previewProgram.texCoordLocation, 2, gl.FLOAT, false, 0, 0); // Pass aspect ratio previewProgram.aspectRatio = gl.getUniformLocation(previewProgram, "u_aspectRatio"); gl.uniform1f(previewProgram.aspectRatio, gl.drawingBufferWidth / gl.drawingBufferHeight); // Locate psi, theta, focal length, horizontal extent, vertical extent, and vertical offset previewProgram.psi = gl.getUniformLocation(previewProgram, "u_psi"); previewProgram.theta = gl.getUniformLocation(previewProgram, "u_theta"); previewProgram.f = gl.getUniformLocation(previewProgram, "u_f"); previewProgram.h = gl.getUniformLocation(previewProgram, "u_h"); previewProgram.v = gl.getUniformLocation(previewProgram, "u_v"); previewProgram.vo = gl.getUniformLocation(previewProgram, "u_vo"); previewProgram.rot = gl.getUniformLocation(previewProgram, "u_rot"); // Pass horizontal extent gl.uniform1f(previewProgram.h, 1.0); // Create texture previewProgram.texture = gl.createTexture(); gl.bindTexture(glBindType, previewProgram.texture); // Upload preview image to the texture var previewImage, vext, voff; var uploadPreview = function() { gl.useProgram(previewProgram); gl.uniform1i(gl.getUniformLocation(previewProgram, "u_splitImage"), 0); gl.texImage2D(glBindType, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, previewImage); // Set parameters for rendering any size gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.REPEAT); 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); // Pass vertical extent and vertical offset gl.uniform1f(previewProgram.v, vext); gl.uniform1f(previewProgram.vo, voff); gl.useProgram(program); }; if (image.shtHash) { previewImage = shtDecodeImage(image.shtHash); // Vertical extent & offset are chosen to set the top and bottom // pixels in the preview image to be exactly at the zenith and // nadir, respectively, which matches the pre-calculated Ylm vext = (2 + 1 / 31) / 2; voff = 1 - (2 + 1 / 31) / 2; uploadPreview(); } if (image.equirectangularThumbnail) { if (typeof image.equirectangularThumbnail === "string") { if (image.equirectangularThumbnail.slice(0, 5) == "data:") { // Data URI previewImage = new Image(); previewImage.onload = function() { vext = 1; voff = 0; uploadPreview(); }; previewImage.src = image.equirectangularThumbnail; } else { console.log("Error: thumbnail string is not a data URI!"); throw { type: "config error" }; } } else { // ImageData / ImageBitmap / HTMLImageElement / HTMLCanvasElement previewImage = image.equirectangularThumbnail; vext = 1; voff = 0; uploadPreview(); } } // Reactivate main program gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf); gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0); gl.useProgram(program); } } // 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 = window1.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); else if (image.shtHash) { gl.useProgram(previewProgram); gl.uniform1f(previewProgram.aspectRatio, canvas.clientWidth / canvas.clientHeight); gl.useProgram(program); } } }; // Initialize canvas size if (canvas) this.resize(); /** * Set renderer horizon pitch and roll. * @memberof Renderer * @instance * @param {number} horizonPitch - Pitch of horizon (in radians). * @param {number} horizonRoll - Roll of horizon (in radians). */ this.setPose = function(horizonPitch, horizonRoll) { horizonPitch = isNaN(horizonPitch) ? 0 : Number(horizonPitch); horizonRoll = isNaN(horizonRoll) ? 0 : Number(horizonRoll); if (horizonPitch == 0 && horizonRoll == 0) pose = undefined; else 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 {string} [params.returnImage] - Return rendered image? If specified, should be 'ImageBitmap', 'image/jpeg', or 'image/png'. * @param {function} [params.hook] - Hook for executing arbitrary function in this environment. * @param {boolean} [params.dynamic] - Whether or not the image is dynamic (e.g., video) and should be updated. */ this.render = function(pitch, yaw, hfov, params) { var focal, i, s, roll = 0; if (params === undefined) params = {}; if (params.roll) roll = params.roll; if (params.dynamic) var dynamic = params.dynamic; // 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; } // Execute function hook if (params.hook) params.hook({ gl: gl, program: program, previewProgram: previewProgram, imageType: imageType, texCoordBuffer: texCoordBuffer, cubeVertBuf: cubeVertBuf, cubeVertTexCoordBuf: cubeVertTexCoordBuf, cubeVertIndBuf: cubeVertIndBuf }); // 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 { // Draw SHT hash preview, if needed var isPreview = typeof image.shtHash !== "undefined" || typeof image.equirectangularThumbnail !== "undefined"; var drawPreview = isPreview; if (isPreview && program.currentNodes.length >= 6) { drawPreview = false; for(var i = 0; i < 6; i++)if (!program.currentNodes[i].textureLoaded) { drawPreview = true; break; } } if (drawPreview) { gl.useProgram(previewProgram); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.vertexAttribPointer(previewProgram.texCoordLocation, 2, gl.FLOAT, false, 0, 0); gl.bindTexture(gl.TEXTURE_2D, previewProgram.texture); // 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(previewProgram.psi, yaw); gl.uniform1f(previewProgram.theta, pitch); gl.uniform1f(previewProgram.rot, roll); gl.uniform1f(previewProgram.f, focal); // Draw using current buffer gl.drawArrays(gl.TRIANGLES, 0, 6); gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf); gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0); gl.useProgram(program); } // Create perspective matrix var perspMatrix = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 100.0); var perspMatrixNoClip = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, -100, 100.0); // 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, transposeMatrix4(perspMatrix)); gl.uniformMatrix4fv(program.cubeUniform, false, transposeMatrix4(matrix)); // Find current nodes var rotPersp = rotatePersp(perspMatrix, matrix); var rotPerspNoClip = rotatePersp(perspMatrixNoClip, 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 j = 0; j < removed.length; j++)// Explicitly delete textures gl.deleteTexture(removed[j].texture); } program.currentNodes = []; for(s = 0; s < 6; s++){ var ntmp = new MultiresNode(vtmps[s], sides[s], 1, 0, 0, image.fullpath, null); testMultiresNode(rotPersp, rotPerspNoClip, 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); break; } } // Process one pending image tile // This is synchronized to rendering to avoid dropping frames due // to texture loading happening at an inopportune time. if (program.textureLoads.length > 0) program.textureLoads.shift()(true); // Draw tiles multiresDraw(!isPreview); } if (params.returnImage !== undefined) { if (window1.createImageBitmap && params.returnImage == "ImageBitmap") return createImageBitmap(canvas); else { if (params.returnImage.toString().indexOf("image/") == 0) return canvas.toDataURL(params.returnImage); else return canvas.toDataURL("image/png"); // Old default } } }; /** * 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; }; /** * Check if base image tiles are loaded. * @memberof Renderer * @instance * @returns {boolean} Whether or not base image tiles are loaded. */ this.isBaseLoaded = function() { if (program.currentNodes.length >= 6) { for(var i = 0; i < 6; i++){ if (!program.currentNodes[i].textureLoaded) return false; } 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 multiresNodeRenderSo