Protecting selected materials
Written by Samuel Rantaeskola, Product Expert, Simplygon
Disclaimer: This post is written using version 10.3.6400.0 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 this blog post, we will guide you through the art of safeguarding specific materials during mesh reduction. Imagine scenarios where preserving intricate details, such as character faces or dynamic materials like cloth, is crucial to maintaining the overall visual appeal of your 3D models.
While excluding entire sub-meshes with a single material is a straightforward approach, the real challenge arises when you need to protect specific materials within a mesh containing a mix of both protected and reducible elements. We will explore the intricacies of vertex locking — an invaluable tool that empowers you to selectively preserve the essence of your 3D creations.
Many concepts we will use in this blog has already been explored in Protecting features using vertex locks and weights.
Problem to solve
For this blog we will use Unity Chan as an example. It's a rather low poly asset and we are going to reduce it to 50%. In order to maintain fidelity where it's most important we are going to protect the face and eyes. Here it is before reduction.
Solution
The face and eyes are using these materials:
- unitychan:chan:m_face
- unitychan:chan:M_eye_L
- unitychan:chan:M_eye_R
We will device a script that finds the material ids for them and then loops through the verts, locking the ones that are assigned to any of these materials.
Material name to material ID
First we are going to create this little function. It finds the ids of the materials you want to protect and returns a list of protected material ids.
'''
Creates a list where all the protected material IDs are stored.
'''
def create_protected_material_id_list(material_table: Simplygon.spMaterialTable, protected_material_names: list) -> list:
protected_material_ids = []
for i in range(0, material_table.GetItemsCount()):
material = Simplygon.spMaterial.SafeCast(material_table.GetItem(i))
if material.GetName() in protected_material_names:
protected_material_ids.append(i)
return protected_material_ids
Protecting verts
We will be using code that is very similar to the blog linked in the introduction. The only real modification is this little code snippet:
# Unlock all vertices.
locks = geometry.GetVertexLocks()
locks_data = [False] * locks.GetItemCount()
# It's a lot faster to get all the material ID data in one go, rather than get an item at the time.
material_id_data = geometry.GetMaterialIds().GetData()
vertex_id_data = geometry.GetVertexIds().GetData()
for i in range(0, len(material_id_data)):
if material_id_data[i] in protected_material_ids:
# This triangle should be protected. Let's lock the verts.
verts = vertex_id_data[i*3:i*3+3]
# Lock the vertex ids in the corners of this triangle
for j in range(i*3, i*3+3):
vert_id = vertex_id_data[j]
locks_data[vert_id] = True
locks.SetData(locks_data, len(locks_data))
Results
Now we have everything we need to run the process on our test model. Here are the results:
The Script
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
from simplygon10 import simplygon_loader
from simplygon10 import Simplygon
'''
Creates a list where all the protected material IDs are stored.
'''
def create_protected_material_id_list(material_table: Simplygon.spMaterialTable, protected_material_names: list) -> list:
protected_material_ids = []
for i in range(0, material_table.GetItemsCount()):
material = Simplygon.spMaterial.SafeCast(material_table.GetItem(i))
if material.GetName() in protected_material_names:
protected_material_ids.append(i)
return protected_material_ids
'''
Returns all meshes into the mesh list
'''
def find_meshes(scene_node: Simplygon.spSceneNode, meshes: list) -> None:
for i in range(0, scene_node.GetChildCount()):
child = scene_node.GetChild(i)
mesh = Simplygon.spSceneMesh.SafeCast(child)
if mesh:
meshes.append(mesh)
find_meshes(child, meshes)
'''
Locks all verts in the scene belonging to one of the protected materials.
'''
def lock_protected_materials(scene: Simplygon.spScene, protected_material_ids: list) -> None:
meshes = []
find_meshes(scene.GetRootNode(), meshes)
# Loop through all meshes in the scene
for mesh in meshes:
# Get geometry for mesh
geometry = 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()
# It's a lot faster to get all the material ID data in one go, rather than get an item at the time.
material_id_data = geometry.GetMaterialIds().GetData()
vertex_id_data = geometry.GetVertexIds().GetData()
for i in range(0, len(material_id_data)):
if material_id_data[i] in protected_material_ids:
# This triangle should be protected. Let's lock the verts.
verts = vertex_id_data[i*3:i*3+3]
# Lock the vertex ids in the corners of this triangle
for j in range(i*3, i*3+3):
vert_id = vertex_id_data[j]
locks_data[vert_id] = True
locks.SetData(locks_data, len(locks_data))
def reduce_with_protected_materials(sg: Simplygon.ISimplygon, asset_file: str, protected_material_names: list, output_file: str) -> None:
# Load the asset we want to process
scene_importer = sg.CreateSceneImporter()
scene_importer.SetImportFilePath(asset_file)
if scene_importer.Run() == Simplygon.EErrorCodes_NoError:
scene = scene_importer.GetScene()
# Convert the protected material names to IDs
protected_material_ids = create_protected_material_id_list(scene.GetMaterialTable(), protected_material_names)
# Lock down all the verts that are assigned to a protected material.
lock_protected_materials(scene, protected_material_ids)
# Let's run a standard 50% reduction just to verify that it works.
# Keep in mind that if you are locking down a lot of verts and
# run a reduction with a percentage target unprotected areas will suffer.
# Way better to use screen size in a production environment.
reduction_pipeline = sg.CreateReductionPipeline()
reduction_settings = reduction_pipeline.GetReductionSettings()
reduction_settings.SetReductionTargetTriangleRatio(0.5)
reduction_pipeline.RunScene( scene, Simplygon.EPipelineRunMode_RunInThisProcess )
# Export the scene to the output file
scene_exporter = sg.CreateSceneExporter()
scene_exporter.SetExportFilePath(output_file)
scene_exporter.SetScene(scene)
scene_exporter.Run()
else:
errors = sg.CreateStringArray()
sg.GetErrorMessages(errors)
for i in range(0, errors.GetItemCount()):
print(errors.GetItem(i))
raise Exception(f'Failed to import {asset_file}')
def main():
sg = simplygon_loader.init_simplygon()
print(sg.GetVersion())
protected_material_names = ["unitychan:chan:m_face","unitychan:chan:M_eye_L","unitychan:chan:M_eye_R"]
reduce_with_protected_materials(sg, "unitychan.fbx", protected_material_names , "output.fbx")
del sg
if __name__== "__main__":
main()