Ice Wall: Open meshes

In a previous post, I asked how to create a circular ice wall that also acts as the boundary of my map, but no one answered. So I tried doing it myself, and I kind of succeeded.

And here’s the code—thanks to ChatGPT!

IceWallRing.cpp:


 “IceWallRing.h”


 “Components/SplineComponent.h”

 “Components/SplineMeshComponent.h”

 “Engine/StaticMesh.h”

AIceWallRing::AIceWallRing()
{
PrimaryActorTick.bCanEverTick = false;

Spline = CreateDefaultSubobject<USplineComponent>(TEXT("Spline"));
Spline->SetMobility(EComponentMobility::Movable);
RootComponent = Spline;

}

void AIceWallRing::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);

// Editor preview seed
const int32 PreviewSeed = RandomSeed == 0 ? 12345 : RandomSeed;
BuildWall(PreviewSeed);

}

void AIceWallRing::BeginPlay()
{
Super::BeginPlay();

// New game / new play session seed
RuntimeSeed = RandomSeed == 0 ? FMath::Rand() : RandomSeed;

BuildWall(RuntimeSeed);

}

void AIceWallRing::ClearWallMeshes()
{
TArray<USplineMeshComponent*> OldMeshes;
GetComponents(OldMeshes);

for (USplineMeshComponent* OldMesh : OldMeshes)
{
    if (OldMesh)
    {
        OldMesh->DestroyComponent();
    }
}

}

void AIceWallRing::BuildWall(int32 Seed)
{
if (!Spline || WallMeshes.Num() == 0 || PointCount < 3)
{
return;
}

ClearWallMeshes();

Spline->ClearSplinePoints(false);

// Create circle points
for (int32 i = 0; i < PointCount; i++)
{
    const float Angle = ((float)i / (float)PointCount) * 360.f;

    const float X = FMath::Cos(FMath::DegreesToRadians(Angle)) * Radius;
    const float Y = FMath::Sin(FMath::DegreesToRadians(Angle)) * Radius;

    Spline->AddSplinePoint(FVector(X, Y, 0.f), ESplineCoordinateSpace::Local, false);
}

Spline->SetClosedLoop(true, false);
Spline->UpdateSpline();

FRandomStream RandomStream(Seed);

// Create wall segments
for (int32 i = 0; i < PointCount; i++)
{
    const int32 NextIndex = (i + 1) % PointCount;

    const FVector Start = Spline->GetLocationAtSplinePoint(i, ESplineCoordinateSpace::Local);
    const FVector End = Spline->GetLocationAtSplinePoint(NextIndex, ESplineCoordinateSpace::Local);

    const FVector StartTangent =
        Spline->GetTangentAtSplinePoint(i, ESplineCoordinateSpace::Local) * 0.25f;

    const FVector EndTangent =
        Spline->GetTangentAtSplinePoint(NextIndex, ESplineCoordinateSpace::Local) * 0.25f;

    USplineMeshComponent* Mesh = NewObject<USplineMeshComponent>(this);
    Mesh->CreationMethod = EComponentCreationMethod::UserConstructionScript;
    Mesh->SetMobility(EComponentMobility::Movable);

    int32 MeshIndex = RandomStream.RandRange(0, WallMeshes.Num() - 1);
    Mesh->SetStaticMesh(WallMeshes[MeshIndex]);
    Mesh->SetForwardAxis(ESplineMeshAxis::X);

    // BLOCK PLAYER
    Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    Mesh->SetCollisionResponseToAllChannels(ECR_Block);

    Mesh->AttachToComponent(Spline, FAttachmentTransformRules::KeepRelativeTransform);
    Mesh->RegisterComponent();

    Mesh->SetStartAndEnd(Start, StartTangent, End, EndTangent, true);
}

}

IceWallRing.h:

#pragma once


 “CoreMinimal.h”

 “GameFramework/Actor.h”

 “IceWallRing.generated.h”

class USplineComponent;
class UStaticMesh;

UCLASS()
class ICEWALLPROJECT_API AIceWallRing : public AActor
{
GENERATED_BODY()

public:
AIceWallRing();

virtual void OnConstruction(const FTransform& Transform) override;
virtual void BeginPlay() override;

private:
void BuildWall(int32 Seed);
void ClearWallMeshes();

public:
UPROPERTY(EditAnywhere, Category = “Ice Wall”)
float Radius = 1500.f;

UPROPERTY(EditAnywhere, Category = "Ice Wall")
int32 PointCount = 8;

UPROPERTY(EditAnywhere, Category = "Ice Wall")
TArray<UStaticMesh*> WallMeshes;

// 0 = generate a new seed when the game starts
UPROPERTY(EditAnywhere, Category = "Ice Wall|Random")
int32 RandomSeed = 0;

private:
UPROPERTY()
USplineComponent* Spline = nullptr;

int32 RuntimeSeed = 0;

};

Now, as you can see, it works, but it’s also a total failure. Editing the shape of the wall meshes causes them to spawn distorted and broken. Whatever material I use, it appears repetitive—especially since my wall has a radius of 50,000 and a point count of 275, and the wall meshes have a height of 50,000. I did, however, buy the following meshes:

So here’s the new plan: use these amazing (and expensive) open meshes as a second layer on top of my wall, blending them with it to achieve the desired look (like the Game of Thrones wall of ice). But I’m extremely lazy and don’t have the patience to design all of this along the entire wall. Is there a way to automate it, or should I come up with another plan?

PLEASE HELPPPPP

Basically, get one good rock, that you blow up to 100 times it’s normal size, and the geometry still looks ok, and either write a blueprint to place them, or use PCG.

You put them in a circle, but set their rotations as random on all axes, that will give a pretty good ice wall effect.

Also, you might need to re-code the material to use world aligned textures, to avoid them becoming too low res.

Hey there @cherbzzz! I agree with Clockwork here, and I would recommend PCG as well. You can use it to spread meshes along this wall relatively easily.

Here’s a PCG course leading with a video relevant for specifically placing objects on meshes in a controlled way:

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:

  1. the pillars that are generated, aligned, and rotated
  2. 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)
  3. 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:

  1. light and shadow break at abrupt transitions (like in the previous image)
  2. 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

@SupportiveEntity Thank you very much to you too

Ok, great. This bit looks like UV problems