3

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".

Epilogue

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().

steveOw
  • 879
  • 12
  • 41
  • Hold on is your main issue that you made an arrow that points up and lookAt points in the z-direction? In that case you are putting _exhaustive_ effort into solving a problem that only requires a recalibration of your mindset. As long as you design an arrow that points in that direction, lookAt will point the arrow correctly. On top of that you could use an `ArrowHelper` to see _how_ this could be constructed and works. That helper also uses `lookAt` without any issues. – somethinghere Dec 03 '20 at 17:05
  • @somethinghere - see Epilogue I added to my question. Thanks for your input! – steveOw Dec 04 '20 at 12:48

2 Answers2

2

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. Groups 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(...);.

Further work

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.

TheJim01
  • 8,411
  • 1
  • 30
  • 54
  • I mean, isn't the OP already doing this by grouping everything using an Object3D itself instead of a Group? There's no real difference between a Group and an Object3D, so I don't see _that_ changing anything? Otherwise, indeed `lookAt` is the solution here. I don't understand how the OP can start using THREE.js without coming across it - it's in every tutorial. – somethinghere Dec 03 '20 at 15:50
  • Thanks very much. Ive been playing with adding the arrow components to a `helper mesh` which I then will make invisible but using a `group` is more streamlined. Then I guess I have to swap relevant `Y operations` and `Z operations`. But Ive no idea what the quaternion settings should be in `thisArrow.quaternion.set()`, maybe I can leave them unchanged? – steveOw Dec 03 '20 at 15:51
  • @somethinghere I refer to the problems with `lookAt`in my question. The problem is that the Arrow helper does not "work intuitively" with `lookAt` - i.e. the Arrow does not point at the lookAt target. – steveOw Dec 03 '20 at 15:56
  • Cancel my previous comment, I get it now, no need for coordinate swapping or quaternions, just use Object3D.add() and Object3D.rotateX as described in an Epilogue to my question. Thanks for your answer. – steveOw Dec 04 '20 at 12:46
1

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.

somethinghere
  • 16,311
  • 2
  • 28
  • 42
  • Thanks for the impressive snippet. Ive used "nesting" and lookAt in the past but thought it might be a good idea to follow the "official" design patterns hence the idea to adapt the Arrow Helper tool design. I'm very happy to avoid quaternions but I wonder why the official Arrow Helper setDirection function includes them? – steveOw Dec 03 '20 at 21:38
  • @steveOw Because setDirection takes a normalized vector to point at, while `lookAt` takes a global position to look at. So you can `lookAt([10,10,10])` but you cannot `setDirection([10,10,10])` (well you can but I promises it will start getting confusing quickly because its not a normalised vector). If you want to implement setDirection yourself you could do `this.quaternion.setFromUnitVectors( [0,0,1], [othervector])` and be done with it. You could even implement this from your Y-direction. – somethinghere Dec 03 '20 at 21:44
  • By the way, the arrow helper _also_ can be used with `lookAt`, setDirection is really only useful if you want to absolutely be sure the arrow is pointing in a certain direction, unrelated to it's current position etc... – somethinghere Dec 03 '20 at 21:50
  • As I understand it now: an arrow helper can be used with `lookAt` only if it is first set set to point to [0,0,1]. I dont reckon that such pre-configuration would be intuitive for casual users of THREE.js, nor is it [officially documented](https://threejs.org/docs/index.html#api/en/helpers/ArrowHelper) . – steveOw Dec 03 '20 at 22:38
  • Not true, since the arrow will have that as a default. It is also documented that threejs uses 0,0,1 as its baseline for calculations, thats the preferred direction. See here: https://stackoverflow.com/a/14168533/2991619 - its somewhere in the docs as well but couldnt find it yet. – somethinghere Dec 03 '20 at 22:54
  • You are right about the default (but you have to look at the source code to find that out!). Often having to use source code and stackoverflow as documentation is part of what makes THREE.js so often awkward to use, imo. – steveOw Dec 03 '20 at 23:08
  • @steveOw I think this has more to do with how 3D coordinate systems generally work. I think it has to with right handedness (https://en.wikipedia.org/wiki/Right-hand_rule) I'm also pretty convinced I read this in their docs once upon a time, but I can't find it for the life of me. Maybe that should be something in the FAQ, I agree. – somethinghere Dec 04 '20 at 00:15