dot(CameraViewInv.at, SurfaceNormal) can return > 0.0 even if the the surface is visible. Think of a box whose right side normal is slightly away from the camera - as it's moved to the left of the frustum, it can become visible.
What avoids this phenomenon is dot(VecFromCameraToSurface, SurfaceNormal).
Another way is to check the winding of your screen-space vertices (ie. vertices after model, view and projection transform). This is done by checking the sign of the z component of the cross product of two edges on the screen space triangle (or quad). Code is here (not OpenGL specific):
// culls CW verts, passes CCW verts. Invert the condition for opposite behavior.
bool shouldCullTriangleScreenSpace(vec2 points[3])
{
edgeA = points[1] - points[0];
edgeB = points[2] - points[0];
return (edgeA.x * edgeB.y - edgeA.y * edgeB.x) < 0.0;
}
EDIT: I just wanted to mention an extension to the dot(VecFromCameraToSurface, SurfaceNormal) technique that takes into account post-tessellation extrusion.
The idea is to precompute some metadata for the patch from the range of post-tessellation normals. First generate an average normal for the patch, and figure out the maximum angle of deviation for all normals on the patch from that normal.
This can be done like this at precalculation time:
void getConeAngleForPatch(Patch p, vec3& coneDir, float& cosConeAngle)
{
// compute cone direction (average of all normals in the post-tessellation patch)
vec3 coneDir = 0;
foreach normal N generated by p
coneDir += N
normalize(coneDir);
// calculate the cos of the maximum angle between normals N and coneDir
cosConeAngle = 1.0;
foreach normal N generated by p
cosConeAngle = min(cosConeAngle, dot(N, coneDir));
}
Then your patch backface culling trick in the shader becomes:
dot(UnitVecFromCameraToSurface, coneDir) < -cosConeAngle
...instead of:
dot(UnitVecFromCameraToSurface, SurfaceNormal) < 0