|
| 1 | + |
1 | 2 | Rendering polygons
|
2 | 3 | ===============================================================================
|
3 | 4 |
|
4 | 5 | .. contents:: .
|
5 | 6 | :local:
|
6 | 7 | :depth: 2
|
7 | 8 | :class: toc chapter-10
|
8 |
| - |
| 9 | + |
| 10 | +Polygons are an important topic for scientific visualization because they can |
| 11 | +be used to display bars, histograms, charts, filled plots, etc. Displaying |
| 12 | +polygons using OpenGL is really fast, provided we have the proper |
| 13 | +triangulation. The teaser image comes from the `tiger demo |
| 14 | +<https://github.com/glumpy/glumpy/blob/master/examples/tiger.py>`_ of glumpy. |
| 15 | + |
9 | 16 |
|
10 | 17 | Triangulation
|
11 | 18 | -------------------------------------------------------------------------------
|
12 | 19 |
|
13 |
| -Odd-fill rule |
| 20 | +In order to draw a polygon, we need to triangulate it, i.e., we have to |
| 21 | +decompose it into a sum of non overlapping triangles. To do that, we have to |
| 22 | +consider whether the polygon is convex or concave: |
| 23 | + |
| 24 | +.. image:: images/chapter-10/polygons.png |
| 25 | + :width: 100% |
| 26 | + |
| 27 | +To know if a given polygon is concave or convex, it is rather easy. Convex |
| 28 | +polygons have all their diagonals contained inside, while it is not true for |
| 29 | +concave polygons, i.e. you can find two summits such that when you connect |
| 30 | +them, the segment is outside the polygon. Another test is to find a straight |
| 31 | +line that cross a concave polygon at more than two points as shown on the |
| 32 | +figure above with the red lines. |
| 33 | + |
| 34 | + |
| 35 | +Convex polygons |
| 36 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| 37 | + |
| 38 | +For convex polygons, we have to consider two cases: |
| 39 | + |
| 40 | +1. points are ordered and describe the contour of the polygon |
| 41 | +2. points are unordered and spread randomly onto the 2d plane |
| 42 | + |
| 43 | +For the second case, we can use scipy to compute the convex hull of the points |
| 44 | +such as to be in the first case situation: |
| 45 | + |
| 46 | +.. code:: python |
| 47 | +
|
| 48 | + import numpy as np |
| 49 | + import scipy.spatial |
| 50 | +
|
| 51 | + P = np.random.uniform(-1.0, 1.0, (100,2)) |
| 52 | + P = P[scipy.spatial.ConvexHull(P).vertices] |
| 53 | +
|
| 54 | +From this ordered set of vertices describing the contour, it is now easy to |
| 55 | +render the polygon using the `gl.GL_TRIANGLE_FAN` primitives: |
| 56 | + |
| 57 | +.. code:: python |
| 58 | +
|
| 59 | + @window.event |
| 60 | + def on_draw(dt): |
| 61 | + window.clear() |
| 62 | + polygon.draw(gl.GL_TRIANGLE_FAN) |
| 63 | +
|
| 64 | +You can see on the figures below that it is better to use only the convex hull |
| 65 | +points to compute the triangulation. You can also check that all other points |
| 66 | +are actually inside the polygon area. |
| 67 | + |
| 68 | + |
| 69 | +.. figure:: images/chapter-10/convex-polygon-point.png |
| 70 | + :figwidth: 30% |
| 71 | + :figclass: left |
| 72 | + |
| 73 | + Figure |
| 74 | + |
| 75 | + A cloud of random points. Convex hull points have been highlighted. |
| 76 | + See `convex-polygon-point.py <code/chapter-10/convex-polygon-point.py>`_ |
| 77 | + |
| 78 | +.. figure:: images/chapter-10/convex-polygon.png |
| 79 | + :figwidth: 30% |
| 80 | + :figclass: left |
| 81 | + |
| 82 | + Figure |
| 83 | + |
| 84 | + A Delaunay triangulation with a lof of useless triangles. |
| 85 | + See `convex-polygon.py <code/chapter-10/convex-polygon.py>`_ |
| 86 | + |
| 87 | + |
| 88 | +.. figure:: images/chapter-10/convex-polygon-fan.png |
| 89 | + :figwidth: 30% |
| 90 | + :figclass: left |
| 91 | + |
| 92 | + Figure |
| 93 | + |
| 94 | + A triangulation restricted to points belonging to the convex hull. |
| 95 | + See `convex-polygon-fan.py <code/chapter-10/convex-polygon-fan.py>`_ |
| 96 | + |
| 97 | + |
| 98 | +Concave polygons |
| 99 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| 100 | + |
| 101 | +For concave polygons, we could consider the two aforementionned cases where |
| 102 | +points are either ordered and describe the contour of the polygon or points are |
| 103 | +unordered and spread randomly onto the 2d plane. However, for the latter case, |
| 104 | +things become more difficult because the solution is not unique as shown on the |
| 105 | +figure below. |
| 106 | + |
| 107 | +.. figure:: images/chapter-10/concave-hull.png |
| 108 | + :figwidth: 100% |
| 109 | + |
| 110 | + Figure |
| 111 | + |
| 112 | + The concave hull (or `alpha shape |
| 113 | + <https://en.wikipedia.org/wiki/Alpha_shape>`_) of a set of points is not |
| 114 | + unique. Images by `Martin Laloux |
| 115 | + <http://www.portailsig.org/content/sur-la-creation-des-enveloppes-concaves-concave-hull-et-les-divers-moyens-d-y-parvenir-forme>`_. |
| 116 | + |
| 117 | +This is the reason why we'll restrict ourselves to the first case, that is, we |
| 118 | +have a set or ordered points describing the contour of a concave polygon. But |
| 119 | +even in such simple case, triangulation is not obvious and we'll thus need a |
| 120 | +dedicated library. We'll use the `triangles <http://dzhelil.info/triangle/>`_ |
| 121 | +library but there are others: |
| 122 | + |
| 123 | +.. figure:: images/chapter-10/firefox.png |
| 124 | + :figwidth: 30% |
| 125 | + :figclass: right |
| 126 | + |
| 127 | + Figure |
| 128 | + |
| 129 | + The firefox logo, tesselated (Bézier curves converted to segments) and |
| 130 | + triangulated. See `firefox.py <code/chapter-10/firefox.py>`_ |
| 131 | + |
| 132 | +.. code:: python |
| 133 | + |
| 134 | + def triangulate(vertices): |
| 135 | + n = len(vertices) |
| 136 | + segments = (np.repeat(np.arange(n+1),2)[1:-1]) % n |
| 137 | + T = triangle.triangulate({'vertices': vertices, |
| 138 | + 'segments': segments}, "p") |
| 139 | + return T["vertices"], T["triangles"] |
| 140 | +
|
| 141 | +
|
| 142 | + |
| 143 | +On the image on the right, we've parsed (see `svg.py |
| 144 | +<code/chapter-10/svg.py>`_) the firefox icon SVG path and tesselated the Bézier |
| 145 | +curves into line segments. Then we have triangulated the resulting path and |
| 146 | +obtained the displayed triangulation using `gl.GL_TRIANGLES`. See `firefox.py |
| 147 | +<code/chapter-10/firefox.py>`_ |
| 148 | + |
| 149 | + |
| 150 | +Fill rule |
| 151 | +------------------------------------------------------------------------------- |
| 152 | + |
| 153 | +The fill-rule property is used to specify how to paint the different parts of a |
| 154 | +shape. As explained in the `SVG specification`_, *for a simple, |
| 155 | +non-intersecting path, it is intuitively clear what region lies "inside"; |
| 156 | +however, for a more complex path, such as a path that intersects itself or |
| 157 | +where one subpath encloses another, the interpretation of "inside" is not so |
| 158 | +obvious. The fill-rule property provides two options for how the inside of a |
| 159 | +shape is determined: non-zero and even-odd.* |
| 160 | + |
| 161 | +.. figure:: images/chapter-10/fillrule-nonzero.png |
| 162 | + :figwidth: 45% |
| 163 | + :figclass: left |
| 164 | + |
| 165 | + Figure |
| 166 | + |
| 167 | + From the `SVG Specification`_: *The nonzero fill rule determines the |
| 168 | + "insideness" of a point on the canvas by drawing a ray from that point to |
| 169 | + infinity in any direction and then examining the places where a segment of |
| 170 | + the shape crosses the ray.* |
| 171 | + |
| 172 | + |
| 173 | +.. figure:: images/chapter-10/fillrule-evenodd.png |
| 174 | + :figwidth: 45% |
| 175 | + :figclass: left |
| 176 | + |
| 177 | + Figure |
| 178 | + |
| 179 | + From the `SVG Specification`_: *The evenodd fill rule determines the |
| 180 | + "insideness" of a point on the canvas by drawing a ray from that point to |
| 181 | + infinity in any direction and counting the number of path segments from the |
| 182 | + given shape that the ray crosses.* |
| 183 | + |
| 184 | +---- |
| 185 | + |
| 186 | +To enforce the fill-rule property, we'll need to use the `stencil buffer |
| 187 | +<https://www.khronos.org/opengl/wiki/Stencil_Test>`_ that allows to have |
| 188 | +per-sample operation and test performed after the fragment shader |
| 189 | +stage. Depending on the `stencil function |
| 190 | +<https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glStencilFunc.xhtml>`_ |
| 191 | +and `stencil operation |
| 192 | +<https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glStencilOp.xhtml>`_ |
| 193 | +we'll define, we can control precisely how a shape is rendered. But first, we |
| 194 | +need to tell OpenGL we'll be using a stencil buffer. In glumpy, the default is |
| 195 | +to have no stencil buffer, that is, the default bit depth of the stencil buffer |
| 196 | +is zero. To activate it, we thus simply need to specify some non-zero stencil |
| 197 | +bit depth (e.g. 8 for 256 possible values): |
| 198 | + |
| 199 | +.. code:: python |
| 200 | + |
| 201 | + config = app.configuration.Configuration() |
| 202 | + config.stencil_size = 8 |
| 203 | + window = app.Window(config=config, width=512, height=512) |
| 204 | +
|
| 205 | + @window.event |
| 206 | + def on_init(): |
| 207 | + gl.glEnable(gl.GL_STENCIL_TEST) |
| 208 | +
|
| 209 | +Note that we also need to activate the stencil test in the `on_init` window event. |
| 210 | + |
| 211 | +.. _SVG Specification: https://www.w3.org/TR/SVG/painting.html |
| 212 | + |
| 213 | + |
| 214 | +Non-zero fill rule |
| 215 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| 216 | + |
| 217 | +The non-zero fill rule implementation is easy because it corresponds to the |
| 218 | +default triangulation we've just seen above and no extra work is necessary. |
| 219 | + |
| 220 | + |
| 221 | +Odd-even fill rule |
| 222 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| 223 | + |
| 224 | +In order to enforce the odd-even fill rule, we need to use a 2-pass |
| 225 | +rendering. The first pass will write to the stencil buffer according to the |
| 226 | +operation we define and the second pass will read the stencil buffer in order |
| 227 | +to decide if a fragment need to be painted or not. For the first pass, we thus |
| 228 | +disable depth and color writing and we instruct OpenGL to increment stencil |
| 229 | +value if a shape is drawn clockwise (CW) and to decrement it for counter clock |
| 230 | +wise shapes (CCW): |
| 231 | + |
| 232 | +.. code:: python |
| 233 | + |
| 234 | + # Disable color and depth writing |
| 235 | + gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) |
| 236 | + gl.glDepthMask(gl.GL_FALSE) |
| 237 | +
|
| 238 | + # Always write to stencil |
| 239 | + gl.glStencilFunc(gl.GL_ALWAYS, 0, 0) |
| 240 | + |
| 241 | + # Increment value for CW shape |
| 242 | + gl.glStencilOpSeparate(gl.GL_FRONT, gl.GL_KEEP, gl.GL_KEEP, gl.GL_INCR) |
| 243 | + |
| 244 | + # Decrement value for CCW shape |
| 245 | + gl.glStencilOpSeparate(gl.GL_BACK, gl.GL_KEEP, gl.GL_KEEP, gl.GL_DECR) |
| 246 | +
|
| 247 | +
|
| 248 | +Once the stencil buffer has been written, we can use the stored value to decide |
| 249 | +for the condition to be tested for writing to the render buffer. Using the |
| 250 | +`glStencilFunc` function, we can express virtually any condition we want: |
| 251 | + |
| 252 | + =============== ================================================ |
| 253 | + `glStencilFunc` `(func, ref, mask)` |
| 254 | + =============== ================================================ |
| 255 | + `GL_NEVER` Always fails |
| 256 | + --------------- ------------------------------------------------ |
| 257 | + `GL_LESS` Passes if ( ref & mask ) < ( stencil & mask ) |
| 258 | + --------------- ------------------------------------------------ |
| 259 | + `GL_LEQUAL` Passes if ( ref & mask ) <= ( stencil & mask ) |
| 260 | + --------------- ------------------------------------------------ |
| 261 | + `GL_GREATER` Passes if ( ref & mask ) > ( stencil & mask ) |
| 262 | + --------------- ------------------------------------------------ |
| 263 | + `GL_GEQUAL` Passes if ( ref & mask ) >= ( stencil & mask ) |
| 264 | + --------------- ------------------------------------------------ |
| 265 | + `GL_EQUAL` Passes if ( ref & mask ) = ( stencil & mask ) |
| 266 | + --------------- ------------------------------------------------ |
| 267 | + `GL_NOTEQUAL` Passes if ( ref & mask ) != ( stencil & mask ) |
| 268 | + --------------- ------------------------------------------------ |
| 269 | + `GL_ALWAYS` Always passes |
| 270 | + =============== ================================================ |
| 271 | + |
| 272 | + |
| 273 | + |
| 274 | +.. figure:: images/chapter-10/winding.png |
| 275 | + :figwidth: 30% |
| 276 | + :figclass: right |
| 277 | + |
| 278 | + Figure |
| 279 | + |
| 280 | + Odd-even fill rule using the stencil buffer. |
| 281 | + See `winding.py <code/chapter-10/winding.py>`_ |
| 282 | + |
| 283 | +For the actual odd-even fill rule, we only need to test for the last bit in the |
| 284 | +stencil buffer: |
| 285 | + |
| 286 | +.. code:: python |
| 287 | + |
| 288 | + # Enable color and depth writing |
| 289 | + gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) |
| 290 | + gl.glDepthMask(gl.GL_TRUE) |
| 291 | +
|
| 292 | + # Actual stencil test |
| 293 | + # Odd-even |
| 294 | + gl.glStencilFunc(gl.GL_EQUAL, 0x01, 0x1) |
| 295 | +
|
| 296 | + # Non zero |
| 297 | + # gl.glStencilFunc(gl.GL_NOTEQUAL, 0x00, 0xff) |
| 298 | + |
| 299 | + # Positive |
| 300 | + # gl.glStencilFunc(gl.GL_LESS, 0x0, 0xff) |
| 301 | +
|
| 302 | + # Stencil operation (for both CW and CCW shapes) |
| 303 | + gl.glStencilOp(gl.GL_KEEP, gl.GL_KEEP, gl.GL_KEEP) |
| 304 | +
|
| 305 | +
|
| 306 | +Gradient and pattern |
| 307 | +------------------------------------------------------------------------------- |
| 308 | + |
| 309 | +Antialiasing |
14 | 310 | -------------------------------------------------------------------------------
|
15 | 311 |
|
16 |
| -Holes |
| 312 | +Exercises |
17 | 313 | -------------------------------------------------------------------------------
|
18 | 314 |
|
19 | 315 |
|
0 commit comments