diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
index 2103808dd985..a3578a9948dd 100644
--- a/.github/workflows/dev.yml
+++ b/.github/workflows/dev.yml
@@ -1,7 +1,7 @@
name: dev
-on:
+on:
push:
- branches:
+ branches:
- main
pull_request:
concurrency:
@@ -24,6 +24,8 @@ jobs:
run: npm run markdownlint
- name: format code
run: npm run prettier-check
+ - name: build
+ run: npm run build
- name: tsc
run: npm run tsc
coverage:
@@ -80,4 +82,4 @@ jobs:
run: npm pack &> /dev/null
- name: package workspace modules
run: npm pack --workspaces &> /dev/null
- - uses: ./.github/actions/verify-package
\ No newline at end of file
+ - uses: ./.github/actions/verify-package
diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml
index 1b8b4c402342..b13272e81094 100644
--- a/.github/workflows/prod.yml
+++ b/.github/workflows/prod.yml
@@ -30,6 +30,8 @@ jobs:
run: npm run markdownlint
- name: format code
run: npm run prettier-check
+ - name: build
+ run: npm run build
- name: tsc
run: npm run tsc
diff --git a/packages/engine/Source/Renderer/RenderState.js b/packages/engine/Source/Renderer/RenderState.js
index 87398a2e7adf..8ee20d6c9991 100644
--- a/packages/engine/Source/Renderer/RenderState.js
+++ b/packages/engine/Source/Renderer/RenderState.js
@@ -496,7 +496,7 @@ RenderState.fromCache = function (renderState) {
};
/**
- * @private
+ * @ignore
*/
RenderState.removeFromCache = function (renderState) {
const states = new RenderState(renderState);
diff --git a/packages/engine/Source/Renderer/VertexArray.js b/packages/engine/Source/Renderer/VertexArray.js
index eeaa20253b3a..8307bded980b 100644
--- a/packages/engine/Source/Renderer/VertexArray.js
+++ b/packages/engine/Source/Renderer/VertexArray.js
@@ -12,6 +12,9 @@ import Buffer from "./Buffer.js";
import BufferUsage from "./BufferUsage.js";
import ContextLimits from "./ContextLimits.js";
import AttributeType from "../Scene/AttributeType.js";
+import assert from "../Core/assert.js";
+
+/** @import {TypedArray, TypedArrayConstructor} from "../Core/globalTypes.js"; */
function addAttribute(attributes, attribute, index, context) {
const hasVertexBuffer = defined(attribute.vertexBuffer);
@@ -801,6 +804,83 @@ function setConstantAttributes(vertexArray, gl) {
}
}
+/**
+ * Copies into a vertex attribute buffer from the given array, at a given
+ * range specified as offset and count, in number of (VECN) vertices. Array
+ * and vertex attribute must have the same length, which can be larger
+ * than the specified range to update.
+ * @param {number} attributeIndex
+ * @param {TypedArray} array
+ * @param {number} vertexOffset
+ * @param {number} vertexCount
+ */
+VertexArray.prototype.copyAttributeFromRange = function (
+ attributeIndex,
+ array,
+ vertexOffset,
+ vertexCount,
+) {
+ const attribute = this.getAttribute(attributeIndex);
+ const buffer = /** @type {Buffer} */ (attribute.vertexBuffer);
+ const elementsPerVertex = attribute.componentsPerAttribute;
+
+ //>>includeStart('debug', pragmas.debug);
+ assert(buffer.sizeInBytes === array.byteLength, "Invalid buffer length");
+ //>>includeEnd('debug');
+
+ const ArrayConstructor = /** @type {TypedArrayConstructor} */ (
+ array.constructor
+ );
+
+ const byteOffset =
+ vertexOffset * elementsPerVertex * ArrayConstructor.BYTES_PER_ELEMENT;
+
+ // Create a zero-copy ArrayView onto the specified range of the source array.
+ const rangeArrayView = new ArrayConstructor(
+ /** @type {ArrayBuffer} */ (array.buffer),
+ array.byteOffset + byteOffset,
+ vertexCount * elementsPerVertex,
+ );
+
+ buffer.copyFromArrayView(rangeArrayView, byteOffset);
+};
+
+/**
+ * Copies into the index buffer from the given array, at a given range
+ * specified as offset and count, in number of (uint) indices. Array
+ * and index buffer must have the same length, which can be larger
+ * than the specified range to update.
+ * @param {TypedArray} array
+ * @param {number} indexOffset
+ * @param {number} indexCount
+ */
+VertexArray.prototype.copyIndexFromRange = function (
+ array,
+ indexOffset,
+ indexCount,
+) {
+ const buffer = /** @type {Buffer} */ (this._indexBuffer);
+
+ //>>includeStart('debug', pragmas.debug);
+ assert(buffer.sizeInBytes === array.byteLength, "Invalid buffer length");
+ //>>includeEnd('debug');
+
+ const ArrayConstructor = /** @type {TypedArrayConstructor} */ (
+ array.constructor
+ );
+
+ const byteOffset = indexOffset * ArrayConstructor.BYTES_PER_ELEMENT;
+
+ // Create a zero-copy ArrayView onto the specified range of the source array.
+ const rangeArrayView = new ArrayConstructor(
+ /** @type {ArrayBuffer} */ (array.buffer),
+ array.byteOffset + byteOffset,
+ indexCount,
+ );
+
+ buffer.copyFromArrayView(rangeArrayView, byteOffset);
+};
+
VertexArray.prototype._bind = function () {
if (defined(this._vao)) {
this._context.glBindVertexArray(this._vao);
diff --git a/packages/engine/Source/Scene/BufferPointCollection.js b/packages/engine/Source/Scene/BufferPointCollection.js
index 9558a410f185..b87ec15d7a8f 100644
--- a/packages/engine/Source/Scene/BufferPointCollection.js
+++ b/packages/engine/Source/Scene/BufferPointCollection.js
@@ -4,12 +4,15 @@ import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPoint from "./BufferPoint.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Frozen from "../Core/Frozen.js";
+import renderPoints from "./renderBufferPointCollection.js";
/** @import Color from "../Core/Color.js"; */
-/** @import FrameState from "./FrameState.js" */
+/** @import Matrix4 from "../Core/Matrix4.js"; */
+/** @import FrameState from "./FrameState.js"; */
/**
* @typedef {object} BufferPointOptions
+ * @property {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {Color} [color=Color.WHITE]
* @property {Cartesian3} [position=Cartesian3.ZERO]
@@ -116,6 +119,10 @@ class BufferPointCollection extends BufferPrimitiveCollection {
*/
update(frameState) {
super.update(frameState);
+
+ if (this.show) {
+ this._renderContext = renderPoints(this, frameState, this._renderContext);
+ }
}
}
diff --git a/packages/engine/Source/Scene/BufferPolygonCollection.js b/packages/engine/Source/Scene/BufferPolygonCollection.js
index a870253837b5..7e20828b0b2f 100644
--- a/packages/engine/Source/Scene/BufferPolygonCollection.js
+++ b/packages/engine/Source/Scene/BufferPolygonCollection.js
@@ -5,17 +5,20 @@ import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPolygon from "./BufferPolygon.js";
import Frozen from "../Core/Frozen.js";
import assert from "../Core/assert.js";
-import ComponentDatatype from "../Core/ComponentDatatype.js";
import IndexDatatype from "../Core/IndexDatatype.js";
+import renderPolygons from "./renderBufferPolygonCollection.js";
/** @import { TypedArray } from "../Core/globalTypes.js"; */
/** @import Color from "../Core/Color.js"; */
+/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js" */
+/** @import ComponentDatatype from "../Core/ComponentDatatype.js"; */
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
/**
* @typedef {object} BufferPolygonOptions
+ * @property {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {Color} [color=Color.WHITE]
* @property {TypedArray} [positions]
@@ -73,7 +76,6 @@ class BufferPolygonCollection extends BufferPrimitiveCollection {
* @param {number} [options.holeCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.triangleCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE]
- * @param {IndexDatatype} [options.indexDatatype=IndexDatatype.UNSIGNED_INT]
* @param {boolean} [options.show=true]
* @param {boolean} [options.debugShowBoundingVolume=false]
*/
@@ -120,15 +122,8 @@ class BufferPolygonCollection extends BufferPrimitiveCollection {
*/
this._triangleIndexView = null;
- this._allocateHoleIndexBuffer(
- // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
- options.indexDatatype ?? IndexDatatype.UNSIGNED_INT,
- );
-
- this._allocateTriangleIndexBuffer(
- // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
- options.indexDatatype ?? IndexDatatype.UNSIGNED_INT,
- );
+ this._allocateHoleIndexBuffer();
+ this._allocateTriangleIndexBuffer();
}
_getCollectionClass() {
@@ -143,27 +138,25 @@ class BufferPolygonCollection extends BufferPrimitiveCollection {
// COLLECTION LIFECYCLE
/**
- * @param {IndexDatatype} datatype
* @private
* @ignore
*/
- _allocateHoleIndexBuffer(datatype) {
+ _allocateHoleIndexBuffer() {
// @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
- this._holeIndexView = ComponentDatatype.createTypedArray(
- datatype,
+ this._holeIndexView = IndexDatatype.createTypedArray(
+ this._positionCountMax,
this._holeCountMax,
);
}
/**
- * @param {IndexDatatype} datatype
* @private
* @ignore
*/
- _allocateTriangleIndexBuffer(datatype) {
+ _allocateTriangleIndexBuffer() {
// @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
- this._triangleIndexView = ComponentDatatype.createTypedArray(
- datatype,
+ this._triangleIndexView = IndexDatatype.createTypedArray(
+ this._positionCountMax,
this._triangleCountMax * 3,
);
}
@@ -292,6 +285,14 @@ class BufferPolygonCollection extends BufferPrimitiveCollection {
*/
update(frameState) {
super.update(frameState);
+
+ if (this.show) {
+ this._renderContext = renderPolygons(
+ this,
+ frameState,
+ this._renderContext,
+ );
+ }
}
/////////////////////////////////////////////////////////////////////////////
diff --git a/packages/engine/Source/Scene/BufferPolylineCollection.js b/packages/engine/Source/Scene/BufferPolylineCollection.js
index e6e74644b9f6..d649c91f5ad2 100644
--- a/packages/engine/Source/Scene/BufferPolylineCollection.js
+++ b/packages/engine/Source/Scene/BufferPolylineCollection.js
@@ -3,13 +3,16 @@
import defined from "../Core/defined.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPolyline from "./BufferPolyline.js";
+import renderPolylines from "./renderBufferPolylineCollection.js";
/** @import { TypedArray } from "../Core/globalTypes.js"; */
/** @import Color from "../Core/Color.js"; */
+/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js" */
/**
* @typedef {object} BufferPolylineOptions
+ * @property {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {Color} [color=Color.WHITE]
* @property {TypedArray} [positions]
@@ -115,6 +118,14 @@ class BufferPolylineCollection extends BufferPrimitiveCollection {
*/
update(frameState) {
super.update(frameState);
+
+ if (this.show) {
+ this._renderContext = renderPolylines(
+ this,
+ frameState,
+ this._renderContext,
+ );
+ }
}
}
diff --git a/packages/engine/Source/Scene/BufferPrimitiveCollection.js b/packages/engine/Source/Scene/BufferPrimitiveCollection.js
index 5bbe9b3063ec..96de7d6a8ff4 100644
--- a/packages/engine/Source/Scene/BufferPrimitiveCollection.js
+++ b/packages/engine/Source/Scene/BufferPrimitiveCollection.js
@@ -5,8 +5,10 @@ import Color from "../Core/Color.js";
import Cartesian3 from "../Core/Cartesian3.js";
import DeveloperError from "../Core/DeveloperError.js";
import Frozen from "../Core/Frozen.js";
+import Matrix4 from "../Core/Matrix4.js";
import assert from "../Core/assert.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
+import defined from "../Core/defined.js";
/** @import { TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */
/** @import BufferPrimitive from "./BufferPrimitive.js"; */
@@ -63,13 +65,14 @@ class BufferPrimitiveCollection {
* implementations, so the collection should be ignorant of the renderer's implementation
* and context data. A collection only has one renderer active at a time.
*
- * @type {unknown}
+ * @type {{destroy: Function}|null}
* @ignore
*/
_renderContext = null;
/**
* @param {object} options
+ * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.vertexCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {boolean} [options.show=true]
@@ -85,12 +88,26 @@ class BufferPrimitiveCollection {
this.show = options.show ?? true;
/**
- * Bounding volume for all primitives in the collection, including both
+ * Transforms geometry from model to world coordinates.
+ * @type {Matrix4}
+ * @default Matrix4.IDENTITY
+ */
+ this.modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY);
+
+ /**
+ * Local bounding volume for all primitives in the collection, including both
* shown and hidden primitives.
* @type {BoundingSphere}
*/
this.boundingVolume = new BoundingSphere();
+ /**
+ * World bounding volume for all primitives in the collection, including both
+ * shown and hidden primitives.
+ * @type {BoundingSphere}
+ */
+ this.boundingVolumeWC = new BoundingSphere();
+
/**
* This property is for debugging only; it is not for production use nor is it optimized.
*
@@ -234,6 +251,16 @@ class BufferPrimitiveCollection {
return false;
}
+ /** Destroys collection and its GPU resources. */
+ destroy() {
+ if (defined(this._renderContext)) {
+ this._renderContext.destroy();
+ this._renderContext = undefined;
+ this._dirtyOffset = 0;
+ this._dirtyCount = this.primitiveCount;
+ }
+ }
+
/**
* Sorts primitives of the collection.
*
@@ -390,7 +417,11 @@ class BufferPrimitiveCollection {
3,
this.boundingVolume,
);
-
+ BoundingSphere.transform(
+ this.boundingVolume,
+ this.modelMatrix,
+ this.boundingVolumeWC,
+ );
this._dirtyBoundingVolume = false;
}
diff --git a/packages/engine/Source/Scene/renderBufferPointCollection.js b/packages/engine/Source/Scene/renderBufferPointCollection.js
new file mode 100644
index 000000000000..7febf8f94f4d
--- /dev/null
+++ b/packages/engine/Source/Scene/renderBufferPointCollection.js
@@ -0,0 +1,286 @@
+// @ts-check
+
+import defined from "../Core/defined.js";
+import Cartesian3 from "../Core/Cartesian3.js";
+import BufferPoint from "./BufferPoint.js";
+import Buffer from "../Renderer/Buffer.js";
+import BufferUsage from "../Renderer/BufferUsage.js";
+import VertexArray from "../Renderer/VertexArray.js";
+import ComponentDatatype from "../Core/ComponentDatatype.js";
+import RenderState from "../Renderer/RenderState.js";
+import BlendingState from "./BlendingState.js";
+import Color from "../Core/Color.js";
+import ShaderSource from "../Renderer/ShaderSource.js";
+import ShaderProgram from "../Renderer/ShaderProgram.js";
+import DrawCommand from "../Renderer/DrawCommand.js";
+import Pass from "../Renderer/Pass.js";
+import PrimitiveType from "../Core/PrimitiveType.js";
+import BufferPointCollectionVS from "../Shaders/BufferPointCollectionVS.js";
+import BufferPointCollectionFS from "../Shaders/BufferPointCollectionFS.js";
+import EncodedCartesian3 from "../Core/EncodedCartesian3.js";
+import AttributeCompression from "../Core/AttributeCompression.js";
+import Matrix4 from "../Core/Matrix4.js";
+import BoundingSphere from "../Core/BoundingSphere.js";
+
+/** @import FrameState from "./FrameState.js"; */
+/** @import BufferPointCollection from "./BufferPointCollection.js"; */
+/** @import {TypedArray} from "../Core/globalTypes.js"; */
+
+/**
+ * TODO(PR#13211): Need 'keyof' syntax to avoid duplicating attribute names.
+ * @typedef {'positionHigh' | 'positionLow' | 'showPixelSizeAndColor' | 'outlineWidthAndOutlineColor'} BufferPointAttribute
+ * @ignore
+ */
+
+/**
+ * @type {Record}
+ * @ignore
+ */
+const BufferPointAttributeLocations = {
+ positionHigh: 0,
+ positionLow: 1,
+ showPixelSizeAndColor: 2,
+ outlineWidthAndOutlineColor: 3,
+};
+
+/**
+ * @typedef {object} BufferPointRenderContext
+ * @property {VertexArray} [vertexArray]
+ * @property {Record} [attributeArrays]
+ * @property {RenderState} [renderState]
+ * @property {ShaderProgram} [shaderProgram]
+ * @property {DrawCommand} [command]
+ * @property {Function} destroy
+ * @ignore
+ */
+
+// Scratch variables.
+const point = new BufferPoint();
+const color = new Color();
+const cartesian = new Cartesian3();
+const encodedCartesian = new EncodedCartesian3();
+
+/**
+ * @param {BufferPointCollection} collection
+ * @param {FrameState} frameState
+ * @param {BufferPointRenderContext} [renderContext]
+ * @returns {BufferPointRenderContext}
+ * @ignore
+ */
+function renderBufferPointCollection(collection, frameState, renderContext) {
+ const context = frameState.context;
+ renderContext = renderContext || { destroy: destroyRenderContext };
+
+ if (!defined(renderContext.attributeArrays)) {
+ const featureCountMax = collection.primitiveCountMax;
+
+ renderContext.attributeArrays = {
+ positionHigh: new Float32Array(featureCountMax * 3),
+ positionLow: new Float32Array(featureCountMax * 3),
+ showPixelSizeAndColor: new Float32Array(featureCountMax * 3),
+ outlineWidthAndOutlineColor: new Float32Array(featureCountMax * 2),
+ };
+ }
+
+ if (collection._dirtyCount > 0) {
+ const { attributeArrays } = renderContext;
+
+ const positionHighArray = attributeArrays.positionHigh;
+ const positionLowArray = attributeArrays.positionLow;
+ const showPixelSizeAndColorArray = attributeArrays.showPixelSizeAndColor;
+ const outlineWidthAndOutlineColorArray =
+ attributeArrays.outlineWidthAndOutlineColor;
+
+ const { _dirtyOffset, _dirtyCount } = collection;
+
+ for (let i = _dirtyOffset, il = _dirtyOffset + _dirtyCount; i < il; i++) {
+ collection.get(i, point);
+
+ if (!point._dirty) {
+ continue;
+ }
+
+ point.getPosition(cartesian);
+ EncodedCartesian3.fromCartesian(cartesian, encodedCartesian);
+
+ positionHighArray[i * 3] = encodedCartesian.high.x;
+ positionHighArray[i * 3 + 1] = encodedCartesian.high.y;
+ positionHighArray[i * 3 + 2] = encodedCartesian.high.z;
+
+ positionLowArray[i * 3] = encodedCartesian.low.x;
+ positionLowArray[i * 3 + 1] = encodedCartesian.low.y;
+ positionLowArray[i * 3 + 2] = encodedCartesian.low.z;
+
+ showPixelSizeAndColorArray[i * 3] = point.show ? 1 : 0;
+ showPixelSizeAndColorArray[i * 3 + 1] = 5; // TODO: Material API.
+ showPixelSizeAndColorArray[i * 3 + 2] = AttributeCompression.encodeRGB8(
+ point.getColor(color),
+ );
+
+ outlineWidthAndOutlineColorArray[i * 2] = 0; // TODO: Material API.
+ outlineWidthAndOutlineColorArray[i * 2 + 1] =
+ AttributeCompression.encodeRGB8(Color.WHITE); // TODO: Material API.
+
+ point._dirty = false;
+ }
+ }
+
+ if (!defined(renderContext.vertexArray)) {
+ const { attributeArrays } = renderContext;
+
+ renderContext.vertexArray = new VertexArray({
+ context,
+ attributes: [
+ {
+ index: BufferPointAttributeLocations.positionHigh,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.positionHigh,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPointAttributeLocations.positionLow,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.positionLow,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPointAttributeLocations.showPixelSizeAndColor,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.showPixelSizeAndColor,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPointAttributeLocations.outlineWidthAndOutlineColor,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 2,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.outlineWidthAndOutlineColor,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ ],
+ });
+ } else if (collection._dirtyCount > 0) {
+ for (const key in BufferPointAttributeLocations) {
+ if (Object.hasOwn(BufferPointAttributeLocations, key)) {
+ const attribute = /** @type {BufferPointAttribute} */ (key);
+ renderContext.vertexArray.copyAttributeFromRange(
+ BufferPointAttributeLocations[attribute],
+ renderContext.attributeArrays[attribute],
+ collection._dirtyOffset,
+ collection._dirtyCount,
+ );
+ }
+ }
+ }
+
+ if (!defined(renderContext.renderState)) {
+ renderContext.renderState = RenderState.fromCache({
+ blending: BlendingState.ALPHA_BLEND,
+ depthTest: { enabled: true },
+ });
+ }
+
+ if (!defined(renderContext.shaderProgram)) {
+ renderContext.shaderProgram = ShaderProgram.fromCache({
+ context,
+ vertexShaderSource: new ShaderSource({
+ sources: [BufferPointCollectionVS],
+ }),
+ fragmentShaderSource: new ShaderSource({
+ sources: [BufferPointCollectionFS],
+ }),
+ attributeLocations: BufferPointAttributeLocations,
+ });
+ }
+
+ if (
+ !defined(renderContext.command) ||
+ isCommandDirty(collection, renderContext.command)
+ ) {
+ renderContext.command = new DrawCommand({
+ vertexArray: renderContext.vertexArray,
+ renderState: renderContext.renderState,
+ shaderProgram: renderContext.shaderProgram,
+ primitiveType: PrimitiveType.POINTS,
+ pass: Pass.OPAQUE,
+ owner: collection,
+ count: collection.primitiveCount,
+ modelMatrix: collection.modelMatrix,
+ boundingVolume: collection.boundingVolumeWC,
+ debugShowBoundingVolume: collection.debugShowBoundingVolume,
+ });
+ }
+
+ frameState.commandList.push(renderContext.command);
+
+ collection._dirtyCount = 0;
+ collection._dirtyOffset = 0;
+
+ return renderContext;
+}
+
+/**
+ * Returns true if DrawCommand is out of date for the given collection.
+ * @param {BufferPointCollection} collection
+ * @param {DrawCommand} command
+ * @ignore
+ */
+function isCommandDirty(collection, command) {
+ const isModelMatrixEqual = Matrix4.equals(
+ collection.modelMatrix,
+ command._modelMatrix,
+ );
+
+ const isBoundingVolumeEqual = BoundingSphere.equals(
+ collection.boundingVolumeWC,
+ command._boundingVolume,
+ );
+
+ return (
+ collection.primitiveCount !== command._count ||
+ collection.debugShowBoundingVolume !== command.debugShowBoundingVolume ||
+ !isModelMatrixEqual ||
+ !isBoundingVolumeEqual
+ );
+}
+
+/**
+ * Destroys render context resources. Deleting properties from the context
+ * object isn't necessary, as collection.destroy() will discard the object.
+ * @ignore
+ */
+function destroyRenderContext() {
+ const context = /** @type {BufferPointRenderContext} */ (this);
+
+ if (defined(context.vertexArray)) {
+ context.vertexArray.destroy();
+ }
+
+ if (defined(context.shaderProgram)) {
+ context.shaderProgram.destroy();
+ }
+
+ if (defined(context.renderState)) {
+ RenderState.removeFromCache(context.renderState);
+ }
+}
+
+export default renderBufferPointCollection;
diff --git a/packages/engine/Source/Scene/renderBufferPolygonCollection.js b/packages/engine/Source/Scene/renderBufferPolygonCollection.js
new file mode 100644
index 000000000000..05351ed55eb8
--- /dev/null
+++ b/packages/engine/Source/Scene/renderBufferPolygonCollection.js
@@ -0,0 +1,339 @@
+// @ts-check
+
+import defined from "../Core/defined.js";
+import Cartesian3 from "../Core/Cartesian3.js";
+import BufferPolygon from "./BufferPolygon.js";
+import Buffer from "../Renderer/Buffer.js";
+import BufferUsage from "../Renderer/BufferUsage.js";
+import VertexArray from "../Renderer/VertexArray.js";
+import ComponentDatatype from "../Core/ComponentDatatype.js";
+import RenderState from "../Renderer/RenderState.js";
+import BlendingState from "./BlendingState.js";
+import Color from "../Core/Color.js";
+import ShaderSource from "../Renderer/ShaderSource.js";
+import ShaderProgram from "../Renderer/ShaderProgram.js";
+import DrawCommand from "../Renderer/DrawCommand.js";
+import Pass from "../Renderer/Pass.js";
+import PrimitiveType from "../Core/PrimitiveType.js";
+import BufferPolygonCollectionVS from "../Shaders/BufferPolygonCollectionVS.js";
+import BufferPolygonCollectionFS from "../Shaders/BufferPolygonCollectionFS.js";
+import EncodedCartesian3 from "../Core/EncodedCartesian3.js";
+import AttributeCompression from "../Core/AttributeCompression.js";
+import IndexDatatype from "../Core/IndexDatatype.js";
+import BoundingSphere from "../Core/BoundingSphere.js";
+import Matrix4 from "../Core/Matrix4.js";
+
+/** @import {TypedArray} from "../Core/globalTypes.js"; */
+/** @import FrameState from "./FrameState.js"; */
+/** @import BufferPolygonCollection from "./BufferPolygonCollection.js"; */
+
+/**
+ * TODO(PR#13211): Need 'keyof' syntax to avoid duplicating attribute names.
+ * @typedef {'positionHigh' | 'positionLow' | 'showAndColor'} BufferPolygonAttribute
+ * @ignore
+ */
+
+/**
+ * @type {Record}
+ * @ignore
+ */
+const BufferPolygonAttributeLocations = {
+ positionHigh: 0,
+ positionLow: 1,
+ showAndColor: 2,
+};
+
+/**
+ * @typedef {object} BufferPolygonRenderContext
+ * @property {VertexArray} [vertexArray]
+ * @property {Record} [attributeArrays]
+ * @property {TypedArray} [indexArray]
+ * @property {RenderState} [renderState]
+ * @property {ShaderProgram} [shaderProgram]
+ * @property {DrawCommand} [command]
+ * @property {Function} destroy
+ * @ignore
+ */
+
+// Scratch variables.
+const polygon = new BufferPolygon();
+const color = new Color();
+const cartesian = new Cartesian3();
+const encodedCartesian = new EncodedCartesian3();
+
+/**
+ * @param {BufferPolygonCollection} collection
+ * @param {FrameState} frameState
+ * @param {BufferPolygonRenderContext} [renderContext]
+ * @returns {BufferPolygonRenderContext}
+ * @ignore
+ */
+function renderBufferPolygonCollection(collection, frameState, renderContext) {
+ const context = frameState.context;
+ renderContext = renderContext || { destroy: destroyRenderContext };
+
+ if (
+ !defined(renderContext.attributeArrays) ||
+ !defined(renderContext.indexArray)
+ ) {
+ const { vertexCountMax, triangleCountMax } = collection;
+
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ renderContext.indexArray = IndexDatatype.createTypedArray(
+ vertexCountMax,
+ triangleCountMax * 3,
+ );
+
+ renderContext.attributeArrays = {
+ positionHigh: new Float32Array(vertexCountMax * 3),
+ positionLow: new Float32Array(vertexCountMax * 3),
+ showAndColor: new Float32Array(vertexCountMax * 2),
+ };
+ }
+
+ if (collection._dirtyCount > 0) {
+ const { attributeArrays } = renderContext;
+ const { _dirtyOffset, _dirtyCount } = collection;
+
+ const indexArray = renderContext.indexArray;
+ const positionHighArray = attributeArrays.positionHigh;
+ const positionLowArray = attributeArrays.positionLow;
+ const showAndColorArray = attributeArrays.showAndColor;
+
+ for (let i = _dirtyOffset, il = _dirtyOffset + _dirtyCount; i < il; i++) {
+ collection.get(i, polygon);
+
+ if (!polygon._dirty) {
+ continue;
+ }
+
+ let tOffset = polygon.triangleOffset;
+ let vOffset = polygon.vertexOffset;
+
+ const polygonIndexArray = polygon.getTriangles();
+
+ // Update index.
+ for (let j = 0, jl = polygon.triangleCount; j < jl; j++) {
+ indexArray[tOffset * 3] = vOffset + polygonIndexArray[j * 3];
+ indexArray[tOffset * 3 + 1] = vOffset + polygonIndexArray[j * 3 + 1];
+ indexArray[tOffset * 3 + 2] = vOffset + polygonIndexArray[j * 3 + 2];
+
+ tOffset++;
+ }
+
+ const show = polygon.show;
+ const cartesianArray = polygon.getPositions();
+ const encodedColor = AttributeCompression.encodeRGB8(
+ polygon.getColor(color),
+ );
+
+ // Update vertex arrays.
+ for (let j = 0, jl = polygon.vertexCount; j < jl; j++) {
+ Cartesian3.fromArray(cartesianArray, j * 3, cartesian);
+ EncodedCartesian3.fromCartesian(cartesian, encodedCartesian);
+
+ positionHighArray[vOffset * 3] = encodedCartesian.high.x;
+ positionHighArray[vOffset * 3 + 1] = encodedCartesian.high.y;
+ positionHighArray[vOffset * 3 + 2] = encodedCartesian.high.z;
+
+ positionLowArray[vOffset * 3] = encodedCartesian.low.x;
+ positionLowArray[vOffset * 3 + 1] = encodedCartesian.low.y;
+ positionLowArray[vOffset * 3 + 2] = encodedCartesian.low.z;
+
+ showAndColorArray[vOffset * 2] = show ? 1 : 0;
+ showAndColorArray[vOffset * 2 + 1] = encodedColor;
+
+ vOffset++;
+ }
+
+ polygon._dirty = false;
+ }
+ }
+
+ if (!defined(renderContext.vertexArray)) {
+ const { attributeArrays } = renderContext;
+
+ renderContext.vertexArray = new VertexArray({
+ context,
+
+ indexBuffer: Buffer.createIndexBuffer({
+ context,
+ typedArray: renderContext.indexArray,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ indexDatatype: IndexDatatype.fromTypedArray(renderContext.indexArray),
+ }),
+
+ attributes: [
+ {
+ index: BufferPolygonAttributeLocations.positionHigh,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.positionHigh,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPolygonAttributeLocations.positionLow,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.positionLow,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPolygonAttributeLocations.showAndColor,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 2,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.showAndColor,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ ],
+ });
+ } else if (collection._dirtyCount > 0) {
+ const { indexOffset, indexCount, vertexOffset, vertexCount } =
+ getPolygonDirtyRanges(collection);
+
+ renderContext.vertexArray.copyIndexFromRange(
+ renderContext.indexArray,
+ indexOffset,
+ indexCount,
+ );
+
+ for (const key in BufferPolygonAttributeLocations) {
+ if (Object.hasOwn(BufferPolygonAttributeLocations, key)) {
+ const attribute = /** @type {BufferPolygonAttribute} */ (key);
+ renderContext.vertexArray.copyAttributeFromRange(
+ BufferPolygonAttributeLocations[attribute],
+ renderContext.attributeArrays[attribute],
+ vertexOffset,
+ vertexCount,
+ );
+ }
+ }
+ }
+
+ if (!defined(renderContext.renderState)) {
+ renderContext.renderState = RenderState.fromCache({
+ blending: BlendingState.DISABLED,
+ depthTest: { enabled: true },
+ });
+ }
+
+ if (!defined(renderContext.shaderProgram)) {
+ renderContext.shaderProgram = ShaderProgram.fromCache({
+ context,
+ vertexShaderSource: new ShaderSource({
+ sources: [BufferPolygonCollectionVS],
+ }),
+ fragmentShaderSource: new ShaderSource({
+ sources: [BufferPolygonCollectionFS],
+ }),
+ attributeLocations: BufferPolygonAttributeLocations,
+ });
+ }
+
+ if (
+ !defined(renderContext.command) ||
+ isCommandDirty(collection, renderContext.command)
+ ) {
+ renderContext.command = new DrawCommand({
+ vertexArray: renderContext.vertexArray,
+ renderState: renderContext.renderState,
+ shaderProgram: renderContext.shaderProgram,
+ primitiveType: PrimitiveType.TRIANGLES,
+ pass: Pass.OPAQUE,
+ owner: collection,
+ count: collection.triangleCount * 3,
+ modelMatrix: collection.modelMatrix,
+ boundingVolume: collection.boundingVolumeWC,
+ debugShowBoundingVolume: collection.debugShowBoundingVolume,
+ });
+ }
+
+ frameState.commandList.push(renderContext.command);
+
+ collection._dirtyCount = 0;
+ collection._dirtyOffset = 0;
+
+ return renderContext;
+}
+
+/**
+ * Returns true if DrawCommand is out of date for the given collection.
+ * @param {BufferPolygonCollection} collection
+ * @param {DrawCommand} command
+ * @ignore
+ */
+function isCommandDirty(collection, command) {
+ const isModelMatrixEqual = Matrix4.equals(
+ collection.modelMatrix,
+ command._modelMatrix,
+ );
+
+ const isBoundingVolumeEqual = BoundingSphere.equals(
+ collection.boundingVolumeWC,
+ command._boundingVolume,
+ );
+
+ return (
+ collection.triangleCount * 3 !== command._count ||
+ collection.debugShowBoundingVolume !== command.debugShowBoundingVolume ||
+ !isModelMatrixEqual ||
+ !isBoundingVolumeEqual
+ );
+}
+
+/**
+ * Computes dirty ranges for attribute and index buffers in a collection.
+ * @param {BufferPolygonCollection} collection
+ * @ignore
+ */
+function getPolygonDirtyRanges(collection) {
+ const { _dirtyOffset, _dirtyCount } = collection;
+
+ collection.get(_dirtyOffset, polygon);
+ const vertexOffset = polygon.vertexOffset;
+ const indexOffset = polygon.triangleOffset * 3;
+
+ collection.get(_dirtyOffset + _dirtyCount - 1, polygon);
+ const vertexCount = polygon.vertexOffset + polygon.vertexCount - vertexOffset;
+ const indexCount =
+ (polygon.triangleOffset + polygon.triangleCount) * 3 - indexOffset;
+
+ return { indexOffset, indexCount, vertexOffset, vertexCount };
+}
+
+/**
+ * Destroys render context resources. Deleting properties from the context
+ * object isn't necessary, as collection.destroy() will discard the object.
+ * @ignore
+ */
+function destroyRenderContext() {
+ const context = /** @type {BufferPolygonRenderContext} */ (this);
+
+ if (defined(context.vertexArray)) {
+ context.vertexArray.destroy();
+ }
+
+ if (defined(context.shaderProgram)) {
+ context.shaderProgram.destroy();
+ }
+
+ if (defined(context.renderState)) {
+ RenderState.removeFromCache(context.renderState);
+ }
+}
+
+export default renderBufferPolygonCollection;
diff --git a/packages/engine/Source/Scene/renderBufferPolylineCollection.js b/packages/engine/Source/Scene/renderBufferPolylineCollection.js
new file mode 100644
index 000000000000..9ed20cff97d7
--- /dev/null
+++ b/packages/engine/Source/Scene/renderBufferPolylineCollection.js
@@ -0,0 +1,479 @@
+// @ts-check
+
+import defined from "../Core/defined.js";
+import Cartesian3 from "../Core/Cartesian3.js";
+import BufferPolyline from "./BufferPolyline.js";
+import Buffer from "../Renderer/Buffer.js";
+import BufferUsage from "../Renderer/BufferUsage.js";
+import VertexArray from "../Renderer/VertexArray.js";
+import ComponentDatatype from "../Core/ComponentDatatype.js";
+import RenderState from "../Renderer/RenderState.js";
+import BlendingState from "./BlendingState.js";
+import Color from "../Core/Color.js";
+import ShaderSource from "../Renderer/ShaderSource.js";
+import ShaderProgram from "../Renderer/ShaderProgram.js";
+import DrawCommand from "../Renderer/DrawCommand.js";
+import Pass from "../Renderer/Pass.js";
+import PrimitiveType from "../Core/PrimitiveType.js";
+import BufferPolylineCollectionVS from "../Shaders/BufferPolylineCollectionVS.js";
+import BufferPolylineCollectionFS from "../Shaders/BufferPolylineCollectionFS.js";
+import EncodedCartesian3 from "../Core/EncodedCartesian3.js";
+import AttributeCompression from "../Core/AttributeCompression.js";
+import IndexDatatype from "../Core/IndexDatatype.js";
+import PolylineCommon from "../Shaders/PolylineCommon.js";
+import Matrix4 from "../Core/Matrix4.js";
+import BoundingSphere from "../Core/BoundingSphere.js";
+
+/** @import FrameState from "./FrameState.js"; */
+/** @import BufferPolylineCollection from "./BufferPolylineCollection.js"; */
+/** @import {TypedArray} from "../Core/globalTypes.js"; */
+
+/**
+ * TODO(PR#13211): Need 'keyof' syntax to avoid duplicating attribute names.
+ * @typedef {'positionHigh' | 'positionLow' | 'prevPositionHigh' | 'prevPositionLow' | 'nextPositionHigh' | 'nextPositionLow' | 'showColorWidthAndTexCoord'} BufferPolylineAttribute
+ * @ignore
+ */
+
+/**
+ * @type {Record}
+ * @ignore
+ */
+const BufferPolylineAttributeLocations = {
+ positionHigh: 0,
+ positionLow: 1,
+ prevPositionHigh: 2,
+ prevPositionLow: 3,
+ nextPositionHigh: 4,
+ nextPositionLow: 5,
+ showColorWidthAndTexCoord: 6,
+};
+
+/**
+ * @typedef {object} BufferPolylineRenderContext
+ * @property {VertexArray} [vertexArray]
+ * @property {Record} [attributeArrays]
+ * @property {TypedArray} [indexArray]
+ * @property {RenderState} [renderState]
+ * @property {ShaderProgram} [shaderProgram]
+ * @property {DrawCommand} [command]
+ * @property {Function} destroy
+ * @ignore
+ */
+
+// Scratch variables.
+const polyline = new BufferPolyline();
+const color = new Color();
+const cartesian = new Cartesian3();
+const prevCartesian = new Cartesian3();
+const nextCartesian = new Cartesian3();
+const cartesianEnc = new EncodedCartesian3();
+const prevCartesianEnc = new EncodedCartesian3();
+const nextCartesianEnc = new EncodedCartesian3();
+
+/**
+ * Renders line segments as quads, each composed of two triangles. Writes each
+ * vertex twice, extruding the pairs in opposing directions outward.
+ *
+ * Tips:
+ * - # segments in polyline primitive = vertexCount - 1
+ * - # segments in collection = vertexCount - primitiveCount
+ * - # vertices rendered = vertexCount * 2
+ * - # indices = segmentCount * 6
+ *
+ * 0 - 2 - 4 - 6 - 8
+ * | \ | \ | \ | \ | ...
+ * 1 - 3 - 5 - 7 - 9
+ *
+ * @param {BufferPolylineCollection} collection
+ * @param {FrameState} frameState
+ * @param {BufferPolylineRenderContext} [renderContext]
+ * @returns {BufferPolylineRenderContext}
+ * @ignore
+ */
+function renderBufferPolylineCollection(collection, frameState, renderContext) {
+ const context = frameState.context;
+ renderContext = renderContext || { destroy: destroyRenderContext };
+
+ if (
+ !defined(renderContext.attributeArrays) ||
+ !defined(renderContext.indexArray)
+ ) {
+ // Number of primitives can only increase, which _decreases_ remaining
+ // segment capacity: use `primitiveCount` here, not `primitiveCountMax`.
+ const segmentCountMax =
+ collection.vertexCountMax - collection.primitiveCount;
+ const vertexCountMax = collection.vertexCountMax * 2;
+
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ renderContext.indexArray = IndexDatatype.createTypedArray(
+ vertexCountMax,
+ segmentCountMax * 6,
+ );
+
+ renderContext.attributeArrays = {
+ positionHigh: new Float32Array(vertexCountMax * 3),
+ positionLow: new Float32Array(vertexCountMax * 3),
+ prevPositionHigh: new Float32Array(vertexCountMax * 3),
+ prevPositionLow: new Float32Array(vertexCountMax * 3),
+ nextPositionHigh: new Float32Array(vertexCountMax * 3),
+ nextPositionLow: new Float32Array(vertexCountMax * 3),
+ showColorWidthAndTexCoord: new Float32Array(vertexCountMax * 4),
+ };
+ }
+
+ if (collection._dirtyCount > 0) {
+ const { _dirtyOffset, _dirtyCount } = collection;
+ const { attributeArrays } = renderContext;
+
+ const indexArray = renderContext.indexArray;
+ const positionHighArray = attributeArrays.positionHigh;
+ const positionLowArray = attributeArrays.positionLow;
+ const prevPositionHighArray = attributeArrays.prevPositionHigh;
+ const prevPositionLowArray = attributeArrays.prevPositionLow;
+ const nextPositionHighArray = attributeArrays.nextPositionHigh;
+ const nextPositionLowArray = attributeArrays.nextPositionLow;
+ const showColorWidthAndTexCoordArray =
+ attributeArrays.showColorWidthAndTexCoord;
+
+ for (let i = _dirtyOffset, il = _dirtyOffset + _dirtyCount; i < il; i++) {
+ collection.get(i, polyline);
+
+ if (!polyline._dirty) {
+ continue;
+ }
+
+ const cartesianArray = polyline.getPositions();
+ polyline.getColor(color);
+ const show = polyline.show;
+ const width = polyline.width;
+
+ let vOffset = polyline.vertexOffset * 2; // vertex offset
+ let iOffset = (polyline.vertexOffset - i) * 6; // index offset
+
+ for (let j = 0, jl = polyline.vertexCount; j < jl; j++) {
+ const isFirstSegment = j === 0;
+ const isLastSegment = j === jl - 1;
+
+ // For first/last vertices, infer missing vertices by mirroring the segment.
+ Cartesian3.fromArray(cartesianArray, j * 3, cartesian);
+ if (isFirstSegment) {
+ Cartesian3.fromArray(cartesianArray, (j + 1) * 3, nextCartesian);
+ Cartesian3.subtract(cartesian, nextCartesian, prevCartesian);
+ Cartesian3.add(cartesian, prevCartesian, prevCartesian);
+ } else if (isLastSegment) {
+ Cartesian3.fromArray(cartesianArray, (j - 1) * 3, prevCartesian);
+ Cartesian3.subtract(cartesian, prevCartesian, nextCartesian);
+ Cartesian3.add(cartesian, nextCartesian, nextCartesian);
+ } else {
+ Cartesian3.fromArray(cartesianArray, (j - 1) * 3, prevCartesian);
+ Cartesian3.fromArray(cartesianArray, (j + 1) * 3, nextCartesian);
+ }
+
+ // For each segment, draw two triangles.
+ if (!isLastSegment) {
+ indexArray[iOffset] = vOffset;
+ indexArray[iOffset + 1] = vOffset + 1;
+ indexArray[iOffset + 2] = vOffset + 2;
+
+ indexArray[iOffset + 3] = vOffset + 2;
+ indexArray[iOffset + 4] = vOffset + 1;
+ indexArray[iOffset + 5] = vOffset + 3;
+
+ iOffset += 6;
+ }
+
+ EncodedCartesian3.fromCartesian(cartesian, cartesianEnc);
+ EncodedCartesian3.fromCartesian(prevCartesian, prevCartesianEnc);
+ EncodedCartesian3.fromCartesian(nextCartesian, nextCartesianEnc);
+
+ const encodedColor = AttributeCompression.encodeRGB8(color);
+
+ // TODO(donmccurdy): Diverging from PolylineCollection.js, which writes
+ // internal vertices to buffer 4x, not 2x. Not sure that's needed?
+ for (let k = 0; k < 2; k++) {
+ // Position.
+ positionHighArray[vOffset * 3] = cartesianEnc.high.x;
+ positionHighArray[vOffset * 3 + 1] = cartesianEnc.high.y;
+ positionHighArray[vOffset * 3 + 2] = cartesianEnc.high.z;
+
+ positionLowArray[vOffset * 3] = cartesianEnc.low.x;
+ positionLowArray[vOffset * 3 + 1] = cartesianEnc.low.y;
+ positionLowArray[vOffset * 3 + 2] = cartesianEnc.low.z;
+
+ // Previous position.
+ prevPositionHighArray[vOffset * 3] = prevCartesianEnc.high.x;
+ prevPositionHighArray[vOffset * 3 + 1] = prevCartesianEnc.high.y;
+ prevPositionHighArray[vOffset * 3 + 2] = prevCartesianEnc.high.z;
+
+ prevPositionLowArray[vOffset * 3] = prevCartesianEnc.low.x;
+ prevPositionLowArray[vOffset * 3 + 1] = prevCartesianEnc.low.y;
+ prevPositionLowArray[vOffset * 3 + 2] = prevCartesianEnc.low.z;
+
+ // Next position.
+ nextPositionHighArray[vOffset * 3] = nextCartesianEnc.high.x;
+ nextPositionHighArray[vOffset * 3 + 1] = nextCartesianEnc.high.y;
+ nextPositionHighArray[vOffset * 3 + 2] = nextCartesianEnc.high.z;
+
+ nextPositionLowArray[vOffset * 3] = nextCartesianEnc.low.x;
+ nextPositionLowArray[vOffset * 3 + 1] = nextCartesianEnc.low.y;
+ nextPositionLowArray[vOffset * 3 + 2] = nextCartesianEnc.low.z;
+
+ // Properties.
+ showColorWidthAndTexCoordArray[vOffset * 4] = show ? 1 : 0;
+ showColorWidthAndTexCoordArray[vOffset * 4 + 1] = encodedColor;
+ showColorWidthAndTexCoordArray[vOffset * 4 + 2] = width;
+ showColorWidthAndTexCoordArray[vOffset * 4 + 3] = j / (jl - 1); // texcoord.s
+
+ vOffset++;
+ }
+ }
+
+ polyline._dirty = false;
+ }
+ }
+
+ if (!defined(renderContext.vertexArray)) {
+ const attributeArrays = renderContext.attributeArrays;
+
+ renderContext.vertexArray = new VertexArray({
+ context,
+
+ indexBuffer: Buffer.createIndexBuffer({
+ context,
+ typedArray: renderContext.indexArray,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ indexDatatype: IndexDatatype.fromTypedArray(renderContext.indexArray),
+ }),
+
+ attributes: [
+ {
+ index: BufferPolylineAttributeLocations.positionHigh,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.positionHigh,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPolylineAttributeLocations.positionLow,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.positionLow,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+
+ {
+ index: BufferPolylineAttributeLocations.prevPositionHigh,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.prevPositionHigh,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPolylineAttributeLocations.prevPositionLow,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.prevPositionLow,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+
+ {
+ index: BufferPolylineAttributeLocations.nextPositionHigh,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.nextPositionHigh,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ {
+ index: BufferPolylineAttributeLocations.nextPositionLow,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 3,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.nextPositionLow,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+
+ {
+ index: BufferPolylineAttributeLocations.showColorWidthAndTexCoord,
+ componentDatatype: ComponentDatatype.FLOAT,
+ componentsPerAttribute: 4,
+ vertexBuffer: Buffer.createVertexBuffer({
+ typedArray: attributeArrays.showColorWidthAndTexCoord,
+ context,
+ // @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
+ usage: BufferUsage.STATIC_DRAW,
+ }),
+ },
+ ],
+ });
+ } else if (collection._dirtyCount > 0) {
+ const { indexOffset, indexCount, vertexOffset, vertexCount } =
+ getPolylineDirtyRanges(collection);
+
+ renderContext.vertexArray.copyIndexFromRange(
+ renderContext.indexArray,
+ indexOffset,
+ indexCount,
+ );
+
+ for (const key in BufferPolylineAttributeLocations) {
+ if (Object.hasOwn(BufferPolylineAttributeLocations, key)) {
+ const attribute = /** @type {BufferPolylineAttribute} */ (key);
+ renderContext.vertexArray.copyAttributeFromRange(
+ BufferPolylineAttributeLocations[attribute],
+ renderContext.attributeArrays[attribute],
+ vertexOffset,
+ vertexCount,
+ );
+ }
+ }
+ }
+
+ if (!defined(renderContext.renderState)) {
+ renderContext.renderState = RenderState.fromCache({
+ blending: BlendingState.DISABLED,
+ depthTest: { enabled: true },
+ });
+ }
+
+ if (!defined(renderContext.shaderProgram)) {
+ renderContext.shaderProgram = ShaderProgram.fromCache({
+ context,
+ vertexShaderSource: new ShaderSource({
+ sources: [PolylineCommon, BufferPolylineCollectionVS],
+ }),
+ fragmentShaderSource: new ShaderSource({
+ sources: [BufferPolylineCollectionFS],
+ }),
+ attributeLocations: BufferPolylineAttributeLocations,
+ });
+ }
+
+ if (
+ !defined(renderContext.command) ||
+ isCommandDirty(collection, renderContext.command)
+ ) {
+ renderContext.command = new DrawCommand({
+ vertexArray: renderContext.vertexArray,
+ renderState: renderContext.renderState,
+ shaderProgram: renderContext.shaderProgram,
+ primitiveType: PrimitiveType.TRIANGLES,
+ pass: Pass.OPAQUE,
+ owner: collection,
+ count: getDrawIndexCount(collection),
+ modelMatrix: collection.modelMatrix,
+ boundingVolume: collection.boundingVolumeWC,
+ debugShowBoundingVolume: collection.debugShowBoundingVolume,
+ });
+ }
+
+ frameState.commandList.push(renderContext.command);
+
+ collection._dirtyCount = 0;
+ collection._dirtyOffset = 0;
+
+ return renderContext;
+}
+
+/**
+ * Returns true if DrawCommand is out of date for given collection.
+ * @param {BufferPolylineCollection} collection
+ * @param {DrawCommand} command
+ * @ignore
+ */
+function isCommandDirty(collection, command) {
+ const isModelMatrixEqual = Matrix4.equals(
+ collection.modelMatrix,
+ command._modelMatrix,
+ );
+
+ const isBoundingVolumeEqual = BoundingSphere.equals(
+ collection.boundingVolumeWC,
+ command._boundingVolume,
+ );
+
+ return (
+ getDrawIndexCount(collection) !== command._count ||
+ collection.debugShowBoundingVolume !== command.debugShowBoundingVolume ||
+ !isModelMatrixEqual ||
+ !isBoundingVolumeEqual
+ );
+}
+
+/**
+ * Returns number of drawn (not allocated) indices for given collection.
+ * @param {BufferPolylineCollection} collection
+ * @ignore
+ */
+function getDrawIndexCount(collection) {
+ return (collection.vertexCount - collection.primitiveCount) * 6;
+}
+
+/**
+ * Computes dirty ranges for attribute and index buffers in a collection.
+ * @param {BufferPolylineCollection} collection
+ * @ignore
+ */
+function getPolylineDirtyRanges(collection) {
+ const { _dirtyOffset, _dirtyCount } = collection;
+
+ collection.get(_dirtyOffset, polyline);
+ const vertexOffset = polyline.vertexOffset * 2;
+ const segmentOffset = vertexOffset - _dirtyOffset;
+ const indexOffset = segmentOffset * 6;
+
+ collection.get(_dirtyOffset + _dirtyCount - 1, polyline);
+ const vertexCount =
+ (polyline.vertexOffset + polyline.vertexCount) * 2 - vertexOffset;
+ const segmentCount = vertexCount / 2 - _dirtyCount;
+ const indexCount = segmentCount * 6;
+
+ return { indexOffset, indexCount, vertexOffset, vertexCount };
+}
+
+/**
+ * Destroys render context resources. Deleting properties from the context
+ * object isn't necessary, as collection.destroy() will discard the object.
+ * @ignore
+ */
+function destroyRenderContext() {
+ const context = /** @type {BufferPolylineRenderContext} */ (this);
+
+ if (defined(context.vertexArray)) {
+ context.vertexArray.destroy();
+ }
+
+ if (defined(context.shaderProgram)) {
+ context.shaderProgram.destroy();
+ }
+
+ if (defined(context.renderState)) {
+ RenderState.removeFromCache(context.renderState);
+ }
+}
+
+export default renderBufferPolylineCollection;
diff --git a/packages/engine/Source/Shaders/BufferPointCollectionFS.glsl b/packages/engine/Source/Shaders/BufferPointCollectionFS.glsl
new file mode 100644
index 000000000000..96fb7eb18446
--- /dev/null
+++ b/packages/engine/Source/Shaders/BufferPointCollectionFS.glsl
@@ -0,0 +1,26 @@
+in vec4 v_color;
+in vec4 v_outlineColor;
+in float v_innerRadiusFrac;
+
+void main()
+{
+ // Distance between fragment and point center, 0 to 0.5.
+ float distanceToCenter = length(gl_PointCoord - vec2(0.5));
+ float delta = fwidth(distanceToCenter);
+
+ float outerLimit = 0.5;
+ float innerLimit = 0.5 * v_innerRadiusFrac;
+
+ float outerAlpha = 1.0 - smoothstep(max(0.0, outerLimit - delta), outerLimit, distanceToCenter);
+ float innerAlpha = 1.0 - smoothstep(innerLimit - delta, innerLimit, distanceToCenter);
+
+ vec4 color = vec4(mix(v_outlineColor.rgb, v_color.rgb, innerAlpha), outerAlpha);
+
+ if (color.a < 0.005) // matches 0/255 and 1/255
+ {
+ discard;
+ }
+
+ out_FragColor = czm_gammaCorrect(color);
+ czm_writeLogDepth();
+}
diff --git a/packages/engine/Source/Shaders/BufferPointCollectionVS.glsl b/packages/engine/Source/Shaders/BufferPointCollectionVS.glsl
new file mode 100644
index 000000000000..bf99bb54bf15
--- /dev/null
+++ b/packages/engine/Source/Shaders/BufferPointCollectionVS.glsl
@@ -0,0 +1,44 @@
+in vec3 positionHigh;
+in vec3 positionLow;
+in vec3 showPixelSizeAndColor;
+in vec2 outlineWidthAndOutlineColor;
+
+out vec4 v_color;
+out vec4 v_outlineColor;
+out float v_innerRadiusFrac;
+
+void main()
+{
+ // Unpack attributes.
+ float show = showPixelSizeAndColor.x;
+ float pixelSize = showPixelSizeAndColor.y;
+ vec4 color = czm_decodeRGB8(showPixelSizeAndColor.z);
+ float outlineWidth = outlineWidthAndOutlineColor.x;
+ vec4 outlineColor = czm_decodeRGB8(outlineWidthAndOutlineColor.y);
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ float innerRadius = 0.5 * pixelSize * czm_pixelRatio;
+ float outerRadius = (0.5 * pixelSize + outlineWidth) * czm_pixelRatio;
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ vec4 p = czm_translateRelativeToEye(positionHigh, positionLow);
+ vec4 positionEC = czm_modelViewRelativeToEye * p;
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ gl_Position = czm_projection * positionEC;
+ czm_vertexLogDepth();
+
+ v_color = color;
+ v_color.a *= show;
+
+ v_outlineColor = outlineColor;
+ v_outlineColor.a *= show;
+
+ v_innerRadiusFrac = innerRadius / outerRadius;
+
+ gl_PointSize = 2.0 * outerRadius * show;
+ gl_Position *= show;
+}
diff --git a/packages/engine/Source/Shaders/BufferPolygonCollectionFS.glsl b/packages/engine/Source/Shaders/BufferPolygonCollectionFS.glsl
new file mode 100644
index 000000000000..a37692f77041
--- /dev/null
+++ b/packages/engine/Source/Shaders/BufferPolygonCollectionFS.glsl
@@ -0,0 +1,12 @@
+in vec4 v_color;
+
+void main()
+{
+ if (v_color.a < 0.005) // matches 0/255 and 1/255
+ {
+ discard;
+ }
+
+ out_FragColor = czm_gammaCorrect(v_color);
+ czm_writeLogDepth();
+}
diff --git a/packages/engine/Source/Shaders/BufferPolygonCollectionVS.glsl b/packages/engine/Source/Shaders/BufferPolygonCollectionVS.glsl
new file mode 100644
index 000000000000..6a909bd05ba6
--- /dev/null
+++ b/packages/engine/Source/Shaders/BufferPolygonCollectionVS.glsl
@@ -0,0 +1,26 @@
+in vec3 positionHigh;
+in vec3 positionLow;
+in vec2 showAndColor;
+
+out vec4 v_color;
+
+void main()
+{
+ float show = showAndColor.x;
+ vec4 color = czm_decodeRGB8(showAndColor.y);
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ vec4 p = czm_translateRelativeToEye(positionHigh, positionLow);
+ vec4 positionEC = czm_modelViewRelativeToEye * p;
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ gl_Position = czm_projection * positionEC;
+ czm_vertexLogDepth();
+
+ v_color = color;
+ v_color.a *= show;
+
+ gl_Position *= show;
+}
diff --git a/packages/engine/Source/Shaders/BufferPolylineCollectionFS.glsl b/packages/engine/Source/Shaders/BufferPolylineCollectionFS.glsl
new file mode 100644
index 000000000000..a37692f77041
--- /dev/null
+++ b/packages/engine/Source/Shaders/BufferPolylineCollectionFS.glsl
@@ -0,0 +1,12 @@
+in vec4 v_color;
+
+void main()
+{
+ if (v_color.a < 0.005) // matches 0/255 and 1/255
+ {
+ discard;
+ }
+
+ out_FragColor = czm_gammaCorrect(v_color);
+ czm_writeLogDepth();
+}
diff --git a/packages/engine/Source/Shaders/BufferPolylineCollectionVS.glsl b/packages/engine/Source/Shaders/BufferPolylineCollectionVS.glsl
new file mode 100644
index 000000000000..373c2f15b6c2
--- /dev/null
+++ b/packages/engine/Source/Shaders/BufferPolylineCollectionVS.glsl
@@ -0,0 +1,45 @@
+in vec3 positionHigh;
+in vec3 positionLow;
+in vec3 prevPositionHigh;
+in vec3 prevPositionLow;
+in vec3 nextPositionHigh;
+in vec3 nextPositionLow;
+in vec4 showColorWidthAndTexCoord;
+
+out vec4 v_color;
+out vec2 v_st;
+out float v_width;
+out float v_polylineAngle;
+
+void main()
+{
+ float show = showColorWidthAndTexCoord.x;
+ vec4 color = czm_decodeRGB8(showColorWidthAndTexCoord.y);
+ float width = showColorWidthAndTexCoord.z;
+ float texCoord = showColorWidthAndTexCoord.w;
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ bool usePrevious = texCoord == 1.0;
+ float expandDir = gl_VertexID % 2 == 1 ? 1.0 : -1.0;
+ float polylineAngle;
+
+ vec4 positionEC = czm_translateRelativeToEye(positionHigh, positionLow);
+ vec4 prevPositionEC = czm_translateRelativeToEye(prevPositionHigh, prevPositionLow);
+ vec4 nextPositionEC = czm_translateRelativeToEye(nextPositionHigh, nextPositionLow);
+
+ vec4 positionWC = getPolylineWindowCoordinates(positionEC, prevPositionEC, nextPositionEC, expandDir, width, usePrevious, polylineAngle);
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ gl_Position = czm_viewportOrthographic * positionWC * show;
+
+ v_color = color;
+ v_color.a *= show;
+
+ v_st.s = texCoord;
+ v_st.t = czm_writeNonPerspective(clamp(expandDir, 0.0, 1.0), gl_Position.w);
+
+ v_width = width;
+ v_polylineAngle = polylineAngle;
+}
diff --git a/packages/engine/Specs/Scene/BufferPointCollectionSpec.js b/packages/engine/Specs/Scene/BufferPointCollectionSpec.js
index 24e44b542ad4..b76185dd4170 100644
--- a/packages/engine/Specs/Scene/BufferPointCollectionSpec.js
+++ b/packages/engine/Specs/Scene/BufferPointCollectionSpec.js
@@ -6,7 +6,7 @@ import {
BufferPointCollection,
} from "../../index.js";
-describe("BufferPointCollection", () => {
+describe("Scene/BufferPointCollection", () => {
const position = new Cartesian3();
const color = new Color();
diff --git a/packages/engine/Specs/Scene/BufferPolygonCollectionSpec.js b/packages/engine/Specs/Scene/BufferPolygonCollectionSpec.js
index 65f169966e64..002471b3554d 100644
--- a/packages/engine/Specs/Scene/BufferPolygonCollectionSpec.js
+++ b/packages/engine/Specs/Scene/BufferPolygonCollectionSpec.js
@@ -6,7 +6,7 @@ import {
BufferPolygonCollection,
} from "../../index.js";
-describe("BufferPolygonCollection", () => {
+describe("Scene/BufferPolygonCollection", () => {
const color = new Color();
it("featureId", () => {
@@ -168,7 +168,7 @@ describe("BufferPolygonCollection", () => {
triangleCountMax: 1,
});
- expect(collection.byteLength).toBe(36 + 72 + 12);
+ expect(collection.byteLength).toBe(36 + 72 + 6);
collection = new BufferPolygonCollection({
primitiveCountMax: 128,
@@ -177,7 +177,7 @@ describe("BufferPolygonCollection", () => {
triangleCountMax: 1024,
});
- expect(collection.byteLength).toBe(4608 + 24576 + 512 + 12288);
+ expect(collection.byteLength).toBe(4608 + 24576 + 256 + 6144);
});
it("clone", () => {
diff --git a/packages/engine/Specs/Scene/BufferPolylineCollectionSpec.js b/packages/engine/Specs/Scene/BufferPolylineCollectionSpec.js
index dea856dd2446..db974572ed22 100644
--- a/packages/engine/Specs/Scene/BufferPolylineCollectionSpec.js
+++ b/packages/engine/Specs/Scene/BufferPolylineCollectionSpec.js
@@ -6,7 +6,7 @@ import {
BufferPolylineCollection,
} from "../../index.js";
-describe("BufferPolylineCollection", () => {
+describe("Scene/BufferPolylineCollection", () => {
const color = new Color();
it("featureId", () => {
diff --git a/packages/engine/Specs/Scene/renderBufferPointCollectionSpec.js b/packages/engine/Specs/Scene/renderBufferPointCollectionSpec.js
new file mode 100644
index 000000000000..cf74021882b5
--- /dev/null
+++ b/packages/engine/Specs/Scene/renderBufferPointCollectionSpec.js
@@ -0,0 +1,123 @@
+import {
+ BufferPoint,
+ BufferPointCollection,
+ Camera,
+ Cartesian3,
+ Color,
+ SceneMode,
+} from "../../index.js";
+
+import createScene from "../../../../Specs/createScene.js";
+
+describe(
+ "Scene/renderBufferPointCollection",
+ () => {
+ let scene;
+ let collection;
+
+ beforeAll(function () {
+ scene = createScene();
+ scene.primitives.destroyPrimitives = false;
+ });
+
+ afterAll(function () {
+ scene.destroyForSpecs();
+ });
+
+ beforeEach(function () {
+ collection = new BufferPointCollection();
+ scene.mode = SceneMode.SCENE3D;
+ scene.camera = new Camera(scene);
+ });
+
+ afterEach(function () {
+ scene.primitives.removeAll();
+ if (!collection.isDestroyed()) {
+ collection.destroy();
+ }
+ });
+
+ it("renders points", function () {
+ const point = new BufferPoint();
+ collection.add({ position: new Cartesian3(0, -1000, 0) }, point);
+
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+ });
+
+ it("renders points with color", function () {
+ const point = new BufferPoint();
+ const color = Color.RED;
+ collection.add({ position: new Cartesian3(0, -1000, 0), color }, point);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 0, 0, 255]);
+
+ point.setColor(Color.GREEN);
+ expect(scene).toRender([0, 128, 0, 255]);
+ });
+
+ it("renders points with updated positions", function () {
+ const point = new BufferPoint();
+ collection.add({ position: new Cartesian3(0, 0, 0) }, point);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ point.setPosition(new Cartesian3(0, -1000, 0));
+ expect(scene).toRender([255, 255, 255, 255]);
+ });
+
+ it("renders points with sort order", function () {
+ const point = new BufferPoint();
+
+ collection.add({ position: new Cartesian3(0, -1000, 0) }, point);
+ point.setColor(Color.RED);
+
+ collection.add({ position: new Cartesian3(0, -1000, 0) }, point);
+ point.setColor(Color.BLUE);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 0, 0, 255]);
+
+ const colorA = new Color();
+ const colorB = new Color();
+ collection.sort((a, b) =>
+ a.getColor(colorA).blue > b.getColor(colorB).blue ? -1 : 1,
+ );
+ expect(scene).toRender([0, 0, 255, 255]);
+ });
+
+ it("does not render if empty", function () {
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+
+ it("does not render if collection.show = false", function () {
+ const point = new BufferPoint();
+ collection.add({ position: new Cartesian3(0, -1000, 0) }, point);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+
+ collection.show = false;
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+
+ it("does not render if point.show = false", function () {
+ const point = new BufferPoint();
+ collection.add({ position: new Cartesian3(0, -1000, 0) }, point);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+
+ point.show = false;
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+ },
+ "WebGL",
+);
diff --git a/packages/engine/Specs/Scene/renderBufferPolygonCollectionSpec.js b/packages/engine/Specs/Scene/renderBufferPolygonCollectionSpec.js
new file mode 100644
index 000000000000..91fa807efd7a
--- /dev/null
+++ b/packages/engine/Specs/Scene/renderBufferPolygonCollectionSpec.js
@@ -0,0 +1,144 @@
+import {
+ BufferPolygon,
+ BufferPolygonCollection,
+ Camera,
+ Cartesian3,
+ Color,
+ ComponentDatatype,
+ SceneMode,
+} from "../../index.js";
+
+import createScene from "../../../../Specs/createScene.js";
+
+describe(
+ "Scene/renderBufferPolygonCollection",
+ () => {
+ let scene;
+ let collection;
+
+ // prettier-ignore
+ const positions = new Int32Array([
+ -1000, -1000, -1000,
+ -1000, -1000, +2000,
+ -1000, +2000, -1000,
+ ]);
+
+ const triangles = new Uint16Array([0, 1, 2]);
+
+ beforeAll(function () {
+ scene = createScene();
+ scene.primitives.destroyPrimitives = false;
+ });
+
+ afterAll(function () {
+ scene.destroyForSpecs();
+ });
+
+ beforeEach(function () {
+ collection = new BufferPolygonCollection({
+ positionDatatype: ComponentDatatype.INT,
+ });
+ scene.mode = SceneMode.SCENE3D;
+ scene.camera = new Camera(scene);
+ scene.camera.position = new Cartesian3(10.0, 0.0, 0.0);
+ scene.camera.direction = new Cartesian3(-1, 0, 0);
+ scene.camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);
+ });
+
+ afterEach(function () {
+ scene.primitives.removeAll();
+ if (!collection.isDestroyed()) {
+ collection.destroy();
+ }
+ });
+
+ it("renders polygons", function () {
+ const polygon = new BufferPolygon();
+ collection.add({ positions, triangles }, polygon);
+
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+ });
+
+ it("renders polygons with color", function () {
+ const polygon = new BufferPolygon();
+ collection.add({ positions, triangles, color: Color.RED }, polygon);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 0, 0, 255]);
+
+ polygon.setColor(Color.GREEN);
+ expect(scene).toRender([0, 128, 0, 255]);
+ });
+
+ it("renders polygons with updated positions", function () {
+ // prettier-ignore
+ const badPositions = new Int32Array([
+ -1000, +1000, -1000,
+ -1000, +1000, +2000,
+ -1000, +3000, -1000,
+ ]);
+
+ const polygon = new BufferPolygon();
+ collection.add({ positions: badPositions, triangles }, polygon);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ polygon.setPositions(positions);
+ expect(scene).toRender([255, 255, 255, 255]);
+ });
+
+ it("renders polygons with sort order", function () {
+ const polygon = new BufferPolygon();
+
+ collection.add({ positions, triangles }, polygon);
+ polygon.setColor(Color.RED);
+
+ collection.add({ positions, triangles }, polygon);
+ polygon.setColor(Color.BLUE);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 0, 0, 255]);
+
+ const colorA = new Color();
+ const colorB = new Color();
+ collection.sort((a, b) =>
+ a.getColor(colorA).blue > b.getColor(colorB).blue ? -1 : 1,
+ );
+ expect(scene).toRender([0, 0, 255, 255]);
+ });
+
+ it("does not render if empty", function () {
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+
+ it("does not render if collection.show = false", function () {
+ const polygon = new BufferPolygon();
+ collection.add({ positions, triangles }, polygon);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+
+ collection.show = false;
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+
+ it("does not render if polygon.show = false", function () {
+ const polygon = new BufferPolygon();
+ collection.add({ positions, triangles }, polygon);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+
+ polygon.show = false;
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+ },
+ "WebGL",
+);
diff --git a/packages/engine/Specs/Scene/renderBufferPolylineCollectionSpec.js b/packages/engine/Specs/Scene/renderBufferPolylineCollectionSpec.js
new file mode 100644
index 000000000000..cb9a7f110a8e
--- /dev/null
+++ b/packages/engine/Specs/Scene/renderBufferPolylineCollectionSpec.js
@@ -0,0 +1,130 @@
+import {
+ BufferPolyline,
+ BufferPolylineCollection,
+ Camera,
+ Color,
+ ComponentDatatype,
+ SceneMode,
+} from "../../index.js";
+
+import createScene from "../../../../Specs/createScene.js";
+
+describe(
+ "Scene/renderBufferPolylineCollection",
+ () => {
+ let scene;
+ let collection;
+
+ beforeAll(function () {
+ scene = createScene();
+ scene.primitives.destroyPrimitives = false;
+ });
+
+ afterAll(function () {
+ scene.destroyForSpecs();
+ });
+
+ beforeEach(function () {
+ collection = new BufferPolylineCollection({
+ positionDatatype: ComponentDatatype.INT,
+ });
+ scene.mode = SceneMode.SCENE3D;
+ scene.camera = new Camera(scene);
+ });
+
+ afterEach(function () {
+ scene.primitives.removeAll();
+ if (!collection.isDestroyed()) {
+ collection.destroy();
+ }
+ });
+
+ it("renders polylines", function () {
+ const line = new BufferPolyline();
+ const positions = new Int32Array([0, -1000000, 0, 0, +1000000, 0]);
+ collection.add({ positions }, line);
+
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+ });
+
+ it("renders polylines with color", function () {
+ const line = new BufferPolyline();
+ const positions = new Int32Array([0, -1000000, 0, 0, +1000000, 0]);
+ collection.add({ positions, color: Color.RED }, line);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 0, 0, 255]);
+
+ line.setColor(Color.GREEN);
+ expect(scene).toRender([0, 128, 0, 255]);
+ });
+
+ it("renders polylines with updated positions", function () {
+ const line = new BufferPolyline();
+ const positions = new Int32Array([0, +5000, 0, 0, +1000000, 0]);
+ collection.add({ positions }, line);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ line.setPositions(new Int32Array([0, -1000000, 0, 0, +1000000, 0]));
+ expect(scene).toRender([255, 255, 255, 255]);
+ });
+
+ it("renders polylines with sort order", function () {
+ const line = new BufferPolyline();
+ const positions = new Int32Array([0, -1000000, 0, 0, +1000000, 0]);
+
+ collection.add({ positions }, line);
+ line.setColor(Color.RED);
+
+ collection.add({ positions }, line);
+ line.setColor(Color.BLUE);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 0, 0, 255]);
+
+ const colorA = new Color();
+ const colorB = new Color();
+ collection.sort((a, b) =>
+ a.getColor(colorA).blue > b.getColor(colorB).blue ? -1 : 1,
+ );
+ expect(scene).toRender([0, 0, 255, 255]);
+ });
+
+ it("does not render if empty", function () {
+ expect(scene).toRender([0, 0, 0, 255]);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+
+ it("does not render if collection.show = false", function () {
+ const line = new BufferPolyline();
+ const positions = new Int32Array([0, -1000000, 0, 0, +1000000, 0]);
+ collection.add({ positions }, line);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+
+ collection.show = false;
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+
+ it("does not render if polyline.show = false", function () {
+ const line = new BufferPolyline();
+ const positions = new Int32Array([0, -1000000, 0, 0, +1000000, 0]);
+ collection.add({ positions }, line);
+
+ scene.primitives.add(collection);
+ expect(scene).toRender([255, 255, 255, 255]);
+
+ line.show = false;
+ expect(scene).toRender([0, 0, 0, 255]);
+ });
+ },
+ "WebGL",
+);