From 150dfae33b1d50dcbc7d07c44d7ee03a5f1686fc Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Sat, 17 Feb 2024 15:02:09 -0500 Subject: [PATCH] Use a list of missing multires tiles to avoid trying load them. This avoids the 404 errors that would previously result. A compact encoding is used, with `!` plus the face letter used to specify a new face, `>` plus a base83-encoded number used to specify a new level, and a list of base83-encoded numbers used to specify x and y tile coordinates, where the base83 encoding uses the minimum number of characters to encode the maximum tile number for the level in question. The base83 encoding is the same as the one used for SHT hashes. --- doc/json-config-parameters.md | 6 ++++ src/js/libpannellum.js | 28 ++++++++++++++++++ utils/multires/generate.py | 67 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/doc/json-config-parameters.md b/doc/json-config-parameters.md index 1e18927..8af06c0 100644 --- a/doc/json-config-parameters.md +++ b/doc/json-config-parameters.md @@ -506,6 +506,12 @@ used, the image size should be kept small, since it needs to be loaded with the configuration parameters. +#### `missingTiles` (string) + +This specifies tiles that are missing and should not be loaded. A compact +encoding is used for these data. + + ## Dynamic content specific options diff --git a/src/js/libpannellum.js b/src/js/libpannellum.js index 2e82464..43fb402 100644 --- a/src/js/libpannellum.js +++ b/src/js/libpannellum.js @@ -658,6 +658,28 @@ function Renderer(container, context) { gl.vertexAttribPointer(program.vertPosLocation, 3, gl.FLOAT, false, 0, 0); gl.useProgram(program); } + + // Parse missing tiles list, if it exists + if (image.missingTiles) { + var missingTiles = []; + var perSide = image.missingTiles.split('!'); + var level = -1; + for (var i = 1; i < perSide.length; i++) { + var side = perSide[i].at(0); + var perLevel = perSide[i].indexOf('>') < 0 ? [side, perSide[i].slice(1)] : perSide[i].split('>'); + for (var j = 1; j < perLevel.length; j++) { + if (perSide[i].indexOf('>') >= 0) + var level = shtB83decode(perLevel[j].at(0), 1)[0]; + var maxTileNum = Math.ceil(image.cubeResolution / + Math.pow(2, image.maxLevel - level) / image.tileResolution) - 1; + var numTileDigits = Math.ceil(Math.log(maxTileNum + 1) / Math.log(83)); + var tiles = perLevel[j].slice(1).length > 0 ? shtB83decode(perLevel[j].slice(1), numTileDigits) : [0, 0]; + for (var k = 0; k < tiles.length / 2; k++) + missingTiles.push([side, level, tiles[k * 2], tiles[k * 2 + 1]].toString()); + } + } + image.missingTileList = missingTiles; + } } // Check if there was an error @@ -1069,6 +1091,7 @@ function Renderer(container, context) { // Clear canvas if (clear) gl.clear(gl.COLOR_BUFFER_BIT); + // Determine tiles that need to be drawn var node_paths = {}; for (var i = 0; i < program.currentNodes.length; i++) { @@ -1134,6 +1157,11 @@ function Renderer(container, context) { * @param {number} hfov - Horizontal field of view to check at. */ function testMultiresNode(rotPersp, rotPerspNoClip, node, pitch, yaw, hfov) { + // Don't try to load missing tiles (I wish there were a better way to check than `toString`) + if (image.missingTileList !== undefined && + image.missingTileList.indexOf([node.side, node.level, node.x, node.y].toString()) >= 0) + return; + if (checkSquareInView(rotPersp, node.vertices)) { // In order to determine if this tile resolution needs to be loaded // for this node, start by calculating positions of node corners diff --git a/utils/multires/generate.py b/utils/multires/generate.py index 229f227..1e2d348 100755 --- a/utils/multires/generate.py +++ b/utils/multires/generate.py @@ -61,6 +61,14 @@ try: except: sys.stderr.write("Unable to import pyshtools. Not generating SHT preview.\n") +b83chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" +def b83encode(vals, length): + result = "" + for val in vals: + for i in range(1, length + 1): + result += b83chars[int(val // (83 ** (length - i))) % 83] + return result + def img2shtHash(img, lmax=5): ''' Create spherical harmonic transform (SHT) hash preview. @@ -74,15 +82,6 @@ def img2shtHash(img, lmax=5): quantB = encodeFloat(b / maxVal, 9) return quantR * 19 ** 2 + quantG * 19 + quantB - b83chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" - - def b83encode(vals, length): - result = "" - for val in vals: - for i in range(1, length + 1): - result += b83chars[int(val // (83 ** (length - i))) % 83] - return result - # Calculate SHT coefficients r = pysh.expand.SHExpandDH(img[..., 0], sampling=2, lmax_calc=lmax) g = pysh.expand.SHExpandDH(img[..., 1], sampling=2, lmax_calc=lmax) @@ -210,6 +209,10 @@ partialPano = True if args.haov != -1 and args.vaov != -1 else False colorList = ast.literal_eval(args.backgroundColor) colorTuple = (int(colorList[0]*255), int(colorList[1]*255), int(colorList[2]*255)) +# Don't generate preview for partial panoramas +if haov < 360 or vaov < 180: + genPreview = False + if args.debug: print('maxLevel: '+ str(levels)) print('tileResolution: '+ str(tileSize)) @@ -243,6 +246,7 @@ faces = ['face0000.tif', 'face0001.tif', 'face0002.tif', 'face0003.tif', 'face00 # Generate tiles print('Generating tiles...') +missingTiles = [] for f in range(0, 6): size = cubeSize faceExists = os.path.exists(os.path.join(args.output, faces[f])) @@ -271,8 +275,45 @@ for f in range(0, 6): background = Image.new(tile.mode[:-1], tile.size, colorTuple) background.paste(tile, tile.split()[-1]) tile = background - tile.save(os.path.join(args.output, str(level), faceLetters[f] + str(i) + '_' + str(j) + extension), quality=args.quality) + colors = tile.getcolors(1) + if not genPreview and colors is not None and colors[0][1] == colorTuple: + missingTiles.append((f, level, j, i)) + else: + tile.save(os.path.join(args.output, str(level), faceLetters[f] + str(i) + '_' + str(j) + extension), quality=args.quality) + else: + missingTiles.append((f, level, j, i)) size = int(size / 2) + else: + missingTiles.append((f, level, 0, 0)) + +# Tell viewer not to load missing tiles +if len(missingTiles) > 0: + # Remove children of missing tiles, since they won't be loaded anyway + tilesToRemove = [] + for t in missingTiles: + tilesToRemove.append((t[0], t[1] + 1, t[2] * 2, t[3] * 2)) + tilesToRemove.append((t[0], t[1] + 1, t[2] * 2, t[3] * 2 + 1)) + tilesToRemove.append((t[0], t[1] + 1, t[2] * 2 + 1, t[3] * 2)) + tilesToRemove.append((t[0], t[1] + 1, t[2] * 2 + 1, t[3] * 2 + 1)) + for t in tilesToRemove: + if t in missingTiles: + missingTiles.pop(missingTiles.index(t)) + # Encode missing tile list as string + missingTilesStr = '' + prevFace = prevLevel = None + for missingTile in sorted(missingTiles): + face = missingTile[0] + level = missingTile[1] + if face != prevFace: + missingTilesStr += '!' + faceLetters[face] + #prevLevel = None + if level != prevLevel: + missingTilesStr += '>' + b83encode([level], 1) + maxTileNum = math.ceil(cubeSize / 2**(levels - level) / tileSize) - 1 + numTileDigits = math.ceil(math.log(maxTileNum + 1, 83)) + missingTilesStr += b83encode(missingTile[2:], numTileDigits) + prevFace = face + prevLevel = level # Generate fallback tiles if args.fallbackSize > 0: @@ -297,8 +338,6 @@ if not args.debug: os.remove(os.path.join(args.output, face)) # Generate preview (but not for partial panoramas) -if haov < 360 or vaov < 180: - genPreview = False if genPreview: # Generate SHT-hash preview shtHash = img2shtHash(np.array(Image.open(args.inputFile).resize((1024, 512)))) @@ -328,7 +367,7 @@ if vaov < 180: text.append(' "pitch": ' + str( args.vOffset)+ ',') text.append(' "maxPitch": ' + str(+vaov/2+args.vOffset)+ ',') if colorTuple != (0, 0, 0): - text.append(' "backgroundColor": "' + args.backgroundColor+ '",') + text.append(' "backgroundColor": ' + args.backgroundColor+ ',') if args.avoidbackground and (haov < 360 or vaov < 180): text.append(' "avoidShowingBackground": true,') if args.autoload: @@ -339,6 +378,8 @@ if genPreview: text.append(' "shtHash": "' + shtHash + '",') if args.thumbnailSize > 0: text.append(' "equirectangularThumbnail": "' + equiPreview + '",') +if len(missingTiles) > 0: + text.append(' "missingTiles": "' + missingTilesStr + '",') text.append(' "path": "/%l/%s%y_%x",') if args.fallbackSize > 0: text.append(' "fallbackPath": "/fallback/%s",')