(Feat) Add Cooking and Workbench
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "Icon Studio",
|
||||
"Description": "Studio de rendu d'icones : previsualise un mesh dans une scene isolee, avec eclairage de studio et fond transparent, puis exporte un PNG et une texture UI prete a l'emploi. Traite une selection entiere de DataAssets et remplit leur champ d'icone. Plugin made by Mathew Simon.",
|
||||
"Category": "Editor Tools",
|
||||
"CreatedBy": "Mathew Simon",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "",
|
||||
"MarketplaceURL": "",
|
||||
"SupportURL": "",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"IsExperimentalVersion": false,
|
||||
"Installed": false,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "IconStudio",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "PostEngineInit"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class IconStudio : ModuleRules
|
||||
{
|
||||
public IconStudio(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
// UIconStudioSettings est un UDeveloperSettings : il apparait tout seul
|
||||
// dans Project Settings, sans une ligne d'enregistrement a ecrire.
|
||||
"DeveloperSettings",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"UnrealEd",
|
||||
"EditorSubsystem",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
// ReadPixels et la creation de render targets passent par le RHI.
|
||||
"RenderCore",
|
||||
"RHI",
|
||||
// FImage / FImageUtils::SaveImageByExtension pour l'ecriture du PNG.
|
||||
"ImageCore",
|
||||
"AssetRegistry",
|
||||
// Lecture de la selection du Content Browser pour le mode batch.
|
||||
"ContentBrowser",
|
||||
// La fenetre est ecrite en Slate : les deux panneaux de reglages sont
|
||||
// des IDetailsView du moteur, et l'onglet se range dans Window > Tools.
|
||||
"PropertyEditor",
|
||||
"WorkspaceMenuStructure",
|
||||
// Le bouton dans la barre d'outils et l'entree du menu Tools.
|
||||
"ToolMenus",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#include "SIconStudioTab.h"
|
||||
|
||||
#include "Framework/Docking/TabManager.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Styling/AppStyle.h"
|
||||
#include "ToolMenus.h"
|
||||
#include "Widgets/Docking/SDockTab.h"
|
||||
#include "WorkspaceMenuStructure.h"
|
||||
#include "WorkspaceMenuStructureModule.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "IconStudio"
|
||||
|
||||
namespace IconStudioModuleLocal
|
||||
{
|
||||
/** Identifiant de l'onglet. Sert aussi de cle a la disposition sauvegardee. */
|
||||
static const FName StudioTabName(TEXT("IconStudio"));
|
||||
|
||||
TSharedRef<SDockTab> SpawnStudioTab(const FSpawnTabArgs& Args)
|
||||
{
|
||||
return SNew(SDockTab)
|
||||
.TabRole(ETabRole::NomadTab)
|
||||
[
|
||||
SNew(SIconStudioTab)
|
||||
];
|
||||
}
|
||||
|
||||
void OpenStudio()
|
||||
{
|
||||
// TryInvokeTab remet au premier plan l'onglet deja ouvert au lieu d'en
|
||||
// creer un second, et le rouvre a l'endroit ou l'utilisateur l'avait
|
||||
// docke la derniere fois.
|
||||
FGlobalTabmanager::Get()->TryInvokeTab(StudioTabName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Le module pose le point d'entree de l'outil : l'onglet lui-meme, un bouton
|
||||
* dans la barre d'outils et une entree dans le menu Tools.
|
||||
*
|
||||
* Le reste s'enregistre tout seul -- le sous-systeme est instancie par le
|
||||
* moteur, les reglages apparaissent dans Project Settings grace a
|
||||
* UDeveloperSettings.
|
||||
*/
|
||||
class FIconStudioModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override
|
||||
{
|
||||
using namespace IconStudioModuleLocal;
|
||||
|
||||
FGlobalTabmanager::Get()
|
||||
->RegisterNomadTabSpawner(StudioTabName, FOnSpawnTab::CreateStatic(&SpawnStudioTab))
|
||||
.SetDisplayName(LOCTEXT("IconStudioLabel", "Icon Studio"))
|
||||
.SetTooltipText(LOCTEXT("IconStudioTooltip", "Render item icons from a preset: preview, transparent background, batch over DataAssets."))
|
||||
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("ClassIcon.Texture2D")))
|
||||
// Le range dans Window > Tools, avec les autres outils de l'editeur.
|
||||
.SetGroup(WorkspaceMenu::GetMenuStructure().GetToolsCategory());
|
||||
|
||||
// Le systeme de menus n'est pas forcement pret quand un module de plugin
|
||||
// demarre : ce rappel est la seule facon fiable d'etendre un menu, qu'on
|
||||
// soit charge avant ou apres l'editeur de niveau.
|
||||
UToolMenus::RegisterStartupCallback(
|
||||
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FIconStudioModule::RegisterMenus));
|
||||
}
|
||||
|
||||
virtual void ShutdownModule() override
|
||||
{
|
||||
UToolMenus::UnRegisterStartupCallback(this);
|
||||
UToolMenus::UnregisterOwner(this);
|
||||
|
||||
if (FSlateApplication::IsInitialized())
|
||||
{
|
||||
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(IconStudioModuleLocal::StudioTabName);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void RegisterMenus()
|
||||
{
|
||||
// Tout ce qui est enregistre sous ce proprietaire part d'un bloc au
|
||||
// dechargement du module : sans lui, un rechargement a chaud laisserait
|
||||
// un bouton mort dans la barre d'outils.
|
||||
FToolMenuOwnerScoped OwnerScoped(this);
|
||||
|
||||
const FSlateIcon Icon(FAppStyle::GetAppStyleSetName(), TEXT("ClassIcon.Texture2D"));
|
||||
const FText Label = LOCTEXT("IconStudioLabel", "Icon Studio");
|
||||
const FText Tooltip = LOCTEXT("IconStudioTooltip", "Render item icons from a preset: preview, transparent background, batch over DataAssets.");
|
||||
const FUIAction Action(FExecuteAction::CreateStatic(&IconStudioModuleLocal::OpenStudio));
|
||||
|
||||
if (UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu(TEXT("LevelEditor.MainMenu.Tools")))
|
||||
{
|
||||
FToolMenuSection& Section = ToolsMenu->FindOrAddSection(
|
||||
TEXT("IconStudio"), LOCTEXT("IconStudioSection", "Icon Studio"));
|
||||
|
||||
Section.AddMenuEntry(TEXT("OpenIconStudio"), Label, Tooltip, Icon, Action);
|
||||
}
|
||||
|
||||
// Section "User" de la barre d'outils : l'emplacement prevu par le
|
||||
// moteur pour les outils maison, a droite des boutons natifs.
|
||||
if (UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu(TEXT("LevelEditor.LevelEditorToolBar.User")))
|
||||
{
|
||||
FToolMenuSection& Section = Toolbar->FindOrAddSection(TEXT("IconStudio"));
|
||||
|
||||
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
||||
TEXT("OpenIconStudio"), Action, Label, Tooltip, Icon));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FIconStudioModule, IconStudio);
|
||||
@@ -0,0 +1,23 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#include "IconStudioPreset.h"
|
||||
|
||||
UIconStudioPreset::UIconStudioPreset()
|
||||
{
|
||||
// Eclairage de studio classique en trois points, angles donnes par rapport
|
||||
// a la camera : principale en haut a gauche, deboucheur doux a droite,
|
||||
// contre-jour derriere. Ces valeurs marchent telles quelles sur presque tout
|
||||
// objet ; elles sont la pour avoir quelque chose de correct au premier rendu,
|
||||
// pas pour rester figees.
|
||||
KeyLight = FIconStudioLight(-40.f, -35.f, 5.f, /*bShadows=*/true);
|
||||
FillLight = FIconStudioLight(65.f, -5.f, 1.5f, /*bShadows=*/false);
|
||||
RimLight = FIconStudioLight(155.f, 25.f, 4.f, /*bShadows=*/false);
|
||||
RimLight.Color = FLinearColor(0.85f, 0.92f, 1.f);
|
||||
|
||||
// Cubemap du moteur : sans reflet ambiant, tout ce qui est metallique sort
|
||||
// noir. Reference en soft pour ne pas charger la texture tant qu'aucun rendu
|
||||
// n'a ete demande.
|
||||
SkyCubemap = TSoftObjectPtr<UTextureCube>(FSoftObjectPath(TEXT("/Engine/EngineMaterials/DefaultCubemap.DefaultCubemap")));
|
||||
|
||||
OutputDirectory.Path = TEXT("/Game/UI/Icons");
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#include "IconStudioRenderer.h"
|
||||
|
||||
#include "IconStudioPreset.h"
|
||||
|
||||
#include "Components/DirectionalLightComponent.h"
|
||||
#include "Components/SceneCaptureComponent2D.h"
|
||||
#include "Components/SkyLightComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "ContentStreaming.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Engine/TextureCube.h"
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "PreviewScene.h"
|
||||
#include "RenderingThread.h"
|
||||
#include "TextureResource.h"
|
||||
|
||||
namespace IconStudioRendererLocal
|
||||
{
|
||||
/** Un pixel de la scene de rendu, avant reduction. */
|
||||
struct FSample
|
||||
{
|
||||
FLinearColor Color = FLinearColor::Black;
|
||||
bool bCovered = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose la rotation d'une lumiere dans le repere de la camera.
|
||||
*
|
||||
* L'ordre compte : c'est la rotation camera qui porte l'offset, et non
|
||||
* l'inverse. Ecrit dans l'autre sens, incliner la camera vers le bas ferait
|
||||
* pivoter les lumieres autour d'un axe qui n'a rien a voir.
|
||||
*/
|
||||
FRotator ComposeLightRotation(const FRotator& CameraRotation, const FIconStudioLight& Light)
|
||||
{
|
||||
const FQuat Composed = CameraRotation.Quaternion() * FRotator(Light.Pitch, Light.Yaw, 0.f).Quaternion();
|
||||
return Composed.Rotator();
|
||||
}
|
||||
}
|
||||
|
||||
FIconStudioRenderer::FIconStudioRenderer()
|
||||
{
|
||||
}
|
||||
|
||||
FIconStudioRenderer::~FIconStudioRenderer()
|
||||
{
|
||||
// La scene detient ses composants : les detruire soi-meme laisserait des
|
||||
// proxies de rendu enregistres dans un monde qui n'existe plus.
|
||||
Scene.Reset();
|
||||
}
|
||||
|
||||
void FIconStudioRenderer::AddReferencedObjects(FReferenceCollector& Collector)
|
||||
{
|
||||
Collector.AddReferencedObject(MeshComponent);
|
||||
Collector.AddReferencedObject(CaptureComponent);
|
||||
Collector.AddReferencedObject(KeyLightComponent);
|
||||
Collector.AddReferencedObject(FillLightComponent);
|
||||
Collector.AddReferencedObject(RimLightComponent);
|
||||
Collector.AddReferencedObject(SkyLightComponent);
|
||||
Collector.AddReferencedObject(ColorTarget);
|
||||
Collector.AddReferencedObject(DepthTarget);
|
||||
Collector.AddReferencedObject(PreviewTexture);
|
||||
Collector.AddReferencedObject(AppliedSkyCubemap);
|
||||
}
|
||||
|
||||
void FIconStudioRenderer::EnsureScene()
|
||||
{
|
||||
if (Scene.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Pas d'eclairage par defaut : le studio pose lui-meme ses trois lumieres,
|
||||
// et une directionnelle surnumeraire fausserait tous les presets. Pas de
|
||||
// scene physique non plus, rien ne tombe ici.
|
||||
FPreviewScene::ConstructionValues CVS;
|
||||
CVS.SetCreateDefaultLighting(false)
|
||||
.SetCreatePhysicsScene(false)
|
||||
.ShouldSimulatePhysics(false)
|
||||
.SetTransactional(false)
|
||||
.AllowAudioPlayback(false)
|
||||
.SetForceMipsResident(true);
|
||||
|
||||
Scene = MakeUnique<FPreviewScene>(CVS);
|
||||
|
||||
MeshComponent = NewObject<UStaticMeshComponent>(GetTransientPackage(), NAME_None, RF_Transient);
|
||||
MeshComponent->SetMobility(EComponentMobility::Movable);
|
||||
Scene->AddComponent(MeshComponent, FTransform::Identity);
|
||||
|
||||
auto MakeLight = [this](TObjectPtr<UDirectionalLightComponent>& Out)
|
||||
{
|
||||
Out = NewObject<UDirectionalLightComponent>(GetTransientPackage(), NAME_None, RF_Transient);
|
||||
Out->SetMobility(EComponentMobility::Movable);
|
||||
// Une directionnelle en mouvement declenche des cascades d'ombres
|
||||
// dimensionnees pour un niveau entier ; ici l'objet tient dans quelques
|
||||
// dizaines de centimetres, d'ou une seule cascade tres courte.
|
||||
Out->DynamicShadowDistanceMovableLight = 2000.f;
|
||||
Out->DynamicShadowCascades = 1;
|
||||
Scene->AddComponent(Out, FTransform::Identity);
|
||||
};
|
||||
|
||||
MakeLight(KeyLightComponent);
|
||||
MakeLight(FillLightComponent);
|
||||
MakeLight(RimLightComponent);
|
||||
|
||||
SkyLightComponent = NewObject<USkyLightComponent>(GetTransientPackage(), NAME_None, RF_Transient);
|
||||
SkyLightComponent->SetMobility(EComponentMobility::Movable);
|
||||
SkyLightComponent->SourceType = SLS_SpecifiedCubemap;
|
||||
SkyLightComponent->bLowerHemisphereIsBlack = false;
|
||||
SkyLightComponent->CastShadows = false;
|
||||
SkyLightComponent->bRealTimeCapture = false;
|
||||
Scene->AddComponent(SkyLightComponent, FTransform::Identity);
|
||||
|
||||
CaptureComponent = NewObject<USceneCaptureComponent2D>(GetTransientPackage(), NAME_None, RF_Transient);
|
||||
CaptureComponent->SetMobility(EComponentMobility::Movable);
|
||||
// La capture est declenchee a la main : sans monde tique, "chaque image" ne
|
||||
// veut rien dire, et on ne veut surtout pas d'un rendu par frame d'editeur.
|
||||
CaptureComponent->bCaptureEveryFrame = false;
|
||||
CaptureComponent->bCaptureOnMovement = false;
|
||||
CaptureComponent->bAlwaysPersistRenderingState = false;
|
||||
CaptureComponent->PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_RenderScenePrimitives;
|
||||
CaptureComponent->CompositeMode = SCCM_Overwrite;
|
||||
Scene->AddComponent(CaptureComponent, FTransform::Identity);
|
||||
}
|
||||
|
||||
void FIconStudioRenderer::ApplyLighting(const UIconStudioPreset* Preset, const FRotator& CameraRotation)
|
||||
{
|
||||
using namespace IconStudioRendererLocal;
|
||||
|
||||
auto Setup = [&CameraRotation](UDirectionalLightComponent* Component, const FIconStudioLight& Light)
|
||||
{
|
||||
Component->SetVisibility(Light.bEnabled);
|
||||
if (!Light.bEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Component->SetWorldRotation(ComposeLightRotation(CameraRotation, Light));
|
||||
Component->SetIntensity(Light.Intensity);
|
||||
Component->SetLightColor(Light.Color);
|
||||
Component->SetCastShadows(Light.bCastShadows);
|
||||
};
|
||||
|
||||
Setup(KeyLightComponent, Preset->KeyLight);
|
||||
Setup(FillLightComponent, Preset->FillLight);
|
||||
Setup(RimLightComponent, Preset->RimLight);
|
||||
|
||||
// Recapturer le ciel est l'operation la plus chere de toute la mise en place :
|
||||
// on ne la refait que si la cubemap ou son intensite ont reellement change.
|
||||
UTextureCube* const Cubemap = Preset->SkyCubemap.LoadSynchronous();
|
||||
const bool bSkyDirty = (Cubemap != AppliedSkyCubemap) || !FMath::IsNearlyEqual(AppliedSkyIntensity, Preset->SkyIntensity);
|
||||
|
||||
SkyLightComponent->SetVisibility(Cubemap != nullptr && Preset->SkyIntensity > 0.f);
|
||||
SkyLightComponent->SetLightColor(Preset->SkyColor);
|
||||
|
||||
if (bSkyDirty)
|
||||
{
|
||||
SkyLightComponent->Cubemap = Cubemap;
|
||||
SkyLightComponent->SetIntensity(Preset->SkyIntensity);
|
||||
SkyLightComponent->RecaptureSky();
|
||||
Scene->UpdateCaptureContents();
|
||||
|
||||
AppliedSkyCubemap = Cubemap;
|
||||
AppliedSkyIntensity = Preset->SkyIntensity;
|
||||
}
|
||||
}
|
||||
|
||||
void FIconStudioRenderer::ApplyCamera(const UIconStudioPreset* Preset, const FRotator& CameraRotation, const FBoxSphereBounds& Bounds)
|
||||
{
|
||||
const FRotationMatrix CameraBasis(CameraRotation);
|
||||
const FVector Forward = CameraBasis.GetUnitAxis(EAxis::X);
|
||||
const FVector Right = CameraBasis.GetUnitAxis(EAxis::Y);
|
||||
const FVector Up = CameraBasis.GetUnitAxis(EAxis::Z);
|
||||
|
||||
// Etendue reelle de la boite englobante une fois projetee sur les axes de la
|
||||
// camera. Se contenter du rayon de la sphere englobante, comme on le voit
|
||||
// souvent, cadre bien trop large des qu'un objet est allonge : une planche
|
||||
// occuperait le tiers de son icone.
|
||||
const FVector Extent = Bounds.BoxExtent;
|
||||
auto ProjectExtent = [&Extent](const FVector& Axis)
|
||||
{
|
||||
return FMath::Abs(Axis.X * Extent.X) + FMath::Abs(Axis.Y * Extent.Y) + FMath::Abs(Axis.Z * Extent.Z);
|
||||
};
|
||||
|
||||
const float HalfWidth = ProjectExtent(Right);
|
||||
const float HalfHeight = ProjectExtent(Up);
|
||||
const float HalfDepth = ProjectExtent(Forward);
|
||||
|
||||
const float Padding = 1.f + FMath::Max(0.f, Preset->FramingPadding);
|
||||
const float FrameHalfSize = Preset->bUseFixedScale
|
||||
? FMath::Max(1.f, Preset->FixedFrameWidth) * 0.5f
|
||||
: FMath::Max(HalfWidth, HalfHeight) * Padding;
|
||||
|
||||
// Recul : de quoi degager la profondeur de l'objet, plus une marge pour que
|
||||
// le plan proche ne vienne jamais trancher dedans.
|
||||
const float Distance = Preset->bOrthographic
|
||||
? HalfDepth + FrameHalfSize * 2.f + 100.f
|
||||
: FrameHalfSize / FMath::Tan(FMath::DegreesToRadians(Preset->FieldOfView * 0.5f)) + HalfDepth;
|
||||
|
||||
const FVector Offset = Right * (Preset->FramingOffset.X * FrameHalfSize * 2.f)
|
||||
+ Up * (Preset->FramingOffset.Y * FrameHalfSize * 2.f);
|
||||
|
||||
CaptureComponent->SetWorldLocationAndRotation(Bounds.Origin - Forward * Distance - Offset, CameraRotation);
|
||||
CaptureComponent->ProjectionType = Preset->bOrthographic ? ECameraProjectionMode::Orthographic : ECameraProjectionMode::Perspective;
|
||||
CaptureComponent->OrthoWidth = FrameHalfSize * 2.f;
|
||||
CaptureComponent->FOVAngle = Preset->FieldOfView;
|
||||
CaptureComponent->bOverride_CustomNearClippingPlane = true;
|
||||
CaptureComponent->CustomNearClippingPlane = 1.f;
|
||||
|
||||
// Seuil de vide : tout ce qui est au-dela ne peut plus etre l'objet. Genereux
|
||||
// a dessein, la valeur du fond etant de plusieurs ordres de grandeur au-dessus.
|
||||
DepthThreshold = Distance + HalfDepth * 2.f + FrameHalfSize * 4.f + 1000.f;
|
||||
}
|
||||
|
||||
void FIconStudioRenderer::ApplyPostProcess(const UIconStudioPreset* Preset)
|
||||
{
|
||||
FPostProcessSettings& PP = CaptureComponent->PostProcessSettings;
|
||||
PP = FPostProcessSettings();
|
||||
CaptureComponent->PostProcessBlendWeight = 1.f;
|
||||
|
||||
// Exposition figee. L'exposition automatique est le piege numero un de ce
|
||||
// genre d'outil : elle compense la luminosite moyenne de l'image, donc un
|
||||
// objet sombre serait eclairci et un objet clair assombri, et deux icones
|
||||
// cote a cote n'auraient plus la meme lumiere alors que le preset, lui,
|
||||
// n'a pas bouge.
|
||||
PP.bOverride_AutoExposureMethod = true;
|
||||
PP.AutoExposureMethod = AEM_Manual;
|
||||
PP.bOverride_AutoExposureBias = true;
|
||||
PP.AutoExposureBias = Preset->ExposureBias;
|
||||
PP.bOverride_AutoExposureMinBrightness = true;
|
||||
PP.AutoExposureMinBrightness = 1.f;
|
||||
PP.bOverride_AutoExposureMaxBrightness = true;
|
||||
PP.AutoExposureMaxBrightness = 1.f;
|
||||
|
||||
PP.bOverride_BloomIntensity = true;
|
||||
PP.BloomIntensity = Preset->BloomIntensity;
|
||||
|
||||
// Tout ce qui simule un objectif photo n'a rien a faire sur une icone : le
|
||||
// vignettage assombrit les coins, l'aberration colore les bords, le flou de
|
||||
// mouvement bave sur un objet qui ne bouge pas.
|
||||
PP.bOverride_VignetteIntensity = true;
|
||||
PP.VignetteIntensity = 0.f;
|
||||
PP.bOverride_SceneFringeIntensity = true;
|
||||
PP.SceneFringeIntensity = 0.f;
|
||||
PP.bOverride_MotionBlurAmount = true;
|
||||
PP.MotionBlurAmount = 0.f;
|
||||
PP.bOverride_DepthOfFieldFocalDistance = true;
|
||||
PP.DepthOfFieldFocalDistance = 0.f;
|
||||
PP.bOverride_AmbientOcclusionIntensity = true;
|
||||
PP.AmbientOcclusionIntensity = Preset->bAmbientOcclusion ? 0.5f : 0.f;
|
||||
|
||||
FEngineShowFlags& Flags = CaptureComponent->ShowFlags;
|
||||
|
||||
// L'antialiasing du moteur doit rester eteint : le masque de transparence
|
||||
// vient de la profondeur, qui n'est jamais lissee, alors que la couleur le
|
||||
// serait -- les deux ne coincideraient plus sur les bords. La suranalyse
|
||||
// fait ce travail bien mieux, et de facon identique sur les deux passes.
|
||||
Flags.SetAntiAliasing(false);
|
||||
Flags.SetTemporalAA(false);
|
||||
Flags.SetMotionBlur(false);
|
||||
|
||||
// Rien de tout cela n'existe dans une scene de studio, mais un show flag
|
||||
// laisse actif suffit a colorer le fond ou a voiler l'objet.
|
||||
Flags.SetAtmosphere(false);
|
||||
Flags.SetFog(false);
|
||||
Flags.SetVolumetricFog(false);
|
||||
Flags.SetGrid(false);
|
||||
Flags.SetSelectionOutline(false);
|
||||
Flags.SetLensFlares(false);
|
||||
Flags.SetScreenSpaceReflections(false);
|
||||
Flags.SetToneCurve(true);
|
||||
Flags.SetEyeAdaptation(true);
|
||||
}
|
||||
|
||||
UTextureRenderTarget2D* FIconStudioRenderer::EnsureTarget(TObjectPtr<UTextureRenderTarget2D>& Target, int32 Size, bool bFloatingPoint)
|
||||
{
|
||||
if (Target && Target->SizeX == Size && Target->SizeY == Size)
|
||||
{
|
||||
return Target;
|
||||
}
|
||||
|
||||
Target = NewObject<UTextureRenderTarget2D>(GetTransientPackage(), NAME_None, RF_Transient);
|
||||
// RTF_RGBA8 et non sa variante sRGB : la capture LDR ecrit deja des valeurs
|
||||
// encodees en gamma, un second encodage par le materiel les delaverait.
|
||||
Target->RenderTargetFormat = bFloatingPoint ? RTF_RGBA16f : RTF_RGBA8;
|
||||
Target->ClearColor = FLinearColor::Black;
|
||||
Target->bAutoGenerateMips = false;
|
||||
Target->InitAutoFormat(Size, Size);
|
||||
Target->UpdateResourceImmediate(true);
|
||||
|
||||
return Target;
|
||||
}
|
||||
|
||||
bool FIconStudioRenderer::CaptureTo(UTextureRenderTarget2D* Target, uint8 CaptureSource)
|
||||
{
|
||||
if (!Target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CaptureComponent->TextureTarget = Target;
|
||||
CaptureComponent->CaptureSource = static_cast<ESceneCaptureSource>(CaptureSource);
|
||||
|
||||
// Le monde de preview n'est pas tique : sans cet appel, les changements de
|
||||
// transformation et de mesh faits juste au-dessus ne sont pas encore parvenus
|
||||
// au thread de rendu, et la capture montrerait l'objet precedent.
|
||||
if (UWorld* World = Scene->GetWorld())
|
||||
{
|
||||
World->SendAllEndOfFrameUpdates();
|
||||
}
|
||||
|
||||
CaptureComponent->CaptureScene();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FIconStudioRenderer::Render(UStaticMesh* Mesh, UMaterialInterface* MaterialOverride, const UIconStudioPreset* Preset, EIconStudioPass Pass, FIconStudioImage& OutImage)
|
||||
{
|
||||
using namespace IconStudioRendererLocal;
|
||||
|
||||
if (!Mesh || !Preset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureScene();
|
||||
|
||||
MeshComponent->EmptyOverrideMaterials();
|
||||
MeshComponent->SetStaticMesh(Mesh);
|
||||
if (MaterialOverride)
|
||||
{
|
||||
MeshComponent->SetMaterial(0, MaterialOverride);
|
||||
}
|
||||
MeshComponent->SetWorldTransform(FTransform::Identity);
|
||||
|
||||
// Sans ca, une texture encore en cours de streaming rend l'icone floue --
|
||||
// et comme le monde n'est pas tique, le streaming ne rattraperait jamais son
|
||||
// retard tout seul. StreamAllResources bloque le temps qu'il faut ; c'est
|
||||
// acceptable pour un outil d'editeur, et c'est la seule facon d'avoir un
|
||||
// resultat reproductible entre le premier rendu et les suivants.
|
||||
MeshComponent->SetTextureForceResidentFlag(true);
|
||||
MeshComponent->PrestreamTextures(60.f, /*bPrioritizeCharacterTextures=*/false);
|
||||
IStreamingManager::Get().StreamAllResources(0.f);
|
||||
|
||||
const FRotator CameraRotation(Preset->CameraPitch, Preset->CameraYaw, 0.f);
|
||||
const FBoxSphereBounds Bounds = Mesh->GetBounds();
|
||||
|
||||
ApplyLighting(Preset, CameraRotation);
|
||||
ApplyCamera(Preset, CameraRotation, Bounds);
|
||||
ApplyPostProcess(Preset);
|
||||
|
||||
const int32 FinalSize = FMath::Max(16, Preset->Resolution);
|
||||
const int32 Supersample = FMath::Clamp(Preset->SupersampleFactor, 1, 8);
|
||||
const int32 RenderSize = FinalSize * Supersample;
|
||||
|
||||
UTextureRenderTarget2D* const Color = EnsureTarget(ColorTarget, RenderSize, /*bFloatingPoint=*/false);
|
||||
UTextureRenderTarget2D* const Depth = EnsureTarget(DepthTarget, RenderSize, /*bFloatingPoint=*/true);
|
||||
|
||||
// Passe de masque d'abord : c'est elle qui decide de ce qui existe, et la
|
||||
// passe de couleur ne fait que l'habiller.
|
||||
CaptureTo(Depth, SCS_SceneDepth);
|
||||
TArray<FFloat16Color> DepthPixels;
|
||||
if (!Depth->GameThread_GetRenderTargetResource()->ReadFloat16Pixels(DepthPixels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8 ColorSource = SCS_FinalColorLDR;
|
||||
bool bColorIsSRGB = true;
|
||||
switch (Pass)
|
||||
{
|
||||
case EIconStudioPass::BaseColor:
|
||||
ColorSource = SCS_BaseColor;
|
||||
// Le G-Buffer stocke l'albedo tel quel : le lire comme du sRGB
|
||||
// l'eclaircirait deux fois.
|
||||
bColorIsSRGB = false;
|
||||
break;
|
||||
case EIconStudioPass::Normal:
|
||||
ColorSource = SCS_Normal;
|
||||
bColorIsSRGB = false;
|
||||
break;
|
||||
case EIconStudioPass::Depth:
|
||||
ColorSource = SCS_DeviceDepth;
|
||||
bColorIsSRGB = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
CaptureTo(Color, ColorSource);
|
||||
TArray<FColor> ColorPixels;
|
||||
FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
|
||||
ReadFlags.SetLinearToGamma(false);
|
||||
if (!Color->GameThread_GetRenderTargetResource()->ReadPixels(ColorPixels, ReadFlags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 Expected = RenderSize * RenderSize;
|
||||
if (ColorPixels.Num() < Expected || DepthPixels.Num() < Expected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Reduction : un pixel final = SupersampleFactor^2 pixels de rendu.
|
||||
// La couleur est moyennee sur les seuls sous-pixels couverts, ce qui donne
|
||||
// directement une couleur non premultipliee -- pas de division par l'alpha,
|
||||
// donc pas de bruit sur les bords tres transparents.
|
||||
// ------------------------------------------------------------------
|
||||
TArray<FLinearColor> Accum;
|
||||
TArray<float> Coverage;
|
||||
Accum.SetNumZeroed(FinalSize * FinalSize);
|
||||
Coverage.SetNumZeroed(FinalSize * FinalSize);
|
||||
|
||||
const float SubPixelCount = static_cast<float>(Supersample * Supersample);
|
||||
|
||||
for (int32 Y = 0; Y < FinalSize; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < FinalSize; ++X)
|
||||
{
|
||||
FLinearColor Sum = FLinearColor(0.f, 0.f, 0.f, 0.f);
|
||||
int32 Covered = 0;
|
||||
|
||||
for (int32 SubY = 0; SubY < Supersample; ++SubY)
|
||||
{
|
||||
const int32 SourceRow = (Y * Supersample + SubY) * RenderSize;
|
||||
for (int32 SubX = 0; SubX < Supersample; ++SubX)
|
||||
{
|
||||
const int32 SourceIndex = SourceRow + X * Supersample + SubX;
|
||||
const float SampleDepth = DepthPixels[SourceIndex].R.GetFloat();
|
||||
if (!(SampleDepth > 0.f && SampleDepth < DepthThreshold))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FColor& Raw = ColorPixels[SourceIndex];
|
||||
// Moyenner des valeurs encodees en gamma assombrit les
|
||||
// degrades ; on repasse en lineaire le temps de l'operation.
|
||||
// L'alpha du render target n'a lui aucun sens ici -- il est
|
||||
// force a 1 pour ne compter que les sous-pixels, jamais leur
|
||||
// canal alpha parasite.
|
||||
FLinearColor Sample = bColorIsSRGB
|
||||
? FLinearColor(Raw)
|
||||
: FLinearColor(Raw.R / 255.f, Raw.G / 255.f, Raw.B / 255.f, 1.f);
|
||||
Sample.A = 1.f;
|
||||
Sum += Sample;
|
||||
++Covered;
|
||||
}
|
||||
}
|
||||
|
||||
const int32 Index = Y * FinalSize + X;
|
||||
Coverage[Index] = Covered / SubPixelCount;
|
||||
Accum[Index] = Covered > 0 ? Sum / static_cast<float>(Covered) : FLinearColor(0.f, 0.f, 0.f, 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Etalement des bords : un pixel entierement transparent n'a aucune couleur
|
||||
// definie. Laisse a noir, il se melange aux pixels voisins des que l'icone
|
||||
// est filtree ou reduite, et l'objet se retrouve cerne d'un liseré sombre.
|
||||
// On propage donc la couleur du bord vers l'exterieur, sans toucher a l'alpha.
|
||||
// ------------------------------------------------------------------
|
||||
for (int32 PassIndex = 0; PassIndex < FMath::Clamp(Preset->EdgeBleedPasses, 0, 8); ++PassIndex)
|
||||
{
|
||||
TArray<FLinearColor> Bled = Accum;
|
||||
bool bAnyFilled = false;
|
||||
|
||||
for (int32 Y = 0; Y < FinalSize; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < FinalSize; ++X)
|
||||
{
|
||||
const int32 Index = Y * FinalSize + X;
|
||||
if (Coverage[Index] > 0.f || Accum[Index].A > 0.f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FLinearColor Sum(0.f, 0.f, 0.f, 0.f);
|
||||
int32 Count = 0;
|
||||
for (int32 OffsetY = -1; OffsetY <= 1; ++OffsetY)
|
||||
{
|
||||
for (int32 OffsetX = -1; OffsetX <= 1; ++OffsetX)
|
||||
{
|
||||
const int32 NeighbourX = X + OffsetX;
|
||||
const int32 NeighbourY = Y + OffsetY;
|
||||
if (NeighbourX < 0 || NeighbourY < 0 || NeighbourX >= FinalSize || NeighbourY >= FinalSize)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32 NeighbourIndex = NeighbourY * FinalSize + NeighbourX;
|
||||
if (Coverage[NeighbourIndex] > 0.f || Accum[NeighbourIndex].A > 0.f)
|
||||
{
|
||||
Sum += Accum[NeighbourIndex];
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Count > 0)
|
||||
{
|
||||
Bled[Index] = Sum / static_cast<float>(Count);
|
||||
// L'alpha sert ici de marqueur "ce pixel a recu une couleur",
|
||||
// pour que la passe suivante puisse s'appuyer dessus. Il est
|
||||
// remis a la couverture reelle au moment de l'encodage.
|
||||
Bled[Index].A = 1.f;
|
||||
bAnyFilled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bAnyFilled)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Accum = MoveTemp(Bled);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Encodage final
|
||||
// ------------------------------------------------------------------
|
||||
const bool bSolid = Preset->BackgroundMode == EIconStudioBackground::SolidColor;
|
||||
const FLinearColor Background = Preset->BackgroundColor;
|
||||
|
||||
OutImage.Size = FinalSize;
|
||||
OutImage.Pixels.SetNumUninitialized(FinalSize * FinalSize);
|
||||
|
||||
for (int32 Index = 0; Index < FinalSize * FinalSize; ++Index)
|
||||
{
|
||||
const float Alpha = Coverage[Index];
|
||||
FLinearColor Linear = Accum[Index];
|
||||
Linear.A = 1.f;
|
||||
|
||||
if (bSolid)
|
||||
{
|
||||
// Le fond opaque est compose ici, et non rendu : changer sa couleur
|
||||
// ne coute alors pas un seul rendu, et les deux modes partagent
|
||||
// exactement le meme masque.
|
||||
Linear = Linear * Alpha + Background * (1.f - Alpha);
|
||||
Linear.A = 1.f;
|
||||
}
|
||||
|
||||
FColor Encoded = bColorIsSRGB ? Linear.ToFColor(/*bSRGB=*/true) : Linear.ToFColor(/*bSRGB=*/false);
|
||||
Encoded.A = bSolid ? 255 : static_cast<uint8>(FMath::RoundToInt(FMath::Clamp(Alpha, 0.f, 1.f) * 255.f));
|
||||
OutImage.Pixels[Index] = Encoded;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
UTexture2D* FIconStudioRenderer::UpdatePreviewTexture(const FIconStudioImage& Image)
|
||||
{
|
||||
if (!Image.IsValid())
|
||||
{
|
||||
return PreviewTexture;
|
||||
}
|
||||
|
||||
if (!PreviewTexture || PreviewTexture->GetSizeX() != Image.Size || PreviewTexture->GetSizeY() != Image.Size)
|
||||
{
|
||||
// Pas d'AddToRoot : le renderer est un FGCObject, il declare deja cette
|
||||
// texture dans AddReferencedObjects. L'enraciner en plus la rendrait
|
||||
// immortelle, y compris apres la fermeture de l'outil.
|
||||
PreviewTexture = UTexture2D::CreateTransient(Image.Size, Image.Size, PF_B8G8R8A8);
|
||||
PreviewTexture->SRGB = true;
|
||||
PreviewTexture->Filter = TF_Bilinear;
|
||||
}
|
||||
|
||||
FTexture2DMipMap& Mip = PreviewTexture->GetPlatformData()->Mips[0];
|
||||
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
|
||||
FMemory::Memcpy(Data, Image.Pixels.GetData(), Image.Pixels.Num() * sizeof(FColor));
|
||||
Mip.BulkData.Unlock();
|
||||
PreviewTexture->UpdateResource();
|
||||
|
||||
return PreviewTexture;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#include "IconStudioSession.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UIconStudioSession::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
|
||||
// Modifier la liste du lot ne change rien a l'image affichee : inutile de
|
||||
// relancer un rendu complet a chaque ligne ajoutee.
|
||||
const FName PropertyName = PropertyChangedEvent.GetPropertyName();
|
||||
static const TSet<FName> PreviewProperties = {
|
||||
GET_MEMBER_NAME_CHECKED(UIconStudioSession, Preset),
|
||||
GET_MEMBER_NAME_CHECKED(UIconStudioSession, Subject),
|
||||
GET_MEMBER_NAME_CHECKED(UIconStudioSession, Pass),
|
||||
};
|
||||
|
||||
if (PreviewProperties.Contains(PropertyName))
|
||||
{
|
||||
OnSessionChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#include "IconStudioSettings.h"
|
||||
|
||||
UIconStudioSettings::UIconStudioSettings()
|
||||
{
|
||||
// Ordre volontaire : un DataAsset qui exposerait a la fois un mesh "en main"
|
||||
// et un mesh "au sol" doit rendre celui qu'on voit dans le monde.
|
||||
MeshPropertyNames = { TEXT("WorldMesh"), TEXT("Mesh"), TEXT("StaticMesh") };
|
||||
MaterialOverridePropertyName = TEXT("WorldMeshMaterial");
|
||||
IconPropertyName = TEXT("Icon");
|
||||
DisplayNamePropertyName = TEXT("ItemId");
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#include "IconStudioSubsystem.h"
|
||||
|
||||
#include "IconStudioPreset.h"
|
||||
#include "IconStudioRenderer.h"
|
||||
#include "IconStudioSession.h"
|
||||
#include "IconStudioSettings.h"
|
||||
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "ContentBrowserModule.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "IContentBrowserSingleton.h"
|
||||
#include "ImageCore.h"
|
||||
#include "ImageUtils.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Misc/ScopedSlowTask.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogIconStudio, Log, All);
|
||||
|
||||
#define LOCTEXT_NAMESPACE "IconStudio"
|
||||
|
||||
namespace IconStudioLocal
|
||||
{
|
||||
/** Lit une propriete objet par son nom, en verifiant le type attendu. */
|
||||
UObject* GetObjectProperty(const UObject* Owner, FName PropertyName, const UClass* ExpectedClass)
|
||||
{
|
||||
if (!Owner || PropertyName.IsNone())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FObjectPropertyBase* Property = FindFProperty<FObjectPropertyBase>(Owner->GetClass(), PropertyName);
|
||||
if (!Property)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UObject* Value = Property->GetObjectPropertyValue_InContainer(Owner);
|
||||
return (Value && Value->IsA(ExpectedClass)) ? Value : nullptr;
|
||||
}
|
||||
|
||||
/** Lit un nom lisible, quel que soit le type textuel de la propriete. */
|
||||
FString GetTextualProperty(const UObject* Owner, FName PropertyName)
|
||||
{
|
||||
if (!Owner || PropertyName.IsNone())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
const UClass* Class = Owner->GetClass();
|
||||
if (const FNameProperty* NameProperty = FindFProperty<FNameProperty>(Class, PropertyName))
|
||||
{
|
||||
const FName Value = NameProperty->GetPropertyValue_InContainer(Owner);
|
||||
return Value.IsNone() ? FString() : Value.ToString();
|
||||
}
|
||||
if (const FStrProperty* StringProperty = FindFProperty<FStrProperty>(Class, PropertyName))
|
||||
{
|
||||
return StringProperty->GetPropertyValue_InContainer(Owner);
|
||||
}
|
||||
if (const FTextProperty* TextProperty = FindFProperty<FTextProperty>(Class, PropertyName))
|
||||
{
|
||||
return TextProperty->GetPropertyValue_InContainer(Owner).ToString();
|
||||
}
|
||||
|
||||
return FString();
|
||||
}
|
||||
}
|
||||
|
||||
void UIconStudioSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
Renderer = MakeUnique<FIconStudioRenderer>();
|
||||
|
||||
// Regler une lumiere du preset doit rafraichir l'image tout de suite, or le
|
||||
// preset est edite dans son propre Details View, que l'outil ne possede pas.
|
||||
// Ecouter le signal global est le seul point d'accroche qui couvre les deux
|
||||
// panneaux -- d'ou le filtrage strict juste apres, cette delegation etant
|
||||
// emise pour toute propriete modifiee dans l'editeur.
|
||||
PropertyChangedHandle = FCoreUObjectDelegates::OnObjectPropertyChanged.AddUObject(this, &UIconStudioSubsystem::HandleObjectPropertyChanged);
|
||||
}
|
||||
|
||||
void UIconStudioSubsystem::Deinitialize()
|
||||
{
|
||||
FCoreUObjectDelegates::OnObjectPropertyChanged.Remove(PropertyChangedHandle);
|
||||
PropertyChangedHandle.Reset();
|
||||
|
||||
Renderer.Reset();
|
||||
Session = nullptr;
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
UIconStudioSession* UIconStudioSubsystem::GetSession()
|
||||
{
|
||||
if (!Session)
|
||||
{
|
||||
Session = NewObject<UIconStudioSession>(this, NAME_None, RF_Transient);
|
||||
Session->OnSessionChanged.AddUObject(this, &UIconStudioSubsystem::HandleSessionChanged);
|
||||
}
|
||||
return Session;
|
||||
}
|
||||
|
||||
void UIconStudioSubsystem::HandleSessionChanged()
|
||||
{
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
void UIconStudioSubsystem::HandleObjectPropertyChanged(UObject* Object, FPropertyChangedEvent& Event)
|
||||
{
|
||||
if (!Session || !Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Seul le preset en cours nous interesse : la session, elle, previent deja
|
||||
// par son propre delegue.
|
||||
if (Object == Session->Preset)
|
||||
{
|
||||
RefreshPreview();
|
||||
}
|
||||
}
|
||||
|
||||
void UIconStudioSubsystem::Status(const FString& Message, bool bIsError)
|
||||
{
|
||||
if (bIsError)
|
||||
{
|
||||
UE_LOG(LogIconStudio, Warning, TEXT("%s"), *Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogIconStudio, Log, TEXT("%s"), *Message);
|
||||
}
|
||||
|
||||
OnStatus.Broadcast(Message);
|
||||
}
|
||||
|
||||
UStaticMesh* UIconStudioSubsystem::ResolveMesh(UObject* Subject, UMaterialInterface*& OutMaterialOverride) const
|
||||
{
|
||||
OutMaterialOverride = nullptr;
|
||||
|
||||
if (!Subject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Cas direct : on previsualise un mesh pour lui-meme.
|
||||
if (UStaticMesh* AsMesh = Cast<UStaticMesh>(Subject))
|
||||
{
|
||||
return AsMesh;
|
||||
}
|
||||
|
||||
const UIconStudioSettings* Settings = GetDefault<UIconStudioSettings>();
|
||||
|
||||
for (const FName& PropertyName : Settings->MeshPropertyNames)
|
||||
{
|
||||
if (UObject* Found = IconStudioLocal::GetObjectProperty(Subject, PropertyName, UStaticMesh::StaticClass()))
|
||||
{
|
||||
OutMaterialOverride = Cast<UMaterialInterface>(
|
||||
IconStudioLocal::GetObjectProperty(Subject, Settings->MaterialOverridePropertyName, UMaterialInterface::StaticClass()));
|
||||
return Cast<UStaticMesh>(Found);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FString UIconStudioSubsystem::BuildAssetName(UObject* Subject, const UIconStudioPreset* Preset) const
|
||||
{
|
||||
const UIconStudioSettings* Settings = GetDefault<UIconStudioSettings>();
|
||||
|
||||
FString BaseName = IconStudioLocal::GetTextualProperty(Subject, Settings->DisplayNamePropertyName);
|
||||
if (BaseName.IsEmpty())
|
||||
{
|
||||
// Retomber sur le nom de l'asset, en retirant son prefixe de convention :
|
||||
// DA_Branch donnerait sinon T_Icon_DA_Branch.
|
||||
BaseName = Subject->GetName();
|
||||
int32 UnderscoreIndex = INDEX_NONE;
|
||||
if (BaseName.FindChar(TEXT('_'), UnderscoreIndex) && UnderscoreIndex <= 3)
|
||||
{
|
||||
BaseName.MidInline(UnderscoreIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return ObjectTools::SanitizeObjectName(Preset->AssetPrefix + BaseName);
|
||||
}
|
||||
|
||||
UTexture2D* UIconStudioSubsystem::CreateOrUpdateTexture(const FIconStudioImage& Image, const FString& PackagePath, const FString& AssetName, TArray<UPackage*>& OutTouchedPackages)
|
||||
{
|
||||
const FString PackageName = PackagePath / AssetName;
|
||||
if (!FPackageName::IsValidLongPackageName(PackageName))
|
||||
{
|
||||
Status(FString::Printf(TEXT("Invalid output path: %s"), *PackageName), /*bIsError=*/true);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Regenerer une icone doit MODIFIER la texture existante, jamais en creer une
|
||||
// seconde : tout ce qui la reference -- les DataAssets, les widgets -- doit
|
||||
// continuer de pointer au meme endroit.
|
||||
const FString ObjectPath = PackageName + TEXT(".") + AssetName;
|
||||
UObject* Existing = LoadObject<UObject>(nullptr, *ObjectPath, nullptr, LOAD_NoWarn | LOAD_Quiet);
|
||||
if (Existing && !Existing->IsA<UTexture2D>())
|
||||
{
|
||||
Status(FString::Printf(TEXT("%s already exists and is not a texture."), *ObjectPath), /*bIsError=*/true);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UPackage* Package = Existing ? Existing->GetOutermost() : CreatePackage(*PackageName);
|
||||
if (!Package)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UTexture2D* Texture = Cast<UTexture2D>(Existing);
|
||||
const bool bIsNew = (Texture == nullptr);
|
||||
if (bIsNew)
|
||||
{
|
||||
Texture = NewObject<UTexture2D>(Package, *AssetName, RF_Public | RF_Standalone);
|
||||
}
|
||||
|
||||
Texture->Modify();
|
||||
// FColor est stocke en BGRA en memoire : la source de texture prend donc les
|
||||
// pixels tels quels, sans conversion.
|
||||
Texture->Source.Init(Image.Size, Image.Size, 1, 1, TSF_BGRA8, reinterpret_cast<const uint8*>(Image.Pixels.GetData()));
|
||||
|
||||
// Reglages d'une texture d'interface : compression qui preserve l'alpha,
|
||||
// aucun mip -- une icone est affichee a taille fixe, ses mips ne serviraient
|
||||
// qu'a la rendre floue -- et pas de streaming, pour qu'elle soit la des la
|
||||
// premiere image du HUD.
|
||||
Texture->SRGB = true;
|
||||
Texture->CompressionSettings = TC_EditorIcon;
|
||||
Texture->MipGenSettings = TMGS_NoMipmaps;
|
||||
Texture->LODGroup = TEXTUREGROUP_UI;
|
||||
Texture->NeverStream = true;
|
||||
Texture->Filter = TF_Bilinear;
|
||||
Texture->PostEditChange();
|
||||
|
||||
if (bIsNew)
|
||||
{
|
||||
FAssetRegistryModule::AssetCreated(Texture);
|
||||
}
|
||||
|
||||
Package->MarkPackageDirty();
|
||||
OutTouchedPackages.AddUnique(Package);
|
||||
|
||||
return Texture;
|
||||
}
|
||||
|
||||
bool UIconStudioSubsystem::AssignIcon(UObject* Subject, UTexture2D* Texture, TArray<UPackage*>& OutTouchedPackages) const
|
||||
{
|
||||
if (!Subject || !Texture)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIconStudioSettings* Settings = GetDefault<UIconStudioSettings>();
|
||||
if (Settings->IconPropertyName.IsNone())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FObjectPropertyBase* Property = FindFProperty<FObjectPropertyBase>(Subject->GetClass(), Settings->IconPropertyName);
|
||||
if (!Property || !Property->PropertyClass || !Texture->IsA(Property->PropertyClass))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Subject->Modify();
|
||||
Property->SetObjectPropertyValue_InContainer(Subject, Texture);
|
||||
|
||||
// Sans PostEditChangeProperty, un DataAsset qui valide ses donnees ou met en
|
||||
// cache quelque chose ne verrait jamais passer la modification.
|
||||
FPropertyChangedEvent ChangedEvent(Property);
|
||||
Subject->PostEditChangeProperty(ChangedEvent);
|
||||
Subject->MarkPackageDirty();
|
||||
|
||||
OutTouchedPackages.AddUnique(Subject->GetOutermost());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIconStudioSubsystem::WritePng(const FIconStudioImage& Image, const FString& AssetName) const
|
||||
{
|
||||
const FString Directory = FPaths::ProjectSavedDir() / TEXT("IconStudio");
|
||||
const FString FullPath = Directory / (AssetName + TEXT(".png"));
|
||||
|
||||
FImage Output;
|
||||
Output.Init(Image.Size, Image.Size, ERawImageFormat::BGRA8, EGammaSpace::sRGB);
|
||||
FMemory::Memcpy(Output.RawData.GetData(), Image.Pixels.GetData(), Image.Pixels.Num() * sizeof(FColor));
|
||||
|
||||
return FImageUtils::SaveImageByExtension(*FullPath, Output);
|
||||
}
|
||||
|
||||
UTexture2D* UIconStudioSubsystem::RefreshPreview()
|
||||
{
|
||||
UIconStudioSession* const Current = GetSession();
|
||||
|
||||
// Un seul point de sortie qui previent toujours, meme en cas d'echec : c'est
|
||||
// ce signal qui sert aussi a la fenetre pour raccrocher son panneau de
|
||||
// preset. S'en dispenser quand il n'y a rien a rendre laisserait l'interface
|
||||
// afficher les reglages du preset precedent.
|
||||
UTexture2D* Preview = nullptr;
|
||||
|
||||
if (Current->Preset && Current->Subject && Renderer.IsValid())
|
||||
{
|
||||
UMaterialInterface* MaterialOverride = nullptr;
|
||||
if (UStaticMesh* const Mesh = ResolveMesh(Current->Subject, MaterialOverride))
|
||||
{
|
||||
FIconStudioImage Image;
|
||||
if (Renderer->Render(Mesh, MaterialOverride, Current->Preset, Current->Pass, Image))
|
||||
{
|
||||
Preview = Renderer->UpdatePreviewTexture(Image);
|
||||
}
|
||||
else
|
||||
{
|
||||
Status(TEXT("Render failed."), /*bIsError=*/true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Status(FString::Printf(TEXT("%s exposes no static mesh to render."), *Current->Subject->GetName()), /*bIsError=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
OnPreviewUpdated.Broadcast(Preview);
|
||||
return Preview;
|
||||
}
|
||||
|
||||
bool UIconStudioSubsystem::GenerateInternal(UObject* Subject, TArray<UPackage*>& OutTouchedPackages)
|
||||
{
|
||||
UIconStudioSession* const Current = GetSession();
|
||||
|
||||
const UIconStudioPreset* const Preset = EnsurePreset();
|
||||
if (!Preset)
|
||||
{
|
||||
Status(TEXT("No preset available."), /*bIsError=*/true);
|
||||
return false;
|
||||
}
|
||||
if (!Subject)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIconStudioSettings* Settings = GetDefault<UIconStudioSettings>();
|
||||
|
||||
if (Current->bSkipIfIconAlreadySet)
|
||||
{
|
||||
if (IconStudioLocal::GetObjectProperty(Subject, Settings->IconPropertyName, UTexture2D::StaticClass()))
|
||||
{
|
||||
Status(FString::Printf(TEXT("%s already has an icon, skipped."), *Subject->GetName()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
UMaterialInterface* MaterialOverride = nullptr;
|
||||
UStaticMesh* const Mesh = ResolveMesh(Subject, MaterialOverride);
|
||||
if (!Mesh)
|
||||
{
|
||||
Status(FString::Printf(TEXT("%s exposes no static mesh, skipped."), *Subject->GetName()), /*bIsError=*/true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// La passe choisie dans la session s'applique aussi au lot : exporter les
|
||||
// base colors de toute une famille d'objets est un besoin reel. C'est bien
|
||||
// ce qu'on voit dans la previsualisation qui sera ecrit sur le disque.
|
||||
FIconStudioImage Image;
|
||||
if (!Renderer->Render(Mesh, MaterialOverride, Preset, Current->Pass, Image))
|
||||
{
|
||||
Status(FString::Printf(TEXT("Render failed for %s."), *Subject->GetName()), /*bIsError=*/true);
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString AssetName = BuildAssetName(Subject, Preset);
|
||||
|
||||
if (Preset->bAlsoWritePngToDisk && !WritePng(Image, AssetName))
|
||||
{
|
||||
Status(FString::Printf(TEXT("Could not write the PNG for %s."), *AssetName), /*bIsError=*/true);
|
||||
}
|
||||
|
||||
UTexture2D* const Texture = CreateOrUpdateTexture(Image, Preset->OutputDirectory.Path, AssetName, OutTouchedPackages);
|
||||
if (!Texture)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Current->bAssignIconToDataAsset && Subject != Mesh)
|
||||
{
|
||||
if (AssignIcon(Subject, Texture, OutTouchedPackages))
|
||||
{
|
||||
Status(FString::Printf(TEXT("%s -> %s (assigned)"), *Subject->GetName(), *AssetName));
|
||||
return true;
|
||||
}
|
||||
|
||||
Status(FString::Printf(TEXT("%s -> %s (created, but no '%s' property to assign it to)"),
|
||||
*Subject->GetName(), *AssetName, *Settings->IconPropertyName.ToString()));
|
||||
return true;
|
||||
}
|
||||
|
||||
Status(FString::Printf(TEXT("%s -> %s"), *Subject->GetName(), *AssetName));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIconStudioSubsystem::SavePackageToDisk(UPackage* Package)
|
||||
{
|
||||
if (!Package)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString Filename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
|
||||
|
||||
FSavePackageArgs SaveArgs;
|
||||
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
SaveArgs.SaveFlags = SAVE_NoError;
|
||||
SaveArgs.Error = GError;
|
||||
|
||||
return UPackage::SavePackage(Package, nullptr, *Filename, SaveArgs);
|
||||
}
|
||||
|
||||
void UIconStudioSubsystem::SavePackages(const TArray<UPackage*>& Packages) const
|
||||
{
|
||||
if (!GetDefault<UIconStudioSettings>()->bSaveModifiedPackages)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (UPackage* Package : Packages)
|
||||
{
|
||||
SavePackageToDisk(Package);
|
||||
}
|
||||
}
|
||||
|
||||
UIconStudioPreset* UIconStudioSubsystem::EnsurePreset()
|
||||
{
|
||||
UIconStudioSession* const Current = GetSession();
|
||||
if (Current->Preset)
|
||||
{
|
||||
return Current->Preset;
|
||||
}
|
||||
|
||||
const IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
||||
|
||||
// Tant que le registre balaie encore le projet, une recherche infructueuse
|
||||
// ne prouve rien : creer un preset maintenant en ferait un second a cote de
|
||||
// celui qu'on n'a pas encore vu.
|
||||
if (AssetRegistry.IsLoadingAssets())
|
||||
{
|
||||
Status(TEXT("Asset registry still scanning, try again in a second."), /*bIsError=*/true);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FARFilter Filter;
|
||||
Filter.ClassPaths.Add(UIconStudioPreset::StaticClass()->GetClassPathName());
|
||||
Filter.bRecursiveClasses = true;
|
||||
|
||||
TArray<FAssetData> Existing;
|
||||
AssetRegistry.GetAssets(Filter, Existing);
|
||||
|
||||
if (!Existing.IsEmpty())
|
||||
{
|
||||
Current->Preset = Cast<UIconStudioPreset>(Existing[0].GetAsset());
|
||||
if (Current->Preset)
|
||||
{
|
||||
Status(FString::Printf(TEXT("Using preset %s."), *Current->Preset->GetName()));
|
||||
return Current->Preset;
|
||||
}
|
||||
}
|
||||
|
||||
// Aucun preset dans le projet : on en pose un, avec les valeurs par defaut
|
||||
// de la classe. C'est un asset normal, que l'auteur retouche, renomme ou
|
||||
// duplique ensuite comme n'importe quel autre.
|
||||
static const FString DefaultPackageName(TEXT("/Game/EditorTools/DA_IconPreset_Default"));
|
||||
static const FString DefaultAssetName(TEXT("DA_IconPreset_Default"));
|
||||
|
||||
UPackage* const Package = CreatePackage(*DefaultPackageName);
|
||||
if (!Package)
|
||||
{
|
||||
Status(TEXT("Could not create the default preset package."), /*bIsError=*/true);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIconStudioPreset* const Preset = NewObject<UIconStudioPreset>(Package, *DefaultAssetName, RF_Public | RF_Standalone);
|
||||
FAssetRegistryModule::AssetCreated(Preset);
|
||||
Package->MarkPackageDirty();
|
||||
SavePackageToDisk(Package);
|
||||
|
||||
Current->Preset = Preset;
|
||||
Status(FString::Printf(TEXT("Created a default preset: %s"), *DefaultPackageName));
|
||||
|
||||
return Preset;
|
||||
}
|
||||
|
||||
bool UIconStudioSubsystem::GenerateForSubject(UObject* Subject)
|
||||
{
|
||||
TArray<UPackage*> Touched;
|
||||
const bool bSuccess = GenerateInternal(Subject, Touched);
|
||||
SavePackages(Touched);
|
||||
return bSuccess;
|
||||
}
|
||||
|
||||
bool UIconStudioSubsystem::GenerateCurrent()
|
||||
{
|
||||
return GenerateForSubject(GetSession()->Subject);
|
||||
}
|
||||
|
||||
int32 UIconStudioSubsystem::GenerateBatch()
|
||||
{
|
||||
UIconStudioSession* const Current = GetSession();
|
||||
if (Current->BatchTargets.IsEmpty())
|
||||
{
|
||||
Status(TEXT("Batch list is empty."), /*bIsError=*/true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
TArray<UPackage*> Touched;
|
||||
int32 SuccessCount = 0;
|
||||
|
||||
// Un lot de deux cents objets fige l'editeur plusieurs secondes : sans barre
|
||||
// de progression, l'utilisateur croit a un plantage et tue le processus au
|
||||
// milieu de l'ecriture des packages.
|
||||
FScopedSlowTask SlowTask(static_cast<float>(Current->BatchTargets.Num()), LOCTEXT("GeneratingIcons", "Generating icons..."));
|
||||
SlowTask.MakeDialog(/*bShowCancelButton=*/true);
|
||||
|
||||
for (const TObjectPtr<UObject>& Target : Current->BatchTargets)
|
||||
{
|
||||
if (SlowTask.ShouldCancel())
|
||||
{
|
||||
Status(TEXT("Batch cancelled."));
|
||||
break;
|
||||
}
|
||||
|
||||
SlowTask.EnterProgressFrame(1.f, FText::FromString(Target ? Target->GetName() : TEXT("...")));
|
||||
|
||||
if (GenerateInternal(Target, Touched))
|
||||
{
|
||||
++SuccessCount;
|
||||
}
|
||||
}
|
||||
|
||||
SavePackages(Touched);
|
||||
Status(FString::Printf(TEXT("Batch finished: %d / %d icons generated."), SuccessCount, Current->BatchTargets.Num()));
|
||||
|
||||
return SuccessCount;
|
||||
}
|
||||
|
||||
int32 UIconStudioSubsystem::AddContentBrowserSelectionToBatch()
|
||||
{
|
||||
FContentBrowserModule& ContentBrowser = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
|
||||
|
||||
TArray<FAssetData> Selection;
|
||||
ContentBrowser.Get().GetSelectedAssets(Selection);
|
||||
|
||||
UIconStudioSession* const Current = GetSession();
|
||||
int32 AddedCount = 0;
|
||||
|
||||
for (const FAssetData& Asset : Selection)
|
||||
{
|
||||
// GetAsset charge l'objet : c'est voulu, on a besoin de lire ses
|
||||
// proprietes, et il faudrait le charger de toute facon pour le rendre.
|
||||
UObject* Loaded = Asset.GetAsset();
|
||||
if (!Loaded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UMaterialInterface* Unused = nullptr;
|
||||
if (!ResolveMesh(Loaded, Unused))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Current->BatchTargets.Contains(Loaded))
|
||||
{
|
||||
Current->BatchTargets.Add(Loaded);
|
||||
++AddedCount;
|
||||
}
|
||||
}
|
||||
|
||||
Status(FString::Printf(TEXT("%d asset(s) added to the batch (%d total)."), AddedCount, Current->BatchTargets.Num()));
|
||||
return AddedCount;
|
||||
}
|
||||
|
||||
bool UIconStudioSubsystem::SetSubjectFromContentBrowser()
|
||||
{
|
||||
FContentBrowserModule& ContentBrowser = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
|
||||
|
||||
TArray<FAssetData> Selection;
|
||||
ContentBrowser.Get().GetSelectedAssets(Selection);
|
||||
|
||||
for (const FAssetData& Asset : Selection)
|
||||
{
|
||||
UObject* Loaded = Asset.GetAsset();
|
||||
UMaterialInterface* Unused = nullptr;
|
||||
if (Loaded && ResolveMesh(Loaded, Unused))
|
||||
{
|
||||
GetSession()->Subject = Loaded;
|
||||
RefreshPreview();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Status(TEXT("No renderable asset selected in the Content Browser."), /*bIsError=*/true);
|
||||
return false;
|
||||
}
|
||||
|
||||
void UIconStudioSubsystem::ClearBatch()
|
||||
{
|
||||
GetSession()->BatchTargets.Reset();
|
||||
Status(TEXT("Batch list cleared."));
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,362 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#include "SIconStudioTab.h"
|
||||
|
||||
#include "IconStudioPreset.h"
|
||||
#include "IconStudioSession.h"
|
||||
#include "IconStudioSubsystem.h"
|
||||
|
||||
#include "Editor.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "IDetailsView.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "PropertyEditorModule.h"
|
||||
#include "Styling/AppStyle.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Layout/SScaleBox.h"
|
||||
#include "Widgets/Layout/SSeparator.h"
|
||||
#include "Widgets/Layout/SSplitter.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "IconStudio"
|
||||
|
||||
namespace IconStudioTabLocal
|
||||
{
|
||||
/** Un bouton de la barre d'actions, tous construits pareil. */
|
||||
TSharedRef<SWidget> MakeButton(const FText& Label, const FText& Tooltip, FOnClicked OnClicked)
|
||||
{
|
||||
return SNew(SButton)
|
||||
.Text(Label)
|
||||
.ToolTipText(Tooltip)
|
||||
.HAlign(HAlign_Center)
|
||||
.ContentPadding(FMargin(10.f, 4.f))
|
||||
.OnClicked(OnClicked);
|
||||
}
|
||||
}
|
||||
|
||||
UIconStudioSubsystem* SIconStudioTab::GetStudio()
|
||||
{
|
||||
return GEditor ? GEditor->GetEditorSubsystem<UIconStudioSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
void SIconStudioTab::Construct(const FArguments& InArgs)
|
||||
{
|
||||
PreviewBrush.DrawAs = ESlateBrushDrawType::Image;
|
||||
PreviewBrush.ImageSize = FVector2D(256.f, 256.f);
|
||||
|
||||
StatusMessage = LOCTEXT("Ready", "Pick a preset and a subject to start.");
|
||||
|
||||
ChildSlot
|
||||
[
|
||||
SNew(SSplitter)
|
||||
.Orientation(Orient_Horizontal)
|
||||
|
||||
+ SSplitter::Slot()
|
||||
.Value(0.62f)
|
||||
[
|
||||
BuildPreviewPanel()
|
||||
]
|
||||
|
||||
+ SSplitter::Slot()
|
||||
.Value(0.38f)
|
||||
[
|
||||
BuildSettingsPanel()
|
||||
]
|
||||
];
|
||||
|
||||
// L'abonnement vient apres la construction : les panneaux de details doivent
|
||||
// exister avant qu'un premier rendu ne cherche a les remplir.
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
PreviewHandle = Studio->OnPreviewUpdated.AddSP(this, &SIconStudioTab::HandlePreviewUpdated);
|
||||
StatusHandle = Studio->OnStatus.AddSP(this, &SIconStudioTab::HandleStatus);
|
||||
|
||||
SessionDetails->SetObject(Studio->GetSession());
|
||||
|
||||
// Ouvrir la fenetre suffit a avoir un preset utilisable : celui du
|
||||
// projet s'il en existe un, un neuf sinon.
|
||||
Studio->EnsurePreset();
|
||||
Studio->RefreshPreview();
|
||||
}
|
||||
}
|
||||
|
||||
SIconStudioTab::~SIconStudioTab()
|
||||
{
|
||||
// Le sous-systeme survit a la fenetre : sans desabonnement il diffuserait
|
||||
// dans un widget detruit a la premiere generation suivante.
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
Studio->OnPreviewUpdated.Remove(PreviewHandle);
|
||||
Studio->OnStatus.Remove(StatusHandle);
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> SIconStudioTab::BuildPreviewPanel()
|
||||
{
|
||||
// Le damier est le seul moyen de VOIR la transparence : sur un aplat, une
|
||||
// icone au fond noir et une icone au fond transparent sont identiques, et on
|
||||
// ne s'apercoit du probleme qu'une fois la texture dans le jeu.
|
||||
const FSlateBrush* Checkerboard = FAppStyle::Get().GetBrush(TEXT("Checkerboard"));
|
||||
|
||||
const TAttribute<EVisibility> PlaceholderVisibility = TAttribute<EVisibility>::CreateLambda(
|
||||
[this]() { return PreviewBrush.GetResourceObject() ? EVisibility::Collapsed : EVisibility::Visible; });
|
||||
|
||||
return SNew(SVerticalBox)
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.f)
|
||||
.Padding(6.f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderImage(FAppStyle::Get().GetBrush(TEXT("ToolPanel.GroupBorder")))
|
||||
.Padding(8.f)
|
||||
[
|
||||
SNew(SOverlay)
|
||||
|
||||
+ SOverlay::Slot()
|
||||
[
|
||||
SNew(SImage).Image(Checkerboard)
|
||||
]
|
||||
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Center)
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SScaleBox)
|
||||
.Stretch(EStretch::ScaleToFit)
|
||||
// Jamais au-dela de la taille native : une icone de 256
|
||||
// pixels etiree sur tout le panneau parait floue et donne une
|
||||
// fausse idee de sa nettete reelle. On la juge a sa taille.
|
||||
.StretchDirection(EStretchDirection::DownOnly)
|
||||
[
|
||||
SNew(SImage)
|
||||
.Image(&PreviewBrush)
|
||||
.Visibility(this, &SIconStudioTab::GetPreviewVisibility)
|
||||
]
|
||||
]
|
||||
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Center)
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LOCTEXT("NoPreview", "No preview.\nPick a preset, then select an asset and press Use Selection."))
|
||||
.Justification(ETextJustify::Center)
|
||||
.Visibility(PlaceholderVisibility)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(6.f, 0.f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0.f, 0.f, 4.f, 0.f)
|
||||
[
|
||||
IconStudioTabLocal::MakeButton(
|
||||
LOCTEXT("UseSelection", "Use Selection"),
|
||||
LOCTEXT("UseSelectionTip", "Preview the first renderable asset selected in the Content Browser."),
|
||||
FOnClicked::CreateSP(this, &SIconStudioTab::OnUseSelectionClicked))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(0.f, 0.f, 4.f, 0.f)
|
||||
[
|
||||
IconStudioTabLocal::MakeButton(
|
||||
LOCTEXT("Refresh", "Refresh"),
|
||||
LOCTEXT("RefreshTip", "Render the preview again."),
|
||||
FOnClicked::CreateSP(this, &SIconStudioTab::OnRefreshClicked))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.f)
|
||||
[
|
||||
IconStudioTabLocal::MakeButton(
|
||||
LOCTEXT("Generate", "Generate"),
|
||||
LOCTEXT("GenerateTip", "Write the icon of the previewed subject and assign it."),
|
||||
FOnClicked::CreateSP(this, &SIconStudioTab::OnGenerateClicked))
|
||||
]
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(6.f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(this, &SIconStudioTab::GetStatusText)
|
||||
.AutoWrapText(true)
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> SIconStudioTab::BuildSettingsPanel()
|
||||
{
|
||||
FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
|
||||
|
||||
FDetailsViewArgs SessionArgs;
|
||||
SessionArgs.bAllowSearch = false;
|
||||
SessionArgs.bHideSelectionTip = true;
|
||||
SessionArgs.bShowOptions = false;
|
||||
// Le bandeau de nom afficherait "IconStudioSession", ce qui n'apprend rien.
|
||||
SessionArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
|
||||
SessionDetails = PropertyEditor.CreateDetailView(SessionArgs);
|
||||
|
||||
FDetailsViewArgs PresetArgs = SessionArgs;
|
||||
// Le preset a beaucoup plus de champs : la recherche y devient utile.
|
||||
PresetArgs.bAllowSearch = true;
|
||||
PresetDetails = PropertyEditor.CreateDetailView(PresetArgs);
|
||||
|
||||
return SNew(SVerticalBox)
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SessionDetails.ToSharedRef()
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.f, 4.f)
|
||||
[
|
||||
SNew(SSeparator)
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.f)
|
||||
[
|
||||
PresetDetails.ToSharedRef()
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(4.f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.f)
|
||||
.Padding(0.f, 0.f, 4.f, 0.f)
|
||||
[
|
||||
IconStudioTabLocal::MakeButton(
|
||||
LOCTEXT("AddSelection", "Add Selection"),
|
||||
LOCTEXT("AddSelectionTip", "Add every renderable asset selected in the Content Browser to the batch."),
|
||||
FOnClicked::CreateSP(this, &SIconStudioTab::OnAddSelectionClicked))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1.f)
|
||||
.Padding(0.f, 0.f, 4.f, 0.f)
|
||||
[
|
||||
IconStudioTabLocal::MakeButton(
|
||||
LOCTEXT("GenerateBatch", "Generate Batch"),
|
||||
LOCTEXT("GenerateBatchTip", "Render every asset in the batch list and assign their icons."),
|
||||
FOnClicked::CreateSP(this, &SIconStudioTab::OnGenerateBatchClicked))
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
IconStudioTabLocal::MakeButton(
|
||||
LOCTEXT("ClearBatch", "Clear"),
|
||||
LOCTEXT("ClearBatchTip", "Empty the batch list."),
|
||||
FOnClicked::CreateSP(this, &SIconStudioTab::OnClearBatchClicked))
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
EVisibility SIconStudioTab::GetPreviewVisibility() const
|
||||
{
|
||||
return PreviewBrush.GetResourceObject() ? EVisibility::Visible : EVisibility::Collapsed;
|
||||
}
|
||||
|
||||
void SIconStudioTab::HandlePreviewUpdated(UTexture2D* PreviewTexture)
|
||||
{
|
||||
if (PreviewTexture)
|
||||
{
|
||||
PreviewBrush.SetResourceObject(PreviewTexture);
|
||||
PreviewBrush.ImageSize = FVector2D(PreviewTexture->GetSizeX(), PreviewTexture->GetSizeY());
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewBrush.SetResourceObject(nullptr);
|
||||
}
|
||||
|
||||
// Le panneau du bas suit le preset choisi dans celui du haut. SetObject sur
|
||||
// le meme objet ne fait rien, donc editer une valeur ne coupe pas la saisie
|
||||
// en cours malgre le rendu declenche a chaque frappe.
|
||||
if (PresetDetails.IsValid())
|
||||
{
|
||||
UIconStudioSubsystem* const Studio = GetStudio();
|
||||
UIconStudioPreset* const Preset = Studio ? Studio->GetSession()->Preset.Get() : nullptr;
|
||||
PresetDetails->SetObject(Preset);
|
||||
}
|
||||
}
|
||||
|
||||
void SIconStudioTab::HandleStatus(const FString& Message)
|
||||
{
|
||||
StatusMessage = FText::FromString(Message);
|
||||
}
|
||||
|
||||
FReply SIconStudioTab::OnRefreshClicked()
|
||||
{
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
Studio->RefreshPreview();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SIconStudioTab::OnGenerateClicked()
|
||||
{
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
Studio->GenerateCurrent();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SIconStudioTab::OnUseSelectionClicked()
|
||||
{
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
Studio->SetSubjectFromContentBrowser();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SIconStudioTab::OnAddSelectionClicked()
|
||||
{
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
Studio->AddContentBrowserSelectionToBatch();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SIconStudioTab::OnGenerateBatchClicked()
|
||||
{
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
Studio->GenerateBatch();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SIconStudioTab::OnClearBatchClicked()
|
||||
{
|
||||
if (UIconStudioSubsystem* const Studio = GetStudio())
|
||||
{
|
||||
Studio->ClearBatch();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,67 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Styling/SlateBrush.h"
|
||||
#include "Widgets/SCompoundWidget.h"
|
||||
|
||||
class IDetailsView;
|
||||
class UIconStudioSubsystem;
|
||||
class UTexture2D;
|
||||
|
||||
/**
|
||||
* La fenetre du studio, entierement en Slate.
|
||||
*
|
||||
* Aucun asset a creer dans l'editeur : l'outil est complet des le chargement du
|
||||
* module. En contrepartie, retoucher la mise en page demande une recompilation --
|
||||
* un echange raisonnable pour un outil dont l'interface ne bouge quasiment pas.
|
||||
*
|
||||
* Les deux panneaux de droite sont des IDetailsView, ceux du moteur : ils
|
||||
* fabriquent leurs champs a partir des UPROPERTY de la session et du preset.
|
||||
* Ajouter un reglage a UIconStudioPreset le fait donc apparaitre tout seul, sans
|
||||
* une ligne d'interface a ecrire.
|
||||
*/
|
||||
class SIconStudioTab : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SIconStudioTab) {}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs);
|
||||
virtual ~SIconStudioTab() override;
|
||||
|
||||
private:
|
||||
static UIconStudioSubsystem* GetStudio();
|
||||
|
||||
TSharedRef<SWidget> BuildPreviewPanel();
|
||||
TSharedRef<SWidget> BuildSettingsPanel();
|
||||
|
||||
void HandlePreviewUpdated(UTexture2D* PreviewTexture);
|
||||
void HandleStatus(const FString& Message);
|
||||
|
||||
FReply OnRefreshClicked();
|
||||
FReply OnGenerateClicked();
|
||||
FReply OnUseSelectionClicked();
|
||||
FReply OnAddSelectionClicked();
|
||||
FReply OnGenerateBatchClicked();
|
||||
FReply OnClearBatchClicked();
|
||||
|
||||
FText GetStatusText() const { return StatusMessage; }
|
||||
EVisibility GetPreviewVisibility() const;
|
||||
|
||||
TSharedPtr<IDetailsView> SessionDetails;
|
||||
TSharedPtr<IDetailsView> PresetDetails;
|
||||
|
||||
/**
|
||||
* La brosse pointe sur la texture de previsualisation, qui est recreee des
|
||||
* que la resolution change : elle est donc reassignee a chaque rendu, jamais
|
||||
* construite une fois pour toutes.
|
||||
*/
|
||||
FSlateBrush PreviewBrush;
|
||||
|
||||
FText StatusMessage;
|
||||
|
||||
FDelegateHandle PreviewHandle;
|
||||
FDelegateHandle StatusHandle;
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "IconStudioTypes.h"
|
||||
#include "IconStudioPreset.generated.h"
|
||||
|
||||
class UTextureCube;
|
||||
|
||||
/**
|
||||
* Le "look" d'une serie d'icones : angle, cadrage, lumieres, resolution.
|
||||
*
|
||||
* C'est la piece centrale de l'outil. Regler ces valeurs a la main objet par
|
||||
* objet donnerait deux cents icones qui ne vont pas ensemble -- des tailles
|
||||
* apparentes differentes, des ombres qui changent de cote. Un preset se regle
|
||||
* une fois sur un objet representatif, puis tous les autres passent dedans.
|
||||
*
|
||||
* Creer autant de presets que de familles d'icones : un pour l'inventaire, un
|
||||
* autre pour les grandes vignettes de recettes, par exemple.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class ICONSTUDIO_API UIconStudioPreset : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UIconStudioPreset();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Camera et cadrage
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Rotation horizontale autour de l'objet. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "-180", ClampMax = "180", Units = "deg"))
|
||||
float CameraYaw = 135.f;
|
||||
|
||||
/** Negatif = on regarde l'objet de haut. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "-89", ClampMax = "89", Units = "deg"))
|
||||
float CameraPitch = -25.f;
|
||||
|
||||
/**
|
||||
* Projection orthographique par defaut : sans point de fuite, une caisse
|
||||
* reste une caisse et deux objets de tailles differentes se comparent a
|
||||
* l'oeil. La perspective ne sert que pour un rendu de presentation.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
bool bOrthographic = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (EditCondition = "!bOrthographic", ClampMin = "5", ClampMax = "120", Units = "deg"))
|
||||
float FieldOfView = 30.f;
|
||||
|
||||
/** Marge laissee entre l'objet et le bord de l'image, en fraction de sa taille. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "0", UIMax = "0.5"))
|
||||
float FramingPadding = 0.06f;
|
||||
|
||||
/**
|
||||
* Decalage fin du cadre, en fraction de la largeur d'image.
|
||||
* Sert quand l'objet est visuellement desequilibre (un manche tres long).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (UIMin = "-0.5", UIMax = "0.5"))
|
||||
FVector2D FramingOffset = FVector2D::ZeroVector;
|
||||
|
||||
/**
|
||||
* Cadre tous les objets a la meme echelle au lieu de remplir l'image.
|
||||
*
|
||||
* Cadrage automatique : une pomme et un tronc occupent la meme place dans
|
||||
* leur icone. Echelle fixe : le tronc est visiblement plus gros, ce qui aide
|
||||
* a lire l'inventaire mais demande de choisir une taille de reference qui
|
||||
* ne laisse rien deborder.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||
bool bUseFixedScale = false;
|
||||
|
||||
/** Largeur du cadre en centimetres quand l'echelle est fixe. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (EditCondition = "bUseFixedScale", ClampMin = "1", Units = "cm"))
|
||||
float FixedFrameWidth = 100.f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Eclairage
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Lumiere principale : celle qui dessine la forme. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lighting")
|
||||
FIconStudioLight KeyLight;
|
||||
|
||||
/** Deboucheur : remonte les ombres du cote oppose, sans ombre portee. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lighting")
|
||||
FIconStudioLight FillLight;
|
||||
|
||||
/** Contre-jour : decolle l'objet du fond par un liseré lumineux. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lighting")
|
||||
FIconStudioLight RimLight;
|
||||
|
||||
/**
|
||||
* Cubemap d'ambiance. Sans elle, tout ce qui est metallique ou lisse rend
|
||||
* noir : le speculaire n'a rien a reflechir dans une scene vide.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lighting")
|
||||
TSoftObjectPtr<UTextureCube> SkyCubemap;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lighting", meta = (ClampMin = "0", UIMax = "5"))
|
||||
float SkyIntensity = 1.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lighting")
|
||||
FLinearColor SkyColor = FLinearColor::White;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Rendu
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Cote de l'image finale, en pixels. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render", meta = (ClampMin = "16", ClampMax = "2048"))
|
||||
int32 Resolution = 256;
|
||||
|
||||
/**
|
||||
* Facteur de suranalyse : on rend a Resolution x ce facteur, puis on reduit.
|
||||
*
|
||||
* C'est le seul antialiasing utilisable ici. L'antialiasing temporel du
|
||||
* moteur a besoin d'un historique de plusieurs images, qu'une capture unique
|
||||
* n'a pas : il produirait un tremblement different a chaque rendu, et surtout
|
||||
* un masque de transparence different entre les deux passes de fond.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render", meta = (ClampMin = "1", ClampMax = "8"))
|
||||
int32 SupersampleFactor = 4;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render")
|
||||
EIconStudioBackground BackgroundMode = EIconStudioBackground::Transparent;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render", meta = (EditCondition = "BackgroundMode == EIconStudioBackground::SolidColor", EditConditionHides))
|
||||
FLinearColor BackgroundColor = FLinearColor(0.05f, 0.05f, 0.06f, 1.f);
|
||||
|
||||
/** Correction d'exposition, en diaphragmes. L'exposition auto est desactivee. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render", meta = (UIMin = "-5", UIMax = "5"))
|
||||
float ExposureBias = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render", meta = (ClampMin = "0", UIMax = "2"))
|
||||
float BloomIntensity = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render")
|
||||
bool bAmbientOcclusion = true;
|
||||
|
||||
/**
|
||||
* Etale la couleur des bords sur les pixels transparents.
|
||||
*
|
||||
* Un pixel entierement transparent n'a pas de couleur definie ; laisse a
|
||||
* noir, il ressort en liseré sombre des que l'icone est filtree ou reduite.
|
||||
* Le defaut classique des icones generees.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Render", meta = (ClampMin = "0", ClampMax = "8"))
|
||||
int32 EdgeBleedPasses = 2;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Sortie
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Dossier de destination des textures generees. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Output", meta = (ContentDir))
|
||||
FDirectoryPath OutputDirectory;
|
||||
|
||||
/** Prefixe des assets crees. Le suffixe vient du nom de l'objet source. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Output")
|
||||
FString AssetPrefix = TEXT("T_Icon_");
|
||||
|
||||
/**
|
||||
* Ecrit aussi un PNG dans Saved/IconStudio/.
|
||||
* Utile pour retoucher une icone dans un logiciel externe ; l'asset, lui,
|
||||
* est cree dans tous les cas.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Output")
|
||||
bool bAlsoWritePngToDisk = true;
|
||||
|
||||
/** Resolution effective du rendu avant reduction. */
|
||||
int32 GetRenderResolution() const { return Resolution * FMath::Clamp(SupersampleFactor, 1, 8); }
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/GCObject.h"
|
||||
#include "IconStudioTypes.h"
|
||||
|
||||
class FPreviewScene;
|
||||
class UDirectionalLightComponent;
|
||||
class UIconStudioPreset;
|
||||
class UMaterialInterface;
|
||||
class USceneCaptureComponent2D;
|
||||
class USkyLightComponent;
|
||||
class UStaticMesh;
|
||||
class UStaticMeshComponent;
|
||||
class UTexture2D;
|
||||
class UTextureCube;
|
||||
class UTextureRenderTarget2D;
|
||||
|
||||
/**
|
||||
* Le moteur de rendu du studio : une scene isolee, trois lumieres et une
|
||||
* capture, reutilisees d'un objet a l'autre.
|
||||
*
|
||||
* Rien de tout cela ne touche au niveau ouvert dans l'editeur. FPreviewScene
|
||||
* cree son propre UWorld, invisible et non tique, comme celui de l'editeur de
|
||||
* Static Mesh -- on peut donc generer un lot entier pendant qu'une carte est
|
||||
* chargee, sans y poser le moindre acteur. La scene et ses composants sont
|
||||
* construits une seule fois puis reconfigures a chaque rendu : recreer un UWorld
|
||||
* par icone couterait bien plus cher que tout le reste du travail.
|
||||
*
|
||||
* COMMENT LA TRANSPARENCE EST OBTENUE
|
||||
*
|
||||
* Une capture de scene ne fournit pas de canal alpha exploitable : elle efface
|
||||
* toujours sa cible en noir opaque, sans tenir compte de la couleur de fond du
|
||||
* render target, et l'alpha qui ressort du post-process ne vaut quelque chose
|
||||
* qu'avec le support alpha active pour le projet entier -- un reglage de rendu
|
||||
* global qu'un outil d'icones n'a pas a imposer.
|
||||
*
|
||||
* D'ou deux captures du meme point de vue : une de couleur, une de PROFONDEUR.
|
||||
* La profondeur dit exactement ou se trouve de la geometrie, et c'est elle qui
|
||||
* sert de masque. Comme l'antialiasing du moteur est desactive, chaque pixel de
|
||||
* rendu est franc : soit objet, soit vide. La douceur des bords vient de la
|
||||
* suranalyse -- on rend a quatre fois la taille finale et on moyenne, ce qui
|
||||
* donne un alpha a seize niveaux et une couleur de bord juste.
|
||||
*
|
||||
* Limite assumee : un materiau TRANSLUCIDE n'ecrit pas dans le tampon de
|
||||
* profondeur, il sera donc absent du masque. Pour du verre ou du feuillage
|
||||
* translucide, passer le materiau en Masked le temps du rendu.
|
||||
*/
|
||||
class ICONSTUDIO_API FIconStudioRenderer : public FGCObject
|
||||
{
|
||||
public:
|
||||
FIconStudioRenderer();
|
||||
virtual ~FIconStudioRenderer();
|
||||
|
||||
//~ FGCObject
|
||||
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
|
||||
virtual FString GetReferencerName() const override { return TEXT("FIconStudioRenderer"); }
|
||||
//~ End FGCObject
|
||||
|
||||
/**
|
||||
* Rend un mesh et renvoie l'image composee, alpha compris.
|
||||
* MaterialOverride ne remplace que le premier slot, comme le fait le jeu.
|
||||
*/
|
||||
bool Render(UStaticMesh* Mesh, UMaterialInterface* MaterialOverride, const UIconStudioPreset* Preset, EIconStudioPass Pass, FIconStudioImage& OutImage);
|
||||
|
||||
/**
|
||||
* Recopie une image dans la texture de previsualisation et renvoie celle-ci.
|
||||
*
|
||||
* Une UTexture2D transitoire, et non un render target : l'image finale est
|
||||
* assemblee sur le CPU (masque, reduction, etalement des bords), donc le seul
|
||||
* moyen de montrer exactement ce qui sera exporte est de reinjecter ces
|
||||
* pixels-la dans une texture.
|
||||
*/
|
||||
UTexture2D* UpdatePreviewTexture(const FIconStudioImage& Image);
|
||||
|
||||
UTexture2D* GetPreviewTexture() const { return PreviewTexture; }
|
||||
|
||||
private:
|
||||
void EnsureScene();
|
||||
void ApplyLighting(const UIconStudioPreset* Preset, const FRotator& CameraRotation);
|
||||
void ApplyCamera(const UIconStudioPreset* Preset, const FRotator& CameraRotation, const FBoxSphereBounds& Bounds);
|
||||
void ApplyPostProcess(const UIconStudioPreset* Preset);
|
||||
|
||||
UTextureRenderTarget2D* EnsureTarget(TObjectPtr<UTextureRenderTarget2D>& Target, int32 Size, bool bFloatingPoint);
|
||||
|
||||
/** Une capture sur la cible donnee. Le mode de capture change ce qui est ecrit dans les pixels. */
|
||||
bool CaptureTo(UTextureRenderTarget2D* Target, uint8 CaptureSource);
|
||||
|
||||
TUniquePtr<FPreviewScene> Scene;
|
||||
|
||||
TObjectPtr<UStaticMeshComponent> MeshComponent;
|
||||
TObjectPtr<USceneCaptureComponent2D> CaptureComponent;
|
||||
TObjectPtr<UDirectionalLightComponent> KeyLightComponent;
|
||||
TObjectPtr<UDirectionalLightComponent> FillLightComponent;
|
||||
TObjectPtr<UDirectionalLightComponent> RimLightComponent;
|
||||
TObjectPtr<USkyLightComponent> SkyLightComponent;
|
||||
|
||||
/** Couleur en 8 bits sRGB, profondeur en virgule flottante : deux formats, deux cibles. */
|
||||
TObjectPtr<UTextureRenderTarget2D> ColorTarget;
|
||||
TObjectPtr<UTextureRenderTarget2D> DepthTarget;
|
||||
|
||||
TObjectPtr<UTexture2D> PreviewTexture;
|
||||
|
||||
/** Cubemap deja appliquee, pour ne pas recapturer le ciel a chaque rendu. */
|
||||
TObjectPtr<UTextureCube> AppliedSkyCubemap;
|
||||
float AppliedSkyIntensity = -1.f;
|
||||
|
||||
/** Au-dela de cette distance, un pixel est du vide. Recalcule a chaque cadrage. */
|
||||
float DepthThreshold = 0.f;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "IconStudioTypes.h"
|
||||
#include "IconStudioSession.generated.h"
|
||||
|
||||
class UIconStudioPreset;
|
||||
|
||||
/**
|
||||
* Ce sur quoi on travaille en ce moment : le preset choisi, l'objet affiche,
|
||||
* la liste du prochain lot.
|
||||
*
|
||||
* Cet objet n'existe que pour etre pose dans un widget Details View. C'est ce
|
||||
* qui evite d'avoir a dessiner une interface : le moteur genere lui-meme les
|
||||
* champs, les listes deroulantes et les selecteurs d'assets a partir des
|
||||
* UPROPERTY ci-dessous. Ajouter un reglage a l'outil ne demande donc aucune
|
||||
* retouche du Widget Blueprint.
|
||||
*
|
||||
* Il est transitoire a dessein : une session de travail ne se sauvegarde pas,
|
||||
* ce qui doit survivre est dans le preset.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class ICONSTUDIO_API UIconStudioSession : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Le look applique. Sans preset, rien ne se rend. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Subject")
|
||||
TObjectPtr<UIconStudioPreset> Preset;
|
||||
|
||||
/**
|
||||
* L'objet affiche dans la previsualisation : un StaticMesh directement, ou
|
||||
* un DataAsset dont on ira lire le mesh par reflexion.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Subject", meta = (AllowedClasses = "/Script/Engine.StaticMesh,/Script/Engine.DataAsset"))
|
||||
TObjectPtr<UObject> Subject;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Subject")
|
||||
EIconStudioPass Pass = EIconStudioPass::Final;
|
||||
|
||||
/** Les objets traites par le bouton de lot. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Batch", meta = (AllowedClasses = "/Script/Engine.StaticMesh,/Script/Engine.DataAsset"))
|
||||
TArray<TObjectPtr<UObject>> BatchTargets;
|
||||
|
||||
/**
|
||||
* Ecrit la texture generee dans le champ d'icone du DataAsset.
|
||||
* Sans effet sur un StaticMesh, qui n'a pas de champ ou ecrire.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Batch")
|
||||
bool bAssignIconToDataAsset = true;
|
||||
|
||||
/** Laisse tranquille les objets qui ont deja une icone. Utile pour completer un lot. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Batch")
|
||||
bool bSkipIfIconAlreadySet = false;
|
||||
|
||||
/** Prevenu des qu'un champ change, pour relancer la previsualisation. */
|
||||
DECLARE_MULTICAST_DELEGATE(FOnIconStudioSessionChanged);
|
||||
FOnIconStudioSessionChanged OnSessionChanged;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "IconStudioSettings.generated.h"
|
||||
|
||||
/**
|
||||
* Le pont entre l'outil et les DataAssets du jeu.
|
||||
*
|
||||
* Le plugin ne connait volontairement aucune classe du projet : il ne compile
|
||||
* pas contre UItemDataAsset et n'inclut aucun de ses en-tetes. Il retrouve le
|
||||
* mesh et le champ d'icone par REFLEXION, a partir des noms de proprietes
|
||||
* donnes ici. C'est ce qui lui permet de servir tel quel a un autre projet, ou
|
||||
* a une autre famille d'assets dans celui-ci -- il suffit de changer deux noms.
|
||||
*
|
||||
* Les valeurs par defaut correspondent a UItemDataAsset.
|
||||
*/
|
||||
UCLASS(config = Editor, defaultconfig, meta = (DisplayName = "Icon Studio"))
|
||||
class ICONSTUDIO_API UIconStudioSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UIconStudioSettings();
|
||||
|
||||
virtual FName GetContainerName() const override { return TEXT("Project"); }
|
||||
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
|
||||
|
||||
/**
|
||||
* Proprietes ou chercher le mesh a rendre, dans l'ordre.
|
||||
* La premiere qui existe et pointe vers un StaticMesh gagne.
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category = "Data Asset Binding")
|
||||
TArray<FName> MeshPropertyNames;
|
||||
|
||||
/** Propriete ou chercher un materiau qui remplace celui du mesh. Vide pour ne pas en chercher. */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Data Asset Binding")
|
||||
FName MaterialOverridePropertyName;
|
||||
|
||||
/** Propriete Texture2D a remplir avec l'icone generee. */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Data Asset Binding")
|
||||
FName IconPropertyName;
|
||||
|
||||
/**
|
||||
* Propriete servant a nommer l'asset produit (un FName ou un FString).
|
||||
* Vide, ou introuvable, on retombe sur le nom de l'asset source.
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category = "Data Asset Binding")
|
||||
FName DisplayNamePropertyName;
|
||||
|
||||
/**
|
||||
* Sauve les packages modifies a la fin d'un lot.
|
||||
*
|
||||
* Decoche, les textures generees et les DataAssets modifies restent
|
||||
* seulement en memoire : un editeur ferme sans Save All perd tout le lot.
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category = "Batch")
|
||||
bool bSaveModifiedPackages = true;
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EditorSubsystem.h"
|
||||
#include "IconStudioRenderer.h"
|
||||
#include "IconStudioTypes.h"
|
||||
#include "IconStudioSubsystem.generated.h"
|
||||
|
||||
class UIconStudioPreset;
|
||||
class UIconStudioSession;
|
||||
class UMaterialInterface;
|
||||
class UStaticMesh;
|
||||
class UTexture2D;
|
||||
|
||||
// Delegues C++ et non dynamiques : la fenetre est un widget Slate, qui n'est pas
|
||||
// un UObject et ne peut donc pas s'abonner a un delegue dynamique. Les fonctions
|
||||
// de l'outil restent BlueprintCallable pour rester pilotables depuis un script.
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FOnIconStudioPreviewUpdated, UTexture2D* /*PreviewTexture*/);
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FOnIconStudioStatus, const FString& /*Message*/);
|
||||
|
||||
/**
|
||||
* L'API de l'outil, cote Blueprint comme cote C++.
|
||||
*
|
||||
* Un sous-systeme d'editeur plutot qu'un objet possede par le widget : la scene
|
||||
* de rendu coute cher a construire, et un Editor Utility Widget est detruit et
|
||||
* recree a chaque fois qu'on ferme son onglet. Poser l'etat ici, c'est retrouver
|
||||
* son preset et sa liste de lot en rouvrant la fenetre, et ne payer la mise en
|
||||
* place du monde de preview qu'une fois par session d'editeur.
|
||||
*
|
||||
* Le widget ne fait donc que trois choses : afficher la texture renvoyee par
|
||||
* RefreshPreview, montrer la session dans un Details View, et appeler les deux
|
||||
* boutons de generation.
|
||||
*/
|
||||
UCLASS()
|
||||
class ICONSTUDIO_API UIconStudioSubsystem : public UEditorSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ USubsystem
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
//~ End USubsystem
|
||||
|
||||
/** L'objet a poser dans le Details View de la fenetre. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "Icon Studio")
|
||||
UIconStudioSession* GetSession();
|
||||
|
||||
/**
|
||||
* Garantit qu'un preset est selectionne : reprend celui du projet s'il en
|
||||
* existe un, en cree un sinon.
|
||||
*
|
||||
* Sans lui, l'outil s'ouvre sur une fenetre qui ne fait rien tant qu'on n'a
|
||||
* pas devine qu'il fallait aller creer un Data Asset dans le Content Browser.
|
||||
* Un outil qui demande une manip prealable pour donner signe de vie est un
|
||||
* outil qu'on croit casse.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
UIconStudioPreset* EnsurePreset();
|
||||
|
||||
/**
|
||||
* Relance le rendu et renvoie la texture a afficher.
|
||||
* Renvoie null s'il manque le preset ou le sujet -- pas une erreur, juste
|
||||
* une fenetre pas encore renseignee.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
UTexture2D* RefreshPreview();
|
||||
|
||||
/** Genere l'icone d'un seul objet : PNG, texture, et affectation du champ. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
bool GenerateForSubject(UObject* Subject);
|
||||
|
||||
/** Genere celle du sujet actuellement previsualise. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
bool GenerateCurrent();
|
||||
|
||||
/** Traite toute la liste de lot. Renvoie le nombre d'icones ecrites. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
int32 GenerateBatch();
|
||||
|
||||
/** Ajoute la selection du Content Browser au lot, sans doublon. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
int32 AddContentBrowserSelectionToBatch();
|
||||
|
||||
/** Prend le premier asset selectionne dans le Content Browser comme sujet. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
bool SetSubjectFromContentBrowser();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Icon Studio")
|
||||
void ClearBatch();
|
||||
|
||||
/**
|
||||
* Emis a chaque tentative de rendu, reussie ou non -- avec un pointeur nul
|
||||
* dans le second cas. La fenetre s'en sert aussi pour raccrocher son panneau
|
||||
* de preset, qui doit suivre meme quand il n'y a rien a afficher.
|
||||
*/
|
||||
FOnIconStudioPreviewUpdated OnPreviewUpdated;
|
||||
|
||||
/** Ligne de statut lisible, affichee sous les boutons. */
|
||||
FOnIconStudioStatus OnStatus;
|
||||
|
||||
private:
|
||||
/** Retrouve le mesh a rendre, que le sujet soit un mesh ou un DataAsset. */
|
||||
UStaticMesh* ResolveMesh(UObject* Subject, UMaterialInterface*& OutMaterialOverride) const;
|
||||
|
||||
/** Nom de l'asset produit, prefixe compris. */
|
||||
FString BuildAssetName(UObject* Subject, const UIconStudioPreset* Preset) const;
|
||||
|
||||
UTexture2D* CreateOrUpdateTexture(const FIconStudioImage& Image, const FString& PackagePath, const FString& AssetName, TArray<UPackage*>& OutTouchedPackages);
|
||||
bool AssignIcon(UObject* Subject, UTexture2D* Texture, TArray<UPackage*>& OutTouchedPackages) const;
|
||||
bool WritePng(const FIconStudioImage& Image, const FString& AssetName) const;
|
||||
|
||||
bool GenerateInternal(UObject* Subject, TArray<UPackage*>& OutTouchedPackages);
|
||||
|
||||
/** Ecrit un package sur le disque, sans condition. */
|
||||
static bool SavePackageToDisk(UPackage* Package);
|
||||
|
||||
/** Ecrit les packages touches par une generation, si le reglage l'autorise. */
|
||||
void SavePackages(const TArray<UPackage*>& Packages) const;
|
||||
|
||||
void HandleSessionChanged();
|
||||
void HandleObjectPropertyChanged(UObject* Object, struct FPropertyChangedEvent& Event);
|
||||
void Status(const FString& Message, bool bIsError = false);
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UIconStudioSession> Session;
|
||||
|
||||
TUniquePtr<FIconStudioRenderer> Renderer;
|
||||
|
||||
FDelegateHandle PropertyChangedHandle;
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
// Icon Studio - Mathew Simon
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "IconStudioTypes.generated.h"
|
||||
|
||||
/**
|
||||
* Ce que contient l'image produite.
|
||||
*
|
||||
* Les passes autres que Final servent au debug et a la retouche : recuperer la
|
||||
* base color seule permet de recolorer une icone dans un logiciel externe sans
|
||||
* refaire le rendu. Elles gardent le meme masque de transparence que la passe
|
||||
* finale -- c'est lui qui decoupe l'objet, pas la passe.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EIconStudioPass : uint8
|
||||
{
|
||||
/** Rendu complet, eclairage et post-process compris. C'est la passe des icones. */
|
||||
Final UMETA(DisplayName = "Final Color"),
|
||||
|
||||
/** Albedo brut, sans aucune lumiere. */
|
||||
BaseColor UMETA(DisplayName = "Base Color"),
|
||||
|
||||
/** Normales monde encodees en RGB. */
|
||||
Normal UMETA(DisplayName = "World Normal"),
|
||||
|
||||
/** Profondeur encodee sur RGB. Utilisable comme masque, pas comme image. */
|
||||
Depth UMETA(DisplayName = "Device Depth"),
|
||||
};
|
||||
|
||||
/** Ce qu'on trouve derriere l'objet. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EIconStudioBackground : uint8
|
||||
{
|
||||
/** Canal alpha decoupe sur l'objet. C'est ce qu'il faut pour l'inventaire. */
|
||||
Transparent UMETA(DisplayName = "Transparent"),
|
||||
|
||||
/** Aplat de couleur opaque, compose apres coup sur le meme masque. */
|
||||
SolidColor UMETA(DisplayName = "Solid Color"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Une des trois lumieres du studio.
|
||||
*
|
||||
* Les angles sont RELATIFS A LA CAMERA, jamais au monde. Sans ca, changer
|
||||
* l'angle de vue d'un objet deplacerait ses ombres et deux icones de la meme
|
||||
* serie n'auraient pas le meme eclairage -- exactement ce qu'on cherche a
|
||||
* eviter en travaillant avec des presets.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FIconStudioLight
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light")
|
||||
bool bEnabled = true;
|
||||
|
||||
/** Positif = la lumiere part vers la droite de la camera. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light", meta = (EditCondition = "bEnabled", ClampMin = "-180", ClampMax = "180", Units = "deg"))
|
||||
float Yaw = 0.f;
|
||||
|
||||
/** Negatif = la lumiere vient d'en haut. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light", meta = (EditCondition = "bEnabled", ClampMin = "-90", ClampMax = "90", Units = "deg"))
|
||||
float Pitch = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light", meta = (EditCondition = "bEnabled", ClampMin = "0", UIMax = "20"))
|
||||
float Intensity = 3.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light", meta = (EditCondition = "bEnabled"))
|
||||
FLinearColor Color = FLinearColor::White;
|
||||
|
||||
/**
|
||||
* Une ombre portee n'a rien a se poser dessus dans une scene vide, mais elle
|
||||
* fait toute la lisibilite des creux d'un objet un peu detaille.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Light", meta = (EditCondition = "bEnabled"))
|
||||
bool bCastShadows = true;
|
||||
|
||||
FIconStudioLight() = default;
|
||||
|
||||
FIconStudioLight(float InYaw, float InPitch, float InIntensity, bool bInShadows)
|
||||
: bEnabled(true), Yaw(InYaw), Pitch(InPitch), Intensity(InIntensity), bCastShadows(bInShadows)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Image produite par le studio : BGRA 8 bits, espace sRGB, alpha NON premultiplie.
|
||||
*
|
||||
* Non premultiplie parce que c'est ce que UMG attend d'une texture d'interface,
|
||||
* et ce que tout logiciel d'image comprend a l'ouverture du PNG.
|
||||
*/
|
||||
struct FIconStudioImage
|
||||
{
|
||||
int32 Size = 0;
|
||||
TArray<FColor> Pixels;
|
||||
|
||||
bool IsValid() const { return Size > 0 && Pixels.Num() == Size * Size; }
|
||||
};
|
||||
Reference in New Issue
Block a user