Unreal Engine isn't just great for making games, it's also a powerful tool for crafting lifelike scenes with its built-in Render Engine. Moreover, you can streamline tasks by automating common actions using Python scripts.
In this article, BIMCAD Vietnam will guide you through importing a Datasmith model, making modifications, and generating still images using a Python script.
Before creating any scripts, let's start by setting up a sample project that we'll be working with. For this, we'll choose the Architecture -> Blank template. Once the project is created and loaded, it's important to ensure that certain plugins are enabled for this project. You can do this by going to the Edit -> Plugins tab:
When everything is set up, we should just save and close this project.
To start Unreal Engine in command line, we will use next powershell script:
.\UnrealEditor-Cmd.exe $project_path -Unattended -NoLoadStartupPackages ABSLOG= $log_path -run=pythonscript -script= $py_script_path
.\UnrealEditor-Cmd.exe
– Unreal Engine executable, that in our case stored in Engine\Binaries\Win64
$project_path
– path to Unreal Engine Project
-Unattended
– argument that prevent pop-up dialogs
-NoLoadStartupPackages
– prevent loading startup packages, that slightly speed up project loading
ABSLOG= $log_path
– Set up output file for logging
-run=pythonscript -script= $py_script_path
– the most important argument that point on Python script that will be run after loading project.
Depending on what arguments we pass to this command, numbers of actions we can do will differ. For example, using -run=pythonscript -script=
wouldn’t load Engine UI what is much faster approach in comparing with using –ExecutePythonScript=
, that will also load Python script, but with loading UI. More about commandline arguments can be found in official documentation article.
First thing we need to do is load a map. This is necessary part because we will save this map after.
subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
load_level_res = subsystem.load_level("/Game/%s" % MAP_NAME)
Using this code, we will load target level and continue working with it.
ds_scene_in_memory = unreal.DatasmithSceneElement.construct_datasmith_scene_from_file(ds_file_on_disk)
if ds_scene_in_memory is None:
unreal.log_error("Scene loading failed.")
quit()
# Set import options.
import_options = ds_scene_in_memory.get_options(unreal.DatasmithImportOptions)
import_options.base_options.scene_handling = unreal.DatasmithImportScene.CURRENT_LEVEL
# Finalize the process by creating assets and actors.
# Your destination folder must start with /Game/
result = ds_scene_in_memory.import_scene("/Game/GeneratedFromScriptScene")
if not result.import_succeed:
unreal.log("Importing failed.")
quit()
# Clean up the Datasmith Scene.
ds_scene_in_memory.destroy_scene()
unreal.log("Custom import process complete!")
At first step we loading datasmith object in memory using unreal.DatasmithSceneElement.construct_datasmith_scene_from_file(ds_file_on_disk)
method, where ds_file_on_disk
is path to datasmith object.
After checking that we successfully loaded datasmith object, we set import options and finally import assets in project using ds_scene_in_memory.import_scene("/Game/GeneratedFromScriptScene")
method. Imported Actors and Meshes we will use in next steps.
At this step we will create and set textures for meshes. The relation between mesh and texture can be various. Is our case we use dictionary with key as mesh name and value as path to texture:
MESH_TEX_DICT = dict([
("MeshName1", "path_to_texture_A.png"), # Brick Wall
("MeshName2", "path_to_texture_A.png"), # Brick Wall
("MeshName3", "path_to_texture_B.png") # Grass
])
Since we create Material for each texture to apply it in Mesh, we will create base Material with Texture parameter and for every Texture we will create Material Instance with specified texture parameter.
def create_base_mtl():
AssetTools = unreal.AssetToolsHelpers.get_asset_tools()
base_mtl = AssetTools.create_asset("M_Base" , "/Game/Materials", unreal.Material, unreal.MaterialFactoryNew())
mtl_expr = unreal.MaterialEditingLibrary.create_material_expression(base_mtl, unreal.MaterialExpressionTextureSampleParameter2D, -300)
mtl_expr.set_editor_property('parameter_name', "texture_base_color")
unreal.MaterialEditingLibrary.connect_material_property(mtl_expr, "RGBA", unreal.MaterialProperty.MP_BASE_COLOR)
return base_mtl
To import texture in project we will use unreal.AssetImportTask
and set filename property to full path to texture and destination_path
as project relative path.
# Import Texture
texture_import_task = unreal.AssetImportTask()
texture_import_task.set_editor_property('filename', path)
texture_import_task.set_editor_property('destination_path', '/Game/Textures')
texture_import_task.replace_existing = True
texture_import_task.replace_existing_settings = True
AssetTools = unreal.AssetToolsHelpers.get_asset_tools()
AssetTools.import_asset_tasks([texture_import_task])
As a result, we get texture assets, that we will use to create Material Instances:
mi_asset = AssetTools.create_asset("MI_" + tex_name, "/Game/Materials", unreal.MaterialInstanceConstant, unreal.MaterialInstanceConstantFactoryNew())
unreal.MaterialEditingLibrary.set_material_instance_parent(mi_asset, parent_material)
unreal.MaterialEditingLibrary.set_material_instance_texture_parameter_value(mi_asset, param_name, tex_asset)
For Level Sequences we have to use CineCamera. We can create it prior or at this step. However, at this time we will use one we loaded from Datasmith object. For creating level sequence, we will use next code:
sequence = unreal.AssetTools.create_asset(AssetTools, seq_name, folder, unreal.LevelSequence, unreal.LevelSequenceFactoryNew())
sequence.set_playback_start(0)
sequence.set_playback_end(1)
camera_cut_track = sequence.add_master_track(unreal.MovieSceneCameraCutTrack)
binding = sequence.add_possessable(camera_actor)
# Add a camera cut track for this camera
camera_cut_section = camera_cut_track.add_section()
camera_cut_section.set_start_frame(0)
camera_cut_section.set_end_frame(1)
camera_binding_id = unreal.MovieSceneObjectBindingID()
camera_binding_id.set_editor_property("Guid", binding.get_id())
camera_cut_section.set_editor_property("CameraBindingID", camera_binding_id)
camera_component_binding = sequence.add_possessable(camera_actor.get_cine_camera_component())
camera_component_binding.set_parent(binding)
In order to create and save Movie Queue with Level Sequences we will create a new queue, for each Level Sequence we want to be in this queue we allocate new job, set parameters for map and Level Sequence, set configuration, which can be the same for each Level Sequence and then we save queue to a manifest file. However, we cannot set path and name for our file and to fix this, we move file into /Game/
folder and rename it.
queue = unreal.MoviePipelineQueue(name="def_render_queue")
# Create new movie pipeline job
def create_job(level_sequence: str):
job = queue.allocate_new_job()
job.set_editor_property('map', unreal.SoftObjectPath("/Game/%s" % MAP_NAME))
job.set_editor_property('sequence', unreal.SoftObjectPath(level_sequence))
job.set_configuration(get_preview_config())
for seq in level_sequences: create_job(seq)
# Save Queue Manifest file
_, manifest_path = unreal.MoviePipelineEditorLibrary.save_queue_to_manifest_file(queue)
# Move and Rename Queue manifest file
manifest_path = os.path.abspath(manifest_path)
manifest_dir, manifest_file = os.path.split(manifest_path)
new_path = os.path.join(manifest_dir, "mrq_manifest.utxt")
os.replace(manifest_path, new_path)
After that we save our map and project using next line of code:
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
In order to render Movie Queue or Level Sequence inside this Python script we will use subprocess
module, that allow us to call processes. To use this module we will pass an array of arguments to subprocess.call
method. We also remove environment variables before calling this method and path them together.
def render_sequence(sequence_path, config_path):
cmd_args = [
sys.executable, # exe
"\"%s\"" % unreal.Paths.get_project_file_path(), # project
"/Game/%s" % MAP_NAME, # game map
"-game",
"-Multiprocess",
"-NoLoadStartupPackages"
"-NoLoadingScreen",
"-log",
"-Unattended",
"-messaging",
"-windowed",
"-ResX=1280",
"-ResY=720",
"ABSLOG=\"%s\"" % LOG_RENDER_PATH,
"-MoviePipelineConfig=\"%s\"" % config_path,
"-LevelSequence=\"%s\"" % sequence_path
]
return exec_cmd(cmd_args)
def render_movie_queue():
cmd_args = [
sys.executable, # exe
"\"%s\"" % unreal.Paths.get_project_file_path(), # project
"/Game/%s" % MAP_NAME, # game map
"-game",
"-Multiprocess",
"-NoLoadStartupPackages"
"-NoLoadingScreen",
"-log",
"-Unattended",
"-messaging",
"-windowed",
"-ResX=1280",
"-ResY=720",
"ABSLOG=\"%s\"" % LOG_RENDER_PATH,
# This need to be a path relative the to the Unreal project "Saved" folder.
"-MoviePipelineConfig=\"%s\"" % MANIFEST_PATH
]
return exec_cmd(cmd_args)
def exec_cmd(cmd_args: list[str]):
unreal.log("Movie Queue command-line arguments: {}".format(" ".join(cmd_args)))
# Make a shallow copy of the current environment and clear some variables
run_env = copy.copy(os.environ)
# Prevent SG TK to try to bootstrap in the new process
if "UE_SHOTGUN_BOOTSTRAP" in run_env: del run_env["UE_SHOTGUN_BOOTSTRAP"]
if "UE_SHOTGRID_BOOTSTRAP" in run_env: del run_env["UE_SHOTGRID_BOOTSTRAP"]
return subprocess.call(cmd_args, env=run_env)