Dart DocumentationthreeTubeGeometry

TubeGeometry class

@author WestLangley / https://github.com/WestLangley @author zz85 / https://github.com/zz85 @author miningold / https://github.com/miningold

Modified from the TorusKnotGeometry by @oosmoxiecode

Creates a tube which extrudes along a 3d spline

Uses parallel transport frames as described in http://www.cs.indiana.edu/pub/techreports/TR425.pdf

class TubeGeometry extends Geometry {

 var path, segments;
 num nSegments, radius, segmentsRadius;
 bool closed;

 var grid;
 List<Vector3> tangents, normals, binormals;

 Object3D debug;

 TubeGeometry ( path, [segments = 64, this.radius = 1.0, this.segmentsRadius = 8, closed = false, bool debug])
   : grid = [], super() {

   if ( debug ) this.debug = new Object3D();

   var tangent,
       normal,
       binormal,

       numpoints = segments + 1,

       x, y, z,
       tx, ty, tz,
       u, v,

       cx, cy,
       pos, pos2 = new Vector3.zero(),
       i, j,
       ip, jp,
       a, b, c, d,
       uva, uvb, uvc, uvd;

   var frames = _frenetFrames(path, segments, closed);

   // consruct the grid
   grid.length = numpoints;

   for ( i = 0; i < numpoints; i++ ) {

     grid[ i ] = new List(this.segmentsRadius);

     u = i / ( numpoints - 1 );

     pos = path.getPointAt( u );

     tangent = tangents[ i ];
     normal = normals[ i ];
     binormal = binormals[ i ];

     if ( debug ) {

       this.debug.add(new ArrowHelper(tangent, pos, radius, 0x0000ff));
       this.debug.add(new ArrowHelper(normal, pos, radius, 0xff0000));
       this.debug.add(new ArrowHelper(binormal, pos, radius, 0x00ff00));

     }

     for ( j = 0; j < this.segmentsRadius; j++ ) {

       v = j / this.segmentsRadius * 2 * Math.PI;

       cx = -this.radius * Math.cos( v ); // TODO: Hack: Negating it so it faces outside.
       cy = this.radius * Math.sin( v );

       pos2.setFrom( pos );
       pos2.x += cx * normal.x + cy * binormal.x;
       pos2.y += cx * normal.y + cy * binormal.y;
       pos2.z += cx * normal.z + cy * binormal.z;

       this.grid[ i ][ j ] = _vert( pos2.x, pos2.y, pos2.z );

     }
   }


   // construct the mesh

   for ( i = 0; i < this.nSegments; i++ ) {

     for ( j = 0; j < this.segmentsRadius; j++ ) {

       ip = ( closed ) ? (i + 1) % this.nSegments : i + 1;
       jp = (j + 1) % this.segmentsRadius;

       a = this.grid[ i ][ j ];    // *** NOT NECESSARILY PLANAR ! ***
       b = this.grid[ ip ][ j ];
       c = this.grid[ ip ][ jp ];
       d = this.grid[ i ][ jp ];

       uva = new UV( i / this.nSegments, j / this.segmentsRadius );
       uvb = new UV( ( i + 1 ) / this.nSegments, j / this.segmentsRadius );
       uvc = new UV( ( i + 1 ) / this.nSegments, ( j + 1 ) / this.segmentsRadius );
       uvd = new UV( i / this.nSegments, ( j + 1 ) / this.segmentsRadius );

       this.faces.add( new Face4( a, b, c, d ) );
       this.faceVertexUvs[ 0 ].add( [ uva, uvb, uvc, uvd ] );

     }
   }

   computeCentroids();
   computeFaceNormals();
   computeVertexNormals();

 }


 _vert( x, y, z ) {
   vertices.add( new Vector3(x, y, z) );
   return vertices.length - 1;
 }


 // FrenetFrames


 TubeGeometry.FrenetFrames(path, segments, closed) {
   _frenetFrames(path, segments, closed);
 }

// For computing of Frenet frames, exposing the tangents, normals and binormals the spline
 _frenetFrames(ppath, psegments, pclosed) {

   this.path = ppath;
   this.segments = psegments;

   this.closed = pclosed;

   var
     tangent = new Vector3.zero(),
     normal = new Vector3.zero(),
     binormal = new Vector3.zero(),

     mat = new Matrix4.identity(),
     theta,
     epsilon = 0.0001,
     smallest,

     tx, ty, tz,
     i, u, v;

   if(segments is num){
     var length = segments;
     segments = [];
       for ( i = 1; i <= length; i++ ) {
         segments.add( i / ( length ));
       }
   }
   this.nSegments = segments.length;
   var numpoints = this.nSegments + 1;

   // expose internals
   tangents = new List<Vector3>(numpoints);
   normals = new List<Vector3>(numpoints);
   binormals = new List<Vector3>(numpoints);

   // compute the tangent vectors for each segment on the path

   for ( i = 0; i < numpoints; i++ ) {
     tangents[ i ] = (i == 0)? path.getTangentAt( 0 ): path.getTangentAt( segments[i - 1] );
     tangents[ i ].normalize();
   }

   _initialNormal1([lastBinormal = null]) {
     // fixed start binormal. Has dangers of 0 vectors
     if (lastBinormal==null)
       lastBinormal = new Vector3( 0.0, 0.0, 1.0 );
     normals[0] = lastBinormal.cross(tangents[0]).normalize();
     binormals[0] = tangents[0].cross(normals[0]).normalize();
   }

   _initialNormal2() {
     // This uses the Frenet-Serret formula for deriving binormal
     var t2 = path.getTangentAt( epsilon );

     normals[0] = (t2 - tangents[0]).normalize();
     binormals[0] = tangents[0].cross(normals[0]);

     normals[0] = binormals[0].cross(tangents[0]).normalize(); // last binormal x tangent
     binormals[0] = tangents[0].cross(normals[0]).normalize();

   }

   _initialNormal3() {
     // select an initial normal vector perpenicular to the first tangent vector,
     // and in the direction of the smallest tangent xyz component

     smallest = double.INFINITY;
     tx = ( tangents[ 0 ].x ).abs();
     ty = ( tangents[ 0 ].y ).abs();
     tz = ( tangents[ 0 ].z ).abs();

     if ( tx <= smallest ) {
       smallest = tx;
       normal.setValues( 1.0, 0.0, 0.0 );
     }

     if ( ty <= smallest ) {
       smallest = ty;
       normal.setValues( 0.0, 1.0, 0.0 );
     }

     if ( tz <= smallest ) {
       normal.setValues( 0.0, 0.0, 1.0 );
     }

     Vector3 vec = tangents[0].cross(normal).normalize();

     normals[0] = tangents[0].cross(vec);
     binormals[0] = tangents[0].cross(normals[0]);
   }

   _initialNormal3();


   // compute the slowly-varying normal and binormal vectors for each segment on the path

   for ( i = 1; i < numpoints; i++ ) {

     normals[ i ] = normals[ i-1 ].clone();

     binormals[ i ] = binormals[ i-1 ].clone();

     Vector3 vec = tangents[i-1].cross(tangents[i]);

     if ( vec.length > epsilon ) {

       vec.normalize();

       theta = Math.acos( tangents[ i-1 ].dot( tangents[ i ] ) );

       normals[ i ].applyProjection(makeRotationAxis ( mat, vec, theta ));
     }

     binormals[i] = tangents[i].cross(normals[i]);

   }


   // if the curve is closed, postprocess the vectors so the first and last normal vectors are the same

   if ( closed ) {

     theta = Math.acos( normals[ 0 ].dot( normals[ numpoints-1 ] ) );
     theta /= ( numpoints - 1 );

     if ( tangents[ 0 ].dot( normals[0].cross(normals[numpoints-1]) )  > 0 ) {

       theta = -theta;

     }

     for ( i = 1; i < numpoints; i++ ) {

       // twist a little...
       normals[ i ].applyProjection( makeRotationAxis( mat, tangents[ i ], theta * i ) );
       binormals[ i ] = tangents[i].cross(normals[i]);

     }

   }
 }
}

Extends

Geometry > TubeGeometry

Constructors

new TubeGeometry(path, [segments = 64, num radius = 1.0, num segmentsRadius = 8, closed = false, bool debug]) #

Creates a new Object instance.

Object instances have no meaningful state, and are only useful through their identity. An Object instance is equal to itself only.

docs inherited from Object
TubeGeometry ( path, [segments = 64, this.radius = 1.0, this.segmentsRadius = 8, closed = false, bool debug])
 : grid = [], super() {

 if ( debug ) this.debug = new Object3D();

 var tangent,
     normal,
     binormal,

     numpoints = segments + 1,

     x, y, z,
     tx, ty, tz,
     u, v,

     cx, cy,
     pos, pos2 = new Vector3.zero(),
     i, j,
     ip, jp,
     a, b, c, d,
     uva, uvb, uvc, uvd;

 var frames = _frenetFrames(path, segments, closed);

 // consruct the grid
 grid.length = numpoints;

 for ( i = 0; i < numpoints; i++ ) {

   grid[ i ] = new List(this.segmentsRadius);

   u = i / ( numpoints - 1 );

   pos = path.getPointAt( u );

   tangent = tangents[ i ];
   normal = normals[ i ];
   binormal = binormals[ i ];

   if ( debug ) {

     this.debug.add(new ArrowHelper(tangent, pos, radius, 0x0000ff));
     this.debug.add(new ArrowHelper(normal, pos, radius, 0xff0000));
     this.debug.add(new ArrowHelper(binormal, pos, radius, 0x00ff00));

   }

   for ( j = 0; j < this.segmentsRadius; j++ ) {

     v = j / this.segmentsRadius * 2 * Math.PI;

     cx = -this.radius * Math.cos( v ); // TODO: Hack: Negating it so it faces outside.
     cy = this.radius * Math.sin( v );

     pos2.setFrom( pos );
     pos2.x += cx * normal.x + cy * binormal.x;
     pos2.y += cx * normal.y + cy * binormal.y;
     pos2.z += cx * normal.z + cy * binormal.z;

     this.grid[ i ][ j ] = _vert( pos2.x, pos2.y, pos2.z );

   }
 }


 // construct the mesh

 for ( i = 0; i < this.nSegments; i++ ) {

   for ( j = 0; j < this.segmentsRadius; j++ ) {

     ip = ( closed ) ? (i + 1) % this.nSegments : i + 1;
     jp = (j + 1) % this.segmentsRadius;

     a = this.grid[ i ][ j ];    // *** NOT NECESSARILY PLANAR ! ***
     b = this.grid[ ip ][ j ];
     c = this.grid[ ip ][ jp ];
     d = this.grid[ i ][ jp ];

     uva = new UV( i / this.nSegments, j / this.segmentsRadius );
     uvb = new UV( ( i + 1 ) / this.nSegments, j / this.segmentsRadius );
     uvc = new UV( ( i + 1 ) / this.nSegments, ( j + 1 ) / this.segmentsRadius );
     uvd = new UV( i / this.nSegments, ( j + 1 ) / this.segmentsRadius );

     this.faces.add( new Face4( a, b, c, d ) );
     this.faceVertexUvs[ 0 ].add( [ uva, uvb, uvc, uvd ] );

   }
 }

 computeCentroids();
 computeFaceNormals();
 computeVertexNormals();

}

new TubeGeometry.FrenetFrames(path, segments, closed) #

TubeGeometry.FrenetFrames(path, segments, closed) {
 _frenetFrames(path, segments, closed);
}

Properties

var animation #

inherited from Geometry
var bones, animation

List<Vector3> binormals #

List<Vector3> tangents, normals, binormals

var bones #

inherited from Geometry
var bones

BoundingBox boundingBox #

inherited from Geometry
BoundingBox boundingBox

BoundingSphere boundingSphere #

inherited from Geometry
BoundingSphere boundingSphere

bool closed #

bool closed

List colors #

inherited from Geometry
List colors

Object3D debug #

Object3D debug

List<Face> faces #

inherited from Geometry
List<Face> faces

List faceUvs #

inherited from Geometry
List faceUvs

List<List> faceVertexUvs #

inherited from Geometry
List<List> faceVertexUvs

var grid #

var grid

bool hasTangents #

inherited from Geometry
bool hasTangents

int id #

inherited from Geometry
int id

bool isDynamic #

inherited from Geometry
bool get isDynamic => _dynamic;
set isDynamic(bool value) => _dynamic = value;

List lineDistances #

inherited from Geometry
List lineDistances

List materials #

inherited from Geometry
List materials

List morphColors #

inherited from Geometry
List morphColors

List morphNormals #

inherited from Geometry
List morphColors, morphNormals

List<MorphTarget> morphTargets #

inherited from Geometry
List<MorphTarget> morphTargets

String name #

inherited from Geometry
String name

List<Vector3> normals #

List<Vector3> tangents, normals

num nSegments #

num nSegments

var path #

var path

num radius #

num nSegments, radius

var segments #

var path, segments

num segmentsRadius #

num nSegments, radius, segmentsRadius

List skinIndices #

inherited from Geometry
List skinWeights, skinIndices

List skinWeights #

inherited from Geometry
List skinWeights

List<Vector3> tangents #

List<Vector3> tangents

List<Vector3> vertices #

inherited from Geometry
List<Vector3> vertices

Operators

dynamic operator [](String key) #

inherited from Geometry
operator [] (String key) => _data[key];

dynamic operator []=(String key, value) #

inherited from Geometry
operator []= (String key, value) => _data[key] = value;

Methods

void applyMatrix(Matrix4 matrix) #

inherited from Geometry
void applyMatrix( Matrix4 matrix ) {
 Matrix4 matrixRotation = new Matrix4.identity();
 extractRotation( matrixRotation, matrix);

 vertices.forEach((vertex) =>  vertex.applyProjection(matrix));

 faces.forEach((face) {

   face.normal.applyProjection(matrixRotation);

   face.vertexNormals.forEach((normal) => normal.applyProjection(matrixRotation));

   face.centroid.applyProjection(matrix);
 });
}

dynamic clone() #

inherited from Geometry
clone() {

 // TODO

}

void computeBoundingBox() #

inherited from Geometry
void computeBoundingBox() {
 if ( boundingBox == null ) {
   boundingBox = new BoundingBox( min: new Vector3.zero(), max: new Vector3.zero() );
 }

 if ( vertices.length > 0 ) {
   Vector3 position, firstPosition = vertices[ 0 ];

   boundingBox.min.setFrom( firstPosition );
   boundingBox.max.setFrom( firstPosition );

   Vector3 min = boundingBox.min,
           max = boundingBox.max;

   num vl = vertices.length;
   for ( int v = 1; v < vl; v ++ ) {
     position = vertices[ v ];

     if ( position.x < min.x ) {
       min.x = position.x;
     } else if ( position.x > max.x ) {
       max.x = position.x;
     }

     if ( position.y < min.y ) {
       min.y = position.y;
     } else if ( position.y > max.y ) {
       max.y = position.y;
     }

     if ( position.z < min.z ) {
       min.z = position.z;
     } else if ( position.z > max.z ) {
       max.z = position.z;
     }
   }
 }
}

void computeBoundingSphere() #

inherited from Geometry
void computeBoundingSphere() {
 num radiusSq;

 var maxRadiusSq = vertices.fold(0, (num curMaxRadiusSq, Vector3 vertex) {
   radiusSq = vertex.length2;
   return ( radiusSq > curMaxRadiusSq ) ?  radiusSq : curMaxRadiusSq;
 });

 boundingSphere = new BoundingSphere(radius: Math.sqrt(maxRadiusSq) );
}

void computeCentroids() #

inherited from Geometry
void computeCentroids() {

 faces.forEach((Face face) {

   face.centroid.setValues( 0.0, 0.0, 0.0 );

   face.indices.forEach((idx) {
     face.centroid.add( vertices[ idx ] );
   });

   face.centroid /= face.size.toDouble();

 });
}

void computeFaceNormals() #

inherited from Geometry
void computeFaceNormals() {
 faces.forEach((face) {

   var vA = vertices[ face.a ],
       vB = vertices[ face.b ],
       vC = vertices[ face.c ];

   Vector3 cb = vC - vB;
   Vector3 ab = vA - vB;
   cb = cb.cross( ab );

   cb.normalize();

   face.normal = cb;

 });
}

void computeTangents() #

inherited from Geometry
void computeTangents() {
 // based on http://www.terathon.com/code/tangent.html
 // tangents go to vertices

 var f, fl, face;
 num i, il, vertexIndex, test, w;
 Vector3 vA, vB, vC;
 UV uvA, uvB, uvC;

 List uv;

 num x1, x2, y1, y2, z1, z2, s1, s2, t1, t2, r;

 Vector3 sdir = new Vector3.zero(),
         tdir = new Vector3.zero(),
         tmp = new Vector3.zero(),
         tmp2 = new Vector3.zero(),
         n = new Vector3.zero(),
         t;

 List<Vector3> tan1 = vertices.map((_) => new Vector3.zero()).toList(),
               tan2 = vertices.map((_) => new Vector3.zero()).toList();

 var handleTriangle = ( context, a, b, c, ua, ub, uc ) {

   vA = context.vertices[ a ];
   vB = context.vertices[ b ];
   vC = context.vertices[ c ];

   uvA = uv[ ua ];
   uvB = uv[ ub ];
   uvC = uv[ uc ];

   x1 = vB.x - vA.x;
   x2 = vC.x - vA.x;
   y1 = vB.y - vA.y;
   y2 = vC.y - vA.y;
   z1 = vB.z - vA.z;
   z2 = vC.z - vA.z;

   s1 = uvB.u - uvA.u;
   s2 = uvC.u - uvA.u;
   t1 = uvB.v - uvA.v;
   t2 = uvC.v - uvA.v;

   r = 1.0 / ( s1 * t2 - s2 * t1 );
   sdir.setValues( ( t2 * x1 - t1 * x2 ) * r,
       ( t2 * y1 - t1 * y2 ) * r,
       ( t2 * z1 - t1 * z2 ) * r );
   tdir.setValues( ( s1 * x2 - s2 * x1 ) * r,
         ( s1 * y2 - s2 * y1 ) * r,
         ( s1 * z2 - s2 * z1 ) * r );

   tan1[ a ].add( sdir );
   tan1[ b ].add( sdir );
   tan1[ c ].add( sdir );

   tan2[ a ].add( tdir );
   tan2[ b ].add( tdir );
   tan2[ c ].add( tdir );

 };

 fl = this.faces.length;

 for ( f = 0; f < fl; f ++ ) {

   face = this.faces[ f ];
   UV uv = faceVertexUvs[ 0 ][ f ]; // use UV layer 0 for tangents

   // TODO - Come up with a way to handle an arbitrary number of vertexes
   var triangles = [];
   if ( face.size == 3 ) {
     triangles.add([0, 1, 2]);
   } else if ( face.size == 4 ) {
     triangles.add([0, 1, 3]);
     triangles.add([1, 2, 3]);
   }

   triangles.forEach((t) {
     handleTriangle( this, face.indices[t[0]], face.indices[t[1]], face.indices[t[2]], t[0], t[1], t[2] );
   });
 }

 faces.forEach((face) {

   il = face.vertexNormals.length;

   for ( i = 0; i < il; i++ ) {

     n.setFrom( face.vertexNormals[ i ] );

     vertexIndex = face.indices[i];

     t = tan1[ vertexIndex ];

     // Gram-Schmidt orthogonalize

     tmp.setFrom( t );
     tmp.sub( n.scale( n.dot( t ) ) ).normalize();

     // Calculate handedness

     tmp2 = face.vertexNormals[i].cross(t);
     test = tmp2.dot( tan2[ vertexIndex ] );
     w = (test < 0.0) ? -1.0 : 1.0;

     face.vertexTangents[ i ] = new Vector4( tmp.x, tmp.y, tmp.z, w );

   }

 });

 hasTangents = true;

}

void computeVertexNormals() #

inherited from Geometry
void computeVertexNormals() {

 List<Vector3> vertices;


 // create internal buffers for reuse when calling this method repeatedly
 // (otherwise memory allocation / deallocation every frame is big resource hog)
 if ( __tmpVertices == null ) {

   __tmpVertices = [];
   this.vertices.forEach((_) => __tmpVertices.add(new Vector3.zero()));
   vertices = __tmpVertices;

   faces.forEach((face) {
     face.vertexNormals = new List.generate(face.size, (_) => new Vector3.zero(), growable: false);
   });

 } else {
   vertices = __tmpVertices;

   var vl = this.vertices.length;
   for ( var v = 0; v < vl; v ++ ) {
     vertices[ v ].setValues( 0.0, 0.0, 0.0 );
   }

 }

 faces.forEach((Face face) {

   face.indices.forEach((idx) {
     vertices[ idx ].add( face.normal );
   });

 });

 vertices.forEach((v) => v.normalize());

 faces.forEach((Face face) {

   var i = 0;
   face.indices.forEach((idx) {
     face.vertexNormals[ i++ ].setFrom( vertices[ idx ] );
   });

 });
}

int mergeVertices() #

inherited from Geometry
int mergeVertices() {
 Map verticesMap = {}; // Hashmap for looking up vertice by position coordinates (and making sure they are unique)
 List<Vector3> unique = [];
 List<int> changes = [];

 String key;
 int precisionPoints = 4; // number of decimal points, eg. 4 for epsilon of 0.0001
 num precision = Math.pow( 10, precisionPoints );
 int i, il;
 var abcd = 'abcd', o, k, j, jl, u;

 Vector3 v;
 il = this.vertices.length;

 for( i = 0; i < il; i++) {
   v = this.vertices[i];

   key = [ ( v.x * precision ).round().toStringAsFixed(0),
                         ( v.y * precision ).round().toStringAsFixed(0),
                         ( v.z * precision ).round().toStringAsFixed(0) ].join('_' );

   if ( verticesMap[ key ] == null ) {
     verticesMap[ key ] = i;
     unique.add( v );
     //TODO: pretty sure this is an acceptable change in syntax here:
     //changes[ i ] = unique.length - 1;
     changes.add( unique.length - 1);
   } else {
     //print('Duplicate vertex found. $i could be using  ${verticesMap[key]}');
     //print('changes len ${changes.length} add at i = $i');
     //changes[ i ] = changes[ verticesMap[ key ] ];
     changes.add( changes[ verticesMap[ key ] ] );
   }

 }


 // Start to patch face indices

 faces.forEach((Face face) {
   for (var i = 0; i < face.size; i++) {
     face.indices[i] = changes[ face.indices[i] ];

     /* TODO

     // check dups in (a, b, c, d) and convert to -> face3

     var o = [ face.a, face.b, face.c, face.d ];

     for ( var k = 3; k > 0; k -- ) {

       if ( o.indexOf( face[ abcd[ k ] ] ) != k ) {

         // console.log('faces', face.a, face.b, face.c, face.d, 'dup at', k);

         o.removeAt( k );

         this.faces[ i ] = new THREE.Face3( o[0], o[1], o[2], face.normal, face.color, face.materialIndex );

         for ( j = 0, jl = this.faceVertexUvs.length; j < jl; j ++ ) {

           u = this.faceVertexUvs[ j ][ i ];
           if ( u ) u.removeAt( k );

         }

         this.faces[ i ].vertexColors = face.vertexColors;

         break;
       }

     }*/

   }
 });

 // Use unique set of vertices
 var diff = vertices.length - unique.length;
 vertices = unique;
 return diff;
}