THREE.js Thick Arrow con capacidad lookAt ()

Dec 03 2020

Quería hacer una malla de "Flecha gruesa", es decir, una flecha como el Arrow Helper estándar pero con el eje hecho de a en cylinderlugar de a line.


tldr; no copie el diseño de Arrow Helper; consulte la sección Epílogo al final de la pregunta.


Así que copié y modifiqué el código para mis necesidades (prescindí del constructor y los métodos) e hice los cambios y ahora funciona bien: -

// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =  
//= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

//... 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
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =  
//= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

Esto funciona bien para una flecha de dirección fija donde la dirección de la flecha se puede proporcionar en el momento de la construcción.

Pero ahora necesito cambiar la orientación de la flecha con el tiempo (para rastrear un objetivo en movimiento). Actualmente, la función Object3D.lookAt () no es suficiente porque la flecha apunta a lo largo de su eje y de Object3D, mientras que lookAt () orienta el eje z de Object3D para mirar la posición de destino dada.

Con la experimentación he llegado a mitad de camino usando: -

geometry.applyMatrix( new THREE.Matrix4().makeRotationX( Math.PI / 2 ) );

en las geometrías del eje y la cabeza (las 2 líneas están comentadas en el extracto de código anterior). Esto parece hacer que las mallas del cilindro apunten en la dirección correcta. Pero el problema es que las mallas están mal formadas y la malla de la cabeza se desplaza lejos de la malla del eje.

Con prueba y error, podría ajustar el código para que la flecha funcione para mi ejemplo actual. Pero (dada mi débil comprensión de los cuaterniones) no estoy seguro de que (a) sea lo suficientemente general para aplicarse en todas las situaciones o (b) sea lo suficientemente a prueba de futuro contra la evolución de THREE.js.

Por eso, agradecería cualquier solución / recomendación sobre cómo lograr la capacidad lookAt () para esta "Flecha gruesa".

Epílogo

Mi principal conclusión es NO seguir el diseño de Helper Arrow.

Como indican las respuestas de TheJim01 y algo aquí, hay un enfoque más fácil usando la función de "anidación" de Object3D.add ().

Por ejemplo:-

(1) cree dos mallas de cilindros (para flecha y punta de flecha) que de manera predeterminada apuntarán en la dirección Y; haga que la longitud de la geometría sea = 1.0 para ayudar en el futuro cambio de escala.

(2) Agregue las mallas a un objeto Object3D principal.

(3) Gire el padre +90 grados alrededor del eje X usando parent.rotateX(Math.PI/2).

(4) Agregue el padre a un objeto de abuelo.

(5) Uso posterior grandparent.lookAt(target_point_as_World_position_Vec3_or_x_y_z).

NB lookAt () no funcionará correctamente si los padres o abuelos tienen una escala diferente a (n,n,n).

Los tipos de objeto padre y abuelo pueden ser simples THREE.Object3D, o THREE.Group, o THREE.Mesh(invisibles si es necesario, por ejemplo, al establecer pequeñas dimensiones o .visibility=false)

Arrow Helper se puede usar dinámicamente, pero solo si la dirección interna se establece en (0,0,1) antes de usar lookAt ().

Respuestas

2 TheJim01 Dec 03 2020 at 22:07

Puede postularse lookAta cualquiera Object3D.Object3D.lookAt( ... )

Ya ha descubierto que lookAthace que las formas apunten en la +Zdirección y lo está compensando. Pero se puede dar un paso más con la introducción de un Group. Groups también se derivan de Object3D, por lo que también admiten el lookAtmétodo.

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>

La clave aquí es que el cono / eje se hace apuntar en la +Zdirección y luego se agrega al Group. Esto significa que sus orientaciones ahora son locales para el grupo . Cuando el grupo lookAtcambia, las formas siguen su ejemplo. Y debido a que las formas de la "flecha" apuntan en la +Zdirección local del grupo , eso significa que también apuntan a cualquier posición que se le haya asignado group.lookAt(...);.

Más trabajo

Esto es sólo un punto de partida. Deberá adaptar esto a cómo desea que funcione construyendo la flecha en la posición correcta, con la longitud correcta, etc. Aún así, el patrón de agrupación debería lookAtfacilitar el trabajo.

1 somethinghere Dec 04 2020 at 00:37

Todo lo que necesita es una mayor comprensión de la anidación, que le permite colocar objetos en relación con sus padres. Como se mencionó en la respuesta anterior, puede usar Groupo Object3D, pero no es necesario. Simplemente puede colocar la punta de la flecha en su cilindro y apuntar su cilindro en la dirección z, luego usar los métodos incorporados, no complicar demasiado las cosas lookAt.

Trate de no usar matrices o cuaterniones para cosas simples como esta, ya que hace que sea mucho más difícil resolver las cosas. Dado que THREE.js permite marcos anidados, ¡haz uso de eso!

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>

Puede girar usando el mouse (si tiene desplazamiento horizontal, debería estar en los trackpads) y hacer clic para apuntar la flecha. También he añadido algunos puntos de seguimiento para que pueda ver que `lookAt' hace el trabajo sin complicar demasiado, y que está está señalando el punto de que ha hecho clic en la esfera de envoltura.

Y con eso, definitivamente escribí la palabra con shaftdemasiada frecuencia. Empieza a sonar extraño.