
文 | 峻川
出品 | 车路漫漫
正文共: 9232字 7图
预计阅读时间: 24分钟
大家好,我是峻川。
之前给大家分享了鱼眼摄像头的仿真:《Carla传感器模型深度解析——鱼眼相机篇》,本期继续分享激光雷达的仿真。
本文的激光雷达代码完全源自github上的开源项目PCSim: LiDAR Point Cloud Simulation and Sensor Placement,可以实现在Carla中模拟多种激光射线扫描类型的激光雷达和多次回波等效果。正文中作为示例的是本文作者基于此改编的适配最新版Carla"Unreal Engine 5.5 version of CARLA"的激光雷达代码。文末会附上改编后的代码。
具体这些区别于Carla自带激光雷达的Lidar特殊效果的原理请参考以上链接中的论文,本文只聚焦于代码的适配。
创作不易,如果对你有所帮助,还请帮忙点个赞。
在Carla的文档中,有如何建立新传感器模型的教程:how-to-add-a-new-sensor,建议首先阅读链接中的文档以熟悉这一过程。
Carla中激光雷达代码的核心,是Unreal/CarlaUnreal/Plugins/Carla/Source/Carla/Sensor/RayCastSemanticLidar中的ARayCastSemanticLidar这个Sensor Actor。该ARayCastSemanticLidar的代码实现了Carla中的语义激光雷达传感器,支持多种扫描模式。以下是关键部分的解读:
FLidarDescription:存储激光雷达参数(通道数、FOV、扫描频率等)FSemanticLidarData:存储采集的点云数据(包含语义信息)LaserAngles:存储各激光束的垂直角度初始化阶段:
Set()方法解析蓝图参数CreateLasers()创建激光束角度配置每帧处理(PostPhysTick):

射线投射核心逻辑:
bool ShootLaser(...) {
1. 计算激光发射方向
2. 执行物理射线检测
3. 处理反射(EnableGhost时):
if(击中高反射物体 && 随机数>0.5){
计算反射向量
发射二次射线
合并反射点数据
}
4. 返回检测结果
}
并行处理:
ParallelFor(ChannelCount, [&](int32 idxChannel) {
// 每个通道独立进行射线检测
for(每个扫描点){
计算垂直/水平角度
执行ShootLaser
记录命中点
}
});
语义信息采集:
Detection.object_tag = HitInfo.Component->CustomDepthStencilValue;
// 通过UE的CustomDepthStencil获取物体语义标签
多路径反射模拟(EnableGhost):
并行计算:使用UE的ParallelFor并行处理多个通道
内存预分配:
PointsPerChannel.resize(Description.Channels);
RecordedHits.reserve(MaxPointsPerChannel);
物理场景锁:
GetPxScene()->lockRead();
// 物理计算...
GetPxScene()->unlockRead();
#if WITH_ROS2
ROS2->ProcessDataFromSemanticLidar(...);
// 转换坐标系并发布ROS2消息
#endif
实现与ROS2的数据对接,支持自动驾驶系统集成。
其中的重点函数:
void ARayCastSemanticLidar::SimulateLidar(const float DeltaTime) 是语义激光雷达的核心模拟函数,负责不同扫描模式的点云生成逻辑。以下是对该函数的详细解析:

TRACE_CPUPROFILER_EVENT_SCOPE(ARayCastSemanticLidar::SimulateLidar);
if((Description.LidarType == "default") || (Description.LidarType == "Surround"))
{
// 机械式扫描逻辑
}
else if (Description.LidarType == "Solid_state")
{
// 固态扫描逻辑
}
else if (Description.LidarType == "Risley_pris m")
{
// 棱镜式扫描逻辑
}
Description 结构体包含雷达配置参数// 计算每通道点数
const uint32 PointsToScanWithOneLaser =
FMath::RoundHalfFromZero((Description.PointsPerSecond * DeltaTime)/ChannelCount);
// 角度计算
constfloat AngleDistanceOfTick = Description.RotationFrequency * HorizontalFov * DeltaTime;
constfloat AngleDistanceOfLaserMeasure = AngleDistanceOfTick / PointsToScanWithOneLaser;
// 并行射线检测
ParallelFor(ChannelCount, [&](int32 idxChannel) {
for (auto idxPts = 0u; idxPts < PointsToScanWithOneLaser; idxPts++) {
constfloat VertAngle = LaserAngles[idxChannel];
constfloat HorizAngle = ...;
if (ShootLaser(...)) {
WritePointAsync(...);
}
}
});
// 保存数据
ComputeAndSaveDetections(ActorTransf);
核心算法:
AngleDistanceOfTick 计算每帧水平旋转角度AngleDistanceOfLaserMeasure 实现角度细分ParallelFor 多线程处理不同通道典型参数值(示例):
PointsPerSecond = 1,000,000 // 每秒百万点
RotationFrequency = 10 // 10Hz旋转
HorizontalFov = 360 // 水平视场角
// 环形缓冲区管理
const int Currentidx = SemanticLidarData.GetidxPtsOneLaser();
const int Lastidx = (Currentidx + PointsToScan) % (TotalPoints/ChannelCount);
// 角度获取
float VertAngle = Description.vfov[(idxPts+Currentidx) % TotalPoints];
float HorizAngle= Description.hfov[(idxPts+Currentidx) % TotalPoints];
// 数据持久化
SemanticLidarData.SetidxPtsOneLaser(Lastidx);
核心特点:
hfov和vfov数组定义扫描路径扫描模式示例:
hfov = [0, 1, 2, ..., 359] // 水平角度序列
vfov = [5,5,5,...,5] // 固定垂直角度(模拟Flash雷达)
// CSV数据读取
float Azimuth = Description.livox_csv_info[i][1];
float Height = Description.livox_csv_info[i][2] - 90;
// 时间戳生成
float l_timestamp = Livox_loop_count * 4.0 + simTime;
// 反射模拟
if(Reflectivity[objtag] > 0.5 && random > 0.5) {
FVector Exitvector = FMath::GetReflectionVector(...);
// 二次射线检测
}
关键技术:
Livox_loop_count 记录扫描周期数典型CSV结构:
Time(s), Azimuth(deg), Elevation(deg)
0.0, 45.2, 30.1
0.1, 46.8, 29.8
...
// 物理场景锁定
GetWorld()->GetPhysicsScene()->GetPxScene()->lockRead();
// 并行处理...
GetWorld()->GetPhysicsScene()->GetPxScene()->unlockRead();
// 数据转换
FTransform ActorTransf = GetTransform();
ComputeAndSaveDetections(ActorTransf);
lockRead/unlockRead保证物理查询线程安全并行优化:
ParallelFor(ChannelCount, [&](int32 idxChannel) {
// 通道级并行
});
内存管理:
RecordedHits.reserve(MaxPointsPerChannel); // 预分配内存
SemanticLidarData.ResetMemory(...); // 内存复用
射线检测优化:
GetWorld()->ParallelLineTraceSingleByChannel(...); // 批量射线检测
扫描模式扩展:
else if (Description.LidarType == "Custom") {
// 实现新的扫描模式
}
噪声注入:
// 在ShootLaser中添加噪声
detection.point += FMath::RandRange(-0.1f, 0.1f);
自定义材质系统:
Reflectivity[objtag] = GetMaterialReflectivity(HitResult);
// 每帧调用时序
World.Tick()
→ PostPhysTick()
→ SimulateLidar()
→ ShootLaser() x N
→ ComputeAndSaveDetections()
→ DataStream.SerializeAndSend()
→ ROS2.ProcessData()
该函数通过精细的扫描模式分派、高效的并行计算和灵活的数据处理,实现了多模式语义激光雷达的实时仿真,是Carla传感器系统的核心组件之一。
另一个关键函数:bool ARayCastSemanticLidar::ShootLaser(...)
这是激光雷达的核心射线投射函数,负责:

// 计算射线方向
FTransform ActorTransf = GetTransform();
FVector LidarBodyLoc = ActorTransf.GetLocation();
FRotator LidarBodyRot = ActorTransf.Rotator();
FRotator LaserRot(VerticalAngle, HorizontalAngle, 0);
FRotator ResultRot = UKis metMathLibrary::ComposeRotators(LaserRot, LidarBodyRot);
FVector EndTrace = Description.Range * UKis metMathLibrary::GetForwardVector(ResultRot) + LidarBodyLoc;
// 执行射线检测
GetWorld()->ParallelLineTraceSingleByChannel(
HitInfo,
LidarBodyLoc,
EndTrace,
ECC_GameTraceChannel2, // 自定义碰撞通道
TraceParams
);
ParallelLineTraceSingleByChannel并行射线检测ECC_GameTraceChannel2通常用于激光雷达专用检测// 反射率检查
std::vector<float> Reflectivity = {0.0,...,0.80,...};
if(Reflectivity[objtag] > 0.5) {
// 50%概率触发反射
float random = std::rand()%(999+1)/(float)(999+1);
if(random > 0.5) {
// 计算反射向量
FVector Exitvector = FMath::GetReflectionVector(UnitPoint, HitResult.ImpactNormal);
// 二次射线检测
FVector EndTrace1 = ReflectRange * Exitvector + ExitPoint;
GetWorld()->ParallelLineTraceSingleByChannel(...);
// 修正反射点坐标
HitPoint = FirstHitPoint + Incidencevector * distance/Incidencedistance;
}
}
CustomDepthStencilValue// 获取语义标签
Detection.object_tag = HitInfo.Component->CustomDepthStencilValue;
// 获取ActorID
const FActorRegistry &Registry = GetEpisode().GetActorRegistry();
const FCarlaActor* view = Registry.FindCarlaActor(HitInfo.Actor.Get());
Detection.object_idx = view ? view->GetActorId() : 0;
CustomDepthStencilValue实现语义标签与游戏对象的绑定
FMath::GetReflectionVector实现ReflectRange = (Range - Incidencedistance) * 0.8f;
ParallelLineTraceSingleByChannel | ||
std::rand() | ||
Description.Range | ||
Reflectivity[12] | ||
ReflectRange | ||
random |
可视化调试:
// 绘制原始射线(红色)
DrawDebugLine(GetWorld(), LidarBodyLoc, EndTrace, FColor::Red, false, 0.1f);
// 绘制反射射线(绿色)
DrawDebugLine(GetWorld(), ExitPoint, EndTrace1, FColor::Green, false, 0.1f);
验证反射逻辑:
性能测试:
TRACE_CPUPROFILER_EVENT_SCOPE(ShootLaser);
// 统计单次调用耗时
动态反射率配置:
// 从材质系统获取实时反射率
float Reflectivity = HitResult.Material->GetReflectivity();
多级反射:
// 递归实现多次反射
void ProcessReflection(int depth) {
if(depth > MAX_REFLECT_DEPTH) return;
// ...
}
噪声注入:
// 添加角度噪声
float NoisyVertAngle = VerticalAngle + FMath::RandRange(-0.1f, 0.1f);
该函数通过精确的物理模拟和高效的算法实现,为自动驾驶仿真提供了高真实度的激光雷达点云数据,同时保持实时性能。
根据文档how-to-add-a-new-sensor,接下来是编制Sensor data serializer,Sensor data object,在SensorRegistry.h中注册你的新传感器等环节,本文提供的代码在这些部分中基本都是参照Carla原有传感器的代码进行编写的,读者可自行对照文末提供的代码进行分析。
本文适配的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_UE5.5.zip的百度云链接:
https://pan.baidu.com/s/10RzEAvGs_Ie6PTZq0jwkEA
提取码: h9fb
读者在编译前可提前下载,并放置在对应位置。
首次编译成功,可以运行Carla后,如果对UE和Carla的代码进行了修改,需要对UnrealEngine5_carla和CarlaUE5分别再次进行编译:
# UE5:
./Setup.sh
./GenerateProjectFiles.sh
make
# CarlaUE5:
./CarlaSetup.sh --interactive --python-root=/home/vipuser/.conda/envs/carla/bin
最新版本的CarlaUE5可以通过--python-root参数指定需要的python虚拟环境。CarlaSetup.sh中的参数launch修改为1可以在运行脚本后打开UE窗口。
在合入本文代码后,还需要修改Unreal/CarlaUnreal/Plugins/Carla/Source/Carla/Sensor/Risley_pris m.cpp代码中的filename,指向您本地的RisleyPris mCsv文件夹。
点击UE窗口中的Run后,运行PythonAPI\lidar_lib_example\lidar_display.py,会在窗口中显示激光雷达效果。
关于lidar_display.py的一些说明:
在脚本中切换ego_horizon,ego_rs和ego_pandar64可以使用不同类型的激光雷达进行仿真。使用LidarMotonDistortion("./11_horizon/", 10)功能,会将加入了motion distortion的点云保存成ply文件,效果如下图:

最后附上本文代码:Lidar_CarlaUE5.zip,链接:
https://pan.baidu.com/s/1tX9OEZrC_gIeJiFSc9akwQ
提取码: cxgv
如果您需要适配UE 4.26版本的激光雷达代码,还可以参考以下链接carla中的PCSim branch:
https://github.com/GimpelZhang/carla/tree/PCSim
本文完。