OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
从着色器到屏幕的坐标变换
顶点着色器上的原始 gl_Position 坐标变换为最终的屏幕坐标流程
剪裁空间
当顶点着色器把一个值写到 gl_Position 的时候,这个位置是在剪裁空间(clip space)中的。在剪裁空间中,对于任何给定的位置,它的x、y以及z分量都需要在那个位置的-w和w之间。比如,如果一个位置的w是1,那么其x、y以及z分量都需要在-1和1之间。任何这个范围外的事物在屏幕上都不可见的
透视除法
OpenGL 使用透视除法将剪裁空间中的位置坐标变换为归一化设备坐标,不管渲染区域的大小和形状,对于其中的每个可视坐标,其x、y和z分量的取值都位于[-1,1]的范围内
为了在屏幕上创建三维的幻象,OpenGL会把每个 gl_Position 的x、y和z分量都除以它的w分量。因此,当w分量用来表示距离的时候,就使得较远处的物体被移动到距离渲染区域中心更近的地方,这个中心的作用就像一个消失点
为什么不把 z 解释为距离,而是保留 z 分量作为深度缓冲区(depth buffer),并增加 w 作为第四个分量,因为我们可以把投影的影响与实际的 z 坐标解耦,以便我们可以在正交投影和透视投影之间切换
视口变换
OpenGL 把归一化设备坐标的 x 和 y 分量映射到屏幕上的一个区域内,这个区域是操作系统预留出来用于显示的,被称为视口(viewport),这些变换后的坐标被称为窗口坐标( window coordinate)
实现三维显示
在顶点数据中加入 z 和 w 分量,近处的点 w 值为 1,越远的 w 的值大,这样确实可以实现三维的显示效果,但不能改变桌子的角度或者缩小、放大。要实现这些变换,不能使用硬编码指定 w 的值,而要用矩阵来生成这些值,用透视投影矩阵自动生成w的值
透视投影矩阵
透视投影矩阵需要和透视除法一起发挥作用。投影矩阵不能自己做透视除法,而透视除法需要 w 才能起作用,因此,透视投影矩阵最重要的任务就是为 w 产生正确的值
能实现这些的方法之一是利用 z 分量,把它作为物体与焦点(观察点)的距离并且把这个距离映射到w,这个距离越大,w值越大,所得物体越小
示意图
可以看出,相比与正交投影矩阵,透视投影矩阵是一个更加通用的投影矩阵,可以同时对宽高比和视野进行调整,因此,使用透视投影矩阵,就可以去掉正交投影矩阵了
创建透视投影矩阵
Matrix.perspectiveM()方法可以创建一个透视投影矩阵,其原型如下
public static void perspectiveM(float[] m, int offset,
float fovy, float aspect, float zNear, float zFar) {
参数说明
参数 | 说明 |
---|---|
m | 最终生成的透视投影矩阵数组,长度至少有16个元素,才能存储透视投影矩阵 |
offset | 正交投影矩阵起始的偏移值 |
fovy | y 方向的视野,以度为单位 |
aspect | 屏幕的宽高比,它等于宽度/高度 |
zNear | 到近处平面的距离,必须是正值。比如,如果此值被设为1,那近处平面就位于一个z值为-1处 |
zFar | 到远处平面的距离,必须是正值且大于到近处平面的距离 |
也可以自己实现透视矩阵,透视矩阵最终的数值如下
a:如果我们想象一个相机拍摄的场景,这个变量就代表那个相机的焦距。焦距是由1/tan(视野/2) 计算得到的。这个视野必须小于180度。比如,一个90度的视野,它的焦距会被设置为1/tan(90° /2),也就是1/1或者1
其他的和Matrix.perspectiveM()方法参数含义一致
自己实现透视矩阵代码
public class MatrixHelper {
/**
* @param matrix 矩阵
* @param yFovInDegrees 视野角度
* @param aspect 宽高比
* @param n 近处到平面距离
* @param f 远处到平面距离
* @return
*/
public static float[] perspectiveM(float[] matrix, float yFovInDegrees, float aspect, float n, float f) {
// 计算焦距
float angleInRadians = (float) (yFovInDegrees * Math.PI / 180.0);
float a = (float) (1f / tan(angleInRadians / 2));
// 输出矩阵
for (int i = 0; i < matrix.length; i++) {
switch (i) {
case 0:
matrix[i] = a / aspect;
break;
case 5:
matrix[i] = a;
break;
case 10:
matrix[i] = -((f + n) / (f - n));
break;
case 11:
matrix[i] = -1f;
break;
case 14:
matrix[i] = -((2 * f * n) / (f - n));
break;
default:
matrix[i] = 0f;
break;
}
}
return matrix;
}
}
使用透视矩阵实现三维效果
// 用45度的视野创建一个透视投影。这个视椎体从z值为-1的位置开始,在z值为-10的位置结束
Matrix.perspectiveM(projectionMatrix, 0, 45, (float) width / height, 1, 10);
使用上面的代码替换之前的正交投影矩阵,运行程序,会发现图像不见了,因为没有给顶点指定 z 的位置,默认情况下它 z 在 0 的位置。而这个视椎体是从z值为-1的位置开始的,除非把它移到那个距离内,否则我们无法看见图像
无法创建包含 z 位置为 0 的四椎体,因为 Matrix.perspectiveM 方法要求 zNear 和 zFar 都必须是正数
因此,在使用投影矩阵进行投影之前,需要使用一个平移矩阵把顶点移到可见的位置。通常把这个矩阵称为模型矩阵(model matrix)
模型矩阵
把模型矩阵设为单位矩阵,再沿着z轴平移 -2,当我们把顶点的坐标与这个矩阵相乘的时候,那些坐标最终会沿着z轴负方向移动2个单位,同时,如果只是平移的话,看不到三维效果,因为每个顶点都平移了,因此还需要旋转一下
Matrix.setIdentityM(modelMatrix, 0);
// 沿着z轴平移 -2
Matrix.translateM(modelMatrix, 0, 0f, 0f, -3f);
// 沿着 x 轴旋转45度
Matrix.rotateM(modelMatrix, 0, -45f, 1f, 0f, 0f);
可以把模型矩阵与投影矩阵相乘,得到一个最终矩阵,然后把这个矩阵传递给顶点着色器,这种方式可以在着色器中仅保留一个矩阵
// 设为单位矩阵
Matrix.setIdentityM(modelMatrix, 0);
// 沿着z轴平移 -2
Matrix.translateM(modelMatrix, 0, 0f, 0f, -3f);
// 沿着 x 轴旋转45度
Matrix.rotateM(modelMatrix, 0, -45f, 1f, 0f, 0f);
// 将视图投影矩阵和模型矩阵相乘
float[] temp = new float[16];
Matrix.multiplyMM(temp, 0, projectionMatrix, 0, modelMatrix, 0);
System.arraycopy(temp, 0, projectionMatrix, 0, temp.length);
不论什么时候把两个矩阵相乘,都需要一个临时变量来存储其结果。如果尝试直接写入这个结果,这个结果将是未定义的
修改渲染器
修改渲染器中的代码,去掉正交投影矩阵,加上模型矩阵和视图投影矩阵的使用
public class MyRenderer implements GLSurfaceView.Renderer {
private final FloatBuffer mVertexBuffer;
private Context mContext;
private int a_color;
private int a_position;
private int u_matrix;
private float[] projectionMatrix = new float[16];
private float[] modelMatrix = new float[16];
public MyRenderer(Context context) {
mContext = context;
float[] vertex = new float[]{
0f, 0f, 1f, 1f, 1f,
-0.5f, -0.8f, 0f, 0f, 0f,
0.5f, -0.8f, 0f, 0f, 0f,
0.5f, 0.8f, 0f, 0f, 0f,
-0.5f, 0.8f, 0f, 0f, 0f,
-0.5f, -0.8f, 0f, 0f, 0f,
-0.5f, 0f, 1f, 0f, 0f,
0.5f, 0f, 1f, 0f, 0f,
0f, -0.5f, 0f, 1f, 0f,
0f, 0.5f, 0f, 0f, 1f
};
mVertexBuffer = ByteBuffer.allocateDirect(vertex.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertex);
mVertexBuffer.position(0);
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
glClearColor(1f, 1f, 1f, 1f);
int vertexShader = ShaderHelper.compileVertexShader(mContext, R.raw.ratio3_vertex);
int fragmentShader = ShaderHelper.compileFragmentShader(mContext, R.raw.ratio3_fragment);
int program = ProgramHelper.getProgram(vertexShader, fragmentShader);
glUseProgram(program);
a_color = glGetAttribLocation(program, "a_Color");
a_position = glGetAttribLocation(program, "a_Position");
u_matrix = glGetUniformLocation(program, "u_Matrix");
mVertexBuffer.position(0);
glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GL_FLOAT, false, STRIDE, mVertexBuffer);
glEnableVertexAttribArray(a_position);
mVertexBuffer.position(POSITION_COMPONENT_COUNT);
glVertexAttribPointer(a_color, COLOR_COMPONENT_COUNT, GL_FLOAT, false, STRIDE, mVertexBuffer);
glEnableVertexAttribArray(a_color);
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
glViewport(0, 0, width, height);
// 用45度的视野创建一个透视投影。这个视椎体从z值为-1的位置开始,在z值为-10的位置结束
Matrix.perspectiveM(projectionMatrix, 0, 45, (float) width / height, 1, 10);
// 设为单位矩阵
Matrix.setIdentityM(modelMatrix, 0);
// 沿着z轴平移 -2
Matrix.translateM(modelMatrix, 0, 0f, 0f, -3f);
// 沿着 x 轴旋转45度
Matrix.rotateM(modelMatrix, 0, -45f, 1f, 0f, 0f);
// 将视图投影矩阵和模型矩阵相乘
float[] temp = new float[16];
Matrix.multiplyMM(temp, 0, projectionMatrix, 0, modelMatrix, 0);
System.arraycopy(temp, 0, projectionMatrix, 0, temp.length);
}
@Override
public void onDrawFrame(GL10 gl10) {
glClear(GL_COLOR_BUFFER_BIT);
glUniformMatrix4fv(u_matrix, 1, false, projectionMatrix, 0);
glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
glDrawArrays(GL_LINES, 6, 2);
glDrawArrays(GL_POINTS, 8, 1);
glDrawArrays(GL_POINTS, 9, 1);
}
}