프로그래머블 셰이더는 GPU(그래픽 처리 장치)의 중요한 기능 중 하나로, 그래픽 프로그래밍에 있어 매우 유연하고 강력한 도구이다. 쉐이더는 그래픽 파이프라인에서 특정 단계를 프로그램 가능하게 만들어 그래픽 렌더링을 보다 정교하고 맞춤화된 방식으로 처리할 수 있다.

셰이더의 종류

프로그래머블 셰이더는 주로 세 가지 유형으로 분류된다:

셰이더 언어

셰이더를 작성하는 데 사용되는 주요 언어는 다음과 같다: - GLSL (OpenGL Shading Language): OpenGL API에서 사용되는 셰이더 언어이다. - HLSL (High-Level Shading Language): DirectX API에서 사용되는 셰이더 언어이다. - SPIR-V (Standard Portable Intermediate Representation): Vulkan API에서 사용하는 셰이더 중간 표현 언어이다.

기본 버텍스 셰이더 예제 (GLSL)

#version 330 core
layout(location = 0) in vec3 position;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0);
}

위의 코드는 OpenGL의 버텍스 셰이더의 기본 예제이다. 이 셰이더는 모델링, 뷰, 그리고 투영 행렬을 사용하여 버텍스의 최종 위치를 계산한다.

기본 프래그먼트 셰이더 예제 (GLSL)

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Red color
}

위의 코드는 OpenGL의 프래그먼트 셰이더의 기본 예제이다. 화면의 픽셀을 빨간색으로 렌더링한다.

연산 과정

셰이더는 그래픽 파이프라인에서 다양한 연산을 수행하며, GPU의 병렬 연산 구조를 최대한 활용하여 성능을 극대화한다. 예를 들어 복잡한 조명 모델을 구현하거나 다양한 후처리 효과를 적용하는 것이 가능한다.

수학적 모델링

그래픽 렌더링에서 셰이더는 다양한 수학적 연산을 수행하는데, 이 과정에서 행렬과 벡터 연산이 많이 사용된다. 예를 들어, 화면 공간 변환을 위해 모델링, 뷰, 투영 행렬을 조합하여 다음과 같은 형태의 변환을 사용한다:

\mathbf{P} = \mathbf{Projection} \cdot \mathbf{View} \cdot \mathbf{Model} \cdot \mathbf{v}

여기서: - \mathbf{P}는 최종 변환된 위치 - \mathbf{Projection}은 투영 행렬 - \mathbf{View}는 뷰 행렬 - \mathbf{Model}은 모델링 행렬 - \mathbf{v}는 원본 버텍스 위치

텍스처링

프래그먼트 셰이더 단계에서 자주 사용되는 기술 중 하나로 텍스처링이 있다. 텍스처는 2D 이미지 데이터를 나타내며, 셰이더에서 이를 샘플링하여 픽셀의 색상을 결정할 수 있다. 이때 텍스처 좌표를 활용하여 텍스처의 특정 부분을 참조한다.

기본적인 텍스처 샘플링 예제 (GLSL)

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);
}

위의 코드는 텍스처를 샘플링하여 텍스처 데이터를 프래그먼트 셰이더의 출력 색상으로 사용하는 예제이다.

고급 셰이더 기술

프로그래밍 가능한 셰이더를 활용하여 다양한 고급 그래픽 효과를 구현할 수 있다. 그중 몇 가지를 소개한다.

1. 노멀 매핑(Normal Mapping)

노멀 매핑은 더 정교한 표면 디테일을 생성하기 위해 사용되는 기술로, 기본적으로 평평한 표면에도 복잡한 노멀 데이터를 적용하여 더 사실적인 빛 반사 효과를 만들어 낸다. 이는 기본 평면에 텍스처를 적용하는 것처럼 보이게 하지만 실제로는 훨씬 더 복잡한 표면 구조를 시뮬레이션한다.

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;
in vec3 Tangent;
in vec3 Bitangent;

uniform sampler2D normalMap;
uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    vec3 normal = texture(normalMap, TexCoords).rgb;
    normal = normalize(normal * 2.0 - 1.0);  // [0,1] -> [-1,1]

    // Transform normal vector to world space
    mat3 TBN = mat3(Tangent, Bitangent, Normal);
    normal = normalize(TBN * normal);

    // Lighting calculations
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(lightDir, normal), 0.0);

    // Final color
    vec3 diffuse = diff * vec3(1.0, 1.0, 1.0); 
    FragColor = vec4(diffuse, 1.0);
}

2. 환경 맵(Environment Mapping)

환경 맵은 물체의 표면에서 주변 환경을 반사하는 효과를 내기 위해 사용된다. 이는 주로 금속성 물체나 물 등의 반사 표면을 시뮬레이션하는 데 사용된다.

#version 330 core
out vec4 FragColor;

in vec3 WorldPos;
in vec3 Normal;

uniform samplerCube environmentMap;
uniform vec3 cameraPos;

void main()
{
    vec3 I = normalize(WorldPos - cameraPos);
    vec3 R = reflect(I, normalize(Normal));
    FragColor = texture(environmentMap, R);
}

3. 피직스 기반 렌더링(Physically Based Rendering, PBR)

PBR은 더 사실적인 재료 표현을 위해 사용되는 기술로, 물리적 특성을 기반으로 빛의 상호작용을 시뮬레이션한다. 이는 재질의 매끄러움, 금속성, 반사율 등을 고려하여 더욱 현실적인 이미지를 만들어 낸다.

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

uniform sampler2D albedoMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    // Fetch material properties
    vec3 albedo = texture(albedoMap, TexCoords).rgb;
    float metallic = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao = texture(aoMap, TexCoords).r;

    // Compute lighting
    vec3 lightDir = normalize(lightPos - WorldPos);
    vec3 viewDir = normalize(viewPos - WorldPos);
    vec3 halfDir = normalize(lightDir + viewDir);

    // Reflectance equation
    float NDF = DistributionGGX(normalize(Normal), halfDir, roughness);
    float G = GeometrySmith(normalize(Normal), viewDir, lightDir, roughness);
    vec3 F = fresnelSchlick(max(dot(halfDir, viewDir), 0.0), albedo);

    vec3 nominator = NDF * G * F;
    float denominator = 4.0 * max(dot(normalize(Normal), viewDir), 0.0) * max(dot(normalize(Normal), lightDir), 0.0) + 0.001;
    vec3 specular = nominator / denominator;

    vec3 kS = F;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - metallic; 

    vec3 irradiance = vec3(1.0); // Assume white light for simplicity
    vec3 diffuse = irradiance * albedo;

    vec3 ambient = vec3(0.03) * albedo * ao;

    vec3 color = ambient + (kD * diffuse + specular) * irradiance;
    FragColor = vec4(color, 1.0);
}

이처럼 셰이더는 현대 게임 그래픽에서 필수적인 요소로 자리 잡고 있다. 능숙한 셰이더 프로그래밍은 고품질의 그래픽을 구현하는 데 큰 도움이 된다.