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

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

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

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

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

 

[opengl].[#2.GLSL] 08. 잔소리...Toon shading

이전 시간에 Vertex Shader에 외부에서 조정되는 값을 넣어, Animation 효과를 내어 보았습니다. https://learn-and-give.tistory.com/34 [opengl].[#2.GLSL] 07. Vertex Shader에서 간단한 애니메이션 구현 앞에서 Vertex Shader

learn-and-give.tistory.com

Shader 없이 Toon shading을 한번 구현 해 보겠습니다.

미리 말해두면 좀 김이 빠지는데, 목적은 Shader 안쓰면 얼마나 고생스럽고, 고생 대비 효과가 좋지 않은지, Shader가 얼마나 좋은 것인지 체검 해 보는 것이 목적입니다. 그러니, 중간에 '이걸 왜 해...?' 싶을 수 있죠.ㅋㅋ

 

 

실험용 구

주전자는 Vertex가 많아서 좋은데, Vertex 개수가 적어지면 어떻게 되는지 비교 해 볼 수가 없었습니다. 그래서, 파라미터를 직접 설정하여 구를 그릴 수 있도록 함수를 하나 만들었습니다. 원리는 간단합니다. 위경도를 반복문으로 하여 Vertex 위치를 삼각함수로 계산하고, 각 Vertex의 법선은, 그 Vertex의 좌표, 정확히 말하면 그 좌표를 정규화 한 값이 됩니다.

 

void DrawSphere(float rad, int numLatitude, int numLongitude)
{	//WONIL : CAD&Graphics_01_ex.02 Sphere

	//최소한 육면체 이상은 되어야 하므로 위도는 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));


			glBegin(GL_TRIANGLES);
				glNormal3fv(p1);
				glVertex3fv(p1);
				glNormal3fv(p3);
				glVertex3fv(p3);
				glNormal3fv(p2);
				glVertex3fv(p2);
			glEnd();

			glBegin(GL_TRIANGLES);
				glNormal3fv(p2);
				glVertex3fv(p2);
				glNormal3fv(p3);
				glVertex3fv(p3);
				glNormal3fv(p4);
				glVertex3fv(p4);
			glEnd();

		}


	}

}

 

조명 설정 함수 분리

 

조명을 설정하면 그 결과는 색/재질로 표현되기 때문에 조명과 재질은 함께 고려해야 합니다. 조명과 재질 사이의 관계를 깊이 있게 보려는 것은 아니므로, COLOR MATERIAL이라는 옵션으로, 색을 지정하면 그것이 재질의 특성으로 반영되도록 하고, 조명의 위치에 의해서만 특정이 달라지는 방향광을 구현하겠습니다. 이런 처리를 쉽게 하기 위해 조명 설정을 별도의 함수로 분리하여 OpenGL 초기화 할 때 처리하겠습니다. Flat shading이 좀 더 날 것의 느낌을 잘 보여줄 것 같네요.

void LightInit()
{
	// Specify Black as the clear color
	glClearColor(0.0f, .0f, .0f, 0.0f);
	// Specify the back of the buffer as clear depth
	glClearDepth(1.0f);
	// Enable Depth Testing
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_LIGHTING);
	glEnable(GL_COLOR_MATERIAL);
	glFrontFace(GL_CCW);
	glShadeModel(GL_FLAT);

	float lpos[4] = { 1.f, 1.f, 1.f, 0.f };	
	glLightfv(GL_LIGHT0, GL_POSITION, lpos);

	float amb[4] = { 0.1f, 0.1f, 0.1f, 1.f };
	float dif[4] = { 0.8f, 0.8f, 0.8f, 1.f };
	float spc[4] = { 1.f, 1.f, 1.f, 1.f };

	glLightfv(GL_LIGHT0, GL_AMBIENT, amb);
	glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);

	glEnable(GL_LIGHT0);
}

 

이제, 구를 그리는 코드까지 적용하면~

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glColor3f(0.5f, 0.5f, 1.f);
	DrawSphere(0.8f, 10, 10);
	glFlush();
	glutSwapBuffers();
}

 

 

 

자, 이 상태에서, Toon Shading은 어떻게 구현 할 수 있을까요?

조명에 의한 색상을 계산 할 때는 조명의 위치와 Vertex의 Normal 정보를 이용하여 지정 된 색상에 대한 밝기가 결정 됩니다. 색상은 구 전체에 동일하게 적용되어 있고, 조명의 위치도 동일하니, 변경 가능한 것은 Normal vector 뿐입니다.

 

 실제로는 법선 벡터가 각 Vertex의 위치에서 다 다르지만, Toon shading과 같이 명암의 구분이 단계적으로 되게 하려면 Normal 벡터를 그룹을 만들어 주는 것으로 해 볼 수 있을 것 같습니다. Vertex의 Y좌표(위도)에 따라서 구역을 정하고, 지정 된 구역 내의 Normal의 Y성분을 구간별로 같은 값을 할당해주면 될 것 같습니다.

 

y성분을 조정하기 때문에 효과가 잘 나타나도록 조명의 위치를 위쪽으로 수정하겠습니다. 법선의 계산은 아래 코드와 같이 0.6과 0.2를 기준으로 하겠습니다. 원래 법선 벡터는 정규화 해줘야 하지만 대략의 느낌을 보기 위해서 그냥 y값을 조정한 상태 그대로 두겠습니다. 법선 벡터는 Vertex 좌표를 그대로 복사한 후, y좌표의 범위를 확인하여 수정해줍니다.

			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;

 

이렇게 해서 실행 해 보면??

 뭐 좀 대충 느낌은 오는데 톱니 모양이 눈에 거슬리네요. 하지만 이것은 Vertex 기준으로 하기 때문에 어쩔 수 없는 한계입니다.(그래서, Shader로 가야하는 것이죠) Vertex의 밀도를 좀 높혀볼까요? 위 그림은 위/경도 방향을 각각 10개로 나눴는데 50개씩 넣어보면 아래와 같습니다. 많이 완화 되긴 했지만, 톱니 모양도 그대로 보이네요. 톱니 모양을 없애기 위해서는 Edge 방향이 조명이 나눠지는 방향에 딱 맞게 배열되어 있어야 할텐데, 움직이는 형상에서 Edge를 조명에 따라 조정하는것은(형상을 유지하면서) 불가능하죠

 

그래서, Shader가 필요하게 됩니다. 앞에서 Shader는 각 Vertex에 할당 된 attribute의 보간 되어 각 Fragment로 전달 된다고 하였습니다. Vertex의 Normal 정보를 attribute로 설정하여, Fragment 단위로 그려지면 고급스러운 랜더링이 되는데(퐁쉐이딩) 이 때, 보간 된 Normal vector를 위의 수식처럼 조정해주면 Fragment 단위로 이런 처리가 되게 되죠.

 

 Fixed Rendering Pipeline에서 구현하기 어려운 것을 보여주는 한 예로써 Toon shading을 소개했는데, 사실 이것 뿐만 아니라 매우 다양한 표현이 가능하죠. 다양한 표현을 하려면 어떤 것이 어려운지, 해결 하고자 하는 문제가 무엇인지 알아보는 것이 가치를 확인하는 좋은 방법이라, 좀 주변 이야기를 많이 한 것 같네요.

 

 다음 시간부터 GLSL을 이용한 조명 효과 구현을 진행 해 보도록 하겠습니다.

728x90
반응형

댓글