일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Specular
- 작업 집합
- Private Bytes
- ColorGradingLutPass
- C언어
- 가상 바이트
- 게임 수학
- Cell Look
- Virtual Byte
- VR
- Cartoon Rendering
- 벡터
- OculusMotionVectorPass
- ASW(Application SpaceWarp)
- 개인 바이트
- AppSW
- 3d
- URP
- Three(Two) Tone Shading
- Windows Build
- Cell Shader
- Rim Light
- 프로그래밍 기초
- Toon Shader
- URP로 변경
- 메모리 누수
- working set
- Today
- Total
WinCNT
Unity에서 커스텀 ShaderGUI를 구현해보자! 그 두번째 본문
서론
전에 커스텀 ShaderGUI를 구현하는 법을 작성한 적이 있었다
https://wincnt-shim.tistory.com/371
그 글을 작성한 뒤로 새롭게 알게 된 방법들이 있어서 추가로 정리하고자 한다
그리고 셰이더 키워드를 On/Off하는 방법도 겸사겸사 정리하고자 한다
ShaderGUI의 존재 의의를 뒤흔드는 MaterialPropertyDrawer
사실 간단한 UI라면 ShaderGUI를 계승한 클래스를 만들지 않고도 MaterialPropertyDrawer를 사용해서 구현할 수 있다
Unity - Scripting API: MaterialPropertyDrawer
사실 많이들 봤을 거라고 생각한다
꽤나 강력한 기능으로 Enum과 Toggle 정도는 한 조각의 케이크마냥 쉽게 구현할 수 있다
심지어 Toggle은 셰이더의 Keyword도 On/Off도 바꿔줄 수 있다
Shader "Custom/Example"
{
Properties
{
[KeywordEnum(None, Add, Multiply)] _Overlay("Overlay mode", Float) = 0
[Toggle] _Invert("Invert color?", Float) = 0
}
// rest of shader code...
}
ShaderGUI와 MaterialPropertyDrawer를 병행하면 살짝 귀찮을 때가 있다…특히 Header라던가 Space라던가…
MaterialPropertyDrawer가 아무리 편리해도 모든 범위를 커버할 순 없으므로사실 대부분은 커버할 수 있을 거 같은데ShaderGUI은 필요하며, 둘을 병행해서 사용하기도 한다
문제는 다음과 같이 Header나 Space 등을 쓰는 경우 조금 귀찮아질 수 있다
Shader "Custom/Example"
{
Properties
{
[Header(UV Scroll)]
[Space(10)]
[Toggle] _IsScroll("Is Scrolling", Float) = 0.0
}
// rest of shader code...
}
위의 소스 코드의 경우는 ShaderGUI가 없거나 base.OnGUI면 다음과 같이 별 문제 없이 나온다
그런데 디폴트는 그대로 두고, ShaderGUI에서는 아래와 같이 조금 다른 Label를 사용하거나 싶다고 해보자
이 때 평소 하던대로 ShaderProperty를 쓰면 Header와 Label이 이중(…)으로 나오게 된다
GUILayout.Label(Styles.ScrollText, EditorStyles.boldLabel);
_materialEditor.ShaderProperty(IsScroll, Styles.IsScrollText);
그렇다고 DefaultShaderProperty를 쓰기에는 Toggle이 없어져버리고…
GUILayout.Label(Styles.ScrollText, EditorStyles.boldLabel);
_materialEditor.DefaultShaderProperty(IsScroll, Styles.IsScrollText.text);
“그냥 셰이더 쪽의 MaterialPropertyDrawer을 포기하면 되는 거 아님?”이라고 생각할 수 있는데, 사실 필자도 그렇게 생각한다
그래도 다른 방법이 없냐하면 또 그런 것은 아니다!!
EditorGUI.Toggle과 EditorGUILayout.GetControlRect를 이용해서 구현할 수 있다
// UV Scroll Label
GUILayout.Label(Styles.ScrollText, EditorStyles.boldLabel);
// Is Scroll
bool isScroll = (IsScroll.floatValue != 0.0f);
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = IsScroll.hasMixedValue;
isScroll = EditorGUI.Toggle(EditorGUILayout.GetControlRect(), Styles.IsScrollText, isScroll);
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck())
{
IsScroll.floatValue = isScroll ? 1.0f : 0.0f;
}
그럼 짜잔~!
프로퍼티에 Int(정수)만 입력되도록 하기!
셰이더 쪽에는 분명 int로 설정했는데도 Inspector에는 float로 나오는 게 신경쓰였다
Shader "Custom/Example"
{
Properties
{
_AnimationIndex("Animation Index Control", int) = 0.0
}
}
이건 Toggle과 비슷한 방식으로 EditorGUI의 IntField를 써서 해결했다! 만세!
var animIdx = (int)AnimationIndex.floatValue;
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = AnimationIndex.hasMixedValue;
animIdx = EditorGUI.IntField(EditorGUILayout.GetControlRect(), Styles.AnimationIndexText, animIdx);
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck())
AnimationIndex.floatValue = animIdx;
…라고 생각하던 시기가 저에게도 있었습니다
Int는 Toggle과는 다르게 MaterialEditor에 API가 있었다!
_materialEditor.IntShaderProperty(AnimationIndex, Styles.AnimationIndexText);
참고로 내부를 확인해보니 거의 같았다…이거 써야지
public static void IntShaderProperty(this MaterialEditor editor, MaterialProperty prop, GUIContent label, System.Func<int, int> transform = null)
{
MaterialEditor.BeginProperty(prop);
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = prop.hasMixedValue;
int newValue = EditorGUI.IntField(GetRect(prop), label, (int)prop.floatValue);
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck())
{
if (transform != null)
newValue = transform(newValue);
prop.floatValue = newValue;
}
MaterialEditor.EndProperty();
}
프로퍼티 2개를 그럴듯하게 한 줄로 표시하기!
UI를 작성하다 보니 위와 같이 X랑 Y가 두 줄로 표시되는 것이 매우 마음에 들지 않았다…
그래서 Tiling과 Offset를 표시하는 API를 분석해서 나름 구현해봤다
float width = EditorGUIUtility.labelWidth;
float height = EditorGUIUtility.singleLineHeight;
Rect position = EditorGUILayout.GetControlRect(true);
Rect labelPosition = new Rect(position.x, position.y, width, height);
MaterialEditor.BeginProperty(position, SpeedX);
EditorGUI.BeginChangeCheck();
// Label
EditorGUI.PrefixLabel(labelPosition, Styles.SpeedText);
// Property
Rect propPosition = new Rect(position.x + width, position.y, position.width - width, height);
var value = EditorGUI.Vector2Field(propPosition, GUIContent.none, new Vector2(SpeedX.floatValue, SpeedY.floatValue));
if (EditorGUI.EndChangeCheck())
{
SpeedX.floatValue = value.x;
SpeedY.floatValue = value.y;
}
MaterialEditor.EndProperty();
핵심은 EditorGUI.Vector2Field라는 API였는데 아무튼 좋은 느낌으로 완성!
X, Y 말고는 다른 라벨을 붙일 수 없나?
X, Y 말고 위의 Row와 Col인 라벨을 붙이는 법이 없나 찾아봤다
다행히 MultiFloatField를 쓰면 어떻게 될 것 같아서 사용해봤다!
if (Row != null && Column != null)
{
GUIContent label = EditorGUIUtility.TrTextContent("Column and Row");
GUIContent[] subLabel = { Styles.RowText, Styles.ColumnText };
float[] val = new[] { Row.floatValue, Column.floatValue };
EditorGUI.BeginChangeCheck();
EditorGUI.MultiFloatField(EditorGUILayout.GetControlRect(), label, subLabel, val);
if (EditorGUI.EndChangeCheck())
{
Row.floatValue = val[0];
Column.floatValue = val[1];
}
// 다음 프로퍼티와 겹치지 않도록 조정
EditorGUILayout.GetControlRect();
}
그럼 따란~!
그런데 생각보다 알아보기 힘들어서 되돌렸다
Enum으로 Drop-down list 만들기
이번에는 Opaque, Cutoff의 값을 가진 Surface Type을 추가해서 셰이더의 키워드를 바꿀 수 있도록 해봤다
셰이더 키워드 설정은 나중에 정리하도록 하고 우선은 Surface Type란 Enum을 ShaderGUI에 어떻게 표시할 수 있는지 찾아봤다
EnumPopup를 사용
찾아보니 EnumPopup이란 것이 있길래 사용해봤다
(참고로 Styles은 GUIContent 등을 정리한 내부 클래스이다)
private enum SurfaceType
{
Opaque,
Cutoff
}
public static readonly GUIContent SurfaceTypeText = EditorGUIUtility.TrTextContent("Surface Type", "Opaque / Cutoff");
private static SurfaceType _surfaceType = SurfaceType.Opaque;
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
_surfaceType = (SurfaceType)EditorGUILayout.EnumPopup(Styles.SurfaceTypeText, _surfaceType);
}
Enum이 잘 나오긴 하지만 아쉽게도 Undo와 Redo(Ctrl + Z, Ctrl + Y)는 적용되지 않았다
셰이더 쪽에 프로퍼티가 없어도 된다는 장점이 있지만 일단 기각
PopupShaderProperty를 사용
다음은 PopupShaderProperty란 메소드를 사용해봤다
프로퍼티가 필요하다고 하니 먼저 셰이더 쪽에 추가해줬다(Find Proerties 등은 생략)
Shader "SSS/Custom"
{
Properties
{
// ...생략...
[HideInInspector]_Surface("__surface", Float) = 0.0
// ...생략...
}
}
그리고 다음과 같이 사용해봤다
private static class Styles
{
public static readonly string[] SurfaceTypeNames = Enum.GetNames(typeof(SurfaceType));
// ...생략...
}
// ...생략...
private enum SurfaceType
{
Opaque,
Cutout,
Transparent
}
// ...생략...
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
SurfaceType surfaceType = (SurfaceType)_materialEditor.PopupShaderProperty(Surface, Styles.SurfaceTypeText, Styles.surfaceTypeNames);
// ...생략...
}
Enum도 잘 나오고Undo와 Redo(Ctrl + Z, Ctrl + Y)도 잘 됐다!
PopupShaderProperty의 내부
궁금해서 메소드의 내부를 조금 확인해봤다
/// <summary>
/// Draw a popup selection field for a float shader property.
/// </summary>
/// <param name="editor"><see cref="MaterialEditor"/></param>
/// <param name="prop">The MaterialProperty to make a field for</param>
/// <param name="label">Label for the property</param>
/// <param name="displayedOptions">An array with the options shown in the popup</param>
/// <returns>The index of the option that has been selected by the user</returns>
public static int PopupShaderProperty(this MaterialEditor editor, MaterialProperty prop, GUIContent label, string[] displayedOptions)
{
MaterialEditor.BeginProperty(prop);
int val = (int)prop.floatValue;
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = prop.hasMixedValue;
int newValue = EditorGUILayout.Popup(label, val, displayedOptions);
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck() && (newValue != val || prop.hasMixedValue))
{
editor.RegisterPropertyChangeUndo(label.text);
prop.floatValue = val = newValue;
}
MaterialEditor.EndProperty();
return val;
}
Undo와 Redo(Ctrl + Z, Ctrl + Y)가 적용된 건 분명 editor.RegisterPropertyChangeUndo(label.text);가 있기 때문이겠지
Emission 프로퍼티는 있는 거 쓰자!
이번에 추가로 구현하면서 Emission은 의외로 신경써야 할 점이 많다는 것을 깨달았다
그냥 빛나게 하는 것 뿐이면 간단하지만, 전역 조명(GI)이 연관되면 복잡해진다!
예를 들면, Emission은 Light Map에 구울까? 아니면 리얼 타임으로 영향을 줄까? 역시 주변에는 영향을 주지 않고 그냥 물체가 빛나기만 할까? Emission의 Color가 검정색이면 어떻게 해야하지? 등등
설정 자체는 머티리얼의 globalIlluminationFlags라는 프로퍼티를 통해서 할 수 있다
Unity - Scripting API: Material.globalIlluminationFlags
하지만 다양한 플래그를 관리하려면 머리만 아플 뿐…
다행히 이 부분은 디폴트 Lit 셰이더의 GUI에 이미 구현되어 있기 때문에, 그대로 가져다 쓰기로 했다
아래는 예시 코드이다
public override void ValidateMaterial(Material material)
{
// Emission
if (EmissionColor != null)
MaterialEditor.FixupEmissiveFlag(material);
bool shouldEmissionBeEnabled = (material.globalIlluminationFlags & MaterialGlobalIlluminationFlags.EmissiveIsBlack) == 0;
CoreUtils.SetKeyword(material, "_EMISSION", shouldEmissionBeEnabled);
}
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);
// ...생략...
// Emission
var emissive = _materialEditor.EmissionEnabledProperty();
using (new EditorGUI.DisabledScope(!emissive))
{
if ((EmissionMask == null) || (EmissionColor == null))
return;
using (new EditorGUI.IndentLevelScope(2))
{
_materialEditor.TexturePropertyWithHDRColor(Styles.EmissionMap, EmissionMask, EmissionColor, true);
}
_materialEditor.LightmapEmissionFlagsProperty(MaterialEditor.kMiniTextureFieldLabelIndentLevel, emissive);
}
}
그러면 디폴트 Lit 셰이더와 같이 Emission의 On/Off나 GI 플러그 설정을 그대로 쓸 수 있다!
주의해야 할 점은 셰이더 쪽에 _EmissionColor라는 프로퍼티를 가지고 있어야 한다는 것이다
이는 FixupEmissiveFlag 메소드가 내부에서 _EmissionColor란 이름의 프로퍼티에서 색상 값을 획득하기 때문이다(당연히 색상 값은 플러그 설정에도 영향을 준다)
일반적으로 Emission을 사용하면 Emission Color도 사용할테고 이름도 보통 _EmissionColor라고 할테니 딱히 문제는 없다고 할 수 있다
하지만 현재 프로젝트에는 한 머티리얼에 Main과 Sub 텍스처를 동시에 설정하고 물론 Emission도 따로 설정하고 심지어 Emission Color는 설정하지 않는 셰이더가 있는데 그건 어떻게 해야하지 으아아아!! 살려줘!!
Blend를 설정하기
ShaderGUI를 통해서 Blend 단계의 설정도 컨트롤할 수 있다
Unity Shader파일에서의 Blend에 대한 건 공식 문서에 자세히 설명되어 있다
Unity - Manual: ShaderLab command: Blend
Early-Z…또 너야?! 하지만 이번엔 괜찮았죠?
그런데 공식 문서를 보니 매우 신경쓰이는 문구를 발견했다
Enabling blending disables some optimizations on the GPU (mostly hidden surface removal/Early-Z), which can lead to increased GPU frame times. 블렌딩을 활성화하면 GPU의 일부 최적화(주로 숨겨진 표면 제거/Early-Z)가 비활성화되어 GPU 프레임 시간이 늘어날 수 있습니다.
이 문구를 보고 불투명 오브젝트에도 불구하고 Blend가 On이라서 퍼포먼스가 떨어지진다는 최악의 경우가 발생하지 않을까 매우 걱정이 되었다
(알파 테스트나 반투명일 때는 퍼포먼스가 떨어지는 거 알고 사용하니 별 상관 없지만)
그래도 실제로는 어떤지 궁금해서 Blend Off로 설정한 셰이더가 프레임 디버그에서 어떻게 나오는지 확인해봤다
이걸 보니 ‘혹시 유니티에서는 Blend One Zero를 해도 Blend를 Off로 해주지 않을까?’라는 생각이 들기 시작했다
나름 근거를 얻고 싶어서 진짜 엄청 찾아봤는데 검색어 선정하기 어려워서 좀처럼 나오지 않았다
그래도 결국 다음과 같은 Forum을 찾았다! 만세!
Is There Extra Performance Cost for "Blend One Zero" ?
조사가 끝났으면 구현이 시작되지
조사가 끝났을 때 일이 다 끝난 것처럼 느껴졌지만 실제로는 만들어진 건 아무것도 없었다
이제부터는 어떻게 구현해봤는지 정리해보려고 한다
하지만 기본은 똑같았다
셰이더 쪽에 프로퍼티를 선언하고, ShaderGUI 쪽에서 상황에 알맞게 값을 설정해주면 된다
아래는 샘플 코드의 일부분이다(셰이더의 프로퍼티 값을 찾는 부분은 생략)
Shader
Shader "Custom/Example"
{
Properties
{
[HideInInspector][Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("__SrcBlend", Float) = 1
[HideInInspector][Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("__DstBlend", Float) = 0
// rest of shader code...
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
Pass
{
Name "UVScroll"
Tags { "LightMode" = "UniversalForward" }
// <https://forum.unity.com/threads/is-there-extra-performance-cost-for-blend-one-zero.1154021/>
Blend [_SrcBlend][_DstBlend]
}
}
}
ShaderGUI
public override void ValidateMaterial(Material material)
{
if (material == null)
throw new ArgumentNullException("material");
bool alphaClip = false;
if (Cutoff != null)
alphaClip = Cutoff.floatValue >= 0.5;
// Clear
int renderQueue = material.shader.renderQueue;
material.SetOverrideTag("RenderType", "");
if (SrcBlend != null) SrcBlend.floatValue = (float)BlendMode.One;
if (DstBlend != null) DstBlend.floatValue = (float)BlendMode.Zero;
if (AlphaToMask != null) AlphaToMask.floatValue = 0.0f;
material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
material.DisableKeyword("_ALPHATEST_ON");
if (Surface != null)
{
SurfaceType surfaceType = (SurfaceType)Surface.floatValue;
if (surfaceType == SurfaceType.Opaque)
{
material.SetOverrideTag("RenderType", "");
if (SrcBlend != null) SrcBlend.floatValue = (float)BlendMode.One;
if (DstBlend != null) DstBlend.floatValue = (float)BlendMode.Zero;
if (ZWrite != null) ZWrite.floatValue = 1.0f;
if (AlphaToMask != null) AlphaToMask.floatValue = 0.0f;
material.renderQueue = (int)RenderQueue.Geometry;
}
else if (surfaceType == SurfaceType.Cutout)
{
material.SetOverrideTag("RenderType", "TransparentCutout");
if (SrcBlend != null) SrcBlend.floatValue = (float)BlendMode.One;
if (DstBlend != null) DstBlend.floatValue = (float)BlendMode.Zero;
if (ZWrite != null) ZWrite.floatValue = 1.0f;
if (AlphaToMask != null) AlphaToMask.floatValue = 1.0f;
CoreUtils.SetKeyword(material, "_ALPHATEST_ON", alphaClip);
material.renderQueue = (int)RenderQueue.AlphaTest;
}
else
{
// Blending Mode
BlendingMode blendMode = (BlendingMode)Blend.floatValue;
var srcBlend = UnityEngine.Rendering.BlendMode.One;
var dstBlend = UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha;
switch (blendMode)
{
case BlendingMode.Alpha:
srcBlend = UnityEngine.Rendering.BlendMode.SrcAlpha;
dstBlend = UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha;
break;
case BlendingMode.Premultiply:
srcBlend = UnityEngine.Rendering.BlendMode.One;
dstBlend = UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha;
break;
case BlendingMode.Additive:
srcBlend = UnityEngine.Rendering.BlendMode.One;
dstBlend = UnityEngine.Rendering.BlendMode.One;
break;
case BlendingMode.Multiply:
srcBlend = UnityEngine.Rendering.BlendMode.DstColor;
dstBlend = UnityEngine.Rendering.BlendMode.Zero;
break;
}
material.SetOverrideTag("RenderType", "Transparent");
if (SrcBlend != null) SrcBlend.floatValue = (float)srcBlend;
if (DstBlend != null) DstBlend.floatValue = (float)dstBlend;
if (AlphaToMask != null) AlphaToMask.floatValue = 1.0f;
CoreUtils.SetKeyword(material, "_SURFACE_TYPE_TRANSPARENT", true);
CoreUtils.SetKeyword(material, "_ALPHATEST_ON", alphaClip);
// Geometry, AlphaTest < GeometryLast < Transparent
material.renderQueue = (int)RenderQueue.GeometryLast;
}
if (QueueOffset != null)
material.renderQueue += (int)QueueOffset.floatValue;
}
잘 사용하면 위의 코드처럼 반투명이 아닌 특수한 반투명을 구현하는 등의 응용도 가능하다!
코드에선 생략됐지만 ZTest도 같은 방식으로 가능하고, Blend Mode에서 RGB 값과 알파 값의 팩터를 별도로 설정하는 것도 가능하다
마무리
이번 작업을 통해서 ShaderGUI에 대한 이해가 더 깊어지게 되었다
개인적으로는 드디어 셰이더에 키워드를 설정할 수 있게 되었다는 실감이 들었다
아무튼 이번에도 크나큰 도움을 준 BaseShaderGUI.cs에 대해 무한한 감사를……
참고 사이트
How to write a general shader code for different render type like standard shader
Is There Extra Performance Cost for "Blend One Zero" ?
[Unity]ZTestやBlend等をシェーダー外から設定する - Qiita
'Unity > Unity 관련' 카테고리의 다른 글
Unity의 EditorWindow를 한 번 알아보자 그 두번째! 이번엔 Reflection과 Action으로 Method를 실행해보자! (0) | 2024.03.28 |
---|---|
Unity의 EditorWindow를 한 번 알아보자 그 첫번째! 덤으로 Reflection과 PropertyDrawer로 List 요소만 표시해보자! (0) | 2024.03.25 |
UniTask를 인스톨하고 살짝 사용해봤다! (0) | 2023.11.30 |
Unity 2022.2부터 Navigation이 AI Navigation로 바뀌었다길래 사용해봤다 (0) | 2023.11.14 |
Github Actions 사용해보기! (0) | 2023.10.31 |