目录
原理
先设置绘制线段的起点和终点,然后我们将起点和终点的高度升高,然后通过插值,在起点和终点之间添加多个点,再由这些点向地心发出射线,这样我们就可以获取到这些点在地表的投影点,最后将投影点连起来就是地表贴合线。
注意:使用该方法时,需要保证地形加载完毕,因为只要地形加载后地表才会有碰撞信息,才能被射线检测到
效果
步骤
1. 在“xxx.Build.cs”中引入“CesiumRuntime”模块
2. 新建一个Actor类,这里命名为“SurfaceLineActor”
在“SurfaceLineActor.h”中添加所需头文件
定义一个函数“CalculatePointsOnSurface”用于计算地表点,该函数需要传入起点终点的经纬度、细分点数、起终点向上偏移的距离
再定义一个函数用于绘制地表贴合线,该函数需要传入地表点数组、线的颜色、线的粗细共3个参数。
定义地心坐标、添加一个LineBatchComponent组件用于高效绘制线条、定义一个数组“Points”用于储存起终点之间的点
实现函数“CalculatePointsOnSurface”如下
TArray<FVector> ASurfaceLineActor::CalculatePointsOnSurface(FVector StartPointLLA, FVector EndPointLLA, int32 NumberOfSegments, double TraceUpOffset)
{
//将起点和终点拔高一段距离以方便往地心打射线
StartPointLLA = StartPointLLA + FVector(0, 0, TraceUpOffset);
EndPointLLA = EndPointLLA + FVector(0, 0, TraceUpOffset);
//获取Georeference
ACesiumGeoreference* Georeference = ACesiumGeoreference::GetDefaultGeoreference(GetWorld());
if (!Georeference)
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceLineActor: CesiumGeoreference not found. Cannot calculate surface points."));
}
//将起终点(经纬高)转换为起终点(UE世界坐标)
FVector StartPoint = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(StartPointLLA);
FVector EndPoint = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(EndPointLLA);
Points.Empty();
if (NumberOfSegments <= 0) // 分段数不满足要求,返回起点和终点
{
// 如果段数无效,至少包含起点和终点
Points.Add(StartPoint);
if (StartPoint != EndPoint) // 避免重复添加同一点
{
Points.Add(EndPoint);
}
}
else
{
//Points.Reserve(NumberOfSegments + 1); // 预分配空间提高效率
for (int32 i = 0; i < NumberOfSegments; ++i)
{
float T = static_cast<float>(i) / static_cast<float>(NumberOfSegments);
FVector CurrentPoint = FMath::Lerp(StartPoint, EndPoint, T); // 使用 FMath::Lerp 进行线性插值计算点的坐标
Points.Add(CurrentPoint);
}
}
// 用于存储射线检测到的地表点
TArray<FVector> CalculatedSurfacePoints;
//起点和终点构成的连线中每个点都朝地心发出射线
for (int32 i = 0; i < Points.Num(); ++i)
{
FHitResult HitResult;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this); // 射线检测忽视自身
//QueryParams.AddIgnoredActors(ActorsToIgnore); // 忽视射线检测的Actor
QueryParams.bTraceComplex = true; // 使用复杂碰撞
QueryParams.bReturnPhysicalMaterial = false; // 是否检测物理材料
ECollisionChannel TraceChannel = ECC_Visibility;
UWorld* World = GetWorld();
bool bHit = World->LineTraceSingleByChannel(
HitResult, // 射线检测结果
Points[i], // 射线起点
EarthOrigin, // 射线终点(地心)
TraceChannel, // 射线碰撞通道
QueryParams // 射线碰撞其他参数
);
画射线检测(非必需)
//DrawDebugLine(
// GetWorld(),
// Points[i], //起点
// EarthOrigin, //终点(地心)
// bHit ? FColor::Green : FColor::Red, //颜色
// false, // false表示它持续“LifeTime”,如果LifeTime为0,则持续一帧
// 0.0, // DebugLine生命周期时长
// 0, // DepthPriority (0 for SDPG_World)
// 2.0f
//);
if (bHit)
{
画射线碰撞点(非必需)
//DrawDebugSphere(
// GetWorld(),
// HitResult.Location,
// 30.0f, // Radius
// 12, // Segments
// FColor::Cyan,
// false, // Persistent lines
// 0.0f // Lifetime
//);
FVector OutHitLocation = HitResult.ImpactPoint;
CalculatedSurfacePoints.Add(OutHitLocation);
}
}
return CalculatedSurfacePoints;
}
3. 测试一下“CalculatePointsOnSurface”函数是否生效,这里设置起点为成都市市中心终点为都江堰
运行后可以看到地表点位置基本准确
4. 接下来只需要通过LineBatchComponent组件将地表点连线即可
函数“DrawSurfaceLine”实现如下
void ASurfaceLineActor::DrawSurfaceLine(TArray<FVector> SurfacePoints, FLinearColor LineColor, float LineThickness)
{
if (!LineBatchComponent)
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceLineActor: LineBatchComponent is null."));
return;
}
LineBatchComponent->Flush(); // 清除之前的线
if (SurfacePoints.Num() >= 2)
{
for (int32 i = 0; i < SurfacePoints.Num() - 1; ++i)
{
if (SurfacePoints[i].ContainsNaN() || SurfacePoints[i + 1].ContainsNaN())
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceLineActor: Encountered NaN point at segment %d, skipping draw."), i);
continue;
}
LineBatchComponent->DrawLine(
SurfacePoints[i],
SurfacePoints[i + 1],
LineColor,
SDPG_World, // 场景深度优先级组-根据渲染顺序需要进行调整
LineThickness,
0.0f // 线的生命周期时长:0表示无限生命
);
}
}
}
再通过蓝图在获取地表点数组后传入函数 “DrawSurfaceLine”
此时效果如下:
5. 选取山地进行测试,插值数量设置为200,此时运行效果如下
源码
“SurfaceLineActor.h”
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/LineBatchComponent.h"
#include "CesiumGeoreference.h"
#include "SurfaceLineActor.generated.h"
UCLASS()
class GLOBEPAWNTEST_API ASurfaceLineActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASurfaceLineActor();
// 计算地表点
UFUNCTION(BlueprintCallable, Category = "Line Properties")
TArray<FVector> CalculatePointsOnSurface(
FVector StartPointLLA = FVector(0, 0, 0),
FVector EndPointLLA = FVector(0, 0, 0),
int32 NumberOfSegments = 50, // 线段细分数 (越高质量越好,但性能开销越大)
double TraceUpOffset = 100000.0 // 射线检测时向上偏移的距离(确保起点在潜在地形之上) 默认100km
);
// 调用此函数来绘制或更新地表贴合线
UFUNCTION(BlueprintCallable, Category = "Line Properties")
void DrawSurfaceLine(
TArray<FVector> SurfacePoints,
FLinearColor LineColor = FLinearColor::Red, // 线的颜色
float LineThickness = 10.0f // 线的粗细
);
//地心坐标
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector EarthOrigin = FVector(0, 0, -637810000.0);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
ULineBatchComponent* LineBatchComponent;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
private:
TArray<FVector> Points; //用于存储起点和终点连线之间的点
};
“SurfaceLineActor.cpp”
// Fill out your copyright notice in the Description page of Project Settings.
#include "SurfaceLineActor.h"
#include "CesiumRuntime/Public/CesiumGeoreference.h"
// Sets default values
ASurfaceLineActor::ASurfaceLineActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
LineBatchComponent = CreateDefaultSubobject<ULineBatchComponent>(TEXT("LineBatcher"));
// 可以将 LineBatchComponent 设为根组件,或者附加到其他组件上
RootComponent = LineBatchComponent;
LineBatchComponent->bCalculateAccurateBounds = false; // Optimization for dynamic lines
}
// Called when the game starts or when spawned
void ASurfaceLineActor::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void ASurfaceLineActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ASurfaceLineActor::DrawSurfaceLine(TArray<FVector> SurfacePoints, FLinearColor LineColor, float LineThickness)
{
if (!LineBatchComponent)
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceLineActor: LineBatchComponent is null."));
return;
}
LineBatchComponent->Flush(); // 清除之前的线
if (SurfacePoints.Num() >= 2)
{
for (int32 i = 0; i < SurfacePoints.Num() - 1; ++i)
{
if (SurfacePoints[i].ContainsNaN() || SurfacePoints[i + 1].ContainsNaN())
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceLineActor: Encountered NaN point at segment %d, skipping draw."), i);
continue;
}
LineBatchComponent->DrawLine(
SurfacePoints[i],
SurfacePoints[i + 1],
LineColor,
SDPG_World, // 场景深度优先级组-根据渲染顺序需要进行调整
LineThickness,
0.0f // 线的生命周期时长:0表示无限生命
);
}
}
}
TArray<FVector> ASurfaceLineActor::CalculatePointsOnSurface(FVector StartPointLLA, FVector EndPointLLA, int32 NumberOfSegments, double TraceUpOffset)
{
//将起点和终点拔高一段距离以方便往地心打射线
StartPointLLA = StartPointLLA + FVector(0, 0, TraceUpOffset);
EndPointLLA = EndPointLLA + FVector(0, 0, TraceUpOffset);
//获取Georeference
ACesiumGeoreference* Georeference = ACesiumGeoreference::GetDefaultGeoreference(GetWorld());
if (!Georeference)
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceLineActor: CesiumGeoreference not found. Cannot calculate surface points."));
}
//将起终点(经纬高)转换为起终点(UE世界坐标)
FVector StartPoint = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(StartPointLLA);
FVector EndPoint = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(EndPointLLA);
Points.Empty();
if (NumberOfSegments <= 0) // 分段数不满足要求,返回起点和终点
{
// 如果段数无效,至少包含起点和终点
Points.Add(StartPoint);
if (StartPoint != EndPoint) // 避免重复添加同一点
{
Points.Add(EndPoint);
}
}
else
{
//Points.Reserve(NumberOfSegments + 1); // 预分配空间提高效率
for (int32 i = 0; i < NumberOfSegments; ++i)
{
float T = static_cast<float>(i) / static_cast<float>(NumberOfSegments);
FVector CurrentPoint = FMath::Lerp(StartPoint, EndPoint, T); // 使用 FMath::Lerp 进行线性插值计算点的坐标
Points.Add(CurrentPoint);
}
Points.Add(EndPoint); //需要加上终点
}
// 用于存储射线检测到的地表点
TArray<FVector> CalculatedSurfacePoints;
//起点和终点构成的连线中每个点都朝地心发出射线
for (int32 i = 0; i < Points.Num(); ++i)
{
FHitResult HitResult;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this); // 射线检测忽视自身
//QueryParams.AddIgnoredActors(ActorsToIgnore); // 忽视射线检测的Actor
QueryParams.bTraceComplex = true; // 使用复杂碰撞
QueryParams.bReturnPhysicalMaterial = false; // 是否检测物理材料
ECollisionChannel TraceChannel = ECC_Visibility;
UWorld* World = GetWorld();
bool bHit = World->LineTraceSingleByChannel(
HitResult, // 射线检测结果
Points[i], // 射线起点
EarthOrigin, // 射线终点(地心)
TraceChannel, // 射线碰撞通道
QueryParams // 射线碰撞其他参数
);
画射线检测(非必需)
//DrawDebugLine(
// GetWorld(),
// Points[i], //起点
// EarthOrigin, //终点(地心)
// bHit ? FColor::Green : FColor::Red, //颜色
// false, // false表示它持续“LifeTime”,如果LifeTime为0,则持续一帧
// 0.0, // DebugLine生命周期时长
// 0, // DepthPriority (0 for SDPG_World)
// 2.0f
//);
if (bHit)
{
画射线碰撞点(非必需)
//DrawDebugSphere(
// GetWorld(),
// HitResult.Location,
// 30.0f, // Radius
// 12, // Segments
// FColor::Cyan,
// false, // Persistent lines
// 0.0f // Lifetime
//);
FVector OutHitLocation = HitResult.ImpactPoint;
CalculatedSurfacePoints.Add(OutHitLocation);
}
}
return CalculatedSurfacePoints;
}