Hi, thanks so much for your help @ClockworkOcean , everyone. I tried that solution, but it didn’t work out as I’d hoped, and ChatGPT didn’t help either. But since I’m a developer I learned game dev and came up with an elegant solution (took me some time), I’m not a pro yet, so chat had to tweak the code a bit:
I said I should approach this just like any other terrain creation: first I’ll define the shape, then apply the repeating texture, and finally add random details using the foliage mod.
Drawing a little inspiration from your comments and this idea, I’ve created something that comes closest to what I’ve been aiming for so far, which is:
The wall is mainly made up of these elements:
- the pillars that are generated, aligned, and rotated
- details at the bottom—I decided not to use them and instead added snow piles at the bottom to create a smooth transition (so you can ignore them)
- a wall cover (inner veil)-- because when I applied the material to each pillar, it clearly marked the transition between each pillar and made it stand out as a line
I still have two problems:
- light and shadow break at abrupt transitions (like in the previous image)
- The material breaks during abrupt transitions, revealing the pillars’ material (See the following images)
I tried to fix the problem, but it’s not working. And when the distance between two points is too great and I try to add additional points between them in the material cover , everything crashes.
Here is my code:
AIceWallActor.h:
#pragma once
“CoreMinimal.h”
“GameFramework/Actor.h”
“ProceduralMeshComponent.h”
“AIceWallActor.generated.h”
class UMaterialInterface;
UCLASS()
class ICEWALLPROJECT_API AIceWallActor : public AActor
{
GENERATED_BODY()
public:
AIceWallActor();
#if WITH_EDITOR
virtual void OnConstruction(const FTransform& Transform) override;
#endif
protected:
virtual void BeginPlay() override;
public:
// -------------------------
// Main shape
// -------------------------
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "1000.0"))
float Radius = 50000.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "1000.0"))
float WallHeight = 16000.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "100.0"))
float WallThickness = 3200.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "100.0"))
float PillarWidth = 1800.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "0.0"))
float PillarOverlap = 350.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "0.0"))
float BevelAmount = 180.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "16", ClampMax = "128"))
int32 OrganicRingSegments = 48;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float EdgeRoundness = 0.85f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "0.0"))
float OrganicSurfaceAmount = 180.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "0.0"))
float OrganicTopVariation = 350.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape", meta = (ClampMin = "0.0"))
float OrganicBottomVariation = 120.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape")
bool bSmoothOrganicNormals = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape")
bool bAutoSmoothPillarConnections = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Shape")
float GroundSinkAmount = 300.0f;
// -------------------------
// UV / material
// -------------------------
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material", meta = (ClampMin = "1.0"))
float UVTileSize = 300.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material")
bool bUseCylindricalWallUVs = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material")
bool bUseSmoothUVWarp = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material", meta = (ClampMin = "0.0"))
float UVWarpStrength = 0.35f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material", meta = (ClampMin = "0.01"))
float UVWarpFrequency = 0.45f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material", meta = (ClampMin = "0.0"))
float UVTwistStrength = 0.18f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material", meta = (ClampMin = "0.0"))
float UVRandomOffsetStrength = 4.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Material")
TObjectPtr<UMaterialInterface> IceMaterial;
// -------------------------
// Continuous inner material veil
// -------------------------
UPROPERTY(EditAnywhere, Category = "Ice Wall|Inner Veil")
bool bGenerateInnerVeil = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Inner Veil")
TObjectPtr<UMaterialInterface> InnerVeilMaterial;
// -------------------------
// Pillar variation
// -------------------------
UPROPERTY(EditAnywhere, Category = "Ice Wall|Rendering")
bool bDoubleSidedGeometry = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Variation")
int32 RandomSeed = 12345;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Variation", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float HeightVariationPercent = 0.18f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Variation", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float RadialJitterPercent = 0.05f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Variation", meta = (ClampMin = "0.0", ClampMax = "45.0"))
float MaxYawJitter = 2.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Variation", meta = (ClampMin = "0.0", ClampMax = "30.0"))
float MaxLeanAmount = 1.5f;
// -------------------------
// Bottom detail chunks
// -------------------------
UPROPERTY(EditAnywhere, Category = "Ice Wall|Bottom Detail")
bool bGenerateBottomDetails = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Bottom Detail", meta = (ClampMin = "0"))
int32 BottomDetailCount = 450;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Bottom Detail")
float BottomDetailMinHeight = 500.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Bottom Detail")
float BottomDetailMaxHeight = 2800.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Bottom Detail")
float BottomDetailMinWidth = 250.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Bottom Detail")
float BottomDetailMaxWidth = 850.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Bottom Detail")
float BottomDetailSpread = 2600.0f;
// -------------------------
// Generation
// -------------------------
UPROPERTY(EditAnywhere, Category = "Ice Wall|Generation")
bool bGenerateInEditor = true;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Generation")
bool bGenerateAtBeginPlay = false;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Collision")
bool bEnableCollision = true;
// -------------------------
// Future lift opening
// -------------------------
UPROPERTY(EditAnywhere, Category = "Ice Wall|Lift Opening")
bool bCreateLiftOpening = false;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Lift Opening")
float LiftOpeningAngle = 0.0f;
UPROPERTY(EditAnywhere, Category = "Ice Wall|Lift Opening", meta = (ClampMin = "0.0", ClampMax = "90.0"))
float LiftOpeningWidthDegrees = 8.0f;
UFUNCTION(CallInEditor, Category = "Ice Wall")
void GenerateWall();
UFUNCTION(CallInEditor, Category = "Ice Wall")
void ClearWall();
private:
UPROPERTY()
TObjectPtr<USceneComponent> SceneRoot;
UPROPERTY()
TObjectPtr<UProceduralMeshComponent> ProceduralWall;
private:
void GenerateMainPillars(
TArray<FVector>& Vertices,
TArray<int32>& Triangles,
TArray<FVector>& Normals,
TArray<FVector2D>& UVs,
TArray<FLinearColor>& VertexColors,
TArray<FProcMeshTangent>& Tangents,
FRandomStream& Random
);
void GenerateBottomDetails(
TArray<FVector>& Vertices,
TArray<int32>& Triangles,
TArray<FVector>& Normals,
TArray<FVector2D>& UVs,
TArray<FLinearColor>& VertexColors,
TArray<FProcMeshTangent>& Tangents,
FRandomStream& Random
);
void AddOrganicRoundedBox(
TArray<FVector>& Vertices,
TArray<int32>& Triangles,
TArray<FVector>& Normals,
TArray<FVector2D>& UVs,
TArray<FLinearColor>& VertexColors,
TArray<FProcMeshTangent>& Tangents,
const FTransform& BoxTransform,
const FVector& BoxSize,
FRandomStream& Random
);
FVector2D MakeWallUV(
const FVector& WorldPos,
const FVector2D& LocalUV,
float RandomPhase
) const;
void GenerateInnerVeilFromWallMesh(
const TArray<FVector>& SourceVertices,
const TArray<int32>& SourceTriangles,
TArray<FVector>& VeilVertices,
TArray<int32>& VeilTriangles,
TArray<FVector>& VeilNormals,
TArray<FVector2D>& VeilUVs,
TArray<FLinearColor>& VeilVertexColors,
TArray<FProcMeshTangent>& VeilTangents
) const;
bool IsInsideLiftOpening(float AngleDegrees) const;
float NormalizeAngleDegrees(float Angle) const;
};
and AIceWallActor.cpp:
“AIceWallActor.h”
“Components/SceneComponent.h”
“Materials/MaterialInterface.h”
AIceWallActor::AIceWallActor()
{
PrimaryActorTick.bCanEverTick = false;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
RootComponent = SceneRoot;
ProceduralWall = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("Procedural Ice Wall"));
ProceduralWall->SetupAttachment(RootComponent);
ProceduralWall->bUseAsyncCooking = true;
bReplicates = false;
}
void AIceWallActor::BeginPlay()
{
Super::BeginPlay();
if (bGenerateAtBeginPlay)
{
GenerateWall();
}
}
#if WITH_EDITOR
void AIceWallActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (bGenerateInEditor)
{
GenerateWall();
}
}
#endif
void AIceWallActor::ClearWall()
{
if (ProceduralWall)
{
ProceduralWall->ClearAllMeshSections();
}
}
void AIceWallActor::GenerateWall()
{
if (!ProceduralWall)
{
return;
}
ClearWall();
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FLinearColor> VertexColors;
TArray<FProcMeshTangent> Tangents;
// Reserve memory to reduce editor hitching.
Vertices.Reserve(100000);
Triangles.Reserve(150000);
Normals.Reserve(100000);
UVs.Reserve(100000);
VertexColors.Reserve(100000);
Tangents.Reserve(100000);
FRandomStream Random(RandomSeed);
GenerateMainPillars(
Vertices,
Triangles,
Normals,
UVs,
VertexColors,
Tangents,
Random
);
if (bGenerateBottomDetails)
{
GenerateBottomDetails(
Vertices,
Triangles,
Normals,
UVs,
VertexColors,
Tangents,
Random
);
}
ProceduralWall->CreateMeshSection_LinearColor(
0,
Vertices,
Triangles,
Normals,
UVs,
VertexColors,
Tangents,
bEnableCollision
);
if (IceMaterial)
{
ProceduralWall->SetMaterial(0, IceMaterial);
}
if (bGenerateInnerVeil)
{
TArray<FVector> VeilVertices;
TArray<int32> VeilTriangles;
TArray<FVector> VeilNormals;
TArray<FVector2D> VeilUVs;
TArray<FLinearColor> VeilVertexColors;
TArray<FProcMeshTangent> VeilTangents;
VeilVertices.Reserve(100000);
VeilTriangles.Reserve(150000);
VeilNormals.Reserve(100000);
VeilUVs.Reserve(100000);
VeilVertexColors.Reserve(100000);
VeilTangents.Reserve(100000);
GenerateInnerVeilFromWallMesh(
Vertices,
Triangles,
VeilVertices,
VeilTriangles,
VeilNormals,
VeilUVs,
VeilVertexColors,
VeilTangents
);
ProceduralWall->CreateMeshSection_LinearColor(
1,
VeilVertices,
VeilTriangles,
VeilNormals,
VeilUVs,
VeilVertexColors,
VeilTangents,
false
);
if (InnerVeilMaterial)
{
ProceduralWall->SetMaterial(1, InnerVeilMaterial);
}
else if (IceMaterial)
{
ProceduralWall->SetMaterial(1, IceMaterial);
}
}
else
{
ProceduralWall->ClearMeshSection(1);
}
ProceduralWall->SetCollisionEnabled(
bEnableCollision ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision
);
ProceduralWall->SetCollisionObjectType(ECC_WorldStatic);
ProceduralWall->SetCanEverAffectNavigation(true);
}
void AIceWallActor::GenerateMainPillars(
TArray& Vertices,
TArray& Triangles,
TArray& Normals,
TArray& UVs,
TArray& VertexColors,
TArray& Tangents,
FRandomStream& Random
)
{
const float Circumference = 2.0f * PI * Radius;
const float SmoothConnectionOverlap = bAutoSmoothPillarConnections
? FMath::Max(PillarOverlap, PillarWidth * 0.65f)
: PillarOverlap;
const float EffectivePillarWidth = FMath::Max(100.0f, PillarWidth - SmoothConnectionOverlap);
const int32 PillarCount = FMath::Max(12, FMath::CeilToInt(Circumference / EffectivePillarWidth));
const float AngleStep = 2.0f * PI / static_cast<float>(PillarCount);
for (int32 i = 0; i < PillarCount; ++i)
{
const float Angle = i * AngleStep;
const float AngleDegrees = FMath::RadiansToDegrees(Angle);
if (IsInsideLiftOpening(AngleDegrees))
{
continue;
}
const FVector Outward = FVector(
FMath::Cos(Angle),
FMath::Sin(Angle),
0.0f
);
const float HeightMultiplier = Random.FRandRange(
1.0f - HeightVariationPercent,
1.0f + HeightVariationPercent
);
const float FinalHeight = WallHeight * HeightMultiplier;
const float RadialJitter = Random.FRandRange(
-WallThickness * RadialJitterPercent,
WallThickness * RadialJitterPercent
);
const float YawJitter = Random.FRandRange(-MaxYawJitter, MaxYawJitter);
const float LeanPitch = Random.FRandRange(-MaxLeanAmount, MaxLeanAmount);
const float LeanRoll = Random.FRandRange(-MaxLeanAmount, MaxLeanAmount);
const FVector Location =
Outward * (Radius + RadialJitter) +
FVector(0.0f, 0.0f, FinalHeight * 0.5f - GroundSinkAmount);
const float ArcWidth = (Circumference / static_cast<float>(PillarCount)) + SmoothConnectionOverlap;
const FVector BoxSize = FVector(
WallThickness,
ArcWidth,
FinalHeight
);
const FRotator Rotation(
LeanPitch,
AngleDegrees + YawJitter,
LeanRoll
);
const FTransform BoxTransform(
Rotation,
Location,
FVector(1.0f)
);
AddOrganicRoundedBox(
Vertices,
Triangles,
Normals,
UVs,
VertexColors,
Tangents,
BoxTransform,
BoxSize,
Random
);
}
}
void AIceWallActor::GenerateBottomDetails(
TArray& Vertices,
TArray& Triangles,
TArray& Normals,
TArray& UVs,
TArray& VertexColors,
TArray& Tangents,
FRandomStream& Random
)
{
for (int32 i = 0; i < BottomDetailCount; ++i)
{
const float Angle = Random.FRandRange(0.0f, 2.0f * PI);
const float AngleDegrees = FMath::RadiansToDegrees(Angle);
if (IsInsideLiftOpening(AngleDegrees))
{
continue;
}
const FVector Outward = FVector(
FMath::Cos(Angle),
FMath::Sin(Angle),
0.0f
);
const float RandomRadiusOffset = Random.FRandRange(
-BottomDetailSpread,
BottomDetailSpread
);
const float Height = Random.FRandRange(
BottomDetailMinHeight,
BottomDetailMaxHeight
);
const float Width = Random.FRandRange(
BottomDetailMinWidth,
BottomDetailMaxWidth
);
const float Depth = Random.FRandRange(
BottomDetailMinWidth,
BottomDetailMaxWidth * 1.6f
);
const FVector Location =
Outward * (Radius + RandomRadiusOffset) +
FVector(0.0f, 0.0f, Height * 0.5f - GroundSinkAmount);
const FRotator Rotation(
Random.FRandRange(-12.0f, 12.0f),
AngleDegrees + Random.FRandRange(-30.0f, 30.0f),
Random.FRandRange(-12.0f, 12.0f)
);
const FVector BoxSize = FVector(
Depth,
Width,
Height
);
const FTransform BoxTransform(
Rotation,
Location,
FVector(1.0f)
);
AddOrganicRoundedBox(
Vertices,
Triangles,
Normals,
UVs,
VertexColors,
Tangents,
BoxTransform,
BoxSize,
Random
);
}
}
FVector2D AIceWallActor::MakeWallUV(
const FVector& WorldPos,
const FVector2D& LocalUV,
float RandomPhase
) const
{
const float SafeUVTileSize = FMath::Max(UVTileSize, 1.0f);
if (!bUseCylindricalWallUVs)
{
FVector2D UV = LocalUV;
if (bUseSmoothUVWarp)
{
UV.X += FMath::Sin(UV.Y * UVWarpFrequency + RandomPhase) * UVWarpStrength;
UV.Y += FMath::Sin(UV.X * UVWarpFrequency + RandomPhase * 1.7f) * UVWarpStrength;
}
return UV;
}
const float Angle = FMath::Atan2(WorldPos.Y, WorldPos.X);
float U =
(NormalizeAngleDegrees(FMath::RadiansToDegrees(Angle)) / 360.0f) *
((2.0f * PI * Radius) / SafeUVTileSize);
float V = WorldPos.Z / SafeUVTileSize;
if (bUseSmoothUVWarp)
{
const float HeightAlpha = FMath::Clamp(
(WorldPos.Z + GroundSinkAmount) / FMath::Max(WallHeight, 1.0f),
0.0f,
1.0f
);
const float SmoothTwist = FMath::Sin(HeightAlpha * PI) * UVTwistStrength;
U += FMath::Sin(V * UVWarpFrequency + RandomPhase) * UVWarpStrength;
V += FMath::Sin(U * UVWarpFrequency * 0.65f + RandomPhase * 1.37f) * UVWarpStrength;
U += HeightAlpha * SmoothTwist * 10.0f;
U += FMath::Sin(RandomPhase) * UVRandomOffsetStrength;
V += FMath::Cos(RandomPhase * 0.73f) * UVRandomOffsetStrength;
}
return FVector2D(U, V);
}
void AIceWallActor::AddOrganicRoundedBox(
TArray& Vertices,
TArray& Triangles,
TArray& Normals,
TArray& UVs,
TArray& VertexColors,
TArray& Tangents,
const FTransform& BoxTransform,
const FVector& BoxSize,
FRandomStream& Random
)
{
const FVector Half = BoxSize * 0.5f;
const int32 RingSegments = FMath::Clamp(OrganicRingSegments, 16, 128);
const float SafeUVTileSize = FMath::Max(UVTileSize, 1.0f);
const float RandomPhase = Random.FRandRange(0.0f, 10000.0f);
/*
Superellipse shape:
EdgeRoundness close to 0 = more box-like.
EdgeRoundness close to 1 = smoother / rounder.
*/
const float SuperEllipsePower = FMath::Lerp(7.5f, 2.2f, EdgeRoundness);
TArray<FVector> BottomRing;
TArray<FVector> TopRing;
TArray<FVector> SideNormals;
BottomRing.Reserve(RingSegments);
TopRing.Reserve(RingSegments);
SideNormals.Reserve(RingSegments);
for (int32 i = 0; i < RingSegments; ++i)
{
const float T = static_cast<float>(i) / static_cast<float>(RingSegments);
const float Angle = T * 2.0f * PI;
const float C = FMath::Cos(Angle);
const float S = FMath::Sin(Angle);
const float SignC = C >= 0.0f ? 1.0f : -1.0f;
const float SignS = S >= 0.0f ? 1.0f : -1.0f;
const float AbsC = FMath::Abs(C);
const float AbsS = FMath::Abs(S);
float X = SignC * Half.X * FMath::Pow(AbsC, 2.0f / SuperEllipsePower);
float Y = SignS * Half.Y * FMath::Pow(AbsS, 2.0f / SuperEllipsePower);
/*
Smooth inward/outward organic shape.
This stops every pillar connection from looking identical.
*/
const float OrganicA = FMath::Sin(Angle * 3.0f + RandomPhase);
const float OrganicB = FMath::Sin(Angle * 5.0f + RandomPhase * 0.37f);
const float OrganicC = FMath::Sin(Angle * 9.0f + RandomPhase * 0.11f);
const float OrganicMix =
OrganicA * 0.55f +
OrganicB * 0.30f +
OrganicC * 0.15f;
const float SmoothVerticalBias =
0.65f + 0.35f * FMath::Sin(Angle + RandomPhase * 0.21f);
const float OrganicOffset =
OrganicMix * OrganicSurfaceAmount * SmoothVerticalBias;
FVector2D Direction2D(X, Y);
if (!Direction2D.IsNearlyZero())
{
Direction2D.Normalize();
X += Direction2D.X * OrganicOffset;
Y += Direction2D.Y * OrganicOffset;
}
/*
Top and bottom get smooth variation too,
so they do not look like perfectly cut boxes.
*/
const float TopWave =
FMath::Sin(Angle * 2.0f + RandomPhase * 0.19f) * 0.55f +
FMath::Sin(Angle * 6.0f + RandomPhase * 0.41f) * 0.30f +
FMath::Sin(Angle * 11.0f + RandomPhase * 0.07f) * 0.15f;
const float BottomWave =
FMath::Sin(Angle * 3.0f + RandomPhase * 0.29f) * 0.6f +
FMath::Sin(Angle * 7.0f + RandomPhase * 0.13f) * 0.4f;
const float TopZ = Half.Z + TopWave * OrganicTopVariation;
const float BottomZ = -Half.Z + BottomWave * OrganicBottomVariation;
BottomRing.Add(FVector(X, Y, BottomZ));
TopRing.Add(FVector(X, Y, TopZ));
FVector LocalNormal(
X / FMath::Max(Half.X, 1.0f),
Y / FMath::Max(Half.Y, 1.0f),
0.0f
);
LocalNormal.Normalize();
SideNormals.Add(LocalNormal);
}
auto AddVertex = [&](
const FVector& LocalPos,
const FVector& LocalNormal,
const FVector2D& LocalUV
)
{
const FVector WorldPos = BoxTransform.TransformPosition(LocalPos);
FVector NormalToUse = LocalNormal;
if (bSmoothOrganicNormals)
{
NormalToUse = LocalNormal.GetSafeNormal();
}
const FVector WorldNormal =
BoxTransform.TransformVectorNoScale(NormalToUse).GetSafeNormal();
const float HeightAlpha = FMath::Clamp(
(WorldPos.Z + GroundSinkAmount) / FMath::Max(WallHeight, 1.0f),
0.0f,
1.0f
);
const float SnowMask =
FMath::Clamp((HeightAlpha - 0.72f) / 0.28f, 0.0f, 1.0f);
const float VerticalFaceMask =
FMath::Clamp(
FMath::Abs(LocalNormal.X) + FMath::Abs(LocalNormal.Y),
0.0f,
1.0f
);
const float BottomMask = HeightAlpha < 0.18f ? 1.0f : 0.0f;
/*
Vertex color meaning:
R = top snow / high area mask
G = blue glowing seam / crack boost mask
B = edge highlight mask
A = bottom darkening/detail mask
*/
const FLinearColor VC(
SnowMask,
VerticalFaceMask,
VerticalFaceMask,
BottomMask
);
Vertices.Add(WorldPos);
Normals.Add(WorldNormal);
UVs.Add(MakeWallUV(WorldPos, LocalUV, RandomPhase));
VertexColors.Add(VC);
const FVector TangentDirection =
BoxTransform.TransformVectorNoScale(FVector(0.0f, 1.0f, 0.0f)).GetSafeNormal();
Tangents.Add(FProcMeshTangent(TangentDirection, false));
};
auto AddTriangle = [&](int32 A, int32 B, int32 C)
{
Triangles.Add(A);
Triangles.Add(B);
Triangles.Add(C);
if (bDoubleSidedGeometry)
{
Triangles.Add(C);
Triangles.Add(B);
Triangles.Add(A);
}
};
// -------------------------
// Organic side surface
// -------------------------
for (int32 i = 0; i < RingSegments; ++i)
{
const int32 Next = (i + 1) % RingSegments;
const int32 StartIndex = Vertices.Num();
const FVector A = BottomRing[i];
const FVector B = BottomRing[Next];
const FVector C = TopRing[Next];
const FVector D = TopRing[i];
const FVector NormalA = SideNormals[i];
const FVector NormalB = SideNormals[Next];
const float USize = FVector::Distance(A, B) / SafeUVTileSize;
const float VSize = FVector::Distance(A, D) / SafeUVTileSize;
AddVertex(A, NormalA, FVector2D(0.0f, 0.0f));
AddVertex(B, NormalB, FVector2D(USize, 0.0f));
AddVertex(C, NormalB, FVector2D(USize, VSize));
AddVertex(D, NormalA, FVector2D(0.0f, VSize));
AddTriangle(StartIndex + 0, StartIndex + 1, StartIndex + 2);
AddTriangle(StartIndex + 0, StartIndex + 2, StartIndex + 3);
}
// -------------------------
// Smooth top cap
// -------------------------
FVector TopCenter(0.0f, 0.0f, Half.Z);
for (const FVector& P : TopRing)
{
TopCenter.Z += P.Z;
}
TopCenter.Z /= static_cast<float>(TopRing.Num() + 1);
for (int32 i = 0; i < RingSegments; ++i)
{
const int32 Next = (i + 1) % RingSegments;
const int32 StartIndex = Vertices.Num();
AddVertex(
TopCenter,
FVector(0.0f, 0.0f, 1.0f),
FVector2D(0.5f, 0.5f)
);
AddVertex(
TopRing[i],
FVector(0.0f, 0.0f, 1.0f),
FVector2D(TopRing[i].X / SafeUVTileSize, TopRing[i].Y / SafeUVTileSize)
);
AddVertex(
TopRing[Next],
FVector(0.0f, 0.0f, 1.0f),
FVector2D(TopRing[Next].X / SafeUVTileSize, TopRing[Next].Y / SafeUVTileSize)
);
AddTriangle(StartIndex + 0, StartIndex + 1, StartIndex + 2);
}
// -------------------------
// Smooth bottom cap
// -------------------------
FVector BottomCenter(0.0f, 0.0f, -Half.Z);
for (const FVector& P : BottomRing)
{
BottomCenter.Z += P.Z;
}
BottomCenter.Z /= static_cast<float>(BottomRing.Num() + 1);
for (int32 i = 0; i < RingSegments; ++i)
{
const int32 Next = (i + 1) % RingSegments;
const int32 StartIndex = Vertices.Num();
AddVertex(
BottomCenter,
FVector(0.0f, 0.0f, -1.0f),
FVector2D(0.5f, 0.5f)
);
AddVertex(
BottomRing[Next],
FVector(0.0f, 0.0f, -1.0f),
FVector2D(BottomRing[Next].X / SafeUVTileSize, BottomRing[Next].Y / SafeUVTileSize)
);
AddVertex(
BottomRing[i],
FVector(0.0f, 0.0f, -1.0f),
FVector2D(BottomRing[i].X / SafeUVTileSize, BottomRing[i].Y / SafeUVTileSize)
);
AddTriangle(StartIndex + 0, StartIndex + 1, StartIndex + 2);
}
}
void AIceWallActor::GenerateInnerVeilFromWallMesh(
const TArray& SourceVertices,
const TArray& SourceTriangles,
TArray& VeilVertices,
TArray& VeilTriangles,
TArray& VeilNormals,
TArray& VeilUVs,
TArray& VeilVertexColors,
TArray& VeilTangents
) const
{
const float SafeUVTileSize = FMath::Max(UVTileSize, 1.0f);
/*
Small hardcoded offset so the veil does not z-fight with the wall.
*/
const float SkinOffset = 10.0f;
auto AddVeilVertex = [&](
const FVector& SourcePos,
const FVector& InwardDirection,
float HeightAlpha
) -> int32
{
const FVector FinalPos = SourcePos + InwardDirection * SkinOffset;
const int32 NewIndex = VeilVertices.Num();
VeilVertices.Add(FinalPos);
VeilNormals.Add(InwardDirection);
/*
Continuous cylindrical UVs.
This does not reset per pillar.
*/
const float Angle = FMath::Atan2(FinalPos.Y, FinalPos.X);
const float U =
(NormalizeAngleDegrees(FMath::RadiansToDegrees(Angle)) / 360.0f) *
((2.0f * PI * Radius) / SafeUVTileSize);
const float V = FinalPos.Z / SafeUVTileSize;
VeilUVs.Add(FVector2D(U, V));
const float SnowMask =
FMath::Clamp((HeightAlpha - 0.72f) / 0.28f, 0.0f, 1.0f);
const float BottomMask = HeightAlpha < 0.18f ? 1.0f : 0.0f;
VeilVertexColors.Add(FLinearColor(
SnowMask,
0.25f,
1.0f,
BottomMask
));
const FVector TangentDirection(
-InwardDirection.Y,
InwardDirection.X,
0.0f
);
VeilTangents.Add(FProcMeshTangent(TangentDirection.GetSafeNormal(), false));
return NewIndex;
};
auto AddTriangle = [&](int32 A, int32 B, int32 C)
{
VeilTriangles.Add(A);
VeilTriangles.Add(B);
VeilTriangles.Add(C);
if (bDoubleSidedGeometry)
{
VeilTriangles.Add(C);
VeilTriangles.Add(B);
VeilTriangles.Add(A);
}
};
for (int32 TriIndex = 0; TriIndex + 2 < SourceTriangles.Num(); TriIndex += 3)
{
const int32 IA = SourceTriangles[TriIndex + 0];
const int32 IB = SourceTriangles[TriIndex + 1];
const int32 IC = SourceTriangles[TriIndex + 2];
if (!SourceVertices.IsValidIndex(IA) ||
!SourceVertices.IsValidIndex(IB) ||
!SourceVertices.IsValidIndex(IC))
{
continue;
}
const FVector A = SourceVertices[IA];
const FVector B = SourceVertices[IB];
const FVector C = SourceVertices[IC];
const FVector Center = (A + B + C) / 3.0f;
FVector RadialOutward(Center.X, Center.Y, 0.0f);
if (RadialOutward.IsNearlyZero())
{
continue;
}
RadialOutward.Normalize();
const FVector RadialInward = -RadialOutward;
const FVector TriangleNormal =
FVector::CrossProduct(B - A, C - A).GetSafeNormal();
/*
Ignore top and bottom caps.
We only want the vertical inside wall skin.
*/
if (FMath::Abs(TriangleNormal.Z) > 0.55f)
{
continue;
}
/*
Keep only triangles facing the island / inside of the wall.
This is the important scan step.
*/
const float InsideFacingAmount =
FVector::DotProduct(TriangleNormal, RadialInward);
if (InsideFacingAmount < 0.25f)
{
continue;
}
/*
Avoid catching random bottom detail chunks far away from the main wall.
*/
const float CenterRadius = FVector(Center.X, Center.Y, 0.0f).Size();
const float ExpectedInnerRadius = Radius - WallThickness * 0.5f;
const float AllowedDistance = WallThickness * 1.25f;
if (FMath::Abs(CenterRadius - ExpectedInnerRadius) > AllowedDistance)
{
continue;
}
const float HeightAlphaA = FMath::Clamp(
(A.Z + GroundSinkAmount) / FMath::Max(WallHeight, 1.0f),
0.0f,
1.0f
);
const float HeightAlphaB = FMath::Clamp(
(B.Z + GroundSinkAmount) / FMath::Max(WallHeight, 1.0f),
0.0f,
1.0f
);
const float HeightAlphaC = FMath::Clamp(
(C.Z + GroundSinkAmount) / FMath::Max(WallHeight, 1.0f),
0.0f,
1.0f
);
const int32 VA = AddVeilVertex(A, RadialInward, HeightAlphaA);
const int32 VB = AddVeilVertex(B, RadialInward, HeightAlphaB);
const int32 VC = AddVeilVertex(C, RadialInward, HeightAlphaC);
/*
Reverse winding so the veil surface faces inward.
*/
AddTriangle(VA, VC, VB);
}
}
bool AIceWallActor::IsInsideLiftOpening(float AngleDegrees) const
{
if (!bCreateLiftOpening || LiftOpeningWidthDegrees <= 0.0f)
{
return false;
}
const float A = NormalizeAngleDegrees(AngleDegrees);
const float B = NormalizeAngleDegrees(LiftOpeningAngle);
float Difference = FMath::Abs(A - B);
if (Difference > 180.0f)
{
Difference = 360.0f - Difference;
}
return Difference <= LiftOpeningWidthDegrees * 0.5f;
}
float AIceWallActor::NormalizeAngleDegrees(float Angle) const
{
float Result = FMath::Fmod(Angle, 360.0f);
if (Result < 0.0f)
{
Result += 360.0f;
}
return Result;
}
For the variables, you’ll need to modify them to get the same result as my wall, mainly the max lean amount.
Thanks again