Scripting with Simplygon: Introduction

Written by Samuel Rantaeskola, Product Expert, Simplygon

Disclaimer: The code in this post is written on version 10.3.500 of Simplygon. Should you encounter this post running a different version, some of the API calls might differ. However, the core concepts should still remain valid.

Introduction

Simplygon provides a variety of scripting options to cater to different levels of control over input and output. This post explores four levels of scripting depth, discussing the advantages and disadvantages of each approach. The goal is to educate you on the different scripting options, rather than demonstrating various optimization techniques. To keep things simple, we'll use a basic sphere and a straightforward 50% reduction in all the examples.

While our examples use Python, the same concepts apply to C# and C++ as well.

Simple Pipeline Usage

If you need to quickly optimize assets stored in a file format that Simplygon supports, you can leverage pipelines in a straightforward manner.

First, export a pipeline file from one of the integrations. Below is an image showing how to export a pipeline from Maya:

Pipeline export from Maya

The simplest script in Simplygon loads the pipeline and applies it to the incoming object in just five lines of code:

def simplest_script(sg: Simplygon.ISimplygon, pipeline_path: str, asset_path: str, output_path: str) -> None:
    serializer = sg.CreatePipelineSerializer()
    pipeline = serializer.LoadPipelineFromFile(pipeline_path)
    pipeline.RunSceneFromFile(asset_path, output_path, Simplygon.EPipelineRunMode_RunInThisProcess)

simplest_script(sg, "reduction.json", "sphere.fbx", "simplest_script_output.fbx")

While this approach is convenient, it offers limited customization options. Modifying the pipeline requires casting it to the correct type and adjusting settings accordingly. Additionally, you have no control over the input or output data.

Programmatic Pipeline Configuration

If you seek more control over the optimization settings, setting up the pipeline programmatically is recommended. You can use the UI to experiment with settings, as they correspond directly to their API counterparts.

Here's a function that demonstrates how to configure the pipeline programmatically while working directly with source data from a file:

def scripting_your_pipeline(sg: Simplygon.ISimplygon, reduction_ratio: float, asset_path: str, output_path: str) -> None:
    pipeline = sg.CreateReductionPipeline()
    pipeline_settings = pipeline.GetReductionSettings()
    pipeline_settings.SetReductionTargetTriangleRatio(reduction_ratio)
    # Instruct the reducer to only use triangle ratio
    pipeline_settings.SetReductionTargets(Simplygon.EStopCondition_All, True, False,False,False)
    pipeline.RunSceneFromFile(asset_path, output_path, Simplygon.EPipelineRunMode_RunInThisProcess)

scripting_your_pipeline(sg, 0.5, "sphere.fbx", "scripting_your_pipeline_output.fbx")    

This approach provides greater flexibility in configuring pipeline settings. By exposing more parameters, you can customize optimization on a per-object basis. However, it still maintains limitations on controlling the input and output data directly.

Manual Scene Loading with Checks

If you require control over the input data, you can load the scene manually before applying optimization. This allows you to perform checks on the input data, such as evaluating polygon count and making decisions based on those checks. Having direct access to the scene data provides enhanced control.

Here's a function that demonstrates how to achieve this:

def scripted_pipeline_working_on_scene(sg: Simplygon.ISimplygon, reduction_ratio: float, scene: Simplygon.spScene) -> Simplygon.spScene:

    pipeline = sg.CreateReductionPipeline()
    pipeline_settings = pipeline.GetReductionSettings()
    pipeline_settings.SetReductionTargetTriangleRatio(reduction_ratio)
    # Instruct the reducer to only use triangle ratio
    pipeline_settings.SetReductionTargets(Simplygon.EStopCondition_All, True, False,False,False)
    
    # If you want to keep the original scene intact you can copy the scene and run the process on that.
    scene_copy = scene.NewCopy()
    pipeline.RunScene(scene_copy, Simplygon.EPipelineRunMode_RunInThisProcess)
    return scene_copy

importer = sg.CreateSceneImporter()
importer.SetImportFilePath("sphere.fbx")
importer.Run()
sphere_scene = importer.GetScene()    
# Here you can do some magic on the scene, prior to running the optimization.
output = scripted_pipeline_working_on_scene(sg, 0.5, sphere_scene)
scene_exporter = sg.CreateSceneExporter()
scene_exporter.SetScene(output)
scene_exporter.SetExportFilePath("scripted_pipeline_working_on_scene_output.fbx")
scene_exporter.Run()

This approach builds upon the previous method by allowing inspection of the input data and modification of the output data post-optimization. This capability opens up numerous possibilities that were not available in the earlier approaches.

Direct Integration with Custom Formats

For those using custom formats who wish to interact directly with the Simplygon scene graph, manually filling in geometry data provides a highly advanced approach. This method offers complete control over both input and output data. Our API documentation includes multiple examples demonstrating how to effectively manage this data.

Before implementing this approach, it is advisable to familiarize yourself with Simplygon's geometry data format. This knowledge will facilitate seamless integration and optimize your workflow.

In this example, we'll generate a sphere programmatically. Here's the code that accomplishes this:

def generate_simplygon_sphere(sg: Simplygon.ISimplygon, radius: int=1, lat_segments: int=20, lon_segments: int=20) -> Simplygon.spScene:

    # Helper function to flatten a list of tuples
    def flatten_tuple_list(list_of_tuples: list) -> list:
        return [item for sublist in list_of_tuples for item in sublist]
    
    vertices = []
    normals = []
    triangles = []
    uvs = []

    for i in range(lat_segments + 1):
        lat = np.pi * i / lat_segments
        sin_lat = np.sin(lat)
        cos_lat = np.cos(lat)

        for j in range(lon_segments + 1):
            lon = 2 * np.pi * j / lon_segments
            sin_lon = np.sin(lon)
            cos_lon = np.cos(lon)

            x = radius * cos_lon * sin_lat
            y = radius * cos_lat
            z = radius * sin_lon * sin_lat
            vertices.append((x, y, z))
            uvs.append((j / lon_segments, i / lat_segments))
            normal = np.array((cos_lon * sin_lat, cos_lat, sin_lon * sin_lat))
            normals.append(normal)
            
    for i in range(lat_segments):
        for j in range(lon_segments):
            first = i * (lon_segments + 1) + j
            second = first + lon_segments + 1

            triangles.append([first, second, first + 1])
            triangles.append([second, second + 1, first + 1])

    geometry = sg.CreateGeometryData()
    # Set vertex- and triangle-counts for the Geometry. 
    # NOTE: The number of vertices and triangles has to be set before vertex- and triangle-data is 
    # loaded into the GeometryData. 
    geometry.SetVertexCount(len(vertices))
    geometry.SetTriangleCount(len(triangles))
    
    # Add vertex-coordinates array to the Geometry. 
    sg_vertices = geometry.GetCoords()
    flattened_verts = flatten_tuple_list(vertices)
    sg_vertices.SetData(flattened_verts, len(flattened_verts))
    
    # Add triangles to the Geometry. Each triangle-corner contains the id for the vertex that corner 
    # uses. 
    sg_triangles = geometry.GetVertexIds()
    flattened_triangles = flatten_tuple_list(triangles)
    sg_triangles.SetData(flattened_triangles, len(flattened_triangles))
        
    # Normals and UVs is a corner field in the simplygon format. This means that we have to unwrap these buffers
    # so that there is a unique normal and uv per corner of the triangle. 
    # For more information about the geometry format you can refer to this page:
    # https://documentation.simplygon.com/SimplygonSDK_10.3.2100.0/api/concepts/geometrydata.html
    
    # Create a normal and uv list per corner which is the same length as the flattened triangle list
    corner_normals = [0]*len(flattened_triangles)
    corner_uvs = [0]*len(flattened_triangles)
    for i in range(0, len(flattened_triangles)):
        vert_id = flattened_triangles[i]
        corner_normals[i] = normals[vert_id]
        corner_uvs[i] = uvs[vert_id]

    # Let's add the normals to the geometry
    geometry.AddNormals()
    sg_normals = geometry.GetNormals()
    flattened_normals = flatten_tuple_list(corner_normals)
    sg_normals.SetData(flattened_normals, len(flattened_normals))

    # Let's add the uvs to the geometry
    geometry.AddTexCoords(0)
    sg_uvs = geometry.GetTexCoords(0)
    flattened_uvs = flatten_tuple_list(corner_uvs)
    sg_uvs.SetData(flattened_uvs, len(flattened_uvs))

    # Create a scene and a SceneMesh node with the geometry. 
    scene = sg.CreateScene()
    scene_mesh = sg.CreateSceneMesh()
    scene_mesh.SetName('sphere')
    scene_mesh.SetGeometry(geometry)
    scene.GetRootNode().AddChild(scene_mesh)
    return scene

Now that our Simplygon scene is set up, we can utilize the function defined in the previous section. Below is the code to optimize our generated sphere:

sphere = generate_simplygon_sphere(sg)
output = scripted_pipeline_working_on_scene(sg, 0.5, sphere)
scene_exporter = sg.CreateSceneExporter()
scene_exporter.SetScene(output)
scene_exporter.SetExportFilePath("generated_scene_output.fbx")
scene_exporter.Run()

Conclussions

  • Simple Pipeline Usage:

    • Advantages: Quick and easy setup, suitable for straightforward optimizations.
    • Disadvantages: Limited customization options, lacks control over input and output data directly.
  • Programmatic Pipeline Configuration:

    • Advantages: Offers more flexibility in configuring pipeline settings programmatically.
    • Disadvantages: Still limited in controlling input and output data directly; requires understanding of Simplygon's API.
  • Manual Scene Loading with Checks:

    • Advantages: Enables detailed checks and preprocessing of input data before optimization.
    • Disadvantages: Requires additional code for data inspection; limited to modifications based on initial scene state.
  • Direct Integration with Custom Formats:

    • Advantages: Provides full control over input and output data, ideal for custom formats and complex optimizations.
    • Disadvantages: Advanced approach requiring deep understanding of Simplygon's geometry data format and API integration.

Complete script

Here is the complete script if you want to try this out for yourself.

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from simplygon10 import simplygon_loader
from simplygon10 import Simplygon        
import numpy as np

def simplest_script(sg: Simplygon.ISimplygon, pipeline_path: str, asset_path: str, output_path: str) -> None:
    """
    Simplest possible script to run a simplygon process on a file.

    :param sg: simplygon instance
    :param pipeline_path: path to a simplygon pipeline file that has been created in one of the integrations
    :param asset_path: path to the asset you want to process (fbx, glb, obj or usd)
    :param output_path: path to the resulting file (fbx, glb, obj or usd)
    """ 
    serializer = sg.CreatePipelineSerializer()
    pipeline = serializer.LoadPipelineFromFile(pipeline_path)
    pipeline.RunSceneFromFile(asset_path, output_path, Simplygon.EPipelineRunMode_RunInThisProcess)

def scripting_your_pipeline(sg: Simplygon.ISimplygon, reduction_ratio: float, asset_path: str, output_path: str) -> None:
    """
    Script where the pipeline in configured in script

    :param sg: Simplygon instance
    :param reduction_ratio: the reduction ration you want to use. 
    :param asset_path: path to the asset you want to process (fbx, glb, obj or usd)
    :param output_path: path to the resulting file (fbx, glb, obj or usd)
    """ 
    pipeline = sg.CreateReductionPipeline()
    pipeline_settings = pipeline.GetReductionSettings()
    pipeline_settings.SetReductionTargetTriangleRatio(reduction_ratio)
    # Instruct the reducer to only use triangle ratio
    pipeline_settings.SetReductionTargets(Simplygon.EStopCondition_All, True, False,False,False)
    pipeline.RunSceneFromFile(asset_path, output_path, Simplygon.EPipelineRunMode_RunInThisProcess)

def scripted_pipeline_working_on_scene(sg: Simplygon.ISimplygon, reduction_ratio: float, scene: Simplygon.spScene) -> Simplygon.spScene:
    """
    The pipeline is scripted and executed on the incoming scene. Returns the processed result as a scene.

    :param sg: Simplygon instance
    :param reduction_ratio: the reduction ration you want to use. 
    :param scene: a Simplygon scene that contains the data you want to process
    :return: the processed scene
    """ 
    pipeline = sg.CreateReductionPipeline()
    pipeline_settings = pipeline.GetReductionSettings()
    pipeline_settings.SetReductionTargetTriangleRatio(reduction_ratio)
    # Instruct the reducer to only use triangle ratio
    pipeline_settings.SetReductionTargets(Simplygon.EStopCondition_All, True, False,False,False)
    
    # If you want to keep the original scene intact you can copy the scene and run the process on that.
    scene_copy = scene.NewCopy()
    pipeline.RunScene(scene_copy, Simplygon.EPipelineRunMode_RunInThisProcess)
    return scene_copy

    
def generate_simplygon_sphere(sg: Simplygon.ISimplygon, radius: int=1, lat_segments: int=20, lon_segments: int=20) -> Simplygon.spScene:
    """
    Generates a sphere and creates a Simplygon scene with the information.
    :param sg: Simplygon instance
    :param radius: radius of the sphere
    :param lat_segments: number of latitude segements
    :param lon_segments: number of longitude segements
    :return: a Simplygon scene that contains a sphere
    """     
    # Helper function to flatten a list of tuples
    def flatten_tuple_list(list_of_tuples: list) -> list:
        return [item for sublist in list_of_tuples for item in sublist]
    
    vertices = []
    normals = []
    triangles = []
    uvs = []

    for i in range(lat_segments + 1):
        lat = np.pi * i / lat_segments
        sin_lat = np.sin(lat)
        cos_lat = np.cos(lat)

        for j in range(lon_segments + 1):
            lon = 2 * np.pi * j / lon_segments
            sin_lon = np.sin(lon)
            cos_lon = np.cos(lon)

            x = radius * cos_lon * sin_lat
            y = radius * cos_lat
            z = radius * sin_lon * sin_lat
            vertices.append((x, y, z))
            uvs.append((j / lon_segments, i / lat_segments))
            normal = np.array((cos_lon * sin_lat, cos_lat, sin_lon * sin_lat))
            normals.append(normal)
            
    for i in range(lat_segments):
        for j in range(lon_segments):
            first = i * (lon_segments + 1) + j
            second = first + lon_segments + 1

            triangles.append([first, second, first + 1])
            triangles.append([second, second + 1, first + 1])

    geometry = sg.CreateGeometryData()
    # Set vertex- and triangle-counts for the Geometry. 
    # NOTE: The number of vertices and triangles has to be set before vertex- and triangle-data is 
    # loaded into the GeometryData. 
    geometry.SetVertexCount(len(vertices))
    geometry.SetTriangleCount(len(triangles))
    
    # Add vertex-coordinates array to the Geometry. 
    sg_vertices = geometry.GetCoords()
    flattened_verts = flatten_tuple_list(vertices)
    sg_vertices.SetData(flattened_verts, len(flattened_verts))
    
    # Add triangles to the Geometry. Each triangle-corner contains the id for the vertex that corner 
    # uses. 
    sg_triangles = geometry.GetVertexIds()
    flattened_triangles = flatten_tuple_list(triangles)
    sg_triangles.SetData(flattened_triangles, len(flattened_triangles))
        
    # Normals and UVs is a corner field in the simplygon format. This means that we have to unwrap these buffers
    # so that there is a unique normal and uv per corner of the triangle. 
    # For more information about the geometry format you can refer to this page:
    # https://documentation.simplygon.com/SimplygonSDK_10.3.2100.0/api/concepts/geometrydata.html
    
    # Create a normal and uv list per corner which is the same length as the flattened triangle list
    corner_normals = [0]*len(flattened_triangles)
    corner_uvs = [0]*len(flattened_triangles)
    for i in range(0, len(flattened_triangles)):
        vert_id = flattened_triangles[i]
        corner_normals[i] = normals[vert_id]
        corner_uvs[i] = uvs[vert_id]

    # Let's add the normals to the geometry
    geometry.AddNormals()
    sg_normals = geometry.GetNormals()
    flattened_normals = flatten_tuple_list(corner_normals)
    sg_normals.SetData(flattened_normals, len(flattened_normals))

    # Let's add the uvs to the geometry
    geometry.AddTexCoords(0)
    sg_uvs = geometry.GetTexCoords(0)
    flattened_uvs = flatten_tuple_list(corner_uvs)
    sg_uvs.SetData(flattened_uvs, len(flattened_uvs))

    # Create a scene and a SceneMesh node with the geometry. 
    scene = sg.CreateScene()
    scene_mesh = sg.CreateSceneMesh()
    scene_mesh.SetName('sphere')
    scene_mesh.SetGeometry(geometry)
    scene.GetRootNode().AddChild(scene_mesh)
    return scene

def main() -> None:    
    sg = simplygon_loader.init_simplygon()
    print(sg.GetVersion())

    # Let's first try the simplest approach using an exported pipeline file and an fbx.
    simplest_script(sg, "reduction.json", "sphere.fbx", "simplest_script_output.fbx")
    
    # Now we'll set up the pipeline in code and apply it to the fbx
    scripting_your_pipeline(sg, 0.5, "sphere.fbx", "scripting_your_pipeline_output.fbx")
    
    # Instead of using the importing function in pipeline, we import the asset ourselves and
    # take care of the exporting.
    importer = sg.CreateSceneImporter()
    importer.SetImportFilePath("sphere.fbx")
    importer.Run()
    sphere_scene = importer.GetScene()    
    # Here you can do some magic on the scene, prior to running the optimization.
    output = scripted_pipeline_working_on_scene(sg, 0.5, sphere_scene)
    scene_exporter = sg.CreateSceneExporter()
    scene_exporter.SetScene(output)
    scene_exporter.SetExportFilePath("scripted_pipeline_working_on_scene_output.fbx")
    scene_exporter.Run()

    #  Lastly we have decided to fill out the geometry data ourselves, perhaps from our own mesh format.
    sphere = generate_simplygon_sphere(sg)
    output = scripted_pipeline_working_on_scene(sg, 0.5, sphere)
    scene_exporter = sg.CreateSceneExporter()
    scene_exporter.SetScene(output)
    scene_exporter.SetExportFilePath("generated_scene_output.fbx")
    scene_exporter.Run()

    del sg

if __name__== "__main__":
    main()
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*