1

libgdx の耳クリッピング三角測量の問題に苦しんでいます。ある時点で無限ループに入り、プログラム全体がクラッシュします。クラッシュが発生した正確な場所をビデオに記録しました。

Triangulator のソースコード:

/*
 * Copyright 2010 Mario Zechner (contact@badlogicgames.com), Nathan Sweet (admin@esotericsoftware.com), Nicolas Gramlich
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

package com.badlogic.gdx.math;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * A simple implementation of the ear cutting algorithm to triangulate simple
 * polygons without holes. For more information:
 * http://cgm.cs.mcgill.ca/~godfried/teaching/cg-projects/97/Ian/algorithm2.html
 * http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
 * 
 * @author badlogicgames@gmail.com
 * @author Nicolas Gramlich (Improved performance. Collinear edges are now supported.)
 */
public final class EarClippingTriangulator {

  private static final int CONCAVE = 1;
  private static final int CONVEX = -1;

  private int concaveVertexCount;

  /**
   * Triangulates the given (concave) polygon to a list of triangles. The
   * resulting triangles have clockwise order. 
   * @param polygon the polygon
   * @return the triangles
   */
  public List<Vector2> computeTriangles(final List<Vector2> polygon) {
    // TODO Check if LinkedList performs better
    final ArrayList<Vector2> triangles = new ArrayList<Vector2>();
    final ArrayList<Vector2> vertices = new ArrayList<Vector2>(polygon.size());
    vertices.addAll(polygon);

    if(vertices.size() == 3) {
      triangles.addAll(vertices);
      return triangles;
    }

    while(vertices.size() >= 3) {
      // TODO Usually(Always?) only the Types of the vertices next to the ear change! --> Improve
      final int vertexTypes[] = this.classifyVertices(vertices);

      final int vertexCount = vertices.size();
      for(int index = 0; index < vertexCount; index++) {
        if(this.isEarTip(vertices, index, vertexTypes)) {
          this.cutEarTip(vertices, index, triangles);
          break;
        }
      }
    }

    return triangles;
  }

  private static boolean areVerticesClockwise(final ArrayList<Vector2> pVertices) {
    final int vertexCount = pVertices.size();

    float area = 0;
    for(int i = 0; i < vertexCount; i++) {
      final Vector2 p1 = pVertices.get(i);
      final Vector2 p2 = pVertices.get(EarClippingTriangulator.computeNextIndex(pVertices, i));
      area += p1.x * p2.y - p2.x * p1.y;
    }

    if(area < 0) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * @param pVertices
   * @return An array of length <code>pVertices.size()</code> filled with either {@link EarClippingTriangulator#CONCAVE} or
   * {@link EarClippingTriangulator#CONVEX}.
   */
  private int[] classifyVertices(final ArrayList<Vector2> pVertices) {
    final int vertexCount = pVertices.size();

    final int[] vertexTypes = new int[vertexCount];
    this.concaveVertexCount = 0;

    /* Ensure vertices are in clockwise order. */
    if(!EarClippingTriangulator.areVerticesClockwise(pVertices)) {
      Collections.reverse(pVertices);
    }

    for(int index = 0; index < vertexCount; index++) {
      final int previousIndex = EarClippingTriangulator.computePreviousIndex(pVertices, index);
      final int nextIndex = EarClippingTriangulator.computeNextIndex(pVertices, index);

      final Vector2 previousVertex = pVertices.get(previousIndex);
      final Vector2 currentVertex = pVertices.get(index);
      final Vector2 nextVertex = pVertices.get(nextIndex);

      if(EarClippingTriangulator.isTriangleConvex(previousVertex.x, previousVertex.y, currentVertex.x, currentVertex.y, nextVertex.x, nextVertex.y)) {
        vertexTypes[index] = CONVEX;
      } else {
        vertexTypes[index] = CONCAVE;
        this.concaveVertexCount++;
      }
    }

    return vertexTypes;
  }

  private static boolean isTriangleConvex(final float pX1, final float pY1, final float pX2, final float pY2, final float pX3, final float pY3) {
    if(EarClippingTriangulator.computeSpannedAreaSign(pX1, pY1, pX2, pY2, pX3, pY3) < 0) {
      return false;
    } else {
      return true;
    }
  }

  private static int computeSpannedAreaSign(final float pX1, final float pY1, final float pX2, final float pY2, final float pX3, final float pY3) {
    float area = 0;

    area += pX1 * (pY3 - pY2);
    area += pX2 * (pY1 - pY3);
    area += pX3 * (pY2 - pY1);

    return (int)Math.signum(area);
  }

  /**
   * @return <code>true</code> when the Triangles contains one or more vertices, <code>false</code> otherwise.
   */
  private static boolean isAnyVertexInTriangle(final ArrayList<Vector2> pVertices, final int[] pVertexTypes, final float pX1, final float pY1, final float pX2, final float pY2, final float pX3, final float pY3) {
    int i = 0;

    final int vertexCount = pVertices.size();
    while(i < vertexCount - 1) {
      if((pVertexTypes[i] == CONCAVE)) {
        final Vector2 currentVertex = pVertices.get(i);

        final float currentVertexX = currentVertex.x;
        final float currentVertexY = currentVertex.y;

        /* TODO The following condition fails for perpendicular, axis aligned triangles! 
         * Removing it doesn't seem to cause problems. 
         * Maybe it was an optimization?
         * Maybe it tried to handle collinear pieces ? */
//        if(((currentVertexX != pX1) && (currentVertexY != pY1)) || ((currentVertexX != pX2) && (currentVertexY != pY2)) || ((currentVertexX != pX3) && (currentVertexY != pY3))) {
          final int areaSign1 = EarClippingTriangulator.computeSpannedAreaSign(pX1, pY1, pX2, pY2, currentVertexX, currentVertexY);
          final int areaSign2 = EarClippingTriangulator.computeSpannedAreaSign(pX2, pY2, pX3, pY3, currentVertexX, currentVertexY);
          final int areaSign3 = EarClippingTriangulator.computeSpannedAreaSign(pX3, pY3, pX1, pY1, currentVertexX, currentVertexY);

          if(areaSign1 > 0 && areaSign2 > 0 && areaSign3 > 0) {
            return true;
          } else if(areaSign1 <= 0 && areaSign2 <= 0 && areaSign3 <= 0) {
            return true;
          }
//        }
      }
      i++;
    }
    return false;
  }

  private boolean isEarTip(final ArrayList<Vector2> pVertices, final int pEarTipIndex, final int[] pVertexTypes) {
    if(this.concaveVertexCount != 0) {
      final Vector2 previousVertex = pVertices.get(EarClippingTriangulator.computePreviousIndex(pVertices, pEarTipIndex));
      final Vector2 currentVertex = pVertices.get(pEarTipIndex);
      final Vector2 nextVertex = pVertices.get(EarClippingTriangulator.computeNextIndex(pVertices, pEarTipIndex));

      if(EarClippingTriangulator.isAnyVertexInTriangle(pVertices, pVertexTypes, previousVertex.x, previousVertex.y, currentVertex.x, currentVertex.y, nextVertex.x, nextVertex.y)) {
        return false;
      } else {
        return true;
      }
    } else {
      return true;
    }
  }

  private void cutEarTip(final ArrayList<Vector2> pVertices, final int pEarTipIndex, final ArrayList<Vector2> pTriangles) {
    final int previousIndex = EarClippingTriangulator.computePreviousIndex(pVertices, pEarTipIndex);
    final int nextIndex = EarClippingTriangulator.computeNextIndex(pVertices, pEarTipIndex);

    if(!EarClippingTriangulator.isCollinear(pVertices, previousIndex, pEarTipIndex, nextIndex)) {
      pTriangles.add(new Vector2(pVertices.get(previousIndex)));
      pTriangles.add(new Vector2(pVertices.get(pEarTipIndex)));
      pTriangles.add(new Vector2(pVertices.get(nextIndex)));
    }

    pVertices.remove(pEarTipIndex);
    if(pVertices.size() >= 3) {
      EarClippingTriangulator.removeCollinearNeighborEarsAfterRemovingEarTip(pVertices, pEarTipIndex);
    }
  }

  private static void removeCollinearNeighborEarsAfterRemovingEarTip(final ArrayList<Vector2> pVertices, final int pEarTipCutIndex) {
    final int collinearityCheckNextIndex = pEarTipCutIndex % pVertices.size();
    int collinearCheckPreviousIndex = EarClippingTriangulator.computePreviousIndex(pVertices, collinearityCheckNextIndex);

    if(EarClippingTriangulator.isCollinear(pVertices, collinearityCheckNextIndex)) {
      pVertices.remove(collinearityCheckNextIndex);

      if(pVertices.size() > 3) {
        /* Update */
        collinearCheckPreviousIndex = EarClippingTriangulator.computePreviousIndex(pVertices, collinearityCheckNextIndex);
        if(EarClippingTriangulator.isCollinear(pVertices, collinearCheckPreviousIndex)){
          pVertices.remove(collinearCheckPreviousIndex);
        }
      }
    } else if(EarClippingTriangulator.isCollinear(pVertices, collinearCheckPreviousIndex)){
      pVertices.remove(collinearCheckPreviousIndex);
    }
  }

  private static boolean isCollinear(final ArrayList<Vector2> pVertices, final int pIndex) {
    final int previousIndex = EarClippingTriangulator.computePreviousIndex(pVertices, pIndex);
    final int nextIndex = EarClippingTriangulator.computeNextIndex(pVertices, pIndex);

    return EarClippingTriangulator.isCollinear(pVertices, previousIndex, pIndex, nextIndex);
  }

  private static boolean isCollinear(final ArrayList<Vector2> pVertices, final int pPreviousIndex, final int pIndex, final int pNextIndex) {
    final Vector2 previousVertex = pVertices.get(pPreviousIndex);
    final Vector2 vertex = pVertices.get(pIndex);
    final Vector2 nextVertex = pVertices.get(pNextIndex);

    return EarClippingTriangulator.computeSpannedAreaSign(previousVertex.x, previousVertex.y, vertex.x, vertex.y, nextVertex.x, nextVertex.y) == 0;
  }

  private static int computePreviousIndex(final List<Vector2> pVertices, final int pIndex) {
    return pIndex == 0 ? pVertices.size() - 1 : pIndex - 1;
  }

  private static int computeNextIndex(final List<Vector2> pVertices, final int pIndex) {
    return pIndex == pVertices.size() - 1 ? 0 : pIndex + 1;
  }
}

クラッシュは、上記のソース コードの 53# 行で発生します...
次の条件で computeTriangles 関数の while ループ: while(vertices.size() >= 3) { ...

4

1 に答える 1

2

問題は、ポリゴンが単純である必要があることです。つまり、それ自体と交差することはできません。EarClippingTriangulatorjavadoc はこの事実を述べています。ただし、無効な入力で無限ループに入るのは、かなり悪い動作です。

とは言うものの、三角測量機が有効な単純な多角形を自己交差のある多角形にクリップしてしまい、無限ループに陥ってしまうバグもあります。これは、ポイントが (ほぼまたは正確に) 同一線上にある場合に発生する可能性があります。これに関する少なくとも 2 つのバグ レポートがあります。 自分自身による#1407#207

マージされたばかりのプル リクエストを送信したので、2013 年 6 月 30 日以降の libgdx リリースでは、これらの問題が少なくなるはずです。

于 2013-06-30T04:46:15.693 に答える