| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 | import AxisAlignedBoundingBox from "./AxisAlignedBoundingBox.js";import BoundingSphere from "./BoundingSphere.js";import Cartesian2 from "./Cartesian2.js";import Cartesian3 from "./Cartesian3.js";import defaultValue from "./defaultValue.js";import defined from "./defined.js";import DeveloperError from "./DeveloperError.js";import Ellipsoid from "./Ellipsoid.js";import EllipsoidalOccluder from "./EllipsoidalOccluder.js";import CesiumMath from "./Math.js";import Matrix4 from "./Matrix4.js";import OrientedBoundingBox from "./OrientedBoundingBox.js";import Rectangle from "./Rectangle.js";import TerrainEncoding from "./TerrainEncoding.js";import Transforms from "./Transforms.js";import WebMercatorProjection from "./WebMercatorProjection.js";/** * Contains functions to create a mesh from a heightmap image. * * @namespace HeightmapTessellator * * @private */const HeightmapTessellator = {};/** * The default structure of a heightmap, as given to {@link HeightmapTessellator.computeVertices}. * * @constant */HeightmapTessellator.DEFAULT_STRUCTURE = Object.freeze({  heightScale: 1.0,  heightOffset: 0.0,  elementsPerHeight: 1,  stride: 1,  elementMultiplier: 256.0,  isBigEndian: false,});const cartesian3Scratch = new Cartesian3();const matrix4Scratch = new Matrix4();const minimumScratch = new Cartesian3();const maximumScratch = new Cartesian3();/** * Fills an array of vertices from a heightmap image. * * @param {Object} options Object with the following properties: * @param {Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array} options.heightmap The heightmap to tessellate. * @param {Number} options.width The width of the heightmap, in height samples. * @param {Number} options.height The height of the heightmap, in height samples. * @param {Number} options.skirtHeight The height of skirts to drape at the edges of the heightmap. * @param {Rectangle} options.nativeRectangle A rectangle in the native coordinates of the heightmap's projection.  For *                 a heightmap with a geographic projection, this is degrees.  For the web mercator *                 projection, this is meters. * @param {Number} [options.exaggeration=1.0] The scale used to exaggerate the terrain. * @param {Number} [options.exaggerationRelativeHeight=0.0] The height from which terrain is exaggerated. * @param {Rectangle} [options.rectangle] The rectangle covered by the heightmap, in geodetic coordinates with north, south, east and *                 west properties in radians.  Either rectangle or nativeRectangle must be provided.  If both *                 are provided, they're assumed to be consistent. * @param {Boolean} [options.isGeographic=true] True if the heightmap uses a {@link GeographicProjection}, or false if it uses *                  a {@link WebMercatorProjection}. * @param {Cartesian3} [options.relativeToCenter=Cartesian3.ZERO] The positions will be computed as <code>Cartesian3.subtract(worldPosition, relativeToCenter)</code>. * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid to which the heightmap applies. * @param {Object} [options.structure] An object describing the structure of the height data. * @param {Number} [options.structure.heightScale=1.0] The factor by which to multiply height samples in order to obtain *                 the height above the heightOffset, in meters.  The heightOffset is added to the resulting *                 height after multiplying by the scale. * @param {Number} [options.structure.heightOffset=0.0] The offset to add to the scaled height to obtain the final *                 height in meters.  The offset is added after the height sample is multiplied by the *                 heightScale. * @param {Number} [options.structure.elementsPerHeight=1] The number of elements in the buffer that make up a single height *                 sample.  This is usually 1, indicating that each element is a separate height sample.  If *                 it is greater than 1, that number of elements together form the height sample, which is *                 computed according to the structure.elementMultiplier and structure.isBigEndian properties. * @param {Number} [options.structure.stride=1] The number of elements to skip to get from the first element of *                 one height to the first element of the next height. * @param {Number} [options.structure.elementMultiplier=256.0] The multiplier used to compute the height value when the *                 stride property is greater than 1.  For example, if the stride is 4 and the strideMultiplier *                 is 256, the height is computed as follows: *                 `height = buffer[index] + buffer[index + 1] * 256 + buffer[index + 2] * 256 * 256 + buffer[index + 3] * 256 * 256 * 256` *                 This is assuming that the isBigEndian property is false.  If it is true, the order of the *                 elements is reversed. * @param {Number} [options.structure.lowestEncodedHeight] The lowest value that can be stored in the height buffer.  Any heights that are lower *                 than this value after encoding with the `heightScale` and `heightOffset` are clamped to this value.  For example, if the height *                 buffer is a `Uint16Array`, this value should be 0 because a `Uint16Array` cannot store negative numbers.  If this parameter is *                 not specified, no minimum value is enforced. * @param {Number} [options.structure.highestEncodedHeight] The highest value that can be stored in the height buffer.  Any heights that are higher *                 than this value after encoding with the `heightScale` and `heightOffset` are clamped to this value.  For example, if the height *                 buffer is a `Uint16Array`, this value should be `256 * 256 - 1` or 65535 because a `Uint16Array` cannot store numbers larger *                 than 65535.  If this parameter is not specified, no maximum value is enforced. * @param {Boolean} [options.structure.isBigEndian=false] Indicates endianness of the elements in the buffer when the *                  stride property is greater than 1.  If this property is false, the first element is the *                  low-order element.  If it is true, the first element is the high-order element. * * @example * const width = 5; * const height = 5; * const statistics = Cesium.HeightmapTessellator.computeVertices({ *     heightmap : [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], *     width : width, *     height : height, *     skirtHeight : 0.0, *     nativeRectangle : { *         west : 10.0, *         east : 20.0, *         south : 30.0, *         north : 40.0 *     } * }); * * const encoding = statistics.encoding; * const position = encoding.decodePosition(statistics.vertices, index); */HeightmapTessellator.computeVertices = function (options) {  //>>includeStart('debug', pragmas.debug);  if (!defined(options) || !defined(options.heightmap)) {    throw new DeveloperError("options.heightmap is required.");  }  if (!defined(options.width) || !defined(options.height)) {    throw new DeveloperError("options.width and options.height are required.");  }  if (!defined(options.nativeRectangle)) {    throw new DeveloperError("options.nativeRectangle is required.");  }  if (!defined(options.skirtHeight)) {    throw new DeveloperError("options.skirtHeight is required.");  }  //>>includeEnd('debug');  // This function tends to be a performance hotspot for terrain rendering,  // so it employs a lot of inlining and unrolling as an optimization.  // In particular, the functionality of Ellipsoid.cartographicToCartesian  // is inlined.  const cos = Math.cos;  const sin = Math.sin;  const sqrt = Math.sqrt;  const atan = Math.atan;  const exp = Math.exp;  const piOverTwo = CesiumMath.PI_OVER_TWO;  const toRadians = CesiumMath.toRadians;  const heightmap = options.heightmap;  const width = options.width;  const height = options.height;  const skirtHeight = options.skirtHeight;  const hasSkirts = skirtHeight > 0.0;  const isGeographic = defaultValue(options.isGeographic, true);  const ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);  const oneOverGlobeSemimajorAxis = 1.0 / ellipsoid.maximumRadius;  const nativeRectangle = Rectangle.clone(options.nativeRectangle);  const rectangle = Rectangle.clone(options.rectangle);  let geographicWest;  let geographicSouth;  let geographicEast;  let geographicNorth;  if (!defined(rectangle)) {    if (isGeographic) {      geographicWest = toRadians(nativeRectangle.west);      geographicSouth = toRadians(nativeRectangle.south);      geographicEast = toRadians(nativeRectangle.east);      geographicNorth = toRadians(nativeRectangle.north);    } else {      geographicWest = nativeRectangle.west * oneOverGlobeSemimajorAxis;      geographicSouth =        piOverTwo -        2.0 * atan(exp(-nativeRectangle.south * oneOverGlobeSemimajorAxis));      geographicEast = nativeRectangle.east * oneOverGlobeSemimajorAxis;      geographicNorth =        piOverTwo -        2.0 * atan(exp(-nativeRectangle.north * oneOverGlobeSemimajorAxis));    }  } else {    geographicWest = rectangle.west;    geographicSouth = rectangle.south;    geographicEast = rectangle.east;    geographicNorth = rectangle.north;  }  let relativeToCenter = options.relativeToCenter;  const hasRelativeToCenter = defined(relativeToCenter);  relativeToCenter = hasRelativeToCenter ? relativeToCenter : Cartesian3.ZERO;  const includeWebMercatorT = defaultValue(options.includeWebMercatorT, false);  const exaggeration = defaultValue(options.exaggeration, 1.0);  const exaggerationRelativeHeight = defaultValue(    options.exaggerationRelativeHeight,    0.0  );  const hasExaggeration = exaggeration !== 1.0;  const includeGeodeticSurfaceNormals = hasExaggeration;  const structure = defaultValue(    options.structure,    HeightmapTessellator.DEFAULT_STRUCTURE  );  const heightScale = defaultValue(    structure.heightScale,    HeightmapTessellator.DEFAULT_STRUCTURE.heightScale  );  const heightOffset = defaultValue(    structure.heightOffset,    HeightmapTessellator.DEFAULT_STRUCTURE.heightOffset  );  const elementsPerHeight = defaultValue(    structure.elementsPerHeight,    HeightmapTessellator.DEFAULT_STRUCTURE.elementsPerHeight  );  const stride = defaultValue(    structure.stride,    HeightmapTessellator.DEFAULT_STRUCTURE.stride  );  const elementMultiplier = defaultValue(    structure.elementMultiplier,    HeightmapTessellator.DEFAULT_STRUCTURE.elementMultiplier  );  const isBigEndian = defaultValue(    structure.isBigEndian,    HeightmapTessellator.DEFAULT_STRUCTURE.isBigEndian  );  let rectangleWidth = Rectangle.computeWidth(nativeRectangle);  let rectangleHeight = Rectangle.computeHeight(nativeRectangle);  const granularityX = rectangleWidth / (width - 1);  const granularityY = rectangleHeight / (height - 1);  if (!isGeographic) {    rectangleWidth *= oneOverGlobeSemimajorAxis;    rectangleHeight *= oneOverGlobeSemimajorAxis;  }  const radiiSquared = ellipsoid.radiiSquared;  const radiiSquaredX = radiiSquared.x;  const radiiSquaredY = radiiSquared.y;  const radiiSquaredZ = radiiSquared.z;  let minimumHeight = 65536.0;  let maximumHeight = -65536.0;  const fromENU = Transforms.eastNorthUpToFixedFrame(    relativeToCenter,    ellipsoid  );  const toENU = Matrix4.inverseTransformation(fromENU, matrix4Scratch);  let southMercatorY;  let oneOverMercatorHeight;  if (includeWebMercatorT) {    southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(      geographicSouth    );    oneOverMercatorHeight =      1.0 /      (WebMercatorProjection.geodeticLatitudeToMercatorAngle(geographicNorth) -        southMercatorY);  }  const minimum = minimumScratch;  minimum.x = Number.POSITIVE_INFINITY;  minimum.y = Number.POSITIVE_INFINITY;  minimum.z = Number.POSITIVE_INFINITY;  const maximum = maximumScratch;  maximum.x = Number.NEGATIVE_INFINITY;  maximum.y = Number.NEGATIVE_INFINITY;  maximum.z = Number.NEGATIVE_INFINITY;  let hMin = Number.POSITIVE_INFINITY;  const gridVertexCount = width * height;  const edgeVertexCount = skirtHeight > 0.0 ? width * 2 + height * 2 : 0;  const vertexCount = gridVertexCount + edgeVertexCount;  const positions = new Array(vertexCount);  const heights = new Array(vertexCount);  const uvs = new Array(vertexCount);  const webMercatorTs = includeWebMercatorT ? new Array(vertexCount) : [];  const geodeticSurfaceNormals = includeGeodeticSurfaceNormals    ? new Array(vertexCount)    : [];  let startRow = 0;  let endRow = height;  let startCol = 0;  let endCol = width;  if (hasSkirts) {    --startRow;    ++endRow;    --startCol;    ++endCol;  }  const skirtOffsetPercentage = 0.00001;  for (let rowIndex = startRow; rowIndex < endRow; ++rowIndex) {    let row = rowIndex;    if (row < 0) {      row = 0;    }    if (row >= height) {      row = height - 1;    }    let latitude = nativeRectangle.north - granularityY * row;    if (!isGeographic) {      latitude =        piOverTwo - 2.0 * atan(exp(-latitude * oneOverGlobeSemimajorAxis));    } else {      latitude = toRadians(latitude);    }    let v = (latitude - geographicSouth) / (geographicNorth - geographicSouth);    v = CesiumMath.clamp(v, 0.0, 1.0);    const isNorthEdge = rowIndex === startRow;    const isSouthEdge = rowIndex === endRow - 1;    if (skirtHeight > 0.0) {      if (isNorthEdge) {        latitude += skirtOffsetPercentage * rectangleHeight;      } else if (isSouthEdge) {        latitude -= skirtOffsetPercentage * rectangleHeight;      }    }    const cosLatitude = cos(latitude);    const nZ = sin(latitude);    const kZ = radiiSquaredZ * nZ;    let webMercatorT;    if (includeWebMercatorT) {      webMercatorT =        (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) -          southMercatorY) *        oneOverMercatorHeight;    }    for (let colIndex = startCol; colIndex < endCol; ++colIndex) {      let col = colIndex;      if (col < 0) {        col = 0;      }      if (col >= width) {        col = width - 1;      }      const terrainOffset = row * (width * stride) + col * stride;      let heightSample;      if (elementsPerHeight === 1) {        heightSample = heightmap[terrainOffset];      } else {        heightSample = 0;        let elementOffset;        if (isBigEndian) {          for (            elementOffset = 0;            elementOffset < elementsPerHeight;            ++elementOffset          ) {            heightSample =              heightSample * elementMultiplier +              heightmap[terrainOffset + elementOffset];          }        } else {          for (            elementOffset = elementsPerHeight - 1;            elementOffset >= 0;            --elementOffset          ) {            heightSample =              heightSample * elementMultiplier +              heightmap[terrainOffset + elementOffset];          }        }      }      heightSample = heightSample * heightScale + heightOffset;      maximumHeight = Math.max(maximumHeight, heightSample);      minimumHeight = Math.min(minimumHeight, heightSample);      let longitude = nativeRectangle.west + granularityX * col;      if (!isGeographic) {        longitude = longitude * oneOverGlobeSemimajorAxis;      } else {        longitude = toRadians(longitude);      }      let u = (longitude - geographicWest) / (geographicEast - geographicWest);      u = CesiumMath.clamp(u, 0.0, 1.0);      let index = row * width + col;      if (skirtHeight > 0.0) {        const isWestEdge = colIndex === startCol;        const isEastEdge = colIndex === endCol - 1;        const isEdge = isNorthEdge || isSouthEdge || isWestEdge || isEastEdge;        const isCorner =          (isNorthEdge || isSouthEdge) && (isWestEdge || isEastEdge);        if (isCorner) {          // Don't generate skirts on the corners.          continue;        } else if (isEdge) {          heightSample -= skirtHeight;          if (isWestEdge) {            // The outer loop iterates north to south but the indices are ordered south to north, hence the index flip below            index = gridVertexCount + (height - row - 1);            longitude -= skirtOffsetPercentage * rectangleWidth;          } else if (isSouthEdge) {            // Add after west indices. South indices are ordered east to west.            index = gridVertexCount + height + (width - col - 1);          } else if (isEastEdge) {            // Add after west and south indices. East indices are ordered north to south. The index is flipped like above.            index = gridVertexCount + height + width + row;            longitude += skirtOffsetPercentage * rectangleWidth;          } else if (isNorthEdge) {            // Add after west, south, and east indices. North indices are ordered west to east.            index = gridVertexCount + height + width + height + col;          }        }      }      const nX = cosLatitude * cos(longitude);      const nY = cosLatitude * sin(longitude);      const kX = radiiSquaredX * nX;      const kY = radiiSquaredY * nY;      const gamma = sqrt(kX * nX + kY * nY + kZ * nZ);      const oneOverGamma = 1.0 / gamma;      const rSurfaceX = kX * oneOverGamma;      const rSurfaceY = kY * oneOverGamma;      const rSurfaceZ = kZ * oneOverGamma;      const position = new Cartesian3();      position.x = rSurfaceX + nX * heightSample;      position.y = rSurfaceY + nY * heightSample;      position.z = rSurfaceZ + nZ * heightSample;      Matrix4.multiplyByPoint(toENU, position, cartesian3Scratch);      Cartesian3.minimumByComponent(cartesian3Scratch, minimum, minimum);      Cartesian3.maximumByComponent(cartesian3Scratch, maximum, maximum);      hMin = Math.min(hMin, heightSample);      positions[index] = position;      uvs[index] = new Cartesian2(u, v);      heights[index] = heightSample;      if (includeWebMercatorT) {        webMercatorTs[index] = webMercatorT;      }      if (includeGeodeticSurfaceNormals) {        geodeticSurfaceNormals[index] = ellipsoid.geodeticSurfaceNormal(          position        );      }    }  }  const boundingSphere3D = BoundingSphere.fromPoints(positions);  let orientedBoundingBox;  if (defined(rectangle)) {    orientedBoundingBox = OrientedBoundingBox.fromRectangle(      rectangle,      minimumHeight,      maximumHeight,      ellipsoid    );  }  let occludeePointInScaledSpace;  if (hasRelativeToCenter) {    const occluder = new EllipsoidalOccluder(ellipsoid);    occludeePointInScaledSpace = occluder.computeHorizonCullingPointPossiblyUnderEllipsoid(      relativeToCenter,      positions,      minimumHeight    );  }  const aaBox = new AxisAlignedBoundingBox(minimum, maximum, relativeToCenter);  const encoding = new TerrainEncoding(    relativeToCenter,    aaBox,    hMin,    maximumHeight,    fromENU,    false,    includeWebMercatorT,    includeGeodeticSurfaceNormals,    exaggeration,    exaggerationRelativeHeight  );  const vertices = new Float32Array(vertexCount * encoding.stride);  let bufferIndex = 0;  for (let j = 0; j < vertexCount; ++j) {    bufferIndex = encoding.encode(      vertices,      bufferIndex,      positions[j],      uvs[j],      heights[j],      undefined,      webMercatorTs[j],      geodeticSurfaceNormals[j]    );  }  return {    vertices: vertices,    maximumHeight: maximumHeight,    minimumHeight: minimumHeight,    encoding: encoding,    boundingSphere3D: boundingSphere3D,    orientedBoundingBox: orientedBoundingBox,    occludeePointInScaledSpace: occludeePointInScaledSpace,  };};export default HeightmapTessellator;
 |