模型的制作过程
1. 建模
用三角面组装拼凑成型。
2. 展UV
将3D物体平铺到2D图上,2D图上的每个点都会与模型上点对应。
3. 材质和纹理贴图
纹理:一张2D图片
贴图:把纹理通过UV坐标映射到3D物体表面
纹理贴图:模型的颜色信息、UV信息等
材质:模型的表现,通过纹理贴图提供信息,使用不同的着色器算法,呈现出不同的表现效果,比如:金属、塑料、玻璃、透明等等。
4. 骨骼绑定
模型制作完成后,为了让模型动起来,首先要进行骨骼绑定。
骨骼绑定:为模型定义骨骼信息,定义骨骼控制哪些网格信息。
5. 动画制作
骨骼绑定后,可以利用这些骨骼的旋转来制作3D动画。
在一条时间轴上,制作关键帧的位置,通过一些规则决定从上一帧到下一帧的变化应该如何过渡,通过不断的制作关键帧就可以制作最终的动画效果。
## 模型导入概述
Unity支持很多模型格式(.fbx、.dae、.3ds、.dxf、.obj等等)
99%的模型都不是在Unity中制作的,都是美术人员在建模软件中制作
当他们制作完模型后,虽然Unity支持很多模型格式,但是官方建议是将模型在建模软件中导出为FBX格式后再使用。
> 👀️ 导出模型的注意事项
>
> 1. [参考官网](https://docs.unity.cn/cn/2019.4/Manual/CreatingDCCAssets.html)
> 2. 坐标轴,人物面朝向为Z轴正方向,Y轴正方向为头顶方向,X轴正方向为人物右侧
## Model页签
### Scene场景参数
Scale Factor:当模型的比例不符合项目的预期比例时,可以修改此值来改变模型的全局比例。Unity的物理系统希望游戏世界的1米在导入模型文件中为1个单位。
Convert Units:启用可将模型文件中定义的模型比例转换为Unity的比例。不同的比例格式不一样
* .fbx、.max、.jas = 0.01
* .3ds = 0.1
* .mb、.ma、.lxo、.dxf、.blend、.dae = 1
Preserve Hierarchy:始终创建一个显示预制体根。通常在导入的时候,FBX会将模型中的空根节点进行优化去掉它。但是如果多个FBX文件中包含同一层级的空根对象,可以勾选它来保留他们。
主要作用:比如有两个fbx文件,1:包含骨骼和网格 2:只包含骨骼动画。如果不启动它,导入2的时候,Unity将剥离根节点,会让层级不匹配,让动画不能正常播放。
### Meshes网格参数
Mesh Compression:网格压缩,设置压缩比会减少网格的文件大小。提高压缩比会降低网格的精度。
跳转此参数可以优化游戏包的大小
* OFF:不使用压缩
* Low:低压缩比
* Medium:中等压缩比
* High:高压缩比
Read/Write Enabled:是否开启读写网格信息
如果开启,Unity将网格数据传给GPU后,在CPU中还会保留可寻址内存,意味着我们可以通过代码访问网格数据进行处理。
如果不开启,Unity将网格数据传给GPU后,会将CPU中的可寻址内存中网格数据删除,我们无法再得到网格数据。
开启时,会增加内存占用。关闭时,可以节约运行时内存使用量
何时开启?
* 需要在代码中读取或写入网格数据
* 需要运行时合并网格
* 需要使用网格碰撞器时
* 需要运行时使用NavMesh构建组件来烘焙NavMesh时
* 等待
### Geometry参数
Weld Vertices:合并在空间中共享相同位置的顶点,前提是这些顶点总体上共享相同的属性(UV、法线、切线等)
开启后:相当于会通过减少网格的总数量来优化网格的顶点技数
一般都是开启的,除非你想有意保留这些重复顶点,之后通过代码去获取他们来进行处理。
## Rig页签[官网](https://docs.unity3d.com/cn/2023.2/Manual/FBXImporter-Rig.html#GenericRig)

| Rig | 操纵 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Animation Type | None:不存在类型<br />Legacy: 旧版动画(很少使用)<br />Generic:骨架为非人型;如蜥蜴;猫等动画<br />Humanoid:骨架为人型(有两条腿、两条手臂和一个头) |
### Generic 类型
Root Node:选择用于此Avatar的根节点的骨骼(仅当选择Create From This Model才会出现)
### Humanoid 类型
Avatar Definition:选择获取Avatar定义的位置。
* No Avatar:没有化身系统信息
* Create From This Model:根据此模型创建Avatar化身信息
* Copy from Other Avatar:指向另一个模型上的Avatar化身信息
### 化身系统
Mapping:关节映射信息设置
Muscles&Settings:肌肉设置
#### Mapping
我们需要这个页签对模型关节进行映射设置,因为人物动画就是改变这些关节的角度。
Mapping:
* Clear:清空映射
* AutoMap:自动映射
* Load:从文件中读取
* Save:保存映射信息
Pose:
* Reset:重置姿势
* Sample Bind-Pose:绑定姿势示例
* Enforce T-Pose:强制T姿势
## Animation页签
Import Animation:从此资源导入动画。如果禁用下面的都没有,并且不会导入任何动画。
Anim Compression:导入动画时使用的压缩类型
* OFF:禁用动画压缩,在导入时Unity不会减少关键字数量,效果做好性能较低,文件较大,运行时内存占用也会变大,不建议使用。
* Keyframe Reduction:减少冗余关键帧,只适用于Generic通用动画类型
* Keyframe Reduction and Compression:旧版动画
* Optimal:让Unity决定如何压缩。仅适用于Generic和Humanoid
### 动画剪辑
## Animator 组件
参考官网:https://docs.unity3d.com/cn/2023.2/Manual/class-Animator.html
### 2. 深入理解 Humanoid的Root motion
角色动画的重心(通过各个骨骼的位置来计算的)
unity会根据具体动画计算重心在水平平面的投影,并将这个投影当做root motion的“根骨骼”节点来对待,这个点被称作 root transform。在humanoid动画中, unity会计算一个root transform
Root Motion会把动画文件中描述的Root Transform的坐标和角度值,转化为相对位移和相对转角,并以此来移动游戏对象。
## Animator Controller
## 1D混合树

### 使用1D混合树建立简单的前进和后退动画
1. 创建混合树
2. 设置参数,一般前进的阈值在2,闲置的阈值在0, 回退的阈值为-1.5
3. 编写脚本(这里使用的是新版输入系统)
```language
using UnityEngine;
using UnityEngine.InputSystem;
namespace Demo4
{
public class PlayerMove : MonoBehaviour
{
private Animator anim;
// 设置阈值,避免摇杆的误碰
float threashold = 0.1f;
// 向前走的移动速度
public float fowardSpeed = 2f;
// 向后走的移动速度
public float backwardSpeed = 1.5f;
float targetSpeed;
float currentSpeed;
Vector3 movement;
private void Start()
{
anim = this.GetComponent<Animator>();
}
private void Update()
{
currentSpeed = Mathf.Lerp(targetSpeed, currentSpeed, 0.9f);
movement = new Vector3(0, 0, currentSpeed * Time.deltaTime);
transform.position += movement;
anim.SetFloat("Speed", currentSpeed);
}
public void PlayerMoveTest(InputAction.CallbackContext callback)
{
// 二维向量的x,表示左右移动, x为1时,表示按键的A,或者摇杆的最右方
// 二维向量的y,表示前后移动, y为1时,表示按键的W,或者摇杆的最上方
Vector2 movement = callback.ReadValue<Vector2>();
targetSpeed = 0f;
if(movement.y > threashold) // 输入w或者摇杆往上摇
{
targetSpeed = fowardSpeed * movement.y;
}
if(movement.y < -threashold) // 输入s或者摇杆往下摇
{
targetSpeed = backwardSpeed * movement.y;
}
}
}
}
```
### 在Bleed Tree中使用Root Motion,实现简单的人物移动
注意:!!!!!如果人物模型上右刚体RigBody,请一定把约束关了(Constraints的z取消勾选),否则,人物是无法移动的。

注意:!!!如果使用Root Motion需要将Apply Root Motion勾选上。
* 在使用Root Motion动画移动时,那么移动速度的阈值就不能手动设置了,需要去自动计算该动画的阈值。选择Velocity Z就会自动计算前后方向的阈值了。

* 但是,会发现前进的速度和后退的速度是不一致的,我们可以设置Adjust Time Scale来调整前进和后退的速度。

* 但是,角色的移动速度不是由我们决定的,而是由Root Motion决定的。我们可以调整播放速度,来控制角色的移动速度。
* humanoid动画是在不同人物骨骼上复用动画的一种机制,同样的行走动画,在小一点的角色身上自然要走的慢一点。不同的角色模型走的速度不一样?如何解决这个问题?
* 最好的方法,就是为不同的人物,制作只属于自己的状态机。然后针对性的设置动画状态。
* 如果不用的人物共用一个状态机,又怎么办呢?首先,不同人物的速度不一致,是因为Root Motion会根据Scale的缩放来设置位移偏移。从理论角度来说,不同人物模型的速度不同,是因为两者的大小不同或者缩放值不同而导致的。
* 在anim.humanScale可以得到人物的缩放比例,我们可以在一开始就将他们的动画速度保持一致就可以了,动画的播放速度/缩放比例。
* 但是我们不想让整个的animator播放的速度都受到影响,我们可以在动画只进入这个Bleed Tree下才改变这个速度。我们只需要在下面设置参数即可。

* 如何在Root Motion中自定义移动速度?
* 最好的办法是修改动画的播放速度。
* 另一种方法是将Root Motion交给脚本设置。可以使用刚体组件。
```language
using UnityEngine;
using UnityEngine.InputSystem;
namespace Demo4
{
public class PlayerMove : MonoBehaviour
{
private Animator anim;
float threashold = 0.1f;
// 向前走的移动速度
public float fowardSpeed = 0.8f;
// 向后走的移动速度
public float backwardSpeed = 0.8f;
float targetSpeed;
float currentSpeed;
private Rigidbody rig;
private void Start()
{
anim = this.GetComponent<Animator>();
rig = this.GetComponent<Rigidbody>();
// 解决移动速度不受模型尺寸影响
anim.SetFloat("ScaleFactory", 1 / anim.humanScale);
}
// 由脚本来控制Root Motion
private void OnAnimatorMove()
{
Move();
}
void Move()
{
currentSpeed = Mathf.Lerp(targetSpeed, currentSpeed, 0.9f);
// 设置了刚体,需要将animator组件的update mode设置为animate physics
anim.SetFloat("Speed", currentSpeed);
// 用于解决下落慢的问题
Vector3 v= new Vector3(anim.velocity.x, rig.velocity.y, anim.velocity.z);
rig.velocity = v;
}
public void PlayerMoveTest(InputAction.CallbackContext callback)
{
Vector2 movement = callback.ReadValue<Vector2>();
targetSpeed = movement.y > 0f ? fowardSpeed movement.y : backwardSpeed movement.y;
}
}
}
```
## 2D混合树(有2个参数)
## 组合动画
## 动画曲线
## IK动画
## 应用场景:连招的实现
### 演示效果

### 状态机设置

### 思路
1. 实时获取每一时刻的状态。通过状态和时间判断是否使用连招。
2. 实时监控键盘的输入,根据输入设置条件
### 核心代码实现
```language
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 每个攻击动画的信息
public class AttackInfo
{
// 对应不同的攻击片段
public int id;
// 两个攻击片段过渡的时间
public float time;
// 是否是第一个动画片段
public bool isFirst = false;
public AttackInfo(int id, float time,bool isFirst)
{
this.id = id;
this.time = time;
this.isFirst = isFirst;
}
}
public class Demo2 : MonoBehaviour
{
// 键:动画片段的名称
// 值:该动画片段的相关信息
public Dictionary<string, AttackInfo> dic;
private Animator animator;
// 当前执行动画的片段
private AnimatorStateInfo state;
// 是否需要过渡到下一个动画片段
int count = 0;
private void Start()
{
animator = this.GetComponent<Animator>();
state = animator.GetCurrentAnimatorStateInfo(0);
dic = new Dictionary<string, AttackInfo>();
dic.Add("Atk1", new AttackInfo(1, 0.7f, true));
dic.Add("Atk2", new AttackInfo(2, 0.7f, false));
dic.Add("Atk3", new AttackInfo(3, 0.7f, false));
}
// 攻击输入
private void AttackInput()
{
if (Input.GetKeyDown(KeyCode.Space))
{
// 如果当前状态是闲置状态 (按下的第一次)
if (state.IsName("idle"))
{
// attack设置为true,此时动画状态会从idle过渡到Atk1
animator.SetBool("attack", true);
count = 1;
}
// 如果当前所执行的动画片段不是idle状态
// 那么当前的状态是 某一个攻击状态
else
{
// 先去找到当前的动画片段
foreach (var item in dic)
{
// 找到当前的动画片段
if(state.IsName(item.Key))
{
// 是否执行下一个攻击片段
count = item.Value.id + 1;
}
}
}
}
}
// 要攻击的动画片段
void AttackAction()
{
// 更新当前的动画状态
state = animator.GetCurrentAnimatorStateInfo(0);
// 如果不是idle状态
if (!state.IsName("idle"))
{
// 设置attack为false,此时就会往idle状态上过渡
animator.SetBool("attack", false);
}
// 先去找到当前状态的攻击动画
foreach (var item in dic)
{
// 1. 找到当前状态
// 2. 如果过渡到idle所有的时间比过渡到下一个动画片段用的时间长,就过渡到下一个动画
// state.normalizedTime 获取当前动画的播放进度
if(state.IsName(item.Key) && state.normalizedTime > item.Value.time
&& (count == item.Value.id + 1))
{
animator.SetBool("attack", true);
}
}
}
private void Update()
{
AttackInput();
AttackAction();
}
}
```
## 动画插件:Animation Rigging
## 动画插件:Dynamic Bone
## 动画插件: Magica Cloth
评论区