Weighted Vertex Normals
Introduction
At some point in a 3d graphics pipeline, vertex normal vectors need to be computed (either real-time or design-time) to achieve proper lighting of curved surfaces. There are a few ways to do this, however, the most commonly used method has significant flaws. In this article I will address two of those problems, and propose a practical and robust solution.The assumption is made that the reader is familiar with surface normal vectors and what they are used for.
Also assumed is that the geometry may have 'hard edges', for which multiple normals may lie at the same vertex. This is for completeness sake only, it is not a requirement, nor do the proposed enhancements affect have any influence on the appearance of hard edges.
The included code uses per-polygon smoothing-groups (most popular in 3d authoring software) to achieve hard edges, though this can be replaced/combined by algorithmic criteria (e.g. angle between triangles greater than 90 degrees etc).
Hard edges can also be created by duplicating vertices on hard edges (most optimal for rendering on today's graphics hardware), but for simplicity we'll assume there are no duplicate vertices.
Also note that the pseudo-code presented herein is far from optimal, and can be optimized in many ways.
Terminology
| face | A triangle consisting of three vertices. |
| polygon | A planar surface consisting of three or more vertices. |
| facet normal | The normal vector of the plane in which a face or polygon lies. |
| vertex normal | A normal at one of the three vertices of a face. There may be more than one vertex normal per vertex (hard edges). |
The Problem
When vertex normals are generated, it generally goes like this:
for each face A in mesh
{
n = face A facet normal
// loop through all 3 vertices
for each vert in face A
(
for each face B in mesh
{
if A != B { // ignore self
if face A and B smoothing groups match { // criteria for hard-edges
// accumulate normal
if faces share at least one vert {
n += face B facet normal
}
}
}
}
// normalize vertex normal
vn = normalize(n)
}
}
In English: vertex v will have a normal n which is the average of the combined facet normals of all connected polygons.
In most situations this will look fine. But consider the situation below:
In this case the triangles that make up the thin beveled edges of the box will 'claim' much of the normal orientation. This causes the 'rounded' shading on the large flat sides of the box (problem #1).
This is then made worse because two corners of those large sides contribute 2x the facet normal (2 triangles touch the vertex), while the other two corners contribute only 1x (one triangle touches the vertex). This results into the diagonal artifact when shaded (problem #2).
A poor man's solution would be to simply align the normals to the axii of the faces when such geometry is generated (difficult to preserve) and/or let the artist correct it by hand (waste of time). However, we'll be looking for a generic solution that works for arbitrary geometry constructed from triangles (known as 'triangle soup') and n-sided polygons alike.
Weighting By Surface Area
The solution is to determine the influence of each face in it's contribution to the vertex normal. The obvious way to do that is by using the surface area of each face as 'weight'. Small polygons will have little influence, large polygons have large influence.
for each face A in mesh
{
n = face A facet normal
// loop through all 3 vertices
for each vert in face A
{
for each face B in mesh
{
if A != B { // ignore self
if face A and B smoothing groups match { // criteria for hard-edges
// accumulate normal
if faces share at least one vert {
n += (face b facet normal) * face B surface area // multiply by area
}
}
}
}
// normalize vertex normal
vn = normalize(n)
}
}
As you can see, we simply multiply the facet normal by the triangle area when we accumulate it.
Since we are already normalizing the resulting vector, we don't have to do anything else. Behold:
That looks much more pleasing. The beveled edges do actually still have have a slight influence over the larger sides of the box, but this is hardly noticeable in most situations (and in other cases even desirable).
Weighting By Angle
While the above technique does fix the most visible problems, there's another issue that is worthwhile to consider. This problem is most noticeable on cylindrical shapes:
It may be hard to spot, but if you look closely you will notice an artifact running diagonally from vertex A to vertex D. The three faces that influence vertex A, are faces t, u and v. Though both faces U and V belong to the same polygon (UV), the facet normal of that polygon is essentially contributed twice. In this case that causes the combined vertex normal A to point slightly to the right.
At vertex C the opposite happens, there st pulls the normal to the left. The result being that the two vertex normals are pointing in different directions while they should be parallel.
We could figure out which faces lie in the same plane and simply don't accumulate coincident facet normals, but this works only in a limited number of cases.
A robust solution is to calculate the angle of the corners of the polygons, and use that as weight (just like surface area). In example of the figure above, at vertex A, the combined angles of the two corners of faces u and v will equal the corner angle of face t.
In pseudo code, that becomes:
for each face A in mesh
{
n = face A facet normal
// loop through all 3 vertices
for each vert in face A
{
for each face B in mesh
{
if A != B { // ignore self
if face A and B smoothing groups match { // criteria for hard-edges
// accumulate normal
// v1, v2, v3 are the vertices of face a
if face B shares v1 {
angle = angle_between_vectors( v1 - v2 , v1 - v3 )
n += (face B facet normal * (face B surface area) * angle // multiply by angle
}
if face B shares v2 {
angle = angle_between_vectors( v2 - v1 , v2 - v3 )
n += (face B facet normal) * (face B surface area) * angle // multiply by angle
}
if face B shares v3 {
angle = angle_between_vectors( v3 - v1 , v3 - v2 )
n += (face B facet normal) * (face B surface area) * angle // multiply by angle
}
}
}
}
// normalize vertex normal
vn = normalize(n)
}
}
Here, angle is the angle in radians (or degrees) between the two vectors of the two line segments that touch
each of the three vertices in a face.
Conclusion
Weighted vertex normals improve the appearance of virtually all geometry, and is generally superior to the traditional non-weighted average. Since vertex normal generation is most often a design-time process, there no impact on performance, unless the normals are re-calculated from scratch in realtime.Even though tangent space normal mapping is widely used nowadays, tangent vector computation still requires vertex normals, and here these enhancements also improve the visual quality. For best quality, tangent and bi-tangent vectors can be weighted similarly.
Additionally, weighted vertex normals also allow for smooth shaded beveled edges without significantly increasing the polygon count, and can greatly reduce distortions of specular reflection and environment mapped reflective/refractive surfaces.
Page created: 2007-12-23