9

Given a mesh in Unity & C# (that itself was created in realtime by merging simpler base meshes), how could we during runtime* turn it into a smooth, almost like wrapped-in-cloth mesh version of itself? Not quite a fully convex version, but more rounded, softening sharp edges, bridging deep gaps and so on. The surface would also ideally look like when the "smoothing angle" normals setting is applied to imported objects. Thanks!

enter image description here Before & after sketch

*The mesh setup is made by people and its specifics unknown beforehand. All its basic shape parts (before we merge them) are known though. The base parts may also remain unmerged if that helps a solution, and it would be extra terrific if there was a runtime solution that would fastly apply the wrapper mash even with base parts that change their transform over time, but a static one-time conversion would be great too.

(Some related keywords may be: marching cube algorithm & metaballs, skin above bones, meshfilter converting, smoothing shader, softening, vertices subdivision.)

Philipp Lenssen
  • 8,818
  • 13
  • 56
  • 77

1 Answers1

10

There are many ways to get something similar so you can pick your preferred one:

Marching Cubes

This algorithm is easy to use but the result always inherits the blocky 'style' of it. If that's the look you want then use it. If you need something more smooth and/or pixel perfect then look for other ways.

Ray Marching and Signed Distance Functions

This is quite interesting technique that may give you a lot of control. You can represent your base parts with simple cube/cylinder/etc. equations and blend them together with simple math.

Here you can see some examples: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm

The best thing here is that it's very simple to setup, you don't even need to merge your base parts, you just push your data to renderer. Worse, is that it may get computationaly hard on rendering part.

Old school mesh modifications

Here you have the most options but it's also most complicated. You start with your base parts which don't have much data by themselves so you should probably join them into one mesh using CSG Union operation.

Having this mesh you can compute neighbors data for your primitives:

  • for each vertex find triangles containing it.
  • for each vertex find edges containing it.
  • for each edge find triangles containing it.

etc.

With such data you may be able to do things like:

  • Find and cut some sharp vertex.
  • Find and cut some sharp edge.
  • Move the vertex to minimize angle between triangles/edges it creates.

and so on...

There are really a lot of details that may work for you or not, you just need to test some to see which one gives the preferred results .

One simple thing I'd start with:

  • For each vertex find all vertices connected to it by any edge.
  • Compute average position of all those vertices.
  • Use some alpha parameter in [0,1] range to blend between initial vertex position and averaged one.
  • Implement multiple iterations of this algorithm and add parameter for it.
  • Experiment with alpha and number of iterations.

Using this way you also have two distinct phases: computation and rendering, so doing it with animation may become too slow, but just rendering the mesh will be faster than in Ray Marching approach.

Hope this helps.

EDIT:

Unfortunately I've never had such need so I don't have any sample code but here you have some pseudo-code that may help you:

You have your mesh:

Mesh mesh;

Array of vertex neighbors:

For any vertex index N, triNeighbors[N] will store indices of other vertices connected by edge

List<HashSet<int>>     triNeighbors = new List<HashSet<int>>();

int[] meshTriangles = mesh.triangles;
// iterate vert indices per triangle and store neighbors
for( int i = 0; i < meshTriangles.Length; i += 3 ) {
    // three indices making a triangle
    int v0 = meshTriangles[i];
    int v1 = meshTriangles[i+1];
    int v2 = meshTriangles[i+2];
    int maxV = Mathf.Max( Mathf.Max( v0, v1 ), v2 );

    while( triNeighbors.Count <= maxV )
        triNeighbors.Add( new HashSet<int>() );

    triNeighbors[v0].Add( v1 );
    triNeighbors[v0].Add( v2 );

    triNeighbors[v1].Add( v0 );
    triNeighbors[v1].Add( v2 );

    triNeighbors[v2].Add( v0 );
    triNeighbors[v2].Add( v1 );
}

Now, for any single vertex, with index N you can compute its new, averaged position like:

int counter = 0;
int N = 0;
Vector3 sum = Vector3.zero;
if( triNeighbors.Count > N && triNeighbors[N] != null )
{
    foreach( int V in triNeighbors[N] ) {
        sum += mesh.vertices[ V ];
        counter++;
    }
    sum /= counter;
}

There may be some bugs in this code, I've just made it up but you should get the point.

kolenda
  • 2,741
  • 2
  • 19
  • 30
  • Thank you very much! The last approach may be one of the most suitable indeed. I already have all the parts merged into a single mesh (using Unity's CombineMeshes). Do you have some C# sample code that could get me started on the vertex position averager? – Philipp Lenssen May 10 '18 at 16:09
  • @PhilippLenssen I wrote you some sample pseudo-code. – kolenda May 16 '18 at 10:26
  • Thank you! Also, it's important that the resulting mesh shows a smooth surface... that "smoothing angle" setting one can adjust in Unity's obj model importer near the normals settings. – Philipp Lenssen May 17 '18 at 15:36
  • For smoothing you'd need similar struct, that for any given vertex index N store all triangles that use it. Then compute normals for those triangles, add them and normalize - that's the vertex normal. – kolenda May 17 '18 at 15:50
  • You can also try such a trick - you already have all vertex neighbors for any given vertex. They don't lie on a single plane, but if you could find a plane that is close to them as much as possible, then its normal would be very close to the value you need. Just an idea to try. – kolenda May 17 '18 at 15:56
  • Hi! I've now been trying to incorporate your sample code, but it throws errors -- would you be able to check if it works on your end (and also pls let me know if I need any "using" directive)? Thanks! – Philipp Lenssen Jun 25 '18 at 09:50
  • This was just a quick and dirty pseudo code, I've never compiled it on my side. Could you show us the errors you have? – kolenda Jun 25 '18 at 10:10
  • Sure, below. Would you be able to test the code on your end please? Your answer got the 100 karma bounty already. I know some errors below are trivial (upper-case .zero) but some I don't understand. Thanks! CS0308: The non-generic type `System.Array' cannot be used with the type arguments CS0019: Operator `<' cannot be applied to operands of type `int' and `int[]' CS0841: A local variable `triNeighbors' cannot be used before it is declared CS0117: `UnityEngine.Vector3' does not contain a definition for `Zero' CS0841: A local variable `triNeighbors' cannot be used before it is declared – Philipp Lenssen Jun 25 '18 at 13:49
  • I've copied it to my project and fixed all errors I had, please check the updated code. If you still have problems let me know. – kolenda Jun 25 '18 at 20:43
  • Sorry, the line "foreach (int V in triNeighbors[N]) {" keeps throwing several errors (undefined N, and V not convertabel to int) -- if this runs for you, may I ask what .Net version you are using in Unity? Is this maybe a new feature? (Edit: Hmm, it doesn't even work for me when I switch to .Net 4.) – Philipp Lenssen Jun 26 '18 at 05:30
  • You need to understand first what the code is trying to do. The `N` parameter was meant to be provided by you - this is the index of vertex you're averaging. I've added this param to the code, try now. Regarding .Net version I'm not sure at the moment, I'll check when I get back to my computer. – kolenda Jun 26 '18 at 07:53
  • Unity 2018.1.3f1 and ".NET 4.x Equivalent" – kolenda Jun 26 '18 at 22:02
  • Thanks! I'm not getting "Argument ouf of range" on the very first execution of triNeighbors[v0].Add(v1). A fully working example (including the part where it's then average in the later loop) would be very much appreciated, but I understand time is a limited resource! – Philipp Lenssen Jun 27 '18 at 09:24
  • It looks like this code http://answers.unity.com/comments/1374301/view.html is doing what yours aims to as well, but when I run it it doesn't do anything visible to the mesh in terms of softening it. – Philipp Lenssen Jun 27 '18 at 09:44
  • The code you've linked smooths normals only, not the geometry but is similar in the idea. They iterate over vertices, look for all tris using current vertex and average their normals. My code iterates over vertices, looks for all vertices connected by an edge and average their position. Regarding your current error I'll try to make it fully working but I'm not sure if I manage to do it before the weekend. Cheers. – kolenda Jun 29 '18 at 12:32
  • I've fixed initialization of triNeighbors, try now. – kolenda Jul 02 '18 at 22:06