600 lines
15 KiB
JavaScript
600 lines
15 KiB
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 };
|