openGL学习笔记(更新ing)

本文章暂不介绍GLFW以及GL_GLAD的配置方法。学习赵新政

下节内容:OpenGL学习笔记——VBO与VAO-CSDN博客

初识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中,我们可以设置相应的函数指针

  1. Application内定义静态函数(如果非静态则不能被作为函数指针提供参数,下面会报错) framebufferSizecallback,并且设置到GLFW内承接Resize事件
  2. 定义Application内的函数指针类型ResizeCallback
  3. 定义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);
}

  • 20
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
课程解决的问题: 作为游戏行业或者图形学从业者,你是否面临以下问题: 到底openGL底层如何实现的? 到底矩阵操作变换是怎么做到的? 到底光栅化的算法以及原理是什么? 到底如何才能从3D世界投射到2D屏幕呢? 图形学有这么多的矩阵操作,到底如何推导如何应用呢? 学完这门课程,你应该就可以从底层了解一个初级的openGL图形接口如何实现,图形学最底层的封装到底面临哪些挑战;跟随我们一行一行写完代码,你就会得到一个迷你版本的openGL图形库,你可以深度体会图形从模型变换,观察矩阵变换,投影矩阵变换一直到光栅化纹理操作的全套模拟流程。 课程介绍: 本课程将带领学员不使用任何图形库,实现从0到1的图形学接口封装以及算法讲解,并且带领大家手敲代码,一行一行进行实现。 涵盖了(环境搭建,绘制点,Bresenham算法绘制完美直线,三角形拆分绘制算法,颜色插值算法,图片操作,图片二次插值放缩算法,纹理系统接口搭建及封装,矩阵操作理论以及实践,openGL类似接口封装,3D世界的图形学理论及接口封装等) 最终将带领大家通过C++实现一个3D世界的图形接口,方便所有人入门图形学,进行接下来的openGL接口以及GPU编程的学习   本课程为系列课程的第一步入门,且带领所有人进行实现,更加实用,可以让大家打牢图形学的基础知识及编程技能

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值