andywiecko / BurstTriangulator

2d Delaunay triangulation with mesh refinement for Unity with Burst compiler

Home Page:https://andywiecko.github.io/BurstTriangulator/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Incorrect triangulation when using constrained edges

HalfVoxel opened this issue · comments

Thanks for making this package!

I modified one test case slightly, and I can get a repeatable very buggy output.

[Test, TestCaseSource(nameof(constraintBenchmarkTestData))]
public void ConstraintBenchmarkTest((int count, int N) input)
{
    var (count, N) = input;
    var debuggerInitialValue = Unity.Jobs.LowLevel.Unsafe.JobsUtility.JobDebuggerEnabled;
    Unity.Jobs.LowLevel.Unsafe.JobsUtility.JobDebuggerEnabled = false;

    var points = new List<float2>(count * count);
    for (int i = 0; i < count; i++)
    {
        for (int j = 0; j < count; j++)
        {
            var p = math.float2(i / (float)(count - 1), j / (float)(count - 1));
            points.Add(p);
        }
    }

    var constraints = new List<int>(N + 1);
    for (int k = 0; k < 2; k++) {
        var offset = points.Count;
        for (int i = 0; i < N; i++)
        {
            var phi = 2 * math.PI / N * i + 0.1452f;
            var p = (0.06f * (k+1)) * math.float2(math.cos(phi), math.sin(phi)) + 0.5f;
            points.Add(p);
            if (i < N - 2) {
                constraints.Add(offset + i);
                constraints.Add(offset + ((i + 1) % N));
            }
        }
    }

    for (int i = 0; i < constraints.Count; i += 2) {
        var a = points[constraints[i]];
        var b = points[constraints[i + 1]];
        UnityEngine.Debug.DrawLine(new UnityEngine.Vector3(a.x, a.y, -0.05f), new UnityEngine.Vector3(b.x, b.y, -0.05f), UnityEngine.Color.blue, 5);
    }

    using var positions = new NativeArray<float2>(points.ToArray(), Allocator.Persistent);
    using var constraintEdges = new NativeArray<int>(constraints.ToArray(), Allocator.Persistent);

    var stopwatch = Stopwatch.StartNew();
    using var triangulator = new Triangulator(capacity: count * count + N, Allocator.Persistent)
    {
        Input = { Positions = positions, ConstraintEdges = constraintEdges },
        Settings = {
            RefineMesh = false,
            ConstrainEdges = true,
            RestoreBoundary = false,
            ValidateInput = true
        },
    };

    var dependencies = default(JobHandle);
    var rep = 1;
    for (int i = 0; i < rep; i++) dependencies = triangulator.Schedule(dependencies);
    dependencies.Complete();
    stopwatch.Stop();
    var log = $"{N} {stopwatch.Elapsed.TotalMilliseconds / rep}";
    UnityEngine.Debug.Log(log);
    triangulator.Draw(5);

    // NOTE: Uncomment this to write all the test cases into a file.
    //using var writer = new System.IO.StreamWriter("tmp.txt", true);
    //writer.WriteLine(log);

    Unity.Jobs.LowLevel.Unsafe.JobsUtility.JobDebuggerEnabled = debuggerInitialValue;
}

Interestingly, it only happens for some input combinations. For N<204, it doesn't seem to happen.

count=100, N=201
bild

count=100, N=204
bild

The errors are the same every time, so I don't think it's simply uninitialized output.

PS: I'm working on integrating this package with my code, and in the process, I'll probably refactor it quite a lot. Hopefully improving performance in the process. I'll be happy to share any changes that I make :)

After some investigation:

  • The input validation does not trigger
  • This happens even when only running the delaunay triangulation, without constraining any edges.

Hi @HalfVoxel,

Thank you for your detailed contribution. I appreciate it.

Lately, I haven't had much time due to personal issues. However, in the coming weeks, I plan to make some changes to the package, especially since there have been reported issues in #105. Any contributions are welcome :)

This happens even when only running the delaunay triangulation, without constraining any edges.

See my comment in #105 (comment). I suspect there might be some internal bugs related to Burst. Have you attempted to run your example with Burst disabled? I'll give it a try this evening.

I will let you know if I find out any fixes.

Best,
Andrzej

Hi

Indeed running it without burst seems to resolve the issue.
It seems to have something to do with hash collisions in the DelaunayTriangulationJob. Raising the size of the hash array also solves the issue, even when using burst. This hash array seems very sketchy to be honest, since any collisions will cause problems (though I can't say I understand the code fully yet).

Possibly the bug exists outside of burst too, it's just that some burst optimizations make it more visible (e.g. slightly different floating point operations).

Great!

Thank you for informing me about the hash collisions.
I believe the easiest solution would be to increase the array size and make it a parameter in the triangulator settings.

The DelaunayTriangulationJob is adapted from delaunator.

I've already tried to adjust with Burst compile options (FloatPrecision, FloatMode, ...) without success.

Hi @HalfVoxel ,

I have great news! I've found what is causing the problem. There were issues with floating-point precision.
After changing from float2 to double2, it triangulates correctly and we obtain the desired result.

Before

image

After

image


I'm going to prepare a new release next week which will fix this issue.

Best,
Andrzej

Cool!

Would still be interesting to know what actually caused this. As doubles is still a floating point format.

Do you know what impact this has on performance?

I was actually considering making your package support int2 coordinates, for improved robustness.

Would still be interesting to know what actually caused this. As doubles is still a floating point format.

I believe single-precision floats encounter difficulties with Orient2d, as illustrated in the first figure on this page. At least, that's my current explanation for why floats are not suitable for complex triangulations.

Do you know what impact this has on performance?

I'm going to measure the performance with float2 vs double2. This note will be certainly included in README.md as well as release notes.


I also have a question for you. As a user, would you like to have control over the floating-point precision to switch between float2 and double2? Do you have any suggestions for the API? I've been thinking about implementing a Triangulator.Double class with the same API as Triangulator (probably using some codegen).
In a similar manner we can define Triangulator.Int.

Aha.
Yeah, floats are not great for this. Doubles will have the same problem, of course, just at more extreme scales.

I also have a question for you. As a user, would you like to have control over the floating-point precision to switch between float2 and double2? Do you have any suggestions for the API? I've been thinking about implementing a Triangulator.Double class with the same API as Triangulator (probably using some codegen).
In a similar manner we can define Triangulator.Int.

If floats cannot be used reliably, I probably never would want to use them. There's no point using a library if it can cause errors in random situations (even if it's a low probability like 1 in 100000 cases, it definitely will happen to some user/player).
I would want to use integer coordinates, though.
I have some ideas for how this can be done, and I'll probably submit a PR. But I'll wait until you have reviewed my current PR :)

If floats cannot be used reliably, I probably never would want to use them. There's no point using a library if it can cause errors in random situations (even if it's a low probability like 1 in 100000 cases, it definitely will happen to some user/player).

I have been considering preserving the float2 data type primarily for performance reasons, although I need to conduct measurements to verify this. I believe that for 'easy' and controlled input, float2 should be deemed as reliable and stable. However, double2 precision should be enabled by default. Maybe some user will benefit from this.
Another advantage of using double2 is that we can easily adapt "robust-predicates" for them.

Regarding performance improvements, I've been working on a local branch to optimize the constraintHalfedges construction, reducing complexity from $\mathcal O(mn)$ to $\mathcal O(n)$, where $m$ halfedges count and $n$ input constraints count. I'll merge this branch after your PR. I truly appreciate the solid work you've done here; congratulations!

I would want to use integer coordinates, though.
I have some ideas for how this can be done, and I'll probably submit a PR.

I'm excited to see such features. They could be incredibly beneficial, not only for performance reasons but also because they pave the way for using this package in scenarios such as lock-step simulation where fixed-point arithmetic is necessary. It's an excellent idea to add support for int2 coordinates!

Once again, thank you for the PR #110 . I'll review it by the end of the week.