距离上一篇博客已经有点久了,中间忙的飞起,忽然发现很久没写了,这样不好,写一篇和工作无关的吧。
一直想搞清UE4距离场的原理,网上有几乎找不到任何有关UE4距离场实现的内容,加上上篇末说要写一个完全的Rendering过程,而UE4下有个距离场的渲染,刚好用来追踪理解UE4距离场,并顺便理下距离场的Rendering相关。
先说下我现在对UE4模型距离场比较浅显的认识,就是我们把场景里的所有不透明模型信息移植到GPU中,不同于我们直接看到的场景,是按照现实中的摆放,在距离场中,声明了一个3D纹理,我们把这个3D纹理可以看做是一个房子,房子里填满了很多个长方体,长方体之间不穿插。而在场景中的每一个不透明模型,对应房子里的一个长方体,注意,他们之间的位置并没关系,可能在场景中模型属于中间,在房子里,可能放在左上角的长方体中。每个长方体里保存的就是当前模型的距离场数据(简单来说,模型内是负数,模型外是正数),这样就把场景里不透明模型的整个信息全部保存到一张3D纹理中,可以说信息量非常集中。而3D纹理占用的显存因为多了一个深度的维度,非常高,所以这个3D纹理默认分辨率并不是特别高,在4.14里只有512*512*1024*f16=512M。
在这,用UE4里的模型距离场来对比一下深度图,我们知道深度图其实只是相当于对应一个特定角度的摄像机所得到的最近的不透明像素的距离,像Shadow mapping这种只需要比较个二个位置下的深度的大小的结果,用来处理很不错,而需要知道模型与场景非特定角度信息,如AO这些深度度则满足不了要求,只有距离场才能满足。
在开始讲UE4模型距离场的渲染前,我们还要来看下,UE4里的Compute Shader,这种类型的着色器比较特殊,不同于常见的顶点与片断着色器,他并不是渲染管线的一部分,一般来说和CUDA/OpenCL类似,用来做GPU通用计算,同样,也要调度GPU划分线程组,线程组划分线程,主要有如下部分要理解。
SV_GroupID是线程块的三维ID,SV_GroupThreadID对应线程块里的线程组ID,SV_DispatchThreadID对应所有线程里的ID,SV_GroupIndex如固定维度的三维数组和一维数组可以相互转化一样,这个指的是在当前线程组中一维索引。如下是引用 https://msdn.microsoft.com/en-us/library/windows/desktop/ff471568(v=vs.85).aspx 里的图来说明。
这里为什么要理解Compute Shader的这些概念了,因为距离场的构成与使用大部分都是Compute Shader,Compute Shader的基本概念也很容易理清,大部分代码和我们平常写的没什么区别,参数,逻辑,同步。
UE4还有一个概念,叫全局距离场,和模型距离场类似,但是其实只能算是模型距离场的衍生物,可以算是对模型场的一种优化使用方式,不要被这个名字骗了,以为他才是主要的,没有模型距离场,就不可能有全局距离场。
接下来,我们按照UE4中的模型距离场可视化渲染流程来说明过程,主要有如下几个部分,如何创建距离场,对应的CPU与GPU的数据有那些。先看下UE里的模型距离可视化渲染是什么样子的图片。
如上面所说,模型距离场是一个装着格子的房子,如何组装这个房子,更新房子的类就是FDistanceFieldVolumeTextureAtlas,对应的对象是GDistanceFieldVolumeTextureAtlas,每个格子对应一个静态模型的FDistanceFieldVolumeTexture,有个很重要的值就是Size,表示这个长方体格子的三维大小。
首先UStaticMesh在加载时,就自动被GDistanceFieldVolumeTextureAtlas收录了。
然后会调用MeshUtilities->GenerateSignedDistanceFieldVolumeData生成对应FStaticMesh的距离场数据,这段代码不贴了,简单说明下。
计算Mesh格子的大小DistanceFieldVolumeBounds,这个对应模型的FDistanceFieldVolumeTexture数据里的3D纹理的各维大小,从代码上来看,比模型的MeshBounds要大一圈,这样网格就包含在FDistanceFieldVolumeTexture之中,并且还要包含边缘的数据。
而后是VolumeDimensions,对应FDistanceFieldVolumeTexture里的3D纹理的各维索引长度,对应各个像素点,各个维度最小8个像素,不然会穿帮。
最后根据索引解析成三角,同Unity类似的Mesh-SubMesh类似,UE4里FStaticMeshLODResources->FStaticMeshSection结构也是FStaticMeshLODResources包含顶点数据buffer,顶点buffer,而FStaticMeshSection对应材质索引,顶点区块,顶点索引起点等。根据顶点索引查找区块对应材质,如果是不透明的,就添加进距离场运算,然后使用K树分割成多维空间,建立搜索索引,然后生成一个上下密度大约在384,平面密度在600左右的点空间,每个上下对应点生成一条射线,这射线与对应模型前面分解的三角形计算得到距离场数据,在物体内为负,表面接近0,物体外一段距离为正值(主要逻辑在FMeshDistanceFieldAsyncTask::DoWork)。
这样各个UStaticMesh的FDistanceFieldVolumeTexture都有值了,size就是上面的VolumeDimensions,LocalBoundingBox就是DistanceFieldVolumeBounds,对应的DistanceFieldVolume初始化VolumeDimensions个零,CompressedDistanceFieldVolume就是上面最后生成的距离场数据。
嗯,终于到渲染这步了,在FDeferredShadingSceneRenderer::Render中,我们可以看到,在prez-pass之前,就会调用GDistanceFieldVolumeTextureAtlas->UpdateAllocations(),这个方法很简单,就是把如上的所有UStaticMesh的FDistanceFieldVolumeTexture数据,提交到对应的GPU中的3D纹理DistanceFieldTexture中。
接着上面马上调用FDeferredShadingSceneRenderer::UpdateGlobalDistanceFieldObjectBuffers,这个也很简单,上面所说的部分,只是提供给GPU一个距离场,而场景中模型与距离场中的格子对应关系并没有,如格子从GPU距离场到世界空间的互相转化的矩阵,对应UVAdd,UVScale,模型的box bounds等信息,这些信息都会存在Scene->DistanceFieldSceneData.ObjectBuffers里,对应的GPU里的ObjectData,ObjectBounds。ObjectData如上所说,每一个节点,包含格子从GPU距离场到世界空间的互相转化的矩阵,对应UVAdd,UVScale,模型的box 等的所有显存信息。
这里有一个Compute Shader就是FUploadObjectsToBufferCS生成的临时数据提交到RWObjectBounds/RWObjectData的,这里就不分析了,很容易理解,每个Compute Shader的类,如上篇文章所说,每个shader,直接定位到相应位置,类后一定有个宏显示他是在那个usf文件里,对应的入口函数是那个。
在接着如下,可以看到针对每个view生成一个全局距离场的3维纹理,限于本文篇幅,只分析模型距离场,全局距离场只是大致提下,在这,每个View生成四个底密度的3维纹理,大小一样,密度不一样,分割成多个Grid,规划每个grid的大小,检测每个view对应的clipmap需要更新的区块,有4.17里逻辑在GlobalDistanceField.usf里的函数CompositeObjectDistanceFieldsCS中,用的数据就是上面的ObjectData,ObjectBounds里的,当然还有一些GPU计算摄像计Cull的过程,在这先不说,因为这个逻辑在下面渲染模型距离场可视化时还用再见到,我们等到那个位置来仔细分析。这里填充了全局距离场的GPU数据,这也是我在上面所说,没有模型距离场,就没有全局距离场的原因,模型距离场的精度比全局距离场也要更高。
在这先介绍一个usf文件,DistanceFieldLightingShared.usf 可以看到很多Load开头的函数,这些函数大都是取ObjectData里的数据,我们知道ObjectData包含了很多信息,如上面所说从GPU距离场到世界空间的互相转化的矩阵,对应UVAdd,UVScale等,这里主要用于单独取这些数据,而对应的我们可以看到还有如CulledObjectData/CulledObjectBounds等加了Culled前缀的GPU信息,这些信息就是通过计算当前摄像机对应ObjectData的Cull通过后的模型,减少计算量。
接上面更新全局距离场后,做了一些延迟渲染应该做的事,如预渲染深度,渲染GBuffer,渲染灯光,渲染透明物体等等后,可以看到RenderMeshDistanceFieldVisualization 渲染模型距离场可视化了,我们来看下如何如何使用模型距离场的一个例子。
在RenderMeshDistanceFieldVisualization函数中,我们开始就调用一个函数,CullObjectsToView(这里版本可能有点变化,我记的4.14是直接在函数里,没有单独拉出来,现在是4.17发现单独拉出来了),这个函数主要是使用GPU来进行摄像机的cull过程,这个过程还是比较有意思的,我们来分析下,shader是FCullObjectsForVolumeCS,usf文件是GlobalDistanceField.usf,入口是CullObjectsForVolumeCS函数,我们先来看下代码。
DispatchComputeShader(RHICmdList, *ComputeShader, FMath::DivideAndRoundUp<uint32>(Scene->DistanceFieldSceneData.NumObjectsInBuffer, UpdateObjectsGroupSize), 1, 1); class FCullObjectsForViewCS : public FGlobalShader { DECLARE_SHADER_TYPE(FCullObjectsForViewCS,Global) public: static bool ShouldCache(EShaderPlatform Platform) { return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5) && DoesPlatformSupportDistanceFieldAO(Platform); } static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Platform,OutEnvironment); OutEnvironment.SetDefine(TEXT("UPDATEOBJECTS_THREADGROUP_SIZE"), UpdateObjectsGroupSize); } FCullObjectsForViewCS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { ObjectBufferParameters.Bind(Initializer.ParameterMap); CulledObjectParameters.Bind(Initializer.ParameterMap); AOParameters.Bind(Initializer.ParameterMap); NumConvexHullPlanes.Bind(Initializer.ParameterMap, TEXT("NumConvexHullPlanes")); ViewFrustumConvexHull.Bind(Initializer.ParameterMap, TEXT("ViewFrustumConvexHull")); ObjectBoundingGeometryIndexCount.Bind(Initializer.ParameterMap, TEXT("ObjectBoundingGeometryIndexCount")); } FCullObjectsForViewCS() { } void SetParameters(FRHICommandList& RHICmdList, const FScene* Scene, const FSceneView& View, const FDistanceFieldAOParameters& Parameters) { FUnorderedAccessViewRHIParamRef OutUAVs[6]; OutUAVs[0] = GAOCulledObjectBuffers.Buffers.ObjectIndirectArguments.UAV; OutUAVs[1] = GAOCulledObjectBuffers.Buffers.Bounds.UAV; OutUAVs[2] = GAOCulledObjectBuffers.Buffers.Data.UAV; OutUAVs[3] = GAOCulledObjectBuffers.Buffers.BoxBounds.UAV; OutUAVs[4] = Scene->DistanceFieldSceneData.ObjectBuffers->Data.UAV; OutUAVs[5] = Scene->DistanceFieldSceneData.ObjectBuffers->Bounds.UAV; RHICmdList.TransitionResources(EResourceTransitionAccess::ERWBarrier, EResourceTransitionPipeline::EComputeToCompute, OutUAVs, ARRAY_COUNT(OutUAVs)); FComputeShaderRHIParamRef ShaderRHI = GetComputeShader(); FGlobalShader::SetParameters<FViewUniformShaderParameters>(RHICmdList, ShaderRHI, View.ViewUniformBuffer); ObjectBufferParameters.Set(RHICmdList, ShaderRHI, *(Scene->DistanceFieldSceneData.ObjectBuffers), Scene->DistanceFieldSceneData.NumObjectsInBuffer); CulledObjectParameters.Set(RHICmdList, ShaderRHI, GAOCulledObjectBuffers.Buffers); AOParameters.Set(RHICmdList, ShaderRHI, Parameters); // Shader assumes max 6 check(View.ViewFrustum.Planes.Num() <= 6); SetShaderValue(RHICmdList, ShaderRHI, NumConvexHullPlanes, View.ViewFrustum.Planes.Num()); SetShaderValueArray(RHICmdList, ShaderRHI, ViewFrustumConvexHull, View.ViewFrustum.Planes.GetData(), View.ViewFrustum.Planes.Num()); SetShaderValue(RHICmdList, ShaderRHI, ObjectBoundingGeometryIndexCount, StencilingGeometry::GLowPolyStencilSphereIndexBuffer.GetIndexCount()); } void UnsetParameters(FRHICommandList& RHICmdList, const FScene* Scene) { ObjectBufferParameters.UnsetParameters(RHICmdList, GetComputeShader(), *(Scene->DistanceFieldSceneData.ObjectBuffers)); CulledObjectParameters.UnsetParameters(RHICmdList, GetComputeShader()); FUnorderedAccessViewRHIParamRef OutUAVs[6]; OutUAVs[0] = GAOCulledObjectBuffers.Buffers.ObjectIndirectArguments.UAV; OutUAVs[1] = GAOCulledObjectBuffers.Buffers.Bounds.UAV; OutUAVs[2] = GAOCulledObjectBuffers.Buffers.Data.UAV; OutUAVs[3] = GAOCulledObjectBuffers.Buffers.BoxBounds.UAV; OutUAVs[4] = Scene->DistanceFieldSceneData.ObjectBuffers->Data.UAV; OutUAVs[5] = Scene->DistanceFieldSceneData.ObjectBuffers->Bounds.UAV; RHICmdList.TransitionResources(EResourceTransitionAccess::ERWBarrier, EResourceTransitionPipeline::EComputeToCompute, OutUAVs, ARRAY_COUNT(OutUAVs)); } virtual bool Serialize(FArchive& Ar) { bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar); Ar << ObjectBufferParameters; Ar << CulledObjectParameters; Ar << AOParameters; Ar << NumConvexHullPlanes; Ar << ViewFrustumConvexHull; Ar << ObjectBoundingGeometryIndexCount; return bShaderHasOutdatedParameters; } private: FDistanceFieldObjectBufferParameters ObjectBufferParameters; FDistanceFieldCulledObjectBufferParameters CulledObjectParameters; FAOParameters AOParameters; FShaderParameter NumConvexHullPlanes; FShaderParameter ViewFrustumConvexHull; FShaderParameter ObjectBoundingGeometryIndexCount; }; IMPLEMENT_SHADER_TYPE(,FCullObjectsForViewCS,TEXT("/Engine/Private/DistanceFieldObjectCulling.usf"),TEXT("CullObjectsForViewCS"),SF_Compute); [numthreads(UPDATEOBJECTS_THREADGROUP_SIZE, 1, 1)] void CullObjectsForViewCS( uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) { uint ObjectIndex = DispatchThreadId.x; #define USE_FRUSTUM_CULLING 1 #if USE_FRUSTUM_CULLING if (DispatchThreadId.x == 0) { // RWObjectIndirectArguments is zeroed by a clear before this shader, only need to set things that are non-zero (and are not read by this shader as that would be a race condition) // IndexCount, NumInstances, StartIndex, BaseVertexIndex, FirstInstance RWObjectIndirectArguments[0] = ObjectBoundingGeometryIndexCount; } if (GroupThreadId.x == 0) { NumGroupObjects = 0; } GroupMemoryBarrierWithGroupSync(); if (ObjectIndex < NumSceneObjects) { uint SourceIndex = ObjectIndex; float4 ObjectBoundingSphere = float4(ObjectBounds[4 * SourceIndex + 0], ObjectBounds[4 * SourceIndex + 1], ObjectBounds[4 * SourceIndex + 2], ObjectBounds[4 * SourceIndex + 3]); float DistanceToViewSq = dot(View.WorldCameraOrigin - ObjectBoundingSphere.xyz, View.WorldCameraOrigin - ObjectBoundingSphere.xyz); if (DistanceToViewSq < Square(AOMaxViewDistance + ObjectBoundingSphere.w) && ViewFrustumIntersectSphere(ObjectBoundingSphere.xyz, ObjectBoundingSphere.w + AOObjectMaxDistance)) { uint DestIndex; InterlockedAdd(NumGroupObjects, 1U, DestIndex); GroupObjectIndices[DestIndex] = SourceIndex; } } GroupMemoryBarrierWithGroupSync(); if (GroupThreadId.x == 0) { InterlockedAdd(RWObjectIndirectArguments[1], NumGroupObjects, GroupBaseIndex); } GroupMemoryBarrierWithGroupSync(); if (GroupThreadId.x < NumGroupObjects) { uint SourceIndex = GroupObjectIndices[GroupThreadId.x]; uint DestIndex = GroupBaseIndex + GroupThreadId.x; CopyCulledObjectData(DestIndex, SourceIndex); } #else if (DispatchThreadId.x == 0) { // IndexCount, NumInstances, StartIndex, BaseVertexIndex, FirstInstance RWObjectIndirectArguments[0] = ObjectBoundingGeometryIndexCount; RWObjectIndirectArguments[1] = NumSceneObjects; } GroupMemoryBarrierWithGroupSync(); if (ObjectIndex < NumSceneObjects) { uint SourceIndex = ObjectIndex; uint DestIndex = ObjectIndex; CopyCulledObjectData(DestIndex, SourceIndex); } #endif }