Add auto-generated selection outline post-process and gizmo polish

Selection Outline:
- Post-process material auto-generated in C++ (no manual UE editor work)
- 8-neighbor edge detection on CustomStencil buffer
- Dynamic pixel-accurate offsets via SceneTexture InvSize (any resolution/ratio)
- Lerp blending (no overexposure), color AF1500, 1px thickness
- BL_SceneColorAfterDOF placement (before TAA, matches stencil resolution)
- Deferred creation via OnPostEngineInit (no startup crash)
- Material saved as persistent .uasset, loaded at runtime

Gizmo Polish:
- Arrow shaft thickness halved (0.03), cone tips halved (0.075x0.1)
- Gizmo stays at initial position during rotation/scale (no jitter)
- Gizmo follows selection during translation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 10:36:53 +02:00
parent 88544c4c85
commit 03c79a2610
5 changed files with 230 additions and 6 deletions

View File

@@ -343,11 +343,9 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
}
else if (Mode == EPS_Editor_TransformMode::Rotate)
{
// Use combined mouse X+Y screen movement to drive rotation angle
const float RotationSpeed = 1.0f;
const float AngleDelta = (LookInput.X + LookInput.Y) * RotationSpeed;
// Accumulate total angle since drag start
GizmoDragAccumulator += AngleDelta;
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(GizmoDragAccumulator));
@@ -386,9 +384,13 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
}
}
// In translate mode, gizmo follows the selection. In rotate/scale, stay at initial position to avoid jitter.
if (Mode == EPS_Editor_TransformMode::Translate)
{
Gizmo->SetActorLocation(SM->GetSelectionPivot());
}
}
}
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
{

View File

@@ -114,8 +114,8 @@ UStaticMeshComponent* APS_Editor_Gizmo::CreateArrow(const FName& Name, USceneCom
Comp->SetStaticMesh(CylinderMesh.Object);
}
// Shaft: radius ~3 units, length ~60 units
Comp->SetRelativeScale3D(FVector(0.06f, 0.06f, 0.6f));
// Shaft: thin cylinder, length ~60 units
Comp->SetRelativeScale3D(FVector(0.03f, 0.03f, 0.6f));
Comp->SetRelativeRotation(Rotation);
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);
@@ -136,7 +136,7 @@ UStaticMeshComponent* APS_Editor_Gizmo::CreateCone(const FName& Name, USceneComp
Comp->SetStaticMesh(ConeMesh.Object);
}
Comp->SetRelativeScale3D(FVector(0.15f, 0.15f, 0.2f));
Comp->SetRelativeScale3D(FVector(0.075f, 0.075f, 0.1f));
Comp->SetRelativeRotation(Rotation);
const FVector AxisDir = Rotation.RotateVector(FVector::UpVector);

View File

@@ -1,10 +1,232 @@
#include "PS_Editor.h"
#if WITH_EDITOR
#include "Materials/Material.h"
#include "MaterialDomain.h"
#include "Materials/MaterialExpressionSceneTexture.h"
#include "Materials/MaterialExpressionAdd.h"
#include "Materials/MaterialExpressionSubtract.h"
#include "Materials/MaterialExpressionMultiply.h"
#include "Materials/MaterialExpressionAbs.h"
#include "Materials/MaterialExpressionSaturate.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionConstant4Vector.h"
#include "Materials/MaterialExpressionTextureCoordinate.h"
#include "Materials/MaterialExpressionLinearInterpolate.h"
#include "Materials/MaterialExpressionComponentMask.h"
#include "Materials/MaterialExpressionAppendVector.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "UObject/SavePackage.h"
#include "Misc/PackageName.h"
#endif
#define LOCTEXT_NAMESPACE "FPS_EditorModule"
#if WITH_EDITOR
static void CreateOutlineMaterialAsset()
{
const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SelectionOutline");
const FString AssetName = TEXT("M_PS_Editor_SelectionOutline");
if (LoadObject<UMaterial>(nullptr, *(MaterialPath + TEXT(".") + AssetName)))
{
return;
}
UPackage* Package = CreatePackage(*MaterialPath);
UMaterial* Mat = NewObject<UMaterial>(Package, *AssetName, RF_Public | RF_Standalone);
Mat->MaterialDomain = EMaterialDomain::MD_PostProcess;
Mat->BlendableLocation = BL_SceneColorAfterDOF; // Before TAA, matches stencil buffer resolution
auto AddExpr = [&](UMaterialExpression* Expr) { Mat->GetExpressionCollection().AddExpression(Expr); };
const float ThicknessPixels = 1.0f;
// ---- TexCoord ----
UMaterialExpressionTextureCoordinate* TexCoord = NewObject<UMaterialExpressionTextureCoordinate>(Mat);
AddExpr(TexCoord);
TexCoord->MaterialExpressionEditorX = -1600;
TexCoord->MaterialExpressionEditorY = 400;
// ---- Center stencil sample ----
UMaterialExpressionSceneTexture* StencilCenter = NewObject<UMaterialExpressionSceneTexture>(Mat);
StencilCenter->SceneTextureId = PPI_CustomStencil;
AddExpr(StencilCenter);
StencilCenter->Coordinates.Connect(0, TexCoord);
StencilCenter->MaterialExpressionEditorX = -1200;
StencilCenter->MaterialExpressionEditorY = 0;
// ---- Get InvSize from SceneTexture (output index 2) = 1/resolution ----
// This gives dynamic pixel size regardless of resolution/aspect ratio
UMaterialExpressionMultiply* PixelOffset = NewObject<UMaterialExpressionMultiply>(Mat);
AddExpr(PixelOffset);
PixelOffset->A.Connect(2, StencilCenter); // Output 2 = InvSize (float2)
PixelOffset->ConstB = ThicknessPixels;
PixelOffset->MaterialExpressionEditorX = -1000;
PixelOffset->MaterialExpressionEditorY = 200;
// Split into X and Y components
UMaterialExpressionComponentMask* OffsetX = NewObject<UMaterialExpressionComponentMask>(Mat);
AddExpr(OffsetX);
OffsetX->Input.Connect(0, PixelOffset);
OffsetX->R = true; OffsetX->G = false; OffsetX->B = false; OffsetX->A = false;
OffsetX->MaterialExpressionEditorX = -800;
OffsetX->MaterialExpressionEditorY = 100;
UMaterialExpressionComponentMask* OffsetY = NewObject<UMaterialExpressionComponentMask>(Mat);
AddExpr(OffsetY);
OffsetY->Input.Connect(0, PixelOffset);
OffsetY->R = false; OffsetY->G = true; OffsetY->B = false; OffsetY->A = false;
OffsetY->MaterialExpressionEditorX = -800;
OffsetY->MaterialExpressionEditorY = 300;
UMaterialExpressionConstant* ZeroConst = NewObject<UMaterialExpressionConstant>(Mat);
ZeroConst->R = 0.0f;
AddExpr(ZeroConst);
ZeroConst->MaterialExpressionEditorX = -800;
ZeroConst->MaterialExpressionEditorY = 500;
// Negative offsets
UMaterialExpressionMultiply* NegX = NewObject<UMaterialExpressionMultiply>(Mat);
AddExpr(NegX);
NegX->A.Connect(0, OffsetX);
NegX->ConstB = -1.0f;
NegX->MaterialExpressionEditorX = -800;
NegX->MaterialExpressionEditorY = 0;
UMaterialExpressionMultiply* NegY = NewObject<UMaterialExpressionMultiply>(Mat);
AddExpr(NegY);
NegY->A.Connect(0, OffsetY);
NegY->ConstB = -1.0f;
NegY->MaterialExpressionEditorX = -800;
NegY->MaterialExpressionEditorY = 400;
// ---- 8 direction samples ----
struct FDirDef { UMaterialExpression* XE; UMaterialExpression* YE; };
FDirDef Dirs[8] = {
{ OffsetX, ZeroConst }, // right
{ NegX, ZeroConst }, // left
{ ZeroConst, OffsetY }, // down
{ ZeroConst, NegY }, // up
{ OffsetX, OffsetY }, // bottom-right
{ NegX, OffsetY }, // bottom-left
{ OffsetX, NegY }, // top-right
{ NegX, NegY }, // top-left
};
UMaterialExpression* EdgeAccum = nullptr;
for (int32 i = 0; i < 8; ++i)
{
int32 PosY = 100 + i * 160;
// Append(X, Y) -> float2 offset
UMaterialExpressionAppendVector* OffsetVec = NewObject<UMaterialExpressionAppendVector>(Mat);
AddExpr(OffsetVec);
OffsetVec->A.Connect(0, Dirs[i].XE);
OffsetVec->B.Connect(0, Dirs[i].YE);
OffsetVec->MaterialExpressionEditorX = -600;
OffsetVec->MaterialExpressionEditorY = PosY;
// TexCoord + offset
UMaterialExpressionAdd* UVAdd = NewObject<UMaterialExpressionAdd>(Mat);
AddExpr(UVAdd);
UVAdd->A.Connect(0, TexCoord);
UVAdd->B.Connect(0, OffsetVec);
UVAdd->MaterialExpressionEditorX = -400;
UVAdd->MaterialExpressionEditorY = PosY;
// Sample stencil at offset
UMaterialExpressionSceneTexture* StencilN = NewObject<UMaterialExpressionSceneTexture>(Mat);
StencilN->SceneTextureId = PPI_CustomStencil;
AddExpr(StencilN);
StencilN->Coordinates.Connect(0, UVAdd);
StencilN->MaterialExpressionEditorX = -200;
StencilN->MaterialExpressionEditorY = PosY;
// abs(center - neighbor)
UMaterialExpressionSubtract* Sub = NewObject<UMaterialExpressionSubtract>(Mat);
AddExpr(Sub);
Sub->A.Connect(0, StencilCenter);
Sub->B.Connect(0, StencilN);
Sub->MaterialExpressionEditorX = 0;
Sub->MaterialExpressionEditorY = PosY;
UMaterialExpressionAbs* AbsN = NewObject<UMaterialExpressionAbs>(Mat);
AddExpr(AbsN);
AbsN->Input.Connect(0, Sub);
AbsN->MaterialExpressionEditorX = 200;
AbsN->MaterialExpressionEditorY = PosY;
if (!EdgeAccum)
{
EdgeAccum = AbsN;
}
else
{
UMaterialExpressionAdd* AccumAdd = NewObject<UMaterialExpressionAdd>(Mat);
AddExpr(AccumAdd);
AccumAdd->A.Connect(0, EdgeAccum);
AccumAdd->B.Connect(0, AbsN);
AccumAdd->MaterialExpressionEditorX = 400;
AccumAdd->MaterialExpressionEditorY = PosY;
EdgeAccum = AccumAdd;
}
}
// ---- Saturate edge mask ----
UMaterialExpressionSaturate* Saturate = NewObject<UMaterialExpressionSaturate>(Mat);
AddExpr(Saturate);
Saturate->Input.Connect(0, EdgeAccum);
Saturate->MaterialExpressionEditorX = 600;
Saturate->MaterialExpressionEditorY = 500;
// ---- Outline color (orange, float4) ----
UMaterialExpressionConstant4Vector* OutlineColor = NewObject<UMaterialExpressionConstant4Vector>(Mat);
OutlineColor->Constant = FColor(0xAF, 0x15, 0x00, 0xFF).ReinterpretAsLinear();
AddExpr(OutlineColor);
OutlineColor->MaterialExpressionEditorX = 600;
OutlineColor->MaterialExpressionEditorY = 700;
// ---- Scene input ----
UMaterialExpressionSceneTexture* SceneInput = NewObject<UMaterialExpressionSceneTexture>(Mat);
SceneInput->SceneTextureId = PPI_PostProcessInput0;
AddExpr(SceneInput);
SceneInput->MaterialExpressionEditorX = 600;
SceneInput->MaterialExpressionEditorY = 300;
// ---- Lerp(Scene, OutlineColor, EdgeMask) ----
UMaterialExpressionLinearInterpolate* Lerp = NewObject<UMaterialExpressionLinearInterpolate>(Mat);
AddExpr(Lerp);
Lerp->A.Connect(0, SceneInput);
Lerp->B.Connect(0, OutlineColor);
Lerp->Alpha.Connect(0, Saturate);
Lerp->MaterialExpressionEditorX = 800;
Lerp->MaterialExpressionEditorY = 500;
// ---- Output ----
Mat->GetEditorOnlyData()->EmissiveColor.Connect(0, Lerp);
Mat->PreEditChange(nullptr);
Mat->PostEditChange();
FString PackageFilename = FPackageName::LongPackageNameToFilename(MaterialPath, FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
UPackage::SavePackage(Package, Mat, *PackageFilename, SaveArgs);
FAssetRegistryModule::AssetCreated(Mat);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)"));
}
#endif
void FPS_EditorModule::StartupModule()
{
UE_LOG(LogTemp, Log, TEXT("PS_Editor module started."));
#if WITH_EDITOR
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
#endif
}
void FPS_EditorModule::ShutdownModule()