unity点选构建Mesh并保存OBJ

这篇博客讲述了如何在Unity中实现策划指定区域选择,通过构建和保存自定义Mesh来满足需求。作者分享了简单shader的编写、脚本控制以及自创Mesh的保存过程,旨在简化后续程序使用。

最近有一份需求,就是让策划任意选择可一片区域,表明是有某种用途的。埋头写了两头,试了三四种方法,最终不得已用小方格来代替,并最终构建mesh保存下来,这样做程序的就很方便用了。我们的目标就是没有蛀牙大笑oh,应该是累死别人,轻松自己!!!

1,简单shader编写

首先应该搞一个简单的Shader来显示我们的所编辑的内容
Shader "Custom/BlockShader" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_Size ("Size",Vector) = (64,32,0,0) //在面板上显示长多少格,宽多少格
		_Color ("Color", Color) = (0,0,0,0.5)
		_RimColor ("Rim Color", Color) = (1,1,0,1)
		_RimPower ("Rim Power", Range(0,0.2)) = 0.01
		_HitPoint ("Hit Point",Vector) = (0.0,1,0.0,0.5)//鼠标点击点
		_SelectColor ("Select Color",Color) = (0,1,0,1)//选中颜色
		_HitColor ("Already hit Color", Color) = (0.2,0.8,0.9,0.5)
	}
	SubShader {
		Tags { "Queue"="Transparent" }
		
		CGPROGRAM
		#pragma surface surf Lambert alpha
		#pragma target 3.0

		sampler2D _MainTex;
		half4 _Size;
		fixed4 _Color;
		fixed4 _RimColor;
		float _RimPower;
		float4 _HitPoint;
		fixed4 _SelectColor;
		fixed4 _HitColor;

		struct Input {
			float2 uv_MainTex;
		};

		void surf (Input IN, inout SurfaceOutput o) {
			float2 uv = IN.uv_MainTex;
			fixed4 c = _Color;
			float radiox = 1.0 / (_Size.x * 2);//每格大小的一半
			float radioy = 1.0 / (_Size.y * 2);

			float nx = floor(uv.x/(radiox * 2)) * radiox * 2 + radiox;//找到格的中心点
			float ny = floor(uv.y/(radioy * 2)) * radioy * 2 + radioy;
			fixed4 tc = tex2D (_MainTex, float2(nx,ny) );
			if(tc.g > 0.85) //因为选中我设置为了绿色(0,1,0,1)
			{
				c = _HitColor;
				//if(uv.x >= nx-radiox && uv.x <nx+ radiox && uv.y >= ny-radioy && uv.y <= ny+radioy)
				//{ 
				//	c = _HitColor;
				//}
			}


			if(uv.x >= _HitPoint.x-radiox && uv.x <_HitPoint.x+ radiox && uv.y >= _HitPoint.y-radioy && uv.y <= _HitPoint.y+radioy)
			{ 
				c = _SelectColor;//选中颜色
			}

			if(fmod(uv.x ,(radiox * 2)) < (radiox * 2) * _RimPower)//边框
			{
				c = _RimColor;
			}
			if(fmod(uv.y ,(radioy * 2)) < (radioy * 2) * _RimPower)
			{
				c = _RimColor;
			}

			o.Albedo = c.rgb;
			o.Alpha = 0.5;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}
我知道,以上写的很粗糙。。。但是,谁让我是pc上用的呢,谁还管效率是毛线
下一步,需要每套的脚本控制

2,脚本控制

主要是为了运行时,通过点击生成方格
using System.Collections.Generic;
using UnityEngine;

/**
 * 
 * 用于编辑器美术使用脚本,功能为画地形方块
 * 
 */
public class TerrianBlockControl : MonoBehaviour
{
    public int width;
    public int height;
    public Texture2D texture;
#if UNITY_EDITOR
    private Material material;
    private MeshRenderer meshRenderer;
<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)`
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值