From 5654428986809877bf5d27af31bbb0918d0658e1 Mon Sep 17 00:00:00 2001 From: Hong Date: Fri, 6 Mar 2026 18:25:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B8=A7=E7=8E=87=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=20GPU=E6=95=B0=E6=8D=AE=E8=AF=BB=E6=93=8D=E4=BD=9C=E5=BC=82?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/CameraCaptureActor.cpp | 21 +- .../CameraCapture/Source/CameraCaptureActor.h | 1 + .../Source/CameraCaptureSubSystem.h | 30 ++- .../Source/CameraCaptureSubsystem.cpp | 224 ++++++++++++++---- 4 files changed, 216 insertions(+), 60 deletions(-) diff --git a/Plugins/CameraCapture/Source/CameraCaptureActor.cpp b/Plugins/CameraCapture/Source/CameraCaptureActor.cpp index f5db9ff..a56445f 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureActor.cpp +++ b/Plugins/CameraCapture/Source/CameraCaptureActor.cpp @@ -11,6 +11,14 @@ ACameraCaptureActor::ACameraCaptureActor() // PrimaryActorTick.bCanEverTick = false; } +void ACameraCaptureActor::CaptureNow() +{ + if (ViewCapture) + { + ViewCapture->CaptureScene(); + } +} + void ACameraCaptureActor::BeginPlay() { Super::BeginPlay(); @@ -25,8 +33,8 @@ void ACameraCaptureActor::PostEditChangeProperty(FPropertyChangedEvent& Property // 任何关键参数变化都重建(避免 RT 尺寸/数量不匹配) BuildViewCaptures(); } -#endif +#endif void ACameraCaptureActor::BuildViewCaptures() { if (ViewCapture) @@ -35,14 +43,10 @@ void ACameraCaptureActor::BuildViewCaptures() ViewCapture = nullptr; } - if (ViewRT) - { - ViewRT = nullptr; - } - ViewRT = NewObject(this); + ViewRT->RenderTargetFormat = RTF_RGBA8; + ViewRT->InitCustomFormat(ViewWidth, ViewHeight, PF_B8G8R8A8, true); ViewRT->ClearColor = FLinearColor::Black; - ViewRT->InitAutoFormat(ViewWidth, ViewHeight); ViewRT->UpdateResourceImmediate(true); ViewCapture = NewObject(this, TEXT("ViewCapture")); @@ -50,7 +54,8 @@ void ACameraCaptureActor::BuildViewCaptures() ViewCapture->RegisterComponent(); ViewCapture->TextureTarget = ViewRT; - ViewCapture->CaptureSource = ESceneCaptureSource::SCS_FinalColorHDR; + ViewCapture->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR; + ViewCapture->bCaptureEveryFrame = false; ViewCapture->bCaptureOnMovement = false; diff --git a/Plugins/CameraCapture/Source/CameraCaptureActor.h b/Plugins/CameraCapture/Source/CameraCaptureActor.h index 3504b83..527950c 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureActor.h +++ b/Plugins/CameraCapture/Source/CameraCaptureActor.h @@ -39,6 +39,7 @@ public: USceneCaptureComponent2D* GetViewCapture() { return ViewCapture; } UTextureRenderTarget2D* GetViewRT() { return ViewRT; } + void CaptureNow(); // ===== 多视图参数 ===== diff --git a/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h b/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h index fb2a486..de8a469 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h +++ b/Plugins/CameraCapture/Source/CameraCaptureSubSystem.h @@ -3,12 +3,31 @@ #pragma once #include "CoreMinimal.h" +#include "RHIGPUReadback.h" +#include "RHICommandList.h" +#include "Engine/TextureRenderTarget2D.h" #include "Subsystems/WorldSubsystem.h" #include "Tickable.h" #include "CameraCaptureSubSystem.generated.h" +struct FCameraReadbackItem +{ + int32 CameraIndex = INDEX_NONE; + int32 Width = 0; + int32 Height = 0; + TUniquePtr Readback; +}; + +struct FPendingAtlasCapture +{ + int64 FrameId = 0; + int32 AtlasW = 0; + int32 AtlasH = 0; + + TArray Items; +}; class ACameraCaptureActor; @@ -37,7 +56,13 @@ private: void CaptureViewToImageTick(); - void AsyncWriteImageToDisk(TArray& AtlasPixels, const int32 AtlasW, const int32 AtlasH); + // :发起 Capture + Enqueue GPU Readback + void BeginCaptureViewToAtlas(); + + // 轮询并取回 GPU 数据,再拼图 + void TryResolveCaptureViewAtlas(); + + void AsyncWriteImageToDisk(TArray AtlasPixels, const int32 AtlasW, const int32 AtlasH); TArray> AllCaptureCameras; @@ -45,4 +70,7 @@ private: UPROPERTY() FString OutPutDirectoryPath; + TUniquePtr PendingAtlasCapture; + bool bReadbackInFlight = false; + }; diff --git a/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp b/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp index c5dc8db..c9391ad 100644 --- a/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp +++ b/Plugins/CameraCapture/Source/CameraCaptureSubsystem.cpp @@ -76,72 +76,194 @@ TStatId UCameraCaptureSubSystem::GetStatId() const void UCameraCaptureSubSystem::CaptureViewToImageTick() { - if (AllCaptureCameras.IsEmpty()) - { - return; - } + if (AllCaptureCameras.IsEmpty()) + { + return; + } - TArray AtlasPixels; + // 已经有一批在等 GPU 回读,就先尝试取回 + if (bReadbackInFlight) + { + TryResolveCaptureViewAtlas(); + return; + } + + // 没有在飞行中的回读,就启动新的一批 + BeginCaptureViewToAtlas(); + +} + +void UCameraCaptureSubSystem::BeginCaptureViewToAtlas() +{ 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; - } + PendingAtlasCapture = MakeUnique(); + PendingAtlasCapture->FrameId = (int64)GFrameCounter; + PendingAtlasCapture->AtlasW = AtlasW; + PendingAtlasCapture->AtlasH = AtlasH; - UTextureRenderTarget2D* RenderTarget = AllCaptureCameras[CameraIndex]->GetViewRT(); - if (!RenderTarget) - { - continue; - } + const int32 MaxTiles = AtlasGridX * AtlasGridY; + const int32 CameraCount = FMath::Min(AllCaptureCameras.Num(), MaxTiles); - FTextureRenderTargetResource* RenderTargetRes = RenderTarget->GameThread_GetRenderTargetResource(); - if (!RenderTargetRes) + PendingAtlasCapture->Items.Reserve(CameraCount); + + for (int32 CameraIndex = 0; CameraIndex < CameraCount; ++CameraIndex) + { + if (!AllCaptureCameras[CameraIndex].IsValid()) { 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) + ACameraCaptureActor* CaptureActor = AllCaptureCameras[CameraIndex].Get(); + if (!CaptureActor) { - 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]; - } + continue; } - } - AsyncWriteImageToDisk(AtlasPixels, AtlasW, AtlasH); + // 先让这台 SceneCapture 更新一次 RT + CaptureActor->CaptureNow(); // 你自己封装一个函数,内部调用 ViewCapture->CaptureScene() + + UTextureRenderTarget2D* RenderTarget = CaptureActor->GetViewRT(); + if (!RenderTarget) + { + continue; + } + + FTextureRenderTargetResource* RTRes = RenderTarget->GameThread_GetRenderTargetResource(); + if (!RTRes) + { + continue; + } + + FRHITexture* SourceTexture = RTRes->GetRenderTargetTexture(); + if (!SourceTexture) + { + continue; + } + + FCameraReadbackItem Item; + Item.CameraIndex = CameraIndex; + Item.Width = ViewWidth; + Item.Height = ViewHeight; + Item.Readback = MakeUnique( + FName(*FString::Printf(TEXT("CameraReadback_%d"), CameraIndex)) + ); + + FRHIGPUTextureReadback* ReadbackPtr = Item.Readback.Get(); + const FResolveRect CopyRect(0, 0, ViewWidth, ViewHeight); + + ENQUEUE_RENDER_COMMAND(QueueCameraTextureReadback)( + [ReadbackPtr, SourceTexture, CopyRect](FRHICommandListImmediate& RHICmdList) + { + if (ReadbackPtr && SourceTexture) + { + ReadbackPtr->EnqueueCopy(RHICmdList, SourceTexture, CopyRect); + } + }); + + PendingAtlasCapture->Items.Add(MoveTemp(Item)); + } + + bReadbackInFlight = PendingAtlasCapture->Items.Num() > 0; } -void UCameraCaptureSubSystem::AsyncWriteImageToDisk(TArray& AtlasPixels, const int32 AtlasW, const int32 AtlasH) +void UCameraCaptureSubSystem::TryResolveCaptureViewAtlas() +{ + if (!bReadbackInFlight || !PendingAtlasCapture.IsValid()) + { + return; + } + + // 只要有一个还没准备好,就下一帧再来 + for (const FCameraReadbackItem& Item : PendingAtlasCapture->Items) + { + if (!Item.Readback.IsValid() || !Item.Readback->IsReady()) + { + return; + } + } + + // 全部 ready,开始组装 atlas + TArray AtlasPixels; + AtlasPixels.SetNumZeroed(PendingAtlasCapture->AtlasW * PendingAtlasCapture->AtlasH); + + for (const FCameraReadbackItem& Item : PendingAtlasCapture->Items) + { + if (!Item.Readback.IsValid()) + { + continue; + } + + int32 RowPitchInPixels = 0; + int32 BufferHeight = 0; + + void* RawPtr = Item.Readback->Lock(RowPitchInPixels, &BufferHeight); + if (!RawPtr) + { + Item.Readback->Unlock(); + continue; + } + + // RawPtr 指向 GPU 读回后的 CPU 可访问数据 + const FColor* SrcPixels = static_cast(RawPtr); + + // 先拷到一个连续的小图缓存里 + TArray ViewPixels; + ViewPixels.SetNumUninitialized(Item.Width * Item.Height); + + for (int32 Y = 0; Y < Item.Height; ++Y) + { + const FColor* SrcRow = SrcPixels + Y * RowPitchInPixels; + FColor* DstRow = ViewPixels.GetData() + Y * Item.Width; + FMemory::Memcpy(DstRow, SrcRow, sizeof(FColor) * Item.Width); + } + + Item.Readback->Unlock(); + + // 如果你最后导出的图上下颠倒,这里保留翻转 + FlipVertical(ViewPixels, Item.Width, Item.Height); + + const int32 GX = Item.CameraIndex % AtlasGridX; + const int32 GY = Item.CameraIndex / AtlasGridX; + + const int32 DstX0 = GX * Item.Width; + const int32 DstY0 = GY * Item.Height; + + for (int32 Y = 0; Y < Item.Height; ++Y) + { + const int32 AtlasY = DstY0 + Y; + if (AtlasY < 0 || AtlasY >= PendingAtlasCapture->AtlasH) + { + continue; + } + + const int32 SrcRowIndex = Y * Item.Width; + const int32 DstRowIndex = AtlasY * PendingAtlasCapture->AtlasW; + + for (int32 X = 0; X < Item.Width; ++X) + { + const int32 AtlasX = DstX0 + X; + if (AtlasX < 0 || AtlasX >= PendingAtlasCapture->AtlasW) + { + continue; + } + + AtlasPixels[DstRowIndex + AtlasX] = ViewPixels[SrcRowIndex + X]; + } + } + } + + const int32 AtlasW = PendingAtlasCapture->AtlasW; + const int32 AtlasH = PendingAtlasCapture->AtlasH; + + PendingAtlasCapture.Reset(); + bReadbackInFlight = false; + + AsyncWriteImageToDisk(MoveTemp(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) );