{"id":3836,"date":"2014-11-10T10:42:59","date_gmt":"2014-11-10T16:42:59","guid":{"rendered":"http:\/\/www.realtimerendering.com\/blog\/?p=3836"},"modified":"2022-10-13T08:02:57","modified_gmt":"2022-10-13T14:02:57","slug":"limits-of-triangles","status":"publish","type":"post","link":"https:\/\/www.realtimerendering.com\/blog\/limits-of-triangles\/","title":{"rendered":"Limits of Triangles"},"content":{"rendered":"<p>You&#8217;re mapping a latitude-longitude texture on to a sphere. Pretty straightforward, right? Compute the UV coordinates at the vertices and let the rasterizer do the work. The only problem is that you can&#8217;t get exactly the right answer at the poles. Part of the problem is that getting a good U value at each pole is problematic: U is essentially undefined. It&#8217;s like asking what longitude you&#8217;re at when you&#8217;re sitting at the north pole &#8211; you can take your pick, since none is correct.<\/p>\n<p>One way around this is to look at the other vertices in the triangle (i.e., those not at the pole) and average\u00a0the U values they have. This gives a tessellation at the north pole something like this:<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/marked_monkey.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3837\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/marked_monkey.png\" alt=\"marked_monkey\" width=\"790\" height=\"396\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/marked_monkey.png 790w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/marked_monkey-300x150.png 300w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/marked_monkey-500x250.png 500w\" sizes=\"auto, (max-width: 790px) 100vw, 790px\" \/><\/a><\/p>\n<p>What&#8217;s interesting about this tessellation is that it leaves out half of the horizontal strip of texture near the pole. The sawtooth of triangles will display one half of the texture in this strip, but will not display the triangles covered in red. Even if you form these triangles, adding them to the mesh, they won&#8217;t appear. Recall that all points along the upper edge of the texture are located at the same position in world space, the north pole of the model itself. So any red triangles added will collapse; they&#8217;ll have zero area, as their two points along the top edge are co-located.<\/p>\n<p>I show\u00a0a different tessellation along the bottom strip of the texture, a more traditional way to generate the UV coordinates and triangles. Again, all the triangles with edges along the bottom of the texture will collapse to zero area, and half of the texture in the strip will not be rendered. The triangles that are rendered here are somewhat arbitrary &#8211; at least the triangles along the top edge have a symmetry to them.<\/p>\n<p>There are two ways I know to improve\u00a0the rendering. One is to tessellate more: the more lines of latitude you make, the smaller the problem areas at the poles become. The artifacts are\u00a0still there, but contained in a smaller area. The other, ultimate solution\u00a0is that you could compute the UV location per pixel, not per vertex.<\/p>\n<p>That works for texturing, if you can afford to fold the spherical mapping into the pixel shader. However, this problem isn&#8217;t limited to spheres, and isn&#8217;t limited to textures. Another example is the cone\u00a0and having a normal per vertex. This is a common object, but is surprisingly messy. For a cone\u00a0pointing up you typically make a separate vertex for each triangle meeting at the tip, with a separate normal. This is similar to the uppermost strip for the sphere mapping above: the normal points out in a direction somewhere between the two bottom normals for the triangle in the cone.<\/p>\n<p>However, you have the same sort of interpolation problem! Here&#8217;s a cone with a tessellation of 40 vertices around the top and bottom edges:<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals1.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3847\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals1.jpg\" alt=\"cone_normals1\" width=\"315\" height=\"270\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals1.jpg 315w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals1-300x257.jpg 300w\" sizes=\"auto, (max-width: 315px) 100vw, 315px\" \/><\/a><\/p>\n<p>From triangle to triangle along the side of the cone, you have normals smoothly interpolating along the bottom of the cone. Moving across a vertical edge near the tip, you have sudden shifts in the normal&#8217;s direction as you go from one normal to another. Each\u00a0zero-area triangle that is\u00a0formed by two points touching the pole is\u00a0what &#8220;causes&#8221; the discontinuity in shading when crossing an edge.<\/p>\n<p>If you start to add &#8220;lines of latitude&#8221;, to vertically tessellate the surface, things improve. Here are cones with two strips of triangles, three strips, then ten strips:<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals2.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3846\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals2.jpg\" alt=\"cone_normals2\" width=\"315\" height=\"270\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals2.jpg 315w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals2-300x257.jpg 300w\" sizes=\"auto, (max-width: 315px) 100vw, 315px\" \/><\/a> <a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals3.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3849\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals3.jpg\" alt=\"cone_normals3\" width=\"315\" height=\"270\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals3.jpg 315w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals3-300x257.jpg 300w\" sizes=\"auto, (max-width: 315px) 100vw, 315px\" \/><\/a> <a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals10.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3848\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals10.jpg\" alt=\"cone_normals10\" width=\"315\" height=\"270\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals10.jpg 315w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_normals10-300x257.jpg 300w\" sizes=\"auto, (max-width: 315px) 100vw, 315px\" \/><\/a><\/p>\n<p>It&#8217;s interesting to me that the first image, two strips of triangles, doesn&#8217;t fully improve the bottom part of the cone. Even though there are no zero-area triangles and so the\u00a0normal is the same for\u00a0each vertex in this area, the normals change at different rates across the surface and our eyes pick this up as Mach banding of a sort. Three strips gives three bands of poor interpolation, and so on. With\u00a0ten bands things look good, at least for this lighting and amount of specularity. Increasing the tessellation gets us closer and closer to the ideal per-pixel computation.<\/p>\n<p>Here are some images of the cone mapping to show the discontinuities. The first gives a low tessellation: 10 vertices around, no tessellation vertically<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_lo_res.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3841\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_lo_res.jpg\" alt=\"cone_lo_res\" width=\"614\" height=\"553\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_lo_res.jpg 614w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_lo_res-300x270.jpg 300w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_lo_res-333x300.jpg 333w\" sizes=\"auto, (max-width: 614px) 100vw, 614px\" \/><\/a><\/p>\n<p>Half of the texture is missing on each face of the cone. If you increase the vertical tessellation, like so:<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_wireframe.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3842\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_wireframe.jpg\" alt=\"cone_wireframe\" width=\"634\" height=\"536\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_wireframe.jpg 634w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_wireframe-300x253.jpg 300w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_wireframe-354x300.jpg 354w\" sizes=\"auto, (max-width: 634px) 100vw, 634px\" \/><\/a><\/p>\n<p>You get this:<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_lat.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3845\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_lat.jpg\" alt=\"cone_hi_lat\" width=\"614\" height=\"553\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_lat.jpg 614w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_lat-300x270.jpg 300w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_lat-333x300.jpg 333w\" sizes=\"auto, (max-width: 614px) 100vw, 614px\" \/><\/a><\/p>\n<p>It&#8217;s better, but there&#8217;s still a dropout at the tip (it should say &#8220;1,0 | 0,0&#8221;), plus you can see the lines on the surface are not straight &#8211; the vertical lines on the faces wobble. Each quadrilateral is a trapezoid, so using two triangles for this mapping doesn&#8217;t match it all that well.<\/p>\n<p>If you increase the tessellation in both directions, you get closer still to a per pixel mapping, the correct answer:<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_both.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3844\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_both.jpg\" alt=\"cone_hi_both\" width=\"614\" height=\"553\" srcset=\"https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_both.jpg 614w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_both-300x270.jpg 300w, https:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_hi_both-333x300.jpg 333w\" sizes=\"auto, (max-width: 614px) 100vw, 614px\" \/><\/a><\/p>\n<p>This tessellation is 40 points around, 10 points from bottom to top. The very tip still has half its information missing, but otherwise things look reasonable. Cranking the resolution up to 50 around and 50 from bottom to top mostly cleans up the tip (ignoring\u00a0the lack of high-quality\u00a0texture sampling):<\/p>\n<p><a href=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_super_high_tip.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3843\" src=\"http:\/\/www.realtimerendering.com\/blog\/wp-content\/uploads\/2014\/11\/cone_super_high_tip.jpg\" alt=\"cone_super_high_tip\" width=\"134\" height=\"78\" \/><\/a><\/p>\n<p>This is <a href=\"http:\/\/tog.acm.org\/resources\/RTNews\/html\/rtnv10n2.html#art8\">an old phenomenon<\/a>, but still a surprising one, at least to me. We&#8217;re used to tessellating any higher-order surface into triangles for rasterization or ray tracing. Usually I\u00a0increase tessellation just to capture geometric details, not normal or texture interpolation. You would think it would be possible to easily set up triangles in such a way that relatively simple mappings would not have problems such as these. You basically want something more like a quadrilateral mapping, where the left edge of the triangle is using, say, U = 0.20\u00a0along\u00a0its edge and the right edge uses U = 0.30. The problem is that the point at the tip has a U of say 0.25, so both edges interpolate from there.<\/p>\n<p>I should note that this problem is solvable by messing with the mappings themselves. For example, you could instead use a different texture and a cube map projected onto a sphere to solve that case, which would also decrease distortion and so require less texture resolution overall.<\/p>\n<p>Woo, Pearce, and Ouellette have\u00a0<a href=\"http:\/\/geekshavefeelings.com\/x\/wp-content\/uploads\/2010\/03\/Its-Really-Not-a-Rendering-Bug-You-see....pdf\">a lovely old paper<\/a>\u00a0about this and other common rendering bugs and their solutions.\u00a0They\u00a0reference a paper by Nelson Max from 1989, <a href=\"http:\/\/link.springer.com\/article\/10.1007\/BF01901391\">&#8220;Smooth Appearance for Polygonal Surfaces&#8221;<\/a>\u00a0for using C<sup>1<\/sup> continuity. This doesn&#8217;t seem easy to do on the GPU without a lot of extra data. Has anyone tried solving this general problem, of treating\u00a0two triangles as a quadrilateral mapping? I don&#8217;t really need to solve this problem right now, I just think it&#8217;s interesting, something that&#8217;s doable in a pixel shader in some way. I&#8217;m hoping someone&#8217;s created\u00a0an elegant solution.<\/p>\n<p><em>Update:<\/em> and I got a solution of sorts. Thanks to Tom Forsyth for getting the ball rolling on <a href=\"https:\/\/twitter.com\/pointinpolygon\/status\/531850354061635584\">Twitter<\/a>. <a href=\"http:\/\/www.reedbeta.com\/blog\/2012\/05\/26\/quadrilateral-interpolation-part-1\/\">Nathan Reed&#8217;s post on this topic<\/a> talks about setup,\u00a0I\u00f1igo\u00a0Qu\u00edlez talks about performing <a href=\"http:\/\/www.iquilezles.org\/www\/articles\/ibilinear\/ibilinear.htm\">bilinear interpolation<\/a>\u00a0in the shader\u00a0and <a href=\"https:\/\/www.shadertoy.com\/view\/lsBSDm\">gives a ShaderToy demo<\/a>.\u00a0However, these seem a bit apples and oranges: Nathan is working with projective interpolation, Inigo is working with equi-spaced bilinear interpolation &#8211; they&#8217;re different. See page 14 on of <a href=\"http:\/\/www.cs.cmu.edu\/~ph\/texfund\/texfund.pdf\">Heckbert&#8217;s master&#8217;s thesis<\/a>, for example.<\/p>\n<p><em>Another update:<\/em> <a href=\"https:\/\/www.unchainedgeometry.com\/ScoopBaryCone.html\">Jules Bloomenthal notes<\/a> how his <a href=\"https:\/\/www.jcgt.org\/published\/0011\/03\/04\/\">barycentric quad rasterization work<\/a> fixes cones.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You&#8217;re mapping a latitude-longitude texture on to a sphere. Pretty straightforward, right? Compute the UV coordinates at the vertices and let the rasterizer do the work. The only problem is that you can&#8217;t get exactly the right answer at the poles. Part of the problem is that getting a good U value at each pole [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[48],"tags":[573,574,571,572],"class_list":["post-3836","post","type-post","status-publish","format-standard","hentry","category-reports","tag-cone","tag-sphere","tag-texture-mapping","tag-triangles"],"_links":{"self":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts\/3836","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/comments?post=3836"}],"version-history":[{"count":5,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts\/3836\/revisions"}],"predecessor-version":[{"id":5780,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/posts\/3836\/revisions\/5780"}],"wp:attachment":[{"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/media?parent=3836"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/categories?post=3836"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.realtimerendering.com\/blog\/wp-json\/wp\/v2\/tags?post=3836"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}