본문 바로가기
튜터리얼_스터디

(HLSL)아티스트를 위한 URP 셰이더 Shader #7 - 출력 실습

by 대마왕J 2021. 8. 29.

 

이제 그러면 노드에서 했던 것과 같이, float 수치를 입력받아서 색을 출력하는걸 해 보도록 하겠습니다. 

재미있는거니 빨리빨리 시작하고 싶군요 하악하악. 

이게 아닌가

아참, 이번엔 노드랑 완전히 똑같은 내용으로 해보는걸 시도해 보려고 해요. 원래 그게 목표이기도 했고. 
그래서 같은 식으로 해보려고 하는데 어떨지 모르겠네. 뭐 어떻게 될지는 해보면 알겠지요. 

어떻게든 되겠지이이ㅣ이ㅣㅇ이ㅣ이잉

 

 


Float 입력을 조합하여 색상 만들어 보기 

이전에 스위즐링 시간에 익힌 바에 의하면, float 3개를 보여서 합치면 float3로 만들 수 있다는 것을 알 수 있었습니다. 
이를 응용해서 , R G B 를각각 0~ 1로 입력해서 색을 만드는 기능을 만들어 보지요 
시작 코드는, 저번 시간의 시작 코드와 같습니다. 
다운로드가 필요하면 , 6강에서 다운받으시면 됩니다. 

Shader "Example/URPUnlitShaderBasic"
{
    Properties
    {

    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

            half4 frag() : SV_Target
            {
                half4 customColor = half4(0.5, 0, 0, 1);
                return customColor;
            }
            ENDHLSL
        }
    }
}

먼저 인터페이스부터 만들어 줍시다. 

일단 아래처럼 프로퍼티에 Float 3개를 각각 하나씩 만든 다음에 이름을 각각 R, G, B  라고 해 줍시다. 

Properties
{
  _R("R",float) = 0
  _G("G",float) = 0
  _B("B",float) = 0
}

그리고 저장해 주면 이렇게 나오게 되죠

음.. 뭐 ... 이걸로도 되긴 되지만... 
보통 칼라는 0~ 1 사이만 필요한 경우가 대부분이잖아요? 슬라이더면 더 보기 좋겠죠?
슬라이더로 바꿔줍시다 

Properties
{
  _R("R",Range(0,1)) = 0
  _G("G",Range(0,1)) = 0
  _B("B",Range(0,1)) = 0
}

오예 슬라이더로 바뀌었네요 오예

요거 가끔 슬라이더로 안나와요!! 라고 하시는 분들 있는데 
그럴땐 이렇게 해보세요 ㅋㅋㅋ

요로케 쭈욱
그래요 그런 표정 질 줄 알았어

자 이제 기본 인터페이스는 잘 만들었습니다. 

그럼 이번에는 인터페이스를 출력과 연결해 봅시다. 

인터페이스 (프로퍼티스) 안에 선언된 변수를 코드내에 연결하는 법은 두 단계를 거치면 됩니다. 

1. 첫 번째는 Pass {  } 안에 프로퍼티스에서 선언한 변수와 같은 이름, 같은 크기를 가진 변수를 선언해 주면 되는 것이지요.  여기서는  float _R, float _G, float _B  이렇게 3개가 되겠지요 

1. 그리고 두 번째는 이렇게 선언한 변수를 출력과 연결시키는 겁니다. 
제일 깔끔한 방법중 하나가, float4 변수를 선언해 주고 (frag 함수의 출력은 float4 니까) 거기의 rgb는 위에서 받아온 변수를 넣어주고, 알파는 ... 어차피 작동안할거지만 1을 넣어주는 겁니다. 

풀 코드는 대충 이렇게 되겠지요 
역시 이 코딩이 이해 안가시면 책을 먼저 보심이 ( 셰이더 스타트업 ...) 

Shader "Example/URPUnlitShaderBasic"
{
    Properties
    {
        _R("R",Range(0,1)) = 0
        _G("G",Range(0,1)) = 0
        _B("B",Range(0,1)) = 0
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            //대충 pass 안 여기쯤에 프로퍼티와 같은 이름의 변수를 생성시킵니다. 대소문자 주의 
            float _R;
            float _G;
            float _B;

            struct Attributes
            {
                float4 positionOS   : POSITION;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

            half4 frag() : SV_Target
            {
                //커스텀칼라는 잠시 재워둡시다. 이번엔 안씀. 지워도 돼요
                //half4 customColor = half4(0.5, 0, 0, 1);
                

                //출력은 float4 가 되어야 하니까 하나 선언해주고
                //RGB에는 위에 받아온 변수를 넣어주고 
                //A 는 뭐 그냥 1 넣어주면 되고 
                // 그리고 리턴 
                float4 finalColor;
                finalColor.rgb = float3(_R, _G, _B);
                finalColor.a = 1;

                return finalColor;
            }
            ENDHLSL
        }
    }
}

그럼 이제 욜케 작동되게 됩니다. 

혹시나 안되시는 분을 위해 비교 가능하게끔 코드를 올려놓을께요 

NewUnlitShaderStudy.shader
0.00MB

 

거의 초반에 말했던, '색상은 숫자다' 라는 놀이를 해 보세요. 내가 원하는 색상을 숫자로 하면 어떻게 될지 예상하고 맞춰 보는 놀이를요!! 

룰루루 즐겁게



밝기 조절해 보기 

그럼 이번엔 여기에 추가로 밝기를 조절하는 기능을 만들어 보도록 하죠. 
밝아지는 방법은? 숫자를 더하는 겁니다! 

'밝기 조절' 이라는 이름을 가진 float을 하나 만들어서, 아까의 결과에 add (더하기) 해주면 밝아지는 기능을 만들 수 있을 겁니다. 
역시 아까처럼 프로퍼티를 추가하고, 변수를 만들어서 더해줍시다. 

Shader "Example/URPUnlitShaderBasic"
{
    Properties
    {
        _R("R",Range(0,1)) = 0
        _G("G",Range(0,1)) = 0
        _B("B",Range(0,1)) = 0

        //밝기 조절하는 float 프로퍼티를 하나 만들어 줍니다. 
        _Brightness("밝기조절", float) = 0
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            float _R;
            float _G;
            float _B;
            //역시 여기에 추가 선언해 줍니다. 
            float _Brightness;

            struct Attributes
            {
                float4 positionOS   : POSITION;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

            half4 frag() : SV_Target
            {
                float4 finalColor;
                finalColor.rgb = float3(_R, _G, _B);
                //결과물에 더해줍니다. 
                finalColor.rgb += _Brightness;
                finalColor.a = 1;

                return finalColor;
            }
            ENDHLSL
        }
    }
}

오 뿐만아니라, 음수를 더하면 어둡게도 만들 수 있네요. 하긴 음수 더하기는 결국 빼기니까요! 

 



그렇다면 이번엔 범위를 제한해 봅시다.

어디서부터 어디까지 더하게 만들어야 
무슨 색이 있더라도 밝기조절을 해서 완벽히 검은색과 흰 색을 만들 수 있을까요?

 

자자 생각을 좀 해봐요. 

그런 표정 짓지 말고

 

노드로 짠다고 이런 간단한 수학도 안하면요, 결국 셰이더를 짜는게 아니라 그냥 어디서 노드 붙여넣기만 하게 돼요. 
중간에 값 틀려도 못 잡아낸다고요.. 아니 쉽다고 생각하는게 당연한 사람도 있지만, 제가 많은 사람을 가르쳐 본 바에 의하면 여기서부터 생각 안하는 사람도 많더라고요. 

아뇨 진짜예요 진지함.

그러니 여기선 (매우 쉽지만) 조금 수학처럼 생각해 보셔야 합니다. 
그래픽 아티스트분들은 그냥 감각으로 게이지를 조절해 오던거 잘 알고 있지만 여기서는 그래선 실력이 안는다고요 

자 생각해 봅시다. 
RGB로 더해서 나올 수 있는 색상의 범위가 어디부터 어디까지예요? 

R    0 ~ 1
G    0 ~ 1
B    0 ~ 1

까지니까, 

나올 수 있는 가장 낮은 값은 0,0,0 이고 가장 높은 값은 1,1,1 이란걸 쉽게 알 수 있을 겁니다. 
그럼 가장 낮은 색인 0,0,0 일때 얼마를 더해야 흰색이 나올까요? 1이지요.  float3(0,0,0)+1 = float3(1,1,1) 이니까요 

그리고 가장 높은 색인 1,1,1 일때 얼마를 더해야 검은색이 나올까요? -1 이죠.
float3(1,1,1) + (-1) == float3(1,1,1) -1 == float3 (0,0,0) 
그러므로 1 ~ -1 까지 더해주면, 어떤 경우에서건 완벽히 검은색과 완벽히 흰색을 만들어 줄 수 있게 될 것입니다. 

자 그럼 밝기조절을 슬라이더바로 만들고, -1 ~ 1 범위로 움직이게 하세요 
그러면 RGB가 무슨 색이건, 밝기 조절을 통해 완벽한 흰색과 완벽한 검은색으로 만들 수 있도록 조절 가능할 것입니다. 
_Brightness("밝기조절", Range(-1,1)) = 0
이렇게 말이죠

Shader "Example/URPUnlitShaderBasic"
{
    Properties
    {
        _R("R",Range(0,1)) = 0
        _G("G",Range(0,1)) = 0
        _B("B",Range(0,1)) = 0

        //밝기 조절하는 float 프로퍼티를 하나 만들어 줍니다. 
        //-1~ 1 사이의 슬라이더로 만들면 밝고 어두운걸 모두 조절할 수 있습니다. 
        _Brightness("밝기조절", Range(-1,1)) = 0
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            float _R;
            float _G;
            float _B;
            //역시 여기에 추가 선언해 줍니다. 
            float _Brightness;

            struct Attributes
            {
                float4 positionOS   : POSITION;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

            half4 frag() : SV_Target
            {
                float4 finalColor;
                finalColor.rgb = float3(_R, _G, _B);
                //결과물에 더해줍니다. 
                finalColor.rgb += _Brightness;
                finalColor.a = 1;

                return finalColor;
            }
            ENDHLSL
        }
    }
}

 

1 이상 되는 수나 0 이하로 되는 경우는 어떡해요?

라는 질문을 하신다면, 칭찬해 드리겠습니다. 논리적인 생각을 하신 거니까요. 


실제로 만약 회색에 이 수치를 대입한다면, 

float3(0.5, 0.5, 0.5) + 1 == float3(1.5, 1.5, 1.5) 가 됩니다. 1을 넘어버리죠!!
물론 모니터에서는 1 이상 표현할 수가 없기 때문에 그냥 흰색으로 잘 나올겁니다만.. 어쨌건 이렇게 원하지 않는 범위를 벗어나는건 , 나중에 '반드시' 문제를 일으키게 됩니다. 

float3(0.5, 0.5, 0.5) -1 == float3(-0.5, -0.5, -0.5) 도 마찬가지로 0 이하로 내려가는 숫자도 나올 수 있는 공식이라는게 문제입니다. 

이럴땐 셰이더에서 그냥 두지 않습니다. 넘어가는 숫자를 잘라 버리는 방법을 이용하지요


그걸 잘라주는 대표적 함수가 saturate 와 clamp 입니다. 

return saturate(finalColor);

뭐 꼭 return 에다가 해줄 필요는 없지만 , 어쨌건 saturate를 하면 그 안의 숫자들은 0~ 1사이로 잘리게 됩니다. 

return clamp(finalColor,0,1);

clamp 함수의 사용법은 다음과 같습니다.
clamp(값 , 값이이것보다 작으면 이 숫자로 고정, 값이이것보다 크면 이 숫자로 고정)

이 둘 중 어떤걸 써도 상관은 없습니다. clamp가 범위를 직접 지정해줄 수 있는 장점이 있지만 아무래도 그만큼 계산이 약간 더 있겠죠. 

half4 frag() : SV_Target
{
  float4 finalColor;
  finalColor.rgb = float3(_R, _G, _B);
  finalColor.rgb += _Brightness;
  finalColor.a = 1;

  //아래 둘의 결과물은 똑같다. 둘 중 하나만 활성화 시켜 쓰도록 하자
  return saturate(finalColor);
  //return clamp(finalColor,0,1);
}

그리고 결과물을 봐도... 아무것도 달라지지 않았을 겁니다 ㅎ 모니터에서는 어차피 0 ~ 1 사이로밖에 표현 못하니까 말이죠. 그렇지만, 나중에 고오오오급이 되고 연산에 연산을 연산군처럼 연산하면 그 때부터는 이걸 넣냐 안넣냐에 따라서 확확 달라질 겁니다. 블룸이라던가.. 라이트 연산이라던가.. 등등..

 

 


SRP Bacthing

노드라면 여기서 끝이었겠지만, 코드는 좀 다른게 하나 있습니다.
유니티 URP는 셰이더 작성법이 살짝 바뀐게 있거든요. 

바로 SRP Batcher 때문인데요, URP가 기존 레거시보다 굉장히 빠를 수 있는 요인 중 하나입니다. 이걸 제대로 못하면 URP의 제 성능을 끌어내지 못하게 되므로 이 배치가 깨지지 않도록 주의해서 코딩을 해야 하는데요, 뭐 노드로 하면 자동으로 되니까 문제가 없지만 코드로 할때는 제법 신경써야 한답니다. 이걸 깨뜨리면 셰이더를 짠다고 할 수가 없지 

https://blog.unity.com/kr/technology/srp-batcher-speed-up-your-rendering

 

SRP Batcher로 렌더링 속도 개선 | Unity Blog

왼쪽은 기본 SRP 렌더링 루프이며, 오른쪽은 SRP Batcher 루프입니다. SRP Batcher의 컨텍스트에서 “배치”는 “바인드”, “드로우”, “바인드”, “드로우”… GPU 커맨드의 시퀀스에 불과합니다.

blog.unity.com

SRP Batcher에 대한 내용은 위의 내용을 보면 알 수 있는데요, 
아주 간단하게 말하자면

'셰이더의 변수를 GPU 메모리에 올려놓고 처리해서, 셰이더만 같으면 그냥 한번에 처리 가능한 기술'

정도로 생각하면 아주 편합니다. 이전에는 '메터리얼이 같아야' 한 번에 배칭 처리가 가능했잖아요? 
하지만 URP의 SRP Batcher를 사용하면 '아예 셰이더만 같으면' 배칭 처리가 가능합니다. 물론 옛날 배칭과는 좀 달라요. 같다고 생각하시면 안됨

'전체 프로세스 중 꽤 무거운 부분인 렌더 상태 (Render State) 만 배칭시켜서 처리하는 방법'입니다. 

즉 배칭은 배칭인데, 전부는 아니고 렌더 스테이트만 GPU 안에서 배칭시켜서 빠르게 최적화 해주는 기법이지요. 

그래서 기존에 드로우콜이나 Static 배칭과는 완전 다른 개념으로 접근해야 합니다. 
뭐 재미없어지니까 자세한 얘기는 각설하고 

하여간 좋은거라고 알면 됨.

여하간 알겠고, 좋은거니까 어떻게 가동시키는지나 알려주세요.. .라고 생각하고 있죠 다들?
좋아요 바람직하고 버릇없고 좋네. 맘에들어. 

일단 지금 만든 셰이더를 그냥 프로젝트 창에서 선택해 보면, 인스펙터에서 이런 메뉴를 볼 수 있는데요 

오호라 SRP Bacher 가 낫 컴포터블 하데요. 어머나 이거 큰일났네. 쏘 시리어스 하고 덴저려스 하고 크레이지하네잉 
잘 돌아는 가지만 URP의 성능을 제대로 발휘할 수 없는 셰이더가 된거예요. 불량학생이 된거네. 부모님 모시고 와 

자 그러면 어떻게 하면 되지요?

C Buffer 

뭐 간단한 편입니다 . 변수 부분을 C Buffer로 감싸주면 돼요. 
이걸 전부 다 감싸는게 아니라 감싸면 안되는 것도 있는데 그건 나중에 천천히 알기로 하고.. 
지금은 이렇게만 해주면 됩니다. 

// C Buffer 로 감싸준다
CBUFFER_START(UnityPerMaterial)
float _R;
float _G;
float _B;
float _Brightness;
CBUFFER_END

CBUFFER_START(UnityPerMaterial)  ~ CBUFFER_END 안에다가 변수를 넣어주기만 하면 되는데요. 

그럼 짜잔~! 이렇게 SRP 배쳐로부터 합격마크를 받습니다. 

아아주 간단하죠?

 

자 오늘까지 얘기는 여기까지고요. 풀 코드를 올려놓고 다운로드도 올려 놓을께요 (그래야 내가 나중에 이걸로 이어서 하기 편하지 

Shader "Example/URPUnlitShaderBasic"
{
    Properties
    {
        _R("R",Range(0,1)) = 0
        _G("G",Range(0,1)) = 0
        _B("B",Range(0,1)) = 0

        _Brightness("밝기조절", Range(-1,1)) = 0
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // C Buffer 로 감싸준다
            CBUFFER_START(UnityPerMaterial)
            float _R;
            float _G;
            float _B;
            float _Brightness;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS   : POSITION;
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

            half4 frag() : SV_Target
            {
                float4 finalColor;
                finalColor.rgb = float3(_R, _G, _B);
                finalColor.rgb += _Brightness;
                finalColor.a = 1;

                //아래 둘의 결과물은 똑같다.
                return saturate(finalColor);
                //return clamp(finalColor,0,1);
            }
            ENDHLSL
        }
    }
}

NewUnlitShaderStudy.shader
0.00MB

 

 

자 그럼 다음시간에 또!! 

블로그 주인장에게 커피값을 후원할 수 있습니다! 

 

반응형

댓글