Bootstrap

【C++/OpenGL】OpenGL初步学习——绘制一个矩形

最近在学点图形学,之前只是知道渲染管线,和一些图形学大概的知识,实际上对于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 )这样的调用形状,会在编译期将宏展开成

  1. GLClearError();
  2. FUNCTION
  3. 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);
}
;