diff --git a/Content/Levels/L_Test.umap b/Content/Levels/L_Test.umap index 02f9153..0545de5 100644 Binary files a/Content/Levels/L_Test.umap and b/Content/Levels/L_Test.umap differ 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)); }