Accueil / npm / three@0.186.1 / examples / jsm / geometries / LoftGeometry.js

LoftGeometry.js

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<Array<Vector3>>} 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 = [];

		// uvs follow arc length so uneven sections and points map evenly

		const rowU = [ 0 ];

		for ( let i = 1; i < rows; i ++ ) {

			let distance = 0;

			for ( let j = 0; j < columns; j ++ ) {

				distance += sections[ i ][ j ].distanceTo( sections[ i - 1 ][ j ] );

			}

			rowU.push( rowU[ i - 1 ] + distance / columns );

		}

		const totalU = rowU[ rows - 1 ];

		// generate vertices and uvs

		for ( let i = 0; i < rows; i ++ ) {

			const section = sections[ i ];

			// distance around the section

			const colV = [ 0 ];

			for ( let j = 1; j < pointsPerRow; j ++ ) {

				colV.push( colV[ j - 1 ] + section[ j % columns ].distanceTo( section[ ( j - 1 ) % columns ] ) );

			}

			const totalV = colV[ pointsPerRow - 1 ];

			for ( let j = 0; j < pointsPerRow; j ++ ) {

				const point = section[ j % columns ];

				vertices.push( point.x, point.y, point.z );
				uvs.push(
					totalU > 0 ? rowU[ i ] / totalU : i / ( rows - 1 ),
					totalV > 0 ? colV[ j ] / totalV : 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 };

Exemple