three
Version:
JavaScript 3D library
600 lines (408 loc) • 15.5 kB
JavaScript
import {
BufferAttribute,
BufferGeometry,
Color,
DynamicDrawUsage,
Matrix4,
Mesh,
MeshStandardMaterial,
Vector3
} from 'three';
/**
* @classdesc This module can be used to paint tube-like meshes
* along a sequence of points. This module is used in a XR
* painter demo.
*
* ```js
* const painter = new TubePainter();
* scene.add( painter.mesh );
* ```
*
* @name TubePainter
* @class
* @three_import import { TubePainter } from 'three/addons/misc/TubePainter.js';
*/
function TubePainter() {
const BUFFER_SIZE = 1000000 * 3;
const positions = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
positions.usage = DynamicDrawUsage;
const normals = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
normals.usage = DynamicDrawUsage;
const colors = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
colors.usage = DynamicDrawUsage;
const geometry = new BufferGeometry();
geometry.setAttribute( 'position', positions );
geometry.setAttribute( 'normal', normals );
geometry.setAttribute( 'color', colors );
geometry.drawRange.count = 0;
const material = new MeshStandardMaterial( { vertexColors: true } );
const mesh = new Mesh( geometry, material );
mesh.frustumCulled = false;
//
function getPoints( size ) {
const PI2 = Math.PI * 2;
const sides = 15;
const array = [];
const radius = 0.01 * size;
for ( let i = 0; i < sides; i ++ ) {
const angle = ( i / sides ) * PI2;
array.push( new Vector3( Math.sin( angle ) * radius, Math.cos( angle ) * radius, 0 ) );
}
return array;
}
//
const vector = new Vector3();
const vector1 = new Vector3();
const vector2 = new Vector3();
const vector3 = new Vector3();
const vector4 = new Vector3();
const color1 = new Color( 0xffffff );
const color2 = new Color( 0xffffff );
let size1 = 1;
let size2 = 1;
function addCap( position, matrix, isEndCap, capSize ) {
let count = geometry.drawRange.count;
const points = getPoints( capSize );
const sides = points.length;
const radius = 0.01 * capSize;
const latSegments = 4;
const directionSign = isEndCap ? - 1 : 1;
for ( let lat = 0; lat < latSegments; lat ++ ) {
const phi1 = ( lat / latSegments ) * Math.PI * 0.5;
const phi2 = ( ( lat + 1 ) / latSegments ) * Math.PI * 0.5;
const z1 = Math.sin( phi1 ) * radius * directionSign;
const r1 = Math.cos( phi1 ) * radius;
const z2 = Math.sin( phi2 ) * radius * directionSign;
const r2 = Math.cos( phi2 ) * radius;
for ( let i = 0; i < sides; i ++ ) {
const theta1 = ( i / sides ) * Math.PI * 2;
const theta2 = ( ( i + 1 ) / sides ) * Math.PI * 2;
// First ring
const x1 = Math.sin( theta1 ) * r1;
const y1 = Math.cos( theta1 ) * r1;
const x2 = Math.sin( theta2 ) * r1;
const y2 = Math.cos( theta2 ) * r1;
// Second ring
const x3 = Math.sin( theta1 ) * r2;
const y3 = Math.cos( theta1 ) * r2;
const x4 = Math.sin( theta2 ) * r2;
const y4 = Math.cos( theta2 ) * r2;
// Transform to world space
vector1.set( x1, y1, z1 ).applyMatrix4( matrix ).add( position );
vector2.set( x2, y2, z1 ).applyMatrix4( matrix ).add( position );
vector3.set( x3, y3, z2 ).applyMatrix4( matrix ).add( position );
vector4.set( x4, y4, z2 ).applyMatrix4( matrix ).add( position );
// First triangle
normal.set( x1, y1, z1 ).normalize().transformDirection( matrix );
vector.set( x2, y2, z1 ).normalize().transformDirection( matrix );
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
if ( isEndCap ) {
vector1.toArray( positions.array, count * 3 );
vector2.toArray( positions.array, ( count + 1 ) * 3 );
vector3.toArray( positions.array, ( count + 2 ) * 3 );
normal.toArray( normals.array, count * 3 );
vector.toArray( normals.array, ( count + 1 ) * 3 );
side.toArray( normals.array, ( count + 2 ) * 3 );
} else {
vector1.toArray( positions.array, count * 3 );
vector3.toArray( positions.array, ( count + 1 ) * 3 );
vector2.toArray( positions.array, ( count + 2 ) * 3 );
normal.toArray( normals.array, count * 3 );
side.toArray( normals.array, ( count + 1 ) * 3 );
vector.toArray( normals.array, ( count + 2 ) * 3 );
}
color1.toArray( colors.array, count * 3 );
color1.toArray( colors.array, ( count + 1 ) * 3 );
color1.toArray( colors.array, ( count + 2 ) * 3 );
count += 3;
// Second triangle
if ( r2 > 0.001 ) {
normal.set( x2, y2, z1 ).normalize().transformDirection( matrix );
vector.set( x4, y4, z2 ).normalize().transformDirection( matrix );
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
if ( isEndCap ) {
vector2.toArray( positions.array, count * 3 );
vector4.toArray( positions.array, ( count + 1 ) * 3 );
vector3.toArray( positions.array, ( count + 2 ) * 3 );
normal.toArray( normals.array, count * 3 );
vector.toArray( normals.array, ( count + 1 ) * 3 );
side.toArray( normals.array, ( count + 2 ) * 3 );
} else {
vector3.toArray( positions.array, count * 3 );
vector4.toArray( positions.array, ( count + 1 ) * 3 );
vector2.toArray( positions.array, ( count + 2 ) * 3 );
side.toArray( normals.array, count * 3 );
vector.toArray( normals.array, ( count + 1 ) * 3 );
normal.toArray( normals.array, ( count + 2 ) * 3 );
}
color1.toArray( colors.array, count * 3 );
color1.toArray( colors.array, ( count + 1 ) * 3 );
color1.toArray( colors.array, ( count + 2 ) * 3 );
count += 3;
}
}
}
geometry.drawRange.count = count;
}
function updateEndCap( position, matrix, capSize ) {
if ( endCapStartIndex === null ) return;
const points = getPoints( capSize );
const sides = points.length;
const radius = 0.01 * capSize;
const latSegments = 4;
let count = endCapStartIndex;
for ( let lat = 0; lat < latSegments; lat ++ ) {
const phi1 = ( lat / latSegments ) * Math.PI * 0.5;
const phi2 = ( ( lat + 1 ) / latSegments ) * Math.PI * 0.5;
const z1 = - Math.sin( phi1 ) * radius;
const r1 = Math.cos( phi1 ) * radius;
const z2 = - Math.sin( phi2 ) * radius;
const r2 = Math.cos( phi2 ) * radius;
for ( let i = 0; i < sides; i ++ ) {
const theta1 = ( i / sides ) * Math.PI * 2;
const theta2 = ( ( i + 1 ) / sides ) * Math.PI * 2;
// First ring
const x1 = Math.sin( theta1 ) * r1;
const y1 = Math.cos( theta1 ) * r1;
const x2 = Math.sin( theta2 ) * r1;
const y2 = Math.cos( theta2 ) * r1;
// Second ring
const x3 = Math.sin( theta1 ) * r2;
const y3 = Math.cos( theta1 ) * r2;
const x4 = Math.sin( theta2 ) * r2;
const y4 = Math.cos( theta2 ) * r2;
// Transform positions to world space
vector1.set( x1, y1, z1 ).applyMatrix4( matrix ).add( position );
vector2.set( x2, y2, z1 ).applyMatrix4( matrix ).add( position );
vector3.set( x3, y3, z2 ).applyMatrix4( matrix ).add( position );
vector4.set( x4, y4, z2 ).applyMatrix4( matrix ).add( position );
// Transform normals to world space
normal.set( x1, y1, z1 ).normalize().transformDirection( matrix );
vector.set( x2, y2, z1 ).normalize().transformDirection( matrix );
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
// First triangle
vector1.toArray( positions.array, count * 3 );
vector2.toArray( positions.array, ( count + 1 ) * 3 );
vector3.toArray( positions.array, ( count + 2 ) * 3 );
normal.toArray( normals.array, count * 3 );
vector.toArray( normals.array, ( count + 1 ) * 3 );
side.toArray( normals.array, ( count + 2 ) * 3 );
color1.toArray( colors.array, count * 3 );
color1.toArray( colors.array, ( count + 1 ) * 3 );
color1.toArray( colors.array, ( count + 2 ) * 3 );
count += 3;
// Second triangle
if ( r2 > 0.001 ) {
normal.set( x2, y2, z1 ).normalize().transformDirection( matrix );
vector.set( x4, y4, z2 ).normalize().transformDirection( matrix );
side.set( x3, y3, z2 ).normalize().transformDirection( matrix );
vector2.toArray( positions.array, count * 3 );
vector4.toArray( positions.array, ( count + 1 ) * 3 );
vector3.toArray( positions.array, ( count + 2 ) * 3 );
normal.toArray( normals.array, count * 3 );
vector.toArray( normals.array, ( count + 1 ) * 3 );
side.toArray( normals.array, ( count + 2 ) * 3 );
color1.toArray( colors.array, count * 3 );
color1.toArray( colors.array, ( count + 1 ) * 3 );
color1.toArray( colors.array, ( count + 2 ) * 3 );
count += 3;
}
}
}
positions.addUpdateRange( endCapStartIndex * 3, endCapVertexCount * 3 );
normals.addUpdateRange( endCapStartIndex * 3, endCapVertexCount * 3 );
colors.addUpdateRange( endCapStartIndex * 3, endCapVertexCount * 3 );
}
function stroke( position1, position2, matrix1, matrix2, size1, size2 ) {
if ( position1.distanceToSquared( position2 ) === 0 ) return;
let count = geometry.drawRange.count;
const points1 = getPoints( size1 );
const points2 = getPoints( size2 );
for ( let i = 0, il = points2.length; i < il; i ++ ) {
const vertex1_2 = points2[ i ];
const vertex2_2 = points2[ ( i + 1 ) % il ];
const vertex1_1 = points1[ i ];
const vertex2_1 = points1[ ( i + 1 ) % il ];
vector1.copy( vertex1_2 ).applyMatrix4( matrix2 ).add( position2 );
vector2.copy( vertex2_2 ).applyMatrix4( matrix2 ).add( position2 );
vector3.copy( vertex2_1 ).applyMatrix4( matrix1 ).add( position1 );
vector4.copy( vertex1_1 ).applyMatrix4( matrix1 ).add( position1 );
vector1.toArray( positions.array, ( count + 0 ) * 3 );
vector2.toArray( positions.array, ( count + 1 ) * 3 );
vector4.toArray( positions.array, ( count + 2 ) * 3 );
vector2.toArray( positions.array, ( count + 3 ) * 3 );
vector3.toArray( positions.array, ( count + 4 ) * 3 );
vector4.toArray( positions.array, ( count + 5 ) * 3 );
vector1.copy( vertex1_2 ).applyMatrix4( matrix2 ).normalize();
vector2.copy( vertex2_2 ).applyMatrix4( matrix2 ).normalize();
vector3.copy( vertex2_1 ).applyMatrix4( matrix1 ).normalize();
vector4.copy( vertex1_1 ).applyMatrix4( matrix1 ).normalize();
vector1.toArray( normals.array, ( count + 0 ) * 3 );
vector2.toArray( normals.array, ( count + 1 ) * 3 );
vector4.toArray( normals.array, ( count + 2 ) * 3 );
vector2.toArray( normals.array, ( count + 3 ) * 3 );
vector3.toArray( normals.array, ( count + 4 ) * 3 );
vector4.toArray( normals.array, ( count + 5 ) * 3 );
color2.toArray( colors.array, ( count + 0 ) * 3 );
color2.toArray( colors.array, ( count + 1 ) * 3 );
color1.toArray( colors.array, ( count + 2 ) * 3 );
color2.toArray( colors.array, ( count + 3 ) * 3 );
color1.toArray( colors.array, ( count + 4 ) * 3 );
color1.toArray( colors.array, ( count + 5 ) * 3 );
count += 6;
}
geometry.drawRange.count = count;
}
//
const direction = new Vector3();
const normal = new Vector3();
const side = new Vector3();
const point1 = new Vector3();
const point2 = new Vector3();
const matrix1 = new Matrix4();
const matrix2 = new Matrix4();
const lastNormal = new Vector3();
const prevDirection = new Vector3();
const rotationAxis = new Vector3();
let isFirstSegment = true;
let endCapStartIndex = null;
let endCapVertexCount = 0;
function calculateRMF() {
if ( isFirstSegment === true ) {
if ( Math.abs( direction.y ) < 0.99 ) {
vector.copy( direction ).multiplyScalar( direction.y );
normal.set( 0, 1, 0 ).sub( vector ).normalize();
} else {
vector.copy( direction ).multiplyScalar( direction.x );
normal.set( 1, 0, 0 ).sub( vector ).normalize();
}
} else {
rotationAxis.crossVectors( prevDirection, direction );
const rotAxisLength = rotationAxis.length();
if ( rotAxisLength > 0.0001 ) {
rotationAxis.divideScalar( rotAxisLength );
vector.addVectors( prevDirection, direction );
const c1 = - 2.0 / ( 1.0 + prevDirection.dot( direction ) );
const dot = lastNormal.dot( vector );
normal.copy( lastNormal ).addScaledVector( vector, c1 * dot );
} else {
normal.copy( lastNormal );
}
}
side.crossVectors( direction, normal ).normalize();
normal.crossVectors( side, direction ).normalize();
if ( isFirstSegment === false ) {
const smoothFactor = 0.3;
normal.lerp( lastNormal, smoothFactor ).normalize();
side.crossVectors( direction, normal ).normalize();
normal.crossVectors( side, direction ).normalize();
}
lastNormal.copy( normal );
prevDirection.copy( direction );
matrix1.makeBasis( side, normal, vector.copy( direction ).negate() );
}
function moveTo( position ) {
point2.copy( position );
lastNormal.set( 0, 1, 0 );
isFirstSegment = true;
endCapStartIndex = null;
endCapVertexCount = 0;
}
function lineTo( position ) {
point1.copy( position );
direction.subVectors( point1, point2 );
const length = direction.length();
if ( length === 0 ) return;
direction.normalize();
calculateRMF();
if ( isFirstSegment === true ) {
color2.copy( color1 );
size2 = size1;
matrix2.copy( matrix1 );
addCap( point2, matrix2, false, size2 );
// End cap is added immediately after start cap and updated in-place
endCapStartIndex = geometry.drawRange.count;
addCap( point1, matrix1, true, size1 );
endCapVertexCount = geometry.drawRange.count - endCapStartIndex;
}
stroke( point1, point2, matrix1, matrix2, size1, size2 );
updateEndCap( point1, matrix1, size1 );
point2.copy( point1 );
matrix2.copy( matrix1 );
color2.copy( color1 );
size2 = size1;
isFirstSegment = false;
}
function setSize( value ) {
size1 = value;
}
function setColor( value ) {
color1.copy( value );
}
//
let count = 0;
function update() {
const start = count;
const end = geometry.drawRange.count;
if ( start === end ) return;
positions.addUpdateRange( start * 3, ( end - start ) * 3 );
positions.needsUpdate = true;
normals.addUpdateRange( start * 3, ( end - start ) * 3 );
normals.needsUpdate = true;
colors.addUpdateRange( start * 3, ( end - start ) * 3 );
colors.needsUpdate = true;
count = end;
}
return {
/**
* The "painted" tube mesh. Must be added to the scene.
*
* @name TubePainter#mesh
* @type {Mesh}
*/
mesh: mesh,
/**
* Moves the current painting position to the given value.
*
* @method
* @name TubePainter#moveTo
* @param {Vector3} position The new painting position.
*/
moveTo: moveTo,
/**
* Draw a stroke from the current position to the given one.
* This method extends the tube while drawing with the XR
* controllers.
*
* @method
* @name TubePainter#lineTo
* @param {Vector3} position The destination position.
*/
lineTo: lineTo,
/**
* Sets the size of newly rendered tube segments.
*
* @method
* @name TubePainter#setSize
* @param {number} size The size.
*/
setSize: setSize,
/**
* Sets the color of newly rendered tube segments.
*
* @method
* @name TubePainter#setColor
* @param {Color} color The color.
*/
setColor: setColor,
/**
* Updates the internal geometry buffers so the new painted
* segments are rendered.
*
* @method
* @name TubePainter#update
*/
update: update
};
}
export { TubePainter };