본문 바로가기

컴퓨터그래픽스

[OpenGL 공부] Coordinates Systems

학습을 위해 참고한 사이트

https://learnopengl.com/Getting-started/Coordinate-Systems

 

LearnOpenGL - Coordinate Systems

Coordinate Systems Getting-started/Coordinate-Systems In the last chapter we learned how we can use matrices to our advantage by transforming all vertices with transformation matrices. OpenGL expects all the vertices, that we want to become visible, to be

learnopengl.com

화면에 보이기까지의 좌표 공간 변환 과정

로컬 공간 : 개체가 생성되었을 때 가장 처음 속하는 좌표 공간으로, 개체마다 각각의 로컬 공간에 속해져 있음.

월드 공간 : 전역 좌표계의 원점을 기준으로 하여, 각 개체들을 전역 공간에 배치시킴

시점 공간 : 카메라 또는 관찰자가 보는 방향에 따른 좌표계로, 전역 좌표를 시점 좌표로 변환

클립 공간 : 시점 좌표를 클립 좌표에 투영, -1.0 ~ 1.0 범위 내에서 화면에 표시될 정점을 결정

스크린 공간 : 클립 좌표를 화면 좌표로 변환(뷰포트 변환)

이후 화면 좌표까지 변환이 완료된 정점들은 래스터라이저로 전송되어 프래그먼트로 변환

 

Local Space

개체가 시작되는 위치로, 각 개체에 국한된 좌표 공간. 모든 모델의 초기 위치는 (0, 0, 0)에 속한다.

World Space

전역 공간은 실제 월드(게임)에서 흩어져 있는 모든 정점의 좌표들이 모인 공간으로, 각각의 개체들은 자신만의 좌표 공간인 로컬 공간에서 벗어나 전역 공간에 배치된다. 변환에 사용되는 Model matrix는 전역 공간에 모델을 배치하기 위해 사용되는 변환 행렬을 의미한다.

View Space

카메라의 관점에서 전역 공간을 바라본 것으로, View matrix는 카메라의 전면을 향하도록 전역 공간을 변환하는 행렬을 의미한다.

Clip Space

정점 셰이더의 실행이 끝나면 OpenGL은 Projection Matrix를 사용하여 특정 범위를 벗어나는 좌표를 잘라내는 작업인 클리핑(Clipping)을 수행하여 절단된 좌표계가 구성되는데, 이를 클립 공간이라고 한다. 이후 클립 공간에서는 실제 화면 좌표로 나타내기 이전에 투상 과정과 정규화 좌표계로 변환 과정을 거치게 된다. 투영 행렬은 투영과 더불어 클리핑 +  정규화된 장치 좌표(NDC) 변환이 통합되어 있다. NDC로 변환할 때 실제 3D 좌표에 사용하기 위해서 각 정점을 w로 나누어 주는 작업인 원근 분할을 수행한다.

정점 데이터 변환 과정

동차 좌표계

3D에서 행렬을 계산하기 위해 3x3 행렬 대신 차원 하나를 추가한 4x4 행렬을 사용하는데, 이처럼 x, y, z 요소에 더불어 w 요소를 추가한 동차 좌표계는 변환 행렬을 나타낼 때 4x4 행렬로 나타내어 이를 행렬 곱으로 처리할 수 있었다.

동차 좌표계에서 w가 0인 경우 점(point), w가 1인 경우 벡터(vector)로 나타내는데, 4차원의 벡터를 실제 3D 공간으로 축소시키기 위해서는 w의 크기가 반드시 1이어야 한다는 점을 나타낸다. 따라서 어떤 점 v가 동차 좌표계에서 (x, y, z, w)의 값을 가질 때, 실제 3D 공간에서의 좌표는 (x/w, y/w, z/w, 1)이 될 것이다. 따라서 동차 좌표계에서 크기보다는 시점으로부터 방향 자체가 중요하다는 것을 알 수 있다. 동차 좌표계에 존재하는 한 직선상에 존재하는 모든 좌표들은 결국에 3D 공간에서 사용하기 위해서 w를 1로 스케일 하기 때문이다. 

 

원근 투상(Perspective Projection)

원근 투상(좌측)과 평행 투상(우측)에 따른 차이

 

일반적으로 현실의 우리 눈과 카메라는 멀리 떨어진 물체일수록 작게 보이게 되고 이는 입체감을 부여한다. 위 사진을 보면 평행 투상일 때와 다르게 원근 투상일 때 시점으로부터 멀수록 모습이 축소되어 나타난다.

원근 투상 방법

 

 

원근 투상이란 시점이 물체로부터 유한한 거리(dmin, dmax은 각각 가시 부피의 전방 절단면과 후방 절단면)에 있을 때, 시점에서 출발하는 투상선이 방사형으로 퍼져가는 방법을 의미한다. 위 그림에서 정육면체인 물체에 대해서 원근 투상을 시킨다고 했을 때, 동일한 선분의 길이라도 카메라로부터 거리에 따라 멀수록 실제 투영되는 image에는 축소된 형태로 나타나게 된다.

 

원근 변환과 원근 분할

원근 변환(Perspective Transform)은 말그대로 원근감을 부여하기 위해 동차 좌표에 가하는 변환을 의미한다.

원근 분할(Perspective Division)은 평행 투상에도 적용되지만 원근 투상을 사용할 때 의미 있게 사용되며, 동차 좌표에 의해 정의된 좌표를 실제 3D 좌표로 가져오기 위해 사용된다. 

 

 

OpenGL에서 원근 투상을 위해서는 다음과 같은 함수를 사용한다.

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

 

결국에는 perspective 함수를 호출하면 원근 투상과 정규화 변환이 동시에 일어나게 되는데, 동차 좌표계를 사용해 이를 하나의 행렬로써 표현할 수 있게 된다. 아래의 행렬은 시점으로부터 시선이 가시 부피의 한가운데를 통과할 때 대칭인 절두체의 경우 적용되는 투영 행렬이다.

투영 행렬

 

해당 투영 행렬이 어떻게 구성되는지는 아래의 사이트에서 자세하게 다루고 있다.

https://www.songho.ca/opengl/gl_projectionmatrix.html

 

OpenGL Projection Matrix

OpenGL Projection Matrix Related Topics: OpenGL Transformation, OpenGL Matrix Updates: The MathML version is available here. Overview A computer monitor is a 2D surface. A 3D scene rendered by OpenGL must be projected onto the computer screen as a 2D image

www.songho.ca

 

정규화 변환 전후에 따른 가시 부피 변화

 

정규화 가시 부피를 사용함으로써 얻는 이점은 다음과 같다.

1. 평행 투상, 원근 투상 모두 동일하게 정규화 가시 부피를 적용하여 파이프라인 구조 통일

2. 가시 부피 밖의 물체를 절단할 때, 정규화 가시 부피를 기준으로 절단하는 것이 훨씬 단순

3. 정규화 가시 부피는 원점을 기준으로 가로, 세로 길이를 1로 정규화함으로써 화면 좌표계로 변환에 용이

 

Z - fighting

Z-fighting은 3D 그래픽스에서 나타나는 현상으로, 두 개 이상의 물체가 서로 겹쳐 있을 때, 깊이 버퍼(또는 Z 버퍼)의 해상도 부족으로 인해 물체들이 깊이 값을 충돌하여 표면이 제대로 표시되지 않는 문제를 말한다.

 

Zn : NDC에서의 z좌표, Ze : 시점 기준 z좌표

 

시점 좌표에 투영 행렬을 곱하고, w값으로 나누면 NDC에서의 z좌표를 구할 수 있는데, zn과 ze의 관계로부터 다음과 같은 비선형 관계가 나타난다. 

[-f -n]의 폭이 클수록 정밀도 오류 가능성이 증가한다.

 

시점 좌표에서 가시 부피의 전방 절단면에 해당하는 z값인 -n과 후방 절단면의 -f는 각각 [-1, 1]로 변환되는데, 둘 사이의 거리가 멀수록 NDC에서 후방 절단면에 근접한 물체들의 깊이 값이 충돌할 가능성이 높아지게 된다.

이러한 비선형성에 따른 오류를 피하기 위해, 원래의 z값을 저장해 두었다가 투상의 최종 단계에서 판단하는 Z 버퍼를 사용한다.

 

Screen Space

정규화된 가시 부피의 z = 0에 해당하는 평면이 뷰 포트(윈도우)로 투상된다. 투상을 위해 z 성분을 없애기 전에,  깊이 정보를 활용하여 물체 간의 깊이를 판단한다.


 

실제 코드 작성

// model matrix
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));

// view matrix
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));

// projection matrix
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);

좌표 공간의 변환을 위해 필요한 model, view, projection matrix를 정의해준다.

모델 행렬은 x축을 시계 방향으로 55도만큼 회전시키는 변환을 가한다.

또한 뷰 행렬은 카메라를 이동하고자 하는 방향과 반대 방향으로 이동해야 한다. (카메라는 원점에 고정되고 물체에 변환을 가함)

투영 행렬에 사용한 perspective(평행 투상은 ortho 사용) 함수에서 첫 번째 인수는 화각(fov)으로 y방향에 대한 시야의 각도를 조정한다. 화각이 넓어질수록 가시 부피의 단면이 커지므로 상대적으로 물체가 작게 그려진다. 두 번째 인수는 시야의 가로와 세로의 비율로 일반적으로 뷰포트의 너비, 높이와 같은 비로 설정한다. 세 번째, 네 번째는 가시 부피의 전방(near), 후방(far) 면 위치로, 시야로부터 near, far만큼 떨어진 거리에 따라 가시 부피가 배치된다.

 

정점 데이터를 처리하는 정점 셰이더에 각 변환 행렬을 적용하기 위한 uniform 변수를 생성하고 들어온 정점 데이터에 변환 행렬을 곱한다.

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 texCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() 
{	
	gl_Position = projection * view * model * vec4(aPos, 1.0);
	texCoord = vec2(aTexCoord.x, aTexCoord.y);
}

 

unsigned int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

unsigned int viewLoc = glGetUniformLocation(ourShader.ID, "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));

unsigned int projectionLoc = glGetUniformLocation(ourShader.ID, "projection");
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

 이후 셰이더의 uniform 변수에 사용할 변환 행렬의 정보를 알려준다.

 

변환 행렬 적용 전
모델, 뷰, 투영 행렬 적용

Z - Buffer

다음은 z값을 활용한 정점 데이터를 사용하여 6개의 면으로 이루어진 큐브를 그린 결과이다.

일부 면이 다른 면 위에 그려지고 있다.

OpenGL은 (동일한 그리기 호출 내에서) 렌더링된 삼각형의 순서를 보장하지 않으므로 일부 삼각형은 한 삼각형이 다른 삼각형 앞에 분명히 있어야 함에도 불구하고 서로 겹쳐서 그려집니다.

Z 버퍼를 사용하지 않는 경우, 정점 데이터에 z값이 존재함에도 불구하고 깊이 정보가 반영되지 않는 결과를 나타내고 있다. 따라서 OpenGL에게 픽셀 위의 그릴 시기와 그리지 않을 시기를 결정시켜 주기 위해서 Z 버퍼를 사용하여 깊이 정보를 저장해주어야 한다.

 

다음의 코드를 통해 깊이 테스트를 활성화시킬 수 있다.

glEnable(GL_DEPTH_TEST);

 

또한 렌더링을 반복하면서, 이전 깊이 값이 버퍼에 남아 다음 렌더링에 영향을 끼치는 것을 방지하기 위해서 glClear에 다음과 같은 옵션을 추가한다. GL_DEPTH_BUFFER_BIT

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 

깊이 테스트가 적용된 실행 결과는 다음과 같다.

 

전체 코드

더보기
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <string>
#include <iostream>
#include <fstream>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "shader.h"

using namespace std;

// callback 함수
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
	{
		std::cout << "Pressed ESC" << std::endl;
		glfwSetWindowShouldClose(window, true);
	}
}

float vertices[] = {
	-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
	 0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
	 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
	 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
	-0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
	-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

	-0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
	 0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
	 0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
	 0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
	-0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
	-0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

	-0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
	-0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
	-0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
	-0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
	-0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
	-0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

	 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
	 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
	 0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
	 0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
	 0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
	 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

	-0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
	 0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
	 0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
	 0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
	-0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
	-0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

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

int main() {
	// GLFW 초기화
	glfwInit();
	// GLFW 설정
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	// 윈도우 객체 생성, 모든 window 데이터를 보유
	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// GLAD가 OpenGL 함수 포인터를 관리
	// OS 마다 다른 OpenGL 함수 포인터의 주소를 로드하기 위해 GLAD 함수를 거침
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	unsigned int VBO, VAO;

	glGenBuffers(1, &VBO);
	glGenVertexArrays(1, &VAO);

	glBindVertexArray(VAO);

	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	// position attribute
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// texture coords attribute
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);

	//texture load & settings
	unsigned int texture1, texture2;

	glGenTextures(1, &texture1);
	glBindTexture(GL_TEXTURE_2D, texture1);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	int width, height, nrChannels;
	unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
	if (data != 0)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);

	glGenTextures(1, &texture2);
	glBindTexture(GL_TEXTURE_2D, texture2);

	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);

	stbi_set_flip_vertically_on_load(true);
	data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
	if (data != 0)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);

	Shader ourShader("shaders/vertShader.glsl", "shaders/fragShader.glsl");
	ourShader.use();
	glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
	glUniform1i(glGetUniformLocation(ourShader.ID, "texture2"), 1);

	while (!glfwWindowShouldClose(window)) {
		// 입력 감지
		processInput(window);

		// 렌더링 명령 ...
		glEnable(GL_DEPTH_TEST);
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture1);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, texture2);

		// model matrix
		float timeValue = glfwGetTime();
		glm::mat4 model = glm::mat4(1.0f);
		model = glm::rotate(model, glm::radians(-55.0f + (timeValue * 10.0f)), glm::vec3(1.0f, 1.0f, 0.0f));

		// view matrix
		glm::mat4 view = glm::mat4(1.0f);
		view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));

		// projection matrix
		glm::mat4 projection;
		projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);

		unsigned int modelLoc = glGetUniformLocation(ourShader.ID, "model");
		glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
		
		unsigned int viewLoc = glGetUniformLocation(ourShader.ID, "view");
		glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));

		unsigned int projectionLoc = glGetUniformLocation(ourShader.ID, "projection");
		glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

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

		// 렌더링 명령 ...

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwTerminate();
	return 0;
}

 

'컴퓨터그래픽스' 카테고리의 다른 글

[OpenGL 공부] camera (2)  (1) 2024.02.07
[OpenGL 공부] Camera (1)  (0) 2024.02.02
[OpenGL 공부] Transformations  (0) 2024.01.20
[OpenGL 공부] Textures (exercises)  (0) 2024.01.16
[OpenGL 공부] Textures (2)  (1) 2024.01.15