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  阅读(8106)  评论(1编辑  收藏  举报

导航