Jelajahi Sumber

Added tapered cylinder shape (#1241)

Jorrit Rouwe 11 bulan lalu
induk
melakukan
57083c6352

+ 2 - 0
Jolt/Jolt.cmake

@@ -277,6 +277,8 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/TaperedCapsuleShape.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/TaperedCapsuleShape.gliffy
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/TaperedCapsuleShape.h
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/TaperedCylinderShape.cpp
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/TaperedCylinderShape.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/TriangleShape.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/Shape/TriangleShape.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ShapeCast.h

+ 2 - 6
Jolt/Physics/Collision/Shape/CylinderShape.cpp

@@ -391,18 +391,14 @@ void CylinderShape::RestoreBinaryState(StreamIn &inStream)
 
 bool CylinderShape::IsValidScale(Vec3Arg inScale) const
 {
-	// X and Z need same scale
-	Vec3 abs_scale = inScale.Abs();
-	return ConvexShape::IsValidScale(inScale) && abs_scale.Swizzle<SWIZZLE_Z, SWIZZLE_Y, SWIZZLE_X>().IsClose(abs_scale, ScaleHelpers::cScaleToleranceSq);
+	return ConvexShape::IsValidScale(inScale) && ScaleHelpers::IsUniformScaleXZ(inScale.Abs());
 }
 
 Vec3 CylinderShape::MakeScaleValid(Vec3Arg inScale) const
 {
 	Vec3 scale = ScaleHelpers::MakeNonZeroScale(inScale);
 
-	// Average X and Z
-	Vec3 abs_scale = scale.Abs();
-	return 0.5f * scale.GetSign() * (abs_scale + abs_scale.Swizzle<SWIZZLE_Z, SWIZZLE_Y, SWIZZLE_X>());
+	return scale.GetSign() * ScaleHelpers::MakeUniformScaleXZ(scale.Abs());
 }
 
 void CylinderShape::sRegister()

+ 6 - 0
Jolt/Physics/Collision/Shape/ScaleHelpers.h

@@ -23,6 +23,9 @@ namespace ScaleHelpers
 	/// Test if a scale is uniform
 	inline bool				IsUniformScale(Vec3Arg inScale)									{ return inScale.Swizzle<SWIZZLE_Y, SWIZZLE_Z, SWIZZLE_X>().IsClose(inScale, cScaleToleranceSq); }
 
+	/// Test if a scale is uniform in XZ
+	inline bool				IsUniformScaleXZ(Vec3Arg inScale)								{ return inScale.Swizzle<SWIZZLE_Z, SWIZZLE_Y, SWIZZLE_X>().IsClose(inScale, ScaleHelpers::cScaleToleranceSq); }
+
 	/// Scale the convex radius of an object
 	inline float			ScaleConvexRadius(float inConvexRadius, Vec3Arg inScale)		{ return min(inConvexRadius * inScale.Abs().ReduceMin(), cDefaultConvexRadius); }
 
@@ -38,6 +41,9 @@ namespace ScaleHelpers
 	/// Get the average scale if inScale, used to make the scale uniform when a shape doesn't support non-uniform scale
 	inline Vec3				MakeUniformScale(Vec3Arg inScale)								{ return Vec3::sReplicate((inScale.GetX() + inScale.GetY() + inScale.GetZ()) / 3.0f); }
 
+	/// Average the scale in XZ, used to make the scale uniform when a shape doesn't support non-uniform scale in the XZ plane
+	inline Vec3				MakeUniformScaleXZ(Vec3Arg inScale)								{ return 0.5f * (inScale + inScale.Swizzle<SWIZZLE_Z, SWIZZLE_Y, SWIZZLE_X>()); }
+
 	/// Checks in scale can be rotated to child shape
 	/// @param inRotation Rotation of child shape
 	/// @param inScale Scale in local space of parent shape

+ 4 - 3
Jolt/Physics/Collision/Shape/Shape.h

@@ -119,11 +119,12 @@ enum class EShapeSubType : uint8
 
 	// Other shapes
 	Plane,
+	TaperedCylinder,
 };
 
 // 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, 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 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, EShapeSubType::TaperedCylinder };
+static constexpr EShapeSubType sConvexSubShapeTypes[] = { EShapeSubType::Sphere, EShapeSubType::Box, EShapeSubType::Triangle, EShapeSubType::Capsule, EShapeSubType::TaperedCapsule, EShapeSubType::Cylinder, EShapeSubType::ConvexHull, EShapeSubType::TaperedCylinder, 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 sDecoratorSubShapeTypes[] = { EShapeSubType::RotatedTranslated, EShapeSubType::Scaled, EShapeSubType::OffsetCenterOfMass };
 
@@ -131,7 +132,7 @@ static constexpr EShapeSubType sDecoratorSubShapeTypes[] = { EShapeSubType::Rota
 static constexpr uint NumSubShapeTypes = uint(size(sAllSubShapeTypes));
 
 /// 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", "Plane" };
+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", "TaperedCylinder" };
 static_assert(size(sSubShapeTypeNames) == NumSubShapeTypes);
 
 /// Class that can construct shapes and that is serializable using the ObjectStream system.

+ 679 - 0
Jolt/Physics/Collision/Shape/TaperedCylinderShape.cpp

@@ -0,0 +1,679 @@
+// 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/TaperedCylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/ScaleHelpers.h>
+#include <Jolt/Physics/SoftBody/SoftBodyVertex.h>
+#include <Jolt/ObjectStream/TypeDeclarations.h>
+#include <Jolt/Core/StreamIn.h>
+#include <Jolt/Core/StreamOut.h>
+#ifdef JPH_DEBUG_RENDERER
+	#include <Jolt/Renderer/DebugRenderer.h>
+#endif // JPH_DEBUG_RENDERER
+
+JPH_NAMESPACE_BEGIN
+
+// Approximation of a face of the tapered capsule
+static const Vec3 cTaperedCapsuleFace[] =
+{
+	Vec3(0.0f,			0.0f,	1.0f),
+	Vec3(0.7071067f,	0.0f,	0.7071067f),
+	Vec3(1.0f,			0.0f,	0.0f),
+	Vec3(0.7071067f,	0.0f,	-0.7071067f),
+	Vec3(-0.0f,			0.0f,	-1.0f),
+	Vec3(-0.7071067f,	0.0f,	-0.7071067f),
+	Vec3(-1.0f,			0.0f,	0.0f),
+	Vec3(-0.7071067f,	0.0f,	0.7071067f)
+};
+
+JPH_IMPLEMENT_SERIALIZABLE_VIRTUAL(TaperedCylinderShapeSettings)
+{
+	JPH_ADD_BASE_CLASS(TaperedCylinderShapeSettings, ConvexShapeSettings)
+
+	JPH_ADD_ATTRIBUTE(TaperedCylinderShapeSettings, mHalfHeight)
+	JPH_ADD_ATTRIBUTE(TaperedCylinderShapeSettings, mTopRadius)
+	JPH_ADD_ATTRIBUTE(TaperedCylinderShapeSettings, mBottomRadius)
+	JPH_ADD_ATTRIBUTE(TaperedCylinderShapeSettings, mConvexRadius)
+}
+
+ShapeSettings::ShapeResult TaperedCylinderShapeSettings::Create() const
+{
+	if (mCachedResult.IsEmpty())
+	{
+		Ref<Shape> shape;
+		if (mTopRadius == mBottomRadius)
+		{
+			// Convert to regular cylinder
+			CylinderShapeSettings settings;
+			settings.mHalfHeight = mHalfHeight;
+			settings.mRadius = mTopRadius;
+			settings.mMaterial = mMaterial;
+			settings.mConvexRadius = mConvexRadius;
+			new CylinderShape(settings, mCachedResult);
+		}
+		else
+		{
+			// Normal tapered cylinder shape
+			new TaperedCylinderShape(*this, mCachedResult);
+		}
+	}
+	return mCachedResult;
+}
+
+TaperedCylinderShapeSettings::TaperedCylinderShapeSettings(float inHalfHeightOfTaperedCylinder, float inTopRadius, float inBottomRadius, float inConvexRadius, const PhysicsMaterial *inMaterial) :
+	ConvexShapeSettings(inMaterial),
+	mHalfHeight(inHalfHeightOfTaperedCylinder),
+	mTopRadius(inTopRadius),
+	mBottomRadius(inBottomRadius),
+	mConvexRadius(inConvexRadius)
+{
+}
+
+TaperedCylinderShape::TaperedCylinderShape(const TaperedCylinderShapeSettings &inSettings, ShapeResult &outResult) :
+	ConvexShape(EShapeSubType::TaperedCylinder, inSettings, outResult),
+	mTopRadius(inSettings.mTopRadius),
+	mBottomRadius(inSettings.mBottomRadius),
+	mConvexRadius(inSettings.mConvexRadius)
+{
+	if (mTopRadius < 0.0f)
+	{
+		outResult.SetError("Invalid top radius");
+		return;
+	}
+
+	if (mBottomRadius < 0.0f)
+	{
+		outResult.SetError("Invalid bottom radius");
+		return;
+	}
+
+	if (inSettings.mHalfHeight <= 0.0f)
+	{
+		outResult.SetError("Invalid height");
+		return;
+	}
+
+	if (inSettings.mConvexRadius < 0.0f)
+	{
+		outResult.SetError("Invalid convex radius");
+		return;
+	}
+
+	if (inSettings.mTopRadius < inSettings.mConvexRadius)
+	{
+		outResult.SetError("Convex radius must be smaller than convex radius");
+		return;
+	}
+
+	if (inSettings.mBottomRadius < inSettings.mConvexRadius)
+	{
+		outResult.SetError("Convex radius must be smaller than bottom radius");
+		return;
+	}
+
+	// Calculate the center of mass (using wxMaxima).
+	// Radius of cross section for tapered cylinder from 0 to h:
+	// r(x):=br+x*(tr-br)/h;
+	// Area:
+	// area(x):=%pi*r(x)^2;
+	// Total volume of cylinder:
+	// volume(h):=integrate(area(x),x,0,h);
+	// Center of mass:
+	// com(br,tr,h):=integrate(x*area(x),x,0,h)/volume(h);
+	// Results:
+	// ratsimp(com(br,tr,h),br,bt);
+	// Non-tapered cylinder should have com = 0.5:
+	// ratsimp(com(r,r,h));
+	// Cone with tip at origin and height h should have com = 3/4 h
+	// ratsimp(com(0,r,h));
+	float h = 2.0f * inSettings.mHalfHeight;
+	float tr = mTopRadius;
+	float tr2 = Square(tr);
+	float br = mBottomRadius;
+	float br2 = Square(br);
+	float com = h * (3 * tr2 + 2 * br * tr + br2) / (4.0f * (tr2 + br * tr + br2));
+	mTop = h - com;
+	mBottom = -com;
+
+	outResult.Set(this);
+}
+
+class TaperedCylinderShape::TaperedCylinder final : public Support
+{
+public:
+					TaperedCylinder(float inTop, float inBottom, float inTopRadius, float inBottomRadius, float inConvexRadius) :
+		mTop(inTop),
+		mBottom(inBottom),
+		mTopRadius(inTopRadius),
+		mBottomRadius(inBottomRadius),
+		mConvexRadius(inConvexRadius)
+	{
+		static_assert(sizeof(TaperedCylinder) <= sizeof(SupportBuffer), "Buffer size too small");
+		JPH_ASSERT(IsAligned(this, alignof(TaperedCylinder)));
+	}
+
+	virtual Vec3	GetSupport(Vec3Arg inDirection) const override
+	{
+		float x = inDirection.GetX(), y = inDirection.GetY(), z = inDirection.GetZ();
+		float o = sqrt(Square(x) + Square(z));
+		if (o > 0.0f)
+		{
+			Vec3 top_support((mTopRadius * x) / o, mTop, (mTopRadius * z) / o);
+			Vec3 bottom_support((mBottomRadius * x) / o, mBottom, (mBottomRadius * z) / o);
+			return inDirection.Dot(top_support) > inDirection.Dot(bottom_support)? top_support : bottom_support;
+		}
+		else
+		{
+			if (y > 0.0f)
+				return Vec3(0, mTop, 0);
+			else
+				return Vec3(0, mBottom, 0);
+		}
+	}
+
+	virtual float	GetConvexRadius() const override
+	{
+		return mConvexRadius;
+	}
+
+private:
+	float			mTop;
+	float			mBottom;
+	float			mTopRadius;
+	float			mBottomRadius;
+	float			mConvexRadius;
+};
+
+JPH_INLINE void TaperedCylinderShape::GetScaled(Vec3Arg inScale, float &outTop, float &outBottom, float &outTopRadius, float &outBottomRadius, float &outConvexRadius) const
+{
+	Vec3 abs_scale = inScale.Abs();
+	float scale_xz = abs_scale.GetX();
+	float scale_y = inScale.GetY();
+
+	outTop = scale_y * mTop;
+	outBottom = scale_y * mBottom;
+	outTopRadius = scale_xz * mTopRadius;
+	outBottomRadius = scale_xz * mBottomRadius;
+	outConvexRadius = min(abs_scale.GetY(), scale_xz) * mConvexRadius;
+
+	// Negative Y-scale flips the top and bottom
+	if (outBottom > outTop)
+	{
+		swap(outTop, outBottom);
+		swap(outTopRadius, outBottomRadius);
+	}
+}
+
+const ConvexShape::Support *TaperedCylinderShape::GetSupportFunction(ESupportMode inMode, SupportBuffer &inBuffer, Vec3Arg inScale) const
+{
+	JPH_ASSERT(IsValidScale(inScale));
+
+	// Get scaled tapered cylinder
+	float top, bottom, top_radius, bottom_radius, convex_radius;
+	GetScaled(inScale, top, bottom, top_radius, bottom_radius, convex_radius);
+
+	switch (inMode)
+	{
+	case ESupportMode::IncludeConvexRadius:
+	case ESupportMode::Default:
+		return new (&inBuffer) TaperedCylinder(top, bottom, top_radius, bottom_radius, 0.0f);
+
+	case ESupportMode::ExcludeConvexRadius:
+		return new (&inBuffer) TaperedCylinder(top - convex_radius, bottom + convex_radius, top_radius - convex_radius, bottom_radius - convex_radius, convex_radius);
+	}
+
+	JPH_ASSERT(false);
+	return nullptr;
+}
+
+JPH_INLINE static Vec3 sCalculateSideNormalXZ(Vec3Arg inSurfacePosition)
+{
+	return (Vec3(1, 0, 1) * inSurfacePosition).NormalizedOr(Vec3::sAxisX());
+}
+
+JPH_INLINE static Vec3 sCalculateSideNormal(Vec3Arg inNormalXZ, float inTop, float inBottom, float inTopRadius, float inBottomRadius)
+{
+	float tan_alpha = (inBottomRadius - inTopRadius) / (inTop - inBottom);
+	return Vec3(inNormalXZ.GetX(), tan_alpha, inNormalXZ.GetZ()).Normalized();
+}
+
+void TaperedCylinderShape::GetSupportingFace(const SubShapeID &inSubShapeID, Vec3Arg inDirection, Vec3Arg inScale, Mat44Arg inCenterOfMassTransform, SupportingFace &outVertices) const
+{
+	JPH_ASSERT(inSubShapeID.IsEmpty(), "Invalid subshape ID");
+	JPH_ASSERT(IsValidScale(inScale));
+
+	// Get scaled tapered cylinder
+	float top, bottom, top_radius, bottom_radius, convex_radius;
+	GetScaled(inScale, top, bottom, top_radius, bottom_radius, convex_radius);
+
+	// Get the normal of the side of the cylinder
+	Vec3 normal_xz = sCalculateSideNormalXZ(-inDirection);
+	Vec3 normal = sCalculateSideNormal(normal_xz, top, bottom, top_radius, bottom_radius);
+
+	constexpr float cMinRadius = 1.0e-3f;
+
+	// Check if the normal is closer to the side than to the top or bottom
+	if (abs(normal.Dot(inDirection)) > abs(inDirection.GetY()))
+	{
+		// Return the side of the cylinder
+		outVertices.push_back(inCenterOfMassTransform * (normal_xz * top_radius + Vec3(0, top, 0)));
+		outVertices.push_back(inCenterOfMassTransform * (normal_xz * bottom_radius + Vec3(0, bottom, 0)));
+	}
+	else if (inDirection.GetY() < 0.0f)
+	{
+		// Top of the cylinder
+		if (top_radius > cMinRadius)
+		{
+			Vec3 top_3d(0, top, 0);
+			for (Vec3 v : cTaperedCapsuleFace)
+				outVertices.push_back(inCenterOfMassTransform * (top_radius * v + top_3d));
+		}
+	}
+	else
+	{
+		// Bottom of the cylinder
+		if (bottom_radius > cMinRadius)
+		{
+			Vec3 bottom_3d(0, bottom, 0);
+			for (const Vec3 *v = cTaperedCapsuleFace + std::size(cTaperedCapsuleFace) - 1; v >= cTaperedCapsuleFace; --v)
+				outVertices.push_back(inCenterOfMassTransform * (bottom_radius * *v + bottom_3d));
+		}
+	}
+}
+
+MassProperties TaperedCylinderShape::GetMassProperties() const
+{
+	MassProperties p;
+
+	// Calculate mass
+	float density = GetDensity();
+	p.mMass = GetVolume() * density;
+
+	// Calculate inertia of a tapered cylinder (using wxMaxima)
+	// Radius:
+	// r(x):=br+(x-b)*(tr-br)/(t-b);
+	// Where t=top, b=bottom, tr=top radius, br=bottom radius
+	// Area of the cross section of the cylinder at x:
+	// area(x):=%pi*r(x)^2;
+	// Inertia x slice at x (using inertia of a solid disc, see https://en.wikipedia.org/wiki/List_of_moments_of_inertia, note needs to be multiplied by density):
+	// dix(x):=area(x)*r(x)^2/4;
+	// Inertia y slice at y (note needs to be multiplied by density)
+	// diy(x):=area(x)*r(x)^2/2;
+	// Volume:
+	// volume(b,t):=integrate(area(x),x,b,t);
+	// The constant density (note that we have this through GetDensity() so we'll use that instead):
+	// density(b,t):=m/volume(b,t);
+	// Inertia tensor element xx, note that we use the parallel axis theorem to move the inertia: Ixx' = Ixx + m translation^2, also note we multiply by density here:
+	// Ixx(br,tr,b,t):=integrate(dix(x)+area(x)*x^2,x,b,t)*density(b,t);
+	// Inertia tensor element yy:
+	// Iyy(br,tr,b,t):=integrate(diy(x),x,b,t)*density(b,t);
+	// Note that we can simplfy Ixx by using:
+	// Ixx_delta(br,tr,b,t):=Ixx(br,tr,b,t)-Iyy(br,tr,b,t)/2;
+	// For a cylinder this formula matches what is listed on the wiki:
+	// factor(Ixx(r,r,-h/2,h/2));
+	// factor(Iyy(r,r,-h/2,h/2));
+	// For a cone with tip at origin too:
+	// factor(Ixx(0,r,0,h));
+	// factor(Iyy(0,r,0,h));
+	// Now for the tapered cylinder:
+	// rat(Ixx(br,tr,b,t),br,bt);
+	// rat(Iyy(br,tr,b,t),br,bt);
+	// rat(Ixx_delta(br,tr,b,t),br,bt);
+	float t = mTop;
+	float t2 = Square(t);
+	float t3 = t * t2;
+
+	float b = mBottom;
+	float b2 = Square(b);
+	float b3 = b * b2;
+
+	float br = mBottomRadius;
+	float br2 = Square(br);
+	float br3 = br * br2;
+	float br4 = Square(br2);
+
+	float tr = mTopRadius;
+	float tr2 = Square(tr);
+	float tr3 = tr * tr2;
+	float tr4 = Square(tr2);
+
+	float inertia_y = (JPH_PI / 10.0f) * density * (t - b) * (br4 + tr * br3 + tr2 * br2 + tr3 * br + tr4);
+	float inertia_x_delta = (JPH_PI / 30.0f) * density * ((t3 + 2 * b * t2 + 3 * b2 * t - 6 * b3) * br2 + (3 * t3 + b * t2 - b2 * t - 3 * b3) * tr * br + (6 * t3 - 3 * b * t2 - 2 * b2 * t - b3) * tr2);
+	float inertia_x = inertia_x_delta + inertia_y / 2;
+	float inertia_z = inertia_x;
+	p.mInertia = Mat44::sScale(Vec3(inertia_x, inertia_y, inertia_z));
+	return p;
+}
+
+Vec3 TaperedCylinderShape::GetSurfaceNormal(const SubShapeID &inSubShapeID, Vec3Arg inLocalSurfacePosition) const
+{
+	JPH_ASSERT(inSubShapeID.IsEmpty(), "Invalid subshape ID");
+
+	constexpr float cEpsilon = 1.0e-5f;
+
+	if (inLocalSurfacePosition.GetY() > mTop - cEpsilon)
+		return Vec3(0, 1, 0);
+	else if (inLocalSurfacePosition.GetY() < mBottom + cEpsilon)
+		return Vec3(0, -1, 0);
+	else
+		return sCalculateSideNormal(sCalculateSideNormalXZ(inLocalSurfacePosition), mTop, mBottom, mTopRadius, mBottomRadius);
+}
+
+AABox TaperedCylinderShape::GetLocalBounds() const
+{
+	float max_radius = max(mTopRadius, mBottomRadius);
+	return AABox(Vec3(-max_radius, mBottom, -max_radius), Vec3(max_radius, mTop, max_radius));
+}
+
+void TaperedCylinderShape::CollideSoftBodyVertices(Mat44Arg inCenterOfMassTransform, Vec3Arg inScale, SoftBodyVertex *ioVertices, uint inNumVertices, [[maybe_unused]] float inDeltaTime, [[maybe_unused]] Vec3Arg inDisplacementDueToGravity, int inCollidingShapeIndex) const
+{
+	JPH_ASSERT(IsValidScale(inScale));
+
+	Mat44 inverse_transform = inCenterOfMassTransform.InversedRotationTranslation();
+
+	// Get scaled tapered cylinder
+	float top, bottom, top_radius, bottom_radius, convex_radius;
+	GetScaled(inScale, top, bottom, top_radius, bottom_radius, convex_radius);
+	Vec3 top_3d(0, top, 0);
+	Vec3 bottom_3d(0, bottom, 0);
+
+	for (SoftBodyVertex *v = ioVertices, *sbv_end = ioVertices + inNumVertices; v < sbv_end; ++v)
+		if (v->mInvMass > 0.0f)
+		{
+			Vec3 local_pos = inverse_transform * v->mPosition;
+
+			// Calculate penetration into side surface
+			Vec3 normal_xz = sCalculateSideNormalXZ(local_pos);
+			Vec3 side_normal = sCalculateSideNormal(normal_xz, top, bottom, top_radius, bottom_radius);
+			Vec3 side_support_top = normal_xz * top_radius + top_3d;
+			float side_penetration = (side_support_top - local_pos).Dot(side_normal);
+
+			// Calculate penetration into top and bottom plane
+			float top_penetration = top - local_pos.GetY();
+			float bottom_penetration = local_pos.GetY() - bottom;
+			float min_top_bottom_penetration = min(top_penetration, bottom_penetration);
+
+			Vec3 point, normal;
+			if (side_penetration < 0.0f || min_top_bottom_penetration < 0.0f)
+			{
+				// We're outside the cylinder
+				// Calculate the closest point on the line segment from bottom to top support point:
+				// closest_point = bottom + fraction * (top - bottom) / |top - bottom|^2
+				Vec3 side_support_bottom = normal_xz * bottom_radius + bottom_3d;
+				Vec3 bottom_to_top = side_support_top - side_support_bottom;
+				float fraction = (local_pos - side_support_bottom).Dot(bottom_to_top);
+
+				// Calculate the distance to the axis of the cylinder
+				float distance_to_axis = normal_xz.Dot(local_pos);
+				bool inside_top_radius = distance_to_axis <= top_radius;
+				bool inside_bottom_radius = distance_to_axis <= bottom_radius;
+
+				/*
+					Regions of tapered cylinder (side view):
+
+						_  B |       |
+						 --_ |   A   |
+							 t-------+
+					   C    /         \
+						   /  tapered  \
+					_     /  cylinder   \
+					 --_ /               \
+						b-----------------+
+					 D  |        E        |
+						|                 |
+
+					t = side_support_top, b = side_support_bottom
+					Lines between B and C and C and D are at a 90 degree angle to the line between t and b
+				*/
+				if (fraction >= bottom_to_top.LengthSq() // Region B: Above the line segment
+					&& !inside_top_radius) // Outside the top radius
+				{
+					// Top support point is closest
+					point = side_support_top;
+					normal = (local_pos - point).NormalizedOr(Vec3::sAxisY());
+				}
+				else if (fraction < 0.0f // Region D: Below the line segment
+					&& !inside_bottom_radius) // Outside the bottom radius
+				{
+					// Bottom support point is closest
+					point = side_support_bottom;
+					normal = (local_pos - point).NormalizedOr(Vec3::sAxisY());
+				}
+				else if (top_penetration < 0.0f // Region A: Above the top plane
+					&& inside_top_radius) // Inside the top radius
+				{
+					// Top plane is closest
+					point = top_3d;
+					normal = Vec3(0, 1, 0);
+				}
+				else if (bottom_penetration < 0.0f // Region E: Below the bottom plane
+					&& inside_bottom_radius) // Inside the bottom radius
+				{
+					// Bottom plane is closest
+					point = bottom_3d;
+					normal = Vec3(0, -1, 0);
+				}
+				else // Region C
+				{
+					// Side surface is closest
+					point = side_support_top;
+					normal = side_normal;
+				}
+			}
+			else if (side_penetration < min_top_bottom_penetration)
+			{
+				// Side surface is closest
+				point = side_support_top;
+				normal = side_normal;
+			}
+			else if (top_penetration < bottom_penetration)
+			{
+				// Top plane is closest
+				point = top_3d;
+				normal = Vec3(0, 1, 0);
+			}
+			else
+			{
+				// Bottom plane is closest
+				point = bottom_3d;
+				normal = Vec3(0, -1, 0);
+			}
+
+			// Calculate penetration
+			Plane plane = Plane::sFromPointAndNormal(point, normal);
+			float penetration = -plane.SignedDistance(local_pos);
+			if (penetration > v->mLargestPenetration)
+			{
+				v->mLargestPenetration = penetration;
+
+				// Store collision
+				v->mCollisionPlane = plane.GetTransformed(inCenterOfMassTransform);
+				v->mCollidingShapeIndex = inCollidingShapeIndex;
+			}
+		}
+}
+
+class TaperedCylinderShape::TCSGetTrianglesContext
+{
+public:
+	explicit	TCSGetTrianglesContext(Mat44Arg inTransform) : mTransform(inTransform) { }
+
+	Mat44		mTransform;
+	uint		mProcessed = 0; // Which elements we processed, bit 0 = top, bit 1 = bottom, bit 2 = side
+};
+
+void TaperedCylinderShape::GetTrianglesStart(GetTrianglesContext &ioContext, const AABox &inBox, Vec3Arg inPositionCOM, QuatArg inRotation, Vec3Arg inScale) const
+{
+	static_assert(sizeof(TCSGetTrianglesContext) <= sizeof(GetTrianglesContext), "GetTrianglesContext too small");
+	JPH_ASSERT(IsAligned(&ioContext, alignof(TCSGetTrianglesContext)));
+
+	// Make sure the scale is not inside out
+	Vec3 scale = ScaleHelpers::IsInsideOut(inScale)? Vec3(-1, 1, 1) * inScale : inScale;
+
+	// Mark top and bottom processed if their radius is too small
+	TCSGetTrianglesContext *context = new (&ioContext) TCSGetTrianglesContext(Mat44::sRotationTranslation(inRotation, inPositionCOM) * Mat44::sScale(scale));
+	constexpr float cMinRadius = 1.0e-3f;
+	if (mTopRadius < cMinRadius)
+		context->mProcessed |= 0b001;
+	if (mBottomRadius < cMinRadius)
+		context->mProcessed |= 0b010;
+}
+
+int TaperedCylinderShape::GetTrianglesNext(GetTrianglesContext &ioContext, int inMaxTrianglesRequested, Float3 *outTriangleVertices, const PhysicsMaterial **outMaterials) const
+{
+	constexpr int cNumVertices = int(std::size(cTaperedCapsuleFace));
+
+	static_assert(cGetTrianglesMinTrianglesRequested >= 2 * cNumVertices);
+	JPH_ASSERT(inMaxTrianglesRequested >= cGetTrianglesMinTrianglesRequested);
+
+	TCSGetTrianglesContext &context = (TCSGetTrianglesContext &)ioContext;
+
+	int total_num_triangles = 0;
+
+	// Top cap
+	Vec3 top_3d(0, mTop, 0);
+	if ((context.mProcessed & 0b001) == 0)
+	{
+		Vec3 v0 = context.mTransform * (top_3d + mTopRadius * cTaperedCapsuleFace[0]);
+		Vec3 v1 = context.mTransform * (top_3d + mTopRadius * cTaperedCapsuleFace[1]);
+
+		for (const Vec3 *v = cTaperedCapsuleFace + 2, *v_end = cTaperedCapsuleFace + cNumVertices; v < v_end; ++v)
+		{
+			Vec3 v2 = context.mTransform * (top_3d + mTopRadius * *v);
+
+			v0.StoreFloat3(outTriangleVertices++);
+			v1.StoreFloat3(outTriangleVertices++);
+			v2.StoreFloat3(outTriangleVertices++);
+
+			v1 = v2;
+		}
+
+		total_num_triangles = cNumVertices - 2;
+		context.mProcessed |= 0b001;
+	}
+
+	// Bottom cap
+	Vec3 bottom_3d(0, mBottom, 0);
+	if ((context.mProcessed & 0b010) == 0
+		&& total_num_triangles + cNumVertices - 2 < inMaxTrianglesRequested)
+	{
+		Vec3 v0 = context.mTransform * (bottom_3d + mBottomRadius * cTaperedCapsuleFace[0]);
+		Vec3 v1 = context.mTransform * (bottom_3d + mBottomRadius * cTaperedCapsuleFace[1]);
+
+		for (const Vec3 *v = cTaperedCapsuleFace + 2, *v_end = cTaperedCapsuleFace + cNumVertices; v < v_end; ++v)
+		{
+			Vec3 v2 = context.mTransform * (bottom_3d + mBottomRadius * *v);
+
+			v0.StoreFloat3(outTriangleVertices++);
+			v2.StoreFloat3(outTriangleVertices++);
+			v1.StoreFloat3(outTriangleVertices++);
+
+			v1 = v2;
+		}
+
+		total_num_triangles += cNumVertices - 2;
+		context.mProcessed |= 0b010;
+	}
+
+	// Side
+	if ((context.mProcessed & 0b100) == 0
+		&& total_num_triangles + 2 * cNumVertices < inMaxTrianglesRequested)
+	{
+		Vec3 v0t = context.mTransform * (top_3d + mTopRadius * cTaperedCapsuleFace[cNumVertices - 1]);
+		Vec3 v0b = context.mTransform * (bottom_3d + mBottomRadius * cTaperedCapsuleFace[cNumVertices - 1]);
+
+		for (const Vec3 *v = cTaperedCapsuleFace, *v_end = cTaperedCapsuleFace + cNumVertices; v < v_end; ++v)
+		{
+			Vec3 v1t = context.mTransform * (top_3d + mTopRadius * *v);
+			v0t.StoreFloat3(outTriangleVertices++);
+			v0b.StoreFloat3(outTriangleVertices++);
+			v1t.StoreFloat3(outTriangleVertices++);
+
+			Vec3 v1b = context.mTransform * (bottom_3d + mBottomRadius * *v);
+			v1t.StoreFloat3(outTriangleVertices++);
+			v0b.StoreFloat3(outTriangleVertices++);
+			v1b.StoreFloat3(outTriangleVertices++);
+
+			v0t = v1t;
+			v0b = v1b;
+		}
+
+		total_num_triangles += 2 * cNumVertices;
+		context.mProcessed |= 0b100;
+	}
+
+	// Store materials
+	if (outMaterials != nullptr)
+	{
+		const PhysicsMaterial *material = GetMaterial();
+		for (const PhysicsMaterial **m = outMaterials, **m_end = outMaterials + total_num_triangles; m < m_end; ++m)
+			*m = material;
+	}
+
+	return total_num_triangles;
+}
+
+#ifdef JPH_DEBUG_RENDERER
+void TaperedCylinderShape::Draw(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform, Vec3Arg inScale, ColorArg inColor, bool inUseMaterialColors, bool inDrawWireframe) const
+{
+	// Preserve flip along y axis but make sure we're not inside out
+	Vec3 scale = ScaleHelpers::IsInsideOut(inScale)? Vec3(-1, 1, 1) * inScale : inScale;
+	RMat44 world_transform = inCenterOfMassTransform * Mat44::sScale(scale);
+
+	DebugRenderer::EDrawMode draw_mode = inDrawWireframe? DebugRenderer::EDrawMode::Wireframe : DebugRenderer::EDrawMode::Solid;
+	inRenderer->DrawTaperedCylinder(world_transform, mTop, mBottom, mTopRadius, mBottomRadius, inUseMaterialColors? GetMaterial()->GetDebugColor() : inColor, DebugRenderer::ECastShadow::On, draw_mode);
+}
+#endif // JPH_DEBUG_RENDERER
+
+void TaperedCylinderShape::SaveBinaryState(StreamOut &inStream) const
+{
+	ConvexShape::SaveBinaryState(inStream);
+
+	inStream.Write(mTop);
+	inStream.Write(mBottom);
+	inStream.Write(mTopRadius);
+	inStream.Write(mBottomRadius);
+	inStream.Write(mConvexRadius);
+}
+
+void TaperedCylinderShape::RestoreBinaryState(StreamIn &inStream)
+{
+	ConvexShape::RestoreBinaryState(inStream);
+
+	inStream.Read(mTop);
+	inStream.Read(mBottom);
+	inStream.Read(mTopRadius);
+	inStream.Read(mBottomRadius);
+	inStream.Read(mConvexRadius);
+}
+
+float TaperedCylinderShape::GetVolume() const
+{
+	// Volume of a tapered cylinder is: integrate(%pi*(b+x*(t-b)/h)^2,x,0,h) where t is the top radius, b is the bottom radius and h is the height
+	return (JPH_PI / 3.0f) * (mTop - mBottom) * (Square(mTopRadius) + mTopRadius * mBottomRadius + Square(mBottomRadius));
+}
+
+bool TaperedCylinderShape::IsValidScale(Vec3Arg inScale) const
+{
+	return ConvexShape::IsValidScale(inScale) && ScaleHelpers::IsUniformScaleXZ(inScale.Abs());
+}
+
+Vec3 TaperedCylinderShape::MakeScaleValid(Vec3Arg inScale) const
+{
+	Vec3 scale = ScaleHelpers::MakeNonZeroScale(inScale);
+
+	return scale.GetSign() * ScaleHelpers::MakeUniformScaleXZ(scale.Abs());
+}
+
+void TaperedCylinderShape::sRegister()
+{
+	ShapeFunctions &f = ShapeFunctions::sGet(EShapeSubType::TaperedCylinder);
+	f.mConstruct = []() -> Shape * { return new TaperedCylinderShape; };
+	f.mColor = Color::sGreen;
+}
+
+JPH_NAMESPACE_END

+ 116 - 0
Jolt/Physics/Collision/Shape/TaperedCylinderShape.h

@@ -0,0 +1,116 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Collision/Shape/ConvexShape.h>
+#include <Jolt/Physics/PhysicsSettings.h>
+
+JPH_NAMESPACE_BEGIN
+
+/// Class that constructs a TaperedCylinderShape
+class JPH_EXPORT TaperedCylinderShapeSettings final : public ConvexShapeSettings
+{
+	JPH_DECLARE_SERIALIZABLE_VIRTUAL(JPH_EXPORT, TaperedCylinderShapeSettings)
+
+	/// Default constructor for deserialization
+							TaperedCylinderShapeSettings() = default;
+
+	/// Create a tapered cylinder centered around the origin with bottom at (0, -inHalfHeightOfTaperedCylinder, 0) with radius inBottomRadius and top at (0, inHalfHeightOfTaperedCylinder, 0) with radius inTopRadius
+							TaperedCylinderShapeSettings(float inHalfHeightOfTaperedCylinder, float inTopRadius, float inBottomRadius, float inConvexRadius = cDefaultConvexRadius, const PhysicsMaterial *inMaterial = nullptr);
+
+	// See: ShapeSettings
+	virtual ShapeResult		Create() const override;
+
+	float					mHalfHeight = 0.0f;
+	float					mTopRadius = 0.0f;
+	float					mBottomRadius = 0.0f;
+	float					mConvexRadius = 0.0f;
+};
+
+/// A cylinder with different top and bottom radii
+class JPH_EXPORT TaperedCylinderShape final : public ConvexShape
+{
+public:
+	JPH_OVERRIDE_NEW_DELETE
+
+	/// Constructor
+							TaperedCylinderShape() : ConvexShape(EShapeSubType::TaperedCylinder) { }
+							TaperedCylinderShape(const TaperedCylinderShapeSettings &inSettings, ShapeResult &outResult);
+
+	// See Shape::GetCenterOfMass
+	virtual Vec3			GetCenterOfMass() const override										{ return Vec3(0, -0.5f * (mTop + mBottom), 0); }
+
+	// See Shape::GetLocalBounds
+	virtual AABox			GetLocalBounds() const override;
+
+	// See Shape::GetInnerRadius
+	virtual float			GetInnerRadius() const override											{ return min(mTopRadius, mBottomRadius); }
+
+	// See Shape::GetMassProperties
+	virtual MassProperties	GetMassProperties() const override;
+
+	// See Shape::GetSurfaceNormal
+	virtual Vec3			GetSurfaceNormal(const SubShapeID &inSubShapeID, Vec3Arg inLocalSurfacePosition) const override;
+
+	// See Shape::GetSupportingFace
+	virtual void			GetSupportingFace(const SubShapeID &inSubShapeID, Vec3Arg inDirection, Vec3Arg inScale, Mat44Arg inCenterOfMassTransform, SupportingFace &outVertices) const override;
+
+	// See ConvexShape::GetSupportFunction
+	virtual const Support *	GetSupportFunction(ESupportMode inMode, SupportBuffer &inBuffer, Vec3Arg inScale) 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;
+
+#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
+	virtual void			SaveBinaryState(StreamOut &inStream) const override;
+
+	// See Shape::GetStats
+	virtual Stats			GetStats() const override												{ return Stats(sizeof(*this), 0); }
+
+	// See Shape::GetVolume
+	virtual float			GetVolume() const override;
+
+	// See Shape::IsValidScale
+	virtual bool			IsValidScale(Vec3Arg inScale) const override;
+
+	// See Shape::MakeScaleValid
+	virtual Vec3			MakeScaleValid(Vec3Arg inScale) const override;
+
+	// Register shape functions with the registry
+	static void				sRegister();
+
+protected:
+	// See: Shape::RestoreBinaryState
+	virtual void			RestoreBinaryState(StreamIn &inStream) override;
+
+private:
+	// Class for GetSupportFunction
+	class					TaperedCylinder;
+
+	// Class for GetTrianglesTart
+	class					TCSGetTrianglesContext;
+
+	// Scale the cylinder
+	JPH_INLINE void			GetScaled(Vec3Arg inScale, float &outTop, float &outBottom, float &outTopRadius, float &outBottomRadius, float &outConvexRadius) const;
+
+	float					mTop = 0.0f;
+	float					mBottom = 0.0f;
+	float					mTopRadius = 0.0f;
+	float					mBottomRadius = 0.0f;
+	float					mConvexRadius = 0.0f;
+};
+
+JPH_NAMESPACE_END

+ 4 - 0
Jolt/RegisterTypes.cpp

@@ -16,6 +16,7 @@
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/ScaledShape.h>
 #include <Jolt/Physics/Collision/Shape/MeshShape.h>
 #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
@@ -39,6 +40,7 @@ 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, TaperedCapsuleShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, CylinderShapeSettings)
+JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, TaperedCylinderShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, ScaledShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, MeshShapeSettings)
 JPH_DECLARE_RTTI_WITH_NAMESPACE_FOR_FACTORY(JPH_EXPORT, JPH, ConvexHullShapeSettings)
@@ -124,6 +126,7 @@ void RegisterTypesInternal(uint64 inVersionID)
 	CapsuleShape::sRegister();
 	TaperedCapsuleShape::sRegister();
 	CylinderShape::sRegister();
+	TaperedCylinderShape::sRegister();
 	MeshShape::sRegister();
 	ConvexHullShape::sRegister();
 	HeightFieldShape::sRegister();
@@ -148,6 +151,7 @@ void RegisterTypesInternal(uint64 inVersionID)
 		JPH_RTTI(CapsuleShapeSettings),
 		JPH_RTTI(TaperedCapsuleShapeSettings),
 		JPH_RTTI(CylinderShapeSettings),
+		JPH_RTTI(TaperedCylinderShapeSettings),
 		JPH_RTTI(ScaledShapeSettings),
 		JPH_RTTI(MeshShapeSettings),
 		JPH_RTTI(ConvexHullShapeSettings),

+ 94 - 58
Jolt/Renderer/DebugRenderer.cpp

@@ -355,6 +355,73 @@ void DebugRenderer::Create8thSphere(Array<uint32> &ioIndices, Array<Vertex> &ioV
 	Create8thSphereRecursive(ioIndices, ioVertices, inDir1, idx1, inDir2, idx2, inDir3, idx3, inUV, inGetSupport, inLevel);
 }
 
+DebugRenderer::Batch DebugRenderer::CreateCylinder(float inTop, float inBottom, float inTopRadius, float inBottomRadius, int inLevel)
+{
+	Array<Vertex> cylinder_vertices;
+	Array<uint32> cylinder_indices;
+
+	for (int q = 0; q < 4; ++q)
+	{
+		Float2 uv = (q & 1) == 0? Float2(0.25f, 0.75f) : Float2(0.25f, 0.25f);
+
+		uint32 center_start_idx = (uint32)cylinder_vertices.size();
+
+		Float3 nt(0.0f, 1.0f, 0.0f);
+		Float3 nb(0.0f, -1.0f, 0.0f);
+		cylinder_vertices.push_back({ Float3(0.0f, inTop, 0.0f), nt, uv, Color::sWhite });
+		cylinder_vertices.push_back({ Float3(0.0f, inBottom, 0.0f), nb, uv, Color::sWhite });
+
+		uint32 vtx_start_idx = (uint32)cylinder_vertices.size();
+
+		int num_parts = 1 << inLevel;
+		for (int i = 0; i <= num_parts; ++i)
+		{
+			// Calculate top and bottom vertex
+			float angle = 0.5f * JPH_PI * (float(q) + float(i) / num_parts);
+			float s = Sin(angle);
+			float c = Cos(angle);
+			Float3 vt(inTopRadius * s, inTop, inTopRadius * c);
+			Float3 vb(inBottomRadius * s, inBottom, inBottomRadius * c);
+
+			// Calculate normal
+			Vec3 edge = Vec3(vt) - Vec3(vb);
+			Float3 n;
+			edge.Cross(Vec3(s, 0, c).Cross(edge)).Normalized().StoreFloat3(&n);
+
+			cylinder_vertices.push_back({ vt, nt, uv, Color::sWhite });
+			cylinder_vertices.push_back({ vb, nb, uv, Color::sWhite });
+			cylinder_vertices.push_back({ vt, n, uv, Color::sWhite });
+			cylinder_vertices.push_back({ vb, n, uv, Color::sWhite });
+		}
+
+		for (int i = 0; i < num_parts; ++i)
+		{
+			uint32 start = vtx_start_idx + 4 * i;
+
+			// Top
+			cylinder_indices.push_back(center_start_idx);
+			cylinder_indices.push_back(start);
+			cylinder_indices.push_back(start + 4);
+
+			// Bottom
+			cylinder_indices.push_back(center_start_idx + 1);
+			cylinder_indices.push_back(start + 5);
+			cylinder_indices.push_back(start + 1);
+
+			// Side
+			cylinder_indices.push_back(start + 2);
+			cylinder_indices.push_back(start + 3);
+			cylinder_indices.push_back(start + 7);
+
+			cylinder_indices.push_back(start + 2);
+			cylinder_indices.push_back(start + 7);
+			cylinder_indices.push_back(start + 6);
+		}
+	}
+
+	return CreateTriangleBatch(cylinder_vertices, cylinder_indices);
+}
+
 void DebugRenderer::CreateQuad(Array<uint32> &ioIndices, Array<Vertex> &ioVertices, Vec3Arg inV1, Vec3Arg inV2, Vec3Arg inV3, Vec3Arg inV4)
 {
 	// Make room
@@ -556,64 +623,7 @@ void DebugRenderer::Initialize()
 		}
 
 		// Cylinder
-		{
-			Array<Vertex> cylinder_vertices;
-			Array<uint32> cylinder_indices;
-			for (int q = 0; q < 4; ++q)
-			{
-				Float2 uv = (q & 1) == 0? Float2(0.25f, 0.75f) : Float2(0.25f, 0.25f);
-
-				uint32 center_start_idx = (uint32)cylinder_vertices.size();
-
-				Float3 nt(0.0f, 1.0f, 0.0f);
-				Float3 nb(0.0f, -1.0f, 0.0f);
-				cylinder_vertices.push_back({ Float3(0.0f, 1.0f, 0.0f), nt, uv, Color::sWhite });
-				cylinder_vertices.push_back({ Float3(0.0f, -1.0f, 0.0f), nb, uv, Color::sWhite });
-
-				uint32 vtx_start_idx = (uint32)cylinder_vertices.size();
-
-				int num_parts = 1 << level;
-				for (int i = 0; i <= num_parts; ++i)
-				{
-					float angle = 0.5f * JPH_PI * (float(q) + float(i) / num_parts);
-					float s = Sin(angle);
-					float c = Cos(angle);
-					Float3 vt(s, 1.0f, c);
-					Float3 vb(s, -1.0f, c);
-					Float3 n(s, 0, c);
-
-					cylinder_vertices.push_back({ vt, nt, uv, Color::sWhite });
-					cylinder_vertices.push_back({ vb, nb, uv, Color::sWhite });
-					cylinder_vertices.push_back({ vt, n, uv, Color::sWhite });
-					cylinder_vertices.push_back({ vb, n, uv, Color::sWhite });
-				}
-
-				for (int i = 0; i < num_parts; ++i)
-				{
-					uint32 start = vtx_start_idx + 4 * i;
-
-					// Top
-					cylinder_indices.push_back(center_start_idx);
-					cylinder_indices.push_back(start);
-					cylinder_indices.push_back(start + 4);
-
-					// Bottom
-					cylinder_indices.push_back(center_start_idx + 1);
-					cylinder_indices.push_back(start + 5);
-					cylinder_indices.push_back(start + 1);
-
-					// Side
-					cylinder_indices.push_back(start + 2);
-					cylinder_indices.push_back(start + 3);
-					cylinder_indices.push_back(start + 7);
-
-					cylinder_indices.push_back(start + 2);
-					cylinder_indices.push_back(start + 7);
-					cylinder_indices.push_back(start + 6);
-				}
-			}
-			mCylinder->mLODs.push_back({ CreateTriangleBatch(cylinder_vertices, cylinder_indices), distance });
-		}
+		mCylinder->mLODs.push_back({ CreateCylinder(1.0f, -1.0f, 1.0f, 1.0f, level), distance });
 	}
 }
 
@@ -1054,6 +1064,29 @@ void DebugRenderer::DrawPie(RVec3Arg inCenter, float inRadius, Vec3Arg inNormal,
 	DrawGeometry(matrix, inColor, geometry, ECullMode::Off, inCastShadow, inDrawMode);
 }
 
+void DebugRenderer::DrawTaperedCylinder(RMat44Arg inMatrix, float inTop, float inBottom, float inTopRadius, float inBottomRadius, ColorArg inColor, ECastShadow inCastShadow, EDrawMode inDrawMode)
+{
+	TaperedCylinder tapered_cylinder { inTop, inBottom, inTopRadius, inBottomRadius };
+
+	GeometryRef &geometry = mTaperedCylinders[tapered_cylinder];
+	if (geometry == nullptr)
+	{
+		TaperedCylinderBatces::iterator it = mPrevTaperedCylinders.find(tapered_cylinder);
+		if (it != mPrevTaperedCylinders.end())
+			geometry = it->second;
+	}
+	if (geometry == nullptr)
+	{
+		float max_radius = max(inTopRadius, inBottomRadius);
+		geometry = new Geometry(AABox(Vec3(-max_radius, inBottom, -max_radius), Vec3(max_radius, inTop, max_radius)));
+
+		for (int level = sMaxLevel; level >= 1; --level)
+			geometry->mLODs.push_back({ CreateCylinder(inTop, inBottom, inTopRadius, inBottomRadius, level), sLODDistanceForLevel[sMaxLevel - level] });
+	}
+
+	DrawGeometry(inMatrix, inColor, geometry, ECullMode::CullBackFace, inCastShadow, inDrawMode);
+}
+
 void DebugRenderer::NextFrame()
 {
 	mPrevSwingConeLimits.clear();
@@ -1064,6 +1097,9 @@ void DebugRenderer::NextFrame()
 
 	mPrevPieLimits.clear();
 	std::swap(mPieLimits, mPrevPieLimits);
+
+	mPrevTaperedCylinders.clear();
+	std::swap(mTaperedCylinders, mPrevTaperedCylinders);
 }
 
 JPH_NAMESPACE_END

+ 36 - 0
Jolt/Renderer/DebugRenderer.h

@@ -164,6 +164,17 @@ public:
 	/// @param inDrawMode determines if we draw the geometry solid or in wireframe.
 	void					DrawPie(RVec3Arg inCenter, float inRadius, Vec3Arg inNormal, Vec3Arg inAxis, float inMinAngle, float inMaxAngle, ColorArg inColor, ECastShadow inCastShadow = ECastShadow::On, EDrawMode inDrawMode = EDrawMode::Solid);
 
+	/// Draw a tapered cylinder
+	/// @param inMatrix Matrix that transforms the cylinder to world space.
+	/// @param inTop Top of cylinder (along Y axis)
+	/// @param inBottom Bottom of cylinder (along Y axis)
+	/// @param inTopRadius Radius at the top
+	/// @param inBottomRadius Radius at the bottom
+	/// @param inColor Color to use for drawing the pie.
+	/// @param inCastShadow determines if this geometry should cast a shadow or not.
+	/// @param inDrawMode determines if we draw the geometry solid or in wireframe.
+	void					DrawTaperedCylinder(RMat44Arg inMatrix, float inTop, float inBottom, float inTopRadius, float inBottomRadius, ColorArg inColor, ECastShadow inCastShadow = ECastShadow::On, EDrawMode inDrawMode = EDrawMode::Solid);
+
 	/// Singleton instance
 	static DebugRenderer *	sInstance;
 
@@ -287,6 +298,9 @@ private:
 	void					Create8thSphereRecursive(Array<uint32> &ioIndices, Array<Vertex> &ioVertices, Vec3Arg inDir1, uint32 &ioIdx1, Vec3Arg inDir2, uint32 &ioIdx2, Vec3Arg inDir3, uint32 &ioIdx3, const Float2 &inUV, SupportFunction inGetSupport, int inLevel);
 	void					Create8thSphere(Array<uint32> &ioIndices, Array<Vertex> &ioVertices, Vec3Arg inDir1, Vec3Arg inDir2, Vec3Arg inDir3, const Float2 &inUV, SupportFunction inGetSupport, int inLevel);
 
+	/// Helper functions to create a vertex and index buffer for a cylinder
+	Batch					CreateCylinder(float inTop, float inBottom, float inTopRadius, float inBottomRadius, int inLevel);
+
 	/// Helper function for DrawSwingConeLimits and DrawSwingPyramidLimits
 	Geometry *				CreateSwingLimitGeometry(int inNumSegments, const Vec3 *inVertices);
 
@@ -342,6 +356,28 @@ private:
 	using PieBatces = UnorderedMap<float, GeometryRef>;
 	PieBatces				mPieLimits;
 	PieBatces				mPrevPieLimits;
+
+	struct TaperedCylinder
+	{
+		bool				operator == (const TaperedCylinder &inRHS) const
+		{
+			return mTop == inRHS.mTop
+				&& mBottom == inRHS.mBottom
+				&& mTopRadius == inRHS.mTopRadius
+				&& mBottomRadius == inRHS.mBottomRadius;
+		}
+
+		float				mTop;
+		float				mBottom;
+		float				mTopRadius;
+		float				mBottomRadius;
+	};
+
+	JPH_MAKE_HASH_STRUCT(TaperedCylinder, TaperedCylinderHasher, t.mTop, t.mBottom, t.mTopRadius, t.mBottomRadius)
+
+	using TaperedCylinderBatces = UnorderedMap<TaperedCylinder, GeometryRef, TaperedCylinderHasher>;
+	TaperedCylinderBatces	mTaperedCylinders;
+	TaperedCylinderBatces	mPrevTaperedCylinders;
 };
 
 JPH_NAMESPACE_END

+ 4 - 0
Samples/Samples.cmake

@@ -215,6 +215,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledSphereShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTaperedCapsuleShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTaperedCapsuleShapeTest.h
+	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTaperedCylinderShapeTest.cpp
+	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTaperedCylinderShapeTest.h
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTriangleShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/ScaledShapes/ScaledTriangleShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/BoxShapeTest.cpp
@@ -247,6 +249,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/Shapes/RotatedTranslatedShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/TaperedCapsuleShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/TaperedCapsuleShapeTest.h
+	${SAMPLES_ROOT}/Tests/Shapes/TaperedCylinderShapeTest.cpp
+	${SAMPLES_ROOT}/Tests/Shapes/TaperedCylinderShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/TriangleShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/TriangleShapeTest.h
 	${SAMPLES_ROOT}/Tests/Vehicle/MotorcycleTest.cpp

+ 5 - 0
Samples/SamplesApp.cpp

@@ -32,6 +32,7 @@
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/TriangleShape.h>
 #include <Jolt/Physics/Collision/Shape/PlaneShape.h>
 #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
@@ -202,6 +203,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SphereShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, TaperedCapsuleShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, CapsuleShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, CylinderShapeTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, TaperedCylinderShapeTest)
 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, TriangleShapeTest)
@@ -221,6 +223,7 @@ static TestNameAndRTTI sShapeTests[] =
 	{ "Capsule Shape",						JPH_RTTI(CapsuleShapeTest) },
 	{ "Tapered Capsule Shape",				JPH_RTTI(TaperedCapsuleShapeTest) },
 	{ "Cylinder Shape",						JPH_RTTI(CylinderShapeTest) },
+	{ "Tapered Cylinder Shape",				JPH_RTTI(TaperedCylinderShapeTest) },
 	{ "Convex Hull Shape",					JPH_RTTI(ConvexHullShapeTest) },
 	{ "Mesh Shape",							JPH_RTTI(MeshShapeTest) },
 	{ "Mesh Shape Per Triangle User Data",	JPH_RTTI(MeshShapeUserDataTest) },
@@ -239,6 +242,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledBoxShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledCapsuleShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledTaperedCapsuleShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledCylinderShapeTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledTaperedCylinderShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledConvexHullShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledMeshShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ScaledHeightFieldShapeTest)
@@ -256,6 +260,7 @@ static TestNameAndRTTI sScaledShapeTests[] =
 	{ "Capsule Shape",						JPH_RTTI(ScaledCapsuleShapeTest) },
 	{ "Tapered Capsule Shape",				JPH_RTTI(ScaledTaperedCapsuleShapeTest) },
 	{ "Cylinder Shape",						JPH_RTTI(ScaledCylinderShapeTest) },
+	{ "Tapered Cylinder Shape",				JPH_RTTI(ScaledTaperedCylinderShapeTest) },
 	{ "Convex Hull Shape",					JPH_RTTI(ScaledConvexHullShapeTest) },
 	{ "Mesh Shape",							JPH_RTTI(ScaledMeshShapeTest) },
 	{ "Height Field Shape",					JPH_RTTI(ScaledHeightFieldShapeTest) },

+ 17 - 2
Samples/Tests/General/FunnelTest.cpp

@@ -11,6 +11,7 @@
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/ScaledShape.h>
 #include <Jolt/Physics/Body/BodyCreationSettings.h>
@@ -48,7 +49,7 @@ void FunnelTest::Initialize()
 	RefConst<Shape> shape;
 	for (int i = 0; i < 1000; ++i)
 	{
-		switch (random() % 8)
+		switch (random() % 10)
 		{
 		case 0:
 			{
@@ -98,6 +99,20 @@ void FunnelTest::Initialize()
 			}
 
 		case 6:
+			{
+				shape = TaperedCylinderShapeSettings(0.5f * feature_size(random), feature_size(random), feature_size(random)).Create().Get();
+				scale = scale.Swizzle<SWIZZLE_X, SWIZZLE_Y, SWIZZLE_X>(); // Scale X must be same as Z
+				break;
+			}
+
+		case 7:
+			{
+				shape = TaperedCylinderShapeSettings(0.5f * feature_size(random), 0.0f, feature_size(random), 0.0f).Create().Get();
+				scale = scale.Swizzle<SWIZZLE_X, SWIZZLE_Y, SWIZZLE_X>(); // Scale X must be same as Z
+				break;
+			}
+
+		case 8:
 			{
 				// Simple compound
 				StaticCompoundShapeSettings compound_shape_settings;
@@ -109,7 +124,7 @@ void FunnelTest::Initialize()
 				break;
 			}
 
-		case 7:
+		case 9:
 			{
 				// Compound with sub compound and rotation
 				Ref<StaticCompoundShapeSettings> sub_compound = new StaticCompoundShapeSettings();

+ 31 - 23
Samples/Tests/General/LoadSaveSceneTest.cpp

@@ -13,8 +13,9 @@
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/SphereShape.h>
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
-#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
+#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/MutableCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
@@ -127,6 +128,11 @@ static HeightFieldShapeSettings *sCreateHeightField()
 
 Ref<PhysicsScene> LoadSaveSceneTest::sCreateScene()
 {
+	int color = 0;
+	auto next_color = [&color]() { return Color::sGetDistinctColor(color++); };
+	RVec3 pos(0, cMaxHeight, 0);
+	auto next_pos = [&pos]() { pos += RVec3(0, 1.0f, 0); return pos; };
+
 	// Create scene
 	Ref<PhysicsScene> scene = new PhysicsScene();
 
@@ -137,22 +143,24 @@ Ref<PhysicsScene> LoadSaveSceneTest::sCreateScene()
 	scene->AddBody(BodyCreationSettings(sCreateHeightField(), RVec3(50, 0, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
 
 	// Some simple primitives
-	scene->AddBody(BodyCreationSettings(new TriangleShapeSettings(Vec3(-2, 0, 0), Vec3(0, 1, 0), Vec3(2, 0, 0), 0.0f, new PhysicsMaterialSimple("Triangle Material", Color::sGetDistinctColor(0))), RVec3(0, cMaxHeight, 0), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Static, Layers::NON_MOVING));
-	scene->AddBody(BodyCreationSettings(new SphereShapeSettings(0.2f, new PhysicsMaterialSimple("Sphere Material", Color::sGetDistinctColor(1))), RVec3(0, cMaxHeight + 1.0f, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
-	scene->AddBody(BodyCreationSettings(new BoxShapeSettings(Vec3(0.2f, 0.2f, 0.4f), 0.01f, new PhysicsMaterialSimple("Box Material", Color::sGetDistinctColor(2))), RVec3(0, cMaxHeight + 2.0f, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
-	scene->AddBody(BodyCreationSettings(new CapsuleShapeSettings(1.5f, 0.2f, new PhysicsMaterialSimple("Capsule Material", Color::sGetDistinctColor(3))), RVec3(0, cMaxHeight + 3.0f, 0), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
-	scene->AddBody(BodyCreationSettings(new TaperedCapsuleShapeSettings(0.5f, 0.1f, 0.2f, new PhysicsMaterialSimple("Tapered Capsule Material", Color::sGetDistinctColor(4))), RVec3(0, cMaxHeight + 4.0f, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
-	scene->AddBody(BodyCreationSettings(new CylinderShapeSettings(0.5f, 0.2f, cDefaultConvexRadius, new PhysicsMaterialSimple("Cylinder Material", Color::sGetDistinctColor(5))), RVec3(0, cMaxHeight + 5.0f, 0), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new TriangleShapeSettings(Vec3(-2, 0, 0), Vec3(0, 1, 0), Vec3(2, 0, 0), 0.0f, new PhysicsMaterialSimple("Triangle Material", next_color())), next_pos(), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Static, Layers::NON_MOVING));
+	scene->AddBody(BodyCreationSettings(new SphereShapeSettings(0.2f, new PhysicsMaterialSimple("Sphere Material", next_color())), next_pos(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new BoxShapeSettings(Vec3(0.2f, 0.2f, 0.4f), 0.01f, new PhysicsMaterialSimple("Box Material", next_color())), next_pos(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new CapsuleShapeSettings(1.5f, 0.2f, new PhysicsMaterialSimple("Capsule Material", next_color())), next_pos(), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new TaperedCapsuleShapeSettings(0.5f, 0.1f, 0.2f, new PhysicsMaterialSimple("Tapered Capsule Material", next_color())), next_pos(), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new CylinderShapeSettings(0.5f, 0.2f, cDefaultConvexRadius, new PhysicsMaterialSimple("Cylinder Material", next_color())), next_pos(), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new TaperedCylinderShapeSettings(0.5f, 0.2f, 0.4f, cDefaultConvexRadius, new PhysicsMaterialSimple("Tapered Cylinder Material", next_color())), next_pos(), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new TaperedCylinderShapeSettings(0.5f, 0.4f, 0.0f, 0.0f, new PhysicsMaterialSimple("Cone Material", next_color())), next_pos(), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
 
 	// Compound with sub compound and rotation
 	StaticCompoundShapeSettings *sub_compound = new StaticCompoundShapeSettings();
-	sub_compound->AddShape(Vec3(0, 0.5f, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new BoxShapeSettings(Vec3(0.5f, 0.1f, 0.2f), cDefaultConvexRadius, new PhysicsMaterialSimple("Compound Box Material", Color::sGetDistinctColor(6))));
-	sub_compound->AddShape(Vec3(0.5f, 0, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new CylinderShapeSettings(0.5f, 0.2f, cDefaultConvexRadius, new PhysicsMaterialSimple("Compound Cylinder Material", Color::sGetDistinctColor(7))));
-	sub_compound->AddShape(Vec3(0, 0, 0.5f), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), new TaperedCapsuleShapeSettings(0.5f, 0.1f, 0.2f, new PhysicsMaterialSimple("Compound Tapered Capsule Material", Color::sGetDistinctColor(8))));
+	sub_compound->AddShape(Vec3(0, 0.5f, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new BoxShapeSettings(Vec3(0.5f, 0.1f, 0.2f), cDefaultConvexRadius, new PhysicsMaterialSimple("Compound Box Material", next_color())));
+	sub_compound->AddShape(Vec3(0.5f, 0, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new CylinderShapeSettings(0.5f, 0.2f, cDefaultConvexRadius, new PhysicsMaterialSimple("Compound Cylinder Material", next_color())));
+	sub_compound->AddShape(Vec3(0, 0, 0.5f), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), new TaperedCapsuleShapeSettings(0.5f, 0.1f, 0.2f, new PhysicsMaterialSimple("Compound Tapered Capsule Material", next_color())));
 	StaticCompoundShapeSettings *compound_shape = new StaticCompoundShapeSettings();
 	compound_shape->AddShape(Vec3(0, 0, 0), Quat::sRotation(Vec3::sAxisX(), -0.25f * JPH_PI) * Quat::sRotation(Vec3::sAxisZ(), 0.25f * JPH_PI), sub_compound);
 	compound_shape->AddShape(Vec3(0, -0.1f, 0), Quat::sRotation(Vec3::sAxisX(), 0.25f * JPH_PI) * Quat::sRotation(Vec3::sAxisZ(), -0.75f * JPH_PI), sub_compound);
-	scene->AddBody(BodyCreationSettings(compound_shape, RVec3(0, cMaxHeight + 6.0f, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(compound_shape, next_pos(), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
 
 	// Convex hull shape
 	Array<Vec3> tetrahedron;
@@ -160,18 +168,18 @@ Ref<PhysicsScene> LoadSaveSceneTest::sCreateScene()
 	tetrahedron.push_back(Vec3(0, 0, 0.5f));
 	tetrahedron.push_back(Vec3(0.5f, 0, -0.5f));
 	tetrahedron.push_back(Vec3(0, -0.5f, 0));
-	Ref<ConvexHullShapeSettings> convex_hull = new ConvexHullShapeSettings(tetrahedron, cDefaultConvexRadius, new PhysicsMaterialSimple("Convex Hull Material", Color::sGetDistinctColor(9)));
-	scene->AddBody(BodyCreationSettings(convex_hull, RVec3(0, cMaxHeight + 7.0f, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
+	Ref<ConvexHullShapeSettings> convex_hull = new ConvexHullShapeSettings(tetrahedron, cDefaultConvexRadius, new PhysicsMaterialSimple("Convex Hull Material", next_color()));
+	scene->AddBody(BodyCreationSettings(convex_hull, next_pos(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
 
 	// Rotated convex hull
-	scene->AddBody(BodyCreationSettings(new RotatedTranslatedShapeSettings(Vec3::sReplicate(0.5f), Quat::sRotation(Vec3::sAxisZ(), 0.25f * JPH_PI), convex_hull), RVec3(0, cMaxHeight + 8.0f, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
+	scene->AddBody(BodyCreationSettings(new RotatedTranslatedShapeSettings(Vec3::sReplicate(0.5f), Quat::sRotation(Vec3::sAxisZ(), 0.25f * JPH_PI), convex_hull), next_pos(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
 
 	// Mutable compound
 	MutableCompoundShapeSettings *mutable_compound = new MutableCompoundShapeSettings();
-	mutable_compound->AddShape(Vec3(0, 0.5f, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new BoxShapeSettings(Vec3(0.5f, 0.1f, 0.2f), cDefaultConvexRadius, new PhysicsMaterialSimple("MutableCompound Box Material", Color::sGetDistinctColor(10))));
-	mutable_compound->AddShape(Vec3(0.5f, 0, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new CapsuleShapeSettings(0.5f, 0.1f, new PhysicsMaterialSimple("MutableCompound Capsule Material", Color::sGetDistinctColor(11))));
-	mutable_compound->AddShape(Vec3(0, 0, 0.5f), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), new TaperedCapsuleShapeSettings(0.5f, 0.2f, 0.1f, new PhysicsMaterialSimple("MutableCompound Tapered Capsule Material", Color::sGetDistinctColor(12))));
-	scene->AddBody(BodyCreationSettings(mutable_compound, RVec3(0, cMaxHeight + 9.0f, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
+	mutable_compound->AddShape(Vec3(0, 0.5f, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new BoxShapeSettings(Vec3(0.5f, 0.1f, 0.2f), cDefaultConvexRadius, new PhysicsMaterialSimple("MutableCompound Box Material", next_color())));
+	mutable_compound->AddShape(Vec3(0.5f, 0, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), new CapsuleShapeSettings(0.5f, 0.1f, new PhysicsMaterialSimple("MutableCompound Capsule Material", next_color())));
+	mutable_compound->AddShape(Vec3(0, 0, 0.5f), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), new TaperedCapsuleShapeSettings(0.5f, 0.2f, 0.1f, new PhysicsMaterialSimple("MutableCompound Tapered Capsule Material", next_color())));
+	scene->AddBody(BodyCreationSettings(mutable_compound, next_pos(), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
 
 	// Connect the first two dynamic bodies with a distance constraint
 	DistanceConstraintSettings *dist_constraint = new DistanceConstraintSettings();
@@ -180,18 +188,18 @@ Ref<PhysicsScene> LoadSaveSceneTest::sCreateScene()
 
 	// Add soft body cube
 	Ref<SoftBodySharedSettings> sb_cube_settings = SoftBodyCreator::CreateCube(5, 0.2f);
-	sb_cube_settings->mMaterials = { new PhysicsMaterialSimple("Soft Body Cube Material", Color::sGetDistinctColor(13)) };
-	SoftBodyCreationSettings sb_cube(sb_cube_settings, RVec3(0, cMaxHeight + 10.0f, 0), Quat::sIdentity(), Layers::MOVING);
+	sb_cube_settings->mMaterials = { new PhysicsMaterialSimple("Soft Body Cube Material", next_color()) };
+	SoftBodyCreationSettings sb_cube(sb_cube_settings, next_pos(), Quat::sIdentity(), Layers::MOVING);
 	scene->AddSoftBody(sb_cube);
 
 	// Add the same shape again to test sharing
-	sb_cube.mPosition = RVec3(0, cMaxHeight + 11.0f, 0);
+	sb_cube.mPosition = next_pos();
 	scene->AddSoftBody(sb_cube);
 
 	// Add soft body sphere
 	Ref<SoftBodySharedSettings> sb_sphere_settings = SoftBodyCreator::CreateSphere(0.5f);
-	sb_sphere_settings->mMaterials = { new PhysicsMaterialSimple("Soft Body Sphere Material", Color::sGetDistinctColor(14)) };
-	SoftBodyCreationSettings sb_sphere(sb_sphere_settings, RVec3(0, cMaxHeight + 12.0f, 0), Quat::sIdentity(), Layers::MOVING);
+	sb_sphere_settings->mMaterials = { new PhysicsMaterialSimple("Soft Body Sphere Material", next_color()) };
+	SoftBodyCreationSettings sb_sphere(sb_sphere_settings, next_pos(), Quat::sIdentity(), Layers::MOVING);
 	sb_sphere.mPressure = 2000.0f;
 	scene->AddSoftBody(sb_sphere);
 

+ 40 - 0
Samples/Tests/ScaledShapes/ScaledTaperedCylinderShapeTest.cpp

@@ -0,0 +1,40 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/ScaledShapes/ScaledTaperedCylinderShapeTest.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/ScaledShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(ScaledTaperedCylinderShapeTest)
+{
+	JPH_ADD_BASE_CLASS(ScaledTaperedCylinderShapeTest, Test)
+}
+
+void ScaledTaperedCylinderShapeTest::Initialize()
+{
+	// Floor
+	CreateFloor();
+
+	// Create tapered cylinder
+	RefConst<Shape> tapered_cylinder_shape = TaperedCylinderShapeSettings(2.0f, 0.75f, 1.25f).Create().Get();
+
+	// Original shape
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(tapered_cylinder_shape, RVec3(-20, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Uniformly scaled shape
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(new ScaledShape(tapered_cylinder_shape, Vec3::sReplicate(0.25f)), RVec3(-10, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Non-uniform scaled shape
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(new ScaledShape(tapered_cylinder_shape, Vec3(0.25f, 0.5f, 0.25f)), RVec3(0, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Flipped in 2 axis
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(new ScaledShape(tapered_cylinder_shape, Vec3(-1.5f, -0.5f, 1.5f)), RVec3(10, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Inside out
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(new ScaledShape(tapered_cylinder_shape, Vec3(-0.25f, 1.5f, 0.25f)), RVec3(20, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+}

+ 16 - 0
Samples/Tests/ScaledShapes/ScaledTaperedCylinderShapeTest.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 ScaledTaperedCylinderShapeTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, ScaledTaperedCylinderShapeTest)
+
+	// See: Test
+	virtual void	Initialize() override;
+};

+ 68 - 0
Samples/Tests/Shapes/TaperedCylinderShapeTest.cpp

@@ -0,0 +1,68 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/Shapes/TaperedCylinderShapeTest.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(TaperedCylinderShapeTest)
+{
+	JPH_ADD_BASE_CLASS(TaperedCylinderShapeTest, Test)
+}
+
+void TaperedCylinderShapeTest::Initialize()
+{
+	// Floor
+	CreateFloor();
+
+	RefConst<ShapeSettings> big_taperedcylinder = new TaperedCylinderShapeSettings(2.0f, 1.0f, 3.0f);
+	RefConst<ShapeSettings> big_taperedcylinder2 = new TaperedCylinderShapeSettings(2.0f, 3.0f, 1.0f);
+
+	// Tapered cylinder on large radius
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(big_taperedcylinder, RVec3(0, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Tapered cylinder on small radius
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(big_taperedcylinder2, RVec3(10, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Tapered cylinder on side
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(big_taperedcylinder, RVec3(20, 10, 0), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	RefConst<ShapeSettings> big_cone = new TaperedCylinderShapeSettings(2.0f, 0.0f, 3.0f, 0.0f);
+	RefConst<ShapeSettings> big_cone2 = new TaperedCylinderShapeSettings(2.0f, 3.0f, 0.0f, 0.0f);
+
+	// Cone on large radius
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(big_cone, RVec3(0, 10, 10), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Cone on small radius
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(big_cone2, RVec3(10, 10, 10), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	// Cone on side
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(big_cone, RVec3(20, 10, 10), Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+
+	RefConst<ShapeSettings> long_taperedcylinder = new TaperedCylinderShapeSettings(5, 0.5f, 1.0f);
+
+	// Tower of tapered cylinders
+	for (int i = 0; i < 10; ++i)
+	{
+		for (int j = 0; j < 2; ++j)
+		{
+			RVec3 position;
+			Quat rotation;
+			if (i & 1)
+			{
+				position = RVec3(-4.0f + 8.0f * j, 2.0f + 3.0f * i, -20.0f);
+				rotation = Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI + (j & 1) * JPH_PI);
+			}
+			else
+			{
+				position = RVec3(0, 2.0f + 3.0f * i, -20.0f - 4.0f + 8.0f * j);
+				rotation = Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI + (j & 1) * JPH_PI);
+			}
+			mBodyInterface->CreateAndAddBody(BodyCreationSettings(long_taperedcylinder, position, rotation, EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+		}
+	}
+}

+ 16 - 0
Samples/Tests/Shapes/TaperedCylinderShapeTest.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 TaperedCylinderShapeTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, TaperedCylinderShapeTest)
+
+	// See: Test
+	virtual void	Initialize() override;
+};

+ 2 - 0
Samples/Tests/SoftBody/SoftBodyShapesTest.cpp

@@ -12,6 +12,7 @@
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
@@ -71,6 +72,7 @@ void SoftBodyShapesTest::Initialize()
 		new RotatedTranslatedShape(Vec3::sZero(), rotate_x, new CapsuleShape(1, 0.5f)),
 		new RotatedTranslatedShape(Vec3::sZero(), rotate_x, TaperedCapsuleShapeSettings(1.0f, 1.0f, 0.5f).Create().Get()),
 		new RotatedTranslatedShape(Vec3::sZero(), rotate_x, new CylinderShape(1, 0.5f)),
+		new RotatedTranslatedShape(Vec3::sZero(), rotate_x, TaperedCylinderShapeSettings(1, 0.5f, 1.0f).Create().Get()),
 		tetrahedron.Create().Get(),
 		compound_shape.Create().Get(),
 	};

+ 20 - 0
UnitTests/Physics/RayShapeTests.cpp

@@ -12,6 +12,7 @@
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/ScaledShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/MutableCompoundShape.h>
@@ -470,6 +471,25 @@ TEST_SUITE("RayShapeTests")
 		TestRayHelper(shape, Vec3(0, 0, -2), Vec3(0, 0, 2));
 	}
 
+	TEST_CASE("TestTaperedCylinderShapeRay")
+	{
+		// Create tapered cylinder shape
+		Ref<Shape> shape = TaperedCylinderShapeSettings(4, 1, 3).Create().Get();
+
+		// Ray through origin
+		TestRayHelper(shape, Vec3(-2, 0, 0), Vec3(2, 0, 0));
+		TestRayHelper(shape, Vec3(0, -4, 0), Vec3(0, 4, 0));
+		TestRayHelper(shape, Vec3(0, 0, -2), Vec3(0, 0, 2));
+
+		// Ray halfway to the top
+		TestRayHelper(shape, Vec3(-1.5f, 2, 0), Vec3(1.5f, 2, 0));
+		TestRayHelper(shape, Vec3(0, 2, -1.5f), Vec3(0, 2, 1.5f));
+
+		// Ray halfway to the bottom
+		TestRayHelper(shape, Vec3(-2.5f, -2, 0), Vec3(2.5f, -2, 0));
+		TestRayHelper(shape, Vec3(0, -2, -2.5f), Vec3(0, -2, 2.5f));
+	}
+
 	TEST_CASE("TestScaledShapeRay")
 	{
 		// Create convex hull shape of a box (off center so the center of mass is not zero)

+ 14 - 0
UnitTests/Physics/ShapeTests.cpp

@@ -10,6 +10,7 @@
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
 #include <Jolt/Physics/Collision/Shape/ScaledShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/MutableCompoundShape.h>
@@ -169,6 +170,19 @@ TEST_SUITE("ShapeTests")
 		CHECK(cylinder->MakeScaleValid(Vec3(-1.0e-10f, 1, 1.0e-10f)) == Vec3(-ScaleHelpers::cMinScale, 1, ScaleHelpers::cMinScale));
 		CHECK(cylinder->MakeScaleValid(Vec3(2, 5, -4)) == Vec3(3, 5, -3));
 
+		Ref<Shape> tapered_cylinder = TaperedCylinderShapeSettings(0.5f, 2.0f, 3.0f).Create().Get();
+		CHECK(!tapered_cylinder->IsValidScale(Vec3::sZero()));
+		CHECK(!tapered_cylinder->IsValidScale(Vec3(0, 1, 0)));
+		CHECK(!tapered_cylinder->IsValidScale(Vec3(1, 0, 1)));
+		CHECK(tapered_cylinder->IsValidScale(Vec3(2, 2, 2)));
+		CHECK(tapered_cylinder->IsValidScale(Vec3(-1, 1, -1)));
+		CHECK(!tapered_cylinder->IsValidScale(Vec3(2, 1, 1)));
+		CHECK(tapered_cylinder->IsValidScale(Vec3(1, 2, 1)));
+		CHECK(!tapered_cylinder->IsValidScale(Vec3(1, 1, 2)));
+		CHECK(tapered_cylinder->MakeScaleValid(Vec3::sZero()).IsClose(Vec3::sReplicate(ScaleHelpers::cMinScale), cMinScaleToleranceSq));
+		CHECK(tapered_cylinder->MakeScaleValid(Vec3(-1.0e-10f, 1, 1.0e-10f)) == Vec3(-ScaleHelpers::cMinScale, 1, ScaleHelpers::cMinScale));
+		CHECK(tapered_cylinder->MakeScaleValid(Vec3(2, 5, -4)) == Vec3(3, 5, -3));
+
 		Ref<Shape> triangle = new TriangleShape(Vec3(1, 2, 3), Vec3(4, 5, 6), Vec3(7, 8, 9));
 		CHECK(!triangle->IsValidScale(Vec3::sZero()));
 		CHECK(!triangle->IsValidScale(Vec3::sAxisX()));

+ 51 - 0
UnitTests/Physics/TaperedCylinderShapeTests.cpp

@@ -0,0 +1,51 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include "UnitTestFramework.h"
+#include "PhysicsTestContext.h"
+#include <Jolt/Physics/Collision/Shape/TaperedCylinderShape.h>
+
+TEST_SUITE("TaperedCylinderShapeTests")
+{
+	TEST_CASE("TestMassAndInertia")
+	{
+		const float cDensity = 3.0f;
+		const float cRadius = 5.0f;
+		const float cHeight = 7.0f;
+
+		TaperedCylinderShapeSettings settings1(0.5f * cHeight, cRadius, 0.0f, 0.0f);
+		settings1.SetDensity(cDensity);
+
+		TaperedCylinderShapeSettings settings2(0.5f * cHeight, 0.0f, cRadius, 0.0f);
+		settings2.SetDensity(cDensity);
+
+		RefConst<TaperedCylinderShape> cylinder1 = StaticCast<TaperedCylinderShape>(settings1.Create().Get());
+		RefConst<TaperedCylinderShape> cylinder2 = StaticCast<TaperedCylinderShape>(settings2.Create().Get());
+
+		MassProperties m1 = cylinder1->GetMassProperties();
+		MassProperties m2 = cylinder2->GetMassProperties();
+
+		// Mass/inertia is the same for both shapes because they are mirrored versions (inertia is calculated from COM)
+		CHECK_APPROX_EQUAL(m1.mMass, m2.mMass);
+		CHECK_APPROX_EQUAL(m1.mInertia, m2.mInertia);
+
+		// Center of mass for a cone is at 1/4 h (if cone runs from -h/2 to h/2)
+		// See: https://www.miniphysics.com/uy1-centre-of-mass-of-a-cone.html
+		Vec3 expected_com1(0, cHeight / 4.0f, 0);
+		Vec3 expected_com2 = -expected_com1;
+		CHECK_APPROX_EQUAL(cylinder1->GetCenterOfMass(), expected_com1);
+		CHECK_APPROX_EQUAL(cylinder2->GetCenterOfMass(), expected_com2);
+
+		// Mass of cone
+		float expected_mass = cDensity * JPH_PI * Square(cRadius) * cHeight / 3.0f;
+		CHECK_APPROX_EQUAL(expected_mass, m1.mMass);
+
+		// Inertia of cone (according to https://en.wikipedia.org/wiki/List_of_moments_of_inertia)
+		float expected_inertia_xx = expected_mass * (3.0f / 20.0f * Square(cRadius) + 3.0f / 80.0f * Square(cHeight));
+		float expected_inertia_yy = expected_mass * (3.0f / 10.0f * Square(cRadius));
+		CHECK_APPROX_EQUAL(expected_inertia_xx, m1.mInertia(0, 0), 1.0e-3f);
+		CHECK_APPROX_EQUAL(expected_inertia_yy, m1.mInertia(1, 1), 1.0e-3f);
+		CHECK_APPROX_EQUAL(expected_inertia_xx, m1.mInertia(2, 2), 1.0e-3f);
+	}
+}

+ 1 - 0
UnitTests/UnitTests.cmake

@@ -64,6 +64,7 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/Physics/SliderConstraintTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/SoftBodyTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/SubShapeIDTest.cpp
+	${UNIT_TESTS_ROOT}/Physics/TaperedCylinderShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/TransformedShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/WheeledVehicleTests.cpp
 	${UNIT_TESTS_ROOT}/PhysicsTestContext.cpp