cesium
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
585 lines (518 loc) • 18.8 kB
JavaScript
import AttributeCompression from "../../Core/AttributeCompression.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import Color from "../../Core/Color.js";
import Check from "../../Core/Check.js";
import ComponentDatatype from "../../Core/ComponentDatatype.js";
import defaultValue from "../../Core/defaultValue.js";
import defined from "../../Core/defined.js";
import Matrix4 from "../../Core/Matrix4.js";
import PrimitiveType from "../../Core/PrimitiveType.js";
import MersenneTwister from "../../ThirdParty/mersenne-twister.js";
import Buffer from "../../Renderer/Buffer.js";
import BufferUsage from "../../Renderer/BufferUsage.js";
import AlphaMode from "../AlphaMode.js";
import AttributeType from "../AttributeType.js";
import Axis from "../Axis.js";
import parseBatchTable from "../parseBatchTable.js";
import DracoLoader from "../DracoLoader.js";
import StructuralMetadata from "../StructuralMetadata.js";
import ResourceLoader from "../ResourceLoader.js";
import MetadataClass from "../MetadataClass.js";
import ModelComponents from "../ModelComponents.js";
import PntsParser from "../PntsParser.js";
import PropertyTable from "../PropertyTable.js";
import ResourceLoaderState from "../ResourceLoaderState.js";
import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
const Components = ModelComponents.Components;
const Scene = ModelComponents.Scene;
const Node = ModelComponents.Node;
const Primitive = ModelComponents.Primitive;
const Attribute = ModelComponents.Attribute;
const Quantization = ModelComponents.Quantization;
const FeatureIdAttribute = ModelComponents.FeatureIdAttribute;
const Material = ModelComponents.Material;
const MetallicRoughness = ModelComponents.MetallicRoughness;
/**
* Loads a .pnts point cloud and transcodes it into a {@link ModelComponents}
*
* @alias PntsLoader
* @constructor
* @augments ResourceLoader
* @private
*
* @param {Object} options An object containing the following properties
* @param {ArrayBuffer} options.arrayBuffer The array buffer of the pnts contents
* @param {Number} [options.byteOffset] The byte offset to the beginning of the pnts contents in the array buffer
*/
export default function PntsLoader(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const arrayBuffer = options.arrayBuffer;
const byteOffset = defaultValue(options.byteOffset, 0);
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.arrayBuffer", arrayBuffer);
//>>includeEnd('debug');
this._arrayBuffer = arrayBuffer;
this._byteOffset = byteOffset;
this._parsedContent = undefined;
this._decodePromise = undefined;
this._decodedAttributes = undefined;
this._promise = undefined;
this._process = function (frameState) {};
this._state = ResourceLoaderState.UNLOADED;
this._buffers = [];
// The batch table object contains a json and a binary component access using keys of the same name.
this._components = undefined;
this._transform = Matrix4.IDENTITY;
}
if (defined(Object.create)) {
PntsLoader.prototype = Object.create(ResourceLoader.prototype);
PntsLoader.prototype.constructor = PntsLoader;
}
Object.defineProperties(PntsLoader.prototype, {
/**
* A promise that resolves to the resource when the resource is ready, or undefined if the resource hasn't started loading.
*
* @memberof PntsLoader.prototype
*
* @type {Promise.<PntsLoader>|undefined}
* @readonly
* @private
*/
promise: {
get: function () {
return this._promise;
},
},
/**
* The cache key of the resource
*
* @memberof PntsLoader.prototype
*
* @type {String}
* @readonly
* @private
*/
cacheKey: {
get: function () {
return undefined;
},
},
/**
* The loaded components.
*
* @memberof PntsLoader.prototype
*
* @type {ModelComponents.Components}
* @readonly
* @private
*/
components: {
get: function () {
return this._components;
},
},
/**
* A world-space transform to apply to the primitives.
* See {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/PointCloud#global-semantics}
*
* @memberof PntsLoader.prototype
*
* @type {Matrix4}
* @readonly
* @private
*/
transform: {
get: function () {
return this._transform;
},
},
});
/**
* Loads the resource.
* @returns {Promise.<PntsLoader>} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
PntsLoader.prototype.load = function () {
this._parsedContent = PntsParser.parse(this._arrayBuffer, this._byteOffset);
this._state = ResourceLoaderState.PROCESSING;
const loader = this;
this._promise = new Promise(function (resolve, reject) {
loader._process = function (frameState) {
if (loader._state === ResourceLoaderState.PROCESSING) {
if (!defined(loader._decodePromise)) {
decodeDraco(loader, frameState.context).then(resolve).catch(reject);
}
}
};
});
};
PntsLoader.prototype.process = function (frameState) {
this._process(frameState);
};
function decodeDraco(loader, context) {
const parsedContent = loader._parsedContent;
const draco = parsedContent.draco;
let decodePromise;
if (!defined(draco)) {
// The draco extension wasn't present,
decodePromise = Promise.resolve();
} else {
decodePromise = DracoLoader.decodePointCloud(draco, context);
}
if (!defined(decodePromise)) {
// Could not schedule Draco decoding this frame.
return;
}
loader._decodePromise = decodePromise;
return decodePromise
.then(function (decodeDracoResult) {
if (loader.isDestroyed()) {
return;
}
if (defined(decodeDracoResult)) {
processDracoAttributes(loader, draco, decodeDracoResult);
}
makeComponents(loader, context);
loader._state = ResourceLoaderState.READY;
return loader;
})
.catch(function (error) {
loader.unload();
loader._state = ResourceLoaderState.FAILED;
const errorMessage = "Failed to load Draco";
return Promise.reject(loader.getError(errorMessage, error));
});
}
function processDracoAttributes(loader, draco, result) {
loader._state = ResourceLoaderState.READY;
const parsedContent = loader._parsedContent;
let attribute;
if (defined(result.POSITION)) {
attribute = {
name: "POSITION",
semantic: VertexAttributeSemantic.POSITION,
typedArray: result.POSITION.array,
componentDatatype: ComponentDatatype.FLOAT,
type: AttributeType.VEC3,
isQuantized: false,
};
if (defined(result.POSITION.data.quantization)) {
// Draco quantization range == quantized volume scale - size in meters of the quantized volume
// Internal quantized range is the range of values of the quantized data, e.g. 255 for 8-bit, 1023 for 10-bit, etc
const quantization = result.POSITION.data.quantization;
const range = quantization.range;
const quantizedVolumeScale = Cartesian3.fromElements(range, range, range);
const quantizedVolumeOffset = Cartesian3.unpack(quantization.minValues);
const quantizedRange = (1 << quantization.quantizationBits) - 1.0;
attribute.isQuantized = true;
attribute.quantizedRange = quantizedRange;
attribute.quantizedVolumeOffset = quantizedVolumeOffset;
attribute.quantizedVolumeScale = quantizedVolumeScale;
attribute.quantizedComponentDatatype = ComponentDatatype.UNSIGNED_SHORT;
attribute.quantizedType = AttributeType.VEC3;
}
parsedContent.positions = attribute;
}
if (defined(result.NORMAL)) {
attribute = {
name: "NORMAL",
semantic: VertexAttributeSemantic.NORMAL,
typedArray: result.NORMAL.array,
componentDatatype: ComponentDatatype.FLOAT,
type: AttributeType.VEC3,
isQuantized: false,
octEncoded: false,
octEncodedZXY: false,
};
if (defined(result.NORMAL.data.quantization)) {
const octEncodedRange =
(1 << result.NORMAL.data.quantization.quantizationBits) - 1.0;
attribute.quantizedRange = octEncodedRange;
attribute.octEncoded = true;
attribute.octEncodedZXY = true;
attribute.quantizedComponentDatatype = ComponentDatatype.UNSIGNED_BYTE;
attribute.quantizedType = AttributeType.VEC2;
}
parsedContent.normals = attribute;
}
if (defined(result.RGBA)) {
parsedContent.colors = {
name: "COLOR",
semantic: VertexAttributeSemantic.COLOR,
setIndex: 0,
typedArray: result.RGBA.array,
componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
type: AttributeType.VEC4,
normalized: true,
isTranslucent: true,
};
} else if (defined(result.RGB)) {
parsedContent.colors = {
name: "COLOR",
semantic: VertexAttributeSemantic.COLOR,
setIndex: 0,
typedArray: result.RGB.array,
componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
type: AttributeType.VEC3,
normalized: true,
isTranslucent: false,
};
}
// Transcode Batch ID (3D Tiles 1.0) -> Feature ID (3D Tiles Next)
if (defined(result.BATCH_ID)) {
const batchIds = result.BATCH_ID.array;
parsedContent.batchIds = {
name: "_FEATURE_ID",
semantic: VertexAttributeSemantic.FEATURE_ID,
setIndex: 0,
typedArray: batchIds,
componentDatatype: ComponentDatatype.fromTypedArray(batchIds),
type: AttributeType.SCALAR,
};
}
let styleableProperties = parsedContent.styleableProperties;
const batchTableProperties = draco.batchTableProperties;
for (const name in batchTableProperties) {
if (batchTableProperties.hasOwnProperty(name)) {
const property = result[name];
if (!defined(styleableProperties)) {
styleableProperties = {};
}
styleableProperties[name] = {
typedArray: property.array,
componentCount: property.data.componentsPerAttribute,
};
}
}
parsedContent.styleableProperties = styleableProperties;
}
function makeAttribute(loader, attributeInfo, context) {
let typedArray = attributeInfo.typedArray;
let quantization;
if (attributeInfo.octEncoded) {
quantization = new Quantization();
quantization.octEncoded = attributeInfo.octEncoded;
quantization.octEncodedZXY = attributeInfo.octEncodedZXY;
quantization.normalizationRange = attributeInfo.quantizedRange;
quantization.type = attributeInfo.quantizedType;
quantization.componentDatatype = attributeInfo.quantizedComponentDatatype;
}
if (attributeInfo.isQuantized) {
quantization = new Quantization();
const normalizationRange = attributeInfo.quantizedRange;
quantization.normalizationRange = normalizationRange;
// volume offset sometimes requires 64-bit precision so this is handled
// in the components.transform matrix.
quantization.quantizedVolumeOffset = Cartesian3.ZERO;
const quantizedVolumeDimensions = attributeInfo.quantizedVolumeScale;
quantization.quantizedVolumeDimensions = quantizedVolumeDimensions;
quantization.quantizedVolumeStepSize = Cartesian3.divideByScalar(
quantizedVolumeDimensions,
normalizationRange,
new Cartesian3()
);
quantization.componentDatatype = attributeInfo.quantizedComponentDatatype;
quantization.type = attributeInfo.quantizedType;
}
const attribute = new Attribute();
attribute.name = attributeInfo.name;
attribute.semantic = attributeInfo.semantic;
attribute.setIndex = attributeInfo.setIndex;
attribute.componentDatatype = attributeInfo.componentDatatype;
attribute.type = attributeInfo.type;
attribute.normalized = defaultValue(attributeInfo.normalized, false);
attribute.min = attributeInfo.min;
attribute.max = attributeInfo.max;
attribute.quantization = quantization;
if (attributeInfo.isRGB565) {
typedArray = AttributeCompression.decodeRGB565(typedArray);
}
if (defined(attributeInfo.constantColor)) {
const packedColor = new Array(4);
attribute.constant = Color.pack(attributeInfo.constantColor, packedColor);
} else {
const buffer = Buffer.createVertexBuffer({
typedArray: typedArray,
context: context,
usage: BufferUsage.STATIC_DRAW,
});
buffer.vertexArrayDestroyable = false;
loader._buffers.push(buffer);
attribute.buffer = buffer;
}
return attribute;
}
let randomNumberGenerator;
let randomValues;
function getRandomValues(samplesLength) {
// Use same random values across all runs
if (!defined(randomValues)) {
// Use MersenneTwister directly to avoid interfering with CesiumMath.nextRandomNumber()
// See https://github.com/CesiumGS/cesium/issues/9730
randomNumberGenerator = new MersenneTwister(0);
randomValues = new Array(samplesLength);
for (let i = 0; i < samplesLength; ++i) {
randomValues[i] = randomNumberGenerator.random();
}
}
return randomValues;
}
const scratchMin = new Cartesian3();
const scratchMax = new Cartesian3();
const scratchPosition = new Cartesian3();
function computeApproximateExtrema(positions) {
const positionsArray = positions.typedArray;
const maximumSamplesLength = 20;
const pointsLength = positionsArray.length / 3;
const samplesLength = Math.min(pointsLength, maximumSamplesLength);
const randomValues = getRandomValues(maximumSamplesLength);
const maxValue = Number.MAX_VALUE;
const minValue = -Number.MAX_VALUE;
let min = Cartesian3.fromElements(maxValue, maxValue, maxValue, scratchMin);
let max = Cartesian3.fromElements(minValue, minValue, minValue, scratchMax);
let i;
let index;
let position;
if (positions.isQuantized) {
// The quantized volume offset is not used here since it will become part of
// the model matrix.
min = Cartesian3.ZERO;
max = positions.quantizedVolumeScale;
} else {
for (i = 0; i < samplesLength; ++i) {
index = Math.floor(randomValues[i] * pointsLength);
position = Cartesian3.unpack(positionsArray, index * 3, scratchPosition);
Cartesian3.minimumByComponent(min, position, min);
Cartesian3.maximumByComponent(max, position, max);
}
}
positions.min = Cartesian3.clone(min);
positions.max = Cartesian3.clone(max);
}
// By default, point clouds are rendered as dark gray.
const defaultColorAttribute = {
name: VertexAttributeSemantic.COLOR,
semantic: VertexAttributeSemantic.COLOR,
setIndex: 0,
constantColor: Color.DARKGRAY,
componentDatatype: ComponentDatatype.FLOAT,
type: AttributeType.VEC4,
isQuantized: false,
isTranslucent: false,
};
function makeAttributes(loader, parsedContent, context) {
const attributes = [];
let attribute;
const positions = parsedContent.positions;
if (defined(positions)) {
computeApproximateExtrema(positions);
attribute = makeAttribute(loader, positions, context);
attribute.count = parsedContent.pointsLength;
attributes.push(attribute);
}
if (defined(parsedContent.normals)) {
attribute = makeAttribute(loader, parsedContent.normals, context);
attributes.push(attribute);
}
if (defined(parsedContent.colors)) {
attribute = makeAttribute(loader, parsedContent.colors, context);
attributes.push(attribute);
} else {
attribute = makeAttribute(loader, defaultColorAttribute, context);
attributes.push(attribute);
}
if (defined(parsedContent.batchIds)) {
attribute = makeAttribute(loader, parsedContent.batchIds, context);
attributes.push(attribute);
}
return attributes;
}
function makeStructuralMetadata(parsedContent) {
const batchLength = parsedContent.batchLength;
const pointsLength = parsedContent.pointsLength;
const batchTableBinary = parsedContent.batchTableBinary;
if (defined(batchTableBinary)) {
const count = defaultValue(batchLength, pointsLength);
return parseBatchTable({
count: count,
batchTable: parsedContent.batchTableJson,
binaryBody: batchTableBinary,
});
}
// If batch table is not defined, create a property table without any properties.
const emptyPropertyTable = new PropertyTable({
name: MetadataClass.BATCH_TABLE_CLASS_NAME,
count: pointsLength,
});
return new StructuralMetadata({
schema: {},
propertyTables: [emptyPropertyTable],
});
}
function makeComponents(loader, context) {
const parsedContent = loader._parsedContent;
const metallicRoughness = new MetallicRoughness();
metallicRoughness.metallicFactor = 0;
metallicRoughness.roughnessFactor = 0.9;
const material = new Material();
material.metallicRoughness = metallicRoughness;
const colors = parsedContent.colors;
if (defined(colors) && colors.isTranslucent) {
material.alphaMode = AlphaMode.BLEND;
}
// Render point clouds as unlit, unless normals are present, in which case
// render as a PBR material.
const isUnlit = !defined(parsedContent.normals);
material.unlit = isUnlit;
const primitive = new Primitive();
primitive.attributes = makeAttributes(loader, parsedContent, context);
primitive.primitiveType = PrimitiveType.POINTS;
primitive.material = material;
if (defined(parsedContent.batchIds)) {
const featureIdAttribute = new FeatureIdAttribute();
featureIdAttribute.propertyTableId = 0;
featureIdAttribute.setIndex = 0;
featureIdAttribute.positionalLabel = "featureId_0";
primitive.featureIds.push(featureIdAttribute);
}
const node = new Node();
node.index = 0;
node.primitives = [primitive];
const scene = new Scene();
scene.nodes = [node];
scene.upAxis = Axis.Z;
scene.forwardAxis = Axis.X;
const components = new Components();
components.scene = scene;
components.nodes = [node];
components.structuralMetadata = makeStructuralMetadata(parsedContent);
if (defined(parsedContent.rtcCenter)) {
components.transform = Matrix4.multiplyByTranslation(
components.transform,
parsedContent.rtcCenter,
components.transform
);
}
const positions = parsedContent.positions;
if (defined(positions) && positions.isQuantized) {
// The volume offset is sometimes in ECEF, so this is applied here rather
// than the dequantization shader to avoid jitter
components.transform = Matrix4.multiplyByTranslation(
components.transform,
positions.quantizedVolumeOffset,
components.transform
);
}
loader._components = components;
// Free the parsed content so we don't hold onto the large typed arrays.
loader._parsedContent = undefined;
}
PntsLoader.prototype.unload = function () {
const buffers = this._buffers;
for (let i = 0; i < buffers.length; i++) {
buffers[i].destroy();
}
buffers.length = 0;
this._components = undefined;
this._parsedContent = undefined;
};