LoftGeometry.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import {
  2. BufferGeometry,
  3. Float32BufferAttribute,
  4. ShapeUtils,
  5. Vector2,
  6. Vector3
  7. } from 'three';
  8. const _vector = /*@__PURE__*/ new Vector3();
  9. /**
  10. * This class can be used to generate a geometry by lofting (skinning) a surface
  11. * through a series of cross sections. Each section is an array of points in 3D
  12. * space and all sections must have the same number of points.
  13. *
  14. * `LoftGeometry` is the general case of geometries like {@link LatheGeometry}
  15. * (which revolves a fixed profile around an axis) or {@link TubeGeometry}
  16. * (which sweeps a circular section along a path): the sections can have any
  17. * shape, and can change shape, size, position and orientation from one
  18. * section to the next.
  19. *
  20. * Sections wind around the loft so the resulting face normals point outwards
  21. * when each section is ordered counterclockwise as seen from the end of the
  22. * loft, looking back towards the start. If the surface appears inside out,
  23. * reverse the point order of each section.
  24. *
  25. * ```js
  26. * const sections = [];
  27. *
  28. * for ( let i = 0; i <= 10; i ++ ) {
  29. *
  30. * const points = [];
  31. * const radius = 2 + Math.sin( i * 0.8 );
  32. *
  33. * for ( let j = 0; j < 32; j ++ ) {
  34. *
  35. * const angle = j / 32 * Math.PI * 2;
  36. * points.push( new THREE.Vector3( Math.sin( angle ) * radius, i, Math.cos( angle ) * radius ) );
  37. *
  38. * }
  39. *
  40. * sections.push( points );
  41. *
  42. * }
  43. *
  44. * const geometry = new LoftGeometry( sections, { capStart: true, capEnd: true } );
  45. * const material = new THREE.MeshStandardMaterial( { color: 0x00ff00 } );
  46. * const mesh = new THREE.Mesh( geometry, material );
  47. * scene.add( mesh );
  48. * ```
  49. *
  50. * @augments BufferGeometry
  51. * @three_import import { LoftGeometry } from 'three/addons/geometries/LoftGeometry.js';
  52. */
  53. class LoftGeometry extends BufferGeometry {
  54. /**
  55. * Constructs a new loft geometry.
  56. *
  57. * @param {Array<Array<Vector3>>} sections - The cross sections to skin. At least
  58. * two sections are required and all sections must have the same number of points.
  59. * @param {Object} [options={}] - The loft options.
  60. * @param {boolean} [options.closed=true] - Whether each section is treated as a
  61. * closed ring (e.g. a fuselage) or an open strip (e.g. a ribbon).
  62. * @param {boolean} [options.capStart=false] - Whether the first section is closed
  63. * with a cap or not.
  64. * @param {boolean} [options.capEnd=false] - Whether the last section is closed
  65. * with a cap or not.
  66. */
  67. constructor( sections = [], options = {} ) {
  68. super();
  69. this.type = 'LoftGeometry';
  70. const { closed = true, capStart = false, capEnd = false } = options;
  71. /**
  72. * Holds the constructor parameters that have been
  73. * used to generate the geometry. Any modification
  74. * after instantiation does not change the geometry.
  75. *
  76. * @type {Object}
  77. */
  78. this.parameters = {
  79. sections: sections,
  80. closed: closed,
  81. capStart: capStart,
  82. capEnd: capEnd
  83. };
  84. const rows = sections.length;
  85. if ( rows < 2 ) {
  86. console.error( 'THREE.LoftGeometry: At least two sections are required.' );
  87. return;
  88. }
  89. const columns = sections[ 0 ].length;
  90. for ( let i = 1; i < rows; i ++ ) {
  91. if ( sections[ i ].length !== columns ) {
  92. console.error( 'THREE.LoftGeometry: All sections must have the same number of points.' );
  93. return;
  94. }
  95. }
  96. // closed sections repeat their first point so the surface can wrap
  97. // around with continuous uvs
  98. const pointsPerRow = closed ? columns + 1 : columns;
  99. // buffers
  100. const indices = [];
  101. const vertices = [];
  102. const uvs = [];
  103. // uvs follow arc length so uneven sections and points map evenly
  104. const rowU = [ 0 ];
  105. for ( let i = 1; i < rows; i ++ ) {
  106. let distance = 0;
  107. for ( let j = 0; j < columns; j ++ ) {
  108. distance += sections[ i ][ j ].distanceTo( sections[ i - 1 ][ j ] );
  109. }
  110. rowU.push( rowU[ i - 1 ] + distance / columns );
  111. }
  112. const totalU = rowU[ rows - 1 ];
  113. // generate vertices and uvs
  114. for ( let i = 0; i < rows; i ++ ) {
  115. const section = sections[ i ];
  116. // distance around the section
  117. const colV = [ 0 ];
  118. for ( let j = 1; j < pointsPerRow; j ++ ) {
  119. colV.push( colV[ j - 1 ] + section[ j % columns ].distanceTo( section[ ( j - 1 ) % columns ] ) );
  120. }
  121. const totalV = colV[ pointsPerRow - 1 ];
  122. for ( let j = 0; j < pointsPerRow; j ++ ) {
  123. const point = section[ j % columns ];
  124. vertices.push( point.x, point.y, point.z );
  125. uvs.push(
  126. totalU > 0 ? rowU[ i ] / totalU : i / ( rows - 1 ),
  127. totalV > 0 ? colV[ j ] / totalV : j / ( pointsPerRow - 1 )
  128. );
  129. }
  130. }
  131. // generate indices
  132. for ( let i = 0; i < rows - 1; i ++ ) {
  133. for ( let j = 0; j < pointsPerRow - 1; j ++ ) {
  134. const a = i * pointsPerRow + j;
  135. const b = i * pointsPerRow + j + 1;
  136. const c = ( i + 1 ) * pointsPerRow + j + 1;
  137. const d = ( i + 1 ) * pointsPerRow + j;
  138. // faces one and two
  139. indices.push( a, b, d );
  140. indices.push( b, c, d );
  141. }
  142. }
  143. // generate caps
  144. if ( capStart === true ) generateCap( 0 );
  145. if ( capEnd === true ) generateCap( rows - 1 );
  146. // build geometry
  147. this.setIndex( indices );
  148. this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
  149. this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
  150. this.computeVertexNormals();
  151. // the seam vertices of closed sections are duplicated so their computed
  152. // normals must be averaged to achieve smooth shading across the seam
  153. if ( closed === true ) {
  154. const normals = this.getAttribute( 'normal' );
  155. for ( let i = 0; i < rows; i ++ ) {
  156. const a = i * pointsPerRow;
  157. const b = i * pointsPerRow + ( pointsPerRow - 1 );
  158. _vector.set(
  159. normals.getX( a ) + normals.getX( b ),
  160. normals.getY( a ) + normals.getY( b ),
  161. normals.getZ( a ) + normals.getZ( b )
  162. ).normalize();
  163. normals.setXYZ( a, _vector.x, _vector.y, _vector.z );
  164. normals.setXYZ( b, _vector.x, _vector.y, _vector.z );
  165. }
  166. }
  167. function generateCap( sectionIndex ) {
  168. const section = sections[ sectionIndex ];
  169. // compute the centroid of the section and the normal of its plane
  170. // via Newell's method
  171. const centroid = new Vector3();
  172. const normal = new Vector3();
  173. for ( let i = 0; i < columns; i ++ ) {
  174. const p = section[ i ];
  175. const q = section[ ( i + 1 ) % columns ];
  176. centroid.add( p );
  177. normal.x += ( p.y - q.y ) * ( p.z + q.z );
  178. normal.y += ( p.z - q.z ) * ( p.x + q.x );
  179. normal.z += ( p.x - q.x ) * ( p.y + q.y );
  180. }
  181. centroid.divideScalar( columns );
  182. normal.normalize();
  183. // make sure the cap faces away from the rest of the surface
  184. const neighbor = sections[ sectionIndex === 0 ? 1 : rows - 2 ];
  185. _vector.set( 0, 0, 0 );
  186. for ( let i = 0; i < columns; i ++ ) _vector.add( neighbor[ i ] );
  187. _vector.divideScalar( columns ).sub( centroid );
  188. if ( normal.dot( _vector ) > 0 ) normal.negate();
  189. // project the section onto the cap plane
  190. const tangent = new Vector3( 1, 0, 0 );
  191. if ( Math.abs( normal.x ) > 0.9 ) tangent.set( 0, 1, 0 );
  192. const bitangent = new Vector3().crossVectors( normal, tangent ).normalize();
  193. tangent.crossVectors( bitangent, normal );
  194. const contour = [];
  195. const points = section.slice();
  196. for ( let i = 0; i < columns; i ++ ) {
  197. _vector.subVectors( points[ i ], centroid );
  198. contour.push( new Vector2( _vector.dot( tangent ), _vector.dot( bitangent ) ) );
  199. }
  200. // triangulateShape() expects contours in counterclockwise order
  201. if ( ShapeUtils.isClockWise( contour ) === true ) {
  202. contour.reverse();
  203. points.reverse();
  204. }
  205. const faces = ShapeUtils.triangulateShape( contour, [] );
  206. // compute the bounding box of the contour for uv generation
  207. const min = new Vector2( Infinity, Infinity );
  208. const max = new Vector2( - Infinity, - Infinity );
  209. for ( let i = 0; i < columns; i ++ ) {
  210. min.min( contour[ i ] );
  211. max.max( contour[ i ] );
  212. }
  213. const width = Math.max( max.x - min.x, Number.EPSILON );
  214. const height = Math.max( max.y - min.y, Number.EPSILON );
  215. // generate vertices, uvs and indices; cap vertices are not shared
  216. // with the wall so the cap is flat shaded with a hard edge
  217. const indexOffset = vertices.length / 3;
  218. for ( let i = 0; i < columns; i ++ ) {
  219. const point = points[ i ];
  220. vertices.push( point.x, point.y, point.z );
  221. uvs.push( ( contour[ i ].x - min.x ) / width, ( contour[ i ].y - min.y ) / height );
  222. }
  223. for ( let i = 0; i < faces.length; i ++ ) {
  224. const face = faces[ i ];
  225. indices.push( indexOffset + face[ 0 ], indexOffset + face[ 1 ], indexOffset + face[ 2 ] );
  226. }
  227. }
  228. }
  229. copy( source ) {
  230. super.copy( source );
  231. this.parameters = Object.assign( {}, source.parameters );
  232. return this;
  233. }
  234. }
  235. export { LoftGeometry };
粤ICP备19079148号