目标
我注意到材质编辑器中有两个节点从名字上看似乎不好分辨:
本篇的目标是:
- 实验比较材质节点中“ActorPosition”和“ObjectPosition”的区别
- 作为知识上的扩展,我想观察其值是如何从代码上得到的。
实验
为了更有目的性地实验,我在实验前查阅了官方文档《坐标表达式 | Unreal Engine Documentation》,得到对二者如下的描述:
- “ActorPosition” 输出矢量3(RGB)数据,该数据表示对象在世界场景空间中的位置以及其上的材质。
- “ObjectPosition” 表达式输出对象边界的世界场景空间中心位置。
另外,测试时我还加上了“AbsoluteWorldPosition”节点:
- “AbsoluteWorldPosition”表达式输出当前像素在全局空间中的位置。
当然,文字描述毕竟是抽象的,进行实验才能有具体的认识。
下面使用地形进行实验,因为他的Actor位置和bound中心的位置并不一样。
1. Absolute World Position
2. ActorPosition
可以看到,它是整体变化的,并且是以Actor坐标为基准。
3. Object Position
可以看到,它也是整体变化的,不过区别是它是以整体的中心为基准。
4. Object Position(测试多个地形组件)
对于地形,当地形有多个组件时:
可以看到每个地形组件是独立变化的。这或许暗示着“Object Position”的值计算的单位。
观察
关于材质节点对应的C++类UMaterialExpression
,在之前的博客《用C++扩展一个UE4材质节点》中有讨论。
“ActorPosition”和“ObjectPosition”看起来有很多相似性,这里选择“ActorPosition”进行观察。
1. 观察对应的 UMaterialExpression
“ActorPosition”节点的类是UMaterialExpressionActorPositionWS
。
(我注意到在上一部分测试的三个节点都是红色且带有 “Input Data”字样 ,我观察到这是bShaderInputData
成员决定的。)
在Compile
函数中可以看到:
int32 UMaterialExpressionActorPositionWS::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
if (Material != nullptr && (Material->MaterialDomain != MD_Surface) && (Material->MaterialDomain != MD_DeferredDecal) && (Material->MaterialDomain != MD_RuntimeVirtualTexture) && (Material->MaterialDomain != MD_Volume))
{
return CompilerError(Compiler, TEXT("Expression only available in the Surface and Deferred Decal material domains."));
}
return Compiler->ActorWorldPosition();
}
它直接使用了FMaterialCompiler* Compiler
的ActorWorldPosition
方法。
2. FHLSLMaterialTranslator::ActorWorldPosition
virtual int32 ActorWorldPosition() override
{
if (bCompilingPreviousFrame && ShaderFrequency == SF_Vertex)
{
// Decal VS doesn't have material code so FMaterialVertexParameters
// and primitve uniform buffer are guaranteed to exist if ActorPosition
// material node is used in VS
return AddInlinedCodeChunk(
MCT_Float3,
TEXT("mul(mul(float4(GetActorWorldPosition(Parameters.PrimitiveId), 1), GetPrimitiveData(Parameters.PrimitiveId).WorldToLocal), Parameters.PrevFrameLocalToWorld)"));
}
else
{
return AddInlinedCodeChunk(MCT_Float3, TEXT("GetActorWorldPosition(Parameters.PrimitiveId)"));
}
}
可以看到,有一个分支,但最终都是使用AddInlinedCodeChunk
来直接向shader代码中加入字符串。根据注释所解释,上面的分支应该是属于少数的情况,在大部分情况下应该走下面的分支,即加入:
GetActorWorldPosition(Parameters.PrimitiveId)
3. MaterialTemplate.ush
在\Engine\Shaders\
路径下搜索GetActorWorldPosition可以在\Engine\Shaders\Private\MaterialTemplate.ush
中找到这个函数的定义:
#if DECAL_PRIMITIVE
/*
* Deferred decal don't have a Primitive uniform buffer, because we don't know on which primitive the decal
* is being projected to. But the user may still need to get the decal's actor world position.
* So instead of setting up a primitive buffer that may cost to much CPU effort to be almost never used,
* we directly fetch this value from the DeferredDecal.usf specific uniform variable DecalToWorld.
*/
float3 GetActorWorldPosition(uint PrimitiveId)
{
return DecalToWorld[3].xyz;
}
#else
float3 GetActorWorldPosition(uint PrimitiveId)
{
return GetPrimitiveData(PrimitiveId).ActorWorldPosition;
}
类似上述可以看到一个分支,同样根据注释所解释,上面的分支应该是属于少数的情况,在大部分情况下应该走下面的分支,即:
GetPrimitiveData(PrimitiveId).ActorWorldPosition
此外,在\Engine\Source\Runtime\RenderCore\Private\ShaderCore.cpp
可以看到,MaterialTemplate.ush
在编译shader文件的时候是会自动加入的:
// Always add ShaderVersion.ush, so if shader forgets to include it, it will still won't break DDC.
AddShaderSourceFileEntry(OutVirtualFilePaths, FString(TEXT("/Engine/Public/ShaderVersion.ush")), ShaderPlatform);
AddShaderSourceFileEntry(OutVirtualFilePaths, FString(TEXT("/Engine/Private/MaterialTemplate.ush")), ShaderPlatform);
AddShaderSourceFileEntry(OutVirtualFilePaths, FString(TEXT("/Engine/Private/Common.ush")), ShaderPlatform);
AddShaderSourceFileEntry(OutVirtualFilePaths, FString(TEXT("/Engine/Private/Definitions.usf")), ShaderPlatform);
4. shader中的FPrimitiveSceneData
在SceneData.ush
可以看到GetPrimitiveData的定义:
// Fetch from scene primitive buffer
FPrimitiveSceneData GetPrimitiveData(uint PrimitiveId)
{
// Note: layout must match FPrimitiveSceneShaderData in C++
...
它返回一个FPrimitiveSceneData
类型的结构体
// Must match FPrimitiveUniformShaderParameters in C++
struct FPrimitiveSceneData
{
float4x4 LocalToWorld;
float4 InvNonUniformScaleAndDeterminantSign;
float4 ObjectWorldPositionAndRadius;
float4x4 WorldToLocal;
float4x4 PreviousLocalToWorld;
float4x4 PreviousWorldToLocal;
float3 ActorWorldPosition;
float UseSingleSampleShadowFromStationaryLights;
float3 ObjectBounds;
float LpvBiasMultiplier;
float DecalReceiverMask;
float PerObjectGBufferData;
float UseVolumetricLightmapShadowFromStationaryLights;
float DrawsVelocity;
float4 ObjectOrientation;
float4 NonUniformScale;
float3 LocalObjectBoundsMin;
uint LightingChannelMask;
float3 LocalObjectBoundsMax;
uint LightmapDataIndex;
float3 PreSkinnedLocalBoundsMin;
int SingleCaptureIndex;
float3 PreSkinnedLocalBoundsMax;
uint OutputVelocity;
float4 CustomPrimitiveData[NUM_CUSTOM_PRIMITIVE_DATA];
};
正如其提到的它需要和C++中的FPrimitiveUniformShaderParameters
相匹配。看来,它是一个UniformBuffer,对应于C++中这个结构体。
5. C++中的FPrimitiveUniformShaderParameters
在\Engine\Source\Runtime\Engine\Public\PrimitiveUniformShaderParameters.h
中可以看到FPrimitiveUniformShaderParameters
的定义:
而在这个定义下面,有个GetPrimitiveUniformShaderParameters
函数,返回一个FPrimitiveUniformShaderParameters
对象,加上其函数名字的描述,我相信这就是设定这个UniformBuffer数据的地方,其中可以发现我现在感兴趣的“ActorPosition”和“ObjectPosition”。
在此断点并观察堆栈就可知道这些值是如何设置的。
6. 观察值是如何设置的
我创建一个空场景来观察,并且隐藏了网格:
然后观察到堆栈:
最终结论
“ActorPosition”和“ObjectPosition”都是UniformBuffer中的数据,不过区别是:
- “ActorPosition”是Actor的位置。具体来说,是调用
FPrimitiveSceneProxy
的GetActorPosition()
。 - “ObjectPosition”是bound的中心。具体来说,是调用
FPrimitiveSceneProxy
的GetBounds()
来获得一个FBoxSphereBounds
对象,中心即为它的Origin
。