[Edit: For my own testing purposes, it would actually be great if I could just find out what the current orientation of an object is. For instance, if I could just print to the console whatever is the axis of symmetry of the cylinder, that would really help me to figure out what my current code is doing so that I can fix it. But after scanning the documentation, I haven’t seen a function that will tell me the orientation.]
I would like to be able to customize the appearance of an arrow (in particular the width). So I’ve been trying to build it from scratch but it seems like it takes a ton of calculation. I’m wondering if I’m doing this in the best possible way.
The basic behavior that I’d like is to have a function customArrow( fx, fy, fz, ix, iy, iz, thickness, color)
where you specify the coordinates of the final position of the arrow (fx,fy,fz
) and the initial position (i.e. where the “tail” of the arrow is, ix,iy,iz
), along with some other customizations like thickness, color, and maybe other things I’ll add on later.
In order to implement this I’ve tried building the shaft of the arrow as a TubeGeometry object. I believe ThreeJS initializes this along the y-axis. So I then need to both rotate and translate this into the position where I want it to be. This has involved a bunch of math, and I’m sure if I keep working at it, I can eventually get it.
But is there a better way to do this? Here’s what I’ve been trying and right now I can see that it’s wrong, but it gives a sense of how I’m approaching the problem and how much work this seems to involve.
export function customArrow( tx, ty, tz, ox=0, oy=0, oz=0, color='yellow', thickness=.01 ) {
const ovec = new THREE.Vector3( ox,oy,oz );
const tvec = new THREE.Vector3( tx,ty,tz );
const dvec = (tvec.clone()).sub(ovec);
const mag = dvec.length();
const tube = new THREE.CylinderGeometry( thickness, thickness, mag, 50);
const mater = new THREE.MeshBasicMaterial( {color: color} );
const cylinder = new THREE.Mesh( tube, mater );
cylinder.position.set(0,0,0);
const quaternion = new THREE.Quaternion();
const axis = new THREE.Vector3( tz-oz,0,ox-tx );
const angle = Math.acos( (ty-oy)/mag )
quaternion.setFromAxisAngle(
axis, angle
);
cylinder.setRotationFromQuaternion(quaternion);
cylinder.position.set(...(ovec.add(dvec.multiplyScalar(.5)).toArray()));
const pyramid = new THREE.CylinderGeometry( 0, thickness*2, mag/5, 50);
const head = new THREE.Mesh(pyramid,mater);
head.position.set(0,0,0);
head.setRotationFromQuaternion(quaternion);
head.position.set(...((tvec.addScaledVector(dvec,-mag/5)).toArray()));
const group = new THREE.Group();
group.add(cylinder);
group.add(head);
return group;
};
Here is a working example of the difficulty I’m facing. I’m using ArrowHelper
just to draw the orientation axes, in order to be able to see what cyl
looks like.
I’ve also eliminated the function and tried to implement this directly, because I have found that there is something in the geometry that I’m getting wrong. Once I get the geometry right I’ll abstract it into a function.
import * as THREE from 'three';
function main() {
const canvas = document.querySelector( '#c' );
const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
const fov = 75;
const aspect = 1;
const near = 0.1;
const far = 10;
const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
const scene = new THREE.Scene();
const origin = new THREE.Vector3( 0,0,0 );
const xdir = new THREE.Vector3( 1,0,0 );
const ydir = new THREE.Vector3( 0,1,0 );
const zdir = new THREE.Vector3( 0,0,1 );
{
const color = 0xFFFFFF;
const intensity = 3;
const light = new THREE.DirectionalLight( 'white', intensity );
light.position.set( - 1, 2, 4 );
scene.add( light );
}
scene.add(
new THREE.ArrowHelper(xdir, origin, 3, "red"),
new THREE.ArrowHelper(ydir, origin, 2, "white"),
new THREE.ArrowHelper(zdir, origin, 1, "blue")
);
const mat = new THREE.MeshPhongMaterial( { color: 0x44aa88 } ); // greenish blue
// Here is where I start drawing the cylinder.
// Terminal vector coordinates.
const tx = 3;
const ty = -1;
const tz = 1;
// Original vector coordinates.
const ox = 2;
const oy = 0;
const oz = -1;
const ovec = new THREE.Vector3(ox,oy,oz);
const tvec = new THREE.Vector3(tx,ty,tz);
// Direction vector from the original to the terminal vector.
const dvec = (tvec.clone()).sub(ovec);
const mag = dvec.length();
// The cylinder mesh object.
const cylGeom = new THREE.CylinderGeometry( .05, .05, mag, 50);
const cyl = new THREE.Mesh( cylGeom, mat );
const rotax = new THREE.Vector3(tz-oz,0,ox-tx);
// Constructing the rotation which I would hope takes the y-axis to the direction of the direction vector. (But it does not.) I will actually perform the rotation in an animated loop, just to help me see what's going on.
const angle = Math.acos( (ty-oy)/mag );
const quat = new THREE.Quaternion();
// Set the position (center of the cylinder) to the midpoint of the original and terminal points.
cyl.position.set( ...(ovec.addScaledVector(dvec,.5)) );
// Make two spheres at the end points, just to help see where the cylinder should be.
const sg = new THREE.SphereGeometry(.1);
const s1 = new THREE.Mesh( sg, mat );
const s2 = new THREE.Mesh( sg, mat );
s2.scale.set( .5,.5,.5 );
s1.position.set( tx,ty,tz );
s2.position.set( ox,oy,oz );
scene.add( cyl,s1,s2 );
// Parameters for a rotating camera to help view the results.
var theta = 0;
const phi = 1;
const radius = 6;
var x = radius*Math.cos(theta)*Math.cos(phi);
var y = radius*Math.sin(theta)*Math.cos(phi);
var z = radius*Math.sin(phi);
camera.position.set( x,y,z );
var anglescale; // A parameter to allow the angle of the quaternion to increase as the animation proceeds.
function render( time ) {
time *= 0.001; // convert time to seconds
renderer.render( scene, camera );
requestAnimationFrame( render );
theta += .001;
x = radius*Math.cos(theta)*Math.cos(phi);
y = radius*Math.sin(theta)*Math.cos(phi);
z = radius*Math.sin(phi);
camera.position.set( x,y,z );
camera.lookAt( 0,0,0 );
anglescale = Math.min(time/10,1);
quat.setFromAxisAngle(rotax, angle*anglescale);
cyl.setRotationFromQuaternion(quat);
}
requestAnimationFrame( render );
}
main();
When I run this I find that the cylinder does not run between the two spheres, as it should. Clearly something is wrong with the angle of rotation. I calculated this as Math.acos( (ty-oy)/mag )
, based on taking the dot-product with the direction vector and <0,1,0> and dividing by the magnitude(s). In case the issue is just a matter of orientation, I also tried using the negative of this angle, but that also didn’t work.)
Noteworthy is the fact that as the cylinder rotates it passes through the two spheres at its endpoints, so this tells me that the axis of rotation is correct.
Also, I only learned about quaternions maybe a day or two ago, so I definitely don’t get them in any kind of deep way. So there is a very good chance that I just don’t understand how to use them.