【UE5 C++】绘制地表贴合线

发布于:2025-05-31 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

原理

效果

步骤

源码


原理

        先设置绘制线段的起点和终点,然后我们将起点和终点的高度升高,然后通过插值,在起点和终点之间添加多个点,再由这些点向地心发出射线,这样我们就可以获取到这些点在地表的投影点,最后将投影点连起来就是地表贴合线。

        注意:使用该方法时,需要保证地形加载完毕,因为只要地形加载后地表才会有碰撞信息,才能被射线检测到

效果

步骤

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;
}