【问题标题】:THREE.js Thick Arrow with lookAt() capability具有 lookAt() 功能的 THREE.js 粗箭头
【发布时间】:2021-03-15 10:51:03
【问题描述】:

我想制作一个“粗箭头”网格,即像 standard Arrow Helper 这样的箭头,但轴由 cylinder 而不是 line 制成。


tldr; 不要复制 Arrow Helper 的设计;请参阅问题末尾的结语部分。


所以我复制并修改了代码以满足我的需要(免除了构造函数和方法)并进行了更改,现在它可以正常工作了:-

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

//... 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() 功能的任何解决方案/建议,我将不胜感激。

结语

我的主要观点是不遵循辅助箭头的设计。

正如 TheJim01 和 somehere 的回答所表明的,使用 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,n,n),则lookAt() 将无法正常工作。

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

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

【问题讨论】:

  • 等一下,您的主要问题是您制作了一个向上的箭头并在 z 方向上查看点?在这种情况下,您将竭尽努力解决只需要重新调整心态的问题。只要您设计了一个指向该方向的箭头,lookAt 就会正确地指向该箭头。最重要的是,您可以使用ArrowHelper 来查看如何 这可以构造和工作。该助手还使用lookAt 没有任何问题。
  • @somethinghere - 请参阅我添加到问题中的结语。感谢您的意见!

标签: javascript three.js


【解决方案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; }
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r123/three.min.js"&gt;&lt;/script&gt;

您可以使用鼠标滚动(如果它有水平滚动,应该在触控板上)并单击以指向箭头。我还添加了一些跟踪点,以便您可以看到 `lookAt'确实 工作而不会过于复杂,那就是 is 指向您在包裹球体上单击的点。

因此,我确实经常输入 shaft 这个词。它开始听起来很奇怪。

【讨论】:

  • 感谢令人印象深刻的 sn-p。我过去使用过“嵌套”和lookAt,但认为遵循“官方”设计模式可能是一个好主意,因此提出了适应箭头助手工具设计的想法。我很高兴避免使用四元数,但我想知道为什么官方 Arrow Helper setDirection 函数包含它们?
  • @steveOw 因为 setDirection 需要一个归一化的向量来指向,而 lookAt 需要一个全局位置来查看。所以你可以lookAt([10,10,10]),但你不能setDirection([10,10,10])(你可以,但我保证它会很快开始变得混乱,因为它不是一个标准化的向量)。如果你想自己实现 setDirection,你可以做 this.quaternion.setFromUnitVectors( [0,0,1], [othervector]) 并完成它。您甚至可以从 Y 方向实现这一点。
  • 顺便说一下,箭头助手 also 可以与lookAt 一起使用,setDirection 仅在您想绝对确定箭头指向某个特定位置时才真正有用方向,与当前位置无关等...
  • 据我所知:箭头助手只能与 lookAt 一起使用,前提是它首先设置为指向 [0,0,1]。我不认为这样的预配置对于 THREE.js 的普通用户来说是直观的,officially documented 也不是。
  • 不正确,因为箭头会将其作为默认值。还记录了threejs使用0,0,1作为其计算基线,这是首选方向。见这里:stackoverflow.com/a/14168533/2991619 - 它也在文档中的某个地方,但还找不到。
【解决方案2】:

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

您已经发现lookAt 导致形状指向+Z 方向,并且正在对此进行补偿。但是通过引入Group 可以更进一步。 Groups 也是从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;
}
&lt;script src="https://threejs.org/build/three.min.js"&gt;&lt;/script&gt;

这里的关键是使圆锥/轴指向+Z方向,然后添加到Group。这意味着他们的方向现在是该组的本地。当组的lookAt 发生变化时,形状也会随之变化。由于“箭头”形状指向该组的本地 +Z 方向,这意味着它们也指向指定给 group.lookAt(...); 的任何位置。

进一步的工作

这只是一个起点。您需要调整它以适应您希望它如何在正确的位置、正确的长度等处构造箭头。不过,分组模式应该使 lookAt 更易于使用。

【讨论】:

  • 我的意思是,OP 不是已经通过使用 Object3D 本身而不是 Group 对所有内容进行分组来做到这一点吗? Group 和 Object3D 之间没有真正的区别,所以我没有看到 that 改变了什么?否则,确实lookAt 是这里的解决方案。我不明白 OP 如何在不遇到它的情况下开始使用 THREE.js - 它在每个教程中。
  • 非常感谢。我一直在尝试将箭头组件添加到helper mesh,然后我将使其不可见,但使用group 更加精简。然后我想我必须交换相关的Y operationsZ operations。但是我不知道thisArrow.quaternion.set() 中的四元数设置应该是什么,也许我可以保持不变?
  • @somethinghere 我在我的问题中提到了lookAt 的问题。问题是箭头助手不能“直观地工作”lookAt - 即箭头不指向 lookAt 目标。
  • 取消我之前的评论,我现在明白了,不需要坐标交换或四元数,只需使用我的问题后记中描述的 Object3D.add() 和 Object3D.rotateX。谢谢你的回答。
猜你喜欢
  • 2019-06-17
  • 1970-01-01
  • 1970-01-01
  • 2019-06-25
  • 2016-08-01
  • 2017-12-25
  • 2016-02-22
  • 1970-01-01
  • 2016-11-29
相关资源
最近更新 更多