Skip to main content

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.


caution

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.

PresetUse ForWrap TextureHeight IterationsRadius PixelsHeight ScaleAnglesTapsNormal Edge Guard
UV Atlas / UnwrappedPacked mesh UVs, hard UV seams, non-tileable propsfalse6468462
Tileable MaterialSeamless surfaces, trim sheets, tiling material normalstrue6424164120

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

FieldRecommended DefaultDescription
HeightIterations64Jacobi 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.
NumAngles4Number of GTAO search directions per pixel. 2 is faster but more directional; 4 is the recommended default; 8+ is usually not worth the cost.
NumTaps6 atlas / 12 tileableNumber of horizon samples per direction. Lower values reduce seam crossing on atlases; higher values improve broader occlusion on tileables.
RadiusPixels6 atlas / 24 tileableSearch radius in texture pixels. Small values are safer for packed UVs. Larger values capture broader cavities on continuous/tileable textures.
HeightScale8 atlas / 16 tileableScales reconstructed height during AO search. Raise for stronger cavity contrast; lower if AO looks dirty, crushed, or seam-prone.
NormalEdgeGuardStrength2 atlas / 0 tileableDetects sharp slope discontinuities and fades them toward white visibility. Useful for UV seam artifacts. Leave disabled for clean tileable materials.
bWrapTexturefalse atlas / true tileableWraps texture-border sampling. Enable only for seamless tileables. It does not protect internal UV island borders.
bFlipGreenChannelfalseFlips normal-map Y before slope reconstruction. Use when source normal convention is inverted.
bApplyAlbedoInterreflectionfalseUses 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.

FieldDefaultDescription
NormalEdgeGuardStrength0.0Detects 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 / unoccluded
  • 0.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 RadiusPixels low on atlases.
  • Keep HeightScale moderate on atlases.
  • Use NormalEdgeGuardStrength to fade seam artifacts toward white visibility.
  • Use Tileable Material preset only for seamless/tileable maps.
  • Do not enable Wrap Texture for UV atlases.

caution bWrapTexture=false only prevents wrapping at the outer texture border. It does not detect or protect internal UV island borders.

  • 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 HeightScale first.
  • Tune RadiusPixels second.
  • Increase NumTaps only after radius and scale are correct.
  • Use bFlipGreenChannel only 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.

Sources