视图矩阵推导过程(基于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等编著)【电子工业出版社】》和《工程数学线性代数第六版》。