定制ShaderGraph(三)---将URP默认的Keyword作为可选项
定制ShaderGraph(三)—将URP默认的Keyword作为可选项
00 前置知识
关于keyword的调用链
图示:
Library/PackageCache/com.unity.render-pipelines.universal@12.1.10/Editor/ShaderGraph/Targets/UniversalLitSubTarget.cs的Setup()函数
1
2
3
4
5
public override void Setup(ref TargetSetupContext context)
{
...
context.AddSubShader(PostProcessSubShader(SubShaders.LitGLESSubShader(target, workflowMode, target.renderType, target.renderQueue, complexLit)));
}
同文件中的SubShader部分的result.passes.Add代码, 这里是Pass相关, 而keyword是在这一步加入的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#region SubShader
static class SubShaders
{
...
public static SubShaderDescriptor LitGLESSubShader(UniversalTarget target, WorkflowMode workflowMode, string renderType, string renderQueue, bool complexLit)
{
...
if (complexLit)
// 这里是加入Pass, keyword是在这里进行加入的
result.passes.Add(LitPasses.ForwardOnly(target, workflowMode, complexLit, CoreBlockMasks.Vertex, LitBlockMasks.FragmentComplexLit, CorePragmas.Forward));
else
// 这里是加入Pass, keyword是在这里进行加入的
result.passes.Add(LitPasses.Forward(target, workflowMode));
...
return result;
}
}
#endregion
继续同文件深挖, 到Pass部分
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#region Passes
static class LitPasses
{
...
public static PassDescriptor Forward(UniversalTarget target, WorkflowMode workflowMode, PragmaCollection pragmas = null)
{
var result = new PassDescriptor()
{
...
// Conditional State
renderStates = CoreRenderStates.UberSwitchedRenderState(target),
pragmas = pragmas ?? CorePragmas.Forward, // NOTE: SM 2.0 only GL
defines = new DefineCollection() { CoreDefines.UseFragmentFog },
// 这里就是keyword相关的注入点
keywords = new KeywordCollection() { LitKeywords.Forward },
includes = LitIncludes.Forward,
// Custom Interpolator Support
customInterpolators = CoreCustomInterpDescriptors.Common
};
CorePasses.AddTargetSurfaceControlsToPass(ref result, target);
AddWorkflowModeControlToPass(ref result, target, workflowMode);
AddReceiveShadowsControlToPass(ref result, target, target.receiveShadows);
return result;
}
public static PassDescriptor ForwardOnly(
UniversalTarget target,
WorkflowMode workflowMode,
bool complexLit,
BlockFieldDescriptor[] vertexBlocks,
BlockFieldDescriptor[] pixelBlocks,
PragmaCollection pragmas)
{
var result = new PassDescriptor
{
...
// Conditional State
renderStates = CoreRenderStates.UberSwitchedRenderState(target),
pragmas = pragmas,
defines = new DefineCollection() { CoreDefines.UseFragmentFog },
// 这里就是keyword相关的注入点
keywords = new KeywordCollection() { LitKeywords.Forward },
includes = LitIncludes.Forward,
// Custom Interpolator Support
customInterpolators = CoreCustomInterpDescriptors.Common
};
if (complexLit)
result.defines.Add(LitDefines.ClearCoat, 1);
CorePasses.AddTargetSurfaceControlsToPass(ref result, target);
AddWorkflowModeControlToPass(ref result, target, workflowMode);
AddReceiveShadowsControlToPass(ref result, target, target.receiveShadows);
return result;
}
...
}
#endregion
继续跳转到keyword部分, 然后我们终于找到keyword定义的部分
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
#region Keywords
static class LitKeywords
{
...
public static readonly KeywordCollection Forward = new KeywordCollection
{
{ ScreenSpaceAmbientOcclusion },
{ CoreKeywordDescriptors.StaticLightmap },
{ CoreKeywordDescriptors.DynamicLightmap },
{ CoreKeywordDescriptors.DirectionalLightmapCombined },
{ CoreKeywordDescriptors.MainLightShadows },
{ CoreKeywordDescriptors.AdditionalLights },
{ CoreKeywordDescriptors.AdditionalLightShadows },
{ CoreKeywordDescriptors.ReflectionProbeBlending },
{ CoreKeywordDescriptors.ReflectionProbeBoxProjection },
{ CoreKeywordDescriptors.ShadowsSoft },
{ CoreKeywordDescriptors.LightmapShadowMixing },
{ CoreKeywordDescriptors.ShadowsShadowmask },
{ CoreKeywordDescriptors.DBuffer },
{ CoreKeywordDescriptors.LightLayers },
{ CoreKeywordDescriptors.DebugDisplay },
{ CoreKeywordDescriptors.LightCookies },
{ CoreKeywordDescriptors.ClusteredRendering },
};
...
}
#endregion
01 实施
首先, 我们之前已经将各个部分分割成了多个文件, 以确保逻辑清晰.
其中
ExternalPasses: 即(原UniversalSubTarget文件, 下同)#region Passes部分, RequiredFields部分, PortMasks部分(暂时未做继续拆分)
Includes: 即#region Includes部分
Keywords: 即#region Keywords部分和#region Defines部分(暂时未做继续拆分)
SubShader: 即#region Subshader部分
UniversalVehicleSubTargetExternal: 额外的其他部分
UniversalVehicleSubTargetExternalUI: 额外的与面板相关的部分, 即大部分的自定义部分放在这个文件中
UniversalVehicleSubTarget: 分割后剩下的部分
01.0 思路-数据结构
首先, 我需要一组数据结构, 一方面用于面板绘制, 另一方面用于Keyword启用逻辑
作为面板绘制的时候, 直接在UniversalVehicleSubTargetExternalUI调用即可;
作为Keyword启用逻辑时, 则需要层层传递到Keywords中, 才可以生效.
现在我们来制作这个数据结构:
01.0 实现-数据结构
先看看Unity绘制UI的数据结构
1
2
3
4
5
6
[SerializeField] bool m_ClearCoat = false;// 序列化用数据
public bool clearCoat // 对应属性
{
get => m_ClearCoat;
set => m_ClearCoat = value;
}
以及实际绘制时的调用方式
1
2
3
4
5
6
7
8
9
context.AddProperty("Clear Coat", new Toggle() { value = clearCoat }, (evt) =>
{
if (Equals(clearCoat, evt.newValue)) // 用Equals, 效率略低, 需装箱, 但可以支持null
return;
registerUndo("Change Clear Coat");
clearCoat = evt.newValue;
onChange();
});
这里仍旧保持Unity原本的序列化数据和属性不变, 在属性和UI之间, 新增一个数据结构做桥接.
- 通过
Func<bool> Get,Action<bool> Set对接属性, 同时对接UI - 通过
string Label对接UI - 通过
KeywordDescriptor? Descriptor对接keyword逻辑 - 数据结构放在一个新的文件
SubTargetExternalUIUtils.cs
暂时命名为ToggleDefinition
ToggleDefinition类代码为:
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
32
33
34
35
36
37
38
39
40
41
42
/// <summary>
/// 单个 toggle 的定义:面板显示名, 对应的属性, undo 文案
/// </summary>
internal class ToggleDefinition
{
public readonly string CustomLabel; // UI 显示
public readonly KeywordDescriptor? Descriptor;
public Func<bool> Get; // 读取当前值
public Action<bool> Set; // 写入新值
// 构造函数
public ToggleDefinition(KeywordDescriptor? descriptor, string customLabel = null)
{
Descriptor = descriptor;
CustomLabel = customLabel;
}
public ToggleDefinition(string customLabel = null)
{
CustomLabel = customLabel;
_customLabelHash = !string.IsNullOrEmpty(customLabel)
? customLabel.GetHashCode()
: 0;
}
public string Label
{
get
{
if (!string.IsNullOrEmpty(CustomLabel))
return CustomLabel;
if (Descriptor.HasValue)
return Descriptor.Value.displayName;
throw new InvalidOperationException(
"ToggleDefinition must have either CustomLabel or Descriptor set");
}
}
// 根据Label自动组装好的UndoMessage
public string UndoMessage => $"Change {Label}";
}
加入Hash算法相关的代码, 以便于之后使用HashSet自动去重
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
internal class ToggleDefinition
{
...
// 将两个构造函数改为支持HashS值生成的
public ToggleDefinition(KeywordDescriptor? descriptor, string customLabel = null)
{
Descriptor = descriptor;
CustomLabel = customLabel;
// 预先计算各部分的哈希
_refNameHash = descriptor.HasValue
? (descriptor.Value.referenceName?.GetHashCode() ?? 0)
: 0;
_displayNameHash = descriptor.HasValue
? (descriptor.Value.displayName?.GetHashCode() ?? 0)
: 0;
_customLabelHash = !string.IsNullOrEmpty(customLabel)
? customLabel.GetHashCode()
: 0;
}
public ToggleDefinition(string customLabel = null)
{
CustomLabel = customLabel;
_customLabelHash = !string.IsNullOrEmpty(customLabel)
? customLabel.GetHashCode()
: 0;
}
...
// 预先计算好的子哈希
private readonly int _refNameHash;
private readonly int _displayNameHash;
private readonly int _customLabelHash;
public bool Equals(ToggleDefinition other)
{
if (ReferenceEquals(this, other)) return true;
if (other is null) return false;
if (Descriptor.HasValue && other.Descriptor.HasValue)
{
var a = Descriptor.Value;
var b = other.Descriptor.Value;
// 优先按 referenceName 区分,不同则直接返回
if (!string.IsNullOrEmpty(a.referenceName) ||
!string.IsNullOrEmpty(b.referenceName))
{
return a.referenceName == b.referenceName;
}
// referenceName 都为空时,退而按 displayName
return a.displayName == b.displayName;
}
// 都没有 descriptor,则按 CustomLabel
return string.Equals(CustomLabel, other.CustomLabel, StringComparison.Ordinal);
}
public override bool Equals(object obj)
=> obj is ToggleDefinition td && Equals(td);
public override int GetHashCode()
{
unchecked
{
// 按 _refNameHash -> _dispNameHash -> _customLabelHash 顺序合并
int hash = _refNameHash;
hash = hash * 397 ^ _displayNameHash;
hash = hash * 397 ^ _customLabelHash;
return hash;
}
}
/// <summary>
/// 备用哈希算法:使用 HashCode.Combine(.NET Standard 2.1+ / .NET Core)
/// </summary>
public int GetHashCodeBackUp()
{
return HashCode.Combine(_refNameHash, _displayNameHash, _customLabelHash);
}
public static bool operator ==(ToggleDefinition left, ToggleDefinition right)
=> Equals(left, right);
public static bool operator !=(ToggleDefinition left, ToggleDefinition right)
=> !Equals(left, right);
}
在UniversalVehicleSubTargetExternalUI.cs声明数据结构, 以及数据初始化(在Setup()函数中调用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private readonly HashSet<ToggleDefinition> m_keywordToggleDefinitions = new HashSet<ToggleDefinition>();
private void InitKeywordToggleDefinitions()
{
m_keywordToggleDefinitions.Add(new ToggleDefinition(LitKeywords.ScreenSpaceAmbientOcclusion)
{
Get = () => screenSpaceAmbientOcclusion,
Set = v => screenSpaceAmbientOcclusion = v,
});
...
}
public override void Setup(ref TargetSetupContext context)
{
context.AddAssetDependency(kSourceCodeGuid, AssetCollection.Flags.SourceDependency);
#region 辅助数据初始化
InitKeywordToggleDefinitions();
#endregion
}
02.0 思路-统一绘制函数
当数据结构一定的情况下, 需要一个绘制函数进行foreach的绘制, 可以避免反复写绘制函数
绘制ToggleDefinition[]的伪代码为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 折叠打开后,依次绘制 toggle
int toggleIndent = foldIndent + 1;
foreach (var td in section.Toggles)
{
context.AddProperty(
label: td.Label,
indentLevel: toggleIndent,
new Toggle { value = td.Get() },
evt =>
{
if (td.Get() == evt.newValue) return; // 用==, 效率略高, 且无需装箱, 在确定两个值都不会为null时可以使用
registerUndo(td.UndoMessage);
td.Set(evt.newValue);
onChange();
});
}
同时, 我们需要把一类的物体放在一个Foldout中, 以免大量的Toggle同时显示在面板上.
02.1 实现-统一绘制函数
与ToggleDefinition一样, 声明一个SectionDefinition, 用来提供绘制Foldout所需的信息, 并且将HashSet<ToggleDefinition>作为字段放在其中, 以方便绘制函数统一绘制.
1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 折叠面板的定义:标题、颜色、折叠状态访问器,以及它下面的所有 toggles
/// </summary>
internal class SectionDefinition
{
public string Title;
public Color LabelColor;
public Func<bool> GetFold;
public Action<bool> SetFold;
public HashSet<ToggleDefinition> Toggles;
}
然后, 默认的TargetPropertyGUIContext中并没有AddFoldout的支持, 所以得自己实现一个, 参照AddProperty的实现, 代码如下:
注: 其中kIndentWidthInPixel的值为15. 另, ApplyPadding(PropertyRow row, int indentLevel)是私有方法, 直接copy实现即可(也可以用反射), 这两个部分都在文件PackageCache\com.unity.shadergraph\Editor\Generation\Contexts\TargetPropertyGUIContext.cs中. 所以, 我把这部分写在TargetPropertyGUIContextExtensions.cs中, 作为扩展方法.
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
32
33
34
35
36
37
38
/// <summary>
/// 在 TargetPropertyGUIContext 中添加一个 Foldout 控件:
/// 可选 tooltip、indentLevel、labelColor、callback;
/// 使用 PropertyRow 包装,调用 ApplyPadding 保持缩进一致。
/// </summary>
public static void AddFoldout(this TargetPropertyGUIContext context,
string title,
string tooltip,
Foldout foldout,
int indentLevel,
Color? labelColor,
EventCallback<ChangeEvent<bool>> callback)
{
// 注册回调
if (callback != null)
foldout.RegisterValueChangedCallback(callback);
// 构建行容器
var labelItem = new Label(title) { tooltip = tooltip };
var row = new PropertyRow(labelItem);
// 手动应用缩进:复制 ApplyPadding 的内部逻辑
// 本来应该用 context.ApplyPadding, 是私有方法, 我不想反射去取
// 内部是: row.Q(className:"unity-label").style.marginLeft = (ctx.globalIndentLevel + indentLevel) * kIndentWidthInPixel;
var unityLabel = row.Q(className: "unity-label");
if (unityLabel != null)
{
unityLabel.style.marginLeft = (context.globalIndentLevel + indentLevel) * kIndentWidthInPixel;
}
// 可选上色
if (labelColor.HasValue)
labelItem.style.color = labelColor.Value;
// 把 Foldout 本体放入行内
row.Add(foldout);
context.hierarchy.Add(row);
}
最终的绘制函数为
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
32
33
34
35
36
37
38
39
40
internal static void DrawSection(
ref TargetPropertyGUIContext context,
SectionDefinition section,
Action onChange,
Action<string> registerUndo,
int foldIndent = 0)
{
// 折叠面板
var foldout = new Foldout { value = section.GetFold() };
context.AddFoldout(
title: section.Title,
foldout: foldout,
indentLevel: foldIndent,
labelColor: section.LabelColor,
callback: evt =>
{
section.SetFold(evt.newValue);
onChange();
});
if (!section.GetFold())
return;
// 折叠打开后,依次绘制 toggle
int toggleIndent = foldIndent + 1;
foreach (var td in section.Toggles)
{
context.AddProperty(
label: td.Label,
indentLevel: toggleIndent,
new Toggle { value = td.Get() },
evt =>
{
if (td.Get() == evt.newValue) return;
registerUndo(td.UndoMessage);
td.Set(evt.newValue);
onChange();
});
}
}
增加绘制函数, 绘制函数需要用到的SectionDefinition和需要调用的属性(classicKeywordsFoldoutOn), 以及属性对应的序列化字段(m_ClassicKeywordsFoldoutOn默认值为false即收起), 和通常的属性和属性对应的序列化字段一样, 正常声明即可. 绘制函数在 GetPropertiesGUI()方法中, 绘制完原本的参数后, 再调用即可.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public override void GetPropertiesGUI(ref TargetPropertyGUIContext context, Action onChange,
Action<String> registerUndo)
{
...
context.AddProperty("Clear Coat", new Toggle() { value = clearCoat }, (evt) =>
{
if (Equals(clearCoat, evt.newValue))
return;
registerUndo("Change Clear Coat");
clearCoat = evt.newValue;
onChange();
});
#region 加入自定义UI
// 自定义 External Control 块
DrawExternalControlGUI(ref context, onChange, registerUndo);
#endregion
}
private void DrawExternalControlGUI(
ref TargetPropertyGUIContext context,
Action onChange,
Action<string> registerUndo)
{
#if UNITY_EDITOR
Debug.Log("DrawExternalControlGUI");
#endif
var sections = CreateSectionDefinitions();
foreach (var section in sections)
{
SubTargetExternalUIUtils.DrawSection(
ref context,
section,
onChange,
registerUndo
);
}
}
SectionDefinition[] CreateSectionDefinitions()
{
return new SectionDefinition[]
{
...
new SectionDefinition()
{
Title = CustomStyles.ClassicKeywordsFoldoutName,
LabelColor = CustomStyles.ClassicKeywordsFoldoutColor,
GetFold = () => classicKeywordsFoldoutOn,
SetFold = v => classicKeywordsFoldoutOn = v,
Toggles = m_keywordToggleDefinitions
},
...
}
}
03.0 思路-正式添加
分三个步骤
- 添加序列化字段
- 添加字段对应属性
- 在初始化函数中添加对应的
ToggleDefinition
03.1 实现-正式添加
添加序列化字段
1
[SerializeField] private bool m_ScreenSpaceAmbientOcclusion = true;
添加字段对应属性
1
2
3
4
5
public bool screenSpaceAmbientOcclusion
{
get => m_ScreenSpaceAmbientOcclusion;
set => m_ScreenSpaceAmbientOcclusion = value;
}
在初始化函数中添加对应的ToggleDefinition
1
2
3
4
5
6
7
8
9
private void InitKeywordToggleDefinitions()
{
m_keywordToggleDefinitions.Add(new ToggleDefinition(LitKeywords.ScreenSpaceAmbientOcclusion)
{
Get = () => screenSpaceAmbientOcclusion,
Set = v => screenSpaceAmbientOcclusion = v,
});
...
}
至此, 添加结束
