本文主要介绍如何使用OpenGL实现着色器程序,首先会简单介绍着色器在OpenGL渲染管线中是什么个位置,接着是介绍可以通过GLSL语言实现的两类着色器:顶点着色器和片段着色器,最后使用OpenGL实现了个DEMO(在文章最后面,提供了下载),演示如何使用OpenGL接口创建着色器程序。
会例代码下载地址:https://github.com/twinklingstar20/twinklingstar_cn_demo_basic_shader/
一. 简介
OpenGL是把图像数据和几何数据发送给图形硬件,进行一系列处理,最终显示到屏幕上。我们可以随意的设置管线上某些阶段的状态、参数等,但是OpenGL图形管线的基本操作和顺序是不能改变的,即是“固定渲染管线”。通过OpenGL着色语言和支持它的OpenGL API接口,就允许应用程序开发者使用高级编程语言来实现自己的着色器,这就使得开发者能够使用硬件来实现更丰富的渲染效果,这种就称为“可编程渲染管线”。在应用程序当中,这两种管线不能同时使用,只能使用其中的一种渲染管线。
使用OpenGL的着色器,可以丰富渲染效果,能实现的功能包括:
(1) 更加真实的材质-金属、石头、木头等 (2) 更加真实的光照效果-区域光照、柔和阴影等 (3) 自然现象-火、烟、水、云等 (4) 高级渲染效果-全局光照、光线追踪器等 (5) 非照片级材质-绘画效果、笔写效果等 (6) 纹理内存新的一些用途-向量的存储、模糊值、多项式系数等 (7) 过程纹理-动态生成的2D、3D纹理等 (8) 图像处理-卷积、复杂混合、模糊掩盖锐化处理(unsharp masking)等 (9) 动画效果-关键帧插值、粒子系统、程序定义的运动等 (10) 自定义的抗锯齿方法(11) 一般的计算-排序、数学建模、流体动力学等
上面所述的许多技术原先只能通过软件来实现,如果通过OpenGL提供的GLSL语言和接口,可以将那些功能放在硬件层实现,明显渲染效果会得到提升,而且减轻了CPU的负担。
二. 顶点处理器(Vertex Processor)和片断处理器(Fragment Processor)
在《OpenGL原理介绍》一文中,对OpenGL渲染管线的各个阶段进行了介绍,并不是渲染管线中的各个阶段都能通过着色语言实现的着色器来替换。GLSL着色语言能实现两类处理器的功能:顶点处理器和片断处理器。GLSL语言经过很精细地设计,使硬件能够并行的处理顶点和片段,这就给硬件产商实现更快的图形硬件。图1,显示了OpenGL中可编程渲染管线的逻辑图。
图1. OpenGL中的可编程渲染管线的逻辑图
2.1 顶点处理器
传统的顶点处理器能对顶点值和它相关的数据进行处理,主要操作有:
(1) 顶点变换 (2) 法向量变换和单位化 (3) 生成相应的纹理坐标 (4) 纹理坐标变换 (5) 雾坐标 (6) 光照计算(7) 等等
运行在顶点处理器上的着色器就称为顶点着色器(Vertex Shader),通过glVertex*()等函数指定的顶点数据和跟它相关的法向量(glNormal*())、颜色(glColor*())等属性信息都会经过顶点处理器的处理,传给下一个阶段。但是也不是顶点管线中所有的操作都可以通过顶点着色器替换,在顶点着色器完成后,下面的几个操作还会依次进行,对顶点处理管线的介绍参见文章《顶点渲染管线》:
(1) 透视除法 (2) 视口映射 (3) 图元装配 (4) 视景体裁剪 (5) 背面剔除 (6) 多边形模式处理 (7) 多边形偏移(8) 深度范围截取
2.2 片断处理器
前面说过,一个片断对应一个像素,但是它不是像素值,它有纹理坐标、深度等信息,片断通过一系列的计算,得到像素值。传统的片断处理器,能完成下面功能:
(1) 雾 (2) 提取纹理单元,用于纹理贴图 (3) 颜色混合 (4) Alpha测试(8) 等等
运行在片断处理器上的着色器就称为片断着色器(Fragment Shader),片断着色器不能改变片段的(x,y)位置,不能替换要求多个片断信息的操作,因为为了尽可能的支持片断处理级别的并行,片断着色器只能进行访问单个片段的操作,对周围的片断的访问是被禁止的。但是与顶点着色器类似,片断处理器不能替换OpenGL像素处理管线中所有的操作,对像素处理管线的介绍参见文章《像素相关的操作》。即不管是否使用片断着色器,OpenGL总是会执行下面的操作:
(1) 平面(flat)或者光滑着色(smooth) (2) 像素覆盖计算 (3) 像素所有权测试 (4) 裁剪操作 (5) 点画模式应用 (6) Alpha测试 (7) 深度测试 (8) 模板测试 (9) Alpha混合 (10) 像素的逻辑操作 (11) 颜色值的抖动(12) 颜色掩码操作
三. OpenGL着色器程序
windows中vs平台支持的OpenGL版本是1.1,但是GLSL语言至少要求OpenGL 2.0的版本,所以需要安装上glew库。没有专门编译GLSL语言的编译器,着色器程序的编译和链接是在硬件端进行的,是通过OpenGL提供的接口,把着色器源代码传入图形硬件中进行编译、链接、运行,编译和链接的结果也可以通过OpenGL接口返回。
3.1 OpenGL接口
GLuint glCreateShader(GLenum shaderType);
void glShaderSource(GLuint shader,GLsizei count,const GLchar **string,const GLint *length);
void glGetShaderSource(GLuint shader,GLsizei bufSize,GLsizei *length,GLchar *source);
void glCompileShader(GLuint shader);
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
void glGetShaderInfoLog(GLuint shader,GLsizei maxLength,GLsizei *length,GLchar * infoLog);
GLuint glCreateProgram(void);
void glAttachShader(GLuint program,GLuint shader);
void glDetachShader(GLuint program,GLuint shader);
void glLinkProgram(GLuint program);
void glGetProgramiv(GLuint program,GLenum pname, GLint *params);
void glGetProgramInfoLog(GLuint program,GLsizei maxLength,GLsizei *length,GLchar *infoLog);
void glUseProgram(GLuint program);
void glDeleteProgram(GLuint program);
void glDeleteShader(GLuint shader);
GLboolean glIsShader(GLuint shader);
GLboolean glIsProgram(GLuint program);
void glValidateProgram(GLuint program);
3.2 着色器创建流程
图2. 着色器创建流程图
在应用程序当中使用顶点或者片断着色器,需要执行的步骤如下所示:
(1) 创建一个着色器对象 (2) 把着色器源代码编译为目标代码(3) 验证这个着色器已经成功通过编译
接着,把创建好的着色器加入到一个着色器程序当中
(1) 创建一个着色器程序 (2) 把适当的着色器对象连接到这个着色器程序中 (3) 链接到着色器程序 (4) 验证这着色器阶段已经成功能完成(5) 使用着色器进行顶点或者片断处理
3.3 OpenGL着色器使用例子
参考了【4】提供的代码,实现了个简单的DEMO。下面是一个简单的创建着色器程序的代码片断,具体的演示代码,参见文章最后面的附录中,代码的演示结果如图3所示。
GLuint genShader(GLenum type,const char* fileName,char*& log)
{
//创建着色器对象
GLuint shader = glCreateShader(type);
//从文件中读取着色器的实现代码
char* shaderSource = readShaderSource(fileName);
if( !shaderSource )
return 0;
const char* ptrShaderSource = shaderSource;
//将着色器的实现代码与创建的着色器对象绑定
glShaderSource(shader,1,&ptrShaderSource,NULL);
free(shaderSource);
//编译着色器对象
glCompileShader(shader);
GLint status = 0;
//查看编译状态
glGetShaderiv(shader,GL_COMPILE_STATUS,&status);
if( !status )
{
GLint length;
//读取日志信息的长度
glGetShaderiv(shader,GL_INFO_LOG_LENGTH,&length);
log = (GLchar*)malloc(length);
//读取日志信息
glGetShaderInfoLog(shader,length,&length,log);
#if 1
printf("%s\n",log);
#endif
//删除着色器对象
glDeleteShader(shader);
return 0;
}
return shader;
}
GLuint linkProgram(GLuint* shader,int shaderNum,char*& log)
{
//创建着色器程序
GLuint program = glCreateProgram();
int i;
//往着色器程序中加入着色器对象
for( i=0 ; i<shaderNum ; i++ )
glAttachShader(program,shader[i]);
//链接着色器程序
glLinkProgram(program);
GLint status;
//查看链接状态
glGetProgramiv(program,GL_LINK_STATUS,&status);
if( !status )
{
GLint length;
//读取日志信息的长度
glGetProgramiv(program,GL_INFO_LOG_LENGTH,&length);
log = (GLchar*)malloc(length);
//读取日志信息
glGetProgramInfoLog(program,length,&length,log);
#if 1
printf("%s\n",log);
#endif
//删除着色器对象
glDeleteProgram(program);
return 0;
}
return program;
}
void useProgram(GLuint program)
{
//运行创建成功的着色器程序
glUseProgram(program);
}
bool setShaders()
{
char* log = NULL;
//创建一个顶点着色器对象
GLuint vertexShader = genShader(GL_VERTEX_SHADER,"toon.vert",log);
if( !vertexShader )
{
free(log);
return false;
}
//创建一个片断着色器对象
GLuint fragmentShader = genShader(GL_FRAGMENT_SHADER,"toon.frag",log);
if( !fragmentShader )
{
free(log);
glDeleteShader(vertexShader);
return false;
}
//把创建好的顶点和片断着色器对象链接到着色器程序中
GLuint shader[2] = {vertexShader,fragmentShader};
GLuint program = linkProgram(shader,2,log);
if( !program )
{
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
free(log);
return false;
}
//使用创建成功的着色器程序
useProgram(program);
return true;
}
四.参考
【1】 《OpenGL Shading Language, Third Edition》
【2】 《OpenGL Programming Guide-Sixth Edition-The Official Guide Learning OpenGL, Version 2.1》
【3】 http://www.opengl.org/sdk/docs/man2/
【4】 http://www.lighthouse3d.com/tutorials/glsl-tutorial/setup-for-glsl-example/
原文链接:http://www.twinklingstar.cn/2013/793/basic-shader/