前言
前面已經建立了 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郵箱:[email protected]