diff --git a/Source/Scene/GltfLoader.js b/Source/Scene/GltfLoader.js index 7f9d0ad49640..49c0d10443f2 100644 --- a/Source/Scene/GltfLoader.js +++ b/Source/Scene/GltfLoader.js @@ -870,16 +870,20 @@ function finalizeAttribute( } if (loadTypedArray) { - // The accessor's byteOffset and byteStride should be ignored since values - // are tightly packed in a typed array const bufferViewTypedArray = vertexBufferLoader.typedArray; attribute.typedArray = getPackedTypedArray( gltf, accessor, bufferViewTypedArray ); - attribute.byteOffset = 0; - attribute.byteStride = undefined; + + if (!loadBuffer) { + // If the buffer isn't loaded, then the accessor's byteOffset and + // byteStride should be ignored, since values are only available in a + // tightly packed typed array + attribute.byteOffset = 0; + attribute.byteStride = undefined; + } } } @@ -1043,39 +1047,37 @@ function loadInstancedAttribute( InstanceAttributeSemantic, gltfSemantic ); - const modelSemantic = semanticInfo.modelSemantic; const isTransformAttribute = modelSemantic === InstanceAttributeSemantic.TRANSLATION || modelSemantic === InstanceAttributeSemantic.ROTATION || modelSemantic === InstanceAttributeSemantic.SCALE; - const isTranslationAttribute = modelSemantic === InstanceAttributeSemantic.TRANSLATION; - const loadFor2D = - isTranslationAttribute && - loader._loadAttributesFor2D && - !frameState.scene3DOnly; - - // In addition to the loader options, load the attributes as typed arrays if: - // - the instances have rotations, so that instance matrices are computed on the CPU. - // This avoids the expensive quaternion -> rotation matrix conversion in the shader. - // - the translation accessor does not have a min and max, so the values can be used - // for computing an accurate bounding volume. - // - the attributes contain feature IDs, in order to add the instance's feature ID - // to the pick object. - // - translations are required for 2D + // Load the attributes as typed arrays only if: + // - loadAttributesAsTypedArray is true + // - the instances have rotations. This only applies to the transform attributes, + // since The instance matrices are computed on the CPU. This avoids the + // expensive quaternion -> rotation matrix conversion in the shader. // - GPU instancing is not supported. - let loadTypedArray = + const loadAsTypedArrayOnly = loader._loadAttributesAsTypedArray || - ((hasRotation || !hasTranslationMinMax) && isTransformAttribute) || - modelSemantic === InstanceAttributeSemantic.FEATURE_ID || + (hasRotation && isTransformAttribute) || !frameState.context.instancedArrays; - const loadBuffer = !loadTypedArray; - loadTypedArray = loadTypedArray || loadFor2D; + const loadBuffer = !loadAsTypedArrayOnly; + + // Load the translations as a typed array in addition to the buffer if + // - the accessor does not have a min and max. The values will be used + // for computing an accurate bounding volume. + // - the model will be projected to 2D. + const loadFor2D = loader._loadAttributesFor2D && !frameState.scene3DOnly; + const loadTranslationAsTypedArray = + isTranslationAttribute && (!hasTranslationMinMax || loadFor2D); + + const loadTypedArray = loadAsTypedArrayOnly || loadTranslationAsTypedArray; // Don't pass in draco object since instanced attributes can't be draco compressed return loadAttribute( diff --git a/Source/Scene/Model/GeometryPipelineStage.js b/Source/Scene/Model/GeometryPipelineStage.js index f5381e00b8de..856bcbdf524f 100644 --- a/Source/Scene/Model/GeometryPipelineStage.js +++ b/Source/Scene/Model/GeometryPipelineStage.js @@ -327,8 +327,8 @@ function addAttributeToRenderResources( count: attribute.count, componentsPerAttribute: componentsPerAttribute, componentDatatype: ComponentDatatype.FLOAT, // Projected positions will always be floats. - offsetInBytes: attribute.byteOffset, - strideInBytes: attribute.byteStride, + offsetInBytes: 0, + strideInBytes: undefined, normalize: attribute.normalized, }; diff --git a/Source/Scene/Model/I3dmLoader.js b/Source/Scene/Model/I3dmLoader.js index 154ed4e656d0..75d95ebffa09 100644 --- a/Source/Scene/Model/I3dmLoader.js +++ b/Source/Scene/Model/I3dmLoader.js @@ -1,36 +1,40 @@ import AttributeCompression from "../../Core/AttributeCompression.js"; -import Axis from "../Axis.js"; +import BoundingSphere from "../../Core/BoundingSphere.js"; import Cartesian3 from "../../Core/Cartesian3.js"; -import Cesium3DTileFeatureTable from "../Cesium3DTileFeatureTable.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 Ellipsoid from "../../Core/Ellipsoid.js"; -import StructuralMetadata from "../StructuralMetadata.js"; import getStringFromTypedArray from "../../Core/getStringFromTypedArray.js"; -import GltfLoader from "../GltfLoader.js"; -import I3dmParser from "../I3dmParser.js"; import Matrix3 from "../../Core/Matrix3.js"; import Matrix4 from "../../Core/Matrix4.js"; +import Quaternion from "../../Core/Quaternion.js"; +import RuntimeError from "../../Core/RuntimeError.js"; +import Transforms from "../../Core/Transforms.js"; +import Buffer from "../../Renderer/Buffer.js"; +import BufferUsage from "../../Renderer/BufferUsage.js"; +import AttributeType from "../AttributeType.js"; +import Axis from "../Axis.js"; +import Cesium3DTileFeatureTable from "../Cesium3DTileFeatureTable.js"; +import GltfLoader from "../GltfLoader.js"; +import InstanceAttributeSemantic from "../InstanceAttributeSemantic.js"; +import I3dmParser from "../I3dmParser.js"; import MetadataClass from "../MetadataClass.js"; import ModelComponents from "../ModelComponents.js"; import parseBatchTable from "../parseBatchTable.js"; import PropertyTable from "../PropertyTable.js"; -import Quaternion from "../../Core/Quaternion.js"; import ResourceLoader from "../ResourceLoader.js"; -import RuntimeError from "../../Core/RuntimeError.js"; -import Transforms from "../../Core/Transforms.js"; -import InstanceAttributeSemantic from "../InstanceAttributeSemantic.js"; -import AttributeType from "../AttributeType.js"; -import BoundingSphere from "../../Core/BoundingSphere.js"; +import StructuralMetadata from "../StructuralMetadata.js"; const I3dmLoaderState = { - UNLOADED: 0, + NOT_LOADED: 0, LOADING: 1, PROCESSING: 2, - READY: 3, - FAILED: 4, + POST_PROCESSING: 3, + READY: 4, + FAILED: 5, + UNLOADED: 6, }; const Attribute = ModelComponents.Attribute; @@ -107,10 +111,20 @@ function I3dmLoader(options) { this._loadIndicesForWireframe = loadIndicesForWireframe; this._loadPrimitiveOutline = loadPrimitiveOutline; - this._state = I3dmLoaderState.UNLOADED; + this._state = I3dmLoaderState.NOT_LOADED; this._promise = undefined; this._gltfLoader = undefined; + this._gltfLoaderPromise = undefined; + this._process = function (loader, frameState) {}; + this._postProcess = function (loader, frameState) {}; + + // Instanced attributes are initially parsed as typed arrays, but if they + // do not need to be further processed (e.g. turned into transform matrices), + // it is more efficient to turn them into buffers. The I3dmLoader will own the + // resources and store them here. + this._buffers = []; + this._components = undefined; this._transform = Matrix4.IDENTITY; this._batchTable = undefined; @@ -267,34 +281,47 @@ I3dmLoader.prototype.load = function () { this._gltfLoader = gltfLoader; this._state = I3dmLoaderState.LOADING; - const that = this; gltfLoader.load(); - this._promise = gltfLoader.promise - .then(function () { - if (that.isDestroyed()) { - return; - } + const that = this; + const processPromise = new Promise(function (resolve) { + that._process = function (loader, frameState) { + loader._gltfLoader.process(frameState); + }; + + that._postProcess = function (loader, frameState) { + const gltfLoader = loader._gltfLoader; const components = gltfLoader.components; // Combine the RTC_CENTER transform from the i3dm and the CESIUM_RTC // transform from the glTF. In practice CESIUM_RTC is not set for // instanced models but multiply the transforms just in case. components.transform = Matrix4.multiplyTransformation( - that._transform, + loader._transform, components.transform, components.transform ); - createInstances(that, components); - createStructuralMetadata(that, components); - that._components = components; + createInstances(loader, components, frameState); + createStructuralMetadata(loader, components); + loader._components = components; // Now that we have the parsed components, we can release the array buffer - that._arrayBuffer = undefined; + loader._arrayBuffer = undefined; - that._state = I3dmLoaderState.READY; - return that; + loader._state = I3dmLoaderState.READY; + resolve(loader); + }; + }); + + this._promise = gltfLoader.promise + .then(function () { + if (that.isDestroyed()) { + return; + } + that._state = I3dmLoaderState.POST_PROCESSING; + + return processPromise; }) .catch(function (error) { if (that.isDestroyed()) { @@ -324,7 +351,11 @@ I3dmLoader.prototype.process = function (frameState) { } if (this._state === I3dmLoaderState.PROCESSING) { - this._gltfLoader.process(frameState); + this._process(this, frameState); + } + + if (this._state === I3dmLoaderState.POST_PROCESSING) { + this._postProcess(this, frameState); } }; @@ -363,7 +394,7 @@ const positionScratch = new Cartesian3(); const propertyScratch1 = new Array(4); const transformScratch = new Matrix4(); -function createInstances(loader, components) { +function createInstances(loader, components, frameState) { let i; const featureTable = loader._featureTable; const instancesLength = loader._instancesLength; @@ -507,6 +538,7 @@ function createInstances(loader, components) { // Create instances. const instances = new Instances(); instances.transformInWorldSpace = true; + const buffers = loader._buffers; // Create translation vertex attribute. const translationAttribute = new Attribute(); @@ -515,7 +547,24 @@ function createInstances(loader, components) { translationAttribute.componentDatatype = ComponentDatatype.FLOAT; translationAttribute.type = AttributeType.VEC3; translationAttribute.count = instancesLength; + // The min / max values of the translation attribute need to be computed + // by the model pipeline, so so a pointer to the typed array is stored. translationAttribute.typedArray = translationTypedArray; + // If there is no rotation attribute, however, the translations can also be + // loaded as a buffer to prevent additional resource creation in the pipeline. + if (!hasRotation) { + const buffer = Buffer.createVertexBuffer({ + context: frameState.context, + typedArray: translationTypedArray, + usage: BufferUsage.STATIC_DRAW, + }); + // Destruction of resources is handled by I3dmLoader.unload(). + buffer.vertexArrayDestroyable = false; + buffers.push(buffer); + + translationAttribute.buffer = buffer; + } + instances.attributes.push(translationAttribute); // Create rotation vertex attribute. @@ -538,7 +587,23 @@ function createInstances(loader, components) { scaleAttribute.componentDatatype = ComponentDatatype.FLOAT; scaleAttribute.type = AttributeType.VEC3; scaleAttribute.count = instancesLength; - scaleAttribute.typedArray = scaleTypedArray; + if (hasRotation) { + // If rotations are present, all transform attributes are loaded + // as typed arrays to compute transform matrices for the model. + scaleAttribute.typedArray = scaleTypedArray; + } else { + const buffer = Buffer.createVertexBuffer({ + context: frameState.context, + typedArray: translationTypedArray, + usage: BufferUsage.STATIC_DRAW, + }); + // Destruction of resources is handled by I3dmLoader.unload(). + buffer.vertexArrayDestroyable = false; + buffers.push(buffer); + + scaleAttribute.buffer = buffer; + } + instances.attributes.push(scaleAttribute); } @@ -550,7 +615,16 @@ function createInstances(loader, components) { featureIdAttribute.componentDatatype = ComponentDatatype.FLOAT; featureIdAttribute.type = AttributeType.SCALAR; featureIdAttribute.count = instancesLength; - featureIdAttribute.typedArray = featureIdArray; + const buffer = Buffer.createVertexBuffer({ + context: frameState.context, + typedArray: featureIdArray, + usage: BufferUsage.STATIC_DRAW, + }); + // Destruction of resources is handled by I3dmLoader.unload(). + buffer.vertexArrayDestroyable = false; + buffers.push(buffer); + featureIdAttribute.buffer = buffer; + instances.attributes.push(featureIdAttribute); // Create feature ID attribute. @@ -765,12 +839,32 @@ function processScale(featureTable, i, instanceScale) { } } +function unloadBuffers(loader) { + const buffers = loader._buffers; + const length = buffers.length; + for (let i = 0; i < length; i++) { + const buffer = buffers[i]; + if (!buffer.isDestroyed()) { + buffer.destroy(); + } + } + buffers.length = 0; +} + +I3dmLoader.prototype.isUnloaded = function () { + return this._state === I3dmLoaderState.UNLOADED; +}; + I3dmLoader.prototype.unload = function () { if (defined(this._gltfLoader)) { this._gltfLoader.unload(); } + + unloadBuffers(this); + this._components = undefined; this._arrayBuffer = undefined; + this._state = I3dmLoaderState.UNLOADED; }; export default I3dmLoader; diff --git a/Source/Scene/Model/InstancingPipelineStage.js b/Source/Scene/Model/InstancingPipelineStage.js index 4b22ff0b1a95..2ceb444c0925 100644 --- a/Source/Scene/Model/InstancingPipelineStage.js +++ b/Source/Scene/Model/InstancingPipelineStage.js @@ -37,7 +37,7 @@ InstancingPipelineStage.name = "InstancingPipelineStage"; // Helps with debuggin * * * If the scene is in either 2D or CV mode, this stage also: @@ -61,6 +61,8 @@ InstancingPipelineStage.process = function (renderResources, node, frameState) { const model = renderResources.model; const sceneGraph = model.sceneGraph; + const runtimeNode = renderResources.runtimeNode; + const use2D = frameState.mode !== SceneMode.SCENE3D && !frameState.scene3DOnly && @@ -163,7 +165,7 @@ InstancingPipelineStage.process = function (renderResources, node, frameState) { sceneGraph.axisCorrectionMatrix, // This transforms from the node's coordinate system to the root // of the node hierarchy - renderResources.runtimeNode.computedTransform, + runtimeNode.computedTransform, nodeTransformScratch ); }; @@ -184,7 +186,7 @@ InstancingPipelineStage.process = function (renderResources, node, frameState) { const context = frameState.context; const modelMatrix2D = Matrix4.fromTranslation( - renderResources.instancingReferencePoint2D, + runtimeNode.instancingReferencePoint2D, new Matrix4() ); @@ -333,7 +335,9 @@ function projectTransformsTo2D( nodeComputedTransform ); - const referencePoint = renderResources.instancingReferencePoint2D; + const runtimeNode = renderResources.runtimeNode; + const referencePoint = runtimeNode.instancingReferencePoint2D; + const count = transforms.length; for (let i = 0; i < count; i++) { const transform = transforms[i]; @@ -382,7 +386,8 @@ function projectTranslationsTo2D( nodeComputedTransform ); - const referencePoint = renderResources.instancingReferencePoint2D; + const runtimeNode = renderResources.runtimeNode; + const referencePoint = runtimeNode.instancingReferencePoint2D; const count = translations.length; for (let i = 0; i < count; i++) { const translation = translations[i]; @@ -411,10 +416,11 @@ const scratchProjectedMax = new Cartesian3(); function computeReferencePoint2D(renderResources, frameState) { // Compute the reference point by averaging the instancing translation // min / max values after they are projected to 2D. + const runtimeNode = renderResources.runtimeNode; const modelMatrix = renderResources.model.sceneGraph.computedModelMatrix; const transformedPositionMin = Matrix4.multiplyByPoint( modelMatrix, - renderResources.instancingTranslationMin, + runtimeNode.instancingTranslationMin, scratchProjectedMin ); @@ -426,7 +432,7 @@ function computeReferencePoint2D(renderResources, frameState) { const transformedPositionMax = Matrix4.multiplyByPoint( modelMatrix, - renderResources.instancingTranslationMax, + runtimeNode.instancingTranslationMax, scratchProjectedMax ); @@ -436,7 +442,12 @@ function computeReferencePoint2D(renderResources, frameState) { transformedPositionMax ); - return Cartesian3.lerp(projectedMin, projectedMax, 0.5, new Cartesian3()); + runtimeNode.instancingReferencePoint2D = Cartesian3.lerp( + projectedMin, + projectedMax, + 0.5, + new Cartesian3() + ); } function transformsToTypedArray(transforms) { @@ -521,10 +532,13 @@ function getInstanceTransformsAsMatrices(instances, count, renderResources) { const translationTypedArray = hasTranslation ? translationAttribute.typedArray : new Float32Array(count * 3); - // Rotations get initialized to (0, 0, 0, 0). The w-component is set to 1 in the loop below. + + // Rotations get initialized to (0, 0, 0, 0). + // The w-component is set to 1 in the loop below. const rotationTypedArray = hasRotation ? rotationAttribute.typedArray : new Float32Array(count * 4); + // Scales get initialized to (1, 1, 1). let scaleTypedArray; if (hasScale) { @@ -578,25 +592,74 @@ function getInstanceTransformsAsMatrices(instances, count, renderResources) { transforms[i] = transform; } - renderResources.instancingTranslationMax = instancingTranslationMax; - renderResources.instancingTranslationMin = instancingTranslationMin; + const runtimeNode = renderResources.runtimeNode; + runtimeNode.instancingTranslationMin = instancingTranslationMin; + runtimeNode.instancingTranslationMax = instancingTranslationMax; + + // Unload the typed arrays. These are just pointers to the arrays + // in the vertex buffer loader. + if (hasTranslation) { + translationAttribute.typedArray = undefined; + } + if (hasRotation) { + rotationAttribute.typedArray = undefined; + } + if (hasScale) { + scaleAttribute.typedArray = undefined; + } return transforms; } -function getInstanceTranslationsAsCartesian3s(translationAttribute, count) { - const translations = new Array(count); +function getInstanceTranslationsAsCartesian3s( + translationAttribute, + count, + renderResources +) { + const instancingTranslations = new Array(count); const translationTypedArray = translationAttribute.typedArray; + const instancingTranslationMin = new Cartesian3( + Number.MAX_VALUE, + Number.MAX_VALUE, + Number.MAX_VALUE + ); + const instancingTranslationMax = new Cartesian3( + -Number.MAX_VALUE, + -Number.MAX_VALUE, + -Number.MAX_VALUE + ); + for (let i = 0; i < count; i++) { - translations[i] = new Cartesian3( + const translation = new Cartesian3( translationTypedArray[i * 3], translationTypedArray[i * 3 + 1], translationTypedArray[i * 3 + 2] ); + + instancingTranslations[i] = translation; + + Cartesian3.minimumByComponent( + instancingTranslationMin, + translation, + instancingTranslationMin + ); + Cartesian3.maximumByComponent( + instancingTranslationMax, + translation, + instancingTranslationMax + ); } - return translations; + const runtimeNode = renderResources.runtimeNode; + runtimeNode.instancingTranslationMin = instancingTranslationMin; + runtimeNode.instancingTranslationMax = instancingTranslationMax; + + // Unload the typed array. This is just a pointer to the array + // in the vertex buffer loader. + translationAttribute.typedArray = undefined; + + return instancingTranslations; } function createVertexBuffer(typedArray, frameState) { @@ -620,43 +683,52 @@ function processTransformAttributes( instancingVertexAttributes, use2D ) { - const translationAttribute = ModelUtility.getAttributeBySemantic( - instances, - InstanceAttributeSemantic.TRANSLATION - ); - - let translationMax; - let translationMin; - if (defined(translationAttribute)) { - translationMax = translationAttribute.max; - translationMin = translationAttribute.min; - } - const rotationAttribute = ModelUtility.getAttributeBySemantic( instances, InstanceAttributeSemantic.ROTATION ); + // Only use matrices for the transforms if the rotation attribute is defined. + if (defined(rotationAttribute)) { + processTransformMatrixAttributes( + renderResources, + instances, + instancingVertexAttributes, + frameState, + use2D + ); + } else { + processTransformVec3Attributes( + renderResources, + instances, + instancingVertexAttributes, + frameState, + use2D + ); + } +} + +function processTransformMatrixAttributes( + renderResources, + instances, + instancingVertexAttributes, + frameState, + use2D +) { const shaderBuilder = renderResources.shaderBuilder; const count = instances.attributes[0].count; - const useMatrices = - defined(rotationAttribute) || - !defined(translationMax) || - !defined(translationMin); - const statistics = renderResources.model.statistics; + const model = renderResources.model; + const runtimeNode = renderResources.runtimeNode; - // Packed typed arrays are omitted from statistics because they don't - // necessarily correspond to the size of the GPU buffer containing - // their data. It's also difficult to track which typed arrays have - // already been counted. - const hasCpuCopy = false; + shaderBuilder.addDefine("HAS_INSTANCE_MATRICES"); + const attributeString = "Transform"; let transforms; - if (useMatrices) { - shaderBuilder.addDefine("HAS_INSTANCE_MATRICES"); - const attributeString = "Transform"; - + let buffer = runtimeNode.instancingTransformsBuffer; + if (!defined(buffer)) { + // This function computes the transforms, sets the translation min / max, + // and unloads the typed arrays associated with the attributes. transforms = getInstanceTransformsAsMatrices( instances, count, @@ -664,98 +736,126 @@ function processTransformAttributes( ); const transformsTypedArray = transformsToTypedArray(transforms); - const buffer = createVertexBuffer(transformsTypedArray, frameState); - renderResources.model._pipelineResources.push(buffer); + buffer = createVertexBuffer(transformsTypedArray, frameState); + model._modelResources.push(buffer); - processMatrixAttributes( - renderResources, - buffer, - instancingVertexAttributes, - attributeString - ); - - // Count the buffer here since it had to be allocated - // in this stage. - statistics.addBuffer(buffer, hasCpuCopy); - } else { - if (defined(translationAttribute)) { - shaderBuilder.addDefine("HAS_INSTANCE_TRANSLATION"); - - const translationMax = translationAttribute.max; - const translationMin = translationAttribute.min; - renderResources.instancingTranslationMax = translationMax; - renderResources.instancingTranslationMin = translationMin; - - let buffer = translationAttribute.buffer; - let byteOffset = translationAttribute.byteOffset; - let byteStride = translationAttribute.byteStride; - - if (!defined(buffer)) { - buffer = createVertexBuffer( - translationAttribute.typedArray, - frameState - ); - renderResources.model._pipelineResources.push(buffer); + runtimeNode.instancingTransformsBuffer = buffer; + } - byteOffset = 0; - byteStride = undefined; + processMatrixAttributes( + renderResources, + buffer, + instancingVertexAttributes, + attributeString + ); - // Count the buffer here if it had to be allocated - // in this stage. Otherwise, it will be counted in - // NodeStatisticsPipelineStage. - statistics.addBuffer(buffer, hasCpuCopy); - } + if (!use2D) { + return; + } - const attributeString = "Translation"; + // Force the scene mode to be CV. In 2D, projected positions will have + // an x-coordinate of 0, which eliminates the height data that is + // necessary for rendering in CV mode. + const frameStateCV = clone(frameState); + frameStateCV.mode = SceneMode.COLUMBUS_VIEW; - processVec3Attribute( - renderResources, - buffer, - byteOffset, - byteStride, - instancingVertexAttributes, - attributeString - ); - } + // To prevent jitter, the positions are defined relative to a common + // reference point. For convenience, this is the center of the instanced + // translation bounds projected to 2D. + computeReferencePoint2D(renderResources, frameStateCV); - const scaleAttribute = ModelUtility.getAttributeBySemantic( - instances, - InstanceAttributeSemantic.SCALE + let buffer2D = runtimeNode.instancingTransformsBuffer2D; + if (!defined(buffer2D)) { + const projectedTransforms = projectTransformsTo2D( + transforms, + renderResources, + frameStateCV, + transforms ); + const projectedTypedArray = transformsToTypedArray(projectedTransforms); - if (defined(scaleAttribute)) { - shaderBuilder.addDefine("HAS_INSTANCE_SCALE"); + // This memory is counted during the statistics stage at the end + // of the pipeline. + buffer2D = createVertexBuffer(projectedTypedArray, frameState); + model._modelResources.push(buffer2D); - let buffer = scaleAttribute.buffer; - let byteOffset = scaleAttribute.byteOffset; - let byteStride = scaleAttribute.byteStride; + runtimeNode.instancingTransformsBuffer2D = buffer2D; + } - if (!defined(buffer)) { - buffer = createVertexBuffer(scaleAttribute.typedArray, frameState); - renderResources.model._pipelineResources.push(buffer); + const attributeString2D = "Transform2D"; + processMatrixAttributes( + renderResources, + buffer2D, + instancingVertexAttributes, + attributeString2D + ); +} - byteOffset = 0; - byteStride = undefined; +function processTransformVec3Attributes( + renderResources, + instances, + instancingVertexAttributes, + frameState, + use2D +) { + const shaderBuilder = renderResources.shaderBuilder; + const runtimeNode = renderResources.runtimeNode; + const translationAttribute = ModelUtility.getAttributeBySemantic( + instances, + InstanceAttributeSemantic.TRANSLATION + ); + const scaleAttribute = ModelUtility.getAttributeBySemantic( + instances, + InstanceAttributeSemantic.SCALE + ); - // Count the buffer here if it had to be allocated - // in this stage. Otherwise, it will be counted in - // NodeStatisticsPipelineStage. - statistics.addBuffer(buffer, hasCpuCopy); - } + if (defined(scaleAttribute)) { + shaderBuilder.addDefine("HAS_INSTANCE_SCALE"); + const attributeString = "Scale"; - const attributeString = "Scale"; + // Instanced scale attributes are loaded as buffers only. + processVec3Attribute( + renderResources, + scaleAttribute.buffer, + scaleAttribute.byteOffset, + scaleAttribute.byteStride, + instancingVertexAttributes, + attributeString + ); + } - processVec3Attribute( - renderResources, - buffer, - byteOffset, - byteStride, - instancingVertexAttributes, - attributeString - ); - } + if (!defined(translationAttribute)) { + return; } + let instancingTranslations; + const typedArray = translationAttribute.typedArray; + if (defined(typedArray)) { + // This function computes and set the translation min / max, and unloads + // the typed array associated with the attribute. + // The translations are also returned in case they're used for 2D projection. + instancingTranslations = getInstanceTranslationsAsCartesian3s( + translationAttribute, + translationAttribute.count, + renderResources + ); + } else if (!defined(runtimeNode.instancingTranslationMin)) { + runtimeNode.instancingTranslationMin = translationAttribute.min; + runtimeNode.instancingTranslationMax = translationAttribute.max; + } + + shaderBuilder.addDefine("HAS_INSTANCE_TRANSLATION"); + const attributeString = "Translation"; + + processVec3Attribute( + renderResources, + translationAttribute.buffer, + translationAttribute.byteOffset, + translationAttribute.byteStride, + instancingVertexAttributes, + attributeString + ); + if (!use2D) { return; } @@ -769,76 +869,39 @@ function processTransformAttributes( // To prevent jitter, the positions are defined relative to a common // reference point. For convenience, this is the center of the instanced // translation bounds projected to 2D. - const referencePoint = computeReferencePoint2D(renderResources, frameStateCV); - renderResources.instancingReferencePoint2D = referencePoint; - - const runtimeNode = renderResources.runtimeNode; - - if (useMatrices) { - let buffer = runtimeNode.instancingTransformsBuffer2D; - if (!defined(buffer)) { - const projectedTransforms = projectTransformsTo2D( - transforms, - renderResources, - frameStateCV, - transforms - ); - const projectedTypedArray = transformsToTypedArray(projectedTransforms); + computeReferencePoint2D(renderResources, frameStateCV); - // This memory is counted during the statistics stage at the end - // of the pipeline. - buffer = createVertexBuffer(projectedTypedArray, frameState); - renderResources.model._modelResources.push(buffer); - - runtimeNode.instancingTransformsBuffer2D = buffer; - } + let buffer2D = runtimeNode.instancingTranslationBuffer2D; - const attributeString2D = "Transform2D"; - processMatrixAttributes( + if (!defined(buffer2D)) { + const projectedTranslations = projectTranslationsTo2D( + instancingTranslations, renderResources, - buffer, - instancingVertexAttributes, - attributeString2D + frameStateCV, + instancingTranslations ); - } else { - let buffer = runtimeNode.instancingTranslationBuffer2D; - - if (!defined(buffer)) { - const translations = getInstanceTranslationsAsCartesian3s( - translationAttribute, - count - ); - const projectedTranslations = projectTranslationsTo2D( - translations, - renderResources, - frameStateCV, - translations - ); - const projectedTypedArray = translationsToTypedArray( - projectedTranslations - ); + const projectedTypedArray = translationsToTypedArray(projectedTranslations); - // This memory is counted during the statistics stage at the end - // of the pipeline. - buffer = createVertexBuffer(projectedTypedArray, frameState); - renderResources.model._modelResources.push(buffer); + // This memory is counted during the statistics stage at the end + // of the pipeline. + buffer2D = createVertexBuffer(projectedTypedArray, frameState); + renderResources.model._modelResources.push(buffer2D); - runtimeNode.instancingTranslationBuffer2D = buffer; - } + runtimeNode.instancingTranslationBuffer2D = buffer2D; + } - const byteOffset = 0; - const byteStride = undefined; + const byteOffset = 0; + const byteStride = undefined; - const attributeString2D = "Translation2D"; - processVec3Attribute( - renderResources, - buffer, - byteOffset, - byteStride, - instancingVertexAttributes, - attributeString2D - ); - } + const attributeString2D = "Translation2D"; + processVec3Attribute( + renderResources, + buffer2D, + byteOffset, + byteStride, + instancingVertexAttributes, + attributeString2D + ); } function processMatrixAttributes( @@ -927,7 +990,6 @@ function processFeatureIdAttributes( instancingVertexAttributes ) { const attributes = instances.attributes; - const model = renderResources.model; const shaderBuilder = renderResources.shaderBuilder; // Load Feature ID vertex attributes. These are loaded as typed arrays in GltfLoader @@ -944,24 +1006,9 @@ function processFeatureIdAttributes( renderResources.featureIdVertexAttributeSetIndex = attribute.setIndex + 1; } - const vertexBuffer = Buffer.createVertexBuffer({ - context: frameState.context, - typedArray: attribute.typedArray, - usage: BufferUsage.STATIC_DRAW, - }); - vertexBuffer.vertexArrayDestroyable = false; - model._pipelineResources.push(vertexBuffer); - - // Packed typed arrays are omitted from statistics because they don't - // necessarily correspond to the size of the GPU buffer containing - // their data. It's also difficult to track which typed arrays have - // already been counted. - const hasCpuCopy = false; - model.statistics.addBuffer(vertexBuffer, hasCpuCopy); - instancingVertexAttributes.push({ index: renderResources.attributeIndex++, - vertexBuffer: vertexBuffer, + vertexBuffer: attribute.buffer, componentsPerAttribute: AttributeType.getNumberOfComponents( attribute.type ), diff --git a/Source/Scene/Model/ModelRuntimeNode.js b/Source/Scene/Model/ModelRuntimeNode.js index 83820e3f9778..41f6caccb59f 100644 --- a/Source/Scene/Model/ModelRuntimeNode.js +++ b/Source/Scene/Model/ModelRuntimeNode.js @@ -112,17 +112,49 @@ function ModelRuntimeNode(options) { /** * Update stages to apply to this node. * + * @type {Object[]} + * @readonly + * * @private */ this.updateStages = []; + /** + * The component-wise minimum value of the translations of the instances. + * This value is set by InstancingPipelineStage. + * + * @type {Cartesian3} + * + * @private + */ + this.instancingTranslationMin = undefined; + + /** + * The component-wise maximum value of the translations of the instances. + * This value is set by InstancingPipelineStage. + * + * @type {Cartesian3} + * + * @private + */ + this.instancingTranslationMax = undefined; + + /** + * A buffer containing the instanced transforms. The memory is managed + * by Model; this is just a reference. + * + * @type {Buffer} + * + * @private + */ + this.instancingTransformsBuffer = undefined; + /** * A buffer containing the instanced transforms projected to 2D world * coordinates. Used for rendering in 2D / CV mode. The memory is managed * by Model; this is just a reference. * * @type {Buffer} - * @readonly * * @private */ @@ -134,12 +166,25 @@ function ModelRuntimeNode(options) { * managed by Model; this is just a reference. * * @type {Buffer} - * @readonly * * @private */ this.instancingTranslationBuffer2D = undefined; + /** + * If the model is instanced and projected to 2D, the reference point is the + * average of the instancing translation max and min. The 2D translations are + * defined relative to this point to avoid precision issues on the GPU. + *

+ * This value is set by InstancingPipelineStage. + *

+ * + * @type {Cartesian3} + * + * @private + */ + this.instancingReferencePoint2D = undefined; + initialize(this); } diff --git a/Source/Scene/Model/NodeRenderResources.js b/Source/Scene/Model/NodeRenderResources.js index b71c3ed029af..d81c9600f5d6 100644 --- a/Source/Scene/Model/NodeRenderResources.js +++ b/Source/Scene/Model/NodeRenderResources.js @@ -155,40 +155,6 @@ function NodeRenderResources(modelRenderResources, runtimeNode) { * @private */ this.instanceCount = 0; - - /** - * The component-wise maximum value of the translations of the instances. - * This value is set by InstancingPipelineStage. - * - * @type {Cartesian3} - * - * @private - */ - this.instancingTranslationMax = undefined; - - /** - * The component-wise minimum value of the translations of the instances. - * This value is set by InstancingPipelineStage. - * - * @type {Cartesian3} - * - * @private - */ - this.instancingTranslationMin = undefined; - - /** - * If the model is instanced and projected to 2D, the reference point is the - * average of the instancing translation max and min. The 2D translations are - * defined relative to this point to avoid precision issues on the GPU. - *

- * This value is set by InstancingPipelineStage. - *

- * - * @type {Cartesian3} - * - * @private - */ - this.instancingReferencePoint2D = undefined; } export default NodeRenderResources; diff --git a/Source/Scene/Model/NodeStatisticsPipelineStage.js b/Source/Scene/Model/NodeStatisticsPipelineStage.js index ef2db0902085..40bfe89ca3ad 100644 --- a/Source/Scene/Model/NodeStatisticsPipelineStage.js +++ b/Source/Scene/Model/NodeStatisticsPipelineStage.js @@ -1,12 +1,12 @@ import defined from "../../Core/defined.js"; /** - * The node statistics update stage updates memory usage statistics for - * Model on the node level. This counts the binary resources - * that exist for the lifetime of the Model (e.g. attributes - * loaded by GltfLoader). It does not count resources that are created - * every time the pipeline is run. The individual pipeline stages are - * responsible for keeping track of additional memory they allocate. + * The node statistics update stage updates memory usage statistics for a Model + * on the node level. This counts the binary resources that exist for the + * lifetime of the Model (e.g. attributes loaded by GltfLoader). It does not + * count resources that are created every time the pipeline is run. + * The individual pipeline stages are responsible for keeping track of any + * additional memory they allocate. * * @namespace NodeStatisticsPipelineStage * @@ -20,12 +20,12 @@ NodeStatisticsPipelineStage.process = function ( node, frameState ) { - const model = renderResources.model; - const statistics = model.statistics; + const statistics = renderResources.model.statistics; + const instances = node.instances; const runtimeNode = renderResources.runtimeNode; - countInstancingAttributes(statistics, node.instances); - countInstancing2DBuffers(statistics, runtimeNode); + countInstancingAttributes(statistics, instances); + countGeneratedBuffers(statistics, runtimeNode); }; function countInstancingAttributes(statistics, instances) { @@ -38,24 +38,30 @@ function countInstancingAttributes(statistics, instances) { for (let i = 0; i < length; i++) { const attribute = attributes[i]; if (defined(attribute.buffer)) { - // Packed typed arrays are not counted + // Any typed arrays should have been unloaded before this stage. const hasCpuCopy = false; statistics.addBuffer(attribute.buffer, hasCpuCopy); } } } -function countInstancing2DBuffers(statistics, runtimeNode) { +function countGeneratedBuffers(statistics, runtimeNode) { + if (defined(runtimeNode.instancingTransformsBuffer)) { + // The typed array containing the computed transforms isn't saved + // after the buffer is created. + const hasCpuCopy = false; + statistics.addBuffer(runtimeNode.instancingTransformsBuffer, hasCpuCopy); + } if (defined(runtimeNode.instancingTransformsBuffer2D)) { - // The typed array containing the computed 2D transforms - // isn't saved after the buffer is created. + // The typed array containing the computed 2D transforms isn't saved + // after the buffer is created. const hasCpuCopy = false; statistics.addBuffer(runtimeNode.instancingTransformsBuffer2D, hasCpuCopy); } if (defined(runtimeNode.instancingTranslationBuffer2D)) { - // The typed array containing the computed 2D translations - // isn't saved after the buffer is created. + // The typed array containing the computed 2D translations isn't saved + // after the buffer is created. const hasCpuCopy = false; statistics.addBuffer(runtimeNode.instancingTranslationBuffer2D, hasCpuCopy); } @@ -63,6 +69,6 @@ function countInstancing2DBuffers(statistics, runtimeNode) { // Exposed for testing NodeStatisticsPipelineStage._countInstancingAttributes = countInstancingAttributes; -NodeStatisticsPipelineStage._countInstancing2DBuffers = countInstancing2DBuffers; +NodeStatisticsPipelineStage._countGeneratedBuffers = countGeneratedBuffers; export default NodeStatisticsPipelineStage; diff --git a/Source/Scene/Model/PrimitiveRenderResources.js b/Source/Scene/Model/PrimitiveRenderResources.js index e27fdf3cdaaf..5a0d8d1cceb9 100644 --- a/Source/Scene/Model/PrimitiveRenderResources.js +++ b/Source/Scene/Model/PrimitiveRenderResources.js @@ -234,8 +234,8 @@ function PrimitiveRenderResources(nodeRenderResources, runtimePrimitive) { const positionMinMax = ModelUtility.getPositionMinMax( primitive, - nodeRenderResources.instancingTranslationMin, - nodeRenderResources.instancingTranslationMax + this.runtimeNode.instancingTranslationMin, + this.runtimeNode.instancingTranslationMax ); /** @@ -273,7 +273,7 @@ function PrimitiveRenderResources(nodeRenderResources, runtimePrimitive) { ); /** - * Options for configuring the lighting stage such as selecting between + * Options for configuring the lighting stage, such as selecting between * unlit and PBR shading. * * @type {ModelLightingOptions} diff --git a/Specs/Scene/GltfLoaderSpec.js b/Specs/Scene/GltfLoaderSpec.js index 758175ae577f..61170d5b31ae 100644 --- a/Specs/Scene/GltfLoaderSpec.js +++ b/Specs/Scene/GltfLoaderSpec.js @@ -2165,9 +2165,24 @@ describe( }); }); - it("loads BoxInstanced", function () { - return loadGltf(boxInstanced).then(function (gltfLoader) { - const components = gltfLoader.components; + describe("loads instanced models", function () { + let sceneWithNoInstancing; + + beforeAll(function () { + // Disable instancing extension. + sceneWithNoInstancing = createScene(); + sceneWithNoInstancing.context._instancedArrays = undefined; + }); + + function verifyBoxInstancedAttributes(loader, options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + const interleaved = defaultValue(options.interleaved, false); + const instancingDisabled = defaultValue( + options.instancingDisabled, + false + ); + + const components = loader.components; const scene = components.scene; const rootNode = scene.nodes[0]; const primitive = rootNode.primitives[0]; @@ -2180,7 +2195,6 @@ describe( attributes, VertexAttributeSemantic.NORMAL ); - const structuralMetadata = components.structuralMetadata; const instances = rootNode.instances; const instancedAttributes = instances.attributes; const translationAttribute = getAttribute( @@ -2283,10 +2297,37 @@ describe( expect(featureIdAttribute.max).toBeUndefined(); expect(featureIdAttribute.constant).toBe(0); expect(featureIdAttribute.quantization).toBeUndefined(); - expect(featureIdAttribute.typedArray).toBeDefined(); - expect(featureIdAttribute.buffer).toBeUndefined(); - expect(featureIdAttribute.byteOffset).toBe(0); - expect(rotationAttribute.byteStride).toBeUndefined(); + // The feature IDs should only be loaded as a typed array + // if instancing is disabled. + if (instancingDisabled) { + expect(featureIdAttribute.typedArray).toEqual( + new Float32Array([0, 0, 1, 1]) + ); + expect(featureIdAttribute.buffer).toBeUndefined(); + } else { + expect(featureIdAttribute.typedArray).toBeUndefined(); + expect(featureIdAttribute.buffer).toBeDefined(); + } + + if (interleaved && !instancingDisabled) { + expect(featureIdAttribute.byteOffset).toBe(40); + expect(featureIdAttribute.byteStride).toBe(44); + } else if (instancingDisabled) { + // Feature IDs are available in a packed array. + expect(featureIdAttribute.byteOffset).toBe(0); + expect(featureIdAttribute.byteStride).toBeUndefined(); + } else { + expect(featureIdAttribute.byteOffset).toBe(0); + expect(featureIdAttribute.byteStride).toBe(4); + } + } + + function verifyBoxInstancedStructuralMetadata(loader) { + const components = loader.components; + const structuralMetadata = components.structuralMetadata; + const scene = components.scene; + const rootNode = scene.nodes[0]; + const instances = rootNode.instances; expect(instances.featureIds.length).toBe(2); @@ -2357,131 +2398,14 @@ describe( expect(sectionTable.getProperty(0, "id")).toBe(10293); expect(sectionTable.getProperty(1, "name")).toBe("right"); expect(sectionTable.getProperty(1, "id")).toBe(54923); - }); - }); + } - it("loads BoxInstanced with EXT_feature_metadata", function () { - return loadGltf(boxInstancedLegacy).then(function (gltfLoader) { - const components = gltfLoader.components; + function verifyBoxInstancedStructuralMetadataLegacy(loader) { + const components = loader.components; + const structuralMetadata = components.structuralMetadata; const scene = components.scene; const rootNode = scene.nodes[0]; - const primitive = rootNode.primitives[0]; - const attributes = primitive.attributes; - const positionAttribute = getAttribute( - attributes, - VertexAttributeSemantic.POSITION - ); - const normalAttribute = getAttribute( - attributes, - VertexAttributeSemantic.NORMAL - ); - const structuralMetadata = components.structuralMetadata; const instances = rootNode.instances; - const instancedAttributes = instances.attributes; - const translationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.TRANSLATION - ); - const rotationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.ROTATION - ); - const scaleAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.SCALE - ); - const featureIdAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.FEATURE_ID, - 0 - ); - - expect(positionAttribute).toBeDefined(); - expect(normalAttribute).toBeDefined(); - - expect(translationAttribute.semantic).toBe( - InstanceAttributeSemantic.TRANSLATION - ); - expect(translationAttribute.componentDatatype).toBe( - ComponentDatatype.FLOAT - ); - expect(translationAttribute.type).toBe(AttributeType.VEC3); - expect(translationAttribute.normalized).toBe(false); - expect(translationAttribute.count).toBe(4); - expect(translationAttribute.min).toBeUndefined(); - expect(translationAttribute.max).toBeUndefined(); - expect(translationAttribute.constant).toEqual(Cartesian3.ZERO); - expect(translationAttribute.quantization).toBeUndefined(); - expect(translationAttribute.typedArray).toEqual( - new Float32Array([-2, 2, 0, -2, -2, 0, 2, -2, 0, 2, 2, 0]) - ); - expect(translationAttribute.buffer).toBeUndefined(); - expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBeUndefined(); - - expect(rotationAttribute.semantic).toBe( - InstanceAttributeSemantic.ROTATION - ); - expect(rotationAttribute.componentDatatype).toBe( - ComponentDatatype.FLOAT - ); - expect(rotationAttribute.type).toBe(AttributeType.VEC4); - expect(rotationAttribute.normalized).toBe(false); - expect(rotationAttribute.count).toBe(4); - expect(rotationAttribute.min).toBeUndefined(); - expect(rotationAttribute.max).toBeUndefined(); - expect(rotationAttribute.constant).toEqual(Cartesian4.ZERO); - expect(rotationAttribute.quantization).toBeUndefined(); - expect(rotationAttribute.typedArray).toEqual( - // prettier-ignore - new Float32Array([ - 0.3826833963394165, 0, 0, 0.9238795042037964, - 0.3535534143447876, 0.3535534143447876, 0.1464466005563736, 0.8535534143447876, - 0.46193981170654297, 0.19134169816970825, 0.46193981170654297, 0.7325378060340881, - 0.5319756865501404, 0.022260000929236412, 0.43967971205711365, 0.7233173847198486, - ]) - ); - expect(rotationAttribute.buffer).toBeUndefined(); - expect(rotationAttribute.byteOffset).toBe(0); - expect(rotationAttribute.byteStride).toBeUndefined(); - - expect(scaleAttribute.semantic).toBe(InstanceAttributeSemantic.SCALE); - expect(scaleAttribute.componentDatatype).toBe(ComponentDatatype.FLOAT); - expect(scaleAttribute.type).toBe(AttributeType.VEC3); - expect(scaleAttribute.normalized).toBe(false); - expect(scaleAttribute.count).toBe(4); - expect(scaleAttribute.min).toBeUndefined(); - expect(scaleAttribute.max).toBeUndefined(); - expect(scaleAttribute.constant).toEqual(Cartesian3.ZERO); - expect(scaleAttribute.quantization).toBeUndefined(); - expect(scaleAttribute.typedArray).toEqual( - // prettier-ignore - new Float32Array([ - 0.6000000238418579, 0.699999988079071, 1, - 1, 1, 0.5, - 0.75, 0.20000000298023224, 0.5, - 0.800000011920929, 0.6000000238418579, 0.8999999761581421, - ]) - ); - expect(scaleAttribute.buffer).toBeUndefined(); - expect(scaleAttribute.byteOffset).toBe(0); - expect(scaleAttribute.byteStride).toBeUndefined(); - - expect(featureIdAttribute.setIndex).toBe(0); - expect(featureIdAttribute.componentDatatype).toBe( - ComponentDatatype.FLOAT - ); - expect(featureIdAttribute.type).toBe(AttributeType.SCALAR); - expect(featureIdAttribute.normalized).toBe(false); - expect(featureIdAttribute.count).toBe(4); - expect(featureIdAttribute.min).toBeUndefined(); - expect(featureIdAttribute.max).toBeUndefined(); - expect(featureIdAttribute.constant).toBe(0); - expect(featureIdAttribute.quantization).toBeUndefined(); - expect(featureIdAttribute.typedArray).toBeDefined(); - expect(featureIdAttribute.buffer).toBeUndefined(); - expect(featureIdAttribute.byteOffset).toBe(0); - expect(rotationAttribute.byteStride).toBeUndefined(); expect(instances.featureIds.length).toBe(2); @@ -2550,107 +2474,46 @@ describe( expect(sectionTable.getProperty(0, "id")).toBe(10293); expect(sectionTable.getProperty(1, "name")).toBe("right"); expect(sectionTable.getProperty(1, "id")).toBe(54923); - }); - }); - - it("loads BoxInstanced when WebGL instancing is disabled", function () { - // Disable extension - const instancedArrays = scene.context._instancedArrays; - scene.context._instancedArrays = undefined; - - return loadGltf(boxInstanced) - .then(function (gltfLoader) { - const components = gltfLoader.components; - const scene = components.scene; - const rootNode = scene.nodes[0]; - const primitive = rootNode.primitives[0]; - const attributes = primitive.attributes; - const positionAttribute = getAttribute( - attributes, - VertexAttributeSemantic.POSITION - ); - const normalAttribute = getAttribute( - attributes, - VertexAttributeSemantic.NORMAL - ); - const instances = rootNode.instances; - const instancedAttributes = instances.attributes; - const translationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.TRANSLATION - ); - const rotationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.ROTATION - ); - const scaleAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.SCALE - ); - const featureIdAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.FEATURE_ID, - 0 - ); - - expect(positionAttribute).toBeDefined(); - expect(normalAttribute).toBeDefined(); + } - expect(translationAttribute.typedArray).toEqual( - new Float32Array([-2, 2, 0, -2, -2, 0, 2, -2, 0, 2, 2, 0]) - ); - expect(translationAttribute.buffer).toBeUndefined(); - expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBeUndefined(); + it("loads BoxInstanced", function () { + return loadGltf(boxInstanced).then(function (gltfLoader) { + verifyBoxInstancedAttributes(gltfLoader); + verifyBoxInstancedStructuralMetadata(gltfLoader); + }); + }); - expect(rotationAttribute.typedArray).toEqual( - // prettier-ignore - new Float32Array([ - 0.3826833963394165, 0, 0, 0.9238795042037964, - 0.3535534143447876, 0.3535534143447876, 0.1464466005563736, 0.8535534143447876, - 0.46193981170654297, 0.19134169816970825, 0.46193981170654297, 0.7325378060340881, - 0.5319756865501404, 0.022260000929236412, 0.43967971205711365, 0.7233173847198486, - ]) - ); - expect(rotationAttribute.buffer).toBeUndefined(); - expect(rotationAttribute.byteOffset).toBe(0); - expect(rotationAttribute.byteStride).toBeUndefined(); - - expect(scaleAttribute.typedArray).toEqual( - // prettier-ignore - new Float32Array([ - 0.6000000238418579, 0.699999988079071, 1, - 1, 1, 0.5, - 0.75, 0.20000000298023224, 0.5, - 0.800000011920929, 0.6000000238418579, 0.8999999761581421, - ]) - ); - expect(scaleAttribute.buffer).toBeUndefined(); - expect(scaleAttribute.byteOffset).toBe(0); - expect(scaleAttribute.byteStride).toBeUndefined(); + it("loads BoxInstanced with EXT_feature_metadata", function () { + return loadGltf(boxInstancedLegacy).then(function (gltfLoader) { + verifyBoxInstancedAttributes(gltfLoader); + verifyBoxInstancedStructuralMetadataLegacy(gltfLoader); + }); + }); - expect(featureIdAttribute.typedArray).toEqual( - new Float32Array([0, 0, 1, 1]) - ); - expect(featureIdAttribute.buffer).toBeUndefined(); - expect(featureIdAttribute.byteOffset).toBe(0); - expect(featureIdAttribute.byteStride).toBeUndefined(); - }) - .finally(function () { - // Re-enable extension - scene.context._instancedArrays = instancedArrays; + it("loads BoxInstanced when WebGL instancing is disabled", function () { + const options = { + scene: sceneWithNoInstancing, + }; + return loadGltf(boxInstanced, options).then(function (gltfLoader) { + verifyBoxInstancedAttributes(gltfLoader, { + instancingDisabled: true, + }); + verifyBoxInstancedStructuralMetadata(gltfLoader); }); - }); + }); - it("loads BoxInstanced with default feature ids", function () { - function modifyGltf(gltf) { - // Delete feature ID accessor's buffer view - delete gltf.accessors[6].bufferView; - return gltf; - } + it("loads BoxInstanced with default feature ids", function () { + function modifyGltf(gltf) { + // Delete feature ID accessor's buffer view + delete gltf.accessors[6].bufferView; + return gltf; + } - return loadModifiedGltfAndTest(boxInstanced, undefined, modifyGltf).then( - function (gltfLoader) { + return loadModifiedGltfAndTest( + boxInstanced, + undefined, + modifyGltf + ).then(function (gltfLoader) { const components = gltfLoader.components; const scene = components.scene; const rootNode = scene.nodes[0]; @@ -2665,151 +2528,38 @@ describe( expect(featureIdAttribute.buffer).toBeUndefined(); expect(featureIdAttribute.typedArray).toBeUndefined(); expect(featureIdAttribute.constant).toEqual(0.0); - } - ); - }); - - it("loads BoxInstancedInterleaved", function () { - // Disable extension - const instancedArrays = scene.context._instancedArrays; - scene.context._instancedArrays = undefined; - - return loadGltf(boxInstancedInterleaved) - .then(function (gltfLoader) { - const components = gltfLoader.components; - const scene = components.scene; - const rootNode = scene.nodes[0]; - const primitive = rootNode.primitives[0]; - const attributes = primitive.attributes; - const positionAttribute = getAttribute( - attributes, - VertexAttributeSemantic.POSITION - ); - const normalAttribute = getAttribute( - attributes, - VertexAttributeSemantic.NORMAL - ); - const instances = rootNode.instances; - const instancedAttributes = instances.attributes; - const translationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.TRANSLATION - ); - const rotationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.ROTATION - ); - const scaleAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.SCALE - ); - const featureIdAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.FEATURE_ID, - 0 - ); - - expect(positionAttribute).toBeDefined(); - expect(normalAttribute).toBeDefined(); - - expect(translationAttribute.typedArray).toEqual( - new Float32Array([-2, 2, 0, -2, -2, 0, 2, -2, 0, 2, 2, 0]) - ); - expect(translationAttribute.buffer).toBeUndefined(); - expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBeUndefined(); - - expect(rotationAttribute.typedArray).toEqual( - // prettier-ignore - new Float32Array([ - 0.3826833963394165, 0, 0, 0.9238795042037964, - 0.3535534143447876, 0.3535534143447876, 0.1464466005563736, 0.8535534143447876, - 0.46193981170654297, 0.19134169816970825, 0.46193981170654297, 0.7325378060340881, - 0.5319756865501404, 0.022260000929236412, 0.43967971205711365, 0.7233173847198486, - ]) - ); - expect(rotationAttribute.buffer).toBeUndefined(); - expect(rotationAttribute.byteOffset).toBe(0); - expect(rotationAttribute.byteStride).toBeUndefined(); - - expect(scaleAttribute.typedArray).toEqual( - // prettier-ignore - new Float32Array([ - 0.6000000238418579, 0.699999988079071, 1, - 1, 1, 0.5, - 0.75, 0.20000000298023224, 0.5, - 0.800000011920929, 0.6000000238418579, 0.8999999761581421, - ]) - ); - expect(scaleAttribute.buffer).toBeUndefined(); - expect(scaleAttribute.byteOffset).toBe(0); - expect(scaleAttribute.byteStride).toBeUndefined(); - - expect(featureIdAttribute.typedArray).toEqual( - new Float32Array([0, 0, 1, 1]) - ); - expect(featureIdAttribute.buffer).toBeUndefined(); - expect(featureIdAttribute.byteOffset).toBe(0); - expect(featureIdAttribute.byteStride).toBeUndefined(); - }) - .finally(function () { - // Re-enable extension - scene.context._instancedArrays = instancedArrays; }); - }); - - it("loads BoxInstancedTranslation", function () { - return loadGltf(boxInstancedTranslation).then(function (gltfLoader) { - const components = gltfLoader.components; - const scene = components.scene; - const rootNode = scene.nodes[0]; - const primitive = rootNode.primitives[0]; - const attributes = primitive.attributes; - const positionAttribute = getAttribute( - attributes, - VertexAttributeSemantic.POSITION - ); - const normalAttribute = getAttribute( - attributes, - VertexAttributeSemantic.NORMAL - ); - const instances = rootNode.instances; - const instancedAttributes = instances.attributes; - const translationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.TRANSLATION - ); + }); - expect(positionAttribute).toBeDefined(); - expect(normalAttribute).toBeDefined(); + it("loads BoxInstancedInterleaved", function () { + return loadGltf(boxInstancedInterleaved).then(function (gltfLoader) { + verifyBoxInstancedAttributes(gltfLoader, { + interleaved: true, + }); + }); + }); - expect(translationAttribute.semantic).toBe( - InstanceAttributeSemantic.TRANSLATION - ); - expect(translationAttribute.componentDatatype).toBe( - ComponentDatatype.FLOAT - ); - expect(translationAttribute.type).toBe(AttributeType.VEC3); - expect(translationAttribute.normalized).toBe(false); - expect(translationAttribute.count).toBe(4); - expect(translationAttribute.min).toBeUndefined(); - expect(translationAttribute.max).toBeUndefined(); - expect(translationAttribute.constant).toEqual(Cartesian3.ZERO); - expect(translationAttribute.quantization).toBeUndefined(); - expect(translationAttribute.typedArray).toEqual( - new Float32Array([-2, 2, 0, -2, -2, 0, 2, -2, 0, 2, 2, 0]) - ); - expect(translationAttribute.buffer).toBeUndefined(); - expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBeUndefined(); + it("loads BoxInstancedInterleaved with instancing disabled", function () { + const options = { + scene: sceneWithNoInstancing, + }; + return loadGltf(boxInstancedInterleaved, options).then(function ( + gltfLoader + ) { + verifyBoxInstancedAttributes(gltfLoader, { + interleaved: true, + instancingDisabled: true, + }); + }); }); - }); - it("loads BoxInstancedTranslationWithMinMax", function () { - return loadGltf(boxInstancedTranslationMinMax).then(function ( - gltfLoader + function verifyBoxInstancedTranslation( + loader, + expectMinMax, + expectBufferDefined, + expectTypedArrayDefined ) { - const components = gltfLoader.components; + const components = loader.components; const scene = components.scene; const rootNode = scene.nodes[0]; const primitive = rootNode.primitives[0]; @@ -2841,104 +2591,93 @@ describe( expect(translationAttribute.type).toBe(AttributeType.VEC3); expect(translationAttribute.normalized).toBe(false); expect(translationAttribute.count).toBe(4); - expect(translationAttribute.min).toEqual(new Cartesian3(-2, -2, 0)); - expect(translationAttribute.max).toEqual(new Cartesian3(2, 2, 0)); - expect(translationAttribute.constant).toEqual(Cartesian3.ZERO); - expect(translationAttribute.quantization).toBeUndefined(); - expect(translationAttribute.typedArray).toBeUndefined(); - expect(translationAttribute.buffer).toBeDefined(); - expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBe(12); - }); - }); - - it("loads BoxInstancedTranslation when WebGL instancing is disabled", function () { - // Disable extension - const instancedArrays = scene.context._instancedArrays; - scene.context._instancedArrays = undefined; - return loadGltf(boxInstancedTranslation) - .then(function (gltfLoader) { - const components = gltfLoader.components; - const scene = components.scene; - const rootNode = scene.nodes[0]; - const primitive = rootNode.primitives[0]; - const attributes = primitive.attributes; - const positionAttribute = getAttribute( - attributes, - VertexAttributeSemantic.POSITION - ); - const normalAttribute = getAttribute( - attributes, - VertexAttributeSemantic.NORMAL - ); - const instances = rootNode.instances; - const instancedAttributes = instances.attributes; - const translationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.TRANSLATION - ); + if (expectMinMax) { + expect(translationAttribute.min).toEqual(new Cartesian3(-2, -2, 0)); + expect(translationAttribute.max).toEqual(new Cartesian3(2, 2, 0)); + } else { + expect(translationAttribute.min).toBeUndefined(); + expect(translationAttribute.max).toBeUndefined(); + } - expect(positionAttribute).toBeDefined(); - expect(normalAttribute).toBeDefined(); + expect(translationAttribute.constant).toEqual(Cartesian3.ZERO); + expect(translationAttribute.quantization).toBeUndefined(); + if (expectTypedArrayDefined) { expect(translationAttribute.typedArray).toEqual( new Float32Array([-2, 2, 0, -2, -2, 0, 2, -2, 0, 2, 2, 0]) ); + } else { + expect(translationAttribute.typedArray).toBeUndefined(); + expect(translationAttribute.byteOffset).toBe(0); + } + + if (expectBufferDefined) { + expect(translationAttribute.buffer).toBeDefined(); + expect(translationAttribute.byteOffset).toBe(0); + expect(translationAttribute.byteStride).toBe(12); + } else { expect(translationAttribute.buffer).toBeUndefined(); + // Byte stride is undefined for typed arrays. expect(translationAttribute.byteOffset).toBe(0); expect(translationAttribute.byteStride).toBeUndefined(); - }) - .finally(function () { - // Re-enable extension - scene.context._instancedArrays = instancedArrays; + } + } + + it("loads BoxInstancedTranslation", function () { + return loadGltf(boxInstancedTranslation).then(function (gltfLoader) { + // The translation accessor does not have a min/max, so it must load + // the typed array in addition to the buffer. + const expectMinMax = false; + const expectBufferDefined = true; + const expectTypedArrayDefined = true; + + verifyBoxInstancedTranslation( + gltfLoader, + expectMinMax, + expectBufferDefined, + expectTypedArrayDefined + ); }); - }); + }); - it("loads BoxInstancedTranslationWithMinMax for 2D", function () { - return loadGltf(boxInstancedTranslationMinMax, { - loadAttributesFor2D: true, - }).then(function (gltfLoader) { - const components = gltfLoader.components; - const scene = components.scene; - const rootNode = scene.nodes[0]; - const primitive = rootNode.primitives[0]; - const attributes = primitive.attributes; - const positionAttribute = getAttribute( - attributes, - VertexAttributeSemantic.POSITION - ); - const normalAttribute = getAttribute( - attributes, - VertexAttributeSemantic.NORMAL - ); - const instances = rootNode.instances; - const instancedAttributes = instances.attributes; - const translationAttribute = getAttribute( - instancedAttributes, - InstanceAttributeSemantic.TRANSLATION - ); + it("loads BoxInstancedTranslation when WebGL instancing is disabled", function () { + const options = { + scene: sceneWithNoInstancing, + }; + return loadGltf(boxInstancedTranslation, options).then(function ( + gltfLoader + ) { + const expectMinMax = false; + const expectBufferDefined = false; + const expectTypedArrayDefined = true; - expect(positionAttribute).toBeDefined(); - expect(normalAttribute).toBeDefined(); + verifyBoxInstancedTranslation( + gltfLoader, + expectMinMax, + expectBufferDefined, + expectTypedArrayDefined + ); + }); + }); - expect(translationAttribute.semantic).toBe( - InstanceAttributeSemantic.TRANSLATION - ); - expect(translationAttribute.componentDatatype).toBe( - ComponentDatatype.FLOAT - ); - expect(translationAttribute.type).toBe(AttributeType.VEC3); - expect(translationAttribute.normalized).toBe(false); - expect(translationAttribute.count).toBe(4); - expect(translationAttribute.min).toEqual(new Cartesian3(-2, -2, 0)); - expect(translationAttribute.max).toEqual(new Cartesian3(2, 2, 0)); - expect(translationAttribute.constant).toEqual(Cartesian3.ZERO); - expect(translationAttribute.quantization).toBeUndefined(); - expect(translationAttribute.typedArray).toBeDefined(); - expect(translationAttribute.buffer).toBeDefined(); - expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBe(undefined); + it("loads BoxInstancedTranslationWithMinMax", function () { + return loadGltf(boxInstancedTranslationMinMax).then(function ( + gltfLoader + ) { + // The translation accessor does have a min/max, so it only needs to + // load the buffer. + const expectMinMax = true; + const expectBufferDefined = true; + const expectTypedArrayDefined = false; + + verifyBoxInstancedTranslation( + gltfLoader, + expectMinMax, + expectBufferDefined, + expectTypedArrayDefined + ); + }); }); }); @@ -3744,8 +3483,8 @@ describe( expect(positionAttribute.buffer).toBeDefined(); expect(positionAttribute.typedArray).toBeDefined(); - expect(positionAttribute.byteOffset).toBe(0); - expect(positionAttribute.byteStride).toBeUndefined(); + expect(positionAttribute.byteOffset).toBe(12); + expect(positionAttribute.byteStride).toBe(24); // Typed arrays of other attributes should not be defined expect(normalAttribute.buffer).toBeDefined(); @@ -3821,8 +3560,8 @@ describe( InstanceAttributeSemantic.FEATURE_ID, 0 ); - expect(featureIdAttribute.typedArray).toBeDefined(); - expect(featureIdAttribute.buffer).toBeUndefined(); + expect(featureIdAttribute.typedArray).toBeUndefined(); + expect(featureIdAttribute.buffer).toBeDefined(); }); }); @@ -3835,8 +3574,8 @@ describe( gltfLoader ) { // Since the translation attribute has no min / max readily defined, - // it will load in as a typed array to find these bounds at runtime. - // This ensures no additional buffers are created for 2D. + // it will load in as a typed array in addition to a buffer in order + // to find these bounds at runtime. const components = gltfLoader.components; const scene = components.scene; const rootNode = scene.nodes[0]; @@ -3872,9 +3611,9 @@ describe( expect(translationAttribute.constant).toEqual(Cartesian3.ZERO); expect(translationAttribute.quantization).toBeUndefined(); expect(translationAttribute.typedArray).toBeDefined(); - expect(translationAttribute.buffer).toBeUndefined(); + expect(translationAttribute.buffer).toBeDefined(); expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBeUndefined(); + expect(translationAttribute.byteStride).toBe(12); }); }); @@ -3886,11 +3625,8 @@ describe( return loadGltf(boxInstancedTranslationMinMax, options).then(function ( gltfLoader ) { - // Since the only instanced attribute is translation, and since its - // min / max is defined, this will be loaded as a buffer normally - // because it doesn't need further processing with a typed array. - // However, typed arrays are necessary for 2D projection, so this - // should load both a buffer and a typed array for the attribute. + // Typed arrays are necessary for 2D projection, so this should load + // both a buffer and a typed array for the attribute. const components = gltfLoader.components; const scene = components.scene; const rootNode = scene.nodes[0]; @@ -3930,7 +3666,7 @@ describe( expect(translationAttribute.typedArray).toBeDefined(); expect(translationAttribute.buffer).toBeDefined(); expect(translationAttribute.byteOffset).toBe(0); - expect(translationAttribute.byteStride).toBeUndefined(); + expect(translationAttribute.byteStride).toBe(12); }); }); }); diff --git a/Specs/Scene/Model/GeometryPipelineStageSpec.js b/Specs/Scene/Model/GeometryPipelineStageSpec.js index b479d4da0719..435d4d008363 100644 --- a/Specs/Scene/Model/GeometryPipelineStageSpec.js +++ b/Specs/Scene/Model/GeometryPipelineStageSpec.js @@ -383,8 +383,8 @@ describe( expect(position2DAttribute.componentDatatype).toEqual( ComponentDatatype.FLOAT ); - expect(position2DAttribute.offsetInBytes).toBe(288); - expect(position2DAttribute.strideInBytes).toBe(12); + expect(position2DAttribute.offsetInBytes).toBe(0); + expect(position2DAttribute.strideInBytes).toBeUndefined(); const texCoord0Attribute = attributes[3]; expect(texCoord0Attribute.index).toEqual(3); diff --git a/Specs/Scene/Model/I3dmLoaderSpec.js b/Specs/Scene/Model/I3dmLoaderSpec.js index 0daacf5d279f..5ae48ca0cb71 100644 --- a/Specs/Scene/Model/I3dmLoaderSpec.js +++ b/Specs/Scene/Model/I3dmLoaderSpec.js @@ -101,14 +101,26 @@ describe( }).toThrowError(RuntimeError); } - function verifyInstances(components, expectedSemantics, instancesLength) { + function verifyInstances(loader, expectedSemantics, instancesLength) { + const components = loader.components; + const structuralMetadata = components.structuralMetadata; + expect(structuralMetadata).toBeDefined(); + + let bufferCount = 0; for (let i = 0; i < components.nodes.length; i++) { const node = components.nodes[i]; - // Every node that has a primitive should have an ModelComponents.Instances object. + + // Every node that has a primitive should have a + // ModelComponents.Instances object. if (node.primitives.length > 0) { - expect(node.instances).toBeDefined(); - const attributesLength = node.instances.attributes.length; + const instances = node.instances; + expect(instances).toBeDefined(); + const attributesLength = instances.attributes.length; expect(attributesLength).toEqual(expectedSemantics.length); + + const hasRotation = + expectedSemantics.indexOf(InstanceAttributeSemantic.ROTATION) >= 0; + // Iterate through the attributes of the node with instances and check for all expected semantics. for (let j = 0; j < attributesLength; j++) { const attribute = node.instances.attributes[j]; @@ -116,9 +128,32 @@ describe( true ); expect(attribute.count).toEqual(instancesLength); + + const isTransformAttribute = + attribute.semantic === InstanceAttributeSemantic.TRANSLATION || + attribute.semantic === InstanceAttributeSemantic.ROTATION || + attribute.semantic === InstanceAttributeSemantic.SCALE; + + const isTranslationAttribute = + attribute.semantic === InstanceAttributeSemantic.TRANSLATION; + + if (hasRotation && isTransformAttribute) { + expect(attribute.typedArray).toBeDefined(); + expect(attribute.buffer).toBeUndefined(); + } else if (isTranslationAttribute) { + expect(attribute.typedArray).toBeDefined(); + expect(attribute.buffer).toBeDefined(); + bufferCount++; + } else { + expect(attribute.typedArray).toBeUndefined(); + expect(attribute.buffer).toBeDefined(); + bufferCount++; + } } } } + + expect(loader._buffers.length).toEqual(bufferCount); } it("releases array buffer when finished loading", function () { @@ -130,13 +165,8 @@ describe( it("loads InstancedGltfExternalUrl", function () { return loadI3dm(instancedGltfExternalUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); - verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -149,13 +179,8 @@ describe( it("loads InstancedWithBatchTableUrl", function () { return loadI3dm(instancedWithBatchTableUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); - verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -168,13 +193,8 @@ describe( it("loads InstancedWithBatchTableBinaryUrl", function () { return loadI3dm(instancedWithBatchTableBinaryUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); - verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -187,13 +207,8 @@ describe( it("loads InstancedWithoutBatchTableUrl", function () { return loadI3dm(instancedWithoutBatchTableUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); - verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -206,12 +221,8 @@ describe( it("loads InstancedOrientationUrl", function () { return loadI3dm(instancedOrientationUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -224,12 +235,8 @@ describe( it("loads InstancedOct32POrientationUrl", function () { return loadI3dm(instancedOct32POrientationUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -242,12 +249,8 @@ describe( it("loads InstancedScaleUrl", function () { return loadI3dm(instancedScaleUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -261,12 +264,8 @@ describe( it("loads InstancedScaleNonUniformUrl", function () { return loadI3dm(instancedScaleNonUniformUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -280,12 +279,8 @@ describe( it("loads InstancedRTCUrl", function () { return loadI3dm(instancedRTCUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -307,12 +302,8 @@ describe( it("loads InstancedQuantizedUrl", function () { return loadI3dm(instancedQuantizedUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -320,9 +311,11 @@ describe( ], 25 ); + + const transform = loader.components.transform; // The transform is computed from the quantized positions // prettier-ignore - expect(components.transform).toEqualEpsilon(new Matrix4( + expect(transform).toEqualEpsilon(new Matrix4( 1, 0, 0, 1215013.8125, 0, 1, 0, -4736316.75, 0, 0, 1, 4081608.5, @@ -335,12 +328,8 @@ describe( return loadI3dm(instancedQuantizedOct32POrientationUrl).then(function ( loader ) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -353,12 +342,8 @@ describe( it("loads InstancedWithTransformUrl", function () { return loadI3dm(instancedWithTransformUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.FEATURE_ID, @@ -370,12 +355,8 @@ describe( it("loads InstancedWithBatchIdsUrl", function () { return loadI3dm(instancedWithBatchIdsUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -388,12 +369,8 @@ describe( it("loads InstancedTexturedUrl", function () { return loadI3dm(instancedTexturedUrl).then(function (loader) { - const components = loader.components; - const structuralMetadata = components.structuralMetadata; - - expect(structuralMetadata).toBeDefined(); verifyInstances( - components, + loader, [ InstanceAttributeSemantic.TRANSLATION, InstanceAttributeSemantic.ROTATION, @@ -404,6 +381,19 @@ describe( }); }); + it("destroys buffers when unloaded", function () { + return loadI3dm(instancedGltfExternalUrl).then(function (loader) { + // This i3dm has a rotation attribute, so only the feature IDs + // are loaded in a buffer. + const buffers = loader._buffers; + expect(buffers.length).toBe(1); + + const buffer = buffers[0]; + loader.destroy(); + expect(buffer.isDestroyed()).toBe(true); + }); + }); + it("throws with invalid format", function () { const arrayBuffer = Cesium3DTilesTester.generateInstancedTileBuffer({ gltfFormat: 2, diff --git a/Specs/Scene/Model/InstancingPipelineStageSpec.js b/Specs/Scene/Model/InstancingPipelineStageSpec.js index 6d8e225f2756..4503b6ba712b 100644 --- a/Specs/Scene/Model/InstancingPipelineStageSpec.js +++ b/Specs/Scene/Model/InstancingPipelineStageSpec.js @@ -4,6 +4,7 @@ import { combine, GltfLoader, I3dmLoader, + InstanceAttributeSemantic, InstancingPipelineStage, Matrix4, Math as CesiumMath, @@ -39,9 +40,11 @@ describe( beforeAll(function () { scene = createScene(); + scene.renderForSpecs(); scene2D = createScene(); scene2D.morphTo2D(0.0); + scene2D.renderForSpecs(); }); afterAll(function () { @@ -61,7 +64,7 @@ describe( ResourceCache.clearForSpecs(); }); - function mockRenderResources() { + function mockRenderResources(node) { return { attributeIndex: 1, attributes: [], @@ -73,11 +76,13 @@ describe( _pipelineResources: [], statistics: new ModelStatistics(), }, - runtimeNode: {}, + runtimeNode: { + node: node, + }, }; } - function mockRenderResourcesFor2D() { + function mockRenderResourcesFor2D(node, components) { return { attributeIndex: 1, attributes: [], @@ -90,12 +95,14 @@ describe( statistics: new ModelStatistics(), _projectTo2D: true, sceneGraph: { + components: components, computedModelMatrix: Matrix4.IDENTITY, axisCorrectionMatrix: Matrix4.IDENTITY, }, }, runtimeNode: { computedTransform: Matrix4.IDENTITY, + node: node, }, }; } @@ -143,38 +150,61 @@ describe( }); } - it("correctly computes instancing TRANSLATION min and max from typed arrays", function () { - const renderResources = mockRenderResources(); + function verifyTypedArraysUnloaded(instances) { + const attributes = instances.attributes; + const length = attributes.length; + for (let i = 0; i < length; i++) { + const attribute = attributes[i]; + expect(attribute.typedArray).toBeUndefined(); + } + } + it("computes instancing TRANSLATION min and max from typed arrays", function () { return loadGltf(boxInstanced).then(function (gltfLoader) { const components = gltfLoader.components; const node = components.nodes[0]; - scene.renderForSpecs(); + const renderResources = mockRenderResources(node); InstancingPipelineStage.process( renderResources, node, scene.frameState ); - expect(renderResources.instancingTranslationMax).toEqual( + expect(renderResources.attributes.length).toBe(4); + + const runtimeNode = renderResources.runtimeNode; + expect(runtimeNode.instancingTranslationMin).toEqual( + new Cartesian3(-2, -2, 0) + ); + expect(runtimeNode.instancingTranslationMax).toEqual( new Cartesian3(2, 2, 0) ); - expect(renderResources.instancingTranslationMin).toEqual( + + // Ensure that the max / min are only computed once by checking if + // they are still defined after the stage is re-run. + InstancingPipelineStage.process( + renderResources, + node, + scene.frameState + ); + + expect(runtimeNode.instancingTranslationMin).toEqual( new Cartesian3(-2, -2, 0) ); - expect(renderResources.attributes.length).toBe(4); + expect(runtimeNode.instancingTranslationMax).toEqual( + new Cartesian3(2, 2, 0) + ); }); }); it("sets instancing TRANSLATION min and max from attributes", function () { - const renderResources = mockRenderResources(); - return loadGltf(boxInstancedTranslationMinMax).then(function ( gltfLoader ) { const components = gltfLoader.components; const node = components.nodes[0]; + const renderResources = mockRenderResources(node); InstancingPipelineStage.process( renderResources, @@ -182,70 +212,39 @@ describe( scene.frameState ); - expect(renderResources.instancingTranslationMax).toEqual( + expect(renderResources.attributes.length).toBe(1); + + const runtimeNode = renderResources.runtimeNode; + expect(runtimeNode.instancingTranslationMax).toEqual( new Cartesian3(2, 2, 0) ); - expect(renderResources.instancingTranslationMin).toEqual( + expect(runtimeNode.instancingTranslationMin).toEqual( new Cartesian3(-2, -2, 0) ); - expect(renderResources.attributes.length).toBe(1); - }); - }); - - it("creates instancing matrices vertex attributes when ROTATION is present", function () { - const renderResources = mockRenderResources(); - - return loadGltf(boxInstanced).then(function (gltfLoader) { - const components = gltfLoader.components; - const node = components.nodes[0]; - scene.renderForSpecs(); + // Ensure that the max / min are still defined after the stage is re-run. InstancingPipelineStage.process( renderResources, node, scene.frameState ); - expect(renderResources.attributes.length).toBe(4); - - const shaderBuilder = renderResources.shaderBuilder; - ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, [ - "HAS_INSTANCING", - "HAS_INSTANCE_MATRICES", - ]); - ShaderBuilderTester.expectHasFragmentDefines(shaderBuilder, [ - "HAS_INSTANCING", - "HAS_INSTANCE_MATRICES", - ]); - ShaderBuilderTester.expectHasAttributes(shaderBuilder, undefined, [ - "attribute vec4 a_instancingTransformRow0;", - "attribute vec4 a_instancingTransformRow1;", - "attribute vec4 a_instancingTransformRow2;", - "attribute float a_instanceFeatureId_0;", - ]); - - // The model has feature IDs, so a resource will also be created - // for those. - expect(renderResources.model._pipelineResources.length).toEqual(2); - expect(renderResources.model._modelResources.length).toEqual(0); - - // Matrices are stored as 3 vec4s, so this is - // 4 matrices * 12 floats/matrix * 4 bytes/float = 192 - const matrixSize = 192; - // 4 floats - const featureIdSize = 16; - expect(renderResources.model.statistics.geometryByteLength).toBe( - matrixSize + featureIdSize + expect(runtimeNode.instancingTranslationMin).toEqual( + new Cartesian3(-2, -2, 0) + ); + expect(runtimeNode.instancingTranslationMax).toEqual( + new Cartesian3(2, 2, 0) ); }); }); - it("creates instance matrices vertex attributes when TRANSLATION min and max are not present", function () { - const renderResources = mockRenderResources(); - - return loadGltf(boxInstancedTranslation).then(function (gltfLoader) { + it("creates instancing matrices vertex attributes when ROTATION is present", function () { + return loadGltf(boxInstanced).then(function (gltfLoader) { const components = gltfLoader.components; const node = components.nodes[0]; + const instances = node.instances; + const renderResources = mockRenderResources(node); + const runtimeNode = renderResources.runtimeNode; scene.renderForSpecs(); InstancingPipelineStage.process( @@ -254,7 +253,7 @@ describe( scene.frameState ); - expect(renderResources.attributes.length).toBe(3); + expect(renderResources.attributes.length).toBe(4); const shaderBuilder = renderResources.shaderBuilder; ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, [ @@ -269,31 +268,26 @@ describe( "attribute vec4 a_instancingTransformRow0;", "attribute vec4 a_instancingTransformRow1;", "attribute vec4 a_instancingTransformRow2;", + "attribute float a_instanceFeatureId_0;", ]); - expect(renderResources.model._pipelineResources.length).toEqual(1); - expect(renderResources.model._modelResources.length).toEqual(0); + expect(runtimeNode.instancingTransformsBuffer).toBeDefined(); + verifyTypedArraysUnloaded(instances); - // Matrices are stored as 3 vec4s, so this is - // 4 matrices * 12 floats/matrix * 4 bytes/float = 192 - const matrixSize = 192; - const featureIdSize = 0; - expect(renderResources.model.statistics.geometryByteLength).toBe( - matrixSize + featureIdSize - ); + // A resource will be created for the computed matrix transforms. + expect(renderResources.model._modelResources.length).toEqual(1); + // The resource will be counted by NodeStatisticsPipelineStage. + expect(renderResources.model.statistics.geometryByteLength).toBe(0); }); }); it("creates instancing matrices vertex attributes for 2D", function () { - const renderResources = mockRenderResourcesFor2D(); return loadGltf(boxInstanced, { loadAttributesFor2D: true, }).then(function (gltfLoader) { const components = gltfLoader.components; - renderResources.model.sceneGraph.components = components; const node = components.nodes[0]; - const runtimeNode = renderResources.runtimeNode; - runtimeNode.node = node; + const renderResources = mockRenderResourcesFor2D(node, components); scene2D.renderForSpecs(); InstancingPipelineStage.process( @@ -329,9 +323,13 @@ describe( "uniform mat4 u_modelView2D;", ]); - expect(renderResources.instancingReferencePoint2D).toBeDefined(); + const runtimeNode = renderResources.runtimeNode; + expect(runtimeNode.instancingTransformsBuffer).toBeDefined(); + expect(runtimeNode.instancingTransformsBuffer2D).toBeDefined(); + expect(runtimeNode.instancingReferencePoint2D).toBeDefined(); + const translationMatrix = Matrix4.fromTranslation( - renderResources.instancingReferencePoint2D, + runtimeNode.instancingReferencePoint2D, scratchMatrix4 ); const expectedMatrix = Matrix4.multiplyTransformation( @@ -342,26 +340,19 @@ describe( const uniformMap = renderResources.uniformMap; expect(uniformMap.u_modelView2D()).toEqual(expectedMatrix); - expect(runtimeNode.instancingTransformsBuffer2D).toBeDefined(); - expect(renderResources.model._pipelineResources.length).toEqual(2); - expect(renderResources.model._modelResources.length).toEqual(1); + expect(renderResources.model._pipelineResources.length).toEqual(0); + expect(renderResources.model._modelResources.length).toEqual(2); - // The 2D buffer will be counted by NodeStatisticsPipelineStage, - // so the memory counted here should stay the same. - const matrixSize = 192; - const featureIdSize = 16; - expect(renderResources.model.statistics.geometryByteLength).toBe( - matrixSize + featureIdSize - ); + // The 2D buffer will be counted by NodeStatisticsPipelineStage. + expect(renderResources.model.statistics.geometryByteLength).toBe(0); }); }); it("correctly creates transform matrices", function () { - const renderResources = mockRenderResources(); - return loadGltf(boxInstanced).then(function (gltfLoader) { const components = gltfLoader.components; const node = components.nodes[0]; + const renderResources = mockRenderResources(node); const expectedTransformsTypedArray = new Float32Array([ 0.5999999642372131, @@ -434,14 +425,13 @@ describe( }); }); - it("creates TRANSLATION vertex attributes", function () { - const renderResources = mockRenderResources(); - + it("creates TRANSLATION vertex attributes with min/max present", function () { return loadGltf(boxInstancedTranslationMinMax).then(function ( gltfLoader ) { const components = gltfLoader.components; const node = components.nodes[0]; + const renderResources = mockRenderResources(node); scene.renderForSpecs(); InstancingPipelineStage.process( @@ -466,26 +456,76 @@ describe( "attribute vec3 a_instanceTranslation;", ]); - // No additional buffer was created + // No additional buffer was created. expect(renderResources.model._pipelineResources.length).toEqual(0); expect(renderResources.model._modelResources.length).toEqual(0); // Attributes with buffers already loaded in will be counted - // in NodeStatisticsPipelineStage + // in NodeStatisticsPipelineStage. + expect(renderResources.model.statistics.geometryByteLength).toBe(0); + }); + }); + + it("creates TRANSLATION vertex attributes without min/max present", function () { + return loadGltf(boxInstancedTranslation).then(function (gltfLoader) { + const components = gltfLoader.components; + const node = components.nodes[0]; + const renderResources = mockRenderResources(node); + const instances = node.instances; + + scene.renderForSpecs(); + InstancingPipelineStage.process( + renderResources, + node, + scene.frameState + ); + + expect(renderResources.attributes.length).toBe(1); + + const translationAttribute = ModelUtility.getAttributeBySemantic( + instances, + InstanceAttributeSemantic.TRANSLATION + ); + // Expect the typed array to be unloaded. + expect(translationAttribute.typedArray).toBeUndefined(); + + const shaderBuilder = renderResources.shaderBuilder; + ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, [ + "HAS_INSTANCING", + "HAS_INSTANCE_TRANSLATION", + ]); + ShaderBuilderTester.expectHasFragmentDefines(shaderBuilder, [ + "HAS_INSTANCING", + "HAS_INSTANCE_TRANSLATION", + ]); + + ShaderBuilderTester.expectHasAttributes(shaderBuilder, undefined, [ + "attribute vec3 a_instanceTranslation;", + ]); + + // No additional buffer was created. + expect(renderResources.model._pipelineResources.length).toEqual(0); + expect(renderResources.model._modelResources.length).toEqual(0); + + // Attributes with buffers already loaded in will be counted + // in NodeStatisticsPipelineStage. expect(renderResources.model.statistics.geometryByteLength).toBe(0); }); }); it("creates TRANSLATION vertex attributes for 2D", function () { const renderResources = mockRenderResourcesFor2D(); + const model = renderResources.model; + const runtimeNode = renderResources.runtimeNode; return loadGltf(boxInstancedTranslationMinMax, { loadAttributesFor2D: true, }).then(function (gltfLoader) { const components = gltfLoader.components; - renderResources.model.sceneGraph.components = components; const node = components.nodes[0]; - const runtimeNode = renderResources.runtimeNode; + const instances = node.instances; + + model.sceneGraph.components = components; runtimeNode.node = node; scene2D.renderForSpecs(); @@ -497,6 +537,13 @@ describe( expect(renderResources.attributes.length).toBe(2); + const translationAttribute = ModelUtility.getAttributeBySemantic( + instances, + InstanceAttributeSemantic.TRANSLATION + ); + // Expect the typed array to be unloaded. + expect(translationAttribute.typedArray).toBeUndefined(); + const shaderBuilder = renderResources.shaderBuilder; ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, [ "HAS_INSTANCING", @@ -517,9 +564,10 @@ describe( "uniform mat4 u_modelView2D;", ]); - expect(renderResources.instancingReferencePoint2D).toBeDefined(); + expect(runtimeNode.instancingReferencePoint2D).toBeDefined(); + const translationMatrix = Matrix4.fromTranslation( - renderResources.instancingReferencePoint2D, + runtimeNode.instancingReferencePoint2D, scratchMatrix4 ); const expectedMatrix = Matrix4.multiplyTransformation( @@ -531,11 +579,11 @@ describe( expect(uniformMap.u_modelView2D()).toEqual(expectedMatrix); expect(runtimeNode.instancingTranslationBuffer2D).toBeDefined(); - expect(renderResources.model._pipelineResources.length).toEqual(0); - expect(renderResources.model._modelResources.length).toEqual(1); + expect(model._pipelineResources.length).toEqual(0); + expect(model._modelResources.length).toEqual(1); - // Both resources will be counted in NodeStatisticsPipelineStage - expect(renderResources.model.statistics.geometryByteLength).toBe(0); + // The resource will be counted in NodeStatisticsPipelineStage. + expect(model.statistics.geometryByteLength).toBe(0); }); }); @@ -644,14 +692,8 @@ describe( CesiumMath.EPSILON8 ); - // Matrices are stored as 3 vec4s, so this is - // 25 matrices * 12 floats/matrix * 4 bytes/float = 1200 - const matrixSize = 1200; - // 25 floats - const featureIdSize = 100; - expect(renderResources.model.statistics.geometryByteLength).toBe( - matrixSize + featureIdSize - ); + // The matrix transforms buffer will be counted by NodeStatisticsPipelineStage. + expect(renderResources.model.statistics.geometryByteLength).toBe(0); }); }); }, diff --git a/Specs/Scene/Model/ModelSpec.js b/Specs/Scene/Model/ModelSpec.js index 6dd0b5b6426d..7b0c4c7f6bb2 100644 --- a/Specs/Scene/Model/ModelSpec.js +++ b/Specs/Scene/Model/ModelSpec.js @@ -894,6 +894,7 @@ describe( gltf: boxTexturedGlbUrl, modelMatrix: modelMatrix, projectTo2D: true, + incrementallyLoadTextures: false, }, scene2D ).then(function (model) { @@ -911,6 +912,7 @@ describe( gltf: boxTexturedGlbUrl, modelMatrix: modelMatrix, projectTo2D: true, + incrementallyLoadTextures: false, }, sceneCV ).then(function (model) { diff --git a/Specs/Scene/Model/NodeStatisticsPipelineStageSpec.js b/Specs/Scene/Model/NodeStatisticsPipelineStageSpec.js index 11bf5963f9c2..75eb34766a4a 100644 --- a/Specs/Scene/Model/NodeStatisticsPipelineStageSpec.js +++ b/Specs/Scene/Model/NodeStatisticsPipelineStageSpec.js @@ -119,11 +119,10 @@ describe( }); }); - it("_countInstancingAttributes does not count attributes without buffers", function () { - // This model contains instanced rotations, so the transformation - // attributes will be loaded in as packed typed arrays only. - // Feature IDs are also only loaded as packed typed arrays. - return loadGltf(boxInstanced).then(function (gltfLoader) { + it("_countInstancingAttributes counts attributes with buffers", function () { + return loadGltf(boxInstancedTranslationMinMax).then(function ( + gltfLoader + ) { const statistics = new ModelStatistics(); const components = gltfLoader.components; const node = components.nodes[0]; @@ -133,14 +132,18 @@ describe( node.instances ); - expect(statistics.geometryByteLength).toBe(0); + // Model contains four translated instances: + // 4 instances * 3 floats * 4 bytes per float + const expectedByteLength = 4 * 12; + expect(statistics.geometryByteLength).toBe(expectedByteLength); }); }); - it("_countInstancingAttributes counts attributes with buffers", function () { - return loadGltf(boxInstancedTranslationMinMax).then(function ( - gltfLoader - ) { + it("_countInstancingAttributes does not count attributes without buffers", function () { + // This model contains instanced rotations, so the transformation + // attributes will be loaded in as packed typed arrays only. + // Feature IDs, however, are loaded as buffers. + return loadGltf(boxInstanced).then(function (gltfLoader) { const statistics = new ModelStatistics(); const components = gltfLoader.components; const node = components.nodes[0]; @@ -150,15 +153,36 @@ describe( node.instances ); - // Model contains four translated instances: - // 4 instances * 3 floats * 4 bytes per float - const expectedByteLength = 4 * 12; - + // 4 feature ids * 4 bytes per float + const expectedByteLength = 16; expect(statistics.geometryByteLength).toBe(expectedByteLength); }); }); - it("_countInstancing2DBuffers counts instancing transform buffer for 2D", function () { + it("_countGeneratedBuffers counts instancing transform buffer", function () { + return loadGltf(boxInstanced).then(function (gltfLoader) { + const statistics = new ModelStatistics(); + const mockRuntimeNode = { + instancingTransformsBuffer: { + // Matrices are stored as 3 vec4s, so this is + // 4 matrices * 12 floats/matrix * 4 bytes/float = 192 + sizeInBytes: 192, + }, + }; + + NodeStatisticsPipelineStage._countGeneratedBuffers( + statistics, + mockRuntimeNode + ); + + const transformsBuffer = mockRuntimeNode.instancingTransformsBuffer; + expect(statistics.geometryByteLength).toBe( + transformsBuffer.sizeInBytes + ); + }); + }); + + it("_countGeneratedBuffers counts instancing transform buffer for 2D", function () { return loadGltf(boxInstanced).then(function (gltfLoader) { const statistics = new ModelStatistics(); const mockRuntimeNode = { @@ -169,7 +193,7 @@ describe( }, }; - NodeStatisticsPipelineStage._countInstancing2DBuffers( + NodeStatisticsPipelineStage._countGeneratedBuffers( statistics, mockRuntimeNode ); @@ -181,7 +205,7 @@ describe( }); }); - it("_countInstancing2DBuffers counts instancing translation buffer for 2D", function () { + it("_countGeneratedBuffers counts instancing translation buffer for 2D", function () { return loadGltf(boxInstancedTranslationMinMax).then(function ( gltfLoader ) { @@ -194,7 +218,7 @@ describe( }, }; - NodeStatisticsPipelineStage._countInstancing2DBuffers( + NodeStatisticsPipelineStage._countGeneratedBuffers( statistics, mockRuntimeNode );