最近在学点图形学,之前只是知道渲染管线,和一些图形学大概的知识,实际上对于Shader的编写经验几乎为0。因此打算开始学学OpenGL。毕竟听说这个比较底层,虽然工作肯定是写HLSL或者Unity的ShaderLab,但对于学习图形学想必还是很有用的,网上的相关资料和教程也是相当之丰富。
最近学的基本上都可以用一个文件写下来,之后开始学对这些流程的抽象,因此现在先对写下的文件进行一个总结
使用的库为GLFW + GLEW 的经典款搭配。
基础配置篇
首先是GLFW的文件模板,官网就有,直接复制粘贴即可
GLFW是用于创建给OpenGL用的窗口,上下文,输入操作等。并且可以跨平台,很轻,因为除了这些外其他什么也不做。
GLEW则是用于抓取你显卡上已经被编写好的OpenGL的代码并供你调用的库。OpenGL的具体实现由各家硬件提供商自行提供,本身只提供接口和规范(大坨大坨的头文件)。
两个加起来的代码大概是这样的。
我将渲染循环的东西专门掏出来写了个hpp文件(.h + .cpp),方便我自己替换使用
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "Test1.hpp"
using std::cout;
using std::endl;
int main(void)
{
/* Initialize the library */
if (!glfwInit())
return -1;
/* Create a windowed mode window and its OpenGL context */
GLFWwindow* window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
/* Make the window's context current */
glfwMakeContextCurrent(window);
//这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术
glewExperimental = GL_TRUE;
//初始化GLEW(要在初始化了OpenGL上下文之后)
if (glewInit() != GLEW_OK)
return -1;
//课程代码
Test1(window);
glfwTerminate();
return 0;
}
课程代码篇
错误处理
从最基础的开始说起,毕竟做的都很底层,包装了很多东西,这个感觉就是包了个类似于lua的pcall一样的功能,不过是个低配版()
通过宏GLCall( FUNCTION )这样的调用形状,会在编译期将宏展开成
- GLClearError();
- FUNCTION
- ASSERT(GLLogCall(#x, FILE, LINE))
的形状,实际上就是用错误处理的函数包住你的函数,保证只报它的错,就像计时的开始计时和结束计时一样。
GLClearError函数尝试将所有未处理的Error取出,保证错误池在本次处理前保持空置。
GLLogCall则负责打印本次遇到的所有Error
#define ASSERT(x) if (!(x)) __debugbreak();
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall(#x, __FILE__, __LINE__));
static void GLClearError()
{
while (glGetError() != GL_NO_ERROR);
}
static bool GLLogCall(const char* function, const char* file, int line)
{
while (GLenum error = glGetError())
{
cout << "[OpenGL Error] ( " << error << " ) :\n" << function <<
"" << file << " : " << line << endl;
return false;
}
return true;
}
着色器处理
着色器源文件
首先上GLSL源文件,课程是用的自定义的文件结构和自己写的解析方法,把顶点着色器和片元着色器合并起来了,这样也和Unity的Shader语法很像。
顶点处理原封不动设置顶点位置,片元定义了一个u_Color来接收CPU端的参数进行显示。
#shader vertex
#version 330 core
layout(location = 0) in vec4 position;
void main()
{
gl_Position = position;
}
#shader fragment
#version 330 core
layout(location = 0) out vec4 color;
uniform vec4 u_Color;
void main()
{
color = u_Color;
}
着色器解析
着色器解析使用ifstream来流式读取文件,创建两个stringstream对象来分别保存顶点着色器和片元着色器的字符内容。
文件读取完成后将两个着色器的内容打成个pair返回出去。
static std::pair<string, string> ParseShader(const string& filePath)
{
ifstream stream(filePath);
enum class ShaderType
{
NONE = -1, VERTEX = 0, FRAGMENT = 1
};
string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;
while (getline(stream, line))
{
if (line.find("#shader") != string::npos)
{
if (line.find("vertex") != string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != string::npos)
type = ShaderType::FRAGMENT;
}
else if (type != ShaderType::NONE)
{
ss[(int)type] << line << "\n";
}
}
return { ss[0].str(), ss[1].str() };
}
着色器编译
单个着色器的编译,编译流程如下:
- 调用glCreateShader,输入Shader类型,返回一个创建好的Shader的Id作为其识别标志
- glShaderSource传入id对应的shader代码,glCompileShader进行指定id的Shader编译
- glGetShaderiv函数判断是否编译成功,编译失败会在控制台打印信息,并调用glDeleteShader来释放这片内存。
- 编译成功返回创建的Shader的Id
static uint CompileShader(uint type, const std::string& source)
{
uint id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE)
{
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
char* message = (char*)alloca(length * sizeof(char));
glGetShaderInfoLog(id, length, &length, message);
cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertext" : "fragment") << " shader!" << endl;
cout << message << endl;
glDeleteShader(id);
return 0;
}
return id;
}
着色器创建
glCreateProgram可以创建一个程序,可以把Program理解成在GPU端运行的一段程序,其可以挂载多个Shader(通过ID,使用glAttachShader),然后使用glLinkProgram将其链接到GPU以保证其可用。
glDeleteShader是可选的操作,最后将这段GPU程序的对象返回(也是个id)
static uint CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
uint program = glCreateProgram();
uint vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
uint fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
//已经使用了的着色器可以清理,节省一点空间但不再能调试了
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
渲染循环处理
预处理
渲染一个简单的矩形。
显卡渲染一个矩形,需要将其分割为两个三角形,总共6个顶点。其中两个顶点重合,因此可以只创建四个顶点,然后使用索引来索引四个顶点的位置。
因此需要在GPU上创建顶点的缓存和索引的缓存,这样CPU不用一直往GPU送数据(类似DrawCall)。
创建缓冲区的流程如下:
- glGenBuffers创建一个缓冲区,使用引用的方法返回缓冲区id
- glBindBuffer将其绑定到GPU上,保证其可用
- glBufferData对缓冲区内填充数据
对于顶点缓存区,需要额外设置顶点的布局,使用glVertexAttribPointer函数。
缓存创建和数据填充完成后,应用shader,使用封装好的ParseShader和CreateShader函数,最终得到一个已经链接好的,附加了顶点和片元着色器的程序Program。使用glUseProgram来应用这段程序。
接下来是在这段Program中抓取我们在Shader中定义的uniform变量u_Color,为其赋值。使用glGetUniformLocation可以抓取Program中的变量位置,然后使用glUniformXX()函数对这个区域赋值,我们用的是Vec4,因此使用glUniform4f来进行赋值。
//顶点信息
float positions[] =
{
-.5f, -.5f,
.5f, -.5f,
.5f, .5f,
-.5f, .5f,
};
//创建索引缓存
uint indices[] =
{
0, 1, 2,
2, 3, 0
};
//创建顶点缓存
uint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8, positions, GL_STATIC_DRAW);
//设置数据布局
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
//创建索引缓存
uint ibo;
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint) * 6, indices, GL_STATIC_DRAW);
//应用Shader
auto shaderSourcePair = ParseShader("res/shaders/Basic.shader");
uint shader = CreateShader(shaderSourcePair.first, shaderSourcePair.second);
GLCall(glUseProgram(shader));
int location = glGetUniformLocation(shader, "u_Color");
ASSERT(location != -1)
GLCall(glUniform4f(location, rand() % 100 / 100.f, rand() % 100 / 100.f, rand() % 100 / 100.f, 1.f));
渲染循环
接下来是真正的渲染循环,
每帧都会检查是否需要关闭窗口,如果关闭,则退出循环。
循环内流程如下:
- 清空画面残留内容
- 绘制矩形
- 双缓冲绘图交换AB缓存区
- 轮询输入事件
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
//WORK ON THERE
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr))
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
}
循环退出后记得清理Shader程序
glDeleteProgram(shader);
输入变色
因为可以从CPU端程序修改Shader参数,因此可以制作一个简易的输入响应变色功能
首先是绑定函数,如果要绑定按键输入,需要具有指定的参数的函数:
这个函数会在按下键盘的数字1时调用OnKeyPressed。
function<void()> OnKeyPressed;
void Key_Callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_1 && action == GLFW_PRESS)
{
OnKeyPressed();
}
}
OnKeyPressed在运行时定义,用于存储匿名函数(渲染循环进行前)。这样代码就能响应这个操作
//注册按键事件
glfwSetKeyCallback(window, Key_Callback);
OnKeyPressed = [location]()
{
GLCall(glUniform4f(location, rand() % 100 / 100.f, rand() % 100 / 100.f, rand() % 100 / 100.f, 1.f));
};
整体代码
Application.cpp
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "learn1.hpp"
#include "Test1.hpp"
using std::cout;
using std::endl;
int main(void)
{
/* Initialize the library */
if (!glfwInit())
return -1;
/* Create a windowed mode window and its OpenGL context */
GLFWwindow* window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
/* Make the window's context current */
glfwMakeContextCurrent(window);
//这样做能让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术
glewExperimental = GL_TRUE;
//初始化GLEW(要在初始化了OpenGL上下文之后)
if (glewInit() != GLEW_OK)
return -1;
//课程代码
Test1(window);
glfwTerminate();
return 0;
}
Test1.hpp
#pragma once
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <fstream>
#include <sstream>
#include <functional>
using uint = unsigned int;
using std::string;
using std::ifstream;
using std::cout;
using std::endl;
using std::function;
#define ASSERT(x) if (!(x)) __debugbreak();
#define GLCall(x) GLClearError();\
x;\
ASSERT(GLLogCall(#x, __FILE__, __LINE__));
static void GLClearError()
{
while (glGetError() != GL_NO_ERROR);
}
static bool GLLogCall(const char* function, const char* file, int line)
{
while (GLenum error = glGetError())
{
cout << "[OpenGL Error] ( " << error << " ) :\n" << function <<
"" << file << " : " << line << endl;
return false;
}
return true;
}
static std::pair<string, string> ParseShader(const string& filePath)
{
ifstream stream(filePath);
enum class ShaderType
{
NONE = -1, VERTEX = 0, FRAGMENT = 1
};
string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;
while (getline(stream, line))
{
if (line.find("#shader") != string::npos)
{
if (line.find("vertex") != string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != string::npos)
type = ShaderType::FRAGMENT;
}
else if (type != ShaderType::NONE)
{
ss[(int)type] << line << "\n";
}
}
return { ss[0].str(), ss[1].str() };
}
static uint CompileShader(uint type, const std::string& source)
{
uint id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE)
{
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
char* message = (char*)alloca(length * sizeof(char));
glGetShaderInfoLog(id, length, &length, message);
cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertext" : "fragment") << " shader!" << endl;
cout << message << endl;
glDeleteShader(id);
return 0;
}
return id;
}
static uint CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
uint program = glCreateProgram();
uint vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
uint fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
//已经使用了的着色器可以清理,节省一点空间但不再能调试了
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
function<void()> OnKeyPressed;
void Key_Callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_1 && action == GLFW_PRESS)
{
OnKeyPressed();
}
}
void Test1(GLFWwindow* window)
{
//顶点信息
float positions[] =
{
-.5f, -.5f,
.5f, -.5f,
.5f, .5f,
-.5f, .5f,
};
//创建索引缓存
uint indices[] =
{
0, 1, 2,
2, 3, 0
};
//创建顶点缓存
uint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8, positions, GL_STATIC_DRAW);
//设置数据布局
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
//创建索引缓存
uint ibo;
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint) * 6, indices, GL_STATIC_DRAW);
//应用Shader
auto shaderSourcePair = ParseShader("res/shaders/Basic.shader");
uint shader = CreateShader(shaderSourcePair.first, shaderSourcePair.second);
GLCall(glUseProgram(shader));
int location = glGetUniformLocation(shader, "u_Color");
ASSERT(location != -1)
GLCall(glUniform4f(location, rand() % 100 / 100.f, rand() % 100 / 100.f, rand() % 100 / 100.f, 1.f));
//注册按键事件
glfwSetKeyCallback(window, Key_Callback);
OnKeyPressed = [location]()
{
GLCall(glUniform4f(location, rand() % 100 / 100.f, rand() % 100 / 100.f, rand() % 100 / 100.f, 1.f));
};
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
//检查事件
glfwPollEvents();
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
//WORK ON THERE
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr))
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
}
glDeleteProgram(shader);
}