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 :
- CustomMesh.cs (where I do almost all the work) : https://pastebin.com/wxV7PNix
- An exemple of the json output : https://pastebin.com/DQwwCNnG
- Bender file to help a lot visualize everything : https://gofile.io/?c=ysTGPc
- The basic classes I use for deserialization of my json :
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 :
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));
}
}