1/* 2 * libpannellum - A WebGL and CSS 3D transform based Panorama Renderer 3 * Copyright (c) 2012-2020 Matthew Petroff 4 * 5 * Permission is hereby granted, free of charge, to any person obtaining a copy 6 * of this software and associated documentation files (the "Software"), to deal 7 * in the Software without restriction, including without limitation the rights 8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 * copies of the Software, and to permit persons to whom the Software is 10 * furnished to do so, subject to the following conditions: 11 * 12 * The above copyright notice and this permission notice shall be included in 13 * all copies or substantial portions of the Software. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 * THE SOFTWARE. 22 */ 23 24window.libpannellum = (function(window, document, undefined) { 25 26'use strict'; 27 28/** 29 * Creates a new panorama renderer. 30 * @constructor 31 * @param {HTMLElement} container - The container element for the renderer. 32 */ 33function Renderer(container) { 34 var canvas = document.createElement('canvas'); 35 canvas.style.width = canvas.style.height = '100%'; 36 container.appendChild(canvas); 37 38 var program, gl, vs, fs; 39 var fallbackImgSize; 40 var world; 41 var vtmps; 42 var pose; 43 var image, imageType, dynamic; 44 var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf; 45 var globalParams; 46 47 /** 48 * Initialize renderer. 49 * @memberof Renderer 50 * @instance 51 * @param {Image|Array|Object} image - Input image; format varies based on 52 * `imageType`. For `equirectangular`, this is an image; for 53 * `cubemap`, this is an array of images for the cube faces in the 54 * order [+z, +x, -z, -x, +y, -y]; for `multires`, this is a 55 * configuration object. 56 * @param {string} imageType - The type of the image: `equirectangular`, 57 * `cubemap`, or `multires`. 58 * @param {boolean} dynamic - Whether or not the image is dynamic (e.g. video). 59 * @param {number} haov - Initial horizontal angle of view. 60 * @param {number} vaov - Initial vertical angle of view. 61 * @param {number} voffset - Initial vertical offset angle. 62 * @param {function} callback - Load callback function. 63 * @param {Object} [params] - Other configuration parameters (`horizonPitch`, `horizonRoll`, `backgroundColor`). 64 */ 65 this.init = function(_image, _imageType, _dynamic, haov, vaov, voffset, callback, params) { 66 // Default argument for image type 67 if (_imageType === undefined) 68 _imageType = 'equirectangular'; 69 70 if (_imageType != 'equirectangular' && _imageType != 'cubemap' && 71 _imageType != 'multires') { 72 console.log('Error: invalid image type specified!'); 73 throw {type: 'config error'}; 74 } 75 76 imageType = _imageType; 77 image = _image; 78 dynamic = _dynamic; 79 globalParams = params || {}; 80 81 // Clear old data 82 if (program) { 83 if (vs) { 84 gl.detachShader(program, vs); 85 gl.deleteShader(vs); 86 } 87 if (fs) { 88 gl.detachShader(program, fs); 89 gl.deleteShader(fs); 90 } 91 gl.bindBuffer(gl.ARRAY_BUFFER, null); 92 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); 93 if (program.texture) 94 gl.deleteTexture(program.texture); 95 if (program.nodeCache) 96 for (var i = 0; i < program.nodeCache.length; i++) 97 gl.deleteTexture(program.nodeCache[i].texture); 98 gl.deleteProgram(program); 99 program = undefined; 100 } 101 pose = undefined; 102 103 var s; 104 var faceMissing = false;
105 var cubeImgWidth; 106 if (imageType == 'cubemap') { 107 for (s = 0; s < 6; s++) { 108 if (image[s].width > 0) { 109 if (cubeImgWidth === undefined) 110 cubeImgWidth = image[s].width; 111 if (cubeImgWidth != image[s].width) 112 console.log('Cube faces have inconsistent widths: ' + cubeImgWidth + ' vs. ' + image[s].width); 113 } else 114 faceMissing = true; 115 } 116 } 117 function fillMissingFaces(imgSize) { 118 if (faceMissing) { // Fill any missing fallback/cubemap faces with background 119 var nbytes = imgSize * imgSize * 4; // RGB, plus non-functional alpha 120 var imageArray = new Uint8ClampedArray(nbytes); 121 var rgb = params.backgroundColor ? params.backgroundColor : [0, 0, 0]; 122 rgb[0] *= 255; 123 rgb[1] *= 255; 124 rgb[2] *= 255; 125 // Maybe filling could be done faster, see e.g. https://stackoverflow.com/questions/1295584/most-efficient-way-to-create-a-zero-filled-javascript-array 126 for (var i = 0; i < nbytes; i++) { 127 imageArray[i++] = rgb[0]; 128 imageArray[i++] = rgb[1]; 129 imageArray[i++] = rgb[2]; 130 } 131 var backgroundSquare = new ImageData(imageArray, imgSize, imgSize); 132 for (s = 0; s < 6; s++) { 133 if (image[s].width == 0) 134 image[s] = backgroundSquare; 135 } 136 } 137 } 138 139 // This awful browser specific test exists because iOS 8/9 and IE 11 140 // don't display non-power-of-two cubemap textures but also don't 141 // throw an error (tested on an iPhone 5c / iOS 8.1.3 / iOS 9.2 / 142 // iOS 10.3.1). 143 // Therefore, the WebGL context is never created for these browsers for 144 // NPOT cubemaps, and the CSS 3D transform fallback renderer is used 145 // instead. 146 if (!(imageType == 'cubemap' && 147 (cubeImgWidth & (cubeImgWidth - 1)) !== 0 && 148 (navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/) || 149 navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 9_/) || 150 navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 10_/) || 151 navigator.userAgent.match(/Trident.*rv[ :]*11\./)))) { 152 // Enable WebGL on canvas 153 if (!gl) 154 gl = canvas.getContext('experimental-webgl', {alpha: false, depth: false}); 155 if (gl && gl.getError() == 1286) 156 handleWebGLError1286(); 157 } 158 159 // If there is no WebGL, fall back to CSS 3D transform renderer. 160 // This will discard the image loaded so far and load the fallback image. 161 // While browser specific tests are usually frowned upon, the 162 // fallback viewer only really works with WebKit/Blink and IE 10/11 163 // (it doesn't work properly in Firefox). 164 if (!gl && ((imageType == 'multires' && image.hasOwnProperty('fallbackPath')) || 165 imageType == 'cubemap') && 166 ('WebkitAppearance' in document.documentElement.style || 167 navigator.userAgent.match(/Trident.*rv[ :]*11\./) || 168 navigator.appVersion.indexOf('MSIE 10') !== -1)) { 169 // Remove old world if it exists 170 if (world) { 171 container.removeChild(world); 172 } 173 174 // Initialize renderer 175 world = document.createElement('div'); 176 world.className = 'pnlm-world'; 177 178 // Add images 179 var path; 180 if (image.basePath) { 181 path = image.basePath + image.fallbackPath; 182 } else { 183 path = image.fallbackPath; 184 } 185 var sides = ['f', 'r', 'b', 'l', 'u', 'd']; 186 var loaded = 0; 187 var onLoad = function() { 188 // Draw image on canvas 189 var faceCanvas = document.createElement('canvas'); 190 faceCanvas.className = 'pnlm-face pnlm-' + sides[this.side] + 'face'; 191 world.appendChild(faceCanvas); 192 var faceContext = faceCanvas.getContext('2d'); 193 faceCanvas.style.width = this.width + 4 + 'px'; 194 faceCanvas.style.height = this.height + 4 + 'px'; 195 faceCanvas.width = this.width + 4;
196 faceCanvas.height = this.height + 4; 197 faceContext.drawImage(this, 2, 2); 198 var imgData = faceContext.getImageData(0, 0, faceCanvas.width, faceCanvas.height); 199 var data = imgData.data; 200 201 // Duplicate edge pixels 202 var i; 203 var j; 204 for (i = 2; i < faceCanvas.width - 2; i++) { 205 for (j = 0; j < 4; j++) { 206 data[(i + faceCanvas.width) * 4 + j] = data[(i + faceCanvas.width * 2) * 4 + j]; 207 data[(i + faceCanvas.width * (faceCanvas.height - 2)) * 4 + j] = data[(i + faceCanvas.width * (faceCanvas.height - 3)) * 4 + j]; 208 } 209 } 210 for (i = 2; i < faceCanvas.height - 2; i++) { 211 for (j = 0; j < 4; j++) { 212 data[(i * faceCanvas.width + 1) * 4 + j] = data[(i * faceCanvas.width + 2) * 4 + j]; 213 data[((i + 1) * faceCanvas.width - 2) * 4 + j] = data[((i + 1) * faceCanvas.width - 3) * 4 + j]; 214 } 215 } 216 for (j = 0; j < 4; j++) { 217 data[(faceCanvas.width + 1) * 4 + j] = data[(faceCanvas.width * 2 + 2) * 4 + j]; 218 data[(faceCanvas.width * 2 - 2) * 4 + j] = data[(faceCanvas.width * 3 - 3) * 4 + j]; 219 data[(faceCanvas.width * (faceCanvas.height - 2) + 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 3) + 2) * 4 + j]; 220 data[(faceCanvas.width * (faceCanvas.height - 1) - 2) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 2) - 3) * 4 + j]; 221 } 222 for (i = 1; i < faceCanvas.width - 1; i++) { 223 for (j = 0; j < 4; j++) { 224 data[i * 4 + j] = data[(i + faceCanvas.width) * 4 + j]; 225 data[(i + faceCanvas.width * (faceCanvas.height - 1)) * 4 + j] = data[(i + faceCanvas.width * (faceCanvas.height - 2)) * 4 + j]; 226 } 227 } 228 for (i = 1; i < faceCanvas.height - 1; i++) { 229 for (j = 0; j < 4; j++) { 230 data[(i * faceCanvas.width) * 4 + j] = data[(i * faceCanvas.width + 1) * 4 + j]; 231 data[((i + 1) * faceCanvas.width - 1) * 4 + j] = data[((i + 1) * faceCanvas.width - 2) * 4 + j]; 232 } 233 } 234 for (j = 0; j < 4; j++) { 235 data[j] = data[(faceCanvas.width + 1) * 4 + j]; 236 data[(faceCanvas.width - 1) * 4 + j] = data[(faceCanvas.width * 2 - 2) * 4 + j]; 237 data[(faceCanvas.width * (faceCanvas.height - 1)) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 2) + 1) * 4 + j]; 238 data[(faceCanvas.width * faceCanvas.height - 1) * 4 + j] = data[(faceCanvas.width * (faceCanvas.height - 1) - 2) * 4 + j]; 239 } 240 241 // Draw image width duplicated edge pixels on canvas 242 faceContext.putImageData(imgData, 0, 0); 243 244 incLoaded.call(this); 245 }; 246 var incLoaded = function() { 247 if (this.width > 0) { 248 if (fallbackImgSize === undefined) 249 fallbackImgSize = this.width; 250 if (fallbackImgSize != this.width) 251 console.log('Fallback faces have inconsistent widths: ' + fallbackImgSize + ' vs. ' + this.width); 252 } else 253 faceMissing = true; 254 loaded++; 255 if (loaded == 6) { 256 fallbackImgSize = this.width; 257 container.appendChild(world); 258 callback(); 259 } 260 }; 261 faceMissing = false; 262 for (s = 0; s < 6; s++) { 263 var faceImg = new Image(); 264 faceImg.crossOrigin = globalParams.crossOrigin ? globalParams.crossOrigin : 'anonymous'; 265 faceImg.side = s; 266 faceImg.onload = onLoad; 267 faceImg.onerror = incLoaded;
267 // ignore missing face to support partial fallback image 268 if (imageType == 'multires') { 269 faceImg.src = path.replace('%s', sides[s]) + '.' + image.extension; 270 } else { 271 faceImg.src = image[s].src; 272 } 273 } 274 fillMissingFaces(fallbackImgSize); 275 return; 276 } else if (!gl) { 277 console.log('Error: no WebGL support detected!'); 278 throw {type: 'no webgl'}; 279 } 280 if (imageType == 'cubemap') 281 fillMissingFaces(cubeImgWidth); 282 if (image.basePath) { 283 image.fullpath = image.basePath + image.path; 284 } else { 285 image.fullpath = image.path; 286 } 287 image.invTileResolution = 1 / image.tileResolution; 288 289 var vertices = createCube(); 290 vtmps = []; 291 for (s = 0; s < 6; s++) { 292 vtmps[s] = vertices.slice(s * 12, s * 12 + 12); 293 vertices = createCube(); 294 } 295 296 // Make sure image isn't too big 297 var maxWidth = 0; 298 if (imageType == 'equirectangular') { 299 maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE); 300 if (Math.max(image.width / 2, image.height) > maxWidth) { 301 console.log('Error: The image is too big; it\'s ' + image.width + 'px wide, '+ 302 'but this device\'s maximum supported size is ' + (maxWidth * 2) + 'px.'); 303 throw {type: 'webgl size error', width: image.width, maxWidth: maxWidth * 2}; 304 } 305 } else if (imageType == 'cubemap') { 306 if (cubeImgWidth > gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)) { 307 console.log('Error: The image is too big; it\'s ' + cubeImgWidth + 'px wide, ' + 308 'but this device\'s maximum supported size is ' + maxWidth + 'px.'); 309 throw {type: 'webgl size error', width: cubeImgWidth, maxWidth: maxWidth}; 310 } 311 } 312 313 // Store horizon pitch and roll if applicable 314 if (params !== undefined) { 315 var horizonPitch = isNaN(params.horizonPitch) ? 0 : Number(params.horizonPitch), 316 horizonRoll = isNaN(params.horizonRoll) ? 0 : Number(params.horizonRoll); 317 if (horizonPitch != 0 || horizonRoll != 0) 318 pose = [horizonPitch, horizonRoll]; 319 } 320 321 // Set 2d texture binding 322 var glBindType = gl.TEXTURE_2D; 323 324 // Create viewport for entire canvas 325 gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 326 327 // Check precision support 328 if (gl.getShaderPrecisionFormat) { 329 var precision = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); 330 if (precision && precision.precision < 1) { 331 // `highp` precision not supported; https://stackoverflow.com/a/33308927 332 fragEquiCubeBase = fragEquiCubeBase.replace('highp', 'mediump'); 333 } 334 } 335 336 // Create vertex shader 337 vs = gl.createShader(gl.VERTEX_SHADER); 338 var vertexSrc = v; 339 if (imageType == 'multires') { 340 vertexSrc = vMulti; 341 } 342 gl.shaderSource(vs, vertexSrc); 343 gl.compileShader(vs); 344 345 // Create fragment shader 346 fs = gl.createShader(gl.FRAGMENT_SHADER); 347 var fragmentSrc = fragEquirectangular; 348 if (imageType == 'cubemap') { 349 glBindType = gl.TEXTURE_CUBE_MAP; 350 fragmentSrc = fragCube; 351 } else if (imageType == 'multires') { 352 fragmentSrc = fragMulti; 353 } 354 gl.shaderSource(fs, fragmentSrc); 355 gl.compileShader(fs); 356 357 // Link WebGL program 358 program = gl.createProgram(); 359 gl.attachShader(program, vs); 360 gl.attachShader(program, fs); 361 gl.linkProgram(program); 362 363 // Log errors 364 if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) 365 console.log(gl.getShaderInfoLog(vs)); 366 if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) 367 console.log(gl.getShaderInfoLog(fs)); 368 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) 369 console.log(gl.getProgramInfoLog(program)); 370 371 // Use WebGL program 372 gl.useProgram(program); 373 374 program.drawInProgress = false;
375 376 // Set background clear color (does not apply to cubemap/fallback image) 377 var color = params.backgroundColor ? params.backgroundColor : [0, 0, 0]; 378 gl.clearColor(color[0], color[1], color[2], 1.0); 379 gl.clear(gl.COLOR_BUFFER_BIT); 380 381 // Look up texture coordinates location 382 program.texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); 383 gl.enableVertexAttribArray(program.texCoordLocation); 384 385 if (imageType != 'multires') { 386 // Provide texture coordinates for rectangle 387 if (!texCoordBuffer) 388 texCoordBuffer = gl.createBuffer(); 389 gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); 390 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,1,1,1,1,-1,-1,1,1,-1,-1,-1]), gl.STATIC_DRAW); 391 gl.vertexAttribPointer(program.texCoordLocation, 2, gl.FLOAT, false, 0, 0); 392 393 // Pass aspect ratio 394 program.aspectRatio = gl.getUniformLocation(program, 'u_aspectRatio'); 395 gl.uniform1f(program.aspectRatio, gl.drawingBufferWidth / gl.drawingBufferHeight); 396 397 // Locate psi, theta, focal length, horizontal extent, vertical extent, and vertical offset 398 program.psi = gl.getUniformLocation(program, 'u_psi'); 399 program.theta = gl.getUniformLocation(program, 'u_theta'); 400 program.f = gl.getUniformLocation(program, 'u_f'); 401 program.h = gl.getUniformLocation(program, 'u_h'); 402 program.v = gl.getUniformLocation(program, 'u_v'); 403 program.vo = gl.getUniformLocation(program, 'u_vo'); 404 program.rot = gl.getUniformLocation(program, 'u_rot'); 405 406 // Pass horizontal extent, vertical extent, and vertical offset 407 gl.uniform1f(program.h, haov / (Math.PI * 2.0)); 408 gl.uniform1f(program.v, vaov / Math.PI); 409 gl.uniform1f(program.vo, voffset / Math.PI * 2); 410 411 // Set background color 412 if (imageType == 'equirectangular') { 413 program.backgroundColor = gl.getUniformLocation(program, 'u_backgroundColor'); 414 gl.uniform4fv(program.backgroundColor, color.concat([1])); 415 } 416 417 // Create texture 418 program.texture = gl.createTexture(); 419 gl.bindTexture(glBindType, program.texture); 420 421 // Upload images to texture depending on type 422 if (imageType == 'cubemap') { 423 // Load all six sides of the cube map 424 gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[1]); 425 gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[3]); 426 gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[4]); 427 gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[5]); 428 gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[0]); 429 gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[2]); 430 } else { 431 if (image.width <= maxWidth) { 432 gl.uniform1i(gl.getUniformLocation(program, 'u_splitImage'), 0); 433 // Upload image to the texture 434 gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); 435 } else { 436 // Image needs to be split into two parts due to texture size limits 437 gl.uniform1i(gl.getUniformLocation(program, 'u_splitImage'), 1); 438 439 // Draw image on canvas 440 var cropCanvas = document.createElement('canvas'); 441 cropCanvas.width = image.width / 2; 442 cropCanvas.height = image.height; 443 var cropContext = cropCanvas.getContext('2d'); 444 cropContext.drawImage(image, 0, 0); 445 446 // Upload first half of image to the texture 447 var cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height); 448 gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage); 449 450 // Create and bind texture for second half of image 451 program.texture2 = gl.createTexture(); 452 gl.activeTexture(gl.TEXTURE1); 453 gl.bindTexture(glBindType, program.texture2); 454 gl.uniform1i(gl.getUniformLocation(program, 'u_image1'), 1); 455 456 // Upload second half of image to the texture
457 cropContext.drawImage(image, -image.width / 2, 0); 458 cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height); 459 gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage); 460 461 // Set parameters for rendering any size 462 gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 463 gl.texParameteri(glBindType, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 464 gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 465 gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 466 467 // Reactivate first texture unit 468 gl.activeTexture(gl.TEXTURE0); 469 } 470 } 471 472 // Set parameters for rendering any size 473 gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 474 gl.texParameteri(glBindType, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 475 gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 476 gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 477 478 } else { 479 // Look up vertex coordinates location 480 program.vertPosLocation = gl.getAttribLocation(program, 'a_vertCoord'); 481 gl.enableVertexAttribArray(program.vertPosLocation); 482 483 // Create buffers 484 if (!cubeVertBuf) 485 cubeVertBuf = gl.createBuffer(); 486 if (!cubeVertTexCoordBuf) 487 cubeVertTexCoordBuf = gl.createBuffer(); 488 if (!cubeVertIndBuf) 489 cubeVertIndBuf = gl.createBuffer(); 490 491 // Bind texture coordinate buffer and pass coordinates to WebGL 492 gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertTexCoordBuf); 493 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0,1,0,1,1,0,1]), gl.STATIC_DRAW); 494 495 // Bind square index buffer and pass indicies to WebGL 496 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertIndBuf); 497 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0,1,2,0,2,3]), gl.STATIC_DRAW); 498 499 // Find uniforms 500 program.perspUniform = gl.getUniformLocation(program, 'u_perspMatrix'); 501 program.cubeUniform = gl.getUniformLocation(program, 'u_cubeMatrix'); 502 //program.colorUniform = gl.getUniformLocation(program, 'u_color'); 503 504 program.level = -1; 505 506 program.currentNodes = []; 507 program.nodeCache = []; 508 program.nodeCacheTimestamp = 0; 509 } 510 511 // Check if there was an error 512 var err = gl.getError(); 513 if (err !== 0) { 514 console.log('Error: Something went wrong with WebGL!', err); 515 throw {type: 'webgl error'}; 516 } 517 518 callback(); 519 }; 520 521 /** 522 * Destroy renderer. 523 * @memberof Renderer 524 * @instance 525 */ 526 this.destroy = function() { 527 if (container !== undefined) { 528 if (canvas !== undefined && container.contains(canvas)) { 529 container.removeChild(canvas); 530 } 531 if (world !== undefined && container.contains(world)) { 532 container.removeChild(world); 533 } 534 } 535 if (gl) { 536 // The spec says this is only supposed to simulate losing the WebGL 537 // context, but in practice it tends to actually free the memory. 538 var extension = gl.getExtension('WEBGL_lose_context'); 539 if (extension) 540 extension.loseContext(); 541 } 542 }; 543 544 /** 545 * Resize renderer (call after resizing container). 546 * @memberof Renderer 547 * @instance 548 */ 549 this.resize = function() { 550 var pixelRatio = window.devicePixelRatio || 1; 551 canvas.width = canvas.clientWidth * pixelRatio; 552 canvas.height = canvas.clientHeight * pixelRatio; 553 if (gl) { 554 if (gl.getError() == 1286) 555 handleWebGLError1286(); 556 gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 557 if (imageType != 'multires') { 558 gl.uniform1f(program.aspectRatio, canvas.clientWidth / canvas.clientHeight); 559 } 560 } 561 }; 562 // Initialize canvas size 563 this.resize(); 564 565 /** 566 * Set renderer horizon pitch and roll. 567 * @memberof Renderer 568 * @instance 569 * @param {number} horizonPitch - Pitch of horizon (in radians). 570 * @param {number} horizonRoll - Roll of horizon (in radians). 571 */ 572 this.setPose = function(horizonPitch, horizonRoll) { 573 horizonPitch = isNaN(horizonPitch) ? 0 : Number(horizonPitch); 574 horizonRoll = isNaN(horizonRoll) ? 0 : Number(horizonRoll); 575 if (horizonPitch == 0 && horizonRoll == 0) 576 pose = undefined; 577 else 578 pose = [horizonPitch, horizonRoll]; 579 }; 580 581 /**
582 * Render new view of panorama. 583 * @memberof Renderer 584 * @instance 585 * @param {number} pitch - Pitch to render at (in radians). 586 * @param {number} yaw - Yaw to render at (in radians). 587 * @param {number} hfov - Horizontal field of view to render with (in radians). 588 * @param {Object} [params] - Extra configuration parameters. 589 * @param {number} [params.roll] - Camera roll (in radians). 590 * @param {boolean} [params.returnImage] - Return rendered image? 591 */ 592 this.render = function(pitch, yaw, hfov, params) { 593 var focal, i, s, roll = 0; 594 if (params === undefined) 595 params = {}; 596 if (params.roll) 597 roll = params.roll; 598 599 // Apply pitch and roll transformation if applicable 600 if (pose !== undefined) { 601 var horizonPitch = pose[0], 602 horizonRoll = pose[1]; 603 604 // Calculate new pitch and yaw 605 var orig_pitch = pitch, 606 orig_yaw = yaw, 607 x = Math.cos(horizonRoll) * Math.sin(pitch) * Math.sin(horizonPitch) + 608 Math.cos(pitch) * (Math.cos(horizonPitch) * Math.cos(yaw) + 609 Math.sin(horizonRoll) * Math.sin(horizonPitch) * Math.sin(yaw)), 610 y = -Math.sin(pitch) * Math.sin(horizonRoll) + 611 Math.cos(pitch) * Math.cos(horizonRoll) * Math.sin(yaw), 612 z = Math.cos(horizonRoll) * Math.cos(horizonPitch) * Math.sin(pitch) + 613 Math.cos(pitch) * (-Math.cos(yaw) * Math.sin(horizonPitch) + 614 Math.cos(horizonPitch) * Math.sin(horizonRoll) * Math.sin(yaw)); 615 pitch = Math.asin(Math.max(Math.min(z, 1), -1)); 616 yaw = Math.atan2(y, x); 617 618 // Calculate roll 619 var v = [Math.cos(orig_pitch) * (Math.sin(horizonRoll) * Math.sin(horizonPitch) * Math.cos(orig_yaw) - 620 Math.cos(horizonPitch) * Math.sin(orig_yaw)), 621 Math.cos(orig_pitch) * Math.cos(horizonRoll) * Math.cos(orig_yaw), 622 Math.cos(orig_pitch) * (Math.cos(horizonPitch) * Math.sin(horizonRoll) * Math.cos(orig_yaw) + 623 Math.sin(orig_yaw) * Math.sin(horizonPitch))], 624 w = [-Math.cos(pitch) * Math.sin(yaw), Math.cos(pitch) * Math.cos(yaw)]; 625 var roll_adj = Math.acos(Math.max(Math.min((v[0]*w[0] + v[1]*w[1]) / 626 (Math.sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]) * 627 Math.sqrt(w[0]*w[0]+w[1]*w[1])), 1), -1)); 628 if (v[2] < 0) 629 roll_adj = 2 * Math.PI - roll_adj; 630 roll += roll_adj; 631 } 632 633 // If no WebGL 634 if (!gl && (imageType == 'multires' || imageType == 'cubemap')) { 635 // Determine face transforms 636 s = fallbackImgSize / 2; 637 638 var transforms = { 639 f: 'translate3d(-' + (s + 2) + 'px, -' + (s + 2) + 'px, -' + s + 'px)', 640 b: 'translate3d(' + (s + 2) + 'px, -' + (s + 2) + 'px, ' + s + 'px) rotateX(180deg) rotateZ(180deg)', 641 u: 'translate3d(-' + (s + 2) + 'px, -' + s + 'px, ' + (s + 2) + 'px) rotateX(270deg)', 642 d: 'translate3d(-' + (s + 2) + 'px, ' + s + 'px, -' + (s + 2) + 'px) rotateX(90deg)', 643 l: 'translate3d(-' + s + 'px, -' + (s + 2) + 'px, ' + (s + 2) + 'px) rotateX(180deg) rotateY(90deg) rotateZ(180deg)', 644 r: 'translate3d(' + s + 'px, -' + (s + 2) + 'px, -' + (s + 2) + 'px) rotateY(270deg)' 645 }; 646 focal = 1 / Math.tan(hfov / 2); 647 var zoom = focal * canvas.clientWidth / 2 + 'px'; 648 var transform = 'perspective(' + zoom + ') translateZ(' + zoom + ') rotateX(' + pitch + 'rad) rotateY(' + yaw + 'rad) '; 649 650 // Apply face transforms 651 var faces = Object.keys(transforms); 652 for (i = 0; i < 6; i++) { 653 var face = world.querySelector('.pnlm-' + faces[i] + 'face'); 654 if (!face) 655 continue; // ignore missing face to support partial cubemap/fallback image 656 face.style.webkitTransform = transform + transforms[faces[i]]; 657 face.style.transform = transform + transforms[faces[i]]; 658 } 659 return; 660 } 661 662 if (imageType != 'multires') { 663 // Calculate focal length from vertical field of view 664 var vfov = 2 * Math.atan(Math.tan(hfov * 0.5) / (gl.drawingBufferWidth / gl.drawingBufferHeight)); 665 focal = 1 / Math.tan(vfov * 0.5); 666 667 // Pass psi, theta, roll, and focal length 668 gl.uniform1f(program.psi, yaw); 669 gl.uniform1f(program.theta, pitch); 670 gl.uniform1f(program.rot, roll); 671 gl.uniform1f(program.f, focal); 672 673 if (dynamic === true) { 674 // Update texture if dynamic 675 if (imageType == 'equirectangular') { 676 gl.bindTexture(gl.TEXTURE_2D, program.texture); 677 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); 678 } 679 } 680 681 // Draw using current buffer 682 gl.drawArrays(gl.TRIANGLES, 0, 6); 683 684 } else { 685 // Create perspective matrix 686 var perspMatrix = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 100.0); 687 688 // Find correct zoom level 689 checkZoom(hfov); 690 691 // Create rotation matrix 692 var matrix = identityMatrix3(); 693 matrix = rotateMatrix(matrix, -roll, 'z'); 694 matrix = rotateMatrix(matrix, -pitch, 'x'); 695 matrix = rotateMatrix(matrix, yaw, 'y'); 696 matrix = makeMatrix4(matrix); 697 698 // Set matrix uniforms 699 gl.uniformMatrix4fv(program.perspUniform, false, new Float32Array(trans
vendor: 16,328 bytes, lines 699-1101
699poseMatrix4(perspMatrix))); 700 gl.uniformMatrix4fv(program.cubeUniform, false, new Float32Array(transposeMatrix4(matrix))); 701 702 // Find current nodes 703 var rotPersp = rotatePersp(perspMatrix, matrix); 704 program.nodeCache.sort(multiresNodeSort); 705 if (program.nodeCache.length > 200 && 706 program.nodeCache.length > program.currentNodes.length + 50) { 707 // Remove older nodes from cache 708 var removed = program.nodeCache.splice(200, program.nodeCache.length - 200); 709 for (var j = 0; j < removed.length; j++) { 710 // Explicitly delete textures 711 gl.deleteTexture(removed[j].texture); 712 } 713 } 714 program.currentNodes = []; 715 716 var sides = ['f', 'b', 'u', 'd', 'l', 'r']; 717 for (s = 0; s < 6; s++) { 718 var ntmp = new MultiresNode(vtmps[s], sides[s], 1, 0, 0, image.fullpath); 719 testMultiresNode(rotPersp, ntmp, pitch, yaw, hfov); 720 } 721 722 program.currentNodes.sort(multiresNodeRenderSort); 723 724 // Unqueue any pending requests for nodes that are no longer visible 725 for (i = pendingTextureRequests.length - 1; i >= 0; i--) { 726 if (program.currentNodes.indexOf(pendingTextureRequests[i].node) === -1) { 727 pendingTextureRequests[i].node.textureLoad = false; 728 pendingTextureRequests.splice(i, 1); 729 } 730 } 731 732 // Allow one request to be pending, so that we can create a texture buffer for that in advance of loading actually beginning 733 if (pendingTextureRequests.length === 0) { 734 for (i = 0; i < program.currentNodes.length; i++) { 735 var node = program.currentNodes[i]; 736 if (!node.texture && !node.textureLoad) { 737 node.textureLoad = true; 738 739 setTimeout(processNextTile, 0, node); 740 741 // Only process one tile per frame to improve responsiveness 742 break; 743 } 744 } 745 } 746 747 // Draw tiles 748 multiresDraw(); 749 } 750 751 if (params.returnImage !== undefined) { 752 return canvas.toDataURL('image/png'); 753 } 754 }; 755 756 /** 757 * Check if images are loading. 758 * @memberof Renderer 759 * @instance 760 * @returns {boolean} Whether or not images are loading. 761 */ 762 this.isLoading = function() { 763 if (gl && imageType == 'multires') { 764 for ( var i = 0; i < program.currentNodes.length; i++ ) { 765 if (!program.currentNodes[i].textureLoaded) { 766 return true; 767 } 768 } 769 } 770 return false; 771 }; 772 773 /** 774 * Retrieve renderer's canvas. 775 * @memberof Renderer 776 * @instance 777 * @returns {HTMLElement} Renderer's canvas. 778 */ 779 this.getCanvas = function() { 780 return canvas; 781 }; 782 783 /** 784 * Sorting method for multires nodes. 785 * @private 786 * @param {MultiresNode} a - First node. 787 * @param {MultiresNode} b - Second node. 788 * @returns {number} Base tiles first, then higher timestamp first. 789 */ 790 function multiresNodeSort(a, b) { 791 // Base tiles are always first 792 if (a.level == 1 && b.level != 1) { 793 return -1; 794 } 795 if (b. level == 1 && a.level != 1) { 796 return 1; 797 } 798 799 // Higher timestamp first 800 return b.timestamp - a.timestamp; 801 } 802 803 /** 804 * Sorting method for multires node rendering. 805 * @private 806 * @param {MultiresNode} a - First node. 807 * @param {MultiresNode} b - Second node. 808 * @returns {number} Lower zoom levels first, then closest to center first. 809 */ 810 function multiresNodeRenderSort(a, b) { 811 // Lower zoom levels first 812 if (a.level != b.level) { 813 return a.level - b.level; 814 } 815 816 // Lower distance from center first 817 return a.diff - b.diff; 818 } 819 820 /** 821 * Draws multires nodes. 822 * @private 823 */ 824 function multiresDraw() { 825 if (!program.drawInProgress) { 826 program.drawInProgress = true; 827 gl.clear(gl.COLOR_BUFFER_BIT); 828 for ( var i = 0; i < program.currentNodes.length; i++ ) { 829 if (program.currentNodes[i].textureLoaded > 1) { 830 //var color = program.currentNodes[i].color; 831 //gl.uniform4f(program.colorUniform, color[0], color[1], color[2], 1.0); 832 833 // Bind vertex buffer and pass vertices to WebGL 834 gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertBuf); 835 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(program.currentNodes[i].vertices), gl.STATIC_DRAW); 836 gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0); 837 838 // Prep for texture 839 gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertTexCoordBuf); 840 gl.vertexAttribPointer(program.texCoordLocation, 2, gl.FLOAT, false, 0, 0); 841 842 // Bind texture and draw tile 843 gl.bindTexture(gl.TEXTURE_2D, program.currentNodes[i].texture); // Bind program.currentNodes[i].texture to TEXTURE0 844 gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); 845 } 846 } 847 program.drawInProgress = false; 848 } 849 } 850 851 /** 852 * Creates new multires node. 853 * @constructor 854 * @private 855 * @param {number[]} vertices - Node's verticies. 856 * @param {string} side - Node's cube face. 857 * @param {number} level - Node's zoom level. 858 * @param {number} x - Node's x position. 859 * @param {number} y - Node's y position. 860 * @param {string} path - Node's path. 861 */ 862 function MultiresNode(vertices, side, level, x, y, path) { 863 this.vertices = vertices; 864 this.side = side; 865 this.level = level; 866 this.x = x; 867 this.y = y; 868 this.path = path.replace('%s',side).replace('%l',level).replace('%x',x).replace('%y',y); 869 } 870 871 /** 872 * Test if multires node is visible. If it is, add it to current nodes, 873 * load its texture, and load appropriate child nodes. 874 * @private 875 * @param {number[]} rotPersp - Rotated perspective matrix. 876 * @param {MultiresNode} node - Multires node to check. 877 * @param {number} pitch - Pitch to check at. 878 * @param {number} yaw - Yaw to check at. 879 * @param {number} hfov - Horizontal field of view to check at. 880 */ 881 function testMultiresNode(rotPersp, node, pitch, yaw, hfov) { 882 if (checkSquareInView(rotPersp, node.vertices)) { 883 // Calculate central angle between center of view and center of tile 884 var v = node.vertices; 885 var x = v[0] + v[3] + v[6] + v[ 9]; 886 var y = v[1] + v[4] + v[7] + v[10]; 887 var z = v[2] + v[5] + v[8] + v[11]; 888 var r = Math.sqrt(x*x + y*y + z*z); 889 var theta = Math.asin(z / r); 890 var phi = Math.atan2(y, x); 891 var ydiff = phi - yaw; 892 ydiff += (ydiff > Math.PI) ? -2 * Math.PI : (ydiff < -Math.PI) ? 2 * Math.PI : 0; 893 ydiff = Math.abs(ydiff); 894 node.diff = Math.acos(Math.sin(pitch) * Math.sin(theta) + Math.cos(pitch) * Math.cos(theta) * Math.cos(ydiff)); 895 896 // Add node to current nodes and load texture if needed 897 var inCurrent = false; 898 for (var k = 0; k < program.nodeCache.length; k++) { 899 if (program.nodeCache[k].path == node.path) { 900 inCurrent = true; 901 program.nodeCache[k].timestamp = program.nodeCacheTimestamp++; 902 program.nodeCache[k].diff = node.diff; 903 program.currentNodes.push(program.nodeCache[k]); 904 break; 905 } 906 } 907 if (!inCurrent) { 908 //node.color = [Math.random(), Math.random(), Math.random()]; 909 node.timestamp = program.nodeCacheTimestamp++; 910 program.currentNodes.push(node); 911 program.nodeCache.push(node); 912 } 913 914 // TODO: Test error 915 // Create child nodes 916 if (node.level < program.level) { 917 var cubeSize = image.cubeResolution * Math.pow(2, node.level - image.maxLevel); 918 var numTiles = Math.ceil(cubeSize * image.invTileResolution) - 1; 919 var doubleTileSize = cubeSize % image.tileResolution * 2; 920 var lastTileSize = (cubeSize * 2) % image.tileResolution; 921 if (lastTileSize === 0) { 922 lastTileSize = image.tileResolution; 923 } 924 if (doubleTileSize === 0) { 925 doubleTileSize = image.tileResolution * 2; 926 } 927 var f = 0.5; 928 if (node.x == numTiles || node.y == numTiles) { 929 f = 1.0 - image.tileResolution / (image.tileResolution + lastTileSize); 930 } 931 var i = 1.0 - f; 932 var children = []; 933 var vtmp, ntmp; 934 var f1 = f, f2 = f, f3 = f, i1 = i, i2 = i, i3 = i; 935 // Handle non-symmetric tiles 936 if (lastTileSize < image.tileResolution) { 937 if (node.x == numTiles && node.y != numTiles) { 938 f2 = 0.5; 939 i2 = 0.5; 940 if (node.side == 'd' || node.side == 'u') { 941 f3 = 0.5; 942 i3 = 0.5; 943 } 944 } else if (node.x != numTiles && node.y == numTiles) { 945 f1 = 0.5; 946 i1 = 0.5; 947 if (node.side == 'l' || node.side == 'r') { 948 f3 = 0.5; 949 i3 = 0.5; 950 } 951 } 952 } 953 // Handle small tiles that have fewer than four children 954 if (doubleTileSize <= image.tileResolution) { 955 if (node.x == numTiles) { 956 f1 = 0; 957 i1 = 1; 958 if (node.side == 'l' || node.side == 'r') { 959 f3 = 0; 960 i3 = 1; 961 } 962 } 963 if (node.y == numTiles) { 964 f2 = 0; 965 i2 = 1; 966 if (node.side == 'd' || node.side == 'u') { 967 f3 = 0; 968 i3 = 1; 969 } 970 } 971 } 972 973 vtmp = [ v[0], v[1], v[2], 974 v[0]*f1+v[3]*i1, v[1]*f+v[4]*i, v[2]*f3+v[5]*i3, 975 v[0]*f1+v[6]*i1, v[1]*f2+v[7]*i2, v[2]*f3+v[8]*i3, 976 v[0]*f+v[9]*i, v[1]*f2+v[10]*i2, v[2]*f3+v[11]*i3 977 ]; 978 ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x*2, node.y*2, image.fullpath); 979 children.push(ntmp); 980 if (!(node.x == numTiles && doubleTileSize <= image.tileResolution)) { 981 vtmp = [v[0]*f1+v[3]*i1, v[1]*f+v[4]*i, v[2]*f3+v[5]*i3, 982 v[3], v[4], v[5], 983 v[3]*f+v[6]*i, v[4]*f2+v[7]*i2, v[5]*f3+v[8]*i3, 984 v[0]*f1+v[6]*i1, v[1]*f2+v[7]*i2, v[2]*f3+v[8]*i3 985 ]; 986 ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x*2+1, node.y*2, image.fullpath); 987 children.push(ntmp); 988 } 989 if (!(node.x == numTiles && doubleTileSize <= image.tileResolution) && 990 !(node.y == numTiles && doubleTileSize <= image.tileResolution)) { 991 vtmp = [v[0]*f1+v[6]*i1, v[1]*f2+v[7]*i2, v[2]*f3+v[8]*i3, 992 v[3]*f+v[6]*i, v[4]*f2+v[7]*i2, v[5]*f3+v[8]*i3, 993 v[6], v[7], v[8], 994 v[9]*f1+v[6]*i1, v[10]*f+v[7]*i, v[11]*f3+v[8]*i3 995 ]; 996 ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x*2+1, node.y*2+1, image.fullpath); 997 children.push(ntmp); 998 } 999 if (!(node.y == numTiles && doubleTileSize <= image.tileResolution)) { 1000 vtmp = [ v[0]*f+v[9]*i, v[1]*f2+v[10]*i2, v[2]*f3+v[11]*i3, 1001 v[0]*f1+v[6]*i1, v[1]*f2+v[7]*i2, v[2]*f3+v[8]*i3, 1002 v[9]*f1+v[6]*i1, v[10]*f+v[7]*i, v[11]*f3+v[8]*i3, 1003 v[9], v[10], v[11] 1004 ]; 1005 ntmp = new MultiresNode(vtmp, node.side, node.level + 1, node.x*2, node.y*2+1, image.fullpath); 1006 children.push(ntmp); 1007 } 1008 for (var j = 0; j < children.length; j++) { 1009 testMultiresNode(rotPersp, children[j], pitch, yaw, hfov); 1010 } 1011 } 1012 } 1013 } 1014 1015 /** 1016 * Creates cube vertex array. 1017 * @private 1018 * @returns {number[]} Cube vertex array. 1019 */ 1020 function createCube() { 1021 return [-1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, // Front face 1022 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, // Back face 1023 -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, // Up face 1024 -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // Down face 1025 -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, // Left face 1026 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1 // Right face 1027 ]; 1028 } 1029 1030 /** 1031 * Creates 3x3 identity matrix. 1032 * @private 1033 * @returns {number[]} Identity matrix. 1034 */ 1035 function identityMatrix3() { 1036 return [ 1037 1, 0, 0, 1038 0, 1, 0, 1039 0, 0, 1 1040 ]; 1041 } 1042 1043 /** 1044 * Rotates a 3x3 matrix. 1045 * @private 1046 * @param {number[]} m - Matrix to rotate. 1047 * @param {number[]} angle - Angle to rotate by in radians. 1048 * @param {string} axis - Axis to rotate about (`x`, `y`, or `z`). 1049 * @returns {number[]} Rotated matrix. 1050 */ 1051 function rotateMatrix(m, angle, axis) { 1052 var s = Math.sin(angle); 1053 var c = Math.cos(angle); 1054 if (axis == 'x') { 1055 return [ 1056 m[0], c*m[1] + s*m[2], c*m[2] - s*m[1], 1057 m[3], c*m[4] + s*m[5], c*m[5] - s*m[4], 1058 m[6], c*m[7] + s*m[8], c*m[8] - s*m[7] 1059 ]; 1060 } 1061 if (axis == 'y') { 1062 return [ 1063 c*m[0] - s*m[2], m[1], c*m[2] + s*m[0], 1064 c*m[3] - s*m[5], m[4], c*m[5] + s*m[3], 1065 c*m[6] - s*m[8], m[7], c*m[8] + s*m[6] 1066 ]; 1067 } 1068 if (axis == 'z') { 1069 return [ 1070 c*m[0] + s*m[1], c*m[1] - s*m[0], m[2], 1071 c*m[3] + s*m[4], c*m[4] - s*m[3], m[5], 1072 c*m[6] + s*m[7], c*m[7] - s*m[6], m[8] 1073 ]; 1074 } 1075 } 1076 1077 /** 1078 * Turns a 3x3 matrix into a 4x4 matrix. 1079 * @private 1080 * @param {number[]} m - Input matrix. 1081 * @returns {number[]} Expanded matrix. 1082 */ 1083 function makeMatrix4(m) { 1084 return [ 1085 m[0], m[1], m[2], 0, 1086 m[3], m[4], m[5], 0, 1087 m[6], m[7], m[8], 0, 1088 0, 0, 0, 1 1089 ]; 1090 } 1091 1092 /** 1093 * Transposes a 4x4 matrix. 1094 * @private 1095 * @param {number[]} m - Input matrix. 1096 * @returns {number[]} Transposed matrix. 1097 */ 1098 function transposeMatrix4(m) { 1099 return [ 1100 m[ 0], m[ 4], m[ 8], m[12], 1101 m[ 1], m[ 5], m[ 9], m[13],
1102 m[ 2], m[ 6], m[10], m[14], 1103 m[ 3], m[ 7], m[11], m[15] 1104 ]; 1105 } 1106 1107 /** 1108 * Creates a perspective matrix. 1109 * @private 1110 * @param {number} hfov - Desired horizontal field of view. 1111 * @param {number} aspect - Desired aspect ratio. 1112 * @param {number} znear - Near distance. 1113 * @param {number} zfar - Far distance. 1114 * @returns {number[]} Generated perspective matrix. 1115 */ 1116 function makePersp(hfov, aspect, znear, zfar) { 1117 var fovy = 2 * Math.atan(Math.tan(hfov/2) * gl.drawingBufferHeight / gl.drawingBufferWidth); 1118 var f = 1 / Math.tan(fovy/2); 1119 return [ 1120 f/aspect, 0, 0, 0, 1121 0, f, 0, 0, 1122 0, 0, (zfar+znear)/(znear-zfar), (2*zfar*znear)/(znear-zfar), 1123 0, 0, -1, 0 1124 ]; 1125 } 1126 1127 /** 1128 * Processes a loaded texture image into a WebGL texture. 1129 * @private 1130 * @param {Image} img - Input image. 1131 * @param {WebGLTexture} tex - Texture to bind image to. 1132 */ 1133 function processLoadedTexture(img, tex) { 1134 gl.bindTexture(gl.TEXTURE_2D, tex); 1135 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img); 1136 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 1137 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 1138 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 1139 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 1140 gl.bindTexture(gl.TEXTURE_2D, null); 1141 } 1142 1143 var pendingTextureRequests = []; 1144 1145 // Based on http://blog.tojicode.com/2012/03/javascript-memory-optimization-and.html 1146 var loadTexture = (function() { 1147 var cacheTop = 4; // Maximum number of concurrents loads 1148 var textureImageCache = {}; 1149 var crossOrigin; 1150 1151 function TextureImageLoader() { 1152 var self = this; 1153 this.texture = this.callback = null; 1154 this.image = new Image(); 1155 this.image.crossOrigin = crossOrigin ? crossOrigin : 'anonymous'; 1156 var loadFn = (function() { 1157 if (self.image.width > 0 && self.image.height > 0) { // ignore missing tile to supporting partial image 1158 processLoadedTexture(self.image, self.texture); 1159 self.callback(self.texture, true); 1160 } else { 1161 self.callback(self.texture, false); 1162 } 1163 releaseTextureImageLoader(self); 1164 }); 1165 this.image.addEventListener('load', loadFn); 1166 this.image.addEventListener('error', loadFn); // ignore missing tile file to support partial image, otherwise retry loop causes high CPU load 1167 } 1168 1169 TextureImageLoader.prototype.loadTexture = function(src, texture, callback) { 1170 this.texture = texture; 1171 this.callback = callback; 1172 this.image.src = src; 1173 }; 1174 1175 function PendingTextureRequest(node, src, texture, callback) { 1176 this.node = node; 1177 this.src = src; 1178 this.texture = texture; 1179 this.callback = callback; 1180 } 1181 1182 function releaseTextureImageLoader(til) { 1183 if (pendingTextureRequests.length) { 1184 var req = pendingTextureRequests.shift(); 1185 til.loadTexture(req.src, req.texture, req.callback); 1186 } else 1187 textureImageCache[cacheTop++] = til; 1188 } 1189 1190 for (var i = 0; i < cacheTop; i++) 1191 textureImageCache[i] = new TextureImageLoader(); 1192 1193 return function(node, src, callback, _crossOrigin) { 1194 crossOrigin = _crossOrigin; 1195 var texture = gl.createTexture(); 1196 if (cacheTop) 1197 textureImageCache[--cacheTop].loadTexture(src, texture, callback); 1198 else 1199 pendingTextureRequests.push(new PendingTextureRequest(node, src, texture, callback)); 1200 return texture; 1201 }; 1202 })(); 1203 1204 /** 1205 * Loads image and creates texture for a multires node / tile. 1206 * @private 1207 * @param {MultiresNode} node - Input node. 1208 */ 1209 function processNextTile(node) { 1210 loadTexture(node, node.path + '.' + image.extension, function(texture, loaded) { 1211 node.texture = texture; 1212 node.textureLoaded = loaded ? 2 : 1; 1213 }, globalParams.crossOrigin); 1214 } 1215 1216 /** 1217 * Finds and applies optimal multires zoom level. 1218 * @private 1219 * @param {number} hfov - Horizontal field of view to check at. 1220 */ 1221 function checkZoom(hfov) { 1222 // Find optimal level 1223 var newLevel = 1; 1224 while ( newLevel < image.maxLevel && 1225 gl.drawingBufferWidth >
1225 image.tileResolution * 1226 Math.pow(2, newLevel - 1) * Math.tan(hfov / 2) * 0.707 ) { 1227 newLevel++; 1228 } 1229 1230 // Apply change 1231 program.level = newLevel; 1232 } 1233 1234 /** 1235 * Rotates perspective matrix. 1236 * @private 1237 * @param {number[]} p - Perspective matrix. 1238 * @param {number[]} r - Rotation matrix. 1239 * @returns {number[]} Rotated matrix. 1240 */ 1241 function rotatePersp(p, r) { 1242 return [ 1243 p[ 0]*r[0], p[ 0]*r[1], p[ 0]*r[ 2], 0, 1244 p[ 5]*r[4], p[ 5]*r[5], p[ 5]*r[ 6], 0, 1245 p[10]*r[8], p[10]*r[9], p[10]*r[10], p[11], 1246 -r[8], -r[9], -r[10], 0 1247 ]; 1248 } 1249 1250 /** 1251 * Applies rotated perspective matrix to a 3-vector 1252 * (last element is inverted). 1253 * @private 1254 * @param {number[]} m - Rotated perspective matrix. 1255 * @param {number[]} v - Input 3-vector. 1256 * @returns {number[]} Resulting 4-vector. 1257 */ 1258 function applyRotPerspToVec(m, v) { 1259 return [ 1260 m[ 0]*v[0] + m[ 1]*v[1] + m[ 2]*v[2], 1261 m[ 4]*v[0] + m[ 5]*v[1] + m[ 6]*v[2], 1262 m[11] + m[ 8]*v[0] + m[ 9]*v[1] + m[10]*v[2], 1263 1/(m[12]*v[0] + m[13]*v[1] + m[14]*v[2]) 1264 ]; 1265 } 1266 1267 /** 1268 * Checks if a vertex is visible. 1269 * @private 1270 * @param {number[]} m - Rotated perspective matrix. 1271 * @param {number[]} v - Input vertex. 1272 * @returns {number} 1 or -1 if the vertex is or is not visible, 1273 * respectively. 1274 */ 1275 function checkInView(m, v) { 1276 var vpp = applyRotPerspToVec(m, v); 1277 var winX = vpp[0]*vpp[3]; 1278 var winY = vpp[1]*vpp[3]; 1279 var winZ = vpp[2]*vpp[3]; 1280 var ret = [0, 0, 0]; 1281 1282 if ( winX < -1 ) 1283 ret[0] = -1; 1284 if ( winX > 1 ) 1285 ret[0] = 1; 1286 if ( winY < -1 ) 1287 ret[1] = -1; 1288 if ( winY > 1 ) 1289 ret[1] = 1; 1290 if ( winZ < -1 || winZ > 1 ) 1291 ret[2] = 1; 1292 return ret; 1293 } 1294 1295 /** 1296 * Checks if a square (tile) is visible. 1297 * @private 1298 * @param {number[]} m - Rotated perspective matrix. 1299 * @param {number[]} v - Square's vertex array. 1300 * @returns {boolean} Whether or not the square is visible. 1301 */ 1302 function checkSquareInView(m, v) { 1303 var check1 = checkInView(m, v.slice(0, 3)); 1304 var check2 = checkInView(m, v.slice(3, 6)); 1305 var check3 = checkInView(m, v.slice(6, 9)); 1306 var check4 = checkInView(m, v.slice(9, 12)); 1307 var testX = check1[0] + check2[0] + check3[0] + check4[0]; 1308 if ( testX == -4 || testX == 4 ) 1309 return false; 1310 var testY = check1[1] + check2[1] + check3[1] + check4[1]; 1311 if ( testY == -4 || testY == 4 ) 1312 return false; 1313 var testZ = check1[2] + check2[2] + check3[2] + check4[2]; 1314 return testZ != 4; 1315 1316 1317 } 1318 1319 /** 1320 * On iOS (iPhone 5c, iOS 10.3), this WebGL error occurs when the canvas is 1321 * too big. Unfortuately, there's no way to test for this beforehand, so we 1322 * reduce the canvas size if this error is thrown. 1323 * @private 1324 */ 1325 function handleWebGLError1286() { 1326 console.log('Reducing canvas size due to error 1286!'); 1327 canvas.width = Math.round(canvas.width / 2); 1328 canvas.height = Math.round(canvas.height / 2); 1329 } 1330} 1331 1332// Vertex shader for equirectangular and cube 1333var v = [ 1334'attribute vec2 a_texCoord;', 1335'varying vec2 v_texCoord;', 1336 1337'void main() {', 1338 // Set position 1339 'gl_Position = vec4(a_texCoord, 0.0, 1.0);', 1340 1341 // Pass the coordinates to the fragment shader 1342 'v_texCoord = a_texCoord;', 1343'}' 1344].join(''); 1345 1346// Vertex shader for multires 1347var vMulti = [ 1348'attribute vec3 a_vertCoord;', 1349'attribute vec2 a_texCoord;', 1350 1351'uniform mat4 u_cubeMatrix;', 1352'uniform mat4 u_perspMatrix;', 1353 1354'varying mediump vec2 v_texCoord;', 1355 1356'void main(void) {', 1357 // Set position 1358 'gl_Position = u_perspMatrix * u_cubeMatrix * vec4(a_vertCoord, 1.0);', 1359 1360 // Pass the coordinates to the fragment shader 1361 'v_texCoord = a_texCoord;', 1362'}' 1363].join(''); 1364 1365// Fragment shader 1366var fragEquiCubeBase = [ 1367'precision highp float;', // mediump looks bad on some mobile devices 1368 1369'uniform float u_aspectRatio;', 1370'uniform float u_psi;', 1371'uniform float u_theta;', 1372'uniform float u_f;', 1373'uniform float u_h;', 1374'uniform float u_v;', 1375'uniform float u_vo;', 1376'uniform float u_rot;', 1377
1378'const float PI = 3.14159265358979323846264;', 1379 1380// Texture 1381'uniform sampler2D u_image0;', 1382'uniform sampler2D u_image1;', 1383'uniform bool u_splitImage;', 1384'uniform samplerCube u_imageCube;', 1385 1386// Coordinates passed in from vertex shader 1387'varying vec2 v_texCoord;', 1388 1389// Background color (display for partial panoramas) 1390'uniform vec4 u_backgroundColor;', 1391 1392'void main() {', 1393 // Map canvas/camera to sphere 1394 'float x = v_texCoord.x * u_aspectRatio;', 1395 'float y = v_texCoord.y;', 1396 'float sinrot = sin(u_rot);', 1397 'float cosrot = cos(u_rot);', 1398 'float rot_x = x * cosrot - y * sinrot;', 1399 'float rot_y = x * sinrot + y * cosrot;', 1400 'float sintheta = sin(u_theta);', 1401 'float costheta = cos(u_theta);', 1402 'float a = u_f * costheta - rot_y * sintheta;', 1403 'float root = sqrt(rot_x * rot_x + a * a);', 1404 'float lambda = atan(rot_x / root, a / root) + u_psi;', 1405 'float phi = atan((rot_y * costheta + u_f * sintheta) / root);', 1406].join('\n'); 1407 1408// Fragment shader 1409var fragCube = fragEquiCubeBase + [ 1410 // Look up color from texture 1411 'float cosphi = cos(phi);', 1412 'gl_FragColor = textureCube(u_imageCube, vec3(cosphi*sin(lambda), sin(phi), cosphi*cos(lambda)));', 1413'}' 1414].join('\n'); 1415 1416// Fragment shader 1417var fragEquirectangular = fragEquiCubeBase + [ 1418 // Wrap image 1419 'lambda = mod(lambda + PI, PI * 2.0) - PI;', 1420 1421 // Map texture to sphere 1422 'vec2 coord = vec2(lambda / PI, phi / (PI / 2.0));', 1423 1424 // Look up color from texture 1425 // Map from [-1,1] to [0,1] and flip y-axis 1426 'if(coord.x < -u_h || coord.x > u_h || coord.y < -u_v + u_vo || coord.y > u_v + u_vo)', 1427 'gl_FragColor = u_backgroundColor;', 1428 'else {', 1429 'if(u_splitImage) {', 1430 // Image was split into two textures to work around texture size limits 1431 'if(coord.x < 0.0)', 1432 'gl_FragColor = texture2D(u_image0, vec2((coord.x + u_h) / u_h, (-coord.y + u_v + u_vo) / (u_v * 2.0)));', 1433 'else', 1434 'gl_FragColor = texture2D(u_image1, vec2((coord.x + u_h) / u_h - 1.0, (-coord.y + u_v + u_vo) / (u_v * 2.0)));', 1435 '} else {', 1436 'gl_FragColor = texture2D(u_image0, vec2((coord.x + u_h) / (u_h * 2.0), (-coord.y + u_v + u_vo) / (u_v * 2.0)));', 1437 '}', 1438 '}', 1439'}' 1440].join('\n'); 1441 1442// Fragment shader 1443var fragMulti = [ 1444'varying mediump vec2 v_texCoord;', 1445'uniform sampler2D u_sampler;', 1446//'uniform mediump vec4 u_color;', 1447 1448'void main(void) {', 1449 // Look up color from texture 1450 'gl_FragColor = texture2D(u_sampler, v_texCoord);', 1451// 'gl_FragColor = u_color;', 1452'}' 1453].join(''); 1454 1455return { 1456 renderer: function(container, image, imagetype, dynamic) { 1457 return new Renderer(container, image, imagetype, dynamic); 1458 } 1459}; 1460 1461})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.