UE5相机系统初探(一)

UE5相机系统初探(一)

和Unity类似,UE的相机也是由名为Camera的component控制的。那么,在UE中要如何实现一个跟随玩家的第三人称相机呢?假设我们已经有了一个表示玩家的类ACF_Character,首先第一步就是要先在ACF_Character类中定义camera component:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
UCameraComponent* Camera;

我们希望camera component可以在蓝图中编辑,也可以在属性窗口中修改一些参数,因而这里设置UPROPERTY为EditAnywhere和BlueprintReadWrite。

下一步要在构造函数中创建该component,并将其挂接到root component下:

ACF_Character::ACF_Character()
{// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.PrimaryActorTick.bCanEverTick = true;Camera = CreateDefaultSubobject<UCameraComponent>("Camera");Camera->SetupAttachment(RootComponent);}

此时编译并创建相应的蓝图类,就可以预览camera component了:

在这里插入图片描述

这里相机的位置默认为0,0,0,这样会视角穿透玩家,需要调整一下,比如调整为离玩家一定距离俯视玩家:

在这里插入图片描述

然后,我们到场景里运行下看看实际效果:

在这里插入图片描述

作为一个基本的相机系统,我们希望相机可以一直跟随玩家移动,并且视角永远朝向玩家的前方。这里先加入玩家前后移动与左右移动的代码,并在Project Settings里进行绑定:

void ACF_Character::MoveForward(float Value)
{if ((Controller != nullptr) && (Value != 0.0f)){const FRotator Rotation = Controller->GetControlRotation();const FRotator Yaw(0, Rotation.Yaw, 0);const FVector Direction = FRotationMatrix(Yaw).GetUnitAxis(EAxis::X);AddMovementInput(Direction, Value);}
}void ACF_Character::MoveRight(float Value)
{if ((Controller != nullptr) && (Value != 0.0f)){const FRotator Rotation = Controller->GetControlRotation();const FRotator Yaw(0, Rotation.Yaw, 0);const FVector Direction = FRotationMatrix(Yaw).GetUnitAxis(EAxis::Y);AddMovementInput(Direction, Value);}
}

在这里插入图片描述

玩家正面朝向x轴方向,肩膀和y轴平行,所以forward使用的EAxis::X,而right使用的是EAxis::Y。

在这里插入图片描述

如果此时运行游戏,按方向键,会出现反直觉的奇怪现象,玩家除了location会变化,rotation也会变化,而我们明明只是调用了AddMovementInput,并没有设置玩家的rotation。

在这里插入图片描述

这是因为Character Movement这个component默认会勾选Orient Rotation To Movement:

在这里插入图片描述

它的初衷是希望玩家移动时,朝向会跟着插值到移动的方向,不然表现会很奇怪,会出现玩家倒着走的情况。

在这里插入图片描述

那么实际上,这个选项还是应当勾上的。问题在于我们的camera component是挂在玩家上的,因此玩家旋转时相机也跟着旋转了,导致所有的按键方向最后看上去都变成了前进的方向。

在Camera Option中,还有一个Use Pawn Control Rotation的选项,表示是否把controller当前的rotation设置给camera。具体逻辑可以参见源码:

// CameraComponent.cpp
if (bUsePawnControlRotation)
{const APawn* OwningPawn = Cast<APawn>(GetOwner());const AController* OwningController = OwningPawn ? OwningPawn->GetController() : nullptr;if (OwningController && OwningController->IsLocalPlayerController()){const FRotator PawnViewRotation = OwningPawn->GetViewRotation();if (!PawnViewRotation.Equals(GetComponentRotation())){SetWorldRotation(PawnViewRotation);}}
}

但如果勾选上了,表现依旧会非常奇怪,这是因为我们现在的输入逻辑压根就不会修改controller的rotation,所以rotation恒定为0,但又因为camera component挂接在root component下,它的location会随着变化。我们可以控制台输入showdebug camera显示当前相机的信息:

在这里插入图片描述

首先注意到camera在世界坐标系下的rotation为0,而玩家当前的rotation为(Y=51.04),右边视图显示的是camera component的transform,它表示camera相对于root component也就是玩家的旋转,因此是-51.04。自由视角下看起来更直观:

在这里插入图片描述

那么,为了方便解决这类问题,我们可以使用UE提供的Spring Arm Component。这是个非常强大的组件,它还可以处理相机被其他物体所遮挡的情况。首先在头文件中引入变量声明:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
USpringArmComponent* SpringArm;

接着在构造函数中创建component,注意此时camera component需要挂在spring arm上,spring arm会控制它的child component的transform:

ACF_Character::ACF_Character()
{// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.PrimaryActorTick.bCanEverTick = true;SpringArm = CreateDefaultSubobject<USpringArmComponent>("SpringArm");SpringArm->SetupAttachment(RootComponent);Camera = CreateDefaultSubobject<UCameraComponent>("Camera");Camera->SetupAttachment(SpringArm);}

在蓝图中预览:

在这里插入图片描述

这里主要注意Camera Category下的几个字段。首先是Target Arm Length,它表示弹簧臂的长度,可以用作camera component在玩家forward方向下的距离。如果要调整其他方向的距离,可以使用Socket Offset字段或是Target Offset字段。当然,这两个字段也可以用来调整forward方向,那么它们到底有什么区别呢?如果只是设置offset,看上去效果都一样:

在这里插入图片描述

在这里插入图片描述

看各种解释也是比较云里雾里,不如直接看代码。用到这两个offset的代码主要集中在USpringArmComponent::UpdateDesiredArmLocation这个函数,相关代码如下:

void USpringArmComponent::UpdateDesiredArmLocation(bool bDoTrace, bool bDoLocationLag, bool bDoRotationLag, float DeltaTime)
{FRotator DesiredRot = GetTargetRotation();PreviousDesiredRot = DesiredRot;// Get the spring arm 'origin', the target we want to look atFVector ArmOrigin = GetComponentLocation() + TargetOffset;// We lag the target, not the actual camera position, so rotating the camera around does not have lagFVector DesiredLoc = ArmOrigin;PreviousArmOrigin = ArmOrigin;PreviousDesiredLoc = DesiredLoc;// Now offset camera position back along our rotationDesiredLoc -= DesiredRot.Vector() * TargetArmLength;// Add socket offset in local spaceDesiredLoc += FRotationMatrix(DesiredRot).TransformVector(SocketOffset);{ResultLoc = DesiredLoc;bIsCameraFixed = false;UnfixedCameraPosition = ResultLoc;}// Form a transform for new world transform for cameraFTransform WorldCamTM(DesiredRot, ResultLoc);// Convert to relative to componentFTransform RelCamTM = WorldCamTM.GetRelativeTransform(GetComponentTransform());// Update socket location/rotationRelativeSocketLocation = RelCamTM.GetLocation();RelativeSocketRotation = RelCamTM.GetRotation();UpdateChildTransforms();
}

从代码中可以看出,TargetOffset用处就是对目标的原点进行了偏移,DesiredRot.Vector()表示旋转后的forward方向(x轴),所以TargetArmLength用于计算forward方向下摄像机的位置。最后SocketOffset相当于以此时目标位置为原点旋转范围的半径。这么说有点抽象,我们来看下分别设置TargetOffsetSocketOffset时旋转spring arm component的效果,就一目了然了。

在这里插入图片描述

在这里插入图片描述

我们再次回到DesiredRot.Vector(),前面说它表示旋转后的forward方向向量,这次来看下它的内部实现:

template<typename T>
UE::Math::TVector<T> UE::Math::TRotator<T>::Vector() const
{	// Remove winding and clamp to [-360, 360]const T PitchNoWinding = FMath::Fmod(Pitch, (T)360.0);const T YawNoWinding = FMath::Fmod(Yaw, (T)360.0);T CP, SP, CY, SY;FMath::SinCos( &SP, &CP, FMath::DegreesToRadians(PitchNoWinding) );FMath::SinCos( &SY, &CY, FMath::DegreesToRadians(YawNoWinding) );UE::Math::TVector<T> V = UE::Math::TVector<T>( CP*CY, CP*SY, SP );return V;
}

这个是怎么得到的呢?首先我们知道TRotator也就包含三个旋转分量:

template<typename T>
struct TRotator
{
public:/** Rotation around the right axis (around Y axis), Looking up and down (0=Straight Ahead, +Up, -Down) */T Pitch;/** Rotation around the up axis (around Z axis), Turning around (0=Forward, +Right, -Left)*/T Yaw;/** Rotation around the forward axis (around X axis), Tilting your head, (0=Straight, +Clockwise, -CCW) */T Roll;
};

我们认为每次旋转都是围绕固定轴(世界坐标系)旋转,也就是外旋,那么按照外旋方式,是以X-Y-Z(roll-pitch-yaw)的旋转顺序旋转,最终得到的是一个左乘的旋转矩阵。不过这里的输入向量很简单,就是(1,0,0)。分别按顺序乘以3个旋转矩阵:
R o l l = [ 1 0 0 0 c o s ( r o l l ) − s i n ( r o l l ) 0 s i n ( r o l l ) c o s ( r o l l ) ] Roll = \begin{bmatrix} 1 & 0 & 0 \\ 0 & cos(roll) & -sin(roll) \\ 0 & sin(roll) & cos(roll) \end{bmatrix} Roll= 1000cos(roll)sin(roll)0sin(roll)cos(roll)

R o l l ⋅ [ 1 , 0 , 0 ] T = [ 1 , 0 , 0 ] T Roll \cdot [1, 0, 0]^T = [1, 0, 0]^T Roll[1,0,0]T=[1,0,0]T

P i t c h = [ c o s ( p i t c h ) 0 − s i n ( p i t c h ) 0 1 0 s i n ( p i t c h ) 0 c o s ( p i t c h ) ] Pitch = \begin{bmatrix} cos(pitch) & 0 & -sin(pitch) \\ 0 & 1 & 0 \\ sin(pitch) & 0 & cos(pitch) \end{bmatrix} Pitch= cos(pitch)0sin(pitch)010sin(pitch)0cos(pitch)

P i t c h ⋅ [ 1 , 0 , 0 ] T = [ c o s ( p i t c h ) , 0 , s i n ( p i t c h ) ] T Pitch \cdot [1, 0, 0]^T = [cos(pitch), 0, sin(pitch)]^T Pitch[1,0,0]T=[cos(pitch),0,sin(pitch)]T

Y a w = [ c o s ( y a w ) − s i n ( y a w ) 0 s i n ( y a w ) c o s ( y a w ) 0 0 0 1 ] Yaw = \begin{bmatrix} cos(yaw) & -sin(yaw) & 0 \\ sin(yaw) & cos(yaw) & 0 \\ 0 & 0 & 1 \end{bmatrix} Yaw= cos(yaw)sin(yaw)0sin(yaw)cos(yaw)0001

Y a w ⋅ [ c o s ( p i t c h ) , 0 , s i n ( p i t c h ) ] T = [ c o s ( y a w ) c o s ( p i t c h ) , s i n ( y a w ) c o s ( p i t c h ) , s i n ( p i t c h ) ] T Yaw \cdot [cos(pitch), 0, sin(pitch)]^T = [cos(yaw)cos(pitch), sin(yaw)cos(pitch), sin(pitch)]^T Yaw[cos(pitch),0,sin(pitch)]T=[cos(yaw)cos(pitch),sin(yaw)cos(pitch),sin(pitch)]T

这就和代码里完全一致了。如果用几何方式来推导,答案也是一样的。

在这里插入图片描述

如图,易知B’的坐标为 ( c o s ( p i t c h ) , 0 , s i n ( p i t c h ) ) (cos(pitch), 0, sin(pitch)) (cos(pitch),0,sin(pitch)),然后在绕z轴旋转时,平行于z轴的分量是不参与旋转的,只需计算垂直的分量,那么容易知道DB’的长度为 c o s ( p i t c h ) cos(pitch) cos(pitch),B"E的长度为 s i n ( y a w ) c o s ( p i t c h ) sin(yaw)cos(pitch) sin(yaw)cos(pitch),B’‘F的长度为 c o s ( y a w ) c o s ( p i t c h ) cos(yaw)cos(pitch) cos(yaw)cos(pitch)。所以最后B’'的坐标为 ( c o s ( y a w ) c o s ( p i t c h ) , s i n ( y a w ) c o s ( p i t c h ) , s i n ( p i t c h ) ) (cos(yaw)cos(pitch), sin(yaw)cos(pitch), sin(pitch)) (cos(yaw)cos(pitch),sin(yaw)cos(pitch),sin(pitch))

不过此时运行起来发现,相机依旧是跟随着玩家旋转,似乎并没有什么卵用。但是,如果我们勾选上Spring Arm Component里的Use Pawn Control Rotation,一切就变得大不同:

在这里插入图片描述

这是为什么呢?为什么在Camera Component中勾选这个选项是没用的,而Spring Arm Component就可以了呢?这点还是要去源码里找答案:

FRotator USpringArmComponent::GetTargetRotation() const
{FRotator DesiredRot = GetDesiredRotation();if (bUsePawnControlRotation){if (APawn* OwningPawn = Cast<APawn>(GetOwner())){const FRotator PawnViewRotation = OwningPawn->GetViewRotation();if (DesiredRot != PawnViewRotation){DesiredRot = PawnViewRotation;}}}
}

可以看到,这里rotation的计算和前面是一样的,但别忘了我们前面讨论的UpdateDesiredArmLocation函数,相机的位置会更新为距离Spring Arm的forward方向TargetArmLength,再加上固定的SocketOffset,同时相机rotation和Spring Arm保持一致,就像这样:

在这里插入图片描述

到目前为止,我们终于让WASD键只控制玩家移动,不再控制相机,相机也能默默地跟随玩家了。那么下一步很自然地,就是如何手动控制相机呢?首先还是先做下绑定:

在这里插入图片描述

然后,基于我们现在的设置,其实只要对controller施加旋转,理论上就能达到想要的效果了:

void ACF_Character::TurnAtRate(float Rate)
{AddControllerYawInput(Rate * TurnRate * GetWorld()->GetDeltaSeconds());
}void ACF_Character::LookUpAtRate(float Rate)
{AddControllerPitchInput(Rate * LookUpRate * GetWorld()->GetDeltaSeconds());
}

到目前为止,我们有了一个初步可用的第三人称相机了。在下一章节中,我们再对当前的相机做一些优化,让它的功能更加丰富。

在这里插入图片描述

Reference

[1] Camera Framework Essentials for Games

[2] Working with Camera Components

[3] Using Spring Arm Components

[4] Property Specifiers

[5] How can I use “Add Movement Input” without rotation?

[6] 虚幻引擎相机系统原理机制源码剖析

[7] How to convert Euler angles to directional vector?

[8] 欧拉角顺序与转换

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/57782.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python】OpenCV—Tracking(10.4)—Centroid

文章目录 1、任务描述2、人脸检测模型3、完整代码4、结果展示5、涉及到的库函数6、参考 1、任务描述 基于质心实现多目标&#xff08;以人脸为例&#xff09;跟踪 人脸检测采用深度学习的方法 核心步骤&#xff1a; 步骤#1&#xff1a;接受边界框坐标并计算质心 步骤#2&…

【maven】idea执行了maven的install命令给本地安装项目依赖包 安装后删除

目录 事件起因环境和工具操作过程解决办法1、找到对应的目录下的文件&#xff0c;手动去删除&#xff0c;比如我的依赖库的路径是D:\qc_code\apache-maven-3.8.2\repository 我只需要找到这个目录下对应的依赖包进行手动删除即可&#xff08;不推荐&#xff0c;强行删除文件夹文…

PostgreSQL 到 PostgreSQL 数据迁移同步

简述 PostgreSQL 是一个历史悠久且广泛使用的数据库&#xff0c;不仅具备标准的关系型数据库能力&#xff0c;还具有相当不错的复杂 SQL 执行能力。用户常常会将 PostgreSQL 应用于在线事务型业务&#xff0c;以及部分数据分析工作&#xff0c;所以 PostgreSQL 到 PostgreSQL …

Java项目实战II基于Java+Spring Boot+MySQL的智能推荐的卫生健康系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 基于Java、…

免费插件集-illustrator插件-Ai插件-闭合开放路径

文章目录 1.介绍2.安装3.通过窗口>扩展>知了插件4.功能解释5.总结 1.介绍 本文介绍一款免费插件&#xff0c;加强illustrator使用人员工作效率&#xff0c;实现图形编辑中闭合开放路径。首先从下载网址下载这款插件https://download.csdn.net/download/m0_67316550/8789…

Java基于SpringBoot+Vue框架的房屋租赁管理系统(附源码,文档)

博主介绍&#xff1a;✌Java徐师兄、7年大厂程序员经历。全网粉丝13w、csdn博客专家、掘金/华为云等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不…

【前端】CSS知识梳理

基础&#xff1a;标签选择器、类选择器、id选择器和通配符选择器 font:font-style(normal) font-weight(400) font-size(16px) /line-height(0) font-family(宋体&#xff09; 复合&#xff1a; 后代选择器&#xff08; &#xff09;、子选择器&#xff08;>)、并集选择器(…

第16课 核心函数(方法)

掌握常用的内置函数及其用法。 数学类函数&#xff1a;abs、divmod、max、min、pow、round、sum。 类型转换函数&#xff1a;bool、int、float、str、ord、chr、bin、hex、tuple、list、dict、set、enumerate、range、object。 序列操作函数&#xff1a;all、any、filter、m…

基于AI深度学习的中医针灸实训室腹针穴位智能辅助定位系统开发

在中医针灸的传统治疗中&#xff0c;穴位取穴的精确度对于治疗效果至关重要。然而&#xff0c;传统的定位方法&#xff0c;如体表标志法、骨度折量法和指寸法&#xff0c;由于观察角度、个体差异&#xff08;如人体姿态和皮肤纹理&#xff09;以及环境因素的干扰&#xff0c;往…

C++ 实现俄罗斯方块游戏

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

《高频电子线路》—— 相位平衡条件判断准则

文章内容来源于【中国大学MOOC 华中科技大学通信&#xff08;高频&#xff09;电子线路精品公开课】&#xff0c;此篇文章仅作为笔记分享。 相位平衡条件判断准则 反馈电压或者从电感上获得&#xff0c;或者从电容上获得。分别为电感反馈三端振荡器&#xff0c;或者是电容反馈…

砥砺十年风雨路,向新而行创新程丨怿星科技十周年庆典回顾

10月24日&#xff0c;是一年中的第256天&#xff0c;也是程序员节&#xff0c;同时也是怿星的生日。2014年到2024年&#xff0c;年华似水匆匆一瞥&#xff0c;多少岁月轻描淡写&#xff0c;怿星人欢聚一堂&#xff0c;共同为怿星科技的十周年庆生&#xff01; 01.回忆往昔&…

Android——横屏竖屏

系统配置变更的处理机制 为了避免横竖屏切换时重新加载界面的情况&#xff0c;Android设计了一中配置变更机制&#xff0c;在指定的环境配置发生变更之时&#xff0c;无需重启活动页面&#xff0c;只需执行特定的变更行为。该机制的视线过程分为两步&#xff1a; 修改 Androi…

mysql上课总结(5)(MySQL的完整性约束(详细介绍))

目录 一、完整性约束。 &#xff08;1&#xff09;概念与目的。 <1>概念。 <2>目的。 &#xff08;2&#xff09;各个约束的详细&#xff08;表格&#xff09; &#xff08;3&#xff09;各个约束的简要总结。 <1>主键约束。 <2>唯一约束。 <3>非…

msys2更换国内源(多个文件(不是3个文件的版本!))

msys2更换国内源 起因排查答案如下mirrorlist.mingw64mirrorlist.ucrt64mirrorlist.mingw32mirrorlist.mingwmirrorlist.clang64mirrorlist.clang32mirrorlist.msys 不想看经过的直接跳到答案 起因 查了很多个教程大部分都是【打开MSYS2软件内的\etc\pacman.d\ 中3个文件&…

使用 MMDetection 实现 Pascal VOC 数据集的目标检测项目练习(二) ubuntu的下载安装

首先&#xff0c;Linux系统是人工智能和深度学习首选系统。原因如下: 开放性和自由度&#xff1a;Linux 是一个开源操作系统&#xff0c;允许开发者自由修改和分发代码。这在开发和研究阶段非常有用&#xff0c;因为开发者可以轻松地访问和修改底层代码。社区支持&#xff1a;…

TCP Analysis Flags 之 TCP Keep-Alive

前言 默认情况下&#xff0c;Wireshark 的 TCP 解析器会跟踪每个 TCP 会话的状态&#xff0c;并在检测到问题或潜在问题时提供额外的信息。在第一次打开捕获文件时&#xff0c;会对每个 TCP 数据包进行一次分析&#xff0c;数据包按照它们在数据包列表中出现的顺序进行处理。可…

关于数学建模的一些介绍

为了更好了解世界&#xff0c;我们可以通过数学来描述许多特定的现象&#xff0c;而数学模型就是现实世界的理想化&#xff0c;不过它永远不能完全精确地表示现实世界。 在这篇文章中&#xff0c;我将介绍一些数学建模的基本概念以及相应的基础知识&#xff0c;而关于更具体的…

CSRA的LINUX操作系统24年11月2日下午上课笔记

压缩和解压缩&#xff1a;zip 、gzip、bz、xz # zip 压缩 # 压缩文件夹 # 解压缩 # unzip -v 查看压缩包中的内容 # bzip2 dir1/* :将dir1中的所有文件压缩 # gzip # 压缩文件夹 # 解压缩 tar 归档命令&#xff1a; # 创建tar包 tar -c*f # 释放tar包 tar -xf[c] # c …

Java JUC(四) 自定义线程池实现与原理分析

目录 一. 阻塞队列 BlockingQue 二. 拒绝策略 RejectPolicy 三. 线程池 ThreadPool 四. 模拟运行 在 Java基础&#xff08;二&#xff09; 多线程编程 中&#xff0c;我们简单介绍了线程池 ThreadPoolExecutor 的核心概念与基本使用。在本文中&#xff0c;我们将基于前面学…