概述
研究android8.1 HWUI源码的过程中,发现OpenGL是绕不过的一个知识点,不理解OpenGL的绘制基础,必然无法很好的理解Hwui基本原理,同时熟悉OpenGL之后,HWUI也是一个非常优秀的OpenGL 2D渲染的代码,本文将介绍一下OpenGL绘制图形的重要原理,为学习HWUI源码扫清障碍,本文我们将以会一直一个白色的矩形为例,结合代码实践加以说明。
OpenGL的向量矩阵
向量
OpenGL中,一个点(位置)通过一个向量表示,通常有4个元素:x,y,z,w,z代表深度,w代表距离,下面讲解透视矩阵的时候讲解。如果不指定z的值,其默认取值为0;如果不指定w的值,其默认值为1.
矩阵
OpenGL中的矩阵式4*4矩阵,要特别注意OpenGL中的矩阵Matrix是按列优先存储在内存中的。接下来我们看一个平移矩阵A
1 0 0 Xtransition
0 1 0 Ytransition
0 0 1 Ztransition
0 0 0 1
假设Xtransition = Ytransition =100,Ztransition = 0,那么这样得到矩阵A,A的作用是平移x ,y坐标100个位置。我们有一个坐标点B(2,2),这个坐标点z和w值没有指定,其默认值分别是,0和1,所以按照四维向量来看该点是B(2,2,0,1),那么使用矩阵A平移坐标点B(2,2,0,1)相当于A * B = (102, 102, 0 , 1),正是我们需要的。
注意:矩阵相乘满足结合率(A*B)*C = A*(B*C);不满足交换律
OpenGL渲染显示的pipeline
我们都知道OpenGL绘制最终是将一个个的点绘制成图,对于入门者来说难以理解的就是MVP矩阵,即model模型矩阵,View视图矩阵,projection投影矩阵。分析这三个矩阵之前我们先介绍下OpenGL显示渲染管线:
输入坐标-->mvp矩阵变换-->gl_Position--->透视除法---->归一化坐标--->视口变换--->窗口坐标。
注意,这里gl_Position就是shader里面的那个着色器的原始的gl_Position坐标。往往着色器代码会这写,其中a_Position就是我们调用openGL接口输入的顶点坐标。
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main()
{
v_Color = a_Color;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * a_Position;
}
正交投影矩阵
正交投影矩阵是重要的投影矩阵之一,2D图形渲染中经常使用,比如android的hwui渲染引擎就使用正交投影,与之相对的是透视投影矩阵,本小节讲解正交投影矩阵。
要理解上面的整个工作流程最好先从渲染的最后即窗口坐标这里反向分析,假设mvp矩阵都没有设置,且计算的坐标点的w分量我们都假设是1(这样透视除法就可以忽略),屏幕的分辨率是width x height = 1280 * 800,我们在屏幕右上角绘制一个1/4屏幕大小的矩形,由于此时没有矩阵作用于a_Position,我们程序使用的vertex坐标就是归一化的坐标,即[-1,1]之间。这里涉及一个概念是:裁剪空间。当着色器把一个值写入gl_Position的时候,OpenGL期望这个位置是在裁剪空间(clip space)中的。剪裁空间背后的逻辑非常简单:对于任何给定的位置,它的x,y以及z都要在那个位置的-w和w之间。由于默认我们没有指定坐标点w分量,其默认值是1,所以要求x, y,z都在【-1,1】之间,这个范围之外的任何事物在屏幕不可见。
根据上面的分析我们要在屏幕中央绘制矩形给出的表做点如下:
float[] vertexColorTriangles = { //演示HWUI渲染的坐标 0f, 0f, 0f, 1f, 1f, 0f, 0f, 1f, 0f, 1f, 0f, 1f, 1f, 1f, 0f, 1f, };
为什么要引入正交投影矩阵
这样绘制有什么问题吗?我想有几个方面的问题:
1. 缩放比例:我们输入的坐标明明是一个长度=1的正方向的坐标,但是实际上显示的却是一个非正方形的矩形。这个点不是很友好,因为我们实际编程的时候拿到的需求可能就是一个屏幕的特定位置绘制一个正方形,最终输入坐标是矩形,绘制结果确实长方形。
2.限制输入坐标是[-1,-1]
由于没有使用任何的矩阵变换,受限于w=1的裁剪空间,输入的坐标a_Position必须是在[-1,1],这显然不方便编程,因为我们编程实现的时候,往往不想关心【-1,-1】范围内坐标和窗口坐标的映射关系,而是直接使用屏幕坐标,比如想在屏幕左上角(100,100)位置开始绘制一个宽高等于200的正方体,如果能直接输入的坐标是(200,200)编程就很方便了怎么实现上面的目标?:
正交投影矩阵可以解决上面的两个问题。
正交投影矩阵的代码编程
shader代码:
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main()
{
v_Color = a_Color;
gl_Position = u_Matrix * a_Position;
gl_PointSize = 10.0;
}
OpenGL顶点和矩阵设置以及绘制代码:
//设置绘制顶点
vertexColorTriangles = new float[]{
//演示只使用正交投影,不使用其他投影矩阵的情况
100f, 100f, 0f, 1f, 0.5f, 0.5f, 0.5f,
100f, 300f, 0f, 1f, 0.5f, 0.5f, 0.5f,
300f, 100f, 0f, 1f, 0.5f, 0.5f, 0.5f,
300f, 300f, 0f, 1f, 0.5f, 0.5f, 0.5f,
};
//正交投影矩阵,width:屏幕宽度,height:屏幕高度,当前环境下分别等于1280和800
使用openGL Matrix.java 中的orthoM函数:
orthoM(orthoProjectionMatrix, 0, 0f, width, height, 0, -1f, 1f);
//模型矩阵和视图矩阵都设置成单位矩阵
setIdentityM(modelMatrix, 0);
setIdentityM(viewMatrix, 0);
//计算最终的mvp矩阵
multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
multiplyMM(mvpMatrix, 0, orthoProjectionMatrix, 0, modelViewMatrix, 0);
//绘制代码,由于视图矩阵和模型矩阵都设置成单位矩阵,最终的mvpMatrix就等于orthoProjectionMatrix正交投影矩阵
glClear(GL_COLOR_BUFFER_BIT);
glUniformMatrix4fv(uMatrixLocation, 1, false, mvpMatrix, 0);
// Draw
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
绘制的效果:在屏幕左上角(100,100)位置绘制了一个宽高等于200的正方形。
注意:上面orthoM函数生成正交矩阵时候我们注意到y的分量设置: