Browse Source

Light type culling with shader options (#12456)

This PR adds the ability for lights to cause shader options on meshes to be modified depending on which light types affect various meshes. This allows for meshes to use a shader which only includes code for the exact light types that actually affect the mesh. This was done in an abstract way so that other systems that want to affect mesh shader options within some bounds can do so. This functionality defaults to off and must be turned on with CVar (see below).

Adding ability for meshes to have boolean shader options to be set on them on a per-mesh basis. Other feature processors can request a bit for an arbitrary shader option, then set that bit on/off on meshes. When meshes detect a change in their shader options, they rebuild their draw packet with the appropriate shader option flags. Note, this bit is separate from the actual bit used by the shader for its shader options and is specific to the mesh feature processor.

Most of the light feature processors now hook into one or more shader options and can query meshes in the scene and mark them for the shader option needed to render that light type. This feature defaults to off but can be turned on by setting the CVar r_enablePerMeshShaderOptionFlags to true. The console function MeshFeatureProcessor.ReportShaderOptionFlags() can be used to report all shader option combinates used by various meshes in the scene. This list can then be used to create various shader variants that support the different options.

Added shader options which cover most light types and some of their more expensive features. Options were added for o_enableSphereLights, o_enableSphereLightShadows, o_enableDiskLights, o_enableDiskLightShadows, o_enableCapsuleLights, o_enableQuadLightLTC, o_enableQuadLightApprox, and o_enablePolygonLights. Simple point and spot lights don't have options because they are too cheap to be worth it. It's likely that some of the above options can be removed as we do more profiling since some of the light types aren't terribly expensive if they don't do shadows or LTC.

Added a new Hemisphere shape which supports collisions with Aabb and spheres, including unit tests. This is only the mathematical representation of a hemisphere; no hemisphere shape component is included. This was needed to create better bounds tests for disk, quad, and polygon lights.

Removed unneeded explicit assignment operators from Vector3/Vector4. These would interfere with the ability to use those classes in a union. Initially this change made use of a union, and while that's no longer the case it's still beneficial to remove this unneeded code.

Updated polygon lights to include the direction in the SRG instead of calculating it from the edges.

Added the ability to use the visitor pattern with the TagRegistry and TagBitRegistery (with unit tests).

Fixed the PreviewRenderer to respect the order feature processors are added to the Scene. This is important when some feature processors depend on others.
Ken Pruiksma 2 years ago
parent
commit
833532c74f
63 changed files with 1716 additions and 396 deletions
  1. 7 7
      Code/Framework/AzCore/AzCore/Math/Capsule.h
  2. 7 5
      Code/Framework/AzCore/AzCore/Math/Capsule.inl
  3. 45 0
      Code/Framework/AzCore/AzCore/Math/Hemisphere.h
  4. 64 0
      Code/Framework/AzCore/AzCore/Math/Hemisphere.inl
  5. 8 0
      Code/Framework/AzCore/AzCore/Math/IntersectSegment.cpp
  6. 9 0
      Code/Framework/AzCore/AzCore/Math/IntersectSegment.h
  7. 9 0
      Code/Framework/AzCore/AzCore/Math/ShapeIntersection.h
  8. 195 23
      Code/Framework/AzCore/AzCore/Math/ShapeIntersection.inl
  9. 0 2
      Code/Framework/AzCore/AzCore/Math/Vector3.h
  10. 0 8
      Code/Framework/AzCore/AzCore/Math/Vector3.inl
  11. 0 2
      Code/Framework/AzCore/AzCore/Math/Vector4.h
  12. 0 8
      Code/Framework/AzCore/AzCore/Math/Vector4.inl
  13. 2 0
      Code/Framework/AzCore/AzCore/azcore_files.cmake
  14. 58 0
      Code/Framework/AzCore/Tests/Math/HemisphereTests.cpp
  15. 221 0
      Code/Framework/AzCore/Tests/Math/ShapeIntersectionTests.cpp
  16. 1 0
      Code/Framework/AzCore/Tests/azcoretests_files.cmake
  17. 13 4
      Code/Framework/AzFramework/AzFramework/Visibility/IVisibilitySystem.h
  18. 28 0
      Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp
  19. 4 0
      Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.h
  20. 10 0
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/LightingOptions.azsli
  21. 13 11
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/CapsuleLight.azsli
  22. 14 13
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/DiskLight.azsli
  23. 14 13
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PointLight.azsli
  24. 8 14
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PolygonLight.azsli
  25. 15 13
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/QuadLight.azsli
  26. 3 1
      Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli
  27. 21 0
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/LightCommon.h
  28. 5 1
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/PolygonLightFeatureProcessorInterface.h
  29. 99 0
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshCommon.h
  30. 28 8
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
  31. 8 0
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h
  32. 2 0
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/MultiIndexedDataVector.h
  33. 53 13
      Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.cpp
  34. 6 2
      Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.h
  35. 118 39
      Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.cpp
  36. 7 2
      Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.h
  37. 57 17
      Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.cpp
  38. 5 2
      Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.h
  39. 63 35
      Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.cpp
  40. 8 5
      Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.h
  41. 79 16
      Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.cpp
  42. 8 2
      Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.h
  43. 12 15
      Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.cpp
  44. 19 17
      Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.h
  45. 15 16
      Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.cpp
  46. 4 1
      Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.h
  47. 149 9
      Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp
  48. 0 1
      Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp
  49. 8 7
      Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake
  50. 1 0
      Gems/Atom/Feature/Common/Code/atom_feature_common_public_files.cmake
  51. 15 1
      Gems/Atom/RHI/Code/Include/Atom/RHI/TagBitRegistry.h
  52. 19 1
      Gems/Atom/RHI/Code/Include/Atom/RHI/TagRegistry.h
  53. 55 0
      Gems/Atom/RHI/Code/Tests/TagRegistryTests.cpp
  54. 12 10
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Culling.h
  55. 4 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/MeshDrawPacket.h
  56. 9 4
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h
  57. 4 16
      Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp
  58. 62 23
      Gems/Atom/RPI/Code/Source/RPI.Public/MeshDrawPacket.cpp
  59. 7 2
      Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp
  60. 2 2
      Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/PreviewRenderer/PreviewerFeatureProcessorProviderBus.h
  61. 3 3
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp
  62. 1 1
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.h
  63. 0 1
      Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGrid.cpp

+ 7 - 7
Code/Framework/AzCore/AzCore/Math/Capsule.h

@@ -19,18 +19,16 @@ namespace AZ
     public:
         AZ_TYPE_INFO(Capsule, "{4B7E9154-B258-40D2-81A4-A27A25030588}");
 
-        //! Returns a capsule with uninitialized data members.
-        //! Many of the member functions are not safe to call until the data members have been initialized.
-        static Capsule CreateUninitialized();
+        Capsule() = default; // Creates an uninitialized Capsule. Will not be usable until its data is set.
 
         //! Constructs a Capsule from the centers of the two hemispherical caps (the order is arbitrary), and the radius.
         Capsule(const Vector3& firstHemisphereCenter, const Vector3& secondHemisphereCenter, float radius);
 
-        //! Constructs a Capsule using the ends of the provided LineSegment as the centers of the hemispherical caps, and using the provided
-        //! radius.
+        //! Constructs a Capsule using the ends of the provided LineSegment as the centers of the hemispherical caps, and
+        //! using the provided radius.
         Capsule(const LineSegment& lineSegment, float radius);
 
-        //! Gets the centre of the first hemispherical cap (the order of the caps is arbitrary).
+        //! Gets the center of the first hemispherical cap (the order of the caps is arbitrary).
         const Vector3& GetFirstHemisphereCenter() const;
 
         //! Gets the center of the second hemispherical cap (the order of the caps is arbitrary).
@@ -60,8 +58,10 @@ namespace AZ
         //! Returns whether this capsule is identical to another within the tolerance (allowing either order for the caps).
         bool IsClose(const Capsule& rhs, float tolerance = Constants::Tolerance) const;
 
+        //! Returns true if the point is inside or on the surface of the capsule.
+        bool Contains(const AZ::Vector3& point) const;
+
     private:
-        Capsule() = default;
 
         Vector3 m_firstHemisphereCenter; //!< The center of one of the hemispherical ends (order is interchangeable). 
         Vector3 m_secondHemisphereCenter; //!< The center of the other hemispherical end (order is interchangeable).

+ 7 - 5
Code/Framework/AzCore/AzCore/Math/Capsule.inl

@@ -6,13 +6,10 @@
  *
  */
 
+#include <AzCore/Math/IntersectSegment.h>
+
 namespace AZ
 {
-    AZ_MATH_INLINE Capsule Capsule::CreateUninitialized()
-    {
-        return Capsule();
-    }
-
     AZ_MATH_INLINE Capsule::Capsule(const Vector3& firstHemisphereCenter, const Vector3& secondHemisphereCenter, float radius)
         : m_firstHemisphereCenter(firstHemisphereCenter)
         , m_secondHemisphereCenter(secondHemisphereCenter)
@@ -83,5 +80,10 @@ namespace AZ
              (m_firstHemisphereCenter.IsClose(rhs.m_secondHemisphereCenter, tolerance) &&
               m_secondHemisphereCenter.IsClose(rhs.m_firstHemisphereCenter, tolerance)));
     }
+
+    AZ_MATH_INLINE bool Capsule::Contains(const AZ::Vector3& point) const
+    {
+        return Intersect::PointSegmentDistanceSq(point, m_firstHemisphereCenter, m_secondHemisphereCenter) <= m_radius * m_radius;
+    }
 } // namespace AZ
 

+ 45 - 0
Code/Framework/AzCore/AzCore/Math/Hemisphere.h

@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Math/Sphere.h>
+#include <AzCore/Math/Vector3.h>
+
+namespace AZ
+{
+    class Aabb;
+
+    //! A simple bounding hemisphere class for fast intersection testing.
+    class Hemisphere
+    {
+    public:
+        AZ_TYPE_INFO(Hemisphere, "{B246E336-780E-494B-B330-0985025B7888}");
+
+        Hemisphere() = default;
+        Hemisphere(const Vector3& center, float radius, const Vector3& normalizedDirection);
+
+        static Hemisphere CreateFromSphereAndDirection(const Sphere& sphere, const Vector3& normalizedDirection);
+
+        Vector3 GetCenter() const;
+        float GetRadius() const;
+        const Vector3& GetDirection() const;
+        void SetCenter(const Vector3& center);
+        void SetRadius(float radius);
+        void SetDirection(const Vector3& direction);
+
+        bool operator==(const Hemisphere& rhs) const;
+        bool operator!=(const Hemisphere& rhs) const;
+
+    private:
+        Vector4 m_centerRadius;
+        Vector3 m_direction;
+    };
+} // namespace AZ
+
+#include <AzCore/Math/Hemisphere.inl>

+ 64 - 0
Code/Framework/AzCore/AzCore/Math/Hemisphere.inl

@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Math/Aabb.h>
+
+namespace AZ
+{
+    AZ_MATH_INLINE Hemisphere::Hemisphere(const Vector3& center, float radius, const Vector3& normalizedDirection)
+        : m_centerRadius(AZ::Vector4(center, radius))
+        , m_direction(normalizedDirection)
+    {
+        AZ_MATH_ASSERT(normalizedDirection.IsNormalized(), "The direction is not normalized");
+    }
+
+    AZ_MATH_INLINE Hemisphere Hemisphere::CreateFromSphereAndDirection(const Sphere& sphere, const Vector3& normalizedDirection)
+    {
+        return Hemisphere(sphere.GetCenter(), sphere.GetRadius(), normalizedDirection);
+    }
+
+    AZ_MATH_INLINE Vector3 Hemisphere::GetCenter() const
+    {
+        return Vector3(m_centerRadius);
+    }
+
+    AZ_MATH_INLINE float Hemisphere::GetRadius() const
+    {
+        return m_centerRadius.GetW();
+    }
+
+    AZ_MATH_INLINE const Vector3& Hemisphere::GetDirection() const
+    {
+        return m_direction;
+    }
+
+    AZ_MATH_INLINE void Hemisphere::SetCenter(const Vector3& center)
+    {
+        m_centerRadius.Set(center, GetRadius());
+    }
+
+    AZ_MATH_INLINE void Hemisphere::SetRadius(float radius)
+    {
+        m_centerRadius.SetW(radius);
+    }
+
+    AZ_MATH_INLINE void Hemisphere::SetDirection(const Vector3& direction)
+    {
+        m_direction = direction;
+    }
+
+    AZ_MATH_INLINE bool Hemisphere::operator==(const Hemisphere& rhs) const
+    {
+        return (m_centerRadius == rhs.m_centerRadius) && (m_direction == rhs.m_direction);
+    }
+
+    AZ_MATH_INLINE bool Hemisphere::operator!=(const Hemisphere& rhs) const
+    {
+        return !(*this == rhs);
+    }
+}

+ 8 - 0
Code/Framework/AzCore/AzCore/Math/IntersectSegment.cpp

@@ -1656,6 +1656,14 @@ namespace AZ
         }
     }
 
+    float Intersect::PointSegmentDistanceSq(const Vector3& point, const Vector3& segmentStart, const Vector3& segmentEnd)
+    {
+        float proportion;
+        Vector3 closestPointOnCapsuleAxis;
+        Intersect::ClosestPointSegment(point, segmentStart, segmentEnd, proportion, closestPointOnCapsuleAxis);
+        return closestPointOnCapsuleAxis.GetDistanceSq(point);
+    }
+
 #if 0
 //////////////////////////////////////////////////////////////////////////
 // TEST AABB/RAY TEST using slopes

+ 9 - 0
Code/Framework/AzCore/AzCore/Math/IntersectSegment.h

@@ -476,6 +476,15 @@ namespace AZ
             const Vector3& segmentEnd,
             float& proportion,
             Vector3& closestPointOnSegment);
+
+        //! Calculate the distance squared from the provided point to the closest point on
+        //! segment segmentStart/segmentEnd.
+        //! @param point The point to test
+        //! @param segmentStart The start of the segment
+        //! @param segmentEnd The end of the segment
+        //! @return the distance squared from the point to the segment.
+        float PointSegmentDistanceSq(const Vector3& point, const Vector3& segmentStart, const Vector3& segmentEnd);
+
     } // namespace Intersect
 } // namespace AZ
 

+ 9 - 0
Code/Framework/AzCore/AzCore/Math/ShapeIntersection.h

@@ -11,6 +11,7 @@
 #include <AzCore/Math/Aabb.h>
 #include <AzCore/Math/Capsule.h>
 #include <AzCore/Math/Frustum.h>
+#include <AzCore/Math/Hemisphere.h>
 #include <AzCore/Math/Obb.h>
 #include <AzCore/Math/Plane.h>
 #include <AzCore/Math/Sphere.h>
@@ -32,18 +33,23 @@ namespace AZ
         //! Tests to see if Arg1 overlaps Arg2. Symmetric.
         //! @{
         bool Overlaps(const Aabb& aabb1, const Aabb& aabb2);
+        bool Overlaps(const Aabb& aabb, const Sphere& sphere);
         bool Overlaps(const Sphere& sphere, const Aabb& aabb);
         bool Overlaps(const Sphere& sphere, const Frustum& frustum);
         bool Overlaps(const Sphere& sphere, const Plane& plane);
         bool Overlaps(const Sphere& sphere1, const Sphere& sphere2);
         bool Overlaps(const Sphere& sphere, const Obb& obb);
         bool Overlaps(const Sphere& sphere, const Capsule& capsule);
+        bool Overlaps(const Hemisphere& hemisphere, const Sphere& sphere);
+        bool Overlaps(const Hemisphere& hemisphere, const Aabb& aabb); // Can have false positives for near intersections.
         bool Overlaps(const Frustum& frustum, const Sphere& sphere);
         bool Overlaps(const Frustum& frustum, const Obb& obb);
         bool Overlaps(const Frustum& frustum, const Aabb& aabb);
         bool Overlaps(const Capsule& capsule1, const Capsule& capsule2);
         bool Overlaps(const Capsule& capsule, const Obb& obb);
         bool Overlaps(const Capsule& capsule, const Sphere& sphere);
+        bool Overlaps(const Capsule& capsule, const Aabb& aabb);
+        bool Overlaps(const Aabb& aabb, const Capsule& capsule);
         bool Overlaps(const Obb& obb1, const Obb& obb2);
         bool Overlaps(const Obb& obb, const Capsule& capsule);
         bool Overlaps(const Obb& obb, const Sphere& sphere);
@@ -56,6 +62,9 @@ namespace AZ
         bool Contains(const Sphere& sphere,  const Aabb& aabb);
         bool Contains(const Sphere& sphere,  const Vector3& point);
         bool Contains(const Sphere& sphere1, const Sphere& sphere2);
+        bool Contains(const Hemisphere& hemisphere, const Aabb& aabb);
+        bool Contains(const Capsule& capsule, const Sphere& sphere);
+        bool Contains(const Capsule& capsule, const Aabb& aabb);
         bool Contains(const Frustum& frustum,  const Aabb& aabb);
         bool Contains(const Frustum& frustum,  const Sphere& sphere);
         bool Contains(const Frustum& frustum,  const Vector3& point);

+ 195 - 23
Code/Framework/AzCore/AzCore/Math/ShapeIntersection.inl

@@ -29,7 +29,6 @@ namespace AZ
             return false;
         }
 
-
         AZ_MATH_INLINE IntersectResult Classify(const Plane& plane, const Sphere& sphere)
         {
             float distance = plane.GetPointDist(sphere.GetCenter());
@@ -48,7 +47,6 @@ namespace AZ
             }
         }
 
-
         AZ_MATH_INLINE IntersectResult Classify(const Plane& plane, const Obb& obb)
         {
             // Compute the projection interval radius onto the plane normal, then compute the distance to the plane and see if it's outside the interval.
@@ -72,18 +70,20 @@ namespace AZ
             }
         }
 
-
         AZ_MATH_INLINE IntersectResult Classify(const Frustum& frustum, const Sphere& sphere)
         {
             return frustum.IntersectSphere(sphere);
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Aabb& aabb1, const Aabb& aabb2)
         {
             return aabb1.Overlaps(aabb2);
         }
 
+        AZ_MATH_INLINE bool Overlaps(const Aabb& aabb, const Sphere& sphere)
+        {
+            return Overlaps(sphere, aabb);
+        }
 
         AZ_MATH_INLINE bool Overlaps(const Sphere& sphere, const Aabb& aabb)
         {
@@ -92,39 +92,72 @@ namespace AZ
             return distSq <= radiusSq;
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Sphere& sphere, const Frustum& frustum)
         {
             return Overlaps(frustum, sphere);
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Sphere& sphere, const Plane& plane)
         {
             const float dist = plane.GetPointDist(sphere.GetCenter());
             return dist * dist <= sphere.GetRadius() * sphere.GetRadius();
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Sphere& sphere1, const Sphere& sphere2)
         {
             const float radiusSum = sphere1.GetRadius() + sphere2.GetRadius();
             return sphere1.GetCenter().GetDistanceSq(sphere2.GetCenter()) <= (radiusSum * radiusSum);
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Sphere& sphere, const Obb& obb)
         {
             const float radius = sphere.GetRadius();
             return obb.GetDistanceSq(sphere.GetCenter()) < radius * radius;
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Sphere& sphere, const Capsule& capsule)
         {
             return Overlaps(capsule, sphere);
         }
 
+        AZ_MATH_INLINE bool Overlaps(const Hemisphere& hemisphere, const Sphere& sphere)
+        {
+            float sphereDistanceToPlane = hemisphere.GetDirection().Dot(sphere.GetCenter() - hemisphere.GetCenter());
+
+            if (sphereDistanceToPlane >= 0.0f)
+            {
+                // Sphere is in front of hemisphere, so treat the hemisphere as a sphere
+                return Overlaps(Sphere(hemisphere.GetCenter(), hemisphere.GetRadius()), sphere);
+            }
+            else if (sphereDistanceToPlane > -sphere.GetRadius())
+            {
+                // Sphere is behind hemisphere, project the sphere onto the plane, then check radius of circle.
+                Vector3 projectedSphereCenter = sphere.GetCenter() + hemisphere.GetDirection() * -sphereDistanceToPlane;
+                float circleRadius = AZStd::sqrt(sphere.GetRadius() * sphere.GetRadius() - sphereDistanceToPlane * sphereDistanceToPlane);
+                const float radiusSum = hemisphere.GetRadius() + circleRadius;
+                return hemisphere.GetCenter().GetDistanceSq(projectedSphereCenter) < (radiusSum * radiusSum);
+            }
+            return false; // too far behind hemisphere to intersect
+        }
+
+        AZ_MATH_INLINE bool Overlaps(const Hemisphere& hemisphere, const Aabb& aabb)
+        {
+            float distSq = aabb.GetDistanceSq(hemisphere.GetCenter());
+            float radiusSq = hemisphere.GetRadius() * hemisphere.GetRadius();
+            if (distSq > radiusSq)
+            {
+                return false;
+            }
+
+            if (aabb.Contains(hemisphere.GetCenter()))
+            {
+                return true;
+            }
+
+            Vector3 nearestPointToPlane = aabb.GetSupport(-hemisphere.GetDirection());
+            bool abovePlane = hemisphere.GetDirection().Dot(hemisphere.GetCenter() - nearestPointToPlane) > 0.0f;
+            return !abovePlane; // This has false positives but is reasonably tight.
+        }
 
         AZ_MATH_INLINE bool Overlaps(const Frustum& frustum, const Sphere& sphere)
         {
@@ -138,7 +171,6 @@ namespace AZ
             return true;
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Frustum& frustum, const Aabb& aabb)
         {
             //For an AABB, extents.Dot(planeAbs) computes the projection interval radius of the AABB onto the plane normal.
@@ -162,7 +194,6 @@ namespace AZ
             return true;
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Frustum& frustum, const Obb& obb)
         {
             for (Frustum::PlaneId planeId = Frustum::PlaneId::Near; planeId < Frustum::PlaneId::MAX; ++planeId)
@@ -176,7 +207,6 @@ namespace AZ
             return true;
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Capsule& capsule1, const Capsule& capsule2)
         {
             Vector3 closestPointSegment1;
@@ -190,7 +220,6 @@ namespace AZ
             return closestPointSegment1.GetDistanceSq(closestPointSegment2) <= radiusSum * radiusSum;
         }
 
-
         AZ_MATH_INLINE bool Overlaps(const Capsule& capsule, const Sphere& sphere)
         {
             float proportion;
@@ -200,29 +229,119 @@ namespace AZ
             return closestPointOnCapsuleAxis.GetDistanceSq(sphere.GetCenter()) <= radiusSum * radiusSum;
         }
 
+        AZ_MATH_INLINE bool Overlaps(const Aabb& aabb, const Capsule& capsule)
+        {
+            return Overlaps(capsule, aabb);
+        }
+
+        AZ_MATH_INLINE bool Overlaps(const Capsule& capsule, const Aabb& aabb)
+        {
+            // First attempt a cheap rejection by comparing to the aabb's sphere.
+            Vector3 aabbSphereCenter;
+            float aabbSphereRadius;
+            aabb.GetAsSphere(aabbSphereCenter, aabbSphereRadius);
+            Sphere aabbSphere(aabbSphereCenter, aabbSphereRadius);
+            if (!Overlaps(capsule, aabbSphere))
+            {
+                return false;
+            }
+
+            // Now do the more expensive test. The idea is to start with the end points of the capsule then
+            // - Clamp the points with the aabb
+            // - Find the closest points on the line segment to the clamped points.
+            // - If the distance between the clamped point and the line segment point is less than the radius, then we know it intersects.
+            // - Generate new clamped points from the points on the line segment for next iteration.
+            // - If the two clamped points are equal to each other, or either of the new clamped points is equivalent to the previous clamped points,
+            //   then we know we've already found the closest point possible on the aabb, so fail because previous distance check failed.
+            // - Loop with new clamped points.
+
+            float capsuleRadiusSq = capsule.GetRadius() * capsule.GetRadius();
+            const Vector3& capsuleStart = capsule.GetFirstHemisphereCenter();
+            const Vector3& capsuleEnd = capsule.GetSecondHemisphereCenter();
+            const Vector3 capsuleSegment = capsuleEnd - capsuleStart;
+            if (capsuleSegment.IsClose(AZ::Vector3::CreateZero()))
+            {
+                // capsule is nearly a sphere, and already failed sphere check above.
+                return false;
+            }
+            const float segmentLengthSquared = capsuleSegment.Dot(capsuleSegment);
+            const float rcpSegmentLengthSquared = 1.0f / segmentLengthSquared;
+
+            Vector3 clampedPoint1 = capsuleStart.GetClamp(aabb.GetMin(), aabb.GetMax());
+            Vector3 clampedPoint2 = capsuleEnd.GetClamp(aabb.GetMin(), aabb.GetMax());
+
+            // Simplified from Intersect::ClosestPointSegment with certain parts pre-calculated, no need to return proportion.
+            auto getClosestPointOnCapsule = [&](const Vector3& point) -> Vector3
+            {
+                float proportion = (point - capsuleStart).Dot(capsuleSegment);
+                if (proportion <= 0.0f)
+                {
+                    return capsuleStart;
+                }
+                if (proportion >= segmentLengthSquared)
+                {
+                    return capsuleEnd;
+                }
+                return capsuleStart + (proportion * capsuleSegment * rcpSegmentLengthSquared);
+            };
+
+            constexpr uint32_t MaxIterations = 16;
+            for (uint32_t i = 0; i < MaxIterations; ++i)
+            {
+                // Check point 1
+                Vector3 closestPointOnCapsuleAxis1 = getClosestPointOnCapsule(clampedPoint1);
+                if (clampedPoint1.GetDistanceSq(closestPointOnCapsuleAxis1) < capsuleRadiusSq)
+                {
+                    return true;
+                }
+
+                // Check point 2
+                Vector3 closestPointOnCapsuleAxis2 = getClosestPointOnCapsule(clampedPoint2);
+                if (clampedPoint2.GetDistanceSq(closestPointOnCapsuleAxis2) < capsuleRadiusSq)
+                {
+                    return true;
+                }
+
+                // If the points are the same, and previous tests failed, then this is the best point, but it's too far away.
+                if (clampedPoint1.IsClose(clampedPoint2))
+                {
+                    return false;
+                }
+
+                // Choose better points.
+                Vector3 newclampedPoint1 = closestPointOnCapsuleAxis1.GetClamp(aabb.GetMin(), aabb.GetMax());
+                Vector3 newclampedPoint2 = closestPointOnCapsuleAxis2.GetClamp(aabb.GetMin(), aabb.GetMax());
+
+                if (newclampedPoint1.IsClose(clampedPoint1) || newclampedPoint2.IsClose(clampedPoint2))
+                {
+                    // Capsule is parallel to AABB or beyond the end points and failing above tests, so it must be outside the capsule.
+                    return false;
+                }
+
+                clampedPoint1 = newclampedPoint1;
+                clampedPoint2 = newclampedPoint2;
+            }
+
+            return true; // prefer false positive
+        }
 
         AZ_MATH_INLINE bool Contains(const Aabb& aabb1, const Aabb& aabb2)
         {
             return aabb1.Contains(aabb2);
         }
 
-
         AZ_MATH_INLINE bool Contains(const Aabb& aabb, const Sphere& sphere)
         {
             // Convert the sphere to an aabb
             return Contains(aabb, AZ::Aabb::CreateCenterRadius(sphere.GetCenter(), sphere.GetRadius()));
         }
 
-
         AZ_MATH_INLINE bool Contains(const Sphere& sphere, const Aabb& aabb)
         {
-            const float maxDistSq = sphere.GetCenter().GetDistanceSq(aabb.GetMax());
-            const float minDistSq = sphere.GetCenter().GetDistanceSq(aabb.GetMin());
             const float radiusSq = sphere.GetRadius() * sphere.GetRadius();
-            return maxDistSq <= radiusSq && minDistSq <= radiusSq;
+            return aabb.GetMaxDistanceSq(sphere.GetCenter()) <= radiusSq;
         }
 
-
         AZ_MATH_INLINE bool Contains(const Sphere& sphere, const Vector3& point)
         {
             const float distSq = sphere.GetCenter().GetDistanceSq(point);
@@ -230,13 +349,23 @@ namespace AZ
             return distSq <= radiusSq;
         }
 
-
         AZ_MATH_INLINE bool Contains(const Sphere& sphere1, const Sphere& sphere2)
         {
             const float radiusDiff = sphere1.GetRadius() - sphere2.GetRadius();
-            return sphere1.GetCenter().GetDistanceSq(sphere2.GetCenter()) <= (radiusDiff * radiusDiff);
+            return sphere1.GetCenter().GetDistanceSq(sphere2.GetCenter()) <= (radiusDiff * radiusDiff) * AZ::GetSign(radiusDiff);
         }
 
+        AZ_MATH_INLINE bool Contains(const Hemisphere& hemisphere, const Aabb& aabb)
+        {
+            const float radiusSq = hemisphere.GetRadius() * hemisphere.GetRadius();
+            if (aabb.GetMaxDistanceSq(hemisphere.GetCenter()) <= radiusSq)
+            {
+                // points are inside sphere, check to make sure it's on the right side of the hemisphere plane
+                Vector3 nearestPointToPlane = aabb.GetSupport(hemisphere.GetDirection());
+                return hemisphere.GetDirection().Dot(nearestPointToPlane - hemisphere.GetCenter()) >= 0.0f;
+            }
+            return false;
+        }
 
         AZ_MATH_INLINE bool Contains(const Frustum& frustum, const Aabb& aabb)
         {
@@ -257,7 +386,6 @@ namespace AZ
             return true;
         }
 
-
         AZ_MATH_INLINE bool Contains(const Frustum& frustum, const Sphere& sphere)
         {
             for (Frustum::PlaneId planeId = Frustum::PlaneId::Near; planeId < Frustum::PlaneId::MAX; ++planeId)
@@ -270,7 +398,6 @@ namespace AZ
             return true;
         }
 
-
         AZ_MATH_INLINE bool Contains(const Frustum& frustum, const Vector3& point)
         {
             for (Frustum::PlaneId planeId = Frustum::PlaneId::Near; planeId < Frustum::PlaneId::MAX; ++planeId)
@@ -282,5 +409,50 @@ namespace AZ
             }
             return true;
         }
+
+        AZ_MATH_INLINE bool Contains(const Capsule& capsule, const Sphere& sphere)
+        {
+            float proportion;
+            Vector3 closestPointOnCapsuleAxis;
+            Intersect::ClosestPointSegment(sphere.GetCenter(), capsule.GetFirstHemisphereCenter(), capsule.GetSecondHemisphereCenter(), proportion, closestPointOnCapsuleAxis);
+            return Contains(Sphere(closestPointOnCapsuleAxis, capsule.GetRadius()), sphere);
+        }
+
+        AZ_MATH_INLINE bool Contains(const Capsule& capsule, const Aabb& aabb)
+        {
+            AZ::Vector3 aabbSphereCenter;
+            float aabbSphereRadius;
+            aabb.GetAsSphere(aabbSphereCenter, aabbSphereRadius);
+            AZ::Sphere aabbSphere(aabbSphereCenter, aabbSphereRadius);
+
+            if (Contains(capsule, aabbSphere))
+            {
+                return true;
+            }
+            else if (!Overlaps(capsule, aabbSphere))
+            {
+                return false;
+            }
+
+            // Unable to determine with fast sphere based checks, so check each point in the aabb.
+            for (const AZ::Vector3& aabbPoint :
+                {
+                    aabb.GetMin(),
+                    aabb.GetMax(),
+                    AZ::Vector3(aabb.GetMin().GetX(), aabb.GetMin().GetY(), aabb.GetMax().GetZ()),
+                    AZ::Vector3(aabb.GetMin().GetX(), aabb.GetMax().GetY(), aabb.GetMin().GetZ()),
+                    AZ::Vector3(aabb.GetMin().GetX(), aabb.GetMax().GetY(), aabb.GetMax().GetZ()),
+                    AZ::Vector3(aabb.GetMax().GetX(), aabb.GetMin().GetY(), aabb.GetMin().GetZ()),
+                    AZ::Vector3(aabb.GetMax().GetX(), aabb.GetMin().GetY(), aabb.GetMax().GetZ()),
+                    AZ::Vector3(aabb.GetMax().GetX(), aabb.GetMax().GetY(), aabb.GetMin().GetZ()),
+                })
+            {
+                if (!capsule.Contains(aabbPoint))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
     }
 }

+ 0 - 2
Code/Framework/AzCore/AzCore/Math/Vector3.h

@@ -233,8 +233,6 @@ namespace AZ
         float GetMaxElement() const;
         float GetMinElement() const;
 
-        Vector3& operator=(const Vector3& rhs);
-
         Vector3 operator-() const;
         Vector3 operator+(const Vector3& rhs) const;
         Vector3 operator-(const Vector3& rhs) const;

+ 0 - 8
Code/Framework/AzCore/AzCore/Math/Vector3.inl

@@ -552,14 +552,6 @@ namespace AZ
         return AZStd::min<float>(m_x, AZStd::min<float>(m_y, m_z));
     }
 
-
-    AZ_MATH_INLINE Vector3& Vector3::operator=(const Vector3& rhs)
-    {
-        m_value = rhs.m_value;
-        return *this;
-    }
-
-
     AZ_MATH_INLINE Vector3 Vector3::operator-() const
     {
         return Vector3(Simd::Vec3::Sub(Simd::Vec3::ZeroFloat(), m_value));

+ 0 - 2
Code/Framework/AzCore/AzCore/Math/Vector4.h

@@ -242,8 +242,6 @@ namespace AZ
         //! Returns the homogenized vector, i.e. divides all components by w, return value is a Vector3.
         Vector3 GetHomogenized() const;
 
-        Vector4& operator=(const Vector4& rhs);
-
         Vector4 operator-() const;
         Vector4 operator+(const Vector4& rhs) const;
         Vector4 operator-(const Vector4& rhs) const;

+ 0 - 8
Code/Framework/AzCore/AzCore/Math/Vector4.inl

@@ -591,14 +591,6 @@ namespace AZ
         return Vector3(Simd::Vec3::Div(Simd::Vec4::ToVec3(m_value), divisor));
     }
 
-
-    AZ_MATH_INLINE Vector4& Vector4::operator=(const Vector4& rhs)
-    {
-        m_value = rhs.m_value;
-        return *this;
-    }
-
-
     AZ_MATH_INLINE Vector4 Vector4::operator-() const
     {
         return Vector4(Simd::Vec4::Sub(Simd::Vec4::ZeroFloat(), m_value));

+ 2 - 0
Code/Framework/AzCore/AzCore/azcore_files.cmake

@@ -281,6 +281,8 @@ set(FILES
     Math/Geometry3DUtils.cpp
     Math/Geometry3DUtils.h
     Math/Guid.h
+    Math/Hemisphere.h
+    Math/Hemisphere.inl
     Math/Internal/MathTypes.h
     Math/Internal/SimdMathVec1_neon.inl
     Math/Internal/SimdMathVec1_scalar.inl

+ 58 - 0
Code/Framework/AzCore/Tests/Math/HemisphereTests.cpp

@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Math/Hemisphere.h>
+#include <AzCore/UnitTest/TestTypes.h>
+#include <AZTestShared/Math/MathTestHelpers.h>
+
+namespace UnitTest
+{
+    TEST(MATH_Hemisphere, TestConstruct)
+    {
+        AZ::Vector3 pos = AZ::Vector3(10.0f, 10.0f, 10.0f);
+        float radius = 15.0f;
+        AZ::Vector3 direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
+
+        AZ::Hemisphere hemisphere(pos, radius, direction);
+        EXPECT_EQ(hemisphere.GetCenter(), pos);
+        EXPECT_EQ(hemisphere.GetRadius(), radius);
+        EXPECT_EQ(hemisphere.GetDirection(), direction);
+    }
+
+    TEST(MATH_Hemisphere, TestSet)
+    {
+        AZ::Vector3 pos = AZ::Vector3(10.0f, 10.0f, 10.0f);
+        float radius = 15.0f;
+        AZ::Vector3 direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
+
+        AZ::Hemisphere hemisphere;
+        hemisphere.SetCenter(pos);
+        hemisphere.SetRadius(radius);
+        hemisphere.SetDirection(direction);
+
+        EXPECT_EQ(hemisphere.GetCenter(), pos);
+        EXPECT_EQ(hemisphere.GetRadius(), radius);
+        EXPECT_EQ(hemisphere.GetDirection(), direction);
+    }
+
+    TEST(MATH_Hemisphere, TestAssignment)
+    {
+        AZ::Vector3 pos = AZ::Vector3(10.0f, 10.0f, 10.0f);
+        float radius = 15.0f;
+        AZ::Vector3 direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
+        AZ::Hemisphere hemisphere1(pos, radius, direction);
+
+        AZ::Vector3 newPos = AZ::Vector3(20.0f, 20.0f, 20.0f);
+        float newRadius = 25.0f;
+        AZ::Vector3 newDirection = AZ::Vector3(0.0f, 1.0f, 0.0f);
+        AZ::Hemisphere hemisphere2(newPos, newRadius, newDirection);
+
+        hemisphere1 = hemisphere2;
+        EXPECT_EQ(hemisphere1, hemisphere2);
+    }
+} // namespace UnitTest

+ 221 - 0
Code/Framework/AzCore/Tests/Math/ShapeIntersectionTests.cpp

@@ -801,4 +801,225 @@ namespace UnitTest
         EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(obb1, obb2));
         EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(obb2, obb1));
     }
+
+    TEST(MATH_ShapeIntersection, HemisphereVsSphere)
+    {
+        const AZ::Sphere unitSphere = AZ::Sphere::CreateUnitSphere();
+        const AZ::Sphere sphere1 = AZ::Sphere(AZ::Vector3(2.0f, 2.0f, 2.0f), 2.0f);
+
+        // hemisphere overlaps unit sphere, doesn't touch other sphere
+        const AZ::Hemisphere hemisphere0(AZ::Vector3(0.0f, 0.0f, 0.0f), 1.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere0, unitSphere));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere0, sphere1));
+
+        // hemisphere doesn't overlap unit sphere, but overlaps other sphere
+        const AZ::Hemisphere hemisphere1(AZ::Vector3(1.0f, 1.0f, 1.0f), 1.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere1, unitSphere));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere1, sphere1));
+
+        // hemisphere faces away from unit sphere but is still within range
+        const AZ::Hemisphere hemisphere2(AZ::Vector3(0.0f, 0.0f, -0.5f), 2.0f, AZ::Vector3(0.0f, 0.0f, -1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere2, unitSphere));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere2, sphere1));
+
+        // hemisphere faces away from unit sphere and off to the side but is still barely within range
+        const AZ::Hemisphere hemisphere3(AZ::Vector3(0.0f, 2.0f, -0.9f), 2.0f, AZ::Vector3(0.0f, 0.0f, -1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere3, unitSphere));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere3, sphere1));
+
+        // hemisphere faces away from unit sphere but is out of range.
+        const AZ::Hemisphere hemisphere4(AZ::Vector3(0.0f, 0.0f, -1.0f), 2.0f, AZ::Vector3(0.0f, 0.0f, -1.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere4, unitSphere));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere4, sphere1));
+
+        // hemisphere faces towards unit sphere and is in range
+        const AZ::Hemisphere hemisphere5(AZ::Vector3(0.0f, 0.0f, -1.5f), 1.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere5, unitSphere));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere5, sphere1));
+    }
+
+    TEST(MATH_ShapeIntersection, HemisphereVsAabb)
+    {
+        const AZ::Aabb unitAabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3(1.0f));
+
+        // hemisphere on top of aabb
+        const AZ::Hemisphere hemisphere0(AZ::Vector3(0.0f, 0.0f, 0.0f), 1.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere0, unitAabb));
+
+        // hemisphere just above aabb pointing away
+        const AZ::Hemisphere hemisphere1(AZ::Vector3(0.0f, 0.0f, 1.1f), 1.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere1, unitAabb));
+
+        // hemisphere just above aabb pointing towards
+        const AZ::Hemisphere hemisphere2(AZ::Vector3(0.0f, 0.0f, 1.1f), 1.0f, AZ::Vector3(0.0f, 0.0f, -1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere2, unitAabb));
+
+        // hemisphere farther above aabb pointing towards
+        const AZ::Hemisphere hemisphere3(AZ::Vector3(0.0f, 0.0f, 2.1f), 1.0f, AZ::Vector3(0.0f, 0.0f, -1.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(hemisphere3, unitAabb));
+
+        // hemisphere just above aabb pointing away, but at an angle so the plane of the hemisphere intersects
+        const AZ::Hemisphere hemisphere4(AZ::Vector3(0.0f, 0.0f, 1.1f), 1.0f, AZ::Vector3(0.0f, 1.0f, 1.0f).GetNormalized());
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere4, unitAabb));
+
+        // false positive case - hemisphere points away from aabb at an angle where the plane still intersects the aabb, but not near enough
+        // to the hemisphere to actually intersect. This typically happens when the hemisphere is much smaller than the aabb.
+        const AZ::Hemisphere hemisphere5(AZ::Vector3(0.0f, 0.0f, 1.2f), 0.3f, AZ::Vector3(0.0f, 1.0f, 1.0f).GetNormalized());
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(hemisphere5, unitAabb));
+
+    }
+
+    TEST(MATH_ShapeIntersection, HemisphereContainsAabb)
+    {
+        const AZ::Aabb unitAabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3(1.0f));
+
+        // hemisphere intersecting aabb, but not big enough
+        const AZ::Hemisphere hemisphere0(AZ::Vector3(0.0f, 0.0f, 0.0f), 2.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Contains(hemisphere0, unitAabb));
+
+        // hemisphere contains aabb
+        const AZ::Hemisphere hemisphere1(AZ::Vector3(0.0f, 0.0f, -1.0f), 3.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Contains(hemisphere1, unitAabb));
+
+        // aabb contains hemisphere
+        const AZ::Hemisphere hemisphere2(AZ::Vector3(0.0f, 0.0f, 0.0f), 0.5f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Contains(hemisphere2, unitAabb));
+
+        // just one point of aabb is outside
+        const AZ::Hemisphere hemisphere3(AZ::Vector3(1.0f, 1.0f, -1.0f), 3.0f, AZ::Vector3(0.0f, 0.0f, 1.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Contains(hemisphere3, unitAabb));
+    }
+
+    TEST(MATH_ShapeIntersection, CapsuleOverlapsAabb)
+    {
+        const AZ::Aabb unitAabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne());
+        AZ::Capsule capsule;
+
+        // Clear overlap (both shapes pass through origin)
+        capsule = AZ::Capsule(AZ::Vector3(-1.0f, 0.0f, 0.0f), AZ::Vector3(1.0f, 0.0f, 0.0f), 1.0f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+
+        // Clear no overlap
+        capsule = AZ::Capsule(AZ::Vector3(-10.0f, 0.0f, 0.0f), AZ::Vector3(-9.0, 0.0f, 0.0f), 1.0f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+
+        // Large capsule vs small aabb tests
+
+        // aabb completely contained in capsule
+        capsule = AZ::Capsule(AZ::Vector3(-10.0f, 0.0f, 0.0f), AZ::Vector3(10.0, 0.0f, 0.0f), 5.0f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+
+        // aabb near cap, slightly overlapping
+        capsule = AZ::Capsule(AZ::Vector3(-10.0f, 0.0f, 0.0f), AZ::Vector3(-5.9f, 0.0f, 0.0f), 5.0f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+        
+        // aabb near cap, but not overlapping
+        capsule = AZ::Capsule(AZ::Vector3(-10.0f, 0.0f, 0.0f), AZ::Vector3(-6.1f, 0.0f, 0.0f), 5.0f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+
+        // aabb near side, slightly overlapping
+        capsule = AZ::Capsule(AZ::Vector3(-10.0f, 5.9f, 0.0f), AZ::Vector3(10.0f, 5.9f, 0.0f), 5.0f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+
+        // aabb near side, not overlapping
+        capsule = AZ::Capsule(AZ::Vector3(-10.0f, 6.1f, 0.0f), AZ::Vector3(10.0f, 6.1f, 0.0f), 5.0f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+
+        // aabb diagonal, overlapping
+        capsule = AZ::Capsule(AZ::Vector3(-8.0f, -5.0f, 0.0f), AZ::Vector3(5.0f, 8.0f, 0.0f), 0.8f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+
+        // aabb diagonal, not overlapping
+        capsule = AZ::Capsule(AZ::Vector3(-8.0f, -5.0f, 0.0f), AZ::Vector3(5.0f, 8.0f, 0.0f), 0.7f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, unitAabb));
+    }
+
+    TEST(MATH_ShapeIntersection, CapsuleOverlapsCapsule)
+    {
+        AZ::Capsule capsule;
+        AZ::Capsule longCapsule = AZ::Capsule(AZ::Vector3(-10.0f, 0.0f, 0.0f), AZ::Vector3(10.0f, 0.0f, 0.0f), 1.0f);
+        AZ::Capsule shortCapsule = AZ::Capsule(AZ::Vector3(-0.5f, 0.0f, 0.0f), AZ::Vector3(0.5f, 0.0f, 0.0f), 1.0f);
+
+        // capsule overlaps self
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(longCapsule, longCapsule));
+
+        // narrow perpendicular capsule miss
+        capsule = AZ::Capsule(AZ::Vector3(0.0f, -10.0f, 2.0f), AZ::Vector3(0.0f, 10.0f, 2.0f), 0.5f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, longCapsule));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, shortCapsule));
+
+        // narrow perpendicular capsule hit
+        capsule = AZ::Capsule(AZ::Vector3(0.0f, -10.0f, 1.4f), AZ::Vector3(0.0f, 10.0f, 1.4f), 0.5f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, longCapsule));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, shortCapsule));
+
+        // wide perpendicular capsule miss
+        capsule = AZ::Capsule(AZ::Vector3(0.0f, -10.0f, 6.1f), AZ::Vector3(0.0f, 10.0f, 6.1), 5.0f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, longCapsule));
+        EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule, shortCapsule));
+
+        // wide perpendicular capsule hit
+        capsule = AZ::Capsule(AZ::Vector3(0.0f, -10.0f, 5.9f), AZ::Vector3(0.0f, 10.0f, 5.9f), 5.0f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, longCapsule));
+        EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule, shortCapsule));
+
+        // diagonal capsules
+        {
+            AZ::Capsule capsule1 = AZ::Capsule(AZ::Vector3(-10.0f, -10.0f, 0.0f), AZ::Vector3(10.0f, 10.0f, 0.0f), 1.0f);
+            AZ::Capsule capsule2 = AZ::Capsule(AZ::Vector3(10.0f, -10.0f, 1.0f), AZ::Vector3(-10.0f, 10.0f, 1.0f), 1.0f);
+            EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule1, capsule2));
+
+            AZ::Capsule capsule3 = AZ::Capsule(AZ::Vector3(-10.0f, -10.0f, 0.0f), AZ::Vector3(10.0f, 10.0f, 0.0f), 2.0f);
+            AZ::Capsule capsule4 = AZ::Capsule(AZ::Vector3(10.0f, -10.0f, 4.1f), AZ::Vector3(-10.0f, 10.0f, 4.1f), 2.0f);
+            EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule3, capsule4));
+        }
+
+        // parallel capsules, end point intersection
+        {
+            AZ::Capsule capsule1 = AZ::Capsule(AZ::Vector3(-10.0f, 0.0, 0.0f), AZ::Vector3(-1.0f, 0.0f, 0.0f), 1.1f);
+            AZ::Capsule capsule2 = AZ::Capsule(AZ::Vector3(1.0f, 0.0, 0.0f), AZ::Vector3(10.0f, 0.0f, 0.0f), 1.1f);
+            EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(capsule1, capsule2));
+
+            AZ::Capsule capsule3 = AZ::Capsule(AZ::Vector3(-10.0f, 0.0, 0.0f), AZ::Vector3(-1.0f, 0.0f, 0.0f), 0.9f);
+            AZ::Capsule capsule4 = AZ::Capsule(AZ::Vector3(1.0f, 0.0, 0.0f), AZ::Vector3(10.0f, 0.0f, 0.0f), 0.9f);
+            EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(capsule3, capsule4));
+        }
+    }
+
+    TEST(MATH_ShapeIntersection, CapsuleContainsSphere)
+    {
+        AZ::Capsule capsule = AZ::Capsule(AZ::Vector3(-10.0f, 0.0f, 0.0f), AZ::Vector3(10.0f, 0.0f, 0.0f), 1.0f);
+
+        AZ::Sphere sphere = AZ::Sphere(AZ::Vector3(0.0f, 0.0f, 0.0f), 1.0f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Contains(capsule, sphere));
+
+        sphere = AZ::Sphere(AZ::Vector3(-10.0f, 0.0f, 0.0f), 0.5f);
+        EXPECT_TRUE(AZ::ShapeIntersection::Contains(capsule, sphere));
+
+        sphere = AZ::Sphere(AZ::Vector3(10.0f, 0.0f, 0.0f), 1.5f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Contains(capsule, sphere));
+
+        sphere = AZ::Sphere(AZ::Vector3(10.0f, 10.0f, 0.0f), 0.5f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Contains(capsule, sphere));
+    }
+
+    TEST(MATH_ShapeIntersection, CapsuleContainsAabb)
+    {
+        AZ::Capsule capsule = AZ::Capsule(AZ::Vector3(-1.0f, 0.0f, 0.0f), AZ::Vector3(1.0f, 0.0f, 0.0f), 2.0f);
+        AZ::Aabb unitAabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne());
+
+        EXPECT_TRUE(AZ::ShapeIntersection::Contains(capsule, unitAabb));
+
+        capsule = AZ::Capsule(AZ::Vector3(-1.0f, 0.0f, 0.0f), AZ::Vector3(1.0f, 0.0f, 0.0f), 1.0f);
+        EXPECT_FALSE(AZ::ShapeIntersection::Contains(capsule, unitAabb));
+
+        // long thin aabb inside long capsule
+        capsule = AZ::Capsule(AZ::Vector3(-10.0f, 0.0f, 0.0f), AZ::Vector3(10.0f, 0.0f, 0.0f), 2.0f);
+        AZ::Aabb longAabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3(10.0f, 1.0f, 1.0f));
+        EXPECT_TRUE(AZ::ShapeIntersection::Contains(capsule, longAabb));
+
+        longAabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3(11.0f, 1.0f, 10.0f));
+        EXPECT_FALSE(AZ::ShapeIntersection::Contains(capsule, longAabb));
+    }
+
 } // namespace UnitTest

+ 1 - 0
Code/Framework/AzCore/Tests/azcoretests_files.cmake

@@ -116,6 +116,7 @@ set(FILES
     Math/CrcTestsCompileTimeLiterals.h
     Math/FrustumTests.cpp
     Math/FrustumPerformanceTests.cpp
+    Math/HemisphereTests.cpp
     Math/IntersectionPerformanceTests.cpp
     Math/IntersectionTestHelpers.cpp
     Math/IntersectionTestHelpers.h

+ 13 - 4
Code/Framework/AzFramework/AzFramework/Visibility/IVisibilitySystem.h

@@ -13,8 +13,10 @@
 #include <AzCore/RTTI/RTTI.h>
 #include <AzCore/EBus/EBus.h>
 #include <AzCore/Math/Aabb.h>
-#include <AzCore/Math/Sphere.h>
+#include <AzCore/Math/Capsule.h>
 #include <AzCore/Math/Frustum.h>
+#include <AzCore/Math/Hemisphere.h>
+#include <AzCore/Math/Sphere.h>
 #include <AzCore/Name/Name.h>
 #include <AzCore/Interface/Interface.h>
 #include <AzCore/std/containers/vector.h>
@@ -79,19 +81,26 @@ namespace AzFramework
         //! Intersects an axis aligned bounding box against the visibility system.
         //! @param aabb the axis aligned bounding box to test against
         //! @param callback the callback to invoke when a node is visible
-        //! @return the intersection result of the aabb against the visibility system
         virtual void Enumerate(const AZ::Aabb& aabb, const EnumerateCallback& callback) const = 0;
 
         //! Intersects a sphere against the visibility system.
         //! @param sphere the sphere to test against
         //! @param callback the callback to invoke when a node is visible
-        //! @return the intersection result of the sphere against the visibility system
         virtual void Enumerate(const AZ::Sphere& sphere, const EnumerateCallback& callback) const = 0;
 
+        //! Intersects a hemisphere against the visibility system.
+        //! @param hemisphere the hemisphere to test against
+        //! @param callback the callback to invoke when a node is visible
+        virtual void Enumerate(const AZ::Hemisphere& hemisphere, const EnumerateCallback& callback) const = 0;
+
+        //! Intersects a capsule against the visibility system.
+        //! @param capsule the capsule to test against
+        //! @param callback the callback to invoke when a node is visible
+        virtual void Enumerate(const AZ::Capsule& capsule, const EnumerateCallback& callback) const = 0;
+
         //! Intersects a frustum against the visibility system.
         //! @param frustum the frustum to test against
         //! @param callback the callback to invoke when a node is visible
-        //! @return the intersection result of the frustum against the visibility system
         virtual void Enumerate(const AZ::Frustum& frustum, const EnumerateCallback& callback) const = 0;
 
         //! Enumerate *all* OctreeNodes that have any entries in them (without any culling).

+ 28 - 0
Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp

@@ -161,6 +161,22 @@ namespace AzFramework
         }
     }
 
+    void OctreeNode::Enumerate(const AZ::Hemisphere& hemisphere, const IVisibilityScene::EnumerateCallback& callback) const
+    {
+        if (AZ::ShapeIntersection::Overlaps(hemisphere, m_bounds))
+        {
+            EnumerateHelper(hemisphere, callback);
+        }
+    }
+
+    void OctreeNode::Enumerate(const AZ::Capsule& capsule, const IVisibilityScene::EnumerateCallback& callback) const
+    {
+        if (AZ::ShapeIntersection::Overlaps(capsule, m_bounds))
+        {
+            EnumerateHelper(capsule, callback);
+        }
+    }
+
     void OctreeNode::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const
     {
         if (AZ::ShapeIntersection::Overlaps(frustum, m_bounds))
@@ -384,6 +400,18 @@ namespace AzFramework
         m_root.Enumerate(sphere, callback);
     }
 
+    void OctreeScene::Enumerate(const AZ::Hemisphere& hemisphere, const IVisibilityScene::EnumerateCallback& callback) const
+    {
+        AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
+        m_root.Enumerate(hemisphere, callback);
+    }
+
+    void OctreeScene::Enumerate(const AZ::Capsule & capsule, const IVisibilityScene::EnumerateCallback& callback) const
+    {
+        AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
+        m_root.Enumerate(capsule, callback);
+    }
+
     void OctreeScene::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const
     {
         AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);

+ 4 - 0
Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.h

@@ -51,6 +51,8 @@ namespace AzFramework
         //! @{
         void Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const;
         void Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const;
+        void Enumerate(const AZ::Hemisphere& hemisphere, const IVisibilityScene::EnumerateCallback& callback) const;
+        void Enumerate(const AZ::Capsule& capsule, const IVisibilityScene::EnumerateCallback& callback) const;
         void Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const;
         //! @}
 
@@ -106,6 +108,8 @@ namespace AzFramework
         void RemoveEntry(VisibilityEntry& entry) override;
         void Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const override;
         void Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const override;
+        void Enumerate(const AZ::Hemisphere& hemisphere, const IVisibilityScene::EnumerateCallback& callback) const override;
+        void Enumerate(const AZ::Capsule& capsule, const IVisibilityScene::EnumerateCallback& callback) const override;
         void Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const override;
         void EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const override;
         uint32_t GetEntryCount() const override;

+ 10 - 0
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/LightingOptions.azsli

@@ -64,7 +64,17 @@ option bool o_specularF0_enableMultiScatterCompensation = true;
 option bool o_enableShadows = true;
 option bool o_enableDirectionalLights = true;
 option bool o_enablePunctualLights = true;
+
 option bool o_enableAreaLights = true;
+option bool o_enableSphereLights = true;
+option bool o_enableSphereLightShadows = true;
+option bool o_enableDiskLights = true;
+option bool o_enableDiskLightShadows = true;
+option bool o_enableCapsuleLights = true;
+option bool o_enableQuadLightLTC = true;
+option bool o_enableQuadLightApprox = true;
+option bool o_enablePolygonLights = true;
+
 option bool o_enableIBL = true;
 option bool o_enableSubsurfaceScattering = false;
 option bool o_area_light_validation = false;

+ 13 - 11
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/CapsuleLight.azsli

@@ -238,19 +238,21 @@ void ApplyCapsuleLights(Surface surface, inout LightingData lightingData)
         
 #if ENABLE_CAPSULE_LIGHTS
 
-        ViewSrg::CapsuleLight light = ViewSrg::m_capsuleLights[currLightIndex];
-
-    #if ENABLE_AREA_LIGHT_VALIDATION
-        if (o_area_light_validation)
-        {
-            ValidateCapsuleLight(light, surface, lightingData);
-        }
-        else
-    #endif
+        if (o_enableCapsuleLights)
         {
-            ApplyCapsuleLight(light, surface, lightingData);
-        }
+            ViewSrg::CapsuleLight light = ViewSrg::m_capsuleLights[currLightIndex];
 
+#if ENABLE_AREA_LIGHT_VALIDATION
+            if (o_area_light_validation)
+            {
+                ValidateCapsuleLight(light, surface, lightingData);
+            }
+            else
+#endif
+            {
+                ApplyCapsuleLight(light, surface, lightingData);
+            }
+        }
 #endif
     }
 }

+ 14 - 13
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/DiskLight.azsli

@@ -82,7 +82,7 @@ void ApplyDiskLight(ViewSrg::DiskLight light, Surface surface, inout LightingDat
         // - If transmission mode is thin object -> ignore back lighting
         float transmissionDistance = -1.0f;
 
-        if (o_enableShadows)
+        if (o_enableShadows && o_enableDiskLightShadows)
         {
             litRatio = ProjectedShadow::GetVisibility(
                 light.m_shadowIndex,
@@ -219,20 +219,21 @@ void ApplyDiskLights(Surface surface, inout LightingData lightingData)
         lightingData.tileIterator.LoadAdvance();
         
 #if ENABLE_DISK_LIGHTS
-
-        ViewSrg::DiskLight light = ViewSrg::m_diskLights[currLightIndex];
-        
-    #if ENABLE_AREA_LIGHT_VALIDATION
-        if (o_area_light_validation)
-        {
-            ValidateDiskLight(light, surface, lightingData);
-        }
-        else
-    #endif
+        if (o_enableDiskLights)
         {
-            ApplyDiskLight(light, surface, lightingData);
+            ViewSrg::DiskLight light = ViewSrg::m_diskLights[currLightIndex];
+            
+#if ENABLE_AREA_LIGHT_VALIDATION
+            if (o_area_light_validation)
+            {
+                ValidateDiskLight(light, surface, lightingData);
+            }
+            else
+#endif
+            {
+                ApplyDiskLight(light, surface, lightingData);
+            }
         }
-
 #endif
     }
 }

+ 14 - 13
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PointLight.azsli

@@ -90,7 +90,7 @@ void ApplyPointLight(ViewSrg::PointLight light, Surface surface, inout LightingD
         // - If transmission mode is thin object -> ignore back lighting
         float transmissionDistance = -1.0f;
 
-        if (o_enableShadows)
+        if (o_enableShadows && o_enableSphereLightShadows)
         {
             const float3 lightDir = normalize(light.m_position - surface.position);
             const uint shadowIndex = ComputeShadowIndex(light, surface);
@@ -190,20 +190,21 @@ void ApplyPointLights(Surface surface, inout LightingData lightingData)
         lightingData.tileIterator.LoadAdvance();
     
 #if ENABLE_SPHERE_LIGHTS
-        
-        ViewSrg::PointLight light = ViewSrg::m_pointLights[currLightIndex];
-        
-    #if ENABLE_AREA_LIGHT_VALIDATION
-        if (o_area_light_validation)
+        if (o_enableSphereLights)
         {
-            ValidatePointLight(light, surface, lightingData);
-        }
-        else
-    #endif
-        {
-            ApplyPointLight(light, surface, lightingData);
+            ViewSrg::PointLight light = ViewSrg::m_pointLights[currLightIndex];
+            
+#if ENABLE_AREA_LIGHT_VALIDATION
+            if (o_area_light_validation)
+            {
+                ValidatePointLight(light, surface, lightingData);
+            }
+            else
+#endif
+            {
+                ApplyPointLight(light, surface, lightingData);
+            }
         }
-
 #endif
     }
 }

+ 8 - 14
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PolygonLight.azsli

@@ -30,16 +30,7 @@ void ApplyPoylgonLight(ViewSrg::PolygonLight light, Surface surface, inout Light
 
     if (!doubleSided)
     {
-        // Determine the direction of the polygon light by crossing the first two edges
-        float3 edge1 = ViewSrg::m_polygonLightPoints[startIndex].xyz - ViewSrg::m_polygonLightPoints[startIndex + 1].xyz;
-        float3 edge2 = ViewSrg::m_polygonLightPoints[startIndex + 2].xyz - ViewSrg::m_polygonLightPoints[startIndex + 1].xyz;
-        float3 lightDirection = normalize(cross(edge2, edge1));
-
-        // The sign bit on light.m_rgbIntensityNits.x stores if first two edges are concave or convex. Flip lightDirection based on this bit.
-        lightDirection = asfloat(asuint(lightDirection) ^ asuint(light.m_rgbIntensityNits.x) & 0x80000000); 
-
-        float posToLightDotLightDirection = dot(posToLight, -lightDirection);
-        if (posToLightDotLightDirection <= 0.0)
+        if (dot(posToLight, -light.m_direction) <= 0.0)
         {
             return; // Light isn't facing the surface
         }
@@ -69,11 +60,14 @@ void ApplyPoylgonLight(ViewSrg::PolygonLight light, Surface surface, inout Light
 void ApplyPolygonLights(Surface surface, inout LightingData lightingData)
 {
 #if ENABLE_POLYGON_LTC_LIGHTS
-    for (uint currLightIndex = 0; currLightIndex <  ViewSrg::m_polygonLightCount; ++currLightIndex)
+    if (o_enablePolygonLights)
     {
-        ViewSrg::PolygonLight light = ViewSrg::m_polygonLights[currLightIndex];
-        
-        ApplyPoylgonLight(light, surface, lightingData);
+        for (uint currLightIndex = 0; currLightIndex <  ViewSrg::m_polygonLightCount; ++currLightIndex)
+        {
+            ViewSrg::PolygonLight light = ViewSrg::m_polygonLights[currLightIndex];
+            
+            ApplyPoylgonLight(light, surface, lightingData);
+        }
     }
 #endif
 }

+ 15 - 13
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/QuadLight.azsli

@@ -108,7 +108,7 @@ void ApplyQuadLight(ViewSrg::QuadLight light, Surface surface, inout LightingDat
         float3 p3 = posToLight +  left +  up;
 
         bool useFastApproximation = (light.m_flags & QuadLightFlag::UseFastApproximation) > 0;
-        if (!useFastApproximation)
+        if (!useFastApproximation && o_enableQuadLightLTC)
         {
             float3 p[4] = {p0, p1, p2, p3};
             
@@ -122,7 +122,7 @@ void ApplyQuadLight(ViewSrg::QuadLight light, Surface surface, inout LightingDat
             lightingData.diffuseLighting += surface.albedo * diffuse * intensity;
             lightingData.specularLighting += specular * intensity;
         }
-        else
+        else if (useFastApproximation && o_enableQuadLightApprox)
         {
             // Calculate solid angle of light
             float solidAngle = RectangleSolidAngle(p0, p1, p2, p3);
@@ -262,19 +262,21 @@ void ApplyQuadLights(Surface surface, inout LightingData lightingData)
 
 #if ENABLE_QUAD_LIGHTS
 
-        ViewSrg::QuadLight light = ViewSrg::m_quadLights[currLightIndex];
-        
-    #if ENABLE_AREA_LIGHT_VALIDATION
-        if (o_area_light_validation)
+        if (o_enableQuadLightApprox || o_enableQuadLightLTC)
         {
-            ValidateQuadLight(light, surface, lightingData);
-        }
-        else
-    #endif
-        {
-            ApplyQuadLight(light, surface, lightingData);
+            ViewSrg::QuadLight light = ViewSrg::m_quadLights[currLightIndex];
+            
+#if ENABLE_AREA_LIGHT_VALIDATION
+            if (o_area_light_validation)
+            {
+                ValidateQuadLight(light, surface, lightingData);
+            }
+            else
+#endif
+            {
+                ApplyQuadLight(light, surface, lightingData);
+            }
         }
-
 #endif
     }
 }

+ 3 - 1
Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli

@@ -204,8 +204,10 @@ partial ShaderResourceGroup ViewSrg
     {
         float3 m_position;
         uint m_startEndIndex; // 16 bit start and end indices packed into one 32 bit uint
-        float3 m_rgbIntensityNits; // sign bit on red indicates winding order of first two edges. Used to determine direction.
+        float3 m_rgbIntensityNits;
         float m_invAttenuationRadiusSquared; // negative sign bit indicates double sided.
+        float3 m_direction;
+        float m_padding0;
     };
 
     StructuredBuffer<PolygonLight> m_polygonLights;

+ 21 - 0
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/LightCommon.h

@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Math/Internal/MathTypes.h>
+
+namespace AZ::Render::LightCommon
+{
+    inline float GetRadiusFromInvRadiusSquared(float invRadiusSqaured)
+    {
+        return (invRadiusSqaured <= 0.0f) ? 1.0f : Sqrt(1.0f / invRadiusSqaured);
+    }
+
+} // namespace AZ::Render::LightCommon
+

+ 5 - 1
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/PolygonLightFeatureProcessorInterface.h

@@ -27,12 +27,16 @@ namespace AZ
 
             // Standard RGB Color, except the red sign bit is used to store if points {0, 1, 2} create concave or
             // convex edges. This is used in the shader to determine directionality.
-            AZStd::array<float, 3> m_rgbIntensityNits = { { 0.0f, 0.0f, 0.0f } };
+            AZStd::array<float, 3> m_rgbIntensityNits = { 0.0f, 0.0f, 0.0f };
 
             // Inverse of the distance at which this light no longer has an effect, squared. Also used for falloff calculations.
             // Negative sign bit used to indicate if the light emits both directions.
             float m_invAttenuationRadiusSquared = 0.0f;
 
+            AZStd::array<float, 3> m_direction = { 0.0f, 0.0f, 0.0f };
+
+            float m_padding = 0.0f;
+
             // Convenience functions for setting start / end index.
 
             uint32_t GetStartIndex()

+ 99 - 0
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshCommon.h

@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Math/Aabb.h>
+#include <AzCore/Math/Frustum.h>
+#include <AzCore/Math/Hemisphere.h>
+#include <AzCore/Math/ShapeIntersection.h>
+#include <AzCore/Math/Sphere.h>
+#include <AzCore/std/containers/variant.h>
+
+#include <Atom/RPI.Public/Culling.h>
+#include <Atom/RPI.Public/Scene.h>
+
+namespace AZ::Render::MeshCommon
+{
+    template <typename BoundsType>
+    void MarkMeshesForBounds(AZ::RPI::Scene* scene, const BoundsType& bounds, AZ::RPI::Cullable::FlagType flag)
+    {
+        AzFramework::IVisibilityScene* visScene = scene->GetVisibilityScene();
+
+        visScene->Enumerate(bounds, [flag, &boundsRef = bounds](const AzFramework::IVisibilityScene::NodeData& nodeData)
+            {
+                bool nodeIsContainedInBounds = ShapeIntersection::Contains(boundsRef, nodeData.m_bounds);
+                for (auto* visibleEntry : nodeData.m_entries)
+                {
+                    if (visibleEntry->m_typeFlags == AzFramework::VisibilityEntry::TYPE_RPI_Cullable)
+                    {
+                        RPI::Cullable* cullable = static_cast<RPI::Cullable*>(visibleEntry->m_userData);
+
+                        if (nodeIsContainedInBounds || ShapeIntersection::Overlaps(boundsRef, cullable->m_cullData.m_boundingSphere))
+                        {
+                            // This flag is cleared by the mesh feature processor each frame in OnEndPrepareRender()
+                            cullable->m_flags.fetch_or(flag);
+                        }
+                    }
+                }
+            }
+        );
+    }
+
+    using BoundsVariant = AZStd::variant<Sphere, Hemisphere, Frustum, Aabb, Capsule>;
+
+    template <typename BoundsType>
+    struct EmptyFilter
+    {
+        constexpr bool operator()(const BoundsType&) const { return true; }
+    };
+
+    template <typename BoundsType, class Filter = EmptyFilter<BoundsType>>
+    void MarkMeshesWithFlag(AZ::RPI::Scene* scene, AZStd::span<const BoundsType> boundsCollection, AZ::RPI::Cullable::FlagType flag, Filter filter = {})
+    {
+        for (const BoundsType& bounds : boundsCollection)
+        {
+            if (filter(bounds))
+            {
+                MarkMeshesForBounds(scene, bounds, flag);
+            }
+        }
+    }
+
+    template <class Filter = EmptyFilter<BoundsVariant>>
+    void MarkMeshesWithFlag(AZ::RPI::Scene* scene, AZStd::span<const BoundsVariant> boundsCollection, AZ::RPI::Cullable::FlagType flag, Filter filter = {})
+    {
+        for (const BoundsVariant& bounds : boundsCollection)
+        {
+            if (filter(bounds))
+            {
+                if (AZStd::holds_alternative<Sphere>(bounds))
+                {
+                    MarkMeshesForBounds(scene, AZStd::get<Sphere>(bounds), flag);
+                }
+                else if (AZStd::holds_alternative<Hemisphere>(bounds))
+                {
+                    MarkMeshesForBounds(scene, AZStd::get<Hemisphere>(bounds), flag);
+                }
+                else if (AZStd::holds_alternative<Frustum>(bounds))
+                {
+                    MarkMeshesForBounds(scene, AZStd::get<Frustum>(bounds), flag);
+                }
+                else if (AZStd::holds_alternative<Aabb>(bounds))
+                {
+                    MarkMeshesForBounds(scene, AZStd::get<Aabb>(bounds), flag);
+                }
+                else if (AZStd::holds_alternative<Capsule>(bounds))
+                {
+                    MarkMeshesForBounds(scene, AZStd::get<Capsule>(bounds), flag);
+                }
+            }
+        }
+    }
+
+}

+ 28 - 8
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h

@@ -8,21 +8,27 @@
 
 #pragma once
 
-#include <Atom/Feature/Mesh/MeshFeatureProcessorInterface.h>
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/Console/Console.h>
+
+#include <AzFramework/Asset/AssetCatalogBus.h>
+
+#include <AtomCore/std/parallel/concurrency_checker.h>
+
+#include <Atom/RHI/TagBitRegistry.h>
+
 #include <Atom/RPI.Public/Culling.h>
 #include <Atom/RPI.Public/MeshDrawPacket.h>
 #include <Atom/RPI.Public/Shader/ShaderSystemInterface.h>
+
+#include <Atom/Feature/Mesh/MeshFeatureProcessorInterface.h>
 #include <Atom/Feature/Material/MaterialAssignment.h>
 #include <Atom/Feature/Material/MaterialAssignmentBus.h>
 #include <Atom/Feature/TransformService/TransformServiceFeatureProcessor.h>
 #include <Atom/Feature/Mesh/ModelReloaderSystemInterface.h>
-#include <RayTracing/RayTracingFeatureProcessor.h>
-#include <AzCore/Asset/AssetCommon.h>
-#include <AtomCore/std/parallel/concurrency_checker.h>
-#include <AzCore/Console/Console.h>
-#include <AzFramework/Asset/AssetCatalogBus.h>
 
-#include <AzCore/Component/TickBus.h>
+#include <RayTracing/RayTracingFeatureProcessor.h>
 
 namespace AZ
 {
@@ -132,6 +138,10 @@ namespace AZ
 
             AZ_RTTI(AZ::Render::MeshFeatureProcessor, "{6E3DFA1D-22C7-4738-A3AE-1E10AB88B29B}", AZ::Render::MeshFeatureProcessorInterface);
 
+            AZ_CONSOLEFUNC(MeshFeatureProcessor, ReportShaderOptionFlags, AZ::ConsoleFunctorFlags::Null, "Report currently used shader option flags.");
+
+            using FlagRegistry = RHI::TagBitRegistry<RPI::Cullable::FlagType>;
+
             static void Reflect(AZ::ReflectContext* context);
 
             MeshFeatureProcessor() = default;
@@ -190,9 +200,16 @@ namespace AZ
             bool GetVisible(const MeshHandle& meshHandle) const override;
             void SetUseForwardPassIblSpecular(const MeshHandle& meshHandle, bool useForwardPassIblSpecular) override;
 
+            RHI::Ptr <FlagRegistry> GetFlagRegistry();
+
             // called when reflection probes are modified in the editor so that meshes can re-evaluate their probes
             void UpdateMeshReflectionProbes();
+
+            void ReportShaderOptionFlags(const AZ::ConsoleCommandContainer& arguments);
+
         private:
+            MeshFeatureProcessor(const MeshFeatureProcessor&) = delete;
+
             void ForceRebuildDrawPackets(const AZ::ConsoleCommandContainer& arguments);
             AZ_CONSOLEFUNC(MeshFeatureProcessor,
                 ForceRebuildDrawPackets,
@@ -200,7 +217,7 @@ namespace AZ
                 "(For Testing) Invalidates all mesh draw packets, causing them to rebuild on the next frame."
             );
 
-            MeshFeatureProcessor(const MeshFeatureProcessor&) = delete;
+            void PrintShaderOptionFlags();
 
             // RPI::SceneNotificationBus::Handler overrides...
             void OnRenderPipelineChanged(AZ::RPI::RenderPipeline* pipeline, RPI::SceneNotification::RenderPipelineChangeType changeType) override;
@@ -211,7 +228,10 @@ namespace AZ
             RayTracingFeatureProcessor* m_rayTracingFeatureProcessor = nullptr;
             AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler m_handleGlobalShaderOptionUpdate;
             RPI::MeshDrawPacketLods m_emptyDrawPacketLods;
+            RHI::Ptr<FlagRegistry> m_flagRegistry = nullptr;
             bool m_forceRebuildDrawPackets = false;
+            bool m_reportShaderOptionFlags = false;
+            bool m_enablePerMeshShaderOptionFlags = false;
         };
     } // namespace Render
 } // namespace AZ

+ 8 - 0
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h

@@ -22,6 +22,14 @@ namespace AZ
 {
     namespace Render
     {
+        AZ_CVAR(bool,
+            r_enablePerMeshShaderOptionFlags,
+            false,
+            nullptr,
+            AZ::ConsoleFunctorFlags::Null,
+            "Enable allowing systems to set shader options on a per-mesh basis."
+        );
+
         class ModelDataInstance;
         
         //! Settings to apply to a mesh handle when acquiring it for the first time

+ 2 - 0
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/MultiIndexedDataVector.h

@@ -27,6 +27,8 @@ namespace AZ
 
             MultiIndexedDataVector()
             {
+                static_assert((AZStd::is_default_constructible_v<Ts> && ...), "MultiIndexDataVector can only be used with types that are default constructible");
+
                 m_dataToIndices.reserve(InitialReservedCount);
                 m_indices.reserve(InitialReservedCount);
 

+ 53 - 13
Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.cpp

@@ -12,6 +12,9 @@
 #include <AzCore/Math/Color.h>
 
 #include <Atom/Feature/CoreLights/CoreLightsConstants.h>
+#include <Atom/Feature/CoreLights/LightCommon.h>
+#include <Atom/Feature/Mesh/MeshCommon.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessor.h>
 
 #include <Atom/RHI/Factory.h>
 
@@ -49,19 +52,25 @@ namespace AZ
             desc.m_srgLayout = RPI::RPISystemInterface::Get()->GetViewSrgLayout().get();
 
             m_lightBufferHandler = GpuBufferHandler(desc);
+
+            MeshFeatureProcessor* meshFeatureProcessor = GetParentScene()->GetFeatureProcessor<MeshFeatureProcessor>();
+            if (meshFeatureProcessor)
+            {
+                m_lightMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enableCapsuleLights"));
+            }
         }
 
         void CapsuleLightFeatureProcessor::Deactivate()
         {
-            m_capsuleLightData.Clear();
+            m_lightData.Clear();
             m_lightBufferHandler.Release();
         }
 
         CapsuleLightFeatureProcessor::LightHandle CapsuleLightFeatureProcessor::AcquireLight()
         {
-            uint16_t id = m_capsuleLightData.GetFreeSlotIndex();
+            uint16_t id = m_lightData.GetFreeSlotIndex();
 
-            if (id == IndexedDataVector<CapsuleLightData>::NoFreeSlot)
+            if (id == MultiIndexedDataVector<CapsuleLightData>::NoFreeSlot)
             {
                 return LightHandle(LightHandle::NullIndex);
             }
@@ -76,7 +85,7 @@ namespace AZ
         {
             if (handle.IsValid())
             {
-                m_capsuleLightData.RemoveIndex(handle.GetIndex());
+                m_lightData.RemoveIndex(handle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
                 handle.Reset();
                 return true;
@@ -91,7 +100,8 @@ namespace AZ
             LightHandle handle = AcquireLight();
             if (handle.IsValid())
             {
-                m_capsuleLightData.GetData(handle.GetIndex()) = m_capsuleLightData.GetData(sourceLightHandle.GetIndex());
+                m_lightData.GetData<0>(handle.GetIndex()) = m_lightData.GetData<0>(sourceLightHandle.GetIndex());
+                m_lightData.GetData<1>(handle.GetIndex()) = m_lightData.GetData<1>(sourceLightHandle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
             }
             return handle;
@@ -104,9 +114,14 @@ namespace AZ
 
             if (m_deviceBufferNeedsUpdate)
             {
-                m_lightBufferHandler.UpdateBuffer(m_capsuleLightData.GetDataVector());
+                m_lightBufferHandler.UpdateBuffer(m_lightData.GetDataVector<0>());
                 m_deviceBufferNeedsUpdate = false;
             }
+
+            if (r_enablePerMeshShaderOptionFlags)
+            {
+                MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<1>()), m_lightMeshFlag.GetIndex());
+            }
         }
 
         void CapsuleLightFeatureProcessor::Render(const CapsuleLightFeatureProcessor::RenderPacket& packet)
@@ -125,7 +140,7 @@ namespace AZ
 
             auto transformedColor = AZ::RPI::TransformColor(lightRgbIntensity, AZ::RPI::ColorSpaceId::LinearSRGB, AZ::RPI::ColorSpaceId::ACEScg);
 
-            auto& rgbIntensity = m_capsuleLightData.GetData(handle.GetIndex()).m_rgbIntensity;
+            auto& rgbIntensity = m_lightData.GetData<0>(handle.GetIndex()).m_rgbIntensity;
             rgbIntensity[0] = transformedColor.GetR();
             rgbIntensity[1] = transformedColor.GetG();
             rgbIntensity[2] = transformedColor.GetB();
@@ -137,7 +152,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to CapsuleLightFeatureProcessor::SetCapsuleLineSegment().");
 
-            CapsuleLightData& capsuleData = m_capsuleLightData.GetData(handle.GetIndex());
+            CapsuleLightData& capsuleData = m_lightData.GetData<0>(handle.GetIndex());
             startPoint.StoreToFloat3(capsuleData.m_startPoint.data());
 
             if (startPoint.IsClose(endPoint))
@@ -153,6 +168,8 @@ namespace AZ
                 direction.StoreToFloat3(capsuleData.m_direction.data());
             }
 
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -160,8 +177,13 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to CapsuleLightFeatureProcessor::SetAttenuationRadius().");
 
+            CapsuleLightData& capsuleData = m_lightData.GetData<0>(handle.GetIndex());
+
             attenuationRadius = AZStd::max<float>(attenuationRadius, 0.001f); // prevent divide by zero.
-            m_capsuleLightData.GetData(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+            capsuleData.m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -169,7 +191,9 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to CapsuleLightFeatureProcessor::SetCapsuleRadius().");
 
-            m_capsuleLightData.GetData(handle.GetIndex()).m_radius = radius;
+            m_lightData.GetData<0>(handle.GetIndex()).m_radius = radius;
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -177,7 +201,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to CapsuleLightFeatureProcessor::SetAffectsGI().");
 
-            m_capsuleLightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGI = affectsGI;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -185,7 +209,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to CapsuleLightFeatureProcessor::SetAffectsGIFactor().");
 
-            m_capsuleLightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -193,7 +217,8 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to CapsuleLightFeatureProcessor::SetCapsuleData().");
 
-            m_capsuleLightData.GetData(handle.GetIndex()) = data;
+            m_lightData.GetData<0>(handle.GetIndex()) = data;
+            UpdateBounds(handle);
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -207,5 +232,20 @@ namespace AZ
             return m_lightBufferHandler.GetElementCount();
         }
 
+        void CapsuleLightFeatureProcessor::UpdateBounds(LightHandle handle)
+        {
+            CapsuleLightData& capsuleData = m_lightData.GetData<0>(handle.GetIndex());
+
+            AZ::Vector3 startPoint = AZ::Vector3::CreateFromFloat3(capsuleData.m_startPoint.data());
+            AZ::Vector3 direction = AZ::Vector3::CreateFromFloat3(capsuleData.m_direction.data());
+            AZ::Vector3 endPoint = startPoint + direction * capsuleData.m_length;
+            float attenuationRadius = LightCommon::GetRadiusFromInvRadiusSquared(capsuleData.m_invAttenuationRadiusSquared);
+
+            AZ::Capsule& bounds = m_lightData.GetData<1>(handle.GetIndex());
+            bounds.SetFirstHemisphereCenter(startPoint);
+            bounds.SetSecondHemisphereCenter(endPoint);
+            bounds.SetRadius(attenuationRadius);
+        }
+
     } // namespace Render
 } // namespace AZ

+ 6 - 2
Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.h

@@ -8,9 +8,10 @@
 
 #pragma once
 
+#include <AzCore/Math/Capsule.h>
 #include <Atom/Feature/CoreLights/CapsuleLightFeatureProcessorInterface.h>
 #include <Atom/Feature/Utils/GpuBufferHandler.h>
-#include <Atom/Feature/Utils/IndexedDataVector.h>
+#include <Atom/Feature/Utils/MultiIndexedDataVector.h>
 #include <Atom/Feature/CoreLights/PhotometricValue.h>
 
 namespace AZ
@@ -55,10 +56,13 @@ namespace AZ
         private:
             CapsuleLightFeatureProcessor(const CapsuleLightFeatureProcessor&) = delete;
 
+            void UpdateBounds(LightHandle handle);
+
             static constexpr const char* FeatureProcessorName = "CapsuleLightFeatureProcessor";
 
-            IndexedDataVector<CapsuleLightData> m_capsuleLightData;
+            MultiIndexedDataVector<CapsuleLightData, AZ::Capsule> m_lightData;
             GpuBufferHandler m_lightBufferHandler;
+            RHI::Handle<uint32_t> m_lightMeshFlag;
             bool m_deviceBufferNeedsUpdate = false;
         };
     } // namespace Render

+ 118 - 39
Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.cpp

@@ -13,6 +13,7 @@
 #include <AzCore/Math/Vector3.h>
 
 #include <Atom/Feature/CoreLights/CoreLightsConstants.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessor.h>
 
 #include <Atom/RHI/Factory.h>
 
@@ -21,6 +22,8 @@
 #include <Atom/RPI.Public/Scene.h>
 #include <Atom/RPI.Public/View.h>
 
+#include <AzCore/std/containers/variant.h>
+
 namespace AZ
 {
     namespace Render
@@ -51,24 +54,31 @@ namespace AZ
 
             m_lightBufferHandler = GpuBufferHandler(desc);
             m_shadowFeatureProcessor = GetParentScene()->GetFeatureProcessor<ProjectedShadowFeatureProcessor>();
+
+            MeshFeatureProcessor* meshFeatureProcessor = GetParentScene()->GetFeatureProcessor<MeshFeatureProcessor>();
+            if (meshFeatureProcessor)
+            {
+                m_lightMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enableDiskLights"));
+                m_shadowMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enableDiskLightShadows"));
+            }
         }
 
         void DiskLightFeatureProcessor::Deactivate()
         {
-            m_diskLightData.Clear();
+            m_lightData.Clear();
             m_lightBufferHandler.Release();
         }
 
         DiskLightFeatureProcessor::LightHandle DiskLightFeatureProcessor::AcquireLight()
         {
-            uint16_t id = m_diskLightData.GetFreeSlotIndex();
+            uint16_t id = m_lightData.GetFreeSlotIndex();
 
             if (id == IndexedDataVector<DiskLightData>::NoFreeSlot)
             {
                 return LightHandle::Null;
             }
             else
-           {
+            {
                 m_deviceBufferNeedsUpdate = true;
                 return LightHandle(id);
             }
@@ -78,12 +88,12 @@ namespace AZ
         {
             if (handle.IsValid())
             {
-                ShadowId shadowId = ShadowId(m_diskLightData.GetData(handle.GetIndex()).m_shadowIndex);
+                ShadowId shadowId = ShadowId(m_lightData.GetData<0>(handle.GetIndex()).m_shadowIndex);
                 if (shadowId.IsValid())
                 {
                     m_shadowFeatureProcessor->ReleaseShadow(shadowId);
                 }
-                m_diskLightData.RemoveIndex(handle.GetIndex());
+                m_lightData.RemoveIndex(handle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
                 handle.Reset();
                 return true;
@@ -99,9 +109,12 @@ namespace AZ
             if (handle.IsValid())
             {
                 // Get a reference to the new light
-                DiskLightData& light = m_diskLightData.GetData(handle.GetIndex());
+                DiskLightData& light = m_lightData.GetData<0>(handle.GetIndex());
                 // Copy data from the source light on top of it.
-                light = m_diskLightData.GetData(sourceLightHandle.GetIndex());
+                light = m_lightData.GetData<0>(sourceLightHandle.GetIndex());
+                m_lightData.GetData<1>(handle.GetIndex()) = m_lightData.GetData<1>(sourceLightHandle.GetIndex());
+
+                static_assert(AZStd::variant_detail::copy_assignable_traits<AZ::Frustum, AZ::Hemisphere, AZ::Sphere, AZ::Aabb> != AZStd::variant_detail::SpecialFunctionTraits::Unavailable);
 
                 ShadowId shadowId = ShadowId(light.m_shadowIndex);
                 if (shadowId.IsValid())
@@ -125,9 +138,36 @@ namespace AZ
 
             if (m_deviceBufferNeedsUpdate)
             {
-                m_lightBufferHandler.UpdateBuffer(m_diskLightData.GetDataVector());
+                m_lightBufferHandler.UpdateBuffer(m_lightData.GetDataVector<0>());
                 m_deviceBufferNeedsUpdate = false;
             }
+
+            if (r_enablePerMeshShaderOptionFlags)
+            {
+                // Helper lambdas
+                auto indexHasShadow = [&](LightHandle::IndexType index) -> bool
+                {
+                    ShadowId shadowId = ShadowId(m_lightData.GetData<0>(index).m_shadowIndex);
+                    return shadowId.IsValid();
+                };
+
+                // Filter lambdas
+                auto hasShadow = [&](const MeshCommon::BoundsVariant& bounds) -> bool
+                {
+                    return indexHasShadow(m_lightData.GetIndexForData<1>(&bounds));
+                };
+                auto noShadow = [&](const MeshCommon::BoundsVariant& bounds) -> bool
+                {
+                    return !indexHasShadow(m_lightData.GetIndexForData<1>(&bounds));
+                };
+
+                // Mark meshes that have point lights without shadow using only the light flag.
+                MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<1>()), m_lightMeshFlag.GetIndex(), noShadow);
+
+                // Mark meshes that have point lights with shadow using a combination of light and shadow flags.
+                uint32_t lightAndShadow = m_lightMeshFlag.GetIndex() | m_shadowMeshFlag.GetIndex();
+                MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<1>()), lightAndShadow, hasShadow);
+            }
         }
 
         void DiskLightFeatureProcessor::Render(const DiskLightFeatureProcessor::RenderPacket& packet)
@@ -146,7 +186,7 @@ namespace AZ
 
             auto transformedColor = AZ::RPI::TransformColor(lightRgbIntensity, AZ::RPI::ColorSpaceId::LinearSRGB, AZ::RPI::ColorSpaceId::ACEScg);
 
-            AZStd::array<float, 3>& rgbIntensity = m_diskLightData.GetData(handle.GetIndex()).m_rgbIntensity;
+            AZStd::array<float, 3>& rgbIntensity = m_lightData.GetData<0>(handle.GetIndex()).m_rgbIntensity;
             rgbIntensity[0] = transformedColor.GetR();
             rgbIntensity[1] = transformedColor.GetG();
             rgbIntensity[2] = transformedColor.GetB();
@@ -158,22 +198,26 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetPosition().");
 
-            AZStd::array<float, 3>& position = m_diskLightData.GetData(handle.GetIndex()).m_position;
+            AZStd::array<float, 3>& position = m_lightData.GetData<0>(handle.GetIndex()).m_position;
             lightPosition.StoreToFloat3(position.data());
 
-            m_deviceBufferNeedsUpdate = true;
+            UpdateBounds(handle);
             UpdateShadow(handle);
+
+            m_deviceBufferNeedsUpdate = true;
         }
 
         void DiskLightFeatureProcessor::SetDirection(LightHandle handle, const AZ::Vector3& lightDirection)
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetDirection().");
 
-            AZStd::array<float, 3>& direction = m_diskLightData.GetData(handle.GetIndex()).m_direction;
-            lightDirection.StoreToFloat3(direction.data());
+            AZStd::array<float, 3>& direction = m_lightData.GetData<0>(handle.GetIndex()).m_direction;
+            lightDirection.GetNormalized().StoreToFloat3(direction.data());
 
-            m_deviceBufferNeedsUpdate = true;
+            UpdateBounds(handle);
             UpdateShadow(handle);
+
+            m_deviceBufferNeedsUpdate = true;
         }
 
         void DiskLightFeatureProcessor::SetAttenuationRadius(LightHandle handle, float attenuationRadius)
@@ -181,10 +225,10 @@ namespace AZ
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetAttenuationRadius().");
 
             attenuationRadius = AZStd::max<float>(attenuationRadius, 0.001f); // prevent divide by zero.
-            DiskLightData& light = m_diskLightData.GetData(handle.GetIndex());
+            DiskLightData& light = m_lightData.GetData<0>(handle.GetIndex());
             light.m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
-            
-            m_deviceBufferNeedsUpdate = true;
+
+            UpdateBounds(handle);
 
             // Update the shadow near far planes if necessary
             ShadowId shadowId = ShadowId(light.m_shadowIndex);
@@ -193,27 +237,34 @@ namespace AZ
                 m_shadowFeatureProcessor->SetNearFarPlanes(ShadowId(light.m_shadowIndex),
                     light.m_bulbPositionOffset, attenuationRadius + light.m_bulbPositionOffset);
             }
+
+            m_deviceBufferNeedsUpdate = true;
+
         }
 
         void DiskLightFeatureProcessor::SetDiskRadius(LightHandle handle, float radius)
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetDiskRadius().");
             
-            DiskLightData& light = m_diskLightData.GetData(handle.GetIndex());
+            DiskLightData& light = m_lightData.GetData<0>(handle.GetIndex());
             light.m_diskRadius = radius;
+
             UpdateBulbPositionOffset(light);
-            m_deviceBufferNeedsUpdate = true;
+            UpdateBounds(handle);
             UpdateShadow(handle);
+
+            m_deviceBufferNeedsUpdate = true;
         }
 
         void DiskLightFeatureProcessor::SetConstrainToConeLight(LightHandle handle, bool useCone)
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetDiskRadius().");
 
-            uint32_t& flags = m_diskLightData.GetData(handle.GetIndex()).m_flags;
+            uint32_t& flags = m_lightData.GetData<0>(handle.GetIndex()).m_flags;
             useCone ? flags |= DiskLightData::Flags::UseConeAngle : flags &= ~DiskLightData::Flags::UseConeAngle;
-            m_deviceBufferNeedsUpdate = true;
             UpdateShadow(handle);
+
+            m_deviceBufferNeedsUpdate = true;
         }
         
         void DiskLightFeatureProcessor::SetConeAngles(LightHandle handle, float innerRadians, float outerRadians)
@@ -228,7 +279,7 @@ namespace AZ
         
         void DiskLightFeatureProcessor::ValidateAndSetConeAngles(LightHandle handle, float innerRadians, float outerRadians)
         {
-            DiskLightData& light = m_diskLightData.GetData(handle.GetIndex());
+            DiskLightData& light = m_lightData.GetData<0>(handle.GetIndex());
 
             // Assume if the cone angles are being set that the user wants to constrain to a cone angle
             SetConstrainToConeLight(handle, true);
@@ -250,16 +301,18 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetDiskData().");
 
-            m_diskLightData.GetData(handle.GetIndex()) = data;
-            m_deviceBufferNeedsUpdate = true;
+            m_lightData.GetData<0>(handle.GetIndex()) = data;
             UpdateShadow(handle);
+            UpdateBounds(handle);
+
+            m_deviceBufferNeedsUpdate = true;
         }
 
         const DiskLightData&  DiskLightFeatureProcessor::GetDiskData(LightHandle handle) const
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::GetDiskData().");
 
-            return m_diskLightData.GetData(handle.GetIndex());
+            return m_lightData.GetData<0>(handle.GetIndex());
         }
 
         const Data::Instance<RPI::Buffer> DiskLightFeatureProcessor::GetLightBuffer()const
@@ -274,7 +327,7 @@ namespace AZ
 
         void DiskLightFeatureProcessor::SetShadowsEnabled(LightHandle handle, bool enabled)
         {
-            DiskLightData& light = m_diskLightData.GetData(handle.GetIndex());
+            DiskLightData& light = m_lightData.GetData<0>(handle.GetIndex());
             ShadowId shadowId = ShadowId(light.m_shadowIndex);
             if (shadowId.IsValid() && enabled == false)
             {
@@ -303,7 +356,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetShadowSetting().");
             
-            DiskLightData& light = m_diskLightData.GetData(handle.GetIndex());
+            DiskLightData& light = m_lightData.GetData<0>(handle.GetIndex());
             ShadowId shadowId = ShadowId(light.m_shadowIndex);
 
             AZ_Assert(shadowId.IsValid(), "Attempting to set a shadow property when shadows are not enabled.");
@@ -347,7 +400,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetAffectsGI().");
 
-            m_diskLightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGI = affectsGI;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -355,13 +408,13 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to DiskLightFeatureProcessor::SetAffectsGIFactor().");
 
-            m_diskLightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
             m_deviceBufferNeedsUpdate = true;
         }
 
         void DiskLightFeatureProcessor::UpdateShadow(LightHandle handle)
         {
-            const DiskLightData& diskLight = m_diskLightData.GetData(handle.GetIndex());
+            const DiskLightData& diskLight = m_lightData.GetData<0>(handle.GetIndex());
             ShadowId shadowId = ShadowId(diskLight.m_shadowIndex);
             if (shadowId.IsNull())
             {
@@ -392,14 +445,7 @@ namespace AZ
 
             desc.m_aspectRatio = 1.0f;
             desc.m_nearPlaneDistance = diskLight.m_bulbPositionOffset;
-            
-            const float invRadiusSquared = diskLight.m_invAttenuationRadiusSquared;
-            if (invRadiusSquared <= 0.f)
-            {
-                AZ_Assert(false, "Attenuation radius must be set before using the light.");
-                return;
-            }
-            const float attenuationRadius = sqrtf(1.f / invRadiusSquared);
+            const float attenuationRadius = LightCommon::GetRadiusFromInvRadiusSquared(diskLight.m_invAttenuationRadiusSquared);
             desc.m_farPlaneDistance = attenuationRadius + diskLight.m_bulbPositionOffset;
 
             m_shadowFeatureProcessor->SetShadowProperties(shadowId, desc);
@@ -411,7 +457,40 @@ namespace AZ
             // light stores the cosine of outerConeRadians, making the equation (radius * tan(pi/2 - acosf(cosConeRadians)).
             // This simplifies to the equation below.
             float cosConeRadians = light.m_cosOuterConeAngle;
-            light.m_bulbPositionOffset = light.m_diskRadius * cosConeRadians / sqrt(1 - cosConeRadians * cosConeRadians);
+            light.m_bulbPositionOffset = light.m_diskRadius * cosConeRadians / sqrt(1.0f - cosConeRadians * cosConeRadians);
+        }
+
+        void DiskLightFeatureProcessor::UpdateBounds(LightHandle handle)
+        {
+            DiskLightData data = m_lightData.GetData<0>(handle.GetIndex());
+
+            float radius = LightCommon::GetRadiusFromInvRadiusSquared(data.m_invAttenuationRadiusSquared);
+            AZ::Vector3 position = AZ::Vector3::CreateFromFloat3(data.m_position.data());
+            AZ::Vector3 normal = AZ::Vector3::CreateFromFloat3(data.m_direction.data());
+
+            // At greater than a 68 degree cone angle, a hemisphere will have a smaller volume than a frustum.
+            constexpr float CosFrustumHemisphereVolumeCrossoverAngle = 0.37f;
+
+            if (data.m_cosOuterConeAngle < CosFrustumHemisphereVolumeCrossoverAngle)
+            {
+                // Wide angle, use a hemisphere for bounds instead of frustum
+                MeshCommon::BoundsVariant& bounds = m_lightData.GetData<1>(handle.GetIndex());
+                bounds.emplace<Hemisphere>(Hemisphere(position, radius, normal));
+            }
+            else
+            {
+                ViewFrustumAttributes desc;
+                desc.m_aspectRatio = 1.0f;
+
+                desc.m_nearClip = data.m_bulbPositionOffset;
+                desc.m_farClip = data.m_bulbPositionOffset + radius;
+                desc.m_verticalFovRadians = GetMax(0.001f, acosf(data.m_cosOuterConeAngle) * 2.0f);
+                desc.m_worldTransform = AZ::Transform::CreateLookAt(position, position + normal);
+
+                m_lightData.GetData<1>(handle.GetIndex()) = AZ::Frustum(desc);
+            }
+
         }
+
     } // namespace Render
 } // namespace AZ

+ 7 - 2
Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.h

@@ -9,9 +9,11 @@
 #pragma once
 
 #include <Atom/Feature/CoreLights/DiskLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/LightCommon.h>
 #include <Atom/Feature/CoreLights/PhotometricValue.h>
+#include <Atom/Feature/Mesh/MeshCommon.h>
 #include <Atom/Feature/Utils/GpuBufferHandler.h>
-#include <Atom/Feature/Utils/IndexedDataVector.h>
+#include <Atom/Feature/Utils/MultiIndexedDataVector.h>
 #include <Shadows/ProjectedShadowFeatureProcessor.h>
 
 namespace AZ
@@ -77,6 +79,7 @@ namespace AZ
             static void UpdateBulbPositionOffset(DiskLightData& light);
 
             void ValidateAndSetConeAngles(LightHandle handle, float innerRadians, float outerRadians);
+            void UpdateBounds(LightHandle handle);
             void UpdateShadow(LightHandle handle);
 
             // Convenience function for forwarding requests to the ProjectedShadowFeatureProcessor
@@ -85,8 +88,10 @@ namespace AZ
 
             ProjectedShadowFeatureProcessor* m_shadowFeatureProcessor = nullptr;
 
-            IndexedDataVector<DiskLightData> m_diskLightData;
+            MultiIndexedDataVector<DiskLightData, MeshCommon::BoundsVariant> m_lightData;
             GpuBufferHandler m_lightBufferHandler;
+            RHI::Handle<uint32_t> m_lightMeshFlag;
+            RHI::Handle<uint32_t> m_shadowMeshFlag;
 
             bool m_deviceBufferNeedsUpdate = false;
         };

+ 57 - 17
Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.cpp

@@ -12,6 +12,9 @@
 #include <AzCore/Math/Color.h>
 
 #include <Atom/Feature/CoreLights/CoreLightsConstants.h>
+#include <Atom/Feature/CoreLights/LightCommon.h>
+#include <Atom/Feature/Mesh/MeshCommon.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessor.h>
 
 #include <Atom/RHI/Factory.h>
 
@@ -57,19 +60,26 @@ namespace AZ
             m_shadowFeatureProcessor = GetParentScene()->GetFeatureProcessor<ProjectedShadowFeatureProcessor>();
 
             m_lightBufferHandler = GpuBufferHandler(desc);
+
+            MeshFeatureProcessor* meshFeatureProcessor = GetParentScene()->GetFeatureProcessor<MeshFeatureProcessor>();
+            if (meshFeatureProcessor)
+            {
+                m_lightMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enableSphereLights"));
+                m_shadowMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enableSphereLightShadows"));
+            }
         }
 
         void PointLightFeatureProcessor::Deactivate()
         {
-            m_pointLightData.Clear();
+            m_lightData.Clear();
             m_lightBufferHandler.Release();
         }
 
         PointLightFeatureProcessor::LightHandle PointLightFeatureProcessor::AcquireLight()
         {
-            uint16_t id = m_pointLightData.GetFreeSlotIndex();
+            uint16_t id = m_lightData.GetFreeSlotIndex();
 
-            if (id == IndexedDataVector<PointLightData>::NoFreeSlot)
+            if (id == MultiIndexedDataVector<PointLightData>::NoFreeSlot)
             {
                 return LightHandle::Null;
             }
@@ -86,14 +96,14 @@ namespace AZ
             {
                 for (int i = 0; i < PointLightData::NumShadowFaces; ++i)
                 {
-                    ShadowId shadowId = ShadowId(m_pointLightData.GetData(handle.GetIndex()).m_shadowIndices[i]);
+                    ShadowId shadowId = ShadowId(m_lightData.GetData<0>(handle.GetIndex()).m_shadowIndices[i]);
                     if (shadowId.IsValid())
                     {
                         m_shadowFeatureProcessor->ReleaseShadow(shadowId);
                     }
                 }
 
-                m_pointLightData.RemoveIndex(handle.GetIndex());
+                m_lightData.RemoveIndex(handle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
                 handle.Reset();
                 return true;
@@ -108,7 +118,8 @@ namespace AZ
             LightHandle handle = AcquireLight();
             if (handle.IsValid())
             {
-                m_pointLightData.GetData(handle.GetIndex()) = m_pointLightData.GetData(sourceLightHandle.GetIndex());
+                m_lightData.GetData<0>(handle.GetIndex()) = m_lightData.GetData<0>(sourceLightHandle.GetIndex());
+                m_lightData.GetData<1>(handle.GetIndex()) = m_lightData.GetData<1>(sourceLightHandle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
             }
             return handle;
@@ -121,9 +132,30 @@ namespace AZ
 
             if (m_deviceBufferNeedsUpdate)
             {
-                m_lightBufferHandler.UpdateBuffer(m_pointLightData.GetDataVector());
+                m_lightBufferHandler.UpdateBuffer(m_lightData.GetDataVector<0>());
                 m_deviceBufferNeedsUpdate = false;
             }
+
+            if (r_enablePerMeshShaderOptionFlags)
+            {
+                auto hasShadow = [&](const AZ::Sphere& sphere) -> bool
+                {
+                    LightHandle::IndexType index = m_lightData.GetIndexForData<1>(&sphere);
+                    ShadowId shadowId = ShadowId(m_lightData.GetData<0>(index).m_shadowIndices[0]);
+                    return shadowId.IsValid();
+                };
+                auto noShadow = [&](const AZ::Sphere& sphere) -> bool
+                {
+                    return !hasShadow(sphere);
+                };
+
+                // Mark meshes that have point lights without shadow using only the light flag.
+                MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<1>()), m_lightMeshFlag.GetIndex(), noShadow);
+
+                // Mark meshes that have point lights with shadow using a combination of light and shadow flags.
+                uint32_t lightAndShadow = m_lightMeshFlag.GetIndex() | m_shadowMeshFlag.GetIndex();
+                MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<1>()), lightAndShadow, hasShadow);
+            }
         }
 
         void PointLightFeatureProcessor::Render(const PointLightFeatureProcessor::RenderPacket& packet)
@@ -142,7 +174,7 @@ namespace AZ
 
             auto transformedColor = AZ::RPI::TransformColor(lightRgbIntensity, AZ::RPI::ColorSpaceId::LinearSRGB, AZ::RPI::ColorSpaceId::ACEScg);
 
-            AZStd::array<float, 3>& rgbIntensity = m_pointLightData.GetData(handle.GetIndex()).m_rgbIntensity;
+            AZStd::array<float, 3>& rgbIntensity = m_lightData.GetData<0>(handle.GetIndex()).m_rgbIntensity;
             rgbIntensity[0] = transformedColor.GetR();
             rgbIntensity[1] = transformedColor.GetG();
             rgbIntensity[2] = transformedColor.GetB();
@@ -154,9 +186,11 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PointLightFeatureProcessor::SetPosition().");
 
-            AZStd::array<float, 3>& position = m_pointLightData.GetData(handle.GetIndex()).m_position;
+            AZStd::array<float, 3>& position = m_lightData.GetData<0>(handle.GetIndex()).m_position;
             lightPosition.StoreToFloat3(position.data());
 
+            m_lightData.GetData<1>(handle.GetIndex()).SetCenter(lightPosition);
+
             m_deviceBufferNeedsUpdate = true;
             UpdateShadow(handle);
         }
@@ -166,7 +200,9 @@ namespace AZ
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PointLightFeatureProcessor::SetAttenuationRadius().");
 
             attenuationRadius = AZStd::max<float>(attenuationRadius, 0.001f); // prevent divide by zero.
-            m_pointLightData.GetData(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+            m_lightData.GetData<0>(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+            m_lightData.GetData<1>(handle.GetIndex()).SetRadius(attenuationRadius);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -174,7 +210,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PointLightFeatureProcessor::SetBulbRadius().");
 
-            m_pointLightData.GetData(handle.GetIndex()).m_bulbRadius = bulbRadius;
+            m_lightData.GetData<0>(handle.GetIndex()).m_bulbRadius = bulbRadius;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -190,7 +226,7 @@ namespace AZ
 
         void PointLightFeatureProcessor::SetShadowsEnabled(LightHandle handle, bool enabled)
         {
-            auto& light = m_pointLightData.GetData(handle.GetIndex());
+            auto& light = m_lightData.GetData<0>(handle.GetIndex());
             for (int i = 0; i < PointLightData::NumShadowFaces; ++i)
             {
                 ShadowId shadowId = ShadowId(light.m_shadowIndices[i]);
@@ -217,7 +253,11 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PointLightFeatureProcessor::SetPointData().");
 
-            m_pointLightData.GetData(handle.GetIndex()) = data;
+            m_lightData.GetData<0>(handle.GetIndex()) = data;
+
+            AZ::Vector3 position = AZ::Vector3::CreateFromFloat3(data.m_position.data());
+            float radius = LightCommon::GetRadiusFromInvRadiusSquared(data.m_invAttenuationRadiusSquared);
+            m_lightData.GetData<1>(handle.GetIndex()).Set(AZ::Sphere(position, radius));
             m_deviceBufferNeedsUpdate = true;
             UpdateShadow(handle);
         }
@@ -226,7 +266,7 @@ namespace AZ
         {
             constexpr float SqrtHalf = 0.707106781187f; // sqrt(0.5);
 
-            const auto& pointLight = m_pointLightData.GetData(handle.GetIndex());
+            const auto& pointLight = m_lightData.GetData<0>(handle.GetIndex());
             for (int i = 0; i < PointLightData::NumShadowFaces; ++i)
             {
                 ShadowId shadowId = ShadowId(pointLight.m_shadowIndices[i]);
@@ -262,7 +302,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PointLightFeatureProcessor::SetShadowSetting().");
 
-            auto& light = m_pointLightData.GetData(handle.GetIndex());
+            auto& light = m_lightData.GetData<0>(handle.GetIndex());
             for (int lightIndex = 0; lightIndex < PointLightData::NumShadowFaces; ++lightIndex)
             {
                 ShadowId shadowId = ShadowId(light.m_shadowIndices[lightIndex]);
@@ -284,7 +324,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PointLightFeatureProcessor::SetAffectsGI().");
 
-            m_pointLightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGI = affectsGI;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -292,7 +332,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PointLightFeatureProcessor::SetAffectsGIFactor().");
 
-            m_pointLightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
             m_deviceBufferNeedsUpdate = true;
         }
 

+ 5 - 2
Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.h

@@ -8,10 +8,11 @@
 
 #pragma once
 
+#include <AzCore/Math/Sphere.h>
 #include <Atom/Feature/CoreLights/PhotometricValue.h>
 #include <Atom/Feature/CoreLights/PointLightFeatureProcessorInterface.h>
 #include <Atom/Feature/Utils/GpuBufferHandler.h>
-#include <Atom/Feature/Utils/IndexedDataVector.h>
+#include <Atom/Feature/Utils/MultiIndexedDataVector.h>
 #include <Shadows/ProjectedShadowFeatureProcessor.h>
 
 namespace AZ
@@ -71,8 +72,10 @@ namespace AZ
             void SetShadowSetting(LightHandle handle, Functor&&, ParamType&& param);
             ProjectedShadowFeatureProcessor* m_shadowFeatureProcessor = nullptr;
 
-            IndexedDataVector<PointLightData> m_pointLightData;
+            MultiIndexedDataVector<PointLightData, AZ::Sphere> m_lightData;
             GpuBufferHandler m_lightBufferHandler;
+            RHI::Handle<uint32_t> m_lightMeshFlag;
+            RHI::Handle<uint32_t> m_shadowMeshFlag;
             bool m_deviceBufferNeedsUpdate = false;
 
             AZStd::array<AZ::Transform, PointLightData::NumShadowFaces> m_pointShadowTransforms;

+ 63 - 35
Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.cpp

@@ -12,6 +12,7 @@
 #include <AzCore/Math/Vector3.h>
 #include <AzCore/Math/Color.h>
 
+#include <Atom/Feature/Mesh/MeshFeatureProcessor.h>
 #include <Atom/Feature/CoreLights/CoreLightsConstants.h>
 
 #include <Atom/RHI/Factory.h>
@@ -61,28 +62,34 @@ namespace AZ::Render
         m_lightPolygonPointBufferHandler = GpuBufferHandler(desc);
 
         Interface<ILtcCommon>::Get()->LoadMatricesForSrg(GetParentScene()->GetShaderResourceGroup());
+
+        MeshFeatureProcessor* meshFeatureProcessor = GetParentScene()->GetFeatureProcessor<MeshFeatureProcessor>();
+        if (meshFeatureProcessor)
+        {
+            m_lightMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enablePolygonLights"));
+        }
     }
 
     void PolygonLightFeatureProcessor::Deactivate()
     {
-        m_polygonLightData.Clear();
+        m_lightData.Clear();
         m_lightBufferHandler.Release();
         m_lightPolygonPointBufferHandler.Release();
     }
 
     PolygonLightFeatureProcessor::LightHandle PolygonLightFeatureProcessor::AcquireLight()
     {
-        uint16_t id = m_polygonLightData.GetFreeSlotIndex();
-        if (id == m_polygonLightData.NoFreeSlot)
+        uint16_t id = m_lightData.GetFreeSlotIndex();
+        if (id == m_lightData.NoFreeSlot)
         {
             return LightHandle::Null;
         }
         else
         {
             // Set initial values for the start / end index of the light. Only the end needs to be recalculated as points are added / removed.
-            PolygonLightData& lightData = m_polygonLightData.GetData<0>(id);
-            lightData.SetStartIndex(m_polygonLightData.GetRawIndex(id) * MaxPolygonPoints);
-            lightData.SetEndIndex(m_polygonLightData.GetRawIndex(id) * MaxPolygonPoints + 1);
+            PolygonLightData& lightData = m_lightData.GetData<0>(id);
+            lightData.SetStartIndex(m_lightData.GetRawIndex(id) * MaxPolygonPoints);
+            lightData.SetEndIndex(m_lightData.GetRawIndex(id) * MaxPolygonPoints + 1);
             return LightHandle(id);
         }
         // Intentionally don't set m_deviceBufferNeedsUpdate to true since the light doesn't yet have data.
@@ -92,7 +99,7 @@ namespace AZ::Render
     {
         if (handle.IsValid())
         {
-            auto movedIndex = m_polygonLightData.RemoveIndex(handle.GetIndex());
+            auto movedIndex = m_lightData.RemoveIndex(handle.GetIndex());
 
             // fix up the start/end index for the light that moved into this deleted light's position since its
             // points were also moved.
@@ -116,11 +123,12 @@ namespace AZ::Render
         if (handle.IsValid())
         {
             // Duplicate the light data, update the start / end index fields to point to the new point buffer location.
-            PolygonLightData& lightData = m_polygonLightData.GetData<0>(handle.GetIndex());
-            lightData = m_polygonLightData.GetData<0>(sourceLightHandle.GetIndex());
+            PolygonLightData& lightData = m_lightData.GetData<0>(handle.GetIndex());
+            lightData = m_lightData.GetData<0>(sourceLightHandle.GetIndex());
             EvaluateStartEndIndices(handle.GetIndex());
 
-            m_polygonLightData.GetData<1>(handle.GetIndex()) = m_polygonLightData.GetData<1>(sourceLightHandle.GetIndex());
+            m_lightData.GetData<1>(handle.GetIndex()) = m_lightData.GetData<1>(sourceLightHandle.GetIndex());
+            m_lightData.GetData<2>(handle.GetIndex()) = m_lightData.GetData<2>(sourceLightHandle.GetIndex());
 
             m_deviceBufferNeedsUpdate = true;
         }
@@ -134,18 +142,23 @@ namespace AZ::Render
 
         if (m_deviceBufferNeedsUpdate)
         {
-            m_lightBufferHandler.UpdateBuffer(m_polygonLightData.GetDataVector<0>());
+            m_lightBufferHandler.UpdateBuffer(m_lightData.GetDataVector<0>());
 
-            if (m_polygonLightData.GetDataCount() > 0)
+            if (m_lightData.GetDataCount() > 0)
             {
                 // A single array of MaxPolygonPoints points exists for each light, but we want to treat each
                 // individual point as its own element instead of each array being its own element. Since all
                 // the arrays are stored in a contiguous vector, we can treat it as one giant array.
-                const LightPosition* firstPosition = m_polygonLightData.GetDataVector<1>().at(0).data();
-                m_lightPolygonPointBufferHandler.UpdateBuffer(firstPosition, static_cast<uint32_t>(m_polygonLightData.GetDataCount() * MaxPolygonPoints));
+                const LightPosition* firstPosition = m_lightData.GetDataVector<1>().at(0).data();
+                m_lightPolygonPointBufferHandler.UpdateBuffer(firstPosition, static_cast<uint32_t>(m_lightData.GetDataCount() * MaxPolygonPoints));
             }
             m_deviceBufferNeedsUpdate = false;
         }
+
+        if (r_enablePerMeshShaderOptionFlags)
+        {
+            MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<2>()), m_lightMeshFlag.GetIndex());
+        }
     }
 
     void PolygonLightFeatureProcessor::Render(const PolygonLightFeatureProcessor::RenderPacket& packet)
@@ -165,12 +178,8 @@ namespace AZ::Render
 
         auto transformedColor = AZ::RPI::TransformColor(lightRgbIntensity, AZ::RPI::ColorSpaceId::LinearSRGB, AZ::RPI::ColorSpaceId::ACEScg);
 
-        AZStd::array<float, 3>& rgbIntensity = m_polygonLightData.GetData<0>(handle.GetIndex()).m_rgbIntensityNits;
-
-        // Maintain sign bit in redsince it stores the convex / concave information of first two edges.
-        rgbIntensity[0] = copysignf(transformedColor.GetR(), rgbIntensity[0]);
-        rgbIntensity[1] = transformedColor.GetG();
-        rgbIntensity[2] = transformedColor.GetB();
+        AZStd::array<float, 3>& rgbIntensity = m_lightData.GetData<0>(handle.GetIndex()).m_rgbIntensityNits;
+        transformedColor.GetAsVector3().StoreToFloat3(rgbIntensity.data());
 
         m_deviceBufferNeedsUpdate = true;
     }
@@ -179,9 +188,11 @@ namespace AZ::Render
     {
         AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PolygonLightFeatureProcessor::SetTransform().");
 
-        PolygonLightData& data = m_polygonLightData.GetData<0>(handle.GetIndex());
+        PolygonLightData& data = m_lightData.GetData<0>(handle.GetIndex());
         position.StoreToFloat3(data.m_position.data());
 
+        UpdateBounds(handle);
+
         m_deviceBufferNeedsUpdate = true;
     }
 
@@ -189,10 +200,13 @@ namespace AZ::Render
     {
         AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PolygonLightFeatureProcessor::SetLightEmitsBothDirections().");
 
-        float& invAttenuationRadiusSquared = m_polygonLightData.GetData<0>(handle.GetIndex()).m_invAttenuationRadiusSquared;
+        float& invAttenuationRadiusSquared = m_lightData.GetData<0>(handle.GetIndex()).m_invAttenuationRadiusSquared;
 
         // Light emitting both directions is stored in the sign of the attenuation radius since that must always be positive.
         invAttenuationRadiusSquared = lightEmitsBothDirections ? -abs(invAttenuationRadiusSquared) : abs(invAttenuationRadiusSquared);
+
+        UpdateBounds(handle);
+
         m_deviceBufferNeedsUpdate = true;
     }
 
@@ -201,9 +215,12 @@ namespace AZ::Render
         AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to PolygonLightFeatureProcessor::SetAttenuationRadius().");
 
         attenuationRadius = AZStd::max<float>(attenuationRadius, 0.001f); // prevent divide by zero.
-        float& invAttenuationRadiusSquared = m_polygonLightData.GetData<0>(handle.GetIndex()).m_invAttenuationRadiusSquared;
+        float& invAttenuationRadiusSquared = m_lightData.GetData<0>(handle.GetIndex()).m_invAttenuationRadiusSquared;
         float sign = invAttenuationRadiusSquared < 0.0f ? -1.0f : 1.0f; // preserve SetLightEmitsBothDirections data stored in the sign.
         invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius) * sign;
+
+        UpdateBounds(handle);
+
         m_deviceBufferNeedsUpdate = true;
     }
 
@@ -217,7 +234,7 @@ namespace AZ::Render
             return; // not enough points
         }
 
-        PolygonPoints& pointArray = m_polygonLightData.GetData<1>(handle.GetIndex());
+        PolygonPoints& pointArray = m_lightData.GetData<1>(handle.GetIndex());
         uint32_t clippedCount = AZ::GetMin<uint32_t>(vertexCount, MaxPolygonPoints);
         for (uint32_t i = 0; i < clippedCount; ++i)
         {
@@ -225,13 +242,11 @@ namespace AZ::Render
             pointArray.at(i).y = vertices[i].GetY();
             pointArray.at(i).z = vertices[i].GetZ();
         }
-        PolygonLightData& data = m_polygonLightData.GetData<0>(handle.GetIndex());
+        PolygonLightData& data = m_lightData.GetData<0>(handle.GetIndex());
         data.SetEndIndex(data.GetStartIndex() + clippedCount);
+        direction.StoreToFloat3(data.m_direction.data());
 
-        Vector3 directionFromEdges = CrossEdges(vertices[0], vertices[1], vertices[2]);
-
-        float& red = data.m_rgbIntensityNits.at(0);
-        red = copysignf(red, directionFromEdges.Dot(direction));
+        UpdateBounds(handle);
 
         m_deviceBufferNeedsUpdate = true;
     }
@@ -248,17 +263,30 @@ namespace AZ::Render
 
     void PolygonLightFeatureProcessor::EvaluateStartEndIndices(PolygonLightDataVector::IndexType index)
     {
-        PolygonLightData& lightData = m_polygonLightData.GetData<0>(index);
+        PolygonLightData& lightData = m_lightData.GetData<0>(index);
         uint32_t length = lightData.GetEndIndex() - lightData.GetStartIndex();
-        lightData.SetStartIndex(m_polygonLightData.GetRawIndex(index) * MaxPolygonPoints);
+        lightData.SetStartIndex(m_lightData.GetRawIndex(index) * MaxPolygonPoints);
         lightData.SetEndIndex(lightData.GetStartIndex() + length);
     }
 
-    Vector3 PolygonLightFeatureProcessor::CrossEdges(const LightPosition& p0, const LightPosition& p1, const LightPosition& p2)
+    void PolygonLightFeatureProcessor::UpdateBounds(LightHandle handle)
     {
-        Vector3 edge1 = Vector3(p1.x, p1.y, p1.z) - Vector3(p0.x, p0.y, p0.z);
-        Vector3 edge2 = Vector3(p1.x, p1.y, p1.z) - Vector3(p2.x, p2.y, p2.z);
-        return edge2.Cross(edge1);
+        PolygonLightData data = m_lightData.GetData<0>(handle.GetIndex());
+        MeshCommon::BoundsVariant bounds = m_lightData.GetData<2>(handle.GetIndex());
+
+        AZ::Vector3 position = AZ::Vector3::CreateFromFloat3(data.m_position.data());
+        float radius = LightCommon::GetRadiusFromInvRadiusSquared(abs(data.m_invAttenuationRadiusSquared));
+
+        // Emits both directions is stored in the sign of m_invAttenuationRadiusSquared.
+        if (data.m_invAttenuationRadiusSquared < 0.0f)
+        {
+            bounds.emplace<Sphere>(AZ::Sphere(position, radius));
+        }
+        else
+        {
+            AZ::Vector3 normal = AZ::Vector3::CreateFromFloat3(data.m_direction.data());
+            bounds.emplace<Hemisphere>(AZ::Hemisphere(position, radius, normal));
+        }
     }
 
 } // namespace AZ::Render

+ 8 - 5
Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.h

@@ -8,7 +8,9 @@
 
 #pragma once
 
+#include <Atom/Feature/CoreLights/LightCommon.h>
 #include <Atom/Feature/CoreLights/PolygonLightFeatureProcessorInterface.h>
+#include <Atom/Feature/Mesh/MeshCommon.h>
 #include <Atom/Feature/Utils/GpuBufferHandler.h>
 #include <Atom/Feature/Utils/MultiIndexedDataVector.h>
 #include <Atom/RPI.Reflect/Asset/AssetUtils.h>
@@ -70,19 +72,20 @@ namespace AZ
                 }
             };
 
-            // Calculates cross product between vectors p1->p0 and p1->p2
-            static Vector3 CrossEdges(const LightPosition& p0, const LightPosition& p1, const LightPosition& p2);
-
             using PolygonPoints = AZStd::array<LightPosition, MaxPolygonPoints>;
-            using PolygonLightDataVector = MultiIndexedDataVector<PolygonLightData, PolygonPoints>;
+            using PolygonLightDataVector = MultiIndexedDataVector<PolygonLightData, PolygonPoints, MeshCommon::BoundsVariant>;
 
             // Recalculates the start / end indices of the points for this polygon if it recently moved in memory.
             void EvaluateStartEndIndices(PolygonLightDataVector::IndexType index);
 
-            PolygonLightDataVector m_polygonLightData;
+            void UpdateBounds(LightHandle handle);
+
+            PolygonLightDataVector m_lightData;
             
             GpuBufferHandler m_lightBufferHandler;
             GpuBufferHandler m_lightPolygonPointBufferHandler;
+
+            RHI::Handle<uint32_t> m_lightMeshFlag;
             bool m_deviceBufferNeedsUpdate = false;
         };
     } // namespace Render

+ 79 - 16
Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.cpp

@@ -13,6 +13,8 @@
 #include <AzCore/Math/Color.h>
 
 #include <Atom/Feature/CoreLights/CoreLightsConstants.h>
+#include <Atom/Feature/CoreLights/LightCommon.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessor.h>
 
 #include <Atom/RHI/Factory.h>
 
@@ -54,19 +56,26 @@ namespace AZ
             m_lightBufferHandler = GpuBufferHandler(desc);
 
             Interface<ILtcCommon>::Get()->LoadMatricesForSrg(GetParentScene()->GetShaderResourceGroup());
+
+            MeshFeatureProcessor* meshFeatureProcessor = GetParentScene()->GetFeatureProcessor<MeshFeatureProcessor>();
+            if (meshFeatureProcessor)
+            {
+                m_lightLtcMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enableQuadLightLTC"));
+                m_lightApproxMeshFlag = meshFeatureProcessor->GetFlagRegistry()->AcquireTag(AZ::Name("o_enableQuadLightApprox"));
+            }
         }
 
         void QuadLightFeatureProcessor::Deactivate()
         {
-            m_quadLightData.Clear();
+            m_lightData.Clear();
             m_lightBufferHandler.Release();
         }
 
         QuadLightFeatureProcessor::LightHandle QuadLightFeatureProcessor::AcquireLight()
         {
-            uint16_t id = m_quadLightData.GetFreeSlotIndex();
+            uint16_t id = m_lightData.GetFreeSlotIndex();
 
-            if (id == m_quadLightData.NoFreeSlot)
+            if (id == m_lightData.NoFreeSlot)
             {
                 return LightHandle::Null;
             }
@@ -81,7 +90,7 @@ namespace AZ
         {
             if (handle.IsValid())
             {
-                m_quadLightData.RemoveIndex(handle.GetIndex());
+                m_lightData.RemoveIndex(handle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
                 handle.Reset();
                 return true;
@@ -96,7 +105,8 @@ namespace AZ
             LightHandle handle = AcquireLight();
             if (handle.IsValid())
             {
-                m_quadLightData.GetData(handle.GetIndex()) = m_quadLightData.GetData(sourceLightHandle.GetIndex());
+                m_lightData.GetData<0>(handle.GetIndex()) = m_lightData.GetData<0>(sourceLightHandle.GetIndex());
+                m_lightData.GetData<1>(handle.GetIndex()) = m_lightData.GetData<1>(sourceLightHandle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
             }
             return handle;
@@ -109,9 +119,26 @@ namespace AZ
 
             if (m_deviceBufferNeedsUpdate)
             {
-                m_lightBufferHandler.UpdateBuffer(m_quadLightData.GetDataVector());
+                m_lightBufferHandler.UpdateBuffer(m_lightData.GetDataVector<0>());
                 m_deviceBufferNeedsUpdate = false;
             }
+
+            if (r_enablePerMeshShaderOptionFlags)
+            {
+                auto usesLtc = [&](const MeshCommon::BoundsVariant& bounds) -> bool
+                {
+                    LightHandle::IndexType index = m_lightData.GetIndexForData<1>(&bounds);
+                    return (m_lightData.GetData<0>(index).m_flags & QuadLightFlag::UseFastApproximation) == 0;
+                };
+                auto usesFastApproximation = [&](const MeshCommon::BoundsVariant& bounds) -> bool
+                {
+                    LightHandle::IndexType index = m_lightData.GetIndexForData<1>(&bounds);
+                    return (m_lightData.GetData<0>(index).m_flags & QuadLightFlag::UseFastApproximation) > 0;
+                };
+
+                MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<1>()), m_lightLtcMeshFlag.GetIndex(), usesLtc);
+                MeshCommon::MarkMeshesWithFlag(GetParentScene(), AZStd::span(m_lightData.GetDataVector<1>()), m_lightApproxMeshFlag.GetIndex(), usesFastApproximation);
+            }
         }
 
         void QuadLightFeatureProcessor::Render(const QuadLightFeatureProcessor::RenderPacket& packet)
@@ -130,7 +157,7 @@ namespace AZ
 
             auto transformedColor = AZ::RPI::TransformColor(lightRgbIntensity, AZ::RPI::ColorSpaceId::LinearSRGB, AZ::RPI::ColorSpaceId::ACEScg);
 
-            AZStd::array<float, 3>& rgbIntensity = m_quadLightData.GetData(handle.GetIndex()).m_rgbIntensityNits;
+            AZStd::array<float, 3>& rgbIntensity = m_lightData.GetData<0>(handle.GetIndex()).m_rgbIntensityNits;
             rgbIntensity[0] = transformedColor.GetR();
             rgbIntensity[1] = transformedColor.GetG();
             rgbIntensity[2] = transformedColor.GetB();
@@ -142,9 +169,11 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetPosition().");
 
-            AZStd::array<float, 3>& position = m_quadLightData.GetData(handle.GetIndex()).m_position;
+            AZStd::array<float, 3>& position = m_lightData.GetData<0>(handle.GetIndex()).m_position;
             lightPosition.StoreToFloat3(position.data());
 
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -152,9 +181,12 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetOrientation().");
 
-            QuadLightData& data = m_quadLightData.GetData(handle.GetIndex());
+            QuadLightData& data = m_lightData.GetData<0>(handle.GetIndex());
             orientation.TransformVector(Vector3::CreateAxisX()).StoreToFloat3(data.m_leftDir.data());
             orientation.TransformVector(Vector3::CreateAxisY()).StoreToFloat3(data.m_upDir.data());
+
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -162,7 +194,10 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetLightEmitsBothDirections().");
 
-            m_quadLightData.GetData(handle.GetIndex()).SetFlag(QuadLightFlag::EmitBothDirections, lightEmitsBothDirections);
+            m_lightData.GetData<0>(handle.GetIndex()).SetFlag(QuadLightFlag::EmitBothDirections, lightEmitsBothDirections);
+
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -170,7 +205,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetLightEmitsBothDirections().");
 
-            m_quadLightData.GetData(handle.GetIndex()).SetFlag(QuadLightFlag::UseFastApproximation, useFastApproximation);
+            m_lightData.GetData<0>(handle.GetIndex()).SetFlag(QuadLightFlag::UseFastApproximation, useFastApproximation);
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -179,7 +214,10 @@ namespace AZ
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetAttenuationRadius().");
 
             attenuationRadius = AZStd::max<float>(attenuationRadius, 0.001f); // prevent divide by zero.
-            m_quadLightData.GetData(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+            m_lightData.GetData<0>(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -187,9 +225,12 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetQuadDimensions().");
 
-            QuadLightData& data = m_quadLightData.GetData(handle.GetIndex());
+            QuadLightData& data = m_lightData.GetData<0>(handle.GetIndex());
             data.m_halfWidth = width * 0.5f;
             data.m_halfHeight = height * 0.5f;
+
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -197,7 +238,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetAffectsGI().");
 
-            m_quadLightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGI = affectsGI;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -205,7 +246,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetAffectsGIFactor().");
 
-            m_quadLightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
+            m_lightData.GetData<0>(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -213,7 +254,10 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to QuadLightFeatureProcessor::SetQuadData().");
 
-            m_quadLightData.GetData(handle.GetIndex()) = data;
+            m_lightData.GetData<0>(handle.GetIndex()) = data;
+
+            UpdateBounds(handle);
+
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -227,5 +271,24 @@ namespace AZ
             return m_lightBufferHandler.GetElementCount();
         }
 
+        void QuadLightFeatureProcessor::UpdateBounds(LightHandle handle)
+        {
+            const QuadLightData& data = m_lightData.GetData<0>(handle.GetIndex());
+            MeshCommon::BoundsVariant& bounds = m_lightData.GetData<1>(handle.GetIndex());
+
+            AZ::Vector3 position = AZ::Vector3::CreateFromFloat3(data.m_position.data());
+            float radius = LightCommon::GetRadiusFromInvRadiusSquared(data.m_invAttenuationRadiusSquared);
+
+            if ((data.m_flags & QuadLightFlag::EmitBothDirections) > 0)
+            {
+                bounds.emplace<Sphere>(AZ::Sphere(position, radius));
+            }
+            else
+            {
+                AZ::Vector3 normal = AZ::Vector3::CreateFromFloat3(data.m_upDir.data());
+                bounds.emplace<Hemisphere>(AZ::Hemisphere(position, radius, normal));
+            }
+        }
+
     } // namespace Render
 } // namespace AZ

+ 8 - 2
Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.h

@@ -8,9 +8,11 @@
 
 #pragma once
 
+#include <Atom/Feature/CoreLights/LightCommon.h>
 #include <Atom/Feature/CoreLights/QuadLightFeatureProcessorInterface.h>
+#include <Atom/Feature/Mesh/MeshCommon.h>
 #include <Atom/Feature/Utils/GpuBufferHandler.h>
-#include <Atom/Feature/Utils/IndexedDataVector.h>
+#include <Atom/Feature/Utils/MultiIndexedDataVector.h>
 
 namespace AZ
 {
@@ -57,10 +59,14 @@ namespace AZ
         private:
             QuadLightFeatureProcessor(const QuadLightFeatureProcessor&) = delete;
 
+            void UpdateBounds(LightHandle handle);
+
             static constexpr const char* FeatureProcessorName = "QuadLightFeatureProcessor";
 
-            IndexedDataVector<QuadLightData> m_quadLightData;
+            MultiIndexedDataVector<QuadLightData, MeshCommon::BoundsVariant> m_lightData;
             GpuBufferHandler m_lightBufferHandler;
+            RHI::Handle<uint32_t> m_lightLtcMeshFlag;
+            RHI::Handle<uint32_t> m_lightApproxMeshFlag;
             bool m_deviceBufferNeedsUpdate = false;
         };
     } // namespace Render

+ 12 - 15
Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.cpp

@@ -11,8 +11,6 @@
 #include <AzCore/Math/Vector3.h>
 #include <AzCore/Math/Color.h>
 
-#include <Atom/Feature/CoreLights/CoreLightsConstants.h>
-
 #include <Atom/RHI/Factory.h>
 
 #include <Atom/RPI.Public/ColorManagement/TransformColor.h>
@@ -45,7 +43,7 @@ namespace AZ
             desc.m_bufferName = "SimplePointLightBuffer";
             desc.m_bufferSrgName = "m_simplePointLights";
             desc.m_elementCountSrgName = "m_simplePointLightCount";
-            desc.m_elementSize = sizeof(SimplePointLightData);
+            desc.m_elementSize = sizeof(ShaderData);
             desc.m_srgLayout = RPI::RPISystemInterface::Get()->GetViewSrgLayout().get();
 
             m_lightBufferHandler = GpuBufferHandler(desc);
@@ -53,15 +51,15 @@ namespace AZ
 
         void SimplePointLightFeatureProcessor::Deactivate()
         {
-            m_pointLightData.Clear();
+            m_lightData.Clear();
             m_lightBufferHandler.Release();
         }
 
         SimplePointLightFeatureProcessor::LightHandle SimplePointLightFeatureProcessor::AcquireLight()
         {
-            uint16_t id = m_pointLightData.GetFreeSlotIndex();
+            uint16_t id = m_lightData.GetFreeSlotIndex();
 
-            if (id == IndexedDataVector<SimplePointLightData>::NoFreeSlot)
+            if (id == LightContainer::NoFreeSlot)
             {
                 return LightHandle::Null;
             }
@@ -76,7 +74,7 @@ namespace AZ
         {
             if (handle.IsValid())
             {
-                m_pointLightData.RemoveIndex(handle.GetIndex());
+                m_lightData.RemoveIndex(handle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
                 handle.Reset();
                 return true;
@@ -91,7 +89,7 @@ namespace AZ
             LightHandle handle = AcquireLight();
             if (handle.IsValid())
             {
-                m_pointLightData.GetData(handle.GetIndex()) = m_pointLightData.GetData(sourceLightHandle.GetIndex());
+                m_lightData.GetData(handle.GetIndex()) = m_lightData.GetData(sourceLightHandle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
             }
             return handle;
@@ -104,7 +102,7 @@ namespace AZ
 
             if (m_deviceBufferNeedsUpdate)
             {
-                m_lightBufferHandler.UpdateBuffer(m_pointLightData.GetDataVector());
+                m_lightBufferHandler.UpdateBuffer(m_lightData.GetDataVector());
                 m_deviceBufferNeedsUpdate = false;
             }
         }
@@ -125,7 +123,7 @@ namespace AZ
 
             auto transformedColor = AZ::RPI::TransformColor(lightRgbIntensity, AZ::RPI::ColorSpaceId::LinearSRGB, AZ::RPI::ColorSpaceId::ACEScg);
 
-            AZStd::array<float, 3>& rgbIntensity = m_pointLightData.GetData(handle.GetIndex()).m_rgbIntensity;
+            AZStd::array<float, 3>& rgbIntensity = m_lightData.GetData(handle.GetIndex()).m_rgbIntensity;
             rgbIntensity[0] = transformedColor.GetR();
             rgbIntensity[1] = transformedColor.GetG();
             rgbIntensity[2] = transformedColor.GetB();
@@ -137,9 +135,8 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimplePointLightFeatureProcessor::SetPosition().");
 
-            AZStd::array<float, 3>& position = m_pointLightData.GetData(handle.GetIndex()).m_position;
+            AZStd::array<float, 3>& position = m_lightData.GetData(handle.GetIndex()).m_position;
             lightPosition.StoreToFloat3(position.data());
-
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -148,7 +145,7 @@ namespace AZ
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimplePointLightFeatureProcessor::SetAttenuationRadius().");
 
             attenuationRadius = AZStd::max<float>(attenuationRadius, 0.001f); // prevent divide by zero.
-            m_pointLightData.GetData(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+            m_lightData.GetData(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -156,7 +153,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimplePointLightFeatureProcessor::SetAffectsGI().");
 
-            m_pointLightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
+            m_lightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -164,7 +161,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimplePointLightFeatureProcessor::SetAffectsGIFactor().");
 
-            m_pointLightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
+            m_lightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
             m_deviceBufferNeedsUpdate = true;
         }
 

+ 19 - 17
Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.h

@@ -8,6 +8,7 @@
 
 #pragma once
 
+#include <AzCore/Math/Sphere.h>
 #include <Atom/Feature/CoreLights/PhotometricValue.h>
 #include <Atom/Feature/CoreLights/SimplePointLightFeatureProcessorInterface.h>
 #include <Atom/Feature/Utils/GpuBufferHandler.h>
@@ -20,21 +21,6 @@ namespace AZ
 
     namespace Render
     {
-
-        struct SimplePointLightData
-        {
-            AZStd::array<float, 3> m_position = { { 0.0f, 0.0f, 0.0f } };
-            float m_invAttenuationRadiusSquared = 0.0f; // Inverse of the distance at which this light no longer has an effect, squared. Also used for falloff calculations.
-
-            AZStd::array<float, 3> m_rgbIntensity = { { 0.0f, 0.0f, 0.0f } };
-            float m_affectsGIFactor = 1.0f;
-
-            bool m_affectsGI = true;
-            float m_padding0 = 0.0f;
-            float m_padding1 = 0.0f;
-            float m_padding2 = 0.0f;
-        };
-
         class SimplePointLightFeatureProcessor final
             : public SimplePointLightFeatureProcessorInterface
         {
@@ -43,6 +29,20 @@ namespace AZ
 
             static void Reflect(AZ::ReflectContext* context);
 
+            struct ShaderData
+            {
+                AZStd::array<float, 3> m_position = { { 0.0f, 0.0f, 0.0f } };
+                float m_invAttenuationRadiusSquared = 0.0f; // Inverse of the distance at which this light no longer has an effect, squared. Also used for falloff calculations.
+
+                AZStd::array<float, 3> m_rgbIntensity = { { 0.0f, 0.0f, 0.0f } };
+                float m_affectsGIFactor = 1.0f;
+
+                bool m_affectsGI = true;
+                float m_padding0 = 0.0f;
+                float m_padding1 = 0.0f;
+                float m_padding2 = 0.0f;
+            };
+
             SimplePointLightFeatureProcessor();
             virtual ~SimplePointLightFeatureProcessor() = default;
 
@@ -62,7 +62,7 @@ namespace AZ
             void SetAffectsGI(LightHandle handle, bool affectsGI) override;
             void SetAffectsGIFactor(LightHandle handle, float affectsGIFactor) override;
 
-            const Data::Instance<RPI::Buffer>  GetLightBuffer() const;
+            const Data::Instance<RPI::Buffer> GetLightBuffer() const;
             uint32_t GetLightCount()const;
 
         private:
@@ -70,9 +70,11 @@ namespace AZ
 
             static constexpr const char* FeatureProcessorName = "SimplePointLightFeatureProcessor";
 
-            IndexedDataVector<SimplePointLightData> m_pointLightData;
+            using LightContainer = IndexedDataVector<ShaderData>;
+            LightContainer m_lightData;
             GpuBufferHandler m_lightBufferHandler;
             bool m_deviceBufferNeedsUpdate = false;
+
         };
     } // namespace Render
 } // namespace AZ

+ 15 - 16
Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.cpp

@@ -12,6 +12,7 @@
 #include <AzCore/Math/Color.h>
 
 #include <Atom/Feature/CoreLights/CoreLightsConstants.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessor.h>
 
 #include <Atom/RHI/Factory.h>
 
@@ -53,13 +54,13 @@ namespace AZ
 
         void SimpleSpotLightFeatureProcessor::Deactivate()
         {
-            m_pointLightData.Clear();
+            m_lightData.Clear();
             m_lightBufferHandler.Release();
         }
 
         SimpleSpotLightFeatureProcessor::LightHandle SimpleSpotLightFeatureProcessor::AcquireLight()
         {
-            uint16_t id = m_pointLightData.GetFreeSlotIndex();
+            uint16_t id = m_lightData.GetFreeSlotIndex();
 
             if (id == IndexedDataVector<SimpleSpotLightData>::NoFreeSlot)
             {
@@ -76,7 +77,7 @@ namespace AZ
         {
             if (handle.IsValid())
             {
-                m_pointLightData.RemoveIndex(handle.GetIndex());
+                m_lightData.RemoveIndex(handle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
                 handle.Reset();
                 return true;
@@ -91,7 +92,7 @@ namespace AZ
             LightHandle handle = AcquireLight();
             if (handle.IsValid())
             {
-                m_pointLightData.GetData(handle.GetIndex()) = m_pointLightData.GetData(sourceLightHandle.GetIndex());
+                m_lightData.GetData(handle.GetIndex()) = m_lightData.GetData(sourceLightHandle.GetIndex());
                 m_deviceBufferNeedsUpdate = true;
             }
             return handle;
@@ -104,7 +105,7 @@ namespace AZ
 
             if (m_deviceBufferNeedsUpdate)
             {
-                m_lightBufferHandler.UpdateBuffer(m_pointLightData.GetDataVector());
+                m_lightBufferHandler.UpdateBuffer(m_lightData.GetDataVector());
                 m_deviceBufferNeedsUpdate = false;
             }
         }
@@ -125,7 +126,7 @@ namespace AZ
 
             auto transformedColor = AZ::RPI::TransformColor(lightRgbIntensity, AZ::RPI::ColorSpaceId::LinearSRGB, AZ::RPI::ColorSpaceId::ACEScg);
 
-            AZStd::array<float, 3>& rgbIntensity = m_pointLightData.GetData(handle.GetIndex()).m_rgbIntensity;
+            AZStd::array<float, 3>& rgbIntensity = m_lightData.GetData(handle.GetIndex()).m_rgbIntensity;
             rgbIntensity[0] = transformedColor.GetR();
             rgbIntensity[1] = transformedColor.GetG();
             rgbIntensity[2] = transformedColor.GetB();
@@ -137,9 +138,8 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimpleSpotLightFeatureProcessor::SetPosition().");
 
-            AZStd::array<float, 3>& position = m_pointLightData.GetData(handle.GetIndex()).m_position;
+            AZStd::array<float, 3>& position = m_lightData.GetData(handle.GetIndex()).m_position;
             lightPosition.StoreToFloat3(position.data());
-
             m_deviceBufferNeedsUpdate = true;
         }
         
@@ -147,16 +147,16 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimpleSpotLightFeatureProcessor::SetDirection().");
 
-            AZStd::array<float, 3>& direction = m_pointLightData.GetData(handle.GetIndex()).m_direction;
+            AZStd::array<float, 3>& direction = m_lightData.GetData(handle.GetIndex()).m_direction;
             lightDirection.StoreToFloat3(direction.data());
-
             m_deviceBufferNeedsUpdate = true;
         }
 
         void SimpleSpotLightFeatureProcessor::SetConeAngles(LightHandle handle, float innerRadians, float outerRadians)
         {
-            m_pointLightData.GetData(handle.GetIndex()).m_cosInnerConeAngle = cosf(innerRadians);
-            m_pointLightData.GetData(handle.GetIndex()).m_cosOuterConeAngle = cosf(outerRadians);
+            SimpleSpotLightData& data = m_lightData.GetData(handle.GetIndex());
+            data.m_cosInnerConeAngle = cosf(innerRadians);
+            data.m_cosOuterConeAngle = cosf(outerRadians);
         }
 
         void SimpleSpotLightFeatureProcessor::SetAttenuationRadius(LightHandle handle, float attenuationRadius)
@@ -164,7 +164,7 @@ namespace AZ
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimpleSpotLightFeatureProcessor::SetAttenuationRadius().");
 
             attenuationRadius = AZStd::max<float>(attenuationRadius, 0.001f); // prevent divide by zero.
-            m_pointLightData.GetData(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
+            m_lightData.GetData(handle.GetIndex()).m_invAttenuationRadiusSquared = 1.0f / (attenuationRadius * attenuationRadius);
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -172,7 +172,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimpleSpotLightFeatureProcessor::SetAffectsGI().");
 
-            m_pointLightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
+            m_lightData.GetData(handle.GetIndex()).m_affectsGI = affectsGI;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -180,7 +180,7 @@ namespace AZ
         {
             AZ_Assert(handle.IsValid(), "Invalid LightHandle passed to SimpleSpotLightFeatureProcessor::SetAffectsGIFactor().");
 
-            m_pointLightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
+            m_lightData.GetData(handle.GetIndex()).m_affectsGIFactor = affectsGIFactor;
             m_deviceBufferNeedsUpdate = true;
         }
 
@@ -193,6 +193,5 @@ namespace AZ
         {
             return m_lightBufferHandler.GetElementCount();
         }
-
     } // namespace Render
 } // namespace AZ

+ 4 - 1
Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.h

@@ -8,6 +8,9 @@
 
 #pragma once
 
+#include <AzCore/Math/Frustum.h>
+#include <AzCore/Math/Hemisphere.h>
+#include <Atom/Feature/CoreLights/LightCommon.h>
 #include <Atom/Feature/CoreLights/PhotometricValue.h>
 #include <Atom/Feature/CoreLights/SimpleSpotLightFeatureProcessorInterface.h>
 #include <Atom/Feature/Utils/GpuBufferHandler.h>
@@ -73,7 +76,7 @@ namespace AZ
 
             static constexpr const char* FeatureProcessorName = "SimpleSpotLightFeatureProcessor";
 
-            IndexedDataVector<SimpleSpotLightData> m_pointLightData;
+            IndexedDataVector<SimpleSpotLightData> m_lightData;
             GpuBufferHandler m_lightBufferHandler;
             bool m_deviceBufferNeedsUpdate = false;
         };

+ 149 - 9
Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp

@@ -61,15 +61,27 @@ namespace AZ
             };
             RPI::ShaderSystemInterface::Get()->Connect(m_handleGlobalShaderOptionUpdate);
             EnableSceneNotification();
+
+            // Must read cvar from AZ::Console due to static variable in multiple libraries, see ghi-5537
+            bool enablePerMeshShaderOptionFlagsCvar = false;
+            if (auto* console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
+            {
+                console->GetCvarValue("r_enablePerMeshShaderOptionFlags", enablePerMeshShaderOptionFlagsCvar);
+
+                // push the cvars value so anything in this dll can access it directly.
+                console->PerformCommand(AZStd::string::format("r_enablePerMeshShaderOptionFlags %s", enablePerMeshShaderOptionFlagsCvar ? "true" : "false").c_str());
+            }
         }
 
         void MeshFeatureProcessor::Deactivate()
         {
+            m_flagRegistry.reset();
+
             m_handleGlobalShaderOptionUpdate.Disconnect();
 
             DisableSceneNotification();
             AZ_Warning("MeshFeatureProcessor", m_modelData.size() == 0,
-                "Deactivaing the MeshFeatureProcessor, but there are still outstanding mesh handles.\n"
+                "Deactivating the MeshFeatureProcessor, but there are still outstanding mesh handles.\n"
             );
             m_transformService = nullptr;
             m_forceRebuildDrawPackets = false;
@@ -163,11 +175,76 @@ namespace AZ
         void MeshFeatureProcessor::OnBeginPrepareRender()
         {
             m_meshDataChecker.soft_lock();
+
+            if (!r_enablePerMeshShaderOptionFlags && m_enablePerMeshShaderOptionFlags)
+            {
+                // Per mesh shader option flags was on, but now turned off, so reset all the shader options.
+                for (auto& model : m_modelData)
+                {
+                    for (RPI::MeshDrawPacketList& drawPacketList : model.m_drawPacketListsByLod)
+                    {
+                        for (RPI::MeshDrawPacket& drawPacket : drawPacketList)
+                        {
+                            m_flagRegistry->VisitTags(
+                                [&](AZ::Name shaderOption, [[maybe_unused]] FlagRegistry::TagType tag)
+                                {
+                                    drawPacket.UnsetShaderOption(shaderOption);
+                                }
+                            );
+                            drawPacket.Update(*GetParentScene(), true);
+                        }
+                    }
+                    model.m_cullable.m_flags = 0;
+                    model.m_cullable.m_prevFlags = 0;
+                    model.m_cullableNeedsRebuild = true;
+                    model.BuildCullable();
+                }
+            }
+
+            m_enablePerMeshShaderOptionFlags = r_enablePerMeshShaderOptionFlags;
+
+            if (m_enablePerMeshShaderOptionFlags)
+            {
+                for (auto& model : m_modelData)
+                {
+                    if (model.m_cullable.m_prevFlags != model.m_cullable.m_flags)
+                    {
+                        // Per mesh shader option flags have changed, so rebuild the draw packet with the new shader options.
+                        for (RPI::MeshDrawPacketList& drawPacketList : model.m_drawPacketListsByLod)
+                        {
+                            for (RPI::MeshDrawPacket& drawPacket : drawPacketList)
+                            {
+                                m_flagRegistry->VisitTags(
+                                    [&](AZ::Name shaderOption, FlagRegistry::TagType tag)
+                                    {
+                                        bool shaderOptionValue = (model.m_cullable.m_flags & tag.GetIndex()) > 0;
+                                        drawPacket.SetShaderOption(shaderOption, AZ::RPI::ShaderOptionValue(shaderOptionValue));
+                                    }
+                                );
+                                drawPacket.Update(*GetParentScene(), true);
+                            }
+                        }
+                        model.m_cullableNeedsRebuild = true;
+                        model.BuildCullable();
+                    }
+                }
+            }
+
         }
 
         void MeshFeatureProcessor::OnEndPrepareRender()
         {
             m_meshDataChecker.soft_unlock();
+
+            if (m_reportShaderOptionFlags)
+            {
+                m_reportShaderOptionFlags = false;
+                PrintShaderOptionFlags();
+            }
+            for (auto& model : m_modelData)
+            {
+                model.m_cullable.m_prevFlags = model.m_cullable.m_flags.exchange(0);
+            }
         }
 
         MeshFeatureProcessor::MeshHandle MeshFeatureProcessor::AcquireMesh(
@@ -513,6 +590,16 @@ namespace AZ
             }
         }
 
+
+        RHI::Ptr<MeshFeatureProcessor::FlagRegistry> MeshFeatureProcessor::GetFlagRegistry()
+        {
+            if (m_flagRegistry == nullptr)
+            {
+                m_flagRegistry = FlagRegistry::Create();
+            }
+            return m_flagRegistry;
+        };
+
         void MeshFeatureProcessor::ForceRebuildDrawPackets([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
         {
             m_forceRebuildDrawPackets = true;
@@ -536,7 +623,67 @@ namespace AZ
             }
         }
 
+        void MeshFeatureProcessor::ReportShaderOptionFlags([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
+        {
+            m_reportShaderOptionFlags = true;
+        }
+
+        void MeshFeatureProcessor::PrintShaderOptionFlags()
+        {
+            AZStd::map<FlagRegistry::TagType, AZ::Name> tags;
+            AZStd::string registeredFoundMessage = "Registered flags: ";
+
+            auto gatherTags = [&](const Name& name, FlagRegistry::TagType tag)
+            {
+                tags[tag] = name;
+                registeredFoundMessage.append(name.GetCStr() + AZStd::string(", "));
+            };
+
+            m_flagRegistry->VisitTags(gatherTags);
+
+            registeredFoundMessage.erase(registeredFoundMessage.end() - 2);
+
+            AZ_Printf("MeshFeatureProcessor", registeredFoundMessage.c_str());
+
+            AZStd::map<uint32_t, uint32_t> flagStats;
+
+            for (auto& model : m_modelData)
+            {
+                ++flagStats[model.m_cullable.m_flags.load()];
+            }
+
+            for (auto [flag, references] : flagStats)
+            {
+                AZStd::string flagList;
+
+                if (flag == 0)
+                {
+                    flagList = "(None)";
+                }
+                else
+                {
+                    for (auto [tag, name] : tags)
+                    {
+                        if ((tag.GetIndex() & flag) > 0)
+                        {
+                            flagList.append(name.GetCStr());
+                            flagList.append(", ");
+                        }
+                    }
+                    flagList.erase(flagList.end() - 2);
+                }
+
+                AZ_Printf("MeshFeatureProcessor", "Found %u references to [%s]", references, flagList.c_str());
+            }
+        }
+
+        MeshFeatureProcessorInterface::ModelChangedEvent& ModelDataInstance::MeshLoader::GetModelChangedEvent()
+        {
+            return m_modelChangedEvent;
+        }
+
         // ModelDataInstance::MeshLoader...
+
         ModelDataInstance::MeshLoader::MeshLoader(const Data::Asset<RPI::ModelAsset>& modelAsset, ModelDataInstance* parent)
             : m_modelAsset(modelAsset)
             , m_parent(parent)
@@ -562,10 +709,7 @@ namespace AZ
             Data::AssetBus::Handler::BusDisconnect();
         }
 
-        MeshFeatureProcessorInterface::ModelChangedEvent& ModelDataInstance::MeshLoader::GetModelChangedEvent()
-        {
-            return m_modelChangedEvent;
-        }
+        // ModelDataInstance...
 
         //! AssetBus::Handler overrides...
         void ModelDataInstance::MeshLoader::OnAssetReady(Data::Asset<Data::AssetData> asset)
@@ -661,8 +805,6 @@ namespace AZ
             }
         }
 
-        // ModelDataInstance...
-
         void ModelDataInstance::DeInit()
         {
             m_scene->GetCullingScene()->UnregisterCullable(m_cullable);
@@ -1371,8 +1513,6 @@ namespace AZ
                 cullData.m_hideFlags |= RPI::View::UsageReflectiveCubeMap;
             }
 
-            cullData.m_scene = m_scene;     //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this
-
 #ifdef AZ_CULL_DEBUG_ENABLED
             m_cullable.SetDebugName(AZ::Name(AZStd::string::format("%s - objectId: %u", m_model->GetModelAsset()->GetName().GetCStr(), m_objectId.GetIndex())));
 #endif

+ 0 - 1
Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp

@@ -105,7 +105,6 @@ namespace AZ
             AZ_Error("ReflectionProbeFeatureProcessor", m_renderInnerSrg.get(), "Failed to create render inner reflection shader resource group");
 
             // setup culling
-            m_cullable.m_cullData.m_scene = m_scene;
             m_cullable.SetDebugName(AZ::Name("ReflectionProbe Volume"));
         }
 

+ 8 - 7
Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake

@@ -14,6 +14,7 @@ set(FILES
     Include/Atom/Feature/AuxGeom/AuxGeomFeatureProcessor.h
     Include/Atom/Feature/ColorGrading/LutResolution.h
     Include/Atom/Feature/CoreLights/CoreLightsConstants.h
+    Include/Atom/Feature/CoreLights/LightCommon.h
     Include/Atom/Feature/CubeMapCapture/CubeMapCaptureFeatureProcessorInterface.h
     Include/Atom/Feature/DisplayMapper/AcesOutputTransformPass.h
     Include/Atom/Feature/DisplayMapper/AcesOutputTransformLutPass.h
@@ -82,6 +83,13 @@ set(FILES
     Source/CoreLights/DiskLightFeatureProcessor.cpp
     Source/CoreLights/EsmShadowmapsPass.h
     Source/CoreLights/EsmShadowmapsPass.cpp
+    Source/CoreLights/LightCullingPass.cpp
+    Source/CoreLights/LightCullingPass.h
+    Source/CoreLights/LightCullingTilePreparePass.cpp
+    Source/CoreLights/LightCullingTilePreparePass.h
+    Source/CoreLights/LightCullingRemap.cpp
+    Source/CoreLights/LightCullingRemap.h
+    Source/CoreLights/LightCullingConstants.h
     Source/CoreLights/LtcCommon.h
     Source/CoreLights/LtcCommon.cpp
     Source/CoreLights/PointLightFeatureProcessor.h
@@ -102,13 +110,6 @@ set(FILES
     Source/CoreLights/ShadowmapAtlas.cpp
     Source/CoreLights/ShadowmapPass.h
     Source/CoreLights/ShadowmapPass.cpp
-    Source/CoreLights/LightCullingPass.cpp
-    Source/CoreLights/LightCullingPass.h
-    Source/CoreLights/LightCullingTilePreparePass.cpp
-    Source/CoreLights/LightCullingTilePreparePass.h
-    Source/CoreLights/LightCullingRemap.cpp
-    Source/CoreLights/LightCullingRemap.h
-    Source/CoreLights/LightCullingConstants.h
     Source/Checkerboard/CheckerboardColorResolvePass.cpp
     Source/Checkerboard/CheckerboardColorResolvePass.h
     Source/Checkerboard/CheckerboardPass.cpp

+ 1 - 0
Gems/Atom/Feature/Common/Code/atom_feature_common_public_files.cmake

@@ -26,6 +26,7 @@ set(FILES
     Include/Atom/Feature/Decals/DecalFeatureProcessorInterface.h
     Include/Atom/Feature/DisplayMapper/DisplayMapperFeatureProcessorInterface.h
     Include/Atom/Feature/ImageBasedLights/ImageBasedLightFeatureProcessorInterface.h
+    Include/Atom/Feature/Mesh/MeshCommon.h
     Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h
     Include/Atom/Feature/MorphTargets/MorphTargetInputBuffers.h
     Include/Atom/Feature/ParamMacros/EndParams.inl

+ 15 - 1
Gems/Atom/RHI/Code/Include/Atom/RHI/TagBitRegistry.h

@@ -56,11 +56,14 @@ namespace AZ
             //! Returns the number of allocated tags in the registry.
             size_t GetAllocatedTagCount() const { return m_tagRegistry.GetAllocatedTagCount(); };
 
+            template <class TagVisitor>
+            void VisitTags(TagVisitor visitor);
+
         private:
             TagBitRegistry() = default;
             static TagType ConvertToUnderlyingType(TagType tag);
             static TagType ConvertFromUnderlyingType(TagType tag);
-            TagRegistry<typename IndexType, sizeof(IndexType) * 8> m_tagRegistry;
+            TagRegistry<IndexType, sizeof(IndexType) * 8> m_tagRegistry;
         };
 
         template<typename IndexType>
@@ -104,5 +107,16 @@ namespace AZ
         {
             return tag.IsValid() ? TagType(1 << tag.GetIndex()) : tag;
         }
+
+        template<typename IndexType>
+        template<class TagVisitor>
+        void TagBitRegistry<IndexType>::VisitTags(TagVisitor visitor)
+        {
+            auto tagConverter = [&](const Name& name, TagType tag)
+            {
+                visitor(name, ConvertFromUnderlyingType(tag));
+            };
+            m_tagRegistry.VisitTags(tagConverter);
+        }
     }
 }

+ 19 - 1
Gems/Atom/RHI/Code/Include/Atom/RHI/TagRegistry.h

@@ -67,6 +67,9 @@ namespace AZ
             //! Returns the number of allocated tags in the registry.
             size_t GetAllocatedTagCount() const;
 
+            template <class TagVisitor>
+            void VisitTags(TagVisitor visitor);
+
         private:
             TagRegistry() = default;
 
@@ -75,7 +78,6 @@ namespace AZ
                 Name m_name;
                 size_t m_refCount = 0;
             };
-
             mutable AZStd::shared_mutex m_mutex;
             AZStd::array<Entry, MaxTagCount> m_entriesByTag;
             size_t m_allocatedTagCount = 0;
@@ -187,5 +189,21 @@ namespace AZ
         {
             return m_allocatedTagCount;
         }
+
+        template<typename IndexType, size_t MaxTagCount>
+        template<class TagVisitor>
+        void TagRegistry<IndexType, MaxTagCount>::VisitTags(TagVisitor visitor)
+        {
+            size_t entriesToFind = m_allocatedTagCount;
+            for (uint32_t i = 0; entriesToFind > 0 && i < MaxTagCount; ++i)
+            {
+                const Entry& entry = m_entriesByTag.at(i);
+                if (entry.m_refCount > 0)
+                {
+                    visitor(entry.m_name, TagType(i));
+                    --entriesToFind;
+                }
+            }
+        }
     }
 }

+ 55 - 0
Gems/Atom/RHI/Code/Tests/TagRegistryTests.cpp

@@ -132,6 +132,33 @@ namespace UnitTest
         EXPECT_EQ(tagRegistry->FindTag(Name("B")), TagType::Null);
     }
 
+    TEST_F(TagRegistryTest, VisitTagRegistry)
+    {
+        auto tagRegistry = RHI::TagRegistry<IndexType, Count>::Create();
+
+        AZStd::array<Name, Count> names = { Name("A"), Name("B"), Name("C"), Name("D"), };
+        AZStd::array<TagType, Count> tags;
+        for (uint8_t i = 0; i < Count; ++i)
+        {
+            tags[i] = tagRegistry->AcquireTag(names[i]);
+        }
+
+        uint32_t visitCount = 0;
+        auto visitor = [&](const AZ::Name& name, TagType tag)
+        {
+            ++visitCount;
+            EXPECT_EQ(tagRegistry->GetName(tag), name);
+        };
+
+        tagRegistry->VisitTags(visitor);
+        EXPECT_EQ(visitCount, Count);
+
+        tagRegistry->ReleaseTag(tags[1]);
+        visitCount = 0;
+        tagRegistry->VisitTags(visitor);
+        EXPECT_EQ(visitCount, Count - 1);
+    }
+
     TEST_F(TagRegistryTest, ConstructResetTagBitRegistry)
     {
         auto tagBitRegistry = RHI::TagBitRegistry<IndexType>::Create();
@@ -177,4 +204,32 @@ namespace UnitTest
             tagBitRegistry->ReleaseTag(tags[i]);
         }
     }
+
+    TEST_F(TagRegistryTest, VisitTagBitRegistry)
+    {
+        auto tagBitRegistry = RHI::TagBitRegistry<IndexType>::Create();
+
+        AZStd::array<Name, Count> names = { Name("A"), Name("B"), Name("C"), Name("D"), };
+        AZStd::array<TagType, Count> tags;
+        for (uint8_t i = 0; i < Count; ++i)
+        {
+            tags[i] = tagBitRegistry->AcquireTag(names[i]);
+        }
+
+        uint32_t visitCount = 0;
+        auto visitor = [&](const AZ::Name& name, TagType tag)
+        {
+            ++visitCount;
+            EXPECT_EQ(tagBitRegistry->GetName(tag), name);
+        };
+
+        tagBitRegistry->VisitTags(visitor);
+        EXPECT_EQ(visitCount, Count);
+
+        tagBitRegistry->ReleaseTag(tags[1]);
+        visitCount = 0;
+        tagBitRegistry->VisitTags(visitor);
+        EXPECT_EQ(visitCount, Count - 1);
+    }
+
 }

+ 12 - 10
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Culling.h

@@ -58,10 +58,10 @@ namespace AZ
 
                 //! World-space bounding sphere
                 AZ::Sphere m_boundingSphere;
-                //! World-space bouding oriented-bounding-box
+                //! World-space bounding oriented-bounding-box
                 AZ::Obb m_boundingObb;
 
-                //! Will only pass visibity if at least one of the drawListMask bits matches the view's drawListMask.
+                //! Will only pass visibility if at least one of the drawListMask bits matches the view's drawListMask.
                 //! Set to all 1's if the object type doesn't have a drawListMask
                 RHI::DrawListMask m_drawListMask;
                 //! Will hide this object if any of the hideFlags match the View's usage flags. Useful to hide objects from certain Views.
@@ -71,8 +71,6 @@ namespace AZ
                 //! UUID and type of the component that owns this cullable (optional)
                 AZ::Uuid m_componentUuid = AZ::Uuid::CreateNull();
                 uint32_t m_componentType = 0;
-
-                class RPI::Scene* m_scene = nullptr;  //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this
             };
             CullData m_cullData;
 
@@ -88,7 +86,7 @@ namespace AZ
             {
                 LodType m_lodType = LodType::Default;
                 LodOverride m_lodOverride = 0;
-                // the minimum possibe area a sphere enclosing a mesh projected onto the screen should have before it is culled.
+                // the minimum possible area a sphere enclosing a mesh projected onto the screen should have before it is culled.
                 float m_minimumScreenCoverage = 1.0f / 1080.0f; // For default, mesh should cover at least a screen pixel at 1080p to be drawn;
                 // The screen area decay between 0 and 1, i.e. closer to 1 -> lose quality immediately, closer to 0 -> never lose quality 
                 float m_qualityDecayRate = 0.5f;
@@ -113,6 +111,10 @@ namespace AZ
             };
             LodData m_lodData;
 
+            using FlagType = uint32_t;
+            FlagType m_prevFlags = 0;
+            AZStd::atomic<FlagType> m_flags = 0;
+
             //! Flag indicating if the object is visible in any view, meaning it passed the culling tests in the previous frame.
             //! This flag must be manually cleared by the Cullable object every frame.
             bool m_isVisible = false;
@@ -196,7 +198,7 @@ namespace AZ
 
             //! Finds or creates a new CullStats struct for a given view.
             //! Once accessed, use the CullStats to accumulate metrics for a frame.
-            //! Is designed to be threadsafe
+            //! Is designed to be thread-safe
             CullStats& GetCullStatsForView(View* view);
 
             void ResetCullStats();
@@ -218,7 +220,7 @@ namespace AZ
             AZStd::mutex m_perViewCullStatsMutex;
         };
 
-        //! Selects an lod (based on size-in-screnspace) and adds the appropriate DrawPackets to the view.
+        //! Selects an lod (based on size-in-screen-space) and adds the appropriate DrawPackets to the view.
         uint32_t AddLodDataToView(const Vector3& pos, const Cullable::LodData& lodData, RPI::View& view);
 
         //! Centralized manager for culling-related processing for a given scene.
@@ -239,7 +241,7 @@ namespace AZ
 
             struct OcclusionPlane
             {
-                // World space corners of the occluson plane
+                // World space corners of the occlusion plane
                 Vector3 m_cornerBL;
                 Vector3 m_cornerTL;
                 Vector3 m_cornerTR;
@@ -275,12 +277,12 @@ namespace AZ
 
             //! Adds a Cullable to the underlying visibility system(s).
             //! Must be called at least once on initialization and whenever a Cullable's position or bounds is changed.
-            //! Is not threadsafe, so call this from the main thread outside of Begin/EndCulling()
+            //! Is not thread-safe, so call this from the main thread outside of Begin/EndCulling()
             void RegisterOrUpdateCullable(Cullable& cullable);
 
             //! Removes a Cullable from the underlying visibility system(s).
             //! Must be called once for each cullable object on de-initialization.
-            //! Is not threadsafe, so call this from the main thread outside of Begin/EndCulling()
+            //! Is not thread-safe, so call this from the main thread outside of Begin/EndCulling()
             void UnregisterCullable(Cullable& cullable);
 
             //! Returns the number of cullables that have been added to the CullingScene

+ 4 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/MeshDrawPacket.h

@@ -57,6 +57,8 @@ namespace AZ
             void SetStencilRef(uint8_t stencilRef) { m_stencilRef = stencilRef; }
             void SetSortKey(RHI::DrawItemSortKey sortKey) { m_sortKey = sortKey; };
             bool SetShaderOption(const Name& shaderOptionName, RPI::ShaderOptionValue value);
+            bool UnsetShaderOption(const Name& shaderOptionName);
+            void ClearShaderOptions();
 
             Data::Instance<Material> GetMaterial() const;
             const ModelLod::Mesh& GetMesh() const;
@@ -64,6 +66,8 @@ namespace AZ
 
         private:
             bool DoUpdate(const Scene& parentScene);
+            void ForValidShaderOptionName(const Name& shaderOptionName, const AZStd::function<bool(const ShaderCollection::Item&, ShaderOptionIndex)>& callback);
+            bool MaterialOwnsShaderOption(const Name& shaderOptionName);
 
             ConstPtr<RHI::DrawPacket> m_drawPacket;
 

+ 9 - 4
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h

@@ -35,6 +35,11 @@
 #include <AzFramework/Scene/Scene.h>
 #include <AzFramework/Scene/SceneSystemInterface.h>
 
+namespace AzFramework
+{
+    class IVisibilityScene;
+}
+
 namespace AZ
 {
     namespace RPI
@@ -159,10 +164,9 @@ namespace AZ
 
             bool HasOutputForPipelineState(RHI::DrawListTag drawListTag) const;
 
-            AZ::RPI::CullingScene* GetCullingScene()
-            {
-                return m_cullingScene;
-            }
+            AzFramework::IVisibilityScene* GetVisibilityScene() const { return m_visibilityScene; }
+
+            AZ::RPI::CullingScene* GetCullingScene() const { return m_cullingScene; }
 
             RenderPipelinePtr FindRenderPipelineForWindow(AzFramework::NativeWindowHandle windowHandle, ViewType viewType = ViewType::Default);
 
@@ -246,6 +250,7 @@ namespace AZ
             // CPU simulation job completion for track all feature processors' simulation jobs
             AZ::JobCompletion* m_simulationCompletion = nullptr;
 
+            AzFramework::IVisibilityScene* m_visibilityScene;
             AZ::RPI::CullingScene* m_cullingScene;
 
             // Cached views for current rendering frame. It gets re-built every frame.

+ 4 - 16
Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp

@@ -240,7 +240,6 @@ namespace AZ
 
                     if ((c->m_cullData.m_drawListMask & worklistData->m_view->GetDrawListMask()).none() ||
                         c->m_cullData.m_hideFlags & worklistData->m_view->GetUsageFlags() ||
-                        c->m_cullData.m_scene != worklistData->m_scene ||       //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this
                         c->m_isHidden)
                     {
                         continue;
@@ -793,10 +792,7 @@ namespace AZ
         void CullingScene::Activate(const Scene* parentScene)
         {
             m_parentScene = parentScene;
-
-            AZ_Assert(m_visScene == nullptr, "IVisibilityScene already created for this RPI::Scene");
-            AZ::Name visSceneName(AZStd::string::format("RenderCullScene[%s]", m_parentScene->GetName().GetCStr()));
-            m_visScene = AZ::Interface<AzFramework::IVisibilitySystem>::Get()->CreateVisibilityScene(visSceneName);
+            m_visScene = parentScene->GetVisibilityScene();
 
             m_taskGraphActive = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
 
@@ -810,11 +806,7 @@ namespace AZ
 #ifdef AZ_CULL_DEBUG_ENABLED
             AZ_Assert(CountObjectsInScene() == 0, "All culling entries must be removed from the scene before shutdown.");
 #endif
-            if (m_visScene)
-            {
-                AZ::Interface<AzFramework::IVisibilitySystem>::Get()->DestroyVisibilityScene(m_visScene);
-                m_visScene = nullptr;
-            }
+            m_visScene = nullptr;
         }
 
         void CullingScene::BeginCullingTaskGraph(const AZStd::vector<ViewPtr>& views)
@@ -928,17 +920,13 @@ namespace AZ
         {
             size_t numObjects = 0;
             m_visScene->EnumerateNoCull(
-                [this, &numObjects](const AzFramework::IVisibilityScene::NodeData& nodeData)
+                [&numObjects](const AzFramework::IVisibilityScene::NodeData& nodeData)
                 {
                     for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries)
                     {
                         if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable)
                         {
-                            Cullable* c = static_cast<Cullable*>(visibleEntry->m_userData);
-                            if (c->m_cullData.m_scene == m_parentScene)       //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this
-                            {
-                                ++numObjects;
-                            }
+                            ++numObjects;
                         }
                     }
                 }

+ 62 - 23
Gems/Atom/RPI/Code/Source/RPI.Public/MeshDrawPacket.cpp

@@ -59,49 +59,88 @@ namespace AZ
             return m_modelLod->GetMeshes()[m_modelLodMeshIndex];
         }
 
-        bool MeshDrawPacket::SetShaderOption(const Name& shaderOptionName, RPI::ShaderOptionValue value)
+        void MeshDrawPacket::ForValidShaderOptionName(const Name& shaderOptionName, const AZStd::function<bool(const ShaderCollection::Item&, ShaderOptionIndex)>& callback)
         {
-            // check if the material owns this option in any of its shaders, if so it can't be set externally
             for (auto& shaderItem : m_material->GetShaderCollection())
             {
                 const ShaderOptionGroupLayout* layout = shaderItem.GetShaderOptions()->GetShaderOptionLayout();
                 ShaderOptionIndex index = layout->FindShaderOptionIndex(shaderOptionName);
                 if (index.IsValid())
                 {
-                    if (shaderItem.MaterialOwnsShaderOption(index))
+                    bool shouldContinue = callback(shaderItem, index);
+                    if (!shouldContinue)
                     {
-                        return false;
+                        return;
                     }
                 }
             }
+        }
 
-            for (auto& shaderItem : m_material->GetShaderCollection())
-            {
-                const ShaderOptionGroupLayout* layout = shaderItem.GetShaderOptions()->GetShaderOptionLayout();
-                ShaderOptionIndex index = layout->FindShaderOptionIndex(shaderOptionName);
-                if (index.IsValid())
+        bool MeshDrawPacket::MaterialOwnsShaderOption(const Name& shaderOptionName)
+        {
+            // check if the material owns this option in any of its shaders, if so it can't be set externally
+            bool materialOwnsShaderOption = false;
+            ForValidShaderOptionName(shaderOptionName,
+                [&](const ShaderCollection::Item& shaderItem, ShaderOptionIndex index)
                 {
-                    // try to find an existing option entry in the list
-                    auto itEntry = AZStd::find_if(m_shaderOptions.begin(), m_shaderOptions.end(), [&shaderOptionName](const ShaderOptionPair& entry)
-                    {
-                        return entry.first == shaderOptionName;
-                    });
+                    materialOwnsShaderOption = shaderItem.MaterialOwnsShaderOption(index);
+                    return !materialOwnsShaderOption; // will stop execution if set to true.
+                }
+            );
+            return materialOwnsShaderOption;
+        }
 
-                    // store the option name and value, they will be used in DoUpdate() to select the appropriate shader variant
-                    if (itEntry == m_shaderOptions.end())
-                    {
-                        m_shaderOptions.push_back({ shaderOptionName, value });
-                    }
-                    else
-                    {
-                        itEntry->second = value;
-                    }
+        bool MeshDrawPacket::SetShaderOption(const Name& shaderOptionName, ShaderOptionValue value)
+        {
+            // check if the material owns this option in any of its shaders, if so it can't be set externally
+            if (MaterialOwnsShaderOption(shaderOptionName))
+            {
+                return false;
+            }
+
+            // Try to find an existing option entry in the list
+            for (ShaderOptionPair& shaderOptionPair : m_shaderOptions)
+            {
+                if (shaderOptionPair.first == shaderOptionName)
+                {
+                    shaderOptionPair.second = value;
+                    return true;
                 }
             }
 
+            // Shader option isn't on the list, look to see if it's even valid for at least one shader item, and if so, add it.
+            ForValidShaderOptionName(shaderOptionName,
+                [&]([[maybe_unused]] const ShaderCollection::Item& shaderItem, [[maybe_unused]] ShaderOptionIndex index)
+                {
+                    // Store the option name and value, they will be used in DoUpdate() to select the appropriate shader variant
+                    m_shaderOptions.push_back({ shaderOptionName, value });
+                    return false; // stop checking other shader items.
+                }
+            );
+
             return true;
         }
 
+        bool MeshDrawPacket::UnsetShaderOption(const Name& shaderOptionName)
+        {
+            // try to find an existing option entry in the list, then remove it by swapping it with the back.
+            for (ShaderOptionPair& shaderOptionPair : m_shaderOptions)
+            {
+                if (shaderOptionPair.first == shaderOptionName)
+                {
+                    shaderOptionPair = m_shaderOptions.back();
+                    m_shaderOptions.pop_back();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        void MeshDrawPacket::ClearShaderOptions()
+        {
+            m_shaderOptions.clear();
+        }
+
         bool MeshDrawPacket::Update(const Scene& parentScene, bool forceUpdate /*= false*/)
         {
             // Why we need to check "!m_material->NeedsCompile()"...

+ 7 - 2
Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp

@@ -25,6 +25,7 @@
 #include <AzCore/Task/TaskGraph.h>
 
 #include <AzFramework/Entity/EntityContext.h>
+#include <AzFramework/Visibility/IVisibilitySystem.h>
 
 
 namespace AZ
@@ -34,6 +35,11 @@ namespace AZ
         ScenePtr Scene::CreateScene(const SceneDescriptor& sceneDescriptor)
         {
             Scene* scene = aznew Scene();
+            scene->m_name = sceneDescriptor.m_nameId;
+
+            AZ::Name visSceneName(AZStd::string::format("RenderCullScene[%s]", scene->m_name.GetCStr()));
+            scene->m_visibilityScene = AZ::Interface<AzFramework::IVisibilitySystem>::Get()->CreateVisibilityScene(visSceneName);
+
             for (const auto& fpId : sceneDescriptor.m_featureProcessorNames)
             {
                 scene->EnableFeatureProcessor(FeatureProcessorId{ fpId });
@@ -46,8 +52,6 @@ namespace AZ
                 scene->m_srg = ShaderResourceGroup::Create(shaderAsset, sceneSrgLayout->GetName());
             }
 
-            scene->m_name = sceneDescriptor.m_nameId;
-
             return ScenePtr(scene);
         }
 
@@ -129,6 +133,7 @@ namespace AZ
 
             Deactivate();
 
+            AZ::Interface<AzFramework::IVisibilitySystem>::Get()->DestroyVisibilityScene(m_visibilityScene);
             delete m_cullingScene;
         }
 

+ 2 - 2
Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/PreviewRenderer/PreviewerFeatureProcessorProviderBus.h

@@ -8,7 +8,7 @@
 #pragma once
 
 #include <AzCore/EBus/EBus.h>
-#include <AzCore/std/containers/unordered_set.h>
+#include <AzCore/std/containers/vector.h>
 #include <AzCore/std/string/string.h>
 
 namespace AtomToolsFramework
@@ -18,7 +18,7 @@ namespace AtomToolsFramework
     {
     public:
         //! Get a list of custom feature processors to register with preview image renderer
-        virtual void GetRequiredFeatureProcessors(AZStd::unordered_set<AZStd::string>& featureProcessors) const = 0;
+        virtual void GetRequiredFeatureProcessors(AZStd::vector<AZStd::string>& featureProcessors) const = 0;
     };
 
     using PreviewerFeatureProcessorProviderBus = AZ::EBus<PreviewerFeatureProcessorProviderRequests>;

+ 3 - 3
Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp

@@ -38,7 +38,7 @@ namespace AtomToolsFramework
         m_entityContext->InitContext();
 
         // Create and register a scene with all required feature processors
-        AZStd::unordered_set<AZStd::string> featureProcessors;
+        AZStd::vector<AZStd::string> featureProcessors;
         PreviewerFeatureProcessorProviderBus::Broadcast(
             &PreviewerFeatureProcessorProviderBus::Handler::GetRequiredFeatureProcessors, featureProcessors);
 
@@ -247,9 +247,9 @@ namespace AtomToolsFramework
         m_renderPipeline->RemoveFromRenderTick();
     }
 
-    void PreviewRenderer::GetRequiredFeatureProcessors(AZStd::unordered_set<AZStd::string>& featureProcessors) const
+    void PreviewRenderer::GetRequiredFeatureProcessors(AZStd::vector<AZStd::string>& featureProcessors) const
     {
-        featureProcessors.insert({
+        featureProcessors.insert(featureProcessors.end(),  {
             "AZ::Render::TransformServiceFeatureProcessor",
             "AZ::Render::MeshFeatureProcessor",
             "AZ::Render::SimplePointLightFeatureProcessor",

+ 1 - 1
Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.h

@@ -53,7 +53,7 @@ namespace AtomToolsFramework
 
     private:
         //! AZ::Render::PreviewerFeatureProcessorProviderBus::Handler interface overrides...
-        void GetRequiredFeatureProcessors(AZStd::unordered_set<AZStd::string>& featureProcessors) const override;
+        void GetRequiredFeatureProcessors(AZStd::vector<AZStd::string>& featureProcessors) const override;
 
         static constexpr float AspectRatio = 1.0f;
         static constexpr float NearDist = 0.001f;

+ 0 - 1
Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGrid.cpp

@@ -48,7 +48,6 @@ namespace AZ
             m_visualizationTlasInstancesAttachmentId = AZStd::string::format("ProbeVisualizationTlasInstancesAttachmentId_%s", uuidString.c_str());
 
             // setup culling
-            m_cullable.m_cullData.m_scene = m_scene;
             m_cullable.SetDebugName(AZ::Name("DiffuseProbeGrid Volume"));
 
             // create the visualization TLAS