ribbonBuilder.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import { TmpVectors } from "../../Maths/math.vector.js";
  2. import { Mesh, _CreationDataStorage } from "../mesh.js";
  3. import { VertexBuffer } from "../../Buffers/buffer.js";
  4. import { VertexData } from "../mesh.vertexData.js";
  5. import { CompatibilityOptions } from "../../Compat/compatibilityOptions.js";
  6. /**
  7. * Creates the VertexData for a Ribbon
  8. * @param options an object used to set the following optional parameters for the ribbon, required but can be empty
  9. * * pathArray array of paths, each of which an array of successive Vector3
  10. * * closeArray creates a seam between the first and the last paths of the pathArray, optional, default false
  11. * * closePath creates a seam between the first and the last points of each path of the path array, optional, default false
  12. * * offset a positive integer, only used when pathArray contains a single path (offset = 10 means the point 1 is joined to the point 11), default rounded half size of the pathArray length
  13. * * sideOrientation optional and takes the values : Mesh.FRONTSIDE (default), Mesh.BACKSIDE or Mesh.DOUBLESIDE
  14. * * frontUvs only usable when you create a double-sided mesh, used to choose what parts of the texture image to crop and apply on the front side, optional, default vector4 (0, 0, 1, 1)
  15. * * backUVs only usable when you create a double-sided mesh, used to choose what parts of the texture image to crop and apply on the back side, optional, default vector4 (0, 0, 1, 1)
  16. * * invertUV swaps in the U and V coordinates when applying a texture, optional, default false
  17. * * uvs a linear array, of length 2 * number of vertices, of custom UV values, optional
  18. * * colors a linear array, of length 4 * number of vertices, of custom color values, optional
  19. * @returns the VertexData of the ribbon
  20. */
  21. export function CreateRibbonVertexData(options) {
  22. let pathArray = options.pathArray;
  23. const closeArray = options.closeArray || false;
  24. const closePath = options.closePath || false;
  25. const invertUV = options.invertUV || false;
  26. const defaultOffset = Math.floor(pathArray[0].length / 2);
  27. let offset = options.offset || defaultOffset;
  28. offset = offset > defaultOffset ? defaultOffset : Math.floor(offset); // offset max allowed : defaultOffset
  29. const sideOrientation = options.sideOrientation === 0 ? 0 : options.sideOrientation || VertexData.DEFAULTSIDE;
  30. const customUV = options.uvs;
  31. const customColors = options.colors;
  32. const positions = [];
  33. const indices = [];
  34. const normals = [];
  35. const uvs = [];
  36. const us = []; // us[path_id] = [uDist1, uDist2, uDist3 ... ] distances between points on path path_id
  37. const vs = []; // vs[i] = [vDist1, vDist2, vDist3, ... ] distances between points i of consecutive paths from pathArray
  38. const uTotalDistance = []; // uTotalDistance[p] : total distance of path p
  39. const vTotalDistance = []; // vTotalDistance[i] : total distance between points i of first and last path from pathArray
  40. let minlg; // minimal length among all paths from pathArray
  41. const lg = []; // array of path lengths : nb of vertex per path
  42. const idx = []; // array of path indexes : index of each path (first vertex) in the total vertex number
  43. let p; // path iterator
  44. let i; // point iterator
  45. let j; // point iterator
  46. // if single path in pathArray
  47. if (pathArray.length < 2) {
  48. const ar1 = [];
  49. const ar2 = [];
  50. for (i = 0; i < pathArray[0].length - offset; i++) {
  51. ar1.push(pathArray[0][i]);
  52. ar2.push(pathArray[0][i + offset]);
  53. }
  54. pathArray = [ar1, ar2];
  55. }
  56. // positions and horizontal distances (u)
  57. let idc = 0;
  58. const closePathCorr = closePath ? 1 : 0; // the final index will be +1 if closePath
  59. const closeArrayCorr = closeArray ? 1 : 0;
  60. let path;
  61. let l;
  62. minlg = pathArray[0].length;
  63. let vectlg;
  64. let dist;
  65. for (p = 0; p < pathArray.length + closeArrayCorr; p++) {
  66. uTotalDistance[p] = 0;
  67. us[p] = [0];
  68. path = p === pathArray.length ? pathArray[0] : pathArray[p];
  69. l = path.length;
  70. minlg = minlg < l ? minlg : l;
  71. j = 0;
  72. while (j < l) {
  73. positions.push(path[j].x, path[j].y, path[j].z);
  74. if (j > 0) {
  75. vectlg = path[j].subtract(path[j - 1]).length();
  76. dist = vectlg + uTotalDistance[p];
  77. us[p].push(dist);
  78. uTotalDistance[p] = dist;
  79. }
  80. j++;
  81. }
  82. if (closePath) {
  83. // an extra hidden vertex is added in the "positions" array
  84. j--;
  85. positions.push(path[0].x, path[0].y, path[0].z);
  86. vectlg = path[j].subtract(path[0]).length();
  87. dist = vectlg + uTotalDistance[p];
  88. us[p].push(dist);
  89. uTotalDistance[p] = dist;
  90. }
  91. lg[p] = l + closePathCorr;
  92. idx[p] = idc;
  93. idc += l + closePathCorr;
  94. }
  95. // vertical distances (v)
  96. let path1;
  97. let path2;
  98. let vertex1 = null;
  99. let vertex2 = null;
  100. for (i = 0; i < minlg + closePathCorr; i++) {
  101. vTotalDistance[i] = 0;
  102. vs[i] = [0];
  103. for (p = 0; p < pathArray.length - 1 + closeArrayCorr; p++) {
  104. path1 = pathArray[p];
  105. path2 = p === pathArray.length - 1 ? pathArray[0] : pathArray[p + 1];
  106. if (i === minlg) {
  107. // closePath
  108. vertex1 = path1[0];
  109. vertex2 = path2[0];
  110. }
  111. else {
  112. vertex1 = path1[i];
  113. vertex2 = path2[i];
  114. }
  115. vectlg = vertex2.subtract(vertex1).length();
  116. dist = vectlg + vTotalDistance[i];
  117. vs[i].push(dist);
  118. vTotalDistance[i] = dist;
  119. }
  120. }
  121. // uvs
  122. let u;
  123. let v;
  124. if (customUV) {
  125. for (p = 0; p < customUV.length; p++) {
  126. uvs.push(customUV[p].x, CompatibilityOptions.UseOpenGLOrientationForUV ? 1.0 - customUV[p].y : customUV[p].y);
  127. }
  128. }
  129. else {
  130. for (p = 0; p < pathArray.length + closeArrayCorr; p++) {
  131. for (i = 0; i < minlg + closePathCorr; i++) {
  132. u = uTotalDistance[p] != 0.0 ? us[p][i] / uTotalDistance[p] : 0.0;
  133. v = vTotalDistance[i] != 0.0 ? vs[i][p] / vTotalDistance[i] : 0.0;
  134. if (invertUV) {
  135. uvs.push(v, u);
  136. }
  137. else {
  138. uvs.push(u, CompatibilityOptions.UseOpenGLOrientationForUV ? 1.0 - v : v);
  139. }
  140. }
  141. }
  142. }
  143. // indices
  144. p = 0; // path index
  145. let pi = 0; // positions array index
  146. let l1 = lg[p] - 1; // path1 length
  147. let l2 = lg[p + 1] - 1; // path2 length
  148. let min = l1 < l2 ? l1 : l2; // current path stop index
  149. let shft = idx[1] - idx[0]; // shift
  150. const path1nb = lg.length - 1; // number of path1 to iterate on
  151. while (pi <= min && p < path1nb) {
  152. // stay under min and don't go over next to last path
  153. // draw two triangles between path1 (p1) and path2 (p2) : (p1.pi, p2.pi, p1.pi+1) and (p2.pi+1, p1.pi+1, p2.pi) clockwise
  154. indices.push(pi, pi + shft, pi + 1);
  155. indices.push(pi + shft + 1, pi + 1, pi + shft);
  156. pi += 1;
  157. if (pi === min) {
  158. // if end of one of two consecutive paths reached, go to next existing path
  159. p++;
  160. shft = idx[p + 1] - idx[p];
  161. l1 = lg[p] - 1;
  162. l2 = lg[p + 1] - 1;
  163. pi = idx[p];
  164. min = l1 < l2 ? l1 + pi : l2 + pi;
  165. }
  166. }
  167. // normals
  168. VertexData.ComputeNormals(positions, indices, normals);
  169. if (closePath) {
  170. // update both the first and last vertex normals to their average value
  171. let indexFirst = 0;
  172. let indexLast = 0;
  173. for (p = 0; p < pathArray.length; p++) {
  174. indexFirst = idx[p] * 3;
  175. if (p + 1 < pathArray.length) {
  176. indexLast = (idx[p + 1] - 1) * 3;
  177. }
  178. else {
  179. indexLast = normals.length - 3;
  180. }
  181. normals[indexFirst] = (normals[indexFirst] + normals[indexLast]) * 0.5;
  182. normals[indexFirst + 1] = (normals[indexFirst + 1] + normals[indexLast + 1]) * 0.5;
  183. normals[indexFirst + 2] = (normals[indexFirst + 2] + normals[indexLast + 2]) * 0.5;
  184. const l = Math.sqrt(normals[indexFirst] * normals[indexFirst] + normals[indexFirst + 1] * normals[indexFirst + 1] + normals[indexFirst + 2] * normals[indexFirst + 2]);
  185. normals[indexFirst] /= l;
  186. normals[indexFirst + 1] /= l;
  187. normals[indexFirst + 2] /= l;
  188. normals[indexLast] = normals[indexFirst];
  189. normals[indexLast + 1] = normals[indexFirst + 1];
  190. normals[indexLast + 2] = normals[indexFirst + 2];
  191. }
  192. }
  193. if (closeArray) {
  194. let indexFirst = idx[0] * 3;
  195. let indexLast = idx[pathArray.length] * 3;
  196. for (i = 0; i < minlg + closePathCorr; i++) {
  197. normals[indexFirst] = (normals[indexFirst] + normals[indexLast]) * 0.5;
  198. normals[indexFirst + 1] = (normals[indexFirst + 1] + normals[indexLast + 1]) * 0.5;
  199. normals[indexFirst + 2] = (normals[indexFirst + 2] + normals[indexLast + 2]) * 0.5;
  200. const l = Math.sqrt(normals[indexFirst] * normals[indexFirst] + normals[indexFirst + 1] * normals[indexFirst + 1] + normals[indexFirst + 2] * normals[indexFirst + 2]);
  201. normals[indexFirst] /= l;
  202. normals[indexFirst + 1] /= l;
  203. normals[indexFirst + 2] /= l;
  204. normals[indexLast] = normals[indexFirst];
  205. normals[indexLast + 1] = normals[indexFirst + 1];
  206. normals[indexLast + 2] = normals[indexFirst + 2];
  207. indexFirst += 3;
  208. indexLast += 3;
  209. }
  210. }
  211. // sides
  212. VertexData._ComputeSides(sideOrientation, positions, indices, normals, uvs, options.frontUVs, options.backUVs);
  213. // Colors
  214. let colors = null;
  215. if (customColors) {
  216. colors = new Float32Array(customColors.length * 4);
  217. for (let c = 0; c < customColors.length; c++) {
  218. colors[c * 4] = customColors[c].r;
  219. colors[c * 4 + 1] = customColors[c].g;
  220. colors[c * 4 + 2] = customColors[c].b;
  221. colors[c * 4 + 3] = customColors[c].a;
  222. }
  223. }
  224. // Result
  225. const vertexData = new VertexData();
  226. const positions32 = new Float32Array(positions);
  227. const normals32 = new Float32Array(normals);
  228. const uvs32 = new Float32Array(uvs);
  229. vertexData.indices = indices;
  230. vertexData.positions = positions32;
  231. vertexData.normals = normals32;
  232. vertexData.uvs = uvs32;
  233. if (colors) {
  234. vertexData.set(colors, VertexBuffer.ColorKind);
  235. }
  236. if (closePath) {
  237. vertexData._idx = idx;
  238. }
  239. return vertexData;
  240. }
  241. /**
  242. * Creates a ribbon mesh. The ribbon is a parametric shape. It has no predefined shape. Its final shape will depend on the input parameters
  243. * * The parameter `pathArray` is a required array of paths, what are each an array of successive Vector3. The pathArray parameter depicts the ribbon geometry
  244. * * The parameter `closeArray` (boolean, default false) creates a seam between the first and the last paths of the path array
  245. * * The parameter `closePath` (boolean, default false) creates a seam between the first and the last points of each path of the path array
  246. * * The parameter `offset` (positive integer, default : rounded half size of the pathArray length), is taken in account only if the `pathArray` is containing a single path
  247. * * It's the offset to join the points from the same path. Ex : offset = 10 means the point 1 is joined to the point 11
  248. * * The optional parameter `instance` is an instance of an existing Ribbon object to be updated with the passed `pathArray` parameter : https://doc.babylonjs.com/features/featuresDeepDive/mesh/dynamicMeshMorph#ribbon
  249. * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE
  250. * * If you create a double-sided mesh, you can choose what parts of the texture image to crop and stick respectively on the front and the back sides with the parameters `frontUVs` and `backUVs` (Vector4). Detail here : https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#side-orientation
  251. * * The optional parameter `invertUV` (boolean, default false) swaps in the geometry the U and V coordinates to apply a texture
  252. * * The parameter `uvs` is an optional flat array of `Vector2` to update/set each ribbon vertex with its own custom UV values instead of the computed ones
  253. * * The parameters `colors` is an optional flat array of `Color4` to set/update each ribbon vertex with its own custom color values
  254. * * Note that if you use the parameters `uvs` or `colors`, the passed arrays must be populated with the right number of elements, it is to say the number of ribbon vertices. Remember that if you set `closePath` to `true`, there's one extra vertex per path in the geometry
  255. * * Moreover, you can use the parameter `color` with `instance` (to update the ribbon), only if you previously used it at creation time
  256. * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created
  257. * @param name defines the name of the mesh
  258. * @param options defines the options used to create the mesh
  259. * @param scene defines the hosting scene
  260. * @returns the ribbon mesh
  261. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/param/ribbon_extra
  262. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/param
  263. */
  264. export function CreateRibbon(name, options, scene = null) {
  265. const pathArray = options.pathArray;
  266. const closeArray = options.closeArray;
  267. const closePath = options.closePath;
  268. const sideOrientation = Mesh._GetDefaultSideOrientation(options.sideOrientation);
  269. const instance = options.instance;
  270. const updatable = options.updatable;
  271. if (instance) {
  272. // existing ribbon instance update
  273. // positionFunction : ribbon case
  274. // only pathArray and sideOrientation parameters are taken into account for positions update
  275. const minimum = TmpVectors.Vector3[0].setAll(Number.MAX_VALUE);
  276. const maximum = TmpVectors.Vector3[1].setAll(-Number.MAX_VALUE);
  277. const positionFunction = (positions) => {
  278. let minlg = pathArray[0].length;
  279. const mesh = instance;
  280. let i = 0;
  281. const ns = mesh._originalBuilderSideOrientation === Mesh.DOUBLESIDE ? 2 : 1;
  282. for (let si = 1; si <= ns; ++si) {
  283. for (let p = 0; p < pathArray.length; ++p) {
  284. const path = pathArray[p];
  285. const l = path.length;
  286. minlg = minlg < l ? minlg : l;
  287. for (let j = 0; j < minlg; ++j) {
  288. const pathPoint = path[j];
  289. positions[i] = pathPoint.x;
  290. positions[i + 1] = pathPoint.y;
  291. positions[i + 2] = pathPoint.z;
  292. minimum.minimizeInPlaceFromFloats(pathPoint.x, pathPoint.y, pathPoint.z);
  293. maximum.maximizeInPlaceFromFloats(pathPoint.x, pathPoint.y, pathPoint.z);
  294. i += 3;
  295. }
  296. if (mesh._creationDataStorage && mesh._creationDataStorage.closePath) {
  297. const pathPoint = path[0];
  298. positions[i] = pathPoint.x;
  299. positions[i + 1] = pathPoint.y;
  300. positions[i + 2] = pathPoint.z;
  301. i += 3;
  302. }
  303. }
  304. }
  305. };
  306. const positions = instance.getVerticesData(VertexBuffer.PositionKind);
  307. positionFunction(positions);
  308. if (instance.hasBoundingInfo) {
  309. instance.getBoundingInfo().reConstruct(minimum, maximum, instance._worldMatrix);
  310. }
  311. else {
  312. instance.buildBoundingInfo(minimum, maximum, instance._worldMatrix);
  313. }
  314. instance.updateVerticesData(VertexBuffer.PositionKind, positions, false, false);
  315. if (options.colors) {
  316. const colors = instance.getVerticesData(VertexBuffer.ColorKind);
  317. for (let c = 0, colorIndex = 0; c < options.colors.length; c++, colorIndex += 4) {
  318. const color = options.colors[c];
  319. colors[colorIndex] = color.r;
  320. colors[colorIndex + 1] = color.g;
  321. colors[colorIndex + 2] = color.b;
  322. colors[colorIndex + 3] = color.a;
  323. }
  324. instance.updateVerticesData(VertexBuffer.ColorKind, colors, false, false);
  325. }
  326. if (options.uvs) {
  327. const uvs = instance.getVerticesData(VertexBuffer.UVKind);
  328. for (let i = 0; i < options.uvs.length; i++) {
  329. uvs[i * 2] = options.uvs[i].x;
  330. uvs[i * 2 + 1] = CompatibilityOptions.UseOpenGLOrientationForUV ? 1.0 - options.uvs[i].y : options.uvs[i].y;
  331. }
  332. instance.updateVerticesData(VertexBuffer.UVKind, uvs, false, false);
  333. }
  334. if (!instance.areNormalsFrozen || instance.isFacetDataEnabled) {
  335. const indices = instance.getIndices();
  336. const normals = instance.getVerticesData(VertexBuffer.NormalKind);
  337. const params = instance.isFacetDataEnabled ? instance.getFacetDataParameters() : null;
  338. VertexData.ComputeNormals(positions, indices, normals, params);
  339. if (instance._creationDataStorage && instance._creationDataStorage.closePath) {
  340. let indexFirst = 0;
  341. let indexLast = 0;
  342. for (let p = 0; p < pathArray.length; p++) {
  343. indexFirst = instance._creationDataStorage.idx[p] * 3;
  344. if (p + 1 < pathArray.length) {
  345. indexLast = (instance._creationDataStorage.idx[p + 1] - 1) * 3;
  346. }
  347. else {
  348. indexLast = normals.length - 3;
  349. }
  350. normals[indexFirst] = (normals[indexFirst] + normals[indexLast]) * 0.5;
  351. normals[indexFirst + 1] = (normals[indexFirst + 1] + normals[indexLast + 1]) * 0.5;
  352. normals[indexFirst + 2] = (normals[indexFirst + 2] + normals[indexLast + 2]) * 0.5;
  353. normals[indexLast] = normals[indexFirst];
  354. normals[indexLast + 1] = normals[indexFirst + 1];
  355. normals[indexLast + 2] = normals[indexFirst + 2];
  356. }
  357. }
  358. if (!instance.areNormalsFrozen) {
  359. instance.updateVerticesData(VertexBuffer.NormalKind, normals, false, false);
  360. }
  361. }
  362. return instance;
  363. }
  364. else {
  365. // new ribbon creation
  366. const ribbon = new Mesh(name, scene);
  367. ribbon._originalBuilderSideOrientation = sideOrientation;
  368. ribbon._creationDataStorage = new _CreationDataStorage();
  369. const vertexData = CreateRibbonVertexData(options);
  370. if (closePath) {
  371. ribbon._creationDataStorage.idx = vertexData._idx;
  372. }
  373. ribbon._creationDataStorage.closePath = closePath;
  374. ribbon._creationDataStorage.closeArray = closeArray;
  375. vertexData.applyToMesh(ribbon, updatable);
  376. return ribbon;
  377. }
  378. }
  379. /**
  380. * Class containing static functions to help procedurally build meshes
  381. * @deprecated use CreateRibbon directly
  382. */
  383. export const RibbonBuilder = {
  384. // eslint-disable-next-line @typescript-eslint/naming-convention
  385. CreateRibbon,
  386. };
  387. VertexData.CreateRibbon = CreateRibbonVertexData;
  388. Mesh.CreateRibbon = (name, pathArray, closeArray = false, closePath, offset, scene, updatable = false, sideOrientation, instance) => {
  389. return CreateRibbon(name, {
  390. pathArray: pathArray,
  391. closeArray: closeArray,
  392. closePath: closePath,
  393. offset: offset,
  394. updatable: updatable,
  395. sideOrientation: sideOrientation,
  396. instance: instance,
  397. }, scene);
  398. };
  399. //# sourceMappingURL=ribbonBuilder.js.map