ue4模板类实例化_UE4ISM(实例化静态网格体)理解

ue4模板类实例化_UE4ISM(实例化静态⽹格体)理解
知乎⽂章可能格式有错乱,如果发现格式问题,请看原⽂吧。
环境: Unreal Engine 4.25.2
此⽂介绍了ISM即UInstancedStaticMeshComponent的使⽤,分析了编辑和渲染代码。A component that efficiently renders multiple instances of the same StaticMesh.
TL;DRISM的所有实例⼀起,⽆论如何都只使⽤1个drawcall
ISM的LOD是以所有实例的包围盒计算的,统⼀变化
ISM没有⾃⼰的剔除逻辑
ISM在UE4.25上不会被发布(Shipping)版本渲染,因为有bug,4.26修复了
ISM的逐实例变换在世界坐标变换后被应⽤
使⽤创建⼀个Actor,附加UInstancedStaticMeshComponent组件,并给ISM组件赋值StaticMesh
在ISM的Detail窗⼝中⼿动添加实例并指定其Transform空场景⽤蓝图随机⽣成了1000个A类型⽯头的实例和1000个B类型⽯头的实例
可以看到Mesh draw calls数量只是从4变到了6,只增加了2
编辑时
编辑器的instance数据保存在UInstancedStaticMeshComponent::PerInstanceSMData,类型为TArray;也⽀持附加逐实例的⾃定义数据,存储在UInstancedStaticMeshComponent::PerInstanceSMCustomData,类型为TArray,它们数据可以被蓝图和材质读取。
在ISM的Detail窗⼝中进⾏编辑操作时,会触发PostEditChangeChainProperty对PerInstanceSMData和PerInstanceSMCustomData 进⾏相应的修改:增加实例:UInstancedStaticMeshComponent::AddInstanceInternal
删除实例: UInstancedStaticMeshComponent::RemoveInstanceInternal
清除实例:UInstancedStaticMeshComponent::ClearInstances
移动实例,即修改FInstancedStaticMeshInstanceData::Transform:修改UInstancedStaticMeshComponent::NumEdits
增加、修改、删除⾃定义实例数据(略)
以上过程基本都⽤下⾯的代码收尾:
// Force recreation of the render dataInstanceUpdateCmdBuffer.Edit();
MarkRenderStateDirty();
这会导致⽴刻重新CreateSceneProxy⽣成渲染数据并以当前的Transform绘制所有实例,可以在UInstancedStaticMeshComponent::CreateSceneProxy开头⾥打个断点看看此时的堆栈:
UInstancedStaticMeshComponent::CreateSceneProxy() InstancedStaticMesh.cpp:1473
FScene::AddPrimitive(UPrimitiveComponent *) RendererScene.cpp:1372
UPrimitiveComponent::CreateRenderState_Concurrent(FRegisterComponentContext *) PrimitiveComponent.cpp:646
UStaticMeshComponent::CreateRenderState_Concurrent(FRegisterComponentContext *) StaticMeshComponent.cpp:620
UActorComponent::ExecuteRegisterEvents(FRegisterComponentContext *) ActorComponent.cpp:1561
...
UInstancedStaticMeshComponent::PostEditChangeChainProperty(FPropertyChangedChainEvent &)
InstancedStaticMesh.cpp:3085
在蓝图的Detail窗⼝中修改ISM组件上的instance数据时,会调⽤UInstancedStaticMeshComponent::ApplyComponentInstanceData 更新其PerInstanceSMData:
UInstancedStaticMeshComponent::ApplyComponentInstanceData(FInstancedStaticMeshComponentInstanceData * InstancedMeshData) Line 1413 C++
FInstancedStaticMeshComponentInstanceData::ApplyToComponent
FComponentInstanceDataCache::ApplyToActor(AActor * Actor, const ECacheApplyPhase CacheApplyPhase) Line 596 C++
同样最后也会引起重新CreateSceneProxy⽣成渲染数据再重绘。
在Detail窗⼝中,也有调整剔除相关的参数,但后⾯可以看到,这些参数只在开启了dithered lod transition才对渲染结果有影响,产⽣渐隐效果。
/** 从这个距离开始,实例开始渐隐 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Culling)
int32 InstanceStartCullDistance;
/** 从这个距离开始,实例完全被剔除 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Culling)
int32 InstanceEndCullDistance;
在CreateSceneProxy, SerializeRenderData等等需要使⽤instance数据时,调⽤
UInstancedStaticMeshComponent::BuildRenderData将PerInstanceSMData转换为FStaticMeshInstanceData.
⽬前UE4不⽀持直接在3D场景中移动单个实例,虽然发现有相关的Customization代码(FInstancedStaticMeshSCSEditorCustomization),但对直接添加的ISM组件不起作⽤,似乎是在蓝图编辑器⾥编辑时⽤的,但也没能尝试出来是如何使⽤的。⽬前必须通过Detail窗⼝中调整实例的Transform来移动(或通过蓝图、C++去修改单个instance的Transform属性)。但查到有个叫instance tool插件称⾃⼰可以做到在3D场景中编辑单个实例。
运⾏时
创建SceneProxy
UInstancedStaticMeshComponent对应的Proxy类型为FInstancedStaticMeshSceneProxy.
在UInstancedStaticMeshComponent::CreateSceneProxy()时,复制实例的渲染数据PerInstanceRenderData到FStaticMeshSceneProxy::InstancedRenderData
保存StaticMesh的LOD数据到FStaticMeshSceneProxy::LODModels
设置每个实例的静态LightMap、ShadowMap偏移
SetupProxy(InComponent) (稍后分析)
实际设置实例的核⼼⽅法是各类SetInstance和SetInstanceXXX⽅法,详细的看下这些⽅法的代码很容易看懂。
但其中有个点可能要特别说明⼀下:
void UInstancedStaticMeshComponent::BuildRenderData(/*...*/)
{
FRandomStream RandomStream = FRandomStream(InstancingRandomSeed);
//.. OutData.SetInstance(RenderIndex, InstanceData.Transform, RandomStream.GetFraction(), LightmapUVBias, ShadowmapUVBias);
//..}
void SetInstance(int32 InstanceIndex, const FMatrix& Transform, float RandomInstanceID, const FVector2D& LightmapUVBias, const FVector2D& ShadowmapUVBias)
{
FVector4 Origin(Transform.M[3][0], Transform.M[3][1], Transform.M[3][2], RandomInstanceID);
SetInstanceOriginInternal(InstanceIndex, Origin);
//...}
上⾯的RandomInstanceID是个随机数,它被存储到了Origin的FVector4域的w上,来⾃从
UInstancedStaticMeshComponent::InstancingRandomSeed⽣成的随机序列,它在⼀些使⽤了ISM和其⼦类HISM的逻辑中⽤于随机⽣成instance,未来在分析LandscapeGrass的代码时就可以看到其应⽤了。
接着在FInstancedStaticMeshSceneProxy的构造函数内,调⽤UInstancedStaticMeshComponent::SetupProxy(InComponent)。
⾸先,检查StaticMesh的所有LOD对应的材质是否可以⽤于实例化渲染(EMaterialUsage::MATUSAGE_InstancedStaticMeshes)。 然后,复制LOD参数到FInstancedStaticMeshSceneProxy::UserData_AllInstances,类型是FInstancingUserData,⾥⾯存储着渲染时需要的数据:
struct FInstancingUserData
{
class FInstancedStaticMeshRenderData* RenderData;//实例的渲染数据(主要是InstanceBuffer),SetupProxy⾥设置为nullptr class FStaticMeshRenderData* MeshRenderData;//Static Mesh的渲染数据
int32 StartCullDistance;//渐隐距离 int32 EndCullDistance;//剔除距离
int32 MinLOD;//最⼩LOD
bool bRenderSelected;
bool bRenderUnselected;
FVector AverageInstancesScale;//Instance的平均缩放变换 FVector InstancingOffset;//Static Mesh的包围盒中⼼};
Proxy⾥的UserData_AllInstances之后会被MeshBatch引⽤,最终传输到。
(并没有)剔除
先说结论,默认情况下UE并没有对实例进⾏剔除。这⾥不考虑启⽤dithered lod transition的情况,后⾯可能会专门写⼀篇来分析dithered lod transition的实现。
在获取ISM Shader binding数据时,即FInstancedStaticMeshVertexFactoryShaderParameters::GetElementShaderBindings中,
⾸先,根据MeshBatch中的实例数据和View,计算并添加以下ShaderParameter的Binding。这⾥因为它们都是给dithered lod transition⽤的,相关的代码分析略。
LocalVertexFactory.ush
#if USE_DITHERED_LOD_TRANSITION
float4 InstancingViewZCompareZero;
float4 InstancingViewZCompareOne;
float4 InstancingViewZConstant;
float4 InstancingWorldViewOriginZero;
float4 InstancingWorldViewOriginOne;
#endif
ShaderBindings.Add(InstancingViewZCompareZeroParameter, InstancingViewZCompareZero);
ShaderBindings.Add(InstancingViewZCompareOneParameter, InstancingViewZCompareOne); ShaderBindings.Add(InstancingViewZConstantParameter, InstancingViewZConstant); ShaderBindings.Add(InstancingOffsetParameter, InstancingOffset);
ousiaShaderBindings.Add(InstancingWorldViewOriginZeroParameter, InstancingWorldViewOriginZero); ShaderBindings.Add(InstancingWorldViewOriginOneParameter, InstancingWorldViewOriginOne);
然后,从MeshBatch中拿到之前设置的StartCullDistance和EndCullDistance,绑定到Shader参数InstancingWorldViewOriginOneParameter和InstancingFadeOutParamsParameter中:
const float MaxDrawDistanceScale = GetCachedScalabilityCVars().ViewDistanceScale;
const float StartDistance = InstancingUserData->StartCullDistance * MaxDrawDistanceScale; const float EndDistance = InstancingUserData->EndCullDistance * MaxDrawDistanceScale; InstancingFadeOutParams.X = StartDistance;
if( EndDistance > 0 )
{
if( EndDistance > StartDistance )
巨大奇迹
{
InstancingFadeOutParams.Y = 1.f / (float)(EndDistance - StartDistance);
}
else
{
InstancingFadeOutParams.Y = 1.f;
}
}
else
{
InstancingFadeOutParams.Y = 0.f;
}
//Debug⽤的console variable,⽤来在vertex shader中强制剔除所有instanceif (CVarCullAllInVertexShader.GetValueOnRenderThread() > 0)
{
InstancingFadeOutParams.Z = 0.0f;
一位全减器
InstancingFadeOutParams.W = 0.0f;
}
else
InstancingFadeOutParams.Z = InstancingUserData->bRenderSelected ? 1.f : 0.f;
InstancingFadeOutParams.W = InstancingUserData->bRenderUnselected ? 1.f : 0.f;
}
可以看到中保存了渐隐和剔除信息,在看下vertex shader中是如何渐隐和剔除instance的。
float4 InstancingFadeOutParams;
float GetPerInstanceFadeAmount(FMaterialPixelParameters Parameters)
{范安翔
return float(Parameters.PerInstanceParams.y);
}
FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
Intermediates.PerInstanceParams.y = 1.0 - saturate((length(InstanceLocation + ) -InstancingFadeOutParams.x) * InstancingFadeOutParams.y);
}
//然⽽PerInstanceParams.y并没有被⼊⼝点函数Main使⽤,不论vs还是ps都没有
虽然vertex shader中有对这些数据进⾏计算,但却没有体现到vertex shader、pixel shader的输出上,从抓帧的结果来看也是这样,所以说移动端并没有对ISM的实例进⾏剔除。相关代码基本没有SM5、ES3_1的分⽀,所以很可能PC端的实现也是这样的。
粗糙的LOD计算
先说结论,所有instance统⼀切换使⽤的StaticMesh的LOD,当所有instance的包围盒的并集在屏幕上的投影⼤⼩达到StaticMesh的某LOD的切换⼤⼩时切换。实际上如果实例分散得⽐较开,它们得LOD就会始终保持在LOD0,因为分散的实例整体很难达到切换LOD1的⼤⼩。所有相同StaticMesh的实例的渲染只⽤⼀个drawcall.
LOD相关的逻辑在UInstancedStaticMeshComponent::GetDynamicMeshElements中。
⾸先提⼀下ISM计算Proxy包围盒的⽅法,计算LOD时会⽤到。
FBoxSphereBounds UInstancedStaticMeshComponent::CalcBounds(const FTransform& BoundTransform) const
{
涪江//检查Static Mesh和instance数量 if(GetStaticMesh() && PerInstanceSMData.Num() > 0)
{
//应⽤函数参数的变换 FMatrix BoundTransformMatrix = BoundTransform.ToMatrixWithScale();
FBoxSphereBounds RenderBounds = GetStaticMesh()->GetBounds();
FBoxSphereBounds NewBounds = RenderBounds.TransformBy(PerInstanceSMData[0].Transform * BoundTransformMatrix);
//应⽤每个instance的变换,求所有instance的包围盒的并集 for (int32 InstanceIndex = 1; InstanceInd
ex <
PerInstanceSMData.Num(); InstanceIndex++)
桃花心木教学实录{
NewBounds = NewBounds + RenderBounds.TransformBy(PerInstanceSMData[InstanceIndex].Transform * BoundTransformMatrix);

本文发布于:2024-09-25 12:31:41,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/212519.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:实例   数据   剔除   渲染   没有   代码   相关   可能
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议