583 lines
18 KiB
C#
583 lines
18 KiB
C#
#if UNITY_EDITOR
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using Unity.Collections;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
public static class CyalumeMeshDiagnostics
|
|
{
|
|
[MenuItem("Tools/Cyalume/Dump Selected Mesh Filters")]
|
|
private static void DumpSelectedMeshFilters()
|
|
{
|
|
var filters = CollectSelectedMeshFilters();
|
|
if (filters.Count == 0)
|
|
{
|
|
Debug.LogWarning("CyalumeMeshDiagnostics: Select one or more GameObjects with MeshFilter components.");
|
|
return;
|
|
}
|
|
|
|
var report = new StringBuilder();
|
|
report.AppendLine($"Cyalume mesh dump: {filters.Count} selection(s)");
|
|
|
|
foreach (var filter in filters)
|
|
{
|
|
report.AppendLine(BuildMeshReport(filter));
|
|
report.AppendLine(new string('-', 80));
|
|
}
|
|
|
|
Debug.Log(report.ToString());
|
|
}
|
|
|
|
[MenuItem("Tools/Cyalume/Compare First Two Selected Mesh Filters")]
|
|
private static void CompareFirstTwoSelectedMeshFilters()
|
|
{
|
|
var filters = CollectSelectedMeshFilters();
|
|
if (filters.Count < 2)
|
|
{
|
|
Debug.LogWarning("CyalumeMeshDiagnostics: Select at least two GameObjects with MeshFilter components.");
|
|
return;
|
|
}
|
|
|
|
var left = filters[0];
|
|
var right = filters[1];
|
|
var leftMesh = left.sharedMesh;
|
|
var rightMesh = right.sharedMesh;
|
|
|
|
var report = new StringBuilder();
|
|
report.AppendLine("Cyalume mesh compare");
|
|
report.AppendLine($"Left : {left.name}");
|
|
report.AppendLine($"Right: {right.name}");
|
|
report.AppendLine();
|
|
report.AppendLine(BuildMeshReport(left));
|
|
report.AppendLine(new string('=', 80));
|
|
report.AppendLine(BuildMeshReport(right));
|
|
report.AppendLine(new string('=', 80));
|
|
report.AppendLine(BuildComparison(leftMesh, rightMesh));
|
|
|
|
Debug.Log(report.ToString());
|
|
}
|
|
|
|
private static List<MeshFilter> CollectSelectedMeshFilters()
|
|
{
|
|
var results = new List<MeshFilter>();
|
|
var seen = new HashSet<MeshFilter>();
|
|
|
|
foreach (var gameObject in Selection.gameObjects)
|
|
{
|
|
if (gameObject == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var filter = gameObject.GetComponent<MeshFilter>();
|
|
if (filter != null && seen.Add(filter))
|
|
{
|
|
results.Add(filter);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static string BuildMeshReport(MeshFilter filter)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine($"GameObject : {filter.name}");
|
|
|
|
var renderer = filter.GetComponent<Renderer>();
|
|
sb.AppendLine($"Renderer : {(renderer != null ? renderer.enabled.ToString() : "missing")}");
|
|
|
|
var mesh = filter.sharedMesh;
|
|
if (mesh == null)
|
|
{
|
|
sb.AppendLine("Mesh : <null>");
|
|
return sb.ToString();
|
|
}
|
|
|
|
sb.AppendLine($"Mesh : {mesh.name}");
|
|
sb.AppendLine($"Readable : {SafeRead(() => mesh.isReadable, false)}");
|
|
sb.AppendLine($"Vertices : {SafeRead(() => mesh.vertexCount, -1)}");
|
|
sb.AppendLine($"SubMeshes : {SafeRead(() => mesh.subMeshCount, -1)}");
|
|
sb.AppendLine($"Bounds : {SafeBounds(mesh)}");
|
|
sb.AppendLine($"AssetPath : {AssetDatabase.GetAssetPath(mesh)}");
|
|
sb.AppendLine($"Importer : {DescribeImporter(mesh)}");
|
|
sb.AppendLine($"Attrs : {BuildAttributeSummary(mesh)}");
|
|
sb.AppendLine($"AttrLayout : {BuildAttributeLayout(mesh)}");
|
|
sb.AppendLine($"RawData : {BuildRawAttributeSummary(mesh)}");
|
|
|
|
var uv = SafeArrayRead(() => mesh.uv);
|
|
sb.AppendLine($"UV Count : {(uv != null ? uv.Length.ToString() : "<unavailable>")}");
|
|
|
|
var colors32 = SafeArrayRead(() => mesh.colors32);
|
|
if (colors32 == null)
|
|
{
|
|
sb.AppendLine("Colors32 : <unavailable>");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"Colors32 : {colors32.Length}");
|
|
sb.AppendLine($"ColorStats : {BuildColorStats(colors32)}");
|
|
sb.AppendLine($"ColorSample: {BuildColorSample(colors32, 8)}");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string BuildComparison(Mesh left, Mesh right)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
if (left == null || right == null)
|
|
{
|
|
sb.AppendLine("Comparison unavailable: one of the meshes is null.");
|
|
return sb.ToString();
|
|
}
|
|
|
|
sb.AppendLine("Quick compare");
|
|
sb.AppendLine($"Readable equal : {SafeRead(() => left.isReadable, false) == SafeRead(() => right.isReadable, false)}");
|
|
sb.AppendLine($"Vertex count : {SafeRead(() => left.vertexCount, -1)} vs {SafeRead(() => right.vertexCount, -1)}");
|
|
sb.AppendLine($"UV count : {SafeArrayRead(() => left.uv)?.Length ?? -1} vs {SafeArrayRead(() => right.uv)?.Length ?? -1}");
|
|
sb.AppendLine($"Attrs : {BuildAttributeSummary(left)} vs {BuildAttributeSummary(right)}");
|
|
sb.AppendLine($"AttrLayout L : {BuildAttributeLayout(left)}");
|
|
sb.AppendLine($"AttrLayout R : {BuildAttributeLayout(right)}");
|
|
sb.AppendLine($"RawData L : {BuildRawAttributeSummary(left)}");
|
|
sb.AppendLine($"RawData R : {BuildRawAttributeSummary(right)}");
|
|
|
|
var leftColors = SafeArrayRead(() => left.colors32);
|
|
var rightColors = SafeArrayRead(() => right.colors32);
|
|
|
|
sb.AppendLine($"Colors count : {leftColors?.Length ?? -1} vs {rightColors?.Length ?? -1}");
|
|
|
|
if (leftColors != null && rightColors != null)
|
|
{
|
|
sb.AppendLine($"Alpha stats : {BuildAlphaSummary(leftColors)} vs {BuildAlphaSummary(rightColors)}");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string DescribeImporter(Mesh mesh)
|
|
{
|
|
var path = AssetDatabase.GetAssetPath(mesh);
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
return "<no importer>";
|
|
}
|
|
|
|
var importer = AssetImporter.GetAtPath(path);
|
|
if (importer == null)
|
|
{
|
|
return "<missing importer>";
|
|
}
|
|
|
|
if (importer is ModelImporter modelImporter)
|
|
{
|
|
return $"ModelImporter(readable={modelImporter.isReadable})";
|
|
}
|
|
|
|
return importer.GetType().Name;
|
|
}
|
|
|
|
private static string BuildRawAttributeSummary(Mesh mesh)
|
|
{
|
|
try
|
|
{
|
|
using var dataArray = Mesh.AcquireReadOnlyMeshData(mesh);
|
|
if (dataArray.Length == 0)
|
|
{
|
|
return "empty";
|
|
}
|
|
|
|
var data = dataArray[0];
|
|
if (data.vertexCount <= 0)
|
|
{
|
|
return "vertexCount=0";
|
|
}
|
|
|
|
var descriptors = mesh.GetVertexAttributes();
|
|
if (descriptors == null || descriptors.Length == 0)
|
|
{
|
|
return "no descriptors";
|
|
}
|
|
|
|
var offsetsByStream = new Dictionary<int, int>();
|
|
var parts = new List<string>();
|
|
|
|
foreach (var descriptor in descriptors)
|
|
{
|
|
if (!offsetsByStream.TryGetValue(descriptor.stream, out var offset))
|
|
{
|
|
offset = 0;
|
|
}
|
|
|
|
var part = DescribeRawAttribute(data, descriptor, offset);
|
|
parts.Add(part);
|
|
offsetsByStream[descriptor.stream] = offset + GetFormatSize(descriptor.format) * descriptor.dimension;
|
|
}
|
|
|
|
return string.Join(" | ", parts);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return $"<error: {ex.Message}>";
|
|
}
|
|
}
|
|
|
|
private static string DescribeRawAttribute(Mesh.MeshData data, VertexAttributeDescriptor descriptor, int offset)
|
|
{
|
|
var stride = data.GetVertexBufferStride(descriptor.stream);
|
|
var raw = data.GetVertexData<byte>(descriptor.stream);
|
|
if (!raw.IsCreated || raw.Length == 0)
|
|
{
|
|
return $"{descriptor.attribute}:<no data>";
|
|
}
|
|
|
|
switch (descriptor.attribute)
|
|
{
|
|
case VertexAttribute.Color:
|
|
return $"{descriptor.attribute}:{DescribeColor(raw, data.vertexCount, stride, offset)}";
|
|
case VertexAttribute.TexCoord0:
|
|
case VertexAttribute.TexCoord1:
|
|
case VertexAttribute.TexCoord2:
|
|
return $"{descriptor.attribute}:{DescribeVector2(raw, data.vertexCount, stride, offset, descriptor.format)}";
|
|
default:
|
|
return $"{descriptor.attribute}:ok";
|
|
}
|
|
}
|
|
|
|
private static string BuildAttributeSummary(Mesh mesh)
|
|
{
|
|
return
|
|
$"pos={SafeHasAttr(mesh, VertexAttribute.Position)} " +
|
|
$"nrm={SafeHasAttr(mesh, VertexAttribute.Normal)} " +
|
|
$"tan={SafeHasAttr(mesh, VertexAttribute.Tangent)} " +
|
|
$"col={SafeHasAttr(mesh, VertexAttribute.Color)} " +
|
|
$"uv0={SafeHasAttr(mesh, VertexAttribute.TexCoord0)} " +
|
|
$"uv1={SafeHasAttr(mesh, VertexAttribute.TexCoord1)} " +
|
|
$"uv2={SafeHasAttr(mesh, VertexAttribute.TexCoord2)} " +
|
|
$"uv3={SafeHasAttr(mesh, VertexAttribute.TexCoord3)}";
|
|
}
|
|
|
|
private static string BuildAttributeLayout(Mesh mesh)
|
|
{
|
|
try
|
|
{
|
|
var attributes = mesh.GetVertexAttributes();
|
|
if (attributes == null || attributes.Length == 0)
|
|
{
|
|
return "empty";
|
|
}
|
|
|
|
var sb = new StringBuilder();
|
|
for (var i = 0; i < attributes.Length; i++)
|
|
{
|
|
var attribute = attributes[i];
|
|
if (i > 0)
|
|
{
|
|
sb.Append(" | ");
|
|
}
|
|
|
|
var stride = SafeRead(() => mesh.GetVertexBufferStride(attribute.stream), -1);
|
|
sb.Append(
|
|
$"{attribute.attribute}:{attribute.format}x{attribute.dimension}@s{attribute.stream}/stride{stride}");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return $"<error: {ex.Message}>";
|
|
}
|
|
}
|
|
|
|
private static string DescribeColor(NativeArray<byte> raw, int vertexCount, int stride, int offset)
|
|
{
|
|
byte minR = byte.MaxValue;
|
|
byte minG = byte.MaxValue;
|
|
byte minB = byte.MaxValue;
|
|
byte minA = byte.MaxValue;
|
|
byte maxR = byte.MinValue;
|
|
byte maxG = byte.MinValue;
|
|
byte maxB = byte.MinValue;
|
|
byte maxA = byte.MinValue;
|
|
var zeroAlpha = 0;
|
|
|
|
var sample = new StringBuilder();
|
|
var sampleCount = Mathf.Min(vertexCount, 4);
|
|
|
|
for (var i = 0; i < vertexCount; i++)
|
|
{
|
|
var baseIndex = i * stride + offset;
|
|
var r = raw[baseIndex + 0];
|
|
var g = raw[baseIndex + 1];
|
|
var b = raw[baseIndex + 2];
|
|
var a = raw[baseIndex + 3];
|
|
|
|
if (r < minR) minR = r;
|
|
if (g < minG) minG = g;
|
|
if (b < minB) minB = b;
|
|
if (a < minA) minA = a;
|
|
if (r > maxR) maxR = r;
|
|
if (g > maxG) maxG = g;
|
|
if (b > maxB) maxB = b;
|
|
if (a > maxA) maxA = a;
|
|
if (a == 0) zeroAlpha++;
|
|
|
|
if (i < sampleCount)
|
|
{
|
|
if (sample.Length > 0)
|
|
{
|
|
sample.Append(",");
|
|
}
|
|
|
|
sample.Append($"({r},{g},{b},{a})");
|
|
}
|
|
}
|
|
|
|
return $"R[{minR},{maxR}] G[{minG},{maxG}] B[{minB},{maxB}] A[{minA},{maxA}] zeroA={zeroAlpha}/{vertexCount} sample={sample}";
|
|
}
|
|
|
|
private static string DescribeVector2(NativeArray<byte> raw, int vertexCount, int stride, int offset, VertexAttributeFormat format)
|
|
{
|
|
var minX = float.PositiveInfinity;
|
|
var minY = float.PositiveInfinity;
|
|
var maxX = float.NegativeInfinity;
|
|
var maxY = float.NegativeInfinity;
|
|
var sample = new StringBuilder();
|
|
var sampleCount = Mathf.Min(vertexCount, 4);
|
|
|
|
for (var i = 0; i < vertexCount; i++)
|
|
{
|
|
var baseIndex = i * stride + offset;
|
|
float x;
|
|
float y;
|
|
|
|
switch (format)
|
|
{
|
|
case VertexAttributeFormat.Float16:
|
|
x = HalfToFloat((ushort)(raw[baseIndex + 0] | (raw[baseIndex + 1] << 8)));
|
|
y = HalfToFloat((ushort)(raw[baseIndex + 2] | (raw[baseIndex + 3] << 8)));
|
|
break;
|
|
case VertexAttributeFormat.Float32:
|
|
x = BitConverter.ToSingle(new[]
|
|
{
|
|
raw[baseIndex + 0],
|
|
raw[baseIndex + 1],
|
|
raw[baseIndex + 2],
|
|
raw[baseIndex + 3]
|
|
}, 0);
|
|
y = BitConverter.ToSingle(new[]
|
|
{
|
|
raw[baseIndex + 4],
|
|
raw[baseIndex + 5],
|
|
raw[baseIndex + 6],
|
|
raw[baseIndex + 7]
|
|
}, 0);
|
|
break;
|
|
default:
|
|
return $"unsupported({format})";
|
|
}
|
|
|
|
if (x < minX) minX = x;
|
|
if (y < minY) minY = y;
|
|
if (x > maxX) maxX = x;
|
|
if (y > maxY) maxY = y;
|
|
|
|
if (i < sampleCount)
|
|
{
|
|
if (sample.Length > 0)
|
|
{
|
|
sample.Append(",");
|
|
}
|
|
|
|
sample.Append($"({x:0.###},{y:0.###})");
|
|
}
|
|
}
|
|
|
|
return $"x[{minX:0.###},{maxX:0.###}] y[{minY:0.###},{maxY:0.###}] sample={sample}";
|
|
}
|
|
|
|
private static int GetFormatSize(VertexAttributeFormat format)
|
|
{
|
|
switch (format)
|
|
{
|
|
case VertexAttributeFormat.Float32:
|
|
case VertexAttributeFormat.UInt32:
|
|
case VertexAttributeFormat.SInt32:
|
|
return 4;
|
|
case VertexAttributeFormat.Float16:
|
|
case VertexAttributeFormat.UNorm16:
|
|
case VertexAttributeFormat.SNorm16:
|
|
case VertexAttributeFormat.UInt16:
|
|
case VertexAttributeFormat.SInt16:
|
|
return 2;
|
|
case VertexAttributeFormat.UNorm8:
|
|
case VertexAttributeFormat.SNorm8:
|
|
case VertexAttributeFormat.UInt8:
|
|
case VertexAttributeFormat.SInt8:
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private static float HalfToFloat(ushort value)
|
|
{
|
|
var sign = (value >> 15) & 0x1;
|
|
var exponent = (value >> 10) & 0x1F;
|
|
var mantissa = value & 0x3FF;
|
|
|
|
if (exponent == 0)
|
|
{
|
|
if (mantissa == 0)
|
|
{
|
|
return sign == 0 ? 0f : -0f;
|
|
}
|
|
|
|
return (float)((sign == 0 ? 1 : -1) * Math.Pow(2, -14) * (mantissa / 1024.0));
|
|
}
|
|
|
|
if (exponent == 31)
|
|
{
|
|
if (mantissa == 0)
|
|
{
|
|
return sign == 0 ? float.PositiveInfinity : float.NegativeInfinity;
|
|
}
|
|
|
|
return float.NaN;
|
|
}
|
|
|
|
return (float)((sign == 0 ? 1 : -1) * Math.Pow(2, exponent - 15) * (1.0 + mantissa / 1024.0));
|
|
}
|
|
|
|
private static string SafeBounds(Mesh mesh)
|
|
{
|
|
try
|
|
{
|
|
var bounds = mesh.bounds;
|
|
return $"center={bounds.center}, extents={bounds.extents}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return $"<error: {ex.Message}>";
|
|
}
|
|
}
|
|
|
|
private static T SafeRead<T>(Func<T> getter, T fallback)
|
|
{
|
|
try
|
|
{
|
|
return getter();
|
|
}
|
|
catch
|
|
{
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
private static T[] SafeArrayRead<T>(Func<T[]> getter)
|
|
{
|
|
try
|
|
{
|
|
return getter();
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static bool SafeHasAttr(Mesh mesh, VertexAttribute attribute)
|
|
{
|
|
try
|
|
{
|
|
return mesh.HasVertexAttribute(attribute);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string BuildColorStats(Color32[] colors)
|
|
{
|
|
if (colors.Length == 0)
|
|
{
|
|
return "empty";
|
|
}
|
|
|
|
byte minR = byte.MaxValue;
|
|
byte minG = byte.MaxValue;
|
|
byte minB = byte.MaxValue;
|
|
byte minA = byte.MaxValue;
|
|
byte maxR = byte.MinValue;
|
|
byte maxG = byte.MinValue;
|
|
byte maxB = byte.MinValue;
|
|
byte maxA = byte.MinValue;
|
|
var zeroAlphaCount = 0;
|
|
|
|
foreach (var color in colors)
|
|
{
|
|
if (color.r < minR) minR = color.r;
|
|
if (color.g < minG) minG = color.g;
|
|
if (color.b < minB) minB = color.b;
|
|
if (color.a < minA) minA = color.a;
|
|
if (color.r > maxR) maxR = color.r;
|
|
if (color.g > maxG) maxG = color.g;
|
|
if (color.b > maxB) maxB = color.b;
|
|
if (color.a > maxA) maxA = color.a;
|
|
if (color.a == 0) zeroAlphaCount++;
|
|
}
|
|
|
|
return $"R[{minR},{maxR}] G[{minG},{maxG}] B[{minB},{maxB}] A[{minA},{maxA}] zeroA={zeroAlphaCount}/{colors.Length}";
|
|
}
|
|
|
|
private static string BuildAlphaSummary(Color32[] colors)
|
|
{
|
|
if (colors.Length == 0)
|
|
{
|
|
return "empty";
|
|
}
|
|
|
|
byte minA = byte.MaxValue;
|
|
byte maxA = byte.MinValue;
|
|
var zeroAlphaCount = 0;
|
|
|
|
foreach (var color in colors)
|
|
{
|
|
if (color.a < minA) minA = color.a;
|
|
if (color.a > maxA) maxA = color.a;
|
|
if (color.a == 0) zeroAlphaCount++;
|
|
}
|
|
|
|
return $"A[{minA},{maxA}] zeroA={zeroAlphaCount}/{colors.Length}";
|
|
}
|
|
|
|
private static string BuildColorSample(Color32[] colors, int maxCount)
|
|
{
|
|
if (colors.Length == 0)
|
|
{
|
|
return "empty";
|
|
}
|
|
|
|
var count = Mathf.Min(colors.Length, maxCount);
|
|
var sb = new StringBuilder();
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var color = colors[i];
|
|
if (i > 0)
|
|
{
|
|
sb.Append(" | ");
|
|
}
|
|
|
|
sb.Append($"#{i}:({color.r},{color.g},{color.b},{color.a})");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
#endif |