Unity导出stl格式
stl是常用的3D打印格式,目前有不少文章介绍stl的,这里不多介绍。 导出stl分为ascii形式和二进制形式,区别在于ascii可以直接用文本文件打开查看,而二进制直接打开是乱码,但是二进制形式读写速度较快,生成的文件也比ascii要小很多。
开发环境
unity:2018.2.16 2019.3.15 模型查看工具:Meshlab2020.09 、 CAD Assistant 测试模型:assetstore 上 的 office building
核心模块
/// <summary>
/// 将单个mesh数据使用StreamWrite写出为stl
/// </summary>
/// <param name="mesh">待导出的mesh</param>
/// <param name="sw">输出流</param>
/// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
/// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
private static void ExportMeshToStl(Mesh mesh, StreamWriter sw, Transform trans, bool exchangeCoordinate = true)
{
for (int j = 0; j < mesh.subMeshCount; j++)
{
int[] tris;
if (mesh.subMeshCount == 1)
{
sw.Write("\nsolid " + mesh.name + "\n");
tris = mesh.triangles;
}
else
{
sw.Write("\nsolid " + mesh.name + "_" + j + "\n");
tris = mesh.GetIndices(j);
}
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
for (int i = 0; i < tris.Length / 3; i++)
{
//法线变换到世界空间
Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);
//顶点变换到世界空间
Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);
//如果需要从左手系变换到右手系
if (exchangeCoordinate)
{
nor1.x *= -1;
nor2.x *= -1;
nor3.x *= -1;
worldPos1.x *= -1;
worldPos2.x *= -1;
worldPos3.x *= -1;
}
Vector3 normal = (nor1 + nor2 + nor3) / 3;
sw.Write("\tfacet normal " + normal.x + " " + normal.y + " " + normal.z);
sw.Write("\n\t\touter loop\n");
sw.Write("\t\t\tvertex " + worldPos1.x + " " + worldPos1.y + " " + worldPos1.z + "\n");
if (exchangeCoordinate)
{
sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
}
else
{
sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
}
sw.Write("\t\tendloop\n");
sw.Write("\tendfacet\n");
}
if (mesh.subMeshCount == 1)
{
sw.Write("endsolid " + mesh.name);
}
else
{
sw.Write("endsolid " + mesh.name + "_" + j);
}
}
}
/// <summary>
/// 将单个mesh数据使用StreamWrite写出为stl,二进制格式
/// </summary>
/// <param name="mesh">待导出的mesh</param>
/// <param name="bw">BinaryWriter 写出二进制数据的类</param>
/// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
/// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
private static void ExportMeshToStl(Mesh mesh, BinaryWriter bw, Transform trans, bool exchangeCoordinate = true)
{
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
int[] tris = mesh.triangles;
//每个三角面片固定占用50个字节
for (int i = 0; i < tris.Length / 3; i++)
{
Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);
Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);
if (exchangeCoordinate)
{
nor1.x *= -1;
nor2.x *= -1;
nor3.x *= -1;
worldPos1.x *= -1;
worldPos2.x *= -1;
worldPos3.x *= -1;
}
Vector3 normal = (nor1 + nor2 + nor3) / 3;
bw.Write(normal.x);
bw.Write(normal.y);
bw.Write(normal.z);
bw.Write(worldPos1.x);
bw.Write(worldPos1.y);
bw.Write(worldPos1.z);
if (exchangeCoordinate)
{
bw.Write(worldPos3.x);
bw.Write(worldPos3.y);
bw.Write(worldPos3.z);
bw.Write(worldPos2.x);
bw.Write(worldPos2.y);
bw.Write(worldPos2.z);
}
else
{
bw.Write(worldPos2.x);
bw.Write(worldPos2.y);
bw.Write(worldPos2.z);
bw.Write(worldPos3.x);
bw.Write(worldPos3.y);
bw.Write(worldPos3.z);
}
//填充两个字节 三角面片的最后2个字节用来描述三角面片的属性信息(包括颜色属性等)暂时没有用
bw.Seek(2, SeekOrigin.Current);
}
}
完整代码
/****************************************************
文件:Exporter.cs
作者:TKB
邮箱: 544726237@qq.com
日期:2021/7/24 23:9:12
功能:导出stl
*****************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
namespace TLib
{
public class Exporter
{
#region 导出stl
/// <summary>
/// 导出Transfrom及其子mesh为单个stl
/// </summary>
/// <param name="trans">待导出的transfrom</param>
/// <param name="outputPath">导出的stl完整路径,如D:/TKB/output.stl</param>
/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
/// <param name="isBinary">是否以格式导出</param>
public static void ExportStl(Transform trans, string outputPath, bool exchangeCoordinate = true, bool isBinary = true)
{
ExportStl(trans.gameObject, outputPath, exchangeCoordinate, isBinary);
}
/// <summary>
/// 导出GameObject及其子mesh为单个stl
/// </summary>
/// <param name="go">待导出的GameObject</param>
/// <param name="outputPath">导出的stl完整路径,如D:/TKB/output.stl</param>
/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
/// <param name="isBinary">是否以格式导出</param>
public static void ExportStl(GameObject go, string outputPath, bool exchangeCoordinate = true, bool isBinary = true)
{
if (!go) return;
if (!Directory.Exists(Path.GetDirectoryName(outputPath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
}
if (File.Exists(outputPath))
{
try
{
File.Delete(outputPath);
Debug.LogWarning("该路径已存在同名文件,已删除!" + outputPath);
}
catch (Exception e)
{
Debug.LogError(e + "该路径已存在同名文件并且删除失败!" + outputPath);
return;
}
}
MeshFilter[] meshFilters = go.GetComponentsInChildren<MeshFilter>();
SkinnedMeshRenderer[] skinnedMeshRenderers = go.GetComponentsInChildren<SkinnedMeshRenderer>();
int meshCount = meshFilters.Length + skinnedMeshRenderers.Length;
try
{
FileStream meshFS = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); ;
StreamWriter meshSW = null;
BinaryWriter meshBW = null;
if (!isBinary)
{
meshSW = new StreamWriter(meshFS, Encoding.UTF8);
}
else
{
meshBW = new BinaryWriter(meshFS, Encoding.UTF8);
//文件的起始80字节是文件头存储零件名,可以放入任何文字信息
meshBW.Write(go.name);
meshBW.Seek(80, SeekOrigin.Begin);
//紧随着用4个字节的整数来描述实体的三角面片个数
int count = 0;
for (int i = 0; i < meshFilters.Length; i++)
{
Mesh mesh;
#if UNITY_EDITOR
mesh = meshFilters[i].sharedMesh;
#else
mesh = meshFilters[i].mesh;
#endif
count += mesh.triangles.Length;
}
for (int i = 0; i < skinnedMeshRenderers.Length; i++)
{
Mesh mesh;
#if UNITY_EDITOR
mesh = skinnedMeshRenderers[i].sharedMesh;
#else
mesh = meshFilters[i].mesh;
#endif
count += mesh.triangles.Length;
}
meshBW.Write(count/3);
}
for (int i = 0; i < meshFilters.Length; i++)
{
Mesh mesh;
#if UNITY_EDITOR
mesh = meshFilters[i].sharedMesh;
UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + i + "/" + meshCount, i * 1.0f / meshCount);
#else
mesh = meshFilters[i].mesh;
#endif
if (!isBinary)
ExportMeshToStl(mesh, meshSW, meshFilters[i].transform, exchangeCoordinate);
else
ExportMeshToStl(mesh, meshBW, meshFilters[i].transform, exchangeCoordinate);
}
for (int i = 0; i < skinnedMeshRenderers.Length; i++)
{
Mesh mesh;
#if UNITY_EDITOR
mesh = skinnedMeshRenderers[i].sharedMesh;
UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + (i + meshFilters.Length) + "/" + meshCount, i * 1.0f / meshCount);
#else
mesh = meshFilters[i].mesh;
#endif
if (!isBinary)
ExportMeshToStl(mesh, meshSW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
else
ExportMeshToStl(mesh, meshBW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
}
if (!isBinary)
{
meshSW.Close();
}
else
{
meshBW.Close();
}
meshFS.Close();
}
catch (Exception e)
{
Debug.LogError(e);
}
finally
{
UnityEditor.EditorUtility.ClearProgressBar();
}
}
/// <summary>
/// 将Transform及其子对象导出为多个stl,每个mesh对应一个
/// </summary>
/// <param name="trans">待导出的Transform</param>
/// <param name="outputDir">导出的文件夹路径,stl存放的位置</param>
/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
/// <param name="isBinary">是否以格式导出</param>
public static void ExportStls(Transform trans, string outputDir, bool exchangeCoordinate = true, bool isBinary = true)
{
ExportStls(trans.gameObject, outputDir, exchangeCoordinate, isBinary);
}
/// <summary>
/// 将GameObject及其子对象导出为多个stl,每个mesh对应一个
/// </summary>
/// <param name="go">待导出的GameObject</param>
/// <param name="outputDir">导出的文件夹路径,stl存放的位置</param>
/// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
/// <param name="isBinary">是否以格式导出</param>
public static void ExportStls(GameObject go, string outputDir, bool exchangeCoordinate = true, bool isBinary = true)
{
if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir);
MeshFilter[] meshFilters = go.GetComponentsInChildren<MeshFilter>();
SkinnedMeshRenderer[] skinnedMeshRenderers = go.GetComponentsInChildren<SkinnedMeshRenderer>();
Dictionary<string, int> meshNameDic = new Dictionary<string, int>();
int meshCount = meshFilters.Length + skinnedMeshRenderers.Length;
for (int i = 0; i < meshFilters.Length; i++)
{
try
{
string name = meshFilters[i].gameObject.name;
if (meshNameDic.ContainsKey(name))
{
meshNameDic[name]++;
name += meshNameDic[name];
}
else meshNameDic.Add(name, 0);
string stlPath = Path.Combine(outputDir, name + ".stl");
FileStream meshFS = new FileStream(stlPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
StreamWriter meshSW = null;
BinaryWriter meshBW = null;
Mesh mesh;
#if UNITY_EDITOR
mesh = meshFilters[i].sharedMesh;
UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + i + "/" + meshCount, i * 1.0f / meshCount);
#else
mesh = meshFilters[i].mesh;
#endif
if (!isBinary)
{
meshSW = new StreamWriter(meshFS, Encoding.UTF8);
ExportMeshToStl(mesh, meshSW, meshFilters[i].transform, exchangeCoordinate);
meshSW.Close();
}
else
{
meshBW = new BinaryWriter(meshFS, Encoding.UTF8);
//文件的起始80字节是文件头存储零件名,可以放入任何文字信息
meshBW.Write(name);
meshBW.Seek(80, SeekOrigin.Begin);
meshBW.Write(mesh.triangles.Length / 3);
ExportMeshToStl(mesh, meshBW, meshFilters[i].transform, exchangeCoordinate);
meshBW.Close();
}
meshFS.Close();
}
catch (Exception e)
{
Debug.LogError(e);
}
}
for (int i = 0; i < skinnedMeshRenderers.Length; i++)
{
try
{
string name = skinnedMeshRenderers[i].gameObject.name;
if (meshNameDic.ContainsKey(name))
{
name += meshNameDic[name];
meshNameDic[name]++;
}
else meshNameDic.Add(name, 1);
string stlPath = Path.Combine(outputDir, name + ".stl");
FileStream meshFS = new FileStream(stlPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
StreamWriter meshSW = null;
BinaryWriter meshBW = null;
Mesh mesh;
#if UNITY_EDITOR
mesh = skinnedMeshRenderers[i].sharedMesh;
UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + (i + meshFilters.Length) + "/" + meshCount, i * 1.0f / meshCount);
#else
mesh = meshFilters[i].mesh;
materials;
#endif
if (!isBinary)
{
meshSW = new StreamWriter(meshFS, Encoding.UTF8);
ExportMeshToStl(mesh, meshSW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
meshSW.Close();
}
else
{
meshBW = new BinaryWriter(meshFS, Encoding.UTF8);
//文件的起始80字节是文件头存储零件名,可以放入任何文字信息
meshBW.Write(name);
meshBW.Seek(80, SeekOrigin.Begin);
meshBW.Write(mesh.triangles.Length / 3);
ExportMeshToStl(mesh, meshBW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
meshBW.Close();
}
meshFS.Close();
}
catch (Exception e)
{
Debug.LogError(e);
}
}
UnityEditor.EditorUtility.ClearProgressBar();
}
/// <summary>
/// 将单个mesh数据使用StreamWrite写出为stl
/// </summary>
/// <param name="mesh">待导出的mesh</param>
/// <param name="sw">输出流</param>
/// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
/// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
private static void ExportMeshToStl(Mesh mesh, StreamWriter sw, Transform trans, bool exchangeCoordinate = true)
{
for (int j = 0; j < mesh.subMeshCount; j++)
{
int[] tris;
if (mesh.subMeshCount == 1)
{
sw.Write("\nsolid " + mesh.name + "\n");
tris = mesh.triangles;
}
else
{
sw.Write("\nsolid " + mesh.name + "_" + j + "\n");
tris = mesh.GetIndices(j);
}
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
for (int i = 0; i < tris.Length / 3; i++)
{
Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);
Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);
if (exchangeCoordinate)
{
nor1.x *= -1;
nor2.x *= -1;
nor3.x *= -1;
worldPos1.x *= -1;
worldPos2.x *= -1;
worldPos3.x *= -1;
}
Vector3 normal = (nor1 + nor2 + nor3) / 3;
sw.Write("\tfacet normal " + normal.x + " " + normal.y + " " + normal.z);
sw.Write("\n\t\touter loop\n");
sw.Write("\t\t\tvertex " + worldPos1.x + " " + worldPos1.y + " " + worldPos1.z + "\n");
if (exchangeCoordinate)
{
sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
}
else
{
sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
}
sw.Write("\t\tendloop\n");
sw.Write("\tendfacet\n");
}
if (mesh.subMeshCount == 1)
{
sw.Write("endsolid " + mesh.name);
}
else
{
sw.Write("endsolid " + mesh.name + "_" + j);
}
}
}
/// <summary>
/// 将单个mesh数据使用StreamWrite写出为stl,二进制格式
/// </summary>
/// <param name="mesh">待导出的mesh</param>
/// <param name="bw">BinaryWriter 写出二进制数据的类</param>
/// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
/// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
private static void ExportMeshToStl(Mesh mesh, BinaryWriter bw, Transform trans, bool exchangeCoordinate = true)
{
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
int[] tris = mesh.triangles;
//每个三角面片固定占用50个字节
for (int i = 0; i < tris.Length / 3; i++)
{
Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);
Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);
if (exchangeCoordinate)
{
nor1.x *= -1;
nor2.x *= -1;
nor3.x *= -1;
worldPos1.x *= -1;
worldPos2.x *= -1;
worldPos3.x *= -1;
}
Vector3 normal = (nor1 + nor2 + nor3) / 3;
bw.Write(normal.x);
bw.Write(normal.y);
bw.Write(normal.z);
bw.Write(worldPos1.x);
bw.Write(worldPos1.y);
bw.Write(worldPos1.z);
if (exchangeCoordinate)
{
bw.Write(worldPos3.x);
bw.Write(worldPos3.y);
bw.Write(worldPos3.z);
bw.Write(worldPos2.x);
bw.Write(worldPos2.y);
bw.Write(worldPos2.z);
}
else
{
bw.Write(worldPos2.x);
bw.Write(worldPos2.y);
bw.Write(worldPos2.z);
bw.Write(worldPos3.x);
bw.Write(worldPos3.y);
bw.Write(worldPos3.z);
}
//填充两个字节 三角面片的最后2个字节用来描述三角面片的属性信息(包括颜色属性等)暂时没有用
bw.Seek(2, SeekOrigin.Current);
}
}
}
#endregion
}
测试脚本
/****************************************************
文件:ExportStlExample.cs
作者:TKB
邮箱: 544726237@qq.com
日期:2021/7/26 22:19:56
功能:编辑器环境测试将选中的物体导出stl
*****************************************************/
using UnityEngine;
namespace TLib
{
public class ExportStlExample
{
#if UNITY_EDITOR
//将当前选中的物体下所有mesh导出到一个stl中
[UnityEditor.MenuItem("Tools/导出stl", false)]
private static void OnClickExportObj()
{
GameObject go = UnityEditor.Selection.activeObject as GameObject;
Exporter.ExportStl(go.transform, Application.dataPath +"/"+ go.name+".stl",true,false);
UnityEditor.AssetDatabase.Refresh();
}
//将当前选中的物体下的mesh分别导出为stl
[UnityEditor.MenuItem("Tools/导出stls", false)]
private static void OnClickExportObj1()
{
GameObject go = UnityEditor.Selection.activeObject as GameObject;
Exporter.ExportStls(go, Application.dataPath + "/Exports");
UnityEditor.AssetDatabase.Refresh();
}
#endif
}
}
效果
unity中的模型截图: 选择导出为stl 在meshlab中的截图: 选择导出为stls时导出了3000+的模型:
注意
这个测试模型导出二进制格式时使用meshlab打开报错,但是用CAD Assistant可以打开,原因还不清楚
|