WebGL自學課程(13):獲得三維拾取向量

孫群發表於2013-05-05

一個三維點最終展現在螢幕上要經過很多的矩陣變換,如果要根據螢幕上的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;
};


相關文章