Unreal Engine 5: Using Python to Import and Render Datasmith Models

Unreal Engine 5: Using Python to Import and Render Datasmith Models

BIMCAD Journalist 16/01/2024
unreal engine.png

Unreal Engine 5


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.

Sample Project


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:

Unreal Engine Python Editor Script Plugin
Unreal Engine Sequencer Scripting
Unreal Engine Datasmith Importer
Unreal Engine Movie Render Queue

When everything is set up, we should just save and close this project.

Unreal Engine Commandlet


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.exeUnreal Engine executable, that in our case stored in Engine\Binaries\Win64

$project_pathpath to Unreal Engine Project

-Unattendedargument that prevent pop-up dialogs

-NoLoadStartupPackagesprevent loading startup packages, that slightly speed up project loading

ABSLOG= $log_pathSet up output file for logging

-run=pythonscript -script= $py_script_paththe 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.

Python Script


| Load Level

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.

| Import Datasmith

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.

| Create Texture and Material Assets

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)

| Create Level Sequences

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)

| Create Render Movie Queue

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)

| Render Movie Queue and Level Sequence

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)
Get a free consultation
contact us
Fill out the form and contact us today for an absolutely free consultation of anything related to BIM/CAD.
footer
logo
text-logo
Quality & Information Security
are at the core of our priorities.
iso9001
iso27001
awardsaokhue
Contact
Email:sales@tgl-sol.com
Hotline:(+84) 377 359 728
Ho Chi Minh Office:42/1 Ung Van Khiem Street, Ward 25, Binh Thanh District, Ho Chi Minh
Da Nang Office:01 Tran Van Ky Street, Hoa Khanh Nam Ward, Lien Chieu District, Da Nang
Headquarter:3F Tojikyo Building, 16-2 Kodenmacho, Nihonbashi, Chuo-ku, Tokyo, Japan
Follow Us
BIM/CAD © 2023 All Rights Reserved
Hey 👋 I'm right here to help you, so just click the chat button.