import Cartesian4 from "../Core/Cartesian4.js"; import CesiumMath from "../Core/Math.js"; import Check from "../Core/Check.js"; import Color from "../Core/Color.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import mergeSort from "../Core/mergeSort.js"; import PixelFormat from "../Core/PixelFormat.js"; import PixelDatatype from "../Renderer/PixelDatatype.js"; import Sampler from "../Renderer/Sampler.js"; import Texture from "../Renderer/Texture.js"; import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js"; import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; import TextureWrap from "../Renderer/TextureWrap.js"; import Material from "./Material.js"; var scratchColor = new Color(); var scratchColorAbove = new Color(); var scratchColorBelow = new Color(); var scratchColorBlend = new Color(); var scratchPackedFloat = new Cartesian4(); var scratchColorBytes = new Uint8Array(4); function lerpEntryColor(height, entryBefore, entryAfter, result) { var lerpFactor = entryBefore.height === entryAfter.height ? 0.0 : (height - entryBefore.height) / (entryAfter.height - entryBefore.height); return Color.lerp(entryBefore.color, entryAfter.color, lerpFactor, result); } function createNewEntry(height, color) { return { height: height, color: Color.clone(color), }; } function removeDuplicates(entries) { // This function expects entries to be sorted from lowest to highest. // Remove entries that have the same height as before and after. entries = entries.filter(function (entry, index, array) { var hasPrev = index > 0; var hasNext = index < array.length - 1; var sameHeightAsPrev = hasPrev ? entry.height === array[index - 1].height : true; var sameHeightAsNext = hasNext ? entry.height === array[index + 1].height : true; var keep = !sameHeightAsPrev || !sameHeightAsNext; return keep; }); // Remove entries that have the same color as before and after. entries = entries.filter(function (entry, index, array) { var hasPrev = index > 0; var hasNext = index < array.length - 1; var sameColorAsPrev = hasPrev ? Color.equals(entry.color, array[index - 1].color) : false; var sameColorAsNext = hasNext ? Color.equals(entry.color, array[index + 1].color) : false; var keep = !sameColorAsPrev || !sameColorAsNext; return keep; }); // Also remove entries that have the same height AND color as the entry before. entries = entries.filter(function (entry, index, array) { var hasPrev = index > 0; var sameColorAsPrev = hasPrev ? Color.equals(entry.color, array[index - 1].color) : false; var sameHeightAsPrev = hasPrev ? entry.height === array[index - 1].height : true; var keep = !sameColorAsPrev || !sameHeightAsPrev; return keep; }); return entries; } function preprocess(layers) { var i, j; var layeredEntries = []; var layersLength = layers.length; for (i = 0; i < layersLength; i++) { var layer = layers[i]; var entriesOrig = layer.entries; var entriesLength = entriesOrig.length; //>>includeStart('debug', pragmas.debug); if (!Array.isArray(entriesOrig) || entriesLength === 0) { throw new DeveloperError("entries must be an array with size > 0."); } //>>includeEnd('debug'); var entries = []; for (j = 0; j < entriesLength; j++) { var entryOrig = entriesOrig[j]; //>>includeStart('debug', pragmas.debug); if (!defined(entryOrig.height)) { throw new DeveloperError("entry requires a height."); } if (!defined(entryOrig.color)) { throw new DeveloperError("entry requires a color."); } //>>includeEnd('debug'); var height = CesiumMath.clamp( entryOrig.height, createElevationBandMaterial._minimumHeight, createElevationBandMaterial._maximumHeight ); // premultiplied alpha var color = Color.clone(entryOrig.color, scratchColor); color.red *= color.alpha; color.green *= color.alpha; color.blue *= color.alpha; entries.push(createNewEntry(height, color)); } var sortedAscending = true; var sortedDescending = true; for (j = 0; j < entriesLength - 1; j++) { var currEntry = entries[j + 0]; var nextEntry = entries[j + 1]; sortedAscending = sortedAscending && currEntry.height <= nextEntry.height; sortedDescending = sortedDescending && currEntry.height >= nextEntry.height; } // When the array is fully descending, reverse it. if (sortedDescending) { entries = entries.reverse(); } else if (!sortedAscending) { // Stable sort from lowest to greatest height. mergeSort(entries, function (a, b) { return CesiumMath.sign(a.height - b.height); }); } var extendDownwards = defaultValue(layer.extendDownwards, false); var extendUpwards = defaultValue(layer.extendUpwards, false); // Interpret a single entry to extend all the way up and down. if (entries.length === 1 && !extendDownwards && !extendUpwards) { extendDownwards = true; extendUpwards = true; } if (extendDownwards) { entries.splice( 0, 0, createNewEntry( createElevationBandMaterial._minimumHeight, entries[0].color ) ); } if (extendUpwards) { entries.splice( entries.length, 0, createNewEntry( createElevationBandMaterial._maximumHeight, entries[entries.length - 1].color ) ); } entries = removeDuplicates(entries); layeredEntries.push(entries); } return layeredEntries; } function createLayeredEntries(layers) { // clean up the input data and check for errors var layeredEntries = preprocess(layers); var entriesAccumNext = []; var entriesAccumCurr = []; var i; function addEntry(height, color) { entriesAccumNext.push(createNewEntry(height, color)); } function addBlendEntry(height, a, b) { var result = Color.multiplyByScalar(b, 1.0 - a.alpha, scratchColorBlend); result = Color.add(result, a, result); addEntry(height, result); } // alpha blend new layers on top of old ones var layerLength = layeredEntries.length; for (i = 0; i < layerLength; i++) { var entries = layeredEntries[i]; var idx = 0; var accumIdx = 0; // swap the arrays entriesAccumCurr = entriesAccumNext; entriesAccumNext = []; var entriesLength = entries.length; var entriesAccumLength = entriesAccumCurr.length; while (idx < entriesLength || accumIdx < entriesAccumLength) { var entry = idx < entriesLength ? entries[idx] : undefined; var prevEntry = idx > 0 ? entries[idx - 1] : undefined; var nextEntry = idx < entriesLength - 1 ? entries[idx + 1] : undefined; var entryAccum = accumIdx < entriesAccumLength ? entriesAccumCurr[accumIdx] : undefined; var prevEntryAccum = accumIdx > 0 ? entriesAccumCurr[accumIdx - 1] : undefined; var nextEntryAccum = accumIdx < entriesAccumLength - 1 ? entriesAccumCurr[accumIdx + 1] : undefined; if ( defined(entry) && defined(entryAccum) && entry.height === entryAccum.height ) { // New entry directly on top of accum entry var isSplitAccum = defined(nextEntryAccum) && entryAccum.height === nextEntryAccum.height; var isStartAccum = !defined(prevEntryAccum); var isEndAccum = !defined(nextEntryAccum); var isSplit = defined(nextEntry) && entry.height === nextEntry.height; var isStart = !defined(prevEntry); var isEnd = !defined(nextEntry); if (isSplitAccum) { if (isSplit) { addBlendEntry(entry.height, entry.color, entryAccum.color); addBlendEntry(entry.height, nextEntry.color, nextEntryAccum.color); } else if (isStart) { addEntry(entry.height, entryAccum.color); addBlendEntry(entry.height, entry.color, nextEntryAccum.color); } else if (isEnd) { addBlendEntry(entry.height, entry.color, entryAccum.color); addEntry(entry.height, nextEntryAccum.color); } else { addBlendEntry(entry.height, entry.color, entryAccum.color); addBlendEntry(entry.height, entry.color, nextEntryAccum.color); } } else if (isStartAccum) { if (isSplit) { addEntry(entry.height, entry.color); addBlendEntry(entry.height, nextEntry.color, entryAccum.color); } else if (isEnd) { addEntry(entry.height, entry.color); addEntry(entry.height, entryAccum.color); } else if (isStart) { addBlendEntry(entry.height, entry.color, entryAccum.color); } else { addEntry(entry.height, entry.color); addBlendEntry(entry.height, entry.color, entryAccum.color); } } else if (isEndAccum) { if (isSplit) { addBlendEntry(entry.height, entry.color, entryAccum.color); addEntry(entry.height, nextEntry.color); } else if (isStart) { addEntry(entry.height, entryAccum.color); addEntry(entry.height, entry.color); } else if (isEnd) { addBlendEntry(entry.height, entry.color, entryAccum.color); } else { addBlendEntry(entry.height, entry.color, entryAccum.color); addEntry(entry.height, entry.color); } } else { // eslint-disable-next-line no-lonely-if if (isSplit) { addBlendEntry(entry.height, entry.color, entryAccum.color); addBlendEntry(entry.height, nextEntry.color, entryAccum.color); } else if (isStart) { addEntry(entry.height, entryAccum.color); addBlendEntry(entry.height, entry.color, entryAccum.color); } else if (isEnd) { addBlendEntry(entry.height, entry.color, entryAccum.color); addEntry(entry.height, entryAccum.color); } else { addBlendEntry(entry.height, entry.color, entryAccum.color); } } idx += isSplit ? 2 : 1; accumIdx += isSplitAccum ? 2 : 1; } else if ( defined(entry) && defined(entryAccum) && defined(prevEntryAccum) && entry.height < entryAccum.height ) { // New entry between two accum entries var colorBelow = lerpEntryColor( entry.height, prevEntryAccum, entryAccum, scratchColorBelow ); if (!defined(prevEntry)) { addEntry(entry.height, colorBelow); addBlendEntry(entry.height, entry.color, colorBelow); } else if (!defined(nextEntry)) { addBlendEntry(entry.height, entry.color, colorBelow); addEntry(entry.height, colorBelow); } else { addBlendEntry(entry.height, entry.color, colorBelow); } idx++; } else if ( defined(entryAccum) && defined(entry) && defined(prevEntry) && entryAccum.height < entry.height ) { // Accum entry between two new entries var colorAbove = lerpEntryColor( entryAccum.height, prevEntry, entry, scratchColorAbove ); if (!defined(prevEntryAccum)) { addEntry(entryAccum.height, colorAbove); addBlendEntry(entryAccum.height, colorAbove, entryAccum.color); } else if (!defined(nextEntryAccum)) { addBlendEntry(entryAccum.height, colorAbove, entryAccum.color); addEntry(entryAccum.height, colorAbove); } else { addBlendEntry(entryAccum.height, colorAbove, entryAccum.color); } accumIdx++; } else if ( defined(entry) && (!defined(entryAccum) || entry.height < entryAccum.height) ) { // New entry completely before or completely after accum entries if ( defined(entryAccum) && !defined(prevEntryAccum) && !defined(nextEntry) ) { // Insert blank gap between last entry and first accum entry addEntry(entry.height, entry.color); addEntry(entry.height, createElevationBandMaterial._emptyColor); addEntry(entryAccum.height, createElevationBandMaterial._emptyColor); } else if ( !defined(entryAccum) && defined(prevEntryAccum) && !defined(prevEntry) ) { // Insert blank gap between last accum entry and first entry addEntry( prevEntryAccum.height, createElevationBandMaterial._emptyColor ); addEntry(entry.height, createElevationBandMaterial._emptyColor); addEntry(entry.height, entry.color); } else { addEntry(entry.height, entry.color); } idx++; } else if ( defined(entryAccum) && (!defined(entry) || entryAccum.height < entry.height) ) { // Accum entry completely before or completely after new entries addEntry(entryAccum.height, entryAccum.color); accumIdx++; } } } // one final cleanup pass in case duplicate colors show up in the final result var allEntries = removeDuplicates(entriesAccumNext); return allEntries; } /** * @typedef createElevationBandMaterialEntry * * @property {Number} height The height. * @property {Color} color The color at this height. */ /** * @typedef createElevationBandMaterialBand * * @property {createElevationBandMaterialEntry[]} entries A list of elevation entries. They will automatically be sorted from lowest to highest. If there is only one entry and extendsDownards and extendUpwards are both false, they will both be set to true. * @property {Boolean} [extendDownwards=false] If true, the band's minimum elevation color will extend infinitely downwards. * @property {Boolean} [extendUpwards=false] If true, the band's maximum elevation color will extend infinitely upwards. */ /** * Creates a {@link Material} that combines multiple layers of color/gradient bands and maps them to terrain heights. * * The shader does a binary search over all the heights to find out which colors are above and below a given height, and * interpolates between them for the final color. This material supports hundreds of entries relatively cheaply. * * @function createElevationBandMaterial * * @param {Object} options Object with the following properties: * @param {Scene} options.scene The scene where the visualization is taking place. * @param {createElevationBandMaterialBand[]} options.layers A list of bands ordered from lowest to highest precedence. * @returns {Material} A new {@link Material} instance. * * @demo {@link https://sandcastle.cesium.com/index.html?src=Elevation%20Band%20Material.html|Cesium Sandcastle Elevation Band Demo} * * @example * scene.globe.material = Cesium.createElevationBandMaterial({ * scene : scene, * layers : [{ * entries : [{ * height : 4200.0, * color : new Cesium.Color(0.0, 0.0, 0.0, 1.0) * }, { * height : 8848.0, * color : new Cesium.Color(1.0, 1.0, 1.0, 1.0) * }], * extendDownwards : true, * extendUpwards : true, * }, { * entries : [{ * height : 7000.0, * color : new Cesium.Color(1.0, 0.0, 0.0, 0.5) * }, { * height : 7100.0, * color : new Cesium.Color(1.0, 0.0, 0.0, 0.5) * }] * }] * }); */ function createElevationBandMaterial(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); var scene = options.scene; var layers = options.layers; //>>includeStart('debug', pragmas.debug); Check.typeOf.object("options.scene", scene); Check.defined("options.layers", layers); Check.typeOf.number.greaterThan("options.layers.length", layers.length, 0); //>>includeEnd('debug'); var entries = createLayeredEntries(layers); var entriesLength = entries.length; var i; var heightTexBuffer; var heightTexDatatype; var heightTexFormat; var isPackedHeight = !createElevationBandMaterial._useFloatTexture( scene.context ); if (isPackedHeight) { heightTexDatatype = PixelDatatype.UNSIGNED_BYTE; heightTexFormat = PixelFormat.RGBA; heightTexBuffer = new Uint8Array(entriesLength * 4); for (i = 0; i < entriesLength; i++) { Cartesian4.packFloat(entries[i].height, scratchPackedFloat); Cartesian4.pack(scratchPackedFloat, heightTexBuffer, i * 4); } } else { heightTexDatatype = PixelDatatype.FLOAT; heightTexFormat = PixelFormat.LUMINANCE; heightTexBuffer = new Float32Array(entriesLength); for (i = 0; i < entriesLength; i++) { heightTexBuffer[i] = entries[i].height; } } var heightsTex = Texture.create({ context: scene.context, pixelFormat: heightTexFormat, pixelDatatype: heightTexDatatype, source: { arrayBufferView: heightTexBuffer, width: entriesLength, height: 1, }, sampler: new Sampler({ wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.NEAREST, magnificationFilter: TextureMagnificationFilter.NEAREST, }), }); var colorsArray = new Uint8Array(entriesLength * 4); for (i = 0; i < entriesLength; i++) { var color = entries[i].color; color.toBytes(scratchColorBytes); colorsArray[i * 4 + 0] = scratchColorBytes[0]; colorsArray[i * 4 + 1] = scratchColorBytes[1]; colorsArray[i * 4 + 2] = scratchColorBytes[2]; colorsArray[i * 4 + 3] = scratchColorBytes[3]; } var colorsTex = Texture.create({ context: scene.context, pixelFormat: PixelFormat.RGBA, pixelDatatype: PixelDatatype.UNSIGNED_BYTE, source: { arrayBufferView: colorsArray, width: entriesLength, height: 1, }, sampler: new Sampler({ wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.LINEAR, magnificationFilter: TextureMagnificationFilter.LINEAR, }), }); var material = Material.fromType("ElevationBand", { heights: heightsTex, colors: colorsTex, }); return material; } /** * Function for checking if the context will allow floating point textures for heights. * * @param {Context} context The {@link Context}. * @returns {Boolean} true if floating point textures can be used for heights. * @private */ createElevationBandMaterial._useFloatTexture = function (context) { return context.floatingPointTexture; }; /** * This is the height that gets stored in the texture when using extendUpwards. * There's nothing special about it, it's just a really big number. * @private */ createElevationBandMaterial._maximumHeight = +5906376425472; /** * This is the height that gets stored in the texture when using extendDownwards. * There's nothing special about it, it's just a really big number. * @private */ createElevationBandMaterial._minimumHeight = -5906376425472; /** * Color used to create empty space in the color texture * @private */ createElevationBandMaterial._emptyColor = new Color(0.0, 0.0, 0.0, 0.0); export default createElevationBandMaterial;