Aggregation and remeshing - Evaluating the best method for proxy creation

Disclaimer: The code in this post is written on version 10.1.3300 of Simplygon. Should you encounter this post running a different version, some of the API calls might differ. However, the core concepts should still remain valid.

Introduction

Are you trying to create a representation object that can be viewed from a distance, but unsure whether to use remeshing or aggregation? Both approaches have their benefits and drawbacks, so in this blog post we'll compare the two to help you make an informed decision.

On one hand, remeshing is an effective way to drastically reduce the geometry of a collection of objects. This can be especially useful if you're working with large, complex models and need to reduce the number of polygons for performance reasons. However, one drawback of remeshing is that it often requires creating a new material, costing memory.

On the other hand, aggregation allows you to combine multiple objects into a single mesh, which can also help reduce the overall complexity of the model. Unlike remeshing, aggregation doesn't require creating a new material, but it may not be as effective at reducing the geometry as much.

Ultimately, the decision between remeshing and aggregation will depend on your specific needs and the trade-offs you're willing to make. With this post we will show how to create a script that allows you to decide on the best approach for you.

Helper functions

Before we begin the main part of the post, we need to define some helper functions. These functions have been discussed in previous blog posts. We will not go into much detail about them and you can find them at the end of this post.

With this out of the way we can get into the meat of the post.

Example assets

For our example we are going to use this collection of assets.

example_assets

This collection comprises of assets built up from multiple meshes, materials, and polycounts ranging from 50K to 130K. To facilitate the selection of a suitable proxy generation method for this collection, we will establish a framework for evaluation. However, since the requirements vary based on the game and rendering techniques utilized, we cannot offer a one-size-fits-all solution. Instead, we will equip you with the necessary tools to make your own informed decision.

The script

The objective of this blog post is to provide you with a solution that enables you to complete this function:

'''
This is the magic function where you decide which proxy works best for you. Things to consider:
* How many polys can we handle for this asset?
* Is object count an issue with this asset?
* Is it worth spending extra memory on new textures to reduce number of materials?
* Bonus: Compare the results automatically in a render.
'''
def decide_on_proxy(results: ProcessingResult) -> AssetInformation:
    # Naively we're returning a random proxy.
    return random.choice(results.proxies)

We are currently utilizing an excessively simplistic approach, which involves randomly choosing one of the proxies. As previously stated, any efforts to elaborate on this approach will prove inadequate since it is heavily reliant on various factors such as the game, renderer, asset types, art style, and more. However, we will furnish you with the necessary scripts to initiate the analysis of your proxies.

Starting point

First, we need a starting point.

'''
Creates proxies for all assets in the input folder and outputs them into the output folder.
'''
def process_assets(sg: Simplygon.ISimplygon, input_folder:str, output_folder: str) -> None:
    files = glob.glob(f'{input_folder}/*.fbx')
    for f in files:
        file_name = os.path.basename(f)
        asset_name = file_name[:file_name.find(".")]
        scene = import_scene(sg, f)
        processing_results = ProcessingResult(asset_name, f, scene)
        create_asset_proxies(sg, processing_results, output_folder)
        output_results(processing_results)
        print(f'The {decide_on_proxy(processing_results).name} is the best proxy for this object')

def main():
    sg = simplygon_loader.init_simplygon()
    output_folder = "output"
    if os.path.isdir(output_folder):
        shutil.rmtree(output_folder)
    process_assets(sg, "input", output_folder)
    del sg

if __name__== "__main__":
    main()

This is a straightforward piece of code that iterates through all FBX assets located in a designated folder and performs the create_asset_proxies function on each one of them. Before we examine that function we need to have some structures to pass data along.

Data structures

To monitor the data we produce, we established the subsequent data structures, which we can subsequently employ to assess the proxies we generate.

'''
Contains the stats about an asset.
'''
class AssetInformation:
    name: str
    path: str
    scene: Simplygon.spScene
    mesh_count: int
    poly_count: int
    material_count: int

    def __init__(self, name: str, path:str, scene: Simplygon.spScene):
        self.name = name
        self.path = path
        self.scene = scene
        self.mesh_count = get_mesh_count(scene)
        self.poly_count = get_poly_count(scene)
        self.material_count = get_material_count(scene)

'''
Contains all stats about the source asset and all processed proxies.
'''
class ProcessingResult:
    source_info: AssetInformation
    proxies: list

    def __init__(self, source_name: str, source_path: str, source_scene: Simplygon.spScene):
        self.source_info = AssetInformation(source_name, source_path, source_scene)
        self.proxies = []

    def add_proxy_results(self, proxy_stats: AssetInformation):
        self.proxies.append(proxy_stats)

Creating a bunch of proxy options

To determine the optimal proxy strategy, you can generate multiple options for evaluation, although the processing time required for this may be a factor. In our illustration, we evaluate three distinct methods: remeshing, reduction and aggregation, and reduction together with aggregation with material merging. If desired, you can readily extend this function to include additional cases for evaluation.

'''
Use this function to create an array of different proxies you want to evalute. The resulting proxies will be 
returned in the processing results for later scrutiny
'''
def create_asset_proxies(sg: Simplygon.ISimplygon, processing_results: ProcessingResult, output_folder: str)-> None:
    # Output all the proxies into a folder in the output folder.
    proxy_folder = os.path.join(output_folder, processing_results.source_info.name)
    # Let's first create a remeshed proxy
    pipeline = create_remeshing_pipeline(sg, "Remeshed", 300, 1024)
    processing_results.add_proxy_results(run_pipeline(sg, pipeline, processing_results.source_info, proxy_folder))

    # An intermediate reduction that we will aggregate later. We won't store it on disk.
    pipeline = create_reduction_pipeline(sg, "Reduction", 300)
    reduction_info = run_pipeline(sg, pipeline, processing_results.source_info)

    # First an aggregation without material merging on the reduced object
    pipeline = create_aggregaton_pipline(sg, "Aggregation")
    processing_results.add_proxy_results(run_pipeline(sg, pipeline, reduction_info, proxy_folder))

    # Then an aggregation with material merging on the reduced object
    pipeline = create_aggregaton_pipline(sg, "AggregationMergedMats", 512)
    processing_results.add_proxy_results(run_pipeline(sg, pipeline, reduction_info, proxy_folder))

What are the rationales for employing these distinct pipelines? The remeshing pipeline is designed to generate a proxy for a 300px screen size (information regarding screen size). It will create an entirely new mesh with a new material to represent the original asset.

We utilize the reducer as a pre-step before aggregating the meshes. The reduction target is the same as the remesher's, but it generally yields a considerably higher poly count since it does not generate a new mesh. It is preferable to reduce prior to aggregation rather than the reverse, as the aggregator will make the reducer's job a bit more difficult. We are not concerned with storing the reduced asset; we just pass it into the two distinct aggregation processes.

Lastly, we execute two distinct aggregation processes.

The first one merely combines all objects into one while eliminating any interiors. This reduces the mesh count and, to some degree, the poly count. We leave the materials unaltered, implying that the resulting proxy will not necessitate any additional texture memory.

The second aggregation merges all materials via chart aggregation. This results in far more efficient utilization of texture space compared to the remesher since the UVs are not unwrapped. This means that in cases where numerous objects share the same texture, we can employ a much lower texture resolution while maintaining the same visual fidelity as the remesher. Comparing material casting and aggregation does a deep dive into this topic.

The complete script with the setup functions for the distinct pipelines will be provided at the conclusion of this post.

Running the pipelines

Executing the pipelines merely entails configuring the material casting using the helper functions found in material_caster_helper.py (located at the end of this blog post). In most proxy scenarios, it is also beneficial to cull the objects with the terrain to eliminate any triangles that protrude through the ground. We have supplied a helper function in scene_helpers.py (also located at the bottom of this post). The function will return details about the newly generated proxy.

'''
Will apply the provided pipelines to provided asset.
The result will be returned in a new asset information object.
If a clipping pattern is provided the asset will be clipped with that geometry.
'''
def run_pipeline(sg: Simplygon.ISimplygon, pipeline: Simplygon.spPipeline, asset_information: AssetInformation, output_folder: str = None) -> AssetInformation:
    # Load the asset we want to process
    print(f'Processing {asset_information.name} with {pipeline.GetName()}')
    scene = asset_information.scene.NewCopy()
    mapping_image_settings = pipeline.GetMappingImageSettings()
    # If generate mapping image is set on the pipeline we'll assume that material casters should be added.
    if mapping_image_settings.GetGenerateMappingImage():
        setup_material_casters(sg, pipeline, scene)

    (clipping_set_id,process_set_id) = split_into_clipping_and_processing(sg, scene, '*terrain*')
    # With the name of the pipeline we can access the different settings objects neatly in python.
    pipe_type = pipeline.GetClass()[1:-8]
    pipe_settings = eval(f'Simplygon.sp{pipe_type}Pipeline.SafeCast(pipeline).Get{pipe_type}Settings()')
    # Reduction, for example, doesn't have geometry culling.
    if 'GetGeometryCullingSettings' in dir(pipeline):
        culling_settings = pipeline.GetGeometryCullingSettings()
        culling_settings.SetUseClippingGeometry(True)
        culling_settings.SetClippingGeometrySelectionSetID(clipping_set_id)
    else:
        # We're keeping the terrain in the scene if we're not culling with it.
        pipe_settings.SetKeepUnprocessedSceneMeshes(True)

    # Process set ID is part of the pipeline specific settings. This is an dirty way to do it and does not work for all
    # pipeline types. But it does cover reduction, remeshing and aggregation.
    pipe_settings.SetProcessSelectionSetID(process_set_id)
    pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
    # Save the results if the output path is specified
    if output_folder != None:
        export_scene(sg, scene, os.path.join(output_folder, f'{pipeline.GetName()}.fbx'))

    return AssetInformation(pipeline.GetName(), output_folder, scene)

The results

We now have all the necessary components to conduct tests on our sample assets. After running the tests, we obtained statistics for the assets that were featured in the image earlier in the blog post. These statistics include information on the size, quality, and effectiveness of the generated proxies. By analyzing these statistics, we can evaluate the different proxies..

Mesh count

Asset Original
cottage 70
gem_mine 300
gold_mine 123
mushroom_house 300
treehouse 88

All of the methods mentioned will result in a single-mesh output, so the only thing to consider is whether the original mesh's mesh count and poly count justify the additional geometry's expense.

Poly count

Asset Original Remesh Reduction+Aggregation Reduction+ Aggregation w. mat merge
cottage 86058 6221 28529 28529
gem_mine 51935 3351 14283 14283
gold_mine 138737 3084 18841 18841
mushroom_house 48780 6661 9886 9886
treehouse 43840 5920 16713 16713

In most cases, the remesher appears to be the superior method. While the relationship between poly count and memory usage is highly dependent on the renderer, a rough estimate suggests that each triangle consumes between 20-30 bytes. As a result, the aggregation-based versions of the cottage would require roughly 600KB more memory than the remeshed version.

Material count

Asset Original Original size on disk Remesh size on disk Merged size on disk
cottage 16 5.41 MB 3.71 MB 0.6 MB
gem_mine 23 5.76 MB 3.40 MB 0.5 MB
gold_mine 8 4.18 MB 3.18 MB 0.6 MB
mushroom_house 12 12.1 MB 2.86 MB 0.3 MB
treehouse 12 7.82 MB 3.26 MB 0.4 MB

The remeshing method and the aggregation with material merging will both result in a single material, whereas the aggregation method alone will retain the original material. The estimated size on the dish is only a rough approximation, calculated using PNG files, and is likely to differ in-game depending on how the textures are packed.

Here is a visual breakdown of the mushroom_house and its proxies. Can you identify which one is the original and which ones are the proxies? process results

Below are the wireframe results labeled with the process that was used to create the proxy.

process results

Deciding on the best proxy - for you

Let's take a closer look at the decide_on_proxy function mentioned earlier in the post and consider what factors we should take into account when building it.

The decision of which proxy to use can be complex, especially when it comes to memory consumption. If a game keeps all assets in memory at all times, then the memory consumption of the textures and geometry will add to the overall memory consumption of the game. However, many games today use some form of streaming solution, where a proxy can reduce the overall memory footprint.

For example, if we replace the mushroom house with a proxy, we can discard 12 MB of textures and 1.5 MB of geometry data. If we use the aggregation with material merging method, we would only load 0.3 MB of texture memory and 0.3 MB of geometry data, resulting in a gain of almost 13 MB. But it's not always that simple. The mushroom house might be using some textures that are also used on other objects, or it could even use some mesh that is part of other objects as well.

To decide on the optimal proxy for a certain asset collection, we should take into account information like how many objects are using a certain texture and mesh. Objects that only use common materials and meshes might be better left untouched, while objects with many single-use meshes and materials might benefit from a high-quality proxy to be shifted out early.

There is no single solution that fits all games, nor even a solution that works for every asset. However, developing strategies to make the most use of proxies in your game can help. By storing all proxies created and setting up the pipeline so that you can rapidly toggle between different proxy variants, you can test performance and memory impact on-the-fly and optimize your game in the late stages of development much more easily.

Taking visual fidelity into account

A forthcoming article will cover the subsequent phase of selecting the optimal proxy, which involves considering visual accuracy. Until the publication of that article, a function named calculate_visual_fidelity included in the attached script will act as a temporary placeholder. In the upcoming function, we will explore a technique for measuring a metric by contrasting the output meshes of the original asset and its proxies at the desired distance, allowing us to evaluate the visual accuracy of the results. This metric can be used to identify the most appropriate proxy method for a specific asset. If you wish to investigate this subject prior to the article's release, you may review the ImageComparer API.

The complete scripts

file_helpers.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from simplygon10 import Simplygon

'''
Imports the asset file and returns it as a scene.
'''
def import_scene(sg: Simplygon.ISimplygon, asset_path: str) -> Simplygon.spScene:
    scene_importer = sg.CreateSceneImporter()
    scene_importer.SetImportFilePath(asset_path)

    import_result = scene_importer.Run()
    if Simplygon.Failed(import_result):
        raise Exception(f'Import of {asset_path} failed because {str(import_result)}')

    return scene_importer.GetScene()

'''
Exports the scene into the provided asset file.
'''
def export_scene(sg: Simplygon.ISimplygon, scene: Simplygon.spScene, asset_path: str) -> None:
    scene_exporter = sg.CreateSceneExporter()
    scene_exporter.SetExportFilePath(asset_path)
    scene_exporter.SetScene(scene)

    export_result = scene_exporter.Run()
    if Simplygon.Failed(export_result):
        raise Exception(f'Exporting to {asset_path} failed because {str(export_result)}')

scene_helpers.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from simplygon10 import Simplygon
import fnmatch

'''
Creates a selection set and adds all meshes in the scene which name matches the provided pattern.
'''
def split_into_clipping_and_processing(sg: Simplygon.ISimplygon, scene: Simplygon.spScene, clipping_pattern: str) -> None:
    clipping_set = sg.CreateSelectionSet()
    processing_set = sg.CreateSelectionSet()
    fill_selection_sets(scene, clipping_set, processing_set, clipping_pattern)
    clipping_set_id = scene.GetSelectionSetTable().AddSelectionSet(clipping_set)
    process_set_id = scene.GetSelectionSetTable().AddSelectionSet(processing_set)
    return (clipping_set_id, process_set_id)

'''
Adds all the meshes which name matches the provided pattern to the provided selection set.
'''
def fill_selection_sets(scene: Simplygon.spScene, clipping_set: Simplygon.spSelectionSet, processing_set: Simplygon.spSelectionSet, clipping_pattern: str) -> None:
    meshes = []
    find_meshes(scene.GetRootNode(), meshes)
    for mesh in meshes:
        if fnmatch.fnmatch(mesh.GetName(), clipping_pattern):
            clipping_set.AddItem(mesh.GetNodeGUID())
        else:
            processing_set.AddItem(mesh.GetNodeGUID())

'''
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)
        if child.IsA("ISceneMesh"):
            meshes.append(Simplygon.spSceneMesh.SafeCast(child))
        find_meshes(child, meshes)

'''
Returns number of polys in the scene
'''
def get_poly_count(scene: Simplygon.spScene) -> int:
    meshes = []
    find_meshes(scene.GetRootNode(), meshes)
    polys = 0
    for mesh in meshes:
        polys += mesh.GetGeometry().GetTriangleCount()
    return polys

'''
Returns number of materials in the scene.
'''
def get_material_count(scene: Simplygon.spScene) -> int:
    return scene.GetMaterialTable().GetMaterialsCount()

'''
Returns number of mesh in the scene.
'''
def get_mesh_count(scene: Simplygon.spScene) -> int:
    meshes = []
    find_meshes(scene.GetRootNode(), meshes)
    return len(meshes)

material_casting_helpers.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from simplygon10 import Simplygon
from simplygon10 import simplygon_loader

'''
Finds all the material channels with textures connected to them and creates the appropriate caster for the channel.
'''
def add_default_casters(sg: Simplygon.ISimplygon, pipeline: Simplygon.spPipeline, scene: Simplygon.spScene) -> None:
    materials = scene.GetMaterialTable()
    texture_channels = sg.CreateStringArray()
    materials.GetMaterialChannelsWithTextureInputs(texture_channels)
    for i in range(0, texture_channels.GetItemCount()):
        channel = texture_channels.GetItem(i)
        if channel == Simplygon.SG_MATERIAL_CHANNEL_NORMALS or "normal" in channel:
            # We will add a normal caster no matter what. The remeshing turns out better that way.
            continue
        elif channel == Simplygon.SG_MATERIAL_CHANNEL_OPACITY or "transparency" in channel:
            caster = sg.CreateOpacityCaster()
            caster.SetMappingImage( pipeline.GetMappingImage(0))
            caster.SetSourceMaterials( scene.GetMaterialTable())
            caster.SetSourceTextures( scene.GetTextureTable() )
            caster_settings = caster.GetOpacityCasterSettings()
            caster_settings.SetOutputImageFileFormat( Simplygon.EImageOutputFormat_PNG )
            caster_settings.SetOutputPixelFormat( Simplygon.EPixelFormat_R8 )
        elif channel == Simplygon.SG_MATERIAL_CHANNEL_BASECOLOR:
            # GLB stores transparency in the alpha channel, so we need to handle that.
            caster = sg.CreateColorCaster()
            caster_settings = caster.GetColorCasterSettings()
            caster_settings.SetOpacityChannelComponent(Simplygon.EColorComponent_Alpha)
        else:
            caster = sg.CreateColorCaster()
            caster_settings = caster.GetColorCasterSettings()
        caster_settings.SetMaterialChannel(channel) 
        pipeline.AddMaterialCaster( caster, 0 )
    # Finally, let's add a normal caster so that the remesher can move details into the normal map.
    caster = sg.CreateNormalCaster()
    caster_settings = caster.GetNormalCasterSettings()
    caster_settings.SetGenerateTangentSpaceNormals(True)
    pipeline.AddMaterialCaster( caster, 0 )

'''
Sets up the material casters by looking at finding all the material channels with texture inputs and creates casters for those.
If you want to cast custom channels with a lot of custom shading information, Compute casters is a suggest venue to look at.
'''
def setup_material_casters(sg: Simplygon.ISimplygon, pipeline: Simplygon.spPipeline, scene: Simplygon.spScene) -> None:
    mapping_image_settings = pipeline.GetMappingImageSettings()
    mapping_image_settings.SetGenerateMappingImage(True)
    mapping_image_settings.SetTexCoordName("MaterialLOD")
    if pipeline.IsA('IAggregationPipeline'):
        # Enable the chart aggregator and reuse UV space. 
        mapping_image_settings.SetTexCoordGeneratorType(Simplygon.ETexcoordGeneratorType_ChartAggregator)
        mapping_image_settings.SetApplyNewMaterialIds( True )
        mapping_image_settings.SetGenerateTangents( True )
        mapping_image_settings.SetUseFullRetexturing( True )
        chart_aggregator_settings = mapping_image_settings.GetChartAggregatorSettings()    
        chart_aggregator_settings.SetChartAggregatorMode(Simplygon.EChartAggregatorMode_SurfaceArea)
        chart_aggregator_settings.SetSeparateOverlappingCharts( False )
    add_default_casters(sg, pipeline, scene)

proxy_evaluator.py

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT license. 
from simplygon10 import simplygon_loader
from simplygon10 import Simplygon
from material_casting_helpers import *
from file_helpers import *
from scene_helpers import *
import os, glob, shutil, random

'''
Contains the stats about an asset.
'''
class AssetInformation:
    name: str
    path: str
    scene: Simplygon.spScene
    mesh_count: int
    poly_count: int
    material_count: int

    def __init__(self, name: str, path:str, scene: Simplygon.spScene):
        self.name = name
        self.path = path
        self.scene = scene
        self.mesh_count = get_mesh_count(scene)
        self.poly_count = get_poly_count(scene)
        self.material_count = get_material_count(scene)

'''
Contains all stats about the source asset and all processed proxies.
'''
class ProcessingResult:
    source_info: AssetInformation
    proxies: list

    def __init__(self, source_name: str, source_path: str, source_scene: Simplygon.spScene):
        self.source_info = AssetInformation(source_name, source_path, source_scene)
        self.proxies = []

    def add_proxy_results(self, proxy_stats: AssetInformation):
        self.proxies.append(proxy_stats)


''' 
Creates a remeshing pipline with the specified screen_size and texture size for casting.
'''
def create_remeshing_pipeline(sg: Simplygon.ISimplygon, name: str, screen_size: int, texture_size: int) -> Simplygon.spRemeshingPipeline: 
    remeshing_pipeline = sg.CreateRemeshingPipeline()
    remeshing_pipeline.SetName(name)
    remesher_settings = remeshing_pipeline.GetRemeshingSettings()
    remesher_settings.SetOnScreenSize(screen_size)    
    mapping_image_settings = remeshing_pipeline.GetMappingImageSettings()
    mapping_image_settings.SetGenerateMappingImage(True)
    material_settings = mapping_image_settings.GetOutputMaterialSettings(0)
    material_settings.SetTextureWidth(texture_size)
    material_settings.SetTextureHeight(texture_size)
    return remeshing_pipeline

''' 
Creates a reduction pipline with the specified screen_size.
'''
def create_reduction_pipeline(sg: Simplygon.ISimplygon, name: str, screen_size: int) -> Simplygon.spReductionPipeline: 
    reduction_pipeline = sg.CreateReductionPipeline()
    reduction_pipeline.SetName(name)
    reduction_settings = reduction_pipeline.GetReductionSettings()
    reduction_settings.SetReductionTargets(Simplygon.EStopCondition_All, False, False, False, True)
    reduction_settings.SetReductionTargetOnScreenSize(screen_size)
    return reduction_pipeline


''' 
Creates an aggregation pipline with the specified screen_size. 
If texture size is set to larger than 0 material casting will be enabled.
'''
def create_aggregaton_pipline(sg: Simplygon.ISimplygon, name: str, texture_size: int = 0) -> Simplygon.spAggregationPipeline: 
    aggregation_pipeline = sg.CreateAggregationPipeline()
    aggregation_pipeline.SetName(name)
    aggregator_settings = aggregation_pipeline.GetAggregationSettings()
    aggregator_settings.SetEnableGeometryCulling(True)
    aggregator_settings.SetGeometryCullingPrecision(0.2)    
    if texture_size > 0:
        mapping_image_settings = aggregation_pipeline.GetMappingImageSettings()
        mapping_image_settings.SetGenerateMappingImage(True)
        material_settings = mapping_image_settings.GetOutputMaterialSettings(0)
        material_settings.SetTextureWidth(texture_size)
        material_settings.SetTextureHeight(texture_size)
    return aggregation_pipeline


'''
Will apply the provided pipelines to provided asset.
The result will be returned in a new asset information object.
If a clipping pattern is provided the asset will be clipped with that geometry.
'''
def run_pipeline(sg: Simplygon.ISimplygon, pipeline: Simplygon.spPipeline, asset_information: AssetInformation, output_folder: str = None) -> AssetInformation:
    # Load the asset we want to process
    print(f'Processing {asset_information.name} with {pipeline.GetName()}')
    scene = asset_information.scene.NewCopy()
    mapping_image_settings = pipeline.GetMappingImageSettings()
    # If generate mapping image is set on the pipeline we'll assume that material casters should be added.
    if mapping_image_settings.GetGenerateMappingImage():
        setup_material_casters(sg, pipeline, scene)

    (clipping_set_id,process_set_id) = split_into_clipping_and_processing(sg, scene, '*terrain*')
    # With the name of the pipeline we can access the different settings objects neatly in python.
    pipe_type = pipeline.GetClass()[1:-8]
    pipe_settings = eval(f'Simplygon.sp{pipe_type}Pipeline.SafeCast(pipeline).Get{pipe_type}Settings()')
    # Reduction, for example, doesn't have geometry culling.
    if 'GetGeometryCullingSettings' in dir(pipeline):
        culling_settings = pipeline.GetGeometryCullingSettings()
        culling_settings.SetUseClippingGeometry(True)
        culling_settings.SetClippingGeometrySelectionSetID(clipping_set_id)
    else:
        # We're keeping the terrain in the scene if we're not culling with it.
        pipe_settings.SetKeepUnprocessedSceneMeshes(True)

    # Process set ID is part of the pipeline specific settings. This is an dirty way to do it and does not work for all
    # pipeline types. But it does cover reduction, remeshing and aggregation.
    pipe_settings.SetProcessSelectionSetID(process_set_id)
    pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
    # Save the results if the output path is specified
    if output_folder != None:
        export_scene(sg, scene, os.path.join(output_folder, f'{pipeline.GetName()}.fbx'))

    return AssetInformation(pipeline.GetName(), output_folder, scene)


'''
Use this function to create an array of different proxies you want to evalute. The resulting proxies will be 
returned in the processing results for later scrutiny
'''
def create_asset_proxies(sg: Simplygon.ISimplygon, processing_results: ProcessingResult, output_folder: str)-> None:
    # Output all the proxies into a folder in the output folder.
    proxy_folder = os.path.join(output_folder, processing_results.source_info.name)
    # Let's first create a remeshed proxy
    pipeline = create_remeshing_pipeline(sg, "Remeshed", 300, 1024)
    processing_results.add_proxy_results(run_pipeline(sg, pipeline, processing_results.source_info, proxy_folder))

    # An intermediate reduction that we will aggregate later. We won't store it on disk.
    pipeline = create_reduction_pipeline(sg, "Reduction", 300)
    reduction_info = run_pipeline(sg, pipeline, processing_results.source_info)

    # First an aggregation without material merging on the reduced object
    pipeline = create_aggregaton_pipline(sg, "Aggregation")
    processing_results.add_proxy_results(run_pipeline(sg, pipeline, reduction_info, proxy_folder))

    # Then an aggregation with material merging on the reduced object
    pipeline = create_aggregaton_pipline(sg, "AggregationMergedMats", 512)
    processing_results.add_proxy_results(run_pipeline(sg, pipeline, reduction_info, proxy_folder))

'''
This is the magic function where you decide which proxy works best for you. Things to consider:
* How many polys can we handle for this asset?
* Is object count an issue with this asset?
* Is it worth spending extra memory on new textures to reduce number of materials?
* Bonus: Compare the results automatically in a render.
'''
def decide_on_proxy(results: ProcessingResult) -> AssetInformation:
    # Naively we're returning a random proxy.
    return random.choice(results.proxies)

'''
Use the image comparison feature on images that you capture in your engine in a representative lighting with correct shaders and desired distance.
Read more: https://documentation.simplygon.com/SimplygonSDK_10.1.3300.0/api/reference/tools/imagecomparer.html#properties
The idea would be to use the average error over a large number (20ish) of renders from different angles (that are possible in your game)
This information can be taken into account when deciding on proxy method for a specific asset.  
'''
def caclulcate_visual_fidelity(results: ProcessingResult) -> int:
    # Calculate a metric by comparing the render result beween the source and the proxies at the target distance.
    return 0

'''
There are a lot of libs that can help with nicely formated tables. We don't want to require external deps though.
'''
def print_line(columns:list, column_widths:list) -> None:
    line = ''
    for i in range(0,len(columns)):
        line = line+columns[i].ljust(column_widths[i])+'\t'
    print(line)
'''
There are a lot of libs that can help with nicely formated tables. We don't want to require external deps though.
'''
def output_results(results: ProcessingResult) -> None:
    column_widths = [25,15,15,15]
    print(str().ljust(sum(column_widths),'-'))
    print_line(['Name', 'MeshCount', 'PolyCount', 'MaterialCount'], column_widths)
    src = results.source_info
    print_line([src.name, str(src.mesh_count), str(src.poly_count), str(src.material_count)], column_widths)
    for proxy in results.proxies:
        print_line([proxy.name, str(proxy.mesh_count), str(proxy.poly_count), str(proxy.material_count)], column_widths)
    print(str().ljust(sum(column_widths),'-'))

'''
Creates proxies for all assets in the input folder and outputs them into the output folder.
'''
def process_assets(sg: Simplygon.ISimplygon, input_folder:str, output_folder: str) -> None:
    files = glob.glob(f'{input_folder}/*.fbx')
    for f in files:
        file_name = os.path.basename(f)
        asset_name = file_name[:file_name.find(".")]
        scene = import_scene(sg, f)
        processing_results = ProcessingResult(asset_name, f, scene)
        create_asset_proxies(sg, processing_results, output_folder)
        output_results(processing_results)
        print(f'The {decide_on_proxy(processing_results).name} is the best proxy for this object')

def main():
    sg = simplygon_loader.init_simplygon()
    output_folder = "output"
    if os.path.isdir(output_folder):
        shutil.rmtree(output_folder)
    process_assets(sg, "input", output_folder)
    del sg

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

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*