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?
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 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.
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()
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.
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.
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.