多边形裁剪(Polygon Clipping) 1

原文地址: https://sean.cm/a/polygon-clipping-pt1

Greiner-Hormann裁剪算法无法处理重合线。 所以我研究并写了另一篇适用于所有多边形的文章。

在此处阅读后续内容:多边形裁剪(第 2 部分)

问题

首先, 让我们定义问题,

假设您有两个多边形,每个多边形都以 2D 形式存在

var poly1 = [ // red
  [ 181, 270 ],
  [  85, 418 ],
  [ 171, 477 ],
  [ 491, 365 ],
  [ 218, 381 ],
  [ 458, 260 ]
];
var poly2 = [ // blue
  [ 474, 488 ],
  [ 659, 363 ],
  [ 255, 283 ],
  [  56, 340 ],
  [ 284, 488 ],
  [ 371, 342 ]
];

多边形

 奇偶规则

多边形遵循奇偶规则来确定一个点是否被视为区域“内部”。

基本规则是想象您正在用一条水平线上从左到右扫描。每次越过边缘时,都会在外部和内部之间切换。

奇偶规则

 那么:给定这两个多边形,我们如何计算不同的布尔运算?

基本理念

 首先,让我们定义一些基本的规则.

顺时针vs逆时针(Forward vs. Backward Movement)

如果我们坐在多边形的任何一点上,我们总是可以向前一个点或者后一个点移动

顺时针只是意味着沿箭头方向移动,逆时针则相反

运动方向

 插入点

 在处理过程中,我们需要在多边形中插入点。只要我们对如何插入它们很聪明,它就不会改变多边形的形状:

插入

交叉点

识别和分类交叉点是算法中的魔法.

如果您考虑一下,我们将执行的每个操作(交集、联合、差异)都会产生一个包含多边形之间所有交点的多边形

多边形交点

 我们不关心同一多边形内的交叉点.

另外:如果你想象我们正沿着一个多边形行走并遇到一个十字路口,我们有3个选择.

1. 保持在同一个多边形上(这是毫无意义的)

2. 切换多边形,开始顺时针移动

3. 切换多边形,并开始逆时针移动

 

因此,如果我们能够智能地选择在每个交叉路口的方向,我们就可以追踪到正确的结果形状.

交叉点示例

想象一下, 我们想象一下,我们正在追踪两个多边形合并union的结果.

在每个交叉点,我们都希望朝着最终形状继续增长的方向移动.

我们可以这样做:

我们也可以在相反的方向得到相同的结果:

关于所有四个决定,我们可以说哪些是正确的?

在每个交叉点,我们总是朝着远离我们离开的多边形的方向前进.

因此,例如,如果我们沿着 Blue 行驶,然后遇到一个十字路口,我们应该继续沿着 Red 向远离Blue的方向行使.

会有什么不同?这是Red-Blue(从Red中减去Blue区域):

 而在另一个方向:

对此我们能说什么?

当从红色切换到蓝色时,我们进入红色。当从蓝色切换到红色时,我们远离红色.

所以我们有两个基本的决定:

1. 当从红色切换到蓝色时,我们是进入还是远离红色?

2.当从蓝色切换到红色时,我们是进入还是远离蓝色?

对于联合(union)来说, 答案总是离开. 但是对于Red-Blue(Red减Blue),我们想要进入红色, 远离蓝色。如果你玩玩,你会注意到交叉(intersection )意味着总是进入你要离开的

这给了我们下边的表

OperatorInto Red?Into Blue?
联合(Union)FalseFalse
Red减Blue(Red - Blue)TrueFalse
Blue减Red(Blue - Red)FalseTrue
交叉(Intersection)TrueTrue

交叉入口/ 交叉出口

我们不知道如何进入离开——我们只知道沿着多边形顺时针,逆时针移动。我们如何把两者同意起来.

如果我们在一个交点的两边取两个点,并测试它们是否在另一个多边形内,我们可以保证一个点在外面,一个点在里面:

如果第一个点在外面,那么我们可以认为这条线是通过交点进入多边形的。如果第一个点在,则该线通过交点离开多边形.

所以,我们真的只需要将每个交叉点标记为交叉入口/ 交叉出口

当我们沿着一条路径行驶时,每个路口都会切换我们是在里面还是外面。它必须.

因此,我们只需要计算第一个点是否在另一个多边形内部。如果是,那么第一个交叉点是一个交叉出口——否则第一个交叉点是一个交叉入口.

而且由于路径上的每个交叉点都在entryexit之间切换,我们不必继续测试点是在内部还是外部(这很昂贵).

表现

最后,重要的是要认识到交叉点是相对于多边形的交叉入口或交叉出口.

这意味着每个交叉点有四种可能性.

 白色代表进入,黑色代表退出。左半球为红色,右半球为蓝色

 实际上,对于每个交点,我们将在每个多边形中插入一个点。所以每个交点会有两个点,一个存储在每个多边形中。每个点都会跟踪它是进入还是退出.

现在我们准备好代码啦.

步骤1. 将多边形转换为链表

双链表对于这个算法来说是一个有用的多边形表示,因为我们将同时插入点和遍历。通过使用双链表,我们不必担心插入会破坏遍历.

我们还需要跟踪一个点是否是一个交点,所以我们可以从false这里初始化它开始:

function UpgradePolygon(p){
  // converts a list of points into a double linked list
  var root = null;
  for (var i = 0; i < p.length; i++){
    var node = {
      point: p[i],
      intersection: false,
      next: null,
      prev: null
    };
    if (root === null){
      // root just points to itself:
      //    +-> (root) <-+
      //    |            |
      //    +------------+
      node.next = node;
      node.prev = node;
      root = node;
    }
    else{
      // change this:
      //    ...-- (prev) <--------------> (root) --...
      // to this:
      //    ...-- (prev) <--> (node) <--> (root) --...
      var prev = root.prev;
      prev.next = node;
      node.prev = prev;
      node.next = root;
      root.prev = node;
    }
  }
  return root;
}

步骤2. 计算并插入交叉点

接下来,我们需要遍历每个边组合,看看它们是否相交。如果它们确实彼此相交,那么我们需要在多边形中插入交点.

线交点

首先,我们需要一个辅助函数来计算两条线的交点:

function LinesIntersect(a0, a1, b0, b1){
  var adx = a1[0] - a0[0];
  var ady = a1[1] - a0[1];
  var bdx = b1[0] - b0[0];
  var bdy = b1[1] - b0[1];

  var axb = adx * bdy - ady * bdx;
  var ret = {
    cross: axb,
    alongA: Infinity,
    alongB: Infinity,
    point: [Infinity, Infinity]
  };
  if (axb === 0)
    return ret;

  var dx = a0[0] - b0[0];
  var dy = a0[1] - b0[1];

  ret.alongA = (bdx * dy - bdy * dx) / axb;
  ret.alongB = (adx * dy - ady * dx) / axb;

  ret.point = [
    a0[0] + ret.alongA * adx,
    a0[1] + ret.alongA * ady
  ];

  return ret;
}

它计算两条线的交点,并返回每条线上的交点“沿”多远。因此,例如,如果alongA0.75,那么交集发生在从a0到 的75% 处a1. 

这些值是重要的,因为他们可能是负数或大于1,因此,如果两条线实际相交,我们需要测试alongAalongB0和1(不含)之间.

下一个非交点

由于我们将在我们的链表中插入交点,所以有一个帮助函数来查找下一个交点.

function NextNonIntersection(node){
  do{
    node = node.next;
  } while (node.intersection);
  return node;
}

每个边组合(Edge Pair)

现在我们可以编写迭代每个边组合的代码:

var root1 = UpgradePolygon(poly1);
var root2 = UpgradePolygon(poly2);

var here1 = root1;
var here2 = root2;
do{
  do{
    //
    // TODO: test intersection between:
    //    here1 -> NextNonIntersection(here1)  and
    //    here2 -> NextNonIntersection(here2)
    //
    here2 = NextNonIntersection(here2);
  } while (here2 !== root2);
  here1 = NextNonIntersection(here1);
} while (here1 !== root1);

交叉点测试

给定两个节点,我们可以测试交集:

var next1 = NextNonIntersection(here1);
var next2 = NextNonIntersection(here2);

var i = LinesIntersect(
  here1.point, next1.point,
  here2.point, next2.point
);

if (i.alongA > 0 && i.alongA < 1 &&
  i.alongB > 0 && i.alongB < 1){
  //
  // TODO: insert intersection points in both polygons at
  //       the correct location, referencing each other
  //
}

插入交叉点

最后,如果两条边相交,那么我们要在两个非交点之间插入我们的交叉点.

为了将它插入正确的位置,我们必须跟踪alongAalongB值以确保如果两个交点在同一条边上,它们以正确的顺序插入.

我们将要创建两个节点,一个用于每个多边形——但这些节点应该相互指向,以便我们稍后在遇到交叉点时可以在多边形之间“跳跃”

var node1 = {
  point: i.point,
  intersection: true,
  next: null,
  prev: null,
  dist: i.alongA,
  friend: null
};
var node2 = {
  point: i.point,
  intersection: true,
  next: null,
  prev: null,
  dist: i.alongB,
  friend: null
};

// point the nodes at each other
node1.friend = node2;
node2.friend = node1;

var inext, iprev;

// find insertion between here1 and next1, based on dist
inext = here1.next;
while (inext !== next1 && inext.dist < node1.dist)
  inext = inext.next;
iprev = inext.prev;

// insert node1 between iprev and inext
inext.prev = node1;
node1.next = inext;
node1.prev = iprev;
iprev.next = node1;

// find insertion between here2 and next2, based on dist
inext = here2.next;
while (inext !== next2 && inext.dist < node2.dist)
  inext = inext.next;
iprev = inext.prev;

// insert node2 between iprev and inext
inext.prev = node2;
node2.next = inext;
node2.prev = iprev;
iprev.next = node2;

步骤3. 计算交叉入口/交叉出口

我们知道交叉口在进入和退出之间交替。但是第一个交叉点是什么?是入口还是出口.

简单:如果多边形的第一个点在另一个多边形内,那么第一个交点必须是出口.

但是,计算一个点是否在多边形内部实际上有点复杂.

点在多边形内

function PointInPolygon(point, root){
  var odd = false;
  var x = point[0];
  var y = point[1];
  var here = root;
  do {
    var next = here.next;
    var hx = here.point[0];
    var hy = here.point[1];
    var nx = next.point[0];
    var ny = next.point[1];
    if (((hy < y && ny >= y) || (hy >= y && ny < y)) &&
      (hx <= x || nx <= x) &&
      (hx + (y - hy) / (ny - hy) * (nx - hx) < x)){
      odd = !odd;
    }
    here = next;
  } while (here !== root);
  return odd;
}

PointInPolygon通过计算水平线相交的边数来工作。水平线从(-Infinity, y)(x, y)。它只关心交叉点的数量是奇数还是偶数。它基于光线投射

交替进入/退出

现在我们可以轻松计算出一个交叉点是入口还是出口:

function CalculateEntryExit(root, isEntry){
  var here = root;
  do{
    if (here.intersection){
      here.isEntry = isEntry;
      isEntry = !isEntry;
    }
    here = here.next;
  } while (here !== root);
}

var is1in2 = PointInPolygon(root1.point, root2);
var is2in1 = PointInPolygon(root2.point, root1);

CalculateEntryExit(root1, !is1in2);
CalculateEntryExit(root2, !is2in1);

步骤4. 生成结果

我们已经走了很长一段路!这是我们到目前为止所拥有的.

我们已经计算并插入了交点,并将它们标记为每个多边形的入口或出口.

现在是有趣的部分!

从哪里开始

 我们从哪里开始追踪结果?我们不能只选择一个随机点,因为有些点实际上可以从结果中完全删除.

由于所有操作都包括每个交集,我们应该从寻找未处理的交集开始.

我们添加到最终结果中的每个交点,我们都标记为已处理.

然后,我们只是继续跟踪,直到我们不再有任何交集需要处理.

var result = [];
var isect = root1;
var into = [intoBlue, intoRed]; // explained below
while (true){
  do{
    if (isect.intersection && !isect.processed)
      break;
    isect = isect.next;
  } while (isect !== root1);
  if (isect === root1)
    break;

  //
  // TODO: process isect
  //
}

 转向哪个方向

最后,我们来到了症结所在:

当我们遇到十字路口时,我们怎么知道该往哪个方向转弯?

让我们来推理一下:

Is Entry?Move Into?Move Forward?
TrueTrueTrue
TrueFalseFalse
FalseTrueFalse
FalseFalseTrue

因此,如果 ,我们应该继续前进isEntry === intoPoly

由于我们所在的多边形来回切换,我们只需通过将intoBlue和存储intoRedinto列表中来使我们的决策动态化,并将 其curpoly用作索引.

var curpoly = 0;
var clipped = [];

var here = isect;
do{
  // mark intersection as processed
  here.processed = true;
  here.friend.processed = true;

  var moveForward = here.isEntry === into[curpoly];
  do{
    clipped.push(here.point);
    if (moveForward)
      here = here.next;
    else
      here = here.prev;
  } while (!here.intersection);

  // we've hit the next intersection so switch polygons
  here = here.friend;
  curpoly = 1 - curpoly;
} while (!here.processed);

result.push(clipped);

没有交叉点

如果没有交叉点?

我们的结果集将是空的……这可能是正确的,也可能是错误的——这取决于操作.

一个简单的检查就足以修复它:

if (result.length <= 0){
  if (is1in2 === intoBlue)
    result.push(poly1);
  if (is2in1 === intoRed)
    result.push(poly2);
}

演示

单击此处启动演示!

演示截图

 您可以拖动每个多边形的点,并通过单击按钮切换操作。

 附录:限制

抱歉,这个算法有一个严重的局限性:

您不能拥有完美重叠的点或边.

如果你仔细想想,这是有道理的:整个算法都是基于交叉点的思想.

如果点或边直接重叠,那么您就不会得到那种好的跳跃效果.

最初的论文建议稍微“扰乱”点,这样线条就不会完全重叠。我最初认为这是一个小调整,不会有有问题.

但是,我错了.

扰动点会破坏数据——因此可能很重要的源数据的属性(例如,平滑边缘)变得无效.

“扰动”的真实世界结果

幸运的是我研究了另一种处理一切的算法,并写了一篇后续文章

此处阅读后续内容:多边形裁剪(第 2 部分)

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
多边形偏移是一种用于在计算机图形学中修改多边形表面的技术。通过对多边形的每个顶点进行偏移,可以修改多边形的表面位置,从而实现比如凸出或凹进的效果。 多边形偏移通常用于增加视觉效果,例如渲染立体感或创建阴影效果。在渲染立体模型时,通过对多边形进行适当的偏移,可以使模型的表面看起来更加真实,有一定的立体感。此外,多边形偏移还可以用于生成类似于蚊子网格的效果,使表面看起来更加细致。 多边形偏移的实现通常依赖于图形处理器(GPU)的功能。在渲染管线的几何处理阶段,通常有一个单独的过程来处理多边形偏移。具体实现的方法可以有多种,例如通过修改多边形的顶点坐标或修改深度缓冲区的值来实现偏移效果。 然而,多边形偏移也可能带来一些问题。如果偏移值过大,可能会导致多边形之间的重叠或遮挡关系发生错误。此外,多边形偏移还可能导致视觉上的锯齿或不连续的边缘效果。为了解决这些问题,通常需要调整偏移值或使用其他方法来平滑边缘效果。 总的来说,多边形偏移是一种在计算机图形学中常用的技术,通过修改多边形的表面位置来实现不同的视觉效果。虽然多边形偏移可能会引发一些问题,但通过合理调整参数和采用其他处理方式,可以获得更好的渲染结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值