• 浅谈角色换装功能--Unity简单例子实现


      在前置篇中,基本上梳理了一下换装功能背后涉及到的美术工作流。但程序员嘛,功能终归是要落到代码上的。本文中会结合Unity提供的API及之前提到的内容来实现一个简单的换装功能。效果如下:
      

               (图1:最终效果展示)


     资源导出规则


     所有的换装实现都是和导出规则相对应的。先说一下我这个小例子的导出规则。

    1.角色的主干部分,包括头,胳膊,大腿。整体导出作为一个基础蒙皮。

    2.其他部分的蒙皮,手套,下装,衣服,头发。每一种样式都一个个单独导出。

    3.从MAX中导出FBX资源时,要注意导出蒙皮时候,骨骼也要选上,否则导出的就是普通mesh,而不是蒙皮了。

                    

              (图2:角色基础部分的导出内容,左侧为主干部分,右侧为一个头发部件.都要带上骨骼)            


    基本流程


     如图3,将max导出的所有fbx放入Unity后,每个部件都是单独的,我们要做的就是把这些分散的部件攒在一起,让他们正确的显示并响应动画。

            

    (图3:Unity中显示的所有导出身体部件,Girl为主干模型)

      写具体代码之前,我们先说一下几个关键的Unity组件,Animator,SkinnedMeshRenderer.Animator会读取动画信息,我们在前置篇提到,max只制作动画的关键帧,而游戏渲染是一帧一帧的,关键帧之间的动画如何过渡,就是引擎自己负责的,也就是Animator做的事,Animator计算好当前帧的骨骼姿态后。会根据结果去改变Animator组件所在节点下的骨骼结构节点,只有我们在max里将骨骼正确导出,才会出现这些节点。SkinnedMeshRenderer则负责蒙皮计算,在每一帧中根据Animator计算出来后的骨骼位置,找到自己关联了哪些骨骼及权重,然后进行变换和插值,计算出mesh顶点的正确位置。再将这些顶点信息传入对应的材质球中进行渲染。


     实现代码


    下面是一个简单的实现代码,我会对一些关键代码进行说明。这个脚本是挂在角色主干部分的Prefab上。

      1 public class SkinTest : MonoBehaviour 
      2 {
      3 
      4     public GameObject[] Hairs;
      5     public GameObject[] Clothes;
      6     public GameObject[] Gloves;
      7     public GameObject[] Unders;
      8     
      9     private int hairIndex = 0;
     10     private int clothesIndex = 0;
     11     private int glovesIndex = 0;
     12     private int underIndex = 0;
     13 
     14     private List<Transform> bones;
     15     private GameObject rootBone;
     16     void Start () 
     17     {
     18         rootBone = gameObject.transform.FindChild("Bip001").gameObject;
     19         bones = new List<Transform>();
     20 
     21         BuildPlayer();
     22     }
     23     
     24     public void BuildPlayer()
     25     {
     26         bones.Clear();
     27         List<CombineInstance> combineInstances = new List<CombineInstance>();
     28         List<Material> materials = new List<Material>();
     29         List<SkinnedMeshRenderer> smrList = new List<SkinnedMeshRenderer>();
     30         Transform[] transforms = rootBone.GetComponentsInChildren<Transform>(true);
     31 
     32         if(Hairs!=null && Hairs.Length > hairIndex && Hairs[hairIndex]!=null)
     33         {
     34             SkinnedMeshRenderer smr = Hairs[hairIndex].GetComponentInChildren<SkinnedMeshRenderer>();
     35             if (smr != null)
     36             {
     37                 smrList.Add(smr);
     38             }
     39         }
     40 
     41         if (Clothes != null && Clothes.Length > clothesIndex && Clothes[clothesIndex] != null)
     42         {
     43             SkinnedMeshRenderer smr = Clothes[clothesIndex].GetComponentInChildren<SkinnedMeshRenderer>();
     44             if (smr != null)
     45             {
     46                 smrList.Add(smr);
     47             }
     48         }
     49 
     50         if (Gloves != null && Gloves.Length > glovesIndex && Gloves[glovesIndex] != null)
     51         {
     52             SkinnedMeshRenderer smr = Gloves[glovesIndex].GetComponentInChildren<SkinnedMeshRenderer>();
     53             if (smr != null)
     54             {
     55                 smrList.Add(smr);
     56             }
     57         }
     58 
     59         if (Unders != null && Unders.Length > underIndex && Unders[underIndex] != null)
     60         {
     61             SkinnedMeshRenderer smr = Unders[underIndex].GetComponentInChildren<SkinnedMeshRenderer>();
     62             if (smr != null)
     63             {
     64                 smrList.Add(smr);
     65             }
     66         }
     67 
     68         for(int i =0;i<smrList.Count;i++)
     69         {
     70             SkinnedMeshRenderer smr = smrList[i];
     71             if(smr)
     72             {
     73                 for(int sub =0;sub<smr.sharedMesh.subMeshCount;sub++)
     74                 {
     75                     for (int j = 0; j < smr.bones.Length; j++)
     76                     {
     77                         for (int index = 0; index < transforms.Length; index++)
     78                         {
     79                             if (smr.bones[j].name.Equals(transforms[index].name))
     80                             {
     81                                 bones.Add(transforms[index]);
     82                                 break;
     83                             }
     84                         }
     85                     }
     86 
     87                     CombineInstance ci = new CombineInstance();
     88 
     89                     ci.mesh = smr.sharedMesh;
     90                     ci.subMeshIndex = sub;
     91                     combineInstances.Add(ci);
     92                }
     93                materials.AddRange(smr.sharedMaterials);
     94 
     95             }
     96         }
     97 
     98         SkinnedMeshRenderer oldSkin = GetComponent<SkinnedMeshRenderer>();
     99         if(oldSkin!=null)
    100         {
    101             GameObject.DestroyImmediate(oldSkin);
    102         }
    103 
    104         SkinnedMeshRenderer newSmr = gameObject.AddComponent<SkinnedMeshRenderer>();
    105         newSmr.sharedMesh = new Mesh();
    106         newSmr.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);
    107         newSmr.bones = bones.ToArray();
    108         newSmr.rootBone = rootBone.transform;
    109         newSmr.materials = materials.ToArray();
    110     }
    111 }

    4~15行,一些基本变量,存放用于换装的Prefab的引用,以及索引下标,bones用来存储Skin合并后的骨骼引用,rootBone用来存储根骨骼。

    18行,找到根骨的节点,此处的Bip001是3Dmax中Bip结构的默认根节点。主干部分的蒙皮导出时带有骨骼,所以可以在Prefab的子节点上找到。

    27~30行,建立一些List用来存储SkinMeshRenderer合并过程中所用到的一些中间内容。这里再提一下SkinMeshRenderer,我们会发现一个SkinMeshRenderer一般都只包含一个Material,但它是可以包含多个的。当我们的SkinMeshRenderer里对应的Mesh是包含多个subMesh的时候,那么他们需要多个材质球来对应每个SubMesh。我们导出的各个部件里都有自己的SkinMeshRenderer,我们要做的是把他们合为一个整体,这样做会对计算性能上有提升,逻辑处理上也更统一。后面我们再细说。

    32~66行,这部分是根据各个部位的索引号去配置好的Prefab数组中查找到对应配件,只获取SkinMeshRenderer组件就够了,因为他里面包含了我们所需的蒙皮的所有信息。把他们放到List中后面统一处理。

    68~96行,循环遍历处理我们前面获取的各个部件的SkinMeshRenderer.这里要说一下关于SkinMeshRenderer的Bones变量,它返回的是这个Skin绑定了哪些骨骼,Unity是以Transform引用数组的形式返回的,引用的是原来每个部件Prefab下自己的Bip下的骨骼节点,当我们把这些SkinMeshRenderer整合成一个的时候,就需要把引用重新指定成主体模型上的相应骨骼节点,这正是73~85行做的事。注意我这里根据部位里是否有多个subMesh来重复添加多次骨骼,这是必须的,而且顺序也是一定要保证的。在FBX上的Optimize Mesh选项可以解决这个问题,不过会引入其它问题,这里不展开了。每个部件Skin对应的材质球也都按顺序放到List中。89~91行的CombineInstance是Unity用来进行Mesh合并的一个数据结构,我们最终是需要把每个部件Skin对应的Mesh合并到一起,这里注意,合并到一起,并不一定是真的变成了一个Mesh,因为部件和部件之间的材质不一定完全一致,这时候的Mesh合并实际上只是一种逻辑上的合并,真正渲染时各个部件的Mesh顶点数据还是各走一个DrawCall。即使是这样,逻辑上的这种整合对于Unity的性能也是有好处的,这涉及到渲染层面节省顶点Buffer的问题,也涉及到提高Unity引擎一些自身逻辑效率的问题,这里不展开。subMeshIndex这个变量,对于普通的部件Skin里只包含一个subMesh,所以一般指定0,但有时候会包含多个,如果在max里部件本身就由多个材质构成,那么每个材质负责的Mesh部分到了Unity里就变成一个SubMesh了。93行我们把材质球也按顺序(顺序很重要),放到了List里,你也许会问为什么不合并呢?理论上如果所有部件用的都是统一材质,或者材质基本相似的话是可以通过合并贴图,重新赋值UV来让所有部件正真的合并在一起,只用一个Mesh。

    104~106行,我们最终要把所有分散的SkinMeshRenderer合并到一起,添加一个SkinnedMeshRenderer组件,但是这个组件的所有变量都是默认空的。所以105行我们给这个Renderer新建一个空的Mesh。106行通过CombineMesh来利用我们前面创建的CombineInstance数据把Mesh合并。这里说明一下后两个参数,第一个参数如果为true,则表示会把所有Mesh真的合并到一起,也就是合并之后subMeshCount为1。这一搬是与我前面提到的材质合并配合使用的。第三个参数为true的话我们需要给每个CombineInstance提供一个变换矩阵,在它们被合并之前,它们会先利用这个矩阵进行一次空间变换。

    107行,将前面骨骼节点集合传递给前面新建的SkinMeshRenderer,必须保证顺序。

    108行,rootBone习惯性的赋值为骨骼结构的根节点,这里设为空也没问题。

    109行,同骨骼节点一样集合一样,材质球集合传递给SkinMeshRenderer,保证顺序与部件合并的顺序相同。


    总结


      换装功能的实现代码并没有统一规范,这跟部件的设计规则有很大关系,所以本文只提供一种最简单基本的思路。还可以在这个基础上继续展开,深入优化。有些地方我没有深入去剖析,一笔带过。一方面是有些内容我也并不深入了解,另一方面是怕大家过于纠结细节,迷失方向。对于一些更深入的内容,我计划有时间再写一篇来分享。上面的实现在实际项目中也有很多问题,比如每个部件的Fbx导出都需要带全套骨骼。这造成一些数据上的冗余,如果要是在资源打包上依然没有办法去掉冗余的话,就会造成运行时内存的浪费,希望大家来一起讨论。   

      尊重他人智慧成果,若要转载,请注明作者esfog,原文地址http://www.cnblogs.com/Esfog/p/EquipChange_SimpleArchive.html

  • 相关阅读:
    postgresql string转decimal后进行排序
    postgresql 自增长ID跳过问题
    携程apollo配置中心服务端如何感知配置更新?
    MATLAB绘制B样条曲线
    BP神经网络
    三次B样条曲线拟合算法
    淘淘IDEA Mavne POM基本配置文件
    slf4j日志的简单用法
    idea测试web连接mysql数据库
    虚拟机如何设置外网ip
  • 原文地址:https://www.cnblogs.com/Esfog/p/EquipChange_SimpleArchive.html
Copyright © 2020-2023  润新知