래스터화된 각 픽셀의 최종 색상을 결정한다. 조명 계산, 텍스처 샘플링, 그림자 처리 등이 있다.
픽셀 하나당 한 번 실행한다.
버텍스 셰이더와의 관계
버텍스 셰이더
- 버텍스마다 실행한다.
- 실행 횟수가 적다. (메시 버텍스 수)
픽셀 셰이더
- 픽셀마다 실행한다.
- 실행 횟수가 많다. (화면 픽셀 수 x 오버드로우)
1920x1080 해상도
- 버텍스 셰이더 : 메시 버텍스 수천 ~ 수만 번
- 픽셀 셰이더 : 최대 207만 번
-> 픽셀 셰이더 최적화가 더 임팩트가 크다
역할
- 래스터화로 생성된 각 픽셀의 최종 색상(RGBA)을 결정하는 프로그램이다
입력
- 버텍스 셰이더에서 보간된 데이터 (UV, 법선, 월드 포지션...)
출력
- 픽셀 하나의 최종 색상 (float4)
픽셀 셰이더 실행 흐름
1. 버텍스 셰이더 출력
- 버텍스 A : UV(0.0, 0.0), Normal(0, 1, 0)
- 버텍스 B : UV(1.0, 0.0), Normal(0, 1, 0)
- 버텍스 C : UV(0.5, 1.0), Normal(0, 1, 0)
2. 래스터화 (보간)
3. 픽셀 P (삼각형 내부)
- UV = 보간된 값 (0.37, 0.43)
- Normal = 보간된 값
4. 픽셀 셰이더 실행 (이 픽셀 하나에 대해)
- 텍스처 샘플링 (UV로 색상 읽기)
- 법선 맵 샘플링 (법선 조작)
- 조명 계산 (PBR)
- 최종 색상 변환
5. 출력 병합 (Z버퍼, 블렌딩)
6. 프레임 버퍼에 기록
기본 픽셀 셰이더 구조
// HLSL 픽셀 셰이더 예제
// 텍스처 리소스
Texture2D AlbedoMap : register(t0);
Texture2D NormalMap : register(t1);
Texture2D RoughnessMap : register(t2);
Texture2D MetallicMap : register(t3);
Texture2D AOMap : register(t4);
SamplerState Sampler : register(s0);
// 조명 데이터
cbuffer LightBuffer : register(b1)
{
float3 LightDirection;
float3 LightColor;
float3 LightIntensity;
float3 CameraPosition;
float3 AmbientColor;
};
// 버텍스 셰이더에서 받는 입력
struct PixelInput
{
float4 ClipPosition : SV_POSITION;
float3 WorldPosition : TEXCOORD0;
float3 WorldNormal : TEXCOORD1;
float2 TexCoord : TEXCOORD2;
float3 Tangent : TEXCOORD3;
float3 Bitangent : TEXCOORD4;
};
// 픽셀 하나의 최종 색상 반환
float4 PS(PixelInput input) : SV_TARGET {
// 1. 텍스처 샘플링
float4 albedo = AlbedoMap.Sample(Sampler, input.TexCoord);
float roughness = RoughnessMap.Sample(Sampler, input.TexCoord).r;
float metallic = MetallicMap.Sample(Sampler, input.TexCoord).r;
float ao = AOMap.Sample(Sampler, input.TexCoord).r;
// 2. 노멀 맵 적용
float3 normalSample = NormalMap.Sample(Sampler, input.TexCoord).rgb;
float3 tangentNormal = normalSample * 2.0 - 1.0;
float3x3 TBN = float3x3(normalize(input.Tangent), normalize(input.Bitangent), normalize(input.WorldNormal));
float3 N = normalize(mul(tangentNormal, TBN));
// 3. 조명 벡터 계산
float3 L = normalize(-LightDirection);
float3 V = normalize(CameraPosition - input.WorldPosition);
float3 H = normalize(L + V);
// 4. PBR 조명 계산
// Diffuse (Lambert)
float NdotL = max(dot(N, L), 0.0);
// F0 : 기본 반사율
// 비금속: 0.04, 금속: albedo 색상
float3 F0 = lerp(float3(0.04, 0.04, 0.04), albedo.rgb, metallic);
// Fresnel (Schlick 근사)
float VdotH = max(dot(V, H), 0.0);
float3 F = F0 + (1.0 - F0) * pow(1.0 - VdotH, 5.0);
// Distribution (GGX/Trowbridge-Reitz)
float alpha = roughness * roughness;
float alpha2 = alpha * alpha;
float NdotH = max(dot(N, H), 0.0);
float denom = NdotH * NdotH * (alpha2 - 1.0) + 1.0;
float D = alpha2 / (3.14159 * denom * denom);
// Geometry (Smith)
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
float NdotV = max(dot(N, V), 0.0);
float G = (NdotL / (NdotL * (1-k) + k)) * (NdotV / (NdotV * (1-k) + k));
// Specular BRDF
float3 specular = (D * F * G) / (4.0 * NdotV * NdotL + 0.001);
// Diffuse (금속은 Diffuse 없다)
float3 kD = (1.0 - F) * (1.0 - metallic);
float3 diffuse = kD * albedo.rgb / 3.14159;
// 5. 최종 색상 합산
float3 color = (diffuse + specular) * LightColor * LightIntensity * NdotL;
// AO 적용
color += AmbientColor * albedo.rgb * ao;
// 감마 보정 (Linear -> sRGB)
color = pow(color, float3(1.0/2.2, 1.0/2.2, 1.0/2.2));
return float4(color, albedo.a);
}
GLSL ES 픽셀 셰이더 - 임베디드/WebGL
// GLSL ES 2.0 (OpenGL ES / WebGL)
#version 100
precision mediump float; // 정밀도 명시 필수
varying vec2 vTexCoord;
varying vec3 vWorldNormal;
varying vec3 vWorldPosition;
uniform sampler2D uAlbedo;
uniform sampler2D uNormalMap;
uniform vec3 uLightDir;
uniform vec3 uLightColor;
uniform vec3 uCameraPos;
void main() {
// 텍스처 샘플링 (ES 2.0 문법)
vec4 albedo = texture2D(uAlbedo, vTexCoord);
// 법선 맵
vec3 normalSample = texture2D(
uNormalMap, vTexCoord).rgb;
vec3 normal = normalize(
normalSample * 2.0 - 1.0);
// 단순 Lambert 조명
float NdotL = max(
dot(normal, normalize(-uLightDir)),
0.0);
// 최종 색상
// ES에서는 gl_FragColor 사용
gl_FragColor = vec4(
albedo.rgb * uLightColor * NdotL,
albedo.a
);
}
픽셀 셰이더 테크닉들
// 테크닉 1: 알파 클리핑 (Masked 모드)
float4 PS_Masked(PixelInput input) : SV_TARGET {
float4 albedo = AlbedoMap.Sample(
Sampler, input.TexCoord);
// 임계값 이하면 픽셀 버림
// → Z버퍼 쓰기 가능 → Early-Z 작동
clip(albedo.a - 0.5);
// if(albedo.a < 0.5) discard; 와 동일
return albedo;
}
// 테크닉 2: 절차적 텍스처 (Procedural)
float4 PS_Procedural(PixelInput input)
: SV_TARGET {
float2 uv = input.TexCoord;
// 체커보드 패턴 (텍스처 없이)
float2 grid = floor(uv * 8.0);
float checker = fmod(
grid.x + grid.y, 2.0);
float3 color = lerp(
float3(0.1, 0.1, 0.1),
float3(0.9, 0.9, 0.9),
checker
);
return float4(color, 1.0);
}
// 테크닉 3: 외곽선 렌더링
float4 PS_Outline(PixelInput input)
: SV_TARGET {
// 법선이 카메라 방향과 수직에 가까우면
// 외곽선으로 처리
float3 N = normalize(input.WorldNormal);
float3 V = normalize(CameraPosition
- input.WorldPosition);
float rim = 1.0 - abs(dot(N, V));
rim = pow(rim, 3.0); // 외곽선 두께 조절
float3 color = lerp(
float3(1, 1, 1), // 기본 색상
float3(0, 0, 0), // 외곽선 색상
rim
);
return float4(color, 1.0);
}
// 테크닉 4: Fresnel 효과
float4 PS_Fresnel(PixelInput input)
: SV_TARGET {
float3 N = normalize(input.WorldNormal);
float3 V = normalize(CameraPosition
- input.WorldPosition);
// 시선과 수직일수록 강한 반사
float fresnel = pow(
1.0 - abs(dot(N, V)),
4.0
);
float3 baseColor = float3(0.1, 0.3, 0.8);
float3 fresnelColor = float3(1.0, 1.0, 1.0);
float3 color = lerp(
baseColor, fresnelColor, fresnel);
return float4(color, 1.0);
}
// 테크닉 5: 디졸브 효과
float4 PS_Dissolve(PixelInput input)
: SV_TARGET {
float4 albedo = AlbedoMap.Sample(
Sampler, input.TexCoord);
float noise = NoiseMap.Sample(
Sampler, input.TexCoord).r;
// 노이즈 값이 임계값 이하면 픽셀 버림
clip(noise - DissolveAmount);
// 경계선 발광 효과
float edge = smoothstep(
DissolveAmount,
DissolveAmount + 0.05,
noise
);
float3 color = lerp(
EdgeColor.rgb,
albedo.rgb,
edge
);
return float4(color, 1.0);
}
픽셀 셰이더 최적화 기법
비용이 높은 연산들
1. 텍스처 샘플링
- 메모리 대역폭 소모
- 캐시 미스 시 지연이 크다
- 텍스처 수 최소화, 채널 패킹
2. sin/cos/pow/sqrt
- GPU에서 비교적 비싸다
- 사전 계산해서 uniform으로 전달한다
- 근사식으로 대체한다
3. 분기문 (if/else)
- GPU 분기 = 양쪽 다 실행 후 선택한다
- step(), lerp(), saturate()로 대체한다
최적화 방법
1. 버텍스 셰이더로 연산 이동
- 픽셀마다 고정된 값 -> 버텍스에서 계산하고 보간한다
2. Unlit 셰이더 사용
- 조명 계산이 없다
- 파티클, 이펙트에서 오버드로우 비용 감소한다
3. 텍스처 채널 패킹
- R = Roughness, G = Metallic, B = AO, A = Emissive 의 텍스처 4개를 1개로 합친다
- 샘플링 횟수 75% 감소된다
4. 하프 정밀도 (half/mediump)
- float -> half (16비트)
- 모바일/임베디드에서 연산 속도 향상
- 색상, UV, 간단한 조명에 충분하다
감마 보정 - 자주 헷갈리는 부분
모니터가 비선형적으로 밝기를 표시한다. 0.5값이 50% 밝기가 아니라는 것이다.
sRGB 공간 vs 선형 공간
- 텍스처 저장 : sRGB(비선형, 사람 눈에 맞게)
- 조명 계산 : 선형 공간에서 해야 정확하다
올바른 흐름 :
텍스처 읽기 -> sRGB to Linear 변환 -> 선형 공간에서 조명 계산 -> Linear to sRGB 변환 -> 프레임버퍼 출력
노멀 맵은 sRGB OFF -> 벡터 데이터이므로 감마 보정이 불필요하다
연결되는 단어들
- BRDF(Bidirectional Reflectance Distribution Function) : 빛이 표면에서 어떻게 반사되는지 정의하는 함수이다. PBR의 수학적 기반이며 Lambert(Diffuse) + GGX(Specular)가 게임 업계 표준이다.
- 감마 보정 (Gamma Correction) : 선형 색상 공간과 sRGB 색상 공간 변환. 픽셀 셰이더에서 조명은 선형 공간에서 계산해야 정확
- MRT (Multiple Render Targets) : 픽셀 셰이더가 여러 렌더 타깃에 동시에 출력하는 기능이다. 디퍼드 렌더링의 G-Buffer 채우기에 사용된다.
- SV_TARGET : 픽셀 셰이더의 출력 시맨틱. SV_TARGET0, SV_TARGET1...으로 여러 렌더 타킷에 동시 출력이 가능하다
'Word' 카테고리의 다른 글
| 버텍스 셰이더 (Vertex Shader) (0) | 2026.07.31 |
|---|---|
| 렌더링 파이프라인 (Rendering Pipeline) (0) | 2026.07.29 |
| 텍스처 샘플링 (Texture Sampling) (0) | 2026.07.28 |
| 오버드로우 (Overdraw) (0) | 2026.07.27 |
| UV 매핑 (UV Mapping) (0) | 2026.07.22 |
