Normal To AO
Demos:
uexNormalToAO converts tangent-space normal maps into baked grayscale visibility maps using a GPU-accelerated approximation of the material-surface occlusion workflow described by Activision for Call of Duty: WWII.
The plugin reconstructs a height field from the normal map, then runs an offline orthographic GTAO-style visibility bake over that height field. It reuses UE5 GTAO horizon/integral math where practical, but it is not a byte-exact port of Activision's internal tool or UE5's scene-depth GTAO pass.
Activision's technique is more than a texture baker. The baked map stores surface visibility, but their full material-surface occlusion model also affects diffuse interreflection, direct micro-shadowing, and indirect specular occlusion.
This plugin currently bakes the texture-side visibility result. It does not modify UE5 lighting shaders for CoD-style direct micro-shadowing or specular cone occlusion.
Material Advances in Call of Duty: WWII
Architecture
The converter has two parts:
The converter has two parts:
FuexNormalToAOProcessor - Processing API and CPU fallback/reference path.
uexNormalToAO GPU compute path - Standalone UE5 compute-shader implementation used for the main bake. It computes the slope field, height relaxation, and GTAO-style visibility on the GPU, then reads the grayscale result back for UE asset creation.
FuexNormalToAOModule - Editor UI shell, preset handling, texture selection, batch generation, and asset creation.
Algorithm
1. Normal To Slopes
The normal map is decoded into tangent-space normals, then converted into height derivatives:
Dx = -Normal.X / Normal.Z;
Dy = -Normal.Y / Normal.Z;
These slopes are the constraints used by height reconstruction.
2. Coarse-To-Fine Height Reconstruction
The plugin builds a mip chain of slope fields, solves the lowest mip first, upsamples that height field, then relaxes the next finer mip. This matches the structure described by Activision more closely than brute-forcing only the full-resolution texture.
The output height texture is normalized for preview/export; it is not an absolute physical height map.
3. Orthographic GTAO Visibility
The generated height field is treated as an orthographic depth surface. For each pixel, the plugin searches horizon angles in paired directions, then evaluates a GTAO inner integral against the normal-derived slope normal.
The implementation uses UE5-style GTAO sampling concepts and inner-integral math, but removes scene-depth-specific pieces such as HZB traversal, temporal filtering, camera projection, and screen fade.
4. Optional Albedo Interreflection
Raw visibility can look grey because shallow surface detail genuinely reduces hemispherical visibility. Activision accounts for material interreflection so high-albedo surfaces do not become unrealistically dark.
The plugin can optionally bake an albedo-compensated approximation into the output texture. This is useful for UE material AO workflows, but it is not the same as applying the full CoD lighting model at shading time.
Presets
The plugin provides two starting presets. Presets are only starting points; material scale, normal-map strength, and UV layout still matter.
| Preset | Use For | Wrap Texture | Height Iterations | Radius Pixels | Height Scale | Angles | Taps | Normal Edge Guard |
|---|---|---|---|---|---|---|---|---|
| UV Atlas / Unwrapped | Packed mesh UVs, hard UV seams, non-tileable props | false | 64 | 6 | 8 | 4 | 6 | 2 |
| Tileable Material | Seamless surfaces, trim sheets, tiling material normals | true | 64 | 24 | 16 | 4 | 12 | 0 |
UV Atlas / Unwrapped
Use this for packed UV layouts where unrelated islands sit next to each other in texture space. The preset uses a short search radius and fewer taps to reduce cross-island contamination. Normal Edge Guard is enabled to fade sharp seam artifacts toward white visibility.
This preset is conservative. It avoids many seam artifacts, but it cannot make a UV atlas behave like a physically continuous heightfield.
Tileable Material
Use this for seamless/tileable normal maps where neighboring pixels represent neighboring surface points. The preset enables wrapping and uses a larger radius/tap count, which gives better broad cavity response.
Do not use this preset on packed UV atlases unless the texture is intentionally laid out as a continuous field.
Settings
| Field | Recommended Default | Description |
|---|---|---|
HeightIterations | 64 | Jacobi relaxation iterations per mip level. This is a convergence knob, not a texture-resolution value. Higher values can improve smooth convergence but cost more GPU work and can worsen atlas seam bleeding. |
NumAngles | 4 | Number of GTAO search directions per pixel. 2 is faster but more directional; 4 is the recommended default; 8+ is usually not worth the cost. |
NumTaps | 6 atlas / 12 tileable | Number of horizon samples per direction. Lower values reduce seam crossing on atlases; higher values improve broader occlusion on tileables. |
RadiusPixels | 6 atlas / 24 tileable | Search radius in texture pixels. Small values are safer for packed UVs. Larger values capture broader cavities on continuous/tileable textures. |
HeightScale | 8 atlas / 16 tileable | Scales reconstructed height during AO search. Raise for stronger cavity contrast; lower if AO looks dirty, crushed, or seam-prone. |
NormalEdgeGuardStrength | 2 atlas / 0 tileable | Detects sharp slope discontinuities and fades them toward white visibility. Useful for UV seam artifacts. Leave disabled for clean tileable materials. |
bWrapTexture | false atlas / true tileable | Wraps texture-border sampling. Enable only for seamless tileables. It does not protect internal UV island borders. |
bFlipGreenChannel | false | Flips normal-map Y before slope reconstruction. Use when source normal convention is inverted. |
bApplyAlbedoInterreflection | false | Uses the selected albedo map to brighten visibility based on diffuse reflectance. This is an artist-facing approximation, not the full CoD lighting model. |
Artifact Mitigation
Atlas textures can produce dark dots, seams, or edge halos when the normal map contains abrupt UV-island borders. Issues are usualy caused by discontinuous normal/height data inside one texture.
The plugin provides cleanup controls for this case.
| Field | Default | Description |
|---|---|---|
NormalEdgeGuardStrength | 0.0 | Detects sharp normal/slope discontinuities and fades those pixels toward white visibility. Useful for black dots or seams around internal packed UV islands. |
Texture Inputs
Normal Map
Required. This is the source tangent-space normal map.
Albedo Map
Optional. The plugin can auto-detect an albedo texture in the same folder using common suffixes such as:
- _BaseColor
- _Albedo
- _Alb
- _Color
- _BC
- _Diffuse
- _D
If no match is found, the field stays empty for manual selection.
Output
AO Texture
The main output is a grayscale visibility texture:
1.0= fully visible / unoccluded0.0= fully occluded
info
Despite the asset name _AO, the stored value is visibility, not inverted occlusion.
Height Texture Removed
caution Height texture export was removed because normal-to-height reconstruction is unreliable for packed UV atlases. The solver treats the texture as a continuous heightfield, but UV islands are usually discontinuous and separated by gutters. This can create smooth blobs, pits, or large gradients that do not represent real surface height.
The plugin still reconstructs an internal height field for AO generation, but it no longer exposes that height as a production output.
Artifact Mitigation
Atlas textures can produce dark dots, seams, or edge halos when the normal map contains abrupt UV-island borders. This is expected: the bake operates in texture space, while packed UV islands are not physically continuous.
Recommended fixes:
- Use UV Atlas / Unwrapped preset for packed mesh textures.
- Keep
RadiusPixelslow on atlases. - Keep
HeightScalemoderate on atlases. - Use
NormalEdgeGuardStrengthto fade seam artifacts toward white visibility. - Use Tileable Material preset only for seamless/tileable maps.
- Do not enable
Wrap Texturefor UV atlases.
caution
bWrapTexture=false only prevents wrapping at the outer texture border. It does not detect or protect internal UV island borders.
Recommended Workflow
- Select a normal map.
- Choose the correct preset first:
- UV Atlas / Unwrapped for packed prop/mesh textures.
- Tileable Material for seamless material normals.
- Let the plugin auto-pick the albedo map, or select one manually.
- Leave Apply Albedo Interreflection disabled for raw visibility export.
- Enable Apply Albedo Interreflection only when exporting a softer artist-facing AO map for UE material AO.
- Tune
HeightScalefirst. - Tune
RadiusPixelssecond. - Increase
NumTapsonly after radius and scale are correct. - Use
bFlipGreenChannelonly if the result looks inverted because of normal-map convention mismatch.
Batch Conversion
Multiple normal maps can be converted in a single operation directly from the Content Browser. Select all the normal map textures you want to convert, right-click the selection, and choose Bulk Generate AO From Normals.