본문 바로가기

💻 내 소개 안녕하세요 엄청짱 프로그래머 손다빈 입니다.
  • 나이 : 96년생
  • 특이사항 : MZ세대, INFJ, 오른손잡이, 아이폰 유저
  • 좋아하는 음식 : 햄버거피자치킨솥뚜껑삼겹살떡볶이오튀김밥
  • 취미 : 개발, Programming, 코딩, 프로그래밍, Coding

🥷기술
Unity
Godot
Cpp
Javascript
D3
Vue

🐱 우리집 고양이 소개
츄르 먹은 후 츄르 먹기 전
  • 이름 : 콜라
  • 나이 : 8살
  • 종 : Nado moreum

📱 개인 프로젝트
🏢 참여한 프로젝트
빌런즈 Life is Pair 도씨어부키우기 직장상사혼내주기 서바이벌빙고 SlitherCoin

🌱 내 잔디밭

횡스크롤 뷰 2D 게임 강 셰이더 만들기 본문

글 묶음/내 밥줄 Unity, C#

횡스크롤 뷰 2D 게임 강 셰이더 만들기

초긍정 개발자 다빈맨 2023. 12. 17. 20:12

Kingdom

 

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)

NoiseTexture 512x512.png

 

게임 뷰에서 확인해보면 노이즈 텍스쳐가 잘 보이네요. 강이 흐르는 연출을 위해 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 를 샘플링하기 직전 노이즈 텍셔츠의 컬러 값으로 왜곡시켜줍니다.

 

훨씬 킹덤이랑 비슷한 셰이더가 된 것 같습니다.

킹덤(Kingdom)에 적용된 물 표면의 허연- 효과

 

킹덤에 적용된 물 셰이더를 보면 물 거품(?) 인지, 물에 비친 빛 느낌인지 모르겠지만 하얀색 하이라이트가 강에 둥둥 떠다니네요. 이것도 노이즈 텍스쳐를 이용하면 쉽게 표현할 수 있을 것 같습니다.

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