From c8aa3652ff164572be91ada251b5e11d5baca519 Mon Sep 17 00:00:00 2001 From: Hong Date: Fri, 6 Mar 2026 17:47:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=91=84=E5=83=8F=E6=9C=BA?= =?UTF-8?q?=E6=8D=95=E6=8D=89=E7=9A=84=E7=BB=9F=E4=B8=80=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Content/Levels/L_Test.umap | Bin 50418 -> 73087 bytes .../Source/CameraCaptureActor.cpp | 224 +++--------------- .../CameraCapture/Source/CameraCaptureActor.h | 49 ++-- .../Source/CameraCaptureSubSystem.h | 19 +- .../Source/CameraCaptureSubsystem.cpp | 137 +++++++++++ 5 files changed, 218 insertions(+), 211 deletions(-) diff --git a/Content/Levels/L_Test.umap b/Content/Levels/L_Test.umap index 02f915342cba4b602ff3c792e8cb9e15497c9876..0545de53d8596662c2e14dfb028028516a38b2bf 100644 GIT binary patch literal 73087 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
Pp*abFITQd4Ga6=6g= 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 diff --git a/Plugins/CameraCapture/Source/CameraCaptureActor.cpp b/Plugins/CameraCapture/Source/CameraCaptureActor.cpp index 77b08ca..f5db9ff 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureActor.cpp +++ b/Plugins/CameraCapture/Source/CameraCaptureActor.cpp @@ -1,21 +1,14 @@ #include "CameraCaptureActor.h" + #include "Components/SceneCaptureComponent2D.h" #include "Camera/CameraComponent.h" #include "Engine/TextureRenderTarget2D.h" -#include "Engine/World.h" -#include "HAL/PlatformFilemanager.h" -#include "Misc/Paths.h" -#include "Misc/FileHelper.h" -#include "IImageWrapper.h" -#include "IImageWrapperModule.h" -#include "Modules/ModuleManager.h" -#include "Async/Async.h" ACameraCaptureActor::ACameraCaptureActor() { - PrimaryActorTick.bCanEverTick = true; + // PrimaryActorTick.bCanEverTick = false; } void ACameraCaptureActor::BeginPlay() @@ -24,21 +17,6 @@ void ACameraCaptureActor::BeginPlay() BuildViewCaptures(); } -void ACameraCaptureActor::Tick(float DeltaSeconds) -{ - Super::Tick(DeltaSeconds); - - if (!bCaptureEveryFrame) return; - - UpdateViewTransforms(); - CaptureViews(); - - if (bGenerateAtlas) - { - GenerateAtlas(); - } -} - #if WITH_EDITOR void ACameraCaptureActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { @@ -51,59 +29,44 @@ void ACameraCaptureActor::PostEditChangeProperty(FPropertyChangedEvent& Property void ACameraCaptureActor::BuildViewCaptures() { - // Destroy old components - for (USceneCaptureComponent2D* Comp : ViewCaptures) + if (ViewCapture) { - if (Comp) - { - Comp->DestroyComponent(); - } - } - ViewCaptures.Empty(); - ViewRTs.Empty(); - AtlasRT = nullptr; - - if (ViewCount < 1) ViewCount = 1; - if (ViewWidth < 16) ViewWidth = 16; - if (ViewHeight < 16) ViewHeight = 16; - - // Recreate view captures - for (int32 i = 0; i < ViewCount; ++i) - { - UTextureRenderTarget2D* RT = NewObject(this); - RT->RenderTargetFormat = RTF_RGBA8; - RT->ClearColor = FLinearColor::Black; - RT->InitAutoFormat(ViewWidth, ViewHeight); - RT->UpdateResourceImmediate(true); - - USceneCaptureComponent2D* Capture = NewObject(this); - Capture->SetupAttachment(GetRootComponent()); - Capture->RegisterComponent(); - - Capture->TextureTarget = RT; - Capture->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR; - Capture->bCaptureEveryFrame = false; // 手动触发 - Capture->bCaptureOnMovement = false; - - ViewCaptures.Add(Capture); - ViewRTs.Add(RT); + ViewCapture->DestroyComponent(); + ViewCapture = nullptr; } - // Atlas RT (optional) - if (bGenerateAtlas) + if (ViewRT) { - if (AtlasGridX < 1) AtlasGridX = 1; - if (AtlasGridY < 1) AtlasGridY = 1; - - AtlasRT = NewObject(this); - AtlasRT->RenderTargetFormat = RTF_RGBA8; - AtlasRT->ClearColor = FLinearColor::Black; - AtlasRT->InitAutoFormat(ViewWidth * AtlasGridX, ViewHeight * AtlasGridY); - AtlasRT->UpdateResourceImmediate(true); + ViewRT = nullptr; } + + ViewRT = NewObject(this); + ViewRT->ClearColor = FLinearColor::Black; + ViewRT->InitAutoFormat(ViewWidth, ViewHeight); + ViewRT->UpdateResourceImmediate(true); + + ViewCapture = NewObject(this, TEXT("ViewCapture")); + ViewCapture->SetupAttachment(GetRootComponent()); + ViewCapture->RegisterComponent(); + + ViewCapture->TextureTarget = ViewRT; + ViewCapture->CaptureSource = ESceneCaptureSource::SCS_FinalColorHDR; + ViewCapture->bCaptureEveryFrame = false; + ViewCapture->bCaptureOnMovement = false; + + ViewCapture->PostProcessBlendWeight = 1.0f; + + ViewCapture->PostProcessSettings.bOverride_AutoExposureMethod = true; + ViewCapture->PostProcessSettings.AutoExposureMethod = EAutoExposureMethod::AEM_Manual; + + ViewCapture->PostProcessSettings.bOverride_AutoExposureBias = true; + ViewCapture->PostProcessSettings.AutoExposureBias = 1.0f; + + ViewCapture->SetRelativeLocation(FVector::ZeroVector); + ViewCapture->SetRelativeRotation(FRotator::ZeroRotator); } -void ACameraCaptureActor::UpdateViewTransforms() +void ACameraCaptureActor::UpdateViewTransforms(const int32 Index) { // 以本 Actor(CameraActor)的 transform 为中心 const FVector Center = GetActorLocation(); @@ -117,129 +80,18 @@ void ACameraCaptureActor::UpdateViewTransforms() FOV = Cam->FieldOfView; } - for (int32 i = 0; i < ViewCaptures.Num(); ++i) + if (ViewCapture) { - if (!ViewCaptures[i]) continue; - // 线性水平分布:居中对称 - const float Offset = (i - (ViewCount - 1) * 0.5f) * Baseline; + const float Offset = (Index - (ViewCount - 1) * 0.5f) * Baseline; const FVector Pos = Center + Right * Offset; - ViewCaptures[i]->SetWorldLocationAndRotation(Pos, Rot); - ViewCaptures[i]->FOVAngle = FOV; + ViewCapture->SetWorldLocationAndRotation(Pos, Rot); + ViewCapture->FOVAngle = FOV; } } void ACameraCaptureActor::CaptureViews() { - for (USceneCaptureComponent2D* Capture : ViewCaptures) - { - if (Capture) - { - Capture->CaptureScene(); - } - } + ViewCapture->CaptureScene(); } - -static void FlipVertical(TArray& Pixels, int32 W, int32 H) -{ - for (int32 Row = 0; Row < H / 2; ++Row) - { - const int32 A = Row * W; - const int32 B = (H - 1 - Row) * W; - for (int32 Col = 0; Col < W; ++Col) - { - Swap(Pixels[A + Col], Pixels[B + Col]); - } - } -} - -void ACameraCaptureActor::GenerateAtlas() -{ - if (!AtlasRT) return; - - // 这里做一个“可编译的基础版”:CPU 拼合 AtlasPixels,并(可选)保存到磁盘 - // 注意:把像素写回 AtlasRT 需要更底层的 RHI/渲染线程操作;高性能版我建议改成 GPU Blit。 - const int32 AtlasW = AtlasRT->SizeX; - const int32 AtlasH = AtlasRT->SizeY; - - TArray AtlasPixels; - AtlasPixels.SetNumZeroed(AtlasW * AtlasH); - - const int32 MaxViews = FMath::Min(ViewCount, AtlasGridX * AtlasGridY); - - for (int32 i = 0; i < MaxViews; ++i) - { - UTextureRenderTarget2D* RT = ViewRTs.IsValidIndex(i) ? ViewRTs[i] : nullptr; - if (!RT) continue; - - FTextureRenderTargetResource* Res = RT->GameThread_GetRenderTargetResource(); - if (!Res) continue; - - TArray Pixels; - Pixels.SetNumUninitialized(ViewWidth * ViewHeight); - - FReadSurfaceDataFlags ReadFlags(RCM_UNorm); - ReadFlags.SetLinearToGamma(true); - - if (!Res->ReadPixels(Pixels, ReadFlags)) continue; - FlipVertical(Pixels, ViewWidth, ViewHeight); - - const int32 GX = i % AtlasGridX; - const int32 GY = i / AtlasGridX; - - const int32 DstX0 = GX * ViewWidth; - const int32 DstY0 = GY * ViewHeight; - - for (int32 y = 0; y < ViewHeight; ++y) - { - const int32 DstY = DstY0 + y; - if (DstY < 0 || DstY >= AtlasH) continue; - - const int32 SrcRow = y * ViewWidth; - const int32 DstRow = DstY * AtlasW; - - for (int32 x = 0; x < ViewWidth; ++x) - { - const int32 DstX = DstX0 + x; - if (DstX < 0 || DstX >= AtlasW) continue; - - AtlasPixels[DstRow + DstX] = Pixels[SrcRow + x]; - } - } - } - - // ✅ 如果你需要“输出到目录”,这里就直接把 AtlasPixels 编码写盘 - // (你也可以加开关:bDumpAtlasToDisk) - const FString OutDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("CameraCapture")); - IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile(); - if (!PF.DirectoryExists(*OutDir)) PF.CreateDirectoryTree(*OutDir); - - const FString FullPath = FPaths::Combine( - OutDir, - FString::Printf(TEXT("atlas_%010lld.png"), (int64)GFrameCounter) - ); - - auto EncodeAndWrite = [AtlasPixels = MoveTemp(AtlasPixels), AtlasW, AtlasH, FullPath]() - { - IImageWrapperModule& ImageWrapperModule = - FModuleManager::LoadModuleChecked(TEXT("ImageWrapper")); - - TSharedPtr Wrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); - if (!Wrapper.IsValid()) return; - - if (!Wrapper->SetRaw(AtlasPixels.GetData(), AtlasPixels.Num() * sizeof(FColor), - AtlasW, AtlasH, ERGBFormat::BGRA, 8)) - { - return; - } - - const TArray64& Compressed = Wrapper->GetCompressed(); - TArray Bytes; - Bytes.Append(Compressed.GetData(), (int32)Compressed.Num()); - FFileHelper::SaveArrayToFile(Bytes, *FullPath); - }; - - // 基础版默认异步写盘,避免卡 - Async(EAsyncExecution::ThreadPool, MoveTemp(EncodeAndWrite)); -} \ No newline at end of file diff --git a/Plugins/CameraCapture/Source/CameraCaptureActor.h b/Plugins/CameraCapture/Source/CameraCaptureActor.h index 781a9a4..3504b83 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureActor.h +++ b/Plugins/CameraCapture/Source/CameraCaptureActor.h @@ -4,6 +4,17 @@ #include "Camera/CameraActor.h" #include "CameraCaptureActor.generated.h" + +#define CAMERA_WIDTH_NUMS 3 +#define CAMERA_HEIGHTH_NUMS 3 + +const int32 AtlasGridX = 3; +const int32 AtlasGridY = 3; +const int32 ViewWidth = 1024; +const int32 ViewHeight = 1024; + + + UENUM(BlueprintType) enum class EViewDistribution : uint8 { @@ -11,6 +22,12 @@ enum class EViewDistribution : uint8 Circular UMETA(DisplayName = "Circular") }; + +class USceneCaptureComponent2D; +class UTextureRenderTarget2D; + + + UCLASS() class CAMERACAPTURE_API ACameraCaptureActor : public ACameraActor { @@ -19,6 +36,10 @@ class CAMERACAPTURE_API ACameraCaptureActor : public ACameraActor public: ACameraCaptureActor(); + USceneCaptureComponent2D* GetViewCapture() { return ViewCapture; } + UTextureRenderTarget2D* GetViewRT() { return ViewRT; } + + // ===== 多视图参数 ===== UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiView") @@ -30,28 +51,11 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiView") EViewDistribution Distribution = EViewDistribution::LinearHorizontal; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiView") - int32 ViewWidth = 1024; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiView") - int32 ViewHeight = 1024; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiView") bool bCaptureEveryFrame = true; - // Atlas - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Atlas") - bool bGenerateAtlas = true; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Atlas") - int32 AtlasGridX = 3; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Atlas") - int32 AtlasGridY = 3; - protected: virtual void BeginPlay() override; - virtual void Tick(float DeltaSeconds) override; #if WITH_EDITOR virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; @@ -62,18 +66,15 @@ protected: private: void BuildViewCaptures(); - void UpdateViewTransforms(); + void UpdateViewTransforms(const int32 Index); void CaptureViews(); - void GenerateAtlas(); private: - UPROPERTY(Transient) - TArray ViewCaptures; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) + TObjectPtr ViewCapture; UPROPERTY(Transient) - TArray ViewRTs; + TObjectPtr ViewRT; - UPROPERTY(Transient) - class UTextureRenderTarget2D* AtlasRT; }; \ No newline at end of file diff --git a/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h b/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h index 053bbc6..fb2a486 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h +++ b/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h @@ -4,15 +4,17 @@ #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" +#include "Tickable.h" #include "CameraCaptureSubSystem.generated.h" + class ACameraCaptureActor; UCLASS() -class CAMERACAPTURE_API UCameraCaptureSubSystem : public UWorldSubsystem +class CAMERACAPTURE_API UCameraCaptureSubSystem : public UWorldSubsystem, public FTickableGameObject { GENERATED_BODY() @@ -22,10 +24,25 @@ public: virtual void Deinitialize() override; virtual void OnWorldBeginPlay(UWorld& InWorld) override; + virtual void Tick(float DeltaTime) override; + + virtual TStatId GetStatId() const override; + + virtual bool IsTickable() const override + { + return true; + } private: + void CaptureViewToImageTick(); + + void AsyncWriteImageToDisk(TArray& AtlasPixels, const int32 AtlasW, const int32 AtlasH); + TArray> AllCaptureCameras; + // 输出的目录 + UPROPERTY() + FString OutPutDirectoryPath; }; diff --git a/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp b/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp index 1dfc81e..c5dc8db 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp +++ b/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp @@ -2,13 +2,25 @@ #include "CameraCaptureSubSystem.h" #include "CameraCaptureActor.h" +#include "Components/SceneCaptureComponent2D.h" +#include "Camera/CameraComponent.h" #include "EngineUtils.h" +#include "Engine/World.h" +#include "Engine/TextureRenderTarget2D.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" +#include "HAL/PlatformFilemanager.h" +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" +#include "Modules/ModuleManager.h" +#include "Async/Async.h" void UCameraCaptureSubSystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); + OutPutDirectoryPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("CameraCapture")); } void UCameraCaptureSubSystem::Deinitialize() @@ -30,4 +42,129 @@ void UCameraCaptureSubSystem::OnWorldBeginPlay(UWorld& InWorld) } } + if (AllCaptureCameras.Num() != CAMERA_WIDTH_NUMS * CAMERA_HEIGHTH_NUMS) + { + return; + } + + +} + +static void FlipVertical(TArray& Pixels, int32 W, int32 H) +{ + for (int32 Row = 0; Row < H / 2; ++Row) + { + const int32 A = Row * W; + const int32 B = (H - 1 - Row) * W; + for (int32 Col = 0; Col < W; ++Col) + { + Swap(Pixels[A + Col], Pixels[B + Col]); + } + } +} + +void UCameraCaptureSubSystem::Tick(float DeltaTime) +{ + CaptureViewToImageTick(); + +} + +TStatId UCameraCaptureSubSystem::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UCameraCaptureSubSystem, STATGROUP_Tickables); +} + +void UCameraCaptureSubSystem::CaptureViewToImageTick() +{ + if (AllCaptureCameras.IsEmpty()) + { + return; + } + + TArray AtlasPixels; + const int32 AtlasW = ViewWidth * AtlasGridX; + const int32 AtlasH = ViewHeight * AtlasGridY; + AtlasPixels.SetNumZeroed(AtlasW * AtlasH); + + for (int32 CameraIndex = 0; CameraIndex < AllCaptureCameras.Num(); CameraIndex++) + { + if (!AllCaptureCameras[CameraIndex].IsValid()) + { + continue; + } + + UTextureRenderTarget2D* RenderTarget = AllCaptureCameras[CameraIndex]->GetViewRT(); + if (!RenderTarget) + { + continue; + } + + FTextureRenderTargetResource* RenderTargetRes = RenderTarget->GameThread_GetRenderTargetResource(); + if (!RenderTargetRes) + { + continue; + } + + TArray Pixels; + Pixels.SetNumUninitialized(ViewWidth * ViewHeight); + + FReadSurfaceDataFlags ReadFlags(RCM_UNorm); + ReadFlags.SetLinearToGamma(true); + + if (!RenderTargetRes->ReadPixels(Pixels, ReadFlags)) continue; + FlipVertical(Pixels, ViewWidth, ViewHeight); + + const int32 GX = CameraIndex % AtlasGridX; + const int32 GY = CameraIndex / AtlasGridX; + + const int32 DstX0 = GX * ViewWidth; + const int32 DstY0 = GY * ViewHeight; + + for (int32 y = 0; y < ViewHeight; ++y) + { + const int32 DstY = DstY0 + y; + if (DstY < 0 || DstY >= AtlasH) continue; + + const int32 SrcRow = y * ViewWidth; + const int32 DstRow = DstY * AtlasW; + + for (int32 x = 0; x < ViewWidth; ++x) + { + const int32 DstX = DstX0 + x; + if (DstX < 0 || DstX >= AtlasW) continue; + + AtlasPixels[DstRow + DstX] = Pixels[SrcRow + x]; + } + } + } + + AsyncWriteImageToDisk(AtlasPixels, AtlasW, AtlasH); +} + +void UCameraCaptureSubSystem::AsyncWriteImageToDisk(TArray& AtlasPixels, const int32 AtlasW, const int32 AtlasH) +{ + const FString FullPath = FPaths::Combine( OutPutDirectoryPath, FString::Printf(TEXT("atlas_%010lld.png"), (int64)GFrameCounter) ); + + auto EncodeAndWrite = [AtlasPixels = MoveTemp(AtlasPixels), AtlasW, AtlasH, FullPath]() + { + IImageWrapperModule& ImageWrapperModule = + FModuleManager::LoadModuleChecked(TEXT("ImageWrapper")); + + TSharedPtr Wrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); + if (!Wrapper.IsValid()) return; + + if (!Wrapper->SetRaw(AtlasPixels.GetData(), AtlasPixels.Num() * sizeof(FColor), + AtlasW, AtlasH, ERGBFormat::BGRA, 8)) + { + return; + } + + const TArray64& Compressed = Wrapper->GetCompressed(); + TArray Bytes; + Bytes.Append(Compressed.GetData(), (int32)Compressed.Num()); + FFileHelper::SaveArrayToFile(Bytes, *FullPath); + }; + + // 基础版默认异步写盘,避免卡 + Async(EAsyncExecution::ThreadPool, MoveTemp(EncodeAndWrite)); }