THREE.js Gruba strzałka z funkcją lookAt ()

Dec 03 2020

Chciałem zrobić siatkę „grubej strzały”, tj. Taką jak standardowa strzała pomocnicza, ale z trzonem wykonanym z a cylinderzamiast a line.


tldr; nie kopiuj projektu Arrow Helper; zobacz sekcję Epilog na końcu pytania.


Skopiowałem więc i zmodyfikowałem kod do swoich potrzeb (zrezygnowałem z konstruktora i metod) i wprowadziłem zmiany i teraz działa dobrze: -

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

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

Działa to OK w przypadku strzałki o stałym kierunku, gdzie kierunek strzałki może być dostarczony w czasie budowy.

Ale teraz muszę zmienić orientację strzałek w czasie (do śledzenia ruchomego celu). Obecnie funkcja Object3D.lookAt () nie jest wystarczająca, ponieważ strzałka wskazuje wzdłuż jej osi Y Object3D, podczas gdy lookAt () orientuje oś Z Object3D, aby patrzyła na daną pozycję docelową.

Dzięki eksperymentom dotarłem tam częściowo, używając: -

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

na geometrii wału i głowicy (2 linie są zakomentowane w powyższym fragmencie kodu). Wydaje się, że siatki cylindrów są skierowane we właściwym kierunku. Ale problem polega na tym, że oczka są źle ukształtowane, a siatka głowicy jest odsunięta od siatki wału.

Metoda prób i błędów może być w stanie dostosować kod, aby strzałka działała w moim obecnym przykładzie. Ale (biorąc pod uwagę moje słabe rozumienie kwaternionów) nie jestem pewien, czy (a) byłby wystarczająco ogólny, aby można go było zastosować we wszystkich sytuacjach, lub (b) byłby wystarczająco odporny na przyszłość przeciwko ewolucji TRZY.js.

Byłbym więc wdzięczny za wszelkie rozwiązania / zalecenia, jak osiągnąć funkcję lookAt () dla tej „grubej strzały”.

Epilog

Moim głównym wnioskiem NIE jest podążanie za projektem Strzały Pomocniczej.

Jak wskazują TheJim01 i coś w tym miejscu odpowiedzi, istnieje łatwiejsze podejście przy użyciu funkcji "zagnieżdżania" Object3D.add ().

Na przykład:-

(1) utwórz dwie siatki cylindryczne (dla strzałek i grotów), które domyślnie będą wskazywały kierunek Y; spraw, aby długość geometrii = 1,0, aby ułatwić przyszłe skalowanie.

(2) Dodaj siatki do nadrzędnego obiektu Object3D.

(3) Obróć rodzica o +90 stopni wokół osi X za pomocą parent.rotateX(Math.PI/2).

(4) Dodaj rodzica do obiektu dziadka.

(5) Następnie użyj grandparent.lookAt(target_point_as_World_position_Vec3_or_x_y_z).

Uwaga lookAt () nie będzie działać poprawnie, jeśli rodzic lub dziadek mają skalowanie inne niż (n,n,n).

Rodzic i dziadek mogą być zwykłe THREE.Object3D, lub THREE.Group, lub THREE.Mesh(niewidoczne, jeśli jest to wymagane, np. Przez ustawienie małych wymiarów lub .visibility=false)

Arrow Helper może być używany dynamicznie, ale tylko wtedy, gdy kierunek wewnętrzny jest ustawiony na (0,0,1) przed użyciem lookAt ().

Odpowiedzi

2 TheJim01 Dec 03 2020 at 22:07

Możesz aplikować lookAtdo dowolnego Object3D.Object3D.lookAt( ... )

Odkryłeś już, że lookAtpowoduje to, że kształty wskazują +Zkierunek i kompensujesz to. Ale można pójść o krok dalej, wprowadzając plik Group. Groups również pochodzą z Object3D, więc obsługują również lookAtmetodę.

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>

Kluczowe jest tutaj to, że stożek / wałek są skierowane w +Zkierunku, a następnie dodawane do Group. Oznacza to, że ich orientacje są teraz lokalne dla grupy . Kiedy grupa się lookAtzmienia, kształty podążają za nią. A ponieważ kształty „strzałek” wskazują lokalny +Zkierunek grupy, oznacza to, że wskazują również dowolną pozycję, do której została przypisana group.lookAt(...);.

Dalsza praca

To tylko punkt wyjścia. Będziesz musiał dostosować to do tego, jak chcesz, aby działało z konstruowaniem strzałki we właściwej pozycji, o odpowiedniej długości itp. Mimo to, wzór grupowania powinien lookAtułatwić pracę.

1 somethinghere Dec 04 2020 at 00:37

Wszystko, czego potrzebujesz, to więcej zrozumienia zagnieżdżania, które pozwoli ci umieszczać obiekty względem ich rodziców. Jak wspomniano powyżej, w odpowiedzi, to mógłby użyć Grouplub Object3D, ale nie muszą. Możesz po prostu zagnieździć grot strzałki na cylindrze i skierować go w kierunku z, a następnie użyć wbudowanych metod, które nie powodują nadmiernych komplikacji lookAt.

Staraj się nie używać macierzy ani kwaternionów do prostych rzeczy, takich jak ta, ponieważ znacznie trudniej jest to rozgryźć. Ponieważ THREE.js pozwala na zagnieżdżone ramki, skorzystaj z tego!

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>

Możesz przewijać za pomocą myszy (jeśli ma przewijanie w poziomie, powinna znajdować się na trackpadach) i kliknąć, aby wskazać strzałkę. Dodałem też kilka punktów śledzenia, dzięki czemu można zobaczyć, że `LOOKat” robi pracę bez overcomplicating, i że jest to , wskazując na punkt kliknięcia na sferze owijania.

I dzięki temu zdecydowanie shaftzbyt często wpisałem to słowo . To zaczyna brzmieć dziwnie.