1

I would like to animate a bezier curve in ThreeJS. The start, end and control points will update. Eventually I will need to have many curves animating at once. What's the most efficient way to do this?

If you run the code snippet below, you'll see that I'm creating the Bezier object, Geometry and Line each time the frame renders. I'm removing the previous line from the scene and then adding the new, updated line. Is there a better way? Perhaps updating only the geometry and not adding the line again?

var camera, scene, renderer, geometry, material, mesh;

init();
animate();

/**
 Create the scene, camera, renderer
*/
function init() {
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.z = 500;
  scene.add(camera);
  addCurve();
  renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);
}

/**
 Add the initial bezier curve to the scene
*/
function addCurve() {
  testPoint = 0;
  curve = new THREE.CubicBezierCurve3(
    new THREE.Vector3( testPoint, 0, 0 ),
    new THREE.Vector3( -5, 150, 0 ),
    new THREE.Vector3( 20, 150, 0 ),
    new THREE.Vector3( 10, 0, 0 )
  );
  curveGeometry = new THREE.Geometry();
  curveGeometry.vertices = curve.getPoints( 50 );
  curveMaterial = new THREE.LineBasicMaterial( { color : 0xff0000 } );
  curveLine = new THREE.Line( curveGeometry, curveMaterial );
  scene.add(curveLine);
}

/**
 On each frame render, remove the old line, create new curve, geometry and add the new line
*/
function updateCurve() {
  testPoint ++;
  scene.remove(curveLine);
  curve = new THREE.CubicBezierCurve3(
    new THREE.Vector3( testPoint, 0, 0 ),
    new THREE.Vector3( -5, 150, 0 ),
    new THREE.Vector3( 20, 150, 0 ),
    new THREE.Vector3( 10, 0, 0 )
  );
  curveGeometry = new THREE.Geometry();
  curveGeometry.vertices = curve.getPoints( 50 );
  curveLine = new THREE.Line( curveGeometry, curveMaterial );
  scene.add(curveLine);
}

function animate() {
  requestAnimationFrame(animate);
  render();
}

function render() {
  updateCurve();
  renderer.render(scene, camera);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r83/three.min.js"></script>
WestLangley
  • 102,557
  • 10
  • 276
  • 276
shackleton
  • 701
  • 1
  • 12
  • 27
  • 1
    (1) Type `renderer.info` into the console. You not not disposing of your geometries correctly. Instead, create one `Line` and update the vertices. See http://stackoverflow.com/questions/31399856/drawing-a-line-with-three-js-dynamically/31411794#31411794 (2) Avoid `new` in tight loops. Create objects and reuse them. – WestLangley Jan 18 '17 at 23:02

1 Answers1

4

Creating new lines per each frame is very exepnsive opertation.

What is that the best way to create animated curves?

Probably using shaders. But it can take much more time to implement, so if my next suggestions will be enough for you, just them.

Improve curve updating in your code

I tried not to change a lot of your code. Marked edited places with "// EDITED" comment. I added an array cause you said that there will be many curves.

Explanation

  • As @WestLangley said, try to avoid using new keyword inside animation loop.
  • CubicBezierCurve3 has v0, v1, v2 and v3 attributes. Those are THREE.Vector3`s that you provide from the beginning. .getPoints() uses them to return you vertices array. So you can simply change them and no new keyword is needed. See this line for more details.
  • Rather then recreating line, in your case you can simply update geometry. As you have a THREE.Line - your geometry only needs vertices. After changing vertices you should set geometry.verticesNeedUpdate = true or Three.js will ignore your changes.

var camera, scene, renderer, geometry, material, mesh, curves = [];

init();
animate();

/**
 Create the scene, camera, renderer
*/
function init() {
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.z = 500;
  scene.add(camera);
  addCurve();
  renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);
}

/**
 Add the initial bezier curve to the scene
*/
function addCurve() {
  testPoint = 0;
  curve = new THREE.CubicBezierCurve3(
    new THREE.Vector3( testPoint, 0, 0 ),
    new THREE.Vector3( -5, 150, 0 ),
    new THREE.Vector3( 20, 150, 0 ),
    new THREE.Vector3( 10, 0, 0 )
  );
  curveGeometry = new THREE.Geometry();
  curveGeometry.vertices = curve.getPoints( 50 );
  curveMaterial = new THREE.LineBasicMaterial( { color : 0xff0000 } );
  curveLine = new THREE.Line( curveGeometry, curveMaterial );
  scene.add(curveLine);
  
  // EDITED
  curves.push(curveLine); // Add curve to curves array
  curveLine.curve = curve; // Link curve object to this curveLine
}

/**
 On each frame render, remove the old line, create new curve, geometry and add the new line
*/
function updateCurve() {
  testPoint ++;
  
  // EDITED
  for (var i = 0, l = curves.length; i < l; i++) {
    var curveLine = curves[i];
    
    // Update x value of v0 vector
    curveLine.curve.v0.x = testPoint;
    // Update vertices
    curveLine.geometry.vertices = curveLine.curve.getPoints( 50 ); 
    // Let's three.js know that vertices are changed
    curveLine.geometry.verticesNeedUpdate = true;
  }
}

function animate() {
  requestAnimationFrame(animate);
  render();
}

function render() {
  updateCurve();
  renderer.render(scene, camera);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r83/three.min.js"></script>