본문 바로가기
자료는 자료지/외부에서 퍼온자료

02 Shader 코드 입문 기초

by 대마왕J 2026. 1. 2.

1. 셰이더의 기본 구조와 속성 정의

셰이더 파일은 유니티 인스펙터와 소통하는 부분과 실제 그래픽 연산을 수행하는 부분으로 나뉩니다.

셰이더 블록 전체 구조

    • Shader "경로/이름": 셰이더의 전체 이름을 정의하며, 슬래시를 사용해 메뉴 구조로 관리할 수 있습니다
      • 셰이더 파일의 가장 첫 줄에 위치하며, 유니티 에디터 내에서 해당 셰이더를 찾기 위한 경로를 지정합니다.
      • 코드 내의 이름과 실제 파일의 이름이 일치하지 않아도 작동하지만, 관리를 위해 통일하는 것이 권장됩니다.
    • Properties: 재질(Material) 패널에 나타날 변수들을 선언합니다
      • 이 블록에 선언된 변수들은 아티스트가 유니티 인스펙터 창에서 직접 조절할 수 있는 인터페이스가 됩니다.
      • 선언된 변수들은 이후 Pass 블록 내부에서 동일한 이름으로 다시 선언되어야 셰이더 연산에 활용될 수 있습니다
        • 주요 속성 타입 (Properties)
          • Float: 단일 실수 값 (_Name ("Display", Float) = 1.0)
          • Range: 슬라이더 형태의 범위 값 (Range(0.0, 1.0))
          • Vector: 4차원 벡터 값 (Vector)
          • Color: 색상 선택기 (회색 등 기본값 설정 가능)
          • 2D (Texture): 텍스처 슬롯이며, 기본값으로 "white", "black", "red" 등을 지정하여 텍스처가 없을 때의 동작을 제어합니다

 

타입 설명 코드 예시
Float 일반 실수 값 _MyFloat ("Float", Float) = 1.0
Range 범위가 제한된 슬라이더 _MyRange ("Range", Range(0, 1)) = 0.5
Color 색상 선택기 _MyColor ("Color", Color) = (1,1,1,1)
Vector 4차원 벡터  _MyVector ("Vector", Vector) = (0,0,0,0)
2D 텍스처 이미지 슬롯 _MainTex ("Texture", 2D) = "white" {}
  • SubShader: 실제 렌더링 로직이 포함되며, 그래픽 카드 사양에 따라 여러 개를 작성할 수 있습니다. 즉 옵션별로 다르게 동작하도록 여러 개를 만들 수 있습니다. 이 때는 LOD 300 이나 LOD 100 처럼 옵션별로 동작하는 설정을 정의할 수 있습니다. 
    • 여러 개의 SubShader가 있을 경우, 유니티는 위에서부터 아래로 확인하며 현재 하드웨어가 지원하는 첫 번째 SubShader를 실행합니다
  • Pass: 하나의 완전한 렌더링 실행 단위를 의미하며, 이 안에서 CG 코드가 작성됩니다
    • GPU가 모델의 데이터를 받아 화면에 그리는 한 번의 과정을 의미합니다.
    • 여러 개의 Pass 를 가진 멀티패스 렌더링을 구현할 경우, 위쪽 Pass부터 순차적으로 실행되어 결과가 중첩됩니다.
// 1. Shader 경로 및 이름 정의 
// 슬래시(/)를 사용하여 유니티 재질(Material) 메뉴에서 계층 구조로 관리할 수 있습니다. 
Shader "Tutorial/ShaderBlockStructure"
{
    // 2. Properties (속성 블록) 
    // 유니티 인스펙터 창(Material 패널)에 나타날 변수들을 선언하는 곳입니다. 
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {} // 텍스처 슬롯 
        _Color ("Color", Color) = (1, 1, 1, 1) // 색상 선택기 
        _Value ("Float Value", Float) = 1.0 // 수치 입력 
    }

    // 3. SubShader (서브셰이더 블록) 
    // 실제 렌더링 로직이 포함됩니다. 사양이나 옵션별로 여러 개를 작성할 수 있습니다. 
    SubShader
    {
        // LOD(Level of Detail) 설정을 통해 기기 성능에 따라 실행될 서브셰이더를 구분할 수 있습니다.
        LOD 300

        // 4. Pass (패스 블록) 
        // 하나의 완전한 렌더링 실행 단위를 의미합니다. 
        // 특정 모델을 여러 번 그려야 할 때 여러 개의 Pass를 작성할 수 있습니다. 
        Pass
        {
            CGPROGRAM
            // HLSL/Cg 코드가 작성되는 구역입니다. 
            
            // 정점 셰이더와 프래그먼트 셰이더의 이름을 정의합니다. 
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc" // 유니티 내장 함수 라이브러리 포함 

            // 셰이더 로직 구현 생략...
            ENDCG
        }
    }

    // 모든 SubShader가 실행 불가능할 때 사용할 예비(Fallback) 셰이더
    Fallback "Diffuse"
}

 

2. GPU 파이프라인과 데이터 구조체 (Semantics)

셰이더 코드는 CPU에서 데이터를 받아 GPU의 각 단계를 거치며 처리됩니다.

데이터 전달 구조체

  • appdata (Input): CPU에서 모델 데이터를 가져옵니다.
    • POSITION: 모델의 정점 좌표.
    • TEXCOORD0: 첫 번째 UV 채널 (요즘에는 최대 8개까지 가능하긴 함. 보간기로는 16)
    • NORMAL: 법선 벡터.
    • TANGENT: 탄젠트 벡터.
    • COLOR: 정점 색상
    • SV_VertexID : 버텍스 고유 인덱스
    • SV_InstanceID: GPU 인스턴싱을 사용할때 현재 그려지고 있는 물체가 몇 번째 복사본(인스턴스)인지 알려주는 번호
    • BLENDWEIGHTS: 각 정점이 주변 뼈(Bone)들에 의해 받는 영향력(가중치) 값. 보통 내부 메크로에서 처리해서잘 보기는 힘듬 
    • BLENDINDICES: 정점에 영향을 주는 뼈들의 인덱스 번호. 역시 보통 내부 메크로에서 처리해서잘 보기는 힘듬 
struct appdata {
    float4 vertex : POSITION;      // 정점 좌표 
    float3 normal : NORMAL;        // 법선 
    float2 uv : TEXCOORD0;         // UV 
    
    // 추가로 가져올 수 있는 데이터들
    uint vid : SV_VertexID;        // 버텍스 ID (버텍스 넘버)
    uint iid : SV_InstanceID;      // 인스턴스 ID
    float4 weights : BLENDWEIGHTS; // 본 가중치
    float4 indices : BLENDINDICES; // 본 인덱스
};

v2f (Vertex to Fragment): 정점 셰이더가 계산하여 프래그먼트 셰이더로 넘겨주는 데이터입니다.

  • SV_POSITION: 클립 공간 좌표 (필수).
  • TEXCOORDn: UV나 기타 보간 데이터 전송용 범용 저장소 (최대 16개 슬롯).
// 2. 정점 셰이더에서 프래그먼트 셰이더로 넘겨주는 데이터 구조체 (v2f)
// SV_POSITION은 화면 출력을 위해 필수이며, 
// TEXCOORDn 슬롯들은 데이터 전달용 보간기로 사용됩니다.
struct v2f
{
    float4 pos : SV_POSITION;  // 클립 공간 좌표 (필수)
    float2 uv : TEXCOORD0;     // 첫 번째 통로: UV 데이터
    float3 wNormal : TEXCOORD1;// 두 번째 통로: 월드 공간 법선 데이터
    float3 custom : TEXCOORD2; // 세 번째 통로: 커스텀 연산 데이터 (예: 월드 포지션 등)
    // 이와 같은 TEXCOORD 슬롯을 최대 15번까지(총 16개) 확장할 수 있습니다.
};

3. 정점 셰이더 (Vertex Shader)와 공간 변환

  • 공간 변환 과정: 모델 공간 → 세계 공간 → 카메라 공간 → 클립 공간 순으로 변환됩니다
  • MVP 변환: 세 가지 행렬(Model, View, Projection)을 곱해 한 번에 변환할 수 있으며, 유니티에서는 UnityObjectToClipPos() 함수를 사용해 이를 수행합니다
  • 변수 초기화: 출력 구조체(v2f)를 선언하고 모든 데이터를 채운 뒤 리턴해야 합니다
// 3. 정점 셰이더 (Vertex Shader)
v2f vert (appdata v)
{
    // 변수 초기화: 출력 구조체 v2f를 선언합니다. 
    v2f o;

    // 공간 변환 과정 (MVP 변환) 
    // 1. 모델 공간 -> 세계 공간 (Model Matrix)
    // 2. 세계 공간 -> 카메라 공간 (View Matrix)
    // 3. 카메라 공간 -> 클립 공간 (Projection Matrix)

    // 유니티에서는 아래 함수 하나로 위 3단계 MVP 변환을 한 번에 수행합니다. 
    o.pos = UnityObjectToClipPos(v.vertex);

    // 나머지 데이터 채우기 
    o.uv = v.uv;

    // 모든 데이터를 채운 뒤 구조체를 리턴합니다. 
    return o;
}

 

4. 프래그먼트 셰이더 (Fragment Shader)와 연산

각 픽셀의 최종 색상을 결정하며 텍스처링과 정밀도 제어가 중요합니다.

  • 데이터 정밀도: 성능 최적화를 위해 세 가지 타입을 구분해 사용합니다.
    • float: 32비트 (좌표 계산용)
    • half: 16비트 (대부분의 벡터 및 UV용)
    • fixed: 8비트 (0~1 사이의 색상값용, 현재는 half로 대체됨) 
    • 그렇지만 모바일에서는 기기 칩셋 특성에 따른 오류때문에 float 만 사용하는것을 추천
  • 텍스처 샘플링 (Texturing):
    • _MainTex_ST: 텍스처 이름 뒤에 _ST를 붙여 선언하면 유니티 인스펙터의 Tiling(xy)과 Offset(zw) 데이터를 자동으로 받아옵니다.
    • tex2D(_MainTex, i.uv): UV 좌표를 사용하여 텍스처에서 색상을 읽어옵니다.
// 4. 프래그먼트 셰이더 (Fragment Shader)
// SV_Target 시맨틱을 통해 최종 색상을 렌더 타겟으로 출력 
float4 frag (v2f i) : SV_Target
{
    // 모바일 칩셋 오류 방지를 위해 float 사용 
    float4 texColor;

    // 텍스처 샘플링 (Texturing): UV 좌표를 사용하여 색상 추출 
    texColor = tex2D(_MainTex, i.uv); 

    // 최종 색상 결정: 샘플링된 색상 
    float4 finalColor = texColor; 

    return finalColor;
}

5. 고급 제어 및 특수 효과 기법

컬링과 투영 (Culling & Mapping)

  • Culling: Cull Back(뒷면 제거), Cull Front(앞면 제거), Cull Off(양면 렌더링) 명령으로 가시성을 제어합니다

Pass {
    Cull Back  // 뒷면 제거 (기본값) [cite: 9]
    Cull Front // 앞면 제거 [cite: 9]
    Cull Off   // 양면 렌더링 (그림자나 평면 물체에 사용) 
    
    CGPROGRAM
    // ... 셰이더 코드
}
  • 트라이플레너 매핑 (Triplanar Mapping): UV 대신 모델 공간의 XYZ 좌표를 사용하여 텍스처를 투영할 수 있습니다
    아래 코드는 그중 일부인 XY 좌표만으로 UV를 만든 예시이고, 노말 방향과 함께 섞어서 최종적으로 Z 방향까지 대입시켜야 합니다. 그럼 코드가 길어지니까 귀찮아서 여기까지만... 

// 프래그먼트 셰이더 내부
// 모델 공간의 정점 좌표(v.vertex)를 샘플링 데이터로 사용 
float2 customUV = i.objPos.xy; // XY 평면 투영 
float4 col = tex2D(_MainTex, customUV);

알파 테스트와 애니메이션

  • Alpha Test (Clip): clip(pixel.r - _Cutoff) 함수를 사용해 특정 밝기 이하의 픽셀을 완전히 버립니다

// 프래그먼트 셰이더 내부
float alpha = tex2D(_MainTex, i.uv).r; // 텍스처의 R 채널을 알파로 사용 
clip(alpha - _Cutout); // 결과값이 0보다 작으면 픽셀을 렌더링하지 않음
  • UV 애니메이션: _Time.y 변수를 UV에 더해 텍스처가 흐르거나 움직이는 효과(충격파, 물결 등)를 만들 수 있습니다.

// 정점 또는 프래그먼트 셰이더 내부
float2 animatedUV = i.uv; 
animatedUV.y += _Time.y * _Speed; // 시간에 따라 Y축 방향으로 흐르는 효과 
float4 col = tex2D(_MainTex, animatedUV);

알파 블렌딩 (Alpha Blending)

  • 설정: Blend SrcAlpha OneMinusSrcAlpha 공식을 주로 사용합니다. 물론 다른 조합도 있습니다. 
  • 순서 제어: 깊이 판정 오류를 막기 위해 ZWrite Off로 깊이 쓰기를 끄고, Render Queue를 Transparent (3000)로 설정해야 하는 것이 일반적입니다 

Tags { "Queue" = "Transparent" } // 렌더 큐를 3000번대로 설정
ZWrite Off // 깊이 버퍼 기록 중단 
Blend SrcAlpha OneMinusSrcAlpha // 기본 알파 블렌딩 공식 
// Blend One One // 가산 혼합 (Additve)

림 라이트 (Rim Light) 구현

  1. 노멀 벡터 변환: 모델의 법선을 월드공간으로 변환합니다 (UnityObjectToWorldNormal 또는 행렬 연산)
  2. 시선 벡터 계산: _WorldSpaceCameraPos에서 세계 공간의 정점 위치를 빼서 구합니다
  3. 내적 연산 (N dot V): 법선과 시선 벡터를 내적하여 카메라를 향하는 면을 판별합니다
  4. 결과 반전: 1.0 - (N · V) 공식을 통해 가장자리만 밝게 빛나는 림 라이트를 완성합니다

// 1. 데이터 구조체 정의
struct v2f {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0; // 월드 노멀 전달용
    float3 worldPos : TEXCOORD1;    // 월드 포지션 전달용
};

// 2. 정점 셰이더 (변환 및 좌표 계산)
v2f vert (appdata v) {
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    
    // 단계 1: 모델의 법선을 월드 공간으로 변환 
    o.worldNormal = UnityObjectToWorldNormal(v.normal); 
    
    // 단계 2: 세계 공간의 정점 위치 계산 
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; 
    
    return o;
}

// 3. 프래그먼트 셰이더 (내적 및 반전 연산)
fixed4 frag (v2f i) : SV_Target {
    // 벡터 정규화 (보간 과정에서 변형된 강도 보정) 
    float3 n = normalize(i.worldNormal);
    
    // 단계 2 확장: 시선 벡터 계산 (카메라 위치 - 정점 위치) 
    float3 v = normalize(_WorldSpaceCameraPos - i.worldPos); 
    
    // 단계 3: 내적 연산 (N dot V) 수행 및 0~1 제한 
    float nv = saturate(dot(n, v)); 
    
    // 단계 4: 결과 반전 (1.0 - NV)으로 가장자리 추출 
    float rim = 1.0 - nv; 
    
    // 최종 출력 (림 라이트 강도 조절 가능)
    return fixed4(rim, rim, rim, 1.0);
}
반응형

댓글