Here’s my current working classes for procedural meshes that calculate tangents correctly. It is currently highly inefficient since it stores an extra copy of all of the triangles in the mesh. If I get some free time I’ll make a tutorial on how I make the procedural doors.
// UE4 Procedural Mesh Generation from the Epic Wiki (https://wiki.unrealengine.com/Procedural_Mesh_Generation)
//
// forked from "Engine/Plugins/Runtime/CustomMeshComponent/Source/CustomMeshComponent/Classes/CustomMeshComponent.h"
#pragma once
#include "ProceduralMeshComponent.generated.h"
USTRUCT(BlueprintType)
struct FProceduralMeshVertex
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, Category = Vertex)
FVector Position;
UPROPERTY(EditAnywhere, Category = Vertex)
FColor Color;
UPROPERTY(EditAnywhere, Category = Vertex)
float U;
UPROPERTY(EditAnywhere, Category = Vertex)
float V;
UPROPERTY(EditAnywhere, Category = Vertex)
FVector vNormal = FVector(0.f, 1.f, 0.f);
UPROPERTY(EditAnywhere, Category = Vertex)
FVector vTangent;
UPROPERTY(EditAnywhere, Category = Vertex)
FVector vBitangent;
UPROPERTY(EditAnywhere, Category = Vertex)
bool manualNormals = false;
};
USTRUCT(BlueprintType)
struct FProceduralMeshTriangle
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, Category = Triangle)
FProceduralMeshVertex Vertex0;
UPROPERTY(EditAnywhere, Category = Triangle)
FProceduralMeshVertex Vertex1;
UPROPERTY(EditAnywhere, Category = Triangle)
FProceduralMeshVertex Vertex2;
UPROPERTY(EditAnywhere, Category = Triangle)
FVector faceNormal = FVector(0.f, 1.f, 0.f);
UPROPERTY(EditAnywhere, Category = Triangle)
int32 SmoothingGroup = 0;
//used to keep track of the triangles intended position in the array
UPROPERTY(EditAnywhere, Category = Triangle)
int32 triangleIndexNumber;
UPROPERTY(EditAnywhere, Category = Triangle)
TArray<int32> attachedTriangles;
UPROPERTY(EditAnywhere, Category = Triangle)
bool normalsHaveBeenCalculated = false;
};
USTRUCT(BlueprintType)
struct FProceduralMeshTriangleSmoothingGroup
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, Category = SmoothingGroup)
TArray<FProceduralMeshTriangle> SGTriangle;
UPROPERTY(EditAnywhere, Category = SmoothingGroup)
int32 SGIndexNumber;
UPROPERTY(EditAnywhere, Category = SmoothingGroup)
float SmoothingTolernace;
};
/** Component that allows you to specify custom triangle mesh geometry */
UCLASS(editinlinenew, meta = (BlueprintSpawnableComponent), ClassGroup = Rendering)
class UProceduralMeshComponent : public UMeshComponent, public IInterface_CollisionDataProvider
{
GENERATED_UCLASS_BODY()
public:
/** Set the geometry to use on this triangle mesh */
UFUNCTION(BlueprintCallable, Category = "Components|ProceduralMesh")
bool SetProceduralMeshTriangles(const TArray<FProceduralMeshTriangle>& Triangles);
/** Description of collision */
UPROPERTY(BlueprintReadOnly, Category = "Collision")
class UBodySetup* ModelBodySetup;
// Begin Interface_CollisionDataProvider Interface
virtual bool GetPhysicsTriMeshData(struct FTriMeshCollisionData* CollisionData, bool InUseAllTriData) override;
virtual bool ContainsPhysicsTriMeshData(bool InUseAllTriData) const override;
virtual bool WantsNegXTriMesh() override{ return false; }
// End Interface_CollisionDataProvider Interface
// Begin UPrimitiveComponent interface.
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
virtual class UBodySetup* GetBodySetup() override;
// End UPrimitiveComponent interface.
// Begin UMeshComponent interface.
virtual int32 GetNumMaterials() const override;
// End UMeshComponent interface.
void UpdateBodySetup();
void UpdateCollision();
//calcualte our normals, tangents, and bitangents
FProceduralMeshTriangle GetNormals(TArray<FProceduralMeshTriangle>& Triangles);
private:
// Begin USceneComponent interface.
virtual FBoxSphereBounds CalcBounds(const FTransform & LocalToWorld) const override;
// Begin USceneComponent interface.
/** */
TArray<FProceduralMeshTriangle> ProceduralMeshTris;
friend class FProceduralMeshSceneProxy;
};
/** Scene proxy */
class FProceduralMeshSceneProxy : public FPrimitiveSceneProxy
{
public:
FProceduralMeshSceneProxy(UProceduralMeshComponent* Component)
: FPrimitiveSceneProxy(Component)
#if ENGINE_MAJOR_VERSION >= 4 && ENGINE_MINOR_VERSION >= 5
, MaterialRelevance(Component->GetMaterialRelevance(GetScene().GetFeatureLevel()))
#else
, MaterialRelevance(Component->GetMaterialRelevance())
#endif
{
//Store an array of identical triangles
TArray<FProceduralMeshTriangle> storedTriangles;
for (int TriIdx = 0; TriIdx < Component->ProceduralMeshTris.Num(); TriIdx++)
{
storedTriangles.Add(Component->ProceduralMeshTris[TriIdx]);
storedTriangles[TriIdx].triangleIndexNumber = TriIdx;
//http://www.lighthouse3d.com/opengl/terrain/index.php3?normals
const FVector Edge01 = (storedTriangles[TriIdx].Vertex1.Position - storedTriangles[TriIdx].Vertex0.Position);
const FVector Edge02 = (storedTriangles[TriIdx].Vertex2.Position - storedTriangles[TriIdx].Vertex0.Position);
FVector faceNormal = -(Edge01 ^ Edge02).GetSafeNormal();
//If we don't have manually input vertex normals then calculate our own
if (!storedTriangles[TriIdx].Vertex0.manualNormals || !storedTriangles[TriIdx].Vertex1.manualNormals || !storedTriangles[TriIdx].Vertex2.manualNormals)
{
storedTriangles[TriIdx].Vertex0.vNormal = faceNormal;
storedTriangles[TriIdx].Vertex1.vNormal = faceNormal;
storedTriangles[TriIdx].Vertex2.vNormal = faceNormal;
}
}
//search predefined attached triangles for matching vertices and average tangents
for (int i = 0; i < storedTriangles.Num() - 1; i++)
{
//store an array of all attached triangles so that they can have thier normals calculated
TArray<FProceduralMeshTriangle> attachedTriangles;
if (!storedTriangles*.normalsHaveBeenCalculated)
{
attachedTriangles.Add(storedTriangles*);
storedTriangles*.normalsHaveBeenCalculated = true;
//add all attached triangles to the array that gets bulk normals calculation
//set normalsHaveBeenCalculated to true to prevent the values from getting overwritten
for (int j = 0; j < storedTriangles*.attachedTriangles.Num(); j++)
{
attachedTriangles.Add(storedTriangles[storedTriangles*.attachedTriangles[j]]);
storedTriangles[storedTriangles*.attachedTriangles[j]].normalsHaveBeenCalculated = true;
}
//Lengyel, Eric. “Computing Tangent Space Basis Vectors for an Arbitrary Mesh”. Terathon Software 3D Graphics Library, 2001. http://www.terathon.com/code/tangent.html
for (long a = 0; a < attachedTriangles.Num(); a++)
{
//determine which way the normals are facing
int8 handedness;
FVector v1 = attachedTriangles[a].Vertex0.Position;
FVector v2 = attachedTriangles[a].Vertex1.Position;
FVector v3 = attachedTriangles[a].Vertex2.Position;
FVector2D w1 = FVector2D(attachedTriangles[a].Vertex0.U, attachedTriangles[a].Vertex0.V);
FVector2D w2 = FVector2D(attachedTriangles[a].Vertex1.U, attachedTriangles[a].Vertex1.V);
FVector2D w3 = FVector2D(attachedTriangles[a].Vertex2.U, attachedTriangles[a].Vertex2.V);
float x1 = v2.X - v1.X;
float x2 = v3.X - v1.X;
float y1 = v2.Y - v1.Y;
float y2 = v3.Y - v1.Y;
float z1 = v2.Z - v1.Z;
float z2 = v3.Z - v1.Z;
float s1 = w2.X - w1.X;
float s2 = w3.X - w1.X;
float t1 = w2.Y - w1.Y;
float t2 = w3.Y - w1.Y;
float r = 1.0F / (s1 * t2 - s2 * t1);
FVector sdir((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r,
(t2 * z1 - t1 * z2) * r);
FVector tdir((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r,
(s1 * z2 - s2 * z1) * r);
attachedTriangles[a].Vertex0.vTangent += sdir;
attachedTriangles[a].Vertex1.vTangent += sdir;
attachedTriangles[a].Vertex2.vTangent += sdir;
attachedTriangles[a].Vertex0.vBitangent += tdir;
attachedTriangles[a].Vertex1.vBitangent += tdir;
attachedTriangles[a].Vertex2.vBitangent += tdir;
//attachedTriangles[a].
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex0.vTangent = attachedTriangles[a].Vertex0.vTangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex0.vBitangent = attachedTriangles[a].Vertex0.vBitangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex1.vTangent = attachedTriangles[a].Vertex1.vTangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex1.vBitangent = attachedTriangles[a].Vertex1.vBitangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex2.vTangent = attachedTriangles[a].Vertex2.vTangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex2.vBitangent = attachedTriangles[a].Vertex2.vBitangent;
const FVector n = attachedTriangles[a].Vertex0.vNormal;
const FVector t = attachedTriangles[a].Vertex0.vTangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex0.vNormal = storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex0.vNormal;
//calculateHandedness
handedness = (((n ^ t) | storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex0.vBitangent) < 0.0F) ? -1.0F : 1.0F;
// Gram-Schmidt orthogonalize
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex0.vTangent = (t - n * (n | t)).GetSafeNormal();
//calculate Bitangent
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex0.vBitangent = (n ^ t) * handedness;
const FVector nn = attachedTriangles[a].Vertex1.vNormal;
const FVector tt = attachedTriangles[a].Vertex1.vTangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex1.vNormal = storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex1.vNormal;
//calculateHandedness
handedness = (((nn ^ tt) | storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex1.vBitangent) < 0.0F) ? -1.0F : 1.0F;
// Gram-Schmidt orthogonalize
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex1.vTangent = (tt - nn * (nn | tt)).GetSafeNormal();
//calculate Bitangent
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex1.vBitangent = (nn ^ tt) * handedness;
const FVector nnn = attachedTriangles[a].Vertex2.vNormal;
const FVector ttt = attachedTriangles[a].Vertex2.vTangent;
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex2.vNormal = storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex2.vNormal;
//calculateHandedness
handedness = (((nnn ^ ttt) | storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex2.vBitangent) < 0.0F) ? -1.0F : 1.0F;
// Gram-Schmidt orthogonalize
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex2.vTangent = (ttt - nnn * (nnn | ttt)).GetSafeNormal();
//calculate Bitangent
storedTriangles[attachedTriangles[a].triangleIndexNumber].Vertex2.vBitangent = (nnn ^ ttt) * handedness;
}
}
}
// Add each triangle to the vertex/index buffer
for (int TriIdx = 0; TriIdx<Component->ProceduralMeshTris.Num(); TriIdx++)
{
FProceduralMeshTriangle& Tri = Component->ProceduralMeshTris[TriIdx];
FDynamicMeshVertex Vert0;
Vert0.Position = Tri.Vertex0.Position;
Vert0.Color = Tri.Vertex0.Color;
Vert0.SetTangents(storedTriangles[TriIdx].Vertex0.vTangent,
storedTriangles[TriIdx].Vertex0.vBitangent,
storedTriangles[TriIdx].Vertex0.vNormal);
Vert0.TextureCoordinate.Set(Tri.Vertex0.U, Tri.Vertex0.V);
int32 VIndex = VertexBuffer.Vertices.Add(Vert0);
IndexBuffer.Indices.Add(VIndex);
FDynamicMeshVertex Vert1;
Vert1.Position = Tri.Vertex1.Position;
Vert1.Color = Tri.Vertex1.Color;
Vert1.SetTangents(storedTriangles[TriIdx].Vertex1.vTangent,
storedTriangles[TriIdx].Vertex1.vBitangent,
storedTriangles[TriIdx].Vertex1.vNormal);
Vert1.TextureCoordinate.Set(Tri.Vertex1.U, Tri.Vertex1.V);
VIndex = VertexBuffer.Vertices.Add(Vert1);
IndexBuffer.Indices.Add(VIndex);
FDynamicMeshVertex Vert2;
Vert2.Position = Tri.Vertex2.Position;
Vert2.Color = Tri.Vertex2.Color;
Vert2.SetTangents(storedTriangles[TriIdx].Vertex2.vTangent,
storedTriangles[TriIdx].Vertex2.vBitangent,
storedTriangles[TriIdx].Vertex2.vNormal);
Vert2.TextureCoordinate.Set(Tri.Vertex2.U, Tri.Vertex2.V);
VIndex = VertexBuffer.Vertices.Add(Vert2);
IndexBuffer.Indices.Add(VIndex);
}
// Init vertex factory
VertexFactory.Init(&VertexBuffer);
// Enqueue initialization of render resource
BeginInitResource(&VertexBuffer);
BeginInitResource(&IndexBuffer);
BeginInitResource(&VertexFactory);
// Grab material
Material = Component->GetMaterial(0);
if (Material == NULL)
{
Material = UMaterial::GetDefaultMaterial(MD_Surface);
}
}