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

[opengl].[#2.GLSL] 13. ambient 조명과 specular 조명

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

앞에서 Diffuse 조명과 재질을 쉐이더로 구현하는 방법을 살펴봤습니다.

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

 

[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

 

Ambient 조명

 

이제 ambient 조명을 살펴보겠습니다.
Ambient 조명은, 구현의 관점에서만 본다면 가장 쉽습니다. 모든 곳에 적용되기 때문에 해당 성분을 다 더해주면 되죠. 조금 의외인 점이라면, 전체 ambient 외에 조명에도 ambient가 있는데, 조명에 의해서 계속 된 반사의 결과로 주변광 같은 효과가 생기는 것을 표현하는 것 같습니다. Diffuse 성분을 약하게 적용하면 될 것 같은데 따로 줘야 하는 이유가 뭔가 있겠죠.  이렇게 두가지 성분의 ambient와 재질의 ambient 특성을 곱해준 후 더해주면 됩니다.

 

 

코드를 보는 것이 좋겠네요

Vertex Shader.

void main()
{
	vec3 normal, lightDir;
	vec4 diffuse, ambient, globalAmbient;
	float NdotL;
	
	normal = normalize(gl_NormalMatrix * gl_Normal);
	lightDir = normalize(vec3(gl_LightSource[0].position));
	NdotL = max(dot(normal, lightDir), 0.0);
	
	diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;

	ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
	globalAmbient = gl_LightModel.ambient * gl_FrontMaterial.ambient;
	
	gl_FrontColor =  NdotL * diffuse + globalAmbient + ambient;
	gl_Position = ftransform();
}

 

ambient의 특성을 파악하기 위해 몇 가지 실험을 좀 해보겠습니다.

먼저, 조명의 ambient/ diffuse는 모두 흰색으로 하고, 재질은 ambient/ diffuse 모두 파란색으로 하겠습니다.

void LightInit()
{
	glClearColor(0.0f, .0f, .0f, 0.0f);
	glClearDepth(1.0f);
	glEnable(GL_DEPTH_TEST);

	float lpos[4] = { 1.f, 1.f, 1.f, 0.f };
	float dif[4] = { 1.f, 1.f, 1.f, 1.f };
	float amb[4] = { 1.f, 1.f, 1.f, 1.f };

	glLightfv(GL_LIGHT0, GL_POSITION, lpos);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);
	glLightfv(GL_LIGHT0, GL_AMBIENT, amb);

	float material_amb_diffuse[4] = { 0.f, 0.f, 1.f, 1.f };
	glMaterialfv(GL_FRONT, GL_DIFFUSE, material_amb_diffuse);
	glMaterialfv(GL_FRONT, GL_AMBIENT, material_amb_diffuse);
}

결과는 아래와 같이 전혀 입체감이 없는 구로 그려집니다.

주변광이 확산광처럼 밝을 수가 없는데, 주변광이나 확산광이 동일하기 때문에 전혀 입체감이 나지 않는 것입니다.

ambient 조명만 0으로 설정해 보면..

void LightInit()
{
	glClearColor(0.0f, .0f, .0f, 0.0f);
	glClearDepth(1.0f);
	glEnable(GL_DEPTH_TEST);

	float lpos[4] = { 1.f, 1.f, 1.f, 0.f };
	float dif[4] = { 1.f, 1.f, 1.f, 1.f };
	float amb[4] = { 0.f, 0.f, 0.f, 1.f };

	glLightfv(GL_LIGHT0, GL_POSITION, lpos);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);
	glLightfv(GL_LIGHT0, GL_AMBIENT, amb);

	float material_amb_diffuse[4] = { 0.f, 0.f, 1.f, 1.f };
	glMaterialfv(GL_FRONT, GL_DIFFUSE, material_amb_diffuse);
	glMaterialfv(GL_FRONT, GL_AMBIENT, material_amb_diffuse);
}

조명의 ambient를 빼니 확산광이 비춰지는 쪽이 밝고 반대쪽은 어두워졌습니다. 그런데, 완전히 검은색은 아닌 것이 이상하네요. 재질의 ambient도 0으로 해보겠습니다.

void LightInit()
{
	glClearColor(0.0f, .0f, .0f, 0.0f);
	glClearDepth(1.0f);
	glEnable(GL_DEPTH_TEST);

	float lpos[4] = { 1.f, 1.f, 1.f, 0.f };
	float dif[4] = { 1.f, 1.f, 1.f, 1.f };
	float amb[4] = { 0.f, 0.f, 0.f, 1.f };

	glLightfv(GL_LIGHT0, GL_POSITION, lpos);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);
	glLightfv(GL_LIGHT0, GL_AMBIENT, amb);

	float material_amb_diffuse[4] = { 0.f, 0.f, 1.f, 1.f };
	glMaterialfv(GL_FRONT, GL_DIFFUSE, material_amb_diffuse);
	glMaterialfv(GL_FRONT, GL_AMBIENT, amb);
}

이렇게 하니 완전히 검은색으로 되는군요. Diffuse는 곱하기라서 한쪽이라도 0이면 다 0이 되지만, ambient는 global ambient라는 항목이 조명과 무관한(무관하다기 보다는 다른 성분을 참조하는...) 항이 있기 때문에 이런 현상이 생기는 것 같습니다..

void main()
{
	vec3 normal, lightDir;
	vec4 diffuse, ambient, globalAmbient;
	float NdotL;
	
	normal = normalize(gl_NormalMatrix * gl_Normal);
	lightDir = normalize(vec3(gl_LightSource[0].position));
	NdotL = max(dot(normal, lightDir), 0.0);
	
	diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;

	ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
	globalAmbient = gl_LightModel.ambient * gl_FrontMaterial.ambient;
	
	gl_FrontColor =  NdotL * diffuse + globalAmbient + ambient;
	gl_Position = ftransform();
}

푸른색 공이 붉으스름한 공간에 흰색 조명을 받으며 있다고 가정해 보겠습니다.

void LightInit()
{
	glClearColor(0.0f, .0f, .0f, 0.0f);
	glClearDepth(1.0f);
	glEnable(GL_DEPTH_TEST);

	float lpos[4] = { 1.f, 1.f, 1.f, 0.f };
	float dif[4] = { 1.f, 1.f, 1.f, 1.f };
	float amb[4] = { 0.5f, 0.f, 0.f, 1.f };

	glLightfv(GL_LIGHT0, GL_POSITION, lpos);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);
	glLightfv(GL_LIGHT0, GL_AMBIENT, amb);

	float material_amb_diffuse[4] = { 0.2f, 0.2f, 1.f, 1.f };
	glMaterialfv(GL_FRONT, GL_DIFFUSE, material_amb_diffuse);
	glMaterialfv(GL_FRONT, GL_AMBIENT, material_amb_diffuse);
}

결과는~

이렇게 다양한 표현을 할 수 있는 장점이 있지만, 그만큼 어렵기 때문에 그냥 Color material을 쓰는 것이 편리하였습니다.

 

 

 

 

 

Specular 조명의 구현

 

Ambient는 Diffuse에 비하면 양이 적어서, Specular도 같이 살펴보겠습니다.  

Specular는 반짝거리는 물체의 가장 밝게 빛나는 부분의 하이라이트를 표현하는 성분인데, 세차를 한 자동차를 살펴본다고 가정 해 보면, 우리가 움직이면서 밝게 빛나는 부분이 달라집니다. 조명이 가만히 있는데도 말이죠. 이를 Specular가 Diffuse 조명과 달리, 시점에 따라 영향을 받는다는 뜻입니다. 아래 그림으로 이 조명을 이해해 봅시다.

시점의 위치가 반사광에서 얼마나 가까운가가 밝기에 영향을 주는 것을 알 수 있습니다. 앞에서 말한 자동차의 경우를 아래 그림을 참고해서 다시 상상 해 봅시다. 둘 다 멋진 차는 맞는데, 오른쪽이 좀 더 광택이 있어 보입니다. 이 "광택이 있다."는 표현이 나타내는 것은 하이라이트인 부분이 매우 밝고 그 경계가 선명한 경우 사용된다고 볼 수 있습니다.

 그림으로 설명한 모델에서, Shininess 속성은 시선이 반사광에서 벗어날 때 어느 정도로 밝기가 감소하는가를 나타냅니다. 만약,  이 Shininess 값이 크면 시선이 반사광에서 벗어날 때 급격이 어두워집니다. 그래서 경계가 또렷해지게 되죠.반면, Shininess 값이 작으면 왼쪽 사진처럼 밝기가 서서히 변하게 됩니다.

 

 이 속성을 설명하는 그림을 하나 보겠습니다.

앞에서 설명을 할 때는, Shininess 가 클수록 더 반짝인다고 했는데 이 그림을 보면 이 값이 가장 작은 가장 왼쪽 주전자가 가장 밝아 보입니다. 이 그림은 잘 보면 삼각형 모양이 좀 드러나는 것이, Shader로 그려진 그림이 아닌 것을 알 수 있습니다. Diffuse 조명 구현 할 때 봤던 그런 번지는 느낌이 보입니다 .즉, 밝기의 표현이 Vertex에 의해서 결정되는데, Vertex 밀도가 높지 않으니, Shininess가 높은 경우, 그 집중 된 부분에 소수의 Vertex만 있어 어둡게 보이게 됩니다. 뒤에서 Shader로 구현한 결과를 보면 훨씬 더 잘 이해 될 것 입니다.

 시선과 반사광 사이의 각도로 Specular를 구하기 때문에, 먼저 반사광 R을 구해야 하는데, 간단한 벡터 식으로 구할 수 있습니다. .

 수식 보다는 그림이 이해하기 훨씬 쉽습니다.

(Dot을 구하는 순서가 바뀌면 부호가 바뀐다.)
 

면의 법선과 입사광/반사광의 관계로부터 유도 되는 식입니다.
R-L = - 2 X N X Dot(L,N)
R = - 2 X N X Dot(L,N)+L

 

이제 R은 구했고, 이를 이용하여 Specular를 구하는데, 이것은 Shader의 앞 부분에서 소개했던 Phong 모델에 표현되어 있습니다.

여기서, 지수 S가 바로 Shininess입니다. 지수로 쓰이기 때문에 급격한 변화를 일으키게 됩니다..

위의 수식은 입사광과 법선으로 반사광을 구하고, 다시 시선 방향을 적용하여 구했는데, 반사광은 결국 입사광에 의해서 결정되기 때문에, 수식을 좀 더 단순하게 만들 수 있습니다.  Blinn이라는 사람이 좀 더 개선 된 모델을 제안했는데, 결국 입력은 조명과 시선이기 때문에 아래와 같이 식을 좀 더 단순화 하였는데, RHalf-vector를 사용하였습니다.

간단히 설명하면, R 대신 훨씬 구하기 쉬운 H를 구했는데, 이것을 사용해도 반사광을 사용한 것과 거의 유사한 결과가 나옵니다. 그래서인지, OpenGL의 조명 구조체에 미리 계산되어 제공되니 안쓰면 안되겠네요. ^^ 
  

이런 내용으로 Vertex Shader 를 다음과 같이 작성합니다.

void main()
{

	vec3 normal, lightDir;
	vec4 diffuse, ambient, globalAmbient,specular;
	float NdotL,NdotHV;
	
	normal = normalize(gl_NormalMatrix * gl_Normal);
	lightDir = normalize(vec3(gl_LightSource[0].position));
	NdotL = max(dot(normal, lightDir), 0.0);
	
	diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;

	ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
	globalAmbient = gl_LightModel.ambient * gl_FrontMaterial.ambient;


	/* compute the specular term if NdotL is  larger than zero */
	if (NdotL > 0.0) {
	
		// normalize the half-vector, and then compute the 
		// cosine (dot product) with the normal
		NdotHV = max(dot(normal, gl_LightSource[0].halfVector.xyz),0.0);
		specular = gl_FrontMaterial.specular * gl_LightSource[0].specular * 
				pow(NdotHV,gl_FrontMaterial.shininess);
	}
	else
		specular = vec4(0.0,0.0,0.0,0.0);
	
	gl_FrontColor =  NdotL * diffuse + globalAmbient + ambient + specular;
	gl_Position = ftransform();
}

 

specular 를 계산하는 식을 보면 Material에 specular, shininess를 사용하고, 조명의 specular를 사용합니다. 이 값을 아래와 같이 설정 해 줍니다.

void LightInit()
{
	glClearColor(0.0f, .0f, .0f, 0.0f);
	glClearDepth(1.0f);
	glEnable(GL_DEPTH_TEST);

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

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

	float material_amb_diffuse[4] = { 0.2f, 0.2f, 1.f, 1.f };
	glMaterialfv(GL_FRONT, GL_DIFFUSE, material_amb_diffuse);
	glMaterialfv(GL_FRONT, GL_AMBIENT, material_amb_diffuse);
	glMaterialfv(GL_FRONT, GL_SPECULAR, spc);
	glMateriali(GL_FRONT, GL_SHININESS, 128);
}

 Specular 효과가 나기는 했지만, 막 놀랍게 멋지고 그러지는 않고, 쉐이더 쓰기 전에 보던 specular 느낌과 크게 다르지 않습니다. 왜냐하면, Vertex shader에서 계산했기 때문이죠. 

 

 Fragment shader의 능력을 보여주마!

드디어, Fragment shader의 진수를 보여줄 때가 왔습니다 Phong shading 모델을 설명하면서 법선을 보간하고, 보간 된 법선으로 Fragment shader에서 계산을 해야 한다고 했었죠? 드디어 그 시간이 왔습니다.

 

 Vertex shader는 Fragment shader가 멋진 마무리를 할 수 있도록, 재료만 잘 전달 해주면 됩니다. VS에서 FS로 값을 전달하려면 varying을 쓰면 되는 것 잊지 않으셨죠?

 

Vertex shader

varying vec4 diffuse,ambient;
varying vec3 normal,lightDir,halfVector;

void main()
{	
	normal = normalize(gl_NormalMatrix * gl_Normal);
	lightDir = normalize(vec3(gl_LightSource[0].position));
	halfVector = normalize(gl_LightSource[0].halfVector.xyz);
				
	diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;
	ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
	ambient += gl_LightModel.ambient * gl_FrontMaterial.ambient;

	gl_Position = ftransform();
}

 

이제 보간 된 값을 받아서 멋진 그림을 그려 낼 Fragment shader입니다.

Fragment shader

varying vec4 diffuse,ambient;
varying vec3 normal,lightDir,halfVector;
void main()
{
	vec3 n,halfV;
	float NdotL,NdotHV;
	
	n = normalize(normal);
	NdotL = max(dot(n,lightDir),0.0);

	vec4 color = ambient;
	if (NdotL > 0.0)
	{
		color += diffuse * NdotL;
		halfV = normalize(halfVector);
		NdotHV = max(dot(n,halfV),0.0);
		color += gl_FrontMaterial.specular * 
				gl_LightSource[0].specular * 
				pow(NdotHV, gl_FrontMaterial.shininess);
	}
	gl_FragColor = color;
}

앞에서 작성한 코드는 나눈 셈이라 코드는 크게 설명 할 것이 없지만, 실제 계산 과정에는 많은 차이가 있죠. 보간 된 값의 사용, 그리고 계산 되는 단위가 pixel이라는 점.

 

결과는~~~~

오우~~~~ 저 영롱한 specular!!!

Shininess 값을 바꾸면 정말 느낌이 달라질까요?? 값을 낮춰보겠습니다. 

128은 매끈한 플라스틱, 혹은 유리 구슬 같고, 아래는 쇠구슬 같은 느낌이네요. 

여기서 너무 놀라면 안됩니다.

 

제가 가장 감동을 받았던 것은 따로 있으니까요.

비교를 위해서 Shininess를 다시 128로 해두고, 구의 vertex 개수를 아주 팍 줄여보겠습니다.

구의 윤곽의 매끈함이 사라진 것 말고는 별 차이가 없죠?? 놀라운 점은 바로 '별 차이가 없다'는 점입니다. Vertex 개수를 엄청 줄였음에도 불구하고 말이죠. 원래는 위/경도를 각각 40등분해서 구를 그렸습니다. 대충 1600개 정도였겠네요. 그것을 64개 정도로 줄인 것입니다. 윤곽 부분은 형상 자체가 저렇게 생겼으니 어쩔 수 없지만, Fragment shader에 의해 계산 된 색상은 Vertex의 부족함이 전혀 드러나지 않습니다. 좀 더 줄여서 5등분씩 나눴을 경우는~

박수 한번 쳐줘야 할 것 처럼 멋지죠.

자, 이것으로 방향광에 대한 구현은 살펴봤고, 다음부터는 위치에 영향을 받는 positional light에 대해서 살펴보겠습니다.

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

 

[opengl].[#2.GLSL] 14. Positional light

앞에서 Ambient+Specular light의 구현을 해보았습니다. https://learn-and-give.tistory.com/61 [opengl].[#2.GLSL] 13. ambient 조명과 specular 조명 앞에서 Diffuse 조명과 재질을 쉐이더로 구현하는 방법을 살펴봤습니다. [

learn-and-give.tistory.com

 

 

728x90
반응형

댓글