Android 3D 编程:HelloArrow(用 OpenGL ES 2.0 实现)
前一篇 HelloArrow 中用 OpenGL ES 1.x 实现 RenderingEngine 接口,在屏幕上绘制一个向上的箭头,并且当屏幕旋转时箭头也随之旋转,始终保持向上。本篇将用 OpenGL ES 2.0 实现同一接口和功能
运行时检测OpenGL ES版本
对此Android文档中并没有提及,但是<NDK>/sampels/hello-gl2 示例工程中透露出了一些蛛丝马迹,从中我们可以归纳出,要进行OpenGL ES 2.0的渲染,需要有2个步骤:
(1)创建OpenGL ES 2.0 的 Rendering Context
EGL 创建 Context 的函数是
EGLContext eglCreateContext(EGLDisplay display,
EGLConfig config,
EGLContext share_context,
EGLint const * attrib_list)
最后一个参数我们一般会给一个NULL值。根据 EGL 1.0 规范,最后一个参数不使用,但是EGL实现可能扩展此参数用于特定目的。Android 的 EGL 正是在此处引入一个值为 0x3098 的属性,用于指定 OpenGL ES 的版本。OpenGL ES 2.0 的版本值为 2
(2)获取 OpenGL ES 2.0 的 EGL Config
对于
EGLBoolean eglChooseConfig(EGLDisplay display,
EGLint const * attrib_list,
EGLConfig * configs,
EGLint config_size,
EGLint * num_config)
的 attrib_list 参数,Android 引入了一个 EGL10.EGL_RENDERABLE_TYPE=0x3040 的属性,EGL 1.0 规范中并未规定此属性。要进行OpenGL ES 2.0 的渲染,需指定此属性值为 4
根据以上两点,我们可以在运行时首先初始化 EGL 支持 OpenGL ES 2.0,如果失败,则回退到 OpenGL ES 1.x,如下:
public class EGLHelper {
public static final int OPENGL_ES_VERSION_1x = 1;
public static final int OPENGL_ES_VERSION_2 = 2;
private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
private static final int EGL_OPENGL_ES2_BIT = 4;
private EGL10 egl;
private EGLDisplay eglDisplay;
private EGLConfig eglConfig;
private EGLContext eglContext;
private EGLSurface eglSurface;
public EGLHelper() {
}
public boolean initialize(SurfaceHolder holder, int glesVersion) {
if (OPENGL_ES_VERSION_1x != glesVersion && OPENGL_ES_VERSION_2 != glesVersion) {
throw new IllegalArgumentException("GL ES version has to be one of " + OPENGL_ES_VERSION_1x + " and "
+ OPENGL_ES_VERSION_2);
}
// ...
// Choose an EGLConfig
int[] attrList;
if (OPENGL_ES_VERSION_1x == glesVersion) {
attrList = new int[] { //
EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, //
EGL10.EGL_RED_SIZE, 8, //
EGL10.EGL_GREEN_SIZE, 8, //
EGL10.EGL_BLUE_SIZE, 8, //
EGL10.EGL_DEPTH_SIZE, 16, //
EGL10.EGL_NONE //
};
} else {
attrList = new int[] { //
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //
EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, //
EGL10.EGL_RED_SIZE, 8, //
EGL10.EGL_GREEN_SIZE, 8, //
EGL10.EGL_BLUE_SIZE, 8, //
EGL10.EGL_DEPTH_SIZE, 16, //
EGL10.EGL_NONE //
};
}
EGLConfig[] configOut = new EGLConfig[1];
int[] configNumOut = new int[1];
if (egl.eglChooseConfig(eglDisplay, attrList, configOut, 1, configNumOut) && 1 == configNumOut[0]) {
eglConfig = configOut[0];
} else {
// ...
return false;
}
// Create rendering context
int[] contextAttrs;
if (OPENGL_ES_VERSION_1x == glesVersion) {
contextAttrs = null;
} else {
contextAttrs = new int[] { EGL_CONTEXT_CLIENT_VERSION, OPENGL_ES_VERSION_2, //
EGL10.EGL_NONE };
}
eglContext = egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, contextAttrs);
if (null == eglContext || EGL10.EGL_NO_CONTEXT == eglContext) {
// ...
return false;
}
// ...
return true;
}
public void destroy() {
// ...
}
}
然后:
eglHelper = new EGLHelper();
if (eglHelper.initialize(getHolder(), EGLHelper.OPENGL_ES_VERSION_2)) {
// Create RenderingEngine2
} else if (eglHelper.initialize(getHolder(), EGLHelper.OPENGL_ES_VERSION_1x)) {
// Create RenderingEngine1
} else {
// ...
}
Shader
OpenGL ES 2.0 引入了 Shader 的概念。Shader 就是交给 OpenGL 去执行的一段程序,用 OpenGL Shading Language(GLSL)编写。Shader 分为 Vertex Shader 和 Fragement Shader 两类,Vertex Shader 用于处理通过 glDrawArrays() 提交给 OpenGL 的顶点数据,Fragement Shader 用于计算顶点的颜色
目前我所知的就是这么多了。本篇 HellowArrow 程序所用的 Vertex Shader 和 Fragement Shader 我直接从原书复制。Vertex Shader 源代码文件 Shade.vert 内容为
const char* VERTEX_SHADER =STRINGIFY(
attribute vec4 Position;
attribute vec4 SourceColor;
varying vec4 DestinationColor;
uniform mat4 Projection;
uniform mat4 Modelview;
void main(void)
{
DestinationColor = SourceColor;
gl_Position = Projection * Modelview * Position;
}
);
很怪异的语法,后面再学习。Fragement Shader 源文件 Shader.frag 的内容为:
const char * FRAG_SHADER=STRINGIFY(
varying lowp vec4 DestinationColor;
void main(void)
{
gl_FragColor = DestinationColor;
}
);
这两个 Shader 源文件其实是两个 STRINGIFY 宏的扩展(或者说调用吧),GLSL 作为宏变量 。STRINGIFY 宏定义在 RenderingEngine2.c 中,并且用#include 将这两个文件的内容包含进来:
#define STRINGIFY(A) #A
#include "Shader.vert"
#include "Shader.frag"
注意上面 STRINGIFY 宏定义中“#A”前缀的 # 符号。# 是一个 c 预处理符,它将宏变量转换为字符串字面值,例如:
#define AS_STRING(A) #A
const char * str = AS_STRING(this is a c string!);
const char * str_qt = AS_STRING("this is a quoted c string!");
等效于
const char * str = "this is a c string!";
const char * str_qt = "\"this is a quoted c string!\"";
因此,前面的 RenderingEngine2.c 代码片断定义了 VERTEX_SHADER 和 FRAG_SHADER 两个字符串变量,字符串内容分别为 Vertex Shade 和 Fragement Shader 代码。这两段代码会在运行时提交给 OpenGL 编译并执行
RenderingEngine2.c
RenderingEngine2.c 对应于前一篇中的 RenderingEngine1.c,OpenGL 绘图操作(包括动画)都在这个文件中实现,只不过这次用 OpenGL ES 2.0 而不是 OpenGL ES 1.x 来实现。下面我只列出 RenderingEngine2.c 与 RenderingEngine1.c 中不同的地方
initialize()
与 RenderingEngine1.c 的 initialize() 函数一样,主要功能还是设置 Viewport 和正交投影矩阵,代码为:
static GLuint simpleProgram;
void initialize(int width, int height) {
glViewport(0, 0, width, height);
simpleProgram = buildProgram(VERTEX_SHADER, FRAG_SHADER);
glUseProgram(simpleProgram);
// Initialize the projection matrix.
applyOrtho(2, 3);
}
glViewport() 函数在前一篇讲过了,它设置 OpenGL 绘图的范围
接下来出现了 OpenGL ES 2.0 的新东西。OpenGL ES 2.0 将几个 Shader 链接成一个单元,称为 Program(程序),我们这里用一个 buildProgram() 函数创建一个 Program,它由前面定义的 Vertex Shader 和 Fragement Shader 构成。buildProgram() 函数的代码是:
static GLuint buildProgram(const char* vertexShaderSource,
const char* fragmentShaderSource) {
GLuint vertexShader = buildShader(vertexShaderSource, GL_VERTEX_SHADER);
GLuint fragmentShader = buildShader(fragmentShaderSource, GL_FRAGMENT_SHADER);
GLuint programHandle = glCreateProgram();
glAttachShader(programHandle, vertexShader);
glAttachShader(programHandle, fragmentShader);
glLinkProgram(programHandle);
GLint linkSuccess;
glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
// ...
}
return programHandle;
}
buildShader() 是自定义函数,用 GLSL 创建 Shader,稍后再看这个函数。其他的几个 glXxx() 函数,从函数名就能看出它的作用
buildShader() 代码为:
static GLuint buildShader(const char* source, GLenum shaderType) {
GLuint shaderHandle = glCreateShader(shaderType);
glShaderSource(shaderHandle, 1, &source, 0);
glCompileShader(shaderHandle);
GLint compileSuccess;
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
// ...
}
return shaderHandle;
}
很直观的过程:创建、指定GLSL代码、编译
前一篇中用 OpenGL ES 1.0 设置正交投影的过程是:
glMatrixMode(GL_PROJECTION);
// ...
glOrthof(-maxX, maxX, -maxY, maxY, -1.0f, 1.0f);
glMatrixMode(GL_MODELVIEW);
但现在通过自定义 applyOrtho() 函数来设置OpenGL ES 的正交投影矩阵:
static void applyOrtho(float maxX, float maxY) {
float a = 1.0f / maxX;
float b = 1.0f / maxY;
float ortho[16] = {//
a, 0, 0, 0, //
0, b, 0, 0, //
0, 0, -1, 0,//
0, 0, 0, 1 //
};
GLint projectionUniform = glGetUniformLocation(simpleProgram,
"Projection");
glUniformMatrix4fv(projectionUniform, 1, 0, &ortho[0]);
}
太奥妙了,直接上矩阵了,不懂。后面再慢慢淆吧:)
render()
render() 又是一段看不懂的代码啊:
void render() {
glClearColor(0.5f, 0.5f, 0.5f, 1);
glClear(GL_COLOR_BUFFER_BIT);
applyRotation(-currentDegree);
GLuint positionSlot = glGetAttribLocation(simpleProgram, "Position");
GLuint colorSlot = glGetAttribLocation(simpleProgram, "SourceColor");
glEnableVertexAttribArray(positionSlot);
glEnableVertexAttribArray(colorSlot);
GLsizei stride = sizeof(struct Vertex);
const GLvoid* pCoords = vertices[0].position;
const GLvoid* pColors = vertices[0].color;
glVertexAttribPointer(positionSlot, 2, GL_FLOAT, GL_FALSE, stride, pCoords);
glVertexAttribPointer(colorSlot, 4, GL_FLOAT, GL_FALSE, stride, pColors);
GLsizei vertexCount = sizeof(vertices) / sizeof(struct Vertex);
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
glDisableVertexAttribArray(positionSlot);
glDisableVertexAttribArray(colorSlot);
}
glClearColor()、glClear() 清屏
旋转图形,原来 OpenGL ES 1.0 是 glRotatef(),现在通过自定义函数 applyRotate():
static void applyRotation(float degrees) {
float radians = degrees * 3.14159f / 180.0f;
float s = sin(radians);
float c = cos(radians);
float zRotation[16] = { //
c, s, 0, 0, //
-s, c, 0, 0,//
0, 0, 1, 0,//
0, 0, 0, 1//
};
GLint modelviewUniform = glGetUniformLocation(simpleProgram, "Modelview");
glUniformMatrix4fv(modelviewUniform, 1, 0, &zRotation[0]);
}
跟前面 initialize() 里面的 applyOrtho() 一样,直接用矩阵运算。。。无论如何,applyRotate() 将图形旋转了 -currentDegree,这个和前一篇最终结果是一样的
接下来又不同了。OpenGL ES 1.0 通过 glVertexPointer()、glColorPointer() 函数将顶点坐标、颜色分量数组的地址提交给OpenGL,现在对应的函数是 两个 glXxxAttribPointer(),它们的第1个参数似乎和前面的 Vertex Shader 联系上了。。。大概的流程是一致的。。。先这样吧
updateAnimation()、onRotate()
这两个函数只是更新 desiredDegree 和 currentDegree 这2个状态变量,与 RenderingEngine1.c 中的一模一样
本篇完
本篇用OpenGL ES 2.0 重新实现了 RenderingEngine 接口。对 Shader、GLSL 以及使用 2.0 与 1.0 进行3D渲染之间的区别留下了第一印象,但是,未知和疑问也更多了。路漫漫其修远兮。。。
posted on 2011-10-19 17:58 bye_passer 阅读(8122) 评论(1) 编辑 收藏 举报