一直忙于学习技术和工作好久没写博客

这次分享一下我写的一个动作表情工具

先说一下需求:美术把一帧帧表情图导出来,一张张排好序号,然后放到编辑器里面打开一个工具界面可以选动作,同时切换对应的表情,在编辑器模式下播放动作和表情,还可以调整一下表情,最后可以保存数据放到游戏项目里面用

其实我是不喜欢这种,因为不共用,我喜欢的是左眼右眼嘴巴分开mesh,这样子每个部分复用率高,可以避免内存问题。不过分开3个mesh的话调表情的工作量会增大

还有其他实现方法例如使用blendshape,可以参考unity chan的demo

这里面涉及几个工具

  1. 模型prefab生成工具,包括animatorController(就是美术给的模型fbx和动作fbx)
  2. 表情图处理工具,包括几个表情合在一张大图里面(例如1-1,2-1,2-2,3-1,这里面一张图时间为0.2秒,前面序号相同的表示同一个表情,所以表情2时间为0.2*2,这样子要合成的图片就是1,2,3三张图片合成一张,同时把对应的数据导出来)
  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动作表情工具(编辑器模式下一边播动作一边播表情)相关推荐

  1. Unity Editor - 在编辑器模式下执行exe程序、打开文件、定位脚本行

    执行exe程序.打开文件 当我们想在Unity 编辑器下 通过exe程序打开指定脚本时我们可以调用以下API 这里我是通过vscode 打开 lua 脚本 并定位到具体某一行. static void ...

  2. UNITY编辑器模式下static变量的坑

    UNITY编辑器模式下static变量的坑 在unity中写编辑器扩展工具,如在编辑器中加个菜单,点击这个菜单项时执行打包功能. 类如下,其中的静态变量,如果每次进来不清空,则LIST会越来越大,打包 ...

  3. 实现Unity编辑器模式下的旋转

    最近在做一个模型展示的项目,我的想法是根据滑动屏幕的x方向差值和Y方向的差值,来根据世界坐标下的X轴和Y轴进行旋转,但是实习时候总是有一些卡顿.在观察unity编辑器下的旋转之后,发现编辑器下的旋转非 ...

  4. uniapp开发微信小程序APPID的获取,微信开发者工具游客模式下,调用 wx.operateWXData 是受限的

    1.去微信公众平台注册账号 链接:小程序https://mp.weixin.qq.com/wxopen/waregister?action=step12.注册完账号去开发->开发管理->开 ...

  5. Unity3d编辑器模式下创建和替换Prefab

    最近在项目中需要开发一套地图数据生成编辑器,记录自己在这个过程中使用的一些好用的创建和替换Prefab的方法. PrefabUtility.CreatePrefab(localPath,obj) 这个 ...

  6. Unity中实现Scene模式下的鼠标操作效果

    using System.Collections; using System.Collections.Generic; using UnityEngine;   public class Camera ...

  7. Unity 在编辑模式下退出游戏

    实现在编辑器环境下退出编辑器,在非编译器环境下退出游戏 我们需要判断当前是否在编辑器环境中,在unity中是使用预处理的方式判断当前的环境,常用的预处理标识符如下: 标识符 解释 UNITY_EDIT ...

  8. Unity 编辑器环境下加载任意处资源 Resources.LoadAssetAtPath方法

    1.Resources.LoadAssetAtPath 在Unity3D中如何实现动态加载资源的方法,就是把资源放在Resources目录下,使用Resources.Load方法即可动态加载资源. 但 ...

  9. 【Unity】AVPro使用踩坑,编辑器模式使用视频播放正常,打包后视频无法播放的问题

    编辑器模式使用视频播放正常,打包后视频无法播放的问题 这个主要是AVPro的坑 一般使用会直接Browse给取文件路径,然后面板上面就能看到视频文件的名字,这个方法在编辑器模式下播放是可以获取到文件的 ...

最新文章

  1. C和指针 (pointers on C)——第七章:函数(上)
  2. 在本机上安装zabbix,来监控服务器 六
  3. SQL数据库对象的修改
  4. ubuntu下面使用clion
  5. linux的备份命令及其参数,linux cpio命令参数及用法详解--linux备份文件命令
  6. java开发者工具开源版_开源工具如何帮助飓风受害者
  7. Windows Server 2003群集配置手记(转载)
  8. 最速下降法求解步骤及例题
  9. mbot机器人编程课件_mbot机器人教程创客大赛
  10. Employee类的层级结构
  11. DeepFool: a simple and accurate method to fool deep neural networks
  12. 青龙自动薅羊毛—【万年历】秒到
  13. 英语句子摘抄——书虫系列
  14. 在设备后台安装CAB而不让用户发觉
  15. poj 计算几何 分类
  16. MathType完美兼容Word 2019 最详细的安装配置教程转载
  17. 斐讯n1 linux升级内核,斐讯N1盒子OpenWRT固件升级全记录
  18. Revit“原点”、“中心”、“测量点”在哪里?
  19. 小米4 刷入魔趣教程
  20. NLP实战:面向中文电子病历的命名实体识别

热门文章

  1. FineReport安装教程
  2. python爬虫获取天猫店经营者资质证书(更新到2020.06.13
  3. Linux背景知识(1)RedHat和Centos
  4. 电解电容规格书中寿命才2000小时,够用吗?
  5. linux 安装 qq (非国际版)
  6. 基于Crawler4j的WEB爬虫
  7. 天龙服务器端修改,天龙一键端怎么架云服务器
  8. Java基础篇之什么是BufferedReader
  9. Lonely Christmas
  10. 【开源电路】STM32F103VCT6开发板