2015. 2. 26.

[DirectX9]텍스처(Texture)

 3D 그래픽스에서 사실성을 부여할 수 있는 대표적인 방법 중 하나가 텍스처이다. 텍스처는 메시와 같은 기하물체의 표면에 이미지을 씌우는 방법이다. 이미지를 씌우면 기하물체로 표현하기에는 부담스러운 표현을 비교적 간단하게 표현할 수 있다.


텍스처 맵핑(Texture Mapping)


 위의 그림에서 왼쪽의 3D 물체는 와이어프레임으로 출력되고 있다. 이 물체를 구성하는 폴리곤들의 표면에 가운데에 있는 2D 이미지를 씌운다고 생각하자. 그 결과는 오른쪽과 같이 지구처럼 나타날 것이다. 3D 구와 구에 덮어씌울 2D 이미지 한 장으로 지구를 만들었다. 이처럼 텍스처 맵핑은 단순하지만 효과적으로 사실성을 표현할 수 있는 기법 중 하나이다.

 DirectX9에서 텍스처 맵핑을 하는 과정은 다음과 같다.
  1. Direct3DTexture9 인터페이스 선언: 텍스처를 사용하기 위한 변수
  2. 텍스처 좌표를 갖는 정점 선언: 사용자 정의 정점 구조체에서 선언
  3. 텍스처 생성: 일반적으로 파일을 읽어서 생성
  4. 텍스처 스테이지 설정: 텍스처 스테이지는 텍스처의 좌표 생성 및 랩핑 모드 등 텍스처 제어에 관한 설정
  5. 그려질 텍스처를 지정: D3D 디바이스에 텍스처를 알린다.
  6. 메시 그리기
 각각의 과정에 대해서 좀더 살펴보면 정점 버퍼를 생성할 때도 인터페이스가 필요했듯이, 텍스처를 사용하기 위해서도 텍스처 인터페이스가 필요하다. 그리고 사용자 정의 정점 구조체의 정점이 텍스처 상의 uv 좌표계에서 어디에 맵핑될지도 설정하여야 한다. 사용자 정의 정점 구조체에 텍스처 좌표값을 위한 변수가 추가되었으므로 FVF 플래그도 변경해주어야 한다. 그 다음 텍스처를 생성한다. 텍스처는 D3D 디바이스를 통해 D3DXCreateTextureFromFile() 함수를 호출하여 생성할 수 있다. 텍스처를 생성한 후, 텍스처를 어떻게 제어할 것인지에 관한 정보를 텍스처 스테이지를 통해 설정한다. 텍스처와 정점의 색을 섞을지 말지 등을 결정한다. 그리고 생성한 텍스처에 인덱스를 부여하여 D3D가 텍스처를 관리할 수 있도록 한다. 

 지금까지 설명한 과정을 코드로 다시 살펴보자. 아래의 코드는 텍스처 맵핑을 하는 핵심적인 부분들을 발췌한 것이다. 완성된 코드가 아님을 알려둔다.

 아래의 이미지는 텍스처로 활용한 이미지이다. 텍스처를 생성할 때 파일명을 "sami.jpg"로 지정하였으므로 확인하도록 하자.



원통에 위의 이미지를 씌워보자
 #ifdef...#endif 선언이 자주 눈에 띌 것이다. 모르는 사람은 #ifdef...#endif에 대한 포스트을 확인하도록 하자. 어려운 것이 아니므로 금방 이해할 수 있을 것이다. SHOW_HOW_TO_USE_TCI가 선언되어 있는지 여부에 따라 컴파일이 달라진다. SHOW_HOW_TO_USE_TCI가 선언되지 않는 경우에는 알고리즘을 통해서 각각 정점에 해당하는 uv 좌표를 하나하나 설정해주고 있다. 위 이미지의 해상도는 100 * 100이다. uz 좌표계에서 (100, 0) 픽셀은 (1, 0)에 맵핑된다. (100, 100) 픽셀은 (1, 1)에 맵핑된다. 

 원기둥에 텍스처는 어떻게 맵핑되는지 살펴보자. 정점 버퍼 0번째 정점의 uv 좌표는 (0, 1)이다. 1번째 정점의 uv 좌표는 (0, 0)이다. 2번째 정점의 uv 좌표는 (1/49, 1), 3번째 정점의 uv 좌표는 (1/49, 0)이 된다. 결국 정점의 uv 좌표는 텍스처 이미지를 세로로 50등분한 수치가 정점에 맵핑된다. 

 참고로 텍스처로 사용할 파일은 main 함수가 실행되는 소스 파일과 같은 경우에 있거나, 그보다 한 단계 상위 폴더에 위치해야 한다. 이는 OS가 제공하는 현재 디렉토리(Current Directory) 개념과 관련이 있으므로 생소한 사람은 찾아보면 좋을 것 같다.


정리하면
  • 텍스처 이미지는 uv 좌표를 가진다.
  • 텍스처 맵핑을 하기 위해서 필요한 과정을 숙지해두자.

[C++]#ifdef...#endif, #ifndef...#endif, #ifdef...#else...#endif

 특정 조건에 따라서 다른 코드를 컴파일하고 싶은 경우가 있다. 예를 들면 1으로 설정하면 A라는 코드가 컴파일되고, 1이 아닌 경우에는 B라는 코드가 컴파일되도록 하고 싶다. 이럴 경우 사용하면 좋은 것이 바로 #ifdef...#endif 이다. #ifdef...#endif는 여러 라이브러리에서 자주 사용되는 만큼 한 번 이해하고 넘어가는 것이 좋다. 간단하게 이해할 수 있으므로 이번 기회에 짚고 넘어가자.


#ifdef...#endif

 3D 공간에서 정점을 나타내는 구조체를 만들고자 한다. 만약 색상값이 있는 경우에는 이 구조체에 색상값을 나타내는 변수가 필요하지만, 색상값이 없는 경우는 색상값을 나타내는 변수가 필요없다. 그래서 색상값이 존재하는지 여부를 사용자가 #define을 통해 지정하면 컴파일러가 알아서 색상값을 나타내는 변수를 넣거나 빼도록 만들고 싶다. 이를 어떻게 처리할지 알아보자.

 먼저 색상값이 있는 정점이라고 선언해보자.
 보통 우리는 #define을 매크로 함수를 만들고자 사용한다. 혹은 숫자 형태의 상수를 변수명처럼 사용하고 싶을 때 자주 사용한다. 예를 들면 DirectX9에서 PI 값은 다음과 같이 정의되어 있다.
 #define은 전처리 단계에서 처리되므로 전처리 과정에서 D3DX_PI는 모두 ((FLOAT)  3.141592654f)으로 치환될 것이다. 그렇다면 위의 경우처럼 #define VERTEX_WITH_COLOR와 같이 선언된 경우 VERTEX_WITH_COLOR를 무엇이란 말인가? 아무런 값으로도 정의내리지 않았는데, 이를 어떻게 설명해야 하나? 참고로 #define 다음에 나오는 VERTEX_WITH_COLOR와 같은 것을 Identifier라고 부른다. C++ 컴파일러는 이 경우 다음과 같이 해석한다.
 그러면 처음 말한 것처럼 색상값에 따라 구조체를 다르게 설정할 수 있도록 코드를 완성해보자.
 #ifdef를 사용하고 #endif를 사용하지 않으면 컴파일 에러를 일으킨다. 반드시 #endif를 사용하여 어느 영역까지 #ifdef에 해당하는 범위인지 설정하도록 하자.


#ifndef...#endif

 #ifdef...#endif에 대해 이해하고 있다면 #ifndef는 따로 설명할 필요가 없다.
 #ifdef가 identifier가 설정되었는지를 묻는다면 #ifndef는 identifier가 설정되지 않았는지를 묻는다. n이 'and'를 의미하는 것이 아니라 'not'을 의미한다. 구체적인 코드는 생략해도 될 것같다.


#ifdef...#else...#endif
 if...else 문을 사용하는 것과 유사하게 사용하면 된다.


정리하면

  • #ifdef...#endif를 사용하여 조건부 컴파일이 가능하다.
  • #ifdef 'identifier' 형태로 선언하면 #ifdef 'identifire'는 TRUE를 반환한다.

[DirectX9]광원(Lights)

 3D 그래픽스에서 광원은 단순히 물체를 보이게 만들어주는 역할에 그치지 않는다. 우리가 살아가는 물리 공간에서 그러하듯이 광원은 물체의 색을 결정하는 중요한 요소이다. 광원을 어떻게 설정하느냐에 따라 물체가 색을 지니게 되고, 그 색에 따라 물체 표면의 색이 결정된다. 지금부터 3D 그래픽스에서 사용되는 광원 모델과 DirectX9에서 이 광원을 사용하는 방법에 대해서 알아보겠다.


Local Illumination Model

 현실의 물리 공간은 빛이 계속적으로 반사하여, 셀 수 없이 수많은 빛이 합해진 결과로 물체의 색상이 결정된다. 우리가 당연하게 생각하고 살아가는 공간 속에 물체의 색은 이렇게 결정된다. 아래의 그림은 물리 공간에서 빛이 반사되어 색이 결정되는 모델을 흉내내어 그래픽 처리한 결과이다.


 붉은 벽과 푸른 벽에 빛이 반사되고, 이 색이 흰 색 구의 표면에 비치게 된다. 그래서 구면이 은은하게 붉은 색과 푸른 색을 띈다. 현실과 가장 유사하게 표현할 수 있다는 장점을 지닌 이런 광원 모델이 바로 Global Illumination Model이다. 하지만 Global Illumination Model은 치명적인 단점이 있다. 연산량이 지나치게 많다는 점이다. 광원으로부터 나온 빛이 끊임없이 반사되어 물체의 재질과 상호작용하는 모든 과정을 연산하는 것은 심각한 성능 저하를 발생시킨다. 그래서 반사되는 횟수를 제한하여 Global Illumination Model의 성능 회적화를 꾀하기도 한다.

 하지만 게임과 같은 Realtime Rendering 환경에서 일반적으로 Local Illumination Model을 사용한다. Local Illumination Model은 성능을 향상시키기 위해서 Global Illumination Model처럼 복잡한 연산 과정을 거치는 대신에, 이 과정을 간소화하여 표현한 모델이다. Local Illumination Model 중 Phong Reflection Model을 예로 들어 살펴보자.


Phong Illumination Model


주변광(Ambient Light)

  Phong Reflection Model은 세 개의 항으로 이루어져있다. Ambient, Diffuse, Specular 세 개의 항은 바로 그것들이다. Ambient 항은 주변광을 의미한다. 주변광이란 광원의 위치와 무관하게 똑같은 양으로 모든 점에 반사되는 색을 의미한다. 실제 물리 공간에서는 빛에 직접 노출되지 않는 곳도 눈으로 볼 수 있다. 이것이 가능한 이유는 물체 면에 반사된 빛이 다른 물체면에 반사되어, 이런 과정이 반복적으로 이뤄지다보면 빛에 직접 노출되지 않는 면에도 빛이 도달하게 된다. 하지만 이런 과정을 컴퓨터로 연산하고 시뮬레이션하기는 쉽지 않다. 그래서 Phong Reflection Model에서는 이런 형태의 반사를 연산하지 않고, 시점의 위치와는 무관하게 모든 물체면에 대해서 일정하게 빛이 비춰진다고 설정한다. 위 그림 가장 왼쪽에 보이는 Ambient를 살펴보자. 주변광은 거리와 상관없이 모든 물체면에 빛이 일정하다 비친다고 하였다. 그래서 물체의 굴곡에 상관없이 일정한 밝기의 빛이 비춰지므로 주변광만 비춰진다면 모든 물체는 동일한 색을 띄게 된다. 주목할 사실은 주변광은 모든 물체면에 동일하게 비춰지기 때문에 주변광만으로는 입체감을 전혀 느낄 수 없다는 점이다. 주변광을 수식으로 표현하면 다음과 같다.


 K는 주변광 계수를 나타내고, L은 주변광에 관해서 물체면에 입사되는 입사광의 세기를 나타낸다. K를 증가시키면 물체의 모든 면이 동일하게 밝아진다.


확산광(Diffuse Light)

 이제 Diffuse 항에 대해서 알아보자. Diffuse는 확산광을 의미한다. 먼저 아래의 그림을 보도록하자.

 L은 광원을 나타내는 벡터이고, R은 표면에 반사한 빛을 나타내는 벡터이다. 표면에 비치닌 빛의 세기는 무엇에 영향을 받을까? L과 N이 이루는 각도에 영향을 받는다. 이 각도가 작으면 작을수록 표면에 도달하는 빛의 세기는 강해진다. 정오에 태양빛이 가장 뜨겁게 느껴지는 것과 같은 원리이다. 이를 수식으로 근사화하여 표현할 수 있는데, 좋은 모델이 될 수 있는 것이 바로 코사인 함수이다.


 코사인 함수에서 볼 수 있듯이 L과 N이 이루는 각도가 0일 경우 1, 90도 일 경우 0이 된다. 빛의 세기를 표현하는 모델에 대한 한트를 얻었으니 확산광을 나타내는 수식을 살펴보자.


 L과 N을 정규화한 벡터가 각각 l과 n이라면, l과 n을 내적한 결과는 코사인 값이 된다. 주변광과 마찬가지로 K는 확산광 계수, L은 확산광에 관해서 물체면에 입사되는 입사광의 세기를 나타낸다.


반사광(Specular Light)

 반사광은 우리가 흔히 하이라이트라고 말하는 현상을 표현한다. 당구공처럼 매끈한 물체의 표면에 빛을 비추면 물체의 표면색과는 상관없이 반짝이는 현상을 확인할 수 있다. 이를 하이라이트라 부른다. 반사광은 이런 하이라이트를 표시하는 빛이다. 먼저 어떤 경우 반사광을 확인할 수 있는지 알아보자.


 반사광은 확산광의 방향과 이를 바라보는 시점의 방향이 일치하면 확인할 수 있다. 위의 그림에서 세타 값이 0에 가까울수록 반사광의 세기는 강해진다. 이는 확산광을 표현하는 것과 유사한 모델을 적용할 수 있다는 생각이 든다. 각도의 크기가 0인 경우 반사광의 세기는 1, 각도가 커지면 반사광의 세기가 점점 작아진다를 사실을 코사인 함수로 표현할 수 있다.


  r은 표면에 반사된 빛의 방향을 나타내는 벡터를 정규화한 것이고, v는 시점의 위치를 정규화한 벡터이다. r과 v를 내적하면 코사인 값만 남게 된다. 결국 우리가 표현하고자 했던 반사광을 수식으로 잘 나타낼 수 있음을 확인할 수 있다.

 위의 수식에서 알파 값이 의미하는 것은 하이라이트의 크기이다. r과 v를 내적한 값은 0에서 1 사이 값을 지니게 된다. 알파 값이 커질 수록 결국 빛의 세기는 점점 약해진다. 이를 코사인 그래프로 확인하면 다음과 같다.


  알파 값이 커지면 커질수록 하이라이트의 크기는 작아지게 된다. 이 알파값을 광택 계수(Shineness Coefficient)라고 부른다.


완성된 Phong Reflection Model


 주변광, 확산광, 반사광을 합한 결과가 바로 Phong Reflection Model의 결과이다. 

 이 모델은 광원의 특성에 해당하는 I 값들, 물체 특성을 나타내는 K 값들(반사 계수)을 정해지면, 그에 따른 결과가 나온다. 그리고 I 값과 K 값은 R, G, B로 세분화되어 표현된다. 


DirectX9의 광원 사용

 DirectX9에서 광원을 사용할 때는 지금까지 설명한 Local Illumination Model과 같은 복잡한 과정을 거칠 필요없이 프레임워크에서 제공하는 재질과 광원만 설정해주면 손쉽게 사용할 수 있다. 하지만 DirectX9에서 사용하는 광원 모델은 고정된 파이프라인을 사용할 때만 유효하고, 셰이더를 사용하게 되면 쓸모없게 된다고 한다. 그러니 알아두도록 하자.

 다시 DirectX9로 돌아와서 재질에 대해서 알아보도록 하자. 재질은 물체의 표면 상태를 말하는 것으로, 빛이 재질에 반사되어 변화되는 과정을 계산하기 위해 설정하는 값이다. 재질의 종류에는 네 가지가 있다.
  • 주변색(Ambient): 광원의 위치와 무관하게 똑같은 양으로 모든 점에서 반사되는 색
  • 확산색(Diffuse): 광원에 반사될 때 출력되는 가장 주된 색
  • 반사색(specular): 특정한 방향으로만 반사되는 색, 광원의 위치와 카메라의 위치에 따라 달라진다.
  • 방출색(emissive): 메시 표면에서 자체적으로 방출되는 색(이 색이 다른 메시에 영향을 주지는 못한다)
 재질을 설정하는 코드는 다음과 같다.

 재질 설정이 끝나면 광원을 설정한다. DirectX9에서 광원은 네 종류가 있다.
  • 주변 광원(Ambient Light): 3차원 공간 내에서 메시의 배치나 위치와는 전혀 상관없이 똑같은 양으로 모든 곳을 비추는 빛의 강도(방향, 위치를 가지지 않으며 색과 강도만을 가진다)
  • 점 광원(Point Light): 백열전구와 같은 빛이다. 광원의 위치에 따라 빛의 강도가 달라진다.
  • 방향성 광원(Directional Light): 태양과 같이 하나의 방향으로 비춰지는 빛이다. 광원의 위치는 상관없고, 방향이 가장 중요한 요소이다.
  • 점적광원(spot Light): 정해진 위치와 범위에만 비추는 특수한 조명이다. 무대 조명을 생각하면 이해하기 편하다.
 광원을 설정하는 코드는 다음과 같다.


원통을 빙글빙글 도는 방향성 광원



정리하면
  • 재질은 메시(혹은 물체)의 표면 상태를 의미하는 것으로 빛이 물체의 표면에서 반사되어 변화되는 과정을 수학적으로 모델링하기 위해 사용하는 값들이다.
  • DirectX9에서는 재질과 광원을 설정하며 물체에 작용하는 빛을 표현한다.

2015. 2. 24.

[DirectX9]월드 변환(World Transform), 뷰 변환(Viewing Transform), 투영 변환(Projection Transform)

 3D 그래픽스에서 행렬을 빼놓을 수 없는 도구이다. 행렬을 통해 그래픽스에서 필요한 여러 변환을 간단하게 표현할 수 있다. 그래서 렌더링 파이프라인의 단계를 구분할 때, 어떤 변환이 이루어지는지 즉, 어떤 행렬이 곱해져서 변환되는지를 기준으로 판단하기도 한다. 이번에는 그래픽스에서 행렬을 활용하는 예 중에서도 가장 중요한 월드 변환, 뷰 변환, 투영 변환에 대해서 알아보고자 한다.


간단한 렌더링 파이프라인


 그림은 간단한 형태의 렌더링 파이프라인을 설명하고 있다. 한 정점이 입력되면 이 정점은 렌더링 파이프라인 과정을 거쳐서 출력되고, 우리는 출력된 정보를 디스플레이 장치를 통해 확인할 수 있다. 먼저 정점이 입력되면 이 정점은 월드 변환(World Transformation), 뷰 변환(Viewing Transformation), 투영 변환(Projection Transformation)의 과정을 거치게 된다. 그리고 클리핑(Clipping)과 같은 추가적인 과정을 더 거치게 된다. 추가적인 과정은 위의 그림에서는 표현하지 않았다. 중요한 사실은 우리가 디스플레이로 확인할 수 있는 영상은 결국 2D로 변환된 영상이다. 하지만 우리는 DirectX9와 같은 프레임워크를 사용하여 3D  좌표 공간을 대상으로 프로그래밍한다. 여기서 알 수 있는 사실은 3D 정보가 렌더링 파이프라인을 거쳐서 2D 정보로 변환된다는 점이다. 실제로 렌더링 파이프라인을 거치면 3D 정보는 2D 정보로 변환된다.

 아래 그림은 렌더링 파이프라인의 전체적인 모습을 보여준다.


 그렇다면 월드 변환, 뷰 변환, 투영 변환은 각각 무엇을 의미하며, 어떻게 이뤄지는지 알아보자. 그리고 코드에서는 어떻게 구현되는지도 살펴보자.


월드 변환

 우리가 월드 공간에서 나타내고자 하는 기하물체들은 정점들의 모임으로 표현된다. 직육면체의 경우 8개의 정점으로 이뤄져있다. 그렇다면 직육면체를 모델링할때 기준이 되는 좌표계는 무엇인가? 로컬 좌표계 혹은 모델 좌표계이다. 


 모델 좌표계를 기준으로 물체를 구성하는 정점이 정의되고, 이 정점들의 모임으로 물체를 나타낼 수 있다. 그러면 이런 모델들이 3개가 모여있는 공간 즉, 월드를 나타내고자 할 경우는 어떻게 해야할까? 각각의 모델은 자신의 기준 좌표계가 있다. 이 기준 좌표계의 (0, 0, 0)을 기준으로 3개의 모델을 나타내면 어떻게 될까? 아마 3개의 물체가 겹쳐져서 그려질 것이다. 원래 월드 공간에서 3개의 모델을 겹치게 나타내고 싶다면 아무런 문제가 없지만, 3개의 모델이 겹쳐지지 않게 그리고 싶으면 모델 좌표계를 월드 좌표계로 그대로 사용할 수 없다. 모델 좌표계가 월드 좌표계의 원점을 기준으로 얼마나 떨어져있는지를 나타낸다면 해당 모델은 다른 모델과 구분될 수 있을 것이다. 이런 생각이 바로 월드 변환의 아이디어이다.


 직육면체, 삼각뿔, 원기둥 모두 각자의 모델 좌표계를 기준으로 물체가 정의되어 있다. 이를 월드 공간에 배치하기 위해서 각각의 모델 좌표계를 월드 좌표계를 기준으로 이동시키면 된다. 이동 행렬(Translate Matrix)를 모델 좌표계에 곱해주면 모델을 구성하는 각 정점은 월드 좌표계를 기준으로 하는 정점으로 변환된다.



뷰 변환, 카메라 변환

 월드 공간 상에 물체를 배치했다. 배치된 물체를 보기 위한 카메라가 필요하다. 월드 공간 상의 물체가 이제 카메라의 위치를 원점으로 하는 좌표계에 배치된다. 이렇게 3차원 월드 좌표계를 카메라의 위치를 기준으로 한 카메라 좌표계로 변환하는 것을 뷰 변환 혹은 카메라 변환이라고 한다.

 모델 좌표계, 월드 좌표계, 카메라 좌표계로 변환하는 과정을 다시 살펴보자.

세 물체가 각각 모델 좌표 공간에 위치한다.

세 물체가 월드 좌표 공간에 위치한다.

두 물체가 카메라 좌표 공간에 위치한다.

 월드 공간에 위치하는 물체를 카메라 좌표 공간으로 변환하기 위해서는 변환 행렬이 필요하다. DirectX9에서는 카메라 변환 행렬을 쉽게 계산할 수 있는 함수를 제공하고 있다. D3DXMatrixLookAtLH(), D3DXMatrixLookAtRH()가 카메라 변환 행렬을 계산하는 함수이다.

  • AT: 카메라가 바라보는 지점
  • EYE: 카메라의 위치
  • UP: 카메라의 상향 벡터
 함수의 인자를 전달하면 카메라 변환 행렬이 계산된다.


투영 변환

 월드 좌표계와 카메라 좌표계는 모두 3차원 좌표계이다. 우리가 디스플레이로 확인하게 되는 렌더링 결과들은 모두 2차원의 화면이다. 이는 3차원 좌표계를 2차원 좌표계로 바꾸는 변환이 필요하다는 것을 알 수 있다.

 3차원 좌표계를 어떻게 2차원 좌표계로 변환할 것인가? 가장 쉬운 방법은 3차원 좌표계의 한 축을 없애버리면 된다. 예를 들어 x, y, z 축 중에서 z 축을 제거하면, 즉 z 값을 모두 0으로 만들면 자동적으로 x, y 축으로 구성된 2차원 좌표계로 변환된다. 하지만 이런 방식으로 투영 변환하게 되면 z 값으로 구별할 수 있었던 물체 간의 앞뒤 정보 즉, 깊이 정보가 투영 변환을 거치게 되면 소실된다. 참고로 이런 문제를 해결 위해 z 버퍼(z-buffer)가 필요하다.

 먼저 직교 투영(Orthographic Projection)와 원근 투영(Perspective Projection)에 대해서 살펴보자.




 위의 그림은 직교 투영과 원근 투영의 방법과 결과에 대해 보여주고 있다. 직교 투영과 원근 투영의 가장 큰 차이는 투영 결과이다. 위의 그림에서 확인할 수 있듯이 직교 투영은 물체가 뒤에 있더라도 앞에 있는 물체와 크기 차이가 없다. 원근 투영은 이와 달리 앞에 있는 물체는 크게, 뒤에 있는 물체는 작게 나타낸다.

 우리는 특별한 경우가 아니면 원근 투영을 사용하게 될 것이다. 투영 변환을 하면 3차원 좌표 공간에은 2차원 좌표 공간으로 변환된다. 카메라 변환 행렬처럼 투영 변환 행렬을 계산해주는 함수가 있다.
 함수 인자를 전달하면 투영 변환 행렬이 계산된다.


세 가지 중요한 변환들
  • 월드 변환(World Transform)
  • 뷰 변환(View Transform)
  • 투영 변환(Projection Transform)
이 변환을 위한 행렬들을 모두 D3D 디바이스에 설정하면 정점은 이 변환을 거치게 된다. 그리고 변환을 거친 결과들이 디스플레이에 표시된다.

 월드 행렬을 설정하는 부분은 y축 회전을 표시하기 위한 코드가 추가되어 있다. 뷰 행렬과 투영 행렬을 계산하여 D3D 디바이스에 적용하는 하고 있다.


y축을 기준으로 회전하는 삼각형


정리하면
  • 정점은 월드 변환, 뷰 변환, 투영 변환을 거친다.
  • 해당 변환 행렬을 계산하여 D3D 디바이스에 설정하면 된다.

다음 주제: [DirectX9]광원(Lights)

[DirectX9]정점 버퍼(Vertex Buffer)

 3D 그래픽스에서 기하물체는 정점(Vertex)로 표현된다. 정점은 기본적으로 x, y, z 좌표 값을 가지고, 색상값과 같은 추가적인 정보도 가질 수 있다. 이런 정점을 연결하면 선분이 되기도 하고, 삼각형과 같은 기하물체를 이루기도 한다. DirectX에서는 정점을 좀더 효율적으로 관리하고 사용하기 위해서 정점 버퍼(Vertex Buffer) 기능을 제공한다. 이 정점 버퍼는 무엇이고, 정점 버퍼를 사용하는 방법은 어떻게 되는지 알아보자.


FVF(Flexible Vertex Format)

 FVF는 사용자가 직접 정점의 구조를 정의해서 사용할 수 있도록 DirectX에서 제공하는 기능이다. 사용자가 필요로 하는 정점 포맷을 선택하여 조합할 수 있다.


 위의 그림은 FVF를 구성하는 요소를 보여주고 있다. x, y, z는 정점의 좌표값, w는 동차 좌표계를 표시할 때 사용하는 w값이다. 다음은 확산광, 반사광, 텍스처 좌표가 뒤를 따른다. FVF는 이런 값들의 조합으로 이루어진다. 그러면 실제로 코드에서 어떻게 사용하는지를 살펴보자.
 먼저 사용자 정의 정점 구조체를 선언하였다. 구조체 내부에는 x, y, z, w 값과 함께 정점의 색상값도 선언해두었다. 우리가 사용할 정점은 이 정보만 가지는 정점이다. 반사광, 텍스처 좌표값 등은 우리가 정의한 정점에서는 사용되지 않는다. 이제 정의한 정점 구조체에 대한 플래그 선언을 해주자. 플래그 선언은 D3D 디바이스가 사용자 정의 정점 구조체에 대한 정보를 파악하기 위해 필요하다. 다시 말해 플래그 선언을 통해 사용자는 D3D 디바이스에게 사용자 정의 정점 구조체가 어떤 정보들을 담고 있는지 알려준다. 다음에 또 설명하겠지만 이 플래그 선언은 정점 버퍼를 생성할 때도 사용된다.

 그리고 사용자 정의 정점 구조체를 정의할 때는 그 순서에 주의해야 한다. FVF 플래그를 설정하는 D3DFVF_XYZRHW는 이미 그 순서가 정해져 있다. 반드시 x, y, z, rhw 순서로 이루어져야 한다. 하지만 우리가 다음과 같이 사용자 정의 정점 구조체를 선언하면 어떤 결과가 나올지 장담할 수 없게 된다.
 사용자의 의도와는 달리 rhw값은 x값으로, x값은 y값으로, y값은 z값으로, z값은 rhw값으로 정의되고 있다.

 이제 사용자 정의 정점 구조체, FVF 플래그 설정을 마쳤다. 사용자가 어떤 정점을 사용할지 설정을 끝낸 것이다. 이제 이 정점을 저장할 버퍼를 만들어 보자. 우리가 만든 임의의 정점 구조체에 대한 정보는 FVF 플래그 설정을 통해서 전달하면 되므로 어려울 것 없다.


정점 버퍼

 정점을 모아두는 일종의 메모리이다. 그런데 이 메모리가 단순한 배열이나 new, malloc() 등에 의한 메모리와 구별되는 것은 정점 처리만을 위해 만들어진 특수한 메모리이기 때문에 효율적이라는 점이다. 정점 버퍼는 크게 두 가지 메모리 영역을 사용한다고 한다. 하나는 비디오 메모리이고, 하나는 시스템 메모리이다.

 비디오 메모리에 생성된 정점 버퍼는 GPU의 하드웨어 가속을 사용할 수 있다는 장점이 있지만, 시스템 메모리에 비해 제한된 메모리 용량을 사용할 수 밖에 없다. 그나마 제한된 메모리 용량마저도 텍스처와 함께 사용하기 때문에 언제나 넉넉하게 사용할 수 있는 것이 아니다. 반면에 시스템 메모리에 생성된 정점 버퍼는 하드웨어 가속을 사용할 수 없다는 단점이 있지만, 비디오 메모리에 비해 넉넉한 용량을 사용할 수 있다. 이제 정점 버퍼를 만드는 함수를 살펴보도록 하자.
 우리가 CreateVertexBuffer() 함수를 통해 생성한 것은 정점 버퍼의 인터페이스이다. 정점 버퍼에 직접 접근할 수 있는 주소값을 얻은 것이 아니다. 정점 버퍼에 정점을 입력하기 위해서는 정점 버퍼의 주소값이 필요하다. 이를 위해서 사용하는 함수가 바로 Lock() 함수이다.
 세 번째 out parameter로 주소값을 얻어와서 그 주소값에 정점을 복사해서 넣으면 결과적으로 정점 버퍼에 정점을 보관할 수 있게 된다. Lock() 함수를 사용한 정점 버퍼는 반드시 Unlock()을 호출해야 한다는 점을 잊지말자. 결국 Lock() 함수와 Unlock() 함수는 항상 세트로 사용되어야 한다고 생각하자.

 지금까지 사용자 정의 정점 구조체를 선언하고, FVF 플래그를 설정하고, 정점 버퍼를 생성하는 방법에 대해서 알아보았다. 여기에 해당하는 내용을 코드로 살펴보자.
 정점 버퍼를 위한 변수 g_pVB가 선언되었다. 그리고 사용자 정의 정점 구조체 CUSTOMVERTEX를 선언하였고, FVF 플래그도 설정하였다. 그리고 D3D 디바이스를 통해 정점 버퍼를 생성하였고, 그 인터페이스를 g_pVB에 저장하였다. 생성한 정점 버퍼에 Lock() 함수를 사용하여 정점 정보를 입력하였고, Unlock() 함수를 호출하였다.

 정점 버퍼를 비디오 메모리에 생성하고, Lock() 함수를 호출하여 얻은 메모리 주소을 통해 비디오 메모리를 일반 메모리처럼 사용할 수 있다. 하지만 일반 메모리와 달리 비디오 메모리에 접근하는 것은 PCI-Express 등의 슬롯을 통해서 접근하기 때문에 일반 메모리에 접근하는 것과 달리 매우 긴 접근 시간이 필요하다. 만약 매 프레임마다 Lock() 함수를 호출하여 비디오 메모리 주소를 얻어와서 비디오 메모리에 접근한다면 병목이 발생하여 성능이 저하될 것이다.

 정점 버퍼를 위한 리소스가 생성되었으므로 이 리소스를 반환하는 코드도 추가하여야 한다. Cleanup() 함수에 여기에 해당하는 내용이 구현되어 있다.


정점 버퍼를 사용하여 그리기

 정점 버퍼를 사용하여 기하물체를 그리기 위해서는 다음의 과정을 거쳐야 한다.

  1. SetStreamSource() 함수를 호출하여 정점 버퍼와 디바이스의 데이터 스트림과 연결시킨다.
  2. SetFVF() 함수를 호출하여 디바이스의 정점 포맷을 지정한다.
  3. DrawPrimitive() 함수를 후출하여 정점 버퍼의 정점을 활용하여 기하물체를 그린다.
 이 과정을 보여주는 코드를 살펴보자.

 실제로 기하물체를 그리는 기능을 하는 함수는 DrawPrimitive() 함수이다.



정점 버퍼를 사용하여 삼각형 그리기



실행결과

 위와 같이 삼각형 내부의 색이 그라데이션 처리가 된 것처럼 채워진 이유는 DirectX9 내부적으로 폴리곤 내부를 칠하는 작업인 쉐이딩(shading)을 할 때, 정점의 색을 보간(interpolation)하기 때문이다.

정리하면

  • 사용자 정의 정점 구조체를 설정하고, FVF 플래그를 설정할 수 있다.
  • 정점 버퍼를 활용하면 하드웨어 가속을 받을 수 있다.
  • Lock() 함수를 통해 정점 버퍼가 생성된 비디오 메모리 주소를 얻어올 수 있다.
  • 비디오 메모리에 접근하기 위해서는 매우 긴 시간이 필요하므로 주의할 필요가 있다.

2015. 2. 23.

[DirectX9]Visual Studio 2013 설정

 Visual Studio 2013에서 DirectX9 설정하는 방법이 MSDN에 잘 나와있다. 영문으로 되어 있어서 필수적인 부분만 발췌해서 설명하겠다.


프로젝트 생성


 먼저 프로젝트를 생성하자. 나는 프로젝트명을 test라고 하였다. win32 Project로 생성하면 된다.


프로젝트 -> 속성


 지금부터 우리는 Include Directory, Library Directory, Addictional Dependencies를 변경할 것이다.


프로젝트 -> 속성 -> Include Directory 


 $(DXSDK_DIR)Include 추가


프로젝트 -> 속성 -> Library Directory 


$(DXSDK_DIR)Lib\x86 추가


프로젝트 -> 속성 -> Additional Dependencies


 d3d9.lib, d3dx9.lib 추가


정리하면

  • Include Directory 추가:  $(DXSDK_DIR)Include
  • Library Directory 추가: $(DXSDK_DIR)Lib\x86
  • Additional Dependecies 추가: d3d9.lib, d3dx9.lib

다음 주제: [DirectX9]디바이스 생성

2015. 2. 11.

[Windows Network Programming]도메인 이름 시스템(Domain Name System)

 우리가 Google에 접속하기 위해서는 두 가지 방법을 사용한다. 하나는 브라우저의 주소창에 google.co.kr을 직접 입력하는 방법이다. 다른 하나는 네이버와 같은 검색 사이트에서 google을 검색하여 사이트 링크를 통해서 접속하는 방법이다. 그런데 네이버에 접속하기 위해서는 우리는 naver.com이라고 주소창에 입력한다. 우리는 특정 사이트에 접속하기 위해서 IP 주소를 직접 입력하지 않는다. IP 주소 대신 도메인 이름을 입력한다. 이번에는 도메인 이름이 무엇이고, 이를 어떻게 사용할지 알아보도록 하자.


도메인 이름 시스템(Domain Name System)

 주소창에 IP 주소 74.125.224.17를 입력해보자. 어떤 사이트가 나오는지 확인해보자. 이 사이트는 Google이다. Google이라는 사이트에 접속하기 위해서는 기본적으로 Google의 IP 주소를 알아야 한다. 그런데 이 IP 주소를 외워서 Google에 접속하는 것은 매우 힘든 일이다. 숫자를 암기하는 것도 번거롭고, 숫자를 브라우저의 주소창에 입력하는 것도 귀찮은 일이다. 이런 문제를 해결하기 위해서 도메인 이름이 사용된다.

 도메인 이름이란 IP 주소와 대응하는 이름으로, 사람이 기억해서 사용하기 쉽게 만든 것이다. 그러면 우리는 대부분 도메인 이름을 입력하여 사이트에 접속하는데, 도메인 이름을 입력하면 이를 IP 주소로 변환해주는 역할을 하는 무언가가 필요하다는 생각이 들 것이다. 이런 역할을 하는 것이 바로 도메인 이름 서버(DSN Server, Domain Name System Server)이다. 이 서버는 IP 주소와 도메인 이름 사이의 변환 정보를 가지고 요청에 따라 IP 주소를 알려주는 역할을 한다. 그리고 어느 한 도메인 이름 서버가 모든 정보를 가지고 있지는 않다는 점에서 일종의 분산 데이터베이스이다.


 위 그림은 DNS Server가 어떻게 동작하는지 보여준다. 번호를 따라 살펴보자.
  1. 사용자: "http://www.howstuffworks.com에 접속해야 해."
  2. DNS Server1: "도메인 이름 http://www.howstuffworks.com이 나의 데이터베이스에 저장되어 있지 않네. 내가 다른 DNS Server에 알아볼게."
  3. DNS Server2: "내 cache에 그 데이터가 있어. 그 도메인 이름은 IP 주소 70.42.251.42야."
  4. DNS Server1: "좋아좋아. 이 데이터가 더 요청될지도 모르니깐 나도 그 데이터를 보관해야 겠다."
  5. 사용자: "IP 주소를 알려줘서 고마워."
 이 과정은 DSN Server의 역할과 함께 DSN Server가 분산 데이터베이스라는 점도 보여준다.


윈속에서 IP 주소와 도메인 이름 사이의 변환
 gethostbyname() 함수는 함수의 이름대로 도메인 이름으로 호스트의 IP 주소를 반환한다. msdn에 따르면 Windows Socket 2 애플리케이션 개발자는 gethostbyname() 함수 대신에 getaddrinfo() 함수를 사용하기를 권고하고 있다. 여튼 gethostbyname() 함수는 도메인 이름 -> IP 주소 변환을 사용할 수 있다. 반환형은 hostent의 포인터이다.
 hostent 구조체는 인자로 전달한 도메인 이름을 통해 얻을 수 있는 IP 주소를 저장할 수 있다.

 그러면 IP 주소를 가지고 도메인 이름을 알 수는 없을까? 가능하다. gethostbyaddr() 함수를 사용하면 된다.
 이 함수 hostent 구조체를 사용한다.


IP 주소 <-> 도메인 이름 변환 함수 응용


정리하면

  • 도메인 이름 시스템은 도메인 이름을 통해 IP 주소 정보를 제공한다.
  • HOSTENT 구조체는 도메인 이름, IP 주소를 저장한다.
  • gethostbyname() 함수, gethostbyaddr() 함수를 통해 도메인 이름, IP 주소 정보를 얻을 수 있다.

[Windows Network Programming]소켓 주소 구조체(Socket address structures)와 바이트 정렬(Byte Ordering)

 윈속, 소켓에 대한 대략적인 소개를 했다. socket() 함수를 통해 프로토콜까지 지정하였다. 하지만 구체적으로 IP주소, 포트번호 등에 대해서는 아직 언급하지 않았다. 이에 대한 정보는 소켓 주소 구조체에 저장된다.


소켓 주소 구조체(Socket address structures)

 네트워크 프로그램에서 필요로 하는 주소 정보를 담고 있는 구조체이다.

  • 주소 체계: TCP/IP 프로토콜을 사용한다면, 이 값은 AF_INET이 된다. 주소 체계는 윈속과 소켓을 설명하면서 이미 언급한 내용이다.
  • 주소 정보: 해당 주소 체계에서 사용하는 주소 정보를 담고 있는 배열이다. TCP/IP 프로토콜를 사용한다면 IP 주소와 포트 번호가 저장된다.
 실제 프로그래밍에서는 SOCKADDR 구조체를 사용하기보다는 사용할 프로토콜에 맞는 소켓 주소 구조체를 사용한다. 다음은 그 예시를 보여준다.
  • 기반 소켓 주소 구조체: SOCKADDR
  • TCP/IP: SOCKADDR_IN
  • IrDA: SOCKADDR_IRDA
그럼 우리가 가장 자주 사용할 TCP/IP의 소켓 주소 구조체인 SOCKADDR_IN에 대해서 알아보자.


SOCKADDR_IN

 이 구조체는 위에서 설명했듯이 TCP/IP의 소켓 주소 구조체이다. 구조체의 정의를 코드로 살펴보자.

  • 주소 체계: 항상 AF_INET값을 사용한다.
  • 포트 번호
  • IP 주소: in_addr 구조체를 사용한다.
  • sin_zero[8]: 실제 사용하지 않아서 0으로 채운다.
이 내용을 그림으로 표현하면 다음과 같다.
 주소 체계, 포트 번호 sin_zero[8]는 주석 그대로 받아들이면 된다. 좀더 살펴볼 것은 in_addr 구조체이다. 이 구조체는 IP 주소를 저장하기 위해 정의되어 있다. 
 공용체(union)가 나왔다. 링크를 참조해서 이해하도록 하자. 공용체를 사용하면 동일한 메모리 영역을 in_addr의 정의에서처럼 4개의 unsigned char 변수로 구성된 구조체, 2개의 unsigned short 변수로 구성된 구조체, 1개의 unsigned long 변수로 구성된 구조체 단위로 접근할 수 있다. 보통은 unsigned long 단위로 접근한다. 그래서 매크로로 재정의해서 사용하기 변하게 해두었다.

실제 소켓 주소 구조체를 사용하는 방법은 다음과 같다.
 코드 중간에 inet_addr() 함수, htons() 함수는 조금 후에 설명할 것이다. 소켓 주소 구조체를 초기화하는 형태만 보고 넘어가자. sin_zero[8]은 zeromemory()  함수를 통해 0으로 초기화되었다.


바이트 정렬 함수

 바이트 정렬(Byte Ordering)이란 메모리에 데이터를 저장할 때의 바이트 순서를 가리키는 용어로, 빅 엔디안(Big-Endian)과 리틀 엔디안(Little Endian) 방식이 있다.

  • 빅 엔디안: 최상위 바이트(MSB, Most Significant Byte)부터 차례대로 저장하는 방식
  • 리틀 엔디안: 최하위 바이트(LSB, Least Significant Byte)부터 차례대로 저장하는 방식

  이 그림은 레지스터에 0A0B0C0D 값이 메모리에 어떻게 저장되어 있는지를 나타내고 있다. 이 값은 16진수로 표현된 4byte 크기의 값이다. 이 값의 MSB는 0A이고, LSB는 0D이다. MSB는 어떤 값이 가지는 값 중에 가장 최상위 바이트의 값을 말한다. 0A0B0C0D에서 최상위 바이트 값은 0A이다. LSB는 최하위 바이트 값으로 0D를 말한다. 빅 엔디안은 MSB부터 차례대로 메모리에 저장하는 방식이다. 그러므로 0A, 0B, 0C, 0D 순서로 메모리 영역에 차례대로 저장된다. 바이트 단위로 저장되는 것을 명심하도록 하자. 리틀 엔디안은 LSB부터 차례대로 메모리에 저장하는 방식이다. 그러므로 0D, 0D, 0B, 0A 순서로 메모리 영역에 저장된다.



 이 그림도 마찬가지 사실을 말해준다. 8byte 크기의 데이터 1122334455667788을 메모리에 저장하고자 할때, 빅 엔디안 방식과 리틀 엔디안 방식으로 각각 어떻게 처리하는지 보여준다. Byte 0 ~ Byte 7의 의미는 해당 바이트의 데이터가 얼마나 큰가를 나타낸다. 예를 들면 Byte 7에 해당하는 11이라는 값은 2^60 +  2^56의 값을 가지는 숫자이다. 반면 Byte 0에 해당하는 88이라는 값은 2^7 + 2^3의 값을 가지는 숫자이다. 그래서 Byte 7은 데이터 1122334455667788에서 가장 높은 자리 숫자의 바이트를 의미하며, Byte 0은 가장 낮은 자리 숫자의 바이트를 의미한다. 이는 다시 말해 해당 바이트가 원래 나타내는 숫자의 크기로 볼 수 있다. 바이트 단위로 표시된 숫자를 원래 데이터로 변환할 때 적용되는 가중치라고 생각할 수 있다. 다음 그림이 바로 MSB와 LSB의 가중치에 대한 예이다. MSB의 1은 2^7에 해당하는 bit이고, LSB의 1은 2^0에 해당하는 bit이다. 같은 1이라는 표현이라고 실제로 의미하는 숫자의 크기, 즉 가중치는 다르다.(이 그림에서는 MSB, LSB의 B가 bit를 의미하고 있다)

 장황한 MSB, LSB와 빅 엔디안, 리틀 엔디안에 대한 설명을 이 정도로만 하고, 이제 바이트 정렬이 네트워크와 무슨 상관이 있는지 알아보자.


바이트 정렬을 고려해야 하는 경우

 파일에 데이터를 저장하고 읽는 경우, 네트워크를 통해 데이터를 전송하는 경우 등에 바이트 정렬에 유의해야 한다. 각 시스템이 빅 엔디안 방식인지 리틀 엔디안 방식인지에 따라 데이터를 저장하고 해석하는 방식이 완전히 달리지기 때문이다. 만약 A라는 시스템은 빅 엔디안 방식을 사용하고, B라는 시스템은 리틀 엔디안 방식을 사용한다고 가정하자. A가 네트워크를 통해 B에게 0x11223344라는 데이터를 전송하려고 한다. A는 빅 엔디안을 따르기 때문에 자신의 데이터를 그대로 보낼 것이다. 하지만 이 데이터를 받은 B의 경우 리틀 엔디안을 따르기 때문에 0x44332211로 해석할 것이다. 동일한 데이터라도 바이트 정렬에 따라 전혀 다른 데이터가 될 수 있다. 그러므로 어떤 방식으로 바이트 정렬을 사용할지 정하고 고려해야 한다.

  • IP 주소, 포트 번호의 바이트 정렬 방식은 빅 엔디안 방식으로 통일하여 사용한다.
  • 네트워크에서는 빅 엔디안을 네트워크 바이트 정렬(Network Byte Ordering)이라고 부른다. 시스템이 사용하는 고유한 바이트 정렬 방식을 호스트 바이트 정렬(Host Byte Ordering)이다.

바이트 정렬을 위한 유틸리티 함수
 각 함수의 기능은 주석으로 요약해두었다. htons() 함수는 host-to-network-short라는 의미로 호스트 바이트 정렬로 저장된 값을 입력으로 받아서 네트워크 바이트 정렬로 변환한 값을 반환한다. ntohs() 함수는 network-to-host-short라는 의미로 네트워크 바이트 정렬로 저장된 값을 입력으로 받아서 호스트 바이트 정렬로 변환한 값을 반환한다.


 만약 호스트가 리틀 엔디안으로 바이트 정렬되어 있다면, ntohs() 함수를 통해서 바이트 정렬이 바뀐다. 네트워트 바이트 정렬은 빅 엔디안 방식을 다르고, 호스트가 리틀 엔디안을 따르기 때문에 변환이 이루어진다. htons() 함수를 호출하면 어떻게 되겠는가? 호스트의 리틀 엔디안 바이트 정렬이 네트워크 바이트 정렬인 빅 엔디안으로 변환될 것이다.

 호스트가 빅 엔디안일 경우를 생각해보자. 호스트도 빅 엔디안이고, 네트워크고 빅 엔디안이다. ntohs() 함수를 호출해도 빅 엔디안에서 빅 엔디안으로 변환되므로 바이트 정렬에 변화가 없다. htons() 함수를 호출해도 결과는 마찬가지이다.

 특이한 점은 SOCKADDR_IN 구조체는 호스트 바이트 정렬과 네트워크 바이트 정렬 모두 사용한다.
  • sin_family: 호스트 바이트 정렬
  • sin_port, sin_addr: 네트워크 바이트 정렬
그러므로 SOCKADDR_IN 구조체를 초기화할 때는 바이트 정렬 함수를 사용해서 초기화해주어야 한다.
 앞에 등장한 소켓 주소 구조체 초기화 코드이다. addr.sin_port 변수를 초기화할 때, htons() 함수를 호출하였음을 확인할 수 있다. sin_port는 네트워크 바이트 정렬을 따른다. 그러므로 호스트 바이트 정렬을 네트워크 바이트 정렬로 변환해주었다.


IP 주소 변환 함수
 inet_addr() 함수는 IPv4 방식의 IP 주소가 저장된 문자열을 IN_ADDR 구조체에 맞게 변환해준다. 그러므로 문자열을 인자로 전달하여 이 함수를 호출한 결과값을 소켓 주소 구조체에 사용한다.

 그렇다면 소켓 주소 구조체에 보관된 아이피 주소를 확인하기 위해서는 어떻게 해야할까? inet_addr() 함수에서 힌트를 얻어보자면 IN_ADDR 구조체에 저장된 IP 주소를 ASCII 문자열의 형태로 반환해서 출력하면 될 것 같다. 이런 기능을 제공하는 함수가 바로 inet_ntoa() 함수이다.
 inet_ntoa() 함수를 사용해서 IP 주소를 확인해보자.
 참고로 inet_addr() 함수는 이미 네트워크 바이트 정렬 된 IP 주소를 반환하므로 htonl() 함수를 적용해서는 안 된다. inet_addr() 함수에 의해 sin_addr 구조체는 이미 네트워크 바이트 정렬을 따르기 때문이다.


정리하면
  • 윈속, 소켓을 생성 및 초기화한 후, 소켓 주소 구조체에 주소 체계, 포트 번호, IP 주소를 저장한다.
  • 바이트 정렬에는 빅 엔디안과 리틀 엔디안이 있다.
  • 네트워크 프로그래밍에서 호스트와 네트워크의 바이트 정렬 방식 간의 변환은 중요하다. 이를 위해 바이트 정렬 함수, IP 주소 반환 함수 등을 활용할 수 있다.