Reputation: 1021
I wanted to make a "Thick Arrow" mesh i.e. an arrow like the standard Arrow Helper but with the shaft made out of a cylinder
instead of a line
.
tldr; do not copy the Arrow Helper design; see the Epilogue section at end of the question.
So I copied and modified the code for my needs (dispensed with constructor and methods) and made the changes and now it works OK:-
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
//= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
//... START of ARROWMAKER SET of FUNCTIONS
// adapted from https://github.com/mrdoob/three.js/blob/master/src/helpers/ArrowHelper.js
//====================================
function F_Arrow_Fat_noDoesLookAt_Make ( dir, origin, length, shaftBaseWidth, shaftTopWidth, color, headLength, headBaseWidth, headTopWidth )
{
//... dir is assumed to be normalized
var thisArrow = new THREE.Object3D();////SW
if ( dir === undefined ) dir = new THREE.Vector3( 0, 0, 1 );
if ( origin === undefined ) origin = new THREE.Vector3( 0, 0, 0 );
if ( length === undefined ) length = 1;
if ( shaftBaseWidth === undefined ) shaftBaseWidth = 0.02 * length;
if ( shaftTopWidth === undefined ) shaftTopWidth = 0.02 * length;
if ( color === undefined ) color = 0xffff00;
if ( headLength === undefined ) headLength = 0.2 * length;
if ( headBaseWidth === undefined ) headBaseWidth = 0.4 * headLength;
if ( headTopWidth === undefined ) headTopWidth = 0.2 * headLength;//... 0.0 for a point.
/* CylinderBufferGeometry parameters from:-
// https://threejs.org/docs/index.html#api/en/geometries/CylinderBufferGeometry
* radiusTop — Radius of the cylinder at the top. Default is 1.
* radiusBottom — Radius of the cylinder at the bottom. Default is 1.
* height — Height of the cylinder. Default is 1.
* radialSegments — Number of segmented faces around the circumference of the cylinder. Default is 8
* heightSegments — Number of rows of faces along the height of the cylinder. Default is 1.
* openEnded — A Boolean indicating whether the ends of the cylinder are open or capped. Default is false, meaning capped.
* thetaStart — Start angle for first segment, default = 0 (three o'clock position).
* thetaLength — The central angle, often called theta, of the circular sector. The default is 2*Pi, which makes for a complete cylinder.
*/
//var shaftGeometry = new THREE.CylinderBufferGeometry( 0.0, 0.5, 1, 8, 1 );//for strongly tapering, pointed shaft
var shaftGeometry = new THREE.CylinderBufferGeometry( 0.1, 0.1, 1, 8, 1 );//shaft is cylindrical
//shaftGeometry.translate( 0, - 0.5, 0 );
shaftGeometry.translate( 0, + 0.5, 0 );
//... for partial doesLookAt capability
//shaftGeometry.applyMatrix( new THREE.Matrix4().makeRotationX( Math.PI / 2 ) );
var headGeometry = new THREE.CylinderBufferGeometry( 0, 0.5, 1, 5, 1 ); //for strongly tapering, pointed head
headGeometry.translate( 0, - 0.5, 0 );
//... for partial doesLookAt capability
//headGeometry.applyMatrix( new THREE.Matrix4().makeRotationX( Math.PI / 2 ) );
thisArrow.position.copy( origin );
/*thisArrow.line = new Line( _lineGeometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
thisArrow.line.matrixAutoUpdate = false;
thisArrow.add( thisArrow.line ); */
thisArrow.shaft = new THREE.Mesh( shaftGeometry, new THREE.MeshLambertMaterial( { color: color } ) );
thisArrow.shaft.matrixAutoUpdate = false;
thisArrow.add( thisArrow.shaft );
thisArrow.head = new THREE.Mesh( headGeometry, new THREE.MeshLambertMaterial( { color: color } ) );
thisArrow.head.matrixAutoUpdate = false;
thisArrow.add( thisArrow.head );
//thisArrow.setDirection( dir );
//thisArrow.setLength( length, headLength, headTopWidth );
var arkle = new THREE.AxesHelper (2 * length);
thisArrow.add (arkle);
F_Arrow_Fat_noDoesLookAt_setDirection( thisArrow, dir ) ;////SW
F_Arrow_Fat_noDoesLookAt_setLength ( thisArrow, length, headLength, headBaseWidth ) ;////SW
F_Arrow_Fat_noDoesLookAt_setColor ( thisArrow, color ) ;////SW
scene.add ( thisArrow );
//... this screws up for the F_Arrow_Fat_noDoesLookAt kind of Arrow
//thisArrow.lookAt(0,0,0);//...makes the arrow's blue Z axis lookAt Point(x,y,z).
}
//... EOFn F_Arrow_Fat_noDoesLookAt_Make().
//=============================================
function F_Arrow_Fat_noDoesLookAt_setDirection( thisArrow, dir )
{
// dir is assumed to be normalized
if ( dir.y > 0.99999 )
{
thisArrow.quaternion.set( 0, 0, 0, 1 );
} else if ( dir.y < - 0.99999 )
{
thisArrow.quaternion.set( 1, 0, 0, 0 );
} else
{
const _axis = /*@__PURE__*/ new THREE.Vector3();
_axis.set( dir.z, 0, - dir.x ).normalize();
const radians = Math.acos( dir.y );
thisArrow.quaternion.setFromAxisAngle( _axis, radians );
}
}
//... EOFn F_Arrow_Fat_noDoesLookAt_setDirection().
//=========================================
function F_Arrow_Fat_noDoesLookAt_setLength( thisArrow, length, headLength, headBaseWidth )
{
if ( headLength === undefined ) headLength = 0.2 * length;
if ( headBaseWidth === undefined ) headBaseWidth = 0.2 * headLength;
thisArrow.shaft.scale.set( 1, Math.max( 0.0001, length - headLength ), 1 ); // see #17458
//x&z the same, y as per length-headLength
//thisArrow.shaft.position.y = length;//SW ???????
thisArrow.shaft.updateMatrix();
thisArrow.head.scale.set( headBaseWidth, headLength, headBaseWidth ); //x&z the same, y as per length
thisArrow.head.position.y = length;
thisArrow.head.updateMatrix();
}
//...EOFn F_Arrow_Fat_noDoesLookAt_setLength().
//========================================
function F_Arrow_Fat_noDoesLookAt_setColor( thisArrow, color )
{
thisArrow.shaft.material.color.set( color );
thisArrow.head.material.color.set( color );
}
//...EOFn F_Arrow_Fat_noDoesLookAt_setColor().
//... END of ARROWMAKER SET of FUNCTIONS
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
//= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
This works OK for a fixed-direction arrow where the arrow direction can be supplied at time of construction.
But now I need to change the arrow orientation over time (for tracking a moving target). Currently the Object3D.lookAt() function is not sufficient because the arrow points along its Object3D y-axis, whereas lookAt() orients the Object3D z-axis to look at the given target position.
With experimentation I have gotten part-way there by using:-
geometry.applyMatrix( new THREE.Matrix4().makeRotationX( Math.PI / 2 ) );
on the shaft and head geometries (the 2 lines are commented out in the above code extract). This seems to get the cylinder meshes pointing in the correct direction. But the problem is that the meshes are mis-shaped and the head mesh is displaced away from the shaft mesh.
With trial and error I might be able to adjust the code to get the arrow to work for my present example. But (given my weak understanding of quaternions) I am not confident that it would (a) be general enough to apply in all situations or (b) be sufficiently future-proof against evolution of THREE.js.
So I would be grateful for any solutions/recommendations on how to achieve the lookAt() capability for this "Thick Arrow".
My main takeaway is NOT to follow the design of the Helper Arrow.
As TheJim01's and somethinghere's answers indicate, there is an easier approach using the Object3D.add() "nesting" function.
For example:-
(1) create two cylinder meshes (for arrowshaft and arrowhead) which by default will point in the Y-direction; make geometry length =1.0 to assist future re-scaling.
(2) Add the meshes to a parent Object3D object.
(3) Rotate the parent +90 degrees around the X-axis using parent.rotateX(Math.PI/2)
.
(4) Add the parent to a grandparent object.
(5) Subsequently use grandparent.lookAt(target_point_as_World_position_Vec3_or_x_y_z)
.
N.B. lookAt() will not work properly if parent or grandparent have scaling other than (n,n,n)
.
The parent and grandparent object types may be plain THREE.Object3D
, or THREE.Group
, or THREE.Mesh
(made invisible if required e.g. by setting small dimensions or .visibility=false
)
Arrow Helper can be used dynamically but only if the internal direction is set to (0,0,1) before using lookAt().
Upvotes: 3
Views: 2379
Reputation: 17350
All you require is some more understanding of nesting, which allows you to place objects relative to their parents. As mentioned in the answer above, you could use Group
or Object3D
, but you don't have to. You can just nest your arrowhead on your cylinder and point your cylinder into the z-direction, then use the built-in, dont-overcomplicate-things methods lookAt
.
Try not to use matrices or quaternions for simple things like this, as it makes for a way harder time figuring things out. Since THREE.js allows for nested frames, make use of that!
const renderer = new THREE.WebGLRenderer;
const camera = new THREE.PerspectiveCamera;
const scene = new THREE.Scene;
const mouse = new THREE.Vector2;
const raycaster = new THREE.Raycaster;
const quaternion = new THREE.Quaternion;
const sphere = new THREE.Mesh(
new THREE.SphereGeometry( 10, 10, 10 ),
new THREE.MeshBasicMaterial({ transparent: true, opacity: .1 })
);
const arrow = new THREE.Group;
const arrowShaft = new THREE.Mesh(
// We want to ensure our arrow is completely offset into one direction
// So the translation ensure every bit of it is in Y+
new THREE.CylinderGeometry( .1, .3, 3 ).translate( 0, 1.5, 0 ),
new THREE.MeshBasicMaterial({ color: 'blue' })
);
const arrowPoint = new THREE.Mesh(
// Same thing, translate to all vertices or +Y
new THREE.ConeGeometry( 1, 2, 10 ).translate( 0, 1, 0 ),
new THREE.MeshBasicMaterial({ color: 'red' })
);
const trackerPoint = new THREE.Mesh(
new THREE.SphereGeometry( .2 ),
new THREE.MeshBasicMaterial({ color: 'green' })
);
const clickerPoint = new THREE.Mesh(
trackerPoint.geometry,
new THREE.MeshBasicMaterial({ color: 'yellow' })
);
camera.position.set( 10, 10, 10 );
camera.lookAt( scene.position );
// Place the point at the top of the shaft
arrowPoint.position.y = 3;
// Point the shaft into the z-direction
arrowShaft.rotation.x = Math.PI / 2;
// Attach the point to the shaft
arrowShaft.add( arrowPoint );
// Add the shaft to the global arrow group
arrow.add( arrowShaft );
// Add the arrow to the scene
scene.add( arrow );
scene.add( sphere );
scene.add( trackerPoint );
scene.add( clickerPoint );
renderer.domElement.addEventListener( 'mousemove', mouseMove );
renderer.domElement.addEventListener( 'click', mouseClick );
renderer.domElement.addEventListener( 'wheel', mouseWheel );
render();
document.body.appendChild( renderer.domElement );
function render(){
renderer.setSize( innerWidth, innerHeight );
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.render( scene, camera );
}
function mouseMove( event ){
mouse.set(
event.clientX / event.target.clientWidth * 2 - 1,
-event.clientY / event.target.clientHeight * 2 + 1
);
raycaster.setFromCamera( mouse, camera );
const hit = raycaster.intersectObject( sphere ).shift();
if( hit ){
trackerPoint.position.copy( hit.point );
render();
}
document.body.classList.toggle( 'tracking', !!hit );
}
function mouseClick( event ){
clickerPoint.position.copy( trackerPoint.position );
arrow.lookAt( trackerPoint.position );
render();
}
function mouseWheel( event ){
const angle = Math.PI * event.wheelDeltaX / innerWidth;
camera.position.applyQuaternion(
quaternion.setFromAxisAngle( scene.up, angle )
);
camera.lookAt( scene.position );
render();
}
body { padding: 0; margin: 0; }
body.tracking { cursor: none; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r123/three.min.js"></script>
You can wheel around using your mouse (if it has horizontal scroll, should be on trackpads) and click to point the arrow. I also added some tracking points so you can see that `lookAt' does work without overcomplicating it, and that is is pointing at the point you clicked on the wrapping sphere.
And with that, I definitely typed the word shaft
too often. It's starting to sound weird.
Upvotes: 1
Reputation: 8886
You can apply lookAt
to any Object3D
. Object3D.lookAt( ... )
You have already discovered that lookAt
causes the shapes to point in the +Z
direction, and are compensating for that. But it can be taken a step further with the introduction of a Group
. Group
s are also derived from Object3D
, so they also support the lookAt
method.
let W = window.innerWidth;
let H = window.innerHeight;
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000);
camera.position.set(10, 10, 50);
camera.lookAt(scene.position);
scene.add(camera);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 0, -1);
camera.add(light);
const group = new THREE.Group();
scene.add(group);
const arrowMat = new THREE.MeshLambertMaterial({color:"green"});
const arrowGeo = new THREE.ConeBufferGeometry(2, 5, 32);
const arrowMesh = new THREE.Mesh(arrowGeo, arrowMat);
arrowMesh.rotation.x = Math.PI / 2;
arrowMesh.position.z = 2.5;
group.add(arrowMesh);
const cylinderGeo = new THREE.CylinderBufferGeometry(1, 1, 5, 32);
const cylinderMesh = new THREE.Mesh(cylinderGeo, arrowMat);
cylinderMesh.rotation.x = Math.PI / 2;
cylinderMesh.position.z = -2.5;
group.add(cylinderMesh);
function render() {
renderer.render(scene, camera);
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
renderer.setSize(W, H);
camera.aspect = W / H;
camera.updateProjectionMatrix();
render();
}
window.addEventListener("resize", resize);
resize();
let rad = 0;
function animate() {
rad += 0.05;
group.lookAt(Math.sin(rad) * 100, Math.cos(rad) * 100, 100);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
background: skyblue;
}
<script src="https://threejs.org/build/three.min.js"></script>
The key here is that the cone/shaft are made to point in the +Z
direction, and then added to the Group
. This means their orientations are now local to the group. When the group's lookAt
changes, the shapes follow suit. And because the "arrow" shapes point in the group's local +Z
direction, that means they also point at whatever position was given to group.lookAt(...);
.
This is just a starting point. You'll need to adapt this to how you want it to work with constructing the arrow at the correct position, with the correct length, etc. Still, the grouping pattern should make lookAt
easier to work with.
Upvotes: 2