前言
着色器是运行在GPU上的程序,为图形渲染管线特定部分而运行,从某种意义上来说,着色器是把输入转化为输出的程序。着色器程序是完全独立的程序,着色器之间不能直接通信,只能通过输入输出实现通信。在前面的几篇文章中,粗略地介绍了一下着色器,接下来我将详细的介绍着色器。
GLSL
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。用法与特性可学习 GLSL 中文手册
封装我们自己的着色器类
通过我们几篇博客了解到着色器需要编写、编译、管理着色器,这是件麻烦事,在着色器主题的最后,我们会写一个类来让我们的生活轻松一点,它可以从硬盘读取着色器,然后编译并链接它们,并对它们进行错误检测,这就变得很好用了。这也会让你了解该如何封装目前所学的知识到一个抽象对象中。
我们会把着色器类全部放在在头文件里,主要是为了学习用途,当然也方便移植。类结构设计如下:
- 声明一个抽象类 ShaderStreamInterface,该抽象类代表一个着色器程序的 GLSL 源代码。
#define _SHADER_SRC(...) #__VA_ARGS__
#define SHADER_SRC(...) _SHADER_SRC(__VA_ARGS__)
class ShaderStreamInterface {
public:
virtual char *getVertexCode() = 0;
virtual char *getFragmentCode() = 0;
protected:
char *vertextCode;
char *fragmentCode;
};
- 声明一个着色器类 Shader,勇于编译、链接、管理着色器。
在Shader.h 文件中代码如下
#define GLEW_STATIC
#include <GL/glew.h>
#include <stdio.h>
#include <string.h>
#include <fstream>
#include <sstream>
#include <iostream>
#include "ShaderStreamInterface.h"
/** 着色器类 */
class Shader {
public:
// 程序ID
unsigned int ID;
// 构造器读取并构建着色器
Shader(ShaderStreamInterface *shaderStream);
// 使用/激活程序
Shader * use();
// uniform工具函数
Shader * setBool(const std::string &name, bool value);
Shader * setInt(const std::string &name, int value);
Shader * setFloat(const std::string &name, float value);
Shader * setColor(const std::string &name, float red, float green, float blue, float alpha);
};
在Shader.cpp 文件中的代码如下:
Shader:: Shader(ShaderStreamInterface *shaderStream) {
// 1. 从文件路径中获取顶点/片段着色器
const char* vShaderCode = shaderStream->getVertexCode();
const char* fShaderCode = shaderStream->getFragmentCode();
// 2. 编译着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];
// 顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印编译错误(如果有的话)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success) {
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 片段着色器也类似
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
// 着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印连接错误(如果有的话)
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
}
Shader * Shader:: use() {
glUseProgram(ID);
return this;
}
Shader * Shader:: setBool(const std::string &name, bool value) {
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
return this;
}
Shader * Shader:: setInt(const std::string &name, int value) {
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
return this;
}
Shader * Shader:: setFloat(const std::string &name, float value) {
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
return this;
}
Shader * Shader:: setColor(const std::string &name, float red, float green, float blue, float alpha) {
glUniform4f(glGetUniformLocation(ID, name.c_str()), red, green, blue, alpha);
return this;
}
上面的设计方式有如下几大优点:
- 功能职责分明,ShaderStreamInterface 抽象类专注于着色器代码的编写、加载和管理,,Shader 类专注于着色器的编译、链接、管理着色器的生命周期。
- 扩展性强,如果需要编写新的着色器代码,只需要新写一个类继承自 ShaderStreamInterface 即可。
- 易于调试,可以分别对 Shader 类以及 ShaderStreamInterface 其子类进行单元测试。
现在整个着色器类已经设计完成,接下来我们来完成绘制一个有颜色的三角形
绘制有颜色的三角形
- 定义 ShaderStream 类继承自 ShaderStreamInterface,并编写顶点着色器和片段着色器代码
.hpp
#include <stdio.h>
#include "ShaderStreamInterface.h"
/** 章节案例 */
class ShaderStream: public ShaderStreamInterface {
public:
ShaderStream();
char *getVertexCode() {
return vertextCode;
}
char *getFragmentCode() {
return fragmentCode;
}
protected:
char *vertexStream();
char *fragmentStream();
};
.cpp
ShaderStream:: ShaderStream() {
// 顶点着色器
vertextCode = vertexStream();
// 片段着色器代码
fragmentCode = fragmentStream();
}
char * ShaderStream:: vertexStream() {
return SHADER_SRC(
\#version 330 core\n
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
}
);
}
char * ShaderStream:: fragmentStream() {
return SHADER_SRC(
\#version 330 core\n
out vec4 FragColor;
in vec3 ourColor;
void main() {
FragColor = vec4(ourColor, 1.0);
}
);
}
- 来到主应用程序 main 函数中,引入相关类
#include "ShaderStream.hpp"
#include "Shader.hpp"
- 初始化glfw,初始化窗口,初始化 glew,前面已经讲过了,这里不再重复。
- 初始化着色器
// 着色器代码对象
ShaderStream *shaderCode = new ShaderStream();
// 着色器程序
Shader *shader = new Shader(shaderCode);
- 创建顶点数组、顶点缓冲对象、顶点数组对象,这块是跟之前你好 三角形一样一样的
// 创建顶点数组多想 VAO
float vertices[] = {
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
unsigned int vao, vbo;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
- 执行渲染循环
while (!glfwWindowShouldClose(window)) {
procesInput(window);
glClearColor(0.2, 0.2, 0.3, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
shader->use();
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
- 处理退出,释放资源
std::cout << "Hello, World!\n";
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vbo);
glDeleteProgram(shader->ID);
glfwTerminate();
运行command + R 运行,得到一个五彩斑斓的三角形:
如果你运行出来有问题或者出错了,情仔细检查一下代码,或者在本文下方下载笔者写的demo。