Notice
Recent Posts
Recent Comments
Link
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

게임공장

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

카테고리 없음

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

짱승_ 2018. 8. 7. 22:01

Blending

고급 OpenGL/Blending

  OpenGL에서 Blending이란 흔히 투명한 오브젝트를 구현하는 기술로 알려져있습니다. 투명은 하나의 단색 컬러 가지고 있는 것이 아니라 오브젝트 자체가 가지고 있는 컬러와 뒤에 있는 다른 오브젝트의 컬러를 여러 비율로 혼합하는 것을 말합니다. 색이 있는 유리창은 투명한 오브젝트입니다. 이 유리는 자기 본연의 컬러를 가지고 있지만 최종 컬러는 유리 뒤에 있는 모든 오브젝트들의 컬러도 포함하고 있습니다. 우리는 여러 오브젝트들의 여러가지 컬러들을 하나의 컬러로 blend(섞다)하기 때문에 이를 blending이라고 부릅니다. 따라서 투명도는 다음과 같은 결과를 보여줍니다.


Image of full transparent window and partially transparent window

  투명한 오브젝트들은 완전히 투명(모든 컬러들이 통과)하거나 불완전하게 투명(컬러들이 통과하지만 본연의 컬러의 일부도 출력)할 수 있습니다. 오브젝트에 대한 투명도는 컬러의 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.25 alpha 값을 가지고 있고 모서리에는 0.0 alpha 값을 가지고 있습니다.


Texture image of window with transparency

  우리는 곧 이 창문 텍스처를 scene에 추가할 예정입니다. 하지만 먼저 텍스처에 대해 완전히 투명하거나 완전히 불투명하게 만든는 것을 구현하기 위한 좀 더 쉬운 기술을 다룰것입니다.

Fragments 폐기

  일부 이미지들은 부분적인 투명도를 신경쓰지 않고 텍스처의 컬러 값에 기반하여 무엇인가를 보여주거나 아예 보여주지 않거나 합니다. 잔디를 생각해 봅시다. 잔디와 같은 것을 생성하기 위해 여러분은 일반적으로 잔디 텍스처를 2D 사각형에 입히고 이 사각형을 여러분의 scene에 배치할 것입니다. 하지만 잔디는 정확히 2D 사각형 모양이 아니므로 여러분은 잔디 텍스처의 일부분은 무시하고 오직 잔디만 출력해야 합니다.


  다음의 텍스처는 정확히 이러한 텍스처입니다. 완전히 불투명(alpha 값이 1.0)하거나 완전히 투명(alpha 값이 0.0)할 뿐입니다. 잔디가 없는 부분을 볼 수 있을 것입니다. 이 이미지에서 웹사이트의 배경을 보여주는 곳 말입니다.


Texture image of grass with transparency

  그래서 잔디와 같은 식물들을 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);
}  

  응용 프로그램을 실행하면 다음과 같이 보일 것입니다.


Not discarding transparent parts of texture results in weird artifacts in OpenGL

  이는 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만 렌더링합니다. 이제 다음과 같은 결과를 볼 수 있을 것입니다.


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

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);	
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);    

  여기에서 전체 소스 코드를 확인할 수 있습니다.

Blending

  fragment를 폐기하는 방법은 훌륭하지만 반투명한 이미지를 렌더링해야할 때는 적합하지 않습니다. 여러 투명도를 가진 이미지를 렌더링하기 위해 우리는 blending을 활성화해야 합니다. OpenGL의 대부분의 기능들처럼 GL_BLEND를 활성화시켜 blending을 사용할 수 있습니다.


glEnable(GL_BLEND);  

  blending을 활성화했으므로 OpenGL에게 실제로 어떻게 섞어야할지를 말해주어야합니다.


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


Image of grass leaves rendered with fragment discarding in OpenGL
  • C¯source: 원본 컬러 벡터. 이 것은 텍스처의 원본 컬러 벡터입니다.
  • C¯destination: 목적 컬러 벡터. 이 것은 컬러 버퍼에 현재 저장된 컬러 벡터입니다.
  • Fsource: 원본 지수 값. 원본 컬러 alpha 값의 영향력을 설정합니다.
  • Fdestination: 목적 지수 값. 목적 컬러 alpha 값의 영향력을 설정합니다.

  Fragment shader가 실행되고 모든 테스트들이 처리되고 난 후에 이 blend 방정식이 현재 컬러 버퍼에 저장된 값(현재 fragment 전에 저장된 이전 fragment의 컬러)과 함께 fragment의 컬러 출력을 옅게 만들어줍니다. 원본과 목적 컬러는 OpenGL에 의해 자동적으로 설정되지만 원복, 목적 지수는 우리가 선택하여 설정할 수 있습니다. 간단한 예제로 시작해봅시다.


Two squares where one has alpha value lower than 1

&nbps; 우리는 두개의 사각형을 가지고 있고 빨간 사각형 위에 반투명한 녹색 사각형을 그리고 싶습니다. 이 빨간 사각형은 목적 컬러(따라서 color buffer에 먼저 저장되어집니다)가 될 것이고 이제 그 위에 녹색 사각형을 그릴 것입니다.


  그렇다면 우리는 지수 값을 무엇으로 설정해야 할까요? 우리는 적어도 녹색 사각형에 alpha 값을 곱하기 원하므로 F src 값을 원본 컬러 벡터의 alpha 값인 0.6으로 설정합니다. 그런 다음 목적 사각형의 영향력은 alpha 값의 나머지로 설정합니다. 녹색 사각형이 60%의 영향력을 가지고 있다면 빨간 사각형은 40%(1.0 - 0.6)의 영향력을 가지기를 원합니다. 그래서 우리는 F d estination 값을 1 - (원본 컬러 벡터의 alpha 값)으로 설정합니다. 따라서 이 방정식은 다음과 같습니다.


Two squares where one has alpha value lower than 1

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


Two containers where one has alpha value lower than 1

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


  이는 아주 훌륭합니다. 하지만 실제로 OpenGL에게 지수들을 이렇게 사용하라고 어떻게 말할 수 있을까요? glBlendFunc라고 불리는 함수가 존재합니다.


  glBlendFunc(GLenum sfactor, GLenum dfactor) 함수는 두개의 파라미터로 원본 지수와 목적 지수에 대한 옵션을 설정합니다. OpenGL은 약간의 옵션을 정의해놓았습니다. 밑의 표는 흔히 사용되는 옵션들입니다. 상수 컬러 벡터 C¯costantglBlendColor 함수를 통해 별도로 설정할 수 있습니다.


옵션
GL_ZERO 지수를 0으로 설정합니다.
GL_ONE 지수를 1로 설정합니다.
GL_SRC_COLOR 지수를 원본 컬러 벡터 C¯source로 설정합니다.
GL_ONE_MINUS_SRC_COLOR 지수를 1 - (원본 컬러 벡터 C¯source)로 설정합니다.
GL_DST_COLOR 지수를 목적 컬러 벡터 C¯destination로 설정합니다.
GL_ONE_MINUS_DST_COLOR 지수를 1 - (목적 컬러 벡터 C¯destination)로 설정합니다.
GL_SRC_ALPHA 지수를 원본 컬러 벡터 C¯sourcealpha 요소로 설정합니다.
GL_ONE_MINUS_SRC_ALPHA 지수를 1- (원본 컬러 벡터 C¯sourcealpha 요소)로 설정합니다.
GL_DST_ALPHA 지수를 목적 컬러 벡터 C¯destinationalpha 요소로 설정합니다.
GL_ONE_MINUS_DST_ALPHA 지수를 1 - (목적 컬러 벡터 C¯destinationalpha 요소)로 설정합니다.
GL_CONSTANT_COLOR 지수를 상수 컬러 벡터 C¯costant로 설정합니다.
GL_ONE_MINUS_CONSTANT_COLOR 지수를 1 - (상수 컬러 벡터 C¯costant)로 설정합니다.
GL_CONSTANT_ALPHA 지수를 상수 컬러 벡터 C¯costantalpha 요소로 설정합니다.
GL_ONE_MINUS_CONSTANT_ALPHA 지수를 1 - (상수 컬러 벡터 C¯costantalpha 요소)로 설정합니다.

  방금 전 두개의 사각형에 대한 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: 기본값으로서 두 개의 요소를 서로 더합니다: Source + Destination
  • GL_FUNC_SUBTRACT: 두 개의 요소를 서로 뺍니다: Source - Destination
  • GL_FUNC_REVERSE_SUBTRACT: 순서를 반대로 하여 두 개의 요소를 서로 뺍니다: Destination - Source

  일반적으로 glBlendEquation 함수는 사용하지 않습니다. GL_FUNC_ADD가 대부분의 연산에 필요한 방정식이기 때문이죠 하지만 여러분이 원래의 방식을 깨트리고 싶다면 다른 방정식이 여러분에게 맞을 것입니다.

반투명 텍스처 렌더링

  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 컬러를 혼합합니다. 창문 텍스처의 유리 부분이 반투명이기 때문에 창문의 뒷 배경을 볼 수 있어야 합니다.


A blended scene in OpenGL where order is incorrect.

  하지만 가까이에서 보면 뭔가 이상하다는 것을 알아차릴 것입니다. 앞에 있는 창문의 투명한 부분이 뒤에 있는 창문을 가리고 있습니다. 왜 이런일이 생겼을까요?


  이 이유는 depth testing이 blending과 함께 사용되기 곤란하기 때문입니다. depth buffer를 작성할 때 이 fragment가 투명한지 투명하지 않은지를 생각하지 않고 작성됩니다. 이 결과 창문의 전체 사각형은 투명도에 아무 관계없이 depth testing이 수행됩니다. 투명한 부분이 창문 뒤의 배경을 보일 수 있도록 해야함에도 불구하고 depth test는 그들을 폐기해버립니다.


  그래서 우리는 창문들을 간단하게 렌더링할 수 없지만 이를 해결하고 싶습니다. 창문 뒤가 보일 수 있게 하기위해서는 뒤에 있는 창문들을 먼저 그려야합니다. 이는 우리가 직접 가까운 창문에서 먼 창문까지 정렬하여 그려야한다는 것을 의미합니다.

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

순서를 어기지 마세요

  여러 오브젝트들로 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은 다음과 같이 보일 것입니다.


Image of an OpenGL scene with blending enabled, objects are sorted from far to near

  여기에서 정렬을 추가한 전체 소스 코드를 확인할 수 있습니다.


  거리에 대해서 오브젝트들을 정렬하는 방법은 특정 상황에서 잘 동작하는 반면에 회전, 확대 혹은 다른 변환들을 염두에 두지 않고 특이한 모양의 오브젝트들은 간단히 위치 벡터를 사용하는 것만으로는 부족합니다.


  오브젝트를 정렬하는 것은 여러분이 어떠한 유형의 scene을 가지고 있는지에 따라 다르고 꽤 어려운 작업입니다. 추가 처리 파워를 소요하기도 합니다. 단색 오브젝트와 투명한 오브젝트가 공존하는 scene을 완벽히 렌더링하는 것은 쉽지 않습니다. order independent transparency 같은 고급 기술들도 존재하지만 이 강좌의 범위를 벗어납니다. 이제는 정상적으로 오브젝트들을 blending 할 수 있어야 합니다.



출처 : https://learnopengl.com, Joey de Vries

반응형