@cxm-2016
2017-04-05T14:28:02.000000Z
字数 14023
阅读 6066
OpenGL-ES
版本:1
作者:陈小默
声明:禁止商用,禁止转载
上一篇:NDK开发OpenGL ES 3.0(三)——着色器基础
如遇MarkDown支持不完整的问题 请点击这里查看原文
参考书目:
[1]Donald Hearn,M.Pauline Barker.计算机图形学 第四版(蔡士杰 译).北京:电子工业出版社
[2]Dave Shreiner,Graham Sellers.OpenGL编程指南 第八版(王锐 译).北京:机械工业出版社
[3]Dan Ginsburg,Budirjanto Purnomo.OpenGL ES 3.0 编程指南 第二版(姚军 译).北京:机械工业出版社
参考示例:
[1]GoogleSamples - Android NDK
本章通过旋转的彩色方块的例子来介绍OpenGL中的顶点数组、顶点属相和缓冲区对象的相关概念。示例改编自谷歌官方Demo。
这里的过程跟第二章没有区别,以下展示详细代码不再详解
在CMakeList.txt文件夹添加如下内容
add_library( rect-lib
SHARED
src/main/cpp/esUtil.cpp
src/main/cpp/rect-lib.cpp)
target_link_libraries(rect-lib
${OPENGL_LIB}
android
EGL
log
m)
创建Java文件RectLib
public class RectLib {
static {
System.loadLibrary("rect-lib");
}
public static native boolean init();
public static native void resize(int width,int height);
public static native void step();
public static native void destroy();
}
创建一个RectView文件
class RectView(context: Context) : GLSurfaceView(context){
init {
setEGLConfigChooser(8,8,8,0,16,0)
setEGLContextClientVersion(3)
setRenderer(RectRender())
}
override fun onPause() {
RectLib.destroy()
super.onPause()
}
class RectRender:GLSurfaceView.Renderer{
override fun onDrawFrame(gl: GL10?) {
RectLib.step()
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
RectLib.resize(width, height)
}
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
RectLib.init()
}
}
}
创建一个KotlinActivity:RectActivity
class RectActivity : AppCompatActivity() {
lateinit var view: GLSurfaceView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
view = RectView(this)
setContentView(view)
}
override fun onResume() {
super.onResume()
view.onResume()
}
override fun onPause() {
super.onPause()
view.onPause()
}
}
在MainActivity的布局中添加一个按钮
<Button
android:text="绘制一个旋转的矩形"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_drawRectangle" />
在MainActivity中添加点击事件
btn_drawRectangle.setOnClickListener {
val intent = Intent(this,RectActivity::class.java)
startActivity(intent)
}
#define LOG_TAG "rect-lib"
#include "esUtil.h"
#include <math.h>
#include <time.h>
#define STR(s) #s
#define STRV(s) STR(s)
#define POS_ATTRIB 0
#define COLOR_ATTRIB 1
#define SCALEROT_ATTRIB 2
#define OFFSET_ATTRIB 3
#define TWO_PI (2.0 * M_PI)
#define MAX_ROT_SPEED (0.3 * TWO_PI)
static const char VERTEX_SHADER[] =
"#version 300 es\n"
"layout(location = " STRV(POS_ATTRIB) ") in vec2 pos;\n"
"layout(location=" STRV(COLOR_ATTRIB) ") in vec4 color;\n"
"layout(location=" STRV(SCALEROT_ATTRIB) ") in vec4 scaleRot;\n"
"layout(location=" STRV(OFFSET_ATTRIB) ") in vec2 offset;\n"
"out vec4 vColor;\n"
"void main() {\n"
" mat2 sr = mat2(scaleRot.xy, scaleRot.zw);\n"
" gl_Position = vec4(sr*pos + offset, 0.0, 1.0);\n"
" vColor = color;\n"
"}\n";
static const char FRAGMENT_SHADER[] =
"#version 300 es\n"
"precision mediump float;\n"
"in vec4 vColor;\n"
"out vec4 outColor;\n"
"void main() {\n"
" outColor = vColor;\n"
"}\n";
struct Vertex {
GLfloat pos[2];
GLubyte rgba[4];
};
const Vertex QUAD[4] = {
{{-0.7f, -0.7f}, {0x00, 0xFF, 0x00}},
{{0.7f, -0.7f}, {0x00, 0x00, 0xFF}},
{{-0.7f, 0.7f}, {0xFF, 0x00, 0x00}},
{{0.7f, 0.7f}, {0xFF, 0xFF, 0xFF}},
};
class Renderer {
public:
bool init();
Renderer();
~Renderer();
void resize(int w, int h);
void render();
private:
enum {
VB_INSTANCE, VB_SCALEROT, VB_OFFSET, VB_COUNT
};
float mScale[2];
float mAngularVelocity;
uint64_t mLastFrameNs;
float mAngles;
GLuint mProgram;
GLuint mVB[VB_COUNT];
GLuint mVBState;
float *mapTransformBuf();
void unmapTransformBuf();
void draw();
void calcSceneParams(int w, int h);
void step();
};
Renderer::Renderer() : mLastFrameNs(0),
mProgram(0),
mVBState(0) {
memset(mScale, 0, sizeof(mScale));
for (int i = 0; i < VB_COUNT; i++)
mVB[i] = 0;
}
Renderer::~Renderer() {
glDeleteVertexArrays(1, &mVBState);
glDeleteBuffers(VB_COUNT, mVB);
glDeleteProgram(mProgram);
}
bool Renderer::init() {
mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
if (!mProgram)
return false;
glGenBuffers(VB_COUNT, mVB);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_INSTANCE]);
glBufferData(GL_ARRAY_BUFFER, sizeof(QUAD), &QUAD[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_SCALEROT]);
glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(float), NULL, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_OFFSET]);
glBufferData(GL_ARRAY_BUFFER, 2 * sizeof(float), NULL, GL_STATIC_DRAW);
glGenVertexArrays(1, &mVBState);
glBindVertexArray(mVBState);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_INSTANCE]);
glVertexAttribPointer(POS_ATTRIB, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(const GLvoid *) offsetof(Vertex, pos));
glVertexAttribPointer(COLOR_ATTRIB, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex),
(const GLvoid *) offsetof(Vertex, rgba));
glEnableVertexAttribArray(POS_ATTRIB);
glEnableVertexAttribArray(COLOR_ATTRIB);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_SCALEROT]);
glVertexAttribPointer(SCALEROT_ATTRIB, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);
glEnableVertexAttribArray(SCALEROT_ATTRIB);
glVertexAttribDivisor(SCALEROT_ATTRIB, 1);
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_OFFSET]);
glVertexAttribPointer(OFFSET_ATTRIB, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0);
glEnableVertexAttribArray(OFFSET_ATTRIB);
glVertexAttribDivisor(OFFSET_ATTRIB, 1);
return true;
}
void Renderer::resize(int w, int h) {
calcSceneParams(w, h);
mAngles = float(drand48() * TWO_PI);
mAngularVelocity = float(MAX_ROT_SPEED * (2.0 * drand48() - 1.0));
mLastFrameNs = 0;
glViewport(0, 0, w, h);
}
void Renderer::calcSceneParams(int w, int h) {
const float CELL_SIZE = 0.5f;
const float scene2clip[2] = {1.0f, fmaxf(w, h) / fminf(w, h)};
int major = w >= h ? 0 : 1;
int minor = w >= h ? 1 : 0;
mScale[major] = CELL_SIZE * scene2clip[0];
mScale[minor] = CELL_SIZE * scene2clip[1];
}
void Renderer::step() {
timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
auto nowNs = now.tv_sec * 1000000000ull + now.tv_nsec;
if (mLastFrameNs > 0) {
float dt = float(nowNs - mLastFrameNs) * 0.000000001f;
mAngles += mAngularVelocity * dt;
if (mAngles >= TWO_PI) {
mAngles -= TWO_PI;
} else if (mAngles <= -TWO_PI) {
mAngles += TWO_PI;
}
float *transforms = mapTransformBuf();
float s = sinf(mAngles);
float c = cosf(mAngles);
transforms[0] = c * mScale[0];
transforms[1] = s * mScale[1];
transforms[2] = -s * mScale[0];
transforms[3] = c * mScale[1];
}
unmapTransformBuf();
mLastFrameNs = nowNs;
}
void Renderer::render() {
step();
glClearColor(0.2f, 0.2f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
draw();
checkGlError("Renderer::render");
}
float *Renderer::mapTransformBuf() {
glBindBuffer(GL_ARRAY_BUFFER, mVB[VB_SCALEROT]);
return (float *) glMapBufferRange(GL_ARRAY_BUFFER,
0, 4 * sizeof(float),
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
}
void Renderer::unmapTransformBuf() {
glUnmapBuffer(GL_ARRAY_BUFFER);
}
void Renderer::draw() {
glUseProgram(mProgram);
glBindVertexArray(mVBState);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 1);
}
Renderer *createRenderer() {
Renderer *renderer = new Renderer;
if (!renderer->init()) {
delete renderer;
return NULL;
}
return renderer;
}
static Renderer *g_renderer = NULL;
extern "C" {
JNIEXPORT jboolean JNICALL Java_com_github_cccxm_gles_model_RectLib_init(JNIEnv *env, jclass type);
JNIEXPORT void JNICALL Java_com_github_cccxm_gles_model_RectLib_resize(JNIEnv *env, jclass type,
jint width, jint height);
JNIEXPORT void JNICALL Java_com_github_cccxm_gles_model_RectLib_step(JNIEnv *env, jclass type);
JNIEXPORT void JNICALL Java_com_github_cccxm_gles_model_RectLib_destroy(JNIEnv *env, jclass type);
};
jboolean
Java_com_github_cccxm_gles_model_RectLib_init(JNIEnv *env, jclass type) {
if (g_renderer) {
delete g_renderer;
g_renderer = NULL;
}
g_renderer = createRenderer();
if (!g_renderer)
return JNI_FALSE;
return JNI_TRUE;
}
void
Java_com_github_cccxm_gles_model_RectLib_resize(JNIEnv *env, jclass type, jint width, jint height) {
if (g_renderer) {
g_renderer->resize(width, height);
}
}
void
Java_com_github_cccxm_gles_model_RectLib_step(JNIEnv *env, jclass type) {
if (g_renderer) {
g_renderer->render();
}
}
void
Java_com_github_cccxm_gles_model_RectLib_destroy(JNIEnv *env, jclass type) {
delete g_renderer;
g_renderer = NULL;
}
#undef LOG_TAG
上面程序使用的顶点着色器代码如下:
#version 300 es
layout(location = 0) in vec2 pos;
layout(location = 1) in vec4 color;
layout(location = 2) in vec4 scaleRot;
layout(location = 3) in vec2 offset;
out vec4 vColor;
void main() {
mat2 sr = mat2(scaleRot.xy, scaleRot.zw);
gl_Position = vec4(sr*pos + offset, 0.0, 1.0);
vColor = color;
}
程序的第一行是版本信息,提示编译器如何编译这个着色器。
layout(location = 0) in vec2 pos;
上述语句in关键字表明了这个参数是从外部获取的(可能来自于用户程序,也可能来自于上一个着色器),而之前的布局限定符layout(location = 0)则是定位该参数的来源,表明这个参数被存放在坐标(索引)为0的布局对象中。
out vec4 vColor;
上述语句表示参数vColor是输出参数,其值将会被交给下一个着色器。
mat2 sr = mat2(scaleRot.xy, scaleRot.zw);
上述语句生成了一个2X2的矩阵,其第一列的值来源于scaleRot的xy分量,第二列的值来源于scaleRot的zw分量。
gl_Position = vec4(sr*pos + offset, 0.0, 1.0);
上述语句中gl_Position是一个全局变量,指代当前正在进行计算的数据的坐标,这里生成了一个4维向量,向量的前两个维度来源于2X2矩阵sr和之前的位置相乘生成的新矩阵与向量offset相加的结果。这一步的作用是计算新位置以达到图形旋转的目的。以下为坐标旋转方程:
其中为旋转后的坐标位置为原始坐标,旋转矩阵
矩阵相乘的计算公式为:对于乘法 有
vColor = color;
上述语句表示,对收到的颜色值不做处理直接发送到下一个着色阶段(对于本程序就是发送给片元着色阶段)。
本例使用的片元着色器代码如下:
#version 300 es
precision mediump float;
in vec4 vColor;
out vec4 outColor;
void main() {
outColor = vColor;
}
该着色器代码的作用就是将收到的颜色值输出,用于后续渲染。
顶点数组中包含了每一个顶点的属性,是保存在用户内核的缓冲区。(语文不好的跟我念:顶点数组是缓冲区,这个缓冲区在用户内核,里面存放的是每一个顶点的属性)。
如果我们有单个的顶点需要处理的话,可以使用下列方法将一个顶点对象映射为一个索引,这样我们就可以在需要的地方使用索引而不是顶点对象去操作数据。OpenGL提供了下列方法去创建单个顶点对象。
void glVertexAttrib1f (GLuint index, GLfloat x);
void glVertexAttrib1fv (GLuint index, const GLfloat *v);
void glVertexAttrib2f (GLuint index, GLfloat x, GLfloat y);
void glVertexAttrib2fv (GLuint index, const GLfloat *v);
void glVertexAttrib3f (GLuint index, GLfloat x, GLfloat y, GLfloat z);
void glVertexAttrib3fv (GLuint index, const GLfloat *v);
void glVertexAttrib4f (GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
void glVertexAttrib4fv (GLuint index, const GLfloat *v);
void glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
void glVertexAttribIPointer (GLuint index, GLint size, GLenum type, GLsizei stride, const void *pointer);
前一个方法允许任何数字类型数组,后一个方法只允许整型数组。
- index:顶点索引,可由用户自行指定。
- size:对于坐标来说不应该使用size(尺度)最贴切的是dimension(维度),取值范围1~4,代表当前顶点使用的坐标系,是线性坐标系(只有x),或是4维空间坐标系(x,y,z,w)。
- type:数据格式,包括常用的数据类型,比如GL_FLOAT等。
- normalized:是否规范化,对于使用glVertexAttribPointer方法的非GL_FLOAT数组,将次标记设置为真后,有符号数将被按比例映射到GL_FLOAT[-1.0,1.0]区间,无符号数将被按比例映射到[0,1.0]区间。
- stride:偏移量,如果该数组不仅仅用于存放坐标的话,需要说明偏移量,否则设置为0即可。
- ptr:用作缓冲区的数组
如果我们使用index
指定了一个顶点数组,那么我们可以使用以下两种方式选择开启或者关闭使用此顶点数组。
void glEnableVertexAttribArray (GLuint index);
void glDisableVertexAttribArray (GLuint index);
在7.4.1的分析中可以看出,一旦我们将一个索引与顶点数组绑定起来,我们就可以在着色器中自由的使用数组中的数据。这种方式使用起来最为方便,但是在某些情况下,我们可能还需要另外的方式(值得注意的是,这种绑定在下一次程序链接时生效)
void glBindAttribLocation (GLuint program, GLuint index, const GLchar *name);
- program:程序对象索引。
- index:定点数组索引。
- name:着色器中使用此数据的变量的名称。
例如:我们可以使用此方法修改7.4.1中的着色器
glBindAttribLocation(program,0,"pos");//用户程序添加绑定
in vec2 pos;//不需要布局限定符
使用顶点数组操作可能需要多次调用8.1中的方法来切换状态。实际上在OpenGL ES 3.0 中,同一时刻总是只有一个活动的顶点数组对象。那么我们可以通过更简单的方法去指定当前正在使用的顶点数组。使用方式就是记录一段你需要对该数组进行的操作,并保存到一个操作对象当中,然后再每次需要时调用此操作记录对象即可。
void glGenVertexArrays (GLsizei n, GLuint *arrays);
- n:需要的顶点数组对象个数
- arrays:保存对象索引的数组
顶点数组对象的目的是简化操作,我们需要将一个数组对象和一系列关于顶点数组和缓存的操作绑定起来。当我们调用完毕下列方法的时候,其后所有关于顶点数组和缓存的操作都将被记录到顶点数组对象的状态当中。
void glBindVertexArray (GLuint array);
- array:需要使用或修改状态的顶点数组对象的索引。
同理,当我们不再需要对一段顶点数组进行操作的时候,需要通知OpenGL删除关于此数组的操作记录。我们可以使用如下方法:
void glDeleteVertexArrays (GLsizei n, const GLuint *arrays);
- n:需要删除的对象个数。
- arrays:包含数组对象索引的数组。
假如我们将数据存储到用户内存,那么每当我们调用绘图方法时,都需要将数据从用户内存复制到图形内存中。但是我们没有必要再每次绘图时都去复制数据。为了节省内存和电力开销,OpenGL ES 提供了顶点缓存技术。OpenGL ES 3.0支持两类缓存区对象:
- GL_ARRAY_BUFFER:标志指定缓冲区用于保存顶点数据
- GL_ELEMENT_ARRAY_BUFFER:标志指定缓冲区用于保存图元索引数据
由于这个缓冲区需要在两个内核空间中共享,所以OpenGL不建议像创建顶点数组那样让用户自己去指定索引。OpenGL提供了一个函数用来获取当前空闲的缓冲区索引。
void glGenBuffers (GLsizei n, GLuint *buffers);
- n:需要得到的索引数量。
- buffers:保存索引的数组。
我们通过8.3.1获取到了可用的缓冲区索引,接下来我们需要将索引与缓冲区类型进行绑定。
void glBindBuffer (GLenum target, GLuint buffer);
如果我们通过8.3.2绑定了缓冲区索引,接下来就可以将数据绑定到缓冲区了。
void glBufferData (GLenum target, GLsizeiptr size, const void *data, GLenum usage);
- target:缓冲区存放的数据结构。
- size:缓冲区大小,单位:字节。
- data:数据源,其内容将会被复制到缓冲区中。
- usage:缓冲区的使用方法。
使用方式主要分为以下三类:
只读型(一次修改多次访问):
读写型(多次修改多次访问):
数据流型(一次修改一次访问):
这三种类型的后缀DRAW表示数据将被送往GPU渲染、READ表述数据会回传给用户程序、COPY表示其同时具有前两种性质。
该方法中的data可以为NULL,如果传入NULL则表示延迟初始化。那么我们应该使用什么方法去再次加载数据呢?接下来介绍一个可以初始化和更新数据的函数:
void glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const void *data);
- offset:缓冲区待修改数据偏移量。
- size:被修改的数据存储字节数。
- data:数据源。
当我们使用完缓冲区后,应当命令OpenGL销毁这个区域:
void glDeleteBuffers (GLsizei n, const GLuint *buffers);
- n:要删除的缓冲区个数。
- buffers:包含缓冲区索引的数组。
当我们需要操作保存在缓冲区的数据时会发现,仅通过8.4节的方式是不能高效的满足需求的。因为我们需要在程序中保存一个引用并不断的调用glBufferSubData
方法去更新缓冲区,此时缓冲区的存在完全没有意义。为了你补这个缺陷,OpenGL ES 3.0 中提供了映射缓冲区对象的概念,通过映射缓冲区对象,我们可以直接获得缓冲区中的数据引用——在某些共享内存的架构上,映射缓冲区甚至可以直接得到GPU存储缓冲区地址的直接指针——从而提高性能。
我们可以通过以下指令获取指向所有或部分缓存的指针,如果出现错误,该函数将返回NULL。
void* glMapBufferRange (GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
- target:缓冲区存放的数据结构。
- offset:缓冲区数据存储的偏移量,单位:字节。
- length:需要的缓冲区字节数。
- access:访问方式,是如下方式的一种或组合。
缓冲区对象的访问方式:
必须包含的选项:
可选的选项:
当我们不再使用缓冲区映射时,需要使用如下函数解除当前正在使用的映射关系
GLboolean glUnmapBuffer (GLenum target);
- target:缓冲区存放的数据结构。
如果我们希望对映射的修改能够立即同步到缓冲区,可以使用glUnmapBuffer解除映射,然后映射中的数据就会被自动刷新到缓冲区中。但是这种方式缺点明显,比如对于连续刷新来说操作繁琐,而且,假如只修改一小部分数据,这种方式却刷新了整个缓冲区。OpenGL ES 3.0提供了部分刷新的方法
void glFlushMappedBufferRange (GLenum target, GLintptr offset, GLsizeiptr length);
- target:缓冲区存放的数据结构。
- offset:缓冲区数据存储的偏移量,单位:字节。
- length:从偏移点开始刷新的长度。
OpenGL提供了如下所示的函数来在两个缓冲区之间复制数据
void glCopyBufferSubData (GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);