Changed AssetsHelpers from editor window to context menu workflow.

This commit is contained in:
Misaki
2024-12-24 16:16:12 +09:00
parent a93660b771
commit 509357011c
35 changed files with 519 additions and 227 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5012fabff1a87ad418b1eb647915e70c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace Misaki.ArtToolEditor
{
internal static class Constants
{
internal static class Shader
{
internal const string DECAL_SHADER_PATH = "HDRP/Decal";
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c17900c8aa57f5840b6fc3ea27cd9a67

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: da7d1a712bed7f748acb4c0c7c343c5e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace Misaki.ArtToolEditor
{
public enum OutputDirectoryType
{
CurrentDirectory,
ParentDirectory,
AssetsDirectory,
Custom
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9ab43ed4982202d4a89c86dce7742a6c

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ae194d6a82c67d14eb5070e5c5336edd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering.HighDefinition;
namespace Misaki.ArtToolEditor
{
public static class AssetCreationHelpers
{
public static GameObject CreateDecal(string path)
{
var decal = new GameObject("Temp_Decal");
decal.AddComponent<DecalProjector>();
PrefabUtility.SaveAsPrefabAsset(decal, path);
Object.DestroyImmediate(decal);
return AssetDatabase.LoadAssetAtPath<GameObject>(path);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4e43fc7bacddd9a46ba17e461058fc90

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e7734f9b2e8f59f4dbc841e7fbe22629
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d38cb46c0b7e6504c84d6caff2e89903
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering.HighDefinition;
namespace Misaki.ArtToolEditor
{
public class CreateDecal
{
[MenuItem("Assets/Art Tools/Material Helpers/Create Decal", true)]
public static bool CreateDecalMenuValidator()
{
foreach (var selectedObject in Selection.objects)
{
if (selectedObject is not Material)
{
return false;
}
}
return true;
}
[MenuItem("Assets/Art Tools/Material Helpers/Create Decal")]
public static void CreateDecalMenu()
{
OutputOptionsWindow.ShowWindow("Decal Output Options", Selection.objects, CreateDecalInternal);
}
private static string CreateDecalInternal(Object inputObject, string outputDirectory)
{
if (inputObject is not Material mat)
{
return null;
}
var outputPath = Path.Combine(outputDirectory, mat.name + ".prefab");
var decal = AssetCreationHelpers.CreateDecal(outputPath);
mat.shader = Shader.Find(Constants.Shader.DECAL_SHADER_PATH);
decal.transform.rotation = Quaternion.Euler(90, 0, 0);
decal.GetComponent<DecalProjector>().material = mat;
return outputPath;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a4585b41cdb0577478ac9c4e563dc639

View File

@@ -0,0 +1,52 @@
using System.IO;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Misaki.ArtToolEditor
{
public class CreateTerrainLayer
{
private const string Terrain_Layer_Extension = ".terrainlayer";
[MenuItem("Assets/Art Tools/Material Helpers/Create Terrain Layer", true)]
public static bool CreateDecalMenuValidator()
{
foreach (var selectedObject in Selection.objects)
{
if (selectedObject is not Material)
{
return false;
}
}
return true;
}
[MenuItem("Assets/Art Tools/Material Helpers/Create Terrain Layer")]
public static void CreateDecalMenu()
{
OutputOptionsWindow.ShowWindow("Terrain Layer Output Options", Selection.objects, CreateTerrainLayerInternal);
}
private static string CreateTerrainLayerInternal(Object inputObject, string outputDirectory)
{
if (inputObject is not Material mat)
{
return null;
}
var terrainLayer = new TerrainLayer()
{
diffuseTexture = mat.GetTexture("_BaseColorMap") as Texture2D,
normalMapTexture = mat.GetTexture("_NormalMap") as Texture2D,
maskMapTexture = mat.GetTexture("_MaskMap") as Texture2D,
};
var outputPath = Path.Combine(outputDirectory, mat.name + Terrain_Layer_Extension);
AssetDatabase.CreateAsset(terrainLayer, outputPath);
return outputPath;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4ba5b5a3211673749a18dae69e07a0f3

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 223cd2d5e7a5b9b4cba370fb38777463
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Misaki.ArtToolEditor
{
public class RemapMaterials
{
private const string Material_Extension = ".mat";
private static bool _useMaterialRemap = false;
private static ModelImporterMaterialLocation _materialLocation = ModelImporterMaterialLocation.InPrefab;
private static ModelImporterMaterialName _materialRemapNamingOption = ModelImporterMaterialName.BasedOnMaterialName;
private static ModelImporterMaterialSearch _materialRemapSearchOption = ModelImporterMaterialSearch.RecursiveUp;
[MenuItem("Assets/Art Tools/Mesh Helpers/Extract Materials", true)]
public static bool CreateDecalMenuValidator()
{
foreach (var selectedObject in Selection.objects)
{
var assetImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(selectedObject));
if (assetImporter is not ModelImporter)
{
return false;
}
}
return true;
}
[MenuItem("Assets/Art Tools/Mesh Helpers/Extract Materials")]
public static void CreateDecalMenu()
{
var window = EditorWindow.GetWindow<OutputOptionsWindow>(true, "Extract Materials Output Options");
window.WithSource(Selection.objects);
window.WithContentBeforeButton(() =>
{
var root = new VisualElement();
var materialRemapOptionContainer = new VisualElement()
{
style =
{
display = DisplayStyle.None,
}
};
var useMaterialRemap = new DropdownField("Use Material Remap", new List<string> { "False", "True" }, "False");
useMaterialRemap.RegisterValueChangedCallback(evt =>
{
_useMaterialRemap = evt.newValue == "True";
materialRemapOptionContainer.style.display = _useMaterialRemap ? DisplayStyle.Flex : DisplayStyle.None;
});
var materialLocation = new EnumField("Material Location", _materialLocation);
materialLocation.RegisterValueChangedCallback(evt => _materialLocation = (ModelImporterMaterialLocation)evt.newValue);
var materialRemapNamingOption = new EnumField("Material Naming Option", _materialRemapNamingOption);
materialRemapNamingOption.RegisterValueChangedCallback(evt => _materialRemapNamingOption = (ModelImporterMaterialName)evt.newValue);
var materialRemapSearchOption = new EnumField("Material Search Option", _materialRemapSearchOption);
materialRemapSearchOption.RegisterValueChangedCallback(evt => _materialRemapSearchOption = (ModelImporterMaterialSearch)evt.newValue);
materialRemapOptionContainer.Add(materialLocation);
materialRemapOptionContainer.Add(materialRemapNamingOption);
materialRemapOptionContainer.Add(materialRemapSearchOption);
root.Add(useMaterialRemap);
root.Add(materialRemapOptionContainer);
return root;
});
window.WithOutput(ExtractMaterialsInternal);
window.WithPostOutput(ExtractMaterialsPostInternal);
window.ShowUtility();
}
private static string ExtractMaterialsInternal(Object inputObject, string outputDirectory)
{
var assetImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(inputObject));
if (assetImporter is not ModelImporter modelImporter)
{
return null;
}
if (_useMaterialRemap)
{
modelImporter.materialLocation = _materialLocation;
modelImporter.SearchAndRemapMaterials(_materialRemapNamingOption, _materialRemapSearchOption);
modelImporter.SaveAndReimport();
return null;
}
else
{
var materials = AssetDatabase.LoadAllAssetsAtPath(modelImporter.assetPath).Where(x => x.GetType() == typeof(Material));
foreach (var material in materials)
{
var newAssetPath = Path.Combine(outputDirectory, material.name) + Material_Extension;
newAssetPath = AssetDatabase.GenerateUniqueAssetPath(newAssetPath);
AssetDatabase.ExtractAsset(material, newAssetPath);
}
return modelImporter.assetPath;
}
}
private static void ExtractMaterialsPostInternal(string assetPath)
{
if (string.IsNullOrEmpty(assetPath))
{
return;
}
AssetDatabase.WriteImportSettingsIfDirty(assetPath);
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9cc9f1a0be83aa444884c32f825c5303

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee2d58939cc61cf48afb86056b47a69b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<engine:VisualElement data-source-type="Misaki.ArtToolEditor.OutputOptionsWindow, Misaki.ArtTool.Editor" style="flex-grow: 1;">
<engine:ScrollView>
<engine:VisualElement name="content-on-top" />
<engine:Foldout text="Source" style="margin-bottom: 4px;">
<engine:ListView header-title="Source Objects" selection-type="None" allow-add="false" allow-remove="false" show-border="true" name="source-object-list" binding-source-selection-mode="AutoAssign" style="max-height: 100px;">
<Bindings>
<engine:DataBinding property="itemsSource" data-source-path="_sourceObjects" binding-mode="ToTargetOnce" />
</Bindings>
</engine:ListView>
</engine:Foldout>
<engine:VisualElement name="content-after-list" />
<engine:EnumField label="Output Directory" value="Center" type="Misaki.ArtToolEditor.OutputDirectoryType, Misaki.ArtTool.Editor" name="directory-type-enum">
<Bindings>
<engine:DataBinding property="value" data-source-path="_outputOptions.outputDirectoryType" binding-mode="TwoWay" />
</Bindings>
</engine:EnumField>
<engine:TextField label="Custom Path" placeholder-text="path" name="custom-path-input" tooltip="Support absolute and relative path">
<Bindings>
<engine:DataBinding property="value" data-source-path="_outputOptions.customPath" binding-mode="TwoWay" />
</Bindings>
</engine:TextField>
<engine:VisualElement name="content-before-button" />
<engine:Button text="Output" name="output-button" />
<engine:VisualElement name="content-on-bottom" />
</engine:ScrollView>
</engine:VisualElement>
</engine:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0413a47b12d366049baaf9a7e4b20fb7
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
namespace Misaki.ArtToolEditor
{
public struct OutputOptions
{
public OutputDirectoryType outputDirectoryType;
public string customPath;
}
public class OutputOptionsWindow : EditorWindow
{
[SerializeField]
private VisualTreeAsset _visualTreeAsset = default;
[SerializeField]
private OutputOptions _outputOptions;
[SerializeField]
private IEnumerable<Object> _sourceObjects;
private VisualElement _contentOnTop;
private VisualElement _contentAfterList;
private VisualElement _contentBeforeButton;
private VisualElement _contentOnBottom;
private Func<Object, string, string> onOutput;
private Action<string> postOutput;
/// <summary>
/// Sets the source objects for the output options window.
/// </summary>
/// <param name="source"> The source objects to process. </param>
public void WithSource(IEnumerable<Object> source) => _sourceObjects = source;
/// <summary>
/// Sets the output options for the output options window.
/// </summary>
/// <param name="content"> The output options to set. </param>
public void WithContentOnTop(Func<VisualElement> content) => _contentOnTop.Add(content());
/// <summary>
/// Sets the output options for the output options window.
/// </summary>
/// <param name="content"> The output options to set. </param>
public void WithContentAfterList(Func<VisualElement> content) => _contentAfterList.Add(content());
/// <summary>
/// Sets the output options for the output options window.
/// </summary>
/// <param name="content"> The output options to set. </param>
public void WithContentBeforeButton(Func<VisualElement> content) => _contentBeforeButton.Add(content());
/// <summary>
/// Sets the output options for the output options window.
/// </summary>
/// <param name="content"> The output options to set. </param>
public void WithContentOnBottom(Func<VisualElement> content) => _contentOnBottom.Add(content());
/// <summary>
/// Sets the output action for the output options window.
/// </summary>
/// <param name="outputAction"> The output action to set.
/// <para>The parameters of the Func are:</para>
/// <para>- Object: The object to process.</para>
/// <para>- string: The output directory.</para>
/// The Func returns the path of the asset, which will be use later in the post output.</param>
public void WithOutput(Func<Object, string, string> outputAction) => onOutput = outputAction;
/// <summary>
/// Sets the post output action for the output options window.
/// </summary>
/// <param name="postOutputAction"> The post output action to set. </param>
public void WithPostOutput(Action<string> postOutputAction) => postOutput = postOutputAction;
public static OutputOptionsWindow ShowWindow(string title, IEnumerable<Object> source, Func<Object, string, string> outputAction)
{
var window = GetWindow<OutputOptionsWindow>(true, title);
window.WithSource(source);
window.WithOutput(outputAction);
window.ShowUtility();
return window;
}
private void CreateGUI()
{
var outputOptionsElement = _visualTreeAsset.Instantiate();
SetupOutputElements(outputOptionsElement);
rootVisualElement.Add(outputOptionsElement);
rootVisualElement.dataSource = this;
}
private void SetupOutputElements(VisualElement outPutOptions)
{
var sourceObjectList = outPutOptions.Q<ListView>("source-object-list");
sourceObjectList.makeItem = () => new ObjectField() { enabledSelf = false };
sourceObjectList.bindItem = (element, i) =>
{
var objectField = element as ObjectField;
objectField.objectType = typeof(Object);
objectField.value = _sourceObjects.ElementAt(i);
};
var directoryTypeEnum = outPutOptions.Q<EnumField>("directory-type-enum");
var customPathInput = outPutOptions.Q<TextField>("custom-path-input");
directoryTypeEnum.RegisterValueChangedCallback((evt) =>
{
customPathInput.style.display = evt.newValue.Equals(OutputDirectoryType.Custom) ? DisplayStyle.Flex : DisplayStyle.None;
});
var outputButton = outPutOptions.Q<Button>("output-button");
outputButton.clickable.clicked += () =>
{
if (_sourceObjects == null)
{
Close();
}
var assetsToReload = new HashSet<string>();
foreach (var obj in _sourceObjects)
{
var objectPath = AssetDatabase.GetAssetPath(obj);
var outputDirectory = GetOutputDirectory(Path.GetDirectoryName(objectPath));
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}
assetsToReload.Add(onOutput?.Invoke(obj, outputDirectory));
}
foreach (var assetPath in assetsToReload)
{
postOutput?.Invoke(assetPath);
}
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
Close();
};
_contentOnTop = outPutOptions.Q<VisualElement>("content-on-top");
_contentAfterList = outPutOptions.Q<VisualElement>("content-after-list");
_contentBeforeButton = outPutOptions.Q<VisualElement>("content-before-button");
_contentOnBottom = outPutOptions.Q<VisualElement>("content-on-bottom");
}
private string GetOutputDirectory(string inputDirectory)
{
switch (_outputOptions.outputDirectoryType)
{
case OutputDirectoryType.CurrentDirectory:
return inputDirectory;
case OutputDirectoryType.ParentDirectory:
return Path.GetDirectoryName(inputDirectory);
case OutputDirectoryType.AssetsDirectory:
return "Assets";
case OutputDirectoryType.Custom:
if (Path.IsPathRooted(_outputOptions.customPath))
{
return _outputOptions.customPath;
}
else
{
return Path.Combine(inputDirectory, _outputOptions.customPath);
}
default:
return inputDirectory;
}
}
}
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 29f3e54f493766149b280d2753bca2af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- _visualTreeAsset: {fileID: 9197481963319205126, guid: 0413a47b12d366049baaf9a7e4b20fb7,
type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: