Skip to content

Instantly share code, notes, and snippets.

@MineClever
Forked from luttje/SYourLandscapeModule.cpp
Created January 14, 2025 17:22
Show Gist options
  • Save MineClever/81f01a4e59c76ebcac1cea460a619bd2 to your computer and use it in GitHub Desktop.
Save MineClever/81f01a4e59c76ebcac1cea460a619bd2 to your computer and use it in GitHub Desktop.
Unreal Engine 5 - Programatically create a landscape with spline points and segments
#include "LandscapeSubsystem.h"
#include "LandscapeComponent.h"
#include "LandscapeSplineActor.h"
#include "LandscapeSplinesComponent.h"
#include "LandscapeSplineControlPoint.h"
#include "Editor/LandscapeEditor/Private/LandscapeEdMode.h"
// Source: https://forums.unrealengine.com/t/creating-a-lanscape-using-c-or-the-python-api/504997/4
ALandscape* CreateLandscape(const FTransform& LandscapeTransform, const int32& SectionSize, const int32& SectionsPerComponent, const int32& ComponentCountX, const int32& ComponentCountY)
{
int32 QuadsPerComponent = SectionSize * SectionsPerComponent;
int32 SizeX = ComponentCountX * QuadsPerComponent + 1;
int32 SizeY = ComponentCountY * QuadsPerComponent + 1;
TArray<FLandscapeImportLayerInfo> MaterialImportLayers;
MaterialImportLayers.Reserve(0);
TMap<FGuid, TArray<uint16>> HeightDataPerLayers;
TMap<FGuid, TArray<FLandscapeImportLayerInfo>> MaterialLayerDataPerLayers;
TArray<uint16> HeightData;
HeightData.SetNum(SizeX * SizeY);
for (int32 i = 0; i < HeightData.Num(); i++)
{
HeightData[i] = 32768;
}
HeightDataPerLayers.Add(FGuid(), MoveTemp(HeightData)); /*ENewLandscapePreviewMode.NewLandscape*/
// ComputeHeightData will also modify/expand material layers data, which is why we create MaterialLayerDataPerLayers after calling ComputeHeightData
MaterialLayerDataPerLayers.Add(FGuid(), MoveTemp(MaterialImportLayers));
//FScopedTransaction Transaction(TEXT("Undo", "Creating New Landscape"));
UWorld* World = GEditor->GetEditorWorldContext().World();
ALandscape* Landscape = World->SpawnActor<ALandscape>();
Landscape->bCanHaveLayersContent = false;
Landscape->LandscapeMaterial = nullptr;
Landscape->SetActorTransform(LandscapeTransform);
// automatically calculate a lighting LOD that won't crash lightmass (hopefully)
// < 2048x2048 -> LOD0
// >=2048x2048 -> LOD1
// >= 4096x4096 -> LOD2
// >= 8192x8192 -> LOD3
//const FGuid& InGuid, int32 InMinX, int32 InMinY, int32 InMaxX, int32 InMaxY, int32 InNumSubsections, int32 InSubsectionSizeQuads, const TMap<FGuid, TArray<uint16>>& InImportHeightData,
// const TCHAR* const InHeightmapFileName, const TMap<FGuid, TArray<FLandscapeImportLayerInfo>>& InImportMaterialLayerInfos, ELandscapeImportAlphamapType InImportMaterialLayerType, const TArray<struct FLandscapeLayer>* InImportLayers = nullptr
Landscape->Import(FGuid::NewGuid(), 0, 0, SizeX - 1, SizeY - 1, SectionsPerComponent, QuadsPerComponent, HeightDataPerLayers, nullptr, MaterialLayerDataPerLayers, ELandscapeImportAlphamapType::Additive);
Landscape->StaticLightingLOD = FMath::DivideAndRoundUp(FMath::CeilLogTwo((SizeX * SizeY) / (2048 * 2048) + 1), (uint32)2);
// Register all the landscape components
ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo();
LandscapeInfo->UpdateLayerInfoMap(Landscape);
Landscape->RegisterAllComponents();
// Need to explicitly call PostEditChange on the LandscapeMaterial property or the landscape proxy won't update its material
FPropertyChangedEvent MaterialPropertyChangedEvent(FindFieldChecked< FProperty >(Landscape->GetClass(), FName("LandscapeMaterial")));
Landscape->PostEditChangeProperty(MaterialPropertyChangedEvent);
Landscape->PostEditChange();
return Landscape;
}
// Copied mostly from <UE 5.3 Source>\Engine\Source\Editor\LandscapeEditor\Private\LandscapeEdModeSplineTools.cpp
void AddSegment(ULandscapeSplineControlPoint* Start, ULandscapeSplineControlPoint* End, bool bAutoRotateStart, bool bAutoRotateEnd)
{
if (Start == End)
{
//UE_LOG( TEXT("Can't join spline control point to itself.") );
return;
}
if (Start->GetOuterULandscapeSplinesComponent() != End->GetOuterULandscapeSplinesComponent())
{
//UE_LOG( TEXT("Can't join spline control points across different terrains.") );
return;
}
for (const FLandscapeSplineConnection& Connection : Start->ConnectedSegments)
{
// if the *other* end on the connected segment connects to the "end" control point...
if (Connection.GetFarConnection().ControlPoint == End)
{
//UE_LOG( TEXT("Spline control points already joined connected!") );
return;
}
}
ULandscapeSplinesComponent* SplinesComponent = Start->GetOuterULandscapeSplinesComponent();
SplinesComponent->Modify();
Start->Modify();
End->Modify();
ULandscapeSplineSegment* NewSegment = NewObject<ULandscapeSplineSegment>(SplinesComponent, NAME_None, RF_Transactional);
TArray<TObjectPtr<ULandscapeSplineSegment>>& Segments = SplinesComponent->GetSegments();
Segments.Add(NewSegment);
NewSegment->Connections[0].ControlPoint = Start;
NewSegment->Connections[1].ControlPoint = End;
NewSegment->Connections[0].SocketName = Start->GetBestConnectionTo(End->Location);
NewSegment->Connections[1].SocketName = End->GetBestConnectionTo(Start->Location);
FVector StartLocation; FRotator StartRotation;
Start->GetConnectionLocationAndRotation(NewSegment->Connections[0].SocketName, StartLocation, StartRotation);
FVector EndLocation; FRotator EndRotation;
End->GetConnectionLocationAndRotation(NewSegment->Connections[1].SocketName, EndLocation, EndRotation);
// Set up tangent lengths
NewSegment->Connections[0].TangentLen = static_cast<float>((EndLocation - StartLocation).Size());
NewSegment->Connections[1].TangentLen = NewSegment->Connections[0].TangentLen;
NewSegment->AutoFlipTangents();
// set up other segment options
ULandscapeSplineSegment* CopyFromSegment = nullptr;
if (Start->ConnectedSegments.Num() > 0)
{
CopyFromSegment = Start->ConnectedSegments[0].Segment;
}
else if (End->ConnectedSegments.Num() > 0)
{
CopyFromSegment = End->ConnectedSegments[0].Segment;
}
else
{
// Use defaults
}
if (CopyFromSegment != nullptr)
{
NewSegment->LayerName = CopyFromSegment->LayerName;
NewSegment->SplineMeshes = CopyFromSegment->SplineMeshes;
NewSegment->LDMaxDrawDistance = CopyFromSegment->LDMaxDrawDistance;
NewSegment->bRaiseTerrain = CopyFromSegment->bRaiseTerrain;
NewSegment->bLowerTerrain = CopyFromSegment->bLowerTerrain;
NewSegment->bPlaceSplineMeshesInStreamingLevels = CopyFromSegment->bPlaceSplineMeshesInStreamingLevels;
NewSegment->BodyInstance = CopyFromSegment->BodyInstance;
NewSegment->bCastShadow = CopyFromSegment->bCastShadow;
NewSegment->TranslucencySortPriority = CopyFromSegment->TranslucencySortPriority;
NewSegment->RuntimeVirtualTextures = CopyFromSegment->RuntimeVirtualTextures;
NewSegment->VirtualTextureLodBias = CopyFromSegment->VirtualTextureLodBias;
NewSegment->VirtualTextureCullMips = CopyFromSegment->VirtualTextureCullMips;
NewSegment->VirtualTextureRenderPassType = CopyFromSegment->VirtualTextureRenderPassType;
NewSegment->bRenderCustomDepth = CopyFromSegment->bRenderCustomDepth;
NewSegment->CustomDepthStencilWriteMask = CopyFromSegment->CustomDepthStencilWriteMask;
NewSegment->CustomDepthStencilValue = CopyFromSegment->CustomDepthStencilValue;
}
Start->ConnectedSegments.Add(FLandscapeSplineConnection(NewSegment, 0));
End->ConnectedSegments.Add(FLandscapeSplineConnection(NewSegment, 1));
bool bUpdatedStart = false;
bool bUpdatedEnd = false;
if (bAutoRotateStart)
{
Start->AutoCalcRotation();
Start->UpdateSplinePoints();
bUpdatedStart = true;
}
if (bAutoRotateEnd)
{
End->AutoCalcRotation();
End->UpdateSplinePoints();
bUpdatedEnd = true;
}
// Control points' points are currently based on connected segments, so need to be updated.
if (!bUpdatedStart && Start->Mesh)
{
Start->UpdateSplinePoints();
}
if (!bUpdatedEnd && End->Mesh)
{
End->UpdateSplinePoints();
}
// If we've called UpdateSplinePoints on either control point it will already have called UpdateSplinePoints on the new segment
if (!(bUpdatedStart || bUpdatedEnd))
{
NewSegment->UpdateSplinePoints();
}
}
// Copied mostly from <UE 5.3 Source>\Engine\Source\Editor\LandscapeEditor\Private\LandscapeEdModeSplineTools.cpp
void AddControlPoint(ALandscape* landscape, ULandscapeSplinesComponent* SplinesComponent, const FVector& LocalLocation)
{
bool bCopyMeshToNewControlPoint = false;
bool bDuplicatingControlPoint = false;
bool bAutoRotateOnJoin = true;
SplinesComponent->Modify();
ULandscapeSplineControlPoint* NewControlPoint = NewObject<ULandscapeSplineControlPoint>(SplinesComponent, NAME_None, RF_Transactional);
TArray<TObjectPtr<ULandscapeSplineControlPoint>>& ControlPoints = SplinesComponent->GetControlPoints();
int32 ControlPointCount = ControlPoints.Num();
ControlPoints.Add(NewControlPoint);
NewControlPoint->Location = LocalLocation;
if (ControlPointCount > 0)
{
ULandscapeSplineControlPoint* PreviousPoint = ControlPoints[ControlPointCount - 1];
FVector NewSegmentDirection = (NewControlPoint->Location - PreviousPoint->Location) * (PreviousPoint->ConnectedSegments.Num() == 0 || PreviousPoint->ConnectedSegments[0].End ? 1.0f : -1.0f);
NewControlPoint->Rotation = NewSegmentDirection.Rotation();
NewControlPoint->Width = PreviousPoint->Width;
NewControlPoint->LayerWidthRatio = PreviousPoint->LayerWidthRatio;
NewControlPoint->SideFalloff = PreviousPoint->SideFalloff;
NewControlPoint->LeftSideFalloffFactor = PreviousPoint->LeftSideFalloffFactor;
NewControlPoint->RightSideFalloffFactor = PreviousPoint->RightSideFalloffFactor;
NewControlPoint->LeftSideLayerFalloffFactor = PreviousPoint->LeftSideLayerFalloffFactor;
NewControlPoint->RightSideLayerFalloffFactor = PreviousPoint->RightSideLayerFalloffFactor;
NewControlPoint->EndFalloff = PreviousPoint->EndFalloff;
if (bCopyMeshToNewControlPoint)
{
NewControlPoint->Mesh = PreviousPoint->Mesh;
NewControlPoint->MeshScale = PreviousPoint->MeshScale;
NewControlPoint->bPlaceSplineMeshesInStreamingLevels = PreviousPoint->bPlaceSplineMeshesInStreamingLevels;
NewControlPoint->BodyInstance = PreviousPoint->BodyInstance;
NewControlPoint->bCastShadow = PreviousPoint->bCastShadow;
}
// Get the last control point and add a segment between it and the new control point
AddSegment(PreviousPoint, NewControlPoint, bAutoRotateOnJoin, !bDuplicatingControlPoint);
}
else
{
// required to make control point visible
NewControlPoint->UpdateSplinePoints();
}
if (!SplinesComponent->IsRegistered())
{
SplinesComponent->RegisterComponent();
}
else
{
SplinesComponent->MarkRenderStateDirty();
}
if (landscape && landscape->GetLandscapeSplinesReservedLayer())
{
// TODO: Only update dirty regions
landscape->RequestSplineLayerUpdate();
}
}
/*
* This function will create a landscape with spline points.
*/
void CreateLandscapeWithSplinePoints()
{
UWorld* world = GEditor->GetEditorWorldContext().World();
int32 sectionSize = 1;
TArray<FVector> splinePoints;
splinePoints.Add(FVector(0, 0, 0));
splinePoints.Add(FVector(0, 1000, 0));
splinePoints.Add(FVector(1000, 1000, 0));
splinePoints.Add(FVector(1000, 0, 0));
ALandscape* landscape = CreateLandscape(
FTransform3d(),
63, // Section size and count
sectionSize,
8, // Component counts X and Y
8
);
ULandscapeInfo* landscapeInfo = landscape->GetLandscapeInfo();
uint32 worldPartitionGridSize = sectionSize;
// Triggers the setup of streaming proxies, except when WorldPartition is not enabled
world->GetSubsystem<ULandscapeSubsystem>()->ChangeGridSize(landscapeInfo, worldPartitionGridSize);
// Add the splines to the first landscapecomponent
TSet<ULandscapeComponent*> landscapeComponents;
landscapeInfo->GetComponentsInRegion(0, 0, 1, 1, landscapeComponents);
// The first landscape component
ULandscapeComponent* landscapeComponent = *landscapeComponents.CreateIterator();
ULandscapeSplinesComponent* landscapeSplinesComponent = landscape->GetSplinesComponent();
// Check if the splines component exists, if not create it
if (landscapeSplinesComponent == nullptr)
{
landscape->CreateSplineComponent();
landscapeSplinesComponent = landscape->GetSplinesComponent();
}
// Create the spline points in the first landscape component
for (int32 i = 0; i < splinePoints.Num(); i++)
{
AddControlPoint(landscape, landscapeSplinesComponent, splinePoints[i]);
}
}
using UnrealBuildTool;
/*
*
* Make sure you add the needed Landscape, LandscapeEditor and Foliage dependencies
*
*/
public class YourLandscapeModule : ModuleRules
{
public YourLandscapeModule(ReadOnlyTargetRules Target) : base(Target)
{
PublicDependencyModuleNames.AddRange(
new string[]
{
// ...
"Core",
"LandscapeEditor",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
// ...
"Landscape",
"Foliage",
}
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment