《OpenGL ES 2.0 for Android》读书笔记

这是一本关于OpenGL ES 2.0(如下简称OpenGL)快速入门的书。本书使用OpenGL2.0完成了一个3D游戏的制做,游戏名叫作Air Hockey,从Android开发环境的搭建到最后游戏的开发完工,做者每一步都讲述的很详实,是一个很好的学习OpenGL的例子。🌰 本文是我在通读全篇后写下的总结。java

推荐的一些在线资料

OpenGL的绘图方式 —— 点、线、三角形

咱们都知道OpenGL是用来2D或3D绘图的,能够绘制直线、各种图形、各种图像。github

OpenGL其实只能绘制三角形,肯定三个顶点,而后就能够绘制一个三角形,多个三角形拼在一块儿就能够组成各式各样的图形,把图片资源贴到这些各式各样的图形上就能够实现图像的绘制。编程

因此,想要用OpenGL绘制图形,只须要肯定两个问题:顶点、三角形上的颜色。数组

Air Hockey的效果图

经过本文的讲解,最终作出的效果以下。所有使用OpenGL绘制而成,并添加了交互逻辑。这个游戏貌似国内不多人玩,能够去应用商店下载一个玩一玩。bash

-w400

OpenGL坐标和屏幕坐标

OpenGL中的坐标涉及到各类转换操做,也是比较容易混乱的一点,这里单独说明。app

咱们平时理解的二维坐标

本游戏主要有一个桌子,两个冰球,而后还有中间的一条线。 换句话说,就是有一个长方形、两个圆点、一条直线。 根据上面的三角形绘制理论,一个长方形等于两个三角形。因此界面的元素实际上是两个三角形+两个圆点+一条直线。 定义坐标以下: https://cdn.wxdut.com/ ide

-w400

上面的坐标表示以下:函数

float[] tableVerticesWithTriangles = {
            
            // Triangle 1
            0f, 0f,
            9f, 14f,
            0f, 14f,
            
            // Triangle 2
            0f, 0f,
            9f, 0f,
            9f, 14f
            
            // Line 1
            0f, 7f,
            9f, 7f,
            
            // Mallets
            4.5f, 2f,
            4.5f, 12f
};
复制代码

三角形顶点描述方向

细心的会发现,上面描述三角形的三个顶点的时候是顺时针方向(counter-clockwise order),也被称为风向(winding order)。后面默认都是这个方向。

OpenGL坐标

上面咱们定义一套坐标,看起来很是合理,可是有个问题:手机屏幕大小不一,纵横坐标的范围又是多少?咱们上面定义了一个Mallet,坐标为(4.5f, 2f),在不一样屏幕的手机上显示效果确定不同,并且这个坐标里的4.5f2f也是随意写的,只有相对大小,没有具体的参照。

事实上,OpenGL的坐标范围都是[-1, +1] https://cdn.wxdut.com/

-w400

也就是说,想经过OpenGL绘制到屏幕上的内容,其坐标值必须在[-1, +1]之间,不然就没法显示到屏幕上。

因此咱们须要对上面定义的坐标进行修改,使其可以显示到屏幕上。

float[] tableVerticesWithTriangles = {
            // Triangle 1
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,
            // Triangle 2
            -0.5f, -0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,
            // Line 1
            -0.5f, 0f,
            0.5f, 0f,
            // Mallets
            0f, -0.25f,
            0f, 0.25f
};
复制代码

这样一来,咱们想绘制的东西就会显示到屏幕上。

调整屏幕纵横比

常常上一步的处理,咱们可让东西绘制到屏幕上,可是依然会有问题。OpenGL认为全部的屏幕的范围都是[-1,+1] 最简单的一个问题是,好比咱们想绘制一个正方形,坐标范围为[-1,+1],显示到屏幕上就变成了长方形。被拉长了,这个应该很好理解。

好比在OpenGL中,一个常规的坐标范围是正方形:

-w400
可是到了一个 720*1280的手机上就变成了下面的样子:
-w400

为了解决这个问题,咱们还须要一些额外处理。

咱们把OpenGL的坐标称为normalized device coordinates,宽和高的范围都是[-1,+1]

在一个宽320高720的屏幕上,咱们想要显示一个全屏的长方形,则x轴坐标范围为[0, 320],x轴坐标范围为[0, 720],这种坐标咱们定义为virtual coordinate space。然而OpenGL能识别的坐标范围是[-1,+1],因此咱们须要把前者换算成后者,也就是把virtual coordinate space转换成normalized device coordinates,以便于OpenGL能正常显示。

  • 这里有两个关键词:

    • normalized device coordinates:这个是OpenGL的坐标,宽和高的范围都是[-1,+1]
    • virtual coordinate space:这个是根据屏幕纵横比调整以后的坐标,宽的范围为[-1,+1],高的范围为[-height/width,+height/width],其中height是屏幕的高,width是屏幕的宽。

正交投影

上面提到须要把方便易懂的virtual coordinate space坐标转换成normalized device coordinates,而后传给OpenGL绘制。这里就用到了下面提到的正交变换API。

orthoM(float[] m, int mOffset, float left, float right, float bottom, float top, float near, float far)
复制代码

参数解释以下

参数 含义
float[] m 正交变换矩阵
int mOffset 偏移量,默认为0
float left x轴最小值
float right x轴最大值
float bottom y轴最小值
float top y轴最大值
float near z轴最小值
float far z轴最大值

该函数会生成下面这个变换矩阵:

在编程时,要考虑到这一点,在设置位置时须要进行一下正交变换。

写个伪代码方便理解:

normalized_device_coordinates = orthoM(virtual_coordinate_space);
复制代码

OpenGL管线(Pipeline)

我理解的管线其实就是OpenGL从用户指定的顶点数据,一直到最终显示到手机屏幕上,中间所须要经历的步骤,把这些步骤按照时间前后顺序串成一条线,称为管线。

为了理解上面的管线图,咱们取图像上的一个像素点的显示过程来讲明。 图像上的一个像素点要想最终显示到显示器上,有两个关键点:

① 像素点显示的位置 ② 像素点显示的颜色值

那上面的图就能够这么理解:

上面的两个步骤分别经过两种不一样的Shader处理:

① 使用Vertex Shader肯定每个点的具体显示的位置 ② 使用Fragment Shader肯定每个点的具体颜色值

Vertex Shader和Fragment Shader的关系能够用下图表示。

-w400

Shader

Shader有两种,分别为Vertex Shader和Fragment Shader。

Shader有专门的语言,OpenGL Shading Language,简称GLSL。语法相似于C语言,通常在/res/raw文件夹下,命名为xxx.glsl。若是对glsl语言不熟悉的话墙裂建议先看一下OpenGL Shading Language(GLSL)语法一览

Shader一般用xxx.glsl文件描述,该文件中通常形式以下:

attribute vec4 a_Position;
void main()
{
    gl_Position = a_Position;
}
复制代码

其中main方法是Shader的入口函数,当Shader被调用时main方法就会被执行。

Shader的初始化

定义Shader

有了上面的glsl的基本语法知识后,咱们开始尝试用glsl来表达Shader。

// AirHockey1/res/raw/simple_vertex_shader.glsl
attribute vec4 a_Position;
void main()
{
    gl_Position = a_Position;
}
复制代码

上面的Vertex Shader很简单,就是声明了一个vec4的变量a_Position,而且在Shader执行时进行赋值操做gl_Position = a_Position;

// AirHockey1/res/raw/simple_fragment_shader.glsl
precision mediump float;
uniform vec4 u_Color;
void main()
{
    gl_FragColor = u_Color;
}
复制代码

上面的Fragment Shader也很简单,就是声明了一个vec4的变量u_Color,而且在Shader执行时进行赋值操做gl_FragColor = u_Color;

建立Shader

final int shaderObjectId = glCreateShader(type);
    if (shaderObjectId == 0) {
        if (LoggerConfig.ON) {
        Log.w(TAG, "Could not create new shader.");
    }
    return 0;
}
复制代码

调用APIglCreateShader建立一个空的Shader,其中参数type有两种,分别为GL_VERTEX_SHADERGL_FRAGMENT_SHADER,分别表示Vertex Shader和Fragment Shader。

该函数返回一个int,是Shader的惟一标识ID,咱们后面能够用它来找到这个Shader,相似于Java中的指针同样,指向了建立的对象。

该函数默认返回一个大于0的数值,若是返回0,则表示建立失败。

填充Shader

上一步建立了一个空的Shader,id为shaderObjectId,下面给Shader填充具体的逻辑。

glShaderSource(shaderObjectId, shaderCode);
复制代码

调用APIglShaderSource为Shader填充具体的逻辑,其中,参数shaderObjectId是Shader的惟一标识ID,在调用glCreateShader建立Shader的时候获得的;参数shaderCode是指上面用glsl写的Shader代码。

编译Shader

上面咱们建立并填充了Shader,下面对Shader进行编译。

glCompileShader(shaderObjectId);
复制代码

调用APIglCompileShader对Shader进行编译,其中,参数shaderObjectId是Shader的惟一标识ID,在调用glCreateShader建立Shader的时候获得的。

不过这一步不必定会成功,好比你的shaderCode写得有问题,咱们须要确保这一步成功才能继续下面的工做。

OpenGL不会自动throw Exception,不过咱们可使用API获取执行状态。

final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

if (LoggerConfig.ON) {
    // Print the shader info log to the Android log output.
    Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
        + glGetShaderInfoLog(shaderObjectId));
}

if (compileStatus[0] == 0) {
    // If it failed, delete the shader object.
    glDeleteShader(shaderObjectId);
    if (LoggerConfig.ON) {
        Log.w(TAG, "Compilation of shader failed.");
    }
    return 0;
}
复制代码

连接Shader

咱们知道Vertex Shader和Fragment Shader分别是OpenGL管线中的重要的两步,Vertex Shader肯定位置,Fragment Shader肯定该位置的颜色。它们之间是一一对应,不可或缺的,咱们须要将它们连接起来。

在OpenGL中,Vertex Shader和Fragment Shader连接到一块儿,成为一个Program。

建立Program
final int programObjectId = glCreateProgram();
    if (programObjectId == 0) {
        if (LoggerConfig.ON) {
        Log.w(TAG, "Could not create new program");
    }
    return 0;
}
复制代码

调用APIglCreateProgram建立一个空的Program。

该函数返回一个int,是Shader的惟一标识ID,咱们后面能够用它来找到这个Shader,相似于Java中的指针同样,指向了建立的对象。

该函数默认返回一个大于0的数值,若是返回0,则表示建立失败。

绑定Shader

上面建立了Program,下面给这个Program绑定Vertex Shader和Fragment Shader。

glAttachShader(programObjectId, vertexShaderId);
glAttachShader(programObjectId, fragmentShaderId);
复制代码

调用APIglAttachShader为Program绑定Shader。参数也很好理解了,programObjectId是Program的惟一ID标识,vertexShaderId和fragmentShaderId是指Shader的惟一ID标识。

连接Shader

给Program填充了Shader以后就能够进行连接了。

glLinkProgram(programObjectId);
复制代码

一样的,连接Shader也不必定会成功,咱们须要验证下。

final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);

if (LoggerConfig.ON) {
    // Print the program info log to the Android log output.
    Log.v(TAG, "Results of linking program:\n"
        + glGetProgramInfoLog(programObjectId));
}

if (linkStatus[0] == 0) {
    // If it failed, delete the program object.
    glDeleteProgram(programObjectId);
    if (LoggerConfig.ON) {
        Log.w(TAG, "Linking of program failed.");
    }
    return 0;
}
复制代码
验证Program

因为配置不一样,有可能设置的Program不兼容,这里验证下。

public static boolean validateProgram(int programObjectId) {

    glValidateProgram(programObjectId);
    
    final int[] validateStatus = new int[1];
    glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);
    
    Log.v(TAG, "Results of validating program: " + validateStatus[0]
        + "\nLog:" + glGetProgramInfoLog(programObjectId));
    
    return validateStatus[0] != 0;
}
复制代码

Shader的赋值

给Vertex Shader赋值

上面咱们进行了Shader的初始化,并连接成了Program。下面咱们经过对两个Shader进行赋值来实现绘制效果。

private static final String A_POSITION = "a_Position";
private int aPositionLocation;

// 得到Vertex Shader的位置参数的地址,以便于后续赋值
aPositionLocation = glGetAttribLocation(program, A_POSITION);

// 给Vertex Shader赋值
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,
false, 0, vertexData);

// 通知OpenGL使用顶点数据进行绘制
glEnableVertexAttribArray(aPositionLocation);
复制代码

这里有个重要的API——glVertexAttribPointer,用来给Vertex Shader赋值。它的参数比较多,说明以下:

函数定义:glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, Buffer ptr)

参数说明:

参数 类型 做用
index int Vertex Shader的a_Position的位置,也就是上面取到的aPositionLocation
size int 这个是指Position的维度,好比二维坐标就填2,三维坐标就填3,以此类推
type int 这里是指Position的数据类型,float就填GL_FLOAT,int就填GL_INT,以此类推
normalized boolean 这里只有Position的数据类型为int的时候才会用到,其余场景为false
stride int 跨度,指的是相邻两个Position数据之间的间隔,默认填0
ptr Buffer Position的数据buffer

给Fragment Shader赋值

private static final String U_COLOR = "u_Color";
private int uColorLocation;

uColorLocation = glGetUniformLocation(program, U_COLOR);
复制代码

显示到屏幕上

咱们先来回顾一下Vertex数组:

float[] tableVerticesWithTriangles = {
// Triangle 1
0f, 0f,
9f, 14f,
0f, 14f,
// Triangle 2
0f, 0f,
9f, 0f,
9f, 14f,
// Line 1
0f, 7f,
9f, 7f,
// Mallets
4.5f, 2f,
4.5f, 12f
};
复制代码
// 指定颜色,颜色写入的位置为uColorLocation,颜色值的rgba分别为1.0f, 1.0f, 1.0f, 1.0f。
glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);

// 绘制两个三角形,从数组的下标0开始,绘制六个顶点。
glDrawArrays(GL_TRIANGLES, 0, 6);

// 绘制两条线,从数组的下标6开始,绘制两个顶点。
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2);

// Draw the first mallet blue.
glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
glDrawArrays(GL_POINTS, 8, 1);
// Draw the second mallet red.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_POINTS, 9, 1);
复制代码

Shader使用小结

本章节刚开始讲到了Shader的分类、glsl语言。

后面详述了Shader的建立、填充、编译、连接,还讲到了Shader的赋值与最终显示。

Texture

咱们上面使用简单的Vertex Shader和Fragment Shader实现了基本图形和颜色的绘制,可是这远远不够。 若是想绘制一些图片呢,就须要用到新的东西——Texture。

上面提到,Fragment Shader有众多的fragment,一个fragment相似于一个像素。Texture包含了不少的texel,这texel也能够理解成一个像素点。

使用Texture能够把各种图片加载到OpenGL中进而进行显示,从而实现炫酷的游戏场景。

-w400
举个例子,上图中,游戏的背景是一张图片,而不是简单的纯色背景。

  • 注意

在OpenGL ES 2.0中,Texture不必定要是正方形,可是S和T的值必须是2的n次方。

Texture坐标和图片坐标

Texture自己也是有坐标的,对于二维Texture来讲,两个维度分别被称为S和T,再也不是x和y轴。 而且,S和T轴的范围都是[0,1]

-w400

除了Texture有坐标外,图片自己也有坐标,坐标以下。其中,左上角为原点,y轴向下,x轴向左。

-w400

  • 关于坐标这一点必定要搞清楚,否则后面各类变换会懵逼的。总结下其实也没几个坐标。
  1. OpenGL有坐标范围,坐标值范围是[-1,1],中心为原点。
  2. Texture有坐标,坐标值范围是[0,1],左下角为原点。
  3. 图片有坐标,左上角为原点,x轴向左,y轴向下。

把图片加载到Texture中

使用Texture,第一步固然是建立并加载图片进来。

建立Texture

建立一个空的Texture的方式和上面建立Shader差很少,也是直接调用API,而后底层建立一个Texture并返回Texture的惟一ID标识,咱们后面能够根据这个ID来得到这个Texture。

一样的,若是ID为0,则建立失败,正常状况ID是大于0的。

final int[] textureObjectIds = new int[1];
glGenTextures(1, textureObjectIds, 0);
if (textureObjectIds[0] == 0) {
    if (LoggerConfig.ON) {
        Log.w(TAG, "Could not generate a new OpenGL texture object.");
    }
    return 0;
}
复制代码

这里有一个新的API:glGenTextures,参数说明以下:

参数 含义
int n 给新建立的Texture返回n个惟一ID标识,这里只须要填1就好了
int[] textures OpenGL会将Texture的ID存放到这个数组里,固然,textures.length >= n + offset
int offset 参数textures的offset,textures.length >= n + offset,你懂的

加载Bitmap并绑定到Texture

这里很显然,应该是调用API把Bitmap和Texture绑定起来进行显示,逻辑很简单。

OpenGL在同一时间只能绑定一个Texture,因此这里先把texture绑定到OpenGL,而后再将bitmap传给OpenGL,就能够实现绑定操做。

glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);
texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
// OpenGL会copy一份bitmap,因此这里直接回收掉就行
bitmap.recycle();
glGenerateMipmap(GL_TEXTURE_2D);
复制代码

使用Texture

上面咱们已经把bitmap和texture绑定了,下面使用这个texture进行绘制。

// 使用该Texture
glActiveTexture(GL_TEXTURE0);
glUniform1i(uTextureUnitLocation, 0);
复制代码

建立Texture相关的Shader

建立Vertex Shader

uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;
varying vec2 v_TextureCoordinates;
void main()
{
    v_TextureCoordinates = a_TextureCoordinates;
    gl_Position = u_Matrix * a_Position;
}
复制代码

建立Fragment Shader

precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;
void main()
{
    gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);
}
复制代码

u_TextureUnit是Texture的数据,v_TextureCoordinates是Texture的某个位置,texture2D返回这个Texture在v_TextureCoordinates这个位置的颜色,并赋值给gl_FragColor,进而绘制到屏幕。

OpenGL绘图实例

顺手写了两个demo,有须要的能够参考下。OpenGL-ES-2.0-for-Android

主要看一下下面两个功能:

参考文档

www.learnopengles.com/understandi…

相关文章
相关标签/搜索