Visibility culling through generated cameras in Max

Disclaimer: The code in this post is written on version 9.1 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

There are often cases when there is limited visibility in a scene, for example when a character is walking amongst tall buildings or when driving around in a car on a race track. Geometry might be partially or fully occluded by other objects, is backfacing or just not visible from all the possible locations of the character. Unnecessary geometry and possibly unused textures may have a negative impact on performance in terms of computing, memory and storage. To solve these problems we can utilize Simplygon's visibility feature.

Cull non-visible geometry using a visibility volume

If we take this city block as an example.

City block

Let's say that all possible locations for a character is limited to the space at the center of these buildings. From this area the character will not be able to see any backfacing or blocked geometry, some of the inside, the outer walls and parts of the roof will not be visible. To utilize Simplygon's visibility feature we need to tell Simplygon from which points the scene can be viewed. This can be achieved by setting up cameras or by simply setting up a visibility volume from which Simplygon will generate omni-directional cameras from.

Visibility volume

This specific case fits perfectly for the usage of visibility volumes, so let's create a mesh that covers the allowed area!

The amount of generated cameras from the visibility volume is determined by the number of vertices in the geometry, in this case we need to make sure that there are points evenly distributed over the geometry. Too many points may result in longer optimization time, so starting off with a reasonable amount is prefered, then increase the tesselation in case there are visible areas that did not get preserved.

Visibility volume in green
Visibility volume placed in the scene

The next step is to create a selection set that contains the visibility volume. Simply select the volume, then go to the text field next to "Manage Selection Sets", enter the name for the set and press enter. We'll use the name "visibilityvolume" in this example.

Create selection set

Optimization settings

Now that the scene is prepared with the visibility volume, let's create a Reduction pipeline and assign the correct settings.

from pymxs import runtime as rt

# create a Reduction Pipeline object
reductionPipeline = rt.sgsdk_CreatePipeline('ReductionPipeline')

# set DefaultTangentCalculatorType to Autodesk3dsMax (1)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'GlobalSettings/DefaultTangentCalculatorType', 1)

# set the triangle ratio to 50%
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetTriangleRatio', 0.5)

# enable triangle ratio as reduction target
# disable triangle count reduction target
# disable max deviation reduction target
# disable onscreensize reduction target
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetTriangleRatioEnabled', True)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetTriangleCountEnabled', False)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetMaxDeviationEnabled', False)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetOnScreenSizeEnabled', False)

# visibility settings
# enable culling of occluded geometry
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/VisibilitySettings/CullOccludedGeometry', True)

# enable backface culling
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/VisibilitySettings/UseBackfaceCulling', True)

# assign the visibility volume selection set
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/VisibilitySettings/CameraSelectionSetName', 'visibilityvolume')

# more settings can be set for the Reduction pipeline,
# see Pipeline documentation and/or inspect the generated file
# that is saved out by this script!

If we want to use material baking we need to enable mapping image settings as well as add some material casters.

# set bMaterialBake to True to enable material baking,
# if material baking is enabled Simplygon will generate
# a new material (with textures) shared by all the optimized meshes.
bMaterialBake = True
if bMaterialBake:
    # enable material baking
    # mapping image is required for material baking
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/GenerateMappingImage', True)

    # in this case we want to generate texture coordinates (UVs)
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/GenerateTexCoords', True)

    # the name of the resulting texture coordinate field
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/TexCoordName', 'MaterialLOD')

    # width of the baked textures
    # height of the baked textures
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/Output0/TextureWidth', 1024)
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/Output0/TextureHeight', 1024)

    # add material casters
    bCasterAdded = rt.sgsdk_AddMaterialCaster(reductionPipeline, 'ColorCaster')
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/0/ColorCasterSettings/MaterialChannel', 'base_weight')

    bCasterAdded = rt.sgsdk_AddMaterialCaster(reductionPipeline, 'ColorCaster')
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/1/ColorCasterSettings/MaterialChannel', 'base_color')

    bCasterAdded = rt.sgsdk_AddMaterialCaster(reductionPipeline, 'NormalCaster')
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/2/NormalCasterSettings/MaterialChannel', 'bump')

    # set the correct tangent space type,
    # in this example we use tangent space normals
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/2/NormalCasterSettings/GenerateTangentSpaceNormals', True)

Run the optimization

To be able to execute Simplygon we need to select at least one mesh in the scene, in this example we will select the entire city block and then exclude the visibility volume. After the selection we can start executing Simplygon with the given settings. When the optimization has completed and the optimized objects have been returned to Max we can query the plug-in for the optimized mesh names and perform operations as needed. In this case we will just offset the meshes for demonstration purposes. At the end of the script we should clear out all pipeline objects that reside in memory (to avoid them stacking up after multiple runs).

# select all objects in scene
rt.select(rt.objects)

# deselect visibility volume (name of mesh)
rt.deselect(rt.getNodeByName('camera_volume'))

# execute pipeline on selection,
# returns result to Max once completed
rt.sgsdk_RunPipelineOnSelection(reductionPipeline)

# get the names of the optimized meshes
processedMeshes = rt.sgsdk_GetProcessedMeshes()

# for each optimized mesh, move them a bit aside
for meshName in processedMeshes:
    o = rt.getNodeByName(meshName)
    offset = rt.Point3(0, 5000, 0)
    rt.move(o, offset)
    
# clear all pipelines
rt.sgsdk_ClearPipelines()

We are now ready to run the script, press CTRL + E (or go to Tools → Evaluate All).

Result

Let's take a look at the result!

If we take a look at the optimized asset below we can see that most of the occluded geometry (from the visibility volume's perspective) has been removed; the outer walls, some internal geometry as well as some parts of the roof. As we enabled material baking in our settings there is only one material that is shared between all optimized geometries (SimplygonCastMaterial). We can see that the three material channels (base_weight, base_color and bump) got mapped back properly with one 1024x1024 texture per channel (total of three). When material baking is enabled there is only one uv-channel per optimized mesh, as specified in the settings it is named "MaterialLOD".

Here are some geometric comparison stats for this specific asset.

Asset Objects Triangles Vertices
Original 119 136 430 89 624
Optimized 63 38 119 31 506
Optimized asset to the left, original to the right

Here's a comparison shot in wireframe (top-down). If we look closer at the buildings we can see that most non-visible geometry (from the visibility volume's perspective) has been removed.

Optimized asset to the left, original to the right

Complete script

from pymxs import runtime as rt

# create a Reduction Pipeline object
reductionPipeline = rt.sgsdk_CreatePipeline('ReductionPipeline')

# set DefaultTangentCalculatorType to Autodesk3dsMax (1)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'GlobalSettings/DefaultTangentCalculatorType', 1)

# set the triangle ratio to 50%
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetTriangleRatio', 0.5)

# enable triangle ratio as reduction target
# disable triangle count reduction target
# disable max deviation reduction target
# disable onscreensize reduction target
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetTriangleRatioEnabled', True)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetTriangleCountEnabled', False)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetMaxDeviationEnabled', False)
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/ReductionSettings/ReductionTargetOnScreenSizeEnabled', False)

# visibility settings
# enable culling of occluded geometry
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/VisibilitySettings/CullOccludedGeometry', True)

# enable backface culling
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/VisibilitySettings/UseBackfaceCulling', True)

# assign the visibility volume selection set
bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/VisibilitySettings/CameraSelectionSetName', 'visibilityvolume')

# more settings can be set for the Reduction pipeline,
# see Pipeline documentation and/or inspect the generated file
# that is saved out by this script!

# set bMaterialBake to True to enable material baking,
# if material baking is enabled Simplygon will generate
# a new material (with textures) shared by all the optimized meshes.
bMaterialBake = True
if bMaterialBake:
    # enable material baking
    # mapping image is required for material baking
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/GenerateMappingImage', True)

    # in this case we want to generate texture coordinates (UVs)
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/GenerateTexCoords', True)

    # the name of the resulting texture coordinate field
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/TexCoordName', 'MaterialLOD')

    # width of the baked textures
    # height of the baked textures
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/Output0/TextureWidth', 1024)
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'ReductionProcessor/MappingImageSettings/Output0/TextureHeight', 1024)

    # add material casters
    bCasterAdded = rt.sgsdk_AddMaterialCaster(reductionPipeline, 'ColorCaster')
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/0/ColorCasterSettings/MaterialChannel', 'base_weight')

    bCasterAdded = rt.sgsdk_AddMaterialCaster(reductionPipeline, 'ColorCaster')
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/1/ColorCasterSettings/MaterialChannel', 'base_color')

    bCasterAdded = rt.sgsdk_AddMaterialCaster(reductionPipeline, 'NormalCaster')
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/2/NormalCasterSettings/MaterialChannel', 'bump')

    # set the correct tangent space type,
    # in this example we use tangent space normals
    bResult = rt.sgsdk_SetSetting(reductionPipeline, 'MaterialCaster/2/NormalCasterSettings/GenerateTangentSpaceNormals', True)

# select all objects in scene
rt.select(rt.objects)

# deselect visibility volume (name of mesh)
rt.deselect(rt.getNodeByName('camera_volume'))

# execute pipeline on selection,
# returns result to Max once completed
rt.sgsdk_RunPipelineOnSelection(reductionPipeline)

# get the names of the optimized meshes
processedMeshes = rt.sgsdk_GetProcessedMeshes()

# for each optimized mesh, move them a bit aside
for meshName in processedMeshes:
    o = rt.getNodeByName(meshName)
    offset = rt.Point3(0, 5000, 0)
    rt.move(o, offset)
    
# clear all pipelines
rt.sgsdk_ClearPipelines()
⇐ Back to blog post list