Post Lists

2018년 7월 29일 일요일

Advanced OpenGL - Blending(3)

https://learnopengl.com/Advanced-OpenGL/Blending

Blending
OpenGL에서 Blending은 또한 흔히 오브젝트 내에서 transparency(투명성)을 구현하는 기술로 알려져있다. 투명성은 오브젝트들이 (또는 그것들의 부분들이) solid color를 가지지 않고, 그 오브젝트 자체에서 색들의 조합을 가지고, 다양한 강도를 가지고 그것뒤에 다른 오브젝트를 갖는 것에 대한 것이다. 색이 칠해진 유리창은 투명한 오브젝트이다; 그 유리는 그것 자신의 컬러를 가지지만, 최종 결과는 또한 그 유리 뒤에 있는 모든 오브젝트들의 색을 포함한다. 이것은 또한 blending이라는 이름이 온 이유이다. 우리가 (다른 오브젝트의) 몇 가지 색들을 하나의 색으로 blend(섞기) 때문이다. 투명성은 따라서 우리가 오브젝트를 관통해서 볼 수 있게 한다.

투명한 오브젝트들은 완전히 투명하거나 (그것은 모든 색들이 관통하게 한다) 또는 부분적으로 투명하다 (그것은 색들이 관통하게 할 뿐만 아니라, 그것 자신의 색들을 보여준다). 한 오브젝트의 투명성의 양은 그것의 컬러의 alpha value로 정의된다. 그 알파 컬러 값은 너가 이제까지 종종  보아온 컬러 벡터의 4번째 component이다. 이 튜토리얼 까지, 우리는 항상 이 4번 째 component를 1.0의 값으로 유지하여, 오브젝트에게 0.0의 투명성을 주었다. 반면에 0.0의 알파값은 완전히 투명한 오브젝트를 만들어 낼 것이다. 0.5의 알파값은 그 오브젝트의 컬러가 그것 자신의 색 50%와 그 오브젝트 뒤에 있는 컬러들의 50%로 구성된다고 말해준다.

우리가 이제까지 사용한 텍스쳐는 모두 3개의 컬러 컴포넌트로 구성되어 있었다: red, green, blue. 그렇지만 몇몇 텍스쳐들은 또한 texel당 alpha value를 포함하는 삽입된 alpha channel을 가진다. 이 알파 값은 우리에게 정확히 그 텍스쳐의 어떤 부분이 투명성을 가져야 하고 얼마나 많이 가져야 하는지를 알려준다. 예를들어, 다음의 window texture는 그것의 유리 부분에서 0.25의 alpha value를 가진다. (그것은 보통 완전히 빨간색이지만, 그것이 75%의 투명도를 가지기 때문에, 그것은 크게 그것을 통해서 웹사이트의 배경을 보여준다. 이것은 그것을 덜 붉은색으로 만든다.) 그리고 0.0의 알파값은 그것의 코너에 있다:

우리는 곧 이 윈도우 텍스쳐를 scene에 더할 것이지만, 처음에 우리는 완전히 투명하거나 또는 완전히 불투명한 텍스쳐에 대해 투명성을 구현하는 좀 더 쉬운 기법에 대해 이야기할 것이다.

Discarding fragments
몇 몇 이미지들은 부분적 투명성을 신경쓰지 않지만, 한 텍스쳐의 컬러 값에 기반으로하여 어떤 것을 보여주고 싶거나 또는 아무것도 보여주고 싶지도 않길 원한다. 잔디를 생각해보아라; 적은 노력으로 잔디같은 어떤 것을 만들기 위해서, 너는 일반적으로 잔디 텍스쳐를 2D quad에 복사하고, 그 quad를 scene에 놓아야 한다. 그러나, 잔디는 정확히 2D 사각형의 모양이 아니다. 그래서 너는 잔디 텍스쳐의 몇몇 부분만을 보여주길 원하고, 다른 것들은 무시하길 원한다.

다음의 텍스쳐는 정확히 그러한 텍스쳐인데, 그것은 완전 불투명하거나(1.0의 알파값) 완전히 투명하거나 (0.0의 알파값) 그리고 그 사이에 있는 것은 없다. 너는 잔디가 없는 곳에서, 그 이미지는 자신의 이미지 대신에 웹사이트의 배경 컬러를 보여주는 것을 알 수 있다.

그래서 잔디같은 초목을 너의 scene에 추가 할 때, 우리는 grass의 square image를 보고 싶어 하지 않는다. 그러나 오히려 그 실제 잔디만을 보여주고 싶다. 그리고 그 이미지의 나머지는 관통하여 보고 싶다. 우리는 그 텍스쳐의 투명한 부분을 보여주는 fragments들을 버리고 싶다. 그 fragment를 color buffer에 저장하지 않으면서. 우리가 거기에 본격적으로 들어가기 전에, 우리는 처음에 어떻게 투명한 텍스쳐를 load하는지 알 필요가 있다.

alpha 값이 있는 텍스쳐를 불러오기 위해서, 바꿀 것은 많이 없다. stb_image는 자동적으로 그 이미지가 가능하다면, 그 이미지의 alpha channel을 불러온다. 그러나 우리는 OpenGL에게 우리의 텍스쳐가 텍스쳐 생성 절차에서 지금 알파 채널을 사용한다고 말할 필요가 있다:

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

또한 너는 fragment shader에서 텍스쳐의 모든 4개의 컬러 컴포넌트를 가여와야 한다. RGB 컴포넌트들이 아닌:


void main()
{
    // FragColor = vec4(vec3(texture(texture1, Texcoords)), 1.0);
    FragColor = texture(texture1, TexCoords);
}

투명한 텍스쳐들을 어떻게 load하는지 알았으니, depth testing tutorial에서 도입된 기본적인 scene에서 grass의 이러한 잎들을 추가하여 그것을 테스트해 볼 시간이다.

우리는 작은 벡터를 만드는데, 거기에서 우리는 잔디의 잎들의 위치를 나타내기 위해 몇 가지 glm::vec3의 변수들을 추가한다.


std::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));

그 잔디 오브젝트들 각각은 잔디 텍스쳐가 부착된 하나의 single quad로서 렌더링된다. 그것은 완벽한 잔디의 3D 표현은 아니지만, 실제로 복잡한 모델들을 로드하는 것보다 좀 더 효율적이다. 몇 가지 트릭과 함께, 같은 위치에 좀 더 회전된 grass quads를 추가하는 것처럼 너는 여전히 좋은 결과들을 얻을 수 있다.

grass texture가 한 quad object에 더해지기 때문에, 우리는 다른 VAO를 다시 만들 필요가 있고, VBO를 채우고, 적절한 vertex attribute pointers들을 설정할 필요가 있다. 그러고나서, 바닥과 두 개의 정육면체들을 그린 후에, 우리는 잔디잎들을 그릴 것이다:


glBindVertexArray(vegetationVAO);
glBindTexture(GL_TEXTURE_2D, grassTexture);
for (unsigned int i = 0; i < vegetation.size(); ++i)
{
 model = glm::mat4(1.f);
 model = glm::translate(model, vegetation[i]);
 shader.setMat4("model", model);
 glDrawArrays(GL_TRIANGLES, 0, 6);
}

너의 프로그램을 돌리는 것은 이것처럼 보일 것이다:



이것은 OpenGL이 기본적으로 alpha 값으로 무엇을 할 지 모르기 때문이다, 뿐만 아니라 그것들을 어떻게 버릴지도. 운좋게도, 쉐이더의 사용 덕분에 이것은 꽤 쉽다. GLSL은 우리에게 discard라는 명령어를 주는데, 이것은 (한 번 호출되면) fragment가 더 이상 처리되지 않고, 컬러 버퍼에 들어가지 않도록 보장해준다. 이 명령어 덕분에, 우리는 fragment shader에서 어떤 threshold아래에서 한 fragment shader가 알파값을 가졌는지를 체크하고, 만약 그렇다면 그것이 처리되지 않은 것처럼 그 fragment를 버릴 수 있다:


#version 330

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의 threshold보다 더 작은 알파값을 가졌는지를 체크하고, 만약 그렇다면, fragment를 버린다. 이 fragment shader는 그것이 오직 (거의) 완전히 투명하지 않은ㅇ fragments만을 렌더링하도록 우리에게 보장해준다. 이제 그것은 이것처럼 보일 것이다:



  Green Box
  텍스쳐를 그것들의 가장자리에서 샘플링 할 때, OpenGL이 그 텍스쳐의 다음의 반복된 값으로 경계값을 보간한다는 것에 유의해라 (우리가 그것의 wrapping parameter를 GL_REPEAT로 설정 했기 때문에). 이것은 보통 괜찮지만, 우리가 transparent values를 사용하고 있기 때문에, 그 텍스쳐 이미지의 꼭대기가 bottom border의 solid color 값으로 보간된 투명 값을 가진다. 그 결과는 너가 볼지도 모르는 너의 텍스쳐 처리 된 quad 주변에 감싸진 반투명의 색칠해진 경계이다. 이것을 방지하기 위해서, 너가 alpha texture를 사용할 때 마다, texture wrapping method를 GL_CLAMP_TO_EDGE로 설정해라:
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE2D_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

너는 여기에서 소스코드를 볼 수 있다.

Blending
fragments를 버리는 것은 훌륭하지만, 그것은 우리에게 반 투명 이미지를 렌더링하는 유연성을 주지 않는다; 우리는 fragment를 렌더링하거나 또는 그것을 버리거나이다. 투명도가 다른 수준을 가진 이미지들을 렌더링하기 위해서, 우리는 blending을 활성화 해야만 한다. 대부분의 OpenGL의 기능처럼, 우리는 GL_BLEND를 활성화하여 blending을 활성화 할 수 있다:

  glEnable(GL_BLEND);

blending을 활성화 했으니, 우리는 OpenGL에게 그것을 어떻게 실제로 blend할 지를 말할 필요가 있다.

OpenGL에서 blending은 다음의 공식으로 처리된다:




  • C_source : 소스 컬러 벡터. 이것은 텍스쳐로부터 나온 컬러 벡터이다.
  • C_destination : 목적지 color vector. color buffer에 현재 저장된 컬러 벡터.
  • F_source : source factor value. 소스 컬러에 알파값의 영향을 설정
  • F_destination : destination factor value. destination color에 알파값 영향 설정.
fragment shader가 작동하고 모든 테스트가 통과된 후에, 이 blend equation은 fragment's의 컬러에서 output을 loose하게 하고, color buffer에 있는 것도 함께 loose하게 한다. (현재 fragment전에 저장된 이전의 fragment color). 그 source와 destination colors는 자동적으로 OpenGL에서 설정되지만, source와 destination factor는 우리가 선택한 값으로 설정되어질 수 있다. 간단한 예로 시작해보자:

우리는 두 개의 사각형을 가지고 있는데, 거기에서, 우리는 빨간색 사각형 위에 반 투명한 초록색 사각형을 그리고자 한다. 그 빨간 사각형은 destination color가 될 것이고 (그래서 따라서 color buffer에서 처음에 들어갈 것이다) 그리고 우리는 이제 그 빨간 사각형 위에 초록 사각형을 그릴 것이다.

그러고나서 의문점이 발생한다: 우니느 그 factor values들을 무엇으로 설정해야 하는가? 음, 우리는 적어도 초록 사각형을 그것의 알파 값으로 곱하길 원한다. 그래서 우리는 F_src를 그 source color vector의 알파값 0.6으로 동일하게 설정한다. 그러고나서 destination square가 알파값의 나머지와 동일한 contribution을 갖게하는 것이 말이 된다. 만약 초록 사각형이 최종 컬러에 60%를 기여한다면, 우리는 빨간색 사각혀이 최종 컬러의 40%를 기여하길 원한다. 예를들어 1.0 - 0.6. 그래서 우리는 F_destination을 1 - source color vector의 알파값으로 정한다. 그 방정식은 따라서 :



그 결과는 결합된 square fragments가 60%는 초록색 40%는 빨간색의 더러운 색을 주는 컬러를 포함한다는 것이다:

그 최종 결과는 그러고나서 컬러 버퍼에 저장되고, 이전의 컬러를 대체한다.

그래서 이것은 훌륭하지만,. 어떻게 우리는 OpenGL에게 이러한 것과 같은 factors들을 사용하라고 말하는가? 음 glBlendFunc라고 불려지는 함수가 있어야 발생한다.

glBlenFunc(GLenum sfactor, GLenum dfactor) 함수는 source와 destination factor에 대한 옵션을 설정하는 두 개의 파라미터들을 기대한다. OpenGL은 꽤 우리가 아래에서 가장 흔한 옵션들을 열거해논 것 중에서 우리가 설정할 몇 가지 옵션들을 정의했다. constant color vector C_constant가 개별적으로 glBlendColor 함수로 설정될 수 있다는 것에 유의해라.


  • GL_ZERO : Factor은 0과 같다.
  • GL_ONE : Factor는 1과 같다.
  • GL_SRC_COLOR : Factor는 source color vector와 같다. (C_source)
  • GL_ONE_MINUS_SRC_COLOR : Factor는 1 - C_source와 같다.
  • GL_DST_COLOR : Factor는 C_destination과 같다.
  • GL_ONE_MINUS_DST_COLOR : Factor는 1 - C_destination과 같다.
  • GL_SRC_ALPHA : Factor는 C_source의 alpha component와 같다.
  • GL_ONE_MINUS_SRC_ALPHA : Factor는 1 -  Alpha value of C_source와 같다.
  • GL_DST_ALPHA : Factor는 C_destination의 Alpha value와 같다.
  • GL_ONE_MINUS_DST_ALPHA : Factor는 1 - Alpha value of C_destination과 같다.
  • GL_CONSTANT_COLOR : Factor는 constant color vector와 같다. (C_constant)
  • GL_ONE_MINUS_CONSTANT_COLOR : Factor는 1 - C_constant와 같다.
  • GL_CONSTANT ALPHA : Factor는 C_constant의 alpha value와 같다.
  • GL_ONE_MINUS_CONSTANT_ALPHA : Factor는 1 - alpha value of C_constant와 같다.
이전에 우리가 두 개의 사각형으로부터 가졌던 블렌딩 결과를 얻기 위해, 우리는 source factor에 대해 source color vector의 alpha를 취하고 싶고, destination factor로 1 - alpha를 취하고 싶다. 이것은 glBlendFunc에 다음과 같이 옮겨진다:

  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

RGB와 alpha channel에 가각 glBlendFuncSeparate를 사용하여 다른 옵션을 설정하는 것이 또한 가능하다:

  glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);

이 함수는 RGB 컴포넌트들을 우리가 이전에 설정한 대로 설정할 뿐만 아니라, 그 최종 알파 컴포넌트가 source의 알파 값에 영향을 받도록 한다.

OpenGL은 우리가 방정식의 source와 destination 부분 사이의 연산자를 바꾸도록 하여, 좀 더 유연성을 준다. 지금 당장, source와 destination components들이 함께 더해지지만, 우리는 또한 원한다면 그것들을 뺄 수 있다. glBlendEquation(GLenum mode)는 우리가 이 연산을 설정하게 해주고, 3가지 가능한 옵션들을 갖는다:

  • GL_FUNC_ADD : 기본, 서로에게 두 컴포넌트들을 더한다 : C_result = Src + Dst.
  • GL_FUNC_SUBTRACT : 서로로부터 두 컴포넌트들을 뺀다 : C_result = Src - Dst.
  • GL_FUNC_REVERSE_SUBTRACT : 두 컴포넌트를 역순으로 뺀다 : C_result = Dst - Src.

보통 우리는 glBlendEquation에 대한 호출을 생략할 수 있다. GL_FUNC_ADD가 대부분의 연산에 대해 선호되는 blending 방정식이기 때문이다. 그러나 만약 너가 정말 주류 circuit을 깨려고 최선을 다한다면, 다른 방정식이 너의 필요에 부합할지도 모른다.

Rendering semi-transparent textures
우리는 OpenGL이 블렌딩과 관련하여 어떻게 작동하는지를 알았으므로, 몇 가지 반 투명 창들을 추가하여 우리의 지식을 테스트할 시간이다. 우리는 이 튜토리얼의 시작에서 처럼, 같은 scene을 사용할 것이지만, 잔디 텍스쳐를 렌더링 하는 대신에, 우리는 이제 이 튜토리얼의 시작에 있는 투명한 창 텍스쳐를 사용할 것이다.

처음에 초기화동안, 우리는 blending과 적절한 blending function을 설정한다:

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

blending을 활성화 했으니, fragments를 버릴 필요가 없다. 그래서 우리는 fragment shader를 원래의 버전으로 되도릴 것이다:


#version 330 core

out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{
 FragColor = texture(texture1, TexCoords);
}

이번에 (OpenGL이 한 fragment를 렌더링 할 때 마다) 그것은 현재의 fragment의 컬러와 그것의 알파값을 기반으로 컬러 버퍼에 현재 있는 fragment color와 결합한다. 윈도우 텍스쳐의 유리 부분은 반투명이기 때문에, 우리는 scene의 나머지를 이 창을 관통하여 볼 수 있을 것이다.


그러나 만약 너가 더 가까이에서 본다면, 너는 무언가가 꺼지는 것을 눈치챌지도 모른다. 앞 window의 투명한 부분들은 배경에 있는 유리들을 가린다. 왜 이것이 발생하는가?

이것에 대한 이유는, depth testing이 blending과 결합되어 까다롭게 작동하기 때문이다. depth buffer에 write할 때, depth test는 fragment가 투명한지 아닌지를 신경쓰지 않는다. 그래서 투명한 부분들은 depth buffer에 어떤 다른 갑승로 쓰여진다. 그 결과는 window의 전체 quad가 투명도에 상관없이 depth testing을 위해 체크된다. 투명한 부분이 그것의 뒤에 있는 창들을 보여줘야만 할지라도 그 depth test는 그것들을 버린다.

그래서 우리는 우리가 depth buffer가 모든 문제를 우리를 위해 해결해달라고 원하고 기대할지라도 창들을 간단히 렌더링할 수 없다; 이것은 또한 blending이 끔찍해 지는 부분이다. 윈도우들이 그것들 뒤에 있는 윈도우들을 보여주도록 하기 위해, 우리는 그 윈도우를 배경에 먼저 그려야 한다. 이것은 우리가 그 윈도우들을 먼곳에서 가까운 곳까지 자동으로 정렬해야만 하고 그것들을 그에 따라서 그려야 하는 것을 의미한다.

  Green Box
  잔디 잎들 처럼 완전히 투명한 오브젝트들과 함께, 우리는 그것들을 blending 하는 대신에 투명한 fragments들을 간단히 버리는 옵션을 가지고 있고, 이것은 이러한 문제들을 해결해준다는 것에 유의해라. (어떠한 depth 문제가 없다.)

Don't break the order
다양한 오브젝트들에게 blending이 작동하도록 하기 위해서, 우리는 가장 먼 오브젝트를 처음에 그려야하고, 마지막으로 가장 가까운 오브젝트를 그려야만 한다. 보통의 non-blended 오브젝트들은 depth buffer를 사용하여 정상처럼 여전히 그려질 수 있다. 그래서 그것들은 정렬될 필요가 없다. 우리는 (정렬된) 투명한 오브젝트들을 그리기 전에 그러한 것들을 처음에 그리도록 해야만 한다. non-transparent와 transparent 오브젝트들을 함께 scene에서 그릴 때, 일반적인 outline은 보통 다음과 같다:


  1. 모든 불투명한 오브젝트들을 처음에 그려라.
  2. 모든 투명한 오브젝트들을 정렬해라.
  3. 모든 투명한 오브젝트들을 정렬된 순서로 그려라.
투명한 오브젝트들을 정렬하는 한 방법은 viewer의 관점으로부터 한 오브젝트의 거리를 얻어내는 것이다. 이것은 카메라의 포지션 벡터와 오브젝트의 포지션 벡터 사이의 거리를 취하여 얻어질 수 있다. 우리는 그러고나서 STL 라이브러리의 map 자료구조에서 그에 상응하는 position vector와 함께 이 거리를 저장한다. map은 자동적으로 키를 기반으로 그것의 값들을 정렬한다. 그래서 우리가 모든 위치를 거리와 함께 키로서 추가 했다면, 그것들은 자동적으로 그것들의 거리 값으로 정렬된다:


std::map<float, glm::vec3> sorted;
for(unsigned int i = 0; i < windows.size(); ++i)
{
   float distance = glm::length2(camera.Position - windows[i];
   sorted[distance] = windows[i];
}

그 결과는 가장 낮은 거리에서 가장 먼 거리의 distance key value를 기반으로 창의 포지션들 각각을 저장하는 정렬된 컨테이너 오브젝트이다.

그러고나서, 렌더링 할 때, 우리는 역순으로 (가장 먼 것에서 가장 가까운 것으로) 맵의 값을 취한다. 그러고나서 정확한 순서로 그에 상응하는 창들을 그린다:


for(std::map<float, glm::vec3>::reverse_iterator it = sorted.begin(); it != sorted.rend(); ++it)
{
   model = glm::mat4(1.0);
   model = glm::translate(model, it->second);
   shader.setMat4("model", model);
   glDrawArrays(GL_TRIANGLES, 0, 6);
}

우리는 MAP으로부터 아이템들 각각을 역순으로 반복하기 위해 reverse iterator를 취하고, 그러고나서 각 window quad를 상응하는 window position으로 그린다. 투명한 오브젝트들을 정렬하는 것에 대해 이 상대적으로 간단한 접근법은 이전의 문제를 고치고 이제 그 scene은 이렇게 보인다:

너는 여기에서 정렬이 들어있는 완전한 소스 코드를 찾을 수 있다.

그 오브젝트들을 거리에 따라 정렬하는 이 접근법이 이 특정한 시나리오에는 잘 작용하는 반면에, 그것은 회전과 scaling 또는 다른 변형을 고려하지 않고, 이상하게 생긴 오브젝트들은 간단히 position vector보다는 다른 방법이 필요하다.

너의 scene에서 오브젝트들을 정렬하는 것은 너가 가진 scene의 타입에 크게 의존하는 어려운 특징이다. 그것을 사용하는 추가 프로세싱 파워도 물론이고. 한 scene을 solid와 transparent object와 함께 렌더링하는 것은 그렇게 쉽지 않다. order independent transparency와 같은 좀 더 고급 기법이 있지만, 이러한 것들은 이 튜토리얼의 범위를 벗어난다. 이제, 너는 일반적으로 너의 오브젝트들을 블렌딩하여 살아갈 것이지만, 만약 너가 신세심하고 그 한계를 안다면, 너는 여전히 꽤 멋진 블렌딩 구현을 할 수 있다.

===============================================
OIT Articles
https://software.intel.com/en-us/articles/oit-approximation-with-pixel-synchronization-update-2014

내 게임에서는 Solid objects와 Transparent Objects를 구분하고, 그것에 따라서
Transparent objects를 정렬하여 거리가 먼 것을 렌더링 하여 blending하여 transparency를 명확하게 할 필요까진 없을 것 같다.

나중 나중에, 좀 더 visual effect를 추가하고 싶다면, OIT 자료로 이 기능을 구현하는게 좋을 듯 하다.

댓글 없음:

댓글 쓰기