// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Math module" // For conditions of distribution and use, see copyright notice in Config.hpp // Sources: // http://www.crownandcutlass.com/features/technicaldetails/frustum.html // http://www.lighthouse3d.com/tutorials/view-frustum-culling/ #include #include #include #include #include namespace Nz { /*! * \ingroup math * \class Nz::Frustum * \brief Math class that represents a frustum in the three dimensional vector space * * Frustums are used to determine what is inside the camera's field of view. They help speed up the rendering process */ /*! * \brief Constructs a Frustum by specifying its planes * * \param corners Corners * \param planes Frustum of type U to convert to type T */ template constexpr Frustum::Frustum(const EnumArray>& planes) : m_planes(planes) { } /*! * \brief Constructs a Frustum object from another type of Frustum * * \param frustum Frustum of type U to convert to type T */ template template constexpr Frustum::Frustum(const Frustum& frustum) { for (auto&& [planeEnum, plane] : m_planes.iter_kv()) plane = Frustum(frustum.GetPlane(planeEnum)); } template constexpr bool Frustum::ApproxEqual(const Frustum& frustum, T maxDifference) const { for (auto&& [planeEnum, plane] : m_planes.iter_kv()) { if (!plane.ApproxEqual(frustum.GetPlane(planeEnum))) return false; } return true; } /*! * \brief Computes the position of a frustum corner * \return The corner position * * \param corner Which corner to compute */ template constexpr Vector3 Frustum::ComputeCorner(BoxCorner corner) const { switch (corner) { case BoxCorner::FarLeftBottom: return Plane::Intersect(GetPlane(FrustumPlane::Far), GetPlane(FrustumPlane::Left), GetPlane(FrustumPlane::Bottom)); case BoxCorner::FarLeftTop: return Plane::Intersect(GetPlane(FrustumPlane::Far), GetPlane(FrustumPlane::Left), GetPlane(FrustumPlane::Top)); case BoxCorner::FarRightBottom: return Plane::Intersect(GetPlane(FrustumPlane::Far), GetPlane(FrustumPlane::Right), GetPlane(FrustumPlane::Bottom)); case BoxCorner::FarRightTop: return Plane::Intersect(GetPlane(FrustumPlane::Far), GetPlane(FrustumPlane::Right), GetPlane(FrustumPlane::Top)); case BoxCorner::NearLeftBottom: return Plane::Intersect(GetPlane(FrustumPlane::Near), GetPlane(FrustumPlane::Left), GetPlane(FrustumPlane::Bottom)); case BoxCorner::NearLeftTop: return Plane::Intersect(GetPlane(FrustumPlane::Near), GetPlane(FrustumPlane::Left), GetPlane(FrustumPlane::Top)); case BoxCorner::NearRightBottom: return Plane::Intersect(GetPlane(FrustumPlane::Near), GetPlane(FrustumPlane::Right), GetPlane(FrustumPlane::Bottom)); case BoxCorner::NearRightTop: return Plane::Intersect(GetPlane(FrustumPlane::Near), GetPlane(FrustumPlane::Right), GetPlane(FrustumPlane::Top)); } NazaraError("invalid frustum corner"); return Vector3(); } template constexpr EnumArray> Frustum::ComputeCorners() const { return { ComputeCorner(BoxCorner::FarLeftBottom), ComputeCorner(BoxCorner::FarLeftTop), ComputeCorner(BoxCorner::FarRightBottom), ComputeCorner(BoxCorner::FarRightTop), ComputeCorner(BoxCorner::NearLeftBottom), ComputeCorner(BoxCorner::NearLeftTop), ComputeCorner(BoxCorner::NearRightBottom), ComputeCorner(BoxCorner::NearRightTop) }; } /*! * \brief Checks whether or not a bounding volume is contained in the frustum * \return true if the bounding volume is entirely in the frustum * * \param volume Volume to check * * \remark If volume is infinite, true is returned * \remark If volume is null, false is returned * \remark If enumeration of the volume is not defined in Extent, a NazaraError is thrown and false is returned * \remark If enumeration of the intersection is not defined in IntersectionSide, a NazaraError is thrown and false is returned. This should not never happen for a user of the library */ template constexpr bool Frustum::Contains(const BoundingVolume& volume) const { switch (volume.extent) { case Extent::Finite: { IntersectionSide side = Intersect(volume.aabb); switch (side) { case IntersectionSide::Inside: return true; case IntersectionSide::Intersecting: return Contains(volume.obb); case IntersectionSide::Outside: return false; } NazaraErrorFmt("invalid intersection side ({0:#x})", UnderlyingCast(side)); return false; } case Extent::Infinite: return true; case Extent::Null: return false; } NazaraErrorFmt("invalid extent type ({0:#x})", UnderlyingCast(volume.extent)); return false; } /*! * \brief Checks whether or not a box is contained in the frustum * \return true if the box is entirely in the frustum * * \param box Box to check */ template constexpr bool Frustum::Contains(const Box& box) const { // https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html // https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling Vector3 center = box.GetCenter(); Vector3 extents = box.GetLengths() * T(0.5); for (const auto& plane : m_planes) { Vector3 projectedExtents = extents * plane.normal.GetAbs(); float radius = projectedExtents.x + projectedExtents.y + projectedExtents.z; float distance = plane.SignedDistance(center); if (distance < T(radius)) return false; } return true; } /*! * \brief Checks whether or not an oriented box is contained in the frustum * \return true if the oriented box is entirely in the frustum * * \param orientedbox Oriented box to check */ template constexpr bool Frustum::Contains(const OrientedBox& orientedbox) const { return Contains(orientedbox.GetCorners().data(), 8); } /*! * \brief Checks whether or not a sphere is contained in the frustum * \return true if the sphere is entirely in the frustum * * \param sphere Sphere to check */ template constexpr bool Frustum::Contains(const Sphere& sphere) const { for (const auto& plane : m_planes) { if (plane.SignedDistance(sphere.GetPosition()) < -sphere.radius) return false; } return true; } /*! * \brief Checks whether or not a Vector3 is contained in the frustum * \return true if the Vector3 is in the frustum * * \param point Vector3 which represents a point in the space */ template constexpr bool Frustum::Contains(const Vector3& point) const { for (const auto& plane : m_planes) { if (plane.SignedDistance(point) < T(0.0)) return false; } return true; } /*! * \brief Checks whether or not a set of Vector3 is contained in the frustum * \return true if the set of Vector3 is in the frustum * * \param points Pointer to Vector3 which represents a set of points in the space * \param pointCount Number of points to check */ template constexpr bool Frustum::Contains(const Vector3* points, std::size_t pointCount) const { for (const auto& plane : m_planes) { for (std::size_t i = 0; i < pointCount; ++i) { if (plane.SignedDistance(points[i]) < T(0.0)) return false; } } return true; } template constexpr Box Frustum::GetAABB() const { EnumArray> corners = ComputeCorners(); Vector3f max = corners.front(); Vector3f min = corners.front(); for (std::size_t i = 1; i < corners.size(); ++i) { max.Maximize(corners[static_cast(i)]); min.Minimize(corners[static_cast(i)]); } return Box::FromExtents(min, max); } /*! * \brief Gets the Plane for the face * \return The face of the frustum according to enum FrustumPlane * * \param plane Enumeration of type FrustumPlane * * \remark If enumeration is not defined in FrustumPlane and NAZARA_DEBUG defined, a NazaraError is thrown and a Plane uninitialised is returned */ template constexpr const Plane& Frustum::GetPlane(FrustumPlane plane) const { NazaraAssert(plane <= FrustumPlane::Max, "invalid plane"); return m_planes[plane]; } template constexpr const EnumArray>& Frustum::GetPlanes() const { return m_planes; } /*! * \brief Checks whether or not a bounding volume intersects with the frustum * \return IntersectionSide How the bounding volume is intersecting with the frustum * * \param volume Volume to check * * \remark If volume is infinite, IntersectionSide::Intersecting is returned * \remark If volume is null, IntersectionSide::Outside is returned * \remark If enumeration of the volume is not defined in Extent, a NazaraError is thrown and IntersectionSide::Outside is returned * \remark If enumeration of the intersection is not defined in IntersectionSide, a NazaraError is thrown and IntersectionSide::Outside is returned. This should not never happen for a user of the library */ template constexpr IntersectionSide Frustum::Intersect(const BoundingVolume& volume) const { switch (volume.extent) { case Extent::Finite: { IntersectionSide side = Intersect(volume.aabb); switch (side) { case IntersectionSide::Inside: return IntersectionSide::Inside; case IntersectionSide::Intersecting: return Intersect(volume.obb); case IntersectionSide::Outside: return IntersectionSide::Outside; } NazaraErrorFmt("invalid intersection side ({0:#x})", UnderlyingCast(side)); return IntersectionSide::Outside; } case Extent::Infinite: return IntersectionSide::Intersecting; // We can not contain infinity case Extent::Null: return IntersectionSide::Outside; } NazaraErrorFmt("invalid extent type ({0:#x})", UnderlyingCast(volume.extent)); return IntersectionSide::Outside; } /*! * \brief Checks whether or not a box intersects with the frustum * \return IntersectionSide How the box is intersecting with the frustum * * \param box Box to check */ template constexpr IntersectionSide Frustum::Intersect(const Box& box) const { // https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html // https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling IntersectionSide side = IntersectionSide::Inside; Vector3 center = box.GetCenter(); Vector3 extents = box.GetLengths() * T(0.5); for (const auto& plane : m_planes) { Vector3 projectedExtents = extents * plane.normal.GetAbs(); float radius = projectedExtents.x + projectedExtents.y + projectedExtents.z; float distance = plane.SignedDistance(center); if (distance < T(-radius)) return IntersectionSide::Outside; else if (distance < T(radius)) side = IntersectionSide::Intersecting; } return side; } /*! * \brief Checks whether or not an oriented box intersects with the frustum * \return IntersectionSide How the oriented box is intersecting with the frustum * * \param oriented box OrientedBox to check */ template constexpr IntersectionSide Frustum::Intersect(const OrientedBox& orientedbox) const { return Intersect(orientedbox.GetCorners().data(), 8); } /*! * \brief Checks whether or not a sphere intersects with the frustum * \return IntersectionSide How the sphere is intersecting with the frustum * * \param sphere Sphere to check */ template constexpr IntersectionSide Frustum::Intersect(const Sphere& sphere) const { // http://www.lighthouse3d.com/tutorials/view-frustum-culling/geometric-approach-testing-points-and-spheres/ IntersectionSide side = IntersectionSide::Inside; for (const auto& plane : m_planes) { T distance = plane.SignedDistance(sphere.GetPosition()); if (distance < -sphere.radius) return IntersectionSide::Outside; else if (distance < sphere.radius) side = IntersectionSide::Intersecting; } return side; } /*! * \brief Checks whether or not a set of Vector3 intersects with the frustum * \return IntersectionSide How the set of Vector3 is intersecting with the frustum * * \param points Pointer to Vector3 which represents a set of points in the space * \param pointCount Number of points to check */ template constexpr IntersectionSide Frustum::Intersect(const Vector3* points, std::size_t pointCount) const { IntersectionSide side = IntersectionSide::Inside; for (const auto& plane : m_planes) { bool outside = true; for (std::size_t i = 0; i < pointCount; ++i) { // If at least one point is outside of the frustum, we're intersecting if (plane.SignedDistance(points[i]) < T(0.0)) side = IntersectionSide::Intersecting; else outside = false; } // But if no point is intersecting on this plane, then it's outside if (outside) return IntersectionSide::Outside; } return side; } template constexpr Frustum Frustum::Reduce(T nearFactor, T farFactor) const { EnumArray> planes = m_planes; planes[FrustumPlane::Near].distance = Lerp(m_planes[FrustumPlane::Near].distance, -m_planes[FrustumPlane::Far].distance, nearFactor); planes[FrustumPlane::Far].distance = Lerp(-m_planes[FrustumPlane::Near].distance, m_planes[FrustumPlane::Far].distance, farFactor); return Frustum(planes); } template template constexpr void Frustum::Split(std::initializer_list splitFactors, F&& callback) const { return Split(splitFactors.begin(), splitFactors.size(), std::forward(callback)); } template template constexpr void Frustum::Split(const T* splitFactors, std::size_t factorCount, F&& callback) const { T previousFar = T(0.0); for (std::size_t i = 0; i < factorCount; ++i) { T farFactor = splitFactors[i]; callback(previousFar, farFactor); previousFar = farFactor; } callback(previousFar, T(1.0)); } /*! * \brief Gives a string representation * \return A string representation of the object: "Frustum(Plane ...)" */ template std::string Frustum::ToString() const { std::ostringstream ss; ss << *this; return ss.str(); } /*! * \brief Builds the frustum object * \return A reference to this frustum which is the build up camera's field of view * * \param angle FOV angle * \param ratio Rendering ratio (typically 16/9 or 4/3) * \param zNear Distance where 'vision' begins * \param zFar Distance where 'vision' ends * \param eye Position of the camera * \param target Position of the target of the camera * \param up Direction of up vector according to the orientation of camera */ template constexpr bool Frustum::operator==(const Frustum& frustum) const { for (auto&& [planeEnum, plane] : m_planes.iter_kv()) { if (!plane != frustum.GetPlane(planeEnum)) return false; } return true; } template constexpr bool Frustum::operator!=(const Frustum& frustum) const { return !operator==(frustum); } template constexpr bool Frustum::ApproxEqual(const Frustum& lhs, const Frustum& rhs, T maxDifference) { return lhs.ApproxEqual(rhs, maxDifference); } template Frustum Frustum::Build(RadianAngle angle, T ratio, T zNear, T zFar, const Vector3& eye, const Vector3& target, const Vector3& up) { angle /= T(2.0); T tangent = angle.GetTan(); T nearH = zNear * tangent; T nearW = nearH * ratio; T farH = zFar * tangent; T farW = farH * ratio; Vector3 f = Vector3::Normalize(target - eye); Vector3 u = Vector3::Normalize(up); Vector3 s = Vector3::Normalize(f.CrossProduct(u)); u = s.CrossProduct(f); Vector3 nc = eye + f * zNear; Vector3 fc = eye + f * zFar; // Computing the frustum EnumArray> corners; corners[BoxCorner::FarLeftBottom] = fc - u * farH - s * farW; corners[BoxCorner::FarLeftTop] = fc + u * farH - s * farW; corners[BoxCorner::FarRightTop] = fc + u * farH + s * farW; corners[BoxCorner::FarRightBottom] = fc - u * farH + s * farW; corners[BoxCorner::NearLeftBottom] = nc - u * nearH - s * nearW; corners[BoxCorner::NearLeftTop] = nc + u * nearH - s * nearW; corners[BoxCorner::NearRightTop] = nc + u * nearH + s * nearW; corners[BoxCorner::NearRightBottom] = nc - u * nearH + s * nearW; // Construction of frustum's planes EnumArray> planes; planes[FrustumPlane::Bottom] = Plane(corners[BoxCorner::NearLeftBottom], corners[BoxCorner::NearRightBottom], corners[BoxCorner::FarRightBottom]); planes[FrustumPlane::Far] = Plane(corners[BoxCorner::FarRightTop], corners[BoxCorner::FarLeftTop], corners[BoxCorner::FarLeftBottom]); planes[FrustumPlane::Left] = Plane(corners[BoxCorner::NearLeftTop], corners[BoxCorner::NearLeftBottom], corners[BoxCorner::FarLeftBottom]); planes[FrustumPlane::Near] = Plane(corners[BoxCorner::NearLeftTop], corners[BoxCorner::NearRightTop], corners[BoxCorner::NearRightBottom]); planes[FrustumPlane::Right] = Plane(corners[BoxCorner::NearRightBottom], corners[BoxCorner::NearRightTop], corners[BoxCorner::FarRightBottom]); planes[FrustumPlane::Top] = Plane(corners[BoxCorner::NearRightTop], corners[BoxCorner::NearLeftTop], corners[BoxCorner::FarLeftTop]); return Frustum(planes); } /*! * \brief Constructs the frustum from a Matrix4 * \return A reference to this frustum which is the build up of projective matrix * * \param viewProjMatrix Matrix which represents the transformation of the frustum */ template Frustum Frustum::Extract(const Matrix4& viewProjMatrix) { EnumArray> planes; planes[FrustumPlane::Left].normal.x = viewProjMatrix(3, 0) + viewProjMatrix(0, 0); planes[FrustumPlane::Left].normal.y = viewProjMatrix(3, 1) + viewProjMatrix(0, 1); planes[FrustumPlane::Left].normal.z = viewProjMatrix(3, 2) + viewProjMatrix(0, 2); planes[FrustumPlane::Left].distance = viewProjMatrix(3, 3) + viewProjMatrix(0, 3); planes[FrustumPlane::Right].normal.x = viewProjMatrix(3, 0) - viewProjMatrix(0, 0); planes[FrustumPlane::Right].normal.y = viewProjMatrix(3, 1) - viewProjMatrix(0, 1); planes[FrustumPlane::Right].normal.z = viewProjMatrix(3, 2) - viewProjMatrix(0, 2); planes[FrustumPlane::Right].distance = viewProjMatrix(3, 3) - viewProjMatrix(0, 3); planes[FrustumPlane::Bottom].normal.x = viewProjMatrix(3, 0) - viewProjMatrix(1, 0); planes[FrustumPlane::Bottom].normal.y = viewProjMatrix(3, 1) - viewProjMatrix(1, 1); planes[FrustumPlane::Bottom].normal.z = viewProjMatrix(3, 2) - viewProjMatrix(1, 2); planes[FrustumPlane::Bottom].distance = viewProjMatrix(3, 3) - viewProjMatrix(1, 3); planes[FrustumPlane::Top].normal.x = viewProjMatrix(3, 0) + viewProjMatrix(1, 0); planes[FrustumPlane::Top].normal.y = viewProjMatrix(3, 1) + viewProjMatrix(1, 1); planes[FrustumPlane::Top].normal.z = viewProjMatrix(3, 2) + viewProjMatrix(1, 2); planes[FrustumPlane::Top].distance = viewProjMatrix(3, 3) + viewProjMatrix(1, 3); planes[FrustumPlane::Near].normal.x = viewProjMatrix(2, 0); planes[FrustumPlane::Near].normal.y = viewProjMatrix(2, 1); planes[FrustumPlane::Near].normal.z = viewProjMatrix(2, 2); planes[FrustumPlane::Near].distance = viewProjMatrix(2, 3); planes[FrustumPlane::Far].normal.x = viewProjMatrix(3, 0) - viewProjMatrix(2, 0); planes[FrustumPlane::Far].normal.y = viewProjMatrix(3, 1) - viewProjMatrix(2, 1); planes[FrustumPlane::Far].normal.z = viewProjMatrix(3, 2) - viewProjMatrix(2, 2); planes[FrustumPlane::Far].distance = viewProjMatrix(3, 3) - viewProjMatrix(2, 3); for (auto& plane : planes) plane.Normalize(); return Frustum(planes); } /*! * \brief Serializes a Frustum * \return true if successfully serialized * * \param context Serialization context * \param matrix Input frustum */ template bool Serialize(SerializationContext& context, const Frustum& frustum, TypeTag>) { for (const auto& plane : frustum.m_planes) { if (!Serialize(context, plane)) return false; } return true; } /*! * \brief Unserializes a Frustum * \return true if successfully unserialized * * \param context Serialization context * \param matrix Output frustum */ template bool Unserialize(SerializationContext& context, Frustum* frustum, TypeTag>) { for (auto& plane : frustum->m_planes) { if (!Unserialize(context, &plane)) return false; } return true; } /*! * \brief Output operator * \return The stream * * \param out The stream * \param frustum The frustum to output */ template std::ostream& operator<<(std::ostream& out, const Nz::Frustum& frustum) { return out << "Frustum(Bottom: " << frustum.GetPlane(Nz::FrustumPlane::Bottom) << ",\n" << " Far: " << frustum.GetPlane(Nz::FrustumPlane::Far) << ",\n" << " Left: " << frustum.GetPlane(Nz::FrustumPlane::Left) << ",\n" << " Near: " << frustum.GetPlane(Nz::FrustumPlane::Near) << ",\n" << " Right: " << frustum.GetPlane(Nz::FrustumPlane::Right) << ",\n" << " Top: " << frustum.GetPlane(Nz::FrustumPlane::Top) << ")\n"; } } #include