본문 바로가기
공허의 유산/표현의 자유

[opengl].[#2.GLSL] 11. Fragment Shader를 이용한 조명의 구현(Diffuse/Toon shading)

by 바른생활머시마 2023. 2. 4.
728x90
반응형

앞에서 Vertex shader를 이용하여 Diffuse 조명을 구현 해 보았습니다.

https://learn-and-give.tistory.com/58

 

[opengl].[#2.GLSL] 10. Vertex Shader를 이용한 조명의 구현(Diffuse)

앞에서 Shader 없이, Vertex의 밀도를 높히는 방법으로 Toon shading을 구현하려고 별 짓을 다 해 보았습니다. https://learn-and-give.tistory.com/39 [opengl].[#2.GLSL] 09. Shader 없이 구현하는 Toon shading 앞에서 Toon shad

learn-and-give.tistory.com

Diffuse 조명의 구현은 그럴싸 하게 되었지만, Toon shading은 여전히 만족스럽지 못했습니다. 이 때 계산을 Vertex 단위로 하고, 그 결과를 보간하는 방식이었기 때문에 경계 부분이 아주 거칠게 표현되었는데, 이를 극복하기 위해 계산 위치를 Fragment Shader로 옮겨 보도록 하겠습니다.

 

 

Toon shading을 위한 Vertex shader 수정

Vertex shader가 할 일은 각 Vertex에 할당 된 Normal값ㅇ르 이용하여 각 Vertex의 밝기를 계산하여 그 값을 Fragment shader로 전달 해 주는 역할만 합니다. 여기서 처음으로 Vertex shader에서 Fragment shader로 값을 넘기는 방법을 사용하게 되는데, 이 때 변수형 앞에 varying 이라는 예약어를 사용합니다. 

 

 정확한 근거를 가지고 설명하는 것은 아닌데, 저는 이렇게 이해했어요.

 uniform은 vertex shader나 fragment shader에도 동일한, uniform한 값을 가지는 변수에 사용되고,

 varying은 vertex shader에서 값을 할당하면 interpolation에 의해 여러가지(vary) 값으로 조정 된 값이 전달되는 변수에 쓰는 것으로 정리를 해 보았습니다.

 

Vertex shader code

uniform vec3 lightDir;
varying float intensity;
void main()
{
         intensity = dot(lightDir,gl_Normal);
         gl_Position = ftransform();
}

 

이렇게 전달되면, intensity가 vertex별로 계산 된 후, 보간된 상태로 각 Fragment(pixel)에 전달됩니다. Vertex shader에서 Toon shading을 위한 단계를 계산 할 경우에는 한 삼각형 내에서는 서로 다른 단계가 명확하게 표현 될 수 없었습니다. 모두 같은 단계이거나 서로 다른 단계가 보간되거나.... 이것을 fragment shader로 옮기면 삼각형 내에서도 보간 된 결과에 따라 단계가 나눠질 수 있기 때문에 매끈한 경계 생성이 가능하게 됩니다. 

 

 

Fragment shader code

코드는 아래와 같습니다.

varying float intensity;
void main()
{
         vec4 color;
         if (intensity > 0.95)        color = vec4(0.5,0.5,1.0,1.0);
         else if (intensity > 0.5)    color = vec4(0.3,0.3,0.6,1.0);
         else if (intensity > 0.25)   color = vec4(0.2,0.2,0.4,1.0);
         else                        color = vec4(0.1,0.1,0.2,1.0);        
         gl_FragColor = color;
}

결과는~~~~~

위경도 방향 10개의 vertex로 그려진 sphere

오우~ 이제 뭔가 Toon shading같은 맛이 나네요.

 

하지만, 여전히 Vertex의 밀도에 영향을 받는 점이 보이네요. Vertex를 좀 더 추가해 보면 추가 한 보람이 있을만큼 눈에 띄게 품질이 좋아집니다.

위경도 방향 40개의 vertex로 그려진 sphere

 

 

 

Normal과 Light, 온순하지만 그리 호락호락 한 놈은 아니야...

 

위에서 구현한 Toon shading에 보완 할 점이 좀 있습니다. 

 

 

Intenisity 보간 v.s. 법선 보간

 

Vertex shader에서 계산 된 intensity를 fragment shader에서 보간하여 toon shading을 그렸는데, 이렇게 되면, GLSL 리뷰 거의 앞 부분에서 이야기 했던 법선 방향의 보간을 일어나지 않아서 vertex 밀도에 따른 한계가 발생하게 됩니다. 따라서, intensity가 아니라 normal 자체를 보간의 대상으로 해야 합니다.  예를 들어, 매끈한 식탁 중앙 위에 아래를 비추는 조명이 있을 경우, 각 꼭지점은 조명과의 상대적 위치에 따라 결정되는 각도로 입사광이 계산되고 그 결과로 Diffuse 조명의 밝기가 결정됩니다. 이때 중앙에는 Vertex가 없으면 조명에 의해 가장 밝게 보여질 색상은 계산 될 기회가 없게 됩니다.Fragment shader를 쓰더라도 색상이나 intensity를 보간하면 그렇게 되죠.

 

중앙의 밝은 곳이 다른 곳과 다른 점은, 조명과의 상대적인 위치가 다르고, 그래서 조명이 계산 될 각도가 다른 것입니다. 즉, Fragment별로 계산 될 밝기나 색상에 필요한 것은 계산되어 보간 된 밝기나 색상이 아니라, 밝기가 색상 계산에 필요한 정확한(잘 근사화 된) 방향 정보, 즉 법선값입니다.  이에 대한 효과는 표면의 난반사를 고려한 Diffuse(확산광) 조명에서는 잘 드러나지 않으니, 나중에 Specular 조명에서 확인해 보기로 하겠습니다. 우선, 적어도 보간의 대상은 법선 정보가 되도록 처리를해줘야 한다는 것은 이해를 하고 이를 코드에 반영 해 보겠습니다.

 

 조명 계산을 Fragment shader에서 하기 위해서는 Fragment shader로 조명의 위치도 넘겨야 하니, 조명의 위치도 varying이 되어야 하겠습니다. 그리고, 조명의 위치는 uniform이 아니라 예약 된 키워드로도 받는 방법이 있는데(gl_LightSource[0]) Fixed rendering pipeline 기반의 코드로부터 변환 할 때 활용하면 편리 할 것 같습니다. 여기서는 uniform으로 전달하던 방법 그대로 사용 해 보겠습니다. 

 

 또한, 법선으로서의 Normal vector는 단위 벡터인데, 벡터 연산을 하다보면 그 크기가 1이 되지 않을 수 있습니다. 이런 경우, 법선은 단위 벡터로 만들어줘야 하는데, GLSL에서는 normalize라는 함수를 제공하여 이를 간단히 처리 할 수 있습니다.

 

이런 내용을 반영한 Vertex shader 코드는 아래와 같습니다.

varying float normal;
void main()
{
         normal = gl_Normal;
         gl_Position = ftransform();
}

Fragment shader 코드는 아래와 같습니다.

uniform vec3 lightDir;
varying vec3 normal;

void main()
{
	vec4 color;
	float intensity;
	intensity = max(dot(lightDir,normalize(normal)),0.0);

     if (intensity > 0.95)        color = vec4(0.5,0.5,1.0,1.0);
     else if (intensity > 0.5)    color = vec4(0.3,0.3,0.6,1.0);
     else if (intensity > 0.25)   color = vec4(0.2,0.2,0.4,1.0);
     else                        color = vec4(0.1,0.1,0.2,1.0);        
     gl_FragColor = color;
}

 

결과는~

일단, 이 Toon shading 예제에서는 크게 달라지는 점이 없습니다.

 

 

Normal  벡터를 연산한 후 정규화 해야 되는 이유를 그림으로 보면 이해가 쉽습니다.

아래 그림은 양 끝의 두 벡터를 보간한 것을 나타내는 그림입니다.

보시다시피, 녹색 벡터가 단위 벡터인 경우, 그 사이 벡터들은 크기가 1보다 작게 됩니다. 그러므로, 법선 벡터의 크기가 단위 벡터이어야 하므로 1로 정규화 해줘야 됩니다. 

 그런데, 이론상은 보간 된 값을 다시 정규화 해줘야 하지만, 내부적으로 그런 것을 처리 해 주는 로직이 있는지 정규화 해주지 않아도 뭐 별로 크게 차이가 나지 않는 것 같아요. 법선의 용도가 방향을 나타내기 위한 용도이므로, 크기를 다르게 입력해도 내부적으로 자동으로 법선을 정규화 해주는 것이 맞는 것 같기는 한데, 또 성능을 고려하면 이미 정규화 다 된 것을 일부러 한번 더 검사하고 정규화 해주는 과정을 거치는 것도 좋지 않은 것 같네요. HW나 쉐이더가 어떻게 처리하는지는 정확히 모르겠지만 이해하고 있다면 거기 맞게 조치 할 수 있으니 이해를 하는 것으로 정리하고 넘어가도록 하겠습니다.

 

 

매끈한 탁자로 확인 해 보는 법선 보간의 가치

 법선 보간이나 쉐이더의 능력을 설명하면서 몇 차례 언급한 매끈한 테이블의 실제 테스트 사례를 잠시 살펴 보겠습니다.

위에서도 잠깐 언급한 색상 보간의 경우부터 그려보겠습니다. 아래와 같이 기대를 하였으나, 색상 보간을 쓰게 되면...

결과는 이렇게 나옵니다.

 

그런데, 법선을 보간하여 Fragment에서 조명을 계산하면 아래와 같이, 기대한 그림이 그려집니다. OpenGL로 그려진 결과는 왼쪽 그림인데, 그 효과를 눈에 잘 띄도록 contrast를 조정 해 보면 오른쪽처럼, 기대했던 그림이 그려지는 것을 볼 수 있습니다.

이제 조명의 구현에  필요한 여러가지 요소들에 대해서 살펴보았고, 대표적인 조명 모델들을 Shader로 구현하는 것을 이어서 살펴보겠습니다.

https://learn-and-give.tistory.com/60

 

[opengl].[#2.GLSL] 12. 조명 정보 구조체와 재질 정보 구조체

앞에서 쉐이더를 이용하여 멋지게 Toon shading을 구현 해 보았습니다. https://learn-and-give.tistory.com/59 [opengl].[#2.GLSL] 11. Fragment Shader를 이용한 조명의 구현(Diffuse/Toon shading) 앞에서 Vertex shader를 이용하

learn-and-give.tistory.com

 

728x90
반응형

댓글