視圖矩陣推導過程(Demo基於WebGL 2.0實現)
一、概述
首先,我們需要了解些概念:
攝像機座標系或者攝像機空間:物體經攝像機觀察後,進入攝像機空間。
視變化,是將世界座標系下的座標變化到攝像機座標系,視變換是通過乘以視圖矩陣實現的。
我們要知道視並不存在真正的攝像機,只不過是在世界座標系裏面選擇一個點,作爲攝像機的位置。然後根據一些參數,在這個點構建一個座標系。然後通過視圖矩陣將世界座標系的座標變換到攝像機座標系下。
WebGL成像採用的是虛擬相機模型。在場景中你通過模型變換,將物體放在場景中不同位置後,最終哪些部分需要成像,顯示在屏幕上,主要由視變換和後面要介紹的投影變換、視口變換等決定。其中視變換階段,通過假想的相機來處理矩陣計算能夠方便處理。對於WebGL來說並不存在真正的相機,所謂的相機座標空間(camera space 或者eye space)只是爲了方便處理,而引入的座標空間。
完整變換流程概述可見:https://blog.csdn.net/weixin_37683659/article/details/79622618
二、推導目標
我們先簡單說一下我們的目標,在世界座標系中選取一點作爲觀察點,並以觀察點建立一個座標系,以觀察點建立的座標系就是我們需要的攝像機座標系,在建立此座標系後,我們做的就是通過矩陣將世界座標系下點的座標變換到攝像機座標系下。
三、線性代數的準備
我們首先要了解線性代數中的基變換與座標變換:
基變換:
座標變換:
座標變換公式證明:
上述是同濟大學線性代數書中的一部分。
四、推導過程
1、構建攝像機座標系:
攝像機9參數:
視點:相機在世界座標中的位置 eye(eyeX, eyeY, eyeZ)
觀測點:被觀察的目標點,指明相機的朝向 at(atX, atY,atZ)
頂部朝向:確定在相機哪個方向是向上的,一般取(0, 1, 0) up(upX,upY, upZ)
如下圖所示(截圖來自《交互式計算機圖形學》):
在使用過程中,我們是要指定的參數即爲攝像機位置(eye),攝像機指向的目標位置(target)和攝像機頂部朝向(up)向 量三個參數。
Step1 : 首選計算攝像機鏡頭方向 forwrad=(target−eye),
進行歸一化forward=forward/|forwrad|。
Step2: 根據up vector和forward確定攝像機的side向量:
歸一化up vector:viewUp′=viewUp/|viewUp|。
叉積:side=cross(forward,viewUp′)
Step3 : 根據forward和side計算up向量:
叉積:up=cross(side,forward)(注意此up向量是垂直於forward和side構成的平面)
這樣eye位置,以及forward、side、up三個基向量構成一個新的座標系,注意這個座標系是一個左手座標系,因此在實際使用中,需要對forward進行一個翻轉,利用-forward、side、up和eye來構成一個右手座標系,稱爲觀察座標系或者u-v-n座標系。
我們的目標是計算世界座標系中的物體在攝像機座標系下的座標,也就是從相機的角度來解釋物體的座標。從一個座標系的座標變換到另一個座標系,這就是不同座標系間座標轉換的過程。
2、利用旋轉和平移矩陣求逆矩陣
將世界座標系旋轉和平移至於相機座標系重合,這樣這個旋轉R和平移T矩陣的組合矩陣M=T∗R,就是將相機座標系中坐 標變換到世界座標系中座標的變換矩陣,那麼所求的視變換矩陣(世界座標系中座標轉換到相機座標系中座標的矩陣)view=M−1.
首先。我們要清楚將世界座標系座標系平移到相機座標系的目的是將它們放到同一線性空間下。上面已經提到過這是進行座標的基礎。平移的部分前面的博客已經介紹過。
我們寫出平移矩陣:
接下來,我們要求旋轉矩陣,利用基變換和座標變換進行求解。我們在此求的是將攝像機座標系下變換到世界座標系中座標的變換矩陣。
此時對於攝像機座標系中的一個點P(X, Y, Z),求在世界座標系中的點p(x, y, z),則:p=P * (U, V, N) 即
X = X * Ux + Y * Uy + Z * Uz;
Y = X * Vx + Y * Vy + Z * Vz;
Z = X * Nx + Y *Ny + Z * Nz;
此時我們可以得到一個座標基矩陣,(其實我們此時可以對旋轉有了更深層次的理解,旋轉其實可以理解爲基變換)就是上面求得的side、up、forward基向量構成的矩陣,寫成4×4的矩陣:
那麼所求的矩陣view計算過程如下:
在計算過程中,使用到了旋轉矩陣的性質,即旋轉矩陣是正交矩陣,它的逆矩陣等於矩陣的轉置。因此所求的:
五、案例demo
接下來我們看一個具體案例demo,此處爲攝像機旋轉觀察立方體(按A鍵即可):
效果圖:
着色器:
var VertexShader='#version 300 es \n' +
'uniform mat4 uMVPMatrix; \n ' + //總變換矩陣
' in vec3 aPosition; \n' + //頂點位置
' in vec4 aColor; \n' + //頂點顏色
'out vec4 aaColor;\n' + //傳遞給片元着色器的變量
'void main(){\n' +
'gl_Position=uMVPMatrix * vec4(aPosition,1);\n' +
'aaColor=aColor;\n' +
'}\n';
var FragmentShader='#version 300 es \n' +
'precision mediump float; \n' +
'in vec4 aaColor; \n' + //接受頂點着色器的值
'out vec4 fragColor; \n' + //輸出到片元的顏色
'void main(){ \n' +
'fragColor=aaColor;\n' + //給此片元的顏色值
'}\n';
初始化方法:
function start() {
var canvas = document.getElementById("webglcanvas");
gl = canvas.getContext('webgl2', { antialias: true });
if (!gl){ //若獲取GL上下文失敗
alert("創建GLES上下文失敗,不支持webGL2.0!"); //顯示錯誤提示信息
return;
}
//鏈接着色器
initShader(gl);
//綁定數據
var n = bindDataBuffer(gl);
//設置視口
gl.viewport(0,0,canvas.width, canvas.height);
gl.enable(gl.DEPTH_TEST);
//初始化相機座標
g_eyeX=0.4*Math.cos(angle);
g_eyeZ=0.4*Math.sin(angle);
//主繪製方法
draw(gl,n);
//dom監聽
document.onkeydown = function (event) {
if(event.key === 'a') {
angle+=0.01;
g_eyeX=0.4*Math.cos(angle);
g_eyeZ=0.4*Math.sin(angle);
console.log(angle)
draw(gl,n);
}
}
}
繪製方法:
//主繪製方法
function draw(gl,n) {
var cameraMatrix=multiMatrix44(getOrthoProjection(-1.0,1.0,-1.0,1.0,-1.0,1.0),setcamera(g_eyeX,0.4,g_eyeZ,0,0,0,0,1,0));
console.log(getOrthoProjection(-1.0,1.0,-1.0,1.0,-1.0,1.0))
var uMVPMatrix=gl.getUniformLocation(gl.program,"uMVPMatrix");
gl.uniformMatrix4fv(uMVPMatrix,false,new Float32Array(cameraMatrix));
var a_Position=gl.getAttribLocation(gl.program,"aPosition");
gl.enableVertexAttribArray(a_Position);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,0,0);
var a_Color=gl.getAttribLocation(gl.program,"aColor");
gl.enableVertexAttribArray(a_Color);
gl.bindBuffer(gl.ARRAY_BUFFER,colorBuffer);
gl.vertexAttribPointer(a_Color,4,gl.FLOAT,false,0,0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);// 指定清空canvas的顏色
gl.clear(gl.COLOR_BUFFER_BIT );
gl.drawArrays(gl.TRIANGLES, 0, n);
}
設置攝像機
function setcamera(eyeX,eyeY,eyeZ,targetX,targetY,targetZ,upX,upY,upZ) {
//求向量空間下的基(攝像機)
var zAxis=subVector([targetX,targetY,targetZ],[eyeX,eyeY,eyeZ]);
var N=normalizeVector(zAxis);
var xAxis=crossMultiVector(N,[upX,upY,upZ]);
var U=normalizeVector(xAxis);
var V=crossMultiVector(U,N);
//旋轉矩陣(線性變換部分爲基變換公式中的過度矩陣)
var R=[
U[0],V[0],-N[0],0,
U[1],V[1],-N[1],0,
U[2],V[2],-N[2],0,
0,0,0,1
]
//平移矩陣(實際上是將兩個座標系變換到同一個向量空間下)
var T=translation(-eyeX,-eyeY,-eyeZ);
console.log("攝像機"+multiMatrix44(R,T))
return multiMatrix44(R,T);
}
向量計算以及矩陣計算:
/**向量及矩陣運算**/
//向量減法
function subVector(v1,v2) {
return[v1[0]-v2[0],v1[1]-v2[1],v1[2]-v2[2]];
}
//向量加法
function addVector(v1,v2) {
return[v1[0]+v2[0],v1[1]+v2[1],v1[2]+v2[2]];
}
//向量歸一化
function normalizeVector(v) {
var len=Math.sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
return (len>0.00001)? [v[0]/len,v[1]/len,v[2]/len]:[0,0,0];
}
//向量叉乘
function crossMultiVector(v1,v2) {
return[
v1[1]*v2[2]-v1[2]*v2[1],
v1[2]*v2[0]-v1[0]*v2[2],
v1[0]*v2[1]-v1[1]*v2[0]
]
}
//向量點乘
function dotMultiVector(v1, v2) {
var res = 0;
for (var i = 0; i < v1.length; i++) {
res += v1[i] * v2[i];
}
return res;
}
//矩陣轉置
function transposeMatrix(mat) {
var res = new Float32Array(16);
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
res[i * 4 + j] = mat[j * 4 + i];
}
}
return res;
}
//4 x 4 矩陣乘法
function multiMatrix44(m1, m2) {
var mat1 = transposeMatrix(m1);
var mat2 = transposeMatrix(m2);
var res = new Float32Array(16);
for (var i = 0; i < 4; i++) {
var row = [mat1[i * 4], mat1[i * 4 + 1], mat1[i * 4 + 2], mat1[i * 4 + 3]];
for (var j = 0; j < 4; j++) {
var col = [mat2[j], mat2[j + 4], mat2[j + 8], mat2[j + 12]];
res[i * 4 + j] = dotMultiVector(row, col);
}
}
return transposeMatrix(res);
}
細心的朋友可以發現,我用正交投影矩陣乘了攝像機矩陣之後,傳入到着色器中去的,如果不設置投影矩陣會出現很大的問題,這個我會在後面闡述。
代碼知識簡要貼出一部分,其餘代碼是鏈接着色器程序,檢測着色器編譯狀態以及創建緩衝(綁定頂點和顏色數據)等,這些並不是本博客探究的重點,我貼出了向量及矩陣計算的部分,這是產生攝像機矩陣的重點,大家可以自行將代碼粘貼下來進行嘗試。
demo下載:https://download.csdn.net/download/weixin_37683659/10329752
六、總結
由於水平有限,寫的不好,大家多交流。這些東西需要一定的數學基礎和計算機圖形學的基礎。這裏推薦兩本書,《交互式計算機圖形學 基於OpenGL着色器的自頂向下方法(第6版)》(美Edward Angel等編著)【電子工業出版社】》和《工程數學線性代數第六版》。