Unity Avatar Camera Controller 第一第三人称相机控制

Posted CoderZ1010

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity Avatar Camera Controller 第一第三人称相机控制相关的知识,希望对你有一定的参考价值。

文章目录


简介

本文介绍如何实现用于Avatar角色的相机控制脚本,支持第一人称、第三人称以及两种模式之间的切换,工具已上传至SKFramework框架的Package Manager中:

Variables

  • Avatar:相机跟随的Avatar角色;
  • Control Mode:控制模式 第一人称/第三人称;
  • Mode Change Key:切换第一/第三人称模式的快捷键,若不支持切换设为None即可;

  • Forward Align With Avatar:视角前方是否与Avatar对齐,为flase时表示视角可以在水平方向旋转;

  • Horizontal Sensitivity:水平方向旋转的灵敏度;
  • Vertical Sensitivity:垂直方向旋转的灵敏度;
  • Rot Y Min Limit:垂直方向上旋转最小值限制;
  • Rot Y Max Limit:垂直方向上旋转最大值限制;
  • Rotation Lerp Time:插值到目标旋转值所需的时间;
  • Height:相机与Avatar角色的高度差;
  • Distance:相机与Avatar角色的默认距离;
  • Min Distance Limit:相机距人物最小距离限制;
  • Max Distance Limit:相机距人物最大距离限制;
  • fpmDistance:第一人称模式所用的固定距离(第一人称时距离固定);
  • scollSensitivity:鼠标滚轮的灵敏度(第三人称时可滚动距离);

  • Invert Scroll Direction:反转鼠标滚轮滚动的反向;
  • Horizontal Offset:与Avatar在水平方向上的偏移值(仅在Forward Align With Avatar为true时开启使用,可以让Avatar在视野中偏左或偏右);

  • Obstacle Layer:用于避障检测的Layer层级

如下例所示,将场景中障碍物体的Layer设为Obstacle

避障检测时检测该层级:

Ctrl Avatar Rot When FP Mode:是否在第一人称模式下旋转视角时,同步旋转Avatar角色的朝向;

实现

Target Position

影响相机坐标的元素包括Avatar与相机的距离(Distance)、Avatar与相机的高度差(Height)、目标旋转值、水平方向上的偏移量(Horizontal Offset)及避障检测的影响。

  • Avatar与相机的距离:第三人称模式下通过鼠标滚轮控制,并通过最大最小值进行钳制,第一人称模式下使用固定值,代码如下:
//鼠标滚轮滚动改变距离
distance -= Input.GetAxis("Mouse ScrollWheel") * Time.deltaTime * 100f * scollSensitivity * (invertScrollDirection ? -1f : 1f);
//距离钳制
distance = Mathf.Clamp(distance, minDistanceLimit, maxDistanceLimit);
//插值方式计算距离
targetDistance = controlMode == ControlMode.ThirdPersonControl
    ? Mathf.Lerp(targetDistance, distance, Time.deltaTime * scollSensitivity)
    : fpmDistance;
  • 调用避障检测之前,目标坐标值等于Avatar角色的坐标加上Height高度,加上Distance距离,并乘上目标旋转值,代码如下:
//目标坐标值
Vector3 targetPosition = targetRotation * Vector3.forward * -targetDistance + avatar.position + Vector3.up * height;
  • 避障检测,通过SphereCast球形物理检测,检测碰撞点并向前移动:
//避障检测
private Vector3 ObstacleAvoidance(Vector3 current, Vector3 target, float radius, float maxDistance)

    Ray ray = new Ray(target, current - target);
    if (Physics.SphereCast(ray, radius, out RaycastHit hit, maxDistance, obstacleLayer))
    
        return ray.GetPoint(hit.distance - radius * 2f);
    
    return current;

  • 最终加上水平方向上偏移量的影响:
//避障
targetPosition = ObstacleAvoidance(targetPosition, avatar.position + Vector3.up * height, .1f, distance);
transform.position = targetPosition + Vector3.left * horizontalOffset;

Target Rotation

  • 获取水平及垂直方向上的输入值,让旋转x、y值自增/自减,并通过最大最小值限制垂直方向上的取值范围:
//检测鼠标右键按下
if (Input.GetMouseButton(1))

    horizontal = forwardAlignWithAvatar ? 0f : Input.GetAxis("Mouse X") * Time.deltaTime * 100f * horizontalSensitivity;
    vertical = Input.GetAxis("Mouse Y") * Time.deltaTime * 100f * verticalSensitivity;

    rotX += horizontal;
    rotY -= vertical;
    //钳制旋转y值角度
    rotY = Mathf.Clamp(rotY, rotYMinLimit, rotYMaxLimit);

  • 加入插值运算,实现平滑旋转:
//目标旋转值
Quaternion targetRotation = Quaternion.Euler(rotY, rotX, 0f);
//旋转值插值率
float rotationLerpPct = 1f - Mathf.Exp(Mathf.Log(1f - .99f) / rotationLerpTime * Time.deltaTime);
//插值方式计算旋转值
targetRotation = Quaternion.Lerp(transform.rotation, targetRotation, rotationLerpPct);
  • 第一人称模式时,相机视角旋转的同时控制Avatar角色的旋转:
transform.rotation = targetRotation;

//第一人称控制模式 相机视角旋转的同时控制Avatar角色的旋转
if (controlMode == ControlMode.FirstPersonControl && ctrlAvatarRotWhenFPMode)

    Vector3 euler = Vector3.zero;
    //只取相机的RotY
    euler.y = targetRotation.eulerAngles.y;
    avatar.rotation = Quaternion.Euler(euler);

Others

  • 切换控制模式:
if (Input.GetKeyDown(modeChangeKey))

    controlMode = controlMode == ControlMode.FirstPersonControl
        ? ControlMode.ThirdPersonControl
        : ControlMode.FirstPersonControl;

  • 相机控制代码需写在MonoBehaviour生命周期函数LateUpdate中,确保Avatar角色运动完成后,相机再进行跟随。

Unity动态创建Avatar骨骼映射

目录

前言

为了让单个动画可以通用于多个不同的人型模型上,Unity官方开发了一套骨骼重定向系统,把不同人型模型的骨骼映射到一套通用的骨骼映射上,然后再让动画去驱动这个通用的骨骼映射,从而实现驱动不同的模型。不过目前只支持人型模型,只需要把模型导入到Unity,就能自动生成骨骼映射(在Unity里是一个Avatar文件)。但是,并不是所有的模型格式导入到Unity都能自动生成这个Avatar,例如Glb/Gltf格式。为了让这些格式的模型也能支持通用的动画,通常需要放到一些建模软件里进行操作再导出为Fbx格式,这个过程是复杂且痛苦的。还有一种更无可奈可的情况是:完整人型模型是由各个模型在运行时动态组装生成的,并没有一个完整的模型可以导入建模软件进行操作。因此,我们应该寻找一种方式,使其可以在Unity运行阶段创建Avatar的骨骼映射。

1 了解Avatar骨骼映射

为了动态去创建这个Avatar文件,我们首先需要了解它包含了哪些内容,我们打开一个Unity已经创建好的Avatar:

可以看到,这个文件主要保存的是骨骼的映射关系。上面的人体里的每一个圆点代表着一个关节点,Optional Bone下面左边那一列就是Unity里设定好的关节点的名称,它们在每个Avatar文件里都是一样的,而右边部分就是当前这个模型的骨骼节点,Unity已经帮我们映射好了它们与通用人型骨骼的对应关系。当我们把这个Avatar映射文件赋值给Animator后,Animator就会去驱动固定的那些骨骼信息点,而这些固定的骨骼信息点就会去根据它们和模型真实骨骼的映射关系找到真正需要驱动的骨骼点,从而对其进行驱动,最终整个模型就动起来了。

2 动态创建Avatar骨骼映射

2.1 寻找相关API

由于通常情况下模型导入时unity就能帮我们创建好这个Avatar映射,我们根本就没真正接触这个过程,因此首先需要去查询官方是否有开放相关接口。幸运的是,确实有相关的接口,官方API描述如下:

AvatarBuilder.BuildHumanAvatar

Declaration

public static Avatar BuildHumanAvatar(GameObject go, HumanDescription humanDescription);

Parameters

go
humanDescription

Returns

Avatar Returns the Avatar, you must always always check the avatar is valid before using it with Avatar.isValid.

Description

Create a humanoid avatar.
The avatar is created using the supplied HumanDescription object which specifies the muscle space range limits and retargeting parameters like arm/leg twist and arm/leg stretch. See Also: HumanDescription.

从API中我们可以得知,使用它需要传入两个参数,第一个参数好理解,就是我们当前这个模型本身,第二个参数是个描述数据,我们再看看它的定义:

HumanDescription

struct in UnityEngine/Implemented in:UnityEngine.AnimationModule

Description

Class that holds humanoid avatar parameters to pass to the AvatarBuilder.BuildHumanAvatar function.

Properties

armStretch
feetSpacing
hasTranslationDoF
human
legStretch
lowerArmTwist
lowerLegTwist
skeleton
upperArmTwist
upperLegTwist

它具有一堆属性,光看这里我们也不知道其如何赋值,但恰好这些数据在上面的Avatar映射文件里见过:

可以看到,都是对骨骼的一些限制,大部分直接默认值即可,但有两个属性是图上没显示的,也就是hunman和skeleton这两个属性,但是从描述可以知道,hunman保存的刚好就是骨骼映射关系,而skeleton保存的是模型的骨骼集合,因此我们只需要创建出这两个属性需要的数据即可。

2.2 创建Skeleton数据

由于skeleton保存的是模型的骨骼集合,因此比较好操作,我们先把它创建起来,代码也很简单,就是把模型的所有Transform都收集起来即可。即使不是骨骼的Transform也无所谓,后面会根据映射关系从这些骨骼中找出对应的骨骼Transform。因此我们直接写代码:

  private static SkeletonBone[] CreateSkeleton(GameObject avatarRoot)
    
        List<SkeletonBone> skeleton = new List<SkeletonBone>();
       
        Transform[] avatarTransforms = avatarRoot.GetComponentsInChildren<Transform>();
        foreach (Transform avatarTransform in avatarTransforms)
        
            SkeletonBone bone = new SkeletonBone()
            
                name = avatarTransform.name,
                position = avatarTransform.localPosition,
                rotation = avatarTransform.localRotation,
                scale = avatarTransform.localScale
            ;

            skeleton.Add(bone);
        
        return skeleton.ToArray();
    

代码很简单,就是传入当前的模型,然后获取模型所有的Transform组件,然后把数据赋值给SkeletonBone即可再添加到集合中即可。

2.3 创建Human映射关系

这里的映射关系本来是在建模软件里做的,那我们如何能知道它们之间的关系呢?有两个方法:

  • 1 询问CP同学得知这个模型的基础骨骼和Avatar的对应关系,简单快捷高效
  • 2 自己根据Transform的名称和节点在模型的位置来猜测对应关系,可以在视图里对比,这样低效且不一定正确,可能需要多次尝试才能找到关系
    其实还有第三种方法:写算法去自动匹配,但是会比较困难,毕竟导入的模型不一定就是标准的T Pose,而且计算也不是那么好写的,感兴趣的大佬可以去尝试一下,到时候成功了麻烦教教我~
    从上面我们千辛万苦终于得到了映射关系,我这里是使用一个Dictionary来保存,大概如下:
 public static Dictionary<string, string> HumanSkeletonMap = new Dictionary<string, string>()
        
            "pelvis", "Hips" ,
            "spine_01", "Spine" ,
            "spine_02", "Chest",
            "spine_03", "UpperChest" ,
						...
            ...此处省略了一堆key-value...
						...
            "neck_01", "Neck" ,
            "head", "Head" ,
            "eye_EyeJoint_L", "LeftEye" ,
            "eye_EyeJoint_R", "RightEye" ,
            "mouth_JawJoint_M", "Jaw" ,
        ;

但是看了一下上面映射属性的数据结构是HumanBone,我们还需要写个函数去做映射,代码如下:

    private static HumanBone[] CreateHuman(GameObject avatarRoot)
    
        List<HumanBone> human = new List<HumanBone>();

        Transform[] avatarTransforms = avatarRoot.GetComponentsInChildren<Transform>();
        foreach (Transform avatarTransform in avatarTransforms)
        
            if (HumanSkeletonMap.TryGetValue(avatarTransform.name, out string humanName))
            
                HumanBone bone = new HumanBone
                
                    boneName = avatarTransform.name,
                    humanName = humanName,
                    limit = new HumanLimit()
                ;
                bone.limit.useDefaultValues = true;

                human.Add(bone);
            
        
        return human.ToArray();
    

和上面的skeleton集合的写法有点相似,就是赋值本分稍有区别,这里主要记录的是映射关系,所以就把上面的Dictionary里的映射关系赋值进去即可,最终我们得到了一个映射集合。

2.4 创建Avatar

拥有了SkeletonBone骨骼数据集合和HumanBone映射数据集合,我们就可以创建Avatar的描述文件了,其他属性都设置默认值,代码如下:

HumanDescription humanDescription = new HumanDescription()
        
            armStretch = 0.05f,
            feetSpacing = 0f,
            hasTranslationDoF = false,
            legStretch = 0.05f,
            lowerArmTwist = 0.5f,
            lowerLegTwist = 0.5f,
            upperArmTwist = 0.5f,
            upperLegTwist = 0.5f,
            skeleton = CreateSkeleton(gameObject),
            human = CreateHuman(gameObject),
        ;

现在我们连描述文件也有了,就可以创建最终的Avatar了,代码也很简单:

 Avatar avatar = AvatarBuilder.BuildHumanAvatar(gameObject, humanDescription);

到此,我们成功创建出了Avatar,使用时直接把这个Avatar赋值给Animator,即可使用通用人型动画驱动我们的模型了!

3 总结

从上面的步骤中不难看出,真正困难的只有创建骨骼映射那部分,毕竟我们不一定那么方便地就能找到模型的骨骼映射关系,但是一旦找到它们的关系,其他部分就再简单不过了。过程中我们可能会遇到一些千奇百怪的问题,比如模型动作非常诡异等,Mesh扭曲等等,这些问题大部分都是因为映射关系不对,少部分是因为骨骼集合里没收集上所有骨骼信息,不要惊慌,多细心检查,最终肯定能成功!

以上是关于Unity Avatar Camera Controller 第一第三人称相机控制的主要内容,如果未能解决你的问题,请参考以下文章

Unity动态创建Avatar骨骼映射

Unity Avatar Foot IK - Avatar Foot Placement Resolution

Unity Avatar Cover System - 如何实现一个Avatar角色的智能掩体系统

Unity 3D 人形角色动画(Avatar)||Unity 3D 导航系统||Unity 3D 障碍物

UMA - Unity Multipurpose Avatar

Unity 如何实现游戏Avatar角色头部跟随视角转动