Загрузка данных


#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.IO;
using System;
using System.Linq;

namespace MarieIT.MaskMapGenerator
{
    public class HDRPMaskMapGenerator : EditorWindow
    {
        private enum Tab { MaskMapGenerator, TextureManipulator}
        
        [Header("Textures")]
        private Texture2D sourceTexture;
        private Texture2D generatedPreview;

        [Header("Vectors")]
        private Vector2 scrollPosition;

        [Header("Saving Options")]
        private bool saveMetallic = true;
        private bool saveAO = true;
        private bool saveDetail = true;
        private bool saveSmoothness = true;
        // Normal Map 
        private bool saveNormalMap = true;
        private float normalStrength = 1.0f;

        [Header("Destination")]
        private string destinationFolderRelative = ""; // Main Folder Path
        private string subfolderName = "";             // Subfolder Path
        private bool createSubFolder = false;

        // Creating the Editor Menu
        [MenuItem("Tools/MarieIT/Mask Map Generator")]
        public static void ShowWindow() { GetWindow<HDRPMaskMapGenerator>("HDRP Mask Map Generator"); }

        private void OnEnable()
        {
            if (string.IsNullOrEmpty(destinationFolderRelative)) 
                destinationFolderRelative = "Assets/MarieIT/HDRP Mask Map Generator/Demo/Textures/MaskMaps";

            if (normalStrength <= 0) normalStrength = 1.0f;

            if (string.IsNullOrEmpty(subfolderName))
                subfolderName = "MaskMaps_" + DateTime.Now.ToString("yyyyMMdd");
       }

        private void OnGUI()
        {
            scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

            DrawHeader();
            DrawTextureGenerator();

            EditorGUILayout.EndScrollView();
        }

        #region Main GUI

        private void DrawHeader()
        {
            EditorGUILayout.LabelField("HDRP Mask Map Generator v1.1.0", EditorStyles.boldLabel);
            EditorGUILayout.HelpBox(
                "This Tool creates a Mask Map from a single texture and optionally generates/saves supporting textures (Metallic, AO, Detail, Smoothness).\n\n" +
                "Key improvements: progress + cancel for large textures, choose which textures to save, pick destination folder inside your project, and organized subfolder per source texture.", MessageType.Info);
            EditorGUILayout.Space(8);
        }

        private void DrawTextureGenerator()
        {
            EditorGUILayout.LabelField("Source Texture", EditorStyles.boldLabel);
            sourceTexture = (Texture2D)EditorGUILayout.ObjectField("Source Texture", sourceTexture, typeof(Texture2D), false);

            EditorGUILayout.Space(6);

            EditorGUILayout.LabelField("Which generated textures should be saved to disk?", EditorStyles.boldLabel);
            saveMetallic = EditorGUILayout.ToggleLeft("Save Metallic (R)", saveMetallic);
            saveAO = EditorGUILayout.ToggleLeft("Save AO (G)", saveAO);
            saveDetail = EditorGUILayout.ToggleLeft("Save Detail (B)", saveDetail);
            saveSmoothness = EditorGUILayout.ToggleLeft("Save Smoothness (A)", saveSmoothness);
            
            EditorGUILayout.Space(5);
            saveNormalMap = EditorGUILayout.ToggleLeft("Save Normal Map", saveNormalMap);
            if (saveNormalMap)
            {
                EditorGUI.indentLevel++;
                EditorGUILayout.HelpBox(
                    "Generates a normal map based on the texture’s brightness values (height map). " +
                    "Higher strength = more pronounced surface detail. Recommend: 1.0-2.0", 
                    MessageType.Info);
                normalStrength = EditorGUILayout.Slider("Normal Map Strength", normalStrength, 0.1f, 5.0f);
                EditorGUI.indentLevel--;
            }

            EditorGUILayout.Space(6);

            DrawFolderSelector("Destination Folder", ref destinationFolderRelative);

            EditorGUILayout.Space(6);
            if (GUILayout.Button($"Create Subfolder - {(createSubFolder ? "Activated" : "Not activated")}", GUILayout.Width(250))) createSubFolder = createSubFolder ? false : true;

            if (createSubFolder) 
            {
                EditorGUILayout.LabelField("Subfolder (optional)", EditorStyles.boldLabel);
                subfolderName = EditorGUILayout.TextField(subfolderName);
            }

            EditorGUILayout.Space(10);

            GUI.backgroundColor = new Color(0.2f, 0.8f, 0.2f);
            if (GUILayout.Button("Generate Texture/s", GUILayout.Height(40)))
            {
                if (ValidateInput())
                {
                    try { ProcessTextures(); }
                    catch (OperationCanceledException)
                    {
                        EditorUtility.DisplayDialog("Cancelled", "Texture generation was cancelled by the user.", "OK");
                    }
                    catch (Exception ex)
                    {
                        EditorUtility.DisplayDialog("Error", "An unexpected error occurred: " + ex.Message, "OK");
                    }
                }
            }
            GUI.backgroundColor = Color.white;

            EditorGUILayout.Space(10);

            DrawPreview("Mask Map Preview", generatedPreview);
        }

        #endregion

        #region Helpers

        #region Validaters

        private bool ValidateInput()
        {
            if (sourceTexture == null)
            {
                EditorUtility.DisplayDialog("Missing Texture", "Please assign a Source Texture before starting.", "OK");
                return false;
            }

            if (string.IsNullOrEmpty(destinationFolderRelative) || !destinationFolderRelative.StartsWith("Assets"))
            {
                EditorUtility.DisplayDialog("Invalid Destination", "Destination folder must be inside the project's Assets folder. Set it before continuing.", "OK");
                return false;
            }

            if (sourceTexture.width * sourceTexture.height > 4096 * 4096)
            {
                return EditorUtility.DisplayDialog("Large Texture Warning",
                    "This texture is very large and may take a while to process. Continue?",
                    "Yes", "Cancel");
            }

            return true;
        }

        #endregion

        #region Generation

        private void ProcessTextures()
        {
            if (!MakeTextureReadable(sourceTexture)) return;

            string sourceName = Path.GetFileNameWithoutExtension(sourceTexture.name);
            string destBase = destinationFolderRelative.Replace("\\", "/");

            if (!AssetDatabase.IsValidFolder(destBase))
                CreateFolderRecursively(destBase);
            
            string finalDest = destBase;
            if (!string.IsNullOrEmpty(subfolderName) && createSubFolder)
            {
                finalDest = Path.Combine(destBase, subfolderName).Replace("\\", "/");
                if (!AssetDatabase.IsValidFolder(finalDest))
                    CreateFolderRecursively(finalDest);
            }

            string perSourceFolder = Path.Combine(finalDest, sourceName).Replace("\\", "/");
            if (!AssetDatabase.IsValidFolder(perSourceFolder))
                CreateFolderRecursively(perSourceFolder);

            EditorUtility.DisplayCancelableProgressBar("Generating Textures", "Processing...", 0f);

            int width = sourceTexture.width;
            int height = sourceTexture.height;
            Color32[] originalPixels = sourceTexture.GetPixels32();

            if (originalPixels == null || originalPixels.Length != width * height)
            {
                EditorUtility.ClearProgressBar();
                EditorUtility.DisplayDialog("Error", "Failed to read pixels from the source texture.", "OK");
                return;
            }

            Color32[] maskPixels = new Color32[originalPixels.Length];
            Color32[] metallicPixels = saveMetallic ? new Color32[originalPixels.Length] : null;
            Color32[] aoPixels = saveAO ? new Color32[originalPixels.Length] : null;
            Color32[] detailPixels = saveDetail ? new Color32[originalPixels.Length] : null;
            Color32[] smoothPixels = saveSmoothness ? new Color32[originalPixels.Length] : null;

            for (int y = 0; y < height; y++)
            {
                int rowStart = y * width;
                for (int x = 0; x < width; x++)
                {
                    int i = rowStart + x;
                    Color32 p = originalPixels[i];

                    int grey = (int)(0.299f * p.r + 0.587f * p.g + 0.114f * p.b);
                    byte greyByte = (byte)Mathf.Clamp(grey, 0, 255);

                    byte metallicByte = 0;
                    byte aoByte = (byte)(255 - greyByte);
                    byte detailByte = greyByte;
                    byte smoothByte = greyByte;

                    maskPixels[i] = new Color32(metallicByte, aoByte, detailByte, smoothByte);

                    if (saveMetallic) metallicPixels[i] = new Color32(metallicByte, metallicByte, metallicByte, 255);
                    if (saveAO) aoPixels[i] = new Color32(aoByte, aoByte, aoByte, 255);
                    if (saveDetail) detailPixels[i] = new Color32(detailByte, detailByte, detailByte, 255);
                    if (saveSmoothness) smoothPixels[i] = new Color32(smoothByte, smoothByte, smoothByte, 255);
                }

                float progress = (float)y / height * 0.8f;
                if (EditorUtility.DisplayCancelableProgressBar("Generating Textures", $"Row {y+1}/{height}", progress))
                {
                    EditorUtility.ClearProgressBar();
                    throw new OperationCanceledException();
                }
            }

            Texture2D maskMap = new Texture2D(width, height, TextureFormat.RGBA32, false);
            maskMap.SetPixels32(maskPixels);
            maskMap.Apply(false, false);
            generatedPreview = maskMap;
            Repaint();

            string maskPath = Path.Combine(perSourceFolder, sourceName + "_MaskMap.png").Replace("\\", "/");
            WriteTextureToPngAndImport(maskMap, maskPath, false, false);
            DestroyImmediate(maskMap);

            if (saveMetallic) SaveTexture(metallicPixels, width, height, perSourceFolder, sourceName + "_Metallic.png");
            if (saveAO) SaveTexture(aoPixels, width, height, perSourceFolder, sourceName + "_AO.png");
            if (saveDetail) SaveTexture(detailPixels, width, height, perSourceFolder, sourceName + "_Detail.png");
            if (saveSmoothness) SaveTexture(smoothPixels, width, height, perSourceFolder, sourceName + "_Smoothness.png");

            if (saveNormalMap)
            {
                Texture2D normalMap = GenerateNormalMap(sourceTexture, normalStrength);
                string normalPath = Path.Combine(perSourceFolder, sourceName + "_Normal.png").Replace("\\", "/");
                SaveNormalMap(normalMap, normalPath);
                DestroyImmediate(normalMap);
            }

            EditorUtility.ClearProgressBar();
            AssetDatabase.Refresh();
            EditorUtility.DisplayDialog("Success!", $"Mask Map Saved to:\n{perSourceFolder}", "OK");

            maskPixels = null;
            metallicPixels = null;
            aoPixels = null;
            detailPixels = null;
            smoothPixels = null;
            originalPixels = null;

            GC.Collect();
            Resources.UnloadUnusedAssets();
        }

        private Texture2D GenerateNormalMap(Texture2D source, float strength)
        {
            int width = source.width;
            int height = source.height;
            Color[] sourcePixels = source.GetPixels();
            Color[] normalPixels = new Color[width * height];

            for (int y = 0; y < height; y++)
            {
                if (y % 10 == 0)
                {
                    float progress = 0.8f + (0.2f * y / height);
                    if (EditorUtility.DisplayCancelableProgressBar("Generating Textures",
                        $"Generating Normal Map... {y}/{height}", progress))
                    {
                        EditorUtility.ClearProgressBar();
                        throw new OperationCanceledException();
                    }
                }

                for (int x = 0; x < width; x++)
                {
                    float left = GetHeightValue(sourcePixels, x - 1, y, width, height);
                    float right = GetHeightValue(sourcePixels, x + 1, y, width, height);
                    float up = GetHeightValue(sourcePixels, x, y + 1, width, height);
                    float down = GetHeightValue(sourcePixels, x, y - 1, width, height);

                    // Gradient calculation
                    float dx = (right - left) * strength;
                    float dy = (up - down) * strength;
                    
                    // Normal Vector
                    float length = Mathf.Sqrt(dx * dx + dy * dy + 1.0f);
                    float nx = -dx / length;
                    float ny = -dy / length;
                    float nz = 1.0f / length;

                    // Convert to color (0-1 range to 0-255 in texture)
                    normalPixels[y * width + x] = new Color(
                        nx * 0.5f + 0.5f,
                        ny * 0.5f + 0.5f,
                        nz * 0.5f + 0.5f,
                        1.0f
                    );
                }
            }

            Texture2D normalMap = new Texture2D(width, height, TextureFormat.RGBA32, true);
            normalMap.SetPixels(normalPixels);
            normalMap.Apply();

            return normalMap;
        }

        private void WriteTextureToPngAndImport(Texture2D tex, string assetRelativePath, bool isSRGB, bool makeReadable)
        {
            string projectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
            string absPath = Path.Combine(projectPath, assetRelativePath).Replace("\\", "/");
            string dir = Path.GetDirectoryName(absPath).Replace("\\", "/");
            
            if (!Directory.Exists(dir))
                Directory.CreateDirectory(dir);

            File.WriteAllBytes(absPath, tex.EncodeToPNG());
            
            string rel = assetRelativePath.StartsWith("Assets") ? assetRelativePath : ("Assets" + absPath.Substring(Application.dataPath.Length));
            rel = rel.Replace("\\", "/");

            AssetDatabase.ImportAsset(rel);

            TextureImporter importer = AssetImporter.GetAtPath(rel) as TextureImporter;
            if (importer != null)
            {
                importer.sRGBTexture = isSRGB;
                importer.isReadable = makeReadable;
                importer.textureType = TextureImporterType.Default;
                importer.SaveAndReimport();
            }
        }

        #endregion

        #region Saving

        private void SaveTexture(Color32[] pixels, int w, int h, string folder, string name)
        {
            Texture2D tex = new Texture2D(w, h, TextureFormat.RGBA32, false);
            tex.SetPixels32(pixels);
            tex.Apply(false, false);
            string path = Path.Combine(folder, name).Replace("\\", "/");
            WriteTextureToPngAndImport(tex, path, true, false);
            DestroyImmediate(tex);
        }

        private void SaveNormalMap(Texture2D normalMap, string assetRelativePath)
        {
            string projectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
            string absPath = Path.Combine(projectPath, assetRelativePath).Replace("\\", "/");
            string dir = Path.GetDirectoryName(absPath).Replace("\\", "/");

            if (!Directory.Exists(dir))
                Directory.CreateDirectory(dir);

            File.WriteAllBytes(absPath, normalMap.EncodeToPNG());

            string rel = assetRelativePath.StartsWith("Assets") ? assetRelativePath : ("Assets" + absPath.Substring(Application.dataPath.Length));
            rel = rel.Replace("\\", "/");

            AssetDatabase.ImportAsset(rel);

            TextureImporter importer = AssetImporter.GetAtPath(rel) as TextureImporter;
            if (importer != null)
            {
                importer.textureType = TextureImporterType.NormalMap;
                importer.isReadable = false;
                importer.textureCompression = TextureImporterCompression.Compressed;
                importer.SaveAndReimport();
            }
        }

        #endregion

        #region Getters

        private float GetHeightValue(Color[] pixels, int x, int y, int width, int height)
        {
            x = Mathf.Clamp(x, 0, width - 1);
            y = Mathf.Clamp(y, 0, height - 1);
            Color pixel = pixels[y * width + x];
            return pixel.grayscale;
        }

        #endregion

        #region Texture Setting

        private bool MakeTextureReadable(Texture2D texture)
        {
            if (texture.isReadable) return true;

            string path = AssetDatabase.GetAssetPath(texture);
            TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
            if (importer != null)
            {
                importer.isReadable = true;
                importer.textureCompression = TextureImporterCompression.Uncompressed; // avoid compression artifacts for processing
                importer.SaveAndReimport();
                AssetDatabase.Refresh();
                return true;
            }

            EditorUtility.DisplayDialog("Error", $"Couldn't make the Texture '{texture.name}' readable. Please activate it manually in the Import Settings.", "OK");
            return false;
        }

        #endregion

        #region Creator

        private void CreateFolderRecursively(string relativePath)
        {
            relativePath = relativePath.Replace("\\", "/");
            if (AssetDatabase.IsValidFolder(relativePath)) return;

            string[] parts = relativePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 0) return;

            string current = parts[0]; // should be "Assets"
            for (int i = 1; i < parts.Length; i++)
            {
                string next = current + "/" + parts[i];
                if (!AssetDatabase.IsValidFolder(next))
                {
                    AssetDatabase.CreateFolder(current, parts[i]);
                }
                current = next;
            }
        }

        #endregion

        #endregion

        #region Utility

        private void DrawFolderSelector(string label, ref string folderPath)
        {
            EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
            EditorGUILayout.BeginHorizontal();
            folderPath = EditorGUILayout.TextField(folderPath);
            if (GUILayout.Button("Choose...", GUILayout.Width(80)))
            {
                string abs = EditorUtility.OpenFolderPanel("Select folder (must be inside Assets)", Application.dataPath, "");
                if (!string.IsNullOrEmpty(abs))
                {
                    string dataPath = Application.dataPath.Replace("\\", "/");
                    abs = abs.Replace("\\", "/");
                    if (abs.StartsWith(dataPath, StringComparison.OrdinalIgnoreCase))
                        folderPath = "Assets" + abs.Substring(dataPath.Length);
                    else 
                    {
                        EditorUtility.DisplayDialog("Invalid Folder", "Please choose a folder inside Assets", "OK");
                        folderPath = "Assets/Generated";
                    }
                }
            }
            EditorGUILayout.EndHorizontal();
        }

        private void DrawPreview(string label, Texture2D preview)
        {
            EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
            if (preview != null)
            {
                float maxWidth = Mathf.Max(64f, position.width - 40f);
                float aspect = (float)preview.width / Mathf.Max(1, preview.height);
                float height = Mathf.Min(512f, maxWidth / aspect);
                Rect rect = EditorGUILayout.GetControlRect(false, height);
                EditorGUI.DrawPreviewTexture(rect, preview, null, ScaleMode.ScaleToFit);
            }
            else
            {
                Rect rect = EditorGUILayout.GetControlRect(false, 64f);
                EditorGUI.LabelField(rect, "Preview will appear here after processing.");
            }
        }

        #endregion

        private void OnDestroy()
        {
            if (generatedPreview != null) { DestroyImmediate(generatedPreview); }
        }
    }
}
#endif