THREE.js具有lookAt()功能的粗箭头

3
我想制作一个“粗箭头”网格,即像标准箭头助手一样的箭头,但是箭杆由cylinder而不是line构成。


tldr; 不要复制箭头助手设计,请参见问题末尾的结论部分。


因此,我复制并修改了代码以满足我的需求(放弃了构造函数和方法),并进行了更改,现在它可以正常工作:

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

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

对于固定方向的箭头,在构建时可以提供箭头方向,这样可以正常工作。

但现在我需要随着时间改变箭头的方向(以跟踪移动目标)。目前,Object3D.lookAt()函数不够用,因为箭头沿着其Object3D y轴指向,而lookAt()将Object3D z轴定向到给定的目标位置。

通过尝试,我已经实现了部分功能:

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

在轴和头部几何形状上(以上代码提取中的两行被注释掉),这似乎可以使圆柱网格指向正确的方向。但问题是网格形状不正确,头部网格与轴网格分离。

通过试错,我可能能够调整代码以使箭头适用于我的当前示例。但是(考虑到我对四元数的理解很弱),我不确定它是否(a)足够通用以适用于所有情况或(b)是否足够未来证明其针对THREE.js的演变具备可靠性。

因此,我将感激任何关于如何实现“粗箭头”的lookAt()功能的解决方案/建议。

结语

我的主要收获不是遵循Helper Arrow的设计。

正如TheJim01和somethinghere的答案所示,使用Object3D.add()“嵌套”函数有一种更简单的方法。

例如:

(1) 创建两个圆柱网格(用于箭杆和箭头),默认情况下将指向Y方向;将几何长度设置为1.0以帮助将来重新缩放。

(2) 将网格添加到父Object3D对象中。

(3) 使用parent.rotateX(Math.PI/2)将父对象沿X轴旋转+90度。

(4) 将父对象添加到祖父对象中。

(5) 随后使用grandparent.lookAt(target_point_as_World_position_Vec3_or_x_y_z)

N.B.如果父级或祖先具有除(n,n,n)之外的缩放,则lookAt()将无法正常工作。

父对象和祖先对象类型可以是普通的THREE.Object3D,也可以是THREE.GroupTHREE.Mesh(如果需要可以设置为不可见,例如通过设置小尺寸或.visibility=false

Arrow Helper可以动态使用,但仅在使用lookAt()之前将内部方向设置为(0,0,1)时才能使用。


等一下,你的主要问题是你制作了一个指向上方并且lookAt指向z方向的箭头吗?如果是这样,那么你正在为解决一个只需要重新调整思维方式的问题而付出过多的努力。只要你设计一个指向那个方向的箭头,lookAt就会正确地指向箭头。此外,你还可以使用ArrowHelper来看看如何构建和使用它。这个辅助工具也使用lookAt而没有任何问题。 - somethinghere
@somethinghere - 请看我在问题中添加的结语。谢谢你的建议! - steveOw
2个回答

2

您可以将lookAt应用于任何Object3DObject3D.lookAt( ... )

您已经发现lookAt会导致形状指向+Z方向,并在进行补偿。但是,通过引入Group,可以进一步实现这一点。Group也是从Object3D派生的,因此它们也支持lookAt方法。

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>

重点在于圆锥/轴被制作成指向+Z方向,并添加到Group中。这意味着它们的方向现在是相对于组的本地坐标系。当组的lookAt发生变化时,形状也会随之改变。并且因为“箭头”形状指向组的本地+Z方向,这意味着它们也指向给定给group.lookAt(...);的任何位置。

进一步工作

这只是一个起点。您需要将其适应为如何在正确位置上构建箭头,具有正确的长度等。尽管如此,分组模式应该使lookAt更容易使用。

我的意思是,通过使用Object3D本身而不是Group来对所有内容进行分组,OP已经在做这个了,不是吗?Group和Object3D之间没有真正的区别,所以我不认为这会改变什么。否则,确实lookAt是解决方案。我不明白OP如何开始使用THREE.js而不遇到它-它在每个教程中都有提到。 - somethinghere
非常感谢。我一直在尝试将箭头组件添加到一个“辅助网格”中,然后将其设置为不可见,但使用“组”更加简洁。然后,我猜我需要交换相关的“Y操作”和“Z操作”。但是我不知道在thisArrow.quaternion.set()中应该设置什么四元数值,也许我可以保持它们不变? - steveOw
@somethinghere 我在我的问题中提到了关于lookAt的问题。问题是Arrow助手在使用lookAt时不够直观 - 也就是说,箭头没有指向lookAt的目标。 - steveOw
取消我的先前评论,我现在明白了,不需要进行坐标交换或四元数操作,只需按照我问题中的附录使用Object3D.add()和Object3D.rotateX即可。感谢你的回答。 - steveOw

1
你只需要更加了解嵌套,这样可以让你将对象相对于它们的父级放置。如上面的答案所述,你可以使用GroupObject3D,但不一定需要。你可以将箭头嵌套在圆柱体上,并将圆柱体指向z方向,然后使用内置且不会使事情变得过于复杂的方法lookAt
尽量不要在像这样简单的事情中使用矩阵或四元数,因为那会使你更难搞清楚事情。由于THREE.js允许嵌套框架,所以要充分利用它!

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>

你可以使用鼠标滚轮(如果有水平滚动条,应该在触控板上)进行旋转,并单击以指向箭头。我还添加了一些跟踪点,以便您可以看到“lookAt”不会过于复杂化而仍然起作用,并且它指向您在包装球上单击的点。
至此,我肯定打了太多次“shaft”这个词。它开始听起来有点奇怪。

感谢这个令人印象深刻的片段。我过去使用过“嵌套”和“lookAt”,但我认为遵循“官方”的设计模式可能是一个不错的主意,所以我想改编Arrow Helper工具的设计。我很高兴能够避免使用四元数,但我想知道为什么官方的Arrow Helper setDirection函数包含它们? - steveOw
@steveOw 因为setDirection需要一个归一化的向量来指向,而lookAt需要一个全局位置来注视。所以你可以使用lookAt([10,10,10]),但是你不能使用setDirection([10,10,10])(当然你可以,但我保证很快就会变得混乱,因为它不是一个归一化的向量)。如果你想自己实现setDirection,你可以这样做:this.quaternion.setFromUnitVectors([0,0,1], [othervector]),然后就完成了。你甚至可以从你的Y方向来实现这个。 - somethinghere
顺便说一下,箭头助手也可以与lookAt一起使用,setDirection只有在您确保箭头指向特定方向时才真正有用,与其当前位置无关等。 - somethinghere
不对,因为箭头默认就是这样的。而且有文档记录,three.js 使用 0,0,1 作为计算的基准方向,这是首选方向。在这里可以看到:https://dev59.com/kWzXa4cB1Zd3GeqPYNQl#14168533 - 文档中也有提到,但我还没找到具体位置。 - somethinghere
关于默认值,你是对的(但你必须查看源代码才能找到这个信息!)。在我看来,经常不得不使用源代码和Stack Overflow作为文档的一部分,正是使得THREE.js使用起来往往很笨拙。 - steveOw
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接