Simple remeshing scripting in 3ds Max 2021

Disclaimer: The code in this post is written on version 9.1 of Simplygon. If you encounter this post at a later stage, some of the API calls might have changed. However, the core concepts should still remain valid.

Introduction

3ds Max 2021 has introduced a new Physical Material system. Simplygon 9.1 introduces support for this material system, and this blog post will show you how to write a simple remeshing Python script using that new Physical Material system in the Simplygon 3ds Max 2021 plug-in.

If you haven't installed the Simplygon 3ds Max 2021 plug-in already, it's time to do so now. Head on over to https://www.simplygon.com and download the latest 9.1 version to try it out. Autodesk 3ds Max plug-ins are installed automatically when running the installer, and after installation you need to restart 3ds Max for it to load successfully (if it was running when you installed Simplygon).

The original asset with material

Since the asset I'll be using in the demo uses multiple materials I will only show a part of a selected material here as example of what the source materials look like. Below is a snapshot of the original asset (~930.000 triangles) together with (a part of) the material definition of the material set to the roof of the vehicle: Original asset with material

The script

We will be using the Simplygon 3ds Max 2021 plug-in, which means we access all the Simplygon methods in script using the sgsdk_ prefixed methods on the 3ds Max runtime object rt.

Cleaning the scene and loading the asset

To make the script possible to run repeatedly without side-effects, we will start off the script by cleaning the scene and (re-)loading the asset every time the script is run. Please note that this might not be how you typically should write your scripts, and rather meant only for demo purposes in this blog post (for you to more easily iterate):

from pymxs import runtime as rt

# Reset scene and load asset
rt.resetMaxFile(rt.Name('noPrompt'))
rt.sgsdk_ClearPipelines()
rt.sgsdk_Reset()
rt.importFile('<path to asset>', rt.Name('noPrompt'))

# Select all objects in the scene
rt.select(rt.objects)

# Run pipeline on selection
run_pipeline()

Since we're cleaning the scene every time the script runs, it's safe to run rt.select(rt.objects) (select all objects in the scene) since we know we want to process everything in the scene.

The run_pipeline() method

So, now we have cleared the scene, loaded the asset and selected everything that we want to process. It's time to write the run_pipeline() method.

We first need to define the method itself and create a shading network template (for each material) that will be used by Simplygon. Please note that this is not the actual material that will end up on the processed object, but rather meta data for Simplygon to use when applying a baked representation of the shading network on the processed object:

def run_pipeline():
    pb_material = rt.sgsdk_CreateMaterialMetaData('01 - default')

For this asset we decide that we want to grab the base_color_map texture nodes of each material and bake them into a new material. To do that we create Simplygon texture nodes for each 3ds Max texture slot we want to bake and specify what Simplygon material channel to connect the texture to. Check out our documentation on the mappings of physical materials for more information. In this example we are demonstrating how to override a material channel's input source (base_color in this case) to use the texture base_color_map as the source. All color and texture properties in the Physical Material are mappable and can be combined using shading nodes. Please see the mapping table in the documentation for more information.

    # Map the material channels
    base_color_texture = rt.sgsdk_CreateShadingTextureNode('base_color_map')
    rt.sgsdk_ConnectNodeToChannel(base_color_texture, pb_material, 'base_color')

Now it's time to set up the remeshing pipeline object together with some general settings like texture size:

    # Setup the remeshing pipeline
    remeshing_pipeline = rt.sgsdk_CreatePipeline('RemeshingPipeline')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'GlobalSettings/DefaultTangentCalculatorType', 1)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/RemeshingSettings/OnScreenSize', 300)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/RemeshingSettings/HoleFilling', 1)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/GenerateMappingImage', True)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/GenerateTexCoords', True)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/TexCoordName', 'MaterialLOD')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/Output0/TextureWidth', 1024)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/Output0/TextureHeight', 1024)

And before we run the processing, we want to setup what material channels that should be casted:

    # Add casters for the textures we setup previously and other channels that we want
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/0/ColorCasterSettings/MaterialChannel', 'base_color')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'NormalCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/1/NormalCasterSettings/MaterialChannel', 'bump')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/1/NormalCasterSettings/GenerateTangentSpaceNormals', True)
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/2/ColorCasterSettings/MaterialChannel', 'reflectivity')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/3/ColorCasterSettings/MaterialChannel', 'refl_color')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/4/ColorCasterSettings/MaterialChannel', 'roughness')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/5/ColorCasterSettings/MaterialChannel', 'metalness')

Now we're ready to run the processing on the selection, and we do this by calling the sgsdk_RunPipelineOnSelection() method passing the newely created pipeline object as argument:

    # Run the pipeline
    rt.sgsdk_RunPipelineOnSelection(remeshing_pipeline)

The result

This is what the resulting material + processed mesh (~6.400 triangles) looks like: Processing and material result

The complete script

Here's what the final script looks like:

from pymxs import runtime as rt

def run_pipeline():
    pbr_material = rt.sgsdk_CreateMaterialMetaData('01 - default')

    # Map the material channels
    base_color_texture = rt.sgsdk_CreateShadingTextureNode('base_color_map')
    rt.sgsdk_ConnectNodeToChannel(base_color_texture, pbr_material, 'base_color')

    # Setup the remeshing pipeline
    remeshing_pipeline = rt.sgsdk_CreatePipeline('RemeshingPipeline')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'GlobalSettings/DefaultTangentCalculatorType', 1)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/RemeshingSettings/OnScreenSize', 300)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/RemeshingSettings/HoleFilling', 1)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/GenerateMappingImage', True)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/GenerateTexCoords', True)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/TexCoordName', 'MaterialLOD')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/Output0/TextureWidth', 1024)
    rt.sgsdk_SetSetting(remeshing_pipeline, 'RemeshingProcessor/MappingImageSettings/Output0/TextureHeight', 1024)

    # Add casters for the textures we setup previously and other channels that we want
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/0/ColorCasterSettings/MaterialChannel', 'base_color')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'NormalCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/1/NormalCasterSettings/MaterialChannel', 'bump')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/1/NormalCasterSettings/GenerateTangentSpaceNormals', True)
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/2/ColorCasterSettings/MaterialChannel', 'reflectivity')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/3/ColorCasterSettings/MaterialChannel', 'refl_color')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/4/ColorCasterSettings/MaterialChannel', 'roughness')
    rt.sgsdk_AddMaterialCaster(remeshing_pipeline, 'ColorCaster')
    rt.sgsdk_SetSetting(remeshing_pipeline, 'MaterialCaster/5/ColorCasterSettings/MaterialChannel', 'metalness')

    # Run the pipeline
    rt.sgsdk_RunPipelineOnSelection(remeshing_pipeline)

# Load Max scene
rt.resetMaxFile(rt.Name('noPrompt'))
rt.sgsdk_ClearPipelines()
rt.sgsdk_Reset()
rt.importFile('<path to asset>', rt.Name('noPrompt'))

# Select all objects in the scene
rt.select(rt.objects)

# Run pipeline on selection
run_pipeline()
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*