diff --git a/CHANGES.md b/CHANGES.md
index ecc231f785dc..e4376dcab8c7 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -15,6 +15,8 @@
- Added `Cesium3DTileset.getHeight` to sample height values of the loaded tiles. If using WebGL 1, the `enablePick` option must be set to true to use this function. [#11581](https://github.com/CesiumGS/cesium/pull/11581)
- Added `Cesium3DTileset.disableCollision` to allow the camera from to go inside or below a 3D tileset, for instance, to be used with 3D Tiles interiors. [#11581](https://github.com/CesiumGS/cesium/pull/11581)
- The `Cesium3DTileset.dynamicScreenSpaceError` optimization is now enabled by default, as this improves performance for street-level horizon views. Furthermore, the default settings of this feature were tuned for improved performance. `Cesium3DTileset.dynamicScreenSpaceErrorDensity` was changed from 0.00278 to 0.0002. `Cesium3DTileset.dynamicScreenSpaceErrorFactor` was changed from 4 to 24. [#11718](https://github.com/CesiumGS/cesium/pull/11718)
+- Fog rendering now applies to glTF models and 3D Tiles. This can be configured using `scene.fog` and `scene.atmosphere`. [#11744](https://github.com/CesiumGS/cesium/pull/11744)
+- Added `scene.atmosphere` to store common atmosphere lighting parameters. [#11744](https://github.com/CesiumGS/cesium/pull/11744) and [#11681](https://github.com/CesiumGS/cesium/issues/11681)
##### Fixes :wrench:
diff --git a/Specs/createFrameState.js b/Specs/createFrameState.js
index 672a2376fe58..8f4ba1c1f6b2 100644
--- a/Specs/createFrameState.js
+++ b/Specs/createFrameState.js
@@ -1,4 +1,5 @@
import {
+ Atmosphere,
defaultValue,
GeographicProjection,
JulianDate,
@@ -51,6 +52,8 @@ function createFrameState(context, camera, frameNumber, time) {
frameState.minimumDisableDepthTestDistance = 0.0;
+ frameState.atmosphere = new Atmosphere();
+
return frameState;
}
export default createFrameState;
diff --git a/packages/engine/Source/Renderer/AutomaticUniforms.js b/packages/engine/Source/Renderer/AutomaticUniforms.js
index 2a8930c68edf..0b16c23ae9a3 100644
--- a/packages/engine/Source/Renderer/AutomaticUniforms.js
+++ b/packages/engine/Source/Renderer/AutomaticUniforms.js
@@ -954,7 +954,8 @@ const AutomaticUniforms = {
/**
* An automatic GLSL uniform containing the ellipsoid radii of curvature at the camera position.
- * The .x component is the prime vertical radius, .y is the meridional.
+ * The .x component is the prime vertical radius of curvature (east-west direction)
+ * .y is the meridional radius of curvature (north-south direction)
* This uniform is only valid when the {@link SceneMode} is SCENE3D
.
*/
czm_eyeEllipsoidCurvature: new AutomaticUniform({
@@ -1591,6 +1592,125 @@ const AutomaticUniforms = {
},
}),
+ /**
+ * An automatic GLSL uniform scalar used to set a minimum brightness when dynamic lighting is applied to fog.
+ *
+ * @see czm_fog
+ */
+ czm_fogMinimumBrightness: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT,
+ getValue: function (uniformState) {
+ return uniformState.fogMinimumBrightness;
+ },
+ }),
+
+ /**
+ * An automatic uniform representing the color shift for the atmosphere in HSB color space
+ *
+ * @example
+ * uniform vec3 czm_atmosphereHsbShift;
+ */
+ czm_atmosphereHsbShift: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT_VEC3,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereHsbShift;
+ },
+ }),
+ /**
+ * An automatic uniform representing the intensity of the light that is used for computing the atmosphere color
+ *
+ * @example
+ * uniform float czm_atmosphereLightIntensity;
+ */
+ czm_atmosphereLightIntensity: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereLightIntensity;
+ },
+ }),
+ /**
+ * An automatic uniform representing the Rayleigh scattering coefficient used when computing the atmosphere scattering
+ *
+ * @example
+ * uniform vec3 czm_atmosphereRayleighCoefficient;
+ */
+ czm_atmosphereRayleighCoefficient: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT_VEC3,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereRayleighCoefficient;
+ },
+ }),
+ /**
+ * An automatic uniform representing the Rayleigh scale height in meters used for computing atmosphere scattering.
+ *
+ * @example
+ * uniform vec3 czm_atmosphereRayleighScaleHeight;
+ */
+ czm_atmosphereRayleighScaleHeight: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereRayleighScaleHeight;
+ },
+ }),
+ /**
+ * An automatic uniform representing the Mie scattering coefficient used when computing atmosphere scattering.
+ *
+ * @example
+ * uniform vec3 czm_atmosphereMieCoefficient;
+ */
+ czm_atmosphereMieCoefficient: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT_VEC3,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereMieCoefficient;
+ },
+ }),
+ /**
+ * An automatic uniform storign the Mie scale height used when computing atmosphere scattering.
+ *
+ * @example
+ * uniform float czm_atmosphereMieScaleHeight;
+ */
+ czm_atmosphereMieScaleHeight: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereMieScaleHeight;
+ },
+ }),
+ /**
+ * An automatic uniform representing the anisotropy of the medium to consider for Mie scattering.
+ *
+ * @example
+ * uniform float czm_atmosphereAnisotropy;
+ */
+ czm_atmosphereMieAnisotropy: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereMieAnisotropy;
+ },
+ }),
+
+ /**
+ * An automatic uniform representing which light source to use for dynamic lighting
+ *
+ * @example
+ * uniform float czm_atmosphereDynamicLighting
+ */
+ czm_atmosphereDynamicLighting: new AutomaticUniform({
+ size: 1,
+ datatype: WebGLConstants.FLOAT,
+ getValue: function (uniformState) {
+ return uniformState.atmosphereDynamicLighting;
+ },
+ }),
+
/**
* An automatic GLSL uniform representing the splitter position to use when rendering with a splitter.
* This will be in pixel coordinates relative to the canvas.
diff --git a/packages/engine/Source/Renderer/UniformState.js b/packages/engine/Source/Renderer/UniformState.js
index 41e2e821b0aa..cc5030e6f5d6 100644
--- a/packages/engine/Source/Renderer/UniformState.js
+++ b/packages/engine/Source/Renderer/UniformState.js
@@ -161,6 +161,16 @@ function UniformState() {
this._specularEnvironmentMapsMaximumLOD = undefined;
this._fogDensity = undefined;
+ this._fogMinimumBrightness = undefined;
+
+ this._atmosphereHsbShift = undefined;
+ this._atmosphereLightIntensity = undefined;
+ this._atmosphereRayleighCoefficient = new Cartesian3();
+ this._atmosphereRayleighScaleHeight = new Cartesian3();
+ this._atmosphereMieCoefficient = new Cartesian3();
+ this._atmosphereMieScaleHeight = undefined;
+ this._atmosphereMieAnisotropy = undefined;
+ this._atmosphereDynamicLighting = undefined;
this._invertClassificationColor = undefined;
@@ -915,6 +925,99 @@ Object.defineProperties(UniformState.prototype, {
},
},
+ /**
+ * A scalar used as a minimum value when brightening fog
+ * @memberof UniformState.prototype
+ * @type {number}
+ */
+ fogMinimumBrightness: {
+ get: function () {
+ return this._fogMinimumBrightness;
+ },
+ },
+
+ /**
+ * A color shift to apply to the atmosphere color in HSB.
+ * @memberof UniformState.prototype
+ * @type {Cartesian3}
+ */
+ atmosphereHsbShift: {
+ get: function () {
+ return this._atmosphereHsbShift;
+ },
+ },
+ /**
+ * The intensity of the light that is used for computing the atmosphere color
+ * @memberof UniformState.prototype
+ * @type {number}
+ */
+ atmosphereLightIntensity: {
+ get: function () {
+ return this._atmosphereLightIntensity;
+ },
+ },
+ /**
+ * The Rayleigh scattering coefficient used in the atmospheric scattering equations for the sky atmosphere.
+ * @memberof UniformState.prototype
+ * @type {Cartesian3}
+ */
+ atmosphereRayleighCoefficient: {
+ get: function () {
+ return this._atmosphereRayleighCoefficient;
+ },
+ },
+ /**
+ * The Rayleigh scale height used in the atmospheric scattering equations for the sky atmosphere, in meters.
+ * @memberof UniformState.prototype
+ * @type {number}
+ */
+ atmosphereRayleighScaleHeight: {
+ get: function () {
+ return this._atmosphereRayleighScaleHeight;
+ },
+ },
+ /**
+ * The Mie scattering coefficient used in the atmospheric scattering equations for the sky atmosphere.
+ * @memberof UniformState.prototype
+ * @type {Cartesian3}
+ */
+ atmosphereMieCoefficient: {
+ get: function () {
+ return this._atmosphereMieCoefficient;
+ },
+ },
+ /**
+ * The Mie scale height used in the atmospheric scattering equations for the sky atmosphere, in meters.
+ * @memberof UniformState.prototype
+ * @type {number}
+ */
+ atmosphereMieScaleHeight: {
+ get: function () {
+ return this._atmosphereMieScaleHeight;
+ },
+ },
+ /**
+ * The anisotropy of the medium to consider for Mie scattering.
+ * @memberof UniformState.prototype
+ * @type {number}
+ */
+ atmosphereMieAnisotropy: {
+ get: function () {
+ return this._atmosphereMieAnisotropy;
+ },
+ },
+ /**
+ * Which light source to use for dynamically lighting the atmosphere
+ *
+ * @memberof UniformState.prototype
+ * @type {DynamicAtmosphereLightingType}
+ */
+ atmosphereDynamicLighting: {
+ get: function () {
+ return this._atmosphereDynamicLighting;
+ },
+ },
+
/**
* A scalar that represents the geometric tolerance per meter
* @memberof UniformState.prototype
@@ -1411,6 +1514,30 @@ UniformState.prototype.update = function (frameState) {
}
this._fogDensity = frameState.fog.density;
+ this._fogMinimumBrightness = frameState.fog.minimumBrightness;
+
+ const atmosphere = frameState.atmosphere;
+ if (defined(atmosphere)) {
+ this._atmosphereHsbShift = Cartesian3.fromElements(
+ atmosphere.hueShift,
+ atmosphere.saturationShift,
+ atmosphere.brightnessShift,
+ this._atmosphereHsbShift
+ );
+ this._atmosphereLightIntensity = atmosphere.lightIntensity;
+ this._atmosphereRayleighCoefficient = Cartesian3.clone(
+ atmosphere.rayleighCoefficient,
+ this._atmosphereRayleighCoefficient
+ );
+ this._atmosphereRayleighScaleHeight = atmosphere.rayleighScaleHeight;
+ this._atmosphereMieCoefficient = Cartesian3.clone(
+ atmosphere.mieCoefficient,
+ this._atmosphereMieCoefficient
+ );
+ this._atmosphereMieScaleHeight = atmosphere.mieScaleHeight;
+ this._atmosphereMieAnisotropy = atmosphere.mieAnisotropy;
+ this._atmosphereDynamicLighting = atmosphere.dynamicLighting;
+ }
this._invertClassificationColor = frameState.invertClassificationColor;
diff --git a/packages/engine/Source/Scene/Atmosphere.js b/packages/engine/Source/Scene/Atmosphere.js
new file mode 100644
index 000000000000..296ad0f19d25
--- /dev/null
+++ b/packages/engine/Source/Scene/Atmosphere.js
@@ -0,0 +1,127 @@
+import Cartesian3 from "../Core/Cartesian3.js";
+import DynamicAtmosphereLightingType from "./DynamicAtmosphereLightingType.js";
+
+/**
+ * Common atmosphere settings used by 3D Tiles and models for rendering sky atmosphere, ground atmosphere, and fog.
+ *
+ *
+ * This class is not to be confused with {@link SkyAtmosphere}, which is responsible for rendering the sky. + *
+ *+ * While the atmosphere settings affect the color of fog, see {@link Fog} to control how fog is rendered. + *
+ * + * @alias Atmosphere + * @constructor + * + * @example + * // Turn on dynamic atmosphere lighting using the sun direction + * scene.atmosphere.dynamicLighting = Cesium.DynamicAtmosphereLightingType.SUNLIGHT; + * + * @example + * // Turn on dynamic lighting using whatever light source is in the scene + * scene.light = new Cesium.DirectionalLight({ + * direction: new Cesium.Cartesian3(1, 0, 0) + * }); + * scene.atmosphere.dynamicLighting = Cesium.DynamicAtmosphereLightingType.SCENE_LIGHT; + * + * @example + * // Adjust the color of the atmosphere effects. + * scene.atmosphere.hueShift = 0.4; // Cycle 40% around the color wheel + * scene.atmosphere.brightnessShift = 0.25; // Increase the brightness + * scene.atmosphere.saturationShift = -0.1; // Desaturate the colors + * + * @see SkyAtmosphere + * @see Globe + * @see Fog + */ +function Atmosphere() { + /** + * The intensity of the light that is used for computing the ground atmosphere color. + * + * @type {number} + * @default 10.0 + */ + this.lightIntensity = 10.0; + + /** + * The Rayleigh scattering coefficient used in the atmospheric scattering equations for the ground atmosphere. + * + * @type {Cartesian3} + * @default Cartesian3(5.5e-6, 13.0e-6, 28.4e-6) + */ + this.rayleighCoefficient = new Cartesian3(5.5e-6, 13.0e-6, 28.4e-6); + + /** + * The Mie scattering coefficient used in the atmospheric scattering equations for the ground atmosphere. + * + * @type {Cartesian3} + * @default Cartesian3(21e-6, 21e-6, 21e-6) + */ + this.mieCoefficient = new Cartesian3(21e-6, 21e-6, 21e-6); + + /** + * The Rayleigh scale height used in the atmospheric scattering equations for the ground atmosphere, in meters. + * + * @type {number} + * @default 10000.0 + */ + this.rayleighScaleHeight = 10000.0; + + /** + * The Mie scale height used in the atmospheric scattering equations for the ground atmosphere, in meters. + * + * @type {number} + * @default 3200.0 + */ + this.mieScaleHeight = 3200.0; + + /** + * The anisotropy of the medium to consider for Mie scattering. + *+ * Valid values are between -1.0 and 1.0. + *
+ * + * @type {number} + * @default 0.9 + */ + this.mieAnisotropy = 0.9; + + /** + * The hue shift to apply to the atmosphere. Defaults to 0.0 (no shift). + * A hue shift of 1.0 indicates a complete rotation of the hues available. + * + * @type {number} + * @default 0.0 + */ + this.hueShift = 0.0; + + /** + * The saturation shift to apply to the atmosphere. Defaults to 0.0 (no shift). + * A saturation shift of -1.0 is monochrome. + * + * @type {number} + * @default 0.0 + */ + this.saturationShift = 0.0; + + /** + * The brightness shift to apply to the atmosphere. Defaults to 0.0 (no shift). + * A brightness shift of -1.0 is complete darkness, which will let space show through. + * + * @type {number} + * @default 0.0 + */ + this.brightnessShift = 0.0; + + /** + * When not DynamicAtmosphereLightingType.NONE, the selected light source will + * be used for dynamically lighting all atmosphere-related rendering effects. + * + * @type {DynamicAtmosphereLightingType} + * @default DynamicAtmosphereLightingType.NONE + */ + this.dynamicLighting = DynamicAtmosphereLightingType.NONE; +} + +export default Atmosphere; diff --git a/packages/engine/Source/Scene/DynamicAtmosphereLightingType.js b/packages/engine/Source/Scene/DynamicAtmosphereLightingType.js new file mode 100644 index 000000000000..d1d9967a730e --- /dev/null +++ b/packages/engine/Source/Scene/DynamicAtmosphereLightingType.js @@ -0,0 +1,56 @@ +/** + * Atmosphere lighting effects (sky atmosphere, ground atmosphere, fog) can be + * further modified with dynamic lighting from the sun or other light source + * that changes over time. This enum determines which light source to use. + * + * @enum {number} + */ +const DynamicAtmosphereLightingType = { + /** + * Do not use dynamic atmosphere lighting. Atmosphere lighting effects will + * be lit from directly above rather than using the scene's light source. + * + * @type {number} + * @constant + */ + NONE: 0, + /** + * Use the scene's current light source for dynamic atmosphere lighting. + * + * @type {number} + * @constant + */ + SCENE_LIGHT: 1, + /** + * Force the dynamic atmosphere lighting to always use the sunlight direction, + * even if the scene uses a different light source. + * + * @type {number} + * @constant + */ + SUNLIGHT: 2, +}; + +/** + * Get the lighting enum from the older globe flags + * + * @param {Globe} globe The globe + * @return {DynamicAtmosphereLightingType} The corresponding enum value + * + * @private + */ +DynamicAtmosphereLightingType.fromGlobeFlags = function (globe) { + const lightingOn = globe.enableLighting && globe.dynamicAtmosphereLighting; + if (!lightingOn) { + return DynamicAtmosphereLightingType.NONE; + } + + // Force sunlight + if (globe.dynamicAtmosphereLightingFromSun) { + return DynamicAtmosphereLightingType.SUNLIGHT; + } + + return DynamicAtmosphereLightingType.SCENE_LIGHT; +}; + +export default Object.freeze(DynamicAtmosphereLightingType); diff --git a/packages/engine/Source/Scene/Fog.js b/packages/engine/Source/Scene/Fog.js index 217ef14afcb2..877d45b44f05 100644 --- a/packages/engine/Source/Scene/Fog.js +++ b/packages/engine/Source/Scene/Fog.js @@ -173,6 +173,7 @@ Fog.prototype.update = function (frameState) { frameState.mode !== SceneMode.SCENE3D ) { frameState.fog.enabled = false; + frameState.fog.density = 0; return; } diff --git a/packages/engine/Source/Scene/FrameState.js b/packages/engine/Source/Scene/FrameState.js index 1ed60a2c1e1e..adfffba7958a 100644 --- a/packages/engine/Source/Scene/FrameState.js +++ b/packages/engine/Source/Scene/FrameState.js @@ -254,7 +254,8 @@ function FrameState(context, creditDisplay, jobScheduler) { /** * @typedef FrameState.Fog * @type {object} - * @property {boolean} enabledtrue
if fog is enabled, false
otherwise.
+ * @property {boolean} enabled true
if fog is enabled, false
otherwise. This affects both fog culling and rendering.
+ * @property {boolean} renderable true
if fog should be rendered, false
if not. This flag should be checked in combination with fog.enabled.
* @property {number} density A positive number used to mix the color and fog color based on camera distance.
* @property {number} sse A scalar used to modify the screen space error of geometry partially in fog.
* @property {number} minimumBrightness The minimum brightness of terrain with fog applied.
@@ -269,11 +270,18 @@ function FrameState(context, creditDisplay, jobScheduler) {
* @default false
*/
enabled: false,
+ renderable: false,
density: undefined,
sse: undefined,
minimumBrightness: undefined,
};
+ /**
+ * The current Atmosphere
+ * @type {Atmosphere}
+ */
+ this.atmosphere = undefined;
+
/**
* A scalar used to vertically exaggerate the scene
* @type {number}
diff --git a/packages/engine/Source/Scene/Globe.js b/packages/engine/Source/Scene/Globe.js
index fc7da4fb14db..284b440006b8 100644
--- a/packages/engine/Source/Scene/Globe.js
+++ b/packages/engine/Source/Scene/Globe.js
@@ -1106,6 +1106,7 @@ Globe.prototype.beginFrame = function (frameState) {
tileProvider.undergroundColor = this._undergroundColor;
tileProvider.undergroundColorAlphaByDistance = this._undergroundColorAlphaByDistance;
tileProvider.lambertDiffuseMultiplier = this.lambertDiffuseMultiplier;
+
surface.beginFrame(frameState);
}
};
diff --git a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js
index e32b797efc48..a6851b02e469 100644
--- a/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js
+++ b/packages/engine/Source/Scene/GlobeSurfaceTileProvider.js
@@ -2502,7 +2502,9 @@ function addDrawCommandsForTile(tileProvider, tile, frameState) {
uniformMapProperties.localizedTranslucencyRectangle
);
- // For performance, use fog in the shader only when the tile is in fog.
+ // For performance, render fog only when fog is enabled and the effect of
+ // fog would be non-negligible. This prevents the shader from running when
+ // the camera is in space, for example.
const applyFog =
enableFog &&
CesiumMath.fog(tile._distance, frameState.fog.density) >
diff --git a/packages/engine/Source/Scene/Model/AtmospherePipelineStage.js b/packages/engine/Source/Scene/Model/AtmospherePipelineStage.js
new file mode 100644
index 000000000000..46b9e97debda
--- /dev/null
+++ b/packages/engine/Source/Scene/Model/AtmospherePipelineStage.js
@@ -0,0 +1,59 @@
+import Cartesian3 from "../../Core/Cartesian3.js";
+import CesiumMath from "../../Core/Math.js";
+import ShaderDestination from "../../Renderer/ShaderDestination.js";
+import AtmosphereStageFS from "../../Shaders/Model/AtmosphereStageFS.js";
+import AtmosphereStageVS from "../../Shaders/Model/AtmosphereStageVS.js";
+
+/**
+ * The atmosphere pipeline stage applies all earth atmosphere effects that apply
+ * to models, including fog.
+ *
+ * @namespace AtmospherePipelineStage
+ *
+ * @private
+ */
+const AtmospherePipelineStage = {
+ name: "AtmospherePipelineStage", // Helps with debugging
+};
+
+AtmospherePipelineStage.process = function (
+ renderResources,
+ model,
+ frameState
+) {
+ const shaderBuilder = renderResources.shaderBuilder;
+
+ shaderBuilder.addDefine("HAS_ATMOSPHERE", undefined, ShaderDestination.BOTH);
+ shaderBuilder.addDefine(
+ "COMPUTE_POSITION_WC_ATMOSPHERE",
+ undefined,
+ ShaderDestination.BOTH
+ );
+
+ shaderBuilder.addVarying("vec3", "v_atmosphereRayleighColor");
+ shaderBuilder.addVarying("vec3", "v_atmosphereMieColor");
+ shaderBuilder.addVarying("float", "v_atmosphereOpacity");
+
+ shaderBuilder.addVertexLines([AtmosphereStageVS]);
+ shaderBuilder.addFragmentLines([AtmosphereStageFS]);
+
+ // Add a uniform so fog is only calculated when the efcfect would
+ // be non-negligible For example when the camera is in space, fog density decreases
+ // to 0 so fog shouldn't be rendered. Since this state may change rapidly if
+ // the camera is moving, this is implemented as a uniform, not a define.
+ shaderBuilder.addUniform("bool", "u_isInFog", ShaderDestination.FRAGMENT);
+ renderResources.uniformMap.u_isInFog = function () {
+ // We only need a rough measure of distance to the model, so measure
+ // from the camera to the bounding sphere center.
+ const distance = Cartesian3.distance(
+ frameState.camera.positionWC,
+ model.boundingSphere.center
+ );
+
+ return (
+ CesiumMath.fog(distance, frameState.fog.density) > CesiumMath.EPSILON3
+ );
+ };
+};
+
+export default AtmospherePipelineStage;
diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js
index 1a6a56c5e032..33378231ccda 100644
--- a/packages/engine/Source/Scene/Model/Model.js
+++ b/packages/engine/Source/Scene/Model/Model.js
@@ -459,6 +459,8 @@ function Model(options) {
this._projectTo2D = defaultValue(options.projectTo2D, false);
this._enablePick = defaultValue(options.enablePick, false);
+ this._fogRenderable = undefined;
+
this._skipLevelOfDetail = false;
this._ignoreCommands = defaultValue(options.ignoreCommands, false);
@@ -1798,6 +1800,7 @@ Model.prototype.update = function (frameState) {
updateSkipLevelOfDetail(this, frameState);
updateClippingPlanes(this, frameState);
updateSceneMode(this, frameState);
+ updateFog(this, frameState);
updateVerticalExaggeration(this, frameState);
this._defaultTexture = frameState.context.defaultTexture;
@@ -1993,6 +1996,14 @@ function updateSceneMode(model, frameState) {
}
}
+function updateFog(model, frameState) {
+ const fogRenderable = frameState.fog.enabled && frameState.fog.renderable;
+ if (fogRenderable !== model._fogRenderable) {
+ model.resetDrawCommands();
+ model._fogRenderable = fogRenderable;
+ }
+}
+
function updateVerticalExaggeration(model, frameState) {
const verticalExaggerationNeeded = frameState.verticalExaggeration !== 1.0;
if (model._verticalExaggerationOn !== verticalExaggerationNeeded) {
diff --git a/packages/engine/Source/Scene/Model/ModelSceneGraph.js b/packages/engine/Source/Scene/Model/ModelSceneGraph.js
index 1ff305d3adcf..0dacc4c85b5d 100644
--- a/packages/engine/Source/Scene/Model/ModelSceneGraph.js
+++ b/packages/engine/Source/Scene/Model/ModelSceneGraph.js
@@ -9,6 +9,7 @@ import SceneMode from "../SceneMode.js";
import SplitDirection from "../SplitDirection.js";
import buildDrawCommand from "./buildDrawCommand.js";
import TilesetPipelineStage from "./TilesetPipelineStage.js";
+import AtmospherePipelineStage from "./AtmospherePipelineStage.js";
import ImageBasedLightingPipelineStage from "./ImageBasedLightingPipelineStage.js";
import ModelArticulation from "./ModelArticulation.js";
import ModelColorPipelineStage from "./ModelColorPipelineStage.js";
@@ -606,6 +607,7 @@ ModelSceneGraph.prototype.configurePipeline = function (frameState) {
modelPipelineStages.length = 0;
const model = this._model;
+ const fogRenderable = frameState.fog.enabled && frameState.fog.renderable;
if (defined(model.color)) {
modelPipelineStages.push(ModelColorPipelineStage);
@@ -638,6 +640,10 @@ ModelSceneGraph.prototype.configurePipeline = function (frameState) {
if (ModelType.is3DTiles(model.type)) {
modelPipelineStages.push(TilesetPipelineStage);
}
+
+ if (fogRenderable) {
+ modelPipelineStages.push(AtmospherePipelineStage);
+ }
};
ModelSceneGraph.prototype.update = function (frameState, updateForAnimations) {
diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js
index 0571d06eaf3d..1d56d15d9333 100644
--- a/packages/engine/Source/Scene/Scene.js
+++ b/packages/engine/Source/Scene/Scene.js
@@ -37,6 +37,7 @@ import Context from "../Renderer/Context.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import Pass from "../Renderer/Pass.js";
import RenderState from "../Renderer/RenderState.js";
+import Atmosphere from "./Atmosphere.js";
import BrdfLutGenerator from "./BrdfLutGenerator.js";
import Camera from "./Camera.js";
import Cesium3DTilePass from "./Cesium3DTilePass.js";
@@ -46,6 +47,7 @@ import DebugCameraPrimitive from "./DebugCameraPrimitive.js";
import DepthPlane from "./DepthPlane.js";
import DerivedCommand from "./DerivedCommand.js";
import DeviceOrientationCameraController from "./DeviceOrientationCameraController.js";
+import DynamicAtmosphereLightingType from "./DynamicAtmosphereLightingType.js";
import Fog from "./Fog.js";
import FrameState from "./FrameState.js";
import GlobeTranslucencyState from "./GlobeTranslucencyState.js";
@@ -512,6 +514,14 @@ function Scene(options) {
*/
this.cameraEventWaitTime = 500.0;
+ /**
+ * Settings for atmosphere lighting effects affecting 3D Tiles and model rendering. This is not to be confused with
+ * {@link Scene#skyAtmosphere} which is responsible for rendering the sky.
+ *
+ * @type {Atmosphere}
+ */
+ this.atmosphere = new Atmosphere();
+
/**
* Blends the atmosphere to geometry far from the camera for horizon views. Allows for additional
* performance improvements by rendering less geometry and dispatching less terrain requests.
@@ -3167,6 +3177,7 @@ Scene.prototype.updateEnvironment = function () {
const environmentState = this._environmentState;
const renderPass = frameState.passes.render;
const offscreenPass = frameState.passes.offscreen;
+ const atmosphere = this.atmosphere;
const skyAtmosphere = this.skyAtmosphere;
const globe = this.globe;
const globeTranslucencyState = this._globeTranslucencyState;
@@ -3185,17 +3196,19 @@ Scene.prototype.updateEnvironment = function () {
} else {
if (defined(skyAtmosphere)) {
if (defined(globe)) {
- skyAtmosphere.setDynamicAtmosphereColor(
- globe.enableLighting && globe.dynamicAtmosphereLighting,
- globe.dynamicAtmosphereLightingFromSun
+ skyAtmosphere.setDynamicLighting(
+ DynamicAtmosphereLightingType.fromGlobeFlags(globe)
);
environmentState.isReadyForAtmosphere =
environmentState.isReadyForAtmosphere ||
!globe.show ||
globe._surface._tilesToRender.length > 0;
} else {
+ const dynamicLighting = atmosphere.dynamicLighting;
+ skyAtmosphere.setDynamicLighting(dynamicLighting);
environmentState.isReadyForAtmosphere = true;
}
+
environmentState.skyAtmosphereCommand = skyAtmosphere.update(
frameState,
globe
@@ -3757,6 +3770,7 @@ function render(scene) {
}
frameState.backgroundColor = backgroundColor;
+ frameState.atmosphere = scene.atmosphere;
scene.fog.update(frameState);
us.update(frameState);
diff --git a/packages/engine/Source/Scene/SkyAtmosphere.js b/packages/engine/Source/Scene/SkyAtmosphere.js
index 11cccc5f23f5..628737ce5610 100644
--- a/packages/engine/Source/Scene/SkyAtmosphere.js
+++ b/packages/engine/Source/Scene/SkyAtmosphere.js
@@ -217,14 +217,13 @@ Object.defineProperties(SkyAtmosphere.prototype, {
});
/**
+ * Set the dynamic lighting enum value for the shader
+ * @param {DynamicAtmosphereLightingType} lightingEnum The enum that determines the dynamic atmosphere light source
+ *
* @private
*/
-SkyAtmosphere.prototype.setDynamicAtmosphereColor = function (
- enableLighting,
- useSunDirection
-) {
- const lightEnum = enableLighting ? (useSunDirection ? 2.0 : 1.0) : 0.0;
- this._radiiAndDynamicAtmosphereColor.z = lightEnum;
+SkyAtmosphere.prototype.setDynamicLighting = function (lightingEnum) {
+ this._radiiAndDynamicAtmosphereColor.z = lightingEnum;
};
const scratchModelMatrix = new Matrix4();
diff --git a/packages/engine/Source/Shaders/AtmosphereCommon.glsl b/packages/engine/Source/Shaders/AtmosphereCommon.glsl
index 84acc5255659..d68274fa5381 100644
--- a/packages/engine/Source/Shaders/AtmosphereCommon.glsl
+++ b/packages/engine/Source/Shaders/AtmosphereCommon.glsl
@@ -11,14 +11,6 @@ const float ATMOSPHERE_THICKNESS = 111e3; // The thickness of the atmosphere in
const int PRIMARY_STEPS_MAX = 16; // Maximum number of times the ray from the camera to the world position (primary ray) is sampled.
const int LIGHT_STEPS_MAX = 4; // Maximum number of times the light is sampled from the light source's intersection with the atmosphere to a sample position on the primary ray.
-/**
- * Rational approximation to tanh(x)
-*/
-float approximateTanh(float x) {
- float x2 = x * x;
- return max(-1.0, min(+1.0, x * (27.0 + x2) / (27.0 + 9.0 * x2)));
-}
-
/**
* This function computes the colors contributed by Rayliegh and Mie scattering on a given ray, as well as
* the transmittance value for the ray.
@@ -65,8 +57,8 @@ void computeScattering(
float x = 1e-7 * primaryRayAtmosphereIntersect.stop / length(primaryRayLength);
// Value close to 0.0: close to the horizon
// Value close to 1.0: above in the sky
- float w_stop_gt_lprl = 0.5 * (1.0 + approximateTanh(x));
-
+ float w_stop_gt_lprl = 0.5 * (1.0 + czm_approximateTanh(x));
+
// The ray should start from the first intersection with the outer atmopshere, or from the camera position, if it is inside the atmosphere.
float start_0 = primaryRayAtmosphereIntersect.start;
primaryRayAtmosphereIntersect.start = max(primaryRayAtmosphereIntersect.start, 0.0);
@@ -77,7 +69,7 @@ void computeScattering(
// (1) from outer space we have to use more ray steps to get a realistic rendering
// (2) within atmosphere we need fewer steps for faster rendering
float x_o_a = start_0 - ATMOSPHERE_THICKNESS; // ATMOSPHERE_THICKNESS used as an ad-hoc constant, no precise meaning here, only the order of magnitude matters
- float w_inside_atmosphere = 1.0 - 0.5 * (1.0 + approximateTanh(x_o_a));
+ float w_inside_atmosphere = 1.0 - 0.5 * (1.0 + czm_approximateTanh(x_o_a));
int PRIMARY_STEPS = PRIMARY_STEPS_MAX - int(w_inside_atmosphere * 12.0); // Number of times the ray from the camera to the world position (primary ray) is sampled.
int LIGHT_STEPS = LIGHT_STEPS_MAX - int(w_inside_atmosphere * 2.0); // Number of times the light is sampled from the light source's intersection with the atmosphere to a sample position on the primary ray.
diff --git a/packages/engine/Source/Shaders/Builtin/Functions/applyHSBShift.glsl b/packages/engine/Source/Shaders/Builtin/Functions/applyHSBShift.glsl
new file mode 100644
index 000000000000..55ee1592a8d6
--- /dev/null
+++ b/packages/engine/Source/Shaders/Builtin/Functions/applyHSBShift.glsl
@@ -0,0 +1,32 @@
+/**
+ * Apply a HSB color shift to an RGB color.
+ *
+ * @param {vec3} rgb The color in RGB space.
+ * @param {vec3} hsbShift The amount to shift each component. The xyz components correspond to hue, saturation, and brightness. Shifting the hue by +/- 1.0 corresponds to shifting the hue by a full cycle. Saturation and brightness are clamped between 0 and 1 after the adjustment
+ * @param {bool} ignoreBlackPixels If true, black pixels will be unchanged. This is necessary in some shaders such as atmosphere-related effects.
+ *
+ * @return {vec3} The RGB color after shifting in HSB space and clamping saturation and brightness to a valid range.
+ */
+vec3 czm_applyHSBShift(vec3 rgb, vec3 hsbShift, bool ignoreBlackPixels) {
+ // Convert rgb color to hsb
+ vec3 hsb = czm_RGBToHSB(rgb);
+
+ // Perform hsb shift
+ // Hue cycles around so no clamp is needed.
+ hsb.x += hsbShift.x; // hue
+ hsb.y = clamp(hsb.y + hsbShift.y, 0.0, 1.0); // saturation
+
+ // brightness
+ //
+ // Some shaders such as atmosphere-related effects need to leave black
+ // pixels unchanged
+ if (ignoreBlackPixels) {
+ hsb.z = hsb.z > czm_epsilon7 ? hsb.z + hsbShift.z : 0.0;
+ } else {
+ hsb.z = hsb.z + hsbShift.z;
+ }
+ hsb.z = clamp(hsb.z, 0.0, 1.0);
+
+ // Convert shifted hsb back to rgb
+ return czm_HSBToRGB(hsb);
+}
diff --git a/packages/engine/Source/Shaders/Builtin/Functions/approximateTanh.glsl b/packages/engine/Source/Shaders/Builtin/Functions/approximateTanh.glsl
new file mode 100644
index 000000000000..4c8132762cf9
--- /dev/null
+++ b/packages/engine/Source/Shaders/Builtin/Functions/approximateTanh.glsl
@@ -0,0 +1,10 @@
+/**
+ * Compute a rational approximation to tanh(x)
+ *
+ * @param {float} x A real number input
+ * @returns {float} An approximation for tanh(x)
+*/
+float czm_approximateTanh(float x) {
+ float x2 = x * x;
+ return max(-1.0, min(1.0, x * (27.0 + x2) / (27.0 + 9.0 * x2)));
+}
diff --git a/packages/engine/Source/Shaders/Builtin/Functions/computeAtmosphereColor.glsl b/packages/engine/Source/Shaders/Builtin/Functions/computeAtmosphereColor.glsl
new file mode 100644
index 000000000000..d4cc21971e7a
--- /dev/null
+++ b/packages/engine/Source/Shaders/Builtin/Functions/computeAtmosphereColor.glsl
@@ -0,0 +1,44 @@
+/**
+ * Compute the atmosphere color, applying Rayleigh and Mie scattering. This
+ * builtin uses automatic uniforms so the atmophere settings are synced with the
+ * state of the Scene, even in other contexts like Model.
+ *
+ * @name czm_computeAtmosphereColor
+ * @glslFunction
+ *
+ * @param {vec3} positionWC Position of the fragment in world coords (low precision)
+ * @param {vec3} lightDirection Light direction from the sun or other light source.
+ * @param {vec3} rayleighColor The Rayleigh scattering color computed by a scattering function
+ * @param {vec3} mieColor The Mie scattering color computed by a scattering function
+ * @param {float} opacity The opacity computed by a scattering function.
+ */
+vec4 czm_computeAtmosphereColor(
+ vec3 positionWC,
+ vec3 lightDirection,
+ vec3 rayleighColor,
+ vec3 mieColor,
+ float opacity
+) {
+ // Setup the primary ray: from the camera position to the vertex position.
+ vec3 cameraToPositionWC = positionWC - czm_viewerPositionWC;
+ vec3 cameraToPositionWCDirection = normalize(cameraToPositionWC);
+
+ float cosAngle = dot(cameraToPositionWCDirection, lightDirection);
+ float cosAngleSq = cosAngle * cosAngle;
+
+ float G = czm_atmosphereMieAnisotropy;
+ float GSq = G * G;
+
+ // The Rayleigh phase function.
+ float rayleighPhase = 3.0 / (50.2654824574) * (1.0 + cosAngleSq);
+ // The Mie phase function.
+ float miePhase = 3.0 / (25.1327412287) * ((1.0 - GSq) * (cosAngleSq + 1.0)) / (pow(1.0 + GSq - 2.0 * cosAngle * G, 1.5) * (2.0 + GSq));
+
+ // The final color is generated by combining the effects of the Rayleigh and Mie scattering.
+ vec3 rayleigh = rayleighPhase * rayleighColor;
+ vec3 mie = miePhase * mieColor;
+
+ vec3 color = (rayleigh + mie) * czm_atmosphereLightIntensity;
+
+ return vec4(color, opacity);
+}
diff --git a/packages/engine/Source/Shaders/Builtin/Functions/computeGroundAtmosphereScattering.glsl b/packages/engine/Source/Shaders/Builtin/Functions/computeGroundAtmosphereScattering.glsl
new file mode 100644
index 000000000000..d8b172e3b259
--- /dev/null
+++ b/packages/engine/Source/Shaders/Builtin/Functions/computeGroundAtmosphereScattering.glsl
@@ -0,0 +1,30 @@
+/**
+ * Compute atmosphere scattering for the ground atmosphere and fog. This method
+ * uses automatic uniforms so it is always synced with the scene settings.
+ *
+ * @name czm_computeGroundAtmosphereScattering
+ * @glslfunction
+ *
+ * @param {vec3} positionWC The position of the fragment in world coordinates.
+ * @param {vec3} lightDirection The direction of the light to calculate the scattering from.
+ * @param {vec3} rayleighColor The variable the Rayleigh scattering will be written to.
+ * @param {vec3} mieColor The variable the Mie scattering will be written to.
+ * @param {float} opacity The variable the transmittance will be written to.
+ */
+void czm_computeGroundAtmosphereScattering(vec3 positionWC, vec3 lightDirection, out vec3 rayleighColor, out vec3 mieColor, out float opacity) {
+ vec3 cameraToPositionWC = positionWC - czm_viewerPositionWC;
+ vec3 cameraToPositionWCDirection = normalize(cameraToPositionWC);
+ czm_ray primaryRay = czm_ray(czm_viewerPositionWC, cameraToPositionWCDirection);
+
+ float atmosphereInnerRadius = length(positionWC);
+
+ czm_computeScattering(
+ primaryRay,
+ length(cameraToPositionWC),
+ lightDirection,
+ atmosphereInnerRadius,
+ rayleighColor,
+ mieColor,
+ opacity
+ );
+}
diff --git a/packages/engine/Source/Shaders/Builtin/Functions/computeScattering.glsl b/packages/engine/Source/Shaders/Builtin/Functions/computeScattering.glsl
new file mode 100644
index 000000000000..76edcf9ed525
--- /dev/null
+++ b/packages/engine/Source/Shaders/Builtin/Functions/computeScattering.glsl
@@ -0,0 +1,150 @@
+/**
+ * This function computes the colors contributed by Rayliegh and Mie scattering on a given ray, as well as
+ * the transmittance value for the ray. This function uses automatic uniforms
+ * so the atmosphere settings are always synced with the current scene.
+ *
+ * @name czm_computeScattering
+ * @glslfunction
+ *
+ * @param {czm_ray} primaryRay The ray from the camera to the position.
+ * @param {float} primaryRayLength The length of the primary ray.
+ * @param {vec3} lightDirection The direction of the light to calculate the scattering from.
+ * @param {vec3} rayleighColor The variable the Rayleigh scattering will be written to.
+ * @param {vec3} mieColor The variable the Mie scattering will be written to.
+ * @param {float} opacity The variable the transmittance will be written to.
+ */
+void czm_computeScattering(
+ czm_ray primaryRay,
+ float primaryRayLength,
+ vec3 lightDirection,
+ float atmosphereInnerRadius,
+ out vec3 rayleighColor,
+ out vec3 mieColor,
+ out float opacity
+) {
+ const float ATMOSPHERE_THICKNESS = 111e3; // The thickness of the atmosphere in meters.
+ const int PRIMARY_STEPS_MAX = 16; // Maximum number of times the ray from the camera to the world position (primary ray) is sampled.
+ const int LIGHT_STEPS_MAX = 4; // Maximum number of times the light is sampled from the light source's intersection with the atmosphere to a sample position on the primary ray.
+
+ // Initialize the default scattering amounts to 0.
+ rayleighColor = vec3(0.0);
+ mieColor = vec3(0.0);
+ opacity = 0.0;
+
+ float atmosphereOuterRadius = atmosphereInnerRadius + ATMOSPHERE_THICKNESS;
+
+ vec3 origin = vec3(0.0);
+
+ // Calculate intersection from the camera to the outer ring of the atmosphere.
+ czm_raySegment primaryRayAtmosphereIntersect = czm_raySphereIntersectionInterval(primaryRay, origin, atmosphereOuterRadius);
+
+ // Return empty colors if no intersection with the atmosphere geometry.
+ if (primaryRayAtmosphereIntersect == czm_emptyRaySegment) {
+ rayleighColor = vec3(1.0, 0.0, 1.0);
+ return;
+ }
+
+ // To deal with smaller values of PRIMARY_STEPS (e.g. 4)
+ // we implement a split strategy: sky or horizon.
+ // For performance reasons, instead of a if/else branch
+ // a soft choice is implemented through a weight 0.0 <= w_stop_gt_lprl <= 1.0
+ float x = 1e-7 * primaryRayAtmosphereIntersect.stop / length(primaryRayLength);
+ // Value close to 0.0: close to the horizon
+ // Value close to 1.0: above in the sky
+ float w_stop_gt_lprl = 0.5 * (1.0 + czm_approximateTanh(x));
+
+ // The ray should start from the first intersection with the outer atmopshere, or from the camera position, if it is inside the atmosphere.
+ float start_0 = primaryRayAtmosphereIntersect.start;
+ primaryRayAtmosphereIntersect.start = max(primaryRayAtmosphereIntersect.start, 0.0);
+ // The ray should end at the exit from the atmosphere or at the distance to the vertex, whichever is smaller.
+ primaryRayAtmosphereIntersect.stop = min(primaryRayAtmosphereIntersect.stop, length(primaryRayLength));
+
+ // For the number of ray steps, distinguish inside or outside atmosphere (outer space)
+ // (1) from outer space we have to use more ray steps to get a realistic rendering
+ // (2) within atmosphere we need fewer steps for faster rendering
+ float x_o_a = start_0 - ATMOSPHERE_THICKNESS; // ATMOSPHERE_THICKNESS used as an ad-hoc constant, no precise meaning here, only the order of magnitude matters
+ float w_inside_atmosphere = 1.0 - 0.5 * (1.0 + czm_approximateTanh(x_o_a));
+ int PRIMARY_STEPS = PRIMARY_STEPS_MAX - int(w_inside_atmosphere * 12.0); // Number of times the ray from the camera to the world position (primary ray) is sampled.
+ int LIGHT_STEPS = LIGHT_STEPS_MAX - int(w_inside_atmosphere * 2.0); // Number of times the light is sampled from the light source's intersection with the atmosphere to a sample position on the primary ray.
+
+ // Setup for sampling positions along the ray - starting from the intersection with the outer ring of the atmosphere.
+ float rayPositionLength = primaryRayAtmosphereIntersect.start;
+ // (1) Outside the atmosphere: constant rayStepLength
+ // (2) Inside atmosphere: variable rayStepLength to compensate the rough rendering of the smaller number of ray steps
+ float totalRayLength = primaryRayAtmosphereIntersect.stop - rayPositionLength;
+ float rayStepLengthIncrease = w_inside_atmosphere * ((1.0 - w_stop_gt_lprl) * totalRayLength / (float(PRIMARY_STEPS * (PRIMARY_STEPS + 1)) / 2.0));
+ float rayStepLength = max(1.0 - w_inside_atmosphere, w_stop_gt_lprl) * totalRayLength / max(7.0 * w_inside_atmosphere, float(PRIMARY_STEPS));
+
+ vec3 rayleighAccumulation = vec3(0.0);
+ vec3 mieAccumulation = vec3(0.0);
+ vec2 opticalDepth = vec2(0.0);
+ vec2 heightScale = vec2(czm_atmosphereRayleighScaleHeight, czm_atmosphereMieScaleHeight);
+
+ // Sample positions on the primary ray.
+ for (int i = 0; i < PRIMARY_STEPS_MAX; ++i) {
+
+ // The loop should be: for (int i = 0; i < PRIMARY_STEPS; ++i) {...} but WebGL1 cannot
+ // loop with non-constant condition, so it has to break early instead
+ if (i >= PRIMARY_STEPS) {
+ break;
+ }
+
+ // Calculate sample position along viewpoint ray.
+ vec3 samplePosition = primaryRay.origin + primaryRay.direction * (rayPositionLength + rayStepLength);
+
+ // Calculate height of sample position above ellipsoid.
+ float sampleHeight = length(samplePosition) - atmosphereInnerRadius;
+
+ // Calculate and accumulate density of particles at the sample position.
+ vec2 sampleDensity = exp(-sampleHeight / heightScale) * rayStepLength;
+ opticalDepth += sampleDensity;
+
+ // Generate ray from the sample position segment to the light source, up to the outer ring of the atmosphere.
+ czm_ray lightRay = czm_ray(samplePosition, lightDirection);
+ czm_raySegment lightRayAtmosphereIntersect = czm_raySphereIntersectionInterval(lightRay, origin, atmosphereOuterRadius);
+
+ float lightStepLength = lightRayAtmosphereIntersect.stop / float(LIGHT_STEPS);
+ float lightPositionLength = 0.0;
+
+ vec2 lightOpticalDepth = vec2(0.0);
+
+ // Sample positions along the light ray, to accumulate incidence of light on the latest sample segment.
+ for (int j = 0; j < LIGHT_STEPS_MAX; ++j) {
+
+ // The loop should be: for (int j = 0; i < LIGHT_STEPS; ++j) {...} but WebGL1 cannot
+ // loop with non-constant condition, so it has to break early instead
+ if (j >= LIGHT_STEPS) {
+ break;
+ }
+
+ // Calculate sample position along light ray.
+ vec3 lightPosition = samplePosition + lightDirection * (lightPositionLength + lightStepLength * 0.5);
+
+ // Calculate height of the light sample position above ellipsoid.
+ float lightHeight = length(lightPosition) - atmosphereInnerRadius;
+
+ // Calculate density of photons at the light sample position.
+ lightOpticalDepth += exp(-lightHeight / heightScale) * lightStepLength;
+
+ // Increment distance on light ray.
+ lightPositionLength += lightStepLength;
+ }
+
+ // Compute attenuation via the primary ray and the light ray.
+ vec3 attenuation = exp(-((czm_atmosphereMieCoefficient * (opticalDepth.y + lightOpticalDepth.y)) + (czm_atmosphereRayleighCoefficient * (opticalDepth.x + lightOpticalDepth.x))));
+
+ // Accumulate the scattering.
+ rayleighAccumulation += sampleDensity.x * attenuation;
+ mieAccumulation += sampleDensity.y * attenuation;
+
+ // Increment distance on primary ray.
+ rayPositionLength += (rayStepLength += rayStepLengthIncrease);
+ }
+
+ // Compute the scattering amount.
+ rayleighColor = czm_atmosphereRayleighCoefficient * rayleighAccumulation;
+ mieColor = czm_atmosphereMieCoefficient * mieAccumulation;
+
+ // Compute the transmittance i.e. how much light is passing through the atmosphere.
+ opacity = length(exp(-((czm_atmosphereMieCoefficient * opticalDepth.y) + (czm_atmosphereRayleighCoefficient * opticalDepth.x))));
+}
diff --git a/packages/engine/Source/Shaders/Builtin/Functions/getDynamicAtmosphereLightDirection.glsl b/packages/engine/Source/Shaders/Builtin/Functions/getDynamicAtmosphereLightDirection.glsl
new file mode 100644
index 000000000000..70063302734f
--- /dev/null
+++ b/packages/engine/Source/Shaders/Builtin/Functions/getDynamicAtmosphereLightDirection.glsl
@@ -0,0 +1,22 @@
+/**
+ * Select which direction vector to use for dynamic atmosphere lighting based on an enum value
+ *
+ * @name czm_getDynamicAtmosphereLightDirection
+ * @glslfunction
+ * @see DynamicAtmosphereLightingType.js
+ *
+ * @param {vec3} positionWC the position of the vertex/fragment in world coordinates. This is normalized and returned when dynamic lighting is turned off.
+ * @param {float} lightEnum The enum value for selecting between light sources.
+ * @return {vec3} The normalized light direction vector. Depending on the enum value, it is either positionWC, czm_lightDirectionWC or czm_sunDirectionWC
+ */
+vec3 czm_getDynamicAtmosphereLightDirection(vec3 positionWC, float lightEnum) {
+ const float NONE = 0.0;
+ const float SCENE_LIGHT = 1.0;
+ const float SUNLIGHT = 2.0;
+
+ vec3 lightDirection =
+ positionWC * float(lightEnum == NONE) +
+ czm_lightDirectionWC * float(lightEnum == SCENE_LIGHT) +
+ czm_sunDirectionWC * float(lightEnum == SUNLIGHT);
+ return normalize(lightDirection);
+}
diff --git a/packages/engine/Source/Shaders/GlobeFS.glsl b/packages/engine/Source/Shaders/GlobeFS.glsl
index bde6ce15c82c..74c568efb406 100644
--- a/packages/engine/Source/Shaders/GlobeFS.glsl
+++ b/packages/engine/Source/Shaders/GlobeFS.glsl
@@ -268,20 +268,6 @@ vec4 sampleAndBlend(
return vec4(outColor, max(outAlpha, 0.0));
}
-vec3 colorCorrect(vec3 rgb) {
-#ifdef COLOR_CORRECT
- // Convert rgb color to hsb
- vec3 hsb = czm_RGBToHSB(rgb);
- // Perform hsb shift
- hsb.x += u_hsbShift.x; // hue
- hsb.y = clamp(hsb.y + u_hsbShift.y, 0.0, 1.0); // saturation
- hsb.z = hsb.z > czm_epsilon7 ? hsb.z + u_hsbShift.z : 0.0; // brightness
- // Convert shifted hsb back to rgb
- rgb = czm_HSBToRGB(hsb);
-#endif
- return rgb;
-}
-
vec4 computeDayColor(vec4 initialColor, vec3 textureCoordinates, float nightBlend);
vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat3 enuToEye, vec4 imageryColor, float specularMapValue, float fade);
@@ -396,7 +382,7 @@ void main()
materialInput.st = v_textureCoordinates.st;
materialInput.normalEC = normalize(v_normalEC);
materialInput.positionToEyeEC = -v_positionEC;
- materialInput.tangentToEyeMatrix = czm_eastNorthUpToEyeCoordinates(v_positionMC, normalize(v_normalEC));
+ materialInput.tangentToEyeMatrix = czm_eastNorthUpToEyeCoordinates(v_positionMC, normalize(v_normalEC));
materialInput.slope = v_slope;
materialInput.height = v_height;
materialInput.aspect = v_aspect;
@@ -442,7 +428,7 @@ void main()
{
bool dynamicLighting = false;
#if defined(DYNAMIC_ATMOSPHERE_LIGHTING) && (defined(ENABLE_DAYNIGHT_SHADING) || defined(ENABLE_VERTEX_LIGHTING))
- dynamicLighting = true;
+ dynamicLighting = true;
#endif
vec3 rayleighColor;
@@ -472,30 +458,35 @@ void main()
opacity = v_atmosphereOpacity;
#endif
- rayleighColor = colorCorrect(rayleighColor);
- mieColor = colorCorrect(mieColor);
+ #ifdef COLOR_CORRECT
+ const bool ignoreBlackPixels = true;
+ rayleighColor = czm_applyHSBShift(rayleighColor, u_hsbShift, ignoreBlackPixels);
+ mieColor = czm_applyHSBShift(mieColor, u_hsbShift, ignoreBlackPixels);
+ #endif
vec4 groundAtmosphereColor = computeAtmosphereColor(positionWC, lightDirection, rayleighColor, mieColor, opacity);
// Fog is applied to tiles selected for fog, close to the Earth.
#ifdef FOG
vec3 fogColor = groundAtmosphereColor.rgb;
-
+
// If there is lighting, apply that to the fog.
#if defined(DYNAMIC_ATMOSPHERE_LIGHTING) && (defined(ENABLE_VERTEX_LIGHTING) || defined(ENABLE_DAYNIGHT_SHADING))
float darken = clamp(dot(normalize(czm_viewerPositionWC), atmosphereLightDirection), u_minimumBrightness, 1.0);
- fogColor *= darken;
+ fogColor *= darken;
#endif
#ifndef HDR
fogColor.rgb = czm_acesTonemapping(fogColor.rgb);
fogColor.rgb = czm_inverseGamma(fogColor.rgb);
#endif
-
+
const float modifier = 0.15;
finalColor = vec4(czm_fog(v_distance, finalColor.rgb, fogColor.rgb, modifier), finalColor.a);
#else
+ // Apply ground atmosphere. This happens when the camera is far away from the earth.
+
// The transmittance is based on optical depth i.e. the length of segment of the ray inside the atmosphere.
// This value is larger near the "circumference", as it is further away from the camera. We use it to
// brighten up that area of the ground atmosphere.
@@ -507,20 +498,20 @@ void main()
#if defined(DYNAMIC_ATMOSPHERE_LIGHTING) && (defined(ENABLE_VERTEX_LIGHTING) || defined(ENABLE_DAYNIGHT_SHADING))
float fadeInDist = u_nightFadeDistance.x;
float fadeOutDist = u_nightFadeDistance.y;
-
+
float sunlitAtmosphereIntensity = clamp((cameraDist - fadeOutDist) / (fadeInDist - fadeOutDist), 0.05, 1.0);
float darken = clamp(dot(normalize(positionWC), atmosphereLightDirection), 0.0, 1.0);
vec3 darkenendGroundAtmosphereColor = mix(groundAtmosphereColor.rgb, finalAtmosphereColor.rgb, darken);
finalAtmosphereColor = mix(darkenendGroundAtmosphereColor, finalAtmosphereColor, sunlitAtmosphereIntensity);
#endif
-
+
#ifndef HDR
finalAtmosphereColor.rgb = vec3(1.0) - exp(-fExposure * finalAtmosphereColor.rgb);
#else
finalAtmosphereColor.rgb = czm_saturation(finalAtmosphereColor.rgb, 1.6);
#endif
-
+
finalColor.rgb = mix(finalColor.rgb, finalAtmosphereColor.rgb, fade);
#endif
}
@@ -544,7 +535,7 @@ void main()
finalColor.a *= interpolateByDistance(alphaByDistance, v_distance);
}
#endif
-
+
out_FragColor = finalColor;
}
diff --git a/packages/engine/Source/Shaders/Model/AtmosphereStageFS.glsl b/packages/engine/Source/Shaders/Model/AtmosphereStageFS.glsl
new file mode 100644
index 000000000000..3a0b20e86fa0
--- /dev/null
+++ b/packages/engine/Source/Shaders/Model/AtmosphereStageFS.glsl
@@ -0,0 +1,111 @@
+// 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
+//
+// This version uses only a single iteration for best performance. For fog
+// rendering, the difference is negligible.
+vec2 nearestPointOnEllipseFast(vec2 pos, vec2 radii) {
+ vec2 p = abs(pos);
+ vec2 inverseRadii = 1.0 / radii;
+ vec2 evoluteScale = (radii.x * radii.x - radii.y * radii.y) * vec2(1.0, -1.0) * inverseRadii;
+
+ // 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 = cos(pi/4)
+ vec2 tTrigs = vec2(0.70710678118);
+ vec2 v = radii * tTrigs;
+
+ // Find the evolute of the ellipse (center of curvature) at v.
+ vec2 evolute = evoluteScale * tTrigs * tTrigs * tTrigs;
+ // 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;
+
+ return v * sign(pos);
+}
+
+vec3 computeEllipsoidPositionWC(vec3 positionMC) {
+ // Get the world-space position and project onto a meridian plane of
+ // the ellipsoid
+ vec3 positionWC = (czm_model * vec4(positionMC, 1.0)).xyz;
+
+ vec2 positionEllipse = vec2(length(positionWC.xy), positionWC.z);
+ vec2 nearestPoint = nearestPointOnEllipseFast(positionEllipse, czm_ellipsoidRadii.xz);
+
+ // Reconstruct a 3D point in world space
+ return vec3(nearestPoint.x * normalize(positionWC.xy), nearestPoint.y);
+}
+
+void applyFog(inout vec4 color, vec4 groundAtmosphereColor, vec3 lightDirection, float distanceToCamera) {
+
+ vec3 fogColor = groundAtmosphereColor.rgb;
+
+ // If there is dynamic lighting, apply that to the fog.
+ const float NONE = 0.0;
+ if (czm_atmosphereDynamicLighting != NONE) {
+ float darken = clamp(dot(normalize(czm_viewerPositionWC), lightDirection), czm_fogMinimumBrightness, 1.0);
+ fogColor *= darken;
+ }
+
+ // Tonemap if HDR rendering is disabled
+ #ifndef HDR
+ fogColor.rgb = czm_acesTonemapping(fogColor.rgb);
+ fogColor.rgb = czm_inverseGamma(fogColor.rgb);
+ #endif
+
+ // Matches the constant in GlobeFS.glsl. This makes the fog falloff
+ // more gradual.
+ const float fogModifier = 0.15;
+ vec3 withFog = czm_fog(distanceToCamera, color.rgb, fogColor, fogModifier);
+ color = vec4(withFog, color.a);
+}
+
+void atmosphereStage(inout vec4 color, in ProcessedAttributes attributes) {
+ vec3 rayleighColor;
+ vec3 mieColor;
+ float opacity;
+
+ vec3 positionWC;
+ vec3 lightDirection;
+
+ // When the camera is in space, compute the position per-fragment for
+ // more accurate ground atmosphere. All other cases will use
+ //
+ // The if condition will be added in https://github.com/CesiumGS/cesium/issues/11717
+ if (false) {
+ positionWC = computeEllipsoidPositionWC(attributes.positionMC);
+ lightDirection = czm_getDynamicAtmosphereLightDirection(positionWC, czm_atmosphereDynamicLighting);
+
+ // The fog color is derived from the ground atmosphere color
+ czm_computeGroundAtmosphereScattering(
+ positionWC,
+ lightDirection,
+ rayleighColor,
+ mieColor,
+ opacity
+ );
+ } else {
+ positionWC = attributes.positionWC;
+ lightDirection = czm_getDynamicAtmosphereLightDirection(positionWC, czm_atmosphereDynamicLighting);
+ rayleighColor = v_atmosphereRayleighColor;
+ mieColor = v_atmosphereMieColor;
+ opacity = v_atmosphereOpacity;
+ }
+
+ //color correct rayleigh and mie colors
+ const bool ignoreBlackPixels = true;
+ rayleighColor = czm_applyHSBShift(rayleighColor, czm_atmosphereHsbShift, ignoreBlackPixels);
+ mieColor = czm_applyHSBShift(mieColor, czm_atmosphereHsbShift, ignoreBlackPixels);
+
+ vec4 groundAtmosphereColor = czm_computeAtmosphereColor(positionWC, lightDirection, rayleighColor, mieColor, opacity);
+
+ if (u_isInFog) {
+ float distanceToCamera = length(attributes.positionEC);
+ applyFog(color, groundAtmosphereColor, lightDirection, distanceToCamera);
+ } else {
+ // Ground atmosphere
+ }
+}
diff --git a/packages/engine/Source/Shaders/Model/AtmosphereStageVS.glsl b/packages/engine/Source/Shaders/Model/AtmosphereStageVS.glsl
new file mode 100644
index 000000000000..ad5809f50d9f
--- /dev/null
+++ b/packages/engine/Source/Shaders/Model/AtmosphereStageVS.glsl
@@ -0,0 +1,12 @@
+void atmosphereStage(ProcessedAttributes attributes) {
+ vec3 lightDirection = czm_getDynamicAtmosphereLightDirection(v_positionWC, czm_atmosphereDynamicLighting);
+
+ czm_computeGroundAtmosphereScattering(
+ // This assumes the geometry stage came before this.
+ v_positionWC,
+ lightDirection,
+ v_atmosphereRayleighColor,
+ v_atmosphereMieColor,
+ v_atmosphereOpacity
+ );
+}
diff --git a/packages/engine/Source/Shaders/Model/GeometryStageFS.glsl b/packages/engine/Source/Shaders/Model/GeometryStageFS.glsl
index 4e52d7b2087d..6f5217a11ba3 100644
--- a/packages/engine/Source/Shaders/Model/GeometryStageFS.glsl
+++ b/packages/engine/Source/Shaders/Model/GeometryStageFS.glsl
@@ -3,7 +3,7 @@ void geometryStage(out ProcessedAttributes attributes)
attributes.positionMC = v_positionMC;
attributes.positionEC = v_positionEC;
- #ifdef COMPUTE_POSITION_WC_CUSTOM_SHADER
+ #if defined(COMPUTE_POSITION_WC_CUSTOM_SHADER) || defined(COMPUTE_POSITION_WC_STYLE) || defined(COMPUTE_POSITION_WC_ATMOSPHERE)
attributes.positionWC = v_positionWC;
#endif
diff --git a/packages/engine/Source/Shaders/Model/GeometryStageVS.glsl b/packages/engine/Source/Shaders/Model/GeometryStageVS.glsl
index 2cda604bb8d9..58ebdff53291 100644
--- a/packages/engine/Source/Shaders/Model/GeometryStageVS.glsl
+++ b/packages/engine/Source/Shaders/Model/GeometryStageVS.glsl
@@ -1,4 +1,4 @@
-vec4 geometryStage(inout ProcessedAttributes attributes, mat4 modelView, mat3 normal)
+vec4 geometryStage(inout ProcessedAttributes attributes, mat4 modelView, mat3 normal)
{
vec4 computedPosition;
@@ -16,7 +16,7 @@ vec4 geometryStage(inout ProcessedAttributes attributes, mat4 modelView, mat3 no
#endif
// Sometimes the custom shader and/or style needs this
- #if defined(COMPUTE_POSITION_WC_CUSTOM_SHADER) || defined(COMPUTE_POSITION_WC_STYLE)
+ #if defined(COMPUTE_POSITION_WC_CUSTOM_SHADER) || defined(COMPUTE_POSITION_WC_STYLE) || defined(COMPUTE_POSITION_WC_ATMOSPHERE)
// Note that this is a 32-bit position which may result in jitter on small
// scales.
v_positionWC = (czm_model * vec4(positionMC, 1.0)).xyz;
@@ -27,7 +27,7 @@ vec4 geometryStage(inout ProcessedAttributes attributes, mat4 modelView, mat3 no
#endif
#ifdef HAS_TANGENTS
- v_tangentEC = normalize(normal * attributes.tangentMC);
+ v_tangentEC = normalize(normal * attributes.tangentMC);
#endif
#ifdef HAS_BITANGENTS
@@ -37,6 +37,6 @@ vec4 geometryStage(inout ProcessedAttributes attributes, mat4 modelView, mat3 no
// All other varyings need to be dynamically generated in
// GeometryPipelineStage
setDynamicVaryings(attributes);
-
+
return computedPosition;
}
diff --git a/packages/engine/Source/Shaders/Model/ModelColorStageFS.glsl b/packages/engine/Source/Shaders/Model/ModelColorStageFS.glsl
index ad03772237e2..099fc0f6fcb6 100644
--- a/packages/engine/Source/Shaders/Model/ModelColorStageFS.glsl
+++ b/packages/engine/Source/Shaders/Model/ModelColorStageFS.glsl
@@ -4,4 +4,4 @@ void modelColorStage(inout czm_modelMaterial material)
float highlight = ceil(model_colorBlend);
material.diffuse *= mix(model_color.rgb, vec3(1.0), highlight);
material.alpha *= model_color.a;
-}
\ No newline at end of file
+}
diff --git a/packages/engine/Source/Shaders/Model/ModelFS.glsl b/packages/engine/Source/Shaders/Model/ModelFS.glsl
index f56da251646a..13d2aec6663e 100644
--- a/packages/engine/Source/Shaders/Model/ModelFS.glsl
+++ b/packages/engine/Source/Shaders/Model/ModelFS.glsl
@@ -79,5 +79,9 @@ void main()
silhouetteStage(color);
#endif
+ #ifdef HAS_ATMOSPHERE
+ atmosphereStage(color, attributes);
+ #endif
+
out_FragColor = color;
}
diff --git a/packages/engine/Source/Shaders/Model/ModelVS.glsl b/packages/engine/Source/Shaders/Model/ModelVS.glsl
index e9a4eb5e63ec..36a65de3e1e3 100644
--- a/packages/engine/Source/Shaders/Model/ModelVS.glsl
+++ b/packages/engine/Source/Shaders/Model/ModelVS.glsl
@@ -7,7 +7,7 @@ czm_modelVertexOutput defaultVertexOutput(vec3 positionMC) {
return vsOutput;
}
-void main()
+void main()
{
// Initialize the attributes struct with all
// attributes except quantized ones.
@@ -66,14 +66,14 @@ void main()
// Update the position for this instance in place
#ifdef HAS_INSTANCING
- // The legacy instance stage is used when rendering i3dm models that
+ // The legacy instance stage is used when rendering i3dm models that
// encode instances transforms in world space, as opposed to glTF models
// that use EXT_mesh_gpu_instancing, where instance transforms are encoded
// in object space.
#ifdef USE_LEGACY_INSTANCING
mat4 instanceModelView;
mat3 instanceModelViewInverseTranspose;
-
+
legacyInstancingStage(attributes, instanceModelView, instanceModelViewInverseTranspose);
modelView = instanceModelView;
@@ -104,7 +104,12 @@ void main()
// Compute the final position in each coordinate system needed.
// This returns the value that will be assigned to gl_Position.
- vec4 positionClip = geometryStage(attributes, modelView, normal);
+ vec4 positionClip = geometryStage(attributes, modelView, normal);
+
+ // This must go after the geometry stage as it needs v_positionWC
+ #ifdef HAS_ATMOSPHERE
+ atmosphereStage(attributes);
+ #endif
#ifdef HAS_SILHOUETTE
silhouetteStage(attributes, positionClip);
diff --git a/packages/engine/Source/Shaders/SkyAtmosphereCommon.glsl b/packages/engine/Source/Shaders/SkyAtmosphereCommon.glsl
index 0c27d89f9311..9a958474b9d4 100644
--- a/packages/engine/Source/Shaders/SkyAtmosphereCommon.glsl
+++ b/packages/engine/Source/Shaders/SkyAtmosphereCommon.glsl
@@ -8,16 +8,6 @@ float interpolateByDistance(vec4 nearFarScalar, float distance)
return mix(startValue, endValue, t);
}
-vec3 getLightDirection(vec3 positionWC)
-{
- float lightEnum = u_radiiAndDynamicAtmosphereColor.z;
- vec3 lightDirection =
- positionWC * float(lightEnum == 0.0) +
- czm_lightDirectionWC * float(lightEnum == 1.0) +
- czm_sunDirectionWC * float(lightEnum == 2.0);
- return normalize(lightDirection);
-}
-
void computeAtmosphereScattering(vec3 positionWC, vec3 lightDirection, out vec3 rayleighColor, out vec3 mieColor, out float opacity, out float underTranslucentGlobe)
{
float ellipsoidRadiiDifference = czm_ellipsoidRadii.x - czm_ellipsoidRadii.z;
@@ -28,7 +18,7 @@ void computeAtmosphereScattering(vec3 positionWC, vec3 lightDirection, out vec3
float distanceAdjustModifier = ellipsoidRadiiDifference / 2.0;
float distanceAdjust = distanceAdjustModifier * clamp((czm_eyeHeight - distanceAdjustMin) / (distanceAdjustMax - distanceAdjustMin), 0.0, 1.0);
- // Since atmosphere scattering assumes the atmosphere is a spherical shell, we compute an inner radius of the atmosphere best fit
+ // Since atmosphere scattering assumes the atmosphere is a spherical shell, we compute an inner radius of the atmosphere best fit
// for the position on the ellipsoid.
float radiusAdjust = (ellipsoidRadiiDifference / 4.0) + distanceAdjust;
float atmosphereInnerRadius = (length(czm_viewerPositionWC) - czm_eyeHeight) - radiusAdjust;
@@ -46,7 +36,7 @@ void computeAtmosphereScattering(vec3 positionWC, vec3 lightDirection, out vec3
// Check for intersection with the inner radius of the atmopshere.
czm_raySegment primaryRayEarthIntersect = czm_raySphereIntersectionInterval(primaryRay, vec3(0.0), atmosphereInnerRadius + radiusAdjust);
if (primaryRayEarthIntersect.start > 0.0 && primaryRayEarthIntersect.stop > 0.0) {
-
+
// Compute position on globe.
vec3 direction = normalize(positionWC);
czm_ray ellipsoidRay = czm_ray(positionWC, -direction);
@@ -62,7 +52,7 @@ void computeAtmosphereScattering(vec3 positionWC, vec3 lightDirection, out vec3
vec3 nearColor = vec3(0.0);
rayleighColor = mix(nearColor, horizonColor, exp(-angle) * opacity);
-
+
// Set the traslucent flag to avoid alpha adjustment in computeFinalColor funciton.
underTranslucentGlobe = 1.0;
return;
diff --git a/packages/engine/Source/Shaders/SkyAtmosphereFS.glsl b/packages/engine/Source/Shaders/SkyAtmosphereFS.glsl
index d0c67c94d440..d8f213396452 100644
--- a/packages/engine/Source/Shaders/SkyAtmosphereFS.glsl
+++ b/packages/engine/Source/Shaders/SkyAtmosphereFS.glsl
@@ -11,8 +11,9 @@ in float v_translucent;
void main (void)
{
- vec3 lightDirection = getLightDirection(v_outerPositionWC);
-
+ float lightEnum = u_radiiAndDynamicAtmosphereColor.z;
+ vec3 lightDirection = czm_getDynamicAtmosphereLightDirection(v_outerPositionWC, lightEnum);
+
vec3 mieColor;
vec3 rayleighColor;
float opacity;
@@ -42,14 +43,8 @@ void main (void)
#endif
#ifdef COLOR_CORRECT
- // Convert rgb color to hsb
- vec3 hsb = czm_RGBToHSB(color.rgb);
- // Perform hsb shift
- hsb.x += u_hsbShift.x; // hue
- hsb.y = clamp(hsb.y + u_hsbShift.y, 0.0, 1.0); // saturation
- hsb.z = hsb.z > czm_epsilon7 ? hsb.z + u_hsbShift.z : 0.0; // brightness
- // Convert shifted hsb back to rgb
- color.rgb = czm_HSBToRGB(hsb);
+ const bool ignoreBlackPixels = true;
+ color.rgb = czm_applyHSBShift(color.rgb, u_hsbShift, ignoreBlackPixels);
#endif
// For the parts of the sky atmosphere that are not behind a translucent globe,
diff --git a/packages/engine/Source/Shaders/SkyAtmosphereVS.glsl b/packages/engine/Source/Shaders/SkyAtmosphereVS.glsl
index 88d0bf59bcf8..8ed97f1a0979 100644
--- a/packages/engine/Source/Shaders/SkyAtmosphereVS.glsl
+++ b/packages/engine/Source/Shaders/SkyAtmosphereVS.glsl
@@ -12,7 +12,8 @@ out float v_translucent;
void main(void)
{
vec4 positionWC = czm_model * position;
- vec3 lightDirection = getLightDirection(positionWC.xyz);
+ float lightEnum = u_radiiAndDynamicAtmosphereColor.z;
+ vec3 lightDirection = czm_getDynamicAtmosphereLightDirection(positionWC.xyz, lightEnum);
#ifndef PER_FRAGMENT_ATMOSPHERE
computeAtmosphereScattering(
@@ -24,7 +25,7 @@ void main(void)
v_translucent
);
#endif
-
+
v_outerPositionWC = positionWC.xyz;
gl_Position = czm_modelViewProjection * position;
}
diff --git a/packages/engine/Specs/Renderer/AutomaticUniformSpec.js b/packages/engine/Specs/Renderer/AutomaticUniformSpec.js
index 55319d78300b..f6381a39f9ef 100644
--- a/packages/engine/Specs/Renderer/AutomaticUniformSpec.js
+++ b/packages/engine/Specs/Renderer/AutomaticUniformSpec.js
@@ -5,7 +5,9 @@ import {
Color,
defaultValue,
DirectionalLight,
+ DynamicAtmosphereLightingType,
Ellipsoid,
+ Fog,
GeographicProjection,
Matrix4,
OrthographicFrustum,
@@ -1783,6 +1785,213 @@ describe(
}).contextToRender();
});
+ it("has czm_fogDensity", function () {
+ const frameState = createFrameState(
+ context,
+ createMockCamera(
+ undefined,
+ undefined,
+ undefined,
+ // Provide position and direction because the default position of (0, 0, 0)
+ // will lead to a divide by zero when updating fog below.
+ new Cartesian3(1.0, 0.0, 0.0),
+ new Cartesian3(0.0, 1.0, 0.0)
+ )
+ );
+ const fog = new Fog();
+ fog.density = 0.1;
+ fog.update(frameState);
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_fogDensity != 0.0);" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_fogMinimumBrightness", function () {
+ const frameState = createFrameState(
+ context,
+ createMockCamera(
+ undefined,
+ undefined,
+ undefined,
+ // Provide position and direction because the default position of (0, 0, 0)
+ // will lead to a divide by zero when updating fog below
+ new Cartesian3(1.0, 0.0, 0.0),
+ new Cartesian3(0.0, 1.0, 0.0)
+ )
+ );
+ const fog = new Fog();
+ fog.minimumBrightness = 0.25;
+ fog.update(frameState);
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_fogMinimumBrightness == 0.25);" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereHsbShift", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ atmosphere.hueShift = 1.0;
+ atmosphere.saturationShift = 2.0;
+ atmosphere.brightnessShift = 3.0;
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_atmosphereHsbShift == vec3(1.0, 2.0, 3.0));" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereLightIntensity", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ atmosphere.lightIntensity = 2.0;
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_atmosphereLightIntensity == 2.0);" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereRayleighCoefficient", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ atmosphere.rayleighCoefficient = new Cartesian3(1.0, 2.0, 3.0);
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_atmosphereRayleighCoefficient == vec3(1.0, 2.0, 3.0));" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereRayleighScaleHeight", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ atmosphere.rayleighScaleHeight = 100.0;
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_atmosphereRayleighScaleHeight == 100.0);" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereMieCoefficient", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ atmosphere.mieCoefficient = new Cartesian3(1.0, 2.0, 3.0);
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_atmosphereMieCoefficient == vec3(1.0, 2.0, 3.0));" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereMieScaleHeight", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ atmosphere.mieScaleHeight = 100.0;
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_atmosphereMieScaleHeight == 100.0);" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereMieAnisotropy", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ atmosphere.mieAnisotropy = 100.0;
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ " out_FragColor = vec4(czm_atmosphereMieAnisotropy == 100.0);" +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
+ it("has czm_atmosphereDynamicLighting", function () {
+ const frameState = createFrameState(context, createMockCamera());
+ const atmosphere = frameState.atmosphere;
+ const enumValue = DynamicAtmosphereLightingType.SCENE_LIGHT;
+ atmosphere.dynamicLighting = enumValue;
+
+ const us = context.uniformState;
+ us.update(frameState);
+
+ const fs =
+ "void main() {" +
+ ` out_FragColor = vec4(czm_atmosphereDynamicLighting == float(${enumValue}));` +
+ "}";
+ expect({
+ context,
+ fragmentShader: fs,
+ }).contextToRender();
+ });
+
it("has czm_pass and czm_passEnvironment", function () {
const us = context.uniformState;
us.updatePass(Pass.ENVIRONMENT);
diff --git a/packages/engine/Specs/Scene/DynamicAtmosphereLightingTypeSpec.js b/packages/engine/Specs/Scene/DynamicAtmosphereLightingTypeSpec.js
new file mode 100644
index 000000000000..71c1802eeca8
--- /dev/null
+++ b/packages/engine/Specs/Scene/DynamicAtmosphereLightingTypeSpec.js
@@ -0,0 +1,47 @@
+import { DynamicAtmosphereLightingType } from "../../index.js";
+
+describe("Scene/DynamicAtmosphereLightingType", function () {
+ function mockGlobe() {
+ return {
+ enableLighting: false,
+ dynamicAtmosphereLighting: false,
+ dynamicAtmosphereLightingFromSun: false,
+ };
+ }
+
+ it("returns OFF when lighting is disabled", function () {
+ const globe = mockGlobe();
+
+ expect(DynamicAtmosphereLightingType.fromGlobeFlags(globe)).toBe(
+ DynamicAtmosphereLightingType.NONE
+ );
+
+ globe.enableLighting = true;
+
+ expect(DynamicAtmosphereLightingType.fromGlobeFlags(globe)).toBe(
+ DynamicAtmosphereLightingType.NONE
+ );
+
+ globe.enableLighting = false;
+ globe.dynamicAtmosphereLighting = true;
+ expect(DynamicAtmosphereLightingType.fromGlobeFlags(globe)).toBe(
+ DynamicAtmosphereLightingType.NONE
+ );
+ });
+
+ it("selects a light type depending on globe.dynamicAtmosphereLightingFromSun", function () {
+ const globe = mockGlobe();
+ globe.enableLighting = true;
+ globe.dynamicAtmosphereLighting = true;
+
+ globe.dynamicAtmosphereLightingFromSun = true;
+ expect(DynamicAtmosphereLightingType.fromGlobeFlags(globe)).toBe(
+ DynamicAtmosphereLightingType.SUNLIGHT
+ );
+
+ globe.dynamicAtmosphereLightingFromSun = false;
+ expect(DynamicAtmosphereLightingType.fromGlobeFlags(globe)).toBe(
+ DynamicAtmosphereLightingType.SCENE_LIGHT
+ );
+ });
+});
diff --git a/packages/engine/Specs/Scene/Model/AtmospherePipelineStageSpec.js b/packages/engine/Specs/Scene/Model/AtmospherePipelineStageSpec.js
new file mode 100644
index 000000000000..753fbcbcb5e8
--- /dev/null
+++ b/packages/engine/Specs/Scene/Model/AtmospherePipelineStageSpec.js
@@ -0,0 +1,120 @@
+import {
+ _shadersAtmosphereStageFS,
+ _shadersAtmosphereStageVS,
+ Cartesian3,
+ AtmospherePipelineStage,
+ ModelRenderResources,
+ Transforms,
+} from "../../../index.js";
+import createScene from "../../../../../Specs/createScene.js";
+import ShaderBuilderTester from "../../../../../Specs/ShaderBuilderTester.js";
+import loadAndZoomToModelAsync from "./loadAndZoomToModelAsync.js";
+
+describe(
+ "Scene/Model/AtmospherePipelineStage",
+ function () {
+ const boxTexturedGlbUrl =
+ "./Data/Models/glTF-2.0/BoxTextured/glTF-Binary/BoxTextured.glb";
+
+ let scene;
+ let model;
+ beforeAll(async function () {
+ scene = createScene();
+
+ const center = Cartesian3.fromDegrees(0, 0, 0);
+ model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGlbUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(center),
+ },
+ scene
+ );
+ });
+
+ let renderResources;
+ beforeEach(async function () {
+ renderResources = new ModelRenderResources(model);
+
+ // position the camera a little bit east of the model
+ // and slightly above it.
+ scene.frameState.camera.position = Cartesian3.fromDegrees(0.01, 0, 1000);
+ scene.frameState.camera.direction = new Cartesian3(0, -1, 0);
+
+ // Reset the fog density
+ scene.fog.density = 2e-4;
+ });
+
+ afterAll(async function () {
+ scene.destroyForSpecs();
+ });
+
+ it("configures shader", function () {
+ AtmospherePipelineStage.process(renderResources, model, scene.frameState);
+
+ const shaderBuilder = renderResources.shaderBuilder;
+
+ ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, [
+ "HAS_ATMOSPHERE",
+ "COMPUTE_POSITION_WC_ATMOSPHERE",
+ ]);
+ ShaderBuilderTester.expectHasFragmentDefines(shaderBuilder, [
+ "HAS_ATMOSPHERE",
+ "COMPUTE_POSITION_WC_ATMOSPHERE",
+ ]);
+
+ ShaderBuilderTester.expectHasVaryings(shaderBuilder, [
+ "vec3 v_atmosphereRayleighColor;",
+ "vec3 v_atmosphereMieColor;",
+ "float v_atmosphereOpacity;",
+ ]);
+
+ ShaderBuilderTester.expectVertexLinesEqual(shaderBuilder, [
+ _shadersAtmosphereStageVS,
+ ]);
+ ShaderBuilderTester.expectFragmentLinesEqual(shaderBuilder, [
+ _shadersAtmosphereStageFS,
+ ]);
+
+ ShaderBuilderTester.expectHasVertexUniforms(shaderBuilder, []);
+ ShaderBuilderTester.expectHasFragmentUniforms(shaderBuilder, [
+ "uniform bool u_isInFog;",
+ ]);
+ });
+
+ it("u_isInFog() is false if the camera is at the model center", function () {
+ const frameState = scene.frameState;
+ frameState.camera.position = Cartesian3.clone(
+ model.boundingSphere.center,
+ frameState.camera.position
+ );
+ scene.renderForSpecs();
+
+ AtmospherePipelineStage.process(renderResources, model, frameState);
+
+ const uniformMap = renderResources.uniformMap;
+ expect(uniformMap.u_isInFog()).toBe(false);
+ });
+
+ it("u_isInFog() is false if the camera is in space", function () {
+ const frameState = scene.frameState;
+
+ frameState.camera.position = Cartesian3.fromDegrees(0.01, 0, 900000);
+ scene.renderForSpecs();
+
+ AtmospherePipelineStage.process(renderResources, model, frameState);
+
+ const uniformMap = renderResources.uniformMap;
+ expect(uniformMap.u_isInFog()).toBe(false);
+ });
+
+ it("u_isInFog() is true when the tile is in fog", function () {
+ scene.renderForSpecs();
+
+ AtmospherePipelineStage.process(renderResources, model, scene.frameState);
+
+ const uniformMap = renderResources.uniformMap;
+ expect(uniformMap.u_isInFog()).toBe(true);
+ });
+ },
+ "WebGL"
+);
diff --git a/packages/engine/Specs/Scene/Model/ModelMatrixUpdateStageSpec.js b/packages/engine/Specs/Scene/Model/ModelMatrixUpdateStageSpec.js
index eddd956faa54..17ffae777fe4 100644
--- a/packages/engine/Specs/Scene/Model/ModelMatrixUpdateStageSpec.js
+++ b/packages/engine/Specs/Scene/Model/ModelMatrixUpdateStageSpec.js
@@ -131,68 +131,6 @@ describe(
leafNode._transformDirty = true;
}
- it("updates leaf nodes using node transform setter", function () {
- return loadAndZoomToModelAsync(
- {
- gltf: simpleSkin,
- },
- scene
- ).then(function (model) {
- const sceneGraph = model.sceneGraph;
- const node = getStaticLeafNode(model);
- const primitive = node.runtimePrimitives[0];
- const drawCommand = primitive.drawCommand;
-
- const expectedOriginalTransform = Matrix4.clone(node.transform);
- expect(node._transformDirty).toEqual(false);
-
- const translation = new Cartesian3(0, 5, 0);
- node.transform = Matrix4.multiplyByTranslation(
- node.transform,
- translation,
- new Matrix4()
- );
- expect(node._transformDirty).toEqual(true);
- expect(
- Matrix4.equals(node.originalTransform, expectedOriginalTransform)
- ).toBe(true);
-
- const expectedComputedTransform = Matrix4.multiplyTransformation(
- sceneGraph.computedModelMatrix,
- node.transform,
- new Matrix4()
- );
-
- const expectedModelMatrix = Matrix4.multiplyByTranslation(
- drawCommand.modelMatrix,
- translation,
- new Matrix4()
- );
-
- const expectedBoundingSphere = BoundingSphere.transform(
- primitive.boundingSphere,
- expectedComputedTransform,
- new BoundingSphere()
- );
-
- scene.renderForSpecs();
-
- expect(
- Matrix4.equalsEpsilon(
- drawCommand.modelMatrix,
- expectedModelMatrix,
- CesiumMath.EPSILON15
- )
- ).toBe(true);
- expect(
- BoundingSphere.equals(
- drawCommand.boundingVolume,
- expectedBoundingSphere
- )
- ).toBe(true);
- });
- });
-
function applyTransform(node, transform) {
const expectedOriginalTransform = Matrix4.clone(node.originalTransform);
expect(node._transformDirty).toEqual(false);
@@ -209,207 +147,258 @@ describe(
).toBe(true);
}
- it("updates nodes with children using node transform setter", function () {
- return loadAndZoomToModelAsync(
+ it("updates leaf nodes using node transform setter", async function () {
+ const model = await loadAndZoomToModelAsync(
{
gltf: simpleSkin,
},
scene
- ).then(function (model) {
- modifyModel(model);
- scene.renderForSpecs();
-
- const rootNode = getParentRootNode(model);
- const staticLeafNode = getStaticLeafNode(model);
- const transformedLeafNode = getChildLeafNode(model);
-
- let rootDrawCommand = getDrawCommand(rootNode);
- let staticDrawCommand = getDrawCommand(staticLeafNode);
- let transformedDrawCommand = getDrawCommand(transformedLeafNode);
-
- const childTransformation = Matrix4.fromTranslation(
- new Cartesian3(0, 5, 0)
- );
- applyTransform(transformedLeafNode, childTransformation);
-
- const rootTransformation = Matrix4.fromTranslation(
- new Cartesian3(12, 5, 0)
- );
- applyTransform(rootNode, rootTransformation);
-
- const expectedRootModelMatrix = Matrix4.multiplyTransformation(
- rootTransformation,
- rootDrawCommand.modelMatrix,
- new Matrix4()
- );
- const expectedStaticLeafModelMatrix = Matrix4.clone(
- staticDrawCommand.modelMatrix,
- new Matrix4()
- );
-
- const finalTransform = new Matrix4();
- Matrix4.multiply(
- rootTransformation,
- childTransformation,
- finalTransform
- );
- const expectedTransformedLeafModelMatrix = Matrix4.multiplyTransformation(
- finalTransform,
- transformedDrawCommand.modelMatrix,
- new Matrix4()
- );
-
- scene.renderForSpecs();
- rootDrawCommand = getDrawCommand(rootNode);
- staticDrawCommand = getDrawCommand(staticLeafNode);
- transformedDrawCommand = getDrawCommand(transformedLeafNode);
-
- expect(rootDrawCommand.modelMatrix).toEqual(expectedRootModelMatrix);
- expect(staticDrawCommand.modelMatrix).toEqual(
- expectedStaticLeafModelMatrix
- );
- expect(transformedDrawCommand.modelMatrix).toEqual(
- expectedTransformedLeafModelMatrix
- );
- });
+ );
+ scene.renderForSpecs();
+
+ const sceneGraph = model.sceneGraph;
+ const node = getStaticLeafNode(model);
+ const primitive = node.runtimePrimitives[0];
+
+ let drawCommand = getDrawCommand(node);
+
+ const transform = Matrix4.fromTranslation(new Cartesian3(0, 5, 0));
+ applyTransform(node, transform);
+
+ const expectedComputedTransform = Matrix4.multiplyTransformation(
+ sceneGraph.computedModelMatrix,
+ node.transform,
+ new Matrix4()
+ );
+
+ const expectedModelMatrix = Matrix4.multiplyTransformation(
+ drawCommand.modelMatrix,
+ transform,
+ new Matrix4()
+ );
+
+ const expectedBoundingSphere = BoundingSphere.transform(
+ primitive.boundingSphere,
+ expectedComputedTransform,
+ new BoundingSphere()
+ );
+
+ scene.renderForSpecs();
+ drawCommand = getDrawCommand(node);
+
+ expect(
+ Matrix4.equalsEpsilon(
+ drawCommand.modelMatrix,
+ expectedModelMatrix,
+ CesiumMath.EPSILON15
+ )
+ ).toBe(true);
+ expect(
+ BoundingSphere.equals(
+ drawCommand.boundingVolume,
+ expectedBoundingSphere
+ )
+ ).toBe(true);
});
- it("updates with new model matrix", function () {
- return loadAndZoomToModelAsync(
+ it("updates nodes with children using node transform setter", async function () {
+ const model = await loadAndZoomToModelAsync(
{
gltf: simpleSkin,
},
scene
- ).then(function (model) {
- modifyModel(model);
- scene.renderForSpecs();
-
- const rootNode = getParentRootNode(model);
- const staticLeafNode = getStaticLeafNode(model);
- const transformedLeafNode = getChildLeafNode(model);
-
- let rootDrawCommand = getDrawCommand(rootNode);
- let staticDrawCommand = getDrawCommand(staticLeafNode);
- let transformedDrawCommand = getDrawCommand(transformedLeafNode);
-
- const expectedRootModelMatrix = Matrix4.multiplyTransformation(
- modelMatrix,
- rootDrawCommand.modelMatrix,
- new Matrix4()
- );
- const expectedStaticLeafModelMatrix = Matrix4.multiplyTransformation(
- modelMatrix,
- staticDrawCommand.modelMatrix,
- new Matrix4()
- );
- const expectedTransformedLeafModelMatrix = Matrix4.multiplyTransformation(
- modelMatrix,
- transformedDrawCommand.modelMatrix,
- new Matrix4()
- );
-
- model.modelMatrix = modelMatrix;
- scene.renderForSpecs();
-
- rootDrawCommand = getDrawCommand(rootNode);
- staticDrawCommand = getDrawCommand(staticLeafNode);
- transformedDrawCommand = getDrawCommand(transformedLeafNode);
-
- expect(rootDrawCommand.modelMatrix).toEqual(expectedRootModelMatrix);
- expect(staticDrawCommand.modelMatrix).toEqual(
- expectedStaticLeafModelMatrix
- );
- expect(transformedDrawCommand.modelMatrix).toEqual(
- expectedTransformedLeafModelMatrix
- );
- });
+ );
+
+ modifyModel(model);
+ scene.renderForSpecs();
+
+ const rootNode = getParentRootNode(model);
+ const staticLeafNode = getStaticLeafNode(model);
+ const transformedLeafNode = getChildLeafNode(model);
+
+ let rootDrawCommand = getDrawCommand(rootNode);
+ let staticDrawCommand = getDrawCommand(staticLeafNode);
+ let transformedDrawCommand = getDrawCommand(transformedLeafNode);
+
+ const childTransformation = Matrix4.fromTranslation(
+ new Cartesian3(0, 5, 0)
+ );
+ applyTransform(transformedLeafNode, childTransformation);
+
+ const rootTransformation = Matrix4.fromTranslation(
+ new Cartesian3(12, 5, 0)
+ );
+ applyTransform(rootNode, rootTransformation);
+
+ const expectedRootModelMatrix = Matrix4.multiplyTransformation(
+ rootTransformation,
+ rootDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+ const expectedStaticLeafModelMatrix = Matrix4.clone(
+ staticDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+
+ const finalTransform = new Matrix4();
+ Matrix4.multiply(rootTransformation, childTransformation, finalTransform);
+ const expectedTransformedLeafModelMatrix = Matrix4.multiplyTransformation(
+ finalTransform,
+ transformedDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+
+ scene.renderForSpecs();
+ rootDrawCommand = getDrawCommand(rootNode);
+ staticDrawCommand = getDrawCommand(staticLeafNode);
+ transformedDrawCommand = getDrawCommand(transformedLeafNode);
+
+ expect(rootDrawCommand.modelMatrix).toEqual(expectedRootModelMatrix);
+ expect(staticDrawCommand.modelMatrix).toEqual(
+ expectedStaticLeafModelMatrix
+ );
+ expect(transformedDrawCommand.modelMatrix).toEqual(
+ expectedTransformedLeafModelMatrix
+ );
});
- it("updates with new model matrix and model scale", function () {
- return loadAndZoomToModelAsync(
+ it("updates with new model matrix", async function () {
+ const model = await loadAndZoomToModelAsync(
{
gltf: simpleSkin,
},
scene
- ).then(function (model) {
- modifyModel(model);
- scene.renderForSpecs();
-
- const modelScale = 5.0;
- const scaledModelMatrix = Matrix4.multiplyByUniformScale(
- modelMatrix,
- modelScale,
- new Matrix4()
- );
-
- const rootNode = getParentRootNode(model);
- const staticLeafNode = getStaticLeafNode(model);
- const transformedLeafNode = getChildLeafNode(model);
-
- let rootDrawCommand = getDrawCommand(rootNode);
- let staticDrawCommand = getDrawCommand(staticLeafNode);
- let transformedDrawCommand = getDrawCommand(transformedLeafNode);
-
- const expectedRootModelMatrix = Matrix4.multiplyTransformation(
- scaledModelMatrix,
- rootDrawCommand.modelMatrix,
- new Matrix4()
- );
- const expectedStaticLeafModelMatrix = Matrix4.multiplyTransformation(
- scaledModelMatrix,
- staticDrawCommand.modelMatrix,
- new Matrix4()
- );
- const expectedTransformedLeafModelMatrix = Matrix4.multiplyTransformation(
- scaledModelMatrix,
- transformedDrawCommand.modelMatrix,
- new Matrix4()
- );
-
- model.modelMatrix = modelMatrix;
- model.scale = modelScale;
- scene.renderForSpecs();
- rootDrawCommand = getDrawCommand(rootNode);
- staticDrawCommand = getDrawCommand(staticLeafNode);
- transformedDrawCommand = getDrawCommand(transformedLeafNode);
-
- expect(rootDrawCommand.modelMatrix).toEqual(expectedRootModelMatrix);
- expect(staticDrawCommand.modelMatrix).toEqual(
- expectedStaticLeafModelMatrix
- );
- expect(transformedDrawCommand.modelMatrix).toEqual(
- expectedTransformedLeafModelMatrix
- );
- });
+ );
+
+ modifyModel(model);
+ scene.renderForSpecs();
+
+ const rootNode = getParentRootNode(model);
+ const staticLeafNode = getStaticLeafNode(model);
+ const transformedLeafNode = getChildLeafNode(model);
+
+ let rootDrawCommand = getDrawCommand(rootNode);
+ let staticDrawCommand = getDrawCommand(staticLeafNode);
+ let transformedDrawCommand = getDrawCommand(transformedLeafNode);
+
+ const expectedRootModelMatrix = Matrix4.multiplyTransformation(
+ modelMatrix,
+ rootDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+ const expectedStaticLeafModelMatrix = Matrix4.multiplyTransformation(
+ modelMatrix,
+ staticDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+ const expectedTransformedLeafModelMatrix = Matrix4.multiplyTransformation(
+ modelMatrix,
+ transformedDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+
+ model.modelMatrix = modelMatrix;
+ scene.renderForSpecs();
+
+ rootDrawCommand = getDrawCommand(rootNode);
+ staticDrawCommand = getDrawCommand(staticLeafNode);
+ transformedDrawCommand = getDrawCommand(transformedLeafNode);
+
+ expect(rootDrawCommand.modelMatrix).toEqual(expectedRootModelMatrix);
+ expect(staticDrawCommand.modelMatrix).toEqual(
+ expectedStaticLeafModelMatrix
+ );
+ expect(transformedDrawCommand.modelMatrix).toEqual(
+ expectedTransformedLeafModelMatrix
+ );
+ });
+
+ it("updates with new model matrix and model scale", async function () {
+ const model = await loadAndZoomToModelAsync(
+ {
+ gltf: simpleSkin,
+ },
+ scene
+ );
+
+ modifyModel(model);
+ scene.renderForSpecs();
+
+ const modelScale = 5.0;
+ const scaledModelMatrix = Matrix4.multiplyByUniformScale(
+ modelMatrix,
+ modelScale,
+ new Matrix4()
+ );
+
+ const rootNode = getParentRootNode(model);
+ const staticLeafNode = getStaticLeafNode(model);
+ const transformedLeafNode = getChildLeafNode(model);
+
+ let rootDrawCommand = getDrawCommand(rootNode);
+ let staticDrawCommand = getDrawCommand(staticLeafNode);
+ let transformedDrawCommand = getDrawCommand(transformedLeafNode);
+
+ const expectedRootModelMatrix = Matrix4.multiplyTransformation(
+ scaledModelMatrix,
+ rootDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+ const expectedStaticLeafModelMatrix = Matrix4.multiplyTransformation(
+ scaledModelMatrix,
+ staticDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+ const expectedTransformedLeafModelMatrix = Matrix4.multiplyTransformation(
+ scaledModelMatrix,
+ transformedDrawCommand.modelMatrix,
+ new Matrix4()
+ );
+
+ model.modelMatrix = modelMatrix;
+ model.scale = modelScale;
+ scene.renderForSpecs();
+ rootDrawCommand = getDrawCommand(rootNode);
+ staticDrawCommand = getDrawCommand(staticLeafNode);
+ transformedDrawCommand = getDrawCommand(transformedLeafNode);
+
+ expect(rootDrawCommand.modelMatrix).toEqual(expectedRootModelMatrix);
+ expect(staticDrawCommand.modelMatrix).toEqual(
+ expectedStaticLeafModelMatrix
+ );
+ expect(transformedDrawCommand.modelMatrix).toEqual(
+ expectedTransformedLeafModelMatrix
+ );
});
- it("updates render state cull face when scale is negative", function () {
- return loadAndZoomToModelAsync(
+ it("updates render state cull face when scale is negative", async function () {
+ const model = await loadAndZoomToModelAsync(
{
gltf: simpleSkin,
},
scene
- ).then(function (model) {
- modifyModel(model);
+ );
+ modifyModel(model);
- const rootNode = getParentRootNode(model);
- const childNode = getChildLeafNode(model);
+ const rootNode = getParentRootNode(model);
+ const childNode = getChildLeafNode(model);
- const rootPrimitive = rootNode.runtimePrimitives[0];
- const childPrimitive = childNode.runtimePrimitives[0];
+ const rootPrimitive = rootNode.runtimePrimitives[0];
+ const childPrimitive = childNode.runtimePrimitives[0];
- const rootDrawCommand = rootPrimitive.drawCommand;
- const childDrawCommand = childPrimitive.drawCommand;
+ const rootDrawCommand = rootPrimitive.drawCommand;
+ const childDrawCommand = childPrimitive.drawCommand;
- expect(rootDrawCommand.cullFace).toBe(CullFace.BACK);
- expect(childDrawCommand.cullFace).toBe(CullFace.BACK);
+ expect(rootDrawCommand.cullFace).toBe(CullFace.BACK);
+ expect(childDrawCommand.cullFace).toBe(CullFace.BACK);
- model.modelMatrix = Matrix4.fromUniformScale(-1);
- scene.renderForSpecs();
+ model.modelMatrix = Matrix4.fromUniformScale(-1);
+ scene.renderForSpecs();
- expect(rootDrawCommand.cullFace).toBe(CullFace.FRONT);
- expect(childDrawCommand.cullFace).toBe(CullFace.FRONT);
- });
+ expect(rootPrimitive.drawCommand).toBe(rootDrawCommand);
+
+ expect(rootDrawCommand.cullFace).toBe(CullFace.FRONT);
+ expect(childDrawCommand.cullFace).toBe(CullFace.FRONT);
});
},
"WebGL"
diff --git a/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js b/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js
index dff8c900829f..6e73cc3fbc8b 100644
--- a/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js
+++ b/packages/engine/Specs/Scene/Model/ModelRuntimePrimitiveSpec.js
@@ -2,6 +2,7 @@ import {
AlphaPipelineStage,
BatchTexturePipelineStage,
Cesium3DTileStyle,
+ ClassificationPipelineStage,
CustomShader,
CustomShaderMode,
CustomShaderPipelineStage,
@@ -30,7 +31,8 @@ import {
WireframePipelineStage,
ClassificationType,
} from "../../../index.js";
-import ClassificationPipelineStage from "../../../Source/Scene/Model/ClassificationPipelineStage.js";
+
+import createFrameState from "../../../../../Specs/createFrameState.js";
describe("Scene/Model/ModelRuntimePrimitive", function () {
const mockPrimitive = {
@@ -43,33 +45,21 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
allowPicking: true,
featureIdLabel: "featureId_0",
};
- const mockFrameState = {
- context: {
- webgl2: false,
- },
- mode: SceneMode.SCENE3D,
+ const mockWebgl1Context = {
+ webgl2: false,
};
-
- const mockFrameStateWebgl2 = {
- context: {
- webgl2: true,
- },
+ const mockWebgl2Context = {
+ webgl2: true,
};
- const mockFrameState2D = {
- context: {
- webgl2: false,
- },
- mode: SceneMode.SCENE2D,
- };
+ const mockFrameState = createFrameState(mockWebgl1Context);
+ const mockFrameStateWebgl2 = createFrameState(mockWebgl2Context);
- const mockFrameState3DOnly = {
- context: {
- webgl2: false,
- },
- mode: SceneMode.SCENE3D,
- scene3DOnly: true,
- };
+ const mockFrameState2D = createFrameState(mockWebgl1Context);
+ mockFrameState2D.mode = SceneMode.SCENE2D;
+
+ const mockFrameState3DOnly = createFrameState(mockWebgl1Context);
+ mockFrameState3DOnly.scene3DOnly = true;
const emptyVertexShader =
"void vertexMain(VertexInput vsInput, inout vec3 position) {}";
@@ -160,7 +150,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -203,7 +192,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
SelectedFeatureIdPipelineStage,
BatchTexturePipelineStage,
CPUStylingPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
PickingPipelineStage,
AlphaPipelineStage,
@@ -250,7 +238,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
SelectedFeatureIdPipelineStage,
BatchTexturePipelineStage,
CPUStylingPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
PickingPipelineStage,
AlphaPipelineStage,
@@ -307,7 +294,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
PickingPipelineStage,
AlphaPipelineStage,
@@ -335,7 +321,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
CustomShaderPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
@@ -366,7 +351,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
GeometryPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
CustomShaderPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
@@ -397,7 +381,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
CustomShaderPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
@@ -438,7 +421,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -473,7 +455,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -507,7 +488,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -538,7 +518,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -570,7 +549,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -600,7 +578,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -633,7 +610,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -675,7 +651,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -708,7 +683,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -740,7 +714,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -772,7 +745,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -803,7 +775,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -835,7 +806,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -866,7 +836,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -897,7 +866,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -929,7 +897,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
PrimitiveOutlinePipelineStage,
AlphaPipelineStage,
@@ -962,7 +929,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -993,7 +959,6 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
MaterialPipelineStage,
FeatureIdPipelineStage,
MetadataPipelineStage,
- VerticalExaggerationPipelineStage,
LightingPipelineStage,
AlphaPipelineStage,
PrimitiveStatisticsPipelineStage,
@@ -1002,4 +967,29 @@ describe("Scene/Model/ModelRuntimePrimitive", function () {
primitive.configurePipeline(mockFrameState);
verifyExpectedStages(primitive.pipelineStages, expectedStages);
});
+
+ it("configures pipeline stages for vertical exaggeration", function () {
+ const primitive = new ModelRuntimePrimitive({
+ primitive: mockPrimitive,
+ node: mockNode,
+ model: mockModel,
+ });
+ const frameState = createFrameState(mockWebgl2Context);
+ frameState.verticalExaggeration = 2.0;
+
+ const expectedStages = [
+ GeometryPipelineStage,
+ MaterialPipelineStage,
+ FeatureIdPipelineStage,
+ MetadataPipelineStage,
+ VerticalExaggerationPipelineStage,
+ LightingPipelineStage,
+ PickingPipelineStage,
+ AlphaPipelineStage,
+ PrimitiveStatisticsPipelineStage,
+ ];
+
+ primitive.configurePipeline(frameState);
+ verifyExpectedStages(primitive.pipelineStages, expectedStages);
+ });
});
diff --git a/packages/engine/Specs/Scene/Model/ModelSceneGraphSpec.js b/packages/engine/Specs/Scene/Model/ModelSceneGraphSpec.js
index b5196ff355d3..8de1acdc3ee6 100644
--- a/packages/engine/Specs/Scene/Model/ModelSceneGraphSpec.js
+++ b/packages/engine/Specs/Scene/Model/ModelSceneGraphSpec.js
@@ -4,6 +4,8 @@ import {
Color,
CustomShader,
CustomShaderPipelineStage,
+ Fog,
+ AtmospherePipelineStage,
Math as CesiumMath,
Matrix4,
ModelColorPipelineStage,
@@ -41,82 +43,81 @@ describe(
afterEach(function () {
scene.primitives.removeAll();
+ scene.fog = new Fog();
ResourceCache.clearForSpecs();
});
- it("creates runtime nodes and runtime primitives from a model", function () {
- return loadAndZoomToModelAsync({ gltf: vertexColorGltfUrl }, scene).then(
- function (model) {
- const sceneGraph = model._sceneGraph;
- const components = sceneGraph._components;
+ it("creates runtime nodes and runtime primitives from a model", async function () {
+ const model = await loadAndZoomToModelAsync(
+ { gltf: vertexColorGltfUrl },
+ scene
+ );
+ const sceneGraph = model._sceneGraph;
+ const components = sceneGraph._components;
- expect(sceneGraph).toBeDefined();
+ expect(sceneGraph).toBeDefined();
- const runtimeNodes = sceneGraph._runtimeNodes;
- expect(runtimeNodes.length).toEqual(components.nodes.length);
+ const runtimeNodes = sceneGraph._runtimeNodes;
+ expect(runtimeNodes.length).toEqual(components.nodes.length);
- expect(runtimeNodes[0].runtimePrimitives.length).toEqual(1);
- expect(runtimeNodes[1].runtimePrimitives.length).toEqual(1);
- }
- );
+ expect(runtimeNodes[0].runtimePrimitives.length).toEqual(1);
+ expect(runtimeNodes[1].runtimePrimitives.length).toEqual(1);
});
- it("builds draw commands for all opaque styled features", function () {
+ it("builds draw commands for all opaque styled features", async function () {
const style = new Cesium3DTileStyle({
color: {
conditions: [["${height} > 1", "color('red')"]],
},
});
- return loadAndZoomToModelAsync(
+ const model = await loadAndZoomToModelAsync(
{
gltf: buildingsMetadata,
},
scene
- ).then(function (model) {
- model.style = style;
+ );
+ model.style = style;
- const frameState = scene.frameState;
- const commandList = frameState.commandList;
- commandList.length = 0;
+ const frameState = scene.frameState;
+ const commandList = frameState.commandList;
+ commandList.length = 0;
- // Reset the draw commands so we can inspect the draw command generation.
- model._drawCommandsBuilt = false;
- scene.renderForSpecs();
+ // Reset the draw commands so we can inspect the draw command generation.
+ model._drawCommandsBuilt = false;
+ scene.renderForSpecs();
- expect(commandList.length).toEqual(1);
- expect(commandList[0].pass).toEqual(Pass.OPAQUE);
- });
+ expect(commandList.length).toEqual(1);
+ expect(commandList[0].pass).toEqual(Pass.OPAQUE);
});
- it("builds draw commands for all translucent styled features", function () {
+ it("builds draw commands for all translucent styled features", async function () {
const style = new Cesium3DTileStyle({
color: {
conditions: [["${height} > 1", "color('red', 0.1)"]],
},
});
- return loadAndZoomToModelAsync(
+ const model = await loadAndZoomToModelAsync(
{
gltf: buildingsMetadata,
},
scene
- ).then(function (model) {
- model.style = style;
+ );
+ model.style = style;
- const frameState = scene.frameState;
- const commandList = frameState.commandList;
- commandList.length = 0;
+ const frameState = scene.frameState;
+ const commandList = frameState.commandList;
+ commandList.length = 0;
- // Reset the draw commands so we can inspect the draw command generation.
- model._drawCommandsBuilt = false;
- scene.renderForSpecs();
+ // Reset the draw commands so we can inspect the draw command generation.
+ model._drawCommandsBuilt = false;
+ scene.renderForSpecs();
- expect(commandList.length).toEqual(1);
- expect(commandList[0].pass).toEqual(Pass.TRANSLUCENT);
- });
+ expect(commandList.length).toEqual(1);
+ expect(commandList[0].pass).toEqual(Pass.TRANSLUCENT);
});
- it("builds draw commands for both opaque and translucent styled features", function () {
+ it("builds draw commands for both opaque and translucent styled features", async function () {
const style = new Cesium3DTileStyle({
color: {
conditions: [
@@ -126,268 +127,301 @@ describe(
},
});
- return loadAndZoomToModelAsync(
+ const model = await loadAndZoomToModelAsync(
{
gltf: buildingsMetadata,
},
scene
- ).then(function (model) {
- model.style = style;
+ );
+ model.style = style;
- const frameState = scene.frameState;
- const commandList = frameState.commandList;
- commandList.length = 0;
+ const frameState = scene.frameState;
+ const commandList = frameState.commandList;
+ commandList.length = 0;
- // Reset the draw commands so we can inspect the draw command generation.
- model._drawCommandsBuilt = false;
- scene.renderForSpecs();
+ // Reset the draw commands so we can inspect the draw command generation.
+ model._drawCommandsBuilt = false;
+ scene.renderForSpecs();
- expect(commandList.length).toEqual(2);
- expect(commandList[0].pass).toEqual(Pass.TRANSLUCENT);
- expect(commandList[1].pass).toEqual(Pass.OPAQUE);
- });
+ expect(commandList.length).toEqual(2);
+ expect(commandList[0].pass).toEqual(Pass.TRANSLUCENT);
+ expect(commandList[1].pass).toEqual(Pass.OPAQUE);
});
- it("builds draw commands for each primitive", function () {
+ it("builds draw commands for each primitive", async function () {
spyOn(ModelSceneGraph.prototype, "buildDrawCommands").and.callThrough();
spyOn(ModelSceneGraph.prototype, "pushDrawCommands").and.callThrough();
- return loadAndZoomToModelAsync({ gltf: parentGltfUrl }, scene).then(
- function (model) {
- const sceneGraph = model._sceneGraph;
- const runtimeNodes = sceneGraph._runtimeNodes;
-
- let primitivesCount = 0;
- for (let i = 0; i < runtimeNodes.length; i++) {
- primitivesCount += runtimeNodes[i].runtimePrimitives.length;
- }
-
- const frameState = scene.frameState;
- frameState.commandList.length = 0;
- scene.renderForSpecs();
- expect(
- ModelSceneGraph.prototype.buildDrawCommands
- ).toHaveBeenCalled();
- expect(ModelSceneGraph.prototype.pushDrawCommands).toHaveBeenCalled();
- expect(frameState.commandList.length).toEqual(primitivesCount);
-
- // Reset the draw command list to see if they're re-built.
- model._drawCommandsBuilt = false;
- frameState.commandList.length = 0;
- scene.renderForSpecs();
- expect(
- ModelSceneGraph.prototype.buildDrawCommands
- ).toHaveBeenCalled();
- expect(ModelSceneGraph.prototype.pushDrawCommands).toHaveBeenCalled();
- expect(frameState.commandList.length).toEqual(primitivesCount);
- }
+ const model = await loadAndZoomToModelAsync(
+ { gltf: parentGltfUrl },
+ scene
);
+
+ const sceneGraph = model._sceneGraph;
+ const runtimeNodes = sceneGraph._runtimeNodes;
+
+ let primitivesCount = 0;
+ for (let i = 0; i < runtimeNodes.length; i++) {
+ primitivesCount += runtimeNodes[i].runtimePrimitives.length;
+ }
+
+ const frameState = scene.frameState;
+ frameState.commandList.length = 0;
+ scene.renderForSpecs();
+ expect(ModelSceneGraph.prototype.buildDrawCommands).toHaveBeenCalled();
+ expect(ModelSceneGraph.prototype.pushDrawCommands).toHaveBeenCalled();
+ expect(frameState.commandList.length).toEqual(primitivesCount);
+
+ // Reset the draw command list to see if they're re-built.
+ model._drawCommandsBuilt = false;
+ frameState.commandList.length = 0;
+ scene.renderForSpecs();
+ expect(ModelSceneGraph.prototype.buildDrawCommands).toHaveBeenCalled();
+ expect(ModelSceneGraph.prototype.pushDrawCommands).toHaveBeenCalled();
+ expect(frameState.commandList.length).toEqual(primitivesCount);
});
- it("stores runtime nodes correctly", function () {
- return loadAndZoomToModelAsync({ gltf: parentGltfUrl }, scene).then(
- function (model) {
- const sceneGraph = model._sceneGraph;
- const components = sceneGraph._components;
- const runtimeNodes = sceneGraph._runtimeNodes;
+ it("stores runtime nodes correctly", async function () {
+ const model = await loadAndZoomToModelAsync(
+ { gltf: parentGltfUrl },
+ scene
+ );
+
+ const sceneGraph = model._sceneGraph;
+ const components = sceneGraph._components;
+ const runtimeNodes = sceneGraph._runtimeNodes;
- expect(runtimeNodes[0].node).toEqual(components.nodes[0]);
- expect(runtimeNodes[1].node).toEqual(components.nodes[1]);
+ expect(runtimeNodes[0].node).toEqual(components.nodes[0]);
+ expect(runtimeNodes[1].node).toEqual(components.nodes[1]);
- const rootNodes = sceneGraph._rootNodes;
- expect(rootNodes[0]).toEqual(0);
- }
- );
+ const rootNodes = sceneGraph._rootNodes;
+ expect(rootNodes[0]).toEqual(0);
});
- it("propagates node transforms correctly", function () {
- return loadAndZoomToModelAsync(
+ it("propagates node transforms correctly", async function () {
+ const model = await loadAndZoomToModelAsync(
{
gltf: parentGltfUrl,
upAxis: Axis.Z,
forwardAxis: Axis.X,
},
scene
- ).then(function (model) {
- const sceneGraph = model._sceneGraph;
- const components = sceneGraph._components;
- const runtimeNodes = sceneGraph._runtimeNodes;
-
- expect(components.upAxis).toEqual(Axis.Z);
- expect(components.forwardAxis).toEqual(Axis.X);
-
- const parentTransform = ModelUtility.getNodeTransform(
- components.nodes[0]
- );
- const childTransform = ModelUtility.getNodeTransform(
- components.nodes[1]
- );
- expect(runtimeNodes[0].transform).toEqual(parentTransform);
- expect(runtimeNodes[0].transformToRoot).toEqual(Matrix4.IDENTITY);
- expect(runtimeNodes[1].transform).toEqual(childTransform);
- expect(runtimeNodes[1].transformToRoot).toEqual(parentTransform);
- });
+ );
+ const sceneGraph = model._sceneGraph;
+ const components = sceneGraph._components;
+ const runtimeNodes = sceneGraph._runtimeNodes;
+
+ expect(components.upAxis).toEqual(Axis.Z);
+ expect(components.forwardAxis).toEqual(Axis.X);
+
+ const parentTransform = ModelUtility.getNodeTransform(
+ components.nodes[0]
+ );
+ const childTransform = ModelUtility.getNodeTransform(components.nodes[1]);
+ expect(runtimeNodes[0].transform).toEqual(parentTransform);
+ expect(runtimeNodes[0].transformToRoot).toEqual(Matrix4.IDENTITY);
+ expect(runtimeNodes[1].transform).toEqual(childTransform);
+ expect(runtimeNodes[1].transformToRoot).toEqual(parentTransform);
});
- it("creates runtime skin from model", function () {
- return loadAndZoomToModelAsync({ gltf: simpleSkinGltfUrl }, scene).then(
- function (model) {
- const sceneGraph = model._sceneGraph;
- const components = sceneGraph._components;
- const runtimeNodes = sceneGraph._runtimeNodes;
-
- expect(runtimeNodes[0].node).toEqual(components.nodes[0]);
- expect(runtimeNodes[1].node).toEqual(components.nodes[1]);
- expect(runtimeNodes[2].node).toEqual(components.nodes[2]);
-
- const rootNodes = sceneGraph._rootNodes;
- expect(rootNodes[0]).toEqual(0);
- expect(rootNodes[1]).toEqual(1);
-
- const runtimeSkins = sceneGraph._runtimeSkins;
- expect(runtimeSkins[0].skin).toEqual(components.skins[0]);
- expect(runtimeSkins[0].joints).toEqual([
- runtimeNodes[1],
- runtimeNodes[2],
- ]);
- expect(runtimeSkins[0].jointMatrices.length).toEqual(2);
-
- const skinnedNodes = sceneGraph._skinnedNodes;
- expect(skinnedNodes[0]).toEqual(0);
-
- expect(runtimeNodes[0].computedJointMatrices.length).toEqual(2);
- }
+ it("creates runtime skin from model", async function () {
+ const model = await loadAndZoomToModelAsync(
+ { gltf: simpleSkinGltfUrl },
+ scene
);
+
+ const sceneGraph = model._sceneGraph;
+ const components = sceneGraph._components;
+ const runtimeNodes = sceneGraph._runtimeNodes;
+
+ expect(runtimeNodes[0].node).toEqual(components.nodes[0]);
+ expect(runtimeNodes[1].node).toEqual(components.nodes[1]);
+ expect(runtimeNodes[2].node).toEqual(components.nodes[2]);
+
+ const rootNodes = sceneGraph._rootNodes;
+ expect(rootNodes[0]).toEqual(0);
+ expect(rootNodes[1]).toEqual(1);
+
+ const runtimeSkins = sceneGraph._runtimeSkins;
+ expect(runtimeSkins[0].skin).toEqual(components.skins[0]);
+ expect(runtimeSkins[0].joints).toEqual([
+ runtimeNodes[1],
+ runtimeNodes[2],
+ ]);
+ expect(runtimeSkins[0].jointMatrices.length).toEqual(2);
+
+ const skinnedNodes = sceneGraph._skinnedNodes;
+ expect(skinnedNodes[0]).toEqual(0);
+
+ expect(runtimeNodes[0].computedJointMatrices.length).toEqual(2);
});
- it("creates articulation from model", function () {
- return loadAndZoomToModelAsync({ gltf: boxArticulationsUrl }, scene).then(
- function (model) {
- const sceneGraph = model._sceneGraph;
- const components = sceneGraph._components;
- const runtimeNodes = sceneGraph._runtimeNodes;
-
- expect(runtimeNodes[0].node).toEqual(components.nodes[0]);
-
- const rootNodes = sceneGraph._rootNodes;
- expect(rootNodes[0]).toEqual(0);
-
- const runtimeArticulations = sceneGraph._runtimeArticulations;
- const runtimeArticulation =
- runtimeArticulations["SampleArticulation"];
- expect(runtimeArticulation).toBeDefined();
- expect(runtimeArticulation.name).toBe("SampleArticulation");
- expect(runtimeArticulation.runtimeNodes.length).toBe(1);
- expect(runtimeArticulation.runtimeStages.length).toBe(10);
- }
+ it("creates articulation from model", async function () {
+ const model = await loadAndZoomToModelAsync(
+ { gltf: boxArticulationsUrl },
+ scene
);
+
+ const sceneGraph = model._sceneGraph;
+ const components = sceneGraph._components;
+ const runtimeNodes = sceneGraph._runtimeNodes;
+
+ expect(runtimeNodes[0].node).toEqual(components.nodes[0]);
+
+ const rootNodes = sceneGraph._rootNodes;
+ expect(rootNodes[0]).toEqual(0);
+
+ const runtimeArticulations = sceneGraph._runtimeArticulations;
+ const runtimeArticulation = runtimeArticulations["SampleArticulation"];
+ expect(runtimeArticulation).toBeDefined();
+ expect(runtimeArticulation.name).toBe("SampleArticulation");
+ expect(runtimeArticulation.runtimeNodes.length).toBe(1);
+ expect(runtimeArticulation.runtimeStages.length).toBe(10);
});
- it("applies articulations", function () {
- return loadAndZoomToModelAsync(
+ it("applies articulations", async function () {
+ const model = await loadAndZoomToModelAsync(
{
gltf: boxArticulationsUrl,
},
scene
- ).then(function (model) {
- const sceneGraph = model._sceneGraph;
- const runtimeNodes = sceneGraph._runtimeNodes;
- const rootNode = runtimeNodes[0];
-
- expect(rootNode.transform).toEqual(rootNode.originalTransform);
-
- sceneGraph.setArticulationStage("SampleArticulation MoveX", 1.0);
- sceneGraph.setArticulationStage("SampleArticulation MoveY", 2.0);
- sceneGraph.setArticulationStage("SampleArticulation MoveZ", 3.0);
- sceneGraph.setArticulationStage("SampleArticulation Yaw", 4.0);
- sceneGraph.setArticulationStage("SampleArticulation Pitch", 5.0);
- sceneGraph.setArticulationStage("SampleArticulation Roll", 6.0);
- sceneGraph.setArticulationStage("SampleArticulation Size", 0.9);
- sceneGraph.setArticulationStage("SampleArticulation SizeX", 0.8);
- sceneGraph.setArticulationStage("SampleArticulation SizeY", 0.7);
- sceneGraph.setArticulationStage("SampleArticulation SizeZ", 0.6);
-
- // Articulations shouldn't affect the node until applyArticulations is called.
- expect(rootNode.transform).toEqual(rootNode.originalTransform);
-
- sceneGraph.applyArticulations();
-
- // prettier-ignore
- const expected = [
- 0.714769048324, -0.0434061192623, -0.074974104652, 0,
- -0.061883302957, 0.0590679731276, -0.624164586760, 0,
- 0.037525155822, 0.5366347296529, 0.047064101083, 0,
- 1, 3, -2, 1,
- ];
-
- expect(rootNode.transform).toEqualEpsilon(
- expected,
- CesiumMath.EPSILON10
- );
- });
+ );
+ const sceneGraph = model._sceneGraph;
+ const runtimeNodes = sceneGraph._runtimeNodes;
+ const rootNode = runtimeNodes[0];
+
+ expect(rootNode.transform).toEqual(rootNode.originalTransform);
+
+ sceneGraph.setArticulationStage("SampleArticulation MoveX", 1.0);
+ sceneGraph.setArticulationStage("SampleArticulation MoveY", 2.0);
+ sceneGraph.setArticulationStage("SampleArticulation MoveZ", 3.0);
+ sceneGraph.setArticulationStage("SampleArticulation Yaw", 4.0);
+ sceneGraph.setArticulationStage("SampleArticulation Pitch", 5.0);
+ sceneGraph.setArticulationStage("SampleArticulation Roll", 6.0);
+ sceneGraph.setArticulationStage("SampleArticulation Size", 0.9);
+ sceneGraph.setArticulationStage("SampleArticulation SizeX", 0.8);
+ sceneGraph.setArticulationStage("SampleArticulation SizeY", 0.7);
+ sceneGraph.setArticulationStage("SampleArticulation SizeZ", 0.6);
+
+ // Articulations shouldn't affect the node until applyArticulations is called.
+ expect(rootNode.transform).toEqual(rootNode.originalTransform);
+
+ sceneGraph.applyArticulations();
+
+ // prettier-ignore
+ const expected = [
+ 0.714769048324, -0.0434061192623, -0.074974104652, 0,
+ -0.061883302957, 0.0590679731276, -0.624164586760, 0,
+ 0.037525155822, 0.5366347296529, 0.047064101083, 0,
+ 1, 3, -2, 1,
+ ];
+
+ expect(rootNode.transform).toEqualEpsilon(expected, CesiumMath.EPSILON10);
});
- it("adds ModelColorPipelineStage when color is set on the model", function () {
+ it("adds ModelColorPipelineStage when color is set on the model", async function () {
spyOn(ModelColorPipelineStage, "process");
- return loadAndZoomToModelAsync(
+ await loadAndZoomToModelAsync(
{
color: Color.RED,
gltf: parentGltfUrl,
},
scene
- ).then(function () {
- expect(ModelColorPipelineStage.process).toHaveBeenCalled();
- });
+ );
+ expect(ModelColorPipelineStage.process).toHaveBeenCalled();
});
- it("adds CustomShaderPipelineStage when customShader is set on the model", function () {
+ it("adds CustomShaderPipelineStage when customShader is set on the model", async function () {
spyOn(CustomShaderPipelineStage, "process");
- return loadAndZoomToModelAsync(
+ const model = await loadAndZoomToModelAsync(
{
gltf: buildingsMetadata,
},
scene
- ).then(function (model) {
- model.customShader = new CustomShader();
- model.update(scene.frameState);
- expect(CustomShaderPipelineStage.process).toHaveBeenCalled();
- });
+ );
+ model.customShader = new CustomShader();
+ model.update(scene.frameState);
+ expect(CustomShaderPipelineStage.process).toHaveBeenCalled();
});
- it("pushDrawCommands ignores hidden nodes", function () {
- return loadAndZoomToModelAsync(
+ it("does not add fog stage when fog is not enabled", async function () {
+ spyOn(AtmospherePipelineStage, "process");
+ scene.fog.enabled = false;
+ scene.fog.renderable = false;
+ const model = await loadAndZoomToModelAsync(
+ {
+ gltf: buildingsMetadata,
+ },
+ scene
+ );
+ model.customShader = new CustomShader();
+ model.update(scene.frameState);
+ expect(AtmospherePipelineStage.process).not.toHaveBeenCalled();
+ });
+
+ it("does not add fog stage when fog is not renderable", async function () {
+ spyOn(AtmospherePipelineStage, "process");
+ scene.fog.enabled = true;
+ scene.fog.renderable = false;
+ const model = await loadAndZoomToModelAsync(
+ {
+ gltf: buildingsMetadata,
+ },
+ scene
+ );
+ model.customShader = new CustomShader();
+ model.update(scene.frameState);
+ expect(AtmospherePipelineStage.process).not.toHaveBeenCalled();
+ });
+
+ it("adds fog stage when fog is enabled and renderable", async function () {
+ spyOn(AtmospherePipelineStage, "process");
+ scene.fog.enabled = true;
+ scene.fog.renderable = true;
+ const model = await loadAndZoomToModelAsync(
+ {
+ gltf: buildingsMetadata,
+ },
+ scene
+ );
+ model.customShader = new CustomShader();
+ model.update(scene.frameState);
+ expect(AtmospherePipelineStage.process).toHaveBeenCalled();
+ });
+
+ it("pushDrawCommands ignores hidden nodes", async function () {
+ const model = await loadAndZoomToModelAsync(
{
gltf: duckUrl,
},
scene
- ).then(function (model) {
- const frameState = scene.frameState;
- const commandList = frameState.commandList;
-
- const sceneGraph = model._sceneGraph;
- const rootNode = sceneGraph._runtimeNodes[0];
- const meshNode = sceneGraph._runtimeNodes[2];
-
- expect(rootNode.show).toBe(true);
- expect(meshNode.show).toBe(true);
-
- sceneGraph.pushDrawCommands(frameState);
- const originalLength = commandList.length;
- expect(originalLength).not.toEqual(0);
-
- commandList.length = 0;
- meshNode.show = false;
- sceneGraph.pushDrawCommands(frameState);
- expect(commandList.length).toEqual(0);
-
- meshNode.show = true;
- rootNode.show = false;
- sceneGraph.pushDrawCommands(frameState);
- expect(commandList.length).toEqual(0);
-
- rootNode.show = true;
- sceneGraph.pushDrawCommands(frameState);
- expect(commandList.length).toEqual(originalLength);
- });
+ );
+ const frameState = scene.frameState;
+ const commandList = frameState.commandList;
+
+ const sceneGraph = model._sceneGraph;
+ const rootNode = sceneGraph._runtimeNodes[0];
+ const meshNode = sceneGraph._runtimeNodes[2];
+
+ expect(rootNode.show).toBe(true);
+ expect(meshNode.show).toBe(true);
+
+ sceneGraph.pushDrawCommands(frameState);
+ const originalLength = commandList.length;
+ expect(originalLength).not.toEqual(0);
+
+ commandList.length = 0;
+ meshNode.show = false;
+ sceneGraph.pushDrawCommands(frameState);
+ expect(commandList.length).toEqual(0);
+
+ meshNode.show = true;
+ rootNode.show = false;
+ sceneGraph.pushDrawCommands(frameState);
+ expect(commandList.length).toEqual(0);
+
+ rootNode.show = true;
+ sceneGraph.pushDrawCommands(frameState);
+ expect(commandList.length).toEqual(originalLength);
});
it("throws for undefined options.model", function () {
diff --git a/packages/engine/Specs/Scene/Model/ModelSpec.js b/packages/engine/Specs/Scene/Model/ModelSpec.js
index 25c1f5c802a2..bec39b42978a 100644
--- a/packages/engine/Specs/Scene/Model/ModelSpec.js
+++ b/packages/engine/Specs/Scene/Model/ModelSpec.js
@@ -1,4 +1,5 @@
import {
+ Atmosphere,
Axis,
Cartesian2,
Cartesian3,
@@ -12,11 +13,14 @@ import {
Credit,
defaultValue,
defined,
+ DirectionalLight,
DistanceDisplayCondition,
+ DynamicAtmosphereLightingType,
DracoLoader,
Ellipsoid,
Event,
FeatureDetection,
+ Fog,
HeadingPitchRange,
HeadingPitchRoll,
HeightReference,
@@ -39,6 +43,7 @@ import {
ShadowMode,
SplitDirection,
StyleCommandsNeeded,
+ SunLight,
Transforms,
WireframeIndexGenerator,
} from "../../../index.js";
@@ -4411,6 +4416,441 @@ describe(
});
});
+ describe("fog", function () {
+ const sunnyDate = JulianDate.fromIso8601("2024-01-11T15:00:00Z");
+ const darkDate = JulianDate.fromIso8601("2024-01-11T00:00:00Z");
+
+ afterEach(function () {
+ scene.atmosphere = new Atmosphere();
+ scene.fog = new Fog();
+ scene.light = new SunLight();
+ scene.camera.switchToPerspectiveFrustum();
+ });
+
+ function viewFog(scene, model) {
+ // In order for fog to create a visible change, the camera needs to be
+ // further away from the model. This would make the box sub-pixel
+ // so to make it fill the canvas, use an ortho camera the same
+ // width of the box to make the scene look 2D.
+ const center = model.boundingSphere.center;
+ scene.camera.lookAt(center, new Cartesian3(1000, 0, 0));
+ scene.camera.switchToOrthographicFrustum();
+ scene.camera.frustum.width = 1;
+ }
+
+ it("renders a model in fog", async function () {
+ // Move the fog very close to the camera;
+ scene.fog.density = 1.0;
+
+ // Increase the brightness to make the fog color
+ // stand out more for this test
+ scene.atmosphere.brightnessShift = 1.0;
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(
+ Cartesian3.fromDegrees(0, 0, 10.0)
+ ),
+ },
+ scene
+ );
+
+ viewFog(scene, model);
+
+ const renderOptions = {
+ scene,
+ time: sunnyDate,
+ };
+
+ // First, turn off the fog to capture the original color
+ let originalColor;
+ scene.fog.enabled = false;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ originalColor = rgba;
+ expect(originalColor).not.toEqual([0, 0, 0, 255]);
+ });
+
+ // Now turn on fog. The result should be bluish
+ // than before due to scattering.
+ scene.fog.enabled = true;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+
+ // The result should have a bluish tint
+ const [r, g, b, a] = rgba;
+ expect(b).toBeGreaterThan(r);
+ expect(b).toBeGreaterThan(g);
+ expect(a).toBe(255);
+ });
+ });
+
+ it("renders a model in fog (sunlight)", async function () {
+ // Move the fog very close to the camera;
+ scene.fog.density = 1.0;
+
+ // Increase the brightness to make the fog color
+ // stand out more for this test
+ scene.atmosphere.brightnessShift = 1.0;
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(
+ Cartesian3.fromDegrees(0, 0, 10.0)
+ ),
+ },
+ scene
+ );
+
+ // In order for fog to render, the camera needs to be
+ // further away from the model. This would make the box sub-pixel
+ // so to make it fill the canvas, use an ortho camera the same
+ // width of the box to make the scene look 2D.
+ const center = model.boundingSphere.center;
+ scene.camera.lookAt(center, new Cartesian3(1000, 0, 0));
+ scene.camera.switchToOrthographicFrustum();
+ scene.camera.frustum.width = 1;
+
+ // Grab the color when dynamic lighting is off for comparison
+ scene.atmosphere.dynamicLighting = DynamicAtmosphereLightingType.NONE;
+ const renderOptions = {
+ scene,
+ time: sunnyDate,
+ };
+ let originalColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ originalColor = rgba;
+ expect(originalColor).not.toEqual([0, 0, 0, 255]);
+ });
+
+ // switch the lighting model to sunlight
+ scene.atmosphere.dynamicLighting =
+ DynamicAtmosphereLightingType.SUNLIGHT;
+
+ // Render in the sun, it should be a different color than the
+ // original
+ let sunnyColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ sunnyColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+ });
+
+ // Render in the dark, it should be a different color and
+ // darker than in sun
+ renderOptions.time = darkDate;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+ expect(rgba).not.toEqual(sunnyColor);
+
+ const [sunR, sunG, sunB, sunA] = sunnyColor;
+ const [r, g, b, a] = rgba;
+ expect(r).toBeLessThan(sunR);
+ expect(g).toBeLessThan(sunG);
+ expect(b).toBeLessThan(sunB);
+ expect(a).toBe(sunA);
+ });
+ });
+
+ it("renders a model in fog (scene light)", async function () {
+ // Move the fog very close to the camera;
+ scene.fog.density = 1.0;
+
+ // Increase the brightness to make the fog color
+ // stand out more for this test
+ scene.atmosphere.brightnessShift = 1.0;
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(
+ Cartesian3.fromDegrees(0, 0, 10.0)
+ ),
+ },
+ scene
+ );
+
+ viewFog(scene, model);
+
+ // Grab the color when dynamic lighting is off for comparison
+ scene.atmosphere.dynamicLighting = DynamicAtmosphereLightingType.NONE;
+ const renderOptions = {
+ scene,
+ time: sunnyDate,
+ };
+ let originalColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ originalColor = rgba;
+ expect(originalColor).not.toEqual([0, 0, 0, 255]);
+ });
+
+ // Also grab the color in sunlight for comparison
+ scene.atmosphere.dynamicLighting =
+ DynamicAtmosphereLightingType.SUNLIGHT;
+ let sunnyColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ sunnyColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+ });
+
+ // Set a light on the scene, but since dynamicLighting is SUNLIGHT,
+ // it should have no effect yet
+ scene.light = new DirectionalLight({
+ direction: new Cartesian3(0, 1, 0),
+ });
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).toEqual(sunnyColor);
+ });
+
+ // Set dynamic lighting to use the scene light, now it should
+ // render a different color from the other light sources
+ scene.atmosphere.dynamicLighting =
+ DynamicAtmosphereLightingType.SCENE_LIGHT;
+
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+ expect(rgba).not.toEqual(sunnyColor);
+ });
+ });
+
+ it("adjusts atmosphere light intensity", async function () {
+ // Move the fog very close to the camera;
+ scene.fog.density = 1.0;
+
+ // Increase the brightness to make the fog color
+ // stand out more. We'll use the light intensity to
+ // modulate this.
+ scene.atmosphere.brightnessShift = 1.0;
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(
+ Cartesian3.fromDegrees(0, 0, 10.0)
+ ),
+ },
+ scene
+ );
+
+ viewFog(scene, model);
+
+ const renderOptions = {
+ scene,
+ time: sunnyDate,
+ };
+
+ // Grab the original color.
+ let originalColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ originalColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+
+ // The result should have a bluish tint from the atmosphere
+ const [r, g, b, a] = rgba;
+ expect(b).toBeGreaterThan(r);
+ expect(b).toBeGreaterThan(g);
+ expect(a).toBe(255);
+ });
+
+ // Turn down the light intensity
+ scene.atmosphere.lightIntensity = 5.0;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+
+ // Check that each component (except alpha) is darker than before
+ const [oldR, oldG, oldB, oldA] = originalColor;
+ const [r, g, b, a] = rgba;
+ expect(r).toBeLessThan(oldR);
+ expect(g).toBeLessThan(oldG);
+ expect(b).toBeLessThan(oldB);
+ expect(a).toBe(oldA);
+ });
+ });
+
+ it("applies a hue shift", async function () {
+ // Move the fog very close to the camera;
+ scene.fog.density = 1.0;
+
+ // Increase the brightness to make the fog color
+ // stand out more for this test
+ scene.atmosphere.brightnessShift = 1.0;
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(
+ Cartesian3.fromDegrees(0, 0, 10.0)
+ ),
+ },
+ scene
+ );
+
+ viewFog(scene, model);
+
+ const renderOptions = {
+ scene,
+ time: sunnyDate,
+ };
+
+ // Grab the original color.
+ let originalColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ originalColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+
+ // The result should have a bluish tint from the atmosphere
+ const [r, g, b, a] = rgba;
+ expect(b).toBeGreaterThan(r);
+ expect(b).toBeGreaterThan(g);
+ expect(a).toBe(255);
+ });
+
+ // Shift the fog color to be reddish
+ scene.atmosphere.hueShift = 0.4;
+ let redColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ redColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(redColor).not.toEqual(originalColor);
+
+ // Check for a reddish tint
+ const [r, g, b, a] = rgba;
+ expect(r).toBeGreaterThan(g);
+ expect(r).toBeGreaterThan(b);
+ expect(a).toBe(255);
+ });
+
+ // ...now greenish
+ scene.atmosphere.hueShift = 0.7;
+ let greenColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ greenColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(greenColor).not.toEqual(originalColor);
+ expect(greenColor).not.toEqual(redColor);
+
+ // Check for a greenish tint
+ const [r, g, b, a] = rgba;
+ expect(g).toBeGreaterThan(r);
+ expect(g).toBeGreaterThan(b);
+ expect(a).toBe(255);
+ });
+
+ // ...and all the way around the color wheel back to bluish
+ scene.atmosphere.hueShift = 1.0;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).toEqual(originalColor);
+ });
+ });
+
+ it("applies a brightness shift", async function () {
+ // Move the fog very close to the camera;
+ scene.fog.density = 1.0;
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(
+ Cartesian3.fromDegrees(0, 0, 10.0)
+ ),
+ },
+ scene
+ );
+
+ viewFog(scene, model);
+
+ const renderOptions = {
+ scene,
+ time: sunnyDate,
+ };
+
+ // Grab the original color.
+ let originalColor;
+ scene.atmosphere.brightnessShift = 1.0;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ originalColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+
+ // The result should have a bluish tint from the atmosphere
+ const [r, g, b, a] = rgba;
+ expect(b).toBeGreaterThan(r);
+ expect(b).toBeGreaterThan(g);
+ expect(a).toBe(255);
+ });
+
+ // Turn down the brightness
+ scene.atmosphere.brightnessShift = 0.5;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+
+ // Check that each component (except alpha) is darker than before
+ const [oldR, oldG, oldB, oldA] = originalColor;
+ const [r, g, b, a] = rgba;
+ expect(r).toBeLessThan(oldR);
+ expect(g).toBeLessThan(oldG);
+ expect(b).toBeLessThan(oldB);
+ expect(a).toBe(oldA);
+ });
+ });
+
+ it("applies a saturation shift", async function () {
+ // Move the fog very close to the camera;
+ scene.fog.density = 1.0;
+
+ const model = await loadAndZoomToModelAsync(
+ {
+ url: boxTexturedGltfUrl,
+ modelMatrix: Transforms.eastNorthUpToFixedFrame(
+ Cartesian3.fromDegrees(0, 0, 10.0)
+ ),
+ },
+ scene
+ );
+
+ viewFog(scene, model);
+
+ const renderOptions = {
+ scene,
+ time: sunnyDate,
+ };
+
+ // Grab the original color.
+ let originalColor;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ originalColor = rgba;
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+
+ // The result should have a bluish tint from the atmosphere
+ const [r, g, b, a] = rgba;
+ expect(b).toBeGreaterThan(r);
+ expect(b).toBeGreaterThan(g);
+ expect(a).toBe(255);
+ });
+
+ // Turn down the saturation all the way
+ scene.atmosphere.saturationShift = -1.0;
+ expect(renderOptions).toRenderAndCall(function (rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba).not.toEqual(originalColor);
+
+ // Check that each component (except alpha) is the same
+ // as grey values have R = G = B
+ const [r, g, b, a] = rgba;
+ expect(g).toBe(r);
+ expect(b).toBe(g);
+ expect(a).toBe(255);
+ });
+ });
+ });
+
it("pick returns position of intersection between ray and model surface", async function () {
const model = await loadAndZoomToModelAsync(
{
diff --git a/packages/engine/Specs/Scene/SceneSpec.js b/packages/engine/Specs/Scene/SceneSpec.js
index 74a8a66a7efb..55ebadab6fe0 100644
--- a/packages/engine/Specs/Scene/SceneSpec.js
+++ b/packages/engine/Specs/Scene/SceneSpec.js
@@ -1,4 +1,5 @@
import {
+ Atmosphere,
BoundingSphere,
Cartesian2,
Cartesian3,
@@ -577,7 +578,7 @@ describe(
});
});
- it("renders sky atmopshere without a globe", function () {
+ it("renders sky atmosphere without a globe", function () {
s.globe = new Globe(Ellipsoid.UNIT_SPHERE);
s.globe.show = false;
s.camera.position = new Cartesian3(1.02, 0.0, 0.0);
@@ -2471,6 +2472,26 @@ describe(
scene.destroyForSpecs();
});
});
+
+ it("updates frameState.atmosphere", function () {
+ const scene = createScene();
+ const frameState = scene.frameState;
+
+ // Before the first render, the atmosphere has not yet been set
+ expect(frameState.atmosphere).toBeUndefined();
+
+ // On the first render, the atmosphere settings are propagated to the
+ // frame state
+ const originalAtmosphere = scene.atmosphere;
+ scene.renderForSpecs();
+ expect(frameState.atmosphere).toBe(originalAtmosphere);
+
+ // If we change the atmosphere to a new object
+ const anotherAtmosphere = new Atmosphere();
+ scene.atmosphere = anotherAtmosphere;
+ scene.renderForSpecs();
+ expect(frameState.atmosphere).toBe(anotherAtmosphere);
+ });
},
"WebGL"
diff --git a/packages/engine/Specs/Scene/SkyAtmosphereSpec.js b/packages/engine/Specs/Scene/SkyAtmosphereSpec.js
index edc027b84dea..b7be045cfd39 100644
--- a/packages/engine/Specs/Scene/SkyAtmosphereSpec.js
+++ b/packages/engine/Specs/Scene/SkyAtmosphereSpec.js
@@ -1,5 +1,6 @@
import {
Cartesian3,
+ DynamicAtmosphereLightingType,
Ellipsoid,
SceneMode,
SkyAtmosphere,
@@ -52,9 +53,9 @@ describe(
s.destroy();
});
- it("draws sky with setDynamicAtmosphereColor set to true", function () {
+ it("draws sky with dynamic lighting (scene light source)", function () {
const s = new SkyAtmosphere();
- s.setDynamicAtmosphereColor(true, false);
+ s.setDynamicLighting(DynamicAtmosphereLightingType.SCENE_LIGHT);
expect(scene).toRender([0, 0, 0, 255]);
scene.render();
@@ -67,9 +68,9 @@ describe(
s.destroy();
});
- it("draws sky with setDynamicAtmosphereColor set to true using the sun direction", function () {
+ it("draws sky with dynamic lighting (sunlight)", function () {
const s = new SkyAtmosphere();
- s.setDynamicAtmosphereColor(true, true);
+ s.setDynamicLighting(DynamicAtmosphereLightingType.SUNLIGHT);
expect(scene).toRender([0, 0, 0, 255]);
scene.render();
@@ -82,9 +83,9 @@ describe(
s.destroy();
});
- it("draws sky with setDynamicAtmosphereColor set to false", function () {
+ it("draws sky with dynamic lighting off", function () {
const s = new SkyAtmosphere();
- s.setDynamicAtmosphereColor(false, false);
+ s.setDynamicLighting(DynamicAtmosphereLightingType.NONE);
expect(scene).toRender([0, 0, 0, 255]);
scene.render();