13장 중급 그래픽스 - ② 텍스처로 렌더링
더욱 현실감 있는 그래픽 구현을 위해서는 다양한 그래픽 기술들이 사용된다. 여기에서는 몇 가지 중급 그래픽스 개념을 다룬다. 텍스처 품질을 향상시키거나 텍스처로 렌더링 하는 방법. 그리고 지연 셰이딩같이 장면을 라이팅하는 방법을 살펴본다.
💡 13장의 목차
- 텍스처 샘플링 기법
- 텍스처로 렌더링
- 지연 셰이딩
텍스처로 렌더링
지금까지는 렌더링 결과를 프레임 버퍼에 그렸다. 사실 렌더링 결과라는 것은 그리 특별한 것이 아닌 2D 이미지이다. 그래서 렌더링 결과를 프레임 버퍼가 아닌 텍스처에 그리는 것도 가능하다. 일부 그래픽 기술은 색상 버퍼로 최종 출력을 계산하기 전에 임시 저장소로써 텍스처를 사용하기도 한다. 그 외에도 거울을 구현할 수도 있다. 거울의 텍스처는 거울 시점으로 렌더링한 이미지이기 때문이다.
텍스처 생성하기
텍스처로 렌더링하기 위해서는 텍스처를 생성해야한다. 기존 텍스처 생성과 차이는 다음과 같다.
- RGBA 포맷을 지정하지 않고 파라미터로 포맷 설정
- 텍스처는 초기 이미지 데이터가 없다 (매 프레임마다 렌더링을 통해서 이미지가 생성됨)
glTexImage2D
의 마지막 파리미터는 nullptr이다.
glTexImage2D
함수의 파라미터는 다음과 같다.
- 첫 번째 파라미터는 텍스처 타겟을 지정.
- 두 번째 파라미터는 텍스처의 mipmap 레벨을 수동으로 지정하고 싶을 때 지정. 그 외는 0으로 지정. (마지막 파라미터가
nullptr
이면 무시됨) - 세 번째 파라미터는 저장하고 싶은 텍스처의 포맷. (마지막 파라미터가
nullptr
이면 무시됨) - 네 번째, 다섯 번째 파라미터는 결과 텍스처의 너비와 높이
- 여섯 번째 파라미터는 항상 0을 지정해야합니다.
- 일곱 번째, 원본 이미지의 포맷 (렌더링 결과를 담을 것이기 때문에 항상 RGB)
- 여덟 번째 파라미터는 원본 이미지의 데이터타입
- 마지막, 실제 이미지 데이터입니다. (데이터 파일은 매 프레임마다 전달할 것이기 때문에
nullptr
)
아래와 같이 렌더링 결과를 담을 텍스처 생성함수를 구현한다.
void Texture::CreateForRendering(int width, int height, unsigned int format)
{
mWidth = width;
mHeight = height;
// 텍스처 생성 후 ID를 받아온다.
glGenTextures(1, &mTextureID);
// GL_TEXTURE_2D에 방금 생성한 ID의 텍스처를 바인딩한다.
glBindTexture(GL_TEXTURE_2D, mTextureID);
// 이미지의 크기와 포맷을 지정하고 데이터는 nullptr
glTexImage2D(GL_TEXTURE_2D, 0, format, mWidth, mHeight, 0, GL_RGB, GL_FLOAT, nullptr);
// 거울 효과를 위해서는 항상 최근접 이웃 필터링을 사용.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
프레임 버퍼 개체 생성
프레임 버퍼 개체(Framebuffer Object)는 프레임 버퍼에 관한 모든 정보를 포함한다. OpenGL은 ID가 0인 기본 프레임 버퍼 개체를 제공하는데 이 기본 프레임 버퍼 개체는 현재까지 그려왔던 프레임 버퍼다. 추가로 FBO를 생성하면 렌더링에서 사용할 프레임 버퍼를 선택하는 것이 가능하다.
프레임버퍼를 생성 및 바인딩하고, 렌더링을 위한 텍스처를 생성하는 CreateMirrorTarget
함수를 아래와 같이 구현한다. (설명은 주석을 참고)
bool Renderer::CreateMirrorTarget()
{
int width = static_cast<int>(mScreenWidth) / 4;
int height = static_cast<int>(mScreenHeight) / 4;
// 거울 텍스처를 위한 프레임 버퍼 생성
glGenFramebuffers(1, &mMirrorBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, mMirrorBuffer);
// 렌더링을 위해 사용할 텍스처 생성
mMirrorTexture = new Texture();
mMirrorTexture->CreateForRendering(width, height, GL_RGB);
// 깊이 버퍼 추가
GLuint depthBuffer;
glGenRenderbuffers(1, &depthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuffer);
// 거울 텍스처를 프레임 버퍼의 출력 타깃으로 설정
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, mMirrorTexture->GetTextureID(), 0);
// 이 프레임 버퍼가 그리는 출력 버퍼 리스트 설정
GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0 };
glDrawBuffers(1, drawBuffers);
// 모든 설정이 제대로 됐는지 확인
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
// 문제가 발생했다면 프레임 버퍼 삭제
// 텍스처를 삭제하고 false 리턴
glDeleteFramebuffers(1, &mMirrorBuffer);
mMirrorTexture->Unload();
delete mMirrorTexture;
mMirrorTexture = nullptr;
return false;
}
return true;
}
프레임 버퍼 개체로 렌더링
거울을 지원하려면 3D 장면을 두 번 렌더링해야한다. 거울 관점에서 한 번 렌더링하고, 한 번은 일반 카메라 관점에서 렌더링한다. 3D 장면을 여러번 렌더링 하기위해서 Draw3DScene
함수구현은 다음과 같다. 거울의 관점과 일반 카메라의 관점의 차이는 결국 view 메트릭스의 차이이다. 때문에 view 메트릭스를 매개변수로 받을 수 있도록 한다.
void Renderer::Draw3DScene(unsigned int framebuffer,
const Matrix4& view, const Matrix4& proj, float viewPortScale /*= 1.0f*/)
{
// 현재 프레임 버퍼를 설정
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// 뷰포트 스케일값으로 뷰포트 크기 설정
glViewport(0, 0,
static_cast<int>(mScreenWidth * viewPortScale),
static_cast<int>(mScreenHeight * viewPortScale)
);
// 색상 버퍼 / 깊이 버퍼 클리어
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 메시 컴포넌트를 그린다
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
// mesh 셰이더 활성화
mMeshShader->SetActive();
// mesh 셰이더에 뷰-투영 행렬 적용
mMeshShader->SetMatrixUniform("uViewProj", view * proj);
// 조명관련 셰이더 변수 설정
SetLightUniforms(mMeshShader, view);
// 모든 메시에 동일한 셰이더 적용 중.
for (auto mc : mMeshComps)
{
if (mc->GetVisible())
{
mc->Draw(mMeshShader);
}
}
// 스키닝 셰이더를 활용한 스키닝 메시 그리기
mSkinnedShader->SetActive();
mSkinnedShader->SetMatrixUniform("uViewProj", view * mProjection);
SetLightUniforms(mSkinnedShader, view);
for (auto sk : mSkeletalMeshes)
{
if (sk->GetVisible())
{
sk->Draw(mSkinnedShader);
}
}
}
그리고 이 Draw3DScene
함수를 이용해서 거울관점에서 한 번 렌더링하고(mMirrorBuffer
), 기본 카메라 관점(0)에서 한 번더 렌더링 하는 Draw
함수를 다음과 같이 구현한다. 거울 텍스처는 백밀러의 view 매트릭스를, 일반 카메라는 플레이어의 view 매트릭스은 mView
를 활용해서 렌더링을 한다. Draw3DScene
함수로 3D 오브젝트를 모두 그린 후에는 2D 스프라이트와 UI들을 그려준다.
void Renderer::Draw()
{
// 거울 텍스처를 그린다. (뷰포트 스케일값: 0.25)
Draw3DScene(mMirrorBuffer, mMirrorView, mProjection, 0.25f);
// 이제 기본 프레임 버퍼(기본 프레임 버퍼의 ID는 0이다.)로 3D 장면을 그린다.
Draw3DScene(0, mView, mProjection);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 깊이 스텐실 비활성화
glDisable(GL_DEPTH_TEST);
// 알파블렌딩 활성화
glEnable(GL_BLEND);
glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
// 스프라이트 셰이더와 버텍스 활성화
mSpriteShader->SetActive();
mSpriteVerts->SetActive();
for (auto sprite : mSprites)
{
if (sprite->GetVisible())
{
sprite->Draw(mSpriteShader);
}
}
// UI를 그린다.
for (auto ui : mGame->GetUIStack())
{
ui->Draw(mSpriteShader);
}
// Swap the buffers
SDL_GL_SwapWindow(mWindow);
}
이제 9장에서 구현한 Followw 카메라의 방향만 바꾼 MirrorCamera 클래스를 생성한뒤 캐릭터 엑터에 붙여서 mMirrorView를 갱신한다.
HUD에 거울 텍스처 그리기
OpenGL은 UV원점을 왼쪽 상단 구석(일반적) 대신 이미지의 왼쪽 하단 구석으로 지정한다. UIScreen 클래스에서 기존 DrawTexture에서 텍스처를 그릴때 스케일 행렬을 사용했는데 거울 텍스처를 그릴때는 스케일 y값을 반전시키면 정상적인 결과를 얻을 수 있다.
void UIScreen::DrawTexture(class Shader* shader, class Texture* texture,
const Vector2& offset, float scale /*1.0f*/, bool flipY /*false*/)
{
float yScale = static_cast<float>(texture->GetHeight()) * scale;
if (flipY) { yScale *= -1.0f; }
Matrix4 scaleMat = Matrix4::CreateScale(
static_cast<float>(texture->GetWidth()) * scale,
yScale,
1.0f);
// 이하 동일
}
마지막으로 헤드업 디스플레이를 담당하는 HUD클래스의 Draw
함수에 거울 텍스처를 추가하면 정상적으로 출력된다.
Texture* mirror = mGame->GetRenderer()->GetMirrorTexture();
DrawTexture(shader, mirror, Vector2(-350.0f, -250.0f), 1.0f, true);
출처:
에이콘 출판 <Game Programming in C++>