[关闭]
@cxm-2016 2017-04-05T06:28:12.000000Z 字数 8162 阅读 6645

NDK开发OpenGL ES 3.0(三)——着色器基础

OpenGL-ES

版本:2
作者:陈小默
版权声明:禁止商用,禁止转载

该文章仅被发布于作业部落简书


上一篇:NDK开发OpenGL ES 3.0(二)——初见GLES,第一个三角形


参考书目:
[1]Donald Hearn,M.Pauline Barker.计算机图形学 第四版(蔡士杰 译).北京:电子工业出版社
[2]Dave Shreiner,Graham Sellers.OpenGL编程指南 第八版(王锐 译).北京:机械工业出版社
[3]Dan Ginsburg,Budirjanto Purnomo.OpenGL ES 3.0 编程指南 第二版(姚军 译).北京:机械工业出版社


五、OpenGL管线

OpenGL的处理是以命令流的形式进行,这种方式被形象的称为管线,数据从管子的一端进入,从另一端输出。其他的一切操作都是在整个流上进行。


5.1 固定功能渲染流水线

OpenGL最初的内部实现流程时固定的,也就是说,它们无论接收到怎样的输入数据,都只会执行相同的操作——因此,这样的工作方式被称为固定功能OpenGL流水线。

5.2 可编程功能渲染流水线

传统的固定功能流水线的一个问题是其不能很好的现代图形处理硬件相结合。为了充分利用显卡设备,OpenGL将其流水线固定的顶点操作和片元操作使用用户编程的方式取代了。
图5.2-1

5.3 OpenGL管线工作流程

  • 1、向OpenGL传输数据
    OpenGL需要将所有的数据都保存到缓存对象(buffer object)中,它相当于OpenGL服务端维护的一块内存区域。
  • 2、将数据传输到OpenGL
    在缓存初始化完成之后,通过调用绘制命令将顶点数据传送到OpenGL服务端。我们可以将一个顶点视为一个需要统一处理的数据包。这个包中的数据可以是我们需要的任何数据。
  • 3、顶点着色(vertex shading stage)
    该阶段将接收你顶点缓存中给出的顶点数据。对于绘制命令传输的每个点,OpenGL都会用一个顶点着色器来处理顶点相关的数据(至少会计算出每个顶点经过建模和观察投影变换后在裁剪空间的坐标)。传递着色器会将数据复制并传递到下一个着色阶段。对于复杂的场景也会有相应的着色方式。
  • 4、细分着色(tessellation shading stage)
    这是一个可选阶段,与应用程序中显式地指定几何图元的方法不同,它会在OpenGL管线内部生成新的几何体。这个阶段启用之后,会收到来自顶点着色器阶段的输出数据,并且对收到的数据进一步处理。
  • 5、 几何着色(geometry shading stage)
    该阶段也是一个可选阶段,假如启用了该阶段,它会收到来自顶点着色或者细分着色(如果它也被启用)的数据,然后在OpenGL管线内部对几乎所有的几何图形进行修改。几何着色器不同于前两个着色器,它不仅仅只能修改输入数据,甚至可以创建新的图元交给后续的流水线处理。该阶段作用于每个独立的几何图元。
  • 6、图元装配
    该阶段会将这些顶点与相关的集合图元之间组织起来,准备下一步的剪切和光栅化工作。
  • 7、剪切
    该阶段用来保证相关的像素不会在窗口之外进行绘制。也就是说将不能被窗口显示的部分去除。
  • 8、光栅化
    图元信息最终被传递给光栅化单元,生成对应的片元。
  • 9、片元着色(fragment shading stage)
    这是最后一个可以通过编程控制屏幕显示颜色的阶段。在这个阶段中会使用着色器来计算片元的最终颜色和它的深度值。(顶点着色决定了一个图元应该处于屏幕的什么位置,片元着色决定了某个片元的颜色应该是什么)。
  • 10、片元操作
    在该阶段会使用深度测试和模版测试的方式来决定一个片元是否是可见的。如果一个片元成功的通过了所有激活的测试,那么他就可以直接被绘制到帧缓存中了,它所对应的像素的颜色值会被更新,如果开启了融合模式,那么对应的片元颜色会与该像素的颜色相叠加,形成一个新的颜色并写入帧缓存中。

5.4 在OpenGL中使用着色器

在OpenGL中使用着色器的步骤如下:

  • 创建着色器对象
  • 关联源码到着色器对象
  • 编译着色器
  • 创建程序对象
  • 把着色器对象关联到程序对象
  • 链接程序

5.4.1 创建着色器对象

OpenGL ES要求程序必须提供一个顶点着色器和一个片元着色器,否则程序将不会进行任何的绘制操作。为了创建着色器程序,需要创建两个着色器对象,该对象实际是一个无符号整型,是其内部真实对象的索引。

  1. GLuint vShader,fShader;
  2. //创建一个顶点着色器,并将索引交给vShader
  3. vShader = glCreateShader(GL_VERTEX_SHADER);
  4. //创建一个片元着色器,并将索引交给fShader
  5. fShader = glCreateShader(GL_FRAGMENT_SHADER);

5.4.2 关联源码到着色器

  1. GLchar * vSource,fSource;//这里是着色器代码的引用
  2. //将vSource指向的源码关联到vShader索引所在的对象,第二个参数表示vSource指向了几个着色器对象
  3. //最后一个参数表示源码的长度,输入NULL或者0表示以默认的字符串结尾标志'\0'作为源码结束标记
  4. glShaderSource(vShader,1,(const GLchar **)&vSource,NULL);
  5. glShaderSource(fShader,1,(const GLchar **)&fSource,NULL);

5.4.3 编译着色器

  1. glCompileShader(vShader);
  2. glCompileShader(fShader);
  3. //编译结果
  4. GLint status;
  5. //获取编译结果,并且将结果赋值给status
  6. glGetShaderiv(vShader,GL_SHADER_COMPILER,&status);
  7. if(status!=GL_TRUE){
  8. //顶点着色器编译失败
  9. }
  10. glGetShaderiv(fShader,GL_SHADER_COMPILER,&status);
  11. if(status!=GL_TRUE){
  12. //片元着色器编译失败
  13. }

5.4.4 创建程序对象

  1. GLuint program;
  2. //创建一个程序,并且将索引赋值给program
  3. program = glCreateProgram();

5.4.5 关联着色器并链接程序

  1. glAttachShader(program,vShader);
  2. glAttachShader(program,fShader);
  3. glLinkProgram(program);
  4. //存储日志长度
  5. GLint length;
  6. GLsizei num;
  7. //保存日志
  8. GLchar *log;
  9. //获取日志信息
  10. glGetProgramiv(program,GL_INFO_LOG_LENGTH,&length);
  11. if(length>0){
  12. log = (GLchar*)malloc(sizeof(GLchar)*length);
  13. //从日志缓存中取出关于program length个长度的日志,并保存在log中
  14. glGetProgramInfoLog(program,length,&num,log);
  15. /*
  16. * 在这里查看日志
  17. */
  18. free(log);
  19. }

5.5 使用着色器程序

在OpenGL中我们可以持有多个着色器程序,如果要使用一个着色器程序,我们可以使用如下代码

  1. glUseProgram(program);

如果希望“冻结”一个着色器程序,则再次调用上述方法,只不过将参数设置为0即可glUseProgram(0)

当我们使用完一个着色器程序之后,为了节约资源,我们需要删除着色器对象,下面是一个例子

  1. glDeleteShader(fShader);
  2. glDeleteProgram(program);
  3. glDeleteShader(vShader);

我们可以看到,这里在删除程序对象的前后分别删除了两个着色器对象,在前后删除实际上有什么不一样吗?为了程序安全,如果我们在程序对象还存在的情况下删除了一个着色器对象,实际上OpenGL并不会真正的删除这个着色器对象,而是会给这个着色器设置一个待删除的标记。等到程序对象被删除后,这个着色器对象才会被真正的删除。而在程序对象已经被删除的情况下,这个着色器对象会被立即删除。

5.6 对着色器的封装

看完5.4之后,发现其中的过程相当繁琐,对于以懒出名,懒出奇迹的程序猿,怎么可能每次写这么多代码呢,接下来我们需要封装一个加载着色器的程序(思路参考Google官方示例)

5.6.1 创建一个esUtil.h头文件

将一些通用的方法声明移到此处

  1. #ifndef GLES_ESUTIL_H
  2. #define GLES_ESUTIL_H
  3. #include <GLES3/gl3.h>
  4. #include <android/log.h>
  5. #include <jni.h>
  6. #ifndef LOG_TAG
  7. #define LOG_TAG "ES_LIB"
  8. #endif
  9. #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
  10. #include <stdlib.h>
  11. //检查当前程序错误
  12. bool checkGlError(const char* funcName);
  13. //获取并编译着色器对象
  14. GLuint createShader(GLenum shaderType, const char* src);
  15. //使用着色器生成着色器程序对象
  16. GLuint createProgram(const char* vtxSrc, const char* fragSrc);
  17. #endif //GLES_ESUTIL_H

5.6.2 创建一个esUtils.cpp

在这个文件中对头文件中的声明进行定义

  1. #include "esUtil.h"
  2. bool checkGlError(const char* funcName) {
  3. GLint err = glGetError();
  4. if (err != GL_NO_ERROR) {
  5. ALOGE("GL error after %s(): 0x%08x\n", funcName, err);
  6. return true;
  7. }
  8. return false;
  9. }
  10. GLuint createShader(GLenum shaderType, const char* src) {
  11. GLuint shader = glCreateShader(shaderType);
  12. if (!shader) {
  13. checkGlError("glCreateShader");
  14. return 0;
  15. }
  16. glShaderSource(shader, 1, &src, NULL);
  17. GLint compiled = GL_FALSE;
  18. glCompileShader(shader);
  19. glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
  20. if (!compiled) {
  21. GLint infoLogLen = 0;
  22. glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLen);
  23. if (infoLogLen > 0) {
  24. GLchar* infoLog = (GLchar*)malloc(infoLogLen);
  25. if (infoLog) {
  26. glGetShaderInfoLog(shader, infoLogLen, NULL, infoLog);
  27. ALOGE("Could not compile %s shader:\n%s\n",
  28. shaderType == GL_VERTEX_SHADER ? "vertex" : "fragment",
  29. infoLog);
  30. free(infoLog);
  31. }
  32. }
  33. glDeleteShader(shader);
  34. return 0;
  35. }
  36. return shader;
  37. }
  38. GLuint createProgram(const char* vtxSrc, const char* fragSrc) {
  39. GLuint vtxShader = 0;
  40. GLuint fragShader = 0;
  41. GLuint program = 0;
  42. GLint linked = GL_FALSE;
  43. vtxShader = createShader(GL_VERTEX_SHADER, vtxSrc);
  44. if (!vtxShader)
  45. goto exit;
  46. fragShader = createShader(GL_FRAGMENT_SHADER, fragSrc);
  47. if (!fragShader)
  48. goto exit;
  49. program = glCreateProgram();
  50. if (!program) {
  51. checkGlError("glCreateProgram");
  52. goto exit;
  53. }
  54. glAttachShader(program, vtxShader);
  55. glAttachShader(program, fragShader);
  56. glLinkProgram(program);
  57. glGetProgramiv(program, GL_LINK_STATUS, &linked);
  58. if (!linked) {
  59. ALOGE("Could not link program");
  60. GLint infoLogLen = 0;
  61. glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLen);
  62. if (infoLogLen) {
  63. GLchar* infoLog = (GLchar*)malloc(infoLogLen);
  64. if (infoLog) {
  65. glGetProgramInfoLog(program, infoLogLen, NULL, infoLog);
  66. ALOGE("Could not link program:\n%s\n", infoLog);
  67. free(infoLog);
  68. }
  69. }
  70. glDeleteProgram(program);
  71. program = 0;
  72. }
  73. exit:
  74. glDeleteShader(vtxShader);
  75. glDeleteShader(fragShader);
  76. return program;
  77. }

5.6.3 使用方式

我们在使用这个工具包的时候,需要将其加入到我们的CMakeLi.txt中,就上一篇例子而言,只需要增加一个路径即可

  1. add_library( triangle-lib
  2. SHARED
  3. src/main/cpp/esUtil.cpp
  4. src/main/cpp/triangle-lib.cpp)

接下来,triangle-lib.cpp就可以精简为这个样子,是不是更方便我们专注于业务逻辑

  1. #include "esUtil.h"
  2. static const char VERTEX_SHADER[]=...;
  3. static const char FRAGMENT_SHADER[]=...;
  4. static const GLfloat VERTEX[]=...;
  5. GLuint program;
  6. extern "C"{
  7. JNIEXPORT jboolean JNICALL Java_com_github_cccxm_gles_model_TriangleLib_init(JNIEnv* env, jobject obj);
  8. JNIEXPORT void JNICALL Java_com_github_cccxm_gles_model_TriangleLib_resize(JNIEnv* env, jobject obj, jint width, jint height);
  9. JNIEXPORT void JNICALL Java_com_github_cccxm_gles_model_TriangleLib_step(JNIEnv* env, jobject obj);
  10. }
  11. JNIEXPORT jboolean JNICALL Java_com_github_cccxm_gles_model_TriangleLib_init(JNIEnv* env, jobject obj){
  12. ...
  13. }
  14. JNIEXPORT void JNICALL Java_com_github_cccxm_gles_model_TriangleLib_resize(JNIEnv* env, jobject obj, jint width, jint height){
  15. ...
  16. }
  17. JNIEXPORT void JNICALL Java_com_github_cccxm_gles_model_TriangleLib_step(JNIEnv* env, jobject obj){
  18. ...
  19. }

六、GLSL语言概述

GLSL语言的全称为OpenGL Shading Language,GLSL具备了C++和Java的语法特性。


6.1 语言说明

由于其语法规则与C++或者Java类似,所以这里只介绍不同点。

6.1.1 矢量

在GLSL中,vec关键字用来标识一个矢量,每一个矢量最多可以有4个维度。其规则为

  1. [type]vec[dimension]

[type]可选:

[dimension]表示维度,其取值范围[2,3,4]

比如dvec2表示这个变量是一个double类型的2维向量,idev4表示一个int类型的4维向量。

GLSL支持通过分量的形式访问矢量

分量访问符 符号描述
(x,y,z,w) 此时向量表示坐标
(r,g,b,a) 此时向量表示颜色
(s,t,p,g) 此时向量表示纹理坐标

注意:同一个操作只能选择一种方式访问分量

6.1.2 矩阵

在GLSL中矩阵代表了一个二维数组,其表示形式为

  1. [type]mat[c][r]

类型同6.1.1但是仅仅支持float和double两种类型,其列和行的取值范围都是[2,3,4]如果行列数相同,可以使用简写

  1. mat2 m1;//表示float类型的2X2的矩阵
  2. dmat3x2 m2;//表示double类型的3列2行的矩阵

6.1.3 数组

GLSL在数组的处理上和Java类似,这样便消除了C语言数组可能会造成的安全隐患。它有一个隐藏的方法可以返回数组的个数,这个个数是在编译完成时就确定的,所以我们可以使用下列方式循环一个数组

  1. float coeff[3]=float[]{1.0,2.0,3.0};
  2. for(int i=0;i<coeff.length();i++){
  3. //TODO
  4. }

注意:GLSL不支持C++中的指针和引用

6.1.4 存储限制符

在GLSL中,可以通过给变量设置修饰符来改变自己的行为。GLSL中定义了6中修饰符:

修饰符 描述
const 将一个变量定义为只读类型,如果它初始化时使用的是一个编译时常量,那么它自身也会成为编译时常量
in 设置这个变量为着色器的输入变量
out 设置这个变量为着色器的输出变量
uniform 设置这个变量为可在外部程序访问的全局变量
buffer 设置应用程序共享的一块可读写的内存空间
shared 设置变量是本地工作组共享的

6.1.5 如何在外部程序使用uniform变量

OpenGL提供了这么一个函数

  1. GLint glGetUniformLocation (GLuint program, const GLchar *name);

该方法返回着色器程序中与名称name相对应的uniform变量的索引。如果没有相符的数据,则返回-1;

使用示例:

  1. GLint index;
  2. GLfloat value;
  3. index = glGetUniformLocation(program,"name");
  4. glUniform1f(index,value);

下一篇NDK开发OpenGL ES 3.0(四)——旋转的彩色方块

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注