OpenGl 中的 Shader在一些中文书籍或资料中都被翻译为“着色器”, 单从字面意思也看不出Shader到底是什么,Shader实际上就是一段代码,用于完成特定功能的一个模块。Shader分为Vertex Shader(顶点着色器)和Pixel Shader(像素着色器)两种,其中Pixel Shader在本文中又被称为Fragment Shader(片段着色器)
从本节开始我们將使用Shader来实现游戏中的各种特效,Shader是现代3D图形学中重要的渲染技术。从某种程度上,你可以抱怨这种做法是比较落后的,因为固定渲染管道(fixed function pipeline)提供的3d功能本来只需要开发人员指定配置参数(例如光照属性、旋转值)就可以了,现在都要通过编写Shader代码来实现。然而这种可编程方式为编写程序提供了更大的灵活性和创新性。
OpenGl的可编程管道可以由下图直观的表达:
顶点处理器(vertex processor)负责执行每个通过管道的顶点的vertex shader(数量取决于调用绘图函数是传入的参数),Vertex shaders并不知道渲染图元的拓扑结构(是绘制四边形还是三角形?),因此顶点处理器(vertex processor)是必须的。每个顶点只进入顶点处理器一次,经过变换后沿着管道执行下一步处理。(注:所谓的管道是指从顶点输入到渲染到屏幕上经历的整个过程)
接下来是几何处理器(geometry processor)阶段。在这个阶段,一组连续的顶点如何构成图形的信息将会提供给Shader。这使得我们需要考虑除了顶点自身之外的额外信息。几何处理器(geometry processor)能够改变调用绘图函数时指定的拓扑结构(点、线、三角形等),例如你可以將它用在一组点上,將原指定拓扑结构生成的四边形变成两个三角形(公告牌技术的应用)。此外,你还可以让几何处理器(geometry processor)忽略多个指定的点,让这些点以调用绘图函数时指定的拓扑结构来绘制图形。
管道中的下一阶段为裁剪阶段(Clipper),这是一个任务较为简单的固定功能单元,会裁剪掉上一节教程中的正方形以外的图形元素,除此之外Z轴方向上的近裁剪面和远裁剪面以外的部分也会被裁剪掉。能够映射到屏幕的顶点不会被裁剪,光栅化程序会根据绘图函数指定的拓扑结构(三角形、四边形等)將图形渲染在屏幕上。例如:拓扑结构指定为三角形时光栅化程序会找到三角形内部的所有点并对它们进行渲染。对于每个点光栅化程序会调用片段处理器。在这里你可以通过对纹理采样(或者使用其他技术)确定像素的颜色。
上面的三个可编程阶段是可有可无的,如果不对它们绑定Shader,一些默认的功能将会被执行。
Shader的创建和c/c++程序非常相似,首先编写Shader代码,然后确保它在你的程序中能正确执行。可以在程序中使用字符数组来存储Shader代码或者將Shader写在一个外部的文件中,然后在程序中加载它。接着把这些Shader全部的编译成Shader对象,最后使用链接器將这些Shader链接到一个单独的program 对象加载到GPU中。链接Shader对象使得驱动能够对这些Shader进行裁剪并根据它们的关系做优化处理。
1.在前几节项目解决方案中新建控制台应用。
2.在项目上点击右键选择属性,將配置属性->常规->项目默认值->字符集设置为“使用多字节字符集”。
在配置属性->VC++目录下的包含目录中添加$(SolutionDir)Include和$(SolutionDir)Include\assimp
在库目录中添加$(SolutionDir)Lib
在配置属性->链接器->输入->附加依赖项中添加freeglut.lib、glew32.lib、assimp.lib
清单1.主程序 tutorial04.cpp代码
/*
Copyright 2010 Etay Meiri
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Tutorial 04 - shaders
*/
#include "stdafx.h"
#include <stdio.h>
#include <GL/glew.h>
#include <GL/freeglut.h>
#include "ogldev_util.h"
GLuint VBO;
const char* pVSFileName = "shader.vs";
const char* pFSFileName = "shader.fs";
//创建顶点结构体,用于表示OpenGL中的顶点
struct Vector3f
{
float x;
float y;
float z;
Vector3f(){}
Vector3f(float _x, float _y, float _z)
{
x = _x;
y = _y;
z = _z;
}
};
static void RenderSceneCB()
{
glClear(GL_COLOR_BUFFER_BIT);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(0);
glutSwapBuffers();
}
static void InitializeGlutCallbacks()
{
glutDisplayFunc(RenderSceneCB);
}
static void CreateVertexBuffer()
{
Vector3f Vertices[3];
Vertices[0] = Vector3f(-1.0f, -1.0f, 0.0f);
Vertices[1] = Vector3f(1.0f, -1.0f, 0.0f);
Vertices[2] = Vector3f(0.0f, 1.0f, 0.0f);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
}
static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
GLuint ShaderObj = glCreateShader(ShaderType);
if (ShaderObj == 0) {
fprintf(stderr, "Error creating shader type %d\n", ShaderType);
exit(0);
}
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
glCompileShader(ShaderObj);
GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar InfoLog[1024];
glGetShaderInfoLog(ShaderObj, 1024, NULL, InfoLog);
fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
exit(1);
}
glAttachShader(ShaderProgram, ShaderObj);
}
static void CompileShaders()
{
GLuint ShaderProgram = glCreateProgram();
if (ShaderProgram == 0) {
fprintf(stderr, "Error creating shader program\n");
exit(1);
}
string vs, fs;
if (!ReadFile(pVSFileName, vs)) {
exit(1);
};
if (!ReadFile(pFSFileName, fs)) {
exit(1);
};
AddShader(ShaderProgram, vs.c_str(), GL_VERTEX_SHADER);
AddShader(ShaderProgram, fs.c_str(), GL_FRAGMENT_SHADER);
GLint Success = 0;
GLchar ErrorLog[1024] = { 0 };
glLinkProgram(ShaderProgram);
glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
if (Success == 0) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
exit(1);
}
glValidateProgram(ShaderProgram);
glGetProgramiv(ShaderProgram, GL_VALIDATE_STATUS, &Success);
if (!Success) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Invalid shader program: '%s'\n", ErrorLog);
exit(1);
}
glUseProgram(ShaderProgram);
}
int _tmain(int argc, _TCHAR* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA);
glutInitWindowSize(800, 600);
glutInitWindowPosition(100, 100);
glutCreateWindow("Tutorial 02");
InitializeGlutCallbacks();
// Must be done after glut is initialized!
GLenum res = glewInit();
if (res != GLEW_OK) {
fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
return 1;
}
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
CreateVertexBuffer();
CompileShaders();
glutMainLoop();
return 0;
}
GLuint ShaderProgram = glCreateProgram();
这里我们创建一个Program对象,你可以把它是Shader的容器,我们将会把所有的Shader对象链接到这个Program对象中。
GLuint ShaderObj = glCreateShader(ShaderType);
我们通过glCreateShader函数的调用创建两个Shader对象,其中一个Shader类型为GL_VERTEX_SHADER(Vertex Shader),另外一个为GL_FRAGMENT_SHADER(Fragment Shader)。
Shader对象用于维护我们编写的Shader代码。
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
在编译Shader对象之前我们必须指定它的源码,glShaderSource 函数以Shader对象作为参数,提供了一种灵活的方式指定Shader源码。源码可以分布在多个字符数组中,你需要提供一个存放这些字符数组地址的数组的指针和一个存放每个数组长度的数组的指针。为了简单起见,我们使用一个字符数组存放所有的Shader源码和只有一个元素的GLint数组存放字符数组的长度。 第二个参数指定这两个数组元素个数。
glCompileShader(ShaderObj);
调用该函数编译Shader对象。
GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar InfoLog[1024];
glGetShaderInfoLog(ShaderObj, sizeof(InfoLog), NULL, InfoLog);
fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
}
这段代码用于输出Shader对象编译出错时的错误信息。
glAttachShader(ShaderProgram, ShaderObj);
將编译后的Shader对象附加到之前创建的Program对象中,非常类似于在makefile文件中添加需要链接的对象列表。因为我们没有一个makefile文件来效仿gnu make的行为,所以只能调用函数的方式为链接处理做准备。
glLinkProgram(ShaderProgram);
在所有的Shader对象经过编译并把它们附加到Program对象之后,调用glLinkProgram来链接它们。需要注意的是,完成Program对象的链接后,可以调用glDetachShader 和glDeleteShader 函数来解除附加的Shader对象。OpenGl驱动中维护着它所生成的大多数对象的引用计数,如果一个Shader对象创建之后又被删除,驱动程序去把它去除,但是如果它被附加到Program对象中,调用glDeleteShader 后驱动程序仅仅会把它标记为删除,你还需调用glDetachShader 將它的引用计数置为0,然后它才会被删除。
glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
if (Success == 0) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
}
注意:我们检测Program对象相关错误(例如链接错误)和检测Shader对象错误调用的函数有些不同,使用glGetProgramiv 代替glGetShaderiv ,使用glGetProgramInfoLog代替glGetShaderInfoLog 。
glValidateProgram(ShaderProgram);
看到这段代码,你可能会问为什么已经成功链接Program对象后还要调用glValidateProgram来校验该对象。所不同的是链接错误检测针对的是Shader对象的合并,而该函数是检测Program对象在该管道状态下是否能正确执行。
glUseProgram(ShaderProgram);
最后调用调用上面这个函数,安装Program对象作为当前渲染状态的一部分。这个Program对象会影响所有绘图函数的调用,直到你替换它或使用glUseProgram指定参数为NULL来显式的禁用它。
清单2.shader.vs代码
#version 330
layout (location = 0) in vec3 Position;
void main()
{
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
}
#version 330
告诉编译器GLSL版本为3.3,如果编译器不支持將会抛出异常。
layout (location = 0) in vec3 Position;
这段代码在Shader中声明一个顶点特定属性(vertex specific attribute)Position,它是由3个float类型构成的向量。顶点特定(vertex specific)意味着在GPU调用每一个shader时,在缓冲区中的新顶点的值会被提供。声明的第一部分layout (location = 0),创建属性名和缓冲区中属性的绑定。这样做是为了防止我们的顶点中有多个属性(位置、法线、纹理坐标等)。我们需要让编译器知道顶点中的哪个属性必须映射到shader中声明的属性。有两种做法,我们可以像上面代码一样不明确的设置(指定为0)。如果这样我们可以在程序中使用一个硬编码的值(即调用glVertexAttributePointer函数时的第一个参数值)。或者我们可以不管它(即上面语句直接写成‘in vec3 Position’),然后在运行时使用glGetAttribLocation从程序中查询该location 。这时我们需要將返回值用在glVertexAttributePointer 函数参数中来取代硬编码方式。这里我们选择较为简单的方式,但是在更复杂的程序中最好让编译器决定属性的索引并且在运行时查询它们。这使得把Shader从多个源文件整合起来变的更简单,而无需把它们调整到缓冲区布局中。
void main()
你可以通过把多个Shader对象链接来创建你自己的Shader,然而在每个着色阶段(VS,GS,FS)只能有一个main函数作为Shader的入口点。
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
这里我们通过硬编码方式对传过来的顶点位置进行变换。把X/Y的值减半,Z的值保持不变,gl_Position是一个特殊的内置变量应该包含齐次的顶点坐标位置。光栅化程序会找到这个变量,并使用它作为屏幕空间的位置。使X/Y值减半意味着我们能看到的三角形的大小将是前面教程中的1/4。需要注意的是我们把W的值设为1,这对三角形的正确显示是至关重要的。投影从3D到2D实际上是在两个独立的阶段完成。首先你需要把所有顶点乘上投影矩阵,在顶点到底光栅化程序之前,GPU会为位置属性自动执行所谓的“透视分割”。这意味着所有的组件都会除以gl_Position 的W组件值。在本教程中我们还没有在vertex shader中做任何投影,但是透视分割(perspective divide)阶段不可缺少。
清单3. shader.fs代码
#version 330
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
out vec4 FragColor;
通常片段着色器(fragment shader)的作用是决定像素的颜色。此外,fragment shader可以完全丢弃像素或改变其Z值(将会影响随后的Z test结果)。在该案例中,屏幕图形输出的颜色由上面的变量决定,包含四个组件分别为R、G、B、A(alpha,即透明度),设置到这个变量中的值将会被光栅化程序接收并写入到帧缓冲区。
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
在前面的教程中没有用到片段着色器,所有绘制的图形默认都是白色,这里通过FragColor 设置为红色。
你可以看到一个红色的三角形显示在屏幕中间。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章