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;