THREE.js Arrow with Enhanced Thickness and Dynamic lookAt() Functionality

I had the idea to create a "Thick Arrow" mesh, essentially an arrow similar to the standard Arrow Helper but with the shaft constructed using a cylinder instead of a line.


tldr; do not just copy the Arrow Helper design; refer to the Epilogue section at the end of the question.


So, I decided to replicate and modify the code to suit my requirements (omitting the constructor and methods) and after making the necessary adjustments, it now functions correctly:-

// Modified 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 ) 
{
    var thisArrow = new THREE.Object3D();

    // Parameters set if not defined
    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;

    /* CylinderBufferGeometry parameters:
        * radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength
    */
  
    var shaftGeometry  = new THREE.CylinderBufferGeometry( 0.1, 0.1, 1, 8, 1 ); // cylindrical shaft
    var headGeometry = new THREE.CylinderBufferGeometry( 0, 0.5, 1, 5, 1 );

    // Set positions
    shaftGeometry.translate( 0, +0.5, 0 );
    headGeometry.translate( 0, -0.5, 0 );

    // Create components
    thisArrow.shaft = new THREE.Mesh( shaftGeometry, new THREE.MeshLambertMaterial({ color: color }) );
    thisArrow.head = new THREE.Mesh( headGeometry, new THREE.MeshLambertMaterial({ color: color }) );

    thisArrow.position.copy(origin);
    thisArrow.add(thisArrow.shaft);
    thisArrow.add(thisArrow.head);

    // Additional functionality
    scene.add(thisArrow);
}

// Other supporting functions for direction, length, and color settings

This approach works well for a fixed-direction arrow where the direction is specified during construction.

However, what I now need is the ability to change the orientation of the arrow over time (to track a moving target). The current method using Object3D.lookAt() does not support this functionality as it aligns the y-axis of the object with the target position, whereas the Thick Arrow requires alignment along the z-axis.

Experimenting with rotations yielded partial success, but the shapes are distorted and the head mesh appears offset from the shaft. While trial and error may yield results in specific cases, I seek a more robust solution that is adaptable to different scenarios and future updates in THREE.js.

I welcome any suggestions or recommendations on achieving the desired lookAt() capability for the "Thick Arrow".

Epilogue

The key lesson learned here is not to simply follow the design of the Helper Arrow.

As highlighted in responses from TheJim01 and somethinghere, a simpler approach involves nesting cylinder meshes (for shaft and head), rotating them appropriately within parent objects, and then utilizing lookAt() on a grandparent object to achieve the desired orientation.

Using Object3D.add() for nesting, along with proper rotations, provides a more flexible and scalable solution compared to replicating the Arrow Helper design. Additionally, dynamic usage of Arrow Helper can be achieved by setting the internal direction before calling lookAt().

Answer №1

Utilizing the lookAt method on any Object3D is possible. Check out the documentation for more details: Object3D.lookAt( ... )

If you've noticed that using lookAt causes shapes to face the +Z direction and have adjusted accordingly, consider leveraging a Group. Since Groups are also based on Object3D, they support the lookAt method as well.

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 insight here is that by aligning the cone/shaft in the +Z direction and then grouping them, their orientations become local to the group. As the group's orientation changes with lookAt, the shapes adjust accordingly. This setup allows the "arrow" shapes to automatically point at the position specified in group.lookAt(...);.

Next Steps

This serves as a foundation. You'll need to customize it to fit your requirements, ensuring the arrow is placed correctly with the right length, etc. Nonetheless, utilizing the grouping approach should simplify working with lookAt.

Answer №2

To master nesting in web development, you need to understand how to position objects in relation to their parents. While it's possible to utilize Group or Object3D, it's not a requirement. Simply nest your arrowhead on the cylinder and orient the cylinder in the z-direction, then leverage the straightforward lookAt method.

Avoid complicating things by using matrices or quaternions for basic tasks like this. THREE.js offers nested frames that you can take advantage of!

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(
    // Ensuring arrow is fully offset in Y+
    new THREE.CylinderGeometry( .1, .3, 3 ).translate( 0, 1.5, 0 ),
    new THREE.MeshBasicMaterial({ color: 'blue' })
);
const arrowPoint = new THREE.Mesh(
    // Translate vertices to +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 );

// Position point at top of shaft
arrowPoint.position.y = 3;
// Orient shaft in z-direction
arrowShaft.rotation.x = Math.PI / 2;

// Attach point to shaft
arrowShaft.add( arrowPoint );
// Add shaft to arrow group
arrow.add( arrowShaft );
// Add arrow to 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 use your mouse scroll (if it supports horizontal scrolling, common on trackpads) to rotate and click to adjust the arrow orientation. Additionally, tracking points are included to demonstrate the effectiveness of `lookAt', ensuring proper alignment with the clicked point on the spherical surface.

On a side note, I may have mentioned the word shaft too frequently. It's starting to sound peculiar.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I am facing difficulties accessing an element within a controller in Angular

Struggling to access an element inside an AngularJS controller, I attempted the following: var imageInput = document.getElementById("myImage"); Unfortunately, this approach was unsuccessful as the element returned null. Curiously, when placing the statem ...

Clicking a button in React requires two clicks to update a boolean state by triggering the onClick event

I have a React functional component with input fields, a button, and a tooltip. The tooltip is initially disabled and should only be enabled and visible when the button is clicked and the input fields contain invalid values. The issue I'm facing is t ...

javascript string assignment

Is it possible to conditionally assign a string based on the result of a certain condition, but for some reason, it's not working? var message = ""; if (true) { message += "true"; } else { message += "false" } console.log(message); ...

Find and Scroll Dropdown Menu with Bootstrap Framework

Access the jsfiddle for more information. Have a look at these questions: In my dropdown menu, I have included a search box and multiple sub-menus. However, the search box only filters the first dropdown menu and does not work with the sub-menus. How ca ...

The bundle.js file encountered an issue while running UglifyJs, expecting a name

I have been attempting to utilize UglifyJS to compress/minimize my bundle.js file. However, when executing webpack -p, I encountered the following error message: ERROR in bundle.js from UglifyJs Name expected [bundle.js:105519,6] The line causing the iss ...

Dispatch is functioning properly, however the state remains unchanged

I'm currently developing an application that utilizes redux for state management. In a particular scenario, I need to update the state. Here is how my initial state and reducer function are set up: import { createSlice } from '@reduxjs/toolkit&a ...

What is the best way to create a simulation of a NOR gate using JavaScript?

Can a NOR gate be emulated in JavaScript? It seems that the language only supports AND and OR gates at this time. ...

What is the best way to target a specific item in a list using JavaScript in order to modify its appearance?

How can I modify the appearance of a specific li element when it is clicked? ...

Executing a backend C# function from a front end Javascript function in ASP.NET: A Step-by-Step Guide

As part of my current unpaid internship in C# and Asp.net, my employer has assigned me the task of implementing a JavaScript function to prompt the user for confirmation before deleting a record from the database. After conducting thorough research, I suc ...

Can anyone explain how to successfully implement AJAX data in a PHP file?

I've been struggling to transfer data from my JavaScript file to a PHP variable. I've spent hours searching for solutions, but none seem to apply to my unique code. Here's the JavaScript code I'm working with... $(function(){ ...

Learn how to handle JSON requests in Node.js within a router script

Hello everyone, I've been struggling to retrieve the body of a request from Postman into my scripts. The body always comes up empty. Can anyone help me figure out the correct way to do this? I have set up the server in index.js and reading the reques ...

Guide on running javascript / jquery code once php page has been fully loaded

I am utilizing a jQuery command to call a PHP page, which will execute several SQL queries and then update a cookie value. I need to retrieve this updated value, but only after the PHP page has completed its task. The current code snippet I am using to cal ...

Developing a MySQL Community Server-backed RESTful Web Service with the power of jQuery AJAX

I am currently in the process of developing a RESTful Web Service using MySQL Community Server alongside jQuery AJAX Unfortunately, my usage of jQuery AJAX is not functioning as expected. When attempting to add, delete, update a product, or retrieve all p ...

Can Node.js endpoints effectively handle the garbage collection of new class instances?

Just diving into node.js I'm currently dealing with a lengthy and messy function that constructs a CYPHER query for Neo4j. I am considering transforming it into a class, complete with methods, along with a corresponding mocha spec. The expected usag ...

Utilizing AngularJS to show content based on regular expressions using ng-show

With two images available, I need to display one image at a time based on an input regex pattern. Here is the code snippet: <input type="password" ng-model="password" placeholder="Enter Password"/> <img src="../close.png" ng-show="password != [ ...

Scaling Three.js Mesh to fit renderer while maintaining aspect ratio

Currently, I am using the STLLoader plugin from the Github repository of three.js. This is a snippet of my code: this.loadSTL = function(scene, stlFile){ var loader = new THREE.STLLoader(); loader.addEventListener('load', function(g ...

Convert XML data into a structured table format

We have an XML file named "servers.xml" that needs to be parsed. This file is located on the same server where we want it to be parsed, in the same folder. <root> <list> <server> <server name="28 Disconnects La ...

The connection between the `http.request()` method, the `ClientRequest` variable, and the `callback

I am new to Node.js and currently exploring its API. After referencing some resources, I was able to write a crawler that successfully posts comments on a website. However, I am unsure about the execution order involving the return class - clientRequest an ...

Is it possible to incorporate the arrow function within the debounce function?

export const debounce = (callback: Function, ms = 300) => { let timeoutId: ReturnType<typeof setTimeout> return function (...args: any[]) { clearTimeout(timeoutId) timeoutId = setTimeout(() => callback.apply(this, args), ms) ...

Is it true that URL parameters can be found in two distinct locations within this.props in react-router?

<Route path="lookbook" component={Photos} onEnter={onPageEnter}> <Route path=":photoIdentifier" component={PhotoDetailsModal} onEnter={onPageEnter}> </Route> </Route> While in the PhotoDetailsModal, I used console.log to check ...