现状:Threejs官方demo提供了DragControls.js平面拖拽控件,但只能拖拽Object在垂直于相机法线的平面上移动
目标场景:在六面体空间盒子中沿着六面体平面拖拽物体,并且需要限制在盒子内部
已知:空间盒子大小,对象所在的平面
方案设计

  • 将Object拖拽依赖的信息放在userData中,示例如下
  • 基于拖拽控件,改造部分关键代码,实现目标
// 模型对象部分参数
{
    // ...
    userData: {
        type: 'tm', // Object类型,按需定义
        mesh: {
            // ...
            position3D: {
                // ...
                d: 'bottom', // Object 所在的平面,取值bottom,up,back,front,left,right
                limit: {
                    x: {
                      min: -500,
                      max: 500,
                    },
                    y: {
                      min: 0,
                      max: 1000,
                    },
                    z: {
                      min: -500,
                      max: 500,
                    },
                }
            }
        }
    }
}

源码分析

/** DragControls.js插件关键代码展示 */

// let _selected = null; // 选中的对象
const _plane = new Plane(); // 移动平面
const _raycaster = new Raycaster(); // 基于鼠标和相机位置确定的射线

function onPointerDown( event ) {
    // 此处省略...
    if ( _intersections.length > 0 ) {
        // 赋值选中对象
        _selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object;
        // 根据选中对象位置和相机的法线确定可拖拽平面
        _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
    }
    // 此处省略...
}

function onPointerMove( event ) {
    // 此处省略...
    if ( _selected ) {
        // 判断射线和平面相交,赋值Object新的位置
    if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
        _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
    }
    }
    // 此处省略...
}
let _selected = null; // 选中的对象
const _plane = new Plane(); // 移动平面
const _raycaster = new Raycaster(); // 基于鼠标和相机位置确定的射线

function onPointerDown( event ) {
    // 此处省略...
    if ( _intersections.length > 0 ) {
        // 赋值选中对象
        _selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object;
        // 根据选中对象位置和相机的法线确定可拖拽平面
        _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
    }
    // 此处省略...
}

function onPointerMove( event ) {
    // 此处省略...
    if ( _selected ) {
        // 判断射线和平面相交,赋值Object新的位置
    if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
        _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
    }
    }
    // 此处省略...
}

代码改造

/** newDragControls.js 改造后关键代码 */
// 假设空间盒子的底面位于xz平面,y正方向放置
// 六面体的法线方向
const planeNormal = {
    'bottom': new Vector3(0, 1, 0),
    'up': new Vector3(0, 1, 0),
    'back': new Vector3(0, 0, 1),
    'front': new Vector3(0, 0, 1),
    'left': new Vector3(1, 0, 0),
    'right': new Vector3(1, 0, 0),
}

function onPointerDown( event ) {
    // 此处省略...
    if ( _intersections.length > 0 ) {
        // 获取当前对象所在平面的法线
        const normal = planeNormal[object.userData?.mesh?.position3D?.d] || _camera.getWorldDirection( _plane.normal )
        // 中心点默认取用对象的位置
    let point = _worldPosition.setFromMatrixPosition( object.matrixWorld )
    _plane.setFromNormalAndCoplanarPoint( normal, point);
    }
    // 此处省略...
}

function onPointerMove( event ) {
    // 此处省略...
    if ( _selected ) {
        if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
        const direction = new Vector3()
        direction.copy(_intersection).sub(_raycaster.ray.origin).normalize()
        const angle = _raycaster.ray.direction.angleTo(direction)
                    
        const distance = _raycaster.ray.origin.distanceTo(_intersection)
        const limit = _selected.userData?.mesh?.position3D?.limit
        const max = Math.max(limit?.x?.max * 2, limit?.y?.max * 2, limit?.z?.max * 2) * 2 || 0

        // 优化拖拽体验,避免闪现移动
        if (angle < 1 && (distance < max || !max)) {
            // 限制移动范围
        const newPosition = _intersection.sub( _offset ).applyMatrix4( _inverseMatrix )
    
        if (limit) {
            newPosition.x = calPosition(newPosition.x, limit.x)
            newPosition.y = calPosition(newPosition.y, limit.y)
            newPosition.z = calPosition(newPosition.z, limit.z)
        }
        }
    }
    }
    // 此处省略...
}

// p - 某个方向的坐标 limit - 大小限制 {min: 0, max: 0}
function calPosition (p, limit) {
    let r = p
    if (limit) {
        if (r > limit.max) {
        r = limit.max
    }
    if (r < limit.min) {
        r = limit.min
    }
    }
    return r
}

onPointerDown函数中根据Object所在平面确定了_plane参数,然后在onPointerMove函数中根据拖拽位置确定了Object的目标位置,并根据limit参数限制确定了最终的位置信息,同理可推广到任意平面。

优化彩蛋
如改造代码所示,拖拽点是Object的位置,当移动距离较大时,视觉上物体移动的位置和鼠标的位置存在偏差,给人一种错觉,以底面和顶面尤为显著,故在特定场景下取用户点击的实际位置

function onPointerDown( event ) {
    // 此处省略...
    if ( _intersections.length > 0 ) {
        // 获取当前对象所在平面的法线
        const normal = planeNormal[object.userData?.mesh?.position3D?.d] || _camera.getWorldDirection( _plane.normal )
        // 中心点默认取用对象的位置
    let point = _worldPosition.setFromMatrixPosition( object.matrixWorld )
    // 上下平面,取用射线和对象相交点的平面
    if (['bottom', 'up'].includes(object.userData?.mesh?.position3D?.d)) {
            point = intersection.point
    }
    _plane.setFromNormalAndCoplanarPoint( normal, point);
    }
    // 此处省略...
}

PatWu16
72 声望6 粉丝

仰望星空,脚踏实地