TA logoThe Technical Artist
home/simplygon/standalone usage
simplygon
python
standalone

Standalone Python Processing

Workingtutorial~5 min readreviewed 2026-07-05

This is Simplygon with no DCC open at all: just Python, a folder of assets, and a script that chews through them. It is the version you put on a build machine, the version that runs overnight, and honestly the version where the SDK makes the most sense. Learn this first and the Maya page gets easier after.

01Setup Assumptions

Everything here is written against SDK 10.4 with the simplygon10 Python package. The pip package is just the bindings - you still need the actual SDK installed and licensed on the machine, or nothing below will import.

  • Match your Python version to what your SDK build supports (check the docs for your exact version).
  • Before building anything real, open a Python shell and try from simplygon10 import simplygon_loader, Simplygon. If that line works, you're in business. If not, fix that first - nothing else matters yet.
  • Once settings become "how the studio does it," get them into source control as JSON. Future-you will want the history.
  • Log everything - imports, exports, warnings, the lot. A batch job that fails silently at 2am is a special kind of misery.
install editable reference bundle
python -m pip install -e ".[dev]"

02Initialize The SDK

Initialize once, reuse the instance everywhere, let it go when the process ends. The SDK holds global state, so spinning up multiple instances is asking for weirdness - that's why most people wrap it in a singleton and move on with their lives.

minimal initialization
import gc
from simplygon10 import simplygon_loader, Simplygon

def create_simplygon():
 sg = simplygon_loader.init_simplygon()
 if sg is None:
 raise RuntimeError(Simplygon.GetLastInitializationError())
 print(f"Simplygon {sg.GetVersion()} initialized")
 return sg

def shutdown_simplygon(sg):
 sg = None
 gc.collect()
singleton pattern

In a larger package, wrap initialization in a small SimplygonInstance class so every command shares one SDK instance and your tests can reset it deliberately.

03Import And Export Scenes

Let Simplygon's own importer and exporter handle files - FBX, OBJ, glTF, USD, whatever your SDK build speaks. Same two helpers for every format, no per-format special-casing to maintain.

scene io helpers
from pathlib import Path
from simplygon10 import Simplygon

def load_scene(sg, path):
 importer = sg.CreateSceneImporter()
 importer.SetImportFilePath(str(path))
 result = importer.Run()
 if Simplygon.Failed(result):
 raise RuntimeError(f"Failed to import scene: {path}")
 return importer.GetScene()

def save_scene(sg, scene, path):
 Path(path).parent.mkdir(parents=True, exist_ok=True)
 exporter = sg.CreateSceneExporter()
 exporter.SetScene(scene)
 exporter.SetExportFilePath(str(path))
 result = exporter.Run()
 if Simplygon.Failed(result):
 raise RuntimeError(f"Failed to export scene: {path}")

And always read the log after a run. The warnings are where Simplygon tells you the interesting stuff - missing materials, dodgy UVs, geometry it quietly skipped. Ignore them and you'll "discover" those problems in-engine instead.

log helper
def check_simplygon_log(sg):
 if sg.ErrorOccurred():
 errors = sg.CreateStringArray()
 sg.GetErrorMessages(errors)
 for i in range(errors.GetItemCount()):
 print(f"Error: {errors.GetItem(i)}")
 sg.ClearErrorMessages()
 raise RuntimeError("Simplygon reported errors")

 if sg.WarningOccurred():
 warnings = sg.CreateStringArray()
 sg.GetWarningMessages(warnings)
 for i in range(warnings.GetItemCount()):
 print(f"Warning: {warnings.GetItem(i)}")
 sg.ClearWarningMessages()

04Basic Triangle Reduction

Reduction is the everyday one - fewer triangles, same mesh, nobody's editing the result by hand anyway. The SDK gives you two ways in: the Processor API (explicit, you hold every piece) and the Pipeline API (one object that wraps the whole job and can be saved as JSON). Rule of thumb: pipelines for anything real, processors when you need to poke at the scene mid-process. Here's both, so you can see the difference:

processor api reduction
from simplygon10 import Simplygon

def reduce_with_processor(sg, input_path, output_path, ratio=0.5):
 scene = load_scene(sg, input_path)

 processor = sg.CreateReductionProcessor()
 processor.SetScene(scene)

 settings = processor.GetReductionSettings()
 settings.SetReductionTargets(
 Simplygon.EStopCondition_All,
 True, # triangle ratio
 False, # triangle count
 False, # max deviation
 False, # on-screen size
 )
 settings.SetReductionTargetTriangleRatio(ratio)

 processor.RunProcessing()
 check_simplygon_log(sg)
 save_scene(sg, scene, output_path)
pipeline api reduction
from simplygon10 import Simplygon

def reduce_with_pipeline(sg, input_path, output_path, ratio=0.5):
 pipeline = sg.CreateReductionPipeline()
 settings = pipeline.GetReductionSettings()
 settings.SetReductionTargets(
 Simplygon.EStopCondition_All,
 True, False, False, False
 )
 settings.SetReductionTargetTriangleRatio(ratio)

 pipeline.RunSceneFromFile(
 str(input_path),
 str(output_path),
 Simplygon.EPipelineRunMode_RunInThisProcess,
 )
 check_simplygon_log(sg)
targeting rule

Ratio ("give me 50%") is easy to reason about, so it's where everyone starts. But screen size ("make it look right at 300 pixels") is what you actually want for LODs - it lets Simplygon decide how many triangles each asset needs instead of you guessing. The tips page has the full rant.

05Generate An LOD Chain

Notice each LOD gets reduced from a fresh copy of the original, not from the previous LOD - that way errors don't compound down the chain. The classic screen-size ladder is 600 / 300 / 100 pixels, and the _LOD0 naming isn't cosmetic: it's what engines auto-import.

screen-size lod chain
from pathlib import Path
from simplygon10 import Simplygon

def reduce_for_screen_size(sg, original_scene, screen_size):
 scene_copy = original_scene.NewCopy()
 pipeline = sg.CreateReductionPipeline()

 settings = pipeline.GetReductionSettings()
 settings.SetReductionTargets(
 Simplygon.EStopCondition_Any,
 False, # triangle ratio
 True, # on-screen size
 False, # triangle count
 False, # max deviation
 )
 settings.SetReductionTargetOnScreenSize(screen_size)

 pipeline.RunScene(scene_copy, Simplygon.EPipelineRunMode_RunInThisProcess)
 check_simplygon_log(sg)
 return pipeline.GetProcessedScene()

def create_lod_chain(sg, input_path, output_dir, screen_sizes=(600, 300, 100)):
 original_scene = load_scene(sg, input_path)
 output_dir = Path(output_dir)

 for index, size in enumerate(screen_sizes):
 lod_scene = reduce_for_screen_size(sg, original_scene, size)
 output_path = output_dir / f"{Path(input_path).stem}_LOD{index}.fbx"
 save_scene(sg, lod_scene, output_path)

06Remeshing Proxies

Remeshing throws the original topology in the bin and builds a fresh shell around whatever's visible. Sounds drastic. It is. That's why it's perfect for proxies, HLODs, and 3D scans where the topology was garbage anyway. The catch: new mesh means new UVs, so the original textures no longer fit. The casters below handle that by re-baking color, normals, and friends onto the new surface.

remesh with material casting
from simplygon10 import Simplygon

def remesh_scene(sg, scene, screen_size=300, texture_size=1024):
 pipeline = sg.CreateRemeshingPipeline()

 remesh_settings = pipeline.GetRemeshingSettings()
 remesh_settings.SetOnScreenSize(screen_size)

 mapping = pipeline.GetMappingImageSettings()
 mapping.SetGenerateMappingImage(True)
 output_material = mapping.GetOutputMaterialSettings(0)
 output_material.SetTextureWidth(texture_size)
 output_material.SetTextureHeight(texture_size)

 color = sg.CreateColorCaster()
 color.GetColorCasterSettings().SetMaterialChannel("Diffuse")
 pipeline.AddMaterialCaster(color, 0)

 normal = sg.CreateNormalCaster()
 normal_settings = normal.GetNormalCasterSettings()
 normal_settings.SetMaterialChannel("Normals")
 normal_settings.SetGenerateTangentSpaceNormals(True)
 pipeline.AddMaterialCaster(normal, 0)

 pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
 check_simplygon_log(sg)
 return pipeline.GetProcessedScene()

One gotcha worth knowing now: channel names follow the source format. FBX speaks Diffuse and Normals; glTF speaks PBR names like Basecolor. When a bake comes out black, the channel name is suspect number one.

07Aggregation And Material Merging

Aggregation is the "leave my triangles alone" option: it merges many objects into fewer meshes, and with casters attached it bakes all those material slots down to one atlas. Draw calls plummet, silhouettes stay pixel-identical. Kitbash props and modular buildings love this.

merge geometry and bake maps
from simplygon10 import Simplygon

def merge_materials(sg, scene, texture_size=2048):
 pipeline = sg.CreateAggregationPipeline()

 aggregation = pipeline.GetAggregationSettings()
 aggregation.SetMergeGeometries(True)

 mapping = pipeline.GetMappingImageSettings()
 mapping.SetGenerateMappingImage(True)

 output_material = mapping.GetOutputMaterialSettings(0)
 output_material.SetTextureWidth(texture_size)
 output_material.SetTextureHeight(texture_size)

 color = sg.CreateColorCaster()
 color.GetColorCasterSettings().SetMaterialChannel("Diffuse")
 pipeline.AddMaterialCaster(color, 0)

 normal = sg.CreateNormalCaster()
 normal.GetNormalCasterSettings().SetMaterialChannel("Normals")
 normal.GetNormalCasterSettings().SetGenerateTangentSpaceNormals(True)
 pipeline.AddMaterialCaster(normal, 0)

 pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
 check_simplygon_log(sg)
 return pipeline.GetProcessedScene()

If one giant atlas would blow your texture budget - or a hero material needs to keep its fancy shader - split into multiple output materials instead of cramming everything into one 8K sheet.

08Validate Before Processing

Batch jobs live or die on this: check the asset before you spend two minutes processing it. Degenerate triangles, missing UVs, broken material refs, bad bone weights - catch them at the door and your overnight run finishes instead of face-planting on asset 12 of 400.

Check Why It Matters
Triangles and normalsBad geometry can create reduction artifacts, holes, or unusable mapping images.
UV setsMaterial casters need valid UVs for baking and texture output.
MaterialsMissing material channels become missing baked textures.
Skin weightsCharacter reduction and bone reduction must preserve valid influence data.
Scale and transformsScreen-size targets and camera visibility are only meaningful if input scale is sane.
validation shell
def validate_before_processing(scene):
 issues = []

 # Fill this with studio-specific checks:
 # - required UV set exists
 # - required material channels exist
 # - no empty meshes
 # - no unsupported file format edge cases
 # - skeletal meshes have valid weights

 if issues:
 raise RuntimeError("\n".join(issues))

09Batch Processor Shape

Here's where it all comes together: a script that eats a folder. The useful bits are boring but non-negotiable: one bad mesh should not kill the batch, every failure needs a clear log entry, and output paths need to be predictable enough for the next tool to find them.

batch reduction cli
import argparse
from pathlib import Path

def iter_assets(root, extensions=(".fbx", ".obj", ".gltf", ".glb")):
 root = Path(root)
 for path in root.rglob("*"):
 if path.suffix.lower() in extensions:
 yield path

def process_folder(input_dir, output_dir, ratio=0.5):
 sg = create_simplygon()
 try:
 for source in iter_assets(input_dir):
 relative = source.relative_to(input_dir)
 output = Path(output_dir) / relative
 try:
 print(f"Processing {source}")
 reduce_with_pipeline(sg, source, output, ratio=ratio)
 except Exception as exc:
 print(f"FAILED {source}: {exc}")
 finally:
 shutdown_simplygon(sg)

if __name__ == "__main__":
 parser = argparse.ArgumentParser()
 parser.add_argument("input_dir")
 parser.add_argument("output_dir")
 parser.add_argument("--ratio", type=float, default=0.5)
 args = parser.parse_args()

 process_folder(args.input_dir, args.output_dir, args.ratio)
next upgrade

Once this runs clean, the fun upgrades queue up naturally: a progress observer so long jobs aren't a black box, a JSON summary your producer can read, serialized pipeline files per platform, and - if your license covers it - distributed processing across the farm. Each one is an afternoon, not a project.