02: Clipping lines

So far, we are able to draw any line that we want... with a caveat. What happens, if we specify a point of the line outside of the image? In the current version, the script will just crash, since you will try to write values outside of the allowed range. This section will solve that issue.

Clipping refers to the process of restricting our drawing operations to a certain region only and to "clip" away anything not in that region.

As screens or windows are generally rectangular and rectangles have nice geometric properties, it makes sense, that an efficient algorithm exists to clip lines with the screen.

A well-known algorithm for this is called the Cohen–Sutherland algorithm [1], which we will implement.

We will split up the procedure into two parts:

  1. Determine where a point lies with respect to a rectangle/the screen
  2. Use the previous information to clip the line

You can find the full rasterization code here: Rasterizer 02

Region codes

In the first step for implementing the Cohen-Sutherland we will assign codes to points based on where they lie with respect to a rectangle. This information can then be used to check, if a line needs to be clipped.

There are 4 lines making up a rectangle and we use those for classification. We identify four regions around a rectangle, two mutually exclusive pairs:

  1. Top and bottom
  2. Left and right

These can of course be mixed, so a point could be above and to the left of a rectangle, but it can't be above and below at the same time.

We encode those four regions the following way: We use 4 bits b3b2b1b0b_3 b_2 b_1 b_0. A bit is set to 11, if a point is in the corresponding region, and 00 otherwise, where we arbitrarily use the following order:

  1. Top
  2. Bottom
  3. Right
  4. Left

A point to the bottom right would then be 01100110.

What code do we get, when a point is inside the rectangle?

[[0000]]

This bit representation is useful, in that we can combine it with fast bit operations. The bottom right point is just the logical or operation between Bottom (00100010) and Right (01000100), which is usually written with the |-operator:

code = BOTTOM | RIGHT

We can check, if any of those bits is set (=1=1), by performing a logical and operation with a number containing 11 at the bit positions we are interested in and checking the result, usually written with the & operator.

This whole technique is called a bitmask or bitflags.

The operation to check, if all of the bits set in a variable FLAGS are set, you can use the following:

if((code & FLAGS) === FLAGS) {...}

FLAGS could for example just be BOTTOM or any combination of the four regions.

If we just interpret the bits as digits in a binary number, we can write them down in the decimal system as:

  1. Top =0001=110= 0001 = 1_{10}
  2. Bottom =0010=210= 0010 = 2_{10}
  3. Right =0100=410= 0100 = 4_{10}
  4. Left =1000=810= 1000 = 8_{10}

The actual values don't really matter though, the bits they use up just need to be disjoint.

NOTE: In JavaScript, all numbers are floating point numbers. The bitwise operations |,& still work. Internally, the variables are converted to 32-bit integers and the operations are applied. This will introduce some additional overhead, but shouldn't really be noticeable in the grand scheme of things.

One way to specify a rectangle is to specify the lower left point and the top right one. We can use that to very easily find the codes as follows:

code = 0;
if(point is smaller than x_min) {
    code = code | LEFT;
}
if(point is greater than x_max) {
    code = code | RIGHT;
}
if(point is smaller than y_min) {
    code = code | BOTTOM;
}
if(point is greater than y_max) {
    code = code | TOP;
}

We will implement this functionality below!

We specify a rectangle in the middle of the frame and color each of the 8 (including the overlaps) with a different color.

Exercise:

Solution:

See the solution

Next we will do the actual clipping by using our region code.

Clipping the lines at the screen

Now that we can efficiently check, where a point lies in relation to a rectangle (for example our screen), we can use that for clipping.

The basic idea is to use the codes of the line endpoints to quickly check if we need to clip the line.

There are three cases to consider, with the third having sub-cases:

  1. The line is fully inside -> No clipping needed
  2. The line is fully outside -> Line doesn't need to be drawn
  3. Line crosses one of the four lines going through the rectangle sides

The first case happens when both points are inside the rectangle. In this case, the region code of both points is 00000000. So we can quickly compute the or operation of the codes of both endpoints. This effectively "gathers" all 11 s that appear in the codes. If the result of that or is still 00, the line is inside and we can return both points.

For the second case, we think about when we can be absolutely sure, that a line will be outside the rectangle. Two objects do not intersect, if we can find a line that separates them (this is a case of the so called Separating Axis Theorem, SAT). We have information about four lines, the rectangle sides! If a line is completely on the left, right, bottom or top, then it can't intersect the rectangle. When you look at how the codes are constructed, we can very easily check for that! If the line lines completely on one side, then both point codes will share a 11 at the same location! So we check for that case by combining both codes with and and then checking if the result is not zero. If it is, the line is fully clipped and does not need to be drawn.

The third case is slightly more involved. At least one point of the line will be outside the rectangle and the line must cross at least one line. To make things easier, we just consider one of the points. As 00 corresponds to a point inside, we choose the larger code of the two, thus ensuring we have the point outside. Then we get four cases again:

  1. Point is at the top -> The clipped yy-coordinate is the maximum yy-coordinate. Calculate xx.
  2. Point is at the bottom -> The clipped yy-coordinate is the minimum yy-coordinate. Calculate xx.
  3. Point is to the right -> The clipped xx-coordinate is the maximum xx-coordinate. Calculate yy.
  4. Point is at the left -> The clipped xx-coordinate is the minimum xx-coordinate. Calculate yy.

After we have resolved one of these cases, we replace the point corresponding to the greater code with the newly clipped coordinates. Then we update that points region code and redo the loop.

When everything is clipped, the line will be fully inside and thus the loop terminates.

The last missing piece is how to compete the missing coordinate in these four cases.

We will show how to get the equation for the first case. We have a line defined by the two endpoints A\mathbf{A} and B\mathbf{B}.

We can write the line as

p(t)=A+t(BA)=(AxAy)+t(BxAxByAy)\begin{align*} \mathbf{p}(t) &= \mathbf{A} + t(\mathbf{B} - \mathbf{A}) \\ &= \begin{pmatrix}A_x \\ A_y \end{pmatrix} + t \begin{pmatrix}B_x - A_x \\ B_y - A_y\end{pmatrix} \end{align*}

tt describes where we are on the line and for t[0,1]t\in [0,1], we have the segment between the two points (check by plugging 00 and 11 in). From the equation, we see that the xx and yy coordinate moves with the same tt parameter, so if we have the one that gets us the ymaxy_{\text{max}}, the same one will give us the corresponding xx-coordinate. So let's find tt:

ymax=Ay+t(ByAy)ymaxAy=t(ByAy)ymaxAyByAy=t\begin{align*} y_{\text{max}} &= A_y + t (B_y - A_y) \\ y_{\text{max}} - A_y &= t(B_y - A_y) \\ \frac{y_{\text{max}} - A_y}{B_y - A_y} &= t \end{align*}

With that found, we can plug it back in to get the missing xx-coordinate.

px=Ax+t(BxAx)=Ax+ymaxAyByAy(BxAx)=Ax(ByAy)+(ymaxAy)(BxAx)ByAy=AxByAxAyAy(BxAx)+ymax(BxAx)ByAy=AxByAxAyAyBx+AxAy+ymax(BxAx)ByAy=AxByAyBx+ymax(BxAx)ByAy\begin{align*} p_x &= A_x + t (B_x - A_x) \\ &= A_x + \frac{y_{\text{max}} - A_y}{B_y - A_y} (B_x - A_x) \\ &= \frac{A_x(B_y - A_y) + (y_{\text{max}} - A_y)(B_x - A_x)}{B_y - A_y} \\ &= \frac{A_x B_y - A_x A_y - A_y(B_x - A_x) + y_{\text{max}}(B_x - A_x)}{B_y - A_y} \\ &= \frac{A_x B_y - A_x A_y - A_y B_x + A_x A_y + y_{\text{max}}(B_x - A_x)}{B_y - A_y} \\ &= \frac{A_x B_y - A_y B_x + y_{\text{max}}(B_x - A_x)}{B_y - A_y} \\ \end{align*}

In the solution, the second line is chosen for the implementation, as it is a bit more understandable. Bonus: If you know the formula for the normal of a 2D vector and the implicit equation defining a 2D line, you might express this in terms of n\mathbf{n} and dd.

As the other cases work the same, just with minimum and maximum or xx and yy switched, we will only list the results for the 4 cases here:

  1. Point is at the top -> y=ymax,x=Ax+ymaxAyByAy(BxAx)y = y_{\text{max}}, x = A_x + \frac{y_{\text{max}} - A_y}{B_y - A_y} (B_x - A_x)
  2. Point is at the bottom -> y=ymin,x=Ax+yminAyByAy(BxAx)y = y_{\text{min}}, x = A_x + \frac{y_{\text{min}} - A_y}{B_y - A_y} (B_x - A_x)
  3. Point is to the right -> x=xmax,y=Ay+xmaxAxBxAx(ByAy)x = x_{\text{max}}, y = A_y + \frac{x_{\text{max}} - A_x}{B_x - A_x} (B_y - A_y)
  4. Point is at the left -> x=xmin,y=Ay+xminAxBxAx(ByAy)x = x_{\text{min}}, y = A_y + \frac{x_{\text{min}} - A_x}{B_x - A_x} (B_y - A_y)

Now, you can implement the full procedure below! The code includes previously written code, with small alterations to call the clipping in the actual rasterization of the line.

We added a bit more information to the Pipeline, to specify the drawing region. It is called a viewport and is a rectangular region with a bottom left origin and a width and height.

class Pipeline {
    constructor({
        viewport = {
            x: 0,
            y: 0,
            w: 0,
            h: 0
        },
        framebuffer = Framebuffer.new(),
    } = {}) {
        this.viewport = viewport;
        this.framebuffer = framebuffer;
    }
}

The solution can once again be seen hidden after that.

Exercise:

Solution:

You can run it to check your result.

See the solution

  1. Robert F. Sproull and Ivan E. Sutherland. 1968. A clipping divider. In Proceedings of the December 9-11, 1968, fall joint computer conference, part I (AFIPS '68 (Fall, part I)). Association for Computing Machinery, New York, NY, USA, 765–775. https://doi.org/10.1145/1476589.1476687 ↩︎