0

I've been working on this for around 2 weeks now, most of it works but I just can't get the last part to work, it's way beyond my math skills.

I'll try to explain what I'm working on first to give some context :

We have microsoft visio file representing rooms, with all kinds of things in it. We export that to json (see attached file for a simple test exemple). I've separated this into two arrays, the prefabs that I just place with a position / rotation, really easy, and the "meshes" (be it walls, floor, ceiling, whatever).

One serialized meshes consists of an even number of points, that I'm now able to process as a mesh, with correct vertices, triangles, and uvs. The main and big problem is, these meshes can have one (or multiple) children in it, also representing prefabs, that I need to "cut" from the mesh (for exemple, a door or a window in a wall). Using blender (and a lot of time and effort) I was able to make this work for a single hole, because I just found/hand calculated all triangles that the mesh needs. It's not pretty, but it works. The problem is, more than one hole, the triangles depends on the holes positions and it's just not possible to hardcode it anymore. See the attached .blend file that I made to help visualize everything.

After trying/testing a lot, I noticed that the "outer border" of the mesh, aswell as each hole's inner border could be triangulated pretty easily (at least my algorithm seems to work). So now I only need a way to triangulate the front and back faces (and I'm pretty sure that if I triangulate one, I can use it for the other one by just reversing it to preserve the clockwise-winding). At the moment, I tried using a Delaunay triangulation algorithm, it seems to work but I'm still trying things with it. The problem is, it uses vertices on a 2d plane, where I am in 3d, and I just can't make it works (for exemple, work when the mesh is facing Z, or -Y, etc), like "generalize" it.

I'm sorry for the long text, hope I didn't make too many mistakes (I'm not english) and hope it's fairly clear, if not, please ask question I'll happily answer.

As my code is now pretty long, I've made some pastebins with the relevants classes :

public class SerializedScene
{
    public string version;
    public SerializedPosition playerPosition;    
    public List<SerializedGameObject> hierarchy;
    public List<SerializedMesh> meshes;
}
public class SerializedMesh
{
    public string name;
    public SerializedMaterial material;
    public List<SerializedPosition> points;
    public List<SerializedGameObject> children;
}
public class SerializedGameObject
{
    public string prefabName;
    public string reference;
    public SerializedPosition position;
    public SerializedRotation rotation;
    public List<SerializedGameObject> children;
}

Thanks a LOT to anyone willing to take the time to read all that !

EDIT : As requested by many users, I'll post more code here :

One SerializedMesh in json format :

{
    "name": "wall1",
    "material": {
        "materialName": "WallGrey",
        "textureOffsetX": 1,
        "textureOffsetY": 0.2
    },
    "points": [
        {
            "x": -5,
            "y": 0,
            "z": 4.8
        },
        {
            "x": -5,
            "y": 3.5,
            "z": 4.8
        },
        {
            "x": 5,
            "y": 3.5,
            "z": 4.8
        },
        {
            "x": 5,
            "y": 0,
            "z": 4.8
        },
        {
            "x": -5,
            "y": 0,
            "z": 5
        },
        {
            "x": -5,
            "y": 3.5,
            "z": 5
        },
        {
            "x": 5,
            "y": 3.5,
            "z": 5
        },
        {
            "x": 5,
            "y": 0,
            "z": 5
        }
    ],
    "children": [
        {
            "prefabName": "Prefab1",
            "reference": "",
            "position": {
                "x": 0,
                "y": 2.5,
                "z": 4.9
            },
            "rotation": {
                "x": 0,
                "y": 180,
                "z": 0
            },
            "children": [

            ]
        },
        {
            "prefabName": "Prefab1",
            "reference": "",
            "position": {
                "x": 0,
                "y": 1,
                "z": 4.9
            },
            "rotation": {
                "x": 0,
                "y": 180,
                "z": 0
            },
            "children": [

            ]
        }
    ]
}

Screenshot of the .blend file to help visualize : Custom mesh

And the big method here (whole class still in pastebin) :

public void CreateCustomMesh(SerializedMesh serializedMesh)
    {

        if (serializedMesh.points.Count % 2 != 0)
        {
            Debug.LogError($"CreateCustomMesh - Cannot have odd points count ! ({serializedMesh.points.Count.ToString()} provided)");
            return;
        }

        List<Transform> childrenTransform = new List<Transform>();

        for (int i = 0; i < serializedMesh.children.Count; i++)
        {
            Transform child = SerializedManager.Instance.InstantiateSerializedGameObject(serializedMesh.children[i]);
            child.SetParent(transform);
            childrenTransform.Add(child);
        }

        int pointCount = serializedMesh.points.Count / 2;
        int vertCount = serializedMesh.points.Count + childrenTransform.Count * 8;

        Vector3[] vertices = new Vector3[vertCount];
        int[] triangles;


        // Create vertices
        for (int i = 0; i < serializedMesh.points.Count; i++)
        {
            vertices[i] = serializedMesh.points[i].ToVector3();
        }

        Plane frontFacePlane = new Plane(vertices[0], vertices[1], vertices[2]);
        Plane backFacePlane = new Plane(vertices[pointCount], vertices[pointCount+1], vertices[pointCount+2]);

        for (int i = 0; i < childrenTransform.Count; i++)
        {
            /*
            // World space
            MeshRenderer meshRenderer = childrenTransform[i].GetComponentInChildren<MeshRenderer>();
            if (meshRenderer == null)
            {
                Debug.LogError("CreateCustomMesh - Could not get mesh bounds of children !");
                continue;
            }       
            Bounds bounds = meshRenderer.bounds;
            */

            // Local space
            Mesh mesh = childrenTransform[i].GetComponent<MeshFilter>().mesh;
            if (mesh == null)
            {
                Debug.LogError("CreateCustomMesh - Could not get MeshRenderer of children !");
                continue;
            }
            Bounds bounds = mesh.bounds;



            Vector3 v0 = new Vector3(bounds.center.x - bounds.extents.x, bounds.center.y - bounds.extents.y, bounds.center.z - bounds.extents.z);
            Vector3 v1 = new Vector3(bounds.center.x - bounds.extents.x, bounds.center.y + bounds.extents.y, bounds.center.z - bounds.extents.z);
            Vector3 v2 = new Vector3(bounds.center.x + bounds.extents.x, bounds.center.y + bounds.extents.y, bounds.center.z - bounds.extents.z);
            Vector3 v3 = new Vector3(bounds.center.x + bounds.extents.x, bounds.center.y - bounds.extents.y, bounds.center.z - bounds.extents.z);
            Vector3 v4 = new Vector3(bounds.center.x - bounds.extents.x, bounds.center.y - bounds.extents.y, bounds.center.z + bounds.extents.z);
            Vector3 v5 = new Vector3(bounds.center.x - bounds.extents.x, bounds.center.y + bounds.extents.y, bounds.center.z + bounds.extents.z);
            Vector3 v6 = new Vector3(bounds.center.x + bounds.extents.x, bounds.center.y + bounds.extents.y, bounds.center.z + bounds.extents.z);
            Vector3 v7 = new Vector3(bounds.center.x + bounds.extents.x, bounds.center.y - bounds.extents.y, bounds.center.z + bounds.extents.z);

            Vector3 childForward = childrenTransform[i].forward;
            childForward.x *= -90f;
            v0 = Quaternion.Euler(0, childForward.x, 0) * v0;
            v1 = Quaternion.Euler(0, childForward.x, 0) * v1;
            v2 = Quaternion.Euler(0, childForward.x, 0) * v2;
            v3 = Quaternion.Euler(0, childForward.x, 0) * v3;
            v4 = Quaternion.Euler(0, childForward.x, 0) * v4;
            v5 = Quaternion.Euler(0, childForward.x, 0) * v5;
            v6 = Quaternion.Euler(0, childForward.x, 0) * v6;
            v7 = Quaternion.Euler(0, childForward.x, 0) * v7;




            // Local space only ///////////////////////////////
            Vector3 childPos = childrenTransform[i].position;
            childPos.x -= bounds.center.x * 2;
            childPos.y -= bounds.center.y * 2;
            childPos.z -= bounds.center.z * 2;
            v0 += childPos;
            v1 += childPos;
            v2 += childPos;
            v3 += childPos;
            v4 += childPos;
            v5 += childPos;
            v6 += childPos;
            v7 += childPos;
            ////////////////////////////////////////////////////

            // This is so hole don't shrink or expand the mesh, but remains on the same plane, it works
            v0 = frontFacePlane.ClosestPointOnPlane(v0);
            v1 = frontFacePlane.ClosestPointOnPlane(v1);
            v2 = frontFacePlane.ClosestPointOnPlane(v2);
            v3 = frontFacePlane.ClosestPointOnPlane(v3);

            v4 = backFacePlane.ClosestPointOnPlane(v4);
            v5 = backFacePlane.ClosestPointOnPlane(v5);
            v6 = backFacePlane.ClosestPointOnPlane(v6);
            v7 = backFacePlane.ClosestPointOnPlane(v7);

            /*
            // Create small sphere at each vert pos to help visualize order
            var go0 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go0.transform.localScale = Vector3.one * 0.1f;
            go0.transform.position = v0;
            go0.name = "v0";
            var go1 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go1.transform.localScale = Vector3.one * 0.1f;
            go1.transform.position = v1;
            go1.name = "v1";
            var go2 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go2.transform.localScale = Vector3.one * 0.1f;
            go2.transform.position = v2;
            go2.name = "v2";
            var go3 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go3.transform.localScale = Vector3.one * 0.1f;
            go3.transform.position = v3;
            go3.name = "v3";
            var go4 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go4.transform.localScale = Vector3.one * 0.1f;
            go4.transform.position = v4;
            go4.name = "v4";
            var go5 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go5.transform.localScale = Vector3.one * 0.1f;
            go5.transform.position = v5;
            go5.name = "v5";
            var go6 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go6.transform.localScale = Vector3.one * 0.1f;
            go6.transform.position = v6;
            go6.name = "v6";
            var go7 = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go7.transform.localScale = Vector3.one * 0.1f;
            go7.transform.position = v7;
            go7.name = "v7";
            */


            vertices[serializedMesh.points.Count + 8 * i] = v0;
            vertices[serializedMesh.points.Count + 8 * i + 1] = v1;
            vertices[serializedMesh.points.Count + 8 * i + 2] = v2;
            vertices[serializedMesh.points.Count + 8 * i + 3] = v3;
            vertices[serializedMesh.points.Count + 8 * i + 4] = v4;
            vertices[serializedMesh.points.Count + 8 * i + 5] = v5;
            vertices[serializedMesh.points.Count + 8 * i + 6] = v6;
            vertices[serializedMesh.points.Count + 8 * i + 7] = v7;


#if UNITY_EDITOR
            // Draw bounding box of the holes
            Debug.DrawLine(v0, v1, Color.red, 1000);
            Debug.DrawLine(v1, v2, Color.red, 1000);
            Debug.DrawLine(v2, v3, Color.red, 1000);
            Debug.DrawLine(v3, v0, Color.red, 1000);
            Debug.DrawLine(v4, v5, Color.red, 1000);
            Debug.DrawLine(v4, v7, Color.red, 1000);
            Debug.DrawLine(v5, v6, Color.red, 1000);
            Debug.DrawLine(v6, v7, Color.red, 1000);
            Debug.DrawLine(v0, v4, Color.red, 1000);
            Debug.DrawLine(v1, v5, Color.red, 1000);
            Debug.DrawLine(v2, v6, Color.red, 1000);
            Debug.DrawLine(v3, v7, Color.red, 1000);

#endif

        }

        // If we can't predict the triangulation, use Delaunay
        if (serializedMesh.children.Count > 1 || serializedMesh.points.Count > 8)
        {

            List<int> meshTriangles = new List<int>();

            // Outer border
            for (int i = 0; i < pointCount - 1; i++)
            {
                int v0 = i + pointCount + 1;
                int v1 = i + 1;
                int v2 = i;
                int v3 = i + pointCount;

                meshTriangles.Add(v0);
                meshTriangles.Add(v1);
                meshTriangles.Add(v2);
                meshTriangles.Add(v2);
                meshTriangles.Add(v3);
                meshTriangles.Add(v0);
            }

            // Bridge last face of the outer border
            meshTriangles.Add(pointCount);
            meshTriangles.Add(0);
            meshTriangles.Add(pointCount-1);
            meshTriangles.Add(pointCount-1);
            meshTriangles.Add(2*pointCount-1);
            meshTriangles.Add(pointCount);

            ConstraintOptions options = new ConstraintOptions
            {
                ConformingDelaunay = true,
                SegmentSplitting = 2
            };

            Polygon polygon = new Polygon();

            Vector3 faceNormal = TriangleNormal(serializedMesh.points[0].ToVector3(),serializedMesh.points[1].ToVector3(), serializedMesh.points[2].ToVector3());
            int dir = GetBoxDir(faceNormal);

            for (int i = 0; i < pointCount; i++)
            {
                Vector2 tmp = GetBoxUV(serializedMesh.points[i].ToVector3(), dir);
                Vertex vertex = new Vertex(tmp.x, tmp.y);
                polygon.Add(vertex);
            }
            for (int i = 0; i < polygon.Points.Count; i++)
            {
                polygon.Add(new Segment(polygon.Points[i], i == polygon.Points.Count - 1 ? polygon.Points[0] : polygon.Points[i+1]));
            }

            // TODO : DEBUG : try to build without holes and discard every triangle that share 3 index of the same hole ?

            // Holes
            for (int i = 0; i < childrenTransform.Count; i++)
            {
                // Inner border of child i
                for (int j = 0; j < 3; j++)
                {
                    meshTriangles.Add(j + pointCount*2 + 4 + 8*i);
                    meshTriangles.Add(j + pointCount*2 + 8*i);
                    meshTriangles.Add(j + pointCount*2 + 1 + 8*i);
                    meshTriangles.Add(j + pointCount*2 + 1 + 8*i);
                    meshTriangles.Add(j + pointCount*2 + 5 + 8*i);
                    meshTriangles.Add(j + pointCount*2 + 4 + 8*i);
                }
                // Bridge last face of the inner border of child i
                meshTriangles.Add(pointCount*4 - 1 + 8*i);
                meshTriangles.Add(pointCount*3 - 1 + 8*i);
                meshTriangles.Add(pointCount*2 + 8*i);
                meshTriangles.Add(pointCount*2 + 8*i);
                meshTriangles.Add(pointCount*3 + 8*i);
                meshTriangles.Add(pointCount*4 - 1 + 8*i);

                Vector3 v0 = vertices[serializedMesh.points.Count + 8 * i];
                Vector3 v1 = vertices[serializedMesh.points.Count + 8 * i + 1];
                Vector3 v2 = vertices[serializedMesh.points.Count + 8 * i + 2];
                Vector3 v3 = vertices[serializedMesh.points.Count + 8 * i + 3];

                Vector2 v0tmp = GetBoxUV(v0, dir);
                Vector2 v1tmp = GetBoxUV(v1, dir);
                Vector2 v2tmp = GetBoxUV(v2, dir);
                Vector2 v3tmp = GetBoxUV(v3, dir);

                Vertex vertex0 = new Vertex(v0tmp.x, v0tmp.y);
                Vertex vertex1 = new Vertex(v1tmp.x, v1tmp.y);
                Vertex vertex2 = new Vertex(v2tmp.x, v2tmp.y);
                Vertex vertex3 = new Vertex(v3tmp.x, v3tmp.y);

                /*
                polygon.Add(vertex0);
                polygon.Add(vertex1);
                polygon.Add(vertex2);
                polygon.Add(vertex3);

                polygon.Add(new Segment(vertex0, vertex1));
                polygon.Add(new Segment(vertex1, vertex2));
                polygon.Add(new Segment(vertex2, vertex3));
                polygon.Add(new Segment(vertex3, vertex0));
                */

                List<Vertex> holeVertices = new List<Vertex>()
                {
                    vertex0,
                    vertex1,
                    vertex2,
                    vertex3
                };

                Contour hole = new Contour(holeVertices);
                polygon.Add(hole, true);


            }

            var triangulatedMesh = polygon.Triangulate(options) as TriangleNet.Mesh;

            if (triangulatedMesh == null)
            {
                Debug.LogError($"CreateCustomMesh - Triangulation error of {serializedMesh.name} !");
                return;
            }

            foreach(ITriangle triangle in triangulatedMesh.Triangles)
            {
                var v0 = triangle.GetVertexID(2);
                var v1 = triangle.GetVertexID(1);
                var v2 = triangle.GetVertexID(0);

                // Front face
                meshTriangles.Add(v0);
                meshTriangles.Add(v1);
                meshTriangles.Add(v2);

                // Back face
                meshTriangles.Add(v0 + pointCount);
                meshTriangles.Add(v2 + pointCount);
                meshTriangles.Add(v1 + pointCount);

            }

            List<int> tmpTri = new List<int>();

            for (int i = 0; i < meshTriangles.Count; i++)
            {
                tmpTri.Add(meshTriangles[i]);
            }

            /*
            print("Before discarding : " + tmpTri.Count);

            int nbTriInHole;
            for (int i = 0; i < tmpTri.Count; i += 3)
            {
                nbTriInHole = 0;

                //print($"Triangle {i/3} is ({tmpTri[i]}, {tmpTri[i+1]}, {tmpTri[i+2]})");

                for (int j = 0; j < serializedMesh.children.Count; j++)
                {
                    int tmp = pointCount * 2 + j * 8;
                    if (tmpTri[i] == tmp || tmpTri[i] == tmp + 1 || tmpTri[i] == tmp + 2 ||
                        tmpTri[i] == tmp + 3)
                    {
                        nbTriInHole++;
                    }

                    if (tmpTri[i+1] == tmp || tmpTri[i+1] == tmp + 1 || tmpTri[i+1] == tmp + 2 ||
                        tmpTri[i+1] == tmp + 3)
                    {
                        nbTriInHole++;
                    }

                    if (tmpTri[i+2] == tmp || tmpTri[i+2] == tmp + 1 || tmpTri[i+2] == tmp + 2 ||
                        tmpTri[i+2] == tmp + 3)
                    {
                        nbTriInHole++;
                    }

                }

                if (nbTriInHole > 2)
                {
                    print($"Discarding triangle {i/3} ({tmpTri[i]}, {tmpTri[i+1]}, {tmpTri[i+2]})");
                    tmpTri.RemoveRange(i, 3);
                    var t0 = vertices[tmpTri[i]];
                    t0.z = 0;
                    var t1 = vertices[tmpTri[i+1]];
                    t1.z = 0;
                    var t2 = vertices[tmpTri[i+2]];
                    t2.z = 0;



                    Debug.DrawLine(t0, t1, Color.blue, 4);
                    Debug.DrawLine(t1, t2, Color.blue, 4);
                    Debug.DrawLine(t2, t0, Color.blue, 4);
                }

            }

            print("After discarding : " + tmpTri.Count);
            */

            triangles = tmpTri.ToArray();

        }
        else
        {

            // Create triangles
            int triCount = (12 + childrenTransform.Count * 20) * 3;

            switch (childrenTransform.Count)
            {
                case 0:
                    triangles = new [] {
                        // Front face
                        0, 1, 2,
                        2, 3, 0,

                        // Back face
                        7, 6, 5,
                        5, 4, 7,

                        // Left face
                        4, 5, 1,
                        1, 0, 4,

                        // Right face
                        3, 2, 6,
                        6, 7, 3,

                        // Top face
                        1, 5, 6,
                        6, 2, 1,

                        // Bottom face
                        4, 0, 3,
                        3, 7, 4
                    };
                    break;
                case 1:
                    triangles = new [] {

                        // Front left
                        0, 1, 9,
                        9, 8, 0,

                        // Front top
                        9, 1, 2,
                        2, 10, 9,

                        // Front right
                        11, 10, 2,
                        2, 3, 11,

                        // Front bot
                        0, 8, 11,
                        11, 3, 0,

                        // Back left
                        7, 6, 14,
                        14, 15, 7,

                        // Back top
                        14, 6, 5,
                        5, 13, 14,

                        // Back right
                        12, 13, 5,
                        5, 4, 12,

                        // Back bot
                        7, 15, 12,
                        12, 4, 7,

                        // Outer left
                        4, 5, 1,
                        1, 0, 4,

                        // Outer top
                        1, 5, 6,
                        6, 2, 1,

                        // Outer right
                        3, 2, 6,
                        6, 7, 3,

                        // Outer bot
                        4, 0, 3,
                        3, 7, 4,

                        // Inner left
                        8, 9, 13,
                        13, 12, 8,

                        // Inner top
                        13, 9, 10,
                        10, 14, 13,

                        // Inner right
                        14, 10, 11,
                        11, 15, 14,

                        // Inner bot
                        15, 11, 8,
                        8, 12, 15

                    };
                    break;
                default:
                    triangles = new int[0];
                    break;
            }

            if (triangles.Length != triCount)
            {
                Debug.LogError($"CreateCustomMesh - Wrong triangles count : {triangles.Length.ToString()} (expected {triCount.ToString()})");
                return;
            }

        }


        Mesh.vertices = vertices;
        Mesh.triangles = triangles;
        Mesh.Optimize();
        Mesh.RecalculateNormals();
        CalculateUVs();
        Mesh.RecalculateTangents();
        Mesh.RecalculateBounds();
        MeshCollider.sharedMesh = Mesh;

        if(Mathf.Abs(serializedMesh.material.textureOffsetX) > 0.001f || Mathf.Abs(serializedMesh.material.textureOffsetY) > 0.001f)
        {
            MeshRenderer.material.SetTextureOffset(BaseMap, new Vector2(serializedMesh.material.textureOffsetX, serializedMesh.material.textureOffsetY));
        }

    }
Jichael
  • 755
  • 3
  • 14
  • 1
    Please add all relevant resources to the post and don't link to external content. Especially, post renderings of the Blender file. This will make your question much more accessible. – Nico Schertler Nov 19 '19 at 16:33
  • 1
    The blender file is not for rendering at all, it's only relevant if you can view it in 3d (in blender). I can post the CustomMesh code here, but it's really long it would be worse IMHO – Jichael Nov 20 '19 at 08:39
  • 1
    The term to google here is CSG. There are a few assets on the asset store that might do what you want. I think its too involved for a stackoverflow answer, but there are a number of tutorials and academic papers out there that show how to implement CSG using BSP trees. For example: https://pdfs.semanticscholar.org/eeb5/014f86750c54a87f214b03246799e970d114.pdf – Leo Bartkus Nov 20 '19 at 17:18
  • @NicoSchertler I know, but as I can't know for sure where the "problem" lies (and it's not really a problem, just me not being able to figure out) pretty much all the method is usefull, plus it really need some context, going from just 8 points to a 3d mesh with potential hole is not doable in 10 lines of code. The blender file is really just a mesh with holes and numbers I put on the vertices to help see what vertices are at certains indexes etc, it's not required, it's just to help visualize. Thanks anyway – Jichael Nov 21 '19 at 08:24
  • @LeoBartkus I already read a LOT of resources, watched hours long videos to learn more about graphs and 3d representation and other things, but I still can't get that LAST part to work (everything else is working). I'll continue learning and searching, thanks ! – Jichael Nov 21 '19 at 08:26
  • So your actual problem is to triangulate a polygon with holes? See e.g. [this question](https://stackoverflow.com/questions/406301/polygon-triangulation-with-holes). I assume your faces are always aligned to a principal axis? Then first do a projection to 2D to make things simpler (if all vertices have equal `x` coordinates, use `yz` etc.) – Nico Schertler Nov 21 '19 at 16:56
  • Can you show a screenshot of your bug? – Leo Bartkus Nov 21 '19 at 22:42
  • @NicoSchertler No they should not always be, and that's the problem. I can't find a way to "generalize" this 2D -> 3D conversion, the triangulation seems to work fine – Jichael Nov 25 '19 at 08:45
  • @LeoBartkus What bug ? – Jichael Nov 25 '19 at 08:45
  • What it looks like when you run the code. – Leo Bartkus Nov 25 '19 at 13:37
  • But the polygon is planar? Or not even that? – Nico Schertler Nov 25 '19 at 17:27
  • Each face is on a plane and each front/back face are parallel is it's what you asked ? Like a wall, floor, ceiling... – Jichael Nov 26 '19 at 08:22

0 Answers0