ProceduralMeshComponent won't override mass, causes erratic physics

I’m trying to implement a per-triangle buoyancy algorithm for what will eventually become a realistic naval combat game. In a test actor derived from StaticMeshActor, the algorithm works fine; however, when applied to a custom component derived from ProceduralMeshComponent, it doesn’t work. I’ve determined that (at least part of) the problem is that the new procedural one isn’t taking into account its overridden mass. Despite calling SetMassOverrideInKg() on the component from its parent actor, and despite it showing up (greyed out) in the editor as my chosen value of 500Kg, calling GetMass() to test returns a value of 1.0Kg.

When two identically-sized cubes, one using the old static mesh and one using the new procedural one, are placed next to each other, they fall at the same rate but when they hit the “water” (not actually rendered, just a value of 0 passed to the depth calculation), the static mesh floats as you’d expect while the procedural one is launched several hundred meters into the air. Upon falling down and hitting the water again, it then disappears, presumably having been launched upward again at extreme velocity. I have set both classes to output information about their buoyant forces every tick and determined that their relative masses are the only difference between them.

Additionally, when I stop the game in the editor it gives me a warning that says:

Warning Trying to simulate physics on ''/Temp/UEDPIE_0_Untitled_1.Untitled_1:PersistentLevel.CustomVesselPawn_1.RootComponent'' but it has ComplexAsSimple collision.

Can anyone tell what’s going wrong here? I’ve been trying to fix this single issue for days now and I’ve finally decided it’s time to ask for help. I’ve posted my relevant source code below as well.

Constructor of CustomVesselPawn.cpp

ACustomVesselPawn::ACustomVesselPawn()
{
	PrimaryActorTick.bCanEverTick = true;

	SetActorEnableCollision(true);

	FSurfaceArray NewSurfaces;

	FIntVector v0(1, 1, 1); //+++
	FIntVector v1(-1, 1, 1); //-++
	FIntVector v2(-1, -1, 1); //--+
	FIntVector v3(-1, -1, -1); //---
	FIntVector v4(1, -1, -1); //+--
	FIntVector v5(1, 1, -1); //++-
	FIntVector v6(-1, 1, -1); //-+-
	FIntVector v7(1, -1, 1); //+-+

	NewSurfaces.Add(FSurface(v0, v5, v6, 10));
	NewSurfaces.Add(FSurface(v0, v1, v6, 10));

	NewSurfaces.Add(FSurface(v1, v2, v3, 10));
	NewSurfaces.Add(FSurface(v1, v6, v3, 10));

	NewSurfaces.Add(FSurface(v2, v7, v0, 10));
	NewSurfaces.Add(FSurface(v2, v1, v0, 10));

	NewSurfaces.Add(FSurface(v2, v3, v4, 10));
	NewSurfaces.Add(FSurface(v2, v7, v4, 10));

	NewSurfaces.Add(FSurface(v7, v0, v5, 10));
	NewSurfaces.Add(FSurface(v7, v4, v5, 10));

	NewSurfaces.Add(FSurface(v3, v4, v5, 10));
	NewSurfaces.Add(FSurface(v3, v6, v5, 10));

	UBuoyantProceduralMeshComponent* MeshComponent = CreateDefaultSubobject<UBuoyantProceduralMeshComponent>(TEXT("RootComponent"));
	RootComponent = MeshComponent;

	MeshComponent->SetSurfaceData(NewSurfaces);
	MeshComponent->SetCollisionProfileName(TEXT("Pawn"));
	MeshComponent->SetMassOverrideInKg(NAME_None, 500.0F, true);
	MeshComponent->SetSimulatePhysics(true);
	MeshComponent->SetEnableGravity(true);
	MeshComponent->RegisterComponent();
}

BuoyantProdecuralMeshComponent.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ProceduralMeshComponent.h"
#include "BoatGameUtils.h"
#include "BuoyantProceduralMeshComponent.generated.h"

/**
 * 
 */
USTRUCT()
struct BOATGAME_API FSurface {

	GENERATED_BODY()

	UPROPERTY()
		FIntVector v0;

	UPROPERTY()
		FIntVector v1;

	UPROPERTY()
		FIntVector v2;

	UPROPERTY()
		int Thickness;

	FSurface(FIntVector av0 = FIntVector(0, 0, 0), FIntVector av1 = FIntVector(0, 0, 0), FIntVector av2 = FIntVector(0, 0, 0), int aThickness = 10) {
		v0 = av0;
		v1 = av1;
		v2 = av2;
		Thickness = aThickness;
	}

	operator FTriangle() {
		return { (FVector)(v0 * 50), (FVector)(v1 * 50), (FVector)(v2 * 50) };
	}
};

typedef TArray<FSurface> FSurfaceArray;

UCLASS()
class BOATGAME_API UBuoyantProceduralMeshComponent : public UProceduralMeshComponent
{
	GENERATED_BODY()
	
public:
	UBuoyantProceduralMeshComponent();

	virtual void BeginPlay() override;

	virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;

	static const float WATER_DENSITY;

	void SetSurfaceData(FSurfaceArray NewSurfaces);

private:
	FSurfaceArray Surfaces;

	TArray<FTriIndices> Triangles;
	TArray<FVector> Verts;
	TArray<FVector> Normals;

	void SortTriangleVerticesByDepth(FVector* Vertices, float* Depths);

	FVector SurfaceToUUCoords(FIntVector& SurfaceCoords);
	
};

BuoyantProcedualMeshComponent.cpp:

#include "BoatGame.h"
#include "BuoyantProceduralMeshComponent.h"

const float UBuoyantProceduralMeshComponent::WATER_DENSITY = 0.001F;

UBuoyantProceduralMeshComponent::UBuoyantProceduralMeshComponent() {
	this->bAutoActivate = true;
	PrimaryComponentTick.bCanEverTick = true;
}

void UBuoyantProceduralMeshComponent::BeginPlay() {
	Super::BeginPlay();
}

void UBuoyantProceduralMeshComponent::TickComponent(float DeltaTime, enum ELevelTick TickType,
	FActorComponentTickFunction *ThisTickFunction) {

	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	AActor* Owner = GetOwner();

	FTriangle* SubTris = new FTriangle[Surfaces.Num() * 2];
	unsigned TriCount = 0;

	const int H = 0;
	const int M = 1;
	const int L = 2;

	const float WorldTime = Owner->GetWorld()->GetTimeSeconds();

	const FVector OwnerPos = Owner->GetActorLocation();
	const FTransform OwnerTransform = Owner->GetTransform();

	FVector COM = GetCenterOfMass();

	for (int i = 0; i < Triangles.Num(); i++) {
		FTriIndices Tri = Triangles[i];
		FVector VerticesSorted[] = { Verts[Tri.v0], Verts[Tri.v1], Verts[Tri.v2] };
		float Depth[] = {
			BoatGameUtils::VectorHeightAboveWater(OwnerPos + OwnerTransform.TransformVector(VerticesSorted[0]), WorldTime),
			BoatGameUtils::VectorHeightAboveWater(OwnerPos + OwnerTransform.TransformVector(VerticesSorted[1]), WorldTime),
			BoatGameUtils::VectorHeightAboveWater(OwnerPos + OwnerTransform.TransformVector(VerticesSorted[2]), WorldTime)
		};

		SortTriangleVerticesByDepth(VerticesSorted, Depth);

		if (Depth[L] > 0) continue;	//skip this triangle if the lowest point is above water

		FVector normal = OwnerTransform.GetRotation().RotateVector(Normals[i]);

		if (Depth[H] < 0) {		//if all points are below water, calculation is very easy
			SubTris[TriCount] = { VerticesSorted[H], VerticesSorted[M], VerticesSorted[L], normal };
			TriCount++;
		}
		else if (Depth[M] > 0) {		//if only lowest point is submerged
			float tM = -Depth[L] / (Depth[M] - Depth[L]);
			float tH = -Depth[L] / (Depth[H] - Depth[L]);
			FVector JM = VerticesSorted[L] + tM * (VerticesSorted[M] - VerticesSorted[L]);
			FVector JH = VerticesSorted[L] + tM * (VerticesSorted[H] - VerticesSorted[L]);
			SubTris[TriCount] = { JH, JM, VerticesSorted[L], normal };
			TriCount++;
		}
		else {							//if lowest two points are submerged
			float tM = -Depth[M] / (Depth[H] - Depth[M]);
			float tL = -Depth[L] / (Depth[H] - Depth[L]);
			FVector IM = VerticesSorted[M] + tL * (VerticesSorted[H] - VerticesSorted[M]);
			FVector IL = VerticesSorted[L] + tL * (VerticesSorted[H] - VerticesSorted[L]);
			SubTris[TriCount] = { VerticesSorted[M], VerticesSorted[L], IM, normal };
			TriCount++;
			SubTris[TriCount] = { VerticesSorted[L], IM, IL, normal };
			TriCount++;
		}
		//UE_LOG(LogTemp, Log, TEXT("Triangle %i has vertex depths %f > %f > %f"), i, Depth[0], Depth[1], Depth[2]);
	}

	for (int unsigned i = 0; i < TriCount; i++) {
		FTriangle Tri = SubTris[i];
		FVector Center = OwnerPos + OwnerTransform.TransformVector((Tri.v0 + Tri.v1 + Tri.v2) / 3.0f);
		float CDepth = BoatGameUtils::VectorHeightAboveWater(Center, WorldTime);

		float SurfaceArea = FVector::CrossProduct(Tri.v1 - Tri.v0, Tri.v2 - Tri.v0).Size() / 2.0F;

		FVector F = -WATER_DENSITY * Owner->GetWorldSettings()->GetGravityZ() * SurfaceArea * CDepth * Tri.n;
		F.X = 0;
		F.Y = 0;
		AddForceAtLocation(F, Center);
		UE_LOG(LogTemp, Log, TEXT("(BUOYANT PROCEDURAL MESH) Relative Buoyant Force on Triangle %i = %f; SA=%f; Depth=%f; Mass=%f; MassOverride=%i"),
			i, F.Size() / SurfaceArea, SurfaceArea, CDepth, GetMass(), GetBodyInstance()->bOverrideMass);
	}

}

void UBuoyantProceduralMeshComponent::SortTriangleVerticesByDepth(FVector* Vertices, float* Depths) {
	if (Depths[1] > Depths[0]) {
		BoatGameUtils::SwapIndices<FVector>(Vertices, 0, 1);
		BoatGameUtils::SwapIndices<float>(Depths, 0, 1);
	}

	if (Depths[2] > Depths[1]) {
		BoatGameUtils::SwapIndices<FVector>(Vertices, 1, 2);
		BoatGameUtils::SwapIndices<float>(Depths, 1, 2);
	}

	if (Depths[1] > Depths[0]) {
		BoatGameUtils::SwapIndices<FVector>(Vertices, 0, 1);
		BoatGameUtils::SwapIndices<float>(Depths, 0, 1);
	}
}

void UBuoyantProceduralMeshComponent::SetSurfaceData(FSurfaceArray NewSurfaces) {
	Surfaces = NewSurfaces;
	
	Triangles.Empty();
	Verts.Empty();
	Normals.Empty();

	TArray<int32> TriIndices;
	TArray<FVector> VertNormals;
	TArray<FVector2D> UV0;
	TArray<FColor> Colors;
	TArray<FProcMeshTangent> Tangents;

	for (int i = 0; i < Surfaces.Num(); i++) {
		FSurface Surface = Surfaces[i];
		FTriIndices tri;

		FVector v0, v1, v2;

		v0 = SurfaceToUUCoords(Surface.v0);
		tri.v0 = Verts.Add(v0);

		v1 = SurfaceToUUCoords(Surface.v1);
		tri.v1 = Verts.Add(v1);

		v2 = SurfaceToUUCoords(Surface.v2);
		tri.v2 = Verts.Add(v2);

		Triangles.Add(tri);

		TriIndices.Add(tri.v0);
		TriIndices.Add(tri.v1);
		TriIndices.Add(tri.v2);

		UV0.Add(FVector2D(0, 0));
		UV0.Add(FVector2D(0, 10));
		UV0.Add(FVector2D(10, 10));

		Colors.Add(FColor(100, 100, 100, 100));
		Colors.Add(FColor(100, 100, 100, 100));
		Colors.Add(FColor(100, 100, 100, 100));

		Tangents.Add(FProcMeshTangent(1, 1, 1));
		Tangents.Add(FProcMeshTangent(1, 1, 1));
		Tangents.Add(FProcMeshTangent(1, 1, 1));
	}

	for (int i = 0; i < Triangles.Num(); i++) {
		FTriIndices tri = Triangles[i];
		FVector v0 = Verts[tri.v0];
		FVector v1 = Verts[tri.v1];
		FVector v2 = Verts[tri.v2];

		FVector center = (v0 + v1 + v2) / 3.0F;
		FVector n = FVector::CrossProduct(v1 - v0, v2 - v0);
		n.Normalize();
		if (BoatGameUtils::CountIntersections(center, center + n, i, Triangles, Verts) % 2 == 1) n *= -1;
		Normals.Add(n);

		VertNormals.Add(n);
		VertNormals.Add(n);
		VertNormals.Add(n);
	}

	CreateMeshSection(0, Verts, TriIndices, VertNormals, UV0, Colors, Tangents, true);
}

FVector UBuoyantProceduralMeshComponent::SurfaceToUUCoords(FIntVector& SurfaceCoords) {
	
	return (FVector)(SurfaceCoords * 50);
}

Did you find any solution?

I believe the problem was I had created the visual mesh but no collision convex mesh section, which is required for all physics and not just collisions.