Culling.cpp 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  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/Casting/numeric_cast.h>
  17. #include <AzCore/Jobs/Job.h>
  18. #include <AzCore/Jobs/JobFunction.h>
  19. #include <AzCore/Math/MatrixUtils.h>
  20. #include <AzCore/Math/ShapeIntersection.h>
  21. #include <AzCore/Task/TaskGraph.h>
  22. #include <AzCore/std/parallel/lock.h>
  23. #include <AzCore/std/smart_ptr/unique_ptr.h>
  24. #include <AzFramework/Visibility/OcclusionBus.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. CullingDebugContext& CullingScene::GetDebugContext()
  136. {
  137. return m_debugCtx;
  138. }
  139. const AzFramework::IVisibilityScene* CullingScene::GetVisibilityScene() const
  140. {
  141. return m_visScene;
  142. }
  143. // Search for and return the entity context ID associated with the scene and connected to OcclusionRequestBus. If there is no
  144. // matching scene, return a null ID.
  145. static AzFramework::EntityContextId GetEntityContextIdForOcclusion(const AZ::RPI::Scene* scene)
  146. {
  147. // Active RPI scenes are registered with the AzFramework::SceneSystem using unique names.
  148. auto sceneSystem = AzFramework::SceneSystemInterface::Get();
  149. AZ_Assert(sceneSystem, "Attempting to retrieve the entity context ID for a scene before the scene system interface is ready.");
  150. AzFramework::EntityContextId resultId = AzFramework::EntityContextId::CreateNull();
  151. // Enumerate all scenes registered with the AzFramework::SceneSystem
  152. sceneSystem->IterateActiveScenes(
  153. [&](const AZStd::shared_ptr<AzFramework::Scene>& azScene)
  154. {
  155. // AzFramework::Scene uses "subsystems" bind arbitrary data. This is generally used to maintain an association between
  156. // AzFramework::Scene and AZ::RPI::Scene. We search for the AzFramework::Scene scene with a subsystem matching the input
  157. // scene pointer.
  158. AZ::RPI::ScenePtr* rpiScene = azScene->FindSubsystemInScene<AZ::RPI::ScenePtr>();
  159. if (rpiScene && (*rpiScene).get() == scene)
  160. {
  161. // Each scene should only be bound to one entity context for entities that will appear in the scene.
  162. AzFramework::EntityContext** entityContext =
  163. azScene->FindSubsystemInScene<AzFramework::EntityContext::SceneStorageType>();
  164. if (entityContext)
  165. {
  166. // Return if the entity context is valid and connected to OcclusionRequestBus
  167. const AzFramework::EntityContextId contextId = (*entityContext)->GetContextId();
  168. if (AzFramework::OcclusionRequestBus::HasHandlers(contextId))
  169. {
  170. resultId = contextId;
  171. return false; // Result found, returning
  172. }
  173. }
  174. }
  175. return true; // No match, continuing to search for containing scene.
  176. });
  177. return resultId;
  178. }
  179. struct WorklistData
  180. {
  181. CullingDebugContext* m_debugCtx = nullptr;
  182. const Scene* m_scene = nullptr;
  183. AzFramework::EntityContextId m_sceneEntityContextId;
  184. View* m_view = nullptr;
  185. Frustum m_frustum;
  186. Frustum m_cameraFrustum;
  187. Frustum m_excludeFrustum;
  188. AZ::Job* m_parentJob = nullptr;
  189. AZ::TaskGraphEvent* m_taskGraphEvent = nullptr;
  190. bool m_hasExcludeFrustum = false;
  191. bool m_applyCameraFrustumIntersectionTest = false;
  192. #ifdef AZ_CULL_DEBUG_ENABLED
  193. AuxGeomDrawPtr GetAuxGeomPtr()
  194. {
  195. if (m_debugCtx->m_debugDraw && (m_view->GetName() == m_debugCtx->m_currentViewSelectionName))
  196. {
  197. return AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(m_scene);
  198. }
  199. return nullptr;
  200. }
  201. #endif
  202. };
  203. static AZStd::shared_ptr<WorklistData> MakeWorklistData(
  204. CullingDebugContext& debugCtx,
  205. const Scene& scene,
  206. View& view,
  207. Frustum& frustum,
  208. AZ::Job* parentJob,
  209. AZ::TaskGraphEvent* taskGraphEvent)
  210. {
  211. AZStd::shared_ptr<WorklistData> worklistData = AZStd::make_shared<WorklistData>();
  212. worklistData->m_debugCtx = &debugCtx;
  213. worklistData->m_scene = &scene;
  214. worklistData->m_sceneEntityContextId = GetEntityContextIdForOcclusion(&scene);
  215. worklistData->m_view = &view;
  216. worklistData->m_frustum = frustum;
  217. worklistData->m_parentJob = parentJob;
  218. worklistData->m_taskGraphEvent = taskGraphEvent;
  219. return worklistData;
  220. }
  221. // Used to accumulate NodeData into lists to be handed off to jobs for processing
  222. struct WorkListType
  223. {
  224. void Init()
  225. {
  226. m_entryCount = 0;
  227. u32 reserveCount = r_useEntryCountForNodeJobs ? r_maxNodesWhenUsingEntryCount : r_numNodesPerCullingJob;
  228. m_nodes.reserve(reserveCount);
  229. }
  230. u32 m_entryCount = 0;
  231. AZStd::vector<AzFramework::IVisibilityScene::NodeData> m_nodes;
  232. };
  233. // Used to accumulate VisibilityEntry into lists to be handed off to jobs for processing
  234. struct EntryListType
  235. {
  236. AZStd::vector<AzFramework::VisibilityEntry*> m_entries;
  237. };
  238. static bool TestOcclusionCulling(
  239. const AZStd::shared_ptr<WorklistData>& worklistData, const AzFramework::VisibilityEntry* visibleEntry);
  240. static void ProcessEntrylist(
  241. const AZStd::shared_ptr<WorklistData>& worklistData,
  242. const AZStd::vector<AzFramework::VisibilityEntry*>& entries,
  243. bool parentNodeContainedInFrustum = false,
  244. s32 startIdx = 0,
  245. s32 endIdx = -1)
  246. {
  247. #ifdef AZ_CULL_DEBUG_ENABLED
  248. // These variable are only used for the gathering of debug information.
  249. uint32_t numDrawPackets = 0;
  250. uint32_t numVisibleCullables = 0;
  251. #endif
  252. endIdx = (endIdx == -1) ? s32(entries.size()) : endIdx;
  253. for (s32 i = startIdx; i < endIdx; ++i)
  254. {
  255. AzFramework::VisibilityEntry* visibleEntry = entries[i];
  256. if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable ||
  257. visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  258. {
  259. Cullable* c = static_cast<Cullable*>(visibleEntry->m_userData);
  260. if ((c->m_cullData.m_drawListMask & worklistData->m_view->GetDrawListMask()).none() ||
  261. c->m_cullData.m_hideFlags & worklistData->m_view->GetUsageFlags() ||
  262. c->m_isHidden)
  263. {
  264. continue;
  265. }
  266. if (!parentNodeContainedInFrustum)
  267. {
  268. IntersectResult res = ShapeIntersection::Classify(worklistData->m_frustum, c->m_cullData.m_boundingSphere);
  269. bool entryInFrustum = (res != IntersectResult::Exterior) && (res == IntersectResult::Interior || ShapeIntersection::Overlaps(worklistData->m_frustum, c->m_cullData.m_boundingObb));
  270. if (!entryInFrustum)
  271. {
  272. continue;
  273. }
  274. }
  275. if (worklistData->m_hasExcludeFrustum &&
  276. ShapeIntersection::Classify(worklistData->m_excludeFrustum, c->m_cullData.m_boundingSphere) == IntersectResult::Interior)
  277. {
  278. // Skip item contained in exclude frustum.
  279. continue;
  280. }
  281. if (TestOcclusionCulling(worklistData, visibleEntry))
  282. {
  283. // There are ways to write this without [[maybe_unused]], but they are brittle.
  284. // For example, using #else could cause a bug where the function's parameter
  285. // is changed in #ifdef but not in #else.
  286. [[maybe_unused]] const uint32_t drawPacketCount = AddLodDataToView(
  287. c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view, visibleEntry->m_typeFlags);
  288. c->m_isVisible = true;
  289. worklistData->m_view->ApplyFlags(c->m_flags);
  290. #ifdef AZ_CULL_DEBUG_ENABLED
  291. ++numVisibleCullables;
  292. numDrawPackets += drawPacketCount;
  293. #endif
  294. }
  295. }
  296. }
  297. #ifdef AZ_CULL_DEBUG_ENABLED
  298. AuxGeomDrawPtr auxGeomPtr = worklistData->GetAuxGeomPtr();
  299. if (auxGeomPtr)
  300. {
  301. //Draw bounds on individual objects
  302. if (worklistData->m_debugCtx->m_drawBoundingBoxes || worklistData->m_debugCtx->m_drawBoundingSpheres || worklistData->m_debugCtx->m_drawLodRadii)
  303. {
  304. for (AzFramework::VisibilityEntry* visibleEntry : entries)
  305. {
  306. if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable ||
  307. visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  308. {
  309. Cullable* c = static_cast<Cullable*>(visibleEntry->m_userData);
  310. if (worklistData->m_debugCtx->m_drawBoundingBoxes)
  311. {
  312. auxGeomPtr->DrawObb(c->m_cullData.m_boundingObb, Matrix3x4::Identity(),
  313. parentNodeContainedInFrustum ? Colors::Lime : Colors::Yellow, AuxGeomDraw::DrawStyle::Line);
  314. }
  315. if (worklistData->m_debugCtx->m_drawBoundingSpheres)
  316. {
  317. auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(), c->m_cullData.m_boundingSphere.GetRadius(),
  318. Color(0.5f, 0.5f, 0.5f, 0.3f), AuxGeomDraw::DrawStyle::Shaded);
  319. }
  320. if (worklistData->m_debugCtx->m_drawLodRadii)
  321. {
  322. auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(),
  323. c->m_lodData.m_lodSelectionRadius,
  324. Color(1.0f, 0.5f, 0.0f, 0.3f), RPI::AuxGeomDraw::DrawStyle::Shaded);
  325. }
  326. }
  327. }
  328. }
  329. }
  330. if (worklistData->m_debugCtx->m_enableStats)
  331. {
  332. CullingDebugContext::CullStats& cullStats = worklistData->m_debugCtx->GetCullStatsForView(worklistData->m_view);
  333. //no need for mutex here since these are all atomics
  334. cullStats.m_numVisibleDrawPackets += numDrawPackets;
  335. cullStats.m_numVisibleCullables += numVisibleCullables;
  336. ++cullStats.m_numJobs;
  337. }
  338. #endif
  339. }
  340. static void ProcessVisibilityNode(const AZStd::shared_ptr<WorklistData>& worklistData, const AzFramework::IVisibilityScene::NodeData& nodeData)
  341. {
  342. bool nodeIsContainedInFrustum = !worklistData->m_debugCtx->m_enableFrustumCulling || ShapeIntersection::Contains(worklistData->m_frustum, nodeData.m_bounds);
  343. s32 startIdx = 0, size = s32(nodeData.m_entries.size());
  344. const AZStd::vector<AzFramework::VisibilityEntry*>& entries = nodeData.m_entries;
  345. if (worklistData->m_taskGraphEvent)
  346. {
  347. static const AZ::TaskDescriptor descriptor{ "AZ::RPI::ProcessWorklist", "Graphics" };
  348. AZ::TaskGraph taskGraph{ "ProcessCullableEntries" };
  349. AZ::TaskGraphEvent taskGraphEvent{ "ProcessCullableEntries Wait" };
  350. while (r_useEntryCountForNodeJobs && (size - startIdx) > s32(r_numEntriesPerCullingJob))
  351. {
  352. taskGraph.AddTask(descriptor, [=, &entries]() -> void
  353. {
  354. ProcessEntrylist(worklistData, entries, nodeIsContainedInFrustum, startIdx, startIdx + r_numEntriesPerCullingJob);
  355. });
  356. startIdx += s32(r_numEntriesPerCullingJob);
  357. }
  358. if (!taskGraph.IsEmpty())
  359. {
  360. taskGraph.Detach();
  361. taskGraph.Submit(worklistData->m_taskGraphEvent);
  362. }
  363. ProcessEntrylist(worklistData, nodeData.m_entries, nodeIsContainedInFrustum, startIdx, size);
  364. }
  365. else // Use job system
  366. {
  367. while (r_useEntryCountForNodeJobs && (size - startIdx) > s32(r_numEntriesPerCullingJob))
  368. {
  369. auto processEntries = [=, &entries]() -> void
  370. {
  371. ProcessEntrylist(worklistData, entries, nodeIsContainedInFrustum, startIdx, startIdx + r_numEntriesPerCullingJob);
  372. };
  373. AZ::Job* job = AZ::CreateJobFunction(AZStd::move(processEntries), true);
  374. worklistData->m_parentJob->SetContinuation(job);
  375. job->Start();
  376. startIdx += s32(r_numEntriesPerCullingJob);
  377. }
  378. ProcessEntrylist(worklistData, nodeData.m_entries, nodeIsContainedInFrustum, startIdx, size);
  379. }
  380. #ifdef AZ_CULL_DEBUG_ENABLED
  381. //Draw the node bounds
  382. // "Fully visible" nodes are nodes that are fully inside the frustum. "Partially visible" nodes intersect the edges of the frustum.
  383. // Since the nodes of an octree have lots of overlapping boxes with coplanar edges, it's easier to view these separately, so
  384. // we have a few debug booleans to toggle which ones to draw.
  385. AuxGeomDrawPtr auxGeomPtr = worklistData->GetAuxGeomPtr();
  386. if (auxGeomPtr)
  387. {
  388. if (nodeIsContainedInFrustum && worklistData->m_debugCtx->m_drawFullyVisibleNodes)
  389. {
  390. auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Lime, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off);
  391. }
  392. else if (!nodeIsContainedInFrustum && worklistData->m_debugCtx->m_drawPartiallyVisibleNodes)
  393. {
  394. auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Yellow, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off);
  395. }
  396. }
  397. #endif
  398. }
  399. static void ProcessWorklist(const AZStd::shared_ptr<WorklistData>& worklistData, const WorkListType& worklist)
  400. {
  401. AZ_PROFILE_SCOPE(RPI, "Culling: ProcessWorklist");
  402. AZ_Assert(worklist.m_nodes.size() > 0, "Received empty worklist in ProcessWorklist");
  403. for (const AzFramework::IVisibilityScene::NodeData& nodeData : worklist.m_nodes)
  404. {
  405. ProcessVisibilityNode(worklistData, nodeData);
  406. }
  407. }
  408. static bool TestOcclusionCulling(
  409. const AZStd::shared_ptr<WorklistData>& worklistData, const AzFramework::VisibilityEntry* visibleEntry)
  410. {
  411. #ifdef AZ_CULL_PROFILE_VERBOSE
  412. AZ_PROFILE_SCOPE(RPI, "TestOcclusionCulling");
  413. #endif
  414. if (visibleEntry->m_boundingVolume.Contains(worklistData->m_view->GetCameraTransform().GetTranslation()))
  415. {
  416. // camera is inside bounding volume
  417. return true;
  418. }
  419. // Perform occlusion tests using OcclusionRequestBus if it is connected to the entity context ID for this scene.
  420. if (!worklistData->m_sceneEntityContextId.IsNull())
  421. {
  422. AzFramework::OcclusionState state = AzFramework::OcclusionState::Unknown;
  423. const auto& viewName = worklistData->m_view->GetName();
  424. AzFramework::OcclusionRequestBus::Event(
  425. worklistData->m_sceneEntityContextId,
  426. [&](AzFramework::OcclusionRequestBus::Events* handler)
  427. {
  428. // An occlusion culling system might precompute visibility data for static objects or entities in a scene. If the
  429. // system that implements OcclusionRequestBus supports that behavior then we want to perform an initial visibility
  430. // test using the entity ID. This can avoid potentially more expensive dynamic tests, like those against an
  431. // occlusion buffer.
  432. if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable)
  433. {
  434. auto cullable = static_cast<RPI::Cullable*>(visibleEntry->m_userData);
  435. if (cullable && cullable->m_cullData.m_entityId.IsValid())
  436. {
  437. state = handler->GetOcclusionViewEntityVisibility(viewName, cullable->m_cullData.m_entityId);
  438. }
  439. }
  440. // Entries that don't meet the above criteria or return an inconclusive or partially visible state will perform a
  441. // dynamic, bounding box visibility test. One entity can have multiple visibility entries that may need to be tested
  442. // individually. If the entire entity is hidden, no further testing is required.
  443. if (state != AzFramework::OcclusionState::Hidden)
  444. {
  445. state = handler->GetOcclusionViewAabbVisibility(viewName, visibleEntry->m_boundingVolume);
  446. }
  447. });
  448. // Return immediately to bypass MaskedOcclusionCulling
  449. return state != AzFramework::OcclusionState::Hidden;
  450. }
  451. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  452. MaskedOcclusionCulling* maskedOcclusionCulling = worklistData->m_view->GetMaskedOcclusionCulling();
  453. if (!maskedOcclusionCulling || !worklistData->m_view->GetMaskedOcclusionCullingDirty())
  454. {
  455. return true;
  456. }
  457. const Vector3& minBound = visibleEntry->m_boundingVolume.GetMin();
  458. const Vector3& maxBound = visibleEntry->m_boundingVolume.GetMax();
  459. // compute bounding volume corners
  460. Vector4 corners[8];
  461. corners[0] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f);
  462. corners[1] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f);
  463. corners[2] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f);
  464. corners[3] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f);
  465. corners[4] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f);
  466. corners[5] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f);
  467. corners[6] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f);
  468. corners[7] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f);
  469. // find min clip-space depth and NDC min/max
  470. float minDepth = FLT_MAX;
  471. float ndcMinX = FLT_MAX;
  472. float ndcMinY = FLT_MAX;
  473. float ndcMaxX = -FLT_MAX;
  474. float ndcMaxY = -FLT_MAX;
  475. for (uint32_t index = 0; index < 8; ++index)
  476. {
  477. minDepth = AZStd::min(minDepth, corners[index].GetW());
  478. if (minDepth < 0.00000001f)
  479. {
  480. return true;
  481. }
  482. // convert to NDC
  483. corners[index] /= corners[index].GetW();
  484. ndcMinX = AZStd::min(ndcMinX, corners[index].GetX());
  485. ndcMinY = AZStd::min(ndcMinY, corners[index].GetY());
  486. ndcMaxX = AZStd::max(ndcMaxX, corners[index].GetX());
  487. ndcMaxY = AZStd::max(ndcMaxY, corners[index].GetY());
  488. }
  489. // test against the occlusion buffer, which contains only the manually placed occlusion planes
  490. if (maskedOcclusionCulling->TestRect(ndcMinX, ndcMinY, ndcMaxX, ndcMaxY, minDepth) !=
  491. MaskedOcclusionCulling::CullingResult::VISIBLE)
  492. {
  493. return false;
  494. }
  495. #endif
  496. return true;
  497. }
  498. void CullingScene::ProcessCullablesCommon(
  499. const Scene& scene,
  500. View& view,
  501. AZ::Frustum& frustum [[maybe_unused]])
  502. {
  503. AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesCommon() - %s", view.GetName().GetCStr());
  504. #ifdef AZ_CULL_DEBUG_ENABLED
  505. if (m_debugCtx.m_freezeFrustums)
  506. {
  507. AZStd::lock_guard<AZStd::mutex> lock(m_debugCtx.m_frozenFrustumsMutex);
  508. auto iter = m_debugCtx.m_frozenFrustums.find(&view);
  509. if (iter != m_debugCtx.m_frozenFrustums.end())
  510. {
  511. frustum = iter->second;
  512. }
  513. }
  514. if (m_debugCtx.m_debugDraw && m_debugCtx.m_drawViewFrustum && view.GetName() == m_debugCtx.m_currentViewSelectionName)
  515. {
  516. AuxGeomDrawPtr auxGeomPtr = AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(&scene);
  517. if (auxGeomPtr)
  518. {
  519. auxGeomPtr->DrawFrustum(frustum, AZ::Colors::White);
  520. }
  521. }
  522. if (m_debugCtx.m_enableStats)
  523. {
  524. CullingDebugContext::CullStats& cullStats = m_debugCtx.GetCullStatsForView(&view);
  525. cullStats.m_cameraViewToWorld = view.GetViewToWorldMatrix();
  526. }
  527. #endif //AZ_CULL_DEBUG_ENABLED
  528. // If connected, update the occlusion views for this scene and view combination.
  529. if (const auto& entityContextId = GetEntityContextIdForOcclusion(&scene); !entityContextId.IsNull())
  530. {
  531. AzFramework::OcclusionRequestBus::Event(
  532. entityContextId,
  533. &AzFramework::OcclusionRequestBus::Events::UpdateOcclusionView,
  534. view.GetName(),
  535. view.GetCameraTransform().GetTranslation(),
  536. view.GetWorldToClipMatrix());
  537. // Return immediately to bypass MaskedOcclusionCulling
  538. return;
  539. }
  540. #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED
  541. // setup occlusion culling, if necessary
  542. MaskedOcclusionCulling* maskedOcclusionCulling = view.GetMaskedOcclusionCulling();
  543. if (maskedOcclusionCulling && !m_occlusionPlanes.empty())
  544. {
  545. // frustum cull occlusion planes
  546. using VisibleOcclusionPlane = AZStd::pair<OcclusionPlane, float>;
  547. AZStd::vector<VisibleOcclusionPlane> visibleOccluders;
  548. visibleOccluders.reserve(m_occlusionPlanes.size());
  549. for (const auto& occlusionPlane : m_occlusionPlanes)
  550. {
  551. if (ShapeIntersection::Overlaps(frustum, occlusionPlane.m_aabb))
  552. {
  553. // occluder is visible, compute view space distance and add to list
  554. float depth = (view.GetWorldToViewMatrix() * occlusionPlane.m_aabb.GetMin()).GetZ();
  555. depth = AZStd::min(depth, (view.GetWorldToViewMatrix() * occlusionPlane.m_aabb.GetMax()).GetZ());
  556. visibleOccluders.emplace_back(occlusionPlane, depth);
  557. }
  558. }
  559. // sort the occlusion planes by view space distance, front-to-back
  560. AZStd::sort(visibleOccluders.begin(), visibleOccluders.end(), [](const VisibleOcclusionPlane& LHS, const VisibleOcclusionPlane& RHS)
  561. {
  562. return LHS.second > RHS.second;
  563. });
  564. bool anyVisible = false;
  565. for (const VisibleOcclusionPlane& occlusionPlane : visibleOccluders)
  566. {
  567. // convert to clip-space
  568. const Vector4 projectedBL = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerBL);
  569. const Vector4 projectedTL = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerTL);
  570. const Vector4 projectedTR = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerTR);
  571. const Vector4 projectedBR = view.GetWorldToClipMatrix() * Vector4(occlusionPlane.first.m_cornerBR);
  572. // store to float array
  573. float verts[16];
  574. projectedBL.StoreToFloat4(&verts[0]);
  575. projectedTL.StoreToFloat4(&verts[4]);
  576. projectedTR.StoreToFloat4(&verts[8]);
  577. projectedBR.StoreToFloat4(&verts[12]);
  578. static constexpr const uint32_t indices[6] = { 0, 1, 2, 2, 3, 0 };
  579. // render into the occlusion buffer, specifying BACKFACE_NONE so it functions as a double-sided occluder
  580. if (static_cast<MaskedOcclusionCulling*>(maskedOcclusionCulling)
  581. ->RenderTriangles(verts, indices, 2, nullptr, MaskedOcclusionCulling::BACKFACE_NONE) ==
  582. MaskedOcclusionCulling::CullingResult::VISIBLE)
  583. {
  584. anyVisible = true;
  585. }
  586. }
  587. if (anyVisible)
  588. {
  589. view.SetMaskedOcclusionCullingDirty(true);
  590. }
  591. }
  592. #endif
  593. }
  594. void CullingScene::ProcessCullables(const Scene& scene, View& view, AZ::Job* parentJob, AZ::TaskGraph* taskGraph, AZ::TaskGraphEvent* taskGraphEvent)
  595. {
  596. AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullables() - %s", view.GetName().GetCStr());
  597. AZ_Assert(parentJob != nullptr || taskGraph != nullptr, "ProcessCullables must have either a valid parent job or a valid task graph");
  598. const Matrix4x4& worldToClip = view.GetWorldToClipMatrix();
  599. AZ::Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip);
  600. ProcessCullablesCommon(scene, view, frustum);
  601. AZStd::shared_ptr<WorkListType> worklist = AZStd::make_shared<WorkListType>();
  602. worklist->Init();
  603. AZStd::shared_ptr<WorklistData> worklistData = MakeWorklistData(m_debugCtx, scene, view, frustum, parentJob, taskGraphEvent);
  604. static const AZ::TaskDescriptor descriptor{ "AZ::RPI::ProcessWorklist", "Graphics" };
  605. if (const Matrix4x4* worldToClipExclude = view.GetWorldToClipExcludeMatrix())
  606. {
  607. worklistData->m_hasExcludeFrustum = true;
  608. worklistData->m_excludeFrustum = Frustum::CreateFromMatrixColumnMajor(*worldToClipExclude);
  609. // Get the render pipeline associated with the shadow pass of the given view
  610. RenderPipelinePtr renderPipeline = scene.GetRenderPipeline(view.GetShadowPassRenderPipelineId());
  611. //Only apply this optimization if you only have one view available.
  612. if (renderPipeline && renderPipeline->GetViews(renderPipeline->GetMainViewTag()).size() == 1)
  613. {
  614. RPI::ViewPtr cameraView = renderPipeline->GetDefaultView();
  615. const Matrix4x4& cameraWorldToClip = cameraView->GetWorldToClipMatrix();
  616. worklistData->m_cameraFrustum = Frustum::CreateFromMatrixColumnMajor(cameraWorldToClip);
  617. worklistData->m_applyCameraFrustumIntersectionTest = true;
  618. }
  619. }
  620. auto nodeVisitorLambda = [worklistData, taskGraph, parentJob, &worklist](const AzFramework::IVisibilityScene::NodeData& nodeData) -> void
  621. {
  622. // 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
  623. // intersect with the camera frustum. We do this by checking for an overlap between the camera frustum and the Obb created
  624. // from the node's AABB but rotated and extended towards light direction. This optimization is only activated when someone sets
  625. // a non-negative extrusion value (i.e r_shadowCascadeExtrusionAmount) for their given content.
  626. if (r_shadowCascadeExtrusionAmount >= 0 && worklistData->m_applyCameraFrustumIntersectionTest && worklistData->m_hasExcludeFrustum)
  627. {
  628. // Build an Obb from the Octree node's aabb
  629. AZ::Obb extrudedBounds = AZ::Obb::CreateFromAabb(nodeData.m_bounds);
  630. // Rotate the Obb in the direction of the light
  631. AZ::Quaternion directionalLightRot = worklistData->m_view->GetCameraTransform().GetRotation();
  632. extrudedBounds.SetRotation(directionalLightRot);
  633. AZ::Vector3 halfLength = 0.5f * nodeData.m_bounds.GetExtents();
  634. // 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,
  635. // 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
  636. // 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
  637. // 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
  638. // 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%.
  639. halfLength *= Vector3(1.365f);
  640. // 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
  641. halfLength.SetY(halfLength.GetY() + r_shadowCascadeExtrusionAmount);
  642. extrudedBounds.SetHalfLengths(halfLength);
  643. if (!AZ::ShapeIntersection::Overlaps(worklistData->m_cameraFrustum, extrudedBounds))
  644. {
  645. return;
  646. }
  647. }
  648. auto entriesInNode = nodeData.m_entries.size();
  649. AZ_Assert(entriesInNode > 0, "should not get called with 0 entries");
  650. // Check job spawn condition for entries
  651. bool spawnJob = r_useEntryCountForNodeJobs && (worklist->m_entryCount > 0) &&
  652. ((worklist->m_entryCount + entriesInNode) > r_numEntriesPerCullingJob);
  653. // Check job spawn condition for nodes
  654. spawnJob = spawnJob || (worklist->m_nodes.size() == worklist->m_nodes.capacity());
  655. if (spawnJob)
  656. {
  657. // capture worklistData & worklist by value
  658. auto processWorklist = [worklistData, worklist]()
  659. {
  660. ProcessWorklist(worklistData, *worklist);
  661. };
  662. if (taskGraph != nullptr)
  663. {
  664. taskGraph->AddTask(descriptor, [worklistData, worklist]()
  665. {
  666. ProcessWorklist(worklistData, *worklist);
  667. });
  668. }
  669. else
  670. {
  671. //Kick off a job to process the (full) worklist
  672. AZ::Job* job = AZ::CreateJobFunction(processWorklist, true);
  673. parentJob->SetContinuation(job);
  674. job->Start();
  675. }
  676. worklist = AZStd::make_shared<WorkListType>();
  677. worklist->Init();
  678. }
  679. worklist->m_nodes.emplace_back(AZStd::move(nodeData));
  680. worklist->m_entryCount += u32(entriesInNode);
  681. };
  682. if (m_debugCtx.m_enableFrustumCulling)
  683. {
  684. if (worklistData->m_hasExcludeFrustum)
  685. {
  686. m_visScene->Enumerate(frustum, worklistData->m_excludeFrustum, nodeVisitorLambda);
  687. }
  688. else
  689. {
  690. m_visScene->Enumerate(frustum, nodeVisitorLambda);
  691. }
  692. }
  693. else
  694. {
  695. m_visScene->EnumerateNoCull(nodeVisitorLambda);
  696. }
  697. if (worklist->m_nodes.size() > 0)
  698. {
  699. // capture worklistData & worklist by value
  700. auto processWorklist = [worklistData, worklist]()
  701. {
  702. ProcessWorklist(worklistData, *worklist);
  703. };
  704. if (taskGraph != nullptr)
  705. {
  706. taskGraph->AddTask(descriptor, AZStd::move(processWorklist));
  707. }
  708. else
  709. {
  710. //Kick off a job to process the (full) worklist
  711. AZ::Job* job = AZ::CreateJobFunction(AZStd::move(processWorklist), true);
  712. parentJob->SetContinuation(job);
  713. job->Start();
  714. }
  715. }
  716. }
  717. // Fastest of the three functions: ProcessCullablesJobsEntries, ProcessCullablesJobsNodes, ProcessCullablesTG
  718. void CullingScene::ProcessCullablesJobsEntries(const Scene& scene, View& view, AZ::Job* parentJob)
  719. {
  720. AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesJobsEntries() - %s", view.GetName().GetCStr());
  721. const Matrix4x4& worldToClip = view.GetWorldToClipMatrix();
  722. AZ::Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip);
  723. ProcessCullablesCommon(scene, view, frustum);
  724. // Note 1: Cannot do unique_ptr here because compilation error (auto-deletes function from lambda which the job code complains about)
  725. // Note 2: Having this be a pointer (even a shared pointer) is faster than just having this live on the stack like:
  726. // EntryListType entryList;
  727. // Why isn't immediately clear (did profile several times and noticed the difference of ~0.2-0.3ms, seems making it a stack variable
  728. // increases the runtime for this function, which runs on a single thread and spawns other jobs).
  729. AZStd::shared_ptr<EntryListType> entryList = AZStd::make_shared<EntryListType>();
  730. entryList->m_entries.reserve(r_numEntriesPerCullingJob);
  731. AZStd::shared_ptr<WorklistData> worklistData = MakeWorklistData(m_debugCtx, scene, view, frustum, parentJob, nullptr);
  732. if (const Matrix4x4* worldToClipExclude = view.GetWorldToClipExcludeMatrix())
  733. {
  734. worklistData->m_hasExcludeFrustum = true;
  735. worklistData->m_excludeFrustum = Frustum::CreateFromMatrixColumnMajor(*worldToClipExclude);
  736. }
  737. auto nodeVisitorLambda = [worklistData, parentJob, &entryList](const AzFramework::IVisibilityScene::NodeData& nodeData) -> void
  738. {
  739. AZ_Assert(nodeData.m_entries.size() > 0, "should not get called with 0 entries");
  740. AZ_Assert(entryList->m_entries.size() < entryList->m_entries.capacity(), "we should always have room to push a node on the queue");
  741. u32 remainingCount = u32(nodeData.m_entries.size());
  742. u32 current = 0;
  743. while (remainingCount > 0)
  744. {
  745. u32 availableCount = u32(entryList->m_entries.capacity() - entryList->m_entries.size());
  746. u32 addCount = AZStd::min(availableCount, remainingCount);
  747. for (u32 i = 0; i < addCount; ++i)
  748. {
  749. entryList->m_entries.push_back(nodeData.m_entries[current++]);
  750. }
  751. remainingCount -= addCount;
  752. if (entryList->m_entries.size() == entryList->m_entries.capacity())
  753. {
  754. auto processWorklist = [worklistData, entryList = AZStd::move(entryList)]()
  755. {
  756. ProcessEntrylist(worklistData, entryList->m_entries);
  757. };
  758. AZ::Job* job = AZ::CreateJobFunction(processWorklist, true);
  759. entryList = AZStd::make_shared<EntryListType>();
  760. entryList->m_entries.reserve(r_numEntriesPerCullingJob);
  761. parentJob->SetContinuation(job);
  762. job->Start();
  763. }
  764. }
  765. };
  766. if (m_debugCtx.m_enableFrustumCulling)
  767. {
  768. m_visScene->Enumerate(frustum, nodeVisitorLambda);
  769. }
  770. else
  771. {
  772. m_visScene->EnumerateNoCull(nodeVisitorLambda);
  773. }
  774. if (entryList->m_entries.size() > 0)
  775. {
  776. auto processWorklist = [worklistData, entryList = AZStd::move(entryList)]()
  777. {
  778. ProcessEntrylist(worklistData, entryList->m_entries);
  779. };
  780. AZ::Job* job = AZ::CreateJobFunction(processWorklist, true);
  781. parentJob->SetContinuation(job);
  782. job->Start();
  783. }
  784. }
  785. void CullingScene::ProcessCullablesJobs(const Scene& scene, View& view, AZ::Job& parentJob)
  786. {
  787. if (r_useEntryWorkListsForCulling)
  788. {
  789. ProcessCullablesJobsEntries(scene, view, &parentJob);
  790. }
  791. else
  792. {
  793. ProcessCullables(scene, view, &parentJob, nullptr);
  794. }
  795. }
  796. void CullingScene::ProcessCullablesTG(const Scene& scene, View& view, AZ::TaskGraph& taskGraph, AZ::TaskGraphEvent& taskGraphEvent)
  797. {
  798. ProcessCullables(scene, view, nullptr, &taskGraph, &taskGraphEvent);
  799. }
  800. uint32_t AddLodDataToView(
  801. const Vector3& pos, const Cullable::LodData& lodData, RPI::View& view, AzFramework::VisibilityEntry::TypeFlags typeFlags)
  802. {
  803. #ifdef AZ_CULL_PROFILE_DETAILED
  804. AZ_PROFILE_SCOPE(RPI, "AddLodDataToView");
  805. #endif
  806. uint32_t numVisibleDrawPackets = 0;
  807. auto addLodToDrawPacket = [&](const Cullable::LodData::Lod& lod)
  808. {
  809. #ifdef AZ_CULL_PROFILE_VERBOSE
  810. AZ_PROFILE_SCOPE(RPI, "add draw packets: %zu", lod.m_drawPackets.size());
  811. #endif
  812. 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
  813. if (typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  814. {
  815. view.AddVisibleObject(lod.m_visibleObjectUserData, pos);
  816. }
  817. else if (typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable)
  818. {
  819. for (const RHI::DrawPacket* drawPacket : lod.m_drawPackets)
  820. {
  821. view.AddDrawPacket(drawPacket, pos);
  822. }
  823. }
  824. else
  825. {
  826. AZ_Assert(false, "Invalid cullable type flags.")
  827. }
  828. };
  829. switch (lodData.m_lodConfiguration.m_lodType)
  830. {
  831. case Cullable::LodType::SpecificLod:
  832. if (lodData.m_lodConfiguration.m_lodOverride < lodData.m_lods.size())
  833. {
  834. addLodToDrawPacket(
  835. lodData.m_lods.at(lodData.m_lodConfiguration.m_lodOverride));
  836. }
  837. break;
  838. case Cullable::LodType::ScreenCoverage:
  839. default:
  840. {
  841. const Matrix4x4& viewToClip = view.GetViewToClipMatrix();
  842. // the [1][1] element of a perspective projection matrix stores cot(FovY/2) (equal to
  843. // 2*nearPlaneDistance/nearPlaneHeight), which is used to determine the (vertical) projected size in screen space
  844. const float yScale = viewToClip.GetElement(1, 1);
  845. const bool isPerspective = viewToClip.GetElement(3, 3) == 0.f;
  846. const Vector3 cameraPos = view.GetViewToWorldMatrix().GetTranslation();
  847. const float approxScreenPercentage =
  848. ModelLodUtils::ApproxScreenPercentage(pos, lodData.m_lodSelectionRadius, cameraPos, yScale, isPerspective);
  849. for (uint32_t lodIndex = 0; lodIndex < static_cast<uint32_t>(lodData.m_lods.size()); ++lodIndex)
  850. {
  851. const Cullable::LodData::Lod& lod = lodData.m_lods[lodIndex];
  852. // Note that this supports overlapping lod ranges (to support cross-fading lods, for example)
  853. if (approxScreenPercentage >= lod.m_screenCoverageMin && approxScreenPercentage <= lod.m_screenCoverageMax)
  854. {
  855. addLodToDrawPacket(lod);
  856. }
  857. }
  858. break;
  859. }
  860. }
  861. return numVisibleDrawPackets;
  862. }
  863. void CullingScene::Activate(const Scene* parentScene)
  864. {
  865. m_parentScene = parentScene;
  866. m_visScene = parentScene->GetVisibilityScene();
  867. m_taskGraphActive = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
  868. if (auto* console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
  869. {
  870. // Start with default value
  871. int shadowCascadeExtrusionAmount = r_shadowCascadeExtrusionAmount;
  872. // Get the cvar value from settings registry
  873. console->GetCvarValue("r_shadowCascadeExtrusionAmount", shadowCascadeExtrusionAmount);
  874. // push the cvars value so anything in this dll can access it directly.
  875. console->PerformCommand(
  876. AZStd::string::format("r_shadowCascadeExtrusionAmount %i", shadowCascadeExtrusionAmount).c_str());
  877. }
  878. #ifdef AZ_CULL_DEBUG_ENABLED
  879. AZ_Assert(CountObjectsInScene() == 0, "The culling system should start with 0 entries in this scene.");
  880. #endif
  881. }
  882. void CullingScene::Deactivate()
  883. {
  884. #ifdef AZ_CULL_DEBUG_ENABLED
  885. AZ_Assert(CountObjectsInScene() == 0, "All culling entries must be removed from the scene before shutdown.");
  886. #endif
  887. m_visScene = nullptr;
  888. }
  889. void CullingScene::BeginCullingTaskGraph(const Scene& scene, AZStd::span<const ViewPtr> views)
  890. {
  891. AZ::TaskGraph taskGraph{ "RPI::Culling" };
  892. AZ::TaskDescriptor beginCullingDescriptor{ "RPI_CullingScene_BeginCullingView", "Graphics" };
  893. const auto& entityContextId = GetEntityContextIdForOcclusion(&scene);
  894. for (auto& view : views)
  895. {
  896. taskGraph.AddTask(
  897. beginCullingDescriptor,
  898. [&]()
  899. {
  900. AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCullingTaskGraph");
  901. view->BeginCulling();
  902. AzFramework::OcclusionRequestBus::Event(
  903. entityContextId, &AzFramework::OcclusionRequestBus::Events::CreateOcclusionView, view->GetName());
  904. });
  905. }
  906. if (!taskGraph.IsEmpty())
  907. {
  908. AZ::TaskGraphEvent waitForCompletion{ "RPI::Culling Wait" };
  909. taskGraph.Submit(&waitForCompletion);
  910. waitForCompletion.Wait();
  911. }
  912. }
  913. void CullingScene::BeginCullingJobs(const Scene& scene, AZStd::span<const ViewPtr> views)
  914. {
  915. AZ::JobCompletion beginCullingCompletion;
  916. const auto& entityContextId = GetEntityContextIdForOcclusion(&scene);
  917. for (auto& view : views)
  918. {
  919. const auto cullingLambda = [&]()
  920. {
  921. AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCullingJob");
  922. view->BeginCulling();
  923. AzFramework::OcclusionRequestBus::Event(
  924. entityContextId, &AzFramework::OcclusionRequestBus::Events::CreateOcclusionView, view->GetName());
  925. };
  926. AZ::Job* cullingJob = AZ::CreateJobFunction(AZStd::move(cullingLambda), true, nullptr);
  927. cullingJob->SetDependent(&beginCullingCompletion);
  928. cullingJob->Start();
  929. }
  930. beginCullingCompletion.StartAndWaitForCompletion();
  931. }
  932. void CullingScene::BeginCulling(const Scene& scene, AZStd::span<const ViewPtr> views)
  933. {
  934. AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCulling");
  935. m_cullDataConcurrencyCheck.soft_lock();
  936. m_debugCtx.ResetCullStats();
  937. m_debugCtx.m_numCullablesInScene = GetNumCullables();
  938. m_taskGraphActive = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
  939. // Remove any debug artifacts from the previous occlusion culling session.
  940. const auto& entityContextId = GetEntityContextIdForOcclusion(&scene);
  941. AzFramework::OcclusionRequestBus::Event(
  942. entityContextId, &AzFramework::OcclusionRequestBus::Events::ClearOcclusionViewDebugInfo);
  943. if (views.size() == 1) // avoid job overhead when only 1 job
  944. {
  945. views[0]->BeginCulling();
  946. AzFramework::OcclusionRequestBus::Event(
  947. entityContextId, &AzFramework::OcclusionRequestBus::Events::CreateOcclusionView, views[0]->GetName());
  948. }
  949. else if (m_taskGraphActive && m_taskGraphActive->IsTaskGraphActive())
  950. {
  951. BeginCullingTaskGraph(scene, views);
  952. }
  953. else
  954. {
  955. BeginCullingJobs(scene, views);
  956. }
  957. #ifdef AZ_CULL_DEBUG_ENABLED
  958. AuxGeomDrawPtr auxGeom;
  959. if (m_debugCtx.m_debugDraw)
  960. {
  961. auxGeom = AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(m_parentScene);
  962. AZ_Assert(auxGeom, "Invalid AuxGeomFeatureProcessorInterface");
  963. if (m_debugCtx.m_drawWorldCoordinateAxes)
  964. {
  965. DebugDrawWorldCoordinateAxes(auxGeom.get());
  966. }
  967. }
  968. {
  969. AZStd::lock_guard<AZStd::mutex> lockFrozenFrustums(m_debugCtx.m_frozenFrustumsMutex);
  970. if (m_debugCtx.m_freezeFrustums)
  971. {
  972. for (const ViewPtr& viewPtr : views)
  973. {
  974. auto iter = m_debugCtx.m_frozenFrustums.find(viewPtr.get());
  975. if (iter == m_debugCtx.m_frozenFrustums.end())
  976. {
  977. const Matrix4x4& worldToClip = viewPtr->GetWorldToClipMatrix();
  978. Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip, Frustum::ReverseDepth::True);
  979. m_debugCtx.m_frozenFrustums.insert({ viewPtr.get(), frustum });
  980. }
  981. }
  982. }
  983. else if(m_debugCtx.m_frozenFrustums.size() > 0)
  984. {
  985. m_debugCtx.m_frozenFrustums.clear();
  986. }
  987. }
  988. #endif
  989. }
  990. void CullingScene::EndCulling(const Scene& scene, AZStd::span<const ViewPtr> views)
  991. {
  992. m_cullDataConcurrencyCheck.soft_unlock();
  993. // When culling has completed, destroy all of the occlusion views.
  994. if (const auto& entityContextId = GetEntityContextIdForOcclusion(&scene); !entityContextId.IsNull())
  995. {
  996. for (auto& view : views)
  997. {
  998. AzFramework::OcclusionRequestBus::Event(
  999. entityContextId, &AzFramework::OcclusionRequestBus::Events::DestroyOcclusionView, view->GetName());
  1000. }
  1001. }
  1002. }
  1003. size_t CullingScene::CountObjectsInScene()
  1004. {
  1005. size_t numObjects = 0;
  1006. m_visScene->EnumerateNoCull(
  1007. [&numObjects](const AzFramework::IVisibilityScene::NodeData& nodeData)
  1008. {
  1009. for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries)
  1010. {
  1011. if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable ||
  1012. visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_VisibleObjectList)
  1013. {
  1014. ++numObjects;
  1015. }
  1016. }
  1017. }
  1018. );
  1019. return numObjects;
  1020. }
  1021. } // namespace RPI
  1022. } // namespace AZ