3
0

Culling.cpp 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <Atom/RPI.Public/AuxGeom/AuxGeomDraw.h>
  9. #include <Atom/RPI.Public/AuxGeom/AuxGeomFeatureProcessorInterface.h>
  10. #include <Atom/RPI.Public/Culling.h>
  11. #include <Atom/RPI.Public/Model/ModelLodUtils.h>
  12. #include <Atom/RPI.Public/RPISystemInterface.h>
  13. #include <Atom/RPI.Public/RenderPipeline.h>
  14. #include <Atom/RPI.Public/Scene.h>
  15. #include <Atom/RPI.Public/View.h>
  16. #include <AzCore/Math/MatrixUtils.h>
  17. #include <AzCore/Math/ShapeIntersection.h>
  18. #include <AzCore/Casting/numeric_cast.h>
  19. #include <AzCore/std/parallel/lock.h>
  20. #include <AzCore/Casting/numeric_cast.h>
  21. #include <AzCore/Jobs/JobFunction.h>
  22. #include <AzCore/Jobs/Job.h>
  23. #include <AzCore/Task/TaskGraph.h>
  24. #include <AzCore/std/smart_ptr/unique_ptr.h>
  25. #include <Atom_RPI_Traits_Platform.h>
  26. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  27. #include <MaskedOcclusionCulling/MaskedOcclusionCulling.h>
  28. #endif
  29. //Enables more inner-loop profiling scopes (can create high overhead in telemetry if there are many-many objects in a scene)
  30. //#define AZ_CULL_PROFILE_DETAILED
  31. //Enables more detailed profiling descriptions within the culling system, but adds some performance overhead.
  32. //Enable this to more easily see which jobs are associated with which view.
  33. //#define AZ_CULL_PROFILE_VERBOSE
  34. namespace AZ
  35. {
  36. namespace RPI
  37. {
  38. // Entry work lists
  39. AZ_CVAR(bool, r_useEntryWorkListsForCulling, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Use entity work lists instead of node work lists for job distribution");
  40. AZ_CVAR(uint32_t, r_numEntriesPerCullingJob, 750, nullptr, AZ::ConsoleFunctorFlags::Null, "Controls amount of entries to collect for jobs when using entry work lists");
  41. // Node work lists using entry count
  42. AZ_CVAR(bool, r_useEntryCountForNodeJobs, true, nullptr, AZ::ConsoleFunctorFlags::Null, "Use entity count instead of node count when checking whether to spawn job for node work list");
  43. AZ_CVAR(uint32_t, r_maxNodesWhenUsingEntryCount, 100, nullptr, AZ::ConsoleFunctorFlags::Null, "Controls max amount of nodes to collect when using entry count");
  44. // Node work lists using node count
  45. AZ_CVAR(uint32_t, r_numNodesPerCullingJob, 25, nullptr, AZ::ConsoleFunctorFlags::Null, "Controls amount of nodes to collect for jobs when not using the entry count");
  46. // This value dictates the amount to extrude the octree node OBB when doing a frustum intersection test against the camera frustum to help cut draw calls for shadow cascade passes.
  47. // Default is set to -1 as this is optimization needs to be triggered by the content developer by setting a reasonable non-negative value applicable for their content.
  48. AZ_CVAR(int, r_shadowCascadeExtrusionAmount, -1, nullptr, AZ::ConsoleFunctorFlags::Null, "The amount of meters to extrude the Obb towards light direction when doing frustum overlap test against camera frustum");
  49. #ifdef AZ_CULL_DEBUG_ENABLED
  50. void DebugDrawWorldCoordinateAxes(AuxGeomDraw* auxGeom)
  51. {
  52. auxGeom->DrawCylinder(Vector3(.5, .0, .0), Vector3(1, 0, 0), 0.02f, 1.0f, Colors::Red, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DepthTest::Off);
  53. auxGeom->DrawCylinder(Vector3(.0, .5, .0), Vector3(0, 1, 0), 0.02f, 1.0f, Colors::Green, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DepthTest::Off);
  54. auxGeom->DrawCylinder(Vector3(.0, .0, .5), Vector3(0, 0, 1), 0.02f, 1.0f, Colors::Blue, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DepthTest::Off);
  55. Vector3 axisVerts[] =
  56. {
  57. Vector3(0.f, 0.f , 0.f), Vector3(10000.f, 0.f, 0.f),
  58. Vector3(0.f, 0.f , 0.f), Vector3(0.f, 10000.f, 0.f),
  59. Vector3(0.f, 0.f , 0.f), Vector3(0.f, 0.f, 10000.f)
  60. };
  61. Color colors[] =
  62. {
  63. Colors::Red, Colors::Red,
  64. Colors::Green, Colors::Green,
  65. Colors::Blue, Colors::Blue
  66. };
  67. AuxGeomDraw::AuxGeomDynamicDrawArguments lineArgs;
  68. lineArgs.m_verts = axisVerts;
  69. lineArgs.m_vertCount = 6;
  70. lineArgs.m_colors = colors;
  71. lineArgs.m_colorCount = lineArgs.m_vertCount;
  72. lineArgs.m_depthTest = AuxGeomDraw::DepthTest::Off;
  73. auxGeom->DrawLines(lineArgs);
  74. }
  75. #endif //AZ_CULL_DEBUG_ENABLED
  76. CullingDebugContext::~CullingDebugContext()
  77. {
  78. AZStd::lock_guard<AZStd::mutex> lock(m_perViewCullStatsMutex);
  79. for (auto& iter : m_perViewCullStats)
  80. {
  81. delete iter.second;
  82. iter.second = nullptr;
  83. }
  84. }
  85. CullingDebugContext::CullStats& CullingDebugContext::GetCullStatsForView(View* view)
  86. {
  87. AZStd::lock_guard<AZStd::mutex> lock(m_perViewCullStatsMutex);
  88. auto iter = m_perViewCullStats.find(view);
  89. if (iter != m_perViewCullStats.end())
  90. {
  91. AZ_Assert(iter->second->m_name == view->GetName(), "stored view name does not match");
  92. return *iter->second;
  93. }
  94. else
  95. {
  96. m_perViewCullStats[view] = aznew CullStats(view->GetName());
  97. return *m_perViewCullStats[view];
  98. }
  99. }
  100. void CullingDebugContext::ResetCullStats()
  101. {
  102. m_numCullablesInScene = 0;
  103. AZStd::lock_guard<AZStd::mutex> lockCullStats(m_perViewCullStatsMutex);
  104. for (auto& cullStatsPair : m_perViewCullStats)
  105. {
  106. cullStatsPair.second->Reset();
  107. }
  108. }
  109. void CullingScene::RegisterOrUpdateCullable(Cullable& cullable)
  110. {
  111. // Multiple threads can call RegisterOrUpdateCullable at the same time
  112. // since the underlying visScene is thread safe, but if you're inserting or
  113. // updating between BeginCulling and EndCulling, you'll get non-deterministic
  114. // results depending on a race condition if you happen to update before or after
  115. // the culling system starts Enumerating, so use soft_lock_shared here
  116. m_cullDataConcurrencyCheck.soft_lock_shared();
  117. m_visScene->InsertOrUpdateEntry(cullable.m_cullData.m_visibilityEntry);
  118. m_cullDataConcurrencyCheck.soft_unlock_shared();
  119. }
  120. void CullingScene::UnregisterCullable(Cullable& cullable)
  121. {
  122. // Multiple threads can call RegisterOrUpdateCullable at the same time
  123. // since the underlying visScene is thread safe, but if you're inserting or
  124. // updating between BeginCulling and EndCulling, you'll get non-deterministic
  125. // results depending on a race condition if you happen to update before or after
  126. // the culling system starts Enumerating, so use soft_lock_shared here
  127. m_cullDataConcurrencyCheck.soft_lock_shared();
  128. m_visScene->RemoveEntry(cullable.m_cullData.m_visibilityEntry);
  129. m_cullDataConcurrencyCheck.soft_unlock_shared();
  130. }
  131. uint32_t CullingScene::GetNumCullables() const
  132. {
  133. return m_visScene->GetEntryCount();
  134. }
  135. struct WorklistData
  136. {
  137. CullingDebugContext* m_debugCtx = nullptr;
  138. const Scene* m_scene = nullptr;
  139. View* m_view = nullptr;
  140. Frustum m_frustum;
  141. Frustum m_cameraFrustum;
  142. Frustum m_excludeFrustum;
  143. AZ::Job* m_parentJob = nullptr;
  144. AZ::TaskGraphEvent* m_taskGraphEvent = nullptr;
  145. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  146. MaskedOcclusionCulling* m_maskedOcclusionCulling = nullptr;
  147. #endif
  148. bool m_hasExcludeFrustum = false;
  149. bool m_applyCameraFrustumIntersectionTest = false;
  150. #ifdef AZ_CULL_DEBUG_ENABLED
  151. AuxGeomDrawPtr GetAuxGeomPtr()
  152. {
  153. if (m_debugCtx->m_debugDraw && (m_view->GetName() == m_debugCtx->m_currentViewSelectionName))
  154. {
  155. return AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(m_scene);
  156. }
  157. return nullptr;
  158. }
  159. #endif
  160. };
  161. static AZStd::shared_ptr<WorklistData> MakeWorklistData(
  162. CullingDebugContext& debugCtx,
  163. const Scene& scene,
  164. View& view,
  165. Frustum& frustum,
  166. [[maybe_unused]] void* maskedOcclusionCulling,
  167. AZ::Job* parentJob,
  168. AZ::TaskGraphEvent* taskGraphEvent)
  169. {
  170. AZStd::shared_ptr<WorklistData> worklistData = AZStd::make_shared<WorklistData>();
  171. worklistData->m_debugCtx = &debugCtx;
  172. worklistData->m_scene = &scene;
  173. worklistData->m_view = &view;
  174. worklistData->m_frustum = frustum;
  175. worklistData->m_parentJob = parentJob;
  176. worklistData->m_taskGraphEvent = taskGraphEvent;
  177. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  178. worklistData->m_maskedOcclusionCulling = static_cast<MaskedOcclusionCulling*>(maskedOcclusionCulling);
  179. #endif
  180. return worklistData;
  181. }
  182. // Used to accumulate NodeData into lists to be handed off to jobs for processing
  183. struct WorkListType
  184. {
  185. void Init()
  186. {
  187. m_entryCount = 0;
  188. u32 reserveCount = r_useEntryCountForNodeJobs ? r_maxNodesWhenUsingEntryCount : r_numNodesPerCullingJob;
  189. m_nodes.reserve(reserveCount);
  190. }
  191. u32 m_entryCount = 0;
  192. AZStd::vector<AzFramework::IVisibilityScene::NodeData> m_nodes;
  193. };
  194. // Used to accumulate VisibilityEntry into lists to be handed off to jobs for processing
  195. struct EntryListType
  196. {
  197. AZStd::vector<AzFramework::VisibilityEntry*> m_entries;
  198. };
  199. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  200. static MaskedOcclusionCulling::CullingResult TestOcclusionCulling(
  201. const AZStd::shared_ptr<WorklistData>& worklistData,
  202. AzFramework::VisibilityEntry* visibleEntry);
  203. #endif
  204. static void ProcessEntrylist(const AZStd::shared_ptr<WorklistData>& worklistData, const AZStd::vector<AzFramework::VisibilityEntry*>& entries, bool parentNodeContainedInFrustum = false, s32 startIdx = 0, s32 endIdx = -1)
  205. {
  206. #ifdef AZ_CULL_DEBUG_ENABLED
  207. // These variable are only used for the gathering of debug information.
  208. uint32_t numDrawPackets = 0;
  209. uint32_t numVisibleCullables = 0;
  210. #endif
  211. endIdx = (endIdx == -1) ? s32(entries.size()) : endIdx;
  212. for (s32 i = startIdx; i < endIdx; ++i)
  213. {
  214. AzFramework::VisibilityEntry* visibleEntry = entries[i];
  215. if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable ||
  216. visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  217. {
  218. Cullable* c = static_cast<Cullable*>(visibleEntry->m_userData);
  219. if ((c->m_cullData.m_drawListMask & worklistData->m_view->GetDrawListMask()).none() ||
  220. c->m_cullData.m_hideFlags & worklistData->m_view->GetUsageFlags() ||
  221. c->m_isHidden)
  222. {
  223. continue;
  224. }
  225. if (!parentNodeContainedInFrustum)
  226. {
  227. IntersectResult res = ShapeIntersection::Classify(worklistData->m_frustum, c->m_cullData.m_boundingSphere);
  228. bool entryInFrustum = (res != IntersectResult::Exterior) && (res == IntersectResult::Interior || ShapeIntersection::Overlaps(worklistData->m_frustum, c->m_cullData.m_boundingObb));
  229. if (!entryInFrustum)
  230. {
  231. continue;
  232. }
  233. }
  234. if (worklistData->m_hasExcludeFrustum &&
  235. ShapeIntersection::Classify(worklistData->m_excludeFrustum, c->m_cullData.m_boundingSphere) == IntersectResult::Interior)
  236. {
  237. // Skip item contained in exclude frustum.
  238. continue;
  239. }
  240. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  241. if (TestOcclusionCulling(worklistData, visibleEntry) == MaskedOcclusionCulling::CullingResult::VISIBLE)
  242. #endif
  243. {
  244. // There are ways to write this without [[maybe_unused]], but they are brittle.
  245. // For example, using #else could cause a bug where the function's parameter
  246. // is changed in #ifdef but not in #else.
  247. [[maybe_unused]] const uint32_t drawPacketCount = AddLodDataToView(
  248. c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view, visibleEntry->m_typeFlags);
  249. c->m_isVisible = true;
  250. worklistData->m_view->ApplyFlags(c->m_flags);
  251. #ifdef AZ_CULL_DEBUG_ENABLED
  252. ++numVisibleCullables;
  253. numDrawPackets += drawPacketCount;
  254. #endif
  255. }
  256. }
  257. }
  258. #ifdef AZ_CULL_DEBUG_ENABLED
  259. AuxGeomDrawPtr auxGeomPtr = worklistData->GetAuxGeomPtr();
  260. if (auxGeomPtr)
  261. {
  262. //Draw bounds on individual objects
  263. if (worklistData->m_debugCtx->m_drawBoundingBoxes || worklistData->m_debugCtx->m_drawBoundingSpheres || worklistData->m_debugCtx->m_drawLodRadii)
  264. {
  265. for (AzFramework::VisibilityEntry* visibleEntry : entries)
  266. {
  267. if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable ||
  268. visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  269. {
  270. Cullable* c = static_cast<Cullable*>(visibleEntry->m_userData);
  271. if (worklistData->m_debugCtx->m_drawBoundingBoxes)
  272. {
  273. auxGeomPtr->DrawObb(c->m_cullData.m_boundingObb, Matrix3x4::Identity(),
  274. parentNodeContainedInFrustum ? Colors::Lime : Colors::Yellow, AuxGeomDraw::DrawStyle::Line);
  275. }
  276. if (worklistData->m_debugCtx->m_drawBoundingSpheres)
  277. {
  278. auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(), c->m_cullData.m_boundingSphere.GetRadius(),
  279. Color(0.5f, 0.5f, 0.5f, 0.3f), AuxGeomDraw::DrawStyle::Shaded);
  280. }
  281. if (worklistData->m_debugCtx->m_drawLodRadii)
  282. {
  283. auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(),
  284. c->m_lodData.m_lodSelectionRadius,
  285. Color(1.0f, 0.5f, 0.0f, 0.3f), RPI::AuxGeomDraw::DrawStyle::Shaded);
  286. }
  287. }
  288. }
  289. }
  290. }
  291. if (worklistData->m_debugCtx->m_enableStats)
  292. {
  293. CullingDebugContext::CullStats& cullStats = worklistData->m_debugCtx->GetCullStatsForView(worklistData->m_view);
  294. //no need for mutex here since these are all atomics
  295. cullStats.m_numVisibleDrawPackets += numDrawPackets;
  296. cullStats.m_numVisibleCullables += numVisibleCullables;
  297. ++cullStats.m_numJobs;
  298. }
  299. #endif
  300. }
  301. static void ProcessVisibilityNode(const AZStd::shared_ptr<WorklistData>& worklistData, const AzFramework::IVisibilityScene::NodeData& nodeData)
  302. {
  303. bool nodeIsContainedInFrustum = !worklistData->m_debugCtx->m_enableFrustumCulling || ShapeIntersection::Contains(worklistData->m_frustum, nodeData.m_bounds);
  304. s32 startIdx = 0, size = s32(nodeData.m_entries.size());
  305. const AZStd::vector<AzFramework::VisibilityEntry*>& entries = nodeData.m_entries;
  306. if (worklistData->m_taskGraphEvent)
  307. {
  308. static const AZ::TaskDescriptor descriptor{ "AZ::RPI::ProcessWorklist", "Graphics" };
  309. AZ::TaskGraph taskGraph{ "ProcessCullableEntries" };
  310. AZ::TaskGraphEvent taskGraphEvent{ "ProcessCullableEntries Wait" };
  311. while (r_useEntryCountForNodeJobs && (size - startIdx) > s32(r_numEntriesPerCullingJob))
  312. {
  313. taskGraph.AddTask(descriptor, [=, &entries]() -> void
  314. {
  315. ProcessEntrylist(worklistData, entries, nodeIsContainedInFrustum, startIdx, startIdx + r_numEntriesPerCullingJob);
  316. });
  317. startIdx += s32(r_numEntriesPerCullingJob);
  318. }
  319. if (!taskGraph.IsEmpty())
  320. {
  321. taskGraph.Detach();
  322. taskGraph.Submit(worklistData->m_taskGraphEvent);
  323. }
  324. ProcessEntrylist(worklistData, nodeData.m_entries, nodeIsContainedInFrustum, startIdx, size);
  325. }
  326. else // Use job system
  327. {
  328. while (r_useEntryCountForNodeJobs && (size - startIdx) > s32(r_numEntriesPerCullingJob))
  329. {
  330. auto processEntries = [=, &entries]() -> void
  331. {
  332. ProcessEntrylist(worklistData, entries, nodeIsContainedInFrustum, startIdx, startIdx + r_numEntriesPerCullingJob);
  333. };
  334. AZ::Job* job = AZ::CreateJobFunction(AZStd::move(processEntries), true);
  335. worklistData->m_parentJob->SetContinuation(job);
  336. job->Start();
  337. startIdx += s32(r_numEntriesPerCullingJob);
  338. }
  339. ProcessEntrylist(worklistData, nodeData.m_entries, nodeIsContainedInFrustum, startIdx, size);
  340. }
  341. #ifdef AZ_CULL_DEBUG_ENABLED
  342. //Draw the node bounds
  343. // "Fully visible" nodes are nodes that are fully inside the frustum. "Partially visible" nodes intersect the edges of the frustum.
  344. // Since the nodes of an octree have lots of overlapping boxes with coplanar edges, it's easier to view these separately, so
  345. // we have a few debug booleans to toggle which ones to draw.
  346. AuxGeomDrawPtr auxGeomPtr = worklistData->GetAuxGeomPtr();
  347. if (auxGeomPtr)
  348. {
  349. if (nodeIsContainedInFrustum && worklistData->m_debugCtx->m_drawFullyVisibleNodes)
  350. {
  351. auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Lime, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off);
  352. }
  353. else if (!nodeIsContainedInFrustum && worklistData->m_debugCtx->m_drawPartiallyVisibleNodes)
  354. {
  355. auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Yellow, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off);
  356. }
  357. }
  358. #endif
  359. }
  360. static void ProcessWorklist(const AZStd::shared_ptr<WorklistData>& worklistData, const WorkListType& worklist)
  361. {
  362. AZ_PROFILE_SCOPE(RPI, "Culling: ProcessWorklist");
  363. AZ_Assert(worklist.m_nodes.size() > 0, "Received empty worklist in ProcessWorklist");
  364. for (const AzFramework::IVisibilityScene::NodeData& nodeData : worklist.m_nodes)
  365. {
  366. ProcessVisibilityNode(worklistData, nodeData);
  367. }
  368. }
  369. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  370. static MaskedOcclusionCulling::CullingResult TestOcclusionCulling(
  371. const AZStd::shared_ptr<WorklistData>& worklistData,
  372. AzFramework::VisibilityEntry* visibleEntry)
  373. {
  374. if (!worklistData->m_maskedOcclusionCulling || !worklistData->m_view->GetMaskedOcclusionCullingDirty())
  375. {
  376. return MaskedOcclusionCulling::CullingResult::VISIBLE;
  377. }
  378. #ifdef AZ_CULL_PROFILE_VERBOSE
  379. AZ_PROFILE_SCOPE(RPI, "TestOcclusionCulling");
  380. #endif
  381. if (visibleEntry->m_boundingVolume.Contains(worklistData->m_view->GetCameraTransform().GetTranslation()))
  382. {
  383. // camera is inside bounding volume
  384. return MaskedOcclusionCulling::CullingResult::VISIBLE;
  385. }
  386. const Vector3& minBound = visibleEntry->m_boundingVolume.GetMin();
  387. const Vector3& maxBound = visibleEntry->m_boundingVolume.GetMax();
  388. // compute bounding volume corners
  389. Vector4 corners[8];
  390. corners[0] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f);
  391. corners[1] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f);
  392. corners[2] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f);
  393. corners[3] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f);
  394. corners[4] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f);
  395. corners[5] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f);
  396. corners[6] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f);
  397. corners[7] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f);
  398. // find min clip-space depth and NDC min/max
  399. float minDepth = FLT_MAX;
  400. float ndcMinX = FLT_MAX;
  401. float ndcMinY = FLT_MAX;
  402. float ndcMaxX = -FLT_MAX;
  403. float ndcMaxY = -FLT_MAX;
  404. for (uint32_t index = 0; index < 8; ++index)
  405. {
  406. minDepth = AZStd::min(minDepth, corners[index].GetW());
  407. if (minDepth < 0.00000001f)
  408. {
  409. return MaskedOcclusionCulling::CullingResult::VISIBLE;
  410. }
  411. // convert to NDC
  412. corners[index] /= corners[index].GetW();
  413. ndcMinX = AZStd::min(ndcMinX, corners[index].GetX());
  414. ndcMinY = AZStd::min(ndcMinY, corners[index].GetY());
  415. ndcMaxX = AZStd::max(ndcMaxX, corners[index].GetX());
  416. ndcMaxY = AZStd::max(ndcMaxY, corners[index].GetY());
  417. }
  418. // test against the occlusion buffer, which contains only the manually placed occlusion planes
  419. return worklistData->m_maskedOcclusionCulling->TestRect(ndcMinX, ndcMinY, ndcMaxX, ndcMaxY, minDepth);
  420. }
  421. #endif
  422. void CullingScene::ProcessCullablesCommon(
  423. const Scene& scene [[maybe_unused]],
  424. View& view,
  425. AZ::Frustum& frustum [[maybe_unused]],
  426. void*& maskedOcclusionCulling [[maybe_unused]])
  427. {
  428. AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesCommon() - %s", view.GetName().GetCStr());
  429. #ifdef AZ_CULL_DEBUG_ENABLED
  430. if (m_debugCtx.m_freezeFrustums)
  431. {
  432. AZStd::lock_guard<AZStd::mutex> lock(m_debugCtx.m_frozenFrustumsMutex);
  433. auto iter = m_debugCtx.m_frozenFrustums.find(&view);
  434. if (iter != m_debugCtx.m_frozenFrustums.end())
  435. {
  436. frustum = iter->second;
  437. }
  438. }
  439. if (m_debugCtx.m_debugDraw && m_debugCtx.m_drawViewFrustum && view.GetName() == m_debugCtx.m_currentViewSelectionName)
  440. {
  441. AuxGeomDrawPtr auxGeomPtr = AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(&scene);
  442. if (auxGeomPtr)
  443. {
  444. auxGeomPtr->DrawFrustum(frustum, AZ::Colors::White);
  445. }
  446. }
  447. if (m_debugCtx.m_enableStats)
  448. {
  449. CullingDebugContext::CullStats& cullStats = m_debugCtx.GetCullStatsForView(&view);
  450. cullStats.m_cameraViewToWorld = view.GetViewToWorldMatrix();
  451. }
  452. #endif //AZ_CULL_DEBUG_ENABLED
  453. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  454. // setup occlusion culling, if necessary
  455. maskedOcclusionCulling = m_occlusionPlanes.empty() ? nullptr : view.GetMaskedOcclusionCulling();
  456. if (maskedOcclusionCulling)
  457. {
  458. // frustum cull occlusion planes
  459. using VisibleOcclusionPlane = AZStd::pair<OcclusionPlane, float>;
  460. AZStd::vector<VisibleOcclusionPlane> visibleOccluders;
  461. visibleOccluders.reserve(m_occlusionPlanes.size());
  462. for (const auto& occlusionPlane : m_occlusionPlanes)
  463. {
  464. if (ShapeIntersection::Overlaps(frustum, occlusionPlane.m_aabb))
  465. {
  466. // occluder is visible, compute view space distance and add to list
  467. float depth = (view.GetWorldToViewMatrix() * occlusionPlane.m_aabb.GetMin()).GetZ();
  468. depth = AZStd::min(depth, (view.GetWorldToViewMatrix() * occlusionPlane.m_aabb.GetMax()).GetZ());
  469. visibleOccluders.emplace_back(occlusionPlane, depth);
  470. }
  471. }
  472. // sort the occlusion planes by view space distance, front-to-back
  473. AZStd::sort(visibleOccluders.begin(), visibleOccluders.end(), [](const VisibleOcclusionPlane& LHS, const VisibleOcclusionPlane& RHS)
  474. {
  475. return LHS.second > RHS.second;
  476. });
  477. bool anyVisible = false;
  478. for (const VisibleOcclusionPlane& occlusionPlane : visibleOccluders)
  479. {
  480. // convert to clip-space
  481. const Vector4 projectedBL = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerBL);
  482. const Vector4 projectedTL = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerTL);
  483. const Vector4 projectedTR = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerTR);
  484. const Vector4 projectedBR = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerBR);
  485. // store to float array
  486. float verts[16];
  487. projectedBL.StoreToFloat4(&verts[0]);
  488. projectedTL.StoreToFloat4(&verts[4]);
  489. projectedTR.StoreToFloat4(&verts[8]);
  490. projectedBR.StoreToFloat4(&verts[12]);
  491. static constexpr const uint32_t indices[6] = { 0, 1, 2, 2, 3, 0 };
  492. // render into the occlusion buffer, specifying BACKFACE_NONE so it functions as a double-sided occluder
  493. if (static_cast<MaskedOcclusionCulling*>(maskedOcclusionCulling)
  494. ->RenderTriangles(verts, indices, 2, nullptr, MaskedOcclusionCulling::BACKFACE_NONE) ==
  495. MaskedOcclusionCulling::CullingResult::VISIBLE)
  496. {
  497. anyVisible = true;
  498. }
  499. }
  500. if (anyVisible)
  501. {
  502. view.SetMaskedOcclusionCullingDirty(true);
  503. }
  504. }
  505. #endif
  506. }
  507. void CullingScene::ProcessCullables(const Scene& scene, View& view, AZ::Job* parentJob, AZ::TaskGraph* taskGraph, AZ::TaskGraphEvent* taskGraphEvent)
  508. {
  509. AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullables() - %s", view.GetName().GetCStr());
  510. AZ_Assert(parentJob != nullptr || taskGraph != nullptr, "ProcessCullables must have either a valid parent job or a valid task graph");
  511. const Matrix4x4& worldToClip = view.GetWorldToClipMatrix();
  512. AZ::Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip);
  513. void* maskedOcclusionCulling = nullptr;
  514. ProcessCullablesCommon(scene, view, frustum, maskedOcclusionCulling);
  515. AZStd::shared_ptr<WorkListType> worklist = AZStd::make_shared<WorkListType>();
  516. worklist->Init();
  517. AZStd::shared_ptr<WorklistData> worklistData = MakeWorklistData(m_debugCtx, scene, view, frustum, maskedOcclusionCulling, parentJob, taskGraphEvent);
  518. static const AZ::TaskDescriptor descriptor{ "AZ::RPI::ProcessWorklist", "Graphics" };
  519. if (const Matrix4x4* worldToClipExclude = view.GetWorldToClipExcludeMatrix())
  520. {
  521. worklistData->m_hasExcludeFrustum = true;
  522. worklistData->m_excludeFrustum = Frustum::CreateFromMatrixColumnMajor(*worldToClipExclude);
  523. // Get the render pipeline associated with the shadow pass of the given view
  524. RenderPipelinePtr renderPipeline = scene.GetRenderPipeline(view.GetShadowPassRenderPipelineId());
  525. //Only apply this optimization if you only have one view available.
  526. if (renderPipeline && renderPipeline->GetViews(renderPipeline->GetMainViewTag()).size() == 1)
  527. {
  528. RPI::ViewPtr cameraView = renderPipeline->GetDefaultView();
  529. const Matrix4x4& cameraWorldToClip = cameraView->GetWorldToClipMatrix();
  530. worklistData->m_cameraFrustum = Frustum::CreateFromMatrixColumnMajor(cameraWorldToClip);
  531. worklistData->m_applyCameraFrustumIntersectionTest = true;
  532. }
  533. }
  534. auto nodeVisitorLambda = [worklistData, taskGraph, parentJob, &worklist](const AzFramework::IVisibilityScene::NodeData& nodeData) -> void
  535. {
  536. // For shadow cascades that are greater than index 0 we can do another check to see if we can reject any Octree node that do not
  537. // intersect with the camera frustum. We do this by checking for an overlap between the camera frustum and the Obb created
  538. // from the node's AABB but rotated and extended towards light direction. This optimization is only activated when someone sets
  539. // a non-negative extrusion value (i.e r_shadowCascadeExtrusionAmount) for their given content.
  540. if (r_shadowCascadeExtrusionAmount >= 0 && worklistData->m_applyCameraFrustumIntersectionTest && worklistData->m_hasExcludeFrustum)
  541. {
  542. // Build an Obb from the Octree node's aabb
  543. AZ::Obb extrudedBounds = AZ::Obb::CreateFromAabb(nodeData.m_bounds);
  544. // Rotate the Obb in the direction of the light
  545. AZ::Quaternion directionalLightRot = worklistData->m_view->GetCameraTransform().GetRotation();
  546. extrudedBounds.SetRotation(directionalLightRot);
  547. AZ::Vector3 halfLength = 0.5f * nodeData.m_bounds.GetExtents();
  548. // After converting AABB to OBB we apply a rotation and this can incorrectly fail intersection test. If you have an OBB cube built from an octree node,
  549. // rotating it can cause it to not encapsulate meshes it encapsulated beforehand. The type of shape we want here is essentially a capsule that starts from the
  550. // light and wraps the aabb of the octree node cube and extends towards light direction. This capsule's diameter needs to the size of the body diagonal
  551. // of the cube. Since using capsule shape will make intersection test expensive we simply expand the Obb to have each side be at least the size of the body diagonal
  552. // which is sqrt(3) * side size. Hence we expand the Obb by 73%. Since this is half length, we expand it by 73% / 2, or 36.5%.
  553. halfLength *= Vector3(1.365f);
  554. // Next we extrude the Obb in the direction of the light in order to ensure we capture meshes that are behind the camera but cast a shadow within it's frustum
  555. halfLength.SetY(halfLength.GetY() + r_shadowCascadeExtrusionAmount);
  556. extrudedBounds.SetHalfLengths(halfLength);
  557. if (!AZ::ShapeIntersection::Overlaps(worklistData->m_cameraFrustum, extrudedBounds))
  558. {
  559. return;
  560. }
  561. }
  562. auto entriesInNode = nodeData.m_entries.size();
  563. AZ_Assert(entriesInNode > 0, "should not get called with 0 entries");
  564. // Check job spawn condition for entries
  565. bool spawnJob = r_useEntryCountForNodeJobs && (worklist->m_entryCount > 0) &&
  566. ((worklist->m_entryCount + entriesInNode) > r_numEntriesPerCullingJob);
  567. // Check job spawn condition for nodes
  568. spawnJob = spawnJob || (worklist->m_nodes.size() == worklist->m_nodes.capacity());
  569. if (spawnJob)
  570. {
  571. // capture worklistData & worklist by value
  572. auto processWorklist = [worklistData, worklist]()
  573. {
  574. ProcessWorklist(worklistData, *worklist);
  575. };
  576. if (taskGraph != nullptr)
  577. {
  578. taskGraph->AddTask(descriptor, [worklistData, worklist]()
  579. {
  580. ProcessWorklist(worklistData, *worklist);
  581. });
  582. }
  583. else
  584. {
  585. //Kick off a job to process the (full) worklist
  586. AZ::Job* job = AZ::CreateJobFunction(processWorklist, true);
  587. parentJob->SetContinuation(job);
  588. job->Start();
  589. }
  590. worklist = AZStd::make_shared<WorkListType>();
  591. worklist->Init();
  592. }
  593. worklist->m_nodes.emplace_back(AZStd::move(nodeData));
  594. worklist->m_entryCount += u32(entriesInNode);
  595. };
  596. if (m_debugCtx.m_enableFrustumCulling)
  597. {
  598. if (worklistData->m_hasExcludeFrustum)
  599. {
  600. m_visScene->Enumerate(frustum, worklistData->m_excludeFrustum, nodeVisitorLambda);
  601. }
  602. else
  603. {
  604. m_visScene->Enumerate(frustum, nodeVisitorLambda);
  605. }
  606. }
  607. else
  608. {
  609. m_visScene->EnumerateNoCull(nodeVisitorLambda);
  610. }
  611. if (worklist->m_nodes.size() > 0)
  612. {
  613. // capture worklistData & worklist by value
  614. auto processWorklist = [worklistData, worklist]()
  615. {
  616. ProcessWorklist(worklistData, *worklist);
  617. };
  618. if (taskGraph != nullptr)
  619. {
  620. taskGraph->AddTask(descriptor, AZStd::move(processWorklist));
  621. }
  622. else
  623. {
  624. //Kick off a job to process the (full) worklist
  625. AZ::Job* job = AZ::CreateJobFunction(AZStd::move(processWorklist), true);
  626. parentJob->SetContinuation(job);
  627. job->Start();
  628. }
  629. }
  630. }
  631. // Fastest of the three functions: ProcessCullablesJobsEntries, ProcessCullablesJobsNodes, ProcessCullablesTG
  632. void CullingScene::ProcessCullablesJobsEntries(const Scene& scene, View& view, AZ::Job* parentJob)
  633. {
  634. AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesJobsEntries() - %s", view.GetName().GetCStr());
  635. const Matrix4x4& worldToClip = view.GetWorldToClipMatrix();
  636. AZ::Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip);
  637. void* maskedOcclusionCulling = nullptr;
  638. ProcessCullablesCommon(scene, view, frustum, maskedOcclusionCulling);
  639. // Note 1: Cannot do unique_ptr here because compilation error (auto-deletes function from lambda which the job code complains about)
  640. // Note 2: Having this be a pointer (even a shared pointer) is faster than just having this live on the stack like:
  641. // EntryListType entryList;
  642. // Why isn't immediately clear (did profile several times and noticed the difference of ~0.2-0.3ms, seems making it a stack variable
  643. // increases the runtime for this function, which runs on a single thread and spawns other jobs).
  644. AZStd::shared_ptr<EntryListType> entryList = AZStd::make_shared<EntryListType>();
  645. entryList->m_entries.reserve(r_numEntriesPerCullingJob);
  646. AZStd::shared_ptr<WorklistData> worklistData = MakeWorklistData(m_debugCtx, scene, view, frustum, maskedOcclusionCulling, parentJob, nullptr);
  647. if (const Matrix4x4* worldToClipExclude = view.GetWorldToClipExcludeMatrix())
  648. {
  649. worklistData->m_hasExcludeFrustum = true;
  650. worklistData->m_excludeFrustum = Frustum::CreateFromMatrixColumnMajor(*worldToClipExclude);
  651. }
  652. auto nodeVisitorLambda = [worklistData, parentJob, &entryList](const AzFramework::IVisibilityScene::NodeData& nodeData) -> void
  653. {
  654. AZ_Assert(nodeData.m_entries.size() > 0, "should not get called with 0 entries");
  655. AZ_Assert(entryList->m_entries.size() < entryList->m_entries.capacity(), "we should always have room to push a node on the queue");
  656. u32 remainingCount = u32(nodeData.m_entries.size());
  657. u32 current = 0;
  658. while (remainingCount > 0)
  659. {
  660. u32 availableCount = u32(entryList->m_entries.capacity() - entryList->m_entries.size());
  661. u32 addCount = AZStd::min(availableCount, remainingCount);
  662. for (u32 i = 0; i < addCount; ++i)
  663. {
  664. entryList->m_entries.push_back(nodeData.m_entries[current++]);
  665. }
  666. remainingCount -= addCount;
  667. if (entryList->m_entries.size() == entryList->m_entries.capacity())
  668. {
  669. auto processWorklist = [worklistData, entryList = AZStd::move(entryList)]()
  670. {
  671. ProcessEntrylist(worklistData, entryList->m_entries);
  672. };
  673. AZ::Job* job = AZ::CreateJobFunction(processWorklist, true);
  674. entryList = AZStd::make_shared<EntryListType>();
  675. entryList->m_entries.reserve(r_numEntriesPerCullingJob);
  676. parentJob->SetContinuation(job);
  677. job->Start();
  678. }
  679. }
  680. };
  681. if (m_debugCtx.m_enableFrustumCulling)
  682. {
  683. m_visScene->Enumerate(frustum, nodeVisitorLambda);
  684. }
  685. else
  686. {
  687. m_visScene->EnumerateNoCull(nodeVisitorLambda);
  688. }
  689. if (entryList->m_entries.size() > 0)
  690. {
  691. auto processWorklist = [worklistData, entryList = AZStd::move(entryList)]()
  692. {
  693. ProcessEntrylist(worklistData, entryList->m_entries);
  694. };
  695. AZ::Job* job = AZ::CreateJobFunction(processWorklist, true);
  696. parentJob->SetContinuation(job);
  697. job->Start();
  698. }
  699. }
  700. void CullingScene::ProcessCullablesJobs(const Scene& scene, View& view, AZ::Job& parentJob)
  701. {
  702. if (r_useEntryWorkListsForCulling)
  703. {
  704. ProcessCullablesJobsEntries(scene, view, &parentJob);
  705. }
  706. else
  707. {
  708. ProcessCullables(scene, view, &parentJob, nullptr);
  709. }
  710. }
  711. void CullingScene::ProcessCullablesTG(const Scene& scene, View& view, AZ::TaskGraph& taskGraph, AZ::TaskGraphEvent& taskGraphEvent)
  712. {
  713. ProcessCullables(scene, view, nullptr, &taskGraph, &taskGraphEvent);
  714. }
  715. uint32_t AddLodDataToView(
  716. const Vector3& pos, const Cullable::LodData& lodData, RPI::View& view, AzFramework::VisibilityEntry::TypeFlags typeFlags)
  717. {
  718. #ifdef AZ_CULL_PROFILE_DETAILED
  719. AZ_PROFILE_SCOPE(RPI, "AddLodDataToView");
  720. #endif
  721. uint32_t numVisibleDrawPackets = 0;
  722. auto addLodToDrawPacket = [&](const Cullable::LodData::Lod& lod)
  723. {
  724. #ifdef AZ_CULL_PROFILE_VERBOSE
  725. AZ_PROFILE_SCOPE(RPI, "add draw packets: %zu", lod.m_drawPackets.size());
  726. #endif
  727. numVisibleDrawPackets += static_cast<uint32_t>(lod.m_drawPackets.size()); //don't want to pay the cost of aznumeric_cast<> here so using static_cast<> instead
  728. if (typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  729. {
  730. view.AddVisibleObject(lod.m_visibleObjectUserData, pos);
  731. }
  732. else if (typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable)
  733. {
  734. for (const RHI::DrawPacket* drawPacket : lod.m_drawPackets)
  735. {
  736. view.AddDrawPacket(drawPacket, pos);
  737. }
  738. }
  739. else
  740. {
  741. AZ_Assert(false, "Invalid cullable type flags.")
  742. }
  743. };
  744. switch (lodData.m_lodConfiguration.m_lodType)
  745. {
  746. case Cullable::LodType::SpecificLod:
  747. if (lodData.m_lodConfiguration.m_lodOverride < lodData.m_lods.size())
  748. {
  749. addLodToDrawPacket(
  750. lodData.m_lods.at(lodData.m_lodConfiguration.m_lodOverride));
  751. }
  752. break;
  753. case Cullable::LodType::ScreenCoverage:
  754. default:
  755. {
  756. const Matrix4x4& viewToClip = view.GetViewToClipMatrix();
  757. // the [1][1] element of a perspective projection matrix stores cot(FovY/2) (equal to
  758. // 2*nearPlaneDistance/nearPlaneHeight), which is used to determine the (vertical) projected size in screen space
  759. const float yScale = viewToClip.GetElement(1, 1);
  760. const bool isPerspective = viewToClip.GetElement(3, 3) == 0.f;
  761. const Vector3 cameraPos = view.GetViewToWorldMatrix().GetTranslation();
  762. const float approxScreenPercentage =
  763. ModelLodUtils::ApproxScreenPercentage(pos, lodData.m_lodSelectionRadius, cameraPos, yScale, isPerspective);
  764. for (uint32_t lodIndex = 0; lodIndex < static_cast<uint32_t>(lodData.m_lods.size()); ++lodIndex)
  765. {
  766. const Cullable::LodData::Lod& lod = lodData.m_lods[lodIndex];
  767. // Note that this supports overlapping lod ranges (to support cross-fading lods, for example)
  768. if (approxScreenPercentage >= lod.m_screenCoverageMin && approxScreenPercentage <= lod.m_screenCoverageMax)
  769. {
  770. addLodToDrawPacket(lod);
  771. }
  772. }
  773. break;
  774. }
  775. }
  776. return numVisibleDrawPackets;
  777. }
  778. void CullingScene::Activate(const Scene* parentScene)
  779. {
  780. m_parentScene = parentScene;
  781. m_visScene = parentScene->GetVisibilityScene();
  782. m_taskGraphActive = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
  783. if (auto* console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
  784. {
  785. // Start with default value
  786. int shadowCascadeExtrusionAmount = r_shadowCascadeExtrusionAmount;
  787. // Get the cvar value from settings registry
  788. console->GetCvarValue("r_shadowCascadeExtrusionAmount", shadowCascadeExtrusionAmount);
  789. // push the cvars value so anything in this dll can access it directly.
  790. console->PerformCommand(
  791. AZStd::string::format("r_shadowCascadeExtrusionAmount %i", shadowCascadeExtrusionAmount).c_str());
  792. }
  793. #ifdef AZ_CULL_DEBUG_ENABLED
  794. AZ_Assert(CountObjectsInScene() == 0, "The culling system should start with 0 entries in this scene.");
  795. #endif
  796. }
  797. void CullingScene::Deactivate()
  798. {
  799. #ifdef AZ_CULL_DEBUG_ENABLED
  800. AZ_Assert(CountObjectsInScene() == 0, "All culling entries must be removed from the scene before shutdown.");
  801. #endif
  802. m_visScene = nullptr;
  803. }
  804. void CullingScene::BeginCullingTaskGraph(const AZStd::vector<ViewPtr>& views)
  805. {
  806. AZ::TaskGraph taskGraph{ "RPI::Culling" };
  807. AZ::TaskDescriptor beginCullingDescriptor{"RPI_CullingScene_BeginCullingView", "Graphics"};
  808. for (auto& view : views)
  809. {
  810. taskGraph.AddTask(
  811. beginCullingDescriptor,
  812. [&view]()
  813. {
  814. AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCullingTaskGraph");
  815. view->BeginCulling();
  816. });
  817. }
  818. if (!taskGraph.IsEmpty())
  819. {
  820. AZ::TaskGraphEvent waitForCompletion{ "RPI::Culling Wait" };
  821. taskGraph.Submit(&waitForCompletion);
  822. waitForCompletion.Wait();
  823. }
  824. }
  825. void CullingScene::BeginCullingJobs(const AZStd::vector<ViewPtr>& views)
  826. {
  827. AZ::JobCompletion beginCullingCompletion;
  828. for (auto& view : views)
  829. {
  830. const auto cullingLambda = [&view]()
  831. {
  832. AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCullingJob");
  833. view->BeginCulling();
  834. };
  835. AZ::Job* cullingJob = AZ::CreateJobFunction(AZStd::move(cullingLambda), true, nullptr);
  836. cullingJob->SetDependent(&beginCullingCompletion);
  837. cullingJob->Start();
  838. }
  839. beginCullingCompletion.StartAndWaitForCompletion();
  840. }
  841. void CullingScene::BeginCulling(const AZStd::vector<ViewPtr>& views)
  842. {
  843. AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCulling");
  844. m_cullDataConcurrencyCheck.soft_lock();
  845. m_debugCtx.ResetCullStats();
  846. m_debugCtx.m_numCullablesInScene = GetNumCullables();
  847. m_taskGraphActive = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
  848. if(views.size() == 1) // avoid job overhead when only 1 job
  849. {
  850. views[0]->BeginCulling();
  851. }
  852. else if (m_taskGraphActive && m_taskGraphActive->IsTaskGraphActive())
  853. {
  854. BeginCullingTaskGraph(views);
  855. }
  856. else
  857. {
  858. BeginCullingJobs(views);
  859. }
  860. #ifdef AZ_CULL_DEBUG_ENABLED
  861. AuxGeomDrawPtr auxGeom;
  862. if (m_debugCtx.m_debugDraw)
  863. {
  864. auxGeom = AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(m_parentScene);
  865. AZ_Assert(auxGeom, "Invalid AuxGeomFeatureProcessorInterface");
  866. if (m_debugCtx.m_drawWorldCoordinateAxes)
  867. {
  868. DebugDrawWorldCoordinateAxes(auxGeom.get());
  869. }
  870. }
  871. {
  872. AZStd::lock_guard<AZStd::mutex> lockFrozenFrustums(m_debugCtx.m_frozenFrustumsMutex);
  873. if (m_debugCtx.m_freezeFrustums)
  874. {
  875. for (const ViewPtr& viewPtr : views)
  876. {
  877. auto iter = m_debugCtx.m_frozenFrustums.find(viewPtr.get());
  878. if (iter == m_debugCtx.m_frozenFrustums.end())
  879. {
  880. const Matrix4x4& worldToClip = viewPtr->GetWorldToClipMatrix();
  881. Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip, Frustum::ReverseDepth::True);
  882. m_debugCtx.m_frozenFrustums.insert({ viewPtr.get(), frustum });
  883. }
  884. }
  885. }
  886. else if(m_debugCtx.m_frozenFrustums.size() > 0)
  887. {
  888. m_debugCtx.m_frozenFrustums.clear();
  889. }
  890. }
  891. #endif
  892. }
  893. void CullingScene::EndCulling()
  894. {
  895. m_cullDataConcurrencyCheck.soft_unlock();
  896. }
  897. size_t CullingScene::CountObjectsInScene()
  898. {
  899. size_t numObjects = 0;
  900. m_visScene->EnumerateNoCull(
  901. [&numObjects](const AzFramework::IVisibilityScene::NodeData& nodeData)
  902. {
  903. for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries)
  904. {
  905. if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable ||
  906. visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  907. {
  908. ++numObjects;
  909. }
  910. }
  911. }
  912. );
  913. return numObjects;
  914. }
  915. } // namespace RPI
  916. } // namespace AZ