实现扫描线算法

Posted

技术标签:

【中文标题】实现扫描线算法【英文标题】:Implementing a scanline algorithm 【发布时间】:2017-02-17 17:52:46 【问题描述】:

我的任务是为作业实施扫描线算法的一个版本。该程序通过从文本文件中读取顶点和颜色列表来工作。一次从队列中弹出三个顶点和三种颜色,然后绘制三角形的边。到目前为止,我的程序非常完美地做到了这一点。在此之前,我们的任务是绘制矩形,运行测试后立即清楚地表明,程序需要翻转 y 坐标才能正确绘制图像。

这就是问题所在。我找到了一些用于扫描线三角形填充函数here 的算法,但我注意到我必须修改它们才能考虑到图像被翻转的事实。在计算机图形网格中,较高的 y 值朝向底部。在组织给定三角形的三个顶点时,通常的做法是将 y 值最低的顶点指定为顶部,将 y 值最高的顶点指定为底部。为了解决图像翻转问题,我将其倒置。

无论我做什么,我都无法正确填充图像。只要在绘制单个像素时不翻转 y 值并且顶点以较低 y 值朝向顶部的预期方式排序,它将按原样使用这些函数。但是,生成的图像会垂直翻转。

以下功能几乎是整个程序,除了我认为不需要任何修正的线条图。

void FrameBuffer::drawTriangle(const Vertex& v0, const Vertex& v1, const Vertex& v2, const Color& c0, const Color& c1, const Color& c2, Functional status) 
    //Start by organizing vertices from top to botttom (need to rearrange colors as well)
    Vertex vTop, vMid, vLow;
    Color cTop, cMid, cLow;

    vTop = v0;  cTop = c0;
    if (v1[Y] > vTop[Y]) 
        vMid = vTop;    cMid = cTop;
        vTop = v1;      cTop = c1;
    
    else 
        vMid = v1;      cMid = c1;
    

    if (v2[Y] > vTop[Y]) 
        vLow = vMid;    cLow = cMid;
        vMid = vTop;    cMid = cTop;
        vTop = v2;      cTop = c2;
    
    else if (v2[Y] > vMid[Y]) 
        vLow = vMid;    cLow = cMid;
        vMid = v2;      cMid = c2;
    
    else 
        vLow = v2;      cLow = c2;
    

    //Draw the edges of the triangle
    drawLine(vTop, vMid, cTop, cMid, status);
    drawLine(vTop, vLow, cTop, cLow, status);
    drawLine(vMid, vLow, cMid, cLow, status);

    //Fill the triangle; first case is flat bottom
    if (vMid[Y] == vLow[Y]) 
        fillFlatBottom(vTop, vMid, vLow, status);
    
    //Second case is flat top
    else if (vTop[Y] == vMid[Y]) 
        fillFlatTop(vTop, vMid, vLow, status);
    
    //General case; triangle can be split into flat bottom above a flat top
    else 
        Vertex vNew;        //This represents the point on the triangle across from the "midpoint"
        vNew[Y] = vMid[Y];
        vNew[X] = vTop[X] + ((vMid[Y] - vTop[Y]) / (vLow[Y] - vTop[Y])) * (vLow[X] - vTop[X]);
        vNew[Z] = (vLow[Z] * (vNew[X] - vTop[X]) * (vNew[Y] - vMid[Y]) + vTop[Z] * (vNew[X] - vMid[X]) * (vNew[Y] - vLow[Y]) + vMid[Z] * (vNew[X] - vLow[X]) * (vNew[Y] - vTop[Y]) - vMid[Z] * (vNew[X] - vTop[X]) * (vNew[Y] - vLow[Y]) - vLow[Z] * (vNew[X] - vMid[X]) * (vNew[Y] - vTop[Y]) - vTop[Z] * (vNew[X] - vLow[X]) * (vNew[Y] - vMid[Y])) / ((vNew[X] - vTop[X]) * (vNew[Y] - vMid[Y]) + (vNew[X] - vMid[X]) * (vNew[Y] - vLow[Y]) + (vNew[X] - vLow[X]) * (vNew[Y] - vTop[Y]) - (vNew[X] - vTop[X]) * (vNew[Y] - vLow[Y]) - (vNew[X] - vMid[X]) * (vNew[Y] - vTop[Y]) - (vNew[X] - vLow[X]) * (vNew[Y] - vMid[Y]));

        fillFlatBottom(vTop, vMid, vNew, status);
        fillFlatTop(vMid, vNew, vLow, status);
    


//Draw from bottom up (something is happening here that causes errors)
void FrameBuffer::fillFlatTop(const Vertex& v0, const Vertex& v1, const Vertex& v2, Functional status) 
    double xSlope1 = -(v2[X] - v0[X]) / (v2[Y] - v0[Y]);
    double xSlope2 = -(v2[X] - v1[X]) / (v2[Y] - v1[Y]);

    Vertex curV0 = v2;
    Vertex curV1 = v2;

    //v0 is the highest point with the highest y value and v2 is the lowest (for this case) with the lowest y value
    while (curV0[Y] < v0[Y] && curV1[Y] < v0[Y]) 
        Color curC0 = image.get(curV0[X], curV0[Y]);
        Color curC1 = image.get(curV1[X], curV1[Y]);

        drawLine(curV0, curV1, curC0, curC1, status);

        curV0[Y] += 1;  curV0[X] -= xSlope1;
        curV1[Y] += 1;  curV1[X] -= xSlope2;
    


//Draw from top down (something is happening here that causes errors)
void FrameBuffer::fillFlatBottom(const Vertex& v0, const Vertex& v1, const Vertex& v2, Functional status) 
    double xSlope1 = -(v1[X] - v0[X]) / (v1[Y] - v0[Y]);
    double xSlope2 = -(v2[X] - v0[X]) / (v2[Y] - v0[Y]);

    Vertex curV0 = v0;
    Vertex curV1 = v0;

    //v0 is the highest point with the highest y value and v1 (for this case) is the lowest with the lowest y value
    while (curV0[Y] >= v1[Y] && curV1[Y] >= v1[Y]) 
        Color curC0 = image.get(curV0[X], curV0[Y]);
        Color curC1 = image.get(curV1[X], curV1[Y]);

        drawLine(curV0, curV1, curC0, curC1, status);

        curV0[Y] -= 1;  curV0[X] += xSlope1;
        curV1[Y] -= 1;  curV1[X] += xSlope2;
    

为了提供一些视角,这是未填充任何内容时的结果图像:http://imgur.com/a/VOpWJ

仅当 fillFlatBottom 运行时会发生这种情况:http://imgur.com/a/nexR9

仅当 fillFlatTop 运行时会发生这种情况:http://imgur.com/a/flRCK

我到底做错了什么?很明显,线算法不会导致这个问题。我要么错误地计算了中点对面的点(即 vNew),要么我以某种方式弄乱了填充算法。

【问题讨论】:

【参考方案1】:

现在让我们看看其中一个函数:fillFlatTop。这个想法是让CurV0curV1这两个点沿着三角形的两侧分别从最底部的顶点v2移动到顶部的顶点v0v1

让我们检查一下:

对于curV0,随着yv2 变为v0y 更改为v0.y - v2.y。但是,您希望x 更改为等于v0.x - v2.x,因此对于y 中的每个1 更改,您希望拥有(v0.x - v2.x) / (v0.y - v2.y)

对于curV1,同上,但将0s 替换为1s。

如果您查看您的代码,斜率是正确的(有一个双重否定,但结果是正确的)。

现在,让我们看看fillFlatBottom。同上:

对于 curV0v0 移动到 v1y 的变化是 v1.y - v0.y,对应于 v1.x - v0.xx 变化,因此对于 1 的每个变化987654350@,您希望 x 更改为 (v1.x - v0.x) / (v1.y - v0.y)

对于curV1,同上,用2s代替1s

将此与您的代码进行比较,您的斜率似乎有错误的符号,或者如果您想使用与fillFlatTop 相同的约定,您应该保持斜率不变,但更改增量( x += ...) 减少 (x -= ...),再次获得双重否定:-)

【讨论】:

【参考方案2】:

这是我用来绘制实心三角形的代码。它基于此页面上描述的算法:Triangle Fillers .该代码是基于模板的类的一部分,它使用标准库和一些图像容器,但您可以轻松修改代码以满足您的需求:

struct spFPoint
    
    float x;
    float y;
    ;

//-----------------------
// draw triangle filled
//-----------------------
  template <class T>
  static void TriangleFilled(spImage<T> *img, T val, int x1, int y1, int x2, int y2, int x3, int y3)
    
    // this piece of code is used only for sorting  
    spFPoint pt;
    pt.x = x1;
    pt.y = y1;
    vector <spFPoint> triangle;
    triangle.push_back(pt);
    pt.x = x2;
    pt.y = y2;
    triangle.push_back(pt);
    pt.x = x3;
    pt.y = y3;
    triangle.push_back(pt);
    stable_sort(triangle.begin(), triangle.end(), yAscFPoint);
    // here comes the actual algorithm
    spFPoint A, B, C;
    float   dx1, dx2, sx1, sx2, y;
    A = triangle[0];
    B = triangle[1];
    C = triangle[2];
    B.y-A.y > 0 ? dx1 = (B.x-A.x)/(B.y-A.y) : dx1=0;
    C.y-A.y > 0 ? dx2 = (C.x-A.x)/(C.y-A.y) : dx2=0;
    sx1 = sx2 = A.x;
    y = A.y;
    // upper half
    for (; y <= B.y; y++, sx1+=dx1, sx2+=dx2)
        ScanLine(img, val, y, sx1, sx2);  // or your function for drawing horizontal line 
    // lower half
    C.y-B.y > 0 ? dx1 = (C.x-B.x)/(C.y-B.y) : dx1=0;
    sx1 = B.x;
    y   = B.y;
    for (; y <= C.y; y++, sx1+=dx1, sx2+=dx2)
        ScanLine(img, val, y, sx1, sx2); // or your function for drawing horizontal line
    
  //----------------------
  // draw scanline (single color)
  //----------------------
  template <class T>
  static void ScanLine(spImage<T> *img, T val, int y, int x1, int x2)
    
    if (y < 0 || y > img->Height()-1) return;
    if ((x1 < 0 && x2 < 0) || (x1 > img->Width()-1 && x2 > img->Width()-1 )) return;
    if (x1 > x2)   // swap coordinates
       
       x1 = x1^x2;
       x2 = x1^x2;
       x1 = x1^x2;
       
    if (x1 < 0) x1 = 0;
    if (x2 > img->Width()-1) x2 = img->Width()-1;
    for (int j = x1; j <= x2; j++)
        img->Pixel(y, j) = val;     // draw point
    
  //----------------------
  // sorting function
  //----------------------
  inline static bool yAscFPoint(spFPoint lhs, spFPoint rhs)  return lhs.y < rhs.y;  

如果您的图像或绘图画布是倒置的,在调用 TriangleFilled 函数之前,请反转 y 坐标。在函数调用之前,然后在函数内部这样做更容易:

   y1 = img->Height()-1 - y1;
   y2 = img->Height()-1 - y2;
   y3 = img->Height()-1 - y3;

但是,从你的任务性质来看

这个程序通过读取一个顶点和颜色列表来工作 文本文件。

您似乎需要执行 Gouraud 填充/着色,这是一段较长的代码。

【讨论】:

我喜欢这篇文章的地方:简洁,明确地陈述了它的观点。然后,SO (***(&SE: Stack Exchange)) 是关于问题和答案的。虽然许多问题帖子几乎不符合基本要求一个可回答的问题,而且这篇帖子的标题也不完全有帮助,但有一个明确的问题:What exactly am I doing wrong?。你的帖子没有回答这个问题。 是的,你是对的。但是,我将此代码发布为一个工作示例。它实际上是相同的方法,因此发布者可以通过将原始代码与此代码进行比较来轻松检测到错误。这不是对“我做错了什么?”问题的直接回答。但它可能会有所帮助。

以上是关于实现扫描线算法的主要内容,如果未能解决你的问题,请参考以下文章

openGL实现图形学扫描线种子填充算法

LinuxUbuntu20.04平台安装Clion与OpenGL并实现图形算法--区域填充扫描线算法

LinuxUbuntu20.04平台安装Clion与OpenGL并实现图形算法--区域填充扫描线算法

LinuxUbuntu20.04平台安装Clion与OpenGL并实现图形算法--区域填充扫描线算法

经典按键扫描程序算法实现方式

扫描线填充算法