开头想写几句写这篇文章的原因,其实是因为看到一篇介绍韩国“抗疫”经验的报道,文章里说他们会在一些高危人群聚集的地方,采用一种“样本池合并检测法”:把多个人的样本混合成一个来检测,如果结果是阴性,那这群人都没有感染,如果结果是阳性,再对混合的样本分别检测。相比逐个逐个的检测,这种方法可以提高检测速度,也降低了检测成本,询问中国是否也能开发或这引进这项技术?
当时刚看到这篇文章,就想到了游戏引擎内常用的遮挡剔除的算法,它们有一些相似的地方,都需要利用有限的算力和内存(有限的人力资源和医疗资源),尽可能快的分辨出来那些 游戏内对象(可疑人群)需要被渲染(隔离),都会呈现出一定的空间相关性,都需要兼顾到剔除的准确率,时效性和资源利用率,因为文章最后问到中国是否能够开发和引进韩国的技术,当时我就想到是不是像我这样做技术的,写代码的,是不是再努力做的更多一点,想的更多一点,就可以对他们有些帮助,不需要羡慕国外的技术,就算这看起来不像是我的行业相关的
因为就像报道里提到的,其实这个模式抽象概括一下,放到程序算法里,并不是多么复杂的技术,也还有很多很多优化的空间,最简单的,样本group合并之后,如果检查出来阳性,还可以再根据一些策略做二分查找一类的筛查,这都可以大幅度提高检查效率,只要采样样本的活性时间允许,差别可能只是每次采样的样本可能需要大一点,并且做好每个样本分批多次检测的准备,这样在早期人力资源,医疗资源,时间,都比较紧迫的情况下,应该可以利用有限的资源检查更多的疑似人员,降低发生医疗挤兑的概率,是对社会有贡献的,而这一切,只需要我们改变一下看待问题的角度,技术不单单是技术,我们其实可以做的更多
这个我想在另外一篇专门的文章里再写一些,现在这里就先整理一下 UE4 的遮挡查询的过程吧~
目录
1. RenderOcclusion,筛查提交OcclusionCull整理的结果
2. RenderHzb,构建HZB,如果允许HZBOcclusion,就会提交HzbTest
从Render的InitViews开始,在其中会调用 ComputeViewVisibility,开始计算和标记整理可见性相关性,其中会调用 OcclusionCull 判断遮挡剔除,这里只是整理出来需要查询的 Query,真正提交计算是在后面 Render 的部分。
0.0 WaitOcclusionTests
一般在 DeferedShadingRender::Render 或者 MobileRender 的一开始 调用一次 Wait 等待一下前一帧或者几帧的 Occlusion Query Result
0.OcclusionCull
以下 OcclusionCull 流程:
1.赋值 bHZBOcclusion,在 OpenGL 和 Switch 上禁用 HZB
2.如果有 View.PrecomputedVisibilityData,使用 View.PrecomputedVisibilityData[VisibilityId.ByteIndex] 赋值 View.PrimitiveVisibilityMap
3.若有 ViewState->SceneSoftwareOcclusion ,调用 SceneSoftwareOcclusion->Process 计算 NumOccludedPrimitives
4.否则 若 GetFeatureLevel>ES3_1,就开始 fetch 提交 query,否则直接把所有 View.PrimitiveDefinitelyUnoccludedMap 标记为true,mark primitives as not occluded
4.1 如果 bHZBOcclusion,调用 ViewState->HZBOcclusionTests.MapResults(RHICmdList);
此时的 OcclusionFrameCounter 还是 Invalid
- MapStagingSurface(调用 d3d12::map 可线程安全) 读取 ResultsTextureCPU 到 ResultsBuffer
- 这里 RHIMapStagingSurface 可能因为前一帧的 results 而block,所有可能要 Idle,GRenderThreadIdle += FPlatformTime::Cycles() - IdleStart。
- 处理一下 device remove 等特殊情况,之后可能会crash,但这里 先保证 oc 执行正确
4.2 如果 IsRoundRobinEnabled,且不是 SceneCapture,且是 stereo views,会执行 round-robin query,偶数帧,左眼执行 occlusion query,奇数帧,右眼执行 occlusion query
4.3 FetchVisibilityForPrimitives ,计算 NumOccludedPrimitives
- 如果不允许并行,重置 View.FrameSubIsOccluded,FOcclusionBounds的 array
- 直接调用 FetchVisibilityForPrimitives_Range<true> 单线程处理所有 primitive,是否单线程传true
- 上一步如有输出 OutQueriesToRun,也就是未处理的 PendingIndividualQueriesWhenOptimizing,如果目前积攒query较少,就把所有pending的都 PrimitiveOcclusionHistory->SetCurrentQuery,否则 sort 按 LastQuerySubmitFrame 排序,挑前几个QueriesToDo,执行 PrimitiveOcclusionHistory->SetCurrentQuery
- 如果允许并行,GOcclusionCullParallelPrimFetch&&GSupportsParallelOcclusionQueries
- 默认分 MaxNumCullTasks 4 个CullTask,创建对应 FVisForPrimParams,FPrimitiveOcclusionHistory,FHZBBound,FOcclusionBounds
- 计算 Task 的 BitsPerTask,StartIndices,ProcessRange
- 将 View.PrimitiveVisibilityMap 分发 FetchVisibilityForPrimitivesTask,去执行 FetchVisibilityForPrimitives_Range<false>,单线程false
- 从param 取出来 InsertPrimitiveOcclusionHistory,QueriesToRelease,HZBBoundsToAdd,QueriesToAdd,开始处理自己分到的 View.PrimitiveVisibilityMap
- 根据 Scene->PrimitiveOcclusionFlags 获取 bCanBeOccluded,
- 默认 NumSubQueries = 1,如果允许 GAllowSubPrimitiveQueries,flag是 HasSubprimitiveQueries,那会获取一下 SubBounds,赋值多次 NumSubQueries
- 遍历 NumSubQueries,从 ViewState->PrimitiveOcclusionHistorySet 中通过 FPrimitiveOcclusionHistoryKey 去 Find 这个 Primitivfe对应的PrimitiveOcclusionHistory
- 如果没有找到History,这次会placement new一个加到 ViewState里,如果找到了,history里有,此时如果flag是 bCanBeOccluded
- 如果 bHZBOcclusion,找到 上一次有效帧的 HZBOcclusionTests,调用 bIsOccluded = !HZBOcclusionTests.IsVisible(); 判断 ResultsBuffer中对应bit是否为0,得到是否遮挡
- 否则默认 Hardware query,PrimitiveOcclusionHistory->GetQueryForReading得到 PendingOcclusionQuery 中的PastQuery
- 如果找到 PastQuery, GDynamicRHI->RHIGetRenderQueryResult 得到前一帧渲染的像素是否为 0,这里没等MSAA,如果为0,则 bIsOccluded = true,LastPixelsPercentage = 0.0f,否则 Percentage = NumPixels * View.OneOverNumPossiblePixels
- 如果没有找到 PastQuery,bIsOccluded 就等于 history->WasOccludedLastFrame 或者 (History->LastProvenVisibleTime + PrimitiveProbablyVisibleTime < CurrentRealTime) 预估一个是否可见,percentage 也等于 0.0f/GEngine->MaxOcclusionPixelsFraction
- 然后如果要 bClearQueries,会加入query到 QueriesToRelease
- 现在有了 PrimitiveOcclusionHistory 和 bIsOccluded结果,如果需要提交 bSubmitQueries,先处理一下第一次visible的缓冲缓冲几帧可见以及ConsideredBBoxExpand 等细节措施之后
- 获取 OcclusionBounds 和 View.NearClippingPlane做相交测试,赋值 bAllowBoundsTest,如果为false,和nearplane相交,则当作可见
- 如果 bAllowBoundsTest,设置 PrimitiveOcclusionHistory->LastTestFrameNumber 为当前 OcclusionFrameCount
- 如果允许 bHZBOcclusion,单线程会 HZBOcclusionTests.AddBounds,之后计算,或者多线程加到 HZBBoundsToAdd输出,加入之后,会在后面 Submit 时提交计算
- 如果不允许,声明 bool bRunQuery, bGroupedQuery; decide if a query should be run this frame,对应于作为独立一个query,还是加入到group里多个primitive一次query从而提高效率
- 如果有subquery,分了多次提交,则永远不允许bGroupedQuery,都作为单次提交,否则检查是否 RunQuery,是否 group,如果bIsOccluded 就放到group里,并runQuery,否则没阻挡,会随机计算出一个是否runQuery
- 如果 bRunQuery,单线程就直接 PrimitiveOcclusionHistory->SetCurrentQuery,根据是否group 调用 FOcclusionQueryBatcher::BatchPrimitive 加到 View.GroupedOcclusionQueries / IndividualOcclusionQueries,多线程就加到 QueriesToAdd ,之后处理,group的 query合并就在这个BatchPrimitive 方法里了,在ViewInfo里对初始化的时候 IndividualOcclusionQueries只可以包含 1 个 Primitive,GroupedOcclusionQueries 目前版本包含最大16个primitive,也就是是最多一次会提交16个被遮挡的primitive 的query查询(查了下,在4.19之前OccludedPrimitiveQueryBatchSize 值还是8,这个版本改成了16,提交的log说明的是想要 more agressively batch occlusion queries )
- 在BatchPrimitive 方法 里的group query合并的流程是,检查当前query包含的primitive是否已经达到上限,如果达到,就new 一个 新的 FOcclusionBatch,然后从 OcclusionQueryPool 分配一个新的query赋值,如果没达到上线,就接着把 BoundBox的 8个 vertex,24个float值写入 vb,对应到 它的IB,FOcclusionQueryIndexBuffer 是 NUM_CUBE_VERTICES 36,一个cube 6个面,12个三角,36个index
- FOcclusionQueryIndexBuffer 的 初始化
- 这里声明了 全局的一个 GOcclusionQueryIndexBuffer,利用GCubeIndices 初始化 IB.
- 处理完毕,更新 PrimitiveOcclusionHistory 的各种 last time
- 最后,根据是不是subQuery,是不是 bIsOccluded,更新 View里的对应bit,View.PrimitiveVisibilityMap.AccessCorrespondingBit 或者View.PrimitiveDefinitelyUnoccludedMap.AccessCorrespondingBit
- WaitUntilTasksComplete 等待task完成
- 如果多线程,前面分发task都是输出,没有处理,这里集中处理一下
- 遍历 OutHZBBounds,HZBOcclusionTests.AddBounds
- 遍历 OutQueriesToRelease,History->ReleaseQuery
- 遍历 OutQueriesToRun,PrimitiveOcclusionHistory->SetCurrentQuery
- 遍历 OutputOcclusionHistory,加到 ViewState里,ViewPrimitiveOcclusionHistory.Add
4.4 如果 bHZBOcclusion,调用 ViewState->HZBOcclusionTests.UnmapResults(RHICmdList);,如果允许submit query,设置 当前 ViewState->OcclusionFrameCounter 的 valid frame
- 和上面对应,执行 UnmapStagingSurface,释放 ResultsTextureCPU 和 ResultsBuffer。
***Start Occlusion Query
1. RenderOcclusion,筛查提交OcclusionCull整理的结果
- UpdateDownsampledDepthSurface,Update the quarter-sized depth buffer with the current contents of the scene depth texture. This needs to happen before occlusion tests, which makes use of the small depth buffer. 用 FDownsampleSceneDepthPS 缩减 SceneDepth 缩减到 1/4 大小的 SmallDepth,可以配置是否使用相邻四像素最大的那个,或者直接左上角那个pixel
- BeginOcclusionTests,
- 从 ViewState->ShadowOcclusionQueryMaps 拿到 ShadowKeyOcclusionQueryMap
- ViewState->TrimOcclusionHistory(),Clear primitives which haven't been visible recently out of the occlusion history, and reset old pending occlusion queries. 遍历 PrimitiveOcclusionHistorySet,判断各种 MinQueryTime/MinHistoryTime,调用 Release和Remove
- 先检查是否有 query要提交,从ShadowFrustumQueries, ReflectionQuery ,IndividualOcclusionQueries,GroupedOcclusionQueries中找是否有要提交的
- 首先ShadowFrustumQueries,遍历Scene->Lights 查找是否需要提交query
- 先获取(InitView里准备好的)每个Light 的 AllProjectedShadows,判断属性 bOnePassPointLightShadow / IsWholeSceneDirectionalShadow()等判断 ShadowOcclusionQueryIntersectionMode,从而调用 AllocateProjectedShadowOcclusionQuery,主要做一下sphere或者nearPlane的相交测试,如果允许Issue,就提交这个shadow 到 ShadowOcclusionQueryMap
- 再获取 每个LightInfo 的 VisibleLightInfo.OccludedPerObjectShadows,同样判断执行 AllocateProjectedShadowOcclusionQuery,看是否提交 query,这里一般使用type:SOQ_NearPlaneVsShadowFrustum
- 遍历Scene->PlanarReflections,执行 AllocatePlanarReflectionOcclusionQuery判断是否提交,同样是用 FPlanarReflectionSceneProxy.WorldBounds 和 View.ViewFrustum 做相交测试,如果需要执行,会FindOrAdd 到 ViewState->PlanarReflectionOcclusionHistories 的query数组中
- 然后在IndividualOcclusionQueries,GroupedOcclusionQueries中看看是否有
- 如果以上要 bBatchedQueries,那就是执行了,内容包括所有 View 的 PointLightQueries / CSMQueries / ShadowQueries / ReflectionQueries / IndividualOcclusionQueries / GroupedOcclusionQueries
- 走光栅化流程,先根据是否downSample去使用smallDepth 等条件设置好state,然后设置好 FOcclusionQueryVS 和 FOcclusionQueryPS
- 首先对于 PointLightQueries,直接 ExecutePointLightShadowOcclusionQuery,点光源用sphere代替,StencilingGeometry::DrawVectorSphere(RHICmdList);每个PointLightQuery绘制一次(StencilingGeometry是类似风场那个的自定义的简单的 IB/VB)
- 而对于 CSMQuerieInfos,ShadowQuerieInfos,ReflectionQuerieInfos,由于提交的测试vb比较复杂,封装了方法先 prepare,三个写到一个buffer里,记录好index偏移,再execute。
- CSMQuerieInfos,PrepareDirectionalLightShadowOcclusionQuery,ib/vb 是这层cascade 的 frustum,6个vert(实际是4个vert,2个triangle),BaseVertexIndex += 6;
- ShadowQuerieInfos,PrepareProjectedShadowOcclusionQuery,ib/vb 是shadow's frustum,8个vert(cube),BaseVertexIndex += 8;
- ReflectionQuerieInfos,PreparePlanarReflectionOcclusionQuery,ib/vb 是...,24个vert,BaseVertexIndex += 8;
- CSMQuerieInfos,ExecuteDirectionalLightShadowOcclusionQuery,DrawPrimitive(BaseVertexIndex, 2, 1);绘制,BaseVertexIndex += 6;
- ShadowQuerieInfos,ExecuteProjectedShadowOcclusionQuery,DrawIndexedPrimitive(GCubeIndexBuffer.IndexBufferRHI, BaseVertexIndex, 0, 8, 0, 12, 1);绘制,BaseVertexIndex += 8;
- ReflectionQuerieInfos,ExecutePlanarReflectionOcclusionQuery,DrawIndexedPrimitive(GCubeIndexBuffer.IndexBufferRHI, BaseVertexIndex, 0, 8, 0, 12, 1);,BaseVertexIndex += 8;
- 最后 flush,GroupedOcclusionQueries / IndividualOcclusionQueries。
- 这里的 flush,就是拿取上面声明的全局的 IB(GOcclusionQueryIndexBuffer),存在query里的 VB,然后SetStreamSource,调用DrawIndexedPrimitive 绘制了
2. RenderHzb,构建HZB,如果允许HZBOcclusion,就会提交HzbTest
- 如果 bSSAO || bHZBOcclusion || bSSR || bSSGI || bHair,都需要 BuildHZB。
- 根据 ViewRect,log2计算 NumMips,走 FHZBBuildCS / 或者光栅化 FHZBBuildPS 计算mip,默认只输出 Furthest 的 mip, 如果 RenderScreenSpaceDiffuseIndirect ,就也需要closesetMip,执行第一张 further 和 close一起输出,后面的mip根据需求计算
- 如果 bHZBOcclusion,那接着执行 HZBOcclusionTests.Submit(RHICmdList, View)。
- 从 GRenderTargetPool 中获取 RT, HZBBoundsCenter / HZBBoundsExtent / HZBResultsGPU
- 遍历 所有 OcclusionPrimitives,把 center 和 extent 写入 BoundsCenterTexture 和 BoundsExtentTexture
- FScreenVS / FHZBTestPS,绘制结果到 ResultsTextureGPU
- 最后 从GPU拷贝回来,ResultsTextureGPU copy 到 ResultsTextureCPU
3. FinishOcclusion,
执行一次 RHICmdList.SubmitCommandsHint();
4. FenceOcclusionTests,
设置当前 NumFrames 的 Fence,然后 RHICmdList.ImmediateFlush,然后就到后面帧的Wait
***End Occlusion Query
下篇就写UE4的 Dynamic Instancing 吧,正好最好碰到几个暗坑~