3D 공간의 점을 화면 좌표로 변환하는 프로그램이다. MVP 행렬 변환을 수행한다.
버텍스 셰이더는 버텍스 하나당 한 번 실행한다.
버텍스 셰이더의 위치
CPU (드로우콜 발생)
↓
입력 어셈블러 (버텍스 데이터 읽기)
↓
버텍스 셰이더 <- 여기
↓
래스터화
↓
픽셀 셰이더
역할 : 버텍스(점) 하나를 받아서 변환된 버텍스를 출력하는 프로그램이다.
입력 : 3D 공간의 점 하나 (위치, UV, 법선...)
출력 : 클립 공간의 점 하나 + 픽셀 셰이더로 보낼 데이터
GPU가 버텍스 수만큼 이 셰이더를 병렬 실행한다.
메시에 버텍스 1000개 -> 1000번 병렬 실행.
MVP 행렬 변환 ( 버텍스 셰이더의 핵심 )
- 3D 오브젝트가 화면에 그려지기까지 좌쵸 공간을 단계별로 변환해야 한다.
Model 행렬 (M)
- 로컬 공간 -> 월드 공간 변환
- 오브젝트의 위치, 회전, 스케일 적용
ex)원점에 있는 큐브를 위치 (5, 0, 3)으로 이동한 뒤 Y축으로 45도 회전시킨 다음 크기를 2배로 늘린다 -> Model 행렬 하나로 처리
View 행렬 (V)
- 월드 공간 -> 뷰 공간 변환
- 카메라를 원점으로 이동시키는 변환
- 카메라가 움직이는게 아니라 카메라가 원점에 고정되고 세계 전체가 반대로 이동하는 것이다
Projection 행렬 (P)
- 뷰 공간 -> 클립 공간 변환
- 원근감 적용
- 원근 투영 (Perspective) : 멀리 있으면 작게 보인다. 시야각(FOV)을 적용하며 frustum을 잘라낸다
- 직교 투영 (Orthographic) : 거리와 상관없이 같은 크기이며 UI, 2D 게임, 도면에 사용된다.
결과 : gl_Position = P * V * M * vertex <- 반드시 이 순서로 곱한다. ( 행렬 곱 순서 중요하다 )
MVP 행렬의 수학
cbuffer TransformBuffer : register(b0) {
float4x4 ModelMatrix; // 오브젝트 변환
float4x4 ViewMatrix; // 카메라 변환
float4x4 ProjectionMatrix; // 원근 변환
float4x4 MVPMatrix; // 세 행렬 미리 곱한 것 (최적화)
float4x4 NormalMatrix; // 법선 변환용 (Model의 역전치)
};
struct VertexInput{
float3 Position : POSITION; // 로컬 공간 위치
float3 Normal : NORMAL; // 법선 벡터
float2 TexCoord : TEXCOORD0; // UV 좌표
float4 Tangent : TANGENT; // 탄젠트 (노멀 맵용)
};
struct VertexOutput {
float4 ClipPosition : SV_POSITION; // 클립 공간 위치 (필수)
float3 WorldPosition : TEXCOORD0; // 월드 공간 위치
float3 WorldNormal : TEXCOORD1; //월드 공간 법선
float2 TexCoord : TEXCOORD2; // UV (그대로 전달)
float3 Tangent : TEXCOORD3; // 탄젠트
float3 Bitangent : TEXCOORD4; // 바이탄젠트
};
VertexOutput VS(VertexInput input) {
VertexOutput output;
// 1. 클립 공간 변환 (MVP 한 번에)
output.ClipPosition = mul(
float4(input.Position, 1.0),
MVPMatrix
);
// 2/ 월드 공간 위치 (조명 계산에 필요)
output.WorldPosition = mul(
float4(input.Position, 1.0),
ModelMatrix
).xyz;
// 3. 법선 변환 (Model의 역전치 행렬 사용)
// 단순히 Model 행렬을 쓰면 비균등 스케일 시 법선이 틀어진다.
output.WorldNormal = normalize(
mul(input.Normal,
(float3x3)NormalMatrix)
);
// 4. 탄젠트 변환 (노멀 맵 TBN용)
output.Tangent = normalize(
mul(input.Tangent.xyz,
(float3x3)ModelMatrix)
);
// 5. 바이탄젠트 계산
output.Bitangent = normalize(
cross(output.WorldNormal, output.Tangent) * input.Tangent.w // 손방향(+- 1)
);
// 6. UV는 변환 없이 그대로 전달
output.TexCoord = input.TexCoord;
return output;
}
법선 변환에 역전치 행렬이 필요한 이유
-> 비균등 스케일의 문제
- 오브젝트를 X축으로 2배 늘릴 경우 버텍스 위치는 Model 행렬로 올바르게 변환다. 법선을 같은 행렬로 변환하면 법선이 표면과 더 이상 수직이 아니다.
ex) 원래 : 표면 수평, 법선 수직 (0,1,0) X 2배 스케일 후 :
- 표면이 더 완만해진다.
- 법선을 그냥 X 2배 하면 (0, 1, 0) -> 기울어진다
- 실제로는 표면에 수직인 방향으로 회전해야 한다.
해결 : 역전치 행렬 (Invers Transpose)
Normal = normalize( (ModelMatrix^-1)^T * inputNormal )
균등 스케일(x,y,z 같은 배율)이면 문제가 없지만 비균등 스케일에서 반드시 역전치 필요하다.
버텍스 셰이더로 할 수 있는 것들
1. 기본 : 좌표 변환 (모든 메시에 필수)
- MVP 행렬 곱 -> 화면 좌표로 변환
2. 스키닝 (캐릭터 애니메이션)
- 각 버텍스가 여러 본(Bone)의 영향을 받는다
- 본 행렬 x 가중치로 최종 위치 계산한다
float3 skinnedPos = float3(0,0,0);
for(int i = 0; i < 4; i++) {
skinnedPos += mul(
boneMatrices[boneIndices[i]],
float4(position, 1.0)
).xyz * boneWeights[i];
}
3. 인스턴싱 (Instancing)
- 같은 메시를 여러 위치에 배치한다
- 각 인스턴스의 Transform을 버퍼로 전달한다
- SV_InstanceID로 어떤 인스턴스인지 구별한다.
instanceTransform = transformBuffer[instanceID];
output.position = mul(vertex, instanceTransform);
-> 드로우콜 1개로 수천 개 오브젝트 렌더링
-> UE5 ISM/HISM의 핵심 동작 원리이다.
4. 디졸브 / 클리핑 효과
- 버텍스를 조건에 따라 화면 밖으로 보낸다.
- 완전히 숨기는 것보다 유연한 제어
if(dissolveAmount > threshold) {
output.position = float4(2,2,2,1); // 클립 공간 밖
}
버텍스 셰이더 최적화
버텍스 병목 발생 조건
1. 버텍스 수가 매우 많을 때
- 메시 폴리곤 수 x 드로우콜 수
- 수백만 버텍스 x 무거운 셰이더
2. 스키닝이 복잡할 때
- 본 수가 많은 캐릭터
- 다수의 캐릭터를 동시에 렌더링
3. WP0 계산이 복잡할 때
- 삼각함수 남발
- 텍스처 샘플링 (버텍스 셰이더에서도 가능하다)
최적화 방법
1. LOD로 원거리 버텍스 수 감소
- 버텍스 수가 줄면 버텍스 셰이더 실행 횟수 감수
2. 연산을 CPU로 이동
- 프레임마다 바뀌지 않는 계산 -> CPU에서 미리 계산 후 uniform으로 전달한다.
3. 스키닝을 컴퓨트 셰이더로
- GPU Skinning -> Compute Shader
- 렌더링 파이프라인과 분리해 병렬 처리한다.
4. WP0 비용 제한
- 복잡한 수식 -> 단순화
- 텍스처 기반 WP0 -> 밉맵 최저 레벨 사용
GLSL vs HLSL 버텍스 셰이더 비교
GLSL (OpenGL / OpenGL ES)
#version 330 core
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layouy(location = 2) in vec3 aTexCoord;
out vec3 vWorldPosition;
out vec3 vWorldNormal;
out vec2 vTexCoord;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
uniform mat3 uNormalMatrix;
void main() {
vec4 worldPos = uModel * vec4(aPosition, 1.0);
vWorldPosition = worldPos.xyz;
vWorldNormal = normalize(uNormalMatrix * aNormal);
vTexCoord = aTexCoord;
gl_Position = uProjection * uView * worldPos;
}
HLSL (DirectX / UE5)
cbuffer Transforms : register(b0) {
float4x4 Model;
float4x4 View;
float4x4 Projection;
float3x3 NormalMatrix;
};
struct VIn {
float3 pos : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct VOut {
float4 clipPos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float2 uv : TEXCOORD2;
};
VOut VS(VIn v) {
VOut o;
float4 worldPos = mul(float4(v.pos,1), Model);
o.worldPos = worldPos.xyz;
o.worldNormal = normalize(mul(v.normal, NormalMatrix));
o.uv = v.uv;
o.clipPos = mul(worldPos, mul(View, Projection));
return o;
}
연결되는 단어들
- 스키닝 (Skinning) : 본 애니메이션을 버텍스에 적용하는 과정. 버텍스 셰이더에서 실행하며 본 행렬 x 가중치 합산.
- 인스턴싱 (Instancing) : 같은 메시를 드로우콜 하나로 여러 위치에 그리는 최적화 기법이다. 버텍스 셰이더의 SV_InstanceID로 각 인스턴스 구분.
- WPO (World Position Offset) : UE5 머티리얼에서 버텍스 위치를 셰이더로 조작하는 기능이다. 폴리곤 추가 없이 변형 효과가 있다.
- 클리핑 (Clipping) : 클립 공간에서 -1 ~ 1 범위 밖 버텍스를 잘라내는 것. 버텍스 셰이더 이후 하드웨어가 자동 처리한다.
'Word' 카테고리의 다른 글
| 픽셀/프래그먼트 셰이더 (Pixel / Fragment Shader) (0) | 2026.08.05 |
|---|---|
| 렌더링 파이프라인 (Rendering Pipeline) (0) | 2026.07.29 |
| 텍스처 샘플링 (Texture Sampling) (0) | 2026.07.28 |
| 오버드로우 (Overdraw) (0) | 2026.07.27 |
| UV 매핑 (UV Mapping) (0) | 2026.07.22 |
