123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- 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 <code>extendsDownards</code> and <code>extendUpwards</code> are both <code>false</code>, they will both be set to <code>true</code>.
- * @property {Boolean} [extendDownwards=false] If <code>true</code>, the band's minimum elevation color will extend infinitely downwards.
- * @property {Boolean} [extendUpwards=false] If <code>true</code>, 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} <code>true</code> 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;
|