diff --git a/Source/Core/AttributeCompression.js b/Source/Core/AttributeCompression.js index 1aebb6c44553..d511d8335b37 100644 --- a/Source/Core/AttributeCompression.js +++ b/Source/Core/AttributeCompression.js @@ -476,4 +476,50 @@ AttributeCompression.dequantize = function ( return dequantizedTypedArray; }; +/** + * Decode RGB565-encoded colors into a floating point typed array containing + * normalized RGB values. + * + * @param {Uint16Array} typedArray Array of RGB565 values + * @param {Float32Array} [result] Array to store the normalized VEC3 result + */ +AttributeCompression.decodeRGB565 = function (typedArray, result) { + //>>includeStart('debug', pragmas.debug); + Check.defined("typedArray", typedArray); + + var expectedLength = typedArray.length * 3; + if (defined(result)) { + Check.typeOf.number.equals( + "result.length", + "typedArray.length * 3", + result.length, + expectedLength + ); + } + //>>includeEnd('debug'); + + var count = typedArray.length; + if (!defined(result)) { + result = new Float32Array(count * 3); + } + + var mask5 = (1 << 5) - 1; + var mask6 = (1 << 6) - 1; + var normalize5 = 1.0 / 31.0; + var normalize6 = 1.0 / 63.0; + for (var i = 0; i < count; i++) { + var value = typedArray[i]; + var red = value >> 11; + var green = (value >> 5) & mask6; + var blue = value & mask5; + + var offset = 3 * i; + result[offset] = red * normalize5; + result[offset + 1] = green * normalize6; + result[offset + 2] = blue * normalize5; + } + + return result; +}; + export default AttributeCompression; diff --git a/Source/Scene/B3dmParser.js b/Source/Scene/B3dmParser.js index 34c664ceb9e2..ee86458f834e 100644 --- a/Source/Scene/B3dmParser.js +++ b/Source/Scene/B3dmParser.js @@ -20,9 +20,9 @@ var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; * * @private * - * @param {ArrayBuffer} arrayBuffer The array buffer containing the B3DM. - * @param {Number} [byteOffset=0] The byte offset of the beginning of the B3DM in the array buffer. - * @returns {Object} Returns an object with the batch length, feature table (binary and json), batch table (binary and json) and glTF parts of the B3DM. + * @param {ArrayBuffer} arrayBuffer The array buffer containing the b3dm. + * @param {Number} [byteOffset=0] The byte offset of the beginning of the b3dm in the array buffer. + * @returns {Object} Returns an object with the batch length, feature table (binary and json), batch table (binary and json) and glTF parts of the b3dm. */ B3dmParser.parse = function (arrayBuffer, byteOffset) { var byteStart = defaultValue(byteOffset, 0); diff --git a/Source/Scene/Cesium3DTileContentFactory.js b/Source/Scene/Cesium3DTileContentFactory.js index 0a2011dfc1f4..b08ab706fff8 100644 --- a/Source/Scene/Cesium3DTileContentFactory.js +++ b/Source/Scene/Cesium3DTileContentFactory.js @@ -35,6 +35,15 @@ var Cesium3DTileContentFactory = { ); }, pnts: function (tileset, tile, resource, arrayBuffer, byteOffset) { + if (tileset.enableModelExperimental) { + return ModelExperimental3DTileContent.fromPnts( + tileset, + tile, + resource, + arrayBuffer, + byteOffset + ); + } return new PointCloud3DTileContent( tileset, tile, diff --git a/Source/Scene/ModelComponents.js b/Source/Scene/ModelComponents.js index f9c6c43de3ff..0118bd563b73 100644 --- a/Source/Scene/ModelComponents.js +++ b/Source/Scene/ModelComponents.js @@ -69,7 +69,7 @@ function Quantization() { /** * The step size of the quantization volume, equal to - * quantizedVolumeDimensions / quantizedVolumeOffset (component-wise). + * quantizedVolumeDimensions / normalizationRange (component-wise). * Not applicable for oct encoded attributes. * The type should match the attribute type - e.g. if the attribute type * is AttributeType.VEC4 the dimensions should be a Cartesian4. diff --git a/Source/Scene/ModelExperimental/B3dmLoader.js b/Source/Scene/ModelExperimental/B3dmLoader.js index 48c55b9c559c..a6991e920de1 100644 --- a/Source/Scene/ModelExperimental/B3dmLoader.js +++ b/Source/Scene/ModelExperimental/B3dmLoader.js @@ -40,9 +40,9 @@ var FeatureIdAttribute = ModelComponents.FeatureIdAttribute; * @private * * @param {Object} options Object with the following properties: - * @param {Resource} options.b3dmResource The {@link Resource} containing the B3DM. - * @param {ArrayBuffer} options.arrayBuffer The array buffer of the B3DM contents. - * @param {Number} [options.byteOffset] The byte offset to the beginning of the B3DM contents in the array buffer. + * @param {Resource} options.b3dmResource The {@link Resource} containing the b3dm. + * @param {ArrayBuffer} options.arrayBuffer The array buffer of the b3dm contents. + * @param {Number} [options.byteOffset] The byte offset to the beginning of the b3dm contents in the array buffer. * @param {Resource} [options.baseResource] The {@link Resource} that paths in the glTF JSON are relative to. * @param {Boolean} [options.releaseGltfJson=false] When true, the glTF JSON is released once the glTF is loaded. This is is especially useful for cases like 3D Tiles, where each .gltf model is unique and caching the glTF JSON is not effective. * @param {Boolean} [options.asynchronous=true] Determines if WebGL resource creation will be spread out over several frames or block until all WebGL resources are created. @@ -160,7 +160,6 @@ Object.defineProperties(B3dmLoader.prototype, { * @memberof B3dmLoader.prototype * * @type {ModelComponents.Components} - * @default {@link Matrix4.IDENTITY} * @readonly * @private */ @@ -247,7 +246,7 @@ B3dmLoader.prototype.load = function () { function handleError(b3dmLoader, error) { b3dmLoader.unload(); b3dmLoader._state = B3dmLoaderState.FAILED; - var errorMessage = "Failed to load B3DM"; + var errorMessage = "Failed to load b3dm"; error = b3dmLoader.getError(errorMessage, error); b3dmLoader._promise.reject(error); } diff --git a/Source/Scene/ModelExperimental/CustomShaderGuide/README.md b/Source/Scene/ModelExperimental/CustomShaderGuide/README.md index c8cf398426b9..9d33800174b4 100644 --- a/Source/Scene/ModelExperimental/CustomShaderGuide/README.md +++ b/Source/Scene/ModelExperimental/CustomShaderGuide/README.md @@ -84,7 +84,7 @@ var model = Cesium.ModelExperimental.fromGltf({, **Note**: As of this writing, only tilesets that use the `3DTILES_content_gltf` extension will support `CustomShaders`. Future releases will add support for -other formats such as B3DM. +other formats such as b3dm. ## Uniforms diff --git a/Source/Scene/ModelExperimental/GeometryPipelineStage.js b/Source/Scene/ModelExperimental/GeometryPipelineStage.js index 69ce20dc64d3..b0b09500b64b 100644 --- a/Source/Scene/ModelExperimental/GeometryPipelineStage.js +++ b/Source/Scene/ModelExperimental/GeometryPipelineStage.js @@ -7,6 +7,7 @@ import GeometryStageVS from "../../Shaders/ModelExperimental/GeometryStageVS.js" import FeatureIdPipelineStage from "./FeatureIdPipelineStage.js"; import ShaderDestination from "../../Renderer/ShaderDestination.js"; import ModelExperimentalUtility from "./ModelExperimentalUtility.js"; +import ModelExperimentalType from "./ModelExperimentalType.js"; /** * The geometry pipeline stage processes the vertex attributes of a primitive. @@ -145,6 +146,16 @@ function processAttribute(renderResources, attribute, attributeIndex) { addSemanticDefine(shaderBuilder, attribute); } + // .pnts point clouds store sRGB color rather than linear color + var modelType = renderResources.model.type; + if (modelType === ModelExperimentalType.TILE_PNTS) { + shaderBuilder.addDefine( + "HAS_SRGB_COLOR", + undefined, + ShaderDestination.FRAGMENT + ); + } + // Some GLSL code must be dynamically generated updateAttributesStruct(shaderBuilder, attributeInfo); updateInitialzeAttributesFunction(shaderBuilder, attributeInfo); diff --git a/Source/Scene/ModelExperimental/ModelExperimental.js b/Source/Scene/ModelExperimental/ModelExperimental.js index 4edfcdf6a2cf..216f31ebf93a 100644 --- a/Source/Scene/ModelExperimental/ModelExperimental.js +++ b/Source/Scene/ModelExperimental/ModelExperimental.js @@ -14,6 +14,7 @@ import destroyObject from "../../Core/destroyObject.js"; import Matrix4 from "../../Core/Matrix4.js"; import ModelFeatureTable from "./ModelFeatureTable.js"; import B3dmLoader from "./B3dmLoader.js"; +import PntsLoader from "./PntsLoader.js"; import Color from "../../Core/Color.js"; /** @@ -216,7 +217,12 @@ function initialize(model) { ModelExperimentalUtility.getFailedLoadFunction(model, "model", resource) ); - loader.texturesLoadedPromise + // Transcoded .pnts models do not have textures + var texturesLoadedPromise = defaultValue( + loader.texturesLoadedPromise, + when.resolve() + ); + texturesLoadedPromise .then(function () { model._texturesLoaded = true; }) @@ -833,9 +839,46 @@ ModelExperimental.fromB3dm = function (options) { customShader: options.customShader, content: options.content, show: options.show, + color: options.color, + colorBlendAmount: options.colorBlendAmount, + colorBlendMode: options.colorBlendMode, + featureIdAttributeIndex: options.featureIdAttributeIndex, + featureIdTextureIndex: options.featureIdTextureIndex, + }; + + var model = new ModelExperimental(modelOptions); + return model; +}; + +/** + * @private + */ +ModelExperimental.fromPnts = function (options) { + var loaderOptions = { + arrayBuffer: options.arrayBuffer, + byteOffset: options.byteOffset, + }; + var loader = new PntsLoader(loaderOptions); + + var modelOptions = { + loader: loader, + resource: options.resource, + type: ModelExperimentalType.TILE_PNTS, + modelMatrix: options.modelMatrix, + debugShowBoundingVolume: options.debugShowBoundingVolume, + cull: options.cull, + opaquePass: options.opaquePass, + allowPicking: options.allowPicking, + customShader: options.customShader, + content: options.content, + show: options.show, + color: options.color, + colorBlendAmount: options.colorBlendAmount, + colorBlendMode: options.colorBlendMode, featureIdAttributeIndex: options.featureIdAttributeIndex, featureIdTextureIndex: options.featureIdTextureIndex, }; + var model = new ModelExperimental(modelOptions); return model; }; diff --git a/Source/Scene/ModelExperimental/ModelExperimental3DTileContent.js b/Source/Scene/ModelExperimental/ModelExperimental3DTileContent.js index 53d8b1091041..d5f7f23d67a4 100644 --- a/Source/Scene/ModelExperimental/ModelExperimental3DTileContent.js +++ b/Source/Scene/ModelExperimental/ModelExperimental3DTileContent.js @@ -242,3 +242,32 @@ ModelExperimental3DTileContent.fromB3dm = function ( content._model = ModelExperimental.fromB3dm(modelOptions); return content; }; + +ModelExperimental3DTileContent.fromPnts = function ( + tileset, + tile, + resource, + arrayBuffer, + byteOffset +) { + var content = new ModelExperimental3DTileContent(tileset, tile, resource); + + var modelOptions = { + arrayBuffer: arrayBuffer, + byteOffset: byteOffset, + resource: resource, + cull: false, // The model is already culled by 3D Tiles + releaseGltfJson: true, // Models are unique and will not benefit from caching so save memory + opaquePass: Pass.CESIUM_3D_TILE, // Draw opaque portions of the model during the 3D Tiles pass + modelMatrix: tile.computedTransform, + upAxis: tileset._gltfUpAxis, + forwardAxis: Axis.X, + incrementallyLoadTextures: false, + customShader: tileset.customShader, + content: content, + colorBlendMode: tileset.colorBlendMode, + colorBlendAmount: tileset.colorBlendAmount, + }; + content._model = ModelExperimental.fromPnts(modelOptions); + return content; +}; diff --git a/Source/Scene/ModelExperimental/PntsLoader.js b/Source/Scene/ModelExperimental/PntsLoader.js new file mode 100644 index 000000000000..b2ade810b718 --- /dev/null +++ b/Source/Scene/ModelExperimental/PntsLoader.js @@ -0,0 +1,573 @@ +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 when from "../../ThirdParty/when.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 FeatureMetadata from "../FeatureMetadata.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"; + +var Components = ModelComponents.Components; +var Scene = ModelComponents.Scene; +var Node = ModelComponents.Node; +var Primitive = ModelComponents.Primitive; +var Attribute = ModelComponents.Attribute; +var Quantization = ModelComponents.Quantization; +var FeatureIdAttribute = ModelComponents.FeatureIdAttribute; +var Material = ModelComponents.Material; +var 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); + + var arrayBuffer = options.arrayBuffer; + var 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 = when.defer(); + 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. + * + * @memberof PntsLoader.prototype + * + * @type {Promise.} + * @readonly + * @private + */ + promise: { + get: function () { + return this._promise.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. + * @private + */ +PntsLoader.prototype.load = function () { + this._parsedContent = PntsParser.parse(this._arrayBuffer, this._byteOffset); + this._state = ResourceLoaderState.PROCESSING; +}; + +PntsLoader.prototype.process = function (frameState) { + if (this._state === ResourceLoaderState.PROCESSING) { + if (!defined(this._decodePromise)) { + decodeDraco(this, frameState.context); + } + } +}; + +function decodeDraco(loader, context) { + var parsedContent = loader._parsedContent; + var draco = parsedContent.draco; + + var decodePromise; + if (!defined(draco)) { + // The draco extension wasn't present, + decodePromise = when.resolve(); + } else { + decodePromise = DracoLoader.decodePointCloud(draco, context); + } + + if (!defined(decodePromise)) { + // Could not schedule Draco decoding this frame. + return; + } + + loader._decodePromise = decodePromise; + decodePromise + .then(function (decodeDracoResult) { + if (loader.isDestroyed()) { + return; + } + + if (defined(decodeDracoResult)) { + processDracoAttributes(loader, draco, decodeDracoResult); + } + makeComponents(loader, context); + loader._state = ResourceLoaderState.READY; + loader._promise.resolve(loader); + }) + .otherwise(function (error) { + loader.unload(); + loader._state = ResourceLoaderState.FAILED; + var errorMessage = "Failed to load Draco"; + loader._promise.reject(loader.getError(errorMessage, error)); + }); +} + +function processDracoAttributes(loader, draco, result) { + loader._state = ResourceLoaderState.READY; + var parsedContent = loader._parsedContent; + + var 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 + var quantization = result.POSITION.data.quantization; + var range = quantization.range; + var quantizedVolumeScale = Cartesian3.fromElements(range, range, range); + var quantizedVolumeOffset = Cartesian3.unpack(quantization.minValues); + var 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)) { + var 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)) { + var 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, + }; + } + + var styleableProperties = parsedContent.styleableProperties; + var batchTableProperties = draco.batchTableProperties; + for (var name in batchTableProperties) { + if (batchTableProperties.hasOwnProperty(name)) { + var property = result[name]; + if (!defined(styleableProperties)) { + styleableProperties = {}; + } + styleableProperties[name] = { + typedArray: property.array, + componentCount: property.data.componentsPerAttribute, + }; + } + } + parsedContent.styleableProperties = styleableProperties; +} + +function makeAttribute(loader, attributeInfo, context) { + var typedArray = attributeInfo.typedArray; + var 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(); + var 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; + var quantizedVolumeDimensions = attributeInfo.quantizedVolumeScale; + quantization.quantizedVolumeDimensions = quantizedVolumeDimensions; + quantization.quantizedVolumeStepSize = Cartesian3.divideByScalar( + quantizedVolumeDimensions, + normalizationRange, + new Cartesian3() + ); + quantization.componentDatatype = attributeInfo.quantizedComponentDatatype; + quantization.type = attributeInfo.quantizedType; + } + + var 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)) { + var packedColor = new Array(4); + attribute.constant = Color.pack(attributeInfo.constantColor, packedColor); + } else { + var buffer = Buffer.createVertexBuffer({ + typedArray: typedArray, + context: context, + usage: BufferUsage.STATIC_DRAW, + }); + buffer.vertexArrayDestroyable = false; + loader._buffers.push(buffer); + attribute.buffer = buffer; + } + + return attribute; +} + +var randomNumberGenerator; +var 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 (var i = 0; i < samplesLength; ++i) { + randomValues[i] = randomNumberGenerator.random(); + } + } + return randomValues; +} + +var scratchMin = new Cartesian3(); +var scratchMax = new Cartesian3(); +var scratchPosition = new Cartesian3(); +function computeApproximateExtrema(positions) { + var positionsArray = positions.typedArray; + var maximumSamplesLength = 20; + var pointsLength = positionsArray.length / 3; + var samplesLength = Math.min(pointsLength, maximumSamplesLength); + var randomValues = getRandomValues(maximumSamplesLength); + var maxValue = Number.MAX_VALUE; + var minValue = -Number.MAX_VALUE; + var min = Cartesian3.fromElements(maxValue, maxValue, maxValue, scratchMin); + var max = Cartesian3.fromElements(minValue, minValue, minValue, scratchMax); + var i; + var index; + var 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. +var 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) { + var attributes = []; + var attribute; + var positions = parsedContent.positions; + if (defined(positions)) { + computeApproximateExtrema(positions); + attribute = makeAttribute(loader, positions, context); + 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 makeFeatureMetadata(parsedContent) { + var batchLength = parsedContent.batchLength; + var pointsLength = parsedContent.pointsLength; + var batchTableBinary = parsedContent.batchTableBinary; + + if (defined(batchTableBinary)) { + var 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. + var emptyPropertyTable = new PropertyTable({ + name: MetadataClass.BATCH_TABLE_CLASS_NAME, + count: pointsLength, + }); + return new FeatureMetadata({ + schema: {}, + propertyTables: [emptyPropertyTable], + }); +} + +function makeComponents(loader, context) { + var parsedContent = loader._parsedContent; + + var metallicRoughness = new MetallicRoughness(); + metallicRoughness.metallicFactor = 0; + metallicRoughness.roughnessFactor = 0.9; + + var material = new Material(); + material.metallicRoughness = metallicRoughness; + + var 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. + var isUnlit = !defined(parsedContent.normals); + material.unlit = isUnlit; + + var primitive = new Primitive(); + primitive.attributes = makeAttributes(loader, parsedContent, context); + primitive.primitiveType = PrimitiveType.POINTS; + primitive.material = material; + + if (defined(parsedContent.batchIds)) { + var featureIdAttribute = new FeatureIdAttribute(); + featureIdAttribute.propertyTableId = 0; + featureIdAttribute.setIndex = 0; + primitive.featureIdAttributes = [featureIdAttribute]; + } + + var node = new Node(); + node.primitives = [primitive]; + + var scene = new Scene(); + scene.nodes = [node]; + scene.upAxis = Axis.Z; + scene.forwardAxis = Axis.X; + + var components = new Components(); + components.scene = scene; + components.nodes = [node]; + components.featureMetadata = makeFeatureMetadata(parsedContent); + + if (defined(parsedContent.rtcCenter)) { + components.transform = Matrix4.multiplyByTranslation( + components.transform, + parsedContent.rtcCenter, + components.transform + ); + } + + var 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 () { + var buffers = this._buffers; + for (var i = 0; i < buffers.length; i++) { + buffers[i].destroy(); + } + buffers.length = 0; + + this._components = undefined; + this._parsedContent = undefined; +}; diff --git a/Source/Scene/PntsParser.js b/Source/Scene/PntsParser.js new file mode 100644 index 000000000000..0ababe4cc41b --- /dev/null +++ b/Source/Scene/PntsParser.js @@ -0,0 +1,484 @@ +import arraySlice from "../Core/arraySlice.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import Check from "../Core/Check.js"; +import Color from "../Core/Color.js"; +import combine from "../Core/combine.js"; +import ComponentDatatype from "../Core/ComponentDatatype.js"; +import defaultValue from "../Core/defaultValue.js"; +import defined from "../Core/defined.js"; +import getJsonFromTypedArray from "../Core/getJsonFromTypedArray.js"; +import RuntimeError from "../Core/RuntimeError.js"; +import AttributeType from "./AttributeType.js"; +import Cesium3DTileFeatureTable from "./Cesium3DTileFeatureTable.js"; +import VertexAttributeSemantic from "./VertexAttributeSemantic.js"; + +/** + * Handles parsing of a Point Cloud + * + * @namespace PntsParser + * @private + */ +var PntsParser = {}; + +var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; + +/** + * Parses the contents of a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/PointCloud|Point Cloud}. + * + * @private + * + * @param {*} arrayBuffer The array buffer containing the pnts + * @param {*} [byteOffset=0] The byte offset of the beginning of the pnts in the array buffer + * @returns {Object} An object containing a parsed representation of the point cloud + */ +PntsParser.parse = function (arrayBuffer, byteOffset) { + byteOffset = defaultValue(byteOffset, 0); + //>>includeStart('debug', pragmas.debug); + Check.defined("arrayBuffer", arrayBuffer); + //>>includeEnd('debug'); + + var uint8Array = new Uint8Array(arrayBuffer); + var view = new DataView(arrayBuffer); + byteOffset += sizeOfUint32; // Skip magic + + var version = view.getUint32(byteOffset, true); + if (version !== 1) { + throw new RuntimeError( + "Only Point Cloud tile version 1 is supported. Version " + + version + + " is not." + ); + } + byteOffset += sizeOfUint32; + + // Skip byteLength + byteOffset += sizeOfUint32; + + var featureTableJsonByteLength = view.getUint32(byteOffset, true); + if (featureTableJsonByteLength === 0) { + throw new RuntimeError( + "Feature table must have a byte length greater than zero" + ); + } + byteOffset += sizeOfUint32; + + var featureTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var batchTableJsonByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + var batchTableBinaryByteLength = view.getUint32(byteOffset, true); + byteOffset += sizeOfUint32; + + var featureTableJson = getJsonFromTypedArray( + uint8Array, + byteOffset, + featureTableJsonByteLength + ); + byteOffset += featureTableJsonByteLength; + + var featureTableBinary = new Uint8Array( + arrayBuffer, + byteOffset, + featureTableBinaryByteLength + ); + byteOffset += featureTableBinaryByteLength; + + // Get the batch table JSON and binary + var batchTableJson; + var batchTableBinary; + if (batchTableJsonByteLength > 0) { + // Has a batch table JSON + batchTableJson = getJsonFromTypedArray( + uint8Array, + byteOffset, + batchTableJsonByteLength + ); + byteOffset += batchTableJsonByteLength; + + if (batchTableBinaryByteLength > 0) { + // Has a batch table binary + batchTableBinary = new Uint8Array( + arrayBuffer, + byteOffset, + batchTableBinaryByteLength + ); + byteOffset += batchTableBinaryByteLength; + } + } + + var featureTable = new Cesium3DTileFeatureTable( + featureTableJson, + featureTableBinary + ); + + var pointsLength = featureTable.getGlobalProperty("POINTS_LENGTH"); + featureTable.featuresLength = pointsLength; + + if (!defined(pointsLength)) { + throw new RuntimeError( + "Feature table global property: POINTS_LENGTH must be defined" + ); + } + + var rtcCenter = featureTable.getGlobalProperty( + "RTC_CENTER", + ComponentDatatype.FLOAT, + 3 + ); + if (defined(rtcCenter)) { + rtcCenter = Cartesian3.unpack(rtcCenter); + } + + // Start with the draco compressed properties and add in uncompressed + // properties. + var parsedContent = parseDracoProperties(featureTable, batchTableJson); + parsedContent.rtcCenter = rtcCenter; + parsedContent.pointsLength = pointsLength; + + if (!parsedContent.hasPositions) { + var positions = parsePositions(featureTable); + parsedContent.positions = positions; + parsedContent.hasPositions = + parsedContent.hasPositions || defined(positions); + } + + if (!parsedContent.hasPositions) { + throw new RuntimeError( + "Either POSITION or POSITION_QUANTIZED must be defined." + ); + } + + if (!parsedContent.hasNormals) { + var normals = parseNormals(featureTable); + parsedContent.normals = normals; + parsedContent.hasNormals = parsedContent.hasNormals || defined(normals); + } + + if (!parsedContent.hasColors) { + var colors = parseColors(featureTable); + parsedContent.colors = colors; + parsedContent.hasColors = parsedContent.hasColors || defined(colors); + parsedContent.hasConstantColor = defined(parsedContent.constantColor); + parsedContent.isTranslucent = defined(colors) && colors.isTranslucent; + } + + if (!parsedContent.hasBatchIds) { + var batchIds = parseBatchIds(featureTable); + parsedContent.batchIds = batchIds; + parsedContent.hasBatchIds = parsedContent.hasBatchIds || defined(batchIds); + } + + if (parsedContent.hasBatchIds) { + var batchLength = featureTable.getGlobalProperty("BATCH_LENGTH"); + if (!defined(batchLength)) { + throw new RuntimeError( + "Global property: BATCH_LENGTH must be defined when BATCH_ID is defined." + ); + } + parsedContent.batchLength = batchLength; + } + + if (defined(batchTableBinary)) { + // Copy the batchTableBinary section and let the underlying ArrayBuffer be freed + batchTableBinary = new Uint8Array(batchTableBinary); + parsedContent.batchTableJson = batchTableJson; + parsedContent.batchTableBinary = batchTableBinary; + } + + return parsedContent; +}; + +function parseDracoProperties(featureTable, batchTableJson) { + var featureTableJson = featureTable.json; + var dracoBuffer; + var dracoFeatureTableProperties; + var dracoBatchTableProperties; + + var featureTableDraco = defined(featureTableJson.extensions) + ? featureTableJson.extensions["3DTILES_draco_point_compression"] + : undefined; + var batchTableDraco = + defined(batchTableJson) && defined(batchTableJson.extensions) + ? batchTableJson.extensions["3DTILES_draco_point_compression"] + : undefined; + + if (defined(batchTableDraco)) { + dracoBatchTableProperties = batchTableDraco.properties; + } + + var hasPositions; + var hasColors; + var hasNormals; + var hasBatchIds; + var isTranslucent; + if (defined(featureTableDraco)) { + dracoFeatureTableProperties = featureTableDraco.properties; + var dracoByteOffset = featureTableDraco.byteOffset; + var dracoByteLength = featureTableDraco.byteLength; + if ( + !defined(dracoFeatureTableProperties) || + !defined(dracoByteOffset) || + !defined(dracoByteLength) + ) { + throw new RuntimeError( + "Draco properties, byteOffset, and byteLength must be defined" + ); + } + dracoBuffer = arraySlice( + featureTable.buffer, + dracoByteOffset, + dracoByteOffset + dracoByteLength + ); + hasPositions = defined(dracoFeatureTableProperties.POSITION); + hasColors = + defined(dracoFeatureTableProperties.RGB) || + defined(dracoFeatureTableProperties.RGBA); + hasNormals = defined(dracoFeatureTableProperties.NORMAL); + hasBatchIds = defined(dracoFeatureTableProperties.BATCH_ID); + isTranslucent = defined(dracoFeatureTableProperties.RGBA); + } + + var draco; + if (defined(dracoBuffer)) { + draco = { + buffer: dracoBuffer, + featureTableProperties: dracoFeatureTableProperties, + batchTableProperties: dracoBatchTableProperties, + properties: combine( + dracoFeatureTableProperties, + dracoBatchTableProperties + ), + dequantizeInShader: true, + }; + } + + return { + draco: draco, + hasPositions: hasPositions, + hasColors: hasColors, + isTranslucent: isTranslucent, + hasNormals: hasNormals, + hasBatchIds: hasBatchIds, + }; +} + +function parsePositions(featureTable) { + var featureTableJson = featureTable.json; + + var positions; + if (defined(featureTableJson.POSITION)) { + positions = featureTable.getPropertyArray( + "POSITION", + ComponentDatatype.FLOAT, + 3 + ); + + return { + name: VertexAttributeSemantic.POSITION, + semantic: VertexAttributeSemantic.POSITION, + typedArray: positions, + isQuantized: false, + componentDatatype: ComponentDatatype.FLOAT, + type: AttributeType.VEC3, + }; + } else if (defined(featureTableJson.POSITION_QUANTIZED)) { + positions = featureTable.getPropertyArray( + "POSITION_QUANTIZED", + ComponentDatatype.UNSIGNED_SHORT, + 3 + ); + + var quantizedVolumeScale = featureTable.getGlobalProperty( + "QUANTIZED_VOLUME_SCALE", + ComponentDatatype.FLOAT, + 3 + ); + if (!defined(quantizedVolumeScale)) { + throw new RuntimeError( + "Global property: QUANTIZED_VOLUME_SCALE must be defined for quantized positions." + ); + } + var quantizedRange = (1 << 16) - 1; + + var quantizedVolumeOffset = featureTable.getGlobalProperty( + "QUANTIZED_VOLUME_OFFSET", + ComponentDatatype.FLOAT, + 3 + ); + if (!defined(quantizedVolumeOffset)) { + throw new RuntimeError( + "Global property: QUANTIZED_VOLUME_OFFSET must be defined for quantized positions." + ); + } + + return { + name: VertexAttributeSemantic.POSITION, + semantic: VertexAttributeSemantic.POSITION, + typedArray: positions, + isQuantized: true, + componentDatatype: ComponentDatatype.FLOAT, + type: AttributeType.VEC3, + quantizedRange: quantizedRange, + quantizedVolumeOffset: Cartesian3.unpack(quantizedVolumeOffset), + quantizedVolumeScale: Cartesian3.unpack(quantizedVolumeScale), + quantizedComponentDatatype: ComponentDatatype.UNSIGNED_SHORT, + quantizedType: AttributeType.VEC3, + }; + } +} + +function parseColors(featureTable) { + var featureTableJson = featureTable.json; + + var colors; + if (defined(featureTableJson.RGBA)) { + colors = featureTable.getPropertyArray( + "RGBA", + ComponentDatatype.UNSIGNED_BYTE, + 4 + ); + return { + name: VertexAttributeSemantic.COLOR, + semantic: VertexAttributeSemantic.COLOR, + setIndex: 0, + typedArray: colors, + componentDatatype: ComponentDatatype.UNSIGNED_BYTE, + type: AttributeType.VEC4, + normalized: true, + isRGB565: false, + isTranslucent: true, + }; + } else if (defined(featureTableJson.RGB)) { + colors = featureTable.getPropertyArray( + "RGB", + ComponentDatatype.UNSIGNED_BYTE, + 3 + ); + return { + name: "COLOR", + semantic: VertexAttributeSemantic.COLOR, + setIndex: 0, + typedArray: colors, + componentDatatype: ComponentDatatype.UNSIGNED_BYTE, + type: AttributeType.VEC3, + normalized: true, + isRGB565: false, + isTranslucent: false, + }; + } else if (defined(featureTableJson.RGB565)) { + colors = featureTable.getPropertyArray( + "RGB565", + ComponentDatatype.UNSIGNED_SHORT, + 1 + ); + return { + name: "COLOR", + semantic: VertexAttributeSemantic.COLOR, + setIndex: 0, + typedArray: colors, + // These settings are for the ModelExperimental implementation + // which decodes on the CPU and uploads a VEC3 of float colors. + // PointCloud does the decoding on the GPU so uploads a + // UNSIGNED_SHORT instead. + componentDatatype: ComponentDatatype.FLOAT, + type: AttributeType.VEC3, + normalized: false, + isRGB565: true, + isTranslucent: false, + }; + } else if (defined(featureTableJson.CONSTANT_RGBA)) { + var constantRGBA = featureTable.getGlobalProperty( + "CONSTANT_RGBA", + ComponentDatatype.UNSIGNED_BYTE, + 4 + ); + + var alpha = constantRGBA[3]; + var constantColor = Color.fromBytes( + constantRGBA[0], + constantRGBA[1], + constantRGBA[2], + alpha + ); + + var isTranslucent = alpha < 255; + return { + name: VertexAttributeSemantic.COLOR, + semantic: VertexAttributeSemantic.COLOR, + setIndex: 0, + constantColor: constantColor, + componentDatatype: ComponentDatatype.FLOAT, + type: AttributeType.VEC4, + isQuantized: false, + isTranslucent: isTranslucent, + }; + } + + return undefined; +} + +function parseNormals(featureTable) { + var featureTableJson = featureTable.json; + var normals; + if (defined(featureTableJson.NORMAL)) { + normals = featureTable.getPropertyArray( + "NORMAL", + ComponentDatatype.FLOAT, + 3 + ); + return { + name: VertexAttributeSemantic.NORMAL, + semantic: VertexAttributeSemantic.NORMAL, + typedArray: normals, + octEncoded: false, + octEncodedZXY: false, + componentDatatype: ComponentDatatype.FLOAT, + type: AttributeType.VEC3, + }; + } else if (defined(featureTableJson.NORMAL_OCT16P)) { + normals = featureTable.getPropertyArray( + "NORMAL_OCT16P", + ComponentDatatype.UNSIGNED_BYTE, + 2 + ); + var quantizationBits = 8; + return { + name: VertexAttributeSemantic.NORMAL, + semantic: VertexAttributeSemantic.NORMAL, + typedArray: normals, + octEncoded: true, + octEncodedZXY: false, + quantizedRange: (1 << quantizationBits) - 1, + quantizedType: AttributeType.VEC2, + quantizedComponentDatatype: ComponentDatatype.UNSIGNED_BYTE, + componentDatatype: ComponentDatatype.FLOAT, + type: AttributeType.VEC3, + }; + } + + return undefined; +} + +function parseBatchIds(featureTable) { + var featureTableJson = featureTable.json; + if (defined(featureTableJson.BATCH_ID)) { + var batchIds = featureTable.getPropertyArray( + "BATCH_ID", + ComponentDatatype.UNSIGNED_SHORT, + 1 + ); + return { + name: VertexAttributeSemantic.FEATURE_ID, + semantic: VertexAttributeSemantic.FEATURE_ID, + setIndex: 0, + typedArray: batchIds, + componentDatatype: ComponentDatatype.fromTypedArray(batchIds), + type: AttributeType.SCALAR, + }; + } + + return undefined; +} + +export default PntsParser; diff --git a/Source/Scene/PointCloud.js b/Source/Scene/PointCloud.js index 80415b25df6b..e68c145ba1ee 100644 --- a/Source/Scene/PointCloud.js +++ b/Source/Scene/PointCloud.js @@ -1,4 +1,3 @@ -import arraySlice from "../Core/arraySlice.js"; import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; @@ -10,7 +9,6 @@ import ComponentDatatype from "../Core/ComponentDatatype.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; -import getJsonFromTypedArray from "../Core/getJsonFromTypedArray.js"; import CesiumMath from "../Core/Math.js"; import Matrix4 from "../Core/Matrix4.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; @@ -25,13 +23,14 @@ import Pass from "../Renderer/Pass.js"; import RenderState from "../Renderer/RenderState.js"; import ShaderProgram from "../Renderer/ShaderProgram.js"; import VertexArray from "../Renderer/VertexArray.js"; +import MersenneTwister from "../ThirdParty/mersenne-twister.js"; import when from "../ThirdParty/when.js"; import BlendingState from "./BlendingState.js"; import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js"; -import Cesium3DTileFeatureTable from "./Cesium3DTileFeatureTable.js"; import DracoLoader from "./DracoLoader.js"; import getClipAndStyleCode from "./getClipAndStyleCode.js"; import getClippingFunction from "./getClippingFunction.js"; +import PntsParser from "./PntsParser.js"; import SceneMode from "./SceneMode.js"; import ShadowMode from "./ShadowMode.js"; import StencilConstants from "./StencilConstants.js"; @@ -196,366 +195,98 @@ Object.defineProperties(PointCloud.prototype, { }, }); -var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; - function initialize(pointCloud, options) { - var arrayBuffer = options.arrayBuffer; - var byteOffset = defaultValue(options.byteOffset, 0); - - var uint8Array = new Uint8Array(arrayBuffer); - var view = new DataView(arrayBuffer); - byteOffset += sizeOfUint32; // Skip magic - - var version = view.getUint32(byteOffset, true); - if (version !== 1) { - throw new RuntimeError( - "Only Point Cloud tile version 1 is supported. Version " + - version + - " is not." - ); - } - byteOffset += sizeOfUint32; - - // Skip byteLength - byteOffset += sizeOfUint32; - - var featureTableJsonByteLength = view.getUint32(byteOffset, true); - if (featureTableJsonByteLength === 0) { - throw new RuntimeError( - "Feature table must have a byte length greater than zero" - ); - } - byteOffset += sizeOfUint32; - - var featureTableBinaryByteLength = view.getUint32(byteOffset, true); - byteOffset += sizeOfUint32; - - var batchTableJsonByteLength = view.getUint32(byteOffset, true); - byteOffset += sizeOfUint32; - var batchTableBinaryByteLength = view.getUint32(byteOffset, true); - byteOffset += sizeOfUint32; - - var featureTableJson = getJsonFromTypedArray( - uint8Array, - byteOffset, - featureTableJsonByteLength - ); - byteOffset += featureTableJsonByteLength; - - var featureTableBinary = new Uint8Array( - arrayBuffer, - byteOffset, - featureTableBinaryByteLength - ); - byteOffset += featureTableBinaryByteLength; - - // Get the batch table JSON and binary - var batchTableJson; - var batchTableBinary; - if (batchTableJsonByteLength > 0) { - // Has a batch table JSON - batchTableJson = getJsonFromTypedArray( - uint8Array, - byteOffset, - batchTableJsonByteLength - ); - byteOffset += batchTableJsonByteLength; - - if (batchTableBinaryByteLength > 0) { - // Has a batch table binary - batchTableBinary = new Uint8Array( - arrayBuffer, - byteOffset, - batchTableBinaryByteLength - ); - byteOffset += batchTableBinaryByteLength; - } - } - - var featureTable = new Cesium3DTileFeatureTable( - featureTableJson, - featureTableBinary - ); - - var pointsLength = featureTable.getGlobalProperty("POINTS_LENGTH"); - featureTable.featuresLength = pointsLength; - - if (!defined(pointsLength)) { - throw new RuntimeError( - "Feature table global property: POINTS_LENGTH must be defined" + var parsedContent = PntsParser.parse(options.arrayBuffer, options.byteOffset); + pointCloud._parsedContent = parsedContent; + pointCloud._rtcCenter = parsedContent.rtcCenter; + pointCloud._hasNormals = parsedContent.hasNormals; + pointCloud._hasColors = parsedContent.hasColors; + pointCloud._hasBatchIds = parsedContent.hasBatchIds; + pointCloud._isTranslucent = parsedContent.isTranslucent; + + // If points are not batched and there are per-point properties, use the + // properties as metadata for styling purposes. + if (!parsedContent.hasBatchIds && defined(parsedContent.batchTableBinary)) { + parsedContent.styleableProperties = Cesium3DTileBatchTable.getBinaryProperties( + parsedContent.pointsLength, + parsedContent.batchTableJson, + parsedContent.batchTableBinary ); } - var rtcCenter = featureTable.getGlobalProperty( - "RTC_CENTER", - ComponentDatatype.FLOAT, - 3 - ); - if (defined(rtcCenter)) { - pointCloud._rtcCenter = Cartesian3.unpack(rtcCenter); - } - - var positions; - var colors; - var normals; - var batchIds; - - var hasPositions = false; - var hasColors = false; - var hasNormals = false; - var hasBatchIds = false; - - var isQuantized = false; - var isTranslucent = false; - var isRGB565 = false; - var isOctEncoded16P = false; - - var dracoBuffer; - var dracoFeatureTableProperties; - var dracoBatchTableProperties; - - var featureTableDraco = defined(featureTableJson.extensions) - ? featureTableJson.extensions["3DTILES_draco_point_compression"] - : undefined; - var batchTableDraco = - defined(batchTableJson) && defined(batchTableJson.extensions) - ? batchTableJson.extensions["3DTILES_draco_point_compression"] - : undefined; - - if (defined(batchTableDraco)) { - dracoBatchTableProperties = batchTableDraco.properties; - } - - if (defined(featureTableDraco)) { - dracoFeatureTableProperties = featureTableDraco.properties; - var dracoByteOffset = featureTableDraco.byteOffset; - var dracoByteLength = featureTableDraco.byteLength; - if ( - !defined(dracoFeatureTableProperties) || - !defined(dracoByteOffset) || - !defined(dracoByteLength) - ) { - throw new RuntimeError( - "Draco properties, byteOffset, and byteLength must be defined" - ); - } - dracoBuffer = arraySlice( - featureTableBinary, - dracoByteOffset, - dracoByteOffset + dracoByteLength - ); - hasPositions = defined(dracoFeatureTableProperties.POSITION); - hasColors = - defined(dracoFeatureTableProperties.RGB) || - defined(dracoFeatureTableProperties.RGBA); - hasNormals = defined(dracoFeatureTableProperties.NORMAL); - hasBatchIds = defined(dracoFeatureTableProperties.BATCH_ID); - isTranslucent = defined(dracoFeatureTableProperties.RGBA); + if (defined(parsedContent.draco)) { + var draco = parsedContent.draco; pointCloud._decodingState = DecodingState.NEEDS_DECODE; + draco.dequantizeInShader = pointCloud._dequantizeInShader; } - var draco; - if (defined(dracoBuffer)) { - draco = { - buffer: dracoBuffer, - featureTableProperties: dracoFeatureTableProperties, - batchTableProperties: dracoBatchTableProperties, - properties: combine( - dracoFeatureTableProperties, - dracoBatchTableProperties - ), - dequantizeInShader: pointCloud._dequantizeInShader, - }; - } - - if (!hasPositions) { - if (defined(featureTableJson.POSITION)) { - positions = featureTable.getPropertyArray( - "POSITION", - ComponentDatatype.FLOAT, - 3 - ); - hasPositions = true; - } else if (defined(featureTableJson.POSITION_QUANTIZED)) { - positions = featureTable.getPropertyArray( - "POSITION_QUANTIZED", - ComponentDatatype.UNSIGNED_SHORT, - 3 - ); - isQuantized = true; - hasPositions = true; - - var quantizedVolumeScale = featureTable.getGlobalProperty( - "QUANTIZED_VOLUME_SCALE", - ComponentDatatype.FLOAT, - 3 - ); - if (!defined(quantizedVolumeScale)) { - throw new RuntimeError( - "Global property: QUANTIZED_VOLUME_SCALE must be defined for quantized positions." - ); - } - pointCloud._quantizedVolumeScale = Cartesian3.unpack( - quantizedVolumeScale - ); - pointCloud._quantizedRange = (1 << 16) - 1; - - var quantizedVolumeOffset = featureTable.getGlobalProperty( - "QUANTIZED_VOLUME_OFFSET", - ComponentDatatype.FLOAT, - 3 - ); - if (!defined(quantizedVolumeOffset)) { - throw new RuntimeError( - "Global property: QUANTIZED_VOLUME_OFFSET must be defined for quantized positions." - ); - } - pointCloud._quantizedVolumeOffset = Cartesian3.unpack( - quantizedVolumeOffset - ); - } + var positions = parsedContent.positions; + if (defined(positions)) { + pointCloud._isQuantized = positions.isQuantized; + pointCloud._quantizedVolumeScale = positions.quantizedVolumeScale; + pointCloud._quantizedVolumeOffset = positions.quantizedVolumeOffset; + pointCloud._quantizedRange = positions.quantizedRange; } - if (!hasColors) { - if (defined(featureTableJson.RGBA)) { - colors = featureTable.getPropertyArray( - "RGBA", - ComponentDatatype.UNSIGNED_BYTE, - 4 - ); - isTranslucent = true; - hasColors = true; - } else if (defined(featureTableJson.RGB)) { - colors = featureTable.getPropertyArray( - "RGB", - ComponentDatatype.UNSIGNED_BYTE, - 3 - ); - hasColors = true; - } else if (defined(featureTableJson.RGB565)) { - colors = featureTable.getPropertyArray( - "RGB565", - ComponentDatatype.UNSIGNED_SHORT, - 1 - ); - isRGB565 = true; - hasColors = true; - } + var normals = parsedContent.normals; + if (defined(normals)) { + pointCloud._isOctEncoded16P = normals.octEncoded; } - if (!hasNormals) { - if (defined(featureTableJson.NORMAL)) { - normals = featureTable.getPropertyArray( - "NORMAL", - ComponentDatatype.FLOAT, - 3 - ); - hasNormals = true; - } else if (defined(featureTableJson.NORMAL_OCT16P)) { - normals = featureTable.getPropertyArray( - "NORMAL_OCT16P", - ComponentDatatype.UNSIGNED_BYTE, - 2 + var colors = parsedContent.colors; + if (defined(colors)) { + if (defined(colors.constantColor)) { + pointCloud._constantColor = Color.clone( + colors.constantColor, + pointCloud._constantColor ); - isOctEncoded16P = true; - hasNormals = true; - } - } - if (!hasBatchIds) { - if (defined(featureTableJson.BATCH_ID)) { - batchIds = featureTable.getPropertyArray( - "BATCH_ID", - ComponentDatatype.UNSIGNED_SHORT, - 1 - ); - hasBatchIds = true; + // Constant colors are handled as a uniform rather than a vertex + // attribute. + pointCloud._hasColors = false; } + pointCloud._isRGB565 = colors.isRGB565; } - if (!hasPositions) { - throw new RuntimeError( - "Either POSITION or POSITION_QUANTIZED must be defined." - ); - } - - if (defined(featureTableJson.CONSTANT_RGBA)) { - var constantRGBA = featureTable.getGlobalProperty( - "CONSTANT_RGBA", - ComponentDatatype.UNSIGNED_BYTE, - 4 - ); - pointCloud._constantColor = Color.fromBytes( - constantRGBA[0], - constantRGBA[1], - constantRGBA[2], - constantRGBA[3], - pointCloud._constantColor - ); - } - - if (hasBatchIds) { - var batchLength = featureTable.getGlobalProperty("BATCH_LENGTH"); - if (!defined(batchLength)) { - throw new RuntimeError( - "Global property: BATCH_LENGTH must be defined when BATCH_ID is defined." - ); - } - - if (defined(batchTableBinary)) { - // Copy the batchTableBinary section and let the underlying ArrayBuffer be freed - batchTableBinary = new Uint8Array(batchTableBinary); - } - - if (defined(pointCloud._batchTableLoaded)) { - pointCloud._batchTableLoaded( - batchLength, - batchTableJson, - batchTableBinary - ); - } + // PntsParser parses BATCH_ID as FEATURE_ID for EXT_mesh_features. + // These properties aren't used but rename them to BATCH_ID to avoid + // confusion when debugging. + var batchIds = parsedContent.batchIds; + if (defined(parsedContent.batchIds)) { + batchIds.name = "BATCH_ID"; + batchIds.semantic = "BATCH_ID"; + batchIds.setIndex = undefined; } - // If points are not batched and there are per-point properties, use these properties for styling purposes - var styleableProperties; - if (!hasBatchIds && defined(batchTableBinary)) { - styleableProperties = Cesium3DTileBatchTable.getBinaryProperties( - pointsLength, - batchTableJson, - batchTableBinary + if (parsedContent.hasBatchIds) { + pointCloud._batchTableLoaded( + parsedContent.batchLength, + parsedContent.batchTableJson, + parsedContent.batchTableBinary ); } - pointCloud._parsedContent = { - positions: positions, - colors: colors, - normals: normals, - batchIds: batchIds, - styleableProperties: styleableProperties, - draco: draco, - }; - pointCloud._pointsLength = pointsLength; - pointCloud._isQuantized = isQuantized; - pointCloud._isOctEncoded16P = isOctEncoded16P; - pointCloud._isRGB565 = isRGB565; - pointCloud._isTranslucent = isTranslucent; - pointCloud._hasColors = hasColors; - pointCloud._hasNormals = hasNormals; - pointCloud._hasBatchIds = hasBatchIds; + pointCloud._pointsLength = parsedContent.pointsLength; } var scratchMin = new Cartesian3(); var scratchMax = new Cartesian3(); var scratchPosition = new Cartesian3(); + +// Use MersenneTwister directly to avoid interfering with CesiumMath.nextRandomNumber() +// See https://github.com/CesiumGS/cesium/issues/9730 +var randomNumberGenerator; var randomValues; function getRandomValues(samplesLength) { // Use same random values across all runs if (!defined(randomValues)) { - CesiumMath.setRandomNumberSeed(0); + // 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 (var i = 0; i < samplesLength; ++i) { - randomValues[i] = CesiumMath.nextRandomNumber(); + randomValues[i] = randomNumberGenerator.random(); } } return randomValues; @@ -683,7 +414,7 @@ function createResources(pointCloud, frameState) { var positionsVertexBuffer = Buffer.createVertexBuffer({ context: context, - typedArray: positions, + typedArray: positions.typedArray, usage: BufferUsage.STATIC_DRAW, }); pointCloud._geometryByteLength += positionsVertexBuffer.sizeInBytes; @@ -692,7 +423,7 @@ function createResources(pointCloud, frameState) { if (hasColors) { colorsVertexBuffer = Buffer.createVertexBuffer({ context: context, - typedArray: colors, + typedArray: colors.typedArray, usage: BufferUsage.STATIC_DRAW, }); pointCloud._geometryByteLength += colorsVertexBuffer.sizeInBytes; @@ -702,7 +433,7 @@ function createResources(pointCloud, frameState) { if (hasNormals) { normalsVertexBuffer = Buffer.createVertexBuffer({ context: context, - typedArray: normals, + typedArray: normals.typedArray, usage: BufferUsage.STATIC_DRAW, }); pointCloud._geometryByteLength += normalsVertexBuffer.sizeInBytes; @@ -713,7 +444,7 @@ function createResources(pointCloud, frameState) { batchIds = prepareVertexAttribute(batchIds, "batchIds"); batchIdsVertexBuffer = Buffer.createVertexBuffer({ context: context, - typedArray: batchIds, + typedArray: batchIds.typedArray, usage: BufferUsage.STATIC_DRAW, }); pointCloud._geometryByteLength += batchIdsVertexBuffer.sizeInBytes; @@ -750,7 +481,7 @@ function createResources(pointCloud, frameState) { ); } else { pointCloud._boundingSphere = computeApproximateBoundingSphereFromPositions( - positions + positions.typedArray ); } } @@ -810,7 +541,7 @@ function createResources(pointCloud, frameState) { index: batchIdLocation, vertexBuffer: batchIdsVertexBuffer, componentsPerAttribute: 1, - componentDatatype: ComponentDatatype.fromTypedArray(batchIds), + componentDatatype: ComponentDatatype.fromTypedArray(batchIds.typedArray), normalize: false, offsetInBytes: 0, strideInBytes: 0, @@ -1479,22 +1210,32 @@ function decodeDraco(pointCloud, context) { }; } } - parsedContent.positions = defaultValue( - decodedPositions, - parsedContent.positions - ); - parsedContent.colors = defaultValue( - defaultValue(decodedRgba, decodedRgb), - parsedContent.colors - ); - parsedContent.normals = defaultValue( - decodedNormals, - parsedContent.normals - ); - parsedContent.batchIds = defaultValue( - decodedBatchIds, - parsedContent.batchIds - ); + + if (defined(decodedPositions)) { + parsedContent.positions = { + typedArray: decodedPositions, + }; + } + + var decodedColors = defaultValue(decodedRgba, decodedRgb); + if (defined(decodedColors)) { + parsedContent.colors = { + typedArray: decodedColors, + }; + } + + if (defined(decodedNormals)) { + parsedContent.normals = { + typedArray: decodedNormals, + }; + } + + if (defined(decodedBatchIds)) { + parsedContent.batchIds = { + typedArray: decodedBatchIds, + }; + } + parsedContent.styleableProperties = styleableProperties; }) .otherwise(function (error) { diff --git a/Source/Shaders/Builtin/Functions/linearToSrgb.glsl b/Source/Shaders/Builtin/Functions/linearToSrgb.glsl new file mode 100644 index 000000000000..c6ca342137a9 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/linearToSrgb.glsl @@ -0,0 +1,16 @@ +/** + * Converts a linear RGB color to an sRGB color. + * + * @param {vec3|vec4} linearIn The color in linear color space. + * @returns {vec3|vec4} The color in sRGB color space. The vector type matches the input. + */ +vec3 czm_linearToSrgb(vec3 linearIn) +{ + return pow(linearIn, vec3(1.0/2.2)); +} + +vec4 czm_linearToSrgb(vec4 linearIn) +{ + vec3 srgbOut = pow(linearIn.rgb, vec3(1.0/2.2)); + return vec4(srgbOut, linearIn.a); +} diff --git a/Source/Shaders/Builtin/Functions/srgbToLinear.glsl b/Source/Shaders/Builtin/Functions/srgbToLinear.glsl new file mode 100644 index 000000000000..a2bd931862e7 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/srgbToLinear.glsl @@ -0,0 +1,16 @@ +/** + * Converts an sRGB color to a linear RGB color. + * + * @param {vec3|vec4} srgbIn The color in sRGB space + * @returns {vec3|vec4} The color in linear color space. The vector type matches the input. + */ +vec3 czm_srgbToLinear(vec3 srgbIn) +{ + return pow(srgbIn, vec3(2.2)); +} + +vec4 czm_srgbToLinear(vec4 srgbIn) +{ + vec3 linearOut = pow(srgbIn.rgb, vec3(2.2)); + return vec4(linearOut, srgbIn.a); +} diff --git a/Source/Shaders/ModelExperimental/LightingStageFS.glsl b/Source/Shaders/ModelExperimental/LightingStageFS.glsl index 3e34af75c73d..39075c1a9cb7 100644 --- a/Source/Shaders/ModelExperimental/LightingStageFS.glsl +++ b/Source/Shaders/ModelExperimental/LightingStageFS.glsl @@ -1,22 +1,4 @@ -vec3 LINEARtoSRGB(vec3 linearIn) -{ - #ifndef HDR - return pow(linearIn, vec3(1.0/2.2)); - #else - return linearIn; - #endif -} - #ifdef LIGHTING_PBR -vec3 applyTonemapping(vec3 linearIn) -{ - #ifndef HDR - return czm_acesTonemapping(linearIn); - #else - return linearIn; - #endif -} - vec3 computePbrLighting(czm_modelMaterial inputMaterial) { czm_pbrParameters pbrParameters; @@ -40,8 +22,14 @@ vec3 computePbrLighting(czm_modelMaterial inputMaterial) color *= inputMaterial.occlusion; color += inputMaterial.emissive; - // Convert high-dynamic range to low-dynamic range in HDR mode - color = applyTonemapping(color); + // In HDR mode, the frame buffer is in linear color space. The + // post-processing stages (see PostProcessStageCollection) will handle + // tonemapping. However, if HDR is not enabled, we must tonemap else large + // values may be clamped to 1.0 + #ifndef HDR + color = czm_acesTonemapping(color); + #endif + return color; } #endif @@ -58,7 +46,11 @@ void lightingStage(inout czm_modelMaterial material) color = material.diffuse; #endif - color = LINEARtoSRGB(color); + // If HDR is not enabled, the frame buffer stores sRGB colors rather than + // linear colors so the linear value must be converted. + #ifndef HDR + color = czm_linearToSrgb(color); + #endif material.diffuse = color; } diff --git a/Source/Shaders/ModelExperimental/MaterialStageFS.glsl b/Source/Shaders/ModelExperimental/MaterialStageFS.glsl index 2c9e3e155b9b..42bc6ee1ae2d 100644 --- a/Source/Shaders/ModelExperimental/MaterialStageFS.glsl +++ b/Source/Shaders/ModelExperimental/MaterialStageFS.glsl @@ -11,17 +11,6 @@ vec3 blend(vec3 sourceColor, vec3 styleColor, float styleColorBlend) return color; } -vec3 SRGBtoLINEAR3(vec3 srgbIn) -{ - return pow(srgbIn, vec3(2.2)); -} - -vec4 SRGBtoLINEAR4(vec4 srgbIn) -{ - vec3 linearOut = pow(srgbIn.rgb, vec3(2.2)); - return vec4(linearOut, srgbIn.a); -} - vec2 computeTextureTransform(vec2 texCoord, mat3 textureTransform) { return vec2(textureTransform * vec3(texCoord, 1.0)); @@ -83,7 +72,7 @@ void materialStage(inout czm_modelMaterial material, ProcessedAttributes attribu baseColorTexCoords = computeTextureTransform(baseColorTexCoords, u_baseColorTextureTransform); #endif - baseColorWithAlpha = SRGBtoLINEAR4(texture2D(u_baseColorTexture, baseColorTexCoords)); + baseColorWithAlpha = czm_srgbToLinear(texture2D(u_baseColorTexture, baseColorTexCoords)); #ifdef HAS_BASE_COLOR_FACTOR baseColorWithAlpha *= u_baseColorFactor; @@ -93,7 +82,12 @@ void materialStage(inout czm_modelMaterial material, ProcessedAttributes attribu #endif #ifdef HAS_COLOR_0 - baseColorWithAlpha *= attributes.color_0; + vec4 color = attributes.color_0; + // .pnts files store colors in the sRGB color space + #ifdef HAS_SRGB_COLOR + color = czm_srgbToLinear(color); + #endif + baseColorWithAlpha *= color; #endif material.diffuse = baseColorWithAlpha.rgb; @@ -117,7 +111,7 @@ void materialStage(inout czm_modelMaterial material, ProcessedAttributes attribu emissiveTexCoords = computeTextureTransform(emissiveTexCoords, u_emissiveTextureTransform); #endif - vec3 emissive = SRGBtoLINEAR3(texture2D(u_emissiveTexture, emissiveTexCoords).rgb); + vec3 emissive = czm_srgbToLinear(texture2D(u_emissiveTexture, emissiveTexCoords).rgb); #ifdef HAS_EMISSIVE_FACTOR emissive *= u_emissiveFactor; #endif @@ -133,7 +127,7 @@ void materialStage(inout czm_modelMaterial material, ProcessedAttributes attribu specularGlossinessTexCoords = computeTextureTransform(specularGlossinessTexCoords, u_specularGlossinessTextureTransform); #endif - vec4 specularGlossiness = SRGBtoLINEAR4(texture2D(u_specularGlossinessTexture, specularGlossinessTexCoords)); + vec4 specularGlossiness = czm_srgbToLinear(texture2D(u_specularGlossinessTexture, specularGlossinessTexCoords)); vec3 specular = specularGlossiness.rgb; float glossiness = specularGlossiness.a; #ifdef HAS_SPECULAR_FACTOR @@ -163,7 +157,7 @@ void materialStage(inout czm_modelMaterial material, ProcessedAttributes attribu diffuseTexCoords = computeTextureTransform(diffuseTexCoords, u_diffuseTextureTransform); #endif - vec4 diffuse = SRGBtoLINEAR4(texture2D(u_diffuseTexture, diffuseTexCoords)); + vec4 diffuse = czm_srgbToLinear(texture2D(u_diffuseTexture, diffuseTexCoords)); #ifdef HAS_DIFFUSE_FACTOR diffuse *= u_diffuseFactor; #endif diff --git a/Specs/Core/AttributeCompressionSpec.js b/Specs/Core/AttributeCompressionSpec.js index 5e4892026d53..0f68d7d2a632 100644 --- a/Specs/Core/AttributeCompressionSpec.js +++ b/Specs/Core/AttributeCompressionSpec.js @@ -1278,4 +1278,57 @@ describe("Core/AttributeCompression", function () { expect(result[i]).toEqual(expected[i]); } }); + + it("decodeRGB565 throws without typedArray", function () { + expect(function () { + return AttributeCompression.decodeRGB565(); + }).toThrowDeveloperError(); + }); + + it("decodeRGB565 throws if arrays are the wrong size", function () { + expect(function () { + return AttributeCompression.decodeRGB565( + new Uint16Array([0]), + new Float32Array(1) + ); + }).toThrowDeveloperError(); + }); + + it("decodeRGB565 works", function () { + var input = new Uint16Array([ + 0, + //0b00001_000001_00001 + 2881, + //0b10000_100000_01000 + 33800, + //0b11111_111111_11111 + 65535, + ]); + var expected = new Float32Array([ + 0, + 0, + 0, + 1 / 31, + 1 / 63, + 1 / 31, + 16 / 31, + 32 / 63, + 8 / 31, + 31 / 31, + 63 / 63, + 31 / 31, + ]); + + var result = new Float32Array(input.length * 3); + AttributeCompression.decodeRGB565(input, result); + + for (var i = 0; i < input.length; i++) { + expect(result[i]).toEqual(expected[i]); + } + }); + + it("decodeRGB565 creates a result array if not defined", function () { + var result = AttributeCompression.decodeRGB565(new Uint16Array([0])); + expect(result).toEqual(new Float32Array([0, 0, 0])); + }); }); diff --git a/Specs/Scene/B3dmParserSpec.js b/Specs/Scene/B3dmParserSpec.js index 25b7665b817d..1aa483014f2c 100644 --- a/Specs/Scene/B3dmParserSpec.js +++ b/Specs/Scene/B3dmParserSpec.js @@ -47,13 +47,17 @@ describe( var arrayBuffer = Cesium3DTilesTester.generateBatchedTileBuffer({ version: 2, }); - Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "b3dm"); + expect(function () { + B3dmParser.parse(arrayBuffer); + }).toThrowRuntimeError(); }); it("throws with empty gltf", function () { // Expect to throw DeveloperError in Model due to invalid gltf magic var arrayBuffer = Cesium3DTilesTester.generateBatchedTileBuffer(); - Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "b3dm"); + expect(function () { + B3dmParser.parse(arrayBuffer); + }).toThrowRuntimeError(); }); it("throws on undefined arrayBuffer", function () { diff --git a/Specs/Scene/Implicit3DTileContentSpec.js b/Specs/Scene/Implicit3DTileContentSpec.js index 332239f0a3bc..db4cd552154b 100644 --- a/Specs/Scene/Implicit3DTileContentSpec.js +++ b/Specs/Scene/Implicit3DTileContentSpec.js @@ -1052,7 +1052,7 @@ describe( expect(contents[0].groupMetadata).toBe(ground); expect(contents[1].groupMetadata).toBe(sky); } else { - // parent tile is a single B3DM tile + // parent tile is a single b3dm tile expect(tile.content.groupMetadata).toBe(ground); } }); diff --git a/Specs/Scene/ModelExperimental/B3dmLoaderSpec.js b/Specs/Scene/ModelExperimental/B3dmLoaderSpec.js index 04bf49e0b295..91ab549fd5dc 100644 --- a/Specs/Scene/ModelExperimental/B3dmLoaderSpec.js +++ b/Specs/Scene/ModelExperimental/B3dmLoaderSpec.js @@ -49,23 +49,34 @@ describe("Scene/ModelExperimental/B3dmLoader", function () { ResourceCache.clearForSpecs(); }); + function loadB3dmArrayBuffer(resource, arrayBuffer) { + var loader = new B3dmLoader({ + b3dmResource: resource, + arrayBuffer: arrayBuffer, + }); + b3dmLoaders.push(loader); + loader.load(); + + return waitForLoaderProcess(loader, scene); + } + function loadB3dm(b3dmPath) { var resource = Resource.createIfNeeded(b3dmPath); return Resource.fetchArrayBuffer({ url: b3dmPath, }).then(function (arrayBuffer) { - var loader = new B3dmLoader({ - b3dmResource: resource, - arrayBuffer: arrayBuffer, - }); - b3dmLoaders.push(loader); - loader.load(); - - return waitForLoaderProcess(loader, scene); + return loadB3dmArrayBuffer(resource, arrayBuffer); }); } + function expectLoadError(arrayBuffer) { + var resource = new Resource("http://example.com/test.b3dm"); + expect(function () { + return loadB3dmArrayBuffer(resource, arrayBuffer); + }).toThrowRuntimeError(); + } + it("loads BatchedNoBatchIds", function () { return loadB3dm(noBatchIdsUrl).then(function (loader) { var components = loader.components; @@ -137,16 +148,16 @@ describe("Scene/ModelExperimental/B3dmLoader", function () { var arrayBuffer = Cesium3DTilesTester.generateBatchedTileBuffer({ version: 2, }); - Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "b3dm"); + expectLoadError(arrayBuffer); }); it("throws with empty gltf", function () { // Expect to throw DeveloperError in Model due to invalid gltf magic var arrayBuffer = Cesium3DTilesTester.generateBatchedTileBuffer(); - Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "b3dm"); + expectLoadError(arrayBuffer); }); - it("destroys B3DM loader", function () { + it("destroys b3dm loader", function () { var unloadGltfLoader = spyOn( GltfLoader.prototype, "unload" diff --git a/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js b/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js index 9c8b19e8b960..a89512d1b8f3 100644 --- a/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js +++ b/Specs/Scene/ModelExperimental/GeometryPipelineStageSpec.js @@ -5,6 +5,7 @@ import { FeatureIdPipelineStage, GltfLoader, GeometryPipelineStage, + ModelExperimentalType, Resource, ResourceCache, ShaderBuilder, @@ -128,6 +129,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; GeometryPipelineStage.process(renderResources, positionOnlyPrimitive); @@ -196,6 +200,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(boxTextured).then(function (gltfLoader) { @@ -308,6 +315,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(boomBoxSpecularGlossiness).then(function (gltfLoader) { @@ -450,6 +460,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(microcosm).then(function (gltfLoader) { @@ -570,6 +583,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(boxVertexColors).then(function (gltfLoader) { @@ -712,6 +728,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(pointCloudRGB).then(function (gltfLoader) { @@ -811,6 +830,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; GeometryPipelineStage.process(renderResources, customAttributePrimitive); @@ -897,6 +919,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(buildingsMetadata).then(function (gltfLoader) { @@ -994,6 +1019,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(weather).then(function (gltfLoader) { @@ -1075,6 +1103,9 @@ describe( attributes: [], shaderBuilder: new ShaderBuilder(), attributeIndex: 1, + model: { + type: ModelExperimentalType.TILE_GLTF, + }, }; return loadGltf(dracoMilkTruck).then(function (gltfLoader) { diff --git a/Specs/Scene/ModelExperimental/ModelExperimental3DTileContentSpec.js b/Specs/Scene/ModelExperimental/ModelExperimental3DTileContentSpec.js index efe64aecbfcf..f63e0602097d 100644 --- a/Specs/Scene/ModelExperimental/ModelExperimental3DTileContentSpec.js +++ b/Specs/Scene/ModelExperimental/ModelExperimental3DTileContentSpec.js @@ -60,7 +60,7 @@ describe("Scene/ModelExperimental/ModelExperimental3DTileContent", function () { return Cesium3DTilesTester.resolvesReadyPromise(scene, gltfContentUrl); }); - it("resolves readyPromise with B3DM", function () { + it("resolves readyPromise with b3dm", function () { setCamera(centerLongitude, centerLatitude, 15.0); return Cesium3DTilesTester.resolvesReadyPromise(scene, withBatchTableUrl); }); @@ -73,7 +73,7 @@ describe("Scene/ModelExperimental/ModelExperimental3DTileContent", function () { ); }); - it("renders B3DM content", function () { + it("renders b3dm content", function () { setCamera(centerLongitude, centerLatitude, 15.0); return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then( function (tileset) { @@ -82,7 +82,7 @@ describe("Scene/ModelExperimental/ModelExperimental3DTileContent", function () { ); }); - it("renders B3DM content without features", function () { + it("renders b3dm content without features", function () { setCamera(centerLongitude, centerLatitude, 15.0); return Cesium3DTilesTester.loadTileset(scene, noBatchIdsUrl).then(function ( tileset @@ -109,7 +109,7 @@ describe("Scene/ModelExperimental/ModelExperimental3DTileContent", function () { ); }); - it("picks from B3DM", function () { + it("picks from b3dm", function () { setCamera(centerLongitude, centerLatitude, 15.0); return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then( function (tileset) { @@ -147,7 +147,7 @@ describe("Scene/ModelExperimental/ModelExperimental3DTileContent", function () { ); }); - it("picks from B3DM batch table", function () { + it("picks from b3dm batch table", function () { setCamera(centerLongitude, centerLatitude, 15.0); return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then( function (tileset) { diff --git a/Specs/Scene/ModelExperimental/PntsLoaderSpec.js b/Specs/Scene/ModelExperimental/PntsLoaderSpec.js new file mode 100644 index 000000000000..b95f3f2039c4 --- /dev/null +++ b/Specs/Scene/ModelExperimental/PntsLoaderSpec.js @@ -0,0 +1,637 @@ +import { + AttributeType, + Color, + ComponentDatatype, + MetadataClass, + MetadataComponentType, + MetadataType, + PntsLoader, + Resource, + ResourceCache, + VertexAttributeSemantic, +} from "../../../Source/Cesium.js"; +import createScene from "../../createScene.js"; +import waitForLoaderProcess from "../../waitForLoaderProcess.js"; +import Cesium3DTilesTester from "../../Cesium3DTilesTester.js"; + +describe("Scene/ModelExperimental/PntsLoader", function () { + var pointCloudRGBUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudRGB/pointCloudRGB.pnts"; + var pointCloudRGBAUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudRGBA/pointCloudRGBA.pnts"; + var pointCloudRGB565Url = + "./Data/Cesium3DTiles/PointCloud/PointCloudRGB565/pointCloudRGB565.pnts"; + var pointCloudNoColorUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudNoColor/pointCloudNoColor.pnts"; + var pointCloudConstantColorUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/pointCloudConstantColor.pnts"; + var pointCloudNormalsUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudNormals/pointCloudNormals.pnts"; + var pointCloudNormalsOctEncodedUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/pointCloudNormalsOctEncoded.pnts"; + var pointCloudQuantizedUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudQuantized/pointCloudQuantized.pnts"; + var pointCloudQuantizedOctEncodedUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/pointCloudQuantizedOctEncoded.pnts"; + var pointCloudDracoUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudDraco/pointCloudDraco.pnts"; + var pointCloudDracoPartialUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudDracoPartial/pointCloudDracoPartial.pnts"; + var pointCloudDracoBatchedUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudDracoBatched/pointCloudDracoBatched.pnts"; + var pointCloudWGS84Url = + "./Data/Cesium3DTiles/PointCloud/PointCloudWGS84/pointCloudWGS84.pnts"; + var pointCloudBatchedUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudBatched/pointCloudBatched.pnts"; + var pointCloudWithPerPointPropertiesUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/pointCloudWithPerPointProperties.pnts"; + var pointCloudWithUnicodePropertyNamesUrl = + "./Data/Cesium3DTiles/PointCloud/PointCloudWithUnicodePropertyNames/pointCloudWithUnicodePropertyNames.pnts"; + + var scene; + var pntsLoaders = []; + + beforeAll(function () { + scene = createScene(); + }); + + afterAll(function () { + scene.destroyForSpecs(); + }); + + afterEach(function () { + for (var i = 0; i < pntsLoaders.length; i++) { + var loader = pntsLoaders[i]; + if (!loader.isDestroyed()) { + loader.destroy(); + } + } + pntsLoaders.length = 0; + ResourceCache.clearForSpecs(); + }); + + function loadPntsArrayBuffer(arrayBuffer) { + var loader = new PntsLoader({ + arrayBuffer: arrayBuffer, + }); + pntsLoaders.push(loader); + loader.load(); + return waitForLoaderProcess(loader, scene); + } + + function loadPnts(pntsPath) { + return Resource.fetchArrayBuffer({ + url: pntsPath, + }).then(loadPntsArrayBuffer); + } + + function expectLoadError(arrayBuffer) { + expect(function () { + return loadPntsArrayBuffer(arrayBuffer); + }).toThrowRuntimeError(); + } + + function expectEmptyMetadata(featureMetadata) { + expect(featureMetadata).toBeDefined(); + expect(featureMetadata.schema).toEqual({}); + expect(featureMetadata.propertyTableCount).toEqual(1); + var propertyTable = featureMetadata.getPropertyTable(0); + expect(propertyTable.getPropertyIds(0)).toEqual([]); + } + + function expectMetadata(featureMetadata, expectedProperties) { + expect(featureMetadata).toBeDefined(); + var schema = featureMetadata.schema; + var batchClass = schema.classes[MetadataClass.BATCH_TABLE_CLASS_NAME]; + var properties = batchClass.properties; + + expect(featureMetadata.propertyTableCount).toEqual(1); + var propertyTable = featureMetadata.getPropertyTable(0); + expect(propertyTable.getPropertyIds(0).sort()).toEqual( + Object.keys(expectedProperties).sort() + ); + + for (var propertyName in expectedProperties) { + if (expectedProperties.hasOwnProperty(propertyName)) { + var expectedProperty = expectedProperties[propertyName]; + var property = properties[propertyName]; + + if (expectedProperty.isJson) { + // If the batch table had JSON properties, the property will not + // be in the schema, so check if we can access it. + expect(propertyTable.getProperty(0, propertyName)).toBeDefined(); + } else { + // Check the property declaration is expected + expect(property.type).toEqual(expectedProperty.type); + expect(property.componentType).toEqual( + expectedProperty.componentType + ); + } + } + } + } + + function expectPosition(attribute) { + expect(attribute.name).toBe("POSITION"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.POSITION); + expect(attribute.componentDatatype).toBe(ComponentDatatype.FLOAT); + expect(attribute.type).toBe(AttributeType.VEC3); + expect(attribute.normalized).toBe(false); + expect(attribute.quantization).not.toBeDefined(); + expect(attribute.max).toBeDefined(); + expect(attribute.min).toBeDefined(); + } + + function expectPositionQuantized(attribute) { + expect(attribute.name).toBe("POSITION"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.POSITION); + expect(attribute.componentDatatype).toBe(ComponentDatatype.FLOAT); + expect(attribute.type).toBe(AttributeType.VEC3); + expect(attribute.normalized).toBe(false); + expect(attribute.max).toBeDefined(); + expect(attribute.min).toBeDefined(); + + var quantization = attribute.quantization; + expect(quantization.componentDatatype).toBe( + ComponentDatatype.UNSIGNED_SHORT + ); + expect(quantization.normalizationRange).toBeDefined(); + expect(quantization.octEncoded).toBe(false); + expect(quantization.quantizedVolumeDimensions).toBeDefined(); + expect(quantization.quantizedVolumeOffset).toBeDefined(); + expect(quantization.quantizedVolumeStepSize).toBeDefined(); + expect(quantization.type).toBe(AttributeType.VEC3); + } + + function expectColorRGB(attribute) { + expect(attribute.name).toBe("COLOR"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.COLOR); + expect(attribute.setIndex).toBe(0); + expect(attribute.componentDatatype).toBe(ComponentDatatype.UNSIGNED_BYTE); + expect(attribute.type).toBe(AttributeType.VEC3); + expect(attribute.normalized).toBe(true); + } + + function expectColorRGB565(attribute) { + expect(attribute.name).toBe("COLOR"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.COLOR); + expect(attribute.setIndex).toBe(0); + expect(attribute.componentDatatype).toBe(ComponentDatatype.FLOAT); + expect(attribute.type).toBe(AttributeType.VEC3); + expect(attribute.normalized).toBe(false); + } + + function expectColorRGBA(attribute) { + expect(attribute.name).toBe("COLOR"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.COLOR); + expect(attribute.setIndex).toBe(0); + expect(attribute.componentDatatype).toBe(ComponentDatatype.UNSIGNED_BYTE); + expect(attribute.type).toBe(AttributeType.VEC4); + expect(attribute.normalized).toBe(true); + } + + function expectConstantColor(attribute) { + expect(attribute.name).toBe("COLOR"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.COLOR); + expect(attribute.setIndex).toBe(0); + expect(attribute.componentDatatype).toBe(ComponentDatatype.FLOAT); + expect(attribute.type).toBe(AttributeType.VEC4); + expect(attribute.normalized).toBe(false); + expect(attribute.constant).toBeDefined(); + } + + function expectDefaultColor(attribute) { + expect(attribute.name).toBe("COLOR"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.COLOR); + expect(attribute.setIndex).toBe(0); + expect(attribute.componentDatatype).toBe(ComponentDatatype.FLOAT); + expect(attribute.type).toBe(AttributeType.VEC4); + expect(attribute.normalized).toBe(false); + expect(attribute.constant).toEqual(Color.pack(Color.DARKGRAY, [])); + } + + function expectNormal(attribute) { + expect(attribute.name).toBe("NORMAL"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.NORMAL); + expect(attribute.componentDatatype).toBe(ComponentDatatype.FLOAT); + expect(attribute.type).toBe(AttributeType.VEC3); + expect(attribute.normalized).toBe(false); + expect(attribute.quantization).not.toBeDefined(); + } + + function expectNormalOctEncoded(attribute, componentDatatype, isDraco) { + expect(attribute.name).toBe("NORMAL"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.NORMAL); + expect(attribute.componentDatatype).toBe(ComponentDatatype.FLOAT); + expect(attribute.type).toBe(AttributeType.VEC3); + expect(attribute.normalized).toBe(false); + + var quantization = attribute.quantization; + expect(quantization.componentDatatype).toBe(componentDatatype); + expect(quantization.normalizationRange).toBeDefined(); + expect(quantization.octEncoded).toBe(true); + var isZXY = isDraco; + expect(quantization.octEncodedZXY).toBe(isZXY); + expect(quantization.quantizedVolumeDimensions).not.toBeDefined(); + expect(quantization.quantizedVolumeOffset).not.toBeDefined(); + expect(quantization.quantizedVolumeStepSize).not.toBeDefined(); + expect(quantization.type).toBe(AttributeType.VEC2); + } + + function expectBatchId(attribute, componentDatatype) { + expect(attribute.name).toBe("FEATURE_ID"); + expect(attribute.semantic).toBe(VertexAttributeSemantic.FEATURE_ID); + expect(attribute.componentDatatype).toBe(componentDatatype); + expect(attribute.type).toBe(AttributeType.SCALAR); + expect(attribute.normalized).toBe(false); + expect(attribute.quantization).not.toBeDefined(); + } + + it("loads PointCloudRGB", function () { + return loadPnts(pointCloudRGBUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectColorRGB(attributes[1]); + }); + }); + + it("loads PointCloudRGBA", function () { + return loadPnts(pointCloudRGBAUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectColorRGBA(attributes[1]); + }); + }); + + it("loads PointCloudRGB565", function () { + return loadPnts(pointCloudRGB565Url).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectColorRGB565(attributes[1]); + }); + }); + + it("loads PointCloudNoColor", function () { + return loadPnts(pointCloudNoColorUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectDefaultColor(attributes[1]); + }); + }); + + it("loads PointCloudConstantColor", function () { + return loadPnts(pointCloudConstantColorUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectConstantColor(attributes[1]); + }); + }); + + it("loads PointCloudNormals", function () { + return loadPnts(pointCloudNormalsUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(3); + expectPosition(attributes[0]); + expectNormal(attributes[1]); + expectColorRGB(attributes[2]); + }); + }); + + it("loads PointCloudNormalsOctEncoded", function () { + return loadPnts(pointCloudNormalsOctEncodedUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(3); + expectPosition(attributes[0]); + expectNormalOctEncoded( + attributes[1], + ComponentDatatype.UNSIGNED_BYTE, + false + ); + expectColorRGB(attributes[2]); + }); + }); + + it("loads PointCloudQuantized", function () { + return loadPnts(pointCloudQuantizedUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPositionQuantized(attributes[0], ComponentDatatype.UNSIGNED_BYTE); + expectColorRGB(attributes[1]); + }); + }); + + it("loads PointCloudQuantizedOctEncoded", function () { + return loadPnts(pointCloudQuantizedOctEncodedUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(3); + expectPositionQuantized(attributes[0]); + expectNormalOctEncoded( + attributes[1], + ComponentDatatype.UNSIGNED_BYTE, + false + ); + expectColorRGB(attributes[2]); + }); + }); + + it("loads PointCloudDraco", function () { + return loadPnts(pointCloudDracoUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(3); + expectPositionQuantized(attributes[0]); + expectNormalOctEncoded( + attributes[1], + ComponentDatatype.UNSIGNED_BYTE, + true + ); + expectColorRGB(attributes[2]); + }); + }); + + it("loads PointCloudDracoPartial", function () { + return loadPnts(pointCloudDracoPartialUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(3); + expectPositionQuantized(attributes[0]); + expectNormal(attributes[1]); + expectColorRGB(attributes[2]); + }); + }); + + it("loads PointCloudDracoBatched", function () { + return loadPnts(pointCloudDracoBatchedUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectMetadata(components.featureMetadata, { + dimensions: { + type: MetadataType.VEC3, + componentType: MetadataComponentType.FLOAT32, + }, + name: { + type: MetadataType.SINGLE, + isJson: true, + }, + id: { + type: MetadataType.SINGLE, + componentType: MetadataComponentType.UINT32, + }, + }); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(4); + expectPositionQuantized(attributes[0]); + expectNormalOctEncoded( + attributes[1], + ComponentDatatype.UNSIGNED_BYTE, + true + ); + expectColorRGB(attributes[2]); + expectBatchId(attributes[3], ComponentDatatype.UNSIGNED_BYTE); + }); + }); + + it("loads PointCloudWGS84", function () { + return loadPnts(pointCloudWGS84Url).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectEmptyMetadata(components.featureMetadata); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectColorRGB(attributes[1]); + }); + }); + + it("loads PointCloudBatched", function () { + return loadPnts(pointCloudBatchedUrl).then(function (loader) { + var components = loader.components; + expect(components).toBeDefined(); + expectMetadata(components.featureMetadata, { + dimensions: { + type: MetadataType.VEC3, + componentType: MetadataComponentType.FLOAT32, + }, + name: { + type: MetadataType.SINGLE, + isJson: true, + }, + id: { + type: MetadataType.SINGLE, + componentType: MetadataComponentType.UINT32, + }, + }); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(4); + expectPosition(attributes[0]); + expectNormal(attributes[1]); + expectDefaultColor(attributes[2]); + expectBatchId(attributes[3], ComponentDatatype.UNSIGNED_BYTE); + }); + }); + + it("loads PointCloudWithPerPointProperties", function () { + return loadPnts(pointCloudWithPerPointPropertiesUrl).then(function ( + loader + ) { + var components = loader.components; + expect(components).toBeDefined(); + expectMetadata(components.featureMetadata, { + temperature: { + type: MetadataType.SINGLE, + componentType: MetadataComponentType.FLOAT32, + }, + secondaryColor: { + type: MetadataType.VEC3, + componentType: MetadataComponentType.FLOAT32, + }, + id: { + type: MetadataType.SINGLE, + componentType: MetadataComponentType.UINT16, + }, + }); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectColorRGB(attributes[1]); + }); + }); + + it("loads PointCloudWithUnicodePropertyNames", function () { + return loadPnts(pointCloudWithUnicodePropertyNamesUrl).then(function ( + loader + ) { + var components = loader.components; + expect(components).toBeDefined(); + expectMetadata(components.featureMetadata, { + "temperature ℃": { + type: MetadataType.SINGLE, + componentType: MetadataComponentType.FLOAT32, + }, + secondaryColor: { + type: MetadataType.VEC3, + componentType: MetadataComponentType.FLOAT32, + }, + id: { + type: MetadataType.SINGLE, + componentType: MetadataComponentType.UINT16, + }, + }); + + var primitive = components.nodes[0].primitives[0]; + var attributes = primitive.attributes; + expect(attributes.length).toBe(2); + expectPosition(attributes[0]); + expectColorRGB(attributes[1]); + }); + }); + + it("throws with invalid version", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + version: 2, + }); + expectLoadError(arrayBuffer); + }); + + it("throws if featureTableJsonByteLength is 0", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJsonByteLength: 0, + }); + expectLoadError(arrayBuffer); + }); + + it("throws if the feature table does not contain POINTS_LENGTH", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POSITION: { + byteOffset: 0, + }, + }, + }); + expectLoadError(arrayBuffer); + }); + + it("throws if the feature table does not contain POSITION or POSITION_QUANTIZED", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 1, + }, + }); + expectLoadError(arrayBuffer); + }); + + it("throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_SCALE", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 1, + POSITION_QUANTIZED: { + byteOffset: 0, + }, + QUANTIZED_VOLUME_OFFSET: [0.0, 0.0, 0.0], + }, + }); + expectLoadError(arrayBuffer); + }); + + it("throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_OFFSET", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 1, + POSITION_QUANTIZED: { + byteOffset: 0, + }, + QUANTIZED_VOLUME_SCALE: [1.0, 1.0, 1.0], + }, + }); + expectLoadError(arrayBuffer); + }); + + it("throws if the BATCH_ID semantic is defined but BATCH_LENGTH is not", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 2, + POSITION: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + BATCH_ID: [0, 1], + }, + }); + expectLoadError(arrayBuffer); + }); + + it("destroys pnts loader", function () { + return loadPnts(pointCloudBatchedUrl).then(function (loader) { + expect(loader.components).toBeDefined(); + expect(loader.isDestroyed()).toBe(false); + + loader.destroy(); + + expect(loader.components).toBeUndefined(); + expect(loader.isDestroyed()).toBe(true); + }); + }); +}); diff --git a/Specs/Scene/PntsParserSpec.js b/Specs/Scene/PntsParserSpec.js new file mode 100644 index 000000000000..ac446bb4c5bd --- /dev/null +++ b/Specs/Scene/PntsParserSpec.js @@ -0,0 +1,95 @@ +import { PntsParser } from "../../Source/Cesium.js"; +import Cesium3DTilesTester from "../Cesium3DTilesTester.js"; + +describe("Scene/PntsParser", function () { + it("throws without arrayBuffer", function () { + expect(function () { + return PntsParser.parse(); + }).toThrowDeveloperError(); + }); + + it("throws with invalid version", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + version: 2, + }); + expect(function () { + return PntsParser.parse(arrayBuffer); + }).toThrowRuntimeError(); + }); + + it("throws if featureTableJsonByteLength is 0", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJsonByteLength: 0, + }); + expect(function () { + return PntsParser.parse(arrayBuffer); + }).toThrowRuntimeError(); + }); + + it("throws if the feature table does not contain POINTS_LENGTH", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POSITION: { + byteOffset: 0, + }, + }, + }); + expect(function () { + return PntsParser.parse(arrayBuffer); + }).toThrowRuntimeError(); + }); + + it("throws if the feature table does not contain POSITION or POSITION_QUANTIZED", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 1, + }, + }); + expect(function () { + return PntsParser.parse(arrayBuffer); + }).toThrowRuntimeError(); + }); + + it("throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_SCALE", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 1, + POSITION_QUANTIZED: { + byteOffset: 0, + }, + QUANTIZED_VOLUME_OFFSET: [0.0, 0.0, 0.0], + }, + }); + expect(function () { + return PntsParser.parse(arrayBuffer); + }).toThrowRuntimeError(); + }); + + it("throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_OFFSET", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 1, + POSITION_QUANTIZED: { + byteOffset: 0, + }, + QUANTIZED_VOLUME_SCALE: [1.0, 1.0, 1.0], + }, + }); + expect(function () { + return PntsParser.parse(arrayBuffer); + }).toThrowRuntimeError(); + }); + + it("throws if the BATCH_ID semantic is defined but BATCH_LENGTH is not", function () { + var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ + featureTableJson: { + POINTS_LENGTH: 2, + POSITION: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + BATCH_ID: [0, 1], + }, + }); + expect(function () { + return PntsParser.parse(arrayBuffer); + }).toThrowRuntimeError(); + }); +}); diff --git a/Specs/Scene/PointCloud3DTileContentSpec.js b/Specs/Scene/PointCloud3DTileContentSpec.js index 3cbaa45d7ca2..5ce36aa1dc8c 100644 --- a/Specs/Scene/PointCloud3DTileContentSpec.js +++ b/Specs/Scene/PointCloud3DTileContentSpec.js @@ -160,7 +160,7 @@ describe( Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts"); }); - it("throws if the BATCH_ID semantic is defined but BATCHES_LENGTH is not", function () { + it("throws if the BATCH_ID semantic is defined but BATCH_LENGTH is not", function () { var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({ featureTableJson: { POINTS_LENGTH: 2,