게임공장
[Learn OpenGL 번역] 5-3. 고급 OpenGL - Blending 본문
Blending
고급 OpenGL/Blending
OpenGL에서
투명한 오브젝트들은 완전히 투명(모든 컬러들이 통과)하거나 불완전하게 투명(컬러들이 통과하지만 본연의 컬러의 일부도 출력)할 수 있습니다. 오브젝트에 대한 투명도는 컬러의 1.0
로 설정하여 오브젝트의 투명도를 0.0
으로 만들어왔습니다. 반면에 이 alpha 값이 0.0
을 가지면 이 오브젝트는 완전히 투명하게 됩니다. alpha 값이 0.5
라면 이는 오브젝트의 컬러가 본연의 컬러 50%, 뒤에 있는 오브젝트의 컬러 50%로 이루어진다는 의미입니다.
우리가 지금까지 사용했던 텍스처들은 3
개의 컬러 요소를 가지고 있습니다. red, green, blue 이렇게 3가지입니다. 하지만 일부 텍스쳐들은 texel마다 0.25
alpha 값을 가지고 있고 모서리에는 0.0
alpha 값을 가지고 있습니다.
우리는 곧 이 창문 텍스처를 scene에 추가할 예정입니다. 하지만 먼저 텍스처에 대해 완전히 투명하거나 완전히 불투명하게 만든는 것을 구현하기 위한 좀 더 쉬운 기술을 다룰것입니다.
Fragments 폐기
일부 이미지들은 부분적인 투명도를 신경쓰지 않고 텍스처의 컬러 값에 기반하여 무엇인가를 보여주거나 아예 보여주지 않거나 합니다. 잔디를 생각해 봅시다. 잔디와 같은 것을 생성하기 위해 여러분은 일반적으로 잔디 텍스처를 2D 사각형에 입히고 이 사각형을 여러분의 scene에 배치할 것입니다. 하지만 잔디는 정확히 2D 사각형 모양이 아니므로 여러분은 잔디 텍스처의 일부분은 무시하고 오직 잔디만 출력해야 합니다.
다음의 텍스처는 정확히 이러한 텍스처입니다. 완전히 불투명(alpha 값이 1.0
)하거나 완전히 투명(alpha 값이 0.0
)할 뿐입니다. 잔디가 없는 부분을 볼 수 있을 것입니다. 이 이미지에서 웹사이트의 배경을 보여주는 곳 말입니다.
그래서 잔디와 같은 식물들을 scene에 추가할 때 잔디의 사각 이미지를 보여주는 것을 원하지 않고 실제 잔디를 출력하고 이 부분을 제외한 나머지 부분은 지워지게 하기를 원합니다. 우리는 텍스처의 투명한 부분의 fragment를 color buffer에 저장히자 않고
Alpha 값과 함께 텍스처를 불러오기 위해 수정해야할 사항은 많지 않습니다. stb_image
는 가능하다면 이미지의 alpha 채널을 자동으로 불러오지만 OpenGL에게 텍스처를 생성할 때 텍스처의 alpha 채널을 사용한다고 말해주어야 합니다.
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
또한 fragment shader에서 텍스처의 컬러 요소들을 RGB 요소가 아닌 4
가지의 컬러 요소를 얻어야 합니다.
void main()
{
// FragColor = vec4(vec3(texture(texture1, TexCoords)), 1.0);
FragColor = texture(texture1, TexCoords);
}
우리는 이제 투명 텍스처를 불러오는 방법을 알고 있으므로 depth testing 강좌에서 소개했던 기본적인 scene에 잔디들을 깔아봅시다.
잔디들의 위치를 나타내는 glm::vec3
변수들에 대한 작은 vector를 생성합니다.
vector<glm::vec3> vegetation;
vegetation.push_back(glm::vec3(-1.5f, 0.0f, -0.48f));
vegetation.push_back(glm::vec3( 1.5f, 0.0f, 0.51f));
vegetation.push_back(glm::vec3( 0.0f, 0.0f, 0.7f));
vegetation.push_back(glm::vec3(-0.3f, 0.0f, -2.3f));
vegetation.push_back(glm::vec3( 0.5f, 0.0f, -0.6f));
각 잔디 오브젝트들은 잔디 텍스처를 입힌 사각형으로 렌더링됩니다. 이는 완벽한 3D는 아니지만 실제로 복잡한 모델들을 불러오는 것보다 매우 효율적입니다. 동일한 위치에 회전된 여러 잔디 사각형을 추가하는 트릭을 사용하면 멋진 결과를 얻을 수 있습니다.
잔디 텍스처가 사각형 오브젝트에 추가되었기 때문에 우리는 다른 VAO를 생성하고 VBO를 채우고 적절한 vertex attribute pointer들을 설정해야 합니다. 그런 다음 바닥과 두개의 큐브를 그린 다음 잔디를 그릴 것입니다.
glBindVertexArray (vegetationVAO);
glBindTexture (GL_TEXTURE_2D, grassTexture);
for(unsigned int i = 0; i < vegetation.size(); i++)
{
model = glm::mat4();
model = glm::translate (model, vegetation[i]);
shader.setMat4("model", model);
glDrawArrays (GL_TRIANGLES, 0, 6);
}
응용 프로그램을 실행하면 다음과 같이 보일 것입니다.
이는 OpenGL이 기본적으로는 alpha 값을 가지고 무엇을 해야할 지 모르기 때문에 일어나는 현상입니다. 우리는 이를 직접 수동으로 해주어야 합니다. 운좋게도 shader를 이용하면 쉽게 할 수 있습니다. GLSL은 (호출한 후부터) 더이상 fragment가 처리되지 않고 color buffer에 저장하지 않게 해주는 discard
명령을 제공해줍니다. 이 명령 덕분에 우리는 fragment shader가 alpha 값을 가지고 있는지 확인할 수 있고 가지고 있다면 fragment를 폐기하도록 할 수 있습니다.
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1)
discard;
FragColor = texColor;
}
샘플링된 텍스처 컬러가 0.1
보다 작은 alpha 값을 가지고 있는지 확인하고 그렇다면 fragment를 폐기합니다. 이 fragment shader는 완전히 투명하지 않은 fragment만 렌더링합니다. 이제 다음과 같은 결과를 볼 수 있을 것입니다.
glTexParameter i( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameter i( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
여기에서 전체 소스 코드를 확인할 수 있습니다.
Blending
fragment를 폐기하는 방법은 훌륭하지만 반투명한 이미지를 렌더링해야할 때는 적합하지 않습니다. 여러 투명도를 가진 이미지를 렌더링하기 위해 우리는
glEnable (GL_BLEND);
blending을 활성화했으므로 OpenGL에게 실제로 어떻게 섞어야할지를 말해주어야합니다.
OpenGL에서 blending은 다음의 방정식을 사용하여 수행됩니다.
- : 원본 컬러 벡터. 이 것은 텍스처의 원본 컬러 벡터입니다.
- : 목적 컬러 벡터. 이 것은 컬러 버퍼에 현재 저장된 컬러 벡터입니다.
- : 원본 지수 값. 원본 컬러 alpha 값의 영향력을 설정합니다.
- : 목적 지수 값. 목적 컬러 alpha 값의 영향력을 설정합니다.
Fragment shader가 실행되고 모든 테스트들이 처리되고 난 후에 이
&nbps; 우리는 두개의 사각형을 가지고 있고 빨간 사각형 위에 반투명한 녹색 사각형을 그리고 싶습니다. 이 빨간 사각형은 목적 컬러(따라서 color buffer에 먼저 저장되어집니다)가 될 것이고 이제 그 위에 녹색 사각형을 그릴 것입니다.
그렇다면 우리는 지수 값을 무엇으로 설정해야 할까요? 우리는 적어도 녹색 사각형에 alpha 값을 곱하기 원하므로 F src 값을 원본 컬러 벡터의 alpha 값인 0.6
으로 설정합니다. 그런 다음 목적 사각형의 영향력은 alpha 값의 나머지로 설정합니다. 녹색 사각형이 60%의 영향력을 가지고 있다면 빨간 사각형은 40%(1.0 - 0.6
)의 영향력을 가지기를 원합니다. 그래서 우리는 F destination 값을 1 - (원본 컬러 벡터의 alpha 값)으로 설정합니다. 따라서 이 방정식은 다음과 같습니다.
이 결과는 60%의 녹색과 40%의 빨간색을 포함하고 있는 혼합된 사각형 fragment들입니다.
최종 컬러는 컬러 버퍼에 저장되어 이전의 컬러를 대체합니다.
이는 아주 훌륭합니다. 하지만 실제로 OpenGL에게 지수들을 이렇게 사용하라고 어떻게 말할 수 있을까요?
옵션 | 값 |
---|---|
GL_ZERO |
지수를 0으로 설정합니다. |
GL_ONE |
지수를 1로 설정합니다. |
GL_SRC_COLOR |
지수를 원본 컬러 벡터 로 설정합니다. |
GL_ONE_MINUS_SRC_COLOR |
지수를 1 - (원본 컬러 벡터 )로 설정합니다. |
GL_DST_COLOR |
지수를 목적 컬러 벡터 로 설정합니다. |
GL_ONE_MINUS_DST_COLOR |
지수를 1 - (목적 컬러 벡터 )로 설정합니다. |
GL_SRC_ALPHA |
지수를 원본 컬러 벡터 의 alpha 요소로 설정합니다. |
GL_ONE_MINUS_SRC_ALPHA |
지수를 1- (원본 컬러 벡터 의 alpha 요소)로 설정합니다. |
GL_DST_ALPHA |
지수를 목적 컬러 벡터 의 alpha 요소로 설정합니다. |
GL_ONE_MINUS_DST_ALPHA |
지수를 1 - (목적 컬러 벡터 의 alpha 요소)로 설정합니다. |
GL_CONSTANT_COLOR |
지수를 상수 컬러 벡터 로 설정합니다. |
GL_ONE_MINUS_CONSTANT_COLOR |
지수를 1 - (상수 컬러 벡터 )로 설정합니다. |
GL_CONSTANT_ALPHA |
지수를 상수 컬러 벡터 의 alpha 요소로 설정합니다. |
GL_ONE_MINUS_CONSTANT_ALPHA |
지수를 1 - (상수 컬러 벡터 의 alpha 요소)로 설정합니다. |
방금 전 두개의 사각형에 대한 blending 효과를 얻기 위해 원본 지수로 원본 컬러 벡터의 alpha 값을 취하고 목적 지수로 1 - alpha를 취합니다. 이를
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
RGB와 alpha 채널을 다른 옵션으로 따로 설정하는 것도 가능합니다.
glBlendFunc Separate (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
이 함수는 결과 alpha 요소를 원본 alpha 값에 영향을 받게 하는 것 뿐만 아니라 RGB 요소들에 대해서도 설정합니다.
심지어 OpenGL은 방정식의 원본과 목적 부분 사이의 연산자를 바꿀 수 있도록 해줍니다. 지금 당장은 원본 요소와 목적 요소가 서로 더해집니다. 하지만 원한다면 뺄셈을 할 수도 있습니다.
GL_FUNC_ADD
: 기본값으로서 두 개의 요소를 서로 더합니다: Source + DestinationGL_FUNC_SUBTRACT
: 두 개의 요소를 서로 뺍니다: Source - DestinationGL_FUNC_REVERSE_SUBTRACT
: 순서를 반대로 하여 두 개의 요소를 서로 뺍니다: Destination - Source
일반적으로
반투명 텍스처 렌더링
OpenGL이 blending을 어떻게 수행하는지 알았으므로 여러 반투명 창문을 추가하여 테스트해 볼 시간입니다. 이 강좌의 시작에서 사용했던 scene을 사용할 것입니다. 하지만 잔디 테스처를 렌더링하는 대신 반투명 창문 텍스처를 사용할 것입니다.
먼저, blending을 활성화하고 적절한 blending 함수를 설정하여 초기화합니다.
glEnable (GL_BLEND);
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
blending을 활성화했기 때문에 fragment를 폐기할 필요 없으므로 fragment shader를 원래의 버전으로 수정합니다.
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoords);
}
이번에는(OpenGL이 fragment를 렌더링할때마다) alpha 값을 기반으로 하여 현재 fragment의 컬러와 color buffer에 저장되어 있는 fragment 컬러를 혼합합니다. 창문 텍스처의 유리 부분이 반투명이기 때문에 창문의 뒷 배경을 볼 수 있어야 합니다.
하지만 가까이에서 보면 뭔가 이상하다는 것을 알아차릴 것입니다. 앞에 있는 창문의 투명한 부분이 뒤에 있는 창문을 가리고 있습니다. 왜 이런일이 생겼을까요?
이 이유는 depth testing이 blending과 함께 사용되기 곤란하기 때문입니다. depth buffer를 작성할 때 이 fragment가 투명한지 투명하지 않은지를 생각하지 않고 작성됩니다. 이 결과 창문의 전체 사각형은 투명도에 아무 관계없이 depth testing이 수행됩니다. 투명한 부분이 창문 뒤의 배경을 보일 수 있도록 해야함에도 불구하고 depth test는 그들을 폐기해버립니다.
그래서 우리는 창문들을 간단하게 렌더링할 수 없지만 이를 해결하고 싶습니다. 창문 뒤가 보일 수 있게 하기위해서는 뒤에 있는 창문들을 먼저 그려야합니다. 이는 우리가 직접 가까운 창문에서 먼 창문까지 정렬하여 그려야한다는 것을 의미합니다.
순서를 어기지 마세요
여러 오브젝트들로 blending 작업을 하기 위해 우리는 멀리 있는 오브젝트를 먼저 그리고 가까이에 있는 오브젝트를 나중에 그려야 합니다. 일반적인 blend 되지 않은 오브젝트들은 depth buffer를 사용하여 평소대로 그려지게 되므로 그들은 정렬할 필요가 없습니다. (정렬된) 투명한 오브젝트들을 그리기 전에 먼저 그려주어야 합니다. 투명하지 않은 오브젝트와 투명한 오브젝트들이 공존하는 scene을 그릴 때의 일반적인 과정은 다음과 같습니다.
- 모든 불투명한 오브젝트들을 먼저 그립니다.
- 모든 투명한 오브젝트들을 정렬합니다.
- 모든 투명한 오브젝트들을 정렬한 순서대로 그립니다.
투명한 오브젝트들을 정렬하는 방법 중 하나는 시점으로부터 오브젝트까지의 거리를 얻는 것입니다. 이는 카메라의 위치 벡터와 오브젝트의 위치 벡터 사이의 거리를 취하여 얻을 수 있습니다. 그런 다음 이 거리 값을 위치 벡터와 함께 STL 라이브러리의
std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.Position - windows[i]);
sorted[distance] = windows[i];
}
결과는 distance key 값을 기반으로 짧은 거리부터 먼 거리까지 정렬된 순서의 창문 위치가 저장된 객체입니다.
그런 다음 이번에는 렌더링 할 때 map의 값을 반대 순서(먼 창문에서 가까운 창문으로)로 취하고 해당 창문들을 그립니다.
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate (model, it->second);
shader.setMat4("model", model);
glDrawArrays (GL_TRIANGLES, 0, 6);
}
우리는
여기에서 정렬을 추가한 전체 소스 코드를 확인할 수 있습니다.
거리에 대해서 오브젝트들을 정렬하는 방법은 특정 상황에서 잘 동작하는 반면에 회전, 확대 혹은 다른 변환들을 염두에 두지 않고 특이한 모양의 오브젝트들은 간단히 위치 벡터를 사용하는 것만으로는 부족합니다.
오브젝트를 정렬하는 것은 여러분이 어떠한 유형의 scene을 가지고 있는지에 따라 다르고 꽤 어려운 작업입니다. 추가 처리 파워를 소요하기도 합니다. 단색 오브젝트와 투명한 오브젝트가 공존하는 scene을 완벽히 렌더링하는 것은 쉽지 않습니다.