HeightmapTessellator.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import AxisAlignedBoundingBox from './AxisAlignedBoundingBox.js';
  2. import BoundingSphere from './BoundingSphere.js';
  3. import Cartesian2 from './Cartesian2.js';
  4. import Cartesian3 from './Cartesian3.js';
  5. import defaultValue from './defaultValue.js';
  6. import defined from './defined.js';
  7. import DeveloperError from './DeveloperError.js';
  8. import Ellipsoid from './Ellipsoid.js';
  9. import EllipsoidalOccluder from './EllipsoidalOccluder.js';
  10. import freezeObject from './freezeObject.js';
  11. import CesiumMath from './Math.js';
  12. import Matrix4 from './Matrix4.js';
  13. import OrientedBoundingBox from './OrientedBoundingBox.js';
  14. import Rectangle from './Rectangle.js';
  15. import TerrainEncoding from './TerrainEncoding.js';
  16. import Transforms from './Transforms.js';
  17. import WebMercatorProjection from './WebMercatorProjection.js';
  18. /**
  19. * Contains functions to create a mesh from a heightmap image.
  20. *
  21. * @exports HeightmapTessellator
  22. *
  23. * @private
  24. */
  25. var HeightmapTessellator = {};
  26. /**
  27. * The default structure of a heightmap, as given to {@link HeightmapTessellator.computeVertices}.
  28. *
  29. * @constant
  30. */
  31. HeightmapTessellator.DEFAULT_STRUCTURE = freezeObject({
  32. heightScale : 1.0,
  33. heightOffset : 0.0,
  34. elementsPerHeight : 1,
  35. stride : 1,
  36. elementMultiplier : 256.0,
  37. isBigEndian : false
  38. });
  39. var cartesian3Scratch = new Cartesian3();
  40. var matrix4Scratch = new Matrix4();
  41. var minimumScratch = new Cartesian3();
  42. var maximumScratch = new Cartesian3();
  43. /**
  44. * Fills an array of vertices from a heightmap image.
  45. *
  46. * @param {Object} options Object with the following properties:
  47. * @param {TypedArray} options.heightmap The heightmap to tessellate.
  48. * @param {Number} options.width The width of the heightmap, in height samples.
  49. * @param {Number} options.height The height of the heightmap, in height samples.
  50. * @param {Number} options.skirtHeight The height of skirts to drape at the edges of the heightmap.
  51. * @param {Rectangle} options.nativeRectangle A rectangle in the native coordinates of the heightmap's projection. For
  52. * a heightmap with a geographic projection, this is degrees. For the web mercator
  53. * projection, this is meters.
  54. * @param {Number} [options.exaggeration=1.0] The scale used to exaggerate the terrain.
  55. * @param {Rectangle} [options.rectangle] The rectangle covered by the heightmap, in geodetic coordinates with north, south, east and
  56. * west properties in radians. Either rectangle or nativeRectangle must be provided. If both
  57. * are provided, they're assumed to be consistent.
  58. * @param {Boolean} [options.isGeographic=true] True if the heightmap uses a {@link GeographicProjection}, or false if it uses
  59. * a {@link WebMercatorProjection}.
  60. * @param {Cartesian3} [options.relativeToCenter=Cartesian3.ZERO] The positions will be computed as <code>Cartesian3.subtract(worldPosition, relativeToCenter)</code>.
  61. * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid to which the heightmap applies.
  62. * @param {Object} [options.structure] An object describing the structure of the height data.
  63. * @param {Number} [options.structure.heightScale=1.0] The factor by which to multiply height samples in order to obtain
  64. * the height above the heightOffset, in meters. The heightOffset is added to the resulting
  65. * height after multiplying by the scale.
  66. * @param {Number} [options.structure.heightOffset=0.0] The offset to add to the scaled height to obtain the final
  67. * height in meters. The offset is added after the height sample is multiplied by the
  68. * heightScale.
  69. * @param {Number} [options.structure.elementsPerHeight=1] The number of elements in the buffer that make up a single height
  70. * sample. This is usually 1, indicating that each element is a separate height sample. If
  71. * it is greater than 1, that number of elements together form the height sample, which is
  72. * computed according to the structure.elementMultiplier and structure.isBigEndian properties.
  73. * @param {Number} [options.structure.stride=1] The number of elements to skip to get from the first element of
  74. * one height to the first element of the next height.
  75. * @param {Number} [options.structure.elementMultiplier=256.0] The multiplier used to compute the height value when the
  76. * stride property is greater than 1. For example, if the stride is 4 and the strideMultiplier
  77. * is 256, the height is computed as follows:
  78. * `height = buffer[index] + buffer[index + 1] * 256 + buffer[index + 2] * 256 * 256 + buffer[index + 3] * 256 * 256 * 256`
  79. * This is assuming that the isBigEndian property is false. If it is true, the order of the
  80. * elements is reversed.
  81. * @param {Number} [options.structure.lowestEncodedHeight] The lowest value that can be stored in the height buffer. Any heights that are lower
  82. * than this value after encoding with the `heightScale` and `heightOffset` are clamped to this value. For example, if the height
  83. * buffer is a `Uint16Array`, this value should be 0 because a `Uint16Array` cannot store negative numbers. If this parameter is
  84. * not specified, no minimum value is enforced.
  85. * @param {Number} [options.structure.highestEncodedHeight] The highest value that can be stored in the height buffer. Any heights that are higher
  86. * than this value after encoding with the `heightScale` and `heightOffset` are clamped to this value. For example, if the height
  87. * buffer is a `Uint16Array`, this value should be `256 * 256 - 1` or 65535 because a `Uint16Array` cannot store numbers larger
  88. * than 65535. If this parameter is not specified, no maximum value is enforced.
  89. * @param {Boolean} [options.structure.isBigEndian=false] Indicates endianness of the elements in the buffer when the
  90. * stride property is greater than 1. If this property is false, the first element is the
  91. * low-order element. If it is true, the first element is the high-order element.
  92. *
  93. * @example
  94. * var width = 5;
  95. * var height = 5;
  96. * var statistics = Cesium.HeightmapTessellator.computeVertices({
  97. * heightmap : [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
  98. * width : width,
  99. * height : height,
  100. * skirtHeight : 0.0,
  101. * nativeRectangle : {
  102. * west : 10.0,
  103. * east : 20.0,
  104. * south : 30.0,
  105. * north : 40.0
  106. * }
  107. * });
  108. *
  109. * var encoding = statistics.encoding;
  110. * var position = encoding.decodePosition(statistics.vertices, index * encoding.getStride());
  111. */
  112. HeightmapTessellator.computeVertices = function(options) {
  113. //>>includeStart('debug', pragmas.debug);
  114. if (!defined(options) || !defined(options.heightmap)) {
  115. throw new DeveloperError('options.heightmap is required.');
  116. }
  117. if (!defined(options.width) || !defined(options.height)) {
  118. throw new DeveloperError('options.width and options.height are required.');
  119. }
  120. if (!defined(options.nativeRectangle)) {
  121. throw new DeveloperError('options.nativeRectangle is required.');
  122. }
  123. if (!defined(options.skirtHeight)) {
  124. throw new DeveloperError('options.skirtHeight is required.');
  125. }
  126. //>>includeEnd('debug');
  127. // This function tends to be a performance hotspot for terrain rendering,
  128. // so it employs a lot of inlining and unrolling as an optimization.
  129. // In particular, the functionality of Ellipsoid.cartographicToCartesian
  130. // is inlined.
  131. var cos = Math.cos;
  132. var sin = Math.sin;
  133. var sqrt = Math.sqrt;
  134. var atan = Math.atan;
  135. var exp = Math.exp;
  136. var piOverTwo = CesiumMath.PI_OVER_TWO;
  137. var toRadians = CesiumMath.toRadians;
  138. var heightmap = options.heightmap;
  139. var width = options.width;
  140. var height = options.height;
  141. var skirtHeight = options.skirtHeight;
  142. var isGeographic = defaultValue(options.isGeographic, true);
  143. var ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);
  144. var oneOverGlobeSemimajorAxis = 1.0 / ellipsoid.maximumRadius;
  145. var nativeRectangle = options.nativeRectangle;
  146. var geographicWest;
  147. var geographicSouth;
  148. var geographicEast;
  149. var geographicNorth;
  150. var rectangle = options.rectangle;
  151. if (!defined(rectangle)) {
  152. if (isGeographic) {
  153. geographicWest = toRadians(nativeRectangle.west);
  154. geographicSouth = toRadians(nativeRectangle.south);
  155. geographicEast = toRadians(nativeRectangle.east);
  156. geographicNorth = toRadians(nativeRectangle.north);
  157. } else {
  158. geographicWest = nativeRectangle.west * oneOverGlobeSemimajorAxis;
  159. geographicSouth = piOverTwo - (2.0 * atan(exp(-nativeRectangle.south * oneOverGlobeSemimajorAxis)));
  160. geographicEast = nativeRectangle.east * oneOverGlobeSemimajorAxis;
  161. geographicNorth = piOverTwo - (2.0 * atan(exp(-nativeRectangle.north * oneOverGlobeSemimajorAxis)));
  162. }
  163. } else {
  164. geographicWest = rectangle.west;
  165. geographicSouth = rectangle.south;
  166. geographicEast = rectangle.east;
  167. geographicNorth = rectangle.north;
  168. }
  169. var relativeToCenter = options.relativeToCenter;
  170. var hasRelativeToCenter = defined(relativeToCenter);
  171. relativeToCenter = hasRelativeToCenter ? relativeToCenter : Cartesian3.ZERO;
  172. var exaggeration = defaultValue(options.exaggeration, 1.0);
  173. var includeWebMercatorT = defaultValue(options.includeWebMercatorT, false);
  174. var structure = defaultValue(options.structure, HeightmapTessellator.DEFAULT_STRUCTURE);
  175. var heightScale = defaultValue(structure.heightScale, HeightmapTessellator.DEFAULT_STRUCTURE.heightScale);
  176. var heightOffset = defaultValue(structure.heightOffset, HeightmapTessellator.DEFAULT_STRUCTURE.heightOffset);
  177. var elementsPerHeight = defaultValue(structure.elementsPerHeight, HeightmapTessellator.DEFAULT_STRUCTURE.elementsPerHeight);
  178. var stride = defaultValue(structure.stride, HeightmapTessellator.DEFAULT_STRUCTURE.stride);
  179. var elementMultiplier = defaultValue(structure.elementMultiplier, HeightmapTessellator.DEFAULT_STRUCTURE.elementMultiplier);
  180. var isBigEndian = defaultValue(structure.isBigEndian, HeightmapTessellator.DEFAULT_STRUCTURE.isBigEndian);
  181. var rectangleWidth = Rectangle.computeWidth(nativeRectangle);
  182. var rectangleHeight = Rectangle.computeHeight(nativeRectangle);
  183. var granularityX = rectangleWidth / (width - 1);
  184. var granularityY = rectangleHeight / (height - 1);
  185. if (!isGeographic) {
  186. rectangleWidth *= oneOverGlobeSemimajorAxis;
  187. rectangleHeight *= oneOverGlobeSemimajorAxis;
  188. }
  189. var radiiSquared = ellipsoid.radiiSquared;
  190. var radiiSquaredX = radiiSquared.x;
  191. var radiiSquaredY = radiiSquared.y;
  192. var radiiSquaredZ = radiiSquared.z;
  193. var minimumHeight = 65536.0;
  194. var maximumHeight = -65536.0;
  195. var fromENU = Transforms.eastNorthUpToFixedFrame(relativeToCenter, ellipsoid);
  196. var toENU = Matrix4.inverseTransformation(fromENU, matrix4Scratch);
  197. var southMercatorY;
  198. var oneOverMercatorHeight;
  199. if (includeWebMercatorT) {
  200. southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(geographicSouth);
  201. oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(geographicNorth) - southMercatorY);
  202. }
  203. var minimum = minimumScratch;
  204. minimum.x = Number.POSITIVE_INFINITY;
  205. minimum.y = Number.POSITIVE_INFINITY;
  206. minimum.z = Number.POSITIVE_INFINITY;
  207. var maximum = maximumScratch;
  208. maximum.x = Number.NEGATIVE_INFINITY;
  209. maximum.y = Number.NEGATIVE_INFINITY;
  210. maximum.z = Number.NEGATIVE_INFINITY;
  211. var hMin = Number.POSITIVE_INFINITY;
  212. var arrayWidth = width + (skirtHeight > 0.0 ? 2 : 0);
  213. var arrayHeight = height + (skirtHeight > 0.0 ? 2 : 0);
  214. var size = arrayWidth * arrayHeight;
  215. var positions = new Array(size);
  216. var heights = new Array(size);
  217. var uvs = new Array(size);
  218. var webMercatorTs = includeWebMercatorT ? new Array(size) : [];
  219. var startRow = 0;
  220. var endRow = height;
  221. var startCol = 0;
  222. var endCol = width;
  223. if (skirtHeight > 0.0) {
  224. --startRow;
  225. ++endRow;
  226. --startCol;
  227. ++endCol;
  228. }
  229. var index = 0;
  230. for (var rowIndex = startRow; rowIndex < endRow; ++rowIndex) {
  231. var row = rowIndex;
  232. if (row < 0) {
  233. row = 0;
  234. }
  235. if (row >= height) {
  236. row = height - 1;
  237. }
  238. var latitude = nativeRectangle.north - granularityY * row;
  239. if (!isGeographic) {
  240. latitude = piOverTwo - (2.0 * atan(exp(-latitude * oneOverGlobeSemimajorAxis)));
  241. } else {
  242. latitude = toRadians(latitude);
  243. }
  244. var cosLatitude = cos(latitude);
  245. var nZ = sin(latitude);
  246. var kZ = radiiSquaredZ * nZ;
  247. var v = (latitude - geographicSouth) / (geographicNorth - geographicSouth);
  248. v = CesiumMath.clamp(v, 0.0, 1.0);
  249. var webMercatorT;
  250. if (includeWebMercatorT) {
  251. webMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - southMercatorY) * oneOverMercatorHeight;
  252. }
  253. for (var colIndex = startCol; colIndex < endCol; ++colIndex) {
  254. var col = colIndex;
  255. if (col < 0) {
  256. col = 0;
  257. }
  258. if (col >= width) {
  259. col = width - 1;
  260. }
  261. var longitude = nativeRectangle.west + granularityX * col;
  262. if (!isGeographic) {
  263. longitude = longitude * oneOverGlobeSemimajorAxis;
  264. } else {
  265. longitude = toRadians(longitude);
  266. }
  267. var terrainOffset = row * (width * stride) + col * stride;
  268. var heightSample;
  269. if (elementsPerHeight === 1) {
  270. heightSample = heightmap[terrainOffset];
  271. } else {
  272. heightSample = 0;
  273. var elementOffset;
  274. if (isBigEndian) {
  275. for (elementOffset = 0; elementOffset < elementsPerHeight; ++elementOffset) {
  276. heightSample = (heightSample * elementMultiplier) + heightmap[terrainOffset + elementOffset];
  277. }
  278. } else {
  279. for (elementOffset = elementsPerHeight - 1; elementOffset >= 0; --elementOffset) {
  280. heightSample = (heightSample * elementMultiplier) + heightmap[terrainOffset + elementOffset];
  281. }
  282. }
  283. }
  284. heightSample = (heightSample * heightScale + heightOffset) * exaggeration;
  285. var u = (longitude - geographicWest) / (geographicEast - geographicWest);
  286. u = CesiumMath.clamp(u, 0.0, 1.0);
  287. uvs[index] = new Cartesian2(u, v);
  288. maximumHeight = Math.max(maximumHeight, heightSample);
  289. minimumHeight = Math.min(minimumHeight, heightSample);
  290. if (colIndex !== col || rowIndex !== row) {
  291. var percentage = 0.00001;
  292. if (colIndex < 0) {
  293. longitude -= percentage * rectangleWidth;
  294. } else {
  295. longitude += percentage * rectangleWidth;
  296. }
  297. if (rowIndex < 0) {
  298. latitude += percentage * rectangleHeight;
  299. } else {
  300. latitude -= percentage * rectangleHeight;
  301. }
  302. cosLatitude = cos(latitude);
  303. nZ = sin(latitude);
  304. kZ = radiiSquaredZ * nZ;
  305. heightSample -= skirtHeight;
  306. }
  307. var nX = cosLatitude * cos(longitude);
  308. var nY = cosLatitude * sin(longitude);
  309. var kX = radiiSquaredX * nX;
  310. var kY = radiiSquaredY * nY;
  311. var gamma = sqrt((kX * nX) + (kY * nY) + (kZ * nZ));
  312. var oneOverGamma = 1.0 / gamma;
  313. var rSurfaceX = kX * oneOverGamma;
  314. var rSurfaceY = kY * oneOverGamma;
  315. var rSurfaceZ = kZ * oneOverGamma;
  316. var position = new Cartesian3();
  317. position.x = rSurfaceX + nX * heightSample;
  318. position.y = rSurfaceY + nY * heightSample;
  319. position.z = rSurfaceZ + nZ * heightSample;
  320. positions[index] = position;
  321. heights[index] = heightSample;
  322. if (includeWebMercatorT) {
  323. webMercatorTs[index] = webMercatorT;
  324. }
  325. index++;
  326. Matrix4.multiplyByPoint(toENU, position, cartesian3Scratch);
  327. Cartesian3.minimumByComponent(cartesian3Scratch, minimum, minimum);
  328. Cartesian3.maximumByComponent(cartesian3Scratch, maximum, maximum);
  329. hMin = Math.min(hMin, heightSample);
  330. }
  331. }
  332. var boundingSphere3D = BoundingSphere.fromPoints(positions);
  333. var orientedBoundingBox;
  334. if (defined(rectangle) && rectangle.width < CesiumMath.PI_OVER_TWO + CesiumMath.EPSILON5) {
  335. // Here, rectangle.width < pi/2, and rectangle.height < pi
  336. // (though it would still work with rectangle.width up to pi)
  337. orientedBoundingBox = OrientedBoundingBox.fromRectangle(rectangle, minimumHeight, maximumHeight, ellipsoid);
  338. }
  339. var occludeePointInScaledSpace;
  340. if (hasRelativeToCenter) {
  341. var occluder = new EllipsoidalOccluder(ellipsoid);
  342. occludeePointInScaledSpace = occluder.computeHorizonCullingPoint(relativeToCenter, positions);
  343. }
  344. var aaBox = new AxisAlignedBoundingBox(minimum, maximum, relativeToCenter);
  345. var encoding = new TerrainEncoding(aaBox, hMin, maximumHeight, fromENU, false, includeWebMercatorT);
  346. var vertices = new Float32Array(size * encoding.getStride());
  347. var bufferIndex = 0;
  348. for (var j = 0; j < size; ++j) {
  349. bufferIndex = encoding.encode(vertices, bufferIndex, positions[j], uvs[j], heights[j], undefined, webMercatorTs[j]);
  350. }
  351. var westIndicesSouthToNorth;
  352. var southIndicesEastToWest;
  353. var eastIndicesNorthToSouth;
  354. var northIndicesWestToEast;
  355. if (skirtHeight > 0.0) {
  356. northIndicesWestToEast = [];
  357. southIndicesEastToWest = [];
  358. for (var i1 = 0; i1 < width; ++i1) {
  359. northIndicesWestToEast.push(arrayWidth + 1 + i1);
  360. southIndicesEastToWest.push(arrayWidth * (arrayHeight - 1) - 2 - i1);
  361. }
  362. westIndicesSouthToNorth = [];
  363. eastIndicesNorthToSouth = [];
  364. for (var i2 = 0; i2 < height; ++i2) {
  365. eastIndicesNorthToSouth.push((i2 + 1) * arrayWidth + width);
  366. westIndicesSouthToNorth.push((height - i2) * arrayWidth + 1);
  367. }
  368. } else {
  369. northIndicesWestToEast = [];
  370. southIndicesEastToWest = [];
  371. for (var i3 = 0; i3 < width; ++i3) {
  372. northIndicesWestToEast.push(i3);
  373. southIndicesEastToWest.push(width * height - 1 - i3);
  374. }
  375. westIndicesSouthToNorth = [];
  376. eastIndicesNorthToSouth = [];
  377. for (var i4 = 0; i4 < height; ++i4) {
  378. eastIndicesNorthToSouth.push((i4 + 1) * width - 1);
  379. westIndicesSouthToNorth.push((height - i4 - 1) * width );
  380. }
  381. }
  382. return {
  383. vertices : vertices,
  384. maximumHeight : maximumHeight,
  385. minimumHeight : minimumHeight,
  386. encoding : encoding,
  387. boundingSphere3D : boundingSphere3D,
  388. orientedBoundingBox : orientedBoundingBox,
  389. occludeePointInScaledSpace : occludeePointInScaledSpace,
  390. westIndicesSouthToNorth : westIndicesSouthToNorth,
  391. southIndicesEastToWest : southIndicesEastToWest,
  392. eastIndicesNorthToSouth : eastIndicesNorthToSouth,
  393. northIndicesWestToEast : northIndicesWestToEast
  394. };
  395. };
  396. export default HeightmapTessellator;