diff --git a/Apps/Sandcastle/gallery/Image-Based Lighting.html b/Apps/Sandcastle/gallery/Image-Based Lighting.html index da68bd9c130b..032f3295ae76 100644 --- a/Apps/Sandcastle/gallery/Image-Based Lighting.html +++ b/Apps/Sandcastle/gallery/Image-Based Lighting.html @@ -24,25 +24,7 @@

Loading...

-
- - - - - - - -
Luminance at Zenith - - -
-
+
+ + + + +
+

Loading...

+
+
+
+ + + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + diff --git a/Apps/Sandcastle/gallery/glTF PBR Extensions.html b/Apps/Sandcastle/gallery/glTF PBR Extensions.html index 6168860f97fa..a098495379b3 100644 --- a/Apps/Sandcastle/gallery/glTF PBR Extensions.html +++ b/Apps/Sandcastle/gallery/glTF PBR Extensions.html @@ -115,7 +115,6 @@ const coefficients = [L00, L1_1, L10, L11, L2_2, L2_1, L20, L21, L22]; const imageBasedLighting = new Cesium.ImageBasedLighting({ - luminanceAtZenith: 0.7, sphericalHarmonicCoefficients: coefficients, specularEnvironmentMaps: environmentMapURL, }); @@ -136,7 +135,7 @@ imageBasedLighting.sphericalHarmonicCoefficients = undefined; imageBasedLighting.specularEnvironmentMaps = undefined; imageBasedLighting.imageBasedLightingFactor = Cesium.Cartesian2.ONE; - scene.light.intensity = 1.0; + scene.light.intensity = 2.0; }, }, { diff --git a/CHANGES.md b/CHANGES.md index e47fa63596c9..cff06df4ce3d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,8 +4,25 @@ #### @cesium/engine +##### Breaking Changes :mega: + +- Updated default 3D Tiles and Model lighting when using PBR in order to create a more realistic appearance. To approximate previous default lighting, use the following settings: + + ```js + const environmentMapManager = model.environmentMapManager; // or tileset.environmentMapManager; + environmentMapManager.saturation = 0.35; + environmentMapManager.brightness = 1.4; + environmentMapManager.gamma = 0.8; + environmentMapManager.atmosphereScatteringIntensity = 5.0; + environmentMapManager.groundColor = + Cesium.Color.fromCssColorString("#001850"); + ``` + +- `ImageBasedLighting.luminanceAtZenith` has been removed. Use `DynamicEnvironmentMapManager.atmosphereScatteringIntensity` instead. [#12129](https://github.com/CesiumGS/cesium/pull/12129) + ##### Additions :tada: +- Updated default 3D Tiles and Model lighting when using PBR in order to create a more realistic appearance. Added `DynamicEnvironmentMapManager` to control lighting parameters. These can be accessed via `Cesium3DTileset.environmentMapManager` and `Model.environmentMapManager`. [#12129](https://github.com/CesiumGS/cesium/pull/12129) - Added `ScreenSpaceCameraController.maximumTiltAngle` to limit how much the camera can tilt. [#12169](https://github.com/CesiumGS/cesium/pull/12169) - Update Japan Buildings sandcastle to use Japan Regional Terrain [#12259](https://github.com/CesiumGS/cesium/pull/12259) - Update Bing Maps attribution link [#12229] (https://github.com/CesiumGS/cesium/pull/12265) @@ -13,6 +30,7 @@ ##### Fixes :wrench: - Fix flickering issue caused by bounding sphere retrieval being blocked by the bounding sphere of another entity. [#12230](https://github.com/CesiumGS/cesium/pull/12230) +- Fixed `ImageBasedLighting.imageBasedLightingFactor` not affecting lighting. [#12129](https://github.com/CesiumGS/cesium/pull/12129) ### 1.122 - 2024-10-01 diff --git a/Specs/Cesium3DTilesTester.js b/Specs/Cesium3DTilesTester.js index 430c177ce7eb..6435df08fbb0 100644 --- a/Specs/Cesium3DTilesTester.js +++ b/Specs/Cesium3DTilesTester.js @@ -1,8 +1,10 @@ import { + Cartesian3, Color, defaultValue, defined, JulianDate, + ImageBasedLighting, Resource, Cesium3DTileContentFactory, Cesium3DTileset, @@ -102,12 +104,35 @@ Cesium3DTilesTester.waitForTilesLoaded = function (scene, tileset) { }); }; +// A white ambient light with low intensity +const defaultIbl = new ImageBasedLighting({ + sphericalHarmonicCoefficients: [ + new Cartesian3(0.4, 0.4, 0.4), + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + ], +}); + Cesium3DTilesTester.loadTileset = async function (scene, url, options) { options = defaultValue(options, {}); options.cullRequestsWhileMoving = defaultValue( options.cullRequestsWhileMoving, false, ); + options.imageBasedLighting = defaultValue( + options.imageBasedLighting, + defaultIbl, + ); + options.environmentMapOptions = { + enabled: false, // disable other diffuse lighting by default + ...options.environmentMapOptions, + }; const tileset = await Cesium3DTileset.fromUrl(url, options); diff --git a/packages/engine/Source/DataSources/ModelGraphics.js b/packages/engine/Source/DataSources/ModelGraphics.js index 58f6d142694a..8320768fa319 100644 --- a/packages/engine/Source/DataSources/ModelGraphics.js +++ b/packages/engine/Source/DataSources/ModelGraphics.js @@ -430,6 +430,7 @@ ModelGraphics.prototype.merge = function (source) { this.imageBasedLightingFactor, source.imageBasedLightingFactor, ); + this.lightColor = defaultValue(this.lightColor, source.lightColor); this.distanceDisplayCondition = defaultValue( this.distanceDisplayCondition, diff --git a/packages/engine/Source/Renderer/CubeMap.js b/packages/engine/Source/Renderer/CubeMap.js index 89400a607846..7526ed2b42a4 100644 --- a/packages/engine/Source/Renderer/CubeMap.js +++ b/packages/engine/Source/Renderer/CubeMap.js @@ -1,17 +1,24 @@ +import BoxGeometry from "../Core/BoxGeometry.js"; +import Cartesian3 from "../Core/Cartesian3.js"; import Check from "../Core/Check.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; +import GeometryPipeline from "../Core/GeometryPipeline.js"; import CesiumMath from "../Core/Math.js"; import PixelFormat from "../Core/PixelFormat.js"; +import VertexFormat from "../Core/VertexFormat.js"; +import BufferUsage from "./BufferUsage.js"; import ContextLimits from "./ContextLimits.js"; import CubeMapFace from "./CubeMapFace.js"; +import Framebuffer from "./Framebuffer.js"; import MipmapHint from "./MipmapHint.js"; import PixelDatatype from "./PixelDatatype.js"; import Sampler from "./Sampler.js"; import TextureMagnificationFilter from "./TextureMagnificationFilter.js"; import TextureMinificationFilter from "./TextureMinificationFilter.js"; +import VertexArray from "./VertexArray.js"; /** * @typedef CubeMap.BufferSource @@ -105,7 +112,7 @@ function CubeMap(options) { ({ width, height } = source.positiveX); //>>includeStart('debug', pragmas.debug); - for (const faceName of CubeMap.getFaceNames()) { + for (const faceName of CubeMap.faceNames()) { const face = source[faceName]; if (Number(face.width) !== width || Number(face.height) !== height) { throw new DeveloperError( @@ -231,19 +238,45 @@ function CubeMap(options) { ); } - for (const faceName of CubeMap.getFaceNames()) { + for (const faceName of CubeMap.faceNames()) { loadFace(this[faceName], source?.[faceName], 0); } gl.bindTexture(textureTarget, null); } +/** + * Copy an existing texture to a cubemap face. + * @param {FrameState} frameState The current rendering frameState + * @param {Texture} texture Texture being copied + * @param {CubeMap.FaceName} face The face to which to copy + * @param {number} [mipLevel=0] The mip level at which to copy + */ +CubeMap.prototype.copyFace = function (frameState, texture, face, mipLevel) { + const context = frameState.context; + const framebuffer = new Framebuffer({ + context: context, + colorTextures: [texture], + destroyAttachments: false, + }); + + framebuffer._bind(); + + this[face].copyMipmapFromFramebuffer( + 0, + 0, + texture.width, + texture.height, + defaultValue(mipLevel, 0), + ); + framebuffer._unBind(); + framebuffer.destroy(); +}; + /** * An enum defining the names of the faces of a cube map. - * * @alias {CubeMap.FaceName} * @enum {string} - * * @private */ CubeMap.FaceName = Object.freeze({ @@ -255,7 +288,7 @@ CubeMap.FaceName = Object.freeze({ NEGATIVEZ: "negativeZ", }); -function* makeFacesIterator() { +function* makeFaceNamesIterator() { yield CubeMap.FaceName.POSITIVEX; yield CubeMap.FaceName.NEGATIVEX; yield CubeMap.FaceName.POSITIVEY; @@ -266,22 +299,18 @@ function* makeFacesIterator() { /** * Creates an iterator for looping over the cubemap faces. - * * @type {Iterable} - * * @private */ -CubeMap.getFaceNames = function () { - return makeFacesIterator(); +CubeMap.faceNames = function () { + return makeFaceNamesIterator(); }; /** * Load texel data into one face of a cube map. - * * @param {CubeMapFace} cubeMapFace The face to which texel values will be loaded. * @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|CubeMap.BufferSource} [source] The source for texel values to be loaded into the texture. * @param {number} [mipLevel=0] The mip level to which the texel values will be loaded. - * * @private */ function loadFace(cubeMapFace, source, mipLevel) { @@ -361,6 +390,8 @@ function loadFace(cubeMapFace, source, mipLevel) { } } +CubeMap.loadFace = loadFace; + Object.defineProperties(CubeMap.prototype, { positiveX: { get: function () { @@ -447,6 +478,29 @@ Object.defineProperties(CubeMap.prototype, { }, }); +/** + * Get a vector representing the cubemap face direction + * @param {CubeMap.FaceName} face The relevant face + * @param {Cartesian3} [result] The object onto which to store the result. + * @returns {Cartesian3} The vector representing the cubemap face direction + */ +CubeMap.getDirection = function (face, result) { + switch (face) { + case CubeMap.FaceName.POSITIVEX: + return Cartesian3.clone(Cartesian3.UNIT_X, result); + case CubeMap.FaceName.NEGATIVEX: + return Cartesian3.negate(Cartesian3.UNIT_X, result); + case CubeMap.FaceName.POSITIVEY: + return Cartesian3.clone(Cartesian3.UNIT_Y, result); + case CubeMap.FaceName.NEGATIVEY: + return Cartesian3.negate(Cartesian3.UNIT_Y, result); + case CubeMap.FaceName.POSITIVEZ: + return Cartesian3.clone(Cartesian3.UNIT_Z, result); + case CubeMap.FaceName.NEGATIVEZ: + return Cartesian3.negate(Cartesian3.UNIT_Z, result); + } +}; + /** * Set up a sampler for use with a cube map. * @param {CubeMap} cubeMap The cube map containing the texture to be sampled by this sampler. @@ -539,7 +593,7 @@ CubeMap.prototype.loadMipmaps = function (source, skipColorSpaceConversion) { const mipSource = source[i]; // mipLevel 0 was the base layer, already loaded when the CubeMap was constructed. const mipLevel = i + 1; - for (const faceName of CubeMap.getFaceNames()) { + for (const faceName of CubeMap.faceNames()) { loadFace(this[faceName], mipSource[faceName], mipLevel); } } @@ -592,6 +646,29 @@ CubeMap.prototype.generateMipmap = function (hint) { gl.bindTexture(target, null); }; +/** + * Create a vertex array that can be used for cubemap shaders. + * @param {Context} context The rendering context + * @returns {VertexArray} The created vertex array + */ +CubeMap.createVertexArray = function (context) { + const geometry = BoxGeometry.createGeometry( + BoxGeometry.fromDimensions({ + dimensions: new Cartesian3(2.0, 2.0, 2.0), + vertexFormat: VertexFormat.POSITION_ONLY, + }), + ); + const attributeLocations = (this._attributeLocations = + GeometryPipeline.createAttributeLocations(geometry)); + + return VertexArray.fromGeometry({ + context: context, + geometry: geometry, + attributeLocations: attributeLocations, + bufferUsage: BufferUsage.STATIC_DRAW, + }); +}; + CubeMap.prototype.isDestroyed = function () { return false; }; diff --git a/packages/engine/Source/Renderer/CubeMapFace.js b/packages/engine/Source/Renderer/CubeMapFace.js index 9fc7daf2533e..fcd31230b240 100644 --- a/packages/engine/Source/Renderer/CubeMapFace.js +++ b/packages/engine/Source/Renderer/CubeMapFace.js @@ -245,25 +245,22 @@ CubeMapFace.prototype.copyFrom = function (options) { /** * Copies texels from the framebuffer to the cubemap's face. - * * @param {number} [xOffset=0] An offset in the x direction in the cubemap where copying begins. * @param {number} [yOffset=0] An offset in the y direction in the cubemap where copying begins. * @param {number} [framebufferXOffset=0] An offset in the x direction in the framebuffer where copying begins from. * @param {number} [framebufferYOffset=0] An offset in the y direction in the framebuffer where copying begins from. * @param {number} [width=CubeMap's width] The width of the subimage to copy. * @param {number} [height=CubeMap's height] The height of the subimage to copy. - * - * @exception {DeveloperError} Cannot call copyFromFramebuffer when the texture pixel data type is FLOAT. - * @exception {DeveloperError} Cannot call copyFromFramebuffer when the texture pixel data type is HALF_FLOAT. - * @exception {DeveloperError} This CubeMap was destroyed, i.e., destroy() was called. - * @exception {DeveloperError} xOffset must be greater than or equal to zero. - * @exception {DeveloperError} yOffset must be greater than or equal to zero. - * @exception {DeveloperError} framebufferXOffset must be greater than or equal to zero. - * @exception {DeveloperError} framebufferYOffset must be greater than or equal to zero. - * @exception {DeveloperError} xOffset + source.width must be less than or equal to width. - * @exception {DeveloperError} yOffset + source.height must be less than or equal to height. - * @exception {DeveloperError} This CubeMap was destroyed, i.e., destroy() was called. - * + * @throws {DeveloperError} Cannot call copyFromFramebuffer when the texture pixel data type is FLOAT. + * @throws {DeveloperError} Cannot call copyFromFramebuffer when the texture pixel data type is HALF_FLOAT. + * @throws {DeveloperError} This CubeMap was destroyed, i.e., destroy() was called. + * @throws {DeveloperError} xOffset must be greater than or equal to zero. + * @throws {DeveloperError} yOffset must be greater than or equal to zero. + * @throws {DeveloperError} framebufferXOffset must be greater than or equal to zero. + * @throws {DeveloperError} framebufferYOffset must be greater than or equal to zero. + * @throws {DeveloperError} xOffset + source.width must be less than or equal to width. + * @throws {DeveloperError} yOffset + source.height must be less than or equal to height. + * @throws {DeveloperError} This CubeMap was destroyed, i.e., destroy() was called. * @example * // Copy the framebuffer contents to the +x cube map face. * cubeMap.positiveX.copyFromFramebuffer(); @@ -336,4 +333,84 @@ CubeMapFace.prototype.copyFromFramebuffer = function ( gl.bindTexture(target, null); this._initialized = true; }; + +/** + * Copies texels from the framebuffer to the cubemap's face mipmap. + * @param {number} [xOffset=0] An offset in the x direction in the framebuffer where copying begins from. + * @param {number} [yOffset=0] An offset in the y direction in the framebuffer where copying begins from. + * @param {number} [width=CubeMap's width] The width of the subimage to copy. + * @param {number} [height=CubeMap's height] The height of the subimage to copy. + * @param {number} [level=0] The level of detail. Level 0 is the base image level and level n is the n-th mipmap reduction level. + * @throws {DeveloperError} Cannot call copyFromFramebuffer when the texture pixel data type is FLOAT. + * @throws {DeveloperError} Cannot call copyFromFramebuffer when the texture pixel data type is HALF_FLOAT. + * @throws {DeveloperError} This CubeMap was destroyed, i.e., destroy() was called. + * @throws {DeveloperError} xOffset must be greater than or equal to zero. + * @throws {DeveloperError} yOffset must be greater than or equal to zero. + * @throws {DeveloperError} framebufferXOffset must be greater than or equal to zero. + * @throws {DeveloperError} framebufferYOffset must be greater than or equal to zero. + * @throws {DeveloperError} xOffset + source.width must be less than or equal to width. + * @throws {DeveloperError} yOffset + source.height must be less than or equal to height. + * @throws {DeveloperError} This CubeMap was destroyed, i.e., destroy() was called. + * + * @example + * // Copy the framebuffer contents to the +x cube map face. + * cubeMap.positiveX.copyFromFramebuffer(); + */ +CubeMapFace.prototype.copyMipmapFromFramebuffer = function ( + xOffset, + yOffset, + width, + height, + level, +) { + xOffset = defaultValue(xOffset, 0); + yOffset = defaultValue(yOffset, 0); + width = defaultValue(width, this._size); + height = defaultValue(height, this._size); + level = defaultValue(level, 0); + + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals("xOffset", xOffset, 0); + Check.typeOf.number.greaterThanOrEquals("yOffset", yOffset, 0); + + if (xOffset + width > this._size) { + throw new DeveloperError( + "xOffset + source.width must be less than or equal to width.", + ); + } + if (yOffset + height > this._size) { + throw new DeveloperError( + "yOffset + source.height must be less than or equal to height.", + ); + } + if (this._pixelDatatype === PixelDatatype.FLOAT) { + throw new DeveloperError( + "Cannot call copyFromFramebuffer when the texture pixel data type is FLOAT.", + ); + } + if (this._pixelDatatype === PixelDatatype.HALF_FLOAT) { + throw new DeveloperError( + "Cannot call copyFromFramebuffer when the texture pixel data type is HALF_FLOAT.", + ); + } + //>>includeEnd('debug'); + + const gl = this._context._gl; + const target = this._textureTarget; + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(target, this._texture); + gl.copyTexImage2D( + this._targetFace, + level, + this._internalFormat, + xOffset, + yOffset, + width, + height, + 0, + ); + gl.bindTexture(target, null); + this._initialized = true; +}; export default CubeMapFace; diff --git a/packages/engine/Source/Scene/Atmosphere.js b/packages/engine/Source/Scene/Atmosphere.js index 296ad0f19d25..3f0770feb99b 100644 --- a/packages/engine/Source/Scene/Atmosphere.js +++ b/packages/engine/Source/Scene/Atmosphere.js @@ -1,4 +1,5 @@ import Cartesian3 from "../Core/Cartesian3.js"; +import CesiumMath from "../Core/Math.js"; import DynamicAtmosphereLightingType from "./DynamicAtmosphereLightingType.js"; /** @@ -124,4 +125,25 @@ function Atmosphere() { this.dynamicLighting = DynamicAtmosphereLightingType.NONE; } +/** + * Returns true if the atmosphere shader requires a color correct step. + * @param {Atmosphere} atmosphere The atmosphere instance to check + * @returns {boolean} true if the atmosphere shader requires a color correct step + */ +Atmosphere.requiresColorCorrect = function (atmosphere) { + return !( + CesiumMath.equalsEpsilon(atmosphere.hueShift, 0.0, CesiumMath.EPSILON7) && + CesiumMath.equalsEpsilon( + atmosphere.saturationShift, + 0.0, + CesiumMath.EPSILON7, + ) && + CesiumMath.equalsEpsilon( + atmosphere.brightnessShift, + 0.0, + CesiumMath.EPSILON7, + ) + ); +}; + export default Atmosphere; diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index f3dc6a272f1a..e23d1061792b 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -61,6 +61,7 @@ import Cesium3DTilesetMostDetailedTraversal from "./Cesium3DTilesetMostDetailedT import Cesium3DTilesetBaseTraversal from "./Cesium3DTilesetBaseTraversal.js"; import Cesium3DTilesetSkipTraversal from "./Cesium3DTilesetSkipTraversal.js"; import Ray from "../Core/Ray.js"; +import DynamicEnvironmentMapManager from "./DynamicEnvironmentMapManager.js"; /** * @typedef {Object} Cesium3DTileset.ConstructorOptions @@ -104,6 +105,7 @@ import Ray from "../Core/Ray.js"; * @property {object} [pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting. * @property {Cartesian3} [lightColor] The light color when shading models. When undefined the scene's light color is used instead. * @property {ImageBasedLighting} [imageBasedLighting] The properties for managing image-based lighting for this tileset. + * @param {DynamicEnvironmentMapManager.ConstructorOptions} [options.environmentMapOptions] The properties for managing dynamic environment maps on this model. * @property {boolean} [backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled. * @property {boolean} [enableShowOutline=true] Whether to enable outlines for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. This can be set to false to avoid the additional processing of geometry at load time. When false, the showOutlines and outlineColor options are ignored. * @property {boolean} [showOutline=true] Whether to display the outline for models using the {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/CESIUM_primitive_outline|CESIUM_primitive_outline} extension. When true, outlines are displayed. When false, outlines are not displayed. @@ -827,6 +829,10 @@ function Cesium3DTileset(options) { this._shouldDestroyImageBasedLighting = true; } + this._environmentMapManager = new DynamicEnvironmentMapManager( + options.environmentMapOptions, + ); + /** * The light color when shading models. When undefined the scene's light color is used instead. *

@@ -1859,6 +1865,25 @@ Object.defineProperties(Cesium3DTileset.prototype, { }, }, + /** + * The properties for managing dynamic environment maps on this model. Affects lighting. + * + * @memberof Cesium3DTileset.prototype + * @readonly + * + * @example + * // Change the ground color used for a tileset's environment map to a forest green + * const environmentMapManager = tileset.environmentMapManager; + * environmentMapManager.groundColor = Cesium.Color.fromCssColorString("#203b34"); + * + * @type {DynamicEnvironmentMapManager} + */ + environmentMapManager: { + get: function () { + return this._environmentMapManager; + }, + }, + /** * Indicates that only the tileset's vector tiles should be used for classification. * @@ -2644,6 +2669,7 @@ function handleTileFailure(error, tileset, tile) { } else { console.log(`A 3D tile failed to load: ${url}`); console.log(`Error: ${message}`); + console.log(error.stack); } } @@ -3409,6 +3435,14 @@ Cesium3DTileset.prototype.updateForPass = function ( originalCullingVolume, ); + if (passOptions.isRender) { + const environmentMapManager = this._environmentMapManager; + if (defined(this._root)) { + environmentMapManager.position = this.boundingSphere.center; + } + environmentMapManager.update(frameState); + } + // Update clipping polygons const clippingPolygons = this._clippingPolygons; if (defined(clippingPolygons) && clippingPolygons.enabled) { @@ -3511,6 +3545,11 @@ Cesium3DTileset.prototype.destroy = function () { } this._imageBasedLighting = undefined; + if (!this._environmentMapManager.isDestroyed()) { + this._environmentMapManager.destroy(); + } + this._environmentMapManager = undefined; + return destroyObject(this); }; diff --git a/packages/engine/Source/Scene/DynamicEnvironmentMapManager.js b/packages/engine/Source/Scene/DynamicEnvironmentMapManager.js new file mode 100644 index 000000000000..d8d140ce29f9 --- /dev/null +++ b/packages/engine/Source/Scene/DynamicEnvironmentMapManager.js @@ -0,0 +1,852 @@ +import Cartesian2 from "../Core/Cartesian2.js"; +import Cartesian3 from "../Core/Cartesian3.js"; +import Cartesian4 from "../Core/Cartesian4.js"; +import Color from "../Core/Color.js"; +import defaultValue from "../Core/defaultValue.js"; +import defined from "../Core/defined.js"; +import destroyObject from "../Core/destroyObject.js"; +import DeveloperError from "../Core/DeveloperError.js"; +import JulianDate from "../Core/JulianDate.js"; +import Matrix4 from "../Core/Matrix4.js"; +import PixelFormat from "../Core/PixelFormat.js"; +import SceneMode from "./SceneMode.js"; +import Transforms from "../Core/Transforms.js"; +import ComputeCommand from "../Renderer/ComputeCommand.js"; +import CubeMap from "../Renderer/CubeMap.js"; +import Framebuffer from "../Renderer/Framebuffer.js"; +import Texture from "../Renderer/Texture.js"; +import PixelDatatype from "../Renderer/PixelDatatype.js"; +import Sampler from "../Renderer/Sampler.js"; +import ShaderProgram from "../Renderer/ShaderProgram.js"; +import ShaderSource from "../Renderer/ShaderSource.js"; +import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; +import Atmosphere from "./Atmosphere.js"; +import DynamicAtmosphereLightingType from "./DynamicAtmosphereLightingType.js"; +import AtmosphereCommon from "../Shaders/AtmosphereCommon.js"; +import ComputeIrradianceFS from "../Shaders/ComputeIrradianceFS.js"; +import ComputeRadianceMapFS from "../Shaders/ComputeRadianceMapFS.js"; +import ConvolveSpecularMapFS from "../Shaders/ConvolveSpecularMapFS.js"; +import ConvolveSpecularMapVS from "../Shaders/ConvolveSpecularMapVS.js"; + +/** + * @typedef {object} DynamicEnvironmentMapManager.ConstructorOptions + * Options for the DynamicEnvironmentMapManager constructor + * @property {boolean} [enabled=true] If true, the environment map and related properties will continue to update. + * @property {number} [mipmapLevels=10] The number of mipmap levels to generate for specular maps. More mipmap levels will produce a higher resolution specular reflection. + * @property {number} [maximumSecondsDifference=3600] The maximum amount of elapsed seconds before a new environment map is created. + * @property {number} [maximumPositionEpsilon=1000] The maximum difference in position before a new environment map is created, in meters. Small differences in position will not visibly affect results. + * @property {number} [atmosphereScatteringIntensity=2.0] The intensity of the scattered light emitted from the atmosphere. This should be adjusted relative to the value of {@link Scene.light} intensity. + * @property {number} [gamma=1.0] The gamma correction to apply to the range of light emitted from the environment. 1.0 uses the unmodified emitted light color. + * @property {number} [brightness=1.0] The brightness of light emitted from the environment. 1.0 uses the unmodified emitted environment color. Less than 1.0 makes the light darker while greater than 1.0 makes it brighter. + * @property {number} [saturation=1.0] The saturation of the light emitted from the environment. 1.0 uses the unmodified emitted environment color. Less than 1.0 reduces the saturation while greater than 1.0 increases it. + * @property {Color} [groundColor=DynamicEnvironmentMapManager.AVERAGE_EARTH_GROUND_COLOR] Solid color used to represent the ground. + * @property {number} [groundAlbedo=0.31] The percentage of light reflected from the ground. The average earth albedo is 0.31. + */ + +/** + * Generates an environment map at the given position based on scene's current lighting conditions. From this, it produces multiple levels of specular maps and spherical harmonic coefficients than can be used with {@link ImageBasedLighting} for models or tilesets. + * @alias DynamicEnvironmentMapManager + * @constructor + * @param {DynamicEnvironmentMapManager.ConstructorOptions} [options] An object describing initialization options. + * + * @example + * // Enable time-of-day environment mapping in a scene + * scene.atmosphere.dynamicLighting = Cesium.DynamicAtmosphereLightingType.SUNLIGHT; + * + * // Decrease the directional lighting contribution + * scene.light.intensity = 0.5 + * + * // Increase the intensity of of the environment map lighting contribution + * const environmentMapManager = tileset.environmentMapManager; + * environmentMapManager.atmosphereScatteringIntensity = 3.0; + * + * @example + * // Change the ground color used for a model's environment map to a forest green + * const environmentMapManager = model.environmentMapManager; + * environmentMapManager.groundColor = Cesium.Color.fromCssColorString("#203b34"); + */ +function DynamicEnvironmentMapManager(options) { + this._position = undefined; + + this._radianceMapDirty = false; + this._radianceCommandsDirty = false; + this._convolutionsCommandsDirty = false; + this._irradianceCommandDirty = false; + this._irradianceTextureDirty = false; + this._sphericalHarmonicCoefficientsDirty = false; + + this._shouldRegenerateShaders = false; + + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + + const mipmapLevels = defaultValue(options.mipmapLevels, 10); + this._mipmapLevels = mipmapLevels; + this._radianceMapComputeCommands = new Array(6); + this._convolutionComputeCommands = new Array((mipmapLevels - 1) * 6); + this._irradianceComputeCommand = undefined; + + this._radianceMapFS = undefined; + this._irradianceMapFS = undefined; + this._convolveSP = undefined; + this._va = undefined; + + this._radianceMapTextures = new Array(6); + this._specularMapTextures = new Array((mipmapLevels - 1) * 6); + this._radianceCubeMap = undefined; + this._irradianceMapTexture = undefined; + + this._sphericalHarmonicCoefficients = new Array(9); + + this._lastTime = new JulianDate(); + const width = Math.pow(2, mipmapLevels - 1); + this._textureDimensions = new Cartesian2(width, width); + + this._radiiAndDynamicAtmosphereColor = new Cartesian3(); + this._sceneEnvironmentMap = undefined; + this._backgroundColor = undefined; + + // If this DynamicEnvironmentMapManager has an owner, only its owner should update or destroy it. + // This is because in a Cesium3DTileset multiple models may reference one tileset's DynamicEnvironmentMapManager. + this._owner = undefined; + + /** + * If true, the environment map and related properties will continue to update. + * @type {boolean} + * @default true + */ + this.enabled = defaultValue(options.enabled, true); + + /** + * Disables updates. For internal use. + * @private + * @default true + */ + this.shouldUpdate = true; + + /** + * The maximum amount of elapsed seconds before a new environment map is created. + * @type {number} + * @default 3600 + */ + this.maximumSecondsDifference = defaultValue( + options.maximumSecondsDifference, + 60 * 60, + ); + + /** + * The maximum difference in position before a new environment map is created, in meters. Small differences in position will not visibly affect results. + * @type {number} + * @default 1000 + */ + this.maximumPositionEpsilon = defaultValue( + options.maximumPositionEpsilon, + 1000.0, + ); + + /** + * The intensity of the scattered light emitted from the atmosphere. This should be adjusted relative to the value of {@link Scene.light} intensity. + * @type {number} + * @default 2.0 + * @see DirectionalLight.intensity + * @see SunLight.intensity + */ + this.atmosphereScatteringIntensity = defaultValue( + options.atmosphereScatteringIntensity, + 2.0, + ); + + /** + * The gamma correction to apply to the range of light emitted from the environment. 1.0 uses the unmodified incoming light color. + * @type {number} + * @default 1.0 + */ + this.gamma = defaultValue(options.gamma, 1.0); + + /** + * The brightness of light emitted from the environment. 1.0 uses the unmodified emitted environment color. Less than 1.0 + * makes the light darker while greater than 1.0 makes it brighter. + * @type {number} + * @default 1.0 + */ + this.brightness = defaultValue(options.brightness, 1.0); + + /** + * The saturation of the light emitted from the environment. 1.0 uses the unmodified emitted environment color. Less than 1.0 reduces the + * saturation while greater than 1.0 increases it. + * @type {number} + * @default 1.0 + */ + this.saturation = defaultValue(options.saturation, 1.0); + + /** + * Solid color used to represent the ground. + * @type {Color} + * @default DynamicEnvironmentMapManager.AVERAGE_EARTH_GROUND_COLOR + */ + this.groundColor = defaultValue( + options.groundColor, + DynamicEnvironmentMapManager.AVERAGE_EARTH_GROUND_COLOR, + ); + + /** + * The percentage of light reflected from the ground. The average earth albedo is 0.31. + * @type {number} + * @default 0.31 + */ + this.groundAlbedo = defaultValue(options.groundAlbedo, 0.31); +} + +Object.defineProperties(DynamicEnvironmentMapManager.prototype, { + /** + * A reference to the DynamicEnvironmentMapManager's owner, if any. + * @memberof DynamicEnvironmentMapManager.prototype + * @type {object|undefined} + * @readonly + * @private + */ + owner: { + get: function () { + return this._owner; + }, + }, + + /** + * True if model shaders need to be regenerated to account for updates. + * @memberof DynamicEnvironmentMapManager.prototype + * @type {boolean} + * @readonly + * @private + */ + shouldRegenerateShaders: { + get: function () { + return this._shouldRegenerateShaders; + }, + }, + + /** + * The position around which the environment map is generated. + * @memberof DynamicEnvironmentMapManager.prototype + * @type {Cartesian3|undefined} + */ + position: { + get: function () { + return this._position; + }, + set: function (value) { + if ( + Cartesian3.equalsEpsilon( + value, + this._position, + 0.0, + this.maximumPositionEpsilon, + ) + ) { + return; + } + + this._position = Cartesian3.clone(value, this._position); + this.reset(); + }, + }, + + /** + * The computed radiance map, or undefined if it has not yet been created. + * @memberof DynamicEnvironmentMapManager.prototype + * @type {CubeMap|undefined} + * @readonly + * @private + */ + radianceCubeMap: { + get: function () { + return this._radianceCubeMap; + }, + }, + + /** + * The maximum number of mip levels available in the radiance cubemap. + * @memberof DynamicEnvironmentMapManager.prototype + * @type {number} + * @readonly + * @private + */ + maximumMipmapLevel: { + get: function () { + return this._mipmapLevels; + }, + }, + + /** + * The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. + *

+ * There are nine Cartesian3 coefficients. + * The order of the coefficients is: L0,0, L1,-1, L1,0, L1,1, L2,-2, L2,-1, L2,0, L2,1, L2,2 + *

+ * @memberof DynamicEnvironmentMapManager.prototype + * @readonly + * @type {Cartesian3[]} + * @see {@link https://graphics.stanford.edu/papers/envmap/envmap.pdf|An Efficient Representation for Irradiance Environment Maps} + * @private + */ + sphericalHarmonicCoefficients: { + get: function () { + return this._sphericalHarmonicCoefficients; + }, + }, +}); + +/** + * Sets the owner for the input DynamicEnvironmentMapManager if there wasn't another owner. + * Destroys the owner's previous DynamicEnvironmentMapManager if setting is successful. + * @param {DynamicEnvironmentMapManager} [environmentMapManager] A DynamicEnvironmentMapManager (or undefined) being attached to an object + * @param {object} owner An Object that should receive the new DynamicEnvironmentMapManager + * @param {string} key The Key for the Object to reference the DynamicEnvironmentMapManager + * @private + */ +DynamicEnvironmentMapManager.setOwner = function ( + environmentMapManager, + owner, + key, +) { + // Don't destroy the DynamicEnvironmentMapManager if it's already owned by newOwner + if (environmentMapManager === owner[key]) { + return; + } + // Destroy the existing DynamicEnvironmentMapManager, if any + owner[key] = owner[key] && owner[key].destroy(); + if (defined(environmentMapManager)) { + //>>includeStart('debug', pragmas.debug); + if (defined(environmentMapManager._owner)) { + throw new DeveloperError( + "DynamicEnvironmentMapManager should only be assigned to one object", + ); + } + //>>includeEnd('debug'); + environmentMapManager._owner = owner; + owner[key] = environmentMapManager; + } +}; + +/** + * Cancels any in-progress commands and marks the environment map as dirty. + * @private + */ +DynamicEnvironmentMapManager.prototype.reset = function () { + let length = this._radianceMapComputeCommands.length; + for (let i = 0; i < length; ++i) { + this._radianceMapComputeCommands[i] = undefined; + } + + length = this._convolutionComputeCommands.length; + for (let i = 0; i < length; ++i) { + this._convolutionComputeCommands[i] = undefined; + } + + if (defined(this._irradianceComputeCommand)) { + this._irradianceComputeCommand = undefined; + } + + this._radianceMapDirty = true; + this._radianceCommandsDirty = true; +}; + +const scratchPackedAtmosphere = new Cartesian3(); +const scratchSurfacePosition = new Cartesian3(); + +/** + * Update atmosphere properties and returns true if the environment map needs to be regenerated. + * @param {DynamicEnvironmentMapManager} manager this manager + * @param {FrameState} frameState the current frameState + * @returns {boolean} true if the environment map needs to be regenerated. + * @private + */ +function atmosphereNeedsUpdate(manager, frameState) { + const position = manager._position; + const atmosphere = frameState.atmosphere; + + const ellipsoid = frameState.mapProjection.ellipsoid; + const surfacePosition = ellipsoid.scaleToGeodeticSurface( + position, + scratchSurfacePosition, + ); + const outerEllipsoidScale = 1.025; + + // Pack outer radius, inner radius, and dynamic atmosphere flag + const radiiAndDynamicAtmosphereColor = scratchPackedAtmosphere; + const radius = defined(surfacePosition) + ? Cartesian3.magnitude(surfacePosition) + : ellipsoid.maximumRadius; + radiiAndDynamicAtmosphereColor.x = radius * outerEllipsoidScale; + radiiAndDynamicAtmosphereColor.y = radius; + radiiAndDynamicAtmosphereColor.z = atmosphere.dynamicLighting; + + if ( + !Cartesian3.equalsEpsilon( + manager._radiiAndDynamicAtmosphereColor, + radiiAndDynamicAtmosphereColor, + ) || + frameState.environmentMap !== manager._sceneEnvironmentMap || + frameState.backgroundColor !== manager._backgroundColor + ) { + Cartesian3.clone( + radiiAndDynamicAtmosphereColor, + manager._radiiAndDynamicAtmosphereColor, + ); + manager._sceneEnvironmentMap = frameState.environmentMap; + manager._backgroundColor = frameState.backgroundColor; + return true; + } + + return false; +} + +const scratchCartesian = new Cartesian3(); +const scratchMatrix = new Matrix4(); +const scratchAdjustments = new Cartesian4(); +const scratchColor = new Color(); + +/** + * Renders the highest resolution specular map by creating compute commands for each cube face + * @param {DynamicEnvironmentMapManager} manager this manager + * @param {FrameState} frameState the current frameState + * @private + */ +function updateRadianceMap(manager, frameState) { + const context = frameState.context; + const textureDimensions = manager._textureDimensions; + + if (!defined(manager._radianceCubeMap)) { + manager._radianceCubeMap = new CubeMap({ + context: context, + width: textureDimensions.x, + height: textureDimensions.y, + pixelDatatype: PixelDatatype.UNSIGNED_BYTE, + pixelFormat: PixelFormat.RGBA, + }); + } + + if (manager._radianceCommandsDirty) { + let fs = manager._radianceMapFS; + if (!defined(fs)) { + fs = new ShaderSource({ + sources: [AtmosphereCommon, ComputeRadianceMapFS], + }); + manager._radianceMapFS = fs; + } + + if (Atmosphere.requiresColorCorrect(frameState.atmosphere)) { + fs.defines.push("ATMOSPHERE_COLOR_CORRECT"); + } + + const position = manager._position; + const radiiAndDynamicAtmosphereColor = + manager._radiiAndDynamicAtmosphereColor; + + const ellipsoid = frameState.mapProjection.ellipsoid; + const enuToFixedFrame = Transforms.eastNorthUpToFixedFrame( + position, + ellipsoid, + scratchMatrix, + ); + + const adjustments = scratchAdjustments; + + adjustments.x = manager.brightness; + adjustments.y = manager.saturation; + adjustments.z = manager.gamma; + adjustments.w = manager.atmosphereScatteringIntensity; + + if ( + manager.brightness !== 1.0 || + manager.saturation !== 1.0 || + manager.gamma !== 1.0 + ) { + fs.defines.push("ENVIRONMENT_COLOR_CORRECT"); + } + + let i = 0; + for (const face of CubeMap.faceNames()) { + let texture = manager._radianceMapTextures[i]; + if (defined(texture)) { + texture.destroy(); + } + + texture = new Texture({ + context: context, + width: textureDimensions.x, + height: textureDimensions.y, + pixelDatatype: PixelDatatype.UNSIGNED_BYTE, + pixelFormat: PixelFormat.RGBA, + }); + manager._radianceMapTextures[i] = texture; + + const index = i; + const command = new ComputeCommand({ + fragmentShaderSource: fs, + outputTexture: texture, + uniformMap: { + u_radiiAndDynamicAtmosphereColor: () => + radiiAndDynamicAtmosphereColor, + u_enuToFixedFrame: () => enuToFixedFrame, + u_faceDirection: () => CubeMap.getDirection(face, scratchCartesian), + u_positionWC: () => position, + u_brightnessSaturationGammaIntensity: () => adjustments, + u_groundColor: () => { + return manager.groundColor.withAlpha( + manager.groundAlbedo, + scratchColor, + ); + }, + }, + persists: true, + owner: manager, + postExecute: () => { + const commands = manager._radianceMapComputeCommands; + if (!defined(commands[index])) { + // This command was cancelled + return; + } + commands[index] = undefined; + + const framebuffer = new Framebuffer({ + context: context, + colorTextures: [manager._radianceMapTextures[index]], + destroyAttachments: false, + }); + + // Copy the output texture into the corresponding cubemap face + framebuffer._bind(); + manager._radianceCubeMap[face].copyFromFramebuffer(); + framebuffer._unBind(); + framebuffer.destroy(); + + if (!commands.some(defined)) { + manager._convolutionsCommandsDirty = true; + manager._shouldRegenerateShaders = true; + } + }, + }); + frameState.commandList.push(command); + manager._radianceMapComputeCommands[i] = command; + i++; + } + manager._radianceCommandsDirty = false; + } +} + +/** + * Creates a mipmap chain for the cubemap by convolving the environment map for each roughness level + * @param {DynamicEnvironmentMapManager} manager this manager + * @param {FrameState} frameState the current frameState + * @private + */ +function updateSpecularMaps(manager, frameState) { + const radianceCubeMap = manager._radianceCubeMap; + radianceCubeMap.generateMipmap(); + + const mipmapLevels = manager._mipmapLevels; + const textureDimensions = manager._textureDimensions; + let width = textureDimensions.x / 2; + let height = textureDimensions.y / 2; + const context = frameState.context; + + let facesCopied = 0; + const getPostExecute = (index, texture, face, level) => () => { + // Copy output texture to corresponding face and mipmap level + const commands = manager._convolutionComputeCommands; + if (!defined(commands[index])) { + // This command was cancelled + return; + } + commands[index] = undefined; + + radianceCubeMap.copyFace(frameState, texture, face, level); + facesCopied++; + + // All faces and levels have been copied + if (facesCopied === manager._specularMapTextures.length) { + manager._irradianceCommandDirty = true; + radianceCubeMap.sampler = new Sampler({ + minificationFilter: TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + }); + manager._shouldRegenerateShaders = true; + } + }; + + let index = 0; + for (let level = 1; level < mipmapLevels; ++level) { + for (const face of CubeMap.faceNames()) { + const texture = (manager._specularMapTextures[index] = new Texture({ + context: context, + width: width, + height: height, + pixelDatatype: PixelDatatype.UNSIGNED_BYTE, + pixelFormat: PixelFormat.RGBA, + })); + + let vertexArray = manager._va; + if (!defined(vertexArray)) { + vertexArray = CubeMap.createVertexArray(context, face); + manager._va = vertexArray; + } + + let shaderProgram = manager._convolveSP; + if (!defined(shaderProgram)) { + shaderProgram = ShaderProgram.fromCache({ + context: context, + vertexShaderSource: ConvolveSpecularMapVS, + fragmentShaderSource: ConvolveSpecularMapFS, + attributeLocations: { + positions: 0, + }, + }); + manager._convolveSP = shaderProgram; + } + + const command = new ComputeCommand({ + shaderProgram: shaderProgram, + vertexArray: vertexArray, + outputTexture: texture, + persists: true, + owner: manager, + uniformMap: { + u_roughness: () => level / (mipmapLevels - 1), + u_radianceTexture: () => radianceCubeMap, + u_faceDirection: () => { + return CubeMap.getDirection(face, scratchCartesian); + }, + }, + postExecute: getPostExecute(index, texture, face, level), + }); + manager._convolutionComputeCommands[index] = command; + frameState.commandList.push(command); + ++index; + } + + width /= 2; + height /= 2; + } +} + +const irradianceTextureDimensions = new Cartesian2(3, 3); // 9 coefficients + +/** + * Computes spherical harmonic coefficients by convolving the environment map. + * @param {DynamicEnvironmentMapManager} manager this manager + * @param {FrameState} frameState the current frameState + * @private + */ +function updateIrradianceResources(manager, frameState) { + const context = frameState.context; + const dimensions = irradianceTextureDimensions; + + let texture = manager._irradianceMapTexture; + if (!defined(texture)) { + texture = new Texture({ + context: context, + width: dimensions.x, + height: dimensions.y, + pixelDatatype: PixelDatatype.FLOAT, + pixelFormat: PixelFormat.RGBA, + }); + manager._irradianceMapTexture = texture; + } + + let fs = manager._irradianceMapFS; + if (!defined(fs)) { + fs = new ShaderSource({ + sources: [ComputeIrradianceFS], + }); + manager._irradianceMapFS = fs; + } + + const command = new ComputeCommand({ + fragmentShaderSource: fs, + outputTexture: texture, + uniformMap: { + u_radianceMap: () => manager._radianceCubeMap, + }, + postExecute: () => { + if (!defined(manager._irradianceComputeCommand)) { + // This command was cancelled + return; + } + manager._irradianceTextureDirty = false; + manager._irradianceComputeCommand = undefined; + manager._sphericalHarmonicCoefficientsDirty = true; + }, + }); + manager._irradianceComputeCommand = command; + frameState.commandList.push(command); + manager._irradianceTextureDirty = true; +} + +/** + * Copies coefficients from the output texture using readPixels. + * @param {DynamicEnvironmentMapManager} manager this manager + * @param {FrameState} frameState the current frameState + * @private + */ +function updateSphericalHarmonicCoefficients(manager, frameState) { + const context = frameState.context; + + const framebuffer = new Framebuffer({ + context: context, + colorTextures: [manager._irradianceMapTexture], + destroyAttachments: false, + }); + + const dimensions = irradianceTextureDimensions; + const data = context.readPixels({ + x: 0, + y: 0, + width: dimensions.x, + height: dimensions.y, + framebuffer: framebuffer, + }); + + for (let i = 0; i < 9; ++i) { + manager._sphericalHarmonicCoefficients[i] = Cartesian3.unpack(data, i * 4); + Cartesian3.multiplyByScalar( + manager._sphericalHarmonicCoefficients[i], + manager.atmosphereScatteringIntensity, + manager._sphericalHarmonicCoefficients[i], + ); + } + + framebuffer.destroy(); + manager._shouldRegenerateShaders = true; +} + +/** + * Called when {@link Viewer} or {@link CesiumWidget} render the scene to + * build the resources for the environment maps. + *

+ * Do not call this function directly. + *

+ * @private + */ +DynamicEnvironmentMapManager.prototype.update = function (frameState) { + const mode = frameState.mode; + + if ( + !this.enabled || + !this.shouldUpdate || + !defined(this._position) || + mode === SceneMode.MORPHING + ) { + this._shouldRegenerateShaders = false; + return; + } + + const dynamicLighting = frameState.atmosphere.dynamicLighting; + const regenerateEnvironmentMap = + atmosphereNeedsUpdate(this, frameState) || + (dynamicLighting === DynamicAtmosphereLightingType.SUNLIGHT && + !JulianDate.equalsEpsilon( + frameState.time, + this._lastTime, + this.maximumSecondsDifference, + )); + + if (regenerateEnvironmentMap) { + this.reset(); + this._lastTime = JulianDate.clone(frameState.time, this._lastTime); + } + + if (this._radianceMapDirty) { + updateRadianceMap(this, frameState); + this._radianceMapDirty = false; + } + + if (this._convolutionsCommandsDirty) { + updateSpecularMaps(this, frameState); + this._convolutionsCommandsDirty = false; + } + + if (this._irradianceCommandDirty) { + updateIrradianceResources(this, frameState); + this._irradianceCommandDirty = false; + } + + if (this._irradianceTextureDirty) { + this._shouldRegenerateShaders = false; + return; + } + + if (this._sphericalHarmonicCoefficientsDirty) { + updateSphericalHarmonicCoefficients(this, frameState); + this._sphericalHarmonicCoefficientsDirty = false; + return; + } + + this._shouldRegenerateShaders = false; +}; + +/** + * Returns true if this object was destroyed; otherwise, false. + *

+ * If this object was destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. + * @returns {boolean} true if this object was destroyed; otherwise, false. + * @see DynamicEnvironmentMapManager#destroy + */ +DynamicEnvironmentMapManager.prototype.isDestroyed = function () { + return false; +}; + +/** + * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic + * release of WebGL resources, instead of relying on the garbage collector to destroy this object. + *

+ * Once an object is destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. Therefore, + * assign the return value (undefined) to the object as done in the example. + * @throws {DeveloperError} This object was destroyed, i.e., destroy() was called. + * @example + * mapManager = mapManager && mapManager.destroy(); + * @see DynamicEnvironmentMapManager#isDestroyed + */ +DynamicEnvironmentMapManager.prototype.destroy = function () { + // Cancel in-progress commands + let length = this._radianceMapComputeCommands.length; + for (let i = 0; i < length; ++i) { + this._radianceMapComputeCommands[i] = undefined; + } + + length = this._convolutionComputeCommands.length; + for (let i = 0; i < length; ++i) { + this._convolutionComputeCommands[i] = undefined; + } + + this._irradianceMapComputeCommand = undefined; + + // Destroy all textures + length = this._radianceMapTextures.length; + for (let i = 0; i < length; ++i) { + this._radianceMapTextures[i] = + this._radianceMapTextures[i] && this._radianceMapTextures[i].destroy(); + } + + length = this._specularMapTextures.length; + for (let i = 0; i < length; ++i) { + this._specularMapTextures[i] = + this._specularMapTextures[i] && this._specularMapTextures[i].destroy(); + } + + this._radianceCubeMap = + this._radianceCubeMap && this._radianceCubeMap.destroy(); + this._irradianceMapTexture = + this._irradianceMapTexture && this._irradianceMapTexture.destroy(); + + return destroyObject(this); +}; + +/** + * Average hue of ground color on earth, a warm green-gray. + * @type {Color} + */ +DynamicEnvironmentMapManager.AVERAGE_EARTH_GROUND_COLOR = Object.freeze( + Color.fromCssColorString("#717145"), +); + +export default DynamicEnvironmentMapManager; diff --git a/packages/engine/Source/Scene/ImageBasedLighting.js b/packages/engine/Source/Scene/ImageBasedLighting.js index f0b20cb95fe2..2dffbd654fc7 100644 --- a/packages/engine/Source/Scene/ImageBasedLighting.js +++ b/packages/engine/Source/Scene/ImageBasedLighting.js @@ -20,7 +20,6 @@ import SpecularEnvironmentCubeMap from "./SpecularEnvironmentCubeMap.js"; * @constructor * * @param {Cartesian2} [options.imageBasedLightingFactor=Cartesian2(1.0, 1.0)] Scales diffuse and specular image-based lighting from the earth, sky, atmosphere and star skybox. - * @param {number} [options.luminanceAtZenith=0.2] The sun's luminance at the zenith in kilo candela per meter squared to use for this model's procedural environment map. * @param {Cartesian3[]} [options.sphericalHarmonicCoefficients] The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. * @param {string} [options.specularEnvironmentMaps] A URL to a KTX2 file that contains a cube map of the specular lighting and the convoluted specular mipmaps. */ @@ -59,14 +58,6 @@ function ImageBasedLighting(options) { this._imageBasedLightingFactor = imageBasedLightingFactor; - const luminanceAtZenith = defaultValue(options.luminanceAtZenith, 0.2); - - //>>includeStart('debug', pragmas.debug); - Check.typeOf.number("options.luminanceAtZenith", luminanceAtZenith); - //>>includeEnd('debug'); - - this._luminanceAtZenith = luminanceAtZenith; - const sphericalHarmonicCoefficients = options.sphericalHarmonicCoefficients; //>>includeStart('debug', pragmas.debug); @@ -100,7 +91,6 @@ function ImageBasedLighting(options) { this._previousImageBasedLightingFactor = Cartesian2.clone( imageBasedLightingFactor, ); - this._previousLuminanceAtZenith = luminanceAtZenith; this._previousSphericalHarmonicCoefficients = sphericalHarmonicCoefficients; this._removeErrorListener = undefined; } @@ -156,27 +146,6 @@ Object.defineProperties(ImageBasedLighting.prototype, { }, }, - /** - * The sun's luminance at the zenith in kilo candela per meter squared - * to use for this model's procedural environment map. This is used when - * {@link ImageBasedLighting#specularEnvironmentMaps} and {@link ImageBasedLighting#sphericalHarmonicCoefficients} - * are not defined. - * - * @memberof ImageBasedLighting.prototype - * - * @type {number} - * @default 0.2 - */ - luminanceAtZenith: { - get: function () { - return this._luminanceAtZenith; - }, - set: function (value) { - this._previousLuminanceAtZenith = this._luminanceAtZenith; - this._luminanceAtZenith = value; - }, - }, - /** * The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. When undefined, a diffuse irradiance * computed from the atmosphere color is used. @@ -269,47 +238,30 @@ Object.defineProperties(ImageBasedLighting.prototype, { }, /** - * Whether or not to use the default spherical harmonic coefficients. + * The texture atlas for the specular environment maps. * * @memberof ImageBasedLighting.prototype - * @type {boolean} + * @type {SpecularEnvironmentCubeMap} * * @private */ - useDefaultSphericalHarmonics: { + specularEnvironmentCubeMap: { get: function () { - return this._useDefaultSphericalHarmonics; + return this._specularEnvironmentCubeMap; }, }, /** - * Whether or not the image-based lighting settings use spherical harmonic coefficients. + * Whether or not to use the default spherical harmonics coefficients. * * @memberof ImageBasedLighting.prototype * @type {boolean} * * @private */ - useSphericalHarmonicCoefficients: { - get: function () { - return ( - defined(this._sphericalHarmonicCoefficients) || - this._useDefaultSphericalHarmonics - ); - }, - }, - - /** - * The cube map for the specular environment maps. - * - * @memberof ImageBasedLighting.prototype - * @type {SpecularEnvironmentCubeMap} - * - * @private - */ - specularEnvironmentCubeMap: { + useDefaultSphericalHarmonics: { get: function () { - return this._specularEnvironmentCubeMap; + return this._useDefaultSphericalHarmonics; }, }, @@ -400,15 +352,6 @@ ImageBasedLighting.prototype.update = function (frameState) { ); } - if (this._luminanceAtZenith !== this._previousLuminanceAtZenith) { - this._shouldRegenerateShaders = - this._shouldRegenerateShaders || - defined(this._luminanceAtZenith) !== - defined(this._previousLuminanceAtZenith); - - this._previousLuminanceAtZenith = this._luminanceAtZenith; - } - if ( this._previousSphericalHarmonicCoefficients !== this._sphericalHarmonicCoefficients diff --git a/packages/engine/Source/Scene/Model/ImageBasedLightingPipelineStage.js b/packages/engine/Source/Scene/Model/ImageBasedLightingPipelineStage.js index a1a6deb9a285..43e4b52ddd32 100644 --- a/packages/engine/Source/Scene/Model/ImageBasedLightingPipelineStage.js +++ b/packages/engine/Source/Scene/Model/ImageBasedLightingPipelineStage.js @@ -3,11 +3,14 @@ import defined from "../../Core/defined.js"; import ImageBasedLightingStageFS from "../../Shaders/Model/ImageBasedLightingStageFS.js"; import ShaderDestination from "../../Renderer/ShaderDestination.js"; import SpecularEnvironmentCubeMap from "../SpecularEnvironmentCubeMap.js"; +import Cartesian2 from "../../Core/Cartesian2.js"; const ImageBasedLightingPipelineStage = { name: "ImageBasedLightingPipelineStage", // Helps with debugging }; +const scratchCartesian = new Cartesian2(); + /** * Add shader code, uniforms, and defines related to image based lighting * @param {ModelRenderResources} renderResources @@ -21,8 +24,18 @@ ImageBasedLightingPipelineStage.process = function ( frameState, ) { const imageBasedLighting = model.imageBasedLighting; + const environmentMapManager = model.environmentMapManager; const shaderBuilder = renderResources.shaderBuilder; + // If environment maps or spherical harmonics are not specifically provided, use procedural lighting. + let specularEnvironmentMapAtlas; + if (!defined(imageBasedLighting.specularEnvironmentMaps)) { + specularEnvironmentMapAtlas = environmentMapManager.radianceCubeMap; + } + const sphericalHarmonicCoefficients = + imageBasedLighting.sphericalHarmonicCoefficients ?? + environmentMapManager.sphericalHarmonicCoefficients; + shaderBuilder.addDefine( "USE_IBL_LIGHTING", undefined, @@ -47,7 +60,18 @@ ImageBasedLightingPipelineStage.process = function ( ); } - if (defined(imageBasedLighting.sphericalHarmonicCoefficients)) { + if (defined(specularEnvironmentMapAtlas)) { + shaderBuilder.addDefine( + "COMPUTE_POSITION_WC_ATMOSPHERE", + undefined, + ShaderDestination.BOTH, + ); + } + + if ( + defined(sphericalHarmonicCoefficients) && + defined(sphericalHarmonicCoefficients[0]) + ) { shaderBuilder.addDefine( "DIFFUSE_IBL", undefined, @@ -72,8 +96,9 @@ ImageBasedLightingPipelineStage.process = function ( } if ( - defined(imageBasedLighting.specularEnvironmentCubeMap) && - imageBasedLighting.specularEnvironmentCubeMap.ready + (defined(imageBasedLighting.specularEnvironmentCubeMap) && + imageBasedLighting.specularEnvironmentCubeMap.ready) || + defined(specularEnvironmentMapAtlas) ) { shaderBuilder.addDefine( "SPECULAR_IBL", @@ -104,33 +129,21 @@ ImageBasedLightingPipelineStage.process = function ( } } - if (defined(imageBasedLighting.luminanceAtZenith)) { - shaderBuilder.addDefine( - "USE_SUN_LUMINANCE", - undefined, - ShaderDestination.FRAGMENT, - ); - shaderBuilder.addUniform( - "float", - "model_luminanceAtZenith", - ShaderDestination.FRAGMENT, - ); - } - shaderBuilder.addFragmentLines(ImageBasedLightingStageFS); const uniformMap = { model_iblFactor: function () { - return imageBasedLighting.imageBasedLightingFactor; + return Cartesian2.multiplyByScalar( + imageBasedLighting.imageBasedLightingFactor, + environmentMapManager?.intensity || 1.0, + scratchCartesian, + ); }, model_iblReferenceFrameMatrix: function () { return model._iblReferenceFrameMatrix; }, - model_luminanceAtZenith: function () { - return imageBasedLighting.luminanceAtZenith; - }, model_sphericalHarmonicCoefficients: function () { - return imageBasedLighting.sphericalHarmonicCoefficients; + return sphericalHarmonicCoefficients; }, model_specularEnvironmentMaps: function () { return imageBasedLighting.specularEnvironmentCubeMap.texture; @@ -140,6 +153,15 @@ ImageBasedLightingPipelineStage.process = function ( }, }; + if (defined(specularEnvironmentMapAtlas)) { + uniformMap.model_specularEnvironmentMaps = function () { + return specularEnvironmentMapAtlas; + }; + uniformMap.model_specularEnvironmentMapsMaximumLOD = function () { + return environmentMapManager.maximumMipmapLevel; + }; + } + renderResources.uniformMap = combine(uniformMap, renderResources.uniformMap); }; diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index 15474e5df2ea..1e76382b423c 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -18,6 +18,7 @@ import RuntimeError from "../../Core/RuntimeError.js"; import Pass from "../../Renderer/Pass.js"; import ClippingPlaneCollection from "../ClippingPlaneCollection.js"; import ClippingPolygonCollection from "../ClippingPolygonCollection.js"; +import DynamicEnvironmentMapManager from "../DynamicEnvironmentMapManager.js"; import ColorBlendMode from "../ColorBlendMode.js"; import GltfLoader from "../GltfLoader.js"; import HeightReference, { @@ -155,6 +156,7 @@ import pickModel from "./pickModel.js"; * @privateParam {ClippingPolygonCollection} [options.clippingPolygons] The {@link ClippingPolygonCollection} used to selectively disable rendering the model. * @privateParam {Cartesian3} [options.lightColor] The light color when shading the model. When undefined the scene's light color is used instead. * @privateParam {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting on this model. + * @privateParam {DynamicEnvironmentMapManager.ConstructorOptions} [options.environmentMapOptions] The properties for managing dynamic environment maps on this model. Affects lighting. * @privateParam {boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the material's doubleSided property; when false, back face culling is disabled. Back faces are not culled if the model's color is translucent. * @privateParam {Credit|string} [options.credit] A credit for the data source, which is displayed on the canvas. * @privateParam {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen. @@ -400,6 +402,16 @@ function Model(options) { : new ImageBasedLighting(); this._shouldDestroyImageBasedLighting = !defined(options.imageBasedLighting); + this._environmentMapManager = undefined; + const environmentMapManager = new DynamicEnvironmentMapManager( + options.environmentMapOptions, + ); + DynamicEnvironmentMapManager.setOwner( + environmentMapManager, + this, + "_environmentMapManager", + ); + this._backFaceCulling = defaultValue(options.backFaceCulling, true); this._backFaceCullingDirty = false; @@ -1386,7 +1398,7 @@ Object.defineProperties(Model.prototype, { }, /** - * The light color when shading the model. When undefined the scene's light color is used instead. + * The directional light color when shading the model. When undefined the scene's light color is used instead. *

* Disabling additional light sources by setting * model.imageBasedLighting.imageBasedLightingFactor = new Cartesian2(0.0, 0.0) @@ -1424,7 +1436,7 @@ Object.defineProperties(Model.prototype, { }, set: function (value) { //>>includeStart('debug', pragmas.debug); - Check.typeOf.object("imageBasedLighting", this._imageBasedLighting); + Check.typeOf.object("imageBasedLighting", value); //>>includeEnd('debug'); if (value !== this._imageBasedLighting) { @@ -1441,6 +1453,38 @@ Object.defineProperties(Model.prototype, { }, }, + /** + * The properties for managing dynamic environment maps on this model. Affects lighting. + * @memberof Model.prototype + * @readonly + * + * @example + * // Change the ground color used for a model's environment map to a forest green + * const environmentMapManager = model.environmentMapManager; + * environmentMapManager.groundColor = Cesium.Color.fromCssColorString("#203b34"); + * + * @type {DynamicEnvironmentMapManager} + */ + environmentMapManager: { + get: function () { + return this._environmentMapManager; + }, + set: function (value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("environmentMapManager", value); + //>>includeEnd('debug'); + + if (value !== this.environmentMapManager) { + DynamicEnvironmentMapManager.setOwner( + value, + this, + "_environmentMapManager", + ); + this.resetDrawCommands(); + } + }, + }, + /** * Whether to cull back-facing geometry. When true, back face culling is * determined by the material's doubleSided property; when false, back face @@ -1858,6 +1902,9 @@ Model.prototype.update = function (frameState) { // A custom shader may have to load texture uniforms. updateCustomShader(this, frameState); + // Environment maps, specular maps, and spherical harmonics may need to be updated or regenerated + updateEnvironmentMap(this, frameState); + // The image-based lighting may have to load texture uniforms // for specular maps. updateImageBasedLighting(this, frameState); @@ -1982,6 +2029,23 @@ function updateCustomShader(model, frameState) { } } +function updateEnvironmentMap(model, frameState) { + const environmentMapManager = model._environmentMapManager; + const picking = frameState.passes.pick || frameState.passes.pickVoxel; + if (model._ready && environmentMapManager.owner === model && !picking) { + environmentMapManager.position = model._boundingSphere.center; + environmentMapManager.shouldUpdate = + !defined(model._imageBasedLighting.sphericalHarmonicCoefficients) || + !defined(model._imageBasedLighting.specularEnvironmentMaps); + + environmentMapManager.update(frameState); + + if (environmentMapManager.shouldRegenerateShaders) { + model.resetDrawCommands(); + } + } +} + function updateImageBasedLighting(model, frameState) { model._imageBasedLighting.update(frameState); if (model._imageBasedLighting.shouldRegenerateShaders) { @@ -2338,30 +2402,27 @@ function updateReferenceMatrices(model, frameState) { const referenceMatrix = defaultValue(model.referenceMatrix, modelMatrix); const context = frameState.context; - const ibl = model._imageBasedLighting; - if (ibl.useSphericalHarmonicCoefficients || ibl.useSpecularEnvironmentMaps) { - let iblReferenceFrameMatrix3 = scratchIBLReferenceFrameMatrix3; - let iblReferenceFrameMatrix4 = scratchIBLReferenceFrameMatrix4; + let iblReferenceFrameMatrix3 = scratchIBLReferenceFrameMatrix3; + let iblReferenceFrameMatrix4 = scratchIBLReferenceFrameMatrix4; - iblReferenceFrameMatrix4 = Matrix4.multiply( - context.uniformState.view3D, - referenceMatrix, - iblReferenceFrameMatrix4, - ); - iblReferenceFrameMatrix3 = Matrix4.getRotation( - iblReferenceFrameMatrix4, - iblReferenceFrameMatrix3, - ); - iblReferenceFrameMatrix3 = Matrix3.transpose( - iblReferenceFrameMatrix3, - iblReferenceFrameMatrix3, - ); - model._iblReferenceFrameMatrix = Matrix3.multiply( - yUpToZUp, - iblReferenceFrameMatrix3, - model._iblReferenceFrameMatrix, - ); - } + iblReferenceFrameMatrix4 = Matrix4.multiply( + context.uniformState.view3D, + referenceMatrix, + iblReferenceFrameMatrix4, + ); + iblReferenceFrameMatrix3 = Matrix4.getRotation( + iblReferenceFrameMatrix4, + iblReferenceFrameMatrix3, + ); + iblReferenceFrameMatrix3 = Matrix3.transpose( + iblReferenceFrameMatrix3, + iblReferenceFrameMatrix3, + ); + model._iblReferenceFrameMatrix = Matrix3.multiply( + yUpToZUp, + iblReferenceFrameMatrix3, + model._iblReferenceFrameMatrix, + ); if (model.isClippingEnabled()) { let clippingPlanesMatrix = scratchClippingPlanesMatrix; @@ -2782,6 +2843,16 @@ Model.prototype.destroy = function () { } this._imageBasedLighting = undefined; + // Only destroy the environment map manager if this is the owner. + const environmentMapManager = this._environmentMapManager; + if ( + !environmentMapManager.isDestroyed() && + environmentMapManager.owner === this + ) { + environmentMapManager.destroy(); + } + this._environmentMapManager = undefined; + destroyObject(this); }; @@ -2860,6 +2931,7 @@ Model.prototype.destroyModelResources = function () { * @param {ClippingPolygonCollection} [options.clippingPolygons] The {@link ClippingPolygonCollection} used to selectively disable rendering the model. * @param {Cartesian3} [options.lightColor] The light color when shading the model. When undefined the scene's light color is used instead. * @param {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting on this model. + * @param {DynamicEnvironmentMapManager.ConstructorOptions} [options.environmentMapOptions] The properties for managing dynamic environment maps on this model. * @param {boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the material's doubleSided property; when false, back face culling is disabled. Back faces are not culled if the model's color is translucent. * @param {Credit|string} [options.credit] A credit for the data source, which is displayed on the canvas. * @param {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen. @@ -2990,6 +3062,7 @@ Model.fromGltfAsync = async function (options) { const modelOptions = makeModelOptions(loader, type, options); modelOptions.resource = resource; + modelOptions.environmentMapOptions = options.environmentMapOptions; try { // This load the gltf JSON and ensures the gltf is valid diff --git a/packages/engine/Source/Scene/Model/Model3DTileContent.js b/packages/engine/Source/Scene/Model/Model3DTileContent.js index bdc0b3831a81..e4266704bdad 100644 --- a/packages/engine/Source/Scene/Model/Model3DTileContent.js +++ b/packages/engine/Source/Scene/Model/Model3DTileContent.js @@ -263,6 +263,11 @@ Model3DTileContent.prototype.update = function (tileset, frameState) { : undefined; } + const tilesetEnvironmentMapManager = tileset.environmentMapManager; + if (model.environmentMapManager !== tilesetClippingPlanes) { + model._environmentMapManager = tilesetEnvironmentMapManager; + } + // If the model references a different ClippingPlaneCollection from the tileset, // update the model to use the new ClippingPlaneCollection. if ( diff --git a/packages/engine/Source/Shaders/Builtin/Functions/computeAtmosphereColor.glsl b/packages/engine/Source/Shaders/Builtin/Functions/computeAtmosphereColor.glsl index d4cc21971e7a..e4697721b7fd 100644 --- a/packages/engine/Source/Shaders/Builtin/Functions/computeAtmosphereColor.glsl +++ b/packages/engine/Source/Shaders/Builtin/Functions/computeAtmosphereColor.glsl @@ -42,3 +42,47 @@ vec4 czm_computeAtmosphereColor( return vec4(color, opacity); } + +/** + * 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 {czm_ray} primaryRay Ray from the origin to sky fragment to 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( + czm_ray primaryRay, + vec3 lightDirection, + vec3 rayleighColor, + vec3 mieColor, + float opacity +) { + vec3 direction = normalize(primaryRay.direction); + + float cosAngle = dot(direction, 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/computeScattering.glsl b/packages/engine/Source/Shaders/Builtin/Functions/computeScattering.glsl index 76edcf9ed525..46dee64c07cc 100644 --- a/packages/engine/Source/Shaders/Builtin/Functions/computeScattering.glsl +++ b/packages/engine/Source/Shaders/Builtin/Functions/computeScattering.glsl @@ -40,7 +40,6 @@ void czm_computeScattering( // Return empty colors if no intersection with the atmosphere geometry. if (primaryRayAtmosphereIntersect == czm_emptyRaySegment) { - rayleighColor = vec3(1.0, 0.0, 1.0); return; } diff --git a/packages/engine/Source/Shaders/Builtin/Functions/sphericalHarmonics.glsl b/packages/engine/Source/Shaders/Builtin/Functions/sphericalHarmonics.glsl index 71a240ebf352..f5245f63fa83 100644 --- a/packages/engine/Source/Shaders/Builtin/Functions/sphericalHarmonics.glsl +++ b/packages/engine/Source/Shaders/Builtin/Functions/sphericalHarmonics.glsl @@ -29,7 +29,7 @@ vec3 czm_sphericalHarmonics(vec3 normal, vec3 coefficients[9]) float y = normal.y; float z = normal.z; - return + vec3 L = L00 + L1_1 * y + L10 * z @@ -39,4 +39,6 @@ vec3 czm_sphericalHarmonics(vec3 normal, vec3 coefficients[9]) + L20 * (3.0 * z * z - 1.0) + L21 * (z * x) + L22 * (x * x - y * y); + + return max(L, vec3(0.0)); } diff --git a/packages/engine/Source/Shaders/ComputeIrradianceFS.glsl b/packages/engine/Source/Shaders/ComputeIrradianceFS.glsl new file mode 100644 index 000000000000..4dd70eae20c9 --- /dev/null +++ b/packages/engine/Source/Shaders/ComputeIrradianceFS.glsl @@ -0,0 +1,103 @@ +uniform samplerCube u_radianceMap; + +in vec2 v_textureCoordinates; + + +const float twoSqrtPi = 2.0 * sqrt(czm_pi); + +// Coutesy of https://www.ppsloan.org/publications/StupidSH36.pdf +float computeShBasis(int index, vec3 s) { + if (index == 0) { // l = 0, m = 0 + return 1.0 / twoSqrtPi; + } + + if (index == 1) { // l = 1, m = -1 + return -sqrt(3.0) * s.y / twoSqrtPi; + } + + if (index == 2) { // l = 1, m = 0 + return sqrt(3.0) * s.z / twoSqrtPi; + } + + if (index == 3) { // l = 1, m = 1 + return -sqrt(3.0) * s.x / twoSqrtPi; + } + + if (index == 4) { // l = 2, m = -2 + return sqrt(15.0) * s.y * s.x / twoSqrtPi; + } + + if (index == 5) { // l = 2, m = -1 + return -sqrt(15.0) * s.y * s.z / twoSqrtPi; + } + + if (index == 6) { // l = 2, m = 0 + return sqrt(5.0) * (3.0 * s.z * s.z - 1.0) / 2.0 / twoSqrtPi; + } + + if (index == 7) { // l = 2, m = 1 + return -sqrt(15.0) * s.x * s.z / twoSqrtPi; + } + + if (index == 8) { // l = 2, m = 2 + return sqrt(15.0) * (s.x * s.x - s.y * s.y) / 2.0 / twoSqrtPi; + } + + return 0.0; +} + +float vdcRadicalInverse(int i) +{ + float r; + float base = 2.0; + float value = 0.0; + float invBase = 1.0 / base; + float invBi = invBase; + for (int x = 0; x < 100; x++) + { + if (i <= 0) + { + break; + } + r = mod(float(i), base); + value += r * invBi; + invBi *= invBase; + i = int(float(i) * invBase); + } + return value; +} + +vec2 hammersley2D(int i, int N) +{ + return vec2(float(i) / float(N), vdcRadicalInverse(i)); +} + +// Sample count is relatively low for the sake of performance, but should still be enough to capture directionality needed for third-order harmonics +const int samples = 256; +const float solidAngle = 1.0 / float(samples); + +void main() { + // Get the current coefficient based on the uv + vec2 uv = v_textureCoordinates.xy * 3.0; + int coefficientIndex = int(floor(uv.y) * 3.0 + floor(uv.x)); + + for (int i = 0; i < samples; ++i) { + vec2 xi = hammersley2D(i, samples); + float phi = czm_twoPi * xi.x; + float cosTheta = 1.0 - 2.0 * sqrt(1.0 - xi.y * xi.y); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + vec3 direction = normalize(vec3(sinTheta * cos(phi), cosTheta, sinTheta * sin(phi))); + + // Generate the spherical harmonics basis from the direction + float Ylm = computeShBasis(coefficientIndex, direction); + + vec3 lookupDirection = -direction.xyz; + lookupDirection.z = -lookupDirection.z; + + vec4 color = czm_textureCube(u_radianceMap, lookupDirection, 0.0); + + // Use the relevant function for this coefficient + out_FragColor += Ylm * color * solidAngle * sinTheta; + } + +} diff --git a/packages/engine/Source/Shaders/ComputeRadianceMapFS.glsl b/packages/engine/Source/Shaders/ComputeRadianceMapFS.glsl new file mode 100644 index 000000000000..5ca70b698b13 --- /dev/null +++ b/packages/engine/Source/Shaders/ComputeRadianceMapFS.glsl @@ -0,0 +1,107 @@ +precision highp float; + +in vec2 v_textureCoordinates; + +uniform vec3 u_faceDirection; // Current cubemap face +uniform vec3 u_positionWC; +uniform mat4 u_enuToFixedFrame; +uniform vec4 u_brightnessSaturationGammaIntensity; +uniform vec4 u_groundColor; // alpha component represent albedo + +vec4 getCubeMapDirection(vec2 uv, vec3 faceDir) { + vec2 scaledUV = uv * 2.0 - 1.0; + + if (faceDir.x != 0.0) { + return vec4(faceDir.x, scaledUV.x * faceDir.x, -scaledUV.y, 0.0); + } else if (faceDir.y != 0.0) { + return vec4(scaledUV.x, -scaledUV.y * faceDir.y, faceDir.y, 0.0); + } else { + return vec4(scaledUV.x * faceDir.z, -faceDir.z, -scaledUV.y, 0.0); + } +} + +void main() { + float height = length(u_positionWC); + float atmosphereInnerRadius = u_radiiAndDynamicAtmosphereColor.y; + float ellipsoidHeight = max(height - atmosphereInnerRadius, 0.0); + + // Scale the position to ensure the sky color is present, even when underground. + vec3 positionWC = u_positionWC / height * (ellipsoidHeight + atmosphereInnerRadius); + + float atmosphereOuterRadius = u_radiiAndDynamicAtmosphereColor.x; + float atmosphereHeight = atmosphereOuterRadius - atmosphereInnerRadius; + + vec3 direction = (u_enuToFixedFrame * getCubeMapDirection(v_textureCoordinates, u_faceDirection)).xyz; + vec3 normalizedDirection = normalize(direction); + + czm_ray ray = czm_ray(positionWC, normalizedDirection); + czm_raySegment intersection = czm_raySphereIntersectionInterval(ray, vec3(0.0), atmosphereInnerRadius); + if (!czm_isEmpty(intersection)) { + intersection = czm_rayEllipsoidIntersectionInterval(ray, vec3(0.0), czm_ellipsoidInverseRadii); + } + + bool onEllipsoid = intersection.start >= 0.0; + float rayLength = czm_branchFreeTernary(onEllipsoid, intersection.start, atmosphereOuterRadius); + + // Compute sky color for each position on a sphere at radius centered around the provided position's origin + vec3 skyPositionWC = positionWC + normalizedDirection * rayLength; + + float lightEnum = u_radiiAndDynamicAtmosphereColor.z; + vec3 lightDirectionWC = normalize(czm_getDynamicAtmosphereLightDirection(skyPositionWC, lightEnum)); + vec3 mieColor; + vec3 rayleighColor; + float opacity; + czm_computeScattering( + ray, + rayLength, + lightDirectionWC, + atmosphereInnerRadius, + rayleighColor, + mieColor, + opacity + ); + + vec4 atmopshereColor = czm_computeAtmosphereColor(ray, lightDirectionWC, rayleighColor, mieColor, opacity); + +#ifdef ATMOSPHERE_COLOR_CORRECT + const bool ignoreBlackPixels = true; + atmopshereColor.rgb = czm_applyHSBShift(atmopshereColor.rgb, czm_atmosphereHsbShift, ignoreBlackPixels); +#endif + + vec3 lookupDirection = -normalizedDirection; + // Flipping the X vector is a cheap way to get the inverse of czm_temeToPseudoFixed, since that's a rotation about Z. + lookupDirection.x = -lookupDirection.x; + lookupDirection = -normalize(czm_temeToPseudoFixed * lookupDirection); + lookupDirection.x = -lookupDirection.x; + + // Values outside the atmopshere are rendered as black, when they should be treated as transparent + float skyAlpha = clamp((1.0 - ellipsoidHeight / atmosphereHeight) * atmopshereColor.a, 0.0, 1.0); + skyAlpha = czm_branchFreeTernary(length(atmopshereColor.rgb) <= czm_epsilon7, 0.0, skyAlpha); // Treat black as transparent + + // Blend starmap with atmopshere scattering + float intensity = u_brightnessSaturationGammaIntensity.w; + vec4 sceneSkyBoxColor = czm_textureCube(czm_environmentMap, lookupDirection); + vec3 skyBackgroundColor = mix(czm_backgroundColor.rgb, sceneSkyBoxColor.rgb, sceneSkyBoxColor.a); + vec4 combinedSkyColor = vec4(mix(skyBackgroundColor, atmopshereColor.rgb * intensity, skyAlpha), 1.0); + + // Compute ground color based on amount of reflected light, then blend it with ground atmosphere based on height + vec3 up = normalize(positionWC); + float occlusion = max(dot(lightDirectionWC, up), 0.05); + vec4 groundColor = vec4(u_groundColor.rgb * u_groundColor.a * (vec3(intensity * occlusion) + atmopshereColor.rgb), 1.0); + vec4 blendedGroundColor = mix(groundColor, atmopshereColor, clamp(ellipsoidHeight / atmosphereHeight, 0.0, 1.0)); + + vec4 color = czm_branchFreeTernary(onEllipsoid, blendedGroundColor, combinedSkyColor); + + float brightness = u_brightnessSaturationGammaIntensity.x; + float saturation = u_brightnessSaturationGammaIntensity.y; + float gamma = u_brightnessSaturationGammaIntensity.z; + +#ifdef ENVIRONMENT_COLOR_CORRECT + color.rgb = mix(vec3(0.0), color.rgb, brightness); + color.rgb = czm_saturation(color.rgb, saturation); +#endif + color.rgb = pow(color.rgb, vec3(gamma)); // Normally this would be in the ifdef above, but there is a precision issue with the atmopshere scattering transmittance (alpha). Having this line is a workaround for that issue, even when gamma is 1.0. + color.rgb = czm_gammaCorrect(color.rgb); + + out_FragColor = color; +} diff --git a/packages/engine/Source/Shaders/ConvolveSpecularMapFS.glsl b/packages/engine/Source/Shaders/ConvolveSpecularMapFS.glsl new file mode 100644 index 000000000000..5eb290ecf3e1 --- /dev/null +++ b/packages/engine/Source/Shaders/ConvolveSpecularMapFS.glsl @@ -0,0 +1,70 @@ +precision highp float; + +in vec3 v_textureCoordinates; + +uniform float u_roughness; +uniform samplerCube u_radianceTexture; +uniform vec3 u_faceDirection; + +float vdcRadicalInverse(int i) +{ + float r; + float base = 2.0; + float value = 0.0; + float invBase = 1.0 / base; + float invBi = invBase; + for (int x = 0; x < 100; x++) + { + if (i <= 0) + { + break; + } + r = mod(float(i), base); + value += r * invBi; + invBi *= invBase; + i = int(float(i) * invBase); + } + return value; +} + +vec2 hammersley2D(int i, int N) +{ + return vec2(float(i) / float(N), vdcRadicalInverse(i)); +} + +vec3 importanceSampleGGX(vec2 xi, float alphaRoughness, vec3 N) +{ + float alphaRoughnessSquared = alphaRoughness * alphaRoughness; + float phi = czm_twoPi * xi.x; + float cosTheta = sqrt((1.0 - xi.y) / (1.0 + (alphaRoughnessSquared - 1.0) * xi.y)); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); + vec3 upVector = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangentX = normalize(cross(upVector, N)); + vec3 tangentY = cross(N, tangentX); + return tangentX * H.x + tangentY * H.y + N * H.z; +} + +// Sample count is relatively low for the sake of performance, but should still be enough to prevent artifacting in lower roughnesses +const int samples = 128; + +void main() { + vec3 normal = u_faceDirection; + vec3 V = normalize(v_textureCoordinates); + float roughness = u_roughness; + + vec4 color = vec4(0.0); + float weight = 0.0; + for (int i = 0; i < samples; ++i) { + vec2 xi = hammersley2D(i, samples); + vec3 H = importanceSampleGGX(xi, roughness, V); + vec3 L = 2.0 * dot(V, H) * H - V; // reflected vector + + float NdotL = max(dot(V, L), 0.0); + if (NdotL > 0.0) { + color += vec4(texture(u_radianceTexture, L).rgb, 1.0) * NdotL; + weight += NdotL; + } + } + out_FragColor = color / weight; +} diff --git a/packages/engine/Source/Shaders/ConvolveSpecularMapVS.glsl b/packages/engine/Source/Shaders/ConvolveSpecularMapVS.glsl new file mode 100644 index 000000000000..ef910092c53c --- /dev/null +++ b/packages/engine/Source/Shaders/ConvolveSpecularMapVS.glsl @@ -0,0 +1,24 @@ +in vec3 position; +out vec3 v_textureCoordinates; + +uniform vec3 u_faceDirection; + +vec3 getCubeMapDirection(vec2 uv, vec3 faceDir) { + vec2 scaledUV = uv; + + if (faceDir.x != 0.0) { + return vec3(faceDir.x, scaledUV.y, scaledUV.x * faceDir.x); + } else if (faceDir.y != 0.0) { + return vec3(scaledUV.x, -faceDir.y, -scaledUV.y * faceDir.y); + } else { + return vec3(scaledUV.x * faceDir.z, scaledUV.y, -faceDir.z); + } +} + +void main() +{ + v_textureCoordinates = getCubeMapDirection(position.xy, u_faceDirection); + v_textureCoordinates.y = -v_textureCoordinates.y; + v_textureCoordinates.z = -v_textureCoordinates.z; + gl_Position = vec4(position, 1.0); +} diff --git a/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl b/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl index 714d34e142fd..95acf6550f21 100644 --- a/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl +++ b/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl @@ -1,156 +1,3 @@ -/** - * Compute some metrics for a procedural sky lighting model - * - * @param {vec3} positionWC The position of the fragment in world coordinates. - * @param {vec3} reflectionWC A unit vector in the direction of the reflection, in world coordinates. - * @return {vec3} The dot products of the horizon and reflection directions with the nadir, and an atmosphere boundary distance. - */ -vec3 getProceduralSkyMetrics(vec3 positionWC, vec3 reflectionWC) -{ - // Figure out if the reflection vector hits the ellipsoid - float horizonDotNadir = 1.0 - min(1.0, czm_ellipsoidRadii.x / length(positionWC)); - float reflectionDotNadir = dot(reflectionWC, normalize(positionWC)); - float atmosphereHeight = 0.05; - float smoothstepHeight = smoothstep(0.0, atmosphereHeight, horizonDotNadir); - return vec3(horizonDotNadir, reflectionDotNadir, smoothstepHeight); -} - -/** - * Compute the diffuse irradiance for a procedural sky lighting model. - * - * @param {vec3} skyMetrics The dot products of the horizon and reflection directions with the nadir, and an atmosphere boundary distance. - * @return {vec3} The computed diffuse irradiance. - */ -vec3 getProceduralDiffuseIrradiance(vec3 skyMetrics) -{ - vec3 blueSkyDiffuseColor = vec3(0.7, 0.85, 0.9); - float diffuseIrradianceFromEarth = (1.0 - skyMetrics.x) * (skyMetrics.y * 0.25 + 0.75) * skyMetrics.z; - float diffuseIrradianceFromSky = (1.0 - skyMetrics.z) * (1.0 - (skyMetrics.y * 0.25 + 0.25)); - return blueSkyDiffuseColor * clamp(diffuseIrradianceFromEarth + diffuseIrradianceFromSky, 0.0, 1.0); -} - -/** - * Compute the specular irradiance for a procedural sky lighting model. - * - * @param {vec3} reflectionWC The reflection vector in world coordinates. - * @param {vec3} skyMetrics The dot products of the horizon and reflection directions with the nadir, and an atmosphere boundary distance. - * @param {float} roughness The roughness of the material. - * @return {vec3} The computed specular irradiance. - */ -vec3 getProceduralSpecularIrradiance(vec3 reflectionWC, vec3 skyMetrics, float roughness) -{ - // Flipping the X vector is a cheap way to get the inverse of czm_temeToPseudoFixed, since that's a rotation about Z. - reflectionWC.x = -reflectionWC.x; - reflectionWC = -normalize(czm_temeToPseudoFixed * reflectionWC); - reflectionWC.x = -reflectionWC.x; - - float inverseRoughness = 1.0 - roughness; - inverseRoughness *= inverseRoughness; - vec3 sceneSkyBox = czm_textureCube(czm_environmentMap, reflectionWC).rgb * inverseRoughness; - - // Compute colors at different angles relative to the horizon - vec3 belowHorizonColor = mix(vec3(0.1, 0.15, 0.25), vec3(0.4, 0.7, 0.9), skyMetrics.z); - vec3 nadirColor = belowHorizonColor * 0.5; - vec3 aboveHorizonColor = mix(vec3(0.9, 1.0, 1.2), belowHorizonColor, roughness * 0.5); - vec3 blueSkyColor = mix(vec3(0.18, 0.26, 0.48), aboveHorizonColor, skyMetrics.y * inverseRoughness * 0.5 + 0.75); - vec3 zenithColor = mix(blueSkyColor, sceneSkyBox, skyMetrics.z); - - // Compute blend zones - float blendRegionSize = 0.1 * ((1.0 - inverseRoughness) * 8.0 + 1.1 - skyMetrics.x); - float blendRegionOffset = roughness * -1.0; - float farAboveHorizon = clamp(skyMetrics.x - blendRegionSize * 0.5 + blendRegionOffset, 1.0e-10 - blendRegionSize, 0.99999); - float aroundHorizon = clamp(skyMetrics.x + blendRegionSize * 0.5, 1.0e-10 - blendRegionSize, 0.99999); - float farBelowHorizon = clamp(skyMetrics.x + blendRegionSize * 1.5, 1.0e-10 - blendRegionSize, 0.99999); - - // Blend colors - float notDistantRough = (1.0 - skyMetrics.x * roughness * 0.8); - vec3 specularIrradiance = mix(zenithColor, aboveHorizonColor, smoothstep(farAboveHorizon, aroundHorizon, skyMetrics.y) * notDistantRough); - specularIrradiance = mix(specularIrradiance, belowHorizonColor, smoothstep(aroundHorizon, farBelowHorizon, skyMetrics.y) * inverseRoughness); - specularIrradiance = mix(specularIrradiance, nadirColor, smoothstep(farBelowHorizon, 1.0, skyMetrics.y) * inverseRoughness); - - return specularIrradiance; -} - -#ifdef USE_SUN_LUMINANCE -float clampedDot(vec3 x, vec3 y) -{ - return clamp(dot(x, y), 0.001, 1.0); -} -/** - * Sun luminance following the "CIE Clear Sky Model" - * See page 40 of https://3dvar.com/Green2003Spherical.pdf - * - * @param {vec3} positionWC The position of the fragment in world coordinates. - * @param {vec3} normalEC The surface normal in eye coordinates. - * @param {vec3} lightDirectionEC Unit vector pointing to the light source in eye coordinates. - * @return {float} The computed sun luminance. - */ -float getSunLuminance(vec3 positionWC, vec3 normalEC, vec3 lightDirectionEC) -{ - vec3 normalWC = normalize(czm_inverseViewRotation * normalEC); - vec3 lightDirectionWC = normalize(czm_inverseViewRotation * lightDirectionEC); - vec3 vWC = -normalize(positionWC); - - // Angle between sun and zenith. - float LdotZenith = clampedDot(lightDirectionWC, vWC); - float S = acos(LdotZenith); - // Angle between zenith and current pixel - float NdotZenith = clampedDot(normalWC, vWC); - // Angle between sun and current pixel - float NdotL = clampedDot(normalEC, lightDirectionEC); - float gamma = acos(NdotL); - - float numerator = ((0.91 + 10.0 * exp(-3.0 * gamma) + 0.45 * NdotL * NdotL) * (1.0 - exp(-0.32 / NdotZenith))); - float denominator = (0.91 + 10.0 * exp(-3.0 * S) + 0.45 * LdotZenith * LdotZenith) * (1.0 - exp(-0.32)); - return model_luminanceAtZenith * (numerator / denominator); -} -#endif - -/** - * Compute the light contribution from a procedural sky model - * - * @param {vec3} positionEC The position of the fragment in eye coordinates. - * @param {vec3} normalEC The surface normal in eye coordinates. - * @param {vec3} lightDirectionEC Unit vector pointing to the light source in eye coordinates. - * @param {czm_modelMaterial} The material properties. - * @return {vec3} The computed HDR color - */ - vec3 proceduralIBL( - vec3 positionEC, - vec3 normalEC, - vec3 lightDirectionEC, - czm_modelMaterial material -) { - vec3 viewDirectionEC = -normalize(positionEC); - vec3 positionWC = vec3(czm_inverseView * vec4(positionEC, 1.0)); - vec3 reflectionWC = normalize(czm_inverseViewRotation * reflect(viewDirectionEC, normalEC)); - vec3 skyMetrics = getProceduralSkyMetrics(positionWC, reflectionWC); - - float roughness = material.roughness; - vec3 f0 = material.specular; - - vec3 specularIrradiance = getProceduralSpecularIrradiance(reflectionWC, skyMetrics, roughness); - float NdotV = abs(dot(normalEC, viewDirectionEC)) + 0.001; - vec2 brdfLut = texture(czm_brdfLut, vec2(NdotV, roughness)).rg; - vec3 specularColor = czm_srgbToLinear(f0 * brdfLut.x + brdfLut.y); - vec3 specularContribution = specularIrradiance * specularColor * model_iblFactor.y; - #ifdef USE_SPECULAR - specularContribution *= material.specularWeight; - #endif - - vec3 diffuseIrradiance = getProceduralDiffuseIrradiance(skyMetrics); - vec3 diffuseColor = material.diffuse; - vec3 diffuseContribution = diffuseIrradiance * diffuseColor * model_iblFactor.x; - - vec3 iblColor = specularContribution + diffuseContribution; - - #ifdef USE_SUN_LUMINANCE - iblColor *= getSunLuminance(positionWC, normalEC, lightDirectionEC); - #endif - - return iblColor; -} - #ifdef DIFFUSE_IBL vec3 sampleDiffuseEnvironment(vec3 cubeDir) { @@ -223,7 +70,7 @@ vec3 textureIBL(vec3 viewDirectionEC, vec3 normalEC, czm_modelMaterial material) float Ems = specularWeight * (1.0 - brdfLut.x - brdfLut.y); vec3 FmsEms = FssEss * averageFresnel * Ems / (1.0 - averageFresnel * Ems); vec3 dielectricScattering = (1.0 - FssEss - FmsEms) * material.diffuse; - vec3 diffuseContribution = irradiance * (FmsEms + dielectricScattering); + vec3 diffuseContribution = irradiance * (FmsEms + dielectricScattering) * model_iblFactor.x; #else vec3 diffuseContribution = vec3(0.0); #endif @@ -244,7 +91,7 @@ vec3 textureIBL(vec3 viewDirectionEC, vec3 normalEC, czm_modelMaterial material) #ifdef SPECULAR_IBL vec3 reflectMC = normalize(model_iblReferenceFrameMatrix * reflectEC); vec3 radiance = sampleSpecularEnvironment(reflectMC, roughness); - vec3 specularContribution = radiance * FssEss; + vec3 specularContribution = radiance * FssEss * model_iblFactor.y; #else vec3 specularContribution = vec3(0.0); #endif diff --git a/packages/engine/Source/Shaders/Model/LightingStageFS.glsl b/packages/engine/Source/Shaders/Model/LightingStageFS.glsl index e51c17ede10f..06087ea0f2ce 100644 --- a/packages/engine/Source/Shaders/Model/LightingStageFS.glsl +++ b/packages/engine/Source/Shaders/Model/LightingStageFS.glsl @@ -5,14 +5,10 @@ vec3 computeIBL(vec3 position, vec3 normal, vec3 lightDirection, vec3 lightColor // Environment maps were provided, use them for IBL vec3 viewDirection = -normalize(position); vec3 iblColor = textureIBL(viewDirection, normal, material); - #else - // Use procedural IBL if there are no environment maps - vec3 imageBasedLighting = proceduralIBL(position, normal, lightDirection, material); - float maximumComponent = czm_maximumComponent(lightColorHdr); - vec3 clampedLightColor = lightColorHdr / max(maximumComponent, 1.0); - vec3 iblColor = clampedLightColor * imageBasedLighting; + return iblColor; #endif - return iblColor * material.occlusion; + + return vec3(0.0); } #endif @@ -44,21 +40,6 @@ vec3 addClearcoatReflection(vec3 baseLayerColor, vec3 position, vec3 lightDirect vec3 reflectMC = normalize(model_iblReferenceFrameMatrix * reflect(-viewDirection, normal)); vec3 iblColor = computeSpecularIBL(reflectMC, NdotV, f0, roughness); color += iblColor * material.occlusion; - #elif defined(USE_IBL_LIGHTING) - vec3 positionWC = vec3(czm_inverseView * vec4(position, 1.0)); - vec3 reflectionWC = normalize(czm_inverseViewRotation * reflect(viewDirection, normal)); - vec3 skyMetrics = getProceduralSkyMetrics(positionWC, reflectionWC); - - vec3 specularIrradiance = getProceduralSpecularIrradiance(reflectionWC, skyMetrics, roughness); - vec2 brdfLut = texture(czm_brdfLut, vec2(NdotV, roughness)).rg; - vec3 specularColor = czm_srgbToLinear(f0 * brdfLut.x + brdfLut.y); - vec3 iblColor = specularIrradiance * specularColor * model_iblFactor.y; - #ifdef USE_SUN_LUMINANCE - iblColor *= getSunLuminance(positionWC, normal, lightDirection); - #endif - float maximumComponent = czm_maximumComponent(lightColorHdr); - vec3 clampedLightColor = lightColorHdr / max(maximumComponent, 1.0); - color += clampedLightColor * iblColor * material.occlusion; #endif float clearcoatFactor = material.clearcoatFactor; diff --git a/packages/engine/Source/Shaders/SkyBoxVS.glsl b/packages/engine/Source/Shaders/SkyBoxVS.glsl index b883f5112ab3..89d5351032b6 100644 --- a/packages/engine/Source/Shaders/SkyBoxVS.glsl +++ b/packages/engine/Source/Shaders/SkyBoxVS.glsl @@ -1,5 +1,4 @@ in vec3 position; - out vec3 v_texCoord; void main() diff --git a/packages/engine/Specs/Renderer/CubeMapSpec.js b/packages/engine/Specs/Renderer/CubeMapSpec.js index 86a97c7c28a8..356c26e4f649 100644 --- a/packages/engine/Specs/Renderer/CubeMapSpec.js +++ b/packages/engine/Specs/Renderer/CubeMapSpec.js @@ -1,6 +1,8 @@ import { + BufferUsage, Cartesian3, Color, + ComponentDatatype, defined, PixelFormat, Resource, @@ -246,6 +248,16 @@ describe( expect(cubeMap.flipY).toEqual(true); }); + it("faceNames returns an iterator over each of the faces by name", () => { + let count = 0; + for (const faceName of CubeMap.faceNames()) { + expect(Object.values(CubeMap.FaceName).includes(faceName)).toBeTrue(); + count++; + } + + expect(count).toBe(6); + }); + it("draws with a cube map", function () { cubeMap = new CubeMap({ context: webgl2Context, @@ -1311,6 +1323,24 @@ describe( ); }); + it("createVertexArray produces expected ", function () { + const va = CubeMap.createVertexArray(webgl2Context); + + expect(va.numberOfAttributes).toBe(1); + expect(va.indexBuffer).toBeDefined(); + + expect(va.getAttribute(0).index).toEqual(0); + expect(va.getAttribute(0).componentDatatype).toEqual( + ComponentDatatype.FLOAT, + ); + expect(va.getAttribute(0).componentsPerAttribute).toEqual(3); + expect(va.getAttribute(0).offsetInBytes).toEqual(0); + + expect(va.getAttribute(0).vertexBuffer.usage).toEqual( + BufferUsage.STATIC_DRAW, + ); + }); + it("destroys", function () { const c = new CubeMap({ context: webgl2Context, diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 1377623b5161..d8d9f816ce33 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -31,6 +31,7 @@ import { getJsonFromTypedArray, HeadingPitchRange, HeadingPitchRoll, + ImageBasedLighting, Intersect, JulianDate, Math as CesiumMath, @@ -210,6 +211,9 @@ describe( options = { cullRequestsWhileMoving: false, + environmentMapOptions: { + enabled: scene.highDynamicRangeSupported, + }, }; }); @@ -711,7 +715,7 @@ describe( scene.camera.lookAt(center, viewNorth); expect(renderOptions).toRenderAndCall(function (rgba) { expect(rgba[0]).toBeLessThanOrEqual(108); - expect(rgba[1]).toBeGreaterThan(190); + expect(rgba[1]).toBeGreaterThan(180); expect(rgba[2]).toBeLessThanOrEqual(108); expect(rgba[3]).toEqual(255); }); @@ -721,7 +725,7 @@ describe( expect(renderOptions).toRenderAndCall(function (rgba) { expect(rgba[0]).toBeLessThanOrEqual(108); expect(rgba[1]).toBeLessThanOrEqual(108); - expect(rgba[2]).toBeGreaterThan(190); + expect(rgba[2]).toBeGreaterThan(180); expect(rgba[3]).toEqual(255); }); }); @@ -2699,25 +2703,21 @@ describe( ); }); - it("renders with lightColor", function () { + it("renders with lightColor", async function () { const renderOptions = { scene: scene, time: new JulianDate(2457522.154792), }; - return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then( - function (tileset) { - const ibl = tileset.imageBasedLighting; - expect(renderOptions).toRenderAndCall(function (rgba) { - expect(rgba).not.toEqual([0, 0, 0, 255]); - ibl.imageBasedLightingFactor = new Cartesian2(0.0, 0.0); - expect(renderOptions).toRenderAndCall(function (rgba2) { - expect(rgba2).not.toEqual(rgba); - tileset.lightColor = new Cartesian3(5.0, 5.0, 5.0); - expect(renderOptions).notToRender(rgba2); - }); - }); - }, + const tileset = await Cesium3DTilesTester.loadTileset( + scene, + withoutBatchTableUrl, ); + const ibl = tileset.imageBasedLighting; + ibl.imageBasedLightingFactor = new Cartesian2(0.0, 0.0); + expect(renderOptions).toRenderAndCall(function (rgba) { + tileset.lightColor = new Cartesian3(5.0, 5.0, 5.0); + expect(renderOptions).notToRender(rgba); + }); }); function testBackFaceCulling(url, setViewOptions) { @@ -3327,8 +3327,21 @@ describe( scene: scene, time: new JulianDate(2457522.154792), }; - const tileset = await Cesium3DTilesTester.loadTileset(scene, url); - tileset.luminanceAtZenith = undefined; + const tileset = await Cesium3DTilesTester.loadTileset(scene, url, { + imageBasedLighting: new ImageBasedLighting({ + sphericalHarmonicCoefficients: [ + new Cartesian3(2.0, 2.0, 2.0), + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + ], + }), + }); expect(renderOptions).toRenderAndCall(function (rgba) { sourceRed = rgba[0]; @@ -3336,9 +3349,9 @@ describe( }); expect(renderOptions).toRenderAndCall(function (rgba) { - expect(rgba[0]).withContext("starting red .r").toBeGreaterThan(200); - expect(rgba[1]).withContext("starting red .g").toEqualEpsilon(116, 1); - expect(rgba[2]).withContext("starting red .b").toEqualEpsilon(116, 1); + expect(rgba[0]).withContext("starting red .r").toBeGreaterThan(190); + expect(rgba[1]).withContext("starting red .g").toEqualEpsilon(118, 1); + expect(rgba[2]).withContext("starting red .b").toEqualEpsilon(118, 1); expect(rgba[3]).withContext("starting red .a").toEqual(255); }); @@ -3370,7 +3383,7 @@ describe( expect(rgba[0]) .withContext("hl yellow+alpha .r") .toBeLessThan(sourceRed); - expect(rgba[1]).withContext("hl yellow+alpha .g").toEqualEpsilon(43, 1); + expect(rgba[1]).withContext("hl yellow+alpha .g").toEqualEpsilon(80, 1); expect(rgba[2]).withContext("hl yellow+alpha .b").toEqualEpsilon(0, 1); expect(rgba[3]).withContext("hl yellow+alpha .a").toEqual(255); }); @@ -3390,7 +3403,7 @@ describe( expect(rgba[0]).withContext("replace yellow .r").toBeLessThan(255); expect(rgba[1]).withContext("replace yellow .g").toBeGreaterThan(100); expect(rgba[1]).withContext("replace yellow .g").toBeLessThan(255); - expect(rgba[2]).withContext("replace yellow .b").toEqualEpsilon(73, 1); + expect(rgba[2]).withContext("replace yellow .b").toEqualEpsilon(62, 1); expect(rgba[3]).withContext("replace yellow .a").toEqual(255); }); @@ -3414,7 +3427,7 @@ describe( .toBeLessThan(255); expect(rgba[2]) .withContext("replace yellow+alpha .b") - .toEqualEpsilon(48, 1); + .toEqualEpsilon(80, 1); expect(rgba[3]).withContext("replace yellow+alpha .a").toEqual(255); }); @@ -3440,7 +3453,7 @@ describe( .withContext("mix yellow .g") .toBeGreaterThan(sourceGreen); expect(rgba[1]).withContext("mix yellow .g").toBeLessThan(replaceGreen); - expect(rgba[2]).withContext("mix yellow .b").toEqualEpsilon(94, 1); + expect(rgba[2]).withContext("mix yellow .b").toEqualEpsilon(96, 1); expect(rgba[3]).withContext("mix yellow .a").toEqual(255); }); @@ -3455,7 +3468,7 @@ describe( .toBeLessThanOrEqual(sourceRed); expect(rgba[1]).withContext("mix blend 0.25 .g").toBeGreaterThan(0); expect(rgba[1]).withContext("mix blend 0.25 .g").toBeLessThan(mixGreen); - expect(rgba[2]).withContext("mix blend 0.25 .b").toEqualEpsilon(106, 1); + expect(rgba[2]).withContext("mix blend 0.25 .b").toEqualEpsilon(108, 1); expect(rgba[3]).withContext("mix blend 0.25 .a").toEqual(255); }); @@ -3463,8 +3476,8 @@ describe( tileset.colorBlendAmount = 0.0; expect(renderOptions).toRenderAndCall(function (rgba) { expect(rgba[0]).withContext("mix blend 0.0 .r").toEqual(sourceRed); - expect(rgba[1]).withContext("mix blend 0.0 .g").toEqualEpsilon(116, 1); - expect(rgba[2]).withContext("mix blend 0.0 .b").toEqualEpsilon(116, 1); + expect(rgba[1]).withContext("mix blend 0.0 .g").toEqualEpsilon(118, 1); + expect(rgba[2]).withContext("mix blend 0.0 .b").toEqualEpsilon(118, 1); expect(rgba[3]).withContext("mix blend 0.0 .a").toEqual(255); }); @@ -3473,7 +3486,7 @@ describe( expect(renderOptions).toRenderAndCall(function (rgba) { expect(rgba[0]).withContext("mix blend 1.0 .r").toEqual(replaceRed); expect(rgba[1]).withContext("mix blend 1.0 .g").toEqual(replaceGreen); - expect(rgba[2]).withContext("mix blend 1.0 .b").toEqualEpsilon(73, 1); + expect(rgba[2]).withContext("mix blend 1.0 .b").toEqualEpsilon(62, 1); expect(rgba[3]).withContext("mix blend 1.0 .a").toEqual(255); }); @@ -3488,7 +3501,7 @@ describe( expect(rgba[1]).withContext("mix yellow+alpha .g").toBeGreaterThan(0); expect(rgba[2]) .withContext("mix yellow+alpha .b") - .toEqualEpsilon(43, 1); + .toEqualEpsilon(80, 1); expect(rgba[3]).withContext("mix yellow+alpha .a").toEqual(255); }); } @@ -5775,7 +5788,10 @@ describe( viewNothing(); - const tileset = await Cesium3DTileset.fromUrl(multipleContentsUrl); + const tileset = await Cesium3DTileset.fromUrl( + multipleContentsUrl, + options, + ); scene.primitives.add(tileset); viewAllTiles(); scene.renderForSpecs(); @@ -5954,7 +5970,10 @@ describe( viewNothing(); let errorCount = 0; - const tileset = await Cesium3DTileset.fromUrl(multipleContentsUrl); + const tileset = await Cesium3DTileset.fromUrl( + multipleContentsUrl, + options, + ); tileset.tileFailed.addEventListener(function (event) { errorCount++; expect(endsWith(event.url, ".json")).toBe(true); diff --git a/packages/engine/Specs/Scene/DynamicEnvironmentMapManagerSpec.js b/packages/engine/Specs/Scene/DynamicEnvironmentMapManagerSpec.js new file mode 100644 index 000000000000..57ea8ee5354f --- /dev/null +++ b/packages/engine/Specs/Scene/DynamicEnvironmentMapManagerSpec.js @@ -0,0 +1,1099 @@ +import { + Cartesian3, + Cartographic, + Color, + CubeMap, + DynamicAtmosphereLightingType, + DynamicEnvironmentMapManager, + Ellipsoid, + JulianDate, + Math as CesiumMath, + TextureMinificationFilter, +} from "../../index.js"; +import createScene from "../../../../Specs/createScene.js"; +import Atmosphere from "../../Source/Scene/Atmosphere.js"; + +describe("Scene/DynamicEnvironmentMapManager", function () { + it("constructs with default values", function () { + const manager = new DynamicEnvironmentMapManager(); + + expect(manager.enabled).toBeTrue(); + expect(manager.shouldUpdate).toBeTrue(); + expect(manager.maximumSecondsDifference).toBe(3600); + expect(manager.maximumPositionEpsilon).toBe(1000.0); + expect(manager.atmosphereScatteringIntensity).toBe(2.0); + expect(manager.gamma).toBe(1.0); + expect(manager.brightness).toBe(1.0); + expect(manager.saturation).toBe(1.0); + expect(manager.groundColor).toEqual( + DynamicEnvironmentMapManager.AVERAGE_EARTH_GROUND_COLOR, + ); + expect(manager.groundAlbedo).toBe(0.31); + }); + + describe( + "render tests", + () => { + const time = JulianDate.fromIso8601("2024-08-30T10:45:00Z"); + + let scene; + + beforeAll(() => { + scene = createScene({ + skyBox: false, + }); + }); + + afterAll(() => { + scene.destroyForSpecs(); + }); + + afterEach(() => { + scene.primitives.removeAll(); + scene.atmosphere = new Atmosphere(); + }); + + // Allows the compute commands to be added to the command list at the right point in the pipeline + function EnvironmentMockPrimitive(manager) { + this.update = function (frameState) { + manager.update(frameState); + }; + this.destroy = function () {}; + this.isDestroyed = function () { + return false; + }; + } + + it("creates environment map and spherical harmonics at surface in Philadelphia with static lighting", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.11017649620771408, + 0.13869766891002655, + 0.17165547609329224, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.08705271780490875, + 0.11016352474689484, + 0.15077166259288788, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.0014497060328722, + -0.0013909616973251104, + -0.00141593546140939, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.00010713530355133116, + 0.00016706169117242098, + 0.00006681153899990022, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("creates environment map and spherical harmonics at altitude in Philadelphia with static lighting", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + + const cartographic = Cartographic.fromDegrees( + -75.165222, + 39.952583, + 20000.0, + ); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.028324954211711884, + 0.03880387544631958, + 0.050429586321115494, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + -0.0048759132623672485, + -0.00047372994595207274, + 0.011921915225684643, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.00038596082595176995, + -0.0005534383235499263, + -0.001172146643511951, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.0005309119587764144, + 0.00010014028521254659, + -0.0005452318582683802, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeGreaterThan(0.0); + }); + + it("creates environment map and spherical harmonics above Earth's atmosphere with static lighting", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + + const cartographic = Cartographic.fromDegrees( + -75.165222, + 39.952583, + 1000000.0, + ); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.33580833673477173, + 0.3365404009819031, + 0.3376566469669342, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.2528926134109497, + 0.25208908319473267, + 0.25084879994392395, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + 0.001018671551719308, + 0.0009837104007601738, + 0.0008832928724586964, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + -0.0017577273538336158, + -0.0015308377332985401, + -0.0012394117657095194, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("creates environment map and spherical harmonics at surface in Philadelphia with dynamic lighting", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.atmosphere.dynamicLighting = + DynamicAtmosphereLightingType.SUNLIGHT; + + scene.renderForSpecs(time); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(time); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(time); + scene.renderForSpecs(time); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.034476835280656815, + 0.04265068098902702, + 0.04163559526205063, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.01569328084588051, + 0.023243442177772522, + 0.025639381259679794, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.003795207943767309, + -0.0033528741914778948, + -0.0031588575802743435, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.008755888789892197, + 0.007121194154024124, + 0.005899451207369566, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeGreaterThan(0.0); + }); + + it("creates environment map and spherical harmonics at surface in Sydney with dynamic lighting", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + + const cartographic = Cartographic.fromDegrees(151.2099, -33.865143); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.atmosphere.dynamicLighting = + DynamicAtmosphereLightingType.SUNLIGHT; + + scene.renderForSpecs(time); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(time); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(time); + scene.renderForSpecs(time); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.0054358793422579765, + 0.0054358793422579765, + 0.0027179396711289883, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.0037772462237626314, + 0.0037772462237626314, + 0.0018886231118813157, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.000007333524990826845, + -0.000007333524990826845, + -0.0000036667624954134226, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.000008501945558236912, + 0.000008501945558236912, + 0.000004250972779118456, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("lighting uses atmosphere properties", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + + const cartographic = Cartographic.fromDegrees( + -75.165222, + 39.952583, + 20000.0, + ); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + scene.atmosphere.hueShift = 0.5; + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.05602460727095604, + 0.04545757919549942, + 0.02313476987183094, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.008652083575725555, + 0.004114487674087286, + -0.0017214358085766435, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.00099410570692271, + -0.0008244783966802061, + -0.00026270488160662353, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + -0.000446554331574589, + -0.000012375472579151392, + 0.0005265426589176059, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeGreaterThan(0.0); + }); + + it("lighting uses atmosphereScatteringIntensity value", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + manager.atmosphereScatteringIntensity = 1.0; + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.0322723351418972, + 0.039464931935071945, + 0.047749463468790054, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.025989927351474762, + 0.031872138381004333, + 0.04223670810461044, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.0008744273218326271, + -0.0008044499554671347, + -0.0008345510577782989, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + -0.0000013118115020915866, + -0.000017321406630799174, + -0.000006108442903496325, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("lighting uses gamma value", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + manager.gamma = 0.5; + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.18712928891181946, + 0.21367456018924713, + 0.23666927218437195, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.13568174839019775, + 0.15787045657634735, + 0.19085952639579773, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.0011452456237748265, + -0.0010327763156965375, + -0.001100384397432208, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.00025430042296648026, + 0.00028964842204004526, + 0.00021805899450555444, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("lighting uses brightness value", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + manager.brightness = 0.5; + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.05981340631842613, + 0.07419705390930176, + 0.09077795594930649, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.051604993641376495, + 0.06336799263954163, + 0.08409948647022247, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.000745132565498352, + -0.0006284310948103666, + -0.000669674074742943, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.00004796023131348193, + 0.000024254957679659128, + 0.00004792874096892774, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("lighting uses saturation value", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + manager.saturation = 0.0; + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.13499368727207184, + 0.13499368727207184, + 0.13499368727207184, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.1081928238272667, + 0.1081928238272667, + 0.1081928238272667, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.0014497060328722, + -0.0013909616973251104, + -0.00141593546140939, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.00010713530355133116, + 0.00016706169117242098, + 0.00006681153899990022, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("lighting uses ground color value", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + manager.groundColor = Color.RED; + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.1342056840658188, + 0.11958353966474533, + 0.15991388261318207, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.07575193047523499, + 0.11915278434753418, + 0.15629366040229797, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.0011700564064085484, + -0.0016134318429976702, + -0.0015525781782343984, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.0002928615431301296, + 0.000019326049368828535, + -0.000023931264877319336, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("lighting uses ground albedo value", async function () { + if (!scene.highDynamicRangeSupported) { + return; + } + + const manager = new DynamicEnvironmentMapManager(); + manager.groundAlbedo = 1.0; + + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap).toBeInstanceOf(CubeMap); + + scene.renderForSpecs(); + + expect(manager.radianceCubeMap.sampler.minificationFilter).toEqual( + TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, + ); // Has mipmaps for specular maps + + scene.renderForSpecs(); + scene.renderForSpecs(); + + expect(manager.sphericalHarmonicCoefficients[0]).toEqualEpsilon( + new Cartesian3( + 0.15277373790740967, + 0.1812949925661087, + 0.19759616255760193, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[1]).toEqualEpsilon( + new Cartesian3( + 0.0670194923877716, + 0.09013032913208008, + 0.13857196271419525, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[2]).toEqualEpsilon( + new Cartesian3( + -0.000953961513005197, + -0.000895244418643415, + -0.0011140345595777035, + ), + CesiumMath.EPSILON4, + ); + expect(manager.sphericalHarmonicCoefficients[3]).toEqualEpsilon( + new Cartesian3( + 0.00043638586066663265, + 0.0004962628008797765, + 0.0002673182752914727, + ), + CesiumMath.EPSILON4, + ); + + expect(manager.sphericalHarmonicCoefficients[4].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[4].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[5].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[5].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[6].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[6].z).toBeLessThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[7].x).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].y).toBeGreaterThan(0.0); + expect(manager.sphericalHarmonicCoefficients[7].z).toBeGreaterThan(0.0); + + expect(manager.sphericalHarmonicCoefficients[8].x).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].y).toBeLessThan(0.0); + expect(manager.sphericalHarmonicCoefficients[8].z).toBeLessThan(0.0); + }); + + it("destroys", function () { + if (!scene.highDynamicRangeSupported) { + const manager = new DynamicEnvironmentMapManager(); + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + manager.destroy(); + + expect(manager.isDestroyed()).toBe(true); + return; + } + + const manager = new DynamicEnvironmentMapManager(); + const cartographic = Cartographic.fromDegrees(-75.165222, 39.952583); + manager.position = + Ellipsoid.WGS84.cartographicToCartesian(cartographic); + + const primitive = new EnvironmentMockPrimitive(manager); + scene.primitives.add(primitive); + + scene.renderForSpecs(); + scene.renderForSpecs(); + scene.renderForSpecs(); + scene.renderForSpecs(); + + manager.destroy(); + + expect(manager.isDestroyed()).toBe(true); + }); + }, + "WebGL", + ); +}); diff --git a/packages/engine/Specs/Scene/ImageBasedLightingSpec.js b/packages/engine/Specs/Scene/ImageBasedLightingSpec.js index a129dd4ce0da..ea4edb7bb0f7 100644 --- a/packages/engine/Specs/Scene/ImageBasedLightingSpec.js +++ b/packages/engine/Specs/Scene/ImageBasedLightingSpec.js @@ -22,7 +22,6 @@ describe("Scene/ImageBasedLighting", function () { new Cartesian2(1.0, 1.0), ), ).toBe(true); - expect(imageBasedLighting.luminanceAtZenith).toEqual(0.2); expect(imageBasedLighting.sphericalHarmonicCoefficients).toBeUndefined(); expect(imageBasedLighting.specularEnvironmentMaps).toBeUndefined(); }); @@ -84,15 +83,6 @@ describe("Scene/ImageBasedLighting", function () { ).toBe(true); }); - it("luminanceAtZenith saves previous value", function () { - const imageBasedLighting = new ImageBasedLighting({ - luminanceAtZenith: 0.0, - }); - imageBasedLighting.luminanceAtZenith = 0.5; - expect(imageBasedLighting.luminanceAtZenith).toBe(0.5); - expect(imageBasedLighting._previousLuminanceAtZenith).toBe(0.0); - }); - it("sphericalHarmonicCoefficients saves previous value", function () { const imageBasedLighting = new ImageBasedLighting({ sphericalHarmonicCoefficients: testCoefficients, @@ -115,15 +105,4 @@ describe("Scene/ImageBasedLighting", function () { imageBasedLighting.imageBasedLightingFactor = new Cartesian2(0.5, 0.0); expect(imageBasedLighting.enabled).toBe(true); }); - - it("useSphericalHarmonicCoefficients returns correct values", function () { - const imageBasedLighting = new ImageBasedLighting(); - expect(imageBasedLighting.useSphericalHarmonicCoefficients).toBe(false); - - imageBasedLighting.sphericalHarmonicCoefficients = testCoefficients; - expect(imageBasedLighting.useSphericalHarmonicCoefficients).toBe(true); - - imageBasedLighting.sphericalHarmonicCoefficients = undefined; - expect(imageBasedLighting.useSphericalHarmonicCoefficients).toBe(false); - }); }); diff --git a/packages/engine/Specs/Scene/Model/ImageBasedLightingPipelineStageSpec.js b/packages/engine/Specs/Scene/Model/ImageBasedLightingPipelineStageSpec.js index 9c3f32e422ed..9d985bdb7b6e 100644 --- a/packages/engine/Specs/Scene/Model/ImageBasedLightingPipelineStageSpec.js +++ b/packages/engine/Specs/Scene/Model/ImageBasedLightingPipelineStageSpec.js @@ -22,6 +22,7 @@ describe("Scene/Model/ImageBasedLightingPipelineStage", function () { const imageBasedLighting = new ImageBasedLighting(); const mockModel = { imageBasedLighting: imageBasedLighting, + environmentMapManager: {}, _iblReferenceFrameMatrix: Matrix3.clone(Matrix3.IDENTITY), }; @@ -40,12 +41,10 @@ describe("Scene/Model/ImageBasedLightingPipelineStage", function () { ShaderBuilderTester.expectHasFragmentDefines(shaderBuilder, [ "USE_IBL_LIGHTING", - "USE_SUN_LUMINANCE", ]); ShaderBuilderTester.expectHasFragmentUniforms(shaderBuilder, [ "uniform vec2 model_iblFactor;", "uniform mat3 model_iblReferenceFrameMatrix;", - "uniform float model_luminanceAtZenith;", ]); ShaderBuilderTester.expectFragmentLinesEqual(shaderBuilder, [ @@ -66,10 +65,6 @@ describe("Scene/Model/ImageBasedLightingPipelineStage", function () { mockModel._iblReferenceFrameMatrix, ), ).toBe(true); - - expect(uniformMap.model_luminanceAtZenith()).toEqual( - imageBasedLighting.luminanceAtZenith, - ); }); // These are dummy values, not meant to represent valid spherical harmonic coefficients. @@ -89,10 +84,10 @@ describe("Scene/Model/ImageBasedLightingPipelineStage", function () { const imageBasedLighting = new ImageBasedLighting({ sphericalHarmonicCoefficients: testCoefficients, }); - imageBasedLighting.luminanceAtZenith = undefined; const mockModel = { imageBasedLighting: imageBasedLighting, + environmentMapManager: {}, _iblReferenceFrameMatrix: Matrix3.clone(Matrix3.IDENTITY), }; @@ -151,11 +146,11 @@ describe("Scene/Model/ImageBasedLightingPipelineStage", function () { const imageBasedLighting = new ImageBasedLighting({ specularEnvironmentMaps: "example.ktx2", }); - imageBasedLighting.luminanceAtZenith = undefined; imageBasedLighting._specularEnvironmentCubeMap = mockCubeMap; const mockModel = { imageBasedLighting: imageBasedLighting, + environmentMapManager: {}, _iblReferenceFrameMatrix: Matrix3.clone(Matrix3.IDENTITY), }; diff --git a/packages/engine/Specs/Scene/Model/ModelSpec.js b/packages/engine/Specs/Scene/Model/ModelSpec.js index aa4f099a657a..2f31aa01b5fa 100644 --- a/packages/engine/Specs/Scene/Model/ModelSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelSpec.js @@ -3110,7 +3110,11 @@ describe( describe("light color", function () { it("initializes with light color", async function () { const model = await loadAndZoomToModelAsync( - { gltf: boxTexturedGltfUrl, lightColor: Cartesian3.ZERO }, + { + gltf: boxTexturedGltfUrl, + lightColor: Cartesian3.ZERO, + imageBasedLighting: undefined, + }, scene, ); verifyRender(model, false); @@ -3118,7 +3122,7 @@ describe( it("changing light color works", async function () { const model = await loadAndZoomToModelAsync( - { gltf: boxTexturedGltfUrl }, + { gltf: boxTexturedGltfUrl, imageBasedLighting: undefined }, scene, ); model.lightColor = Cartesian3.ZERO; @@ -3133,7 +3137,7 @@ describe( it("light color doesn't affect unlit models", async function () { const model = await loadAndZoomToModelAsync( - { gltf: boxUnlitUrl }, + { gltf: boxUnlitUrl, imageBasedLighting: undefined }, scene, ); const options = { @@ -3156,7 +3160,6 @@ describe( it("initializes with imageBasedLighting", async function () { const ibl = new ImageBasedLighting({ imageBasedLightingFactor: Cartesian2.ZERO, - luminanceAtZenith: 0.5, }); const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, imageBasedLighting: ibl }, @@ -3167,7 +3170,10 @@ describe( it("creates default imageBasedLighting", async function () { const model = await loadAndZoomToModelAsync( - { gltf: boxTexturedGltfUrl }, + { + gltf: boxTexturedGltfUrl, + imageBasedLighting: undefined, + }, scene, ); const imageBasedLighting = model.imageBasedLighting; @@ -3178,7 +3184,6 @@ describe( new Cartesian2(1, 1), ), ).toBe(true); - expect(imageBasedLighting.luminanceAtZenith).toBe(0.2); expect( imageBasedLighting.sphericalHarmonicCoefficients, ).toBeUndefined(); @@ -3187,10 +3192,23 @@ describe( it("changing imageBasedLighting works", async function () { const imageBasedLighting = new ImageBasedLighting({ - imageBasedLightingFactor: Cartesian2.ZERO, + sphericalHarmonicCoefficients: [ + new Cartesian3(0.35449, 0.35449, 0.35449), + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + ], }); const model = await loadAndZoomToModelAsync( - { gltf: boxTexturedGltfUrl }, + { + gltf: boxTexturedGltfUrl, + imageBasedLighting: undefined, + }, scene, ); const renderOptions = { @@ -3216,6 +3234,17 @@ describe( gltf: boxTexturedGltfUrl, imageBasedLighting: new ImageBasedLighting({ imageBasedLightingFactor: Cartesian2.ZERO, + sphericalHarmonicCoefficients: [ + new Cartesian3(0.35449, 0.35449, 0.35449), + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + ], }), }, scene, @@ -3238,34 +3267,6 @@ describe( }); }); - it("changing luminanceAtZenith works", async function () { - const model = await loadAndZoomToModelAsync( - { - gltf: boxTexturedGltfUrl, - imageBasedLighting: new ImageBasedLighting({ - luminanceAtZenith: 0.0, - }), - }, - scene, - ); - const renderOptions = { - scene: scene, - time: defaultDate, - }; - - let result; - verifyRender(model, true); - expect(renderOptions).toRenderAndCall(function (rgba) { - result = rgba; - }); - - const ibl = model.imageBasedLighting; - ibl.luminanceAtZenith = 0.2; - expect(renderOptions).toRenderAndCall(function (rgba) { - expect(rgba).not.toEqual(result); - }); - }); - it("changing sphericalHarmonicCoefficients works", async function () { if (!scene.highDynamicRangeSupported) { return; diff --git a/packages/engine/Specs/Scene/Model/loadAndZoomToModelAsync.js b/packages/engine/Specs/Scene/Model/loadAndZoomToModelAsync.js index 2a17f97533a4..e50ab6a8cf5c 100644 --- a/packages/engine/Specs/Scene/Model/loadAndZoomToModelAsync.js +++ b/packages/engine/Specs/Scene/Model/loadAndZoomToModelAsync.js @@ -1,7 +1,31 @@ -import { Model } from "../../../index.js"; +import { Model, ImageBasedLighting, Cartesian3 } from "../../../index.js"; import pollToPromise from "../../../../../Specs/pollToPromise.js"; +// A white ambient light with low intensity +const defaultIbl = new ImageBasedLighting({ + sphericalHarmonicCoefficients: [ + new Cartesian3(0.35449, 0.35449, 0.35449), + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + ], +}); + async function loadAndZoomToModelAsync(options, scene) { + options = { + environmentMapOptions: { + enabled: false, // disable other diffuse lighting by default + ...options.environmentMapOptions, + }, + imageBasedLighting: defaultIbl, + ...options, + }; + const model = await Model.fromGltfAsync(options); scene.primitives.add(model); diff --git a/packages/engine/Specs/Scene/ShadowMapSpec.js b/packages/engine/Specs/Scene/ShadowMapSpec.js index 00d862e26af7..2a66f528b10d 100644 --- a/packages/engine/Specs/Scene/ShadowMapSpec.js +++ b/packages/engine/Specs/Scene/ShadowMapSpec.js @@ -11,6 +11,7 @@ import { HeadingPitchRange, HeadingPitchRoll, HeightmapTerrainData, + ImageBasedLighting, JulianDate, Math as CesiumMath, Matrix4, @@ -281,7 +282,29 @@ describe( } async function loadModel(options) { - const model = scene.primitives.add(await Model.fromGltfAsync(options)); + // A white ambient light with low intensity + const defaultIbl = new ImageBasedLighting({ + sphericalHarmonicCoefficients: [ + new Cartesian3(0.35449, 0.35449, 0.35449), + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + Cartesian3.ZERO, + ], + }); + const model = scene.primitives.add( + await Model.fromGltfAsync({ + environmentMapOptions: { + enabled: false, + }, + imageBasedLighting: defaultIbl, + ...options, + }), + ); await pollToPromise( function () { // Render scene to progressively load the model