The half vector H is the microsurface normal
According to the "9.8 BRDF Models for Surface Reflection" of Real-Time Rendering Fourth Edition, [Hammon 2017] proposed a method to calculate the NdotH and LdotH without the knowledge of the half vector H itself.
// |L + V|^2 = L^2 + V^2 + 2L·V = 1 + 1 + 2L·V
// N·H = N·((L + V)/(|L + V|)) = (N·L + N·V)/(|L + V|)
// L·H = L·((L + V)/(|L + V|)) = (L^2 + L·V)/(|L + V|)
// V·H = V·((L + V)/(|L + V|)) = (L·V + V^2)/(|L + V|)
// 2L·H = 2V·H = L·H + V·H = (L^2 + L·V + L·V + V^2)/(|L + V|) = (|L + V|^2)/(|L + V|) = |L + V| = 1 + 1 + 2L·V
// ⇒ L·H = 0.5 * |L + V| = (0.5 * |L + V|^2)/(|L + V|) =(0.5 * (1 + 1 + 2L·V))/(|L + V|) = 1/(|L + V|) + (L·V)/(|L + V|)
// UE: [Init](https://github.com/EpicGames/UnrealEngine/blob/4.27/Engine/Shaders/Private/BRDF.ush#L31)
// U3D: [GetBSDFAngle](https://github.com/Unity-Technologies/Graphics/blob/v10.8.0/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl#L361)
highp float invLenH = inversesqrt(2.0 + 2.0 * NdotL);
highp float NdotH = clamp((NdotL + NdotV) * invLenH, 0.0, 1.0);
highp float LdotH = clamp(invLenH * VdotL + invLenH, 0.0, 1.0);
The NdotH and the LdotH are calculated by GetBSDFAngle in Unity3D and Init in UE4.
By "Equation (9.76)" of Real-Time Rendering Fourth Edition, the NDF by averaging the normal and the roughness separately is NOT the averaging NDF.
NormalCurvatureToRoughness in UE4
TextureNormalVariance in U3D
According to the "9.8.1 Normal Distribution Functions" of Real-Time Rendering Fourth Edition, the GGX should technically be called the Trowbridge-Reitz which is also adopted by the "8.4 Microfacet Models" of PBR Book.
[Heitz 2014]
[Heitz 2014] Eric Heitz. "Understanding the
Masking-Shadowing Function in Microfacet-Based BRDFs." JCGT 2014.
[Hammon 2017] Earl Hammon.
"PBR Diffuse Lighting for GGX+Smith Microsurfaces." GDC 2017.