距離上一篇博客已經有點久了,中間忙的飛起,忽然發現很久沒寫了,這樣不好,寫一篇和工作無關的吧。
一直想搞清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全屏上,整個過程就到這個,如果有不清楚或是錯誤的地方,歡迎大家指出。