그래픽(기타)

(Unity shader)물 만들기 복습

에페아 2021. 5. 12. 04:35

srp에 들어있는 쉐이더그래프는 기능들 이름이 생각이 안나도 검색해서 어찌저찌 찾아쓸 수 있지마는(심지어 결과도 눈으로 바로 보이니까 얘가 뭐였는지도 기억남)

기존 레거시에서 돌아가는 쉐이더는 직접 코딩을 해야하기에(심지어 자동완성도 없다) 계속 복습하면서 상기를 시켜줘야 까먹지를 않을 것 같아서 오랜만에 다시 만져보기로 했다

 

그래도 감은 몸이 기억한다고, 예전에 써놨던거 보니까 착착 기억이 난다

 

복습용으로 만들 물은

1. 물이 찰랑거리게 보이도록 만든다

2. 특정 기준에 따라 물 안쪽, 혹은 반사된 풍경이 보이도록 만든다

3. 물 안쪽이 굴절되서 보이게 만든다

라는 목표로 구현할 것이다

 

뭔가 더 만져보고 싶긴한데

일단 재활이 먼저기도 하고 다른 할일도 있으니 기회가 될 때 더 상급의 기술이나 응용법을 탐구해보는게 좋을듯 하다

 

1. 물이 찰랑거리게 보이도록 만든다

노말맵으로 잔파도를 그리고, 버텍스 y값을 만져서 전체적으로 일렁이는 효과를 줄 것이다

 

먼저 노말맵부터 입혀보자

Properties
{
    _BumpMap("BumpMap", 2D) = "Bump"{}
}

노말"맵" 이니까 프로퍼티에서 텍스쳐를 받아올 수 있도록 만들어준다

괄호 안에 인자는 (에디터에서 표기할 이름, 받을 값의 형식) 이다

이렇게 적어주면

이런식으로 생기게 된다

뒤에 "Bump"라고 적은 것은 초기화용으로 쓰기 위한 (0.5, 0.5, 1) 짜리 노말맵을 넣는 것을 뜻한다

저기에다가 잔파도 용으로 사용할 노말맵을 넣어주면 된다

 

넣어주었으니 이제 노말맵을 적용시켜보자

sampler2D _BumpMap;
float _WaveSpeed;

struct Input
{
	float2 uv_BumpMap;
};

void surf (Input IN, inout SurfaceOutputStandard o)
{
	float4 nor1 = tex2D(_BumpMap, IN.uv_BumpMap + float2(_Time.y * _WaveSpeed, 0));
	float4 nor2 = tex2D(_BumpMap, IN.uv_BumpMap - float2(_Time.y * _WaveSpeed, 0));

	o.Albedo = float3(1,1,1);
	o.Normal = UnpackNormal((nor1+nor2)*0.5);
}

텍스쳐를 사용가능하도록 벡터로 만들어줘야 한다

uv와 베이스가 되는 sampler를 가지고 tex2D를 사용하면 뽑아낼 수 있다

여기서는 잔파도를 움직이게 하기위해 uv를 그대로 쓰는게 아니라 흐르도록 추가로 연산을 해주었다

 

_WaveSpeed는 잔파도 흐르는 속도를 에디터에서 쉽게 조작할 수 있도록 프로퍼티에 방금 추가한 float형 변수다

그냥 숫자값을 때려박아도 상관은 없지만, 파도 속도가 마음에 들때까지 코드창에서 숫자를 바꾸는건 매우 귀찮은 일이기에...

 

암튼 그렇게 뽑아낸 벡터(float4)값을 가지고 UnpackNormal을 사용하여 노말값을 설정해준다

노말을 굳이 nor1, nor2 이렇게 두개를 사용해서 적용시킨 이유는

약간 이런 원리로 잔파도를 만드려는 거라고 생각하면 된다

이렇게 양방향으로 흐르게 하면 그나마 한방향으로만 흘렀을 때의 위화감을 완벽하게까지는 아니더라도 꽤나 없앨 수 있다

그리고, 합연산을 해서 나올 수 있는 최대값이 2로 늘었기 때문에 정규화를 위해 더한 값에 0.5를 곱해준다

노말이니까 정규화안하면 원하는 결과물과 다른 결과가 나오게 되니 주의하도록 하자(굳이 노말이 아니더라도 방향 용도로 사용하는 벡터는 웬만해서는 정규화를 해주는 게 좋다)

 

그리고 우리가 따로 텍스쳐를 안넣어놔서 노말이 제대로 적용이 되나 보기위해서 Albedo값에 (1,1,1)을 시험용으로 넣어놨다

대충 엇비슷하게 나오면 성공

 

이 다음으로는 버텍스를 만져서 일렁이는 효과를 줘보자

	#pragma surface surf Standard vertex:vert

        #pragma target 3.0

        sampler2D _BumpMap;
        float _WaveSpeed;
        float _WavePower;
        float _WaveTilling;

        struct Input
        {
            float2 uv_BumpMap;
        };

        void vert(inout appdata_full v)
        {
            v.vertex.y = sin(abs(v.texcoord.x*2-1) * _WaveTilling + _Time.y) * _WavePower;
        }

버텍스를 건드리기 위해서는 vertex:함수이름 으로 추가로 작성을 해주어야 한다

그럼 해당 함수 안에서 버텍스를 만질 수 있게된다

그래서 이 코드에선 vertex:vert 라고 적었기 때문에

버텍스를 건드리는 함수 이름을 vert 라고 지어서 사용했다

 

인자값은

이것들 중에 선택해서 사용하면 된다

여기선 texcoord(uv)를 사용하기 위해 full을 사용했다

 

버텍스 값을 넣는 데에 사용한 수식은

gimchicchige-mukgoshipda-1223.tistory.com/22?category=863997

 

유니티 SRP 셰이더 그래프 - 14

1. 리플렉션 활용 리플렉션을 넣은 투구를 완성시켜봅시다 먼저 텍스쳐를 통해 리플렉션을 먹일 부분을 마스킹을 해봅시다 그리고 추가로 큐브맵에 노말맵도 입혀봅시다 노말맵을 가져와서 트

gimchicchige-mukgoshipda-1223.tistory.com

이 글 하단부에 설명이 되어있으니 이곳을 참고하자

 

이번에도 따로 에디터에서 값을 조정하기 위해 변수를 추가했으니 해당 부분은 알아서 선택해서 작성하면 된다

값을 그냥 때려박을지, 아니면 지금처럼 변수를 따로 만들건지

 

암튼 대충 이런식으로 결과가 나온다면 성공한 것

 

2. 특정 기준에 따라 물 안쪽, 혹은 반사된 풍경이 보이도록 만든다

일단 먼저 반사됬을 때를 먼저 만들어보자

큐브맵을 사용해서 하늘이 물에 맺히게끔 만들것이다

 

Properties
    {
        _BumpMap("BumpMap", 2D) = "Bump"{}
        _WaveSpeed("Wave Speed", float) = 0.05
        _WavePower("Wave Power", float) = 0.2
        _WaveTilling("Wave Tilling", float) = 25

        _CubeMap("CubeMap", Cube) = ""{}
    }

큐브맵은 Cube를 사용하고, 초기값은 2D 텍스쳐와 형식은 똑같지만 다 비워놓는다

samplerCUBE _CubeMap;

        struct Input
        {
            float2 uv_BumpMap;
            float3 worldRefl;
            INTERNAL_DATA
        };

        void vert(inout appdata_full v)
        {
            v.vertex.y = sin(abs(v.texcoord.x*2-1) * _WaveTilling + _Time.y) * _WavePower;
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            float4 nor1 = tex2D(_BumpMap, IN.uv_BumpMap + float2(_Time.y * _WaveSpeed, 0));
            float4 nor2 = tex2D(_BumpMap, IN.uv_BumpMap - float2(_Time.y * _WaveSpeed, 0));

            o.Normal = UnpackNormal((nor1+nor2)*0.5);

            float4 ref = texCUBE(_CubeMap, WorldReflectionVector(IN, o.Normal));
            o.Albedo = ref.rgb;
        }

사용은 Input에 INTERNAL_DATA와 worldRefl를 추가해줘야 한다

worldRefl을 사용하고 싶은건데 쓰기 위해서는 INTERNAL_DATA를 넣어주어야 해서 같이 넣어준 것

 

그리고 surf 부분에서 ref에다가 2d 텍스쳐 사용할때 처럼 큐브맵도 사용할 수 있도록 texCUBE로 뽑아내준다

인자는 뽑아낼 큐브맵 리소스, 반사벡터이다

하늘이 반사되서 상이 맺히는 거니까 월드기준의 반사벡터가 있어야 따올 수 있는 것.

 

잘 뽑아져 나왔는지 테스트하기 위해 albedo에 넣어주었다

잘 맺힌다면 다음으로 넘어가서,

물 아래쪽이 보이게끔도 만들어 주어야 한다

알파값을 만져도 되긴 하는데, 이 다음 굴절을 만들기 위해 다른 방법을 쓸 것이다

 

SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        GrabPass{}

        CGPROGRAM
        #pragma surface surf Standard vertex:vert noambient noshadow 

        #pragma target 3.0

        sampler2D _BumpMap;
        float _WaveSpeed;
        float _WavePower;
        float _WaveTilling;

        samplerCUBE _CubeMap;

        sampler2D _GrabTexture;

주목할 것은 LOD200 아래에 있는 GrabPass{} 와 맨 아래에 있는 _GrabTexture 이다

GrabPass는 자신이 그리려는 위치의 화면을 캡쳐해놓는 역할을 하고, 캡쳐한 텍스쳐는 _GrabTexture로 들어가게 된다

_GrabTexture라는 이름자체가 이미 그런 역할로 정해져 있는 것이기에 뭐 추가적으로 만져야되는거 없이 찍히면 바로 저 안에 들어가게 되고, 우리는 찍었으면 그걸 갖다쓰기만 하면 된다

 

struct Input
        {
            float2 uv_BumpMap;
            float3 worldRefl;
            float4 screenPos;
            INTERNAL_DATA
        };

        void vert(inout appdata_full v)
        {
            v.vertex.y = sin(abs(v.texcoord.x*2-1) * _WaveTilling + _Time.y) * _WavePower;
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            float4 nor1 = tex2D(_BumpMap, IN.uv_BumpMap + float2(_Time.y * _WaveSpeed, 0));
            float4 nor2 = tex2D(_BumpMap, IN.uv_BumpMap - float2(_Time.y * _WaveSpeed, 0));

            //o.Normal = UnpackNormal((nor1+nor2)*0.5);

            float4 sky = texCUBE(_CubeMap, WorldReflectionVector(IN, o.Normal));

            float4 refrection = tex2D(_GrabTexture, IN.screenPos.xy/IN.screenPos.a);
            o.Albedo = refrection.rgb;
        }

찍은 텍스쳐를 스크린 좌표값을 사용하여 그 위치 그대로 넣어줄 것이다

그러면 캡쳐한 위치에 캡쳐한 이미지를 그리니까 투명인 것 처럼 보여지게 된다

 

스크린 자체를 UV로 활용하기 위해 Input에 float4 screenPos를 추가해준다

왜 포지션인데 flaot4인가?

알파값에 카메라와의 거리가 담기기 때문이다

 

화면캡쳐는 이 오브젝트가 그려지는곳만 캡처하는데, uv로 사용할 스크린포지션 값은 화면 전체를 사용하다 보니, 멀어질 수록 우리가 원하는 결과물과도 멀어진다

그래서 refrection과 같이 포지션에 저 알파값(거리값)을 나누어주면 우리가 원하는 결과물이 나오게 된다

 

확인을 위해 노말을 빼고 refrection값을 넣어본다

대충 이렇게 나오면 성공

완벽하게 투명이 아닌 이유는 라이팅모델 때문인데

말 나온 김에 고쳐보자

SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        GrabPass{}

        CGPROGRAM
        #pragma surface surf WLight vertex:vert noambient noshadow 

맨 아랫줄에서 Standard로 사용하고있던 라이팅 모델을 직접 만든 라이팅 모델로 바꾸기 위해 WLight라고 지어준다

그냥 원하는 이름으로 지어주면 된다

void surf (Input IN, inout SurfaceOutput o)
        {

라이팅 모델을 Standard에서 커스텀모델로 바꿨기 때문에 SurfaceOutputStandard가 아닌 SurfaceOutput으로 바꿔준다

float4 LightingWLight(SurfaceOutput s, float3 lightDIr, float3 viewDir, float atten)
        {
            return float4(s.Albedo.rgb,1);
        }
        ENDCG

그 후 맨 아래에 float4를 반환하는 함수를 만들어주는데

이름 규칙은 Lighting + 아까 위에서 지은 이름(나는 위에서 WLight라고 지었으니 LightingWLight)으로 지으면 된다

인자값은 저런식으로구성되어있다

 

자 이제 결과값을 다시보자

이제 제대로 투명인 것 처럼 나오게 되었다

 

그럼 아까 주석처리했던 노말 넣는부분 다시 활성화해주고,

이렇게,  각이 좁아질 수록 반사되고, 넓어질수록 투영되게끔 만들어준다

맨 처음에 말한 특정 기준이 바로 이거다

 

수면과의 각도는 노말과 바라보는 방향을 내적하면 완벽하게 몇도 까지는 아니더라도 각도에 해당하는 값을 얻어낼 수 있다

그 값으로 판별을 할 것이다

struct Input
        {
            float2 uv_BumpMap;
            float3 worldRefl;
            float4 screenPos;
            float3 viewDir;
            INTERNAL_DATA
        };

        void vert(inout appdata_full v)
        {
            v.vertex.y = sin(abs(v.texcoord.x*2-1) * _WaveTilling + _Time.y) * _WavePower;
        }

        void surf (Input IN, inout SurfaceOutput o)
        {
            float4 nor1 = tex2D(_BumpMap, IN.uv_BumpMap + float2(_Time.y * _WaveSpeed, 0));
            float4 nor2 = tex2D(_BumpMap, IN.uv_BumpMap - float2(_Time.y * _WaveSpeed, 0));

            o.Normal = UnpackNormal((nor1+nor2)*0.5);

            float4 sky = texCUBE(_CubeMap, WorldReflectionVector(IN, o.Normal));
            float4 refrection = tex2D(_GrabTexture, (IN.screenPos/IN.screenPos.a).xy);
            float3 water = lerp(sky, refrection, pow( dot(o.Normal, IN.viewDir), 0.8)).rgb;

            o.Albedo = water;
        }

내가 바라보는 방향벡터를 얻기 위해 Input에 viewDir를 추가한다

그리고 이걸 o.Normal(해당 오브젝트의 노말)과 내적한다

그럼 해당 내적값을 가지고서 수면과의 각도차이가 얼마나 나는지 알 수 있다

 

그래서 그 값을 가지고 텍스쳐를 블렌딩하기 위해 water라는 float3 변수를 새로 만들어주었다

이렇게 해서 내적값이 0에 가까울 수록 반사된 하늘이 나오게 할 것이고, 1에 가까울 수록 물 아래쪽이 투영된 모습이 나오게 할 수 있다

 

저기서 제곱(pow)을 한 이유는 블렌딩 범위를 조절하기 위한 것이다

위에서 계속 해왔던 것과 마찬가지로 에디터에서 조절하고싶으면 프로퍼티에서 추가로 만들어주면 되긴 하지만 이번엔 그냥 숫자를 때려박았다

 

큐브맵이 아래쪽은 너무 어두워서 물 아래에 플랜 하나를 깔아주었다

대충 이런식으로 나오면 잘 된 것이다

 

이 다음으로는 스페큘러를 넣어보자

이렇게 빛나는거.

있는거 없는거 둘이 봤을 때 있는게 확실히 예쁘다

 

	float4 LightingWLight(SurfaceOutput s, float3 lightDIr, float3 viewDir, float atten)
        {
            float3 refVec = s.Normal*dot(s.Normal, viewDir)*2 -viewDir;
            refVec = normalize(refVec);

            float spcr = lerp(0, pow(saturate(dot(refVec, lightDIr)),128), dotData) * _SpacPow;

            return float4( s.Albedo + spcr.rrr,1);
        }

스페큘러를 구현하려면 일단 빛 방향이 있어야 한다

스페큘러 자체가 빛이 반사된게 나한테 보여지는 거니까.

그래서 라이팅 구현부에서 스페큘러 연산을 처리했다

 

_SpacPow 는 프로퍼티에서 스페큘러 강도를 조절하려고 만든건데

dotData 라는 변수가 추가되어있다

 

스페큘러가 물 밖에서 내리쬐는 빛을 반사한 게 들어오는 거기 때문에 물 아래쪽이 비춰지는 부분에서는 스페큘러가 나오지 않도록 만드는 걸 목표로 했다

그래서 스페큘러 계산을 할 때 surf부분에서 물 아래 텍스쳐와 큐브맵 텍스쳐를 블렌딩할 때 쓰던 계산값을 가지고 구현을 할건데

그 상수를 구현할 때 쓰던 Input을 surf 안에서만 사용할 수 있었다

float dotData;

void surf (Input IN, inout SurfaceOutput o)
        {
            float4 nor1 = tex2D(_BumpMap, IN.uv_BumpMap + float2(_Time.y * _WaveSpeed, 0));
            float4 nor2 = tex2D(_BumpMap, IN.uv_BumpMap - float2(_Time.y * _WaveSpeed, 0));
            o.Normal = UnpackNormal((nor1+nor2)*0.5);

            float4 sky = texCUBE(_CubeMap, WorldReflectionVector(IN, o.Normal));
            float4 refrection = tex2D(_GrabTexture, (IN.screenPos/IN.screenPos.a).xy);

            dotData = pow( saturate(1-dot(o.Normal, IN.viewDir)), 0.6);
            float3 water = lerp(refrection, sky, dotData).rgb;

            o.Albedo = water;
        }

그래서 해당 값을 저장해놓고, 라이팅 구현부에서 쓸 수 있도록 함수 밖에서 변수를 하나 만들어서 거기다가 값을 저장해준다

 

이러면 dotData에 넣어준 블렌딩 값을 가지고 라이팅쪽에서 스페큘러를 구현해줄 수 있다

 

좀 더 이뻐보이려고 포스트프로세싱 추가했다

이제 굴절만 넣으면 끝난다

 

float4 refrection = tex2D(_GrabTexture, (IN.screenPos/IN.screenPos.a).xy + o.Normal.xy*0.03);

물 속 캡처를 위에서부터 미리 해놨어서 굴절 넣는거 자체는 의외로 엄청 간단하다

그냥 캡쳐해놓은거에서 uv쪽만 계산하나 추가해주면 된다

이렇게 해서 완성이 되었다

 

 

    Shader "Custom/SWater"
{
    Properties
    {
        _BumpMap("BumpMap", 2D) = "Bump"{}
        _WaveSpeed("Wave Speed", float) = 0.05
        _WavePower("Wave Power", float) = 0.2
        _WaveTilling("Wave Tilling", float) = 25

        _CubeMap("CubeMap", Cube) = ""{}

        _SpacPow("Spacular Power", float) = 2
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        GrabPass{}

        CGPROGRAM
        #pragma surface surf WLight vertex:vert noambient noshadow 

        #pragma target 3.0

        sampler2D _BumpMap;
        float _WaveSpeed;
        float _WavePower;
        float _WaveTilling;

        samplerCUBE _CubeMap;

        sampler2D _GrabTexture;
        float _SpacPow;

        float dotData;

        struct Input
        {
            float2 uv_BumpMap;
            float3 worldRefl;
            float4 screenPos;
            float3 viewDir;
            INTERNAL_DATA
        };

        void vert(inout appdata_full v)
        {
            v.vertex.y = sin(abs(v.texcoord.x*2-1) * _WaveTilling + _Time.y) * _WavePower;
        }

        void surf (Input IN, inout SurfaceOutput o)
        {
            float4 nor1 = tex2D(_BumpMap, IN.uv_BumpMap + float2(_Time.y * _WaveSpeed, 0));
            float4 nor2 = tex2D(_BumpMap, IN.uv_BumpMap - float2(_Time.y * _WaveSpeed, 0));
            o.Normal = UnpackNormal((nor1+nor2)*0.5);

            float4 sky = texCUBE(_CubeMap, WorldReflectionVector(IN, o.Normal));
            float4 refrection = tex2D(_GrabTexture, (IN.screenPos/IN.screenPos.a).xy + o.Normal.xy*0.03);

            dotData = pow( saturate(1-dot(o.Normal, IN.viewDir)), 0.6);
            float3 water = lerp(refrection, sky, dotData).rgb;

            o.Albedo = water;
        }

        float4 LightingWLight(SurfaceOutput s, float3 lightDIr, float3 viewDir, float atten)
        {
            float3 refVec = s.Normal*dot(s.Normal, viewDir)*2 -viewDir;
            refVec = normalize(refVec);

            float spcr = lerp(0, pow(saturate(dot(refVec, lightDIr)),256), dotData) * _SpacPow;

            return float4( s.Albedo + spcr.rrr,1);
        }
        ENDCG
    }
    FallBack "Diffuse"
}

전체 코드는 이렇다

반응형