diff --git a/Apps/Sandcastle/gallery/Voxel Picking.html b/Apps/Sandcastle/gallery/Voxel Picking.html index a10243446139..4877b80625bc 100644 --- a/Apps/Sandcastle/gallery/Voxel Picking.html +++ b/Apps/Sandcastle/gallery/Voxel Picking.html @@ -80,15 +80,17 @@ this.names = ["color"]; this.types = [Cesium.MetadataType.VEC4]; this.componentTypes = [Cesium.MetadataComponentType.FLOAT32]; - this._levelCount = 3; + this.availableLevels = 3; this.globalTransform = globalTransform; } ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) { const { tileLevel, tileX, tileY, tileZ } = options; - if (tileLevel >= this._levelCount) { - return Promise.reject(`No tiles available beyond level ${this._levelCount}`); + if (tileLevel >= this.availableLevels) { + return Promise.reject( + `No tiles available beyond level ${this.availableLevels - 1}`, + ); } const dimensions = this.dimensions; @@ -174,6 +176,7 @@ customShader: customShader, }); voxelPrimitive.nearestSampling = true; + voxelPrimitive.stepSize = 0.7; viewer.scene.primitives.add(voxelPrimitive); camera.flyToBoundingSphere(voxelPrimitive.boundingSphere, { diff --git a/Apps/Sandcastle/gallery/Voxels.html b/Apps/Sandcastle/gallery/Voxels.html index 428cb7db9bf4..b13a599f3b1c 100644 --- a/Apps/Sandcastle/gallery/Voxels.html +++ b/Apps/Sandcastle/gallery/Voxels.html @@ -123,14 +123,14 @@ this.componentTypes = [Cesium.MetadataComponentType.FLOAT32]; this.globalTransform = globalTransform; - this._levelCount = 2; - this._allVoxelData = new Array(this._levelCount); + this.availableLevels = 2; + this._allVoxelData = new Array(this.availableLevels); const allVoxelData = this._allVoxelData; const channelCount = Cesium.MetadataType.getComponentCount(this.types[0]); const { dimensions } = this; - for (let level = 0; level < this._levelCount; level++) { + for (let level = 0; level < this.availableLevels; level++) { const dimAtLevel = Math.pow(2, level); const voxelCountX = dimensions.x * dimAtLevel; const voxelCountY = dimensions.y * dimAtLevel; @@ -158,9 +158,9 @@ ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) { const { tileLevel, tileX, tileY, tileZ } = options; - if (tileLevel >= this._levelCount) { + if (tileLevel >= this.availableLevels) { return Promise.reject( - `No tiles available beyond level ${this._levelCount - 1}`, + `No tiles available beyond level ${this.availableLevels - 1}`, ); } diff --git a/CHANGES.md b/CHANGES.md index a9cdb100e06e..e8a7a93e1692 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,10 @@ ### @cesium/engine +#### Breaking Changes :mega: + +- Voxel rendering now requires a WebGL2 context, which is [enabled by default since 1.101](https://github.com/CesiumGS/cesium/pull/10894). For voxel rendering, make sure the `requestWebGl1` flag in `contextOptions` is NOT set to true. + #### Fixes :wrench: - Materials loaded from type now respect submaterials present in the referenced material type. [#10566](https://github.com/CesiumGS/cesium/issues/10566) @@ -17,6 +21,7 @@ - Prevent runtime errors for certain forms of invalid PNTS files [#12872](https://github.com/CesiumGS/cesium/issues/12872) - Improved performance of clamped labels. [#12905](https://github.com/CesiumGS/cesium/pull/12905) - Fixes issue where multiple instances of a Gaussian splat tileset would transform tile positions incorrectly and render out of position. [#12795](https://github.com/CesiumGS/cesium/issues/12795) +- Converted voxel raymarching to eye coordinates to fix precision issues in large datasets. [#12061](https://github.com/CesiumGS/cesium/issues/12061) #### Additions :tada: diff --git a/packages/engine/Source/Scene/ClippingPlaneCollection.js b/packages/engine/Source/Scene/ClippingPlaneCollection.js index 53faf8d9c2ef..69de5080acb9 100644 --- a/packages/engine/Source/Scene/ClippingPlaneCollection.js +++ b/packages/engine/Source/Scene/ClippingPlaneCollection.js @@ -106,7 +106,7 @@ function ClippingPlaneCollection(options) { * An event triggered when a new clipping plane is added to the collection. Event handlers * are passed the new plane and the index at which it was added. * @type {Event} - * @default Event() + * @readonly */ this.planeAdded = new Event(); @@ -114,7 +114,7 @@ function ClippingPlaneCollection(options) { * An event triggered when a new clipping plane is removed from the collection. Event handlers * are passed the new plane and the index from which it was removed. * @type {Event} - * @default Event() + * @readonly */ this.planeRemoved = new Event(); @@ -472,7 +472,7 @@ ClippingPlaneCollection.prototype.update = function (frameState) { // Compute texture requirements for current planes // In RGBA FLOAT, A plane is 4 floats packed to a RGBA. // In RGBA UNSIGNED_BYTE, A plane is a float in [0, 1) packed to RGBA and an Oct32 quantized normal, - // so 8 bits or 2 pixels in RGBA. + // so 8 bytes or 2 pixels in RGBA. const pixelsNeeded = useFloatTexture ? this.length : this.length * 2; if (defined(clippingPlanesTexture)) { diff --git a/packages/engine/Source/Scene/VoxelBoundsCollection.js b/packages/engine/Source/Scene/VoxelBoundsCollection.js new file mode 100644 index 000000000000..90c51c9a564a --- /dev/null +++ b/packages/engine/Source/Scene/VoxelBoundsCollection.js @@ -0,0 +1,494 @@ +import Cartesian2 from "../Core/Cartesian2.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import Cartesian4 from "../Core/Cartesian4.js"; +import Check from "../Core/Check.js"; +import ClippingPlane from "./ClippingPlane.js"; +import ContextLimits from "../Renderer/ContextLimits.js"; +import defined from "../Core/defined.js"; +import destroyObject from "../Core/destroyObject.js"; +import Event from "../Core/Event.js"; +import Frozen from "../Core/Frozen.js"; +import Intersect from "../Core/Intersect.js"; +import Matrix4 from "../Core/Matrix4.js"; +import PixelFormat from "../Core/PixelFormat.js"; +import PixelDatatype from "../Renderer/PixelDatatype.js"; +import Plane from "../Core/Plane.js"; +import Sampler from "../Renderer/Sampler.js"; +import Texture from "../Renderer/Texture.js"; + +/** + * Specifies a set of clipping planes defining rendering bounds for a {@link VoxelPrimitive}. + * + * @alias VoxelBoundsCollection + * @constructor + * + * @param {object} [options] Object with the following properties: + * @param {ClippingPlane[]} [options.planes=[]] An array of {@link ClippingPlane} objects used to selectively disable rendering on the outside of each plane. + * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix specifying an additional transform relative to the clipping planes original coordinate system. + * @param {boolean} [options.unionClippingRegions=false] If true, a region will be clipped if it is on the outside of any plane in the collection. Otherwise, a region will only be clipped if it is on the outside of every plane. + * + * @private + */ +function VoxelBoundsCollection(options) { + const { + planes, + modelMatrix = Matrix4.IDENTITY, + unionClippingRegions = false, + } = options ?? Frozen.EMPTY_OBJECT; + + this._planes = []; + + /** + * The 4x4 transformation matrix specifying an additional transform relative to the clipping planes + * original coordinate system. + * + * @type {Matrix4} + * @default Matrix4.IDENTITY + */ + this.modelMatrix = Matrix4.clone(modelMatrix); + + /** + * An event triggered when a new clipping plane is added to the collection. Event handlers + * are passed the new plane and the index at which it was added. + * @type {Event} + * @readonly + */ + this.planeAdded = new Event(); + + /** + * An event triggered when a new clipping plane is removed from the collection. Event handlers + * are passed the new plane and the index from which it was removed. + * @type {Event} + * @readonly + */ + this.planeRemoved = new Event(); + + this._unionClippingRegions = unionClippingRegions; + this._testIntersection = unionClippingRegions + ? unionIntersectFunction + : defaultIntersectFunction; + + this._float32View = undefined; + + this._clippingPlanesTexture = undefined; + + // Add each ClippingPlane object. + if (defined(planes)) { + for (let i = 0; i < planes.length; ++i) { + this.add(planes[i]); + } + } +} + +function unionIntersectFunction(value) { + return value === Intersect.OUTSIDE; +} + +function defaultIntersectFunction(value) { + return value === Intersect.INSIDE; +} + +Object.defineProperties(VoxelBoundsCollection.prototype, { + /** + * Returns the number of planes in this collection. This is commonly used with + * {@link VoxelBoundsCollection#get} to iterate over all the planes + * in the collection. + * + * @memberof VoxelBoundsCollection.prototype + * @type {number} + * @readonly + */ + length: { + get: function () { + return this._planes.length; + }, + }, + + /** + * If true, a region will be clipped if it is on the outside of any plane in the + * collection. Otherwise, a region will only be clipped if it is on the + * outside of every plane. + * + * @memberof VoxelBoundsCollection.prototype + * @type {boolean} + * @default false + */ + unionClippingRegions: { + get: function () { + return this._unionClippingRegions; + }, + set: function (value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bool("value", value); + //>>includeEnd('debug'); + if (this._unionClippingRegions === value) { + return; + } + this._unionClippingRegions = value; + this._testIntersection = value + ? unionIntersectFunction + : defaultIntersectFunction; + }, + }, + + /** + * Returns a texture containing packed, untransformed clipping planes. + * + * @memberof VoxelBoundsCollection.prototype + * @type {Texture} + * @readonly + * @private + */ + texture: { + get: function () { + return this._clippingPlanesTexture; + }, + }, + + /** + * Returns a Number encapsulating the state for this VoxelBoundsCollection. + * + * Clipping mode is encoded in the sign of the number, which is just the plane count. + * If this value changes, then shader regeneration is necessary. + * + * @memberof VoxelBoundsCollection.prototype + * @returns {number} A Number that describes the VoxelBoundsCollection's state. + * @readonly + * @private + */ + clippingPlanesState: { + get: function () { + return this._unionClippingRegions + ? this._planes.length + : -this._planes.length; + }, + }, +}); + +/** + * Adds the specified {@link ClippingPlane} to the collection to be used to selectively disable rendering + * on the outside of each plane. Use {@link VoxelBoundsCollection#unionClippingRegions} to modify + * how modify the clipping behavior of multiple planes. + * + * @param {ClippingPlane} plane The ClippingPlane to add to the collection. + * + * @see VoxelBoundsCollection#unionClippingRegions + * @see VoxelBoundsCollection#remove + * @see VoxelBoundsCollection#removeAll + */ +VoxelBoundsCollection.prototype.add = function (plane) { + const newPlaneIndex = this._planes.length; + plane.index = newPlaneIndex; + this._planes.push(plane); + this.planeAdded.raiseEvent(plane, newPlaneIndex); +}; + +/** + * Returns the plane in the collection at the specified index. Indices are zero-based + * and increase as planes are added. Removing a plane shifts all planes after + * it to the left, changing their indices. This function is commonly used with + * {@link VoxelBoundsCollection#length} to iterate over all the planes + * in the collection. + * + * @param {number} index The zero-based index of the plane. + * @returns {ClippingPlane} The ClippingPlane at the specified index. + * + * @see VoxelBoundsCollection#length + */ +VoxelBoundsCollection.prototype.get = function (index) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number("index", index); + //>>includeEnd('debug'); + + return this._planes[index]; +}; + +function indexOf(planes, plane) { + for (let i = 0; i < planes.length; ++i) { + if (Plane.equals(planes[i], plane)) { + return i; + } + } + return -1; +} + +/** + * Checks whether this collection contains a ClippingPlane equal to the given ClippingPlane. + * + * @param {ClippingPlane} [clippingPlane] The ClippingPlane to check for. + * @returns {boolean} true if this collection contains the ClippingPlane, false otherwise. + * + * @see VoxelBoundsCollection#get + */ +VoxelBoundsCollection.prototype.contains = function (clippingPlane) { + return indexOf(this._planes, clippingPlane) !== -1; +}; + +/** + * Removes the first occurrence of the given ClippingPlane from the collection. + * + * @param {ClippingPlane} clippingPlane + * @returns {boolean} true if the plane was removed; false if the plane was not found in the collection. + * + * @see VoxelBoundsCollection#add + * @see VoxelBoundsCollection#contains + * @see VoxelBoundsCollection#removeAll + */ +VoxelBoundsCollection.prototype.remove = function (clippingPlane) { + const planes = this._planes; + const index = indexOf(planes, clippingPlane); + + if (index === -1) { + return false; + } + + // Unlink this VoxelBoundsCollection from the ClippingPlane + if (clippingPlane instanceof ClippingPlane) { + clippingPlane.onChangeCallback = undefined; + clippingPlane.index = -1; + } + + // Shift and update indices + const length = planes.length - 1; + for (let i = index; i < length; ++i) { + const planeToKeep = planes[i + 1]; + planes[i] = planeToKeep; + if (planeToKeep instanceof ClippingPlane) { + planeToKeep.index = i; + } + } + + planes.length = length; + + this.planeRemoved.raiseEvent(clippingPlane, index); + + return true; +}; + +/** + * Removes all planes from the collection. + * + * @see VoxelBoundsCollection#add + * @see VoxelBoundsCollection#remove + */ +VoxelBoundsCollection.prototype.removeAll = function () { + // Dereference this VoxelBoundsCollection from all ClippingPlanes + const planes = this._planes; + for (let i = 0; i < planes.length; ++i) { + const plane = planes[i]; + if (plane instanceof ClippingPlane) { + plane.onChangeCallback = undefined; + plane.index = -1; + } + this.planeRemoved.raiseEvent(plane, i); + } + this._planes = []; +}; + +const scratchPlane = new Plane(Cartesian3.fromElements(1.0, 0.0, 0.0), 0.0); + +// Pack starting at the beginning of the buffer to allow partial update +function transformAndPackPlanes(clippingPlaneCollection, transform) { + const float32View = clippingPlaneCollection._float32View; + const planes = clippingPlaneCollection._planes; + + let floatIndex = 0; + for (let i = 0; i < planes.length; ++i) { + const { normal, distance } = transformPlane( + planes[i], + transform, + scratchPlane, + ); + + float32View[floatIndex] = normal.x; + float32View[floatIndex + 1] = normal.y; + float32View[floatIndex + 2] = normal.z; + float32View[floatIndex + 3] = distance; + + floatIndex += 4; // each plane is 4 floats + } +} + +const scratchPlaneCartesian4 = new Cartesian4(); +const scratchTransformedNormal = new Cartesian3(); + +function transformPlane(plane, transform, result) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("plane", plane); + Check.typeOf.object("transform", transform); + //>>includeEnd('debug'); + + const { normal, distance } = plane; + const planeAsCartesian4 = Cartesian4.fromElements( + normal.x, + normal.y, + normal.z, + distance, + scratchPlaneCartesian4, + ); + let transformedPlane = Matrix4.multiplyByVector( + transform, + planeAsCartesian4, + scratchPlaneCartesian4, + ); + + // Convert the transformed plane to Hessian Normal Form + const transformedNormal = Cartesian3.fromCartesian4( + transformedPlane, + scratchTransformedNormal, + ); + transformedPlane = Cartesian4.divideByScalar( + transformedPlane, + Cartesian3.magnitude(transformedNormal), + scratchPlaneCartesian4, + ); + + return Plane.fromCartesian4(transformedPlane, result); +} + +function computeTextureResolution(pixelsNeeded, result) { + result.x = Math.min(pixelsNeeded, ContextLimits.maximumTextureSize); + result.y = Math.ceil(pixelsNeeded / result.x); + return result; +} + +const textureResolutionScratch = new Cartesian2(); +/** + * Called when {@link Viewer} or {@link CesiumWidget} render the scene to + * build the resources for clipping planes. + *

+ * Do not call this function directly. + *

+ */ +VoxelBoundsCollection.prototype.update = function (frameState, transform) { + let clippingPlanesTexture = this._clippingPlanesTexture; + + // Compute texture requirements for current planes + // In RGBA FLOAT, a plane is 4 floats packed to a single RGBA pixel. + const pixelsNeeded = this.length; + + if (defined(clippingPlanesTexture)) { + const currentPixelCount = + clippingPlanesTexture.width * clippingPlanesTexture.height; + // Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be. + // Optimization note: this isn't exactly the classic resizeable array algorithm + // * not necessarily checking for resize after each add/remove operation + // * random-access deletes instead of just pops + // * alloc ops likely more expensive than demonstrable via big-O analysis + if ( + currentPixelCount < pixelsNeeded || + pixelsNeeded < 0.25 * currentPixelCount + ) { + clippingPlanesTexture.destroy(); + clippingPlanesTexture = undefined; + this._clippingPlanesTexture = undefined; + } + } + + // If there are no bound planes, there's nothing to update. + if (this.length === 0) { + return; + } + + if (!defined(clippingPlanesTexture)) { + const requiredResolution = computeTextureResolution( + pixelsNeeded, + textureResolutionScratch, + ); + // Allocate twice as much space as needed to avoid frequent texture reallocation. + // Allocate in the Y direction, since texture may be as wide as context texture support. + requiredResolution.y *= 2; + + clippingPlanesTexture = new Texture({ + context: frameState.context, + width: requiredResolution.x, + height: requiredResolution.y, + pixelFormat: PixelFormat.RGBA, + pixelDatatype: PixelDatatype.FLOAT, + sampler: Sampler.NEAREST, + flipY: false, + }); + this._float32View = new Float32Array( + requiredResolution.x * requiredResolution.y * 4, + ); + + this._clippingPlanesTexture = clippingPlanesTexture; + } + + const { width, height } = clippingPlanesTexture; + transformAndPackPlanes(this, transform); + clippingPlanesTexture.copyFrom({ + source: { + width: width, + height: height, + arrayBufferView: this._float32View, + }, + }); +}; + +/** + * Function for getting the clipping plane collection's texture resolution. + * If the VoxelBoundsCollection hasn't been updated, returns the resolution that will be + * allocated based on the current plane count. + * + * @param {VoxelBoundsCollection} clippingPlaneCollection The clipping plane collection + * @param {Context} context The rendering context + * @param {Cartesian2} result A Cartesian2 for the result. + * @returns {Cartesian2} The required resolution. + * @private + */ +VoxelBoundsCollection.getTextureResolution = function ( + clippingPlaneCollection, + context, + result, +) { + const texture = clippingPlaneCollection.texture; + if (defined(texture)) { + result.x = texture.width; + result.y = texture.height; + return result; + } + + const pixelsNeeded = clippingPlaneCollection.length; + const requiredResolution = computeTextureResolution(pixelsNeeded, result); + + // Allocate twice as much space as needed to avoid frequent texture reallocation. + requiredResolution.y *= 2; + return requiredResolution; +}; + +/** + * 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. + * + * @see VoxelBoundsCollection#destroy + */ +VoxelBoundsCollection.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. + * + * @example + * voxelBounds = voxelBounds && voxelBounds.destroy(); + * + * @see VoxelBoundsCollection#isDestroyed + */ +VoxelBoundsCollection.prototype.destroy = function () { + this._clippingPlanesTexture = + this._clippingPlanesTexture && this._clippingPlanesTexture.destroy(); + return destroyObject(this); +}; +export default VoxelBoundsCollection; diff --git a/packages/engine/Source/Scene/VoxelBoxShape.js b/packages/engine/Source/Scene/VoxelBoxShape.js index 9c1b893c6e5a..f39bf3d93a3e 100644 --- a/packages/engine/Source/Scene/VoxelBoxShape.js +++ b/packages/engine/Source/Scene/VoxelBoxShape.js @@ -5,6 +5,8 @@ import Check from "../Core/Check.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; +import VoxelBoundsCollection from "./VoxelBoundsCollection.js"; +import ClippingPlane from "./ClippingPlane.js"; /** * A box {@link VoxelShape}. @@ -20,100 +22,186 @@ import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; * @private */ function VoxelBoxShape() { + this._orientedBoundingBox = new OrientedBoundingBox(); + this._boundingSphere = new BoundingSphere(); + this._boundTransform = new Matrix4(); + this._shapeTransform = new Matrix4(); + /** - * An oriented bounding box containing the bounded shape. - * The update function must be called before accessing this value. + * The minimum bounds of the shape. + * @type {Cartesian3} * @private - * @type {OrientedBoundingBox} - * @readonly */ - this.orientedBoundingBox = new OrientedBoundingBox(); + this._minBounds = VoxelBoxShape.DefaultMinBounds.clone(); /** - * A bounding sphere containing the bounded shape. - * The update function must be called before accessing this value. + * The maximum bounds of the shape. + * @type {Cartesian3} * @private - * @type {BoundingSphere} - * @readonly */ - this.boundingSphere = new BoundingSphere(); + this._maxBounds = VoxelBoxShape.DefaultMaxBounds.clone(); /** - * A transformation matrix containing the bounded shape. - * The update function must be called before accessing this value. + * The minimum render bounds of the shape. + * @type {Cartesian3} * @private - * @type {Matrix4} - * @readonly */ - this.boundTransform = new Matrix4(); + this._renderMinBounds = VoxelBoxShape.DefaultMinBounds.clone(); /** - * A transformation matrix containing the shape, ignoring the bounds. - * The update function must be called before accessing this value. + * The maximum render bounds of the shape. + * @type {Cartesian3} * @private - * @type {Matrix4} + */ + this._renderMaxBounds = VoxelBoxShape.DefaultMaxBounds.clone(); + + const { DefaultMinBounds, DefaultMaxBounds } = VoxelBoxShape; + const boundPlanes = [ + new ClippingPlane( + Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), + DefaultMinBounds.x, + ), + new ClippingPlane( + Cartesian3.negate(Cartesian3.UNIT_Y, new Cartesian3()), + DefaultMinBounds.y, + ), + new ClippingPlane( + Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()), + DefaultMinBounds.z, + ), + new ClippingPlane(Cartesian3.UNIT_X, -DefaultMaxBounds.x), + new ClippingPlane(Cartesian3.UNIT_Y, -DefaultMaxBounds.y), + new ClippingPlane(Cartesian3.UNIT_Z, -DefaultMaxBounds.z), + ]; + + this._renderBoundPlanes = new VoxelBoundsCollection({ planes: boundPlanes }); + + this._shaderUniforms = { + boxEcToXyz: new Matrix3(), + boxLocalToShapeUvScale: new Cartesian3(), + boxLocalToShapeUvTranslate: new Cartesian3(), + }; + + this._shaderDefines = { + BOX_INTERSECTION_INDEX: undefined, + }; + + this._shaderMaximumIntersectionsLength = 0; // not known until update +} + +Object.defineProperties(VoxelBoxShape.prototype, { + /** + * An oriented bounding box containing the bounded shape. + * + * @memberof VoxelBoxShape.prototype + * @type {OrientedBoundingBox} * @readonly + * @private */ - this.shapeTransform = new Matrix4(); + orientedBoundingBox: { + get: function () { + return this._orientedBoundingBox; + }, + }, /** - * The minimum bounds of the shape. - * @type {Cartesian3} + * A collection of planes used for the render bounds + * @memberof VoxelBoxShape.prototype + * @type {VoxelBoundsCollection} + * @readonly * @private */ - this._minBounds = VoxelBoxShape.DefaultMinBounds.clone(); + renderBoundPlanes: { + get: function () { + return this._renderBoundPlanes; + }, + }, /** - * The maximum bounds of the shape. - * @type {Cartesian3} + * A bounding sphere containing the bounded shape. + * + * @memberof VoxelBoxShape.prototype + * @type {BoundingSphere} + * @readonly * @private */ - this._maxBounds = VoxelBoxShape.DefaultMaxBounds.clone(); + boundingSphere: { + get: function () { + return this._boundingSphere; + }, + }, /** + * A transformation matrix containing the bounded shape. + * + * @memberof VoxelBoxShape.prototype + * @type {Matrix4} + * @readonly * @private - * @type {Object} + */ + boundTransform: { + get: function () { + return this._boundTransform; + }, + }, + + /** + * A transformation matrix containing the shape, ignoring the bounds. + * + * @memberof VoxelBoxShape.prototype + * @type {Matrix4} * @readonly + * @private */ - this.shaderUniforms = { - renderMinBounds: new Cartesian3(), - renderMaxBounds: new Cartesian3(), - boxUvToShapeUvScale: new Cartesian3(), - boxUvToShapeUvTranslate: new Cartesian3(), - }; + shapeTransform: { + get: function () { + return this._shapeTransform; + }, + }, /** + * @memberof VoxelBoxShape.prototype + * @type {Object} + * @readonly * @private + */ + shaderUniforms: { + get: function () { + return this._shaderUniforms; + }, + }, + + /** + * @memberof VoxelBoxShape.prototype * @type {Object} * @readonly + * @private */ - this.shaderDefines = { - BOX_INTERSECTION_INDEX: undefined, - BOX_HAS_SHAPE_BOUNDS: undefined, - }; + shaderDefines: { + get: function () { + return this._shaderDefines; + }, + }, /** * The maximum number of intersections against the shape for any ray direction. - * @private + * @memberof VoxelBoxShape.prototype * @type {number} * @readonly + * @private */ - this.shaderMaximumIntersectionsLength = 0; // not known until update -} + shaderMaximumIntersectionsLength: { + get: function () { + return this._shaderMaximumIntersectionsLength; + }, + }, +}); const scratchCenter = new Cartesian3(); const scratchScale = new Cartesian3(); const scratchRotation = new Matrix3(); const scratchClipMinBounds = new Cartesian3(); const scratchClipMaxBounds = new Cartesian3(); -const scratchRenderMinBounds = new Cartesian3(); -const scratchRenderMaxBounds = new Cartesian3(); - -const transformLocalToUv = Matrix4.fromRotationTranslation( - Matrix3.fromUniformScale(0.5, new Matrix3()), - new Cartesian3(0.5, 0.5, 0.5), - new Matrix4(), -); /** * Update the shape's state. @@ -148,13 +236,13 @@ VoxelBoxShape.prototype.update = function ( minBounds, clipMinBounds, clipMaxBounds, - scratchRenderMinBounds, + this._renderMinBounds, ); const renderMaxBounds = Cartesian3.clamp( maxBounds, clipMinBounds, clipMaxBounds, - scratchRenderMaxBounds, + this._renderMaxBounds, ); // Box is not visible if: @@ -177,29 +265,39 @@ VoxelBoxShape.prototype.update = function ( return false; } - this.shapeTransform = Matrix4.clone(modelMatrix, this.shapeTransform); + // Update the render bounds planes + const renderBoundPlanes = this._renderBoundPlanes; + renderBoundPlanes.get(0).distance = renderMinBounds.x; + renderBoundPlanes.get(1).distance = renderMinBounds.y; + renderBoundPlanes.get(2).distance = renderMinBounds.z; + renderBoundPlanes.get(3).distance = -renderMaxBounds.x; + renderBoundPlanes.get(4).distance = -renderMaxBounds.y; + renderBoundPlanes.get(5).distance = -renderMaxBounds.z; - this.orientedBoundingBox = getBoxChunkObb( + this._shapeTransform = Matrix4.clone(modelMatrix, this._shapeTransform); + + this._orientedBoundingBox = getBoxChunkObb( renderMinBounds, renderMaxBounds, - this.shapeTransform, - this.orientedBoundingBox, + this._shapeTransform, + this._orientedBoundingBox, ); // All of the box bounds go from -1 to +1, so the model matrix scale can be // used as the oriented bounding box half axes. - this.boundTransform = Matrix4.fromRotationTranslation( - this.orientedBoundingBox.halfAxes, - this.orientedBoundingBox.center, - this.boundTransform, + this._boundTransform = Matrix4.fromRotationTranslation( + this._orientedBoundingBox.halfAxes, + this._orientedBoundingBox.center, + this._boundTransform, ); - this.boundingSphere = BoundingSphere.fromOrientedBoundingBox( - this.orientedBoundingBox, - this.boundingSphere, + this._boundingSphere = BoundingSphere.fromOrientedBoundingBox( + this._orientedBoundingBox, + this._boundingSphere, ); - const { shaderUniforms, shaderDefines } = this; + const shaderUniforms = this._shaderUniforms; + const shaderDefines = this._shaderDefines; // To keep things simple, clear the defines every time for (const key in shaderDefines) { @@ -214,49 +312,87 @@ VoxelBoxShape.prototype.update = function ( shaderDefines["BOX_INTERSECTION_INDEX"] = intersectionCount; intersectionCount += 1; - shaderUniforms.renderMinBounds = Matrix4.multiplyByPoint( - transformLocalToUv, - renderMinBounds, - shaderUniforms.renderMinBounds, + // Compute scale and translation to transform from UV space to bounded UV space + const min = minBounds; + const max = maxBounds; + const boxLocalToShapeUvScale = Cartesian3.fromElements( + boundScale(min.x, max.x), + boundScale(min.y, max.y), + boundScale(min.z, max.z), + shaderUniforms.boxLocalToShapeUvScale, ); - shaderUniforms.renderMaxBounds = Matrix4.multiplyByPoint( - transformLocalToUv, - renderMaxBounds, - shaderUniforms.renderMaxBounds, + shaderUniforms.boxLocalToShapeUvTranslate = Cartesian3.negate( + Cartesian3.multiplyComponents( + boxLocalToShapeUvScale, + min, + shaderUniforms.boxLocalToShapeUvTranslate, + ), + shaderUniforms.boxLocalToShapeUvTranslate, ); - shaderDefines["BOX_HAS_SHAPE_BOUNDS"] = true; + this._shaderMaximumIntersectionsLength = intersectionCount; - const min = minBounds; - const max = maxBounds; + return true; +}; - // Go from UV space to bounded UV space: - // delerp(posUv, minBoundsUv, maxBoundsUv) - // (posUv - minBoundsUv) / (maxBoundsUv - minBoundsUv) - // posUv / (maxBoundsUv - minBoundsUv) - minBoundsUv / (maxBoundsUv - minBoundsUv) - // scale = 1.0 / (maxBoundsUv - minBoundsUv) - // scale = 1.0 / ((maxBounds * 0.5 + 0.5) - (minBounds * 0.5 + 0.5)) - // scale = 2.0 / (maxBounds - minBounds) - // offset = -minBoundsUv / ((maxBounds * 0.5 + 0.5) - (minBounds * 0.5 + 0.5)) - // offset = -2.0 * (minBounds * 0.5 + 0.5) / (maxBounds - minBounds) - // offset = -scale * (minBounds * 0.5 + 0.5) - shaderUniforms.boxUvToShapeUvScale = Cartesian3.fromElements( - 2.0 / (min.x === max.x ? 1.0 : max.x - min.x), - 2.0 / (min.y === max.y ? 1.0 : max.y - min.y), - 2.0 / (min.z === max.z ? 1.0 : max.z - min.z), - shaderUniforms.boxUvToShapeUvScale, - ); +function boundScale(minBound, maxBound) { + return CesiumMath.equalsEpsilon(minBound, maxBound, CesiumMath.EPSILON7) + ? 1.0 + : 1.0 / (maxBound - minBound); +} - shaderUniforms.boxUvToShapeUvTranslate = Cartesian3.fromElements( - -shaderUniforms.boxUvToShapeUvScale.x * (min.x * 0.5 + 0.5), - -shaderUniforms.boxUvToShapeUvScale.y * (min.y * 0.5 + 0.5), - -shaderUniforms.boxUvToShapeUvScale.z * (min.z * 0.5 + 0.5), - shaderUniforms.boxUvToShapeUvTranslate, +const scratchTransformPositionWorldToLocal = new Matrix4(); +/** + * Update any view-dependent transforms. + * @private + * @param {FrameState} frameState The frame state. + */ +VoxelBoxShape.prototype.updateViewTransforms = function (frameState) { + const shaderUniforms = this._shaderUniforms; + const transformPositionWorldToLocal = Matrix4.inverse( + this._shapeTransform, + scratchTransformPositionWorldToLocal, + ); + const transformDirectionWorldToLocal = Matrix4.getMatrix3( + transformPositionWorldToLocal, + shaderUniforms.boxEcToXyz, ); + const rotateViewToWorld = frameState.context.uniformState.inverseViewRotation; + Matrix3.multiply( + transformDirectionWorldToLocal, + rotateViewToWorld, + shaderUniforms.boxEcToXyz, + ); +}; - this.shaderMaximumIntersectionsLength = intersectionCount; +/** + * Convert a local coordinate to the shape's UV space. + * @private + * @param {Cartesian3} positionLocal The local coordinate to convert. + * @param {Cartesian3} result The Cartesian3 to store the result in. + * @returns {Cartesian3} The converted UV coordinate. + */ +VoxelBoxShape.prototype.convertLocalToShapeUvSpace = function ( + positionLocal, + result, +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("positionLocal", positionLocal); + Check.typeOf.object("result", result); + //>>includeEnd('debug'); - return true; + const { boxLocalToShapeUvScale, boxLocalToShapeUvTranslate } = + this._shaderUniforms; + + return Cartesian3.add( + Cartesian3.multiplyComponents( + positionLocal, + boxLocalToShapeUvScale, + result, + ), + boxLocalToShapeUvTranslate, + result, + ); }; const scratchTileMinBounds = new Cartesian3(); @@ -264,7 +400,6 @@ const scratchTileMaxBounds = new Cartesian3(); /** * Computes an oriented bounding box for a specified tile. - * The update function must be called before calling this function. * @private * @param {number} tileLevel The tile's level. * @param {number} tileX The tile's x coordinate. @@ -309,7 +444,7 @@ VoxelBoxShape.prototype.computeOrientedBoundingBoxForTile = function ( return getBoxChunkObb( tileMinBounds, tileMaxBounds, - this.shapeTransform, + this._shapeTransform, result, ); }; @@ -318,7 +453,6 @@ const sampleSizeScratch = new Cartesian3(); /** * Computes an oriented bounding box for a specified sample within a specified tile. - * The update function must be called before calling this function. * @private * @param {SpatialNode} spatialNode The spatial node containing the sample * @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding @@ -385,7 +519,7 @@ VoxelBoxShape.prototype.computeOrientedBoundingBoxForSample = function ( return getBoxChunkObb( sampleMinBounds, sampleMaxBounds, - this.shapeTransform, + this._shapeTransform, result, ); }; @@ -412,6 +546,7 @@ VoxelBoxShape.DefaultMaxBounds = Object.freeze( new Cartesian3(+1.0, +1.0, +1.0), ); +const scratchBoxScale = new Cartesian3(); /** * Computes an {@link OrientedBoundingBox} for a subregion of the shape. * @@ -437,7 +572,7 @@ function getBoxChunkObb(minimumBounds, maximumBounds, matrix, result) { result.center = Matrix4.getTranslation(matrix, result.center); result.halfAxes = Matrix4.getMatrix3(matrix, result.halfAxes); } else { - let scale = Matrix4.getScale(matrix, scratchScale); + let scale = Matrix4.getScale(matrix, scratchBoxScale); const localCenter = Cartesian3.midpoint( minimumBounds, maximumBounds, @@ -448,7 +583,7 @@ function getBoxChunkObb(minimumBounds, maximumBounds, matrix, result) { scale.x * 0.5 * (maximumBounds.x - minimumBounds.x), scale.y * 0.5 * (maximumBounds.y - minimumBounds.y), scale.z * 0.5 * (maximumBounds.z - minimumBounds.z), - scratchScale, + scratchBoxScale, ); const rotation = Matrix4.getRotation(matrix, scratchRotation); result.halfAxes = Matrix3.setScale(rotation, scale, result.halfAxes); diff --git a/packages/engine/Source/Scene/VoxelCylinderShape.js b/packages/engine/Source/Scene/VoxelCylinderShape.js index b19759672570..4bb78629802b 100644 --- a/packages/engine/Source/Scene/VoxelCylinderShape.js +++ b/packages/engine/Source/Scene/VoxelCylinderShape.js @@ -4,9 +4,11 @@ import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import CesiumMath from "../Core/Math.js"; import Check from "../Core/Check.js"; +import ClippingPlane from "./ClippingPlane.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; +import VoxelBoundsCollection from "./VoxelBoundsCollection.js"; /** * A cylinder {@link VoxelShape}. @@ -22,111 +24,179 @@ import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; * @private */ function VoxelCylinderShape() { + this._orientedBoundingBox = new OrientedBoundingBox(); + this._boundingSphere = new BoundingSphere(); + this._boundTransform = new Matrix4(); + this._shapeTransform = new Matrix4(); + /** - * An oriented bounding box containing the bounded shape. - * The update function must be called before accessing this value. + * The minimum bounds of the shape, corresponding to minimum radius, angle, and height. + * @type {Cartesian3} * @private - * @type {OrientedBoundingBox} - * @readonly */ - this.orientedBoundingBox = new OrientedBoundingBox(); + this._minBounds = VoxelCylinderShape.DefaultMinBounds.clone(); /** - * A bounding sphere containing the bounded shape. - * The update function must be called before accessing this value. + * The maximum bounds of the shape, corresponding to maximum radius, angle, and height. + * @type {Cartesian3} * @private - * @type {BoundingSphere} - * @readonly */ - this.boundingSphere = new BoundingSphere(); + this._maxBounds = VoxelCylinderShape.DefaultMaxBounds.clone(); + + const { DefaultMinBounds, DefaultMaxBounds } = VoxelCylinderShape; + const boundPlanes = [ + new ClippingPlane( + Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()), + DefaultMinBounds.z, + ), + new ClippingPlane(Cartesian3.UNIT_Z, -DefaultMaxBounds.z), + ]; + + this._renderBoundPlanes = new VoxelBoundsCollection({ planes: boundPlanes }); + + this._shaderUniforms = { + cameraShapePosition: new Cartesian3(), + cylinderEcToRadialTangentUp: new Matrix3(), + cylinderRenderRadiusMinMax: new Cartesian2(), + cylinderRenderAngleMinMax: new Cartesian2(), + cylinderLocalToShapeUvRadius: new Cartesian2(), + cylinderLocalToShapeUvAngle: new Cartesian2(), + cylinderLocalToShapeUvHeight: new Cartesian2(), + cylinderShapeUvAngleRangeOrigin: 0.0, + }; + + this._shaderDefines = { + CYLINDER_HAS_SHAPE_BOUNDS_ANGLE: undefined, + CYLINDER_HAS_RENDER_BOUNDS_RADIUS_MIN: undefined, + CYLINDER_HAS_RENDER_BOUNDS_RADIUS_FLAT: undefined, + CYLINDER_HAS_RENDER_BOUNDS_ANGLE: undefined, + CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_EQUAL_ZERO: undefined, + CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_UNDER_HALF: undefined, + CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_OVER_HALF: undefined, + CYLINDER_INTERSECTION_INDEX_RADIUS_MAX: undefined, + CYLINDER_INTERSECTION_INDEX_RADIUS_MIN: undefined, + CYLINDER_INTERSECTION_INDEX_ANGLE: undefined, + }; + + this._shaderMaximumIntersectionsLength = 0; // not known until update +} +Object.defineProperties(VoxelCylinderShape.prototype, { /** - * A transformation matrix containing the bounded shape. - * The update function must be called before accessing this value. - * @private - * @type {Matrix4} + * An oriented bounding box containing the bounded shape. + * + * @memberof VoxelCylinderShape.prototype + * @type {OrientedBoundingBox} * @readonly + * @private */ - this.boundTransform = new Matrix4(); + orientedBoundingBox: { + get: function () { + return this._orientedBoundingBox; + }, + }, /** - * A transformation matrix containing the shape, ignoring the bounds. - * The update function must be called before accessing this value. - * @private - * @type {Matrix4} + * A collection of planes used for the render bounds + * @memberof VoxelCylinderShape.prototype + * @type {VoxelBoundsCollection} * @readonly + * @private */ - this.shapeTransform = new Matrix4(); + renderBoundPlanes: { + get: function () { + return this._renderBoundPlanes; + }, + }, /** - * The minimum bounds of the shape, corresponding to minimum radius, angle, and height. - * @type {Cartesian3} + * A bounding sphere containing the bounded shape. + * + * @memberof VoxelCylinderShape.prototype + * @type {BoundingSphere} + * @readonly * @private */ - this._minBounds = VoxelCylinderShape.DefaultMinBounds.clone(); + boundingSphere: { + get: function () { + return this._boundingSphere; + }, + }, /** - * The maximum bounds of the shape, corresponding to maximum radius, angle, and height. - * @type {Cartesian3} + * A transformation matrix containing the bounded shape. + * + * @memberof VoxelCylinderShape.prototype + * @type {Matrix4} + * @readonly * @private */ - this._maxBounds = VoxelCylinderShape.DefaultMaxBounds.clone(); + boundTransform: { + get: function () { + return this._boundTransform; + }, + }, /** + * A transformation matrix containing the shape, ignoring the bounds. + * + * @memberof VoxelCylinderShape.prototype + * @type {Matrix4} + * @readonly * @private + */ + shapeTransform: { + get: function () { + return this._shapeTransform; + }, + }, + + /** + * @memberof VoxelCylinderShape.prototype * @type {Object} * @readonly + * @private */ - this.shaderUniforms = { - cylinderRenderRadiusMinMax: new Cartesian2(), - cylinderRenderAngleMinMax: new Cartesian2(), - cylinderRenderHeightMinMax: new Cartesian2(), - cylinderUvToShapeUvRadius: new Cartesian2(), - cylinderUvToShapeUvAngle: new Cartesian2(), - cylinderUvToShapeUvHeight: new Cartesian2(), - cylinderShapeUvAngleMinMax: new Cartesian2(), - cylinderShapeUvAngleRangeZeroMid: 0.0, - }; + shaderUniforms: { + get: function () { + return this._shaderUniforms; + }, + }, /** - * @private + * @memberof VoxelCylinderShape.prototype * @type {Object} * @readonly + * @private */ - this.shaderDefines = { - CYLINDER_HAS_RENDER_BOUNDS_RADIUS_MIN: undefined, - CYLINDER_HAS_RENDER_BOUNDS_RADIUS_FLAT: undefined, - CYLINDER_HAS_RENDER_BOUNDS_ANGLE: undefined, - CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_EQUAL_ZERO: undefined, - CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_UNDER_HALF: undefined, - CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_OVER_HALF: undefined, - - CYLINDER_HAS_SHAPE_BOUNDS_RADIUS: undefined, - CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT: undefined, - CYLINDER_HAS_SHAPE_BOUNDS_ANGLE: undefined, - CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY: undefined, - CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY: undefined, - CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED: undefined, - - CYLINDER_INTERSECTION_INDEX_RADIUS_MAX: undefined, - CYLINDER_INTERSECTION_INDEX_RADIUS_MIN: undefined, - CYLINDER_INTERSECTION_INDEX_ANGLE: undefined, - }; + shaderDefines: { + get: function () { + return this._shaderDefines; + }, + }, /** * The maximum number of intersections against the shape for any ray direction. - * @private + * @memberof VoxelCylinderShape.prototype * @type {number} * @readonly + * @private */ - this.shaderMaximumIntersectionsLength = 0; // not known until update -} + shaderMaximumIntersectionsLength: { + get: function () { + return this._shaderMaximumIntersectionsLength; + }, + }, +}); const scratchScale = new Cartesian3(); const scratchClipMinBounds = new Cartesian3(); const scratchClipMaxBounds = new Cartesian3(); const scratchRenderMinBounds = new Cartesian3(); const scratchRenderMaxBounds = new Cartesian3(); +const scratchTransformPositionWorldToLocal = new Matrix4(); +const scratchCameraPositionLocal = new Cartesian3(); +const scratchCameraRadialPosition = new Cartesian2(); /** * Update the shape's state. @@ -158,15 +228,15 @@ VoxelCylinderShape.prototype.update = function ( maxBounds = Cartesian3.clone(maxBounds, this._maxBounds); const { DefaultMinBounds, DefaultMaxBounds } = VoxelCylinderShape; - const defaultAngleRange = DefaultMaxBounds.y - DefaultMinBounds.y; - const defaultAngleRangeHalf = 0.5 * defaultAngleRange; + const defaultAngleRange = DefaultMaxBounds.y - DefaultMinBounds.y; // == 2 * PI + const defaultAngleRangeHalf = 0.5 * defaultAngleRange; // == PI const epsilonZeroScale = CesiumMath.EPSILON10; - const epsilonAngleDiscontinuity = CesiumMath.EPSILON3; // 0.001 radians = 0.05729578 degrees const epsilonAngle = CesiumMath.EPSILON10; // Clamp the bounds to the valid range minBounds.x = Math.max(0.0, minBounds.x); + // TODO: require maxBounds.x >= minBounds.x ? maxBounds.x = Math.max(0.0, maxBounds.x); minBounds.y = CesiumMath.negativePiToPi(minBounds.y); maxBounds.y = CesiumMath.negativePiToPi(maxBounds.y); @@ -174,6 +244,10 @@ VoxelCylinderShape.prototype.update = function ( clipMinBounds.y = CesiumMath.negativePiToPi(clipMinBounds.y); clipMaxBounds.y = CesiumMath.negativePiToPi(clipMaxBounds.y); + // TODO: what does this do with partial volumes crossing the antimeridian? + // We could have minBounds.y = +PI/2 and maxBounds.y = -PI/2. + // Then clipMinBounds.y = +PI/4 and clipMaxBounds.y = -PI/4. + // This maximumByComponent would cancel the clipping. const renderMinBounds = Cartesian3.maximumByComponent( minBounds, clipMinBounds, @@ -205,57 +279,35 @@ VoxelCylinderShape.prototype.update = function ( return false; } - this.shapeTransform = Matrix4.clone(modelMatrix, this.shapeTransform); + // Update the render bounds planes + const renderBoundPlanes = this._renderBoundPlanes; + renderBoundPlanes.get(0).distance = renderMinBounds.z; + renderBoundPlanes.get(1).distance = -renderMaxBounds.z; - this.orientedBoundingBox = getCylinderChunkObb( + this._shapeTransform = Matrix4.clone(modelMatrix, this._shapeTransform); + + this._orientedBoundingBox = getCylinderChunkObb( renderMinBounds, renderMaxBounds, - this.shapeTransform, - this.orientedBoundingBox, + this._shapeTransform, + this._orientedBoundingBox, ); - this.boundTransform = Matrix4.fromRotationTranslation( - this.orientedBoundingBox.halfAxes, - this.orientedBoundingBox.center, - this.boundTransform, + this._boundTransform = Matrix4.fromRotationTranslation( + this._orientedBoundingBox.halfAxes, + this._orientedBoundingBox.center, + this._boundTransform, ); - this.boundingSphere = BoundingSphere.fromOrientedBoundingBox( - this.orientedBoundingBox, - this.boundingSphere, + this._boundingSphere = BoundingSphere.fromOrientedBoundingBox( + this._orientedBoundingBox, + this._boundingSphere, ); - const shapeIsDefaultRadius = - minBounds.x === DefaultMinBounds.x && maxBounds.x === DefaultMaxBounds.x; const shapeIsAngleReversed = maxBounds.y < minBounds.y; const shapeAngleRange = maxBounds.y - minBounds.y + shapeIsAngleReversed * defaultAngleRange; - const shapeIsAngleRegular = - shapeAngleRange > defaultAngleRangeHalf + epsilonAngle && - shapeAngleRange < defaultAngleRange - epsilonAngle; - const shapeIsAngleFlipped = - shapeAngleRange < defaultAngleRangeHalf - epsilonAngle; - const shapeIsAngleRangeHalf = - shapeAngleRange >= defaultAngleRangeHalf - epsilonAngle && - shapeAngleRange <= defaultAngleRangeHalf + epsilonAngle; - const shapeHasAngle = - shapeIsAngleRegular || shapeIsAngleFlipped || shapeIsAngleRangeHalf; - const shapeIsMinAngleDiscontinuity = CesiumMath.equalsEpsilon( - minBounds.y, - DefaultMinBounds.y, - undefined, - epsilonAngleDiscontinuity, - ); - const shapeIsMaxAngleDiscontinuity = CesiumMath.equalsEpsilon( - maxBounds.y, - DefaultMaxBounds.y, - undefined, - epsilonAngleDiscontinuity, - ); - const shapeIsDefaultHeight = - minBounds.z === DefaultMinBounds.z && maxBounds.z === DefaultMaxBounds.z; - const renderIsDefaultMinRadius = renderMinBounds.x === DefaultMinBounds.x; const renderIsAngleReversed = renderMaxBounds.y < renderMinBounds.y; const renderAngleRange = renderMaxBounds.y - @@ -271,7 +323,8 @@ VoxelCylinderShape.prototype.update = function ( const renderHasAngle = renderIsAngleRegular || renderIsAngleFlipped || renderIsAngleRangeZero; - const { shaderUniforms, shaderDefines } = this; + const shaderUniforms = this._shaderUniforms; + const shaderDefines = this._shaderDefines; // To keep things simple, clear the defines every time for (const key in shaderDefines) { @@ -286,7 +339,11 @@ VoxelCylinderShape.prototype.update = function ( shaderDefines["CYLINDER_INTERSECTION_INDEX_RADIUS_MAX"] = intersectionCount; intersectionCount += 1; - if (!renderIsDefaultMinRadius) { + if (shapeAngleRange < defaultAngleRange - epsilonAngle) { + shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE"] = true; + } + + if (renderMinBounds.x !== DefaultMinBounds.x) { shaderDefines["CYLINDER_HAS_RENDER_BOUNDS_RADIUS_MIN"] = true; shaderDefines["CYLINDER_INTERSECTION_INDEX_RADIUS_MIN"] = intersectionCount; intersectionCount += 1; @@ -300,65 +357,32 @@ VoxelCylinderShape.prototype.update = function ( if (renderMinBounds.x === renderMaxBounds.x) { shaderDefines["CYLINDER_HAS_RENDER_BOUNDS_RADIUS_FLAT"] = true; } - if (!shapeIsDefaultRadius) { - shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_RADIUS"] = true; - - // delerp(radius, minRadius, maxRadius) - // (radius - minRadius) / (maxRadius - minRadius) - // radius / (maxRadius - minRadius) - minRadius / (maxRadius - minRadius) - // scale = 1.0 / (maxRadius - minRadius) - // offset = -minRadius / (maxRadius - minRadius) - // offset = minRadius / (minRadius - maxRadius) - const radiusRange = maxBounds.x - minBounds.x; - let scale = 0.0; - let offset = 1.0; - if (radiusRange !== 0.0) { - scale = 1.0 / radiusRange; - offset = -minBounds.x / radiusRange; - } - shaderUniforms.cylinderUvToShapeUvRadius = Cartesian2.fromElements( - scale, - offset, - shaderUniforms.cylinderUvToShapeUvRadius, - ); - } - if (!shapeIsDefaultHeight) { - shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT"] = true; - - // delerp(heightUv, minHeightUv, maxHeightUv) - // (heightUv - minHeightUv) / (maxHeightUv - minHeightUv) - // heightUv / (maxHeightUv - minHeightUv) - minHeightUv / (maxHeightUv - minHeightUv) - // scale = 1.0 / (maxHeightUv - minHeightUv) - // scale = 1.0 / ((maxHeight * 0.5 + 0.5) - (minHeight * 0.5 + 0.5)) - // scale = 2.0 / (maxHeight - minHeight) - // offset = -minHeightUv / (maxHeightUv - minHeightUv) - // offset = -minHeightUv / ((maxHeight * 0.5 + 0.5) - (minHeight * 0.5 + 0.5)) - // offset = -2.0 * (minHeight * 0.5 + 0.5) / (maxHeight - minHeight) - // offset = -(minHeight + 1.0) / (maxHeight - minHeight) - // offset = (minHeight + 1.0) / (minHeight - maxHeight) - const heightRange = maxBounds.z - minBounds.z; - let scale = 0.0; - let offset = 1.0; - if (heightRange !== 0.0) { - scale = 2.0 / heightRange; - offset = -(minBounds.z + 1.0) / heightRange; - } - shaderUniforms.cylinderUvToShapeUvHeight = Cartesian2.fromElements( - scale, - offset, - shaderUniforms.cylinderUvToShapeUvHeight, - ); + const radiusRange = maxBounds.x - minBounds.x; + let radialScale = 0.0; + let radialOffset = 1.0; + if (radiusRange !== 0.0) { + radialScale = 1.0 / radiusRange; + radialOffset = -minBounds.x * radialScale; } - shaderUniforms.cylinderRenderHeightMinMax = Cartesian2.fromElements( - renderMinBounds.z, - renderMaxBounds.z, - shaderUniforms.cylinderRenderHeightMinMax, + shaderUniforms.cylinderLocalToShapeUvRadius = Cartesian2.fromElements( + radialScale, + radialOffset, + shaderUniforms.cylinderLocalToShapeUvRadius, ); - if (shapeIsAngleReversed) { - shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED"] = true; + const heightRange = maxBounds.z - minBounds.z; // Default 2.0 + let heightScale = 0.0; + let heightOffset = 1.0; + if (heightRange !== 0.0) { + heightScale = 1.0 / heightRange; + heightOffset = -minBounds.z * heightScale; } + shaderUniforms.cylinderLocalToShapeUvHeight = Cartesian2.fromElements( + heightScale, + heightOffset, + shaderUniforms.cylinderLocalToShapeUvHeight, + ); if (renderHasAngle) { shaderDefines["CYLINDER_HAS_RENDER_BOUNDS_ANGLE"] = true; @@ -382,64 +406,151 @@ VoxelCylinderShape.prototype.update = function ( ); } - if (shapeHasAngle) { - shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE"] = true; - if (shapeIsMinAngleDiscontinuity) { - shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY"] = true; - } - if (shapeIsMaxAngleDiscontinuity) { - shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY"] = true; - } + const uvMinAngle = (minBounds.y - DefaultMinBounds.y) / defaultAngleRange; + const uvMaxAngle = (maxBounds.y - DefaultMinBounds.y) / defaultAngleRange; + const uvAngleRangeZero = 1.0 - shapeAngleRange / defaultAngleRange; - const uvMinAngle = (minBounds.y - DefaultMinBounds.y) / defaultAngleRange; - const uvMaxAngle = (maxBounds.y - DefaultMinBounds.y) / defaultAngleRange; - const uvAngleRangeZero = 1.0 - shapeAngleRange / defaultAngleRange; + // Translate the origin of UV angles (in [0,1]) to the center of the unoccupied space + const uvAngleRangeOrigin = (uvMaxAngle + 0.5 * uvAngleRangeZero) % 1.0; + shaderUniforms.cylinderShapeUvAngleRangeOrigin = uvAngleRangeOrigin; - shaderUniforms.cylinderShapeUvAngleMinMax = Cartesian2.fromElements( - uvMinAngle, - uvMaxAngle, - shaderUniforms.cylinderShapeUvAngleMinMax, + if (shapeAngleRange <= epsilonAngle) { + shaderUniforms.cylinderLocalToShapeUvAngle = Cartesian2.fromElements( + 0.0, + 1.0, + shaderUniforms.cylinderLocalToShapeUvAngle, + ); + } else { + const scale = defaultAngleRange / shapeAngleRange; + const shiftedMinAngle = uvMinAngle - uvAngleRangeOrigin; + const offset = -scale * (shiftedMinAngle - Math.floor(shiftedMinAngle)); + shaderUniforms.cylinderLocalToShapeUvAngle = Cartesian2.fromElements( + scale, + offset, + shaderUniforms.cylinderLocalToShapeUvAngle, ); - shaderUniforms.cylinderShapeUvAngleRangeZeroMid = - (uvMaxAngle + 0.5 * uvAngleRangeZero) % 1.0; - - // delerp(angleUv, uvMinAngle, uvMaxAngle) - // (angelUv - uvMinAngle) / (uvMaxAngle - uvMinAngle) - // angleUv / (uvMaxAngle - uvMinAngle) - uvMinAngle / (uvMaxAngle - uvMinAngle) - // scale = 1.0 / (uvMaxAngle - uvMinAngle) - // scale = 1.0 / (((maxAngle - pi) / (2.0 * pi)) - ((minAngle - pi) / (2.0 * pi))) - // scale = 2.0 * pi / (maxAngle - minAngle) - // offset = -uvMinAngle / (uvMaxAngle - uvMinAngle) - // offset = -((minAngle - pi) / (2.0 * pi)) / (((maxAngle - pi) / (2.0 * pi)) - ((minAngle - pi) / (2.0 * pi))) - // offset = -(minAngle - pi) / (maxAngle - minAngle) - if (shapeAngleRange <= epsilonAngle) { - shaderUniforms.cylinderUvToShapeUvAngle = Cartesian2.fromElements( - 0.0, - 1.0, - shaderUniforms.cylinderUvToShapeUvAngle, - ); - } else { - const scale = defaultAngleRange / shapeAngleRange; - const offset = -(minBounds.y - DefaultMinBounds.y) / shapeAngleRange; - shaderUniforms.cylinderUvToShapeUvAngle = Cartesian2.fromElements( - scale, - offset, - shaderUniforms.cylinderUvToShapeUvAngle, - ); - } } - this.shaderMaximumIntersectionsLength = intersectionCount; + this._shaderMaximumIntersectionsLength = intersectionCount; return true; }; +const scratchRotateRtuToLocal = new Matrix3(); +const scratchRtuRotation = new Matrix3(); +const scratchTransformPositionViewToLocal = new Matrix4(); + +/** + * Update any view-dependent transforms. + * @private + * @param {FrameState} frameState The frame state. + */ +VoxelCylinderShape.prototype.updateViewTransforms = function (frameState) { + const shaderUniforms = this._shaderUniforms; + // 1. Update camera position in cylindrical coordinates + const transformPositionWorldToLocal = Matrix4.inverse( + this._shapeTransform, + scratchTransformPositionWorldToLocal, + ); + const cameraPositionLocal = Matrix4.multiplyByPoint( + transformPositionWorldToLocal, + frameState.camera.positionWC, + scratchCameraPositionLocal, + ); + shaderUniforms.cameraShapePosition = Cartesian3.fromElements( + Cartesian2.magnitude(cameraPositionLocal), + Math.atan2(cameraPositionLocal.y, cameraPositionLocal.x), + cameraPositionLocal.z, + shaderUniforms.cameraShapePosition, + ); + // 2. Find radial, tangent, and up components at camera position + const cameraRadialDirection = Cartesian2.normalize( + Cartesian2.fromCartesian3(cameraPositionLocal, scratchCameraRadialPosition), + scratchCameraRadialPosition, + ); + // As row vectors, the radial, tangent, and up vectors constitute a rotation matrix from local to RTU. + const rotateLocalToRtu = Matrix3.fromRowMajorArray( + [ + cameraRadialDirection.x, + cameraRadialDirection.y, + 0.0, + -cameraRadialDirection.y, + cameraRadialDirection.x, + 0.0, + 0.0, + 0.0, + 1.0, + ], + scratchRotateRtuToLocal, + ); + // 3. Get rotation from eye to local coordinates + const transformPositionViewToWorld = + frameState.context.uniformState.inverseView; + const transformPositionViewToLocal = Matrix4.multiplyTransformation( + transformPositionWorldToLocal, + transformPositionViewToWorld, + scratchTransformPositionViewToLocal, + ); + const transformDirectionViewToLocal = Matrix4.getMatrix3( + transformPositionViewToLocal, + scratchRtuRotation, + ); + // 4. Multiply to get rotation from eye to RTU coordinates + shaderUniforms.cylinderEcToRadialTangentUp = Matrix3.multiply( + rotateLocalToRtu, + transformDirectionViewToLocal, + shaderUniforms.cylinderEcToRadialTangentUp, + ); +}; + +/** + * Convert a UV coordinate to the shape's UV space. + * @private + * @param {Cartesian3} positionLocal The local coordinate to convert. + * @param {Cartesian3} result The Cartesian3 to store the result in. + * @returns {Cartesian3} The converted UV coordinate. + */ +VoxelCylinderShape.prototype.convertLocalToShapeUvSpace = function ( + positionLocal, + result, +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("positionLocal", positionLocal); + Check.typeOf.object("result", result); + //>>includeEnd('debug'); + + let radius = Math.hypot(positionLocal.x, positionLocal.y); + let angle = Math.atan2(positionLocal.y, positionLocal.x); + let height = positionLocal.z; + + const { + cylinderLocalToShapeUvRadius, + cylinderLocalToShapeUvAngle, + cylinderShapeUvAngleRangeOrigin, + cylinderLocalToShapeUvHeight, + } = this._shaderUniforms; + + radius = + radius * cylinderLocalToShapeUvRadius.x + cylinderLocalToShapeUvRadius.y; + + // Convert angle to a "UV" in [0,1] with 0 defined at the center of the unoccupied space. + angle = (angle + Math.PI) / (2.0 * Math.PI); + angle -= cylinderShapeUvAngleRangeOrigin; + angle = angle - Math.floor(angle); + // Scale and shift so [0,1] covers the occupied space. + angle = angle * cylinderLocalToShapeUvAngle.x + cylinderLocalToShapeUvAngle.y; + + height = + height * cylinderLocalToShapeUvHeight.x + cylinderLocalToShapeUvHeight.y; + + return Cartesian3.fromElements(radius, angle, height, result); +}; + const scratchMinBounds = new Cartesian3(); const scratchMaxBounds = new Cartesian3(); /** * Computes an oriented bounding box for a specified tile. - * The update function must be called before calling this function. * @private * @param {number} tileLevel The tile's level. * @param {number} tileX The tile's x coordinate. @@ -484,7 +595,7 @@ VoxelCylinderShape.prototype.computeOrientedBoundingBoxForTile = function ( return getCylinderChunkObb( tileMinBounds, tileMaxBounds, - this.shapeTransform, + this._shapeTransform, result, ); }; @@ -493,7 +604,6 @@ const sampleSizeScratch = new Cartesian3(); /** * Computes an oriented bounding box for a specified sample within a specified tile. - * The update function must be called before calling this function. * @private * @param {SpatialNode} spatialNode The spatial node containing the sample * @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding @@ -557,7 +667,7 @@ VoxelCylinderShape.prototype.computeOrientedBoundingBoxForSample = function ( return getCylinderChunkObb( sampleMinBounds, sampleMaxBounds, - this.shapeTransform, + this._shapeTransform, result, ); }; @@ -639,6 +749,7 @@ function computeLooseOrientedBoundingBox(matrix, result) { return OrientedBoundingBox.fromPoints(corners, result); } +const scratchBoxScale = new Cartesian3(); /** * Computes an {@link OrientedBoundingBox} for a subregion of the shape. * @@ -720,7 +831,7 @@ function getCylinderChunkObb(chunkMinBounds, chunkMaxBounds, matrix, result) { extentX, extentY, extentZ, - scratchScale, + scratchBoxScale, ); const scaleMatrix = Matrix4.fromScale(scale, scratchScaleMatrix); diff --git a/packages/engine/Source/Scene/VoxelEllipsoidShape.js b/packages/engine/Source/Scene/VoxelEllipsoidShape.js index afb26af661db..8babd6f3adef 100644 --- a/packages/engine/Source/Scene/VoxelEllipsoidShape.js +++ b/packages/engine/Source/Scene/VoxelEllipsoidShape.js @@ -1,6 +1,8 @@ +import defined from "../Core/defined.js"; import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; +import Cartographic from "../Core/Cartographic.js"; import Check from "../Core/Check.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import CesiumMath from "../Core/Math.js"; @@ -8,6 +10,7 @@ import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; import Rectangle from "../Core/Rectangle.js"; +import Transforms from "../Core/Transforms.js"; /** * An ellipsoid {@link VoxelShape}. @@ -23,41 +26,10 @@ import Rectangle from "../Core/Rectangle.js"; * @private */ function VoxelEllipsoidShape() { - /** - * An oriented bounding box containing the bounded shape. - * The update function must be called before accessing this value. - * @type {OrientedBoundingBox} - * @readonly - * @private - */ - this.orientedBoundingBox = new OrientedBoundingBox(); - - /** - * A bounding sphere containing the bounded shape. - * The update function must be called before accessing this value. - * @type {BoundingSphere} - * @readonly - * @private - */ - this.boundingSphere = new BoundingSphere(); - - /** - * A transformation matrix containing the bounded shape. - * The update function must be called before accessing this value. - * @type {Matrix4} - * @readonly - * @private - */ - this.boundTransform = new Matrix4(); - - /** - * A transformation matrix containing the shape, ignoring the bounds. - * The update function must be called before accessing this value. - * @type {Matrix4} - * @readonly - * @private - */ - this.shapeTransform = new Matrix4(); + this._orientedBoundingBox = new OrientedBoundingBox(); + this._boundingSphere = new BoundingSphere(); + this._boundTransform = new Matrix4(); + this._shapeTransform = new Matrix4(); /** * @type {Rectangle} @@ -95,31 +67,25 @@ function VoxelEllipsoidShape() { */ this._rotation = new Matrix3(); - /** - * @type {Object} - * @readonly - * @private - */ - this.shaderUniforms = { - ellipsoidRadiiUv: new Cartesian3(), + this._shaderUniforms = { + cameraPositionCartographic: new Cartesian3(), + ellipsoidEcToEastNorthUp: new Matrix3(), + ellipsoidRadii: new Cartesian3(), eccentricitySquared: 0.0, evoluteScale: new Cartesian2(), - ellipsoidInverseRadiiSquaredUv: new Cartesian3(), + ellipsoidCurvatureAtLatitude: new Cartesian2(), + ellipsoidInverseRadiiSquared: new Cartesian3(), ellipsoidRenderLongitudeMinMax: new Cartesian2(), + ellipsoidShapeUvLongitudeRangeOrigin: 0.0, ellipsoidShapeUvLongitudeMinMaxMid: new Cartesian3(), - ellipsoidUvToShapeUvLongitude: new Cartesian2(), - ellipsoidUvToShapeUvLatitude: new Cartesian2(), + ellipsoidLocalToShapeUvLongitude: new Cartesian2(), + ellipsoidLocalToShapeUvLatitude: new Cartesian2(), ellipsoidRenderLatitudeSinMinMax: new Cartesian2(), - ellipsoidInverseHeightDifferenceUv: 0.0, + ellipsoidInverseHeightDifference: 0.0, clipMinMaxHeight: new Cartesian2(), }; - /** - * @type {Object} - * @readonly - * @private - */ - this.shaderDefines = { + this._shaderDefines = { ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE: undefined, ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_RANGE_EQUAL_ZERO: undefined, ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_RANGE_UNDER_HALF: undefined, @@ -142,14 +108,103 @@ function VoxelEllipsoidShape() { ELLIPSOID_INTERSECTION_INDEX_HEIGHT_MIN: undefined, }; + this._shaderMaximumIntersectionsLength = 0; // not known until update +} + +Object.defineProperties(VoxelEllipsoidShape.prototype, { + /** + * An oriented bounding box containing the bounded shape. + * + * @memberof VoxelEllipsoidShape.prototype + * @type {OrientedBoundingBox} + * @readonly + * @private + */ + orientedBoundingBox: { + get: function () { + return this._orientedBoundingBox; + }, + }, + + /** + * A bounding sphere containing the bounded shape. + * + * @memberof VoxelEllipsoidShape.prototype + * @type {BoundingSphere} + * @readonly + * @private + */ + boundingSphere: { + get: function () { + return this._boundingSphere; + }, + }, + + /** + * A transformation matrix containing the bounded shape. + * + * @memberof VoxelEllipsoidShape.prototype + * @type {Matrix4} + * @readonly + * @private + */ + boundTransform: { + get: function () { + return this._boundTransform; + }, + }, + + /** + * A transformation matrix containing the shape, ignoring the bounds. + * + * @memberof VoxelEllipsoidShape.prototype + * @type {Matrix4} + * @readonly + * @private + */ + shapeTransform: { + get: function () { + return this._shapeTransform; + }, + }, + + /** + * @memberof VoxelEllipsoidShape.prototype + * @type {Object} + * @readonly + * @private + */ + shaderUniforms: { + get: function () { + return this._shaderUniforms; + }, + }, + + /** + * @memberof VoxelEllipsoidShape.prototype + * @type {Object} + * @readonly + * @private + */ + shaderDefines: { + get: function () { + return this._shaderDefines; + }, + }, + /** * The maximum number of intersections against the shape for any ray direction. + * @memberof VoxelEllipsoidShape.prototype * @type {number} * @readonly * @private */ - this.shaderMaximumIntersectionsLength = 0; // not known until update -} + shaderMaximumIntersectionsLength: { + get: function () { + return this._shaderMaximumIntersectionsLength; + }, + }, +}); const scratchActualMinBounds = new Cartesian3(); const scratchShapeMinBounds = new Cartesian3(); @@ -159,7 +214,6 @@ const scratchClipMaxBounds = new Cartesian3(); const scratchRenderMinBounds = new Cartesian3(); const scratchRenderMaxBounds = new Cartesian3(); const scratchScale = new Cartesian3(); -const scratchRotationScale = new Matrix3(); const scratchShapeOuterExtent = new Cartesian3(); const scratchRenderOuterExtent = new Cartesian3(); const scratchRenderRectangle = new Rectangle(); @@ -250,7 +304,6 @@ VoxelEllipsoidShape.prototype.update = function ( ), scratchShapeOuterExtent, ); - const shapeMaxExtent = Cartesian3.maximumComponent(shapeOuterExtent); const renderOuterExtent = Cartesian3.add( radii, @@ -300,31 +353,31 @@ VoxelEllipsoidShape.prototype.update = function ( scratchRenderRectangle, ); - this.orientedBoundingBox = getEllipsoidChunkObb( + this._orientedBoundingBox = getEllipsoidChunkObb( renderRectangle, renderMinBounds.z, renderMaxBounds.z, this._ellipsoid, this._translation, this._rotation, - this.orientedBoundingBox, + this._orientedBoundingBox, ); - this.shapeTransform = Matrix4.fromRotationTranslation( - Matrix3.setScale(this._rotation, shapeOuterExtent, scratchRotationScale), + this._shapeTransform = Matrix4.fromRotationTranslation( + this._rotation, this._translation, - this.shapeTransform, + this._shapeTransform, ); - this.boundTransform = Matrix4.fromRotationTranslation( - this.orientedBoundingBox.halfAxes, - this.orientedBoundingBox.center, - this.boundTransform, + this._boundTransform = Matrix4.fromRotationTranslation( + this._orientedBoundingBox.halfAxes, + this._orientedBoundingBox.center, + this._boundTransform, ); - this.boundingSphere = BoundingSphere.fromOrientedBoundingBox( - this.orientedBoundingBox, - this.boundingSphere, + this._boundingSphere = BoundingSphere.fromOrientedBoundingBox( + this._orientedBoundingBox, + this._boundingSphere, ); // Longitude @@ -415,7 +468,8 @@ VoxelEllipsoidShape.prototype.update = function ( shapeIsLatitudeMinOverHalf; const shapeHasLatitude = shapeHasLatitudeMax || shapeHasLatitudeMin; - const { shaderUniforms, shaderDefines } = this; + const shaderUniforms = this._shaderUniforms; + const shaderDefines = this._shaderDefines; // To keep things simple, clear the defines every time for (const key in shaderDefines) { @@ -424,30 +478,28 @@ VoxelEllipsoidShape.prototype.update = function ( } } - // The ellipsoid radii scaled to [0,1]. The max ellipsoid radius will be 1.0 and others will be less. - shaderUniforms.ellipsoidRadiiUv = Cartesian3.divideByScalar( + shaderUniforms.ellipsoidRadii = Cartesian3.clone( shapeOuterExtent, - shapeMaxExtent, - shaderUniforms.ellipsoidRadiiUv, + shaderUniforms.ellipsoidRadii, ); - const { x: radiiUvX, z: radiiUvZ } = shaderUniforms.ellipsoidRadiiUv; - const axisRatio = radiiUvZ / radiiUvX; + const { x: radiiX, z: radiiZ } = shaderUniforms.ellipsoidRadii; + const axisRatio = radiiZ / radiiX; shaderUniforms.eccentricitySquared = 1.0 - axisRatio * axisRatio; shaderUniforms.evoluteScale = Cartesian2.fromElements( - (radiiUvX * radiiUvX - radiiUvZ * radiiUvZ) / radiiUvX, - (radiiUvZ * radiiUvZ - radiiUvX * radiiUvX) / radiiUvZ, + (radiiX * radiiX - radiiZ * radiiZ) / radiiX, + (radiiZ * radiiZ - radiiX * radiiX) / radiiZ, shaderUniforms.evoluteScale, ); // Used to compute geodetic surface normal. - shaderUniforms.ellipsoidInverseRadiiSquaredUv = Cartesian3.divideComponents( + shaderUniforms.ellipsoidInverseRadiiSquared = Cartesian3.divideComponents( Cartesian3.ONE, Cartesian3.multiplyComponents( - shaderUniforms.ellipsoidRadiiUv, - shaderUniforms.ellipsoidRadiiUv, - shaderUniforms.ellipsoidInverseRadiiSquaredUv, + shaderUniforms.ellipsoidRadii, + shaderUniforms.ellipsoidRadii, + shaderUniforms.ellipsoidInverseRadiiSquared, ), - shaderUniforms.ellipsoidInverseRadiiSquaredUv, + shaderUniforms.ellipsoidInverseRadiiSquared, ); // Keep track of how many intersections there are going to be. @@ -460,16 +512,16 @@ VoxelEllipsoidShape.prototype.update = function ( intersectionCount += 1; shaderUniforms.clipMinMaxHeight = Cartesian2.fromElements( - (renderMinBounds.z - shapeMaxBounds.z) / shapeMaxExtent, - (renderMaxBounds.z - shapeMaxBounds.z) / shapeMaxExtent, + renderMinBounds.z - shapeMaxBounds.z, + renderMaxBounds.z - shapeMaxBounds.z, shaderUniforms.clipMinMaxHeight, ); // The percent of space that is between the inner and outer ellipsoid. - const thickness = (shapeMaxBounds.z - shapeMinBounds.z) / shapeMaxExtent; - shaderUniforms.ellipsoidInverseHeightDifferenceUv = 1.0 / thickness; + const thickness = shapeMaxBounds.z - shapeMinBounds.z; + shaderUniforms.ellipsoidInverseHeightDifference = 1.0 / thickness; if (shapeMinBounds.z === shapeMaxBounds.z) { - shaderUniforms.ellipsoidInverseHeightDifferenceUv = 0.0; + shaderUniforms.ellipsoidInverseHeightDifference = 0.0; } // Intersects a wedge for the min and max longitude. @@ -501,36 +553,39 @@ VoxelEllipsoidShape.prototype.update = function ( if (shapeHasLongitude) { shaderDefines["ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE"] = true; - const shapeIsLongitudeReversed = shapeMaxBounds.x < shapeMinBounds.x; + const uvShapeMinLongitude = + (shapeMinBounds.x - DefaultMinBounds.x) / defaultLongitudeRange; + const uvShapeMaxLongitude = + (shapeMaxBounds.x - DefaultMinBounds.x) / defaultLongitudeRange; + const uvLongitudeRangeZero = + 1.0 - shapeLongitudeRange / defaultLongitudeRange; + // Translate the origin of UV angles (in [0,1]) to the center of the unoccupied space + const uvLongitudeRangeOrigin = + (uvShapeMaxLongitude + 0.5 * uvLongitudeRangeZero) % 1.0; + shaderUniforms.ellipsoidShapeUvLongitudeRangeOrigin = + uvLongitudeRangeOrigin; + const shapeIsLongitudeReversed = shapeMaxBounds.x < shapeMinBounds.x; if (shapeIsLongitudeReversed) { shaderDefines["ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED"] = true; } - // delerp(longitudeUv, minLongitudeUv, maxLongitudeUv) - // (longitudeUv - minLongitudeUv) / (maxLongitudeUv - minLongitudeUv) - // longitudeUv / (maxLongitudeUv - minLongitudeUv) - minLongitudeUv / (maxLongitudeUv - minLongitudeUv) - // scale = 1.0 / (maxLongitudeUv - minLongitudeUv) - // scale = 1.0 / (((maxLongitude - pi) / (2.0 * pi)) - ((minLongitude - pi) / (2.0 * pi))) - // scale = 2.0 * pi / (maxLongitude - minLongitude) - // offset = -minLongitudeUv / (maxLongitudeUv - minLongitudeUv) - // offset = -((minLongitude - pi) / (2.0 * pi)) / (((maxLongitude - pi) / (2.0 * pi)) - ((minLongitude - pi) / (2.0 * pi))) - // offset = -(minLongitude - pi) / (maxLongitude - minLongitude) if (shapeLongitudeRange <= epsilonLongitude) { - shaderUniforms.ellipsoidUvToShapeUvLongitude = Cartesian2.fromElements( + shaderUniforms.ellipsoidLocalToShapeUvLongitude = Cartesian2.fromElements( 0.0, 1.0, - shaderUniforms.ellipsoidUvToShapeUvLongitude, + shaderUniforms.ellipsoidLocalToShapeUvLongitude, ); } else { const scale = defaultLongitudeRange / shapeLongitudeRange; + const shiftedMinLongitude = uvShapeMinLongitude - uvLongitudeRangeOrigin; const offset = - -(shapeMinBounds.x - DefaultMinBounds.x) / shapeLongitudeRange; - shaderUniforms.ellipsoidUvToShapeUvLongitude = Cartesian2.fromElements( + -scale * (shiftedMinLongitude - Math.floor(shiftedMinLongitude)); + shaderUniforms.ellipsoidLocalToShapeUvLongitude = Cartesian2.fromElements( scale, offset, - shaderUniforms.ellipsoidUvToShapeUvLongitude, + shaderUniforms.ellipsoidLocalToShapeUvLongitude, ); } } @@ -630,45 +685,92 @@ VoxelEllipsoidShape.prototype.update = function ( if (shapeHasLatitude) { shaderDefines["ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE"] = true; - // delerp(latitudeUv, minLatitudeUv, maxLatitudeUv) - // (latitudeUv - minLatitudeUv) / (maxLatitudeUv - minLatitudeUv) - // latitudeUv / (maxLatitudeUv - minLatitudeUv) - minLatitudeUv / (maxLatitudeUv - minLatitudeUv) - // scale = 1.0 / (maxLatitudeUv - minLatitudeUv) - // scale = 1.0 / (((maxLatitude - pi) / (2.0 * pi)) - ((minLatitude - pi) / (2.0 * pi))) - // scale = 2.0 * pi / (maxLatitude - minLatitude) - // offset = -minLatitudeUv / (maxLatitudeUv - minLatitudeUv) - // offset = -((minLatitude - -pi) / (2.0 * pi)) / (((maxLatitude - pi) / (2.0 * pi)) - ((minLatitude - pi) / (2.0 * pi))) - // offset = -(minLatitude - -pi) / (maxLatitude - minLatitude) - // offset = (-pi - minLatitude) / (maxLatitude - minLatitude) if (shapeLatitudeRange < epsilonLatitude) { - shaderUniforms.ellipsoidUvToShapeUvLatitude = Cartesian2.fromElements( + shaderUniforms.ellipsoidLocalToShapeUvLatitude = Cartesian2.fromElements( 0.0, 1.0, - shaderUniforms.ellipsoidUvToShapeUvLatitude, + shaderUniforms.ellipsoidLocalToShapeUvLatitude, ); } else { const defaultLatitudeRange = DefaultMaxBounds.y - DefaultMinBounds.y; const scale = defaultLatitudeRange / shapeLatitudeRange; const offset = (DefaultMinBounds.y - shapeMinBounds.y) / shapeLatitudeRange; - shaderUniforms.ellipsoidUvToShapeUvLatitude = Cartesian2.fromElements( + shaderUniforms.ellipsoidLocalToShapeUvLatitude = Cartesian2.fromElements( scale, offset, - shaderUniforms.ellipsoidUvToShapeUvLatitude, + shaderUniforms.ellipsoidLocalToShapeUvLatitude, ); } } - this.shaderMaximumIntersectionsLength = intersectionCount; + this._shaderMaximumIntersectionsLength = intersectionCount; return true; }; +const scratchCameraPositionCartographic = new Cartographic(); +const surfacePositionScratch = new Cartesian3(); +const enuTransformScratch = new Matrix4(); +const enuRotationScratch = new Matrix3(); +/** + * Update any view-dependent transforms. + * @private + * @param {FrameState} frameState The frame state. + */ +VoxelEllipsoidShape.prototype.updateViewTransforms = function (frameState) { + const shaderUniforms = this._shaderUniforms; + const ellipsoid = this._ellipsoid; + // TODO: incorporate modelMatrix or shapeTransform here? + const cameraWC = frameState.camera.positionWC; + const cameraPositionCartographic = ellipsoid.cartesianToCartographic( + cameraWC, + scratchCameraPositionCartographic, + ); + Cartesian3.fromElements( + cameraPositionCartographic.longitude, + cameraPositionCartographic.latitude, + cameraPositionCartographic.height, + shaderUniforms.cameraPositionCartographic, + ); + + // TODO: incorporate modelMatrix here? + const surfacePosition = Cartesian3.fromRadians( + cameraPositionCartographic.longitude, + cameraPositionCartographic.latitude, + 0.0, + ellipsoid, + surfacePositionScratch, + ); + + shaderUniforms.ellipsoidCurvatureAtLatitude = ellipsoid.getLocalCurvature( + surfacePosition, + shaderUniforms.ellipsoidCurvatureAtLatitude, + ); + + const enuToWorld = Transforms.eastNorthUpToFixedFrame( + surfacePosition, + ellipsoid, + enuTransformScratch, + ); + const rotateEnuToWorld = Matrix4.getRotation(enuToWorld, enuRotationScratch); + const rotateWorldToView = frameState.context.uniformState.viewRotation; + const rotateEnuToView = Matrix3.multiply( + rotateWorldToView, + rotateEnuToWorld, + enuRotationScratch, + ); + // Inverse is the transpose since it's a pure rotation. + shaderUniforms.ellipsoidEcToEastNorthUp = Matrix3.transpose( + rotateEnuToView, + shaderUniforms.ellipsoidEcToEastNorthUp, + ); +}; + const scratchRectangle = new Rectangle(); /** * Computes an oriented bounding box for a specified tile. - * The update function must be called before calling this function. * @private * @param {number} tileLevel The tile's level. * @param {number} tileX The tile's x coordinate. @@ -732,13 +834,194 @@ VoxelEllipsoidShape.prototype.computeOrientedBoundingBoxForTile = function ( ); }; +const scratchQuadrantPosition = new Cartesian2(); +const scratchInverseRadii = new Cartesian2(); +const scratchEllipseTrigs = new Cartesian2(); +const scratchEllipseGuess = new Cartesian2(); +const scratchEvolute = new Cartesian2(); +const scratchQ = new Cartesian2(); + +/** + * Find the nearest point on an ellipse and its radius. + * @param {Cartesian2} position + * @param {Cartesian2} radii + * @param {Cartesian2} evoluteScale + * @param {Cartesian3} result The Cartesian3 to store the result in. .x and .y components contain the nearest point on the ellipse, .z contains the local radius of curvature. + * @returns {Cartesian3} The nearest point on the ellipse and its radius. + * @private + */ +function nearestPointAndRadiusOnEllipse(position, radii, evoluteScale, result) { + // Map to the first quadrant + const p = Cartesian2.abs(position, scratchQuadrantPosition); + const inverseRadii = Cartesian2.fromElements( + 1.0 / radii.x, + 1.0 / radii.y, + scratchInverseRadii, + ); + // We describe the ellipse parametrically: v = radii * vec2(cos(t), sin(t)) + // but store the cos and sin of t in a vec2 for efficiency. + // Initial guess: t = pi/4 + let tTrigs = Cartesian2.fromElements( + Math.SQRT1_2, + Math.SQRT1_2, + scratchEllipseTrigs, + ); + // TODO: too much duplication. Move v and evolute declarations inside loop? + // Initial guess of point on ellipsoid + let v = Cartesian2.multiplyComponents(radii, tTrigs, scratchEllipseGuess); + // Center of curvature of the ellipse at v + let evolute = Cartesian2.fromElements( + evoluteScale.x * tTrigs.x * tTrigs.x * tTrigs.x, + evoluteScale.y * tTrigs.y * tTrigs.y * tTrigs.y, + scratchEvolute, + ); + for (let i = 0; i < 3; ++i) { + // Find the (approximate) intersection of p - evolute with the ellipsoid. + const distance = Cartesian2.magnitude( + Cartesian2.subtract(v, evolute, scratchQ), + ); + const direction = Cartesian2.normalize( + Cartesian2.subtract(p, evolute, scratchQ), + scratchQ, + ); + const q = Cartesian2.multiplyByScalar(direction, distance, scratchQ); + // Update the estimate of t + tTrigs = Cartesian2.multiplyComponents( + Cartesian2.add(q, evolute, scratchEllipseTrigs), + inverseRadii, + scratchEllipseTrigs, + ); + tTrigs = Cartesian2.normalize( + Cartesian2.clamp( + tTrigs, + Cartesian2.ZERO, + Cartesian2.ONE, + scratchEllipseTrigs, + ), + scratchEllipseTrigs, + ); + v = Cartesian2.multiplyComponents(radii, tTrigs, scratchEllipseGuess); + evolute = Cartesian2.fromElements( + evoluteScale.x * tTrigs.x * tTrigs.x * tTrigs.x, + evoluteScale.y * tTrigs.y * tTrigs.y * tTrigs.y, + scratchEvolute, + ); + } + + // Map back to the original quadrant + return Cartesian3.fromElements( + Math.sign(position.x) * v.x, + Math.sign(position.y) * v.y, + Cartesian2.magnitude(Cartesian2.subtract(v, evolute, scratchQ)), + result, + ); +} + +const scratchEllipseRadii = new Cartesian2(); +const scratchEllipsePosition = new Cartesian2(); +const scratchSurfacePointAndRadius = new Cartesian3(); +const scratchNormal2d = new Cartesian2(); +/** + * Convert a UV coordinate to the shape's UV space. + * @private + * @param {Cartesian3} positionLocal The local position to convert. + * @param {Cartesian3} result The Cartesian3 to store the result in. + * @returns {Cartesian3} The converted UV coordinate. + */ +VoxelEllipsoidShape.prototype.convertLocalToShapeUvSpace = function ( + positionLocal, + result, +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("positionLocal", positionLocal); + Check.typeOf.object("result", result); + //>>includeEnd('debug'); + + let longitude = Math.atan2(positionLocal.y, positionLocal.x); + + const { + ellipsoidRadii, + evoluteScale, + ellipsoidInverseRadiiSquared, + ellipsoidInverseHeightDifference, + ellipsoidShapeUvLongitudeRangeOrigin, + ellipsoidLocalToShapeUvLongitude, + ellipsoidLocalToShapeUvLatitude, + } = this._shaderUniforms; + + const distanceFromZAxis = Math.hypot(positionLocal.x, positionLocal.y); + const posEllipse = Cartesian2.fromElements( + distanceFromZAxis, + positionLocal.z, + scratchEllipsePosition, + ); + const surfacePointAndRadius = nearestPointAndRadiusOnEllipse( + posEllipse, + Cartesian2.fromElements( + ellipsoidRadii.x, + ellipsoidRadii.z, + scratchEllipseRadii, + ), + evoluteScale, + scratchSurfacePointAndRadius, + ); + + const normal2d = Cartesian2.normalize( + Cartesian2.fromElements( + surfacePointAndRadius.x * ellipsoidInverseRadiiSquared.x, + surfacePointAndRadius.y * ellipsoidInverseRadiiSquared.z, + scratchNormal2d, + ), + scratchNormal2d, + ); + let latitude = Math.atan2(normal2d.y, normal2d.x); + + const heightSign = + Cartesian2.magnitude(posEllipse) < + Cartesian2.magnitude(surfacePointAndRadius) + ? -1.0 + : 1.0; + const heightVector = Cartesian2.subtract( + posEllipse, + surfacePointAndRadius, + scratchEllipsePosition, + ); + let height = heightSign * Cartesian2.magnitude(heightVector); + + const { + ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE, + ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE, + } = this._shaderDefines; + + longitude = (longitude + Math.PI) / (2.0 * Math.PI); + if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)) { + longitude -= ellipsoidShapeUvLongitudeRangeOrigin; + longitude = longitude - Math.floor(longitude); + // Scale and shift so [0, 1] covers the occupied space. + longitude = + longitude * ellipsoidLocalToShapeUvLongitude.x + + ellipsoidLocalToShapeUvLongitude.y; + } + + latitude = (latitude + Math.PI / 2.0) / Math.PI; + if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)) { + // Scale and shift so [0, 1] covers the occupied space. + latitude = + latitude * ellipsoidLocalToShapeUvLatitude.x + + ellipsoidLocalToShapeUvLatitude.y; + } + + height = 1.0 + height * ellipsoidInverseHeightDifference; + + return Cartesian3.fromElements(longitude, latitude, height, result); +}; + const sampleSizeScratch = new Cartesian3(); const scratchTileMinBounds = new Cartesian3(); const scratchTileMaxBounds = new Cartesian3(); /** * Computes an oriented bounding box for a specified sample within a specified tile. - * The update function must be called before calling this function. * @private * @param {SpatialNode} spatialNode The spatial node containing the sample * @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding diff --git a/packages/engine/Source/Scene/VoxelPrimitive.js b/packages/engine/Source/Scene/VoxelPrimitive.js index c1b7e08b2aa7..1643229602a0 100644 --- a/packages/engine/Source/Scene/VoxelPrimitive.js +++ b/packages/engine/Source/Scene/VoxelPrimitive.js @@ -130,6 +130,14 @@ function VoxelPrimitive(options) { */ this._paddingAfter = new Cartesian3(); + /** + * This member is not known until the provider is ready. + * + * @type {number} + * @private + */ + this._availableLevels = 1; + /** * This member is not known until the provider is ready. * @@ -332,24 +340,24 @@ function VoxelPrimitive(options) { this._clock = options.clock; // Transforms and other values that are computed when the shape changes - /** * @type {Matrix4} * @private */ - this._transformPositionWorldToUv = new Matrix4(); + this._transformPositionLocalToWorld = new Matrix4(); /** - * @type {Matrix3} + * @type {Matrix4} * @private */ - this._transformDirectionWorldToUv = new Matrix3(); + this._transformPositionWorldToLocal = new Matrix4(); /** + * Transforms a plane in Hessian normal form from local space to view space. * @type {Matrix4} * @private */ - this._transformPositionUvToWorld = new Matrix4(); + this._transformPlaneLocalToView = new Matrix4(); /** * @type {Matrix3} @@ -440,14 +448,16 @@ function VoxelPrimitive(options) { inputDimensions: new Cartesian3(), paddingBefore: new Cartesian3(), paddingAfter: new Cartesian3(), - transformPositionViewToUv: new Matrix4(), - transformPositionUvToView: new Matrix4(), + transformPositionViewToLocal: new Matrix4(), transformDirectionViewToLocal: new Matrix3(), - cameraPositionUv: new Cartesian3(), - cameraDirectionUv: new Cartesian3(), + cameraPositionLocal: new Cartesian3(), + cameraDirectionLocal: new Cartesian3(), + cameraTileCoordinates: new Cartesian4(), + cameraTileUv: new Cartesian3(), ndcSpaceAxisAlignedBoundingBox: new Cartesian4(), clippingPlanesTexture: undefined, clippingPlanesMatrix: new Matrix4(), + renderBoundPlanesTexture: undefined, stepSize: 0, pickColor: new Color(), }; @@ -631,11 +641,7 @@ function initialize(primitive, provider) { // Create the shape object, and update it so it is valid for VoxelTraversal const ShapeConstructor = VoxelShapeType.getShapeConstructor(shapeType); primitive._shape = new ShapeConstructor(); - primitive._shapeVisible = updateShapeAndTransforms( - primitive, - primitive._shape, - provider, - ); + primitive._shapeVisible = updateShapeAndTransforms(primitive); } Object.defineProperties(VoxelPrimitive.prototype, { @@ -1137,20 +1143,10 @@ Object.defineProperties(VoxelPrimitive.prototype, { const scratchIntersect = new Cartesian4(); const scratchNdcAabb = new Cartesian4(); -const scratchTransformPositionWorldToLocal = new Matrix4(); const scratchTransformPositionLocalToWorld = new Matrix4(); const scratchTransformPositionLocalToProjection = new Matrix4(); - -const transformPositionLocalToUv = Matrix4.fromRotationTranslation( - Matrix3.fromUniformScale(0.5, new Matrix3()), - new Cartesian3(0.5, 0.5, 0.5), - new Matrix4(), -); -const transformPositionUvToLocal = Matrix4.fromRotationTranslation( - Matrix3.fromUniformScale(2.0, new Matrix3()), - new Cartesian3(-1.0, -1.0, -1.0), - new Matrix4(), -); +const scratchCameraPositionShapeUv = new Cartesian3(); +const scratchCameraTileCoordinates = new Cartesian4(); /** * Updates the voxel primitive. @@ -1160,6 +1156,7 @@ const transformPositionUvToLocal = Matrix4.fromRotationTranslation( */ VoxelPrimitive.prototype.update = function (frameState) { const provider = this._provider; + const uniforms = this._uniforms; // Update the custom shader in case it has texture uniforms. this._customShader.update(frameState); @@ -1185,10 +1182,9 @@ VoxelPrimitive.prototype.update = function (frameState) { // frame because the member variables can be modified externally via the // getters. const shapeDirty = checkTransformAndBounds(this, provider); - const shape = this._shape; if (shapeDirty) { - this._shapeVisible = updateShapeAndTransforms(this, shape, provider); - if (checkShapeDefines(this, shape)) { + this._shapeVisible = updateShapeAndTransforms(this); + if (checkShapeDefines(this)) { this._shaderDirty = true; } } @@ -1196,6 +1192,8 @@ VoxelPrimitive.prototype.update = function (frameState) { return; } + this._shape.updateViewTransforms(frameState); + // Update the traversal and prepare for rendering. const keyframeLocation = getKeyframeLocation( provider.timeIntervalCollection, @@ -1243,7 +1241,6 @@ VoxelPrimitive.prototype.update = function (frameState) { } const leafNodeTexture = traversal.leafNodeTexture; - const uniforms = this._uniforms; if (defined(leafNodeTexture)) { uniforms.octreeLeafNodeTexture = traversal.leafNodeTexture; uniforms.octreeLeafNodeTexelSizeUv = Cartesian2.clone( @@ -1262,7 +1259,7 @@ VoxelPrimitive.prototype.update = function (frameState) { // Calculate the NDC-space AABB to "scissor" the fullscreen quad const transformPositionWorldToProjection = context.uniformState.viewProjection; - const orientedBoundingBox = shape.orientedBoundingBox; + const { orientedBoundingBox } = this._shape; const ndcAabb = orientedBoundingBoxToNdcAabb( orientedBoundingBox, transformPositionWorldToProjection, @@ -1286,17 +1283,17 @@ VoxelPrimitive.prototype.update = function (frameState) { uniforms.ndcSpaceAxisAlignedBoundingBox, ); const transformPositionViewToWorld = context.uniformState.inverseView; - uniforms.transformPositionViewToUv = Matrix4.multiplyTransformation( - this._transformPositionWorldToUv, + const transformPositionViewToLocal = Matrix4.multiplyTransformation( + this._transformPositionWorldToLocal, transformPositionViewToWorld, - uniforms.transformPositionViewToUv, + uniforms.transformPositionViewToLocal, ); - const transformPositionWorldToView = context.uniformState.view; - uniforms.transformPositionUvToView = Matrix4.multiplyTransformation( - transformPositionWorldToView, - this._transformPositionUvToWorld, - uniforms.transformPositionUvToView, + + this._transformPlaneLocalToView = Matrix4.transpose( + transformPositionViewToLocal, + this._transformPlaneLocalToView, ); + const transformDirectionViewToWorld = context.uniformState.inverseViewRotation; uniforms.transformDirectionViewToLocal = Matrix3.multiply( @@ -1304,32 +1301,85 @@ VoxelPrimitive.prototype.update = function (frameState) { transformDirectionViewToWorld, uniforms.transformDirectionViewToLocal, ); - uniforms.cameraPositionUv = Matrix4.multiplyByPoint( - this._transformPositionWorldToUv, + uniforms.cameraPositionLocal = Matrix4.multiplyByPoint( + this._transformPositionWorldToLocal, frameState.camera.positionWC, - uniforms.cameraPositionUv, + uniforms.cameraPositionLocal, ); - uniforms.cameraDirectionUv = Matrix3.multiplyByVector( - this._transformDirectionWorldToUv, + uniforms.cameraDirectionLocal = Matrix3.multiplyByVector( + this._transformDirectionWorldToLocal, frameState.camera.directionWC, - uniforms.cameraDirectionUv, + uniforms.cameraDirectionLocal, + ); + const cameraTileCoordinates = getTileCoordinates( + this, + uniforms.cameraPositionLocal, + scratchCameraTileCoordinates, + ); + uniforms.cameraTileCoordinates = Cartesian4.fromElements( + Math.floor(cameraTileCoordinates.x), + Math.floor(cameraTileCoordinates.y), + Math.floor(cameraTileCoordinates.z), + cameraTileCoordinates.w, + uniforms.cameraTileCoordinates, ); - uniforms.cameraDirectionUv = Cartesian3.normalize( - uniforms.cameraDirectionUv, - uniforms.cameraDirectionUv, + uniforms.cameraTileUv = Cartesian3.fromElements( + cameraTileCoordinates.x - Math.floor(cameraTileCoordinates.x), + cameraTileCoordinates.y - Math.floor(cameraTileCoordinates.y), + cameraTileCoordinates.z - Math.floor(cameraTileCoordinates.z), + uniforms.cameraTileUv, ); uniforms.stepSize = this._stepSizeMultiplier; + updateRenderBoundPlanes(this, frameState); + // Render the primitive const command = frameState.passes.pick ? this._drawCommandPick : frameState.passes.pickVoxel ? this._drawCommandPickVoxel : this._drawCommand; - command.boundingVolume = shape.boundingSphere; + command.boundingVolume = this._shape.boundingSphere; frameState.commandList.push(command); }; +function updateRenderBoundPlanes(primitive, frameState) { + const uniforms = primitive._uniforms; + const { renderBoundPlanes } = primitive._shape; + if (!defined(renderBoundPlanes)) { + return; + } + renderBoundPlanes.update(frameState, primitive._transformPlaneLocalToView); + uniforms.renderBoundPlanesTexture = renderBoundPlanes.texture; +} + +/** + * Converts a position in local space to tile coordinates. + * + * @param {VoxelPrimitive} primitive The primitive to get the tile coordinates for. + * @param {Cartesian3} positionLocal The position in local space to convert to tile coordinates. + * @param {Cartesian4} result The result object to store the tile coordinates. + * @returns {Cartesian4} The tile coordinates of the supplied position. + * @private + */ +function getTileCoordinates(primitive, positionLocal, result) { + const shapeUv = primitive._shape.convertLocalToShapeUvSpace( + positionLocal, + scratchCameraPositionShapeUv, + ); + + const availableLevels = primitive._availableLevels; + const numTiles = 2 ** (availableLevels - 1); + + return Cartesian4.fromElements( + shapeUv.x * numTiles, + shapeUv.y * numTiles, + shapeUv.z * numTiles, + availableLevels - 1, + result, + ); +} + const scratchExaggerationScale = new Cartesian3(); const scratchExaggerationCenter = new Cartesian3(); const scratchCartographicCenter = new Cartographic(); @@ -1337,7 +1387,6 @@ const scratchExaggerationTranslation = new Cartesian3(); /** * Update the exaggerated bounds of a primitive to account for vertical exaggeration - * Currently only applies to Ellipsoid shape type * @param {VoxelPrimitive} primitive * @param {FrameState} frameState * @private @@ -1513,6 +1562,7 @@ function initFromProvider(primitive, provider, context) { primitive._inputDimensions, uniforms.inputDimensions, ); + primitive._availableLevels = provider.availableLevels ?? 1; // Create the VoxelTraversal, and set related uniforms const keyframeCount = provider.keyframeCount ?? 1; @@ -1586,12 +1636,11 @@ function updateBound(primitive, newBoundKey, oldBoundKey) { /** * Update the shape and related transforms * @param {VoxelPrimitive} primitive - * @param {VoxelShape} shape - * @param {VoxelProvider} provider * @returns {boolean} True if the shape is visible * @private */ -function updateShapeAndTransforms(primitive, shape, provider) { +function updateShapeAndTransforms(primitive) { + const shape = primitive._shape; const visible = shape.update( primitive._compoundModelMatrix, primitive._exaggeratedMinBounds, @@ -1603,29 +1652,16 @@ function updateShapeAndTransforms(primitive, shape, provider) { return false; } - const transformPositionLocalToWorld = shape.shapeTransform; - const transformPositionWorldToLocal = Matrix4.inverse( - transformPositionLocalToWorld, - scratchTransformPositionWorldToLocal, - ); - - // Set member variables when the shape is dirty - primitive._transformPositionWorldToUv = Matrix4.multiplyTransformation( - transformPositionLocalToUv, - transformPositionWorldToLocal, - primitive._transformPositionWorldToUv, + primitive._transformPositionLocalToWorld = Matrix4.clone( + shape.shapeTransform, + primitive._transformPositionLocalToWorld, ); - primitive._transformDirectionWorldToUv = Matrix4.getMatrix3( - primitive._transformPositionWorldToUv, - primitive._transformDirectionWorldToUv, - ); - primitive._transformPositionUvToWorld = Matrix4.multiplyTransformation( - transformPositionLocalToWorld, - transformPositionUvToLocal, - primitive._transformPositionUvToWorld, + primitive._transformPositionWorldToLocal = Matrix4.inverse( + primitive._transformPositionLocalToWorld, + primitive._transformPositionWorldToLocal, ); primitive._transformDirectionWorldToLocal = Matrix4.getMatrix3( - transformPositionWorldToLocal, + primitive._transformPositionWorldToLocal, primitive._transformDirectionWorldToLocal, ); @@ -1679,17 +1715,16 @@ function setTraversalUniforms(traversal, uniforms) { /** * Track changes in shape-related shader defines * @param {VoxelPrimitive} primitive - * @param {VoxelShape} shape * @returns {boolean} True if any of the shape defines changed, requiring a shader rebuild * @private */ -function checkShapeDefines(primitive, shape) { - const shapeDefines = shape.shaderDefines; - const shapeDefinesChanged = Object.keys(shapeDefines).some( - (key) => shapeDefines[key] !== primitive._shapeDefinesOld[key], +function checkShapeDefines(primitive) { + const { shaderDefines } = primitive._shape; + const shapeDefinesChanged = Object.keys(shaderDefines).some( + (key) => shaderDefines[key] !== primitive._shapeDefinesOld[key], ); if (shapeDefinesChanged) { - primitive._shapeDefinesOld = clone(shapeDefines, true); + primitive._shapeDefinesOld = clone(shaderDefines, true); } return shapeDefinesChanged; } @@ -1761,12 +1796,12 @@ function updateClippingPlanes(primitive, frameState) { const uniforms = primitive._uniforms; uniforms.clippingPlanesTexture = clippingPlanes.texture; - // Compute the clipping plane's transformation to uv space and then take the inverse + // Compute the clipping plane's transformation to local space and then take the inverse // transpose to properly transform the hessian normal form of the plane. - // transpose(inverse(worldToUv * clippingPlaneLocalToWorld)) - // transpose(inverse(clippingPlaneLocalToWorld) * inverse(worldToUv)) - // transpose(inverse(clippingPlaneLocalToWorld) * uvToWorld) + // transpose(inverse(worldToLocal * clippingPlaneLocalToWorld)) + // transpose(inverse(clippingPlaneLocalToWorld) * inverse(worldToLocal)) + // transpose(inverse(clippingPlaneLocalToWorld) * localToWorld) uniforms.clippingPlanesMatrix = Matrix4.transpose( Matrix4.multiplyTransformation( @@ -1774,7 +1809,7 @@ function updateClippingPlanes(primitive, frameState) { clippingPlanes.modelMatrix, uniforms.clippingPlanesMatrix, ), - primitive._transformPositionUvToWorld, + primitive._transformPositionLocalToWorld, uniforms.clippingPlanesMatrix, ), uniforms.clippingPlanesMatrix, diff --git a/packages/engine/Source/Scene/VoxelRenderResources.js b/packages/engine/Source/Scene/VoxelRenderResources.js index f501f715d19b..ab7b6f46308c 100644 --- a/packages/engine/Source/Scene/VoxelRenderResources.js +++ b/packages/engine/Source/Scene/VoxelRenderResources.js @@ -8,15 +8,15 @@ import VoxelFS from "../Shaders/Voxels/VoxelFS.js"; import VoxelVS from "../Shaders/Voxels/VoxelVS.js"; import IntersectionUtils from "../Shaders/Voxels/IntersectionUtils.js"; import IntersectDepth from "../Shaders/Voxels/IntersectDepth.js"; -import IntersectClippingPlanes from "../Shaders/Voxels/IntersectClippingPlanes.js"; +import IntersectPlane from "../Shaders/Voxels/IntersectPlane.js"; import IntersectLongitude from "../Shaders/Voxels/IntersectLongitude.js"; import IntersectBox from "../Shaders/Voxels/IntersectBox.js"; import IntersectCylinder from "../Shaders/Voxels/IntersectCylinder.js"; import IntersectEllipsoid from "../Shaders/Voxels/IntersectEllipsoid.js"; import Intersection from "../Shaders/Voxels/Intersection.js"; -import convertUvToBox from "../Shaders/Voxels/convertUvToBox.js"; -import convertUvToCylinder from "../Shaders/Voxels/convertUvToCylinder.js"; -import convertUvToEllipsoid from "../Shaders/Voxels/convertUvToEllipsoid.js"; +import convertLocalToBoxUv from "../Shaders/Voxels/convertLocalToBoxUv.js"; +import convertLocalToCylinderUv from "../Shaders/Voxels/convertLocalToCylinderUv.js"; +import convertLocalToEllipsoidUv from "../Shaders/Voxels/convertLocalToEllipsoidUv.js"; import Octree from "../Shaders/Voxels/Octree.js"; import Megatexture from "../Shaders/Voxels/Megatexture.js"; import VoxelMetadataOrder from "./VoxelMetadataOrder.js"; @@ -84,6 +84,12 @@ function VoxelRenderResources(primitive) { this.clippingPlanes = clippingPlanes; this.clippingPlanesLength = clippingPlanesLength; + const renderBoundPlanes = primitive._shape.renderBoundPlanes; + const renderBoundPlanesLength = renderBoundPlanes?.length ?? 0; + + this.renderBoundPlanes = renderBoundPlanes; + this.renderBoundPlanesLength = renderBoundPlanesLength; + // Build shader shaderBuilder.addVertexLines([VoxelVS]); @@ -116,8 +122,10 @@ function VoxelRenderResources(primitive) { "#line 0", Octree, VoxelUtils, - IntersectionUtils, Megatexture, + IntersectionUtils, + IntersectPlane, + IntersectDepth, ]); if (clippingPlanesLength > 0) { @@ -138,9 +146,8 @@ function VoxelRenderResources(primitive) { ShaderDestination.FRAGMENT, ); } - shaderBuilder.addFragmentLines([IntersectClippingPlanes]); } - shaderBuilder.addFragmentLines([IntersectDepth]); + if (primitive._depthTest) { shaderBuilder.addDefine( "DEPTH_TEST", @@ -151,20 +158,20 @@ function VoxelRenderResources(primitive) { if (shapeType === "BOX") { shaderBuilder.addFragmentLines([ - convertUvToBox, + convertLocalToBoxUv, IntersectBox, Intersection, ]); } else if (shapeType === "CYLINDER") { shaderBuilder.addFragmentLines([ - convertUvToCylinder, + convertLocalToCylinderUv, IntersectLongitude, IntersectCylinder, Intersection, ]); } else if (shapeType === "ELLIPSOID") { shaderBuilder.addFragmentLines([ - convertUvToEllipsoid, + convertLocalToEllipsoidUv, IntersectLongitude, IntersectEllipsoid, Intersection, diff --git a/packages/engine/Source/Scene/VoxelShape.js b/packages/engine/Source/Scene/VoxelShape.js index 14f647dbc1c7..7947fa7d5720 100644 --- a/packages/engine/Source/Scene/VoxelShape.js +++ b/packages/engine/Source/Scene/VoxelShape.js @@ -21,7 +21,6 @@ function VoxelShape() { Object.defineProperties(VoxelShape.prototype, { /** * An oriented bounding box containing the bounded shape. - * The update function must be called before accessing this value. * * @memberof VoxelShape.prototype * @type {OrientedBoundingBox} @@ -34,7 +33,6 @@ Object.defineProperties(VoxelShape.prototype, { /** * A bounding sphere containing the bounded shape. - * The update function must be called before accessing this value. * * @memberof VoxelShape.prototype * @type {BoundingSphere} @@ -47,7 +45,6 @@ Object.defineProperties(VoxelShape.prototype, { /** * A transformation matrix containing the bounded shape. - * The update function must be called before accessing this value. * * @memberof VoxelShape.prototype * @type {Matrix4} @@ -60,7 +57,6 @@ Object.defineProperties(VoxelShape.prototype, { /** * A transformation matrix containing the shape, ignoring the bounds. - * The update function must be called before accessing this value. * * @memberof VoxelShape.prototype * @type {Matrix4} @@ -72,6 +68,7 @@ Object.defineProperties(VoxelShape.prototype, { }, /** + * @memberof VoxelShape.prototype * @type {Object} * @readonly * @private @@ -81,6 +78,7 @@ Object.defineProperties(VoxelShape.prototype, { }, /** + * @memberof VoxelShape.prototype * @type {Object} * @readonly * @private @@ -91,6 +89,7 @@ Object.defineProperties(VoxelShape.prototype, { /** * The maximum number of intersections against the shape for any ray direction. + * @memberof VoxelShape.prototype * @type {number} * @readonly * @private @@ -110,9 +109,26 @@ Object.defineProperties(VoxelShape.prototype, { */ VoxelShape.prototype.update = DeveloperError.throwInstantiationError; +/** + * Update any view-dependent transforms. + * @private + * @param {FrameState} frameState The frame state. + */ +VoxelShape.prototype.updateViewTransforms = + DeveloperError.throwInstantiationError; + +/** + * Converts a local coordinate to the shape's UV space. + * @private + * @param {Cartesian3} positionLocal The local coordinate to convert. + * @param {Cartesian3} result The Cartesian3 to store the result in. + * @returns {Cartesian3} The converted UV coordinate. + */ +VoxelShape.prototype.convertLocalToShapeUvSpace = + DeveloperError.throwInstantiationError; + /** * Computes an oriented bounding box for a specified tile. - * The update function must be called before calling this function. * @private * @param {number} tileLevel The tile's level. * @param {number} tileX The tile's x coordinate. @@ -126,7 +142,6 @@ VoxelShape.prototype.computeOrientedBoundingBoxForTile = /** * Computes an oriented bounding box for a specified sample within a specified tile. - * The update function must be called before calling this function. * @private * @param {SpatialNode} spatialNode The spatial node containing the sample * @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding diff --git a/packages/engine/Source/Scene/buildVoxelDrawCommands.js b/packages/engine/Source/Scene/buildVoxelDrawCommands.js index 19f67111ccca..dbcef201e93a 100644 --- a/packages/engine/Source/Scene/buildVoxelDrawCommands.js +++ b/packages/engine/Source/Scene/buildVoxelDrawCommands.js @@ -1,14 +1,18 @@ -import defined from "../Core/defined.js"; -import PrimitiveType from "../Core/PrimitiveType.js"; import BlendingState from "./BlendingState.js"; +import Cartesian2 from "../Core/Cartesian2.js"; +import ClippingPlaneCollection from "./ClippingPlaneCollection.js"; import CullFace from "./CullFace.js"; -import getClippingFunction from "./getClippingFunction.js"; +import defined from "../Core/defined.js"; import DrawCommand from "../Renderer/DrawCommand.js"; import Pass from "../Renderer/Pass.js"; +import PrimitiveType from "../Core/PrimitiveType.js"; +import processVoxelProperties from "./processVoxelProperties.js"; import RenderState from "../Renderer/RenderState.js"; import ShaderDestination from "../Renderer/ShaderDestination.js"; +import VoxelBoundsCollection from "./VoxelBoundsCollection.js"; import VoxelRenderResources from "./VoxelRenderResources.js"; -import processVoxelProperties from "./processVoxelProperties.js"; + +const textureResolutionScratch = new Cartesian2(); /** * @function @@ -23,27 +27,40 @@ function buildVoxelDrawCommands(primitive, context) { processVoxelProperties(renderResources, primitive); - const { shaderBuilder, clippingPlanes, clippingPlanesLength } = - renderResources; + const { + shaderBuilder, + clippingPlanes, + clippingPlanesLength, + renderBoundPlanes, + renderBoundPlanesLength, + } = renderResources; if (clippingPlanesLength > 0) { - // Extract the getClippingPlane function from the getClippingFunction string. - // This is a bit of a hack. const functionId = "getClippingPlane"; - const entireFunction = getClippingFunction(clippingPlanes, context); - const functionSignatureBegin = 0; - const functionSignatureEnd = entireFunction.indexOf(")") + 1; - const functionBodyBegin = - entireFunction.indexOf("{", functionSignatureEnd) + 1; - const functionBodyEnd = entireFunction.indexOf("}", functionBodyBegin); - const functionSignature = entireFunction.slice( - functionSignatureBegin, - functionSignatureEnd, + const functionSignature = `vec4 ${functionId}(highp sampler2D packedPlanes, int planeNumber)`; + const textureResolution = ClippingPlaneCollection.getTextureResolution( + clippingPlanes, + context, + textureResolutionScratch, + ); + const functionBody = getPlaneFunctionBody(textureResolution); + shaderBuilder.addFunction( + functionId, + functionSignature, + ShaderDestination.FRAGMENT, ); - const functionBody = entireFunction.slice( - functionBodyBegin, - functionBodyEnd, + shaderBuilder.addFunctionLines(functionId, [functionBody]); + } + + if (renderBoundPlanesLength > 0) { + const functionId = "getBoundPlane"; + const functionSignature = `vec4 ${functionId}(highp sampler2D packedPlanes, int planeNumber)`; + const textureResolution = VoxelBoundsCollection.getTextureResolution( + renderBoundPlanes, + context, + textureResolutionScratch, ); + const functionBody = getPlaneFunctionBody(textureResolution); shaderBuilder.addFunction( functionId, functionSignature, @@ -133,4 +150,28 @@ function buildVoxelDrawCommands(primitive, context) { primitive._drawCommandPickVoxel = drawCommandPickVoxel; } +function getPlaneFunctionBody(textureResolution) { + const width = textureResolution.x; + const height = textureResolution.y; + + const pixelWidth = 1.0 / width; + const pixelHeight = 1.0 / height; + + let pixelWidthString = `${pixelWidth}`; + if (pixelWidthString.indexOf(".") === -1) { + pixelWidthString += ".0"; + } + let pixelHeightString = `${pixelHeight}`; + if (pixelHeightString.indexOf(".") === -1) { + pixelHeightString += ".0"; + } + + return `int pixY = planeNumber / ${width}; + int pixX = planeNumber - (pixY * ${width}); + // Sample from center of pixel + float u = (float(pixX) + 0.5) * ${pixelWidthString}; + float v = (float(pixY) + 0.5) * ${pixelHeightString}; + return texture(packedPlanes, vec2(u, v));`; +} + export default buildVoxelDrawCommands; diff --git a/packages/engine/Source/Scene/getClippingFunction.js b/packages/engine/Source/Scene/getClippingFunction.js index 690d542cf4f5..0a283c590ef0 100644 --- a/packages/engine/Source/Scene/getClippingFunction.js +++ b/packages/engine/Source/Scene/getClippingFunction.js @@ -38,66 +38,61 @@ function getClippingFunction(clippingPlaneCollection, context) { } function clippingFunctionUnion(clippingPlanesLength) { - const functionString = - `${ - "float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)\n" + - "{\n" + - " vec4 position = czm_windowToEyeCoordinates(fragCoord);\n" + - " vec3 clipNormal = vec3(0.0);\n" + - " vec3 clipPosition = vec3(0.0);\n" + - " float clipAmount;\n" + // For union planes, we want to get the min distance. So we set the initial value to the first plane distance in the loop below. - " float pixelWidth = czm_metersPerPixel(position);\n" + - " bool breakAndDiscard = false;\n" + - " for (int i = 0; i < " - }${clippingPlanesLength}; ++i)\n` + - ` {\n` + - ` vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);\n` + - ` clipNormal = clippingPlane.xyz;\n` + - ` clipPosition = -clippingPlane.w * clipNormal;\n` + - ` float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;\n` + - ` clipAmount = czm_branchFreeTernary(i == 0, amount, min(amount, clipAmount));\n` + - ` if (amount <= 0.0)\n` + - ` {\n` + - ` breakAndDiscard = true;\n` + - ` break;\n` + // HLSL compiler bug if we discard here: https://bugs.chromium.org/p/angleproject/issues/detail?id=1945#c6 - ` }\n` + - ` }\n` + - ` if (breakAndDiscard) {\n` + - ` discard;\n` + - ` }\n` + - ` return clipAmount;\n` + - `}\n`; - return functionString; + return `float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix) +{ + vec4 position = czm_windowToEyeCoordinates(fragCoord); + vec3 clipNormal = vec3(0.0); + vec3 clipPosition = vec3(0.0); + float clipAmount; + float pixelWidth = czm_metersPerPixel(position); + bool breakAndDiscard = false; + for (int i = 0; i < ${clippingPlanesLength}; ++i) + { + vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix); + clipNormal = clippingPlane.xyz; + clipPosition = -clippingPlane.w * clipNormal; + float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth; + clipAmount = czm_branchFreeTernary(i == 0, amount, min(amount, clipAmount)); + if (amount <= 0.0) + { + breakAndDiscard = true; + // HLSL compiler bug if we discard here: https://bugs.chromium.org/p/angleproject/issues/detail?id=1945#c6 + break; + } + } + if (breakAndDiscard) { + discard; + } + return clipAmount; +} +`; } function clippingFunctionIntersect(clippingPlanesLength) { - const functionString = - `${ - "float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)\n" + - "{\n" + - " bool clipped = true;\n" + - " vec4 position = czm_windowToEyeCoordinates(fragCoord);\n" + - " vec3 clipNormal = vec3(0.0);\n" + - " vec3 clipPosition = vec3(0.0);\n" + - " float clipAmount = 0.0;\n" + - " float pixelWidth = czm_metersPerPixel(position);\n" + - " for (int i = 0; i < " - }${clippingPlanesLength}; ++i)\n` + - ` {\n` + - ` vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);\n` + - ` clipNormal = clippingPlane.xyz;\n` + - ` clipPosition = -clippingPlane.w * clipNormal;\n` + - ` float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;\n` + - ` clipAmount = max(amount, clipAmount);\n` + - ` clipped = clipped && (amount <= 0.0);\n` + - ` }\n` + - ` if (clipped)\n` + - ` {\n` + - ` discard;\n` + - ` }\n` + - ` return clipAmount;\n` + - `}\n`; - return functionString; + return `float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix) +{ + bool clipped = true; + vec4 position = czm_windowToEyeCoordinates(fragCoord); + vec3 clipNormal = vec3(0.0); + vec3 clipPosition = vec3(0.0); + float clipAmount = 0.0; + float pixelWidth = czm_metersPerPixel(position); + for (int i = 0; i < ${clippingPlanesLength}; ++i) + { + vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix); + clipNormal = clippingPlane.xyz; + clipPosition = -clippingPlane.w * clipNormal; + float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth; + clipAmount = max(amount, clipAmount); + clipped = clipped && (amount <= 0.0); + } + if (clipped) + { + discard; + } + return clipAmount; + } +`; } function getClippingPlaneFloat(width, height) { @@ -113,19 +108,17 @@ function getClippingPlaneFloat(width, height) { pixelHeightString += ".0"; } - const functionString = - `${ - "vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)\n" + - "{\n" + - " int pixY = clippingPlaneNumber / " - }${width};\n` + - ` int pixX = clippingPlaneNumber - (pixY * ${width});\n` + - ` float u = (float(pixX) + 0.5) * ${pixelWidthString};\n` + // sample from center of pixel - ` float v = (float(pixY) + 0.5) * ${pixelHeightString};\n` + - ` vec4 plane = texture(packedClippingPlanes, vec2(u, v));\n` + - ` return czm_transformPlane(plane, transform);\n` + - `}\n`; - return functionString; + return `vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform) +{ + int pixY = clippingPlaneNumber / ${width}; + int pixX = clippingPlaneNumber - (pixY * ${width}); + // Sample from center of pixel + float u = (float(pixX) + 0.5) * ${pixelWidthString}; + float v = (float(pixY) + 0.5) * ${pixelHeightString}; + vec4 plane = texture(packedClippingPlanes, vec2(u, v)); + return czm_transformPlane(plane, transform); +} +`; } function getClippingPlaneUint8(width, height) { @@ -141,23 +134,21 @@ function getClippingPlaneUint8(width, height) { pixelHeightString += ".0"; } - const functionString = - `${ - "vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)\n" + - "{\n" + - " int clippingPlaneStartIndex = clippingPlaneNumber * 2;\n" + // clipping planes are two pixels each - " int pixY = clippingPlaneStartIndex / " - }${width};\n` + - ` int pixX = clippingPlaneStartIndex - (pixY * ${width});\n` + - ` float u = (float(pixX) + 0.5) * ${pixelWidthString};\n` + // sample from center of pixel - ` float v = (float(pixY) + 0.5) * ${pixelHeightString};\n` + - ` vec4 oct32 = texture(packedClippingPlanes, vec2(u, v)) * 255.0;\n` + - ` vec2 oct = vec2(oct32.x * 256.0 + oct32.y, oct32.z * 256.0 + oct32.w);\n` + - ` vec4 plane;\n` + - ` plane.xyz = czm_octDecode(oct, 65535.0);\n` + - ` plane.w = czm_unpackFloat(texture(packedClippingPlanes, vec2(u + ${pixelWidthString}, v)));\n` + - ` return czm_transformPlane(plane, transform);\n` + - `}\n`; - return functionString; + return `vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform) +{ + int clippingPlaneStartIndex = clippingPlaneNumber * 2; + int pixY = clippingPlaneStartIndex / ${width}; + int pixX = clippingPlaneStartIndex - (pixY * ${width}); + // Sample from center of pixel + float u = (float(pixX) + 0.5) * ${pixelWidthString}; + float v = (float(pixY) + 0.5) * ${pixelHeightString}; + vec4 oct32 = texture(packedClippingPlanes, vec2(u, v)) * 255.0; + vec2 oct = vec2(oct32.x * 256.0 + oct32.y, oct32.z * 256.0 + oct32.w); + vec4 plane; + plane.xyz = czm_octDecode(oct, 65535.0); + plane.w = czm_unpackFloat(texture(packedClippingPlanes, vec2(u + ${pixelWidthString}, v))); + return czm_transformPlane(plane, transform); +} +`; } export default getClippingFunction; diff --git a/packages/engine/Source/Shaders/Voxels/IntersectBox.glsl b/packages/engine/Source/Shaders/Voxels/IntersectBox.glsl index fd838e2d3335..fc351ddcfe94 100644 --- a/packages/engine/Source/Shaders/Voxels/IntersectBox.glsl +++ b/packages/engine/Source/Shaders/Voxels/IntersectBox.glsl @@ -5,43 +5,30 @@ #define BOX_INTERSECTION_INDEX ### // always 0 */ -uniform vec3 u_renderMinBounds; -uniform vec3 u_renderMaxBounds; - -RayShapeIntersection intersectBox(in Ray ray, in vec3 minBound, in vec3 maxBound) -{ - // Consider the box as the intersection of the space between 3 pairs of parallel planes - // Compute the distance along the ray to each plane - vec3 t0 = (minBound - ray.pos) / ray.dir; - vec3 t1 = (maxBound - ray.pos) / ray.dir; - - // Identify candidate entries/exits based on distance from ray.pos - vec3 entries = min(t0, t1); - vec3 exits = max(t0, t1); - - vec3 directions = sign(ray.dir); - - // The actual intersection points are the furthest entry and the closest exit - float lastEntry = maxComponent(entries); - bvec3 isLastEntry = equal(entries, vec3(lastEntry)); - vec3 entryNormal = -1.0 * vec3(isLastEntry) * directions; - vec4 entry = vec4(entryNormal, lastEntry); - - float firstExit = minComponent(exits); - bvec3 isFirstExit = equal(exits, vec3(firstExit)); - vec3 exitNormal = vec3(isLastEntry) * directions; - vec4 exit = vec4(exitNormal, firstExit); - - if (entry.w > exit.w) { - entry.w = NO_HIT; - exit.w = NO_HIT; +uniform sampler2D u_renderBoundPlanesTexture; + +RayShapeIntersection intersectBoundPlanes(in Ray ray) { + vec4 lastEntry = vec4(ray.dir, -INF_HIT); + vec4 firstExit = vec4(-ray.dir, +INF_HIT); + for (int i = 0; i < 6; i++) { + vec4 boundPlane = getBoundPlane(u_renderBoundPlanesTexture, i); + vec4 intersection = intersectPlane(ray, boundPlane); + if (dot(ray.dir, boundPlane.xyz) < 0.0) { + lastEntry = intersection.w > lastEntry.w ? intersection : lastEntry; + } else { + firstExit = intersection.w < firstExit.w ? intersection: firstExit; + } } - return RayShapeIntersection(entry, exit); + if (lastEntry.w < firstExit.w) { + return RayShapeIntersection(lastEntry, firstExit); + } else { + return RayShapeIntersection(vec4(-ray.dir, NO_HIT), vec4(ray.dir, NO_HIT)); + } } -void intersectShape(in Ray ray, inout Intersections ix) +void intersectShape(in Ray rayUV, in Ray rayEC, inout Intersections ix) { - RayShapeIntersection intersection = intersectBox(ray, u_renderMinBounds, u_renderMaxBounds); + RayShapeIntersection intersection = intersectBoundPlanes(rayEC); setShapeIntersection(ix, BOX_INTERSECTION_INDEX, intersection); } diff --git a/packages/engine/Source/Shaders/Voxels/IntersectCylinder.glsl b/packages/engine/Source/Shaders/Voxels/IntersectCylinder.glsl index 009f342f2743..7ac7086b4103 100644 --- a/packages/engine/Source/Shaders/Voxels/IntersectCylinder.glsl +++ b/packages/engine/Source/Shaders/Voxels/IntersectCylinder.glsl @@ -19,32 +19,30 @@ // Cylinder uniforms uniform vec2 u_cylinderRenderRadiusMinMax; -uniform vec2 u_cylinderRenderHeightMinMax; #if defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE) uniform vec2 u_cylinderRenderAngleMinMax; #endif -/** - * Find the intersection of a ray with the volume defined by two planes of constant z - */ -RayShapeIntersection intersectHeightBounds(in Ray ray, in vec2 minMaxHeight, in bool convex) -{ - float zPosition = ray.pos.z; - float zDirection = ray.dir.z; - - float tmin = (minMaxHeight.x - zPosition) / zDirection; - float tmax = (minMaxHeight.y - zPosition) / zDirection; - - // Normals point outside the volume - float signFlip = convex ? 1.0 : -1.0; - vec4 intersectMin = vec4(0.0, 0.0, -1.0 * signFlip, tmin); - vec4 intersectMax = vec4(0.0, 0.0, 1.0 * signFlip, tmax); - - bool topEntry = zDirection < 0.0; - vec4 entry = topEntry ? intersectMax : intersectMin; - vec4 exit = topEntry ? intersectMin : intersectMax; +uniform sampler2D u_renderBoundPlanesTexture; + +RayShapeIntersection intersectBoundPlanes(in Ray ray) { + vec4 lastEntry = vec4(ray.dir, -INF_HIT); + vec4 firstExit = vec4(-ray.dir, +INF_HIT); + for (int i = 0; i < 2; i++) { + vec4 boundPlane = getBoundPlane(u_renderBoundPlanesTexture, i); + vec4 intersection = intersectPlane(ray, boundPlane); + if (dot(ray.dir, boundPlane.xyz) < 0.0) { + lastEntry = intersection.w > lastEntry.w ? intersection : lastEntry; + } else { + firstExit = intersection.w < firstExit.w ? intersection: firstExit; + } + } - return RayShapeIntersection(entry, exit); + if (lastEntry.w < firstExit.w) { + return RayShapeIntersection(lastEntry, firstExit); + } else { + return RayShapeIntersection(vec4(-ray.dir, NO_HIT), vec4(ray.dir, NO_HIT)); + } } /** @@ -70,8 +68,11 @@ RayShapeIntersection intersectCylinder(in Ray ray, in float radius, in bool conv float t1 = (-b - determinant) / a; float t2 = (-b + determinant) / a; float signFlip = convex ? 1.0 : -1.0; - vec4 intersect1 = vec4(normalize(position + t1 * direction) * signFlip, 0.0, t1); - vec4 intersect2 = vec4(normalize(position + t2 * direction) * signFlip, 0.0, t2); + vec3 normal1 = vec3((position + t1 * direction) * signFlip, 0.0); + vec3 normal2 = vec3((position + t2 * direction) * signFlip, 0.0); + // Return normals in eye coordinates + vec4 intersect1 = vec4(normalize(czm_normal * normal1), t1); + vec4 intersect2 = vec4(normalize(czm_normal * normal2), t2); return RayShapeIntersection(intersect1, intersect2); } @@ -80,21 +81,16 @@ RayShapeIntersection intersectCylinder(in Ray ray, in float radius, in bool conv * Find the intersection of a ray with a right cylindrical solid of given * radius and height bounds. NOTE: The shape is assumed to be convex. */ -RayShapeIntersection intersectBoundedCylinder(in Ray ray, in float radius, in vec2 minMaxHeight) +RayShapeIntersection intersectBoundedCylinder(in Ray ray, in Ray rayEC, in float radius) { RayShapeIntersection cylinderIntersection = intersectCylinder(ray, radius, true); - RayShapeIntersection heightBoundsIntersection = intersectHeightBounds(ray, minMaxHeight, true); + RayShapeIntersection heightBoundsIntersection = intersectBoundPlanes(rayEC); return intersectIntersections(ray, cylinderIntersection, heightBoundsIntersection); } -void intersectShape(Ray ray, inout Intersections ix) +void intersectShape(in Ray ray, in Ray rayEC, inout Intersections ix) { - // Position is converted from [0,1] to [-1,+1] because shape intersections assume unit space is [-1,+1]. - // Direction is scaled as well to be in sync with position. - ray.pos = ray.pos * 2.0 - 1.0; - ray.dir *= 2.0; - - RayShapeIntersection outerIntersect = intersectBoundedCylinder(ray, u_cylinderRenderRadiusMinMax.y, u_cylinderRenderHeightMinMax); + RayShapeIntersection outerIntersect = intersectBoundedCylinder(ray, rayEC, u_cylinderRenderRadiusMinMax.y); setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_RADIUS_MAX, outerIntersect); diff --git a/packages/engine/Source/Shaders/Voxels/IntersectDepth.glsl b/packages/engine/Source/Shaders/Voxels/IntersectDepth.glsl index 69c01bd3cd4a..e152f8cac2ae 100644 --- a/packages/engine/Source/Shaders/Voxels/IntersectDepth.glsl +++ b/packages/engine/Source/Shaders/Voxels/IntersectDepth.glsl @@ -5,8 +5,6 @@ #define DEPTH_INTERSECTION_INDEX ### */ -uniform mat4 u_transformPositionViewToUv; - void intersectDepth(in vec2 screenCoord, in Ray ray, inout Intersections ix) { float logDepthOrDepth = czm_unpackDepth(texture(czm_globeDepthTexture, screenCoord)); float entry; @@ -15,8 +13,7 @@ void intersectDepth(in vec2 screenCoord, in Ray ray, inout Intersections ix) { // Calculate how far the ray must travel before it hits the depth buffer. vec4 eyeCoordinateDepth = czm_screenToEyeCoordinates(screenCoord, logDepthOrDepth); eyeCoordinateDepth /= eyeCoordinateDepth.w; - vec3 depthPositionUv = vec3(u_transformPositionViewToUv * eyeCoordinateDepth); - entry = dot(depthPositionUv - ray.pos, ray.dir); + entry = dot(eyeCoordinateDepth.xyz - ray.pos, ray.dir); exit = +INF_HIT; } else { // There's no depth at this location. diff --git a/packages/engine/Source/Shaders/Voxels/IntersectEllipsoid.glsl b/packages/engine/Source/Shaders/Voxels/IntersectEllipsoid.glsl index 1dec522b6df7..262f756493d1 100644 --- a/packages/engine/Source/Shaders/Voxels/IntersectEllipsoid.glsl +++ b/packages/engine/Source/Shaders/Voxels/IntersectEllipsoid.glsl @@ -26,7 +26,7 @@ #endif uniform float u_eccentricitySquared; uniform vec2 u_ellipsoidRenderLatitudeSinMinMax; -uniform vec2 u_clipMinMaxHeight; +uniform vec2 u_clipMinMaxHeight; // Values are negative: clipHeight - maxShapeHeight RayShapeIntersection intersectZPlane(in Ray ray, in float z) { float t = -ray.pos.z / ray.dir.z; @@ -44,11 +44,11 @@ RayShapeIntersection intersectZPlane(in Ray ray, in float z) { } } -RayShapeIntersection intersectHeight(in Ray ray, in float relativeHeight, in bool convex) +RayShapeIntersection intersectHeight(in Ray ray, in float height, in bool convex) { // Scale the ray by the ellipsoid axes to make it a unit sphere // Note: approximating ellipsoid + height as an ellipsoid - vec3 radiiCorrection = u_ellipsoidRadiiUv / (u_ellipsoidRadiiUv + relativeHeight); + vec3 radiiCorrection = vec3(1.0) / (u_ellipsoidRadii + height); vec3 position = ray.pos * radiiCorrection; vec3 direction = ray.dir * radiiCorrection; @@ -74,10 +74,14 @@ RayShapeIntersection intersectHeight(in Ray ray, in float relativeHeight, in boo float tmax = max(t1, t2); float directionScale = convex ? 1.0 : -1.0; - vec3 d1 = directionScale * normalize(position + tmin * direction); - vec3 d2 = directionScale * normalize(position + tmax * direction); + vec3 d1 = directionScale * (position + tmin * direction); + vec3 d2 = directionScale * (position + tmax * direction); - return RayShapeIntersection(vec4(d1, tmin), vec4(d2, tmax)); + // Return normals in eye coordinates. Use spherical approximation for the normal. + vec3 normal1 = normalize(czm_normal * d1); + vec3 normal2 = normalize(czm_normal * d2); + + return RayShapeIntersection(vec4(normal1, tmin), vec4(normal2, tmax)); } /** @@ -151,16 +155,13 @@ float getLatitudeConeShift(in float sinLatitude) { // Find prime vertical radius of curvature: // the distance along the ellipsoid normal to the intersection with the z-axis float x2 = u_eccentricitySquared * sinLatitude * sinLatitude; - float primeVerticalRadius = inversesqrt(1.0 - x2); + float primeVerticalRadius = u_ellipsoidRadii.x * inversesqrt(1.0 - x2); // Compute a shift from the origin to the intersection of the cone with the z-axis return primeVerticalRadius * u_eccentricitySquared * sinLatitude; } void intersectFlippedCone(in Ray ray, in float cosHalfAngle, out RayShapeIntersection intersections[2]) { - // Undo the scaling from ellipsoid to sphere - ray.pos = ray.pos * u_ellipsoidRadiiUv; - ray.dir = ray.dir * u_ellipsoidRadiiUv; // Shift the ray to account for the latitude cone not being centered at the Earth center ray.pos.z += getLatitudeConeShift(cosHalfAngle); @@ -206,9 +207,6 @@ void intersectFlippedCone(in Ray ray, in float cosHalfAngle, out RayShapeInterse } RayShapeIntersection intersectRegularCone(in Ray ray, in float cosHalfAngle, in bool convex) { - // Undo the scaling from ellipsoid to sphere - ray.pos = ray.pos * u_ellipsoidRadiiUv; - ray.dir = ray.dir * u_ellipsoidRadiiUv; // Shift the ray to account for the latitude cone not being centered at the Earth center ray.pos.z += getLatitudeConeShift(cosHalfAngle); @@ -245,13 +243,7 @@ RayShapeIntersection intersectRegularCone(in Ray ray, in float cosHalfAngle, in } } -void intersectShape(in Ray ray, inout Intersections ix) { - // Position is converted from [0,1] to [-1,+1] because shape intersections assume unit space is [-1,+1]. - // Direction is scaled as well to be in sync with position. - ray.pos = ray.pos * 2.0 - 1.0; - ray.dir *= 2.0; - - // Outer ellipsoid +void intersectShape(in Ray ray, in Ray rayEC, inout Intersections ix) { // Outer ellipsoid RayShapeIntersection outerIntersect = intersectHeight(ray, u_clipMinMaxHeight.y, true); setShapeIntersection(ix, ELLIPSOID_INTERSECTION_INDEX_HEIGHT_MAX, outerIntersect); diff --git a/packages/engine/Source/Shaders/Voxels/IntersectLongitude.glsl b/packages/engine/Source/Shaders/Voxels/IntersectLongitude.glsl index e3b628870545..8fb9b9cb4ca1 100644 --- a/packages/engine/Source/Shaders/Voxels/IntersectLongitude.glsl +++ b/packages/engine/Source/Shaders/Voxels/IntersectLongitude.glsl @@ -1,6 +1,14 @@ // See IntersectionUtils.glsl for the definitions of Ray, NO_HIT, INF_HIT, // RayShapeIntersection +vec4 transformNormalToEC(in vec4 intersection) { + return vec4(normalize(czm_normal * intersection.xyz), intersection.w); +} + +RayShapeIntersection transformNormalsToEC(in RayShapeIntersection ix) { + return RayShapeIntersection(transformNormalToEC(ix.entry), transformNormalToEC(ix.exit)); +} + vec4 intersectLongitude(in Ray ray, in float angle, in bool positiveNormal) { float normalSign = positiveNormal ? 1.0 : -1.0; vec2 planeNormal = vec2(-sin(angle), cos(angle)) * normalSign; @@ -32,8 +40,8 @@ RayShapeIntersection intersectHalfSpace(in Ray ray, in float angle, in bool posi void intersectFlippedWedge(in Ray ray, in vec2 minMaxAngle, out RayShapeIntersection intersections[2]) { - intersections[0] = intersectHalfSpace(ray, minMaxAngle.x, false); - intersections[1] = intersectHalfSpace(ray, minMaxAngle.y, true); + intersections[0] = transformNormalsToEC(intersectHalfSpace(ray, minMaxAngle.x, false)); + intersections[1] = transformNormalsToEC(intersectHalfSpace(ray, minMaxAngle.y, true)); } bool hitPositiveHalfPlane(in Ray ray, in vec4 intersection, in bool positiveNormal) { @@ -46,14 +54,18 @@ bool hitPositiveHalfPlane(in Ray ray, in vec4 intersection, in bool positiveNorm void intersectHalfPlane(in Ray ray, in float angle, out RayShapeIntersection intersections[2]) { vec4 intersection = intersectLongitude(ray, angle, true); vec4 farSide = vec4(normalize(ray.dir), INF_HIT); + bool hitPositiveSide = hitPositiveHalfPlane(ray, intersection, true); + + farSide = transformNormalToEC(farSide); - if (hitPositiveHalfPlane(ray, intersection, true)) { + if (hitPositiveSide) { + intersection = transformNormalToEC(intersection); intersections[0].entry = -1.0 * farSide; - intersections[0].exit = vec4(-1.0 * intersection.xy, 0.0, intersection.w); + intersections[0].exit = vec4(-1.0 * intersection.xyz, intersection.w); intersections[1].entry = intersection; intersections[1].exit = farSide; } else { - vec4 miss = vec4(normalize(ray.dir), NO_HIT); + vec4 miss = vec4(normalize(czm_normal * ray.dir), NO_HIT); intersections[0].entry = -1.0 * farSide; intersections[0].exit = farSide; intersections[1].entry = miss; @@ -88,15 +100,15 @@ RayShapeIntersection intersectRegularWedge(in Ray ray, in vec2 minMaxAngle) if (exitFromInside && enterFromOutside) { // Ray crosses both faces of negative wedge, exiting then entering the positive shape - return RayShapeIntersection(first, last); + return transformNormalsToEC(RayShapeIntersection(first, last)); } else if (!exitFromInside && enterFromOutside) { // Ray starts inside wedge. last is in shadow wedge, and first is actually the entry - return RayShapeIntersection(-1.0 * farSide, first); + return transformNormalsToEC(RayShapeIntersection(-1.0 * farSide, first)); } else if (exitFromInside && !enterFromOutside) { // First intersection was in the shadow wedge, so last is actually the exit - return RayShapeIntersection(last, farSide); + return transformNormalsToEC(RayShapeIntersection(last, farSide)); } else { // !exitFromInside && !enterFromOutside // Both intersections were in the shadow wedge - return RayShapeIntersection(miss, miss); + return transformNormalsToEC(RayShapeIntersection(miss, miss)); } } diff --git a/packages/engine/Source/Shaders/Voxels/IntersectClippingPlanes.glsl b/packages/engine/Source/Shaders/Voxels/IntersectPlane.glsl similarity index 97% rename from packages/engine/Source/Shaders/Voxels/IntersectClippingPlanes.glsl rename to packages/engine/Source/Shaders/Voxels/IntersectPlane.glsl index 75e4e37f8e3b..4d117de72bcf 100644 --- a/packages/engine/Source/Shaders/Voxels/IntersectClippingPlanes.glsl +++ b/packages/engine/Source/Shaders/Voxels/IntersectPlane.glsl @@ -22,6 +22,7 @@ vec4 intersectPlane(in Ray ray, in vec4 plane) { return vec4(n, t); } +#ifdef CLIPPING_PLANES void intersectClippingPlanes(in Ray ray, inout Intersections ix) { vec4 backSide = vec4(-ray.dir, -INF_HIT); vec4 farSide = vec4(ray.dir, +INF_HIT); @@ -30,7 +31,7 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) { #if (CLIPPING_PLANES_COUNT == 1) // Union and intersection are the same when there's one clipping plane, and the code // is more simplified. - vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, 0, u_clippingPlanesMatrix); + vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, 0); vec4 intersection = intersectPlane(ray, planeUv); bool reflects = dot(ray.dir, intersection.xyz) < 0.0; clippingVolume.entry = reflects ? backSide : intersection; @@ -40,7 +41,7 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) { vec4 firstTransmission = vec4(ray.dir, +INF_HIT); vec4 lastReflection = vec4(-ray.dir, -INF_HIT); for (int i = 0; i < CLIPPING_PLANES_COUNT; i++) { - vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i, u_clippingPlanesMatrix); + vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i); vec4 intersection = intersectPlane(ray, planeUv); if (dot(ray.dir, planeUv.xyz) > 0.0) { firstTransmission = intersection.w <= firstTransmission.w ? intersection : firstTransmission; @@ -58,7 +59,7 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) { vec4 lastTransmission = vec4(ray.dir, -INF_HIT); vec4 firstReflection = vec4(-ray.dir, +INF_HIT); for (int i = 0; i < CLIPPING_PLANES_COUNT; i++) { - vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i, u_clippingPlanesMatrix); + vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i); vec4 intersection = intersectPlane(ray, planeUv); if (dot(ray.dir, planeUv.xyz) > 0.0) { lastTransmission = intersection.w > lastTransmission.w ? intersection : lastTransmission; @@ -76,3 +77,4 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) { setShapeIntersection(ix, CLIPPING_PLANES_INTERSECTION_INDEX, clippingVolume); #endif } +#endif diff --git a/packages/engine/Source/Shaders/Voxels/Intersection.glsl b/packages/engine/Source/Shaders/Voxels/Intersection.glsl index 5c36a7b70545..da572b61e708 100644 --- a/packages/engine/Source/Shaders/Voxels/Intersection.glsl +++ b/packages/engine/Source/Shaders/Voxels/Intersection.glsl @@ -11,9 +11,9 @@ #define INTERSECTION_COUNT ### */ -RayShapeIntersection intersectScene(in vec2 screenCoord, in Ray ray, out Intersections ix) { +RayShapeIntersection intersectScene(in vec2 screenCoord, in Ray ray, in Ray rayEC, out Intersections ix) { // Do a ray-shape intersection to find the exact starting and ending points. - intersectShape(ray, ix); + intersectShape(ray, rayEC, ix); // Exit early if the positive shape was completely missed or behind the ray. RayShapeIntersection intersection = getFirstIntersection(ix); @@ -28,7 +28,7 @@ RayShapeIntersection intersectScene(in vec2 screenCoord, in Ray ray, out Interse #endif // Depth - intersectDepth(screenCoord, ray, ix); + intersectDepth(screenCoord, rayEC, ix); // Find the first intersection that's in front of the ray #if (INTERSECTION_COUNT > 1) diff --git a/packages/engine/Source/Shaders/Voxels/Octree.glsl b/packages/engine/Source/Shaders/Voxels/Octree.glsl index 387be666b726..a875f9aa9093 100644 --- a/packages/engine/Source/Shaders/Voxels/Octree.glsl +++ b/packages/engine/Source/Shaders/Voxels/Octree.glsl @@ -29,6 +29,11 @@ struct TraversalData { int parentOctreeIndex; }; +struct TileAndUvCoordinate { + ivec4 tileCoords; + vec3 tileUv; +}; + struct SampleData { int megatextureIndex; ivec4 tileCoords; @@ -79,23 +84,20 @@ int getOctreeParentIndex(in int octreeIndex) { return parentOctreeIndex; } -/** -* Convert a position in the uv-space of the tileset bounding shape -* into the uv-space of a tile within the tileset -*/ -vec3 getTileUv(in vec3 shapePosition, in ivec4 octreeCoords) { - // PERFORMANCE_IDEA: use bit-shifting (only in WebGL2) - float dimAtLevel = exp2(float(octreeCoords.w)); - return shapePosition * dimAtLevel - vec3(octreeCoords.xyz); +vec3 getTileUv(in TileAndUvCoordinate tileAndUv, in ivec4 octreeCoords) { + int levelDifference = tileAndUv.tileCoords.w - octreeCoords.w; + float scalar = exp2(-1.0 * float(levelDifference)); + vec3 originShift = vec3(tileAndUv.tileCoords.xyz - (octreeCoords.xyz << levelDifference)) * scalar; + return tileAndUv.tileUv * scalar + originShift; } -vec3 getClampedTileUv(in vec3 shapePosition, in ivec4 octreeCoords) { - vec3 tileUv = getTileUv(shapePosition, octreeCoords); +vec3 getClampedTileUv(in TileAndUvCoordinate tileAndUv, in ivec4 octreeCoords) { + vec3 tileUv = getTileUv(tileAndUv, octreeCoords); return clamp(tileUv, vec3(0.0), vec3(1.0)); } -void addSampleCoordinates(in vec3 shapePosition, inout SampleData sampleData) { - vec3 tileUv = getClampedTileUv(shapePosition, sampleData.tileCoords); +void addSampleCoordinates(in TileAndUvCoordinate tileAndUv, inout SampleData sampleData) { + vec3 tileUv = getClampedTileUv(tileAndUv, sampleData.tileCoords); vec3 inputCoordinate = tileUv * vec3(u_dimensions); #if defined(PADDING) @@ -162,32 +164,25 @@ void getOctreeLeafSampleDatas(in OctreeNodeData data, in ivec4 octreeCoords, out } #endif -OctreeNodeData traverseOctreeDownwards(in vec3 shapePosition, inout TraversalData traversalData) { - float sizeAtLevel = exp2(-1.0 * float(traversalData.octreeCoords.w)); - vec3 start = vec3(traversalData.octreeCoords.xyz) * sizeAtLevel; - vec3 end = start + vec3(sizeAtLevel); +OctreeNodeData traverseOctreeDownwards(in ivec4 tileCoordinate, inout TraversalData traversalData) { OctreeNodeData childData; for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) { - // Find out which octree child contains the position - // 0 if before center, 1 if after - vec3 center = 0.5 * (start + end); - vec3 childCoord = step(center, shapePosition); - - // Get octree coords for the next level down - ivec4 octreeCoords = traversalData.octreeCoords; - traversalData.octreeCoords = ivec4(octreeCoords.xyz * 2 + ivec3(childCoord), octreeCoords.w + 1); + // tileCoordinate.xyz is defined at the level of detail tileCoordinate.w. + // Find the corresponding coordinate at the level traversalData.octreeCoords.w + int level = traversalData.octreeCoords.w + 1; + int levelDifference = tileCoordinate.w - level; + ivec3 coordinateAtLevel = tileCoordinate.xyz >> levelDifference; + traversalData.octreeCoords = ivec4(coordinateAtLevel, level); - childData = getOctreeChildData(traversalData.parentOctreeIndex, ivec3(childCoord)); + ivec3 childCoordinate = coordinateAtLevel & 1; + childData = getOctreeChildData(traversalData.parentOctreeIndex, childCoordinate); if (childData.flag != OCTREE_FLAG_INTERNAL) { // leaf tile - stop traversing break; } - // interior tile - keep going deeper - start = mix(start, center, childCoord); - end = mix(center, end, childCoord); traversalData.parentOctreeIndex = childData.data; } @@ -198,50 +193,50 @@ OctreeNodeData traverseOctreeDownwards(in vec3 shapePosition, inout TraversalDat * Transform a given position to an octree tile coordinate and a position within that tile, * and find the corresponding megatexture index and texture coordinates */ -void traverseOctreeFromBeginning(in vec3 shapePosition, out TraversalData traversalData, out SampleData sampleDatas[SAMPLE_COUNT]) { +void traverseOctreeFromBeginning(in TileAndUvCoordinate tileAndUv, out TraversalData traversalData, out SampleData sampleDatas[SAMPLE_COUNT]) { traversalData.octreeCoords = ivec4(0); traversalData.parentOctreeIndex = 0; OctreeNodeData nodeData = getOctreeNodeData(vec2(0.0)); if (nodeData.flag != OCTREE_FLAG_LEAF) { - nodeData = traverseOctreeDownwards(shapePosition, traversalData); + nodeData = traverseOctreeDownwards(tileAndUv.tileCoords, traversalData); } #if (SAMPLE_COUNT == 1) getOctreeLeafSampleData(nodeData, traversalData.octreeCoords, sampleDatas[0]); - addSampleCoordinates(shapePosition, sampleDatas[0]); + addSampleCoordinates(tileAndUv, sampleDatas[0]); #else getOctreeLeafSampleDatas(nodeData, traversalData.octreeCoords, sampleDatas); - addSampleCoordinates(shapePosition, sampleDatas[0]); - addSampleCoordinates(shapePosition, sampleDatas[1]); + addSampleCoordinates(tileAndUv, sampleDatas[0]); + addSampleCoordinates(tileAndUv, sampleDatas[1]); #endif } -bool inRange(in vec3 v, in vec3 minVal, in vec3 maxVal) { - return clamp(v, minVal, maxVal) == v; -} - -bool insideTile(in vec3 shapePosition, in ivec4 octreeCoords) { - vec3 tileUv = getTileUv(shapePosition, octreeCoords); - bool inside = inRange(tileUv, vec3(0.0), vec3(1.0)); - // Assume (!) the position is always inside the root tile. - return inside || octreeCoords.w == 0; +bool insideTile(in ivec4 tileCoordinate, in ivec4 octreeCoords) { + int levelDifference = tileCoordinate.w - octreeCoords.w; + if (levelDifference < 0) { + return false; + } + ivec3 coordinateAtLevel = tileCoordinate.xyz >> levelDifference; + return coordinateAtLevel == octreeCoords.xyz; } -void traverseOctreeFromExisting(in vec3 shapePosition, inout TraversalData traversalData, inout SampleData sampleDatas[SAMPLE_COUNT]) { - if (insideTile(shapePosition, traversalData.octreeCoords)) { +void traverseOctreeFromExisting(in TileAndUvCoordinate tileAndUv, inout TraversalData traversalData, inout SampleData sampleDatas[SAMPLE_COUNT]) { + ivec4 tileCoords = tileAndUv.tileCoords; + if (insideTile(tileCoords, traversalData.octreeCoords)) { for (int i = 0; i < SAMPLE_COUNT; i++) { - addSampleCoordinates(shapePosition, sampleDatas[i]); + addSampleCoordinates(tileAndUv, sampleDatas[i]); } return; } - // Go up tree until we find a parent tile containing shapePosition + // Go up tree until we find a parent tile containing tileCoords. + // Assumes all parents are available all they way up to the root. for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) { traversalData.octreeCoords.xyz /= 2; traversalData.octreeCoords.w -= 1; - if (insideTile(shapePosition, traversalData.octreeCoords)) { + if (insideTile(tileCoords, traversalData.octreeCoords)) { break; } @@ -249,14 +244,14 @@ void traverseOctreeFromExisting(in vec3 shapePosition, inout TraversalData trave } // Go down tree - OctreeNodeData nodeData = traverseOctreeDownwards(shapePosition, traversalData); + OctreeNodeData nodeData = traverseOctreeDownwards(tileCoords, traversalData); #if (SAMPLE_COUNT == 1) getOctreeLeafSampleData(nodeData, traversalData.octreeCoords, sampleDatas[0]); - addSampleCoordinates(shapePosition, sampleDatas[0]); + addSampleCoordinates(tileAndUv, sampleDatas[0]); #else getOctreeLeafSampleDatas(nodeData, traversalData.octreeCoords, sampleDatas); - addSampleCoordinates(shapePosition, sampleDatas[0]); - addSampleCoordinates(shapePosition, sampleDatas[1]); + addSampleCoordinates(tileAndUv, sampleDatas[0]); + addSampleCoordinates(tileAndUv, sampleDatas[1]); #endif } diff --git a/packages/engine/Source/Shaders/Voxels/VoxelFS.glsl b/packages/engine/Source/Shaders/Voxels/VoxelFS.glsl index 1164f80fd8a2..3af0eedbf0ab 100644 --- a/packages/engine/Source/Shaders/Voxels/VoxelFS.glsl +++ b/packages/engine/Source/Shaders/Voxels/VoxelFS.glsl @@ -1,9 +1,9 @@ // See Intersection.glsl for the definition of intersectScene // See IntersectionUtils.glsl for the definition of nextIntersection -// See convertUvToBox.glsl, convertUvToCylinder.glsl, or convertUvToEllipsoid.glsl -// for the definition of convertUvToShapeUvSpace. The appropriate function is -// selected based on the VoxelPrimitive shape type, and added to the shader in -// Scene/VoxelRenderResources.js. +// See convertLocalToBoxUv.glsl, convertLocalToCylinderUv.glsl, or convertLocalToEllipsoidUv.glsl +// for the definitions of convertLocalToShapeSpaceDerivative and getTileAndUvCoordinate. +// The appropriate functions are selected based on the VoxelPrimitive shape type, +// and added to the shader in Scene/VoxelRenderResources.js. // See Octree.glsl for the definitions of TraversalData, SampleData, // traverseOctreeFromBeginning, and traverseOctreeFromExisting // See Megatexture.glsl for the definition of accumulatePropertiesFromMegatexture @@ -15,10 +15,10 @@ #define ALPHA_ACCUM_MAX 0.98 // Must be > 0.0 and <= 1.0 #endif -uniform mat4 u_transformPositionUvToView; +uniform mat4 u_transformPositionViewToLocal; uniform mat3 u_transformDirectionViewToLocal; -uniform vec3 u_cameraPositionUv; -uniform vec3 u_cameraDirectionUv; +uniform vec3 u_cameraPositionLocal; +uniform vec3 u_cameraDirectionLocal; uniform float u_stepSize; #if defined(PICKING) @@ -63,16 +63,15 @@ RayShapeIntersection getVoxelIntersection(in vec3 tileUv, in vec3 sampleSizeAlon } vec4 getStepSize(in SampleData sampleData, in Ray viewRay, in RayShapeIntersection shapeIntersection, in mat3 jacobianT, in float currentT) { - // The Jacobian is computed in a space where the shape spans [-1, 1]. - // But the ray is marched in a space where the shape fills [0, 1]. - // So we need to scale the Jacobian by 2. - vec3 gradient = 2.0 * viewRay.rawDir * jacobianT; + vec3 gradient = viewRay.dir * jacobianT; vec3 sampleSizeAlongRay = getSampleSize(sampleData.tileCoords.w) / gradient; RayShapeIntersection voxelIntersection = getVoxelIntersection(sampleData.tileUv, sampleSizeAlongRay); - // Transform normal from shape space to Cartesian space - vec3 voxelNormal = normalize(jacobianT * voxelIntersection.entry.xyz); + // Transform normal from shape space to Cartesian space to eye space + vec3 voxelNormal = jacobianT * voxelIntersection.entry.xyz; + voxelNormal = normalize(czm_normal * voxelNormal); + // Compare with the shape intersection, to choose the appropriate normal vec4 voxelEntry = vec4(voxelNormal, currentT + voxelIntersection.entry.w); vec4 entry = intersectionMax(shapeIntersection.entry, voxelEntry); @@ -114,37 +113,40 @@ int getSampleIndex(in SampleData sampleData) { } /** - * Compute the view ray at the current fragment, in the local UV coordinates of the shape. + * Compute the view ray at the current fragment, in the local coordinates of the shape. */ -Ray getViewRayUv() { +Ray getViewRayLocal() { vec4 eyeCoordinates = czm_windowToEyeCoordinates(gl_FragCoord); - vec3 viewDirUv; - vec3 viewPosUv; + vec3 origin; + vec3 direction; if (czm_orthographicIn3D == 1.0) { eyeCoordinates.z = 0.0; - viewPosUv = (u_transformPositionViewToUv * eyeCoordinates).xyz; - viewDirUv = normalize(u_cameraDirectionUv); + origin = (u_transformPositionViewToLocal * eyeCoordinates).xyz; + direction = u_cameraDirectionLocal; } else { - viewPosUv = u_cameraPositionUv; - viewDirUv = normalize(u_transformDirectionViewToLocal * eyeCoordinates.xyz); + origin = u_cameraPositionLocal; + direction = u_transformDirectionViewToLocal * normalize(eyeCoordinates.xyz); } - #if defined(SHAPE_ELLIPSOID) - // viewDirUv has been scaled to a space where the ellipsoid is a sphere. - // Undo this scaling to get the raw direction. - vec3 rawDir = viewDirUv * u_ellipsoidRadiiUv; - return Ray(viewPosUv, viewDirUv, rawDir); - #else - return Ray(viewPosUv, viewDirUv, viewDirUv); - #endif + return Ray(origin, direction); +} + +Ray getViewRayEC() { + vec4 eyeCoordinates = czm_windowToEyeCoordinates(gl_FragCoord); + vec3 viewPosEC = (czm_orthographicIn3D == 1.0) + ? vec3(eyeCoordinates.xy, 0.0) + : vec3(0.0); + vec3 viewDirEC = normalize(eyeCoordinates.xyz); + return Ray(viewPosEC, viewDirEC); } void main() { - Ray viewRayUv = getViewRayUv(); + Ray viewRayLocal = getViewRayLocal(); + Ray viewRayEC = getViewRayEC(); Intersections ix; vec2 screenCoord = (gl_FragCoord.xy - czm_viewport.xy) / czm_viewport.zw; // [0,1] - RayShapeIntersection shapeIntersection = intersectScene(screenCoord, viewRayUv, ix); + RayShapeIntersection shapeIntersection = intersectScene(screenCoord, viewRayLocal, viewRayEC, ix); // Exit early if the scene was completely missed. if (shapeIntersection.entry.w == NO_HIT) { discard; @@ -152,20 +154,17 @@ void main() float currentT = shapeIntersection.entry.w; float endT = shapeIntersection.exit.w; - vec3 positionUv = viewRayUv.pos + currentT * viewRayUv.dir; - PointJacobianT pointJacobian = convertUvToShapeUvSpaceDerivative(positionUv); + + vec3 positionEC = viewRayEC.pos + currentT * viewRayEC.dir; + TileAndUvCoordinate tileAndUv = getTileAndUvCoordinate(positionEC); + vec3 positionLocal = viewRayLocal.pos + currentT * viewRayLocal.dir; + mat3 jacobianT = convertLocalToShapeSpaceDerivative(positionLocal); // Traverse the tree from the start position TraversalData traversalData; SampleData sampleDatas[SAMPLE_COUNT]; - traverseOctreeFromBeginning(pointJacobian.point, traversalData, sampleDatas); - vec4 step = getStepSize(sampleDatas[0], viewRayUv, shapeIntersection, pointJacobian.jacobianT, currentT); - - #if defined(JITTER) - float noise = hash(screenCoord); // [0,1] - currentT += noise * step.w; - positionUv += noise * step.w * viewRayUv.dir; - #endif + traverseOctreeFromBeginning(tileAndUv, traversalData, sampleDatas); + vec4 step = getStepSize(sampleDatas[0], viewRayLocal, shapeIntersection, jacobianT, currentT); FragmentInput fragmentInput; #if defined(STATISTICS) @@ -182,10 +181,11 @@ void main() // Prepare the custom shader inputs copyPropertiesToMetadata(properties, fragmentInput.metadata); - fragmentInput.attributes.positionEC = vec3(u_transformPositionUvToView * vec4(positionUv, 1.0)); - fragmentInput.attributes.normalEC = normalize(czm_normal * step.xyz); + fragmentInput.attributes.positionEC = positionEC; + // Re-normalize normals: some shape intersections may have been scaled to encode positive/negative shapes + fragmentInput.attributes.normalEC = normalize(step.xyz); - fragmentInput.voxel.viewDirUv = viewRayUv.dir; + fragmentInput.voxel.viewDirUv = viewRayLocal.dir; fragmentInput.voxel.travelDistance = step.w; fragmentInput.voxel.stepCount = stepCount; @@ -233,13 +233,15 @@ void main() } #endif } - positionUv = viewRayUv.pos + currentT * viewRayUv.dir; + positionEC = viewRayEC.pos + currentT * viewRayEC.dir; + tileAndUv = getTileAndUvCoordinate(positionEC); + positionLocal = viewRayLocal.pos + currentT * viewRayLocal.dir; + jacobianT = convertLocalToShapeSpaceDerivative(positionLocal); // Traverse the tree from the current ray position. // This is similar to traverseOctreeFromBeginning but is faster when the ray is in the same tile as the previous step. - pointJacobian = convertUvToShapeUvSpaceDerivative(positionUv); - traverseOctreeFromExisting(pointJacobian.point, traversalData, sampleDatas); - step = getStepSize(sampleDatas[0], viewRayUv, shapeIntersection, pointJacobian.jacobianT, currentT); + traverseOctreeFromExisting(tileAndUv, traversalData, sampleDatas); + step = getStepSize(sampleDatas[0], viewRayLocal, shapeIntersection, jacobianT, currentT); } // Convert the alpha from [0,ALPHA_ACCUM_MAX] to [0,1] diff --git a/packages/engine/Source/Shaders/Voxels/VoxelUtils.glsl b/packages/engine/Source/Shaders/Voxels/VoxelUtils.glsl index ca2fee5fba96..f712e1fd4b50 100644 --- a/packages/engine/Source/Shaders/Voxels/VoxelUtils.glsl +++ b/packages/engine/Source/Shaders/Voxels/VoxelUtils.glsl @@ -1,22 +1,8 @@ struct Ray { vec3 pos; vec3 dir; - vec3 rawDir; }; -#if defined(JITTER) -/** - * Generate a pseudo-random value for a given 2D screen coordinate. - * Similar to https://www.shadertoy.com/view/4djSRW with a modified hashscale. - */ -float hash(vec2 p) -{ - vec3 p3 = fract(vec3(p.xyx) * 50.0); - p3 += dot(p3, p3.yzx + 19.19); - return fract((p3.x + p3.y) * p3.z); -} -#endif - float minComponent(in vec3 v) { return min(min(v.x, v.y), v.z); } @@ -24,8 +10,3 @@ float minComponent(in vec3 v) { float maxComponent(in vec3 v) { return max(max(v.x, v.y), v.z); } - -struct PointJacobianT { - vec3 point; - mat3 jacobianT; -}; diff --git a/packages/engine/Source/Shaders/Voxels/convertLocalToBoxUv.glsl b/packages/engine/Source/Shaders/Voxels/convertLocalToBoxUv.glsl new file mode 100644 index 000000000000..d021d46e07ae --- /dev/null +++ b/packages/engine/Source/Shaders/Voxels/convertLocalToBoxUv.glsl @@ -0,0 +1,30 @@ +uniform vec3 u_boxLocalToShapeUvScale; +uniform vec3 u_boxLocalToShapeUvTranslate; + +uniform ivec4 u_cameraTileCoordinates; +uniform vec3 u_cameraTileUv; +uniform mat3 u_boxEcToXyz; + +mat3 convertLocalToShapeSpaceDerivative(in vec3 positionLocal) { + // For BOX, local space = shape space, so the Jacobian is the identity matrix. + return mat3(1.0); +} + +vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) { + return shapeUv / u_boxLocalToShapeUvScale; +} + +vec3 convertEcToDeltaTile(in vec3 positionEC) { + vec3 dPosition = u_boxEcToXyz * positionEC; + return u_boxLocalToShapeUvScale * dPosition * float(1 << u_cameraTileCoordinates.w); +} + +TileAndUvCoordinate getTileAndUvCoordinate(in vec3 positionEC) { + vec3 deltaTileCoordinate = convertEcToDeltaTile(positionEC); + vec3 tileUvSum = u_cameraTileUv + deltaTileCoordinate; + ivec3 tileCoordinate = u_cameraTileCoordinates.xyz + ivec3(floor(tileUvSum)); + tileCoordinate = min(max(ivec3(0), tileCoordinate), ivec3((1 << u_cameraTileCoordinates.w) - 1)); + ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz; + vec3 tileUv = clamp(tileUvSum - vec3(tileCoordinateChange), 0.0, 1.0); + return TileAndUvCoordinate(ivec4(tileCoordinate, u_cameraTileCoordinates.w), tileUv); +} diff --git a/packages/engine/Source/Shaders/Voxels/convertLocalToCylinderUv.glsl b/packages/engine/Source/Shaders/Voxels/convertLocalToCylinderUv.glsl new file mode 100644 index 000000000000..30e1039221a8 --- /dev/null +++ b/packages/engine/Source/Shaders/Voxels/convertLocalToCylinderUv.glsl @@ -0,0 +1,97 @@ +uniform vec2 u_cylinderLocalToShapeUvRadius; // x = scale, y = offset +uniform vec2 u_cylinderLocalToShapeUvHeight; // x = scale, y = offset +uniform vec2 u_cylinderLocalToShapeUvAngle; // x = scale, y = offset +uniform float u_cylinderShapeUvAngleRangeOrigin; +uniform mat3 u_cylinderEcToRadialTangentUp; +uniform ivec4 u_cameraTileCoordinates; +uniform vec3 u_cameraTileUv; +uniform vec3 u_cameraShapePosition; // (radial distance, angle, height) of camera in shape space + +mat3 convertLocalToShapeSpaceDerivative(in vec3 position) { + vec3 radial = normalize(vec3(position.xy, 0.0)); + vec3 z = vec3(0.0, 0.0, 1.0); + vec3 east = normalize(vec3(-position.y, position.x, 0.0)); + return mat3(radial, east / length(position.xy), z); +} + +vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) { + float radius = shapeUv.x / u_cylinderLocalToShapeUvRadius.x; + float angle = shapeUv.y * czm_twoPi / u_cylinderLocalToShapeUvAngle.x; + float height = shapeUv.z / u_cylinderLocalToShapeUvHeight.x; + + return vec3(radius, angle, height); +} + +/** + * Computes the change in polar coordinates given a change in position. + * @param {vec2} dPosition The change in position in Cartesian coordinates. + * @param {float} cameraRadialDistance The radial distance of the camera from the origin. + * @return {vec2} The change in polar coordinates (radial distance, angle). + */ +vec2 computePolarChange(in vec2 dPosition, in float cameraRadialDistance) { + float dAngle = atan(dPosition.y, cameraRadialDistance + dPosition.x); + // Find the direction of the radial axis at the output angle, in Cartesian coordinates + vec2 outputRadialAxis = vec2(cos(dAngle), sin(dAngle)); + float sinHalfAngle = sin(dAngle / 2.0); + float versine = 2.0 * sinHalfAngle * sinHalfAngle; + float dRadial = dot(dPosition, outputRadialAxis) - cameraRadialDistance * versine; + return vec2(dRadial, dAngle); +} + +vec3 convertEcToDeltaShape(in vec3 positionEC) { + // 1. Rotate to radial, tangent, and up coordinates + vec3 rtu = u_cylinderEcToRadialTangentUp * positionEC; + // 2. Compute change in angular and radial coordinates. + vec2 dPolar = computePolarChange(rtu.xy, u_cameraShapePosition.x); + return vec3(dPolar.xy, rtu.z); +} + +vec3 convertEcToDeltaTile(in vec3 positionEC) { + vec3 deltaShape = convertEcToDeltaShape(positionEC); + // Convert to tileset coordinates in [0, 1] + float dx = u_cylinderLocalToShapeUvRadius.x * deltaShape.x; + float dy = deltaShape.y / czm_twoPi; +#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE) + // Wrap to ensure dy is not crossing through the unoccupied angle range, where + // angle to tile coordinate conversions would be more complicated + float cameraUvAngle = (u_cameraShapePosition.y + czm_pi) / czm_twoPi; + float cameraUvAngleShift = fract(cameraUvAngle - u_cylinderShapeUvAngleRangeOrigin); + float rawOutputUvAngle = cameraUvAngleShift + dy; + float rotation = floor(rawOutputUvAngle); + dy -= rotation; +#endif + dy *= u_cylinderLocalToShapeUvAngle.x; + float dz = u_cylinderLocalToShapeUvHeight.x * deltaShape.z; + // Convert to tile coordinate changes + return vec3(dx, dy, dz) * float(1 << u_cameraTileCoordinates.w); +} + +TileAndUvCoordinate getTileAndUvCoordinate(in vec3 positionEC) { + vec3 deltaTileCoordinate = convertEcToDeltaTile(positionEC); + vec3 tileUvSum = u_cameraTileUv + deltaTileCoordinate; + ivec3 tileCoordinate = u_cameraTileCoordinates.xyz + ivec3(floor(tileUvSum)); + int maxTileCoordinate = (1 << u_cameraTileCoordinates.w) - 1; + tileCoordinate.x = min(max(0, tileCoordinate.x), maxTileCoordinate); + tileCoordinate.z = min(max(0, tileCoordinate.z), maxTileCoordinate); +#if (!defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE)) + ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz; + if (tileCoordinate.y < 0) { + tileCoordinate.y += (maxTileCoordinate + 1); + } else if (tileCoordinate.y > maxTileCoordinate) { + tileCoordinate.y -= (maxTileCoordinate + 1); + } +#else + tileCoordinate.y = min(max(0, tileCoordinate.y), maxTileCoordinate); + ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz; +#endif + vec3 tileUv = tileUvSum - vec3(tileCoordinateChange); + tileUv.x = clamp(tileUv.x, 0.0, 1.0); +#if (!defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE)) + // If there is only one tile spanning 2*PI angle, the coordinate wraps around + tileUv.y = (u_cameraTileCoordinates.w == 0) ? fract(tileUv.y) : clamp(tileUv.y, 0.0, 1.0); +#else + tileUv.y = clamp(tileUv.y, 0.0, 1.0); +#endif + tileUv.z = clamp(tileUv.z, 0.0, 1.0); + return TileAndUvCoordinate(ivec4(tileCoordinate, u_cameraTileCoordinates.w), tileUv); +} diff --git a/packages/engine/Source/Shaders/Voxels/convertLocalToEllipsoidUv.glsl b/packages/engine/Source/Shaders/Voxels/convertLocalToEllipsoidUv.glsl new file mode 100644 index 000000000000..8052c6f354c5 --- /dev/null +++ b/packages/engine/Source/Shaders/Voxels/convertLocalToEllipsoidUv.glsl @@ -0,0 +1,193 @@ +/* Ellipsoid defines (set in Scene/VoxelEllipsoidShape.js) +#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY +#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY +#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE +#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED +#define ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE +*/ + +uniform vec3 u_cameraPositionCartographic; // (longitude, latitude, height) in radians and meters +uniform vec2 u_ellipsoidCurvatureAtLatitude; +uniform mat3 u_ellipsoidEcToEastNorthUp; +uniform vec3 u_ellipsoidRadii; +uniform vec2 u_evoluteScale; // (radii.x ^ 2 - radii.z ^ 2) * vec2(1.0, -1.0) / radii; +uniform vec3 u_ellipsoidInverseRadiiSquared; +#if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY) || defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY) || defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED) + uniform vec3 u_ellipsoidShapeUvLongitudeMinMaxMid; +#endif +#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE) + uniform vec2 u_ellipsoidLocalToShapeUvLongitude; // x = scale, y = offset + uniform float u_ellipsoidShapeUvLongitudeRangeOrigin; +#endif +#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE) + uniform vec2 u_ellipsoidLocalToShapeUvLatitude; // x = scale, y = offset +#endif +uniform float u_ellipsoidInverseHeightDifference; + +uniform ivec4 u_cameraTileCoordinates; +uniform vec3 u_cameraTileUv; + +// robust iterative solution without trig functions +// https://github.com/0xfaded/ellipse_demo/issues/1 +// https://stackoverflow.com/questions/22959698/distance-from-given-point-to-given-ellipse +// Extended to return radius of curvature along with the point +vec3 nearestPointAndRadiusOnEllipse(vec2 pos, vec2 radii) { + vec2 p = abs(pos); + vec2 inverseRadii = 1.0 / radii; + + // We describe the ellipse parametrically: v = radii * vec2(cos(t), sin(t)) + // but store the cos and sin of t in a vec2 for efficiency. + // Initial guess: t = pi/4 + vec2 tTrigs = vec2(0.7071067811865476); + // Initial guess of point on ellipsoid + vec2 v = radii * tTrigs; + // Center of curvature of the ellipse at v + vec2 evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs; + + const int iterations = 3; + for (int i = 0; i < iterations; ++i) { + // Find the (approximate) intersection of p - evolute with the ellipsoid. + vec2 q = normalize(p - evolute) * length(v - evolute); + // Update the estimate of t. + tTrigs = (q + evolute) * inverseRadii; + tTrigs = normalize(clamp(tTrigs, 0.0, 1.0)); + v = radii * tTrigs; + evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs; + } + + return vec3(v * sign(pos), length(v - evolute)); +} + +mat3 convertLocalToShapeSpaceDerivative(in vec3 position) { + vec3 east = normalize(vec3(-position.y, position.x, 0.0)); + + // Convert the 3D position to a 2D position relative to the ellipse (radii.x, radii.z) + // (assume radii.y == radii.x) and find the nearest point on the ellipse and its normal + float distanceFromZAxis = length(position.xy); + vec2 posEllipse = vec2(distanceFromZAxis, position.z); + vec3 surfacePointAndRadius = nearestPointAndRadiusOnEllipse(posEllipse, u_ellipsoidRadii.xz); + vec2 surfacePoint = surfacePointAndRadius.xy; + + vec2 normal2d = normalize(surfacePoint * u_ellipsoidInverseRadiiSquared.xz); + vec3 north = vec3(-normal2d.y * normalize(position.xy), abs(normal2d.x)); + + float heightSign = length(posEllipse) < length(surfacePoint) ? -1.0 : 1.0; + float height = heightSign * length(posEllipse - surfacePoint); + vec3 up = normalize(cross(east, north)); + + return mat3(east / distanceFromZAxis, north / (surfacePointAndRadius.z + height), up); +} + +vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) { + // Convert from [0, 1] to radians [-pi, pi] + float longitude = shapeUv.x * czm_twoPi; + #if defined (ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE) + longitude /= u_ellipsoidLocalToShapeUvLongitude.x; + #endif + + // Convert from [0, 1] to radians [-pi/2, pi/2] + float latitude = shapeUv.y * czm_pi; + #if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE) + latitude /= u_ellipsoidLocalToShapeUvLatitude.x; + #endif + + float height = shapeUv.z / u_ellipsoidInverseHeightDifference; + + return vec3(longitude, latitude, height); +} + +vec3 convertEcToDeltaShape(in vec3 positionEC) { + vec3 enu = u_ellipsoidEcToEastNorthUp * positionEC; + + // 1. Compute the change in longitude from the camera to the ENU point + // First project the camera and ENU positions to the equatorial XY plane, + // positioning the camera on the +x axis, so that enu.x projects along the +y axis + float cosLatitude = cos(u_cameraPositionCartographic.y); + float sinLatitude = sin(u_cameraPositionCartographic.y); + float primeVerticalRadius = 1.0 / u_ellipsoidCurvatureAtLatitude.x; + vec2 cameraXY = vec2((primeVerticalRadius + u_cameraPositionCartographic.z) * cosLatitude, 0.0); + // Note precision loss in positionXY.x if length(enu) << length(cameraXY) + vec2 positionXY = cameraXY + vec2(-enu.y * sinLatitude + enu.z * cosLatitude, enu.x); + float dLongitude = atan(positionXY.y, positionXY.x); + + // 2. Find the longitude component of positionXY, by rotating about Z until the y component is zero. + // Use the versine to compute the change in x directly from the change in angle: + // versine(angle) = 2 * sin^2(angle/2) + float sinHalfLongitude = sin(dLongitude / 2.0); + float dx = length(positionXY) * 2.0 * sinHalfLongitude * sinHalfLongitude; + // Rotate longitude component back to ENU North and Up, and remove from enu + enu += vec3(-enu.x, -dx * sinLatitude, dx * cosLatitude); + + // 3. Compute the change in latitude from the camera to the ENU point. + // First project the camera and ENU positions to the meridional ZX plane, + // positioning the camera on the +Z axis, so that enu.y maps to the +X axis. + float meridionalRadius = 1.0 / u_ellipsoidCurvatureAtLatitude.y; + vec2 cameraZX = vec2(meridionalRadius + u_cameraPositionCartographic.z, 0.0); + vec2 positionZX = cameraZX + vec2(enu.z, enu.y); + float dLatitude = atan(positionZX.y, positionZX.x); + + // 4. Compute the change in height above the ellipsoid + // Find the change in enu.z associated with rotating the point to the latitude of the camera + float sinHalfLatitude = sin(dLatitude / 2.0); + float dz = length(positionZX) * 2.0 * sinHalfLatitude * sinHalfLatitude; + // The remaining change in enu.z is the change in height above the ellipsoid + float dHeight = enu.z + dz; + + return vec3(dLongitude, dLatitude, dHeight); +} + +vec3 convertEcToDeltaTile(in vec3 positionEC) { + vec3 deltaShape = convertEcToDeltaShape(positionEC); + // Convert to tileset coordinates in [0, 1] + float dx = deltaShape.x / czm_twoPi; + +#if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)) + // Wrap to ensure dx is not crossing through the unoccupied angle range, where + // angle to tile coordinate conversions would be more complicated + float cameraUvLongitude = (u_cameraPositionCartographic.x + czm_pi) / czm_twoPi; + float cameraUvLongitudeShift = fract(cameraUvLongitude - u_ellipsoidShapeUvLongitudeRangeOrigin); + float rawOutputUvLongitude = cameraUvLongitudeShift + dx; + float rotation = floor(rawOutputUvLongitude); + dx -= rotation; + dx *= u_ellipsoidLocalToShapeUvLongitude.x; +#endif + + float dy = deltaShape.y / czm_pi; +#if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)) + dy *= u_ellipsoidLocalToShapeUvLatitude.x; +#endif + + float dz = u_ellipsoidInverseHeightDifference * deltaShape.z; + // Convert to tile coordinate changes + return vec3(dx, dy, dz) * float(1 << u_cameraTileCoordinates.w); +} + +TileAndUvCoordinate getTileAndUvCoordinate(in vec3 positionEC) { + vec3 deltaTileCoordinate = convertEcToDeltaTile(positionEC); + vec3 tileUvSum = u_cameraTileUv + deltaTileCoordinate; + ivec3 tileCoordinate = u_cameraTileCoordinates.xyz + ivec3(floor(tileUvSum)); + int maxTileCoordinate = (1 << u_cameraTileCoordinates.w) - 1; + tileCoordinate.y = min(max(0, tileCoordinate.y), maxTileCoordinate); + tileCoordinate.z = min(max(0, tileCoordinate.z), maxTileCoordinate); +#if (!defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)) + ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz; + if (tileCoordinate.x < 0) { + tileCoordinate.x += (maxTileCoordinate + 1); + } else if (tileCoordinate.x > maxTileCoordinate) { + tileCoordinate.x -= (maxTileCoordinate + 1); + } +#else + tileCoordinate.x = min(max(0, tileCoordinate.x), maxTileCoordinate); + ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz; +#endif + vec3 tileUv = tileUvSum - vec3(tileCoordinateChange); +#if (!defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)) + // If there is only one tile spanning 2*PI angle, the coordinate wraps around + tileUv.x = (u_cameraTileCoordinates.w == 0) ? fract(tileUv.x) : clamp(tileUv.x, 0.0, 1.0); +#else + tileUv.x = clamp(tileUv.x, 0.0, 1.0); +#endif + tileUv.y = clamp(tileUv.y, 0.0, 1.0); + tileUv.z = clamp(tileUv.z, 0.0, 1.0); + return TileAndUvCoordinate(ivec4(tileCoordinate, u_cameraTileCoordinates.w), tileUv); +} diff --git a/packages/engine/Source/Shaders/Voxels/convertUvToBox.glsl b/packages/engine/Source/Shaders/Voxels/convertUvToBox.glsl deleted file mode 100644 index e5b078bce28b..000000000000 --- a/packages/engine/Source/Shaders/Voxels/convertUvToBox.glsl +++ /dev/null @@ -1,45 +0,0 @@ -/* Box defines (set in Scene/VoxelBoxShape.js) -#define BOX_HAS_SHAPE_BOUNDS -*/ - -#if defined(BOX_HAS_SHAPE_BOUNDS) - uniform vec3 u_boxUvToShapeUvScale; - uniform vec3 u_boxUvToShapeUvTranslate; -#endif - -PointJacobianT convertUvToShapeSpaceDerivative(in vec3 positionUv) { - // For BOX, UV space = shape space, so we can use positionUv as-is, - // and the Jacobian is the identity matrix, except that a step of 1 - // only spans half the shape space [-1, 1], so the identity is scaled. - return PointJacobianT(positionUv, mat3(0.5)); -} - -vec3 convertShapeToShapeUvSpace(in vec3 positionShape) { -#if defined(BOX_HAS_SHAPE_BOUNDS) - return positionShape * u_boxUvToShapeUvScale + u_boxUvToShapeUvTranslate; -#else - return positionShape; -#endif -} - -PointJacobianT convertUvToShapeUvSpaceDerivative(in vec3 positionUv) { - PointJacobianT pointJacobian = convertUvToShapeSpaceDerivative(positionUv); - pointJacobian.point = convertShapeToShapeUvSpace(pointJacobian.point); - return pointJacobian; -} - -vec3 convertShapeUvToUvSpace(in vec3 shapeUv) { -#if defined(BOX_HAS_SHAPE_BOUNDS) - return (shapeUv - u_boxUvToShapeUvTranslate) / u_boxUvToShapeUvScale; -#else - return shapeUv; -#endif -} - -vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) { -#if defined(BOX_HAS_SHAPE_BOUNDS) - return shapeUv / u_boxUvToShapeUvScale; -#else - return shapeUv; -#endif -} \ No newline at end of file diff --git a/packages/engine/Source/Shaders/Voxels/convertUvToCylinder.glsl b/packages/engine/Source/Shaders/Voxels/convertUvToCylinder.glsl deleted file mode 100644 index 9a725785379b..000000000000 --- a/packages/engine/Source/Shaders/Voxels/convertUvToCylinder.glsl +++ /dev/null @@ -1,99 +0,0 @@ -/* Cylinder defines (set in Scene/VoxelCylinderShape.js) -#define CYLINDER_HAS_SHAPE_BOUNDS_RADIUS -#define CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT -#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE -#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY -#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY -#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED -*/ - -#if defined(CYLINDER_HAS_SHAPE_BOUNDS_RADIUS) - uniform vec2 u_cylinderUvToShapeUvRadius; // x = scale, y = offset -#endif -#if defined(CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT) - uniform vec2 u_cylinderUvToShapeUvHeight; // x = scale, y = offset -#endif -#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE) - uniform vec2 u_cylinderUvToShapeUvAngle; // x = scale, y = offset -#endif -#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY) || defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY) - uniform vec2 u_cylinderShapeUvAngleMinMax; -#endif -#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY) || defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY) || defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED) - uniform float u_cylinderShapeUvAngleRangeZeroMid; -#endif - -PointJacobianT convertUvToShapeSpaceDerivative(in vec3 positionUv) { - // Convert from Cartesian UV space [0, 1] to Cartesian local space [-1, 1] - vec3 position = positionUv * 2.0 - 1.0; - - float radius = length(position.xy); // [0, 1] - vec3 radial = normalize(vec3(position.xy, 0.0)); - - // Shape space height is defined within [0, 1] - float height = positionUv.z; // [0, 1] - vec3 z = vec3(0.0, 0.0, 1.0); - - float angle = atan(position.y, position.x); - vec3 east = normalize(vec3(-position.y, position.x, 0.0)); - - vec3 point = vec3(radius, angle, height); - mat3 jacobianT = mat3(radial, east / length(position.xy), z); - return PointJacobianT(point, jacobianT); -} - -vec3 convertShapeToShapeUvSpace(in vec3 positionShape) { - float radius = positionShape.x; - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_RADIUS) - radius = radius * u_cylinderUvToShapeUvRadius.x + u_cylinderUvToShapeUvRadius.y; - #endif - - float angle = (positionShape.y + czm_pi) / czm_twoPi; - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE) - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED) - // Comparing against u_cylinderShapeUvAngleMinMax has precision problems. u_cylinderShapeUvAngleRangeZeroMid is more conservative. - angle += float(angle < u_cylinderShapeUvAngleRangeZeroMid); - #endif - - // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY) - angle = angle > u_cylinderShapeUvAngleRangeZeroMid ? u_cylinderShapeUvAngleMinMax.x : angle; - #elif defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY) - angle = angle < u_cylinderShapeUvAngleRangeZeroMid ? u_cylinderShapeUvAngleMinMax.y : angle; - #endif - - angle = angle * u_cylinderUvToShapeUvAngle.x + u_cylinderUvToShapeUvAngle.y; - #endif - - float height = positionShape.z; - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT) - height = height * u_cylinderUvToShapeUvHeight.x + u_cylinderUvToShapeUvHeight.y; - #endif - - return vec3(radius, angle, height); -} - -PointJacobianT convertUvToShapeUvSpaceDerivative(in vec3 positionUv) { - PointJacobianT pointJacobian = convertUvToShapeSpaceDerivative(positionUv); - pointJacobian.point = convertShapeToShapeUvSpace(pointJacobian.point); - return pointJacobian; -} - -vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) { - float radius = shapeUv.x; - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_RADIUS) - radius /= u_cylinderUvToShapeUvRadius.x; - #endif - - float angle = shapeUv.y * czm_twoPi; - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE) - angle /= u_cylinderUvToShapeUvAngle.x; - #endif - - float height = shapeUv.z; - #if defined(CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT) - height /= u_cylinderUvToShapeUvHeight.x; - #endif - - return vec3(radius, angle, height); -} diff --git a/packages/engine/Source/Shaders/Voxels/convertUvToEllipsoid.glsl b/packages/engine/Source/Shaders/Voxels/convertUvToEllipsoid.glsl deleted file mode 100644 index bdacd1778d86..000000000000 --- a/packages/engine/Source/Shaders/Voxels/convertUvToEllipsoid.glsl +++ /dev/null @@ -1,139 +0,0 @@ -/* Ellipsoid defines (set in Scene/VoxelEllipsoidShape.js) -#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY -#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY -#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE -#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED -#define ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE -*/ - -uniform vec3 u_ellipsoidRadiiUv; // [0,1] -uniform vec2 u_evoluteScale; // (radiiUv.x ^ 2 - radiiUv.z ^ 2) * vec2(1.0, -1.0) / radiiUv; -uniform vec3 u_ellipsoidInverseRadiiSquaredUv; -#if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY) || defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY) || defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED) - uniform vec3 u_ellipsoidShapeUvLongitudeMinMaxMid; -#endif -#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE) - uniform vec2 u_ellipsoidUvToShapeUvLongitude; // x = scale, y = offset -#endif -#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE) - uniform vec2 u_ellipsoidUvToShapeUvLatitude; // x = scale, y = offset -#endif -uniform float u_ellipsoidInverseHeightDifferenceUv; - -// robust iterative solution without trig functions -// https://github.com/0xfaded/ellipse_demo/issues/1 -// https://stackoverflow.com/questions/22959698/distance-from-given-point-to-given-ellipse -// Extended to return radius of curvature along with the point -vec3 nearestPointAndRadiusOnEllipse(vec2 pos, vec2 radii) { - vec2 p = abs(pos); - vec2 inverseRadii = 1.0 / radii; - - // We describe the ellipse parametrically: v = radii * vec2(cos(t), sin(t)) - // but store the cos and sin of t in a vec2 for efficiency. - // Initial guess: t = pi/4 - vec2 tTrigs = vec2(0.7071067811865476); - // Initial guess of point on ellipsoid - vec2 v = radii * tTrigs; - // Center of curvature of the ellipse at v - vec2 evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs; - - const int iterations = 3; - for (int i = 0; i < iterations; ++i) { - // Find the (approximate) intersection of p - evolute with the ellipsoid. - vec2 q = normalize(p - evolute) * length(v - evolute); - // Update the estimate of t. - tTrigs = (q + evolute) * inverseRadii; - tTrigs = normalize(clamp(tTrigs, 0.0, 1.0)); - v = radii * tTrigs; - evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs; - } - - return vec3(v * sign(pos), length(v - evolute)); -} - -PointJacobianT convertUvToShapeSpaceDerivative(in vec3 positionUv) { - // Convert from UV space [0, 1] to local space [-1, 1] - vec3 position = positionUv * 2.0 - 1.0; - // Undo the scaling from ellipsoid to sphere - position = position * u_ellipsoidRadiiUv; - - float longitude = atan(position.y, position.x); - vec3 east = normalize(vec3(-position.y, position.x, 0.0)); - - // Convert the 3D position to a 2D position relative to the ellipse (radii.x, radii.z) - // (assume radii.y == radii.x) and find the nearest point on the ellipse and its normal - float distanceFromZAxis = length(position.xy); - vec2 posEllipse = vec2(distanceFromZAxis, position.z); - vec3 surfacePointAndRadius = nearestPointAndRadiusOnEllipse(posEllipse, u_ellipsoidRadiiUv.xz); - vec2 surfacePoint = surfacePointAndRadius.xy; - - vec2 normal2d = normalize(surfacePoint * u_ellipsoidInverseRadiiSquaredUv.xz); - float latitude = atan(normal2d.y, normal2d.x); - vec3 north = vec3(-normal2d.y * normalize(position.xy), abs(normal2d.x)); - - float heightSign = length(posEllipse) < length(surfacePoint) ? -1.0 : 1.0; - float height = heightSign * length(posEllipse - surfacePoint); - vec3 up = normalize(cross(east, north)); - - vec3 point = vec3(longitude, latitude, height); - mat3 jacobianT = mat3(east / distanceFromZAxis, north / (surfacePointAndRadius.z + height), up); - return PointJacobianT(point, jacobianT); -} - -vec3 convertShapeToShapeUvSpace(in vec3 positionShape) { - // Longitude: shift & scale to [0, 1] - float longitude = (positionShape.x + czm_pi) / czm_twoPi; - - // Correct the angle when max < min - // Technically this should compare against min longitude - but it has precision problems so compare against the middle of empty space. - #if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED) - longitude += float(longitude < u_ellipsoidShapeUvLongitudeMinMaxMid.z); - #endif - - // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. - #if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY) - longitude = longitude > u_ellipsoidShapeUvLongitudeMinMaxMid.z ? u_ellipsoidShapeUvLongitudeMinMaxMid.x : longitude; - #endif - #if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY) - longitude = longitude < u_ellipsoidShapeUvLongitudeMinMaxMid.z ? u_ellipsoidShapeUvLongitudeMinMaxMid.y : longitude; - #endif - - #if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE) - longitude = longitude * u_ellipsoidUvToShapeUvLongitude.x + u_ellipsoidUvToShapeUvLongitude.y; - #endif - - // Latitude: shift and scale to [0, 1] - float latitude = (positionShape.y + czm_piOverTwo) / czm_pi; - #if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE) - latitude = latitude * u_ellipsoidUvToShapeUvLatitude.x + u_ellipsoidUvToShapeUvLatitude.y; - #endif - - // Height: scale to the range [0, 1] - float height = 1.0 + positionShape.z * u_ellipsoidInverseHeightDifferenceUv; - - return vec3(longitude, latitude, height); -} - -PointJacobianT convertUvToShapeUvSpaceDerivative(in vec3 positionUv) { - PointJacobianT pointJacobian = convertUvToShapeSpaceDerivative(positionUv); - pointJacobian.point = convertShapeToShapeUvSpace(pointJacobian.point); - return pointJacobian; -} - -vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) { - // Convert from [0, 1] to radians [-pi, pi] - float longitude = shapeUv.x * czm_twoPi; - #if defined (ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE) - longitude /= u_ellipsoidUvToShapeUvLongitude.x; - #endif - - // Convert from [0, 1] to radians [-pi/2, pi/2] - float latitude = shapeUv.y * czm_pi; - #if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE) - latitude /= u_ellipsoidUvToShapeUvLatitude.x; - #endif - - float height = shapeUv.z / u_ellipsoidInverseHeightDifferenceUv; - - return vec3(longitude, latitude, height); -} diff --git a/packages/engine/Specs/Scene/VoxelEllipsoidShapeSpec.js b/packages/engine/Specs/Scene/VoxelEllipsoidShapeSpec.js index bfd80ac66e58..5f6e21457784 100644 --- a/packages/engine/Specs/Scene/VoxelEllipsoidShapeSpec.js +++ b/packages/engine/Specs/Scene/VoxelEllipsoidShapeSpec.js @@ -90,17 +90,17 @@ describe("Scene/VoxelEllipsoidShape", function () { ).toEqualEpsilon(expectedOrientedBoundingBox.center, CesiumMath.EPSILON12); const expectedShapeTransform = Matrix4.fromRowMajorArray([ - (scale.x + maxHeight) * Math.cos(angle), - -(scale.x + maxHeight) * Math.sin(angle), + Math.cos(angle), + -Math.sin(angle), 0.0, expectedOrientedBoundingBox.center.x, - (scale.y + maxHeight) * Math.sin(angle), - (scale.y + maxHeight) * Math.cos(angle), + Math.sin(angle), + Math.cos(angle), 0.0, expectedOrientedBoundingBox.center.y, 0.0, 0.0, - scale.z + maxHeight, + 1.0, expectedOrientedBoundingBox.center.z, 0.0, 0.0, diff --git a/packages/engine/Specs/Scene/buildVoxelDrawCommandsSpec.js b/packages/engine/Specs/Scene/buildVoxelDrawCommandsSpec.js index d1aea9c50b59..47ae2b39250a 100644 --- a/packages/engine/Specs/Scene/buildVoxelDrawCommandsSpec.js +++ b/packages/engine/Specs/Scene/buildVoxelDrawCommandsSpec.js @@ -52,7 +52,7 @@ describe("Scene/buildVoxelDrawCommands", function () { const { shaderProgram } = primitive._drawCommand; const fragmentShaderText = shaderProgram._fragmentShaderText; const clippingFunctionSignature = - "vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)"; + "vec4 getClippingPlane(highp sampler2D packedPlanes, int planeNumber)"; expect(fragmentShaderText.includes(clippingFunctionSignature)).toBe(true); }); diff --git a/packages/sandcastle/gallery/voxel-picking/main.js b/packages/sandcastle/gallery/voxel-picking/main.js index f9d1d584d9a5..f2c643ffdb2e 100644 --- a/packages/sandcastle/gallery/voxel-picking/main.js +++ b/packages/sandcastle/gallery/voxel-picking/main.js @@ -35,16 +35,16 @@ function ProceduralMultiTileVoxelProvider(shape) { this.names = ["color"]; this.types = [Cesium.MetadataType.VEC4]; this.componentTypes = [Cesium.MetadataComponentType.FLOAT32]; - this._levelCount = 3; + this.availableLevels = 3; this.globalTransform = globalTransform; } ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) { const { tileLevel, tileX, tileY, tileZ } = options; - if (tileLevel >= this._levelCount) { + if (tileLevel >= this.availableLevels) { return Promise.reject( - `No tiles available beyond level ${this._levelCount}`, + `No tiles available beyond level ${this.availableLevels - 1}`, ); } @@ -129,6 +129,7 @@ function createPrimitive(provider) { customShader: customShader, }); voxelPrimitive.nearestSampling = true; + voxelPrimitive.stepSize = 0.7; viewer.scene.primitives.add(voxelPrimitive); camera.flyToBoundingSphere(voxelPrimitive.boundingSphere, { diff --git a/packages/sandcastle/gallery/voxels/main.js b/packages/sandcastle/gallery/voxels/main.js index 283579416b28..3b88480f81b1 100644 --- a/packages/sandcastle/gallery/voxels/main.js +++ b/packages/sandcastle/gallery/voxels/main.js @@ -92,14 +92,14 @@ function ProceduralMultiTileVoxelProvider(shape) { this.componentTypes = [Cesium.MetadataComponentType.FLOAT32]; this.globalTransform = globalTransform; - this._levelCount = 2; - this._allVoxelData = new Array(this._levelCount); + this.availableLevels = 2; + this._allVoxelData = new Array(this.availableLevels); const allVoxelData = this._allVoxelData; const channelCount = Cesium.MetadataType.getComponentCount(this.types[0]); const { dimensions } = this; - for (let level = 0; level < this._levelCount; level++) { + for (let level = 0; level < this.availableLevels; level++) { const dimAtLevel = Math.pow(2, level); const voxelCountX = dimensions.x * dimAtLevel; const voxelCountY = dimensions.y * dimAtLevel; @@ -127,9 +127,9 @@ function ProceduralMultiTileVoxelProvider(shape) { ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) { const { tileLevel, tileX, tileY, tileZ } = options; - if (tileLevel >= this._levelCount) { + if (tileLevel >= this.availableLevels) { return Promise.reject( - `No tiles available beyond level ${this._levelCount - 1}`, + `No tiles available beyond level ${this.availableLevels - 1}`, ); }