UNPKG

@cesium/engine

Version:

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

551 lines (494 loc) 16.1 kB
import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import ComponentDatatype from "../Core/ComponentDatatype.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 CesiumMath from "../Core/Math.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 Texture from "../Renderer/Texture.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. * * @private */ function Megatexture( context, dimensions, channelCount, componentType, availableTextureMemoryBytes, ) { const maximumTextureMemoryByteLength = 512 * 1024 * 1024; availableTextureMemoryBytes = Math.min( availableTextureMemoryBytes ?? 128 * 1024 * 1024, maximumTextureMemoryByteLength, ); // TODO there are a lot of texture packing rules, see https://github.com/CesiumGS/cesium/issues/9572 // Unsigned short textures not allowed in webgl 1, so treat as float if (componentType === MetadataComponentType.UNSIGNED_SHORT) { componentType = MetadataComponentType.FLOAT32; } if ( componentType === MetadataComponentType.FLOAT32 && !context.floatingPointTexture ) { throw new RuntimeError("Floating point texture not supported"); } const pixelDataType = getPixelDataType(componentType); const pixelFormat = getPixelFormat(channelCount, context.webgl2); const componentTypeByteLength = MetadataComponentType.getSizeInBytes(componentType); const textureDimension = getTextureDimension( availableTextureMemoryBytes, channelCount, componentTypeByteLength, ); const sliceCountPerRegionX = Math.ceil(Math.sqrt(dimensions.x)); const sliceCountPerRegionY = Math.ceil(dimensions.z / sliceCountPerRegionX); const voxelCountPerRegionX = sliceCountPerRegionX * dimensions.x; const voxelCountPerRegionY = sliceCountPerRegionY * dimensions.y; const regionCountPerMegatextureX = Math.floor( textureDimension / voxelCountPerRegionX, ); const regionCountPerMegatextureY = Math.floor( textureDimension / voxelCountPerRegionY, ); if (regionCountPerMegatextureX === 0 || regionCountPerMegatextureY === 0) { throw new RuntimeError("Tileset is too large to fit into megatexture"); } /** * @type {number} * @readonly */ this.channelCount = channelCount; /** * @type {MetadataComponentType} * @readonly */ this.componentType = componentType; /** * @type {number} * @readonly */ this.textureMemoryByteLength = componentTypeByteLength * channelCount * textureDimension ** 2; /** * @type {Cartesian3} * @readonly */ this.voxelCountPerTile = Cartesian3.clone(dimensions, new Cartesian3()); /** * @type {number} * @readonly */ this.maximumTileCount = regionCountPerMegatextureX * regionCountPerMegatextureY; /** * @type {Cartesian2} * @readonly */ this.regionCountPerMegatexture = new Cartesian2( regionCountPerMegatextureX, regionCountPerMegatextureY, ); /** * @type {Cartesian2} * @readonly */ this.voxelCountPerRegion = new Cartesian2( voxelCountPerRegionX, voxelCountPerRegionY, ); /** * @type {Cartesian2} * @readonly */ this.sliceCountPerRegion = new Cartesian2( sliceCountPerRegionX, sliceCountPerRegionY, ); /** * @type {Cartesian2} * @readonly */ this.voxelSizeUv = new Cartesian2( 1.0 / textureDimension, 1.0 / textureDimension, ); /** * @type {Cartesian2} * @readonly */ this.sliceSizeUv = new Cartesian2( dimensions.x / textureDimension, dimensions.y / textureDimension, ); /** * @type {Cartesian2} * @readonly */ this.regionSizeUv = new Cartesian2( voxelCountPerRegionX / textureDimension, voxelCountPerRegionY / textureDimension, ); /** * @type {Texture} * @readonly */ this.texture = new Texture({ context: context, pixelFormat: pixelFormat, pixelDatatype: pixelDataType, flipY: false, width: textureDimension, height: textureDimension, sampler: new Sampler({ wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.LINEAR, magnificationFilter: TextureMagnificationFilter.LINEAR, }), }); const componentDatatype = MetadataComponentType.toComponentDatatype(componentType); /** * @type {Array} */ this.tileVoxelDataTemp = ComponentDatatype.createTypedArray( componentDatatype, voxelCountPerRegionX * voxelCountPerRegionY * channelCount, ); /** * @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; } /** * 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. * @param {boolean} webgl2 true if the context is using webgl2 * @returns {PixelFormat} The pixel format to use for a megatexture. * * @private */ function getPixelFormat(channelCount, webgl2) { if (channelCount === 1) { return webgl2 ? PixelFormat.RED : PixelFormat.LUMINANCE; } else if (channelCount === 2) { return webgl2 ? PixelFormat.RG : PixelFormat.LUMINANCE_ALPHA; } else if (channelCount === 3) { return PixelFormat.RGB; } else if (channelCount === 4) { return PixelFormat.RGBA; } } /** * Compute the largest size of a square texture that will fit in the available memory. * * @param {number} availableTextureMemoryBytes An upper limit on the texture memory size. * @param {number} channelCount The number of metadata channels per texel. * @param {number} componentByteLength The byte length of each component of the metadata. * @returns {number} The dimension of the square texture to use for the megatexture. * * @private */ function getTextureDimension( availableTextureMemoryBytes, channelCount, componentByteLength, ) { // Compute how many texels will fit in the available memory const texelCount = Math.floor( availableTextureMemoryBytes / (channelCount * componentByteLength), ); // Return the largest power of two texture size that will fit in memory return Math.min( ContextLimits.maximumTextureSize, CesiumMath.previousPowerOfTwo(Math.floor(Math.sqrt(texelCount))), ); } /** * @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. */ 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 */ 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; }; /** * @param {number} tileCount The total number of tiles in the tileset. * @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 type of one channel of the metadata. * @returns {number} */ Megatexture.getApproximateTextureMemoryByteLength = function ( tileCount, dimensions, channelCount, componentType, ) { // TODO there's a lot of code duplicate with Megatexture constructor // Unsigned short textures not allowed in webgl 1, so treat as float if (componentType === MetadataComponentType.UNSIGNED_SHORT) { componentType = MetadataComponentType.FLOAT32; } const datatypeSizeInBytes = MetadataComponentType.getSizeInBytes(componentType); const voxelCountTotal = tileCount * dimensions.x * dimensions.y * dimensions.z; const sliceCountPerRegionX = Math.ceil(Math.sqrt(dimensions.x)); const sliceCountPerRegionY = Math.ceil(dimensions.z / sliceCountPerRegionX); const voxelCountPerRegionX = sliceCountPerRegionX * dimensions.x; const voxelCountPerRegionY = sliceCountPerRegionY * dimensions.y; // Find the power of two that can fit all tile data, accounting for slices. // There's probably a non-iterative solution for this, but this is good enough for now. let textureDimension = CesiumMath.previousPowerOfTwo( Math.floor(Math.sqrt(voxelCountTotal)), ); for (;;) { const regionCountX = Math.floor(textureDimension / voxelCountPerRegionX); const regionCountY = Math.floor(textureDimension / voxelCountPerRegionY); const regionCount = regionCountX * regionCountY; if (regionCount >= tileCount) { break; } else { textureDimension *= 2; } } const textureMemoryByteLength = textureDimension * textureDimension * channelCount * datatypeSizeInBytes; return textureMemoryByteLength; }; /** * 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} data The data to be written. */ Megatexture.prototype.writeDataToTexture = function (index, data) { // Unsigned short textures not allowed in webgl 1, so treat as float const tileData = data.constructor === Uint16Array ? new Float32Array(data) : data; const { tileVoxelDataTemp, voxelCountPerTile, sliceCountPerRegion, voxelCountPerRegion, channelCount, regionCountPerMegatexture, } = this; for (let z = 0; z < voxelCountPerTile.z; z++) { const sliceVoxelOffsetX = (z % sliceCountPerRegion.x) * voxelCountPerTile.x; const sliceVoxelOffsetY = Math.floor(z / sliceCountPerRegion.x) * voxelCountPerTile.y; for (let y = 0; y < voxelCountPerTile.y; y++) { const readOffset = getReadOffset(voxelCountPerTile, y, z); const writeOffset = (sliceVoxelOffsetY + y) * voxelCountPerRegion.x + sliceVoxelOffsetX; for (let x = 0; x < voxelCountPerTile.x; x++) { const readIndex = readOffset + x; const writeIndex = writeOffset + x; for (let c = 0; c < channelCount; c++) { tileVoxelDataTemp[writeIndex * channelCount + c] = tileData[readIndex * channelCount + c]; } } } } const voxelOffsetX = (index % regionCountPerMegatexture.x) * voxelCountPerRegion.x; const voxelOffsetY = Math.floor(index / regionCountPerMegatexture.x) * voxelCountPerRegion.y; const source = { arrayBufferView: tileVoxelDataTemp, width: voxelCountPerRegion.x, height: voxelCountPerRegion.y, }; const copyOptions = { source: source, xOffset: voxelOffsetX, yOffset: voxelOffsetY, }; this.texture.copyFrom(copyOptions); }; /** * Get the offset into the data array for a given row of contiguous voxel data. * * @param {Cartesian3} dimensions The number of voxels in each dimension of the tile. * @param {number} y The y index of the voxel row * @param {number} z The z index of the voxel row * @returns {number} The offset into the data array * @private */ function getReadOffset(dimensions, y, z) { const voxelsPerInputSlice = dimensions.y * dimensions.x; const sliceIndex = z; const rowIndex = y; return sliceIndex * voxelsPerInputSlice + rowIndex * dimensions.x; } /** * 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;