UE的Mesh关于LOD的切换是用的屏占比ScreenSize,一般是美术设置几个屏占比的参数,本地看着差不多就行了。
下面研究下这个屏占比在引擎里是怎么运作的,方便理解和优化功能。
Q:这个ScreenSize是怎么生效的? A:ScreenSize先转换为距离,后续以距离的形式剔除和选择LOD。
Q:ScreenSize是如何转换成距离的? A:通过下面这个方法,把LOD的各个级别的ScreenSize转换为Distance,保存在LOD信息里。
也就是说,如果屏幕是方的的情况下:
ScreenSize如果是0.5,绘制距离范围就是物体包围球半径的两倍。
ScreenSize如果是1,绘制距离范围就是物体包围球半径。
ScreenSize如果是2,绘制距离范围就是物体包围球半径的一半。
如果屏幕是16:9的的情况下:
ScreenSize如果是0.5,绘制距离范围就是物体包围球半径的3.56倍。
ScreenSize如果是1,绘制距离范围就是物体包围球半径的1.78倍。
ScreenSize如果是2,绘制距离范围就是物体包围球半径的0.89倍。
1 2 3 4 5 6 7 8 9 10 11 12 13 float ComputeBoundsDrawDistance(const float ScreenSize, const float SphereRadius, const FMatrix& ProjMatrix) { // Get projection multiple accounting for view scaling. //获得屏幕宽高比,正常屏幕都是宽>高,结果就是0.5f * ProjMatrix.M[1][1] //16:9就是 0.5f * 1.7778f const float ScreenMultiple = FMath::Max(0.5f * ProjMatrix.M[0][0], 0.5f * ProjMatrix.M[1][1]); // ScreenSize * 0.5f const float ScreenRadius = FMath::Max(SMALL_NUMBER, ScreenSize * 0.5f); //计算距离 return (ScreenMultiple * SphereRadius) / ScreenRadius; }
HISM的LOD选择过程 在FHierarchicalStaticMeshSceneProxy::GetDynamicMeshElements里,渲染线程里用来收集当前HISM的MeshElement。 里面通过Cluster结构进行HISM的 视锥剔除、遮挡剔除、距离剔除、LOD选择。
核心方法是在FHierarchicalStaticMeshSceneProxy::Traverse()里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FHierarchicalStaticMeshSceneProxy::Traverse() { //视锥剔除ViewFrustum Cull //距离剔除(这里距离剔除是用LOD组的最远一级的距离进行剔除) //Cluster的包围盒中心到摄像机的距离 与每一级LOD的距离进行对比,得到最小LOD和最大LOD。 CalcLOD(MinLOD, MaxLOD, BoundMin, BoundMax, ViewOriginInLocalZero, ViewOriginInLocalOne, LODPlanesMin, LODPlaneMax); //遮挡剔除 //是否可以把当前ClusterNode直接用一个LOD级别渲染 //如果一个LODGroup就会return了,如果不能一个,继续往下面级别的ClusterNode遍历 //只要不被剔除掉,这里就会设置当前Instance使用什么LOD级别渲染 //因为涉及到LODDither切换的情况,这里会传进去两个LOD级别 Params.AddRun(MinLOD, MaxLOD, Node.FirstInstance, Node.LastInstance); //继续往下递归Traverse() }
DitherLOD Transition 当材质开启DitherLODTransition开关之后,会有渐变的lod切换。
在ShaderBind过程中,Alpha是获取一个LODTransition值,这个值是通过一个公式计算:
(真实上一帧时间 - DelayTime - 保存的上上帧时间) / (保存的上一帧时间 - 保存的上上帧时间)
1 2 3 4 5 6 7 8 float GetTemporalLODTransition(float LastRenderTime) const { if (TemporalLODLag == 0.0) { return 0.0f; // no fade } return FMath::Clamp((LastRenderTime - TemporalLODLag - TemporalLODTime[0]) / (TemporalLODTime[1] - TemporalLODTime[0]), 0.0f, 1.0f); }
DelayTime就是切换的时间,在引擎的另一个地方,当帧间隔大于DelayTime,才会去保存TemporalLODTime。
LODTransition的值就会慢慢变大,到DelayTime的时候就到达1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (!View.bDisableDistanceBasedFadeTransitions) { bOk = true; TemporalLODLag = CVarLODTemporalLag.GetValueOnRenderThread(); if (TemporalLODTime[1] < LastRenderTime - TemporalLODLag) { if (TemporalLODTime[0] < TemporalLODTime[1]) { TemporalLODViewOrigin[0] = TemporalLODViewOrigin[1]; TemporalLODTime[0] = TemporalLODTime[1]; } TemporalLODViewOrigin[1] = View.ViewMatrices.GetViewOrigin(); TemporalLODTime[1] = LastRenderTime; if (TemporalLODTime[1] <= TemporalLODTime[0]) { bOk = false; // we are paused or something or otherwise didn't get a good sample } } }
把两个AlphaCutOff的值传给Shader:
InstancingWorldViewOriginOne.W,
InstancingWorldViewOriginZero.W
然后还有一个InstancingViewZCompareZero和InstancingViewZCompareOne,通过两次循环分别设置两层(或者两帧)的对比距离。
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 for (int32 SampleIndex = 0; SampleIndex < 2; SampleIndex++) { FVector4& InstancingViewZCompare(SampleIndex ? InstancingViewZCompareOne : InstancingViewZCompareZero); float FinalCull = MAX_flt; if (MinSize > 0.0) { FinalCull = ComputeBoundsDrawDistance(MinSize, SphereRadius, View->ViewMatrices.GetProjectionMatrix()) * LODScale; } if (InstancingUserData->EndCullDistance > 0.0f) { FinalCull = FMath::Min(FinalCull, InstancingUserData->EndCullDistance * MaxDrawDistanceScale); } FinalCull *= MaxDrawDistanceScale; InstancingViewZCompare.Z = FinalCull; if (int(BatchElement.InstancedLODIndex) < InstancingUserData->MeshRenderData->LODResources.Num() - 1) { float NextCut = ComputeBoundsDrawDistance(InstancingUserData->MeshRenderData->ScreenSize[BatchElement.InstancedLODIndex + 1].GetValue(), SphereRadius, View->ViewMatrices.GetProjectionMatrix()) * LODScale; InstancingViewZCompare.Z = FMath::Min(NextCut, FinalCull); } InstancingViewZCompare.X = MIN_flt; if (int(BatchElement.InstancedLODIndex) > FirstLOD) { float CurCut = ComputeBoundsDrawDistance(InstancingUserData->MeshRenderData->ScreenSize[BatchElement.InstancedLODIndex].GetValue(), SphereRadius, View->ViewMatrices.GetProjectionMatrix()) * LODScale; if (CurCut < FinalCull) { InstancingViewZCompare.Y = CurCut; } else { // this LOD is completely removed by one of the other two factors InstancingViewZCompare.Y = MIN_flt; InstancingViewZCompare.Z = MIN_flt; } } else { // this is the first LOD, so we don't have a fade-in region InstancingViewZCompare.Y = MIN_flt; } } InstancingOffset = InstancingUserData->InstancingOffset; InstancingWorldViewOriginZero = View->GetTemporalLODOrigin(0); InstancingWorldViewOriginOne = View->GetTemporalLODOrigin(1); float Alpha = View->GetTemporalLODTransition(); InstancingWorldViewOriginZero.W = 1.0f - Alpha; InstancingWorldViewOriginOne.W = Alpha; InstancingViewZCompareZero.W = LODRandom; }
在Shader里:
Intermediates.PerInstanceParams.w会在后续PS里作为CutOff值进行像素丢弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Intermediates.PerInstanceParams.x = GetInstanceRandom(Intermediates); float3 InstanceLocation = TransformLocalToWorld(GetInstanceOrigin(Intermediates), Intermediates.PrimitiveId).xyz; Intermediates.PerInstanceParams.y = 1.0 - saturate((length(InstanceLocation + ResolvedView.PreViewTranslation.xyz) - InstancingFadeOutParams.x) * InstancingFadeOutParams.y); // InstancingFadeOutParams.z,w are RenderSelected and RenderDeselected respectively. Intermediates.PerInstanceParams.z = InstancingFadeOutParams.z * SelectedValue + InstancingFadeOutParams.w * (1-SelectedValue); #if USE_DITHERED_LOD_TRANSITION float RandomLOD = InstancingViewZCompareZero.w * Intermediates.PerInstanceParams.x; float ViewZZero = length(InstanceLocation - InstancingWorldViewOriginZero.xyz) + RandomLOD; float ViewZOne = length(InstanceLocation - InstancingWorldViewOriginOne.xyz) + RandomLOD; Intermediates.PerInstanceParams.w = dot(float3(ViewZZero.xxx > InstancingViewZCompareZero.xyz), InstancingViewZConstant.xyz) * InstancingWorldViewOriginZero.w + dot(float3(ViewZOne.xxx > InstancingViewZCompareOne.xyz), InstancingViewZConstant.xyz) * InstancingWorldViewOriginOne.w; Intermediates.PerInstanceParams.z *= abs(Intermediates.PerInstanceParams.w) < .999; #else Intermediates.PerInstanceParams.w = 0; #endif