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", +);