一个三维点最终展现在屏幕上要经过很多的矩阵变换,如果要根据屏幕上的2D点获取对应的三维信息,那么就需要对3D到2D转变的操作进行逆操作,我们先来看一下从3D到2D的具体的变换过程是怎么样的。
三维中一点要想展现在屏幕上需要经过这么几个重要的变换过程:模型变换->视点变换->投影变换->投影除法。
在顶点着色器中我们需要自己手动完成前三个变换过程(模型变换、视点变换、投影变换),如下所示:
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition,1.0);
模型变换:三维对象自身的模型矩阵乘以模型局部坐标中坐标的过程称作模型变换,经过模型变换后会将模型坐标转换为世界坐标系中的坐标,即世界坐标,以齐次坐标方式表示的世界坐标中的第四个分量w为1。
视点变换:视点矩阵乘以世界坐标的过程称作视点变换,经过视点变换后会将世界坐标转换为摄像机坐标系中的坐标,即视坐标,以其次坐标方式表示的视坐标中的第四个分量w为1。
投影变换:投影矩阵乘以视坐标的过程称作投影变换,经过投影变换后视坐标将会转换为裁剪坐标gl_Position,以齐次坐标方式表示的裁剪坐标中的第四个分量w此时应该不为1。
投影除法:裁剪坐标系中的w分量不为1,用裁剪坐标系中的x、y、z依次除以w,得到新的坐标(x/w,y/w,z/w),即归一化坐标(NDC),与视点坐标系相比,z轴方向会翻转,NDC坐标系是左手坐标系,NDC坐标不再是齐次坐标,而是3分量的三维坐标。
需要特别注意的是在WebGL中,模型变换、视点变换以及投影变换都是要我们自己去手动实现的,得到的gl_Postion即为经过了上面三个变换之后的裁剪坐标,其第四个分量的w不为1。在我们手动编程计算完gl_Position之后,进入GPU自身的流水管线,GPU会根据裁剪坐标gl_Position中xyz分量与w分量绝对值的大小进行比较进行裁剪。具体的过程是:GPU依次将gl_Position中x、y、z的绝对值与w的绝对值分别比较,只要有一个分量的绝对值大于w的绝对值,GPU就认为该点不在视景体内,就会被裁减掉,也就是说裁剪的过程是GPU自己进行的,没有被裁减掉的坐标xyz分量的绝对值都小于w的绝对值。经过裁剪之后,GPU进行投影除法,具体的过程是:会将齐次坐标转换为普通的三元的坐标(只有xyz,无w),会让裁剪坐标(裁剪坐标也是齐次坐标,包含w信息)中的xyz依次除以w,得到新的xyz,新的xyz就是归一化后的坐标,即归一化设备坐标(Normalized Device Coordinates),NDC坐标存放于一个立方体坐标系中。归一化后的NDC坐标中的x、y、z的取值范围都是[-1,1],所表达的含义是将Canvas的中心点定位[0,0],左下为[-1,-1],右上为[1,1],x和y能够表示出该点相对于Canvas中心原点的位置。NDC坐标中的z表示了深度信息,取值范围也是[-1,1]。(从fs里gl_DepthData取出来的应该是0-1),表示了深度信息,近裁剪面near对应z=0,远裁剪面far对应z=1。这样就完成了3D到2D的转变。
为了实现从2D到3D的逆变换,我们需要对上面的操作进行逆操作:
1.首先根据2D点坐标计算出NDC坐标,此时要把Canvas看成这样一个坐标系:原点在Canvas中心,并且左下为[-1,1],右上为[1,1],根据x和y的绝对坐标计算出NDC中的x和y(二者范围都是[-1,1])。NDC中的x和y虽然能计算了,但是深度值z是不确定的,我们可以在[0,1]之间任意取值,比如0.5,NDC中的w都为1,这样构建了NDC中的坐标[x,y,0.5,1]。
2.然后执行投影变换的逆操作,即先计算投影矩阵的逆矩阵,然后用该逆矩阵乘以NDC中的齐次坐标,得到“视坐标”。注意此处的视坐标是打了引号的,为什么呢?因为我们知道视坐标中的w分量是1,而NDC坐标经过投影变换的逆操作之后,w分量不再为1,我们需要将“视坐标”中的四个分量都除以“视坐标”中的w分量,这样可以保证w为1了,从而得到真正的视坐标。
3.然后执行视点变换的逆操作,即先计算视点变换的逆矩阵,然后用该逆矩阵乘以视坐标即可得到世界坐标。
4.注意,由于我们在第1步中的深度值z值是不固定的,可以在[0,1]之间任意取值,所以在第三步中得到的世界坐标也应该是无穷多的,也就是说我们无法根据2D点获取某个3D点,但是我们可以计算从摄像机出发沿着的单击屏幕的那条射线的方向。该拾取向量的计算过程很简单:获取摄像机在世界坐标系中的坐标,又已知第三步中计算得到的单击点的世界坐标,根据这两个世界坐标中的点就可以得到世界坐标系中的拾取向量。
具体的代码如下:
/** * 已验证正确 * 获取鼠标拾取向量(世界坐标系中的向量) * @param absoluteX 自左向右增大 * @param absoluteY 自上向下增大 * @return {*} */ World.PerspectiveCamera.prototype.getPickDirection = function(absoluteX,absoluteY){ var relativeCoords = World.Math.getPositionRelativeToCanvasCenter(absoluteX,absoluteY); var relativeX = relativeCoords[0]; var relativeY = relativeCoords[1]; var ndcX = relativeX/(World.Canvas.width/2); var ndcY = relativeY/(World.Canvas.height/2); var ndcZ = 0.5;//深度值,可在0-1之间随意取值 var ndcW = 1; var columnNDC = [ndcX,ndcY,ndcZ,ndcW];//NDC归一化坐标 var inverseProj = this.projMatrix.getInverseMatrix();//投影矩阵的逆矩阵 var columnCameraTemp = inverseProj.multiplyColumn(columnNDC);//带引号的“视坐标” var cameraX = columnCameraTemp[0]/columnCameraTemp[3]; var cameraY = columnCameraTemp[1]/columnCameraTemp[3]; var cameraZ = columnCameraTemp[2]/columnCameraTemp[3]; var cameraW = 1; var columnCamera = [cameraX,cameraY,cameraZ,cameraW];//真实的视坐标 var viewMatrix = this.getViewMatrix(); var inverseView = viewMatrix.getInverseMatrix();//视点矩阵的逆矩阵 var columnWorld = inverseView.multiplyColumn(columnCamera);//单击点的世界坐标 var verticeInWorld = new World.Vertice(columnWorld[0],columnWorld[1],columnWorld[2]); var cameraPositon = this.getPosition();//摄像机的世界坐标 var pickDirection = verticeInWorld.minus(cameraPositon); pickDirection.normalize(); return pickDirection; };