线段裁剪:Cohen-Sutherland算法

发布时间 2023-10-01 18:03:38作者: 明明1109

裁剪算法

计算机内部存储的图形数据量通常较大,而屏幕只显示其中一部分,因此需要确定哪些部分在显示区域内,哪些在显示区域外。这个过程称为裁剪(clipping)。裁剪是二维观察(三维观察)的重要部分,参见计算机图形:二维观察

点裁剪:只需要判断点与正则矩形边界的位置关系。如果落在裁剪矩形内,保留;如果落在裁剪矩形外,则去掉。

直线段裁剪:需要判断线段与裁剪矩形的位置关系,有多种算法:
1)Cohen-Sutherland算法;
2)梁有栋-Barsky算法;
3)Nicholl-Lee-Nicholl算法;


Cohen-Sutherland线段裁剪算法

基本思想

用裁剪窗口对应的正则矩形区域,按边界将平面划分为9部分,每个部分对应一个区域码。通过为线段端点建立区域码,快速判断端点与裁剪矩形的位置关系。

  1. 当线段在矩形内部时,全部保留;
  2. 当线段在矩形外部时,全部裁剪掉;
  3. 当线段与裁剪矩形相交时,求出交点,并裁剪掉矩形外的部分,保留矩形内的线段。最终得到裁剪后的线段。

tips:正则矩形,是指边界与x轴或y轴平行的矩形。

具体步骤

  1. 对矩形边界划分空间得到9个部分均进行编码,从而得到9个区域码:

每个区域码用一个4bit二进制数表示,其含义:

  • 0000 裁剪窗口内部;
  • 1001 裁剪窗口左上;
  • 0001 裁剪窗口正左侧;
  • 0101 裁剪窗口左下;
  • 1000 裁剪窗口正上方;
  • 0100 裁剪窗口正下方;
  • 1010 裁剪窗口右上;
  • 0010 裁剪窗口正右侧;
  • 0110 裁剪窗口右下。

区域码的4个bit位的毎一位都代表一个矩形边界:

  1. 为线段端点建立区域码

要判断线段与矩形位置关系,先判断端点与矩形的关系,为每个端点建立区域码(含4bit有效区域码)。有2种方法:
Ⅰ:比较法
1)如果\(x<xw_{min}\),则bit1 = 1;
2)如果\(x>xw_{max}\),则bit2 = 1;
3)如果\(y<yw_{min}\),则bit3 = 1;
4)如果\(y>yw_{max}\),则bit4 = 1。

Ⅱ:取符号位法
1)计算端点坐标与矩形边界的差;
2)用差值设置区域码对应值,bit1为\(x-xw_{min}\)符号位,bit2为\(xw_{max}-x\)符号位,bit3为\(y-yw_{min}符号位\),bit4为\(yw_{max}-y符号位\)

  1. 判断线段与裁剪矩形的位置关系

为线段2端点建立区域码后,就能快速判断与裁剪窗口的位置关系。
1)矩形内:进行逻辑或操作。如果2个区域码均为0000,则线段完全在矩形内,需保留;
2)矩形外:进行逻辑与操作。如果有一对相同位置都为1,则线段位于矩形之外,需丢弃;
3)相交:线段与矩形边界相交,但不一定与矩形相交,需要进一步测试线段与其他边界交点。可能需要多次求交运算完成线段裁剪,每次处理一条边界后,裁掉其中一部分,其余部分对照窗口其余边界进行检查。

  1. 逐边界对相交线段进行裁剪

假定矩形裁剪窗口边界处理顺序:左、右、下、上,即bit1->bit4。要检查线段是否与某个裁剪矩形边界相交,可以检查2个端点区域码对应bit位:如果一个是1,另一个0,则线段与该边界相交。然而,线段与矩形相交的部分,还需要进一步判断。下面给出具体过程:

如下图,线段P1P2、P3P4均与左边界相交,但P1P2与矩形相交,而P3P4在矩形外,

对于线段P1P2,端点P1、P2区域码分别为0100、1001。按裁剪窗口边界顺序处理(左右下上)

1)检查左边界:P1在左边界内,P2在外。
2)计算出与左边界交点P2',并裁剪掉左边界外的部分P2'~P2。
3)检查右边界:位于右边界内部。
4)检查下边界:P2'在下边界内,P1在外。
5)计算出与下边界交点P1',并裁剪掉下边界外的部分P1'~P1。
6)检查上边界:P2'再上边界外,P1'在内。
7)计算出与上边界交点P2'',并裁减掉上边界外的部分P2'~P2''。

对于线段P3P4,P3在左边界外P4在内(区域码bit1),与左边界交点P3',裁剪掉左边界外的部分P3P3';而P3'P4在下边界外,因此都裁剪掉。

  • 如何求线段与边界交点?
    将边界坐标值(\(x=xw_{min}或xw_{max},y=yw_{min}或yw_{max}\)),代入直线的点斜式:

\[\tag{1} y = y_0+m(x-x_0) \]

计算分析

对区域进行编码,涉及到比较/位运算。
求线段与边界交点时,用到了点斜式,需要先计算直线斜率,涉及到浮点数乘除法运算。因为有4个边界,所以最多需要求4次交点。

程序代码

指定裁剪矩形,对线段裁剪后,绘制线段。

class wcPt2D {
public:
       GLfloat x, y;
};

inline GLint round(const GLfloat a)
{
       return (GLint)(a + 0.5);
}

// APP窗口尺寸
GLint winWidth = 600, winHeight = 500;
// 边界区域码
const GLint winLeftBitCode   = 0x1;
const GLint winRightBitCode  = 0x2;
const GLint winBottomBitCode = 0x4;
const GLint winTopBitCode    = 0x8;

// 根据区域码判断裁剪点是否位于裁剪矩形内
// 返回值设置成bool 更合适
inline GLint inside(GLint code)
{
       return (GLint)(!code);
}

// 判断2个区域码是否位于裁剪矩形外
inline GLint reject(GLint code1, GLint code2)
{
       return code1 & code2;
}

// 判断2个区域码是否位于裁剪矩形内
inline GLint accept(GLint code1, GLint code2)
{
       return (GLint)!(code1 | code2);
}

// 计算裁剪点pt的区域码
// winMin, winMax包含裁剪区域信息
GLubyte encode(wcPt2D pt, wcPt2D winMin, wcPt2D winMax)
{
       GLubyte code = 0x00;

       if (pt.x < winMin.x)
              code = code | winLeftBitCode;
       if (pt.x > winMax.x)
              code = code | winRightBitCode;
       if (pt.y < winMin.y)
              code = code | winBottomBitCode;
       if (pt.y > winMax.y)
              code = code | winTopBitCode;

       return code;
}

void swapPts(wcPt2D* p1, wcPt2D* p2)
{
       wcPt2D tmp;

       tmp = *p1; 
       *p1 = *p2; 
       *p2 = tmp;
}

void swapCode(GLubyte* c1, GLubyte* c2)
{
       GLubyte tmp;

       tmp = *c1;
       *c1 = *c2;
       *c2 = tmp;
}

void lineClipCohSuth(wcPt2D winMin, wcPt2D winMax, wcPt2D p1, wcPt2D p2)
{
       GLubyte code1, code2;
       GLboolean done = false, plotLine = false;
       GLfloat m = 0.0;

       while (!done) {
              // 为线段2端点建立区域码
              code1 = encode(p1, winMin, winMax);
              code2 = encode(p2, winMin, winMax);

              if (accept(code1, code2)) { // 线段在矩形内
                     done = true;
                     plotLine = true;
              }
              else if (reject(code1, code2)) { // 线段在矩形外
                     done = true;
              }
              else { // 线段与矩形相交或在矩形外, 需进一步测试
                     /* Label the endpoint outside the display window as p1. */
                     if (inside(code1)) { // 确保在边界外的点是p1, 那么在边界内的是p2
                           swapPts(&p1, &p2);
                           swapCode(&code1, &code2);
                     }

                     /* Use slope m to find line-clipEdge intersection. */
                     if (p2.x != p1.x) {
                           m = (p2.y - p1.y) / (p2.x - p1.x); // 斜率
                     }

                     // 按左->右->下->上的顺序, 测试边界是否裁剪后线段相交
                     if (code1 & winLeftBitCode) { // 与左边界相交, 更新p1到左边界
                           // 与左边界相交, 则斜率存在
                           p1.y += (winMin.x - p1.x) * m; // y - y0 = (x - x0)m
                           p1.x = winMin.x;
                     }
                     else if (code1 & winRightBitCode) { // 与右边界相交, 更新p1到右边界
                           // 与右边界相交, 则斜率存在
                           p1.y += (winMax.x - p1.x) * m; // y - y0 = (x - x0)m
                           p1.x = winMax.x;
                     }
                     else if (code1 & winBottomBitCode) { // 与下边界相交, 更新p1到下边界
                           /* Need to update p1.x for nonvertical lines only */
                           if (p2.x != p1.x) {
                                  p1.x += (winMin.y - p1.y) / m; // x - x0 = (y - y0)/m
                           }
                           p1.y = winMin.y;
                     }
                     else if (code1 & winTopBitCode) { // 与上边界相交, 更新p1到下边界
                           if (p2.x != p1.x) {
                                  p1.x += (winMax.y - p1.y) / m; // x - x0 = 
(y-y0)/m
                           }
                           p1.y = winMax.y;
                     }
              }
       }

       // 绘制裁剪后的线段p1p2
       if (plotLine) {
              glBegin(GL_LINES);
              glVertex2i(round(p1.x), round(p1.y));
              glVertex2i(round(p2.x), round(p2.y));
              glEnd();
       }
}