在 OpenGL 投影矩阵 这篇文章中,讲述了 OpenGL 坐标系统中的投影矩阵,有两种类型的投影矩阵,分别是正交投影和透视投影。html
这两种投影实质上是两种类型的裁剪空间,分别建立对应视景体对物体坐标进行裁剪,位于裁剪空间内的才会被映射到屏幕上,以下图所示:(图片来源:glumpy.github.io/modern-gl.h…)java
当定义裁剪空间视景体时,咱们都须要提供近平面和远平面的距离,这里的近和远都是指相对于视点
的,视点也就是咱们这篇文章要讲到的摄像机。git
在上面的图片中,咱们能够把投影矩阵的视景体的四条虚线边当作是以摄像机为起始点发出的射线。github
这样一来,当起始点也就是摄像机位置发生改变时,它所发出的射线也会随之改变,那么视景体的形状也就改变了,在其内部所观察到的内容也会发生变化。微信
好比,假设此时摄像机位于彩色小球的上面,那么整个视景体就是垂直立起来了的,所观察到的小球也是朝上的那一面了。ide
因此,能够看到相机的位置和朝向,决定了视景体在什么位置和什么朝向展开。函数
在 OpenGL 坐标系统的转换公式中也能够印证这一点:post
它的计算顺序是左乘,也就是说要先进行视图矩阵的计算,而后再进行投影矩阵的计算,这样一来咱们就要先肯定了相机的位置,而后再根据相机肯定投影矩阵。spa
在这里,肯定相机的位置,并不只仅是定义相机在三维中的 坐标,而是要肯定一个以相机位置为原点的坐标系。.net
在上面也提到,投影矩阵或者说视景体的一个展开,是以相机做为参考的,那么咱们确定还须要一个摄像机的观察方向,这个方向就是视景体展开的方向。
如上图的左二内容所示,摄像机在 Z 轴正方向向坐标系的原点进行观察,假设此时摄像机坐标为 ,而原点为
,那么观察方向就是从
点向
点。而方向向量就是
,就是向量
,它的方向也就是图二中的蓝色箭头所示,能够看到 摄像机的方向向量和它的观察方向正好是相反的。
一个三维的空间坐标系是须要三个互相垂直的轴的,如今已经有了方向向量这一个了。这时能够借助一个辅助向量 上向量 ,把上向量与方向向量进行叉乘,
,就能够获得一个向量,同时垂直于上向量和方向向量,它就是右向量
,它的方向指向
轴正方向 。这里要当心叉乘的顺序,不然获得的方向就是反的了。
如图三因此,灰色的就是辅助上向量 ,而红色箭头所指方向就是
轴正方向。
再利用右向量和方向向量的叉乘,就能够获得指向摄像机 轴方向的向量,如最右图的绿色箭头所示。
这样就构造了三个轴互相垂直的坐标系,它就是摄像机的坐标系。
从上面的内容能够看到,只要知道了相机坐标点,以及观察的点,还有辅助的上向量,就能够肯定摄像机的坐标系了。
肯定摄像机以后,就是用它来生成咱们的观察矩阵,把观察矩阵用于在 OpenGL 渲染管线中进行处理。
和投影矩阵同样,Android 也提供了对应函数 Matrix.setLookAtM
来生成 OpenGL 坐标转换中的观察矩阵。
/** * Defines a viewing transformation in terms of an eye point, a center of * view, and an up vector. * * @param rm returns the result * @param rmOffset index into rm where the result matrix starts * @param eyeX eye point X * @param eyeY eye point Y * @param eyeZ eye point Z * @param centerX center of view X * @param centerY center of view Y * @param centerZ center of view Z * @param upX up vector X * @param upY up vector Y * @param upZ up vector Z */
public static void setLookAtM(float[] rm, int rmOffset, float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ) 复制代码
其中,第一个参数就是要传入的观察矩阵,第二个参数是偏移量,这个通常是 0 ,以后就是相机位置、观察点、辅助上向量。
接下来经过移动摄像机来观察物体,从而加深对摄像机的理解。
用 OpenGL 来绘制一个立方体,并经过旋转移动摄像机,让摄像机绕 轴作圆形旋转,从而能够从不一样方向来观察物体,效果图以下:
让立方体稍微向 轴作一点倾斜,这样最多就能够观察到三个面了。
具体代码示例:
var num = 0
var RotateNum = 360 // 绕 Y 轴作圆形旋转,把圆分红 360 份
val radian = (2 * Math.PI / RotateNum).toFloat() // 每一份对应的弧度
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
//... 省略代码
// 经过 RxJava 的 interval 操做符,每隔 30 毫秒,移动相机观察角度
Observable.interval(30, TimeUnit.MILLISECONDS)
.subscribe {
eyeX = eyeDistance * Math.sin((radian * num).toDouble()).toFloat()
eyeZ = eyeDistance * Math.cos((radian * num).toDouble()).toFloat()
num++
if (num > 360) {
num = 0
}
}
// 设置观察矩阵
MatrixState.setCamera(eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ)
// 让物体稍微向 Z 轴负方向倾斜必定角度
MatrixState.rotate(-30f, 0f, 0f, 1f)
}
复制代码
在 GLSurfaceView 的 onSurfaceChanged 方法里面设定观察矩阵,并经过 RxJava 的 interval 操做符每隔 30 毫秒改变相机位置的 坐标和
坐标,让摄像机在
平面上绕
轴作圆周运动。
在 onDrawFrame 方法里,每当坐标改变了,就改变相机的位置。
override fun onDrawFrame(gl: GL10?) {
//... 省略代码
MatrixState.setCamera(eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ)
GLES20.glUniformMatrix4fv(uViewMatrixAttr, 1, false, MatrixState.getVMatrix(), 0)
//... 省略代码
}
复制代码
因为是作圆周运动,圆的半径是没有变的,因此看到的物体大小是不变的,只是看到的内容不一样。
咱们还能够先后移动相机,这样就至关于人走近或者离开物体,感受到物体大小发生变化(实际上并无)。
如上图,物体仍是那个物体,可是从不一样的远近来观察,所看到的大小就不同了。
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
super.onSurfaceChanged(gl, width, height)
var isPlus = true
var distance = eyeZ
Observable.interval(100, TimeUnit.MILLISECONDS)
.subscribe {
distance = if (isPlus) distance + 0.1f else distance - 0.1f
if (distance < 2.0f) {
isPlus = true
}
if (distance > 5.0f) {
isPlus = false
}
eyeZ = distance updateCamera() } // 将物体调整一下,能够看到三个面 MatrixState.rotate(-45f, 0f, 1f, 0f) MatrixState.rotate(45f, 1f, 0f, 0f) } 复制代码
如上代码所示,咱们改变了相机的 轴坐标,让它在
之间来回移动,这样就达到了先后移动相机的效果。
最后,还能够把两种旋转结合起来,即作圆周运动又先后移动相机,效果以下:
经过上面的例子,就应该对 OpenGL 中的相机有一个更加清晰的认识了。
具体代码详情,能够参考个人 Github 项目:
最后,若是以为文章不错,欢迎关注微信公众号:【纸上浅谈】