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

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

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

앞에서 Shader 없이, Vertex의 밀도를 높히는 방법으로 Toon shading을 구현하려고 별 짓을 다 해 보았습니다. 

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

 

[opengl].[#2.GLSL] 09. Shader 없이 구현하는 Toon shading

앞에서 Toon shading과 NPR에 대해서 조금 이야기했습니다. 요즘은 한계라는 것이 없는 것 같아요. 상상하는 모든 것은 다 표현 가능한 것 같아요. 그러니, 무엇을 상상 할 수 있느냐가 경쟁력인 것

learn-and-give.tistory.com

결과는 전혀 이쁘지 않았고, 이런 문제를 Shader가 얼마나 멋지게 해결 해 주는지 본다면, Shader의 효용성을 구질구질한 설명 없이도 체감 할 수 있습니다.

 

 

 

잘난 쉐이더의 조명 효과

 

쉐이더가 잘났다고 해도, 사실 기본 OpenGL 기능이 없으면 무용지물입니다. 이게 대립되는 개념은 아니기 때문에 뭐가 잘났다라는 이야기도 사실 적절하지 않죠. OpenGL의 pipeline이 있고, 개발자가 관여 할 수 있는 부분이 넓어진 것이죠. 그런데, 가장 처음 GLSL로 조명을 구현하려고 할 때, "이럴꺼면 왜 써야되나??"라는 생각이 드는 경우가 있습니다. 

 

아래 그림을 볼께요.

왼쪽은 Fixed pipeline으로 그린 sphere이고, 오른쪽은 GLSL을 썼지만 조명에 대한 구현을 하지 않은 상태입니다. 이쯤에서 요런 원망이 들죠.

 "내가 구현 안해주면 그래도 기본은 해줘야 되는거 아냐????"

 

사실 맞는 말인 것 같아요. GLSL을 쓰더라도 아주 간단하게 fixed pipeline과 동일한 결과가 나오는 방법이 제공되는 것도 배우는 입장에서는 도움이 될 것 같습니다. 그러나, 그런 것이 없기 때문에 더 열심히 공부하게 되기도 하죠.ㅋ

 

위의 그림에서 왼쪽 그림을 보면, 하이라이트 부분에서 조금 부자연스러운 패턴, 즉 메쉬 형상의 흔적이 슬쩍 보입니다.

이것은 Vertex에서 계산 된 색상을 보간하는 방식 때문에 발생하는 것으로, Vertex가 충분히 많이 있고, 적절하게 색상이 할당되지 않는다면 어색함이 생기게 됩니다.  실제로는 동그란 모양이거나 실제 조명의 모양이 비춰질텐데, fixed pipeline 방식으로는 이렇게 표현되는 한계가 있죠.

 

 

 

Diffuse 조명 구현

 

 조명에 대한 이론적 설명은 여기 저기 많은 자료를 보면 되고, 입사각/반사각 정도만 이해하면 되는 Diffuse 조명을 구현해 보도록 하겠습니다. 한글로는 '확산광'이라고 표현하나 봅니다. 빛의 밝기는 광량에 따라 결정되니, 조명의 입사 방향과 면 사이의 각도, 조금 더 자세히 말하면 그 Cosine 값이 비례해서 밝기가 결정 됩니다. 앞에서 억지로 구현한 Toon shading의 경우, cosine 대신 일종의 step function 을 사용한 셈입니다.

 

 

조명 위치의 전달

조명의 위치가 항상 고정되어 있다면, 그냥 고정값을 쉐이더 코드 내에 넣어두면 되지만, 일반적으로 조명의 위치는 저장하는 변수가 있고, 이를 이용하여 랜더링을 하며, 조명의 위치를 런타임에 변경하기도 합니다. 이를 위해 조명의 위치를 저장하는 변수를 하나 만들어야 하는데, 이 조명의 위치를 저장하는 변수는 쉐이더 밖에 두고, 그 값을 쉐이더로 전달하여 조명 계산이 되도록 하겠습니다. 

 

  여기서~ GLSL 코드 밖에서 GLSL 내부로 '조명의 위치'라는 값을 전달 할 방법이 필요한데, 앞에서 애니메이션 구현을 하면서 값을 전달하는 방법을 이미 한번 해 보았습니다.

 

 먼저, 쉐이더 내부에서 조명의 위치를 받아 줄 변수를 선언하는데, 외부에서 값을 전달 받을 수 있도록, vertex shader에 uniform 변수로 선언을 해야 합니다. 

uniform vec3 lightDir;

 

 

 이제, 이 변수를 본 app에서의 조명 위치와 연동하도록 해보겠습니다.

. 먼저, 조명 위치를 나타내는 변수를 함수 외부에 선언합니다. app의 조명 기능은 사용하지 않을 것이기 때문에 밖에서는 조명에 대한 별다른 설정은 하지 않아도 됩니다.

float light_pos[4] = {   1.f, 1.f, 1.f, 0.f };       //Directional light

 

앞에서 해 본 내용인데, 다시 상기하면서 살펴 봅시다. Shader 설정에서 쉐이더의 uniform 변수의 핸들을 얻어 옵니다. 애니메이션과 달리, 이 예에서 조명의 위치를 다시 바꿀 일은 없으니, 값도 바로 전달하도록 합니다.

loc = glGetUniformLocationARB(program_shader,"lightDir");
glUniform3fARB(loc, light_pos[0],light_pos[1],light_pos[2]);

 

 이제 조명의 위치는 정해졌으니, 조명의 방향과 각 vertex에서 법선을 이용하여 조명이 적용 된 색상을 계산해 보겠습니다.

  vertex에서의 법선은 gl_Normal이라는 예약 된 이름의 변수로 넘어 옵니다. 물론, 이 값은 OpenGL이나 Shader가 스스로 계산 해 주는 것이 아니라, app에서 넘겨주는 값이죠. 앞에서 본 예에서, gl_FrontColor를 본 적이 있죠? 이처럼 어떤 값들은 별다른 처리를 하지 않아도 예약 된 변수 이름으로 GLSL로 넘어오게 됩니다. 우리는 계산 된 밝기값에 따라 Toon shading 효과나 나도록 step function을 구현합니다 .이 때, 밝기는 조명의 방향과 법선의 cosine값으로 결정되며, 이것을 쉽게 구할 수 있도록 내적을 구해주는 함수 dot을 활용합니다.

 

음... cosine값에 내적을 쓰는 이유는....... 혹시 아시는지 모르겠네요~~ ^^

 

내적 = |a| |b| cos(theta)

 

그런데, 두 벡터가 단위 벡터이면 크기가 1이므로 내적은 cosine과 같은 값을 가지므로, 우리가 조명의 밝기에 cosine값을 쓰기로 했으니, 내적을 구해서 쓰면 되는거죠.

 

먼저 밝기를 계산하고~

float intensity;
intensity = dot(lightDir,gl_Normal);

이제 Toon shading을 적용하기 위해 intensity 값에 따라 색상을 지정합니다.(이 부분은 임의로 색상을 지정한 것입니다.)

         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_FrontColor  = color;

위치는 그대로 두도록 하고, Fragment shader는 전달 된 색상이 그대로 출력 되도록 합니다. 즉, 이번에 테스트 해 보는 조명은 Vertex shader만 이용해서 구현 해 보는 것입니다.

.

결과는 아래와 같습니다. 너무 볼 품 없는 것 아닌지..... 명색이 조명인데

 

그런데, 이것으로 조명의 효과를 평가하는 것은 좀 곤란합니다. 이것은 조명의 효과가 일부(Diffuse)만 적용되었을 뿐만 아니라 Toon shading을 위해 일부러 퀄러티를 떨어뜨린 이미지이기 때문이죠. 만약, Toon shading을 적용하지 않는다면 훨씬 그 결과가 좋겠죠??  Toon shading을 위해 추가한 step function 부분을 제거하고 cosine값으로 계산 된 intensity만 조명에 그대로 써보겠습니다.

 

uniform vec3 lightDir;
void main()
{
         float intensity;
         intensity = dot(lightDir,gl_Normal);
         vec4 color;
 
         color[0] = intensity/2.f;   
         color[1] = intensity/2.f;   
         color[2] = intensity;       
         color[3] = 1.f;
                 
         gl_FrontColor  = color;
        
         gl_Position = ftransform();
}

결과는!!!! 에??? 같은 위도상에서의 색상 변화는 뭐 좀 그려려니 하겠는데, 같은 경도에서의 불연속적인 저것은 무엇일까요?? 뭔가 잘못 된 것 같은데.....

 

 

구를 그리는 코드에서, Toon shading을 억지로(!) 구현하기 위해서 Normal값을 조정해주는 코드를 제거하는 것을 빼먹었네요. 아래와 같이 법선을 조정해주는 코드가 반영 된 상태였습니다.

oid DrawSphere(float rad, int numLatitude, int numLongitude)
{	

	//최소한 육면체 이상은 되어야 하므로 위도는 2개, 경도는 3개를 최소로 한다.
	if (numLatitude < 2)	numLatitude = 2;
	if (numLongitude < 3)	numLongitude = 3;

	//삼각함수를 쓰기 위해 Degree가 아닌 Radian을 쓰도록 한다.
	float angleStepLati = PI / (float)numLatitude;		//위도는 180도, 즉 PI를 나눈다.
	float angleStepLong = 2.f * PI / (float)numLongitude;	//경도는 360도, 즉 2*PI를 나눈다.


	float prjLen, prjLenNext;
	float p1[3], p2[3], p3[3], p4[3];
	float n1[3], n2[3], n3[3], n4[3];
	//위도
	for (int countLati = 0;countLati < numLatitude;countLati++)
	{
		p1[1] = p2[1] = rad * cos(angleStepLati * (float)countLati);			//기준 Y좌표.(위도)
		p3[1] = p4[1] = rad * cos(angleStepLati * (float)(countLati + 1));		//다음 Y좌표.(위도)

		prjLen = rad * sin(angleStepLati * (float)countLati);		//기준점의 XZ평면에 투영된 길이
		prjLenNext = rad * sin(angleStepLati * (float)(countLati + 1));	//다음점의 XZ평면에 투영된 길이

		for (int countLong = 0;countLong < numLongitude;countLong++)
		{
			p1[0] = prjLen * cos(angleStepLong * (float)countLong);
			p1[2] = prjLen * sin(angleStepLong * (float)countLong);

			p2[0] = prjLen * cos(angleStepLong * (float)(countLong + 1));
			p2[2] = prjLen * sin(angleStepLong * (float)(countLong + 1));

			p3[0] = prjLenNext * cos(angleStepLong * (float)countLong);
			p3[2] = prjLenNext * sin(angleStepLong * (float)countLong);

			p4[0] = prjLenNext * cos(angleStepLong * (float)(countLong + 1));
			p4[2] = prjLenNext * sin(angleStepLong * (float)(countLong + 1));

			memcpy(n1, p1, sizeof(float) * 3);
			memcpy(n2, p2, sizeof(float) * 3);
			memcpy(n3, p3, sizeof(float) * 3);
			memcpy(n4, p4, sizeof(float) * 3);

			if (n1[1] > 0.6f) n1[1] = 1.f;
			else if (n1[1] > 0.2f) n1[1] = 0.5f;
			else if (n1[1] > -0.2f) n1[1] = 0.f;
			else if (n1[1] > -0.6f) n1[1] = -0.5f;
			else n1[1] = -1.f;

			if (n2[1] > 0.6f) n2[1] = 1.f;
			else if (n2[1] > 0.2f) n2[1] = 0.5f;
			else if (n2[1] > -0.2f) n2[1] = 0.f;
			else if (n2[1] > -0.6f) n2[1] = -0.5f;
			else n2[1] = -1.f;

			if (n3[1] > 0.6f) n3[1] = 1.f;
			else if (n3[1] > 0.2f) n3[1] = 0.5f;
			else if (n3[1] > -0.2f) n3[1] = 0.f;
			else if (n3[1] > -0.6f) n3[1] = -0.5f;
			else n3[1] = -1.f;

			if (n4[1] > 0.6f) n4[1] = 1.f;
			else if (n4[1] > 0.2f) n4[1] = 0.5f;
			else if (n4[1] > -0.2f) n4[1] = 0.f;
			else if (n4[1] > -0.6f) n4[1] = -0.5f;
			else n4[1] = -1.f;


			//			glBegin(GL_LINE_LOOP);
			glBegin(GL_TRIANGLES);
			glNormal3fv(n1);
			glVertex3fv(p1);
			glNormal3fv(n3);
			glVertex3fv(p3);
			glNormal3fv(n2);
			glVertex3fv(p2);
			glEnd();

			//			glBegin(GL_LINE_LOOP);
			glBegin(GL_TRIANGLES);
			glNormal3fv(n2);
			glVertex3fv(p2);
			glNormal3fv(n3);
			glVertex3fv(p3);
			glNormal3fv(n4);
			glVertex3fv(p4);
			glEnd();

		}


	}

}

여기서, 법선을 조정 해 주는 코드를 빼면 아래와 같습니다. Vertex의 위치가 법선 벡터인(크기가 1이 아니므로 정규화 되어 있지는 않지만 방향은 맞으니 intensity 계산 결과는 동일하겠죠?)

void DrawSphere(float rad, int numLatitude, int numLongitude)
{	
	//최소한 육면체 이상은 되어야 하므로 위도는 2개, 경도는 3개를 최소로 한다.
	if (numLatitude < 2)	numLatitude = 2;
	if (numLongitude < 3)	numLongitude = 3;

	//삼각함수를 쓰기 위해 Degree가 아닌 Radian을 쓰도록 한다.
	float angleStepLati = PI / (float)numLatitude;		//위도는 180도, 즉 PI를 나눈다.
	float angleStepLong = 2.f * PI / (float)numLongitude;	//경도는 360도, 즉 2*PI를 나눈다.

	float prjLen, prjLenNext;
	float p1[3], p2[3], p3[3], p4[3];
	float n1[3], n2[3], n3[3], n4[3];
	//위도
	for (int countLati = 0;countLati < numLatitude;countLati++)
	{
		p1[1] = p2[1] = rad * cos(angleStepLati * (float)countLati);			//기준 Y좌표.(위도)
		p3[1] = p4[1] = rad * cos(angleStepLati * (float)(countLati + 1));		//다음 Y좌표.(위도)

		prjLen = rad * sin(angleStepLati * (float)countLati);		//기준점의 XZ평면에 투영된 길이
		prjLenNext = rad * sin(angleStepLati * (float)(countLati + 1));	//다음점의 XZ평면에 투영된 길이

		for (int countLong = 0;countLong < numLongitude;countLong++)
		{
			p1[0] = prjLen * cos(angleStepLong * (float)countLong);
			p1[2] = prjLen * sin(angleStepLong * (float)countLong);

			p2[0] = prjLen * cos(angleStepLong * (float)(countLong + 1));
			p2[2] = prjLen * sin(angleStepLong * (float)(countLong + 1));

			p3[0] = prjLenNext * cos(angleStepLong * (float)countLong);
			p3[2] = prjLenNext * sin(angleStepLong * (float)countLong);

			p4[0] = prjLenNext * cos(angleStepLong * (float)(countLong + 1));
			p4[2] = prjLenNext * sin(angleStepLong * (float)(countLong + 1));

			memcpy(n1, p1, sizeof(float) * 3);
			memcpy(n2, p2, sizeof(float) * 3);
			memcpy(n3, p3, sizeof(float) * 3);
			memcpy(n4, p4, sizeof(float) * 3);

			glBegin(GL_TRIANGLES);
			glNormal3fv(n1);
			glVertex3fv(p1);
			glNormal3fv(n3);
			glVertex3fv(p3);
			glNormal3fv(n2);
			glVertex3fv(p2);
			glEnd();

			glBegin(GL_TRIANGLES);
			glNormal3fv(n2);
			glVertex3fv(p2);
			glNormal3fv(n3);
			glVertex3fv(p3);
			glNormal3fv(n4);
			glVertex3fv(p4);
			glEnd();
		}
	}
}

결과는!!!!

우오~~~~~~~~~~~~~~~~~~~~~~~~~~```!!!

Fixed pipeline 정도의 퀄러티는 나오네요. 구의 Vertex 밀도를 좀 낮춰보면 삼각형 모양이 좀 드러납니다.

 code는 github에 올려뒀고, 포스트 제목으로 커밋을 해두었으니 호~~~옥시 필요하신 분들은 가져다 쓰십셔~

https://github.com/red112/glsl

 

GitHub - red112/glsl: OpenGL Shading Language review

OpenGL Shading Language review. Contribute to red112/glsl development by creating an account on GitHub.

github.com

일단 기본적인 Diffuse 조명은 어느 정도 된 것 같으니, 다음 시간에는 다시 Shader를 이용한 Toon rendering을 해보도록 하겠습니다.

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

 

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

앞에서 Vertex shader를 이용하여 Diffuse 조명을 구현 해 보았습니다. https://learn-and-give.tistory.com/58 [opengl].[#2.GLSL] 10. Vertex Shader를 이용한 조명의 구현(Diffuse) 앞에서 Shader 없이, Vertex의 밀도를 높히는

learn-and-give.tistory.com

 

728x90
반응형

댓글