Hi there,
I am looking for a way to animate curved arrows going from point A to B. They can be static as well. But I am not sure where I should start.
Also before I do animation I am also not sure how I can create a curved object that looks like an arrow.
If anyone can point me to right direction, I would really appreciate it.
Thank you.
If you’re using THREE.Curve
. (Which I never but this is what I see from the doc)
A simple idea is to use curve.getPoint( t )
while t value is 0-1 (start to end of the curve)
You just need to animate the t value from 0 to 1 from time to time.
And use t value to update your arrow position
arrow.position = curve.getPoint(t);
// Same as above but avoid creating new Vector3 instance.
curve.getPoint(t, arrow.position);
Perhaps these examples will give you a starting point.
From the Collection of examples from discourse.threejs.org
discourse.threejs.hofk.de getPoint( ) getPointAt( )
2021 eXtended eXamples CurveGenerator
Thanks for the suggestion by the question. I am close to finishing a multiform geometry (Open source ).
A test resulted in the following
Not yet perfect!
function curvedArrowCenterline( h ) {
return { x: 0.1 * Math.sin( pi * h ), z: 0 };
}
function arrowOutline( h ) {
return{ y: h < 0.9 ? h : ( h === 0.9 ? 0.8 : h ), r: h < 0.9 ? 0.1 : ( h === 0.9 ? 0.2 : 0 ) }
}
UPDATE
Multiform geometry is easier than my Addon. Produces almost infinite many time-varying geometries with functions
It is not a real space curve, but only a horizontal offset of the layers. So a shear per height segment.
In the addon, it is a correctly bent cylinder. However, the arrowhead is not centered due to the height offset. You can create the arrowhead separately and form a group.
The best solution is certainly a specially created geometry.
Here is an example of bending a generic mesh around a curve
I have it there MotionAlongCurve used.
( From the Collection of examples from discourse.threejs.org )
But it does not bend a single mesh. See the following posts.
Looks like the box needs segmentation greater, than 1, along X-axis.
Yeah, needs more vertices in the axes its being curved in.
In fact
I had no subdivision for the example of the movement.
Then an arrow that is curved can be created easily.
If you need many arrows there is also an instanced version which is useful for animating 100s of objects at the same time such as: XR Koi Garden
Everybody likes Koi fish Spline + DataTexture
I looked to see if you can make a curved arrow from the official example
three.js examples LineGeometry
Should work.
<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/how-to-animate-curved-arrows-in-threejs/36808/10 -->
<head>
<title> WebglLinesFat </title>
<meta charset="utf-8">
</head>
<body>
<script type="module">
import * as THREE from '../jsm/three.module.139.js';
import Stats from '../jsm/stats.module.139.js';
import { OrbitControls } from '../jsm/OrbitControls.139.js';
import { LineMaterial } from '../jsm/LineMaterial.139.js';
import { LineSegments2 } from '../jsm/LineSegments2.139.js';
import { Line2 } from '../jsm/Line2.139.js';
import { LineGeometry } from '../jsm/LineGeometry.139.js';
let line, segments ;
let renderer, scene, camera,controls;
let matLine ;
let stats ;
init();
animate();
function init() {
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
renderer.setPixelRatio( window.devicePixelRatio );
//renderer.setClearColor( 0x000000, 0.0 );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 65, window.innerWidth / window.innerHeight, 0.001, 1000 );
camera.position.set( 0, 5, 25 );
controls = new OrbitControls( camera, renderer.domElement );
// Position and THREE.Color Data
const positions = [];
//const colors = [];
const points = [];
for ( let i = - 20; i < 20; i ++ ) {
const t = i / 3;
//points.push( new THREE.Vector3( t * Math.sin( 2 * t ), t, t * Math.cos( 2 * t ) ) );
points.push( new THREE.Vector3( t * Math.sin( t / 6 ), t, -t ) );
}
const spline = new THREE.CatmullRomCurve3( points );
const divisions = Math.round( points.length ); //const divisions = Math.round( 3 * points.length );
const point = new THREE.Vector3();
//const color = new THREE.Color();
for ( let i = 0, l = divisions; i < l; i ++ ) {
const t = i / l;
spline.getPoint( t, point );
positions.push( point.x, point.y, point.z );
//color.setHSL( t, 1.0, 0.5 );
//colors.push( color.r, color.g, color.b );
}
const lineGeometry = new LineGeometry();
lineGeometry.setPositions( positions );
//lineGeometry.setColors( colors );
matLine = new LineMaterial( {
color: 0x0000ff, // 0xffffff,
linewidth: 0.75, // in world units with size attenuation, pixels otherwise
worldUnits: true,
//vertexColors: true,
//resolution: // to be set by renderer, eventually
//alphaToCoverage: true,
} );
line = new Line2( lineGeometry, matLine );
// line.computeLineDistances();
// line.scale.set( 1, 1, 1 );
line.scale.set( 1.2, 1.2, 1.2 );
scene.add( line );
//const geo = new THREE.BufferGeometry();
//geo.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
//geo.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
window.addEventListener( 'resize', onWindowResize );
onWindowResize();
stats = new Stats();
document.body.appendChild( stats.dom );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
stats.update();
// main scene
//renderer.setClearColor( 0x000000, 0 );
//renderer.setViewport( 0, 0, window.innerWidth, window.innerHeight );
// renderer will set this eventually
//matLine.resolution.set( window.innerWidth, window.innerHeight ); // resolution of the viewport
renderer.render( scene, camera );
}
</script>
</body>
</html>
And here is the arrow.
But you have to create the peak separately, otherwise it is only correct from a distance.
for ( let i = - 25; i <= 0.4; i ++ ) {
points.push( new THREE.Vector3( i / 4 * Math.sin( i / 20 ), 0, -i / 2 ) );
}
for ( let i = - 5; i <= 5; i ++ ) {
points.push( new THREE.Vector3( i / 5 , 0, Math.abs( i / 5 ) ) );
}
const matLine = new LineMaterial( {
color: 0xff00ff,
linewidth: 0.01,
} );
UPDATE:
I just noticed that the visible arrow thickness remains constant as the camera moves further away. I must have removed something from the template that is obviously important.
Try to uncomment this line.
Then the complete line disappears. I’ll probably have to simplify the original example again piece by piece to find the spot.
UPDATE:
I obviously got confused with my various test versions. In one it works. Must bring order into it.
I didn’t get, why does it have to be fat lines?
Is usual THREE.Line()
not an option?
… me neither.
Looked at how to do it with a simple line.
With it you can build a beautiful net.
You can make the balls as transparent bubbles and position things in them.
Here only as a wireframe.
The arrow hits the balls on the straight line connecting the centers.
<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/how-to-animate-curved-arrows-in-threejs/36808/17 -->
<head>
<meta charset="utf-8">
<title> CurvedArrowWithTip </title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body></body>
<script src='../js/three.139.js'></script>
<script src='../js/OrbitControls.139.js'></script>
<script>
// @author hofk
const scene = new THREE.Scene( );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setClearColor( 0xdedede, 1.0 );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const camera = new THREE.PerspectiveCamera( 65, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.set( -1, 3, 4 );
const controls = new THREE.OrbitControls( camera, renderer.domElement );
window.addEventListener( "resize", event => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix( );
renderer.setSize(innerWidth, innerHeight ); } );
const gridHelper = new THREE.GridHelper( 5, 10 );
scene.add( gridHelper );
// input data .................................
const v0 = new THREE.Vector3( -1.9, 0.2, 0.4 );
const r0 = 0.16;
const v1 = new THREE.Vector3( 1.7, 0.5, 0.1 );
const r1 = 0.35;
const bend = 0.8; // 0 .. +-1 .. +-2 ...
const φ = -0.9; // radiant, test with 0
//.............................................
const mesh0 = new THREE.Mesh( new THREE.SphereGeometry( r0 ), new THREE.MeshBasicMaterial( { color: 0x11ab22 , wireframe: true } ) );
mesh0.position.set( v0.x, v0.y, v0.z ) ;
scene.add( mesh0 );
const mesh1 = new THREE.Mesh( new THREE.SphereGeometry( r1 ), new THREE.MeshBasicMaterial( { color: 0x11ab22 , wireframe: true } ) );
mesh1.position.set( v1.x, v1.y, v1.z ) ;
scene.add( mesh1 );
const cpc = 100; // bended: curve points count
let points = []; // curve points
const points3 = [];
let k, b, n, r;
const v = new THREE.Vector3( ).subVectors( v1, v0 );
const vn = v.clone( ).normalize( );
const vlen = v.length( );
v0.add( v.normalize( ).multiplyScalar( r0 ) );
v1.sub( v.normalize( ).multiplyScalar( r1 ) );
const vv = new THREE.Vector3( ).subVectors( v1, v0 ).divideScalar( 2 );
// orthogonal see http://lolengine.net/blog/2013/09/21/picking-orthogonal-vector-combing-coconuts
k = ( Math.abs( vn.x ) + 0. ) % 1; // fract
b = new THREE.Vector3( -vn.y, vn.x - k * vn.z, k * vn.y ).normalize( ); // binormal
if( φ !== 0 ) {
const n = new THREE.Vector3( ).crossVectors( vn, b ); // normal
r = new THREE.Vector3( ).addVectors( b.multiplyScalar( Math.cos( φ ) ), n.multiplyScalar( Math.sin( φ ) ) ); // rotated
}
const vm = new THREE.Vector3( ).add( v0 ).add( vv ).add( ( φ === 0 ? b : r ).multiplyScalar( vlen / 4 ).multiplyScalar( bend ) );
points3.push( v0, vm, v1 );
points = new THREE.CatmullRomCurve3( points3 ).getPoints( cpc );
const line = new THREE.Line( new THREE.BufferGeometry( ).setFromPoints( points ), new THREE.LineBasicMaterial( { color: 0xff1122 } ) );
const tipHeight = 0.2;
const tipGeometry = new THREE.ConeGeometry( tipHeight / 4, tipHeight );
tipGeometry.translate( 0, -tipHeight / 2, 0 );
const tipMesh = new THREE.Mesh( tipGeometry , new THREE.MeshBasicMaterial( { color: 0xff1122 , wireframe: true } ) );
const vt = new THREE.Vector3( ).subVectors( points[ cpc ], points[ cpc - Math.floor( 0.05 * cpc ) ] ).normalize( );
tipMesh.quaternion.setFromUnitVectors( new THREE.Vector3( 0, 1, 0 ), vt );
tipMesh.position.set( v1.x, v1.y, v1.z );
line.add( tipMesh ); // scene.add( tipMesh );
scene.add( line );
animate();
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
</script>
</html>