这是3D可视化教程系列的文章,如果第一次阅读请先阅读《3D可视化教程导读》

源码及3D项目文件

  源码及工程项目都放到github上。
  源码:threejs-example

效果

 可在线访问看效果6-屏幕坐标转换 展示网址,展示了从屏幕坐标转换到归一化坐标:
6-屏幕坐标转换

从3D空间到屏幕平面

 three.js里的三维坐标对应canvas的坐标就是这样计算的:

1
2
3
4
5
6
7
8
9
// 获取物体在3D空间里的坐标
const vector = new THREE.Vector3();
box3.setFromObject(object3d);
box3.getCenter(vector);
// 转换为归一化坐标
vector.project(camera);
// 转换成HTML坐标
result.x = vector.x * widthHalf + widthHalf;
result.y = -(vector.y * heightHalf) + heightHalf;

 二维坐标投射到一维,只取其一维的坐标:比如说(1,2)投到X轴上,就是1。同理,三维空间坐标投射到二维平面坐标,取其二维坐标,比如说(4,5,6)投到XY平面,就是(4,5)。3D场景所表现的内容,最终都必须要投射成平面坐标(毕竟我们显示器显示的内容是二维平面的东西)。

ViewFrustum

 然而,3D场景所表现的内容,不仅仅取决于物体的三维坐标,也取决于摄像头(人眼)的坐标。比如说,你站在(0,0,0)的位置上去看,与站在(4,4,4)去看,所展示的视觉效果是不一样的。
 界面上所展示出来的样子,其实是由“物体坐标”与“摄像头坐标”两者共同决定的,里面底层的矩阵转换是怎么计算的,等你有兴趣了解的时候再去看书,里面会提到,现在只需要知道需要转换,调用一下API即可。
 从3D空间转换到屏幕坐标的过程:3D空间坐标->归一化坐标->屏幕坐标,归一化坐标(-1 ~ +1)的转换关系与我们平时所学的二维坐标空间是一致的,对应代码vector.project(camera);
归一化坐标
 在HTML里,左上角是(0,0),数值往右下增长。而在归一化坐标里,中间才是(0,0),往左上增长且最大值为+1。所以两者之间需要转换,才能得到HTML里的坐标。这转换计算是初中数学水平难度,举例假设归一化坐标值为(-0.5,1),canvas占用HTML全屏,其高宽都为1000,转换成HTML坐标的left= (-0.5*500) + 500 = 250,top = -(1*500) + 500 = 0

从屏幕平面到3D空间

Raycaster用于把计算鼠标所指向的3D空间的物体。抓取物体的屏幕坐标转换方式如下,先在onMouseMove里把点击事件的坐标转换成three.js里的坐标并存放到mouse,再通过raycaster.setFromCamera( mouse, camera );来计算出鼠标坐标所指向的位置包含了哪些物体:
 显然,这次计算是 从3D空间到屏幕平面 的反向计算,屏幕坐标->归一化坐标->3D空间坐标,需要注意的是,从3D空间到屏幕平面的转换是多对一关系,即(4,5,6)(4,5,7)两个投到XY平面都是(4,5),所以平面上(4,5)对应的是3D空间(4,5,z)里无数的值。鼠标所指向的3D物体,可能是无数个3D物体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function onMouseMove( event ) {
// calculate mouse position in normalized device coordinates
// (-1 to +1) for both components
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}

function render() {
// update the picking ray with the camera and mouse position
raycaster.setFromCamera( mouse, camera );
// calculate objects intersecting the picking ray
var intersects = raycaster.intersectObjects( scene.children );
for ( var i = 0; i < intersects.length; i++ ) {

intersects[ i ].object.material.color.set( 0xff0000 );

}
renderer.render( scene, camera );
}
window.addEventListener( 'mousemove', onMouseMove, false );
window.requestAnimationFrame(render);

不占全屏的计算方式

 在实际使用时,canvas常常并不会占用整个HTML的屏幕(比如说有左侧菜单栏,左右侧图表,顶标题,底数据栏等等),在转换时需要额外考虑偏移值即可:
不占全屏的转换计算

 从HTML到归一化坐标:

1
2
mouse.x = ( (event.clientX-offsetLeft) / clientWidth ) * 2 - 1;
mouse.y = - ( (event.clientY-offsetTop) / clientHeight ) * 2 + 1;

从归一化到HTML坐标:

1
2
result.x = vector.x * widthHalf + widthHalf + offsetLeft;
result.y = -(vector.y * heightHalf) + heightHalf + offsetTop;

附录