OpenGL编译用户着色器shader
shader相信很多朋友们都听说过,shader就是运行再GPU上的程序。虽然是这么说,但是我们发现,很多IDE开发工具比如说visual studio 没有办法直接去运行shader代码。这是因为,许多编译器不会自动将shader文件编译成可执行的代码然后发送给GPU,shader代码的编译需要开发人员手动动用GPU的shader编译接口,将对应的代码编译成可执行的程序。在这里,笔者就为大家介绍,如何使用OpenGL提供的API编译用户自己的shader程序。
Shader
OpenGL渲染管线
这里先为大家介绍一下OpenGL渲染管线如下图所示
事实上在一众着色器当中,只有顶点着色器和片元着色器是必须的,其他都是可选的。具体想要了解更多,可以去看一些资料或者书籍,比如笔者手上的这本《OpenGL编程指南》
OpenGL shader
OpenGL 支持的 shader 语法是 OpenGL shader language 简称是glsl。这个shader语法和C++基本上是大同小异,很快就可以轻松上手。
话说到这里,让我们回顾一下上一篇文章OpenGL渲染结果移至ImGui窗口上,有细心的本有不难发现,我并没有去编译用户着色器,而且必要的顶点着色器和片元着色器都没有进行编写,但是我们仍然得到我们想要的渲染结果。其原因是,OpenGL自带默认的着色器,所以对于初学者来说,可以尽可能使用少的代码去实现自己想要的结果,这就是为什么图形接口的学习一般都是从OpenGL开始学起。
OpenGL着色器编译
Shader 类
这里笔者写了一个Shader类,整体的代码如下
Shader.h
#pragma once
#include<unordered_map>
typedef unsigned int GLenum;
class Shader {
public:
Shader(const std::string& filePath);
~Shader();
void Bind();
void UBind();
void UploadUniformFloat4(const std::string& name, float* value);
private:
std::string ReadFile(const std::string& filePath);
std::unordered_map<GLenum, std::string> PreProcess(const std::string& source);
void Compile(const std::unordered_map<GLenum, std::string>& shaderSources);
private:
uint32_t m_ShaderID;
std::string m_Name;
};
Shader.cpp
#include<glad/glad.h>
#include<string>
#include<array>
#include<fstream>
#include<iostream>
#include"Shader.h"
static GLenum ShaderTypeFromString(const std::string& type) {
if (type == "vertex")
return GL_VERTEX_SHADER;
else if (type == "fragment" || type == "pixel")
return GL_FRAGMENT_SHADER;
std::cout << "Unknown shader type" << std::endl;
return 0;
}
Shader::Shader(const std::string& filePath) :m_ShaderID(0) {
std::string source = ReadFile(filePath);
auto shaderSource = PreProcess(source);
Compile(shaderSource);
auto lastSlash = filePath.find_last_of("/\\");
lastSlash = lastSlash == std::string::npos ? 0 : lastSlash + 1;
auto lastDot = filePath.rfind('.');
auto count = lastDot == std::string::npos ? filePath.size() - lastSlash : lastDot - lastSlash;
m_Name = filePath.substr(lastSlash, count);
}
Shader::~Shader() {
glDeleteProgram(m_ShaderID);
}
void Shader::Bind(){
glUseProgram(m_ShaderID);
}
void Shader::UBind(){
glUseProgram(0);
}
void Shader::UploadUniformFloat4(const std::string& name, float* value) {
int location = glGetUniformLocation(m_ShaderID, name.c_str());
glUniform4f(location, value[0], value[1], value[2], value[3]);
}
std::string Shader::ReadFile(const std::string& filePath) {
std::string result;
std::ifstream in(filePath, std::ios::in | std::ios::binary);
if (in) {
in.seekg(0, std::ios::end);
result.resize(in.tellg());
in.seekg(0, std::ios::beg);
in.read(&result[0], result.size());
in.close();
}
else {
std::cout << "着色器文件没有正常打开" << std::endl;
__debugbreak();
}
return result;
}
std::unordered_map<GLenum, std::string> Shader::PreProcess(const std::string& source) {
std::unordered_map<GLenum, std::string> shaderSources;
const char* typeToken = "#type";
size_t typeTokenLength = strlen(typeToken);
size_t pos = source.find(typeToken,0);
while (pos != std::string::npos) {
size_t eol = source.find_first_of("\r\n", pos);
if (eol == std::string::npos) {
std::cout << "着色器语法出错" << std::endl;
__debugbreak();
}
size_t begin = pos + typeTokenLength + 1;
std::string type = source.substr(begin, eol - begin);
if (!ShaderTypeFromString(type)) {
std::cout << "这是一个不合法的着色器类型" << std::endl;
__debugbreak();
}
size_t nextLinePos = source.find_first_of("\r\n", eol);
pos = source.find(typeToken, nextLinePos);
shaderSources[ShaderTypeFromString(type)] = source.substr(nextLinePos, pos - (nextLinePos == std::string::npos ? source.size() - 1 : nextLinePos));
}
return shaderSources;
}
void Shader::Compile(const std::unordered_map<GLenum, std::string>& shaderSources) {
unsigned int program = glCreateProgram();
//一次性至多编译两种着色器
if (shaderSources.size() < 2) {
std::cout << "一次性至多编译两种着色器" << std::endl;
__debugbreak();
}
std::array<GLenum, 2> glShaderIDs;
int glShaderIDIndex = 0;
for (auto& kv : shaderSources) {
GLenum type = kv.first;
const std::string& source = kv.second;
unsigned int shader = glCreateShader(type);
const char* sourceCStr = source.c_str();
glShaderSource(shader, 1, &sourceCStr, 0);
glCompileShader(shader);
int isCompiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
if (isCompiled == GL_FALSE) {
int maxLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
std::vector<char> infoLog(maxLength);
glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]);
glDeleteShader(shader);
std::cout << "着色器编译出错:" << infoLog.data() << std::endl;
__debugbreak();
break;
}
glAttachShader(program, shader);
glShaderIDs[glShaderIDIndex++] = shader;
}
m_ShaderID = program;
// Link our program
glLinkProgram(program);
// Note the different functions here: glGetProgram* instead of glGetShader*.
int isLinked = 0;
glGetProgramiv(program, GL_LINK_STATUS, (int*)&isLinked);
if (isLinked == GL_FALSE) {
int maxLength = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength);
// The maxLength includes the NULL character
std::vector<char> infoLog(maxLength);
glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]);
// We don't need the program anymore.
glDeleteProgram(program);
for (auto id : glShaderIDs)
glDeleteShader(id);
std::cout << "用户着色器链接失败:" << infoLog.data() << std::endl;
__debugbreak();
return;
}
for (auto id : glShaderIDs)
glDetachShader(program, id);
}
着色器代码
TextureShader.glsl
#type vertex
#version 450 core
//标记为0的内存位置输入一个有两个分量的向量,这是顶点的位置
layout(location = 0) in vec2 v_Position;
void main(){
//顶点位置的数据进行赋值,需要转换为齐次向量
gl_Position = vec4(v_Position,0.0f,1.0f);
}
#type fragment
#version 450 core
//标记为0的内存位置输出一个有四个分量的向量,这是像素的颜色
layout(location = 0) out vec4 o_Color;
void main(){
o_Color = vec4(0.8f,0.2f,0.3f,1.0f);
}
着色器介绍
上面虽然是一个着色器文件,其实这里面写了两个着色器,一个是顶点着色器,一个是片元着色器。#type vertex 下面的是顶点着色器,#type fragment 下面的是片元着色器。为什么这两个要一起写了?前面也介绍了,这两个着色器是必需要有的,所以笔者推荐这个着色器最好就是一起写。顶点着色器必须要有输入数据,片元着色器必须要有输出数据,不然屏幕上就看不到任何东西。
着色器编译
笔者将其分成了3个步骤进行
1、读取对应的文件内容
std::string Shader::ReadFile(const std::string& filePath) {
std::string result;
std::ifstream in(filePath, std::ios::in | std::ios::binary);
if (in) {
in.seekg(0, std::ios::end);
result.resize(in.tellg());
in.seekg(0, std::ios::beg);
in.read(&result[0], result.size());
in.close();
}
else {
std::cout << "着色器文件没有正常打开" << std::endl;
__debugbreak();
}
return result;
}
将TextureShader.glsl当中的文本信息全部转换成一个string类型当中进行存储。
2、确定着色器的类型,以及每个着色器的代码
std::unordered_map<GLenum, std::string> Shader::PreProcess(const std::string& source) {
std::unordered_map<GLenum, std::string> shaderSources;
const char* typeToken = "#type";
size_t typeTokenLength = strlen(typeToken);
size_t pos = source.find(typeToken,0);
while (pos != std::string::npos) {
size_t eol = source.find_first_of("\r\n", pos);
if (eol == std::string::npos) {
std::cout << "着色器语法出错" << std::endl;
__debugbreak();
}
size_t begin = pos + typeTokenLength + 1;
std::string type = source.substr(begin, eol - begin);
if (!ShaderTypeFromString(type)) {
std::cout << "这是一个不合法的着色器类型" << std::endl;
__debugbreak();
}
size_t nextLinePos = source.find_first_of("\r\n", eol);
pos = source.find(typeToken, nextLinePos);
shaderSources[ShaderTypeFromString(type)] = source.substr(nextLinePos, pos - (nextLinePos == std::string::npos ? source.size() - 1 : nextLinePos));
}
return shaderSources;
}
对于OpenGL来说我们不光要告诉它需要编译的代码,还要告诉它编译的着色器代码是什么类型的着色器代码。在前面可以看到TextureShader.glsl当中有 #type vertex 这样的语句,这个并不是glsl语法,我们在进行文本处理的时候需要省略掉才行,不然的话编译会失败,这个只是用来告诉程序下面着色器代码是什么类型着色器的,所以这里选择返回了一个字典,用来存储着色器的类型和需要编译的程序。能够编译的是下面两段
#version 450 core
//标记为0的内存位置输入一个有两个分量的向量,这是顶点的位置
layout(location = 0) in vec2 v_Position;
void main(){
//顶点位置的数据进行赋值,需要转换为齐次向量
gl_Position = vec4(v_Position,0.0f,1.0f);
}
#version 450 core
//标记为0的内存位置输出一个有四个分量的向量,这是像素的颜色
layout(location = 0) out vec4 o_Color;
void main(){
o_Color = vec4(0.8f,0.2f,0.3f,1.0f);
}
他们已经被分开存储了。
3、编译链接着色器
void Shader::Compile(const std::unordered_map<GLenum, std::string>& shaderSources) {
//注册使用下面两个着色器的程序号
unsigned int program = glCreateProgram();
//一次性至多编译两种着色器
if (shaderSources.size() < 2) {
std::cout << "一次性至多编译两种着色器" << std::endl;
__debugbreak();
}
std::array<GLenum, 2> glShaderIDs;
int glShaderIDIndex = 0;
for (auto& kv : shaderSources) {
GLenum type = kv.first;
const std::string& source = kv.second;
//注册对饮类型的着色器
unsigned int shader = glCreateShader(type);
const char* sourceCStr = source.c_str();
glShaderSource(shader, 1, &sourceCStr, 0);
//编译着色器源码
glCompileShader(shader);
int isCompiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
//检查着色器是否编译失败
if (isCompiled == GL_FALSE) {
int maxLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
std::vector<char> infoLog(maxLength);
glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]);
glDeleteShader(shader);
std::cout << "着色器编译出错:" << infoLog.data() << std::endl;
__debugbreak();
break;
}
//将着色器加入到这个程序当中
glAttachShader(program, shader);
glShaderIDs[glShaderIDIndex++] = shader;
}
m_ShaderID = program;
// Link our program
glLinkProgram(program);
// Note the different functions here: glGetProgram* instead of glGetShader*.
int isLinked = 0;
glGetProgramiv(program, GL_LINK_STATUS, (int*)&isLinked);
//检查程序是否能够链接成功
if (isLinked == GL_FALSE) {
int maxLength = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength);
// The maxLength includes the NULL character
std::vector<char> infoLog(maxLength);
glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]);
// We don't need the program anymore.
glDeleteProgram(program);
for (auto id : glShaderIDs)
glDeleteShader(id);
std::cout << "用户着色器链接失败:" << infoLog.data() << std::endl;
__debugbreak();
return;
}
for (auto id : glShaderIDs)
glDetachShader(program, id);
}
上面大致流程就是,注册程序的编号,创建对应类型的着色器,根据下面的代码
static GLenum ShaderTypeFromString(const std::string& type) {
if (type == "vertex")
return GL_VERTEX_SHADER;
else if (type == "fragment" || type == "pixel")
return GL_FRAGMENT_SHADER;
std::cout << "Unknown shader type" << std::endl;
return 0;
}
可以知道 #type vertex 对应的着色器类型就是GL_VERTEX_SHADER,#type fragment 对应的着色器类型就是GL_FRAGMENT_SHADER。创建了对应的着色器类型过后就是对源码进行编译,放入到程序当中,检查这个程序能否顺利接入管线当中,隔离开然后等待被调用。
使用用户自定义着色器
着色器使用,主函数代码如下
#include<glad/glad.h>
#include<GLFW/glfw3.h>
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include<iostream>
#include"FrameBuffer.h"
#include"Shader.h"
int main() {
glfwInit();
GLFWwindow* window = glfwCreateWindow(640, 480, "Triangles", NULL, NULL);
glfwMakeContextCurrent(window);
glfwSwapInterval(1); // Enable vsync
// 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
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows
//io.ConfigViewportsNoAutoMerge = true;
//io.ConfigViewportsNoTaskBarIcon = true;
// Setup Dear ImGui style
ImGui::StyleColorsDark();
//ImGui::StyleColorsLight();
// When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
ImGuiStyle& style = ImGui::GetStyle();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 130");
//需要初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
float positions[6] = {
-0.5f, -0.5,
0.0f, 0.5f,
0.5f, -0.5f
};
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), NULL);
glEnableVertexAttribArray(0);
bool show_demo_window = true;
ImVec2 viewPortSize(640,480);
float colorEditor[4] = {1.0f, 1.0f, 1.0f, 1.0f};
FrameBuffer *pFrameBuffer = new FrameBuffer(640, 480);
Shader* pShader = new Shader("assets/shaders/TextureShader.glsl");
pShader->UBind();
while (!glfwWindowShouldClose(window)) {
pFrameBuffer->Bind();
pShader->Bind();
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
pFrameBuffer->UBind();
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
ImGui::Begin("ViewPort");
viewPortSize = ImGui::GetContentRegionAvail();
if (viewPortSize.x * viewPortSize.y > 0 && (viewPortSize.x != pFrameBuffer->GetWidth() || viewPortSize.y != pFrameBuffer->GetHeight())) {
pFrameBuffer->Resize(viewPortSize.x, viewPortSize.y);
glViewport(0, 0, viewPortSize.x, viewPortSize.y);
}
uint32_t textureID = pFrameBuffer->GetColorAttachment();
ImGui::Image(reinterpret_cast<void*>(textureID), viewPortSize, { 0,1 }, { 1,0 });
ImGui::End();
ImGui::Begin("ColorEditor");
ImGui::ColorEdit4("##colorEditor", colorEditor);
ImGui::End();
/*if(show_demo_window)
ImGui::ShowDemoWindow(&show_demo_window);*/
// Rendering
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
GLFWwindow* backup_current_context = glfwGetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
glfwMakeContextCurrent(backup_current_context);
}
glfwSwapBuffers(window);
glfwPollEvents();
}
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
delete pFrameBuffer;
delete pShader;
glfwDestroyWindow(window);
glfwTerminate();
}
得到的结果是
三角形被顺利染成了红色,有人可能会说这也有点费了这么大的劲,就把颜色改成了红色,实在是有点无聊,那让我们来做一些比较Cool的事。
我们修改一下着色器
#type vertex
#version 450 core
layout(location = 0) in vec2 v_Position;
void main(){
gl_Position = vec4(v_Position,0.0f,1.0f);
}
#type fragment
#version 450 core
layout(location = 0) out vec4 o_Color;
//增加的片段
uniform vec4 u_Color;
void main(){
o_Color = u_Color;
}
主函数也修改一下
pShader->UBind();
while (!glfwWindowShouldClose(window)) {
pFrameBuffer->Bind();
pShader->Bind();
//新增片段
pShader->UploadUniformFloat4("u_Color", colorEditor);
glClear(GL_COLOR_BUFFER_BIT);
展示一下结果
我们现在可以通过ImGui上面的控件对三角形的颜色进行实时修改了,不用去改动程序,是不是很棒了。下面还是把整个主函数放出来,如果对里面的FrameBuffer类不了解的可以看笔者的OpenGL渲染结果移至ImGui窗口上这篇文章,同样有源代码,希望对大家能有帮助。
#include<glad/glad.h>
#include<GLFW/glfw3.h>
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include<iostream>
#include"FrameBuffer.h"
#include"Shader.h"
int main() {
glfwInit();
GLFWwindow* window = glfwCreateWindow(640, 480, "Triangles", NULL, NULL);
glfwMakeContextCurrent(window);
glfwSwapInterval(1); // Enable vsync
// 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
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows
//io.ConfigViewportsNoAutoMerge = true;
//io.ConfigViewportsNoTaskBarIcon = true;
// Setup Dear ImGui style
ImGui::StyleColorsDark();
//ImGui::StyleColorsLight();
// When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
ImGuiStyle& style = ImGui::GetStyle();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 130");
//需要初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
float positions[6] = {
-0.5f, -0.5,
0.0f, 0.5f,
0.5f, -0.5f
};
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), NULL);
glEnableVertexAttribArray(0);
bool show_demo_window = true;
ImVec2 viewPortSize(640,480);
float colorEditor[4] = {1.0f, 1.0f, 1.0f, 1.0f};
FrameBuffer *pFrameBuffer = new FrameBuffer(640, 480);
Shader* pShader = new Shader("assets/shaders/TextureShader.glsl");
pShader->UBind();
while (!glfwWindowShouldClose(window)) {
pFrameBuffer->Bind();
pShader->Bind();
pShader->UploadUniformFloat4("u_Color", colorEditor);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
pFrameBuffer->UBind();
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
ImGui::Begin("ViewPort");
viewPortSize = ImGui::GetContentRegionAvail();
if (viewPortSize.x * viewPortSize.y > 0 && (viewPortSize.x != pFrameBuffer->GetWidth() || viewPortSize.y != pFrameBuffer->GetHeight())) {
pFrameBuffer->Resize(viewPortSize.x, viewPortSize.y);
glViewport(0, 0, viewPortSize.x, viewPortSize.y);
}
uint32_t textureID = pFrameBuffer->GetColorAttachment();
ImGui::Image(reinterpret_cast<void*>(textureID), viewPortSize, { 0,1 }, { 1,0 });
ImGui::End();
ImGui::Begin("ColorEditor");
ImGui::ColorEdit4("##colorEditor", colorEditor);
ImGui::End();
/*if(show_demo_window)
ImGui::ShowDemoWindow(&show_demo_window);*/
// Rendering
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
GLFWwindow* backup_current_context = glfwGetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
glfwMakeContextCurrent(backup_current_context);
}
glfwSwapBuffers(window);
glfwPollEvents();
}
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
delete pFrameBuffer;
delete pShader;
glfwDestroyWindow(window);
glfwTerminate();
}
原文地址:https://blog.csdn.net/qq_45651072/article/details/144310315
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!