Optimizing assets made of both quads and triangles
Written by Jesper Tingvall, Product Expert, Simplygon
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.
Update in Simplygon 10.1
Since Simplygon 10.1 our quad reducer also handles triangle reduction. That means the steps in this blog post is no longer necessary for most users. It remain however as an example on how to work with vertex locks and geometry data.
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.
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.
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()