当前位置: 首页 > news >正文

从UE5的坐标转换函数出发,手把手带你复现一个简易的3D拾取Demo(C++/蓝图)

从UE5坐标转换到3D拾取:实战开发全流程解析

在虚幻引擎5的交互式应用开发中,3D拾取功能是最基础也最核心的交互手段之一。无论是点击放置物体、角色选择还是UI交互,都离不开屏幕坐标到世界坐标的转换。本文将以一个完整的"点击生成物体"Demo为例,系统讲解从理论到实践的完整实现路径。

1. 环境准备与项目设置

首先创建一个全新的UE5 C++项目(选择Blank模板),确保已安装最新版本的Visual Studio和对应的虚幻引擎版本。在项目设置中,启用以下关键模块:

// YourProjectName.Build.cs PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "NavigationSystem", "AIModule" });

创建两个主要蓝图类:

  1. BP_InteractivePlayerController(继承自PlayerController)
  2. BP_SpawnableActor(继承自Actor,带静态网格组件)

在项目设置中配置默认Player Controller为我们的BP_InteractivePlayerController

2. 核心坐标转换原理剖析

UE5中的坐标转换链条可以表示为:

屏幕坐标 → NDC坐标 → 裁剪空间坐标 → 世界坐标

关键函数DeprojectScreenPositionToWorld的内部工作流程:

  1. 屏幕坐标归一化

    const float NormalizedX = (PixelX - ViewRect.Min.X) / ViewRect.Width(); const float NormalizedY = (PixelY - ViewRect.Min.Y) / ViewRect.Height();
  2. NDC空间转换

    const float ScreenSpaceX = (NormalizedX - 0.5f) * 2.0f; const float ScreenSpaceY = ((1.0f - NormalizedY) - 0.5f) * 2.0f;
  3. 逆矩阵变换

    FMatrix const InvViewProjMatrix = ProjectionData.ComputeViewProjectionMatrix().InverseFast(); FVector4 WorldSpacePos = InvViewProjMatrix.TransformFVector4(ClipSpacePos);

重要参数对比:

参数类型取值范围转换目的
屏幕坐标(0,0)到(分辨率宽,高)原始输入数据
NDC坐标[-1,1]范围标准化设备坐标系
裁剪空间坐标包含深度值(Z)投影前统一空间

3. 完整拾取系统实现

3.1 鼠标点击事件处理

在PlayerController中设置输入绑定:

// 在SetupInputComponent中 InputComponent->BindAction("LeftMouseClick", IE_Pressed, this, &ABP_InteractivePlayerController::HandleMouseClick);

点击事件处理函数:

void ABP_InteractivePlayerController::HandleMouseClick() { float MouseX, MouseY; if (GetMousePosition(MouseX, MouseY)) { FVector WorldLocation, WorldDirection; if (DeprojectScreenPositionToWorld( MouseX, MouseY, WorldLocation, WorldDirection)) { // 执行射线检测 PerformLineTrace(WorldLocation, WorldDirection); } } }

3.2 射线检测与物体生成

实现精确的射线检测:

void ABP_InteractivePlayerController::PerformLineTrace(FVector Start, FVector Direction) { FHitResult HitResult; FCollisionQueryParams TraceParams(FName(TEXT("InteractTrace")), true); if (GetWorld()->LineTraceSingleByChannel( HitResult, Start, Start + (Direction * 10000), // 10米检测距离 ECC_Visibility, TraceParams)) { SpawnActorAtHitLocation(HitResult); } else { // 未命中时在地面生成 FVector GroundPos = CalculateGroundPosition(Start, Direction); SpawnActorAtLocation(GroundPos); } }

物体生成逻辑:

void ABP_InteractivePlayerController::SpawnActorAtLocation(FVector Location) { FActorSpawnParameters SpawnParams; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; GetWorld()->SpawnActor<ABP_SpawnableActor>( SpawnableClass, Location + FVector(0,0,50), // 略微抬高 FRotator::ZeroRotator, SpawnParams); }

4. 高级功能扩展

4.1 拖拽交互实现

在PlayerController中添加拖拽状态跟踪:

// 成员变量 bool bIsDragging = false; AActor* DraggedActor = nullptr; FVector DragOffset; // 输入绑定扩展 InputComponent->BindAction("LeftMouseClick", IE_Released, this, &ABP_InteractivePlayerController::HandleMouseRelease);

拖拽更新逻辑:

void ABP_InteractivePlayerController::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (bIsDragging && DraggedActor) { float MouseX, MouseY; GetMousePosition(MouseX, MouseY); FVector WorldLocation, WorldDirection; DeprojectScreenPositionToWorld(MouseX, MouseY, WorldLocation, WorldDirection); // 计算新的位置(保持原始高度) FVector NewPos = CalculateDragPosition(WorldLocation, WorldDirection); DraggedActor->SetActorLocation(NewPos + DragOffset); } }

4.2 多物体选择与管理

实现选择高亮效果:

// 在选择时调用 void HighlightActor(AActor* Actor) { if (CurrentSelected) { CurrentSelected->GetMesh()->SetRenderCustomDepth(false); } Actor->GetMesh()->SetRenderCustomDepth(true); Actor->GetMesh()->SetCustomDepthStencilValue(252); CurrentSelected = Actor; }

物体管理数据结构:

TArray<ABP_SpawnableActor*> SpawnedActors; // 生成时记录 SpawnedActors.Add(NewActor); // 清除所有物体 for (auto Actor : SpawnedActors) { Actor->Destroy(); } SpawnedActors.Empty();

5. 性能优化与调试技巧

5.1 射线检测优化策略

使用对象查询过滤器:

FCollisionObjectQueryParams ObjectParams; ObjectParams.AddObjectTypesToQuery(ECC_WorldStatic); ObjectParams.AddObjectTypesToQuery(ECC_PhysicsBody); GetWorld()->LineTraceSingleByObjectType( HitResult, Start, End, ObjectParams, TraceParams);

异步射线检测示例:

// 在.h文件中 FTraceHandle TraceHandle; FCollisionQueryParams AsyncTraceParams; // 在.cpp中 TraceHandle = GetWorld()->AsyncLineTraceByChannel( Start, End, ECC_Visibility, AsyncTraceParams, FCollisionResponseParams::DefaultResponseParam, &TraceDelegate);

5.2 调试可视化工具

绘制调试射线:

DrawDebugLine( GetWorld(), Start, End, FColor::Green, false, // 不持久化 2.0f, // 持续时间 0, // 深度优先级 2.0f); // 线宽

显示坐标信息:

GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT("World Pos: X=%.2f, Y=%.2f, Z=%.2f"), HitResult.Location.X, HitResult.Location.Y, HitResult.Location.Z));

在开发这类交互系统时,最常遇到的坑是忽略坐标系的转换顺序。记得在一次项目中,我花了整整一天时间调试一个拾取偏移问题,最后发现是在NDC坐标转换时漏掉了Y轴的反转步骤。这种基础但关键的技术点,往往需要亲手实现几次才能真正掌握。

http://www.jsqmd.com/news/753437/

相关文章:

  • 为什么你的IAsyncEnumerable在Azure Functions中内存暴涨300%?C# 13新配置项AsyncStreamOptions.BufferCapacity正在悄悄改写GC命运
  • 65周作业
  • TTP223触摸模块的5个常见坑与避坑指南:从模式切换、电平匹配到驱动能力详解
  • C#/.NET 6下用NModbus4快速搭建Modbus TCP从站(附完整源码与ModbusPoll测试)
  • 避开MATLAB优化这些坑:fminsearch和fmincon初值设置与全局最优解搜寻指南
  • 2026 全国防水公司 TOP5 权威排名 - 企业资讯
  • 快手网页版扫码登录的Python逆向手记:我是如何‘抓’出那三个关键接口的
  • 为什么92%的C#医疗系统在FHIR 2026适配中卡在Resource Validation?——基于HL7官方Test Server压测的.NET源码级调试日志解密
  • 如何用Python快速接入Taotoken并调用多个大模型API
  • STM32MP257D异构计算模块MYC-LD25X解析与应用
  • 基于MCP协议的邮件设计自动化:AI驱动的高兼容性邮件模板生成
  • 多模态旋转位置编码原理与医疗影像应用实践
  • 企业如何利用多模型聚合能力优化内部知识问答系统
  • AI厨房管家:用Git工作流与LLM打造可复现的智能食谱系统
  • Python 爬虫高级实战:多环境爬虫配置统一管理方案
  • TCGA数据实战:用sva和limma搞定批次效应,附COAD/READ结肠癌数据完整R代码
  • Music Tag Web音乐标签编辑器:从新手到高手的完整使用指南
  • 你的LCD1602 I2C地址不对?手把手教你用Arduino IDE扫描并修复0x27/0x3F地址冲突问题
  • 普遍认为学历越高,薪资一定越高,编程整合学历,岗位,能力,业绩数据,分析学历与收入无绝对关联,打破求职固有偏见。
  • GEEKOM A5迷你主机评测:Ryzen 7 5800H性能解析
  • 如何实现单细胞数据分析:SCP端到端流程的实践指南
  • REIN方法:基于推理初始化的对话系统错误恢复技术
  • 利用 Taotoken 为 AIGC 内容生成平台提供稳定的模型供应链
  • SQL 第一篇:CRUD 实战,从 user 表开始写接口
  • 视频信号耦合技术:AC与DC耦合原理及应用对比
  • RoboMaster 2023赛季大能量机关识别:从OpenCV二值化到findContours轮廓分析,一个完整实战流程
  • 大众觉得投入资金越多生意越红火,编程统计创业投入金额与营收数据,验证小额轻资产创业回报率远超重资产模式。
  • 别再乱用include_directories了!CMake 3.x项目头文件管理,用target_include_directories更香
  • 【电力系统】中性点不接地、经消弧线圈接地发生单相接地故障Simulink仿真(仿真+说明报告)
  • 崩坏星穹铁道终极自动化指南:三月七小助手如何每天为你节省2小时?