外观
RTStamp_Design
约 4088 字大约 14 分钟
2026-08-26
1. 概述
1.1 功能目标
实现 RT Stamp 系统:当技能命中带有指定 Tag 的建筑或地板时,在该物体表面烘焙圆形标记,标记随时间逐帧 fade 消失。
1.2 技术方案概述
基于 WPO UV 展开 + SceneCapture + 双 RenderTarget 的世界空间绘制方案。
核心思路:通过一个特殊材质(Unwrap 材质)利用 World Position Offset 将 mesh 按 UV1 展开为平面,配合正交 SceneCapture 从正上方拍摄,将世界空间中的 SphereMask(命中点画圆)烘焙到 RT 上。物体的显示材质采样该 RT 实现叠加效果,每帧对 RT 做 fade 衰减实现消散。
1.3 核心渲染流程(基于 Demo 验证)
命中发生
│
▼
保存物体原始材质 → 替换为 Unwrap 材质(M_FX_RenderTarget2D_Unwrap)
│
▼
设置材质参数:CaptureLocation、HitLocation、Radius、CaptureSize
│
▼
SceneCapture(SCCM_Overwrite)→ 拍摄到 RT_Primary(干净快照)
│
▼
恢复物体原始材质
│
▼
Canvas K2_DrawMaterial(Merge 材质 Additive)→ RT_Primary 叠加写入 RT_Temp(拖尾累积)
│
▼
每帧 Tick:BeginDrawCanvasToRenderTarget(RT_Temp)
→ K2_DrawMaterial(FadeMaterial, fullscreen)
→ EndDrawCanvasToRenderTarget1.4 模块定位
非独立 Module。遵循项目现有框架,融入 Subsystems/ 和 World/Pool/ 目录结构,使用 Paper 前缀命名。
1.5 目标平台
PC / Console(主机)。
2. 文件结构
Subsystems/
└── RTStamp/
├── PaperRTStampSubsystem.h
├── PaperRTStampSubsystem.cpp
├── PaperRTStampTypes.h
└── PaperRTStampConfig.h/.cpp
World/
└── Pool/
├── PaperRTPool.h
├── PaperRTPool.cpp
├── (已有) PaperComponentPool.h/.cpp
└── (已有) PaperObjectPool.h/.cpp2.1 职责划分
| 文件 | 职责 |
|---|---|
PaperRTStampSubsystem | 对外接口、命中处理、Binding 管理、Tick fade/回收、SceneCapture 调度、生命周期 |
PaperRTPool | RT 资源池:分配、回收、分辨率管理。纯资源管理,不涉及业务逻辑。注意:每个 Slot 包含双 RT(Primary + Temp) |
PaperRTStampTypes | 所有结构体和枚举 |
PaperRTStampConfig | UDataAsset 配置 |
3. 材质体系(已由 Demo 验证)
3.1 Unwrap 材质(M_FX_RenderTarget2D_Unwrap)
用途: 命中时临时替换到目标物体上,配合 SceneCapture 将命中标记烘焙到 RT。
World Position Offset(UV 展开逻辑):
TexCoord[1] → 分离 R(=U) 和 G(=V)
│
▼
构建 2D 坐标: Append(1-V, U) // V 翻转 + UV 交换
│
▼
减 0.5(居中到 [-0.5, 0.5])
│
▼
乘 CaptureSize(缩放到世界空间尺寸,默认 1024)
│
▼
Append Z=0(扩展为 3D)
│
▼
加 CaptureLocation(平移到 Capture 摄像机前方)
│
▼
减去 WorldPosition(原始位置)→ 得到最终 WPO 偏移效果:mesh 的每个顶点被重新定位到以 CaptureLocation 为中心的 XY 平面上,按 UV1 布局展开。
Emissive Color(命中标记绘制):
SphereMask(
A = AbsoluteWorldPosition (Excluding Shader Offsets), // 排除 WPO 的原始世界坐标
B = HitLocation (VectorParameter),
Radius = Radius (ScalarParameter, 默认 100),
Hardness = 10%
) → Emissive Color注意:SphereMask 的 A 输入使用 Excluding Shader Offsets 模式,获取的是 WPO 之前的原始世界坐标。这样球形 mask 在原始 3D 空间中判定,但渲染到的是 UV 展开后的 RT 上。
材质参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
CaptureLocation | Vector | (0,0,0) | SceneCapture Actor / 展开平面中心的世界坐标 |
CaptureSize | Scalar | 1024 | 展开平面的世界空间尺寸 |
HitLocation | Vector | (0,0,0) | 技能命中点的世界坐标 |
Radius | Scalar | 100 | SphereMask 半径(世界空间单位) |
关键设计决策: 在世界空间用 SphereMask 而非 UV 空间画圆,规避了 UV 密度不均匀导致的圆形畸变问题。SphereMask 在原始 3D 空间中是正圆,映射到 RT 上后结果自然正确。
3.2 显示材质(物体原始材质的改造)
物体的原始材质需要增加 RT 叠加层。在 BeginPlay 时通过 CreateDynamicMaterialInstance 创建 MID,由 Subsystem 设置 RT 参数。
// 采样 RT_Temp 获得带拖尾的完整效果(RT_Temp 是叠加累积缓冲)
float StampMask = Texture2DSample(RenderTarget_Temp, TexCoord).r;
float3 FinalColor = lerp(OriginalColor, StampColor, StampMask * StampIntensity);| 参数名 | 类型 | 说明 |
|---|---|---|
RenderTarget | Texture | RT_Primary,干净快照,由 Subsystem 设入 |
RenderTarget_Temp | Texture | RT_Temp,叠加累积 + fade 衰减缓冲,主显示通道 |
3.3 Fade 材质(M_FX_RenderTarget2d_Fade)
每帧通过 Canvas DrawMaterial 绘制到 RT_Temp 上,实现逐帧衰减。
运行时通过 MID 设置的参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
Opacity | Scalar | 每帧衰减乘数,由 Subsystem Tick 设置为 pow(RecycleThreshold, DeltaTime / Duration) |
RenderTarget_Temp | Texture | 自采样输入(读取当前 RT_Temp 内容,乘衰减后写回) |
3.4 Stamp Merge 材质(M_FX_RenderTarget2D_Merge)
命中时通过 Canvas DrawMaterial 将 RT_Primary 叠加写入 RT_Temp,实现拖尾累积。
必须:Blend Mode = Additive(Canvas 的混合行为由材质 Blend Mode 决定)
运行时通过局部 MID 设置的参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
RenderTarget | Texture | RT_Primary,由 ExecuteCapture 每次命中时设入 |
材质逻辑:TexCoord[0] 采样 RenderTarget,输出到 Emissive。Additive Blend Mode 确保 Canvas 将输出值累加到 RT_Temp 现有内容上。
4. 核心数据结构
定义在 PaperRTStampTypes.h 中。
4.1 ERTStampResolution
UENUM(BlueprintType)
enum class ERTStampResolution : uint8
{
Low = 0, // 256x256
Medium = 1, // 512x512
High = 2, // 1024x1024
};4.2 FRTStampBinding
USTRUCT()
struct FRTStampBinding
{
GENERATED_BODY()
// 绑定的目标组件(弱引用,不阻止 GC)
TWeakObjectPtr<UPrimitiveComponent> TargetComp;
int32 SlotIndex = INDEX_NONE;
float CurrentMaxAlpha = 1.f;
float LastHitTime = 0.f;
// 为 false 时不衰减,需通过 StartFade 手动触发
bool bFadeActive = false;
// 每 Binding 可覆盖的衰减时长(秒),<= 0 使用 Config 默认
float FadeDurationOverride = -1.f;
// 最近一次命中的 Radius(取历史最大值),用于 Radius 缩放衰减
float LastHitRadius = 100.f;
// 该 Binding 使用的材质 slot 索引
int32 OriginalMaterialIndex = 0;
// 保存的原始材质
UPROPERTY()
TObjectPtr<UMaterialInterface> OriginalMaterial = nullptr;
// 显示用 MID(采样 RT_Primary 和 RT_Temp)
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> DisplayMID = nullptr;
// Fade 用 MID(自采样 RT_Temp 做衰减)
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> FadeMID = nullptr;
};4.3 FRTStampHitParams
外部调用 OnStampHit 时传入的参数。不需要 HitResult 中的 trace 信息,只需要世界坐标。
USTRUCT(BlueprintType)
struct FRTStampHitParams
{
GENERATED_BODY()
// 命中点世界坐标
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector HitLocation = FVector::ZeroVector;
// 命中的目标组件
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UPrimitiveComponent> HitComponent = nullptr;
// SphereMask 半径(世界空间单位)
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Radius = 100.f;
// Fade 衰减控制
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Opacity = 1.f;
};5. UPaperRTStampConfig
UCLASS(BlueprintType)
class UPaperRTStampConfig : public UDataAsset
{
GENERATED_BODY()
public:
// ---- Pool ----
UPROPERTY(EditAnywhere, Category="Pool", meta=(ClampMin="1", ClampMax="64"))
int32 MaxPoolSize = 16;
// ---- Resolution ----
UPROPERTY(EditAnywhere, Category="Resolution")
float HighResDistance = 1000.f;
UPROPERTY(EditAnywhere, Category="Resolution")
float MedResDistance = 3000.f;
// ---- Capture ----
// Unwrap 展开平面的世界空间尺寸(对应材质的 CaptureSize 参数)
UPROPERTY(EditAnywhere, Category="Capture")
float CaptureSize = 1024.f;
// ---- Fade ----
// 为 true 时命中即自动开始衰减;为 false 时需通过 StartFade 手动触发
UPROPERTY(EditAnywhere, Category="Fade")
bool bAutoFade = true;
// 从满 alpha 衰减到 RecycleThreshold 所需的时间(秒)
UPROPERTY(EditAnywhere, Category="Fade", meta=(ClampMin="0.01"))
float FadeDuration = 5.f;
// 低于此阈值认为已完全消失,回收 Slot
UPROPERTY(EditAnywhere, Category="Fade", meta=(ClampMin="0.001", ClampMax="0.1"))
float RecycleThreshold = 0.01f;
// 为 true 时 FadeDuration 按 (Radius / BaseRadius)^RadiusFadeExponent 缩放
UPROPERTY(EditAnywhere, Category="Fade")
bool bScaleFadeByRadius = false;
// 缩放参考半径。Radius == BaseRadius 时实际时长 == FadeDuration
UPROPERTY(EditAnywhere, Category="Fade", meta=(ClampMin="1.0", EditCondition="bScaleFadeByRadius"))
float BaseRadius = 100.f;
// 缩放指数。0.5 对应 sqrt(Radius),匹配 Sculpt 位移公式
UPROPERTY(EditAnywhere, Category="Fade", meta=(ClampMin="0.01", ClampMax="2.0", EditCondition="bScaleFadeByRadius"))
float RadiusFadeExponent = 0.5f;
// ---- Eviction ----
// 驱逐评分中的时间权重
UPROPERTY(EditAnywhere, Category="Eviction")
float AgeWeight = 0.1f;
// ---- Material ----
// Unwrap 材质(用于 SceneCapture 时临时替换)
UPROPERTY(EditAnywhere, Category="Material")
TObjectPtr<UMaterialInterface> UnwrapMaterial = nullptr;
// Fade 材质(用于每帧 Canvas DrawMaterial 衰减)
UPROPERTY(EditAnywhere, Category="Material")
TObjectPtr<UMaterialInterface> FadeMaterial = nullptr;
// Stamp Merge 材质(Canvas Additive 绘制,将 RT_Primary 叠加写入 RT_Temp;Blend Mode 必须为 Additive)
UPROPERTY(EditAnywhere, Category="Material")
TObjectPtr<UMaterialInterface> StampMergeMaterial = nullptr;
// RT 纹理格式
UPROPERTY(EditAnywhere, Category="Material")
TEnumAsByte<ETextureRenderTargetFormat> RTFormat = RTF_RGBA16f;
};6. UPaperRTPool
放置于 World/Pool/,纯资源管理,不引用任何 RTStamp 业务类型。每个 Slot 含双 RT。
6.1 FRTPoolSlot(定义于 PaperRTPool.h)
USTRUCT()
struct FRTPoolSlot
{
GENERATED_BODY()
UPROPERTY()
TObjectPtr<UTextureRenderTarget2D> RT_Primary = nullptr;
UPROPERTY()
TObjectPtr<UTextureRenderTarget2D> RT_Temp = nullptr;
bool bInUse = false;
float LastActiveTime = 0.f;
int32 SlotIndex = INDEX_NONE;
int32 PixelSize = 0; // 实际像素尺寸,例如 256 / 512 / 1024
};6.2 类声明
UCLASS()
class UPaperRTPool : public UObject
{
GENERATED_BODY()
public:
void Initialize(UObject* InOuter, int32 InMaxPoolSize,
TEnumAsByte<ETextureRenderTargetFormat> InFormat);
void ReleaseAll();
// 分配一个 Slot,返回 SlotIndex;优先级:空闲同尺寸 > 空闲不同尺寸(重建)> 新建 > INDEX_NONE
int32 Allocate(int32 PixelSize);
void Release(int32 SlotIndex);
void ForceRelease(int32 SlotIndex);
UTextureRenderTarget2D* GetPrimaryRT(int32 SlotIndex) const;
UTextureRenderTarget2D* GetTempRT(int32 SlotIndex) const;
int32 GetActiveCount() const;
bool IsSlotValid(int32 SlotIndex) const;
void ReinitSlotSize(int32 SlotIndex, int32 NewPixelSize);
private:
UPROPERTY()
TArray<FRTPoolSlot> Slots;
int32 MaxPoolSize = 16;
TEnumAsByte<ETextureRenderTargetFormat> Format = RTF_RGBA16f;
void CreateRTPair(FRTPoolSlot& OutSlot, int32 PixelSize);
};Debug CVar:
Paper.RTPool.Debug(0=off, 1=on),启用后输出 RT 分配/创建/回收的详细日志。
6.3 CreateRTPair
void UPaperRTPool::CreateRTPair(FRTPoolSlot& OutSlot, int32 PixelSize)
{
// Primary RT
OutSlot.RT_Primary = NewObject<UTextureRenderTarget2D>(this);
OutSlot.RT_Primary->RenderTargetFormat = Format;
OutSlot.RT_Primary->InitAutoFormat(PixelSize, PixelSize);
OutSlot.RT_Primary->ClearColor = FLinearColor::Black;
OutSlot.RT_Primary->UpdateResourceImmediate(true);
// Temp RT(相同规格,用于 fade 自采样)
OutSlot.RT_Temp = NewObject<UTextureRenderTarget2D>(this);
OutSlot.RT_Temp->RenderTargetFormat = Format;
OutSlot.RT_Temp->InitAutoFormat(PixelSize, PixelSize);
OutSlot.RT_Temp->ClearColor = FLinearColor::Black;
OutSlot.RT_Temp->UpdateResourceImmediate(true);
OutSlot.PixelSize = PixelSize;
}6.4 Allocate 逻辑
Allocate(int32 PixelSize):
│
├─ 1. 优先查找:空闲 + 同 PixelSize
│
├─ 2. 其次:空闲 + 不同 PixelSize → ReinitSlotSize
│
├─ 3. 池未满:新建 Slot(含 Primary + Temp),RT 创建失败时回退
│
└─ 4. 池满且无空闲 → 返回 INDEX_NONE(驱逐由 Subsystem 决策)ERTStampResolution → PixelSize 转换 由调用方(
UPaperRTStampSubsystem::ResolutionToPixelSize)负责,在调用Allocate之前完成。池本身只处理原始像素尺寸。
线程安全:
Allocate入口有check(IsInGameThread()),所有 RT 池操作限游戏线程。
7. UPaperRTStampSubsystem
7.1 类声明
UCLASS()
class UPaperRTStampSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
// ==========================================
// 外部接口(Gameplay 调用)
// ==========================================
/** 在目标组件表面烘焙一个标记 */
UFUNCTION(BlueprintCallable, Category="PaperRTStamp")
void OnStampHit(const FRTStampHitParams& Params);
/** 强制清理所有标记 */
UFUNCTION(BlueprintCallable, Category="PaperRTStamp")
void ClearAll();
/** 运行时切换配置 */
UFUNCTION(BlueprintCallable, Category="PaperRTStamp")
void SetConfig(UPaperRTStampConfig* InConfig);
/** 启动指定组件的衰减。FadeDuration=0 使用 Config 默认值 */
UFUNCTION(BlueprintCallable, Category="PaperRTStamp")
void StartFade(UPrimitiveComponent* Comp, float FadeDuration = 0.f);
/** 启动指定 Actor 下所有组件的衰减 */
UFUNCTION(BlueprintCallable, Category="PaperRTStamp")
void StartFadeByActor(AActor* Actor, float FadeDuration = 0.f);
/** 启动所有活跃标记的衰减 */
UFUNCTION(BlueprintCallable, Category="PaperRTStamp")
void StartFadeAll(float FadeDuration = 0.f);
private:
UPROPERTY()
TObjectPtr<UPaperRTStampConfig> Config = nullptr;
UPROPERTY()
TObjectPtr<UPaperRTPool> RTPool = nullptr;
TMap<FObjectKey, FRTStampBinding> ActiveBindings;
UPROPERTY()
TObjectPtr<ASceneCapture2D> SharedCaptureActor = nullptr;
FTSTicker::FDelegateHandle TickHandle;
// ---- 分辨率转换 ----
static int32 ResolutionToPixelSize(ERTStampResolution Resolution);
// ---- 命中处理 ----
ERTStampResolution CalcResolution(const FVector& HitPoint) const;
void SetupMaterialBinding(UPrimitiveComponent* Comp,
UTextureRenderTarget2D* PrimaryRT,
UTextureRenderTarget2D* TempRT,
FRTStampBinding& OutBinding);
void ExecuteCapture(UPrimitiveComponent* Comp,
UTextureRenderTarget2D* PrimaryRT,
UTextureRenderTarget2D* TempRT,
const FRTStampHitParams& Params,
FRTStampBinding& Binding);
// ---- 驱逐 ----
int32 FindEvictionCandidate() const;
int32 AllocateWithEviction(int32 PixelSize);
// ---- 回收 ----
void RestoreMaterial(const FRTStampBinding& Binding);
void CleanupBinding(const FObjectKey& Key);
// ---- 每帧 ----
bool TickFadeAndRecycle(float DeltaTime);
void DrawFadePass(FRTStampBinding& Binding, UTextureRenderTarget2D* TempRT);
// ---- 生命周期 ----
ASceneCapture2D* GetOrCreateSharedCaptureActor();
};8. 核心流程
8.1 OnStampHit 命中流程
OnStampHit(Params):
│
├─ 1. 校验
│ Comp = Params.HitComponent
│ if (!Comp) → Warning + return
│ if (!Config) → Warning + return
│ if (!Config->UnwrapMaterial || !Config->FadeMaterial || !Config->StampMergeMaterial) → Warning + return
│
├─ 2. 查找已有 Binding(re-hit 路径)
│ Key = FObjectKey(Comp)
│ if (ActiveBindings.Contains(Key)):
│ Binding.CurrentMaxAlpha = 1.0
│ Binding.bFadeActive = Config->bAutoFade
│ Binding.FadeDurationOverride = -1
│ Binding.LastHitRadius = Max(Binding.LastHitRadius, Params.Radius)
│ Binding.LastHitTime = Now
│ ExecuteCapture(...)
│ return ← 在已有 RT 上叠加绘制
│
├─ 3. 计算分辨率档位,转换为像素尺寸
│ Resolution = CalcResolution(Params.HitLocation)
│ PixelSize = ResolutionToPixelSize(Resolution)
│
├─ 4. 分配 Slot(含驱逐)
│ SlotIdx = AllocateWithEviction(PixelSize)
│ if (SlotIdx == INDEX_NONE) → return
│
├─ 5. Clear 双 RT
│ PrimaryRT = RTPool->GetPrimaryRT(SlotIdx)
│ TempRT = RTPool->GetTempRT(SlotIdx)
│ ClearRenderTarget2D(PrimaryRT, Black)
│ ClearRenderTarget2D(TempRT, Black)
│
├─ 6. 创建 Binding + 材质绑定
│ NewBinding.TargetComp = Comp ← 弱引用,用于 Tick 中存活检查和材质恢复
│ NewBinding.bFadeActive = Config->bAutoFade
│ NewBinding.LastHitRadius = Params.Radius
│ SetupMaterialBinding(Comp, PrimaryRT, TempRT, NewBinding)
│
├─ 7. 执行 Capture
│ ExecuteCapture(Comp, PrimaryRT, TempRT, Params, NewBinding)
│
└─ 8. 存入 ActiveBindings8.2 SetupMaterialBinding
首次绑定时的完整材质设置流程:
SetupMaterialBinding(Comp, PrimaryRT, TempRT, OutBinding):
│
├─ 1. 保存原始材质
│ OutBinding.OriginalMaterial = Comp->GetMaterial(OutBinding.OriginalMaterialIndex)
│
├─ 2. 创建显示用 MID
│ DisplayMID = Comp->CreateDynamicMaterialInstance(OutBinding.OriginalMaterialIndex, OriginalMaterial)
│ OutBinding.DisplayMID = DisplayMID
│
├─ 3. 设置显示 MID 的 RT 参数
│ DisplayMID->SetTextureParameterValue("RenderTarget", PrimaryRT)
│ DisplayMID->SetTextureParameterValue("RenderTarget_Temp", TempRT)
│
├─ 4. 创建 Fade MID
│ FadeMID = UMaterialInstanceDynamic::Create(Config->FadeMaterial, this)
│ FadeMID->SetTextureParameterValue("RenderTarget_Temp", TempRT)
│ OutBinding.FadeMID = FadeMID8.3 ExecuteCapture
ExecuteCapture(Comp, PrimaryRT, TempRT, Params, Binding):
│
├─ 1. 获取 SharedCaptureActor(懒创建)
│ 创建时一次性配置(GetOrCreateSharedCaptureActor 中设置):
│ bCaptureEveryFrame = false
│ bCaptureOnMovement = false
│ CaptureSource = SCS_SceneColorHDR
│ PrimitiveRenderMode = PRM_UseShowOnlyList
│ 朝向 = 正对展开平面(看向 -Z,Rotation = (-90, 0, 0))
│ 每次 Capture 时设置:
│ ProjectionType = Orthographic(正交投影)
│ OrthoWidth = CaptureSize
│ ShowOnlyActors = { Comp 所属的 Actor }
│ 位置 = CaptureLocation(物体上方)
│
├─ 2. 创建 Unwrap 材质的 MID 并设置参数
│ UnwrapMID = UMaterialInstanceDynamic::Create(Config->UnwrapMaterial, ...)
│ CaptureLocation = Comp->GetComponentLocation() + (0, 0, CaptureSize * 0.5)
│ UnwrapMID->SetVectorParameterValue("CaptureLocation", CaptureLocation)
│ UnwrapMID->SetVectorParameterValue("HitLocation", Params.HitLocation)
│ UnwrapMID->SetScalarParameterValue("Radius", Params.Radius)
│ UnwrapMID->SetScalarParameterValue("CaptureSize", Config->CaptureSize)
│
├─ 3. 替换物体材质为 Unwrap 材质
│ Comp->SetMaterial(0, UnwrapMID)
│
├─ 4. Capture 到 Primary RT(SCCM_Overwrite,保留最新命中快照)
│ CaptureComp->CompositeMode = SCCM_Overwrite
│ CaptureComp->TextureTarget = PrimaryRT
│ CaptureComp->CaptureScene()
│
├─ 5. 恢复 CompositeMode 和物体材质
│ CaptureComp->CompositeMode = SCCM_Overwrite
│ Comp->SetMaterial(0, Binding.DisplayMID)
│
└─ 6. Canvas Additive 绘制 Primary → Temp(全屏四边形,比第二次 CaptureScene 便宜)
MergeMID = Create(Config->StampMergeMaterial)
MergeMID->SetTextureParameterValue("RenderTarget", PrimaryRT)
BeginDrawCanvasToRenderTarget(TempRT)
K2_DrawMaterial(MergeMID, fullscreen)
EndDrawCanvasToRenderTarget设计说明: 每次命中只需一次 CaptureScene(Overwrite → Primary),Primary 的结果随后通过 Canvas K2_DrawMaterial 以 Additive 混合模式叠加到 Temp。Merge 材质的 Blend Mode = Additive,由 Canvas 系统将绘制结果与 Temp 现有内容相加,实现拖尾累积。Fade tick 每帧对整个 Temp 做衰减。全程无 CPU-GPU 同步点。
步骤 3-6 在同一帧同一调用栈内完成,玩家不会看到材质闪烁。
8.4 TickFadeAndRecycle
TickFadeAndRecycle(DeltaTime):
│
├─ 遍历所有 ActiveBindings:
│ │
│ ├─ 1. 检查组件存活
│ │ if (!TargetComp.IsValid()) → 标记待回收,continue
│ │
│ ├─ 2. 检查 Slot 有效性
│ │ TempRT = RTPool->GetTempRT(SlotIndex)
│ │ if (!TempRT) → 标记待回收,continue
│ │
│ ├─ 3. 跳过未激活衰减的 Binding
│ │ if (!Binding.bFadeActive) → continue
│ │
│ ├─ 4. 计算 per-binding 每帧衰减乘数
│ │ Duration = FadeDurationOverride > 0 ? FadeDurationOverride
│ │ : ComputeEffectiveFadeDuration(LastHitRadius)
│ │ FrameFadeRate = pow(RecycleThreshold, DeltaTime / Duration)
│ │
│ ├─ 5. 执行 fade(Canvas DrawMaterial 方式)
│ │ FadeMID->SetScalarParameterValue("Opacity", FrameFadeRate)
│ │ DrawFadePass(Binding, TempRT)
│ │
│ ├─ 6. 更新 alpha 跟踪
│ │ Binding.CurrentMaxAlpha *= FrameFadeRate
│ │
│ └─ 7. 检查回收条件
│ if (CurrentMaxAlpha < Config->RecycleThreshold)
│ → 标记待回收
│
└─ 批量回收:
for each 待回收 Key:
CleanupBinding(Key) // RestoreMaterial + RTPool->Release + Remove衰减公式说明: FrameFadeRate = pow(RecycleThreshold, DeltaTime / Duration)。经过 Duration 秒后,累积乘积精确等于 RecycleThreshold。帧率无关。FadeMaterial 每帧将 RT_Temp 内容乘以 Opacity(= FrameFadeRate),CPU 端 CurrentMaxAlpha *= FrameFadeRate 精确匹配 GPU 行为。
Radius 缩放: 当 bScaleFadeByRadius=true 时,ComputeEffectiveFadeDuration(Radius) 返回 FadeDuration * pow(Radius / BaseRadius, RadiusFadeExponent)。默认指数 0.5 对应 sqrt(Radius),与 Sculpt 位移公式 UseSpeed = 0.5 * sqrt(Radius) * Strength 同比例,使印记消散和建筑形变恢复同步完成。
手动衰减控制: 当 bAutoFade=false 时,命中后 bFadeActive=false,印记保持满 alpha。需要外部调用 StartFade / StartFadeByActor / StartFadeAll 激活衰减。可传入自定义 Duration 覆盖 Config 值。
Re-hit 行为: 重置 CurrentMaxAlpha=1.0、bFadeActive=Config->bAutoFade、FadeDurationOverride=-1;LastHitRadius 取已有值和新 Radius 的最大值,确保衰减时长按最大 Brush 计算。
8.5 DrawFadePass
使用 Canvas DrawMaterial 方式(与 Demo 一致):
void UPaperRTStampSubsystem::DrawFadePass(
FRTStampBinding& Binding, UTextureRenderTarget2D* TempRT)
{
UWorld* World = GetWorld();
UCanvas* Canvas = nullptr;
FVector2D Size;
FDrawToRenderTargetContext Context;
UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(
World, TempRT, Canvas, Size, Context);
if (Canvas)
{
// 全屏绘制 FadeMaterial,实现自采样衰减
Canvas->K2_DrawMaterial(
Binding.FadeMID,
FVector2D::ZeroVector, // ScreenPosition
Size, // ScreenSize = 全屏
FVector2D::ZeroVector, // CoordinatePosition
FVector2D(1.f, 1.f), // CoordinateSize
0.f, // Rotation
FVector2D(0.5f, 0.5f) // PivotPoint
);
}
UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(World, Context);
}8.6 AllocateWithEviction / FindEvictionCandidate
与之前设计一致:
AllocateWithEviction(int32 PixelSize):
1. RTPool->Allocate(PixelSize)
2. 如返回 INDEX_NONE → FindEvictionCandidate → CleanupBinding → ForceRelease → 重新 Allocate(PixelSize)
FindEvictionCandidate():
Score = CurrentMaxAlpha - (TimeSinceLastHit * Config->AgeWeight)
返回 Score 最低的 Binding 对应的 SlotIndex9. 生命周期与边界条件
9.1 组件销毁(Level Streaming Out)
不使用显式的 OnComponentDestroyed 回调。FRTStampBinding::TargetComp 是 TWeakObjectPtr<UPrimitiveComponent>,在 TickFadeAndRecycle 中每帧检查 IsValid(),组件被销毁后自动标记为待回收并在当帧清理。
9.2 关卡切换
Deinitialize 中调用 ClearAll()。Subsystem 生命周期跟随 World。
9.3 Dedicated Server
ShouldCreateSubsystem 返回 false。
9.4 同一物体多次命中
ActiveBindings 的 Key 查找。已有绑定时:重置 CurrentMaxAlpha=1.0、bFadeActive=Config->bAutoFade、FadeDurationOverride=-1,LastHitRadius 取已有值和新 Radius 的最大值,更新 LastHitTime,再次触发 ExecuteCapture。Temp 使用 Canvas Additive 绘制,新 stamp 叠加到已有拖尾内容上,实现多次命中的拖尾累积效果。
9.5 静默失败
池满且无法驱逐时返回 INDEX_NONE,不崩溃。
10. 性能预算
| 指标 | 预算 |
|---|---|
| RT 池最大容量 | 16(可配置,上限建议 32) |
| 每 Slot RT 数量 | 2(Primary + Temp) |
| 单张 RT 最大分辨率 | 1024x1024 |
| 单张 RT 显存占用(RGBA16f, 1024) | ~8MB |
| 单 Slot 显存占用 | ~16MB(双 RT) |
| 最大同时显存占用(16 Slots × 1024) | ~256MB(最坏情况) |
| 实际预期(多数为 256/512) | ~32-64MB |
| SceneCapture 调用频率 | 仅命中时触发(每次 1 次:SCCM_Overwrite → Primary;Temp 通过 Canvas Additive 绘制填充) |
| 每帧 Tick 开销 | N 次 BeginDrawCanvas + DrawMaterial + EndDrawCanvas |
11. 对外接口
11.1 BlueprintCallable
// 在目标组件表面烘焙一个标记
void OnStampHit(const FRTStampHitParams& Params);
// 强制清理所有标记
void ClearAll();
// 运行时切换配置
void SetConfig(UPaperRTStampConfig* InConfig);
// 启动指定组件的衰减。FadeDuration=0 使用 Config 默认值
void StartFade(UPrimitiveComponent* Comp, float FadeDuration = 0.f);
// 启动指定 Actor 下所有组件的衰减
void StartFadeByActor(AActor* Actor, float FadeDuration = 0.f);
// 启动所有活跃标记的衰减
void StartFadeAll(float FadeDuration = 0.f);11.2 调用示例
// 在技能命中回调中(Gameplay 侧)
UPaperRTStampSubsystem* StampSys = GetWorld()->GetSubsystem<UPaperRTStampSubsystem>();
if (StampSys)
{
FRTStampHitParams Params;
Params.HitLocation = DamageEvent.HitLocation;
Params.HitComponent = DamageEvent.HitComponent;
Params.Radius = 150.f;
Params.Opacity = 1.f;
StampSys->OnStampHit(Params);
}12. 待定 / 后续迭代
- 拖尾支持(已完成): 命中时用 Canvas K2_DrawMaterial(Additive Merge 材质)将 RT_Primary 叠加到 RT_Temp,替换原有的 CopyTexture 覆盖方案。每次命中只需 1 次 CaptureScene;Primary 保留干净快照,Temp 叠加累积,配合 Fade tick 自然实现拖尾效果。
- SceneCapture → CPU UV 查询 + DrawMaterialToRenderTarget: 彻底消除 SceneCapture 开销。当前版本已将开销减半,此迭代可进一步消除。
- Atlas RT: 多 Slot 合并为一张大 Atlas 减少 draw call。
- Ping-Pong 显式分离: 如果自采样在某些场景出问题,将 Temp 拆为 Temp_A / Temp_B 做 ping-pong。
- 分辨率动态降级: 大幅 fade 后降采样节省显存。
- RT 格式优化: 评估是否可以从 RGBA16f 降到 RGBA8 以减半显存占用(取决于 fade 精度需求)。
