上节把箭头加上了:

这节来做拖拽改变物体位置的功能。
也就是这个:

在网页里我们怎么实现拖拽呢?
就是 mousedown 的时候记录按下状态。
mousemove 的时候如果是按下就可以拖动。
mouseup 的时候取消按下状态。
在 3D 场景里自然也是这样。
首先,我们把 renderer 导出:

在 controls 实现里监听 mousedown 事件:

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:


arrowTopMesh.name = 'arrowTop';
arrowLeftMesh.name = 'arrowLeft';
arrowFrontMesh.name = 'arrowFront';
跑一下试试:

这样就能知道是选中了那个箭头。
然后我们记录下状态:

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 事件:

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 第一个参数是平面法向量,第二个参数是原点到平面的距离。
试下效果:

现在按下之后左右拖动就可以改变 obj 的 x 位置了。
然后处理下抬起 mouseup:

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

这样,从鼠标按下、拖动、抬起的全流程就处理完了。
同样的方式处理下 y 轴和 z 轴的箭头:

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;
}
试下效果:

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

酷家乐这个还做了拖动一个方向的箭头的时候,把其他方向的隐藏。
这个不是重点,可以自己完善。
案例代码上传了小册仓库
总结
这节我们把箭头的拖拽实现了一下。
和网页里拖拽一样,要处理 mousedown、mousemove、mouseup 事件。
mousedown 的时候判断点击了那个箭头,记录状态。
mousemove 的时候根据状态来移动物体,这里创建一个 Plane 来计算交点,然后设置物体位置。禁用掉 OrbitControls,不然会冲突。
mouseup 的时候恢复之前的状态。恢复 OrbitControls
这样,就实现了酷家乐控制器的拖拽功能。