10.4 다각형 메시 (Polygon Meshes)
장면을 렌더링하려면 그 장면의 기하 구조를 수학적으로 기술해야 합니다. 여러 방법이 있으며, 이 절에서는 실시간 렌더링에서 가장 중요한 삼각형 메시(triangle mesh)를 중심으로 설명합니다. 먼저 맥락을 잡기 위해 다른 방법도 몇 가지 언급하겠습니다.
구성적 입체 기하(constructive solid geometry, CSG)는 기본 도형에 합집합·교집합·차집합 같은 불 연산을 적용해 물체의 모양을 기술하는 체계입니다. 비디오 게임에서는 빠른 프로토타입 제작 도구에 특히 유용하며, 언리얼 엔진이 대표적인 예입니다. 표면 대신 부피를 모델링하는 또 다른 기법은 9.1절에서 설명한 메타볼(metaball)입니다. 유기적인 모양이나 유체를 표현할 때 사용합니다. CSG, 메타볼, 그 밖의 부피 기반 표현은 특정 분야에서 매우 유용합니다. 하지만 렌더링, 특히 실시간 렌더링에서는 주로 물체의 표면을 기술하는 데 관심이 있으며, 임의의 점이 표면 안에 있는지 밖에 있는지 판단할 필요는 드뭅니다. 실제로 표면이 닫혀 있거나 일관된 부피를 정의할 필요조차 없습니다.
가장 흔한 표면 표현은 아마 이미 알고 있을 다각형 메시(polygon mesh)입니다. 물체 표면을 구성하는 다각형의 정점 수를 임의로 허용하는 것이 편리한 상황도 있습니다. 특히 가져오기나 편집 도구에서 그렇습니다. 그러나 실시간 렌더링을 위한 현대 하드웨어는 모든 다각형이 삼각형인 삼각형 메시에 최적화되어 있습니다.
각 다각형을 따로 삼각형들로 분해하면 어떤 다각형 메시든 같은 모양의 삼각형 메시로 바꿀 수 있습니다. 9.7.3절에서 이를 간략히 다뤘습니다. 삼각형 하나와 다각형 하나에 관한 중요한 개념은 각각 9.6절과 9.7절에서 설명했으므로, 여기서는 여러 삼각형을 메시 안에서 연결하는 방법에 집중합니다.
삼각형 메시를 저장하는 아주 직접적인 방법은 코드 10.4처럼 삼각형 배열을 사용하는 것입니다.
코드 10.4 — 삼각형 메시의 단순한 표현
struct Triangle {
Vector3 vertPos[3]; // 정점 위치
};
struct TriangleMesh {
int triCount; // 삼각형 개수
Triangle *triList; // 삼각형 배열
};
C++
복사
이 정도의 단순한 표현이면 충분한 프로그램도 있습니다. 그러나 ‘메시’라는 말에는 이웃 삼각형 사이의 연결이라는 뜻이 담겨 있고, 위 표현은 그 연결을 나타내지 않습니다. 삼각형 메시에는 기본적으로 세 종류의 정보가 있습니다.
•
정점(vertex): 각 삼각형에는 정확히 세 정점이 있습니다. 하나의 정점은 여러 삼각형이 공유할 수 있습니다. 정점의 차수(valence)는 그 정점에 연결된 면의 수입니다.
•
모서리(edge): 두 정점을 연결합니다. 각 삼각형에는 세 모서리가 있습니다. 모서리 하나를 정확히 두 면이 공유하는 경우가 많지만, 예외도 있습니다. 물체가 닫혀 있지 않으면 이웃 면이 하나뿐인 열린 모서리가 있을 수 있습니다.
•
면(face): 삼각형의 표면입니다. 면은 세 정점의 목록이나 세 모서리의 목록으로 저장할 수 있습니다.
메시에서 어떤 연산을 가장 자주 수행하느냐에 따라, 이 정보를 효율적으로 표현하는 여러 방법이 있습니다. 여기서는 인덱스 삼각형 메시(indexed triangle mesh)라는 표준적인 저장 형식을 다룹니다.
10.4.1 인덱스 삼각형 메시
인덱스 삼각형 메시는 정점 목록과 삼각형 목록, 두 목록으로 구성합니다.
•
각 정점은 3차원 위치를 포함합니다. 텍스처 매핑 좌표, 표면 법선, 조명 값 등의 정보도 정점별로 저장할 수 있습니다.
•
각 삼각형은 정점 목록의 항목을 가리키는 정수 세 개로 표현합니다. 면에 ‘앞’과 ‘뒤’가 있다고 생각할 수 있으므로, 보통 정점을 나열하는 순서가 중요합니다. 이 책에서는 앞에서 봤을 때 시계 방향으로 정점을 나열하는 왼손 규약을 사용합니다. 미리 계산한 면의 법선, 텍스처 맵 같은 표면 속성 등 다른 정보도 삼각형별로 저장할 수 있습니다.
코드 10.5는 C에서 인덱스 삼각형 메시를 저장하는 방법을 크게 단순화한 예입니다.
코드 10.5 — 인덱스 삼각형 메시
// Vertex는 정점별로 저장하는 정보입니다.
struct Vertex {
// 정점의 3차원 위치
Vector3 pos;
// 텍스처 매핑 좌표, 표면 법선,
// 조명 값 등을 더 저장할 수 있습니다.
};
// Triangle은 삼각형별로 저장하는 정보입니다.
struct Triangle {
// 정점 목록을 가리키는 인덱스.
// 원문 집필 당시에는 메모리와 대역폭을 줄이기 위해
// 실제 구현에서 32비트보다 16비트 인덱스를 거의 항상 썼습니다.
int vertexIndex[3];
// 법선이나 재질 정보 등을 더 저장할 수 있습니다.
};
// TriangleMesh는 인덱스 삼각형 메시를 저장합니다.
struct TriangleMesh {
// 정점들
int vertexCount;
Vertex *vertexList;
// 삼각형들
int triangleCount;
Triangle *triangleList;
};
C++
복사
그림 10.11은 정육면체와 피라미드를 다각형 메시 또는 삼각형 메시로 표현하는 예입니다. 두 물체는 총 13개의 정점을 가진 하나의 메시에 속합니다. 밝고 굵은 선은 다각형의 윤곽이고, 짙은 녹색의 가는 선은 다각형을 삼각형으로 나누기 위해 추가할 수 있는 모서리의 한 예입니다.
그림 10.11 — 정육면체와 피라미드를 포함하는 단순한 메시
원점을 두 물체 사이의 ‘지면’에 둔다면, 정점 좌표는 표 10.3처럼 정할 수 있습니다.
표 10.3 — 예제 메시의 정점 위치
인덱스 | 위치 | 인덱스 | 위치 | 인덱스 | 위치 | 인덱스 | 위치 |
0 | (−3,2,1) | 4 | (−3,0,1) | 8 | (2,2,0) | 12 | (1,0,−1) |
1 | (−1,2,1) | 5 | (−1,0,1) | 9 | (1,0,1) | ||
2 | (−1,2,−1) | 6 | (−1,0,−1) | 10 | (3,0,1) | ||
3 | (−3,2,−1) | 7 | (−3,0,−1) | 11 | (3,0,−1) |
표 10.4는 같은 메시를 다각형 메시와 삼각형 메시로 각각 표현했을 때 면을 구성하는 정점 인덱스를 보여줍니다. 정점의 순서가 중요하다는 점을 기억하세요. 바깥에서 봤을 때 시계 방향입니다. 그림과 표를 충분히 살펴보고 확실히 이해하고 넘어가세요.
표 10.4 — 예제 메시의 면을 구성하는 정점 인덱스
면 | 다각형 메시 | 삼각형 메시 |
정육면체 위 | {0,1,2,3} | {1,2,3}, {1,3,0} |
정육면체 앞 | {2,6,7,3} | {2,6,7}, {2,7,3} |
정육면체 오른쪽 | {2,1,5,6} | {2,1,5}, {2,5,6} |
정육면체 왼쪽 | {0,3,7,4} | {0,3,7}, {0,7,4} |
정육면체 뒤 | {0,4,5,1} | {0,4,5}, {0,5,1} |
정육면체 아래 | {4,7,6,5} | {4,7,6}, {4,6,5} |
피라미드 앞 | {12,8,11} | {12,8,11} |
피라미드 왼쪽 | {9,8,12} | {9,8,12} |
피라미드 오른쪽 | {8,10,11} | {8,10,11} |
피라미드 뒤 | {8,9,10} | {8,9,10} |
피라미드 아래 | {9,12,11,10} | {9,12,11}, {9,11,10} |
정점은 면 둘레를 따라 시계 방향이어야 하지만, 어느 정점을 ‘첫 번째’로 삼는지는 상관없습니다. 순환 이동해도 메시의 논리적 구조는 같습니다. 예를 들어 정육면체 윗면은 {1,2,3,0}, {2,3,0,1}, {3,0,1,2}로 적어도 같습니다.
코드 10.5의 주석처럼 실제로는 거의 언제나 정점마다 텍스처 좌표, 표면 법선, 기저 벡터, 색상, 스키닝 데이터 등의 추가 정보를 저장합니다. 각 정보는 그것을 사용하는 기법을 설명하는 뒤의 절에서 다룹니다. 삼각형마다 사용할 재질의 인덱스나 면의 평면 방정식도 저장할 수 있습니다. 평면 방정식의 일부가 표면 법선이라는 것은 9.5절에서 보았습니다. 이런 정보는 편집 기능이나 소프트웨어에서 메시를 조작하는 도구에 매우 유용합니다.
그러나 실시간 렌더링에서는 삼각형별로 정점 인덱스 세 개 외의 데이터를 저장하는 일이 드뭅니다. 가장 흔한 방법은 struct Triangle조차 두지 않고, 전체 삼각형 목록을 unsigned short triList[] 같은 배열로 표현하는 것입니다. 배열 길이는 삼각형 개수의 세 배입니다. 속성이 같은 삼각형들을 배치로 묶으면 전체 배치를 이 최적화된 형태로 GPU에 전달할 수 있습니다.
정점별 추가 데이터가 필요한 여러 개념을 살펴본 뒤, 10.10.2절에서 그래픽스 API에 데이터를 전달하는 구체적인 예를 더 보겠습니다.
참고로 렌더링과 편집에 같은 메시 클래스를 억지로 사용하지 않는 편이 일반적으로 훨씬 수월합니다. 요구 사항이 매우 다르기 때문입니다. 도구나 임포터에는 크기가 좀 더 크더라도 유연한 자료구조가 적합합니다.
인덱스 삼각형 메시에서는 모서리를 명시적으로 저장하지 않습니다. 삼각형 목록에는 인접 정보가 암묵적으로 담겨 있어서, 공유 모서리를 찾으려면 목록을 검색해야 합니다. 코드 10.4의 단순한 삼각형 배열에는 논리적인 연결 정보 자체가 없었습니다. 물론 정점 위치나 다른 속성을 비교해서 모서리 양끝의 정점이 같은지 알아내려 할 수는 있습니다.
흥미롭게도 인덱스 표현은 연결이라는 ‘추가’ 정보를 담으면서도, 대부분의 경우 단순한 배열보다 메모리를 적게 사용합니다. 정점별 정보는 정수 인덱스 하나보다 훨씬 크기 때문입니다. 최소한 3차원 위치 벡터는 저장해야 합니다. 실제 메시에서 정점 하나의 차수는 보통 3~6 정도이므로, 단순한 배열에서는 꽤 많은 데이터가 중복됩니다.
이 단순한 인덱스 삼각형 메시 방식은 렌더링을 비롯한 여러 용도에 적합합니다. 그러나 일부 연산을 효율적으로 구현하려면 더 발전된 자료구조가 필요합니다. 기본 문제는 삼각형 사이의 인접 관계가 명시적으로 표현되지 않아 목록을 검색해 알아내야 한다는 점입니다. 이 정보를 상수 시간에 얻을 수 있는 다른 표현법도 있습니다.
한 방법은 모서리 목록을 직접 관리하는 것입니다. 각 모서리를 양끝의 두 정점으로 정의하고, 그 모서리를 공유하는 삼각형 목록도 관리합니다. 그러면 삼각형을 세 정점 대신 세 모서리의 목록으로 볼 수 있고, 정점 목록 대신 모서리 목록의 인덱스 세 개로 저장합니다. 이를 확장한 윙드 에지(winged-edge) 모델 [22]은 각 정점에 그 정점을 사용하는 모서리 하나의 참조도 저장합니다.
모서리와 삼각형을 적절히 순회하면 해당 정점을 사용하는 모든 모서리와 삼각형을 빠르게 찾을 수 있습니다.
좌표가 같다는 것과 같은 정점이라는 것
인덱스가 같으면 동일한 정점 데이터를 공유합니다. 위치가 같더라도 법선이나 UV가 달라야 한다면 별도의 정점으로 저장할 수 있습니다. 바로 다음 절의 상자 예제가 이 차이를 보여줍니다.
예를 들어 정점 데이터가 32바이트이고 하나를 여섯 삼각형이 공유한다면, 단순 배열은 그 정점 정보를 192바이트 저장합니다. 인덱스 방식에서는 데이터 32바이트와 16비트 인덱스 여섯 개 12바이트, 총 44바이트로 표현할 수 있습니다. 이는 원리를 설명하는 국소적인 계산이며, 실제 총량은 전체 메시의 정점·인덱스 구성에 따라 달라집니다.
10.4.2 표면 법선
표면 법선은 그래픽스에서 여러 목적으로 쓰입니다. 올바른 조명 계산(10.6절)과 뒷면 제거(10.10.5절)가 그 예입니다. 일반적으로 표면 법선은 표면에 수직인 단위벡터입니다.(원주 10-10) 특정 면의 법선을 구하고 싶다면, 관심 있는 표면은 그 면을 포함하는 평면입니다. 다각형의 표면 법선은 9.5절의 방법으로 쉽게 구할 수 있습니다.
정점별 법선은 조금 더 까다롭습니다. 엄밀하게 말하면 정점이나 모서리에서는 다각형 메시 표면이 불연속이므로 진정한 하나의 표면 법선이 없습니다. 대신 렌더링할 때는 보통 다각형 메시를 어떤 매끄러운 표면의 근사라고 해석합니다. 따라서 필요한 것은 메시가 정의하는 조각별 선형 표면의 법선이 아니라, 그 메시가 근사하는 매끄러운 표면의 법선에 대한 근사값입니다.
정점 법선의 주된 목적은 조명입니다. 사실상 모든 조명 모델은 조명을 계산할 지점의 표면 법선을 입력으로 받습니다. 법선은 렌더링 방정식 자체의 램버트 인자에도 들어가므로, BRDF가 법선에 의존하지 않더라도 항상 입력으로 필요합니다. 그런데 법선은 정점에서만 주어져 있고 조명은 표면 전체에 걸쳐 계산해야 합니다. 어떻게 할까요?
하드웨어 자원이 허락한다면, 그리고 원문 집필 당시에는 대개 가능했듯이, 정점 법선을 보간하고 그 결과를 다시 정규화하여 면 위의 임의 지점에서 연속 표면의 법선을 근사할 수 있습니다. 그림 10.12는 이 방법을 보여줍니다. 검은 원으로 표시한 원기둥의 단면을 파란 윤곽의 육각기둥으로 근사합니다. 정점의 검은 법선은 실제 원기둥의 법선이고, 정점 사이의 법선들은 보간한 근사값입니다. 실제 계산에는 이 벡터들을 늘여 단위 길이로 만든 값을 사용합니다.
그림 10.12 — 육각기둥으로 근사한 원기둥과 보간된 법선
어떤 점의 법선을 얻으면 픽셀마다 완전한 조명 방정식을 계산할 수 있습니다. 이를 픽셀별 셰이딩(per-pixel shading)이라고 합니다.(원주 10-11) 다른 방법인 구로 셰이딩(Gouraud shading) [26]은 정점에서만 조명을 계산하고, 법선 대신 조명 계산의 결과를 면 위로 보간합니다.(원주 10-12) 계산량이 더 적으며 Nintendo Wii 같은 일부 시스템에서는 이 방법을 사용합니다.
그림 10.13은 측면 수가 서로 다른 기둥에 픽셀별 조명을 적용한 결과입니다. 끝부분에서는 실루엣이 적은 다각형 수를 드러내어 착시가 깨지지만, 매끄러운 표면을 근사하는 이 방법은 아주 거친 메시도 ‘매끄럽게’ 보이게 합니다. 기둥의 양끝을 가리면 옆면이 다섯 개인 기둥도 상당히 그럴듯합니다.
그림 10.13 — 측면 수가 서로 다른 기둥으로 원기둥 근사하기
법선을 보간하여 곡면을 근사적으로 복원하는 방법을 알았으니, 정점 법선을 얻는 방법을 살펴봅시다. 메시가 어떻게 만들어졌느냐에 따라 이 정보가 바로 주어지지 않을 수 있습니다. 매개변수 곡면으로부터 절차적으로 메시를 생성한다면 그때 법선도 제공할 수 있습니다. 모델링 도구가 메시의 일부로 법선을 넘겨줄 수도 있습니다.
그러나 법선이 주어지지 않으면, 가진 정보인 정점 위치와 삼각형만으로 추정해야 합니다. 한 가지 방법은 이웃 삼각형의 법선을 평균 내고 결과를 다시 정규화하는 것입니다. 코드 10.6이 이 고전적인 기법을 보여줍니다.
코드 10.6 — 이웃 면의 법선을 평균 내어 정점 법선 계산하기
struct Vertex {
Vector3 pos;
Vector3 normal;
};
struct Triangle {
int vertexIndex[3];
Vector3 normal;
};
struct TriangleMesh {
int vertexCount;
Vertex *vertexList;
int triangleCount;
Triangle *triangleList;
void computeVertexNormals() {
// 먼저 모든 정점 법선을 0으로 만듭니다.
for (int i = 0; i < vertexCount; ++i) {
vertexList[i].normal.zero();
}
// 면의 법선을 그 면에 연결된 정점 법선에 더합니다.
for (int i = 0; i < triangleCount; ++i) {
// 편리하게 접근할 참조
Triangle &tri = triangleList[i];
// 삼각형 법선을 계산합니다.
Vector3 v0 = vertexList[tri.vertexIndex[0]].pos;
Vector3 v1 = vertexList[tri.vertexIndex[1]].pos;
Vector3 v2 = vertexList[tri.vertexIndex[2]].pos;
tri.normal = cross(v1-v0, v2-v1);
tri.normal.normalize();
// 인접한 정점에 합산합니다.
for (int j = 0; j < 3; ++j) {
vertexList[tri.vertexIndex[j]].normal += tri.normal;
}
}
// 마지막으로 평균 방향을 정규화합니다.
// 어떤 삼각형에도 속하지 않는 고립 정점이나
// 일부 다른 상황에서는 이 코드가 실패할 수 있습니다.
for (int i = 0; i < vertexCount; ++i) {
vertexList[i].normal.normalize();
}
}
};
C++
복사
면의 법선을 평균 내는 방법은 오랫동안 쓰여 왔고 대부분의 경우 잘 작동합니다. 그러나 몇 가지 주의할 점이 있습니다. 첫째, 원래 불연속이어야 하는 부분까지 잘못 ‘매끄럽게’ 만들 수 있습니다. 아주 단순한 상자를 생각해 봅시다. 모서리에서는 조명이 날카롭게 달라져야 합니다. 하지만 면 법선의 평균으로 정점 법선을 만들면 그림 10.14처럼 조명의 불연속이 사라집니다.
그림 10.14 — 오른쪽 상자는 각 꼭짓점에 법선이 하나뿐이므로 모서리의 조명 경계가 사라집니다
표 10.5 — 정점을 공유하여 모서리가 매끄럽게 보이는 상자의 다각형 메시
정점 | 위치 |
0 | (−1,+1,+1) |
1 | (+1,+1,+1) |
2 | (+1,+1,−1) |
3 | (−1,+1,−1) |
4 | (−1,−1,+1) |
5 | (+1,−1,+1) |
6 | (+1,−1,−1) |
7 | (−1,−1,−1) |
면 | 인덱스 |
--- | --- |
위 | {0,1,2,3} |
앞 | {2,6,7,3} |
오른쪽 | {2,1,5,6} |
왼쪽 | {0,3,7,4} |
뒤 | {0,4,5,1} |
아래 | {4,7,6,5} |
근본적인 문제는 정점마다 법선을 하나만 저장하면 상자 모서리의 표면 불연속을 제대로 표현할 수 없다는 점입니다. 해결책은 면들을 분리(detach)하는 것입니다. 실제 기하학적 불연속이 있는 모서리에서 정점을 복제하여 위상적인 불연속을 만들고, 정점 법선들이 함께 평균되는 것을 막습니다.
그러면 면들은 논리적으로 연결되지 않게 되지만, 메시 연결 구조의 이 이음매는 렌더링이나 레이 트레이싱 같은 많은 중요한 작업에서 문제가 되지 않습니다. 표 10.5의 정점 8개짜리 매끄러운 상자와, 면을 분리해 정점이 24개가 된 표 10.6을 비교해 보세요.
표 10.6 — 면들을 분리하여 모서리에서 조명이 불연속인 상자의 다각형 메시
정점 | 위치 |
0 | (−1,+1,+1) |
1 | (+1,+1,+1) |
2 | (+1,+1,−1) |
3 | (−1,+1,−1) |
4 | (−1,+1,−1) |
5 | (+1,+1,−1) |
6 | (+1,−1,−1) |
7 | (−1,−1,−1) |
8 | (+1,+1,−1) |
9 | (+1,+1,+1) |
10 | (+1,−1,+1) |
11 | (+1,−1,−1) |
12 | (−1,+1,+1) |
13 | (−1,+1,−1) |
14 | (−1,−1,−1) |
15 | (−1,−1,+1) |
16 | (+1,+1,+1) |
17 | (−1,+1,+1) |
18 | (−1,−1,+1) |
19 | (+1,−1,+1) |
20 | (+1,−1,−1) |
21 | (−1,−1,−1) |
22 | (−1,−1,+1) |
23 | (+1,−1,+1) |
면 | 인덱스 |
--- | --- |
위 | {0,1,2,3} |
앞 | {4,5,6,7} |
오른쪽 | {8,9,10,11} |
왼쪽 | {12,13,14,15} |
뒤 | {16,17,18,19} |
아래 | {20,21,22,23} |
극단적인 경우는 두 면을 서로 등을 맞대어 놓을 때입니다. 이런 무한히 얇은 양면 기하 구조는 나뭇잎, 천, 빌보드 등에서 나타납니다. 두 법선이 정확히 반대이므로 평균은 영벡터가 되고, 영벡터는 정규화할 수 없습니다. 가장 간단한 해결책은 면을 분리하여 정점 법선이 함께 평균되지 않게 하는 것입니다.
또는 앞뒤가 서로 거울상이라면 단면 다각형 두 개를 양면 다각형 하나로 바꿀 수 있습니다. 이 경우 렌더링할 때 뒷면 제거(10.10.5절)를 끄고, 조명 방정식에서 법선의 방향을 적절히 처리해야 합니다.
좀 더 미묘한 문제도 있습니다. 동일한 법선을 가진 삼각형 수가 많으면 평균이 그 방향으로 치우칩니다. 그림 10.11의 인덱스 1번 정점을 보세요. 윗면의 삼각형 두 개와 연결되지만, 오른쪽 면과 뒤쪽 면에서는 각각 삼각형 하나와만 연결됩니다. 삼각형 법선을 평균하면 윗면의 법선이 각 옆면보다 두 배 많은 ‘표’를 얻어 결과가 편향됩니다.
그러나 이 연결 구조는 사각형을 삼각형으로 나눌 때 어디에 선을 긋느냐는 임의의 결정에서 나왔습니다. 윗면의 대각선을 0번과 2번 정점 사이에 긋도록 바꾸면, 즉 모서리를 ‘뒤집으면’, 윗면의 모든 법선이 달라집니다.
이 문제를 다루는 기법도 있습니다. 예를 들어 각 인접 면이 기여하는 양에 해당 정점의 내각을 가중치로 줄 수 있습니다. 하지만 실제로는 문제를 무시하는 경우도 많습니다. 심각한 예는 대개 지금의 상자처럼 일부러 만든 사례이고, 그런 경우는 애초에 면을 분리해야 하기 때문입니다. 또한 법선 자체가 근사값이므로 방향이 약간 달라져도 시각적으로 구별하기 어려울 때가 많습니다.
일부 모델링 도구는 정점 법선을 제공하지만 범프 매핑에 필요한 기저 벡터까지 주는 도구는 더 적습니다. 10.9절에서 보겠듯 정점의 기저 벡터를 만들어 내는 기법은 지금 설명한 것과 비슷합니다.
넘어가기 전에 표면 법선에 관한 매우 중요한 사실이 있습니다. 어떤 상황에서는 위치를 변환하는 행렬로 법선을 그대로 변환할 수 없습니다. 이는 법선을 위치처럼 평행이동해서는 안 된다는 문제와는 별개입니다. 이유는 법선이 공변 벡터(covariant vector)이기 때문입니다.
위치나 속도 같은 ‘보통’ 벡터는 반변(contravariant)이라고 합니다. 벡터를 기술하는 좌표 공간의 단위 크기를 바꾸면 좌표가 반대 방향으로 반응합니다. 더 큰 단위, 예를 들어 피트 대신 미터를 사용하면 반변 벡터의 좌표값은 더 작아집니다. 이 설명은 크기 변화에 관한 것으로, 평행이동이나 회전은 논점이 아닙니다. 쌍대 벡터(dual vector)라고 부르는 법선과 다른 종류의 기울기는 이런 식으로 반응하지 않습니다.
그림 10.15 — 비균일 확대·축소에서의 법선 변환. 연한 빨강은 물체와 같은 행렬로 변환한 법선, 진한 빨강은 그것을 정규화한 결과, 연한 파랑은 올바른 법선입니다
그림 10.15처럼 원 같은 2차원 물체를 가로로 늘린다고 생각해 봅시다. 오른쪽 그림의 연한 파랑 법선은 더 수직인 방향으로 돌아갑니다. 위치의 가로 좌표 절댓값은 커지는데 법선의 가로 성분 절댓값은 작아집니다. 좌표 공간을 고정하고 물체를 늘리는 효과는, 물체의 크기를 고정하고 좌표 공간의 단위 크기를 줄이는 효과와 같습니다. 법선의 좌표가 좌표 공간의 단위 크기와 같은 방향으로 바뀌므로 공변 벡터라고 부릅니다.
표면 법선을 올바르게 변환하려면 위치 변환 행렬의 역전치(inverse transpose)를 사용해야 합니다. 행렬을 전치하고 역행렬을 구한 결과입니다. 전치를 먼저 하든 역행렬을 먼저 구하든 같으므로 다음처럼 로 쓰기도 합니다.
변환 행렬에 확대·축소나 기울임이 없다면 행렬은 정규직교 행렬이고, 역전치가 원래 행렬과 같습니다. 이 경우 같은 행렬로 법선을 변환해도 안전합니다. 균일 확대·축소가 포함되어 있어도 역전치를 따로 적용하지 않을 수 있지만, 변환 후 법선을 다시 정규화해야 합니다. 비균일 확대·축소나 기울임이 들어 있으면 역전치 행렬로 법선을 변환한 다음 다시 정규화해야 합니다. 기울임은 비균일 확대·축소와 회전을 결합한 것과 구별되지 않습니다.
원문 강조 상자: 일반적으로 법선에는 위치 변환 행렬의 역전치를 적용해야 합니다. 확대·축소가 없으면 같은 행렬을 사용해도 됩니다. 균일 확대·축소가 있으면 변환 뒤 정규화하면 됩니다. 비균일 확대·축소가 있으면 역전치 행렬을 사용하고, 변환한 뒤 다시 정규화해야 합니다.
원주 10-10: 일부 상황에서는 단위 길이가 엄밀히 필수인 것은 아니지만, 실제로는 거의 항상 단위벡터를 사용합니다.
원주 10-11: 정점 법선을 보간하는 이 기법을 혼동하기 쉽게 퐁 셰이딩(Phong shading)이라고 부르기도 합니다. 정반사에 사용하는 퐁 모델과 구별해야 합니다.
원주 10-12: Gouraud의 영어 발음은 ‘거-로우(guh-ROH)’에 가깝습니다.
정규화만으로 고칠 수 없는 이유
법선의 핵심 조건은 접선과 수직이라는 것입니다. 열벡터 규약에서 접선 t를 로 옮겼다면, 을 법선으로 택할 때 다음이 보존됩니다.
가령 원래 접선이 (1,−1), 법선이 (1,1)이고 가로만 두 배 늘리면 새 접선은 (2,−1)입니다. 법선도 같은 방식으로 (2,1)로 바꾸면 내적이 3이라 수직이 아닙니다. 그 벡터를 정규화해도 방향 오류는 남습니다. 역전치를 적용한 (1/2,1)은 새 접선과 내적이 0이며, 그다음 단위 길이로 만들면 됩니다.
코드 메모: 코드 10.6은 원문 그대로의 교육용 알고리즘입니다. 실제 구현에서는 퇴화 삼각형, 사용되지 않는 정점, 합이 0인 법선을 별도로 처리해야 합니다. 표 10.5의 0.577은 의 근사값입니다. 삼각형을 나누는 방식에 따른 편향까지 자동으로 해결한 값이라는 뜻은 아닙니다.









