🎃

Open GLによるFPSカメラの実装

2024/11/15に公開

今回は、FPS(First Person Shooter)スタイルのカメラをOpenGLで実装しました。ゲームやシミュレーションにおいて、プレイヤーの視点を操作するための基礎的な仕組みを構築する方法について紹介します。

実装の概要

必要なライブラリ
本プロジェクトでは以下のライブラリを使用しました:
GLFW: ウィンドウ管理およびイベント処理。
GLEW: OpenGL拡張機能の読み込み。
GLM: 行列計算およびベクトル操作。
stb_image: テクスチャの読み込み。

カメラの基本構造

カメラの位置や方向を制御するため、以下のベクトルを定義しました:

cameraPos: カメラの現在位置。
cameraDirection: カメラが向いている方向。
cameraRight & cameraUp: カメラの右方向および上方向を計算するための補助ベクトル。
また、視点操作のために、以下の変数を使用します:
gYaw: 水平方向の回転角。
gPitch: 垂直方向の回転角。
gFOV: 視野角(Field of View)。

マウスの動きを取得:

double mouseX, mouseY;
glfwGetCursorPos(gWindow, &mouseX, &mouseY);

移動量を回転角に変換

double yaw = (float)(gWindowWidth / 2.0 - mouseX) * MOUSE_SENSITIVITY;
double pitch = (float)(gWindowHeight / 2.0 - mouseY) * MOUSE_SENSITIVITY;
gYaw += glm::radians(yaw);
gPitch += glm::radians(pitch);

ピッチの範囲を制限: カメラが上下90度以上回転しないように制御します

gPitch = glm::clamp(gPitch, -glm::pi<float>() / 2.0f + 0.1f, glm::pi<float>() / 2.0f - 0.1f);

キーボード操作による移動

FPSカメラでは、WASDキーや矢印キーで移動を制御するのが一般的です。本実装では、以下のようにカメラの移動を行いました:

if (glfwGetKey(gWindow, GLFW_KEY_W) == GLFW_PRESS)
    cameraPos += MOVE_SPEED * (float)deltaTime * cameraDirection;
if (glfwGetKey(gWindow, GLFW_KEY_A) == GLFW_PRESS)
    cameraPos += MOVE_SPEED * (float)deltaTime * cameraRight;

投影行列の設定

視点の変更を画面に反映するために、view行列をglm::lookAtを用いて設定します:

view = glm::lookAt(cameraPos, cameraPos + cameraDirection, cameraUp);

マウス操作でカメラの視点を上下左右に変更。

W, A, S, Dキーで前後左右に移動。
テクスチャ付きのキューブと床を表示。

全体のコード

#include <iostream>
#include <sstream>
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include"stb_image/stb_image.h"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

int gWindowWidth = 1024, gWindowHeight = 768;
GLFWwindow* gWindow = NULL;

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, -2.0f);
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = cameraPos - cameraTarget;
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
glm::vec3 cameraUp = glm::normalize(glm::cross(cameraDirection, cameraRight));

const double ZOOM_SENSITIVITY = -1;
const float MOVE_SPEED = 3.0;
const float MOUSE_SENSITIVITY = 0.03f;

float gRadius = 10.0f, gYaw = 0.0f, gPitch = 0.0f;
float gFOV = 45.0f;

const GLchar* vertexShaderSrc =
"#version 330 core \n"
"layout (location = 0) in vec3 pos;"
"layout (location = 1) in vec2 texCoord;"
"out vec2 TexCoord;"
"uniform mat4 model;"
"uniform mat4 view;"
"uniform mat4 projection;"
"void main()"
"{"
"    gl_Position = projection * view * model * vec4(pos, 1.0f);"
"    TexCoord = texCoord;                                                      "
"}";

const GLchar* fragmentShaderSrc =
"#version 330 core \n"
"in vec2 TexCoord;"
"out vec4 frag_color;"
"uniform sampler2D texSampler1;"
"void main()"
"{"
"    frag_color = texture(texSampler1, TexCoord);"
"}";


void glfw_onKey(GLFWwindow* window, int key, int scancode, int action, int mode)
{
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
	{
		glfwSetWindowShouldClose(window, GL_TRUE);
	}
}
void glfw_OnFramebufferSize(GLFWwindow* window, int width, int height)
{
	gWindowWidth = width;
	gWindowHeight = height;
	glViewport(0, 0, gWindowWidth, gWindowHeight);
}

void glfw_onMouseScroll(GLFWwindow* window, double deltaX, double deltaY)
{
		double fov = gFOV + deltaY * ZOOM_SENSITIVITY;		
		fov = glm::clamp(fov, 1.0, 120.0);
		gFOV = fov;
}

int main()
{
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

	gWindow = glfwCreateWindow(gWindowWidth, gWindowHeight, "Window", NULL, NULL);
	glfwMakeContextCurrent(gWindow);
	glewExperimental = GL_TRUE;
	glewInit();

	glfwSetKeyCallback(gWindow, glfw_onKey);
	glfwSetFramebufferSizeCallback(gWindow, glfw_OnFramebufferSize);
	//glfwSetCursorPosCallback(gWindow, glfw_onMouseMove);
	glfwSetScrollCallback(gWindow, glfw_onMouseScroll);

	// Hides and grabs cursor, unlimited movement
	glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
	glfwSetCursorPos(gWindow, gWindowWidth / 2.0, gWindowHeight / 2.0);

	glClearColor(0.23f, 0.38f, 0.47f, 1.0f);

	glViewport(0, 0, gWindowWidth, gWindowHeight);
	glEnable(GL_DEPTH_TEST);


	// 1. Set up an array of vectex data for a cube with index buffer data
	GLfloat vertices[] = {
		// position		 // tex coords

	   // front face
	   -1.0f,  1.0f,  1.0f, 0.0f, 1.0f,
		1.0f, -1.0f,  1.0f, 1.0f, 0.0f,
		1.0f,  1.0f,  1.0f, 1.0f, 1.0f,
	   -1.0f,  1.0f,  1.0f, 0.0f, 1.0f,
	   -1.0f, -1.0f,  1.0f, 0.0f, 0.0f,
		1.0f, -1.0f,  1.0f, 1.0f, 0.0f,

		// back face
		-1.0f,  1.0f, -1.0f, 0.0f, 1.0f,
		 1.0f, -1.0f, -1.0f, 1.0f, 0.0f,
		 1.0f,  1.0f, -1.0f, 1.0f, 1.0f,
		-1.0f,  1.0f, -1.0f, 0.0f, 1.0f,
		-1.0f, -1.0f, -1.0f, 0.0f, 0.0f,
		 1.0f, -1.0f, -1.0f, 1.0f, 0.0f,

		 // left face
		 -1.0f,  1.0f, -1.0f, 0.0f, 1.0f,
		 -1.0f, -1.0f,  1.0f, 1.0f, 0.0f,
		 -1.0f,  1.0f,  1.0f, 1.0f, 1.0f,
		 -1.0f,  1.0f, -1.0f, 0.0f, 1.0f,
		 -1.0f, -1.0f, -1.0f, 0.0f, 0.0f,
		 -1.0f, -1.0f,  1.0f, 1.0f, 0.0f,

		 // right face
		  1.0f,  1.0f,  1.0f, 0.0f, 1.0f,
		  1.0f, -1.0f, -1.0f, 1.0f, 0.0f,
		  1.0f,  1.0f, -1.0f, 1.0f, 1.0f,
		  1.0f,  1.0f,  1.0f, 0.0f, 1.0f,
		  1.0f, -1.0f,  1.0f, 0.0f, 0.0f,
		  1.0f, -1.0f, -1.0f, 1.0f, 0.0f,

		  // top face
		 -1.0f,  1.0f, -1.0f, 0.0f, 1.0f,
		  1.0f,  1.0f,  1.0f, 1.0f, 0.0f,
		  1.0f,  1.0f, -1.0f, 1.0f, 1.0f,
		 -1.0f,  1.0f, -1.0f, 0.0f, 1.0f,
		 -1.0f,  1.0f,  1.0f, 0.0f, 0.0f,
		  1.0f,  1.0f,  1.0f, 1.0f, 0.0f,

		  // bottom face
		 -1.0f, -1.0f,  1.0f, 0.0f, 1.0f,
		  1.0f, -1.0f, -1.0f, 1.0f, 0.0f,
		  1.0f, -1.0f,  1.0f, 1.0f, 1.0f,
		 -1.0f, -1.0f,  1.0f, 0.0f, 1.0f,
		 -1.0f, -1.0f, -1.0f, 0.0f, 0.0f,
		  1.0f, -1.0f, -1.0f, 1.0f, 0.0f,
	};
	glm::vec3 cubePos = glm::vec3(0.0f, 0.0f, 3.0f);
	glm::vec3 floorPos = glm::vec3(0.0f, -1.0f, 0.0f);

	GLuint vbo, vao;

	glGenBuffers(1, &vbo);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);

	// Position attribute
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5* sizeof(GLfloat), (GLvoid*)(0));
	glEnableVertexAttribArray(0);
	// Texture Coord attribute
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
	glEnableVertexAttribArray(1);

	glBindVertexArray(0);

	//////////////////////////////////////////////////////////////////
	GLuint vs = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vs, 1, &vertexShaderSrc, NULL);
	glCompileShader(vs);

	GLint fs = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fs, 1, &fragmentShaderSrc, NULL);
	glCompileShader(fs);
	
	GLint shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, vs);
	glAttachShader(shaderProgram, fs);
	glLinkProgram(shaderProgram);
	GLchar infoLog[512];
	glGetProgramInfoLog(shaderProgram, sizeof(infoLog), NULL, infoLog);
	std::cout << infoLog << std::endl;
	glDeleteShader(vs);
	glDeleteShader(fs);

	///////////////////////////////////////////
	bool generateMipMaps = true;
	GLuint mTextrue;
	std::string fileName = "textures/g1.png";
	int width, height, components;
	unsigned char* imageData = stbi_load(fileName.c_str(),
		&width, &height, &components, STBI_rgb_alpha);

	glGenTextures(1, &mTextrue);
	glBindTexture(GL_TEXTURE_2D, mTextrue);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
		GL_UNSIGNED_BYTE, imageData);
	glGenerateMipmap(GL_TEXTURE_2D);
	stbi_image_free(imageData);
	glBindTexture(GL_TEXTURE_2D, 0);
	///////////////////////////////////////////
	///////////////////////////////////////////
	bool generateMipMaps2 = true;
	GLuint mTextrue2;
	std::string fileName2 = "textures/g2.png";
	int width2, height2, components2;
	unsigned char* imageData2 = stbi_load(fileName2.c_str(),
		&width2, &height2, &components2, STBI_rgb_alpha);

	glGenTextures(1, &mTextrue2);
	glBindTexture(GL_TEXTURE_2D, mTextrue2);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width2, height2, 0, GL_RGBA,
		GL_UNSIGNED_BYTE, imageData2);
	glGenerateMipmap(GL_TEXTURE_2D);
	stbi_image_free(imageData2);
	glBindTexture(GL_TEXTURE_2D, 0);
	///////////////////////////////////////////	
	double lastTime = glfwGetTime();
	//float cubeAngle = 0.0f;

	while (!glfwWindowShouldClose(gWindow))
	{
		double currentTime = glfwGetTime();
		double deltaTime = currentTime - lastTime;

		// Poll for and process events
		glfwPollEvents();
	
		////////////////////////////////////////
		// FPS Camera 
		////////////////////////////////////////
		double mouseX, mouseY;
		// Get the current mouse cursor position delta
		glfwGetCursorPos(gWindow, &mouseX, &mouseY);
		double yaw = (float)(gWindowWidth / 2.0 - mouseX) * MOUSE_SENSITIVITY;
		double pitch = (float)(gWindowHeight / 2.0 - mouseY) * MOUSE_SENSITIVITY;
		gYaw += glm::radians(yaw);
		gPitch += glm::radians(pitch);
		// Constrain the pitch
		gPitch = glm::clamp(gPitch, -glm::pi<float>() / 2.0f + 0.1f,
			glm::pi<float>() / 2.0f - 0.1f);
		// Constraint the yaw [0, 2*pi]
		if (gYaw > glm::two_pi<float>())
			gYaw -= glm::two_pi<float>();
		else if (gYaw < 0.0)
			gYaw += glm::two_pi<float>();

		glm::vec3 look;
		look.x = cosf(gPitch) * sinf(gYaw);
		look.y = sinf(gPitch);
		look.z = cosf(gPitch) * cosf(gYaw);
		cameraDirection = glm::normalize(look);
		cameraRight = glm::normalize(glm::cross(up, cameraDirection));
		cameraUp = glm::normalize(glm::cross(cameraDirection, cameraRight));

		glfwSetCursorPos(gWindow, gWindowWidth / 2.0, gWindowHeight / 2.0);
		//Forward/backward
		if (glfwGetKey(gWindow, GLFW_KEY_W) == GLFW_PRESS)
			cameraPos += MOVE_SPEED * (float)deltaTime * cameraDirection;
		else if (glfwGetKey(gWindow, GLFW_KEY_S) == GLFW_PRESS)
			cameraPos += MOVE_SPEED * (float)deltaTime * -cameraDirection;
		// Strafe left/right
		if (glfwGetKey(gWindow, GLFW_KEY_A) == GLFW_PRESS)
			cameraPos += MOVE_SPEED * (float)deltaTime * cameraRight;
		else if (glfwGetKey(gWindow, GLFW_KEY_D) == GLFW_PRESS)
			cameraPos += MOVE_SPEED * (float)deltaTime * -cameraRight;
		// Up/down
		if (glfwGetKey(gWindow, GLFW_KEY_Z) == GLFW_PRESS)
			cameraPos += MOVE_SPEED * (float)deltaTime * cameraUp;
		else if (glfwGetKey(gWindow, GLFW_KEY_X) == GLFW_PRESS)
			cameraPos += MOVE_SPEED * (float)deltaTime * -cameraUp;
		////////////////////////////////////////
		////////////////////////////////////////

		glm::mat4 model(1.0), view(1.0), projection(1.0);
		model = glm::translate(model,cubePos);
		view = glm::lookAt(cameraPos, cameraPos + cameraDirection, cameraUp);
		projection = glm::perspective(glm::radians(gFOV), 
			(float)gWindowWidth / (float)gWindowHeight, 0.1f, 100.0f);

		// Clear the screen
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		
		////////////////////////////////////////
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, mTextrue);
		GLint TexLoc = glGetUniformLocation(shaderProgram, "texSampler1");
		glUniform1i(TexLoc, 0);
		////////////////////////////////////////

		glUseProgram(shaderProgram);
	
		GLint ModelLoc = glGetUniformLocation(shaderProgram, "model");
		glUniformMatrix4fv(ModelLoc, 1, GL_FALSE, glm::value_ptr(model));
		GLint ViewlLoc = glGetUniformLocation(shaderProgram, "view");
		glUniformMatrix4fv(ViewlLoc, 1, GL_FALSE, glm::value_ptr(view));
		GLint ProjectionlLoc = glGetUniformLocation(shaderProgram, "projection");
		glUniformMatrix4fv(ProjectionlLoc, 1, GL_FALSE, glm::value_ptr(projection));

		glBindVertexArray(vao);
		glDrawArrays(GL_TRIANGLES, 0, 36);
		////////////////////////////////////

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, mTextrue2);
		GLint TexLoc2 = glGetUniformLocation(shaderProgram, "texSampler1");
		glUniform1i(TexLoc2, 0);
		////////////////////////////////////////
		model = glm::translate(glm::mat4(1.0), floorPos) * glm::scale(glm::mat4(1.0),
			glm::vec3(10.0f, 0.01f, 10.0f));
		GLint ModelLoc2 = glGetUniformLocation(shaderProgram, "model");
		glUniformMatrix4fv(ModelLoc2, 1, GL_FALSE, glm::value_ptr(model));

		glDrawArrays(GL_TRIANGLES, 0, 36);
		////////////////////////////////////////

	    glBindVertexArray(0);
		glfwSwapBuffers(gWindow);
		lastTime = currentTime;
	}
	glDeleteTextures(1, &mTextrue);
	glDeleteProgram(shaderProgram);
	glDeleteVertexArrays(1, &vao);
	glDeleteBuffers(1, &vbo);


	glfwTerminate();

	return 0;
}

Discussion