Updated GenerateMask helper

This commit is contained in:
Misaki
2025-01-23 23:56:11 +09:00
parent b00b63cfb4
commit eb6c2d9e54
16 changed files with 503 additions and 232 deletions

View File

@@ -1,29 +1,32 @@
using UnityEngine.UIElements;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace Misaki.ArtToolEditor
{
public abstract class OptionsVisualProvider
{
public object dataSource;
public IAssetsProcessor processor;
public IEnumerable<Object> sourceObjects;
public virtual VisualElement ContentOnTop()
public virtual void ContentOnTop(VisualElement rootVisualElement)
{
return null;
}
public virtual VisualElement ContentAfterList()
public virtual void ContentAfterList(VisualElement rootVisualElement)
{
return null;
}
public virtual VisualElement ContentBeforeButton()
public virtual void ContentBeforeButton(VisualElement rootVisualElement)
{
return null;
}
public virtual VisualElement ContentOnBottom()
public virtual void ContentOnRight(VisualElement rootVisualElement)
{
}
public virtual void ContentOnBottom(VisualElement rootVisualElement)
{
return null;
}
}
}

View File

@@ -1,10 +1,31 @@
namespace Misaki.ArtToolEditor
using System;
namespace Misaki.ArtToolEditor
{
internal enum TextureChannel
{
None,
R,
G,
B,
A
}
internal static class TextureChannelHelpers
{
private static readonly ArgumentException _channelNoneException = new("Channel is none");
public static int ToColorIndex(this TextureChannel channel)
{
return channel switch
{
TextureChannel.None => throw _channelNoneException,
TextureChannel.R => 0,
TextureChannel.G => 1,
TextureChannel.B => 2,
TextureChannel.A => 3,
_ => throw _channelNoneException
};
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace Misaki.ArtToolEditor
{
internal class CaseInsensitiveEqualityComparer : IEqualityComparer<string>
{
private static CaseInsensitiveEqualityComparer _default;
public static CaseInsensitiveEqualityComparer Default = _default ?? new CaseInsensitiveEqualityComparer();
public bool Equals(string x, string y)
{
return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(string obj)
{
return obj.ToLowerInvariant().GetHashCode();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9936a64e41c0ed842b8e241684b1d002

View File

@@ -14,7 +14,7 @@ namespace Misaki.ArtToolEditor
var bytes = exportTexture.EncodeToPNG();
File.WriteAllBytes(textureFullPath, bytes);
UnityEngine.Object.DestroyImmediate(exportTexture);
Object.DestroyImmediate(exportTexture);
}
}
}
}

View File

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

View File

@@ -7,9 +7,10 @@ namespace Misaki.ArtToolEditor
{
internal class ExtractMaterialsVisualProvider : OptionsVisualProvider
{
public override VisualElement ContentAfterList()
public override void ContentAfterList(VisualElement rootVisualElement)
{
var root = new VisualElement();
rootVisualElement.dataSource = processor;
var materialRemapOptionContainer = new VisualElement()
{
style =
@@ -58,9 +59,8 @@ namespace Misaki.ArtToolEditor
materialRemapOptionContainer.Add(materialRemapNamingOptionField);
materialRemapOptionContainer.Add(materialRemapSearchOptionField);
root.Add(useMaterialRemapField);
root.Add(materialRemapOptionContainer);
return root;
rootVisualElement.Add(useMaterialRemapField);
rootVisualElement.Add(materialRemapOptionContainer);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
@@ -20,7 +21,8 @@ namespace Misaki.ArtToolEditor
public TextureChannel BChannelSource = TextureChannel.B;
public TextureChannel AChannelSource = TextureChannel.A;
public string outputNameSuffix = "_ChannelMixer";
public string outputNameRegex = "$";
public string outputNameReplacement = "_ChannelMixer";
public void OnPreProcess(AssetsProcessContext context)
{
@@ -36,10 +38,18 @@ namespace Misaki.ArtToolEditor
var tempRT = RenderTexture.GetTemporary(texture.width, texture.height, 0, GraphicsFormat.R8G8B8A8_UNorm);
var material = new Material(Shader.Find(ShaderConstants.Channel_Mixer_Shader_Path));
material.SetVector(ShaderConstants.Channel_Source_Property, new Vector4((int)RChannelSource, (int)GChannelSource, (int)BChannelSource, (int)AChannelSource));
material.SetVector(
ShaderConstants.Channel_Source_Property,
new Vector4(
RChannelSource.ToColorIndex(),
GChannelSource.ToColorIndex(),
BChannelSource.ToColorIndex(),
AChannelSource.ToColorIndex()));
Graphics.Blit(texture, tempRT, material);
var texturePath = Path.Combine(outputDirectory, texture.name + outputNameSuffix + Constants.Extensions.PNG);
var outputName = Regex.Replace(texture.name, outputNameRegex, outputNameReplacement, RegexOptions.Compiled);
var texturePath = Path.Combine(outputDirectory, outputName + Constants.Extensions.PNG);
TextureHelpers.ExportRenderTextureToPNG(tempRT, texturePath);
AssetDatabase.ImportAsset(texturePath);

View File

@@ -5,28 +5,33 @@ namespace Misaki.ArtToolEditor
{
internal class ChannelMixerVisualProvider : OptionsVisualProvider
{
public override VisualElement ContentAfterList()
public override void ContentAfterList(VisualElement rootVisualElement)
{
var root = new VisualElement();
rootVisualElement.dataSource = processor;
root.Add(CreateChannelSelector("Set Channel R From", nameof(ChannelMixerProcessor.RChannelSource)));
root.Add(CreateChannelSelector("Set Channel G From", nameof(ChannelMixerProcessor.GChannelSource)));
root.Add(CreateChannelSelector("Set Channel B From", nameof(ChannelMixerProcessor.BChannelSource)));
root.Add(CreateChannelSelector("Set Channel A From", nameof(ChannelMixerProcessor.AChannelSource)));
rootVisualElement.Add(CreateChannelSelector("Set Channel R From", nameof(ChannelMixerProcessor.RChannelSource)));
rootVisualElement.Add(CreateChannelSelector("Set Channel G From", nameof(ChannelMixerProcessor.GChannelSource)));
rootVisualElement.Add(CreateChannelSelector("Set Channel B From", nameof(ChannelMixerProcessor.BChannelSource)));
rootVisualElement.Add(CreateChannelSelector("Set Channel A From", nameof(ChannelMixerProcessor.AChannelSource)));
var nameSuffixField = new TextField("Output Name Suffix")
var nameRegexField = new TextField("Output Name Regx")
{
tooltip = "The suffix to append to the output texture name. Will overwrite original texture if set it to empty and use CurrentDirectory as output directory.",
tooltip = "The regex to match when replace the name",
};
nameSuffixField.SetBinding(nameof(TextField.value), new DataBinding() { dataSourcePath = PropertyPath.FromName(nameof(ChannelMixerProcessor.outputNameSuffix)) });
root.Add(nameSuffixField);
nameRegexField.SetBinding(nameof(TextField.value), new DataBinding() { dataSourcePath = PropertyPath.FromName(nameof(ChannelMixerProcessor.outputNameRegex)) });
rootVisualElement.Add(nameRegexField);
return root;
var nameReplaceField = new TextField("Output Name Replacement")
{
tooltip = "The string to replace with",
};
nameReplaceField.SetBinding(nameof(TextField.value), new DataBinding() { dataSourcePath = PropertyPath.FromName(nameof(ChannelMixerProcessor.outputNameReplacement)) });
rootVisualElement.Add(nameReplaceField);
}
private static VisualElement CreateChannelSelector(string label, string binding)
{
var enumField = new EnumField(label, TextureChannel.R);
var enumField = new EnumField(label, TextureChannel.None);
enumField.SetBinding(nameof(EnumField.value), new DataBinding() { dataSourcePath = PropertyPath.FromName(binding) });
return enumField;

View File

@@ -2,10 +2,10 @@ Shader "Hidden/GenerateMask"
{
Properties
{
_TexR ("Red Channel Texture", 2D) = "white"
_TexG ("Green Channel Texture", 2D) = "white"
_TexB ("Blue Channel Texture", 2D) = "white"
_TexA ("Alpha Channel Texture", 2D) = "white"
_MetallicMap ("MetallicMap", 2D) = "white"
_AoMap ("AoMap", 2D) = "white"
_DetailMap ("DetailMap", 2D) = "white"
_SmoothnessMap ("SmoothnessMap", 2D) = "white"
}
SubShader
{
@@ -20,10 +20,11 @@ Shader "Hidden/GenerateMask"
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature_local _HAS_TEX_R
#pragma shader_feature_local _HAS_TEX_G
#pragma shader_feature_local _HAS_TEX_B
#pragma shader_feature_local _HAS_TEX_A
#pragma shader_feature_local _HAS_METALLIC
#pragma shader_feature_local _HAS_AO
#pragma shader_feature_local _HAS_DETAIL
#pragma shader_feature_local _HAS_SMOOTHNESS
#pragma shader_feature_local _HAS_ROUGHNESS
#include "UnityCG.cginc"
@@ -44,35 +45,37 @@ Shader "Hidden/GenerateMask"
return o;
}
sampler2D _TexR;
sampler2D _TexG;
sampler2D _TexB;
sampler2D _TexA;
sampler2D _MetallicMap;
sampler2D _AoMap;
sampler2D _DetailMap;
sampler2D _SmoothnessMap;
float4 _Fallback;
float4 frag (v2f i) : SV_Target
{
#ifdef _HAS_TEX_R
float r = tex2D(_TexR, i.uv).r;
#ifdef _HAS_METALLIC
float r = tex2D(_MetallicMap, i.uv).r;
#else
float r = _Fallback.r;
#endif
#ifdef _HAS_TEX_G
float g = tex2D(_TexG, i.uv).r;
#ifdef _HAS_AO
float g = tex2D(_AoMap, i.uv).r;
#else
float g = _Fallback.g;
#endif
#ifdef _HAS_TEX_B
float b = tex2D(_TexB, i.uv).r;
#ifdef _HAS_DETAIL
float b = tex2D(_DetailMap, i.uv).r;
#else
float b = _Fallback.b;
#endif
#ifdef _HAS_TEX_A
float a = tex2D(_TexA, i.uv).r;
#ifdef _HAS_SMOOTHNESS
float a = tex2D(_SmoothnessMap, i.uv).r;
#elif _HAS_ROUGHNESS
float a = 1.0f - tex2D(_SmoothnessMap, i.uv).r;
#else
float a = _Fallback.a;
#endif

View File

@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Unity.Collections;
using UnityEditor;
using UnityEngine;
@@ -10,184 +10,267 @@ namespace Misaki.ArtToolEditor
{
internal class GenerateMaskProcessor : IAssetsProcessor
{
private struct PackingConfig : IDisposable
public struct TextureSource
{
public Texture2D texture;
public bool isRoughness;
public TextureChannel channel;
}
public struct GroupingConfig
{
public string outputDirectory;
public NativeList<FixedString64Bytes> texturePaths;
public void Dispose()
{
texturePaths.Dispose();
}
public List<TextureSource> textureSources;
}
private class ShaderConstants
{
internal const string Generate_Mask_Shader_Path = "Hidden/GenerateMask";
internal const string Texture_R_Property = "_TexR";
internal const string Texture_G_Property = "_TexG";
internal const string Texture_B_Property = "_TexB";
internal const string Texture_A_Property = "_TexA";
internal const string Metallic_Property = "_MetallicMap";
internal const string Ao_Property = "_AoMap";
internal const string Detail_Property = "_DetailMap";
internal const string Smoothness_Property = "_SmoothnessMap";
internal const string Fallback_Property = "_Fallback";
internal const string Has_Texture_R_Keyword = "_HAS_TEX_R";
internal const string Has_Texture_G_Keyword = "_HAS_TEX_G";
internal const string Has_Texture_B_Keyword = "_HAS_TEX_B";
internal const string Has_Texture_A_Keyword = "_HAS_TEX_A";
internal const string Has_Metallic_Keyword = "_HAS_METALLIC";
internal const string Has_Ao_Keyword = "_HAS_AO";
internal const string Has_Detail_Keyword = "_HAS_DETAIL";
internal const string Has_Smoothness_Keyword = "_HAS_SMOOTHNESS";
internal const string Has_Roughness_Keyword = "_HAS_ROUGHNESS";
}
internal const string IGNORED_GROUP_NAME = "Ignored Items";
private static readonly RegexOptions _regexOptions = RegexOptions.IgnoreCase | RegexOptions.Compiled;
public string metallicTextureRegex = ".*_metallic";
public string aoTextureRegex = ".*_ao|.*_ambientocclusion";
public string detailMaskTextureRegex = ".*_detailmask";
public string smoothnessTextureRegex = ".*_smoothness";
public bool isFallbackToRoughness = true;
public string roughnessTextureRegex = ".*_roughness";
public TextureFallbackType metallicFallbackType = TextureFallbackType.Black;
public TextureFallbackType aoFallbackType = TextureFallbackType.White;
public TextureFallbackType detailMaskFallbackType = TextureFallbackType.White;
public TextureFallbackType smoothnessFallbackType = TextureFallbackType.LinearGray;
public string textureGroupingRegex = "^([^_]+)_";
public string namingRegex;
public TextureChannel namingSource;
public string textureGroupingRegex = "^(.*)_[^_]*$";
public string namingRegex = "(?!.*_).*";
public TextureChannel namingSource = TextureChannel.R;
public string replaceBy = "Mask";
private void GroupInputTexture(in Dictionary<string, GroupingConfig> group, Texture2D inputTexture, string outputDirectory)
{
string groupName;
var match = Regex.Match(inputTexture.name, textureGroupingRegex, _regexOptions);
var targetChannel = GetTextureTargetChannel(inputTexture);
var isRoughness = false;
if (targetChannel == TextureChannel.None && isFallbackToRoughness)
{
if (Regex.IsMatch(inputTexture.name, roughnessTextureRegex, _regexOptions))
{
targetChannel = TextureChannel.A;
isRoughness = true;
}
}
if (!match.Success || targetChannel == TextureChannel.None)
{
groupName = IGNORED_GROUP_NAME;
}
else
{
groupName = match.Groups[1].Value;
}
if (!group.ContainsKey(groupName))
{
group[groupName] = new GroupingConfig
{
outputDirectory = outputDirectory,
textureSources = new List<TextureSource>(4)
};
}
var textureSource = new TextureSource()
{
texture = inputTexture,
isRoughness = isRoughness,
channel = targetChannel,
};
group[groupName].textureSources.Add(textureSource);
}
internal Dictionary<string, GroupingConfig> CreatePreviewGroupingResult(IEnumerable<UnityEngine.Object> sourceObjects)
{
var groupingResult = new Dictionary<string, GroupingConfig>(CaseInsensitiveEqualityComparer.Default);
foreach (var sourceObject in sourceObjects)
{
if (sourceObject is not Texture2D texture)
{
continue;
}
GroupInputTexture(groupingResult, texture, null);
}
return groupingResult;
}
public void OnPreProcess(AssetsProcessContext context)
{
context.userData = new Dictionary<string, PackingConfig>();
context.userData = new Dictionary<string, GroupingConfig>(CaseInsensitiveEqualityComparer.Default);
}
public void OnProcess(UnityEngine.Object source, string outputDirectory, AssetsProcessContext context)
{
if (source is not Texture2D texture)
if (source is not Texture2D texture ||
context.userData is not Dictionary<string, GroupingConfig> groupConfigs)
{
return;
}
var groupConfigs = (Dictionary<string, PackingConfig>)context.userData;
var match = Regex.Match(texture.name, textureGroupingRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (!match.Success)
{
return;
}
var groupName = match.Groups[1].Value;
if (!groupConfigs.ContainsKey(groupName))
{
groupConfigs[groupName] = new PackingConfig
{
outputDirectory = outputDirectory,
texturePaths = new NativeList<FixedString64Bytes>(4, Allocator.TempJob)
};
}
var config = groupConfigs[groupName];
config.texturePaths.AddNoResize(AssetDatabase.GetAssetPath(texture));
GroupInputTexture(groupConfigs, texture, outputDirectory);
}
public void OnPostProcess(AssetsProcessContext context)
{
var groupConfigs = (Dictionary<string, PackingConfig>)context.userData;
try
if (context.userData is not Dictionary<string, GroupingConfig> groupConfigs)
{
foreach (var group in groupConfigs)
{
var metallicTexture = FindTexture(group.Value.texturePaths, metallicTextureRegex);
var aoTexture = FindTexture(group.Value.texturePaths, aoTextureRegex);
var detailMaskTexture = FindTexture(group.Value.texturePaths, detailMaskTextureRegex);
var smoothnessTexture = FindTexture(group.Value.texturePaths, smoothnessTextureRegex);
return;
}
GetTextureSizeAndName(metallicTexture, aoTexture, detailMaskTexture, out var textureSize, out var textureName);
SetupAndBlitTexture(metallicTexture, aoTexture, detailMaskTexture, smoothnessTexture, textureSize, out var tempRT, out var material);
foreach (var group in groupConfigs)
{
if (group.Key == IGNORED_GROUP_NAME)
{
continue;
}
var metallicTexture = group.Value.textureSources.FirstOrDefault(s => s.channel == TextureChannel.R);
var aoTexture = group.Value.textureSources.FirstOrDefault(s => s.channel == TextureChannel.G);
var detailMaskTexture = group.Value.textureSources.FirstOrDefault(s => s.channel == TextureChannel.B);
var smoothnessTexture = group.Value.textureSources.FirstOrDefault(s => s.channel == TextureChannel.A);
if (!TryGetTextureSizeAndName(metallicTexture.texture, aoTexture.texture, detailMaskTexture.texture, smoothnessTexture.texture, out var textureSize, out var textureName))
{
continue;
}
var tempMaterial = new Material(Shader.Find(ShaderConstants.Generate_Mask_Shader_Path));
var tempRT = RenderTexture.GetTemporary(textureSize.x, textureSize.y, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
tempRT.Create();
try
{
SetupAndBlitTexture(metallicTexture, aoTexture, detailMaskTexture, smoothnessTexture, tempRT, tempMaterial);
var texturePath = Path.Combine(group.Value.outputDirectory, textureName + Constants.Extensions.PNG);
TextureHelpers.ExportRenderTextureToPNG(tempRT, texturePath);
AssetDatabase.ImportAsset(texturePath);
UnityEngine.Object.DestroyImmediate(material);
}
finally
{
UnityEngine.Object.DestroyImmediate(tempMaterial);
RenderTexture.ReleaseTemporary(tempRT);
}
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
finally
{
foreach (var group in groupConfigs)
{
group.Value.Dispose();
}
}
}
private static Texture2D FindTexture(NativeList<FixedString64Bytes> texturePaths, string metallicTextureRegex)
private TextureChannel GetTextureTargetChannel(Texture2D texture)
{
foreach (var texturePath in texturePaths)
var textureMapping = new Dictionary<Regex, TextureChannel>
{
if (Regex.IsMatch(texturePath.ToString(), metallicTextureRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled))
{ new Regex(metallicTextureRegex, _regexOptions), TextureChannel.R },
{ new Regex(aoTextureRegex, _regexOptions), TextureChannel.G },
{ new Regex(detailMaskTextureRegex, _regexOptions), TextureChannel.B },
{ new Regex(smoothnessTextureRegex, _regexOptions), TextureChannel.A }
};
foreach (var entry in textureMapping)
{
if (entry.Key.IsMatch(texture.name))
{
return AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath.ToString());
return entry.Value;
}
}
return null;
return TextureChannel.None;
}
private void GetTextureSizeAndName(Texture2D metallicTexture, Texture2D aoTexture, Texture2D detailMaskTexture, out Vector2Int textureSize, out string textureName)
private bool TryGetTextureSizeAndName(Texture2D textureR, Texture2D textureG, Texture2D textureB, Texture2D textureA, out Vector2Int textureSize, out string textureName)
{
textureSize = new Vector2Int(2048, 2048);
textureName = string.Empty;
switch (namingSource)
{
case TextureChannel.R:
textureSize = new Vector2Int(metallicTexture.width, metallicTexture.height);
textureName = Regex.Replace(metallicTexture.name, namingRegex, replaceBy, RegexOptions.IgnoreCase | RegexOptions.Compiled);
break;
return TryGetTextureDetails(textureR, out textureSize, out textureName);
case TextureChannel.G:
textureSize = new Vector2Int(aoTexture.width, aoTexture.height);
textureName = Regex.Replace(aoTexture.name, namingRegex, replaceBy, RegexOptions.IgnoreCase | RegexOptions.Compiled);
break;
return TryGetTextureDetails(textureG, out textureSize, out textureName);
case TextureChannel.B:
textureSize = new Vector2Int(detailMaskTexture.width, detailMaskTexture.height);
textureName = Regex.Replace(detailMaskTexture.name, namingRegex, replaceBy, RegexOptions.IgnoreCase | RegexOptions.Compiled);
break;
return TryGetTextureDetails(textureB, out textureSize, out textureName);
case TextureChannel.A:
textureSize = new Vector2Int(detailMaskTexture.width, detailMaskTexture.height);
textureName = Regex.Replace(detailMaskTexture.name, namingRegex, replaceBy, RegexOptions.IgnoreCase | RegexOptions.Compiled);
break;
return TryGetTextureDetails(textureA, out textureSize, out textureName);
}
textureSize = Vector2Int.one;
textureName = string.Empty;
return false;
}
private void SetupAndBlitTexture(Texture2D textureR, Texture2D textureG, Texture2D textureB, Texture2D textureA, Vector2Int textureSize, out RenderTexture tempRT, out Material material)
private bool TryGetTextureDetails(Texture2D textureR, out Vector2Int textureSize, out string textureName)
{
tempRT = RenderTexture.GetTemporary(textureSize.x, textureSize.y, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
tempRT.Create();
textureSize = Vector2Int.one;
textureName = string.Empty;
material = new Material(Shader.Find(ShaderConstants.Generate_Mask_Shader_Path));
if (textureR != null)
if (textureR == null)
{
material.SetTexture(ShaderConstants.Texture_R_Property, textureR);
material.EnableKeyword(ShaderConstants.Has_Texture_R_Keyword);
return false;
}
if (textureG != null)
textureSize = new Vector2Int(textureR.width, textureR.height);
textureName = Regex.Replace(textureR.name, namingRegex, replaceBy, _regexOptions);
return true;
}
private void SetupAndBlitTexture(TextureSource textureSourceR, TextureSource textureSourceG, TextureSource textureSourceB, TextureSource textureSourceA, RenderTexture tempRT, Material material)
{
if (textureSourceR.texture != null)
{
material.SetTexture(ShaderConstants.Texture_G_Property, textureG);
material.EnableKeyword(ShaderConstants.Has_Texture_G_Keyword);
material.SetTexture(ShaderConstants.Metallic_Property, textureSourceR.texture);
material.EnableKeyword(ShaderConstants.Has_Metallic_Keyword);
}
if (textureB != null)
if (textureSourceG.texture != null)
{
material.SetTexture(ShaderConstants.Texture_B_Property, textureB);
material.EnableKeyword(ShaderConstants.Has_Texture_B_Keyword);
material.SetTexture(ShaderConstants.Ao_Property, textureSourceG.texture);
material.EnableKeyword(ShaderConstants.Has_Ao_Keyword);
}
if (textureA != null)
if (textureSourceB.texture != null)
{
material.SetTexture(ShaderConstants.Texture_A_Property, textureA);
material.EnableKeyword(ShaderConstants.Has_Texture_A_Keyword);
material.SetTexture(ShaderConstants.Detail_Property, textureSourceB.texture);
material.EnableKeyword(ShaderConstants.Has_Detail_Keyword);
}
if (textureSourceA.texture != null)
{
material.SetTexture(ShaderConstants.Smoothness_Property, textureSourceA.texture);
if (textureSourceA.isRoughness)
{
material.EnableKeyword(ShaderConstants.Has_Roughness_Keyword);
}
else
{
material.EnableKeyword(ShaderConstants.Has_Smoothness_Keyword);
}
}
var fallback = new Vector4(

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Unity.Properties;
using UnityEngine.UIElements;
@@ -6,41 +7,133 @@ namespace Misaki.ArtToolEditor
{
internal class GenerateMaskVisualProvider : OptionsVisualProvider
{
public override VisualElement ContentAfterList()
private const string Disable_String = "Disable";
private const string Enable_String = "Enable";
public override void ContentAfterList(VisualElement rootVisualElement)
{
var root = new VisualElement();
rootVisualElement.dataSource = processor;
root.Add(new HelpBox("All regex are case insensitive", HelpBoxMessageType.Info));
rootVisualElement.Add(new HelpBox("All regex are case insensitive", HelpBoxMessageType.Info));
root.Add(CreateTextInputWithEnumField("Metallic Regex",
rootVisualElement.Add(CreateTextInputWithEnumField("Metallic Regex",
nameof(GenerateMaskProcessor.metallicTextureRegex),
nameof(GenerateMaskProcessor.metallicFallbackType),
TextureFallbackType.Black));
root.Add(CreateTextInputWithEnumField("AO Regex",
rootVisualElement.Add(CreateTextInputWithEnumField("AO Regex",
nameof(GenerateMaskProcessor.aoTextureRegex),
nameof(GenerateMaskProcessor.aoFallbackType),
TextureFallbackType.White));
root.Add(CreateTextInputWithEnumField("Detail Mask Regex",
rootVisualElement.Add(CreateTextInputWithEnumField("Detail Mask Regex",
nameof(GenerateMaskProcessor.detailMaskTextureRegex),
nameof(GenerateMaskProcessor.detailMaskFallbackType),
TextureFallbackType.White));
root.Add(CreateTextInputWithEnumField("Smoothness Regex",
rootVisualElement.Add(CreateTextInputWithEnumField("Smoothness Regex",
nameof(GenerateMaskProcessor.smoothnessTextureRegex),
nameof(GenerateMaskProcessor.smoothnessFallbackType),
TextureFallbackType.LinearGray));
root.Add(CreateTextInputField("Grouping Regex",
var roughnessFallbackDropdown = new DropdownField("Roughness Fallback", new List<string> { Disable_String, Enable_String }, Disable_String);
roughnessFallbackDropdown.SetBinding(nameof(DropdownField.index), new DataBinding() { dataSourcePath = PropertyPath.FromName(nameof(GenerateMaskProcessor.isFallbackToRoughness)) });
var roughnessRegexInput = CreateTextInputField("Roughness Regex",
nameof(GenerateMaskProcessor.roughnessTextureRegex));
roughnessFallbackDropdown.RegisterValueChangedCallback((evt) =>
{
roughnessRegexInput.style.display = evt.newValue == Disable_String ? DisplayStyle.None : DisplayStyle.Flex;
});
rootVisualElement.Add(roughnessFallbackDropdown);
rootVisualElement.Add(roughnessRegexInput);
rootVisualElement.Add(CreateTextInputField("Grouping Regex",
nameof(GenerateMaskProcessor.textureGroupingRegex)));
root.Add(CreateTextInputWithEnumField("Naming Regex",
rootVisualElement.Add(CreateTextInputWithEnumField("Naming Regex",
nameof(GenerateMaskProcessor.namingRegex),
nameof(GenerateMaskProcessor.namingSource),
TextureChannel.R));
root.Add(CreateTextInputField("Replace By",
rootVisualElement.Add(CreateTextInputField("Replace By",
nameof(GenerateMaskProcessor.replaceBy)));
}
return root;
public override void ContentOnRight(VisualElement rootVisualElement)
{
rootVisualElement.dataSource = processor;
rootVisualElement.style.width = 300;
var container = new VisualElement();
container.StretchToParentSize();
var title = new Label("Grouping Preview")
{
style =
{
marginTop = 4,
marginBottom = 2
}
};
var groupTreeView = new TreeView()
{
selectionType = SelectionType.None,
reorderable = false,
horizontalScrollingEnabled = true,
style =
{
flexGrow = 1,
}
};
var previewButton = new Button()
{
text = "Preview Grouping Result"
};
previewButton.clicked += () =>
{
if (processor is not GenerateMaskProcessor generateMaskProcessor)
{
return;
}
var result = generateMaskProcessor.CreatePreviewGroupingResult(sourceObjects);
var previewTreeRoot = new List<TreeViewItemData<string>>();
var ignoredGroup = default(TreeViewItemData<string>);
var index = 0;
foreach (var item in result)
{
var children = new List<TreeViewItemData<string>>(4);
foreach (var textureSource in item.Value.textureSources)
{
var roughnessString = textureSource.isRoughness ? " as roughness" : string.Empty;
children.Add(new TreeViewItemData<string>(index++, $"{textureSource.texture.name} to channel {textureSource.channel}" + roughnessString));
}
var groupItem = new TreeViewItemData<string>(index++, item.Key, children);
if (item.Key == GenerateMaskProcessor.IGNORED_GROUP_NAME)
{
ignoredGroup = groupItem;
continue;
}
previewTreeRoot.Add(new TreeViewItemData<string>(index++, item.Key, children));
}
if (ignoredGroup.children != null)
{
previewTreeRoot.Add(ignoredGroup);
}
groupTreeView.SetRootItems(previewTreeRoot);
groupTreeView.Rebuild();
};
container.Add(title);
container.Add(groupTreeView);
container.Add(previewButton);
rootVisualElement.Add(container);
}
private static VisualElement CreateTextInputField(string label, string binding)

View File

@@ -0,0 +1,3 @@
.accent-brush {
background-color: rgba(255, 255, 255, 0.25);
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: adf3dce32e0b15d408c37eb8c41df691
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -1,28 +1,36 @@
<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="_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" style="display: none;">
<Bindings>
<engine:DataBinding property="value" data-source-path="_customPath" binding-mode="TwoWay" />
</Bindings>
</engine:TextField>
<engine:VisualElement name="content-before-button" />
<engine:Button text="Execute" name="output-button" />
<engine:VisualElement name="content-on-bottom" />
<Style src="project://database/Packages/com.misaki.art-tools/Editor/AssetsHelpers/View/OutputOptionsStyle.uss?fileID=7433441132597879392&amp;guid=adf3dce32e0b15d408c37eb8c41df691&amp;type=3#OutputOptionsStyle" />
<engine:VisualElement data-source-type="Misaki.ArtToolEditor.OutputOptionsWindow, Misaki.ArtTool.Editor" style="flex-grow: 1; padding-top: 4px; padding-right: 4px; padding-bottom: 4px; padding-left: 4px;">
<engine:ScrollView style="flex-grow: 1;">
<engine:VisualElement style="flex-grow: 1; flex-direction: row;">
<engine:VisualElement name="content-on-left" style="flex-grow: 1; overflow: hidden;">
<engine:VisualElement name="content-on-top" />
<engine:Foldout text="Source" style="margin-bottom: 4px; padding-right: 4px;">
<engine:ListView header-title="Source Objects" selection-type="None" allow-add="false" allow-remove="false" 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="_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" style="display: none;">
<Bindings>
<engine:DataBinding property="value" data-source-path="_customPath" binding-mode="TwoWay" />
</Bindings>
</engine:TextField>
<engine:VisualElement name="content-before-button" />
</engine:VisualElement>
<engine:VisualElement name="content-on-right" style="height: 100%;" />
</engine:VisualElement>
</engine:ScrollView>
<engine:VisualElement style="margin-top: 8px;">
<engine:Button text="Output" name="output-button" class="accent-brush" />
<engine:VisualElement name="content-on-bottom" />
</engine:VisualElement>
</engine:VisualElement>
</engine:UXML>

View File

@@ -23,9 +23,14 @@ namespace Misaki.ArtToolEditor
[SerializeField]
private IEnumerable<Object> _sourceObjects;
private EnumField _directoryTypeEnum;
private TextField _customPathInput;
private Button _outputButton;
private VisualElement _contentOnTop;
private VisualElement _contentAfterList;
private VisualElement _contentBeforeButton;
private VisualElement _contentOnRight;
private VisualElement _contentOnBottom;
private OptionsVisualProvider _visualProvider;
@@ -42,6 +47,7 @@ namespace Misaki.ArtToolEditor
private void CreateGUI()
{
var outputOptionsElement = _visualTreeAsset.Instantiate();
outputOptionsElement.StretchToParentSize();
SetupOutputElements(outputOptionsElement);
@@ -60,16 +66,16 @@ namespace Misaki.ArtToolEditor
objectField.value = _sourceObjects.ElementAt(i);
};
var directoryTypeEnum = outPutOptions.Q<EnumField>("directory-type-enum");
var customPathInput = outPutOptions.Q<TextField>("custom-path-input");
_directoryTypeEnum = outPutOptions.Q<EnumField>("directory-type-enum");
_customPathInput = outPutOptions.Q<TextField>("custom-path-input");
directoryTypeEnum.RegisterValueChangedCallback((evt) =>
_directoryTypeEnum.RegisterValueChangedCallback((evt) =>
{
customPathInput.style.display = evt.newValue.Equals(OutputDirectoryType.Custom) ? DisplayStyle.Flex : DisplayStyle.None;
_customPathInput.style.display = evt.newValue.Equals(OutputDirectoryType.Custom) ? DisplayStyle.Flex : DisplayStyle.None;
});
var outputButton = outPutOptions.Q<Button>("output-button");
outputButton.clickable.clicked += () =>
_outputButton = outPutOptions.Q<Button>("output-button");
_outputButton.clickable.clicked += () =>
{
if (_processor == null)
{
@@ -107,63 +113,57 @@ namespace Misaki.ArtToolEditor
_contentOnTop = outPutOptions.Q<VisualElement>("content-on-top");
_contentAfterList = outPutOptions.Q<VisualElement>("content-after-list");
_contentBeforeButton = outPutOptions.Q<VisualElement>("content-before-button");
_contentOnRight = outPutOptions.Q<VisualElement>("content-on-right");
_contentOnBottom = outPutOptions.Q<VisualElement>("content-on-bottom");
}
public void InitializeAndShow()
{
_context.sourceObjects = _sourceObjects;
RecreateCustomOptions(_processor);
ShowUtility();
}
private void RecreateCustomOptions(object dataSource)
public void InitializeAndShow(bool hasOutput = true)
{
if (_visualProvider == null)
{
return;
}
_context.sourceObjects = _sourceObjects;
_visualProvider.sourceObjects = _sourceObjects;
if (!hasOutput)
{
_outputButton.text = "Execute";
_directoryTypeEnum.style.display = DisplayStyle.None;
_customPathInput.style.display = DisplayStyle.None;
}
RecreateCustomOptions(_processor);
ShowUtility();
}
private void RecreateCustomOptions(IAssetsProcessor processor)
{
_contentOnTop.Clear();
_contentAfterList.Clear();
_contentBeforeButton.Clear();
_contentOnRight.Clear();
_contentOnBottom.Clear();
_visualProvider.dataSource = dataSource;
_contentOnTop.Add(_visualProvider.ContentOnTop());
_contentAfterList.Add(_visualProvider.ContentAfterList());
_contentBeforeButton.Add(_visualProvider.ContentBeforeButton());
_contentOnBottom.Add(_visualProvider.ContentOnBottom());
_contentOnTop.dataSource = dataSource;
_contentAfterList.dataSource = dataSource;
_contentBeforeButton.dataSource = dataSource;
_contentOnBottom.dataSource = dataSource;
_visualProvider.processor = processor;
_visualProvider.ContentOnTop(_contentOnTop);
_visualProvider.ContentAfterList(_contentAfterList);
_visualProvider.ContentBeforeButton(_contentBeforeButton);
_visualProvider.ContentOnRight(_contentOnRight);
_visualProvider.ContentOnBottom(_contentOnBottom);
}
private string GetOutputDirectory(string inputDirectory)
{
switch (_outputDirectoryType)
return _outputDirectoryType switch
{
case OutputDirectoryType.CurrentDirectory:
return inputDirectory;
case OutputDirectoryType.ParentDirectory:
return Path.GetDirectoryName(inputDirectory);
case OutputDirectoryType.AssetsDirectory:
return "Assets";
case OutputDirectoryType.Custom:
if (Path.IsPathRooted(_customPath))
{
return _customPath;
}
else
{
return Path.Combine(inputDirectory, _customPath);
}
default:
return inputDirectory;
}
OutputDirectoryType.CurrentDirectory => inputDirectory,
OutputDirectoryType.ParentDirectory => Path.GetDirectoryName(inputDirectory),
OutputDirectoryType.AssetsDirectory => "Assets",
OutputDirectoryType.Custom => Path.IsPathRooted(_customPath) ? _customPath : Path.Combine(inputDirectory, _customPath),
_ => inputDirectory
};
}
}
}