OpenGL着色器语言(GLSL)看上去很像C语言,它由OpenGL实现进行编译和连接,并且(经常是)完全在图形硬件中运行。
我们有顶点着色器、片段着色器和几何着色器,前两种是必需的,后一种是可选的。
有三种向顶点着色器传递数据的方式:(1)参数,是对每个顶点而言;
(2)统一值,是针对整个顶点数据批次的常量(所以是一致的)
(3)加载和使用纹理数据
可以为片段着色器设置统一值和纹理数据。将顶点属性发送到片段着色器毫无意义,因为片段着色器只是用来在图元进行光栅化后对片段(最基本的是像素)进行填充。不过,每个顶点数据都可以通过顶点程序传递到片段着色器。但是在着这种情况下,这些数据可能是常量(每个片段都是这样的值),或者这些值也可以用不同的方式在图元表面进行插值。
着色器程序和C语言类似,从入口点main函数开始,并且使用同样的字符集和注释约定,以及很多相同的处理指令。
顶点着色器实例
//顶点着色器
#version 330
in vec4 vVertex; //顶点位置属性
in vec4 vColor; //顶点颜色属性
out vec4 vVaryingColor; //传递到片段着色器的颜色值
void main(void)
{
vVaryingColor=vColor; //简单复制颜色值
gl_Position=vVertex; //简单传递顶点位置
}
片段着色器实例
#version 330
out vec4 vFragColor; //将要进行光栅化的片段颜色
in vec4 vVaryingColor; //从顶点阶段得到的颜色
void main(void)
{
vFragColor=vVaryingColor; //对片段进行颜色插值
}
每个着色器的第一行非命令行都是指定版本。
#version 330
这一行指定这个着色器要求的OpenGL着色语言的最低版本为3.3。如果OpenGL驱动不支持3.3版,那么着色器将不会编译。
顶点着色器分析
属性声明
属性是由C/C++客户端OpenGL代码逐个顶点进行指定的。在顶点着色器中,这些属性只是简单地声明为in。
in vec4 vVertex;
in vec4 vColor;
vVertex和vColor是两个导入属性,即一个4分量顶点位置和一个4分量顶点颜色值。在GLSL中,每个顶点程序都最多可以有16个属性。
标记为in的变量是只读。对变量名进行重用,在着色器中进行一些中间计算看起来比较聪明,但是如果试图这样做的话,那么驱动中的GLSL编译器就会产生一个错误。
声明输出
out vec4 vVaryingColor;
这个变量将成为将要传送到片段着色器的顶点的颜色值。实际上,这个变量必须为片段着色器声明为一个in变量,否则在我们试图将着色器编译和连接到一起时,就会得到一个连接错误。
当在一个顶点着色器中将一个值声明为out,并在片段着色器中将其声明为in时,这个片段着色器接受的变量值为一个插补值。在默认情况下,这些工作将以一种正确透视的方式进行,并且在变量之前指定另一个额外的限定符smooth,以确保完成了这些工作。还可以指定flat声明不应进行插值,或者指定noperspective来声明在各个值之间进行直接线性插值。当使用flat时,还有必要进行一些额外的考虑。
main函数
void amin(void)
{
vVaryingColor=vColor;
gl_Position=vVertex;
}
我们将颜色输入属性分配给即将发出的插补值,并且未经变换直接将输入的顶点值分配给gl_Position。
变量gl_Position是一个预定义的内建4分量向量,它包含顶点着色器要求的一个输出。输入gl_Position的值是几何图形阶段用来装配图元。
片元着色器分析
在渲染一个图元(例如三角形)时,一旦3个顶点由顶点程序进行了处理,那么它们将组装成一个三角形,而这个三角形将由硬件进行光栅化。图形硬件确定独立片段属于(或者更精确地,在颜色缓冲区中)的什么位置,并且为三角形中的每个片段(如果不进行任何多重采样的话则只是一个点)执行片段程序的一个实例。
片段程序的最终输出颜色是一个4分量浮点向量:out vec4 vFragColor;
如果片段程序只有一个输出,那么它在内部将分配为“输出0”。这是片段着色器的第一个输出,并且将传输到由glDrawBuffer设置的缓冲区目标,默认情况下为GL_BLACK,即黑色缓冲区(对于双重缓冲环境来说是这样的)。实际颜色缓冲区常常并不包含4个浮点分量,这样输出值就会映射到目标缓冲区的范围内。
输入片段着色器是经过平滑插值的颜色值,由顶点上游传入。这只是作为一个in变量进行声明的。
in vec4 vVaryingColor;
片段着色器的主程序是将平滑插值颜色值直接分配给片段颜色。
vFragColor=vVaryingColor;
编译、绑定和连接
着色器源代码被传递给驱动程序,然后进行编译,最后进行连接,就像我们要对所有的C/C++程序做的一样。此外,着色器中的属性名需要绑定到由GLSL提供的16个预分配属性槽中的某一个。在整个过程中,我们可以检查错误,甚至可以接收从驱动程序传回的关于试图简历着色器时为什么会失败的诊断信息。
OpenGL API不支持任何类型的文件I/O操作。着色器的源代码采用什么样的方式,由程序员根据哪种方式对应用程序有利来进行编译。最简单的方式是将存储在ASCII纯文本文件中。这样要使用典型的文件系统函数从磁盘中加载文本文件就是一件很简单的事了。我们还可以使用VS2017的扩展与更新功能,下载GLSL包,并将顶点着色器和片元着色器的后缀分别改为.vert和.frag,就可以高亮显示着色器语言。
我们创建一个Shder类来加载和初始化着色器。
Shader.h
#pragma once
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
unsigned int ID; //程序ID
enum Slot
{
DIFFUSE,
SPECULAR
};
//构造器读取并构建着色器
Shader(const GLchar *vertexPath, const GLchar * fragmentPath, const char* geometryPath = nullptr);
//使用(激活)程序
void use();
//uniform工具函数
void setBool(const std::string &name, bool value)const;
void setInt(const std::string &name, int value)const;
void setFloat(const std::string &name, float value)const;
void setMat4(const std::string &name, const glm::mat4 &mat) const;
void setVec3(const std::string & name, float x, float y, float z)const;
void SetUniform3f(const char* paramNameString, glm::vec3 param);
void SetUniform1f(const char* paraNameString, float param);
void SetUniform1i(const char* paraNameString, int slot);
private:
void checkCompileErrors(GLuint shader, std::string type);
};
Shader.cpp
#include <glad/glad.h>
#include<glm\glm.hpp>
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
#include "Shader.h"
Shader::Shader(const GLchar * vertexPath, const GLchar * fragmentPath, const char * geometryPath)
{
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::string geometryCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
std::ifstream gShaderFile;
// ensure ifstream objects can throw exceptions:
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
gShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
// if geometry shader path is present, also load a geometry shader
if (geometryPath != nullptr)
{
gShaderFile.open(geometryPath);
std::stringstream gShaderStream;
gShaderStream << gShaderFile.rdbuf();
gShaderFile.close();
geometryCode = gShaderStream.str();
}
}
catch (std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char * fShaderCode = fragmentCode.c_str();
// 2. compile shaders
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// if geometry shader is given, compile geometry shader
unsigned int geometry;
if (geometryPath != nullptr)
{
const char * gShaderCode = geometryCode.c_str();
geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &gShaderCode, NULL);
glCompileShader(geometry);
checkCompileErrors(geometry, "GEOMETRY");
}
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
if (geometryPath != nullptr)
glAttachShader(ID, geometry);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessery
glDeleteShader(vertex);
glDeleteShader(fragment);
if (geometryPath != nullptr)
{
glDeleteShader(geometry);
}
}
void Shader::use()
{
glUseProgram(ID);
}
// utility uniform functions
// ------------------------------------------------------------------------
void Shader::setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
// ------------------------------------------------------------------------
void Shader::setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
// ------------------------------------------------------------------------
void Shader::setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
void Shader::checkCompileErrors(GLuint shader, std::string type)
{
GLint success;
GLchar infoLog[1024];
if (type != "PROGRAM")
{
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
}
}
else
{
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
}
}
}
void Shader::setMat4(const std::string &name, const glm::mat4 &mat) const
{
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
void Shader::setVec3(const std::string & name, float x,float y,float z) const
{
glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z);
}
void Shader::SetUniform3f(const char * paramNameString, glm::vec3 param)
{
glUniform3f(glGetUniformLocation(ID, paramNameString), param.x,param.y,param.z);
}
void Shader::SetUniform1f(const char * paraNameString, float param)
{
glUniform1f(glGetUniformLocation(ID, paraNameString), param);
}
void Shader::SetUniform1i(const char * paraNameString, int slot)
{
glUniform1i(glGetUniformLocation(ID, paraNameString), slot);
}
指定属性
使用我们的Shader类来获取顶点程序文件和片段程序文件。
Shader *ourShader = new Shader("VertexSource.vert", "fragmentSource.frag");
初始化并将模型数据加载到VAO和VBO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
连接着色器
这一步我们已经在Shader类中实现了,将着色器连接之后,我们就可以丢弃顶点着色器对象。
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
if (geometryPath != nullptr)
glAttachShader(ID, geometry);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessery
glDeleteShader(vertex);
glDeleteShader(fragment);
if (geometryPath != nullptr)
{
glDeleteShader(geometry);
}
使用着色器
我们将此功能放在Shader类的use函数中实现
void Shader::use()
{
glUseProgram(ID);
}
使用ourSahder->use来调用该功能。
然后设置绑定模型
glBindVertexArray(VAO);
绘制图形(此处为三角形)
glDrawArrays(GL_TRIANGLES, 0, 3);
此部分也可以参考文章OpenGL学习之绘制三角形