合并mesh的功能比较常见,可以运行时合也可以编辑模式下提前合并,但是至少需要注意两点。
1.考虑lightmap,合并mesh以后会将lightmap的信息写入uv2,如果有多张lightmap需要传入正确的index,所以参与合批的mesh需要保证处于同一index的lightmap中。
2.如果地图需要切块,那么合并mesh需要考虑中心点的问题。
如下图所示,美术做场景可能是很随意的,完全可能让父节点和子节点相离的十分远,那么合并完以后中心点如果离的过远,是无法判断出当前所处的地图块的,所以合并完以后我们要让中心点居中。
脚本的使用
1.在Hierarchy视图中选择需要合并的多个游戏对象
2.Tools/CombineMesh开始合并,生成prefab并且生成mesh
3.我是用unity2019.2写的例子,所以代码中用了些c#7的语法。
4.注意UV3
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class CombineMesh : MonoBehaviour
{
public int lightmapIndex = -1;
private void Awake()
{
//初始化设置烘焙贴图的index
if (lightmapIndex >= 0)
{
if(TryGetComponent<MeshRenderer>(out var m))
{
m.lightmapIndex = lightmapIndex;
}
}
}
#if UNITY_EDITOR
[MenuItem("Tools/CombineMesh")]
static void StartCombineMesh()
{
if (Selection.activeTransform)
{
MeshFilter[] meshFilters = Selection.activeTransform.GetComponentsInChildren<MeshFilter>();
//计算父节点的中心点
Vector3 centerPos = GetCenter(meshFilters);
CombineInstance[] combine = new CombineInstance[meshFilters.Length];
Material material=null;
int lightmap = -1;
int i = 0;
while (i < meshFilters.Length)
{
var meshRender = meshFilters[i].GetComponent<MeshRenderer>();
if (meshRender)
{
if (material == null)
material = meshRender.sharedMaterial;
if (material!= meshRender.sharedMaterial)
{
Debug.LogError("存在不同材质不予合并");
return;
}
if (lightmap == -1)
lightmap = meshRender.lightmapIndex;
if (lightmap != meshRender.lightmapIndex)
{
Debug.LogError("存在不同烘焙贴图不予合并");
return;
}
combine[i].mesh = meshFilters[i].sharedMesh;
//记录参与合批的lightmapOffset
combine[i].lightmapScaleOffset = meshRender.lightmapScaleOffset;
//每个参与合批mesh的矩阵与中心点进行偏移计算
Matrix4x4 matrix4X4 = meshFilters[i].transform.localToWorldMatrix;
matrix4X4.m03 -= centerPos.x;
matrix4X4.m13 -= centerPos.y;
matrix4X4.m23 -= centerPos.z;
combine[i].transform = matrix4X4;
i++;
}
}
var go = new GameObject("combine", typeof(MeshFilter), typeof(MeshRenderer));
go.transform.position = centerPos;
go.AddComponent<CombineMesh>().lightmapIndex = lightmap;
var mesh = new Mesh();
mesh.CombineMeshes(combine, true, true, true);
//合拼会自动生成UV3,但是我们并不需要,可以这样删除
mesh.uv3 = null;
AssetDatabase.CreateAsset(mesh, "Assets/combine.asset");
go.GetComponent<MeshFilter>().sharedMesh = mesh;
go.GetComponent<MeshRenderer>().sharedMaterial = material;
if (go)
{
PrefabUtility.SaveAsPrefabAssetAndConnect(go, Application.dataPath + "/combine.prefab", InteractionMode.AutomatedAction);
}
}
Vector3 GetCenter(Component[] components)
{
if (components != null && components.Length > 0)
{
Vector3 min = components[0].transform.position;
Vector3 max = min;
foreach (var comp in components)
{
min = Vector3.Min(min, comp.transform.position);
max = Vector3.Max(max, comp.transform.position);
}
return min + ((max - min) / 2);
}
return Vector3.zero;
}
}
#endif
}
如图选择GameObject根节点然后点击菜单栏Tools/CombineMesh就会自动生成prefab,以及mesh都保存在Project视图中,最重要的是中心点已经归于正中心了。
//每个参与合批mesh的矩阵与中心点进行偏移计算
Matrix4x4 matrix4X4 = meshFilters[i].transform.localToWorldMatrix;
matrix4X4.m03 -= centerPos.x;
matrix4X4.m13 -= centerPos.y;
matrix4X4.m23 -= centerPos.z;
combine[i].transform = matrix4X4;
CombineInstance每个参与合批的都需要传入一个矩阵,这里的localToWorldMatrix就是把本地transform转成世界矩阵,对应unity还提供了WorldToLocalMatrix也就是世界转本地。本地和世界就好比localPosition和Position的关系一样。这里为什么要去修改 矩阵的m03 m13 m23呢?因为这三个值就代表着x、y、z。
我们先举个例子,如图所示,这样一个Transform
如下图所示,对应的矩阵信息如下。上面代码矩阵改的m03 m13 m23就对应着Position的x,y,z。我们还能看到缩放信息对应存在m00 m11 m22中,好像挺简单的。
但是旋转就比较麻烦了,先不管矩阵、四元数、欧拉角,完全放空。来我们先回味一下初中几何,如下图所示,在平面几何中,已知起始点A=(1,0) 半径r=1和旋转的角度a=45 求旋转到P点的坐标。
根据三角函数的原理,OM就是P点的X坐标, MP就是P点的Y坐标,它们现在都是未知的,已知半径r=1所以OP = 1
先算OM距离
cos a = 邻边/斜边
cos a = OM/OP
推导出
OM = OP * cos a
P点X坐标 = 1 * cos a
在算MP距离
sin a = 对边/斜边
sin a = MP/OP
推导出
MP = OP * sin a
P点的Y坐标 = 1 * sin a
最终推到得出, P (x,y) = ( cos a , sin a) 通过平面我们就可以根据这个公式计算出任意轴向的旋转。还没完,我们旋转并不是都是像上图这样从A(1,0)点开始旋转的,完全有可能是从已有的旋转角旋转到另一个角。
如下图所示,假如我们不是从A(1,0)开始旋转,而是从P(x,y)点旋转到P1(x,y)旋转角度为B。那么按照上面的推到公式就不满足 P1 (x,y) = ( cos b , sin b) ,而应该加上原本的P(x,y)点。
所以最终P1的推到公式应该是:
P1.x = P.x * cosB – P.y * sinB
P1.y = P.x * sinB + P.y * cosB
尤其注意为什么P1.x为什么要在减去P.y * sinB和P1.y为什么还要加上cosB,这样平面图如果放入3D中其实P点是沿着Z轴在旋转。大家可以好好理解一下,X轴Y轴Z轴旋转其实都是按cos sin来算出来的。
回到unity中我们来做个具体的例子,如下图所示,我们将一个矩形沿着X轴旋转45°,注意观察此时矩形的8个顶点的Y方向和Z方向都发生了改变,但是X方向是没有变化。
所以如果旋转任意轴时在进行矩阵运算的时候,该轴的坐标是不发生变化的,另外两个轴发生变化。请大家在回忆一下矩阵乘法,如下图所示,有了这个公式我们就可以推导完整的localToWorldMatrix了。
如果3个角度都有变化那么3个矩阵需要相乘,但是由于矩阵不具备交换律所以必须按照Untiy的顺序来乘,也就是 Y*X*Z
如下图所示,我们设置XYZ对应的欧拉角,
2.32001 -0.85134 0.46985 8.00000
1.81207 1.43969 -0.34202 9.00000
-0.57789 1.09659 0.81380 10.00000
0.00000 0.00000 0.00000 1.00000
我们来推导,如下代码所示,最终的推导完全一致,需要注意的是矩阵相乘的顺序不满足交换律。
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
void Start()
{
float rotax = transform.rotation.eulerAngles.x * Mathf.Deg2Rad;
float rotay = transform.rotation.eulerAngles.y * Mathf.Deg2Rad;
float rotaz = transform.rotation.eulerAngles.z * Mathf.Deg2Rad;
//由于Mathf.Sin 和 Mathf.Cos需要的是弧度,所以欧拉角需要乘以Mathf.Deg2Rad
float sinx = Mathf.Sin(rotax);
float cosx = Mathf.Cos(rotax);
float siny = Mathf.Sin(rotay);
float cosy = Mathf.Cos(rotay);
float sinz = Mathf.Sin(rotaz);
float cosz = Mathf.Cos(rotaz);
//X轴旋转矩阵
Matrix4x4 rx = Matrix4x4.identity;
rx.m11 = cosx; rx.m12 = -sinx;
rx.m21 = sinx; rx.m22 = cosx;
//Y轴旋转矩阵
Matrix4x4 ry = Matrix4x4.identity;
ry.m00 = cosy; ry.m02 = siny;
ry.m20 = -siny; ry.m22 = cosy;
//Z轴旋转矩阵
Matrix4x4 rz = Matrix4x4.identity;
rz.m00 = cosz; rz.m01 = -sinz;
rz.m10 = sinz; rz.m11 = cosz;
//注意旋转矩阵相乘的顺序,必须满足 Y * X * Z
Matrix4x4 rall = ry * rx * rz;
//缩放矩阵
Matrix4x4 s = Matrix4x4.identity;
s.m00 = transform.localScale.x;
s.m11 = transform.localScale.y;
s.m22 = transform.localScale.z;
//位置矩阵
Matrix4x4 p = Matrix4x4.identity;
p.m03 = transform.position.x;
p.m13 = transform.position.y;
p.m23 = transform.position.z;
//注意矩阵相乘的顺序,必须满足 位置矩阵 * 旋转矩阵 * 缩放矩阵
Debug.Log(p*rall*s);
//两个输出结果是完全相等的
Debug.Log(transform.localToWorldMatrix);
}
}
最后在啰嗦一句,Mesh的合并也可以发生在运行时,如果是运行时进行的合并,一定要调用Mesh.UploadMeshData(true);表示将 mesh在.NET托管堆中的byte[]清空,不然内存就双份了。
- 本文固定链接: https://www.xuanyusong.com/archives/4620
- 转载请注明: 雨松MOMO于 雨松MOMO程序研究院 发表











MeshFilter[] meshFilters = Selection.activeTransform.GetComponentsInChildren(true);
这个数组获取的最大长度为什么只有19683?如果我子物体数量超过这个数字的话就获取不全了
自问自答了,自Unity 2019.2起,Unity添加了可以处理这种情况的新API ——TryGetComponent API。
TryGetComponent这个方法请问一下在哪定义的?