Game Dev/Unity Shader

04. 본격적인 Shader의 세계, Textures and UV Coordinates - 다니엘 릿 쉐이더 프로젝트

Septentrions 2025. 10. 9. 04:57

 

-목차-
00. Introduction
01. Basic Texturing in HLSL
02. Texture Coordinates (UV)
03. HLSL 코드 분석
04. Tiling and Offset Vectors
05. Mipmaps and Level of Detail
06. 마치며

 

 

가장 먼저, Texture란 무엇일까?

사전적 용어로 질감, 재질이란 뜻을 갖고 있다.

그래픽스에서도 다르지 않다. 흔히 텍스처를 맵핑한다는 말을 들어봤을 것이다.

텍스처는 3D 폴리곤에 2D 이미지를 입히는 렌더링 형식 (Texture Mapping)이다. 

 

폴리곤만 있을 때, Texture가 존재할 때의 차이. 텍스처에 대해 정말 적절하게 표현하고 있는 사진이라 볼 수 있다.

 

유니티에서 텍스처 맵핑이란, 당연하게도 오브젝트에  텍스처를 입히는 과정이다.

팔 다리가 있다면, 팔이라는 Surface에 2D Texture를 입힌다고 생각하면 될 것이다.

 


01. Basic Texturing in HLSL

기본적인 텍스처 파일은 오브젝트 표면 (Surface)의 색상 정보를 포함하고 있다.

 

샘플로 사용할 아주 간단한 타일링 쉐이더의 쉐이더 그래프

*BaseColor와 Add가 아니라 Multiply로 해야한다. 실수..........

        _MainTex ("Main Texture", 2D) = "white" {}

다음은 HLSL에서 Texture의 Property에 대한 설명이다.

 

Shader에서 Texture 변수를 선언할 때,

Sampler2D, Texture2D 2가지 방식이 있다.

sampler2D _MainTex;
Texture2D _MainTex;

 

용도는 같지만, sampler2D 는 DirectX 9 스타일이고, Texture2D 는 DirectX 11 스타일이라고 한다.

굳이 오래된 스타일을 없애지 않고 공존하게 만들어야 했을까...

 

차이점을 짚고 넘어가자면,

sampler2D는 텍스처와 샘플링 설정을 함께 선언하는 것이고,

texture2D는 텍스처 자체만 선언하면서, 별도의 samplerState를 지정하는 방식이다.

유니티 Docs는 texture2D 기준으로 설명하고 있으니.. 잠깐 본다면

 

https://docs.unity.cn/kr/2023.2/Manual/SL-SamplerStates.html

 

샘플러 상태 사용 - Unity 매뉴얼

대부분의 경우 텍스처를 셰이더에서 샘플링할 때 텍스처 샘플링 상태는 텍스처 설정에서 나옵니다. 기본적으로 텍스처와 샘플러는 함께 결합되어 있습니다. 다음은 DX9 스타일 셰이더 구문을 사

docs.unity.cn

 

Unity는 DX11 스타일 HLSL 구문으로 텍스처와 샘플러를 선언할 수 있으며, 특수한 명명 규칙으로 일치시킵니다. “sampler”+TextureName 형식의 이름을 가진 샘플러는 샘플링 상태를 해당 텍스처에서 가져옵니다.

라고 설명하고 있다.

Texture2D _MainTex;
SamplerState sampler_MainTex; // "sampler" + "_MainTex"
// ...
half4 color = _MainTex.Sample(sampler_MainTex, uv);

 

샘플로 보여준 쉐이더 그래프를 보면, Sample Texture2D의 Sampler 부분에 해당하는게 sampler_MainTex라고 볼 수 있다.

 

Property로 텍스처를 추가하고나면, 유니티에서 자동으로 선언 가능한 변수가 생성이 된다.

(Reference Naming이 함부로 수정되면 안되는 이유다.)

 

만약 _MainTex라는 Texture2D가 선언된다면

SamplerState로 sampler + _MainTex 명을 가진 변수를 선언 가능하고,

_MainTex_ST 라는 이름으로 Tiling 과 Offset에 관한 float4 변수를 선언 가능하고,

_MainTex_TexelSize 라는 이름으로 float4 변수 선언이 가능하며,

마지막으로 _MainTex_HDR 이라는 이름으로 float4 변수 선언이 가능하다.

_MainTex Texture2D Texture 정보
_MainTex_ST float4 Texture의 Tiling and Offset 정보 
ST = Scale Transform
_MainTex_TexelSize float4 Texture Element 정보
x,y = 1.0/Width, 1.0/Height
z,w = original Width, Height
_MainTex_HDR float4 HDR 관련

 

            HLSLPROGRAM
            #pragma vertex vert;
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            texture2D _MainTex;
            SamplerState sampler_MainTex;

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;    
                float4 _Tiling;
                float4 _MainText_ST;
            CBUFFER_END

 

02. Texture Coordinates (UV)

근데.. 3D 표면상에 Texture가 어디에 입혀질지는 어떻게 알 수 있을까?

이를 위해서 texture coordinates를 이용하면 된다. (texture coordinates = UV coordinates)

UV는 일종의 2D 평면도로서, 3D 모델링의 꼭짓점들을 펼쳐서 원하는 Texture를 맵핑 할 수 있도록 도와준다.

 

XYZ로 이루어진 3D의 좌표를 수평(U), 수직(V)로 표현하는 것을 UV라고 한다.

앞으로 자주자주자주 보게 될 UV

우리가 자주보는 UV는 다음처럼 색상이 그레디언트 된 것처럼 되어 있다.

좌측 하단 (0,0) 위치를 생각해보자 검정색이다. = RGB (0, 0, 0) 

좌측 상단 (0,1) 위치는 초록색 = RGB (0, 1, 0)

우측 하단 (1,0) 위치는 빨간색 = RGB (1, 0, 0)

우측 상단 (1,1) 위치는 노란색이다. = RGB(1, 1, 0)


우리가 사용할 텍스처의 사이즈는 모두 0~1 사이의 값으로 노말라이즈 할 수 있다.

색상을 통해, UV의 좌표를 RGB로 시각적으로 표현한게 알록달록한 형태의 UV이다.

 

유니티에서는 2D이다 보니 UV값은 float2를 사용하고, TEXCOORD0 (여러 채널을 지원한다.)를 사용한다.

            HLSLPROGRAM
            #pragma vertex vert;
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            texture2D _MainTex;
            SamplerState sampler_MainTex;

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;    
                float4 _Tiling;
                float4 _MainTex_ST;
            CBUFFER_END


            struct appdata 
            {
                    float4 positionOS : Position;
                    float2 uv : TEXCOORD0;
            };

            struct v2f 
            {
                    float4 positionCS : SV_Position;
                    float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v) 
            {
                    v2f o;
                    o.positionCS = TransformObjectToHClip(v.positionOS);
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    return o;
            }
            float4 frag(v2f i) : SV_Target 
            {
                // float4 textureSample = tex2D(_MainTex, i.uv); // if _MainTex variable declared with sampler2D
                float4 textureSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                return textureSample * _BaseColor;
            }

            ENDHLSL

 

HLSL SCRIPT01

[HLSL 코드 해석 구간] HLSL SCRIPT 01

(1) texture2D로 선언하여 사용중이므로 tex2D 대신에 SAMPLE_TEXTURE2D를 사용하였다. 

*별도의 Sampler를 선언한다고 했으므로 중간에 sampler_MainTex를 넣어줘야 한다.

 

(2) appdata 와 v2f struct를 보자

 

positionOS, uv야 원하는 명칭을 변수로 선언한건데

뒤에 Position, TEXCOORD0 같은 명칭은 입출력에 대하여 HLSL 공식 Shader Semantic으로서, 의도를 명시해줘야 한다.

생각보다 고민할게 많구나..

https://docs.unity3d.com/kr/2018.4/Manual/SL-ShaderSemantics.html

 

셰이더 시맨틱 - Unity 매뉴얼

HLSL 셰이더 프로그램을 작성할 때 입력 및 출력 변수는 semantics를 통해 각각의 “의도”를 명시해야 합니다. 이는 HLSL 셰이더 언어의 표준 개념이며 자세한 내용은 MSDN의 시맨틱 문서를 참조하십

docs.unity3d.com

https://learn.microsoft.com/ko-kr/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics?redirectedfrom=MSDN

 

의미론 - Win32 apps

의미 체계는 매개 변수의 의도된 사용에 대한 정보를 전달하는 셰이더 입력 또는 출력에 연결된 문자열입니다.

learn.microsoft.com

참고자료를 보기 귀찮은 사람들을 위해, 위 코드에서 사용된 것만 파악하자

Position 개체 공간의 꼭짓점 위치입니다. float4
TEXCOORD0 질감 좌표 (UV Coordinates) float2
SV_Position 셰이더에 대한 입력에 대해 SV_Position 선언된 경우 두 보간 모드인 linearNoPerspective 또는 linearNoPerspectiveCentroid 중 하나를 지정할 수 있습니다. 여기서 후자는 다중 샘플 앤티앨리어싱 시 중심 맞춤 xyzw 값을 제공합니다. 셰이더에서 사용되는 경우 SV_Position 픽셀 위치를 설명합니다. 모든 셰이더에서 0.5 오프셋으로 픽셀 중심을 가져올 수 있습니다. float4
SV_Target 색(COLOR)을 반환  

 

SV는 System-Value의 약자이다.

SV_Position 꼭짓점 셰이더 및 출력에 대한 입력으로 지정할 수 있습니다.

MSDN에서 지독하게 모호하게 표현했다..

출력에 대한 입력? positionCS 라는 변수안에 출력값(결과)를 넣을 수 있게 지정한다는 의미이다.

 

(3) vertex, fragment 부분

 

vert에서 _MainTex의 정보들을 집어넣어주고 있다. 해당 값들은 v2f 구조체화 되서 리턴한다.

frag에서 v2f화 된 정보를 SAMPLER_TEXTURE2D 함수로 샘플링한다. 샘플링 할 때는, 명시된 것처럼 _MainTex, SamplerState, uv0 값을 그대로 넣어주고 있다.

마지막으로 BaseColor를 곱해줘서 색을 반영한다.


03. Tiling and Offset Vectors

Shader Graph에서 Tiling And Offset 노드를 지원한다.

기본적으로 Tiling, Offset은 Vector2 형식이다.

Shader Graph 내에 SamplerState를 보면 Filter, Wrap 모드가 Linear, Repeat 형식으로 되어있다.

이를 HLSL 코드로 옮기기 전에..

어떤 기능인지 간단하게 알아보자

 

Filter : 축소 확대시 필터링 처리

Linear (bilinear) 축소/확대 될 때, 주변 픽셀과 섞이도록 필터링 한다.
Point 주변 픽셀과 필터링 하지 않는다.
Trilinear Linear와 같지만, LOD에 반영되어 섞이도록 필터링 한다. 

 

Wrap : 텍스처의 좌표가 [0, 1]를 벗어날 경우,

Repeat 텍스처를 반복 시켜서 렌더링 한다.
Clamp 텍스처의 가장자리 부분을 반복 시켜서 렌더링 한다.
Mirror 텍스처를 거울로 반사된 것처럼 렌더링 한다.
Mirro Once 한번 반사 된 것처럼 렌더링 한 뒤에 Clamp 효과를 적용한다.

 

Clamp, Repeat, Mirror, Mirror Once, ClampU+RepeatV

 

공식 문서의 예제인데, 예제가 좋아서 가져왔다.

이를 HLSL 코드에서는 어떻게 표현하는가?

도대체 왜 이따구로 명명 규칙을 세웠는지 모르지만...

SamplerState를 선언하면서 변수 명에 따라 Filter / Wrap 모드가 결정된다. (X발)

 

SamplerState pointrepeat;

SamplerState Point_Repeat;

SamplerState sampler_RepeatPoint;

모두 같은 설정이다.

뒤에U나 V를 붙여서, 수평 수직에 대해서 세팅도 가능하다.

위의 예시 이미지 마지막처럼 sampler_ClampU_RepeatV 같은 경우를 생각해보자.

 

            texture2D _MainTex;
            // SamplerState sampler_MainTex;
            SamplerState sampler_RepeatLinear;

			...


            struct appdata 
            {
            ...
            };

            struct v2f 
            {
            ...
            };

            v2f vert (appdata v) 
            {
                    _MainTex_ST.xy = _Tiling;
                    v2f o;
                    o.positionCS = TransformObjectToHClip(v.positionOS);
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    return o;
            }
            float4 frag(v2f i) : SV_Target 
            {
                // float4 textureSample = tex2D(_MainTex, i.uv); // if _MainTex variable declared with sampler2D
                
                float4 textureSample = SAMPLE_TEXTURE2D(_MainTex, sampler_RepeatLinear, i.uv);
                return textureSample * _BaseColor;
            }

 


04. Mipmaps and Level of Detail

위에서 잠깐 LOD에 대한 단어가 나왔다.

Mipmap은 뭐고 Level of Detail (LOD)은 무엇인지 알아보자.

 

mipmapping이란?

가까이 있는 오브젝트는 고화질 버전의 텍스처를 사용하고, 멀리있는 오브젝트는 저화질 버전의 같은 텍스처를 사용한다는 뜻이다.

해당 텍스처에 대한 메모리 사용량은 늘어나겠지만, 멀리 있는 오브젝트는 보다 빠르게 샘플링할 수 있는 장점이 있다.

Shader 에서도 이런 Mipmap 기능을 구현 할 수 있다.

 

Level Of Detail (LOD)

일반적으로 3D Mesh들에 대해 적용 하는 단어이다.

카메라에 멀어질수록, 해당 모델의 Triangle을 줄여서 성능 향상을 위한 기능이다.

LOD는 Mipmap과 같이 사용해서, 적은 폴리곤과 저화질 텍스처를 활용하여 최적화면에서 최대한 이득을 볼 수 있을 것이다.

 

Texture LOD in HLSL

사용 방법은 아주 간단하다.

// 3단계 Texture LOD

float4 sampleUVs = float4(i.uv, 0.0f, 3.0f);
// float4 textureSample = tex2Dlod(_MainTex, sampleUVs);
// float4 textureSample = SAMPLE_TEXTURE2D(_MainTex, sampler_RepeatLinear, i.uv);
float4 lodtextureSample = SAMPLE_TEXTURE2D_LOD(_MainTex, sampler_RepeatLinear, sampleUVs, 200);
return lodtextureSample * _BaseColor;

 

Shader Graph에서도 지원한다.


05. 마치며

본격적으로 HLSL 코드에 들어서게 된 것 같다.

Shader Grpah에서도 쓰면, 당연히 코드에서도 사용 할 수 있게..!

계속 분석하고 만들어보는 경험이 중요할 것이다.

 

 

 

코드 전문

더보기
Shader "LucidBoundary/BasicTextureShader"
{
    Properties
    {
        _BaseColor ("Base Color", Color) = (1,1,1,1)
        _MainTex ("Main Texture", 2D) = "white" {}
        _Tiling("Tiling", Vector) = (1, 1, 0, 0)
        _Offset("Offset", Vector) = (0, 0, 0, 0)
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "QUEUE" = "Geometry"
            "RenderPipeline" = "UniversalPipeline"
        }

        Pass
        {
            Tags
            {
                "LightMode" = "UniversalForward"
            }

            HLSLPROGRAM
            #pragma vertex vert;
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            texture2D _MainTex;
            // SamplerState sampler_MainTex;
            SamplerState sampler_RepeatLinear;

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;    
                float4 _Tiling;
                float4 _Offset;
                float4 _MainTex_ST;
            CBUFFER_END


            struct appdata 
            {
                    float4 positionOS : Position;
                    float2 uv : TEXCOORD0;
            };

            struct v2f 
            {
                    float4 positionCS : SV_Position;
                    float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v) 
            {
                    _MainTex_ST.xy = _Tiling;
                    v2f o;
                    o.positionCS = TransformObjectToHClip(v.positionOS);
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    return o;
            }
            float4 frag(v2f i) : SV_Target 
            {
                // float4 textureSample = tex2D(_MainTex, i.uv); // if _MainTex variable declared with sampler2D
                
                // float4 textureSample = SAMPLE_TEXTURE2D(_MainTex, sampler_RepeatLinear, i.uv);
                float4 lodtextureSample = SAMPLE_TEXTURE2D_LOD(_MainTex, sampler_RepeatLinear, i.uv, 200);
                return lodtextureSample * _BaseColor;
            }

            ENDHLSL            
        }

    }
    FallBack "Diffuse"
}