Skip to content
The Technical ArtistArt. Code. Pipelines. Games.
home/simplygon/standalone usage
simplygon
python
standalone

Standalone Python Processing

Workingtutorial~5 min read

The standalone workflow runs Simplygon from Python without opening a DCC. It’s useful for unattended processing and build-machine jobs. Start with one asset and inspect its output before extending the script to a folder.

On this page Setup Initialise Import / Export Reduction LOD Chains Remeshing Material Merge Validation Batch Jobs

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 the import fails, check the installation and supported Python version before continuing.
  • 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. Include the asset name and settings in error messages so a failed job can be reproduced.
install editable reference bundle
python -m pip install -e ".[dev]"

02Initialise The SDK

Initialise once, reuse the instance everywhere, let it go when the process ends. The SDK holds global state, so share one initialised instance within the process and control its lifetime explicitly.

minimal initialisation
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 initialisation 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). Use pipelines when saved settings and repeatable jobs are the priority; use processors when you need more direct control of the processing steps. 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 compares the target types.

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 creates a new surface instead of preserving the original topology. It can suit proxies, HLODs, and scans where the rendered appearance matters more than editable edge flow. Because the new mesh needs new UVs, material casters bake the source channels onto it.

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. This can reduce draw calls while keeping the source geometry. Check atlas resolution, material features, and engine behaviour on the merged result.

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

Validate the source before processing it. Missing UVs, degenerate triangles, broken material references, and invalid skin weights can all affect the output. Log those problems per asset so a failure can be investigated without rerunning the whole batch.

Check Why It Matters
Triangles and normalsBad geometry can create reduction artefacts, 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 processes a folder. The rules that make it production-grade are boring but non-negotiable - one bad mesh doesn't kill the batch, every failure gets logged with a name, and output paths are predictable enough that the next tool in the chain can 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, serialised pipeline files per platform, and - if your licence covers it - distributed processing across the farm. Add the next feature when the team has a clear need for it, and test it on a small batch first.