Automatically removing UV overlap if generated

USING: UE 5.4.4

GOAL: I am creating a dynamic mesh and a dynamic texture. I need the texture UV map to have no overlap (as it would for a lightmap).

PROBLEM: Using “Auto Generate Patch Builder Mesh UVs” I end up with an island that when unwrapped overlaps itself.

ALREADY TESTED:

  • Using XAtlas instead of PatchBuilder: This is very slow to run and yields a tattered UV map.
  • Repack using Conformal or SpectralConformal: This results in a loss or resolution where UV triangles become very small, but can still yield overlap.
  • Generating Lightmap UVs: same generation → same problem, bigger padding size
  • Manually adding seams to the self-overlapping island: this solves the problem - so I just need to automate this step!

REQUEST:
It seems likely that many of the pieces (maybe all!) needed to address this exist within UE. The problem of course is that “to command the creature you must know its true name” so what I am looking for are links to documentation, or paths to “documentation” (code).

PLAN OF IMPLEMENTATION:

  • Identify overlapping triangles. Since lightmaps can warn about this, it seems like a solution already exists somewhere.
  • Group adjacent triangles with overlap. I can just use vertex index to determine this. But, I’m guessing that a data structure already is defined somewhere that does this.
  • Create a seam to separate the overlap islands. Here, I should only duplicate internal vertices. Since island edges are drawn it seems like a solution exists to identify boundary edges and vertices.

( Ryan Schmidt, if you’re on this forum, thank you for building an amazing library! )

Getting boundaries:

For a standard install on Windows:
C:\Program Files\Epic Games\UE_5.4\Engine\Plugins\Runtime\GeometryScripting\Source\GeometryScriptingCore\Public\GeometryScript\MeshSelectionQueryFunctions.h

From blueprint node:
GetMeshSelectionBoundaryLoops

  • Get the boundary of an overlap island
  • Get the boundary of the containing island
  • The difference are the vertexes of the internal seam(s) that need to be added

Creating Seams:

C:\Program Files\Epic Games\UE_5.4\Engine\Plugins\Runtime\GeometryProcessing\Source\DynamicMesh\Public\Parameterization\DynamicMeshUVEditor.h

C++ method
CreateSeamsAtEdges

UPDATE: Solved the problem…

There are a few ways to check for overlap:

  • Check for overlap between all pairs of triangles.
  • Compute pixels associated with each triangle in the texture, and check for repeated indexes.

Since I don’t know of a quad tree or 2D spatial hash in Unreal (they’re probably both in there… somewhere) I went for the pixel route. This has the advantage that I can adapt it to include padding pixels that are outside of the triangle so I can also verify that the unwrapped gutters are not overlapping.

Given the set of overlapping pixels I computed the set of overlapping triangles…

Then I find the boundary loops:
UE::Geometry::FMeshRegionBoundaryLoops Loops(&Mesh, OverlapTriangleSet.Array(), true);

And then I create a seam at every loop:

UE::Geometry::FDynamicMeshUVEditor UVEditor(&Mesh, UVLayerIndex, false);
for(const UE::Geometry::FEdgeLoop& Loop : Loops.Loops) {
	UE::Geometry::FUVEditResult Result;
	UVEditor.CreateSeamsAtEdges(TSet(Loop.Edges), &Result);
	...
}

Now, with the new islands created by these added seams I can re-pack.

But wait… this isn’t a complete solution!

If there is a group of connected triangles that are flagged as overlapping that self-overlaps the group will not be separated by any seam, so it will still self overlap.

At this point, instead of creating boundary loops for all over the overlapping triangles, create a loop for each triangle, so each triangle is separated.

The need for this step could be detected using connectivity information… or just perform the same process on the repacked chart, with the triangle separation applied.

Hopefully UE updates their UV generation pipeline to include this step… until then I hope this helps someone.

FOLLOWUP: After creating islands from all of the overlapping regions I accidentally recomputed and then repacked the mesh. And discovered that the UVs had a different distortion that once again results in overlap!

Below, the overlap resulting from generated UVs is shown in magenta:

All of the overlap is in one UV island.

Following the algorithm above, seams are added around the borders of the overlap making them into separate islands, after which I execute a repack. Below, you can see that the original large island has distorted into a new shape that once again overlaps itself:

The top-left island is now top-center and rotated. Comparing the shapes you can see where an overlapping island was removed, and the distortion pushed the shape into another overlap.

EXPLANATION: Executing the “Recompute Mesh UVs” node will change the distortion. Only the “Repack Mesh UVs” node should be used.

P.S.
The behavior of the UV generation seems to vary with time. Yesterday I was not able to reproduce the overlapping results. Today I have returned to the original behavior that yields an obvious overlap. BUT, the results tend to be consistent run-to-run and even across restarts of the Editor. If anyone else reading this is trying to resolve this problem, keep a copy of example bad meshes, since you might not be able to regenerate them, even though the process appears to be deterministic on timescales of a day.

FOLLOWUP: For clarity / completeness I’m attaching an example of a self-overlapping connected region of a UV chart.

Personally, I would regard the creation of this kind of chart as a bug. But, since there isn’t much in the way of documentation on “Patch Builder” or “Exp Map” process this could be expected behavior.

In any case, it’s what it does, so now you know to expect it :wink:

FOLLOWUP: Another thing that might be expected of a UV generator is a consistent winding orientation of UVs. (Left-handed, since this Unreal.)

It turns out that while the output of PatchBuilder and ExpMap is mostly left handed, it is NOT always left handed. For me, this resulted in have triangles that appeared to contain no pixels, since I was relying on winding number to check whether a pixel was inside of a padded triangle.

This became much more apparent when I switched to using XAtlas, which seems to orient triangles randomly.

So, once again - it might be expected behavior or it might be a bug. But now you know to expect it!