Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,Uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把Uniform值设置成什么,Uniform会一直保存它们的数据,直到它们被重置或更新。
UE4的材质编辑器可以调用的红色节点中很多就是Uniform,如ObjectBounds,ActorPosition等等。本文记录了如何在UE4.22或更高版本添加自定义的Uniform,并且生成材质节点在任意材质中调用,该Uniform值也将显示在场景的Skylight组件上供用户修改。
创建CPU中的数据结构体
FShaderParametersMetadata
这是一个4.22的新类,4.21有一个类似的结构体FUniformBufferStruct。4.22中可以在ShaderParameterMetadata.h中找到这个类。
FShaderParametersMetadata是一个储存了多个着色器参数的集合或容器。该容器可以储存不同数据类型的参数。每个参数都是一个成员,可以是矩阵、向量、数组或者纹理。
(提示:FShaderParameter.h中还定义了另外一个着色器参数系统。有FShaderParameter, FShaderResourceParameter, FShaderUniformBufferParameter等等。依然可以在单独的着色器中使用,如在LightRendering.cpp中一样。)
如果要定义一组着色器参数,可以用宏来声明这些元数据,这是将它们放进UniformBuffer的快捷办法。
ShaderParameterMacros.h
这个头文件中包含了一些声明参数结构体的宏。比如BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT()和END_GLOBAL_SHADER_PARAMETER_STRUCT()。这两个宏创建了一个类作为ParameterMetadata的壳,如:1
2
3
4
5
6
7
8
9// Begin Macro:
class STRUCT_NAME {
static Metadata;
CreateUniformBuffer() (when declare as GLOBAL)
...
Parameter Macro: Insert all members (Parameter)
...
// End:
some ending stuff + }
对于全局的参数结构体,cpp中需要添加另外一个宏: IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FMyParameterStruct, “MyShaderBindingName”);
这个宏将静态的元数据放入一个全局的容器,扩展后的宏类似下方:1
2FShaderParametersMetadata StructTypeName::Metadata(...);
=> FShaderParametersMetadata(..) { Add-Self-To-Global-Container }
现在有了元数据的壳,可以开始使用SHADER_PARAMETER(Type, Name) 添加一些参数到壳中,用类型和名称创建一个成员,然后添加该成员到元数据的成员数组中。
(提示:有两个版本的声明:GLOBAL和LOCAL,区别是GLOBAL会在类中创建一个UniformBuffer并且所有成员会放进这个缓冲中,另外GLOBAL会将元数据保存在全局的map和list中。而LOCAL方式不会创建内在的UniformBuffer,所以不用调用IMPLEMENT_宏。)
案例:Primitive Uniform, View Uniform
可以参考一下PrimitiveUniform和ViewUniform,两个最重要的UniformShaderParameters。
PrimitiveUniformShaderParameters.h
该头文件中声明了一个FPrimitiveUniformShaderParameters的结构体。之所以比较重要是因为在检索着色器代码的时候经常会看到”Primitive”或”GetPrimitiveData()“这样的变量,都是来自这个uniform结构体的。它保存了一个图元(或图元组件)的信息,包括LocalToWorld, WorldToLocal等等。
FViewUniformShaderParameters(SceneView.h)
可以认为这是当前视图相关的场景信息,包括相机的信息,游戏时间或者其他渲染需要的信息。这也是一个适合添加自定义全局Uniform参数的地方。在这个ShaderParameters中,
4.22加入了StructuredBuffer类型,可以传递数组进StructuredBuffer,名称为PrimitiveSceneData的Buffer包含所有GPU实例化的图元数据。
IMPLEMENT_Macro(SceneView.cpp)
上文提到过,实现宏创建元数据并且保存到全局的容器中。另外,这个宏也会帮助着色器编译器为GPU生成着色器代码。例如着色器编译器会生成一个”cbuffer Primitive{}”和”cbuffer View{}”,名称是传进宏里的名称。cbuffer的内容也是从FPrimitiveUniformShaderParameters或FViewUniformShaderParameters反射的。
(提示:当不支持自动实例化时,就会使用cubuffer Primitive,因此每个Drawcall现在的图元会绑定它的UniformBuffer到cbuffer。当自动实例化开启是,就不使用这个cbuffer了,而是使用ViewUniform中的StructuredArray”PrimitiveSceneData” 。)
GPU:生成着色器代码
之前讨论过,cbuffer结构中的着色器代码在任何.ush文件中都没有预先定义,它们是在编译着色器的时候生成的。下面大致看下原理。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//////////////////////
// ShaderCompiler.cpp
//////////////////////
void GlobalBeginCompileShader(…)
{
…
// 设置编译标识,添加任务到队列,编译器稍后会调用该任务
NewJobs.Add(NewJob);
}
//////////////////////////
// ShaderCompileWorker.cpp
//////////////////////////
static void ProcessCompilationJob(…)
{
...
// 调用ShaderFormatD3D.cpp::CompileShader() (D3D)
Compiler->CompileShader(…);
}
////////////////////////
// D3DShaderCompiler.cpp
////////////////////////
void CompileShader_Windows_SM5(…)
{
…
CompileD3DShader(…);
}
void CompileD3DShader(…)
{
…
if (Input.RootParameterBindings.Num())
{
// 生成全局ShaderParameters的cbuffer代码,将字符串放在PreprocessedShaderSource中
MoveShaderParametersToRootConstantBuffer(Input, PreprocessedShaderSource);
}
// 处理字符串:修改代码便于从"cbuffer.memberData"到"cbuffer_memberData"的代码允许cbuffer访问。
RemoveUniformBuffersFromSource(Input.Environment, PreprocessedShaderSource);
…
}
着色器代码是在MoveShaderParametersToRootConstantBuffer()中生成的,然后RemoveUniformBuffersFromSource()会对着色器代码进行调整。[cbuffer_name].[dataMember]会转化成[cbuffer_name]_[dataMember],例如View.LocalToWorld会变成View_LocalToWorld。
1 | // ShaderCompilerCommon.cpp: |
生成的着色器代码
下方的两个文件在vs中是找不到的,因为它们是在编译时自动生成的。但是,可以修改着色器编译器设置来显示debug信息。在ConsoleVariables.ini中添加r.Shaders.Optimize=0和r.Shaders.KeepDebugInfo=1。然后就可以用RenderDoc抓帧查看资源包括着色器代码。
在View.ush和Primitive.ush中,可以看到cbuffer的内容与C++宏声明的着色器参数是一一对应的,而变量的前缀则是结构体的名称。
更新和绑定缓冲
更新缓冲和绑定缓冲是不同的。更新需要获取参数的数据然后更新数据到UniformBuffer;绑定缓冲是告诉着色器我们将要使用哪些缓冲。例如,我们有十个UniformBuffer,它们是不同Views, Primitivies等等的缓冲,它们会在任意的drawcall前更新。但是渲染一个模型时,如果着色器只需要十个中的两个,只需要绑定其中的两个缓冲到着色器。
SceneRendering.cpp
不同的uniform参数可以有自己的更新管线,所以它们可以在不同的地方更新。这里会展示一下更新ViewUniformBufferParameter的位置。
从DeferredShadingRenderer.cpp::Render()可以追踪到InitViews(),最后到SceneRendering.cpp::SetupUniformBufferParameters(),此处设置了FViewUniformBufferParameter的数据。1
2
3
4
5
6
7
8
9/* SceneRendering.cpp */
void FViewInfo::SetupUniformBufferParameters(…)
{
…
SetupCommonViewUniformBufferParameters(…);
…
}/* SceneView.cpp */
void FSceneView::SetupCommonViewUniformBufferParameters()
{…}
ClobalShader.h
这是绑定缓冲的地方,同样地,不同的渲染pass可以在不同的地方绑定缓冲。发送drawcall的时候缓冲绑定几乎已经完成。因为绑定缓冲意味着链接缓冲到着色器,所以必须同时传递shaderRHI和缓冲到函数中。1
2
3
4
5inline void SetParameters(…, ShaderRHI, ViewUniformBuffer)
{
…
SetUniformBufferParameter(…, ShaderRHI, …, ViewUniformBuffer);
}
D3D11StateCachePrivate.h
SetUniformBufferParameter()最终会来到InternalSetSetConstantBuffer()函数,这个函数会调用D3D11 API [Type]SetConstantBuffers到一个顶点/外壳/域/几何/像素/计算着色器。绑定工作在此时完成。1
2
3
4
5
6D3D11_STATE_CACHE_INLINE void InternalSetSetConstantBuffer(…)
{
…
case SF_Vertex: Direct3DDeviceIMContext->VSSetConstantBuffers(…);
break;
…}
添加Uniform控制参数
添加参数
因为要向View添加一个全局uniform,所以在SceneView.h添加一个VIEW_UNIFORM_BUFFER_MEMBER。
更新参数值
可以借用SkyLightComponent和Proxy来存放自定义的uniform,在SetupUniformBufferParameters()中可以把用户编辑过的值传到ViewUniform。
SceneRendering.cpp:
添加一个新的UPROPERTY”MyCustomClobalUniform”到SceneManagement.h
添加相应的组件变量在SkyLightSceneProxy中——“MyCustomGlobalUniform”。在代理的构造函数中,复制SkyLightComponent的值。
SceneManagement.h:
在着色器代码中访问参数
在.ush或.usf文件或材质编辑器的Custom节点中都可以使用View.TestConstantUniform来调用该数值了,并且可以在天光组件的参数面板中修改该数值进行全局控制。
创建材质节点
添加节点类
在引擎的\Source\Runtime\Engine\Classes\Materials 路径下存放了材质编辑器的所有节点源文件。每个节点都有自己的UMaterialExpression类。所以同样在此路径创建一个头文件MaterialExpressionMyGlobalUniform.h。因为此案例只是一个向量数据,可以拷贝DeltaTime的节点。
确保添加完头文件后重新生成sln,这样可以自动生成.generated.h文件,否则会编译失败。
然后在MaterialExpressions.cpp中实现它的构造函数
节点的编译器设置
在MaterialCompiler.h中添加MyCustomUniform()方法。
在HLSLMaterialTranslator.h中,才是MaterialCompiler真正起作用的地方。调用AddInlinedCodeChunk(DATA_TYPE, CODE_STRING)来实现。
测试
保存代码,重新成成sln工程,编译并打开引擎。