Files
speckle-unity/Packages/systems.speckle.speckle-unity/ConverterUnity.Geometry.cs
T
JR-Morgan 10f0c244ff Re-added commit creation when sending.
Also replaced ParallelExtensionsExtras.dll with source file for cross platform building
2022-05-18 01:00:02 +01:00

549 lines
18 KiB
C#

using System;
using Objects.Geometry;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Objects.Other;
using Objects.Utils;
using Speckle.ConnectorUnity;
using Speckle.Core.Logging;
using Speckle.Core.Models;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using Mesh = Objects.Geometry.Mesh;
using SColor = System.Drawing.Color;
using Transform = Objects.Other.Transform;
namespace Objects.Converter.Unity
{
public partial class ConverterUnity
{
private static readonly int EmissionColor = Shader.PropertyToID("_EmissionColor");
private static readonly int Metallic = Shader.PropertyToID("_Metallic");
private static readonly int Glossiness = Shader.PropertyToID("_Glossiness");
#region helper methods
/// <summary>
///
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="z"></param>
/// <returns></returns>
public Vector3 VectorByCoordinates(double x, double y, double z, string units)
{
// switch y and z
return new Vector3((float)ScaleToNative(x, units), (float)ScaleToNative(z, units),
(float)ScaleToNative(y, units));
}
public Vector3 VectorFromPoint(Point p)
{
// switch y and z
return new Vector3((float)ScaleToNative(p.x, p.units), (float)ScaleToNative(p.z, p.units),
(float)ScaleToNative(p.y, p.units));
}
/// <summary>
///
/// </summary>
/// <param name="ptValues"></param>
/// <returns></returns>
// public Vector3 ArrayToPoint(double[] ptValues, string units)
// {
// double x = ptValues[0];
// double y = ptValues[1];
// double z = ptValues[2];
//
// return PointByCoordinates(x, y, z, units);
// }
/// <summary>
///
/// </summary>
/// <param name="arr"></param>
/// <returns></returns>
public Vector3[] ArrayToPoints(IList<double> arr, string units)
{
if (arr.Count % 3 != 0) throw new Exception("Array malformed: length%3 != 0.");
Vector3[] points = new Vector3[arr.Count / 3];
for (int i = 2, k = 0; i < arr.Count; i += 3)
points[k++] = VectorByCoordinates(arr[i - 2], arr[i - 1], arr[i], units);
return points;
}
public Vector3[] ArrayToPoints(IEnumerable<double> arr, string units, out Vector2[] uv)
{
uv = null;
if (arr.Count() % 3 != 0) throw new Exception("Array malformed: length%3 != 0.");
Vector3[] points = new Vector3[arr.Count() / 3];
uv = new Vector2[points.Length];
var asArray = arr.ToArray();
for (int i = 2, k = 0; i < arr.Count(); i += 3)
{
points[k++] = VectorByCoordinates(asArray[i - 2], asArray[i - 1], asArray[i], units);
}
// get size of mesh
for (int i = 0; i < points.Length; i++) { }
return points;
}
#endregion
#region ToSpeckle
//TODO: more of these
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public Point PointToSpeckle(Vector3 p)
{
//switch y and z
return new Point(p.x, p.z, p.y);
}
/// <summary>
/// Converts the <see cref="MeshFilter"/> component on <paramref name="go"/> into a Speckle <see cref="Mesh"/>
/// </summary>
/// <param name="go">The Unity <see cref="GameObject"/> to convert</param>
/// <returns>The converted <see cref="Mesh"/>, <see langword="null"/> if no <see cref="MeshFilter"/> on <paramref name="go"/> exists</returns>
public Mesh MeshToSpeckle(GameObject go)
{
//TODO: support multiple filters?
var filter = go.GetComponent<MeshFilter>();
if (filter == null) return null;
var nativeMesh = filter.mesh;
var nTriangles = nativeMesh.triangles;
List<int> sFaces = new List<int>(nTriangles.Length * 4);
for (int i = 2; i < nTriangles.Length; i += 3)
{
sFaces.Add(0); //Triangle cardinality indicator
sFaces.Add(nTriangles[i]);
sFaces.Add(nTriangles[i - 1]);
sFaces.Add(nTriangles[i - 2]);
}
var nVertices = nativeMesh.vertices;
List<double> sVertices = new List<double>(nVertices.Length * 3);
foreach (var vertex in nVertices)
{
var p = go.transform.TransformPoint(vertex);
sVertices.Add(p.x);
sVertices.Add(p.z); //z and y swapped
sVertices.Add(p.y);
}
var nColors = nativeMesh.colors;
List<int> sColors = new List<int>(nColors.Length);
sColors.AddRange(nColors.Select(c => c.ToIntColor()));
var nTexCoords = nativeMesh.uv;
List<double> sTexCoords = new List<double>(nTexCoords.Length * 2);
foreach (var uv in nTexCoords)
{
sTexCoords.Add(uv.x);
sTexCoords.Add(uv.y);
}
var mesh = new Mesh();
// get the speckle data from the go here
// so that if the go comes from speckle, typed props will get overridden below
AttachUnityProperties(mesh, go);
mesh.vertices = sVertices;
mesh.faces = sFaces;
mesh.colors = sColors;
mesh.textureCoordinates = sTexCoords;
mesh.units = ModelUnits;
return mesh;
}
#endregion
#region ToNative
private GameObject NewPointBasedGameObject(Vector3[] points, string name)
{
if (points.Length == 0) return null;
float pointDiameter = 1; //TODO: figure out how best to change this?
var go = new GameObject();
go.name = name;
var lineRenderer = go.AddComponent<LineRenderer>();
lineRenderer.positionCount = points.Length;
lineRenderer.SetPositions(points);
lineRenderer.numCornerVertices = lineRenderer.numCapVertices = 8;
lineRenderer.startWidth = lineRenderer.endWidth = pointDiameter;
return go;
}
/// <summary>
/// Converts a Speckle <paramref name="point"/> to a <see cref="GameObject"/> with a <see cref="LineRenderer"/>
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public GameObject PointToNative(Point point)
{
Vector3 newPt = VectorByCoordinates(point.x, point.y, point.z, point.units);
var go = NewPointBasedGameObject(new Vector3[] { newPt, newPt }, point.speckle_type);
return go;
}
/// <summary>
/// Converts a Speckle <paramref name="line"/> to a <see cref="GameObject"/> with a <see cref="LineRenderer"/>
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public GameObject LineToNative(Line line)
{
var points = new List<Vector3> { VectorFromPoint(line.start), VectorFromPoint(line.end) };
var go = NewPointBasedGameObject(points.ToArray(), line.speckle_type);
return go;
}
/// <summary>
/// Converts a Speckle <paramref name="polyline"/> to a <see cref="GameObject"/> with a <see cref="LineRenderer"/>
/// </summary>
/// <param name="polyline"></param>
/// <returns></returns>
public GameObject PolylineToNative(Polyline polyline)
{
var points = polyline.GetPoints().Select(VectorFromPoint);
var go = NewPointBasedGameObject(points.ToArray(), polyline.speckle_type);
return go;
}
/// <summary>
/// Converts a Speckle <paramref name="curve"/> to a <see cref="GameObject"/> with a <see cref="LineRenderer"/>
/// </summary>
/// <param name="curve"></param>
/// <returns></returns>
public GameObject CurveToNative(Curve curve)
{
var points = ArrayToPoints(curve.points, curve.units);
var go = NewPointBasedGameObject(points, curve.speckle_type);
return go;
}
/// <summary>
/// Converts multiple <paramref name="meshes"/> (e.g. with different materials) into one native mesh
/// </summary>
/// <param name="element">The <see cref="Base"/> element from which properties should be grabbed from</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<Mesh> meshes)
{
MeshDataToNative(meshes, out var nativeMesh, out var nativeMaterials);
var go = new GameObject
{
name = element.speckle_type
};
go.SafeMeshSet(nativeMesh, true);
var meshRenderer = go.AddComponent<MeshRenderer>();
meshRenderer.sharedMaterials = nativeMaterials;
var excludeProps = new HashSet<string>(typeof(Base).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Select(x => x.Name));
excludeProps.Add("displayValue");
excludeProps.Add("displayMesh");
var properties = element.GetMembers()
.Where(x => !excludeProps.Contains(x.Key))
.ToDictionary(x => x.Key, x => x.Value);
AttachSpeckleProperties(go, properties);
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(Mesh speckleMesh)
{
if (speckleMesh.vertices.Count == 0 || speckleMesh.faces.Count == 0)
{
return null;
}
return MeshesToNative(speckleMesh, new[] {speckleMesh});
}
/// <summary>
///
/// </summary>
/// <param name="meshes">meshes to be converted as SubMeshes</param>
/// <param name="nativeMesh">The converted native mesh</param>
/// <param name="nativeMaterials">The converted materials (one per converted sub-mesh)</param>
public void MeshDataToNative(IReadOnlyCollection<Mesh> meshes, out UnityEngine.Mesh nativeMesh, out Material[] nativeMaterials)
{
var verts = new List<Vector3>();
var uvs = new List<Vector2>();
var vertexColors = new List<Color>();
var materials = new List<Material>(meshes.Count);
var subMeshes = new List<List<int>>(meshes.Count);
foreach (Mesh 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, materials);
subMeshes.Add(tris);
}
nativeMaterials = materials.ToArray();
Debug.Assert(verts.Count >= 0);
Debug.Assert(verts.Count >= 0);
nativeMesh = new UnityEngine.Mesh();
// center transform pivot according to the bounds of the model
// Bounds meshBounds = new Bounds
// {
// center = verts[0]
// };
//
// foreach (var vert in verts)
// {
// meshBounds.Encapsulate(vert);
// }
//
// // offset mesh vertices
// for (int l = 0; l < verts.Count; l++)
// {
// verts[l] -= meshBounds.center;
// }
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();
}
private void SubmeshToNative(Mesh speckleMesh, List<Vector3> verts, List<int> tris, List<Vector2> texCoords, List<Color> vertexColors, List<Material> materials)
{
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 //TODO this will be broken for submeshes
texCoords.AddRange(GenerateUV(verts, (float)speckleMesh.bbox.xSize.Length, (float)speckleMesh.bbox.ySize.Length));
}
// 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(Mesh)} {speckleMesh.id} has invalid number of vertex {nameof(Mesh.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);
}
// Convert RenderMaterial
materials.Add(GetMaterial(speckleMesh["renderMaterial"] as RenderMaterial));
}
private static IEnumerable<Vector2> GenerateUV(IReadOnlyList<Vector3> verts, float xSize, float ySize)
{
var uv = new Vector2[verts.Count];
for (int i = 0; i < verts.Count; i++)
{
var vert = verts[i];
uv[i] = new Vector2(vert.x / xSize, vert.y / ySize);
}
return uv;
}
private static Matrix4x4 UnflattenMatrix(IList<double> flatMatrix)
{
Matrix4x4 matrix = new Matrix4x4();
for(int row = 0; row < 4; row++)
for(int col = 0; col < 4; col++)
{
matrix[row,col] = (float)flatMatrix[row * 4 + col];
}
return matrix.transpose;
}
#endregion
private Material GetMaterial(RenderMaterial renderMaterial)
{
//todo support more complex materials
var shader = Shader.Find("Standard");
Material mat = new Material(shader);
//if a renderMaterial is passed use that, otherwise try get it from the mesh itself
if (renderMaterial != null)
{
// 1. match material by name, if any
Material matByName = null;
foreach (var _mat in ContextObjects)
{
if (((Material)_mat.NativeObject).name == renderMaterial.name)
{
if (matByName == null) matByName = (Material)_mat.NativeObject;
else Debug.LogWarning("There is more than one Material with the name \'" + renderMaterial.name + "\'!", (Material)_mat.NativeObject);
}
}
if (matByName != null) return matByName;
// 2. re-create material by setting diffuse color and transparency on standard shaders
if (renderMaterial.opacity < 1)
{
shader = Shader.Find("Transparent/Diffuse");
mat = new Material(shader);
}
var c = renderMaterial.diffuse.ToUnityColor();
mat.color = new Color(c.r, c.g, c.b, (float)renderMaterial.opacity);
mat.name = renderMaterial.name ?? "material-"+ Guid.NewGuid().ToString().Substring(0,8);
mat.SetFloat(Metallic, (float)renderMaterial.metalness);
mat.SetFloat(Glossiness, 1 - (float)renderMaterial.roughness);
if (renderMaterial.emissive != SColor.Black.ToArgb()) mat.EnableKeyword ("_EMISSION");
mat.SetColor(EmissionColor, renderMaterial.emissive.ToUnityColor());
#if UNITY_EDITOR
if (StreamManager.GenerateMaterials)
{
if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources");
if (!AssetDatabase.IsValidFolder("Assets/Resources/Materials")) AssetDatabase.CreateFolder("Assets/Resources", "Materials");
if (!AssetDatabase.IsValidFolder("Assets/Resources/Materials/Speckle Generated")) AssetDatabase.CreateFolder("Assets/Resources/Materials", "Speckle Generated");
if (AssetDatabase.LoadAllAssetsAtPath("Assets/Resources/Materials/Speckle Generated/" + mat.name + ".mat").Length == 0) AssetDatabase.CreateAsset(mat, "Assets/Resources/Materials/Speckle Generated/" + mat.name + ".mat");
}
#endif
return mat;
}
// 3. if not renderMaterial was passed, the default shader will be used
return mat;
}
private void AttachSpeckleProperties(GameObject go, Dictionary<string, object> properties)
{
var sd = go.AddComponent<SpeckleProperties>();
sd.Data = properties;
}
private void AttachUnityProperties(Base @base, GameObject go)
{
var sd = go.GetComponent<SpeckleProperties>();
if (sd == null || sd.Data == null)
return;
foreach (var key in sd.Data.Keys)
{
try
{
@base[key] = sd.Data[key];
}
catch(SpeckleException)
{
// Ignore SpeckleExceptions that may be caused by get only properties
}
}
}
}
}