Ronja 님의 허락을 받고 번역한 튜토리얼입니다. 원문은 위 링크에서 확인하실 수 있습니다.
몇몇 부분은 생략·추가하였습니다.
의역과 오역이 넘쳐날 수 있으니 편하게 봐주시고 잘못된 부분은 알려주시면 감사하겠습니다!
목차
- 개요
- 줄무늬 패턴 (1차원)
- 2·3차원의 체크 무늬
- 크기 조절
- 색상 커스텀하기
개요
저에겐, 셰이더를 사용하는 가장 흥미로운 작업들 중 하나가 절차적으로 이미지를 생성하는 것입니다. 이를 시작하기 위해서, 우리는 간단한 체커보드 패턴을 만들어볼 것입니다.
이 튜토리얼은 프로퍼티만 있는 단순한 셰이더를 기반으로 진행되지만, 여느때와 같이, 오늘 할 작업은 더 복잡한 셰이더에서 색상을 생성하는 데에도 사용할 수 있습니다.
줄무늬 패턴 (1차원)
체스보드 텍스처를 생성하기 위해 표면의 월드 포지션을 사용할 것입니다. 이렇게 하면 나중에 모델을 이동하거나 회전시킬 때 생성된 패턴이 바뀌지 않고 맞춰지게 됩니다. 만약 패턴이 모델과 함께 움직이고 회전되게 하고 싶다면, 오브젝트 공간의 좌표를 사용하면 됩니다. (아무것도 곱해지지 않은 appdata 에서의 값)
프래그먼트 셰이더에서 월드 포지션을 사용하기 위해서는, 월드 포지션을 버텍스에서 프래그먼트로 보내는 (v2f) 구조체에 추가해줘야 합니다. 그리고 버텍스 셰이더 안에서 월드 포지션을 생성하고 구조체에도 작성해줍니다.
struct v2f
{
float4 position : SV_POSITION;
float3 worldPos : TEXCOORD0;
}
v2f vert(appdata v)
{
v2f o;
// 오브젝트를 그리기 위해 클립 공간 상의 위치를 계산합니다.
o.position = UnityObjectToClipPos(v.vertex);
// 월드 공간 상의 버텍스 위치를 계산합니다.
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
그 다음 프래그먼트 셰이더에서 1차원 체스 필드, 즉 흰색과 검정색 줄이 번갈아 나오는 패턴을 먼저 만들어볼 수 있습니다. 이를 위해 포지션의 한 축을 가져와 값을 수정합니다. 먼저, 그 값을 내림(floor) 처리합니다. 이는 소수점을 버려 작은 정수로 값을 변경하는 것을 의미합니다. 이렇게 하면 각 유닛마다 하나의 색상을 가지도록 할 수 있습니다.
그런 다음, chessboard 값이 짝수인지 홀수인지 판단합니다. 이를 위해 값을 2로 나누고 소수부분(frac)을 가져옵니다. 이제 짝수는 모두 0으로 (짝수를 2로 나눈 뒤의 숫자는 소수부분이 없는 정수이기 때문에), 모든 홀수는 0.5 가 됩니다 (홀수를 2로 나눈 뒤의 값은 소수 부분이 생기는데, 1은 0.5, 3은 1.5 가 되는 식입니다). 홀수 부분을 회색이 아닌 하얀색으로 만들기 위해 값에 2를 곱해줍니다.
fixed4 frag(v2f i) : SV_TARGET
{
// 다른 차원(축) 추가하기
float chessboard = floor(i.worldPos.x);
// 값을 2로 나눠 소수부분을 가져옵니다. 짝수인 경우 0, 홀수인 경우 0.5가 됩니다.
chessboard = frac(chessboard * 0.5);
// 2를 곱해 홀수인 경우 회색(0.5)이 아닌 하얀색(1)으로 만듭니다.
chessboard *= 2;
return chessboard;
}
2·3차원의 체크 무늬
다음으로, 패턴을 2차원으로 만들어봅니다. 이를 위해선 추가 축 (y) 을 기존의 x축 값에 더해주기만 하면 됩니다. 이는 우리가 행에 1을 더했을 때 모든 짝수 값이 홀수로, 홀수 값은 짝수가 되기 때문입니다. 이는 값을 내림 처리한 주된 이유이기도 합니다. 사실 1차원에서 패턴을 만들 때엔 내림 처리 없이도 쉽게 할 수 있었지만, 내림 처리를 하는 쪽이 축을 추가하는 게 더 쉬워집니다.
fixed4 frag(v2f i) : SV_TARGET
{
// 다른 차원(측) 추가하기
float chessboard = floor(i.worldPos.x) + floor(i.worldPos.y);
// 값을 2로 나눠 소수부분을 가져옵니다. 짝수인 경우 0, 홀수인 경우 0.5가 됩니다.
chessboard = frac(chessboard * 0.5);
// 2를 곱해 홀수인 경우 회색(0.5)이 아닌 하얀색(1)으로 만듭니다.
chessboard *= 2;
return chessboard;
}
이후에 2차원(y)을 추가한 것과 같은 방식으로 3차원(z)을 추가할 수 있습니다.
fixed4 frag(v2f i) : SV_TARGET
{
// 다른 차원(측) 추가하기
float chessboard = floor(i.worldPos.x) + floor(i.worldPos.y) + floor(i.worldPos.z);
// 값을 2로 나눠 소수부분을 가져옵니다. 짝수인 경우 0, 홀수인 경우 0.5가 됩니다.
chessboard = frac(chessboard * 0.5);
// 2를 곱해 홀수인 경우 회색(0.5)이 아닌 하얀색(1)으로 만듭니다.
chessboard *= 2;
return chessboard;
}
크기 조절
다음으로 패턴을 크게 또는 작게 만드는 기능을 추가하려고 합니다. 이를 위해 패턴의 크기 조절을 위한 새 프로퍼티를 추가합니다. 그 후, 다른 작업을 하기 전에 포지션 값을 스케일 값으로 나누어줍니다. 스케일 값이 1보다 작아지면 표면상의 패턴의 밀도가 더 높아지게 됩니다.
다른 작은 변화는 벡터의 성분에 각각 내림 처리를 하지 않고, 벡터 값에 내림 처리를 하게 만든 것입니다. 이는 아무런 영향도 없지만, 더 코드를 읽기 좋게 만든다고 생각했습니다.
···
// 인스펙터에서 조작할 수 있게 값 표시
Properties
{
_Scale ("Pattern Size", Range(0,10)) = 1
}
···
float _Scale;
···
fixed4 frag(v2f i) : SV_TARGET{
// 셰이더 입력에 맞게 포지션 값의 크기를 조절하고, 정수 값을 얻기 위해 내림 처리합니다.
float3 adjustedWorldPos = floor(i.worldPos / _Scale);
// 다른 차원(축) 추가
float chessboard = adjustedWorldPos.x + adjustedWorldPos.y + adjustedWorldPos.z;
// 값을 2로 나눠 소수부분을 가져옵니다. 짝수인 경우 0, 홀수인 경우 0.5가 됩니다.
chessboard = frac(chessboard * 0.5);
// 2를 곱해 홀수인 경우 회색(0.5)이 아닌 하얀색(1)으로 만듭니다.
chessboard *= 2;
return chessboard;
}
//...
색상 커스텀하기
마지막으로 패턴에 색상을 추가하는 기능을 더하고자 합니다. 짝수 영역에 하나의 색상을, 홀수 영역에는 다른 한 색을 적용할 수 있게 합니다. 이를 위해 셰이더에 새로운 프로퍼티 두 가지와 해당 색상에 대한 값들을 추가합니다.
그런 다음, 프래그먼트 셰이더의 끝 부분에서 두 색상 사이를 선형보간(lerp) 해줍니다. 우리는 두 가지 값 (0과 1) 만 사용하므로, 보간 결과는 입력 값이 0 일 때는 시작 색상으로, 입력 값이 1 일 때는 목표 색상을 반환할 것이라고 예상할 수 있습니다. (보간에 대해서 헷갈리신다면, 다른 튜토리얼에서 더 자세하게 설명하고 있습니다.)
···
// 인스펙터에서 조작할 수 있게 값 표시
Properties
{
_Scale ("Pattern Size", Range(0,10)) = 1
_EvenColor("Color 1", Color) = (0,0,0,1)
_OddColor("Color 2", Color) = (1,1,1,1)
}
···
float4 _EvenColor;
float4 _OddColor;
···
fixed4 frag(v2f i) : SV_TARGET{
// 셰이더 입력에 맞게 포지션 값의 크기를 조절하고, 정수 값을 얻기 위해 내림 처리합니다.
float3 adjustedWorldPos = floor(i.worldPos / _Scale);
// 다른 차원(축) 추가
float chessboard = adjustedWorldPos.x + adjustedWorldPos.y + adjustedWorldPos.z;
// 값을 2로 나눠 소수부분을 가져옵니다. 짝수인 경우 0, 홀수인 경우 0.5가 됩니다.
chessboard = frac(chessboard * 0.5);
// 2를 곱해 홀수인 경우 회색(0.5)이 아닌 하얀색(1)으로 만듭니다.
chessboard *= 2;
// 짝수 영역(0) 의 색상과 홀수 영역(1) 의 색상 사이를 보간합니다.
float4 color = lerp(_EvenColor, _OddColor, chessboard);
return color;
}
//...
체크무늬 패턴을 생성하고 보간하는 완성된 셰이더는 다음과 같습니다.
단순한 체크무늬 패턴 만들기가 마음에 드셨길 바랍니다. 그리고 간단한 수학 연산으로 어떻게 셰이더에서 패턴을 생성하는지 이해하는 것에 도움이 되었다면 좋겠습니다.
Shader "Tutorial/011_Chessboard"
{
// 인스펙터에서 조작할 수 있는 값들이 보여집니다.
Properties{
_Scale ("Pattern Size", Range(0,10)) = 1
_EvenColor("Color 1", Color) = (0,0,0,1)
_OddColor("Color 2", Color) = (1,1,1,1)
}
SubShader{
// 메테리얼은 완전히 불투명하고, 다른 불투명 지오메트리와 같은 타이밍에 렌더됩니다.
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}
Pass{
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
float _Scale;
float4 _EvenColor;
float4 _OddColor;
struct appdata{
float4 vertex : POSITION;
};
struct v2f{
float4 position : SV_POSITION;
float3 worldPos : TEXCOORD0;
};
v2f vert(appdata v){
v2f o;
// 렌더될 수 있도록 버텍스 좌표를 오브젝트 공간에서 클립 공간으로 변환
o.position = UnityObjectToClipPos(v.vertex);
//calculate the position of the vertex in the world
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_TARGET{
// 셰이더 입력에 맞게 포지션 값의 크기를 조절하고, 정수 값을 얻기 위해 내림 처리합니다.
float3 adjustedWorldPos = floor(i.worldPos / _Scale);
// 다른 차원(축) 추가
float chessboard = adjustedWorldPos.x + adjustedWorldPos.y + adjustedWorldPos.z;
// 값을 2로 나눠 소수 부분을 가져옵니다. 짝수인 경우 0, 홀수인 경우 0.5가 됩니다.
chessboard = frac(chessboard * 0.5);
// 2를 곱해 홀수인 경우 회색(0.5)이 아닌 하얀색(1)으로 만듭니다.
chessboard *= 2;
// 짝수 영역(0)의 색상과 홀수 영역(1)의 색상 사이를 보간합니다.
float4 color = lerp(_EvenColor, _OddColor, chessboard);
return color;
}
ENDCG
}
}
FallBack "Standard" // FallBack 으로 다른 오브젝트에 그림자가 지도록 그림자 패스를 추가합니다.
}
'Graphics > Ronja's Unity Shader tutorials' 카테고리의 다른 글
트라이플래너 매핑 (Triplanar Mapping) (0) | 2024.07.08 |
---|---|
색상 보간 (0) | 2024.06.15 |
평면 매핑 (Planar Mapping) (0) | 2024.05.05 |
스프라이트 셰이더 (0) | 2024.03.03 |
기본 투명 셰이더 (0) | 2024.01.21 |