├── README.md ├── examples ├── assets │ ├── CurveExtras.js │ └── three.min.js └── index.html └── follow-spline.js /README.md: -------------------------------------------------------------------------------- 1 | Three.js Camera Spline Follower 2 | =============================== 3 | 4 | This file makes it easy to have a Three.js camera follow a given open or closed [spline](https://en.wikipedia.org/wiki/Spline_(mathematics)) (curve). 5 | 6 | This code is a modification of [this example](http://threejs.org/examples/#webgl_geometry_extrude_splines) in the Three.js documentation, which can also be viewed [on GitHub](https://github.com/mrdoob/three.js/blob/master/examples/webgl_geometry_extrude_splines.html). 7 | 8 | ___ 9 | 10 | ### Usage 11 | 12 | First include the `follow-spline` JS file. 13 | 14 | Then inside your JS make sure to do the following on initialization: 15 | 16 | ``` 17 | // Set your camera you want to use to the camera that is returned from 18 | // the initialization function 19 | camera = initFollowCamera(, [offset from center of curve], [look ahead boolean]); 20 | 21 | // Create the object from the curve given 22 | addTube(, [number of segments], [closed or not boolean], [number of radius segments], [mesh to use], [scale], [side to render], [color]); 23 | 24 | ``` 25 | 26 | And inside of your render function make sure to call `renderFollowCamera();` 27 | 28 | That's it! 29 | 30 | ___ 31 | 32 | By default, the camera is placed inside of the curve. You can place it outside of the curve by changing the offset when instantiating the camera. 33 | 34 | For a full example, [look at this CodePen](http://codepen.io/Zeaklous/pen/JKXpzy?editors=0010) or download the files and open `/examples/index.html` in a web browser that runs WebGL. -------------------------------------------------------------------------------- /examples/assets/CurveExtras.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A bunch of parametric curves 3 | * @author zz85 4 | * 5 | * Formulas collected from various sources 6 | * http://mathworld.wolfram.com/HeartCurve.html 7 | * http://mathdl.maa.org/images/upload_library/23/stemkoski/knots/page6.html 8 | * http://en.wikipedia.org/wiki/Viviani%27s_curve 9 | * http://mathdl.maa.org/images/upload_library/23/stemkoski/knots/page4.html 10 | * http://www.mi.sanu.ac.rs/vismath/taylorapril2011/Taylor.pdf 11 | * http://prideout.net/blog/?p=44 12 | */ 13 | 14 | // Lets define some curves 15 | THREE.Curves = {}; 16 | 17 | 18 | THREE.Curves.GrannyKnot = THREE.Curve.create( function() {}, 19 | 20 | function( t ) { 21 | 22 | t = 2 * Math.PI * t; 23 | 24 | var x = - 0.22 * Math.cos( t ) - 1.28 * Math.sin( t ) - 0.44 * Math.cos( 3 * t ) - 0.78 * Math.sin( 3 * t ); 25 | var y = - 0.1 * Math.cos( 2 * t ) - 0.27 * Math.sin( 2 * t ) + 0.38 * Math.cos( 4 * t ) + 0.46 * Math.sin( 4 * t ); 26 | var z = 0.7 * Math.cos( 3 * t ) - 0.4 * Math.sin( 3 * t ); 27 | return new THREE.Vector3( x, y, z ).multiplyScalar( 20 ); 28 | 29 | } 30 | ); 31 | 32 | THREE.Curves.HeartCurve = THREE.Curve.create( 33 | 34 | function( s ) { 35 | 36 | this.scale = ( s === undefined ) ? 5 : s; 37 | 38 | }, 39 | 40 | function( t ) { 41 | 42 | t *= 2 * Math.PI; 43 | 44 | var tx = 16 * Math.pow( Math.sin( t ), 3 ); 45 | var ty = 13 * Math.cos( t ) - 5 * Math.cos( 2 * t ) - 2 * Math.cos( 3 * t ) - Math.cos( 4 * t ), tz = 0; 46 | 47 | return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale ); 48 | 49 | } 50 | 51 | ); 52 | 53 | 54 | 55 | // Viviani's Curve 56 | THREE.Curves.VivianiCurve = THREE.Curve.create( 57 | 58 | function( radius ) { 59 | 60 | this.radius = radius; 61 | 62 | }, 63 | 64 | function( t ) { 65 | 66 | t = t * 4 * Math.PI; // Normalized to 0..1 67 | var a = this.radius / 2; 68 | var tx = a * ( 1 + Math.cos( t ) ), 69 | ty = a * Math.sin( t ), 70 | tz = 2 * a * Math.sin( t / 2 ); 71 | 72 | return new THREE.Vector3( tx, ty, tz ); 73 | 74 | } 75 | 76 | ); 77 | 78 | 79 | THREE.Curves.KnotCurve = THREE.Curve.create( 80 | 81 | function() { 82 | 83 | }, 84 | 85 | function( t ) { 86 | 87 | t *= 2 * Math.PI; 88 | 89 | var R = 10; 90 | var s = 50; 91 | var tx = s * Math.sin( t ), 92 | ty = Math.cos( t ) * ( R + s * Math.cos( t ) ), 93 | tz = Math.sin( t ) * ( R + s * Math.cos( t ) ); 94 | 95 | return new THREE.Vector3( tx, ty, tz ); 96 | 97 | } 98 | 99 | ); 100 | 101 | THREE.Curves.HelixCurve = THREE.Curve.create( 102 | 103 | function() { 104 | 105 | }, 106 | 107 | function( t ) { 108 | 109 | var a = 30; // radius 110 | var b = 150; //height 111 | var t2 = 2 * Math.PI * t * b / 30; 112 | var tx = Math.cos( t2 ) * a, 113 | ty = Math.sin( t2 ) * a, 114 | tz = b * t; 115 | 116 | return new THREE.Vector3( tx, ty, tz ); 117 | 118 | } 119 | 120 | ); 121 | 122 | THREE.Curves.TrefoilKnot = THREE.Curve.create( 123 | 124 | function( s ) { 125 | 126 | this.scale = ( s === undefined ) ? 10 : s; 127 | 128 | }, 129 | 130 | function( t ) { 131 | 132 | t *= Math.PI * 2; 133 | var tx = ( 2 + Math.cos( 3 * t ) ) * Math.cos( 2 * t ), 134 | ty = ( 2 + Math.cos( 3 * t ) ) * Math.sin( 2 * t ), 135 | tz = Math.sin( 3 * t ); 136 | 137 | return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale ); 138 | 139 | } 140 | 141 | ); 142 | 143 | THREE.Curves.TorusKnot = THREE.Curve.create( 144 | 145 | function( s ) { 146 | 147 | this.scale = ( s === undefined ) ? 10 : s; 148 | 149 | }, 150 | 151 | function( t ) { 152 | 153 | var p = 3, 154 | q = 4; 155 | t *= Math.PI * 2; 156 | var tx = ( 2 + Math.cos( q * t ) ) * Math.cos( p * t ), 157 | ty = ( 2 + Math.cos( q * t ) ) * Math.sin( p * t ), 158 | tz = Math.sin( q * t ); 159 | 160 | return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale ); 161 | 162 | } 163 | 164 | ); 165 | 166 | 167 | THREE.Curves.CinquefoilKnot = THREE.Curve.create( 168 | 169 | function( s ) { 170 | 171 | this.scale = ( s === undefined ) ? 10 : s; 172 | 173 | }, 174 | 175 | function( t ) { 176 | 177 | var p = 2, 178 | q = 5; 179 | t *= Math.PI * 2; 180 | var tx = ( 2 + Math.cos( q * t ) ) * Math.cos( p * t ), 181 | ty = ( 2 + Math.cos( q * t ) ) * Math.sin( p * t ), 182 | tz = Math.sin( q * t ); 183 | 184 | return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale ); 185 | 186 | } 187 | 188 | ); 189 | 190 | 191 | THREE.Curves.TrefoilPolynomialKnot = THREE.Curve.create( 192 | 193 | function( s ) { 194 | 195 | this.scale = ( s === undefined ) ? 10 : s; 196 | 197 | }, 198 | 199 | function( t ) { 200 | 201 | t = t * 4 - 2; 202 | var tx = Math.pow( t, 3 ) - 3 * t, 203 | ty = Math.pow( t, 4 ) - 4 * t * t, 204 | tz = 1 / 5 * Math.pow( t, 5 ) - 2 * t; 205 | 206 | return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale ); 207 | 208 | } 209 | 210 | ); 211 | 212 | // var scaleTo = function(x, y) { 213 | // var r = y - x; 214 | // return function(t) { 215 | // t * r + x; 216 | // }; 217 | // } 218 | var scaleTo = function( x, y, t ) { 219 | 220 | var r = y - x; 221 | return t * r + x; 222 | 223 | }; 224 | 225 | THREE.Curves.FigureEightPolynomialKnot = THREE.Curve.create( 226 | 227 | function( s ) { 228 | 229 | this.scale = ( s === undefined ) ? 1 : s; 230 | 231 | }, 232 | 233 | function( t ) { 234 | 235 | t = scaleTo( - 4, 4, t ); 236 | var tx = 2 / 5 * t * ( t * t - 7 ) * ( t * t - 10 ), 237 | ty = Math.pow( t, 4 ) - 13 * t * t, 238 | tz = 1 / 10 * t * ( t * t - 4 ) * ( t * t - 9 ) * ( t * t - 12 ); 239 | 240 | return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale ); 241 | 242 | } 243 | 244 | ); 245 | 246 | THREE.Curves.DecoratedTorusKnot4a = THREE.Curve.create( 247 | 248 | function( s ) { 249 | 250 | this.scale = ( s === undefined ) ? 40 : s; 251 | 252 | }, 253 | 254 | function( t ) { 255 | 256 | t *= Math.PI * 2; 257 | var 258 | x = Math.cos( 2 * t ) * ( 1 + 0.6 * ( Math.cos( 5 * t ) + 0.75 * Math.cos( 10 * t ) ) ), 259 | y = Math.sin( 2 * t ) * ( 1 + 0.6 * ( Math.cos( 5 * t ) + 0.75 * Math.cos( 10 * t ) ) ), 260 | z = 0.35 * Math.sin( 5 * t ); 261 | 262 | return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale ); 263 | 264 | } 265 | 266 | ); 267 | 268 | 269 | THREE.Curves.DecoratedTorusKnot4b = THREE.Curve.create( 270 | 271 | function( s ) { 272 | 273 | this.scale = ( s === undefined ) ? 40 : s; 274 | 275 | }, 276 | 277 | function( t ) { 278 | 279 | var fi = t * Math.PI * 2; 280 | var x = Math.cos( 2 * fi ) * ( 1 + 0.45 * Math.cos( 3 * fi ) + 0.4 * Math.cos( 9 * fi ) ), 281 | y = Math.sin( 2 * fi ) * ( 1 + 0.45 * Math.cos( 3 * fi ) + 0.4 * Math.cos( 9 * fi ) ), 282 | z = 0.2 * Math.sin( 9 * fi ); 283 | 284 | return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale ); 285 | 286 | } 287 | 288 | ); 289 | 290 | 291 | THREE.Curves.DecoratedTorusKnot5a = THREE.Curve.create( 292 | 293 | function( s ) { 294 | 295 | this.scale = ( s === undefined ) ? 40 : s; 296 | 297 | }, 298 | 299 | function( t ) { 300 | 301 | var fi = t * Math.PI * 2; 302 | var x = Math.cos( 3 * fi ) * ( 1 + 0.3 * Math.cos( 5 * fi ) + 0.5 * Math.cos( 10 * fi ) ), 303 | y = Math.sin( 3 * fi ) * ( 1 + 0.3 * Math.cos( 5 * fi ) + 0.5 * Math.cos( 10 * fi ) ), 304 | z = 0.2 * Math.sin( 20 * fi ); 305 | 306 | return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale ); 307 | 308 | } 309 | 310 | ); 311 | 312 | THREE.Curves.DecoratedTorusKnot5c = THREE.Curve.create( 313 | 314 | function( s ) { 315 | 316 | this.scale = ( s === undefined ) ? 40 : s; 317 | 318 | }, 319 | 320 | function( t ) { 321 | 322 | var fi = t * Math.PI * 2; 323 | var x = Math.cos( 4 * fi ) * ( 1 + 0.5 * ( Math.cos( 5 * fi ) + 0.4 * Math.cos( 20 * fi ) ) ), 324 | y = Math.sin( 4 * fi ) * ( 1 + 0.5 * ( Math.cos( 5 * fi ) + 0.4 * Math.cos( 20 * fi ) ) ), 325 | z = 0.35 * Math.sin( 15 * fi ); 326 | 327 | return new THREE.Vector3( x, y, z ).multiplyScalar( this.scale ); 328 | 329 | } 330 | 331 | ); 332 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Three.js Spline follower 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /follow-spline.js: -------------------------------------------------------------------------------- 1 | var scene, splineCamera; 2 | 3 | var targetRotation = 0; 4 | 5 | var binormal = new THREE.Vector3(); 6 | var normal = new THREE.Vector3(); 7 | 8 | extrudePath = THREE.Curve.create( 9 | function( s ) { 10 | this.scale = ( s === undefined ) ? 10 : s; 11 | }, 12 | function( t ) { 13 | t *= Math.PI * 2; 14 | var tx = ( 2 + Math.cos( 3 * t ) ) * Math.cos( 2 * t ), 15 | ty = ( 2 + Math.cos( 3 * t ) ) * Math.sin( 2 * t ), 16 | tz = Math.sin( 3 * t ); 17 | return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale ); 18 | } 19 | ); 20 | 21 | var closed2 = true; 22 | var parent; 23 | var tube, tubeMesh; 24 | var lookAhead; 25 | var scale; 26 | var offset; 27 | 28 | function initFollowCamera(sceneVar, offsetVar, lookAheadVar) { 29 | scene = sceneVar; 30 | 31 | parent = new THREE.Object3D(); 32 | parent.position.y = 100; 33 | scene.add( parent ); 34 | 35 | splineCamera = new THREE.PerspectiveCamera( 84, window.innerWidth / window.innerHeight, 0.01, 1000 ); 36 | parent.add( splineCamera ); 37 | 38 | offset = offsetVar || 0; 39 | lookAhead = lookAheadVar || false; 40 | 41 | return splineCamera; 42 | } 43 | 44 | 45 | function addTube(spline, segNum, closeBool, radSeg, mesh, scaleVar, geoSide, color) { 46 | 47 | extrudePath = spline; 48 | var segments = segNum || 100; 49 | closed2 = closeBool || true; 50 | var radiusSegments = radSeg || 3; 51 | mesh = mesh || null; 52 | 53 | if (tubeMesh) parent.remove(tubeMesh); 54 | 55 | tube = new THREE.TubeGeometry(extrudePath, segments, 2, radiusSegments, closed2); 56 | 57 | color = color || 0x2194ce; 58 | geoSide = geoSide || THREE.DoubleSide; 59 | 60 | addGeometry(tube, mesh, color, geoSide); 61 | 62 | scale = scaleVar || 1; 63 | tubeMesh.scale.set( scale, scale, scale ); 64 | 65 | } 66 | 67 | 68 | function addGeometry(geometry, mesh, color, geoSide) { 69 | 70 | // 3d shape 71 | if(mesh === null) { 72 | tubeMesh = THREE.SceneUtils.createMultiMaterialObject( geometry, [ 73 | new THREE.MeshLambertMaterial({ 74 | color: color, 75 | side: geoSide 76 | }), 77 | new THREE.MeshBasicMaterial({ 78 | color: 0x000000, 79 | opacity: 0.3, 80 | wireframe: true, 81 | transparent: true 82 | })]); 83 | } else 84 | tubeMesh = mesh; 85 | 86 | parent.add( tubeMesh ); 87 | 88 | } 89 | 90 | 91 | // Animate the camera along the spline 92 | function renderFollowCamera() { 93 | var time = Date.now(); 94 | var looptime = 20 * 1000; 95 | var t = ( time % looptime ) / looptime; 96 | 97 | var pos = tube.parameters.path.getPointAt( t ); 98 | pos.multiplyScalar( scale ); 99 | 100 | // interpolation 101 | var segments = tube.tangents.length; 102 | var pickt = t * segments; 103 | var pick = Math.floor( pickt ); 104 | var pickNext = ( pick + 1 ) % segments; 105 | 106 | binormal.subVectors( tube.binormals[ pickNext ], tube.binormals[ pick ] ); 107 | binormal.multiplyScalar( pickt - pick ).add( tube.binormals[ pick ] ); 108 | 109 | 110 | var dir = tube.parameters.path.getTangentAt( t ); 111 | 112 | normal.copy( binormal ).cross( dir ); 113 | 114 | // We move on a offset on its binormal 115 | pos.add( normal.clone().multiplyScalar( offset ) ); 116 | 117 | splineCamera.position.copy( pos ); 118 | 119 | // Using arclength for stablization in look ahead. 120 | var lookAt = tube.parameters.path.getPointAt( ( t + 30 / tube.parameters.path.getLength() ) % 1 ).multiplyScalar( scale ); 121 | 122 | // Camera Orientation 2 - up orientation via normal 123 | if (!lookAhead) 124 | lookAt.copy( pos ).add( dir ); 125 | splineCamera.matrix.lookAt(splineCamera.position, lookAt, normal); 126 | splineCamera.rotation.setFromRotationMatrix( splineCamera.matrix, splineCamera.rotation.order ); 127 | 128 | parent.rotation.y += ( targetRotation - parent.rotation.y ) * 0.05; 129 | } --------------------------------------------------------------------------------