- 나이 : 96년생
- 특이사항 : MZ세대, INFJ, 오른손잡이, 아이폰 유저
- 좋아하는 음식 : 햄버거피자치킨솥뚜껑삼겹살떡볶이오튀김밥
- 취미 : 개발, Programming, 코딩, 프로그래밍, Coding
🥷기술
🐱 우리집 고양이 소개
- 이름 : 콜라
- 나이 : 8살
- 종 : Nado moreum
📱 개인 프로젝트
🏢 참여한 프로젝트
🌱 내 잔디밭
횡스크롤 뷰 2D 게임 강 셰이더 만들기 본문
Kingdom 같은 스타일의 물 셰이더는 횡스크롤 게임에서 특히 많이 사용됩니다. 넓은 공간감을 주면서도 풍부한 비주얼 효과를 제공하는 좋은 방법입니다. 이 글에서는 이런 느낌의 물 셰이더를 구현하는 방법에 대해서 설명합니다.
셰이더를 작성한 에디터 환경은 다음과 같습니다.
- Unity 2021.3.19f1 (아마 대부분의 유니티 버전에서 호환됩니다)
- Built-In 렌더 파이프라인 (URP는 호환 안됨)
| 씬과 에셋 준비하기
먼저 물 셰이더를 구성할 2D 씬을 준비해주세요. 저는 무료로 받을 수 있는 에셋으로 꾸며봤습니다.
그리고 물 셰이더가 적용될 레이어 게임 오브젝트를 하나 만들어 줍니다. 스프라이트 렌더러 컴포넌트를 추가하고, 물 셰이더가 적용될 위치에 늘려 배치해세요. 이제 셰이더만 작성하면 되겠네요.
유니티 Project 영역에서 두가지 에셋을 생성합니다.
1. 셰이더 파일 : 마우스 우클릭 후 Create > Shader > Unlit Shader 로 생성
2. Material : 마우스 우 클릭후 Create > Material 로 생성
저는 이름을 둘 다 2DSideScrollingWater로 통일했습니다.
위에서 생성한 Material의 Shader를 같이 생성했던 셰이더 파일로 지정합니다.
그리고 물 레이어용으로 만들었던 게임오브젝트에 부착된 Sprite Renderer의 Material을 교체합니다.
| 셰이더 작성하기
이제 생성된 셰이더를 스크립트 에디터로 열어봅시다.
Shader "Unlit/2DSideScrollingWater"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
기본적으로 생성된 셰이더 템플릿 파일에서 몇가지 필요 없는 내용을 제거해줍시다.
Shader "Unlit/2DSideScrollingWater"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
// Queue 가 Opaque에서 Transparent로 변경됨
Tags
{
"Queue"="Transparent"
"RenderType"="Transparent"
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
// 임시로 파란색 컬러를 반환
fixed4 frag (v2f i) : SV_Target
{
return half4(0, 0, 1, 0);
}
ENDCG
}
}
}
이전보다는 훨씬 깔끔해졌습니다. 사용 안하는 FOG 관련 내용들을 전부 제거하고, 사용 안하는 텍스쳐의 uv 도 Vertex 셰이더의 Input 구조체에서 제외하였습니다.
그리고 Tags 에서 Queue 를 Opaque에서 Transparent로 변경 후 RenderType도 Transparent로 설정했습니다.
Tags
{
"Queue"="Transparent"
"RenderType"="Transparent"
}
이렇게 하지 않으면 렌더 QueueType이 Transparent로 그려지는 기본 스프라이트 렌더러보다 위에 그려질 수 없습니다.
이 상태로 게임 뷰를 확인해보면 다음과 같습니다.
이제 반사된 것 처럼 보이기 위해서 화면 픽셀 정보가 필요합니다. Bulit-In 렌더 파이프라인에서는 GrabPass를 사용하면 쉽게 화면 프레임 버퍼를 얻을 수 있습니다. 쉽게 말해 화면을 그대로 캡쳐한 텍스쳐 정보를 얻을 수 있어요.
Shader "Unlit/2DSideScrollingWater"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags
{
"Queue"="Transparent"
"RenderType"="Transparent"
}
// GrabPass 추가
GrabPass
{
"_GrabTexture"
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
// GrabTexture의 uv를 저장할 파라미터 선언
float4 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
// GrabPass로 받아올 GrabTexture 변수 선언
sampler2D _GrabTexture;
sampler2D _MainTex;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// (중요!) GrabTexture 텍스쳐 샘플링을 위한 uv 값을 계산.
// 여기서 화면 공간 UV로 변환해줍니다.
o.uv = ComputeGrabScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// GrabTexture 샘플링
fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uv));
return col;
}
ENDCG
}
}
}
위 예제에서 주석으로 추가된 내용을 설명해두었습니다.
이 상태에서 게임 뷰를 확인해 보면 우리가 생성해두었던 레이어가 안보이는 것 처럼 보입니다. 당연하게도 레이어 부분만큼 게임 화면을 그대로 복사해서 렌더링 해주고 있기 때문에 마치 아무것도 안보이는 것 처럼 보입니다.
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = ComputeGrabScreenPos(o.vertex);
// 뒤집어보자!
o.uv.y = 1 - o.uv.y;
return o;
}
그렇다면 이 상태에서 Vertex 셰이더를 조금 수정해봅시다. uv의 y 값을 1에서 빼서 뒤집어보면 어떨까요?
이제 게임 화면이 뒤집혀져서 나옵니다. 마치 반사 된 것 처럼요. 이 아이디어를 기반으로, uv 가 뒤집히는 오프셋을 조금 조정하면 될 것 같습니다.
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_OffsetY ("OffsetY", Range(0, 1)) = 1.0
}
오프셋을 조정할 수 있게끔 _OffsetY 프로퍼티를 추가했습니다. 범위는 UV라 0~1 사이로 제한합니다.
// _OffsetY 변수 기존 텍스쳐 변수랑 같이 선언
float _OffsetY;
sampler2D _GrabTexture;
sampler2D _MainTex;
그리고 변수도 추가해주어야겠죠..!
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = ComputeGrabScreenPos(o.vertex);
o.uv.y = _OffsetY - o.uv.y;
return o;
}
마지막으로 Vertex 셰이더 함수에서 uv 를 뒤집는 부분에 오프셋만 추가하면 됩니다.
이제 인스펙터에서 Offset 을 조정할 수 있게 됐어요. 적당히 0.36으로 맞춰봤습니다.
이제 반사되는 위치도 얼추 맞아서, 원하는 느낌이 슬슬 나오는 것 같습니다. 이대로는 밋밋하니 약간의 왜곡 효과도 같이 넣어 봅시다.
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_NoiseTexture ("NoiseTexture", 2D) = "white" {}
_OffsetY ("OffsetY", Range(0, 1)) = 1.0
}
왜곡 효과를 위해 사용할 _NoiseTexture를 추가합니다.
float _OffsetY;
sampler2D _GrabTexture;
sampler2D _NoiseTexture; // 이거 추가!
sampler2D _MainTex;
_NoiseTexture변수도 당연히 추가해야 합니다.
struct appdata
{
float4 vertex : POSITION;
float2 noiseCoord : TEXCOORD1;
};
struct v2f
{
float4 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float2 noiseCoord : TEXCOORD1;
};
그리고 Noise Texture의 uv 정보를 받아오기 위해 appdata, v2f 구조체에 각각 noiseCoord를 추가했습니다.
fixed4 frag (v2f i) : SV_Target
{
fixed4 noiseCol = tex2D(_NoiseTexture, i.noiseCoord);
fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uv));
// 일단 노이즈가 제대로 받아왔는지 확인
return noiseCol;
}
노이즈 텍스쳐가 잘 받아왔는지 확인하기 위해 _NoiseTexture를 샘플링한 컬러를 반환하도록 합니다.
인스펙터에서 Material에 NoiseTexture를 할당해주세요. 노이즈 텍스쳐는 사실 구글에 검색해서 적당한 것을 사용하면 됩니다. 무료로 사용할 수 있는 텍스쳐로 이것을 사용하셔도 됩니다. (출처 : Wikimedia Common)
게임 뷰에서 확인해보면 노이즈 텍스쳐가 잘 보이네요. 강이 흐르는 연출을 위해 uv의 x 축을 시간에 따라 감소시킵니다.
i.noiseCoord.x -= _Time.x * 0.3;
fixed4 noiseCol = tex2D(_NoiseTexture, i.noiseCoord);
이제 이 노이즈 텍스쳐의 컬러 값을 이용해서 UV를 왜곡시켜 줍니다.
fixed4 frag (v2f i) : SV_Target
{
i.noiseCoord.x -= _Time.x * 0.3;
fixed4 noiseCol = tex2D(_NoiseTexture, i.noiseCoord);
// UV 왜곡
i.uv.xy += noiseCol.x * 0.02;
fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uv));
// 강을 좀 더 어둡게
col.rgb *= 0.45;
return col;
}
위와 같이 GrabTexture 의 uv 를 샘플링하기 직전 노이즈 텍셔츠의 컬러 값으로 왜곡시켜줍니다.
훨씬 킹덤이랑 비슷한 셰이더가 된 것 같습니다.
킹덤에 적용된 물 셰이더를 보면 물 거품(?) 인지, 물에 비친 빛 느낌인지 모르겠지만 하얀색 하이라이트가 강에 둥둥 떠다니네요. 이것도 노이즈 텍스쳐를 이용하면 쉽게 표현할 수 있을 것 같습니다.
fixed4 frag (v2f i) : SV_Target
{
i.noiseCoord.x -= _Time.x * 0.3;
fixed4 noiseCol = tex2D(_NoiseTexture, i.noiseCoord);
// UV 왜곡
i.uv.xy += noiseCol.x * 0.02;
fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uv));
// 강을 좀 더 어둡게
col.rgb *= 0.45;
// 물 거품(?) 인지 빛 인지 아무튼 그것..
if (noiseCol.r > 0.35)
col.rgb += noiseCol.r * 0.5;
return col;
}
마지막에 노이즈의 컬러 값(R 채널)이 일정 임계치를 넘어가면 좀 더 강조하기 위해 rgb를 더해주어 가성비 좋게 비슷한 효과를 구현해보았습니다.
끝! 간단하지만 이쁜 강 셰이더가 구현되었습니다.
'글 묶음 > 내 밥줄 Unity, C#' 카테고리의 다른 글
싱글톤 줄이기 (1) | 2023.09.27 |
---|---|
Effective C# 요약 (0) | 2022.11.20 |
API Level 31 이상 앱 업로드시 android:export 이슈 (17) | 2022.08.13 |
유니티 UGUI 기초 정리 (4) | 2020.06.29 |