Search code examples
pythonunreal-engine4heightmap

Landscape creation in UE4.27


I'm trying to create a landscape with Python Unreal API using unreal.EditorLevelLibrary.spawn_actor_from_class() to spawn a LandscapeProxy actor then alter its heightmap using landscape_import_heightmap_from_render_target() of the LandscapeProxy class.

Spawned actor is of class LandscapePlaceholder which does not support heightmap operations. How to convert it or should I go another way?


Solution

  • The Landscape/Terrain API

    Terrains in UE4 are special actors built over the 'heightmap' concept. Each Terrain is a grid of components (ULandscapeComponent). Each component is mapped to a texture holding height data. The landscape component concept impacts performance and quality of the result. A component is the minimal render unit of a terrain (minimal geometry that can be culled both from the rendering and collisions point of view). A brief explanation on landscape components.

    To build a new terrain (or Landscape in UE4) you need a heightmap. From this heightmap the UE4 API will generate textures mapped to components. Heightmaps are simple arrays of unsigned 16bit values (0 to 65535 with 32768 value considered 'sea level'). In Python (for performance reasons and integration with NumPy) heightmaps are bytearray's (you eventually need to recast them).

    Creating a new Landscape

    Creating a heightmap with random values:

    import unreal_engine as ue
    import struct
    import random
    
    width = 1024
    height = 1024
    heightmap = []
    
    # fill the heightmap with random values
    for y in range(0, height):
        for x in range(0, width):
            heightmap.append(random.randint(0, 65535))
    
    data = struct.pack('{0}H'.format(width * height), *heightmap)
    

    Now 'data' we can use for the landscape API. Before filling a landscape we need to spawn it:

    from unreal_engine.classes import Landscape
    
    new_landscape = ue.get_editor_world().actor_spawn(Landscape)
    

    Do not run the previous script as the editor does not like uninitialized terrain (it will crash). Fill the terrain with the heightmap data created before. Choose how many components we need (the grid resolution) and how many quads are required for each component (component geometry is formed by quad primitives). Once we know terrain size we can expand/adapt the heightmap accordingly:

    unreal_engine.heightmap_expand(data, original_width, original_height, terrain_width, terrain_height)
    

    This will generate a heightmap with the optimal dimensions for the landscape.

    import unreal_engine as ue
    import struct
    import random
    from unreal_engine.classes import Landscape
    
    width = 1024
    height = 1024
    heightmap = []
    
    for y in range(0, height):
        for x in range(0, width):
            heightmap.append(random.randint(0, 65535))
    
    data = struct.pack('{0}H'.format(width * height), *heightmap)
    
    quads_per_section = 63
    number_of_sections = 1
    components_x = 8
    components_y = 8
    
    fixed_data = ue.heightmap_expand(data, width, height, quads_per_section * number_of_sections * components_x + 1, quads_per_section * number_of_sections * components_y + 1)
    
    landscape = ue.get_editor_world().actor_spawn(Landscape)
    landscape.landscape_import(quads_per_section, number_of_sections, components_x, components_y, fixed_data)
    landscape.set_actor_scale(1,1,1)
    

    Instead of specifying quads per component we are using the 'section' concept. UE4 allows another level of subdivision for better control of optimizations (LOD and mipmapping). You can have 1 section (1x1 quad) or 2 (2x2 quads). Other values are not supported. Number of quads is related to textures size, so valid values are: 7x7, 15x15, 31x31, 63x63, 127x127, 255x255 (note the off-by-one weirdness as all terrain tools work with max value and not size). You need to carefully choose the size of the terrain as well as the heightmap.

    Getting/Creating ULandscapeInfo

    Information about a Landscape/Terrain are stored in a uobject called ULandscapeInfo. To retrieve it (or eventually create a new one if you are making weird operations) you have following two functions:

    landscape_info = landscape.get_landscape_info()
    
    # create a new ULandscapeInfo, required if you do not import an heightmap in a manually spawned landscape
    landscape_info = landscape.create_landscape_info()
    

    Retrieving ULandscapeComponent's textures

    To access height values of a terrain, retrieve them from each component:

    import unreal_engine as ue
    
    for component in landscape.LandscapeComponents:
        heightmap_texture = component.HeightmapTexture
        print('{0}x{1} -> {2}'.format(heightmap_texture.texture_get_width(), heightmap_texture.texture_get_height(), len(heightmap_texture.texture_get_source_data())))
    

    This will print texture width, height and data size of each landscape component.

    Exporting the Terrain to a FRawMesh

    FRawMesh is a special structure representing a mesh. You can use it to generate a new StaticMesh. You can generate a new FRawMesh from a landscape with:

    # lod is optional, by default it is 0
    raw_mesh = landscape.landscape_export_to_raw_mesh([lod])
    

    Terrains generally are huge in size.

    The Heightmap api

    A heightmap high-level API is exposed to simplify heightmap manipulation.

    # expand the heightmap to fit the new size
    expanded_data = ue.heightmap_expand(data, data_width, data_height, new_width, new_height)
    
    # import a heightmap file (r16 or grayscale 16bit png) and returns a bytearray
    data = ue.heightmap_import(filename[,width, height])
    

    If width and height are not specified it will try to retrieve them from the file.