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 범위 밖 버텍스를 잘라내는 것. 버텍스 셰이더 이후 하드웨어가 자동 처리한다.

+ Recent posts