THREE.js Thick Arrow com capacidade lookAt ()

Dec 03 2020

Eu queria fazer uma malha de "flecha espessa", ou seja, uma flecha como o auxiliar de flecha padrão, mas com a haste feita de a em cylindervez de a line.


tldr; não copie o design do Arrow Helper; veja a seção Epílogo no final da pergunta.


Então, copiei e modifiquei o código de acordo com minhas necessidades (dispensado construtor e métodos) e fiz as alterações e agora está funcionando bem: -

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

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

Isso funciona bem para uma seta de direção fixa, onde a direção da seta pode ser fornecida no momento da construção.

Mas agora preciso mudar a orientação da seta ao longo do tempo (para rastrear um alvo em movimento). Atualmente, a função Object3D.lookAt () não é suficiente porque a seta aponta ao longo de seu eixo y Object3D, enquanto lookAt () orienta o eixo z Object3D para olhar para a posição de destino fornecida.

Com a experimentação, cheguei lá em parte usando: -

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

nas geometrias do eixo e da cabeça (as 2 linhas são comentadas no extrato do código acima). Isso parece fazer com que as malhas do cilindro apontem na direção correta. Mas o problema é que as malhas estão malformadas e a malha da cabeça está deslocada da malha do eixo.

Com tentativa e erro, posso ajustar o código para que a seta funcione no meu exemplo atual. Mas (dado meu fraco entendimento de quatérnios) não estou confiante de que seria (a) geral o suficiente para ser aplicado em todas as situações ou (b) ser suficientemente à prova de futuro contra a evolução de THREE.js.

Portanto, eu ficaria grato por quaisquer soluções / recomendações sobre como obter o recurso lookAt () para esta "seta espessa".

Epílogo

Minha principal lição é NÃO seguir o design da Helper Arrow.

Como as respostas do TheJim01 e de algo aqui indicam, há uma abordagem mais fácil usando a função de "aninhamento" Object3D.add ().

Por exemplo:-

(1) criar duas malhas de cilindro (para eixo de seta e ponta de seta) que, por padrão, apontarão na direção Y; faça geometria comprimento = 1,0 para auxiliar no reescalonamento futuro.

(2) Adicione as malhas a um objeto Object3D pai.

(3) Gire o pai +90 graus em torno do eixo X usando parent.rotateX(Math.PI/2).

(4) Adicione o pai a um objeto avô.

(5) Use posteriormente grandparent.lookAt(target_point_as_World_position_Vec3_or_x_y_z).

NB lookAt () não funcionará corretamente se os pais ou avós tiverem uma escala diferente de (n,n,n).

Os tipos de objeto pai e avô podem ser simples THREE.Object3D, ou THREE.Group, ou THREE.Mesh(tornados invisíveis se necessário, por exemplo, definindo pequenas dimensões ou .visibility=false)

O Arrow Helper pode ser usado dinamicamente, mas apenas se a direção interna for definida como (0,0,1) antes de usar lookAt ().

Respostas

2 TheJim01 Dec 03 2020 at 22:07

Você pode se inscrever lookAtem qualquer Object3D.Object3D.lookAt( ... )

Você já descobriu que lookAtfaz com que as formas apontem na +Zdireção e está compensando isso. Mas isso pode ser dado um passo adiante com a introdução de a Group. Groups também são derivados de Object3D, portanto, também suportam o 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>

A chave aqui é que o cone / eixo são feitos para apontar na +Zdireção e, em seguida, adicionados ao Group. Isso significa que suas orientações agora são locais para o grupo . Quando o grupo lookAtmuda, as formas seguem o exemplo. E porque as formas de "seta" apontam na +Zdireção local do grupo , isso significa que elas também apontam para qualquer posição dada group.lookAt(...);.

Trabalho posterior

Isto é só um ponto de partida. Você precisará adaptar isso para como deseja trabalhar com a construção da flecha na posição correta, com o comprimento correto, etc. Ainda assim, o padrão de agrupamento deve ser lookAtmais fácil de trabalhar.

1 somethinghere Dec 04 2020 at 00:37

Tudo que você precisa é um pouco mais de compreensão sobre aninhamento, o que permite colocar objetos em relação aos pais. Conforme mencionado na resposta acima, você poderia usar Groupou Object3D, mas não precisa. Você pode simplesmente aninhar a ponta de sua flecha no cilindro e apontar o cilindro na direção z e, em seguida, usar os métodos internos de não complicar as coisas lookAt.

Tente não usar matrizes ou quatérnios para coisas simples como esta, pois torna muito mais difícil descobrir as coisas. Visto que THREE.js permite frames aninhados, faça uso disso!

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>

Você pode girar usando o mouse (se tiver rolagem horizontal, deve estar em trackpads) e clicar para apontar a seta. Eu também acrescentou alguns pontos de rastreamento para que você possa ver que `lookAt' faz trabalho sem complicar-lo, e que se está apontando para o ponto em que você clicou na esfera de embrulho.

E com isso, eu definitivamente digitei a palavra com shaftmuita frequência. Está começando a soar estranho.