Keep original materials during aggregation

Written by Jesper Tingvall, Product Expert, Simplygon

Disclaimer: The code in this post is written using version 10.2.11500.0 of Simplygon and Blender 3.6. 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 we'll showcase how to keep original materials during material merging. This enables you to keep materials that require special shaders separated during optimization.

This blog is a continuation of Aggregation with multiple output materials and will use the same code skeleton. Make sure to read it first.

Prerequisites

This example will use the Simplygon integration in Blender, but the same concepts can be applied to all other integrations of the Simplygon API. If you run it in other integrations you need to change the material casting settings as described in this blog.

Problem to solve

We have an asset that has several opaque and transparent materials as well as some with special shaders. We want to optimize draw calls by merging all opaque materials into one, and all transparent materials into another materials. We want the materials that uses special shaders to remain so we can reuse the original materials and textures for these ones.

Cabinet with pot and goblets containing mystical fluids.

The asset we will use in this blog are from the following sources. The assets have been adapted to suit the content of the blog.

The mystical fluids in the goblets are the ones we want to keep the original materials for.

List of materials.

The original asset contains the following materials. The materials we want to keep, _Slime and _Slime2 are noted by a starting _ character.

Solution

At a high level our solution will work the like following. The first step is to divide up the materials into which ones should be aggregated and which should be kept. After that we will save the original material IDs and UVs. Aggregation is performed and after that the materials that should be kept are restored.

We are going to extend the script we introduced in Aggregation with multiple output materials. The only new functions and changes will be explained in this blog.

Divide up materials

We'll use the same mechanism as in the previous blog, but add another case, MaterialType.KEEP_ORIGINAL. Materials classified as this will be restored after aggregation. The rest of the code is kept as it is, so we'll aggregate all transparent materials into one material and all opaque materials into another one.

# The different kind of material channels we are going to bake our asset into.
class MaterialTypes(IntEnum):
    OPAQUE = 0
    TRANSPARENT = 1
    KEEP_ORIGINAL = 4

In our example we'll mark materials to be kept by that their name starts with a "_".

def get_material_type(scene: Simplygon.spScene, material_id: int) -> MaterialTypes:
    """Return which material type the material is."""
    material = scene.GetMaterialTable().GetMaterial(material_id)
    material_name = material.GetName().lower()
    
    # Material names starting with _ will not be aggregated.
    if material_name[0] == "_":
        return MaterialTypes.KEEP_ORIGINAL
    
    elif material.IsTransparent():
        return MaterialTypes.TRANSPARENT
    else:
        return MaterialTypes.OPAQUE

White Cabinet with pot and goblets containing mystical fluids. Mystical fluids marked

These materials are marked as KEEP_ORIGINAL.

Optimize scene

Our optimize_scene function is almost identical with the one in previous blog, but we have some changes. Before performing the aggregation, we call save_original_material_data which puts the original material ID and UVs into two user defined fields so we can restore them later. We also figure out the dummy material ID for MaterialTypes.KEEP_ORIGINAL. We do not create a new texture table, but instead reuse the old one.

def optimize_scene(sg: Simplygon.ISimplygon, scene : Simplygon.spScene):
    """Optimize scene with aggregator."""

    material_types = get_material_types(scene)
    aggregation_processor = create_aggregator_processor(sg, scene, material_types)
 
    save_original_material_data(scene, ORIGINAL_MATERIAL_IDS_FIELD, ORIGINAL_UVS_FIELD)
    
    # Start the aggregation process.     
    print("Running aggregator...")
    aggregation_processor.RunProcessing()

    # Create temporary material and texture tables the output scene will use.
    new_material_table = sg.CreateMaterialTable()
    new_texture_table = scene.GetTextureTable()  #sg.CreateTextureTable()

    # Remember ID of the material we should restore later
    keep_material_id = get_material_type_index(MaterialTypes.KEEP_ORIGINAL, material_types)

    # Cast all materials
    cast_materials(sg, scene, aggregation_processor, material_types, new_material_table, new_texture_table)

Since the material table is changed after casting; we have new materials, we need to create a dictionary which holds mapping between old material IDs and new material IDs for the materials we want to keep.

Lastly, we restore original material IDs and UV coordinates with restore_original_material_data.

    material_remapping = {}
    material_table = scene.GetMaterialTable()
    for material_id in range(0, material_table.GetMaterialsCount()):
        if get_material_type(scene, material_id) == MaterialTypes.KEEP_ORIGINAL:
            material = material_table.GetMaterial(material_id)
            new_id = new_material_table.AddItem(material)
            material_remapping[material_id] = new_id

    # We can not clear texture table as the textures are used for original materials we want to keep.
            
    # Clear material table and copy in our new materials
    scene.GetMaterialTable().Clear()
    scene.GetMaterialTable().Copy(new_material_table)

    # Restore original materials
    restore_original_material_data(scene, keep_material_id, material_remapping, ORIGINAL_MATERIAL_IDS_FIELD, ORIGINAL_UVS_FIELD)

Saving original material data

To keep the original materials, we need to save original material ID and UVs as the aggregator will write new data. We will save this data as two user fields in our geometry data. User fields are kept after aggregation and reduction. It is a useful way of saving custom data in your model.

There are three types of user fields, per-vertex, per-triangle and per-corner fields. Material ID is saved as a triangle field and UVs as corner field. We are going to use these names to refer to our fields.

ORIGINAL_MATERIAL_IDS_FIELD = "OriginalMaterialIds"
ORIGINAL_UVS_FIELD = "OriginalUVs"

To iterate through all models in our scene we create a selection set containing all scene nodes of type "SceneMesh". We can then loop through the selection set and get the geometry for each mesh.

def save_original_material_data(scene : Simplygon.spScene, original_material_ids_field_name : str, original_uvs_field_name : str):
    scene_meshes_selection_set_id = scene.SelectNodes("SceneMesh")
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet(scene_meshes_selection_set_id)

    # Loop through all meshes in the scene
    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 not geometry: 
            print("Scene without geometry, skipping")
            continue

        if not geometry.GetMaterialIds():
            print("Geometry with no material IDs, skipping")
            continue

        if not geometry.GetTexCoords(0):
            print("Geometry with no TexCoords, skipping")
            continue

We will now create a user field to save material ID. We can see that material ID is a triangle attribute, so a user triangle field sounds good.

When adding a user field we need to specify the field type and tuple size. For material IDs the type is TYPES_ID_RID and we have a size of 1. We can check GetMaterialIDs to validate how it is stored.

Since we specified that the field was of type TYPES_ID_RID we need to cast its array to a RidArray. Using DeepCopy we can copy over our material ID field to our newly created triangle attribute field.

        # Save material IDs
        geometry.AddBaseTypeUserTriangleField( Simplygon.EBaseTypes_TYPES_ID_RID, original_material_ids_field_name , 1 )
        saved_original_material_ids = geometry.GetUserTriangleField(original_material_ids_field_name)
        if saved_original_material_ids != None:
            saved_original_material_ids_array = Simplygon.spRidArray.SafeCast( saved_original_material_ids ) 

            material_ids = geometry.GetMaterialIds()
            saved_original_material_ids_array.DeepCopy(material_ids)

If we look at GetTexCoords we can see that these are stored as a RealArray with a tuple of size 2 per corner. So we'll save them to a field created with AddBaseTypeUserCornerField. We specify that the type is EBaseTypes_TYPES_ID_REAL and a tuple size of 2. Then we copy the first TexCoords field into our newly created corner fields.

        # Save UVs
        geometry.AddBaseTypeUserCornerField( Simplygon.EBaseTypes_TYPES_ID_REAL, original_uvs_field_name , 2 )
        saved_original_uvs = geometry.GetUserCornerField( original_uvs_field_name )
        if saved_original_uvs != None:
            saved_original_uvs_array = Simplygon.spRealArray.SafeCast( saved_original_uvs ) 

            original_uvs = geometry.GetTexCoords(0)
            saved_original_uvs_array.DeepCopy(original_uvs)

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

Change of casting

We are going to introduce one small change to the cast_materials function. We will make so it does not cast any textures for our MaterialTypes.KEEP_ORIGINAL dummy material. Casting textures for this material would be a waste of time as we will not use those textures.

def cast_materials(sg : Simplygon.ISimplygon, scene : Simplygon.spScene, processor : Simplygon.spAggregationProcessor, material_types : list[MaterialTypes], material_table : Simplygon.spMaterialTable, texture_table : Simplygon.spTextureTable):
    """Cast all materials in scene to new texture and material tables."""

    for j in range(0, len(material_types)):
        print(f"Creating output material for {material_types[j]}...")

        # Add new material for each kind
        new_material = sg.CreateMaterial()
        new_material.SetName(f"{material_types[j].name}")
        material_table.AddMaterial( new_material )

        # Do not cast for dummy material KEEP_ORIGINAL as these textures will not be used.
        if material_types[j] == MaterialTypes.KEEP_ORIGINAL:
            continue

        ...

Cabinet with pot and goblets containing white materials.

This is how our asset looks if we process it with what we have so far. Opaque and transparent materials are casted, but our KEEP_ORIGINAL material lacks any textures.

Restore original material data

We'll now add a function which restores the data we saved in save_original_material_data function. We start by creating a selection set of all SceneMesh nodes and iterate through them.

def restore_original_material_data(scene, keep_material_id : int, material_map : dict[int, int], original_material_ids_field_name : str, original_uvs_field_name : str):
    scene_meshes_selection_set_id = scene.SelectNodes("SceneMesh")
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet(scene_meshes_selection_set_id)

    # Loop through all meshes in the scene
    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 not geometry: 
            print("Scene without geometry, skipping")
            continue

        if not geometry.GetMaterialIds():
            print("Geometry with no material IDs, skipping")
            continue

        if not geometry.GetTexCoords(0):
            print("Geometry with no TexCoords, skipping")
            continue

        material_ids = geometry.GetMaterialIds()

We will now restore original UVs and material IDs for the parts of the model where the dummy material is assigned. We start by getting our user field with original UV coordinates. Then we iterate through the UV coordinates and see which triangle ID those are assigned

To calculate the triangle ID from UV index we'll do the following calculation. Each corner has 2 UV coordinates. Each triangle has 3 corners. Thus, the triangle ID can be calculated by triangle_id = uv_coordinate_id / 2 / 3. If the triangle ID has our dummy KEEP_ORIGINAL material, then we restore the original UV coordinates.

        # load original UVs
        saved_original_uvs = geometry.GetUserCornerField( original_uvs_field_name )
        if saved_original_uvs != None:
            saved_original_uvs_array = Simplygon.spRealArray.SafeCast( saved_original_uvs ) 
            current_uvs = geometry.GetTexCoords(0)
            for i in range(0, current_uvs.GetItemCount()):
                triangle_id = math.floor(i / 2.0 /3.0)
                current_material_id = material_ids.GetItem(triangle_id)
                if current_material_id == keep_material_id:
                    current_uvs.SetRealItem(i, saved_original_uvs_array.GetRealItem(i))

We'll do almost the same for material ID. We iterate through all triangles and if the material Id is our dummy KEEP_ORIGINAL we restore the original material ID. We need to refer to our material_map dictionary as the ID might have changed.

Lastly we clean up by removing the temporary selection set.

        # Load saved material IDs
        saved_original_material_ids = geometry.GetUserTriangleField( original_material_ids_field_name )
        if saved_original_material_ids != None:
            saved_original_material_ids_array = Simplygon.spRidArray.SafeCast( saved_original_material_ids ) 
            for triangle_id in range(0, material_ids.GetItemCount()):
                original_material_id = saved_original_material_ids_array.GetItem(triangle_id)
                current_material_id = material_ids.GetItem(triangle_id)
                if current_material_id == keep_material_id:
                    material_ids.SetItem(triangle_id, material_map[original_material_id])

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

Result

Let's run the script and inspect the result.

List of materials; OPAQUE, TRANSPARENT, _SLIME and _SLIME2.

After optimization we get back an asset where all opaque materials are merged into one, all transparent materials are merged into one and all materials we marked for keeping can reuse the original materials.

A side note is that in Blender we always get back a copy of the original materials since it uses gltf as intermediary step. We can change the materials back into the original material after import.

Let us also look at the texture atlases.

Opaque texture atlas.

The opaque texture atlas contains our cabinet's wood material as well as pots and goblets.

Transparent texture atlas.

The transparent texture atlas only contains the windows of cabinet.

Complete script

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

from enum import IntEnum
import bpy
import gc
import math

from simplygon10 import simplygon_loader
from simplygon10 import Simplygon

# Temporary file names.
TMP_DIR = "C:/Tmp/"
IN_FILE = "input.glb"
OUTPUT_FILE = "output.glb"

# Texture size. Change parameters for quality
TEXTURE_SIZE = 4096

# Name of all material channels we want to bake
MATERIAL_CHANNELS = [("Diffuse", Simplygon.EImageColorSpace_sRGB), ("Roughness", Simplygon.EImageColorSpace_Linear), ("Metalness", Simplygon.EImageColorSpace_Linear), ("Normals", Simplygon.EImageColorSpace_Linear)]

# Blender specific name for Normal channel.
NORMAL_CHANNEL = "Normals"

# The different kind of material channels we are going to bake our asset into.
class MaterialTypes(IntEnum):
    OPAQUE = 0
    TRANSPARENT = 1
    AVATAR = 2
    TATTOO = 3
    KEEP_ORIGINAL = 4

ORIGINAL_MATERIAL_IDS_FIELD = "OriginalMaterialIds"
ORIGINAL_UVS_FIELD = "OriginalUVs"

def export_selection(sg: Simplygon.ISimplygon, file_path: str) -> Simplygon.spScene:
    """Export the current selected objects into Simplygon."""
    bpy.ops.export_scene.gltf(filepath = file_path, use_selection=True)
    scene_importer = sg.CreateSceneImporter()
    scene_importer.SetImportFilePath(file_path)
    scene_importer.Run()
    scene = scene_importer.GetScene()
    return scene


def import_results(sg: Simplygon.ISimplygon, scene, file_path : str):
    """Import the Simplygon scene into Blender."""
    scene_exporter = sg.CreateSceneExporter()
    scene_exporter.SetExportFilePath(file_path)
    scene_exporter.SetScene(scene)
    scene_exporter.Run()
    bpy.ops.import_scene.gltf(filepath=file_path)


def save_original_material_data(scene : Simplygon.spScene, original_material_ids_field_name : str, original_uvs_field_name : str):
    scene_meshes_selection_set_id = scene.SelectNodes("SceneMesh")
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet(scene_meshes_selection_set_id)
    # Loop through all meshes in the scene
    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 not geometry: 
            print("Scene without geometry, skipping")
            continue

        if not geometry.GetMaterialIds():
            print("Geometry with no material IDs, skipping")
            continue

        if not geometry.GetTexCoords(0):
            print("Geometry with no TexCoords, skipping")
            continue

        # Save material IDs
        geometry.AddBaseTypeUserTriangleField( Simplygon.EBaseTypes_TYPES_ID_RID, original_material_ids_field_name , 1 )
        saved_original_material_ids = geometry.GetUserTriangleField(original_material_ids_field_name)
        if saved_original_material_ids != None:
            saved_original_material_ids_array = Simplygon.spRidArray.SafeCast( saved_original_material_ids ) 

            material_ids = geometry.GetMaterialIds()
            saved_original_material_ids_array.DeepCopy(material_ids)

        # Save UVs
        geometry.AddBaseTypeUserCornerField( Simplygon.EBaseTypes_TYPES_ID_REAL, original_uvs_field_name , 2 )
        saved_original_uvs = geometry.GetUserCornerField( original_uvs_field_name )
        if saved_original_uvs != None:
            saved_original_uvs_array = Simplygon.spRealArray.SafeCast( saved_original_uvs ) 

            original_uvs = geometry.GetTexCoords(0)
            saved_original_uvs_array.DeepCopy(original_uvs)

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

def restore_original_material_data(scene, keep_material_id : int, material_map : dict[int, int], original_material_ids_field_name : str, original_uvs_field_name : str):
    scene_meshes_selection_set_id = scene.SelectNodes("SceneMesh")
    scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet(scene_meshes_selection_set_id)
    # Loop through all meshes in the scene
    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 not geometry: 
            print("Scene without geometry, skipping")
            continue

        if not geometry.GetMaterialIds():
            print("Geometry with no material IDs, skipping")
            continue

        if not geometry.GetTexCoords(0):
            print("Geometry with no TexCoords, skipping")
            continue

        material_ids = geometry.GetMaterialIds()
        
        # load original UVs
        saved_original_uvs = geometry.GetUserCornerField( original_uvs_field_name )
        if saved_original_uvs != None:
            saved_original_uvs_array = Simplygon.spRealArray.SafeCast( saved_original_uvs ) 
            current_uvs = geometry.GetTexCoords(0)
            for i in range(0, current_uvs.GetItemCount()):
                triangle_id = math.floor(i / 2.0 /3.0)
                current_material_id = material_ids.GetItem(triangle_id)
                if current_material_id == keep_material_id:
                    current_uvs.SetRealItem(i, saved_original_uvs_array.GetRealItem(i))
                    

        # Load saved material IDs
        saved_original_material_ids = geometry.GetUserTriangleField( original_material_ids_field_name )
        if saved_original_material_ids != None:
            saved_original_material_ids_array = Simplygon.spRidArray.SafeCast( saved_original_material_ids ) 
            for triangle_id in range(0, material_ids.GetItemCount()):
                original_material_id = saved_original_material_ids_array.GetItem(triangle_id)
                current_material_id = material_ids.GetItem(triangle_id)
                if current_material_id == keep_material_id:
                    material_ids.SetItem(triangle_id, material_map[original_material_id])

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

    

def setup_output_material(material : Simplygon.spMaterial, material_type : MaterialTypes):
    """Set correct settings for output material depending on material type."""
    if material_type == MaterialTypes.TRANSPARENT or material_type == MaterialTypes.TATTOO:
        material.SetBlendMode(Simplygon.EMaterialBlendMode_Blend)


def get_material_type(scene: Simplygon.spScene, material_id: int) -> MaterialTypes:
    """Return which material type the material is."""
    material = scene.GetMaterialTable().GetMaterial(material_id)
    
    material_name = material.GetName().lower()
    if "slime" in material_name:
        return MaterialTypes.KEEP_ORIGINAL
    
    elif material.IsTransparent():
        return MaterialTypes.TRANSPARENT
    else:
        return MaterialTypes.OPAQUE
    

def get_material_types(scene : Simplygon.spScene) -> list[MaterialTypes]:
    """Returns a list of all material types in scene."""
    material_types = []
    for j in range(0, scene.GetMaterialTable().GetMaterialsCount()):
        material_type = get_material_type(scene, j)
        if not material_type in material_types:
            material_types.append(material_type)
    return material_types


def get_material_type_index(kind : MaterialTypes, material_kinds : list[MaterialTypes]) -> int:
    """Returs the index in material table of a specific material kind. Used to map MaterialTypes -> integers."""
    
    if not kind in material_kinds:
        return -1
    
    return material_kinds.index(kind)


def setup_output_material_settings(output_material : Simplygon.spMappingImageOutputMaterialSettings, material_type : MaterialTypes):
    """Set settings for output material"""

    output_material.SetTextureWidth( TEXTURE_SIZE )
    output_material.SetTextureHeight( TEXTURE_SIZE )


def cast_channel(sg : Simplygon.ISimplygon, scene : Simplygon.spScene, mapping_image : Simplygon.spMappingImage, channel_name : str, sRGB : bool, output_file_name : str) -> str:
    """Cast material channel to texture file. Returns file name."""
    caster = None

    # Special case if channel is a normal channel. Then we use a normal caster.
    if channel_name == NORMAL_CHANNEL:
        caster = sg.CreateNormalCaster()
        caster_settings = caster.GetNormalCasterSettings()
        caster_settings.SetMaterialChannel(channel_name)
        caster_settings.SetOutputImageFileFormat( Simplygon.EImageOutputFormat_PNG)
        
        # Blender specific normal casting settings.
        caster_settings.SetGenerateTangentSpaceNormals(True)
        caster_settings.SetFlipGreen(False)
        caster_settings.SetCalculateBitangentPerFragment(True)
        caster_settings.SetNormalizeInterpolatedTangentSpace(False)
        # Normal maps are per default non-srgb.
        
        # Avoid normal issues
        caster_settings.SetOutputPixelFormat(Simplygon.EPixelFormat_R16G16B16)
        

    else:
        caster = sg.CreateColorCaster()
        caster_settings = caster.GetColorCasterSettings()
        caster_settings.SetMaterialChannel(channel_name)
        caster_settings.SetOutputImageFileFormat( Simplygon.EImageOutputFormat_PNG)
        caster_settings.SetOutputSRGB(sRGB)

    caster.SetMappingImage( mapping_image)
    caster.SetSourceMaterials( scene.GetMaterialTable() )
    caster.SetSourceTextures( scene.GetTextureTable() )
    caster.SetOutputFilePath(output_file_name)

    caster.RunProcessing()
    return caster.GetOutputFilePath()


def create_texture(sg : Simplygon.ISimplygon, file_path : str, color_space : int) -> Simplygon.spTexture:
    """Create a texture from file_path."""
    new_texture = sg.CreateTexture()
    new_texture.SetFilePath( file_path )
    new_texture.SetName(file_path)
    new_texture.SetColorSpace(color_space)
    return new_texture


def create_shading_network(sg : Simplygon.ISimplygon, texture_name : str) -> Simplygon.spShadingNode:
    """Create a simple shading network which displays a texture of name texture_name."""
    texture_node = sg.CreateShadingTextureNode()
    texture_node.SetTexCoordLevel( 0 )
    texture_node.SetTextureName( texture_name )
    return texture_node


def create_aggregator_processor(sg: Simplygon.ISimplygon, scene: Simplygon.spScene, material_types : list[MaterialTypes]):
    """Create aggregation processor with mapping image set to split materials into different material types."""

    # Create the aggregation processor. 
    aggregation_processor = sg.CreateAggregationProcessor()
    aggregation_processor.SetScene( scene )
    aggregation_settings = aggregation_processor.GetAggregationSettings()
    mapping_image_settings = aggregation_processor.GetMappingImageSettings()
    
    # Merge all geometries into a single geometry. 
    aggregation_settings.SetMergeGeometries( True )
    
    # Generates a mapping image which is used after the aggregation to cast new materials to the new 
    # aggregated object. 
    mapping_image_settings.SetGenerateMappingImage( True )
    mapping_image_settings.SetApplyNewMaterialIds( True )
    mapping_image_settings.SetGenerateTangents( True )
    mapping_image_settings.SetUseFullRetexturing( True )

    mapping_image_settings.SetTexCoordGeneratorType( Simplygon.ETexcoordGeneratorType_ChartAggregator )
    chart_aggregator_settings = mapping_image_settings.GetChartAggregatorSettings()
    chart_aggregator_settings.SetChartAggregatorMode( Simplygon.EChartAggregatorMode_SurfaceArea )
    chart_aggregator_settings.SetSeparateOverlappingCharts( False )

    # Set input material count to number of materials in input scene.
    material_count = scene.GetMaterialTable().GetMaterialsCount()
    mapping_image_settings.SetInputMaterialCount( material_count )

    # Set output material count to each material kind present in the input scene.
    mapping_image_settings.SetOutputMaterialCount( len(material_types))
 
    # Set material mapping where all materials of a certain kind maps to the same output material.
    for j in range(0, material_count):
        kind = get_material_type(scene,j )
        new_id = get_material_type_index(kind, material_types)
        mapping_image_settings.GetInputMaterialSettings(j).SetMaterialMapping(new_id)
    
    # Set output material settings.
    for j in range(0, len(material_types)):
        setup_output_material_settings(mapping_image_settings.GetOutputMaterialSettings(j), material_types[j])

    return aggregation_processor


def cast_materials(sg : Simplygon.ISimplygon, scene : Simplygon.spScene, processor : Simplygon.spAggregationProcessor, material_types : list[MaterialTypes], material_table : Simplygon.spMaterialTable, texture_table : Simplygon.spTextureTable):
    """Cast all materials in scene to new texture and material tables."""

    for j in range(0, len(material_types)):
        print(f"Creating output material for {material_types[j]}...")

        # Add new material for each kind
        new_material = sg.CreateMaterial()
        new_material.SetName(f"{material_types[j].name}")
        material_table.AddMaterial( new_material )

        if material_types[j] == MaterialTypes.KEEP_ORIGINAL:
            continue

        for channel in MATERIAL_CHANNELS:
            # Cast texture to file for specific mapping image
            print(f"Casting {channel[0]}...")
            sRGB = channel[1] == Simplygon.EImageColorSpace_sRGB
            casted_texture_file = cast_channel(sg, scene, processor.GetMappingImageForImageIndex(j), channel[0], sRGB, f"{TMP_DIR}{channel[0]}_{j}" )
            print(f"Casted {casted_texture_file}")

            # Create a texture from newly casted texture file.
            texture_table.AddTexture(create_texture(sg, casted_texture_file, channel[1]) )
    
            # Create material from texture
            new_material.AddMaterialChannel( channel[0] )
            new_material.SetShadingNetwork( channel[0], create_shading_network(sg, casted_texture_file) )

            setup_output_material(new_material, material_types[j])


def optimize_scene(sg: Simplygon.ISimplygon, scene : Simplygon.spScene):
    """Optimize scene with aggregator."""

    material_types = get_material_types(scene)
    aggregation_processor = create_aggregator_processor(sg, scene, material_types)
 
    save_original_material_data(scene, ORIGINAL_MATERIAL_IDS_FIELD, ORIGINAL_UVS_FIELD)
    
    # Start the aggregation process.     
    print("Running aggregator...")
    aggregation_processor.RunProcessing()

    # Create temporary material and texture tables the output scene will use.
    new_material_table = sg.CreateMaterialTable()
    new_texture_table = scene.GetTextureTable()  #sg.CreateTextureTable()

    keep_material_id = get_material_type_index(MaterialTypes.KEEP_ORIGINAL, material_types)

    # Cast all materials
    cast_materials(sg, scene, aggregation_processor, material_types, new_material_table, new_texture_table)

    material_remapping = {}
    material_table = scene.GetMaterialTable()
    for material_id in range(0, material_table.GetMaterialsCount()):
        if get_material_type(scene, material_id) == MaterialTypes.KEEP_ORIGINAL:
            material = material_table.GetMaterial(material_id)
            new_id = new_material_table.AddItem(material)
            material_remapping[material_id] = new_id

    # We can not clear texture table above since we still are using it for material casting. Now when all materials are casted we can assign new material table and texture table to scene.
    #scene.GetTextureTable().Clear()
    scene.GetMaterialTable().Clear()
    #scene.GetTextureTable().Copy(new_texture_table)
    scene.GetMaterialTable().Copy(new_material_table)

    restore_original_material_data(scene, keep_material_id, material_remapping, ORIGINAL_MATERIAL_IDS_FIELD, ORIGINAL_UVS_FIELD)


def process_selection(sg : Simplygon.ISimplygon):
    """Remove and bake decals on selected meshes."""

    # Export scene from Blender and import it 
    scene = export_selection(sg, TMP_DIR+IN_FILE)
    
    # Aggregate optimized scene
    optimize_scene(sg, scene)
    
    # Import result into Blender
    import_results(sg, scene, TMP_DIR+OUTPUT_FILE)


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

    process_selection(sg)
    sg = None
    gc.collect()


if __name__== "__main__":
    main()
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*