Contents
1 Introduction
Triangles have been acknowledged as the atoms of shapes for many a year. In Euclidiastes, we find such exhortations as:
"Triangles! Triangles! All is triangles!" says the mathematician.
This pervades literature even to the modern times, as we find in The Geometer who Computed by Herself:
"But still I am the geometer who computes by herself, and all triangles are alike to me."
With such a history, it is unsurprising that present day exponents of the art of computer graphics should turn to triangles when drawing complex shapes on the screen. However, while the Geometer who Computed by Herself would claim that all triangles are alike, sometimes a triangle needs to know that it is part of a larger whole to truly know how to behave.
The simplest shape that a triangle can be a true part of is a quadrilateral, since a quadrilaterals can be built up from two triangles sharing a common side. The claim is, then, that there is a difference between a quadrilateral composed of two triangles, and two triangles that happen to share a common side.
2 Filling in the Evidence
Consider the two quadrilaterals in Figure 1: a parallelogram and a trapezium. Both are composed of two triangles (with the dividing line going from bottom left to top right). The aim is to shade each from left to right, with colour from white to black. This is done by specifying a colour for each vertex of each triangle and interpolating the colour in between. The point here is that each (internal) point of a triangle has a unique expression of the form:
where are the vertices of the triangle and are real numbers satisfying . The numbers , , and can then be used to interpolate any quantity from the corners into the triangle. In particular, if the corners are assigned colours, say , , and , then the point is given the colour
As can be seen, this works well for the parallelogram but not well for the trapezium. There is a distinct line from bottom left to top right showing how the trapezium has been divided into triangles. Indeed, if we split it the other way then we get a different colouring, as in Figure 2.
For the trapezium, a possible solution is to shade it strictly from left to right as in Figure 3. However, then the sides are not uniformly coloured but are themselves shaded. This is not ideal, particularly if the trapezium is itself part of a larger picture, and gets less usable if the quadrilateral is more irregular.
The problem is shown even more clearly if we map a texture, such as a grid, to the quadrilateral as in Figure 4. It is obvious what is happening here. In going from the parallelogram to the trapezium we are distorting one triangle only. The other triangle is left alone.
The grid picture also provides the solution. It is obvious what the picture should look like: the lines running from top to bottom to should gradually change from the left hand edge to the right hand edge but should never have bends, as in Figure 5.
3 Pointing to a Solution
There is no linear transformation that will take an ordinary square or rectangle to a trapezium, or more general quadrilateral. Therefore defining coordinates using linear transformations is not possible and we have to allow for more general coordinate systems.
The thing to notice about Figure 5 is that all of the "vertical" lines have a common intersection point. So to find out which line a point lies on, we need to first compute the intersection point and work from there.
We already know two of the lines: the first and last. So we can work out the intersection point from these.
Let's label the vertices of the trapezium as in Figure 6 with as the intersection point. We also want an arbitrary point inside, say . The whole problem is translation-invariant and so we can assume that one of our points is at the origin. For reasons that will become clear later, we do not want to assume that this is , so we pick one of the vertices. Thus we assume without loss of generality that is at the origin.
The intersection point, , lies on the lines and . It can therefore be written as both and . That is, we look for and such that:
|
We actually only need to know one of them. We can remove the other by a judicious dot product. For any vector, say , let us write for the vector obtained by rotating anticlockwise by . Then and .
We take dot products of both sides of 1 by to get:
which simplifies to:
|
There is a danger that the denominator could be zero. For this to happen, the two lines and would have to be parallel. For now, we shall merely note that this is a special case and assume that it is not true.
Thus the intersection point is:
Now we consider the line . We want to know how far this is from the line towards . To do this, we compute its intersection with the line . This intersection point will have the formula (since is at the origin) and the value of will be the coordinate that we want. Points on the line have the formula so we're looking for:
As before, we use to get rid of the and end up with:
which rearranges to:
|
Before we substitute in for , let us note that as this expression is a fraction, we can multiply top and bottom by the same quantity. Since also involves fractions, this means that we can clear the fractions in . In other words, instead of substituting in the above formula for , we first write:
We ignore the leading fraction, and take the dot product of this with to get:
The numerator, meanwhile, becomes
Hence:
|
By exchanging and , we obtain a formula for the –coordinate. Note that .
|
4 3D
The two formulae, 4 and 5, assume that we are working in with one vertex () at the origin. Our rectangle could be anywhere in –space. Thus our initial data is four –vectors, assumed to live in an affine plane (i.e., a plane not necessarily through the origin). It is easy enough to translate this plane so that one vertex is at the origin by subtracting that vertex from the other three. Dot products are the same irrespective of the ambient space, so the only other ingredient needed is the rotation.
To rotate a vector in a plane, we first find the normal vector to that plane. We can do this using the cross product. This normal vector then defines a rotation of by taking the cross product with it. If we note that every term in 4 and 5 involves exactly two rotated vectors, we don't have to normalise the normal vector since all the lengths will cancel.
Thus the algorithm is as follows:
-
Our input is four –vectors, say , , , . We assume that and are opposite, as are and .
-
We make the replacements , , and .
-
We compute the normal vector and use this to compute , , and .
-
We store the numbers , , and and the vectors , , and (these only need to be computed once for the quadrilateral).
-
For each point inside the quadrilateral, we replace by and compute and according to 4 and 5. Note that the quantities where appears, namely and , can be computed for the vertices and interpolated into the interior of the triangles comprising the quadrilateral by the usual barycentric interpolation.
5 Life on the Edge
We should worry about edge cases. The formulae in 4 and 5 involve fractions and so there is a possibility of the denominators being zero. In addition, the reasoning that led to those formulae depended itself on a certain fraction and we should check what happens if that fraction has zero denominator.
Let's start with this latter case. This is where the denominator in 2 is zero. As stated in the following paragraph, this means that the lines and are parallel meaning that the intersection point is "at infinity". In this case, the formula for the –coordinate becomes
What this formula is doing is projecting both and orthogonally onto the line orthogonal to and measuring the ratio of the image of to the image of . More geometrically, this draws lines parallel to through and and finds their intersection points with the line orthogonal to , then measures the height of the point corresponding relative to that corresponding to . This is exactly what the –coordinate should be in this situation.
The next term to worry about in 4 is the fraction . The denominator of this, , is zero if and are parallel. This means that the quadrilateral is degenerate and is actually a triangle (possibly itself further degenerate) as in Figure 8.
The solution here is straightforward: cycle the vertices round so that the base vertex (viewed as the origin) is opposite the degenerate vertex. Note also that with as the degenerate vertex, with the triangulation scheme then the triangle is not rendered. Rotating the labels means that the quadrilateral is divided more equitably between the two triangles.
The decision as to whether to permute the vertices can be taken at the outset, and should be based on the relative sizes of the angles at the vertices. To avoid the situation above, the best vertex to choose as the origin is the one opposite the largest internal angle.
The last danger is with the full denominator in 4. As we have already considered the quantities and , we can assume that these are non-zero and thus that the denominator in 4 is a multiple of . If this is zero, and are parallel.
Let us restrict our attention to the case of a convex quadrilateral. Then while our quadrilateral may not be as regular as the trapezium of Figure 6, it nevertheless shares some basic properties with it. In particular, it is contained within the cone emanating from and bounded by the lines and . In particular, every point in the quadrilateral has the form:
for some in the line segment and . Then has the formula (since is the origin) with . In particular, there is some and such that
Rearranging, . Now if we assume that is parallel to , we find that either or is itself a multiple of .
In the first case, the intersection of and is inside the quadrilateral and hence (by convexity) we must have either or and the quadrilateral is degenerate. In the second case, the points are collinear, but so are and and this means that the quadrilateral in fact collapses to a line.
In conclusion, for convex quadrilaterals then the danger cases all correspond to degenerate cases which can either be dropped altogether (as they won't render) or a simple modification of the initial data removes the degeneracy. To accomplish this, we add a step to the algorithm between the first and second which decides on the best vertex to choose as the anchor.
6 Go Fly a Kite
The above concentrates on convex quadrilaterals. It is possible to consider non-convex quadrilaterals such as darts. However, the correct grid would pass out of the shape and therefore would not be fully renderable. As pleasing as the ideal shape is, as in Figure 9, the fact that only the part in the dart would render means that it would not look like this on a screen.
Thus although the formulae still produce something for non-convex quadrilaterals, they really only make sense when they are convex.
7 Examining the Edges
The algorithm above was constructed to fix the issue of interpolation when triangles are part of a larger quadrilateral. Unfortunately, as a correspondent pointed out to me, it itself suffers a similar failing when the quadrilaterals are themselves part of a larger shape. The problem is shown in Figure 10 where the lines do not match (for simplicity, only the "vertical" lines are shown).
It does, though, have linear interpolation along two sides: the two adjacent to the "chosen" vertex (i.e., the one we translate to the origin). This is because the, say, –coordinate is determined by intersecting the line with the line and looking at the ratio of the intersect to the whole segment. If instead we intersected the line with the line we would force linear interpolation on the opposite face.
A balanced approach is to interpolate between these two –values as we sweep from the line to the line . To achieve this, we need a parameter that varies from on the line to on the line . The ideal such parameter would be the angle subtended at the intersection of the lines and . However, computing this parameter involves an inverse trigonometric function and thus is more expensive than warranted. A cheaper parameter to use is the computed –coordinate. As we've introduced two –coordinates (corresponding to the lines and ), we similarly have two –coordinates. Again, we could look for a "best" –coordinate or a cheap one. Probably the cheapest non-biased one is to take the average of the two computed –coordinates.
Equations 4 and 5 now become the computations of and . We need similar formulae to compute the values of and at the opposite edges.
Repeating the argument that led to Equation 4, we see that we're looking for the point on the line which is also on the line . That is:
We use the same argument as before. We observe that:
and so taking dot products with kills the term to leave:
Removing the obvious zeros, we are left with:
which rearranges to:
Swapping and yields the corresponding result for .
Armed with we compute the final values as:
8 Simplification
Much of the detail in the formulae can be simplified with a little bit of pre-computation. We already translate so that is at the origin. By further transforming to a coordinate system in which and are and respectively we can make the per-pixel computations very simple.
In this coordinate system, the only vector to be determined is the image of which we write as . That is, . The formulae for and are:
The position vector, , also needs to be computed in this coordinate system. Let us write in this system.
In the new system then , , , and . Then also , , and . Using these, we have the following formulae for and :
The numerators and denominators of these fractions are all affine in and therefore can be computed for the vertices and interpolated in to the triangles making up the quadrilateral. As we're now computing the numerators and denominators only for the vertices, we can use the fact that the vertices are , , , and in the new coordinate system, meaning , , , and . Thus we assign vectors at each vertex from which we compute the numerators and denominators. As the numerators are the same for each and for each we only need to list them once.
-
At , the numerator is ; the denominator is .
-
At , the numerator is ; the denominator is .
-
At , the numerator is ; the denominator is .
-
At , the numerator is ; the denominator is .
At the per-pixel level the division happens. Following the division, we compute the actual and values using the formulae:
Careful examination of these formula shows that to lessen the per-pixel calculations, in place of the above numerators for and we should compute and . Then with and we have:
9 Code
function setup()
m = mesh()
v = {vec2(100,100),vec2(200,0),
vec2(100,200),vec2(300,200)}
m.texture = "Cargo Bot:Codea Icon"
m.shader = shaderGradientFill()
setGFpoints(m,v)
m.shader.colours = {
color(0, 0, 0, 255),
color(255, 0, 0, 255),
color(0, 50, 255, 255),
color(255, 255, 255, 255)}
mm = mesh()
mm.vertices = {v[1],v[2],v[4],v[1],v[3],v[4]}
mm.colors = {
color(0, 0, 0, 255),
color(255, 0, 0, 255),
color(255, 255, 255, 255),
color(0, 0, 0, 255),
color(0, 50, 255, 255),
color(255, 255, 255, 255)
}
mm.texCoords = {
vec2(0,0),
vec2(1,0),
vec2(1,1),
vec2(0,0),
vec2(0,1),
vec2(1,1),
}
mm.texture = "Cargo Bot:Codea Icon"
end
function draw()
background(40,40,50)
m:draw()
translate(0,HEIGHT/2)
mm:draw()
end
function touched(t)
if t.state == BEGAN then
t = vec2(t.x,t.y)
local d = v[1]:distSqr(t)
c = 1
for k,u in ipairs(v) do
if u:distSqr(t) < d then
d = u:distSqr(t)
c = k
end
end
it = t
ip = v[c]
else
v[c] = ip - it + vec2(t.x,t.y)
mm.vertices = {v[1],v[2],v[4],v[1],v[3],v[4]}
setGFpoints(m,v)
end
end
function setGFpoints(m,a,b,c,d)
if type(a) == "table" then
a,b,c,d = unpack(a)
end
m.vertices = {a,b,d,a,c,d}
b,c,d = b-a, c-a, d-a
local bh,ch,dh
if b.z then
local n = b:cross(c)
bh,ch,dh = n:cross(b), n:cross(c), n:cross(d)
else
a = vec3(a.x,a.y,0)
b = vec3(b.x,b.y,0)
c = vec3(c.x,c.y,0)
d = vec3(d.x,d.y,0)
bh = vec3(-b.y,b.x,0)
ch = vec3(-c.y,c.x,0)
dh = vec3(-d.y,d.x,0)
end
local bch = b:dot(ch)
local cc = b:dot(ch - dh)/d:dot(ch)
local bb = c:dot(bh - dh)/d:dot(bh)
m.shader.origin = a
m.shader.bh = bh
m.shader.ch = ch
m.shader.bch = bch
m.shader.cc = cc
m.shader.bb = bb
end
function shaderGradientFill ()
return shader([[
uniform mat4 modelViewProjection;
uniform vec3 origin;
uniform vec3 bh;
uniform vec3 ch;
attribute vec4 position;
varying highp float vb;
varying highp float vc;
void main()
{
vb = dot(position.xyz/position.w-origin,bh);
vc = dot(position.xyz/position.w-origin,ch);
gl_Position = modelViewProjection * position;
}
]],[[
precision highp float;
uniform lowp sampler2D texture;
uniform float bch;
uniform float bb;
uniform float cc;
uniform vec4 colours[4];
varying highp float vb;
varying highp float vc;
void main()
{
float y = vb/(cc*vc - bch);
float x = vc/(bb*vb + bch);
lowp vec4 tex = texture2D( texture, vec2(x,y) );
lowp vec4 col = (1.-y)*(1.-x)*colours[0] + (1.-y)*x*colours[1] + y*(1.-x)*colours[2] + y*x*colours[3];
gl_FragColor = col*tex;
}
]])
end