OpenGL ES的绘制需要有以下步骤:
开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。
OpenGL是一个3D图形库,所以我们在OpenGL中所指定的所有坐标都是3D坐标(xyz)。OpenGL并不是简单地把所有的3D坐标变换成屏幕上的2D像素。
OpenGL仅当3D坐标在3个轴(xyz)上都为-1.0到1.0的范围内才去处理它。
所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。
由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。
这里会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。
顶点着色器接着会处理我们在内存中指定数量的顶点。
我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。
使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。
从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
顶点缓冲对象是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象。
着色器都是使用GLSL语言来写的,而GLSL不同版本之间会有差异。
#version 300 es
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
每个着色器都起始于一个版本声明,这里声明的是GLSL ES 300版本,在Android中它对应的OpenGL ES版本为3.0,而GLSL ES 100版本则对应的是OpenGL ES 2.0版本。如果不写版本默认的就是100。
下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。
GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。
由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量position。
我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)。
layout(location = 0);
用于指定着色器中的变量 position 与 OpenGL程序中顶点数组中数据的关联关系,这里表示该位置数据的位置索引为0。以确保在着色器中能正常的接收输入的数据。
你后面会看到为什么我们会需要这个位置值。
layout限定符是从OpenGL ES 3.0开始出现的,其主要用于设置变量的存储索引(即引用)值.
在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。
在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。
注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。
为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数中只是将position的值转换后赋值给gl_Position。
由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。
这个position的值是哪里进行赋值的呢?
是通过后面java代码中的draw函数来进行赋值的。
每个顶点着色器都必须在gl_Position变量中输出一个位置。这个变量定义传递给管线下一个阶段的位置。
写完顶点着色器后,为了能让OpenGL使用它,我们必须在运行时动态编译它。
片段着色器全是关于计算你的像素最后的颜色输出。颜色使用RGBA。
#version 300 es
out vec4 color;
void main() {
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。
当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。
这三种颜色分量的不同调配可以生成超过1600万种不同的颜色!
片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。
我们可以用out关键字声明输出变量,这里我们命名为color。
下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。
之后也是需要编译着色器。片段着色器声明的这个输出变量color的值会被输出到颜色缓冲区,然后颜色缓冲区再通过EGL窗口显示。
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。
如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。
已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。
当输出和输入不匹配的时候,你会得到一个连接错误。我们需要把之前编译的着色器附加到程序队形上,然后使用glLinkProgram链接他们:
final int shaderProgramId = GLES30.glCreateProgram();
GLES30.glAttachShader(shaderProgramId, vertexShaderId);
GLES30.glAttachShader(shaderProgramId, fragmentShaderId);
GLES30.glLinkProgram(shaderProgramId);
链接完后需要使用glUseProgram方法,用刚创建的程序对象的id作为参数,以激活这个程序对象。
调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。
对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:
GLES30.glDeleteShader(vertexShaderId);
GLES30.glDeleteShader(fragmentShaderId);
现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。
就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。下面我们需要告诉OpenGL怎么做。
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据会被解析成下面的样子:
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
- 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
// glVertexAttribPointer是把顶点位置属性赋值给着色器程序
// 第一个参数0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。
// 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了
// 第二个size是3,是因为上面我们triangleCoords声明的属性就是3位,xyz
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * 4, vertexBuffer);
//启用顶点变量,参数为数据中第一个值在缓冲区开始的位置。 这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的
GLES30.glEnableVertexAttribArray(0);
glEnableVertexAttribArray调用后允许顶点着色器读取句柄对应的GPU数据。默认情况下,出于性能考虑,所有顶点着色器的attribute变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU.由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的attribute数据。glVertexAttribPointer或VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU数据。
glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们:
- 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用
layout(location = 0)
定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0
。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。 - 第二个参数指定顶点属性的大小。顶点属性是一个
vec3
,它由3个值组成,所以大小是3。 - 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中
vec*
都是由浮点数值组成的)。 - 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个
GLfloat
之后,我们把步长设置为3 * sizeof(GLfloat)
也就是3*4。 要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。 一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 - 最后一个参数的类型是数据。
每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。
由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性现在会链接到它的顶点数据。
现在我们已经定义了OpenGL该如何解释顶点数据,接着应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
顶点属性默认是禁用的。
自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲区中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。
在OpenGL中绘制一个物体,代码会像是这样:
// 0. 复制顶点数组到缓冲中供OpenGL使用,使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
IntBuffer vbo = IntBuffer.allocate(1);
GLES30.glGenBuffers(1, vbo);
// 使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0));
// glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数
// 它的第一个参数是目标缓冲类型。 第二个参数是指定传输数据的大小(以字节为单位),用一个简单的sizeof计算出顶点数据的大小就行
// 第三个参数是我们希望发送的实际数据
// 第四个参数指定了我们希望显卡如何管理这些数据,有三种形式: GL_STATIC_DRAW(数据不会或几乎不会改变), GL_DYNAMIC_DRAW(数据会被改变很多), GL_STREAM_DRAW(数据每次绘制时都会改变)
GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, POSITION_VERTEX.size * Float.BYTES,
vertexBuffer, GLES30.GL_STATIC_DRAW);
// 1. 设置顶点属性指针
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * Float.BYTES, vertexBuffer);
GLES30.glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
GLES30.glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。
绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢?
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。
这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。
这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。
一个顶点数组对象会储存以下这些内容:
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
创建一个VAO和创建一个VBO很类似:
IntBuffer arrays = IntBuffer.allocate(1);
GLES30.glGenVertexArrays(1, arrays);
要想使用VAO,要做的只是使用glBindVertexArray绑定VAO(glBindVertexArrays()命令的目的是将指定的VAO标记为活跃,这样生成的缓冲区就会和这个VAO相关联)。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。
当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样:
VAO小助理偷偷帮我们记录一切:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
GLES30.glBindVertexArray(arrays.get(0));
// 2. 把顶点数组复制到缓冲中供OpenGL使用。 注意: 这里绑定了VAO后再绑定VBO,这样他们才能关联起来
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0));
GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, Float.BYTES, vertices, GLES30.GL_STATIC_DRAW);
// 3. 设置顶点属性指针
GLES30.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * Float.BYTES, vertexBuffer);
GLES30.glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
GLES30.glUseProgram(shaderProgram);
// 解绑数据:GLES30.glBindVertexArray(0)
GLES30.glBindVertexArray(0);
someOpenGLFunctionThatDrawsOurTriangle();
就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。
一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。
注意: 先绑定VAO后再绑定VBO,这样他们才能关联起来当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。
当目前是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用(这也意味着它也会存储解绑调用)。
当时VAO不会存储GL_ARRAY_BUFFER(VBO)的glBindBuffer的函数调用。
下面需要实现GLSurfaceView.Render接口,实现要绘制的部分:
- 用EGL创建屏幕上的渲染表面(GLSurfaceView内部实现)
- 写顶点着色器和片段着色器文件。
- 加载编译顶点着色器和片段着色器。
- 确定需要绘制图形的坐标和颜色数据。
- 创建program对象,连接顶点和片断着色器,将坐标数据、颜色数据传到OpenGL ES程序中。
- 设置视图窗口(viewport)。
- 使颜色缓冲区的内容显示到EGL窗口上。
编写着色器需要用到GLSL,但是在Studio中默认是不支持关键字高亮和智能提示的,所以需要先安装插件。
Preferences -> Plugins -> 搜GLSL Support安装就可以了。
安装完GLSL插件后,就可以开始了。一般将GLSL文件放到raw或assets目录,我们这里在raw目录上右键New,然后选择GLSL Shader创建就可以了,创建后默认会生成一个main()函数。
- 顶点着色器(triangle_vertex_shader.glsl)
关键字in意思是输入(input),表示这个顶点属性将会从缓冲区(C++或Java代码中的VBO、VAO等)中接收数值。 顶点属性还可以改为被声明为out,这意味着它们会将值发送到管线中的下一个阶段。
layout(location=0)称为layout修饰符。也就是我们把顶点属性和特定缓冲区关联起来的方法,这意味着这个顶点属性的识别号是0.
// 声明着色器的版本
#version 300 es
// 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。
layout (location = 0) in vec4 vPosition;
// 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。
layout (location = 1) in vec4 aColor;
// 输出一个名为vColor的4分量向量,后面输入到片段着色器中。
out vec4 vColor;
void main() {
// gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition
gl_Position = vPosition;
// gl_PointSize为Shader内置变量,为点的直径
gl_PointSize = 10.0;
// 将输入数据aColor拷贝到vColor的变量中。
vColor = aColor;
}
大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition。
然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor。
另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出给vColor。
-
片段着色器(triangle_fragment_shader.glsl)
// 声明着色器的版本 #version 300 es // 声明着色器中浮点变量的默认精度 precision mediump float; // 声明一个输入名为vColor的4分向量,来自上面的顶点着色器 in vec4 vColor; // 声明一个4分向量的输出变量fragColor out vec4 fragColor; void main() { // 将输入的颜色值数据拷贝到fragColor变量中,输出到颜色缓冲区 fragColor = vColor; }
precision是精度限定符,它可以使着色器创作者指定着色器变量的计算精度。变量可以声明为低、中或高精度。
这些限定符用于提示编译器允许在较低的范围和精度上执行变量的计算。
在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。
当然这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。
精度限定符可以用于指定任何浮点数或整数的变量的精度。指定精度的关键字是lowp、mediump和highp。下面是一些带有精度限定符的声明示例:
highp vec4 position;
mediump float specularExp;
除了精度限定符之外,还有默认精度的概念。也就是说,如果变量声明时没有使用精度限定符,它将拥有该类型的默认精度。默认精度限定符在顶点或片段着色器的开头用以下语法指定:
precision highp float;
precision mediump int;
为float类型指定的精度将用作所有基于浮点值变量的默认精度。同样,为int指定的精度将用作所有基于整数的变量的默认精度。
在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都是highp。
也就是说,顶点着色器中所有没有精度限定符声明的变量都使用最高的精度。
片段着色器的规则与此不同。在片段着色器中,浮点值没有默认的精度值:每个着色器必须声明一个默认的float精度,或者为每个float变量指定精度。
public class TriangleActivity extends Activity {
private GLSurfaceView mGlSurfaceView;
private TriangleRender mTriangleRender;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_triangle);
mGlSurfaceView = findViewById(R.id.mGLSurfaceView);
// OpenGL ES 3.0版本
mGlSurfaceView.setEGLContextClientVersion(3);
mTriangleRender = new TriangleRender();
mGlSurfaceView.setRenderer(mTriangleRender);
mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
@Override
protected void onPause() {
super.onPause();
mGlSurfaceView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mGlSurfaceView.onResume();
}
}
TriangleRender的实现如下:
public class TriangleRender implements GLSurfaceView.Renderer {
//三个顶点
private static final int POSITION_COMPONENT_COUNT = 3;
//顶点位置缓存
private final FloatBuffer vertexBuffer;
//顶点颜色缓存
private final FloatBuffer colorBuffer;
//渲染程序
private int mProgram;
/*****************1.声明绘制图形的坐标和颜色数据 start**************/
//三个顶点的位置参数
private float triangleCoords[] = {
0.5f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
};
//三个顶点的颜色参数
private float color[] = {
1.0f, 0.0f, 0.0f, 1.0f,// top
0.0f, 1.0f, 0.0f, 1.0f,// bottom left
0.0f, 0.0f, 1.0f, 1.0f// bottom right
};
/*****************1.声明绘制图形的坐标和颜色数据 end**************/
public TriangleRender() {
/****************2.为顶点位置及颜色申请本地内存 start************/
//将顶点数据拷贝映射到native内存中,以便OpenGL能够访问
//分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * Float.BYTES) // 直接分配native内存
.order(ByteOrder.nativeOrder()) // 和本地平台保持一致的字节序
.asFloatBuffer(); // 将底层字节映射到FloatBuffer实例,方便使用
vertexBuffer.put(triangleCoords); // 将顶点数据拷贝到native内存中
// 将数组数据put进buffer之后,指针并不是在首位,所以一定要position到0,至关重要!否则会有很多奇妙的错误!将缓冲区的指针移动到头部,保证数据是从最开始处读取
vertexBuffer.position(0);
//顶点颜色相关
colorBuffer = ByteBuffer.allocateDirect(color.length * Float.BYTES)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
colorBuffer.put(color);
colorBuffer.position(0);
/****************2.为顶点位置及颜色申请本地内存 end************/
}
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
//将背景设置为白色
GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
/******************3.加载编译顶点着色器和片段着色器 start**********/
//编译顶点着色程序
String vertexShaderStr = readResource(MyApplication.getInstance(), R.raw.triangle_vertex_shader);
int vertexShaderId = compileVertexShader(vertexShaderStr);
//编译片段着色程序
String fragmentShaderStr = readResource(MyApplication.getInstance(), R.raw.triangle_fragment_shader);
int fragmentShaderId = compileFragmentShader(fragmentShaderStr);
/******************3.加载编译顶点着色器和片段着色器 end**********/
/******************4.创建program,连接顶点和片段着色器并链接program start***********/
//连接程序
mProgram = linkProgram(vertexShaderId, fragmentShaderId);
/******************4.创建program,连接顶点和片段着色器并链接program end***********/
//在OpenGLES环境中使用程序
GLES30.glUseProgram(mProgram);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
/*********5.设置绘制窗口********/
GLES30.glViewport(0, 0, width, height);
}
public void onDrawFrame(GL10 unused) {
/**********6.绘制************/
//把颜色缓冲区设置为我们预设的颜色,绘图设计到多种缓冲区类型:颜色、深度和模板。这里只是向颜色缓冲区中绘制图形
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// glVertexAttribPointer是把顶点位置属性赋值给着色器程序
// 0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。
// 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了
// 第二个size是3,是因为上面我们triangleCoords声明的属性就是3位,xyz
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * Float.BYTES, vertexBuffer);
//启用顶点变量,这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的
// 也就是说由于vPosition在着色器中的位置被指定为0,因此可以简单的通过glVertexAttribPointer()函数调用中的第一个参数和在glEnableVertexAttribArray()函数调用中使用0来引用此变量
GLES30.glEnableVertexAttribArray(0);
//准备颜色数据
/**
* glVertexAttribPointer()方法的参数上面的也说过了,这里再按照这个场景说一下分别为:
* index:顶点属性的索引.(这里我们的顶点位置和颜色向量在着色器中分别为0和1)layout (location = 0) in vec4 vPosition; layout (location = 1) in vec4 aColor;
* size: 指定每个通用顶点属性的元素个数。必须是1、2、3、4。此外,glvertexattribpointer接受符号常量gl_bgra。初始值为4(也就是涉及颜色的时候必为4)。
* type:属性的元素类型。(上面都是Float所以使用GLES30.GL_FLOAT);
* normalized:转换的时候是否要经过规范化,true:是;false:直接转化;
* stride:跨距,默认是0。(由于我们将顶点位置和颜色数据分别存放没写在一个数组中,所以使用默认值0)
* ptr: 本地数据缓存(这里我们的是顶点的位置和颜色数据)。
*/
// 1是aColor在属性的位置,4是因为我们声明的颜色是4位,r、g、b、a。
GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 4 * Float.BYTES, colorBuffer);
//启用顶点颜色句柄
GLES30.glEnableVertexAttribArray(1);
//绘制三个点
// GLES30.glDrawArrays(GLES30.GL_POINTS, 0, POSITION_COMPONENT_COUNT);
//绘制三条线
// GLES30.glLineWidth(3);//设置线宽
// GLES30.glDrawArrays(GLES30.GL_LINE_LOOP, 0, POSITION_COMPONENT_COUNT);
//绘制三角形
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, POSITION_COMPONENT_COUNT);
//禁止顶点数组的句柄
GLES30.glDisableVertexAttribArray(0);
GLES30.glDisableVertexAttribArray(1);
}
/**
* 编译顶点着色器
*
* @param shaderCode
*/
public static int compileVertexShader(String shaderCode) {
return compileShader(GLES30.GL_VERTEX_SHADER, shaderCode);
}
/**
* 编译片段着色器
*
* @param shaderCode
*/
public static int compileFragmentShader(String shaderCode) {
return compileShader(GLES30.GL_FRAGMENT_SHADER, shaderCode);
}
/**
* 加载并编译着色器代码
*
* @param type 顶点着色器:GLES30.GL_VERTEX_SHADER
* 片段着色器:GLES30.GL_FRAGMENT_SHADER
* @param shaderCode
*/
private static int compileShader(int type, String shaderCode) {
//传入渲染器类型参数的type,创建一个对应的着色器对象
final int shaderId = GLES30.glCreateShader(type);
if (shaderId != 0) {
// 传入着色器对象和字符串shaderCode定义的源代码,将二者关联起来
GLES30.glShaderSource(shaderId, shaderCode);
// 传入着色器对象,并对其进行编译
GLES30.glCompileShader(shaderId);
//检测状态
final int[] compileStatus = new int[1];
GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
String logInfo = GLES30.glGetShaderInfoLog(shaderId);
System.err.println(logInfo);
//创建失败
GLES30.glDeleteShader(shaderId);
return 0;
}
return shaderId;
} else {
//创建失败
return 0;
}
}
/**
* 链接小程序
*
* @param vertexShaderId 顶点着色器
* @param fragmentShaderId 片段着色器
*/
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
//创建一个空的OpenGLES程序
final int programId = GLES30.glCreateProgram();
if (programId != 0) {
//将顶点着色器加入到程序
GLES30.glAttachShader(programId, vertexShaderId);
//将片元着色器加入到程序中
GLES30.glAttachShader(programId, fragmentShaderId);
//链接着色器程序
GLES30.glLinkProgram(programId);
// 删除
GLES30.glDeleteShader(vertexShaderId);
GLES30.glDeleteShader(fragmentShaderId);
final int[] linkStatus = new int[1];
GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
String logInfo = GLES30.glGetProgramInfoLog(programId);
System.err.println(logInfo);
GLES30.glDeleteProgram(programId);
return 0;
}
return programId;
} else {
//创建失败
return 0;
}
}
/**
* 验证程序片段是否有效
*
* @param programObjectId
*/
public static boolean validProgram(int programObjectId) {
GLES30.glValidateProgram(programObjectId);
final int[] programStatus = new int[1];
GLES30.glGetProgramiv(programObjectId, GLES30.GL_VALIDATE_STATUS, programStatus, 0);
return programStatus[0] != 0;
}
/**
* 读取资源
*
* @param resourceId
*/
public static String readResource(Context context, int resourceId) {
StringBuilder builder = new StringBuilder();
try {
InputStream inputStream = context.getApplicationContext().getResources().openRawResource(resourceId);
InputStreamReader streamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(streamReader);
String textLine;
while ((textLine = bufferedReader.readLine()) != null) {
builder.append(textLine);
builder.append("\n");
}
} catch (IOException e) {
e.printStackTrace();
} catch (Resources.NotFoundException e) {
e.printStackTrace();
}
return builder.toString();
}
}
调用glCreateShader将根据传入的type参数创建一个新的顶点或片段着色器。返回值是指向新着色器对象的句柄。当完成链接着色器对象时,可以用glDeleteShader删除。
注意,如果一个着色器连接到一个程序对象,那么调用glDeleteShader不会立即删除着色器,而是将着色器编标记为删除,在着色器不再连接到任何程序对象时,它的内存将被释放。
效果如下:
我们设置的是数据来看,应该是等腰三角形,但是实际效果并不是,这是因为OpenGL假设屏幕采用均匀的方形坐标系,所以在把标准坐标系下的坐标绘制到非方形的屏幕上时,就会出现拉伸。 如果想让绘制一个等腰三角形该怎么做呢?
打个比方现在屏幕的宽高比是1:2,那上面的话的三角形的高度就是宽度的2,我们只需要通过换算把高度变成现在的高度 * 1/2 就可以得到等腰三角形了。
这里就牵扯到了要对坐标向量进行换算,这里的换算需要使用矩阵来进行。总体分为两部分:
- 如何获得一个矩阵,可以把坐标范围从【-2,2】换算成【-1,1】的范围内。(提供了Matrix.orthoM来处理矩阵)
- 如何将这个矩阵传递给GLSL中。(与获取顶点索引类似,可以在GLSL中声明一个mat4类型的矩阵变量,获取其索引,再传递值给它)
具有大小和方向的量。它可以形象化的表示为带箭头的线段。箭头代表方向、长度代表大小。
在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。
vec.w分量不是用作表达空间中的位置(因为我们处理的是3D不是4D),而是用在所谓透视划分上。
3D空间中的点可以通过使用形如(2, 8, −3)的符号列出x、y、z的值来表示。
不过,如果用齐次坐标—— 一种在19世纪初首次描述的表示法来表示点会更有用。
在每个点的齐次坐标有4个值,前3个值表示x、y和z,第四个值w总是非零值,通常为1。因此,我们会将之前的点表示为(2, 8, −3, 1)。
用来存储齐次3D坐标的GLSL数据类型是vec4(“vec”代表向量,同时也可以用来表示点)。
由m*n个数按照一定顺序排列成m行n列的矩形数表称为矩阵,而向量则是由n个有序数组成的数组。
所以矩阵中的每一个行可以看做一个行向量,每一列也可以看成一个列向量,所以说向量是矩阵的一部分。
在三维图形学中,一般使用的是4阶矩阵。在DirectX中使用的是行向量,如[xyzw],所以与矩阵相乘时,向量在前矩阵在后。
OpenGL中使用的是列向量,如[xyzx]T,所以与矩阵相乘时,矩阵在前,向量在后,最终通过变换矩阵得到想要的向量。
矩阵是矩形的值的阵列,它的元素通常使用下标访问。第一个下标表示行号,第二个下标表示列号,下标从0开始。我们在3D图形计算中要用到的矩阵尺寸常为4×4。
GLSL中的mat4数据类型用来存储4×4矩阵。
单位矩阵中一条对角线的值为1,其余值全为0。任何点或矩阵乘单位矩阵都不会改变。在GLM中,调用构造函数glm::mat4 m(1.0f)可以在变量m中生成单位矩阵。
在图形学中,矩阵通常用来进行物体的变换。例如矩阵可以用来将点从一处移动到另一处。
矩阵变换:
-
平移矩阵
-
缩放矩阵 缩放矩阵除了放大缩小之外,还可以用来切换坐标系。
例如,我们可以用缩放来在给定右手坐标系的情况下确定左手坐标系中的坐标。 通过反转Z坐标就可以在右手坐标系和左手坐标系中切换。 -
旋转矩阵
数学家欧拉表明,围绕任何轴的旋转都可以表示为绕x轴、y轴、z轴旋转的组合。围绕这3个轴的旋转角度被称为欧拉角。
这个被称为欧拉定理的发现对我们很有用,因为对于每个坐标轴的旋转可以用矩阵变换来表示。
实践中,当在3D空间中旋转轴不穿过原点时,物体使用欧拉角进行旋转需要几个额外的步骤,一般有:
- 平移旋转轴以使它经过原点
- 绕x轴、y轴、z轴旋转适当的欧拉角
- 服用步骤1中的平移
- 投影矩阵
- LookAt矩阵
变换矩阵的重要特性之一就是它们都是4x4的矩阵。这是因为我们决定使用齐次坐标系。 否则,各变换矩阵可能会有不同的纬度并且无法想乘。
这里的相机指的是观察外界的视角并不是我们生活中的相机。这里的相机是指我们从照相机或者摄像机的角度来观察这个世界。 相机对应于OpenGL的世界,决定相机拍摄的结果(最终屏幕上展示的结果),包括相机位置、相机观察方向以及相机的UP方向。
- 相机位置:相机在3D空间里面的坐标点。
- 相机观察方向:相机镜头的朝向,朝前拍、朝后拍、朝左拍、朝右拍。
- 相机UP方向:相机顶端指向的方向,例如斜着拿、反着拿。
Android OpenGL ES程序中,我们可以通过Matrix.setLookAtm来对相机进行设置:
/**
* 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) //up向量在xyz上的分量) {
...
}
相机视角观察到的世界最终要变成平面2D图像展示到屏幕上,这个过程就是投影,从3D到2D的转换。
Android OpenGL ES的投影分为两种:
-
正交投影
物体呈现出来的大小不会随着其距离视点的远近而发生变化。通过Matrix.orthoM()来设置正交投影。
/** * Computes an orthographic projection matrix. * * @param m returns the result * @param mOffset * @param left * @param right * @param bottom * @param top * @param near * @param far */ public static void orthoM(float[] m, //接收正交投影的变换矩阵 int mOffset, //变换矩阵的起始位置(偏移量) float left, //相对观察点近面的左边距 float right,//相对观察点近面的右边距 float bottom, //相对观察点近面的下边距 float top,//相对观察点近面的上边距 float near,//相对观察点近面距离 float far) //相对观察点远面距离{ ... }
-
透视投影
物体离视点越远,呈现出来的越小。离视点越近,呈现出来的越大。通过Matrix.frustumM()来设置透明投影:
/** * Defines a projection matrix in terms of six clip planes. * * @param m the float array that holds the output perspective matrix * @param offset the offset into float array m where the perspective * matrix data is written * @param left * @param right * @param bottom * @param top * @param near * @param far */ public static void frustumM(float[] m, //接收透视投影的变换矩阵 int mOffset, //变换矩阵的起始位置(偏移量) float left,//相对观察点近面的左边距 float right,//相对观察点近面的右边距 float bottom, //相对观察点近面的下边距 float top, //相对观察点近面的上边距 float near, //相对观察点近面距离 float far) //相对观察点远面距离 { ... }
在OpenGL ES中顶点位置信息的表示都是使用的向量,如下每一行是一个顶点的位置向量(x,y,z)。
想要使三角形显示为等腰三角形,就需要在虚拟坐标系中完成对各个顶点位置的变换,也就是对三个向量的变换。而想要实现对向量的变换就要用到变换矩阵。
//三个顶点的位置参数
private float triangleCoords[] = {
0.5f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
};
变换矩阵需要透过相应的相机和投影的操作才能得到,具体方法如下:
Matrix.multiplyMM (float[] result, //接收相乘结果
int resultOffset, //接收矩阵的起始位置(偏移量)
float[] lhs, //左矩阵
int lhsOffset, //左矩阵的起始位置(偏移量)
float[] rhs, //右矩阵
int rhsOffset) //右矩阵的起始位置(偏移量)
也就是说为了解决坐标中宽高不一样的问题,我们可以应用OpenGL正确的比例下通过投影模式和相机视图坐标转换图形对象来完成。
为了应用投影和相机视图,我们创建一个投影矩阵和一个相机视图矩阵,并把他们应用于OpenGL渲染管道中,投影矩阵重新计算你的图形的坐标,使他们正确的映射到Android设备的屏幕,相机视图矩阵创建一个转换,它将从一个特定的位置显示对象。
在上面绘制三角形的基础上进行修改。
将用到的着色器功能以及资源读取glsl的功能封装成工具类:
public class ShaderUtils {
private static final String TAG = "ShaderUtils";
/**
* 编译顶点着色器
* @param shaderCode
*/
public static int compileVertexShader(String shaderCode) {
return compileShader(GLES30.GL_VERTEX_SHADER, shaderCode);
}
/**
* 编译片段着色器
* @param shaderCode
*/
public static int compileFragmentShader(String shaderCode) {
return compileShader(GLES30.GL_FRAGMENT_SHADER, shaderCode);
}
/**
* 编译
* @param type 顶点着色器:GLES30.GL_VERTEX_SHADER
* 片段着色器:GLES30.GL_FRAGMENT_SHADER
* @param shaderCode
*/
private static int compileShader(int type, String shaderCode) {
//创建一个着色器
final int shaderId = GLES30.glCreateShader(type);
if (shaderId != 0) {
GLES30.glShaderSource(shaderId, shaderCode);
GLES30.glCompileShader(shaderId);
//检测状态
final int[] compileStatus = new int[1];
GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
String logInfo = GLES30.glGetShaderInfoLog(shaderId);
System.err.println(logInfo);
//创建失败
GLES30.glDeleteShader(shaderId);
return 0;
}
return shaderId;
} else {
//创建失败
return 0;
}
}
/**
* 链接小程序
* @param vertexShaderId 顶点着色器
* @param fragmentShaderId 片段着色器
*/
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
//创建一个空的OpenGLES程序
final int programId = GLES30.glCreateProgram();
if (programId != 0) {
//将顶点着色器加入到程序
GLES30.glAttachShader(programId, vertexShaderId);
//将片元着色器加入到程序中
GLES30.glAttachShader(programId, fragmentShaderId);
//链接着色器程序
GLES30.glLinkProgram(programId);
//删除
GLES30.glDeleteShader(vertexShaderId);
GLES30.glDeleteShader(fragmentShaderId);
final int[] linkStatus = new int[1];
GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
String logInfo = GLES30.glGetProgramInfoLog(programId);
System.err.println(logInfo);
GLES30.glDeleteProgram(programId);
return 0;
}
return programId;
} else {
//创建失败
return 0;
}
}
/**
* 验证程序片段是否有效
* @param programObjectId
*/
public static boolean validProgram(int programObjectId) {
GLES30.glValidateProgram(programObjectId);
final int[] programStatus = new int[1];
GLES30.glGetProgramiv(programObjectId, GLES30.GL_VALIDATE_STATUS, programStatus, 0);
return programStatus[0] != 0;
}
}
public class ResReadUtils {
/**
* 读取资源
* @param resourceId
*/
public static String readResource(@NonNull Context context, @RawRes int resourceId) {
StringBuilder builder = new StringBuilder();
try {
InputStream inputStream = context.getResources().openRawResource(resourceId);
InputStreamReader streamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(streamReader);
String textLine;
while ((textLine = bufferedReader.readLine()) != null) {
builder.append(textLine);
builder.append("\n");
}
} catch (IOException e) {
e.printStackTrace();
} catch (Resources.NotFoundException e) {
e.printStackTrace();
}
return builder.toString();
}
}
- 修改顶点着色器,增加矩阵变换(修改iso_triangle_vertex_shader.glsl),片段着色器不用修改
#version 300 es
// 声明着色器的版本,OpenGL ES 3.0版本对应的着色器语言版本是 GLSL 300 ES
// 顶点着色器的顶点位置,输入一个名为vPosition的3分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。
layout (location = 0) in vec3 vPosition;
// 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。
layout (location = 1) in vec4 aColor;
// 输出一个名为vColor的4分量向量,后面输入到片段着色器中。
out vec4 vColor;
// 变换矩阵4*4
uniform mat4 u_Matrix;
void main() {
// 先把vec3转成vec4
vec4 pos = vec4(vPosition.x, vPosition.y, vPosition.z, 1.0);
// gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition
gl_Position = u_Matrix * pos;
// gl_PointSize为Shader内置变量,为点的直径
gl_PointSize = 10.0;
// 将输入数据aColor拷贝到vColor的变量中。
vColor = aColor;
}
-
在GLSurfaceView.Render实现类中定义矩阵变量
-
把变换矩阵设置给顶点渲染器
其他所有代码都和上面的一样,只是在Renderer的实现类中增加对转换矩阵的部分,这里我们使用GLTextureView来实现:
public class IsoTriangleActivity extends Activity {
private GLTextureView mGLTextureView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_iso_triangle);
mGLTextureView = findViewById(R.id.mGLTextureView);
IsoTriangleRender render = new IsoTriangleRender();
mGLTextureView.setEGLContextClientVersion(3);
mGLTextureView.setRenderer(render);
mGLTextureView.setRenderMode(GLTextureView.RENDERMODE_WHEN_DIRTY);
}
@Override
protected void onPause() {
super.onPause();
mGLTextureView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mGLTextureView.onResume();
}
}
IsoTriangleRender的实现如下:
public class IsoTriangleRender implements GLTextureView.Renderer {
//一个Float占用4Byte
private static final int BYTES_PER_FLOAT = 4;
//三个顶点
private static final int POSITION_COMPONENT_COUNT = 3;
//顶点位置缓存
private final FloatBuffer vertexBuffer;
//顶点颜色缓存
private final FloatBuffer colorBuffer;
//渲染程序
private int mProgram;
private int uMatrixLocation;
// 矩阵数组
private final float[] mProjectionMatrix = new float[]{
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
};
//三个顶点的位置参数
private float triangleCoords[] = {
0.5f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
};
//三个顶点的颜色参数
private float color[] = {
1.0f, 0.0f, 0.0f, 1.0f,// top
0.0f, 1.0f, 0.0f, 1.0f,// bottom left
0.0f, 0.0f, 1.0f, 1.0f// bottom right
};
public IsoTriangleRender() {
//顶点位置相关
//分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
vertexBuffer.put(triangleCoords);
vertexBuffer.position(0);
//顶点颜色相关
colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
colorBuffer.put(color);
colorBuffer.position(0);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//将背景设置为白色
GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
//编译顶点着色程序
String vertexShaderStr = ResReadUtils.readResource(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader);
int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr);
//编译片段着色程序
String fragmentShaderStr = ResReadUtils.readResource(MyApplication.getInstance(), R.raw.iso_triangle_fragment_shader);
int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr);
//连接程序
mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId);
//在OpenGLES环境中使用程序
GLES30.glUseProgram(mProgram);
/**********新加的部分,获取变换矩阵以及其的位置、颜色等************/
uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix");
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0, 0, width, height);
/**********新加的部分,将变换矩阵传入顶点渲染器************/
//计算宽高比
// 边长比(>=1),非宽高比
float aspectRatio = width > height ?
(float) width / (float) height :
(float) height / (float) width;
// 1. 矩阵数组
// 2. 结果矩阵起始的偏移量
// 3. left:x的最小值
// 4. right:x的最大值
// 5. bottom:y的最小值
// 6. top:y的最大值
// 7. near:z的最小值
// 8. far:z的最大值
if (width > height) {
// 横屏
Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f);
} else {
// 竖屏or正方形
Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f);
}
// 更新u_Matrix的值,即更新矩阵数组
GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0);
}
@Override
public boolean onDrawFrame(GL10 gl) {
//把颜色缓冲区设置为我们预设的颜色
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
//绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertexBuffer中获取vPosition的数据
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer);
//启用顶点位置句柄
GLES30.glEnableVertexAttribArray(0);
//准备颜色数据
GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer);
//启用顶点颜色句柄
GLES30.glEnableVertexAttribArray(1);
//绘制三角形
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, POSITION_COMPONENT_COUNT);
//禁止顶点数组的句柄
GLES30.glDisableVertexAttribArray(0);
GLES30.glDisableVertexAttribArray(1);
return true;
}
@Override
public void onSurfaceDestroyed() {
}
}
- 邮箱 :[email protected]
- Good Luck!