Scripting with Simplygon: A Framework

Written by Samuel Rantaeskola, Product Expert, Simplygon

Disclaimer: The code in this post is written on version 10.3.6400.0 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

If you haven't already read Scripting with Simplygon: Introduction, it would be beneficial to browse through that prior to reading this post.

In this post, we will explore a scripting framework that allows you to extend the functionality of Simplygon objects in Python. This framework will provide you with a starting point to create a library of helpful functions, enabling you to quickly iterate as you refine your asset pipeline.

The approach we will discuss is a seamless extension to Simplygon that allows you to add functionality as needed, without requiring a significant time investment upfront.

Simplygon singleton

The first thing we want to create in our scripting framework is a Simplygon singleton class. This will allow us to access the Simplygon instance throughout the application without needing to pass a Simplygon parameter in every function call. This is a simple class that we will place in a file called simplygon_instance.py.

from simplygon10 import simplygon_loader
from simplygon10 import Simplygon

class SimplygonInstance(Simplygon.ISimplygon):
    """
    Simplygon singleton that allows you to access the Simplygon instance directly.
    SimplygonInstance() will give you access to the Simplygon SDK interface.
    """
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            # Create the singleton instance
            cls._instance = super(SimplygonInstance, cls).__new__(cls, *args, **kwargs)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if not self._initialized:
            # Initialize the Simplygon instance
            self._sg = simplygon_loader.init_simplygon()
            print(f'Simplygon {self._sg.GetVersion()} initialized.')
            # Copy all attributes from the _sg instance to this class
            self.__dict__.update(self._sg.__dict__)
            self._initialized = True

    def __del__(self):
        print("Deleting Simplygon.")
        del self._sg

To access this singleton in other files, all we need to do is use the following code:

from simplygon_instance import SimplygonInstance
SimplygonInstance()

Framework to extend a Simplygon object

The next step in our framework is to enable the extension of Simplygon objects. We need to address two scenarios:

  1. Creating a new object of the extended type.
  2. Extending an object that was created internally by Simplygon.

The solution involves leveraging some Python magic within the constructor of our extended object. For this example, we will focus on extending the Simplygon scene object.

Below you can the class and constructor definition of the extended Scene, which call SceneXT:

from simplygon_instance import SimplygonInstance


class SceneXT(Simplygon.spScene):
    
    def __init__(self, scene: Simplygon.spScene = None):
        if not scene:
            scene = SimplygonInstance().CreateScene()
        self.__dict__.update(scene.__dict__)

This code allows you to either create a completely new scene or extend the functionality of an existing scene created within Simplygon. This is how it would be used:

#Create a new Simplygon scene
scene = SceneXT()

#Extend the functionality on a loaded scene
importer = SimplygonInstance().CreateSceneImporter()
importer.SetImportFilePath(asset_path)
importer.Run()
scene = SceneXT(importer.GetScene()) 

This pattern works for any type of Simplygon object and allows you to seamlessly pass the extended object back into the Simplygon API without any special handling, as shown below:

exporter = SimplygonInstance().CreateSceneExporter()
# The extended Simplygon object will be recognized as Simplygon object.
exporter.SetScene(SceneXT())

Extending the functionality

Now that we have an extended Simplygon object, we can easily add any functionality we want to the class. For example, we might want to simplify the process of listing all the meshes in a scene. To do this, we can add the following function:

def get_meshes(self) -> list:
    """
    Returns all the meshes in the scene.
    :return: a list of all meshes in the scene converted to SceneMeshXT
    """          
    meshes = []
    self.GetRootNode().get_nodes_by_type("ISceneMesh", meshes)
    # Convert all objects to scene meshes
    meshes = [SceneMeshXT.convert_type(mesh) for mesh in meshes]
    return meshes

With this functionality in place, we can use the following code to list all the meshes in an FBX file.

#Create a new Simplygon scene
scene = SceneXT()

#Extend the functionality on a loaded scene
importer = SimplygonInstance().CreateSceneImporter()
importer.SetImportFilePath("asset.fbx")
importer.Run()
scene = SceneXT(importer.GetScene()) 
meshes = scene.get_meshes()
for mesh in meshes:
    print(mesh.GetName())

Extending more objects

The sharp-eyed reader might notice that the code above uses some functionality that isn't available yet. Specifically, we're calling the get_nodes_by_type function on the root node of the scene, but this function doesn't exist in the SceneNode API. We're also using an object called SceneMeshXT, an extension of SceneMesh.

First, let's apply what we've learned in this post to extend the functionality of SceneNode. Let's create the class, which we'll place in a file called scene_node_xt.py.

from simplygon10 import Simplygon 
from simplygon_instance import SimplygonInstance

class SceneNodeXT (Simplygon.spSceneNode):
    
    def __init__(self, scene_node: Simplygon.spSceneNode = None):
        if not scene_node:
            scene_node = SimplygonInstance().CreateSceneNode()
        self.__dict__.update(scene_node.__dict__)

Now, let's add the functionality we need to recursively find all meshes within a tree of scene nodes. Here is the function required:

def get_nodes_by_type(self, sg_type: str, mathes: list) -> None:
    """
    Adds all the nodes that corresponds from this node and its children to the requested type
    into the list of matches
    :param sg_type: the type of node to look for (ISceneMesh, ISceneBone etc)
    :param mathes: the list where the objects will be returned.
    """          
    if self.IsA(sg_type):
        mathes.append(self)
    # Loop through all the children of the node and look for more matches
    for i in range(0, self.GetChildCount()):
        child = SceneNodeXT(self.GetChild(i))
        child.get_nodes_by_type(sg_type, mathes)

With SceneNodeXT in place we can go ahead and extend the scene mesh as well.

from simplygon10 import Simplygon
from simplygon_instance import SimplygonInstance

class SceneMeshXT (Simplygon.spSceneMesh):
    
    def __init__(self, scene_mesh: Simplygon.spSceneMesh = None):
        if not scene_mesh:
            scene_mesh = SimplygonInstance().CreateSceneMesh()
        self.__dict__.update(scene_mesh.__dict__)
        
    @staticmethod
    def convert_type(node: Simplygon.spSceneNode):
        """
        Converts the incoming node to a extended scene mesh. Will error out if it isn't a mesh.
        """
        if not node.IsA("ISceneMesh"):
            raise Exception(f'The provided object is a {node.GetClass()}, not a SceneMesh')
        return SceneMeshXT(Simplygon.spSceneMesh.SafeCast(node))

SceneMesh inherits from SceneNode, but when the object is transferred to Python, that connection is lost. By adding a convert_type function, we can convert a SceneNode to a SceneMesh. However, be cautious—if the object isn’t natively a scene mesh, an error will be raised.

Overriding native functionality

We're not done yet; one issue remains. You may have noticed that this line in the get_meshes function presents some challenges.

self.GetRootNode().get_nodes_by_type("ISceneMesh", meshes)

GetRootNode is a function on the Simplygon Scene object that returns a regular SceneNode, not a SceneNodeXT object. This means that the functionality we just added isn't directly available.

Fortunately, there's one last trick we can use. We can override the native Simplygon function to return an extended object instead, like this:

def GetRootNode(self) -> SceneNodeXT:
    """
    For convenience, we don't have to cast the root node to SceneNodeXT every time.
    """
    return SceneNodeXT(super().GetRootNode())

With these additions, we can now run the code from the previous section. More importantly, we've learned the process of adding functionality as we need it, allowing us to extend Simplygon objects effectively.

Conclussions

In this guide, you have learned how to extend Simplygon objects in Python by creating wrapper classes that add additional functionality. The key takeaways include:

  1. Creating a Singleton: How to create and access a Simplygon singleton across the application.
  2. Extending Simplygon Objects: Techniques for wrapping and extending Simplygon objects, allowing for the addition of custom functionality.
  3. Seamless Integration: How to ensure that extended objects can be passed back into the Simplygon API without special handling.
  4. Overriding Native Functions: How to override native Simplygon functions to return extended objects, making custom functionalities readily available.

Overall, you have gained a solid understanding of how to dynamically enhance Simplygon's capabilities within a Python scripting framework, enabling more efficient and flexible workflows.

With a solid understanding of the framework, we’ll explore in future posts how to further extend and work with the scene graph to develop smooth and maintainable workflows.

The scripts

simplygon_instance.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 

from simplygon10 import simplygon_loader
from simplygon10 import Simplygon

class SimplygonInstance(Simplygon.ISimplygon):
    """
    Simplygon singleton that allows you to access the Simplygon instance directly.
    SimplygonInstance() will give you access to the Simplygon SDK interface.
    """
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            # Create the singleton instance
            cls._instance = super(SimplygonInstance, cls).__new__(cls, *args, **kwargs)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if not self._initialized:
            # Initialize the Simplygon instance
            self._sg = simplygon_loader.init_simplygon()
            print(f'Simplygon {self._sg.GetVersion()} initialized.')
            # Copy all attributes from the _sg instance to this class
            self.__dict__.update(self._sg.__dict__)
            self._initialized = True

    def __del__(self):
        print("Deleting Simplygon.")
        del self._sg

scene_xt.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 

from simplygon10 import Simplygon
from scene_node_xt import SceneNodeXT
from scene_mesh_xt import SceneMeshXT 
from simplygon_instance import SimplygonInstance


class SceneXT(Simplygon.spScene):
    
    def __init__(self, scene: Simplygon.spScene = None):
        if not scene:
            scene = SimplygonInstance().CreateScene()
        self.__dict__.update(scene.__dict__)
            
    """
    Start overrides
    """
    
    def GetRootNode(self) -> SceneNodeXT:
        """
        For convenience, so that we don't have to cast the root node to SceneNodeXT every time.
        """
        return SceneNodeXT(super().GetRootNode())

    """
    End overrides
    """
    
    def get_meshes(self) -> list:
        """
        Returns all the meshes in the scene.
        :return: a list of all meshes in the scene converted to SceneMeshXT
        """          
        meshes = []
        self.GetRootNode().get_nodes_by_type("ISceneMesh", meshes)
        # Convert all objects to scene meshes
        meshes = [SceneMeshXT.convert_type(mesh) for mesh in meshes]
        return meshes

scene_node_xt.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from simplygon10 import Simplygon 
import re
from simplygon_instance import SimplygonInstance


class SceneNodeXT (Simplygon.spSceneNode):
    
    def __init__(self, scene_node: Simplygon.spSceneNode = None):
        if not scene_node:
            scene_node = SimplygonInstance().CreateSceneNode()
        self.__dict__.update(scene_node.__dict__)
        
    def __eq__(self, other) -> bool:
        """
        Returns true if the extended Simplygon object is the same as the incoming object.
        """
        return self.IsSameObjectAs(other)

            
    def get_nodes_by_type(self, sg_type: str, mathes: list) -> None:
        """
        Adds all the nodes that corresponds from this node and its children to the requested type
        into the list of matches
        :param sg_type: the type of node to look for (ISceneMesh, ISceneBone etc)
        :param mathes: the list where the objects will be returned.
        """          
        if self.IsA(sg_type):
            mathes.append(self)
        # Loop through all the children of the node and look for more matches
        for i in range(0, self.GetChildCount()):
            child = SceneNodeXT(self.GetChild(i))
            child.get_nodes_by_type(sg_type, mathes)

scene_mesh_xt.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from simplygon10 import Simplygon
from simplygon_instance import SimplygonInstance

class SceneMeshXT (Simplygon.spSceneMesh):
    
    def __init__(self, scene_mesh: Simplygon.spSceneMesh = None):
        if not scene_mesh:
            scene_mesh = SimplygonInstance().CreateSceneMesh()
        self.__dict__.update(scene_mesh.__dict__)

    @staticmethod
    def convert_type(node: Simplyg:on.spSceneNode):
        """
        Converts the incoming node to a extended scene mesh. Will error out if it isn't a mesh.
        """
        if not node.IsA("ISceneMesh"):
            raise Exception(f'The provided object is a {node.GetClass()}, not a SceneMesh')
        return SceneMeshXT(Simplygon.spSceneMesh.SafeCast(node))

main.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from scene_xt import SceneXT
from simplygon_instance import SimplygonInstance
from simplygon10 import Simplygon

#Create a new Simplygon scene
scene = SceneXT()

#Extend the functionality on a loaded scene
importer = SimplygonInstance().CreateSceneImporter()
importer.SetImportFilePath("assets1.fbx")
result = importer.Run()
if Simplygon.Failed(result):
    raise Exception(f'Failed to load scene from asset.fbx.')

scene = SceneXT(importer.GetScene()) 
meshes = scene.get_meshes()
for mesh in meshes:
    print(mesh.GetName())
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*