OpenGL/공부

[OpenGL] Skybox

ciel45 2024. 7. 18. 20:12

우리가 게임에서 보는 광활한 skybox는, 사실 실제로 맵 전체를 둘러싸고 있지 않다.

 

스카이림의 한 스크린샷인데, 만약 실제로 오로라와 달을 저 멀리 두고 렌더링하는 것이라면, far clipping plane을 엄청 멀리 둬야 할 것이다.

 

실제로는, 저 오로라와 달을 포함한 하늘은 정확히 딱 카메라만 감싸고 있다.

대충 그림으로 그려보자면 이렇게 표현할 수 있다.

 

그런데 저 그림대로라면, 다른 모든 오브젝트들은 skybox에 가려져 안 보여야 할 것이다.

지금까지 화면에 오브젝트들을 그릴 때 depth testing을 해왔기 때문이다.

 

이를 해결하기 위해 처음 skybox를 그릴 때만 depth testing을 비활성화할 것이다.

 

그러면 실제로는 skybox가 가장 가까이 있는 것임에도 불구하고, 다른 오브젝트들을 skybox 위에 그릴 수 있게 된다.

 

 

 

Skybox도 6개의 이미지로 이루어져있으므로 텍스쳐로써 불러올 것인데, cubemap 텍스쳐의 형태로 불러올 것이다.

https://learnopengl.com/Advanced-OpenGL/Cubemaps

 

LearnOpenGL - Cubemaps

Cubemaps Advanced-OpenGL/Cubemaps We've been using 2D textures for a while now, but there are more texture types we haven't explored yet and in this chapter we'll discuss a texture type that is a combination of multiple textures mapped into one: a cube map

learnopengl.com

Cubemap 텍스쳐의 사용을 잘 나타낸 그림이다. 

2D 텍스쳐에서는 UV좌표를 일반적인 XY 좌표계와 같은 방식으로 사용했지만, 6면으로 이루어진 큐브맵에서는 그러기가 힘들다.

 

따라서 큐브맵의 각 텍셀을 나타내는 좌표로써, 상자의 중심점을 시작점으로 하고 원하는 텍셀을 도착점으로 하는 3차원 벡터를 사용하기로 한다.

 

위 그림의 노란색 벡터가 그 예시이다.

 

여기서 좌표의 범위는 (-1, -1, -1) ~ (1, 1, 1)이다.

참고로, 방향만 잘 정해주면 크기는 상관없다.

 

 

큐브맵 텍스쳐도 버퍼를 만들고, 바인딩하는 과정 등은 2D 텍스쳐를 사용할 때와 거의 똑같다.

다만 디스크에서 메모리로 이미지를 로드할 때 6면을 로드해야 하기 때문에, 이 때만 for문을 돌게 된다.

int width, height, nrChannels;
unsigned char *data;  
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
    data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
    glTexImage2D(
        GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 
        0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
    );
}

glTexImage2D의 첫번째 인자로 GL_TEXTURE_CUBE_MAP_POSITIVE_X + i 가 들어가있는 것을 볼 수 있는데, 왜냐하면 열거형의 순서가 다음과 같이 이루어져 있기 때문이다.

 

 i가 0부터 5까지 증가함에 따라 +X, -X, +Y, -Y, +Z, -Z에 해당하는 이미지를 차례대로 로드할 수 있는 것이다.

 

 

다음은 skybox 이미지를 디스크에서 로드하고, GPU로 보내 렌더링까지 하는 C++ 클래스이다.

 

Skybox.h

#pragma once

#include <vector>
#include <string>
#include <iostream>

#include <GL/glew.h>

#include <glm/glm.hpp>
#include <glm\gtc\matrix_transform.hpp>
#include <glm\gtc\type_ptr.hpp>

#include "CommonValues.h"

class Shader;
class Mesh;

class Skybox
{
public:
	Skybox(std::vector<std::string> faceLocations);

	void DrawSkybox(glm::mat4 viewMat, glm::mat4 projMat);

private:
	Mesh* skyMesh;
	Shader* skyShader;

	GLuint textureID;
	GLuint loc_PVM;
	GLuint loc_sampler;
};
#include "Skybox.h"

#include "Shader.h"
#include "Mesh.h"

#include <vector>

Skybox::Skybox(std::vector<std::string> faceLocations)
{
	// shader setup
	skyShader = new Shader();
	skyShader->CreateFromFiles("Shaders/skyVertex.glsl", "Shaders/skyFragment.glsl");

	loc_PVM = skyShader->GetPVMLoc();
	loc_sampler = skyShader->GetSamplerLoc();

	// texture setup
	glGenTextures(1, &textureID);
	glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

	int width, height, bitDepth;
	// skybox의 6면에 각각 이미지를 쏴준다.
	for (size_t i = 0; i < 6; i++)
	{
		unsigned char* texData = stbi_load(faceLocations[i].c_str(), &width, &height, &bitDepth, 0);
		if (!texData)
		{
			printf("Failed to find: %s\n", faceLocations[i].c_str());
			return;
		}

		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, texData);
		stbi_image_free(texData);
	}

	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	
	// mesh setup
	std::vector<unsigned int> skyboxIndices = {
		// front
		0, 1, 2,
		2, 1, 3,
		// right
		2, 3, 5,
		5, 3, 7,
		// back
		5, 7, 4,
		4, 7, 6,
		// left
		4, 6, 0,
		0, 6, 1,
		// top
		4, 0, 5,
		5, 0, 2,
		// bottom
		1, 6, 3,
		3, 6, 7
	};

	std::vector<float> skyboxVertices = {
		-1.0f, 1.0f, -1.0f,		0.0f, 0.0f,		0.0f, 0.0f, 0.0f,
		-1.0f, -1.0f, -1.0f,	0.0f, 0.0f,		0.0f, 0.0f, 0.0f,
		1.0f, 1.0f, -1.0f,		0.0f, 0.0f,		0.0f, 0.0f, 0.0f,
		1.0f, -1.0f, -1.0f,		0.0f, 0.0f,		0.0f, 0.0f, 0.0f,

		-1.0f, 1.0f, 1.0f,		0.0f, 0.0f,		0.0f, 0.0f, 0.0f,
		1.0f, 1.0f, 1.0f,		0.0f, 0.0f,		0.0f, 0.0f, 0.0f,
		-1.0f, -1.0f, 1.0f,		0.0f, 0.0f,		0.0f, 0.0f, 0.0f,
		1.0f, -1.0f, 1.0f,		0.0f, 0.0f,		0.0f, 0.0f, 0.0f
	};

	skyMesh = new Mesh();
	skyMesh->CreateMesh(skyboxVertices, skyboxIndices);
}

void Skybox::DrawSkybox(glm::mat4 viewMat, glm::mat4 projMat)
{
	// viewMat에서 translate 부분을 도려낸다.
	viewMat = glm::mat4(glm::mat3(viewMat));

	// skybox는 depth mask 비활성화
	glDepthMask(GL_FALSE);

	skyShader->UseShader();

	glm::mat4 PVM = projMat * viewMat; // modelMat은 사용하지 않음
	glUniformMatrix4fv(loc_PVM, 1, GL_FALSE, glm::value_ptr(PVM));

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

	glUniform1i(loc_sampler, 0);

	skyShader->Validate();
	skyMesh->RenderMesh();

	glDepthMask(GL_TRUE);
}

전체 소스코드: https://github.com/sys010611/YsEngine

 

skyboxVertices는 skybox 정육면체의 각 꼭짓점을 나타내고, skyboxIndices는 각 면을 그리기 위한 vertex index를 나타낸다.

 

DrawSkybox() 함수를 살펴보자면,

첫 줄을 보면 view matrix를 mat3으로 변환했다가 다시 mat4로 되돌리는 것을 볼 수 있다.

이는 카메라의 translation을 제거하기 위해서이다.

 

translation을 제거하는 이유는, skybox 메시는 항상 원점에 놓은 채로 렌더링할 것이기 때문에, skybox를 그릴 때 만큼은 카메라가 원점에 고정되어 회전만 하는 것으로 치고 싶기 때문이다.

 

그래서 카메라의 transformation을 나타내는 view matrix에서 translation만 잘라내야 하는데, 4x4 transformation 행렬에서 회전 정보는 좌측 상단 3x3 행렬에 저장되어있다.

 

왜냐하면 1, 2, 3열은 각각 x축, y축, z축을 나타내고, 4열이 원점의 위치를 나타내기 때문이다.

 

그래서 3x3 행렬로 변환해서 translation 정보를 없애준 후, 다시 4x4로 바꿔준 것이다.

 

이후 DepthMask를 끄고, skybox 전용 쉐이더를 적용해준 뒤, 

 

쉐이더의 PVM(projection * view * model) 변수에 projection * view 까지만 계산해서 쏴주었다.

즉 model matrix를 적용하지 않겠다는 의미인데, skybox는 원점에서 움직일 필요가 없기 때문.

 

그리고 0번 텍스쳐 유닛에 큐브맵 텍스쳐를 달아주고, 쉐이더 내 sampler를 0번 텍스쳐 유닛과 연결시켜준 뒤,

skybox를 그려주고 (skyMesh->RenderMesh();), 다시 DepthMask를 켜주었다.

 

 

 

skybox 전용 쉐이더 코드는 다음과 같다.

 

skyVertex.glsl

#version 330

layout (location = 0) in vec3 pos;

out vec3 TexCoord;

uniform mat4 PVM;

void main()
{
	TexCoord = pos;
	gl_Position = PVM * vec4(pos, 1.f);
}

 

skyFragment.glsl

#version 330

in vec3 TexCoord;

out vec4 FragColor;

uniform samplerCube sampler;

void main()
{	
	FragColor = texture(sampler, TexCoord);
}

 

 

vertex shader의 main 함수를 보면, 텍스쳐 좌표로 position을 그대로 넣어준 것을 볼 수 있다.

 

왜냐하면 큐브맵 텍스쳐의 좌표계 사용법을 상기해보면, position을 그대로 텍스쳐좌표로 사용할 수 있기 때문이다.

 

상자의 중심이 원점이라고 가정하면, 상자의 면 위의 어느 한 점을 나타내는 좌표는 곧 원점에서 그 좌표를 향하는 벡터와 같다.

 

따라서 vertex shader에서 skybox 메쉬의 버텍스의 각 position을 텍스쳐 좌표로써 넘겨주면, Bilinear Interpolation에 의하여 각 fragment의 텍스쳐 좌표가 정해지는 것이다.

 

Bilinear Interpolation에 대한 자세한 내용은 https://ciel45.tistory.com/90

 

[OpenGL] Polygon Rasterization (Bilinear Interpolation)

프로그래머가 GPU로 넘겨주는 것은 버텍스에 관한 정보이지만, GPU가 만들어내는 최종 결과물은 각 픽셀에 대한 정보이다. 따라서 GPU의 임무는 서로 떨어져있는 버텍스들을 이어 도형(폴리곤)들

ciel45.tistory.com

 

이후 main.cpp에 다음과 같이 skybox를 추가해주었다.

#define STB_IMAGE_IMPLEMENTATION

// ...

int main()
{
    // ...

	// Skybox
	std::vector<std::string> skyboxFaces;
	skyboxFaces.push_back("Textures/Skybox/px.png");
	skyboxFaces.push_back("Textures/Skybox/nx.png");
	skyboxFaces.push_back("Textures/Skybox/py.png");
	skyboxFaces.push_back("Textures/Skybox/ny.png");
	skyboxFaces.push_back("Textures/Skybox/pz.png");
	skyboxFaces.push_back("Textures/Skybox/nz.png");
	skybox = new Skybox(skyboxFaces);

	// ...

    ///////////////////////////////////////////////////////////////////////////
    /// main loop
    //////////////////////////////////////////////////////////////////////////
	while (!mainWindow->GetShouldClose())
	{
		// ...
        
		glm::mat4 viewMat = camera->GetViewMatrix();
		glm::mat4 projMat = camera->GetProjectionMatrix(scenePanel->GetWidth(), scenePanel->GetHeight());

		glm::mat4 identityMat(1.f);
		skybox->DrawSkybox(viewMat, projMat);
        
        // ----------------------------

		// (오브젝트들 렌더링)

		// --------------------------------------------------------------------------------
		sceneBuffer.Unbind();
		// --------------------------------------------------------------------------------

		// ...
	}

    return 0;
}

main.cpp 전문 : https://github.com/sys010611/YsEngine/blob/main/YsEngine/main.cpp

 

YsEngine/YsEngine/main.cpp at main · sys010611/YsEngine

Contribute to sys010611/YsEngine development by creating an account on GitHub.

github.com

 

 

참고로, Skybox 이미지는 다음 사이트들을 통해 쉽게 구할 수 있다.

https://hdri-haven.com/category/skies

 

100% Free HDRIs • Skies • HDRI Haven

 

hdri-haven.com

https://matheowis.github.io/HDRI-to-CubeMap/

 

HDRI to CubeMap

 

matheowis.github.io

 

첫번째 사이트에서 hdr 파일을 받고, 두번째 사이트에서 그걸 6개의 분할된 png로 변환하여 받으면 된다.

 

 

본인의 경우 이렇게 준비해두었다.

 

 

실행 결과:

'OpenGL > 공부' 카테고리의 다른 글

[OpenGL] Tessellation Shader  (0) 2024.08.09
[OpenGL] FrameBuffer 생성, 사용  (0) 2024.07.16
[OpenGL] ImGuizmo 설치, 사용법  (0) 2024.07.16
[OpenGL] Model Loading (Assimp)  (1) 2024.07.13
[OpenGL] Phong Reflection Model - 구현  (0) 2024.06.19