Files
speckle-unity/Packages/systems.speckle.speckle-unity/Runtime/Converter/Unity/ConverterUnity.Mesh.cs
T
Jedd Morgan 1b2eeed3eb 2.18 Update
2024-02-22 18:31:09 +00:00

431 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Speckle.ConnectorUnity.Utils;
using Objects.Utils;
using Speckle.Core.Models;
using UnityEngine;
using UnityEngine.Rendering;
using Material = UnityEngine.Material;
using Mesh = UnityEngine.Mesh;
using SMesh = Objects.Geometry.Mesh;
using Transform = UnityEngine.Transform;
#nullable enable
namespace Objects.Converter.Unity
{
public partial class ConverterUnity
{
#region ToSpeckle
public virtual List<SMesh>? MeshToSpeckle(MeshFilter meshFilter)
{
#if UNITY_EDITOR
Material[]? materials = meshFilter.GetComponent<Renderer>()?.sharedMaterials;
var nativeMesh = meshFilter.sharedMesh;
#else
Material[]? materials = meshFilter.GetComponent<Renderer>()?.materials;
var nativeMesh = meshFilter.mesh;
#endif
if (nativeMesh == null)
return null;
List<SMesh> convertedMeshes = new List<SMesh>(nativeMesh.subMeshCount);
for (int i = 0; i < nativeMesh.subMeshCount; i++)
{
var subMesh = nativeMesh.GetSubMesh(i);
SMesh converted;
switch (subMesh.topology)
{
// case MeshTopology.Points:
// //TODO convert as pointcloud
// continue;
case MeshTopology.Triangles:
converted = SubMeshToSpeckle(
nativeMesh,
meshFilter.transform,
subMesh,
i,
3
);
convertedMeshes.Add(converted);
break;
case MeshTopology.Quads:
converted = SubMeshToSpeckle(
nativeMesh,
meshFilter.transform,
subMesh,
i,
4
);
convertedMeshes.Add(converted);
break;
default:
Debug.LogError(
$"Failed to convert submesh {i} of {typeof(GameObject)} {meshFilter.gameObject.name} to Speckle, Unsupported Mesh Topography {subMesh.topology}. Submesh will be ignored."
);
continue;
}
if (materials == null || materials.Length <= i)
continue;
Material mat = materials[i];
if (mat != null)
converted["renderMaterial"] = MaterialToSpeckle(mat);
}
return convertedMeshes;
}
protected virtual SMesh SubMeshToSpeckle(
Mesh nativeMesh,
Transform instanceTransform,
SubMeshDescriptor subMesh,
int subMeshIndex,
int faceN
)
{
var nFaces = nativeMesh.GetIndices(subMeshIndex, true);
int numFaces = nFaces.Length / faceN;
List<int> sFaces = new List<int>(numFaces * (faceN + 1));
int indexOffset = subMesh.firstVertex;
// int i = 0;
// int j = 0;
// while (i < nFaces.Length)
// {
// if (j == 0)
// {
// sFaces.Add(faceN);
// j = faceN;
// }
// sFaces.Add(nFaces[i] - indexOffset);
// j--;
// i++;
// }
int i = nFaces.Length - 1;
int j = 0;
while (i >= 0) //Traverse backwards to ensure CCW face orientation
{
if (j == 0)
{
//Add face cardinality indicator ever
sFaces.Add(faceN);
j = faceN;
}
sFaces.Add(nFaces[i] - indexOffset);
j--;
i--;
}
int vertexTake = subMesh.vertexCount;
var nVertices = nativeMesh.vertices.Skip(indexOffset).Take(vertexTake);
List<double> sVertices = new List<double>(subMesh.vertexCount * 3);
foreach (var vertex in nVertices)
{
var p = instanceTransform.TransformPoint(vertex);
sVertices.Add(p.x);
sVertices.Add(p.z); //z and y swapped //TODO is this correct? LH -> RH
sVertices.Add(p.y);
}
var nColors = nativeMesh.colors.Skip(indexOffset).Take(vertexTake).ToArray();
List<int> sColors = new List<int>(nColors.Length);
sColors.AddRange(nColors.Select(c => c.ToIntColor()));
var nTexCoords = nativeMesh.uv.Skip(indexOffset).Take(vertexTake).ToArray();
List<double> sTexCoords = new List<double>(nTexCoords.Length * 2);
foreach (var uv in nTexCoords)
{
sTexCoords.Add(uv.x);
sTexCoords.Add(uv.y);
}
var convertedMesh = new SMesh
{
vertices = sVertices,
faces = sFaces,
colors = sColors,
textureCoordinates = sTexCoords,
units = ModelUnits
};
return convertedMesh;
}
#endregion
#region ToNative
/// <summary>
/// Converts multiple <paramref name="meshes"/> (e.g. with different materials) into one native mesh
/// </summary>
/// <param name="element">The <see cref="Base"/> object being converted</param>
/// <param name="meshes">Collection of <see cref="Objects.Geometry.Mesh"/>es that shall be converted</param>
/// <returns>A <see cref="GameObject"/> with the converted <see cref="UnityEngine.Mesh"/>, <see cref="MeshFilter"/>, and <see cref="MeshRenderer"/></returns>
public GameObject MeshesToNative(Base element, IReadOnlyCollection<SMesh> meshes)
{
if (!meshes.Any())
throw new ArgumentException("Expected at least one Mesh", nameof(meshes));
Material[] nativeMaterials = RenderMaterialsToNative(meshes);
if (!TryGetMeshFromCache(element, meshes, out Mesh? nativeMesh, out Vector3 center))
{
//Convert a new one
MeshToNativeMesh(meshes, out nativeMesh, out center);
string name = CoreUtils.GenerateObjectName(element);
nativeMesh.name = name;
LoadedAssets.TrySaveObject(element, nativeMesh);
}
var go = new GameObject();
go.transform.position = center;
go.SafeMeshSet(nativeMesh, nativeMaterials);
return go;
}
/// <summary>
/// Converts <paramref name="speckleMesh"/> to a <see cref="GameObject"/> with a <see cref="MeshRenderer"/>
/// </summary>
/// <param name="speckleMesh">Mesh to convert</param>
/// <returns></returns>
public GameObject MeshToNative(SMesh speckleMesh)
{
if (speckleMesh.vertices.Count == 0 || speckleMesh.faces.Count == 0)
throw new ArgumentException("mesh data was empty", nameof(speckleMesh));
GameObject converted = MeshesToNative(speckleMesh, new[] { speckleMesh });
// Raw meshes shouldn't have dynamic props to attach
//if (converted != null) AttachSpeckleProperties(converted,speckleMesh.GetType(), GetProperties(speckleMesh, typeof(Mesh)));
return converted;
}
protected bool TryGetMeshFromCache(
Base element,
IReadOnlyCollection<SMesh> meshes,
[NotNullWhen(true)] out Mesh? nativeMesh,
out Vector3 center
)
{
if (LoadedAssets.TryGetObject(element, out Mesh? existing))
{
nativeMesh = existing;
//todo This is pretty inefficient, having to convert the mesh data anyway just to get the center... eek
MeshDataToNative(meshes, out List<Vector3> verts, out _, out _, out _);
center = CalculateBounds(verts).center;
return true;
}
nativeMesh = default;
center = default;
return false;
}
/// <summary>
/// Converts Speckle <see cref="SMesh"/>s as a native <see cref="Mesh"/> object
/// </summary>
/// <param name="meshes">meshes to be converted as SubMeshes</param>
/// <param name="nativeMesh">The converted native mesh</param>
public void MeshToNativeMesh(IReadOnlyCollection<SMesh> meshes, out Mesh nativeMesh) =>
MeshToNativeMesh(meshes, out nativeMesh, out _, false);
/// <inheritdoc cref="MeshToNativeMesh(IReadOnlyCollection{Objects.Geometry.Mesh},out Mesh)"/>
/// <param name="recenterVerts">when true, will recenter vertices</param>
/// <param name="center">Center position for the mesh</param>
public void MeshToNativeMesh(
IReadOnlyCollection<SMesh> meshes,
out Mesh nativeMesh,
out Vector3 center,
bool recenterVerts = true
)
{
MeshDataToNative(
meshes,
out List<Vector3> verts,
out List<Vector2> uvs,
out List<Color> vertexColors,
out List<List<int>> subMeshes
);
Debug.Assert(verts.Count >= 0);
center = recenterVerts ? RecenterVertices(verts) : Vector3.zero;
nativeMesh = new Mesh();
nativeMesh.subMeshCount = subMeshes.Count;
nativeMesh.SetVertices(verts);
nativeMesh.SetUVs(0, uvs);
nativeMesh.SetColors(vertexColors);
int j = 0;
foreach (var subMeshTriangles in subMeshes)
{
nativeMesh.SetTriangles(subMeshTriangles, j);
j++;
}
if (nativeMesh.vertices.Length >= UInt16.MaxValue)
nativeMesh.indexFormat = IndexFormat.UInt32;
nativeMesh.Optimize();
nativeMesh.RecalculateBounds();
nativeMesh.RecalculateNormals();
nativeMesh.RecalculateTangents();
}
public void MeshDataToNative(
IReadOnlyCollection<SMesh> meshes,
out List<Vector3> verts,
out List<Vector2> uvs,
out List<Color> vertexColors,
out List<List<int>> subMeshes
)
{
verts = new List<Vector3>();
uvs = new List<Vector2>();
vertexColors = new List<Color>();
subMeshes = new List<List<int>>(meshes.Count);
foreach (SMesh m in meshes)
{
if (m.vertices.Count == 0 || m.faces.Count == 0)
continue;
List<int> tris = new List<int>();
SubmeshToNative(m, verts, tris, uvs, vertexColors);
subMeshes.Add(tris);
}
}
protected void SubmeshToNative(
SMesh speckleMesh,
List<Vector3> verts,
List<int> tris,
List<Vector2> texCoords,
List<Color> vertexColors
)
{
speckleMesh.AlignVerticesWithTexCoordsByIndex();
speckleMesh.TriangulateMesh();
int indexOffset = verts.Count;
// Convert Vertices
verts.AddRange(ArrayToPoints(speckleMesh.vertices, speckleMesh.units));
// Convert texture coordinates
bool hasValidUVs = speckleMesh.TextureCoordinatesCount == speckleMesh.VerticesCount;
if (speckleMesh.textureCoordinates.Count > 0 && !hasValidUVs)
Debug.LogWarning(
$"Expected number of UV coordinates to equal vertices. Got {speckleMesh.TextureCoordinatesCount} expected {speckleMesh.VerticesCount}. \nID = {speckleMesh.id}"
);
if (hasValidUVs)
{
texCoords.Capacity += speckleMesh.TextureCoordinatesCount;
for (int j = 0; j < speckleMesh.TextureCoordinatesCount; j++)
{
var (u, v) = speckleMesh.GetTextureCoordinate(j);
texCoords.Add(new Vector2((float)u, (float)v));
}
}
else if (speckleMesh.bbox != null)
{
// Attempt to generate some crude UV coordinates using bbox
texCoords.AddRange(
GenerateUV(
indexOffset,
verts,
(float)speckleMesh.bbox.xSize.Length,
(float)speckleMesh.bbox.ySize.Length
)
);
}
else
{
texCoords.AddRange(Enumerable.Repeat(Vector2.zero, verts.Count - indexOffset));
}
// Convert vertex colors
if (speckleMesh.colors != null)
{
if (speckleMesh.colors.Count == speckleMesh.VerticesCount)
{
vertexColors.AddRange(speckleMesh.colors.Select(c => c.ToUnityColor()));
}
else if (speckleMesh.colors.Count != 0)
{
//TODO what if only some submeshes have colors?
Debug.LogWarning(
$"{typeof(SMesh)} {speckleMesh.id} has invalid number of vertex {nameof(SMesh.colors)}. Expected 0 or {speckleMesh.VerticesCount}, got {speckleMesh.colors.Count}"
);
}
}
// Convert faces
tris.Capacity += (int)(speckleMesh.faces.Count / 4f) * 3;
for (int i = 0; i < speckleMesh.faces.Count; i += 4)
{
// We can safely assume all faces are triangles since we called TriangulateMesh
tris.Add(speckleMesh.faces[i + 1] + indexOffset);
tris.Add(speckleMesh.faces[i + 3] + indexOffset);
tris.Add(speckleMesh.faces[i + 2] + indexOffset);
}
}
protected static IEnumerable<Vector2> GenerateUV(
int indexOffset,
IReadOnlyList<Vector3> verts,
float xSize,
float ySize
)
{
var uv = new Vector2[verts.Count - indexOffset];
for (int i = 0; i < verts.Count - indexOffset; i++)
{
var vert = verts[i];
uv[i] = new Vector2(vert.x / xSize, vert.y / ySize);
}
return uv;
}
protected static Vector3 RecenterVertices(IList<Vector3> vertices)
{
if (!vertices.Any())
return Vector3.zero;
Bounds meshBounds = CalculateBounds(vertices);
for (int i = 0; i < vertices.Count; i++)
vertices[i] -= meshBounds.center;
return meshBounds.center;
}
protected static Bounds CalculateBounds(IList<Vector3> points)
{
Bounds b = new Bounds { center = points[0] };
foreach (var p in points)
b.Encapsulate(p);
return b;
}
#endregion
}
}