Runtime Landscape Editing

Runtime Landscape Editing

Article written by Ryan B.

Information on editing landscapes during runtime is quite commonly requested, and this article aims to share insight into what would be involved in achieving this without implementing a whole new system.

Landscape edit layers is an editor-only feature and is the only way to achieve procedural modification of landscape. There are no runtime hooks since the intermediate data (the so-called “edit layers”) is entirely editor-only and most of the code is defined only in the editor. The edit layers are collapsed/merged into the final set of heightmaps/weightmaps in the editor on the GPU (see LandscapeEditLayers.cpp) and there’s nothing intrinsically impossible when it comes to perform this on target platforms, provided they have implemented a feature set higher than Shader Model 5.0 (which is the case for all non-mobile platforms and most mobile platforms these days), but the amount of code and the ramifications of landscape (navmesh, collision, gameplay, etc.) makes it a massive endeavor to turn this into a runtime-enabled feature.

Here’s an outline that will hopefully serve as a good starting point for what’s most likely to be technically required to have landscape modifiable at runtime. Note that you may find additional steps required outside of the following which will require further investigation on your behalf.

  1. Add a World Position Offset (WPO) to the material that reads from another texture (single additional layer) or texture array (multiple layers). This can be more or less complex depending on whether (a) the entire landscape can be loaded at runtime and all that is required is a single texture or render target for each of your additional layers or (b) the landscape can only be partially loaded and, similarly to the current landscape, the heightmaps/weightmaps will need to be split into multiple textures/render targets (most likely one per landscape component). Then both displace the vertices (vertically only, if you want the collisions to remain being represented by a heightfield) and compute the normals based on its neighbors (see FinalizeHeightmap in Engine\Shaders\Private\Landscape\LandscapeLayersHeightmapsPS.usf).

  2. Bind these textures to the material by using landscape MIDs rather than MICs. For this, setting bUseDynamicMaterialInstance to true on the landscape actor and then binding the heightmaps/weightmaps textures is required. In the (b) case above, each component will need to receive its own texture.

  3. Re-generate your collision components when there’s a change by:

  4. Reading back the merged heightmaps/weightmaps from the GPU. This can be done asynchronously by copying to a staging texture, writing a GPU fence, and polling that fence on the GPU. This is the purpose of FLandscapeEditLayerReadback, but FLandscapeGrassWeightExporteror can also be piggybacked on as it is also able to export WPO (see ULandscapeComponent::RenderWPOHeightmap). In both cases, this will incur a delay of 2 frames minimum, since the GPU always trails behind the RHI thread / render thread / game thread and the asynchronous readback operation involves keeping the refresh rate constant but not getting the result immediately either. To make the process synchronous, FlushRenderingCommands() can be called. This will add a hitch to the game as the CPU will then wait for the CPU to catch up and execute all of its in flight-commands (up until the last one : the readback command).

  5. Or if your layer heightmap change originates from the CPU, hook into ULandscapeHeightfieldCollisionComponent and rebuild the heightfield data there.

  6. If procedural placement of grass is used (i.e. the material uses LandscapeGrassOutput to spawn different types of grass) then it’s necessary to invalidate the grass and generate new grass data (i.e. density maps for grass). See LandscapeGrass.cpp.

  7. If you modify weightmaps, it’s necessary to output proper physical material indices. Similar to grass, this is done in the editor using a LandscapePhysicalMaterialOutput in your material and runs a dedicated version of the landscape material in order to properly render the index of the dominant physical material, which is then read back asynchronously on the CPU and stored into ULandscapeHeightfieldCollisionComponent.

  8. Regenerating the Navmesh.

Hopefully, this information is sufficient for you to start designing your project. Whatever approach you choose with this, don’t underestimate the work involved. As stated before, landscape code is massive and is at the source of many other in-game systems (physics, rendering, etc.). It also plays a non-negligible part in memory usage, rendering, and streaming performance. Moving these at runtime rather than edit time will probably involve some work to get this to an acceptable state.

Get more answers on the Knowledge Base!

21 Likes

In the time since this was first posted, has anyone implemented this?

Its probably completely pointless.

There is nothing in engine as badly designed as the landacape system.
Working with it given all its limitations when a properly made runtime procedural mesh does everything you need and a lot more is essentially a waste of anyone’s time.

Not to mention the fun fact that even in its current baked state the cost of landscapes is far too high compared to any competitor engine you care to try…
Epic just loves to kick this particular dead horse around instead of designing a more up to date best practice following system.
Thats actually how we ended up with “edit layers” in the first place…

If you are serious about your project happening, just look for pre-made tools including but not limited to using off the shelf Voxel plugins to achieve runtime editing…

1 Like

luoxz implements complete runtime terrain landscape editing

Since nobody ever helps here with this question, I’l do. I managed to make procedural landscape in multiple ways (albeit took me a month of looking throuhg plugins, experimenting, even trying to make my own landscape solution)
.

  1. load from texture using: LandscapeTest->LandscapeComponents[i]->SetHeightmap(HeightMap);
  2. load from rendertexture using:
    ||TArray SurfData;|
    |—|—|
    ||auto RenderTarget = RTHeightMap->GameThread_GetRenderTargetResource();|
    ||RenderTarget->ReadPixels(SurfData);|
  3. just writing heights to FColor:for (int x = 0; x < 256; x++)
    {
    for (int y = 0; y < 256; y++)
    {
    uint8 height = x;
    SurfData.Add(FColor(height, height, height, height));
    }
    }

Additionally need to call some function to trigger the redraw and recreate collisions:

for (int i = 0; i < LandscapeTest->LandscapeComponents.Num(); i++)
{
	LandscapeTest->LandscapeComponents[i]->MarkRenderStateDirty();

	LandscapeTest->LandscapeComponents[i]->InitHeightmapData(SurfData, true);

	// NOTE: crash if edit layers is on.
	// NOTE: crash cuz of no mips.
	// NOTE: srgb collision offset?
	//LandscapeTest->LandscapeComponents[i]->SetHeightmap(HeightMap);
	LandscapeTest->LandscapeComponents[i]->RequestHeightmapUpdate(true, true);

	LandscapeTest->LandscapeComponents[i]->UpdateCachedBounds(true);
	LandscapeTest->LandscapeComponents[i]->PostLoad();
}

//for (int i = 0; i < LandscapeTest->CollisionComponents.Num(); i++)
//	LandscapeTest->CollisionComponents[i]->RecreateCollision();

LandscapeTest->RecreateCollisionComponents();
2 Likes

Thank you for the insight @avol
i believe the whole endeavor would be impossible with blueprints only, right?

@avol
Sure. And how long does something other than a PC take to re-create collision component?

Theres a reason no one does this with the default landacape - aside from the fact the system itself is bad.
That’s the possible size of it.

You have to modify everything - or even just a single component when you do it right which you aren’t btw - and each component is far too much for most systems to handle at runtime along with the rest of your game’s procesing.

You may be able to improve a bit by getting the difference between the original and the new map, resolving which compnents need to be updated, and manually updating collision just for those.

It may work in testing on a single landscape setup - and it could be a decent idea for some stuff - but its a hitch along system rather than a gameplay element, which is what people generally want.

In other words, you can run the code but you will end up overloading the CPU/GPU if you constantly update every frame, which is what players usually intend to do with actions while sculpting.

Dont get me wrong, its not horribly bad (the editor does it, right? Hitching like crazy most of the time but it does it) performance for the task at hand - but if you also start adding movement of foliage and re-drawing of grass etc to the equation like you probably need to, you end up lagging dreadly behind.

On the other hand, making something with an off the shelf voxel system designed specifically for gameplay goes around most of that - and also allows creating holes and 3d representations.

@Aphexx100
At the bare minimum youd need to be able to know how to throw his shared code up in a blueprint class, make a blueprint callable function, and expose the correct textures to the function so that you can feed them in along with whater exec pins need to run the code.

Its not that hard really. But the performance of it is going to be very limiting, not sure its worth anyone’s time.

What would be worth people’s time?
Procedural mesh component.
If you have to bark up any tree at all, that one would probably be the best.

You can then choose to create an analytical mesh based on pixel heights rather than a hard set “one verxtex each unit set” measure… for one…

Yes my big mistake, posted my response too quickly before doing more testing :slight_smile:
Aside from performance (which in my case I didn’t care about, because I only wanted to generate maps with some loading screen) it’s also not compiling to runtime. And has some other issues which make it unusable.

No big deal, and too bad.

Though not viable for everyone, it would at least have been an option for some very select situations.

If you are still at it, I suggest making a fully custom system.

Because in a procedural mesh the positions of the vertex are known (x/y being constant) you can save data by just passing in Z.

The tris order is always
Up Left
Down Left
Down Right

Up Right is ways = to Up left, and down left is always the same as down right when you move to the next instance.
As such, a comma separated list of Z values can easily be enough to generate the whole landscape with Zilch in terms of storage cost.
(Should be called a triangle strip system if i remeber correctly).

If you really want to gut things to nothing, the other data stored is the normal.
You can easily represent it with 2 int16 values.
(Dont think the engine will know what to do with it, but ao long as your math sets the normal to what it should be then the engine is only aware of the normal. Not of how you store and use the data for generation - again, the issue here is generation and the sheer amount of data it requires).

The benefit of making somerhing like this would basically be the ability to stream in an endless line of vertex to continually generate new strips…

Onviosuly you srill have a bottleneck on rendering/amount of tris.

So you need to come up with some sort of aggregation or tiling system.

Unlike Epic’s mess, which does it with some low level mipping (deletes your data at a diatance on a /2 scale with each LOD)

I would suggest finding different and Analytical approaches.

Just like you can simplify a ton of different ways, having a 0 in the data stream for any vertex Z that is below a height requirement treshold would probably net some interesting simplification results (but still use all the verts).
Removing the verts analytically that’s where the real magic happens…

I ended up with using shader world plugin from marketplace. I do need very detailed terrain mesh and lods for my snow system to work properly(reserving WPO for that) and making procedural landscape with procedural mesh component is just too slow and requires too much work to make proper lods.

Max D’s plugin right? If so, congrats. Show off a demo video of the results (if you feel like it). :wink: