Visibility culling and weighting with sphere and hemisphere
Written by Jesper Tingvall, Product Expert, Simplygon
Disclaimer: The code in this post is written using version 10.4.117.0 of Simplygon and 3ds Max 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
This blog covers visibility sphere and hemisphere guided optimization. We'll cover visibility culling in aggregation and visibility weights in reduction.
Prerequisites
This example will use Simplygon's 3ds Max plugin, but the same concepts can be applied to all other integrations of the Simplygon API.
Aggregation with visibility culling sphere
Visibility culling can be used with the aggregation pipeline. A good use case for this is to optimize the LOD0 model by removing any surface not visible from the players point of view. In this example we have a gothic cabinet filled with goblets.
There are some goblets hiding inside it that we can see if we look at the wireframe.
As you can see from the images, the goblets in the lower part of the cabinet will never be visible. On the top only some are partially visible. We are going to use culling to remove all invisible surfaces.
We start by adding an aggregation pipeline (Template → Advanced → Aggregation). Here we are going to set the followings settings.
- With VisibilityCameraMode we can specify what shape our visibility geometry should be. We set this to Sphere.
- Setting CullOccludedGeometry to true will remove all triangles not visible from our camera sphere.
- With SphereFidelity we can specify the number of cameras to use in the camera sphere. If we see that triangles that ought to be visible in extreme angles are removed increasing this can help. Enabling ConservativeMode can also address this issue.
Scripted aggregation with visibility culling
It is also possible to do aggregation with visibility culling with Python scripting in 3ds Max. Here is a script snipped that creates an aggregation pipeline with same settings as above.
def aggregate_with_visibility_culling(sg: Simplygon.ISimplygon, camera_fidelity: int, scene: Simplygon.spScene):
pipeline = sg.CreateAggregationPipeline()
visibility_settings = pipeline.GetVisibilitySettings()
visibility_settings.SetCullOccludedGeometry(True)
# Sphere settings
visibility_settings.SetVisibilityCameraMode(Simplygon.EVisibilityCameraMode_Sphere)
visibility_settings.SetSphereFidelity(camera_fidelity)
pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
Result
After optimization we get this output. We can see that all goblets in the lower parts are removed, since those are not visible from outside at all. On the top parts only the visible parts of the goblets are kept.


Reduction with visibility weighting hemisphere
We will now showcase how visibility can be used to guide a reduction pipeline. Our intended use case is that we want to generate LOD levels with reducer for a top down focused game. We expect that when we are looking at the assets from far then our camera is viewing them from top. So we want to keep more details visible from above.
We start by adding an reduction pipeline (Template → Advanced → Reduction). Here we are going to set the followings settings.
- By setting UseVisibilityWeightsInReducer to true our reducer will use visibility information during reduction.
- With VisibilityCameraMode we can specify what shape our visibility geometry should be. We set this to Hemisphere.
- With Hemisphere we also need to specify HemisphereUpVector to match our coordinate system.
- The coverage angle is set by HemisphereCoverageAngle. We set this to 90 degrees based on that we expect the camera to max be tilted 45 degrees in any direction.
- CullOccludedGeometry will be set to false. We do not want to remove invisible geometry, just make it spend less triangles there.
Scripted reduction with visibility weights
It is also possible to use reduction with visibility weights through scripting.
def reduce_with_visibility_culling_weights(sg: Simplygon.ISimplygon, reduction_ratio: float, coverage_angle: float, camera_fidelity: int, scene: Simplygon.spScene):
pipeline = sg.CreateReductionPipeline()
pipeline.GetReductionSettings().SetReductionTargetTriangleRatio(reduction_ratio)
visibility_settings = pipeline.GetVisibilitySettings()
visibility_settings.SetUseVisibilityWeightsInReducer(True)
# Hemisphere settings
visibility_settings.SetVisibilityCameraMode(Simplygon.EVisibilityCameraMode_Hemisphere)
visibility_settings.SetHemisphereUpVector(Simplygon.EUpVector_PositiveZ) # Up vector in 3ds Max
visibility_settings.SetHemisphereCoverageAngle(coverage_angle)
visibility_settings.SetHemisphereFidelity(camera_fidelity)
pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
Result
Here is our 50% reduced asset after optimization.


Let us compare to a standard 50% reduction. We can see that the top surfaces of our asset has more triangles, while the inside area and bottom that we are not seeing from above has less. This allows us to spend our triangle budget more wise we we know that the assets will only be seen from certain directions.


Complete script
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
from simplygon10 import simplygon_loader
from simplygon10 import Simplygon
from pymxs import runtime as rt
export_path = 'C:/Temp/ExportedScene.sb'
processed_path = 'C:/Temp/ProcessedScene.sb'
def aggregate_with_visibility_culling(sg: Simplygon.ISimplygon, camera_fidelity: int, scene: Simplygon.spScene):
pipeline = sg.CreateAggregationPipeline()
visibility_settings = pipeline.GetVisibilitySettings()
visibility_settings.SetCullOccludedGeometry(True)
# Sphere settings
visibility_settings.SetVisibilityCameraMode(Simplygon.EVisibilityCameraMode_Sphere)
visibility_settings.SetSphereFidelity(camera_fidelity)
pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
def reduce_with_visibility_culling_weights(sg: Simplygon.ISimplygon, reduction_ratio: float, coverage_angle: float, camera_fidelity: int, scene: Simplygon.spScene):
pipeline = sg.CreateReductionPipeline()
pipeline.GetReductionSettings().SetReductionTargetTriangleRatio(reduction_ratio)
visibility_settings = pipeline.GetVisibilitySettings()
visibility_settings.SetUseVisibilityWeightsInReducer(True)
# Hemisphere settings
visibility_settings.SetVisibilityCameraMode(Simplygon.EVisibilityCameraMode_Hemisphere)
visibility_settings.SetHemisphereUpVector(Simplygon.EUpVector_PositiveZ) # Up vector in 3ds Max
visibility_settings.SetHemisphereCoverageAngle(coverage_angle)
visibility_settings.SetHemisphereFidelity(camera_fidelity)
pipeline.RunScene(scene, Simplygon.EPipelineRunMode_RunInThisProcess)
def export_selection_from_max(sg: Simplygon.ISimplygon, temp_path: str) -> Simplygon.spScene:
if not rt.sgsdk_ExportToFile(temp_path, False):
return None
scene_importer = sg.CreateSceneImporter()
scene_importer.SetImportFilePath(temp_path)
if scene_importer.Run() == Simplygon.EErrorCodes_NoError:
return scene_importer.GetScene()
return None
def import_to_max(sg: Simplygon.ISimplygon, scene: Simplygon.spScene, temp_path: str):
scene_exporter = sg.CreateSceneExporter()
scene_exporter.SetExportFilePath(temp_path)
scene_exporter.SetScene(scene)
scene_exporter.Run()
rt.sgsdk_ImportFromFile(processed_path, True, True, True)
def main():
sg = simplygon_loader.init_simplygon()
sg.SetGlobalDefaultTangentCalculatorTypeSetting(Simplygon.ETangentSpaceMethod_Autodesk3dsMax)
scene = export_selection_from_max(sg, export_path)
# aggregate_with_visibility_culling(sg, 6, scene)
reduce_with_visibility_culling_weights(sg, 0.5, 90, 6, scene)
import_to_max(sg, scene, processed_path)
del sg
if __name__== "__main__":
main()