# Bake Ambient Occlusion

> Ambient occlusion (AO) is a rendering technique increasing realism by simulating real-world shadowing. To limit the computational cost in your real time application, you can precompute the self occlusion of an object, and store these values to a dedicated texture.

API functions:

* [algo.beginBakingSession](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#beginbakingsession)
* [algo.endBakingSession](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#endbakingsession)
* [algo.bakeAOMap](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#bakeaomap)
* [algo.filterMeshVertexColors](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#filtermeshvertexcolors)
* [algo.convertNormalMap](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#convertnormalmap)
* [material.filterAO](/asset-transformer-sdk/2026.4/api/python/material_functions.md#filterao)

![AO example](/api/media?file=/asset-transformer-sdk/media/images/ao-shoes-showcase.gif)

## Computation

Similarly to [any other map baking](./bakematerials), ambient occlusion can be baked thanks to the dedicated function [algo.bakeAOMap](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#bakeaomap) called within a session lifecycle:

```python
sessionId = algo.beginBakingSession(destinations, sources, 0, 1024)
aoMaps = algo.bakeAOMap(sessionId, samples = 32)
algo.endBakingSession(sessionId)
```

Occlusions are detected by ray tracing, where the number of rays cast for each texel is given by the parameter `samples`. This is the only map baking function for which the biggest part of the computation load is not held by [algo.beginBakingSession](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#beginbakingsession), but by the function itself, due to these additional rays.

> **Important:**
>
> Note that the set of values accepted by the parameter `samples` are powers of two in the range \[8, 4096]. Any other value will trigger an exception.

Depending on the required map resolution and number of samples, ambient occlusion computation can be time consuming. It is strongly recommended, if possible, [to enable the GPU back-end](./bakematerials#back-ends) for this operation.

Note however that samples are generated so as to ensure a blue noise distribution of the error in texture space, which has pretty nice properties:

* Error is scattered in such a way that it is perceptually less significant,
* It shows good behavior with respect to smoothing (see [Filtering](#filtering)).

This allows to get good quality results, even with quite a few samples.

The figure below shows the impact of blue noise on AO baking results for different sample counts per pixel (spp):

| ![8spp](/api/media?file=/asset-transformer-sdk/media/images/ao-blue-noise08.png) | ![16spp](/api/media?file=/asset-transformer-sdk/media/images/ao-blue-noise16.png) | ![32spp](/api/media?file=/asset-transformer-sdk/media/images/ao-blue-noise32.png) |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| 8 spp                                                                            | 16 spp                                                                            | 32 spp                                                                            |

## Bent normals

The bent normal at a given surface point corresponds to the axis of the cone defined by non-occluded directions. It is used by some rendering engines for advanced shadowing techniques.

![bent normals](/api/media?file=/asset-transformer-sdk/media/images/bent-normal.png)

Bent normals are usually computed contemporaneously with ambient occlusion, thus avoiding additional cost. This is why there is no dedicated function to bake them, but just a flag in the [algo.bakeAOMap](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#bakeaomap) function:

```python
aoMaps = algo.bakeAOMap(sessionId, samples = 32, bentNormals = True)
```

If the flag is enabled, the returned list contains both AO and bent normal maps, stored in an interleaved manner:

* `aoMaps[0]` : first AO map
* `aoMaps[1]` : first bent normal map
* `aoMaps[2]` : second AO map
* `aoMaps[3]` : second bent normal map
* ...

Note that, by default, bent normals are baked in World space. To convert them to Object or Tangent space, please look at the function [algo.convertNormalMap](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#convertnormalmap).

## Filtering

### Filtering AO baked to a texture map

To get a smoother result without requiring to drastically increase the number of samples, the ambient occlusion map can be filtered by using function [material.filterAO](/asset-transformer-sdk/2026.4/api/python/material_functions.md#filterao), which preserves signal discontinuities by combining three Gaussian blurs, respectively depending on:

* texel coordinates (controlled by parameter `sigmaPos`),
* ambient occlusion or bent normal values (controlled by parameter `sigmaValue`),
* normal orientations (controlled by parameter `sigmaNormal`).

The higher the sigma value, the stronger the related blur (see [this paper](https://jo.dreggn.org/home/2010_atrous.pdf) for more details). Note that the third criterion requires a normal map to be provided to the function as well.

[material.filterAO](/asset-transformer-sdk/2026.4/api/python/material_functions.md#filterao) works either on an array of ambient occlusion maps or on an interleaved array of ambient occlusion and bent normal maps, as in the case described above. The code below shows a classic usage of AO filtering:

```python
sessionId = algo.beginBakingSession(destinations, sources, 0, 1024)

# normalMaps = [normalMap[0], ..., normalMap[n-1]]
normalMaps = algo.bakeNormalMap(sessionId)

# aoMaps = [AOMap[0], bentNormalMap[0], ..., AOMap[n-1], bentNormalMap[n-1]]
aoMaps = algo.bakeAOMap(sessionId, samples = 32, bentNormals = True)

algo.endBakingSession(sessionId)

# filteredAO = filtered copy of both ambient occlusion and bent normal maps.
filteredAO = material.filterAO(aoMaps, normalMaps) # apply filter with default values.
```

The following image illustrates the effect of each component of the filter:

| ![NoFilter](/api/media?file=/asset-transformer-sdk/media/images/ao-blue-noise16.png) | ![P](/api/media?file=/asset-transformer-sdk/media/images/ao-filter-p.png) | ![PV](/api/media?file=/asset-transformer-sdk/media/images/ao-filter-pv.png) | ![PVN](/api/media?file=/asset-transformer-sdk/media/images/ao-filter-pvn.png) |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| 16 sppNo filtering                                                                   | `sigmaPos = 2`                                                            | `sigmaPos = 2`, `sigmaValue = 0.2`                                          | `sigmaPos = 2``sigmaValue = 0.2``sigmaNormal = 0.2`                           |

Filtering along positions only is equivalent to applying a Gaussian smoothing to the texture. As expected, most of the details are lost.
Combining it with value filtering makes the result looks like the one of a bilateral filter. Together with normal filtering, it allows to remove noise from the texture while preserving its most salient features.

### Filtering AO baked to vertex colors

When ambient occlusion is baked to mesh vertex colors, the function [algo.filterMeshVertexColors](/asset-transformer-sdk/2026.4/api/python/algo_functions.md#filtermeshvertexcolors) can be used to smooth the result similarly to the process described above: it combines different filters in order to reduce noise in the signal while preserving features.

```python
sessionId = algo.beginVertexBakingSession([destination], sources)
algo.bakeAOMap(sessionId, samples = 128)
algo.endBakingSession(sessionId)

meshes = scene.getPartOccurrences(destination)
algo.filterMeshVertexColors(meshes) # apply filter with default values.
aoValues = algo.getMeshVertexColors(meshes)
```

## Preserving detail with AO-based vertex weights

When decimating meshes, you can use ambient occlusion (AO) baked in vertex colors to guide the decimation process. This technique preserves geometric details in areas with high AO variation (crevices, corners, recesses) while allowing aggressive simplification of other surfaces.

1. **Bake AO to vertex colors** - Compute ambient occlusion and store it in mesh vertex colors
2. **Convert to vertex weights using the `InvertedGrayscale` strategy** - Transform AO values into vertex weights that guide decimation:
   * Dark areas (high occlusion) = high weight = **preserve detail**
   * Bright areas (low occlusion) = low weight = **allow simplification**
3. **Decimate with weights** - The decimation algorithm preserves high-weight vertices and simplifies low-weight areas

This approach produces better visual quality than uniform decimation because it concentrates polygons where geometric detail is most visible.

```python title="Python"
# Bake ambient occlusion into vertex colors
# ...
# ...

# Generate vertex weights from the baked vertex colors
SCALE = 1000 # How important the AO is for the decimation step
algo.createVertexWeightsFromVertexColors([scene.getRoot()], 0, SCALE, algo.VertexWeightStrategy.InvertedGrayscale)

# Reduce to 10% of original triangle count while preserving the previously baked AO
algo.decimateTarget([scene.getRoot()], ["ratio", 10]) 

# Delete vertex weights (cleanup)
algo.deleteVertexWeights([scene.getRoot()])
```
