자료보관함‎ > ‎GDNet‎ > ‎

높이필드 조명용 법선계산


그림 1. 정점 법선을 평균화해 가중치가 적용된 표면 법선로 렌더링한 고해상도 높이필드

소개

이 글을 쓰는 이유


게임 업계에서 몇년 동안 작업해온 프로젝트를 끝내고 몇달전에 GameDev.net 커뮤니티로 돌아온 이후 초보자 포럼과 DirectX 포럼란에서 사람들의 질문에 대답하며 대부분의 시간을 보냈습니다. 그 동안 여러번이나 반복해서 나온 질문중 하나가 조명계산을 위해 높이필드의 법선들을 어떻게 계산하는 것인가였습니다. 이 질문에 서너번이나 대답을 단 이후에 저는 이 글을 쓰기로 마음 먹었습니다. 이 글이 모든 사람들에게 도움이 되기를 바랍니다.

이 글의 목적은 표면 및 정점 법선을 계산하는 사용되는 가장 일반적인 알고리즘을 설명하는 것입니다. 가장 일반적인 알고리즘을 살펴본 다음에 가장 일반적인 알고리즘 두개를 자세히 살펴볼것입니다. 세부적으로 이러한 방법이 왜 효과적인지에 대한 논리적 분석, 저희가 높이필드에 알고 있는 것에 기초하여 수학을 단순화시킬 수 있는 방법, 그리고 특정 알고리즘의 성능비교 등을 포함할 것입니다. 따라서 이 글을 다 읽으신 독자분들은 높이필드에 대해 잘 알게 될것이며 여러분이 필요한 시각효과와 성능에 가장 적합한 방법에 익숙할 것이므로 법선계산을 별로 어렵게 생각하지 않으실 것입니다.

표기법


이 글에서 사용할 수학표기법은 다음과 같습니다. 참고로 변수와 숫자 사이, 또는 변수 사이에 연산자가 생략되어 있다면 이것은 곱하기입니다.


'''굵은 글씨''':     굵은 글씨는 언제나 벡터입니다.
'''| VecA |'''      수직 바들은 그 안에 있는 것의 크기를 뜻합니다. 즉, 이 경우에는 벡터 '''A'''의 크기입니다.
2 * '''B'''         *는 두 항의 곱셈을 나타냅니다. 간단히 2'''B'''로 쓰기도 합니다.
'''B''' / 2         / 연산자는 두 항의 나눗셈을 나타냅니다.
'''A''' × '''B'''         엑스자는 두 항의 외적을 나타냅니다.
'''A''' · '''B'''         점은 두 항의 내적(점곱)을 나타냅니다.
'''θ'''             세타는 이 글에서 각도를 나타내는 그리스 문자입니다.
'''Σ'''             시그마는 여러 항의 합을 나타내는 그리스 문자입니다.
'''Δ'''             델타는 어떤 변수의 변화를 나타내는 그리스 문자입니다.

===데모 프로그램===

이 글의 성능계산, 결과, 논의 등은 저의 높이필드 법선 계산과 렌더러의 연구로부터 모은 실험적인 증거에 기초합니다. 부록 A에서 그 데모 프로그램 링크, 소스코드, 사용키 목록 등을 찾아보실 수 있으실 것입니다. 데모 프로그램이나 이 글에 대한 수정사항, 의견, 응답등은 GameDev.net의 개인메시지 기능을 이용하여 Jeromy Walsh (jwalsh)에게 보내주십시요. 참고로 이 데모의 소스코드는 오픈소스입니다. 자신의 필요에 맞게 수정 및 배포를 하실 수 있습니다. 파일 젤 윗부분에 저작권정보만 지우지 말아주십시요.


==다면체 법선계산의 짧은 역사==
===다양한 방법들과 그 방법들의 창시자===
컴퓨터 그래픽이라는 학문은 약 40년밖에 되지 않은 비교적 새로운 분야입니다. 이 분야를 개척했던 수학자와 컴퓨터공학자 중 프랑스 출신 컴퓨터공학자 헨리 고라우드가 있습니다. 고라우드는 고라우드 쉐이딩이라고 불리기도 하는 부드러운 쉐이딩을 만든사람으로 우리에게 알려져 있습니다. 1971년 고라우드는 "곡면의 연속적인 쉐이딩"이란 책을 펴냅니다. 이 책에서 고라우드는 컴퓨터 법선을 계산하는 가장 일반적인 방법을 기술했습니다.. 앞으로 이 글에서는 그의 알고리즘을 "동일가중평균" (MWE)라고 부를 것입니다. 본질적으로 고라우드는 다면체의 각 면의 법선들을 계산해서 한 정점에 "연결"된 모든 면들을 취해 표면 법선들을 평균화 하면 비교적 부드러운 쉐이딩을 얻을 수 있다고 제안하였습니다. 이 방법에서는 각 표면법선이 정점 법선(다면체에서의 각 정점의 법선을 나타내는 벡터)에 고르게 기여합니다.


고라우드의 알고리듬은 20년이상 정점마다의 조명을 계산하는 사실상의 표준 알고리즘이었습니다. 하지만 1997년 두명의 독일 컴퓨터 공학자인 Thürmer와 Wüthrich가 보다 수학적으로 정확하게 법선을 계산하는 새로운 방법 "각에 따른 가중평균 (WMA)"이라 불리는 이 알고리듬은 한 정점 법선에 "연결"된 면들이 정접법선에 어느정도씩 공헌해야 한다고 제안했습니다. 이 때 그 정점을 가지고 있는 삼각형들의 각에 따라 정점법선을 가중합니다.다시말해 삼각형을 이루는 세 정점들은 모두 동일한 표면법선을 가지지만 이 삼각형의 각 각을 표면법선에 곱하여 3개의 새로운 법선을 만들 수 있다는 것입니다. 물론 이것은 표면법선을 가중한 버전이죠. 이제 이전에 사용했던 표면법선 대신에 이 법선들을 사용하여 새로운 정점 법선을 계산할 수 있습니다.


Thürmer와 Wüthrich가 새로운 알고리즘을 제안한 뒤 얼마 지나지 않아 Nelson Max란 사람이 4개의 새로운 알고리즘을 그래픽스 툴스 저널에 기고했습니다. 이 글에서 이들의 이름을 나열하긴 할 것이지만 이 방법들이 WMA만큼 정확하거나 MWE만큼 빠르지는 않습니다. 따라서 이 방법들을 깊게 살펴보진 않을 것입니다. "사인 및 모서리 길이비에 따른 가중평균", "인접 삼각형의 면에 따른 가중평균", "모서리 길이비에 따른 가중평균", "모서리 길이비의 제곱근에 따른 가중평균"

==높이필드==
===높이필드의 용도===

높이필드는 비디오 게임에서 하나의 주요한 역할을 담당합니다. 플레이어들이 밟고 다니는 지형이 그것입니다. 여러분이 만드는 게임의 장르에 따라 플레이어들이 그 지형을 쳐다보는데 상당한 양의 시간을 보낼 수도 있습니다. 이런 것을 염두에 둔다면 사실적, 매력적으로 보이는 지형을 만들 필요가 있습니다. 이를 가능하게 만드는 가장 효율적인 방법은 조명, 그림자, 텍스처 등을 사용하는 것입니다. 이 글은 조명에 주로 초점을 맞추므로 다른 것들에 대해서는 나중에 따로 시간을 내보도록 하지요. 자 이제 높이필드를 보다 자세히 살펴봅시다.


===높이필드의 일반법칙===
아주 간단히 말하자면 높이필드는 공간에서 특정 (x, z) 점에 있는 지형 한조각의 높이를 나타내는 1차원 또는 2차원 배열의 데이터일 뿐입니다. 달리 말해서 한 2차원 배열에서 [x][z]에 있는 원소를 보거나 1차원 배열에서 그에 해당하는 색인(index)를 계산하면 메모리의 그 위치에 있는 값이 X, Z 점에서의 높이일 것입니다. 1차원 배열의 색인은 다음의 공식으로 계산할 수 있습니다. numVertsWide는 여러분의 지형에서 x 좌표를 따라 존재하는 점들의 수 입니다.


색인 ═ z * numVertsWide + x

메모리에 있는 값은 부동소수점이나 정수를 사용하여 저장할 수 있습니다. 정수의 경우 이 값들은 보통 1바이트 (8비트)나 4바이트 (32비트) 메모리 블럭에 저장됩니다. 높이 값을 8비트 정수로 저장할 때엔 높이필드를 그레이스케일 비트맵의 형태로 쉽게 디스크에 저장하고 읽어올 수 있습니다. 이것은 표준 이미지 편집 프로그램을 지형 오프라인을 수정할 수 있게 해줍니다. 32비트 값으로도 동일한 일을 할 수 있으나 알파값과 색상들이 이미지 편집 프로그램을 사용하기에는 다소 덜 직관적일 것입니다.


이 값들을 8비트 정수로 저장할 때엔 이 높이 값을 수직 확대 구성요소로 곱하는 것이 일반적입니다. 이는 높이필드가 255보다 높고 낮은 고도를 커버할 수 있게 해줍니다. 하지만 지형의 크기에 따라 8비트 정수들이 비사실적이고 각이져 보일 수도 있습니다. 이 글과 데모에서는 높이를 나타내기 위해 단정도(single precision) 32비트 부동소수점을 사용할 것입니다.



8비트 높이필드의 경우 수직 확대 배율 이외에도 수평 확대 배율을 가지는 것도 일반적입니다. 그 이유는 매우 간단합니다. x, z 쌍의 색인을 가질 경우 이것은 여러분의 모든 정잠들이 1미터(세계 단위) 간격 안에 존재하게끔 만듭니다(즉, X = 0, 1, 2, 3….Z = 0, 1, 2, 3). 높이에 8비트값을 사용하면서 정점들을 1미터 간격으로 배치하는 것이 지형을 비사실적으로 보이게 만드는 경우가 많습니다. 가끔 "축적" 또는 "정점당 단위"라고 불리는 수평 확대배율 1.0보다 작거나 큰 간격으로 정점들을 배치할 수 있게 해줍니다. 예를 들어 0.5f의 축적을 쓴다면 모든 정점들이 0.5 미터 떨어져 동일한 공간안에 보다 많은 정점들이 존재할 것이고 여러분의 지형은 그전보다 높은 "해상도"를 가질 것입니다. "축적"과 "해상도"는 반비례 관계입니다. 축적이 증가하면 정점간의 사이가 멀어지고 해상도, 또는 지형의 복잡도는 줄어듭니다.


마지막으로 이 글에서는 높이필드를 부동소수점 값의 1차원 배열로 표현하지만 추리기(culling) 및 렌더링을 고려하여 큰 높이필드를 작은 "타일"들로 나누는 것도 일반적입니다. 이것이 여러분이 시도해보고 싶은 것이라면 이 글에 있는 높이필드를 큰 시스템의 단일 타일로 생각해보실수 있습니다. 궁극적으로 높이필드의 타일을 깐다는 것은 타일과 정점을 매핑한다는 뜻이며, 또한 정점 계산이 현재 타일에 인접해 있는 정점들의 높이 값을 고려해야한다는 것을 의미합니다. 나중에 우리의 우리의 표면법선 계산 알고리즘이 타일방식을 이미 염두에 두고 있다는 사실을 발견하실 것입니다. 여기에 약간 손만 대면 타일기반 지형시스템에서 잘 작동하는 알고리즘을 만드실 수 있을 것입니다.

==높이필드용 법선계산==

<div align=center>
http://images.gamedev.net/features/programming/normalheightfield/image002.gif
</div>
===방법 1: 동일 가중평균 (MWE)===

동일 가중평균(Mean Weighted Equally)는 높이필드의 한 정점에서 법선을 계산하는 2단계 알고리듬입니다(정점법선). 첫번째 단계는 외적계산을 이용하여 높이필드에서 각 삼각형의 표면법선을 계산하는 것입니다. 이 계산된 표면법선은 여러분의 원하는 정점법선의 품질에 따라 정규화될 수도 아닐 수도 있습니다. 두번째 단계는 해당 점점에 연결된 모든 표면 법선을 더하고 그 결과 정점법선의 평균을 구하거나 정규화시키는 것입니다. 다음의 두 공식은 표면 및 정점 법선의 수학적 정의를 나타냅니다.


표면 법선: '''N<sub>s</sub>''' ═ '''A''' × '''B'''
정점 법선: '''N<sub>nwe</sub>''' = &Sigma; ('''N'''<sub>si</sub>) / 6.0f | i = 0…5

위의 표면법선 공식은 "표면 법선은 벡터 '''A'''를 벡터 '''B'''와 외적을 한것 같다."라고 볼 수 있습니다. 이 때, '''A'''와 '''B'''는 이 표면의 평면상에 있는 두 벡터입니다.


위의 정점법선 공식은 "정접 법선은 이 정점이 속해있는 모든 표면의 합을 6으로 나눈 것과 같습니다. 이 때 표면 법선 색인은 0에서 5 사이이다(경계값 포함)."라고 말할 수 있습니다. 고라우드의 알고리듬에서 한 정점에 연결되어 있는 표면은 최고 6개 라고 고라우드가 말한 것을 기억합시다. 하지만 이것은 모든 삼각형이 동일한 방향을 향하는 높이필드에만 적용되는 원칙입니다. 마찬가지로 위의 공식을 구현하기 위해 사용하는 알고리즘에 기초하여 각 정점은 정확히 6개의 표면에 연결되어 있을 것입니다. 저는 정점 법선의 계산을 간단하게 하고 속도를 높이기 위해 이런 일을 했습니다. 이제 공식들을 정의해봤으니 각각을 자세히 살펴보고 이들을 어떻게 높이필드에 적용할 수 있는지를 살펴봅시다. 물론 그 뒤에 이 공식들을 보다 효율적으로 만들 수 있는지 살펴보는 것도 잊지 말아야죠.


====표면법선의 계산====
{| width="100%"
|-
|style="vertical-align:top;" |

위의 표면법선 공식은 우선 표면상에 존재하는 2개의 벡터 '''A'''와 '''B'''를 필요로 합니다. 이 글에서 저희는 오른쪽의 그림과 같이 배치된 정점들로 높이필드를 사용한다고 가정합니다.


오른쪽의 그림에서 각 사각형(quad)는 2개의 삼각형으로 만들어져 있습니다. 하나는 삼각형의 왼쪽아래 다른 것은 오른쪽 위입니다. 이 글은 DirectX의 디폴트 방식인 왼손잡이 좌표계를 사용하고 저 삼각형들은 시계방향 순서대로 증가한다고 가정합니다. 한편 저흐의 높이필드가 xz평면상에 존재한다고 가정하셔도 좋습니다. 따라서 y가 높이를 나타내는 좌표입니다. 오른쪽의 다이어그램에서 사각형 0은 삼각형 0-3-1과 1-3-4로 이루어져 있습니다. 수학적으로 말하자면 평면 또는 표면은 3개의 점으로부터 구해질 수 있습니다. 삼각형의 두 면에서 형성되는 벡터들의 외적을 사용하면 됩니다.


| style="vertical-align:top;"|
http://images.gamedev.net/features/programming/normalheightfield/image004.gif
|}

왼손잡이 삼각형으로부터 법선 벡터를 구하는 공식:

'''A''' = P<sub>1</sub> – P<sub>0</sub>; '''B''' = P<sub>2</sub> – P<sub>0</sub>
'''N''' = '''A''' × '''B'''

{| width="100%"
|-
|style="vertical-align:top;" |

일례로 삼각형 031과 134, 그리고 (x, z)상의 함수인 높이값이 있다면 다음의 공식으로 삼각형의 법선을 계산할 수 있습니다.


// 삼각형 0 법선
'''A''' = 3 – 0; '''A''' = ( 0, h<sub>3</sub>, 1 ) – ( 0, h<sub>0</sub>, 0 )
'''B''' = 1 – 0; '''B''' = ( 1, h<sub>1</sub>, 0 ) – ( 0, h<sub>0</sub>, 0 )
'''N<sub>0</sub>''' = [ ( 0, h<sub>3</sub>, 1 ) – ( 0, h<sub>0</sub>, 0 ) ] × [( 1, h<sub>1</sub>, 0 ) – ( 0, h<sub>0</sub>, 0 ) ]

'''N<sub>0</sub>'''을 정규화하는건 선택사항

// 삼각형 1 법선
'''A''' = 3 – 1; '''A''' = ( 0, h<sub>3</sub>, 1 ) – ( 1, h<sub>1</sub>, 0 )
'''B''' = 4 – 1; '''B''' = ( 1, h<sub>4</sub>, 1 ) – ( 1, h<sub>1</sub>, 0 )
'''N<sub>1</sub>''' = [ ( 0, h3, 1 ) – ( 1, h1, 0 ) ] × [ ( 1, h4, 1 ) – ( 1, h1, 0 ) ]

'''N<sub>1</sub>'''을 정규화하는건 선택사항

| style="vertical-align:top;"|
http://images.gamedev.net/features/programming/normalheightfield/image006.gif
|}

{| width="100%"
|-
|style="vertical-align:top;" |

위의 공식은 높이필드의 한 사각형용으로 표면법선을 계산하는 법을 보여줍니다. 하지만 이것은 특수한 경우일 뿐이며 지형 전체에 이것을 적용하려고 한다면 일반적인 형태의 공식이 필요합니다. 일반적으로 말해 x, z 쌍 대신에 i와 j를 사용하는 공식을 사용하여 공식을 동일하게 유지한 채 전체 지형을 순회해야 합니다. i가 0부터 ''numVertsWide'' - 1까지의 범위를 가진 색인이고 j가 0부터 ''numVertsDeep'' - 1까지의 범위를 가진 색인이라고 가정하고 다음과 같은 코드에서 작동할 수 있는 일반적인 공식을 찾아낼 수 있는지 살펴봅시다.


| style="vertical-align:top;"|
http://images.gamedev.net/features/programming/normalheightfield/image008.gif
|}

<pre>
for( j = 0; j < numVertsDeep; j++ )
 for( i = 0; i < numVertsWide; i++ )
 {
   ...
 }
</pre>

왼쪽 아래에 있는 삼각형(앞으로 삼각형 0이라 일컬음)에서 '''P<sub>0</sub>'''을 왼쪽 아랫점, '''P<sub>1</sub>'''을 왼쪽 윗점, '''P<sub>2</sub>'''를 오른 아랫점으로 합니다. 만약 '''P<sub>0</sub>''', '''P<sub>1</sub>''', '''P<sub>2</sub>'''가 현재 [i, j] 쌍의 함수였다면 이 점들을 아래와 같이 나타낼 수 있을 것입니다.

'''P0''' = [ i,     h<sub>0</sub>,  j ]
'''P1''' = [ i,     h<sub>1</sub>,  j+1 ]
'''P2''' = [ i+1, h<sub>2</sub>,  j ]

// 삼각형 0
'''A''' = '''P<sub>1</sub>''' – '''P<sub>0</sub>'''
'''B''' = '''P<sub>2</sub>''' – '''P<sub>0</sub>'''

이제 높이필드에 있는 어떤 점들도 'i'와 'j'의 함수로 나타낼 수 있으니 법선공식을 다시 살펴봅시다. 공식을 (i, j) 좌표로 다시 나타내더라도 이 쌍들은 마지막에 보다 간단한 계산을 남겨둘 것입니다.


// 외적에 사용할 두 벡터를 계산한다.
'''A''' = P<sub>1</sub> – P<sub>0</sub>; '''A''' = (i, h<sub>1</sub>, j+1) – (i, h<sub>0</sub>, j); '''A''' = ( 0, h<sub>1</sub> - h<sub>0</sub>, 1 )
'''B''' = P<sub>2</sub> – P<sub>0</sub>; '''B''' = (i+1, h<sub>2</sub>, j) – (i, h<sub>0</sub>, j); '''B''' = ( 1, h<sub>2</sub> - h<sub>0</sub>, 0 )

// 일반적인 i, j 형태와 외적은 완전한 정의를 사용하여 삼각형 0의 법선을 계산한다.
'''N<sub>0</sub>''' = '''A''' × '''B'''
'''N<sub>0</sub>''' = [ A<sub>y</sub>B<sub>z</sub> − A<sub>z</sub>B<sub>y</sub>, A<sub>z</sub>B<sub>x</sub> – A<sub>x</sub>B<sub>z</sub>, A<sub>x</sub>B<sub>y</sub> – A<sub>y</sub>B<sub>x</sub> ]

'''N<sub>0</sub>''' = [ ( 0, h<sub>1</sub> - h<sub>0</sub>, 1 ) ] × ( 1, h<sub>2</sub> - h<sub>0</sub>, 0 ) ]
'''N<sub>0</sub>''' = [ 0( h<sub>1</sub> - h<sub>0</sub> ) – 1( h<sub>2</sub> - h<sub>0</sub> ), 1(1) – 0(0), 0( h<sub>1</sub> - h<sub>0</sub> ) – 1( h<sub>1</sub> - h<sub>0</sub> ) ]
'''N<sub>0</sub>''' = [ h<sub>0</sub> - h<sub>2</sub>, 1.0f, h<sub>0</sub> - h<sub>1</sub> ]

위의 공식에서 두 직교벡터의 외적은 매우 간단하게 될 수 있습니다. 외적의 계산은 일반적으로 6개의 곱셈과 3개의 뺄셈을 필요로 하지만 일단 마지막까지 계산을 하면 많은 수의 항들이 제거되어 오직 2개의 뺄셈만이 필요하게 됩니다. D3DXVec3Cross() 함수를 사용하기 전에 DirectX가 벡터의 성질에 대해서는 아무것도 모른다는 사실을 기억하십시요. 벡터의 직교성질에 기초한 이 경우에는 DirectX보다 우리 스스로 외적을 계산하는 것이 훨씬 빠릅니다. 이제 삼각형 0의 외적을 일반적인 형태로 계산해보았으니 일반적인 형태를 사용하여 삼각형 1의 외적을 재빨리 계산해 봅시다. 이것을 마친 뒤에는 전체 높이필드를 순회하면서 표면법선을 계산하는데 필요한 2개의 공식을 가지게 될 것입니다.


// 오른쪽 위 삼각형을 계산하는 데 사용하는 3 점들
'''P<sub>0</sub>''' = [ i+1, j ]
'''P<sub>1</sub>''' = [ i, j+1 ]
'''P<sub>2</sub>''' = [ i+1, j+1 ]

// 외적에 사용할 두 벡터를 계산한다
'''A''' = '''P<sub>1</sub>''' – '''P<sub>0</sub>'''; '''A''' = (     i, h<sub>1</sub>, j+1 ) – ( i+1, h<sub>0</sub>, j ); '''A''' = ( -1, h<sub>1</sub> - h<sub>0</sub>, 1 )
'''B''' = '''P<sub>2</sub>''' – '''P<sub>0</sub>'''; '''B''' = ( i+1, h<sub>2</sub>, j+1 ) – ( i+1, h<sub>0</sub>, j ); '''B''' = ( 0, h<sub>2</sub> - h<sub>0</sub>, 1 )

'''N<sub>0</sub>''' = [ A<sub>y</sub>B<sub>z</sub> − A<sub>z</sub>B<sub>y</sub>, A<sub>z</sub>B<sub>x</sub> – A<sub>x</sub>B<sub>z</sub>, A<sub>x</sub>B<sub>y</sub> – A<sub>y</sub>B<sub>x</sub> ]
'''N<sub>0</sub>''' = [( -1, h<sub>1</sub> - h<sub>0</sub>, 1 ) ] × ( 0, h<sub>2</sub> - h<sub>0</sub>, 1 ) ]
'''N<sub>0</sub>''' = [ 1( h<sub>1</sub> - h<sub>0</sub> ) – 1( h<sub>2</sub> - h<sub>0</sub> ), 1(0) – (-1)(1), (-1)( h<sub>2</sub> - h<sub>0</sub> ) – (0)( h<sub>1</sub> - h<sub>0</sub>) ]
'''N<sub>0</sub>''' = [ h<sub>1</sub> - h<sub>2</sub>, 1.0f, h<sub>0</sub> - h<sub>2</sub> ]

삼각형 0의 공식에서와 마찬가지로 삼각형 1의 공식도 2개의 뺄샘으로 단순화 됩니다. 이제 높이필드에 있는 매 삼각형의 표면법선을 계산하는 데 필요한 모든 장비를 갖추었습니다. 간단히 사각형(quad)들을 순회하면서 각 사각형마다 위의 공식들을 사용하여 2개의 표면 법선을 계산하십시요. 표면 법선을 계산하는 코드를 살펴보기 전에 저희가 순회할 범위를 재빨리 계산해봅시다.


높이필드는 x 방향으로 ''numVertsWide'' 수 만큼의 정점들을, z 방향으로 ''numVertsDeep'' 수만큼의 정점들을 가지고 있을 것입니다. 0부터 ''numVertsWide'' - 1까지 그리고 0부터 ''numVertsDeep'' - 1까지 순회할 수 있고 이는 높이 필드에 있는 모든 삼각형들을 가져다 줄 것입니다. 처음에는 이것이 괜찮은 생각으로 보이지만 이보다 나은 방법이 있습니다. 이 범위들을 계산하는 대신에 여러분의 것보다 모든 방향으로 한 치수씩 큰 높이필드가 있다고 생각해봅시다. 즉 여러분의 높이필드를 이 큰 높이필드의 안에 위치시킨다면 아래의 그림처럼 각 모서리마다 여분의 사각형 행과 열이 하나씩 더 있는 것을 보게 될 것입니다. 이 별도의 사각형들의 목적은 정점 법선 계산을 다룰 때 확실히 알게 될 것입니다. 지금은 그저 이 별도의 표면 법선들을 저장할 충분한 공간을 할당하고 이들을 ( 0.0, 1.0, 0.0 ) 벡터로 초기화 해둡시다. 아래에 실린 코드는 제 데모에서 표면 법선을 계산하는 부분입니다. 제가 큰 범위의 표면을 순회하긴 하지만 이 별도의 사각형들의 법선들을 실제로 계산하고 있지는 않는 것을 볼 수 있으실 것입니다. 저는 이 사각형들의 표면이 위를 향하는 법선들을 가지고 있길 바랍니다. 하지만 저는 여전히 이 범위를 순회하긴 합니다. 색인을 업데이트해서 표면 법선들을 배열안의 올바른 위치에 저장하고 싶기 떄문입니다. 다음의 코드를 살펴보세요.

<pre>
voidHeightfield::ComputeSurfaceNormals( void )
{
 INT normalIndex = 0;

 // z 성분들을 순회한다
 for( int z = -1; z <= m_NumVertsDeep - 1; z++ )
 {
   // x 성분들을 순회한다
   for( int x = -1; x <= m_NumVertsWide - 1; x++ )
   {
     // 현재 x, z가 지형위에 놓여있는가? 아니면 그저 바깥 쪽에 있을 뿐인가?
     if( !IsValidCoord( x, z )   || !IsValidCoord( x+1, z ) ||
         !IsValidCoord( x, z+1 ) || !IsValidCoord( x+1, z+1 ) )
     {
       normalIndex += 2;
       continue;
     }

     // 삼각형 0의 법선을 저장/계산한다
      float height0 = GetHeight( x,   z );
     float height1 = GetHeight( x, z+1 );
     float height2 = GetHeight( x+1, z );
     D3DXVECTOR3 normal1( height0 - height2, 1.0f, height0 - height1 );

     // 삼각형 1의 법선을 저장/계산한다
      height0 = height2;
     height2 = GetHeight( x+1, z+1 );
     D3DXVECTOR3 normal2( height1 - height2, 1.0f, height0 - height2 );

     // 표면법선들을 배열안에 저장한다
      m_pSurfaceNormals[normalIndex++] = normal1;
     m_pSurfaceNormals[normalIndex++] = normal2;
   }
 }
}

</pre>

====정점법선의 계산====

고라우드의 법선 알고리듬의 두번째 단계는 정점 법선의 계산입니다. 두번째 단계는 첫번째 단계보다 훨씬 쉬우며 수학보다는 로직에 더욱 기초해있습니다. 정점 법선을 계산하는 것은 현재 정점에 인전해 있는 표면들을 그저 가져다가 더한뒤 6으로 나누기만 하면 되는 간단한 과정입니다. 여기서 힘든 부분은 나누기가 아니라 저희가 위에서 계산해 놓은 표면법선중에 어떤 것이 현재 삼각형에 인접해 있는지를 판단하는 것입니다. 정점 법선을 계산하는 공식은 다음과 같습니다.


정점 법선: '''N<sub>nwe</sub>''' = Σ ('''N'''<sub>si</sub>) / 6.0f | i = 0…5

{| width="100%"
|-
|style="vertical-align:top;" |

표면중에 어떤 것들이 현재 정점에 인접해 있는지를 판단하기 위해 일단 오른쪽의 그림을 봅시다. 오른쪽의 그림에서 각 정점들을 둘러싸고 있는 표면의 수가 틀립니다. 예를 들어 정점 0은 오직 하나의 표면만이 이에 인접해 있으며, 정점 1은 3개의 표면이 인접해있습니다(왼쪽 위에 2개와 오른쪽 위에 하나). 정점 2는 왼쪽 위에 인접해 있는 표면을 2개 가지고 있습니다. 정점 4는 6개의 인접표면을 가지고 있는데 위에 3개, 아래에 3개가 그것입니다. 이 예제에서 매 정점마다 정점법선을 정확히 계산하는 알고리듬을 만들려면 이 특별한 경우들을 각각 고려해야합니다. 정점이 모서리 - 위, 왼쪽, 아래, 오른쪽에 있습니까? 아니면 이것이 코너 - 왼쪽 위, 오른쪽 위, 왼쪽 아래, 오른쪽 아래에 있습니까? 이런 알고리듬은 우선 위의 문장중 어떤 것이 진짜인지를 판단하기위해 조건문을 계산해야 할 것입니다. 일단 정점의 조건을 알아내면 일련의 if문을 통해 특정 인접 표면들을 더할 것입니다. 음... 이것보다 뭔가 깨끗한 방법이 있을텐데요?


| style="vertical-align:top;"|
http://images.gamedev.net/features/programming/normalheightfield/image003.gif
|}

이 문제를 가지는 주된 이유는 높이필드가 모서리(edge)를 가지기 때문입니다. 만약 높이필드가 모서리를 가지고 있지 않다면 오른쪽 그림에서 중앙에 있는 정점처럼 모든 정점들이 6개의 표면법선을 가질 것입니다. 이런 경우 어떤 정점에서도 6개의 인접 표면을 결정지을 수 있는 믿을만한 알고리즘을 만드는 것이 가능합니다. 따라서 높이필드에서 모서리를 제거해 봅시다! 위에 표면법선을 계산했었던 방법을 다시 떠올려보신다면 제가 별도의 사각형 행과 열을 각 모서리에 추가하라고 말씀드린 것이 기억나실 것입니다. 이 사각형들은 업데이트 되지 않으며 단순히 법선으로 UP 벡터를 가집니다. 이런 일을 한 이유는 위의 이미지를 바로 그 아래와 같은 이미지로 바꾸고 싶었기 때문입니다.


오른쪽의 이미지에 있는 높이필드는 추가적인 일련의 사각형때문에 더이상 모서리를 가지지 않습니다. 저희의 높이필드에 있는 각 정점은 이제 6개의 인접 표면을 가지고 있습니다. 삼각형의 방향에 따라 각 정점은 왼쪽 위로 2개, 오른쪽 위로 1개, 왼쪽 아래로 1개, 오른쪽 아래로 2개의 표면을 가지고 있습니다. 이제 해당 정점 주변에 작은 정사각형이 하나 있다고 상상해봅시다. 이 정사각형은 6개 표면의 일부가 될 것입니다. 이 정사각형을 상하좌우로 미끄러 움직여보면 이것은 언제나 6개의 삼각형 내부에 위치합니다. 따라서 저희는 이 알고리듬을 '''Sliding Six 알고리듬'''이라고 부르겠습니다.

이 알고리듬의 기초개념은 왼쪽 아랫 코너에 이 사각형을 위치시키고, 어떤 표면들에 이 사각형이 속한지를 판단한 뒤, 이 표면들을 현재 정점 색인위치에 더해서 집어넣을 표면목록에 더하는 것입니다. 몇몇 정점들로 이것을 시도해보면 이에 대한 감을 잡을 수 있으실 것입니다.


{| width="100%"
|-
|style="vertical-align:top;" |

정점 0 <br/>
왼쪽 아래에 있는 삼각형은 삼각형 1이다. <br/>
바로 아래에 있는 삼각형은 삼각형 2이다. <br/>
오른쪽 아래에 있는 삼각형은 삼각형 3이다.<br/>
오른쪽 위에 있는 삼각형은 삼각형 8이다. <br/>
바로 위에 있는 삼각형은 삼각형 9이다.<br/>
오른쪽 위에 있는 삼각형은 삼각형 10이다.<br/>

정점 1 <br/>
아래쪽에서 왼쪽부터 오른쪽으로 삼각형 3, 4, 5가 있다.<br/>
위쪽에서 왼쪽부터 오른쪽으로 삼각형 10, 11, 12가 있다.<br/>

정점 2 <br/>
아래쪽에서 왼쪽부터 오른쪽으로 삼각형 5, 6, 7이 있다. <br/>
위쪽에서 왼쪽부터 오른쪽으로 삼각형 12, 13, 14가 있다.<br/>

| style="vertical-align:top;"|
http://images.gamedev.net/features/programming/normalheightfield/image009.gif
|}

위에서 3개의 정점으로 예를 든 이유는 패턴이 진행하는 모습을 보여주고 싶었기 때문입니다. 각 행의 삼각형들은 한 정점에서 다음정점까지 순차적입니다. 정점 0은 표면 1, 2, 3을 가지며, 정점 1은 표면 3, 4, 5를 가지는 식입니다. 이 패턴은 높이필드를 모두 가로지를 때까지 지속될 것입니다. 젤 윗행에 대해서도 마찬가지 법칙이 적용됩니다. 따라서 각 정점의 시작 색인을 구할 수 있다면(이것을 삼각형 색인이라고 부릅시다) 그 색인에서 시작하여 오른쪽으로 둘, 그리고 그에 대응하는 위쪽으로 셋을 이동하면 모든일이 끝날 것입니다. 여기서 한가지 문제점은 삼각형 색인 바로 위에 있는 표면을 어떻게 구하냐 하는 것입니다. 다음의 공식을 사용하면 이를 구할 수 있습니다.


rowOffset = 2( numVertsWide + 1 ) - 1;

이 글에 첨부된 데모 프로그램에서 구현한 알고리듬은 다음과 같습니다:


<pre>
void Heightfield::ComputeVertexNormals( void )
{
 D3DXVECTOR3 vec[6];
 D3DXVECTOR3 vertexNormal;
 INT triIndex    = 1;
 INT indexPlusOne = 2;
 INT indexPlusTwo = 3;
 INT vertIndex   = 0;

 // 높이필드를 구성하는 삼각형 수에 기초하여 오프셋을 계산한다.
 INT rowOffset = ( ( m_NumVertsWide + 1 ) * 2 ) - 1;
 for( INT z = 0; z < m_NumVertsDeep; z++ )
 {
   for( INT x = 0; x < m_NumVertsWide; x++ )
   {
     indexPlusOne = triIndex + 1;
     indexPlusTwo = triIndex + 2;

     // 정점 아래의 세 삼각형들을 구한다.
     vec[0] = m_pSurfaceNormals[ triIndex ];
     vec[1] = m_pSurfaceNormals[ indexPlusOne ];
     vec[2] = m_pSurfaceNormals[ indexPlusTwo ];

     // 정점 위의 세 삼각형들을 구한다.
     vec[3] = m_pSurfaceNormals[ rowOffset + triIndex ];
     vec[4] = m_pSurfaceNormals[ rowOffset + indexPlusOne ];
     vec[5] = m_pSurfaceNormals[ rowOffset + indexPlusTwo ];

     // 벡터를 더한 뒤, 6으로 나눠 평균을 구한다.
     vertexNormal = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5];
     vertexNormal /= 6.0f;

     m_pVertexNormals[vertIndex] = vertexNormal;

     triIndex += 2;
     vertIndex++;
   }

   triIndex += 2;
 }
}

</pre>

<div align=center>
http://images.gamedev.net/features/programming/normalheightfield/image010.gif
</div>

===방법 2: 각에 따른 가중평균 (MWA)===

위의 알고리듬에서와 마찬가지로 각에 따른 가중평균(MWA)은 2단계 알고리듬입니다. 첫번째 단계는 표면법선을 계산하는 것으로 이전의 알고리듬과 동일합니다. 차이점은 각 표면법선이 정점법선 계산에서 사용되기 전에 사후처리 된다는 것뿐입니다. MWE에서와는 다르게 표면법선은 '''반드시''' 정규화되어야 하며, 그렇지 않으면 조명에 어색한 부분이 보일 것입니다. 두번째 단계는 MWE와 거의 비슷하여 연관된 표면법선을 더한뒤 6으로 나누는 것 뿐입니다. 여기 가중치를 부여한 표면법선을 계산하는 데 사용하는 수정된 공식이 있습니다.


표면 법선: '''N<sub>s</sub>''' ═ &theta;('''A''' ×'''B''')

이 알고리듬의 정점법선의 계산법이 MWE와 거의 비슷하므로 다시 다루지는 않겠습니다. 특정 구현부분과 가중치를 부여한 표면법선에 접근해서 살짝 바뀐 부분을 보려면 데모프로그램의 코드를 직접 살펴보십시기 바랍니다.


====표면법선의 계산====

MWA 알고리듬의 첫번째 부분은 표면법선을 계산하는 것인데 이것은 저희가 MWE에서 했던것과 매우 동일합니다. 하지만 주어진 삼각형의 표면법선을 계산한 뒤에는 이것을 그 표면삼각형의 세 각으로 곱하는 사후처리를 해야합니다. 저희는 이렇게 함으로써 이전에 사용했던 하나의 표면법선을 대체할 3개의 새로운 "가중치 부여된" 표면 법선을 만들었습니다. 표면 법선을 구하는 법은 이미 알고 있으니 각을 구하는 법을 알아보도록 합시다. 다음의 수식은 두 벡터 사이의 각을 구할 때 일반적으로 사용되는 방법입니다.


외적의 정의:
│'''A''' × '''B'''│ = │'''A'''││'''B'''│sin(&theta;)

&theta; ═ sin<sup>-1</sup>( │'''A''' × '''B'''│ / │'''A'''││'''B'''│ )

외적의 정의로서 널리 사용되는 위의 수식은 바로 그 아래처럼 다시 작성될 수 있습니다. 아래의 수식은 벡터 '''A'''와 '''B''' 사이의 각은 '''A'''와 '''B''' 사이의 외적의 크기의 역사인(inverse sign)을 취하고 이 모두를 벡터 '''A'''의 크기와 벡터 '''B'''의 크기를 곱한것으로 나눈 것이라고 말합니다. 앞서 말했던 것과 마찬가지로 저희는 이미 주어진 삼각형의 특정 '''A'''와 '''B'''의 외적을 가지고 있으나 삼각형의 모든 변의 크기도 필요합니다. 다음 단계는 벡터의 크기를 계산하는 가장 효율적인 방법을 결정합니다. 다음 수식들은 삼각형 0의 좌변을 따라 위치한 벡터의 크기를 구하는 일반적은 형태를 이끌어 냅니다.


│'''V<sub>0</sub>'''│ = √( ( ∆'''V'''<sub>x</sub> )<sup>2</sup> + ( ∆'''V'''<sub>Y</sub> )<sup>2</sup> + ( ∆'''V'''<sub>Z</sub> )<sup>2</sup> )

│'''V<sub>0</sub>'''│ = √( ( i – i )<sup>2</sup> + ( h<sub>1</sub>-h<sub>0</sub> )<sup>2</sup> + ( j+1-j )<sup>2</sup> )

│'''V<sub>0</sub>'''│ = √( ( 0<sup>2</sup> + ( h<sub>1</sub>-h<sub>0</sub> )<sup>2</sup> + ( 1 )<sup>2</sup> )

│'''V<sub>0</sub>'''│ = √( ( h<sub>1</sub>-h<sub>0</sub> )<sup>2</sup> + 1 )

이 수식을 풀면 벡터의 크기를 하나의 곱셈, 덧셉, 거듭제곱 그리고 루트로 간단히 표현하였습니다. 이것은 값싼 수식은 '''아닙니다.''' 나중에 확인할 수 있듯이 거듭제곱은 이 알고리즘의 성능을 저하시킵니다. 이제 삼각형 좌변의 크기를 계산하였지만 각자 자기의 크길르 가진 2개의 변이 여전히 남아있습니다. 아래는 삼각형 0의 다른 두 변의 크기를 계산하는 공식의 간략한 버전입니다.


│'''V<sub>1</sub>'''│ = √( ( h<sub>2</sub>-h<sub>1</sub> )<sup>2</sup> + 2 ) // 삼각형 0의 빗변
│'''V<sub>2</sub>'''│ = √( ( h<sub>2</sub>-h<sub>0</sub> )<sup>2</sup> + 1 ) // 삼각형 0의 밑변

법선 계산의 공식에서와 마찬가지로 삼각형 1의 방향이 약간 다르므로 길이 계산도 약간 다릅니다. 여기에 삼각형 1의 세 변의 길이를 구하는 식이 있습니다.


│'''V<sub>0</sub>'''│ = √( ( h<sub>1</sub>-h<sub>0</sub> )<sup>2</sup> + 2 ) // 삼각형 1의 빗변
│'''V<sub>1</sub>'''│ = √( ( h<sub>2</sub>-h<sub>1</sub> )<sup>2</sup> + 1 ) // 삼각형 1의 윗변
│'''V<sub>2</sub>'''│ = √( ( h<sub>2</sub>-h<sub>0</sub> )<sup>2</sup> + 1 ) // 삼각형 1의 우변

위의 6개 식이 있으면 높이필드에 있는 매 사각형의 오른쪽 위와 왼쪽 아래의 삼각형들 모두의 각 변의 길이를 계산할 수 있습니다. 다음으로 할 일은 이들을 위의 Theta(각) 공식에 대입하고 삼각형의 각 코너의 각을 계산하는 것입니다. 여기서는 삼각형 0의 예만을 보이겠습니다. 삼각형 1은 독자들 여러분이 직접 연습삼아 해보십시요. 하지만 여전히 소스코드는 제공됩니다.


&theta; ═ sin<sup>-1</sup>( │'''A''' × '''B'''│ / │'''A'''││'''B'''│ )

// 삼각형 0의 아랫쪽 왼쪽 코너의 각을 계산한다
│'''V<sub>0</sub>'''│ = √( ( h<sub>1</sub>-h<sub>0</sub> )<sup>2</sup> + 1 ) // 삼각형 0의 좌변
│'''V<sub>2</sub>'''│ = √( ( h<sub>2</sub>-h<sub>0</sub> )<sup>2</sup> + 1 ) // 삼각형 0의 하변

&theta; ═ sin<sup>-1</sup>( │'''N<sub>s</sub>'''│ / │ '''V<sub>0</sub>'''││ '''V<sub>2</sub>'''│ ) Ns는 이미 계산된 이 삼각형의 표면법선

삼각형 0의 왼쪽 아래코너 정점의 가중 표면법선을 계산하는 방법은 다음과 같습니다.


'''N'''<sub>w0</sub> = &theta; * '''N<sub>s</sub>'''

다음은 높이필드에 있는 매 사각형을 순회하면서 표면법선을 계산하고 삼각형 0과 삼각형 1의 새로운 가중치 부여된 표면법선을 계산하는 수정된 코드입니다.


<pre>
void Heightfield::ComputeWeightedSurfaceNormals( void )
{
 INT normalIndex = 0;

 // z 성분을 순회한다.
 for( int z = -1; z <= m_NumVertsDeep - 1; z++ )
 {
   // x 성분을 순회한다.
   for( int x = -1; x <= m_NumVertsWide - 1; x++ )
   {
     if( !IsValidCoord( x,   z ) || !IsValidCoord( x+1,   z ) ||
         !IsValidCoord( x, z+1 ) || !IsValidCoord( x+1, z+1 ) )
     {
       normalIndex += 6;
       continue;
     }

     // 삼각형 1의 법선을 저장/계산한다.
     float height0 = GetHeight( x,    z );
     float height1 = GetHeight( x,  z+1 );
     float height2 = GetHeight( x+1,  z );
     D3DXVECTOR3 normal1( height0 - height2, 1.0f, height0 - height1 );
     D3DXVec3Normalize( &normal1, &normal1 );

     // 표면법선의 길이와 가중치를 구한다.
     float normal1Mag = D3DXVec3Length( &normal1 );
     float abMag      = sqrt( (height1 - height0) * (height1 - height0) + 1 );
     float acMag      = sqrt( (height2 - height0) * (height2 - height0) + 1 );
     float bcMag      = sqrt( (height2 - height1) * (height2 - height1) + 2 );

     float theta0   = asin( normal1Mag / ( abMag * acMag ) );
     float theta1   = asin( normal1Mag / ( bcMag * abMag ) );
     float theta2   = asin( normal1Mag / ( bcMag * acMag ) );

     m_pWeightedSurfaceNormals[normalIndex++] = normal1 * theta0;
     m_pWeightedSurfaceNormals[normalIndex++] = normal1 * theta1;
     m_pWeightedSurfaceNormals[normalIndex++] = normal1 * theta2;

     // 삼각형 2의 법선을 저장/계산한다.
     height0 = height2;
     height2 = GetHeight( x+1, z+1 );
     D3DXVECTOR3 normal2( height1 - height2, 1.0f, height0 - height2 );
     D3DXVec3Normalize( &normal2, &normal2 );

     // 표면법선의 길이와 가중치를 구한다.
     float normal2Mag = D3DXVec3Length( &normal2 );
     abMag    = sqrt( (height1 - height0) * (height1 - height0) + 2 );
     acMag    = sqrt( (height2 - height0) * (height2 - height0) + 1 );
     bcMag    = sqrt( (height2 - height1) * (height2 - height1) + 1 );

     theta0   = asin( normal2Mag / ( abMag * acMag ) );
     theta1   = asin( normal2Mag / ( bcMag * abMag ) );
     theta2   = asin( normal2Mag / ( bcMag * acMag ) );

     m_pWeightedSurfaceNormals[normalIndex++] = normal2 * theta0;
     m_pWeightedSurfaceNormals[normalIndex++] = normal2 * theta1;
     m_pWeightedSurfaceNormals[normalIndex++] = normal2 * theta2;
   }
 } // 표면법선 계산 끝!
}

</pre>

===각 접근법의 성능분석===
이 글을 쓰는 도중에 저는 다른 주제들의 실제 작동모습을 보여주는 데모를 만들기로 결정하였습니다. 다양한 높이필드 알고리듬을 구현하는 최선의 방법을 모색하던중 한가지 모순에 봉착하게 되었습니다. 어떤 사람들은 표면법선들을 정점법선 계산에 사용하기 전에 정규화시켜야 한다고 말하였고, 어떤 사람들은 아무 상관없다고 말하였습니다. 또한 많은 사람들이 정점법선을 계산하는 최선의 방법은 표면법선의 평균을 구하는 것이라고 하였으나 표면법선들을 더한뒤 그 결과 벡터를 정규화시키기만 해도 된다고 말하는 사람들도 있었습니다. 마지막으로 동등한 가중평균(MWE)와 각에 따른 가중평균(MWA)중 어떤 알고리듬이 최선이고 최고의 성능을 보이지는지에 대한 의견도 분분했습니다. 이 단락은 다양한 방법들간의 차이점을 비교분석해 볼 것입니다.

제가 아래에 제공한 성능분석은 장면에 하나의 높이필드가 있을때에 기초한 것으로 매 프레임마다 사인커브에 기초하여 움직입니다. 참고로 다른 물체들은 이 장면에 존재하지 않습니다. 여러분의 지형이 정적인지 동적인지에 따라, 그리고 장면에서 어떤 일을 하는지에 따라 실시간 상황에서의 실제 성능이 더 나빠질수도 좋아질수도 있습니다. 아래에 제공하는 정보는 전적으로 다른 알고리즘의 계산 복잡도를 비교하는 목적을 가질 뿐입니다.


<div align=center>

http://images.gamedev.net/features/programming/normalheightfield/image011.gif

http://images.gamedev.net/features/programming/normalheightfield/image012.gif

'''그림. 표면 법선을 사용/비사용하여 렌더링한 고해상도 지형'''



</div>

====MWE, 비정규화된 표면 법선 vs. 정규화된 표면 (평균낸 정점 법선들)====
시각적인 관점에서 보면 표면법선을 정규화시키는 것이 어둡고도 부드러운 곡면을 가지는 지형을 만들어 냅니다. 게다가 비교적 낮은 해상도에서 표면법선을 정규화시키는 데 드는 성능저하가 거의 관측되지 않습니다. 하지만 높은 해상도에서는 표면을 정규화시키는 것이 프레임율을 6% (318 fps - 300 fps)정도 저하시킵니다. 해상도가 높을수록 정규화된 표면법선과 비정규화 표면법선간의 차이가 벌어집니다. 참고로 MWE에서 정점법선을 정규화시키면 표면법선을 정규화시킬 필요가 없습니다.

<div align=center>
http://images.gamedev.net/features/programming/normalheightfield/image013.gif

'''그림. 표면법선을 정규화한뒤 정점 법선을 정규화, 평균화 시켜서 그린 고해상도 지형. 거의 차이점이 없다.'''

</div>

====MWE, 정점법선의 정규화 vs. 평균화====
시각적인 관점에서는 정점법선을 정규화시키는 것은 표면법선을 정규화시키지 않았을 때에만 차이점을 가져옵니다. 어떤 경우이던 정점법선을 정규화시키는 것은 지형을 약간 밝게 만들고 그림자를 널리 퍼뜨리는 효과를 가져옵니다. 그러나 이 차이점은 표면법선을 정규화시키지 않았을때에만 잘 눈에 띕니다. 특히 높은 해상도에서 표면법선을 정규화시키지 않았을 때에는 '''반드시''' 정점법선을 정규화시켜야 합니다. 표면법선을 정규화시켰다면 저해상도에서 정점 법선을 정규화시키는데 대한 성능저하는 거의 눈에 띄지 않습니다. 하지만 고해상도에서는 정점 법선을 정규화시키는 것이 프레임수를 다시 6% 정도 떨어뜨립니다. 표면법선과 정점법선을 모두 정규화시키는 것은 6%의 부가적인 프레임수 저하를 가져오고, 표면법선을 평균화시켰을 경우 정점 법선을 정규화시키는데서 얻는 이익은 매우 미미하므로 표면법선은 정규화시키고 정점법선은 평균화시키도록 저는 조언하고 싶네여.WMA를 다룰 때 이는 더더욱 진실이 될 것입니다.

====MWA, 비정규화된 표면 법선 vs. 정규화된 표면 (평균낸 정점 법선들) ====
MWE의 겨웅 표면법선을 정규화시키는 것은 그저 선택사항이었지만 MWA에서는 필수사항 입니다. 우선 표면법선을 정규화시키지 아니한 채 가중치를 부여한 정점법선을 그리면 완전히 평면인 삼각형들에 어색한 부분이 생깁니다. 이것은 완전히 검정색으로 보이거나 완전히 하얀색으로 보일 것입니다. 낙관적으로 보면 각을 계산하는데 필요한 계산의 수가 벡터를 정규화시키는데 필요한 계산수를 훨씬 능가한다는 것입니다. 가중치를 부여한 정점법선을 사용할 때는 언제나 표면법선을 정규화시키도록 합시다.


<div align=center>
http://images.gamedev.net/features/programming/normalheightfield/image015.gif

'''그림. 가중치부여 표면법선과 정규화 또는 평균낸 정점법선을 사용한 고해상도 지형'''

</div>

====MWA, 정점 법선들의 정규화 vs. 평균화 (정규화된 표면) ====
이것이 여태까지 보아온 것중에 가장 흥미로운 조합일 것입니다. 저해상도에서는 정점법선을 정규화시킴으로써 야기된 빛의 증가가 지형을 보다 매력있게 만듭니다. 반면 지형의 해상도가 높아질수록 추가의 빛들이 지형을 비사실적으로 보이게 만듭니다. 표면법선을 정규화시키고 정점법선을 평균낸 고해상도 지형이 제가 본 것 중에 가장 사실적으로 보이는 지형이었습니다. 최고의 해상도에서 제 지향의 굽이침은 고운 비단 같았습니다. 한가지 유일한 단점은 가중치 부여된 법선의 계산시간이 매우 비싸다는 것입니다. 제 경험에 비추어 볼 때 법선에 가중치를 부여하는 것은 프레임수를 80% (1000 fps - 200 fps)가량 떨어뜨렸습니다. 한가지 염두에 두셔야 할 점은 제가 매 프레임마다 높이값을 업데이트하고 법선을 재계산하였다는 것입니다. 실제로는 가중치를 부여한 지형법선을 런타임중에 업데이트 하는 일이 매우 적을 것입니다. 이런 이유로 인해 매력적이고 믿을만한 지형을 찾을 때에 가중치 부여 법선은 여전히 사용가능한 방법중 하나로 고려해볼만 합니다.

==추후 연구과제 / 기타 관심영역==

이 글에서 다룬 주제 이외에 다음 두 분야에 대한 내용을 다뤄달라는 요청을 받았었습니다. 불행히도 저 스스로 이 주제들을 깊이 탐구해볼 시간이 없었지요. 아래에 이 둘을 적어놓았으니 관심 있으신 독자분들께서는 직접 공부를 해보셔도 좋을듯 합니다.


===스무딩그룹(Smoothing Group) ===
스무딩 그룹은 3D 스튜디오 맥스나 기타 모델링 프로그램을 사용하는 사용자들에게는 매우 익숙한 개념일것입니다. 스무딩그룹은 본질적으로 주위의 정점과 동일한 조명규칙을 따르지 않는 셰이딩을 하는 정점들의 하위부분입니다. 예를 들어 실린더를 표현하는 경우에 이 스무딩 그룹은 유용할것입니다. 실린더의 원기둥(cylidrical) 부분은 부드러워야 하므로 부드러운 셰이딩 알고리듬을 적용해야 합니다. 즉, 옆면 부분은 스무딩 그룹번호를 갖게 주어 부드럽게 쉐이딩하게 할것입니다.
하지만 윗면, 정확히 말해 부드러운 모서리(옆면)로부터 윗면으로의 변화는 반드시 부드러운 변화일 필요는 없습니다. 왜냐하면 사용자는 윗면과 옆면을 구분짓게 하는 부분 즉,  동그라미 부분(ring)은  뚜렷하게 선이 보일것(hard-edge)을 예상하기 때문에 이부분의 정점들에 대해서는 평균화한 법선을 가지게 하지 않을 것입니다. 즉 스무딩 그룹번호를 동일하게 주지않고 다르게 주어 구별하게 할것입니다.

스무딩 그룹은 정점들을 복제한뒤 일련의 각 정점들에 다른 법선을 지정하는 방법으로 이러한 작업을 합니다.

높이필드의 경우 스무딩 그룹은 절벽과 낭따러지의 모서리나 높은 산의 정점에 사용하는것에 유용합니다. 위의 두 알고리듬 모두 가파른 벼랑을 부드러운 커브로 만들 것입니다. 현실에서는 부드러운 커브가 아닌 갑작스런 낙하지점도 자주 존재합니다. 따라서 이 때 지형에 보다 사실적인 모서리를 제공하기 위해 스무딩 그룹을 사용할 수 있습니다.

===매개변수 방정식을 이용한 법선계산===
다른 옵션은 매개변수 방정식으로부터 법선을 계산해내는 것입니다. 이것을 잘 보여주는 한 예는 구체입니다. 일반적으로 구체는 구체의 표면을 근접하게 나타내는 삼각형들의 표현으로 나타냅니다. 이 정점들의 법선을 구하려면 한 삼각형의 변들의 외적을 구하는 일반적인 방법을 쓸 수 있을 것입니다. 아니면 수학적으로 구체의 표면에 있는 점들의 법선은 모두 구체의 중앙으로부터 표면상의 그 점까지 향하는 방향 벡터일 뿐입니다. 이것을 염두에 두면 외적을 사용한 법선계산보다 구체의 수학적 정의를 이용하여 구체의 표면법선을 더욱 빠르고 정확하게 계산할 수 있습니다.


높이필드에 이것을 응용하는 방법이 매우 명백한 것은 아닙니다. 제 생각으로는 지형을 잘 알려진 함수를 사용하여 절차적으로 형성할 때에만 이것이 유용할 듯 싶습니다. 제가 제공한 데모에서는 모든 정점의 위치들이 사인 함수를 통해 계산됩니다. 이 예에서는 제 정점들의 높이가 0부터 1 사이의 범위를 가지고 이는 0~360 도의 함수이므로 각 특정 정점의 0 ~ 360도 사이의 각에 기초하여 각 정점들의 법선을 계산하는 것이 가능합니다. 원하시는 독자분들은 직접 구현해보십시요.

==결론==
지난 2주간 유명한 알고리듬들의 성능 및 유용성을 실용적인 측면에서 살펴보는 동안 높이필드의 조명법선의 구현 데모를 작성할 기회가 있었습니다. 이 글은 높이필드의 조명용 법선계산에 대해 제가 발견한 모든 것들을 담고 있습니다. 이 글이 여러분에게 높이필드의 조명법선에 대한 정보에 대해서 도움이 되기를 바랍니다. 이것에 기초하여 저는 가능한한 정규화되고 가중된 표면법선과 평균(정규화 되지 않은) 정점 법선들을 사용하여 저의 높이필드를 구현할 것입니다. GameDev.net 커뮤니티의 회원분들께서는 이 글에 대한 피드백을 토론장에 남겨주시면 고맙겠습니다. 코드 수정이나 의견을 저에게 직접 개인메시지로 보내셔도 좋습니다.

==부록 A: 데모 프로그램, 링크, 사용키==
이 글의 스크린샷, 성능 비교, 코드 리스트들은 모두 여기 첨부한 데모 프로그램으로부터 수집한 정보에 기초해 있습니다. 이 데모는 제게 다양한 종류의 기능을 테스트하고 이 기능들의 비쥬얼과 성능에 미치는 영향을 결정할 수 있는 기회를 가져다 주었습니다. 따라서 이 데모는 제한적인 기능만을 가지고 있습니다.


===링크: ===
[http://members.gamedev.net/jwalsh/HeightfieldSrc.zip Source Code]

[http://members.gamedev.net/jwalsh/HeightfieldDemo.zip Win32 Binary Executable]

===사용키: ===
방향키: 높이필드의 해상도를 증감한다.<br/>
1: 와이어프레임 디스플레이를 on/off한다.<br/>
2: 표면 및 정점 법선의 계산을 on/off한다.<br/>
3: 부드러운 vs. 단순한 세이딩을 선택한다.<br/>
4: 표면 법선의 정규화를 on/off한다.<br/>
5: 정점 법선의 정규화 vs. 평균화를 선택한다.<br/>
6: 각에 따른 정점법선의 가중값을 on/off한다.<br/>
0: 디버그 법선의 디스플레이를 on/off한다.<br/>
9: 높이필드의 애니메이션을 on/off한다.<br/>

==참고문헌==
* "A Comparison of Algorithms for Vertex Normal Computation", Shuangshuang Jin, Robert R. Lewis, David West, 2003, Washington State University

==원문정보==
* 저자: Jeromy Walsh
* 원문보기: [http://www.gamedev.net/reference/programming/features/normalheightfield/ Normal Computations for Heightfield Lighting ]

==번역문 정보==
* 초벌번역: [[사용자:Pope| Pope Kim]]
* 재벌번역: [[User:es|es]]
* 감수: [[User:es|es]]

==현재상태==
* 초벌번역시작 (2006년 1월 28일)
* 초벌번역종료 (2006년 7월 5일)
* 재벌및감수시작 (2006년 7월 16일)
* 감수종료 (2006년 7월 27일)


[[분류:그래픽스 프로그래밍(번역)]]
[[분류:그래픽스 프로그래밍]]
[[분류:그래픽스 프로그래밍(GDNet)]]
[[분류:수학/물리]]
[[분류:수학/물리(번역)]]
[[분류:수학/물리(GDNet)]]
[[분류:GDNet]]
[[분류:알고리듬]]
[[분류:알고리듬(번역)]]
[[분류:알고리듬(GDNet)]]

Comments