
1. 개요
정점 셰이더(VS)는 그래픽스 파이프라인에서 Input Assember 다음에 위치하는 첫 번째 프로그래밍 가능(Programmable) 스테이지이다.
IA가 조립한 모든 정점(Vertex)은 파이프라인을 통해 각 정점 당 정점 셰이더 프로그램이 한번씩 실행된다. (per vertex Operation)
VS는 고립된 환경에서 실행되며, 오직 현재 입력된 정점 1개의 데이터만 접근할 수 있고 다른 정점의 정보는 알지 못한다.
2. 좌표 변환

정점 셰이더는 좌표계 변환(Coordinate Transformation)을 담당한다.
- Object space (Model Space)
Object의 관점에서 정의된 좌표계이다. Object를 만들 때 사용되는 좌표계.
다른 Model과는 보통 관련 없다.
축은 Object와 함께 회전한다.
- World space
3D World(또는 우주)를 정의하는 좌표계이다.
3D World에는 여러 객체, 캐릭터 및 카메라가 포함되어, 단일 좌표계로 모든 모델을 모은다.
- Camera space
카메라의 관점에서 정의된 좌표계이다.
월드 공간 좌표계는 목표 카메라에 따라 재계산된다.
일부 응용 프로그램에서는 Eye space이라고도 한다.
- Clip space
카메라 공간의 점들에 투영 변환(Projection Transform)을 수행하여 정의된 좌표계이다.
이 좌표 변환은 보통 CPU로부터 상수 버퍼(Constant Buffer)를 통해 전달받은 3개의 행렬(Matrix)을 순차적으로 곱하여 수행된다.
WVP: World View Projection transform
3. World transform

각 객체 공간에서 정의된 모든 객체를 월드 공간이라는 단일 환경으로 조립하는 것이다.
아핀 변환으로 구성된 월드 행렬은 [L|t] 로 표시되며, L은 선형 변환 행렬이고 t는 평행 이동 행렬이다.

VS: 법선 벡터 변환 (Transform Vertex Normal)
1. 왜 법선을 다르게 변환하는가?
정점 셰이더는 정점(Vertex)의 위치와 법선(Normal)을 모델 공간에서 월드 공간으로 변환한다.
- 정점(위치): 월드 행렬 W = [L|t] (L: 선형 변환, t: 이동)을 그대로 곱한다.
- 법선(방향): 월드 행렬 W를 그대로 곱하면 렌더링(조명 계산)이 깨질 수 있다.
2. 원인: 비균일 스케일링 (Non-uniform Scaling)
법선은 항상 표면에 수직(90도)을 유지해야 하는 '방향' 벡터이다.
- 이동(Translation, t): 물체가 이동해도 표면의 방향은 변하지 않는다. 따라서 법선은 t의 영향을 받지 않는다.
- 회전 및 균일 스케일: 물체가 회전하거나 모든 축으로 동일하게 커지면(균일 스케일), 법선도 행렬 L을 곱해도 여전히 표면에 수직을 유지한다.
- 비균일 스케일: 물체가 X축으로만 3배 늘어나는 등(비균일 스케일), 축마다 다르게 스케일이 적용되면 문제가 발생한다.
- 표면(Surface)은 X축으로 3배 늘어나며 기울어진다.
- 이때 법선에도 똑같이 L (X축 3배 변환)을 적용하면, 법선이 기울어진 표면에 더 이상 90도를 유지하지 못하고 함께 찌그러진다.
3. 해결책: (L⁻¹)ᵀ (역행렬의 전치)
이 문제를 수학적으로 해결하기 위해, 법선 벡터는 L 대신 L의 역행렬의 전치(Inverse Transpose) 행렬, 즉 (L⁻¹)ᵀ (또는 L^-T)을 곱한다.
이 행렬을 사용하면, 비균일 스케일링으로 표면이 찌그러지더라도 법선은 항상 변형된 표면에 정확히 수직인 방향을 유지할 수 있다.
- 정점 변환: V_world = L * V_model + t
- 법선 변환: N_world = (L⁻¹)ᵀ * N_model
4. 결론: 왜 항상 (L⁻¹)ᵀ 를 사용하는가?
셰이더 코드를 작성할 때, 현재 물체에 적용된 월드 행렬 L이 비균일 스케일링을 포함하는지 여부를 매번 검사하는 것은 비효율적이다.
- 안전성: (L⁻¹)ᵀ는 비균일 스케일링이 있든 없든 항상 올바른 법선 방향을 계산해 준다. (비균일 스케일링이 없는 경우, L과 (L⁻¹)ᵀ는 방향이 같다.)
따라서 개발자는 비균일 스케일링 여부와 관계없이, 항상 (L⁻¹)ᵀ를 사용하여 법선을 변환하는 것이 가장 안전하고 일반적인 방법이다.
5. 최종 처리: 정규화 (Normalization)
(L⁻¹)ᵀ 행렬을 곱하는 과정에서 법선 벡터의 길이가 1이 아닐 수 있다. 조명 계산은 길이가 1인 단위 벡터(Unit Vector)를 기준으로 하므로, 셰이더 코드의 마지막 단계에서 변환된 법선을 **정규화(normalize)**하여 길이를 1로 다시 만들어야 한다.
// HLSL (셰이더 코드 예시)
// 1. (L⁻¹)ᵀ 행렬을 상수 버퍼로 받음 (혹은 월드 행렬로 계산)
float3 normal_World = mul(input.Normal_Model, (float3x3)WorldInverseTranspose);
// 2. 최종적으로 길이를 1로 정규화한다.
normal_World = normalize(normal_World);

4. View transform
1. Camera Pose and Camera Space

- EYE : 카메라 위치
- AT : 카메라가 조준하는 지점
- UP : 카메라 상단으로 수직한 벡터 (대부분 World Space의 vertical axis, y axis로 설정)
2. View Transform Matrix

- Camera Space는 (u, v, n, EYE), World Space는 (e1, e2, e3, O)
- World Space에 있는 모든 객체에 View Matrix를 적용하여, Camera Space로 전환한다. 따라서 Translate, Rotation을 포함한 변환 필요 행렬이 View Matrix이다.
5. Projection transform
1. View Frustum

- 카메라는 제한된 시야(Field of View)를 가지므로 씬의 모든 것을 한 번에 볼 수 없다.
- 이 영역은 일반적으로 카메라의 위치(EYE)를 꼭대기로 하는 사각뿔(Pyramid)에서 앞부분(Near Plane)과 뒷부분(Far Plane)을 잘라낸 모양 절두체 모양.
- 본질적으로, 뷰 프러스텀은 카메라의 눈(EYE)으로 수렴하는 투영 선들의 집합(a convergent pencil of projection lines)으로 구성된 원뿔형 공간이다.
- 이 "수렴"하는 특성 때문에 3D 점들이 2D 화면에 매핑될 때 원근 투영(Perspective Projection) 효과가 발생한다. (즉, 멀리 있는 객체가 더 작게 보인다..)
2. Projection Transform

Projection Transform은 이 비대칭적인 사각뿔 모양의 뷰 프러스텀을, 클리핑(Clipping)하기 편하도록 단순하고 표준화된 큐브(Cube) 형태로 변형(Warp)하는 변환 과정이다.
- 변환 전: 뷰 프러스텀 (사각뿔 모양)
- 변환 후: 클립 공간(Clip Space) (축에 정렬된 정육면체)
변환하는 이유: 클리핑 효율성하지만 원점에 중심을 둔 정육면체는 x, y, z 좌표가 특정 범위(예: -1에서 +1) 안에 있는지 비교하는 단순한 작업만으로 클리핑이 가능해져 매우 효율적이다.
뷰 프러스텀(사각뿔)은 기울어진 평면들을 가지고 있어, 어떤 정점이 이 공간 안에 있는지 밖에 있는지 판별하는 클리핑(Clipping) 연산이 매우 복잡하고 비용이 크다.
- convergent pencil of projection lines.

3. 클립 공간 (Clip Space)

투영 변환을 거친 후의 표준화된 공간을 클립 공간(Clip Space).
- 이 공간은 원점에 중심을 둔 축에 정렬된 큐브입니다.
- DirectX 기준: x: [-1, 1], y: [-1, 1], z: [0, 1] 범위의 2x2x1 크기 큐브.
- (참고: OpenGL은 z: [-1, 1] 범위를 사용하여 2x2x2 크기 큐브를 사용.
결론
- 변환 전: 투영 선들이 카메라의 눈(EYE)을 향해 수렴.
- 변환 중: 투영 행렬은 이 3D 공간을 왜곡(warp)시킨다.
- 변환 후: 모든 투영 선들이 Z축에 평행하게 변한다.
즉, 투영 변환은 복잡한 원근 투영(Perspective)을, 하드웨어가 처리하기 매우 간단한 직교 투영(Orthographic)으로 바꾼다.
이 변환된 '클립 공간' 좌표는 정점 셰이더(VS)의 최종 출력(SV_Position)이 된다.
4. Right-hand System vs Left-hand system


RHS <-> LHS
- 이미지가 반전되는 것을 막기위해, 오브젝트와 view의 z 좌표에 -1을 곱해야한다.
- (z-negation <-> z-axis inversion)
Projection Transform (OpenGL)
z-aixs가 뒤집혔고, view frsutum 크기가 2x2x2로 바뀐다.

6. 기타 역할
VS는 핵심 임무인 좌표 변환 외에도 다음 단계(래스터라이저, 픽셀 셰이더)에 필요한 데이터를 처리하고 전달하는 역할을 한다.
- 데이터 전달 (Pass-through): IA로부터 입력받은 텍스처 좌표(TEXCOORD), 정점 색상(COLOR) 등을 수정 없이 그대로 다음 단계로 출력한다.
- 데이터 변환: 조명 계산에 필요한 노멀 벡터(NORMAL)나 탄젠트 벡터(TANGENT) 등은 좌표 변환(주로 월드 변환)을 거쳐야 하므로, VS가 이 변환을 수행하여 출력한다.
7. 입력과 출력
5.1. 입력 (Input)
- From IA (입력 조립기): 입력 레이아웃(Input Layout)에 의해 정의된 단일 정점 데이터.
- 예: POSITION, NORMAL, TEXCOORD, COLOR ...
- From CPU (메모리): 상수 버퍼(Constant Buffers).
- 예: WVP 행렬, 시간(Time) 값, 애니메이션 뼈(Bone) 행렬 등 셰이더가 실행되는 동안 필요한 '상수' 데이터.
5.2. 출력 (Output)
- 필수 (Required):
- SV_POSITION 시맨틱(Semantic): 반드시 출력해야 하는 값. VS가 계산한 최종 '투영 공간' 좌표이다.
- 파이프라인의 다음 단계인 래스터라이저(Rasterizer)는 이 값을 기준으로 정점이 화면 어디에 위치하는지 판단한다.
- 선택 (Optional):
- 픽셀 셰이더(Pixel Shader)로 전달할 데이터들.
- 예: TEXCOORD, NORMAL, WORLD_POSITION ...
- 중요: VS가 출력한 이 값들은 래스터라이저에 의해 '보간(Interpolated)'되어 픽셀 셰이더로 전달된다.
8. (참고) 상수 버퍼와 물체별 행렬 처리
- 질문: 모든 물체는 각자 다른 월드 행렬을 가지는데, VS는 어떻게 이를 처리하는가?
- 답변:
- 월드 행렬은 CPU(게임 로직)가 매 프레임마다 물체별로 계산한다.
- GPU는 그리기 명령(Draw Call) 단위로 순차적으로 렌더링한다.
- CPU는 1번 물체(Object A)를 그리기 직전, 1번의 월드 행렬을 상수 버퍼에 업데이트(복사)한다.
- CPU가 Draw(Object A) 명령을 호출하면, VS는 상수 버퍼에서 1번 행렬을 읽어와 사용한다.
- 그다음, CPU는 2번 물체(Object B)의 월드 행렬을 동일한 상수 버퍼에 덮어쓰고 Draw(Object B) 명령을 호출한다.
- 이 과정을 모든 물체만큼 반복한다. VS 자체는 물체를 구분하지 않으며, 단지 '현재' 상수 버퍼에 들어있는 데이터를 사용할 뿐이다.
'그래픽스' 카테고리의 다른 글
| 06. Geometry Shader (0) | 2025.11.04 |
|---|---|
| 05. Domain Shader (0) | 2025.11.04 |
| 04. Tessellator (0) | 2025.11.04 |
| 03. Hull Shader (0) | 2025.11.04 |
| 01. Input Assembler (0) | 2025.11.04 |