模型的制作过程

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)

![image-1698311368070](/upload/2023/10/image-1698311368070.png)

| 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混合树

![image-nztg.png](/upload/image-nztg.png)

### 使用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取消勾选),否则,人物是无法移动的。

![image.png](/upload/2022/12/image-218e7e5774d240449218c7084a415426.png)

注意:!!!如果使用Root Motion需要将Apply Root Motion勾选上。

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

![image.png](/upload/2022/12/image-a84b9c234ce04ac0add45324b724be60.png)

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

![image.png](/upload/2022/12/image-e7d86722232d44a0b9f8f0c71e470d2a.png)

* 但是,角色的移动速度不是由我们决定的,而是由Root Motion决定的。我们可以调整播放速度,来控制角色的移动速度。

* humanoid动画是在不同人物骨骼上复用动画的一种机制,同样的行走动画,在小一点的角色身上自然要走的慢一点。不同的角色模型走的速度不一样?如何解决这个问题?

* 最好的方法,就是为不同的人物,制作只属于自己的状态机。然后针对性的设置动画状态。

* 如果不用的人物共用一个状态机,又怎么办呢?首先,不同人物的速度不一致,是因为Root Motion会根据Scale的缩放来设置位移偏移。从理论角度来说,不同人物模型的速度不同,是因为两者的大小不同或者缩放值不同而导致的。

* 在anim.humanScale可以得到人物的缩放比例,我们可以在一开始就将他们的动画速度保持一致就可以了,动画的播放速度/缩放比例。

* 但是我们不想让整个的animator播放的速度都受到影响,我们可以在动画只进入这个Bleed Tree下才改变这个速度。我们只需要在下面设置参数即可。

![image.png](/upload/2022/12/image-3c6b9f22844c4774b26774f2a1ab3fe4.png)

* 如何在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动画

## 应用场景:连招的实现

### 演示效果

![lianzhao2.gif](/upload/2022/11/lianzhao2-9e130e6410084ffb9ad979dedd1a1769.gif)

### 状态机设置

![image.png](/upload/2022/11/image-9fb781bcef854de3b8967d3574d8b504.png)

### 思路

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