Opengl ES之着色器

879 阅读8分钟

前言

在前面我们介绍了 OpenglEs之EGL环境搭建 ,在后面的例子中,我们将无可避免地需要使用到着色器。而着色器才是Opengl的灵魂所在,有了着色器才有了Opengl天马行空的世界。

图形渲染管线

要想理解什么是着色器以及着色器的作用就必须先了解下图形渲染管线。

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。将OpenGL中的3D世界转换到屏幕窗口中的2D世界中展示出来的过程称为图形渲染管线。 也可以说将一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程就是图形渲染管线。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。

图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。

着色器

着色器是进行三维图形学编程的先进方法,从某种意义上来说 Shader 的出现是图形学中的一种”退步”,因为在这之前所有的功能都直接由固定管线提供,而开发人员只需要为其指定参数(如光照属性、旋转角度等),但是由于 Shader 的出现这些功能现在都需要开发者自己通过 Shader 实现。 尽管如此,这种可编程性能够提供给开发者更多的灵活性和创造性。

下图是Opengl图形渲染管线的抽象过程:
图形渲染管线的抽象过程

蓝色部分代表的是我们可以注入自定义的着色器的部分。也就说我们通过自定义顶点着色器、几何着色器和片段着色器就可以实现我们想要各种效果,同时几何着色器又是可选的,一般使用默认的即可,因此我们只需将重点放在顶点着色器和片段着色器即可。

下面参照上图,简单介绍一下各个阶段的主要功能职责:

  • 顶点着色器
    图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理,例如矩阵变换等。

  • 图元装配
    图元装配阶段将顶点着色器输出的所有顶点作为输入,并所有的点装配成指定图元的形状。例如绘制正方形时,就是通过两个三角形组装成正方形,绘制多面体时,就是将n多个三角形组装成多面体的过程。

  • 几何着色器
    图元装配阶段的输出会传递给几何着色器。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。

  • 光栅化
    几何着色器的输出会被传入光栅化阶段,这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

  • 片段着色器
    片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

  • 测试与混合
    这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。 所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

GLSL

OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的。

在OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器,因为GPU中没有默认的顶点/片段着色器。

我们来看一个最简单的顶点着色器的例子:

#version 300 es
in vec4 aPos;
void main()
{
    gl_Position = aPos;
}

GLSL的语法与C语言很类似,相信有过编程经验的伙伴们大概都能看懂以上这个着色器。

顶点着色器的第一行#version 300 es首先声明了着色器版本号,300的意思是代表着色器语言需要是3.0之后的版本。

第二行in vec4 aPos;声明了一个名为aPos的4个分量的输入变量。其中int代表的是定义输入变量,也就是说可以利用这个变量通过CPU向GPU传递数据, 与之对应的是使用out关键字声明的输出变量。注意,int和out是Opengl 3.0之后所做的修改,在Opengl 2.0中与之对应的关键字是attribute和varying,也就是说在Opengl 3中attribute改成了in而varying改成了out。

后面定义的就是一个main函数,这个没什么好解析的,就像所有语言程序一样,作为程序的一个入口,而gl_Position是Opengl中顶点着色器的一个内建输出变量,代表的是最终经过处理后的顶点坐标。

下面我们再来看一个简单的片段着色器的例子:

#version 300 es
precision mediaump float;
out vec4 FragColor;
void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

正如顶点着色器那样,片段着色器的第一行也是声明着色器的版本。第二行precision mediaump float;则是声明着色器中浮点变量的默认精度。

在Opengl ES 3的版本中我们需要手动地使用out关键字声明输出着色器的颜色变量,例如上述例子中的第三行,而在Opengl ES2中这不是必须的,因为在Opengl ES2中直接使用内置的变量gl_FragColor即可。 至于为什么这样做,笔者也不得而知...

这里只是通过两个简单的例子让大家对着色器有一个初步的了解,更多关于着色器GLSL的编写规范大家可以参考相关的资料或者书籍进行自我学习,知识繁多,不是三言两语可描述清楚,这里就不再累赘。。。

着色器链接与使用

上面我们对着色器有了一个初步的认识,那么这些着色器小程序如何使用起来呢?
着色器使用流程

如上图所示,着色器小程序需要通过创建、编译、链接、使用这些重要步骤。这些步骤也没什么好解析的,就像一套规则,大家遵循就是了。

以下是笔者写的一个加载使用着色器的一个工具ShaderUtils.h:

#ifndef NDK_OPENGLES_LEARN_SHADERUTILS_H
#define NDK_OPENGLES_LEARN_SHADERUTILS_H

#include "GLES3/gl3.h"
#include "Log.h"

static int loadShaders(int shaderType, const char *code)
{
    // 按照类型,创建着色器
    int shader = glCreateShader(shaderType);
    // 加载着色器代码
    glShaderSource(shader, 1, &code, 0);
    // 编译
    glCompileShader(shader);
    // 检测编译状态
    int result = GL_FALSE;
    glGetShaderiv(shader,GL_COMPILE_STATUS,&result);
    if(result != GL_TRUE){
        GLint infoLen = 0;
        glGetShaderiv(shader,GL_INFO_LOG_LENGTH,&infoLen);
        char error[infoLen + 1];
        // 获取编译错误
        glGetShaderInfoLog(shader, sizeof(error) / sizeof(error[0]),&infoLen,error);
        LOGE("着色器编译失败:%s,%s",error,code);
    }
    return shader;
}

static int createProgram(const char *vertex , const char * fragment)
{
    int vertexShader = loadShaders(GL_VERTEX_SHADER, vertex);
    int fragmentShader = loadShaders(GL_FRAGMENT_SHADER, fragment);
    int program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    glLinkProgram(program);
    // 获取一下是否有错误
    GLint status = GL_FALSE;
    glGetProgramiv(program,GL_LINK_STATUS,&status);
    if(status != GL_TRUE){
        GLint infoLen = 0;
        glGetProgramiv(program,GL_INFO_LOG_LENGTH,&infoLen);
        char error[infoLen + 1];
        glGetProgramInfoLog(program,sizeof(error) / sizeof(error[0]) ,&infoLen,error);
        LOGE("着色器程序链接失败:",error);
    }
    // 更严谨一点可通过glValidateProgram()验证着色器程序
    // 删除着色器
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    return program;
}

#endif //NDK_OPENGLES_LEARN_SHADERUTILS_H

关于着色器的介绍就到这里,要想用好着色器,需要在弄懂GLSL语法之后多加实践...

参考资料

《LearnOpenGL CN》
《OPENGL ES 3.0编程指南》
...

往期笔记

OpenglEs之EGL环境搭建

关注我,一起进步,人生不止coding!!!