그래픽스 렌더링 파이프라인은 3D 그래픽스의 주요 처리 과정을 설명하는 중요한 개념이다. 이 파이프라인은 다양한 단계로 구성되어 있으며, 각 단계는 입력 데이터를 받아들이고 이를 처리하여 다음 단계로 전달한다. 렌더링 파이프라인의 각 단계는 일반적으로 다음과 같이 구분된다:
정점 처리(Vertex Processing)
정점 처리 단계에서는 모델의 정점 데이터를 변환하고 조명 계산을 수행한다. 주로 다음과 같은 연산이 포함된다: - 정점 변환: 모델 좌표를 세계 좌표, 뷰 좌표 및 클립 좌표로 변환한다. 이를 위해 4x4 행렬이 사용된다. 예를 들어, 모델 좌표에서 세계 좌표로 변환하는 식은 다음과 같다:
여기서 $$ \mathbf{v}\text{world} $$ 는 전역 좌표계의 정점, $$ \mathbf{M}\text{model-to-world} $$ 는 모델 좌표계를 전역 좌표계로 변환하는 변환 행렬, $$ \mathbf{v}_\text{model} $$ 는 모델 좌표계의 정점이다.
- 조명 계산: 각 정점에 대한 조명 효과를 계산한다. 일반적으로 Lambertian reflection 모델이나 Blinn-Phong 모델 등의 조명 모델이 사용된다.
래스터화(Rasterization)
래스터화 단계에서는 3D 데이터를 2D 픽셀로 변환한다. 주로 다음과 같은 연산이 포함된다: - 프리미티브 어셈블리: 정점들을 삼각형이나 선과 같은 기본 프리미티브로 결합한다. - 스캔 변환: 프리미티브를 화면 좌표계로 변환하고 이를 픽셀단위로 잘라서 2D 평면에 투영한다.
픽셀 처리(Pixel Processing/Shading)
픽셀 처리 단계에서는 픽셀 단위로 색상 값을 계산하고 최종 이미지를 생성한다. 주로 다음과 같은 연산이 포함된다: - 프래그먼트 셰이딩: 각 픽셀에 대하여 색상 및 조명 계산을 수행한다. - 텍스처 매핑: 텍스처라는 이미지 데이터를 피사체에 적용하여 현실감을 더한다. 텍스처 좌표를 사용하여 텍스처 이미지를 매핑한다. - 프래그먼트 테스트: 깊이 검사, 스텐실 검사, 블렌딩 등과 같은 다양한 검사를 통해 최종 픽셀 색상을 결정한다.
출력 병합(Output Merging)
출력 병합 단계에서는 최종 픽셀 데이터를 프레임 버퍼에 결합하여 최종 이미지를 형성한다. 주로 다음과 같은 연산이 포함된다: - 블렌딩: 새로운 프래그먼트와 기존 프래그먼트의 색상을 결합하여 투명도 효과를 제작한다. - 깊이 검사: 깊이 버퍼를 사용하여 겹치는 프래그먼트를 처리하여 올바른 깊이 값을 가진 프래그먼트만을 나타낸다.
이 과정들은 렌더링 파이프라인을 구성하는 주요 요소들로 구성된다. 이론적으로 어느 렌더링 과정이든 비슷한 파이프라인 구조를 따른다.
렌더링 파이프라인의 각 단계를 구체적으로 설명했으므로, 실제 그래픽스 응용 프로그램에서 이를 구현하는 방법을 살펴보자.
정점 셰이더(Vertex Shader)
정점 셰이더는 각 정점마다 실행되는 작은 프로그램이다. 정점 변환 및 조명 계산을 gpu에서 수행하여 높은 성능을 얻을 수 있다. 다음은 간단한 GLSL(오픈GL 셰이딩 언어)로 작성된 정점 셰이더 코드 예시이다:
#version 330 core
layout(location = 0) in vec3 position; // 정점의 위치 인풋
layout(location = 1) in vec3 normal; // 정점의 노멀벡터 인풋
uniform mat4 model; // 모델 변환 행렬
uniform mat4 view; // 카메라 변환 행렬
uniform mat4 projection; // 투영 변환 행렬
out vec3 fragNormal; // 프래그먼트 셰이더로 전달될 노멀벡터
void main() {
// 정점 위치 변환
gl_Position = projection * view * model * vec4(position, 1.0);
// 노멀벡터 변환
fragNormal = mat3(transpose(inverse(model))) * normal;
}
래스터화 라이브러리 사용 예시
오픈GL 같은 그래픽스 라이브러리를 사용하면 복잡한 래스터화 과정을 간편하게 처리할 수 있다.
glViewport(0, 0, windowWidth, windowHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 정점 배열 및 속성 지정
glBindVertexArray(VAO);
// 셰이더 프로그램 활성화
glUseProgram(shaderProgram);
// 뷰 행렬과 투영 행렬 설정
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)windowWidth / (float)windowHeight, 0.1f, 100.0f);
// 셰이더로 행렬 전달
GLuint modelLoc = glGetUniformLocation(shaderProgram, "model");
GLuint viewLoc = glGetUniformLocation(shaderProgram, "view");
GLuint projectionLoc = glGetUniformLocation(shaderProgram, "projection");
// Renderer
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
// 래스터화 호출
glDrawArrays(GL_TRIANGLES, 0, 36);
프래그먼트 셰이더(Fragment Shader)
프래그먼트 셰이더는 각 픽셀에 대한 색상 값을 계산하는 역할을 한다. 다음은 간단한 GLSL로 작성된 프래그먼트 셰이더 코드 예시이다:
#version 330 core
in vec3 fragNormal; // 정점 셰이더로부터 전달된 노멀벡터
out vec4 color; // 최종 색상 출력
uniform vec3 lightDir; // 광원 방향
uniform vec3 lightColor; // 광원 색상
uniform vec3 objectColor; // 객체 색상
void main() {
// 단순한 Lambertian 반사 모델을 사용한 조명 계산
float diff = max(dot(normalize(fragNormal), normalize(lightDir)), 0.0);
vec3 diffuse = diff * lightColor;
color = vec4(diffuse * objectColor, 1.0);
}
출력 병합 및 프레임 버퍼
최종적으로 계산된 픽셀 값들은 프레임 버퍼에 저장되어 화면에 출력된다. 오픈GL에서는 프레임 버퍼 객체(FBO)를 사용하여 커스터마이즈된 출력 병합을 수행할 수 있다.
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// 컬러 텍스처 첨부
GLuint texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, windowWidth, windowHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
이와 같은 기법을 통해 3D 그래픽스 렌더링 파이프라인의 모든 단계를 구현할 수 있다.