groundBuilder.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import { Vector3 } from "../../Maths/math.vector.js";
  2. import { Color3 } from "../../Maths/math.color.js";
  3. import { Mesh } from "../mesh.js";
  4. import { VertexData } from "../mesh.vertexData.js";
  5. import { GroundMesh } from "../groundMesh.js";
  6. import { Tools } from "../../Misc/tools.js";
  7. import { EngineStore } from "../../Engines/engineStore.js";
  8. import { Epsilon } from "../../Maths/math.constants.js";
  9. import { CompatibilityOptions } from "../../Compat/compatibilityOptions.js";
  10. /**
  11. * Creates the VertexData for a Ground
  12. * @param options an object used to set the following optional parameters for the Ground, required but can be empty
  13. * @param options.width the width (x direction) of the ground, optional, default 1
  14. * @param options.height the height (z direction) of the ground, optional, default 1
  15. * @param options.subdivisions the number of subdivisions per side, optional, default 1
  16. * @param options.subdivisionsX the number of subdivisions in the x direction, overrides options.subdivisions, optional, default undefined
  17. * @param options.subdivisionsY the number of subdivisions in the y direction, overrides options.subdivisions, optional, default undefined
  18. * @returns the VertexData of the Ground
  19. */
  20. export function CreateGroundVertexData(options) {
  21. const indices = [];
  22. const positions = [];
  23. const normals = [];
  24. const uvs = [];
  25. let row, col;
  26. const width = options.width || 1;
  27. const height = options.height || 1;
  28. const subdivisionsX = (options.subdivisionsX || options.subdivisions || 1) | 0;
  29. const subdivisionsY = (options.subdivisionsY || options.subdivisions || 1) | 0;
  30. for (row = 0; row <= subdivisionsY; row++) {
  31. for (col = 0; col <= subdivisionsX; col++) {
  32. const position = new Vector3((col * width) / subdivisionsX - width / 2.0, 0, ((subdivisionsY - row) * height) / subdivisionsY - height / 2.0);
  33. const normal = new Vector3(0, 1.0, 0);
  34. positions.push(position.x, position.y, position.z);
  35. normals.push(normal.x, normal.y, normal.z);
  36. uvs.push(col / subdivisionsX, CompatibilityOptions.UseOpenGLOrientationForUV ? row / subdivisionsY : 1.0 - row / subdivisionsY);
  37. }
  38. }
  39. for (row = 0; row < subdivisionsY; row++) {
  40. for (col = 0; col < subdivisionsX; col++) {
  41. indices.push(col + 1 + (row + 1) * (subdivisionsX + 1));
  42. indices.push(col + 1 + row * (subdivisionsX + 1));
  43. indices.push(col + row * (subdivisionsX + 1));
  44. indices.push(col + (row + 1) * (subdivisionsX + 1));
  45. indices.push(col + 1 + (row + 1) * (subdivisionsX + 1));
  46. indices.push(col + row * (subdivisionsX + 1));
  47. }
  48. }
  49. // Result
  50. const vertexData = new VertexData();
  51. vertexData.indices = indices;
  52. vertexData.positions = positions;
  53. vertexData.normals = normals;
  54. vertexData.uvs = uvs;
  55. return vertexData;
  56. }
  57. /**
  58. * Creates the VertexData for a TiledGround by subdividing the ground into tiles
  59. * @param options an object used to set the following optional parameters for the Ground
  60. * @param options.xmin ground minimum X coordinate, default -1
  61. * @param options.zmin ground minimum Z coordinate, default -1
  62. * @param options.xmax ground maximum X coordinate, default 1
  63. * @param options.zmax ground maximum Z coordinate, default 1
  64. * @param options.subdivisions a javascript object {w: positive integer, h: positive integer}, `w` and `h` are the numbers of subdivisions on the ground width and height creating 'tiles', default {w: 6, h: 6}
  65. * @param options.subdivisions.w positive integer, default 6
  66. * @param options.subdivisions.h positive integer, default 6
  67. * @param options.precision a javascript object {w: positive integer, h: positive integer}, `w` and `h` are the numbers of subdivisions on the tile width and height, default {w: 2, h: 2}
  68. * @param options.precision.w positive integer, default 2
  69. * @param options.precision.h positive integer, default 2
  70. * @returns the VertexData of the TiledGround
  71. */
  72. export function CreateTiledGroundVertexData(options) {
  73. const xmin = options.xmin !== undefined && options.xmin !== null ? options.xmin : -1.0;
  74. const zmin = options.zmin !== undefined && options.zmin !== null ? options.zmin : -1.0;
  75. const xmax = options.xmax !== undefined && options.xmax !== null ? options.xmax : 1.0;
  76. const zmax = options.zmax !== undefined && options.zmax !== null ? options.zmax : 1.0;
  77. const subdivisions = options.subdivisions || { w: 1, h: 1 };
  78. const precision = options.precision || { w: 1, h: 1 };
  79. const indices = [];
  80. const positions = [];
  81. const normals = [];
  82. const uvs = [];
  83. let row, col, tileRow, tileCol;
  84. subdivisions.h = subdivisions.h < 1 ? 1 : subdivisions.h;
  85. subdivisions.w = subdivisions.w < 1 ? 1 : subdivisions.w;
  86. precision.w = precision.w < 1 ? 1 : precision.w;
  87. precision.h = precision.h < 1 ? 1 : precision.h;
  88. const tileSize = {
  89. w: (xmax - xmin) / subdivisions.w,
  90. h: (zmax - zmin) / subdivisions.h,
  91. };
  92. function applyTile(xTileMin, zTileMin, xTileMax, zTileMax) {
  93. // Indices
  94. const base = positions.length / 3;
  95. const rowLength = precision.w + 1;
  96. for (row = 0; row < precision.h; row++) {
  97. for (col = 0; col < precision.w; col++) {
  98. const square = [base + col + row * rowLength, base + (col + 1) + row * rowLength, base + (col + 1) + (row + 1) * rowLength, base + col + (row + 1) * rowLength];
  99. indices.push(square[1]);
  100. indices.push(square[2]);
  101. indices.push(square[3]);
  102. indices.push(square[0]);
  103. indices.push(square[1]);
  104. indices.push(square[3]);
  105. }
  106. }
  107. // Position, normals and uvs
  108. const position = Vector3.Zero();
  109. const normal = new Vector3(0, 1.0, 0);
  110. for (row = 0; row <= precision.h; row++) {
  111. position.z = (row * (zTileMax - zTileMin)) / precision.h + zTileMin;
  112. for (col = 0; col <= precision.w; col++) {
  113. position.x = (col * (xTileMax - xTileMin)) / precision.w + xTileMin;
  114. position.y = 0;
  115. positions.push(position.x, position.y, position.z);
  116. normals.push(normal.x, normal.y, normal.z);
  117. uvs.push(col / precision.w, row / precision.h);
  118. }
  119. }
  120. }
  121. for (tileRow = 0; tileRow < subdivisions.h; tileRow++) {
  122. for (tileCol = 0; tileCol < subdivisions.w; tileCol++) {
  123. applyTile(xmin + tileCol * tileSize.w, zmin + tileRow * tileSize.h, xmin + (tileCol + 1) * tileSize.w, zmin + (tileRow + 1) * tileSize.h);
  124. }
  125. }
  126. // Result
  127. const vertexData = new VertexData();
  128. vertexData.indices = indices;
  129. vertexData.positions = positions;
  130. vertexData.normals = normals;
  131. vertexData.uvs = uvs;
  132. return vertexData;
  133. }
  134. /**
  135. * Creates the VertexData of the Ground designed from a heightmap
  136. * @param options an object used to set the following parameters for the Ground, required and provided by CreateGroundFromHeightMap
  137. * @param options.width the width (x direction) of the ground
  138. * @param options.height the height (z direction) of the ground
  139. * @param options.subdivisions the number of subdivisions per side
  140. * @param options.minHeight the minimum altitude on the ground, optional, default 0
  141. * @param options.maxHeight the maximum altitude on the ground, optional default 1
  142. * @param options.colorFilter the filter to apply to the image pixel colors to compute the height, optional Color3, default (0.3, 0.59, 0.11)
  143. * @param options.buffer the array holding the image color data
  144. * @param options.bufferWidth the width of image
  145. * @param options.bufferHeight the height of image
  146. * @param options.alphaFilter Remove any data where the alpha channel is below this value, defaults 0 (all data visible)
  147. * @param options.heightBuffer a array of floats where the height data can be saved, if its length is greater than zero.
  148. * @returns the VertexData of the Ground designed from a heightmap
  149. */
  150. export function CreateGroundFromHeightMapVertexData(options) {
  151. const indices = [];
  152. const positions = [];
  153. const normals = [];
  154. const uvs = [];
  155. let row, col;
  156. const filter = options.colorFilter || new Color3(0.3, 0.59, 0.11);
  157. const alphaFilter = options.alphaFilter || 0.0;
  158. let invert = false;
  159. if (options.minHeight > options.maxHeight) {
  160. invert = true;
  161. const temp = options.maxHeight;
  162. options.maxHeight = options.minHeight;
  163. options.minHeight = temp;
  164. }
  165. // Vertices
  166. for (row = 0; row <= options.subdivisions; row++) {
  167. for (col = 0; col <= options.subdivisions; col++) {
  168. const position = new Vector3((col * options.width) / options.subdivisions - options.width / 2.0, 0, ((options.subdivisions - row) * options.height) / options.subdivisions - options.height / 2.0);
  169. // Compute height
  170. const heightMapX = (((position.x + options.width / 2) / options.width) * (options.bufferWidth - 1)) | 0;
  171. const heightMapY = ((1.0 - (position.z + options.height / 2) / options.height) * (options.bufferHeight - 1)) | 0;
  172. const pos = (heightMapX + heightMapY * options.bufferWidth) * 4;
  173. let r = options.buffer[pos] / 255.0;
  174. let g = options.buffer[pos + 1] / 255.0;
  175. let b = options.buffer[pos + 2] / 255.0;
  176. const a = options.buffer[pos + 3] / 255.0;
  177. if (invert) {
  178. r = 1.0 - r;
  179. g = 1.0 - g;
  180. b = 1.0 - b;
  181. }
  182. const gradient = r * filter.r + g * filter.g + b * filter.b;
  183. // If our alpha channel is not within our filter then we will assign a 'special' height
  184. // Then when building the indices, we will ignore any vertex that is using the special height
  185. if (a >= alphaFilter) {
  186. position.y = options.minHeight + (options.maxHeight - options.minHeight) * gradient;
  187. }
  188. else {
  189. position.y = options.minHeight - Epsilon; // We can't have a height below minHeight, normally.
  190. }
  191. if (options.heightBuffer) {
  192. // set the height buffer information in row major order.
  193. options.heightBuffer[row * (options.subdivisions + 1) + col] = position.y;
  194. }
  195. // Add vertex
  196. positions.push(position.x, position.y, position.z);
  197. normals.push(0, 0, 0);
  198. uvs.push(col / options.subdivisions, 1.0 - row / options.subdivisions);
  199. }
  200. }
  201. // Indices
  202. for (row = 0; row < options.subdivisions; row++) {
  203. for (col = 0; col < options.subdivisions; col++) {
  204. // Calculate Indices
  205. const idx1 = col + 1 + (row + 1) * (options.subdivisions + 1);
  206. const idx2 = col + 1 + row * (options.subdivisions + 1);
  207. const idx3 = col + row * (options.subdivisions + 1);
  208. const idx4 = col + (row + 1) * (options.subdivisions + 1);
  209. // Check that all indices are visible (based on our special height)
  210. // Only display the vertex if all Indices are visible
  211. // Positions are stored x,y,z for each vertex, hence the * 3 and + 1 for height
  212. const isVisibleIdx1 = positions[idx1 * 3 + 1] >= options.minHeight;
  213. const isVisibleIdx2 = positions[idx2 * 3 + 1] >= options.minHeight;
  214. const isVisibleIdx3 = positions[idx3 * 3 + 1] >= options.minHeight;
  215. if (isVisibleIdx1 && isVisibleIdx2 && isVisibleIdx3) {
  216. indices.push(idx1);
  217. indices.push(idx2);
  218. indices.push(idx3);
  219. }
  220. const isVisibleIdx4 = positions[idx4 * 3 + 1] >= options.minHeight;
  221. if (isVisibleIdx4 && isVisibleIdx1 && isVisibleIdx3) {
  222. indices.push(idx4);
  223. indices.push(idx1);
  224. indices.push(idx3);
  225. }
  226. }
  227. }
  228. // Normals
  229. VertexData.ComputeNormals(positions, indices, normals);
  230. // Result
  231. const vertexData = new VertexData();
  232. vertexData.indices = indices;
  233. vertexData.positions = positions;
  234. vertexData.normals = normals;
  235. vertexData.uvs = uvs;
  236. return vertexData;
  237. }
  238. /**
  239. * Creates a ground mesh
  240. * @param name defines the name of the mesh
  241. * @param options defines the options used to create the mesh
  242. * @param options.width set the width size (float, default 1)
  243. * @param options.height set the height size (float, default 1)
  244. * @param options.subdivisions sets the number of subdivision per side (default 1)
  245. * @param options.subdivisionsX sets the number of subdivision on the X axis (overrides subdivisions)
  246. * @param options.subdivisionsY sets the number of subdivision on the Y axis (overrides subdivisions)
  247. * @param options.updatable defines if the mesh must be flagged as updatable (default false)
  248. * @param scene defines the hosting scene
  249. * @returns the ground mesh
  250. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#ground
  251. */
  252. export function CreateGround(name, options = {}, scene) {
  253. const ground = new GroundMesh(name, scene);
  254. ground._setReady(false);
  255. ground._subdivisionsX = options.subdivisionsX || options.subdivisions || 1;
  256. ground._subdivisionsY = options.subdivisionsY || options.subdivisions || 1;
  257. ground._width = options.width || 1;
  258. ground._height = options.height || 1;
  259. ground._maxX = ground._width / 2;
  260. ground._maxZ = ground._height / 2;
  261. ground._minX = -ground._maxX;
  262. ground._minZ = -ground._maxZ;
  263. const vertexData = CreateGroundVertexData(options);
  264. vertexData.applyToMesh(ground, options.updatable);
  265. ground._setReady(true);
  266. return ground;
  267. }
  268. /**
  269. * Creates a tiled ground mesh
  270. * @param name defines the name of the mesh
  271. * @param options defines the options used to create the mesh
  272. * @param options.xmin ground minimum X coordinate (float, default -1)
  273. * @param options.zmin ground minimum Z coordinate (float, default -1)
  274. * @param options.xmax ground maximum X coordinate (float, default 1)
  275. * @param options.zmax ground maximum Z coordinate (float, default 1)
  276. * @param options.subdivisions a javascript object `{w: positive integer, h: positive integer}` (default `{w: 6, h: 6}`). `w` and `h` are the numbers of subdivisions on the ground width and height. Each subdivision is called a tile
  277. * @param options.subdivisions.w positive integer, default 6
  278. * @param options.subdivisions.h positive integer, default 6
  279. * @param options.precision a javascript object `{w: positive integer, h: positive integer}` (default `{w: 2, h: 2}`). `w` and `h` are the numbers of subdivisions on the ground width and height of each tile
  280. * @param options.precision.w positive integer, default 2
  281. * @param options.precision.h positive integer, default 2
  282. * @param options.updatable boolean, default false, true if the mesh must be flagged as updatable
  283. * @param scene defines the hosting scene
  284. * @returns the tiled ground mesh
  285. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#tiled-ground
  286. */
  287. export function CreateTiledGround(name, options, scene = null) {
  288. const tiledGround = new Mesh(name, scene);
  289. const vertexData = CreateTiledGroundVertexData(options);
  290. vertexData.applyToMesh(tiledGround, options.updatable);
  291. return tiledGround;
  292. }
  293. /**
  294. * Creates a ground mesh from a height map. The height map download can take some frames,
  295. * so the mesh is not immediately ready. To wait for the mesh to be completely built,
  296. * you should use the `onReady` callback option.
  297. * @param name defines the name of the mesh
  298. * @param url sets the URL of the height map image resource.
  299. * @param options defines the options used to create the mesh
  300. * @param options.width sets the ground width size (positive float, default 10)
  301. * @param options.height sets the ground height size (positive float, default 10)
  302. * @param options.subdivisions sets the number of subdivision per side (positive integer, default 1)
  303. * @param options.minHeight is the minimum altitude on the ground (float, default 0)
  304. * @param options.maxHeight is the maximum altitude on the ground (float, default 1)
  305. * @param options.colorFilter is the filter to apply to the image pixel colors to compute the height (optional Color3, default (0.3, 0.59, 0.11) )
  306. * @param options.alphaFilter will filter any data where the alpha channel is below this value, defaults 0 (all data visible)
  307. * @param options.updatable defines if the mesh must be flagged as updatable
  308. * @param options.onReady is a javascript callback function that will be called once the mesh is just built (the height map download can last some time)
  309. * @param options.onError is a javascript callback function that will be called if there is an error
  310. * @param options.passHeightBufferInCallback a boolean that indicates if the calculated height data will be passed in the onReady callback. Useful if you need the height data for physics, for example.
  311. * @param scene defines the hosting scene
  312. * @returns the ground mesh
  313. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set/height_map
  314. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#ground-from-a-height-map
  315. */
  316. export function CreateGroundFromHeightMap(name, url, options = {}, scene = null) {
  317. const width = options.width || 10.0;
  318. const height = options.height || 10.0;
  319. const subdivisions = options.subdivisions || 1 | 0;
  320. const minHeight = options.minHeight || 0.0;
  321. const maxHeight = options.maxHeight || 1.0;
  322. const filter = options.colorFilter || new Color3(0.3, 0.59, 0.11);
  323. const alphaFilter = options.alphaFilter || 0.0;
  324. const updatable = options.updatable;
  325. const onReady = options.onReady;
  326. scene = scene || EngineStore.LastCreatedScene;
  327. const ground = new GroundMesh(name, scene);
  328. ground._subdivisionsX = subdivisions;
  329. ground._subdivisionsY = subdivisions;
  330. ground._width = width;
  331. ground._height = height;
  332. ground._maxX = ground._width / 2.0;
  333. ground._maxZ = ground._height / 2.0;
  334. ground._minX = -ground._maxX;
  335. ground._minZ = -ground._maxZ;
  336. ground._setReady(false);
  337. let heightBuffer;
  338. if (options.passHeightBufferInCallback) {
  339. heightBuffer = new Float32Array((subdivisions + 1) * (subdivisions + 1));
  340. }
  341. const onBufferLoaded = (buffer, bufferWidth, bufferHeight) => {
  342. const vertexData = CreateGroundFromHeightMapVertexData({
  343. width: width,
  344. height: height,
  345. subdivisions: subdivisions,
  346. minHeight: minHeight,
  347. maxHeight: maxHeight,
  348. colorFilter: filter,
  349. buffer: buffer,
  350. bufferWidth: bufferWidth,
  351. bufferHeight: bufferHeight,
  352. alphaFilter: alphaFilter,
  353. heightBuffer,
  354. });
  355. vertexData.applyToMesh(ground, updatable);
  356. //execute ready callback, if set
  357. if (onReady) {
  358. onReady(ground, heightBuffer);
  359. }
  360. ground._setReady(true);
  361. };
  362. if (typeof url === "string") {
  363. const onload = (img) => {
  364. const bufferWidth = img.width;
  365. const bufferHeight = img.height;
  366. if (scene.isDisposed) {
  367. return;
  368. }
  369. const buffer = scene?.getEngine().resizeImageBitmap(img, bufferWidth, bufferHeight);
  370. onBufferLoaded(buffer, bufferWidth, bufferHeight);
  371. };
  372. Tools.LoadImage(url, onload, options.onError ? options.onError : () => { }, scene.offlineProvider);
  373. }
  374. else {
  375. onBufferLoaded(url.data, url.width, url.height);
  376. }
  377. return ground;
  378. }
  379. /**
  380. * Class containing static functions to help procedurally build meshes
  381. * @deprecated use the functions directly from the module
  382. */
  383. export const GroundBuilder = {
  384. // eslint-disable-next-line @typescript-eslint/naming-convention
  385. CreateGround,
  386. // eslint-disable-next-line @typescript-eslint/naming-convention
  387. CreateGroundFromHeightMap,
  388. // eslint-disable-next-line @typescript-eslint/naming-convention
  389. CreateTiledGround,
  390. };
  391. VertexData.CreateGround = CreateGroundVertexData;
  392. VertexData.CreateTiledGround = CreateTiledGroundVertexData;
  393. VertexData.CreateGroundFromHeightMap = CreateGroundFromHeightMapVertexData;
  394. Mesh.CreateGround = (name, width, height, subdivisions, scene, updatable) => {
  395. const options = {
  396. width,
  397. height,
  398. subdivisions,
  399. updatable,
  400. };
  401. return CreateGround(name, options, scene);
  402. };
  403. Mesh.CreateTiledGround = (name, xmin, zmin, xmax, zmax, subdivisions, precision, scene, updatable) => {
  404. const options = {
  405. xmin,
  406. zmin,
  407. xmax,
  408. zmax,
  409. subdivisions,
  410. precision,
  411. updatable,
  412. };
  413. return CreateTiledGround(name, options, scene);
  414. };
  415. Mesh.CreateGroundFromHeightMap = (name, url, width, height, subdivisions, minHeight, maxHeight, scene, updatable, onReady, alphaFilter) => {
  416. const options = {
  417. width,
  418. height,
  419. subdivisions,
  420. minHeight,
  421. maxHeight,
  422. updatable,
  423. onReady,
  424. alphaFilter,
  425. };
  426. return CreateGroundFromHeightMap(name, url, options, scene);
  427. };
  428. //# sourceMappingURL=groundBuilder.js.map