눈팅하는 게임개발자 블로그

OpenGL Advanced 5-3 Blending 본문

공부한거/OpenGL

OpenGL Advanced 5-3 Blending

Palamore 2020. 12. 9. 20:10

원문 사이트

learnopengl.com/Advanced-OpenGL/Blending

 

LearnOpenGL - Blending

Blending Advanced-OpenGL/Blending Blending in OpenGL is commonly known as the technique to implement transparency within objects. Transparency is all about objects (or parts of them) not having a solid color, but having a combination of colors from the obj

learnopengl.com

번역 사이트

heinleinsgame.tistory.com/26

 

[Learn OpenGL 번역] 5-3. 고급 OpenGL - Blending

Blending 고급 OpenGL/Blending OpenGL에서 Blending 이란 흔히 투명한 오브젝트를 구현하는 기술로 알려져있습니다. 투명은 하나의 단색 컬러 가지고 있는 것이 아니라 오브젝트 자체가 가지고 있는 컬러

heinleinsgame.tistory.com

Blending

OpenGL에서 Blending이란 흔히 투명한 오브젝트를 구현하는 기술로서 알려져있다.

투명은 하나의 단색 컬러를 가지고 있다는 것이 아니라 오브젝트 자체가 가지고 있는 컬러와 뒤에 있는

다른 오브젝트의 컬러를 특정 비율로 혼합하는 것을 의미한다.

색이 있는 유리창은 투명한 오브젝트이다.

해당 유리는 자신 본연의 컬러를 가지고 있지만 최종 컬러는 유리 뒤에 있는 모든 오브젝트들의 컬러도 포함하고 있다.

여러 오브젝트들의 여러가지 컬러들을 하나의 컬러로 blend(섞다)하기 때문에 이를 blending이라고 부른다.

투명도는 다음과 같은 결과를 보여준다.

투명한 오브젝트들은 완전히 투명(모든 컬러들이 통과)하거나 불완전하게 투명(컬러들이 통과하지만 본연의 컬러 일부도 출력)

할 수 있다. 오브젝트에 대한 투명도는 컬러의 alpha값으로 정의된다.

이 alpha 컬러 값은 지금까지 자주 보았던 컬러 벡터의 4번째 요소이다.

지금까지 항상 이 값을 1.0으로 설정하여 오브젝트의 투명도를 0.0으로 만들어왔는데,

반면에 이 alpha 값이 0.0을 가지면 해당 오브젝트는 완전히 투명하게 된다.

alpha값이 0.5라면 이는 오브젝트의 컬러가 본연의 컬러의 50%, 뒤에 있는 오브젝트의 컬러 50%로 이루어진다는 의미이다.

 

지금까지 사용해왔던 텍스처들은 3개의 컬러 요소를 가지고 있다.

Red, Green, Blue의 3가지인데, 하지만 일부 텍스처들은 texel마다 alpha값(alpha 채널)을 가지고 있기도 하다.

이 alpha 값은 텍스처의 어느 부분이 투명도를 얼마나 가지는 지에 대해 정확히 알려준다. 

예를 들어 다음의 창문 텍스처는 유리 부분(일반적으로 완벽한 빨간색이지만 75%의 투명도를 가지기 때문에

웹사이트의 배경이 비치게 된다.)에 0.25alpha값을 가지고 있고 모서리에는 0.0의 alpha값을 가지고 있다.

이제 곧 이 창문 텍스처를 scene에 추가할 예정이다.

하지만 먼저 텍스처에 대해 완전히 투명하거나 완전히 불투명하게 만드는 것을 구현하기 위한

좀 더 쉬운 기술을 다룰 것이다.

 

Discarding Fragments

일부 이미지들은 부분적인 투명도를 신경쓰지 않고 텍스처의 컬러 값에 기반하여 무엇인가를 보여주거나

아예 보여주지 않거나 한다.

잔디를 생각해 보자, 잔디와 같은 것을 생성하기 위해 일반적으로 잔디 텍스처를 2D 사각형에 입히고

이 사각형을 scene에 배치한다. 하지만 잔디는 정확히 2D 사각형 모양이 아니므로

잔디 텍스처의 일부분은 무시하고 오직 잔디만을 출력해야 한다.

 

다음의 텍스처는 정확히 이에 부합하는 잔디 텍스처이다.

완전히 불투명(alpha 값이 1.0)하거나 완전히 투명(alpha값이 0.0)할 뿐이다.

그래서 잔디와 같은 식물들을 scene에 추가할 때 잔디의 사각 이미지를 보여주는 것은 본의가 아니고

실제 잔디를 출력하고 이 부분을 제외한 나머지 부분은 지워지도록 해야 한다.

텍스처의 투명한 부분의 fragment를 color buffer에 저장하지 않고 discard(폐기)해야 한다.

진행하기에 앞서 먼저 투명 텍스처를 로드하는 방법에 대해 알아보자.

 

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만 렌더링한다.

결과는 다음과 같다.

텍스처의 모서리를 샘플링할 때 OpenGL은 모서리를 텍스처가 반복되는 다음 텍스처의 모서리로 보간한다.
(wrapping 파라미터를 GL_REPEAT로 설정했기 때문이다.) 이는 일반적으로는 괜찮지만
투명한 값을 사용하고 있기 때문에 텍스처 이미지의 윗부분에 텍스처 이미지의 바닥 컬러와 보간된 값이
보여지게 된다. 이 결과는 텍스처 사각형 주위를 감싸는 모서리가 완전히 투명하지 않게 될 수 있다.
이를 방지하기 위해 alpha 텍스처를 사용할 때마다 텍스처 wrapping 방법을 GL_CLAMP_TO_EDGE로 설정해야 한다.

 

Blending

fragment를 폐기하는 방법은 훌륭하지만 반투명한 이미지를 렌더링해야 할 때는 적합하지 않다.

여러 투명도를 가진 이미지를 렌더링하기 위해 blending을 활성화해야 한다.

OpenGL의 대부분의 기능들처럼 GL_BLEND를 활성화시켜 blending을 사용할 수 있다.

glEnable(GL_BLEND);  

blending을 활성화했으므로 OpenGL에게 실제로 어떻게 섞어야 할지를 알려주어야 한다.

OpenGL에서 blending은 다음의 방정식을 사용하여 수행된다.

Csource : 텍스처의 원본 컬러 벡터.

Cdestination : 컬러 버퍼에 현재 저장된 컬러 벡터.

Fsource : 원본 컬러 alpha 값의 영향력을 설정.

Fdestination : 목적 컬러 alpha 값의 영향력을 설정.

Fragment shader가 실행되고 모든 테스트들이 처리되고 난 후에 이 blend 방정식이 현재 컬러 버퍼에 저장된 값

(현재 fragment 이전에 저장된 이전 fragment의 컬러)과 함께 fragment의 컬러 출력을 옅게 만들어 준다.

원본과 목적 컬러는 OpenGL에 의해 자동적으로 설정되지만 원본, 목적 지수는 직접 선택해서 설정할 수 있다.

간단한 예제로 시작해보자.

두 개의 사각형이 있고 빨간 사각형 위에 반투명한 녹색 사각형을 그린다고 치면,

이 빨간 사각형은 목적 컬러(따라서 먼저 color buffer에 저장되어진다.)가 될 것이고

이제 그 위에 녹색 사각형을 그릴 것이다.

 

그렇다면 지수 값을 무엇으로 설정해야 하는가?

적어도 녹색 사각형에 alpha값을 곱해야 하므로 Fsrc 값을 원본 컬러 벡터의 alpha 값인 0.6으로 설정한다.

그 다음 목적 사각형의 영향력은 alpha값의 나머지로 설정한다.

녹색 사각형이 60%의 영향력을 가지고 있다면 빨간 사각형은 40%의 영향력을 가져야 한다.

그래서 Fdestination의 값을 1 - (원본 컬러의 alpha 값)으로 설정한다. 따라서 이 방정식은 다음과 같다.

결과 60%의 녹색과 40$의 빨간색을 포함하고 있는 혼합된 사각형 fragment가 된다.

최종 컬러는 컬러 버퍼에 저장되어 이전의 컬러를 대체한다.

이는 아주 훌륭한 결과이다.

OpenGL에게 지수들을 이렇게 활용하라고 알려주기 위해 glBlendFunc라는 함수를 사용한다.

glBlendFunc(GLenum sfactor, GLenum dfactor)함수는 두 개의 파라미터로 원본 지수와 목적 지수에 대한

옵션을 설정한다.

OpenGL은 약간의 옵션을 정의해 놓았다. 밑의 표는 흔히 사용되는 옵션들이다.

상수 컬러 벡터 Cconstant는 glBlendColor 함수를 통해 별도로 설정할 수 있다.

옵션
GL_ZERO 지수를 0으로 설정한다.
GL_ONE 지수를 1로 설정한다.
GL_SRC_COLOR 지수를 원본 컬러 벡터 Csource로 설정한다.
GL_ONE_MINUS_SRC_COLOR 지수를 1 - (원본 컬러 벡터 Csource)로 설정한다.
GL_DST_COLOR 지수를 목적 컬러 벡터(Cdestination)로 설정한다.
GL_ONE_MINUS_DST_COLOR 지수를 1 - (목적 컬러 벡터Cdestination)로 설정한다.
GL_SRC_ALPHA 지수를 원본 컬러 벡터Csource의 alpha 요소로 설정한다.
GL_ONE_MINUS_SRC_ALPHA 지수를 1 - (원본 컬러 벡터Csource의 alpha 요소)로 설정한다.
GL_DST_ALPHA 지수를 목적 컬러 벡터Cdestination의 alpha 요소로 설정한다.
GL_ONE_MINUS_DST_ALPHA 지수를 1 - (목적 컬러 벡터Cdestination의 alpha 요소)로 설정한다.
GL_CONSTANT_COLOR 지수를 상수 컬러 벡터Cconstant로 설정한다.
GL_ONE_MINUS_CONSTANT_COLOR 지수를 1 - (상수 컬러 벡터 Cconstant)로 설정한다.
GL_CONSTANT_ALPHA 지수를 상수 컬러 벡터Cconstant의 alpha 요소로 설정한다.
GL_ONE_MINUS_CONSTANT_ALPHA 지수를 1 - (상수 컬러 벡터 Cconstant의 alpha 요소)로 설정한다.

방금 전 두 개의 사각형에 대한 blending 효과를 얻기 위해 원본 지수로 원본 컬러 벡터의 alpha 값을 취하고

목적 지수로 1 - alpha를 취한다. 이를 glBlendFunc 함수로 나타내면 다음과 같다.

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  

RGB와 alpha 채널을 다른 옵션으로 따로 설정하는 것도 가능하다.

glBlendFuncSeparate 함수를 사용하면 된다.

glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);

이 함수는 결과 alpha 요소를 원본 alpha 값에 영향을 받게 하는 것 뿐만 아니라 RGB 요소들에 대해서도 설정한다.

 

심지어 OpenGL은 방정식의 원본과 목적 부분 사이의 연산자를 바꿀 수 있도록 해준다.

지금 당장은 원본 요소와 목적 요소가 서로 더해진다. 하지만 일부러 뺄셈을 하게 할 수도 있다.

glBlendEquation(GLenum mode) 함수는 이 연산자를 3가지 옵션을 통해 설정할 수 있게 해준다.

 

GL_FUNC_ADD : 기본값으로서 두 개의 요소를 서로 더한다.

GL_FUNC_SUBTRACT : 두 개의 요소를 서로 뺀다.

GL_FUNC_REVERSE_SUBTRACT : 순서를 반대로 하여 두 개의 요소를 서로 뺀다.

 

하지만 일반적으로 glBlendEquation 함수는 사용하지 않는다.

GL_FUNC_ADD가 대부분의 연산에 필요한 방정식이기 때문이다.

 

Rendering semi-transparent Texture

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는 그들을 폐기해 버린다.

 

창문 뒤가 보일 수 있게 하기 위해서는 뒤에 있는 창문들을 먼저 그려야 한다.

이는 직접 가까운 창문에서 먼 창문까지 정렬하여 그려야 한다는 것을 의미한다.

완전히 투명한 오브젝트와는 그들을 혼합하는 것이 아닌 간단히 fragment를 폐기하는 방법이 있었다.
이 경우에는 depth 문제에 대해 생각하지 않아도 된다.

 

Don't break Order

여러 오브젝트들로 blending 작업을 하기 위해 멀리 있는 오브젝트를 먼저 그리고 가까이에 있는 오브젝트를 나중에

그려야 한다. 일반적인 blend 되지 않은 오브젝트들은 depth buffer를 사용하여 평소대로 그려지게 되므로

정렬할 필요가 없지만, (정렬된)투명한 오브젝트들을 그리기 전에 먼저 그려주어야 한다.

투명하지 않은 오브젝트와 투명한 오브젝트들이 공존하는 scene을 그릴 때의 일반적인 과정은 다음과 같다.

1. 모든 불투명한 오브젝트를 먼저 그린다.

2. 모든 투명한 오브젝트들을 정렬한다.

3. 모든 투명한 오브젝트들을 정렬한 순서대로 그린다.

 

투명한 오브젝트들을 정렬하는 방법 중 하나는 시점으로부터 오브젝트까지의 거리를 얻는 것이다.

이는 카메라의 위치 벡터와 오브젝트의 위치 벡터 사이의 거리를 취하여 얻을 수 있다.

그 다음 이 거리 값을 위치 벡터와 함께 STL 라이브러리의 map 자료구조에 저장한다.

map은 key 값을 기반으로 자동으로 정렬해주는 자료 구조이다.

그래서 거리 값을 key 값으로 삽입하고 그에 해당하는 모든 위치 값을 삽입한다면 자동적으로

거리에 따라 정렬이 된다.

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

map으로부터 reverse iterator를 얻어 요소들을 반대 순서로 얻고 각 창문 사각형들을 창문 위치로 이동시킨다.

이는 방금 언급한 문제를 해결하기 위해 투명한 오브젝트들을 정렬하는 비교적 간단한 방법이다.

결과는 다음과 같다.

 

거리에 대해서 오브젝트들을 정렬하는 방법은 특정 상황에서 잘 동작하는 반면 회전, 확대 혹은 다른 변환들을

염두에 두지 않고 특이한 모양의 오브젝트들은 간단히 위치 벡터를 사용하는 것만으로 충분하지 않을 수 있다.

 

오브젝트를 정렬하는 것은 어떠한 유형의 scene을 가지고 있는지에 따라 다르고 꽤 어려운 작업이다.

추가 비용을 소모하기도 한다. 단색 오브젝트와 투명한 오브젝트가 공존하는 scene을 완벽히 렌더링하는 것은

쉽지 않다.

order independent transparency 같은 고급 기술들도 존재하지만 이 강좌의 범위를 벗어난다.

이제는 정상적으로 오브젝트들을 blending 할 수 있어야 한다.

 

 

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

OpenGL Advanced 5-5 Framebuffers  (0) 2020.12.10
OpenGL Advanced 5-4 Face culling  (0) 2020.12.10
OpenGL Advanced 5-2 Stencil testing  (0) 2020.12.08
OpenGL Advanced 5-1 Depth testing  (0) 2020.12.08
OpenGL Model Loading 4-3 Model  (0) 2020.12.07