最近的一个项目要做一个网页的三维,于是学习了一下THREE.JS。主要完成了三维模型的基本显示功能。当然显示模型十分简单,在官网随便看一个例子就知道怎么做了。
接下来就是模型的交互(旋转,缩放,平移等),官网提供了关于交互的插件(Trackball官网示例、Orbit官网示例),这两种插件在旋转和缩放方面都能够很好的支持,但是在平移方面,遇到了一些问题。
当缩放的距离变小,展示模型的大小变大,很有可能会超出屏幕大小;这时很有可能用户感兴趣的细节部分就超出了屏幕,如果只是单一的通过旋转看超出部分,又会影响视角。在上述的两种插件其实都提供了平移功能,插件应该是平移的相机。使用插件平移遇到的问题是:在平移相机之后,尽管看到了想要看到的细节,但是会影响之后的旋转和缩放。目前还不清楚为什么平移会影响到旋转和缩放,之后有空再去学习一下。
为了尽量方便的解决这个问题,并且在借鉴了一些三维软件的交互方式之后,决定只使用插件的旋转和缩放功能;平移功能通过平移模型来达到目的。
首先是把鼠标的屏幕坐标系转换成three.js中的设备坐标(根据官网的说法:射线),主要方法是:当前的设备坐标的中心点是(0, 0),左上角的坐标是(-1, 1)–对应展示三维的dom元素的左上角,右下角的坐标是(1, -1)-对应展示三维的dom元素的右下角。
我们可以通过当前鼠标距离浏览器窗口减去显示区域距离窗口左上角计算出鼠标距离显示区域左上角的偏移量(借鉴前辈博客)。
let left = wrap.getBoundingClientRect().left;
let top = wrap.getBoundingClientRect().top;
let clientX = event.clientX - left;
let clientY = event.clientY - top;
然后通过获取的偏移量计算标准化后的设备坐标
let mouse = new THREE.Vector2();
mouse.x = (clientX / wrap.offsetWidth) * 2 - 1;
mouse.y = -(clientY / wrap.offsetHeight) * 2 + 1;
这一步需要用到上述提到的THREE.JS提供的射线类。根据官方的说法:这个类设计出来的目的是协助光线投射。 Raycasting用于鼠标拾取(计算鼠标所在的3d空间中的对象)等。
这个类有一个setFromCamera方法,通过相机状态和转换后的设备坐标两个参数可以确定这条射线。
let raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
其实上述两个步骤在其它很多博客中都已经解释得很清楚了。这并不足以帮助我解决我面临得问题。平移模型其实是将模型在一个平行与电脑屏幕的平面上平移。
不需要找到模型具体所在的平面。想象电脑屏幕在THREE.JS建立的三维空间中的平面为A,需要找到鼠标移动前,在平面A上对应的三维点a;鼠标移动后,在平面A上对应的三维点b;用b-a得到鼠标在平面A上移动的向量L。这样只需要把要平移的模型沿着向量L移动达到了效果。
通过相机所在的位置,可以确定屏幕在三维空间中的平面。相机一直在该平面的中心,这样相机所在位置就是平面中的一点;可以通过THREE.JS提供的获取相机方向的方法获取相机的方向(相机的方向即平面的法向量)。这样平面上的一点和平面法向量确定就可以确定一个平面(高中数学,但是还是要百度才想起,呜呜呜呜)。代码如下:
function setPlane() {
var cameraPoint=new THREE.Vector3();
var plane=new THREE.Plane();
plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( plane.normal ), cameraPoint);
return plane;
}
在确定了平面之后,找到第二步求出来的射线和平面的交点就是对应的三维坐标点。代码如下:
function screenTOWorld(clientX,clientY) {
//三维坐标
var worldPoint=new THREE.Vector3();
//获取渲染的DOM元素
var rect = renderer.domElement.getBoundingClientRect();
//转设备坐标
mousePoint.x = ( ( clientX - rect.left ) / rect.width ) * 2 - 1;
mousePoint.y = - ( ( clientY - rect.top ) / rect.height ) * 2 + 1;
raycaster.setFromCamera( mousePoint, camera );
//获取平面
var screenPlane=setPlane();
//求交
raycaster.ray.intersectPlane(screenPlane,worldPoint);
return worldPoint;
}
在鼠标点击之前,获得模型的坐标B;鼠标移动的时候,用B加上之前得到的向量L,得到模型应该移动的距离。
鼠标点击事件:
function mouseDown(event) {
event.preventDefault();
lastPoint.copy(object.position);
lastIntersection = screenTOWorld(event.clientX, event.clientY);
inverseMatrix.getInverse(object.matrixWorld);
mouseDown = true;
}
鼠标移动事件:
function mouseMove(event) {
event.preventDefault();
if(mouseDown) {
var intersection = screenTOWorld(event.clientX, event.clientY);
offset.subVectors(intersection,lastIntersection);
var point=offset.add(lastPoint);
object.position.copy(point);
}
}
鼠标弹起事件:
function mouseUp(event) {
mouseDown=false;
}
希望我的这篇博客能够为您现在遇到的困难提供绵薄之力。刚接触三维的项目,如果在理解上,或者某些步骤有问题,可以改进,也希望各位提出意见。谢谢大家!