多边形轮廓上的边缘并不总是正确的

时间:2010-06-14 16:36:31

标签: c++ c algorithm opengl graphics

我正在使用下面的算法来生成四边形,然后渲染这些四边形以形成像这样的轮廓

  

http://img810.imageshack.us/img810/8530/uhohz.png

在图像上看到的问题是,有时线条太薄,因为它们应始终具有相同的宽度。我的算法找到第一个的4个顶点,然后下一个的顶部2个顶点是前一个的2。这会创建连接线,但似乎并不总是有效。我怎么能解决这个问题?

这是我的算法:

 void OGLENGINEFUNCTIONS::GenerateLinePoly(const std::vector<std::vector<GLdouble>> &input,
                          std::vector<GLfloat> &output, int width)
 {
     output.clear();

     if(input.size() < 2)
     {
         return;
     }

     int temp;
     float dirlen;
     float perplen;
     POINTFLOAT start;
     POINTFLOAT end;
     POINTFLOAT dir;
     POINTFLOAT ndir;
     POINTFLOAT perp;
     POINTFLOAT nperp;

     POINTFLOAT perpoffset;
     POINTFLOAT diroffset;

     POINTFLOAT p0, p1, p2, p3;

     for(unsigned int i = 0; i < input.size() - 1; ++i)
     {

         start.x = static_cast<float>(input[i][0]);
         start.y = static_cast<float>(input[i][1]);

         end.x = static_cast<float>(input[i + 1][0]);
         end.y = static_cast<float>(input[i + 1][1]);

         dir.x = end.x - start.x;
         dir.y = end.y - start.y;

         dirlen = sqrt((dir.x * dir.x) + (dir.y * dir.y));

         ndir.x = static_cast<float>(dir.x * 1.0 / dirlen);
         ndir.y = static_cast<float>(dir.y * 1.0 / dirlen);

         perp.x = dir.y;
         perp.y = -dir.x;

         perplen = sqrt((perp.x * perp.x) + (perp.y * perp.y));

         nperp.x = static_cast<float>(perp.x * 1.0 / perplen);
         nperp.y = static_cast<float>(perp.y * 1.0 / perplen);

         perpoffset.x = static_cast<float>(nperp.x * width * 0.5);
         perpoffset.y = static_cast<float>(nperp.y * width * 0.5);

         diroffset.x = static_cast<float>(ndir.x * 0 * 0.5);
         diroffset.y = static_cast<float>(ndir.y * 0 * 0.5);

            // p0 = start + perpoffset - diroffset
            // p1 = start - perpoffset - diroffset
            // p2 = end + perpoffset + diroffset
            // p3 = end - perpoffset + diroffset 

         p0.x = start.x + perpoffset.x - diroffset.x;
         p0.y = start.y + perpoffset.y - diroffset.y;

         p1.x = start.x - perpoffset.x - diroffset.x;
         p1.y = start.y - perpoffset.y - diroffset.y;

         if(i > 0)
         {
             temp = (8 * (i - 1));
             p2.x = output[temp + 2];
             p2.y = output[temp + 3];
             p3.x = output[temp + 4];
             p3.y = output[temp + 5];

         }
         else
         {
             p2.x = end.x + perpoffset.x + diroffset.x;
             p2.y = end.y + perpoffset.y + diroffset.y;

             p3.x = end.x - perpoffset.x + diroffset.x;
             p3.y = end.y - perpoffset.y + diroffset.y;
         }



         output.push_back(p2.x);
         output.push_back(p2.y);
         output.push_back(p0.x);
         output.push_back(p0.y);
         output.push_back(p1.x);
         output.push_back(p1.y);
         output.push_back(p3.x);
         output.push_back(p3.y);

     }
 }

修改

 POINTFLOAT multiply(const POINTFLOAT &a, float b)
 {
     POINTFLOAT result;
     result.x = a.x * b;
     result.y = a.y * b;
     return result;
 }

 POINTFLOAT normalize(const POINTFLOAT &a)
 {
     return multiply(a, 1.0f / sqrt(a.x * a.x + a.y * a.y));
 }


 POINTFLOAT slerp2d( const POINTFLOAT v0, 
                     const POINTFLOAT v1, float t )
 {
     float dot = (v0.x * v1.x + v1.y * v1.y);
     if( dot < -1.0f ) dot = -1.0f;
     if( dot > 1.0f ) dot = 1.0f;

     float theta_0 = acos( dot );
     float theta = theta_0 * t;

     POINTFLOAT v2;
     v2.x = -v0.y;
     v2.y = v0.x;

     POINTFLOAT result;
     result.x = v0.x * cos(theta) + v2.x * sin(theta);
     result.y = v0.y * cos(theta) + v2.y * sin(theta);

     return result;
 }

 void OGLENGINEFUNCTIONS::GenerateLinePoly(const std::vector<std::vector<GLdouble> > &input,
                          std::vector<GLfloat> &output, int width)
 {
     output.clear();

     if(input.size() < 2)
     {
         return;
     }

     float w = width / 2.0f;

     //glBegin(GL_TRIANGLES);
     for( size_t i = 0; i < input.size()-1; ++i )
     {
         POINTFLOAT cur;
         cur.x = input[i][0];
         cur.y = input[i][1];


         POINTFLOAT nxt;
         nxt.x = input[i+1][0];
         nxt.y = input[i+1][1];

         POINTFLOAT b;
         b.x = nxt.x - cur.x;
         b.y = nxt.y - cur.y;

         b = normalize(b);



         POINTFLOAT b_perp;
         b_perp.x = -b.y;
         b_perp.y = b.x;


         POINTFLOAT p0;
         POINTFLOAT p1;
         POINTFLOAT p2;
         POINTFLOAT p3;

         p0.x = cur.x + b_perp.x * w;
         p0.y = cur.y + b_perp.y * w;

         p1.x = cur.x - b_perp.x * w;
         p1.y = cur.y - b_perp.y * w;

         p2.x = nxt.x + b_perp.x * w;
         p2.y = nxt.y + b_perp.y * w;

         p3.x = nxt.x - b_perp.x * w;
         p3.y = nxt.y - b_perp.y * w;

         output.push_back(p0.x);
         output.push_back(p0.y);
         output.push_back(p1.x);
         output.push_back(p1.y);
         output.push_back(p2.x);
         output.push_back(p2.y);

         output.push_back(p2.x);
         output.push_back(p2.y);
         output.push_back(p1.x);
         output.push_back(p1.y);
         output.push_back(p3.x);
         output.push_back(p3.y);



         // only do joins when we have a prv
         if( i == 0 ) continue;

         POINTFLOAT prv;
         prv.x = input[i-1][0];
         prv.y = input[i-1][1];

         POINTFLOAT a;
         a.x = prv.x - cur.x;
         a.y = prv.y - cur.y;

         a = normalize(a);

         POINTFLOAT a_perp;
         a_perp.x = a.y;
         a_perp.y = -a.x;

         float det = a.x * b.y - b.x * a.y;
         if( det > 0 )
         {
             a_perp.x = -a_perp.x;
             a_perp.y = -a_perp.y;

             b_perp.x = -b_perp.x;
             b_perp.y = -b_perp.y;
         }

         // TODO: do inner miter calculation

         // flip around normals and calculate round join points
         a_perp.x = -a_perp.x;
         a_perp.y = -a_perp.y;

         b_perp.x = -b_perp.x;
         b_perp.y = -b_perp.y;

         size_t num_pts = 4;

         std::vector< POINTFLOAT> round( 1 + num_pts + 1 );
         POINTFLOAT nc;
         nc.x = cur.x + (a_perp.x * w);
         nc.y = cur.y + (a_perp.y * w);

         round.front() = nc;

         nc.x = cur.x + (b_perp.x * w);
         nc.y = cur.y + (b_perp.y * w);

         round.back() = nc;

         for( size_t j = 1; j < num_pts+1; ++j )
         {
             float t = (float)j / (float)(num_pts + 1);
             if( det > 0 )
             {
                 POINTFLOAT nin;
                 nin = slerp2d( b_perp, a_perp, 1.0f-t );
                 nin.x *= w;
                 nin.y *= w;

                 nin.x += cur.x;
                 nin.y += cur.y;

                 round[j] = nin;
             }
             else
             {
                 POINTFLOAT nin;
                 nin = slerp2d( a_perp, b_perp, t );
                 nin.x *= w;
                 nin.y *= w;

                 nin.x += cur.x;
                 nin.y += cur.y;

                 round[j] = nin;
             }
         }

         for( size_t j = 0; j < round.size()-1; ++j )
         {

             output.push_back(cur.x);
             output.push_back(cur.y);

             if( det > 0 )
             {
                 output.push_back(round[j + 1].x);
                 output.push_back(round[j + 1].y);
                 output.push_back(round[j].x);
                 output.push_back(round[j].y);
             }
             else
             {

                 output.push_back(round[j].x);
                 output.push_back(round[j].y);

                 output.push_back(round[j + 1].x);
                 output.push_back(round[j + 1].y);
             }
         }
     }
 }

6 个答案:

答案 0 :(得分:8)

需要编写Eigen,但核心操作应该很容易映射到您正在使用的任何矢量类。

// v0 and v1 are normalized
// t can vary between 0 and 1
// http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/
Vector2f slerp2d( const Vector2f& v0, const Vector2f& v1, float t )
{
    float dot = v0.dot(v1);
    if( dot < -1.0f ) dot = -1.0f;
    if( dot > 1.0f ) dot = 1.0f;

    float theta_0 = acos( dot );
    float theta = theta_0 * t;

    Vector2f v2( -v0.y(), v0.x() );

    return ( v0*cos(theta) + v2*sin(theta) );
}


void glPolyline( const vector<Vector2f>& polyline, float width )
{
    if( polyline.size() < 2 ) return;
    float w = width / 2.0f;

    glBegin(GL_TRIANGLES);
    for( size_t i = 0; i < polyline.size()-1; ++i )
    {
        const Vector2f& cur = polyline[ i ];
        const Vector2f& nxt = polyline[i+1];

        Vector2f b = (nxt - cur).normalized();
        Vector2f b_perp( -b.y(), b.x() );

        Vector2f p0( cur + b_perp*w );
        Vector2f p1( cur - b_perp*w );
        Vector2f p2( nxt + b_perp*w );
        Vector2f p3( nxt - b_perp*w );

        // first triangle
        glVertex2fv( p0.data() );
        glVertex2fv( p1.data() );
        glVertex2fv( p2.data() );
        // second triangle
        glVertex2fv( p2.data() );
        glVertex2fv( p1.data() );
        glVertex2fv( p3.data() );

        // only do joins when we have a prv
        if( i == 0 ) continue;

        const Vector2f& prv = polyline[i-1];
        Vector2f a = (prv - cur).normalized();
        Vector2f a_perp( a.y(), -a.x() );

        float det = a.x()*b.y() - b.x()*a.y();
        if( det > 0 )
        {
            a_perp = -a_perp;
            b_perp = -b_perp;
        }

        // TODO: do inner miter calculation

        // flip around normals and calculate round join points
        a_perp = -a_perp;
        b_perp = -b_perp;

        size_t num_pts = 4;
        vector< Vector2f > round( 1 + num_pts + 1 );
        for( size_t j = 0; j <= num_pts+1; ++j )
        {
            float t = (float)j/(float)(num_pts+1);
            if( det > 0 )
                round[j] = cur + (slerp2d( b_perp, a_perp, 1.0f-t ) * w);
            else
                round[j] = cur + (slerp2d( a_perp, b_perp, t ) * w);
        }

        for( size_t j = 0; j < round.size()-1; ++j )
        {
            glVertex2fv( cur.data() );
            if( det > 0 )
            {
                glVertex2fv( round[j+1].data() );
                glVertex2fv( round[j+0].data() );
            }
            else
            {
                glVertex2fv( round[j+0].data() );
                glVertex2fv( round[j+1].data() );
            }
        }
    }
    glEnd();
}

编辑:屏幕截图:

Wireframe

Filled

答案 1 :(得分:3)

怎么样:

  1. 将每一条线画到角落里面
  2. 在垂直于角落角度的每个角落画一条额外的线
  3. 像这样:

    alt text http://www.geekops.co.uk/photos/0000-00-02%20%28Forum%20images%29/CorrectAngleDrawing.png

    蓝色/红色代表您尝试连接的两条线。点缀绿色是您添加的额外一个以平滑角落。上面的图片显示内容会因尖角而略微剪裁。如果这是一个问题,你可以进一步向外延伸两条连接线,并进一步拉出额外的线。

    [编辑] 我发现了我的建议存在缺陷。你有一些凹面部分,根本无法正常工作。对于那些场合,你会想要做一些事情,比如画一个倒角边缘:

    alt text http://www.geekops.co.uk/photos/0000-00-02%20%28Forum%20images%29/CorrectAngleDrawing2.png

    [Edit2] 我对之前发布的代码进行了一些调试。以下应该更有用:

        // PolygonOutlineGen.cpp : A small program to calculate 4-point polygons 
    // to surround an input polygon.
    
    #include <vector>
    #include <math.h>
    #include <iostream>
    #include <iomanip>
    
    using namespace std;
    
    // Describe some structures etc. so the code will compile without 
    // requiring the GL libraries.
    typedef double GLdouble;
    typedef float GLfloat;
    typedef struct POINTFLOAT
    {
        float x;
        float y;
    } POINTFLOAT;
    
    // A function to generate two coordinates representing the start and end
    // of a line perpendicular to start/end, offset by 'width' units.
    void GenerateOffsetLineCoords(
        POINTFLOAT start, 
        POINTFLOAT end, 
        int width,
        POINTFLOAT& perpStart,
        POINTFLOAT& perpEnd)
    {
        float dirlen;
        POINTFLOAT dir;
        POINTFLOAT ndir;
        POINTFLOAT nperp;
        POINTFLOAT perpoffset;
    
        // Work out the offset for a parallel line which is space outwards by 'width' units
        dir.x = end.x - start.x;
        dir.y = end.y - start.y;
        dirlen = sqrt((dir.x * dir.x) + (dir.y * dir.y));
        ndir.x = static_cast<float>(dir.x * 1.0 / dirlen);
        ndir.y = static_cast<float>(dir.y * 1.0 / dirlen);
        nperp.x = -ndir.y;
        nperp.y = ndir.x;
        perpoffset.x = static_cast<float>(nperp.x * width);
        perpoffset.y = static_cast<float>(nperp.y * width);
    
        // Calculate the offset coordinates for the new line
        perpStart.x = start.x + perpoffset.x;
        perpStart.y = start.y + perpoffset.y;
        perpEnd.x = end.x + perpoffset.x;
        perpEnd.y = end.y + perpoffset.y;
    }
    
    // Function to generate quads of coordinate pairs to surround the 'input'
    // polygon.
    void GenerateLinePoly(const std::vector<std::vector<GLdouble>> &input,
        std::vector<GLfloat> &output, int width)
    {
        // Make sure we have something to produce an outline for and that it's not contaminated with previous results
        output.clear();
        if(input.size() < 2)
        {
            return;
        }
    
        // Storage for the pairs of lines which form sections of the outline
        POINTFLOAT line1_start;
        POINTFLOAT line1_end;
        POINTFLOAT line2_start;
        POINTFLOAT line2_end;
    
        // Storage for the outer edges of the quads we'll be generating
        POINTFLOAT line1offset_start;
        POINTFLOAT line1offset_end;
        POINTFLOAT line2offset_start;
        POINTFLOAT line2offset_end;
    
        // Storage for the line we'll use to make smooth joints between polygon sections.
        POINTFLOAT joininglineoffset_start;
        POINTFLOAT joininglineoffset_end;
    
        for(unsigned int i = 0; i < input.size() - 2; ++i)
        {
            // Grab the raw line input for the first line or if we've already done one, just re-use the last results
            if( i == 0 )
            {
                line1_start.x = static_cast<float>(input[i][0]);
                line1_start.y = static_cast<float>(input[i][1]);
                line1_end.x = static_cast<float>(input[i + 1][0]);
                line1_end.y = static_cast<float>(input[i + 1][1]);
    
                GenerateOffsetLineCoords(line1_start, line1_end, width, line1offset_start, line1offset_end);
            }
            else
            {
                line1_start = line2_start;
                line1offset_start = line2offset_start;
                line1_end = line2_end;
                line1offset_end = line2offset_end;
            }
    
            // Grab the second line and work out the coords of it's offset 
            line2_start.x = static_cast<float>(input[i+1][0]);
            line2_start.y = static_cast<float>(input[i+1][1]);
            line2_end.x = static_cast<float>(input[i+2][0]);
            line2_end.y = static_cast<float>(input[i+2][1]);
            GenerateOffsetLineCoords(line2_start, line2_end, width, line2offset_start, line2offset_end);
    
            // Grab the offset for the line which joins the open end
            GenerateOffsetLineCoords(line2offset_start, line1offset_end, width, joininglineoffset_start, joininglineoffset_end);
    
            // Push line 1 onto the output
            output.push_back(line1_start.x);
            output.push_back(line1_start.y);
            output.push_back(line1_end.x);
            output.push_back(line1_end.y);
            output.push_back(line1offset_end.x);
            output.push_back(line1offset_end.y);
            output.push_back(line1offset_start.x);
            output.push_back(line1offset_start.y);
    
            // Push the new section onto the output
            output.push_back(line1offset_end.x);
            output.push_back(line1offset_end.y);
            output.push_back(line2offset_start.x);
            output.push_back(line2offset_start.y);
            output.push_back(joininglineoffset_start.x);
            output.push_back(joininglineoffset_start.y);
            output.push_back(joininglineoffset_end.x);
            output.push_back(joininglineoffset_end.y);
        }
    
        // TODO: Push the remaining line 2 on.
    
        // TODO: Add one last joining piece between the end and the beginning.
    }
    
    int main(int argc, char* argv[])
    {
        // Describe some input data
        std::vector<std::vector<GLdouble>> input;
        std::vector<GLdouble> val1; val1.push_back(010.0); val1.push_back(010.0); input.push_back(val1);
        std::vector<GLdouble> val2; val2.push_back(050.0); val2.push_back(100.0); input.push_back(val2);
        std::vector<GLdouble> val3; val3.push_back(100.0); val3.push_back(100.0); input.push_back(val3);
        std::vector<GLdouble> val4; val4.push_back(010.0); val4.push_back(010.0); input.push_back(val4);
    
        // Generate the quads required to outline the shape
        std::vector<GLfloat> output;
        GenerateLinePoly(input, output, 5);
    
        // Dump the output as pairs of coordinates, grouped into the quads they describe
        cout << setiosflags(ios::fixed) << setprecision(1);
        for(unsigned int i=0; i < output.size(); i++)
        {
           if( (i > 0) && ((i)%2==0) ) { cout << endl; }
           if( (i > 0) && ((i)%8==0) ) { cout << endl; }
           cout << setw(7) << output[i];
        }
        cout << endl;
        return 0;
    }
    

    ..就我所见,看起来它适用于凸多边形: - )

答案 2 :(得分:2)

This code 渲染correct SVG而不是incorrect one。 它比genpfault的解决方案简单,具有更少的四边形渲染的优势。

此处的每个连接都会在 Jon Cage 的答案的最后一张图片中呈现。

答案 3 :(得分:1)

啊,我现在看到了。这是因为你正在重复使用旧的顶点,这些顶点不一定与你的新顶点平行。

只需通过一个简单的示例手动完成您的代码,您的输入点将转动90度。旧顶点将与dir平行,而新顶点将垂直。如果你在线上的点数足够接近,那么你会得到你在图片中看到的奇怪行为。

没有“简单”的解决方案来获得统一的宽度线,但如果你只是一次渲染一对线(即摆脱i > 0的情况),情况会更好。这将给你一些丑陋的尖角,但你不会得到任何细线。

答案 4 :(得分:1)

您正在反转第一段与其余段之间的方向。从输出向量中提取先前值的块应设置p0和p1点,并且每次都应根据端点计算p2和p3。

即。它应该是:

     if(i == 0)
     {
         p0.x = start.x + perpoffset.x - diroffset.x;
         p0.y = start.y + perpoffset.y - diroffset.y;

         p1.x = start.x - perpoffset.x - diroffset.x;
         p1.y = start.y - perpoffset.y - diroffset.y;
     }
     else
     {
         temp = (8 * (i - 1));
         p0.x = output[temp + 0];
         p0.y = output[temp + 1];
         p1.x = output[temp + 6];
         p1.y = output[temp + 7];

     }

     p2.x = end.x + perpoffset.x + diroffset.x;
     p2.y = end.y + perpoffset.y + diroffset.y;

     p3.x = end.x - perpoffset.x + diroffset.x;
     p3.y = end.y - perpoffset.y + diroffset.y;

答案 5 :(得分:1)

您不能使用当前线段上前一个线段的偏移矢量 - 它们与当前线段无关的内容垂直。最好是使用相同的偏移量:

         p0.x = start.x + perpoffset.x;
         p0.y = start.y + perpoffset.y;

         p1.x = start.x - perpoffset.x;
         p1.y = start.y - perpoffset.y;

         p2.x = end.x + perpoffset.x;
         p2.y = end.y + perpoffset.y;

         p3.x = end.x - perpoffset.x;
         p3.y = end.y - perpoffset.y;

然后在每个顶点放一个圆圈以绕过角落。如果舍入不是您想要的方式,则必须更改添加到偏移量的ndir量 - 这取决于在顶点处连接的两个段,而不是一个。您需要识别输入和输出偏移线的交点。从上面开始,并以90度或120度的角度进行一些放大拍摄以获得感觉。对不起,现在没有方便的配方。

最后,您不需要规范化perp向量。无论如何,你计算它的方法都会产生一个单位向量。