Creating a custom Movie Render Queue Setting for Unreal

Tutorial / 25 April 2021

Recently I’ve been working more with Unreal and decided to spend some time writing a plugin for the movie render queue that would output an otio file. OpenTimelineIO is the standard file format we use for cuts in editorial. An otio file contains information about clip order and length, with references to external media files.  The Movie Render Queue is a tool in unreal that allows you to queue up and render out Level Sequences from the engine.

Source code of my plugin: https://github.com/mvanneutigem/UnrealOtioExporter

This plugin is written in C++ because it currently isn’t possible to extend the Movie Render Queue in Unreal using the python API, at least as far as I’m aware. This meant I had to use the C++ API of OpenTimelineIO as well, which proved a bit tricky since at the time there weren't really any examples floating around, resulting in a bit of trial and error on my end.

Getting started

To be able to develop for Unreal Engine you will need to install the latest version of the engine (or at least 4.25+, as this is where the Movie Render Queue was introduced). You will also need visual studio, you can get visual studio community for free. Once these are installed you can create a new project, you don’t need the starter content, but you will need to enable to movie render queue plugin. 

Now, the easiest way to get started with writing C++ code is to open up the project in Unreal and go to the “File” menu and select “New C++ class”. Next you can select a class to inherit from, in my case I wanted to inherit from the “UMoviePipelineOutputBase” class, which is a subclass of the "UMoviePipelineSetting". You should now have a new folder under your main project folder called "Source".
But before you start coding you will want to generate the visual studio project files to have an easier time compiling:


You can do this by right clicking the .uproject file, or from the commandline like so;

"<AbsoluteEnginePath>Binaries\DotNET\UnrealBuildTool.exe" -projectfiles -project="<FullPathToUprojectFile>" -game -rocket -progress 

Now you should have an .sln file you can double click to open the project in visual studio.

Inside of the "Source" folder you should find two target .cs files (we can skip those for now), and another folder. This folder should be named the same as your project, this is your main module. When you open up this folder you will find the C++ class you created (It may be inside the private/public folders depending on what you selected when you created the class). There should also be a build.cs file here. 

The build.cs file is used to add dependencies to your module. Any modules you want to use in your code should be listed here, else you will run into trouble during compilation;

PublicDependencyModuleNames.AddRange(
            new string[]
            {
                "Core",
                "Projects",
                "MovieRenderPipelineCore",
                "MovieSceneTools",
                "MovieSceneTracks",
                "LevelSequence",
                "MovieScene",
                "opentimelineio",
                // ... add other public dependencies that you statically link with here ...
            }
);

 If you go back up to the main project folder and open up your main .uproject file you will find there is now an entry for the main project module listed in there as well. This will tell unreal what modules and plugins to load into your project.

Implementing the setting

To create a custom setting for the Movie Render Queue you have to inherit from a class called the UMoviePipelineSetting (or any of it's subclasses).

https://docs.unrealengine.com/en-US/API/Plugins/MovieRenderPipelineCore/UMoviePipelineSetting/index.html

Next you will have to implement at least the base functions, these will tell Unreal whether the setting will be valid to execute on individual level sequences (Shots) and/or "Master" Level Sequences (Sequences). You can do this directly in the header as they return simple booleans.

protected:
    virtual bool IsValidOnShots() const override { return false; }
    virtual bool IsValidOnMaster() const override { return true; }

Now you can add the logic you want your setting to execute, this logic should live in the "BeginExportImpl()" function, which you can override in the header;

virtual void BeginExportImpl() override; 

And you can add the actual implementation of the function in the cpp file, for a simple test you can log something to the output to make sure the function is actually getting run before you start adding any complex logic.

void UMoviePipelineOtioExporter::BeginExportImpl()
{
    UE_LOG(LogMovieRenderPipeline, Log, TEXT("Running OtioMoviePipelineSetting OTIO export"));
}

Now  you can compile your code, you can either do this by simply clicking the compile button above the viewport in unreal. Or by browsing to your module from the "Windows->Developer Tools->Modules" panel and clicking compile. Or alternatively running the project from inside visual studio, which has the added benefit of attaching the visual studio debugger as well. 

When your code has compiled successfully the option should become available for you to use in the movie render queue. Open the Movie Render Queue dialog from the "windows" menu, next add a level sequence and click the preset which will bring up another window. Then click "+ Setting" and it should be listed there. 

To see the output, you will need to bring up the output window which you can find under "Windows->Developer tools->output log". 

Note: You can also use the output log to test your python code, when you click the "cmd" button in the lower left corner it will switch to python and allow you to directly execute your python commands in the editor.

To expose settings in the GUI of your newly created setting you need to declare them as variables in your setting's header file. You can also specify a category for each property, this can be any string and is used to bundle settings together visually.

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Opentimelineio settings")
    FString FileNameFormat;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Opentimelineio settings")
    FString FileReferenceFormat = "jpeg";

Including third party code

To include third party code, you can either include the entire source code, or you can compile your cpp files into a library. In this case I did the latter. You can use visual studio to do this, opentimelineio comes with a make file you can right click, all you need to do is specify the target (in my case windows x64) and it will generate the dll's, lib's and headers.

If you are wanting to create a plugin you can get an example of the structure by creating a new plugin from the "Plugins" dialog and selecting the "third party" option.

To start from scratch you should add the new module as an entry in your uproject (or uplugin) file, the name specified here should match what you named the module folder and class files, "opentimelineio" in my case.

 "Modules": [
    {
      "Name": "RenderQueueOtioOption",
      "Type": "Runtime",
      "LoadingPhase": "Default",
      "WhitelistPlatforms": [
        "Win64"
      ]
    },
    {
      "Name": "opentimelineio",
      "Type": "Runtime",
      "LoadingPhase": "Default",
      "WhitelistPlatforms": [
        "Win64"
      ]
    }
  ],

Once you have compiled your third party code you can create a new folder called "thirdparty" in your Source folder, and underneath it replicate the folder structure as was present in the main module that unreal created for you. It should have a folder containing a build.cs file, a header and cpp file for your module, and all the third party files you wish to include (headers, lib's, dll's).

In the build.cs file you should specify the locations of your header files.

PublicIncludePaths.AddRange(
    new string[] {
        // ... add public include paths required here ...
        "$(PluginDir)/Source/ThirdParty/opentimelineio/include",
        "$(PluginDir)/Source/ThirdParty/opentimelineio/include/opentimelineio/deps",
    }
);

As well as adding the dll's and lib's as dependencies, these are specific to your target platform so we have to check this matches.

if (Target.Platform == UnrealTargetPlatform.Win64)
{
    // Add the import library
    PublicAdditionalLibraries.Add("$(PluginDir)/Source/ThirdParty/opentimelineio/opentimelineio.lib");
    PublicAdditionalLibraries.Add("$(PluginDir)/Source/ThirdParty/opentimelineio/opentime.lib");

    // Delay-load the DLL, so we can load it from the right place first
    PublicDelayLoadDLLs.Add("opentimelineio.dll");
    PublicDelayLoadDLLs.Add("opentime.dll");

    // Ensure that the DLL is staged along with the executable
    RuntimeDependencies.Add("$(PluginDir)/Source/ThirdParty/opentimelineio/opentimelineio.dll");
    RuntimeDependencies.Add("$(PluginDir)/Source/ThirdParty/opentimelineio/opentime.dll");
}

Next we should fill out the header and cpp file for your third party module, to actually load the third party code library. This can be fairly minimal, just a basic class inheriting from the IModuleInterface with a variable to store the library handle.

#pragma once
#include "Modules/ModuleManager.h"

class FopentimelineioModule : public IModuleInterface
{
public:
    /** IModuleInterface implementation */
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
private:
    /** Handle to the test dll we will load */
    void*    ExampleLibraryHandle;
};

In the cpp file of this third party module you can try to load the library and create a popup if it failed. You can do this in the StartupModule function.

ExampleLibraryHandle = !LibraryPath.IsEmpty() ? FPlatformProcess::GetDllHandle(*LibraryPath) : nullptr;
if (!ExampleLibraryHandle)
{
    FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("ThirdPartyLibraryError", "Failed to load otio library"));
}

Dont forget to free up the memory in the ShutdownModule function.

 FPlatformProcess::FreeDllHandle(ExampleLibraryHandle);

You should now be able to use the third party library in your main module's code (don't forget to add the newly created module as a dependency in your main modules build.cs file!).

OpenTimelineIO

As mentioned before, the opentimelineio C++ api was a bit tricky for me to use. The main thing that tripped me up was the way memory is managed in opentimelineio, you're required to make use of so called  "Retainer" objects to hold your objects. These retainer objects will make sure that your objects don't get deleted until you take back ownership of them for clean up.

// we store the timeline in a container, if we dont do this we run into issues when trying to write out the file.
auto otioTimeline = otio::SerializableObject::Retainer<otio::Timeline>(
    new otio::Timeline(
        masterSequenceName, 
        otio::RationalTime(startTime.Value / tickResolution.Numerator, displayRate.AsDecimal())
    )
);

 If you don't make use of the retainer object you will run into heap memory error's as the objects get deleted twice when you try to write it out to a file. Once by opentimelineio, and once by the unreal garbage collection it seems.

otio::ErrorStatus errorStatus;
if (!otioTimeline.value->to_json_file(otioFilePathStr, &errorStatus))
{
    FString errorMessage = UTF8_TO_TCHAR(
        (otio::ErrorStatus::outcome_to_string(errorStatus.outcome) + ": " + errorStatus.details).c_str()
    );
    UE_LOG(LogMovieRenderPipeline, Error, TEXT("OTIO Error: %s"), *errorMessage);
}

At the end of the function you should call "take_value" on the retainer object to take back ownership of it, which removed the crashing for me.

otio::Timeline* timeline = otioTimeline.take_value(); 

Additional docs

Movie Render Queue: https://docs.unrealengine.com/en-US/RenderingAndGraphics/RayTracing/MovieRenderQueue/index.html
OpenTimelineIO: https://github.com/PixarAnimationStudios/OpenTimelineIO

Lastly I just wanted to mention I'm still learning every day so if you notice any mistakes let me know!

Using OpenMaya for applying transformations to animated objects

Work In Progress / 24 June 2020

Been doing some research on methods to apply spatial transformations to objects.
The resulting tool is still work-in-progress but I wanted to get the first version out there and see if people may have suggestions for improvements.

I chose to approach this using OpenMaya for speed reasons, using the cmds library works as well but doesn't allow you to query values using a time context, rather having to set the currentTime which is much, much slower.

time_context = OpenMaya.MDGContext(OpenMaya.MTime(frame))
matrix_object = plug.asMObject(time_context)
fn_matrix_data = OpenMaya.MFnMatrixData(matrix_object)
matrix = fn_matrix_data.matrix()

Another benefit is being able to directly manipulate animation curves, for animated objects.

m_plug = m_dag_node.findPlug(attribute, False)
m_source = m_plug.source()
if m_source.isNull:
    return
m_source_node = m_source.node()
if not m_source_node.apiTypeStr.startswith('kAnimCurve'):
    return
fn_curve = OpenMayaAnim.MFnAnimCurve(m_source_node)
fn_curve.addKey(OpenMaya.MTime(input), value)

The main outstanding issue are the that it doesn't account for rotation order changes and the way it handles rotations over the 180 degree threshold, it's internally using matrices so this information gets lost during calculations. I've implemented an experimental flipping filter that works quite well but I'm continuing to do research on how to improve it when I have some time.

Full code available on my github: https://github.com/mvanneutigem/maya_tools

Writing a basic deformer for Maya in python

Tutorial / 26 May 2020

This tutorial functions as a starting point and to give some basic pointers on how to approach writing a deformer for Maya, I've based this on the mnCollisionDeformer I recently wrote, the video will cover the basic idea but will leave room for you to implement your own algorithm.

You can find the deformer template to get started from here: https://github.com/mvn882/tutorials/blob/master/plugins/deformerTemplate.py 

You can start from the template this already has the basic functions and class for the deformer set up.
It starts with the imports, we want to import OpenMaya and OpenMayaMPX from the old python API, the newer API does not support MPxDeformerNode's yet. We also want to import Mel eval, this will be used for evaluating custom attribute editor templates.

import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
from maya.mel import eval as mel_eval

Then we'll set a couple of global variables, these are pre-defined in Maya to access geometry data and will work from 2016 upwards.
Sidenote: the MPxDeformerNode inherits from the MPxGeopmetryFilter base class, hence that's what the variables are called.

kInput = OpenMayaMPx.cvar.MPxGeometryFilter_input
kInputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom
kOutputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom
kEnvelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope
kGroupId = OpenMayaMPx.cvar.MPxGeometryFilter_groupId

Now we'll define the actual class, this will inherit from the MPxDeformerNode as this is a simple deformer.

class deformer(OpenMayaMPx.MPxDeformerNode):
    def __init__(self):
        OpenMayaMPx.MPxDeformerNode.__init__(self)

You will have to set a type id and a type name to be used in the initializePlugin method.

type_id = OpenMaya.MTypeId(0x00001)  
type_name = "deformer"

The initalizePlugin method also needs a creator and an initalize method so we'll define these as well.
The creator should be a clas method as it does not use any instance data and will need to be callable without a class instance, this method will simply return a new instance of the class.

@classmethod
def creator(cls):
    return cls()

The initialize method is where we create all the attributes that we want the deformer to have as inputs and outputs, for a collision deformer you'll want to at least have a collider mesh as an input.

@classmethod
def initialize(cls):
    generic_attr_fn = OpenMaya.MFnGenericAttribute()
    cls.collider_attr = generic_attr_fn.create('collider', 'cl')
    generic_attr_fn.addDataAccept( OpenMaya.MFnData.kMesh )
    cls.addAttribute( cls.collider_attr )

This is also where you would define dependencies between in and outputs, so that when an input is changed the corresponding output will be recomputed.

cls.attributeAffects( cls.collider_attr, kOutputGeom )

Now you can add the initializePlugin and uninitializePlugin methods, outside of the class.
The initializePlugin is simply used to register the plugin in Maya, this is also where you set the author and version of the plugin and load any custom Attribute Editor templates.

def initializePlugin(plugin):
    plugin_fn = OpenMayaMPx.MFnPlugin(plugin, "Marieke van Neutigem", "0.0.1")
    plugin_fn.registerNode(
            deformer.type_name,
            deformer.type_id,
            deformer.creator,
            deformer.initialize,
            OpenMayaMPx.MPxNode.kDeformerNode
        )
    mel_eval( gui_template )

The uninitializePlugin method simply deregisters the plugin using the plugin id when the plugin is unloaded from maya.

def uninitializePlugin(plugin):
    plugin_fn = OpenMayaMPx.MFnPlugin(plugin)
    plugin_fn.deregisterNode(deformer.type_id)

Now you can start implementing the deform method, you could implement the compute method instead as you would on a basic deformer but really I recommend only doing that if the deformation is not a simple per vertex operation and requires you to use compute instead.

The deform method gives you access to the data_block, this can be used to retrieve the values of any of the attributes you added to your plugin. You also have a geometry_iterator, this can be used to iterate over all the vertices of the input geometry and modify them on the fly.
Then there's the local to world space matrix that you can use to transform the vertices from the input mesh to world space for any calculations that require that. And lastly, there is the geometry index which is needed to access to the input geometry from the global geometry filter variables.

def deform(self, data_block, geometry_iterator, local_to_world_matrix, geometry_index):

After you've implemented the deform method there's one last thing; you can add a custom attribute editor template. This is useful if you have added some attributes to your plugin but they don't show the way you would want them to. This is a simple Mel script that needs to be evaluated on loading the plugin, the proc has to be named AE[Name of your deformer]Template.

gui_template = """global proc AEdeformerTemplate( string $nodeName )
    {
        editorTemplate -beginScrollLayout;
            editorTemplate -beginLayout "Deformer Attributes" -collapse 0;
                // Add your own attributes here in the way you want them to be displayed.
            editorTemplate -endLayout;
            AEdependNodeTemplate $nodeName;
            editorTemplate -addExtraControls;
        editorTemplate -endScrollLayout;
    }"""

I hope that get's you a bit of a starting point to start writing your own deformers!

You can find the full source code for the mnCollisionDeformer here for reference: https://github.com/mvanneutigem/tutorials/blob/master/plugins/mnCollisionDeformer.py 

mnCollisionDeformer; a collision deformer for Maya.

Work In Progress / 23 May 2020

A collision deformer I'm writing for learning purposes in my spare time.
Not very performant at the minute as it's written in Python and I came up with my own logic rather than using an established algorithm, this also means it's got some issues with edge-cases, but a very good learning exercise.


I will be releasing the code and a tutorial on how to approach something like this when I find some time for code clean up.

Writing a basic maya plugin in python

Tutorial / 17 May 2020

I've been working from home for the last few weeks and with the extra time on my hands, I figured I'd try my hand at creating a tutorial for one of the things that may seem a bit more daunting to get started on; writing Maya plugins.

This is a very basic tutorial going over how to create a simple plugin and should allow you to take what you need from it to start creating your own plugins.

You can find the code from this tutorial on my Github: https://github.com/mvanneutigem/tutorials/blob/master/plugins/demoNode.py