저번 포스팅으로부터 좀 더 나아가서
UV를 수정하거나 Texture을 바꾸는 등
다양하게 수정을 해볼 것이다.
우선, 샘플 예제부터 올리겠다.
해당 예제에서부터 조금씩 수정 해나갈 것이다.
Shader "LucidBoundary/ModifyingTextureShader"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_MainTex ("Main Texture", 2D) = "white" {}
}
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 _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 lodtextureSample = SAMPLE_TEXTURE2D_LOD(_MainTex, sampler_RepeatLinear, i.uv, 200);
return lodtextureSample * _BaseColor;
}
ENDHLSL
}
}
FallBack "Diffuse"
}
01. Modifying Texture Coordinates
UV는 2차원 벡터로 되어 있어 이를 변형하여 좌표 변경을 할 수 있다.
(1) UV Rotation
| cos@ -sin@ |
| sin@ cos@ |
2차원 벡터의 UV에 해당 식을 vert 함수에 넣으면 원하는 각도 (@) 만큼 돌릴 수 있게 된다.
float c = cos(30);
float s = sin(30);
float2x2 rotMat = float2x2(c, -s, s, c);
v.uv = mul(v.uv, rotMat);
UV를 30도 회전 시키려고 한다.
이를 적용하면 다음처럼 나온다.

Shader Graph에서는 엄청나게 간단하다.
Rotate Node를 추가하기만 하면 된다.

(2) Flipbook Mapping
flipbook mapping은 스프라이트 시트처럼 텍스처의 특정 부분을 보여주도록 한다.
애니메이션 처럼도 만들 수 있다.

float2 tileSize = float2(1.0f, 1.0f) / _FlipSize;
float width = _FlipSize.x;
float height = _FlipSize.y;
float tileCnt = width * height;
float tileID = floor((_Time.y * _Speed) % tileCnt);
float tileX = (tileID%width) * tileSize.x;
float tileY = (floor(tileID / width)) * tileSize.y;
o.uv = float2(v.uv.x / width + tileX, v.uv.y / height + tileY);
FlipSize는 수평 (X), 수직(Y)를 몇번에 걸쳐서 표시할 지 결정하는 벡터이다.
전체 수평 크기가 1.0f라면 FlipSize.x가 2일 때
0~0.5f 를 0 ~ 1로 uv 맵핑한다고 생각하면 된다.
각 타일의 크기를 width, height 를 이용하여 구해주고
이에 맞춰 uv에 맵핑하는 방식이다.
Shader Graph에서도 Flipbook 노드를 지원한다.

Shader를 이용하여 애니메이션을 만드는 건 어렵지 않다.
ECS 개발을 할 때도, 그냥 애니메이션을 생으로 구현하는 것보다 Shader를 이용하면 편하게 구현 할 수 있을 것이다.
02. Polar Coordinate Mapping (극좌표 맵핑)
기존의 데카르트 좌표계는 아주 직관적이고 알아보기 쉽다.
UV도 사실상 데카르트를 이용하는데.. 굳이 극좌표가 필요한 이유는 뭘까?
일단 극좌표란?
2D 공간에서 포인트들을 원점과 반지름(radius)의 회전각을 이용해 표현한다.
예를들어, 데카르트에서 (5,0)은 극좌표에서는 (0, 5) (2pi, 5) (4pi, 5) 등 무수히 많은 점을 가질 수 있다.
UV 맵핑에서 이 극좌표는 무슨 의미가 있을까?
일단, Shader에는 데카르트에서 극좌표로 변경하는 기능을 지원한다.
일직선으로 되어있던 그래프가 원형으로 변경된다. 시각적으로 흥미롭게 변하게 된다.
극좌표의 장점은 바로 이 시각적 변화 (Radial)이다.
데카르트로 원형을 표현한다고 생각해보자. 데카르트로 표현하면, 이 텍스처의 확대가 일어날 때는 Aliasing 문제가 발생한다.


특이하게도 공식 docs에 Polar coordinate로 변환하는 코드가 제공되어 있다.
_Center("Center", Vector) = (0.5, 0.5, 0, 0)
_RadialScale("Radial", Float) = 1
_LengthScale("Length", Float) = 1
Center 값은 UV의 중심점을 말하는 것이다.
0.5, 0.5가 UV의 중심점이자 원점이다.
radius 부분에 대해 해석이 필요한데. 마땅한 설명은 없다.
예상하기로는.. delta 값은 원점과 UV 입력 벡터 ( 0~1, 0~1 )가 모두 이루어질 때의 벡터를 length로 magnitude화 시키는 것이고고 2배를 한 것은 360도 (2파이) 를 맞춰주기 위함으로 보인다.
float2 CartToPolar(float2 cartUV)
{
const float PI_value = 3.14159235f;
float2 offset = cartUV - _Center;
float radius = length(offset) * 2;
float angle = atan2(offset.x, offset.y) / (2.0f * PI_value);
return float2(radius, angle);
}
Shader Graph에서는 역시나 노드를 지원한다..
진짜 편하긴 하네

03. Triplanar Mapping
기존의 텍스처 맵핑은 Mesh에 UV 좌표가 맵핑된 시점은 baked 된 상황이다.
그 말은, 유니티 내에서 mesh 생성을 할 때 UV가 존재하지 않을 수 있음을 의미한다.
Triplanar Mapping은 이 부분을 해결 해주는 방법이다.
일단 Triplanar Mapping은 UV없이 맵핑 해준다는 뜻이다.
좀더 깊숙히 들어가보자.
"Planar" 평면의, 란 단어에서 알 수 있듯이 평면(Plane)을 통해서 텍스처를 맵핑한다는 걸로 받아들이며 된다.
예를 들어 천 같은 것으로 물건을 덮는다면, 어떻게 되겠는가?
물건의 면 (surface)에 따라 천이 씌워질 것이다.
텍스처 맵핑도 마찬가지이다.
텍스처는 실제로 UV에 의존하는 것이 아니라, 오브젝트의 맵핑할 포인트들에 따라 맵핑이 되는 것이다.
그럼 "Tri"가 붙는다면?
말그대로 3개란 뜻인데. X,Y,Z 각각 축에 고정된 Planar 를 오브젝트에 맵핑된다고 생각해보자.
X축에서 Surface, Y축에 Surface, Z축에 Surface 모양대로 맵핑이 된다.
이를 블렌딩 한 것이 Triplanar 이다.
Triplanar의 장점은 Surface에 따라 텍스처 맵핑을 하더라도 특정 부분이 늘어나지 않게 하는 효과가 있다.
참 좋은 기능이지만.. 텍스처 맵핑을 3번 시도하는 것이니 성능상의 불편함도 존재한다.
게다가.. Normal map, Specular Map 까지 뽑아야 한다면... 3번씩 또 반복해야 하므로 Shader가 느려질 문제가 있다.


Shader "LucidBoundary/ModifyingTextureShader_Triplanar"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_MainTex ("Main Texture", 2D) = "white" {}
_Tile("Tiling", float) = 1
_BlendPower("Blending", float) = 10
}
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 _MainTex_ST;
float _Tile;
float _BlendPower;
CBUFFER_END
struct appdata
{
float4 positionOS : Position;
float3 normalOS : NORMAL;
// float2 uv : TEXCOORD0;
};
struct v2f
{
float4 positionCS : SV_Position;
float3 positionWS : TEXCOORD0;
float3 normalWS: TEXCOORD1;
};
v2f vert (appdata v)
{
v2f o;
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.positionWS = TransformObjectToWorld(v.positionOS.xyz);
o.normalWS = TransformObjectToWorldNormal(v.normalOS);
// o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float4 frag(v2f i) : SV_Target
{
float2 xAxisUV = i.positionWS.zy * _Tile;
float2 yAxisUV = i.positionWS.xz * _Tile;
float2 zAxisUV = i.positionWS.xy * _Tile;
float4 xSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, xAxisUV);
float4 ySample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, yAxisUV);
float4 zSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, zAxisUV);
float3 weights = pow(abs(i.normalWS), _BlendPower);
weights /= (weights.x + weights.y + weights.z);
float4 outColor = xSample * weights.x + ySample * weights.y + zSample * weights.z;
return outColor;
// float4 lodtextureSample = SAMPLE_TEXTURE2D(_MainTex, sampler_RepeatLinear, radialUV);
// return lodtextureSample * _BaseColor;
}
ENDHLSL
}
}
FallBack "Diffuse"
}
이번엔 코드가 조금 복잡하다.
UV가 이번에는 없으므로 UV를 빼버리고, 노말 벡터를 가진다.
삼면에 텍스처를 입히는 과정 때는 Clip 이 아니라 World Space 기준으로 만들어야 하기 때문에
TrnasformObjectToWorld 함수를 사용한다.
04. UV Shear (갈아내기, 전단)
UV의 값들에 변화를 줘서 전체적으로 밀리는 느낌이 나도록 한다.
| 1 ShU |
| ShV 1 |
이 매트릭스를 UV에 곱해주기만 하면 된다.

v2f vert (appdata v)
{
float2x2 shearMat = float2x2 (1, _Shearing.x, _Shearing.y, 1);
v2f o;
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv = mul(shearMat, o.uv);
return o;
}
코드도 정말 간단하다. u와 v에 원하는 값을 곱해줬을 뿐이다.
05. Texture 3D
3D 텍스처는 디멘션이 하나 추가 되는 것 외에는 컨셉적으로 2D 텍스처랑 비슷하다
3D Texture는 주로 안개나 볼륨메트릭 이펙트 효과를 주는데 쓰인다.
우선, 3D Texture로 사용할 이미지를 하나 구하자.
기본 텍스처의 세팅을 3D로 바꿔야 한다.
바꾸고나서 Column / Row를 수정하면 아래 미리 보기 화면에서 텍스처가 어떻게 보이는지 볼 수 있다.
이 텍스처는 이제 3번째 디멘션 값을 가지고 있는 상태가 된다.


Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_MainTex ("Main Texture", 3D) = "white" {}
}
texture3D _MainTex;
SamplerState sampler_MainTex;
float3 animUV = float3(i.uv, _Time.y);
float4 textureSample = SAMPLE_TEXTURE3D(_MainTex, sampler_MainTex, animUV);
코드도 크게 변경점이 없다시피 하다.
animUV는 타임으로 두었는데. 원하는 숫자를 넣으면 해당 Frame의 텍스처가 출력될 것이다.
06. Cubemap
큐브맵이란 오브젝트 주변을 감싸는 텍스처링된 박스이다.
일반적으로 Skybox를 만드는데 쓰인다.
_Cubemap("Cubemap", CUBE) = "White" {}
큐브맵 부분은 넘어가도록 하겠다.
07. 마치며
생각보다 텍스처 부분에서 다루는게 많았다.
그래도 모르던 부분들도 많았고
코드적으로도 익숙해지려고 노력했다.
빨리 재미나게 만들고 싶은데.. 역시 이론쪽은...
'Game Dev > Unity Shader' 카테고리의 다른 글
| 07. Depth Buffer 심화 (1) - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.15 |
|---|---|
| 06. 깊숙히, 더욱.. Depth Buffer - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.12 |
| 04. 본격적인 Shader의 세계, Textures and UV Coordinates - 다니엘 릿 쉐이더 프로젝트 (1) | 2025.10.09 |
| 03. 코드 없이 쉐이더를 다룰 수 있는 Shader Graph - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.09 |
| 02.기본적인 Shader 코드 양식 - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.07 |