diff --git a/Maya/BatchPlacer.py b/Maya/BatchPlacer.py new file mode 100644 index 0000000..d1e3278 --- /dev/null +++ b/Maya/BatchPlacer.py @@ -0,0 +1,240 @@ +from PySide import QtGui, QtCore +import pysideuic +import xml.etree.ElementTree as xml +from cStringIO import StringIO +from pymel.core import * +import pymel.core as pm +from pymel import * +import maya.OpenMayaUI as apiUI +import maya.OpenMaya as om +import maya.cmds as cmds +from itertools import izip +import math + +def pairwise(iterable): + a = iter(iterable) + return izip(a, a) + +# Helper for loading Qt ui files from nathanhorne.com/?p=451 +def loadUiType(uiFile): + """ + Pyside lacks the "loadUiType" command, so we have to convert the ui file to py code in-memory first + and then execute it in a special frame to retrieve the form_class. + """ + parsed = xml.parse(uiFile) + widget_class = parsed.find('widget').get('class') + form_class = parsed.find('class').text + + with open(uiFile, 'r') as f: + o = StringIO() + frame = {} + + pysideuic.compileUi(f, o, indent=0) + pyc = compile(o.getvalue(), '', 'exec') + exec pyc in frame + + #Fetch the base_class and form class based on their type in the xml from designer + form_class = frame['Ui_%s'%form_class] + base_class = eval('QtGui.%s'%widget_class) + return form_class, base_class + +# UE4 Static Mesh strings +ue4_static_header = "Begin Map\r\n Begin Level\r\n" +ue4_static_footer = " End Level\r\nBegin Surface\r\nEnd Map\r\n" +ue4_static_template_body = " Begin Actor Class=StaticMeshActor Name=$MAYANAME$_1 Archetype=StaticMeshActor'/Script/Engine.Default__StaticMeshActor'\r\n Begin Object Class=StaticMeshComponent Name=\"StaticMeshComponent0\" Archetype=StaticMeshComponent'Default__StaticMeshActor:StaticMeshComponent0'\r\n End Object\r\n Begin Object Name=\"StaticMeshComponent0\"\r\n StaticMesh=$UNREALNAME$\r\n RelativeLocation=(X=$LOC_X$,Y=$LOC_Y$,Z=$LOC_Z$)\r\n RelativeScale3D=(X=$SCALE_X$,Y=$SCALE_Y$,Z=$SCALE_Z$)\r\n BodyInstance=(Scale3D=(X=$SCALE_X$,Y=$SCALE_Y$,Z=$SCALE_Z$))\r\n RelativeRotation=(Pitch=$ROT_Y$,Yaw=$ROT_Z$,Roll=$ROT_X$)\r\n End Object\r\n StaticMeshComponent=StaticMeshComponent0\r\n RootComponent=StaticMeshComponent0\r\n ActorLabel=\"$MAYANAME$\"\r\n End Actor\r\n" + +def GetTemplatedExportBody(MayaName, UnrealName, LocX, LocY, LocZ, RotX, RotY, RotZ, ScaleX, ScaleY, ScaleZ): + body = ue4_static_template_body.replace("$MAYANAME$", MayaName) + body = body.replace("$UNREALNAME$", UnrealName) + body = body.replace("$LOC_X$", str(LocX)) + body = body.replace("$LOC_Y$", str(LocY)) + body = body.replace("$LOC_Z$", str(LocZ)) + body = body.replace("$ROT_X$", str(RotX)) + body = body.replace("$ROT_Y$", str(RotY)) + body = body.replace("$ROT_Z$", str(RotZ)) + body = body.replace("$SCALE_X$", str(ScaleX)) + body = body.replace("$SCALE_Y$", str(ScaleY)) + body = body.replace("$SCALE_Z$", str(ScaleZ)) + return body + +# Paths to Files +homedir = os.environ['UE4_PRODUCTIVITY'].replace("\\","/") + "/" +usersettings = homedir + "Maya/batchplacer.config" +main_ui_filename = homedir + "Maya/batchplacer.ui" +main_form_class, main_base_class = loadUiType(main_ui_filename) +slot_ui_filename = homedir + "Maya/batchplacer_slotrow.ui" +slot_form_class, slot_base_class = loadUiType(slot_ui_filename) + +# Slot Class +class BatchPlacerExportSlotUI(slot_form_class, slot_base_class): + def __init__(self, mainui, parent=None): + super(BatchPlacerExportSlotUI, self).__init__(parent) + self.setupUi(self) + self.connectInterface() + self.parentui = mainui + self.setLayout(self.horizontalLayout) + + def connectInterface(self): + QtCore.QObject.connect(self.btnRemoveSlot, QtCore.SIGNAL("clicked()"), self.removeFromParent) + QtCore.QObject.connect(self.btnMoveUpSlot, QtCore.SIGNAL("clicked()"), self.moveUpInParent) + QtCore.QObject.connect(self.btnMoveDownSlot, QtCore.SIGNAL("clicked()"), self.moveDownInParent) + QtCore.QObject.connect(self.lineObjName, QtCore.SIGNAL("textEdited(QString)"), self.saveAllSlots) + QtCore.QObject.connect(self.lineUnrealName, QtCore.SIGNAL("textEdited(QString)"), self.saveAllSlots) + + def removeFromParent(self): + for i in xrange(self.parentui.listWidget.count()): + slotWidget = self.parentui.listWidget.itemWidget(self.parentui.listWidget.item(i)) + if (self == slotWidget): + self.parentui.listWidget.takeItem(i) + self.parentui.saveAllSlots() + return + + def moveUpInParent(self): + self.parentui.moveUpSlot(self) + + def moveDownInParent(self): + self.parentui.moveDownSlot(self) + + def saveAllSlots(self, newtext): + self.parentui.saveAllSlots() + + +# Interface Class +class BatchPlacerUI(main_form_class, main_base_class): + def __init__(self, parent=None): + super(BatchPlacerUI, self).__init__(parent) + self.setupUi(self) + self.setObjectName('UE4BatchPlacer') + self.connectInterface() + + lines = [line.strip() for line in open(usersettings)] + + for x, y in pairwise(lines): + item_widget = self.addExportSlot() + item_widget.lineObjName.setText(x) + item_widget.lineUnrealName.setText(y) + + def connectInterface(self): + QtCore.QObject.connect(self.btnExportSelected, QtCore.SIGNAL("clicked()"), self.exportSelected) + QtCore.QObject.connect(self.btnAddExportSlot, QtCore.SIGNAL("clicked()"), self.addExportSlot) + + def getUnrealMesh(self, inputName): + if inputName is None: + return None; + for i in xrange(self.listWidget.count()): + slotWidget = self.listWidget.itemWidget(self.listWidget.item(i)) + if slotWidget.lineObjName.text() not in inputName: continue + return slotWidget.lineUnrealName.text() + + def moveUpSlot(self, slotitem): + taken = None + foundId = 0 + mayaName = None + unrealName = None + for i in xrange(self.listWidget.count()): + slotWidget = self.listWidget.itemWidget(self.listWidget.item(i)) + if (slotitem == slotWidget): + if (i is not 0): + mayaName = slotitem.lineObjName.text() + unrealName = slotitem.lineUnrealName.text() + taken = self.listWidget.takeItem(i) + foundId = i + break + if (taken is not None): + self.listWidget.insertItem(foundId-1,taken) + item_widget = BatchPlacerExportSlotUI(self, self) + item_widget.lineObjName.setText(mayaName) + item_widget.lineUnrealName.setText(unrealName) + taken.setSizeHint(item_widget.sizeHint()) + self.listWidget.setItemWidget(taken, item_widget) + self.saveAllSlots() + + def moveDownSlot(self, slotitem): + taken = None + foundId = 0 + mayaName = None + unrealName = None + for i in xrange(self.listWidget.count()): + slotWidget = self.listWidget.itemWidget(self.listWidget.item(i)) + if (slotitem == slotWidget): + if (i is not self.listWidget.count()-1): + mayaName = slotitem.lineObjName.text() + unrealName = slotitem.lineUnrealName.text() + taken = self.listWidget.takeItem(i) + foundId = i + break + if (taken is not None): + self.listWidget.insertItem(foundId+1,taken) + item_widget = BatchPlacerExportSlotUI(self, self) + item_widget.lineObjName.setText(mayaName) + item_widget.lineUnrealName.setText(unrealName) + taken.setSizeHint(item_widget.sizeHint()) + self.listWidget.setItemWidget(taken, item_widget) + self.saveAllSlots() + + def addExportSlot(self): + item = QtGui.QListWidgetItem(self.listWidget) + item_widget = BatchPlacerExportSlotUI(self, self) + item_widget.setLayout(item_widget.horizontalLayout) + item.setSizeHint(item_widget.sizeHint()) + self.listWidget.addItem(item) + self.listWidget.setItemWidget(item, item_widget) + return item_widget + + def exportSelected(self): + exportable = False + export_body = "" + selection = om.MSelectionList() + om.MGlobal.getActiveSelectionList(selection) + selection_iter = om.MItSelectionList(selection) + while not selection_iter.isDone(): + obj = om.MObject() + dagPath = om.MDagPath() + selection_iter.getDependNode(obj) + selection_iter.getDagPath(dagPath) + node = om.MFnDependencyNode(obj) + unrealName = self.getUnrealMesh(node.name()) + if (unrealName is not None): + exportable = True + mt = om.MTransformationMatrix(dagPath.inclusiveMatrix()) + loc = mt.translation(om.MSpace.kWorld) + rot = mt.rotation().asEulerRotation() + scaleUtil = om.MScriptUtil() + scaleUtil.createFromList([0,0,0],3) + scaleVec = scaleUtil.asDoublePtr() + mt.getScale(scaleVec, om.MSpace.kWorld) + scale = [om.MScriptUtil.getDoubleArrayItem(scaleVec, i) for i in range(0,3)] + if (cmds.upAxis(q=True,axis=True) == "y"): + export_body += GetTemplatedExportBody(node.name(), unrealName, loc.x, loc.z, loc.y, math.degrees(rot.x), math.degrees(rot.z), math.degrees(rot.y), scale[0], scale[2], scale[1]) + else: + export_body += GetTemplatedExportBody(node.name(), unrealName, loc.x, loc.y, loc.z, math.degrees(rot.x), math.degrees(rot.y), math.degrees(rot.z), scale[0], scale[1], scale[2]) + selection_iter.next() + if (exportable is True): + export = ue4_static_header + export_body + ue4_static_footer + clipboard = QtGui.QApplication.clipboard() + clipboard.setText(export) + + + def saveAllSlots(self): + f = open(usersettings, 'w') + for i in xrange(self.listWidget.count()): + slotWidget = self.listWidget.itemWidget(self.listWidget.item(i)) + f.write(slotWidget.lineObjName.text() + '\n') + f.write(slotWidget.lineUnrealName.text() + '\n') + f.close() + +# main +def main(): + global ui + ui = BatchPlacerUI() + ui.show() + +def show(): + global ui + if (ui != None): + ui.show() + else: + main() + +if __name__ == "__main__": + main() diff --git a/Maya/LoadProductivityTools.py b/Maya/LoadProductivityTools.py new file mode 100644 index 0000000..31f606a --- /dev/null +++ b/Maya/LoadProductivityTools.py @@ -0,0 +1,9 @@ +import sys +try: BatchPlacer.show() +except: + Dir = 'D:/allarjealdepot/Engine/Extras/ProductivityPlugin/Maya' + if Dir not in sys.path: + sys.path.append(Dir) + try: reload(BatchPlacer) + except: import BatchPlacer + BatchPlacer.main() \ No newline at end of file diff --git a/Maya/batchplacer.ui b/Maya/batchplacer.ui new file mode 100644 index 0000000..617b961 --- /dev/null +++ b/Maya/batchplacer.ui @@ -0,0 +1,185 @@ + + + Dialog + + + Qt::NonModal + + + + 0 + 0 + 580 + 478 + + + + + 0 + 0 + + + + UE4 Batch Placer + + + true + + + + + + QLayout::SetNoConstraint + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignCenter + + + 6 + + + 5 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + <html><head/><body><p align="center"><span style=" font-size:14pt;">UE4 Batch Placer For Maya</span></p><p align="center"><span style=" font-size:10pt;">By Michael Allar</span></p></body></html> + + + + + + + + + + 0 + 0 + + + + Add Export Slot + + + + + + + + + + 0 + 1 + + + + Qt::WheelFocus + + + true + + + QAbstractItemView::NoSelection + + + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + Export Selected To Clipboard For Pasting Into Unreal Engine 4 + + + + + + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Slot Priority + + + Qt::AlignCenter + + + + + + + + 1 + 0 + + + + Maya Object Name + + + Qt::AlignCenter + + + + + + + + 2 + 0 + + + + Unreal Mesh Path + + + Qt::AlignCenter + + + + + + + + + + + + diff --git a/Maya/batchplacer_slotrow.ui b/Maya/batchplacer_slotrow.ui new file mode 100644 index 0000000..67668dc --- /dev/null +++ b/Maya/batchplacer_slotrow.ui @@ -0,0 +1,135 @@ + + + batchplacer_slotrow + + + + 0 + 0 + 385 + 31 + + + + + 0 + 0 + + + + Form + + + + + 0 + 0 + 381 + 31 + + + + + + + + 0 + 0 + + + + + 32 + 1000000 + + + + - + + + + + + + + 0 + 0 + + + + + 32 + 10000 + + + + ^ + + + + + + + + 0 + 0 + + + + + 32 + 10000 + + + + v + + + false + + + + + + + + 1 + 0 + + + + Qt::StrongFocus + + + <html><head/><body><p>Maya Object Name Slot</p><p>Enter the string to search all maya objects for to replace with the unreal mesh indicated in the box to the right.</p></body></html> + + + <html><head/><body><p>Maya Object Name Slot</p><p>Enter the string to search all maya objects for to replace with the unreal mesh indicated in the box to the right.</p></body></html> + + + + + + + + + + + + + + 2 + 0 + + + + Qt::StrongFocus + + + + + + + + + diff --git a/ProductivityPlugin.uplugin b/ProductivityPlugin.uplugin new file mode 100644 index 0000000..230855b --- /dev/null +++ b/ProductivityPlugin.uplugin @@ -0,0 +1,22 @@ +{ + "FileVersion" : 3, + + "FriendlyName" : "Productivity Plugin", + "Version" : 1, + "VersionName" : "1.0", + "CreatedBy" : "Michael Allar", + "CreatedByURL" : "http://michaelallar.com", + "EngineVersion" : "4.7.0", + "Description" : "Some tools that don't come with UE4", + "Category" : "Tools", + "EnabledByDefault" : true, + + "Modules" : + [ + { + "Name" : "ProductivityPlugin", + "Type" : "Developer", + "LoadingPhase": "PostEngineInit" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 8f410d8..41db9d2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ # ProductivityPlugin Some tools and scripts that extend the functionality of UE4 + +This repo is so unstable and nothing here is guaranteed to work, or to be documented. + +# What's Included? +## UE4 Plugin +1. Static Mesh Actor to Instanced Mesh Collection and vice versa + +## 3D Package Scripts +1. [Batch Placer (Maya 2015 Only For Now)](https://www.youtube.com/watch?v=TcbSW4icYV4) + 1. Install by running Setup 3D Package Scripts.bat + 2. Shove this code into a Maya shelf button + +``` +import sys +try: + BatchPlacer.show() + BatchPlacer.activateWindow() + BatchPlacer.raise_() +except: + Dir = os.environ['UE4_PRODUCTIVITY'].replace("\\","/") + "/Maya" + if Dir not in sys.path: + sys.path.append(Dir) + try: reload(BatchPlacer) + except: import BatchPlacer + BatchPlacer.main() +``` \ No newline at end of file diff --git a/Resources/ButtonIcon.psd b/Resources/ButtonIcon.psd new file mode 100644 index 0000000..3ccfa59 Binary files /dev/null and b/Resources/ButtonIcon.psd differ diff --git a/Resources/DarkGrayBackground.png b/Resources/DarkGrayBackground.png new file mode 100644 index 0000000..8ebc63f Binary files /dev/null and b/Resources/DarkGrayBackground.png differ diff --git a/Resources/DefaultIcon128.png b/Resources/DefaultIcon128.png new file mode 100644 index 0000000..3033ae0 Binary files /dev/null and b/Resources/DefaultIcon128.png differ diff --git a/Resources/Icon128.png b/Resources/Icon128.png new file mode 100644 index 0000000..f0aab7c Binary files /dev/null and b/Resources/Icon128.png differ diff --git a/Resources/InstancedToStatic.png b/Resources/InstancedToStatic.png new file mode 100644 index 0000000..8d7a544 Binary files /dev/null and b/Resources/InstancedToStatic.png differ diff --git a/Resources/StaticToInstanced.png b/Resources/StaticToInstanced.png new file mode 100644 index 0000000..5d5ee42 Binary files /dev/null and b/Resources/StaticToInstanced.png differ diff --git a/Resources/TabwindowTestPlugin.PNG b/Resources/TabwindowTestPlugin.PNG new file mode 100644 index 0000000..b117497 Binary files /dev/null and b/Resources/TabwindowTestPlugin.PNG differ diff --git a/Resources/basicpluginSource.PNG b/Resources/basicpluginSource.PNG new file mode 100644 index 0000000..6c06f9c Binary files /dev/null and b/Resources/basicpluginSource.PNG differ diff --git a/Resources/blankpluginSource.PNG b/Resources/blankpluginSource.PNG new file mode 100644 index 0000000..e4ef67c Binary files /dev/null and b/Resources/blankpluginSource.PNG differ diff --git a/Resources/pluginEditorTestAutoPlugin.PNG b/Resources/pluginEditorTestAutoPlugin.PNG new file mode 100644 index 0000000..bb6278a Binary files /dev/null and b/Resources/pluginEditorTestAutoPlugin.PNG differ diff --git a/Resources/testAutoPluginToolbar.PNG b/Resources/testAutoPluginToolbar.PNG new file mode 100644 index 0000000..3bd12b8 Binary files /dev/null and b/Resources/testAutoPluginToolbar.PNG differ diff --git a/Resources/windowMenuTestAutoPlugin.PNG b/Resources/windowMenuTestAutoPlugin.PNG new file mode 100644 index 0000000..cc8387d Binary files /dev/null and b/Resources/windowMenuTestAutoPlugin.PNG differ diff --git a/Setup 3D Package Scripts.bat b/Setup 3D Package Scripts.bat new file mode 100644 index 0000000..baa4ce8 --- /dev/null +++ b/Setup 3D Package Scripts.bat @@ -0,0 +1,2 @@ +@echo off +setx UE4_PRODUCTIVITY "%CD%" \ No newline at end of file diff --git a/Source/ProductivityPlugin/Classes/InstancedMeshWrapper.h b/Source/ProductivityPlugin/Classes/InstancedMeshWrapper.h new file mode 100644 index 0000000..c088d2c --- /dev/null +++ b/Source/ProductivityPlugin/Classes/InstancedMeshWrapper.h @@ -0,0 +1,18 @@ +#pragma once + +#include "GameFramework/Actor.h" +#include "InstancedMeshWrapper.generated.h" + +/** +* +*/ +UCLASS() +class PRODUCTIVITYPLUGIN_API AInstancedMeshWrapper : public AActor +{ + GENERATED_UCLASS_BODY() + +public: + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Meshes") + UInstancedStaticMeshComponent* InstancedMeshes; +}; diff --git a/Source/ProductivityPlugin/Classes/ProductivityPluginCommands.h b/Source/ProductivityPlugin/Classes/ProductivityPluginCommands.h new file mode 100644 index 0000000..de685b9 --- /dev/null +++ b/Source/ProductivityPlugin/Classes/ProductivityPluginCommands.h @@ -0,0 +1,31 @@ +// Some copyright should be here... +#pragma once + +#include "SlateBasics.h" +#include "ProductivityPluginStyle.h" + +class FProductivityPluginCommands : public TCommands +{ +public: + + FProductivityPluginCommands(); + + static void BindGlobalStaticToInstancedActions(); + static TSharedRef< SWidget > GenerateStaticToInstancedMenuContent(TSharedRef InCommandList); + +#if WITH_EDITOR + virtual void RegisterCommands() override; +#endif + + TSharedPtr< FUICommandInfo > StaticToInstanced; + TSharedPtr< FUICommandInfo > StaticToInstancedIsResultGrouped; + + PRODUCTIVITYPLUGIN_API static TSharedPtr GlobalStaticToInstancedActions; +}; + +class PRODUCTIVITYPLUGIN_API FProductivityPluginCommandCallbacks +{ +public: + static void OnToggleStaticToInstancedResultGrouped(); + static bool OnToggleStaticToInstancedResultGroupedEnabled(); +}; \ No newline at end of file diff --git a/Source/ProductivityPlugin/Classes/ProductivityPluginEditorSettings.h b/Source/ProductivityPlugin/Classes/ProductivityPluginEditorSettings.h new file mode 100644 index 0000000..028fc9d --- /dev/null +++ b/Source/ProductivityPlugin/Classes/ProductivityPluginEditorSettings.h @@ -0,0 +1,21 @@ +#pragma once + +#include "ProductivityPluginEditorSettings.generated.h" + +/** +* Implements the Editor's play settings. +*/ +UCLASS(config = EditorUserSettings) +class PRODUCTIVITYPLUGIN_API UProductivityPluginEditorSettings : public UObject +{ + GENERATED_UCLASS_BODY() + +public: + + UPROPERTY(config, EditAnywhere, Category = ProductivityOptions) + bool GroupStaticToInstancedResults; + + void SetGroupStaticToInstancedResults(const bool InGroupStaticToInstancedResults) { GroupStaticToInstancedResults = InGroupStaticToInstancedResults; SaveConfig(); } + bool GetGroupStaticToInstancedResults() const { return GroupStaticToInstancedResults; } + +}; \ No newline at end of file diff --git a/Source/ProductivityPlugin/Classes/ProductivityPluginModule.h b/Source/ProductivityPlugin/Classes/ProductivityPluginModule.h new file mode 100644 index 0000000..f009832 --- /dev/null +++ b/Source/ProductivityPlugin/Classes/ProductivityPluginModule.h @@ -0,0 +1,24 @@ +// Some copyright should be here... + +#pragma once + +#include "ModuleManager.h" + +class FProductivityPluginModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + /** This function will be bound to Command.*/ + void StaticToInstancedClicked(); + +private: + + void AddToolbarExtension(class FToolBarBuilder &); + void AddMenuExtension(class FMenuBuilder &); + + TSharedPtr PluginCommands; +}; \ No newline at end of file diff --git a/Source/ProductivityPlugin/Classes/ProductivityPluginStyle.h b/Source/ProductivityPlugin/Classes/ProductivityPluginStyle.h new file mode 100644 index 0000000..0875801 --- /dev/null +++ b/Source/ProductivityPlugin/Classes/ProductivityPluginStyle.h @@ -0,0 +1,30 @@ +// Some copyright should be here... +#pragma once + +#include "SlateBasics.h" + +/** */ +class FProductivityPluginStyle +{ +public: + + static void Initialize(); + + static void Shutdown(); + + /** reloads textures used by slate renderer */ + static void ReloadTextures(); + + /** @return The Slate style set for the Shooter game */ + static const ISlateStyle& Get(); + + static FName GetStyleSetName(); + +private: + + static TSharedRef< class FSlateStyleSet > Create(); + +private: + + static TSharedPtr< class FSlateStyleSet > StyleInstance; +}; \ No newline at end of file diff --git a/Source/ProductivityPlugin/Classes/ProductivityTypes.h b/Source/ProductivityPlugin/Classes/ProductivityTypes.h new file mode 100644 index 0000000..cb79912 --- /dev/null +++ b/Source/ProductivityPlugin/Classes/ProductivityTypes.h @@ -0,0 +1,20 @@ +#pragma once + +#include "ProductivityTypes.generated.h" + +USTRUCT() +struct FMeshInfo +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Rendering) + UStaticMesh* StaticMesh; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Rendering) + TArray Materials; + + bool operator==(const FMeshInfo& rightInfo) const + { + return StaticMesh == rightInfo.StaticMesh && Materials == rightInfo.Materials; + } +}; \ No newline at end of file diff --git a/Source/ProductivityPlugin/Private/InstancedMeshWrapper.cpp b/Source/ProductivityPlugin/Private/InstancedMeshWrapper.cpp new file mode 100644 index 0000000..52a71b4 --- /dev/null +++ b/Source/ProductivityPlugin/Private/InstancedMeshWrapper.cpp @@ -0,0 +1,7 @@ +#include "ProductivityPluginModulePCH.h" +#include "InstancedMeshWrapper.h" + +AInstancedMeshWrapper::AInstancedMeshWrapper(const class FObjectInitializer& PCIP) : Super(PCIP) +{ + InstancedMeshes = PCIP.CreateDefaultSubobject(this, TEXT("InstancedMeshes_0")); +} diff --git a/Source/ProductivityPlugin/Private/ProductivityPluginCommands.cpp b/Source/ProductivityPlugin/Private/ProductivityPluginCommands.cpp new file mode 100644 index 0000000..dcb675c --- /dev/null +++ b/Source/ProductivityPlugin/Private/ProductivityPluginCommands.cpp @@ -0,0 +1,62 @@ +#include "ProductivityPluginModulePCH.h" +#include "ProductivityPluginCommands.h" +#include "ProductivityPluginEditorSettings.h" + +TSharedPtr FProductivityPluginCommands::GlobalStaticToInstancedActions; + +FProductivityPluginCommands::FProductivityPluginCommands() : TCommands(TEXT("ProductivityPlugin"), NSLOCTEXT("Contexts", "ProductivityPlugin", "ProductivityPlugin Plugin"), NAME_None, FProductivityPluginStyle::GetStyleSetName()) +{ + +} + +#if WITH_EDITOR +void FProductivityPluginCommands::RegisterCommands() +{ + UI_COMMAND(StaticToInstanced, "Statics<>Instanced", "Batch converts all selected static mesh actors to instanced meshes and vice versa.", EUserInterfaceActionType::Button, FInputGesture()); + UI_COMMAND(StaticToInstancedIsResultGrouped, "Group Static Meshes", "If checked, when static meshes are created from an instanced mesh wrapper, they will be grouped.", EUserInterfaceActionType::ToggleButton, FInputGesture()); +} + +TSharedRef< SWidget > FProductivityPluginCommands::GenerateStaticToInstancedMenuContent(TSharedRef InCommandList) +{ + const bool bShouldCloseWindowAfterMenuSelection = true; + FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, InCommandList); + + MenuBuilder.BeginSection("StaticToInstancedOptions"); + { + MenuBuilder.AddMenuEntry(FProductivityPluginCommands::Get().StaticToInstancedIsResultGrouped); + } + MenuBuilder.EndSection(); + + return MenuBuilder.MakeWidget(); +} + +void FProductivityPluginCommands::BindGlobalStaticToInstancedActions() +{ + check(!GlobalStaticToInstancedActions.IsValid()); + + GlobalStaticToInstancedActions = MakeShareable(new FUICommandList); + + const FProductivityPluginCommands& Commands = FProductivityPluginCommands::Get(); + FUICommandList& ActionList = *GlobalStaticToInstancedActions; + + ActionList.MapAction( + FProductivityPluginCommands::Get().StaticToInstancedIsResultGrouped, + FExecuteAction::CreateStatic(&FProductivityPluginCommandCallbacks::OnToggleStaticToInstancedResultGrouped), + FCanExecuteAction(), + FIsActionChecked::CreateStatic(&FProductivityPluginCommandCallbacks::OnToggleStaticToInstancedResultGroupedEnabled) + ); +} + +#endif + +void FProductivityPluginCommandCallbacks::OnToggleStaticToInstancedResultGrouped() +{ + UProductivityPluginEditorSettings* PlayInSettings = GetMutableDefault(); + PlayInSettings->SetGroupStaticToInstancedResults(!PlayInSettings->GetGroupStaticToInstancedResults()); +} + +bool FProductivityPluginCommandCallbacks::OnToggleStaticToInstancedResultGroupedEnabled() +{ + UProductivityPluginEditorSettings* PlayInSettings = GetMutableDefault(); + return PlayInSettings->GetGroupStaticToInstancedResults(); +} diff --git a/Source/ProductivityPlugin/Private/ProductivityPluginEditorSettings.cpp b/Source/ProductivityPlugin/Private/ProductivityPluginEditorSettings.cpp new file mode 100644 index 0000000..11c5c8a --- /dev/null +++ b/Source/ProductivityPlugin/Private/ProductivityPluginEditorSettings.cpp @@ -0,0 +1,8 @@ +#include "ProductivityPluginModulePCH.h" +#include "ProductivityPluginEditorSettings.h" + +UProductivityPluginEditorSettings::UProductivityPluginEditorSettings(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + GroupStaticToInstancedResults = false; +} \ No newline at end of file diff --git a/Source/ProductivityPlugin/Private/ProductivityPluginModule.cpp b/Source/ProductivityPlugin/Private/ProductivityPluginModule.cpp new file mode 100644 index 0000000..2f2119e --- /dev/null +++ b/Source/ProductivityPlugin/Private/ProductivityPluginModule.cpp @@ -0,0 +1,263 @@ +#include "ProductivityPluginModulePCH.h" + +#include "SlateBasics.h" +#include "SlateExtras.h" + +#include "ProductivityPluginStyle.h" +#include "ProductivityPluginCommands.h" + +#include "ILayers.h" +#include "LevelEditor.h" +#include "ScopedTransaction.h" + +#include "ProductivityTypes.h" + +static const FName ProductivityPluginTabName("ProductivityPlugin"); + +#define LOCTEXT_NAMESPACE "ProductivityPlugin" + +void FProductivityPluginModule::StartupModule() +{ + // This code will execute after your module is loaded into memory (but after global variables are initialized, of course.) + + FProductivityPluginStyle::Initialize(); + //FProductivityPluginStyle::ReloadTextures(); + + FProductivityPluginCommands::Register(); + FProductivityPluginCommands::BindGlobalStaticToInstancedActions(); + +#if WITH_EDITOR + + PluginCommands = MakeShareable(new FUICommandList); + + PluginCommands->MapAction( + FProductivityPluginCommands::Get().StaticToInstanced, + FExecuteAction::CreateRaw(this, &FProductivityPluginModule::StaticToInstancedClicked), + FCanExecuteAction()); + + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + + { + TSharedPtr MenuExtender = MakeShareable(new FExtender()); + MenuExtender->AddMenuExtension("WindowLayout", EExtensionHook::After, PluginCommands, FMenuExtensionDelegate::CreateRaw(this, &FProductivityPluginModule::AddMenuExtension)); + + LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); + } + + { + TSharedPtr ToolbarExtender = MakeShareable(new FExtender); + ToolbarExtender->AddToolBarExtension("SourceControl", EExtensionHook::After, PluginCommands, FToolBarExtensionDelegate::CreateRaw(this, &FProductivityPluginModule::AddToolbarExtension)); + + LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); + } + +#endif +} + +void FProductivityPluginModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + + FProductivityPluginStyle::Shutdown(); + + FProductivityPluginCommands::Unregister(); +} + +void FProductivityPluginModule::StaticToInstancedClicked() +{ +#if WITH_EDITOR + + const FScopedTransaction Transaction(LOCTEXT("StaticToInstanced", "Convert Statics to Instances and back")); + + { + + /* Set up selected info */ + TArray SelectedSMAs; + USelection* SMASelection = GEditor->GetSelectedSet(AStaticMeshActor::StaticClass()); + SMASelection->GetSelectedObjects(SelectedSMAs); + + TArray SelectedIMWs; + USelection* IMWSelection = GEditor->GetSelectedSet(AInstancedMeshWrapper::StaticClass()); + IMWSelection->GetSelectedObjects(SelectedIMWs); + + SMASelection->Modify(); + IMWSelection->Modify(); + + GEditor->GetSelectedActors()->DeselectAll(); + GEditor->GetSelectedObjects()->DeselectAll(); + GEditor->SelectNone(true, true, false); + GEditor->NoteSelectionChange(true); + + /* Static Mesh to Instanced */ + TArray MeshInfos; + TArray< TArray > Transforms; + + for (AStaticMeshActor* MeshActor : SelectedSMAs) + { + FMeshInfo info; + info.StaticMesh = MeshActor->GetStaticMeshComponent()->StaticMesh; + MeshActor->GetStaticMeshComponent()->GetUsedMaterials(info.Materials); + + int32 idx = 0; + + if (MeshInfos.Find(info, idx)) + { + Transforms[idx].Add(MeshActor->GetTransform()); + } + else + { + TArray newTransformArray; + newTransformArray.Add(MeshActor->GetTransform()); + MeshInfos.Add(info); + Transforms.Add(newTransformArray); + } + } + + for (int i = 0; i < SelectedSMAs.Num(); ++i) + { + SelectedSMAs[i]->GetLevel()->Modify(); + GEditor->Layers->DisassociateActorFromLayers(SelectedSMAs[i]); + SelectedSMAs[i]->GetWorld()->EditorDestroyActor(SelectedSMAs[i], false); + } + + SelectedSMAs.Empty(); + + for (int i = 0; i < MeshInfos.Num(); ++i) + { + AInstancedMeshWrapper* Wrapper = Cast(GEditor->AddActor(GEditor->LevelViewportClients[0]->GetWorld()->GetLevel(0), AInstancedMeshWrapper::StaticClass(), FTransform::Identity)); + if (Wrapper) + { + Wrapper->Modify(); + Wrapper->InstancedMeshes->SetStaticMesh(MeshInfos[i].StaticMesh); + for (int j = 0; j < MeshInfos[i].Materials.Num(); ++j) + { + Wrapper->InstancedMeshes->SetMaterial(j, MeshInfos[i].Materials[j]); + } + + for (FTransform aTransform : Transforms[i]) + { + Wrapper->InstancedMeshes->AddInstanceWorldSpace(aTransform); + } + } + } + + /* Instanced To Static Mesh */ + + for (AInstancedMeshWrapper* IMW : SelectedIMWs) + { + int32 InstanceCount = IMW->InstancedMeshes->GetInstanceCount(); + UStaticMesh* IMWMesh = IMW->InstancedMeshes->StaticMesh; + UE_LOG(LogProductivityPlugin, Verbose, TEXT("IMW Mesh: %s"), *IMWMesh->GetFullName()); + + bool bGroupResultingMeshes = FProductivityPluginCommandCallbacks::OnToggleStaticToInstancedResultGroupedEnabled(); + + TArray ActorsToGroup; + + for (int i = 0; i < InstanceCount; ++i) + { + FTransform InstanceTransform; + IMW->InstancedMeshes->GetInstanceTransform(i, InstanceTransform, true); + + AStaticMeshActor* SMA = Cast(GEditor->AddActor(GEditor->LevelViewportClients[0]->GetWorld()->GetLevel(0), AStaticMeshActor::StaticClass(), InstanceTransform)); + SMA->Modify(); + //@TODO: Figure out why editor is skipping names + SMA->SetActorLabel(*IMWMesh->GetName()); + SMA->SetMobility(EComponentMobility::Movable); + SMA->GetStaticMeshComponent()->SetStaticMesh(IMWMesh); + SMA->SetMobility(EComponentMobility::Static); + + TArray Materials; + IMW->InstancedMeshes->GetUsedMaterials(Materials); + + for (int j = 0; j < Materials.Num(); ++j) + { + SMA->GetStaticMeshComponent()->SetMaterial(j, Materials[j]); + } + + ActorsToGroup.Add(SMA); + } + + if (bGroupResultingMeshes) + { + if (ActorsToGroup.Num() > 1) + { + + // Store off the current level and make the level that contain the actors to group as the current level + UWorld* World = ActorsToGroup[0]->GetWorld(); + check(World); + { + FActorSpawnParameters SpawnInfo; + SpawnInfo.OverrideLevel = GEditor->LevelViewportClients[0]->GetWorld()->GetLevel(0); + AGroupActor* SpawnedGroupActor = World->SpawnActor(SpawnInfo); + + for (int32 ActorIndex = 0; ActorIndex < ActorsToGroup.Num(); ++ActorIndex) + { + SpawnedGroupActor->Add(*ActorsToGroup[ActorIndex]); + } + + SpawnedGroupActor->CenterGroupLocation(); + SpawnedGroupActor->bLocked = true; + } + } + } + + IMW->Modify(); + IMW->GetLevel()->Modify(); + GEditor->Layers->DisassociateActorFromLayers(IMW); + IMW->GetWorld()->EditorDestroyActor(IMW, false); + } + + SelectedIMWs.Empty(); + + // Remove all references to destroyed actors once at the end, instead of once for each Actor destroyed.. + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); +#endif + } +} + +void FProductivityPluginModule::AddMenuExtension(FMenuBuilder& builder) +{ + { + + //builder.AddMenuEntry( + // FProductivityPluginCommands::Get().StaticToInstanced, + // NAME_None, + // FProductivityPluginCommands::Get().StaticToInstanced->GetLabel(), + // FProductivityPluginCommands::Get().StaticToInstanced->GetDescription(), + // FProductivityPluginCommands::Get().StaticToInstanced->GetIcon(), + // NAME_None); + } +} + +void FProductivityPluginModule::AddToolbarExtension(FToolBarBuilder &builder) +{ + builder.BeginSection("Productivity"); + + builder.AddToolBarButton( + FProductivityPluginCommands::Get().StaticToInstanced, + NAME_None, + FProductivityPluginCommands::Get().StaticToInstanced->GetLabel(), + FProductivityPluginCommands::Get().StaticToInstanced->GetDescription(), + FProductivityPluginCommands::Get().StaticToInstanced->GetIcon(), + NAME_None); + + FUIAction StaticToInstancedOptionsMenuAction; + + builder.AddComboButton( + StaticToInstancedOptionsMenuAction, + FOnGetContent::CreateStatic(&FProductivityPluginCommands::GenerateStaticToInstancedMenuContent, FProductivityPluginCommands::GlobalStaticToInstancedActions.ToSharedRef()), + LOCTEXT("StaticToInstancedOptions_Label", "Static<>Instanced Options"), + LOCTEXT("StaticToInstancedOptions_Tooltip", "Options for converting static meshes to instanced meshes and vice versa."), + FProductivityPluginCommands::Get().StaticToInstanced->GetIcon(), + true + ); + + builder.EndSection(); +} + +DEFINE_LOG_CATEGORY(LogProductivityPlugin) + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FProductivityPluginModule, ProductivityPlugin) \ No newline at end of file diff --git a/Source/ProductivityPlugin/Private/ProductivityPluginModulePCH.h b/Source/ProductivityPlugin/Private/ProductivityPluginModulePCH.h new file mode 100644 index 0000000..d92f6b8 --- /dev/null +++ b/Source/ProductivityPlugin/Private/ProductivityPluginModulePCH.h @@ -0,0 +1,10 @@ +// Some copyright should be here... +#pragma once + +#include "ProductivityPluginModule.h" +#include "InstancedMeshWrapper.h" + +// You should place include statements to your module's private header files here. You only need to +// add includes for headers that are used in most of your module's source files though. + +DECLARE_LOG_CATEGORY_EXTERN(LogProductivityPlugin, VeryVerbose, All); \ No newline at end of file diff --git a/Source/ProductivityPlugin/Private/ProductivityPluginStyle.cpp b/Source/ProductivityPlugin/Private/ProductivityPluginStyle.cpp new file mode 100644 index 0000000..9f1826d --- /dev/null +++ b/Source/ProductivityPlugin/Private/ProductivityPluginStyle.cpp @@ -0,0 +1,67 @@ +// Some copyright should be here... +#include "ProductivityPluginModulePCH.h" + +#include "ProductivityPluginStyle.h" +#include "SlateGameResources.h" + +TSharedPtr< FSlateStyleSet > FProductivityPluginStyle::StyleInstance = NULL; + +void FProductivityPluginStyle::Initialize() +{ + if (!StyleInstance.IsValid()) + { + StyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*StyleInstance); + } +} + +void FProductivityPluginStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*StyleInstance); + ensure(StyleInstance.IsUnique()); + StyleInstance.Reset(); +} + +FName FProductivityPluginStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("ProductivityPluginStyle")); + return StyleSetName; +} + +#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define TTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ ) +#define OTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ ) + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); +const FVector2D Icon40x40(40.0f, 40.0f); + +TSharedRef< FSlateStyleSet > FProductivityPluginStyle::Create() +{ + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("ProductivityPluginStyle")); + Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("ProductivityPlugin/Resources")); + + //Style->Set("ButtonIcon", new IMAGE_BRUSH(TEXT("ButtonIcon"), Icon40x40)); + Style->Set("ProductivityPlugin.StaticToInstanced", new IMAGE_BRUSH(TEXT("StaticToInstanced"), Icon40x40)); + Style->Set("ProductivityPlugin.StaticToInstanced.Small", new IMAGE_BRUSH(TEXT("StaticToInstanced"), Icon20x20)); + + return Style; +} + +#undef IMAGE_BRUSH +#undef BOX_BRUSH +#undef BORDER_BRUSH +#undef TTF_FONT +#undef OTF_FONT + +void FProductivityPluginStyle::ReloadTextures() +{ + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); +} + +const ISlateStyle& FProductivityPluginStyle::Get() +{ + return *StyleInstance; +} diff --git a/Source/ProductivityPlugin/ProductivityPlugin.Build.cs b/Source/ProductivityPlugin/ProductivityPlugin.Build.cs new file mode 100644 index 0000000..09490a1 --- /dev/null +++ b/Source/ProductivityPlugin/ProductivityPlugin.Build.cs @@ -0,0 +1,62 @@ +// Some copyright should be here... + +using UnrealBuildTool; + +public class ProductivityPlugin : ModuleRules +{ + public ProductivityPlugin(TargetInfo Target) + { + + PublicIncludePaths.AddRange( + new string[] { + "ProductivityPlugin/Classes" + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + "ProductivityPlugin/Private", + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + // ... add other public dependencies that you statically link with here ... + } + ); + + + if (UEBuildConfiguration.bBuildEditor) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "UnrealEd", + "LevelEditor", + + // ... add private dependencies that you statically link with here ... + } + ); + } + + + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +}