diff --git a/Content/Blueprints/BP_CameraCaptureActor.uasset b/Content/Blueprints/BP_CameraCaptureActor.uasset new file mode 100644 index 0000000..76997a9 Binary files /dev/null and b/Content/Blueprints/BP_CameraCaptureActor.uasset differ diff --git a/Content/Levels/L_Test.umap b/Content/Levels/L_Test.umap index 6f6f0cc..02f9153 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 new file mode 100644 index 0000000..33aa258 --- /dev/null +++ b/Plugins/CameraCapture/Source/CameraCaptureActor.cpp @@ -0,0 +1,245 @@ +#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; +} + +void ACameraCaptureActor::BeginPlay() +{ + Super::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) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + // 任何关键参数变化都重建(避免 RT 尺寸/数量不匹配) + BuildViewCaptures(); +} +#endif + +void ACameraCaptureActor::BuildViewCaptures() +{ + // Destroy old components + for (USceneCaptureComponent2D* Comp : ViewCaptures) + { + 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); + } + + // Atlas RT (optional) + if (bGenerateAtlas) + { + 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); + } +} + +void ACameraCaptureActor::UpdateViewTransforms() +{ + // 以本 Actor(CameraActor)的 transform 为中心 + const FVector Center = GetActorLocation(); + const FRotator Rot = GetActorRotation(); + const FVector Right = Rot.Quaternion().GetRightVector(); + + // FOV 从主 CameraComponent 取 + float FOV = 90.f; + if (UCameraComponent* Cam = GetCameraComponent()) + { + FOV = Cam->FieldOfView; + } + + for (int32 i = 0; i < ViewCaptures.Num(); ++i) + { + if (!ViewCaptures[i]) continue; + + // 线性水平分布:居中对称 + const float Offset = (i - (ViewCount - 1) * 0.5f) * Baseline; + const FVector Pos = Center + Right * Offset; + + ViewCaptures[i]->SetWorldLocationAndRotation(Pos, Rot); + ViewCaptures[i]->FOVAngle = FOV; + } +} + +void ACameraCaptureActor::CaptureViews() +{ + for (USceneCaptureComponent2D* Capture : ViewCaptures) + { + if (Capture) + { + Capture->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 new file mode 100644 index 0000000..218f921 --- /dev/null +++ b/Plugins/CameraCapture/Source/CameraCaptureActor.h @@ -0,0 +1,75 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Camera/CameraActor.h" +#include "CameraCaptureActor.generated.h" + +UENUM(BlueprintType) +enum class EViewDistribution : uint8 +{ + LinearHorizontal UMETA(DisplayName = "Linear Horizontal"), + Circular UMETA(DisplayName = "Circular") +}; + +UCLASS() +class CAMERACAPTURE_API ACameraCaptureActor : public ACameraActor +{ + GENERATED_BODY() + +public: + ACameraCaptureActor(); + + // ===== 多视图参数 ===== + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiView") + int32 ViewCount = 9; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiView") + float Baseline = 5.0f; // cm + + 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; +#endif + +private: + void BuildViewCaptures(); + void UpdateViewTransforms(); + void CaptureViews(); + void GenerateAtlas(); + +private: + UPROPERTY(Transient) + TArray ViewCaptures; + + UPROPERTY(Transient) + TArray ViewRTs; + + UPROPERTY(Transient) + class UTextureRenderTarget2D* AtlasRT; +}; \ No newline at end of file diff --git a/Source/CameraCaptureDemo.Target.cs b/Source/CameraCaptureDemo.Target.cs index dfafe8e..b1cf729 100644 --- a/Source/CameraCaptureDemo.Target.cs +++ b/Source/CameraCaptureDemo.Target.cs @@ -1,4 +1,4 @@ -// Copyright 2026 (c) Jupiter. All Rights Reserved. +// Copyright Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; using System.Collections.Generic; diff --git a/Source/CameraCaptureDemo/CameraCaptureDemo.Build.cs b/Source/CameraCaptureDemo/CameraCaptureDemo.Build.cs index a8f6631..183a0ca 100644 --- a/Source/CameraCaptureDemo/CameraCaptureDemo.Build.cs +++ b/Source/CameraCaptureDemo/CameraCaptureDemo.Build.cs @@ -1,4 +1,4 @@ -// Copyright 2026 (c) Jupiter. All Rights Reserved. +// Copyright Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; diff --git a/Source/CameraCaptureDemo/CameraCaptureDemo.cpp b/Source/CameraCaptureDemo/CameraCaptureDemo.cpp index 131af7a..95bec95 100644 --- a/Source/CameraCaptureDemo/CameraCaptureDemo.cpp +++ b/Source/CameraCaptureDemo/CameraCaptureDemo.cpp @@ -1,4 +1,4 @@ -// Copyright 2026 (c) Jupiter. All Rights Reserved. +// Copyright Epic Games, Inc. All Rights Reserved. #include "CameraCaptureDemo.h" #include "Modules/ModuleManager.h" diff --git a/Source/CameraCaptureDemo/CameraCaptureDemo.h b/Source/CameraCaptureDemo/CameraCaptureDemo.h index aabcf54..677c8e2 100644 --- a/Source/CameraCaptureDemo/CameraCaptureDemo.h +++ b/Source/CameraCaptureDemo/CameraCaptureDemo.h @@ -1,4 +1,4 @@ -// Copyright 2026 (c) Jupiter. All Rights Reserved. +// Copyright Epic Games, Inc. All Rights Reserved. #pragma once diff --git a/Source/CameraCaptureDemoEditor.Target.cs b/Source/CameraCaptureDemoEditor.Target.cs index e381b09..d0d1079 100644 --- a/Source/CameraCaptureDemoEditor.Target.cs +++ b/Source/CameraCaptureDemoEditor.Target.cs @@ -1,4 +1,4 @@ -// Copyright 2026 (c) Jupiter. All Rights Reserved. +// Copyright Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; using System.Collections.Generic;