Optimizing assets made of both quads and triangles

Disclaimer: This post is written using version 10.0.1400.0 of Simplygon and Maya 2022. 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 this post we'll showcase how to reduce meshes containing both quads and triangles. We are going to use both the triangle reducer and new quad reducer along with vertex locks.

Alien mesh showcasing wireframe with quads.

Prerequisites

This example will use the Simplygon plug-in for Maya, but the same concepts can be applied to all other integrations of the Simplygon API. Notice however that not all integrations support quad data.

Problem to solve

We want to optimize a mesh containing both quads and triangles. Currently our quad reducer only works with quads. Pushing the mesh through our triangle reducer would triangulate it and ignore quad data. Using the quad optimizer would leave the triangles untouched.

Alien mesh with triangulated eyes.

Solution

To solve the problem of handling mixed meshes we are going to use both our triangle reducer and our quad reducer. First we will lock all quads so the triangle reducer does not touch them. Then we will perform a standard triangle reduction on our scene. After that we remove the locks and perform quad reduction.

We are going to use screen_size as target metric for our reduction, deviation would also be suitable. Since we are doing the reduction in two steps using triangle count or triangle ratio would not work since it can not weight removing triangles versus removing quads.

def process_selection(sg):
    """Perform mixed quad and triangle reduction on selected parts in Maya."""
    scene = export_selection(sg)

    # Lock quads
    lock_vertices_in_quads(scene)
    
    # Reduce triangles
    reduce_triangles(sg, scene, screen_size)

    # Unlock all vertices
    unlock_all_vertices(scene)

    # Reduce quads
    reduce_quads(sg, scene, screen_size)
    
    # Export to Maya
    import_results(scene)

Export mesh from Maya with quads

First we define a temporary file which we will use for export and importing from Maya.

tmp_file = "c:/Temp/export.sb"

To keep quads during export from Maya we need to tell exporter to keep quads. We can do this by enabling QuadMode flag via setting QuadMode = True when calling the cmds.Simplygon command form our Maya API.

def export_selection(sg):
    """Export the current selected objects into a Simplygon scene with quads."""
    cmds.Simplygon(exp = tmp_file, QuadMode = True)
    scene = sg.CreateScene()
    scene.LoadFromFile(tmp_file)
    return scene

Lock quads

To lock vertices in quads we are going to use the Vertex-lock field.

First we create a selection set from all scene meshes in out scene. This enables us to iterate over them without having to traverse scene hierarchy. We can do this via SelectNodes and specify that we want nodes of SceneMesh type.

scene_mesh_type_name =  "SceneMesh"
def lock_vertices_in_quads(scene):
    """Lock vertices connected to quad-flagged triangles in all scene meshes."""
    scene_meshes_selection_set_id = scene.SelectNodes(scene_mesh_type_name)
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet( scene_meshes_selection_set_id )

Once we have all scene meshes we can iterate over them. First thing we need to do is to get the node via id stored in our selection set. This can be done via GetNodeByGUID. We also need to SafeCast to a spSceneMesh. This because we want to access the geometry data, which can be done via GetGeometry.

    for node_id in range(scene_meshes_selection_set.GetItemCount()):
        scene_mesh = Simplygon.spSceneMesh.SafeCast( scene.GetNodeByGUID( scene_meshes_selection_set.GetItem( node_id ) ) )
        
        # Get geometry for mesh
        geometry = scene_mesh.GetGeometry()
        if not geometry: 
            continue

If we do not have a vertex lock field we add one and assign it to default value False; unlocked.

        # If we do not have one, add vertex lock field.
        if not geometry.GetVertexLocks():
            geometry.AddVertexLocks()

        # Unlock all vertices.
        locks = geometry.GetVertexLocks()
        locks_data = [False] * locks.GetItemCount()

We are also interested in accessing the quad flags field. This one tells if a triangle is part of a quad. We can get it via GetQuadFlags and convert it into a Python data type via GetData.

We do same thing with vertex id field via GetVertexIds.

        quad_flags = geometry.GetQuadFlags()
        if not quad_flags: 
            continue # Mesh does not contain quads, nothing to lock.
            
        quad_flags_data = quad_flags.GetData()
        vertex_ids = geometry.GetVertexIds()
        vertex_ids_data = vertex_ids.GetData()

We now iterate through every triangle in our geometry and get corresponding quad flag.

Each triangle contains three corners; so we iterate through them. The vertex id can be calculated by triangle * 3 + corner We set them to be locked or not by comparing the triangles quad flag to SG_QUADFLAG_TRIANGLE. If it is not a triangle it means it is part of a quad, and should be locked.

Lastly we write back our Python native list to our vertex lock field via SetData.

        for triangle in range(geometry.GetTriangleCount()):
            triangle_quad_flag = quad_flags_data[triangle]
            for corner in range(0,3):
                vertex_id = vertex_ids_data[triangle * 3 + corner]
                if vertex_id < 0:
                    continue
                    
                # If our triangle is part of a quad, lock vertex
                if triangle_quad_flag != Simplygon.SG_QUADFLAG_TRIANGLE:
                    locks_data[vertex_id] = True

        locks.SetData(locks_data, len(locks_data))

Lastly we clean up our temporary selection set by removing it from our scene via RemoveItem on our scene's SelectionSetTable.

    scene_meshes_selection_set = None
    scene.GetSelectionSetTable().RemoveItem( scene_meshes_selection_set_id )

Reduce triangles

To reduce triangles we are are using a reduction pipeline. This will reduce all triangles in our scene. We can set it to use screen size by calling SetReductionTargets and specifying screen size with SetReductionTargetOnScreenSize. We do not need to specify anything else as reduction pipeline will respect the vertex locks we just added to all quads. After creating the pipeline we run it by calling RunScene

def reduce_triangles(sg, scene, screen_size):
    """Reduce triangles in scene for specific screen_size."""
    reduction_pipeline = sg.CreateReductionPipeline()
    reduction_settings = reduction_pipeline.GetReductionSettings()
    reduction_settings.SetReductionTargetOnScreenSize( screen_size )
    reduction_settings.SetReductionTargets( Simplygon.EStopCondition_Any, False, False, False, True )
    reduction_pipeline.RunScene( scene, Simplygon.EPipelineRunMode_RunInThisProcess )

After reducing all triangles in our scene it looks like this. We can see that the quads are left untouched.

Alien with triangles eyes reduced. Quads are left untouched.

Unlock vertexes

Once triangle reduction is done we need to unlock all vertexes so we can perform quad reduction. As before we start by creating a selection set containing all meshes in our scene. We can then iterate over them, get their geometry and remove vertex lock field by RemoveVertexLocks. Once that is done we clean up our selection set.

def unlock_all_vertices(scene):
    """Remove all vertex locks from scene meshes in scene."""
    scene_meshes_selection_set_id = scene.SelectNodes(scene_mesh_type_name)
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet( scene_meshes_selection_set_id )

    for node_id in range(scene_meshes_selection_set.GetItemCount()):
        scene_mesh = Simplygon.spSceneMesh.SafeCast( scene.GetNodeByGUID( scene_meshes_selection_set.GetItem( node_id ) ) )
        geometry = scene_mesh.GetGeometry()
        if geometry and geometry.GetVertexLocks():
            geometry.RemoveVertexLocks()

    scene_meshes_selection_set = None
    scene.GetSelectionSetTable().RemoveItem( scene_meshes_selection_set_id )

Reduce quads.

To reduce the quads in our scene we are using our new quad reduction pipeline. This pipeline only processes and reduces quads in our scene. Similar to reduction pipeline we can tell it to use screen size as target by calling SetReductionTargets and SetReductionTargetOnScreenSize on QuadReductionSettings.

def reduce_quads(sg, scene, screen_size):
    """Reduce quads in scene for specific screen_size."""
    quad_pipeline = sg.CreateQuadReductionPipeline()
    quad_reduction_settings = quad_pipeline.GetQuadReductionSettings()
    quad_reduction_settings.SetReductionTargetOnScreenSize( screen_size)
    quad_reduction_settings.SetReductionTargets( Simplygon.EStopCondition_Any, False, False, False, True )
    quad_pipeline.RunScene( scene, Simplygon.EPipelineRunMode_RunInThisProcess )
    

Import result into Maya

Lastly we define a function to import our scene back into Maya. We reuse the temporary file we used for export. Similar to our export function we also need to use QuadMode = True to keep quads during import into Maya.

def import_results(scene):
    """Import the Simplygon scene into Maya."""
    scene.SaveToFile(tmp_file)
    cmds.Simplygon(imp=tmp_file, lma=True, QuadMode = True)

Result

After processing our asset we get a mesh where both quads and triangles have been optimized. Here are some results with different screen size targets.

Complete script

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

from functools import reduce
from simplygon10 import simplygon_loader
from simplygon10 import Simplygon
import maya.cmds as cmds
import os


scene_mesh_type_name =  "SceneMesh"

tmp_file = "c:/Temp/export.sb"
screen_size = 50


def export_selection(sg):
    """Export the current selected objects into a Simplygon scene with quads."""
    cmds.Simplygon(exp = tmp_file, QuadMode = True)
    scene = sg.CreateScene()
    scene.LoadFromFile(tmp_file)
    return scene


def import_results(scene):
    """Import the Simplygon scene into Maya."""
    scene.SaveToFile(tmp_file)
    cmds.Simplygon(imp=tmp_file, lma=True, QuadMode = True)


def lock_vertices_in_quads(scene):
    """Lock vertices connected to quad-flagged triangles in all scene meshes."""
    scene_meshes_selection_set_id = scene.SelectNodes(scene_mesh_type_name)
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet( scene_meshes_selection_set_id )
    for node_id in range(scene_meshes_selection_set.GetItemCount()):
        scene_mesh = Simplygon.spSceneMesh.SafeCast( scene.GetNodeByGUID( scene_meshes_selection_set.GetItem( node_id ) ) )
        
        # Get geometry for mesh
        geometry = scene_mesh.GetGeometry()
        if not geometry: 
            continue

        # If we do not have one, add vertex lock field.
        if not geometry.GetVertexLocks():
            geometry.AddVertexLocks()

        # Unlock all vertices.
        locks = geometry.GetVertexLocks()
        locks_data = [False] * locks.GetItemCount()

        quad_flags = geometry.GetQuadFlags()
        if not quad_flags: 
            continue # Mesh does not contain quads, nothing to lock.
            
        quad_flags_data = quad_flags.GetData()

        vertex_ids = geometry.GetVertexIds()
        vertex_ids_data = vertex_ids.GetData()
        for triangle in range(geometry.GetTriangleCount()):
            #triangle_quad_flag = quad_flags.GetItem( triangle )
            triangle_quad_flag = quad_flags_data[triangle]
            for corner in range(0,3):
                vertex_id = vertex_ids_data[triangle * 3 + corner]
                if vertex_id < 0:
                    continue
                    
                # If our triangle is part of a quad, lock vertex
                if triangle_quad_flag != Simplygon.SG_QUADFLAG_TRIANGLE:
                    locks_data[vertex_id] = True

        locks.SetData(locks_data, len(locks_data))

    scene_meshes_selection_set = None
    scene.GetSelectionSetTable().RemoveItem( scene_meshes_selection_set_id )


def reduce_triangles(sg, scene, screen_size):
    """Reduce triangles in scene for specific screen_size."""
    reduction_pipeline = sg.CreateReductionPipeline()
    reduction_settings = reduction_pipeline.GetReductionSettings()
    reduction_settings.SetReductionTargetOnScreenSize( screen_size )
    reduction_settings.SetReductionTargets( Simplygon.EStopCondition_Any, False, False, False, True )
    reduction_pipeline.RunScene( scene, Simplygon.EPipelineRunMode_RunInThisProcess )
    

def unlock_all_vertices(scene):
    """Remove all vertex locks from scene meshes in scene."""
    scene_meshes_selection_set_id = scene.SelectNodes(scene_mesh_type_name)
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet( scene_meshes_selection_set_id )

    for node_id in range(scene_meshes_selection_set.GetItemCount()):
        scene_mesh = Simplygon.spSceneMesh.SafeCast( scene.GetNodeByGUID( scene_meshes_selection_set.GetItem( node_id ) ) )
        geometry = scene_mesh.GetGeometry()
        if geometry and geometry.GetVertexLocks():
            geometry.RemoveVertexLocks()

    scene_meshes_selection_set = None
    scene.GetSelectionSetTable().RemoveItem( scene_meshes_selection_set_id )


def reduce_quads(sg, scene, screen_size):
    """Reduce quads in scene for specific screen_size."""
    quad_pipeline = sg.CreateQuadReductionPipeline()
    quad_reduction_settings = quad_pipeline.GetQuadReductionSettings()
    quad_reduction_settings.SetReductionTargetOnScreenSize( screen_size)
    quad_reduction_settings.SetReductionTargets( Simplygon.EStopCondition_Any, False, False, False, True )
    quad_pipeline.RunScene( scene, Simplygon.EPipelineRunMode_RunInThisProcess )
    
    
    
def process_selection(sg):
    """Perform mixed quad and triangle reduction on selected parts in Maya."""
    scene = export_selection(sg)

    # Lock quads
    lock_vertices_in_quads(scene)
    
    # Reduce triangles
    reduce_triangles(sg, scene, screen_size)

    # Unlock all vertices
    unlock_all_vertices(scene)

    # Reduce quads
    reduce_quads(sg, scene, screen_size)
    
    # Export to Maya
    import_results(scene)


def main():
    sg = simplygon_loader.init_simplygon()

    # Set tangent space for Maya
    sg.SetGlobalDefaultTangentCalculatorTypeSetting(Simplygon.ETangentSpaceMethod_MikkTSpace)

    process_selection(sg)
    del sg


if __name__== "__main__":
    main()
⇐ Back to blog post list