UE5相机系统初探(一)

news2024/11/24 12:34:23

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 at
	FVector ArmOrigin = GetComponentLocation() + TargetOffset;
	// We lag the target, not the actual camera position, so rotating the camera around does not have lag
	FVector DesiredLoc = ArmOrigin;

	PreviousArmOrigin = ArmOrigin;
	PreviousDesiredLoc = DesiredLoc;

	// Now offset camera position back along our rotation
	DesiredLoc -= DesiredRot.Vector() * TargetArmLength;
	// Add socket offset in local space
	DesiredLoc += FRotationMatrix(DesiredRot).TransformVector(SocketOffset);

	{
		ResultLoc = DesiredLoc;
		bIsCameraFixed = false;
		UnfixedCameraPosition = ResultLoc;
	}

	// Form a transform for new world transform for camera
	FTransform WorldCamTM(DesiredRot, ResultLoc);
	// Convert to relative to component
	FTransform RelCamTM = WorldCamTM.GetRelativeTransform(GetComponentTransform());

	// Update socket location/rotation
	RelativeSocketLocation = 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.coloradmin.cn/o/2230577.html

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

相关文章

短剧开发新模式:从内容创新到市场突围的全攻略

在探索短剧开发模式的过程中&#xff0c;理解其核心要素是至关重要的第一步。短剧作为一种独特的叙事形式&#xff0c;其特点是在极短的时间框架内讲述一个完整且吸引人的故事&#xff0c;这要求创作者必须具备高超的叙事技巧和对观众心理的深刻理解。创作者面临的首要挑战是如…

模型剪枝,如何把模型的使用成本降下来?

模型剪枝如何为企业节省百万预算&#xff1f; ©作者|DWT 来源|神州问学 近年来&#xff0c;大型语言模型&#xff08;LLM&#xff09;如GPT-3、LLaMA等在自然语言处理领域取得了令人瞩目的成果。然而&#xff0c;这些模型通常拥有数十亿甚至上千亿的参数&#xff0c;训练…

云效+mqtt实现本地构建和远程自动发版

之前写过一篇jenkinsmqtt实现本地构建和远程自动发版_jenkins远程调用和本地调用-CSDN博客 由于本地搭建jenkins实在太费机器了&#xff0c;这次改用云效搭建。不过云效并没有直接发送mqtt的方法&#xff0c;需要编写中转接口。 中转接口采用go-gin框架实现&#xff0c;代码如…

【蔬菜识别】Python+深度学习+CNN卷积神经网络算法+TensorFlow+人工智能+模型训练

一、介绍 蔬菜识别系统&#xff0c;本系统使用Python作为主要编程语言&#xff0c;通过收集了8种常见的蔬菜图像数据集&#xff08;‘土豆’, ‘大白菜’, ‘大葱’, ‘莲藕’, ‘菠菜’, ‘西红柿’, ‘韭菜’, ‘黄瓜’&#xff09;&#xff0c;然后基于TensorFlow搭建卷积神…

安装Blender并使用

前言 该系列记录了如何用Blenderpro来构建自己的场景数据集&#xff0c;从环境搭建到后期构建数据集的整个流程 本文章是第一部分&#xff0c;BlenderPrc2的安装以及环境配置 部分参考https://blog.csdn.net/weixin_49521551/article/details/121573334 官方文档https://dlr…

ApsaraMQ Serverless 能力再升级,事件驱动架构赋能 AI 应用

本文整理于 2024 年云栖大会阿里云智能集团高级技术专家金吉祥&#xff08;牟羽&#xff09;带来的主题演讲《ApsaraMQ Serverless 能力再升级&#xff0c;事件驱动架构赋能 AI 应用》 云消息队列 ApsaraMQ 全系列产品 Serverless 化&#xff0c;支持按量付费、自适应弹性、跨可…

栈虚拟机和寄存器虚拟机,有什么不同?

本来这节内容是打算直接讲字节码指令的&#xff0c;但讲之前又必须得先讲指令集架构&#xff0c;而指令集架构又分为两种&#xff0c;一种是基于栈的&#xff0c;一种是基于寄存器的。 那不妨我们这节就单独来讲讲栈虚拟机和寄存器虚拟机&#xff0c;它们有什么不同&#xff0…

Rust整合Elasticsearch

Elasticsearch是什么 Lucene&#xff1a;Java实现的搜索引擎类库 易扩展高性能仅限Java开发不支持水平扩展 Elasticsearch&#xff1a;基于Lucene开发的分布式搜索和分析引擎 支持分布式、水平扩展提高RestfulAPI&#xff0c;可被任何语言调用 Elastic Stack是什么 ELK&a…

【Apache Zookeeper】

一、简介 1、场景 如何让⼀个应⽤中多个独⽴的程序协同⼯作是⼀件⾮常困难的事情。开发这样的应⽤&#xff0c;很容易让很多开发⼈员陷⼊如何使多个程序协同⼯作的逻辑中&#xff0c;最后导致没有时间更好地思考和实现他们⾃⼰的应⽤程序逻辑&#xff1b;又或者开发⼈员对协同…

手把手写Linux第一个小程序 - 进度条(5种版本)

本专栏内容为&#xff1a;Linux学习专栏&#xff0c;分为系统和网络两部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握Linux。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;linux &#x1f69a;代码仓库&#xff1a;小小unicorn的代…

TikTok如何用邮箱注册?用哪种邮箱比较好?

要在TikTok上创建一个账号&#xff0c;首先需要进行注册&#xff0c;这是一个简单但至关重要的步骤。在本篇文章中&#xff0c;我们将详细介绍如何用邮箱注册TikTok的整个过程&#xff0c;包括每个步骤的细节和注意事项。此外&#xff0c;我们还将讨论选择哪种邮箱比较好&#…

LabVIEW在Windows和Linux开发的差异

LabVIEW广泛应用于工程和科研领域的自动化和测量控制系统开发&#xff0c;其在Windows和Linux平台上的开发环境有所不同。这些差异主要体现在操作系统兼容性、硬件支持、软件库和驱动程序、实时系统开发以及部署选择上。以下从各个方面详细对比分析LabVIEW在Windows与Linux系统…

哪个牌子的宠物空气净化器好?口碑好的宠物空气净化器推荐!

哪个牌子的宠物空气净化器好&#xff1f;作为一名家电测评博主&#xff0c;我发现市面上宠物空气净化器的牌子越来越多了&#xff0c;很多厂家都看中了宠物行业的红利&#xff0c;想来分一杯羹&#xff0c;这就导致很多技术不成熟的产品流入了市场。今年我测试了50多台宠物空气…

ios 快捷指令扩展(Intents Extension)简单使用 swift语言

本文介绍使用Xcode15 建立快捷指令的Extension&#xff0c;并描述如何修改快捷指令的IntentHandler&#xff0c;带参数跳转主应用&#xff1b;以及展示多个选项的快捷指令弹框(配置intentdefinition文件)&#xff0c;点击选项带参数跳到主应用的方法 创建快捷指令 快捷指令是…

计算机的错误计算(一百四十一)

摘要 探讨 MATLAB中正弦、余弦的计算精度问题。当自变量为大数时&#xff0c;输出可能出错。 从 IEEE-754-2019 知&#xff0c;三角函数的定义域是实数域。 例1. 计算 直接贴图吧&#xff1a; 这样&#xff0c;MATLAB的输出均为错误结果&#xff0c;即没有正确有效数字。…

医院绩效考核管理系统源码,医院如何构建绩效考核体系?

医院绩效考核管理系统作为现代医院管理的重要组成部分&#xff0c;其核心功能旨在提高医院运营效率、优化资源配置、确保医疗服务质量&#xff0c;以及增强医院竞争力。 业务科室绩效考核体系的构建 临床医疗与医技科室绩效考核的设置 临床医疗的绩效考核采用百分制&#xff…

「C/C++」C/C++标准库之#include<cstdlib>通用工具库

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…

Resnet代码实现

图2 18-layer、34-layer的残差结构 图3 50-layer、101-layer、102-layer的残差结构 import torch import torch.nn as nn#这个18或者34层网络的残差模块&#xff0c;根据ResNet的具体实现可以自动匹配 class BasicBlock(nn.Module):conv1 stride1对应的实线残差&#xff0c;因…

为什么大家都在学数字孪生呢?

随着物联网&#xff0c;大数据、人工智能等技术的发展&#xff0c;新一代信息技术与制造业正在深度融合&#xff0c;人们与物理世界的交互方式正在发生转折性的变化。数字化转型正在成为企业的重要战略&#xff0c;而数字孪生则成为全新的焦点。 当下&#xff0c;在数字技术和…

IDEA使用Maven Helper查看整个项目的jar冲突

在插件市场安装Maven Helper&#xff0c;安装好后&#xff0c;重启IDEA&#xff1b;双击打开可能存在jar冲突的pom文件&#xff1b;在右侧面板查看冲突,text是引入的依赖明细&#xff0c;点击Dependecy Analyzer选项卡即可查看冲突的jar。