#include "PushFieldManager.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Kismet/KismetRenderingLibrary.h"
#include "Kismet/KismetMathLibrary.h"
#include "Materials/MaterialInstanceDynamic.h"
APushFieldManager::APushFieldManager()
PrimaryActorTick.bCanEverTick = true;
static UTextureRenderTarget2D* MakeRT(UObject* Outer, int32 Size)
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>(Outer);
RT->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA16f;
RT->ClearColor = FLinearColor::Black;
RT->InitAutoFormat(Size, Size);
RT->UpdateResourceImmediate(true);
void APushFieldManager::BeginPlay()
RT_A = MakeRT(this, Size);
RT_B = MakeRT(this, Size);
if (DecayMaterial) MID_Decay = UMaterialInstanceDynamic::Create(DecayMaterial, this);
if (StampMaterial) MID_Stamp = UMaterialInstanceDynamic::Create(StampMaterial, this);
MoverPosWS.SetNum(MoverCount);
MoverVelWS.SetNum(MoverCount);
const FVector2D Min = FVector2D(GetActorLocation()) - FieldSizeWS * 0.5f;
const FVector2D Max = FVector2D(GetActorLocation()) + FieldSizeWS * 0.5f;
for (int32 i = 0; i < MoverCount; ++i)
MoverPosWS[i] = FVector2D(
FMath::FRandRange(Min.X, Max.X),
FMath::FRandRange(Min.Y, Max.Y)
MoverVelWS[i] = FVector2D::ZeroVector;
UKismetRenderingLibrary::ClearRenderTarget2D(GetWorld(), RT_A, FLinearColor::Black);
UKismetRenderingLibrary::ClearRenderTarget2D(GetWorld(), RT_B, FLinearColor::Black);
void APushFieldManager::Tick(float DeltaSeconds)
Super::Tick(DeltaSeconds);
UpdateMovers(DeltaSeconds);
bUseA = !bUseA; // ping-pong swap
FVector2D APushFieldManager::WorldToUV(const FVector2D& P) const
const FVector2D Center = FVector2D(GetActorLocation());
const FVector2D Min = Center - FieldSizeWS * 0.5f;
FVector2D UV = (P - Min) / FieldSizeWS;
UV.X = FMath::Clamp(UV.X, 0.f, 1.f);
UV.Y = FMath::Clamp(UV.Y, 0.f, 1.f);
void APushFieldManager::UpdateMovers(float Dt)
// Simple dummy motion (replace with curl later)
const FVector2D Center = FVector2D(GetActorLocation());
const FVector2D Min = Center - FieldSizeWS * 0.5f;
const FVector2D Max = Center + FieldSizeWS * 0.5f;
for (int32 i = 0; i < MoverCount; ++i)
MoverVelWS[i] = FVector2D(600.f, 420.f);
MoverPosWS[i] += MoverVelWS[i] * Dt;
if (MoverPosWS[i].X > Max.X) MoverPosWS[i].X = Min.X;
if (MoverPosWS[i].Y > Max.Y) MoverPosWS[i].Y = Min.Y;
void APushFieldManager::DrawField()
UTextureRenderTarget2D* Prev = bUseA ? RT_A : RT_B;
UTextureRenderTarget2D* Next = bUseA ? RT_B : RT_A;
// 1) Decay pass: Next = Prev * Decay
MID_Decay->SetTextureParameterValue(TEXT("PrevTex"), Prev);
MID_Decay->SetScalarParameterValue(TEXT("Decay"), Decay);
UKismetRenderingLibrary::DrawMaterialToRenderTarget(GetWorld(), Next, MID_Decay);
UKismetRenderingLibrary::ClearRenderTarget2D(GetWorld(), Next, FLinearColor::Black);
// 2) Stamps (additive material)
const float RadiusUV = StampRadiusWS / FieldSizeWS.X; // assuming square-ish field
for (int32 i = 0; i < MoverCount; ++i)
const FVector2D UV = WorldToUV(MoverPosWS[i]);
const FVector2D Dir = MoverVelWS[i].GetSafeNormal();
MID_Stamp->SetVectorParameterValue(TEXT("CenterUV"), FLinearColor(UV.X, UV.Y, 0, 0));
MID_Stamp->SetScalarParameterValue(TEXT("RadiusUV"), RadiusUV);
MID_Stamp->SetVectorParameterValue(TEXT("DirXY"), FLinearColor(Dir.X, Dir.Y, 0, 0));
MID_Stamp->SetScalarParameterValue(TEXT("Strength"), StampStrength);
// Draw onto Next; stamp material must be additive to accumulate.
UKismetRenderingLibrary::DrawMaterialToRenderTarget(GetWorld(), Next, MID_Stamp);