OpenGL的可编程管线如下图所示: 顶点处理器—>几何处理器—>裁剪器—>光栅器(片段处理器) [ 顶点处理器,负责对传入渲染管线的每个顶点执行顶点着色器中的内容,顶点着色器并不关心所要渲染的基本图元的拓扑结构。此外,你不能在顶点处理器中丢弃任何一个顶点。每个顶点都只被顶点处理器处理一次。 几何处理器,组成图元所需要的顶点以及其邻接关系都会被提供给着色器。这使得着色器能够考虑除顶点本身之外的其他信息。除此之外,几何处理器也可以将在绘制函数中确定的拓扑关系修改成另外一种拓扑关系。 裁剪器,这是一个单一功能的固定功能单元,通过规范化盒子对图元进行裁剪。同时它还通过近裁剪面和远裁剪面对其进行裁剪。同时他也支持用户自定义裁剪面对场景进行裁剪。未被裁剪掉的顶点会变换到屏幕坐标系之下,之后通过光栅化将顶点按照拓扑结构渲染到屏幕上。 顶点着色器、片元着色器、几何着色器这三个可编程阶段是可选择的,如果我们不向其绑定 Shader 程序就会执行默认的固定管线的函数。 1.
GLuint ShaderProgram = glCreateProgram();
我们通过创建一个shader程序对象来开始我们的着色器工程,我们将会把所有的shader程序都链接到这个sahder程序对象上。 2.
GLuint ShaderObj = glCreateShader(ShaderType);
通过调用上面的函数我们创建了两个 Shader 对象,其中一个 Shader 对象的类型为 GL_VERTEX_SHADER,另一个为 GL_FRAGMENT_SHADER。这两个类型的 Shader对象的指定 Shader 源程序和编译 Shader 程序的过程是一样的。 3.
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShadersource(ShaderObj, 1, p, Lengths);
在对 Shader 对象进行编译之前我们必须为其指定 Shader 源程序,glShadersource 函数需要一个 Shader 对象作为参数,源程序可以分布在多个字符串数组中,你需要提供这些数组的指针数组以及一个用于存放对应数组长度的整数数组。为了简单起见,整个着色器代码我们仅使用一个字符串数组,源程序指针数组和和长度数组都只有一个元素。glShadersource(ShaderObj, 1, p, Lengths)的第二个参数是这两个数组的元素个数。 4.
glCompileShader(ShaderObj);
编译shader程序。 5.
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);
}
很多时候,你会遇到一些编译错误。上面的代码能获得编译状态并且显示编译器遇到的错误。 6.
glAttachShader(ShaderProgram, ShaderObj);
最后,我们将编译之后的 Shader 对象附加到程序对象上,这和在 Makefile 中链接一个对象链表类似。因为我们这儿没有 Makefile 所以通过编程来实现这种功能。只有被附加到程序对象上的 Shader 对象才会参与链接过程。 7.
glLinkProgram(ShaderProgram);
在编译好所有 Shader 对象以及将他们附加到程序对象之后我们就可以进行链接操作。注意在链接程序对象之后你可以通过为每个 Shader 对象调用 glDetachShader 和 glDeleteShader 方法来删除其中的 Shader 对象。OpenGL驱动程序会为其生成的大部分对象维持一个引用计数。如果我们在创建一个 Shader 对象之后又将其删除则驱动程序会将这个 Shader 对象剔除掉,但是如果我们将 Shader 对象附加到程序对象之后调用 glDeleteShader 函数则只会将 Shader 对象标记为删除部分,你需要调用 glDetachShader 函数其引用计数才会下降到0之后才会被移除。 8.
glGetProgramiv(ShaderProgram, GL_LINK_STATUS,&Success);
if (Success == 0) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL,ErrorLog);
fprintf(stderr, "Error linking shader program:'%s'\n", ErrorLog);
}
需要注意的是我们检查程序相关的错误(如链接错误)与检查 Shader 相关错误有些许不同。我们使用 glGetShaderiv 函数和 glGetShaderInfoLog 函数代替 glGetShaderiv 函数和 glGetShaderInfoLog 函数。 9.
glValidateProgram(ShaderProgram);
对链接了的程序对象进行验证。它们之间的区别是链接主要检查基于着色器组合的错误,而上面调用的函数则是验证基于当前的管线状态程序是否能够成功执行。在一个有多个shader程序和很多状态变化的复杂程序中,在每次绘制之前都进行验证是更加明智的。在我们这个简单的程序中我们仅仅对其调用了一次。当然你也可以仅仅在开发过程中进行这样的验证而避免在最终产品中增加这个不必要的开销。 10.
glUseProgram(ShaderProgram);
最将链接之后的 Shader 程序对象添加到渲染管线中。除非我们使用其他的 Shader 程序对象来替换当前的程序对象或者通过调用 glUseProgram(NULL)显式的禁用它的使用(并且启用固定管线),否则这个 Shader 程序对象会对每次的绘制都会产生效果。如果你创建的 Shader 程序对象只包含一种类型的 Shader 程序,那么其他阶段的操作会默认的调用固定管线中的功能。 11.
#version 330
这告诉编译器我们的 Shader 程序是针对3.3版本的 GLSL,如果编译器不支持这个版本则会报错。 12.
layout (location = 0) in vec3 Position;
这条语句出现在顶点着色器中,他声明了一个指定为顶点属性的float类型三维向量,这个向量在shader中被表示为‘Position’。‘顶点属性’意味着 GPU 中的 Shader 程序每调用一次,顶点缓冲区都会为其提供一个新的顶点数据。语句中的第一部分——layout (location = 0)将属性名称与缓冲区中的属性进行绑定 13.
void main()
我们可以通过将多个 Shader 对象链接到一起来创建你自己的着色器,但是在每个着色器阶段(VS,GS,FS)只能有一个 main 函数作为着色器的入口点 14.
gl_Position = vec4(0.5 * Position.x, 0.5 *Position.y, Position.z, 1.0);
在这里我们通过编码对传入的顶点位置进行变换,我们将顶点的 X、Y 分量的值减半而保持 Z 方向值不变,gl-Position 是一个特殊的内置变量,他能够存放齐次(包含X,Y,Z和W分量)顶点坐标。在光栅化过程中系统会寻找这个变量并使用它作为顶点在屏幕上的位置(需要经过一些矩阵变换)。将顶点的X,Y分量减半意味着我们将会看到一个面积只有前面教程中的四分之一的三角形。需要注意的是我们将W分量设置为1.0,这对于三角形的正确显示是非常重要的,实现从3D到2D的投影变换实际上是在两个不同的阶段实现的,首先你需要让所有的顶点都乘上投影矩阵(我们将在后面的教程中对此进行介绍),之后 GPU 在对其进行光栅化之前自动对位置属性(Position)执行透视分割。这意味着 gl-Position 中的所有分量都会除以W分量。 15.
out vec4 FragColor;
一般情况下片元着色器的作用就是确定片元的颜色,除此之外,片元着色器也完全可以丢弃片元或则改变其Z值(Z值的改变会对之后的深度测试产生影响)。输出颜色是通过声明上面的变量实现,四个分量分别表示R,G,B和A(alpha)。被写入到这个变量中的值会被光栅化程序接受并最终写入到帧缓存中。 16.
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
没有使用片元着色器的物体都被绘制成默认的白色。这里我们将颜色设置为红色。 项目代码:
#include <stdio.h>
#include<string.h>
#include <GL/glew.h> // GLEW扩展库,注意glew.h必须要写在前面
#include <GL/freeglut.h> // freeGLUT图形库
#include “ogldev_math_3d.h” //用于OpenGL的3d数学库,这里主要用到了顶点这个数据结构,里面报错的代码可以先注释掉
#include “ogldev_util.h” //用于读取文本文件
GLuint VBO; //全局GLuint引用变量,来操作顶点缓冲器对象
// 定义要读取的顶点着色器脚本和片断着色器脚本的文件名,作为文件读取路径
const char* pVSFileName = “shader.vs”;
const char* pFSFileName = “shader.fs”;
static void RenderCallBack()
{
glClear(GL_COLOR_BUFFER_BIT); //清空颜色缓存
glEnableVertexAttribArray(0); //开启顶点属性
glBindBuffer(GL\_ARRAY\_BUFFER, VBO); //绑定GL\_ARRAY\_BUFFER缓冲器
glVertexAttribPointer(0, 3, GL\_FLOAT, GL\_FALSE, 0, 0); //管线解析bufer中的数据
glDrawArrays(GL\_TRIANGLES, 0, 3); //画三角形,3个顶点
glDisableVertexAttribArray(0); //禁用顶点数据
glutSwapBuffers(); //交换前后缓存
}
static void InitializeGlutCallbacks()
{
glutDisplayFunc(RenderCallBack);
}
static void CreateVertexBuffer()
{
Vector3f Vertices[3]; //创建含有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); //绑定GL\_ARRAY\_BUFFER缓冲器
glBufferData(GL\_ARRAY\_BUFFER, sizeof(Vertices), Vertices, GL\_STATIC\_DRAW); //绑定顶点数据
}
//使用shader文本编译shader对象,并绑定shader都想到着色器程序中
static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
// 根据shader类型参数定义两个shader对象
GLuint ShaderObj = glCreateShader(ShaderType);
// 检查是否定义成功
if (ShaderObj == 0)
{
fprintf(stderr, “Error creating shader type %d\n”, ShaderType);
exit(0);
}
// 定义shader的代码源
const GLchar\* p\[1\];
p\[0\] = pShaderText;
GLint Lengths\[1\];
Lengths\[0\] = strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
glCompileShader(ShaderObj); // 编译shader对象
// 检查和shader相关的错误
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);
}
// 将编译好的shader对象绑定到program object程序对象上
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);
// 链接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);
}
// 设置到管线声明中来使用上面成功建立的shader程序
glUseProgram(ShaderProgram);
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(400, 400);
glutInitWindowPosition(100, 100);
glutCreateWindow("Shader三角形");
glutDisplayFunc(RenderCallBack); //开始渲染
InitializeGlutCallbacks();
// 检查GLEW是否就绪,必须要在GLUT初始化之后!
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(); //通知开始GLUT的内部循环
return 0;
}
顶点着色器shader.vs代码:
#version 330 //告诉编译器我们的目标GLSL编译器版本是3.3
layout (location = 0) in vec3 Position; // 绑定定点属性名和属性,方式二缓冲属性和shader属性对应映射
void main()
{
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0); // 为glVertexAttributePointer提供返回值
}
片段着色器shader.fs代码:
#version 330 //告诉编译器我们的目标GLSL编译器版本是3.3
out vec4 FragColor; // 片段着色器的输出颜色变量
// 着色器的唯一入口函数
void main()
{
// 定义输出颜色值
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
运行结果: BUG修复:在使用ogldev_util.h这个头文件的时候,运行程序一直报ReadFile函数的错误: 我在网上也发现了一些遇到这种问题的人,但是没有找到解决方案。最终还是靠自己解决了这个问题,主要是把ogldev_util.cpp中的ReadFile的核心代码放到了ogldev_util.h中,然后添加部分引用,下面展示部分代码,只修改ogldev_util.h里面的这部分就行:
#ifndef OGLDEV_UTIL_H
#define OGLDEV_UTIL_H
#ifndef WIN32
#include <unistd.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include
#include
#include <string.h>
#include <assert.h>
#include “ogldev_types.h”
#include
#include
#ifdef WIN32
#include <Windows.h>
#else
#include <sys/time.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdarg.h>
using namespace std;
bool ReadFile(const char* fileName, string& outFile)
{
ifstream f(fileName);
bool ret = false;
if (f.is_open()) {
string line;
while (getline(f, line)) {
outFile.append(line);
outFile.append(“\n”);
}
f.close();
ret = true;
}
return ret;
}
char* ReadBinaryFile(const char* pFileName, int& size);
参考链接: [http://ogldev.atspace.co.uk/www/tutorial04/tutorial04.html](http://ogldev.atspace.co.uk/www/tutorial04/tutorial04.html) [http://blog.csdn.net/cordova/article/details/52495077#cpp](http://blog.csdn.net/cordova/article/details/52495077#cpp)