Browse Source

Added PlaneShape (#1222)

An infinite plane. Negative half space is considered solid.
Jorrit Rouwe 1 year ago
parent
commit
ff50a7a800

+ 1 - 0
Docs/Architecture.md

@@ -82,6 +82,7 @@ Each body has a shape attached that determines the collision volume. The followi
 * [TaperedCapsuleShape](@ref TaperedCapsuleShape) - A capsule with different radii at the bottom and top.
 * [TaperedCapsuleShape](@ref TaperedCapsuleShape) - A capsule with different radii at the bottom and top.
 * [CylinderShape](@ref CylinderShape) - A cylinder shape. Note that cylinders are the least stable of all shapes, so use another shape if possible.
 * [CylinderShape](@ref CylinderShape) - A cylinder shape. Note that cylinders are the least stable of all shapes, so use another shape if possible.
 * [ConvexHullShape](@ref ConvexHullShape) - A convex hull defined by a set of points.
 * [ConvexHullShape](@ref ConvexHullShape) - A convex hull defined by a set of points.
+* [PlaneShape](@ref PlaneShape) - An infinite plane. Negative half space is considered solid.
 * [StaticCompoundShape](@ref StaticCompoundShape) - A shape containing other shapes. This shape is constructed once and cannot be changed afterwards. Child shapes are organized in a tree to speed up collision detection.
 * [StaticCompoundShape](@ref StaticCompoundShape) - A shape containing other shapes. This shape is constructed once and cannot be changed afterwards. Child shapes are organized in a tree to speed up collision detection.
 * [MutableCompoundShape](@ref MutableCompoundShape) - A shape containing other shapes. This shape can be constructed/changed at runtime and trades construction time for runtime performance. Child shapes are organized in a list to make modification easy.
 * [MutableCompoundShape](@ref MutableCompoundShape) - A shape containing other shapes. This shape can be constructed/changed at runtime and trades construction time for runtime performance. Child shapes are organized in a list to make modification easy.
 * [MeshShape](@ref MeshShape) - A shape consisting of triangles. They are mostly used for static geometry.
 * [MeshShape](@ref MeshShape) - A shape consisting of triangles. They are mostly used for static geometry.

+ 4 - 0
Docs/ReleaseNotes.md

@@ -4,6 +4,10 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 
 
 ## Unreleased changes
 ## Unreleased changes
 
 
+### New functionality
+
+* Added PlaneShape. An infinite plane. Negative half space is considered solid.
+
 ### Bug fixes
 ### Bug fixes
 
 
 * Fixed an issue where the bounding volume of a HeightFieldShape was not properly adjusted when calling SetHeights leading to missed collisions.
 * Fixed an issue where the bounding volume of a HeightFieldShape was not properly adjusted when calling SetHeights leading to missed collisions.

+ 2 - 2
Jolt/Geometry/ClipPoly.h

@@ -128,10 +128,10 @@ void ClipPolyVsEdge(const VERTEX_ARRAY &inPolygonToClip, Vec3Arg inEdgeVertex1,
 		// In -> Out or Out -> In: Add point on clipping plane
 		// In -> Out or Out -> In: Add point on clipping plane
 		if (cur_inside != prev_inside)
 		if (cur_inside != prev_inside)
 		{
 		{
-			// Solve: (X - inPlaneOrigin) . inPlaneNormal = 0 and X = e1 + t * (e2 - e1) for X
+			// Solve: (inEdgeVertex1 - X) . edge_normal = 0 and X = e1 + t * (e2 - e1) for X
 			Vec3 e12 = e2 - e1;
 			Vec3 e12 = e2 - e1;
 			float denom = e12.Dot(edge_normal);
 			float denom = e12.Dot(edge_normal);
-			Vec3 clipped_point = e1 + (prev_num / denom) * e12;
+			Vec3 clipped_point = denom != 0.0f? e1 + (prev_num / denom) * e12 : e1;
 
 
 			// Project point on line segment v1, v2 so see if it falls outside if the edge
 			// Project point on line segment v1, v2 so see if it falls outside if the edge
 			float projection = (clipped_point - v1).Dot(v12);
 			float projection = (clipped_point - v1).Dot(v12);

+ 15 - 0
Jolt/Geometry/Plane.h

@@ -42,9 +42,20 @@ public:
 		return Plane(transformed_normal, GetConstant() - inTransform.GetTranslation().Dot(transformed_normal));
 		return Plane(transformed_normal, GetConstant() - inTransform.GetTranslation().Dot(transformed_normal));
 	}
 	}
 
 
+	/// Scale the plane, can handle non-uniform and negative scaling
+	inline Plane	Scaled(Vec3Arg inScale) const
+	{
+		Vec3 scaled_normal = GetNormal() / inScale;
+		float scaled_normal_length = scaled_normal.Length();
+		return Plane(scaled_normal / scaled_normal_length, GetConstant() / scaled_normal_length);
+	}
+
 	/// Distance point to plane
 	/// Distance point to plane
 	float			SignedDistance(Vec3Arg inPoint) const									{ return inPoint.Dot(GetNormal()) + GetConstant(); }
 	float			SignedDistance(Vec3Arg inPoint) const									{ return inPoint.Dot(GetNormal()) + GetConstant(); }
 
 
+	/// Project inPoint onto the plane
+	Vec3			ProjectPointOnPlane(Vec3Arg inPoint) const								{ return inPoint - GetNormal() * SignedDistance(inPoint); }
+
 	/// Returns intersection point between 3 planes
 	/// Returns intersection point between 3 planes
 	static bool		sIntersectPlanes(const Plane &inP1, const Plane &inP2, const Plane &inP3, Vec3 &outPoint)
 	static bool		sIntersectPlanes(const Plane &inP1, const Plane &inP2, const Plane &inP3, Vec3 &outPoint)
 	{
 	{
@@ -80,6 +91,10 @@ public:
 	}
 	}
 
 
 private:
 private:
+#ifdef JPH_OBJECT_STREAM
+	friend void		CreateRTTIPlane(class RTTI &);										// For JPH_IMPLEMENT_SERIALIZABLE_OUTSIDE_CLASS
+#endif
+
 	Vec4			mNormalAndConstant;													///< XYZ = normal, W = constant, plane: x . normal + constant = 0
 	Vec4			mNormalAndConstant;													///< XYZ = normal, W = constant, plane: x . normal + constant = 0
 };
 };
 
 

+ 2 - 0
Jolt/Jolt.cmake

@@ -258,6 +258,8 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/MutableCompoundShape.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/MutableCompoundShape.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/OffsetCenterOfMassShape.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/OffsetCenterOfMassShape.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/OffsetCenterOfMassShape.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/OffsetCenterOfMassShape.h
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/PlaneShape.cpp
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/PlaneShape.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/PolyhedronSubmergedVolumeCalculator.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/PolyhedronSubmergedVolumeCalculator.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/RotatedTranslatedShape.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/RotatedTranslatedShape.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/RotatedTranslatedShape.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/RotatedTranslatedShape.h

+ 5 - 0
Jolt/ObjectStream/TypeDeclarations.cpp

@@ -52,4 +52,9 @@ JPH_IMPLEMENT_SERIALIZABLE_OUTSIDE_CLASS(IndexedTriangle)
 	JPH_ADD_ATTRIBUTE(IndexedTriangle, mMaterialIndex)
 	JPH_ADD_ATTRIBUTE(IndexedTriangle, mMaterialIndex)
 }
 }
 
 
+JPH_IMPLEMENT_SERIALIZABLE_OUTSIDE_CLASS(Plane)
+{
+	JPH_ADD_ATTRIBUTE(Plane, mNormalAndConstant)
+}
+
 JPH_NAMESPACE_END
 JPH_NAMESPACE_END

+ 1 - 0
Jolt/ObjectStream/TypeDeclarations.h

@@ -33,6 +33,7 @@ JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, Color);
 JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, AABox);
 JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, AABox);
 JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, Triangle);
 JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, Triangle);
 JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, IndexedTriangle);
 JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, IndexedTriangle);
+JPH_DECLARE_SERIALIZABLE_OUTSIDE_CLASS(JPH_EXPORT, Plane);
 
 
 JPH_NAMESPACE_END
 JPH_NAMESPACE_END
 
 

+ 545 - 0
Jolt/Physics/Collision/Shape/PlaneShape.cpp

@@ -0,0 +1,545 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <Jolt/Jolt.h>
+
+#include <Jolt/Physics/Collision/Shape/PlaneShape.h>
+#include <Jolt/Physics/Collision/Shape/ConvexShape.h>
+#include <Jolt/Physics/Collision/Shape/ScaleHelpers.h>
+#include <Jolt/Physics/Collision/RayCast.h>
+#include <Jolt/Physics/Collision/ShapeCast.h>
+#include <Jolt/Physics/Collision/ShapeFilter.h>
+#include <Jolt/Physics/Collision/CastResult.h>
+#include <Jolt/Physics/Collision/CollisionDispatch.h>
+#include <Jolt/Physics/Collision/TransformedShape.h>
+#include <Jolt/Physics/Collision/CollidePointResult.h>
+#include <Jolt/Physics/SoftBody/SoftBodyVertex.h>
+#include <Jolt/Core/Profiler.h>
+#include <Jolt/Core/StreamIn.h>
+#include <Jolt/Core/StreamOut.h>
+#include <Jolt/Geometry/Plane.h>
+#include <Jolt/ObjectStream/TypeDeclarations.h>
+#ifdef JPH_DEBUG_RENDERER
+	#include <Jolt/Renderer/DebugRenderer.h>
+#endif // JPH_DEBUG_RENDERER
+
+JPH_NAMESPACE_BEGIN
+
+JPH_IMPLEMENT_SERIALIZABLE_VIRTUAL(PlaneShapeSettings)
+{
+	JPH_ADD_BASE_CLASS(PlaneShapeSettings, ShapeSettings)
+
+	JPH_ADD_ATTRIBUTE(PlaneShapeSettings, mPlane)
+	JPH_ADD_ATTRIBUTE(PlaneShapeSettings, mMaterial)
+	JPH_ADD_ATTRIBUTE(PlaneShapeSettings, mSize)
+}
+
+ShapeSettings::ShapeResult PlaneShapeSettings::Create() const
+{
+	if (mCachedResult.IsEmpty())
+		Ref<Shape> shape = new PlaneShape(*this, mCachedResult);
+	return mCachedResult;
+}
+
+inline static void sPlaneGetOrthogonalBasis(Vec3Arg inNormal, Vec3 &outPerp1, Vec3 &outPerp2)
+{
+	outPerp1 = inNormal.Cross(Vec3::sAxisY()).NormalizedOr(Vec3::sAxisX());
+	outPerp2 = outPerp1.Cross(inNormal).Normalized();
+	outPerp1 = inNormal.Cross(outPerp2);
+}
+
+void PlaneShape::GetVertices(Vec3 *outVertices) const
+{
+	// Create orthogonal basis
+	Vec3 normal = mPlane.GetNormal();
+	Vec3 perp1, perp2;
+	sPlaneGetOrthogonalBasis(normal, perp1, perp2);
+
+	// Scale basis
+	perp1 *= mSize;
+	perp2 *= mSize;
+
+	// Calculate corners
+	Vec3 point = -normal * mPlane.GetConstant();
+	outVertices[0] = point + perp1 + perp2;
+	outVertices[1] = point + perp1 - perp2;
+	outVertices[2] = point - perp1 - perp2;
+	outVertices[3] = point - perp1 + perp2;
+}
+
+void PlaneShape::CalculateLocalBounds()
+{
+	// Get the vertices of the plane
+	Vec3 vertices[4];
+	GetVertices(vertices);
+
+	// Encapsulate the vertices and a point mSize behind the plane
+	mLocalBounds = AABox();
+	Vec3 normal = mPlane.GetNormal();
+	for (const Vec3 &v : vertices)
+	{
+		mLocalBounds.Encapsulate(v);
+		mLocalBounds.Encapsulate(v - mSize * normal);
+	}
+}
+
+PlaneShape::PlaneShape(const PlaneShapeSettings &inSettings, ShapeResult &outResult) :
+	Shape(EShapeType::Plane, EShapeSubType::Plane, inSettings, outResult),
+	mPlane(inSettings.mPlane),
+	mMaterial(inSettings.mMaterial),
+	mSize(inSettings.mSize)
+{
+	if (!mPlane.GetNormal().IsNormalized())
+	{
+		outResult.SetError("Plane normal needs to be normalized!");
+		return;
+	}
+
+	CalculateLocalBounds();
+
+	outResult.Set(this);
+}
+
+MassProperties PlaneShape::GetMassProperties() const
+{
+	// Object should always be static, return default mass properties
+	return MassProperties();
+}
+
+void PlaneShape::GetSupportingFace(const SubShapeID &inSubShapeID, Vec3Arg inDirection, Vec3Arg inScale, Mat44Arg inCenterOfMassTransform, SupportingFace &outVertices) const
+{
+	// Get the vertices of the plane
+	Vec3 vertices[4];
+	GetVertices(vertices);
+
+	// Reverse if scale is inside out
+	if (ScaleHelpers::IsInsideOut(inScale))
+	{
+		swap(vertices[0], vertices[3]);
+		swap(vertices[1], vertices[2]);
+	}
+
+	// Transform them to world space
+	outVertices.clear();
+	Mat44 com = inCenterOfMassTransform.PreScaled(inScale);
+	for (const Vec3 &v : vertices)
+		outVertices.push_back(com * v);
+}
+
+#ifdef JPH_DEBUG_RENDERER
+void PlaneShape::Draw(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform, Vec3Arg inScale, ColorArg inColor, bool inUseMaterialColors, bool inDrawWireframe) const
+{
+	// Get the vertices of the plane
+	Vec3 local_vertices[4];
+	GetVertices(local_vertices);
+
+	// Reverse if scale is inside out
+	if (ScaleHelpers::IsInsideOut(inScale))
+	{
+		swap(local_vertices[0], local_vertices[3]);
+		swap(local_vertices[1], local_vertices[2]);
+	}
+
+	// Transform them to world space
+	RMat44 com = inCenterOfMassTransform.PreScaled(inScale);
+	RVec3 vertices[4];
+	for (uint i = 0; i < 4; ++i)
+		vertices[i] = com * local_vertices[i];
+
+	// Determine the color
+	Color color = inUseMaterialColors? GetMaterial(SubShapeID())->GetDebugColor() : inColor;
+
+	// Draw the plane
+	if (inDrawWireframe)
+	{
+		inRenderer->DrawWireTriangle(vertices[0], vertices[1], vertices[2], color);
+		inRenderer->DrawWireTriangle(vertices[0], vertices[2], vertices[3], color);
+	}
+	else
+	{
+		inRenderer->DrawTriangle(vertices[0], vertices[1], vertices[2], color, DebugRenderer::ECastShadow::On);
+		inRenderer->DrawTriangle(vertices[0], vertices[2], vertices[3], color, DebugRenderer::ECastShadow::On);
+	}
+}
+#endif // JPH_DEBUG_RENDERER
+
+bool PlaneShape::CastRay(const RayCast &inRay, const SubShapeIDCreator &inSubShapeIDCreator, RayCastResult &ioHit) const
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Test starting inside of negative half space
+	float distance = mPlane.SignedDistance(inRay.mOrigin);
+	if (distance <= 0.0f)
+	{
+		ioHit.mFraction = 0.0f;
+		ioHit.mSubShapeID2 = inSubShapeIDCreator.GetID();
+		return true;
+	}
+
+	// Test ray parallel to plane
+	float dot = inRay.mDirection.Dot(mPlane.GetNormal());
+	if (dot == 0.0f)
+		return false;
+
+	// Calculate hit fraction
+	float fraction = -distance / dot;
+	if (fraction >= 0.0f && fraction < ioHit.mFraction)
+	{
+		ioHit.mFraction = fraction;
+		ioHit.mSubShapeID2 = inSubShapeIDCreator.GetID();
+		return true;
+	}
+
+	return false;
+}
+
+void PlaneShape::CastRay(const RayCast &inRay, const RayCastSettings &inRayCastSettings, const SubShapeIDCreator &inSubShapeIDCreator, CastRayCollector &ioCollector, const ShapeFilter &inShapeFilter) const
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Test shape filter
+	if (!inShapeFilter.ShouldCollide(this, inSubShapeIDCreator.GetID()))
+		return;
+
+	// Inside solid half space?
+	float distance = mPlane.SignedDistance(inRay.mOrigin);
+	if (inRayCastSettings.mTreatConvexAsSolid
+		&& distance <= 0.0f // Inside plane
+		&& ioCollector.GetEarlyOutFraction() > 0.0f) // Willing to accept hits at fraction 0
+	{
+		// Hit at fraction 0
+		RayCastResult hit;
+		hit.mBodyID = TransformedShape::sGetBodyID(ioCollector.GetContext());
+		hit.mFraction = 0.0f;
+		hit.mSubShapeID2 = inSubShapeIDCreator.GetID();
+		ioCollector.AddHit(hit);
+	}
+
+	float dot = inRay.mDirection.Dot(mPlane.GetNormal());
+	if (dot != 0.0f // Parallel ray will not hit plane
+		&& (inRayCastSettings.mBackFaceMode == EBackFaceMode::CollideWithBackFaces || dot < 0.0f)) // Back face culling
+	{
+		// Calculate hit with plane
+		float fraction = -distance / dot;
+		if (fraction >= 0.0f && fraction < ioCollector.GetEarlyOutFraction())
+		{
+			RayCastResult hit;
+			hit.mBodyID = TransformedShape::sGetBodyID(ioCollector.GetContext());
+			hit.mFraction = fraction;
+			hit.mSubShapeID2 = inSubShapeIDCreator.GetID();
+			ioCollector.AddHit(hit);
+		}
+	}
+}
+
+void PlaneShape::CollidePoint(Vec3Arg inPoint, const SubShapeIDCreator &inSubShapeIDCreator, CollidePointCollector &ioCollector, const ShapeFilter &inShapeFilter) const
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Test shape filter
+	if (!inShapeFilter.ShouldCollide(this, inSubShapeIDCreator.GetID()))
+		return;
+
+	// Check if the point is inside the plane
+	if (mPlane.SignedDistance(inPoint) < 0.0f)
+		ioCollector.AddHit({ TransformedShape::sGetBodyID(ioCollector.GetContext()), inSubShapeIDCreator.GetID() });
+}
+
+void PlaneShape::CollideSoftBodyVertices(Mat44Arg inCenterOfMassTransform, Vec3Arg inScale, SoftBodyVertex *ioVertices, uint inNumVertices, [[maybe_unused]] float inDeltaTime, [[maybe_unused]] Vec3Arg inDisplacementDueToGravity, int inCollidingShapeIndex) const
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Convert plane to world space
+	Plane plane = mPlane.Scaled(inScale).GetTransformed(inCenterOfMassTransform);
+
+	for (SoftBodyVertex *v = ioVertices, *sbv_end = ioVertices + inNumVertices; v < sbv_end; ++v)
+		if (v->mInvMass > 0.0f)
+		{
+			// Calculate penetration
+			float penetration = -plane.SignedDistance(v->mPosition);
+			if (penetration > v->mLargestPenetration)
+			{
+				v->mLargestPenetration = penetration;
+				v->mCollisionPlane = plane;
+				v->mCollidingShapeIndex = inCollidingShapeIndex;
+			}
+		}
+}
+
+// This is a version of GetSupportingFace that returns a face that is large enough to cover the shape we're colliding with but not as large as the regular GetSupportedFace to avoid numerical precision issues
+inline static void sGetSupportingFace(const ConvexShape *inShape, Vec3Arg inShapeCOM, const Plane &inPlane, Mat44Arg inPlaneToWorld, ConvexShape::SupportingFace &outPlaneFace)
+{
+	// Project COM of shape onto plane
+	Plane world_plane = inPlane.GetTransformed(inPlaneToWorld);
+	Vec3 center = world_plane.ProjectPointOnPlane(inShapeCOM);
+
+	// Create orthogonal basis for the plane
+	Vec3 normal = world_plane.GetNormal();
+	Vec3 perp1, perp2;
+	sPlaneGetOrthogonalBasis(normal, perp1, perp2);
+
+	// Base the size of the face on the bounding box of the shape, ensuring that it is large enough to cover the entire shape
+	float size = inShape->GetLocalBounds().GetSize().Length();
+	perp1 *= size;
+	perp2 *= size;
+
+	// Emit the vertices
+	outPlaneFace.resize(4);
+	outPlaneFace[0] = center + perp1 + perp2;
+	outPlaneFace[1] = center + perp1 - perp2;
+	outPlaneFace[2] = center - perp1 - perp2;
+	outPlaneFace[3] = center - perp1 + perp2;
+}
+
+void PlaneShape::sCastConvexVsPlane(const ShapeCast &inShapeCast, const ShapeCastSettings &inShapeCastSettings, const Shape *inShape, Vec3Arg inScale, [[maybe_unused]] const ShapeFilter &inShapeFilter, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, CastShapeCollector &ioCollector)
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Get the shapes
+	JPH_ASSERT(inShapeCast.mShape->GetType() == EShapeType::Convex);
+	JPH_ASSERT(inShape->GetType() == EShapeType::Plane);
+	const ConvexShape *convex_shape = static_cast<const ConvexShape *>(inShapeCast.mShape);
+	const PlaneShape *plane_shape = static_cast<const PlaneShape *>(inShape);
+
+	// Shape cast is provided relative to COM of inShape, so all we need to do is transform our plane with inScale
+	Plane plane = plane_shape->mPlane.Scaled(inScale);
+	Vec3 normal = plane.GetNormal();
+
+	// Get support function
+	ConvexShape::SupportBuffer shape1_support_buffer;
+	const ConvexShape::Support *shape1_support = convex_shape->GetSupportFunction(ConvexShape::ESupportMode::Default, shape1_support_buffer, inShapeCast.mScale);
+
+	// Get the support point of the convex shape in the opposite direction of the plane normal in our local space
+	Vec3 normal_in_convex_shape_space = inShapeCast.mCenterOfMassStart.Multiply3x3Transposed(normal);
+	Vec3 support_point = inShapeCast.mCenterOfMassStart * shape1_support->GetSupport(-normal_in_convex_shape_space);
+	float signed_distance = plane.SignedDistance(support_point);
+	float convex_radius = shape1_support->GetConvexRadius();
+	float penetration_depth = -signed_distance + convex_radius;
+	float dot = inShapeCast.mDirection.Dot(normal);
+
+	// Collision output
+	Mat44 com_hit;
+	Vec3 point1, point2;
+	float fraction;
+
+	// Do we start in collision?
+	if (penetration_depth > 0.0f)
+	{
+		// Back face culling?
+		if (inShapeCastSettings.mBackFaceModeConvex == EBackFaceMode::IgnoreBackFaces && dot > 0.0f)
+			return;
+
+		// Shallower hit?
+		if (penetration_depth <= -ioCollector.GetEarlyOutFraction())
+			return;
+
+		// We're hitting at fraction 0
+		fraction = 0.0f;
+
+		// Get contact point
+		com_hit = inCenterOfMassTransform2;
+		point1 = inCenterOfMassTransform2 * (support_point - normal * convex_radius);
+		point2 = inCenterOfMassTransform2 * (support_point - normal * signed_distance);
+	}
+	else if (dot < 0.0f) // Moving towards the plane?
+	{
+		// Calculate hit fraction
+		fraction = penetration_depth / dot;
+		JPH_ASSERT(fraction >= 0.0f);
+
+		// Further than early out fraction?
+		if (fraction >= ioCollector.GetEarlyOutFraction())
+			return;
+
+		// Get contact point
+		com_hit = inCenterOfMassTransform2.PostTranslated(fraction * inShapeCast.mDirection);
+		point1 = point2 = com_hit * (support_point - normal * convex_radius);
+	}
+	else
+	{
+		// Moving away from the plane
+		return;
+	}
+
+	// Create cast result
+	Vec3 penetration_axis_world = com_hit.Multiply3x3(-normal);
+	bool back_facing = dot > 0.0f;
+	ShapeCastResult result(fraction, point1, point2, penetration_axis_world, back_facing, inSubShapeIDCreator1.GetID(), inSubShapeIDCreator2.GetID(), TransformedShape::sGetBodyID(ioCollector.GetContext()));
+
+	// Gather faces
+	if (inShapeCastSettings.mCollectFacesMode == ECollectFacesMode::CollectFaces)
+	{
+		// Get supporting face of convex shape
+		Mat44 shape_to_world = com_hit * inShapeCast.mCenterOfMassStart;
+		convex_shape->GetSupportingFace(SubShapeID(), normal_in_convex_shape_space, inShapeCast.mScale, shape_to_world, result.mShape1Face);
+
+		// Get supporting face of plane
+		if (!result.mShape1Face.empty())
+			sGetSupportingFace(convex_shape, shape_to_world.GetTranslation(), plane, inCenterOfMassTransform2, result.mShape2Face);
+	}
+
+	// Notify the collector
+	JPH_IF_TRACK_NARROWPHASE_STATS(TrackNarrowPhaseCollector track;)
+	ioCollector.AddHit(result);
+}
+
+struct PlaneShape::PSGetTrianglesContext
+{
+	Float3	mVertices[4];
+	bool	mDone = false;
+};
+
+void PlaneShape::GetTrianglesStart(GetTrianglesContext &ioContext, const AABox &inBox, Vec3Arg inPositionCOM, QuatArg inRotation, Vec3Arg inScale) const
+{
+	static_assert(sizeof(PSGetTrianglesContext) <= sizeof(GetTrianglesContext), "GetTrianglesContext too small");
+	JPH_ASSERT(IsAligned(&ioContext, alignof(PSGetTrianglesContext)));
+
+	PSGetTrianglesContext *context = new (&ioContext) PSGetTrianglesContext();
+
+	// Get the vertices of the plane
+	Vec3 vertices[4];
+	GetVertices(vertices);
+
+	// Reverse if scale is inside out
+	if (ScaleHelpers::IsInsideOut(inScale))
+	{
+		swap(vertices[0], vertices[3]);
+		swap(vertices[1], vertices[2]);
+	}
+
+	// Transform them to world space
+	Mat44 com = Mat44::sRotationTranslation(inRotation, inPositionCOM).PreScaled(inScale);
+	for (uint i = 0; i < 4; ++i)
+		(com * vertices[i]).StoreFloat3(&context->mVertices[i]);
+}
+
+int PlaneShape::GetTrianglesNext(GetTrianglesContext &ioContext, int inMaxTrianglesRequested, Float3 *outTriangleVertices, const PhysicsMaterial **outMaterials) const
+{
+	static_assert(cGetTrianglesMinTrianglesRequested >= 2, "cGetTrianglesMinTrianglesRequested is too small");
+	JPH_ASSERT(inMaxTrianglesRequested >= cGetTrianglesMinTrianglesRequested);
+
+	// Check if we're done
+	PSGetTrianglesContext &context = (PSGetTrianglesContext &)ioContext;
+	if (context.mDone)
+		return 0;
+	context.mDone = true;
+
+	// 1st triangle
+	outTriangleVertices[0] = context.mVertices[0];
+	outTriangleVertices[1] = context.mVertices[1];
+	outTriangleVertices[2] = context.mVertices[2];
+
+	// 2nd triangle
+	outTriangleVertices[3] = context.mVertices[0];
+	outTriangleVertices[4] = context.mVertices[2];
+	outTriangleVertices[5] = context.mVertices[3];
+
+	if (outMaterials != nullptr)
+	{
+		// Get material
+		const PhysicsMaterial *material = GetMaterial(SubShapeID());
+		outMaterials[0] = material;
+		outMaterials[1] = material;
+	}
+
+	return 2;
+}
+
+void PlaneShape::sCollideConvexVsPlane(const Shape *inShape1, const Shape *inShape2, Vec3Arg inScale1, Vec3Arg inScale2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, const CollideShapeSettings &inCollideShapeSettings, CollideShapeCollector &ioCollector, [[maybe_unused]] const ShapeFilter &inShapeFilter)
+{
+	JPH_PROFILE_FUNCTION();
+
+	// Get the shapes
+	JPH_ASSERT(inShape1->GetType() == EShapeType::Convex);
+	JPH_ASSERT(inShape2->GetType() == EShapeType::Plane);
+	const ConvexShape *shape1 = static_cast<const ConvexShape *>(inShape1);
+	const PlaneShape *shape2 = static_cast<const PlaneShape *>(inShape2);
+
+	// Transform the plane to the space of the convex shape
+	Plane scaled_plane = shape2->mPlane.Scaled(inScale2);
+	Plane plane = scaled_plane.GetTransformed(inCenterOfMassTransform1.InversedRotationTranslation() * inCenterOfMassTransform2);
+	Vec3 normal = plane.GetNormal();
+
+	// Get support function
+	ConvexShape::SupportBuffer shape1_support_buffer;
+	const ConvexShape::Support *shape1_support = shape1->GetSupportFunction(ConvexShape::ESupportMode::Default, shape1_support_buffer, inScale1);
+
+	// Get the support point of the convex shape in the opposite direction of the plane normal
+	Vec3 support_point = shape1_support->GetSupport(-normal);
+	float signed_distance = plane.SignedDistance(support_point);
+	float convex_radius = shape1_support->GetConvexRadius();
+	float penetration_depth = -signed_distance + convex_radius;
+	if (penetration_depth > -inCollideShapeSettings.mMaxSeparationDistance)
+	{
+		// Get contact point
+		Vec3 point1 = inCenterOfMassTransform1 * (support_point - normal * convex_radius);
+		Vec3 point2 = inCenterOfMassTransform1 * (support_point - normal * signed_distance);
+		Vec3 penetration_axis_world = inCenterOfMassTransform1.Multiply3x3(-normal);
+
+		// Create collision result
+		CollideShapeResult result(point1, point2, penetration_axis_world, penetration_depth, inSubShapeIDCreator1.GetID(), inSubShapeIDCreator2.GetID(), TransformedShape::sGetBodyID(ioCollector.GetContext()));
+
+		// Gather faces
+		if (inCollideShapeSettings.mCollectFacesMode == ECollectFacesMode::CollectFaces)
+		{
+			// Get supporting face of shape 1
+			shape1->GetSupportingFace(SubShapeID(), normal, inScale1, inCenterOfMassTransform1, result.mShape1Face);
+
+			// Get supporting face of shape 2
+			if (!result.mShape1Face.empty())
+				sGetSupportingFace(shape1, inCenterOfMassTransform1.GetTranslation(), scaled_plane, inCenterOfMassTransform2, result.mShape2Face);
+		}
+
+		// Notify the collector
+		JPH_IF_TRACK_NARROWPHASE_STATS(TrackNarrowPhaseCollector track;)
+		ioCollector.AddHit(result);
+	}
+}
+
+void PlaneShape::SaveBinaryState(StreamOut &inStream) const
+{
+	Shape::SaveBinaryState(inStream);
+
+	inStream.Write(mPlane);
+	inStream.Write(mSize);
+}
+
+void PlaneShape::RestoreBinaryState(StreamIn &inStream)
+{
+	Shape::RestoreBinaryState(inStream);
+
+	inStream.Read(mPlane);
+	inStream.Read(mSize);
+
+	CalculateLocalBounds();
+}
+
+void PlaneShape::SaveMaterialState(PhysicsMaterialList &outMaterials) const
+{
+	outMaterials = { mMaterial };
+}
+
+void PlaneShape::RestoreMaterialState(const PhysicsMaterialRefC *inMaterials, uint inNumMaterials)
+{
+	JPH_ASSERT(inNumMaterials == 1);
+	mMaterial = inMaterials[0];
+}
+
+void PlaneShape::sRegister()
+{
+	ShapeFunctions &f = ShapeFunctions::sGet(EShapeSubType::Plane);
+	f.mConstruct = []() -> Shape * { return new PlaneShape; };
+	f.mColor = Color::sDarkRed;
+
+	for (EShapeSubType s : sConvexSubShapeTypes)
+	{
+		CollisionDispatch::sRegisterCollideShape(s, EShapeSubType::Plane, sCollideConvexVsPlane);
+		CollisionDispatch::sRegisterCastShape(s, EShapeSubType::Plane, sCastConvexVsPlane);
+
+		CollisionDispatch::sRegisterCastShape(EShapeSubType::Plane, s, CollisionDispatch::sReversedCastShape);
+		CollisionDispatch::sRegisterCollideShape(EShapeSubType::Plane, s, CollisionDispatch::sReversedCollideShape);
+	}
+}
+
+JPH_NAMESPACE_END

+ 143 - 0
Jolt/Physics/Collision/Shape/PlaneShape.h

@@ -0,0 +1,143 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Collision/Shape/Shape.h>
+#include <Jolt/Physics/Collision/Shape/SubShapeID.h>
+#include <Jolt/Physics/Collision/PhysicsMaterial.h>
+
+JPH_NAMESPACE_BEGIN
+
+class CollideShapeSettings;
+
+/// Class that constructs a PlaneShape
+class JPH_EXPORT PlaneShapeSettings final : public ShapeSettings
+{
+public:
+	JPH_DECLARE_SERIALIZABLE_VIRTUAL(JPH_EXPORT, PlaneShapeSettings)
+
+	/// Default constructor for deserialization
+									PlaneShapeSettings() = default;
+
+	/// Create a plane shape.
+									PlaneShapeSettings(const Plane &inPlane, const PhysicsMaterial *inMaterial = nullptr, float inSize = cDefaultSize) : mPlane(inPlane), mMaterial(inMaterial), mSize(inSize) { }
+
+	// See: ShapeSettings
+	virtual ShapeResult				Create() const override;
+
+	Plane							mPlane;														///< Plane that describes the shape. The negative half space is considered solid.
+
+	RefConst<PhysicsMaterial>		mMaterial;													///< Surface material of the plane
+
+	static constexpr float			cDefaultSize = 1000.0f;										///< Default size of the plane
+
+	float							mSize = cDefaultSize;										///< The bounding box of this plane will run from [-size, size]. Keep this as low as possible for better broad phase performance.
+};
+
+/// A plane shape. The negative half space is considered solid. Planes cannot be dynamic objects, only static or kinematic.
+/// The plane is considered an infinite shape, but testing collision outside of its bounding box (defined by the size parameter) will not return a collision result.
+/// At the edge of the bounding box collision with the plane will be inconsistent. If you need something of a well defined size, a box shape may be better.
+class JPH_EXPORT PlaneShape final : public Shape
+{
+public:
+	JPH_OVERRIDE_NEW_DELETE
+
+	/// Constructor
+									PlaneShape() : Shape(EShapeType::Plane, EShapeSubType::Plane) { }
+									PlaneShape(const Plane &inPlane, const PhysicsMaterial *inMaterial = nullptr, float inSize = PlaneShapeSettings::cDefaultSize) : Shape(EShapeType::Plane, EShapeSubType::Plane), mPlane(inPlane), mMaterial(inMaterial), mSize(inSize) { CalculateLocalBounds(); }
+									PlaneShape(const PlaneShapeSettings &inSettings, ShapeResult &outResult);
+
+	/// Get the plane
+	const Plane &					GetPlane() const											{ return mPlane; }
+
+	/// Get the size of the bounding box of the plane
+	float							GetSize() const												{ return mSize; }
+
+	// See Shape::MustBeStatic
+	virtual bool					MustBeStatic() const override								{ return true; }
+
+	// See Shape::GetLocalBounds
+	virtual AABox					GetLocalBounds() const override								{ return mLocalBounds; }
+
+	// See Shape::GetSubShapeIDBitsRecursive
+	virtual uint					GetSubShapeIDBitsRecursive() const override					{ return 0; }
+
+	// See Shape::GetInnerRadius
+	virtual float					GetInnerRadius() const override								{ return 0.0f; }
+
+	// See Shape::GetMassProperties
+	virtual MassProperties			GetMassProperties() const override;
+
+	// See Shape::GetMaterial
+	virtual const PhysicsMaterial *	GetMaterial(const SubShapeID &inSubShapeID) const override	{ JPH_ASSERT(inSubShapeID.IsEmpty(), "Invalid subshape ID"); return mMaterial != nullptr? mMaterial : PhysicsMaterial::sDefault; }
+
+	// See Shape::GetSurfaceNormal
+	virtual Vec3					GetSurfaceNormal(const SubShapeID &inSubShapeID, Vec3Arg inLocalSurfacePosition) const override { JPH_ASSERT(inSubShapeID.IsEmpty(), "Invalid subshape ID"); return mPlane.GetNormal(); }
+
+	// See Shape::GetSupportingFace
+	virtual void					GetSupportingFace(const SubShapeID &inSubShapeID, Vec3Arg inDirection, Vec3Arg inScale, Mat44Arg inCenterOfMassTransform, SupportingFace &outVertices) const override;
+
+#ifdef JPH_DEBUG_RENDERER
+	// See Shape::Draw
+	virtual void					Draw(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform, Vec3Arg inScale, ColorArg inColor, bool inUseMaterialColors, bool inDrawWireframe) const override;
+#endif // JPH_DEBUG_RENDERER
+
+	// See Shape::CastRay
+	virtual bool					CastRay(const RayCast &inRay, const SubShapeIDCreator &inSubShapeIDCreator, RayCastResult &ioHit) const override;
+	virtual void					CastRay(const RayCast &inRay, const RayCastSettings &inRayCastSettings, const SubShapeIDCreator &inSubShapeIDCreator, CastRayCollector &ioCollector, const ShapeFilter &inShapeFilter = { }) const override;
+
+	// See: Shape::CollidePoint
+	virtual void					CollidePoint(Vec3Arg inPoint, const SubShapeIDCreator &inSubShapeIDCreator, CollidePointCollector &ioCollector, const ShapeFilter &inShapeFilter = { }) const override;
+
+	// See: Shape::CollideSoftBodyVertices
+	virtual void					CollideSoftBodyVertices(Mat44Arg inCenterOfMassTransform, Vec3Arg inScale, SoftBodyVertex *ioVertices, uint inNumVertices, float inDeltaTime, Vec3Arg inDisplacementDueToGravity, int inCollidingShapeIndex) const override;
+
+	// See Shape::GetTrianglesStart
+	virtual void					GetTrianglesStart(GetTrianglesContext &ioContext, const AABox &inBox, Vec3Arg inPositionCOM, QuatArg inRotation, Vec3Arg inScale) const override;
+
+	// See Shape::GetTrianglesNext
+	virtual int						GetTrianglesNext(GetTrianglesContext &ioContext, int inMaxTrianglesRequested, Float3 *outTriangleVertices, const PhysicsMaterial **outMaterials = nullptr) const override;
+
+	// See Shape::GetSubmergedVolume
+	virtual void					GetSubmergedVolume(Mat44Arg inCenterOfMassTransform, Vec3Arg inScale, const Plane &inSurface, float &outTotalVolume, float &outSubmergedVolume, Vec3 &outCenterOfBuoyancy JPH_IF_DEBUG_RENDERER(, RVec3Arg inBaseOffset)) const override { JPH_ASSERT(false, "Not supported"); }
+
+	// See Shape
+	virtual void					SaveBinaryState(StreamOut &inStream) const override;
+	virtual void					SaveMaterialState(PhysicsMaterialList &outMaterials) const override;
+	virtual void					RestoreMaterialState(const PhysicsMaterialRefC *inMaterials, uint inNumMaterials) override;
+
+	// See Shape::GetStats
+	virtual Stats					GetStats() const override									{ return Stats(sizeof(*this), 0); }
+
+	// See Shape::GetVolume
+	virtual float					GetVolume() const override									{ return 0; }
+
+	// Register shape functions with the registry
+	static void						sRegister();
+
+protected:
+	// See: Shape::RestoreBinaryState
+	virtual void					RestoreBinaryState(StreamIn &inStream) override;
+
+private:
+	struct							PSGetTrianglesContext;										///< Context class for GetTrianglesStart/Next
+
+	// Get 4 vertices that form the plane
+	void							GetVertices(Vec3 *outVertices) const;
+
+	// Cache the local bounds
+	void							CalculateLocalBounds();
+
+	// Helper functions called by CollisionDispatch
+	static void						sCollideConvexVsPlane(const Shape *inShape1, const Shape *inShape2, Vec3Arg inScale1, Vec3Arg inScale2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, const CollideShapeSettings &inCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter);
+	static void						sCastConvexVsPlane(const ShapeCast &inShapeCast, const ShapeCastSettings &inShapeCastSettings, const Shape *inShape, Vec3Arg inScale, const ShapeFilter &inShapeFilter, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, CastShapeCollector &ioCollector);
+
+	Plane							mPlane;
+	RefConst<PhysicsMaterial>		mMaterial;
+	float							mSize;
+	AABox							mLocalBounds;
+};
+
+JPH_NAMESPACE_END

+ 7 - 2
Jolt/Physics/Collision/Shape/Shape.h

@@ -67,6 +67,8 @@ enum class EShapeType : uint8
 	User2,
 	User2,
 	User3,
 	User3,
 	User4,
 	User4,
+
+	Plane,							///< Used by PlaneShape
 };
 };
 
 
 /// This enumerates all shape types, each shape can return its type through Shape::GetSubType
 /// This enumerates all shape types, each shape can return its type through Shape::GetSubType
@@ -114,10 +116,13 @@ enum class EShapeSubType : uint8
 	UserConvex6,
 	UserConvex6,
 	UserConvex7,
 	UserConvex7,
 	UserConvex8,
 	UserConvex8,
+
+	// Other shapes
+	Plane,
 };
 };
 
 
 // Sets of shape sub types
 // Sets of shape sub types
-static constexpr EShapeSubType sAllSubShapeTypes[] = { EShapeSubType::Sphere, EShapeSubType::Box, EShapeSubType::Triangle, EShapeSubType::Capsule, EShapeSubType::TaperedCapsule, EShapeSubType::Cylinder, EShapeSubType::ConvexHull, EShapeSubType::StaticCompound, EShapeSubType::MutableCompound, EShapeSubType::RotatedTranslated, EShapeSubType::Scaled, EShapeSubType::OffsetCenterOfMass, EShapeSubType::Mesh, EShapeSubType::HeightField, EShapeSubType::SoftBody, EShapeSubType::User1, EShapeSubType::User2, EShapeSubType::User3, EShapeSubType::User4, EShapeSubType::User5, EShapeSubType::User6, EShapeSubType::User7, EShapeSubType::User8, EShapeSubType::UserConvex1, EShapeSubType::UserConvex2, EShapeSubType::UserConvex3, EShapeSubType::UserConvex4, EShapeSubType::UserConvex5, EShapeSubType::UserConvex6, EShapeSubType::UserConvex7, EShapeSubType::UserConvex8 };
+static constexpr EShapeSubType sAllSubShapeTypes[] = { EShapeSubType::Sphere, EShapeSubType::Box, EShapeSubType::Triangle, EShapeSubType::Capsule, EShapeSubType::TaperedCapsule, EShapeSubType::Cylinder, EShapeSubType::ConvexHull, EShapeSubType::StaticCompound, EShapeSubType::MutableCompound, EShapeSubType::RotatedTranslated, EShapeSubType::Scaled, EShapeSubType::OffsetCenterOfMass, EShapeSubType::Mesh, EShapeSubType::HeightField, EShapeSubType::SoftBody, EShapeSubType::User1, EShapeSubType::User2, EShapeSubType::User3, EShapeSubType::User4, EShapeSubType::User5, EShapeSubType::User6, EShapeSubType::User7, EShapeSubType::User8, EShapeSubType::UserConvex1, EShapeSubType::UserConvex2, EShapeSubType::UserConvex3, EShapeSubType::UserConvex4, EShapeSubType::UserConvex5, EShapeSubType::UserConvex6, EShapeSubType::UserConvex7, EShapeSubType::UserConvex8, EShapeSubType::Plane };
 static constexpr EShapeSubType sConvexSubShapeTypes[] = { EShapeSubType::Sphere, EShapeSubType::Box, EShapeSubType::Triangle, EShapeSubType::Capsule, EShapeSubType::TaperedCapsule, EShapeSubType::Cylinder, EShapeSubType::ConvexHull, EShapeSubType::UserConvex1, EShapeSubType::UserConvex2, EShapeSubType::UserConvex3, EShapeSubType::UserConvex4, EShapeSubType::UserConvex5, EShapeSubType::UserConvex6, EShapeSubType::UserConvex7, EShapeSubType::UserConvex8 };
 static constexpr EShapeSubType sConvexSubShapeTypes[] = { EShapeSubType::Sphere, EShapeSubType::Box, EShapeSubType::Triangle, EShapeSubType::Capsule, EShapeSubType::TaperedCapsule, EShapeSubType::Cylinder, EShapeSubType::ConvexHull, EShapeSubType::UserConvex1, EShapeSubType::UserConvex2, EShapeSubType::UserConvex3, EShapeSubType::UserConvex4, EShapeSubType::UserConvex5, EShapeSubType::UserConvex6, EShapeSubType::UserConvex7, EShapeSubType::UserConvex8 };
 static constexpr EShapeSubType sCompoundSubShapeTypes[] = { EShapeSubType::StaticCompound, EShapeSubType::MutableCompound };
 static constexpr EShapeSubType sCompoundSubShapeTypes[] = { EShapeSubType::StaticCompound, EShapeSubType::MutableCompound };
 static constexpr EShapeSubType sDecoratorSubShapeTypes[] = { EShapeSubType::RotatedTranslated, EShapeSubType::Scaled, EShapeSubType::OffsetCenterOfMass };
 static constexpr EShapeSubType sDecoratorSubShapeTypes[] = { EShapeSubType::RotatedTranslated, EShapeSubType::Scaled, EShapeSubType::OffsetCenterOfMass };
@@ -126,7 +131,7 @@ static constexpr EShapeSubType sDecoratorSubShapeTypes[] = { EShapeSubType::Rota
 static constexpr uint NumSubShapeTypes = uint(size(sAllSubShapeTypes));
 static constexpr uint NumSubShapeTypes = uint(size(sAllSubShapeTypes));
 
 
 /// Names of sub shape types
 /// Names of sub shape types
-static constexpr const char *sSubShapeTypeNames[] = { "Sphere", "Box", "Triangle", "Capsule", "TaperedCapsule", "Cylinder", "ConvexHull", "StaticCompound", "MutableCompound", "RotatedTranslated", "Scaled", "OffsetCenterOfMass", "Mesh", "HeightField", "SoftBody", "User1", "User2", "User3", "User4", "User5", "User6", "User7", "User8", "UserConvex1", "UserConvex2", "UserConvex3", "UserConvex4", "UserConvex5", "UserConvex6", "UserConvex7", "UserConvex8" };
+static constexpr const char *sSubShapeTypeNames[] = { "Sphere", "Box", "Triangle", "Capsule", "TaperedCapsule", "Cylinder", "ConvexHull", "StaticCompound", "MutableCompound", "RotatedTranslated", "Scaled", "OffsetCenterOfMass", "Mesh", "HeightField", "SoftBody", "User1", "User2", "User3", "User4", "User5", "User6", "User7", "User8", "UserConvex1", "UserConvex2", "UserConvex3", "UserConvex4", "UserConvex5", "UserConvex6", "UserConvex7", "UserConvex8", "Plane" };
 static_assert(size(sSubShapeTypeNames) == NumSubShapeTypes);
 static_assert(size(sSubShapeTypeNames) == NumSubShapeTypes);
 
 
 /// Class that can construct shapes and that is serializable using the ObjectStream system.
 /// Class that can construct shapes and that is serializable using the ObjectStream system.

+ 4 - 0
Jolt/RegisterTypes.cpp

@@ -10,6 +10,7 @@
 #include <Jolt/Core/TickCounter.h>
 #include <Jolt/Core/TickCounter.h>
 #include <Jolt/Physics/Collision/CollisionDispatch.h>
 #include <Jolt/Physics/Collision/CollisionDispatch.h>
 #include <Jolt/Physics/Collision/Shape/TriangleShape.h>
 #include <Jolt/Physics/Collision/Shape/TriangleShape.h>
+#include <Jolt/Physics/Collision/Shape/PlaneShape.h>
 #include <Jolt/Physics/Collision/Shape/SphereShape.h>
 #include <Jolt/Physics/Collision/Shape/SphereShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
@@ -32,6 +33,7 @@ JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, CompoundShapeSettin
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, StaticCompoundShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, StaticCompoundShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, MutableCompoundShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, MutableCompoundShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, TriangleShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, TriangleShapeSettings)
+JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, PlaneShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, SphereShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, SphereShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, BoxShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, BoxShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, CapsuleShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, CapsuleShapeSettings)
@@ -116,6 +118,7 @@ void RegisterTypesInternal(uint64 inVersionID)
 
 
 	// Leaf classes
 	// Leaf classes
 	TriangleShape::sRegister();
 	TriangleShape::sRegister();
+	PlaneShape::sRegister();
 	SphereShape::sRegister();
 	SphereShape::sRegister();
 	BoxShape::sRegister();
 	BoxShape::sRegister();
 	CapsuleShape::sRegister();
 	CapsuleShape::sRegister();
@@ -139,6 +142,7 @@ void RegisterTypesInternal(uint64 inVersionID)
 		JPH_RTTI(StaticCompoundShapeSettings),
 		JPH_RTTI(StaticCompoundShapeSettings),
 		JPH_RTTI(MutableCompoundShapeSettings),
 		JPH_RTTI(MutableCompoundShapeSettings),
 		JPH_RTTI(TriangleShapeSettings),
 		JPH_RTTI(TriangleShapeSettings),
+		JPH_RTTI(PlaneShapeSettings),
 		JPH_RTTI(SphereShapeSettings),
 		JPH_RTTI(SphereShapeSettings),
 		JPH_RTTI(BoxShapeSettings),
 		JPH_RTTI(BoxShapeSettings),
 		JPH_RTTI(CapsuleShapeSettings),
 		JPH_RTTI(CapsuleShapeSettings),

+ 4 - 0
Samples/Samples.cmake

@@ -209,6 +209,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledHeightFieldShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledHeightFieldShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledMeshShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledMeshShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledMeshShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledMeshShapeTest.h
+	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledPlaneShapeTest.cpp
+	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledPlaneShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledSphereShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledSphereShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledSphereShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledSphereShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTaperedCapsuleShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTaperedCapsuleShapeTest.cpp
@@ -237,6 +239,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/Shapes/MeshShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/MeshShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/SphereShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/SphereShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/SphereShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/SphereShapeTest.h
+	${SAMPLES_ROOT}/Tests/Shapes/PlaneShapeTest.cpp
+	${SAMPLES_ROOT}/Tests/Shapes/PlaneShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/RotatedTranslatedShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/RotatedTranslatedShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/RotatedTranslatedShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/RotatedTranslatedShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/TaperedCapsuleShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/TaperedCapsuleShapeTest.cpp

+ 5 - 0
Samples/SamplesApp.cpp

@@ -33,6 +33,7 @@
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/TriangleShape.h>
 #include <Jolt/Physics/Collision/Shape/TriangleShape.h>
+#include <Jolt/Physics/Collision/Shape/PlaneShape.h>
 #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
 #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/MutableCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/MutableCompoundShape.h>
@@ -204,6 +205,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, CylinderShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, StaticCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, StaticCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, MutableCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, MutableCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, TriangleShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, TriangleShapeTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, PlaneShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ConvexHullShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ConvexHullShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, MeshShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, MeshShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, HeightFieldShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, HeightFieldShapeTest)
@@ -225,6 +227,7 @@ static TestNameAndRTTI sShapeTests[] =
 	{ "Static Compound Shape",				JPH_RTTI(StaticCompoundShapeTest) },
 	{ "Static Compound Shape",				JPH_RTTI(StaticCompoundShapeTest) },
 	{ "Mutable Compound Shape",				JPH_RTTI(MutableCompoundShapeTest) },
 	{ "Mutable Compound Shape",				JPH_RTTI(MutableCompoundShapeTest) },
 	{ "Triangle Shape",						JPH_RTTI(TriangleShapeTest) },
 	{ "Triangle Shape",						JPH_RTTI(TriangleShapeTest) },
+	{ "Plane Shape",						JPH_RTTI(PlaneShapeTest) },
 	{ "Rotated Translated Shape",			JPH_RTTI(RotatedTranslatedShapeTest) },
 	{ "Rotated Translated Shape",			JPH_RTTI(RotatedTranslatedShapeTest) },
 	{ "Offset Center Of Mass Shape",		JPH_RTTI(OffsetCenterOfMassShapeTest) }
 	{ "Offset Center Of Mass Shape",		JPH_RTTI(OffsetCenterOfMassShapeTest) }
 };
 };
@@ -240,6 +243,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledHeightFieldShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledStaticCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledStaticCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledMutableCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledMutableCompoundShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledTriangleShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledTriangleShapeTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledPlaneShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledOffsetCenterOfMassShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledOffsetCenterOfMassShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, DynamicScaledShape)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, DynamicScaledShape)
 
 
@@ -256,6 +260,7 @@ static TestNameAndRTTI sScaledShapeTests[] =
 	{ "Static Compound Shape",				JPH_RTTI(ScaledStaticCompoundShapeTest) },
 	{ "Static Compound Shape",				JPH_RTTI(ScaledStaticCompoundShapeTest) },
 	{ "Mutable Compound Shape",				JPH_RTTI(ScaledMutableCompoundShapeTest) },
 	{ "Mutable Compound Shape",				JPH_RTTI(ScaledMutableCompoundShapeTest) },
 	{ "Triangle Shape",						JPH_RTTI(ScaledTriangleShapeTest) },
 	{ "Triangle Shape",						JPH_RTTI(ScaledTriangleShapeTest) },
+	{ "Plane Shape",						JPH_RTTI(ScaledPlaneShapeTest) },
 	{ "Offset Center Of Mass Shape",		JPH_RTTI(ScaledOffsetCenterOfMassShapeTest) },
 	{ "Offset Center Of Mass Shape",		JPH_RTTI(ScaledOffsetCenterOfMassShapeTest) },
 	{ "Dynamic Scaled Shape",				JPH_RTTI(DynamicScaledShape) }
 	{ "Dynamic Scaled Shape",				JPH_RTTI(DynamicScaledShape) }
 };
 };

+ 64 - 0
Samples/Tests/ScaledShapes/ScaledPlaneShapeTest.cpp

@@ -0,0 +1,64 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/ScaledShapes/ScaledPlaneShapeTest.h>
+#include <Jolt/Physics/Collision/Shape/PlaneShape.h>
+#include <Jolt/Physics/Collision/Shape/ScaledShape.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/SphereShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(ScaledPlaneShapeTest)
+{
+	JPH_ADD_BASE_CLASS(ScaledPlaneShapeTest, Test)
+}
+
+void ScaledPlaneShapeTest::Initialize()
+{
+	// Floor
+	CreateFloor();
+
+	RefConst<ShapeSettings> plane_shape = new PlaneShapeSettings(Plane(Vec3(0.1f, 1.0f, 0.1f).Normalized(), -0.5f), nullptr, 5.0f);
+
+	// Original shape
+	Body &body1 = *mBodyInterface->CreateBody(BodyCreationSettings(plane_shape, RVec3(-60, 10, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(body1.GetID(), EActivation::DontActivate);
+
+	// Uniformly scaled shape < 1
+	Body &body2 = *mBodyInterface->CreateBody(BodyCreationSettings(new ScaledShapeSettings(plane_shape, Vec3::sReplicate(0.5f)), RVec3(-40, 10, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(body2.GetID(), EActivation::DontActivate);
+
+	// Uniformly scaled shape > 1
+	Body &body3 = *mBodyInterface->CreateBody(BodyCreationSettings(new ScaledShapeSettings(plane_shape, Vec3::sReplicate(1.5f)), RVec3(-20, 10, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(body3.GetID(), EActivation::DontActivate);
+
+	// Non-uniform scaled shape
+	Body &body4 = *mBodyInterface->CreateBody(BodyCreationSettings(new ScaledShapeSettings(plane_shape, Vec3(0.5f, 1.0f, 1.5f)), RVec3(0, 10, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(body4.GetID(), EActivation::DontActivate);
+
+	// Flipped in 2 axis
+	Body &body5 = *mBodyInterface->CreateBody(BodyCreationSettings(new ScaledShapeSettings(plane_shape, Vec3(-0.5f, 1.0f, -1.5f)), RVec3(20, 10, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(body5.GetID(), EActivation::DontActivate);
+
+	// Inside out
+	Body &body6 = *mBodyInterface->CreateBody(BodyCreationSettings(new ScaledShapeSettings(plane_shape, Vec3(-0.5f, 1.0f, 1.5f)), RVec3(40, 10, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(body6.GetID(), EActivation::DontActivate);
+
+	// Upside down
+	Body &body7 = *mBodyInterface->CreateBody(BodyCreationSettings(new ScaledShapeSettings(plane_shape, Vec3(0.5f, -1.0f, 1.5f)), RVec3(60, 10, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(body7.GetID(), EActivation::DontActivate);
+
+	// Create a number of balls above the planes
+	RefConst<Shape> sphere_shape = new SphereShape(0.2f);
+	RefConst<Shape> box_shape = new BoxShape(Vec3(0.2f, 0.2f, 0.4f), 0.01f);
+	for (int i = 0; i < 7; ++i)
+		for (int j = 0; j < 5; ++j)
+		{
+			Body &dynamic = *mBodyInterface->CreateBody(BodyCreationSettings((j & 1)? box_shape : sphere_shape, RVec3(-60.0f + 20.0f * i, 15.0f + 0.5f * j, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
+			mBodyInterface->AddBody(dynamic.GetID(), EActivation::Activate);
+		}
+}

+ 16 - 0
Samples/Tests/ScaledShapes/ScaledPlaneShapeTest.h

@@ -0,0 +1,16 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+class ScaledPlaneShapeTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, ScaledPlaneShapeTest)
+
+	// See: Test
+	virtual void	Initialize() override;
+};

+ 27 - 0
Samples/Tests/Shapes/PlaneShapeTest.cpp

@@ -0,0 +1,27 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/Shapes/PlaneShapeTest.h>
+#include <Jolt/Physics/Collision/Shape/PlaneShape.h>
+#include <Jolt/Physics/Collision/Shape/SphereShape.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(PlaneShapeTest)
+{
+	JPH_ADD_BASE_CLASS(PlaneShapeTest, Test)
+}
+
+void PlaneShapeTest::Initialize()
+{
+	// Create a plane as floor
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(new PlaneShape(Plane(Vec3(0.1f, 1.0f, 0.0f).Normalized(), 1.0f), nullptr, 100), RVec3(0, 0, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+	// Add some shapes
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(new SphereShape(0.5f), RVec3(0, 1, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3::sReplicate(0.5f)), RVec3(2, 1, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+}

+ 16 - 0
Samples/Tests/Shapes/PlaneShapeTest.h

@@ -0,0 +1,16 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+class PlaneShapeTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, PlaneShapeTest)
+
+	// See: Test
+	virtual void	Initialize() override;
+};