文章目录
-
17. Textures in OpenGL
-
18. Blending in OpenGL
-
19. Maths in OpenGL
-
20. Projection Matrices in OpenGL
-
21. Model View Projection Matrices in OpenGL
-
22. ImGui in OpenGL
-
23. Rendering Multiple Objects in OpenGL
-
24. Setting up a Test Framework for OpenGL
-
25. Creating Tests in OpenGL
-
26. Creating a Texture Test in OpenGL
-
27. How to make your UNIFORMS FASTER in OpenGL
17. Textures in OpenGL
用的是 stb ,地址:
https://github.com/nothings/stb/blob/master/stb_image.h
点上面这个 Raw 再复制。
在Texture类中,我们的bind函数写成这样:
void Bind(unsigned int slot = 0) const;
因为我们可以一次绑定不止一张贴图,上面代码默认slot设置为0,在 Windows 很典型的一些 modern GPU 上有 32 个Texture slots,而一些移动端比如 Android 可能就只有 8 个。
我们这里还调用了:
stbi_set_flip_vertically_on_load(1);
,这是因为 OpenGL 的纹理是左下角是 0,0 点,和 PNG 不一样,所以要先翻转。
还需要保证一些采样的操作,必修要保证下面这四个:
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
这个 S 和 T 就像 X 和 Y 轴。
这里的这个 internalformat 其实就是 GPU 端的存储格式的意思。RGBA8 表示每个通道要8位的。最后一个参数也可以写成0,表示我还没有准备好这个贴图,先给我在GPU端预分配出一个位置。
我们这里最后free掉了:
但其实你也可以保存在 CPU 端一份数据,以便做其他操作。
我们在Bind函数里还要启动这个texture到特定的槽slot上,这里是槽0:
GLCall(glActiveTexture(GL_TEXTURE0));
在glew.h中可以看到总共的槽,在Windows端这里确实是32个:
可以看到它们实际上就是一些整数,0x就是十六进制嘛,观察一下就是一些连续的整数,所以我们甚至可以这样:
GLCall(glActiveTexture(GL_TEXTURE0 + slot));
这里的slot是Bind函数的参数:
void Texture::Bind(unsigned int slot) const
然后shader中这样:
void Shader::SetUniform1i(const std::string& name, int value)
{
GLCall(glUniform1i(GetUniformLocation(name), value));
}
Application中则只需:
shader.SetUniform1i("u_Texture", 0);
,这里的0和之前我们Texture绑定(bind)的槽0对应,具体怎么对应的要在shader中体现。然后shader中片段着色器中有:
uniform sampler2D u_Texture;
和采样的代码
vec4 texColor = texture(u_Texture, v_TexCoord)
接着再加入 UV 坐标之类的。
结果是这样的:
原因是没有做blend,加入这样的代码:
GLCall(glEnable(GL_BLEND));
GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
完美!
重点:
Texture.h:
#pragma once
#include "Renderer.h"
#include <string>
class Texture
{
private:
unsigned int m_RendererID;
std::string m_FilePath;
unsigned char* m_LocalBuffer; // CPU 端存储的 Texture
int m_Width, m_Height, m_BPP; // Bit Per Pixel of the actual texture
public:
Texture(const std::string& path);
~Texture();
void Bind(unsigned int slot = 0) const;
void Unbind() const;
inline int GetWidth() const { return m_Width; };
inline int GetHeight() const { return m_Height; };
};
Texture.cpp:
#include "Texture.h"
#include "vendor/stb_image/stb_image.h"
Texture::Texture(const std::string& path)
: m_RendererID(0), m_FilePath(path), m_LocalBuffer(nullptr),
m_Width(0), m_Height(0), m_BPP(0)
{
stbi_set_flip_vertically_on_load(1); // 因为 OpenGL 是左下角是 0,0 点,和 PNG 不一样,所以要翻转
m_LocalBuffer = stbi_load(path.c_str(), &m_Width, &m_Height, &m_BPP, 4);
GLCall(glGenTextures(1, &m_RendererID));
GLCall(glBindTexture(GL_TEXTURE_2D, m_RendererID));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
GLCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
GLCall(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Width, m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_LocalBuffer));
GLCall(glBindTexture(GL_TEXTURE_2D, 0));
if (m_LocalBuffer)
stbi_image_free(m_LocalBuffer);
}
Texture::~Texture()
{
GLCall(glDeleteTextures(1, &m_RendererID));
}
void Texture::Bind(unsigned int slot) const
{
GLCall(glActiveTexture(GL_TEXTURE0 + slot));
GLCall(glBindTexture(GL_TEXTURE_2D, m_RendererID));
}
void Texture::Unbind() const
{
GLCall(glBindTexture(GL_TEXTURE_2D, 0));
}
完整代码(见我这一次的commit):
https://github.com/hebohang/OpenGL_Cherno_Tutorial/commit/625c4652d007ec2fc3747933bac9191bf562a986
18. Blending in OpenGL
看这个api,第一个参数 sfactor 即 source factor,也就是片段着色器的 output 。而 destination 即 target buffer。
控制混合:
默认的参数就相当于只用source value(1,0,GL_FUNC_ADD),即片段着色器输出啥就是啥。
然而在这里我们是:
GLCall(glEnable(GL_BLEND));
GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
即:
因此如果是透明的那么就相当于只有 destination。
如果是半透明的,在这种情况下就是:
19. Maths in OpenGL
我们用的库是 glm:
https://github.com/g-truc/glm
OpenGL Mathematics (GLM) is a header only C++ mathematics library for graphics software based on the OpenGL Shading Language (GLSL) specifications.
点这个tags:
选择下载这个 glm-0.9.9.8.zip:
源码(glm/glm):
拷贝源码所在的文件夹,放在这里:
为了便于之后查找代码,我们将整个glm包含进项目:
这里我们准备传一个 MVP 矩阵,OpenGL函数是:
glUniformMatrix4fv
,这个 v 应该是 vector 的意思,表示我们传入的参数是一个数组的形式。
最后我们写的代码为:
void Shader::SetUniformMat4f(const std::string& name, const glm::mat4& matrix)
{
GLCall(glUniformMatrix4fv(GetUniformLocation(name), 1, GL_FALSE, &matrix[0][0]));
}
因为是 4:3的大小的窗口(
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
),所以我们的正交投影写为:
glm::mat4 proj = glm::ortho(-2.0f, 2.0f, -1.5f, 1.5f, -1.0f, 1.0f);
,最终运行结果:
源代码:
https://github.com/hebohang/OpenGL_Cherno_Tutorial/commit/333b97499fad27b458af4d482209f3e14557ce49
20. Projection Matrices in OpenGL
NDC(Normalized Device Coordinates):左下角(-1,-1),右上角(1,1)
屏幕空间:左下角(0,0),右上角(width,height)
OpenGL中,NDC,即把裁剪空间变为一个立方体(通过齐次除法),这个立方体的x、y、z的分量的范围都是[-1,1],而在DirectX中z轴的分量是[0,1]。unity选择了OpenGL这种。
正交投影常用于2D场景,透视投影常用于3D场景。
21. Model View Projection Matrices in OpenGL
一般对于变换的复合,我们遵循SRT原则:即先缩放Scale,再旋转Rotate,最后平移Translate。
参考我的笔记:
https://zhuanlan.zhihu.com/p/413224014
代码:
glm::mat4 proj = glm::ortho(-2.0f, 2.0f, -1.5f, 1.5f, -1.0f, 1.0f);
glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(-1, 0, 0));
glm::mat4 mvp = proj * view;
这里相当于向左平移了一个单位,再加个缩放:
glm::mat4 proj = glm::ortho(-2.0f, 2.0f, -1.5f, 1.5f, -1.0f, 1.0f);
glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(-1, 0, 0));
glm::mat4 model = glm::scale(glm::mat4(1.0f), glm::vec3(2, 2, 2));
glm::mat4 mvp = proj * view * model;
结果:
22. ImGui in OpenGL
ImGui 地址:
https://github.com/ocornut/imgui/releases
选择下载源代码:
所有源代码就是这个文件夹下这些 .h 和 .cpp:
因为我们用的是 glfw 的上下文环境,还需要进这里:
点开之后可以看到还有这些:
把sources的这些都复制了,源代码在这个文件夹里:
最后要怎样使用就参考这个 main.cpp 就好了:
使用最新版本的 ImGui:
我这一版是 v1.86,和 cherno 的有很大差异,记录如下:
拷贝的文件如上,但是这个 helloworld 的小测试,我没有用到 imgui_impl_opengl3_loader.h,这个文件使用的是 gl3w,而我们的是 glew,会冲突,所以貌似不能包含。
因此我包含头文件如下:
#include "imgui/imgui.h"
#include "imgui/imgui_impl_glfw.h"
#include "imgui/imgui_impl_opengl3.h"
在官方提供的 main.cpp 中版本设置是这样的:
// Decide GL+GLSL versions
#if defined(IMGUI_IMPL_OPENGL_ES2)
// GL ES 2.0 + GLSL 100
const char* glsl_version = "#version 100";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
#elif defined(__APPLE__)
// GL 3.2 + GLSL 150
const char* glsl_version = "#version 150";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac
#else
// GL 3.0 + GLSL 130
const char* glsl_version = "#version 130";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
//glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only
#endif
。。。。。。
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
//io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
//io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
// Setup Dear ImGui style
ImGui::StyleColorsDark();
//ImGui::StyleColorsClassic();
// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
但是这里貌似 OpenGL 3.0 会有一些问题(或许是和我们设置的核心模式冲突?反正我是会一直出bug,调试了许久)。我最终写的代码如下:
设置 ImGui 上下文:
和原来一样的写法,我没有改:
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
。。。
/* Setup Dear ImGui context */
const char* glsl_version = "#version 130";
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
ImGui::StyleColorsDark();
测试的代码:
循环体外:
// Our state
bool show_demo_window = true;
bool show_another_window = false;
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
窗口的循环体内:
开启ImGui帧:
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
测试代码:
{
static float f = 0.0f;
static int counter = 0;
ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it.
ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too)
ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state
ImGui::Checkbox("Another Window", &show_another_window);
ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f
ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color
if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated)
counter++;
ImGui::SameLine();
ImGui::Text("counter = %d", counter);
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
ImGui::End();
}
渲染:
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
收尾的清楚工作:
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
结果:
我们更改一下测试程序的代码,可以做到控制图标的移动:
修改成这样:
{
ImGui::SliderFloat3("Translation", &translation.x, -2.0f, 2.0f);
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
}
这里的 translation 是一个vec3向量,我们将world矩阵设置为它(translate,位移,通过
glm::mat4 model = glm::translate(glm::mat4(1.0f), translation);
做到)。然后通过这个 SliderFloat3 函数直接控制位移的三个方向,并和我们之前设置的正交投影的视锥体配合:
glm::mat4 proj = glm::ortho(-2.0f, 2.0f, -1.5f, 1.5f, -1.0f, 1.0f);
提交代码:
https://github.com/hebohang/OpenGL_Cherno_Tutorial/commit/b4a527b48d552ef62ffc4d121cc52c17808ba25a
23. Rendering Multiple Objects in OpenGL
两种方式:一种是再弄一块 vertex buffer ,一种是再传入其他的 MVP 矩阵(instance实例化技术?)
这里选择第二种方式,代码如下:
绘制代码:
{
glm::mat4 model = glm::translate(glm::mat4(1.0f), translationA);
glm::mat4 mvp = proj * view * model;
shader.Bind();
shader.SetUniformMat4f("u_MVP", mvp);
renderer.Draw(va, ib, shader);
}
{
glm::mat4 model = glm::translate(glm::mat4(1.0f), translationB);
glm::mat4 mvp = proj * view * model;
shader.Bind();
shader.SetUniformMat4f("u_MVP", mvp);
renderer.Draw(va, ib, shader);
}
ImGui的代码:
{
ImGui::SliderFloat3("Translation A", &translationA.x, 0.0f, 960.0f);
ImGui::SliderFloat3("Translation B", &translationB.x, 0.0f, 960.0f);
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
}
但是这显然不是最好的方案,因为调用了两次 drawcall,费性能。真正要用在项目里头应该会弄个材质类出来。
绘制结果:
commit:
https://github.com/hebohang/OpenGL_Cherno_Tutorial/commit/9f2e2d17a458bd4b78e91f855ededd03c6188747
24. Setting up a Test Framework for OpenGL
有点单元测试的意思了:搭建一个场景专门用于某个东西的测试,比如某个复杂的shader测试,或者测试 Batch Rendering
计划:
我们希望能够用 ImGui 去做一个菜单。点击一个按键,就出来这个场景,然后可以回退到菜单界面,这个场景就随之销毁。
然后还打算做 Batch Rendering,希望写几个类,专门用来处理比如渲染的类、更新的类 之类的。
于是创建一个 Test 类,希望每次进入的时候初始化好,退出的时候就销毁掉,怎么听着就是 RAII(资源获取即初始化,Resource Acquisition Is Initialization) ?
关于 glClear、glColor 之类的API,推荐这篇博客:
https://blog.csdn.net/hebbely/article/details/69951068
参看这篇博客的第四点我们知道:
glClearColor ( ) 的作用是指定刷新颜色缓冲区时所用的颜色,所以完成一个刷新的过程是要glClearColor (COLOR) 与glClear ( GL_COLOR_BUFFER_BIT) 配合使用。
这里cherno写这个Test基类是这样的逻辑:
Test.h:
#pragma once
namespace test {
class Test
{
public:
Test() {}
virtual ~Test() {}
virtual void OnUpdate(float deltaTime) {}
virtual void OnRender() {}
virtual void OnImGuiRender() {}
};
}
可以看到很清爽,分出了三个虚函数:更新、渲染、ImGui 渲染
之后就能写一些列的测试类去继承这个基类了。比如cherno写的TestClearColor类。
关于 glClearColor 除了上面的博客,还能参考百度百科,链接如下:
https://baike.baidu.com/item/glClearColor/9516326?fr=aladdin
然后大改我们的 Application.cpp ,主要的函数如下:
test::TestClearColor test;
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
renderer.Clear();
test.OnUpdate(0.0f);
test.OnRender();
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
test.OnImGuiRender();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
}
这里貌似还没有管 OnUpdate,cherno 写的 TestClearColor 类很有学习的价值,所以我创建了一个分支专门来查看:
https://github.com/hebohang/OpenGL_Cherno_Tutorial/tree/24_Setting_up_a_Test_Framework_for_OpenGL
25. Creating Tests in OpenGL
这里我们的基类没有设置为纯虚类,是因为我们可以让有的子类只重载一个或两个,而不是每次都一定要去重载OnUpdate、OnRender、OnImGuiRender这三个函数,比如下面只重载了 OnImGuiRender 这样:
这个 TestMenu 首先要容纳所有的 tests,并且希望可以用一个具体的名字去演示这些test。因此我们即希望有一个像指针那样可以指向对应的test类,也希望可以给这些存储的test类一个具体的名字,就像在menu中的标签。
这里我们没有存储一个实际的指针,因为一个实际的指针被存储的话,就需要有一个实际存在的实例了,那么就不得不在一开始就创造所有的test类,menu选择只用作切换;但是我们不希望这样。我们不希望点一个按钮只是切换(activate),而是希望实际的 construct 一个 test instance,返回menu的时候再去delete掉它。因此不能简单地就添加一个 test 指针。因此这里要做的是提供一个 lambda 或者 function:
std::vector<std::pair<std::string, std::function<Test*()>>> m_Tests;
这个 std::function<Test*()> 就表明返回的是一个 Test* ,而这个 string 就是给我们的按钮一个标签。
可以看到,用 std::function 实际上就是把这样一个函数给包裹了起来,当点击按钮的时候再调用这个函数,从而 new 一个新的test类;而如果是用 test* 指针,就必须要new出来才能用。
似乎也可以用函数指针,但是现在一般都用 std::function ,因为其其实还能包裹仿函数(即一个重载operator() 的object),这是函数指针做不到的。一个函数指针包裹的实例代码:
#include <iostream>
#include <functional>
class A
{
public:
int val = 4;
};
int main()
{
A*(*FuncPtr)();
FuncPtr = []() { return new A(); };
A* test = FuncPtr();
std::cout << test->val << std::endl;
std::cin.get();
}
并且在菜单中我们还需要一个 CurrentTest,我们首先应当在 Application.cpp 中写出:
test::Test* current;
,然后在TestMenu类中我们实际上就是要去改变这个,所以传入的应该是一个对其的引用,所以是:
Test*& m_CurrentTest;
,即会实际修改到这个指针。
我们在 Application.cpp 中这样写:
test::Test* currentTest = nullptr;
test::TestMenu* menu = new test::TestMenu(currentTest);
currentTest = menu;
上述代码即表示一开始的时候是菜单界面。但是实际上你可以往里头添加一些命令行参数,比如加一个参数把默认界面修改到一个具体的 test 类。这样便于debug,否则每次都需要从菜单里头点进去。
然后我们希望有这样的函数:
发现 Cherno 经常从想要做什么,再去实现,我觉得是个十分需要我们学习的地方。比如上面我们希望这样注册,并且应该是一个模板函数,然后再去实现它。
最后的结果:
初始界面:
点击 Clear Color:
以及回退按钮。
完整代码:
https://github.com/hebohang/OpenGL_Cherno_Tutorial/tree/25_Creating_Tests_in_OpenGL
26. Creating a Texture Test in OpenGL
延续上一节的test框架,我们的最终目的是可以演示各种分开的技术。比如单独展示一下 shadow map 之类的。
之后我们将基于这个框架去做一个 Batch Rendering 的例子。
我们先回到这一节的提交:
点右边那个 Browse files 就能打开这一次 commit 的项目样子:
在把之前些的 Texture 测试封装成一个类之后,点击运行结果 crash 了,然后观察cherno的debug,没有停止这个程序,而是先切换到了主线程:
然后切到了crash的线程观察输出:
其实原因就是这个 vb 在构造函数里头是个栈分配的,构造函数完了就被 delete 了。
有一种设计的方式就是让这些类都只是一个 wrapper,每次提交数据给 GPU 之后就可以扔掉了;但是我们并不是这样设计的,我们是在 CPU 端一直保存着这份数据,并且通过 RAII 这样的设计,如我们设计的这个 vertex buffer 类:
构造的时候在 GPU 端也生成了一个对应的空间,而析构的时候就 delete 了,通过 glDeleteBuffers 把GPU端的存储也给释放掉。相当于是一个CPU端和GPU端的一一对应。
因此我们这里 crash 就是因为栈上的 vb 析构掉了,gpu端也释放掉了。
最后结果:
27. How to make your UNIFORMS FASTER in OpenGL
这一节主要就是想做一个缓存系统(cache)。因为每次去找这个 uniform 在shader代码的哪个位置都是需要耗费时间的。
实际上可以单独写一个程序,去读这些shader代码从而获知存储的位置,这样每次运行时我们都可以用它。这样每次去传递uniform参数的时候实际上我们已经知道位置了,而不需要借助 OpenGL 的函数去设置。
这一节只是一个 hack。
发现在之前已经实现过了。。。关键是用了个 unordered_map:
std::unordered_map<std::string, int> m_UniformLocationCache;