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.

Unity Chan

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: Comparison

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()
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*