19.繪製一條直線:在繪製直線時決定去填充哪些像素

19.Drawing in a Straight Line: Deciding which pixels to fill when drawing a line

On paper, drawing a straight line between two points is easy: Put your ruler down on the page, and run your pencil across. In contrast, drawing a straight line on a computer screen is a matter of deciding which pixels to colour in (known as rasterization). It can be thought of as drawing a pencil line on squared graph paper, and colouring in any squares which the line passes through:

如果在紙上,在兩點之間繪製一條直線是很容易的:把尺子放在紙上,然後用鉛筆划過去。相反地,在電腦屏幕上繪製一條直線是決定哪些像素需要着色的問題(被稱爲光柵化)。它可以被想象爲在方格繪圖紙上用鉛筆繪製一條直線,並且給直線穿過的格子塗上顏色:

But how do you practically do this? One way is to move along the line in small increments, and colour the square you are in after each movement. But if your increment isn’t small enough you can miss some squares which the line barely touches — and if your increment is too small, it can be quite an inefficient exercise.

但是你怎樣實際操作呢?一種方法是以很小的增量沿着直線移動,在每一步移動之後爲你到達的方格着色。但是如你的果增量不是足夠小的話你會漏掉一些直線剛剛觸到的方格——而且如果你的增量太小的話,那將是十分沒有效率的工作。

This post will explain how to draw a line on a computer screen. There are several fast line-drawing algorithms:Bresenham is quite famous, andWu can do anti-aliasing. I’m going to explain an equivalent to Wu’s algorithm, but without the anti-aliasing.

這篇帖子將解釋如何在電腦屏幕上繪製直線。這兒有許多高速的直線繪製算法:Bresenham是非常有名的,而Wu能夠進行反混淆。我將解釋等同於Wu的算法,但是不使用反混淆。

The Algorithm

算法

Every two-dimensional line can be put into one of three categories:

  1. It’s longer in the Y dimension than in X.
  2. It’s the same length in X and Y (and thus is at 45 degrees).
  3. It’s longer in the X dimension than in Y.

每個二維直線可以歸爲以下三種情況:

   1. Y維長度大於X

   2. X和Y的長度相等(因而呈45度角)

     3. X維長度大於Y

Thus we can always pick the longest dimension and call it the major axis, with the other axis being the minor axis. (In the middle case, you can pick X or Y for the major axis, it won’t matter.) Here’s the first part of our key insight for our line-drawing algorithm: by definition, if you move 1 pixel on the major axis, you’ll move at most 1 pixel in the minor axis. After all, if you moved further in the minor axis than major, you have labelled them wrong. Here’s some diagrams to illustrate:

因此我們可以始終選取最長的維度並稱其爲長軸,而另一個軸稱爲短軸。(在中間的情況下,你可以選取X或者Y作爲長軸,這沒有關係)下面是我們直線繪製算法的第一個關鍵要點:根據定義,如果你在長軸上移動一個像素距離,你將最多在短軸上移動一個像素距離。畢竟,如果你在短軸上的移動距離超過了長軸,你便已經犯錯了。以下用圖表來演示:

The major axis is red, the minor axis is blue. In the bottom-right picture, we could have chosen either X or Y to be the major axis.

長軸是紅色,短軸是藍色。在右下方的圖片裏,我們可以選擇X或Y作爲長軸。

And here’s the second part of our insight: if you move 1 pixel in the major axis, and thus at most 1 pixel in the minor axis, you will always touch at most two pixels on the minor axis. You can see this in the diagram above. Trace one pixel in the major axis. To touch three pixels, you would need to begin in pixel A, then move into pixel B, out of pixel B and into pixel C. That would require moving more than one pixel in the minor axis, which we’ve already seen would mean you’ve labelled them wrong.

下面是第二個要點:如果你在長軸上移動一個像素距離,因而最多在短軸上移動一個像素距離,那麼你將始終在短軸上最多接觸到兩個像素。你可以在上圖中觀察這個特點。在長軸上追蹤一個像素。爲了接觸到三個像素,你可能需要從像素A出發,接着移動到像素B,然後離開像素B到達像素C。那將可能需要在短軸上移動超過一個像素的距離,我們已經知道這意味這你已經犯錯了。

So we can write our line-drawing algorithm so that it advances 1 pixel in the major axis, and then just works out which 1 or 2 pixels it touched on the minor axis, then go again.

於是我們可以寫出直線繪製算法,它在長軸上前進一個像素距離,接着只要算出它在短軸上接觸到了哪一個或兩個像素,如此往復。

Specific Case

特殊情況

To begin with, here’s the code for the case when the X axis is the major axis, and the start point is to the left of the end point. We fill the start and end pixels as a special case, because we know straight away that the line must pass through them:

作爲開始,以下代碼適合於當X軸是長軸的情況,並且起點在終點的左邊。我們將起點和終點像素作爲特殊的情況來填充,因爲我們很清楚直線必定通過它們:

        fillPixel(lineStartX, lineStartY);
        
        if (lineStartX == lineEndX && lineStartY == lineEndY)
            return;
        
        double slope = (double)(lineEndY - lineStartY) / (double)(lineEndX - lineStartX);
        fillPixelsLine(lineStartX, lineEndX, lineStartY, slope);
        
        fillPixel(lineEndX, lineEndY);

All the other pixels in the line are filled by the fillPixelsLine function:

直線上的所有其他像素使用fillPixelsLine 方法來填充:

    private void fillPixelsLine(int startX, int endX, int startY, double slope)
    {
        double curY = startY + 0.5 + (0.5 * slope);
        for (int curX = startX + 1; curX != endX; curX++)
        {
            fillPixel(curX, (int)Math.floor(curY));

            double newY = curY + slope;            
            if (Math.floor(newY) != Math.floor(curY))
            {
                fillPixel(curX, (int)Math.floor(newY));
            }
            curY = newY;
        }
    }

The above function is passed the coordinates of the starting pixel, but actually begins on the next pixel. This is effectively the top-left corner of a pixel, but the line starts in the middle. So the “+ 0.5 + 0.5 * slope” for curY starts the Y coordinate in the middle of the pixel (the “+ 0.5″), then works out how much it would be at the end of the pixel:

上面的方法傳入了起始像素的兩個座標值,但事實上是從下一個像素開始工作的。傳入的實際上是像素左上角的座標值,但直線是從中心開始繪製的。於是curY變量的“+ 0.5 + 0.5 * slope”表示從像素中心的Y座標(“+ 0.5″ )開始算起,當直線到達像素末尾時值是多少:

The loop begins by filling a pixel (using the Y at the start, or left-hand edge, of the current column). Then it advances the Y to its next value (the Y at the end, or right-hand edge of the current column) — if this is a different pixel from the earlier one, it is also filled:

循環從填充一個像素開始(使用起點,或是當前列左邊緣的Y座標值)。接着將Y座標增加到它的下一個值(終點,或時當前列右邊緣的Y座標值)——如果這是與前一個像素不同的像素,則填充之:

We tell if the pixel is the same by looking at the integer part using Math.floor: In the above diagram, Math.floor(10.7) is 10, and Math.floor(11.3) is 11, so because those are not equal, we can tell that we have crossed the pixel boundary (located at 11.00).

我們可通過觀察其值的整數部分來判斷是否爲同一個像素,這可使用Math.floor方法:在上圖中,Math.floor(10.7) 是10,而Math.floor(11.3) 是11,於是因爲它們不相等,所以我們可以說直線跨過了像素的邊界(位於11.00處)。

Generalising

推廣

The above code is specialised for the case that the X axis is the major axis, and the line heads in the positive X direction. To generalise the code to handle all cases, we need to add a bit of extra parameterisation to the code:

以上代碼適用於當X軸爲長軸,而直線朝向X正方向的特殊情況。爲了推廣代碼去處理所有情況,我們爲代碼需要添加一些額外的參數:

    private void drawLine(int lineStartX, int lineStartY, int lineEndX, int lineEndY)
    {
        fillPixel(lineStartX, lineStartY, true);
        
        if (lineStartX == lineEndX && lineStartY == lineEndY)
            return;
            
        fillPixel(lineEndX, lineEndY, true);
        
        if (Math.abs(lineEndX - lineStartX) >= Math.abs(lineEndY - lineStartY))
            fillPixels(lineStartX, lineEndX, lineStartY, (double)(lineEndY - lineStartY) / (double)(lineEndX - lineStartX), true);
        else
            fillPixels(lineStartY, lineEndY, lineStartX, (double)(lineEndX - lineStartX) / (double)(lineEndY - lineStartY), false);
    }
    
    private void fillPixels(int start, int end, int startMinor, double slope, boolean horizontal)
    {
        int advance = end > start ? 1 : -1;
        double curMinor = startMinor + 0.5 + (0.5 * advance * slope);
        for (int curMajor = start + advance; curMajor != end; curMajor += advance)
        {
            fillPixel(curMajor, (int)Math.floor(curMinor), horizontal);

            double newMinor = curMinor + (advance * slope);            
            if (Math.floor(newMinor) != Math.floor(curMinor))
                fillPixel(curMajor, (int)Math.floor(newMinor), horizontal);
            curMinor = newMinor;
        }
    }
    
    private void fillPixel(int major, int minor, boolean horizontal)
    {
        if (horizontal) // X is major
            addObject(new Box(), major, minor);
        else // Y is major
            addObject(new Box(), minor, major);
    }

The two added parameters are the horizontal boolean, which indicates whether major is X or Y, and the advance constant, which allows you to go in a negative direction along the major axis. The principle of the code is exactly the same.

所添加的兩個參數是布爾變量horizontal ,它表示長軸是否爲X或者Y,以及常量advance,它允許你沿着長軸向負方向前進。代碼的原理是完全相同的。

Summary

小結

You can see the line-drawing in action — left-click to set the start point, and right-click to set the end point.

Our line-drawing algorithm is perhaps a bit unusual, because it colours inany pixel the line touches, which Bresenham does not. (Wu does, but draws in different transparencies, to implement anti-aliasing.) However, in the next few posts I’m going to show you other uses of this algorithm, besides line-drawing, which require this behaviour.

你可以看看直線繪製的執行情況——點擊鼠標左鍵去設置起點,點擊右鍵去設置終點。

我們的直線繪製算法可能有一點不常見,因爲它給直線接觸到的所有像素進行着色,而Bresenham 算法則沒有。(Wu算法做了,但是用不同的透明度繪製的,以便進行反混淆。)然而,除了進行直線繪製,在接下來的幾篇帖子裏我打算去展示這個算法的其它功用,它們需要藉助這個原理。

發佈了0 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章