diff --git a/Source/Core/decodeVectorPolylinePositions.js b/Source/Core/decodeVectorPolylinePositions.js new file mode 100644 index 000000000000..f0df3a05571f --- /dev/null +++ b/Source/Core/decodeVectorPolylinePositions.js @@ -0,0 +1,51 @@ +import AttributeCompression from "./AttributeCompression.js"; +import Cartesian3 from "./Cartesian3.js"; +import Cartographic from "./Cartographic.js"; +import CesiumMath from "./Math.js"; + +var maxShort = 32767; + +var scratchBVCartographic = new Cartographic(); +var scratchEncodedPosition = new Cartesian3(); + +function decodeVectorPolylinePositions( + positions, + rectangle, + minimumHeight, + maximumHeight, + ellipsoid +) { + var positionsLength = positions.length / 3; + var uBuffer = positions.subarray(0, positionsLength); + var vBuffer = positions.subarray(positionsLength, 2 * positionsLength); + var heightBuffer = positions.subarray( + 2 * positionsLength, + 3 * positionsLength + ); + AttributeCompression.zigZagDeltaDecode(uBuffer, vBuffer, heightBuffer); + + var decoded = new Float64Array(positions.length); + for (var i = 0; i < positionsLength; ++i) { + var u = uBuffer[i]; + var v = vBuffer[i]; + var h = heightBuffer[i]; + + var lon = CesiumMath.lerp(rectangle.west, rectangle.east, u / maxShort); + var lat = CesiumMath.lerp(rectangle.south, rectangle.north, v / maxShort); + var alt = CesiumMath.lerp(minimumHeight, maximumHeight, h / maxShort); + + var cartographic = Cartographic.fromRadians( + lon, + lat, + alt, + scratchBVCartographic + ); + var decodedPosition = ellipsoid.cartographicToCartesian( + cartographic, + scratchEncodedPosition + ); + Cartesian3.pack(decodedPosition, decoded, i * 3); + } + return decoded; +} +export default decodeVectorPolylinePositions; diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index 488c0190713f..59175afb73a6 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -47,6 +47,10 @@ function Batched3DModel3DTileContent( this._batchTable = undefined; this._features = undefined; + this._classificationType = tileset.vectorClassificationOnly + ? undefined + : tileset.classificationType; + // Populate from gltf when available this._batchIdAttributeName = undefined; this._diffuseAttributeOrUniformName = {}; @@ -161,7 +165,7 @@ function getBatchIdAttributeName(gltf) { function getVertexShaderCallback(content) { return function (vs, programId) { var batchTable = content._batchTable; - var handleTranslucent = !defined(content._tileset.classificationType); + var handleTranslucent = !defined(content._classificationType); var gltf = content._model.gltf; if (defined(gltf)) { @@ -183,7 +187,7 @@ function getVertexShaderCallback(content) { function getFragmentShaderCallback(content) { return function (fs, programId) { var batchTable = content._batchTable; - var handleTranslucent = !defined(content._tileset.classificationType); + var handleTranslucent = !defined(content._classificationType); var gltf = content._model.gltf; if (defined(gltf)) { @@ -193,7 +197,8 @@ function getFragmentShaderCallback(content) { } var callback = batchTable.getFragmentShaderCallback( handleTranslucent, - content._diffuseAttributeOrUniformName[programId] + content._diffuseAttributeOrUniformName[programId], + false ); return defined(callback) ? callback(fs) : fs; }; @@ -348,7 +353,7 @@ function initialize(content, arrayBuffer, byteOffset) { } var colorChangedCallback; - if (defined(tileset.classificationType)) { + if (defined(content._classificationType)) { colorChangedCallback = createColorChangedCallback(content); } @@ -403,7 +408,7 @@ function initialize(content, arrayBuffer, byteOffset) { new Matrix4() ); - if (!defined(tileset.classificationType)) { + if (!defined(content._classificationType)) { // PERFORMANCE_IDEA: patch the shader on demand, e.g., the first time show/color changes. // The pick shader still needs to be patched. content._model = new Model({ @@ -571,7 +576,7 @@ Batched3DModel3DTileContent.prototype.update = function (tileset, frameState) { if ( commandStart < commandEnd && (frameState.passes.render || frameState.passes.pick) && - !defined(tileset.classificationType) + !defined(this._classificationType) ) { this._batchTable.addDerivedCommands(frameState, commandStart); } diff --git a/Source/Scene/Cesium3DTileBatchTable.js b/Source/Scene/Cesium3DTileBatchTable.js index 90255e28fa2d..1e2591e8f38b 100644 --- a/Source/Scene/Cesium3DTileBatchTable.js +++ b/Source/Scene/Cesium3DTileBatchTable.js @@ -1195,7 +1195,8 @@ function modifyDiffuse(source, diffuseAttributeOrUniformName, applyHighlight) { Cesium3DTileBatchTable.prototype.getFragmentShaderCallback = function ( handleTranslucent, - diffuseAttributeOrUniformName + diffuseAttributeOrUniformName, + hasPremultipliedAlpha ) { if (this.featuresLength === 0) { return; @@ -1210,8 +1211,13 @@ Cesium3DTileBatchTable.prototype.getFragmentShaderCallback = function ( "varying vec4 tile_featureColor; \n" + "void main() \n" + "{ \n" + - " tile_color(tile_featureColor); \n" + - "}"; + " tile_color(tile_featureColor); \n"; + + if (hasPremultipliedAlpha) { + source += " gl_FragColor.rgb *= gl_FragColor.a; \n"; + } + + source += "}"; } else { if (handleTranslucent) { source += "uniform bool tile_translucentCommand; \n"; @@ -1246,7 +1252,13 @@ Cesium3DTileBatchTable.prototype.getFragmentShaderCallback = function ( " } \n"; } - source += " tile_color(featureProperties); \n" + "} \n"; + source += " tile_color(featureProperties); \n"; + + if (hasPremultipliedAlpha) { + source += " gl_FragColor.rgb *= gl_FragColor.a; \n"; + } + + source += "} \n"; } return source; }; @@ -1268,6 +1280,7 @@ Cesium3DTileBatchTable.prototype.getClassificationFragmentShaderCallback = funct "{ \n" + " tile_main(); \n" + " gl_FragColor = tile_featureColor; \n" + + " gl_FragColor.rgb *= gl_FragColor.a; \n" + "}"; } else { source += @@ -1282,6 +1295,7 @@ Cesium3DTileBatchTable.prototype.getClassificationFragmentShaderCallback = funct " discard; \n" + " } \n" + " gl_FragColor = featureProperties; \n" + + " gl_FragColor.rgb *= gl_FragColor.a; \n" + "} \n"; } return source; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 19094051d8f9..2eef8fd6a5d5 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -90,6 +90,7 @@ import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; * @param {Cartesian3[]} [options.sphericalHarmonicCoefficients] The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. * @param {String} [options.specularEnvironmentMaps] A URL to a KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. * @param {Boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled. + * @param {Boolean} [options.vectorClassificationOnly=false] Indicates that only the tileset's vector tiles should be used for classification. * @param {String} [options.debugHeatmapTilePropertyName] The tile variable to colorize as a heatmap. All rendered tiles will be colorized relative to each other's specified variable value. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. @@ -273,6 +274,11 @@ function Cesium3DTileset(options) { this._clippingPlanesOriginMatrix = undefined; // Combines the above with any run-time transforms. this._clippingPlanesOriginMatrixDirty = true; + this._vectorClassificationOnly = defaultValue( + options.vectorClassificationOnly, + false + ); + /** * Preload tiles when tileset.show is false. Loads tiles as if the tileset is visible but does not render them. * @@ -898,6 +904,15 @@ function Cesium3DTileset(options) { */ this.debugShowUrl = defaultValue(options.debugShowUrl, false); + /** + * Function for examining vector lines as they are being streamed. + * + * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. + * + * @type {Function} + */ + this.examineVectorLinesFunction = undefined; + var that = this; var resource; when(options.url) @@ -1654,6 +1669,22 @@ Object.defineProperties(Cesium3DTileset.prototype, { Cartesian2.clone(value, this._imageBasedLightingFactor); }, }, + + /** + * Indicates that only the tileset's vector tiles should be used for classification. + * + * @memberof Cesium3DTileset.prototype + * + * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. + * + * @type {Boolean} + * @default false + */ + vectorClassificationOnly: { + get: function () { + return this._vectorClassificationOnly; + }, + }, }); /** diff --git a/Source/Scene/ModelInstanceCollection.js b/Source/Scene/ModelInstanceCollection.js index a94956523945..0876cea956c1 100644 --- a/Source/Scene/ModelInstanceCollection.js +++ b/Source/Scene/ModelInstanceCollection.js @@ -455,7 +455,8 @@ function getFragmentShaderCallback(collection) { ); fs = batchTable.getFragmentShaderCallback( true, - diffuseAttributeOrUniformName + diffuseAttributeOrUniformName, + false )(fs); } else { fs = "varying vec4 v_pickColor;\n" + fs; @@ -536,7 +537,8 @@ function getFragmentShaderNonInstancedCallback(collection) { ); fs = batchTable.getFragmentShaderCallback( true, - diffuseAttributeOrUniformName + diffuseAttributeOrUniformName, + false )(fs); } else { fs = "uniform vec4 czm_pickColor;\n" + fs; diff --git a/Source/Scene/PointCloud3DTileContent.js b/Source/Scene/PointCloud3DTileContent.js index 50ec733be009..e45056fccc72 100644 --- a/Source/Scene/PointCloud3DTileContent.js +++ b/Source/Scene/PointCloud3DTileContent.js @@ -153,7 +153,8 @@ function getFragmentShaderLoaded(content) { if (defined(content._batchTable)) { return content._batchTable.getFragmentShaderCallback( false, - undefined + undefined, + false )(fs); } return "uniform vec4 czm_pickColor;\n" + fs; diff --git a/Source/Scene/Vector3DTileClampedPolylines.js b/Source/Scene/Vector3DTileClampedPolylines.js new file mode 100644 index 000000000000..abf0142fb5d0 --- /dev/null +++ b/Source/Scene/Vector3DTileClampedPolylines.js @@ -0,0 +1,738 @@ +import ApproximateTerrainHeights from "../Core/ApproximateTerrainHeights.js"; +import arraySlice from "../Core/arraySlice.js"; +import Cartesian2 from "../Core/Cartesian2.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import Color from "../Core/Color.js"; +import ComponentDatatype from "../Core/ComponentDatatype.js"; +import defaultValue from "../Core/defaultValue.js"; +import defined from "../Core/defined.js"; +import destroyObject from "../Core/destroyObject.js"; +import Ellipsoid from "../Core/Ellipsoid.js"; +import FeatureDetection from "../Core/FeatureDetection.js"; +import IndexDatatype from "../Core/IndexDatatype.js"; +import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; +import Matrix4 from "../Core/Matrix4.js"; +import Rectangle from "../Core/Rectangle.js"; +import TaskProcessor from "../Core/TaskProcessor.js"; +import Buffer from "../Renderer/Buffer.js"; +import BufferUsage from "../Renderer/BufferUsage.js"; +import DrawCommand from "../Renderer/DrawCommand.js"; +import Pass from "../Renderer/Pass.js"; +import RenderState from "../Renderer/RenderState.js"; +import ShaderProgram from "../Renderer/ShaderProgram.js"; +import ShaderSource from "../Renderer/ShaderSource.js"; +import VertexArray from "../Renderer/VertexArray.js"; +import PolylineCommon from "../Shaders/PolylineCommon.js"; +import Vector3DTileClampedPolylinesVS from "../Shaders/Vector3DTileClampedPolylinesVS.js"; +import Vector3DTileClampedPolylinesFS from "../Shaders/Vector3DTileClampedPolylinesFS.js"; +import when from "../ThirdParty/when.js"; +import BlendingState from "./BlendingState.js"; +import Cesium3DTileFeature from "./Cesium3DTileFeature.js"; +import ClassificationType from "./ClassificationType.js"; +import CullFace from "./CullFace.js"; +import StencilConstants from "./StencilConstants.js"; +import StencilFunction from "./StencilFunction.js"; +import StencilOperation from "./StencilOperation.js"; + +/** + * Creates a batch of polylines as volumes with shader-adjustable width. + * + * @alias Vector3DTileClampedPolylines + * @constructor + * + * @param {Object} options An object with following properties: + * @param {Uint16Array} options.positions The positions of the polylines + * @param {Uint32Array} options.counts The number or positions in the each polyline. + * @param {Uint16Array} options.widths The width of each polyline. + * @param {Number} options.minimumHeight The minimum height of the tile's region. + * @param {Number} options.maximumHeight The maximum height of the tile's region. + * @param {Rectangle} options.rectangle The rectangle containing the tile. + * @param {Cartesian3} [options.center=Cartesian3.ZERO] The RTC center. + * @param {Cesium3DTileBatchTable} options.batchTable The batch table for the tile containing the batched polylines. + * @param {Uint16Array} options.batchIds The batch ids for each polyline. + * @param {Cesium3DTileset} options.tileset Tileset carrying minimum and maximum clamping heights. + * + * @private + */ +function Vector3DTileClampedPolylines(options) { + // these arrays hold data from the tile payload + // and are all released after the first update. + this._positions = options.positions; + this._widths = options.widths; + this._counts = options.counts; + this._batchIds = options.batchIds; + + this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); + this._minimumHeight = options.minimumHeight; + this._maximumHeight = options.maximumHeight; + this._center = options.center; + this._rectangle = options.rectangle; + + this._batchTable = options.batchTable; + + this._va = undefined; + this._sp = undefined; + this._rs = undefined; + this._uniformMap = undefined; + this._command = undefined; + + this._transferrableBatchIds = undefined; + this._packedBuffer = undefined; + this._tileset = options.tileset; + this._minimumMaximumVectorHeights = new Cartesian2( + ApproximateTerrainHeights._defaultMinTerrainHeight, + ApproximateTerrainHeights._defaultMaxTerrainHeight + ); + this._boundingVolume = OrientedBoundingBox.fromRectangle( + options.rectangle, + ApproximateTerrainHeights._defaultMinTerrainHeight, + ApproximateTerrainHeights._defaultMaxTerrainHeight, + this._ellipsoid + ); + + // Fat vertices - all information for each volume packed to a vec3 and 5 vec4s + this._startEllipsoidNormals = undefined; + this._endEllipsoidNormals = undefined; + this._startPositionAndHeights = undefined; + this._startFaceNormalAndVertexCornerIds = undefined; + this._endPositionAndHeights = undefined; + this._endFaceNormalAndHalfWidths = undefined; + this._vertexBatchIds = undefined; + + this._indices = undefined; + + this._constantColor = Color.clone(Color.WHITE); + this._highlightColor = this._constantColor; + + this._trianglesLength = 0; + this._geometryByteLength = 0; + + this._ready = false; + this._readyPromise = when.defer(); + + this._verticesPromise = undefined; + + var that = this; + ApproximateTerrainHeights.initialize() + .then(function () { + updateMinimumMaximumHeights(that, that._rectangle, that._ellipsoid); + }) + .otherwise(function (error) { + this._readyPromise.reject(error); + }); +} + +Object.defineProperties(Vector3DTileClampedPolylines.prototype, { + /** + * Gets the number of triangles. + * + * @memberof Vector3DTileClampedPolylines.prototype + * + * @type {Number} + * @readonly + */ + trianglesLength: { + get: function () { + return this._trianglesLength; + }, + }, + + /** + * Gets the geometry memory in bytes. + * + * @memberof Vector3DTileClampedPolylines.prototype + * + * @type {Number} + * @readonly + */ + geometryByteLength: { + get: function () { + return this._geometryByteLength; + }, + }, + + /** + * Gets a promise that resolves when the primitive is ready to render. + * @memberof Vector3DTileClampedPolylines.prototype + * @type {Promise} + * @readonly + */ + readyPromise: { + get: function () { + return this._readyPromise.promise; + }, + }, +}); + +function updateMinimumMaximumHeights(polylines, rectangle, ellipsoid) { + var result = ApproximateTerrainHeights.getMinimumMaximumHeights( + rectangle, + ellipsoid + ); + var min = result.minimumTerrainHeight; + var max = result.maximumTerrainHeight; + var minimumMaximumVectorHeights = polylines._minimumMaximumVectorHeights; + minimumMaximumVectorHeights.x = min; + minimumMaximumVectorHeights.y = max; + + var obb = polylines._boundingVolume; + var rect = polylines._rectangle; + OrientedBoundingBox.fromRectangle(rect, min, max, ellipsoid, obb); +} + +function packBuffer(polylines) { + var rectangle = polylines._rectangle; + var minimumHeight = polylines._minimumHeight; + var maximumHeight = polylines._maximumHeight; + var ellipsoid = polylines._ellipsoid; + var center = polylines._center; + + var packedLength = + 2 + + Rectangle.packedLength + + Ellipsoid.packedLength + + Cartesian3.packedLength; + var packedBuffer = new Float64Array(packedLength); + + var offset = 0; + packedBuffer[offset++] = minimumHeight; + packedBuffer[offset++] = maximumHeight; + + Rectangle.pack(rectangle, packedBuffer, offset); + offset += Rectangle.packedLength; + + Ellipsoid.pack(ellipsoid, packedBuffer, offset); + offset += Ellipsoid.packedLength; + + Cartesian3.pack(center, packedBuffer, offset); + + return packedBuffer; +} + +var createVerticesTaskProcessor = new TaskProcessor( + "createVectorTileClampedPolylines" +); +var attributeLocations = { + startEllipsoidNormal: 0, + endEllipsoidNormal: 1, + startPositionAndHeight: 2, + endPositionAndHeight: 3, + startFaceNormalAndVertexCorner: 4, + endFaceNormalAndHalfWidth: 5, + a_batchId: 6, +}; + +function createVertexArray(polylines, context) { + if (defined(polylines._va)) { + return; + } + + if (!defined(polylines._verticesPromise)) { + var positions = polylines._positions; + var widths = polylines._widths; + var counts = polylines._counts; + var batchIds = polylines._transferrableBatchIds; + + var packedBuffer = polylines._packedBuffer; + + if (!defined(packedBuffer)) { + // Copy because they may be the views on the same buffer. + positions = polylines._positions = arraySlice(positions); + widths = polylines._widths = arraySlice(widths); + counts = polylines._counts = arraySlice(counts); + + batchIds = polylines._transferrableBatchIds = arraySlice( + polylines._batchIds + ); + + packedBuffer = polylines._packedBuffer = packBuffer(polylines); + } + + var transferrableObjects = [ + positions.buffer, + widths.buffer, + counts.buffer, + batchIds.buffer, + packedBuffer.buffer, + ]; + var parameters = { + positions: positions.buffer, + widths: widths.buffer, + counts: counts.buffer, + batchIds: batchIds.buffer, + packedBuffer: packedBuffer.buffer, + }; + + var verticesPromise = (polylines._verticesPromise = createVerticesTaskProcessor.scheduleTask( + parameters, + transferrableObjects + )); + if (!defined(verticesPromise)) { + // Postponed + return; + } + + when(verticesPromise, function (result) { + polylines._startEllipsoidNormals = new Float32Array( + result.startEllipsoidNormals + ); + polylines._endEllipsoidNormals = new Float32Array( + result.endEllipsoidNormals + ); + polylines._startPositionAndHeights = new Float32Array( + result.startPositionAndHeights + ); + polylines._startFaceNormalAndVertexCornerIds = new Float32Array( + result.startFaceNormalAndVertexCornerIds + ); + polylines._endPositionAndHeights = new Float32Array( + result.endPositionAndHeights + ); + polylines._endFaceNormalAndHalfWidths = new Float32Array( + result.endFaceNormalAndHalfWidths + ); + polylines._vertexBatchIds = new Uint16Array(result.vertexBatchIds); + + var indexDatatype = result.indexDatatype; + polylines._indices = + indexDatatype === IndexDatatype.UNSIGNED_SHORT + ? new Uint16Array(result.indices) + : new Uint32Array(result.indices); + + polylines._ready = true; + }).otherwise(function (error) { + polylines._readyPromise.reject(error); + }); + } + + if (polylines._ready && !defined(polylines._va)) { + var startEllipsoidNormals = polylines._startEllipsoidNormals; + var endEllipsoidNormals = polylines._endEllipsoidNormals; + var startPositionAndHeights = polylines._startPositionAndHeights; + var endPositionAndHeights = polylines._endPositionAndHeights; + var startFaceNormalAndVertexCornerIds = + polylines._startFaceNormalAndVertexCornerIds; + var endFaceNormalAndHalfWidths = polylines._endFaceNormalAndHalfWidths; + var batchIdAttribute = polylines._vertexBatchIds; + + var indices = polylines._indices; + + var byteLength = + startEllipsoidNormals.byteLength + endEllipsoidNormals.byteLength; + byteLength += + startPositionAndHeights.byteLength + endPositionAndHeights.byteLength; + byteLength += + startFaceNormalAndVertexCornerIds.byteLength + + endFaceNormalAndHalfWidths.byteLength; + byteLength += batchIdAttribute.byteLength + indices.byteLength; + + polylines._trianglesLength = indices.length / 3; + polylines._geometryByteLength = byteLength; + + var startEllipsoidNormalsBuffer = Buffer.createVertexBuffer({ + context: context, + typedArray: startEllipsoidNormals, + usage: BufferUsage.STATIC_DRAW, + }); + var endEllipsoidNormalsBuffer = Buffer.createVertexBuffer({ + context: context, + typedArray: endEllipsoidNormals, + usage: BufferUsage.STATIC_DRAW, + }); + var startPositionAndHeightsBuffer = Buffer.createVertexBuffer({ + context: context, + typedArray: startPositionAndHeights, + usage: BufferUsage.STATIC_DRAW, + }); + var endPositionAndHeightsBuffer = Buffer.createVertexBuffer({ + context: context, + typedArray: endPositionAndHeights, + usage: BufferUsage.STATIC_DRAW, + }); + var startFaceNormalAndVertexCornerIdsBuffer = Buffer.createVertexBuffer({ + context: context, + typedArray: startFaceNormalAndVertexCornerIds, + usage: BufferUsage.STATIC_DRAW, + }); + var endFaceNormalAndHalfWidthsBuffer = Buffer.createVertexBuffer({ + context: context, + typedArray: endFaceNormalAndHalfWidths, + usage: BufferUsage.STATIC_DRAW, + }); + var batchIdAttributeBuffer = Buffer.createVertexBuffer({ + context: context, + typedArray: batchIdAttribute, + usage: BufferUsage.STATIC_DRAW, + }); + + var indexBuffer = Buffer.createIndexBuffer({ + context: context, + typedArray: indices, + usage: BufferUsage.STATIC_DRAW, + indexDatatype: + indices.BYTES_PER_ELEMENT === 2 + ? IndexDatatype.UNSIGNED_SHORT + : IndexDatatype.UNSIGNED_INT, + }); + + var vertexAttributes = [ + { + index: attributeLocations.startEllipsoidNormal, + vertexBuffer: startEllipsoidNormalsBuffer, + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 3, + }, + { + index: attributeLocations.endEllipsoidNormal, + vertexBuffer: endEllipsoidNormalsBuffer, + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 3, + }, + { + index: attributeLocations.startPositionAndHeight, + vertexBuffer: startPositionAndHeightsBuffer, + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + }, + { + index: attributeLocations.endPositionAndHeight, + vertexBuffer: endPositionAndHeightsBuffer, + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + }, + { + index: attributeLocations.startFaceNormalAndVertexCorner, + vertexBuffer: startFaceNormalAndVertexCornerIdsBuffer, + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + }, + { + index: attributeLocations.endFaceNormalAndHalfWidth, + vertexBuffer: endFaceNormalAndHalfWidthsBuffer, + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + }, + { + index: attributeLocations.a_batchId, + vertexBuffer: batchIdAttributeBuffer, + componentDatatype: ComponentDatatype.UNSIGNED_SHORT, + componentsPerAttribute: 1, + }, + ]; + + polylines._va = new VertexArray({ + context: context, + attributes: vertexAttributes, + indexBuffer: indexBuffer, + }); + + polylines._positions = undefined; + polylines._widths = undefined; + polylines._counts = undefined; + + polylines._ellipsoid = undefined; + polylines._minimumHeight = undefined; + polylines._maximumHeight = undefined; + polylines._rectangle = undefined; + + polylines._transferrableBatchIds = undefined; + polylines._packedBuffer = undefined; + + polylines._startEllipsoidNormals = undefined; + polylines._endEllipsoidNormals = undefined; + polylines._startPositionAndHeights = undefined; + polylines._startFaceNormalAndVertexCornerIds = undefined; + polylines._endPositionAndHeights = undefined; + polylines._endFaceNormalAndHalfWidths = undefined; + polylines._vertexBatchIds = undefined; + + polylines._indices = undefined; + + polylines._readyPromise.resolve(); + } +} + +var modifiedModelViewScratch = new Matrix4(); +var rtcScratch = new Cartesian3(); + +function createUniformMap(primitive, context) { + if (defined(primitive._uniformMap)) { + return; + } + + primitive._uniformMap = { + u_modifiedModelView: function () { + var viewMatrix = context.uniformState.view; + Matrix4.clone(viewMatrix, modifiedModelViewScratch); + Matrix4.multiplyByPoint( + modifiedModelViewScratch, + primitive._center, + rtcScratch + ); + Matrix4.setTranslation( + modifiedModelViewScratch, + rtcScratch, + modifiedModelViewScratch + ); + return modifiedModelViewScratch; + }, + u_highlightColor: function () { + return primitive._highlightColor; + }, + u_minimumMaximumVectorHeights: function () { + return primitive._minimumMaximumVectorHeights; + }, + }; +} + +function getRenderState(mask3DTiles) { + /** + * Cull front faces of each volume (relative to camera) to prevent + * classification drawing from both the front and back faces, double-draw. + * The geometry is "inverted" (inside-out winding order for the indices) but + * the vertex shader seems to re-invert so that the triangles face "out" again. + * So cull FRONT faces. + */ + return RenderState.fromCache({ + cull: { + enabled: true, + face: CullFace.FRONT, + }, + blending: BlendingState.PRE_MULTIPLIED_ALPHA_BLEND, + depthMask: false, + stencilTest: { + enabled: mask3DTiles, + frontFunction: StencilFunction.EQUAL, + frontOperation: { + fail: StencilOperation.KEEP, + zFail: StencilOperation.KEEP, + zPass: StencilOperation.KEEP, + }, + backFunction: StencilFunction.EQUAL, + backOperation: { + fail: StencilOperation.KEEP, + zFail: StencilOperation.KEEP, + zPass: StencilOperation.KEEP, + }, + reference: StencilConstants.CESIUM_3D_TILE_MASK, + mask: StencilConstants.CESIUM_3D_TILE_MASK, + }, + }); +} + +function createRenderStates(primitive) { + if (defined(primitive._rs)) { + return; + } + + primitive._rs = getRenderState(false); + primitive._rs3DTiles = getRenderState(true); +} + +function createShaders(primitive, context) { + if (defined(primitive._sp)) { + return; + } + + var batchTable = primitive._batchTable; + + var vsSource = batchTable.getVertexShaderCallback( + false, + "a_batchId", + undefined + )(Vector3DTileClampedPolylinesVS); + var fsSource = batchTable.getFragmentShaderCallback( + false, + undefined, + true + )(Vector3DTileClampedPolylinesFS); + + var vs = new ShaderSource({ + defines: [ + "VECTOR_TILE", + !FeatureDetection.isInternetExplorer() ? "CLIP_POLYLINE" : "", + ], + sources: [PolylineCommon, vsSource], + }); + var fs = new ShaderSource({ + defines: ["VECTOR_TILE"], + sources: [fsSource], + }); + + primitive._sp = ShaderProgram.fromCache({ + context: context, + vertexShaderSource: vs, + fragmentShaderSource: fs, + attributeLocations: attributeLocations, + }); +} + +function queueCommands(primitive, frameState) { + var command = primitive._command; + if (!defined(primitive._command)) { + var uniformMap = primitive._batchTable.getUniformMapCallback()( + primitive._uniformMap + ); + command = primitive._command = new DrawCommand({ + owner: primitive, + vertexArray: primitive._va, + renderState: primitive._rs, + shaderProgram: primitive._sp, + uniformMap: uniformMap, + boundingVolume: primitive._boundingVolume, + pass: Pass.TERRAIN_CLASSIFICATION, + pickId: primitive._batchTable.getPickId(), + }); + + var derivedTilesetCommand = DrawCommand.shallowClone( + command, + command.derivedCommands.tileset + ); + derivedTilesetCommand.renderState = primitive._rs3DTiles; + derivedTilesetCommand.pass = Pass.CESIUM_3D_TILE_CLASSIFICATION; + command.derivedCommands.tileset = derivedTilesetCommand; + } + + var classificationType = primitive._tileset.classificationType; + if ( + classificationType === ClassificationType.TERRAIN || + classificationType === ClassificationType.BOTH + ) { + frameState.commandList.push(command); + } + if ( + classificationType === ClassificationType.CESIUM_3D_TILE || + classificationType === ClassificationType.BOTH + ) { + frameState.commandList.push(command.derivedCommands.tileset); + } +} + +/** + * Creates features for each polyline and places it at the batch id index of features. + * + * @param {Vector3DTileContent} content The vector tile content. + * @param {Cesium3DTileFeature[]} features An array of features where the polygon features will be placed. + */ +Vector3DTileClampedPolylines.prototype.createFeatures = function ( + content, + features +) { + var batchIds = this._batchIds; + var length = batchIds.length; + for (var i = 0; i < length; ++i) { + var batchId = batchIds[i]; + features[batchId] = new Cesium3DTileFeature(content, batchId); + } +}; + +/** + * Colors the entire tile when enabled is true. The resulting color will be (polyline batch table color * color). + * + * @param {Boolean} enabled Whether to enable debug coloring. + * @param {Color} color The debug color. + */ +Vector3DTileClampedPolylines.prototype.applyDebugSettings = function ( + enabled, + color +) { + this._highlightColor = enabled ? color : this._constantColor; +}; + +function clearStyle(polygons, features) { + var batchIds = polygons._batchIds; + var length = batchIds.length; + for (var i = 0; i < length; ++i) { + var batchId = batchIds[i]; + var feature = features[batchId]; + + feature.show = true; + feature.color = Color.WHITE; + } +} + +var scratchColor = new Color(); + +var DEFAULT_COLOR_VALUE = Color.WHITE; +var DEFAULT_SHOW_VALUE = true; + +/** + * Apply a style to the content. + * + * @param {Cesium3DTileStyle} style The style. + * @param {Cesium3DTileFeature[]} features The dictionary of features. + */ +Vector3DTileClampedPolylines.prototype.applyStyle = function (style, features) { + if (!defined(style)) { + clearStyle(this, features); + return; + } + + var batchIds = this._batchIds; + var length = batchIds.length; + for (var i = 0; i < length; ++i) { + var batchId = batchIds[i]; + var feature = features[batchId]; + + feature.color = defined(style.color) + ? style.color.evaluateColor(feature, scratchColor) + : DEFAULT_COLOR_VALUE; + feature.show = defined(style.show) + ? style.show.evaluate(feature) + : DEFAULT_SHOW_VALUE; + } +}; + +/** + * Updates the batches and queues the commands for rendering. + * + * @param {FrameState} frameState The current frame state. + */ +Vector3DTileClampedPolylines.prototype.update = function (frameState) { + var context = frameState.context; + + createVertexArray(this, context); + createUniformMap(this, context); + createShaders(this, context); + createRenderStates(this); + + if (!this._ready) { + return; + } + + var passes = frameState.passes; + if (passes.render || passes.pick) { + queueCommands(this, frameState); + } +}; + +/** + * Returns true if this object was destroyed; otherwise, false. + *

+ * If this object was destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. + *

+ * + * @returns {Boolean} true if this object was destroyed; otherwise, false. + */ +Vector3DTileClampedPolylines.prototype.isDestroyed = function () { + return false; +}; + +/** + * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic + * release of WebGL resources, instead of relying on the garbage collector to destroy this object. + *

+ * Once an object is destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. Therefore, + * assign the return value (undefined) to the object as done in the example. + *

+ * + * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. + */ +Vector3DTileClampedPolylines.prototype.destroy = function () { + this._va = this._va && this._va.destroy(); + this._sp = this._sp && this._sp.destroy(); + return destroyObject(this); +}; +export default Vector3DTileClampedPolylines; diff --git a/Source/Scene/Vector3DTileContent.js b/Source/Scene/Vector3DTileContent.js index cffa793b4e9f..f1d008070469 100644 --- a/Source/Scene/Vector3DTileContent.js +++ b/Source/Scene/Vector3DTileContent.js @@ -16,6 +16,8 @@ import Cesium3DTileFeatureTable from "./Cesium3DTileFeatureTable.js"; import Vector3DTilePoints from "./Vector3DTilePoints.js"; import Vector3DTilePolygons from "./Vector3DTilePolygons.js"; import Vector3DTilePolylines from "./Vector3DTilePolylines.js"; +import Vector3DTileClampedPolylines from "./Vector3DTileClampedPolylines.js"; +import decodeVectorPolylinePositions from "../Core/decodeVectorPolylinePositions.js"; /** * Represents the contents of a @@ -248,6 +250,14 @@ function getBatchIds(featureTableJson, featureTableBinary) { var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; +function createFloatingPolylines(options) { + return new Vector3DTilePolylines(options); +} + +function createClampedPolylines(options) { + return new Vector3DTileClampedPolylines(options); +} + function initialize(content, arrayBuffer, byteOffset) { byteOffset = defaultValue(byteOffset, 0); @@ -533,7 +543,32 @@ function initialize(content, arrayBuffer, byteOffset) { ); byteOffset += polylinePositionByteLength; - content._polylines = new Vector3DTilePolylines({ + var tileset = content._tileset; + var examineVectorLinesFunction = tileset.examineVectorLinesFunction; + if (defined(examineVectorLinesFunction)) { + var decodedPositions = decodeVectorPolylinePositions( + new Uint16Array(polylinePositions), + rectangle, + minHeight, + maxHeight, + Ellipsoid.WGS84 + ); + examineVectorLines( + decodedPositions, + polylineCounts, + batchIds.polylines, + batchTable, + content.url, + examineVectorLinesFunction + ); + } + + var createPolylines = createFloatingPolylines; + if (defined(tileset.classificationType)) { + createPolylines = createClampedPolylines; + } + + content._polylines = createPolylines({ positions: polylinePositions, widths: widths, counts: polylineCounts, @@ -544,6 +579,7 @@ function initialize(content, arrayBuffer, byteOffset) { rectangle: rectangle, boundingVolume: content.tile.boundingVolume.boundingVolume, batchTable: batchTable, + tileset: tileset, }); } @@ -664,6 +700,9 @@ Vector3DTileContent.prototype.update = function (tileset, frameState) { .all([pointsPromise, polygonPromise, polylinePromise]) .then(function () { that._readyPromise.resolve(that); + }) + .otherwise(function (error) { + that._readyPromise.reject(error); }); } }; @@ -679,4 +718,24 @@ Vector3DTileContent.prototype.destroy = function () { this._batchTable = this._batchTable && this._batchTable.destroy(); return destroyObject(this); }; + +function examineVectorLines( + positions, + counts, + batchIds, + batchTable, + url, + callback +) { + var countsLength = counts.length; + var polylineStart = 0; + for (var i = 0; i < countsLength; i++) { + var count = counts[i] * 3; + var linePositions = positions.slice(polylineStart, polylineStart + count); + polylineStart += count; + + callback(linePositions, batchIds[i], url, batchTable); + } +} + export default Vector3DTileContent; diff --git a/Source/Scene/Vector3DTilePolylines.js b/Source/Scene/Vector3DTilePolylines.js index 3473c5ba2c8d..2771a2d0a57b 100644 --- a/Source/Scene/Vector3DTilePolylines.js +++ b/Source/Scene/Vector3DTilePolylines.js @@ -432,11 +432,11 @@ function createShaders(primitive, context) { "a_batchId", undefined )(Vector3DTilePolylinesVS); - var fsSource = batchTable.getFragmentShaderCallback()( - PolylineFS, + var fsSource = batchTable.getFragmentShaderCallback( false, - undefined - ); + undefined, + false + )(PolylineFS); var vs = new ShaderSource({ defines: [ diff --git a/Source/Scene/Vector3DTilePrimitive.js b/Source/Scene/Vector3DTilePrimitive.js index fdc3edce63bb..1c51a4e44ec8 100644 --- a/Source/Scene/Vector3DTilePrimitive.js +++ b/Source/Scene/Vector3DTilePrimitive.js @@ -302,11 +302,11 @@ function createShaders(primitive, context) { "a_batchId", undefined )(VectorTileVS); - var fsSource = batchTable.getFragmentShaderCallback()( - ShadowVolumeFS, + var fsSource = batchTable.getFragmentShaderCallback( false, - undefined - ); + undefined, + true + )(ShadowVolumeFS); pickId = batchTable.getPickId(); @@ -427,7 +427,7 @@ var colorRenderState = { enabled: false, }, depthMask: false, - blending: BlendingState.ALPHA_BLEND, + blending: BlendingState.PRE_MULTIPLIED_ALPHA_BLEND, }; var pickRenderState = { diff --git a/Source/Shaders/Vector3DTileClampedPolylinesFS.glsl b/Source/Shaders/Vector3DTileClampedPolylinesFS.glsl new file mode 100644 index 000000000000..0fa201cbd80e --- /dev/null +++ b/Source/Shaders/Vector3DTileClampedPolylinesFS.glsl @@ -0,0 +1,52 @@ +#ifdef GL_EXT_frag_depth +#extension GL_EXT_frag_depth : enable +#endif + +varying vec4 v_startPlaneEC; +varying vec4 v_endPlaneEC; +varying vec4 v_rightPlaneEC; +varying float v_halfWidth; +varying vec3 v_volumeUpEC; + +uniform vec4 u_highlightColor; +void main() +{ + float logDepthOrDepth = czm_branchFreeTernary(czm_sceneMode == czm_sceneMode2D, gl_FragCoord.z, czm_unpackDepth(texture2D(czm_globeDepthTexture, gl_FragCoord.xy / czm_viewport.zw))); + + // Discard for sky + if (logDepthOrDepth == 0.0) { +#ifdef DEBUG_SHOW_VOLUME + gl_FragColor = vec4(0.0, 0.0, 1.0, 0.5); + return; +#else // DEBUG_SHOW_VOLUME + discard; +#endif // DEBUG_SHOW_VOLUME + } + + vec4 eyeCoordinate = czm_windowToEyeCoordinates(gl_FragCoord.xy, logDepthOrDepth); + eyeCoordinate /= eyeCoordinate.w; + + float halfMaxWidth = v_halfWidth * czm_metersPerPixel(eyeCoordinate); + + // Expand halfMaxWidth if direction to camera is almost perpendicular with the volume's up direction + halfMaxWidth += halfMaxWidth * (1.0 - dot(-normalize(eyeCoordinate.xyz), v_volumeUpEC)); + + // Check distance of the eye coordinate against the right-facing plane + float widthwiseDistance = czm_planeDistance(v_rightPlaneEC, eyeCoordinate.xyz); + + // Check eye coordinate against the mitering planes + float distanceFromStart = czm_planeDistance(v_startPlaneEC, eyeCoordinate.xyz); + float distanceFromEnd = czm_planeDistance(v_endPlaneEC, eyeCoordinate.xyz); + + if (abs(widthwiseDistance) > halfMaxWidth || distanceFromStart < 0.0 || distanceFromEnd < 0.0) { +#ifdef DEBUG_SHOW_VOLUME + gl_FragColor = vec4(logDepthOrDepth, 0.0, 0.0, 0.5); + return; +#else // DEBUG_SHOW_VOLUME + discard; +#endif // DEBUG_SHOW_VOLUME + } + gl_FragColor = u_highlightColor; + + czm_writeDepthClamp(); +} diff --git a/Source/Shaders/Vector3DTileClampedPolylinesVS.glsl b/Source/Shaders/Vector3DTileClampedPolylinesVS.glsl new file mode 100644 index 000000000000..07668ffbe52a --- /dev/null +++ b/Source/Shaders/Vector3DTileClampedPolylinesVS.glsl @@ -0,0 +1,85 @@ +attribute vec3 startEllipsoidNormal; +attribute vec3 endEllipsoidNormal; +attribute vec4 startPositionAndHeight; +attribute vec4 endPositionAndHeight; +attribute vec4 startFaceNormalAndVertexCorner; +attribute vec4 endFaceNormalAndHalfWidth; +attribute float a_batchId; + +uniform mat4 u_modifiedModelView; +uniform vec2 u_minimumMaximumVectorHeights; + +varying vec4 v_startPlaneEC; +varying vec4 v_endPlaneEC; +varying vec4 v_rightPlaneEC; +varying float v_halfWidth; +varying vec3 v_volumeUpEC; + +void main() +{ + // vertex corner IDs + // 3-----------7 + // /| left /| + // / | 1 / | + // 2-----------6 5 end + // | / | / + // start |/ right |/ + // 0-----------4 + // + float isEnd = floor(startFaceNormalAndVertexCorner.w * 0.251); // 0 for front, 1 for end + float isTop = floor(startFaceNormalAndVertexCorner.w * mix(0.51, 0.19, isEnd)); // 0 for bottom, 1 for top + + vec3 forward = endPositionAndHeight.xyz - startPositionAndHeight.xyz; + vec3 right = normalize(cross(forward, startEllipsoidNormal)); + + vec4 position = vec4(startPositionAndHeight.xyz, 1.0); + position.xyz += forward * isEnd; + + v_volumeUpEC = czm_normal * normalize(cross(right, forward)); + + // Push for volume height + float offset; + vec3 ellipsoidNormal = mix(startEllipsoidNormal, endEllipsoidNormal, isEnd); + + // offset height to create volume + offset = mix(startPositionAndHeight.w, endPositionAndHeight.w, isEnd); + offset = mix(u_minimumMaximumVectorHeights.y, u_minimumMaximumVectorHeights.x, isTop) - offset; + position.xyz += offset * ellipsoidNormal; + + // move from RTC to EC + position = u_modifiedModelView * position; + right = czm_normal * right; + + // Push for width in a direction that is in the start or end plane and in a plane with right + // N = normalEC ("right-facing" direction for push) + // R = right + // p = angle between N and R + // w = distance to push along R if R == N + // d = distance to push along N + // + // N R + // { \ p| } * cos(p) = dot(N, R) = w / d + // d\ \ | |w * d = w / dot(N, R) + // { \| } + // o---------- polyline segment ----> + // + vec3 scratchNormal = mix(-startFaceNormalAndVertexCorner.xyz, endFaceNormalAndHalfWidth.xyz, isEnd); + scratchNormal = cross(scratchNormal, mix(startEllipsoidNormal, endEllipsoidNormal, isEnd)); + vec3 miterPushNormal = czm_normal * normalize(scratchNormal); + + offset = 2.0 * endFaceNormalAndHalfWidth.w * max(0.0, czm_metersPerPixel(position)); // offset = widthEC + offset = offset / dot(miterPushNormal, right); + position.xyz += miterPushNormal * (offset * sign(0.5 - mod(startFaceNormalAndVertexCorner.w, 2.0))); + + gl_Position = czm_depthClamp(czm_projection * position); + + position = u_modifiedModelView * vec4(startPositionAndHeight.xyz, 1.0); + vec3 startNormalEC = czm_normal * startFaceNormalAndVertexCorner.xyz; + v_startPlaneEC = vec4(startNormalEC, -dot(startNormalEC, position.xyz)); + v_rightPlaneEC = vec4(right, -dot(right, position.xyz)); + + position = u_modifiedModelView * vec4(endPositionAndHeight.xyz, 1.0); + vec3 endNormalEC = czm_normal * endFaceNormalAndHalfWidth.xyz; + v_endPlaneEC = vec4(endNormalEC, -dot(endNormalEC, position.xyz)); + v_halfWidth = endFaceNormalAndHalfWidth.w; +} diff --git a/Source/WorkersES6/createVectorTileClampedPolylines.js b/Source/WorkersES6/createVectorTileClampedPolylines.js new file mode 100644 index 000000000000..603326da2bd1 --- /dev/null +++ b/Source/WorkersES6/createVectorTileClampedPolylines.js @@ -0,0 +1,508 @@ +import AttributeCompression from "../Core/AttributeCompression.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import Cartographic from "../Core/Cartographic.js"; +import Ellipsoid from "../Core/Ellipsoid.js"; +import IndexDatatype from "../Core/IndexDatatype.js"; +import CesiumMath from "../Core/Math.js"; +import Rectangle from "../Core/Rectangle.js"; +import createTaskProcessorWorker from "./createTaskProcessorWorker.js"; + +var MAX_SHORT = 32767; +var MITER_BREAK = Math.cos(CesiumMath.toRadians(150.0)); + +var scratchBVCartographic = new Cartographic(); +var scratchEncodedPosition = new Cartesian3(); + +function decodePositionsToRtc( + uBuffer, + vBuffer, + heightBuffer, + rectangle, + minimumHeight, + maximumHeight, + ellipsoid, + center +) { + var positionsLength = uBuffer.length; + var decodedPositions = new Float32Array(positionsLength * 3); + for (var i = 0; i < positionsLength; ++i) { + var u = uBuffer[i]; + var v = vBuffer[i]; + var h = heightBuffer[i]; + + var lon = CesiumMath.lerp(rectangle.west, rectangle.east, u / MAX_SHORT); + var lat = CesiumMath.lerp(rectangle.south, rectangle.north, v / MAX_SHORT); + var alt = CesiumMath.lerp(minimumHeight, maximumHeight, h / MAX_SHORT); + + var cartographic = Cartographic.fromRadians( + lon, + lat, + alt, + scratchBVCartographic + ); + var decodedPosition = ellipsoid.cartographicToCartesian( + cartographic, + scratchEncodedPosition + ); + var rtc = Cartesian3.subtract( + decodedPosition, + center, + scratchEncodedPosition + ); + Cartesian3.pack(rtc, decodedPositions, i * 3); + } + return decodedPositions; +} + +var previousCompressedCartographicScratch = new Cartographic(); +var currentCompressedCartographicScratch = new Cartographic(); +function removeDuplicates(uBuffer, vBuffer, heightBuffer, counts) { + var countsLength = counts.length; + var positionsLength = uBuffer.length; + var markRemoval = new Uint8Array(positionsLength); + var previous = previousCompressedCartographicScratch; + var current = currentCompressedCartographicScratch; + var offset = 0; + for (var i = 0; i < countsLength; i++) { + var count = counts[i]; + var updatedCount = count; + for (var j = 1; j < count; j++) { + var index = offset + j; + var previousIndex = index - 1; + current.longitude = uBuffer[index]; + current.latitude = vBuffer[index]; + previous.longitude = uBuffer[previousIndex]; + previous.latitude = vBuffer[previousIndex]; + + if (Cartographic.equals(current, previous)) { + updatedCount--; + markRemoval[previousIndex] = 1; + } + } + counts[i] = updatedCount; + offset += count; + } + + var nextAvailableIndex = 0; + for (var k = 0; k < positionsLength; k++) { + if (markRemoval[k] !== 1) { + uBuffer[nextAvailableIndex] = uBuffer[k]; + vBuffer[nextAvailableIndex] = vBuffer[k]; + heightBuffer[nextAvailableIndex] = heightBuffer[k]; + nextAvailableIndex++; + } + } +} + +function VertexAttributesAndIndices(volumesCount) { + var vertexCount = volumesCount * 8; + var vec3Floats = vertexCount * 3; + var vec4Floats = vertexCount * 4; + this.startEllipsoidNormals = new Float32Array(vec3Floats); + this.endEllipsoidNormals = new Float32Array(vec3Floats); + this.startPositionAndHeights = new Float32Array(vec4Floats); + this.startFaceNormalAndVertexCornerIds = new Float32Array(vec4Floats); + this.endPositionAndHeights = new Float32Array(vec4Floats); + this.endFaceNormalAndHalfWidths = new Float32Array(vec4Floats); + this.vertexBatchIds = new Uint16Array(vertexCount); + + this.indices = IndexDatatype.createTypedArray(vertexCount, 36 * volumesCount); + + this.vec3Offset = 0; + this.vec4Offset = 0; + this.batchIdOffset = 0; + this.indexOffset = 0; + + this.volumeStartIndex = 0; +} + +var towardCurrScratch = new Cartesian3(); +var towardNextScratch = new Cartesian3(); +function computeMiteredNormal( + previousPosition, + position, + nextPosition, + ellipsoidSurfaceNormal, + result +) { + var towardNext = Cartesian3.subtract( + nextPosition, + position, + towardNextScratch + ); + var towardCurr = Cartesian3.subtract( + position, + previousPosition, + towardCurrScratch + ); + Cartesian3.normalize(towardNext, towardNext); + Cartesian3.normalize(towardCurr, towardCurr); + + if (Cartesian3.dot(towardNext, towardCurr) < MITER_BREAK) { + towardCurr = Cartesian3.multiplyByScalar( + towardCurr, + -1.0, + towardCurrScratch + ); + } + + Cartesian3.add(towardNext, towardCurr, result); + if (Cartesian3.equals(result, Cartesian3.ZERO)) { + result = Cartesian3.subtract(previousPosition, position); + } + + // Make sure the normal is orthogonal to the ellipsoid surface normal + Cartesian3.cross(result, ellipsoidSurfaceNormal, result); + Cartesian3.cross(ellipsoidSurfaceNormal, result, result); + Cartesian3.normalize(result, result); + return result; +} + +// Winding order is reversed so each segment's volume is inside-out +// 3-----------7 +// /| left /| +// / | 1 / | +// 2-----------6 5 end +// | / | / +// start |/ right |/ +// 0-----------4 +// +var REFERENCE_INDICES = [ + 0, + 2, + 6, + 0, + 6, + 4, // right + 0, + 1, + 3, + 0, + 3, + 2, // start face + 0, + 4, + 5, + 0, + 5, + 1, // bottom + 5, + 3, + 1, + 5, + 7, + 3, // left + 7, + 5, + 4, + 7, + 4, + 6, // end face + 7, + 6, + 2, + 7, + 2, + 3, // top +]; +var REFERENCE_INDICES_LENGTH = REFERENCE_INDICES.length; + +var positionScratch = new Cartesian3(); +var scratchStartEllipsoidNormal = new Cartesian3(); +var scratchStartFaceNormal = new Cartesian3(); +var scratchEndEllipsoidNormal = new Cartesian3(); +var scratchEndFaceNormal = new Cartesian3(); +VertexAttributesAndIndices.prototype.addVolume = function ( + preStartRTC, + startRTC, + endRTC, + postEndRTC, + startHeight, + endHeight, + halfWidth, + batchId, + center, + ellipsoid +) { + var position = Cartesian3.add(startRTC, center, positionScratch); + var startEllipsoidNormal = ellipsoid.geodeticSurfaceNormal( + position, + scratchStartEllipsoidNormal + ); + position = Cartesian3.add(endRTC, center, positionScratch); + var endEllipsoidNormal = ellipsoid.geodeticSurfaceNormal( + position, + scratchEndEllipsoidNormal + ); + + var startFaceNormal = computeMiteredNormal( + preStartRTC, + startRTC, + endRTC, + startEllipsoidNormal, + scratchStartFaceNormal + ); + var endFaceNormal = computeMiteredNormal( + postEndRTC, + endRTC, + startRTC, + endEllipsoidNormal, + scratchEndFaceNormal + ); + + var startEllipsoidNormals = this.startEllipsoidNormals; + var endEllipsoidNormals = this.endEllipsoidNormals; + var startPositionAndHeights = this.startPositionAndHeights; + var startFaceNormalAndVertexCornerIds = this + .startFaceNormalAndVertexCornerIds; + var endPositionAndHeights = this.endPositionAndHeights; + var endFaceNormalAndHalfWidths = this.endFaceNormalAndHalfWidths; + var vertexBatchIds = this.vertexBatchIds; + + var batchIdOffset = this.batchIdOffset; + var vec3Offset = this.vec3Offset; + var vec4Offset = this.vec4Offset; + + var i; + for (i = 0; i < 8; i++) { + Cartesian3.pack(startEllipsoidNormal, startEllipsoidNormals, vec3Offset); + Cartesian3.pack(endEllipsoidNormal, endEllipsoidNormals, vec3Offset); + + Cartesian3.pack(startRTC, startPositionAndHeights, vec4Offset); + startPositionAndHeights[vec4Offset + 3] = startHeight; + + Cartesian3.pack(endRTC, endPositionAndHeights, vec4Offset); + endPositionAndHeights[vec4Offset + 3] = endHeight; + + Cartesian3.pack( + startFaceNormal, + startFaceNormalAndVertexCornerIds, + vec4Offset + ); + startFaceNormalAndVertexCornerIds[vec4Offset + 3] = i; + + Cartesian3.pack(endFaceNormal, endFaceNormalAndHalfWidths, vec4Offset); + endFaceNormalAndHalfWidths[vec4Offset + 3] = halfWidth; + + vertexBatchIds[batchIdOffset++] = batchId; + + vec3Offset += 3; + vec4Offset += 4; + } + + this.batchIdOffset = batchIdOffset; + this.vec3Offset = vec3Offset; + this.vec4Offset = vec4Offset; + var indices = this.indices; + var volumeStartIndex = this.volumeStartIndex; + + var indexOffset = this.indexOffset; + for (i = 0; i < REFERENCE_INDICES_LENGTH; i++) { + indices[indexOffset + i] = REFERENCE_INDICES[i] + volumeStartIndex; + } + + this.volumeStartIndex += 8; + this.indexOffset += REFERENCE_INDICES_LENGTH; +}; + +var scratchRectangle = new Rectangle(); +var scratchEllipsoid = new Ellipsoid(); +var scratchCenter = new Cartesian3(); + +var scratchPrev = new Cartesian3(); +var scratchP0 = new Cartesian3(); +var scratchP1 = new Cartesian3(); +var scratchNext = new Cartesian3(); +function createVectorTileClampedPolylines(parameters, transferableObjects) { + var encodedPositions = new Uint16Array(parameters.positions); + var widths = new Uint16Array(parameters.widths); + var counts = new Uint32Array(parameters.counts); + var batchIds = new Uint16Array(parameters.batchIds); + + // Unpack tile decoding parameters + var rectangle = scratchRectangle; + var ellipsoid = scratchEllipsoid; + var center = scratchCenter; + var packedBuffer = new Float64Array(parameters.packedBuffer); + + var offset = 0; + var minimumHeight = packedBuffer[offset++]; + var maximumHeight = packedBuffer[offset++]; + + Rectangle.unpack(packedBuffer, offset, rectangle); + offset += Rectangle.packedLength; + + Ellipsoid.unpack(packedBuffer, offset, ellipsoid); + offset += Ellipsoid.packedLength; + + Cartesian3.unpack(packedBuffer, offset, center); + + var i; + + // Unpack positions and generate volumes + var positionsLength = encodedPositions.length / 3; + var uBuffer = encodedPositions.subarray(0, positionsLength); + var vBuffer = encodedPositions.subarray(positionsLength, 2 * positionsLength); + var heightBuffer = encodedPositions.subarray( + 2 * positionsLength, + 3 * positionsLength + ); + AttributeCompression.zigZagDeltaDecode(uBuffer, vBuffer, heightBuffer); + + removeDuplicates(uBuffer, vBuffer, heightBuffer, counts); + + // Figure out how many volumes and how many vertices there will be. + var countsLength = counts.length; + var volumesCount = 0; + for (i = 0; i < countsLength; i++) { + var polylinePositionCount = counts[i]; + volumesCount += polylinePositionCount - 1; + } + + var attribsAndIndices = new VertexAttributesAndIndices(volumesCount); + + var positionsRTC = decodePositionsToRtc( + uBuffer, + vBuffer, + heightBuffer, + rectangle, + minimumHeight, + maximumHeight, + ellipsoid, + center + ); + + var currentPositionIndex = 0; + var currentHeightIndex = 0; + for (i = 0; i < countsLength; i++) { + var polylineVolumeCount = counts[i] - 1; + var halfWidth = widths[i] * 0.5; + var batchId = batchIds[i]; + var volumeFirstPositionIndex = currentPositionIndex; + for (var j = 0; j < polylineVolumeCount; j++) { + var volumeStart = Cartesian3.unpack( + positionsRTC, + currentPositionIndex, + scratchP0 + ); + var volumeEnd = Cartesian3.unpack( + positionsRTC, + currentPositionIndex + 3, + scratchP1 + ); + + var startHeight = heightBuffer[currentHeightIndex]; + var endHeight = heightBuffer[currentHeightIndex + 1]; + startHeight = CesiumMath.lerp( + minimumHeight, + maximumHeight, + startHeight / MAX_SHORT + ); + endHeight = CesiumMath.lerp( + minimumHeight, + maximumHeight, + endHeight / MAX_SHORT + ); + + currentHeightIndex++; + + var preStart = scratchPrev; + var postEnd = scratchNext; + if (j === 0) { + // Check if this volume is like a loop + var finalPositionIndex = + volumeFirstPositionIndex + polylineVolumeCount * 3; + var finalPosition = Cartesian3.unpack( + positionsRTC, + finalPositionIndex, + scratchPrev + ); + if (Cartesian3.equals(finalPosition, volumeStart)) { + Cartesian3.unpack(positionsRTC, finalPositionIndex - 3, preStart); + } else { + var offsetPastStart = Cartesian3.subtract( + volumeStart, + volumeEnd, + scratchPrev + ); + preStart = Cartesian3.add(offsetPastStart, volumeStart, scratchPrev); + } + } else { + Cartesian3.unpack(positionsRTC, currentPositionIndex - 3, preStart); + } + + if (j === polylineVolumeCount - 1) { + // Check if this volume is like a loop + var firstPosition = Cartesian3.unpack( + positionsRTC, + volumeFirstPositionIndex, + scratchNext + ); + if (Cartesian3.equals(firstPosition, volumeEnd)) { + Cartesian3.unpack( + positionsRTC, + volumeFirstPositionIndex + 3, + postEnd + ); + } else { + var offsetPastEnd = Cartesian3.subtract( + volumeEnd, + volumeStart, + scratchNext + ); + postEnd = Cartesian3.add(offsetPastEnd, volumeEnd, scratchNext); + } + } else { + Cartesian3.unpack(positionsRTC, currentPositionIndex + 6, postEnd); + } + + attribsAndIndices.addVolume( + preStart, + volumeStart, + volumeEnd, + postEnd, + startHeight, + endHeight, + halfWidth, + batchId, + center, + ellipsoid + ); + + currentPositionIndex += 3; + } + currentPositionIndex += 3; + currentHeightIndex++; + } + + var indices = attribsAndIndices.indices; + + transferableObjects.push(attribsAndIndices.startEllipsoidNormals.buffer); + transferableObjects.push(attribsAndIndices.endEllipsoidNormals.buffer); + transferableObjects.push(attribsAndIndices.startPositionAndHeights.buffer); + transferableObjects.push( + attribsAndIndices.startFaceNormalAndVertexCornerIds.buffer + ); + transferableObjects.push(attribsAndIndices.endPositionAndHeights.buffer); + transferableObjects.push(attribsAndIndices.endFaceNormalAndHalfWidths.buffer); + transferableObjects.push(attribsAndIndices.vertexBatchIds.buffer); + transferableObjects.push(indices.buffer); + + return { + indexDatatype: + indices.BYTES_PER_ELEMENT === 2 + ? IndexDatatype.UNSIGNED_SHORT + : IndexDatatype.UNSIGNED_INT, + startEllipsoidNormals: attribsAndIndices.startEllipsoidNormals.buffer, + endEllipsoidNormals: attribsAndIndices.endEllipsoidNormals.buffer, + startPositionAndHeights: attribsAndIndices.startPositionAndHeights.buffer, + startFaceNormalAndVertexCornerIds: + attribsAndIndices.startFaceNormalAndVertexCornerIds.buffer, + endPositionAndHeights: attribsAndIndices.endPositionAndHeights.buffer, + endFaceNormalAndHalfWidths: + attribsAndIndices.endFaceNormalAndHalfWidths.buffer, + vertexBatchIds: attribsAndIndices.vertexBatchIds.buffer, + indices: indices.buffer, + }; +} +export default createTaskProcessorWorker(createVectorTileClampedPolylines); diff --git a/Source/WorkersES6/createVectorTilePolylines.js b/Source/WorkersES6/createVectorTilePolylines.js index 240db47d2073..4bc9f82b17e4 100644 --- a/Source/WorkersES6/createVectorTilePolylines.js +++ b/Source/WorkersES6/createVectorTilePolylines.js @@ -1,58 +1,10 @@ -import AttributeCompression from "../Core/AttributeCompression.js"; import Cartesian3 from "../Core/Cartesian3.js"; -import Cartographic from "../Core/Cartographic.js"; +import decodeVectorPolylinePositions from "../Core/decodeVectorPolylinePositions.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import IndexDatatype from "../Core/IndexDatatype.js"; -import CesiumMath from "../Core/Math.js"; import Rectangle from "../Core/Rectangle.js"; import createTaskProcessorWorker from "./createTaskProcessorWorker.js"; -var maxShort = 32767; - -var scratchBVCartographic = new Cartographic(); -var scratchEncodedPosition = new Cartesian3(); - -function decodePositions( - positions, - rectangle, - minimumHeight, - maximumHeight, - ellipsoid -) { - var positionsLength = positions.length / 3; - var uBuffer = positions.subarray(0, positionsLength); - var vBuffer = positions.subarray(positionsLength, 2 * positionsLength); - var heightBuffer = positions.subarray( - 2 * positionsLength, - 3 * positionsLength - ); - AttributeCompression.zigZagDeltaDecode(uBuffer, vBuffer, heightBuffer); - - var decoded = new Float64Array(positions.length); - for (var i = 0; i < positionsLength; ++i) { - var u = uBuffer[i]; - var v = vBuffer[i]; - var h = heightBuffer[i]; - - var lon = CesiumMath.lerp(rectangle.west, rectangle.east, u / maxShort); - var lat = CesiumMath.lerp(rectangle.south, rectangle.north, v / maxShort); - var alt = CesiumMath.lerp(minimumHeight, maximumHeight, h / maxShort); - - var cartographic = Cartographic.fromRadians( - lon, - lat, - alt, - scratchBVCartographic - ); - var decodedPosition = ellipsoid.cartographicToCartesian( - cartographic, - scratchEncodedPosition - ); - Cartesian3.pack(decodedPosition, decoded, i * 3); - } - return decoded; -} - var scratchRectangle = new Rectangle(); var scratchEllipsoid = new Ellipsoid(); var scratchCenter = new Cartesian3(); @@ -96,7 +48,7 @@ function createVectorTilePolylines(parameters, transferableObjects) { var minimumHeight = scratchMinMaxHeights.min; var maximumHeight = scratchMinMaxHeights.max; - var positions = decodePositions( + var positions = decodeVectorPolylinePositions( encodedPositions, rectangle, minimumHeight, diff --git a/Specs/Scene/Vector3DTileClampedPolylinesSpec.js b/Specs/Scene/Vector3DTileClampedPolylinesSpec.js new file mode 100644 index 000000000000..4aa17d13d5cf --- /dev/null +++ b/Specs/Scene/Vector3DTileClampedPolylinesSpec.js @@ -0,0 +1,139 @@ +import { Cartesian3 } from "../../Source/Cesium.js"; +import { ClassificationType } from "../../Source/Cesium.js"; +import { Color } from "../../Source/Cesium.js"; +import { ColorGeometryInstanceAttribute } from "../../Source/Cesium.js"; +import { destroyObject } from "../../Source/Cesium.js"; +import { Ellipsoid } from "../../Source/Cesium.js"; +import { GeometryInstance } from "../../Source/Cesium.js"; +import { Rectangle } from "../../Source/Cesium.js"; +import { RectangleGeometry } from "../../Source/Cesium.js"; +import { Pass } from "../../Source/Cesium.js"; +import { PerInstanceColorAppearance } from "../../Source/Cesium.js"; +import { Primitive } from "../../Source/Cesium.js"; +import { Vector3DTileClampedPolylines } from "../../Source/Cesium.js"; +import Cesium3DTilesTester from "../Cesium3DTilesTester.js"; +import createScene from "../createScene.js"; + +describe( + "Scene/Vector3DTileClampedPolylines", + function () { + var scene; + var rectangle; + var polylines; + + var ellipsoid = Ellipsoid.WGS84; + + var depthRectanglePrimitive; + var vectorPolylines = + "./Data/Cesium3DTiles/Vector/VectorTilePolylines/tileset.json"; + + beforeAll(function () { + scene = createScene(); + }); + + afterAll(function () { + scene.destroyForSpecs(); + }); + + function MockGlobePrimitive(primitive) { + this._primitive = primitive; + this.pass = Pass.GLOBE; + } + + MockGlobePrimitive.prototype.update = function (frameState) { + var commandList = frameState.commandList; + var startLength = commandList.length; + this._primitive.update(frameState); + + for (var i = startLength; i < commandList.length; ++i) { + var command = commandList[i]; + command.pass = this.pass; + } + }; + + MockGlobePrimitive.prototype.isDestroyed = function () { + return false; + }; + + MockGlobePrimitive.prototype.destroy = function () { + this._primitive.destroy(); + return destroyObject(this); + }; + + beforeEach(function () { + rectangle = Rectangle.fromDegrees(-40.0, -40.0, 40.0, 40.0); + var depthpolylineColorAttribute = ColorGeometryInstanceAttribute.fromColor( + new Color(0.0, 0.0, 1.0, 1.0) + ); + var primitive = new Primitive({ + geometryInstances: new GeometryInstance({ + geometry: new RectangleGeometry({ + ellipsoid: ellipsoid, + rectangle: rectangle, + }), + id: "depth rectangle", + attributes: { + color: depthpolylineColorAttribute, + }, + }), + appearance: new PerInstanceColorAppearance({ + translucent: false, + flat: true, + }), + asynchronous: false, + }); + + // wrap rectangle primitive so it gets executed during the globe pass to lay down depth + depthRectanglePrimitive = new MockGlobePrimitive(primitive); + }); + + afterEach(function () { + scene.primitives.removeAll(); + polylines = polylines && !polylines.isDestroyed() && polylines.destroy(); + }); + + it("renders clamped polylines", function () { + scene.camera.lookAt( + Cartesian3.fromDegrees(0.0, 0.0, 1.5), + new Cartesian3(0.0, 0.0, 1.0) + ); + return Cesium3DTilesTester.loadTileset(scene, vectorPolylines, { + classificationType: ClassificationType.TERRAIN, + }).then(function (tileset) { + scene.primitives.add(depthRectanglePrimitive); + + tileset.show = false; + expect(scene).toRender([0, 0, 255, 255]); + tileset.show = true; + expect(scene).toRender([255, 255, 255, 255]); + }); + }); + + it("picks a clamped polyline", function () { + scene.camera.lookAt( + Cartesian3.fromDegrees(0.0, 0.0, 1.5), + new Cartesian3(0.0, 0.0, 1.0) + ); + return Cesium3DTilesTester.loadTileset(scene, vectorPolylines, { + classificationType: ClassificationType.TERRAIN, + }).then(function (tileset) { + scene.primitives.add(depthRectanglePrimitive); + + tileset.show = false; + expect(scene).toPickPrimitive(depthRectanglePrimitive._primitive); + tileset.show = true; + expect(scene).toPickPrimitive(tileset); + }); + }); + + it("isDestroyed", function () { + polylines = new Vector3DTileClampedPolylines({ + rectangle: new Rectangle(), + }); + expect(polylines.isDestroyed()).toEqual(false); + polylines.destroy(); + expect(polylines.isDestroyed()).toEqual(true); + }); + }, + "WebGL" +);