diff --git a/src/js/libpannellum.js b/src/js/libpannellum.js index 0ec18ab..c632aec 100644 --- a/src/js/libpannellum.js +++ b/src/js/libpannellum.js @@ -42,7 +42,10 @@ function Renderer(container) { var pose; var image, imageType, dynamic; var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf; + var imageVertices; + var texelWidth, texelHeight; var globalParams; + var epsilon = 1e-15; /** * Initialize renderer. @@ -68,7 +71,7 @@ function Renderer(container) { _imageType = 'equirectangular'; if (_imageType != 'equirectangular' && _imageType != 'cubemap' && - _imageType != 'multires') { + _imageType != 'multires' && _imageType != 'multiresrec') { console.log('Error: invalid image type specified!'); throw {type: 'config error'}; } @@ -297,7 +300,7 @@ function Renderer(container) { var maxWidth = 0; if (imageType == 'equirectangular') { maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE); - if (Math.max(image.width / 2, image.height) > maxWidth) { + if (Math.max(image.width / 2, image.height) > maxWidth && !config.multiRes) { 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}; @@ -338,6 +341,8 @@ function Renderer(container) { fragmentSrc = fragCube; } else if (imageType == 'multires') { fragmentSrc = fragMulti; + } else if (imageType == 'multiresrec') { + fragmentSrc = fragMultiresrec; } gl.shaderSource(fs, fragmentSrc); gl.compileShader(fs); @@ -370,12 +375,12 @@ function Renderer(container) { program.texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); gl.enableVertexAttribArray(program.texCoordLocation); - if (imageType != 'multires') { + if (imageType == 'equirectangular' || imageType == 'cubemap') { // 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.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 @@ -462,7 +467,7 @@ function Renderer(container) { gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - } else { + } else if (imageType == 'multires') { // Look up vertex coordinates location program.vertPosLocation = gl.getAttribLocation(program, 'a_vertCoord'); gl.enableVertexAttribArray(program.vertPosLocation); @@ -477,11 +482,11 @@ function Renderer(container) { // 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.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); + 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'); @@ -493,6 +498,75 @@ function Renderer(container) { program.currentNodes = []; program.nodeCache = []; program.nodeCacheTimestamp = 0; + } else if (imageType == 'multiresrec') { + //TODO + let right = haov / (Math.PI * 2.0); + let left = -right; + let up = (vaov / 2.0 + voffset) / Math.PI * 2.0; + let down = (-vaov / 2.0 + voffset) / Math.PI * 2.0; + imageVertices = [ + [left, up], + [right, up], + [right, down], + [left, down] + ]; + + if (image.originalWidth >= image.originalHeight) { + image.relativeTileSize = (right - left) / (image.originalWidth / image.tileResolution); + } else { + image.relativeTileSize = (up - down) / (image.originalHeight / image.tileResolution); + } + + texelWidth = (right - left) / image.originalWidth; + texelHeight = (up - down) / image.originalHeight; + + // 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); + + // Locate parameters for tile boundaries + program.bt = gl.getUniformLocation(program, 'u_bt'); + program.br = gl.getUniformLocation(program, 'u_br'); + program.bb = gl.getUniformLocation(program, 'u_bb'); + program.bl = gl.getUniformLocation(program, 'u_bl'); + + //The extent of a pixel from the original image in the [-1,1]² space + program.texelWidth = gl.getUniformLocation(program, 'u_texelWidth'); + program.texelHeight = gl.getUniformLocation(program, 'u_texelHeight'); + + program.backgroundColor = gl.getUniformLocation(program, 'u_backgroundColor'); + gl.uniform4fv(program.backgroundColor, color.concat([1])); + + //linearly blend fragments on the border of tiles + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + program.level = -1; + + program.currentNodes = []; + program.nodeCache = []; + program.nodeCacheTimestamp = 0; } // Check if there was an error @@ -639,7 +713,7 @@ function Renderer(container) { return; } - if (imageType != 'multires') { + if (imageType == 'equirectangular' || imageType == 'cubemap') { // 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); @@ -661,7 +735,7 @@ function Renderer(container) { // Draw using current buffer gl.drawArrays(gl.TRIANGLES, 0, 6); - } else { + } else if(imageType == 'multires'){ // Create perspective matrix var perspMatrix = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 100.0); @@ -692,13 +766,13 @@ function Renderer(container) { } } 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 @@ -726,6 +800,66 @@ function Renderer(container) { // Draw tiles multiresDraw(); + } else if (imageType == 'multiresrec') { + //TODO + // Find correct zoom level + checkZoom(hfov); + + // 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); + + // Find current nodes + 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 ntmp = new MultiresNode(imageVertices, "equirectangular", 1, 0, 0, image.fullpath); + testMultiresrecNode(ntmp, yaw, pitch, roll, 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 + multiresrecDraw(); } if (params.returnImage !== undefined) { @@ -740,7 +874,7 @@ function Renderer(container) { * @returns {boolean} Whether or not images are loading. */ this.isLoading = function() { - if (gl && imageType == 'multires') { + if (gl && (imageType == 'multires' || imageType == 'multiresrec')) { for ( var i = 0; i < program.currentNodes.length; i++ ) { if (!program.currentNodes[i].textureLoaded) { return true; @@ -829,6 +963,41 @@ function Renderer(container) { } /** + * Draws multires nodes of equrectangular image. + * @private + */ + function multiresrecDraw() { + 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) { + //transform input parameters from [-1,1]² to [0,1]² in screen coordinates (i.e. y-axis pointing downwards) + let v = program.currentNodes[i].vertices; + let factor = Math.pow(2, image.maxLevel - program.currentNodes[i].level - 1); + + // Upload extents of tile relative to full panorama + gl.uniform1f(program.bb, (-v[0][1] + texelHeight/4 + 1) / 2); + gl.uniform1f(program.br, (v[1][0] - texelWidth/4 + 1) / 2); + gl.uniform1f(program.bt, (-v[2][1] - texelHeight/4 + 1) / 2); + gl.uniform1f(program.bl, (v[3][0] + texelWidth/4 + 1) / 2); + gl.uniform1f(program.texelWidth, factor*texelWidth); + gl.uniform1f(program.texelHeight, factor*texelHeight); + + // Prep for texture + gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); + 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.drawArrays(gl.TRIANGLES, 0, 6); + } + } + program.drawInProgress = false; + } + } + + /** * Creates new multires node. * @constructor * @private @@ -987,7 +1156,7 @@ function Renderer(container) { 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); + 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++) { @@ -996,7 +1165,96 @@ function Renderer(container) { } } } - + + function testMultiresrecNode(node, yaw, pitch, roll, hfov) { + let v = node.vertices; + var children = []; + + // Create rotation matrix + var matrix = identityMatrix3(); + matrix = rotateMatrix(matrix, -yaw, 'y'); + matrix = rotateMatrix(matrix, pitch, 'x'); + matrix = rotateMatrix(matrix, roll, 'z'); + + //compute vertices of viewport and tile on sphere + let vecViewCenter = applyMatrix(matrix, [0, 0, -1]); + let vecNodeCenter = applyMatrix(matrix, imageToSphereCoordinates([(v[0][0] + v[1][0]) / 2, (v[1][1] + v[2][1]) / 2])); + var sum = 0; + for (var i = 0; i < 3; ++i) { + let diff = vecViewCenter[i] - vecNodeCenter[i]; + sum += diff * diff; + } + node.diff = Math.sqrt(sum); + + let frustumVertices = frustumVerticesOnImage(hfov) + .map(v => imageToSphereCoordinates(v)) + .map(v => applyMatrix(matrix, v)); + + let nodeVertices = node.vertices + .map(v => imageToSphereCoordinates(v)); + + var inView = false; + if (node.level <= 2) + inView = true; + else + inView = intersect(frustumVertices, nodeVertices); + + if (inView) { + // 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.timestamp = program.nodeCacheTimestamp++; + program.currentNodes.push(node); + program.nodeCache.push(node); + } + + // Create child nodes + if (node.level < program.level) { + let dimX = v[1][0] - v[0][0]; + let dimY = v[1][1] - v[2][1]; + let x0 = v[0][0]; + let y0 = v[0][1]; + let dimXC = Math.min(dimX, texelWidth * Math.pow(2, image.maxLevel - node.level - 1) * image.tileResolution); + let dimYC = Math.min(dimY, texelHeight * Math.pow(2, image.maxLevel - node.level - 1) * image.tileResolution); + + var children = []; + var childrenIndices = [[0, 0]]; + if (dimX > dimXC) { + childrenIndices.push([1, 0]); + if (dimY > dimYC) + childrenIndices.push([1, 1]); + } + if (dimY > dimYC) + childrenIndices.push([0, 1]); + + for (var child of childrenIndices) { + let vtmp = [ + [x0 + dimXC * child[0], y0 - dimYC * child[1]], + [x0 + (child[0] ? dimX : dimXC), y0 - dimYC * child[1]], + [x0 + (child[0] ? dimX : dimXC), y0 - (child[1] ? dimY : dimYC)], + [x0 + dimXC * child[0], y0 - (child[1] ? dimY : dimYC)] + ] + var ntmp = new MultiresNode(vtmp, "equirectangular", node.level + 1, node.x * 2 + child[0], node.y * 2 + child[1], image.fullpath); + + children.push(ntmp); + } + + for (var j = 0; j < children.length; j++) { + testMultiresrecNode(children[j], yaw, pitch, roll, hfov); + } + } + } + } + /** * Creates cube vertex array. * @private @@ -1108,7 +1366,159 @@ function Renderer(container) { 0, 0, -1, 0 ]; } - + + /** + * @private + * @param {number} hfov - Desired horizontal field of view. + * @returns {number[][]} Array of 2-vectors + */ + function frustumVerticesOnImage(hfov) { + let fovy = 2 * Math.atan(Math.tan(hfov / 2) * gl.drawingBufferHeight / gl.drawingBufferWidth); + let x = hfov / Math.PI / 2; + let y = fovy / Math.PI; + return [ + [-x, y], + [x, y], + [x, -y], + [-x, -y] + ]; + } + + /** + * @private + * @param {number[]} v - Vector from [-1,1]² + * @returns {number[]} 3-vector on the unit sphere which is obtained + * when projecting an equirectangular image (normalized to [-1,1]²) back onto the sphere + */ + function imageToSphereCoordinates(v) { + let z = -Math.cos(v[1] * Math.PI / 2.0) * Math.cos(v[0] * Math.PI); + let x = Math.cos(v[1] * Math.PI / 2.0) * Math.sin(v[0] * Math.PI); + let y = Math.sin(v[1] * Math.PI / 2.0); + return [x, y, z]; + } + + /** + * @private + * @param {number[]} v1 - 3-vector on sphere + * @param {number[]} v2 - 3-vector on sphere + * @returns {number[]} normal of plane based on v1, v2 and (0,0,0) + */ + function normal(v1, v2) { + return [ + (v1[1] * v2[2]) - (v1[2] * v2[1]), + (v1[2] * v2[0]) - (v1[0] * v2[2]), + (v1[0] * v2[1]) - (v1[1] * v2[0]) + ]; + } + + /** + * @private + * @param {number[]} normal - normal vector of a plane + * @param {number[]} v - vector to test for side + * @returns {number} -1, 0, 1 + */ + function orientation(normal, v) { + let scalProd = normal[0] * v[0] + normal[1] * v[1] + normal[2] * v[2]; + return Math.sign(scalProd); + } + + /** + * @private + * @param {number[][]} spolygon - convex spherical polygon defined by its vertices + * @param {number[]} v - spherical vector + * @returns {boolean} is v contained in spolygon? + */ + function containmentOnSphere(spolygon, v) { + for (let i = 0; i < spolygon.length; ++i) { + //normals point to the inside of the polygon + let n = normal(spolygon[i], spolygon[(i + 1) % spolygon.length]); + let o = orientation(n, v); + if (o < 0) + return false; + } + return true; + } + + /** + * @private + * @param {number[][]} sTile - vertices of a (equirectangularly projected) tile on the sphere + * @param {number[]} v - spherical vector + * @returns {number[]} weight vector + */ + function linearCombinationOfBoundVectors(sTile, v) { + //test whether x and z component of v and be expressed + // as a positive linear combination of the vectors + // pointing towords the left and right border of the tile + let a = [(sTile[3][0] + sTile[0][0]) / 2, (sTile[3][2] + sTile[0][2]) / 2]; + let b = [(sTile[1][0] + sTile[2][0]) / 2, (sTile[1][2] + sTile[2][2]) / 2]; + let c = [v[0], v[2]]; + //solve (a b) * x = c + let x2 = (a[0] * c[1] - c[0] * a[1]) / (a[0] * b[1] - b[0] * a[1]); + let x1 = (c[0] * b[1] - b[0] * c[1]) / (a[0] * b[1] - b[0] * a[1]); + return [x1, x2]; + } + + + /** + * compute modified Cohen-Sutherland out codes (with left out = right out) + * @private + * @param {number[][]} sTile - vertices of a (equirectangularly projected) tile on the sphere + * @param {number[]} v - spherical vector + * @returns {number} out code + */ + function outCode(sTile, v) { + let ymax = sTile.map(ver => ver[1]).reduce((acc, val) => Math.max(acc, val), -1); + let ymin = sTile.map(ver => ver[1]).reduce((acc, val) => Math.min(acc, val), 1); + if (Math.abs(ymax - 1.0) <= epsilon && Math.abs(ymin + 1.0) <= epsilon) + return 0; //no information where the tile ends in x/z-direction + var oCode = 0; + if (v[1] > ymax) + oCode |= 8; + if (v[1] < ymin) + oCode |= 4; + + let x = linearCombinationOfBoundVectors(sTile, v); + if (x[0] < 0 || x[1] < 0) + oCode |= 1; + return oCode; + } + + /** + * @private + * @param {number[][]} sTile - vertices of a (equirectangularly projected) tile on the sphere + * @param {number[]} viewpolygon - spherical convex (i.e. not more than 180°) polygon + * @returns {boolean} sTile and viewpolygon intersect + */ + function intersect(viewpolygon, sTile) { + var prevOutCode = outCode(sTile, viewpolygon[3]); + for (var v of viewpolygon) { + var oCode = outCode(sTile, v); + if (!oCode) + return true; + if ((prevOutCode & oCode) == 0) + return true; + prevOutCode = oCode; + } + if (outCode(sTile, viewpolygon[3]) == 1 && outCode(sTile, viewpolygon[2]) == 1) { + let left = linearCombinationOfBoundVectors(sTile, viewpolygon[3]); + let right = linearCombinationOfBoundVectors(sTile, viewpolygon[2]); + if (left[0] > 0 && left[1] < 0 && right[0] < 0 && right[1] > 0) + return true; + } + if (outCode(sTile, viewpolygon[0]) == 1 && outCode(sTile, viewpolygon[1]) == 1) { + let left = linearCombinationOfBoundVectors(sTile, viewpolygon[0]); + let right = linearCombinationOfBoundVectors(sTile, viewpolygon[1]); + if (left[0] > 0 && left[1] < 0 && right[0] < 0 && right[1] > 0) + return true; + } + for (let v of sTile) { + if (containmentOnSphere(viewpolygon, v)) + return true; + } + return false; + } + + /** * Processes a loaded texture image into a WebGL texture. * @private @@ -1132,7 +1542,7 @@ function Renderer(container) { var cacheTop = 4; // Maximum number of concurrents loads var textureImageCache = {}; var crossOrigin; - + function TextureImageLoader() { var self = this; this.texture = this.callback = null; @@ -1215,16 +1625,21 @@ function Renderer(container) { * @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++; + if (image.type == 'multires') { + // 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; + } else { + let f = Math.log2(hfov / gl.drawingBufferWidth / Math.PI / texelWidth); + program.level = image.maxLevel - Math.max(0, Math.round(f)); } - - // Apply change - program.level = newLevel; } /** @@ -1259,6 +1674,35 @@ function Renderer(container) { 1/(m[12]*v[0] + m[13]*v[1] + m[14]*v[2]) ]; } + + /** + * Applies matrix to a 3-vector + * @private + * @param {number[]} m - 3x3-Matrix. + * @param {number[]} v - Input 3-vector. + * @returns {number[]} Resulting 3-vector. + */ + function applyMatrix(m, v) { + return [ + m[0] * v[0] + m[1] * v[1] + m[2] * v[2], + m[3] * v[0] + m[4] * v[1] + m[5] * v[2], + m[6] * v[0] + m[7] * v[1] + m[8] * v[2] + ]; + } + + /** + * @private + * @param {number[]} v - Input 4-vector. + * @returns {number[]} Normalized 4-vector. + */ + function normalize(v) { + return [ + v[0] / v[3], + v[1] / v[3], + v[2] / v[3], + 1 + ]; + } /** * Checks if a vertex is visible. @@ -1358,6 +1802,7 @@ var vMulti = [ '}' ].join(''); + // Fragment shader var fragEquiCubeBase = [ 'precision mediump float;', @@ -1371,6 +1816,14 @@ var fragEquiCubeBase = [ 'uniform float u_vo;', 'uniform float u_rot;', +//boundaries of a tile relative to a full panorama +'uniform float u_bt;', +'uniform float u_br;', +'uniform float u_bb;', +'uniform float u_bl;', +'uniform float u_texelWidth;', +'uniform float u_texelHeight;', + 'const float PI = 3.14159265358979323846264;', // Texture @@ -1435,6 +1888,47 @@ var fragEquirectangular = fragEquiCubeBase + [ '}' ].join('\n'); +var fragMultiresrec = fragEquiCubeBase + [ + // Wrap image + 'lambda = mod(lambda + PI, PI * 2.0) - PI;', + + // Map texture to sphere + 'vec2 coord = vec2(lambda / PI, phi / (PI / 2.0));', + 'vec2 fragCoord = vec2((coord.x + 1.0) / 2.0, (-coord.y + 1.0) / 2.0);', + + // Look up color from texture + // Map from [-1,1] to [0,1] and flip y-axis +// 'if(fragCoord.x < u_bl - u_texelWidth || fragCoord.x > u_br + u_texelWidth || fragCoord.y < u_bb - u_texelHeight || fragCoord.y > u_bb + u_texelHeight){', + //'if (fragCoord.x < u_bl - u_texelWidth || fragCoord.x > u_br + u_texelWidth) {', + // 'gl_FragColor = u_backgroundColor;', + // 'gl_FragColor.w = 0.0;', + //'} else {', + 'x = (fragCoord.x - u_bl)/(u_br - u_bl);', + 'float overflowX = 0.0;', + 'if(x < 0.0){', + 'overflowX = -x/u_texelWidth*(u_br - u_bl);', + 'x = 0.0;', + '} else if (x > 1.0){', + 'overflowX = (x-1.0)/u_texelWidth*(u_br - u_bl);', + 'x = 1.0;', + '}', + + 'y = (fragCoord.y - u_bb)/(u_bt-u_bb);', + 'float overflowY = 0.0;', + 'if(y < 0.0){', + 'overflowY = -y/u_texelHeight*(u_bt-u_bb);', + 'y = 0.0;', + '} else if (y > 1.0){', + 'overflowY = (y-1.0)/u_texelHeight*(u_bt-u_bb);', + 'y = 1.0;', + '}', + + 'gl_FragColor = texture2D(u_image0, vec2(x,y));', + 'gl_FragColor.w = 1.0 - overflowX - overflowY;', +// '}', +'}' +].join('\n'); + var fragEquirectangularMulti = function (rows, cols) { return [ 'precision mediump float;', diff --git a/src/js/pannellum.js b/src/js/pannellum.js index 7eae2ad..da99070 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -320,7 +320,7 @@ function init() { } infoDisplay.load.lbox.style.display = 'block'; infoDisplay.load.lbar.style.display = 'none'; - } else if (config.type == 'multires') { + } else if (config.type == 'multires' || config.type == 'multiresrec') { var c = JSON.parse(JSON.stringify(config.multiRes)); // Deep copy c.loader = config.multiRes.loader; // Avoid "undefined" in path, check (optional) multiRes.basePath, too @@ -379,7 +379,7 @@ function init() { panoImage[i].src = sanitizeURL(p); } } - } else if (config.type == 'multires') { + } else if (config.type == 'multires' || config.type == 'multiresrec') { onImageLoad(); } else { p = '';