Unity动作表情工具(编辑器模式下一边播动作一边播表情)
一直忙于学习技术和工作好久没写博客
这次分享一下我写的一个动作表情工具
先说一下需求:美术把一帧帧表情图导出来,一张张排好序号,然后放到编辑器里面打开一个工具界面可以选动作,同时切换对应的表情,在编辑器模式下播放动作和表情,还可以调整一下表情,最后可以保存数据放到游戏项目里面用
其实我是不喜欢这种,因为不共用,我喜欢的是左眼右眼嘴巴分开mesh,这样子每个部分复用率高,可以避免内存问题。不过分开3个mesh的话调表情的工作量会增大
还有其他实现方法例如使用blendshape,可以参考unity chan的demo
这里面涉及几个工具
- 模型prefab生成工具,包括animatorController(就是美术给的模型fbx和动作fbx)
- 表情图处理工具,包括几个表情合在一张大图里面(例如1-1,2-1,2-2,3-1,这里面一张图时间为0.2秒,前面序号相同的表示同一个表情,所以表情2时间为0.2*2,这样子要合成的图片就是1,2,3三张图片合成一张,同时把对应的数据导出来)
- 编辑器动作表情播放工具(表情处理工具导出一份数据出来的,打开这个工具直接读取数据,里面可以调整单个表情时间,添加表情,删除表情等等功能)
先给个大体流程看看
1.模型prefab生成工具
2.表情图处理
(1)原始美术给的表情图
(2)第一次处理表情,把表情筛选出来,生成这个动作表情数据
(3)第二次处理把所有的动作表情数据合并到总的表情数据里面,把筛选出来的图片合成大图
3.表情动作播放工具
规范
工具一般都要有规律,所以有些东西必须规范好
首先我这里规定好目录,在assets下创建Art文件夹,然后创建一个模型名字的文件夹
例如:
- 资源根目录:Assets/Art/模型名/
- 放模型资源目录:Assets/Art/模型名/Model/
- 表情资源根目录:Assets/Art/模型名/Expression/
- 动作表情目录:Assets/Art/模型名/Expression/动作名/
- 放未处理美术表情资源目录:Assets/Art/模型名/Expression/动作名/normal
- 处理过美术表情资源目录(动态创建):Assets/Art/模型名/Expression/动作名/deal
- 合成好的图片资源和表情数据(动态创建):Assets/Art/模型名/Result/
- 这个模型的所有表情数据(动态创建):Assets/Art/模型名/Result/模型名_Express.txt
- 合成的表情图的材质球路径:Assets/Art/ExpressionMaterial/
Ps:表情数据用json为了方便查看,我一般用用protobuf导出数据,因为protobuf比json速度快
这里我根据三个工具,分三个部分讲解
第一部分:模型animatorController,prefab生成工具
1.工具使用
(1)拿到美术给的资源,模型文件命名:模型名@model,动作命名:模型名@动作名
(2)选中model文件夹,右键处理模型,这里会自动生成模型名为名字的预设,还有一个挂上动画片段的animator Controller
2.工具讲解
先贴个代码,里面都有注释,我这里说一说流程
- 根据右键点击处理模型,获取选中的文件夹
- 根据选中的文件夹,首先在这个文件夹创建一个animatorController
使用的接口:AnimatorController.CreateAnimatorControllerAtPath
- 根据选中的文件夹,遍历所有.fbx文件或者.anim文件,以动作名创建一个状态加入animatorController第一个layer.stateMachine,然后这个状态的motion赋值动画片段
- 最后把模型实例到场景,然后赋值animator的animatorController,最后把场景这个模型保存为预设,把场景的模型删除
使用的接口:PrefabUtility.CreatePrefab
using System;
using UnityEngine;
using System.Collections;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Animations;
using System.Drawing;
using System.Collections.Generic;
using System.Web;/// <summary>
/// 动作控制器生成工具
/// </summary>
public class AnimatorTool : MonoBehaviour
{[MenuItem("Assets/处理模型", false)]static void DealAnimator(){//获取选中的目录路径UnityEngine.Object[] arr = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.Assets);string assetPath = AssetDatabase.GetAssetPath(arr[0]);string fullPath = EditorTool.GetFullAssetPath(assetPath);DirectoryInfo info = new DirectoryInfo(fullPath);if (info.Name != "Model"){return;}string folderName = info.Parent.Name;// 创建animationController文件AnimatorController aController = AnimatorController.CreateAnimatorControllerAtPath(string.Format("{0}/animation.controller", assetPath));// 得到其layervar layer = aController.layers[0];// 绑定动画文件AddStateTranstion(string.Format("{0}", assetPath), layer);// 创建预设GameObject go = LoadFbx(folderName, assetPath);if (null != go){PrefabUtility.CreatePrefab(string.Format("{0}/{1}.prefab", assetPath, folderName), go);DestroyImmediate(go);}}/// <summary>/// 添加动画状态机状态/// </summary>/// <param name="path"></param>/// <param name="layer"></param>private static void AddStateTranstion(string path, AnimatorControllerLayer layer){string[] paths = Directory.GetFiles(path, "*.fbx", SearchOption.AllDirectories);for (int i = 0; i < paths.Length; i++){string temp = paths[i].Replace('\\', '/');temp = temp.Substring(path.IndexOf("Assets/"));AnimatorStateMachine sm = layer.stateMachine;// 根据动画文件读取它的AnimationClip对象var datas = AssetDatabase.LoadAllAssetsAtPath(temp);if (datas.Length == 0){return;}// 遍历模型中包含的动画片段,将其加入状态机中foreach (var data in datas){if (!(data is AnimationClip))continue;var newClip = data as AnimationClip;if (newClip.name.StartsWith("__"))continue;// 取出动画名字,添加到state里面var state = sm.AddState(newClip.name);state.motion = newClip;}}//如果动画有处理过把fbx删掉只剩anim文件,就走这里string[] ainPaths = Directory.GetFiles(path, "*.anim", SearchOption.AllDirectories);for (int i = 0; i < ainPaths.Length; i++){string temp = ainPaths[i].Replace('\\', '/');temp = temp.Substring(temp.IndexOf("Assets/"));AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(temp);AnimatorStateMachine sm = layer.stateMachine;var state = sm.AddState(clip.name);state.motion = clip;}}/// <summary>/// 生成带动画控制器的对象/// </summary>/// <param name="name"></param>/// <returns></returns>public static GameObject LoadFbx(string name, string assetPath){UnityEngine.Object objr = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath + "/" + name + "@model.FBX");if (null == objr){return null;}var obj = Instantiate(objr) as GameObject;obj.GetComponent<Animator>().runtimeAnimatorController = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(assetPath + "/animation.controller");return obj;}
}
第二部分表情图处理工具
1.数据类
- OneExpressionsData:单个”表情数据“,数据包括:使用那个贴图,这个贴图对应的材质球偏移位置,表情停留时间
- ExpressionsData:单个“动作表情数据”,数据包括:”表情数据“列表,动作名(用于区分表情)
- Animator2Expression:”所有动作表情数据“,数据包括:“动作表情数据”列表,uv名字(用于查找render)
/// <summary>
/// 所有动作表情数据
/// </summary>
public class Animator2Expression
{/// <summary>/// uv名字/// </summary>public string UVName;/// <summary>/// 所有动作表情数据/// </summary>public List<ExpressionsData> AnimatorExpressionList = new List<ExpressionsData>();public int row;public int column;
}/// <summary>
/// 单个动作表情数据
/// </summary>
public class ExpressionsData
{/// <summary>/// 动作名/// </summary>public string animationName;/// <summary>/// 所有表情数据/// </summary>public List<OneExpressionsData> list = new List<OneExpressionsData>();public bool AddTime(int index){for (int i = 0; i < list.Count; i++){if (index == list[i].index){list[i].waitTime += 0.2d;System.Math.Round(list[i].waitTime, 3);return false;}}OneExpressionsData temp = new OneExpressionsData(index, System.Math.Round(0.2d, 3), GameDef.ExpressionRow, GameDef.ExpressionColumn);list.Add(temp);return true;}
}/// <summary>
/// 单帧表情数据
/// </summary>
public class OneExpressionsData
{/// <summary>/// 使用的图片名(用于读取材质球)/// </summary>public string UseImageName;/// <summary>/// 索引用于生成图片用/// </summary>public int index;/// <summary>/// 表情等待时间/// </summary>public double waitTime;/// <summary>/// 材质球截取x大小/// </summary>public double TilingX;/// <summary>/// 材质球截取y大小/// </summary>public double TilingY;/// <summary>/// 材质球x偏移/// </summary>public double OffestX;/// <summary>/// 材质球y偏移/// </summary>public double OffestY;public OneExpressionsData() { }public OneExpressionsData(int index, double time, float RowNum, float ColumnNum){this.index = index;waitTime = time;TilingX = System.Math.Round(1.0d / ColumnNum, 3);TilingY = System.Math.Round(1.0d / RowNum, 3);}/// <summary>/// 根据所在图片索引计算位置信息/// </summary>/// <param name="ImageIndex"></param>public void SetImageIndex(int ImageIndex){this.index = ImageIndex;int ColumnIndex = ImageIndex / GameDef.ExpressionColumn;int RowIndex = ImageIndex % GameDef.ExpressionRow;SetIndexPos(RowIndex, ColumnIndex);}/// <summary>/// 设置所用的表情图/// </summary>/// <param name="name"></param>public void SetUseImageName(string name){UseImageName = name;}/// <summary>/// 计算材质球位置/// </summary>/// <param name="RowIndex"></param>/// <param name="ColumnIndex"></param>public void SetIndexPos(int RowIndex, int ColumnIndex){OffestX = System.Math.Round(ColumnIndex * TilingX, 3);OffestY = System.Math.Round(-TilingY * (RowIndex + 1), 3);}
}
2.图片处理工具使用
(1)现在以模型CZ-75,动作ShowTouchBody为例,把这些资源放到Assets/Art/CZ-75/Expression/ShowTouchBody/normal路径下
PS:资源放的路径可以看上面的规范
(2)选中Assets/Art/CZ-75/Expression/ShowTouchBody文件夹,然后右键->处理表情,如下图
(3)处理流程
- 筛选图片
- 根据筛选出来的图片合成大图,把对应的表情数据导出来ExpressionsData
- 把这个动作数据整合到Animator2Expression
(4)处理完毕之后
Assets/Art/CZ-75/Expression/ShowTouchBody/deal/这里面是筛选出来的图片和该动作的表情数据(ExpressionsData)
Assets/Art/CZ-75/Result/这里面是合成的图片和所有动作表情数据(Animator2Expression)
游戏里面只用到result里面的文件
PS:可以Assets/Art/CZ-75/选中文件夹右键->整个所有表情,把没有处理的表情全部处理(deal文件夹没有info.txt认为没有处理)
3工具讲解
(1)筛选图片和处理表情数据
我合成图片的索引从左上角开始,先从上到下,在左到右,
然后根据索引计算材质球偏移位置
OneExpressionsData数据类部分代码
public OneExpressionsData(int index, double time, float RowNum, float ColumnNum){this.index = index;waitTime = time;TilingX = System.Math.Round(1.0d / ColumnNum, 3);TilingY = System.Math.Round(1.0d / RowNum, 3);}/// <summary>/// 计算材质球位置/// </summary>/// <param name="RowIndex"></param>/// <param name="ColumnIndex"></param>public void SetIndexPos(int RowIndex, int ColumnIndex){OffestX = System.Math.Round(ColumnIndex * TilingX, 3);OffestY = System.Math.Round(-TilingY * (RowIndex + 1), 3);}
该动作的表情数据处理,都是遍历文件夹里面图片
(例如1-1,2-1,2-2,3-1,这里面一张图时间为0.2秒,前面序号相同的表示同一个表情,所以表情2时间为0.2*2,这样子要合成的图片就是1,2,3三张图片合成一张)
/// <summary>/// 单独处理一个文件夹图片/// </summary>/// <param name="fullPath"></param>/// <param name="updateRootData"></param>static void DealOneAnimatorExpression(string fullPath, bool updateRootData = false){DirectoryInfo mDirectoryInfo = new DirectoryInfo(fullPath);DirectoryInfo mRootDirctoryInfo = mDirectoryInfo.Parent.Parent;if (mDirectoryInfo.Parent.Name != "Expression"){return;}string expressionName = mRootDirctoryInfo.Name + mDirectoryInfo.Name;//合成图片文件夹string outPutPath = mRootDirctoryInfo.ToString() + "/Result/";string[] paths = Directory.GetFiles(fullPath + "/normal/", "*.png", SearchOption.AllDirectories);string dirPath = fullPath + "/deal/";EditorTool.DeleteDirectory(dirPath);EditorTool.InitDirectory(dirPath);EditorTool.InitDirectory(outPutPath);List<string> ppp = new List<string>(paths);ppp.Sort((a, b) =>{string ac = Path.GetFileName(a);string bc = Path.GetFileName(b);return int.Parse(ac.Split('-')[0]).CompareTo(int.Parse(bc.Split('-')[0]));});ExpressionsData data = new ExpressionsData();//动作名字以文件夹命名data.animationName = mDirectoryInfo.Name;//遍历图片设置相同图片时间for (int i = 0; i < ppp.Count; i++){string ac = Path.GetFileName(ppp[i]);int tempIndex = int.Parse(ac.Split('-')[0]);if (data.AddTime(tempIndex)){//把相同图片的一张图片放到deal文件夹File.Copy(ppp[i], dirPath + tempIndex.ToString() + ".png", true);}}//重新设置图片索引int lie = -1;int MergeImageIndex = -1;//遍历“动作表情”里面所有“表情数据”for (int i = 0; i < data.list.Count; i++){OneExpressionsData mOneExpressionsData = data.list[i];if ((i) % GameDef.ExpressionColumn == 0){lie++;}if (i % GameDef.ImageNum == 0){MergeImageIndex++;}//重新设置所有mOneExpressionsData.index = i;//设置使用的图片(合成之后的)mOneExpressionsData.SetUseImageName(expressionName + MergeImageIndex);//计算材质球偏移位置mOneExpressionsData.SetIndexPos(i % GameDef.ExpressionRow, lie % GameDef.ExpressionColumn);}string s = JsonMapper.ToJson(data);//把动作表情数据导出json到deal文件夹EditorTool.SaveJosnFile(s, dirPath + "Info.txt");AssetDatabase.Refresh();//合成图片MergeImage(dirPath, outPutPath, expressionName);if (updateRootData){ConformData(mRootDirctoryInfo.ToString());}}
(2)图片合成我们需要用到将System.Drawing引入Unity项目中
在Unity的安装路径中找到System.Drawing.dll,将其复制到我们的项目文件夹
System.Drawing.dll的具体位置:%Unity根目录%\Editor\Data\Mono\lib\mono\2.0\System.Drawing.dll
(3)多张小图合成一张大图工具代码
/*** Author: YinPeiQuan
**/using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Web;/// <summary>
/// 合成的图片顺序
/// </summary>
public enum SortType
{/// <summary>/// 左上角开始,从左到右,从上到下/// </summary>width,/// <summary>/// 左上角开始,从上到下,从左到右/// </summary>height,
}public class ImageMergeHelper
{/// <summary>/// 将多张图片拼接合并成一张指定大小的图片,各图像进行顺序排列/// </summary>/// <param name="height">新图像的高度</param>/// <param name="width">新图像的宽度</param>/// <param name="bw">图像间距</param>/// <param name="noimgtext">无图片时显示的文字,为空默认为:暂无图片</param>/// <param name="imgs">图像数组</param>/// <returns></returns>public static Image ImgMerge(int height, int width, int bw, SortType mtype , params Image[] imgs){Image ret = new System.Drawing.Bitmap(width, height);Graphics g = Graphics.FromImage(ret);//这里设置透明底g.Clear(Color.Empty);//新图像组合的图像个数int cnt = GameDef.ExpressionRow * GameDef.ExpressionColumn;imgs = imgs.Take<Image>(cnt).ToArray();//求新列表维数int rat = Convert.ToInt16(Math.Sqrt(cnt));if (rat > 0){//图片宽高度不能小于2像素if ((rat + 1) * bw + 2 * rat > width) bw = (width - 2 * rat) / (rat + 1);int th = (height - 2 * rat) / (rat + 1);if (th < bw){//相对高度计算出来的间距,取小不取大,这样图像宽度显示更大一些bw = th;}if (bw <= 0) bw = 1; //防止意外//计算排列图片的尺寸int swidth = (width - (rat + 1) * bw) / rat;int sheight = (height - (rat + 1) * bw) / rat;//依次排列图片int hs = 1; //行数int ls = 1; //列数for (int i = 1; i <= imgs.Length; i++){Rectangle r = new Rectangle(){Height = sheight,Width = swidth,X = bw * ls + swidth * (ls - 1),Y = bw * hs + sheight * (hs - 1)};g.DrawImage(imgs[i - 1], r);//处理完后下一个位置输出if(mtype == SortType.width){ls++;if (i % rat == 0){hs++;ls = 1;}}else if(mtype == SortType.height){hs++;if (i % rat == 0){ls++;hs = 1;}}}GC.Collect();}return ret;}
}
第三部分编辑器播放动作和表情
1.工具使用
(1)把之前生成prefab拖到场景里面
(2)在场景中选中预设,Inspector视图如下图,点击按钮”打开动作表情工具“
(3)把uv拖动工具的uv那里(如果没有uv是播放不了表情)
(4)要播放动作首先点”锁定模型“那个按钮,然后就可以拖拽播放或者点右上角的播放
(5)之后就可以编辑表情数据,都是中文应该都会用
PS:美术经过我调教都会,程序员应该问题不大
2.工具讲解
工具代码AnimatorAndExpressionPlayTool
首先这个代码有点长,我只讲怎么实现编辑器下播放动作,和表情怎么播放
(1)编辑器模式Update
EditorApplication.update += inspectorUpdate;
inspectorUpdate是工具一个方法用于执行update的东西
每帧时间间隔可以使用EditorApplication.timeSinceStartup来记录时间间隔
(2)播放动作接口,m_RunningTime运行的时间
animator.playbackTime = m_RunningTime;
(3)表情图播放
根据m_RunningTime计算当前播放到哪一个表情图
设置材质球偏移位置
m.SetTextureOffset("_MainTex", new Vector2((float)temp.OffestX, (float)temp.OffestY));
m.SetTextureScale("_MainTex", new Vector2((float)temp.TilingX, (float)temp.TilingY));
public void PlayExpression(float time){double tempTime = 0;if (null != m_CurrentData){for (int i=0;i<m_CurrentData.list.Count;i++){OneExpressionsData temp = m_CurrentData.list[i];tempTime += temp.waitTime;if(time < tempTime){PlayIndex = i;Material m = AssetDatabase.LoadAssetAtPath<Material>("Assets/Art/ExpressionMaterial/" + temp .UseImageName + ".mat");if(null != m){m.SetTextureOffset("_MainTex", new Vector2((float)temp.OffestX, (float)temp.OffestY));m.SetTextureScale("_MainTex", new Vector2((float)temp.TilingX, (float)temp.TilingY));}if(null != UVObj){(UVObj as GameObject).GetComponent<Renderer>().material = m;}break;}}}}
最后下载地址,本工具写于unity5.6.3f版本
链接:https://pan.baidu.com/s/1LzwErh5Pe03VMfqDCT6Bbg 密码:cgs4
Unity动作表情工具(编辑器模式下一边播动作一边播表情)相关推荐
- Unity Editor - 在编辑器模式下执行exe程序、打开文件、定位脚本行
执行exe程序.打开文件 当我们想在Unity 编辑器下 通过exe程序打开指定脚本时我们可以调用以下API 这里我是通过vscode 打开 lua 脚本 并定位到具体某一行. static void ...
- UNITY编辑器模式下static变量的坑
UNITY编辑器模式下static变量的坑 在unity中写编辑器扩展工具,如在编辑器中加个菜单,点击这个菜单项时执行打包功能. 类如下,其中的静态变量,如果每次进来不清空,则LIST会越来越大,打包 ...
- 实现Unity编辑器模式下的旋转
最近在做一个模型展示的项目,我的想法是根据滑动屏幕的x方向差值和Y方向的差值,来根据世界坐标下的X轴和Y轴进行旋转,但是实习时候总是有一些卡顿.在观察unity编辑器下的旋转之后,发现编辑器下的旋转非 ...
- uniapp开发微信小程序APPID的获取,微信开发者工具游客模式下,调用 wx.operateWXData 是受限的
1.去微信公众平台注册账号 链接:小程序https://mp.weixin.qq.com/wxopen/waregister?action=step12.注册完账号去开发->开发管理->开 ...
- Unity3d编辑器模式下创建和替换Prefab
最近在项目中需要开发一套地图数据生成编辑器,记录自己在这个过程中使用的一些好用的创建和替换Prefab的方法. PrefabUtility.CreatePrefab(localPath,obj) 这个 ...
- Unity中实现Scene模式下的鼠标操作效果
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Camera ...
- Unity 在编辑模式下退出游戏
实现在编辑器环境下退出编辑器,在非编译器环境下退出游戏 我们需要判断当前是否在编辑器环境中,在unity中是使用预处理的方式判断当前的环境,常用的预处理标识符如下: 标识符 解释 UNITY_EDIT ...
- Unity 编辑器环境下加载任意处资源 Resources.LoadAssetAtPath方法
1.Resources.LoadAssetAtPath 在Unity3D中如何实现动态加载资源的方法,就是把资源放在Resources目录下,使用Resources.Load方法即可动态加载资源. 但 ...
- 【Unity】AVPro使用踩坑,编辑器模式使用视频播放正常,打包后视频无法播放的问题
编辑器模式使用视频播放正常,打包后视频无法播放的问题 这个主要是AVPro的坑 一般使用会直接Browse给取文件路径,然后面板上面就能看到视频文件的名字,这个方法在编辑器模式下播放是可以获取到文件的 ...
最新文章
- C和指针 (pointers on C)——第七章:函数(上)
- 在本机上安装zabbix,来监控服务器 六
- SQL数据库对象的修改
- ubuntu下面使用clion
- linux的备份命令及其参数,linux cpio命令参数及用法详解--linux备份文件命令
- java开发者工具开源版_开源工具如何帮助飓风受害者
- Windows Server 2003群集配置手记(转载)
- 最速下降法求解步骤及例题
- mbot机器人编程课件_mbot机器人教程创客大赛
- Employee类的层级结构
- DeepFool: a simple and accurate method to fool deep neural networks
- 青龙自动薅羊毛—【万年历】秒到
- 英语句子摘抄——书虫系列
- 在设备后台安装CAB而不让用户发觉
- poj 计算几何 分类
- MathType完美兼容Word 2019 最详细的安装配置教程转载
- 斐讯n1 linux升级内核,斐讯N1盒子OpenWRT固件升级全记录
- Revit“原点”、“中心”、“测量点”在哪里?
- 小米4 刷入魔趣教程
- NLP实战:面向中文电子病历的命名实体识别