Culling geometry with camera volumes in Python script

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

In cases where you know from which angles an asset will be viewed, you can use that information to optimize the assets. This could be really helpful when finalizing a game. For example, side scene geometry are typically built up by assets made for viewing all around. Rather than building assets for purpose, you could just use what you got and optimize it through cameras scattered in the player area. You can instruct Simplygon to cull anything fully occluded, but the visibility information can also be guide the reducer and the material caster to keep more where the geometry is most visible.

There are several approaches you could take when attacking the problem.

  • Optimize each asset individually in our integration UIs.
  • Create a script in Max or Maya to enable batch processing of assets.
  • Create a stand alone script that processes assets as they are coming into the engine.
  • Integrate Simplygon into your level editor.

Using cameras in the UI

In our documentions there is a tutorial on how to cull geometry using the UI in Maya.

Using cameras in Max Script

Visibility culling through generated cameras in Max shows how you can create scripts in the Max to cull occluded geometry.

Using cameras in scripts

The most interesting use of custom cameras is when you use that in place in the level editor. This would allow you to continuously optimize the level as it evolves. You could use the nav mesh to generate sample points that optimizes the geometry based of those viewing angles. In our example we will be using SceneCameras to optimize our geometry. When creating cameras there are several different approaches you can take. In our script we will utilize newly added functionality that places cameras on vertices of selected meshes. But you could also choose to scatter the camera positions yourself using the following code:

camera = sg.CreateSceneCamera()
camera.SetCameraType(Simplygon.ECameraType_Omnidirectional)
camera_pos = camera.GetCameraPositions()
camera_pos.DeepCopy(camera_positions)

In the code above camera_positions is a RealArray which describes the positions of the cameras in tuples of three (X,Y,Z).

The scene

We will process this scene using a script that distributes cameras on each of the vertices in the red camera volume. This could be a scene where the player only could roam in that area. All the static geometry can be culled with cameras distributed within that area.

City block

The starting point

As always, we need to create the starting point for the script. We'll use the same pattern as we use in all these posts.

from simplygon10 import simplygon_loader
from simplygon10 import Simplygon


def process_file(sg, asset_file, output_file):
    # Load the asset we want to process
    scene_importer = sg.CreateSceneImporter()
    scene_importer.SetImportFilePath(asset_file)
    scene_importer.Run()
    # Process the asset
    optimized_scene = cull_geometry(sg, scene_importer.GetScene())
    # Export the culled scene into the output file.
    scene_exporter = sg.CreateSceneExporter()
    scene_exporter.SetScene(optimized_scene)
    scene_exporter.SetExportFilePath(output_file)
    scene_exporter.Run()


def main():
    sg = simplygon_loader.init_simplygon()
    process_file(sg, <your_asset>, <output_file>)
    del sg

if __name__== "__main__":
    main()

The key function to look at here is cull_geometry.

Cull geometry function

To perform the culling we will be using a reduction pipeline where we'll keep all polygons apart from the ones that can't be seen from the cameras.

The visibility Settings contains the settings we need to set to enable the camera culling.

def cull_geometry(sg, scene): 
    for i in range(0, scene.GetMaterialTable().GetMaterialsCount()):
        material = scene.GetMaterialTable().GetMaterial(i)
        if material.HasMaterialChannel("Opacity"):
            material.RemoveMaterialChannel("Opacity")
    print("Culling geometry ...")
    # Create a reduction processor. 
    reduction_pipeline = sg.CreateReductionPipeline()
    reduction_settings = reduction_pipeline.GetReductionSettings()
    # We're not going to reduce anything apart from the culled geometry
    reduction_settings.SetReductionTargetTriangleRatio(1)
    visibility_settings = reduction_pipeline.GetVisibilitySettings()
    # Get the mesh to distribute cameras on
    camera_volume = Simplygon.spSceneMesh.SafeCast(get_camera_volume(scene.GetRootNode()))
    if camera_volume == None:
        raise Exception("No camera volume mesh found in the scene.")
    
    # Set up all the cameras in the scene
    camera_selection_set = setup_cameras(sg, scene, camera_volume)
    # Let's remove the camera volume from the scene
    camera_volume.RemoveFromParent()
    visibility_settings.SetCameraSelectionSetName(camera_selection_set.GetName())
    # Enabled GPU based visibility calculations. 
    visibility_settings.SetComputeVisibilityMode( Simplygon.EComputeVisibilityMode_DirectX )
    # Disabled conservative mode. 
    visibility_settings.SetConservativeMode( False )
    # Remove all non visible geometry. 
    visibility_settings.SetCullOccludedGeometry( True )
    # Skip filling nonvisible regions. 
    visibility_settings.SetFillNonVisibleAreaThreshold( 0.0 )
    # Don't remove non occluding triangles. 
    visibility_settings.SetRemoveTrianglesNotOccludingOtherTriangles( False )
    # Remove all back facing triangles. 
    visibility_settings.SetUseBackfaceCulling( True )
    # Don't use visibility weights. 
    visibility_settings.SetUseVisibilityWeightsInReducer( False )
    print("Running visibility culling...")
    reduction_pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
    print("Done")
    return scene

In the above code, this is the section that sets up the cameras, using a mesh in the scene.

```python
scene_selection_set_table = scene.GetSelectionSetTable()
camera_selection_set = sg.CreateSelectionSet()
scene_selection_set_table.AddSelectionSet(camera_selection_set)
camera_selection_set.SetName("Cameras")    
camera_selection_set.AddItem(camera_volume.GetNodeGUID())
visibility_settings.SetCameraSelectionSetName(camera_selection_set.GetName()) 

The camera_volume object is a mesh, from which Simplygon will use the vertex positions to create cameras. Naturally, you can provide several meshes in the selection set as well.

The key is to find the right balance of vertices in the mesh, too few and you might get areas culled which should be visible, too many and the processing time will go up.

Getting the camera volume

The last thing we need is a function that retrieves the camera mesh from the scene. These two functions does the trick:

def get_mesh(scene_node):
    for i in range(0, scene_node.GetChildCount()):
        child = scene_node.GetChild(i)
        if child.IsA("ISceneMesh"):
            return child
        mesh = get_mesh(child)
        if mesh:
            return mesh
    return None

def get_camera_volume(scene_node):
    # Loop through all the nodes in the scene
    for i in range(0, scene_node.GetChildCount()):
        child = scene_node.GetChild(i)
        if child.GetName().startswith("camera_volume"):
            # Validate that the mesh is a child. In some formats meshes are stored as children to 
            # a transform.
            if not child.IsA("ISceneMesh"):
                child = get_mesh(child)
            return child
        # Continue digging for geometries in the scene
        camera_volume = get_camera_volume(child)
        if camera_volume:
            return camera_volume
    return None

These functions will return a mesh from the scene called camera_volume.

Running the script on the scene

Here's the results from running the script on the scene shown earlier.

||

Above left you can see the original scene, on the right after culling.

||

Above is the asset seen from the outside and from within the camera volume.

The Script

Here is the script in it's entirety.

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

def setup_cameras(sg, scene, camera_volume):
    # Create a camera selection set 
    scene_selection_set_table = scene.GetSelectionSetTable()
    camera_selection_set = sg.CreateSelectionSet()
    scene_selection_set_table.AddSelectionSet(camera_selection_set)
    camera_selection_set.SetName("Cameras")    
    # Let's get the geometry data.
    geometry_data = camera_volume.GetGeometry()
    vertices = geometry_data.GetCoords()
    # We need to transform all the vertices to world space
    global_transform = sg.CreateMatrix4x4()
    scene.EvaluateDefaultGlobalTransformation(camera_volume, global_transform)
    global_transform.Point3ArrayMultiply(vertices)
    camera = sg.CreateSceneCamera()
    camera.SetCameraType(Simplygon.ECameraType_Omnidirectional)
    # Copy all the vertex positions to the list of camera positions
    camera_pos = camera.GetCameraPositions()
    camera_pos.DeepCopy(vertices)
    # Add the camera to the scene and the camera selection set
    scene.GetRootNode().AddChild(camera)
    camera_selection_set.AddItem(camera.GetNodeGUID())
    return camera_selection_set

def get_mesh(scene_node):
    for i in range(0, scene_node.GetChildCount()):
        child = scene_node.GetChild(i)
        if child.IsA("ISceneMesh"):
            return child
        mesh = get_mesh(child)
        if mesh:
            return mesh
    return None

def get_camera_volume(scene_node):
    # Loop through all the nodes in the scene
    for i in range(0, scene_node.GetChildCount()):
        child = scene_node.GetChild(i)
        # We only care about meshes
        if child.GetName().startswith("camera_volume"):
            # Validate that the mesh is a child. In some formats meshes are stored as children to 
            # a transform.
            if not child.IsA("ISceneMesh"):
                child = get_mesh(child)
            return child
        # Continue digging for geometries in the scene
        camera_volume = get_camera_volume(child)
        if camera_volume:
            return camera_volume
    return None

def cull_geometry(sg, scene): 
    for i in range(0, scene.GetMaterialTable().GetMaterialsCount()):
        material = scene.GetMaterialTable().GetMaterial(i)
        if material.HasMaterialChannel("Opacity"):
            material.RemoveMaterialChannel("Opacity")
    print("Culling geometry ...")
    # Create a reduction processor. 
    reduction_pipeline = sg.CreateReductionPipeline()
    reduction_settings = reduction_pipeline.GetReductionSettings()
    # We're not going to reduce anything apart from the culled geometry
    reduction_settings.SetReductionTargetTriangleRatio(1)
    visibility_settings = reduction_pipeline.GetVisibilitySettings()
    # Get the mesh to distribute cameras on
    camera_volume = Simplygon.spSceneMesh.SafeCast(get_camera_volume(scene.GetRootNode()))
    if camera_volume == None:
        raise Exception("No camera volume mesh found in the scene.")
    
    # Set up all the cameras in the scene
    camera_selection_set = setup_cameras(sg, scene, camera_volume)
    # Let's remove the camera volume from the scene
    camera_volume.RemoveFromParent()
    visibility_settings.SetCameraSelectionSetName(camera_selection_set.GetName())
    # Enabled GPU based visibility calculations. 
    visibility_settings.SetComputeVisibilityMode( Simplygon.EComputeVisibilityMode_DirectX )
    # Disabled conservative mode. 
    visibility_settings.SetConservativeMode( False )
    # Remove all non visible geometry. 
    visibility_settings.SetCullOccludedGeometry( True )
    # Skip filling nonvisible regions. 
    visibility_settings.SetFillNonVisibleAreaThreshold( 0.0 )
    # Don't remove non occluding triangles. 
    visibility_settings.SetRemoveTrianglesNotOccludingOtherTriangles( False )
    # Remove all back facing triangles. 
    visibility_settings.SetUseBackfaceCulling( True )
    # Don't use visibility weights. 
    visibility_settings.SetUseVisibilityWeightsInReducer( False )
    print("Running visibility culling...")
    reduction_pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
    print("Done")
    return scene


def process_file(sg, asset_file, output_file):
    # Load the asset we want to process
    scene_importer = sg.CreateSceneImporter()
    scene_importer.SetImportFilePath(asset_file)
    scene_importer.Run()
    # Process the asset
    optimized_scene = cull_geometry(sg, scene_importer.GetScene())
    # Export the culled scene into the output file.
    scene_exporter = sg.CreateSceneExporter()
    scene_exporter.SetScene(optimized_scene)
    scene_exporter.SetExportFilePath(output_file)
    scene_exporter.Run()


def main():
    sg = simplygon_loader.init_simplygon()
    print(sg.GetVersion())
    process_file(sg, "buildings.glb", "output.glb")
    del sg

if __name__== "__main__":
    main()

⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*