UNPKG

terriajs-cesium

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

483 lines (434 loc) 14.3 kB
import Cartesian3 from "../Core/Cartesian3.js"; import Check from "../Core/Check.js"; import ContextLimits from "../Renderer/ContextLimits.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; import MetadataComponentType from "./MetadataComponentType.js"; import PixelDatatype from "../Renderer/PixelDatatype.js"; import PixelFormat from "../Core/PixelFormat.js"; import RuntimeError from "../Core/RuntimeError.js"; import Sampler from "../Renderer/Sampler.js"; import Texture3D from "../Renderer/Texture3D.js"; import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js"; import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; import TextureWrap from "../Renderer/TextureWrap.js"; /** * @alias Megatexture * @constructor * * @param {Context} context The context in which to create GPU resources. * @param {Cartesian3} dimensions The number of voxels in each dimension of the tile. * @param {number} channelCount The number of channels in the metadata. * @param {MetadataComponentType} componentType The component type of the metadata. * @param {number} [availableTextureMemoryBytes=134217728] An upper limit on the texture memory size in bytes. * * @exception {RuntimeError} The GL context does not support a 3D texture large enough to contain a tile with the given dimensions. * @exception {RuntimeError} Not enough texture memory available to create a megatexture with the given tile dimensions. * * @private */ function Megatexture( context, dimensions, channelCount, componentType, availableTextureMemoryBytes = 134217728, // 128 MB tileCount, ) { // TODO there are a lot of texture packing rules, see https://github.com/CesiumGS/cesium/issues/9572 const pixelDataType = getPixelDataType(componentType); const pixelFormat = getPixelFormat(channelCount); const bytesPerSample = channelCount * MetadataComponentType.getSizeInBytes(componentType); const textureDimension = Megatexture.get3DTextureDimension( dimensions, bytesPerSample, availableTextureMemoryBytes, tileCount, ); const tileCounts = Cartesian3.divideComponents( textureDimension, dimensions, new Cartesian3(), ); /** * @type {number} * @readonly */ this.channelCount = channelCount; /** * @type {MetadataComponentType} * @readonly */ this.componentType = componentType; /** * @type {number} * @readonly */ this.textureMemoryByteLength = bytesPerSample * textureDimension.x * textureDimension.y * textureDimension.z; /** * @type {Cartesian3} * @readonly */ this.tileCounts = Cartesian3.clone(tileCounts, new Cartesian3()); /** * @type {Cartesian3} * @readonly */ this.voxelCountPerTile = Cartesian3.clone(dimensions, new Cartesian3()); /** * @type {number} * @readonly */ this.maximumTileCount = tileCounts.x * tileCounts.y * tileCounts.z; /** * @type {Texture3D} * @readonly */ this.texture = new Texture3D({ context: context, pixelFormat: pixelFormat, pixelDatatype: pixelDataType, flipY: false, width: textureDimension.x, height: textureDimension.y, depth: textureDimension.z, sampler: new Sampler({ wrapR: TextureWrap.CLAMP_TO_EDGE, wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.LINEAR, magnificationFilter: TextureMagnificationFilter.LINEAR, }), }); /** * @type {MegatextureNode[]} * @readonly */ this.nodes = new Array(this.maximumTileCount); for (let tileIndex = 0; tileIndex < this.maximumTileCount; tileIndex++) { this.nodes[tileIndex] = new MegatextureNode(tileIndex); } for (let tileIndex = 0; tileIndex < this.maximumTileCount; tileIndex++) { const node = this.nodes[tileIndex]; node.previousNode = tileIndex > 0 ? this.nodes[tileIndex - 1] : undefined; node.nextNode = tileIndex < this.maximumTileCount - 1 ? this.nodes[tileIndex + 1] : undefined; } /** * @type {MegatextureNode} * @readonly */ this.occupiedList = undefined; /** * @type {MegatextureNode} * @readonly */ this.emptyList = this.nodes[0]; /** * @type {number} * @readonly */ this.occupiedCount = 0; this._nearestSampling = false; } Object.defineProperties(Megatexture.prototype, { /** * Gets or sets the nearest sampling flag. * @type {boolean} */ nearestSampling: { get: function () { return this._nearestSampling; }, set: function (nearestSampling) { //>>includeStart('debug', pragmas.debug); Check.typeOf.bool("nearestSampling", nearestSampling); //>>includeEnd('debug'); if (this._nearestSampling === nearestSampling) { return; } if (nearestSampling) { this.texture.sampler = Sampler.NEAREST; } else { this.texture.sampler = new Sampler({ wrapR: TextureWrap.CLAMP_TO_EDGE, wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.LINEAR, magnificationFilter: TextureMagnificationFilter.LINEAR, }); } this._nearestSampling = nearestSampling; }, }, }); /** * Get the pixel data type to use in a megatexture. * TODO support more * * @param {MetadataComponentType} componentType The component type of the metadata. * @returns {PixelDatatype} The pixel datatype to use for a megatexture. * * @private */ function getPixelDataType(componentType) { if ( componentType === MetadataComponentType.FLOAT32 || componentType === MetadataComponentType.FLOAT64 ) { return PixelDatatype.FLOAT; } else if (componentType === MetadataComponentType.UINT8) { return PixelDatatype.UNSIGNED_BYTE; } } /** * Get the pixel format to use for a megatexture. * * @param {number} channelCount The number of channels in the metadata. Must be 1 to 4. * @returns {PixelFormat} The pixel format to use for a megatexture. * * @private */ function getPixelFormat(channelCount) { switch (channelCount) { case 1: return PixelFormat.RED; case 2: return PixelFormat.RG; case 3: return PixelFormat.RGB; case 4: return PixelFormat.RGBA; } } /** * @alias MegatextureNode * @constructor * * @param {number} index * * @private */ function MegatextureNode(index) { /** * @type {number} */ this.index = index; /** * @type {MegatextureNode} */ this.nextNode = undefined; /** * @type {MegatextureNode} */ this.previousNode = undefined; } /** * Add an array of tile metadata to the megatexture. * @param {Array} data The data to be added. * @returns {number} The index of the tile's location in the megatexture. * * @exception {DeveloperError} Trying to add when there are no empty spots. */ Megatexture.prototype.add = function (data) { if (this.isFull()) { throw new DeveloperError("Trying to add when there are no empty spots"); } // remove head of empty list const node = this.emptyList; this.emptyList = this.emptyList.nextNode; if (defined(this.emptyList)) { this.emptyList.previousNode = undefined; } // make head of occupied list node.nextNode = this.occupiedList; if (defined(node.nextNode)) { node.nextNode.previousNode = node; } this.occupiedList = node; const index = node.index; this.writeDataToTexture(index, data); this.occupiedCount++; return index; }; /** * @param {number} index * @exception {DeveloperError} Megatexture index out of bounds. */ Megatexture.prototype.remove = function (index) { if (index < 0 || index >= this.maximumTileCount) { throw new DeveloperError("Megatexture index out of bounds"); } // remove from list const node = this.nodes[index]; if (defined(node.previousNode)) { node.previousNode.nextNode = node.nextNode; } if (defined(node.nextNode)) { node.nextNode.previousNode = node.previousNode; } // make head of empty list node.nextNode = this.emptyList; if (defined(node.nextNode)) { node.nextNode.previousNode = node; } node.previousNode = undefined; this.emptyList = node; this.occupiedCount--; }; /** * @returns {boolean} */ Megatexture.prototype.isFull = function () { return this.emptyList === undefined; }; /** * Compute a 3D texture dimension that contains the given number of tiles, or as many tiles as can fit within the available texture memory. * Not used outside the class, but exposed for testing. * @param {Cartesian3} tileDimensions The dimensions of one tile in number of voxels. * @param {number} bytesPerSample The number of bytes per voxel sample. * @param {number} availableTextureMemoryBytes An upper limit on the texture memory size in bytes. * @param {number} [tileCount] The total number of tiles in the tileset. * @returns {Cartesian3} The computed 3D texture dimensions. * * @exception {RuntimeError} The GL context does not support a 3D texture large enough to contain a tile with the given dimensions. * @exception {RuntimeError} Not enough texture memory available to create a megatexture with the given tile dimensions. * @private */ Megatexture.get3DTextureDimension = function ( tileDimensions, bytesPerSample, availableTextureMemoryBytes, tileCount, ) { const { maximum3DTextureSize } = ContextLimits; if (Cartesian3.maximumComponent(tileDimensions) > maximum3DTextureSize) { throw new RuntimeError( "The GL context does not support a 3D texture large enough to contain a tile with the given dimensions.", ); } // Find the number of tiles we can fit. const tileSizeInBytes = bytesPerSample * tileDimensions.x * tileDimensions.y * tileDimensions.z; const maxTileCount = Math.floor( availableTextureMemoryBytes / tileSizeInBytes, ); if (maxTileCount < 1) { throw new RuntimeError( "Not enough texture memory available to create a megatexture with the given tile dimensions.", ); } if (defined(tileCount)) { tileCount = Math.min(tileCount, maxTileCount); } else { tileCount = maxTileCount; } // Sort the tile dimensions from largest to smallest. const sortedDimensions = Object.entries(tileDimensions).sort( (a, b) => b[1] - a[1], ); // Compute the number of tiles that we can fit along each axis of a 3D texture, // starting from the largest texture the context can support const tilesPerDimension = sortedDimensions.map(([axis, dimension]) => Math.floor(maximum3DTextureSize / dimension), ); // Reduce the number of tiles along each dimension until the total number of // tiles is close to but not less than tileCount. // Start from the dimension along which the tiles are largest, since // along this dimension each removed slice will contain the most tiles. for (let i = 0; i < 3; i++) { const currentTileCount = getVolume(tilesPerDimension); if (currentTileCount < tileCount) { break; } const sliceDimensions = tilesPerDimension.slice(); sliceDimensions.splice(i, 1); const tilesPerSlice = sliceDimensions[0] * sliceDimensions[1]; const excessTiles = currentTileCount - tileCount; const slicesToRemove = Math.floor(excessTiles / tilesPerSlice); tilesPerDimension[i] -= slicesToRemove; } // Make sure we are less than maximumTileCount (to fit within memory) if (getVolume(tilesPerDimension) > maxTileCount) { tilesPerDimension[2] = Math.floor( maxTileCount / (tilesPerDimension[0] * tilesPerDimension[1]), ); } // Compute the final texture dimensions const textureDimension = new Cartesian3(); for (let i = 0; i < 3; i++) { const [axis, dimension] = sortedDimensions[i]; textureDimension[axis] = tilesPerDimension[i] * dimension; } return textureDimension; }; function getVolume(dimensionsArray) { return dimensionsArray.reduce((p, d) => p * d); } /** * Write an array of tile metadata to the megatexture. * @param {number} index The index of the tile's location in the megatexture. * @param {Float32Array|Uint16Array|Uint8Array} tileData The data to be written. */ Megatexture.prototype.writeDataToTexture = function (index, tileData) { const { tileCounts, voxelCountPerTile } = this; const source = { arrayBufferView: tileData, width: voxelCountPerTile.x, height: voxelCountPerTile.y, depth: voxelCountPerTile.z, }; const tilesPerZ = tileCounts.x * tileCounts.y; const iz = Math.floor(index / tilesPerZ); const remainder = index - iz * tilesPerZ; const iy = Math.floor(remainder / tileCounts.x); const ix = remainder - iy * tileCounts.x; const copyOptions = { source: source, xOffset: ix * voxelCountPerTile.x, yOffset: iy * voxelCountPerTile.y, zOffset: iz * voxelCountPerTile.z, }; this.texture.copyFrom(copyOptions); }; /** * Returns true if this object was destroyed; otherwise, false. * <br /><br /> * If this object was destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. * * @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>. * * @see Megatexture#destroy */ Megatexture.prototype.isDestroyed = function () { return false; }; /** * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic * release of WebGL resources, instead of relying on the garbage collector to destroy this object. * <br /><br /> * Once an object is destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore, * assign the return value (<code>undefined</code>) to the object as done in the example. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * @see Megatexture#isDestroyed * * @example * megatexture = megatexture && megatexture.destroy(); */ Megatexture.prototype.destroy = function () { this.texture = this.texture && this.texture.destroy(); return destroyObject(this); }; export default Megatexture;