本文章暂不介绍GLFW以及GL_GLAD的配置方法。学习赵新政
下节内容:OpenGL学习笔记——VBO与VAO-优快云博客
初识openGL
#include<iostream>
#include "glad/glad.h"
#include <GLFW/glfw3.h>
//以上是配置好的glad以及glfw 需注意glad需要在glfw上面
接下来看看GLFW官网提供的 Example Code
#include <GLFW/glfw3.h>
int main(void)
{
//创建窗体对象
GLFWwindow* window;
//初始化GLFW
if (!glfwInit())
return -1;
//设置窗体的长和宽 ,以及窗体名字
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
//如果窗体创建失败则进行清理
glfwTerminate();
return -1;
}
//设置window这个窗体为“绘制舞台”
glfwMakeContextCurrent(window);
//执行窗体循环
while (!glfwWindowShouldClose(window))
{
/* Render here */
//清理窗口
glClear(GL_COLOR_BUFFER_BIT);
//切换双缓存
glfwSwapBuffers(window);
//接受并分发窗口消息
glfwPollEvents();
}
//清理
glfwTerminate();
return 0;
}
在我们使用的时候,会有些许不同,比如初始化我们可以这样写
glfwInit();//初始化GLFW
//设置GLFW的主版本号和次版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
//启动核心模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//使用glad加载所有opengl函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD\n";
return -1;
}
在原来的基础上加上这些即可
事件响应函数
还记得上一节中窗体循环中的 glfwPollEvents();么。
它就是用来接受并分发窗体信息,检查消息队列是否有需要处理的鼠标,键盘等信息,如果有的话就将消息批量处理清空队列。
//我们需要用到的是这两个函数 需要在窗体循环前设置
//设置监听 事件响应
glfwSetFramebufferSizeCallback(window, frameBufferSizeCallBack);
glfwSetKeyCallback(window, keyCallBack);
//其中第一个参数是窗体对象,第二个参数是函数指针
让我们进入定义,看看我们需要提供什么样的回调函数
这些参数比较简单,除了窗体对象。我们还需要宽 和 高
这些有点难懂,简单了解一下。key:字母按键码,scancode:物理按键码(不重要),action:按下还是抬起,mods:是否有Shift 或者 Ctrl
接下来我们就可以自己写回调函数啦,以下仅供参考。你们可以写自己的
void frameBufferSizeCallBack(GLFWwindow* window, int width, int height)
{
std::cout << "窗体最新大小:" << width << "," << height << std::endl;
}
void keyCallBack(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_W); //是否按下w
if (action == GLFW_PRESS);// 按压行为
if (action == GLFW_RELEASE);//松开行为
if (mods == GLFW_MOD_CONTROL);//control模式
std::cout << "按下了:" << key << std::endl;
std::cout << "action:" << action << std::endl;
std::cout << "mods" << mods << std::endl;
}
效果so COOL
函数初体验
OpenGL的运行环境是一个大的状态机,每个函数都会改变状态机的状态或者使其执行某个行为
glViewport(GLint x, GLint y,GLsizei width, GLsizei height);
设置窗口中OpenGL负责渲染的区域,即视口Viewport
x,y:表示相对窗口左下角的起始位置. width,height 表示渲染区域的长度,高度
glClearColor(GLfloat red,GLfloat green, GLfloat blue, GLfloat alpha);
设置画布清理的颜色
glClear(GL_COLOR_BUFFER_BIT);
执行画布清理工作
双缓冲:DoubleBuffer
如果直接在一张画板上绘制每一帧,就会“露馅”
双缓冲技术,在每一帧绘制任务完成后,需要把“幕后”的画布放到台前,把台前的撤到“幕后”
void glfwSwapBuffers(GLFWwindow* window);
//设置opengl视口以及清理颜色
glViewport(0, 0, 400, 600);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
//窗体循环中 切换双缓存
glfwSwapBuffers(window);
错误检查与封装
如果我们不小心写错了某个参数会怎么样?比如glClear(-1)
opengl 不会因为参数错误轻易崩溃,但错误也更难查找!OpenGL提供了错误检查函数
GLenum glGetError(); 用于检查之前的调用是否发生了问题,如果有问题则返回最近一个问题的错误码
所以我们可以根据这一特性,将其封装成CheckError文件
// 这里是头文件
#pragma once
#include<iostream>
#include <glad/glad.h>
#include<string>
#include<assert.h>
#define DEBUG
//预编译宏定义
#ifdef DEBUG
#define GL_CALL(func) func;checkError();
#else
#define GL_CALL(func) func;
#endif
void checkError();
这里的define DEBUG是为了方便是否启用检查,否则在发布时我们还得一个个删除GL_CALL
// checkError.cpp
#include "checkError.h"
void checkError() {
GLenum errorCode = glGetError();
std::string error = "";
if (errorCode != GL_NO_ERROR) {
switch (errorCode)
{
case GL_INVALID_ENUM: error = "INVALID_ENUM"; break;
case GL_INVALID_VALUE: error = "INVALID_VALUE"; break;
case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break;
case GL_OUT_OF_MEMORY: error = "OUT OF MEMORY"; break;
default:
error = "UNKNOWN";
break;
}
std::cout << error << "\n";
//assert会根据传入的bool值来决定程序是否停止
assert(false);
}
}
这里列举了常见的四种错误,ENUM,VALUE,OPERATION,MEMORY. 封装好之后我们就可以给每条语句套上,例如GL_CALL(glClear(-1) );
Application类设计
OpenGL的绘制代码会很长,所以需要做必要的封装,让代码优雅整洁
所以我们需要封装一个Application类,将窗体相关代码锁在里面,并且暴露必要的接口。
思路:3D程序都是帧循环驱动,每个对象都应该先初始化(initial),之后的每一帧更新数据或处理事件(update),退出前打扫战场(destroy)。
单例类
这里简单介绍一下单例模式。
单例模式是一种设计模式,用于确保类只有一个实例,并提供一个全局访问点。在许多情况下,单例类被用来管理全局状态或提供全局访问点。
#include <iostream>
class Singleton {
public:
// 公共静态方法,用于获取单例实例
static Singleton& getInstance() {
// 使用静态局部变量确保只创建一个实例
static Singleton instance;
return instance;
}
// 防止复制构造函数
Singleton(Singleton const&) = delete;
void operator=(Singleton const&) = delete;
// 其他公共方法和成员变量可以在这里添加
void doSomething() {
std::cout << "Singleton is doing something." << std::endl;
}
private:
// 私有构造函数,防止外部实例化
Singleton() {}
};
int main() {
// 获取单例实例
Singleton& singleton = Singleton::getInstance();
// 调用单例方法
singleton.doSomething();
return 0;
}
因此我们可以模仿上面的单例类实现Application类的封装。
#pragma once
#include<iostream>
#include <GLFW/glfw3.h>
#define app Application::getInstance()
class Application
{
public:
static Application* mInstance; //一定要放在private里面吗
//可以放在getInstance内部
~Application();
static Application* getInstance();
//访问实例的静态函数
}
private:
Application();
GLFWwindow* mWindow = nullptr;
};
cpp文件
#include"checkError.h"
#include"Application.h"
Application* Application::mInstance = nullptr;
Application* Application::getInstance() {
// 如果mInstance 已经实例化了,就直接返回
//否则先new出来 再返回
if (mInstance == nullptr) {
mInstance = new Application();
}
return mInstance;
}
Application::Application() {
}
Application::~Application() {
}
最后 我们可以在源文件中#include"Application.h" 并且#define app Application::getInstance()可以用来简化语句。我们就可以像app-> xxx 来访问类了。
成员变量与成员函数设计
成员变量思路,要有描述窗体长,宽。还要存储当前窗体对象。
mWidth, mHeight. mWindow
对外接口设计 init 负责初始化当前对象的各类参数。1,设置初始信息。2、生成窗体。3、载入OpenGL函数。
update负责每一帧更新信息或者执行工作。1、更新双缓冲。2、接受并分发窗体消息
destroy负责退出前打扫战场。执行glfwTerminate 退出gl环境
在头文件中添加这些即可,并在private: 中添加mWidth, mHeight.
先写init函数 将初始化的部分封装到init函数中(前面有展示)
bool Application::init(const int& Width,const int& Height) {
mWidth = Width; mHeight = Height;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
mWindow = glfwCreateWindow(mWidth, mHeight, "OpenGLStudy", NULL, NULL);
if (mWindow == NULL) {
return false;
}
glfwMakeContextCurrent(mWindow);
//也可以把加载glad 放进来,但是需要在Application中 #include<glad/glad.h>
return true;
}
update函数
bool Application::update() {
if (glfwWindowShouldClose(mWindow)) {
return false;
}
//切换双缓存
glfwSwapBuffers(mWindow);
glfwPollEvents();
return true;
}
destroy函数
void Application::destroy() {
//退出程序前做相关清理
glfwTerminate();
}
接下来我们就可以在main函数中使用这些封装,替换之前的语句
int main(){
//初始化的封装
if (!(app->init(800, 600) )) {
return -1;
}
//设置监听 暂未封装
//使用glad加载所有opengl函数 (可以移到init中)
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD\n";
return -1;
}
while (app->update())
{
GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
//渲染操作
}
app->destroy();
}
事件回调响应
我们可以通过使用函数指针,来为用户提供接口
Resize 设计思路:在main.cpp中,我们可以写自己的Resize函数,并且通过app->setResizeCallback(Resize);来向Application类提供自己的回调函数
在Application.h中,我们可以设置相应的函数指针
- Application内定义静态函数(如果非静态则不能被作为函数指针提供参数,下面会报错) framebufferSizecallback,并且设置到GLFW内承接Resize事件
- 定义Application内的函数指针类型ResizeCallback
- 定义Application内的ResizeCallback类型成员变量mResizeCallback
"Application.h文件"
//以下只展示添加的内容
using ResizeCallback = void(*)(int width, int height);
class Application(){
public:
void setResizeCallback(ResizeCallback callback) { mResizeCallback = callback; } //函数指针
private:
//不让外界调用
static void frameBufferSizeCallback(GLFWwindow* window, int width, int height);
private:
ResizeCallback mResizeCallback = nullptr; //回调函数
};
"Application.cpp文件"
bool Application::init(int Width,int Height) {
glfwSetFramebufferSizeCalback(mWindow, frameBufferSizeCallback);
//为其提供静态函数
return true;
}
void Application::frameBufferSizeCallback(GLFWwindow* window, int width, int height) {
std::cout << "Resize" << std::endl;
//即使mInstance设置为私有 静态函数也可以访问
if(mInstance->mResizeCallback != nullptr){
mInstance->mResizeCallback(width,height);
}
}
添加完这些以后,我们便可以在我们的main()函数中通过app->setResizeCallback(Resize);
void OnResize(int width, int height) {
GL_CALL(glViewport(0, 0, width, height));
std::cout << "OnResize" << std::endl;
}
流程大概是,init 函数先设置回调函数为frameBufferSizeCallback ,我们再通过app->setResizeCallback(Resize); 使得frameBufferSizeCallback 内部调用的函数指针发生改变,进而在窗体循环中使用我们的Resize函数进行回调
可选做法:学会使用glfw的UserPointer
在init函数中,glfwSetWindowUserPointer(mWindow, this);
将我们当前的Application对象暂时存到mWindow里面
void Application::frameBufferSizeCallback(GLFWwindow* window, int width, int height) {
std::cout << "Resize" << std::endl;
Application* self = (Application*)glfwGetWindowUserPointer(window); //将刚才存储的 实例拿出来
self->mResizeCallback(width,height);
}
Application类完整版(添加了键盘响应)
键盘响应类似于上面的Resize响应,所以我将给出所有Application类代码
"Application.h"
#pragma once
#include<iostream>
#include <GLFW/glfw3.h>
using ResizeCallback = void(*)(int width, int height);
using KeyBoardCallback = void(*)(int key,int action,int mods);
#define app Application::getInstance()
class Application
{
public:
~Application();
static Application* getInstance();
//访问实例的静态函数
bool init(int,int);
bool update();
void destroy();
uint32_t getWidth() const { return mWidth; }
uint32_t getHeight() const { return mHeight; }
void setResizeCallback(ResizeCallback callback) { mResizeCallback = callback; } //函数指针
void setKeyBoardCallback(KeyBoardCallback callback) { mKeyBoardCallback = callback; }
private:
//回调函数,提供给glfw
static void frameBufferSizeCallback(GLFWwindow* window, int width, int height);
static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
private:
Application();
static Application* mInstance;
uint32_t mWidth = 0;
uint32_t mHeight = 0;
GLFWwindow* mWindow = nullptr;
//外部可设置的函数指针
ResizeCallback mResizeCallback = nullptr;
KeyBoardCallback mKeyBoardCallback = nullptr;
};
#include"checkError.h"
#include"Application.h"
Application* Application::mInstance = nullptr;
Application* Application::getInstance() {
// 如果mInstance 已经实例化了,就直接返回
//否则先new出来 再返回
if (mInstance == nullptr) {
mInstance = new Application();
}
return mInstance;
}
Application::Application() {
}
Application::~Application() {
}
bool Application::init(int Width,int Height) {
mWidth = Width; mHeight = Height;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
mWindow = glfwCreateWindow(mWidth, mHeight, "OpenGLStudy", NULL, NULL);
if (mWindow == NULL) {
return false;
}
glfwMakeContextCurrent(mWindow);
//this 就是当前全局唯一的Application对象
glfwSetWindowUserPointer(mWindow, this);
//键盘相应
glfwSetKeyCallback(mWindow, keyCallback);
glfwSetWindowSizeCallback(mWindow, frameBufferSizeCallback);
return true;
}
bool Application::update() {
if (glfwWindowShouldClose(mWindow)) {
return false;
}
//切换双缓存
glfwSwapBuffers(mWindow);
glfwPollEvents();
return true;
}
void Application::destroy() {
//退出程序前做相关清理
glfwTerminate();
}
void Application::frameBufferSizeCallback(GLFWwindow* window, int width, int height) {
std::cout << "Resize" << std::endl;
Application* self = (Application*)glfwGetWindowUserPointer(window); //将刚才存储的 实例拿出来
self->mResizeCallback(width,height);
}
void Application::keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
Application* self = (Application*)glfwGetWindowUserPointer(window);
if (self->mKeyBoardCallback != nullptr)
self->mKeyBoardCallback(key,action,mods);
}