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!