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} enabled true 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();