diff --git a/Apps/SampleData/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx b/Apps/SampleData/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx new file mode 100644 index 000000000000..7a8880d04115 Binary files /dev/null and b/Apps/SampleData/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx differ diff --git a/Apps/SampleData/models/Pawns/Pawns.glb b/Apps/SampleData/models/Pawns/Pawns.glb new file mode 100644 index 000000000000..417011c061d9 Binary files /dev/null and b/Apps/SampleData/models/Pawns/Pawns.glb differ diff --git a/Apps/Sandcastle/gallery/Image-Based Lighting.html b/Apps/Sandcastle/gallery/Image-Based Lighting.html new file mode 100644 index 000000000000..24550eec6d30 --- /dev/null +++ b/Apps/Sandcastle/gallery/Image-Based Lighting.html @@ -0,0 +1,132 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + + + +
Luminance at Zenith + + +
+
+ + + diff --git a/Apps/Sandcastle/gallery/Image-Based Lighting.jpg b/Apps/Sandcastle/gallery/Image-Based Lighting.jpg new file mode 100644 index 000000000000..0277ebbb4168 Binary files /dev/null and b/Apps/Sandcastle/gallery/Image-Based Lighting.jpg differ diff --git a/CHANGES.md b/CHANGES.md index d6057f3b6452..3b973fe47f65 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,13 @@ Change Log ### 1.53 - 2019-01-02 +##### Additions :tada: +* Added image-based lighting for PBR models and 3D Tiles. [#7172](https://github.com/AnalyticalGraphicsInc/cesium/pull/7172) + * `Scene.specularEnvironmentMaps` is a url to a KTX file that contains the specular environment map and convoluted mipmaps for image-based lighting of all PBR models in the scene. + * `Scene.sphericalHarmonicCoefficients` is an array of 9 `Cartesian3` spherical harmonics coefficients for the diffuse irradiance of all PBR models in the scene. + * The `specularEnvironmentMaps` and `sphericalHarmonicCoefficients` properties of `Model` and `Cesium3DTileset` can be used to override the values from the scene for specific models and tilesets. + * The `luminanceAtZenith` property of `Model` and `Cesium3DTileset` adjusts the luminance of the procedural image-based lighting. + ##### Fixes :wrench: * Fixed 3D Tiles visibility checking when running multiple passes within the same frame. [#7289](https://github.com/AnalyticalGraphicsInc/cesium/pull/7289) * Fixed contrast on imagery layers. [#7382](https://github.com/AnalyticalGraphicsInc/cesium/issues/7382) diff --git a/Source/Core/loadKTX.js b/Source/Core/loadKTX.js index 94bfb7a3690b..8b34f6154ad4 100644 --- a/Source/Core/loadKTX.js +++ b/Source/Core/loadKTX.js @@ -5,7 +5,8 @@ define([ './defined', './PixelFormat', './Resource', - './RuntimeError' + './RuntimeError', + './WebGLConstants' ], function( when, Check, @@ -13,7 +14,8 @@ define([ defined, PixelFormat, Resource, - RuntimeError) { + RuntimeError, + WebGLConstants) { 'use strict'; /** @@ -93,6 +95,7 @@ define([ var fileIdentifier = [0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A]; var endiannessTest = 0x04030201; + var faceOrder = ['positiveX', 'negativeX', 'positiveY', 'negativeY', 'positiveZ', 'negativeZ']; var sizeOfUint32 = 4; @@ -100,7 +103,8 @@ define([ var byteBuffer = new Uint8Array(data); var isKTX = true; - for (var i = 0; i < fileIdentifier.length; ++i) { + var i; + for (i = 0; i < fileIdentifier.length; ++i) { if (fileIdentifier[i] !== byteBuffer[i]) { isKTX = false; break; @@ -170,9 +174,9 @@ define([ // Some tools use a sized internal format. // See table 2: https://www.opengl.org/sdk/docs/man/html/glTexImage2D.xhtml - if (glInternalFormat === 0x8051) { // GL_RGB8 + if (glInternalFormat === WebGLConstants.RGB8) { glInternalFormat = PixelFormat.RGB; - } else if (glInternalFormat === 0x8058) { // GL_RGBA8 + } else if (glInternalFormat === WebGLConstants.RGBA8) { glInternalFormat = PixelFormat.RGBA; } @@ -190,6 +194,8 @@ define([ if (glFormat !== 0) { throw new RuntimeError('glFormat must be zero when the texture is compressed.'); } + } else if (glType !== WebGLConstants.UNSIGNED_BYTE) { + throw new RuntimeError('Only unsigned byte buffers are supported.'); } else if (glBaseInternalFormat !== glFormat) { throw new RuntimeError('The base internal format must be the same as the format for uncompressed textures.'); } @@ -201,19 +207,35 @@ define([ if (numberOfArrayElements !== 0) { throw new RuntimeError('Texture arrays are unsupported.'); } - if (numberOfFaces !== 1) { - throw new RuntimeError('Cubemaps are unsupported.'); + + var offset = texture.byteOffset; + var mipmaps = new Array(numberOfMipmapLevels); + for (i = 0; i < numberOfMipmapLevels; ++i) { + var level = mipmaps[i] = {}; + for (var j = 0; j < numberOfFaces; ++j) { + var width = pixelWidth >> i; + var height = pixelHeight >> i; + var levelSize = PixelFormat.isCompressedFormat(glInternalFormat) ? + PixelFormat.compressedTextureSizeInBytes(glInternalFormat, width, height) : + PixelFormat.textureSizeInBytes(glInternalFormat, glType, width, height); + var levelBuffer = new Uint8Array(texture.buffer, offset, levelSize); + level[faceOrder[j]] = new CompressedTextureBuffer(glInternalFormat, width, height, levelBuffer); + offset += levelSize; + } + offset += 3 - ((offset + 3) % 4) + 4; } - // Only use the level 0 mipmap - if (numberOfMipmapLevels > 1) { - var levelSize = PixelFormat.isCompressedFormat(glInternalFormat) ? - PixelFormat.compressedTextureSizeInBytes(glInternalFormat, pixelWidth, pixelHeight) : - PixelFormat.textureSizeInBytes(glInternalFormat, pixelWidth, pixelHeight); - texture = new Uint8Array(texture.buffer, texture.byteOffset, levelSize); + var result = mipmaps; + if (numberOfFaces === 1) { + for (i = 0; i < numberOfMipmapLevels; ++i) { + result[i] = result[i][faceOrder[0]]; + } + } + if (numberOfMipmapLevels === 1) { + result = result[0]; } - return new CompressedTextureBuffer(glInternalFormat, pixelWidth, pixelHeight, texture); + return result; } return loadKTX; diff --git a/Source/Renderer/AutomaticUniforms.js b/Source/Renderer/AutomaticUniforms.js index 988f6dbe5c9e..1134127fb474 100644 --- a/Source/Renderer/AutomaticUniforms.js +++ b/Source/Renderer/AutomaticUniforms.js @@ -1560,6 +1560,82 @@ define([ } }), + /** + * An automatic GLSL uniform containing the specular environment map atlas used within the scene. + * + * @alias czm_specularEnvironmentMaps + * @namespace + * @glslUniform + * + * @example + * // GLSL declaration + * uniform sampler2D czm_specularEnvironmentMaps; + */ + czm_specularEnvironmentMaps : new AutomaticUniform({ + size : 1, + datatype : WebGLConstants.SAMPLER_2D, + getValue : function(uniformState) { + return uniformState.specularEnvironmentMaps; + } + }), + + /** + * An automatic GLSL uniform containing the size of the specular environment map atlas used within the scene. + * + * @alias czm_specularEnvironmentMapSize + * @namespace + * @glslUniform + * + * @example + * // GLSL declaration + * uniform vec2 czm_specularEnvironmentMapSize; + */ + czm_specularEnvironmentMapSize : new AutomaticUniform({ + size : 1, + datatype : WebGLConstants.FLOAT_VEC2, + getValue : function(uniformState) { + return uniformState.specularEnvironmentMaps.dimensions; + } + }), + + /** + * An automatic GLSL uniform containing the maximum level-of-detail of the specular environment map atlas used within the scene. + * + * @alias czm_specularEnvironmentMapsMaximumLOD + * @namespace + * @glslUniform + * + * @example + * // GLSL declaration + * uniform float czm_specularEnvironmentMapsMaximumLOD; + */ + czm_specularEnvironmentMapsMaximumLOD : new AutomaticUniform({ + size : 1, + datatype : WebGLConstants.FLOAT, + getValue : function(uniformState) { + return uniformState.specularEnvironmentMapsMaximumLOD; + } + }), + + /** + * An automatic GLSL uniform containing the spherical harmonic coefficients used within the scene. + * + * @alias czm_sphericalHarmonicCoefficients + * @namespace + * @glslUniform + * + * @example + * // GLSL declaration + * uniform vec3[9] czm_sphericalHarmonicCoefficients; + */ + czm_sphericalHarmonicCoefficients : new AutomaticUniform({ + size : 9, + datatype : WebGLConstants.FLOAT_VEC3, + getValue : function(uniformState) { + return uniformState.sphericalHarmonicCoefficients; + } + }), + /** * An automatic GLSL uniform representing a 3x3 rotation matrix that transforms * from True Equator Mean Equinox (TEME) axes to the pseudo-fixed axes at the current scene time. diff --git a/Source/Renderer/Context.js b/Source/Renderer/Context.js index 2f2809f517d0..5bc23186ddf7 100644 --- a/Source/Renderer/Context.js +++ b/Source/Renderer/Context.js @@ -28,6 +28,7 @@ define([ './ShaderCache', './ShaderProgram', './Texture', + './TextureCache', './UniformState', './VertexArray' ], function( @@ -60,6 +61,7 @@ define([ ShaderCache, ShaderProgram, Texture, + TextureCache, UniformState, VertexArray) { 'use strict'; @@ -234,6 +236,7 @@ define([ this._throwOnWebGLError = false; this._shaderCache = new ShaderCache(this); + this._textureCache = new TextureCache(); var gl = glContext; @@ -469,6 +472,11 @@ define([ return this._shaderCache; } }, + textureCache : { + get : function() { + return this._textureCache; + } + }, uniformState : { get : function() { return this._us; @@ -1288,6 +1296,7 @@ define([ } this._shaderCache = this._shaderCache.destroy(); + this._textureCache = this._textureCache.destroy(); this._defaultTexture = this._defaultTexture && this._defaultTexture.destroy(); this._defaultCubeMap = this._defaultCubeMap && this._defaultCubeMap.destroy(); diff --git a/Source/Renderer/CubeMap.js b/Source/Renderer/CubeMap.js index 6117e7c71387..e16ad7d14dbf 100644 --- a/Source/Renderer/CubeMap.js +++ b/Source/Renderer/CubeMap.js @@ -130,6 +130,10 @@ define([ function createFace(target, sourceFace, preMultiplyAlpha, flipY) { // TODO: gl.pixelStorei(gl._UNPACK_ALIGNMENT, 4); var arrayBufferView = sourceFace.arrayBufferView; + if (!defined(arrayBufferView)) { + arrayBufferView = sourceFace.bufferView; + } + if (arrayBufferView) { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); diff --git a/Source/Renderer/ShaderSource.js b/Source/Renderer/ShaderSource.js index f70c7d8c250a..e6e38b5e03a6 100644 --- a/Source/Renderer/ShaderSource.js +++ b/Source/Renderer/ShaderSource.js @@ -248,6 +248,11 @@ define([ result += '#define OUTPUT_DECLARATION\n\n'; } + // Define a constant for the OES_texture_float_linear extension since WebGL does not. + if (context.textureFloatLinear) { + result += '#define OES_texture_float_linear\n\n'; + } + // append built-ins if (shaderSource.includeBuiltIns) { result += getBuiltinsAndAutomaticUniforms(combinedSources); diff --git a/Source/Renderer/TextureCache.js b/Source/Renderer/TextureCache.js new file mode 100644 index 000000000000..10b93ce1fc5f --- /dev/null +++ b/Source/Renderer/TextureCache.js @@ -0,0 +1,90 @@ +define([ + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject' + ], function( + defined, + defineProperties, + destroyObject) { + 'use strict'; + + /** + * @private + */ + function TextureCache() { + this._textures = {}; + this._numberOfTextures = 0; + this._texturesToRelease = {}; + } + + defineProperties(TextureCache.prototype, { + numberOfTextures : { + get : function() { + return this._numberOfTextures; + } + } + }); + + TextureCache.prototype.getTexture = function(keyword) { + var cachedTexture = this._textures[keyword]; + if (!defined(cachedTexture)) { + return undefined; + } + + // No longer want to release this if it was previously released. + delete this._texturesToRelease[keyword]; + + ++cachedTexture.count; + return cachedTexture.texture; + }; + + TextureCache.prototype.addTexture = function(keyword, texture) { + var cachedTexture = { + texture : texture, + count : 1 + }; + + texture.finalDestroy = texture.destroy; + + var that = this; + texture.destroy = function() { + if (--cachedTexture.count === 0) { + that._texturesToRelease[keyword] = cachedTexture; + } + }; + + this._textures[keyword] = cachedTexture; + ++this._numberOfTextures; + }; + + TextureCache.prototype.destroyReleasedTextures = function() { + var texturesToRelease = this._texturesToRelease; + + for (var keyword in texturesToRelease) { + if (texturesToRelease.hasOwnProperty(keyword)) { + var cachedTexture = texturesToRelease[keyword]; + delete this._textures[keyword]; + cachedTexture.texture.finalDestroy(); + --this._numberOfTextures; + } + } + + this._texturesToRelease = {}; + }; + + TextureCache.prototype.isDestroyed = function() { + return false; + }; + + TextureCache.prototype.destroy = function() { + var textures = this._textures; + for (var keyword in textures) { + if (textures.hasOwnProperty(keyword)) { + textures[keyword].texture.finalDestroy(); + } + } + return destroyObject(this); + }; + + return TextureCache; +}); diff --git a/Source/Renderer/UniformState.js b/Source/Renderer/UniformState.js index 998c409602f4..aff11a5b9196 100644 --- a/Source/Renderer/UniformState.js +++ b/Source/Renderer/UniformState.js @@ -162,8 +162,12 @@ define([ this._orthographicIn3D = false; this._backgroundColor = new Color(); - this._brdfLut = new Sampler(); - this._environmentMap = new Sampler(); + this._brdfLut = undefined; + this._environmentMap = undefined; + + this._sphericalHarmonicCoefficients = undefined; + this._specularEnvironmentMaps = undefined; + this._specularEnvironmentMapsMaximumLOD = undefined; this._fogDensity = undefined; @@ -860,7 +864,7 @@ define([ /** * The look up texture used to find the BRDF for a material * @memberof UniformState.prototype - * @type {Sampler} + * @type {Texture} */ brdfLut : { get : function() { @@ -871,7 +875,7 @@ define([ /** * The environment map of the scene * @memberof UniformState.prototype - * @type {Sampler} + * @type {CubeMap} */ environmentMap : { get : function() { @@ -879,6 +883,39 @@ define([ } }, + /** + * The spherical harmonic coefficients of the scene. + * @memberof UniformState.prototype + * @type {Cartesian3[]} + */ + sphericalHarmonicCoefficients : { + get : function() { + return this._sphericalHarmonicCoefficients; + } + }, + + /** + * The specular environment map atlas of the scene. + * @memberof UniformState.prototype + * @type {Texture} + */ + specularEnvironmentMaps : { + get : function() { + return this._specularEnvironmentMaps; + } + }, + + /** + * The maximum level-of-detail of the specular environment map atlas of the scene. + * @memberof UniformState.prototype + * @type {Number} + */ + specularEnvironmentMapsMaximumLOD : { + get : function() { + return this._specularEnvironmentMapsMaximumLOD; + } + }, + /** * @memberof UniformState.prototype * @type {Number} @@ -1093,6 +1130,10 @@ define([ this._environmentMap = defaultValue(frameState.environmentMap, frameState.context.defaultCubeMap); + this._sphericalHarmonicCoefficients = frameState.sphericalHarmonicCoefficients; + this._specularEnvironmentMaps = frameState.specularEnvironmentMaps; + this._specularEnvironmentMapsMaximumLOD = frameState.specularEnvironmentMapsMaximumLOD; + this._fogDensity = frameState.fog.density; this._invertClassificationColor = frameState.invertClassificationColor; diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index fdc72f3620b5..7429d7cf2c5b 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -386,7 +386,10 @@ define([ addBatchIdToGeneratedShaders : (batchLength > 0), // If the batch table has values in it, generated shaders will need a batchId attribute pickObject : pickObject, imageBasedLightingFactor : tileset.imageBasedLightingFactor, - lightColor : tileset.lightColor + lightColor : tileset.lightColor, + luminanceAtZenith : tileset.luminanceAtZenith, + sphericalHarmonicCoefficients : tileset.sphericalHarmonicCoefficients, + specularEnvironmentMaps : tileset.specularEnvironmentMaps }); } else { // This transcodes glTF to an internal representation for geometry so we can take advantage of the re-batching of vector data. @@ -473,18 +476,19 @@ define([ this._model.shadows = this._tileset.shadows; this._model.imageBasedLightingFactor = this._tileset.imageBasedLightingFactor; this._model.lightColor = this._tileset.lightColor; + this._model.luminanceAtZenith = this._tileset.luminanceAtZenith; + this._model.sphericalHarmonicCoefficients = this._tileset.sphericalHarmonicCoefficients; + this._model.specularEnvironmentMaps = this._tileset.specularEnvironmentMaps; this._model.debugWireframe = this._tileset.debugWireframe; // Update clipping planes var tilesetClippingPlanes = this._tileset.clippingPlanes; - if (defined(tilesetClippingPlanes)) { - this._model.clippingPlanesOriginMatrix = this._tileset.clippingPlanesOriginMatrix; - if (this._tile.clippingPlanesDirty) { - // Dereference the clipping planes from the model if they are irrelevant. - // Link/Dereference directly to avoid ownership checks. - // This will also trigger synchronous shader regeneration to remove or add the clipping plane and color blending code. - this._model._clippingPlanes = (tilesetClippingPlanes.enabled && this._tile._isClipped) ? tilesetClippingPlanes : undefined; - } + this._model.clippingPlanesOriginMatrix = this._tileset.clippingPlanesOriginMatrix; + if (defined(tilesetClippingPlanes) && this._tile.clippingPlanesDirty) { + // Dereference the clipping planes from the model if they are irrelevant. + // Link/Dereference directly to avoid ownership checks. + // This will also trigger synchronous shader regeneration to remove or add the clipping plane and color blending code. + this._model._clippingPlanes = (tilesetClippingPlanes.enabled && this._tile._isClipped) ? tilesetClippingPlanes : undefined; } // If the model references a different ClippingPlaneCollection due to the tileset's collection being replaced with a diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index c374c278f179..0ad7115a8ab6 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -121,6 +121,9 @@ define([ * @param {Object} [options.pointCloudShading] Options for constructing a {@link PointCloudShading} object to control point attenuation based on geometric error and lighting. * @param {Cartesian2} [options.imageBasedLightingFactor=new Cartesian2(1.0, 1.0)] Scales the diffuse and specular image-based lighting from the earth, sky, atmosphere and star skybox. * @param {Cartesian3} [options.lightColor] The color and intensity of the sunlight used to shade models. + * @param {Number} [options.luminanceAtZenith=0.5] 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 KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. * @param {Boolean} [options.debugWireframe=false] For debugging only. When true, render's each tile's content as a wireframe. @@ -593,6 +596,44 @@ define([ */ this.lightColor = options.lightColor; + /** + * 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 Cesium3DTileset#specularEnvironmentMaps} and {@link Cesium3DTileset#sphericalHarmonicCoefficients} are not defined. + * + * @type Number + * + * @default 0.5 + * + */ + this.luminanceAtZenith = defaultValue(options.luminanceAtZenith, 0.5); + + /** + * 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. + *

+ * There are nine Cartesian3 coefficients. + * The order of the coefficients is: L00, L1-1, L10, L11, L2-2, L2-1, L20, L21, L22 + *

+ * + * These values can be obtained by preprocessing the environment map using the cmgen tool of + * {@link https://github.com/google/filament/releases | Google's Filament project}. This will also generate a KTX file that can be + * supplied to {@link Cesium3DTileset#specularEnvironmentMaps}. + * + * @type {Cartesian3[]} + * @demo {@link https://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Image-Based Lighting.html|Sandcastle Image Based Lighting Demo} + * @see {@link https://graphics.stanford.edu/papers/envmap/envmap.pdf|An Efficient Representation for Irradiance Environment Maps} + */ + this.sphericalHarmonicCoefficients = options.sphericalHarmonicCoefficients; + + /** + * A URL to a KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. + * + * @demo {@link https://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Image-Based Lighting.html|Sandcastle Image Based Lighting Demo} + * @type {String} + * @see Cesium3DTileset#sphericalHarmonicCoefficients + */ + this.specularEnvironmentMaps = options.specularEnvironmentMaps; + /** * This property is for debugging only; it is not optimized for production use. *

diff --git a/Source/Scene/FrameState.js b/Source/Scene/FrameState.js index c79e5ad54ac7..b2297b39d959 100644 --- a/Source/Scene/FrameState.js +++ b/Source/Scene/FrameState.js @@ -50,6 +50,24 @@ define([ */ this.environmentMap = undefined; + /** + * The spherical harmonic coefficients used for image-based lighting for PBR models. + * @type {Cartesian3[]} + */ + this.sphericalHarmonicCoefficients = undefined; + + /** + * The specular environment atlas used for image-based lighting for PBR models. + * @type {Texture} + */ + this.specularEnvironmentMaps = undefined; + + /** + * The maximum level-of-detail of the specular environment atlas used for image-based lighting for PBR models. + * @type {Number} + */ + this.specularEnvironmentMapsMaximumLOD = undefined; + /** * The current mode of the scene. * diff --git a/Source/Scene/Instanced3DModel3DTileContent.js b/Source/Scene/Instanced3DModel3DTileContent.js index 45167476fe9c..aa150f33f08b 100644 --- a/Source/Scene/Instanced3DModel3DTileContent.js +++ b/Source/Scene/Instanced3DModel3DTileContent.js @@ -289,7 +289,10 @@ define([ opaquePass : Pass.CESIUM_3D_TILE, // Draw opaque portions during the 3D Tiles pass pickIdLoaded : getPickIdCallback(content), imageBasedLightingFactor : tileset.imageBasedLightingFactor, - lightColor : tileset.lightColor + lightColor : tileset.lightColor, + luminanceAtZenith : tileset.luminanceAtZenith, + sphericalHarmonicCoefficients : tileset.sphericalHarmonicCoefficients, + specularEnvironmentMaps : tileset.specularEnvironmentMaps }; if (gltfFormat === 0) { @@ -468,6 +471,10 @@ define([ this._batchTable.update(tileset, frameState); this._modelInstanceCollection.modelMatrix = this._tile.computedTransform; this._modelInstanceCollection.shadows = this._tileset.shadows; + this._modelInstanceCollection.lightColor = this._tileset.lightColor; + this._modelInstanceCollection.luminanceAtZenith = this._tileset.luminanceAtZenith; + this._modelInstanceCollection.sphericalHarmonicCoefficients = this._tileset.sphericalHarmonicCoefficients; + this._modelInstanceCollection.specularEnvironmentMaps = this._tileset.specularEnvironmentMaps; this._modelInstanceCollection.debugWireframe = this._tileset.debugWireframe; var model = this._modelInstanceCollection._model; @@ -475,13 +482,11 @@ define([ if (defined(model)) { // Update for clipping planes var tilesetClippingPlanes = this._tileset.clippingPlanes; - if (defined(tilesetClippingPlanes)) { - model.clippingPlanesOriginMatrix = this._tileset.clippingPlanesOriginMatrix; - if (this._tile.clippingPlanesDirty) { - // Dereference the clipping planes from the model if they are irrelevant - saves on shading - // Link/Dereference directly to avoid ownership checks. - model._clippingPlanes = (tilesetClippingPlanes.enabled && this._tile._isClipped) ? tilesetClippingPlanes : undefined; - } + model.clippingPlanesOriginMatrix = this._tileset.clippingPlanesOriginMatrix; + if (defined(tilesetClippingPlanes) && this._tile.clippingPlanesDirty) { + // Dereference the clipping planes from the model if they are irrelevant - saves on shading + // Link/Dereference directly to avoid ownership checks. + model._clippingPlanes = (tilesetClippingPlanes.enabled && this._tile._isClipped) ? tilesetClippingPlanes : undefined; } // If the model references a different ClippingPlaneCollection due to the tileset's collection being replaced with a diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 48d37eb915c5..b82a52fda3c2 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -20,6 +20,7 @@ define([ '../Core/getMagic', '../Core/getStringFromTypedArray', '../Core/IndexDatatype', + '../Core/isArray', '../Core/loadCRN', '../Core/loadImageFromTypedArray', '../Core/loadKTX', @@ -70,6 +71,7 @@ define([ './ModelMesh', './ModelNode', './ModelUtility', + './OctahedralProjectedCubeMap', './processModelMaterialsCommon', './processPbrMaterials', './SceneMode', @@ -96,6 +98,7 @@ define([ getMagic, getStringFromTypedArray, IndexDatatype, + isArray, loadCRN, loadImageFromTypedArray, loadKTX, @@ -146,6 +149,7 @@ define([ ModelMesh, ModelNode, ModelUtility, + OctahedralProjectedCubeMap, processModelMaterialsCommon, processPbrMaterials, SceneMode, @@ -288,6 +292,9 @@ define([ * @param {Boolean} [options.dequantizeInShader=true] Determines if a {@link https://github.com/google/draco|Draco} encoded model is dequantized on the GPU. This decreases total memory usage for encoded models. * @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 {Cartesian3} [options.lightColor] The color and intensity of the sunlight used to shade the model. + * @param {Number} [options.luminanceAtZenith=0.5] 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 KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. * * @see Model.fromGltf * @@ -668,7 +675,19 @@ define([ this._imageBasedLightingFactor = new Cartesian2(1.0, 1.0); Cartesian2.clone(options.imageBasedLightingFactor, this._imageBasedLightingFactor); this._lightColor = Cartesian3.clone(options.lightColor); - this._regenerateShaders = false; + + this._luminanceAtZenith = undefined; + this.luminanceAtZenith = defaultValue(options.luminanceAtZenith, 0.5); + + this._sphericalHarmonicCoefficients = options.sphericalHarmonicCoefficients; + this._specularEnvironmentMaps = options.specularEnvironmentMaps; + this._shouldUpdateSpecularMapAtlas = true; + this._specularEnvironmentMapAtlas = undefined; + + this._useDefaultSphericalHarmonics = false; + this._useDefaultSpecularMaps = false; + + this._shouldRegenerateShaders = false; } defineProperties(Model.prototype, { @@ -1109,8 +1128,8 @@ define([ Check.typeOf.number.greaterThanOrEquals('imageBasedLightingFactor.y', value.y, 0.0); Check.typeOf.number.lessThanOrEquals('imageBasedLightingFactor.y', value.y, 1.0); //>>includeEnd('debug'); - this._regenerateShaders = this._regenerateShaders || (this._imageBasedLightingFactor.x > 0.0 && value.x === 0.0) || (this._imageBasedLightingFactor.x === 0.0 && value.x > 0.0); - this._regenerateShaders = this._regenerateShaders || (this._imageBasedLightingFactor.y > 0.0 && value.y === 0.0) || (this._imageBasedLightingFactor.y === 0.0 && value.y > 0.0); + this._shouldRegenerateShaders = this._shouldRegenerateShaders || (this._imageBasedLightingFactor.x > 0.0 && value.x === 0.0) || (this._imageBasedLightingFactor.x === 0.0 && value.x > 0.0); + this._shouldRegenerateShaders = this._shouldRegenerateShaders || (this._imageBasedLightingFactor.y > 0.0 && value.y === 0.0) || (this._imageBasedLightingFactor.y === 0.0 && value.y > 0.0); Cartesian2.clone(value, this._imageBasedLightingFactor); } }, @@ -1136,9 +1155,84 @@ define([ if (value === lightColor || Cartesian3.equals(value, lightColor)) { return; } - this._regenerateShaders = this._regenerateShaders || (defined(lightColor) && !defined(value)) || (defined(value) && !defined(lightColor)); + this._shouldRegenerateShaders = this._shouldRegenerateShaders || (defined(lightColor) && !defined(value)) || (defined(value) && !defined(lightColor)); this._lightColor = Cartesian3.clone(value, lightColor); } + }, + + /** + * 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 Model#specularEnvironmentMaps} and {@link Model#sphericalHarmonicCoefficients} are not defined. + * + * @memberof Model.prototype + * + * @demo {@link https://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Image-Based Lighting.html|Sandcastle Image Based Lighting Demo} + * @type {Number} + * @default 0.5 + */ + luminanceAtZenith : { + get : function() { + return this._luminanceAtZenith; + }, + set : function(value) { + var lum = this._luminanceAtZenith; + if (value === lum) { + return; + } + this._shouldRegenerateShaders = this._shouldRegenerateShaders || (defined(lum) && !defined(value)) || (defined(value) && !defined(lum)); + 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. + *

+ * There are nine Cartesian3 coefficients. + * The order of the coefficients is: L00, L1-1, L10, L11, L2-2, L2-1, L20, L21, L22 + *

+ * + * These values can be obtained by preprocessing the environment map using the cmgen tool of + * {@link https://github.com/google/filament/releases | Google's Filament project}. This will also generate a KTX file that can be + * supplied to {@link Model#specularEnvironmentMaps}. + * + * @memberof Model.prototype + * + * @type {Cartesian3[]} + * @demo {@link https://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Image-Based Lighting.html|Sandcastle Image Based Lighting Demo} + * @see {@link https://graphics.stanford.edu/papers/envmap/envmap.pdf|An Efficient Representation for Irradiance Environment Maps} + */ + sphericalHarmonicCoefficients : { + get : function() { + return this._sphericalHarmonicCoefficients; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + if (defined(value) && (!isArray(value) || value.length !== 9)) { + throw new DeveloperError('sphericalHarmonicCoefficients must be an array of 9 Cartesian3 values.'); + } + //>>includeEnd('debug'); + this._sphericalHarmonicCoefficients = value; + this._shouldRegenerateShaders = true; + } + }, + + /** + * A URL to a KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. + * + * @memberof Model.prototype + * @demo {@link https://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Image-Based Lighting.html|Sandcastle Image Based Lighting Demo} + * @type {String} + * @see Model#sphericalHarmonicCoefficients + */ + specularEnvironmentMaps : { + get : function() { + return this._specularEnvironmentMaps; + }, + set : function(value) { + this._shouldUpdateSpecularMapAtlas = value !== this._specularEnvironmentMaps; + this._specularEnvironmentMaps = value; + } } }); @@ -2002,7 +2096,8 @@ define([ drawFS = 'uniform vec4 czm_pickColor;\n' + drawFS; } - if (model._imageBasedLightingFactor.x > 0.0 || model._imageBasedLightingFactor.y > 0.0) { + var useIBL = model._imageBasedLightingFactor.x > 0.0 || model._imageBasedLightingFactor.y > 0.0; + if (useIBL) { drawFS = '#define USE_IBL_LIGHTING \n\n' + drawFS; } @@ -2021,6 +2116,29 @@ define([ '} \n'; } + var usesSH = defined(model._sphericalHarmonicCoefficients) || model._useDefaultSphericalHarmonics; + var usesSM = (defined(model._specularEnvironmentMapAtlas) && model._specularEnvironmentMapAtlas.ready) || model._useDefaultSpecularMaps; + var addMatrix = usesSH || usesSM || useIBL; + if (addMatrix) { + drawFS = 'uniform mat4 gltf_clippingPlanesMatrix; \n' + drawFS; + } + + if (defined(model._sphericalHarmonicCoefficients)) { + drawFS = '#define DIFFUSE_IBL \n' + '#define CUSTOM_SPHERICAL_HARMONICS \n' + 'uniform vec3 gltf_sphericalHarmonicCoefficients[9]; \n' + drawFS; + } else if (model._useDefaultSphericalHarmonics) { + drawFS = '#define DIFFUSE_IBL \n' + drawFS; + } + + if (defined(model._specularEnvironmentMapAtlas) && model._specularEnvironmentMapAtlas.ready) { + drawFS = '#define SPECULAR_IBL \n' + '#define CUSTOM_SPECULAR_IBL \n' + 'uniform sampler2D gltf_specularMap; \n' + 'uniform vec2 gltf_specularMapSize; \n' + 'uniform float gltf_maxSpecularLOD; \n' + drawFS; + } else if (model._useDefaultSpecularMaps) { + drawFS = '#define SPECULAR_IBL \n' + drawFS; + } + + if (defined(model._luminanceAtZenith)) { + drawFS = '#define USE_SUN_LUMINANCE \n' + 'uniform float gltf_luminanceAtZenith;\n' + drawFS; + } + createAttributesAndProgram(programId, techniqueId, drawFS, drawVS, model, context); } @@ -2063,7 +2181,8 @@ define([ drawFS = 'uniform vec4 czm_pickColor;\n' + drawFS; } - if (model._imageBasedLightingFactor.x > 0.0 || model._imageBasedLightingFactor.y > 0.0) { + var useIBL = model._imageBasedLightingFactor.x > 0.0 || model._imageBasedLightingFactor.y > 0.0; + if (useIBL) { drawFS = '#define USE_IBL_LIGHTING \n\n' + drawFS; } @@ -2082,6 +2201,29 @@ define([ '} \n'; } + var usesSH = defined(model._sphericalHarmonicCoefficients) || model._useDefaultSphericalHarmonics; + var usesSM = (defined(model._specularEnvironmentMapAtlas) && model._specularEnvironmentMapAtlas.ready) || model._useDefaultSpecularMaps; + var addMatrix = !addClippingPlaneCode && (usesSH || usesSM || useIBL); + if (addMatrix) { + drawFS = 'uniform mat4 gltf_clippingPlanesMatrix; \n' + drawFS; + } + + if (defined(model._sphericalHarmonicCoefficients)) { + drawFS = '#define DIFFUSE_IBL \n' + '#define CUSTOM_SPHERICAL_HARMONICS \n' + 'uniform vec3 gltf_sphericalHarmonicCoefficients[9]; \n' + drawFS; + } else if (model._useDefaultSphericalHarmonics) { + drawFS = '#define DIFFUSE_IBL \n' + drawFS; + } + + if (defined(model._specularEnvironmentMapAtlas) && model._specularEnvironmentMapAtlas.ready) { + drawFS = '#define SPECULAR_IBL \n' + '#define CUSTOM_SPECULAR_IBL \n' + 'uniform sampler2D gltf_specularMap; \n' + 'uniform vec2 gltf_specularMapSize; \n' + 'uniform float gltf_maxSpecularLOD; \n' + drawFS; + } else if (model._useDefaultSpecularMaps) { + drawFS = '#define SPECULAR_IBL \n' + drawFS; + } + + if (defined(model._luminanceAtZenith)) { + drawFS = '#define USE_SUN_LUMINANCE \n' + 'uniform float gltf_luminanceAtZenith;\n' + drawFS; + } + createAttributesAndProgram(programId, techniqueId, drawFS, drawVS, model, context); } @@ -2917,10 +3059,11 @@ define([ function createClippingPlanesMatrixFunction(model) { return function() { var clippingPlanes = model.clippingPlanes; - if (!defined(clippingPlanes)) { + if (!defined(clippingPlanes) && !defined(model._sphericalHarmonicCoefficients) && !defined(model._specularEnvironmentMaps)) { return Matrix4.IDENTITY; } - return Matrix4.multiply(model._clippingPlaneModelViewMatrix, clippingPlanes.modelMatrix, scratchClippingPlaneMatrix); + var modelMatrix = defined(clippingPlanes) ? clippingPlanes.modelMatrix : Matrix4.IDENTITY; + return Matrix4.multiply(model._clippingPlaneModelViewMatrix, modelMatrix, scratchClippingPlaneMatrix); }; } @@ -2962,6 +3105,36 @@ define([ }; } + function createLuminanceAtZenithFunction(model) { + return function() { + return model.luminanceAtZenith; + }; + } + + function createSphericalHarmonicCoefficientsFunction(model) { + return function() { + return model._sphericalHarmonicCoefficients; + }; + } + + function createSpecularEnvironmentMapFunction(model) { + return function() { + return model._specularEnvironmentMapAtlas.texture; + }; + } + + function createSpecularEnvironmentMapSizeFunction(model) { + return function() { + return model._specularEnvironmentMapAtlas.texture.dimensions; + }; + } + + function createSpecularEnvironmentMapLOD(model) { + return function() { + return model._specularEnvironmentMapAtlas.maximumMipmapLevel; + }; + } + function triangleCountFromPrimitiveIndices(primitive, indicesCount) { switch (primitive.mode) { case PrimitiveType.TRIANGLES: @@ -3052,11 +3225,16 @@ define([ uniformMap = combine(uniformMap, { gltf_color : createColorFunction(model), gltf_colorBlend : createColorBlendFunction(model), - gltf_clippingPlanes: createClippingPlanesFunction(model), - gltf_clippingPlanesEdgeStyle: createClippingPlanesEdgeStyleFunction(model), - gltf_clippingPlanesMatrix: createClippingPlanesMatrixFunction(model), + gltf_clippingPlanes : createClippingPlanesFunction(model), + gltf_clippingPlanesEdgeStyle : createClippingPlanesEdgeStyleFunction(model), + gltf_clippingPlanesMatrix : createClippingPlanesMatrixFunction(model), gltf_iblFactor : createIBLFactorFunction(model), - gltf_lightColor : createLightColorFunction(model) + gltf_lightColor : createLightColorFunction(model), + gltf_sphericalHarmonicCoefficients : createSphericalHarmonicCoefficientsFunction(model), + gltf_specularMap : createSpecularEnvironmentMapFunction(model), + gltf_specularMapSize : createSpecularEnvironmentMapSizeFunction(model), + gltf_maxSpecularLOD : createSpecularEnvironmentMapLOD(model), + gltf_luminanceAtZenith : createLuminanceAtZenithFunction(model) }); // Allow callback to modify the uniformMap @@ -4299,6 +4477,37 @@ define([ } } + if (this._shouldUpdateSpecularMapAtlas) { + this._shouldUpdateSpecularMapAtlas = false; + this._specularEnvironmentMapAtlas = this._specularEnvironmentMapAtlas && this._specularEnvironmentMapAtlas.destroy(); + this._specularEnvironmentMapAtlas = undefined; + if (defined(this._specularEnvironmentMaps)) { + this._specularEnvironmentMapAtlas = new OctahedralProjectedCubeMap(this._specularEnvironmentMaps); + var that = this; + this._specularEnvironmentMapAtlas.readyPromise.then(function() { + that._shouldRegenerateShaders = true; + }); + } + + // Regenerate shaders to not use an environment map. Will be set to true again if there was a new environment map and it is ready. + this._shouldRegenerateShaders = true; + } + + if (defined(this._specularEnvironmentMapAtlas)) { + this._specularEnvironmentMapAtlas.update(frameState); + } + + var recompileWithDefaultAtlas = !defined(this._specularEnvironmentMapAtlas) && defined(frameState.specularEnvironmentMaps) && !this._useDefaultSpecularMaps; + var recompileWithoutDefaultAtlas = !defined(frameState.specularEnvironmentMaps) && this._useDefaultSpecularMaps; + + var recompileWithDefaultSHCoeffs = !defined(this._sphericalHarmonicCoefficients) && defined(frameState.sphericalHarmonicCoefficients) && !this._useDefaultSphericalHarmonics; + var recompileWithoutDefaultSHCoeffs = !defined(frameState.sphericalHarmonicCoefficients) && this._useDefaultSphericalHarmonics; + + this._shouldRegenerateShaders = this._shouldRegenerateShaders || recompileWithDefaultAtlas || recompileWithoutDefaultAtlas || recompileWithDefaultSHCoeffs || recompileWithoutDefaultSHCoeffs; + + this._useDefaultSpecularMaps = !defined(this._specularEnvironmentMapAtlas) && defined(frameState.specularEnvironmentMaps); + this._useDefaultSphericalHarmonics = !defined(this._sphericalHarmonicCoefficients) && defined(frameState.sphericalHarmonicCoefficients); + var silhouette = hasSilhouette(this, frameState); var translucent = isTranslucent(this); var invisible = isInvisible(this); @@ -4375,13 +4584,20 @@ define([ // Regenerate shaders if ClippingPlaneCollection state changed or it was removed var clippingPlanes = this._clippingPlanes; var currentClippingPlanesState = 0; - if (defined(clippingPlanes) && clippingPlanes.enabled && clippingPlanes.length > 0) { + var useClippingPlanes = defined(clippingPlanes) && clippingPlanes.enabled && clippingPlanes.length > 0; + var usesSH = defined(this._sphericalHarmonicCoefficients) || this._useDefaultSphericalHarmonics; + var usesSM = (defined(this._specularEnvironmentMapAtlas) && this._specularEnvironmentMapAtlas.ready) || this._useDefaultSpecularMaps; + if (useClippingPlanes || usesSH || usesSM) { var clippingPlanesOriginMatrix = defaultValue(this.clippingPlanesOriginMatrix, modelMatrix); Matrix4.multiply(context.uniformState.view3D, clippingPlanesOriginMatrix, this._clippingPlaneModelViewMatrix); + } + + if (useClippingPlanes) { currentClippingPlanesState = clippingPlanes.clippingPlanesState; } - var shouldRegenerateShaders = this._clippingPlanesState !== currentClippingPlanesState || this._regenerateShaders; + var shouldRegenerateShaders = this._shouldRegenerateShaders; + shouldRegenerateShaders = shouldRegenerateShaders || this._clippingPlanesState !== currentClippingPlanesState; this._clippingPlanesState = currentClippingPlanesState; // Regenerate shaders if color shading changed from last update @@ -4397,8 +4613,6 @@ define([ updateColor(this, frameState, false); updateSilhouette(this, frameState, false); } - - this._regenerateShaders = false; } if (justLoaded) { @@ -4492,7 +4706,9 @@ define([ destroyIfNotCached(rendererResources, cachedRendererResources); var programId; - if (isClippingEnabled(model) || isColorShadingEnabled(model) || model._regenerateShaders) { + if (isClippingEnabled(model) || isColorShadingEnabled(model) || model._shouldRegenerateShaders) { + model._shouldRegenerateShaders = false; + rendererResources.programs = {}; rendererResources.silhouettePrograms = {}; @@ -4611,6 +4827,8 @@ define([ } this._clippingPlanes = undefined; + this._specularEnvironmentMapAtlas = this._specularEnvironmentMapAtlas && this._specularEnvironmentMapAtlas.destroy(); + return destroyObject(this); }; diff --git a/Source/Scene/ModelInstanceCollection.js b/Source/Scene/ModelInstanceCollection.js index 93e895274cb0..0b3910226722 100644 --- a/Source/Scene/ModelInstanceCollection.js +++ b/Source/Scene/ModelInstanceCollection.js @@ -90,6 +90,11 @@ define([ * @param {Boolean} [options.asynchronous=true] Determines if model WebGL resource creation will be spread out over several frames or block until completion once all glTF files are loaded. * @param {Boolean} [options.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the model is loaded. * @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the collection casts or receives shadows from each light source. + * @param {Cartesian2} [options.imageBasedLightingFactor=new Cartesian2(1.0, 1.0)] Scales the diffuse and specular image-based lighting from the earth, sky, atmosphere and star skybox. + * @param {Cartesian3} [options.lightColor] The color and intensity of the sunlight used to shade models. + * @param {Number} [options.luminanceAtZenith=1.0] 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 KTX file that contains a cube map of the specular lighting and the convoluted specular mipmaps. * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Draws the bounding sphere for the collection. * @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the instances in wireframe. * @@ -174,6 +179,9 @@ define([ this._imageBasedLightingFactor = new Cartesian2(1.0, 1.0); Cartesian2.clone(options.imageBasedLightingFactor, this._imageBasedLightingFactor); this.lightColor = options.lightColor; + this.luminanceAtZenith = options.luminanceAtZenith; + this.sphericalHarmonicCoefficients = options.sphericalHarmonicCoefficients; + this.specularEnvironmentMaps = options.specularEnvironmentMaps; } defineProperties(ModelInstanceCollection.prototype, { @@ -601,7 +609,10 @@ define([ ignoreCommands : true, opaquePass : collection._opaquePass, imageBasedLightingFactor : collection.imageBasedLightingFactor, - lightColor : collection.lightColor + lightColor : collection.lightColor, + luminanceAtZenith : collection.luminanceAtZenith, + sphericalHarmonicCoefficients : collection.sphericalHarmonicCoefficients, + specularEnvironmentMaps : collection.specularEnvironmentMaps }; if (!usesBatchTable) { @@ -897,6 +908,9 @@ define([ model.imageBasedLightingFactor = this.imageBasedLightingFactor; model.lightColor = this.lightColor; + model.luminanceAtZenith = this.luminanceAtZenith; + model.sphericalHarmonicCoefficients = this.sphericalHarmonicCoefficients; + model.specularEnvironmentMaps = this.specularEnvironmentMaps; model.update(frameState); diff --git a/Source/Scene/OctahedralProjectedCubeMap.js b/Source/Scene/OctahedralProjectedCubeMap.js new file mode 100644 index 000000000000..61111e3300e2 --- /dev/null +++ b/Source/Scene/OctahedralProjectedCubeMap.js @@ -0,0 +1,416 @@ +define([ + '../Core/Cartesian3', + '../Core/ComponentDatatype', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/IndexDatatype', + '../Core/loadKTX', + '../Core/PixelFormat', + '../Renderer/Buffer', + '../Renderer/BufferUsage', + '../Renderer/ComputeCommand', + '../Renderer/CubeMap', + '../Renderer/PixelDatatype', + '../Renderer/ShaderProgram', + '../Renderer/Texture', + '../Renderer/VertexArray', + '../Shaders/OctahedralProjectionAtlasFS', + '../Shaders/OctahedralProjectionFS', + '../Shaders/OctahedralProjectionVS', + '../ThirdParty/when' + ], function( + Cartesian3, + ComponentDatatype, + defined, + defineProperties, + destroyObject, + IndexDatatype, + loadKTX, + PixelFormat, + Buffer, + BufferUsage, + ComputeCommand, + CubeMap, + PixelDatatype, + ShaderProgram, + Texture, + VertexArray, + OctahedralProjectionAtlasFS, + OctahedralProjectionFS, + OctahedralProjectionVS, + when) { + 'use strict'; + + /** + * Packs all mip levels of a cube map into a 2D texture atlas. + * + * Octahedral projection is a way of putting the cube maps onto a 2D texture + * with minimal distortion and easy look up. + * See Chapter 16 of WebGL Insights "HDR Image-Based Lighting on the Web" by Jeff Russell + * and "Octahedron Environment Maps" for reference. + * + * @private + */ + function OctahedralProjectedCubeMap(url) { + this._url = url; + + this._cubeMapBuffers = undefined; + this._cubeMaps = undefined; + this._texture = undefined; + this._mipTextures = undefined; + this._va = undefined; + this._sp = undefined; + + this._maximumMipmapLevel = undefined; + + this._loading = false; + this._ready = false; + this._readyPromise = when.defer(); + } + + defineProperties(OctahedralProjectedCubeMap.prototype, { + /** + * The url to the KTX file containing the specular environment map and convoluted mipmaps. + * @memberof OctahedralProjectedCubeMap.prototype + * @type {String} + * @readonly + */ + url : { + get : function() { + return this._url; + } + }, + /** + * A texture containing all the packed convolutions. + * @memberof OctahedralProjectedCubeMap.prototype + * @type {Texture} + * @readonly + */ + texture : { + get : function() { + return this._texture; + } + }, + /** + * The maximum number of mip levels. + * @memberOf OctahedralProjectedCubeMap.prototype + * @type {Number} + * @readonly + */ + maximumMipmapLevel : { + get : function() { + return this._maximumMipmapLevel; + } + }, + /** + * Determines if the texture atlas is complete and ready to use. + * @memberof OctahedralProjectedCubeMap.prototype + * @type {Boolean} + * @readonly + */ + ready : { + get : function() { + return this._ready; + } + }, + /** + * Gets a promise that resolves when the texture atlas is ready to use. + * @memberof OctahedralProjectedCubeMap.prototype + * @type {Promise} + * @readonly + */ + readyPromise : { + get : function() { + return this._readyPromise.promise; + } + } + }); + + OctahedralProjectedCubeMap.isSupported = function(context) { + return context.halfFloatingPointTexture || context.floatingPointTexture; + }; + + // These vertices are based on figure 1 from "Octahedron Environment Maps". + var v1 = new Cartesian3(1.0, 0.0, 0.0); + var v2 = new Cartesian3(0.0, 0.0, 1.0); + var v3 = new Cartesian3(-1.0, 0.0, 0.0); + var v4 = new Cartesian3(0.0, 0.0, -1.0); + var v5 = new Cartesian3(0.0, 1.0, 0.0); + var v6 = new Cartesian3(0.0, -1.0, 0.0); + + // top left, left, top, center, right, top right, bottom, bottom left, bottom right + var cubeMapCoordinates = [v5, v3, v2, v6, v1, v5, v4, v5, v5]; + var length = cubeMapCoordinates.length; + var flatCubeMapCoordinates = new Float32Array(length * 3); + + var offset = 0; + for (var i = 0; i < length; ++i, offset += 3) { + Cartesian3.pack(cubeMapCoordinates[i], flatCubeMapCoordinates, offset); + } + + var flatPositions = new Float32Array([ + -1.0, 1.0, // top left + -1.0, 0.0, // left + 0.0, 1.0, // top + 0.0, 0.0, // center + 1.0, 0.0, // right + 1.0, 1.0, // top right + 0.0, -1.0, // bottom + -1.0, -1.0, // bottom left + 1.0, -1.0 // bottom right + ]); + var indices = new Uint16Array([ + 0, 1, 2, // top left, left, top, + 2, 3, 1, // top, center, left, + 7, 6, 1, // bottom left, bottom, left, + 3, 6, 1, // center, bottom, left, + 2, 5, 4, // top, top right, right, + 3, 4, 2, // center, right, top, + 4, 8, 6, // right, bottom right, bottom, + 3, 4, 6 //center, right, bottom + ]); + + function createVertexArray(context) { + var positionBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : flatPositions, + usage : BufferUsage.STATIC_DRAW + }); + var cubeMapCoordinatesBuffer = Buffer.createVertexBuffer({ + context : context, + typedArray : flatCubeMapCoordinates, + usage : BufferUsage.STATIC_DRAW + }); + var indexBuffer = Buffer.createIndexBuffer({ + context : context, + typedArray : indices, + usage : BufferUsage.STATIC_DRAW, + indexDatatype : IndexDatatype.UNSIGNED_SHORT + }); + + var attributes = [{ + index : 0, + vertexBuffer : positionBuffer, + componentsPerAttribute : 2, + componentDatatype : ComponentDatatype.FLOAT + }, { + index : 1, + vertexBuffer : cubeMapCoordinatesBuffer, + componentsPerAttribute : 3, + componentDatatype : ComponentDatatype.FLOAT + }]; + return new VertexArray({ + context : context, + attributes : attributes, + indexBuffer : indexBuffer + }); + } + + function createUniformTexture(texture) { + return function() { + return texture; + }; + } + + function cleanupResources(map) { + map._va = map._va && map._va.destroy(); + map._sp = map._sp && map._sp.destroy(); + + var i; + var length; + + var cubeMaps = map._cubeMaps; + if (defined(cubeMaps)) { + length = cubeMaps.length; + for (i = 0; i < length; ++i) { + cubeMaps[i].destroy(); + } + } + var mipTextures = map._mipTextures; + if (defined(mipTextures)) { + length = mipTextures.length; + for (i = 0; i < length; ++i) { + mipTextures[i].destroy(); + } + } + + map._va = undefined; + map._sp = undefined; + map._cubeMaps = undefined; + map._cubeMapBuffers = undefined; + map._mipTextures = undefined; + } + + /** + * Creates compute commands to generate octahedral projections of each cube map + * and then renders them to an atlas. + *

+ * Only needs to be called twice. The first call queues the compute commands to generate the atlas. + * The second call cleans up unused resources. Every call afterwards is a no-op. + *

+ * + * @param {FrameState} frameState The frame state. + * + * @private + */ + OctahedralProjectedCubeMap.prototype.update = function(frameState) { + var context = frameState.context; + if (!context.halfFloatingPointTexture && !context.floatingPointTexture) { + return; + } + + if (defined(this._texture) && defined(this._va)) { + cleanupResources(this); + } + if (defined(this._texture)) { + return; + } + + if (!defined(this._texture) && !this._loading) { + var cachedTexture = context.textureCache.getTexture(this._url); + if (defined(cachedTexture)) { + cleanupResources(this); + this._texture = cachedTexture; + this._maximumMipmapLevel = this._texture.maximumMipmapLevel; + this._ready = true; + this._readyPromise.resolve(); + return; + } + } + + var cubeMapBuffers = this._cubeMapBuffers; + if (!defined(cubeMapBuffers) && !this._loading) { + var that = this; + loadKTX(this._url).then(function(buffers) { + that._cubeMapBuffers = buffers; + that._loading = false; + }); + this._loading = true; + } + if (!defined(this._cubeMapBuffers)) { + return; + } + + this._va = createVertexArray(context); + this._sp = ShaderProgram.fromCache({ + context : context, + vertexShaderSource : OctahedralProjectionVS, + fragmentShaderSource : OctahedralProjectionFS, + attributeLocations : { + position : 0, + cubeMapCoordinates : 1 + } + }); + + // We only need up to 6 mip levels to avoid artifacts. + var length = Math.min(cubeMapBuffers.length, 6); + this._maximumMipmapLevel = length - 1; + var cubeMaps = this._cubeMaps = new Array(length); + var mipTextures = this._mipTextures = new Array(length); + var originalSize = cubeMapBuffers[0].positiveX.width * 2.0; + var uniformMap = { + originalSize : function() { + return originalSize; + } + }; + + var pixelDatatype = context.halfFloatingPointTexture ? PixelDatatype.HALF_FLOAT : PixelDatatype.FLOAT; + var pixelFormat = PixelFormat.RGBA; + + // First we project each cubemap onto a flat octahedron, and write that to a texture. + for (var i = 0; i < length; ++i) { + // Swap +Y/-Y faces since the octahedral projection expects this order. + var positiveY = cubeMapBuffers[i].positiveY; + cubeMapBuffers[i].positiveY = cubeMapBuffers[i].negativeY; + cubeMapBuffers[i].negativeY = positiveY; + + var cubeMap = cubeMaps[i] = new CubeMap({ + context : context, + source : cubeMapBuffers[i] + }); + var size = cubeMaps[i].width * 2; + + var mipTexture = mipTextures[i] = new Texture({ + context : context, + width : size, + height : size, + pixelDatatype : pixelDatatype, + pixelFormat : pixelFormat + }); + + var command = new ComputeCommand({ + vertexArray : this._va, + shaderProgram : this._sp, + uniformMap : { + cubeMap : createUniformTexture(cubeMap) + }, + outputTexture : mipTexture, + persists : true, + owner : this + }); + frameState.commandList.push(command); + + uniformMap['texture' + i] = createUniformTexture(mipTexture); + } + + this._texture = new Texture({ + context : context, + width : originalSize * 1.5 + 2.0, // We add a 1 pixel border to avoid linear sampling artifacts. + height : originalSize, + pixelDatatype : pixelDatatype, + pixelFormat : pixelFormat + }); + + this._texture.maximumMipmapLevel = this._maximumMipmapLevel; + context.textureCache.addTexture(this._url, this._texture); + + var atlasCommand = new ComputeCommand({ + fragmentShaderSource : OctahedralProjectionAtlasFS, + uniformMap : uniformMap, + outputTexture : this._texture, + persists : false, + owner : this + }); + frameState.commandList.push(atlasCommand); + + this._ready = true; + this._readyPromise.resolve(); + }; + + /** + * 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 OctahedralProjectedCubeMap#destroy + */ + OctahedralProjectedCubeMap.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. + *

+ * + * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. + * + * @see OctahedralProjectedCubeMap#isDestroyed + */ + OctahedralProjectedCubeMap.prototype.destroy = function() { + cleanupResources(this); + this._texture = this._texture && this._texture.destroy(); + return destroyObject(this); + }; + + return OctahedralProjectedCubeMap; +}); diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index 4741c2d2ca3a..a5c09a86c60a 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -65,6 +65,7 @@ define([ './InvertClassification', './JobScheduler', './MapMode2D', + './OctahedralProjectedCubeMap', './PerformanceDisplay', './PerInstanceColorAppearance', './PickDepth', @@ -146,6 +147,7 @@ define([ InvertClassification, JobScheduler, MapMode2D, + OctahedralProjectedCubeMap, PerformanceDisplay, PerInstanceColorAppearance, PickDepth, @@ -795,6 +797,19 @@ define([ this.gamma = 2.2; this._sunColor = new Cartesian3(1.8, 1.85, 2.0); + /** + * The spherical harmonic coefficients for image-based lighting of PBR models. + * @type {Cartesian3[]} + */ + this.sphericalHarmonicCoefficients = undefined; + + /** + * The url to the KTX file containing the specular environment map and convoluted mipmaps for image-based lighting of PBR models. + * @type {String} + */ + this.specularEnvironmentMaps = undefined; + this._specularEnvironmentMapAtlas = undefined; + // Give frameState, camera, and screen space camera controller initial state before rendering updateFrameNumber(this, 0.0, JulianDate.now()); updateFrameState(this); @@ -1708,6 +1723,16 @@ define([ frameState.useLogDepth = scene._logDepthBuffer && !(scene.camera.frustum instanceof OrthographicFrustum || scene.camera.frustum instanceof OrthographicOffCenterFrustum); frameState.sunColor = scene._sunColor; + if (defined(scene._specularEnvironmentMapAtlas) && scene._specularEnvironmentMapAtlas.ready) { + frameState.specularEnvironmentMaps = scene._specularEnvironmentMapAtlas.texture; + frameState.specularEnvironmentMapsMaximumLOD = scene._specularEnvironmentMapAtlas.maximumMipmapLevel; + } else { + frameState.specularEnvironmentMaps = undefined; + frameState.specularEnvironmentMapsMaximumLOD = undefined; + } + + frameState.sphericalHarmonicCoefficients = scene.sphericalHarmonicCoefficients; + scene._actualInvertClassificationColor = Color.clone(scene.invertClassificationColor, scene._actualInvertClassificationColor); if (!InvertClassification.isTranslucencySupported(scene._context)) { scene._actualInvertClassificationColor.alpha = 1.0; @@ -2821,6 +2846,20 @@ define([ environmentState.isSkyAtmosphereVisible = defined(environmentState.skyAtmosphereCommand) && environmentState.isReadyForAtmosphere; environmentState.isSunVisible = scene.isVisible(environmentState.sunDrawCommand, cullingVolume, occluder); environmentState.isMoonVisible = scene.isVisible(environmentState.moonCommand, cullingVolume, occluder); + + var envMaps = scene.specularEnvironmentMaps; + var envMapAtlas = scene._specularEnvironmentMapAtlas; + if (defined(envMaps) && (!defined(envMapAtlas) || envMapAtlas.url !== envMaps)) { + envMapAtlas = envMapAtlas && envMapAtlas.destroy(); + scene._specularEnvironmentMapAtlas = new OctahedralProjectedCubeMap(envMaps); + } else if (!defined(envMaps) && defined(envMapAtlas)) { + envMapAtlas.destroy(); + scene._specularEnvironmentMapAtlas = undefined; + } + + if (defined(scene._specularEnvironmentMapAtlas)) { + scene._specularEnvironmentMapAtlas.update(frameState); + } } function updateDebugFrustumPlanes(scene) { @@ -3073,10 +3112,11 @@ define([ * @private */ Scene.prototype.initializeFrame = function() { - // Destroy released shaders once every 120 frames to avoid thrashing the cache + // Destroy released shaders and textures once every 120 frames to avoid thrashing the cache if (this._shaderFrameCount++ === 120) { this._shaderFrameCount = 0; this._context.shaderCache.destroyReleasedShaderPrograms(); + this._context.textureCache.destroyReleasedTextures(); } this._tweens.update(); diff --git a/Source/Scene/processPbrMaterials.js b/Source/Scene/processPbrMaterials.js index 22b975fea4d5..ca2f77f6943f 100644 --- a/Source/Scene/processPbrMaterials.js +++ b/Source/Scene/processPbrMaterials.js @@ -657,9 +657,11 @@ define([ fragmentShader += ' vec3 specularContribution = F * G * D / (4.0 * NdotL * NdotV);\n'; fragmentShader += ' vec3 color = NdotL * lightColor * (diffuseContribution + specularContribution);\n'; - fragmentShader += '#ifdef USE_IBL_LIGHTING \n'; - // Figure out if the reflection vector hits the ellipsoid + // Use the procedural IBL if there are no environment maps + fragmentShader += '#if defined(USE_IBL_LIGHTING) && !defined(DIFFUSE_IBL) && !defined(SPECULAR_IBL) \n'; + fragmentShader += ' vec3 r = normalize(czm_inverseViewRotation * normalize(reflect(v, n)));\n'; + // Figure out if the reflection vector hits the ellipsoid fragmentShader += ' czm_ellipsoid ellipsoid = czm_getWgs84EllipsoidEC();\n'; fragmentShader += ' float vertexRadius = length(v_positionWC);\n'; fragmentShader += ' float horizonDotNadir = 1.0 - min(1.0, ellipsoid.radii.x / vertexRadius);\n'; @@ -697,9 +699,62 @@ define([ fragmentShader += ' specularIrradiance = mix(specularIrradiance, belowHorizonColor, smoothstep(aroundHorizon, farBelowHorizon, reflectionDotNadir) * inverseRoughness);\n'; fragmentShader += ' specularIrradiance = mix(specularIrradiance, nadirColor, smoothstep(farBelowHorizon, 1.0, reflectionDotNadir) * inverseRoughness);\n'; + // Luminance model from page 40 of http://silviojemma.com/public/papers/lighting/spherical-harmonic-lighting.pdf + fragmentShader += '#ifdef USE_SUN_LUMINANCE \n'; + // Angle between sun and zenith + fragmentShader += ' float LdotZenith = clamp(dot(normalize(czm_inverseViewRotation * l), normalize(v_positionWC * -1.0)), 0.001, 1.0);\n'; + fragmentShader += ' float S = acos(LdotZenith);\n'; + // Angle between zenith and current pixel + fragmentShader += ' float NdotZenith = clamp(dot(normalize(czm_inverseViewRotation * n), normalize(v_positionWC * -1.0)), 0.001, 1.0);\n'; + // Angle between sun and current pixel + fragmentShader += ' float gamma = acos(NdotL);\n'; + fragmentShader += ' float numerator = ((0.91 + 10.0 * exp(-3.0 * gamma) + 0.45 * pow(NdotL, 2.0)) * (1.0 - exp(-0.32 / NdotZenith)));\n'; + fragmentShader += ' float denominator = (0.91 + 10.0 * exp(-3.0 * S) + 0.45 * pow(LdotZenith,2.0)) * (1.0 - exp(-0.32));\n'; + fragmentShader += ' float luminance = gltf_luminanceAtZenith * (numerator / denominator);\n'; + fragmentShader += '#endif \n'; + fragmentShader += ' vec2 brdfLut = texture2D(czm_brdfLut, vec2(NdotV, 1.0 - roughness)).rg;\n'; fragmentShader += ' vec3 IBLColor = (diffuseIrradiance * diffuseColor * gltf_iblFactor.x) + (specularIrradiance * SRGBtoLINEAR3(specularColor * brdfLut.x + brdfLut.y) * gltf_iblFactor.y);\n'; - fragmentShader += ' color += IBLColor;\n'; + + fragmentShader += '#ifdef USE_SUN_LUMINANCE \n'; + fragmentShader += ' color += IBLColor * luminance;\n'; + fragmentShader += '#else \n'; + fragmentShader += ' color += IBLColor; \n'; + fragmentShader += '#endif \n'; + + // Environment maps were provided, use them for IBL + fragmentShader += '#elif defined(DIFFUSE_IBL) || defined(SPECULAR_IBL) \n'; + + fragmentShader += ' mat3 fixedToENU = mat3(gltf_clippingPlanesMatrix[0][0], gltf_clippingPlanesMatrix[1][0], gltf_clippingPlanesMatrix[2][0], \n'; + fragmentShader += ' gltf_clippingPlanesMatrix[0][1], gltf_clippingPlanesMatrix[1][1], gltf_clippingPlanesMatrix[2][1], \n'; + fragmentShader += ' gltf_clippingPlanesMatrix[0][2], gltf_clippingPlanesMatrix[1][2], gltf_clippingPlanesMatrix[2][2]); \n'; + fragmentShader += ' const mat3 yUpToZUp = mat3(-1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0); \n'; + fragmentShader += ' vec3 cubeDir = normalize(yUpToZUp * fixedToENU * normalize(reflect(-v, n))); \n'; + + fragmentShader += '#ifdef DIFFUSE_IBL \n'; + fragmentShader += '#ifdef CUSTOM_SPHERICAL_HARMONICS \n'; + fragmentShader += ' vec3 diffuseIrradiance = czm_sphericalHarmonics(cubeDir, gltf_sphericalHarmonicCoefficients); \n'; + fragmentShader += '#else \n'; + fragmentShader += ' vec3 diffuseIrradiance = czm_sphericalHarmonics(cubeDir, czm_sphericalHarmonicCoefficients); \n'; + fragmentShader += '#endif \n'; + fragmentShader += '#else \n'; + fragmentShader += ' vec3 diffuseIrradiance = vec3(0.0); \n'; + fragmentShader += '#endif \n'; + + fragmentShader += '#ifdef SPECULAR_IBL \n'; + fragmentShader += ' vec2 brdfLut = texture2D(czm_brdfLut, vec2(NdotV, roughness)).rg;\n'; + fragmentShader += '#ifdef CUSTOM_SPECULAR_IBL \n'; + fragmentShader += ' vec3 specularIBL = czm_sampleOctahedralProjection(gltf_specularMap, gltf_specularMapSize, cubeDir, roughness * gltf_maxSpecularLOD, gltf_maxSpecularLOD);\n'; + fragmentShader += '#else \n'; + fragmentShader += ' vec3 specularIBL = czm_sampleOctahedralProjection(czm_specularEnvironmentMaps, czm_specularEnvironmentMapSize, cubeDir, roughness * czm_specularEnvironmentMapsMaximumLOD, czm_specularEnvironmentMapsMaximumLOD);\n'; + fragmentShader += '#endif \n'; + fragmentShader += ' specularIBL *= F * brdfLut.x + brdfLut.y;\n'; + fragmentShader += '#else \n'; + fragmentShader += ' vec3 specularIBL = vec3(0.0); \n'; + fragmentShader += '#endif \n'; + + fragmentShader += ' color += diffuseIrradiance * diffuseColor + specularColor * specularIBL;\n'; + fragmentShader += '#endif \n'; } else { fragmentShader += ' vec3 color = baseColor;\n'; diff --git a/Source/Shaders/Builtin/Functions/sampleOctahedralProjection.glsl b/Source/Shaders/Builtin/Functions/sampleOctahedralProjection.glsl new file mode 100644 index 000000000000..6c44a6e55d60 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/sampleOctahedralProjection.glsl @@ -0,0 +1,78 @@ +/** + * Samples the 4 neighboring pixels and return the weighted average. + * + * @private + */ +vec3 czm_sampleOctahedralProjectionWithFiltering(sampler2D projectedMap, vec2 textureSize, vec3 direction, float lod) +{ + direction /= dot(vec3(1.0), abs(direction)); + vec2 rev = abs(direction.zx) - vec2(1.0); + vec2 neg = vec2(direction.x < 0.0 ? rev.x : -rev.x, + direction.z < 0.0 ? rev.y : -rev.y); + vec2 uv = direction.y < 0.0 ? neg : direction.xz; + vec2 coord = 0.5 * uv + vec2(0.5); + vec2 pixel = 1.0 / textureSize; + + if (lod > 0.0) + { + // Each subseqeuent mip level is half the size + float scale = 1.0 / pow(2.0, lod); + float offset = ((textureSize.y + 1.0) / textureSize.x); + + coord.x *= offset; + coord *= scale; + + coord.x += offset + pixel.x; + coord.y += (1.0 - (1.0 / pow(2.0, lod - 1.0))) + pixel.y * (lod - 1.0) * 2.0; + } + else + { + coord.x *= (textureSize.y / textureSize.x); + } + + // Do bilinear filtering + #ifndef OES_texture_float_linear + vec3 color1 = texture2D(projectedMap, coord + vec2(0.0, pixel.y)).rgb; + vec3 color2 = texture2D(projectedMap, coord + vec2(pixel.x, 0.0)).rgb; + vec3 color3 = texture2D(projectedMap, coord + pixel).rgb; + vec3 color4 = texture2D(projectedMap, coord).rgb; + + vec2 texturePosition = coord * textureSize; + + float fu = fract(texturePosition.x); + float fv = fract(texturePosition.y); + + vec3 average1 = mix(color4, color2, fu); + vec3 average2 = mix(color1, color3, fu); + + vec3 color = mix(average1, average2, fv); + #else + vec3 color = texture2D(projectedMap, coord).rgb; + #endif + + return color; +} + + +/** + * Samples from a cube map that has been projected using an octahedral projection from the given direction. + * + * @name czm_sampleOctahedralProjection + * @glslFunction + * + * @param {sampler2D} projectedMap The texture with the octahedral projected cube map. + * @param {vec2} textureSize The width and height dimensions in pixels of the projected map. + * @param {vec3} direction The normalized direction used to sample the cube map. + * @param {float} lod The level of detail to sample. + * @param {float} maxLod The maximum level of detail. + * @returns {vec3} The color of the cube map at the direction. + */ +vec3 czm_sampleOctahedralProjection(sampler2D projectedMap, vec2 textureSize, vec3 direction, float lod, float maxLod) { + float currentLod = floor(lod + 0.5); + float nextLod = min(currentLod + 1.0, maxLod); + + vec3 colorCurrentLod = czm_sampleOctahedralProjectionWithFiltering(projectedMap, textureSize, direction, currentLod); + vec3 colorNextLod = czm_sampleOctahedralProjectionWithFiltering(projectedMap, textureSize, direction, nextLod); + + return mix(colorNextLod, colorCurrentLod, nextLod - lod); +} diff --git a/Source/Shaders/Builtin/Functions/sphericalHarmonics.glsl b/Source/Shaders/Builtin/Functions/sphericalHarmonics.glsl new file mode 100644 index 000000000000..21a2cf67b57b --- /dev/null +++ b/Source/Shaders/Builtin/Functions/sphericalHarmonics.glsl @@ -0,0 +1,41 @@ +/** + * Computes a color from the third order spherical harmonic coefficients and a normalized direction vector. + *

+ * The order of the coefficients is [L00, L1_1, L10, L11, L2_2, L2_1, L20, L21, L22]. + *

+ * + * @name czm_sphericalHarmonics + * @glslFunction + * + * @param {vec3} normal The normalized direction. + * @param {vec3[9]} coefficients The third order spherical harmonic coefficients. + * @returns {vec3} The color at the direction. + * + * @see https://graphics.stanford.edu/papers/envmap/envmap.pdf + */ +vec3 czm_sphericalHarmonics(vec3 normal, vec3 coefficients[9]) +{ + const float c1 = 0.429043; + const float c2 = 0.511664; + const float c3 = 0.743125; + const float c4 = 0.886227; + const float c5 = 0.247708; + + vec3 L00 = coefficients[0]; + vec3 L1_1 = coefficients[1]; + vec3 L10 = coefficients[2]; + vec3 L11 = coefficients[3]; + vec3 L2_2 = coefficients[4]; + vec3 L2_1 = coefficients[5]; + vec3 L20 = coefficients[6]; + vec3 L21 = coefficients[7]; + vec3 L22 = coefficients[8]; + + float x = normal.x; + float y = normal.y; + float z = normal.z; + + return c1 * L22 * (x * x - y * y) + c3 * L20 * z * z + c4 * L00 - c5 * L20 + + 2.0 * c1 * (L2_2 * x * y + L21 * x * z + L2_1 * y * z) + + 2.0 * c2 * (L11 * x + L1_1 * y + L10 * z); +} diff --git a/Source/Shaders/OctahedralProjectionAtlasFS.glsl b/Source/Shaders/OctahedralProjectionAtlasFS.glsl new file mode 100644 index 000000000000..36015ee95b19 --- /dev/null +++ b/Source/Shaders/OctahedralProjectionAtlasFS.glsl @@ -0,0 +1,89 @@ +varying vec2 v_textureCoordinates; + +uniform float originalSize; +uniform sampler2D texture0; +uniform sampler2D texture1; +uniform sampler2D texture2; +uniform sampler2D texture3; +uniform sampler2D texture4; +uniform sampler2D texture5; + +const float yMipLevel1 = 1.0 - (1.0 / pow(2.0, 1.0)); +const float yMipLevel2 = 1.0 - (1.0 / pow(2.0, 2.0)); +const float yMipLevel3 = 1.0 - (1.0 / pow(2.0, 3.0)); +const float yMipLevel4 = 1.0 - (1.0 / pow(2.0, 4.0)); + +void main() +{ + vec2 uv = v_textureCoordinates; + vec2 textureSize = vec2(originalSize * 1.5 + 2.0, originalSize); + vec2 pixel = 1.0 / textureSize; + + float mipLevel = 0.0; + + if (uv.x - pixel.x > (textureSize.y / textureSize.x)) + { + mipLevel = 1.0; + if (uv.y - pixel.y > yMipLevel1) + { + mipLevel = 2.0; + if (uv.y - pixel.y * 3.0 > yMipLevel2) + { + mipLevel = 3.0; + if (uv.y - pixel.y * 5.0 > yMipLevel3) + { + mipLevel = 4.0; + if (uv.y - pixel.y * 7.0 > yMipLevel4) + { + mipLevel = 5.0; + } + } + } + } + } + + if (mipLevel > 0.0) + { + float scale = pow(2.0, mipLevel); + + uv.y -= (pixel.y * (mipLevel - 1.0) * 2.0); + uv.x *= ((textureSize.x - 2.0) / textureSize.y); + + uv.x -= 1.0 + pixel.x; + uv.y -= (1.0 - (1.0 / pow(2.0, mipLevel - 1.0))); + uv *= scale; + } + else + { + uv.x *= (textureSize.x / textureSize.y); + } + + if(mipLevel == 0.0) + { + gl_FragColor = texture2D(texture0, uv); + } + else if(mipLevel == 1.0) + { + gl_FragColor = texture2D(texture1, uv); + } + else if(mipLevel == 2.0) + { + gl_FragColor = texture2D(texture2, uv); + } + else if(mipLevel == 3.0) + { + gl_FragColor = texture2D(texture3, uv); + } + else if(mipLevel == 4.0) + { + gl_FragColor = texture2D(texture4, uv); + } + else if(mipLevel == 5.0) + { + gl_FragColor = texture2D(texture5, uv); + } + else + { + gl_FragColor = vec4(0.0); + } +} diff --git a/Source/Shaders/OctahedralProjectionFS.glsl b/Source/Shaders/OctahedralProjectionFS.glsl new file mode 100644 index 000000000000..4b75a2ec45e9 --- /dev/null +++ b/Source/Shaders/OctahedralProjectionFS.glsl @@ -0,0 +1,10 @@ +varying vec3 v_cubeMapCoordinates; +uniform samplerCube cubeMap; + +void main() +{ + vec4 rgbm = textureCube(cubeMap, v_cubeMapCoordinates); + float m = rgbm.a * 16.0; + vec3 r = rgbm.rgb * m; + gl_FragColor = vec4(r * r, 1.0); +} diff --git a/Source/Shaders/OctahedralProjectionVS.glsl b/Source/Shaders/OctahedralProjectionVS.glsl new file mode 100644 index 000000000000..67bdbb22a0c6 --- /dev/null +++ b/Source/Shaders/OctahedralProjectionVS.glsl @@ -0,0 +1,10 @@ +attribute vec4 position; +attribute vec3 cubeMapCoordinates; + +varying vec3 v_cubeMapCoordinates; + +void main() +{ + gl_Position = position; + v_cubeMapCoordinates = cubeMapCoordinates; +} diff --git a/Specs/Core/loadKTXSpec.js b/Specs/Core/loadKTXSpec.js index e20a1f91dbf7..6f1a6de7d37e 100644 --- a/Specs/Core/loadKTXSpec.js +++ b/Specs/Core/loadKTXSpec.js @@ -161,7 +161,7 @@ defineSuite([ expect(rejectedError).toBeUndefined(); }); - it('returns a promise that resolves to an uncompressed texture containing the first mip level of the original texture', function() { + it('returns a promise that resolves to an uncompressed texture containing all mip levels of the original texture', function() { var testUrl = 'http://example.invalid/testuri'; var promise = loadKTX(testUrl); @@ -181,10 +181,11 @@ defineSuite([ var response = validUncompressedMipmap.buffer; fakeXHR.simulateLoad(response); expect(resolvedValue).toBeDefined(); - expect(resolvedValue.width).toEqual(4); - expect(resolvedValue.height).toEqual(4); - expect(PixelFormat.isCompressedFormat(resolvedValue.internalFormat)).toEqual(false); - expect(resolvedValue.bufferView).toBeDefined(); + expect(resolvedValue.length).toEqual(3); + expect(resolvedValue[0].width).toEqual(4); + expect(resolvedValue[0].height).toEqual(4); + expect(PixelFormat.isCompressedFormat(resolvedValue[0].internalFormat)).toEqual(false); + expect(resolvedValue[0].bufferView).toBeDefined(); expect(rejectedError).toBeUndefined(); }); @@ -215,7 +216,7 @@ defineSuite([ expect(rejectedError).toBeUndefined(); }); - it('returns a promise that resolves to a compressed texture containing the first mip level of the original texture', function() { + it('returns a promise that resolves to a compressed texture containing the all mip levels of the original texture', function() { var testUrl = 'http://example.invalid/testuri'; var promise = loadKTX(testUrl); @@ -235,10 +236,11 @@ defineSuite([ var response = validCompressedMipmap.buffer; fakeXHR.simulateLoad(response); expect(resolvedValue).toBeDefined(); - expect(resolvedValue.width).toEqual(4); - expect(resolvedValue.height).toEqual(4); - expect(PixelFormat.isCompressedFormat(resolvedValue.internalFormat)).toEqual(true); - expect(resolvedValue.bufferView).toBeDefined(); + expect(resolvedValue.length).toEqual(3); + expect(resolvedValue[0].width).toEqual(4); + expect(resolvedValue[0].height).toEqual(4); + expect(PixelFormat.isCompressedFormat(resolvedValue[0].internalFormat)).toEqual(true); + expect(resolvedValue[0].bufferView).toBeDefined(); expect(rejectedError).toBeUndefined(); }); @@ -401,24 +403,16 @@ defineSuite([ expect(rejectedError.message).toEqual('Texture arrays are unsupported.'); }); - it('cubemaps are unsupported', function() { + it('cubemaps are supported', function() { var reinterprestBuffer = new Uint32Array(validUncompressed.buffer); - var invalidKTX = new Uint32Array(reinterprestBuffer); - invalidKTX[13] = 6; + var cubemapKTX = new Uint32Array(reinterprestBuffer); + cubemapKTX[13] = 6; - var promise = loadKTX(invalidKTX.buffer); + var promise = loadKTX(cubemapKTX.buffer); - var resolvedValue; - var rejectedError; promise.then(function(value) { - resolvedValue = value; - }, function(error) { - rejectedError = error; + expect(value).toBeDefined(); }); - - expect(resolvedValue).toBeUndefined(); - expect(rejectedError instanceof RuntimeError).toEqual(true); - expect(rejectedError.message).toEqual('Cubemaps are unsupported.'); }); it('returns undefined if the request is throttled', function() { diff --git a/Specs/Data/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx b/Specs/Data/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx new file mode 100644 index 000000000000..7a8880d04115 Binary files /dev/null and b/Specs/Data/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx differ diff --git a/Specs/Renderer/TextureCacheSpec.js b/Specs/Renderer/TextureCacheSpec.js new file mode 100644 index 000000000000..178b0298c284 --- /dev/null +++ b/Specs/Renderer/TextureCacheSpec.js @@ -0,0 +1,126 @@ +defineSuite([ + 'Renderer/TextureCache', + 'Renderer/Texture', + 'Specs/createContext' + ], function( + TextureCache, + Texture, + createContext) { + 'use strict'; + + var context; + + beforeAll(function() { + context = createContext(); + }); + + afterAll(function() { + context.destroyForSpecs(); + }); + + it('adds and removes', function() { + var cache = new TextureCache(); + + var keyword = 'texture'; + var texture = new Texture({ + context : context, + width : 1.0, + height : 1.0 + }); + + cache.addTexture(keyword, texture); + + expect(cache._textures[keyword].count).toEqual(1); + expect(cache.numberOfTextures).toEqual(1); + + texture.destroy(); + expect(texture.isDestroyed()).toEqual(false); + expect(cache.numberOfTextures).toEqual(1); + + cache.destroyReleasedTextures(); + expect(texture.isDestroyed()).toEqual(true); + expect(cache.numberOfTextures).toEqual(0); + + cache.destroy(); + }); + + it('has a cache hit', function() { + var cache = new TextureCache(context); + + var keyword = 'texture'; + var texture = new Texture({ + context : context, + width : 1.0, + height : 1.0 + }); + + cache.addTexture(keyword, texture); + + var texture2 = cache.getTexture(keyword); + expect(texture2).toBeDefined(); + expect(texture).toBe(texture2); + expect(cache._textures[keyword].count).toEqual(2); + expect(cache.numberOfTextures).toEqual(1); + + texture.destroy(); + texture2.destroy(); + cache.destroyReleasedTextures(); + + expect(texture.isDestroyed()).toEqual(true); + expect(cache.numberOfTextures).toEqual(0); + + cache.destroy(); + }); + + it('avoids thrashing', function() { + var cache = new TextureCache(); + + var keyword = 'texture'; + var texture = new Texture({ + context : context, + width : 1.0, + height : 1.0 + }); + + cache.addTexture(keyword, texture); + + texture.destroy(); + + var texture2 = cache.getTexture(keyword); // still a cache hit + + cache.destroyReleasedTextures(); // does not destroy + expect(texture.isDestroyed()).toEqual(false); + expect(texture2.isDestroyed()).toEqual(false); + + texture2.destroy(); + cache.destroyReleasedTextures(); // destroys + + expect(texture.isDestroyed()).toEqual(true); + expect(texture2.isDestroyed()).toEqual(true); + + cache.destroy(); + }); + + it('is destroyed', function() { + var cache = new TextureCache(); + + var keyword = 'texture'; + var texture = new Texture({ + context : context, + width : 1.0, + height : 1.0 + }); + + cache.addTexture(keyword, texture); + + cache.destroy(); + + expect(texture.isDestroyed()).toEqual(true); + expect(cache.isDestroyed()).toEqual(true); + }); + + it('is not destroyed', function() { + var cache = new TextureCache(); + expect(cache.isDestroyed()).toEqual(false); + }); +}, 'WebGL'); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 0d35f38afcb5..b93a4bac2617 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -2234,6 +2234,8 @@ defineSuite([ function testColorBlendMode(url) { return Cesium3DTilesTester.loadTileset(scene, url).then(function(tileset) { + tileset.luminanceAtZenith = undefined; + // Check that the feature is red var sourceRed; var renderOptions = { diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js index 2d50cd0887b9..30972eef1f4e 100644 --- a/Specs/Scene/ModelSpec.js +++ b/Specs/Scene/ModelSpec.js @@ -622,6 +622,55 @@ defineSuite([ texturedBoxModel.distanceDisplayCondition = undefined; }); + it('renders with spherical harmonics', function() { + if (!scene.highDynamicRangeSupported) { + return; + } + + return loadModel(boomBoxUrl).then(function(m) { + m.scale = 20.0; // Source model is very small, so scale up a bit + + var L00 = new Cartesian3( 0.692622075009195, 0.454351600181900, 0.369101722992350); // L00, irradiance, pre-scaled base + var L1_1 = new Cartesian3( 0.289407068366422, 0.167893101626580, 0.106174907004792); // L1-1, irradiance, pre-scaled base + var L10 = new Cartesian3(-0.591502034778913, -0.281524323171190, 0.124647554708491); // L10, irradiance, pre-scaled base + var L11 = new Cartesian3( 0.349454581171260, 0.163273486841657, -0.030956435452070); // L11, irradiance, pre-scaled base + var L2_2 = new Cartesian3( 0.221711764474260, 0.117719918681220, 0.031381053430064); // L2-2, irradiance, pre-scaled base + var L2_1 = new Cartesian3(-0.348955284677868, -0.187256994042823, -0.026299717727617); // L2-1, irradiance, pre-scaled base + var L20 = new Cartesian3( 0.119982671127227, 0.076784552175028, 0.055517838847755); // L20, irradiance, pre-scaled base + var L21 = new Cartesian3(-0.545546043202299, -0.279787444030397, -0.086854000285261); // L21, irradiance, pre-scaled base + var L22 = new Cartesian3( 0.160417569726332, 0.120896423762313, 0.121102528320197); // L22, irradiance, pre-scaled base + m.sphericalHarmonicCoefficients = [L00, L1_1, L10, L11, L2_2, L2_1, L20, L21, L22]; + + scene.highDynamicRange = true; + verifyRender(m); + primitives.remove(m); + scene.highDynamicRange = false; + }); + }); + + it('renders with specular environment map', function() { + if (!scene.highDynamicRangeSupported) { + return; + } + + return loadModel(boomBoxUrl).then(function(m) { + m.scale = 20.0; // Source model is very small, so scale up a bit + m.specularEnvironmentMaps = './Data/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx'; + + return pollToPromise(function() { + scene.highDynamicRange = true; + scene.render(); + scene.highDynamicRange = false; + return defined(m._specularEnvironmentMapAtlas) && m._specularEnvironmentMapAtlas.ready; + }).then(function() { + scene.highDynamicRange = true; + verifyRender(m); + primitives.remove(m); + scene.highDynamicRange = false; + }); + }); + }); + it('distanceDisplayCondition throws when ner >= far', function() { expect(function() { texturedBoxModel.distanceDisplayCondition = new DistanceDisplayCondition(100.0, 10.0); @@ -947,6 +996,7 @@ defineSuite([ return loadModel(boxGltf2Url).then(function(m) { verifyRender(m); m.show = true; + m.luminanceAtZenith = undefined; expect({ scene : scene, diff --git a/Specs/Scene/OctahedralProjectedCubeMapSpec.js b/Specs/Scene/OctahedralProjectedCubeMapSpec.js new file mode 100644 index 000000000000..296d3748fb80 --- /dev/null +++ b/Specs/Scene/OctahedralProjectedCubeMapSpec.js @@ -0,0 +1,201 @@ +defineSuite([ + 'Scene/OctahedralProjectedCubeMap', + 'Core/Cartesian3', + 'Renderer/ComputeEngine', + 'Renderer/Pass', + 'Specs/createContext', + 'Specs/createFrameState', + 'Specs/pollToPromise' + ], function( + OctahedralProjectedCubeMap, + Cartesian3, + ComputeEngine, + Pass, + createContext, + createFrameState, + pollToPromise) { + 'use strict'; + + var context; + var computeEngine; + var octahedralMap; + + var environmentMapUrl = './Data/EnvironmentMap/kiara_6_afternoon_2k_ibl.ktx'; + var fsOctahedralMap = + 'uniform sampler2D projectedMap;' + + 'uniform vec2 textureSize;' + + 'uniform vec3 direction;' + + 'uniform float lod;' + + 'uniform float maxLod;' + + 'void main() {' + + ' vec3 color = czm_sampleOctahedralProjection(projectedMap, textureSize, direction, lod, maxLod);' + + ' gl_FragColor = vec4(color, 1.0);' + + '}'; + + var fsCubeMap = + 'uniform samplerCube cubeMap;' + + 'uniform vec3 direction;' + + 'void main() {' + + ' vec4 rgbm = textureCube(cubeMap, direction);' + + ' float m = rgbm.a * 16.0;' + + ' vec3 r = rgbm.rgb * m;' + + ' gl_FragColor = vec4(r * r, 1.0);' + + '}'; + + beforeAll(function() { + context = createContext(); + computeEngine = new ComputeEngine(context); + }); + + afterAll(function() { + context.destroyForSpecs(); + computeEngine.destroy(); + }); + + afterEach(function() { + octahedralMap = octahedralMap && octahedralMap.destroy(); + context.textureCache.destroyReleasedTextures(); + }); + + function executeCommands(frameState) { + var length = frameState.commandList.length; + for (var i = 0; i < length; ++i) { + var command = frameState.commandList[i]; + if (command.pass === Pass.COMPUTE) { + command.execute(computeEngine); + } else { + command.execute(context); + } + } + frameState.commandList.length = 0; + } + + function sampleOctahedralMap(octahedralMap, direction, lod, callback) { + expect({ + context : context, + fragmentShader : fsOctahedralMap, + uniformMap : { + projectedMap : function() { + return octahedralMap.texture; + }, + textureSize : function() { + return octahedralMap.texture.dimensions; + }, + direction : function() { + return direction; + }, + lod : function() { + return lod; + }, + maxLod : function() { + return octahedralMap.maximumMipmapLevel; + } + } + }).contextToRenderAndCall(callback); + } + + function sampleCubeMap(cubeMap, direction, callback) { + expect({ + context : context, + fragmentShader : fsCubeMap, + uniformMap : { + cubeMap : function() { + return cubeMap; + }, + direction : function() { + return direction; + } + } + }).contextToRenderAndCall(callback); + } + + function expectCubeMapAndOctahedralMapEqual(octahedralMap, direction, lod) { + return sampleCubeMap(octahedralMap._cubeMaps[lod], direction, function(cubeMapColor) { + var directionFlipY = direction.clone(); + directionFlipY.y *= -1; + + sampleOctahedralMap(octahedralMap, directionFlipY, lod, function(octahedralMapColor) { + return expect(cubeMapColor).toEqualEpsilon(octahedralMapColor, 5); + }); + }); + } + + it('creates a packed texture with the right dimensions', function() { + if (!OctahedralProjectedCubeMap.isSupported(context)) { + return; + } + + octahedralMap = new OctahedralProjectedCubeMap(environmentMapUrl); + var frameState = createFrameState(context); + + return pollToPromise(function() { + octahedralMap.update(frameState); + return octahedralMap.ready; + }).then(function() { + expect(octahedralMap.texture.width).toEqual(770); + expect(octahedralMap.texture.height).toEqual(512); + expect(octahedralMap.maximumMipmapLevel).toEqual(5); + }); + }); + + it('correctly projects the given cube map and all mip levels', function() { + if (!OctahedralProjectedCubeMap.isSupported(context)) { + return; + } + + octahedralMap = new OctahedralProjectedCubeMap(environmentMapUrl); + var frameState = createFrameState(context); + + return pollToPromise(function() { + // We manually call update and execute the commands + // because calling scene.renderForSpecs does not + // actually execute these commands, and we need + // to get the output of the texture. + octahedralMap.update(frameState); + executeCommands(frameState); + + return octahedralMap.ready; + }).then(function() { + var directions = { + positiveX : new Cartesian3(1, 0, 0), + negativeX : new Cartesian3(-1, 0, 0), + positiveY : new Cartesian3(0, 1, 0), + negativeY : new Cartesian3(0, -1, 0), + positiveZ : new Cartesian3(0, 0, 1), + negativeZ : new Cartesian3(0, 0, -1) + }; + + for (var mipLevel = 0; mipLevel < octahedralMap.maximumMipmapLevel; mipLevel++) { + for (var key in directions) { + if (directions.hasOwnProperty(key)) { + var direction = directions[key]; + + expectCubeMapAndOctahedralMapEqual(octahedralMap, direction, mipLevel); + } + } + } + }); + }); + + it('caches projected textures', function() { + if (!OctahedralProjectedCubeMap.isSupported(context)) { + return; + } + + var projection = new OctahedralProjectedCubeMap(environmentMapUrl); + var frameState = createFrameState(context); + + return pollToPromise(function() { + projection.update(frameState); + return projection.ready; + }).then(function() { + var projection2 = new OctahedralProjectedCubeMap(environmentMapUrl); + projection2.update(frameState); + expect(projection2.ready).toEqual(true); + expect(projection.texture).toEqual(projection2.texture); + projection2.destroy(); + }).always(function() { + projection.destroy(); + }); + }); +}, 'WebGL'); diff --git a/Specs/addDefaultMatchers.js b/Specs/addDefaultMatchers.js index 366209b00caf..3a9f3b46db34 100644 --- a/Specs/addDefaultMatchers.js +++ b/Specs/addDefaultMatchers.js @@ -540,6 +540,26 @@ define([ }; }, + contextToRenderAndCall : function(util, customEqualityTesters) { + return { + compare : function(actual, expected) { + var actualRgba = contextRenderAndReadPixels(actual).color; + + var webglStub = !!window.webglStub; + if (!webglStub) { + // The callback may have expectations that fail, which still makes the + // spec fail, as we desired, even though this matcher sets pass to true. + var callback = expected; + callback(actualRgba); + } + + return { + pass : true + }; + } + }; + }, + contextToRender : function(util, customEqualityTesters) { return { compare : function(actual, expected) { @@ -701,8 +721,7 @@ define([ }; } - function expectContextToRender(actual, expected, expectEqual) { - var options = actual; + function contextRenderAndReadPixels(options) { var context = options.context; var vs = options.vertexShader; var fs = options.fragmentShader; @@ -711,11 +730,7 @@ define([ var modelMatrix = options.modelMatrix; var depth = defaultValue(options.depth, 0.0); var clear = defaultValue(options.clear, true); - var epsilon = defaultValue(options.epsilon, 0); - - if (!defined(expected)) { - expected = [255, 255, 255, 255]; - } + var clearColor; if (!defined(context)) { throw new DeveloperError('options.context is required.'); @@ -760,12 +775,47 @@ define([ }] }); - var webglStub = !!window.webglStub; - if (clear) { ClearCommand.ALL.execute(context); + clearColor = context.readPixels(); + } - var clearedRgba = context.readPixels(); + var command = new DrawCommand({ + primitiveType : PrimitiveType.POINTS, + shaderProgram : sp, + vertexArray : va, + uniformMap : uniformMap, + modelMatrix : modelMatrix + }); + + command.execute(context); + var rgba = context.readPixels(); + + sp = sp.destroy(); + va = va.destroy(); + + return { + color : rgba, + clearColor : clearColor + }; + } + + function expectContextToRender(actual, expected, expectEqual) { + var options = actual; + var context = options.context; + var clear = defaultValue(options.clear, true); + var epsilon = defaultValue(options.epsilon, 0); + + if (!defined(expected)) { + expected = [255, 255, 255, 255]; + } + + var webglStub = !!window.webglStub; + + var output = contextRenderAndReadPixels(options); + + if (clear) { + var clearedRgba = output.clearColor; if (!webglStub) { var expectedAlpha = context.options.webgl.alpha ? 0 : 255; if ((clearedRgba[0] !== 0) || @@ -780,15 +830,8 @@ define([ } } - var command = new DrawCommand({ - primitiveType : PrimitiveType.POINTS, - shaderProgram : sp, - vertexArray : va, - uniformMap : uniformMap, - modelMatrix : modelMatrix - }); - command.execute(context); - var rgba = context.readPixels(); + var rgba = output.color; + if (!webglStub) { if (expectEqual) { if (!CesiumMath.equalsEpsilon(rgba[0], expected[0], 0, epsilon) || @@ -811,9 +854,6 @@ define([ } } - sp = sp.destroy(); - va = va.destroy(); - return { pass : true };