【问题标题】:sorting a list of 3D points in clockwise order按顺时针顺序对 3D 点列表进行排序
【发布时间】:2018-06-05 13:53:01
【问题描述】:

我有一个 3D 点列表和一个中心,我想围绕给定的法线向量按(逆)时针顺序对它们进行排序。这些点不共面,但它们和中心绑定到球体的表面并且它们勾勒出多边形。法线向量是从球心到排序中心的向量。我试过this comparison function,但是当两点之间的距离超过π/2时它会失败。

如何获得任意一组点的实际 3D(逆时针)顺时针顺序?

这不是Sorting 3D points on the surface of a sphere in clockwise order 的重复,因为这个问题专门针对角度比较中缺乏传递性的问题。

这不是Sorting a List of 3d coplanar points to be clockwise or counterclockwise 的重复,因为这个问题更多是关于确定一个点是更接近于顺时针还是逆时针,虽然这是一个比较关系,但它并没有给出明确的定义总排序。

【问题讨论】:

    标签: sorting math 3d geometry


    【解决方案1】:

    如您所见,单点积不能单独工作,因为它是一个标量余弦,并且每个余弦值都对应于单位圆的两个点。

    因此,解决方案的一种方法是在法线给出的平面中找到两个垂直的参考向量,然后将它们取三倍积。它们将是可用于排序的角度的正弦和余弦。因此,您可以使用 atan2(y,x) 来获得精确的角度,或者 - 如果速度很重要 - 使用斜率和反斜率来近似 atan2/(pi/4)

    为了得到你需要的两个向量,首先取最长的叉积I x nJ x nK x n,其中IJK是单位轴向量。将此向量称为p。它必须位于平面内,因为它垂直于n。 (为了避免浮点精度问题,您花费的时间最长。)

    现在计算q = n x p。这也位于平面内,因为它垂直于n,但它也垂直于p ...正是我们需要的。

    回顾一下,pq 是任何平面中的垂直向量,n 是其法线。

    现在如果c 是中心,对于多边形中的每个点r,计算三重乘积t = n * ((r - c) x p)u = n * ((r - c) x q)。那么atan2(u, t) 或其近似值就是一个排序度量。

    演示

    只是为了证明这确实有效,包括atan2 近似值:

    public class Sorter3d {
    
      // Sorting key metric calculator.
      static class Order {
        final Vec n, pp, qp;
        final Pt c;
    
        Order(Vec n, Pt c) { 
          this.c = c;
          this.n = n;
          pp = n.cross(Vec.I).longer(n.cross(Vec.J)).longer(n.cross(Vec.K));
          qp = n.cross(pp);
        } 
    
        double getKey(Pt r) {
          Vec rmc = r.minus(c);
          return approxAtan2(n.dot(rmc.cross(pp)), n.dot(rmc.cross(qp)));
        }
      }
    
      // Affine 3d vectors.
      static class Vec {
        static final Vec I = Vec.of(1, 0, 0);
        static final Vec J = Vec.of(0, 1, 0);
        static final Vec K = Vec.of(0, 0, 1);
        final double x, y, z;
        private Vec(double x, double y, double z) { this.x = x; this.y = y; this.z = z; }
        static Vec of(double x, double y, double z) { return new Vec(x, y, z); }
        Vec cross(Vec o) { return Vec.of(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x); }
        double dot(Vec o) { return x * o.x + y * o.y + z * o.z; }
        double dot(Pt o) { return x * o.x + y * o.y + z * o.z; }
        double len2() { return dot(this); }
        double len() { return Math.sqrt(len2()); }
        Vec scale(double s) { return Vec.of(x * s, y * s, z * s); }
        Vec unit() { return scale(1.0 / len()); }
        Vec longer(Vec o) { return len2() > o.len2() ? this : o; }
        public String toString() { return String.format("[%.3f,%.3f,%.3f]", x, y, z); }
      }
    
      // Affine 3d points.
      static class Pt {
        static final Pt O = Pt.of(0, 0, 0);
        final double x, y, z;
        private Pt(double x, double y, double z) { this.x = x; this.y = y; this.z = z; }
        static Pt of(double x, double y, double z) { return new Pt(x, y, z); }
        Pt plus(Vec o) { return Pt.of(x + o.x, y + o.y, z + o.z); }
        Vec minus(Pt o) { return Vec.of(x - o.x, y - o.y, z - o.z); }
        public String toString() { return String.format("(%.3f,%.3f,%.3f)", x, y, z); }
      }
    
      // Return approximation of atan2(y,x) / (PI/2);
      static double approxAtan2(double y, double x) {
        int o = 0;
        if (y < 0) { x = -x; y = -y; o |= 4; }
        if (x <= 0) { double t = x; x = y; y = -t; o |= 2; }
        if (x <= y) { double t = y - x; x += y; y = t; o |= 1; }
        return o + y / x;
      }
    
      public static void main(String [] args) {
        // Make some random points radially sorted about the Z axis.
        int nPts = 17;
        Pt [] pts = new Pt[nPts];
        for (int i = 0; i < nPts; ++i) {
          double r = 1.0 + 10 * Math.random();
          double theta = i * (2 * Math.PI / nPts);
          pts[i] = Pt.of(r * Math.cos(theta), r * Math.sin(theta), 40.0 * (1 - Math.random()));
        }
        // Pick arbitrary normal vector and center point.
        // Rotate z-axis to normal and translate origin to center.
        Vec normal = Vec.of(-42.0, 17.0, -91.0);
        Vec cx = Vec.J.cross(normal).unit();
        Vec cy = normal.cross(cx).unit();
        Vec cz = normal.unit();
        Vec rx = Vec.of(cx.x, cy.x, cz.x);
        Vec ry = Vec.of(cx.y, cy.y, cz.y);
        Vec rz = Vec.of(cx.z, cy.z, cz.z);
        Pt center = Pt.of(11, 12, 13);
        Vec ofs = center.minus(Pt.O);
        Pt [] xPts = new Pt[nPts];
        for (int i = 0; i < nPts; ++i) {
          xPts[i] = Pt.of(rx.dot(pts[i]), ry.dot(pts[i]), rz.dot(pts[i])).plus(ofs);
        }
        // Check the sort keys returned by the sorter.
        Order order = new Order(normal, center);
        for (int i = 0; i < nPts; ++i) {
          System.out.println(order.getKey(xPts[i]));
        }
      }
    }
    

    这会打印一个有效的密钥顺序:

    4.0
    3.9924071330572093
    3.982224060033384
    3.9612544376696253
    3.8080585081381275
    0.03457371559793447
    0.013026386180392412
    0.006090856009723169
    0.0018388671161891966
    7.99632901621898
    7.987892035846782
    7.974282237149798
    7.93316335979413
    4.106158894193932
    4.019755500146331
    4.008967674404233
    4.003810901304664
    

    【讨论】:

    • 可以避免最后一次调用atan2(u, t) 或用更简单的函数替换吗?我不需要角度只是一个总排序
    • 正如我所说,使用斜率和反斜率代替atan2 很容易。我将添加更多解释。
    【解决方案2】:

    好的,我已经设法找到我自己的解决方案,它只使用点积和叉积,没有反三角或平方根或任何东西。您选择列表中的第一个顶点v 并将其用作参考。然后将向量r = v - center 与法线向量交叉,得到半空间分割向量p。如果两个输入在p 的同一侧,那么您可以毫无问题地使用三重乘积,因为它们之间的圆柱角将小于 π。虽然有一些边缘情况需要注意,所以我想我只是分享一些伪代码。

    let c be the center around which the counterclockwise sort is to be performed
    let n be the normal vector 
    
    r := vertices[0] - c // use an arbitrary vector as the twelve o’clock reference
    p := cross(r, c)     // get the half-plane partition vector
    
    // returns true if v1 is clockwise from v2 around c
    function less(v1, v2):
        u1 := v1 - c
        u2 := v2 - c
        h1 := dot(u1, p)
        h2 := dot(u2, p)
    
        if      h2 ≤ 0 and h1 > 0:
            return false
    
        else if h1 ≤ 0 and h2 > 0:
            return true
    
        else if h1 = 0 and h2 = 0:
            return dot(u1, r) > 0 and dot(u2, r) < 0
    
        else:
            return dot(cross(u1, u2), c) > 0
    
        //           h2 > 0     h2 = 0     h2 < 0
        //          ————————   ————————   ————————
        //  h1 > 0 |   *        v1 > v2    v1 > v2
        //  h1 = 0 | v1 < v2       †          *
        //  h1 < 0 | v1 < v2       *          *
    
        //  *   means we can use the triple product because the (cylindrical)
        //      angle between u1 and u2 is less than π
        //  †   means u1 and u2 are either 0 or π around from the zero reference
        //      in which case u1 < u2 only if dot(u1, r) > 0 and dot(u2, r) < 0
    

    【讨论】:

    • 啊,好吧。我在想你需要一个围绕法线的极角度量,带有一些任意零。除非比较函数的结果代表一个总顺序,否则许多排序将无法可靠地工作。使用像这样的成对比较,您将不得不找到一种方法来打破循环,否则请仔细选择排序算法..
    • 我应该补充一点,很久以前我实际上在循环订单上实现了一个比较函数,非常像这样没有考虑太多,并且我系统上的 C qsort() 并没有可靠地返回一个循环顺序。
    • 没关系。我看到您从参考向量中获得了总订单。请注意,如果参考向量的选择不好,这种方法可能会失败。 (还要注意我的方法不需要平方根。我已经放弃了。)
    • 基因:哪个参考向量不好?具有 0 分量的向量?
    • @taylor swift:我看不出你在哪里实际使用了正常的“n”。
    【解决方案3】:

    您将点投影在垂直于法线的平面上(从法线形成正交框架)。然后在这个平面上,使用极坐标并按角度排序。无论如何请注意,角度的零是任意的。

    【讨论】:

      猜你喜欢
      • 2011-10-22
      • 1970-01-01
      • 1970-01-01
      • 2011-10-16
      • 1970-01-01
      • 2010-09-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多