👌

OpenGLでオービットカメラを作成する方法

2024/11/13に公開

3D空間内で物体を中心にカメラが回転し、常に物体を見つめる「オービットカメラ」の実装方法を解説します。オービットカメラは3Dゲームやグラフィックシミュレーションで、オブジェクトを周回して視点を制御するのに便利なカメラ方式です。

このチュートリアルでは、OpenGLを用いてオービットカメラを作成し、マウスやスクロールホイールでカメラの動きを制御します。

1. 必要なライブラリのインクルード

まず、必要なヘッダーをインクルードします。この例では、OpenGLに加えてGLFW、GLEW、GLM、stb_imageを使います。

#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>

2. グローバル変数と初期設定

カメラの回転角度(Yaw, Pitch)やターゲットとの距離(gRadius)、スクリーンサイズなどの変数を定義します。

int Width = 1024, Height = 768;
const float MOUSE_SENSITIVITY = 0.25f;
float gRadius = 10.0f, gYaw = 0.0f, gPitch = 0.0f;

3. シェーダープログラムの設定

頂点シェーダーとフラグメントシェーダーを用意し、カメラ位置に応じて3Dオブジェクトを描画します。

const GLchar* vertexShaderSrc = R"(
#version 330 core
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 = R"(
#version 330 core
in vec2 TexCoord;
out vec4 frag_color;
uniform sampler2D texSampler1;
void main()
{
    frag_color = texture(texSampler1, TexCoord);
}
)";

4. カメラ制御のコールバック関数

void glfw_onMouseMove(GLFWwindow* window, double posX, double posY)
{
    static glm::vec2 lastMousePos = glm::vec2(0, 0);
    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == 1)
    {
        gYaw -= ((float)posX - lastMousePos.x) * MOUSE_SENSITIVITY;
        gPitch += ((float)posY - lastMousePos.y) * MOUSE_SENSITIVITY;
    }
    lastMousePos.x = (float)posX;
    lastMousePos.y = (float)posY;
}

void glfw_onScroll(GLFWwindow* window, double xOffset, double yOffset)
{
    gRadius -= (float)yOffset * 0.5f;
    if (gRadius < 1.0f) gRadius = 1.0f; // カメラが近すぎないように制限
}

5. カメラ位置の計算

カメラがターゲットを中心に回転するように、球面座標を使ってカメラの位置(camPos)を計算します。

float Yaw = glm::radians(gYaw);
float Pitch = glm::radians(gPitch);
Pitch = glm::clamp(Pitch, -glm::pi<float>() / 2.0f + 0.1f, glm::pi<float>() / 2.0f - 0.1f);
gRadius = glm::clamp(gRadius, 2.0f, 80.0f);

camPos.x = targetPos.x + gRadius * cosf(Pitch) * sinf(Yaw);
camPos.y = targetPos.y + gRadius * sinf(Pitch);
camPos.z = targetPos.z + gRadius * cosf(Pitch) * cosf(Yaw);

この計算により、カメラはターゲットの周りを回転し、gRadius の距離を保ったまま Yaw と Pitch に従ってカメラ位置が変わります。

6. glm::lookAt でビュー行列を設定

glm::lookAt を用いて、カメラ位置 (camPos) からターゲット位置 (targetPos) に向かう視点行列を作成します。これにより、カメラがターゲットを中心に常に視線を合わせた状態で回転します。

view = glm::lookAt(camPos, targetPos, up);

7. オブジェクトの描画

カメラ位置とモデル行列、投影行列を設定し、オブジェクトを描画します。

model = glm::translate(model, cubePos);
projection = glm::perspective(glm::radians(60.0f), float(Width) / float(Height), 0.1f, 100.0f);

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

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

完成した全体のコード

#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 Widht = 1024,Height = 768;


const float MOUSE_SENTIVITY = 0.25f;
float gRadius = 10.0f, gYaw = 0.0f, gPitch = 0.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)
{
	Widht = width;
	Height = height;
	glViewport(0, 0, Widht, Height);
}
void glfw_onMouseMove(GLFWwindow* window, double posX, double posY)
{
	static glm::vec2 lastMousePos = glm::vec2(0, 0);
	if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == 1)
	{
		gYaw -= ((float)posX - lastMousePos.x) * MOUSE_SENTIVITY;
		gPitch += ((float)posY - lastMousePos.y) * MOUSE_SENTIVITY;
	}
	if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == 1)
	{
		float dx = 0.01f * ((float)posX - lastMousePos.x);
		float dy = 0.01f * ((float)posY - lastMousePos.y);
		//gRadius += dx - dy;
	}

	lastMousePos.x = (float)posX;
	lastMousePos.y = (float)posY;
}
void glfw_onScroll(GLFWwindow* window, double xOffset, double yOffset)
{
	// yOffsetはスクロールの方向を示します。正なら上、負なら下です。
	gRadius -= (float)yOffset * 0.5f;  // スクロール方向で半径を増減
	if (gRadius < 1.0f) gRadius = 1.0f; // 半径が小さくなりすぎないように制限
}

int main()
{
	glfwInit();
	GLFWwindow* gWindow = glfwCreateWindow(Widht, Height, "Window", NULL, NULL);
	glfwMakeContextCurrent(gWindow);
	glewExperimental = GL_TRUE;
	glewInit();

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

	glClearColor(0.23f, 0.38f, 0.47f, 1.0f);
	glViewport(0, 0, Widht, Height);
	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, -5.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;

		glfwPollEvents();
		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);
		////////////////////////////////////////

		glm::mat4 model(1.0), view(1.0), projection(1.0);
		glm::vec3 camPos(0.0f, 0.0f, 0.0f);
		glm::vec3 targetPos(0.0f, 0.0f, -1.0f);;
		glm::vec3 up(0.0f, 1.0f, 0.0f);
		targetPos = cubePos;

		float Yaw = glm::radians(gYaw);
		float Pitch = glm::radians(gPitch);
        Pitch = glm::clamp(Pitch, -glm::pi<float>() / 2.0f + 0.1f,
			glm::pi<float>() / 2.0f - 0.1f);
		gRadius = glm::clamp(gRadius, 2.0f, 80.0f);

		camPos.x = targetPos.x + gRadius * cosf(Pitch) * sinf(Yaw);
		camPos.y = targetPos.y + gRadius * sinf(Pitch);
		camPos.z = targetPos.z + gRadius * cosf(Pitch) * cosf(Yaw);
		view = glm::lookAt(camPos, targetPos, up);

        float angle = (float)deltaTime * 30.0f;
		cubePos.x = 8.0f * sinf(glm::radians(angle));

		model = glm::translate(model, cubePos);
		projection = glm::perspective(glm::radians(60.0f),
			float(Widht) / float(Height), 0.1f, 100.0f);
	

		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);
	}
	glDeleteTextures(1, &mTextrue);
	glDeleteProgram(shaderProgram);
	glDeleteVertexArrays(1, &vao);
	glDeleteBuffers(1, &vbo);


	glfwTerminate();

	return 0;
}

Discussion