기본적인 조명 효과
실제 세계의 조명은 매우 복잡하며 수많은 요소에 따라 달라지기 때문에, 제한된 처리 능력으로 모든 요소를 계산하는 것은 불가능합니다. 따라서 OpenGL의 조명은 현실을 근사화하여 처리하기 훨씬 쉽고 상대적으로 유사한 모습을 보이는 단순화된 모델을 사용합니다. 이러한 조명 모델은 우리가 이해하는 빛의 물리적 원리에 기반합니다. 그중 하나가 퐁(Phong) 조명 모델입니다. 퐁 조명 모델의 주요 구성 요소는 주변광, 확산광, 반사광의 세 가지입니다. 아래에서 이러한 조명 요소들이 각각 그리고 모두 합쳐졌을 때 어떤 모습인지 확인할 수 있습니다.

- 주변(Ambient) 조명: 완전히 어두울 때에도 세상 어딘가에는 보통 빛(달빛, 멀리 있는 불빛 등)이 존재하기 때문에 물체가 완전히 어두운 경우는 거의 없습니다. 이를 시뮬레이션하기 위해 물체에 항상 색을 부여하는 주변 조명 상수를 사용합니다.
- 확산(Diffuse) 조명: 광원이 물체에 미치는 방향성 영향을 시뮬레이션합니다. 이는 조명 모델에서 시각적으로 가장 중요한 요소입니다. 물체의 어느 부분이 광원을 향할수록 더 밝아집니다.
- 반사(Specular) 조명: 광택이 있는 물체에 나타나는 빛의 밝은 부분을 시뮬레이션합니다. 반사광은 물체의 색상보다는 빛의 색상에 더 영향을 받습니다.
시각적으로 흥미로운 장면을 만들기 위해서는 최소한 이 세 가지 조명 요소를 시뮬레이션해야 합니다. 가장 간단한 것부터 시작해 보겠습니다. 바로 주변 조명입니다.
주변 조명(주변광)
빛은 보통 하나의 광원에서 나오는 것이 아니라, 우리 주변 곳곳에 흩어져 있는 수많은 광원에서 나옵니다. 심지어 눈에 바로 보이지 않는 광원까지도 빛이 도달할 수 있습니다. 빛의 특성 중 하나는 여러 방향으로 산란되고 반사되어 직접 보이지 않는 곳까지 도달할 수 있다는 것입니다. 따라서 빛은 다른 표면에 반사되어 물체의 조명에 간접적인 영향을 미칠 수 있습니다. 이러한 점을 고려한 알고리즘을 전역 조명 알고리즘이라고 하지만, 이러한 알고리즘은 복잡하고 계산 비용이 많이 듭니다.
복잡하고 비용이 많이 드는 알고리즘을 선호하지 않기 때문에, 먼저 매우 단순한 전역 조명 모델인 주변광을 사용하겠습니다. 이전 섹션에서 보셨듯이, 객체 프래그먼트의 최종 색상에 작은 상수(밝은) 색상을 더하여 직접적인 광원이 없더라도 항상 산란광이 있는 것처럼 보이게 합니다.
장면에 주변 조명을 추가하는 것은 매우 쉽습니다. 광원의 색상에 작은 상수 주변 조명 계수를 곱하고, 이 값을 오브젝트의 색상에 곱한 다음, 이 값을 큐브 오브젝트 셰이더에서 프래그먼트 색상으로 사용하면 됩니다.
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
이제 프로그램을 실행하면 첫 번째 단계의 조명이 객체에 성공적으로 적용된 것을 확인할 수 있습니다. 객체는 상당히 어둡지만 주변 조명이 적용되어 완전히 어둡지는 않습니다(라이트 큐브는 다른 셰이더를 사용하기 때문에 영향을 받지 않습니다). 결과는 대략 다음과 같을 것입니다.

확산 조명(확산광)
주변 조명만으로는 그다지 흥미로운 결과를 얻기 어렵지만, 확산 조명을 사용하면 사물에 상당한 시각적 효과를 줄 수 있습니다. 확산 조명은 사물의 각 부분이 광원에서 나오는 빛줄기에 가까울수록 더 밝게 빛나도록 합니다. 확산 조명을 더 잘 이해하려면 다음 이미지를 참고하세요.

왼쪽에는 광원에서 나오는 빛이 물체의 한 프래그먼트를 향해 비추고 있습니다. 우리는 빛이 프래그먼트에 닿는 각도를 측정해야 합니다. 빛이 물체 표면에 수직일 때 빛은 가장 큰 영향을 미칩니다. 빛과 프래그먼트 사이의 각도를 측정하기 위해 우리는 법선 벡터라는 것을 사용합니다. 법선 벡터(normal vector)는 프래그먼트 표면에 수직인 벡터입니다(여기서는 노란색 화살표로 표시됨). 이에 대해서는 나중에 자세히 설명하겠습니다. 두 벡터 사이의 각도는 내적을 이용하여 쉽게 계산할 수 있습니다.
변형 단원에서 배웠듯이, 두 단위 벡터 사이의 각도가 작을수록 내적은 1에 가까워집니다. 두 벡터 사이의 각도가 90도일 때 내적은 0이 됩니다. \(\theta\)에도 동일하게 적용됩니다. \(\theta\)가 커질수록 빛이 프래그먼트의 색상에 미치는 영향은 줄어듭니다.
두 벡터 사이의 각도의 코사인 값만 얻으려면 단위 벡터(길이가 1인 벡터)를 사용해야 하므로 모든 벡터를 정규화해야 합니다. 그렇지 않으면 내적 계산 시 코사인 값 외에도 다른 값이 반환됩니다(변형 참조).
결과적으로 얻어지는 내적은 스칼라 값을 반환하며, 이 값을 사용하여 빛이 프래그먼트의 색상에 미치는 영향을 계산할 수 있습니다. 따라서 빛을 향한 프래그먼트의 방향에 따라 조명이 다르게 나타나는 것을 확인할 수 있습니다.
그렇다면 확산광을 계산하려면 무엇이 필요할까요?
- 법선 벡터: 정점의 표면에 수직인 벡터.
- 방향 광선: 광원의 위치와 프래그먼트의 위치 차이 벡터인 방향 벡터입니다. 이 광선을 계산하려면 광원의 위치 벡터와 프래그먼트의 위치 벡터가 필요합니다.
법선 벡터
법선 벡터는 정점의 표면에 수직인 (단위) 벡터입니다. 정점 자체는 표면이 없기 때문에 (공간상의 한 점일 뿐입니다), 주변 정점들을 이용하여 정점의 표면을 파악하고 법선 벡터를 구합니다. 외적을 이용하면 큐브의 모든 정점에 대한 법선 벡터를 계산할 수 있지만, 3D 큐브는 복잡한 도형이 아니므로 단순히 정점 데이터에 수동으로 추가하는 것이 더 간단합니다. 업데이트된 정점 데이터 배열은 여기에서 확인할 수 있습니다. 법선 벡터가 각 평면의 표면에 수직인 벡터임을 시각적으로 이해해 보세요 (큐브는 6개의 평면으로 이루어져 있습니다).
정점 배열에 추가 데이터를 넣었으므로 큐브의 정점 셰이더를 업데이트해야 합니다.
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...
이제 각 정점에 법선 벡터를 추가하고 정점 셰이더를 업데이트했으므로 정점 속성 포인터도 업데이트해야 합니다. 광원 큐브는 정점 데이터에 동일한 정점 배열을 사용하지만, 조명 셰이더는 새로 추가된 법선 벡터를 사용하지 않습니다. 조명 셰이더나 속성 설정을 업데이트할 필요는 없지만, 최소한 정점 속성 포인터를 수정하여 새 정점 배열의 크기를 반영해야 합니다.
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
조명 셰이더에서 완전히 사용되지 않는 정점 데이터를 사용하는 것이 비효율적으로 보일 수 있지만, 정점 데이터는 컨테이너 객체에서 이미 GPU 메모리에 저장되어 있으므로 새로운 데이터를 GPU 메모리에 저장할 필요가 없습니다. 실제로 이는 조명 전용으로 새로운 VBO를 할당하는 것보다 더 효율적입니다.
모든 조명 계산은 프래그먼트 셰이더에서 수행되므로 정점 셰이더에서 프래그먼트 셰이더로 법선 벡터를 전달해야 합니다. 이제 그렇게 해 보겠습니다.
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
이제 남은 작업은 프래그먼트 셰이더에서 해당 입력 변수를 선언하는 것입니다.
in vec3 Normal;
확산색 계산
이제 각 정점의 법선 벡터는 얻었지만, 광원의 위치 벡터와 프래그먼트의 위치 벡터가 여전히 필요합니다. 광원의 위치는 단일 정적 변수이므로 프래그먼트 셰이더에서 유니폼 변수로 선언할 수 있습니다.
uniform vec3 lightPos;
그런 다음 렌더링 루프 내에서 유니폼을 업데이트합니다(프레임마다 변경되지 않으므로 루프 외부에서 업데이트해도 됩니다). 이전 장에서 선언한 lightPos 벡터를 확산 광원의 위치로 사용합니다.
lightingShader.setVec3("lightPos", lightPos);
다음으로 필요한 것은 실제 프래그먼트의 위치입니다. 모든 조명 계산은 월드 공간에서 수행할 것이므로 먼저 월드 공간 좌표계로 변환된 정점 위치가 필요합니다. 이를 위해 정점 위치 속성에 모델 행렬만 곱해서 (뷰 및 투영 행렬은 곱하지 않음) 월드 공간 좌표로 변환합니다. 이는 정점 셰이더에서 쉽게 구현할 수 있으므로 출력 변수를 선언하고 월드 공간 좌표를 계산해 보겠습니다.
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 벡터를 생성합니다. 이제 필요한 모든 변수가 설정되었으므로 조명 계산을 시작할 수 있습니다.
먼저 광원과 프래그먼트 위치 사이의 방향 벡터를 계산해야 합니다. 이전 섹션에서 광원의 방향 벡터는 광원의 위치 벡터와 프래그먼트의 위치 벡터의 차이 벡터라는 것을 알았습니다. 변환 섹션에서 배웠듯이 두 벡터를 서로 빼면 이 차이를 쉽게 계산할 수 있습니다. 또한 모든 관련 벡터가 단위 벡터가 되도록 하기 위해 법선 벡터와 결과 방향 벡터를 모두 정규화합니다.
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
조명을 계산할 때 일반적으로 벡터의 크기나 위치는 중요하지 않고 방향만 중요합니다. 방향만 중요하기 때문에 대부분의 계산(예: 내적)은 단위 벡터를 사용하여 간소화됩니다. 따라서 조명 계산을 할 때는 관련 벡터를 항상 정규화하여 실제 단위 벡터가 되도록 해야 합니다. 벡터 정규화를 잊는 것은 흔히 발생하는 실수입니다.
다음으로, 법선 벡터와 광원 방향 벡터의 내적을 계산하여 현재 프래그먼트에 미치는 빛의 확산 효과를 구해야 합니다. 이렇게 얻은 값에 광원의 색상을 곱하여 확산 성분을 계산합니다. 두 벡터 사이의 각도가 클수록 확산 성분이 더 어두워집니다.
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
두 벡터 사이의 각도가 90도보다 크면 내적 결과가 음수가 되어 확산 성분이 음수가 됩니다. 따라서 확산 성분(그리고 색상)이 음수가 되지 않도록 두 매개변수 중 가장 큰 값을 반환하는 max 함수를 사용합니다. (결과가 0보다 작다면 0이 대신 사용됩니다.) 음수 색상에 대한 조명은 명확하게 정의되어 있지 않으므로, 특이한 예술가가 아니라면 음수 값을 사용하는 것은 피하는 것이 좋습니다.
이제 주변광과 확산광 성분을 모두 얻었으므로 두 색상을 서로 더한 다음 결과에 객체의 색상을 곱하여 최종 프래그먼트의 출력 색상을 얻습니다.
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
애플리케이션(및 셰이더) 컴파일이 성공적으로 완료되면 다음과 같은 화면이 나타납니다.

확산광을 사용하면 큐브가 다시 실제 큐브처럼 보이기 시작하는 것을 볼 수 있습니다. 머릿속으로 법선 벡터를 시각화하고 카메라를 큐브 주변으로 움직여 보세요. 법선 벡터와 광원의 방향 벡터 사이의 각도가 클수록 프래그먼트가 더 어두워지는 것을 확인할 수 있습니다.
막히는 부분이 있다면 여기에서 전체 소스 코드와 여러분의 소스 코드를 비교해 보세요.
마지막 하나
이전 섹션에서는 정점 셰이더에서 프래그먼트 셰이더로 법선 벡터를 직접 전달했습니다. 하지만 프래그먼트 셰이더의 모든 계산은 월드 공간에서 이루어지므로 법선 벡터도 월드 공간 좌표로 변환해야 하지 않을까요? 기본적으로는 맞지만, 단순히 모델 행렬을 곱하는 것만큼 간단하지는 않습니다.
우선, 법선 벡터는 방향 벡터일 뿐 공간상의 특정 위치를 나타내지 않습니다. 둘째, 법선 벡터는 동차 좌표(정점 위치의 w 성분)를 가지지 않습니다. 즉, 변환은 법선 벡터에 영향을 미치지 않아야 합니다. 따라서 법선 벡터를 모델 행렬과 곱하려면 모델 행렬의 좌측 상단 3x3 행렬을 취하여 변환 항을 제거해야 합니다. (참고로, 법선 벡터의 w 성분을 0으로 설정하고 4x4 행렬과 곱하는 방법도 있습니다.)
둘째, 모델 행렬이 불균일하게 스케일링될 경우, 정점들이 변형되어 법선 벡터가 더 이상 표면에 수직이 아니게 됩니다. 다음 이미지는 이러한 모델 행렬(불균일 스케일링 적용)이 법선 벡터에 미치는 영향을 보여줍니다.

균일하지 않은 스케일을 적용할 때마다 법선 벡터가 해당 표면에 더 이상 수직이 아니게 되어 조명이 왜곡됩니다. (참고로 균일 스케일링은 법선 벡터의 크기만 변경하고 방향은 변경하지 않으며, 이는 정규화를 통해 쉽게 해결할 수 있습니다)
이러한 문제를 해결하는 방법은 법선 벡터에 특화된 다른 모델 행렬을 사용하는 것입니다. 이 행렬을 법선 행렬이라고 하며, 몇 가지 선형 대수 연산을 통해 법선 벡터의 잘못된 스케일링으로 인한 영향을 제거합니다. 이 행렬의 계산 방법에 대해 더 자세히 알고 싶다면 이 글을 참고하세요.
법선 행렬은 '모델 행렬의 좌측 상단 3x3 부분의 역행렬의 전치 행렬'로 정의됩니다. 좀 복잡하죠? 이해가 잘 안 되더라도 걱정하지 마세요. 아직 역행렬과 전치 행렬에 대해서는 다루지 않았으니까요. 대부분의 자료에서는 법선 행렬을 모델-뷰 행렬에서 유도한다고 정의하지만, 우리는 뷰 공간이 아닌 월드 공간에서 작업하므로 모델 행렬에서 유도할 것입니다.
정점 셰이더에서는 모든 행렬 유형에 대해 작동하는 역행렬 및 전치 함수를 사용하여 법선 행렬을 생성할 수 있습니다. 행렬을 3x3 행렬로 형변환하는 이유는 변환 속성을 없애고 vec3 법선 벡터와 곱할 수 있도록 하기 위함입니다.
Normal = mat3(transpose(inverse(model))) * aNormal;
역행렬 계산은 셰이더에게 비용이 많이 드는 연산이므로, 가능한 한 역행렬 계산을 피하는 것이 좋습니다. 역행렬 계산은 장면의 각 정점마다 수행해야 하기 때문입니다. 학습 목적이라면 괜찮지만, 효율적인 애플리케이션을 위해서는 (모델 행렬처럼) 법선 행렬을 CPU에서 계산한 후 유니폼 변수를 통해 셰이더로 전달하는 것이 좋습니다.
확산 조명 부분에서는 객체에 크기 조정을 하지 않았기 때문에 조명이 적절했고, 따라서 법선 행렬을 사용할 필요가 없었으며 법선 벡터에 모델 행렬을 곱하기만 하면 되었습니다. 하지만 비균일 크기 조정을 하는 경우에는 법선 벡터에 법선 행렬을 곱하는 것이 필수적입니다.
반사 조명(반사광)
조명에 대한 이야기로 아직 지치지 않으셨다면, 반사광을 추가하여 퐁 조명 모델을 완성해 볼 수 있습니다.
확산광과 마찬가지로 반사광은 광원의 방향 벡터와 오브젝트의 법선 벡터를 기반으로 하지만, 플레이어가 프래그먼트를 바라보는 방향, 즉 시점 방향도 고려합니다. 반사광은 표면의 반사 특성을 이용합니다. 오브젝트의 표면을 거울이라고 생각하면, 빛이 반사되는 부분에서 반사광이 가장 강하게 나타납니다. 다음 이미지에서 이 효과를 확인할 수 있습니다.

우리는 빛의 방향을 법선 벡터를 중심으로 반사시켜 반사 벡터를 계산합니다. 그런 다음 이 반사 벡터와 시점 방향 사이의 각도 거리를 계산합니다. 두 각도가 가까울수록 반사광의 영향이 커집니다. 결과적으로 표면을 통해 반사된 빛의 방향을 보면 약간의 하이라이트가 나타납니다.
뷰 벡터는 반사광을 구현하는 데 필요한 추가 변수이며, 뷰어의 월드 공간 위치와 프래그먼트의 위치를 이용하여 계산할 수 있습니다. 그런 다음 반사광의 강도를 계산하고, 이를 광원의 색상과 곱한 후, 주변광 및 확산광 성분에 더합니다.
저희는 월드 공간에서 조명 계산을 하기로 했지만, 대부분의 사람들은 뷰 공간에서 조명 계산을 하는 것을 선호합니다. 뷰 공간의 장점은 뷰어의 위치가 항상 (0,0,0)이므로 뷰어의 위치를 이미 알고 있다는 점입니다. 하지만 저는 학습 측면에서 월드 공간에서 조명 계산을 하는 것이 더 직관적이라고 생각합니다. 만약 뷰 공간에서 조명을 계산하고 싶다면, 관련된 모든 벡터를 뷰 행렬로 변환해야 합니다 (법선 행렬도 변경하는 것을 잊지 마세요).
뷰어의 월드 공간 좌표를 얻으려면 카메라 객체(당연히 뷰어)의 위치 벡터를 사용하면 됩니다. 따라서 프래그먼트 셰이더에 유니폼 변수를 하나 더 추가하고 카메라 위치 벡터를 셰이더에 전달해 보겠습니다.
uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position);
이제 필요한 모든 변수를 확보했으므로 반사광 강도를 계산할 수 있습니다. 먼저 반사광 하이라이트가 너무 눈에 띄지 않도록 중간 밝기의 색상을 부여하는 반사광 강도 값을 정의합니다.
float specularStrength = 0.5;
이 값을 1.0f로 설정하면 코랄색 큐브에 비해 지나치게 밝은 반사광이 생성됩니다. 다음 장에서는 이러한 조명 강도를 적절하게 설정하고 객체에 미치는 영향에 대해 설명하겠습니다. 다음으로 시야 방향 벡터와 법선 축을 따라 해당하는 반사 벡터를 계산합니다.
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
lightDir 벡터의 방향을 반전시킨다는 점에 유의하세요. reflect 함수는 첫 번째 벡터가 광원에서 프래그먼트의 위치를 향해야 한다고 가정하지만, 현재 lightDir 벡터는 그 반대 방향, 즉 프래그먼트에서 광원을 향하고 있습니다(이는 이전에 lightDir 벡터를 계산할 때 뺄셈 순서에 따라 달라집니다). 올바른 reflect 벡터를 얻기 위해 lightDir 벡터의 방향을 반전시키는 것입니다. 두 번째 인수는 법선 벡터를 기대하므로 정규화된 법선 벡터를 제공합니다.
reflect 함수
reflect 함수는 반사 방향을 계산해 주는 함수입니다. 첫번째 인자로 빛의 방향 벡터를 받고, 두번째 인자로 법선 벡터를 받습니다.
이제 남은 것은 반사광 성분을 실제로 계산하는 것입니다. 이는 다음 공식을 사용하여 수행됩니다.
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
먼저 시선 방향과 반사 방향의 내적을 계산하고(음수가 아닌지 확인), 그 값을 32제곱합니다. 이 32의 값이 하이라이트의 광택도 값입니다. 물체의 광택도 값이 높을수록 빛을 사방으로 흩뜨리지 않고 제대로 반사하므로 하이라이트가 작아집니다. 아래 이미지는 다양한 광택도 값이 시각적으로 어떤 영향을 미치는지 보여줍니다.

반사광 성분이 너무 눈에 거슬리지 않도록 지수 값을 32로 유지합니다. 이제 남은 작업은 이를 주변광 및 확산광 성분에 더하고, 그 결과를 물체의 색상으로 곱하는 것뿐입니다.
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
이제 퐁 조명 모델의 모든 조명 구성 요소를 계산했습니다. 여러분의 시점에서 보면 다음과 같은 화면이 나타날 것입니다.

완성된 전체 소스코드는 여기에서 확인해 볼 수 있습니다.
초창기 라이팅 셰이더 개발 당시에는 개발자들이 정점 셰이더에서 퐁(Phong) 라이팅 모델을 구현하곤 했습니다. 정점 셰이더에서 라이팅을 처리하는 장점은 프래그먼트에 비해 정점 수가 훨씬 적기 때문에 (비용이 많이 드는) 라이팅 계산 빈도가 낮아 효율성이 훨씬 높다는 것이었습니다. 그러나 정점 셰이더에서 생성되는 색상 값은 해당 정점의 라이팅 색상만을 나타내고, 주변 프래그먼트의 색상 값은 보간된 라이팅 색상을 반영합니다. 그 결과, 많은 수의 정점를 사용하지 않는 한 라이팅이 그다지 사실적이지 못했습니다.

퐁(Phong) 조명 모델을 정점 셰이더에 구현할 경우, 퐁 셰이딩 대신 구로(Gouraud) 셰이딩이라고 부릅니다. 보간으로 인해 조명이 다소 부자연스럽게 보일 수 있습니다. 퐁 셰이딩은 훨씬 더 부드러운 조명 결과를 제공합니다.
이제 여러분은 셰이더가 얼마나 강력한지 이해하기 시작했을 것입니다. 셰이더는 최소한의 정보만으로도 프래그먼트의 모든 오브젝트 색상에 조명이 어떻게 영향을 미치는지 계산할 수 있습니다. 다음 장에서는 조명 모델을 활용하여 할 수 있는 훨씬 더 심도 있는 작업들을 살펴보겠습니다.
연습 문제
- 현재 광원은 움직이지 않는 고정된 광원입니다. 사인 함수나 코사인 함수를 사용하여 시간에 따라 광원을 장면 안에서 움직여 보세요. 시간에 따른 조명 변화를 관찰하면 퐁의 조명 모델을 잘 이해할 수 있습니다. (해결 방법)
- 주변광, 확산광, 반사광의 강도를 다양하게 조절해보고 결과에 어떤 영향을 미치는지 살펴보세요. 광택도 또한 조절해 보세요. 특정 값이 특정 시각적 결과를 가져오는 이유를 이해하려고 노력하세요.
- 월드 공간이 아닌 뷰 공간에서 퐁 셰이딩을 적용해보세요. (해결 방법)
- 퐁 셰이딩 대신 구로 셰이딩을 구현해 보세요. 제대로 했다면 큐브 오브젝트의 조명(특히 반사광)이 약간 어색해 보일 겁니다. 왜 그렇게 어색해 보이는지 분석해 보세요. (해결 방법)