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 @@ + + +
+ + + + + +Luminance at Zenith | ++ + + | +
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
+ *
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
+ *
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.
+ *
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.
+ *
+ * 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 };