cylinderBuilder.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { Vector4, Vector3, Vector2 } from "../../Maths/math.vector.js";
  2. import { Color4 } from "../../Maths/math.color.js";
  3. import { Mesh } from "../mesh.js";
  4. import { VertexData } from "../mesh.vertexData.js";
  5. import { Scene } from "../../scene.js";
  6. import { Axis } from "../../Maths/math.axis.js";
  7. import { CompatibilityOptions } from "../../Compat/compatibilityOptions.js";
  8. /**
  9. * Creates the VertexData for a cylinder, cone or prism
  10. * @param options an object used to set the following optional parameters for the box, required but can be empty
  11. * * height sets the height (y direction) of the cylinder, optional, default 2
  12. * * diameterTop sets the diameter of the top of the cone, overwrites diameter, optional, default diameter
  13. * * diameterBottom sets the diameter of the bottom of the cone, overwrites diameter, optional, default diameter
  14. * * diameter sets the diameter of the top and bottom of the cone, optional default 1
  15. * * tessellation the number of prism sides, 3 for a triangular prism, optional, default 24
  16. * * subdivisions` the number of rings along the cylinder height, optional, default 1
  17. * * arc a number from 0 to 1, to create an unclosed cylinder based on the fraction of the circumference given by the arc value, optional, default 1
  18. * * faceColors an array of Color3 elements used to set different colors to the top, rings and bottom respectively
  19. * * faceUV an array of Vector4 elements used to set different images to the top, rings and bottom respectively
  20. * * hasRings when true makes each subdivision independently treated as a face for faceUV and faceColors, optional, default false
  21. * * enclose when true closes an open cylinder by adding extra flat faces between the height axis and vertical edges, think cut cake
  22. * * sideOrientation optional and takes the values : Mesh.FRONTSIDE (default), Mesh.BACKSIDE or Mesh.DOUBLESIDE
  23. * * 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)
  24. * * 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)
  25. * @returns the VertexData of the cylinder, cone or prism
  26. */
  27. // eslint-disable-next-line @typescript-eslint/naming-convention
  28. export function CreateCylinderVertexData(options) {
  29. const height = options.height || 2;
  30. let diameterTop = options.diameterTop === 0 ? 0 : options.diameterTop || options.diameter || 1;
  31. let diameterBottom = options.diameterBottom === 0 ? 0 : options.diameterBottom || options.diameter || 1;
  32. diameterTop = diameterTop || 0.00001; // Prevent broken normals
  33. diameterBottom = diameterBottom || 0.00001; // Prevent broken normals
  34. const tessellation = (options.tessellation || 24) | 0;
  35. const subdivisions = (options.subdivisions || 1) | 0;
  36. const hasRings = options.hasRings ? true : false;
  37. const enclose = options.enclose ? true : false;
  38. const cap = options.cap === 0 ? 0 : options.cap || Mesh.CAP_ALL;
  39. const arc = options.arc && (options.arc <= 0 || options.arc > 1) ? 1.0 : options.arc || 1.0;
  40. const sideOrientation = options.sideOrientation === 0 ? 0 : options.sideOrientation || VertexData.DEFAULTSIDE;
  41. const faceUV = options.faceUV || new Array(3);
  42. const faceColors = options.faceColors;
  43. // default face colors and UV if undefined
  44. const quadNb = arc !== 1 && enclose ? 2 : 0;
  45. const ringNb = hasRings ? subdivisions : 1;
  46. const surfaceNb = 2 + (1 + quadNb) * ringNb;
  47. let f;
  48. for (f = 0; f < surfaceNb; f++) {
  49. if (faceColors && faceColors[f] === undefined) {
  50. faceColors[f] = new Color4(1, 1, 1, 1);
  51. }
  52. }
  53. for (f = 0; f < surfaceNb; f++) {
  54. if (faceUV && faceUV[f] === undefined) {
  55. faceUV[f] = new Vector4(0, 0, 1, 1);
  56. }
  57. }
  58. const indices = [];
  59. const positions = [];
  60. const normals = [];
  61. const uvs = [];
  62. const colors = [];
  63. const angleStep = (Math.PI * 2 * arc) / tessellation;
  64. let angle;
  65. let h;
  66. let radius;
  67. const tan = (diameterBottom - diameterTop) / 2 / height;
  68. const ringVertex = Vector3.Zero();
  69. const ringNormal = Vector3.Zero();
  70. const ringFirstVertex = Vector3.Zero();
  71. const ringFirstNormal = Vector3.Zero();
  72. const quadNormal = Vector3.Zero();
  73. const Y = Axis.Y;
  74. // positions, normals, uvs
  75. let i;
  76. let j;
  77. let r;
  78. let ringIdx = 1;
  79. let s = 1; // surface index
  80. let cs = 0;
  81. let v = 0;
  82. for (i = 0; i <= subdivisions; i++) {
  83. h = i / subdivisions;
  84. radius = (h * (diameterTop - diameterBottom) + diameterBottom) / 2;
  85. ringIdx = hasRings && i !== 0 && i !== subdivisions ? 2 : 1;
  86. for (r = 0; r < ringIdx; r++) {
  87. if (hasRings) {
  88. s += r;
  89. }
  90. if (enclose) {
  91. s += 2 * r;
  92. }
  93. for (j = 0; j <= tessellation; j++) {
  94. angle = j * angleStep;
  95. // position
  96. ringVertex.x = Math.cos(-angle) * radius;
  97. ringVertex.y = -height / 2 + h * height;
  98. ringVertex.z = Math.sin(-angle) * radius;
  99. // normal
  100. if (diameterTop === 0 && i === subdivisions) {
  101. // if no top cap, reuse former normals
  102. ringNormal.x = normals[normals.length - (tessellation + 1) * 3];
  103. ringNormal.y = normals[normals.length - (tessellation + 1) * 3 + 1];
  104. ringNormal.z = normals[normals.length - (tessellation + 1) * 3 + 2];
  105. }
  106. else {
  107. ringNormal.x = ringVertex.x;
  108. ringNormal.z = ringVertex.z;
  109. ringNormal.y = Math.sqrt(ringNormal.x * ringNormal.x + ringNormal.z * ringNormal.z) * tan;
  110. ringNormal.normalize();
  111. }
  112. // keep first ring vertex values for enclose
  113. if (j === 0) {
  114. ringFirstVertex.copyFrom(ringVertex);
  115. ringFirstNormal.copyFrom(ringNormal);
  116. }
  117. positions.push(ringVertex.x, ringVertex.y, ringVertex.z);
  118. normals.push(ringNormal.x, ringNormal.y, ringNormal.z);
  119. if (hasRings) {
  120. v = cs !== s ? faceUV[s].y : faceUV[s].w;
  121. }
  122. else {
  123. v = faceUV[s].y + (faceUV[s].w - faceUV[s].y) * h;
  124. }
  125. uvs.push(faceUV[s].x + ((faceUV[s].z - faceUV[s].x) * j) / tessellation, CompatibilityOptions.UseOpenGLOrientationForUV ? 1 - v : v);
  126. if (faceColors) {
  127. colors.push(faceColors[s].r, faceColors[s].g, faceColors[s].b, faceColors[s].a);
  128. }
  129. }
  130. // if enclose, add four vertices and their dedicated normals
  131. if (arc !== 1 && enclose) {
  132. positions.push(ringVertex.x, ringVertex.y, ringVertex.z);
  133. positions.push(0, ringVertex.y, 0);
  134. positions.push(0, ringVertex.y, 0);
  135. positions.push(ringFirstVertex.x, ringFirstVertex.y, ringFirstVertex.z);
  136. Vector3.CrossToRef(Y, ringNormal, quadNormal);
  137. quadNormal.normalize();
  138. normals.push(quadNormal.x, quadNormal.y, quadNormal.z, quadNormal.x, quadNormal.y, quadNormal.z);
  139. Vector3.CrossToRef(ringFirstNormal, Y, quadNormal);
  140. quadNormal.normalize();
  141. normals.push(quadNormal.x, quadNormal.y, quadNormal.z, quadNormal.x, quadNormal.y, quadNormal.z);
  142. if (hasRings) {
  143. v = cs !== s ? faceUV[s + 1].y : faceUV[s + 1].w;
  144. }
  145. else {
  146. v = faceUV[s + 1].y + (faceUV[s + 1].w - faceUV[s + 1].y) * h;
  147. }
  148. uvs.push(faceUV[s + 1].x, CompatibilityOptions.UseOpenGLOrientationForUV ? 1 - v : v);
  149. uvs.push(faceUV[s + 1].z, CompatibilityOptions.UseOpenGLOrientationForUV ? 1 - v : v);
  150. if (hasRings) {
  151. v = cs !== s ? faceUV[s + 2].y : faceUV[s + 2].w;
  152. }
  153. else {
  154. v = faceUV[s + 2].y + (faceUV[s + 2].w - faceUV[s + 2].y) * h;
  155. }
  156. uvs.push(faceUV[s + 2].x, CompatibilityOptions.UseOpenGLOrientationForUV ? 1 - v : v);
  157. uvs.push(faceUV[s + 2].z, CompatibilityOptions.UseOpenGLOrientationForUV ? 1 - v : v);
  158. if (faceColors) {
  159. colors.push(faceColors[s + 1].r, faceColors[s + 1].g, faceColors[s + 1].b, faceColors[s + 1].a);
  160. colors.push(faceColors[s + 1].r, faceColors[s + 1].g, faceColors[s + 1].b, faceColors[s + 1].a);
  161. colors.push(faceColors[s + 2].r, faceColors[s + 2].g, faceColors[s + 2].b, faceColors[s + 2].a);
  162. colors.push(faceColors[s + 2].r, faceColors[s + 2].g, faceColors[s + 2].b, faceColors[s + 2].a);
  163. }
  164. }
  165. if (cs !== s) {
  166. cs = s;
  167. }
  168. }
  169. }
  170. // indices
  171. const e = arc !== 1 && enclose ? tessellation + 4 : tessellation; // correction of number of iteration if enclose
  172. i = 0;
  173. for (s = 0; s < subdivisions; s++) {
  174. let i0 = 0;
  175. let i1 = 0;
  176. let i2 = 0;
  177. let i3 = 0;
  178. for (j = 0; j < tessellation; j++) {
  179. i0 = i * (e + 1) + j;
  180. i1 = (i + 1) * (e + 1) + j;
  181. i2 = i * (e + 1) + (j + 1);
  182. i3 = (i + 1) * (e + 1) + (j + 1);
  183. indices.push(i0, i1, i2);
  184. indices.push(i3, i2, i1);
  185. }
  186. if (arc !== 1 && enclose) {
  187. // if enclose, add two quads
  188. indices.push(i0 + 2, i1 + 2, i2 + 2);
  189. indices.push(i3 + 2, i2 + 2, i1 + 2);
  190. indices.push(i0 + 4, i1 + 4, i2 + 4);
  191. indices.push(i3 + 4, i2 + 4, i1 + 4);
  192. }
  193. i = hasRings ? i + 2 : i + 1;
  194. }
  195. // Caps
  196. const createCylinderCap = (isTop) => {
  197. const radius = isTop ? diameterTop / 2 : diameterBottom / 2;
  198. if (radius === 0) {
  199. return;
  200. }
  201. // Cap positions, normals & uvs
  202. let angle;
  203. let circleVector;
  204. let i;
  205. const u = isTop ? faceUV[surfaceNb - 1] : faceUV[0];
  206. let c = null;
  207. if (faceColors) {
  208. c = isTop ? faceColors[surfaceNb - 1] : faceColors[0];
  209. }
  210. // cap center
  211. const vbase = positions.length / 3;
  212. const offset = isTop ? height / 2 : -height / 2;
  213. const center = new Vector3(0, offset, 0);
  214. positions.push(center.x, center.y, center.z);
  215. normals.push(0, isTop ? 1 : -1, 0);
  216. const v = u.y + (u.w - u.y) * 0.5;
  217. uvs.push(u.x + (u.z - u.x) * 0.5, CompatibilityOptions.UseOpenGLOrientationForUV ? 1 - v : v);
  218. if (c) {
  219. colors.push(c.r, c.g, c.b, c.a);
  220. }
  221. const textureScale = new Vector2(0.5, 0.5);
  222. for (i = 0; i <= tessellation; i++) {
  223. angle = (Math.PI * 2 * i * arc) / tessellation;
  224. const cos = Math.cos(-angle);
  225. const sin = Math.sin(-angle);
  226. circleVector = new Vector3(cos * radius, offset, sin * radius);
  227. const textureCoordinate = new Vector2(cos * textureScale.x + 0.5, sin * textureScale.y + 0.5);
  228. positions.push(circleVector.x, circleVector.y, circleVector.z);
  229. normals.push(0, isTop ? 1 : -1, 0);
  230. const v = u.y + (u.w - u.y) * textureCoordinate.y;
  231. uvs.push(u.x + (u.z - u.x) * textureCoordinate.x, CompatibilityOptions.UseOpenGLOrientationForUV ? 1 - v : v);
  232. if (c) {
  233. colors.push(c.r, c.g, c.b, c.a);
  234. }
  235. }
  236. // Cap indices
  237. for (i = 0; i < tessellation; i++) {
  238. if (!isTop) {
  239. indices.push(vbase);
  240. indices.push(vbase + (i + 1));
  241. indices.push(vbase + (i + 2));
  242. }
  243. else {
  244. indices.push(vbase);
  245. indices.push(vbase + (i + 2));
  246. indices.push(vbase + (i + 1));
  247. }
  248. }
  249. };
  250. // add caps to geometry based on cap parameter
  251. if (cap === Mesh.CAP_START || cap === Mesh.CAP_ALL) {
  252. createCylinderCap(false);
  253. }
  254. if (cap === Mesh.CAP_END || cap === Mesh.CAP_ALL) {
  255. createCylinderCap(true);
  256. }
  257. // Sides
  258. VertexData._ComputeSides(sideOrientation, positions, indices, normals, uvs, options.frontUVs, options.backUVs);
  259. const vertexData = new VertexData();
  260. vertexData.indices = indices;
  261. vertexData.positions = positions;
  262. vertexData.normals = normals;
  263. vertexData.uvs = uvs;
  264. if (faceColors) {
  265. vertexData.colors = colors;
  266. }
  267. return vertexData;
  268. }
  269. /**
  270. * Creates a cylinder or a cone mesh
  271. * * The parameter `height` sets the height size (float) of the cylinder/cone (float, default 2).
  272. * * The parameter `diameter` sets the diameter of the top and bottom cap at once (float, default 1).
  273. * * The parameters `diameterTop` and `diameterBottom` overwrite the parameter `diameter` and set respectively the top cap and bottom cap diameter (floats, default 1). The parameter "diameterBottom" can't be zero.
  274. * * The parameter `tessellation` sets the number of cylinder sides (positive integer, default 24). Set it to 3 to get a prism for instance.
  275. * * The parameter `subdivisions` sets the number of rings along the cylinder height (positive integer, default 1).
  276. * * The parameter `hasRings` (boolean, default false) makes the subdivisions independent from each other, so they become different faces.
  277. * * The parameter `enclose` (boolean, default false) adds two extra faces per subdivision to a sliced cylinder to close it around its height axis.
  278. * * The parameter `cap` sets the way the cylinder is capped. Possible values : BABYLON.Mesh.NO_CAP, BABYLON.Mesh.CAP_START, BABYLON.Mesh.CAP_END, BABYLON.Mesh.CAP_ALL (default).
  279. * * The parameter `arc` (float, default 1) is the ratio (max 1) to apply to the circumference to slice the cylinder.
  280. * * You can set different colors and different images to each box side by using the parameters `faceColors` (an array of n Color3 elements) and `faceUV` (an array of n Vector4 elements).
  281. * * The value of n is the number of cylinder faces. If the cylinder has only 1 subdivisions, n equals : top face + cylinder surface + bottom face = 3
  282. * * Now, if the cylinder has 5 independent subdivisions (hasRings = true), n equals : top face + 5 stripe surfaces + bottom face = 2 + 5 = 7
  283. * * Finally, if the cylinder has 5 independent subdivisions and is enclose, n equals : top face + 5 x (stripe surface + 2 closing faces) + bottom face = 2 + 5 * 3 = 17
  284. * * Each array (color or UVs) is always ordered the same way : the first element is the bottom cap, the last element is the top cap. The other elements are each a ring surface.
  285. * * If `enclose` is false, a ring surface is one element.
  286. * * If `enclose` is true, a ring surface is 3 successive elements in the array : the tubular surface, then the two closing faces.
  287. * * Example how to set colors and textures on a sliced cylinder : https://www.html5gamedevs.com/topic/17945-creating-a-closed-slice-of-a-cylinder/#comment-106379
  288. * * You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE
  289. * * 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
  290. * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created.
  291. * @param name defines the name of the mesh
  292. * @param options defines the options used to create the mesh
  293. * @param scene defines the hosting scene
  294. * @returns the cylinder mesh
  295. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#cylinder-or-cone
  296. */
  297. export function CreateCylinder(name, options = {}, scene) {
  298. const cylinder = new Mesh(name, scene);
  299. options.sideOrientation = Mesh._GetDefaultSideOrientation(options.sideOrientation);
  300. cylinder._originalBuilderSideOrientation = options.sideOrientation;
  301. const vertexData = CreateCylinderVertexData(options);
  302. vertexData.applyToMesh(cylinder, options.updatable);
  303. return cylinder;
  304. }
  305. /**
  306. * Class containing static functions to help procedurally build meshes
  307. * @deprecated Please use CreateCylinder directly
  308. */
  309. export const CylinderBuilder = {
  310. // eslint-disable-next-line @typescript-eslint/naming-convention
  311. CreateCylinder,
  312. };
  313. VertexData.CreateCylinder = CreateCylinderVertexData;
  314. Mesh.CreateCylinder = (name, height, diameterTop, diameterBottom, tessellation, subdivisions, scene, updatable, sideOrientation) => {
  315. if (scene === undefined || !(scene instanceof Scene)) {
  316. if (scene !== undefined) {
  317. sideOrientation = updatable || Mesh.DEFAULTSIDE;
  318. updatable = scene;
  319. }
  320. scene = subdivisions;
  321. subdivisions = 1;
  322. }
  323. const options = {
  324. height,
  325. diameterTop,
  326. diameterBottom,
  327. tessellation,
  328. subdivisions,
  329. sideOrientation,
  330. updatable,
  331. };
  332. return CreateCylinder(name, options, scene);
  333. };
  334. //# sourceMappingURL=cylinderBuilder.js.map