일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Virtual Byte
- 게임 수학
- VR
- 메모리 누수
- 벡터
- URP로 변경
- Cartoon Rendering
- ColorGradingLutPass
- ASW(Application SpaceWarp)
- Windows Build
- C언어
- 프로그래밍 기초
- Toon Shader
- URP
- working set
- 가상 바이트
- Rim Light
- 개인 바이트
- Three(Two) Tone Shading
- OculusMotionVectorPass
- 작업 집합
- Private Bytes
- Specular
- Cell Look
- Cell Shader
- 3d
- AppSW
- Today
- Total
WinCNT
Unity에서 커스텀 ShaderGUI를 구현해보자! 그 첫번째 본문
서론
이번에는 Unity에서 커스텀 ShaderGUI를 구현해보기로 했다
ShaderGUI란 특정 셰이더의 머티리얼에 대한 Inspector의 UI를 제작할 수 있게 해주는 클래스라고 생각하면 될 것 같다
이 글에 구현하면서 알게 된 내용을 정리하고자 한다
커스텀 셰이더 GUI를 구현하면 좋은 점?
어디까지나 개인적 의견이지만…
역시 최고의 장점은 머티리얼의 프로터티를 보기 좋게 정리할 수 있다는 점이라고 본다
위는 URP의 디폴트 Lit 셰이더인데 토글, 체크 박스, 셀렉트 박스 등으로 깔끔하게 정리된 것을 볼 수 있다
두번째로는 커스텀 GUI를 통해서 셰이더의 키워드를 설정할 수 있게 된다는 점이다
즉 셰이더에서 #pragma multi_compile과 #pragma shader_feature 등에 의해 셰이더 베리언트가 만들어졌을 경우, GUI를 통해 해당 머티리얼이 어느 셰이더 베리언트를 사용할 지 바꿀 수 있다
셰이더 키워드에 대해선 공식 문서에도 자세하게 설명되어 있어서 첨부한다
HLSL에서 셰이더 키워드 선언 및 사용 - Unity 매뉴얼
셰이더 GUI용 스크립트 파일 생성하기
셰이더의 커스텀 GUI를 구현하기 위해 우선적으로 필요한 것은 스크립트 파일이다
커스텀 GUI는 에디터 상태에서만 사용하기 때문에 Assets > Editor 폴더에 만들어주자
네임스페이스와 클래스의 이름도 잘 설정해주자
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의 ShaderProperty나 DefaultShaderProperty를 사용하면 된다
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);
}
구분선
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에서는 UTS3MaterialHeaderScope나 UTS3MaterialHeaderScopeList 등을 따로 정의해서 사용하고 있다
마무리
커스텀 셰이더 GUI에 대해 모든 걸 망라한 것은 아니지만, 일단 정리하고 싶었던 내용들은 얼추 정리한 것 같다
더 정리할 게 생기면 추가로 글을 쓰면 되겠지
참고 사이트
Unity - Scripting API: ShaderGUI
【Unity】【シェーダ】【エディタ拡張】マテリアルのインスペクタ拡張まとめ - LIGHT11
HLSL에서 셰이더 키워드 선언 및 사용 - Unity 매뉴얼
(UnityScript) Custom Shader GUI
'Unity > Unity 관련' 카테고리의 다른 글
Unity 2022.2부터 Navigation이 AI Navigation로 바뀌었다길래 사용해봤다 (0) | 2023.11.14 |
---|---|
Github Actions 사용해보기! (0) | 2023.10.31 |
CMD와 ADB로 만들었던 스크립트를 PowerShell로 바꿔보자! (0) | 2023.10.10 |
CMD와 ADB로 유니티의 apk를 빌드하고 VR기기에서 실행해보기 (0) | 2023.10.04 |
유니티에서 안드로이드 기기에 연결되어 있으면 Build And Run, 그렇지 않으면 Build만 하는 스크립트 만들어보기 (0) | 2023.09.21 |