HLSL 스터디 (shader, property)

노드로 조작하는 셰이더 그래프는 솔직히 난이도가 쉬운 편이라고 생각한다. 아직 초반이라 그렇겠지?

반면 뭔가 이름부터 영어라 어려워보이는데 HLSL은 먼저 앞에서 배운 셰이더 그래프와 달리 HLSL은 코드로 조작하는 느낌의 공부이다.  (High-Level Shader Language)

 

그넘의 셰이더.. 대체 셰이더가 뭘까?

셰이더는 3D 모델의 표면을 렌더링하고 빛을 계산하며, 재질의 시각적 효과를 제어하고 그래픽 프로그래밍에서 사용된다. 이건 그래픽 처리 장치(GPU)에서 실행된다고 한다.

 

 

 

튜토리얼을 시작하기에 앞서 대마왕J님의 티스토리에 올린 포스팅을 보다가 URP라는 들어보지 못한 새로운 개념이 나와서 검색해봤다.

 

URP(Universal Render Pipeline) 셰이더는 Unity의 URP를 사용하는 프로젝트에서 사용되는 셰이더로, 간단하게 URP는 Unity의 새로운 렌더링 파이프라인 중 하나이다.

(URP 이전에는 Built-in 렌더링 파이프라인(레거시)에서 사용되는 셰이더가 있었다. 이것은 이전 버전의 Unity에서 기본적으로 제공되는 렌더링 파이프라인이며, 새로운 그래픽 기능 및 최적화가 URP와 같은 새로운 렌더링 파이프라인에 비해 제한적일 수 있다.)

 

 

Shader 만들기

 

Project 창에서 +를 누르고  Shader > Unlit Shader

(셰이더 그래프가 아닌 그냥 셰이더.. 처음부터 좀 다르다.)

이것으로 셰이더를 만들어주고, 만든 셰이더에서 create > material로 메테리얼을 만들어준다.

그걸 오브젝트에 적용시켜서 우리가 제대로 셰이더를 만들었는지 확인할 수 있다.

 

(여기서 Unlit shader(무광택 셰이더)는 조명을 고려하지 않고 표면을 렌더링하는 셰이더이다. 이 셰이더는 조명이나 그림자 효과를 적용하지 않고 표면의 색상, 텍스처 및 기타 속성만을 표시한다 )

이렇게 만든 Unlit shader를 코드편집기를 통해 확인하면 뭔가 알 수 없는 많은 코드들이 등장한다. 여기서 우리가 확인한 셰이더는 URP 셰이더가 아니다. 앞에서 설명한 URP 이전, 빌트인(레거시) 유니티의 셰이더라고 한다.

우리는 URP 셰이더를 다루고자 하는데 우리가 확인한 빌트인 유니티버전의 셰이더는 돌아가지 않기 때문에 다시 짜줘야 한다. 아래는 유니티에서 공개한 예제에  대마왕J님이 주석을 달아주셨다. 이것을 복사해서 우리가 생성한 Unlit Shader에 붙여넣으면 동작한다.

// 이 셰이더는 메쉬를 미리 지정된 칼라로 채우는 셰이더입니다 
Shader "Example/URPUnlitShaderBasic"
{
    // 유니티 셰이더의 프로퍼티 블럭(인터페이스를 만드는 곳)입니다. 지금은 비워놨습니다. 
    // 왜냐하면 프레그먼트 (픽셀) 셰이더 코드에서 출력 칼라를 걍 정의해 놨기 때문입니다 
    Properties
    { }

    // 섭셰이더 블럭에 셰이더 코드가 들어 있습니다 
    SubShader
    {
        // 섭셰이더 태그는 언제 어떤 조건에서 섭셰이더 블럭을 정의하는지 또는 
        //패스가 실행되는지를 정의합니다. (그냥 일종의 설정용 태그란 말입니다.) 
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            // HLSL(High Level Shader Language) 코드 블럭입니다. SRP는 HLSL를 사용합니다 
            HLSLPROGRAM
            // 여기는 버텍스 셰이더의 이름을 정의하고요
            #pragma vertex vert
            // 여기는 프레그먼트(픽셀) 셰이더의 이름을 정의합니다. 
            #pragma fragment frag

            // Core.hlsl 파일에는 자주 사용되는 HLSL 메크로나 함수가 정의되어 있습니다. 
            // 그리고 이렇게 #include를 사용하면 다른 HLSL 파일들을 참조할 수 있습니다. 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // 이 구조체에는 어떤 변수가 들어 있는지 정의되어 있습니다. 
            // 이 예제에서는 Attributes 구조체를 버텍스 셰이더의 인풋 구조체로 사용하고 있습니다. 
            struct Attributes
            {
                // positionOS 변수는 오브젝트 스페이스의 버텍스 포지션을 가지고 있습니다 
                float4 positionOS   : POSITION;
            };

            struct Varyings
            {
                // 이 구조체의 포지션 변수는 반드시 SV_POSITION 시멘틱을 가지고 있어야 합니다. 
                float4 positionHCS  : SV_POSITION;
            };

            // 버텍스 셰이더는 Varyings의 요소로 정의됩니다. 
            // 버텍스 셰이더의 타입은 반드시 출력해 주는 구조체의 타입과 일치해야 합니다. 
            Varyings vert(Attributes IN)
            {
                //  Varyings 구조체로 출력(OUT) 선언을 해줍니다. 
                Varyings OUT;
                // TransformObjectToHClip 함수는 오브젝트 좌표계의 버텍스 포지션을 
                // 클립스페이스로 변환해줍니다. 
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                // output을 리턴해 줍니다. 
                return OUT;
            }

            // 프레그먼트 (픽셀) 셰이더 정의입니다. 
            half4 frag() : SV_Target
            {
                // 색상 정의하고 리턴해 줍니다. 
                half4 customColor = half4(0.5, 0, 0, 1);
                return customColor;
            }
            ENDHLSL
        }
    }
}
출처: https://chulin28ho.tistory.com/644 [대충 살아가는 게임개발자:티스토리]

https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/writing-shaders-urp-basic-unlit-structure.html

(유니티에서 공개한 예제)

 

URP unlit basic shader | Universal RP | 12.0.0

URP unlit basic shader This example shows a basic URP-compatible shader. This shader fills the mesh shape with a color predefined in the shader code. To see the shader in action, copy and paste the following ShaderLab code into the Shader asset. // This sh

docs.unity3d.com

 

그럼 이제 이름을 좀 바꿔보자.

우리는 이미 그래프 셰이더에서 배운 내용이 있으니까 무섭지 않다.

블랙보드에서.. 아 여긴 그래프가 아니니까 프로젝트 창에서 이름을 바꾸면... 아무일도 일어나지 않는다. 뭐지?

 

HLSL에서 파일명과 셰이더 이름은 아무 상관이 없다.. 오히려 그렇기 때문에 파일명을 셰이더 명과 같은 느낌의 이름으로 잘 지어야 한다. 

 

아무튼 이렇게 의도하지 않았던 파일명을 바꿨고, 이제 진짜 셰이더 이름을 바꾸자.

 

우리가 어떤 작업을 하고 있는지 알기 위해서 셰이더의 이름을 알기 쉽게 변경해줘야 한다.

아까 위에서 코드편집기를 통하여 확인한 빌트인 셰이더 코드 중 가장 위쪽에 Shader "Example/URPUnlitShaderBasic" 이라 되어있는 곳을 수정해준다. 

이것을 Shader "One/Two/TestShader" 로 수정해주면 One안에 Two폴더, 그 안에 TestShader로 이름이 바뀐 셰이더가 들어있는 것을 확인할 수 있다.

 

 

 


 

 

 

이제, 프로퍼티를 코드로 다루기 위해 코드의 어느 부분을 봐야할까. 

Shader "Unlit/NewUnlitShader"
{
    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
        }
    }
}

 

우선 코드의 큰 틀을 잡기 위해 Properties와 SubShader, Pass 이 세 부분을 간단하게 알아보자.

 

Properties{}는 셰이더의 인터페이스를 통한 세부적인 조절을 담당한다.

SubShader{} 안에는 Tag{}가 있는데 여기 태그에서 우리가 만들 셰이더의 큰 설정을 한다.

Pass{}는 HLSLPROGRAM 에서 ENDHLSL로 감싸져 있다고만 우선 이해하자.

 

 

Property가 뭐지

 

앞에서 본 크게 3 부분으로 나눠지는 코드들 중 첫번째, properties에 대해 알아보자.

셰이더를 만들면 메테리얼의 inspector 창에 우리가 만든 셰이더에 대한 정보가 나온다.

이와같이 여러 수치들을 조절할 수 있는 기능들이 프로퍼티이다. 간단하게 인터페이스라고 생각한다.

그래서 어떤 기능들이 있는데~ 하면 유니티 공식 사이트에 영어로 다 설명해줬다.

https://docs.unity3d.com/Manual/SL-Properties.html

 

Unity - Manual: ShaderLab: defining material properties

ShaderLab: defining a Shader object ShaderLab: assigning a fallback ShaderLab: defining material properties This page contains information on using a Properties block in your ShaderLabUnity’s language for defining the structure of Shader objects. More in

docs.unity3d.com

참고자료 원문

 

 

프로퍼티 만들기 

 

 

Properties{} 부분은 우리가 Pass{}부분에서 깊게 코딩하기에 앞서 그저 유니티의 자체 스크립팅 언어인 ShaderLab을 사용하는 것이다. 따라서 일반적인 코딩과는 다르게 끝에 ;대신 엔터가 의미를 갖고, 대소문자를 구별하지 않는다.

[optional: attribute] name("display text in Inspector", type name) = default value

프로퍼티는 위의 형식을 따른다.

 

 

우선, Float를 만들어보자.

Properties
{
	TestFloat("인스펙터창에 보임", float) = 0
}

float는 한자리만 입력받겠다는 뜻의 프로퍼티이다. 여기서 초기값을 0.0f가 아닌 0으로 써도 괜찮은 이유는 셰이더는 GPU처리이고 GPU는 부동소수점에 특화되어 있기 때문에 묵시적 형변환으로 이해했다.

이 방법 외에 다른 방식으로도 표현할 수 있다.

 

Properties
{
	TestFloat("인스펙터창에 보임", range(0,1)) = 0
}

이것은 셰이더 그래프에서 봤던 슬라이더와 같다. 이렇게 하면 0에서 1까지 슬라이더로 최대 최소값을 제한할 수 있다.

 

다음은 Texture2D이다. 그냥 텍스쳐를 넣는 프로퍼티이다.

Properties
{
	TestText("인터페이스", 2D) = "white"{}
}

 

Cubemap

주변 환경을 텍스쳐에 적용시키는 큐브맵만 받는 프로퍼티이다.

Properties
{
	TestCube("인터페이스", Cube) = ""{}
}

텍스쳐와 모양은 같지만 일반 텍스쳐는 들어갈 수 없다.

 

Color

Properties
{
	TestColor("인터페이스", Color) = (1,1,1,1)
}

셰이더의 색깔을 설정할 수 있는 인터페이스를 만들 수 있다. 물론 코드의 초기값으로 결정할 수도 있다.

 

Vector

Properties
{
	TestVector("인터페이스", Vector) = (1,1,1,1)
}

특정 위치 혹은 각도, 아니면 그냥 float4개를 한번에 사용하고 싶을 때 쓴다.