WinCNT

Unity에서 커스텀 ShaderGUI를 구현해보자! 그 첫번째 본문

Unity/Unity 관련

Unity에서 커스텀 ShaderGUI를 구현해보자! 그 첫번째

WinCNT_SSS 2023. 10. 11. 13:34

서론

이번에는 Unity에서 커스텀 ShaderGUI를 구현해보기로 했다

ShaderGUI란 특정 셰이더의 머티리얼에 대한 Inspector의 UI를 제작할 수 있게 해주는 클래스라고 생각하면 될 것 같다

이 글에 구현하면서 알게 된 내용을 정리하고자 한다


커스텀 셰이더 GUI를 구현하면 좋은 점?

어디까지나 개인적 의견이지만…

역시 최고의 장점은 머티리얼의 프로터티를 보기 좋게 정리할 수 있다는 점이라고 본다

위는 URP의 디폴트 Lit 셰이더인데 토글, 체크 박스, 셀렉트 박스 등으로 깔끔하게 정리된 것을 볼 수 있다

 

두번째로는 커스텀 GUI를 통해서 셰이더의 키워드를 설정할 수 있게 된다는 점이다

즉 셰이더에서 #pragma multi_compile과 #pragma shader_feature 등에 의해 셰이더 베리언트가 만들어졌을 경우, GUI를 통해 해당 머티리얼이 어느 셰이더 베리언트를 사용할 지 바꿀 수 있다

 

셰이더 키워드에 대해선 공식 문서에도 자세하게 설명되어 있어서 첨부한다

HLSL에서 셰이더 키워드 선언 및 사용 - Unity 매뉴얼

 

HLSL에서 셰이더 키워드 선언 및 사용 - Unity 매뉴얼

이 페이지는 HLSL 코드에서 셰이더 키워드를 사용하여 작업하는 방법에 대한 정보를 포함합니다. 셰이더 키워드에 대한 일반적인 안내는 셰이더 키워드를 참조하십시오. 셰이더 그래프에서 셰이

docs.unity3d.com

셰이더 키워드 - Unity 매뉴얼

 

셰이더 키워드 - Unity 매뉴얼

셰이더 키워드를 사용하면 셰이더 코드에서 조건부 동작을 사용할 수 있습니다. 일반적인 코드를 일부 공유하되, 특정한 키워드가 활성화되거나 비활성화되면 기능이 달라지는 셰이더를 만들

docs.unity3d.com


셰이더 GUI용 스크립트 파일 생성하기

셰이더의 커스텀 GUI를 구현하기 위해 우선적으로 필요한 것은 스크립트 파일이다

커스텀 GUI는 에디터 상태에서만 사용하기 때문에 Assets > Editor 폴더에 만들어주자

특수 폴더 이름 - Unity 매뉴얼

 

특수 폴더 이름 - Unity 매뉴얼

보통 Unity 프로젝트를 구성하기 위해 폴더를 생성할 때 원하는 이름은 무엇이든 선택할 수 있습니다. 그러나 몇 가지 폴더 이름의 경우 Unity는 해당 폴더의 내용이 특별한 방식으로 취급되어야

docs.unity3d.com

네임스페이스와 클래스의 이름도 잘 설정해주자

using System;
using UnityEngine;
using UnityEditor;

namespace CustomShaderGUI
{
    public class CustomExampleShaderGUI : ShaderGUI
    {
    }
}

그 다음으로는 연동(?)할 셰이더 파일에 다음과 같이 CustomEditor를 설정해준다

(네임스페이스와 클래스명에는 주의!)

Shader "Custom/CustomShader"
{
    Properties
    {
        // ...
    }
    SubShader
    {
        // ...
    }
    CustomEditor "CustomShaderGUI.CustomExampleShaderGUI"
}

OnGUI

그럼 실제로 셰이더 GUI를 작성해보자

다음과 같이 OnGUI 메소드를 override 해서 코드를 작성하면 인스펙터에 적용된다

using System;
using UnityEngine;
using UnityEditor;

namespace CustomShaderGUI
{
    public class CustomExampleShaderGUI : ShaderGUI
    {
        public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
        {

        }
    }
}

 

예를 들어 라벨를 작성하고 싶으면 다음과 같이 구현하면 되고

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
    // 라벨
    GUILayout.Label("커스텀 셰이더 GUI!!!", EditorStyles.boldLabel);
    EditorGUILayout.Space();
}

디폴트 상태의 GUI를 그대로 표시하려면 다음과 같은 코드를 작성하면 된다

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
    // 디폴트 GUI
    base.OnGUI(materialEditor, properties);
}

셰이더의 프로퍼티 찾기

기본적으로 셰이더의 프로퍼티를 커스텀 셰이더 GUI에 알려줄 필요가 있다

예를 들어 다음과 같이 커스텀 셰이더에 _MainTex라는 프로퍼티가 있다고 하면…

Shader "Toon/Basic/CustomShader"
{
    Properties
    {
        [MainTexture] _MainTex ("Main Texture", 2D) = "white" { }
    }
}

FindProperty에 셰이더의 프로퍼티를 설정해서 획득할 필요가 있다

internal static class CustomShaderProperty
{
    public static readonly string MainTex = "_MainTex";
}
private MaterialProperty MainTex { get; set; }

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
    if (materialEditor == null)
        throw new ArgumentNullException("materialEditor");

    _materialEditor = materialEditor;
    Material material = materialEditor.target as Material;

    FindProperties(properties);
}

void FindProperties(MaterialProperty[] props)
{
    MainTex = FindProperty(CustomShaderProperty.MainTex, props);
}

위에서는 셰이더의 프로퍼티를 따로 클래스를 분리해서 만들었다

프로퍼티가 늘어날수록 복잡해지므로 분리했을 뿐, 딱히 같은 클래스에서 변수로 관리해도 상관 없다

중요한 것은 커스텀 셰이더에서 선언된 프로퍼티명과 일치해야 한다는 점이다

그리고 FindProperty한 리턴 값은 코드의 MainTex와 같이 MaterialProperty 타입의 변수에 대입해두고 사용하면 된다


Styles

각 프로퍼티에 대한 헤더나 툴팁은 GUIContent를 사용해서 지정할 수 있다

일반적으로는 아래의 Styles클래스와 같이 내부 클래스로 관리하는 경우가 많았다

using System;
using UnityEngine;
using UnityEditor;

namespace CustomShaderGUI
{
    public class CustomExampleShaderGUI : ShaderGUI
    {
        private static class Styles
        {
            public static readonly GUIContent MainTexText = EditorGUIUtility.TrTextContent("Map", "Albedo(rgb)");
            // 혹은 public static readonly GUIContent MainTexText2 = new GUIContent("Map", "Albedo(rgb)");
        }

        public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
        {

        }
    }
}

일반적으로 프로퍼티의 헤더의 이름이나 툴팁을 설정할 수 있으며, 이미지도 설정 가능한 모양이다


셰이더 GUI로 구현해본 것들

라벨에 스타일 적용하기

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
    GUIStyle labelStyle = new GUIStyle();
    labelStyle.fontSize = 20;
    labelStyle.fontStyle = FontStyle.Bold;
    labelStyle.normal.textColor = Color.red;
    GUILayout.Label("Custom Shader GUI!!!!!", labelStyle);
}

특정 셰이더 프로퍼티를 디폴트로 그리기

MaterialEditor의 ShaderPropertyDefaultShaderProperty를 사용하면 된다

ShaderProperty는 셰이더에서 설정한 GUI(Header, Enum 등)이 반영되지만, DefaultShaderProperty는 그냥 그대로 출력한다는 차이점이 있다

예를 들면 다음과 같은 셰이더의 설정이 있을 때

Shader "CBToon/Basic/Develop"
{
    Properties
    {
        [Enum(UnityEngine.Rendering.CullMode)] _CullMode ("Cull Mode", Float) = 2// Back

        [Space(5)]
        [Header(BaseColor)]
        [MainTexture] _MainTex ("Albedo(rgb)", 2D) = "white" { }
    }
}

다음의 코드를 적절히 주석 처리해서 각각 출력해보면 차이가 나는 것을 알 수 있다

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
    _materialEditor.DefaultShaderProperty(CullMode, "Default Cull Mode");
    _materialEditor.ShaderProperty(CullMode, Styles.CullModeText);
      
    _materialEditor.DefaultShaderProperty(MainTex, "Default Main Tex");
    _materialEditor.ShaderProperty(MainTex, Styles.MainTexText);
}

DefaultShaderProperty의 경우
ShaderProperty의 경우

구분선

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
    CoreEditorUtils.DrawSplitter();
    GUIStyle labelStyle = new GUIStyle();
    labelStyle.fontSize = 20;
    labelStyle.fontStyle = FontStyle.Bold;
    labelStyle.normal.textColor = Color.yellow;
    GUILayout.Label("Custom Shader GUI!!!!!", labelStyle);
    CoreEditorUtils.DrawSplitter();
}

폴더 메뉴

EditorStyles를 얇은 복사로 하면 유니티 에디터 전체에 영향을 끼치니 주의!!

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
    //var sty = EditorStyles.foldout;   // Shallow Copy
    var sty = new GUIStyle(EditorStyles.foldout);   // Deep Copy
    sty.fontSize = 20;
    sty.fontStyle = FontStyle.Bold;
    sty.normal.textColor = Color.green;
    sty.onNormal.textColor = Color.yellow;
    _foldMenuTest = EditorGUILayout.Foldout(_foldMenuTest, "Temp Header", true, sty);
    if (_foldMenuTest)
    {
        EditorGUI.indentLevel++;
        GUILayout.Label("!!!!!Custom Properties!!!!!");
        EditorGUI.indentLevel--;
        EditorGUILayout.Space();
    }

여러 프로퍼티를 한 줄에 넣기

다음의 코드로 프로퍼티를 한 줄에 넣을 수 있다

EditorGUILayout.BeginHorizontal();
// Property1
// Property2
// ...
EditorGUILayout.EndHorizontal();

하지만 레이아웃의 조절에 신경쓰지 않으면 모양새가 꽤나 망가진다는 단점이 있다

또한 밑에 설명할 MaterialHeaderScopeList와 같이 내부적으로 레이아웃을 조정하는 처리가 있으면 제대로 동작하지 않는다

제대로 사용하려면 GUIStyle, GUILayoutOption을 인수로 넣어줘야 할 것 같다


UPR의 폴더 메뉴 만들기

이 부분은 좀 길어져서 따로 정리하려고 한다

위의 폴더 메뉴와 비슷하게 생겼지만 URP의 Lit 셰이더 등에 있는 폴더 메뉴에 대한 내용이다

기본적으로 Lit셰이더를 분석하고 따라하면서 만들어봤다

카테고리(폴더 이름)에 대해 정의

우선 폴더 이름에 대해 정의하자

Styles클래스에서 폴더의 헤더에 대한 이름을 정하고, enum과 비트 연산으로 접고 펼치기에 대한 플래그를 만든다

enum의 어트리뷰트로 URL를 첨부도 가능한 것 같지만 그쪽은 안 해봤기에 생략한다

using System;
using UnityEngine;
using UnityEditor;

namespace CustomShaderGUI
{
    public class CustomExampleShaderGUI : ShaderGUI
    {
        private static class Styles
        {
            // Categories
            public static readonly GUIContent ShaderSettingsText = EditorGUIUtility.TrTextContent("Shader Settings", "");
            public static readonly GUIContent BasicColorText = EditorGUIUtility.TrTextContent("Basic Color", "");
            public static readonly GUIContent BumpMapSettingsText = EditorGUIUtility.TrTextContent("Bump Map Settings", "");
            // 생략...
            public static readonly GUIContent AdvancedText = EditorGUIUtility.TrTextContent("Advanced Settings", "");
        }

        [Flags]
        private enum Expandable
        {
            ShaderSettings = 1 << 0,
            BasicColor = 1 << 1,
            BumpMapSettings = 1 << 2,
            // 생략...
            Advanced = 1 << 7,
        }
    }
}

관련 변수 선언

그 다음으로 변수를 선언한다

나중에 필터로 사용할 uint.MaxValue와 실질적으로 표시를 담당하는 MaterialHeaderScopeList 타입의 변수를 선언해준다

using System;
using UnityEngine;
using UnityEditor;

namespace CustomShaderGUI
{
    public class CustomExampleShaderGUI : ShaderGUI
    {
        // 생략...

        [Flags]
        private enum Expandable
        {
            // 생략...
        }

        private uint _materialFilter => uint.MaxValue;
        // BumpMapSettings과 Advanced의 초기 상태를 접힌 상태로 지정
        private readonly MaterialHeaderScopeList _materialScopeList = new MaterialHeaderScopeList(uint.MaxValue & ~((uint)Expandable.BumpMapSettings | (uint)Expandable.Advanced));
    }
}

참고로 MaterialHeaderScopeList는 생성자의 인수를 통해 재미있는 설정이 가능하다

인수가 없거나 uint.MaxValue이면 폴더 메뉴가 전부 펼처진 상태가 초기 상태로 지정된다

하지만 위와 같이 enum의 값과 비트 연산해서, 특정 메뉴의 초기 상태을 접힌 상태로 할 수 있다

폴더 메뉴를 펼쳤을 때 실행할 메소드를 등록

다음으로 각각의 폴더 메뉴를 펼쳤을 때 실행할 메소드를 등록해야 한다

드로잉할 처리들을 메소드에 정리하고 그 메소드를 등록하면 된다

다음은 그 예시이다

using System;
using UnityEngine;
using UnityEditor;

namespace CustomShaderGUI
{
    public class CustomExampleShaderGUI : ShaderGUI
    {

        // 생략...
        private void RegisterHeader(Material material, MaterialEditor materialEditor)
        {
            var filter = (Expandable)_materialFilter;
            
            if (filter.HasFlag(Expandable.ShaderSettings))
                _materialScopeList.RegisterHeaderScope(Styles.ShaderSettingsText, (uint)Expandable.ShaderSettings, DrawShaderSettings);
                
            if (filter.HasFlag(Expandable.BasicColor))
                _materialScopeList.RegisterHeaderScope(Styles.BasicThreeToneColorText, (uint)Expandable.BasicThreeToneColor, DrawBasicColor);
                
            if (filter.HasFlag(Expandable.BumpMapSettings))
                _materialScopeList.RegisterHeaderScope(Styles.BumpMapSettingsText, (uint)Expandable.BumpMapSettings, DrawBumpSettings);

            // 생략...

            if (filter.HasFlag(Expandable.Advanced))
                _materialScopeList.RegisterHeaderScope(Styles.AdvancedText, (uint)Expandable.Advanced, DrawAdvancedSettings);
        }

        private void DrawShaderSettings(Material material)
        {
            if (CullMode != null)
                _materialEditor.ShaderProperty(CullMode, Styles.CullModeText);
        }

        private void DrawBasicColor(Material material)
        {
            // 생략...
        }

        // 생략...

    }
}

OnGUI에서 드로잉

마지막으로 OnGUI 메소드에서 MaterialHeaderScopeList의 드로잉 메소드를 호출하면 끝!

using System;
using UnityEngine;
using UnityEditor;

namespace CustomShaderGUI
{
    public class CustomExampleShaderGUI : ShaderGUI
    {

        // 생략...

        public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
        {
            if (materialEditor == null)
                throw new ArgumentNullException("materialEditor");
            
            _materialEditor = materialEditor;
            Material material = materialEditor.target as Material;
            
            FindProperties(properties);
            
            if (_firstTimeApply)
            {
                RegisterHeader(material, _materialEditor);
                _defaultInspector = false;
                _firstTimeApply = false;
            }

            // MaterialHeaderScopeList의 드로잉 메소드 호출!
            _materialScopeList.DrawHeaders(materialEditor, material);
        }

        private void RegisterHeader(Material material, MaterialEditor materialEditor)
        {
            // 생략...
        }

        // 생략...

    }
}

단점

위의 방법은 URP에서 디폴트로 사용하는 MaterialHeaderScopeList를 그대로 가져다 쓴 것이다

그래서 이미 정해진 서식이나 레이아웃으로 사용할 수 있다

또한 스타일에 관한 일부 메소드가 제대로 작동하지 않을 수도 있다

 

이 부분을 어떻게 하고 싶다면 어쩔 수 없이 MaterialHeaderScope과 비슷하게 움직이는 기능을 만들 수 밖에 없는 것 같다

유니티쨩 툰 쉐이더 URP에서는 UTS3MaterialHeaderScopeUTS3MaterialHeaderScopeList 등을 따로 정의해서 사용하고 있다


마무리

커스텀 셰이더 GUI에 대해 모든 걸 망라한 것은 아니지만, 일단 정리하고 싶었던 내용들은 얼추 정리한 것 같다

더 정리할 게 생기면 추가로 글을 쓰면 되겠지


참고 사이트

Unity - Scripting API: ShaderGUI

 

Unity - Scripting API: ShaderGUI

Derive from this class for controlling how shader properties should be presented. For a shader to use this custom GUI use the 'CustomEditor' property in the shader. Note that CustomEditor can also be used for classes deriving from MaterialEditor (search fo

docs.unity3d.com

【Unity】【シェーダ】【エディタ拡張】マテリアルのインスペクタ拡張まとめ - LIGHT11

 

【Unity】【シェーダ】【エディタ拡張】マテリアルのインスペクタ拡張まとめ - LIGHT11

マテリアルのインスペクタを拡張する方法と、よく使うプロパティ描画方法などをまとめました。

light11.hatenadiary.com

HLSL에서 셰이더 키워드 선언 및 사용 - Unity 매뉴얼

 

HLSL에서 셰이더 키워드 선언 및 사용 - Unity 매뉴얼

이 페이지는 HLSL 코드에서 셰이더 키워드를 사용하여 작업하는 방법에 대한 정보를 포함합니다. 셰이더 키워드에 대한 일반적인 안내는 셰이더 키워드를 참조하십시오. 셰이더 그래프에서 셰이

docs.unity3d.com

(UnityScript) Custom Shader GUI

 

(UnityScript) Custom Shader GUI

(StandardShaderGUI.cs) 유니티 엔진 내부의 빌트 인(Built-in) 셰이더인 'Standard' 의 'StandardShaderGUI.cs'를 참고해서 작성했습니다. (Built-in Shader) 유니티 다운로드 아카이브(https://unity3d.com/kr/get-unity/download/ar

walll4542.wixsite.com