0% found this document useful (0 votes)
19 views53 pages

Chapter 3

Chapter 3 discusses the attributes of graphics primitives, including color, point, line, fill-area, and character attributes, as well as aliasing. It explains how these attributes affect the display of graphics and the use of OpenGL state variables to manage them. The chapter also covers various methods for defining colors, point sizes, line styles, and fill styles, highlighting the differences between direct storage and color look-up tables.

Uploaded by

bunabtravel
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
19 views53 pages

Chapter 3

Chapter 3 discusses the attributes of graphics primitives, including color, point, line, fill-area, and character attributes, as well as aliasing. It explains how these attributes affect the display of graphics and the use of OpenGL state variables to manage them. The chapter also covers various methods for defining colors, point sizes, line styles, and fill styles, highlighting the differences between direct storage and color look-up tables.

Uploaded by

bunabtravel
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
You are on page 1/ 53

Chapter 3

ATTRIBUTES OF
GRAPHICS PRIMITIVES
Chapter outline
2

• Introduction

• Color attributes

• Point attributes

• Line attributes

• Fill-Area Attributes

• Character Attributes

• Aliasing
Introduction
3

 Attributes are properties or behaviors of an object.

 Attributes of graphics primitives can be defined a parameter that affects the way the primitive is

displayed.
 For instance the drawing color and the way the line is drew defines line attributes.
State system (or state machine)
o A graphics system that maintains a list for the current values of attributes and other parameters
(state variables or state parameters).
o We assign a value to one or more state parameters, that is, we put the system into a certain state.
o The state will be persistent until we change the value of a state parameter
o The graphics system behaviors are determined by these system state, which can be modified by
calling OpenGL functions.
Introduction
4

OpenGL State Variables


 Attribute values and other parameter settings are specified with separate functions that
define the current OpenGL state
 The OpenGL state variable includes:
o The current color or other attributes

o The current model & viewing transformations

o The current camera model & clipping

o The current lighting & reflectance model

o The current viewport

 All have default values, remaining until a new setting on it.


 At any time, we can query the system to determine the current value of a state parameter
Color Attributes
5

 We have two color state variables namely drawing and the background color variables.
we can define colors in two different ways:
 RGB: the color is defined as the combination of its red, green and blue components.
 RGBA: the color is defined as the combination of its red, green and blue components
together with an alpha value, which indicates the degree of opacity/transparency of the
color.
 The background color was defined with an alpha value, whereas the drawing color did
not.
 If no alpha value is defined it is assumed to be equal to 1, which is completely opaque.

Direct Storage vs. Color Look-Up Tables


 Frame buffer is used by raster graphics systems to store the image ready for display on
the monitor.
Color Attributes
6
 For color displays, the frame buffer must have some way of storing color values.
There are two ways in which graphics packages store color values in a frame
buffer:
 Direct storage
 Look-up table
 Direct storage
 The color values of each pixel are stored directly in the frame buffer. Therefore,
each pixel in the frame buffer must have 3 (RGB) or 4 (RGBA) values stored.
 This means that the frame buffer can take up a lot of memory.
Color look-up tables

7
 Reduce the amount of storage required by just storing an index for each pixel in the frame buffer.
 The actual colors are stored in a separate look-up table, and the index looks up a color in this
table.
 Therefore, using color look-up tables, we can only have a limited number of different colors in
the scene.
 Although color look-up tables do save storage, they slow down the rasterization process as an
extra look-up operation is required.
 Color look-up was commonly used in the early days of computer graphics when memory was
expensive.
 OpenGL uses direct storage by default, but the programmer can choose to use a color look-up
table if they wish.
8

 We change the current drawing color using the glColor* function and the
current background color using the glClearColor function.
 The background color is always set using an RGBA color representation
OpenGL Color Functions
9

 In an OpenGL color routines use one function to set the color for the display window, and use
another function to specify a color for the straight-line segment, points etc.
 Set the color display mode to RGB with the statement
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB);
 The first constant in the argument list states that we are using a single buffer for the frame buffer,
and the second constant puts us into the RGB mode, which is the default color mode.
 In the RGB (or RGBA) mode, we select the current color components with the
function
glColor* (colorComponents);
 Suffix codes are similar to those for the glVertex function.
 We use a code of either 3 or 4 to specify the RGB or RGBA mode along with
the numerical data-type code and an optional vector suffix.
OpenGL Color Functions
10
 The suffix codes for the numerical data types are b (byte), i (integer), s (short), f (float), and d
(double).
 Floating-point values for the color components are in the range from 0.0 to 1.0, and the default color
components for glColor.
 glColor3f(1.0,0.0,0.0); // set drawing color to red.
 glClearColor(1.0,1.0,1.0,1.0); // set background color to opaque white.
An OpenGL color selection can be assigned to individual point positions
within
glBegin/glEnd pairs
 we can define alpha values for background colors, these values will not be used unless we enable the
color blending feature of OpenGL.
 To use color blending in OpenGL we use the line:
 glEnable(GL_BLEND);

Point Attributes
11

 Points are the simplest type of primitive, so the only attributes we can modify are the
color and size of the point.
 In a state system, the displayed color and size of a point is determined by the current
values stored in the attribute list.
 Color components are set with RGB values or an index into a color table.
 For a raster system, point size is an integer multiple of the pixel size,
so that a large point is displayed as a square block of pixels
12

The displayed color of a designated point position is controlled by the current


color values in the state list. Also, a color is specified with either the glColor
function or the glIndex function.
We set the size for an OpenGL point with
glPointSize (size);
And the point is then displayed as a square block of pixels.
Parameter size is assigned a positive floating-point value, which is rounded to
an integer (unless the point is to be antialiased).
Simple example
13

glColor3f (1.0, 0.0, 0.0);


glBegin (GL_POINTS);
glVertex2i (50, 100);
glPointSize (2.0);
glColor3f (0.0, 1.0, 0.0);
glVertex2i (75, 150);
glPointSize (3.0);
glColor3f (0.0, 0.0, 1.0);
glVertex2i (100, 200);
glEnd ( );
Line Attributes

 A straight-line segment can be displayed 14


with three basic attributes: color, width, and
style.
 Line color is typically set with the same function for all graphics primitives, while line
width and line style are selected with separate line functions.
Line Width
 The simplest and most common technique for increasing the width of a line is to plot a
line of width 1 pixel, and then add extra pixels in either the horizontal or vertical
directions.
 We should plot extra pixels vertically, and for others ,we should plot them horizontally.
 Which of these two approaches we use depends on the gradient m of the line.
We identify the following cases:

15
 If |m| ≤ 1 plot extra pixels vertically, i.e. same x-coordinate, different y-
coordinate, as in Figure 2(a).
 If |m| > 1, plot extra pixels horizontally, i.e. same y-coordinate, different x-
coordinate, as in Figure 2(b).
16

 Although this approach is simple and effective, there are two slight problems:
 The actual thickness of a plotted line depends on its slope. Although this is a small
weakness of the technique most graphics packages do not attempt to address this problem.
 You can notice this effect in, for example, the Paint accessory in Microsoft Windows.
 The ends of lines are either vertical or horizontal. Depending on whether we are plotting
extra pixels in the horizontal or vertical directions, the line ends will be horizontal or
vertical.
 Line Style
 The style of a line refers to whether it is plotted as a solid line, dotted, or dashed.
 The normal approach to changing line style is to define a pixel mask.
 A pixel mask specifies a sequence of bit values that determine whether pixels in the plotted
line should be on or off.
OpenGL Line Attribute Functions

17
 For example, the pixel mask 11111000 means a dash length of 5 pixels followed by a spacing of 3 pixels.
 In other words, if the bit in the pixel mask is a 1, we plot a pixel, and if it is a zero, we leave a space.
 OpenGL Line Attribute Functions
OpenGL Line-color Function
 The line color is specified using the built in function
glColor3*(RGB);
OpenGL Line-Width Function
Line width is set in OpenGL with the function
glLineWidth (width);
 We assign a floating-point value to parameter width, and this value is rounded to the nearest nonnegative
integer.
 If the input value rounds to 0.0, the line is displayed with a standard width of 1.0, which is the default
width.
18

 By default, a straight-line segment is displayed as a solid line.


 However, we can also display dashed lines, dotted lines, or a line with a combination of dashes
and dots, and we can vary the length of the dashes and the spacing between dashes or dots.
 We set a current display style for lines with the OpenGL function
glLineStipple (repeatFactor, pattern);
 Parameter pattern is used to reference a 16-bit integer that describes how the line should be
displayed.
 A 1 bit in the pattern denotes an “on” pixel position, and a 0 bit indicates an “off” pixel
position.
 The default pattern is 0xFFFF (each bit position has a value of 1), which produces a solid line
 Integer parameter repeatFactor specifies how many times each bit in the pattern is to be
repeated before the next bit in the pattern is applied.
19

 Before a line can be displayed in the current line-style pattern, we must


activate the line-style feature of OpenGL.
 We accomplish this with the following function:
glEnable (GL_LINE_STIPPLE);
 At any time, we can turn off the line-pattern feature with
glDisable (GL_LINE_STIPPLE);
 For example, the code shown below draws the dashed line shown to the right.
 The pixel mask is the hexadecimal number 00FF, which is 8 zeros followed by 8 ones.
 The repeat factor is 1 so each one or zero in the pixel mask corresponds to a single pixel
in the line.
20
Fill-Area Attributes
21
 The most important attribute for fill-area polygons is whether they are filled or not.
 We can either draw a filled polygon or drawn the outline only.
 Drawing the outline only is known as wireframe rendering.
 If the polygon is to be filled we can specify a fill style.

Fill Styles
 A basic fill-area attribute provided by a general graphics library is the display style of the interior.
 We can display a region with a single color, a specified fill pattern, or in a “hollow” style by showing
only the boundary of the region.
 These three fill styles are illustrated in Fig below
Fill-Area Attributes

 Broadly speaking, we can identify two different


22 approaches to filling enclosed
boundaries:
 Scan-Line Approach and
 Seed-based Approach

1. Scan-line approach: Scan-line fill algorithms are automatic (i.e. they require no user
intervention) and are commonly used by general-purpose graphics packages such as
OpenGL.
 The scan-line fill algorithm automatically fills any non-degenerate polygon by
considering each scan-line (i.e. row of the frame buffer) in turn.
 We move across the scan-line, from left-to-right, until we reach a boundary
 Then we start plotting pixels in the fill color and continue to move
across the scan-line.
 When we reach another boundary we stop filling. At the next boundary we start filling
again, and so on
Scan-line approach:
23

 The starting point (i.e. the far left of the scan-line) is assumed to be outside the polygon,
so points between the first and second boundary crossings are assumed to be inside.
This process is illustrated in Figure 9.

 The scan-line fill algorithm works well for simple polygons, but there are cases in which
it can experience problems. We now consider such a case, illustrated in Figure 10.
 The scan-line algorithm must always treat vertices as two boundary crossings:
 if we did not then as we crossed the vertex in scan-line x in Figure 10 the algorithm
would stop filling when it should continue.
Scan-line approach:
24

However, if we treat vertices as two boundary crossings it can cause problems in


other case, as shown by scan-line y.
 Here we treat the vertex as two boundary crossings, but it causes the
algorithm to continue filling when it should stop.
The difference between scan-lines x and y is that in scan-line x the boundary does not
cross the scan-line at the vertex, whereas in scan-line y it does.
Scan-line approach:
25
 There are two possible answers to this problem. First, we can perform a preprocessing stage to detect which
vertices cross the scan-line and which don't.
 This would work OK, but it also takes time.
 Therefore the more common approach is to insist that all polygons are convex. As we can see from Figure
11, in a convex polygon there is always a maximum of two boundary crossings in every scan-line.
 Therefore if there are two crossing we simply fill between the two crossing points.
 If there is one we know that it is a vertex crossing so we fill a single pixel.
 This greatly improves the efficiency of the scan-line fill algorithm, and is the reason why most graphics
packages insist that all fill-area primitives are convex polygon
2. Seed-Based Fill Algorithms
26

 Seed-based fill algorithms have the advantage that they can fill arbitrarily complex
shapes.
 They are less efficient than scan-line algorithms, and require user interaction in the form
of specifying a seed-point (i.e. a starting point for the fill, known to be inside the
boundary).
 Therefore they are typically used by interactive packages such as paint programs.
 In this section we will examine two similar techniques: boundary-fill and flood-fill.
 Both are recursive in nature and work by starting from the seed point and painting
outwards until some boundary condition is met.
 The difference between the two algorithms is in the nature of the boundary condition.
Boundary fill
27

 To implement the boundary fill algorithm we first define three parameters:


 The seed point, a fill color and a boundary color.
The pseudocode for the algorithm is:
 Start with seed point
 If the current pixel is not in the boundary color and not in the fill color:
Fill current pixel using fill color
Recursively call boundary-fill for all neighboring pixels
 Notice that the termination condition for the algorithm is when we reach pixels in the
boundary color.
 Therefore the boundary must be in a single (known) color.
 We check to see if the current pixel is in the fill color to ensure we don't fill the same
pixel many times
Boundary fill
28

 One question that we must answer before implementing boundary-fill is ,what exactly
we mean by a neighboring point.
 Figure 12 shows that there are two possible interpretations of a neighbour: 4-connected
and 8-connected neighbors'.
 Both interpretations can be useful, although using 8-connected neighbours can lead to
the algorithm, escaping‟ from thin diagonal boundaries, so 4-connected neighbours are
more commonly used.
Boundary fill
29

 Another issue that we must consider is what to do if the region to be filled is already partly
filled.
 For example, in Figure 13 the boundary color is grey, the fill color is red and the seed-point is
the pixel marked by the letter ‘S’
 Since boundary-fill will not process any pixels that are already in the fill color, in this
example only half of the fill-area will be filled:
 The algorithm will stop when it reaches the already-filled pixels.
 One solution to this problem is to preprocess the fill area to remove any partly-filled areas.
Boundary fill
30
Flood Fill:
31
 It is very similar to boundary-fill, but instead of filling until it reaches a particular boundary color, it
continues to fill whilst the pixels are in specific interior color.
 Therefore, first we must define a seed point, a fill color and an interior color.
 Pseudocode for flood-fill is given below:
 Start with seed point
 If current pixel in interior color:
 Fill pixel using fill color

 Recursively call flood-fill for all neighboring pixels.

 Again, for flood-fill we have the same issues regarding partly-filled areas and what we mean by a
neighboring pixel.
 Actually the two algorithms are very similar, and in many cases their operation will be identical.
 But flood fill can be more useful for cases where the boundary is not in a single color. such as that shown
in Figure 14
Flood Fill:

32
OpenGL Fill-Area Attribute Functions
 OpenGL provides a number of features to modify33the appearance of fill-area polygons.
 First, we can choose to fill the polygon or just display an outline (i.e. wireframe rendering).
 We do this by changing the display mode using the glPolygonMode routine. The basic form of this
function is:
 glPolygonMode(face, displayMode) ;

 Here, the face argument can take any of the following values:
 GL_FRONT: apply changes to front-faces of polygons only.
 GL_BACK: apply changes to back-faces of polygons only.
 GL_FRONT_AND_BACK: apply changes to front and back faces of polygons.
The displayMode argument can take any of these values:
 GL_FILL: fill the polygon faces.
 GL_LINE: draw only the edges of the polygons (wireframe rendering).
 GL_POINT: draw only the vertices of the polygons.
34

For example, the following code draws a polygon with four vertices using wireframe
rendering for the front face.

 In addition to changing the polygon drawing mode, we can also specify a fill pattern
for polygons.
 We do this using the polygon stipple feature of OpenGL. The steps can be
summarized as:
 Define a fill pattern
 Enable the polygon stipple feature of OpenGL
 Draw polygons For example, the following code draws an eight-sided polygon with a
35

 The fill pattern accepted by the glPolygonStipple function must be an array


of 128 8-bit bitmaps. These bitmaps represent a 32x32 pixel mask to be used
for filling.
Character Attributes
36

 Character primitives can be used to display text characters. There are two different
types of representation for characters:
1. Bitmap :
 Using a bitmap representation (or font), characters are stored as a grid of pixel values.
 This is a simple representation that allows fast rendering of the character.
 However, such representations are not easily scalable:
If we want to draw a larger version of a character defined using a bitmap, we will get an
aliasing effect (the edges of the characters will appear jagged due to the low resolution).
Character Attributes
37

Similarly, we cannot easily generate bold or italic versions of the character.


For this reason, when using bitmap fonts we normally need to store multiple
fonts to represent the characters in different sizes/styles etc.
38

2. Stroke (or outline) representations.


Using stroke representations, characters are stored using line or curve primitives.
To draw the character we must convert these primitives into pixel values on the
display.
 As such, they are much more easily scalable: to generate a larger version of the
character we just multiply the coordinates of the line/curve primitives by some scaling
factor.
39

Bold and italic characters can be generated using a similar approach.


The disadvantage of stroke fonts is that they take longer to draw than
bitmap fonts.
Character primitives further can be categorized into serif and sans-serif
fonts. Sans-serif fonts have no accents on the characters, whereas serif
fonts do have accents.
 For example, Arial and Verdana fonts are examples of sans-serif fonts
whereas Times-Roman and Garamond are serif fonts:
Character Attributes

40

Finally, we can categorize fonts as either monospace or proportional fonts.


Characters drawn using a monospace font will always take up the same
width on the display, regardless of which character is being drawn.
With proportional fonts, the width used on the display will be
proportional to the actual width of the character, e.g. the letter “i” will take
up less width than the letter “w”.
As shown below, Courier is an example of a monospace font whereas
Times-Roman and Arial are examples of proportional fonts:
OpenGL Character Primitive Routines
41

 OpenGL on its own does not contain any routines dedicated to drawing text characters.
 However, the glut library does contain two different routines for drawing individual
characters (not strings).
 Raster position: specifies where the character should be drawn.
 We need to do this only once for each sequence of characters. After each character is drawn
the raster position will be automatically updated ready for drawing the next character.
 To set the raster position we use the glRasterPos2i routine.
 For example, glRasterPos2i(x, y) positions the raster at coordinate location (x, y).
 glutBitmapCharacter(GLUT_BITMAP_9_BY_15, ‘a’);
 This will draw the character “a” in a monospace bitmap font with width 9 and height 15
pixels.
42
 There are a number of alternative symbolic constants that we can use in place of
GLUT_BITMAP_9_BY_15 to specify different types of bitmap font. For example,
 GLUT_BITMAP_8_BY_13

 GLUT_BITMAP_9_BY_15.

 We can specify proportional bitmap fonts using the following symbolic constants:
 GLUT_BITMAP_TIMES_ROMAN_10

 GLUT_BITMAP_HELVETICA_10

 Here, the number represents the height of the font. Alternatively, we can use stroke fonts
using the glutStrokeCharacter routine. For example,
glutStrokeCharacter(GLUT_STROKE_ROMAN, ‘a’);
 This will draw the letter “a” using the Roman stroke font, which is a proportional font. We
can specify a monospace stroke font using the following symbolic constant:
 GLUT_STROKE_MONO_ROMAN
43
 The attributes that change the appearance of character primitives are:
 Size
 Style/font

 Color

 Stroke character primitives are drawn using the glutStrokeCharacter routine.


 Using this type of character primitive, we can change the font and color as
described above, and also the line width and the line style.
 Remember that stroke characters are stored as set of line primitives.
 Similarly, we can change the style of the lines using the glLineStipple routine.
Aliasing
44

 Aliasing refers to the introduction of errors into an image as a consequence of the


discretization of the primitives when plotting on a raster graphics system.
 Whenever a primitive is drawn on a raster display it has to be scan-converted.
 This involves determining which of the discrete locations of the pixels the primitives should
be plotted at.
 By forcing the primitives to be plotted only at these discrete locations we inevitably introduce
errors.
 Another name for the effect of aliasing is the “staircase effect”, or the “jaggies”.
 In mathematical terms, the actual cause of aliasing is “under sampling”.
 The errors caused by aliasing are known as aliasing artefacts, and they can be
broadly classified into four different types.
45
1. Jagged profiles: This is the most common form of artefact, and occurs when a
smooth boundary is discretized into a “jaggy” one. It is especially noticeable when
there is a high contrast between the two colors.
2. Loss-of-detail: When there is fine detail whose size is less than the size of a pixel,
the detail can be rendered incorrectly, or even disappear completely.
3. Disintegrating textures: This is a related phenomenon to the loss-of-detail. One
important type of detail is texture.
4. Creeping: This is an effect of aliasing that is noticeable in animated scenes. We
saw in the loss-of-detail example that the position of aliased features can change
as well as be distorted.
 If this effect occurs in animated sequences, the position of features may change
between frames, causing objects to “creep” even though they shouldn’t be
noticeably moving.
Anti-aliasing

46
 The term antialiasing refers to any technique (hardware or software) that compensates for the effects of
aliasing.
 There are three possible anti-aliasing algorithms.
 Super-sampling

 Area sampling

 Pixel phasing

Supper-sampling
 The super-sampling technique is also known as postfiltering
 It attempts to compensate for the effects of under-sampling (i.e. reduced resolution) by super-sampling
(increasing the resolution) before plotting.
 To plot an antialiased line, the basic idea of supersampling is as follows:
 Super-sample‟ the image (i.e. increase its resolution).
 Plot the line in the supersampled image.
 Count the number of plotted points within each corresponding „real‟ pixel.
 Plot the real‟ pixel with an intensity that is proportional to the count of supersampled pixels
Supper-sampling
47

 For example, Figure 21 shows a supersampled image (to the left) with a straight-line
plotted in it.
 ‘Real pixels in the original image correspond to a 3x3 block of pixels in the
supersampled image.
 To find the intensity for each original pixel, we count the number of supersampled
pixels that are on‟.
 For the bottom left ‘real pixel there are three plotted supersampled pixels – this is the
maximum number possible, so we plot this pixels with the maximum intensity (in this
case, black).
 For the bottom centre pixel, there is only one supersampled pixel that is ‘on’,
 so we use an intensity that is 1/3 of the maximum.
Supersampling
48

 For the center pixel there are two supersampled pixels that are ‘on, so we
plot this pixel with 2/3 the maximum intensity, and so on.
 The overall effect of this approach is to ‘blur’ the edges of the line slightly,
reducing the staircase effect.
Area Sampling
49

 An alternative antialiasing technique is known as area sampling, or prefiltering.


 The basic idea of area sampling is illustrated in Figure 23(a) ,We determine the location of
the primitive (e.g. a straight line) and then compute the area of overlap of each pixel with
the primitive.
 The plotted intensity of each pixel is proportional to this area of overlap.
 So if the primitive completely covers the pixel, it will be plotted with maximum intensity.
 If it covers 50% of the pixel area, it will be plotted with 50% of the maximum intensity,
and so on.
50

In practice, computing the exact area of overlap can be time consuming.
Therefore a simplified implementation of area sampling approximates the
area by counting supersampled pixels that are inside the primitive.
This is illustrated in Figure 23(b).
 So if there are 6 overlapping supersampled pixels (as in the bottom-left
‘real’ pixel), we say that the area of overlap is approximately 6/9 = 67%.
In fact, this approximation of area sampling is very similar to the
supersampling technique described above.
Pixel Phasing
51

 Supersampling and area sampling were both software techniques, i.e. they were
algorithms that processed image data to produce antialiased image data.
 The final technique, pixel phasing, is a hardware technique.
 Therefore, it is not possible to use pixel phasing to perform antialiasing
unless you are using a display monitor that uses it.
 The basic idea is that the CRT beam is shifted by a fraction of a pixel to bring the plotted
pixels closer to the true mathematical line.
 The hardware normally enables the pixel position to be shifted by 1/2, 1/3, or 1/4 of a
pixel.
 Some systems allow the pixel size to be adjusted too.
 Although this technique can produce impressive results, most monitors do not support it
so specialised hardware is necessary.
OpenGL Anti-Aliasing Functions
52

The OpenGL antialiasing feature can be applied to points, lines or polygons.


We enable the appropriate feature using the glEnable routine.
Forexample:
glEnable(GL_POINT_SMOOTH)
glEnable(GL_LINE_SMOOTH)
glEnable(GL_POLYGON_SMOOTH)
 OpenGL performs antialiasing by colour blending at the edges of primitives.
 Therefore we also need to enable the colour blending feature, as we have already seen in
Section 2.2:
glEnable(GL_BLEND)
Finally we specify the colour blending functions as follows:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
53

You might also like