import { BufferGeometry, Float32BufferAttribute, ShapeUtils, Vector2, Vector3 } from 'three'; const _vector = /*@__PURE__*/ new Vector3(); /** * This class can be used to generate a geometry by lofting (skinning) a surface * through a series of cross sections. Each section is an array of points in 3D * space and all sections must have the same number of points. * * `LoftGeometry` is the general case of geometries like {@link LatheGeometry} * (which revolves a fixed profile around an axis) or {@link TubeGeometry} * (which sweeps a circular section along a path): the sections can have any * shape, and can change shape, size, position and orientation from one * section to the next. * * Sections wind around the loft so the resulting face normals point outwards * when each section is ordered counterclockwise as seen from the end of the * loft, looking back towards the start. If the surface appears inside out, * reverse the point order of each section. * * ```js * const sections = []; * * for ( let i = 0; i <= 10; i ++ ) { * * const points = []; * const radius = 2 + Math.sin( i * 0.8 ); * * for ( let j = 0; j < 32; j ++ ) { * * const angle = j / 32 * Math.PI * 2; * points.push( new THREE.Vector3( Math.sin( angle ) * radius, i, Math.cos( angle ) * radius ) ); * * } * * sections.push( points ); * * } * * const geometry = new LoftGeometry( sections, { capStart: true, capEnd: true } ); * const material = new THREE.MeshStandardMaterial( { color: 0x00ff00 } ); * const mesh = new THREE.Mesh( geometry, material ); * scene.add( mesh ); * ``` * * @augments BufferGeometry * @three_import import { LoftGeometry } from 'three/addons/geometries/LoftGeometry.js'; */ class LoftGeometry extends BufferGeometry { /** * Constructs a new loft geometry. * * @param {Array>} sections - The cross sections to skin. At least * two sections are required and all sections must have the same number of points. * @param {Object} [options={}] - The loft options. * @param {boolean} [options.closed=true] - Whether each section is treated as a * closed ring (e.g. a fuselage) or an open strip (e.g. a ribbon). * @param {boolean} [options.capStart=false] - Whether the first section is closed * with a cap or not. * @param {boolean} [options.capEnd=false] - Whether the last section is closed * with a cap or not. */ constructor( sections = [], options = {} ) { super(); this.type = 'LoftGeometry'; const { closed = true, capStart = false, capEnd = false } = options; /** * Holds the constructor parameters that have been * used to generate the geometry. Any modification * after instantiation does not change the geometry. * * @type {Object} */ this.parameters = { sections: sections, closed: closed, capStart: capStart, capEnd: capEnd }; const rows = sections.length; if ( rows < 2 ) { console.error( 'THREE.LoftGeometry: At least two sections are required.' ); return; } const columns = sections[ 0 ].length; for ( let i = 1; i < rows; i ++ ) { if ( sections[ i ].length !== columns ) { console.error( 'THREE.LoftGeometry: All sections must have the same number of points.' ); return; } } // closed sections repeat their first point so the surface can wrap // around with continuous uvs const pointsPerRow = closed ? columns + 1 : columns; // buffers const indices = []; const vertices = []; const uvs = []; // generate vertices and uvs for ( let i = 0; i < rows; i ++ ) { const section = sections[ i ]; for ( let j = 0; j < pointsPerRow; j ++ ) { const point = section[ j % columns ]; vertices.push( point.x, point.y, point.z ); uvs.push( i / ( rows - 1 ), j / ( pointsPerRow - 1 ) ); } } // generate indices for ( let i = 0; i < rows - 1; i ++ ) { for ( let j = 0; j < pointsPerRow - 1; j ++ ) { const a = i * pointsPerRow + j; const b = i * pointsPerRow + j + 1; const c = ( i + 1 ) * pointsPerRow + j + 1; const d = ( i + 1 ) * pointsPerRow + j; // faces one and two indices.push( a, b, d ); indices.push( b, c, d ); } } // generate caps if ( capStart === true ) generateCap( 0 ); if ( capEnd === true ) generateCap( rows - 1 ); // build geometry this.setIndex( indices ); this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) ); this.computeVertexNormals(); // the seam vertices of closed sections are duplicated so their computed // normals must be averaged to achieve smooth shading across the seam if ( closed === true ) { const normals = this.getAttribute( 'normal' ); for ( let i = 0; i < rows; i ++ ) { const a = i * pointsPerRow; const b = i * pointsPerRow + ( pointsPerRow - 1 ); _vector.set( normals.getX( a ) + normals.getX( b ), normals.getY( a ) + normals.getY( b ), normals.getZ( a ) + normals.getZ( b ) ).normalize(); normals.setXYZ( a, _vector.x, _vector.y, _vector.z ); normals.setXYZ( b, _vector.x, _vector.y, _vector.z ); } } function generateCap( sectionIndex ) { const section = sections[ sectionIndex ]; // compute the centroid of the section and the normal of its plane // via Newell's method const centroid = new Vector3(); const normal = new Vector3(); for ( let i = 0; i < columns; i ++ ) { const p = section[ i ]; const q = section[ ( i + 1 ) % columns ]; centroid.add( p ); normal.x += ( p.y - q.y ) * ( p.z + q.z ); normal.y += ( p.z - q.z ) * ( p.x + q.x ); normal.z += ( p.x - q.x ) * ( p.y + q.y ); } centroid.divideScalar( columns ); normal.normalize(); // make sure the cap faces away from the rest of the surface const neighbor = sections[ sectionIndex === 0 ? 1 : rows - 2 ]; _vector.set( 0, 0, 0 ); for ( let i = 0; i < columns; i ++ ) _vector.add( neighbor[ i ] ); _vector.divideScalar( columns ).sub( centroid ); if ( normal.dot( _vector ) > 0 ) normal.negate(); // project the section onto the cap plane const tangent = new Vector3( 1, 0, 0 ); if ( Math.abs( normal.x ) > 0.9 ) tangent.set( 0, 1, 0 ); const bitangent = new Vector3().crossVectors( normal, tangent ).normalize(); tangent.crossVectors( bitangent, normal ); const contour = []; const points = section.slice(); for ( let i = 0; i < columns; i ++ ) { _vector.subVectors( points[ i ], centroid ); contour.push( new Vector2( _vector.dot( tangent ), _vector.dot( bitangent ) ) ); } // triangulateShape() expects contours in counterclockwise order if ( ShapeUtils.isClockWise( contour ) === true ) { contour.reverse(); points.reverse(); } const faces = ShapeUtils.triangulateShape( contour, [] ); // compute the bounding box of the contour for uv generation const min = new Vector2( Infinity, Infinity ); const max = new Vector2( - Infinity, - Infinity ); for ( let i = 0; i < columns; i ++ ) { min.min( contour[ i ] ); max.max( contour[ i ] ); } const width = Math.max( max.x - min.x, Number.EPSILON ); const height = Math.max( max.y - min.y, Number.EPSILON ); // generate vertices, uvs and indices; cap vertices are not shared // with the wall so the cap is flat shaded with a hard edge const indexOffset = vertices.length / 3; for ( let i = 0; i < columns; i ++ ) { const point = points[ i ]; vertices.push( point.x, point.y, point.z ); uvs.push( ( contour[ i ].x - min.x ) / width, ( contour[ i ].y - min.y ) / height ); } for ( let i = 0; i < faces.length; i ++ ) { const face = faces[ i ]; indices.push( indexOffset + face[ 0 ], indexOffset + face[ 1 ], indexOffset + face[ 2 ] ); } } } copy( source ) { super.copy( source ); this.parameters = Object.assign( {}, source.parameters ); return this; } } export { LoftGeometry };