※ 본 포스팅은 learnopengl.com을 번역 및 가감한 포스팅이며, learnopengl에서는 번역 작업 참여를 적극 장려하고 있습니다!
아래 링크에서 원문을 확인할 수 있습니다.
Calculating the diffuse color
이제 각 정점마다 법선 벡터를 갖게 되었습니다. 근데 아직 더 필요한게 있죠. 조명의 위치 벡터와 프래그먼트 위치 벡터가 필요합니다. 조명 위치는 고정된 변수로 되어있으니 프래그먼트 셰이더에서는 uniform으로 선언하면 됩니다.
uniform vec3 lightPos;
그리고 렌더 루프마다 uniform 값을 업데이트해줍니다. 사실 고정해놓을거니까 루프 밖에서 해도 되요.
이전에 만들었던 lightPos 벡터를 조명 광원의 위치로 지정해줍시다.
lightingShader.setVec3("lightPos", lightPos);
이제 실제 프래그먼트의 위치를 알아야합니다. 우리는 조명 계산을 월드 공간에서 할 거에요. 먼저 월드 공간에서의 정점 위치를 가져와야합니다. 그럴려면 정점 위치 속성을 model matrix와 곱해서 월드 공간 좌표로 변환하면 됩니다. 정점 셰이더에서 수행해서 출력해보죠.
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;
}
마지막으로 프래그먼트 셰이더에 넘겨받을 입력 변수를 추가합시다.
in vec3 FragPos;
FragPos벡터는 프래그먼트 하나하나의 월드 위치를 나타냅니다. 이제 필요한 변수를 모두 준비되었고 조명 계산을 해봅시다.
먼저 계산할 것은 광원과 프래그먼트 위치 사이의 방향벡터입니다. 즉 빛이 나아가는 광원의 방향 벡터를 말합니다. 두 벡터의 차를 구해서 계산하면 되고, 단위 벡터로 방향만 사용해야하기 때문에 normalize합니다.
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
light 계산시 벡터의 방향만을 사용합니다. 그래서 기본적으로 벡터 계산시 normalize 해야합니다. 그래야 내적과 같은 계산은 단순화 시킵니다. 조명 계산시 벡터 normalization으로 단위 벡터로 만들었는지 꼭 확인해주세요.
그 다음, 프래그먼트에 조명의 diffuse의 작용을 계산해야합니다. 법선 벡터를 normalize한 norm과 lightDir 벡터를 내적해서 구하고, 결과를 조명의 색과 곱해서 diffuse 성분을 얻으면 됩니다. 법선 벡터와 빛의 방향벡터를 내적을 하니까 각도가 크면 클수록 어두워지겠죠?
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
만약 두 벡터가 90도 보다 크면 내적했을 때 음수가 나옵니다. 그러면 diffuse 값이 음수가 되겠네요? 그럼 안됩니다. 그래서 max 함수로 음수인 경우 0.0을 사용하게 합니다. 음수 색상이라는 건 없어요.
이렇게 주변광(ambient)와 확산광(diffuse)를 계산했습니다. 이제 두 성분을 더하고 객체의 색과 곱하면 프래그먼트에 두 조명성분이 반영된 색을 얻을 수 있습니다.
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
셰이더와 애플리케이션을 컴파일하면 이런 결과가 나오겠죠.
diffuse를 적용했는데 어떤가요? 이제서야 빛을 쏘고있는 느낌이 좀 납니다. 법선 벡터를 상상해보면 큐브를 돌면서 빛의 방향과 법선 벡터사이의 각도가 크면 어두워진다는걸 알 수 있습니다.
One last thing
법선 벡터를 vertex shader에서 fragment shade로 전달했었죠? 근데 fragment shader에서 계산했던 건 모두 월드 공간에서 이루어지는 건데, 법선 벡터는 큐브의 로컬 공간이었습니다. 그럼 법선 벡터로 공간 좌표로 변환해야하는거 아닌가요? 근데 법선 벡터의 경우는 위치벡터처럼 model matrix랑 곱하기만 하면 변환 되는게 아닙니다.
법선 벡터는 위치를 갖고 있지 않은 데다가, 동차좌표(homogeneous coordinate) w를 갖지 않아요. 그래서 transform matrix로 연산이 안된다는 겁니다. model matrix랑 어떻게 곱하죠? 어떻게 하냐면 행렬의 상단 왼쪽 3x3 부분만을 사용해서 변환 부분을 제거해야합니다(법선 벡터 w를 모두 0으로 해서 4x4 행렬로 만드는 것도 같은 방법입니다).
model matrix가 non-uniform한 scaling(x,y,z이 모두 같지 않은 스케일링이죠)을 수행하는 경우, 정점이 변형되면서 법선 벡터가 표면에 수직이 않게 변할 수 있습니다. non-uniform 스케일링이 어떻에 법선 벡터에 영향을 미치는지 그림을 봅시다.
균일한 스케일링이면 법선은 변하지 않는데, 비균일(non-uniform) 스케일링을 적용하면 이렇게 법선이 수직이 아니게 되고 조명도 이상하게 적용될 거에요.
문제의 해결책은 법선 벡터가 변형된 것처럼 조정된 model matrix를 사용하는 방법이 있습니다. 이걸 법선 행렬(normal matrix)라고 하고, 법선 벡터가 스케일링 되면서 변형된 걸 제거하려고 특정 연산을 사용합니다. 자세한 것은 참조 링크를 보시는 편이 도움이 될거에요.
법선 행렬은 기본적으로 model matrix에서 왼쪽 위 3x3 부분의 역행렬을 전치한 행렬입니다. 뭔가 복잡한데요. 역행렬과 전치행렬이 뭔지 알아야하겠죠. 다른 자료에서는 법선 행렬을 model-view matrix에서 유도한다고 정의하는데, 우리는 view 가 아니라 world 공간에서 하고 있으니까 view는 빼고 model 행렬을 가지고 유도하기로 합니다.
정점 셰이더는 역행렬과 전치행렬 함수를 제공하니까 이걸 사용해서 법선 행렬을 만들 수 있습니다. 행렬을 3x3 행렬로 캐스팅하고, vec3 법선 벡터와 곱합시다.
Normal = mat3(transpose(inverse(model))) * aNormal;
행렬의 역행렬 연산은 생각보다 shader 비용이 많이 들어갑니다. 그래서 역행렬 연산은 줄이는 게 좋아요. 왜냐하면 vertex 하나하나에 대해서 연산이 이루어지기 때문인데 최적화를 위해서라면 이런 연산ㅇ느 CPU에서 법선 행렬을 다 계산하고 GPU shader에 전달하는 방식을 추천합니다.
diffuse lighting 에서는 객체 스케일링을 하지 않았으니 조명에 문제도 없고 법선 행렬도 안해도 되었죠. 그런 경우는 그냥 법선 벡터에 model matrix를 곱해도 충분합니다. 하지만 non-uniform scaling을 하면 법선 벡터를 꼭 법선 행렬과 곱해주어야합니다.
이제 다음에는 specular lighting을 구현해봅시다.
'개발 · 컴퓨터공학 > LearnOpenGL' 카테고리의 다른 글
OpenGL Specular Lighting 스페큘러 확산광 조명 [Learn OpenGL 쉬운 번역] (11) | 2024.09.11 |
---|---|
OpenGL Basic Lighting 조명 [Learn OpenGL 쉬운 번역] (16) | 2024.09.09 |
OpenGL Lighting Scene 조명 씬 구현 [Learn OpenGL 쉬운 번역] (12) | 2024.09.08 |
OpenGL Color 색 [Learn OpenGL 쉬운 번역] (6) | 2024.09.07 |
OpenGL Camera Zoom 카메라 줌 [Learn OpenGL 쉬운 번역] (21) | 2024.09.06 |