게임공장
[Learn OpenGL 번역] 3-2. 조명 - 기본 조명 본문
기본 조명
조명/기본 조명
실생활의 조명은 매우 복잡하고 프로세싱 파워로 계산하기 어려울 정도의 많은 요소들로 이루어져 있습니다. 그러므로 OpenGL에서의 조명은 처리되기 쉽고 실세계의 사물과 비슷하게 보이는 모델을 사용하여 실세계에 대한 근사치를 기반으로 이루어져있습니다. 이 조명 모델들은 우리가 이해한 빛의 물리학을 기반으로 이루어져 있습니다. 이 모델들 중 하나는
Ambient lighting : 어두운 때일지라도 일반적으로 world의 어딘가엔 어떠한 조명이 존재(달, 멀리 떨어져 있는 조명)하므로 오브젝트는 대부분 완전히 어두워지지 않습니다. 이를 시뮬레이션해보기 위해 우리는 오브젝트에 어떠한 색상을 주는 ambient 조명 상수를 사용합니다.Diffuse lighting : 조명 오브젝트가 가지고 있는 방향이 있는 조명을 오브젝트에 비추어 시뮬레이션 합니다. 이 것은 조명 모델에서 시각적으로 가장 중요한 요소입니다. 오브젝트의 많은 부분이 광원을 마주보고 있을수록 더 밝아집니다.Specular lighting : 빛나는 오브젝트에서 볼 수 있는 밝은 지점을 시뮬레이션 합니다. 반사 하이라이트는 종종 오브젝트의 컬러보다는 조명의 컬러에 더 치우쳐집니다.
시각적으로 흥미로운 Scene을 생성하기 위해 최소한 이 3 가지의 조명 요소들을 시뮬레이션해야 합니다. 우리는 가장 간단한 ambient lighting으로 시작해보도록 하겠습니다.
Ambient lighting
빛은 일반적으로 하나의 광원으로부터 오는 것이 아니라 우리 주변에 산재해 있는 많은 광원들로부터 옵니다. 그들이 직접적으로 보이지 않을지라도 말이죠. 빛의 특성들 중 하나는 근처에 있지 않은 지점에 도달하기 위해 어려 방향으로 퍼지고 산란한다(튄다)는 것입니다. 따라서 빛은 면에서 반사될 수 있고 오브젝트에 간접적으로 영향을 줍니다. 이 것을 고려한 알고리즘을
우리는 복잡하고 비용이 많이 드는 알고리즘을 좋아하지 않기 때문에 global illumination 의 아주 간단한 모델을 사용하여 시작할 것입니다. 이 것의 이름은
Scene에 ambient lighting을 추가하는 것은 정말 쉽습니다. 빛의 컬러를 정하고 이를 작은 상수 ambient 요소와 곱합니다. 그리고 이를 오브젝트의 컬러와 곱하여 fragment의 컬러로 이 값을 사용합니다.
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
이제 프로그램을 실행시키면 조명의 첫 번째 단계가 성공적으로 적용되었음을 볼 수 있습니다. 이 오브젝트는 상당히 어둡지만 ambient lighting이 적용되었기 때문에 완전히 어둡지는 않습니다(다른 shader를 사용하기 때문에 빛 큐브는 영향을 받지 않습니다). 이는 다음과 같이 보일 것입니다.
Diffuse lighting
Ambient lighting은 그자체로 아주 흥미로운 결과를 만들디 못합니다. 하지만 diffuse lighting은 오브젝트에 중요한 시각적인 효과를 주기 시작할 것입니다. Diffuse lighting은 해당 오브젝트의 광선에 정렬된 fragment가 광원과 가까이 있을수록 그 오브젝트는 밝아집니다. diffuse lighting에 대해 좀 쉽게 이해를 하기 위해 다음의 이미지를 보세요.
오브젝트의 하나의 fragment를 향해 광선을 쏘고 있는 광원이 왼쪽에 있습니다. 우리는 광선과 fragment 사이의 각이 필요합니다. 광선이 오브젝트의 면에 수직으로 향한다면 빛은 아주 많이 영향을 끼칠것입니다. 광선과 fragment 사이의 각을 측정하기 위해서는
두 유닛 벡터 사이의 각이 작을수록 내적은 1
와 가까워진다는 사실을 변환 강좌에서 언급했던 것을 기억하실 것입니다. 두 벡터 사이의 각이 90
도일때 내적은 0이 됩니다. θ 에 동일하게 적용해본다면 θ 의 값이 클수록 빛의 영향을 더 적게 받게됩니다.
1
인 벡터)를 사용할 것이므로 모든 벡터들을 정규화할 필요가 있습니다. 그렇지 않으면 내적은 그냥 cosine한 값보다 더 큰 값을 리턴할 것입니다(변환 강좌를 보세요).
따라서 내적의 결과로 fragment의 컬러에 대한 빛의 영향을 계산하기 위해 사용할 스칼라를 얻을 수 있습니다. 결과적으로 빛을 향한 방향에 따라 다르게 빛나는 fragment를 만들 수 있습니다.
그래서 diffuse lighring을 계산하기위해 무엇이 필요할까요?
- Normal vector(법선 벡터): vertex의 면에 수직인 벡터
- The directed light ray(방향을 가진 광선): 빛의 위치에서 fragment 위치로 향하는 방향 벡터입니다. 이 광선을 계산하기 위해 빛의 위치 벡터와 fragment의 위치 벡터가 필요합니다.
법선 벡터
법선 벡터는 vertex의 면에 수직인 (단위) 벡터입니다. vertex는 그 자체로 면을 가지고 있지 않기 때문에(단지 공간 내부에 하나의 점일 뿐입니다) vertex의 면을 알아내기 위해 주변의 vertex들을 사용하여 법선 벡터를 구합니다. 큐브의 모든 vertex들에 대한 법선 벡터를 계산하기 위해 외적을 사용하여 약간의 트릭을 사용할 수 있습니다. 하지만 3D 큐브는 복잡한 도형이 아니기 때문에 우리는 간단히 법선 벡터를 vertex data에 수작업으로 넣을 수 있습니다. 수정된 vertex data는 여기에서 확인할 수 있습니다. 이 법선들이 실제로 큐브의 평면에 대해 수직인 벡터들인지 생각해보세요(큐브는 6개의 평면을 가지고 있습니다).
vertex 배열에 데이터를 추가했기 때문에 lighting vertex shader를 수정해야 합니다.
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...
이제 우리는 각 vertex들에 법선 벡터를 추가하였고 vertex shader를 수정하고 vertex attribute pointer 또한 수정하였습니다. 램프는 같은 vertex 배열의 데이터를 사용하지만 lmap shader에서는 새롭게 추가된 법선 벡터를 사용하지 않습니다. lamp shader나 attribute 구성을 수정할 필요가 없고 vertex attribute pointer에 새로운 vertex 배열의 크기를 반영해야 합니다.
glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnable VertexAttribArray (0);
우리는 오직 각 vertex의 처음 3
개의 실수 값을 사용하고 나머지 3
개의 실수 값은 무시하므로 stride 파라미터를float
크기의 6
배로 수정합니다.
모든 빛에 대한 계산은 fragment shader에서 완료되므로 vertex shader의 법선벡터를 fragment shader에 전달해야 합니다.
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
남은 일은 fragment shader에 해당하는 입력 변수를 선언하는 것입니다.
in vec3 Normal;
Diffuse color 계산
이제 우리는 각각의 vertex에 대한 법선 벡터를 가지고 있지만 여전히 fragment 위치 벡터와 빛의 위치 벡터가 필요합니다 빛의 위치는 하나의 정적인 변수이기 때문에 fragment shader에 uniform으로 간단히 선언할 수 있습니다.
uniform vec3 lightPos;
그런 다음 게임 루프 안에서 uniform을 업데이이트 합니다(또는 이 값이 바뀌지 않기 때문에 게임루프 밖에서). 광원의 위치로서 이전 강좌에서 선언한 lightPos 벡터를 사용합니다.
lightingShader.setVec3("lightPos", lightPos);
우리가 필요한 마지막 하나는 실제 fragment의 위치입니다. 우리는 모든 빛에 대한 계산을 world space에서 할 것이므로 world space에 있는 vertex 위치가 필요합니다. 우리는 이를 vertex 위치 attribute를 오직(view, projection 행렬을 제외한) model 행렬과 곱하여 world space 좌표로 변환하는 것만으로 수행할 수 있습니다. 이는 vertex shader에서 쉽게 수행될 수 있으므로 출력 변수를 선언하고 world space 좌표를 계산해봅시다.
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = aNormal;
}
그리고 마지막으로 해당 입력 변수를 fragment shader에 추가 합니다.
in vec3 FragPos;
이제 필요한 모든 변수들이 설정되었고 fragment shader에서 빛에 대한 계산을 시작할 수 있습니다.
계산을 하기 위해 먼저 필요한 것은 광원과 fragment의 위치 사이의 방향 벡터입니다. 빛의 방향 벡터는 빛의 위치 벡터와 fragment의 위치 벡터의 차라고 언급했었습니다. 변환 강좌에서 보셨다시피 우리는 이를 두 개의 벡터에 대한 뺄셈을 수행해서 계산할 수 있습니다. 또한 관련있는 모든 벡터들이 단위 벡터로 변하기를 원하므로 법선 벡터와 최종 방향 벡터를 정규화 합니다.
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
다음 우리는 norm과 lightDir 벡터를 내적하여 실제 diffuse 효과를 계산하기 원합니다. 그러면 diffuse 요소를 얻기 위해 결과 값은 빛의 컬러와 곱해집니다. 최종적으로 두 벡터 사이의 각이 클수록 diffuse 요소는 어두워집니다.
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
두 개의 벡터 사이의 각이 90
도 보다 크다면 내적의 결과는 실제로 음수가 되고 음의 diffuse 요소를 가지게 됩니다. 이러한 이유 때문에 우리는
이제 우리는 ambient와 diffuse 컴포넌트를 가지고 있습니다. 이 두개의 컴포넌트에 설정된 컬러와 오브젝트의 컬러를 곱하여 최종 fragment의 컬러를 결정합니다.
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
응용 프로그램(과 shader)이 성공적으로 컴파일되었으면 다음과 같은 화면을 볼 수 있습니다.
diffuse lighting과 함께 큐브가 실제 큐브처럼 보임을 알 수 있습니다. 머릿속으로 법선 벡터를 시각화 한 후 큐브를 둘러 보고 법선 벡터와 빛의 방향 벡터 사이의 각이 클수록 fragment는 어두워지는 것을 확인해보세요.
문제가 생겼다면 여기에서 소스 코드를 확인하세요.
마지막 하나
현재로서는 법선 벡터를 그냥 그대로 vertex shader에서 fragment shader로 보내기만 했습니다. 하지만 fragment shader에서 수행해왔던 계산들은 모두 world space 좌표에서 수행되므로 법선 벡터를 world space 좌표로 변환해야 하지 않을까요?. 기본적으로는 맞습니다. 하지만 이를 단순히 model 행렬과 곱하는 것이 아니라 좀 복잡합니다.
제일 먼저 법선 벡터는 오직 방향 벡터입니다. 공간에서 특정한 위치를 나타내지 않습니다. 또한 법선 벡터는 동차 좌표(위치 벡터의 w
요소)를 가지고 있지 않습니다. 이는 이동 변환하는 것은 법선 벡터에서 아무런 효과가 없다는 것을 의미합니다. 그리고 법선 벡터에 model 행렬을 곱하고 싶다면 model 행렬의 좌측 상단의 3x3
행렬을 취하여 이동 행렬의 일부를 지워야 합니다(우리는 법선 벡터의 w
요소를 0
으로 설정할 수 있고 4x4 행렬과 곱할 수도 있습니다. 이는 이동을 지우는 것과 같습니다). 우리가 법선 벡터에 적용할 것은 스케일과 회전 변홥입니다.
두 번째로 model 행렬이 불균일 스케일을 수행하면 vertex들이 수정되어 법선 벡터가 더 이상 해당 면과 수직이 되지 않습니다. 그래서 우리는 이 model 행렬로 법선 벡터를 변환할 수 없습니다. 다음 이미지는 불균일 스케일을 수행하는 model 행렬로 법선 벡터를 변환했을 때를 보여줍니다.
불균일 스케일(균일 스케일은 그들의 방향이 변하지 않고 단지 크기만 변하기 때문에 법선에 영향을 끼치지 않습니다. 크기는 정규화를 통해 다시 원래의 상태로 되돌릴 수 있습니다)을 적용할때마다 법선 벡터는 해당 면에 더 이상 수직하지 않아 빛을 왜곡하게 됩니다.
이를 해결하는 트릭은 특별히 법선 벡터에 맞춰서 만들어진 다른 model 행렬을 사용하는 것입니다. 이 행렬은
이 법선 행렬은 'model 행렬의 좌측 상단 모서리의 역행렬에 대한 전치행렬'로 정의될 수 있습니다. 휴, 길고 복잡한 말입니다. 여러분이 이 뜻을 정확히 이해하지 못해도 걱정하지 마세요. 우리는 아직 역행렬과 전치행렬에 대해 아직 다루지 않았습니다. 대부분의 자료에서 법선 행렬을 이 연산들을 model-view 행렬에 적용하는 것으로 정의합니다. 하지만 우리는 world space(view space가 아닌)에서 작업하기 때문에 오직 model 행렬만 사용합니다.
Vertex shader에서 우리는 모든 행렬 유형에서 작동하는 vec3
법선 벡터와 곱셈을 할 수 있다는 것을 알아두세요.
Normal = mat3(transpose(inverse(model))) * aNormal;
Diffuse lighting 섹션에서 조명은 괜찮습니다. 우리는 오브젝트에 어떠한 스케일 연산도 수행하지 않았기 때문입니다. 그래서 실제로 법선 행렬을 사용할 필요가 없고 법선을 모델 행렬과 곱하기만 하면 됩니다. 하지만 불균일 스케일을 수행한다면 법선 벡터에 이 법선 행렬을 반드시 곱해주어야합니다.
Specular Lighting
여러분이 지치지 않았다면 specular 하이라이트를 추가하여 Phong lighting model을 끝낼 수 있습니다.
Diffuse lighting과 마찬가지로 specular lighring은 빛의 방향 벡터와 오브젝트의 법선 벡터를 기반으로 하지만 플레이어가 fragment를 바라보고 있는 방향에 대한 view 방향도 기반으로 합니다. Specular lighring은 빛의 반사 특성을 기반으로 합니다. 오브젝트의 면을 거울이라고 생각하면 이 면에서 반사되어진 빛을 보는 specular lighting은 가장 센 빛일것입니다. 다음의 그림에서 이 효과를 볼 수 있습니다.
우리는 법선 벡터 주위로 빛의 방향을 반사하여 반사 벡터를 계산합니다. 그런 다음 이 반사 벡터와 view 방향 사이의 거리를 계산합니다. 두 벡터 사이의 각이 가까울수록 specular light의 강도는 강해집니다. 최종 효과는 오브젝트를 통해 반사된 빛의 방향을 바라볼 때 약간의 하이라이트를 보는 것입니다.
View 벡터는 specular lighting에 필요한 하나의 추가적인 변수입니다. 우리는 viewer의 world space 위치와 fragment들의 위치를 사용하여 이 변수를 계산할 수 있습니다. 그런 다음 specular의 세기를 계산하고 이를 빛의 컬러와 곱하고 이 를 ambient, diffuse 요소에 추가합니다.
(0,0,0)
이므로 항상 뷰어의 위치를 쉽게 알 수 있습니다. 하지만 world space에서의 lighting 계산이 학습 목적으로는 더 직관적이라고 생각합니다. 여러분이 view space에서 lighting을 꼐산하고 싶다면 모든 관련된 벡터들을 view 행렬을 사용하여 변환시켜야 합니다(법선 행렬도 수정해야 한다는 것을 잊지 마세요).
Viewer의 world space 좌표를 얻기 위해서 간단히 카메라 오브젝트(이는 당연히 viewer입니다)의 위치 벡터를 사용하면 됩니다. 이제 fragment shader에 다른 uniform을 추가하고 해당 카메라 위치 벡터를 fragment shader에 전달해봅시다.
uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position);
이제 우리는 필요한 모든 변수를 가졌으므로 specular 세기를 계산할 수 있습니다. 먼저 specular 하이라이트에 효과가 너무 세지 않을 정도의 중간-밝기의 컬러로 specular 세기 값을 정의합니다.
float specularStrength = 0.5;
이 것을 1.0f
로 설정하면 아주 밝은 specular 요소를 얻을 수 있고 이는 coral 큐브에 대해 지나치게 셉니다. 다음 강좌에서 이 모든 lighting 세기에 대한 적절한 세팅에 대해서 오브젝트에 어떻게 영향을 끼치는지에 대해 다룰 것입니다. 그 다음 view 방향 벡터와 해당 반사 벡터를 계산합니다.
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
lightDir
벡터의 부호를 -로 바꿨다는 것을 알아두세요. reflect
함수는 첫 번째 벡터로 광원으로부터 fragment 위치로 향하는 벡터를 원하는데 lightDir
벡터는 현재 fragment에서 광원으로 향하는 벡터 입니다(lightDir
벡터를 계산할 때 뺄셈의 순서에 따라 달라집니다). 정확한 reflect
벡터를 얻기 위해 먼저 lightDir
벡터의 부호를 반대로 합니다. 두 번째 파라미터는 법선 벡터를 필요로 하므로 우리는 정규화된 norm
벡터를 넘겨줍니다.
그런 다음 남은 할일은 실제로 specular 컴포넌트를 계산하는 것입니다. 이는 다음과 같은 공식으로 수행될 수 있습니다.
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
먼저 view 방향과 reflect 방향을 내적합니다(그리고 이 것이 음수가 되지 않도록 확인합니다). 그런 다음 이 것을 32
제곱 해줍니다. 이 32
값은 하이라이트는
우리는 specular 컴포넌트가 너무 많이 차지하지 않기를 원하므로 32
로 유지시킵니다. 남은 단 한가지 할 일은 이 컴포넌트를 ambient와 diffuse 컴포넌트에 추가하고 오브젝트 컬러의 결과값에 곱하여 혼합하는 것입니다.
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
이제 Phong lighting model 의 모든 lighting 컴포넌트들을 계산했습니다. 여러분의 시점에 기반하여 다음과 같이 보일 것입니다.
여기에서 완전한 소스 코드를 볼 수 있습니다.
lighting shader들의 초창기에는 개발자들은 vertex shader에서 Phong lighting model을 구현하곤 했었습니다. 이를 vertex shader에서 구현하는 것의 장점은 fragment 보다 적은 vertex의 수가 적으므로 (비용이 많이 드는) lighting 계산들이 덜 자주 사용되기 때문에 효율적이다는 것입니다. 하지만 vertex shader의 최종 컬러 값은 오직 vertex만의 lighting 컬러입니다. 그러면 fragment의 컬러 값은 보간된 lighting 컬러가 됩니다. 이 결과는 아주 많은 양의 vertex들이 사용되지 않는다면 lighting이 현실적으로 보이지 않게됩니다.
Phong lighting model이 vertex shader에서 구현되었을 때 이를
이제 shader가 얼마나 강력한지 알아야 합니다. 약간의 정보와 함께 shader는 우리의 모든 오브젝트에 대한 fragment 컬러에 lighting이 얼마나 영향을 끼칠지 계산할 수 있습니다. 다음 강좌에서 lighting 모델을 가지고 우리가 할 수 있는 것들에 대해 좀 더 깊이 다루어 볼 것입니다.
연습
- 지금 당장 광원은 움직이지 않는 지루하고 정적인 광원입니다.
sin 이나cos 함수를 사용하여 광원이 시간이 지남에 따라 scene 주위를 움직이게 해보세요. 시간이 지남에 따라 lighting이 바뀌는 것을 보면 Phong lighting model에 대해 쉽게 이해할 수 있을 것입니다: 해답 - 다른 ambient, diffuse, specular 세기를 가지고 놀아보세요. 그리고 이것들이 결과에 어떻게 영향을 끼치는지 살펴보세요. 또한 shininess 값으로도 실험을 해보세요. 왜 이러한 값이 이러한 시각적인 결과를 낳는지 이해해보세요.
- world space 대신 view space에서 Phong shading을 수행해 보세요: 해답
- Phong shading 대신 Gouraud shading을 구현해보세요. 여러분이 제대로 했다면 뭔가 아쉬운 것을 볼 수 있어야 합니다. 왜 이렇게 이상하게 보이는지 이유를 생각해보세요: 해답
'OpenGL' 카테고리의 다른 글
[Learn OpenGL 번역] 3-4. 조명 - Lighting maps (0) | 2018.07.15 |
---|---|
[Learn OpenGL 번역] 3-3. 조명 - Materials (0) | 2018.07.15 |
[Learn OpenGL 번역] 3-1. 조명 - 색 (0) | 2018.07.15 |
[Learn OpenGL 번역] 2-10. 시작하기 - 복습 (0) | 2018.07.15 |
[Learn OpenGL 번역] 2-9. 시작하기 - 카메라 (0) | 2018.07.15 |