简介
OpenGL着色语言(GLSL)很像C语言,它由OpenGL实现进行编译
和连接,并且完全在图形硬件中运行。一个基本的着色器架构图如下:

可以看到,在基本的架构中,至少需要2个着色器:一个顶点着色器、一个片段着色器。
向着色器传递数据的方式有3种:一是参数,是对每个顶点而言的;二是统一值,是针对整个顶点数据批次的常量(所以是一致的);最后也可以加载和使用纹理数据。
变量和数据类型
基本类型
在GSLS中,可用的数据类型只有4种:
①整数(包括有符号整数和无符号整数)
②浮点数(OpenGL 3.3来说只能使用单精度浮点数)
③布尔值(bool)
在OpenGL着色语言中没有指针,并且没有任何类型的字符串或字符。
函数可以返回这些类型中的任何一种,但也可以声明为void,不过同样不允许void指针。这些数据类型在OpenGL着色语言中的应用与它们在C/C++中一样。
cpp
bool bDone = false; // 布尔值,真(true)或假(false)
int iValue = 42; // 有符号整数
uint uiValue = 3929u; // 无符号整数
float fValue = 42.0f; // 浮点值
向量类型
GSLS相较C/C++,有一个独特的类型,就是向量数据类型。

一个向量数据类型可以像任何其他类型的变量一样进行声明。
例如,我们可以用一个4分量的浮点向量声明一个顶点位置:
cpp
vec4 vVertexPos;
也可以用一个构造函数初始化一个向量:
cpp
vec4 vVertexPos = vec4(39.0f, 10.0f, 0.0f, 1.0f);
注意: 不要把这个构造函数与C++类构造函数混淆。
OpenGL着色语言的向量数据类型并不是类,它们有自己的内建数据类型。
向量可以被赋值给另一个向量,可以相加,也可以用一个标量(非向量类型)进行缩放。
cpp
vVertexPos = vOldPos + vOffset;
vVertexPos = vNewPos;
vVertexPos += vec4(1.0f, 1.0f, 0.0f, 0.0f);
vVertexPos *= 5.0f;
OpenGL着色语言的另一个独特的性质是对一个向量的独立元素进行寻址的方式。
例如,用点号来确定多达4个向量元素的地址,但是可以使用下列3组标识符中的任意一组:Xyzw、rgba或stpq。
典型情况下,可以在引用向量类型数据时使用xyzw组标识符。
cpp
vVertexPos.x = 3.0f;
vVertexPos.xy = vec2(3.0f, 5.0f);
vVertexPos.xyz = vNewPos.xyz;
或在进行颜色操作时使用rgba:
cpp
vOutputColor.r = 1.0f;
vOutputColor.rgba = vec4(1.0f, 1.0f, 0.5f, 1.0f);
或在进行纹理坐标操作时,使用stpq:
cpp
vTexCoord.st = vec2(1.0f, 0.0f);
但需要注意的是,不能将不同的组混合到一个向量访问中:
cpp
vTexCoord.st = vVertex.xt; // 不允许对x和t进行混合
向量数据类型还支持调换操作,调换操作是指将两个或两个以上的向量元素进行交换。
例如,如果要将颜色数据从RGB顺序转换到BGR顺序:
cpp
vNewColor.bgra = vOldColor.rgba;
向量数据类型并不只是OpenGL着色语言的本地数据类型,也是硬件的本地数据类型。
它们的速度很快,而且能一次性完成对所有分量的操作。例如:
cpp
vVertex.x = vOtherVertex.x + 5.0f;
vVertex.y = vOtherVertex.y + 4.0f;
vVertex.z = vOtherVertex.z + 1.0f;
vVertex.xyz = vOtherVertex.xyz + vec3(5.0f, 4.0f, 1.0f);
矩阵类型
除了向量数据类型之外,OpenGL着色语言还支持多种矩阵类型。
不过,和向量类型不同,所有矩阵类型都只支持浮点数,不支持整数或者布尔值矩阵。

实际上在OpenGL着色语言中,一个矩阵就是一个由向量组成的数组(实际上是列向量)。
例如,设置一个4x4矩阵的最后一列:
cpp
mModelView[3] = vec4(0.0f, 0.0f, 0.0f, 1.0f);
相应地,可以按照如下方式恢复一个矩阵的最后一列:
cpp
vec4 vTranslation = mModelView[3];
也可以进行更加细致的查询:
cpp
vec3 vTranslation = mModelView[3].xyz;
矩阵也可以乘以向量,这种运算通常在通过模型视图投影矩阵来对一个向量进行变换时使用:
cpp
vec4 vVertex;
mat4 mvpMatrix;
......
......
vOutPos = mvpMatrix * vVertex;
矩阵数据类型也有它们自己的构造函数。
例如,为了对一个矩阵的最后一列进行硬编码,我们可以编写类似下面这样的代码:
cpp
mat4 vTransform = mat4(1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
在这种情况下,我们会采用单位矩阵作为变换矩阵,还可以使用一个更快的矩阵构造器,它只将单个值填入矩阵的对角线:
cpp
mat4 vTransform = mat4(1.0f);
存储限定符
着色器变量声明也可以选择指定一个存储限定符。
限定符用于将变量标记为输入变量(in 或 uniform)、输出变量(out)或常量(const)。
输入变量接受来自OpenGL客户端(通过C/C++提交的属性)或者以前的着色器阶段(例如从顶点着色器传递到片段着色器)。
输出变量是指在任何着色器阶段进行写入的变量,我们希望后续的着色器阶段能看到这些变量,例如,从顶点着色器传递到片段着色器,或者用片段着色器写入最终片段颜色。
主要的变量限定符有以下几种:

有一个变量限定符inout只能在一个函数中声明一个参数时使用。
因为OpenGL着色语言并不支持指针(或者引用),所以这是将一个值传递到一个函数并且允许这个函数修改并返回同一个变量值的唯一方法。如下:
cpp
int CalculateSometing(float fTime, float fStepSize, inout float
fVariance);
上述接口将会返回一个整数(可能是一个通过/失败标记),但是也可以修改fVariance变量的值,并且调用代码也可以从变量中读取新值。
在C/C++中,为了允许对参数进行修改,我们也以使用指针这种方式声明这个函数:
cpp
int CalculateSomething(float fTime, float fStepSize, float*
fVariance);
在众多限定符中,还需要特别注意的是centroid限定符。
centroid限定符只在多重采样缓冲区进行渲染起作用。
在一个单采样缓冲区中,插值操作总是从像素的中心开始的。
对于多重采样,在使用centroid限定词时,插补值将会被选中,因此它会落到图元和像素中。
在默认情况下,参数将在两个着色器阶段之间以一种透视正确的方法进行插补。
我们可以通过noperspective关键词来指定一个非透视插值,或者甚至可以通过flat关键词而不进行插值。
我们还可以选择使用smooth关键词来声明,这个变量是以一种透视正确的方法进行插补的。
示例:
cpp
smooth out vec3 vSmoothValue;
flat out vec3 vFlatColor;
noperspective float vLinearlySmoothed;
着色器示例
在了解的GSLS的基础后,下面就使用GSLS编写2个简单的着色器。
顶点着色器代码如下:
cpp
#version 130
in vec4 vColor;
in vec4 vVertex;
out vec4 vVaryingColor;
void main(void)
{
vVaryingColor = vColor;
gl_Position = vVertex;
}
片段着色器代码如下:
cpp
#version 130
out vec4 vFragColor;
in vec4 vVaryingColor;
void main(void)
{
vFragColor = vVaryingColor;
}
指定版本
每个着色器的第一行非命令行都是指定版本:
cpp
#version 330
上面这一行就指定了这个着色器要求的OpenGL着色语言的最低版本为3.3。
如果OpenGL驱动不支持3.3版,那么着色器将不会编译。
OpenGL3.2引入了GLSL(OpenGL着色语言)1.5版,OpenGL 3.1引入了GLSL1.4版,而OpenGL 3.0 则引入了GLSL1.3版。
听起来有点晕?这很正常。在OpenGL 3.3中,ARB决定使GLSL版本号与从3.3版开始的所有OpenGL主版本保持同步。
实际上OpenGL 4.0规范是与3.3版同时发布的,而与OpenGL 4.0对应的着色语言版本也是4.0。在着色器中要求4.0版本,如下所示:
cpp
#version 400
属性声明
属性是由C/C++客户端OpenGL代码逐个顶点进行指定的。
在顶点着色器中,这些属性只是简单地声明为in。
cpp
in vec4 vVertex;
in vec4 vColor;
这里声明了两个导入属性,即一个4分量顶点位置和一个4分量顶点颜色值。
在GLSL中每个顶点程序都最多可以有16个属性,每个属性也总是一个4分量向量,即使不使用所有的4分量时也是如此。
例如,如果我们只是指定了一个float值作为参数,那么在内部仍将占据4个浮点值的空间。
另外,标记为in的变量是只读的。
声明输出
为顶点程序声明一个同样是4分量浮点向量的输出变量:
cpp
out vec4 vVaryingColor;
这个变量将成为将要传送到片段着色器的顶点的颜色值。
实际上,这个变量必须为片段着色器声明为一个in变量,否则在我们试图将着色器编译和连接到一起时,就会得到一个连接错误。
当在一个顶点着色器中将一个值声明为out ,并在片段着色器中将其声明为in时,这个片段着色器接受的变量值为一个插补值。
在默认情况下,这些工作将以一种正确透视的方式进行,并且在变量之前指定另一个额外的限定符smooth,以确保完成了这些工作。
我们还可以指定flat声明不应进行任何插值,或者指定noperspective来声明在各个值之间进行直接线性插值(straight linear interpolation)。
cpp
smooth out vec3 vSmoothValue;
flat out vec3 vFlatColor;
noperspective float vLinearlySmoothed;
顶点动作
最后看一下顶点程序的主体,它在批次中将为每个顶点执行一次。
cpp
void main(void)
{
vVaryingColor = vColor;
gl_Position = vVertex;
}
这里的处理很简单,就是将输入颜色属性分配给即将发出的插补值,并且未经变换直接将输入的顶点值分配给gl_Position。
变量gl_Position是一个预定义的内建4分量向量,它包含顶点着色器要求的一个输出。
输入gl_Position的值是几何图形阶段用来装配图元的。
注意,在主函数中我们没有进行任何附加的变换,顶点将只映射到所有3个坐标范围都在+/- 1.0之间的笛卡尔坐标上。
片段三角形
在渲染一个图元(例如一个三角形)时,一旦3个顶点由顶点程序进行了处理,那么它们将组装成一个三角形,而这个三角形将由硬件进行光栅化。
图形硬件确定独立片段属于屏幕上(或者更精确地,在颜色缓冲区中)的什么位置,并且为图元(如三角形)中的每个片段执行片段着色器的一个实例。
片段程序的最终输出颜色是一个4分量浮点向量,我们如下声明这个向量:
cpp
out vec4 vFragColor;
如果片段程序只有一个输出,那么它在内部将分配为"输出0"。
这是片段着色器的第一个输出,并且将传输到由glDrawBuffers设置的缓冲区目标,默认情况下为GL_BACK,即黑色缓冲区(对于双重缓冲区环境来说是这样的)。
实际颜色缓冲区常常并不包含4个浮点分量,这样输出值就会映射到目标缓冲区的范围内。
例如,大多数情况下,这可能只是4个无符号字节分量(每个分量值都在0到255之间)。
当然也可以使用ivec4来输出整型值,而这些值也会被映射到颜色缓冲区的范围内。
输入到片段着色器输入参数是经过平滑插值的颜色值,由顶点程序上游传入。
在顶点着色器程序中作为输出参数,相应的在片段着色器中作为一个in变量进行声明的。
否则在我们试图将着色器编译和连接到一起时,就会得到一个连接错误。
cpp
in vec4 vVaryingColor;
最后看看片段着色器的主函数中进行了哪些处理:
cpp
void main(void)
{
vFragColor = vVaryingColor;
}
只是将平滑插值的颜色值直接分配给片段颜色。
编译、绑定和连接
如同C/C++代码编写完成需要编译、链接,通过GSLS编写的着色器代码也需要编译和链接以供上层使用。
同样的着色器代码会以文本形式存储,如:顶点着色器会使用扩展名.vp进行存储,二片段着色器则采用扩展名.fp进行存储。
为什么不直接将文本作为硬编码的字符数组存储在C/C++源代码中?
这样实现在进行编辑时比较麻烦,而且使程序更加自我封闭(selfcontained),从而使修改着色器和阅读源代码更加麻烦。
下面看看如何编译、链接通过GSLS编写的着色器。可以通过GLTools库提供的gltLoadShaderPairWithAttributes接口完成,具体实现如下:
cpp
GLuint gltLoadShaderPairWithAttributes(const char* szVertexProg, const char* szFragmentProg, ......)
{
// 临时着色器对象
GLuint hVertexShader;
GLuint hFragmentShader;
GLuint hReturn = 0;
GLint testVal;
// 创建着色器对象
hVertexShader = glCreateShader(GL_VERTEX_SHADER);
hFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
// 加载它们。如果失败则进行清除并返回null
// 顶点程序
if (gltLoadShaderFile(szVertexProg, hVertexShader) == false)
{
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
cout << "The shader at " << szVertexProg
<< " could not be found.\n";
return (GLuint)NULL;
}
// 片段程序
if (gltLoadShaderFile(szFragmentProg, hFragmentShader) ==
false)
{
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
cout << "The shader at " << szFragmentProg
<< " could not be found.\n";
return (GLuint)NULL;
}
// 对两者进行编译
glCompileShader(hVertexShader);
glCompileShader(hFragmentShader);
// 在顶点着色器中检查错误
glGetShaderiv(hVertexShader, GL_COMPILE_STATUS, &testVal);
if (testVal == GL_FALSE)
{
char infoLog[1024];
glGetShaderInfoLog(hVertexShader, 1024, NULL, infoLog);
cout << "The shader at " << szVertexProg
<< " failed to compile with the following error : \n"
<< infoLog << "\n";
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
return (GLuint)NULL;
}
// 在片段着色器中检查错误
glGetShaderiv(hFragmentShader, GL_COMPILE_STATUS, &testVal);
if (testVal == GL_FALSE)
{
char infoLog[1024];
glGetShaderInfoLog(hFragmentShader, 1024, NULL,
infoLog);
cout << "The shader at " << hFragmentShader
<< " failed to compile with the following error : \n"
<< infoLog << "\n";
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
return (GLuint)NULL;
}
// 创建最终的程序对象,并连接着色器
hReturn = glCreateProgram();
glAttachShader(hReturn, hVertexShader);
glAttachShader(hReturn, hFragmentShader);
// 现在,我们需要将参数名绑定到它们指定的参数位置列表上
va_list attributeList;
va_start(attributeList, szFragmentProg);
// 重复迭代这个参数列表
char* szNextArg;
int iArgCount = va_arg(attributeList, int); // 参数数量
for (int i = 0; i < iArgCount; i++)
{
int index = va_arg(attributeList, int);
szNextArg = va_arg(attributeList, char*);
glBindAttribLocation(hReturn, index, szNextArg);
}
va_end(attributeList);
// 尝试连接
glLinkProgram(hReturn);
// 这些都不再需要了
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
// 确认连接也有效
glGetProgramiv(hReturn, GL_LINK_STATUS, &testVal);
if (testVal == GL_FALSE)
{
char infoLog[1024];
glGetProgramInfoLog(hReturn, 1024, NULL, infoLog);
cout << "The program " << hReturn
<< " failed to link with the following error : \n"
<< infoLog << "\n";
glDeleteProgram(hReturn);
return (GLuint)NULL;
}
// All done, return our ready to use shader program
return hReturn;
}
指定属性
首先函数原型获取了顶点程序文件的名称、片段程序文件的名称和指定属性的参数的变量数量。
cpp
GLuint gltLoadShaderPairWithAttributes(const char*szVertexProg
,const char *szFragmentProg
,......);
szVertexProg参数指定顶点着色器代码文件路径。
szFragmentProg参数指定片段着色器代码文件路径。
后面是一个可变参数,第一个附加的参数是顶点程序包含属性的数量。
在这之后是一个对应于第一个属性的基于0的索引,以及作为一个字符数组的属性名。
然后,只要有必要,属性槽的数量和名称将进行足够多次数的重复。
例如,为了加载一个带有顶点位置和表面法线属性的着色器:
cpp
hShader = gltLoadShaderPairWithAttributes("vertexProg.vp",
"fragmentProg.fp", 2, 0, "vVertexPos", 1, "vNormal");
对于为两个属性位置进行0和1的选择是任意性的,只要这个值在0~15范围之内。
我们也可以选择7和13。不过,GLTools类GLBatch和GLTriangleBatch则使用一系列一致的属性位置,由typedef指定,如下所示:
cpp
typedef enum GLT_SHADER_ATTRIBUTE { GLT_ATTRIBUTE_VERTEX = 0,
GLT_ATTRIBUTE_COLOR, GLT_ATTRIBUTE_NORMAL,
GLT_ATTRIBUTE_TEXTURE0, GLT_ATTRIBUTE_TEXTURE1,
GLT_ATTRIBUTE_TEXTURE2, GLT_ATTRIBUTE_TEXTURE3,
GLT_ATTRIBUTE_LAST};
加载源代码
首先创建两个着色器对象,分别对应顶点着色器和片段着色器。
cpp
hVertexShader = glCreateShader(GL_VERTEX_SHADER);
hFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
然后,可以使用这两个着色器ID来加载着色器源代码。
cpp
// 顶点程序
if(gltLoadShaderFile(szVertexProg, hVertexShader) == false)
{
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
cout << "The shader at " << szVertexProg
<< " could not be found.\n";
return (GLuint)NULL;
}
// 片段程序
if(gltLoadShaderFile(szFragmentProg, hFragmentShader) ==
false)
{
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
cout << "The shader at " << szFragmentProg
<< " could not be found.\n";
return (GLuint)NULL;
}
因篇幅有效,这里省略了gltLoadShaderFile的具体实现,其大概实现是用来以指定的文件名从磁盘中加载着色器文本的。调用完成后,会将着色器源文件送入着色器对象。
编译着色器
可通过glCompileShader对源代码进行编译:
cpp
glCompileShader(hVertexShader);
glCompileShader(hFragmentShader);
每个OpenGL实现都有一个内建的GLSL编译器,这个编译器由硬件提供商提供。
这就是说,任何指定的提供商都应该最有资格为他们自己的硬件建立编译器。
当然,就像C/C++代码一样,很多因素都会阻碍我们的GLSL着色器进行编译,例如语法错误、实现的故障等。可通过glGetShader函数(需指定GL_COMPILE_STATUS)来判断成功与否:
cpp
glGetShaderiv(hVertexShader, GL_COMPILE_STATUS, &testVal);
如果返回的testVal值为GL_FALSE,那就说明源代码编译失败了。
在编译失败时,我们可以通过glGetShaderInfoLog函数来检查着色器消息日志,看看是什么地方出了问题。
在目前的例子中,我们将错误信息显示在控制台,清除着色器对象,并返回NULL:
cpp
if(testVal == GL_FALSE)
{
char infoLog[1024];
glGetShaderInfoLog(hVertexShader, 1024, NULL, infoLog);
cout << "The shader at " << szVertexProg
<< " failed to compile with the following error:\n"
<< infoLog << "\n";
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
return (GLuint)NULL;
}
绑定
对GLSL源代码进行编译之后,我们就完成了一半工作,但是还必须做一些其他工作,才能对它们进行连接。
首先,我们要创建最终的着色器程序对象,并将顶点着色器和片段着色器与它绑定到一起。
cpp
hReturn = glCreateProgram();
glAttachShader(hReturn, hVertexShader);
glAttachShader(hReturn, hFragmentShader);
在连接着色器程序之前,还需要将属性变量名绑定到指定的数字属性位置。
可通过glBindAttribLocation函数完成,原型如下所示。
cpp
void glBindAttribLocation(GLuint shaderProg, GLuint
attribLocation, const GLchar *szAttributeName);
它接受我们正在讨论的着色器的标识符、将要进行绑定的属性位置和属性变量的名称。
例如,在存储着色器中,我们采用了一个约定,即顶点位置属性总是使用变量名vVertex,而属性位置总是使用GLT_ATTRIBUTE_VERTEX值(值0)。
cpp
glBindAttribLocation(hShader, GLT_ATTRIBUTE_VERTEX, "vVertex");
在进行连接之前,必须按照这种方式对属性位置进行连接。
在此处代码中,我们遍历了可变参数列表,只需简单地为每个需要进行绑定的属性重复调用这个函数。
cpp
// 重复迭代可变参数列表
char *szNextArg;
int iArgCount = va_arg(attributeList, int); // 属性数量
for(int i = 0; i < iArgCount; i++)
{
int index = va_arg(attributeList, int);
szNextArg = va_arg(attributeList, char*);
glBindAttribLocation(hReturn, index, szNextArg);
}
va_end(attributeList);
连接着色器
最后,终于到了对着色器进行连接的时候,在这之后也可以丢弃顶点着色器对象和片段着色器对象。
cpp
glLinkProgram(hReturn);
// 这些都不再需要了
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
现在,OpenGL GLSL着色器已经100%准备好了。
我们已经创建了一个着色器程序,还要说明的是,当不再使用它之后(可能是在程序终止时),需要使用如下函数删除它。
cpp
void glDeleteProgram(GLuint program);
使用
在前面通过GSLS编写着色器程序,并且在C/C++代码中编译、绑定、连接着色器后,就可以在C/C++代码中使用自定义的着色器了。
要使用GLSL着色器,必须使用glUseProgram函数选定它:
cpp
glUseProgram(myShaderProgram);
这样就将着色器设置为活动的,现在顶点着色器和片段着色器会处理所有提交的几何图形。
示例
完整的示例代码如下:
cpp
#include "shadedtriangle/shadedtriangle.h"
#include <GLTools.h>
#include <GLShaderManager.h>
#ifndef FREEGLUT_STATIC
#define FREEGLUT_STATIC
#endif
#include <GL/glut.h>
namespace shadedtriangle {
GLBatch triangleBatch;
GLShaderManager shaderManager;
GLint myIdentityShader;
void ChangeSize(int w, int h)
{
glViewport(0, 0, w, h);
}
void SetupRC()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
shaderManager.InitializeStockShaders();
GLfloat vVerts[] = {
-0.5f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
GLfloat vColors[] = { 1.0f, 0.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f };
triangleBatch.Begin(GL_TRIANGLES, 3);
triangleBatch.CopyVertexData3f(vVerts);
triangleBatch.CopyColorData4f(vColors);
triangleBatch.End();
myIdentityShader = gltLoadShaderPairWithAttributes(
"./src/chapter_6/rewrite/shadedtriangle/shadedidentity.vp"
, "./src/chapter_6/rewrite/shadedtriangle/shadedidentity.fp"
, 2
,GLT_ATTRIBUTE_VERTEX
, "vVertex"
, GLT_ATTRIBUTE_COLOR
, "vColor");
}
void ShutdownRC()
{
glDeleteProgram(myIdentityShader);
}
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glUseProgram(myIdentityShader);
triangleBatch.Draw();
// Perform the buffer swap to display back buffer
glutSwapBuffers();
}
int shadedtriangle_test(int argc, char* argv[])
{
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
glutInitWindowSize(800, 600);
glutCreateWindow("Shaded Triangle");
glutReshapeFunc(ChangeSize);
glutDisplayFunc(RenderScene);
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
return 1;
}
SetupRC();
glutMainLoop();
ShutdownRC();
return 0;
}
}
效果如下:
