本文作为读书笔记,意在方便自己复习并欢迎大家的指点交流。
附录中含有工程地址与书籍pdf下载链接
一、创建Renderer类
实现GLSurfaceView.Renderer接口,接口方法如下:
首先在onSurfaceCreated开始处调用
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)设置清空屏幕用的颜色;前三个参数分别对应红色、绿色和蓝色,最后的参数对应一个特殊的分量,称为阿尔法(alpha),它经常用来表示半透明度或透明度。
然后在onSurfaceChanged开始处调用
GLES20.glViewport(0, 0, width, height)设置视口尺寸,这就告诉OpenGL可以用来渲染surface的大小
最后在onDrawFrame调用
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)清空屏幕;这会擦除屏幕 上的所有颜色,并用之前glClearColor()调用定义的颜色填充整个屏幕。
注意:要在后台线程中渲染
GLSurfaceView会在一个单独的线程中调用渲染器的方法。默认情况下,GLSurfaceView
会以显示设备的刷新频率不断地渲染,当然,它也可以配置为按请求渲染,只需要用
GLSurfaceView.RENDERMODE WHEN DIRTY作为参数调GLSurfaceView.setRenderMode()即可。
既然Android的GLSurfaceView在后台线程中执行渲染,就必须要小心,只能在这个渲染线程中调用OpenGL,
在Android主线程中使用UI(用户界面)相关的调用;两个线程之间的通信可以用如下方法:
在主线程中的GLSurfaceView实例可以调用queueEvent()方法传递一个Runnable给后台渲染线程,
渲染线程可以调用Activity的runOnUIThread()来传递事件(event)给主线程。
二、点 直线 三角形
在OpenGL里,只能绘制点,直线,三角形。点和直线可以用于某些效果,但是,只有三角形才能用来构建拥有复杂的对象和纹理的场景。在OpenGL里, 我们把单独的点放在一个组里构建出三角形,再告诉 OpenGL如何连接这些点。我们想要构建的所有东西都要用点、直线和三角形定义。
当我们定义三角形的时候,我们总是以逆时针的顺序排列顶点;这称为卷曲顺序(winding order)。因为在任何地方都使用这种一致的卷曲顺序,可以优化性能:使用卷曲顺序可以指出一个三角形属于任何给定物体的前面或者后面,OpenGL可以忽略那些无论如何都无法被看到的后面的三角形。
三、把内存从java堆复制到本地堆
ByteBuffer
.allocateDirect(vertex.length * BYTES_PER_FLOAT)//分配一块本地内存,不会被垃圾回收器管理
.order(ByteOrder.nativeOrder())//按照本地字节序排序
.asFloatBuffer()
.put(vertex);//把数据从Dalvik的内存复制到本地内存
四、着色器(shader)
类型:顶点着色器 片段着色器
1.顶点着色器( vertex shader)生成每个顶点的最终位置,针对每个顶点,它都会执行一次; 一旦最终位置确定了,OpenGL就可以把这些可见顶点的集合组装成点、直线 以及三角形。
2.片段着色器(fragment shader)为组成点、直线或者三角形的每个片段生成最终的颜 色,针对每个片段,它都会执行一次; 一个片段是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。
使用流程:
(1)创建一个新的着色器对象
int shaderObjectId = GLES20.glCreateShader(type);
用glCreateShader(调用创建了一个新的着色器对象,并把这个对象的ID存入变量shaderObjectId。这个type可以是代表顶点着色器的GL_ VERTEX _SHADER,或者是代表片段着色器的GL_FRAGMENT_SHADER。
(2)上传着色器源代码
GLES20.glShaderSource(shaderObjectId, shaderCode);
一旦有了有效的着色器对象,就可以调用glShaderSource(shaderObjectId, shaderCode) 上传源代码了。这个调用告诉OpenGL读人字符串shaderCode定义的源代码,并把 它与shaderObjectld所引用的着色器对象关联起来。
(3)编译着色器源代码
GLES20.glCompileShader(shaderObjectId);
(4)取出编译状态
int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
为了检查编译是失败还是成功,首先要创建一个新的长度为1的int数组,称为compileStatus;然后调用glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0)。这就告诉OpenGL读取与shaderObjectId 关联的编译状态,并把它写入 compileStatus的第0个元素。
这是Android平台上的OpenGL的一个通用模式。为了取出一个值,我们通常会使用一个长度为1的数组,并把这个数组传进一个 OpenGL调用。在同一个调用中,我们告诉OpenGL把结果存进数组的第一个元素中。
(5)验证编译状态
if (compileStatus[0] == 0) {
GLES20.glDeleteShader(shaderObjectId);
return 0;
}
如果它是0,编译就失败了,这种情况下,我们就不再需要着色器对象了,因此告诉OpenGL把它删除并返回0给调用代码;如果编译成功,着色器对象就是有效的,我们就可以在代码中使用它了。
五、程序对象(program)
public static int linkProgram(int vertexShader, int fragmentShader) {
//新建程序对象
int programObjectId = GLES20.glCreateProgram();
if (programObjectId == 0) {
Log.e(TAG, "could not create program");
return 0;
}
//附上着色器
GLES20.glAttachShader(programObjectId, vertexShader);
GLES20.glAttachShader(programObjectId, fragmentShader);
//链接程序
GLES20.glLinkProgram(programObjectId);
int[] linkStatus = new int[1];
//检查链接状态
GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
GLES20.glDeleteProgram(programObjectId);
Log.e(TAG, "linkProgram failed");
return 0;
}
return programObjectId;
}
使用program:
(1)获取uniform位置
GLES20.glGetUniformLocation(program, U_MATRIX);
(2)获取属性位置
GLES20.glGetAttribLocation(program, A_POSITION);
(3)关联属性与顶点数据的数组
GLES20.glVertexAttribPointer(positionLocation,POSITION_COUNT,GL_FLOAT,false,STRIDE,vertexData);
(4)使能顶点数组
GLES20.glEnableVertexAttribArray(positionLocation);
必须要在glVertexAttribPointer之后调用,通过这最后一个调用,OpenGL现在就知道去哪里寻找它所需要的数据了。
六、矩阵
(1)正交投影矩阵
在android.opengl.Matrix中。这个类有一个orthoM()的方法,它可以为我们生成一个正交投影来弥补屏幕的宽高比,它通过调整显示区域的宽度和高度使之变换为归一化设备坐标。
正交投影矩阵会把所有在左右之间、上下之间和远近之间的事物映射到归一化设备坐标中从-1到1的 范围,在这个范围内的所有事物在屏幕上都是可见的。
我们看一下orthoM()的所有参数
orthoM(float[] m, int mOffset,float left, float right, float bottom, float top,float near, float far)
- float[] m:目标数组,这个数组的长度至少有16个元素,这样它才能存储正交投影矩阵;
- int mOffset:结果矩阵起始的偏移值;
- float left: x轴的最小范围;
- float right: x轴的最大范围;
- float bottom: y轴的最小范围;
- float top: y轴的最大范围;
- float near: z轴的最小范围;
- float far: z轴的最大范围。
(2)透视投影矩阵
透视除法
即将所有分量都出除以W分量,使W分量为1
透视除法之后,那个位置就在归一化设备坐标中了,不管渲染区域的大小和形状,对于其中的每个可视坐标,其x、y和z分量的取值都位于[-1, 1]的范围内。
视椎体
是由一个透视投影矩阵和投影除法创建的。简单来说,视椎体只是一个立方体,其远端比近端大,从而使其变成一个被截断的金字塔。两端的大小差别越大,观察的范围越宽,我们能看到的也越多
一个视椎体有一个焦点(focal point)。这个焦点可以这样得到,顺着从视椎体较大端向较小端扩展出来的那些直线,一直向前通过较小端直到它们汇聚到一起。当你用透视投影观察一个场景的时候,那个场景看上去就像你的头被放在了焦点处。焦点和视椎体小端的距离被称为焦距 (focal length),它影响视椎体小端和大端的比例,以及其对应的视野。
创建投影矩阵
public static void perspectiveM(float[] m, float yFov, float aspect, float n, float f) {
//把角度转化为弧度
float radians = (float) (yFov * Math.PI / 180.0);
//计算焦距
float a = (float) (1.0 / (Math.tan(radians / 2.0)));
//一次写入矩阵的一列
m[0] = a / aspect;
m[1] = 0f;
m[2] = 0f;
m[3] = 0f;
m[4] = 0f;
m[5] = a;
m[6] = 0f;
m[7] = 0f;
m[8] = 0f;
m[9] = 0f;
m[10] = -((f + n) / (f - n));
m[11] = -1f;
m[12] = 0f;
m[13] = 0f;
m[14] = -((2f * f * n) / (f - n));
m[15] = 0f;
}
(3)模型矩阵
模型矩阵是用来把物体放在世界空间(world-space)坐标系的。
(4)视图矩阵
视图矩阵是出于同模型矩阵一样的原因被使用的,但是它平等地影响场最中的每个物体。因为它影响所有的东西,它在功能上等同于一个相机:来回移动相机,你会从不同的视角看见那些东西。使用另外一个矩阵的优势是它让我们预先把许多变换处理成单个矩阵。
举个例子,想象一下我们要来回旋转一个场景,并把它移动一定量的距离。能实现这些的一种方式是把同样的旋转和平移调用应用于每一个单个的物体。尽管那样可行,但如果只把这些变换存到另外一个矩阵,并把这个矩阵应用于每个物体,会更容易实现。
Matrix.setLookAtM()创建一个特殊类型的视图矩阵
七、纹理
OpenGL 中的纹理可以用来表示图像、照片、甚至由一个数学算法生成的分形数据。每个二维的纹理都由许多小的纹理元素(texel)组成,它们是小块的数据。要使用纹理,最常用的方式是直接从一个图像文件加载数据。
每个二维的纹理都有其自己的坐标空间,其范围是从一个拐角的(0,0)到另一个拐角的(1,1)。按照惯例,一个维度叫做S,而另一个称为工。当我们想要把一个纹理应用于一个三角形或一组三角形的时候,我们要为每个顶点指定一组ST纹理坐标,以便OpenGL 知道需要用那个纹理的哪个部分画到每个三角形上。这些纹理坐标有时也会被称为UV纹理坐标。
对一个OpenGL 纹理来说,它没有内在的方向性,因此我们可以使用不同的坐标把它定向到任何我们喜欢的方向上。然而,大多数计算机图像都有一个默认的方向,它们通常被规定为y轴向下,y的值随着向图像的底部移动而增加。只要我们记住,如果想用正确的方向观看图像,那纹理坐标就必须要考虑这点,这就不会给我们带来任何麻烦。
在标准OpenGL ES 2.0中,纹理不必是正方形,但是每个维度都应该是2的幂(POT)。这就意味着每个维度都是这样的一个数字,如128、256、512等。这样规定的原因在于非POT纹理可以被使用的场合非常有限,而POT纹理适用于各种情况。
(1)纹理过滤
纹理过滤模式
设置纹理过滤
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.GL_TEXTURE_MIN_FILTER 是指缩小的情况
GLES20.GL_TEXTURE_MAG_FILTER 是指放大的情况
每种情况下允许的纹理过滤模式
(2)使用纹理
public static int loadTexture(Context context, int res) {
int[] textureId = new int[1];
//创建纹理对象
GLES20.glGenTextures(1, textureId, 0);
if (textureId[0] == 0) {
return 0;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), res, options);
if (bitmap == null) {
GLES20.glDeleteTextures(1, textureId, 0);
return 0;
}
//绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]);
//设置过滤器
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
//加载位图数据到opengl
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
//生成MIP贴图
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
//释放数据
bitmap.recycle();
//传递 0 解除绑定
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
return textureId[0];
}
八、混合
//使能混合技术
glEnable(GL_BLEND);
//设置为累加混合
glBlendFunc(GL_ONE, GL_ONE);
混合公式:输出=(源因子* 源片段)十(目标因子* 目标片段)
在OpenGL 里,混合技术的工作原理是把片段着色器的结果和已经在帧缓冲区中的颜色进行混合。源片段的值来自于片段着色器,目标片段的值就是已经在帧缓冲区中的值,源因子和目标因子的值是通过调用 glBlendFunc配置的。
九、天空盒
天空盒是表达三维全景的一个方法,不管你的头转向哪边,这个全景在任何方向上都能被看见。渲染天空盒最经典的方法之一是使用一个环绕观察者的立方体,并在其每个面上都附上一个细致的纹理;这也称为立方体贴图。在绘制天空盒的时候,我们只需要保证它出现在场景中所有其他物体的后面。
(1)加载天空盒
public static int loadCubeMap(Context context, int[] cubeResources) {
final int[] textureObjectIds = new int[1];
glGenTextures(1, textureObjectIds, 0);
if (textureObjectIds[0] == 0) {
Log.w(TAG, "Could not generate a new OpenGL texture object.");
return 0;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
final Bitmap[] cubeBitmaps = new Bitmap[6];
for (int i = 0; i < 6; i++) {
cubeBitmaps[i] = BitmapFactory.decodeResource(context.getResources(), cubeResources[i], options);
if (cubeBitmaps[i] == null) {
Log.w(TAG, "Resource ID " + cubeResources[i] + " could not be decoded.");
glDeleteTextures(1, textureObjectIds, 0);
return 0;
}
}
glBindTexture(GL_TEXTURE_CUBE_MAP, textureObjectIds[0]);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, cubeBitmaps[0], 0);
texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, cubeBitmaps[1], 0);
texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, cubeBitmaps[2], 0);
texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, cubeBitmaps[3], 0);
texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, cubeBitmaps[4], 0);
texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, cubeBitmaps[5], 0);
glBindTexture(GL_TEXTURE_2D, 0);
for (Bitmap bitmap : cubeBitmaps) {
bitmap.recycle();
}
return textureObjectIds[0];
}
(2)顶点缓冲区
//顶点缓冲区对象
public class VertexBuffer {
private final int bufferId;
public VertexBuffer(float[] vertexData) {
final int buffers[] = new int[1];
//创建缓冲区对象
glGenBuffers(buffers.length, buffers, 0);
if (buffers[0] == 0) {
throw new RuntimeException("Could not create a new vertex buffer object.");
}
bufferId = buffers[0];
//传递GL_ARRAY_BUFFER 告诉OpenGL这是一个顶点缓冲区
glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
FloatBuffer vertexArray = ByteBuffer
.allocateDirect(vertexData.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexArray.position(0);
//GL_STATIC_DRAW 这个对象将被修改一次,但是会经常使用 (这些只是提示,而不是限制,所以OpenGL可以根据需要做任何优化。大多数情况下,我们都使用GL_STATIC DRAW)
glBufferData(GL_ARRAY_BUFFER, vertexArray.capacity() * BYTES_PER_FLOAT,
vertexArray, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
}
glBufferData 把数据传到缓冲区
(3)索引缓冲区
//索引缓冲区对象
public class IndexBuffer {
private final int bufferId;
public IndexBuffer(short[] indexData) {
final int buffers[] = new int[1];
glGenBuffers(buffers.length, buffers, 0);
if (buffers[0] == 0) {
throw new RuntimeException("Could not create a new index buffer object.");
}
bufferId = buffers[0];
//GL_ELEMENT_ARRAY_BUFFER 索引缓冲区
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[0]);
ShortBuffer indexArray = ByteBuffer
.allocateDirect(indexData.length * BYTES_PER_SHORT)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.put(indexData);
indexArray.position(0);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexArray.capacity() * BYTES_PER_SHORT,
indexArray, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
}
(4)深度缓冲区
它是一个特殊的缓冲区,用于记录屏幕上每个片段的深度。当这个缓冲区打开时,OpenGL 会为每个片段执行深度测试算法:如果片段比已经存在的片段更近,就绘制它;否则,就丟掉它。
//打开深度缓冲区功能
glEnable(GL_DEPTH_TEST);
//告诉opengl在每帧上也要清除深度缓冲区
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
深度测试
glDepthFunc() 当前深度值和深度缓冲区中的深度值,进行比较的函数
参数与说明:
(5)剔除
关闭两面绘制,消减绘制开销
//使能剔除技术消除隐藏面
glEnable(GL_CULL_FACE);
十、光照
(1)分类
环境光 (Ambient light)
环境光看上去来自四面八方,场景中的一切被照亮的程度都一样。这近似于我们从大的、平等的光源获取的光照,比如天空,环境光也能用于虚构在光线到达我们的眼睛之前从许多物体弹开的效果,因此阴影从来不会被漆成黑色
方向光 (Directional light)
方向光看上去似乎来自一个方向,光源好像处于极其远的地方。这与我们从太阳或月亮获取的光照相似。
点光 (Point light)
点光看上去是从附近某处投射的光亮,而且光的密度随着距离而减少。这适于表示近处的光源,其把它们的光投射到四面八方,像一个灯泡或蜡烛一样。
聚光 (Spot light)
聚光与点光类似,只是加了一个限制,只能向一个特定的方向投射。这是我们从手电筒或者聚光灯(顾名思义)所获得的光照类型。我们也可以把光线在物体表面反射的方式分为两类:
漫反射 (Diffuse reflection)
漫反射是指光线平等地向所有方向蔓延,适于表示没有抛光表面的材质,比如地毯或外面的混凝土墙。这些类型的表面似乎有很多不同的观察点一样。
镜面反射 (Specular reflection)
镜面反射在某个特定的方向上反射更加强烈,适于被抛光的或者闪亮的材质,比如光滑的金属或者刚刚打过蜡的汽车。
(2)限制帧率
private void limitFrameRate(int framesPerSecond) {
long elapsedFrameTimeMs = SystemClock.elapsedRealtime() - frameStartTimeMs;
long expectedFrameTimeMs = 1000 / framesPerSecond;
long timeToSleepMs = expectedFrameTimeMs - elapsedFrameTimeMs;
if (timeToSleepMs > 0) {
SystemClock.sleep(timeToSleepMs);
}
frameStartTimeMs = SystemClock.elapsedRealtime();
}
我们首先计算自从上一帧被渲染后逝去了多少时间,然后,计算在需要渲染下一帧之前还剩下多少时间。之后,我们让线程暂停那些时间,这样,我们就让它的速度降低到了需要的帧率。
(3)打印帧率
private void logFrameRate() {
long elapsedRealtimeMs = SystemClock.elapsedRealtime();
double elapsedSeconds = (elapsedRealtimeMs - startTimeMs) / 1000.0;
if (elapsedSeconds >= 1.0) {
Log.d("TAG", frameCount / elapsedSeconds + "fps");
startTimeMs = SystemClock.elapsedRealtime();
frameCount = 0;
}
frameCount++;
}
(4)保留EGL上下文
glSurfaceView.setPreserveEGLContextOnPause(true);
当我们离开主屏幕,稍后返回时,整个场景会被重新加载。这是因为当我们暂停GLSurfaceView时,默认情况下,它会释放所有的OpenGL资源,当我们稍后恢复运行时,渲染表面会被重新创建,又将调用onSurfaceCreated,它会要求我们重新加载所有的数据。
当调用此方法时,会保留EGL上下文 不必重新加载所有的OpenGL数据。