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 defaultValue from "../Core/defaultValue.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 * @param {Cartesian3} dimensions * @param {number} channelCount * @param {MetadataComponentType} componentType * @param {number} [textureMemoryByteLength] * * @private */ function Megatexture( context, dimensions, channelCount, componentType, textureMemoryByteLength ) { // 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; } const supportsFloatingPointTexture = context.floatingPointTexture; if ( componentType === MetadataComponentType.FLOAT32 && !supportsFloatingPointTexture ) { throw new RuntimeError("Floating point texture not supported"); } // TODO support more let pixelType; if ( componentType === MetadataComponentType.FLOAT32 || componentType === MetadataComponentType.FLOAT64 ) { pixelType = PixelDatatype.FLOAT; } else if (componentType === MetadataComponentType.UINT8) { pixelType = PixelDatatype.UNSIGNED_BYTE; } let pixelFormat; if (channelCount === 1) { pixelFormat = context.webgl2 ? PixelFormat.RED : PixelFormat.LUMINANCE; } else if (channelCount === 2) { pixelFormat = context.webgl2 ? PixelFormat.RG : PixelFormat.LUMINANCE_ALPHA; } else if (channelCount === 3) { pixelFormat = PixelFormat.RGB; } else if (channelCount === 4) { pixelFormat = PixelFormat.RGBA; } const maximumTextureMemoryByteLength = 512 * 1024 * 1024; const defaultTextureMemoryByteLength = 128 * 1024 * 1024; textureMemoryByteLength = Math.min( defaultValue(textureMemoryByteLength, defaultTextureMemoryByteLength), maximumTextureMemoryByteLength ); const maximumTextureDimensionContext = ContextLimits.maximumTextureSize; const componentTypeByteLength = MetadataComponentType.getSizeInBytes( componentType ); const texelCount = Math.floor( textureMemoryByteLength / (channelCount * componentTypeByteLength) ); const textureDimension = Math.min( maximumTextureDimensionContext, CesiumMath.previousPowerOfTwo(Math.floor(Math.sqrt(texelCount))) ); 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 {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: pixelType, 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; } /** * @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; } /** * @param {Array} data * @returns {number} */ 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 * @param {Cartesian3} dimensions * @param {number} channelCount number of channels in the metadata. Must be 1 to 4. * @param {MetadataComponentType} componentType * @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.z)); 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; }; /** * @param {number} index * @param {Float32Array|Uint16Array|Uint8Array} data */ 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 voxelDimensionsPerTile = this.voxelCountPerTile; const sliceDimensionsPerRegion = this.sliceCountPerRegion; const voxelDimensionsPerRegion = this.voxelCountPerRegion; const channelCount = this.channelCount; const tileVoxelData = this.tileVoxelDataTemp; for (let z = 0; z < voxelDimensionsPerTile.z; z++) { const sliceVoxelOffsetX = (z % sliceDimensionsPerRegion.x) * voxelDimensionsPerTile.x; const sliceVoxelOffsetY = Math.floor(z / sliceDimensionsPerRegion.x) * voxelDimensionsPerTile.y; for (let y = 0; y < voxelDimensionsPerTile.y; y++) { for (let x = 0; x < voxelDimensionsPerTile.x; x++) { const readIndex = z * voxelDimensionsPerTile.y * voxelDimensionsPerTile.x + y * voxelDimensionsPerTile.x + x; const writeIndex = (sliceVoxelOffsetY + y) * voxelDimensionsPerRegion.x + (sliceVoxelOffsetX + x); for (let c = 0; c < channelCount; c++) { tileVoxelData[writeIndex * channelCount + c] = tileData[readIndex * channelCount + c]; } } } } const regionDimensionsPerMegatexture = this.regionCountPerMegatexture; const voxelWidth = voxelDimensionsPerRegion.x; const voxelHeight = voxelDimensionsPerRegion.y; const voxelOffsetX = (index % regionDimensionsPerMegatexture.x) * voxelDimensionsPerRegion.x; const voxelOffsetY = Math.floor(index / regionDimensionsPerMegatexture.x) * voxelDimensionsPerRegion.y; const source = { arrayBufferView: tileVoxelData, width: voxelWidth, height: voxelHeight, }; const copyOptions = { source: source, xOffset: voxelOffsetX, yOffset: voxelOffsetY, }; this.texture.copyFrom(copyOptions); }; /** * Returns true if this object was destroyed; otherwise, false. *

* If this object was destroyed, it should not be used; calling any function other than * isDestroyed will result in a {@link DeveloperError} exception. * * @returns {boolean} true if this object was destroyed; otherwise, false. * * @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. *

* Once an object is destroyed, it should not be used; calling any function other than * isDestroyed will result in a {@link DeveloperError} exception. Therefore, * assign the return value (undefined) 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;