介绍
着色器是在图形卡上执行的小程序。它提供了比使用OpenGL提供的固定状态和操作更灵活和简单的绘制过程控制。通过这种额外的灵活性,着色器用于创建过于复杂或者不可能用常规OpenGL函数来描述的效果:像素级照明、阴影等等。现代显卡和新版本的OpenGL已经完全基于着色器,而您可能知道的固定状态和函数集(称为“固定管线”)已经被弃用并可能在未来被删除。
着色器是用GLSL(OpenGL着色语言)编写的,它非常类似于C编程语言。
有两种类型的着色器:顶点着色器和片段(或像素)着色器。顶点着色器对每个顶点运行,而片段着色器对每个生成的片段(像素)运行。根据您想要实现的效果,您可以提供顶点着色器、片段着色器或两者都提供。
要理解着色器的作用以及如何高效地使用它们,重要的是要了解渲染管线的基础知识。您还必须学习如何编写GLSL程序,并查找好的教程和示例来开始学习。您还可以查看SFML SDK附带的“着色器”示例。
本教程仅关注SFML特定部分:如何加载和应用您的着色器-而不是编写它们。
加载着色器
在SFML中,着色器由sf::Shader类表示。它处理顶点和片段着色器:一个sf::Shader对象是两者的组合(或仅提供一个,如果另一个没有提供)。
尽管着色器已经成为常见的技术,但仍然有一些旧的显卡可能不支持它们。您在程序中的第一步应该是检查系统是否支持着色器:
if (!sf::Shader::isAvailable())
{
// shaders are not available...
}
如果sf::Shader::isAvailable()返回false,任何试图使用sf::Shader类的尝试都将失败。
最常见的加载着色器的方式是从磁盘上的文件中加载,这可以使用loadFromFile函数实现。
sf::Shader shader;
// load only the vertex shader
if (!shader.loadFromFile("vertex_shader.vert", sf::Shader::Vertex))
{
// error...
}
// load only the fragment shader
if (!shader.loadFromFile("fragment_shader.frag", sf::Shader::Fragment))
{
// error...
}
// load both shaders
if (!shader.loadFromFile("vertex_shader.vert", "fragment_shader.frag"))
{
// error...
}
着色器源代码包含在简单文本文件中(就像你的C++代码一样)。 它们的扩展名并不重要,可以是任何你想要的,甚至可以省略扩展名。 “.vert"和”.frag"只是可能的扩展名示例。
loadFromFile功能有时会在没有明显原因的情况下失败。首先,检查SFML打印到标准输出(检查控制台)的错误消息。如果消息为无法打开文件,则确保工作目录(任何文件路径将相对解释的目录)是您想要的目录:当您从桌面环境运行应用程序时,工作目录是可执行文件夹。但是,当您从IDE(Visual Studio,Code :: Blocks,…)启动程序时,工作目录有时可能设置为项目目录。这通常可以在项目设置中很容易更改。
着色器还可以直接从字符串加载,使用loadFromMemory函数即可。如果您想直接嵌入着色器源代码到您的程序中,这非常有用。
const std::string vertexShader = \
"void main()" \
"{" \
" ..." \
"}";
const std::string fragmentShader = \
"void main()" \
"{" \
" ..." \
"}";
// load only the vertex shader
if (!shader.loadFromMemory(vertexShader, sf::Shader::Vertex))
{
// error...
}
// load only the fragment shader
if (!shader.loadFromMemory(fragmentShader, sf::Shader::Fragment))
{
// error...
}
// load both shaders
if (!shader.loadFromMemory(vertexShader, fragmentShader))
{
// error...
}
最后,与所有其他SFML资源一样,着色器也可以使用loadFromStream函数从自定义输入流中加载。
如果加载失败,请别忘了检查标准错误输出(控制台)以查看GLSL编译器的详细报告。
使用着色器
使用着色器很简单,只需将其作为附加参数传递给draw函数。
window.draw(whatever, &shader);
向着色器传递变量
像其他程序一样,着色器可以接受参数,这样它就能在每次绘制时表现得不同。这些参数被声明为全局变量,在着色器中称为uniform变量。
uniform float myvar;
void main()
{
// use myvar...
}
uniform变量可以由C++程序设置,使用sf::Shader类中的各种setUniform函数的重载形式。
shader.setUniform("myvar", 5.f);
setUniform的重载支持SFML提供的所有类型:
- float (GLSL类型float)
- 2个浮点数,sf::Vector2f (GLSL类型vec2)
- 3个浮点数,sf::Vector3f (GLSL类型vec3)
- 4个浮点数 (GLSL类型vec4)
- sf::Color (GLSL类型vec4)
- sf::Transform (GLSL类型mat4)
- sf::Texture (GLSL类型sampler2D)
GLSL编译器会对未使用的变量进行优化处理(这里的“未使用”是指“未参与最终顶点/像素计算”)。因此,在测试中调用setUniform时出现“无法找到变量“xxx”的错误信息是正常的。
最简单的着色器
在这里你不会学习如何编写GLSL着色器,但了解SFML提供哪些输入数据给着色器以及它期望你做什么是至关重要的。
顶点着色器
SFML有一个固定的顶点格式,由sf::Vertex结构描述。SFML顶点包含2D位置、颜色和2D纹理坐标。这正是你在顶点着色器中得到的输入,存储在内置变量gl_Vertex、gl_Color和gl_MultiTexCoord0中(你不需要声明它们)。
void main()
{
// transform the vertex position
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
// transform the texture coordinates
gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
// forward the vertex color
gl_FrontColor = gl_Color;
}
位置通常需要通过模型视图和投影矩阵进行转换,这些矩阵包含物体变换与当前视图的组合。纹理坐标需要通过纹理矩阵进行转换(这个矩阵可能对您来说没有意义,它只是SFML的实现细节)。最后,颜色只需要被传递即可。当然,如果您不使用纹理坐标和/或颜色,可以忽略它们。
所有这些变量将通过图形卡在原始物体上进行插值,并传递给片段着色器。
片段着色器
片段着色器的功能非常类似:它接收生成的片段的纹理坐标和颜色。在这一点上已经没有位置信息了,此时图形卡已经计算出片段的最终栅格位置。然而,如果您处理纹理实体,还需要当前纹理。
uniform sampler2D texture;
void main()
{
// lookup the pixel in the texture
vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
// multiply it by the color
gl_FragColor = gl_Color * pixel;
}
当前纹理不是自动的,您需要像处理其他输入变量一样处理它,并从C++程序中显式地设置它。由于每个实体可以有不同的纹理,更糟糕的是,可能没有办法获取它并将其传递给着色器,因此SFML提供了setUniform函数的特殊重载,可以为您完成此任务。
shader.setUniform("texture", sf::Shader::CurrentTexture);
这个特殊的参数会自动将正在绘制的实体的纹理设置为具有给定名称的着色器变量。每次绘制新实体时,SFML都会相应地更新着色器纹理变量。
如果您想看到着色器的漂亮示例,可以查看SFML SDK中的Shader示例。
在OpenGL代码中使用sf::Shader
如果您使用的是OpenGL而不是SFML的图形实体,则仍然可以将sf::Shader用作OpenGL程序对象的包装器,并在您的OpenGL代码中使用它。
要激活sf::Shader进行绘制(相当于glUseProgram),您必须调用bind静态函数:
sf::Shader shader;
...
// bind the shader
sf::Shader::bind(&shader);
// draw your OpenGL entity here...
// bind no shader
sf::Shader::bind(NULL);