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";
const scratchColor = new Color();
const scratchColorAbove = new Color();
const scratchColorBelow = new Color();
const scratchColorBlend = new Color();
const scratchPackedFloat = new Cartesian4();
const scratchColorBytes = new Uint8Array(4);
function lerpEntryColor(height, entryBefore, entryAfter, result) {
const 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) {
const hasPrev = index > 0;
const hasNext = index < array.length - 1;
const sameHeightAsPrev = hasPrev
? entry.height === array[index - 1].height
: true;
const sameHeightAsNext = hasNext
? entry.height === array[index + 1].height
: true;
const keep = !sameHeightAsPrev || !sameHeightAsNext;
return keep;
});
// Remove entries that have the same color as before and after.
entries = entries.filter(function (entry, index, array) {
const hasPrev = index > 0;
const hasNext = index < array.length - 1;
const sameColorAsPrev = hasPrev
? Color.equals(entry.color, array[index - 1].color)
: false;
const sameColorAsNext = hasNext
? Color.equals(entry.color, array[index + 1].color)
: false;
const 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) {
const hasPrev = index > 0;
const sameColorAsPrev = hasPrev
? Color.equals(entry.color, array[index - 1].color)
: false;
const sameHeightAsPrev = hasPrev
? entry.height === array[index - 1].height
: true;
const keep = !sameColorAsPrev || !sameHeightAsPrev;
return keep;
});
return entries;
}
function preprocess(layers) {
let i, j;
const layeredEntries = [];
const layersLength = layers.length;
for (i = 0; i < layersLength; i++) {
const layer = layers[i];
const entriesOrig = layer.entries;
const 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');
let entries = [];
for (j = 0; j < entriesLength; j++) {
const 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');
const height = CesiumMath.clamp(
entryOrig.height,
createElevationBandMaterial._minimumHeight,
createElevationBandMaterial._maximumHeight
);
// premultiplied alpha
const color = Color.clone(entryOrig.color, scratchColor);
color.red *= color.alpha;
color.green *= color.alpha;
color.blue *= color.alpha;
entries.push(createNewEntry(height, color));
}
let sortedAscending = true;
let sortedDescending = true;
for (j = 0; j < entriesLength - 1; j++) {
const currEntry = entries[j + 0];
const 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);
});
}
let extendDownwards = defaultValue(layer.extendDownwards, false);
let 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
const layeredEntries = preprocess(layers);
let entriesAccumNext = [];
let entriesAccumCurr = [];
let i;
function addEntry(height, color) {
entriesAccumNext.push(createNewEntry(height, color));
}
function addBlendEntry(height, a, b) {
let 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
const layerLength = layeredEntries.length;
for (i = 0; i < layerLength; i++) {
const entries = layeredEntries[i];
let idx = 0;
let accumIdx = 0;
// swap the arrays
entriesAccumCurr = entriesAccumNext;
entriesAccumNext = [];
const entriesLength = entries.length;
const entriesAccumLength = entriesAccumCurr.length;
while (idx < entriesLength || accumIdx < entriesAccumLength) {
const entry = idx < entriesLength ? entries[idx] : undefined;
const prevEntry = idx > 0 ? entries[idx - 1] : undefined;
const nextEntry = idx < entriesLength - 1 ? entries[idx + 1] : undefined;
const entryAccum =
accumIdx < entriesAccumLength ? entriesAccumCurr[accumIdx] : undefined;
const prevEntryAccum =
accumIdx > 0 ? entriesAccumCurr[accumIdx - 1] : undefined;
const 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
const isSplitAccum =
defined(nextEntryAccum) &&
entryAccum.height === nextEntryAccum.height;
const isStartAccum = !defined(prevEntryAccum);
const isEndAccum = !defined(nextEntryAccum);
const isSplit = defined(nextEntry) && entry.height === nextEntry.height;
const isStart = !defined(prevEntry);
const 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
const 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
const 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
const 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);
const scene = options.scene;
const 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');
const entries = createLayeredEntries(layers);
const entriesLength = entries.length;
let i;
let heightTexBuffer;
let heightTexDatatype;
let heightTexFormat;
const 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;
}
}
const 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,
}),
});
const colorsArray = new Uint8Array(entriesLength * 4);
for (i = 0; i < entriesLength; i++) {
const 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];
}
const 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,
}),
});
const 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;