Unity와 Rider를 이용하여 스토카스틱 타일링 쉐이더를 구현 실습 정리이다.
실습에 사용한 텍스처는 Poly Haven 사이트에서 가져왔다.
스토캐스틱 타일링은 반복되는 텍스처 패턴의 시각적 반복성을 줄이기 위해, 각 타일 영역(coordinate로 구분되는 영역)마다 약간의 무작위성(Stochasticity)을 적용하여 텍스처 오프셋이나 회전 등을 주는 고급 텍스처링 기법이다.
01. 타일 영역 분리
기본 Plane 메쉬에 머티리얼에 기본 셰이더를 적용하고 준비한 텍스처를 입히고 시작했다. Tilling x,y 값을 10으로 두면 간단하게 영역을 나누어 볼 수 있다.
새로운 셰이더 파일을 작성하면서 구현하는 것이기 때문에 new file을 해주었다. 기본적인 셰이더 파일 구조는 다음과 같다.
Shader "StochasticTilling"
{
    SubShader
    {
        Pass
        {
            HLSLPROGRAM
            #pragma vertex Vert // 정점 셰이더 함수 이름 지정
            #pragma fragment Frag // 프래그먼트 셰이더 함수 이름 지정
            
            ENDHLSL
        }
    }
    
}
02. 텍스처 타일링
본격적으로 Unity의 URP(Universal Render Pipeline) 환경에서 사용되는 HLSL 셰이더(Shader) 프로그램 코드 작성해보겠다.
아래 코드는 텍스처 타일링(Tiling) 효과를 처리하는 기능 까지 구현한 상태이다.
Shader "StochasticTilling"
{
    Properties
    {
        // 텍스처 입력: 기본 색상 또는 패턴 텍스처
        _BaseColor ("Base Color (Texture)", 2D) = "white" {}
        // UV 스케일링을 위한 실수 값
        _UVScale ("UV Scale", Float) = 1.0
    }
    SubShader
    {
        Pass
        {
            HLSLPROGRAM
            #pragma vertex Vert // 정점 셰이더 함수 이름 지정
            #pragma fragment Frag // 프래그먼트 셰이더 함수 이름 지정
            // Unity의 기본 렌더 파이프라인(URP) 핵심 셰이더 라이브러리 포함
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            // 정점 속성 (Attributes): GPU로 전달되는 개별 메쉬 정점 데이터 구조
            struct Attributes
            {
                // 로컬 공간(모델 공간) 정점 위치
                float3 position : POSITION; 
                // 로컬 공간 정점 법선 벡터
                float3 normal : NORMAL; 
                // 로컬 공간 정점 탄젠트 벡터 (법선 매핑 등에 사용)
                float4 tangent : TANGENT; 
                // 첫 번째 UV 좌표 (텍스처 좌표)
                float2 uv0 : TEXCOORD0; 
            };
            // Varyings (인터폴레이터): 정점 셰이더에서 프래그먼트 셰이더로 보간되어 전달되는 데이터 구조
            struct Varyings
            {
                // 클립 공간 위치 (정점 셰이더의 필수 출력)
                float4 positionCS : SV_POSITION; 
                // 텍스처 좌표 (사용자 정의 보간 데이터)
                float2 uv0 : TEXCOORD0; 
            };
            // 재질별 설정 (CPU에서 설정 가능)
            cbuffer UnityPerMaterial
            {
                float _UVScale; // UV 스케일 값
            }
            
            // 정점 셰이더 정의: 각 정점별로 실행
             Varyings Vert(Attributes input)
            {
                 // 로컬 공간 위치를 월드 공간 위치로 변환
                 float3 position = TransformObjectToWorld(input.position);
                 Varyings output;
                 // 월드 공간 위치를 클립 공간 위치로 변환 (투영 및 화면 좌표 결정)
                 output.positionCS = TransformWorldToHClip(position);
                 // 텍스처 좌표 계산: 월드 공간의 XZ 평면 좌표에 스케일 적용 (타일링 목적)
                 output.uv0 = position.xz * _UVScale; 
                 return output;
            }
            // 텍스처 선언
            Texture2D _BaseColor; 
            // 텍스처 샘플러 상태 선언
            SamplerState sampler_BaseColor; 
            
            // 프래그먼트 셰이더 (픽셀 셰이더) 정의: 각 픽셀별로 실행
            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.uv0.xy;
                
                // 보간된 UV 좌표를 사용하여 기본 색상 텍스처 샘플링
                half4 baseColor = _BaseColor.Sample(sampler_BaseColor, input.uv0.xy);
                // 최종 출력 색상 반환
                return baseColor;
            }
            
            ENDHLSL
        }
    }
    
}
1. Properties (속성)
CPU(Unity Inspector)에서 조절 가능한 변수들입니다.
- _BaseColor: 메쉬에 적용할 기본 텍스처를 정의합니다.
- _UVScale: 텍스처의 반복 횟수(스케일)를 조절하는 실수 값입니다. 값이 클수록 텍스처가 더 작고 많이 반복되어(타일링되어) 보입니다.
Unity Inspector 창에 새로 생겨난 것을 볼 수 있다.

2. Attributes 및 Varyings (데이터 구조)
렌더링 파이프라인에서 데이터를 주고받는 통로 역할을 합니다.
- Attributes: 정점 셰이더의 입력 데이터입니다. 메쉬의 위치(position), 법선(normal), 탄젠트(tangent), UV 좌표(uv0) 등 기본 기하학 정보를 담고 있습니다.
- Varyings: 정점 셰이더의 출력이자 프래그먼트 셰이더의 입력 데이터입니다. 정점 셰이더에서 계산된 클립 공간 위치(positionCS)와 텍스처 좌표(uv0)를 담아 픽셀 셰이더로 보간하여 전달합니다.
3. Vert (정점 셰이더)
각 메쉬의 정점마다 한 번씩 실행됩니다.
- 입력받은 로컬 공간(input.position) 위치를 월드 공간으로 변환합니다 (TransformObjectToWorld).
- 월드 공간 위치를 클립 공간(output.positionCS)으로 최종 변환하여 화면에 그릴 위치를 결정합니다 (TransformWorldToHClip).
- 핵심: 월드 공간 위치의 XZ 평면 좌표에 _UVScale을 곱하여 새로운 UV 좌표(output.uv0)를 생성합니다. 이는 메쉬가 움직여도 텍스처가 월드 공간에 고정된 것처럼 보이게(월드 공간 매핑) 하거나, 타일링 패턴을 제어하는 데 사용됩니다.
4. Frag (프래그먼트/픽셀 셰이더)
화면의 각 픽셀마다 한 번씩 실행되어 최종 색상을 결정합니다.
- 정점 셰이더에서 보간되어 넘어온 UV 좌표(input.uv0.xy)를 사용합니다.
- coord = floor(uv); 코드는 UV 좌표의 소수점 이하를 버려 타일의 정수 좌표를 얻는 부분입니다. 이는 스토캐스틱 타일링(Stochastic Tiling) 기법이나 타일 경계를 처리하는 데 사용되는 전형적인 단계입니다.
- 계산된 UV 좌표를 이용해 _BaseColor 텍스처를 샘플링하여 색상(baseColor)을 얻습니다.
- 샘플링한 색상을 최종적으로 화면에 출력합니다 (return baseColor).

03. 좌표 시각화
타일 영역이 어떻게 구성되어있는지 확인해보기 위해 로컬 좌표로 수정하여 시각화한다. 아마 규칙적인 패턴이 보일것이다.
Fragment 부분을 다음과 같이 수정하였다.
// 프래그먼트 셰이더 (픽셀 셰이더) 정의: 각 픽셀별로 실행
            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.uv0.xy;
                float2 coord = floor(uv);
                return half4(abs(coord) * 0.1, 0.0h, 1.0h);
                
                // 보간된 UV 좌표를 사용하여 기본 색상 텍스처 샘플링
                half4 baseColor = _BaseColor.Sample(sampler_BaseColor, uv);
                // 최종 출력 색상 반환
                return baseColor;
            }

(1,1) 위치만 다르게 하여 위치를 시각화 하였다. 좌표 기즈모가 있는 지점이 (0,0)이고 코드에서 작성한 것처럼 (1,1)은 빨간 픽셀로 변했음을 확인할 수 있다.
// 프래그먼트 셰이더 (픽셀 셰이더) 정의: 각 픽셀별로 실행
            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.uv0.xy;
                float2 coord = floor(uv);
                if (coord.x == 1 && coord.y ==1)
                {
                    return half4(1.0h, 0.1, 0.0h, 1.0h);
                }
                
                // 보간된 UV 좌표를 사용하여 기본 색상 텍스처 샘플링
                half4 baseColor = _BaseColor.Sample(sampler_BaseColor, uv);
                // 최종 출력 색상 반환
                return baseColor;
            }

모듈 연산으로 x좌표 짝수칸만을 변환시키면

모듈 연산으로 y좌표도 짝수 칸만을 변환시키면
float2 uv = input.uv0.xy;
                int2 coord = int2(floor(uv));
                if (coord.x % 2 == 0 && coord.y % 2 == 0)
                {
                    return half4(1.0h, 0.1, 0.0h, 1.0h);
                }

짝수칸들만 좌우를 뒤집으면
if (coord.x % 2 == 0 && coord.y % 2 == 0)
                {
                    uv.x = 1.0f -uv.x;
                }
04. 랜덤으로 규칙성 제거하기
initRandom(), Hash(), JenkinsHash() 등 랜덤 함수들이 존재한다.
float GenerateHasheRandomFloat(uint x) 함수를 사용하겠다.
asuint()로 float2 coord 변수를 int로 읽어오게 하여 랜덤 함수의 입력값으로 설정하여 Jitter 기능을 구현한다.
// 프래그먼트 셰이더 (픽셀 셰이더) 정의: 각 픽셀별로 실행
            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.uv0.xy;
                float2 coord = int2(floor(uv));
                float2 jitter = float2(GenerateHashedRandomFloat(asuint(coord.x)), GenerateHashedRandomFloat(asuint(coord.y)));
                return half4(jitter.xy, 0.0h, 1.0h);
            }
그래도 규칙성이 있어 보이므로 y좌표 Jitter에다가 x값을 XOR 계산 시켜주어 더 랜덤하게 해보겠다.
float2 jitter = float2(GenerateHashedRandomFloat(asuint(coord.x)), GenerateHashedRandomFloat(asuint(coord.y) ^ asuint(coord.x)));
0부터 시작하는 asuint(coord.x)대신 not을 붙여서 무한대에서 내려오는 값으로 변경하여 다시 계산 해주겠다.
float2 jitter = float2(GenerateHashedRandomFloat(asuint(coord.x)), GenerateHashedRandomFloat(asuint(coord.y) ^ ~asuint(coord.x)));
UV Scale 확대하면

Jitter값 반환 지우고 원래 모습은 다음과 같이 변경 되었다.

UV값에 Jitter값을 x,y에 더해주어 더 섞이게 해보면
// 프래그먼트 셰이더 (픽셀 셰이더) 정의: 각 픽셀별로 실행
            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.uv0.xy;
                float2 coord = int2(floor(uv));
                float2 jitter = float2(GenerateHashedRandomFloat(asuint(coord.x)), GenerateHashedRandomFloat(asuint(coord.y) ^ ~asuint(coord.x)));
                
                uv.x += jitter.x;
                uv.y += jitter.y;
                //return half4(jitter.xy, 0.0h, 1.0h);
                // 보간된 UV 좌표를 사용하여 기본 색상 텍스처 샘플링
                half4 baseColor = _BaseColor.Sample(sampler_BaseColor, uv);
                // 최종 출력 색상 반환
                return baseColor;
            }

리소스를 더 써서 sincos으로 더 섞어보겠다. jitter.xy 에다가 기본적인 라디안(0~10도)에 TWO_PI를 곱해서 강도를 높였고, X와 Y에 대한 rotation 2x2메트릭스를 생성 후 uv와 곱해서 메트릭스를 회전 시켰다. 확실히 섞인 것을 볼 수 있다.
// 프래그먼트 셰이더 (픽셀 셰이더) 정의: 각 픽셀별로 실행
            half4 Frag(Varyings input) : SV_TARGET
            {
                float2 uv = input.uv0.xy;
                float2 coord = int2(floor(uv));
                float2 jitter = float2(GenerateHashedRandomFloat(asuint(coord.x)), GenerateHashedRandomFloat(asuint(coord.y) ^ ~asuint(coord.x)));
                float4 sc;
                sincos(jitter.xy * TWO_PI, sc.xy, sc.zw);
                float2x2 rotationX = float2x2(sc.x, sc.z,-sc.z, sc.x);
                float2x2 rotationZ = float2x2(sc.y, sc.w,-sc.w, sc.y);
                
                uv = mul(rotationX, uv);
                uv = mul(rotationZ, uv);
                //return half4(jitter.xy, 0.0h, 1.0h);
                // 보간된 UV 좌표를 사용하여 기본 색상 텍스처 샘플링
                half4 baseColor = _BaseColor.Sample(sampler_BaseColor, uv);
                // 최종 출력 색상 반환
                return baseColor;
            }

스토카스틱 타일링 쉐이더 구현 성공
UV Scale을 높여서 본 모습 (4.00)

UV Scale을 낮춰서 본 모습 (0.3)

다음으로는 노멀맵과 라이팅을 적용시켜보겠다.
'공부 일지 > 그래픽스' 카테고리의 다른 글
| GPU로 풀 그리기 #2 (0) | 2025.10.05 | 
|---|---|
| GPU로 풀 그리기 #1 (0) | 2025.10.05 | 
| 스토카스틱 타일링 쉐이더 노멀 구현 (0) | 2025.10.05 | 
| OpenGL API [OpenGL로 배우는 3차원 컴퓨터 그래픽스] (0) | 2025.01.20 | 
| 컴퓨터 그래픽스 기본 이론 [OpenGL로 배우는 3차원 컴퓨터 그래픽스] (0) | 2025.01.20 | 
