Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Free typed arrays for instanced models #10731

Merged
merged 6 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 26 additions & 24 deletions Source/Scene/GltfLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions Source/Scene/Model/GeometryPipelineStage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
158 changes: 126 additions & 32 deletions Source/Scene/Model/I3dmLoader.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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.
Expand All @@ -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);
}

Expand All @@ -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.
Expand Down Expand Up @@ -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;
Loading