Unity中Mesh和subMesh的区别

Unity中的Mesh很好理解,就是一个网格的数据结构,里面记录了顶点、顶点索引等信息:

Mesh mesh = new Mesh();
mesh.vertices = ...
mesh.triangles = ...
mesh.uv = ...//都可以对其赋值

什么是subMesh
Mesh里面有这么一个参数mesh.subMeshCount,所以说明了subMesh是 属于 Mesh的,subMesh是mesh的子mesh,subMesh所有的顶点数据信息都来自于对应的Mesh,subMesh自身独有的内容,实际上是记录了一串索引的数组,从subMesh取得的索引信息,加上原Mesh具有的顶点信息,我们就可以绘制出原来整个Mesh的一部分,那么subMesh有什么用呢?举个例子,对于一个贴了多种材质(Material)的模型object,往往不同的材质会对应模型不同的部位,当我们将模型导出的时候,如果每一个材质,我们都导出一个完整的模型的mesh,这样无疑是很浪费内存的,所以说,为了进行区分,每一个材质对应的顶点部分对应的mesh,就组成了一个subMesh,即一种材质对应一个subMesh,如果一个模型只有一个材质,那么导出的subMesh自然就只有一个。

可以看看Unity Document的解释:

Each sub-mesh corresponds to a Material in a Renderer, such as MeshRenderer or SkinnedMeshRenderer. A sub-mesh consists of a list of triangles, which refer to a set of vertices. Vertices can be shared between multiple sub-meshes.

Each sub-mesh corresponds to a Material in a Renderer, such as MeshRenderer or SkinnedMeshRenderer. A sub-mesh consists of a list of triangles, which refer to a set of vertices. Vertices can be shared between multiple sub-meshes.

如何创建和读取subMesh
可以利用Mesh.SetTrianglesMesh.GetTriangles等函数来进行读取和存储subMesh的数据,如下图所示。
在这里插入图片描述
大致是这么个使用方法,需要设置mesh.subMeshCount:

lineMesh.subMeshCount = 2;
lineMesh.vertices = lineVertices.ToArray();
lineMesh.SetNormals(normals);

//绘制0号submesh
lineMesh.SetIndices(lineIndices[0].ToArray(), MeshTopology.Lines, 0);
Gizmos.DrawMesh(lineMesh, 0, gameObject.transform.position);

//绘制1号submesh
lineMesh.SetIndices(lineIndices[1].ToArray(), MeshTopology.Lines, 1);
Gizmos.DrawMesh(lineMesh, 1, gameObject.transform.position);
<think>我们正在使用Unity引擎,并且需要将Mesh导出为OBJ文件格式。OBJ是一种简单的3D模型文件格式,通常包含顶点、法线、纹理坐标面信息。步骤:1.获取目标Mesh:通常来自MeshFilter或SkinnedMeshRenderer(如果是动态骨骼变形的网格)。2.遍历所有顶点、法线、UV面(三角形)。3.按照OBJ格式写入文件:-顶点:格式为 "v xy z"-法线:格式为 "vn nxny nz"-纹理坐标:格式为 "vt uv"-面:格式为 "f vertex_index/uv_index/normal_index ...",注意索引从1开始,并且OBJ文件支持多种格式的面,这里我们使用顶点索引/UV索引/法线索引的格式。4.注意:如果网格有子网格(多个材质),则需要分别处理每个子网格的三角形。但是,需要注意的是,OBJ文件不支持骨骼动画,所以如果是SkinnedMeshRenderer,我们需要先烘焙出静态网格(即当前姿态下的网格)。我们将编写一个静态方法,它接收一个游戏对象(可能包含多个网格)一个文件路径,然后将整个游戏对象的网格导出为一个OBJ文件。另外,由于OBJ文件是文本文件,所以我们可以使用StreamWriter来写入。考虑以下几点: -坐标系的转换:Unity是左手坐标系,而OBJ通常是右手坐标系。通常做法是在导出时翻转Z轴或者翻转法线的Z轴?但常见的做法是在导出时翻转顶点坐标的Z轴(乘以-1),这样在导入到其他软件(如Maya)时方向就正确了。不过,也可以选择不翻转,根据需求而定。这里我们按照常见的做法,翻转Z轴。 -如果游戏对象有多个子对象,我们需要递归地导出所有子对象的网格,并合并到一个OBJ文件中。同时,为了区分不同部分,我们可以在每个对象前加一个“o对象名”的标记。 -索引的处理:每个网格的索引是独立的,但写入OBJ文件时,顶点、法线、UV的索引是全局连续的。因此,我们需要记录当前已经写入的顶点数量、法线数量、UV数量,以便在写入面的时候加上这些偏移量。实现思路:1.创建一个StringBuilder来构建OBJ文件内容(或者直接使用StreamWriter逐行写入)。2.递归遍历游戏对象及其子对象。3.对于每个对象,获取MeshFilter或SkinnedMeshRenderer(如果是SkinnedMeshRenderer,则使用BakeMesh获取当前网格)。4.写入对象名称(使用"o对象名")。5.写入顶点数据(翻转Z轴),并记录顶点数量偏移。6.写入法线数据(翻转Z轴?为了与顶点保持一致,我们也翻转法线的Z轴),并记录法线数量偏移。7.写入UV数据。8.写入面数据,注意每个面的索引需要加上之前累积的偏移量,并且索引从1开始(OBJ格式要求)。9.处理多个子网格(如果存在)的情况:每个子网格对应一组三角形,但顶点、法线、UV是共享的,所以只需要在写入面的时候按子网格分别写入即可。注意:如果游戏对象没有MeshFilter或SkinnedMeshRenderer,则跳过。由于OBJ文件不支持材质名称的定义(但可以在.mtl文件中定义),这里我们只导出几何体。不过,我们可以在OBJ文件中添加使用材质的语句(如"usemtlMaterialName"),但需要同时生成.mtl文件。由于问题只要求导出OBJ,我们暂时不处理材质。但是,为了完整性,我们可以考虑同时生成一个.mtl文件,不过题目没有明确要求。这里我们先只导出几何体。另外,如果网格非常大,使用StringBuilder可能会占用大量内存,因此我们选择使用StreamWriter逐行写入文件。我们将编写一个类,包含一个公共静态方法:ExportGameObjectToOBJ(GameObject gameObject,string filePath,bool includeChildren= true)。步骤:1.创建文件流StreamWriter。2.写入文件头(如注释等)。3.递归遍历对象,收集Mesh数据并写入。4.关闭StreamWriter。对于SkinnedMeshRenderer,我们需要在导出前烘焙网格。使用Mesh实例来存储烘焙后的网格,并在使用后销毁。代码实现如下:</think>### Unity 中将 Mesh 导出为 OBJ 文件 以下是完整的解决方案,包含 C# 脚本代码详细解释: ```csharp using System.IO; using System.Text; using UnityEngine; public class OBJExporter : MonoBehaviour { public static string MeshToOBJ(Mesh mesh, string objectName = "ExportedMesh") { StringBuilder sb = new StringBuilder(); // OBJ 文件头信息 sb.AppendLine("# Exported from Unity\n"); sb.AppendLine($"g {objectName}"); sb.AppendLine(); // 导出顶点数据 (翻转 Z 轴以适应 OBJ 坐标系) foreach (Vector3 vertex in mesh.vertices) { sb.AppendLine($"v {-vertex.x} {vertex.y} {vertex.z}"); } sb.AppendLine(); // 导出 UV 数据 foreach (Vector2 uv in mesh.uv) { sb.AppendLine($"vt {uv.x} {uv.y}"); } sb.AppendLine(); // 导出法线数据 (翻转 Z 轴) foreach (Vector3 normal in mesh.normals) { sb.AppendLine($"vn {-normal.x} {normal.y} {normal.z}"); } sb.AppendLine(); // 导出三角形面数据 (处理子网格) int triangleIndex = 0; for (int submesh = 0; submesh < mesh.subMeshCount; submesh++) { sb.AppendLine($"usemtl material_{submesh}"); sb.AppendLine($"s {submesh + 1}"); int[] triangles = mesh.GetTriangles(submesh); for (int i = 0; i < triangles.Length; i += 3) { // OBJ 索引从 1 开始,格式: 顶点索引/UV索引/法线索引 int idx1 = triangles[i] + 1; int idx2 = triangles[i + 1] + 1; int idx3 = triangles[i + 2] + 1; sb.AppendLine($"f {idx1}/{idx1}/{idx1} {idx2}/{idx2}/{idx2} {idx3}/{idx3}/{idx3}"); } triangleIndex += triangles.Length; sb.AppendLine(); } return sb.ToString(); } public static void ExportGameObject(GameObject gameObject, string filePath) { StringBuilder fullScene = new StringBuilder(); fullScene.AppendLine("# Unity OBJ Exporter\n"); // 递归处理所有子对象 ProcessGameObject(gameObject, fullScene, Matrix4x4.identity); // 写入文件 File.WriteAllText(filePath, fullScene.ToString()); Debug.Log($"OBJ exported to: {filePath}"); } private static void ProcessGameObject( GameObject current, StringBuilder sb, Matrix4x4 parentMatrix) { // 计算当前对象的变换矩阵 Matrix4x4 localToWorld = parentMatrix * current.transform.localToWorldMatrix; // 处理 MeshFilter MeshFilter meshFilter = current.GetComponent<MeshFilter>(); if (meshFilter && meshFilter.sharedMesh) { Mesh mesh = meshFilter.sharedMesh; // 应用变换到顶点法线 Vector3[] transformedVerts = new Vector3[mesh.vertices.Length]; Vector3[] transformedNormals = new Vector3[mesh.normals.Length]; for (int i = 0; i < mesh.vertices.Length; i++) { transformedVerts[i] = localToWorld.MultiplyPoint(mesh.vertices[i]); } for (int i = 0; i < mesh.normals.Length; i++) { transformedNormals[i] = localToWorld.MultiplyVector(mesh.normals[i]).normalized; } // 创建临时网格保存变换后的数据 Mesh tempMesh = new Mesh { vertices = transformedVerts, normals = transformedNormals, uv = mesh.uv, triangles = mesh.triangles, subMeshCount = mesh.subMeshCount }; // 添加子网格数据 for (int i = 0; i < mesh.subMeshCount; i++) { tempMesh.SetTriangles(mesh.GetTriangles(i), i); } // 导出当前网格 sb.AppendLine(MeshToOBJ(tempMesh, current.name)); Object.DestroyImmediate(tempMesh); } // 递归处理子对象 foreach (Transform child in current.transform) { ProcessGameObject(child.gameObject, sb, localToWorld); } } } ``` ### 使用示例 在 Unity 中创建一个空对象,附加以下脚本: ```csharp public class ExportExample : MonoBehaviour { public GameObject targetObject; // 拖入需要导出的对象 void Start() { // 导出到桌面 string path = System.Environment.GetFolderPath( System.Environment.SpecialFolder.Desktop) + "/exported.obj"; OBJExporter.ExportGameObject(targetObject, path); } } ``` ### 代码解释 1. **坐标系转换**: - Unity 使用左手坐标系,OBJ 使用右手坐标系 - 顶点法线的 Z 轴值取反(`-vertex.z` `-normal.z`) - 顶点应用世界变换矩阵保持正确位置 2. **数据结构**: - `v`: 顶点坐标 (x, y, z) - `vt`: UV 纹理坐标 (u, v) - `vn`: 法线向量 (x, y, z) - `f`: 三角形面 (顶点索引/UV索引/法线索引) 3. **关键功能**: - 递归处理子对象层级结构 - 自动处理世界变换矩阵 - 支持多材质/子网格(每个子网格使用 `usemtl` 标识) - 保留对象名称层级关系 4. **特殊处理**: - 动态网格(如 SkinnedMeshRenderer)需要先烘焙 - 大场景导出建议分块处理 - 法线需要重新归一化处理 ### 导出注意事项 1. 材质需要额外导出 MTL 文件(当前代码仅导出几何体) 2. 动态网格需添加烘焙步骤: ```csharp // 在 ProcessGameObject 中添加 SkinnedMeshRenderer skinned = current.GetComponent<SkinnedMeshRenderer>(); if (skinned) { Mesh bakedMesh = new Mesh(); skinned.BakeMesh(bakedMesh); // 使用 bakedMesh 替代 meshFilter.sharedMesh } ``` 3. 纹理坐标处理: - OBJ 使用 (0,0) 左下角坐标系 - Unity 使用 (0,0) 左上角坐标系 - 如需兼容需翻转 V 轴:`Vector2 uv = new Vector2(uv.x, 1 - uv.y)`
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值