#version 3.8; global_settings {assumed_gamma 1.0 } //#include "colors.inc" //#include "functions.inc" #include "math.inc" camera { location <10, 5, -20> //location <0, 10, 3> right x*image_width/image_height up y look_at <0, 0, 0> rotate -x*5 } light_source {< 10, 8, -5> rgb 1} // for documentation illustrations light_source {<-10, 8, -5> rgb 0.8} // fill light sky_sphere {pigment {rgb 0.5}} #macro MakeChamferedBox (_Length, _Width, _Height, _Chamfer) #local _L = _Length / 2; #local _W = _Width / 2; #local _H = _Height / 2; #local ChamferX = _L - _Chamfer; #local ChamferY = _H - _Chamfer; #local ChamferZ = _W - _Chamfer; // Top Face #local TopLeftFront = <-ChamferX, _H, -ChamferZ>; #local TopRightFront = < ChamferX, _H, -ChamferZ>; #local TopRightRear = < ChamferX, _H, ChamferZ>; #local TopLeftRear = <-ChamferX, _H, ChamferZ>; #local UpperLeftFrontFace = <-_L, ChamferY, -ChamferZ>; #local UpperLeftFront = <-ChamferX, ChamferY, -_W>; #local UpperRightFront = < ChamferX, ChamferY, -_W>; #local UpperRightFrontFace = < _L, ChamferY, -ChamferZ>; #local UpperRightRearFace = < _L, ChamferY, ChamferZ>; #local UpperRightRear = < ChamferX, ChamferY, _W>; #local UpperLeftRear = <-ChamferX, ChamferY, _W>; #local UpperLeftRearFace = <-_L, ChamferY, ChamferZ>; #local LowerLeftFrontFace = <-_L, -ChamferY, -ChamferZ>; #local LowerLeftFront = <-ChamferX, -ChamferY, -_W>; #local LowerRightFront = < ChamferX, -ChamferY, -_W>; #local LowerRightFrontFace = < _L, -ChamferY, -ChamferZ>; #local LowerRightRearFace = < _L, -ChamferY, ChamferZ>; #local LowerRightRear = < ChamferX, -ChamferY, _W>; #local LowerLeftRear = <-ChamferX, -ChamferY, _W>; #local LowerLeftRearFace = <-_L, -ChamferY, ChamferZ>; #local BottomLeftFront = <-ChamferX, -_H, -ChamferZ>; #local BottomRightFront = < ChamferX, -_H, -ChamferZ>; #local BottomRightRear = < ChamferX, -_H, ChamferZ>; #local BottomLeftRear = <-ChamferX, -_H, ChamferZ>; #local TriangleArray = array [44][3] { // Top Face {TopLeftFront, TopRightFront, TopRightRear}, {TopLeftFront, TopRightRear, TopLeftRear}, // Top-Front Chamfer {UpperLeftFront, UpperRightFront, TopRightFront}, {UpperLeftFront, TopRightFront, TopLeftFront}, // Top-Rear Chamfer {UpperRightRear, UpperLeftRear, TopLeftRear}, {UpperRightRear, TopLeftRear, TopRightRear}, // Front Face {LowerLeftFront, LowerRightFront, UpperRightFront}, {LowerLeftFront, UpperRightFront, UpperLeftFront}, // Bottom Face {BottomLeftFront, BottomRightRear, BottomRightFront}, {BottomLeftFront, BottomLeftRear, BottomRightRear}, // Bottom-Rear Chamfer // Top-Rear Chamfer {LowerRightRear, BottomLeftRear, LowerLeftRear}, {LowerRightRear, BottomRightRear, BottomLeftRear}, // Bottom-Front Chamfer {BottomLeftFront, BottomRightFront, LowerRightFront}, {BottomLeftFront, LowerRightFront, LowerLeftFront}, // Rear Face {LowerLeftRear, UpperRightRear, LowerRightRear}, {LowerLeftRear, UpperLeftRear, UpperRightRear}, // Right face {LowerRightFrontFace, LowerRightRearFace, UpperRightRearFace}, {LowerRightFrontFace, UpperRightRearFace, UpperRightFrontFace}, // Right-Front Chamfer {LowerRightFront, LowerRightFrontFace, UpperRightFront}, {LowerRightFrontFace, UpperRightFrontFace, UpperRightFront}, // Right-Rear Chamfer {LowerRightRearFace, LowerRightRear, UpperRightRearFace}, {LowerRightRear, UpperRightRear, UpperRightRearFace}, // Right-Top Chamfer {UpperRightFrontFace, UpperRightRearFace, TopRightRear}, {UpperRightFrontFace, TopRightRear, TopRightFront}, // Right-Bottom Chamfer {LowerRightFrontFace, BottomRightRear, LowerRightRearFace}, {LowerRightFrontFace, BottomRightFront, BottomRightRear}, // Right Corner Chamfers {UpperRightRear, TopRightRear, UpperRightRearFace}, {UpperRightFrontFace, TopRightFront, UpperRightFront}, {BottomRightRear, LowerRightRear, LowerRightRearFace}, {BottomRightFront, LowerRightFrontFace, LowerRightFront}, // Left-Front Chamfer {LowerLeftFront, UpperLeftFront, LowerLeftFrontFace}, {LowerLeftFrontFace, UpperLeftFront, UpperLeftFrontFace}, // Left-Rear Chamfer {LowerLeftRearFace, UpperLeftRearFace, LowerLeftRear}, {LowerLeftRear, UpperLeftRearFace, UpperLeftRear}, // Left face {LowerLeftFrontFace, UpperLeftRearFace, LowerLeftRearFace}, {LowerLeftFrontFace, UpperLeftFrontFace, UpperLeftRearFace}, // Left-Top Chamfer {UpperLeftFrontFace, TopLeftRear, UpperLeftRearFace}, {UpperLeftFrontFace, TopLeftFront, TopLeftRear}, // Left-Bottom Chamfer {LowerLeftFrontFace, LowerLeftRearFace, BottomLeftRear}, {LowerLeftFrontFace, BottomLeftRear, BottomLeftFront}, // Left Corner Chamfers {UpperLeftRear, UpperLeftRearFace, TopLeftRear}, {UpperLeftFrontFace, UpperLeftFront, TopLeftFront}, {BottomLeftRear, LowerLeftRearFace, LowerLeftRear}, {BottomLeftFront, LowerLeftFront, LowerLeftFrontFace} }; TriangleArray #end //---------------------------------------------------------------------------- #macro FlatChamferedBox (_Array) #local _Triangles = dimension_size (_Array, 1)-1; mesh { #for (T, 0, _Triangles) triangle {_Array [T][0], _Array [T][1], _Array [T][2]} #end } #end //---------------------------------------------------------------------------- #macro CalcNaiveNormals (_Array) #local _Triangles = dimension_size (_Array, 1)-1; #local _VertexNormals = array [_Triangles + 1][3]; #for (T, 0, _Triangles) // for each face A in mesh // n = face A facet normal #local Vec1 = _Array [T][0] - _Array [T][1]; #local Vec2 = _Array [T][2] - _Array [T][1]; #local N = vnormalize (vcross (Vec1, Vec2)); // loop through all vertices of face A // for each vert in face A #for (V, 0, 2) // for each face B in mesh #for (F, 0, _Triangles) // ignore self // if face A == face B then skip #if (F != T) // criteria for hard-edges //if face A and B smoothing groups match //{ // accumulate normal // if faces share at least one vert {n += (face B facet normal)} #if ( VEq (_Array [F][0], _Array [T][0]) | VEq (_Array [F][0], _Array [T][1]) | VEq (_Array [F][0], _Array [T][2]) | VEq (_Array [F][1], _Array [T][1]) | VEq (_Array [F][1], _Array [T][2]) | VEq (_Array [F][2], _Array [T][2]) ) #local Vec1B = _Array [F][0] - _Array [F][1]; #local Vec2B = _Array [F][2] - _Array [F][1]; #local NB = vnormalize (vcross (Vec1B, Vec2B)); #local N = N + NB; #end #end //} #end // end for F // normalize vertex normal #local vn = vnormalize (N); #local _VertexNormals [T][V] = vn; #end // end for V #end // end for T _VertexNormals #end // end macro //---------------------------------------------------------------------------- #macro CalcGoodNormals (_Array) #local _Triangles = dimension_size (_Array, 1)-1; #local _VertexNormals = array [_Triangles + 1][3]; #for (T, 0, _Triangles) // for each face A in mesh //n = face A facet normal #local Vec1 = _Array [T][0] - _Array [T][1]; #local Vec2 = _Array [T][2] - _Array [T][1]; #local N = vcross (Vec1, Vec2); // loop through all vertices in face A #for (V, 0, 2) // for each vert in face A #for (F, 0, _Triangles) // for each face B in mesh // ignore self #if (F != T) // if face A == face B then skip // criteria for hard-edges // if face A and B smoothing groups match { // accumulate normal // if faces share at least one vert {n += (face B facet normal) * (face B surface area) // multiply by area} #if ( VEq (_Array [F][0], _Array [T][0]) | VEq (_Array [F][0], _Array [T][1]) | VEq (_Array [F][0], _Array [T][2]) | VEq (_Array [F][1], _Array [T][1]) | VEq (_Array [F][1], _Array [T][2]) | VEq (_Array [F][2], _Array [T][2]) ) #local Vec1B = _Array [F][0] - _Array [F][1]; #local Vec2B = _Array [F][2] - _Array [F][1]; #local NB = vcross (Vec1B, Vec2B); #local N = N + NB * vlength (NB); #end //} #end // end if #end // end for F // normalize vertex normal #local vn = vnormalize (N); #local _VertexNormals [T][V] = vn; #end // end for V #end // end for T _VertexNormals #end // end macro //---------------------------------------------------------------------------- #macro SmoothChamferedBox (V_array, N_array) #local _Triangles = dimension_size (V_array, 1)-1; union { mesh { #for (T, 0, _Triangles) smooth_triangle {V_array [T][0], N_array [T][0], V_array [T][1], N_array [T][1], V_array [T][2], N_array [T][2] } // end smooth_triangle #end } // end mesh #for (T, 0, _Triangles) #for (V, 0, 2) #local N = N_array [T][V]; #local Vert = V_array [T][V]; cylinder {0, N 0.01 translate Vert pigment {rgb y} no_shadow} #end #end } #end //---------------------------------------------------------------------------- //############################################################################### #declare Triangles = MakeChamferedBox (4, 3, 5, 0.25) object {FlatChamferedBox (Triangles) texture {pigment {rgb (x+y)} finish {specular 0.4}} interior_texture {pigment {rgb x*0.2} finish {emission 1}} translate -x*10 } #declare NN = CalcNaiveNormals (Triangles) object {SmoothChamferedBox (Triangles, NN) texture {pigment {rgb (x+y)} finish {specular 0.4}} interior_texture {pigment {rgb x*0.2} finish {emission 1}} //translate -x*5 } #declare GN = CalcGoodNormals (Triangles) object {SmoothChamferedBox (Triangles, GN) texture {pigment {rgb (x+y)} finish {specular 0.4}} interior_texture {pigment {rgb x*0.2} finish {emission 1}} translate x*10 } /* Introduction When rendering 3D triangle mesh geometry, vertex normal vectors ('normals') need to be computed either in real-time or at 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. This article illustrates two of those problems, and proposes a practical and robust solution. The assumption is made that the reader is familiar with surface normal vectors and their application in 3D graphics rendering. Notes Geometry may have 'hard edges' (sometimes called 'sharp edges'), in which case multiple vertex normals may lie at the same vertex position. This is covered here for completeness sake only; the proposed enhancements do not have any effect on the presence or appearance of hard edges. The included code uses per-polygon smoothing-groups (most popular in 3D content authoring software) to achieve hard edges, though this can be substituted by any (algorithmic) criteria (e.g. angle between surface normals greater than x degrees). Hard edges can also be created by duplicating vertices (for optimal rendering on modern graphics hardware), but for simplicity we'll assume there are no duplicate vertices present in the model. 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 position (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 vertices of face A for each vert in face A ( for each face B in mesh { // ignore self if face A == face B then skip // criteria for hard-edges if face A and B smoothing groups match { // 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 a discontinuity (the diagonal artifact in the above illustration) when shaded (problem #2). A poor solution would be to simply align the normals to the axii of the faces when such geometry is generated (difficult to preserve) and/or to have an artist correct it by hand (labor intensive). However, we are interested in a generic solution that works for arbitrary geometry constructed from triangles (known as 'triangle soup') and n-sided polygons alike, requiring no artist intervention. Surface Area Weights 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 vertices in face A for each vert in face A { for each face B in mesh { // ignore self if face A == face B then skip // criteria for hard-edges if face A and B smoothing groups match { // 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). Angle Weights 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 low-polygon cylindrical shapes: It may be hard to spot, but if you look closely you will notice a subtle shading discontinuity between vertex A and vertex D. The three faces that influence vertex A, are faces t, u and v. Although both faces U and V belong to the same polygon (UV), the facet normal of that polygon contributes twice. This causes the averaged vertex normal A to point slightly to our right. At vertex C the opposite happens, there st pulls the normal to our left. The result being that the two vertex normals diverge when they should be parallel. We could potentially determine which faces lie in the same plane and skip accumulating coinciding facet normals, but this works only in a small number of situations. A robust solution is to calculate the angle of the corners of the polygons, and use that as additional weight (just like surface area) at that corners vertex. In the above figure, 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 vertices in face A for each vert in face A { for each face B in mesh { // ignore self if face A == face B then skip // criteria for hard-edges if face A and B smoothing groups match { // 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. * Because we normalize the end result, the angle may be computed/stored as either radians or degrees. Only the ratio between neighboring triangle features (surface area, corner angle) contributes as weight, so the choice of angular units does not matter. Conclusion Weighted vertex normals improve the appearance of virtually all geometry, and is generally superior to the traditional non-weighted average. It works because the undesired shading artifacts are displaced from large (highly visible) polygons to their smaller neighbors (less visible), and thereby guarantees improved visuals in virtually all common situations. Since vertex normal generation is most often a design-time process, there is 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 use and computation requires the presence of vertex normals, and here these enhancements also improve visual quality. Tangent and bi-tangent vectors should be accumulated and weighted in parallel to vertex normals before orthogonalization for best quality. Additionally, weighted vertex normals also allow for faux-rounded edges (smooth shaded beveled edges) without significantly increasing the polygon count, and can greatly reduce distortions of specular reflection highlights and environment mapped reflective/refractive surfaces. Author: Martijn Buijs Created on: 2007-12-23 Last modified: 2018-10-18 Contact: martijn AT bytehazard DOT com */