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 @@ + + +
+ + + + + +Maximum Screen Space Error | ++ + + | +
Attenuation | |
Geometric Error Scale | ++ + + | +
Maximum Attenuation | ++ + + | +
Base Resolution | ++ + + | +
Eye Dome Lighting | |
Eye Dome Lighting Strength | ++ + + | +
Eye Dome Lighting Radius | ++ + + | +
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.
+ * 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;