Merge branch 'main' into label-clamping-performance

This commit is contained in:
Gabby Getz 2025-09-29 12:57:08 -04:00 committed by GitHub
commit 7e376b6450
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 267 additions and 64 deletions

View File

@ -12,7 +12,8 @@
- Improved performance and reduced memory usage of `Event` class. [#12896](https://github.com/CesiumGS/cesium/pull/12896)
- Fixes vertical misalignment of glyphs in labels with small fonts [#8474](https://github.com/CesiumGS/cesium/issues/8474)
- Prevent runtime errors for certain forms of invalid PNTS files [#12872](https://github.com/CesiumGS/cesium/issues/12872)
- Improved performance of clamped labels [#12905](https://github.com/CesiumGS/cesium/pull/12905)
- Improved performance of clamped labels. [#12905](https://github.com/CesiumGS/cesium/pull/12905)
- Fixes issue where multiple instances of a Gaussian splat tileset would transform tile positions incorrectly and render out of position. [#12795](https://github.com/CesiumGS/cesium/issues/12795)
#### Additions :tada:

View File

@ -431,3 +431,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
- [Easy Mahaffey](https://github.com/easymaahffey)
- [Pamela Augustine](https://github.com/pamelaAugustine)
- [宋时旺](https://github.com/BlockCnFuture)
- [Marco Zhan](https://github.com/marcoYxz)

View File

@ -123,7 +123,7 @@ function handleBuildWarnings(result) {
export async function build() {
// Configure build options from command line arguments.
const minify = argv.minify ?? false;
const removePragmas = argv.pragmas ?? false;
const removePragmas = argv.removePragmas ?? false;
const sourcemap = argv.sourcemap ?? true;
const node = argv.node ?? true;

View File

@ -46,7 +46,90 @@ function ClippingPolygon(options) {
//>>includeEnd('debug');
this._ellipsoid = options.ellipsoid ?? Ellipsoid.default;
this._positions = [...options.positions];
this._positions = copyArrayCartesian3(options.positions);
/**
* A copy of the input positions.
*
* This is used to detect modifications of the positions in
* <code>coputeRectangle</code>: The rectangle only has
* to be re-computed when these positions have changed.
*
* @type {Cartesian3[]|undefined}
* @private
*/
this._cachedPositions = undefined;
/**
* A cached version of the rectangle that is computed in
* <code>computeRectangle</code>.
*
* This is only re-computed when the positions have changed, as
* determined by comparing the <code>_positions</code> to the
* <code>_cachedPositions</code>
*
* @type {Rectangle|undefined}
* @private
*/
this._cachedRectangle = undefined;
}
/**
* Returns a deep copy of the given array.
*
* If the input is undefined, then <code>undefined</code> is returned.
*
* Otherwise, the result will be a copy of the given array, where
* each element is copied with <code>Cartesian3.clone</code>.
*
* @param {Cartesian3[]|undefined} input The input array
* @returns {Cartesian3[]|undefined} The copy
*/
function copyArrayCartesian3(input) {
if (!defined(input)) {
return undefined;
}
const n = input.length;
const output = Array(n);
for (let i = 0; i < n; i++) {
output[i] = Cartesian3.clone(input[i]);
}
return output;
}
/**
* Returns whether the given arrays are component-wise equal.
*
* When both arrays are undefined, then <code>true</code> is returned.
* When only one array is defined, or they are both defined but have
* different lengths, then <code>false</code> is returned.
*
* Otherwise, returns whether the corresponding elements of the arrays
* are equal, as of <code>Cartesian3.equals</code>.
*
* @param {Cartesian3[]|undefined} a The first array
* @param {Cartesian3[]|undefined} b The second array
* @returns {boolean} Whether the arrays are equal
*/
function equalsArrayCartesian3(a, b) {
if (!defined(a) && !defined(b)) {
return true;
}
if (defined(a) !== defined(b)) {
return false;
}
if (a.length !== b.length) {
return false;
}
const n = a.length;
for (let i = 0; i < n; i++) {
const ca = a[i];
const cb = b[i];
if (!Cartesian3.equals(ca, cb)) {
return false;
}
}
return true;
}
Object.defineProperties(ClippingPolygon.prototype, {
@ -138,12 +221,18 @@ ClippingPolygon.equals = function (left, right) {
* @returns {Rectangle} The result rectangle
*/
ClippingPolygon.prototype.computeRectangle = function (result) {
return PolygonGeometry.computeRectangleFromPositions(
if (equalsArrayCartesian3(this._positions, this._cachedPositions)) {
return Rectangle.clone(this._cachedRectangle, result);
}
const rectangle = PolygonGeometry.computeRectangleFromPositions(
this.positions,
this.ellipsoid,
undefined,
result,
);
this._cachedPositions = copyArrayCartesian3(this._positions);
this._cachedRectangle = Rectangle.clone(rectangle);
return rectangle;
};
const scratchRectangle = new Rectangle();

View File

@ -31,15 +31,28 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) {
}
/**
* Original position, scale and rotation values for splats. Used to maintain
* consistency when multiple transforms may occur. Downstream consumers otherwise may not know
* the underlying data was modified.
* Local copy of the position attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._originalPositions = undefined;
this._originalRotations = undefined;
this._originalScales = undefined;
this._positions = undefined;
/**
* Local copy of the rotation attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._rotations = undefined;
/**
* Local copy of the scale attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._scales = undefined;
/**
* glTF primitive data that contains the Gaussian splat data needed for rendering.
@ -368,6 +381,38 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, {
},
},
/**
* Get the transformed positions of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
positions: {
get: function () {
return this._positions;
},
},
/**
* Get the transformed rotations of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
rotations: {
get: function () {
return this._rotations;
},
},
/**
* Get the transformed scales of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
scales: {
get: function () {
return this._scales;
},
},
/**
* The number of spherical harmonic coefficients used for the Gaussian splats.
* @type {number}
@ -629,21 +674,21 @@ GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) {
this.worldTransform = loader.components.scene.nodes[0].matrix;
this._ready = true;
this._originalPositions = new Float32Array(
this._positions = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.POSITION,
).typedArray,
);
this._originalRotations = new Float32Array(
this._rotations = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.ROTATION,
).typedArray,
);
this._originalScales = new Float32Array(
this._scales = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.SCALE,

View File

@ -450,9 +450,9 @@ GaussianSplatPrimitive.transformTile = function (tile) {
computedModelMatrix,
scratchMatrix4A,
);
const positions = tile.content._originalPositions;
const rotations = tile.content._originalRotations;
const scales = tile.content._originalScales;
const positions = tile.content.positions;
const rotations = tile.content.rotations;
const scales = tile.content.scales;
const attributePositions = ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.POSITION,
@ -471,19 +471,19 @@ GaussianSplatPrimitive.transformTile = function (tile) {
const position = new Cartesian3();
const rotation = new Quaternion();
const scale = new Cartesian3();
for (let i = 0; i < positions.length / 3; ++i) {
position.x = positions[i * 3];
position.y = positions[i * 3 + 1];
position.z = positions[i * 3 + 2];
for (let i = 0; i < attributePositions.length / 3; ++i) {
position.x = attributePositions[i * 3];
position.y = attributePositions[i * 3 + 1];
position.z = attributePositions[i * 3 + 2];
rotation.x = rotations[i * 4];
rotation.y = rotations[i * 4 + 1];
rotation.z = rotations[i * 4 + 2];
rotation.w = rotations[i * 4 + 3];
rotation.x = attributeRotations[i * 4];
rotation.y = attributeRotations[i * 4 + 1];
rotation.z = attributeRotations[i * 4 + 2];
rotation.w = attributeRotations[i * 4 + 3];
scale.x = scales[i * 3];
scale.y = scales[i * 3 + 1];
scale.z = scales[i * 3 + 2];
scale.x = attributeScales[i * 3];
scale.y = attributeScales[i * 3 + 1];
scale.z = attributeScales[i * 3 + 2];
Matrix4.fromTranslationQuaternionRotationScale(
position,
@ -498,18 +498,18 @@ GaussianSplatPrimitive.transformTile = function (tile) {
Matrix4.getRotation(scratchMatrix4C, rotation);
Matrix4.getScale(scratchMatrix4C, scale);
attributePositions[i * 3] = position.x;
attributePositions[i * 3 + 1] = position.y;
attributePositions[i * 3 + 2] = position.z;
positions[i * 3] = position.x;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z;
attributeRotations[i * 4] = rotation.x;
attributeRotations[i * 4 + 1] = rotation.y;
attributeRotations[i * 4 + 2] = rotation.z;
attributeRotations[i * 4 + 3] = rotation.w;
rotations[i * 4] = rotation.x;
rotations[i * 4 + 1] = rotation.y;
rotations[i * 4 + 2] = rotation.z;
rotations[i * 4 + 3] = rotation.w;
attributeScales[i * 3] = scale.x;
attributeScales[i * 3 + 1] = scale.y;
attributeScales[i * 3 + 2] = scale.z;
scales[i * 3] = scale.x;
scales[i * 3 + 1] = scale.y;
scales[i * 3 + 2] = scale.z;
}
};
@ -860,21 +860,27 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
const aggregateAttributeValues = (
componentDatatype,
getAttributeCallback,
numberOfComponents,
) => {
let aggregate;
let offset = 0;
for (const tile of tiles) {
const primitive = tile.content.gltfPrimitive;
const attribute = getAttributeCallback(primitive);
const content = tile.content;
const attribute = getAttributeCallback(content);
const componentsPerAttribute = defined(numberOfComponents)
? numberOfComponents
: AttributeType.getNumberOfComponents(attribute.type);
const buffer = defined(attribute.typedArray)
? attribute.typedArray
: attribute;
if (!defined(aggregate)) {
aggregate = ComponentDatatype.createTypedArray(
componentDatatype,
totalElements *
AttributeType.getNumberOfComponents(attribute.type),
totalElements * componentsPerAttribute,
);
}
aggregate.set(attribute.typedArray, offset);
offset += attribute.typedArray.length;
aggregate.set(buffer, offset);
offset += buffer.length;
}
return aggregate;
};
@ -906,36 +912,27 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
this._positions = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.POSITION,
),
(content) => content.positions,
3,
);
this._scales = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.SCALE,
),
(content) => content.scales,
3,
);
this._rotations = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.ROTATION,
),
(content) => content.rotations,
4,
);
this._colors = aggregateAttributeValues(
ComponentDatatype.UNSIGNED_BYTE,
(gltfPrimitive) =>
(content) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
content.gltfPrimitive,
VertexAttributeSemantic.COLOR,
),
);

View File

@ -11,6 +11,7 @@ import {
import Cesium3DTilesTester from "../../../../Specs/Cesium3DTilesTester.js";
import createScene from "../../../../Specs/createScene.js";
import pollToPromise from "../../../../Specs/pollToPromise.js";
describe(
"Scene/GaussianSplat3DTileContent",
@ -96,6 +97,7 @@ describe(
},
);
});
it("Create and destroy GaussianSplat3DTileContent", async function () {
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
@ -116,6 +118,69 @@ describe(
expect(tile.isDestroyed()).toBe(true);
expect(tile.content).toBeUndefined();
});
it("Load multiple instances of Gaussian splat tileset and validate transformed attributes", async function () {
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
tilesetUrl,
options,
);
scene.camera.lookAt(
tileset.boundingSphere.center,
new HeadingPitchRange(0.0, -1.57, tileset.boundingSphere.radius),
);
const tileset2 = await Cesium3DTilesTester.loadTileset(
scene,
tilesetUrl,
options,
);
const tile = await Cesium3DTilesTester.waitForTileContentReady(
scene,
tileset.root,
);
scene.camera.lookAt(
tileset2.boundingSphere.center,
new HeadingPitchRange(0.0, -1.57, tileset2.boundingSphere.radius),
);
const tile2 = await Cesium3DTilesTester.waitForTileContentReady(
scene,
tileset2.root,
);
const content = tile.content;
const content2 = tile2.content;
expect(content).toBeDefined();
expect(content instanceof GaussianSplat3DTileContent).toBe(true);
expect(content2).toBeDefined();
expect(content2 instanceof GaussianSplat3DTileContent).toBe(true);
await pollToPromise(function () {
scene.renderForSpecs();
return (
tile.content._transformed === true &&
tile2.content._transformed === true
);
});
const positions1 = tile.content._positions;
const positions2 = tile2.content._positions;
expect(positions1.every((p, i) => p === positions2[i])).toBe(true);
const rotations1 = tile.content._rotations;
const rotations2 = tile2.content._rotations;
expect(rotations1.every((r, i) => r === rotations2[i])).toBe(true);
const scales1 = tile.content._scales;
const scales2 = tile2.content._scales;
expect(scales1.every((s, i) => s === scales2[i])).toBe(true);
});
},
"WebGL",
);

View File

@ -299,7 +299,7 @@ if (import.meta.url.endsWith(`${pathToFileURL(process.argv[1])}`)) {
let buildGalleryOptions;
try {
const config = await import(configPath);
const config = await import(pathToFileURL(configPath).href);
const { root, publicDir, gallery, sourceUrl } = config.default;
// Paths are specified relative to the config file

View File

@ -4,7 +4,7 @@ import { readFile, writeFile } from "node:fs/promises";
import { EOL } from "node:os";
import path from "node:path";
import { finished } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import esbuild from "esbuild";
import { globby } from "globby";
@ -387,6 +387,9 @@ export async function bundleWorkers(options) {
workerConfig.logOverride = {
"empty-import-meta": "silent",
};
workerConfig.plugins = options.removePragmas
? [stripPragmaPlugin]
: undefined;
} else {
workerConfig.format = "esm";
workerConfig.splitting = true;
@ -616,7 +619,7 @@ const externalResolvePlugin = {
export async function getSandcastleConfig() {
const configPath = "packages/sandcastle/sandcastle.config.js";
const configImportPath = path.join(projectRoot, configPath);
const config = await import(configImportPath);
const config = await import(pathToFileURL(configImportPath).href);
const options = config.default;
return {
...options,
@ -650,7 +653,9 @@ export async function buildSandcastleGallery(includeDevelopment) {
__dirname,
"../packages/sandcastle/scripts/buildGallery.js",
);
const { buildGalleryList } = await import(buildGalleryScriptPath);
const { buildGalleryList } = await import(
pathToFileURL(buildGalleryScriptPath).href
);
await buildGalleryList({
rootDirectory,