Skip to content

133. 实战:实现酷家乐同款拖拽旋转控制器(三)

Published:

上节把箭头加上了:

2025-09-19 19.35.03.gif

这节来做拖拽改变物体位置的功能。

也就是这个:

2025-09-19 19.39.51.gif

在网页里我们怎么实现拖拽呢?

就是 mousedown 的时候记录按下状态。

mousemove 的时候如果是按下就可以拖动。

mouseup 的时候取消按下状态。

在 3D 场景里自然也是这样。

首先,我们把 renderer 导出:

image.png

在 controls 实现里监听 mousedown 事件:

image.png

renderer.domElement.addEventListener('mousedown', (e) => {
    const y = -((e.offsetY / window.innerHeight) * 2 - 1);
    const x = (e.offsetX / window.innerWidth) * 2 - 1;

    const rayCaster = new THREE.Raycaster();
    rayCaster.setFromCamera(new THREE.Vector2(x, y), camera);

    const intersections = rayCaster.intersectObjects([arrowFrontMesh, arrowLeftMesh, arrowTopMesh]);

    if(intersections.length) {
        console.log(intersections[0].object.name);
    }
});

这里我们给每个箭头一个 name:

image.png

image.png

arrowTopMesh.name = 'arrowTop';
arrowLeftMesh.name = 'arrowLeft';
arrowFrontMesh.name = 'arrowFront';

跑一下试试:

2025-09-19 22.52.22.gif

这样就能知道是选中了那个箭头。

然后我们记录下状态:

image.png

let draggingX = false;
let draggingY = false;
let draggingZ = false;
renderer.domElement.addEventListener('mousedown', (e) => {
    const y = -((e.offsetY / window.innerHeight) * 2 - 1);
    const x = (e.offsetX / window.innerWidth) * 2 - 1;

    const rayCaster = new THREE.Raycaster();
    rayCaster.setFromCamera(new THREE.Vector2(x, y), camera);

    const intersections = rayCaster.intersectObjects([arrowFrontMesh, arrowLeftMesh, arrowTopMesh]);

    if(intersections.length) {
        draggingX = false;
        draggingY = false;
        draggingZ = false;
        switch(intersections[0].object.name) {
            case 'arrowTop':
                draggingY = true;
                break;
            case 'arrowFront':
                draggingZ = true;
                break;
            case 'arrowLeft':
                draggingX = true;
                break;
        }
    }
});

根据名字判断是哪个在拖拽。

然后来处理 mousemove 事件:

image.png

renderer.domElement.addEventListener('mousemove', (e) => {
    const y = -((e.offsetY / window.innerHeight) * 2 - 1);
    const x = (e.offsetX / window.innerWidth) * 2 - 1;

    const rayCaster = new THREE.Raycaster();
    rayCaster.setFromCamera(new THREE.Vector2(x, y), camera);

    if(draggingX) {
        controls.enabled = false;

        const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -this.obj.position.y);

        const intersection = new THREE.Vector3();
        rayCaster.ray.intersectPlane(plane, intersection);

        this.obj.position.x = intersection.x;
    }
});

首先,禁用掉 OrbitControls,不然会冲突。

创建一个平面 Plane,让射线和它计算交点,这个就是我们要拖动到的位置。

Plane 第一个参数是平面法向量,第二个参数是原点到平面的距离。

试下效果:

2025-09-19 23.16.39.gif

现在按下之后左右拖动就可以改变 obj 的 x 位置了。

然后处理下抬起 mouseup:

image.png

renderer.domElement.addEventListener('mouseup', (e) => {
    draggingX = false;
    draggingY = false;
    draggingZ = false;
    controls.enabled = true;
});

2025-09-19 23.21.14.gif

这样,从鼠标按下、拖动、抬起的全流程就处理完了。

同样的方式处理下 y 轴和 z 轴的箭头:

image.png

if(draggingY) {
    controls.enabled = false;

    const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), -this.obj.position.x);

    const intersection = new THREE.Vector3();
    rayCaster.ray.intersectPlane(plane, intersection);

    this.obj.position.y = intersection.y;
}

if(draggingZ) {
    controls.enabled = false;

    const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -this.obj.position.y);

    const intersection = new THREE.Vector3();
    rayCaster.ray.intersectPlane(plane, intersection);

    this.obj.position.z = intersection.z;
}

试下效果:

2025-09-19 23.25.39.gif

这样,我们的仿酷家乐控制器的拖拽功能就完成了。

对比下:

2025-09-19 23.26.36.gif

酷家乐这个还做了拖动一个方向的箭头的时候,把其他方向的隐藏。

这个不是重点,可以自己完善。

案例代码上传了小册仓库

总结

这节我们把箭头的拖拽实现了一下。

和网页里拖拽一样,要处理 mousedown、mousemove、mouseup 事件。

mousedown 的时候判断点击了那个箭头,记录状态。

mousemove 的时候根据状态来移动物体,这里创建一个 Plane 来计算交点,然后设置物体位置。禁用掉 OrbitControls,不然会冲突。

mouseup 的时候恢复之前的状态。恢复 OrbitControls

这样,就实现了酷家乐控制器的拖拽功能。

评论