首页 >Unity3D频道 >【Unity3D拓展编辑器】 > Unity3D研究院编辑器之合并mesh引发的矩阵推导运算(三十四)
2019
08-17

Unity3D研究院编辑器之合并mesh引发的矩阵推导运算(三十四)

合并mesh的功能比较常见,可以运行时合也可以编辑模式下提前合并,但是至少需要注意两点。

1.考虑lightmap,合并mesh以后会将lightmap的信息写入uv2,如果有多张lightmap需要传入正确的index,所以参与合批的mesh需要保证处于同一index的lightmap中。

2.如果地图需要切块,那么合并mesh需要考虑中心点的问题。

如下图所示,美术做场景可能是很随意的,完全可能让父节点和子节点相离的十分远,那么合并完以后中心点如果离的过远,是无法判断出当前所处的地图块的,所以合并完以后我们要让中心点居中。

Snip20190816_1

脚本的使用

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视图中,最重要的是中心点已经归于正中心了。

Snip20190816_2
需要注意的这句代码

                    //每个参与合批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

Snip20190816_3

如下图所示,对应的矩阵信息如下。上面代码矩阵改的m03 m13 m23就对应着Position的x,y,z。我们还能看到缩放信息对应存在m00 m11 m22中,好像挺简单的。

Snip20190816_7

但是旋转就比较麻烦了,先不管矩阵、四元数、欧拉角,完全放空。来我们先回味一下初中几何,如下图所示,在平面几何中,已知起始点A=(1,0) 半径r=1和旋转的角度a=45 求旋转到P点的坐标。

t2019

根据三角函数的原理,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)点。

 

t12019

所以最终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方向是没有变化。

Snip20190817_2

Snip20190817_3

所以如果旋转任意轴时在进行矩阵运算的时候,该轴的坐标是不发生变化的,另外两个轴发生变化。请大家在回忆一下矩阵乘法,如下图所示,有了这个公式我们就可以推导完整的localToWorldMatrix了。

Snip20190817_4

如果3个角度都有变化那么3个矩阵需要相乘,但是由于矩阵不具备交换律所以必须按照Untiy的顺序来乘,也就是 Y*X*Z

如下图所示,我们设置XYZ对应的欧拉角,

Snip20190817_5对应的矩阵内容如下

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[]清空,不然内存就双份了。

最后编辑:
作者:雨松MOMO
专注移动互联网,Unity3D游戏开发
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!

Unity3D研究院编辑器之合并mesh引发的矩阵推导运算(三十四)》有 3 条评论

  1. Armin君说:

    MeshFilter[] meshFilters = Selection.activeTransform.GetComponentsInChildren(true);
    这个数组获取的最大长度为什么只有19683?如果我子物体数量超过这个数字的话就获取不全了

  2. Armin君说:

    自问自答了,自Unity 2019.2起,Unity添加了可以处理这种情况的新API ——TryGetComponent API。

  3. Armin君说:

    TryGetComponent这个方法请问一下在哪定义的?

留下一个回复

你的email不会被公开。 必填项已用 * 标注。