裁剪算法 - Cohen Sutherland Clipping的原理及Java实现

来源:互联网 发布:手机电话录音软件 编辑:程序博客网 时间:2024/05/02 16:42

裁剪是3D图形的一个非常重要的方面,二维裁剪功能被广泛的应用于三维图像领域。本文结合Java代码实例,介绍一个非常好,但又足够简单的裁剪算法-科恩-萨瑟兰算法.

在绘制2D线段时,线段的一个端点或者两个端点可能位于屏幕外面,而其中的一部分仍然是可见的。在这种情况下,需要一个有效的算法来查找可见部分的两个新端点,只绘制基于新端点的线段,所有在屏幕外的部分被裁剪掉,从而提高程序的效率。

算法

绘制线段时,如果线段的一个端点是屏幕外,另一个在里面,通过裁剪,只保留屏幕内部的部分。即使两个端点都在画面外,该线段的一部分也可能是可见的。裁剪算法需要找到可见部分线段的新端点,新端点位于屏幕内部或屏幕的边缘。如下图,黑色矩形表示屏幕,红色是原始线段的端点,蓝色为裁剪后线段的端点:

这里写图片描述

  • A:两个端点都在屏幕上,无需裁剪。
  • B:一个端点在屏幕外,一个端点在屏幕内部,屏幕外的端点需要被裁剪。
  • C:两个端点都在屏幕外面,该线段的任何部分都不可见,无需裁剪
  • D:两个端点是画面外,但线段的一部分是可见的,两个端点都需要被裁剪。

如果继续细分,还有很多不同的情况,比如,每个端点可以在屏幕内部,左边,右边,上面,下面,等… 本算法可以非常有效地识别这些情况,并作对应的裁剪。

该算法将2D空间分为9个区域:中心区域是在屏幕,其它的8个区域是在屏幕以外的不同侧面。每个区域用一个四位的二进制数来标识,该二进制数标识被称为区域码(“outcode”)。编码如下:

这里写图片描述

  • 如果该区域在屏幕的上方,第一个字节位是1
  • 如果该区域在屏幕的下方,第二个字节位是1
  • 如果该区域在屏幕的右边,第三个字节位是1
  • 如果该区域在屏幕的左侧,第四个字节位为1

显然,同一区域不能同时在左和右边,或同时在上方和下方,所以在第三字节位和第四字节位不能为同时为1,第一字节位和第二字节位的不能同时为1。屏幕区域的4个字节位全部为0。

屏幕区域的Java定义如下,

private static final int INSIDE = 0;private static final int LEFT = 1;private static final int RIGHT = 2;private static final int BOTTOM = 4;private static final int TOP = 8;

线段的两个端点可以位于任意9个区域,我们先从一些简单的情形入手:

  • 如果两个端点均在屏幕的内部或边缘,该线段不需要裁剪并需要全部绘制。这种情况下,是简单接受(Trivial Accept)。
  • 如果两个端点均在屏幕(例如,两个端点都在屏幕上方)的同一侧,线段的任何部分都不在屏幕上,该线段不需要裁剪并不需绘制,这种情况下,是简单拒绝(Trivial Reject)。

以上两种情况下可以很容易地通过各区域的区域码(outcode)识别出来:

  • Trivial Accept:两个端点必须位于代码0000的区域中,所以Trivial Accept的情况可以通过 code1 | code2 == 0 来断定。(其中,code1 和code2 的线段两个端点的代码,’|’ 是二进制OR运算符,如果code1 和code2都是0,则code1 | code2 == 0)。
  • Trivial Reject:两个端点均在区域的同一侧,这两个码有两个相应的字节位都是1。例如,如果只有两个端点是在屏幕的左侧,两个代码的第四位均为1。因此,Trivial Reject的情况可以通过code1 & code2 != 0来断定。

其它情况(既不是Trivial Accept,也不是Trivial Reject),通过裁剪操作,可以转化成如上的简单的情况。科恩萨瑟兰算法是一种循环,每个循环只做一个裁剪操作。该操作裁剪其中一个端点,直到新的端点位于屏幕的水平或者垂直边界。在许多情况下,需要多次裁剪才能够最终断定是否该线段被接受或拒绝。但裁剪的次数最多为4次。

屏幕可通过两个坐上方的点P1(xMin,yMin),右下方的点P2(xMax, yMax) 来定义, Java 定义如下

    private double xMin;    private double yMin;    private double xMax;    private double yMax;

Clip method用到了一个辅助功能,getRegionCode,该method返回给定端点的二进制区域代码

    private final int getRegionCode(double x, double y) {        int xcode = x < xMin ? LEFT : x > xMax ? RIGHT : INSIDE;        int ycode = y < yMin ? BOTTOM : y > yMax ? TOP : INSIDE;        return xcode | ycode;    }

Clip功能开始检测简单的情形:

    public boolean clip(Line2D.Float line) {        double p1x = line.getX1(), p1y = line.getY1();        double p2x = line.getX2(), p2y = line.getY2();        double qx = 0d, qy = 0d;        boolean vertical = p1x == p2x;        double slope = vertical ? 0d : (p2y - p1y) / (p2x - p1x);        int c1 = getRegionCode(p1x, p1y);        int c2 = getRegionCode(p2x, p2y);        while (true) {            if(c1 == INSIDE & c2 == INSIDE){                break;            }            if ((c1 & c2) != INSIDE){                return false;            }

如果c1 == INSIDE & c2 == INSIDE 为true, 即为简单接受(Trivial Accept),通过break 跳转到结束代码.

        line.setLine(p1x, p1y, p2x, p2y);        return true;

如果 (c1 & c2) != INSIDE为true, 即为简单拒绝(Trivial Reject)。直接返回false;

如果没有检测到简单的情形,该线段需要被裁剪。每个循环只作4个可能的剪裁操作其中的一个。剪辑,一个坐标的一个端点被设置为原线段与对应区域的边界的交点,新的端点是在屏幕的边界坐标之一,该点的其它坐标值是由直线的方程重新计算。为了找到对应的裁剪操作,我们需要找到屏幕的外部的端点。该端点的代码称为codeout,选择code1或code2中不等于0的一个。

            int c = code1 == INSIDE ? code2 : code1;            if ((c & LEFT) != INSIDE) {                qx = xMin;                qy = (qx - p1x) * slope + p1y;            } else if ((c & RIGHT) != INSIDE) {                qx = xMax;                qy = (qx - p1x) * slope + p1y;            } else if ((c & BOTTOM) != INSIDE) {                qy = yMin;                qx = vertical ? p1x : (qy - p1y) / slope + p1x;            } else if ((c & TOP) != INSIDE) {                qy = yMax;                qx = vertical ? p1x : (qy - p1y) / slope + p1x;            }

上述代码计算裁剪之后新的端点坐标,新的坐标必须赋给端点p1或端点p2的, p1 和 p2 的选取取决于哪个codeout的值。循环结束之后,新线段可能满足一个简单的情况下,如果仍然不满足一个简单的情况,则进行新的循环并作裁剪操作。

            if (c == code1) {                p1x = qx;                p1y = qy;                code1 = getRegionCode(p1x, p1y);            } else {                p2x = qx;                p2y = qy;                code2 = getRegionCode(p2x, p2y);            }

最后附上完整的代码实现

    public final class Clipping {        private static final int INSIDE = 0;        private static final int LEFT = 1;        private static final int RIGHT = 2;        private static final int BOTTOM = 4;        private static final int TOP = 8;        private double xMin;        private double yMin;        private double xMax;        private double yMax;        public Clipping() {        }        public Clipping(Rectangle2D clip) {            setClip(clip);        }        public void setClip(Rectangle2D clip) {            xMin = clip.getX();            xMax = xMin + clip.getWidth();            yMin = clip.getY();            yMax = yMin + clip.getHeight();        }        private final int getRegionCode(double x, double y) {            int xcode = x < xMin ? LEFT : x > xMax ? RIGHT : INSIDE;            int ycode = y < yMin ? BOTTOM : y > yMax ? TOP : INSIDE;            return xcode | ycode;        }        public boolean clip(Line2D.Float line) {            double p1x = line.getX1(), p1y = line.getY1();            double p2x = line.getX2(), p2y = line.getY2();            double qx = 0d, qy = 0d;            boolean vertical = p1x == p2x;            double slope = vertical ? 0d : (p2y - p1y) / (p2x - p1x);            int code1 = getRegionCode(p1x, p1y);            int code2 = getRegionCode(p2x, p2y);            while (true) {                if(code1 == INSIDE & code2 == INSIDE){                    break;                }                           if ((code1 & code2) != INSIDE){                    return false;                }                int codeout = code1 == INSIDE ? code2 : code1;                if ((codeout & LEFT) != INSIDE) {                    qx = xMin;                    qy = (qx - p1x) * slope + p1y;                } else if ((codeout & RIGHT) != INSIDE) {                    qx = xMax;                    qy = (qx - p1x) * slope + p1y;                } else if ((codeout & BOTTOM) != INSIDE) {                    qy = yMin;                    qx = vertical ? p1x : (qy - p1y) / slope + p1x;                } else if ((codeout & TOP) != INSIDE) {                    qy = yMax;                    qx = vertical ? p1x : (qy - p1y) / slope + p1x;                }                if (codeout == code1) {                    p1x = qx;                    p1y = qy;                    code1 = getRegionCode(p1x, p1y);                } else {                    p2x = qx;                    p2y = qy;                    code2 = getRegionCode(p2x, p2y);                }            }            line.setLine(p1x, p1y, p2x, p2y);            return true;        }    }

本文主要参考自http://lodev.org/cgtutor/lineclipping.html, 并结合实际的游戏引擎,给出了Java版本的代码实现。希望对你有所帮助! 反馈请联系jinbing.peng@yahoo.com.

0 0