使用 GLFW 在 OpenGL 的场景中漫游
前言
前面已经建立了 OpenGL 框架,加载了 3D 模型,但是还没有在场景中漫游的功能。为了展示 3D 模型,我只是简单地利用变换视图矩阵的方式使模型在视野中旋转。同时,之前的程序连最简单的改变窗口大小的功能都没有,不能放大窗口而观察模型的更多细节。从这一节开始,我要实现在场景中漫游的功能。
功能的设计很简单,就像所有的 FPS 游戏一样,按A
W
S
D
进行前进后退和左右移动,使用鼠标控制方向,为了简单起见,暂时只考虑左右转动,不实现上下转动的功能。
改变窗口大小
改变窗口大小的功能很简单,添加一个 static 的 onWindowSize() 函数就可以了,然后调用 glfwSetWindowSizeCallback() 注册这个回调函数。添加这个功能后,我们就可以把窗口放大到全屏了,如下图:
切换线框模式和填充模式
前面一直使用的是线框模型,这里可以设置按M
键来切换线框模式和填充模式。这里可以先编写一个 onKey() 方法,然后使用 glfwSetSetKeyCallback() 来设置回调。
前后左右移动摄像机
这时不能使用 glfwSetSetKeyCallback() 来设置回调,因为 onKey() 方法只在每次按键的时候调用一次,即使按着键不动,它也不会连续调用,不符合我们的要求。这时,需要在每一帧的绘图函数里面调用 processInput() 方法,并在 processInput() 方法里面调用 glfwGetKey() 来实现这个效果。
另外,我们的视图矩阵要改了。我们可以在 App 类里面设置三个变量,cameraPosition、cameraFront、cameraUp,分别代表摄像机的位置、前方、上方,然后使用 GLM 的 lookAt() 函数来设置视图矩阵。
根据 3D 场景的复杂程度不同,其渲染速度也会不同,为了保证我们移动速度的一致性,我这里顺便搞一个计算帧率的功能。
左右转动视角
使用 GLFW 的鼠标回调函数,可以很方便地得到鼠标的 X 坐标和 Y 坐标,因此实现左右转动视角的功能非常方便。我没有使用很复杂的三角函数计算,只是利用 GLM 的 rotate() 函数对 cameraFront 向量进行旋转就可以了。我们还可以充分利用 GLFW 捕获鼠标功能,设计为在窗口中点击鼠标后捕获鼠标指针,按ESC
键后释放鼠标捕获,只有在捕获鼠标指针的状态下才能够左右旋转视角。这时主要用到的 API 是 glfwSetCursorPosCallback() 和 glfwSetMouseButtonCallback()。
经过修改后的 app.hpp 完整代码如下:
#ifndef __APP_HPP__
#define __APP_HPP__
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class App
{
private:
const int SCR_WIDTH = 1920;
const int SCR_HEIGHT = 1080;
public:
static App *the_app;
float aspect;
glm::vec3 cameraPosition;
glm::vec3 cameraFront;
glm::vec3 cameraUp;
float cameraSpeed;
double timeFrameStart;
double timeFrameEnd;
double timeAccumulate;
int countFrames;
bool showFps;
bool firstMouse;
double lastX;
bool captureCursor;
App()
{
aspect = (float)SCR_WIDTH / (float)SCR_HEIGHT;
cameraPosition = glm::vec3(0.0f, 0.0f, 0.0f);
cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
firstMouse = true;
}
static void onWindowSize(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
the_app->aspect = (float)width / (float)height;
}
static void onKey(GLFWwindow *window, int key, int scancode, int action, int mods)
{
if (action == GLFW_PRESS)
{
switch (key)
{
case GLFW_KEY_M: //切换线框模式和填充模式
{
static GLenum mode = GL_FILL;
mode = (mode == GL_FILL ? GL_LINE : GL_FILL);
glPolygonMode(GL_FRONT_AND_BACK, mode);
return;
}
case GLFW_KEY_ESCAPE: //停止鼠标捕获,主要是应付鼠标被捕获的情况
{
if (the_app->captureCursor)
{
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
the_app->captureCursor = false;
}
return;
}
case GLFW_KEY_F: //打开和关闭输出fps的功能,输出到控制台
{
the_app->showFps = (the_app->showFps == false ? true : false);
return;
}
}
}
}
virtual void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
{
cameraPosition += cameraSpeed * cameraFront;
}
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
{
cameraPosition -= cameraSpeed * cameraFront;
}
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
{
cameraPosition += cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
}
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
{
cameraPosition -= cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
}
}
static void onMouseMove(GLFWwindow *window, double xpos, double ypos)
{
//std::cout << "xpos:" << xpos << " ypos:" << ypos << std::endl;
if (!the_app->captureCursor)
{
return;
}
if (the_app->firstMouse)
{
the_app->lastX = xpos;
the_app->firstMouse = false;
return;
}
double xoffset = xpos - the_app->lastX;
the_app->lastX = xpos;
double sensitivity = 0.005f; //灵敏度
xoffset *= sensitivity;
glm::mat4 I(1.0f);
glm::vec3 Y(0.0f, 1.0f, 0.0f);
the_app->cameraFront = glm::vec3(glm::vec4(the_app->cameraFront, 1.0f) * glm::rotate(I, (float)xoffset, Y));
}
static void onMouseButton(GLFWwindow *window, int button, int action, int mods)
{
if (action == GLFW_PRESS)
{
switch (button)
{
case GLFW_MOUSE_BUTTON_LEFT:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
the_app->captureCursor = true;
return;
}
}
}
virtual void init()
{
}
virtual void display()
{
}
virtual void run(App *app)
{
if (the_app != NULL)
{ //同一时刻,只能有一个App运行
std::cerr << "The the_app is already run." << std::endl;
return;
}
the_app = app;
glfwInit();
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "StudyOpenGL", NULL, NULL);
if (window == NULL)
{
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return;
}
glfwMakeContextCurrent(window);
glfwSetWindowSizeCallback(window, onWindowSize);
glfwSetKeyCallback(window, onKey);
glfwSetCursorPosCallback(window, onMouseMove);
glfwSetMouseButtonCallback(window, onMouseButton);
if (glewInit() != GLEW_OK)
{
std::cerr << "Failed to initalize GLEW" << std::endl;
return;
}
init(); //Init主要是用来创建VAO、VBO等,并准备要各种数据
while (!glfwWindowShouldClose(window))
{
//记录帧渲染之前的时间
timeFrameStart = glfwGetTime();
display(); //这里才是渲染图形的主战场
//记录帧渲染之后的时间,并计算帧率,如果输出帧率,则每100帧输出一次,同时计算cameraSpeed;
timeFrameEnd = glfwGetTime();
double timeInterval = timeFrameEnd - timeFrameStart;
if (showFps)
{
if (countFrames < 100)
{
countFrames++;
timeAccumulate += timeInterval;
}
else
{
std::cout << "FPS: " << 100.0 / timeAccumulate << std::endl;
countFrames = 0;
timeAccumulate = 0;
}
}
cameraSpeed = 200.0f * (float)timeInterval;
glfwSwapBuffers(window);
processInput(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
return;
}
};
App *App::the_app = NULL;
#define DECLARE_MAIN(a) \
int main(int argc, const char **argv) \
{ \
a *app = new a; \
app->run(app); \
delete app; \
return 0; \
}
#endif
然后,我们的 WanderInScene.cpp 的完整内容如下:
#include "../include/app.hpp"
#include "../include/shader.hpp"
#include "../include/model.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class MyApp : public App {
private:
const GLfloat clearColor[4] = {0.2f, 0.3f, 0.3f, 1.0f};
Model lita;
Shader* simpleShader;
public:
void init(){
ShaderInfo shaders[] = {
{GL_VERTEX_SHADER, "simpleShader.vert"},
{GL_FRAGMENT_SHADER, "simpleShader.frag"},
{GL_NONE, ""}
};
simpleShader = new Shader(shaders);
lita.loadModel("lita.obj");
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );
}
void display(){
glClearBufferfv(GL_COLOR, 0, clearColor);
glClear(GL_DEPTH_BUFFER_BIT);
glm::mat4 I(1.0f);
glm::vec3 X(1.0f, 0.0f, 0.0f);
glm::vec3 Y(0.0f, 1.0f, 0.0f);
glm::vec3 Z(0.0f, 0.0f, 1.0f);
glm::mat4 view_matrix = glm::lookAt(cameraPosition, cameraPosition + cameraFront, cameraUp);
glm::mat4 projection_matrix = glm::perspective(glm::radians(45.0f), aspect, 1.0f, 100.0f);
glm::mat4 allis_model_matrix = glm::translate(I, glm::vec3(0.0f, 2.0f, 0.0f))
* glm::scale(I, glm::vec3(0.8f, 0.8f, 0.8f)) * glm::rotate(I, glm::radians(0.0f), X);
simpleShader->setModelMatrix(allis_model_matrix);
simpleShader->setViewMatrix(view_matrix);
simpleShader->setProjectionMatrix(projection_matrix);
simpleShader->setCurrent();
lita.render();
}
~MyApp(){
if(simpleShader != NULL){
delete simpleShader;
}
}
};
DECLARE_MAIN(MyApp)
编译运行的命令如下:
g++ -o WanderInScene WanderInScene.cpp -lGL -lglfw -lGLEW -lassimp
./WanderInScene
就可以看到程序运行的效果了,我们可以很方便地从不同角度、不同距离观察 3D 模型,如下图:
版权申明
该随笔由京山游侠在2021年08月09日发布于博客园,引用请注明出处,转载或出版请联系博主。QQ邮箱:1841079@qq.com
使用 GLFW 在 OpenGL 的场景中漫游的更多相关文章
- 在WebGL场景中进行棋盘操作的实验
这篇文章讨论如何在基于Babylon.js的WebGL场景中,建立棋盘状的地块和多个可选择的棋子对象,在点选棋子时显示棋子的移动范围,并且在点击移动范围内的空白地块时向目标地块移动棋子.在这一过程中要 ...
- Opengl场景中加光照包含几个步骤
http://zuoye.baidu.com/question/44e2a82d7ad5c0e1d33ddb9a40e0bf86.html Opengl场景中加光照包含几个步骤,各个步骤实现用的函数 ...
- 在WebGL场景中管理多个卡牌对象的实验
这篇文章讨论如何在基于Babylon.js的WebGL场景中,实现多个简单卡牌类对象的显示.选择.分组.排序,同时建立一套实用的3D场景代码框架.由于作者美工能力有限,所以示例场景视觉效果可能欠佳,本 ...
- SharpGL学习笔记(十二) 光源例子:解决光源场景中的常见问题
笔者学到光源这一节,遇到的问题就比较多了,收集了一些如下所述: (1) 导入的3ds模型,如果没有材质光照效果很奇怪.如下图 (2) 导入的3ds模型,有材质,灯光效果发暗,材质偏色,效果也很奇怪. ...
- 3D UI场景中,把XY平面的尺寸映射为屏幕像素的数学模型推导
概述及目录(版权所有,请勿转载,欢迎读者提出错误) 之前用kanzi的3D UI引擎和cocos-2d的时候都有遇到过这个问题,就如何把3D场景中的XY平面的尺寸映射为与屏幕像素一一对应的,即XY平面 ...
- 第七章 人工智能,7.6 DNN在搜索场景中的应用(作者:仁重)
7.6 DNN在搜索场景中的应用 1. 背景 搜索排序的特征分大量的使用了LR,GBDT,SVM等模型及其变种.我们主要在特征工程,建模的场景,目标采样等方面做了很细致的工作.但这些模型的瓶颈也非常的 ...
- 三维场景中使用BillBoard技术
三维场景中对于渲染效果不是很精致的物体可以使用BillBoard技术实现,使用该技术需要将物体实时朝向摄像机,即计算billboard的旋转矩阵M. 首先根据摄像机位置cameraPos和billBo ...
- LoadRunner测试场景中添加负载生成器
如何在LoadRunner测试场景中添加负载生成器 本文对如何在LoadRunner的测试场景中添加负载生成器,如何使用负载生成器的方法,总结形成操作指导手册,以指导测试人员指导开展相关工作. 1.什 ...
- [python]在场景中理解装饰器
原来我也自己通过查资料,来学习python的装饰器,但是效果不好.因为没有接触过需要用到装饰器的场景,所以 一起的资料都只停留在纸面上,但是今天偶然看到了vimer的这篇文章:http://www.v ...
随机推荐
- python用random模块模拟抽奖逻辑(print修改end参数使打印结果不分行)
import random #引入random模块,运用random函数list_one=["10081","10082","10083" ...
- 如果给IIS添加防火墙入站配置,支持外部或者局域网访问
背景简介 也许你试着在本机IIS运行了一些网站,但是奇怪的是,同网络的终端却无法访问你,这时候极有可能被防火墙拦截了,所以我们要找到正确的姿势来开启魔法了. 找到入站规则设置 不管你是Win7还是Wi ...
- 5、SpringBoot整合之SpringBoot整合MybatisPlus
SpringBoot整合MybatisPlus 目录(可点击直接跳转,但还是建议按照顺序观看,四部分具有一定的关联性): 实现基础的增删改查 实现自动填充功能 实现逻辑删除 实现分页 首先给出四部分完 ...
- 使用VS2017开发APP中使用VUE.js开发遇到打包出来的android文件 在低版本的android(4.3)中无法正常使用
使用VS2017开发VUE的APP应用遇到的问题集合 1, 打包出来的apk文件在Android 6.0版本以上手机可以正常打开,在Android 4.3版本手机上无法打开 原因:一开始猜测是不是V ...
- CRM系统不仅给企业带来更多收益而且提升销售效率
将客户信息记录在CRM系统的数据库中,同时共享沟通数据给售前.售后.SDR等上下游,客户资源还能够按照分配规则分配给适合的销售人员,帮助更快成单.全面使用CRM系统会给企业带来更多业绩. 1.全方位客 ...
- SonarQube插件
关于插件我本身使用不多,如果看不惯英文界面,那么就先装个中文插件吧. 或者上微软的官方网站进行下载 将下载的插件上传到自己的sonarqube的服务的机器上,放置插件目录下,重启sonarqube即可 ...
- Blazor 组件入门指南
翻译自 Waqas Anwar 2021年3月19日的文章 <A Beginner's Guide to Blazor Components> [1] Blazor 应用程序是组件的组合, ...
- Linux:VMware配置NAT网络IP
设置虚拟机网络配置 在目标虚拟机下右键, 选择"设置", 打开"虚拟机设置"对话框, 再选择"网络适配器"使用NAT模式的, 如下图所示: ...
- inux下查看最消耗CPU、内存的进程
1.CPU占用最多的前10个进程: ps auxw|head -1;ps auxw|sort -rn -k3|head -10 2.内存消耗最多的前10个进程 ps auxw|head -1;ps a ...
- XCTF reverse maze
一.查壳 二.拖入ida64,静态调试,找到主函数F5反编译 二.1 思路分析(逆向是真的费时间,每个函数都要分析过去): 1.发现每个if最终都会进入LABEL-15 点进去,看看这个函数是干啥的. ...