diff --git a/Apps/Sandcastle/gallery/3D Tiles Point Cloud Shading.html b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Shading.html new file mode 100644 index 000000000000..0ab4b44b86bc --- /dev/null +++ b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Shading.html @@ -0,0 +1,275 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Maximum Screen Space Error + + +
Attenuation
Geometric Error Scale + + +
Maximum Attenuation + + +
Base Resolution + + +
Eye Dome Lighting
Eye Dome Lighting Strength + + +
Eye Dome Lighting Radius + + +
+
+ + + diff --git a/Apps/Sandcastle/gallery/3D Tiles Point Cloud Shading.jpg b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Shading.jpg new file mode 100644 index 000000000000..3ba34dc741ff Binary files /dev/null and b/Apps/Sandcastle/gallery/3D Tiles Point Cloud Shading.jpg differ diff --git a/CHANGES.md b/CHANGES.md index e968d7ef12f4..0e93c3edb779 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,8 @@ Change Log * Fixed `Camera.moveStart` and `Camera.moveEnd` events not being raised when camera is close to the ground. [#4753](https://github.com/AnalyticalGraphicsInc/cesium/issues/4753) * Fixed discrepancy between default value used and commented value for default value for halfAxes of OrientedBoundingBox. [#6147](https://github.com/AnalyticalGraphicsInc/cesium/pull/6147) * Added `Cartographic.toCartesian` to convert from Cartographic to Cartesian3. [#6163](https://github.com/AnalyticalGraphicsInc/cesium/pull/6163) +* Added geometric-error-based point cloud attenuation and eye dome lighting for point clouds using replacement refinement. [#6069](https://github.com/AnalyticalGraphicsInc/cesium/pull/6069) +* Added `BoundingSphere.volume` for computing the volume of a `BoundingSphere`. [#6069](https://github.com/AnalyticalGraphicsInc/cesium/pull/6069) ### 1.41 - 2018-01-02 diff --git a/Source/Core/BoundingSphere.js b/Source/Core/BoundingSphere.js index 7372c40134be..a1549999b480 100644 --- a/Source/Core/BoundingSphere.js +++ b/Source/Core/BoundingSphere.js @@ -1,6 +1,7 @@ define([ './Cartesian3', './Cartographic', + './Math', './Check', './defaultValue', './defined', @@ -14,6 +15,7 @@ define([ ], function( Cartesian3, Cartographic, + CesiumMath, Check, defaultValue, defined, @@ -66,6 +68,7 @@ define([ var fromPointsMinBoxPt = new Cartesian3(); var fromPointsMaxBoxPt = new Cartesian3(); var fromPointsNaiveCenterScratch = new Cartesian3(); + var volumeConstant = (4.0 / 3.0) * CesiumMath.PI; /** * Computes a tight-fitting bounding sphere enclosing a list of 3D Cartesian points. @@ -1302,5 +1305,14 @@ define([ return BoundingSphere.clone(this, result); }; + /** + * Computes the radius of the BoundingSphere. + * @returns {Number} The radius of the BoundingSphere. + */ + BoundingSphere.prototype.volume = function() { + var radius = this.radius; + return volumeConstant * radius * radius * radius; + }; + return BoundingSphere; }); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 8be43d98e100..b67e5ad02032 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -36,6 +36,8 @@ define([ './Cesium3DTileStyleEngine', './ClassificationType', './LabelCollection', + './PointCloudShading', + './PointCloudEyeDomeLighting', './SceneMode', './ShadowMode', './TileBoundingRegion', @@ -79,6 +81,8 @@ define([ Cesium3DTileStyleEngine, ClassificationType, LabelCollection, + PointCloudShading, + PointCloudEyeDomeLighting, SceneMode, ShadowMode, TileBoundingRegion, @@ -124,6 +128,7 @@ define([ * @param {Boolean} [options.debugShowRenderingStatistics=false] For debugging only. When true, draws labels to indicate the number of commands, points, triangles and features for each tile. * @param {Boolean} [options.debugShowMemoryUsage=false] For debugging only. When true, draws labels to indicate the texture and geometry memory in megabytes used by each tile. * @param {Boolean} [options.debugShowUrl=false] For debugging only. When true, draws labels to indicate the url of each tile. + * @param {Object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting. * * @exception {DeveloperError} The tileset must be 3D Tiles version 0.0 or 1.0. See {@link https://github.com/AnalyticalGraphicsInc/3d-tiles#spec-status} * @@ -334,6 +339,14 @@ define([ */ this.colorBlendAmount = 0.5; + /** + * Options for controlling point size based on geometric error and eye dome lighting. + * @type {PointCloudShading} + */ + this.pointCloudShading = new PointCloudShading(options.pointCloudShading); + + this._pointCloudEyeDomeLighting = new PointCloudEyeDomeLighting(); + /** * The event fired to indicate progress of loading new tiles. This event is fired when a new tile * is requested, when a requested tile is finished downloading, and when a downloaded tile has been @@ -1632,6 +1645,7 @@ define([ } } var lengthAfterUpdate = commandList.length; + var addedCommandsLength = lengthAfterUpdate - lengthBeforeUpdate; tileset._backfaceCommands.trim(); @@ -1661,7 +1675,6 @@ define([ */ var backfaceCommands = tileset._backfaceCommands.values; - var addedCommandsLength = (lengthAfterUpdate - lengthBeforeUpdate); var backfaceCommandsLength = backfaceCommands.length; commandList.length += backfaceCommandsLength; @@ -1680,6 +1693,13 @@ define([ // Number of commands added by each update above statistics.numberOfCommands = (commandList.length - numberOfInitialCommands); + // Only run EDL if simple attenuation is on + if (tileset.pointCloudShading.attenuation && + tileset.pointCloudShading.eyeDomeLighting && + (addedCommandsLength > 0)) { + tileset._pointCloudEyeDomeLighting.update(frameState, numberOfInitialCommands, tileset); + } + if (tileset.debugShowGeometricError || tileset.debugShowRenderingStatistics || tileset.debugShowMemoryUsage || tileset.debugShowUrl) { if (!defined(tileset._tileDebugLabels)) { tileset._tileDebugLabels = new LabelCollection(); diff --git a/Source/Scene/PointCloud3DTileContent.js b/Source/Scene/PointCloud3DTileContent.js index 587ad19dbe3e..653d1c0e7c1b 100644 --- a/Source/Scene/PointCloud3DTileContent.js +++ b/Source/Scene/PointCloud3DTileContent.js @@ -16,6 +16,7 @@ define([ '../Core/Matrix3', '../Core/Matrix4', '../Core/oneTimeWarning', + '../Core/OrthographicFrustum', '../Core/Plane', '../Core/PrimitiveType', '../Core/RuntimeError', @@ -53,6 +54,7 @@ define([ Matrix3, Matrix4, oneTimeWarning, + OrthographicFrustum, Plane, PrimitiveType, RuntimeError, @@ -152,6 +154,13 @@ define([ */ this.featurePropertiesDirty = false; + // Options for geometric error based attenuation + this._attenuation = false; + this._geometricErrorScale = undefined; + this._maximumAttenuation = undefined; + this._baseResolution = undefined; + this._baseResolutionApproximation = undefined; + initialize(this, arrayBuffer, byteOffset); } @@ -450,9 +459,17 @@ define([ content._hasColors = defined(colors); content._hasNormals = defined(normals); content._hasBatchIds = defined(batchIds); + + // Compute an approximation for base resolution in case it isn't given. + // Assume a uniform distribution of points in cubical cells throughout the + // bounding sphere around the tile. + // Typical use case is leaves, where lower estimates of interpoint distance might + // lead to underattenuation. + var sphereVolume = content._tile.contentBoundingVolume.boundingSphere.volume(); + content._baseResolutionApproximation = Math.cbrt(sphereVolume / pointsLength); } - var scratchPointSizeAndTilesetTime = new Cartesian2(); + var scratchPointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier = new Cartesian4(); var positionLocation = 0; var colorLocation = 1; @@ -524,10 +541,30 @@ define([ } var uniformMap = { - u_pointSizeAndTilesetTime : function() { - scratchPointSizeAndTilesetTime.x = content._pointSize; - scratchPointSizeAndTilesetTime.y = content._tileset.timeSinceLoad; - return scratchPointSizeAndTilesetTime; + u_pointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier : function() { + var scratch = scratchPointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier; + scratch.x = content._attenuation ? content._maximumAttenuation : content._pointSize; + scratch.y = content._tileset.timeSinceLoad; + + if (content._attenuation) { + var geometricError = content.tile.geometricError; + if (geometricError === 0) { + geometricError = defined(content._baseResolution) ? content._baseResolution : content._baseResolutionApproximation; + } + var frustum = frameState.camera.frustum; + var depthMultiplier; + // Attenuation is maximumAttenuation in 2D/ortho + if (frameState.mode === SceneMode.SCENE2D || frustum instanceof OrthographicFrustum) { + depthMultiplier = Number.POSITIVE_INFINITY; + } else { + depthMultiplier = context.drawingBufferHeight / frameState.camera.frustum.sseDenominator; + } + + scratch.z = geometricError * content._geometricErrorScale; + scratch.w = depthMultiplier; + } + + return scratch; }, u_highlightColor : function() { return content._highlightColor; @@ -818,6 +855,7 @@ define([ var backFaceCulling = content._backFaceCulling; var vertexArray = content._drawCommand.vertexArray; var clippingPlanes = content._tileset.clippingPlanes; + var attenuation = content._attenuation; var colorStyleFunction; var showStyleFunction; @@ -930,11 +968,16 @@ define([ var vs = 'attribute vec3 a_position; \n' + 'varying vec4 v_color; \n' + - 'uniform vec2 u_pointSizeAndTilesetTime; \n' + + 'uniform vec4 u_pointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier; \n' + 'uniform vec4 u_constantColor; \n' + - 'uniform vec4 u_highlightColor; \n' + - 'float u_pointSize; \n' + - 'float u_tilesetTime; \n'; + 'uniform vec4 u_highlightColor; \n'; + vs += 'float u_pointSize; \n' + + 'float u_tilesetTime; \n'; + + if (attenuation) { + vs += 'float u_geometricError; \n' + + 'float u_depthMultiplier; \n'; + } vs += attributeDeclarations; @@ -983,8 +1026,13 @@ define([ vs += 'void main() \n' + '{ \n' + - ' u_pointSize = u_pointSizeAndTilesetTime.x; \n' + - ' u_tilesetTime = u_pointSizeAndTilesetTime.y; \n'; + ' u_pointSize = u_pointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier.x; \n' + + ' u_tilesetTime = u_pointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier.y; \n'; + + if (attenuation) { + vs += ' u_geometricError = u_pointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier.z; \n' + + ' u_depthMultiplier = u_pointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier.w; \n'; + } if (usesColors) { if (isTranslucent) { @@ -1032,6 +1080,11 @@ define([ if (hasPointSizeStyle) { vs += ' gl_PointSize = getPointSizeFromStyle(position, position_absolute, color, normal); \n'; + } else if (attenuation) { + vs += ' vec4 positionEC = czm_modelView * vec4(position, 1.0); \n' + + ' float depth = -positionEC.z; \n' + + // compute SSE for this point + ' gl_PointSize = min((u_geometricError / depth) * u_depthMultiplier, u_pointSize); \n'; } else { vs += ' gl_PointSize = u_pointSize; \n'; } @@ -1042,7 +1095,7 @@ define([ vs += ' normal = czm_normal * normal; \n' + ' float diffuseStrength = czm_getLambertDiffuse(czm_sunDirectionEC, normal); \n' + ' diffuseStrength = max(diffuseStrength, 0.4); \n' + // Apply some ambient lighting - ' color *= diffuseStrength; \n'; + ' color.xyz *= diffuseStrength; \n'; } vs += ' v_color = color; \n' + @@ -1256,6 +1309,19 @@ define([ createShaders(this, frameState, tileset.style); } + // Update attenuation + var pointCloudShading = this._tileset.pointCloudShading; + if (defined(pointCloudShading)) { + var formerAttenuation = this._attenuation; + this._attenuation = pointCloudShading.attenuation; + this._geometricErrorScale = pointCloudShading.geometricErrorScale; + this._maximumAttenuation = defined(pointCloudShading.maximumAttenuation) ? pointCloudShading.maximumAttenuation : tileset.maximumScreenSpaceError; + this._baseResolution = pointCloudShading.baseResolution; + if (this._attenuation !== formerAttenuation) { + createShaders(this, frameState, tileset.style); + } + } + if (updateModelMatrix) { Matrix4.clone(modelMatrix, this._modelMatrix); if (defined(this._rtcCenter)) { diff --git a/Source/Scene/PointCloudEyeDomeLighting.js b/Source/Scene/PointCloudEyeDomeLighting.js new file mode 100644 index 000000000000..880b1775ac08 --- /dev/null +++ b/Source/Scene/PointCloudEyeDomeLighting.js @@ -0,0 +1,361 @@ +define([ + '../Core/Cartesian3', + '../Core/clone', + '../Core/Color', + '../Core/combine', + '../Core/ComponentDatatype', + '../Core/defined', + '../Core/destroyObject', + '../Core/FeatureDetection', + '../Core/Geometry', + '../Core/GeometryAttribute', + '../Core/PixelFormat', + '../Core/PrimitiveType', + '../Renderer/BufferUsage', + '../Renderer/ClearCommand', + '../Renderer/DrawCommand', + '../Renderer/Framebuffer', + '../Renderer/Pass', + '../Renderer/PixelDatatype', + '../Renderer/RenderState', + '../Renderer/Sampler', + '../Renderer/ShaderSource', + '../Renderer/ShaderProgram', + '../Renderer/Texture', + '../Renderer/TextureMagnificationFilter', + '../Renderer/TextureMinificationFilter', + '../Renderer/TextureWrap', + '../Renderer/VertexArray', + '../Scene/BlendEquation', + '../Scene/BlendFunction', + '../Scene/BlendingState', + '../Scene/StencilFunction', + '../Scene/StencilOperation', + '../Shaders/PostProcessFilters/PointCloudEyeDomeLighting' + ], function( + Cartesian3, + clone, + Color, + combine, + ComponentDatatype, + defined, + destroyObject, + FeatureDetection, + Geometry, + GeometryAttribute, + PixelFormat, + PrimitiveType, + BufferUsage, + ClearCommand, + DrawCommand, + Framebuffer, + Pass, + PixelDatatype, + RenderState, + Sampler, + ShaderSource, + ShaderProgram, + Texture, + TextureMagnificationFilter, + TextureMinificationFilter, + TextureWrap, + VertexArray, + BlendEquation, + BlendFunction, + BlendingState, + StencilFunction, + StencilOperation, + PointCloudEyeDomeLightingShader + ) { + 'use strict'; + + /** + * Eye dome lighting. Does not support points with per-point translucency, but does allow translucent styling against the globe. + * Requires support for EXT_frag_depth, OES_texture_float, and WEBGL_draw_buffers extensions in WebGL 1.0. + * + * @private + */ + function PointCloudEyeDomeLighting() { + this._framebuffer = undefined; + this._colorTexture = undefined; // color gbuffer + this._ecAndLogDepthTexture = undefined; // depth gbuffer + this._depthTexture = undefined; // needed to write depth so camera based on depth works + this._drawCommand = undefined; + this._clearCommand = undefined; + + this._strength = 1.0; + this._radius = 1.0; + } + + function createSampler() { + return new Sampler({ + wrapS : TextureWrap.CLAMP_TO_EDGE, + wrapT : TextureWrap.CLAMP_TO_EDGE, + minificationFilter : TextureMinificationFilter.NEAREST, + magnificationFilter : TextureMagnificationFilter.NEAREST + }); + } + + function destroyFramebuffer(processor) { + var framebuffer = processor._framebuffer; + if (!defined(framebuffer)) { + return; + } + + processor._colorTexture.destroy(); + processor._ecAndLogDepthTexture.destroy(); + processor._depthTexture.destroy(); + framebuffer.destroy(); + + processor._framebuffer = undefined; + processor._colorTexture = undefined; + processor._ecAndLogDepthTexture = undefined; + processor._depthTexture = undefined; + processor._drawCommand = undefined; + processor._clearCommand = undefined; + } + + function createFramebuffer(processor, context) { + var screenWidth = context.drawingBufferWidth; + var screenHeight = context.drawingBufferHeight; + + var colorTexture = new Texture({ + context : context, + width : screenWidth, + height : screenHeight, + pixelFormat : PixelFormat.RGBA, + // Firefox as of 57.02 throws FRAMEBUFFER_UNSUPPORTED 0x8CDD if this doesn't match what's in ecTexture + pixelDatatype : FeatureDetection.isFirefox() ? PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE, + sampler : createSampler() + }); + + var ecTexture = new Texture({ + context : context, + width : screenWidth, + height : screenHeight, + pixelFormat : PixelFormat.RGBA, + pixelDatatype : PixelDatatype.FLOAT, + sampler : createSampler() + }); + + var depthTexture = new Texture({ + context : context, + width : screenWidth, + height : screenHeight, + pixelFormat : PixelFormat.DEPTH_COMPONENT, + pixelDatatype : PixelDatatype.UNSIGNED_INT, + sampler : createSampler() + }); + + processor._framebuffer = new Framebuffer({ + context : context, + colorTextures : [ + colorTexture, + ecTexture + ], + depthTexture : depthTexture, + destroyAttachments : false + }); + processor._colorTexture = colorTexture; + processor._ecAndLogDepthTexture = ecTexture; + processor._depthTexture = depthTexture; + } + + var distancesAndEdlStrengthScratch = new Cartesian3(); + + function createCommands(processor, context) { + var blendFS = PointCloudEyeDomeLightingShader; + + var blendUniformMap = { + u_pointCloud_colorTexture : function() { + return processor._colorTexture; + }, + u_pointCloud_ecAndLogDepthTexture : function() { + return processor._ecAndLogDepthTexture; + }, + u_distancesAndEdlStrength : function() { + distancesAndEdlStrengthScratch.x = processor._radius / context.drawingBufferWidth; + distancesAndEdlStrengthScratch.y = processor._radius / context.drawingBufferHeight; + distancesAndEdlStrengthScratch.z = processor._strength; + return distancesAndEdlStrengthScratch; + } + }; + + var blendRenderState = RenderState.fromCache({ + blending : BlendingState.ALPHA_BLEND, + depthMask : true, + depthTest : { + enabled : true + } + }); + + processor._drawCommand = context.createViewportQuadCommand(blendFS, { + uniformMap : blendUniformMap, + renderState : blendRenderState, + pass : Pass.CESIUM_3D_TILE, + owner : processor + }); + + processor._clearCommand = new ClearCommand({ + framebuffer : processor._framebuffer, + color : new Color(0.0, 0.0, 0.0, 0.0), + depth : 1.0, + renderState : RenderState.fromCache(), + pass : Pass.CESIUM_3D_TILE, + owner : processor + }); + } + + function createResources(processor, context) { + var screenWidth = context.drawingBufferWidth; + var screenHeight = context.drawingBufferHeight; + var colorTexture = processor._colorTexture; + var nowDirty = false; + var resized = defined(colorTexture) && + ((colorTexture.width !== screenWidth) || + (colorTexture.height !== screenHeight)); + + if (!defined(colorTexture) || resized) { + destroyFramebuffer(processor); + createFramebuffer(processor, context); + createCommands(processor, context); + nowDirty = true; + } + return nowDirty; + } + + function isSupported(context) { + return context.floatingPointTexture && context.drawBuffers && context.fragmentDepth; + } + + PointCloudEyeDomeLighting.isSupported = isSupported; + + function getECShaderProgram(context, shaderProgram) { + var shader = context.shaderCache.getDerivedShaderProgram(shaderProgram, 'EC'); + if (!defined(shader)) { + var attributeLocations = shaderProgram._attributeLocations; + + var vs = shaderProgram.vertexShaderSource.clone(); + var fs = shaderProgram.fragmentShaderSource.clone(); + + vs.sources = vs.sources.map(function(source) { + source = ShaderSource.replaceMain(source, 'czm_point_cloud_post_process_main'); + return source; + }); + + fs.sources = fs.sources.map(function(source) { + source = ShaderSource.replaceMain(source, 'czm_point_cloud_post_process_main'); + source = source.replace(/gl_FragColor/g, 'gl_FragData[0]'); + return source; + }); + + vs.sources.push( + 'varying vec3 v_positionEC; \n' + + 'void main() \n' + + '{ \n' + + ' czm_point_cloud_post_process_main(); \n' + + ' v_positionEC = (czm_inverseProjection * gl_Position).xyz; \n' + + '}'); + fs.sources.unshift('#extension GL_EXT_draw_buffers : enable \n'); + fs.sources.push( + 'varying vec3 v_positionEC; \n' + + 'void main() \n' + + '{ \n' + + ' czm_point_cloud_post_process_main(); \n' + + // Write log base 2 depth to alpha for EDL + ' gl_FragData[1] = vec4(v_positionEC, log2(-v_positionEC.z)); \n' + + '}'); + + shader = context.shaderCache.createDerivedShaderProgram(shaderProgram, 'EC', { + vertexShaderSource : vs, + fragmentShaderSource : fs, + attributeLocations : attributeLocations + }); + } + + return shader; + } + + PointCloudEyeDomeLighting.prototype.update = function(frameState, commandStart, tileset) { + var passes = frameState.passes; + var isPick = (passes.pick && !passes.render); + if (!isSupported(frameState.context) || isPick) { + return; + } + + this._strength = tileset.pointCloudShading.eyeDomeLightingStrength; + this._radius = tileset.pointCloudShading.eyeDomeLightingRadius; + + var dirty = createResources(this, frameState.context); + + // Hijack existing point commands to render into an offscreen FBO. + var i; + var commandList = frameState.commandList; + var commandEnd = commandList.length; + + for (i = commandStart; i < commandEnd; ++i) { + var command = commandList[i]; + if (command.primitiveType !== PrimitiveType.POINTS || command.pass === Pass.TRANSLUCENT) { + continue; + } + var derivedCommand = command.derivedCommands.pointCloudProcessor; + if (!defined(derivedCommand) || command.dirty || dirty || + (derivedCommand.framebuffer !== this._framebuffer)) { // Prevent crash when tiles out-of-view come in-view during context size change + derivedCommand = DrawCommand.shallowClone(command); + command.derivedCommands.pointCloudProcessor = derivedCommand; + + derivedCommand.framebuffer = this._framebuffer; + derivedCommand.shaderProgram = getECShaderProgram(frameState.context, command.shaderProgram); + derivedCommand.castShadows = false; + derivedCommand.receiveShadows = false; + } + + commandList[i] = derivedCommand; + } + + var clearCommand = this._clearCommand; + var blendCommand = this._drawCommand; + + // Blend EDL into the main FBO + commandList.push(blendCommand); + commandList.push(clearCommand); + }; + + /** + * 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 PointCloudEyeDomeLighting#destroy + */ + PointCloudEyeDomeLighting.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. + * + * @returns {undefined} + * + * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. + * + * @example + * processor = processor && processor.destroy(); + * + * @see PointCloudEyeDomeLighting#isDestroyed + */ + PointCloudEyeDomeLighting.prototype.destroy = function() { + destroyFramebuffer(this); + return destroyObject(this); + }; + + return PointCloudEyeDomeLighting; +}); diff --git a/Source/Scene/PointCloudShading.js b/Source/Scene/PointCloudShading.js new file mode 100644 index 000000000000..9d160e27bf4f --- /dev/null +++ b/Source/Scene/PointCloudShading.js @@ -0,0 +1,90 @@ +define([ + '../Core/defaultValue', + './PointCloudEyeDomeLighting' + ], function( + defaultValue, + PointCloudEyeDomeLighting) { + 'use strict'; + + /** + * Options for performing point attenuation based on geometric error when rendering + * point clouds using 3D Tiles. + * + * @param {Object} [options] Object with the following properties: + * @param {Boolean} [options.attenuation=false] Perform point attenuation based on geometric error. + * @param {Number} [options.geometricErrorScale=1.0] Scale to be applied to each tile's geometric error. + * @param {Number} [options.maximumAttenuation] Maximum attenuation in pixels. Defaults to the Cesium3DTileset's maximumScreenSpaceError. + * @param {Number} [options.baseResolution] Average base resolution for the dataset in meters. Substitute for Geometric Error when not available. + * @param {Boolean} [options.eyeDomeLighting=true] When true, use eye dome lighting when drawing with point attenuation. + * @param {Number} [options.eyeDomeLightingStrength=1.0] Increasing this value increases contrast on slopes and edges. + * @param {Number} [options.eyeDomeLightingRadius=1.0] Increase the thickness of contours from eye dome lighting. + * @constructor + */ + function PointCloudShading(options) { + var pointCloudShading = defaultValue(options, {}); + + /** + * Perform point attenuation based on geometric error. + * @type {Boolean} + * @default false + */ + this.attenuation = defaultValue(pointCloudShading.attenuation, false); + + /** + * Scale to be applied to the geometric error before computing attenuation. + * @type {Number} + * @default 1.0 + */ + this.geometricErrorScale = defaultValue(pointCloudShading.geometricErrorScale, 1.0); + + /** + * Maximum point attenuation in pixels. If undefined, the Cesium3DTileset's maximumScreenSpaceError will be used. + * @type {Number} + */ + this.maximumAttenuation = pointCloudShading.maximumAttenuation; + + /** + * Average base resolution for the dataset in meters. + * Used in place of geometric error when geometric error is 0. + * If undefined, an approximation will be computed for each tile that has geometric error of 0. + * @type {Number} + */ + this.baseResolution = pointCloudShading.baseResolution; + + /** + * Use eye dome lighting when drawing with point attenuation + * Requires support for EXT_frag_depth, OES_texture_float, and WEBGL_draw_buffers extensions in WebGL 1.0, + * otherwise eye dome lighting is ignored. + * + * @type {Boolean} + * @default true + */ + this.eyeDomeLighting = defaultValue(pointCloudShading.eyeDomeLighting, true); + + /** + * Eye dome lighting strength (apparent contrast) + * @type {Number} + * @default 1.0 + */ + this.eyeDomeLightingStrength = defaultValue(pointCloudShading.eyeDomeLightingStrength, 1.0); + + /** + * Thickness of contours from eye dome lighting + * @type {Number} + * @default 1.0 + */ + this.eyeDomeLightingRadius = defaultValue(pointCloudShading.eyeDomeLightingRadius, 1.0); + } + + /** + * Determines if point cloud shading is supported. + * + * @param {Scene} scene The scene. + * @returns {Boolean} true if point cloud shading is supported; otherwise, returns false + */ + PointCloudShading.isSupported = function(scene) { + return PointCloudEyeDomeLighting.isSupported(scene.context); + }; + + return PointCloudShading; +}); diff --git a/Source/Shaders/PostProcessFilters/PointCloudEyeDomeLighting.glsl b/Source/Shaders/PostProcessFilters/PointCloudEyeDomeLighting.glsl new file mode 100644 index 000000000000..9a6f86d448f5 --- /dev/null +++ b/Source/Shaders/PostProcessFilters/PointCloudEyeDomeLighting.glsl @@ -0,0 +1,49 @@ +#extension GL_EXT_frag_depth : enable + +uniform sampler2D u_pointCloud_colorTexture; +uniform sampler2D u_pointCloud_ecAndLogDepthTexture; +uniform vec3 u_distancesAndEdlStrength; +varying vec2 v_textureCoordinates; + +vec2 neighborContribution(float log2Depth, vec2 padding) +{ + vec2 depthAndLog2Depth = texture2D(u_pointCloud_ecAndLogDepthTexture, v_textureCoordinates + padding).zw; + if (depthAndLog2Depth.x == 0.0) // 0.0 is the clear value for the gbuffer + { + return vec2(0.0); // throw out this sample + } + else + { + return vec2(max(0.0, log2Depth - depthAndLog2Depth.y), 1.0); + } +} + +void main() +{ + vec4 ecAlphaDepth = texture2D(u_pointCloud_ecAndLogDepthTexture, v_textureCoordinates); + if (length(ecAlphaDepth.xyz) < czm_epsilon7) + { + discard; + } + else + { + vec4 color = texture2D(u_pointCloud_colorTexture, v_textureCoordinates); + + // sample from neighbors up, down, left, right + float distX = u_distancesAndEdlStrength.x; + float distY = u_distancesAndEdlStrength.y; + + vec2 responseAndCount = vec2(0.0); + + responseAndCount += neighborContribution(ecAlphaDepth.a, vec2(0, distY)); + responseAndCount += neighborContribution(ecAlphaDepth.a, vec2(distX, 0)); + responseAndCount += neighborContribution(ecAlphaDepth.a, vec2(0, -distY)); + responseAndCount += neighborContribution(ecAlphaDepth.a, vec2(-distX, 0)); + + float response = responseAndCount.x / responseAndCount.y; + float shade = exp(-response * 300.0 * u_distancesAndEdlStrength.z); + color.rgb *= shade; + gl_FragColor = vec4(color); + gl_FragDepthEXT = czm_eyeToWindowCoordinates(vec4(ecAlphaDepth.xyz, 1.0)).z; + } +} diff --git a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.js b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.js index 8407d7ea8918..3da27a44c4af 100644 --- a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.js +++ b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.js @@ -80,6 +80,21 @@ define([ displayPanelContents.appendChild(makeCheckbox('showContentBoundingVolumes', 'Content Volumes')); displayPanelContents.appendChild(makeCheckbox('showRequestVolumes', 'Request Volumes')); + displayPanelContents.appendChild(makeCheckbox('pointCloudShading', 'Point Cloud Shading')); + var pointCloudShadingContainer = document.createElement('div'); + pointCloudShadingContainer.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : pointCloudShading, "cesium-cesiumInspector-hide" : !pointCloudShading}'); + pointCloudShadingContainer.appendChild(makeRangeInput('geometricErrorScale', 0, 2, 0.01, 'Geometric Error Scale')); + pointCloudShadingContainer.appendChild(makeRangeInput('maximumAttenuation', 0, 32, 1, 'Maximum Attenuation')); + pointCloudShadingContainer.appendChild(makeRangeInput('baseResolution', 0, 1, 0.01, 'Base Resolution')); + pointCloudShadingContainer.appendChild(makeCheckbox('eyeDomeLighting', 'Eye Dome Lighting (EDL)')); + displayPanelContents.appendChild(pointCloudShadingContainer); + + var edlContainer = document.createElement('div'); + edlContainer.setAttribute('data-bind', 'css: {"cesium-cesiumInspector-show" : eyeDomeLighting, "cesium-cesiumInspector-hide" : !eyeDomeLighting}'); + edlContainer.appendChild(makeRangeInput('eyeDomeLightingStrength', 0, 2.0, 0.1, 'EDL Strength')); + edlContainer.appendChild(makeRangeInput('eyeDomeLightingRadius', 0, 4.0, 0.1, 'EDL Radius')); + pointCloudShadingContainer.appendChild(edlContainer); + updatePanelContents.appendChild(makeCheckbox('freezeFrame', 'Freeze Frame')); updatePanelContents.appendChild(makeCheckbox('dynamicScreenSpaceError', 'Dynamic Screen Space Error')); var sseContainer = document.createElement('div'); diff --git a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js index d4891b7c725d..b7a6153f547d 100644 --- a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js +++ b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js @@ -725,6 +725,162 @@ define([ } } }); + + var pointCloudShading = knockout.observable(); + knockout.defineProperty(this, 'pointCloudShading', { + get : function() { + return pointCloudShading(); + }, + set : function(value) { + pointCloudShading(value); + if (defined(that._tileset)) { + that._tileset.pointCloudShading.attenuation = value; + } + } + }); + /** + * Gets or sets the flag to enable point cloud shading. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.pointCloudShading = false; + + var geometricErrorScale = knockout.observable(); + knockout.defineProperty(this, 'geometricErrorScale', { + get : function() { + return geometricErrorScale(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + geometricErrorScale(value); + if (defined(that._tileset)) { + that._tileset.pointCloudShading.geometricErrorScale = value; + } + } + } + }); + /** + * Gets or sets the geometric error scale. This property is observable. + * + * @type {Number} + * @default 1.0 + */ + this.geometricErrorScale = 1.0; + + var maximumAttenuation = knockout.observable(); + knockout.defineProperty(this, 'maximumAttenuation', { + get : function() { + return maximumAttenuation(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + maximumAttenuation(value); + if (defined(that._tileset)) { + that._tileset.pointCloudShading.maximumAttenuation = value === 0 ? undefined : value; + } + } + } + }); + /** + * Gets or sets the maximum attenuation. This property is observable. + * + * @type {Number} + * @default 0 + */ + this.maximumAttenuation = 0; + + var baseResolution = knockout.observable(); + knockout.defineProperty(this, 'baseResolution', { + get : function() { + return baseResolution(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + baseResolution(value); + if (defined(that._tileset)) { + that._tileset.pointCloudShading.baseResolution = value === 0 ? undefined : value; + } + } + } + }); + /** + * Gets or sets the base resolution. This property is observable. + * + * @type {Number} + * @default 0 + */ + this.baseResolution = 0; + + var eyeDomeLighting = knockout.observable(); + knockout.defineProperty(this, 'eyeDomeLighting', { + get : function() { + return eyeDomeLighting(); + }, + set : function(value) { + eyeDomeLighting(value); + if (defined(that._tileset)) { + that._tileset.pointCloudShading.eyeDomeLighting = value; + } + } + }); + /** + * Gets or sets the flag to enable eye dome lighting. This property is observable. + * + * @type {Boolean} + * @default false + */ + this.eyeDomeLighting = false; + + var eyeDomeLightingStrength = knockout.observable(); + knockout.defineProperty(this, 'eyeDomeLightingStrength', { + get : function() { + return eyeDomeLightingStrength(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + eyeDomeLightingStrength(value); + if (defined(that._tileset)) { + that._tileset.pointCloudShading.eyeDomeLightingStrength = value; + } + } + } + }); + /** + * Gets or sets the eye dome lighting strength. This property is observable. + * + * @type {Number} + * @default 1.0 + */ + this.eyeDomeLightingStrength = 1.0; + + var eyeDomeLightingRadius = knockout.observable(); + knockout.defineProperty(this, 'eyeDomeLightingRadius', { + get : function() { + return eyeDomeLightingRadius(); + }, + set : function(value) { + value = Number(value); + if (!isNaN(value)) { + eyeDomeLightingRadius(value); + if (defined(that._tileset)) { + that._tileset.pointCloudShading.eyeDomeLightingRadius = value; + } + } + } + }); + /** + * Gets or sets the eye dome lighting radius. This property is observable. + * + * @type {Number} + * @default 1.0 + */ + this.eyeDomeLightingRadius = 1.0; + /** * Gets or sets the pick state * @@ -864,7 +1020,8 @@ define([ this._definedProperties = ['properties', 'dynamicScreenSpaceError', 'colorBlendMode', 'picking', 'colorize', 'wireframe', 'showBoundingVolumes', 'showContentBoundingVolumes', 'showRequestVolumes', 'freezeFrame', 'maximumScreenSpaceError', 'dynamicScreenSpaceErrorDensity', 'baseScreenSpaceError', 'skipScreenSpaceErrorFactor', 'skipLevelOfDetail', 'skipLevels', 'immediatelyLoadDesiredLevelOfDetail', 'loadSiblings', 'dynamicScreenSpaceErrorDensitySliderValue', - 'dynamicScreenSpaceErrorFactor', 'pickActive', 'showOnlyPickedTileDebugLabel', 'showGeometricError', 'showRenderingStatistics', 'showMemoryUsage', 'showUrl']; + 'dynamicScreenSpaceErrorFactor', 'pickActive', 'showOnlyPickedTileDebugLabel', 'showGeometricError', 'showRenderingStatistics', 'showMemoryUsage', 'showUrl', + 'pointCloudShading', 'geometricErrorScale', 'maximumAttenuation', 'baseResolution', 'eyeDomeLighting', 'eyeDomeLightingStrength', 'eyeDomeLightingRadius']; this._removePostRenderEvent = scene.postRender.addEventListener(function() { that._update(); }); @@ -999,6 +1156,16 @@ define([ this.skipLevels = tileset.skipLevels; this.immediatelyLoadDesiredLevelOfDetail = tileset.immediatelyLoadDesiredLevelOfDetail; this.loadSiblings = tileset.loadSiblings; + + var pointCloudShading = tileset.pointCloudShading; + this.pointCloudShading = pointCloudShading.attenuation; + this.geometricErrorScale = pointCloudShading.geometricErrorScale; + this.maximumAttenuation = pointCloudShading.maximumAttenuation ? pointCloudShading.maximumAttenuation: 0.0; + this.baseResolution = pointCloudShading.baseResolution ? pointCloudShading.baseResolution : 0.0; + this.eyeDomeLighting = pointCloudShading.eyeDomeLighting; + this.eyeDomeLightingStrength = pointCloudShading.eyeDomeLightingStrength; + this.eyeDomeLightingRadius = pointCloudShading.eyeDomeLightingRadius; + this._scene.requestRender(); } else { this._properties({}); diff --git a/Specs/Core/BoundingSphereSpec.js b/Specs/Core/BoundingSphereSpec.js index ceca57c35302..1dcc7711dab7 100644 --- a/Specs/Core/BoundingSphereSpec.js +++ b/Specs/Core/BoundingSphereSpec.js @@ -906,5 +906,12 @@ defineSuite([ expectBoundingSphereToContainPoint(boundingSphere, point, projection); }); + it('computes the volume of a BoundingSphere', function() { + var sphere = new BoundingSphere(new Cartesian3(), 1.0); + var computedVolume = sphere.volume(); + var expectedVolume = (4.0 / 3.0) * CesiumMath.PI; + expect(computedVolume).toEqualEpsilon(expectedVolume, CesiumMath.EPSILON6); + }); + createPackableSpecs(BoundingSphere, new BoundingSphere(new Cartesian3(1.0, 2.0, 3.0), 4.0), [1.0, 2.0, 3.0, 4.0]); }); diff --git a/Specs/Scene/PointCloud3DTileContentSpec.js b/Specs/Scene/PointCloud3DTileContentSpec.js index 89a368150578..d7fdad687e6b 100644 --- a/Specs/Scene/PointCloud3DTileContentSpec.js +++ b/Specs/Scene/PointCloud3DTileContentSpec.js @@ -15,6 +15,7 @@ defineSuite([ 'Scene/Cesium3DTileStyle', 'Scene/Expression', 'Specs/Cesium3DTilesTester', + 'Specs/createCanvas', 'Specs/createScene', 'ThirdParty/when' ], 'Scene/PointCloud3DTileContent', function( @@ -34,6 +35,7 @@ defineSuite([ Cesium3DTileStyle, Expression, Cesium3DTilesTester, + createCanvas, createScene, when) { 'use strict'; @@ -439,6 +441,117 @@ defineSuite([ }); }); + var noAttenuationPixelCount = 16; + function attenuationTest(postLoadCallback) { + var scene = createScene({ + canvas : createCanvas(10, 10) + }); + var center = new Cartesian3.fromRadians(centerLongitude, centerLatitude, 5.0); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 5.0)); + scene.fxaa = false; + scene.camera.zoomIn(6); + + return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(function(tileset) { + tileset.pointCloudShading.eyeDomeLighting = false; + postLoadCallback(scene, tileset); + scene.destroyForSpecs(); + }); + } + + it('attenuates points based on geometric error', function() { + return attenuationTest(function(scene, tileset) { + tileset.pointCloudShading.attenuation = true; + tileset.pointCloudShading.geometricErrorScale = 1.0; + tileset.pointCloudShading.maximumAttenuation = undefined; + tileset.pointCloudShading.baseResolution = undefined; + tileset.maximumScreenSpaceError = 16; + expect(scene).toRenderPixelCountAndCall(function(pixelCount) { + expect(pixelCount).toBeGreaterThan(noAttenuationPixelCount); + }); + }); + }); + + it('modulates attenuation using the tileset screen space error', function() { + return attenuationTest(function(scene, tileset) { + tileset.pointCloudShading.attenuation = true; + tileset.pointCloudShading.geometricErrorScale = 1.0; + tileset.pointCloudShading.maximumAttenuation = undefined; + tileset.pointCloudShading.baseResolution = undefined; + tileset.maximumScreenSpaceError = 1; + expect(scene).toRenderPixelCountAndCall(function(pixelCount) { + expect(pixelCount).toEqual(noAttenuationPixelCount); + }); + }); + }); + + it('modulates attenuation using the maximumAttenuation parameter', function() { + return attenuationTest(function(scene, tileset) { + tileset.pointCloudShading.attenuation = true; + tileset.pointCloudShading.geometricErrorScale = 1.0; + tileset.pointCloudShading.maximumAttenuation = 1; + tileset.pointCloudShading.baseResolution = undefined; + tileset.maximumScreenSpaceError = 16; + expect(scene).toRenderPixelCountAndCall(function(pixelCount) { + expect(pixelCount).toEqual(noAttenuationPixelCount); + }); + }); + }); + + it('modulates attenuation using the baseResolution parameter', function() { + return attenuationTest(function(scene, tileset) { + tileset.pointCloudShading.attenuation = true; + tileset.pointCloudShading.geometricErrorScale = 1.0; + tileset.pointCloudShading.maximumAttenuation = undefined; + tileset.pointCloudShading.baseResolution = CesiumMath.EPSILON20; + tileset.maximumScreenSpaceError = 16; + expect(scene).toRenderPixelCountAndCall(function(pixelCount) { + expect(pixelCount).toEqual(noAttenuationPixelCount); + }); + }); + }); + + it('modulates attenuation using the baseResolution parameter', function() { + return attenuationTest(function(scene, tileset) { + // pointCloudNoColorUrl is a single tile with GeometricError = 0, + // which results in default baseResolution being computed + tileset.pointCloudShading.attenuation = true; + tileset.pointCloudShading.geometricErrorScale = 1.0; + tileset.pointCloudShading.maximumAttenuation = undefined; + tileset.pointCloudShading.baseResolution = CesiumMath.EPSILON20; + tileset.maximumScreenSpaceError = 16; + expect(scene).toRenderPixelCountAndCall(function(pixelCount) { + expect(pixelCount).toEqual(noAttenuationPixelCount); + }); + }); + }); + + it('modulates attenuation using the geometricErrorScale parameter', function() { + return attenuationTest(function(scene, tileset) { + tileset.pointCloudShading.attenuation = true; + tileset.pointCloudShading.geometricErrorScale = 0.0; + tileset.pointCloudShading.maximumAttenuation = undefined; + tileset.pointCloudShading.baseResolution = undefined; + tileset.maximumScreenSpaceError = 1; + expect(scene).toRenderPixelCountAndCall(function(pixelCount) { + expect(pixelCount).toEqual(noAttenuationPixelCount); + }); + }); + }); + + it('attenuates points based on geometric error in 2D', function() { + return attenuationTest(function(scene, tileset) { + scene.morphTo2D(0); + tileset.pointCloudShading.attenuation = true; + tileset.pointCloudShading.geometricErrorScale = 1.0; + tileset.pointCloudShading.maximumAttenuation = undefined; + tileset.pointCloudShading.baseResolution = undefined; + tileset.maximumScreenSpaceError = 16; + expect(scene).toRenderPixelCountAndCall(function(pixelCount) { + expect(pixelCount).toBeGreaterThan(noAttenuationPixelCount); + }); + }); + }); + it('applies shader style', function() { return Cesium3DTilesTester.loadTileset(scene, pointCloudWithPerPointPropertiesUrl).then(function(tileset) { var content = tileset._root.content; diff --git a/Specs/Scene/PointCloudEyeDomeLightingSpec.js b/Specs/Scene/PointCloudEyeDomeLightingSpec.js new file mode 100644 index 000000000000..f2d33a541e44 --- /dev/null +++ b/Specs/Scene/PointCloudEyeDomeLightingSpec.js @@ -0,0 +1,94 @@ +defineSuite([ + 'Core/Cartesian3', + 'Core/Color', + 'Core/defined', + 'Core/HeadingPitchRange', + 'Core/HeadingPitchRoll', + 'Core/Math', + 'Core/Transforms', + 'Core/PerspectiveFrustum', + 'Scene/PointCloud3DTileContent', + 'Scene/PointCloudEyeDomeLighting', + 'Specs/Cesium3DTilesTester', + 'Specs/createScene' + ], 'Scene/PointCloudEyeDomeLighting', function( + Cartesian3, + Color, + defined, + HeadingPitchRange, + HeadingPitchRoll, + CesiumMath, + Transforms, + PerspectiveFrustum, + PointCloud3DTileContent, + PointCloudEyeDomeLighting, + Cesium3DTilesTester, + createScene) { + 'use strict'; + + var scene; + var centerLongitude = -1.31968; + var centerLatitude = 0.698874; + + var pointCloudNoColorUrl = './Data/Cesium3DTiles/PointCloud/PointCloudNoColor'; + + function setCamera(longitude, latitude) { + // Point the camera to the center of the tile + var center = Cartesian3.fromRadians(longitude, latitude, 5.0); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 5.0)); + } + + beforeAll(function() { + scene = createScene(); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + beforeEach(function() { + var camera = scene.camera; + camera.frustum = new PerspectiveFrustum(); + camera.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; + camera.frustum.fov = CesiumMath.toRadians(60.0); + + setCamera(centerLongitude, centerLatitude); + }); + + afterEach(function() { + scene.primitives.removeAll(); + }); + + it('adds a clear command and a post-processing draw call', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(function(tileset) { + if (!PointCloudEyeDomeLighting.isSupported(scene.frameState.context)) { + return; + } + + tileset.pointCloudShading.eyeDomeLighting = true; + + scene.renderForSpecs(); + var originalLength = scene.frameState.commandList.length; + + tileset.pointCloudShading.attenuation = true; + scene.renderForSpecs(); + var newLength = scene.frameState.commandList.length; + expect(newLength).toEqual(originalLength + 2); + }); + }); + + it('does not change commands for pick calls', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(function(tileset) { + tileset.pointCloudShading.eyeDomeLighting = true; + + scene.pickForSpecs(); + var originalLength = scene.frameState.commandList.length; + + tileset.pointCloudShading.attenuation = true; + scene.pickForSpecs(); + var newLength = scene.frameState.commandList.length; + expect(newLength).toEqual(originalLength); + }); + }); + +}, 'WebGL'); diff --git a/Specs/Scene/PointCloudShadingSpec.js b/Specs/Scene/PointCloudShadingSpec.js new file mode 100644 index 000000000000..a08282804630 --- /dev/null +++ b/Specs/Scene/PointCloudShadingSpec.js @@ -0,0 +1,43 @@ +defineSuite([ + 'Scene/PointCloudShading', + 'Specs/createScene' + ], function( + PointCloudShading, + createScene) { + 'use strict'; + + it('creates expected instance from raw assignment and construction', function() { + var pointCloudShading = new PointCloudShading(); + expect(pointCloudShading.attenuation).toEqual(false); + expect(pointCloudShading.geometricErrorScale).toEqual(1.0); + expect(pointCloudShading.maximumAttenuation).not.toBeDefined(); + expect(pointCloudShading.baseResolution).not.toBeDefined(); + expect(pointCloudShading.eyeDomeLighting).toEqual(true); + expect(pointCloudShading.eyeDomeLightingStrength).toEqual(1.0); + expect(pointCloudShading.eyeDomeLightingRadius).toEqual(1.0); + + var options = { + geometricErrorScale : 2.0, + maximumAttenuation : 16, + baseResolution : 0.1, + eyeDomeLightingStrength : 0.1, + eyeDomeLightingRadius : 2.0 + }; + pointCloudShading = new PointCloudShading(options); + expect(pointCloudShading.attenuation).toEqual(false); + expect(pointCloudShading.geometricErrorScale).toEqual(options.geometricErrorScale); + expect(pointCloudShading.maximumAttenuation).toEqual(options.maximumAttenuation); + expect(pointCloudShading.baseResolution).toEqual(options.baseResolution); + expect(pointCloudShading.eyeDomeLighting).toEqual(true); + expect(pointCloudShading.eyeDomeLightingStrength).toEqual(options.eyeDomeLightingStrength); + expect(pointCloudShading.eyeDomeLightingRadius).toEqual(options.eyeDomeLightingRadius); + }); + + it('provides a method for checking if point cloud shading is supported', function() { + var scene = createScene(); + var context = scene.context; + var expectedSupport = context.floatingPointTexture && context.drawBuffers && context.fragmentDepth; + expect(PointCloudShading.isSupported(scene)).toEqual(expectedSupport); + scene.destroyForSpecs(); + }); +}); diff --git a/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js b/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js index 0806123ea098..3b0294b3e648 100644 --- a/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js +++ b/Specs/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModelSpec.js @@ -161,6 +161,55 @@ defineSuite([ viewModel.showUrl = false; expect(viewModel.tileset.debugShowUrl).toBe(false); }); + + it('pointCloudShading', function() { + viewModel.pointCloudShading = true; + expect(viewModel.tileset.pointCloudShading.attenuation).toBe(true); + viewModel.pointCloudShading = false; + expect(viewModel.tileset.pointCloudShading.attenuation).toBe(false); + }); + + it('geometricErrorScale', function() { + viewModel.geometricErrorScale = 1.0; + expect(viewModel.tileset.pointCloudShading.geometricErrorScale).toBe(1.0); + viewModel.geometricErrorScale = 0.0; + expect(viewModel.tileset.pointCloudShading.geometricErrorScale).toBe(0.0); + }); + + it('maximumAttenuation', function() { + viewModel.maximumAttenuation = 1.0; + expect(viewModel.tileset.pointCloudShading.maximumAttenuation).toBe(1.0); + viewModel.maximumAttenuation = 0.0; + expect(viewModel.tileset.pointCloudShading.maximumAttenuation).not.toBeDefined(); + }); + + it('baseResolution', function() { + viewModel.baseResolution = 1.0; + expect(viewModel.tileset.pointCloudShading.baseResolution).toBe(1.0); + viewModel.baseResolution = 0.0; + expect(viewModel.tileset.pointCloudShading.baseResolution).not.toBeDefined(); + }); + + it('eyeDomeLighting', function() { + viewModel.eyeDomeLighting = true; + expect(viewModel.tileset.pointCloudShading.eyeDomeLighting).toBe(true); + viewModel.eyeDomeLighting = false; + expect(viewModel.tileset.pointCloudShading.eyeDomeLighting).toBe(false); + }); + + it('eyeDomeLightingStrength', function() { + viewModel.eyeDomeLightingStrength = 1.0; + expect(viewModel.tileset.pointCloudShading.eyeDomeLightingStrength).toBe(1.0); + viewModel.eyeDomeLightingStrength = 0.0; + expect(viewModel.tileset.pointCloudShading.eyeDomeLightingStrength).toBe(0.0); + }); + + it('eyeDomeLightingRadius', function() { + viewModel.eyeDomeLightingRadius = 1.0; + expect(viewModel.tileset.pointCloudShading.eyeDomeLightingRadius).toBe(1.0); + viewModel.eyeDomeLightingRadius = 0.0; + expect(viewModel.tileset.pointCloudShading.eyeDomeLightingRadius).toBe(0.0); + }); }); describe('update options', function() { diff --git a/Specs/addDefaultMatchers.js b/Specs/addDefaultMatchers.js index 4ef3a85d986b..775b3f3e996e 100644 --- a/Specs/addDefaultMatchers.js +++ b/Specs/addDefaultMatchers.js @@ -261,6 +261,26 @@ define([ }; }, + toRenderPixelCountAndCall : function(util, customEqualityTesters) { + return { + compare : function(actual, expected) { + var actualRgba = renderAndReadPixels(actual); + + var webglStub = !!window.webglStub; + if (!webglStub) { + // The callback may have expectations that fail, which still makes the + // spec fail, as we desired, even though this matcher sets pass to true. + var callback = expected; + callback(countRenderedPixels(actualRgba)); + } + + return { + pass : true + }; + } + }; + }, + toPickPrimitive : function(util, customEqualityTesters) { return { compare : function(actual, expected, x, y, width, height) { @@ -418,6 +438,21 @@ define([ }; } + function countRenderedPixels(rgba) { + var pixelCount = rgba.length / 4; + var count = 0; + for (var i = 0; i < pixelCount; i++) { + var index = i * 4; + if (rgba[index] !== 0 || + rgba[index + 1] !== 0 || + rgba[index + 2] !== 0 || + rgba[index + 3] !== 255) { + count++; + } + } + return count; + } + function renderAndReadPixels(options) { var scene;