SkinnedMesh原理及一些应用
来源:互联网 发布:马上6是什么软件 编辑:程序博客网 时间:2024/05/22 00:20
人类在运动的时候,实际是骨骼在不断变换位置,然后骨骼带动全身皮肉在运动。在游戏中就反映为骨骼节点带动Mesh中的Vertices在运动,进而播放Mesh动画。这里以Unity工程为工具,来理解一下骨骼和蒙皮的过程。
骨骼
骨骼决定了模型整体在世界坐标系中的位置和朝向,和人类骨骼一样,你的拇指关节移动,你拇指附近的肌肉也会跟着移动。骨骼是有着层次结构的:
body上绑定了SkinnedMeshRenderer,指向的根节点为Pelvis。从这个根节点往下是一个树状的层次结构,有Spine,Thigh等子孙节点,这些就是骨骼。
现在我的SkinnedMeshRenderer是挂在body上的,意味着这就是这个mesh的原点,我的vertices都是基于这个原点的。为了简化问题,假设这个body点恰好就是坐标原点,旋转什么的也都和世界坐标原点一模一样。现在想象在坐标原点聚集着一堆顶点,这些顶点加上skin构成了一个生物模型。
如果我这时旋转body节点呢
当我旋转body节点的时候,因为body的世界旋转变了,而其内部Vertices相对于body的位置又不变,那理论上这个模型应该是会跟随body旋转才对。然后并不是。。。
模型的位置和旋转只会和骨骼有关,我的Pelvis节点并未动过,那我看到的模型也不会变化。那要实现这样的效果的话,我们只能去修改mesh的Vertices的位置,给他们做相应变换。
换句话说,当body旋转时,Vertices相对于骨骼的位置没有改变。我们可以计算出这个变换矩阵,然后反过来去求body旋转之后改变的Vertices。
骨骼计算的过程
1.mesh中的vertices原始局部坐标通过BoneOffsetMatrix转换为BoneSpace中的坐标。(以某个骨骼为原点的坐标系)
先从mesh的local空间转换到世界空间,再从世界空间转换到骨骼空间。
2.根据Vertices在BoneSpace中的坐标计算出世界坐标
只需要将BoneSpace中的局部坐标乘以一个bone.localtoworldmatrix即可。unity中支持最多四根骨骼来控制一个Vertice,每根骨骼根据Vertice在BoneSpace中的坐标算出一个Vertice的世界坐标,然后加权平均。
蒙皮
Vertices位置确定了之后,一张mesh贴上去就是蒙皮了。。。
实战
将骨骼动画渲在UI Canvas下面。
有一个需求是将3D模型渲染到UI层,选择的做法是将3D模型的skinnedmeshrenderer里的mesh信息取出来,再用CanvasRenderer来渲染,这样的一个好处是可以利用UI本身的层级顺序。
将前面的body作为待拷贝的skinnedmeshrenderer,这里旋转body不会改变原有skinnedmeshrenderer所在模型的旋转,但是却旋转了ui下的canvasrenderer渲出的模型。这是因为,旋转body时,改变了mesh中vertices的local坐标,但是canvas下的节点没有设置旋转,导致产生看到的现象。
using System.Collections;using System.Collections.Generic;using UnityEngine;public class CopyMeshToUi : MonoBehaviour{ public SkinnedMeshRenderer SkinnedMeshRenderer; private Material _mat; private CanvasRenderer _canvasRenderer; private Mesh _mesh; void Start() { _mesh = new Mesh(); _mat = SkinnedMeshRenderer.material; GameObject obj = new GameObject("Render3D"); obj.transform.parent = transform; obj.transform.localPosition = Vector3.zero; RectTransform rectTransform = obj.AddComponent<RectTransform>(); rectTransform.SetAsFirstSibling(); _canvasRenderer = obj.AddComponent<CanvasRenderer>(); obj.transform.rotation = SkinnedMeshRenderer.transform.rotation; obj.transform.localScale = (SkinnedMeshRenderer.transform.lossyScale.x / transform.parent.lossyScale.x)* Vector3.one; } void Update() { SkinnedMeshRenderer.BakeMesh(_mesh); _mesh.RecalculateBounds(); _canvasRenderer.Clear(); _canvasRenderer.SetMaterial(_mat,null); _canvasRenderer.SetMesh(_mesh); }}
人物换装
简单的换装包括更改材质,或者更改模型(如武器)
还有一种换装是基于相同骨骼,替换身体的某些部位,比如替换Head。原理也比较简单,取出新的Head的Mesh放在目标模型中,然后将这个Mesh绑定到目标模型的骨骼即可。
public Transform target; // 目标的模型,要求其骨骼已经存在public Transform source; // 源模型,所有的可以替换的部件都在这上面// 模型资源,对应上面的sourceDictionary<string, Dictionary<string, Transform>> data = new Dictionary<string, Dictionary<string, Transform>>();// 目标骨架,对应上面的targetTransform[] hips;// 目标皮肤,替换这里面的内容就行了Dictionary<string, SkinnedMeshRenderer> targetSmr = new Dictionary<string, SkinnedMeshRenderer>();// 初始化时的皮肤string[,] avatarStr = new string[,] { { "coat", "003" }, { "hair", "003" }, { "pant", "003" }, { "hand", "003" }, { "foot", "003" }, { "head", "003" } };// Use this for initializationvoid Start(){ // 获取资源的所有皮肤 SkinnedMeshRenderer[] parts = source.GetComponentsInChildren<SkinnedMeshRenderer>(); // 初始化data,将资源添加到Dictionary容器中 foreach (SkinnedMeshRenderer part in parts) { string[] partName = part.name.Split('-'); if (!data.ContainsKey(partName[0])) { data.Add(partName[0], new Dictionary<string, Transform>()); // 初始化targetSmr,添加骨架上的皮肤类,但当前的皮肤类内容是空的 GameObject partObj = new GameObject(); partObj.name = partName[0]; partObj.transform.parent = target; partObj.transform.localPosition = part.transform.localPosition; partObj.transform.localRotation = part.transform.localRotation; targetSmr.Add(partName[0], partObj.AddComponent<SkinnedMeshRenderer>()); } data[partName[0]].Add(partName[1], part.transform); } // 初始化hips,获取所有的骨骼,在Unity已经添加 hips = target.GetComponentsInChildren<Transform>(); // 初始化皮肤 int length = avatarStr.GetLength(0); for (int i = 0; i < length; ++i) { changeMesh(avatarStr[i, 0], avatarStr[i, 1]); }}// 改变部件public void changeMesh(string part, string item){ SkinnedMeshRenderer smr = data[part][item].GetComponent<SkinnedMeshRenderer>(); //获取当前要替换的皮肤,这是源 // 获取target上与source对应的骨骼,这边千万不能直接把骨骼赋值进去了 List<Transform> bones = new List<Transform>(); foreach (Transform bone in smr.bones) { foreach (Transform hip in hips) { if (hip.name != bone.name) { continue; } bones.Add(hip); break; } } // 这边是目标,进行替换 targetSmr[part].sharedMesh = smr.sharedMesh; //替换皮肤 targetSmr[part].bones = bones.ToArray(); //替换骨骼 targetSmr[part].materials = smr.materials; //替换材质}
看到自己参与的项目中换装直接就是更换Prefab,Prefab中自带mesh和骨骼,简单粗暴有点费。
- SkinnedMesh原理及一些应用
- 骨骼蒙皮动画(SkinnedMesh)的原理解析(一)
- 骨骼蒙皮动画(SkinnedMesh)的原理解析(一)
- 骨骼蒙皮动画(SkinnedMesh)的原理解析(一)
- 骨骼蒙皮动画(SkinnedMesh)的原理解析(一)
- 数据库原理及应用
- 数据库原理及应用
- 数据库原理及应用
- NAT原理及应用
- Ajax原理及应用
- MEPG2原理及应用
- Ajax原理及应用
- FFT原理及应用
- COM原理及应用
- LPC11C14GPIO原理及应用
- LPC11C14GPIO原理及应用
- ping原理及应用
- Filter原理及应用
- [Leetcode]Count The Repetitions
- Vim 文本编辑器_及使用技巧
- 强联通分量的KOSARAJU算法
- Machine Learning第九讲[推荐系统] --(一)基于内容的推荐系统
- Win7 开发WCF时 提示 进程不具有此命名空间的访问权限
- SkinnedMesh原理及一些应用
- Kibana实践
- 链接收藏
- request用getQueryString()获取参数中文转码问题
- threejs 鼠标移动控制模型旋转
- 系统温习Java
- redis集群JedisCluster优化
- IEA和API两盆冷水浇灭油价涨势今晚EIA会是多头救星
- 怎么把图片转换成ico格式