Submitting MAXScript Jobs Using MAXScript

Introduction

In a previous tutorial, we looked at the submission of regular 3ds Max Render Jobs to Deadline using a custom submission script written in MAXScript.

However, Deadline allows a 3ds Max Job to run in a special MAXScript Job mode where instead of rendering images using any supported production renderer, a MAXScript file is run on each frame instead. The MAXScript file can contain code that performs any operations supported by 3ds Max and MAXScript, and this means a lot of power. For example, a MAXScript could perform file import, scene creation and manipulatiion, file export, file caching, physical simulations and other functions that do not involve rendering.

Notable examples from the world of Thinkbox Software's own 3ds Max plugins are the Krakatoa Partitioning on Deadline feature which employs (optionally) MAXScript Jobs to save particles with modified random seeds using several tasks within a single job, or the XMesh Saver caching on Deadline which also uses a MAXScript Job to run the XMesh caching functions under MAXScript control on one or more render machines.

Note that there are some limitations imposed on MAXScript when 3ds Max is run in license-free network rendering mode (the default mode of 3ds Max when run by Deadline). In this mode, the UI is not created, so any operations that depend on UI and Viewport interaction would fail. Also, the saving of .MAX files is disabled in this mode. To work around this, Deadline can launch 3ds Max in full Workstation mode, but this will require a 3ds Max license, so 3ds Max must be licensed on the machine running the Deadline Slave - either via a floating license, or by using a node-locked license (for example if an artist workstation is also used to run Deadline jobs).

In this tutorial, we will discuss the basics of MAXScript Jobs, and then we will see how a MAXScript Job can be submitted by a custom MAXScript submitter by slightly modifying the code developed in the previous tutorial.

Submitting A MAXScript Job Manually Using SMTD

A MAXScript Job can be submitted using the Integrated 3ds Max Submitter (SubmitMaxToDeadline, a.k.a. SMTD). In the SMTD user interface, the "Scripts" tab contains the following relevant controls in the "Run MAXScript Scripts" rollout > "MAXScript Scene Processing Job - No Rendering" group of controls:

Submit Script Job

  • This checkbox must be checked to switch the submission from a regular Render Job to a MAXScript Job.
  • In this mode, the renderer will not be called, instead, the script specified in the text field below will be sent to the Slaves to execute on each frame. 

Single Task

  • When checked, the job will be sent as a single task, one frame Job to ensure the script will be run only once. 
  • This is useful when the script does not need to run once per frame under Deadline's control.
  • However, the script itself could contain one or more FOR loops that perform multiple operations within the Task, for example the Krakatoa Partitioning job (Task as Partition option) runs a FOR loop through all frames in the range to save within the script, thus outputting multiple files from a single Task.

Workstation Mode

  • When checked, 3ds Max will be launched in full UI licensed workstation mode.
  • This is only needed when the script in question requires access to the UI, Viewports, or the .MAX file saving system of 3ds Max.
  • Possible examples include 
    • Importing multiple OBJ or other external files into a 3ds Max scene and saving a new .MAX file with the result.
    • Creating Viewport Animation previews by advancing the time slider and capturing the Viewport image into an animation sequence.
    • Running automated tools that need the UI to be available, for example scripting UV Unwrap processing.

New Script From Template

  • Pressing this button will generate a new MAXScript Job containing the base "boilerplate" code with empty space for the custom MAXScript functionality.
  • The new script will be saved automatically under the user's \Scripts 
  • The script can be modified, saved and reused in future sessions.

Pick Script...

  • Pressing this button lets you pick a previously saved script to submit as the Job's script.
  • Note that Jon Scripts have a specific syntax, so you cannot pick any script, only a script that was specifically written to be run on Deadline. We will discuss this in a bit.

Edit MAXScript File

  • Pressing this button will open the file picked as the Job Script in the MAXScript Editor for further editing.

 

The MAXScript Job Template

Here is the content of the MAXScript Job template script created when you press the New Script From Template button:

(
	local du = DeadlineUtil--this is the interface exposed by the Lightning Plug-in which provides communication between Deadline and 3ds Max
	if du == undefined do--if the script is not being run on Deadline (for testing purposes),
	(
		struct DeadlineUtilStruct --define a stand-in struct with the same methods as the Lightning plug-in
		(
			fn SetTitle title = ( format "Title: %\n" title ),
			fn SetProgress percent = (true),
			fn FailRender msg = ( throw msg ),
			fn GetSubmitInfoEntry key = ( undefined ),
			fn GetSubmitInfoEntryElementCount key = ( 0 ),
			fn GetSubmitInfoEntryElement index key = ( undefined ),
			fn GetJobInfoEntry key = ( undefined ),
			fn GetAuxFilename index = ( undefined ),
			fn GetOutputFilename index = ( undefined ),
			fn LogMessage msg = ( format "%\n" msg ),
			fn WarnMessage msg = ( format "Warning: %\n" msg ),
			CurrentFrame = ((sliderTime as string) as integer),
			CurrentTask = ( -1 ),
			SceneFileName = ( maxFilePath + maxFileName ),
			SceneFilePath = ( maxFilePath ),
			JobsDataFolder = ( "" ),
			PluginsFolder = ( "" )
		)
		du = DeadlineUtilStruct() --create an instance of the stand-in struct
	)--end if
	
	du.SetTitle "MAXScript Job" --set the job title 
	du.LogMessage "Starting MAXScript Job..." --output a message to the log
	local st = timestamp() --get the current system time
	
	
	
	--YOUR SCENE PROCESSING CODE GOES HERE
	
	
	
	du.LogMessage ("Finished MAXScript Job in "+ ((timestamp() - st)/1000.0) as string + " sec.") --output the job duration
	true--return true if the task has finished successfully, return false to fail the task.
)--end script

What is DeadlineUtil?

Deadline communicates with 3ds Max via a plugin called Lightning.DLX. DLX files are 3ds Max plugins that extend the MAXScript system. The Lightning.DLX is used by Deadline to talk to 3ds Max when running a Task of a Job, but also to expose some Deadline functionality to MAXScript for your own needs. The MAXScript Interface it provides is called DeadlineUtil.

However, this Interface is only available when the Lightning.DLX plugin has been loaded, which only happens when Deadline Slave launches 3ds Max. This means that you cannot really call ShowInterface DeadlineUtil to see what properties and methods it offers while running 3ds Max on your workstation where development of MAXScript jobs typically happens. For that reason, the script first assigns DeadlineUtil to a local user variable called `du`, then checks to see if it is defined or not. If it is undefined, the script creates a Struct with the same properties and methods and assigns it to the du local variable instead of the real interface. This way, you can test your script AS IF it was running on Deadline without actually sending it to Deadline!

Of course, some of the properties and methods are just dummy placeholders, but the majority perform about the same as the real ones. For example. du.CurrentTime will return the current frame both locally and on Deadline, but the Deadline result will be based on the Frame the Task wants to process, while in the local version it just returns the current SliderTime.

Once that is done, the script uses the calls to du.SetTitle and LogMessage to output some data to the Deadline Log (or to the MAXScript Listener, of not running on a Slave). You can modify these calls to output the data you want, and add more such calls to inform the user of what is going on, or for debugging purposes.

Then the script captures the current system time in a variable in order to provide precise timing info in the Log independent from the Task's own timing managed by Deadline.

The area of --YOUR SCENE PROCESSING CODE GOES HERE is where the actual script would go - without any changes, the script would really Do Nothing.

Once that code is executed, the script would print to the Log the time it took to run that code, and finally return true which tells Deadline that the task finished successfully. Of course, you can return false if some condition in your own script code points at a problem that should make the task fail. In addition, at any point in your code you can call du.FailRender with a message as argument which will fail the job and output the message to the Log.

If the script does not return true in the end, the Task will fail.

 

Simple Example Script

To see how a MAXScript Job is typically developed, let's create a simple script that will export the geometry objects in the scene to sequences of 3DS files in a network folder.

  • Open 3ds Max
  • Create a default Teapot primitive in the scene
  • Add a Bend modifier to it and keyframe the Angle from 0 to 100 to change from 0 to 90 degrees.
  • Set the Render Setup dialog to Time Output : Active Time Segment 0 to 100.
  • Save the scene to your local Scenes folder under the name "MeshExportOnDeadlineTest_v001.max"
  • Open SMTD.
  • Switch to the Scripts tab.
  • Press the button "New Script From Template" - note that 
    • the name of the script will be taken from the scene name, so the script will be called "MAXScriptJob_MeshExportOnDeadlineTest_v001_xxxxxx.ms", where the xxxxxx is an automatically generated ID number.
    • The script will be opened in the MAXScript Editor automatically
    • The Submit Script Job option will be checked automatically.
  • Edit the script to modify the job title and message
  • Enter the export code in place of the  --YOUR SCENE PROCESSING CODE GOES HERE line:
(
	local du = DeadlineUtil--this is the interface exposed by the Lightning Plug-in which provides communication between Deadline and 3ds Max
	if du == undefined do--if the script is not being run on Deadline (for testing purposes),
	(
		struct DeadlineUtilStruct --define a stand-in struct with the same methods as the Lightning plug-in
		(
			fn SetTitle title = ( format "Title: %\n" title ),
			fn SetProgress percent = (true),
			fn FailRender msg = ( throw msg ),
			fn GetSubmitInfoEntry key = ( undefined ),
			fn GetSubmitInfoEntryElementCount key = ( 0 ),
			fn GetSubmitInfoEntryElement index key = ( undefined ),
			fn GetJobInfoEntry key = ( undefined ),
			fn GetAuxFilename index = ( undefined ),
			fn GetOutputFilename index = ( undefined ),
			fn LogMessage msg = ( format "%\n" msg ),
			fn WarnMessage msg = ( format "Warning: %\n" msg ),
			CurrentFrame = ((sliderTime as string) as integer),
			CurrentTask = ( -1 ),
			SceneFileName = ( maxFilePath + maxFileName ),
			SceneFilePath = ( maxFilePath ),
			JobsDataFolder = ( "" ),
			PluginsFolder = ( "" )
		)
		du = DeadlineUtilStruct() --create an instance of the stand-in struct
	)--end if
	
	du.SetTitle "MAXScript 3ds Animation Export Job" --set the job title 
	du.LogMessage "Starting MAXScript Job..." --output a message to the log
	local st = timestamp() --get the current system time
	
	for o in geometry do
	(
		local paddingString = formattedPrint du.currentFrame format:"04i"
		local theFile = (@"S:\DATA\temp\"+o.name+"_"+paddingString+".3ds")
		du.LogMessage ("Exporting Object ["+ o.name + "] to [" + theFile + "]")
		select o
		exportFile theFile #noPrompt selectedOnly:true
	)
	
	du.LogMessage ("Finished MAXScript Job in "+ ((timestamp() - st)/1000.0) as string + " sec.") --output the job duration
	true--return true if the task has finished successfully, return false to fail the task.
)--end script

The script

  • Loops through all geometry objects in the scene (in our case just the Teapot). 
  • It builds a padding string with the frame number and leading zeros.
  • It builds a file name using a hard-coded path to a mapped network drive visible to all render nodes that also includes the object name, the padded frame number, and the format extension .3DS.
  • It outputs a message with the object name and the file name.
  • It then selects the object and exports the current selection to the file without a prompt.

Submitting the scene to Deadline will create 101 tasks. A copy of the script file will be sent as Auxiliary file with the Job, and will be run by each Slave. Once the job is processed, 101 .3DS files should appear in the output folder. Each should contain a snapshot of the teapot on the respective frame.

Submitting A MAXScript Job With A Custom MAXScript Submitter

Now that we have a clearer idea how a MAXScript Job is submitted using the SMTD UI, let's look at doing the same using a custom scripted submitter.

We can take the intermediate (UI-less) version of the MAXScript Submitter developed in the previous tutorial, and just add a few lines to turn it into a MAXScript Job Submitter:

(
global SMTDSettings
global SMTDFunctions

local theNetworkRoot = @"\\GATEWAY\DeadlineRepository"
local remoteScript = theNetworkRoot + @"\submission\3dsmax\main\SubmitMaxToDeadline_Functions.ms"
local localScript = getDir #userscripts + "\\SubmitMaxToDeadline_Functions.ms"
if doesFileExist remoteScript do
(
if SMTDFunctions == undefined do
(
deleteFile localScript
copyFile remoteScript localScript
fileIn localScript
)

SMTDFunctions.loadSettings()
SMTDSettings.JobName = maxFileName + " [MAXScript Job Test]" 
SMTDSettings.Comment = "Saving 3DS File Sequences using a simple MAXScript Job."
SMTDSettings.Priority = 50
SMTDSettings.ChunkSize = 10
SMTDSettings.SubmitAsMXSJob = true
local MAXScriptFile = @"C:\Users\YOURUSERNAME\AppData\Local\Autodesk\3dsMax\2016 - 64bit\ENU\scripts\SubmitMaxToDeadline\MAXScriptJob_MeshExportOnDeadlineTest_v001_933485.ms"

local maxFileToSubmit = SMTDPaths.tempdir + maxFileName
SMTDFunctions.SaveMaxFileCopy maxFileToSubmit

local SubmitInfoFile = SMTDPaths.tempdir + "\\max_submit_info.job"
local JobInfoFile = SMTDPaths.tempdir+ "\\max_job_info.job"

SMTDFunctions.CreateSubmitInfoFile SubmitInfoFile
SMTDFunctions.CreateJobInfoFile JobInfoFile

local initialArgs = "\""+SubmitInfoFile+"\" \""+JobInfoFile+"\" \""+maxFileToSubmit+ "\" \""+ MAXScriptFile +"\" " 
local result = SMTDFunctions.waitForCommandToComplete initialArgs SMTDSettings.TimeoutSubmission

local renderMsg = SMTDFunctions.getRenderMessage() 
SMTDFunctions.getJobIDFromMessage renderMsg

if result == #success then 
(
format "Submitted successfully as Job %.\n\n%\n\n" \
SMTDSettings.DeadlineSubmissionLastJobID renderMsg
)
else 
format "Job Submission FAILED.\n\n%" renderMsg 
)
)--end script

 

Besides the updated Name and Comment, we have removed the MachineLimit settings from the original script. We are still going to process in chunks of 10 though, so the 101 frames will create 10 tasks with 10 frames, and frame 101 will be in a separate task. This will allow 10 machines to save in parallel, but each one will run through 10 frames at a time which is generally faster than running 101 tasks with one frame each.

The main change is the setting of the SubmitAsMXSJob property to True which is equivalent to checking the "Submit Script Job" checkbox.

We define a local variable to hold the name of the MAXScript file we developed in the previous step. Make sure you update the path to the correct filename. In the end, we add this filename to the list of initial arguments, right after the .MAX scene file.

This is all that is needed to turn a scripted submitter from a Render Job submitter into a MAXScript Job submitter!

Try closing 3ds Max, starting it again without opening SMTD, load the file saved in the first part of the tutorial, and then evaluate this script to run it. The result should be another Job in the Monitor that perform the same saving, but in chunks of 10!

 

Let's Do This Again, But With FumeFX!

One of the typical uses of MAXScript Jobs in production is simulating 3rd party plugins like FumeFX on Deadline. FumeFX exposes functions that let you trigger the simulation the same way you can simulate by pressing the respective UI button, so creating a FumeFX simulation script for Deadline is relatively straight-forward. Such a script could go to great lengths to make the process fool-proof, and there are a large number of such scripts available on the Web already. Here, we will look once again at the absolute minimum requirements, and you can expand from there.

Our script is going to simulate multiple FumeFX grids in one Job. Each FumeFX simulation will get its own Task running the same MAXScript. To do this, we will have to pass to the Job the names of the FumeFX objects we want simulated, and resolve the actual object to process in each Task based on the Frame number accessible through the DeadlineUnit interface. 

Here is the submission script:

(
global SMTDSettings
global SMTDFunctions
	
local FumeFXObjectsToSim = for o in selection where classof o == FumeFX collect o.name	
if FumeFXObjectsToSim.count == 0 do return "No FumeFX Objects Selected!"
local theNetworkRoot = @"\\GATEWAY\DeadlineRepository"
local remoteScript = theNetworkRoot + @"\submission\3dsmax\main\SubmitMaxToDeadline_Functions.ms"
local localScript = getDir #userscripts + "\\SubmitMaxToDeadline_Functions.ms"
if doesFileExist remoteScript do
(
if SMTDFunctions == undefined do
(
deleteFile localScript
copyFile remoteScript localScript
fileIn localScript
)

SMTDFunctions.loadSettings()
SMTDSettings.JobName = maxFileName + " [FumeFX Sims MAXScript Job]" 
SMTDSettings.Comment = "Simulating FumeFX using a simple MAXScript Job."
SMTDSettings.Priority = 50
SMTDSettings.ChunkSize = 1
SMTDSettings.SubmitAsMXSJob = true
renderSceneDialog.close()
rendTimeType = 3
rendStart = 1
rendEnd = FumeFXObjectsToSim.count 
local MAXScriptFile = @"C:\Users\Borislav\AppData\Local\Autodesk\3dsMax\2013 - 64bit\ENU\scripts\SubmitMaxToDeadline\MAXScriptJob_SimulateFumeFX.ms"

local maxFileToSubmit = SMTDPaths.tempdir + maxFileName
SMTDFunctions.SaveMaxFileCopy maxFileToSubmit

local SubmitInfoFile = SMTDPaths.tempdir + "\\max_submit_info.job"
local JobInfoFile = SMTDPaths.tempdir+ "\\max_job_info.job"

SMTDFunctions.CreateSubmitInfoFile SubmitInfoFile
SMTDFunctions.CreateJobInfoFile JobInfoFile
theHandle = openFile JobInfoFile mode:"at"
with printAllElements on format "FumeFXToSimulate=%" FumeFXObjectsToSim to:theHandle
close theHandle

local initialArgs = "\""+SubmitInfoFile+"\" \""+JobInfoFile+"\" \""+maxFileToSubmit+ "\" \""+ MAXScriptFile +"\" " 
local result = SMTDFunctions.waitForCommandToComplete initialArgs SMTDSettings.TimeoutSubmission

local renderMsg = SMTDFunctions.getRenderMessage() 
SMTDFunctions.getJobIDFromMessage renderMsg

if result == #success then 
(
format "Submitted successfully as Job %.\n\n%\n\n" \
SMTDSettings.DeadlineSubmissionLastJobID renderMsg
)
else 
format "Job Submission FAILED.\n\n%" renderMsg 
)
)--end script

In the above script, we 

  • Collect the names of all selected FumeFX Grids. If no grids are selected, we print a message to the Listener.
  • We set the name of the job and the comment to reflect the new functionality.
  • We close the Render Setup dialog if it is open, and then set the time mode to custom range, set the first frame to 1 and the last to the number of selected FumeFX objects. This will cause the necessary number of tasks to be created in the Job, one for each FumeFX object!
  • We set our MAXScript path to a new script that will perform the simulation, see further below.
  • We open the Job Info file for append writing and add a line containing the array of the selected FumeFX objects' names. We will use it to access the object to process in each Task!

 

And this is the Job Script that will trigger the actual simulation on the Deadline Slave:

(
	local du = DeadlineUtil--this is the interface exposed by the Lightning Plug-in which provides communication between Deadline and 3ds Max
	if du == undefined do--if the script is not being run on Deadline (for testing purposes),
	(
		struct DeadlineUtilStruct --define a stand-in struct with the same methods as the Lightning plug-in
		(
			fn SetTitle title = ( format "Title: %\n" title ),
			fn SetProgress percent = (true),
			fn FailRender msg = ( throw msg ),
			fn GetSubmitInfoEntry key = ( undefined ),
			fn GetSubmitInfoEntryElementCount key = ( 0 ),
			fn GetSubmitInfoEntryElement index key = ( undefined ),
			fn GetJobInfoEntry key = ( undefined ),
			fn GetAuxFilename index = ( undefined ),
			fn GetOutputFilename index = ( undefined ),
			fn LogMessage msg = ( format "%\n" msg ),
			fn WarnMessage msg = ( format "Warning: %\n" msg ),
			CurrentFrame = ((sliderTime as string) as integer),
			CurrentTask = ( -1 ),
			SceneFileName = ( maxFilePath + maxFileName ),
			SceneFilePath = ( maxFilePath ),
			JobsDataFolder = ( "" ),
			PluginsFolder = ( "" )
		)
		du = DeadlineUtilStruct() --create an instance of the stand-in struct
	)--end if
	
	du.SetTitle "FumeFX Simulation Job" --set the job title 
	du.LogMessage "Starting FumeFX Simulation Job..." --output a message to the log
	local st = timestamp() --get the current system time
	
	local theFumeFX = execute (du.GetJobInfoEntry "FumeFXToSimulate")
	local theIndex = du.CurrentFrame
	if theIndex < 1 or theIndex > theFumeFX.count do du.FailRender "Index Out Of Range!"
	local theObject = getNodeByName theFumeFX[theIndex]
	theObject.BackBurnerSim = true
	theObject.RunSimulation 0	
	
	du.LogMessage ("Finished FumeFX Simulation Job in "+ ((timestamp() - st)/1000.0) as string + " sec.") --output the job duration
	true--return true if the task has finished successfully, return false to fail the task.
)--end script

 

In this script, we read the value of the array we wrote to the Job Info file and execute it to convert to a MAXScript Array value again. Then we take the current frame and if it is not in the valid range between 1 and the number of names in the array, we fail the task. This could happen if someone changed the frame list of the job!

Then we take the object name with that index and resolve the actual FumeFX object by name.

We set its Backburner flag to true to enable network simulation, and then call RunSimulation with argument 0 to perform the actual simulation.

Note that we assume the user already set the simulation path to a valid network drive accessible to all machines. You could enhance the script to set the path programmatically to a desired network path at submission time.