作者|峻川 编辑|峻川 出品|车路漫漫 写在前面本文的鱼眼相机代码源自github上Carla仓库的Fisheye sensor #3755(https://github.com/carla-simulator/carla/pull/3755),正文中作为示例的是本文作者基于此改编的适配最新版Carla"Unreal Engine 5.5 version of CARLA"的Fisheye sensor代码。文末会附上改编后的代码。 鱼眼相机效果 鱼眼相机模型代码解析在Carla的文档中,有如何建立新传感器模型的教程:how-to-add-a-new-sensor,建议首先阅读链接中的文档以熟悉这一过程。和文档中一样,我们首先从新建立的Sensor Actor开始。AFisheyeSensor::GetSensorDefinition(),AFisheyeSensor::GetImageWidth(),AFisheyeSensor::GetImageHeight(),AFisheyeSensor::GetFOV()获取一些如图像分辨率、相机内参等参数,AFisheyeSensor::Set(const FActorDescription &Description)会将这些参数赋值给类内部的成员。在构造函数中,定义了一个Fisheye对象:Fisheye = CreateDefaultSubobject<USceneCaptureComponentCube>( FName(*FString::Printf(TEXT("SceneCaptureComponentCube_%d"), SCENE_CAPTURE_COUNTER_CUBE))); 1. USceneCaptureComponentCube 的作用动态捕获立方体贴图:USceneCaptureComponentCube 是用于从场景的某个位置捕获 立方体贴图(Cubemap) 的组件。它会实时渲染场景的六个面(前、后、左、右、上、下),生成一个立方体贴图。这种贴图常用于动态环境反射(如金属、水面)、天空盒效果,或需要全方向视角的场景捕捉(如VR,和本文所述的鱼眼相机)。应用场景:例如,在角色周围动态生成环境反射时,可以将 USceneCaptureComponentCube 附加到角色身上,实时捕捉周围环境,并将结果传递给材质系统,实现动态反射效果。2. SCENE_CAPTURE_COUNTER_CUBE 的作用唯一命名标识:SCENE_CAPTURE_COUNTER_CUBE 是一个无符号整型计数器(0u 表示初始值为0),用于为每个 USceneCaptureComponentCube 实例生成唯一名称。通过 FString::Printf 将其嵌入名称字符串(如 "SceneCaptureComponentCube_0"),避免多个组件因同名导致冲突。计数器的递增:通常,在每次创建组件后,计数器会递增(例如 SCENE_CAPTURE_COUNTER_CUBE++),确保后续实例的名称后缀依次为 _1、_2 等。这是Unreal中管理动态生成组件命名的常见做法。在Unreal Engine中,**USceneCaptureComponentCube::CaptureScene()** 是一个关键方法,用于 手动触发 立方体贴图(Cubemap)的场景捕获操作。以下是关于该方法的详细说明:立即捕获场景:CaptureScene() 会立即从 USceneCaptureComponentCube 的当前位置和方向,渲染场景的六个面(前、后、左、右、上、下),生成一个完整的立方体贴图,并将结果写入关联的渲染目标(TextureRenderTargetCube)。下面进入FisheyeSensor在仿真开始时会运行的方法:BeginPlay()void AFisheyeSensor::BeginPlay(){ // 省略} 这是FisheyeSensor中的第一个重要方法。以下是逐行解释:1. 初始化渲染目标CaptureRenderTarget->Init(GetImageWidth(), PF_B8G8R8A8); 作用:初始化一个渲染目标(UTextureRenderTarget2D),设置其宽度为GetImageWidth()返回的值,像素格式为PF_B8G8R8A8(32位BGRA格式,适合存储颜色数据)。细节:渲染目标用于将场景内容捕捉到纹理,后续可通过此纹理获取图像数据。2. 配置鱼眼场景捕捉组件Fisheye->Deactivate();Fisheye->TextureTarget = CaptureRenderTarget;Fisheye->CaptureScene(); 作用:停用鱼眼场景捕捉组件(如USceneCaptureComponent2D),避免未配置时自动渲染。然后将渲染目标绑定到组件,并立即触发一次场景捕捉。注意:CaptureScene()可能在组件未激活时仍可手动调用,确保首次捕捉完成。3. 设置捕捉数据源Fisheye->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR; 作用:指定捕捉的数据源为“最终颜色(低动态范围,LDR)”,即经过后期处理(如色调映射)后的画面,适合显示或存储为标准格式图像。4. 更新并激活组件Fisheye->UpdateContent();Fisheye->Activate(); 作用:UpdateContent()确保参数更改生效,Activate()启用组件,使其后续可以自动更新(如每帧捕捉)。5. 调整渲染超时设置UKis metSystemLibrary::ExecuteConsoleCommand( GetWorld(), FString("g.TimeoutForBlockOnRenderFence 300000")); 作用:通过控制台命令将渲染围栏阻塞超时设置为300秒(300000毫秒),防止高负载渲染任务(如高分辨率捕捉)因超时失败。注意:此设置全局生效,需谨慎使用,避免影响其他场景。6. 通知天气更新Weather->NotifyWeather(this); 作用:触发天气系统更新,确保场景中的天气效果(如雨、雾)被应用到当前捕捉的画面中。细节:GetEpisode()可能指向当前游戏情景,与项目逻辑相关。7. 调用父类逻辑Super::BeginPlay(); 作用:执行父类(如AActor)的BeginPlay(),完成继承链的初始化。那么,在上面的代码中,我们为什么让Fisheye先调用了Deactivate()停用,再调用CaptureScene(),再调用Activate()呢?其行为目的如下:1. 核心问题:为什么停用后还能捕捉到场景?即使调用了Deactivate(),手动调用CaptureScene()仍然有效。Deactivate()的作用是 关闭组件的自动更新逻辑(如Tick或依赖引擎自动触发的渲染),但不会阻止显式调用CaptureScene()这种主动请求的捕获操作。关键区别:自动行为(如bCaptureEveryFrame=true):依赖组件的激活状态(IsActive()),停用后自动捕获停止。手动行为(如CaptureScene()):直接执行,与激活状态无关。因此,代码中Deactivate()的目的是 避免自动更新干扰,而非禁用所有功能。2. 代码流程的详细解释Fisheye->Deactivate(); // 步骤1:停用自动逻辑Fisheye->TextureTarget = CaptureRenderTarget; // 步骤2:绑定渲染目标Fisheye->CaptureScene(); // 步骤3:手动触发捕获Fisheye->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR; // 步骤4:设置捕获源Fisheye->UpdateContent(); // 步骤5:强制更新配置Fisheye->Activate(); // 步骤6:重新激活组件 步骤1:Deactivate()的作用关闭自动更新:防止组件因bCaptureEveryFrame=true或其他引擎机制自动触发捕获。避免中间状态干扰:确保在配置参数(如TextureTarget、CaptureSource)的过程中,组件不会意外执行未完全初始化的捕获操作。步骤3:CaptureScene()的调用立即执行捕获:无论组件是否激活,手动调用会直接渲染场景到TextureTarget。确保参数已设置:在调用前已绑定TextureTarget,因此捕获结果会正确写入目标资源。步骤5:UpdateContent()的必要性同步配置到GPU:某些属性(如CaptureSource)的修改可能需要通过UpdateContent()强制更新渲染代理(Proxy),确保后续操作使用最新参数。避免延迟生效:Unreal的渲染线程可能异步处理参数更新,此方法确保立即生效。步骤6:Activate()的恢复重启自动逻辑:如果后续需要组件自动更新(如恢复bCaptureEveryFrame=true),需重新激活。组件状态管理:保持组件在完成手动操作后处于一致的激活状态。3. 这样做的目的(1) 确保手动捕获的稳定性防止自动捕获干扰:若组件原本是激活状态且bCaptureEveryFrame=true,直接修改参数可能导致捕获过程中途被自动更新打断,产生不完整或冲突的结果。原子化操作:通过Deactivate()冻结组件的自动行为,确保参数修改(TextureTarget、CaptureSource)和手动捕获(CaptureScene())是一个完整且不可中断的流程。(2) 避免资源竞争渲染目标安全绑定:在停用状态下绑定TextureTarget,可避免渲染线程正在使用该资源时发生冲突(如前一帧未完成的捕获仍在写入纹理)。(3) 兼容后续自动更新恢复初始状态:操作完成后重新Activate(),确保组件后续可以正常参与引擎的更新循环(如需要持续自动捕获)。4. 典型应用场景动态切换渲染目标需要临时更换TextureTarget并立即捕获一次,但不希望自动更新持续写入新目标。示例:根据玩家位置切换不同环境贴图的捕获目标。编辑器工具链在非游戏运行时(如编辑器脚本中),通过手动控制捕获流程生成静态环境贴图,避免自动更新浪费性能。复杂参数配置需按顺序设置多个属性(如CaptureSource、后期处理参数),并要求这些参数在捕获时完全生效。在AFisheyeSensor中,每一步鱼眼图像渲染的执行是在AFisheyeSensor::PrePhysTick()方法中实现的,其中最核心的方法就是以下函数:template <typename TSensor>void AFisheyeSensor::SendPixelsInRenderThread(TSensor &Sensor, float MaxAngle, float SizeX, float SizeY, float Fx, float Fy, float Cx, float Cy, float D1, float D2, float D3, float D4){ // 省略} 以下是代码的详细分步解释:1. 函数功能概述其核心功能是:在渲染线程中将立方体贴图(Cubemap)转换为鱼眼图像,并通过Carla传感器数据流发送出去。2. 关键代码解析步骤1:参数检查与初始化check(Sensor.CaptureRenderTarget != nullptr);UTextureRenderTargetCube *RenderTarget = Sensor.CaptureRenderTarget; 目的:确保渲染目标有效,避免空指针崩溃。**UTextureRenderTargetCube**:Unreal中存储立方体贴图的资源,通常由USceneCaptureComponentCube捕获生成。步骤2:准备数据容器TArray64<uint8> RawData; // 存储鱼眼图像原始像素数据FIntPoint Size; // 图像尺寸(输出参数)EPixelFormat Format; // 像素格式(输出参数) **TArray64<uint8>**:支持大尺寸(64位)的字节数组,用于容纳高分辨率图像数据。步骤3:配置鱼眼参数CubemapHelpersFisheye::FFisheyeParams FisheyeParams;FisheyeParams.ImageSize = FIntPoint(SizeX, SizeY); // 鱼眼图像分辨率FisheyeParams.CameraMatrix = FVector4(Fx, Fy, Cx, Cy); // 相机内参矩阵FisheyeParams.DistortionCoeffs = FVector4(D1, D2, D3, D4); // 畸变系数(k1, k2, k3, k4)FisheyeParams.MaxAngle = MaxAngle; // 鱼眼最大视场角(FOV) 参数作用:内参矩阵:将3D点投影到2D图像平面(fx/fy为焦距,cx/cy为主点坐标)。畸变系数:模拟镜头畸变效果。MaxAngle:控制鱼眼图像的视野范围(例如180度全景)。步骤4:立方体贴图展开为鱼眼图像bool bUnwrapSuccess = CubemapHelpersFisheye::GenerateLongLatUnwrapFisheye( RenderTarget, RawData, FisheyeParams, Size, Format); 功能:将立方体贴图的六个面展开为 等距柱状投影(Equirectangular Projection),再通过鱼眼模型转换为 鱼眼图像。关键操作:立方体贴图 → 等距柱状投影:生成360°全景图。等距柱状投影 → 鱼眼投影:根据内参和畸变系数裁剪并变形为鱼眼图像。输出:RawData:存储鱼眼图像的像素数据(RGB或灰度)。Size:图像实际尺寸(可能与输入SizeX/Y不同,取决于算法)。Format:像素格式(如PF_R8G8B8A8)。步骤5:获取Carla传感器数据流auto Stream = Sensor.GetDataStream(Sensor); **GetDataStream**:Carla框架中获取传感器数据流的方法,用于将数据发送到客户端或日志系统。步骤6:数据封装与发送auto Buffer = Stream.PopBufferFromPool(); // 从缓冲池获取内存块// 将鱼眼数据拷贝到缓冲区Buffer.copy_from(carla::sensor::SensorRegistry::get<TSensor *>::type::header_offset, RawData.GetData(), RawData.Num());// 序列化数据carla::Buffer BufferReady(std::move(carla::sensor::SensorRegistry::Serialize(Sensor, std::move(Buffer))));carla::SharedBufferView BufView = carla::BufferView::CreateFrom(std::move(BufferReady));SCOPE_CYCLE_COUNTER(STAT_CarlaSensorStreamSend);TRACE_CPUPROFILER_EVENT_SCOPE_STR("Stream Send");// 发送数据Stream.Send(Sensor, BufView); 关键过程:缓冲池复用:PopBufferFromPool 避免频繁内存分配,提升性能。数据拷贝:将RawData写入缓冲区,跳过可能的头部预留空间(header_offset)。序列化:将数据按Carla协议打包(可能包含时间戳、传感器ID等元数据)。网络发送:通过Stream.Send将数据发送到Carla客户端或日志。3. 代码设计要点线程安全性渲染线程执行:函数名 SendPixelsInRenderThread 指示其在渲染线程调用,确保对渲染资源(如UTextureRenderTargetCube)的安全访问。性能优化缓冲池机制:PopBufferFromPool 减少内存分配开销。数据零拷贝:RawData.GetData() 直接访问内存,避免额外复 制。异步发送:Stream.Send 可能非阻塞,避免阻塞渲染线程。扩展性模板化设计:template <typename TSensor> 支持多种传感器类型复用相同逻辑。参数化配置:相机参数(内参、畸变)通过函数参数传入,适配不同镜头模型。在AFisheyeSensor::SendPixelsInRenderThread()这个函数中,最关键的是对于CubemapHelpersFisheye::GenerateLongLatUnwrapFisheye方法的调用。CubemapHelpersFisheye::GenerateLongLatUnwrapFisheye来自UnrealEngine5_carla的Engine\Source\Runtime\Engine\Private\CubemapUnwrapUtils.cpp,是根据原有的CubemapHelpers编写的针对鱼眼相机仿真的新方法。我们首先来看核心函数GenerateLongLatUnwrapFisheye的解析:1. 函数功能概述GenerateLongLatUnwrapFisheye 是Unreal引擎中用于将 立方体贴图(Cubemap)转换为鱼眼图像 的核心函数。其通过 GPU渲染管线 实现以下功能:立方体贴图展开:将立方体贴图的六个面展开为 等距柱状投影(Equirectangular Projection)。鱼眼畸变模拟:根据相机参数(内参、畸变系数、最大视场角)对展开的全景图进行变形,生成鱼眼图像。像素数据提取:将最终图像数据从GPU读取到CPU内存,供后续处理或传输。2. 关键代码解析1. 参数初始化与渲染配置// 创建自定义的渲染参数(鱼眼变形逻辑)TRefCountPtr<FBatchedElementParameters> BatchedElementParameters;BatchedElementParameters = new FMipLevelBatchedElementParametersFisheye( (float)0, FisheyeParams.DistortionCoeffs, FisheyeParams.CameraMatrix, FisheyeParams.ImageSize, FisheyeParams.MaxAngle, true);// 选择目标像素格式(LDR或HDR)const EPixelFormat TargetPixelFormat = CalculateImageBytes(1, 1, 0, SourcePixelFormat) <= 4 ? PF_B8G8R8A8 : PF_FloatRGBA; **FMipLevelBatchedElementParametersFisheye**:自定义的渲染参数类,负责在渲染时注入 鱼眼变形着色器逻辑,将等距柱状投影转换为鱼眼图像。其参数包括:DistortionCoeffs:镜头畸变系数(k1, k2, k3, k4)。CameraMatrix:相机内参(fx, fy, cx, cy)。MaxAngle:最大视场角(控制鱼眼范围)。像素格式选择:根据输入纹理的位宽选择目标格式(LDR用PF_B8G8R8A8,HDR用PF_FloatRGBA)。2. 渲染目标创建与初始化// 创建2D渲染目标(用于存储鱼眼图像)UTextureRenderTarget2D* RenderTargetLongLat = NewObject<UTextureRenderTarget2D>();RenderTargetLongLat->AddToRoot(); // 防止被GC回收RenderTargetLongLat->ClearColor = FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);RenderTargetLongLat->InitCustomFormat(FisheyeParams.ImageSize.X, FisheyeParams.ImageSize.Y, TargetPixelFormat, false);RenderTargetLongLat->TargetGamma = 0;FRenderTarget* RenderTarget = RenderTargetLongLat->GameThread_GetRenderTargetResource();// 创建画布并绑定渲染目标FCanvas* Canvas = new FCanvas(RenderTarget, NULL, FGameTime(), GMaxRHIFeatureLevel);Canvas->SetRenderTarget_GameThread(RenderTarget);Canvas->Clear(FLinearColor(0, 0, 0, 0)); // 清空为透明 **UTextureRenderTarget2D**:2D渲染目标,用于存储中间渲染结果。**AddToRoot**:防止Unreal垃圾回收器(GC)误删资源。**FCanvas**:Unreal的2D画布工具,允许在游戏线程操作GPU渲染。3. 绘制立方体贴图并应用鱼眼变形// 将立方体贴图绘制到渲染目标,并应用鱼眼参数FCanvasTileItem TileItem(FVector2D(0.0f, 0.0f), TextureResource, FVector2D(FisheyeParams.ImageSize.X, FisheyeParams.ImageSize.Y), FLinearColor::White);TileItem.BatchedElementParameters = BatchedElementParameters; // 注入鱼眼逻辑TileItem.BlendMode = SE_BLEND_Opaque;Canvas->DrawItem(TileItem);// 提交渲染命令并等待完成Canvas->Flush_GameThread();FlushRenderingCommands(); **FCanvasTileItem**:将立方体贴图作为纹理平铺到整个渲染目标。**BatchedElementParameters**:关键参数,绑定自定义的鱼眼变形逻辑(通过Unreal的FBatchedElementParameters机制注入到着色器)。**FlushRenderingCommands**:强制同步渲染线程,确保所有GPU操作完成。4. 读取像素数据到内存// 分配内存并读取渲染结果BitsOUT.AddUninitialized(ImageBytes);bool bReadSuccess = false;switch (TargetPixelFormat){ case PF_B8G8R8A8: // LDR格式 bReadSuccess = RenderTarget->ReadPixelsPtr((FColor*)BitsOUT.GetData()); break; case PF_FloatRGBA: // HDR格式 { TArray<FFloat16Color> FloatColors; bReadSuccess = RenderTarget->ReadFloat16Pixels(FloatColors); FMemory::Memcpy(BitsOUT.GetData(), FloatColors.GetData(), ImageBytes); } break;} **ReadPixelsPtr / ReadFloat16Pixels**:从GPU读取像素数据到CPU内存。内存拷贝:HDR格式需要将FFloat16Color数组转换为字节流。5. 资源清理与输出// 释放资源RenderTargetLongLat->ReleaseResource();RenderTargetLongLat->RemoveFromRoot();RenderTargetLongLat = NULL;delete Canvas;// 设置输出参数if (!bReadSuccess) BitsOUT.Empty(); return bReadSuccess;SizeOUT = FisheyeParams.ImageSize;FormatOUT = TargetPixelFormat;if (bReadSuccess == false){ // 失败时清空数据 BitsOUT.Empty();}return bReadSuccess; 资源管理:显式释放渲染目标和画布,避免内存泄漏。错误处理:若读取失败,清空输出缓冲区。3. 关键技术与设计分析1. 鱼眼变形实现原理自定义着色器注入:通过FMipLevelBatchedElementParametersFisheye向Unreal的渲染管线注入自定义的顶点/像素着色器逻辑。2. 线程与资源安全游戏线程操作:所有FCanvas和UTextureRenderTarget2D操作均在游戏线程执行,通过FlushRenderingCommands同步渲染线程,确保GPU操作完成。资源生命周期:AddToRoot/RemoveFromRoot显式控制渲染目标的GC生命周期,防止异步渲染过程中资源被释放。3. 性能优化格式自适应:根据输入纹理位宽选择LDR/HDR格式,平衡精度与内存。GPU加速:变形计算完全在GPU执行,避免CPU计算瓶颈。内存池复用:外部调用方可能通过TArray64<uint8>& BitsOUT复用内存,减少分配开销。下面我们来看FMipLevelBatchedElementParametersFisheye绑定着色器shader的方法:template<typename TPixelShader>void FMipLevelBatchedElementParametersFisheye::BindShaders(FRHICommandList& RHICmdList, FGraphicsPipelineStateInitializer& GraphicsPSOInit, ERHIFeatureLevel::Type InFeatureLevel, const FMatrix& InTransform, const float InGamma, const FMatrix& ColorWeights, const FTexture* Texture){ // 省略} 1. 功能概述此函数是 具体执行着色器绑定和参数传递 的核心实现,负责将 顶点着色器(VS)和像素着色器(PS) 绑定到图形管线,并传递渲染所需的参数(如畸变系数、相机矩阵等)。它是在代码中前文出现的BindShaders函数的具体实现模板。2. 关键代码逐行分析template<typename TPixelShader>void FMipLevelBatchedElementParametersFisheye::BindShaders(...){ // 1. 设置混合状态(不混合,直接覆盖) GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); // 2. 获取着色器实例 TShaderMapRef<FCubemapTexturePropertiesVSFisheye> VertexShader(...); // 顶点着色器 TShaderMapRef<TPixelShader> PixelShader(...); // 模板化的像素着色器 // 3. 配置管线状态 GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GSimpleElementVertexDeclaration.VertexDeclarationRHI; // 顶点声明 GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); // 绑定VS GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); // 绑定PS GraphicsPSOInit.PrimitiveType = PT_TriangleList; // 图元类型为三角形列表 // 4. 提交管线状态到GPU RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); // 5. 传递参数到着色器 SetShaderParametersLegacyVS(RHICmdList, VertexShader, InTransform); // VS参数(变换矩阵) SetShaderParametersLegacyPS(RHICmdList, PixelShader, Texture, ColorWeights, MipLevel, InGamma, DistortionCoeffs, CameraMatrix, ImageSize, MaxAngle ); // PS参数(包含鱼眼关键参数)} 3. 核心设计解析(1)模板化像素着色器**TPixelShader**:允许传入不同的像素着色器类(如FCubemapTexturePropertiesPSFisheye<true>和<false>),在 编译时生成多份代码,避免运行时分支判断。变体控制:通过模板参数传递HLSL宏定义(如HDR_OUTPUT),实现 同一份HLSL代码的不同编译版本。(2)固定顶点着色器**FCubemapTexturePropertiesVSFisheye**:所有鱼眼变体共享相同的顶点处理逻辑(如坐标变换),无需模板化,减少冗余代码。(3)参数传递架构参数类型 传递方式 对应HLSL变量 InTransform顶点着色器SetParameters 变换矩阵(FMatrix) DistortionCoeffs像素着色器SetParameters DistortionCoeffs (float4) CameraMatrix像素着色器SetParameters CameraMatrix (float4) MaxAngle像素着色器SetParameters MaxAngle (float) 4. 渲染管线配置细节(1)混合状态设置GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); **TStaticBlendState<>**:表示无混合操作(BlendOp=Add, SrcBlend=One, DestBlend=Zero),直接覆盖目标颜色,适合不透明渲染。(2)图元类型GraphicsPSOInit.PrimitiveType = PT_TriangleList; 三角形列表:将顶点数据解释为独立三角形,用于渲染全屏四边形或简单几何体。(3)顶点声明GSimpleElementVertexDeclaration.VertexDeclarationRHI 简单顶点格式:通常包含位置(POSITION)和纹理坐标(TEXCOORD0),匹配全屏四边形的输入结构。5. 与HLSL代码的交互顶点着色器参数传递VertexShader->SetParameters(RHICmdList, InTransform); 对应HLSL中的顶点变换矩阵:float4x4 TransformMatrix;像素着色器参数传递PixelShader->SetParameters(RHICmdList, DistortionCoeffs, ...); 对应鱼眼着色器参数:float4 DistortionCoeffs;float4 CameraMatrix;float MaxAngle;在代码中,我们使用的顶点着色器是UE在SimpleElementVertexShader.usf中原有的顶点着色器:IMPLEMENT_SHADER_TYPE(,FCubemapTexturePropertiesVSFisheye,TEXT("/Engine/Private/SimpleElementVertexShader.usf"),TEXT("Main"),SF_Vertex); 而鱼眼图像的像素转换过程则依靠在UE的SimpleElementPixelShader.usf中新添加的像素着色器。首先解释下什么是前面提到的HLSL,这是在usf文件中使用的编程语言:HLSL 详解HLSL(High-Level Shader Language,高级着色器语言)是微软为 DirectX 图形API设计的一种编程语言,主要用于编写在GPU上运行的 着色器程序(Shader)。它是实现复杂图形效果的核心工具,广泛应用于游戏开发、影视渲染和实时图形处理。1. HLSL 的核心作用(1)定义图形渲染流程通过编写不同类型的着色器,控制GPU如何处理几何体、光照、纹理和像素:顶点着色器(Vertex Shader):处理3D模型的顶点位置、法线、UV坐标等。像素着色器(Pixel Shader / Fragment Shader):计算每个像素的最终颜色。几何着色器(Geometry Shader):动态生成或修改几何图元(如细分曲面)。计算着色器(Compute Shader):执行通用计算任务(如物理模拟、图像处理)。(2)实现高级图形效果光照与阴影:如动态全局光照(GI)、软阴影、环境光遮蔽(SSAO)。材质与纹理:如PBR材质(金属/粗糙度)、法线贴图、视差映射。后处理效果:如景深、运动模糊、HDR色调映射。特殊投影:如我们本文讨论的鱼眼镜头畸变模拟。2. HLSL 的语法特点(1)类C语法结构与C语言类似,支持函数、条件语句、循环等,但包含图形专用类型和语义:// 示例:简单的像素着色器float4 MainPS(float2 UV : TEXCOORD0) : SV_Target{ float4 Color = Texture2D.Sample(TextureSampler, UV); return Color * LightIntensity;} (2)图形专用数据类型类型 描述 示例 float , half 单精度/半精度浮点数 float3 position;int , uint 整数类型 uint vertexID;float2 , float3x3 向量与矩阵 float4 color = {1,0,0,1};Texture2D2D纹理 Texture2D DiffuseMap;SamplerState纹理采样器(过滤/寻址模式) SamplerState LinearWrap;(3)语义绑定(Semantics)通过语义标签将变量与渲染管线数据关联:// 顶点着色器输入struct VSInput{ float3 Position : POSITION; // 顶点位置 float2 UV : TEXCOORD0; // 纹理坐标};// 像素着色器输出float4 MainPS() : SV_Target // 输出到渲染目标{ return float4(1,0,0,1);} 3. HLSL 在 Unreal 引擎中的应用(1)材质系统Unreal的 材质编辑器 底层通过HLSL代码生成最终的着色器。用户可以通过节点图可视化编程,或直接编写 Custom Node 注入HLSL代码。(2)自定义着色器开发者可以直接编写.usf(Unreal Shader File)文件,实现复杂效果(如我们分析的鱼眼投影函数):// 示例:鱼眼畸变计算void CubemapTexturePropertiesFisheye(float2 UV){ float2 NormCoord = (UV - CameraMatrix.zw) / CameraMatrix.xy; // 迭代求解畸变模型...} (3)渲染管线扩展通过修改引擎的 Shader 代码,可以自定义延迟渲染、光线追踪等高级管线。最后我们终于来到了本文最核心的部分:shader文件SimpleElementPixelShader.usf,该Shader文件是Unreal简单元素渲染系统的核心组件,通过高度优化的HLSL代码实现了从基础纹理混合到复杂鱼眼投影的多功能渲染。其中的鱼眼投影函数CubemapTexturePropertiesFisheye是本文讨论的重点:1. 参数定义与物理意义参数名 类型 物理意义 对应现实世界中的标定参数 DistortionCoeffsfloat4鱼眼镜头畸变系数(k₁, k₂, k₃, k₄),用于描述径向畸变的非线性多项式模型。 相机标定中的畸变参数(OpenCV/Kalibr格式) CameraMatrixfloat4相机内参矩阵,存储焦距和主点坐标(f_x, f_y, c_x, c_y)。 内参矩阵:[f_x, 0, c_x; 0, f_y, c_y; 0, 0, 1] MaxAnglefloat鱼眼镜头最大视场角(单位:度),控制有效成像区域的角度范围。 例如:180°、220° 2. 计算流程与参数映射以下是函数内部关键计算步骤与参数的对应关系:步骤1:像素坐标归一化与去主点偏移TextureCoordinate.x = (TextureCoordinate.x - c_x) / f_x;TextureCoordinate.y = (TextureCoordinate.y - c_y) / f_y; 参数作用:c_x, c_y(来自CameraMatrix):将像素坐标从图像坐标系(以左上角为原点)转换到以光心为中心的归一化坐标系。f_x, f_y(来自CameraMatrix):将像素坐标缩放为无单位的归一化坐标(类似于OpenCV的“理想像素”坐标系)。数学对应:步骤2:畸变模型迭代计算float r = sqrt(x*x + y*y);float theta = r;for (int j = 0; j < 10; j++) { float th2 = theta * theta; float th4 = th2 * th2; float th6 = th4 * th2; float th8 = th6 * th2; theta = r / (1.0 + d1*th2 + d2*th4 + d3*th6 + d4*th8);} 参数作用:d1, d2, d3, d4(来自DistortionCoeffs):定义畸变多项式权重,通过牛顿迭代法反向求解无畸变半径。数学模型:通过10次迭代反向求解:步骤3:球面坐标与方向向量映射float lambda = atan2(y, x);float phi = PI/2 - theta;float3 Direction = float3(cos(phi)*cos(lambda), -cos(phi)*sin(lambda), sin(phi)); 参数作用:theta(来自迭代结果):无畸变归一化半径,转换为极角(Polar Angle)。几何映射:lambda:方位角(Azimuth Angle),从x轴逆时针旋转。phi:天顶角(Zenith Angle),从z轴向下倾斜。方向向量:将极坐标转换为3D单位向量,用于立方体贴图采样。步骤4:最大视场角限制与边缘柔化float theta_f = MaxAngle * PI / 360.0f; // 转换为半角弧度BaseColor *= (1 - s moothstep(theta_f - 0.2, theta_f, theta)); 参数作用:MaxAngle:定义有效成像的最大半角(例如180°对应theta_f = PI/2)。技术实现:使用s moothstep在[theta_f-0.2, theta_f]范围内创建渐变透明度,避免硬截断导致的锯齿。3. 参数对最终图像的影响参数调整 图像变化效果 应用场景示例 增大MaxAngle扩大鱼眼图像的视野范围,捕捉更广的环境,边缘拉伸更明显。 超广角监控摄像头模拟 增加d1(k₁)增强桶形畸变(中心膨胀,边缘压缩),图像中心区域放大。 低质量鱼眼镜头模拟 调整f_x, f_y改变焦距:f_x减小 → 水平视场角增大;f_x增大 → 水平视场角减小。 不同焦距镜头的标定参数测试 偏移c_x, c_y图像中心点偏移,模拟光轴与图像传感器中心不对齐的镜头。 非对称安装的镜头校正 4. 完整数据流与参数传递flowchart TD C++[C++侧参数设置] --> |DistortionCoeffs| HLSL(d1, d2, d3, d4) C++ --> |CameraMatrix| HLSL(f_x, f_y, c_x, c_y) C++ --> |MaxAngle| HLSL(theta_f) HLSL --> 像素坐标校正["归一化坐标 (x, y)"] 像素坐标校正 --> 畸变迭代["迭代求解 theta"] 畸变迭代 --> 球面映射["方向向量 Direction"] 球面映射 --> 立方体采样["采样 Cubemap"] 立方体采样 --> 边缘柔化["应用 MaxAngle 渐变"] 边缘柔化 --> 输出颜色["OutColor"] 通过以上这种CubemapTexture展开做鱼眼相机投影的方法产生鱼眼相机图像,仍然没有解决的问题是,六面体Cubemap展开时,面与面的接缝有时会出现在图像中,如下图所示:根据文档how-to-add-a-new-sensor,接下来是编制Sensor data serializer,Sensor data object,在SensorRegistry.h中注册你的新传感器等环节,本文提供的代码在这些部分中基本都是参照Carla原有传感器的代码进行编写的,读者可自行对照文末提供的代码进行分析。在CarlaUE5\PythonAPI\examples提供的示例py文件cam_test_0313.py中,有调用这种Fisheye Sensor的例子。代码验证本文适配的CarlaUE5和UnrealEngine5_carla的确切版本是:CarlaUE5:commit 0bfb05bfdde8435bfecfccdef8a0e05041b22aef (origin/ue5-dev, origin/HEAD)Author: MarcelPiNacy-CVC <169088301+MarcelPiNacy-CVC@users.noreply.github.com>Date: Mon Mar 10 14:24:37 2025 +0100 Remove MakeFile. (#8739)UnrealEngine5_carla:commit eea6a8e677e3509a3e56122d69e3d380bb624bbf (HEAD -> ue5-dev-carla, origin/ue5-dev-carla, origin/HEAD)Author: Marcel Pi <169088301+MarcelPiNacy-CVC@users.noreply.github.com>Date: Fri Feb 28 12:53:51 2025 +0100 Add missing chrono include. 编译CarlaUE5和UnrealEngine5_carla的方法在官方文档中很清晰了。在CarlaUE的编译脚本中,会下载Unreal\CarlaUnreal\Content目录下的大文件(约70G),见文末百度云链接。读者在编译前可提前下载,并放置在对应位置。首次编译成功,可以运行Carla后,如果对UE和Carla的代码进行了修改,需要对UnrealEngine5_carla和CarlaUE5分别再次进行编译:# UE5:./Setup.sh./GenerateProjectFiles.shmake# CarlaUE5:./CarlaSetup.sh --interactive --python-root=/home/vipuser/.conda/envs/carla/bin 最新版本的CarlaUE5可以通过--python-root参数指定需要的python虚拟环境。CarlaSetup.sh中的参数launch修改为1可以在运行脚本后打开UE窗口。点击UE窗口中的Run后,运行PythonAPI\examples\cam_test_0313.py,会将Fisheye生成的鱼眼图像保存在py脚本中写出的保存路径(请先按需修改)。来源:车路漫漫