// Copyright Epic Games, Inc. All Rights Reserved. #include "STG_NodePreview.h" #include "CanvasItem.h" #include "CanvasTypes.h" #include "SImageViewport.h" #include "Texture2DPreview.h" #include "TextureResource.h" #include "TG_Node.h" #include "2D/Tex.h" #include "Device/FX/DeviceBuffer_FX.h" #include "EdGraph/TG_EdGraphNode.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SCheckBox.h" #include "Engine/Texture2D.h" #define LOCTEXT_NAMESPACE "STG_NodePreview" UE::ImageWidgets::IImageViewer::FImageInfo FNodeViewer::GetCurrentImageInfo() const { if (NodeTexture) { return {{}, {(int32)NodeTexture->GetSurfaceWidth(), (int32)NodeTexture->GetSurfaceHeight()}, GetNodeTextureNumMips(), true}; } return {{}, FIntPoint::ZeroValue, 0, false}; } void FNodeViewer::UpdateTexture() { if (!CurrentBlob) return; if (CurrentBlob->IsTiled()) { CurrentBlob->OnFinalise() .then([this]() { const TiledBlobPtr BlobTiled = std::static_pointer_cast(CurrentBlob); return BlobTiled->CombineTiles(false, false); }) .then([this]() { FIntPoint ImageSize = FIntPoint {(int32)CurrentBlob->GetWidth(), (int32)CurrentBlob->GetHeight()}; SetTexture(CurrentBlob); }); } else { CurrentBlob->OnFinalise().then([this]() { SetTexture(CurrentBlob); }); } } void FNodeViewer::DrawCurrentImage(FViewport*, FCanvas* Canvas, const FDrawProperties& Properties) { check(NodeTexture); if (NodeTexture) { FTextureResource* TextureResource = NodeTexture->GetResource(); if (TextureResource != nullptr) { TextureResource->bGreyScaleFormat = IsSingleChannel(); TextureResource->bSRGB = IsSRGB(); DrawTexture(TextureResource, Canvas, Properties.Placement, Properties.Mip); return; } } Util::OnGameThread([this]() { UpdateTexture(); }); } TOptional> FNodeViewer::GetCurrentImagePixelColor(FIntPoint PixelCoords, int32 MipIndex) const { const uint64 PixelIndex = PixelCoords.Y * NodeDescriptor.Width + PixelCoords.X; if (NodeTexture && CurrentBlob && CurrentBlob->IsValid()) { if (!CurrentBlob->GetBufferRef()->HasRaw()) { /// If it's already fetching raw, then we don't want to do it again if (CurrentBlob->GetBufferRef()->IsFetchingRaw()) return {}; CurrentBlob->GetBufferRef()->Raw(); } else { const FLinearColor LinearColor = CurrentBlob->GetBufferRef()->Raw_Now()->GetAsLinearColor(PixelIndex); if (NodeDescriptor.Format == BufferFormat::Byte) { return TVariant(TInPlaceType(), LinearColor.ToFColor(IsSRGB())); } return TVariant(TInPlaceType(), LinearColor); } } return {}; } UE::ImageWidgets::SImageViewport::FDrawSettings FNodeViewer::GetDrawSettings() const { return DrawSettings; } FText FNodeViewer::GetFormatLabelText() const { if (NodeTexture != nullptr) { return FText::Format(FTextFormat::FromString("{0}_{1} {2}"), FText::FromString(TextureHelper::GetChannelsTextFromItemsPerPoint(NodeDescriptor.ItemsPerPoint)), FText::FromString(BufferDescriptor::FormatToString(NodeDescriptor.Format)), FText::FromString(NodeDescriptor.bIsSRGB ? "(sRGB)" : "(Linear)")); } return LabelText; } bool FNodeViewer::IsSingleChannel() const { return NodeDescriptor.ItemsPerPoint == 1; } void FNodeViewer::SetTexture(const BlobPtr& InBlob, const FLinearColor InClearColor /*= FLinearColor(0.1, 0.1, 0.1, 1)*/) { NodeTexture = nullptr; CurrentBlob = InBlob; NodeDescriptor = {}; DrawSettings.ClearColor = InClearColor; if (InBlob) { const DeviceBufferPtr Buffer = InBlob->GetBufferRef().GetPtr(); check(Buffer); NodeTexture = GetTextureFromBuffer(Buffer); NodeDescriptor = Buffer->Descriptor(); check(NodeDescriptor.Width > 0 && NodeDescriptor.Height > 0); } } void FNodeViewer::SetLabelText(FText InText) { LabelText = InText; } void FNodeViewer::SetRGBA(bool bR, bool bG, bool bB, bool bA) { bRGBA[0] = bR; bRGBA[1] = bG; bRGBA[2] = bB; bRGBA[3] = bA; } void FNodeViewer::SetDrawSettings(const UE::ImageWidgets::SImageViewport::FDrawSettings& InDrawSettings) { DrawSettings = InDrawSettings; } void FNodeViewer::DrawTexture(const FTextureResource* TextureResource, FCanvas* Canvas, const FDrawProperties::FPlacement& Placement, const FDrawProperties::FMip& Mip) const { check(GMaxRHIFeatureLevel >= ERHIFeatureLevel::SM5); const bool bIsNormalMap = NodeTexture->IsNormalMap(); const bool bIsVirtualTexture = NodeTexture->IsCurrentlyVirtualTextured(); const TRefCountPtr BatchedElementParameters = new FBatchedElementTexture2DPreviewParameters( Mip.MipLevel, 0, 0, bIsNormalMap, false, false, bIsVirtualTexture, false, Placement.ZoomFactor >= 2.0/*Mip.bUsePointSampling*/); FCanvasTileItem Tile(Placement.Offset, TextureResource, Placement.Size, FLinearColor::White); Tile.BatchedElementParameters = BatchedElementParameters.GetReference(); Tile.BlendMode = GetBlendMode(); Canvas->DrawItem(Tile); } ESimpleElementBlendMode FNodeViewer::GetBlendMode() const { if (NodeTexture) { const TEnumAsByte& CompressionSettings = NodeTexture->CompressionSettings; if (CompressionSettings == TC_Grayscale || CompressionSettings == TC_Alpha) { return SE_BLEND_Opaque; } } int32 Result = SE_BLEND_RGBA_MASK_START; if (IsSingleChannel()) { Result += bRGBA[0] * 0b00111; } else { Result += bRGBA[0] * 0b00001; Result += bRGBA[1] * 0b00010; Result += bRGBA[2] * 0b00100; Result += bRGBA[3] * 0b01000; } return static_cast(Result); } UTexture* FNodeViewer::GetTextureFromBuffer(const DeviceBufferPtr& Buffer) const { UTexture* OutTexture = nullptr; if (const std::shared_ptr FXBuffer = std::static_pointer_cast(Buffer)) { if (!FXBuffer->IsNull()) { const TexPtr Tex = FXBuffer->GetTexture(); check(Tex); UTexture* BufferTexture = Tex->GetTexture(); if (UTextureRenderTarget2D* RenderTarget = Cast(BufferTexture)) { OutTexture = RenderTarget; } else if (UTexture2D* Texture2D = Cast(BufferTexture)) { OutTexture = Texture2D; } else { UE_LOG(LogTemp, Warning, TEXT("Texture is not a UTexture2D | UTextureRenderTarget2D.")); } } else { UE_LOG(LogTemp, Warning, TEXT("Buffer is not an FXBuffer.")); } } else { UE_LOG(LogTemp, Warning, TEXT("BlobTexture failed to find the buffer.")); } return OutTexture; } int32 FNodeViewer::GetNodeTextureNumMips() const { //Enable this when we can switch mip levels #if 0 if (UTexture2D* Texture2D = Cast(NodeTexture)) { return Texture2D->GetNumMips(); } else if(UTextureRenderTarget2D* RenderTarget = Cast(NodeTexture)) { return RenderTarget->GetNumMips(); } #endif return 0; } bool FNodeViewer::IsSRGB() const { return NodeDescriptor.bIsSRGB; } class FPreviewViewerCommands : public TCommands { public: FPreviewViewerCommands() : TCommands(TEXT("NodePreview"), LOCTEXT("ContextDescription", "Node Preview"), NAME_None, FAppStyle::Get().GetStyleSetName()) { } virtual void RegisterCommands() override { UI_COMMAND(ToggleLock, "Toggle Node Preview Lock", "Toggles the node preview lock.", EUserInterfaceActionType::ToggleButton, FInputChord(EKeys::L)); } TSharedPtr ToggleLock; }; STG_NodePreviewWidget::~STG_NodePreviewWidget() { FPreviewViewerCommands::Unregister(); } UE::ImageWidgets::SImageViewport::FDrawSettings STG_NodePreviewWidget::GetDrawSettings() const { return NodeViewer->GetDrawSettings(); } void STG_NodePreviewWidget::Construct(const FArguments& InArgs) { NodeViewer = MakeShared(); NodeViewer->SetDrawSettings(UE::ImageWidgets::SImageViewport::FDrawSettings{ .ClearColor = FVector3f(0.1f), .bBorderEnabled = false, .bBackgroundColorEnabled = true, .BackgroundColor = FLinearColor::Black, .bBackgroundCheckerEnabled = true}); OnNodeBlobChanged = InArgs._OnNodeBlobChanged; FPreviewViewerCommands::Register(); const FPreviewViewerCommands Commands = FPreviewViewerCommands::Get(); CommandList = MakeShared(); CommandList->MapAction(Commands.ToggleLock, FExecuteAction::CreateSP(this, &STG_NodePreviewWidget::ToggleLock), FCanExecuteAction::CreateLambda([this] { return NodeViewer->GetCurrentImageInfo().bIsValid; }), FIsActionChecked::CreateLambda([this] { return LockedNode != nullptr; })); const TSharedPtr ToolbarExtender = MakeShared(); ToolbarExtender->AddToolBarExtension("ToolbarLeft", EExtensionHook::Before, CommandList, FToolBarExtensionDelegate::CreateSP(this, &STG_NodePreviewWidget::AddLockButton)); ToolbarExtender->AddToolBarExtension("ToolbarRight", EExtensionHook::After, CommandList, FToolBarExtensionDelegate::CreateSP(this, &STG_NodePreviewWidget::AddRGBAButtons)); const TSharedPtr StatusBarExtender = MakeShared< UE::ImageWidgets::SImageViewport::FStatusBarExtender>(); StatusBarExtender->AddExtension("StatusBarLeft", EExtensionHook::After, CommandList, UE::ImageWidgets::SImageViewport::FStatusBarExtender::FDelegate::CreateSP(this, &STG_NodePreviewWidget::AddFormatLabel)); ChildSlot .VAlign(VAlign_Fill) .HAlign(HAlign_Fill) [ SAssignNew(Viewport, UE::ImageWidgets::SImageViewport, NodeViewer.ToSharedRef()) .ToolbarExtender(ToolbarExtender) .StatusBarExtender(StatusBarExtender) .DrawSettings(this,&STG_NodePreviewWidget::GetDrawSettings) .ControllerSettings(UE::ImageWidgets::SImageViewport::FControllerSettings{ .DefaultZoomMode = UE::ImageWidgets::SImageViewport::FControllerSettings::EDefaultZoomMode::Fill }) ]; } void STG_NodePreviewWidget::SelectionChanged(UTG_Node* Node) { const bool bUpdatePreview = LockedNode == nullptr && SelectedNode != Node; SelectedNode = Node; if (bUpdatePreview) { Update(); } } void STG_NodePreviewWidget::NodeDeleted(const UTG_Node* Node) { const bool bPreviewNodeDeleted = LockedNode == Node || (LockedNode == nullptr && SelectedNode == Node); if (LockedNode == Node) { LockedNode = nullptr; } if (SelectedNode == Node) { SelectedNode = nullptr; } if (bPreviewNodeDeleted) { Update(); } } void STG_NodePreviewWidget::Update() const { FTG_Variant Variant; BlobPtr Blob; bool bValidVariant = GetOutputVariantFromNode(Variant); if (Variant) { FIntPoint ImageSize = FIntPoint::ZeroValue; if (Variant.IsTexture() && Variant.GetTexture() && Variant.GetTexture().RasterBlob) { Blob = Variant.GetTexture().RasterBlob; ImageSize = FIntPoint {(int32)Blob->GetWidth(), (int32)Blob->GetHeight()}; if (Blob->IsTiled()) { Blob->OnFinalise() .then([Blob]() { const TiledBlobPtr BlobTiled = std::static_pointer_cast(Blob); return BlobTiled->CombineTiles(false, false); }) .then([this, Blob]() { FIntPoint ImageSize = FIntPoint {(int32)Blob->GetWidth(), (int32)Blob->GetHeight()}; NodeViewer->SetTexture(Blob); Viewport->ResetZoom(ImageSize); }); } else { Blob->OnFinalise().then([this, Blob]() { NodeViewer->SetTexture(Blob); }); } } else if(Variant.IsColor()) { NodeViewer->SetTexture(nullptr, Variant.GetColor()); } else { NodeViewer->SetTexture(nullptr, FLinearColor::Black); } NodeViewer->SetLabelText(GetLabelText(Variant, bValidVariant)); Viewport->ResetZoom(ImageSize); [[maybe_unused]] bool Result = OnNodeBlobChanged.ExecuteIfBound(Blob); return; } /// If we get to here then we need to set it to null NodeViewer->SetTexture(nullptr); // Update preview blob and trigger related external updates. [[maybe_unused]] bool Result = OnNodeBlobChanged.ExecuteIfBound(Blob); } bool STG_NodePreviewWidget::GetOutputVariantFromNode(FTG_Variant& OutVariant) const { const UTG_Node* PreviewNode = LockedNode ? LockedNode : SelectedNode; // Get the Variant if preview node is valid or assign a nullptr if not. TArray OutVariants; TArray* OutNames = nullptr; if(PreviewNode) { PreviewNode->GetAllOutputValues(OutVariants, OutNames); if (!OutVariants.IsEmpty()) { OutVariant = OutVariants[0]; return true; } } return false; } FText STG_NodePreviewWidget::GetLabelText(FTG_Variant& InVariant, bool bValidVariant) const { const UTG_Node* PreviewNode = LockedNode ? LockedNode : SelectedNode; const bool bInValidTexture = InVariant.IsTexture() && InVariant.GetTexture() && !InVariant.GetTexture()->IsValid(); const FText PreviewNotAvailable = FText::FromString("Node preview is not available"); if (!PreviewNode) { return FText::FromString("Select a node to preview"); } else if (!bValidVariant) { return PreviewNotAvailable; } else if (InVariant.IsColor()) { return FText::FromString("Color " + InVariant.GetColor().ToFColor(false).ToString()); } else if (InVariant.IsVector()) { return FText::FromString("Vector (" + InVariant.GetVector().ToString() + ")"); } else if (InVariant.IsScalar()) { return FText::FromString(FString::Printf(TEXT("Scalar (%0.3f)"), InVariant.GetScalar())); } else if (bInValidTexture) { return FText::FromString("Texture is not valid"); } return PreviewNotAvailable; } FReply STG_NodePreviewWidget::OnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) { if (CommandList.IsValid() && CommandList->ProcessCommandBindings(InKeyEvent)) { return FReply::Handled(); } return FReply::Unhandled(); } void STG_NodePreviewWidget::AddFormatLabel(SHorizontalBox& HorizontalBox) { HorizontalBox.AddSlot() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text_Raw(NodeViewer.Get(), &FNodeViewer::GetFormatLabelText) ]; } void STG_NodePreviewWidget::AddLockButton(FToolBarBuilder& ToolbarBuilder) const { auto GetLockButtonImage = [this] { return FSlateIcon(FAppStyle::Get().GetStyleSetName(), LockedNode ? "PropertyWindow.Locked" : "PropertyWindow.Unlocked"); }; ToolbarBuilder.AddToolBarButton(FPreviewViewerCommands::Get().ToggleLock, NAME_None, TAttribute(), TAttribute(), TAttribute::CreateLambda(GetLockButtonImage)); ToolbarBuilder.AddSeparator(); } void STG_NodePreviewWidget::AddRGBAButtons(FToolBarBuilder& ToolbarBuilder) { ToolbarBuilder.AddSeparator(); auto GetTextButton = [this](const FString& Label, const FCheckBoxStyle* ButtonStyle, bool& bChecked) { return SNew(SCheckBox) .Style(ButtonStyle) .IsEnabled_Lambda([this, &bChecked] { return NodeViewer->GetCurrentImageInfo().bIsValid && (&bChecked == &bRGBA[0] || !NodeViewer->IsSingleChannel()); }) .IsChecked(bChecked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked) .OnCheckStateChanged_Lambda([&bChecked, this](const ECheckBoxState State) { bChecked = State == ECheckBoxState::Checked; NodeViewer->SetRGBA(bRGBA[0], bRGBA[1], bRGBA[2], bRGBA[3]); }) [ SNew(STextBlock) .Font(FAppStyle::GetFontStyle("EditorViewportToolBar.Font")) .Text(FText::FromString(Label)) ]; }; const FCheckBoxStyle* ButtonStyleStart = &FAppStyle::Get().GetWidgetStyle("EditorViewportToolBar.ToggleButton.Start"); const FCheckBoxStyle* ButtonStyleMiddle = &FAppStyle::Get().GetWidgetStyle("EditorViewportToolBar.ToggleButton.Middle"); const FCheckBoxStyle* ButtonStyleEnd = &FAppStyle::Get().GetWidgetStyle("EditorViewportToolBar.ToggleButton.End"); const TSharedRef RGBA = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ GetTextButton("R", ButtonStyleStart, bRGBA[0]) ] + SHorizontalBox::Slot() .AutoWidth() [ GetTextButton("G", ButtonStyleMiddle, bRGBA[1]) ] + SHorizontalBox::Slot() .AutoWidth() [ GetTextButton("B", ButtonStyleMiddle, bRGBA[2]) ] + SHorizontalBox::Slot() .AutoWidth() [ GetTextButton("A", ButtonStyleEnd, bRGBA[3]) ]; ToolbarBuilder.AddToolBarWidget(RGBA); } void STG_NodePreviewWidget::ToggleLock() { if (LockedNode) { const bool bUpdatePreview = SelectedNode != LockedNode; LockedNode = nullptr; if (bUpdatePreview) { Update(); } } else { LockedNode = SelectedNode; } } #undef LOCTEXT_NAMESPACE