UE4 距离场简单分析
距离上一篇博客已经有点久了,中间忙的飞起,忽然发现很久没写了,这样不好,写一篇和工作无关的吧。
一直想搞清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 }
这段代码分三部分,开始是调用,二是Shader一般写法,如bind对应GPU参数,三是Compute Shader本身逻辑。
第一个部分,我们需要注意DispatchComputeShader这个函数,这个函数的第三之后的三个参数,代表GPU调度的线程组,也就是如最上图的Dispatch(x/64,1,1),对应的SV_GroupID指向的就是Dispatch相应的三维空间,这里的n,1,1表示把三维数组转化成一维数组了。
第二部分,大家可以看到UE4里Shader的写法都是如此,在构造函数,绑定GPU与CPU的参数,在SetParameters方法里设置对应参数的值,如在这里,上面对应ObjectData,CullObjectData都是在这传入的,在这主要是摄像机的参数与原来的ObjectData的绑定,输出的Cull数据绑定,其中FRWShaderParameter参数会分别绑定UVA与SRV资源,如上一个只读的ObjectData,还有一个可读写的RwObjectData.
第三部分可以看到每个Compute Shader的入口函数都有一个numthreads,其对应参数一般在相应的Shader代码的ModifyCompilationEnvironment里指定,numthreads对应的每个线程组如何分线程,这里是(64,1,1),简单来说,线程里的线程也被分成一维数组,这样达到一个啥效果了,简单来看SV_DispatchThreadID.x就是所有线程相应的索引,然后我们来看代码。
前面objectIndex是对应DispatchThreadId.x这个好理解,前面调度就是objectNum/64,线程组是64个,简单来说,每个线程组里有64个模型,而DispatchThreadId表示整个线程中的索引。
在第一个GroupMemoryBarrierWithGroupSync之前,初始化RWObjectIndirectArguments与NumGroupObjects(每个线程组)。
第二个GroupMemoryBarrierWithGroupSync之前,每个线程组里记录当前在摄像机Cull范围下的DestIndex,而DestIndex通过同步方法InterlockedAdd得到的NumGroupObjects的索引。
在第三个GroupMemoryBarrierWithGroupSync之前,把所有的线程组里的Cull之和添加到RWObjectIndirectArguments[1]中。
在第四个GroupMemoryBarrierWithGroupSync之前,每个线程组里把对应的索引取的ObjectData/bounds放入CullObjectData/bounds里,GPU的相应Cull过程就完了。
关于GPU的摄像机CULL过程就到这了,接着上面,我们在RTT池里找一个PF_FloatRGBA,UAV的2维RTT叫VisualizeDistanceField,然后通过TVisualizeMeshDistanceFieldCS这个Compute Shader把CullObjectData/bounds的数据渲染到当前摄像机的平面VisualizeDistanceField上,我们可以看到TVisualizeMeshDistanceFieldCS这个使用个泛型化的bool参数,这样可以去掉一次bool判断,但是多生成一份代码。
TVisualizeMeshDistanceFieldCS如前面所说,可以看到对应的是DistanceFieldVisualization文件,入口是VisualizeMeshDistanceFieldCS,调度是(viewsize.xy/32,1),线程组是(16,16,1).对应VisualizeMeshDistanceFieldCS里逻辑主要如下。
[numthreads(THREADGROUP_SIZEX, THREADGROUP_SIZEY, 1)] void VisualizeMeshDistanceFieldCS( uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID) { uint ThreadIndex = GroupThreadId.y * THREADGROUP_SIZEX + GroupThreadId.x; float2 ScreenUV = float2((DispatchThreadId.xy * DOWNSAMPLE_FACTOR + View.ViewRectMin.xy + .5f) * View.BufferSizeAndInvSize.zw); float2 ScreenPosition = (ScreenUV.xy - View.ScreenPositionScaleBias.wz) / View.ScreenPositionScaleBias.xy; float SceneDepth = CalcSceneDepth(ScreenUV); float4 HomogeneousWorldPosition = mul(float4(ScreenPosition * SceneDepth, SceneDepth, 1), View.ScreenToWorld); float3 OpaqueWorldPosition = HomogeneousWorldPosition.xyz / HomogeneousWorldPosition.w; float TraceDistance = 40000; float3 WorldRayStart = View.WorldCameraOrigin; float3 WorldRayEnd = WorldRayStart + normalize(OpaqueWorldPosition - View.WorldCameraOrigin) * TraceDistance; float3 WorldRayDirection = WorldRayEnd - WorldRayStart; float3 UnitWorldRayDirection = normalize(WorldRayDirection); #if USE_GLOBAL_DISTANCE_FIELD float TotalStepsTaken = 0; float MaxRayTime0; float IntersectRayTime; float StepsTaken; RayTraceThroughGlobalDistanceField((uint)0, WorldRayStart, WorldRayEnd, TraceDistance, 0, MaxRayTime0, IntersectRayTime, StepsTaken); TotalStepsTaken += StepsTaken; if (IntersectRayTime >= TraceDistance) { float MaxRayTime1; RayTraceThroughGlobalDistanceField((uint)1, WorldRayStart, WorldRayEnd, TraceDistance, MaxRayTime0, MaxRayTime1, IntersectRayTime, StepsTaken); TotalStepsTaken += StepsTaken; if (IntersectRayTime >= TraceDistance) { float MaxRayTime2; RayTraceThroughGlobalDistanceField((uint)2, WorldRayStart, WorldRayEnd, TraceDistance, MaxRayTime1, MaxRayTime2, IntersectRayTime, StepsTaken); TotalStepsTaken += StepsTaken; if (IntersectRayTime >= TraceDistance) { float MaxRayTime3; RayTraceThroughGlobalDistanceField((uint)3, WorldRayStart, WorldRayEnd, TraceDistance, MaxRayTime2, MaxRayTime3, IntersectRayTime, StepsTaken); TotalStepsTaken += StepsTaken; } } } float3 Result = saturate(TotalStepsTaken / 400.0f); #else if (ThreadIndex == 0) { NumIntersectingObjects = 0; } GroupMemoryBarrierWithGroupSync(); float3 TileConeVertex; float3 TileConeAxis; float TileConeAngleCos; float TileConeAngleSin; { float2 ViewSize = float2(1 / View.ViewToClip[0][0], 1 / View.ViewToClip[1][1]); float3 TileCorner00 = normalize(float3((GroupId.x + 0) / NumGroups.x * ViewSize.x * 2 - ViewSize.x, ViewSize.y - (GroupId.y + 0) / NumGroups.y * ViewSize.y * 2, 1)); float3 TileCorner10 = normalize(float3((GroupId.x + 1) / NumGroups.x * ViewSize.x * 2 - ViewSize.x, ViewSize.y - (GroupId.y + 0) / NumGroups.y * ViewSize.y * 2, 1)); float3 TileCorner01 = normalize(float3((GroupId.x + 0) / NumGroups.x * ViewSize.x * 2 - ViewSize.x, ViewSize.y - (GroupId.y + 1) / NumGroups.y * ViewSize.y * 2, 1)); float3 TileCorner11 = normalize(float3((GroupId.x + 1) / NumGroups.x * ViewSize.x * 2 - ViewSize.x, ViewSize.y - (GroupId.y + 1) / NumGroups.y * ViewSize.y * 2, 1)); float3 ViewSpaceTileConeAxis = normalize(TileCorner00 + TileCorner10 + TileCorner01 + TileCorner11); TileConeAxis = mul(ViewSpaceTileConeAxis, (float3x3)View.ViewToTranslatedWorld); TileConeAngleCos = dot(ViewSpaceTileConeAxis, TileCorner00); TileConeAngleSin = sqrt(1 - TileConeAngleCos * TileConeAngleCos); TileConeVertex = View.WorldCameraOrigin; } uint NumCulledObjects = GetCulledNumObjects(); LOOP for (uint ObjectIndex = ThreadIndex; ObjectIndex < NumCulledObjects; ObjectIndex += THREADGROUP_TOTALSIZE) { float4 SphereCenterAndRadius = LoadObjectPositionAndRadius(ObjectIndex); BRANCH if (SphereIntersectCone(SphereCenterAndRadius, TileConeVertex, TileConeAxis, TileConeAngleCos, TileConeAngleSin)) { uint ListIndex; InterlockedAdd(NumIntersectingObjects, 1U, ListIndex); if (ListIndex < MAX_INTERSECTING_OBJECTS) { IntersectingObjectIndices[ListIndex] = ObjectIndex; } } } GroupMemoryBarrierWithGroupSync(); float MinRayTime; float TotalStepsTaken; // Trace once to find the distance to first intersection RayTraceThroughTileCulledDistanceFields(WorldRayStart, WorldRayEnd, TraceDistance, MinRayTime, TotalStepsTaken); float TempMinRayTime; // Recompute the ray end point WorldRayEnd = WorldRayStart + UnitWorldRayDirection * MinRayTime; // Trace a second time to only accumulate steps taken before the first intersection, improves visualization RayTraceThroughTileCulledDistanceFields(WorldRayStart, WorldRayEnd, MinRayTime, TempMinRayTime, TotalStepsTaken); float3 Result = saturate(TotalStepsTaken / 200.0f); if (MinRayTime < TraceDistance) { Result += .1f; } #endif RWVisualizeMeshDistanceFields[DispatchThreadId.xy] = float4(Result, 0); }
首先根据线程全局索引DispatchThreadId查找对应uv,在Compute Shader里,根据调度与线程组的分配来生成对应贴图uv好像是一种常见技巧,我记的 Unity有份简单的讲解光线追踪 也是这样,还挻有意思的,大家可以看看。回到上面,得到UV后,根据摄像机的机可以得到摄像机坐标下的平面值,加上之前的深度图,就可以求得在当前摄像机下,无透明的最近点的三维坐标,然后以摄像机为原点,摄像机向对应uv的三维坐标下很远的值为终点,我们使用模型距离场,就不考虑上面那段根据全局距离场的代码,然后调用RayTraceThroughTileCulledDistanceFields把当前原点,终点传入模型距离场中计算。
在第一次RayTraceThroughTileCulledDistanceFields中,根据上面的原点终点生成一条射线,然后查找每个模型对应的距离场信息,如box,radius,世界坐标到对应的距离场矩阵,uvscale/uvadd等,首先把上面的原点和终点转到对应的模型距离中去计算,这样方便计算,我们首先得到这条射线是否与这个距离场相交(请注意,原来起点在眼睛位置,终点在很远地方,这样就算转入到模型距离场中,也一样是起点和终点一样在模型距离场bound的外面),如果相交,LineBoxIntersect返回xy,x是近交点,y是远交点,从近交点不断向远交点慢慢移动,比较模型距离场中对应3d的uv取到的距离场值(DistanceField),可以看到DistanceField<0后中断循环,我们知道,DistanceField<0表示已经在物体里面了,SampleRayTime表示遇到物体的距离,TotalStepsTaken表示前进了多少步,然后在第二次RayTraceThroughTileCulledDistanceFields中,把SampleRayTime,TotalStepsTaken传入计算,这样只计算这一段,在这一段里再次精确多段,多段里取更精确的值出来,然后把TotalStepsTaken这个值放入VisualizeDistanceField中。
在这里结果应该也出来了,最后调用一个DrawRectangle其实就是把VisualizeDistanceField贴到对应的View全屏上,整个过程就到这个,如果有不清楚或是错误的地方,欢迎大家指出。