diff --git a/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs b/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs index 6760898..805f4ba 100644 --- a/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs +++ b/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs @@ -26,7 +26,7 @@ void OnPostRender() { // Copies source texture into destination render texture with a shader // Destination RenderTexture is null to blit directly to screen - Graphics.Blit(displayTexture, null, mat); + Graphics.Blit(displayTexture, null as RenderTexture, mat); } } } \ No newline at end of file diff --git a/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs b/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs new file mode 100644 index 0000000..6cc6cc7 --- /dev/null +++ b/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs @@ -0,0 +1,60 @@ +// converts line renderer points from world space to local space + +using UnityEngine; +using UnityEditor; +using UnityEditor.SceneManagement; + +namespace UnityLibrary.ContextMenu +{ + public static class LineRendererToLocalSpace + { + private const string MenuPath = "CONTEXT/LineRenderer/Convert Points To Local Space"; + + [MenuItem(MenuPath, true)] + private static bool Validate(MenuCommand command) + { + return command != null && command.context is LineRenderer; + } + + [MenuItem(MenuPath)] + private static void Convert(MenuCommand command) + { + var lr = (LineRenderer)command.context; + if (lr == null) return; + + int count = lr.positionCount; + if (count == 0) return; + + Transform t = lr.transform; + + Undo.RecordObject(lr, "Convert LineRenderer To Local Space"); + + // Get current positions in world space no matter what mode it's in. + Vector3[] world = new Vector3[count]; + if (lr.useWorldSpace) + { + lr.GetPositions(world); + } + else + { + Vector3[] local = new Vector3[count]; + lr.GetPositions(local); + for (int i = 0; i < count; i++) + world[i] = t.TransformPoint(local[i]); + } + + // Convert world -> local, switch mode, write back. + Vector3[] newLocal = new Vector3[count]; + for (int i = 0; i < count; i++) + newLocal[i] = t.InverseTransformPoint(world[i]); + + lr.useWorldSpace = false; + lr.SetPositions(newLocal); + + EditorUtility.SetDirty(lr); + + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(lr.gameObject.scene); + } + } +} diff --git a/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs b/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs new file mode 100644 index 0000000..90cc2d2 --- /dev/null +++ b/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs @@ -0,0 +1,54 @@ +// converts LineRenderer points from local space to world space via context menu in Unity Editor + +using UnityEngine; +using UnityEditor; +using UnityEditor.SceneManagement; + +namespace UnityLibrary.ContextMenu +{ + public static class LineRendererToWorldSpace + { + private const string MenuPath = "CONTEXT/LineRenderer/Convert Points To World Space"; + + [MenuItem(MenuPath, true)] + private static bool Validate(MenuCommand command) + { + return command != null && command.context is LineRenderer; + } + + [MenuItem(MenuPath)] + private static void Convert(MenuCommand command) + { + var lr = (LineRenderer)command.context; + if (lr == null) return; + + if (lr.useWorldSpace) + { + Debug.Log("LineRenderer is already using World Space."); + return; + } + + int count = lr.positionCount; + if (count == 0) return; + + Transform t = lr.transform; + + Undo.RecordObject(lr, "Convert LineRenderer To World Space"); + + Vector3[] local = new Vector3[count]; + lr.GetPositions(local); + + Vector3[] world = new Vector3[count]; + for (int i = 0; i < count; i++) + world[i] = t.TransformPoint(local[i]); + + lr.useWorldSpace = true; + lr.SetPositions(world); + + EditorUtility.SetDirty(lr); + + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(lr.gameObject.scene); + } + } +} diff --git a/Assets/Scripts/Editor/PackageManager/PackageManagerFavorites.cs b/Assets/Scripts/Editor/PackageManager/PackageManagerFavorites.cs new file mode 100644 index 0000000..1ccc0f1 --- /dev/null +++ b/Assets/Scripts/Editor/PackageManager/PackageManagerFavorites.cs @@ -0,0 +1,326 @@ +// adds custom buttons into PackageManager left panel (tested in 6.5) +// edit your own favorites in the code + +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; +using UnityEngine; +using UnityEngine.UIElements; + +namespace UnityLibrary.Editor.Tools +{ + + [InitializeOnLoad] + public static class PackageManagerFavorites + { + private const string InjectedElementName = "kelobyte-package-manager-dummy-panel"; + private static double nextScanTime; + private static AddRequest addRequest; + private static string pendingPackageName; + + static PackageManagerFavorites() + { + EditorApplication.update += OnEditorUpdate; + } + + [MenuItem("Window/Package Management/Open Package Manager With Dummy Button")] + private static void OpenPackageManagerAndInject() + { + Type windowType = GetPackageManagerWindowType(); + + if (windowType == null) + { + Debug.LogWarning("Package Manager window type was not found."); + return; + } + + EditorWindow window = EditorWindow.GetWindow(windowType); + window.Show(); + + EditorApplication.delayCall += TryInject; + } + + private static void OnEditorUpdate() + { + if (EditorApplication.timeSinceStartup < nextScanTime) + return; + + nextScanTime = EditorApplication.timeSinceStartup + 0.5; + TryInject(); + } + + private static void TryInject() + { + Type windowType = GetPackageManagerWindowType(); + + if (windowType == null) + return; + + UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(windowType); + + foreach (UnityEngine.Object obj in windows) + { + EditorWindow window = obj as EditorWindow; + + if (window == null) + continue; + + InjectIntoWindow(window); + } + } + + private static Type GetPackageManagerWindowType() + { + Type type = Type.GetType("UnityEditor.PackageManager.UI.PackageManagerWindow, UnityEditor.PackageManagerUIModule"); + + if (type != null) + return type; + + foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + type = assembly.GetType("UnityEditor.PackageManager.UI.PackageManagerWindow"); + + if (type != null) + return type; + } + + return null; + } + + private static void InjectIntoWindow(EditorWindow window) + { + VisualElement root = window.rootVisualElement; + + if (root == null) + return; + + if (root.Q(InjectedElementName) != null) + return; + + VisualElement insertParent = FindInsertParent(root); + int insertIndex = FindInsertIndex(insertParent); + + if (insertParent == null) + return; + + VisualElement panel = CreateDummyPanel(); + + if (insertIndex >= 0 && insertIndex <= insertParent.childCount) + insertParent.Insert(insertIndex, panel); + else + insertParent.Add(panel); + } + + private static VisualElement CreateDummyPanel() + { + VisualElement wrapper = new VisualElement(); + wrapper.name = InjectedElementName; + + wrapper.style.marginTop = 8; + wrapper.style.marginBottom = 8; + wrapper.style.paddingLeft = 6; + wrapper.style.paddingRight = 6; + + Foldout foldout = new Foldout(); + foldout.text = "Favorites"; + foldout.value = true; + + Button newtonsoftButton = CreatePackageButton( + "Newtonsoft Json", + "com.unity.nuget.newtonsoft-json" + ); + + Button gltfastButton = CreatePackageButton( + "Unity glTFast", + "com.unity.cloud.gltfast" + ); + + Button unityGltfButton = CreatePackageButton( + "Khronos UnityGLTF", + "https://github.com/KhronosGroup/UnityGLTF.git" + ); + + Button myEssentials = CreatePackageButton( + "Essentials", + " https://github.com/unitycoder/UnityEditorEssentials.git" + ); + + foldout.Add(newtonsoftButton); + foldout.Add(gltfastButton); + foldout.Add(unityGltfButton); + foldout.Add(myEssentials); + + wrapper.Add(foldout); + + return wrapper; + } + + private static Button CreatePackageButton(string label, string packageNameOrUrl) + { + Button button = new Button(() => + { + AddPackage(packageNameOrUrl); + }); + + button.text = label; + button.style.marginTop = 4; + button.style.height = 24; + + return button; + } + + private static void AddPackage(string packageNameOrUrl) + { + string packageToAdd = packageNameOrUrl.Trim(); + + if (addRequest != null && !addRequest.IsCompleted) + { + Debug.LogWarning("A package add request is already in progress."); + return; + } + + Debug.Log($"Adding package: {packageToAdd}"); + + pendingPackageName = packageToAdd; + addRequest = Client.Add(packageToAdd); + + EditorApplication.update -= MonitorAddPackageRequest; + EditorApplication.update += MonitorAddPackageRequest; + } + + private static void MonitorAddPackageRequest() + { + if (addRequest == null || !addRequest.IsCompleted) + return; + + EditorApplication.update -= MonitorAddPackageRequest; + + if (addRequest.Status == StatusCode.Success) + { + Debug.Log($"Package added: {pendingPackageName}"); + Client.Resolve(); + AssetDatabase.Refresh(); + CompilationPipeline.RequestScriptCompilation(); + } + else + { + string errorMessage = addRequest.Error != null ? addRequest.Error.message : "Unknown error."; + Debug.LogError($"Failed to add package '{pendingPackageName}': {errorMessage}"); + } + + addRequest = null; + pendingPackageName = null; + } + + private static VisualElement FindInsertParent(VisualElement root) + { + Label servicesLabel = FindLabel(root, "Services"); + + if (servicesLabel != null && servicesLabel.parent != null && servicesLabel.parent.parent != null) + return servicesLabel.parent.parent; + + Label cloudLabel = FindLabel(root, "Cloud"); + + if (cloudLabel != null && cloudLabel.parent != null && cloudLabel.parent.parent != null) + return cloudLabel.parent.parent; + + Label sourcesLabel = FindLabel(root, "Sources"); + + if (sourcesLabel != null && sourcesLabel.parent != null && sourcesLabel.parent.parent != null) + return sourcesLabel.parent.parent; + + return FindLikelyLeftPanel(root); + } + + private static int FindInsertIndex(VisualElement parent) + { + if (parent == null) + return -1; + + for (int i = 0; i < parent.childCount; i++) + { + VisualElement child = parent[i]; + + if (ContainsLabel(child, "Services")) + return i + 1; + } + + for (int i = 0; i < parent.childCount; i++) + { + VisualElement child = parent[i]; + + if (ContainsLabel(child, "Cloud")) + return i + 1; + } + + return -1; + } + + private static VisualElement FindLikelyLeftPanel(VisualElement root) + { + List all = new List(); + Collect(root, all); + + foreach (VisualElement element in all) + { + Rect rect = element.worldBound; + + if (rect.width > 120 && rect.width < 260 && rect.height > 250 && rect.x < 250) + return element; + } + + return null; + } + + private static Label FindLabel(VisualElement root, string text) + { + List all = new List(); + Collect(root, all); + + foreach (VisualElement element in all) + { + Label label = element as Label; + + if (label != null && label.text == text) + return label; + } + + return null; + } + + private static bool ContainsLabel(VisualElement root, string text) + { + if (root == null) + return false; + + Label label = root as Label; + + if (label != null && label.text == text) + return true; + + for (int i = 0; i < root.childCount; i++) + { + if (ContainsLabel(root[i], text)) + return true; + } + + return false; + } + + private static void Collect(VisualElement root, List elements) + { + if (root == null) + return; + + elements.Add(root); + + for (int i = 0; i < root.childCount; i++) + Collect(root[i], elements); + } + } +} +#endif diff --git a/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs b/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs new file mode 100644 index 0000000..8a73f3a --- /dev/null +++ b/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs @@ -0,0 +1,513 @@ +// AndroidStoreCaptureTool.cs +// Put this file anywhere under an "Editor" folder. +// Usage: +// 1) Enter Play Mode. +// 2) Open: Tools/Android Store Capture +// 3) Pick output folder and click "Capture All Presets" + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace UnityLibrary.Tools +{ + public class AndroidStoreCaptureTool : EditorWindow + { + [Serializable] + private class Preset + { + public string name; // base file name (without _WxH) + public int width; + public int height; + public CropMode cropMode; + + public Preset(string name, int w, int h, CropMode cropMode) + { + this.name = name; + width = w; + height = h; + this.cropMode = cropMode; + } + } + + private enum CropMode + { + Stretch, // no crop, just scale to target (may distort) + CropToFit // center-crop to target aspect, then scale (no distortion) + } + + private string _outputFolder = "StoreCaptures"; + private int _phoneCount = 2; // Play Console: 2-8 phone screenshots + + // Jobs + private class CaptureJob + { + public Preset preset; + public string filename; + } + + private readonly Queue _queue = new Queue(); + private bool _isRunning; + + // Hidden helper MonoBehaviour that runs coroutines in Play Mode + private CaptureHelper _helper; + + // Presets based on Play Console rules in your message. + // Phone/tablet sizes are common choices within allowed ranges. + private List BuildPresets() + { + var list = new List(); + + // App icon and feature graphic + list.Add(new Preset("appicon", 512, 512, CropMode.CropToFit)); + list.Add(new Preset("featuregraphic", 1024, 500, CropMode.CropToFit)); + + // Phone screenshots (2-8). 9:16 or 16:9. Each side 320..3840. + // We capture portrait by default; toggle to landscape if you want. + for (int i = 1; i <= Mathf.Clamp(_phoneCount, 2, 8); i++) + list.Add(new Preset("phone_" + i.ToString("00"), 1080, 1920, CropMode.CropToFit)); + + // 7-inch tablet screenshots (allowed: 320..3840 each side) + list.Add(new Preset("tablet7_01", 1920, 1200, CropMode.CropToFit)); // landscape 16:10 + list.Add(new Preset("tablet7_02", 1200, 1920, CropMode.CropToFit)); // portrait 10:16 + + // 10-inch tablet screenshots (each side 1080..7680) + list.Add(new Preset("tablet10_01", 2560, 1600, CropMode.CropToFit)); // landscape 16:10 + list.Add(new Preset("tablet10_02", 1600, 2560, CropMode.CropToFit)); // portrait 10:16 + + return list; + } + + [MenuItem("Tools/Android Store Capture")] + public static void Open() + { + var w = GetWindow("Android Store Capture"); + w.minSize = new Vector2(420, 340); + w.Show(); + } + + private void OnDisable() + { + StopRunner(); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("Capture from Game View (Play Mode)", EditorStyles.boldLabel); + + using (new EditorGUILayout.VerticalScope("box")) + { + EditorGUILayout.LabelField("Output", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + _outputFolder = EditorGUILayout.TextField("Folder", _outputFolder); + if (GUILayout.Button("Browse", GUILayout.Width(80))) + { + string picked = EditorUtility.OpenFolderPanel("Pick output folder", Application.dataPath, ""); + if (!string.IsNullOrEmpty(picked)) + { + // Make it project-relative when possible + string proj = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + string full = Path.GetFullPath(picked); + if (full.StartsWith(proj, StringComparison.OrdinalIgnoreCase)) + { + _outputFolder = full.Substring(proj.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + else + { + _outputFolder = full; + } + } + } + EditorGUILayout.EndHorizontal(); + + _phoneCount = EditorGUILayout.IntSlider("Phone screenshots (2-8)", _phoneCount, 2, 8); + } + + using (new EditorGUILayout.VerticalScope("box")) + { + EditorGUILayout.LabelField("Actions", EditorStyles.boldLabel); + + if (!EditorApplication.isPlaying) + { + EditorGUILayout.HelpBox("Enter Play Mode first. This tool captures the rendered Game View.", MessageType.Warning); + } + + GUI.enabled = EditorApplication.isPlaying && !_isRunning; + if (GUILayout.Button("Capture All Presets")) + { + EnqueueAll(); + StartRunner(); + } + + if (GUILayout.Button("Capture Only Icon + Feature Graphic")) + { + EnqueueIconAndFeatureOnly(); + StartRunner(); + } + GUI.enabled = true; + + GUI.enabled = _isRunning; + if (GUILayout.Button("Stop")) + { + StopRunner(); + } + GUI.enabled = true; + + if (_isRunning) + { + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("Running...", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Remaining", _queue.Count.ToString()); + } + } + + using (new EditorGUILayout.VerticalScope("box")) + { + EditorGUILayout.LabelField("Notes", EditorStyles.boldLabel); + EditorGUILayout.LabelField("- Files are named like: appicon_512x512.png, featuregraphic_1024x500.png"); + EditorGUILayout.LabelField("- Phone screenshots are named like: phone_01_1080x1920.png"); + EditorGUILayout.LabelField("- Captures center-crop to match target aspect (no stretching)."); + } + } + + private void EnqueueAll() + { + _queue.Clear(); + + string folder = ResolveOutputFolder(); + Directory.CreateDirectory(folder); + + foreach (var p in BuildPresets()) + { + string fn = $"{p.name}_{p.width}x{p.height}.png"; + _queue.Enqueue(new CaptureJob { preset = p, filename = Path.Combine(folder, fn) }); + } + } + + private void EnqueueIconAndFeatureOnly() + { + _queue.Clear(); + + string folder = ResolveOutputFolder(); + Directory.CreateDirectory(folder); + + var icon = new Preset("appicon", 512, 512, CropMode.CropToFit); + var feature = new Preset("featuregraphic", 1024, 500, CropMode.CropToFit); + + _queue.Enqueue(new CaptureJob { preset = icon, filename = Path.Combine(folder, $"appicon_512x512.png") }); + _queue.Enqueue(new CaptureJob { preset = feature, filename = Path.Combine(folder, $"featuregraphic_1024x500.png") }); + } + + private string ResolveOutputFolder() + { + // If user gave absolute path, use it. Otherwise, place under project root. + if (Path.IsPathRooted(_outputFolder)) + return _outputFolder; + + string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + return Path.Combine(projectRoot, _outputFolder); + } + + private CaptureHelper EnsureHelper() + { + if (_helper != null) return _helper; + + var go = new GameObject("[AndroidStoreCaptureHelper]") + { + hideFlags = HideFlags.HideAndDontSave + }; + _helper = go.AddComponent(); + return _helper; + } + + private void StartRunner() + { + if (_isRunning) return; + if (!EditorApplication.isPlaying) return; + + _isRunning = true; + + GetMainGameView(); + + var helper = EnsureHelper(); + helper.StartCoroutine(RunCaptures()); + } + + private void StopRunner() + { + _isRunning = false; + _queue.Clear(); + + if (_helper != null) + { + _helper.StopAllCoroutines(); + DestroyImmediate(_helper.gameObject); + _helper = null; + } + } + + private IEnumerator RunCaptures() + { + while (_queue.Count > 0) + { + if (!EditorApplication.isPlaying) + { + StopRunner(); + yield break; + } + + var job = _queue.Dequeue(); + + SetGameViewSize(job.preset.width, job.preset.height); + + // Wait for the GameView to resize and re-render + for (int i = 0; i < 6; i++) + yield return null; + + // Wait for end of frame — this is required for ScreenCapture to work + yield return new WaitForEndOfFrame(); + + try + { + ProcessCaptureJob(job); + } + catch (Exception ex) + { + Debug.LogError("Capture failed: " + ex); + } + + Repaint(); + } + + _isRunning = false; + AssetDatabase.Refresh(); + Debug.Log("Android Store Capture: All captures finished."); + Repaint(); + + if (_helper != null) + { + DestroyImmediate(_helper.gameObject); + _helper = null; + } + } + + private void ProcessCaptureJob(CaptureJob job) + { + int targetW = job.preset.width; + int targetH = job.preset.height; + + Texture2D src = ScreenCapture.CaptureScreenshotAsTexture(); + if (src == null) + { + Debug.LogError($"CaptureScreenshotAsTexture returned null for {job.filename}. Skipping."); + return; + } + + Texture2D processed; + if (job.preset.cropMode == CropMode.CropToFit) + processed = CropToAspectThenScale(src, targetW, targetH); + else + processed = ScaleTexture(src, targetW, targetH); + + byte[] png = processed.EncodeToPNG(); + File.WriteAllBytes(job.filename, png); + + DestroyImmediate(src); + if (processed != src) + DestroyImmediate(processed); + + Debug.Log("Saved: " + job.filename); + } + + private static Texture2D CropToAspectThenScale(Texture2D src, int targetW, int targetH) + { + float srcAspect = (float)src.width / src.height; + float dstAspect = (float)targetW / targetH; + + int cropW = src.width; + int cropH = src.height; + + if (srcAspect > dstAspect) + { + // too wide -> crop width + cropW = Mathf.RoundToInt(src.height * dstAspect); + cropH = src.height; + } + else + { + // too tall -> crop height + cropW = src.width; + cropH = Mathf.RoundToInt(src.width / dstAspect); + } + + // Crop from top-left: x starts at 0, y starts from top + int x0 = 0; + int y0 = src.height - cropH; + + Color[] pixels = src.GetPixels(x0, y0, cropW, cropH); + Texture2D cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, false); + cropped.SetPixels(pixels); + cropped.Apply(false, false); + + Texture2D scaled = ScaleTexture(cropped, targetW, targetH); + DestroyImmediate(cropped); + return scaled; + } + + private static Texture2D ScaleTexture(Texture2D src, int targetW, int targetH) + { + Texture2D dst = new Texture2D(targetW, targetH, TextureFormat.RGBA32, false); + + for (int y = 0; y < targetH; y++) + { + float v = (targetH == 1) ? 0f : (float)y / (targetH - 1); + for (int x = 0; x < targetW; x++) + { + float u = (targetW == 1) ? 0f : (float)x / (targetW - 1); + Color c = SampleBilinear(src, u, v); + dst.SetPixel(x, y, c); + } + } + + dst.Apply(false, false); + return dst; + } + + private static Color SampleBilinear(Texture2D tex, float u, float v) + { + float x = u * (tex.width - 1); + float y = v * (tex.height - 1); + + int x0 = Mathf.Clamp((int)Mathf.Floor(x), 0, tex.width - 1); + int y0 = Mathf.Clamp((int)Mathf.Floor(y), 0, tex.height - 1); + int x1 = Mathf.Clamp(x0 + 1, 0, tex.width - 1); + int y1 = Mathf.Clamp(y0 + 1, 0, tex.height - 1); + + float tx = x - x0; + float ty = y - y0; + + Color c00 = tex.GetPixel(x0, y0); + Color c10 = tex.GetPixel(x1, y0); + Color c01 = tex.GetPixel(x0, y1); + Color c11 = tex.GetPixel(x1, y1); + + Color a = Color.Lerp(c00, c10, tx); + Color b = Color.Lerp(c01, c11, tx); + return Color.Lerp(a, b, ty); + } + + // --------------------------- + // GameView sizing (internal) + // --------------------------- + + private static EditorWindow GetMainGameView() + { + Type t = Type.GetType("UnityEditor.GameView,UnityEditor"); + if (t == null) return null; + + // Try "GetMainGameView" first (older Unity versions) + MethodInfo getMain = t.GetMethod("GetMainGameView", BindingFlags.NonPublic | BindingFlags.Static); + if (getMain != null) + { + var result = getMain.Invoke(null, null) as EditorWindow; + if (result != null) return result; + } + + // Fallback: try "GetMainGameViewRenderRect" or just find an open GameView window + var gameView = GetWindow(t, false, null, false); + return gameView; + } + + private static void SetGameViewSize(int width, int height) + { + // Creates/uses a fixed resolution entry in the current platform group, then selects it. + // Unity does not expose this publicly; reflection is used. + + Type sizesType = Type.GetType("UnityEditor.GameViewSizes,UnityEditor"); + Type sizeType = Type.GetType("UnityEditor.GameViewSize,UnityEditor"); + Type groupType = Type.GetType("UnityEditor.GameViewSizeGroupType,UnityEditor"); + + if (sizesType == null || sizeType == null || groupType == null) + return; + + var instanceProp = sizesType.GetProperty("instance", BindingFlags.Public | BindingFlags.Static); + if (instanceProp == null) return; + object sizesInstance = instanceProp.GetValue(null, null); + if (sizesInstance == null) return; + + MethodInfo getGroup = sizesType.GetMethod("GetGroup"); + if (getGroup == null) return; + object group = getGroup.Invoke(sizesInstance, new object[] { (int)Enum.Parse(groupType, "Standalone") }); + if (group == null) return; + + // Find existing + MethodInfo getBuiltinCount = group.GetType().GetMethod("GetBuiltinCount"); + MethodInfo getCustomCount = group.GetType().GetMethod("GetCustomCount"); + MethodInfo getGameViewSize = group.GetType().GetMethod("GetGameViewSize"); + + if (getBuiltinCount == null || getCustomCount == null || getGameViewSize == null) return; + + int builtin = (int)getBuiltinCount.Invoke(group, null); + int custom = (int)getCustomCount.Invoke(group, null); + + int total = builtin + custom; + int foundIndex = -1; + + for (int i = 0; i < total; i++) + { + object gvSize = getGameViewSize.Invoke(group, new object[] { i }); + if (gvSize == null) continue; + var widthProp = gvSize.GetType().GetProperty("width"); + var heightProp = gvSize.GetType().GetProperty("height"); + if (widthProp == null || heightProp == null) continue; + + int w = (int)widthProp.GetValue(gvSize, null); + int h = (int)heightProp.GetValue(gvSize, null); + + if (w == width && h == height) + { + foundIndex = i; + break; + } + } + + if (foundIndex < 0) + { + // Add custom size + Type gvSizeType = Type.GetType("UnityEditor.GameViewSizeType,UnityEditor"); + if (gvSizeType == null) return; + object fixedRes = Enum.Parse(gvSizeType, "FixedResolution"); + + ConstructorInfo ctor = sizeType.GetConstructor(new[] { gvSizeType, typeof(int), typeof(int), typeof(string) }); + if (ctor == null) return; + object newSize = ctor.Invoke(new object[] { fixedRes, width, height, width + "x" + height }); + + MethodInfo addCustom = group.GetType().GetMethod("AddCustomSize"); + if (addCustom == null) return; + addCustom.Invoke(group, new object[] { newSize }); + + custom = (int)getCustomCount.Invoke(group, null); + foundIndex = builtin + (custom - 1); + } + + // Select size in GameView + EditorWindow gv = GetMainGameView(); + if (gv == null) return; + + Type gvType = gv.GetType(); + PropertyInfo selectedSizeIndex = gvType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (selectedSizeIndex != null) + selectedSizeIndex.SetValue(gv, foundIndex, null); + + gv.Repaint(); + } + + // Hidden MonoBehaviour to run coroutines from the editor tool + private class CaptureHelper : MonoBehaviour { } + } +} diff --git a/Assets/Scripts/Editor/Tools/FindWhoReferencesThisGameObject.cs b/Assets/Scripts/Editor/Tools/FindWhoReferencesThisGameObject.cs index dd5b2c5..b3f4b15 100644 --- a/Assets/Scripts/Editor/Tools/FindWhoReferencesThisGameObject.cs +++ b/Assets/Scripts/Editor/Tools/FindWhoReferencesThisGameObject.cs @@ -64,7 +64,7 @@ private void OnGUI() private void FindReferences(GameObject target) { - var allObjects = UnityEngine.Object.FindObjectsOfType(true); + var allObjects = Object.FindObjectsByType(findObjectsInactive: FindObjectsInactive.Include, sortMode: FindObjectsSortMode.None); foreach (var mono in allObjects) { @@ -94,20 +94,26 @@ private void FindReferences(GameObject target) } } } + + continue; } - else if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) + + if (!typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) + continue; + + var value = field.GetValue(mono) as UnityEngine.Object; + if (ReferencesTarget(value, target)) { - var value = field.GetValue(mono) as UnityEngine.Object; - if (value == target) + results.Add(new ReferenceResult { - results.Add(new ReferenceResult - { - message = $"{mono.name} ({type.Name}) -> Field '{field.Name}'", - owner = mono.gameObject - }); - } + message = $"{mono.name} ({type.Name}) -> Field '{field.Name}'", + owner = mono.gameObject + }); } } + + // Also scan serialized properties (handles public fields, [SerializeField] private, arrays/lists, etc.) + FindSerializedReferences(mono, target); } if (results.Count == 0) @@ -119,5 +125,43 @@ private void FindReferences(GameObject target) }); } } + + private void FindSerializedReferences(MonoBehaviour mono, GameObject target) + { + var so = new SerializedObject(mono); + var it = so.GetIterator(); + + // enterChildren=true on first call to include all fields + bool enterChildren = true; + while (it.NextVisible(enterChildren)) + { + enterChildren = false; + + if (it.propertyType != SerializedPropertyType.ObjectReference) + continue; + + var obj = it.objectReferenceValue; + if (!ReferencesTarget(obj, target)) + continue; + + results.Add(new ReferenceResult + { + message = $"{mono.name} ({mono.GetType().Name}) -> Serialized '{it.propertyPath}'", + owner = mono.gameObject + }); + } + } + + private static bool ReferencesTarget(UnityEngine.Object value, GameObject target) + { + if (value == null || target == null) return false; + + if (value == target) return true; + + // most common case: field is Transform/Component referencing the target GO + if (value is Component c && c.gameObject == target) return true; + + return false; + } } } diff --git a/Assets/Scripts/Editor/Tools/InspectorFilter.cs b/Assets/Scripts/Editor/Tools/InspectorFilter.cs new file mode 100644 index 0000000..ab37407 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/InspectorFilter.cs @@ -0,0 +1,289 @@ +// filters the fields of all components of a GameObject based on a user-provided string +// matching against both field names and types, with a UI to input the filter and visual highlights for matched fields. + +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace UnityLibrary.EditorTools +{ + [InitializeOnLoad] + public static class InspectorFilter + { + private static readonly Dictionary FiltersByGameObjectId = new Dictionary(); + private static readonly Dictionary> MatchedFieldsByComponentId = new Dictionary>(); + + static InspectorFilter() + { + Editor.finishedDefaultHeaderGUI += OnFinishedDefaultHeaderGUI; + Selection.selectionChanged += ApplyFilterForCurrentSelection; + Undo.undoRedoPerformed += ApplyFilterForCurrentSelection; + EditorApplication.delayCall += ApplyFilterForCurrentSelection; + } + + internal static bool TryGetFilterForGameObject(GameObject go, out string filter) + { + filter = string.Empty; + if (go == null) + { + return false; + } + + if (!FiltersByGameObjectId.TryGetValue(go.GetInstanceID(), out string value) || string.IsNullOrWhiteSpace(value)) + { + return false; + } + + filter = value.Trim(); + return filter.Length > 0; + } + + internal static bool IsPropertyMatch(SerializedProperty property, string filter) + { + if (property == null || string.IsNullOrWhiteSpace(filter)) + { + return false; + } + + if (property.propertyPath == "m_Script") + { + return false; + } + + return property.name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0 + || property.displayName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static void OnFinishedDefaultHeaderGUI(Editor editor) + { + if (editor.target is GameObject go) + { + DrawGameObjectFilterUI(go); + return; + } + + if (!(editor.target is Component component)) + { + return; + } + + int gameObjectId = component.gameObject.GetInstanceID(); + if (!FiltersByGameObjectId.TryGetValue(gameObjectId, out string filter) || string.IsNullOrWhiteSpace(filter)) + { + return; + } + + if (!MatchedFieldsByComponentId.TryGetValue(component.GetInstanceID(), out List matchedFields) || matchedFields.Count == 0) + { + return; + } + + Color old = GUI.color; + GUI.color = new Color(1f, 0.95f, 0.55f, 1f); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + GUI.color = old; + EditorGUILayout.LabelField("Matched fields: " + string.Join(", ", matchedFields), EditorStyles.miniLabel); + EditorGUILayout.EndVertical(); + } + + private static void DrawGameObjectFilterUI(GameObject go) + { + int id = go.GetInstanceID(); + FiltersByGameObjectId.TryGetValue(id, out string currentFilter); + currentFilter ??= string.Empty; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + const string filterControlName = "InspectorFilter_FilterField"; + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + GUI.SetNextControlName(filterControlName); + string newFilter = EditorGUILayout.TextField("Filter", currentFilter); + bool clearClicked = GUILayout.Button("x", EditorStyles.miniButton, GUILayout.Width(20f)); + bool changed = EditorGUI.EndChangeCheck(); + EditorGUILayout.EndHorizontal(); + + Event e = Event.current; + bool escapePressed = e.type == EventType.KeyDown + && e.keyCode == KeyCode.Escape + && GUI.GetNameOfFocusedControl() == filterControlName; + + if (clearClicked || escapePressed) + { + newFilter = string.Empty; + changed = true; + GUI.FocusControl(null); + if (escapePressed) + { + e.Use(); + } + } + + if (changed) + { + if (string.IsNullOrWhiteSpace(newFilter)) + { + FiltersByGameObjectId.Remove(id); + newFilter = string.Empty; + } + else + { + FiltersByGameObjectId[id] = newFilter; + } + + ApplyFilter(go, newFilter); + ActiveEditorTracker.sharedTracker.ForceRebuild(); + } + else + { + ApplyFilter(go, currentFilter); + } + + EditorGUILayout.EndVertical(); + } + + private static void ApplyFilterForCurrentSelection() + { + if (!(Selection.activeGameObject is GameObject go)) + { + return; + } + + int id = go.GetInstanceID(); + FiltersByGameObjectId.TryGetValue(id, out string filter); + ApplyFilter(go, filter); + ActiveEditorTracker.sharedTracker.ForceRebuild(); + } + + private static void ApplyFilter(GameObject go, string filter) + { + ActiveEditorTracker tracker = ActiveEditorTracker.sharedTracker; + Editor[] editors = tracker.activeEditors; + if (editors == null || editors.Length == 0) + { + return; + } + + bool hasFilter = !string.IsNullOrWhiteSpace(filter); + string normalizedFilter = hasFilter ? filter.Trim() : string.Empty; + + for (int i = 0; i < editors.Length; i++) + { + UnityEngine.Object target = editors[i].target; + if (!(target is Component component) || component.gameObject != go) + { + tracker.SetVisible(i, 1); + continue; + } + + int componentId = component.GetInstanceID(); + + if (!hasFilter) + { + MatchedFieldsByComponentId.Remove(componentId); + tracker.SetVisible(i, 1); + continue; + } + + bool typeMatch = component.GetType().Name.IndexOf(normalizedFilter, StringComparison.OrdinalIgnoreCase) >= 0; + List fieldMatches = GetMatchingSerializedFields(editors[i], normalizedFilter); + bool fieldMatch = fieldMatches.Count > 0; + + if (fieldMatch) + { + MatchedFieldsByComponentId[componentId] = fieldMatches; + } + else + { + MatchedFieldsByComponentId.Remove(componentId); + } + + tracker.SetVisible(i, (typeMatch || fieldMatch) ? 1 : 0); + } + } + + private static List GetMatchingSerializedFields(Editor editor, string filter) + { + List matches = new List(); + + SerializedObject serializedObject = editor.serializedObject; + if (serializedObject == null) + { + return matches; + } + + SerializedProperty iterator = serializedObject.GetIterator(); + bool enterChildren = true; + + while (iterator.NextVisible(enterChildren)) + { + enterChildren = false; + + if (!IsPropertyMatch(iterator, filter)) + { + continue; + } + + string label = iterator.displayName; + if (!matches.Contains(label)) + { + matches.Add(label); + } + } + + return matches; + } + } + + [CustomEditor(typeof(Component), true, isFallback = true)] + [CanEditMultipleObjects] + public class InspectorFilterComponentEditor : Editor + { + private static readonly Color HighlightColor = new Color(0.5058824f, 0.7058824f, 1f, 1f); + + public override void OnInspectorGUI() + { + Component component = target as Component; + if (component == null || !InspectorFilter.TryGetFilterForGameObject(component.gameObject, out string filter)) + { + DrawDefaultInspector(); + return; + } + + serializedObject.Update(); + + SerializedProperty property = serializedObject.GetIterator(); + bool enterChildren = true; + + while (property.NextVisible(enterChildren)) + { + enterChildren = false; + + using (new EditorGUI.DisabledScope(property.propertyPath == "m_Script")) + { + float height = EditorGUI.GetPropertyHeight(property, true); + Rect rect = EditorGUILayout.GetControlRect(true, height); + + if (InspectorFilter.IsPropertyMatch(property, filter)) + { + DrawBorder(rect, HighlightColor, 1f); + } + + EditorGUI.PropertyField(rect, property, true); + } + } + + serializedObject.ApplyModifiedProperties(); + } + + private static void DrawBorder(Rect rect, Color color, float thickness) + { + EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, rect.width, thickness), color); + EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMax - thickness, rect.width, thickness), color); + EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, thickness, rect.height), color); + EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.yMin, thickness, rect.height), color); + } + } + +} diff --git a/Assets/Scripts/Editor/Tools/MeshThumbnailGrabberWindow.cs b/Assets/Scripts/Editor/Tools/MeshThumbnailGrabberWindow.cs new file mode 100644 index 0000000..ea28fba --- /dev/null +++ b/Assets/Scripts/Editor/Tools/MeshThumbnailGrabberWindow.cs @@ -0,0 +1,886 @@ +// editortool to capture thumbnails of meshes, prefabs, or gameobjects with custom settings and save as PNG + +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace UnityLibrary.Editor.Tools +{ + public class MeshThumbnailGrabberWindow : EditorWindow + { + private Object sourceObject; + + private PreviewRenderUtility previewUtility; + + private GameObject previewRoot; + private GameObject previewInstance; + + private Texture2D exportPreviewTexture; + + private readonly List drawables = new List(); + + private Vector2 orbit = new Vector2(135f, -20f); + + private float zoom = 1f; + private float fitDistance = 3f; + private float cameraDistanceMultiplier = 1f; + private float lightIntensity = 1.25f; + + private int outputWidth = 512; + private int outputHeight = 512; + + private Color backgroundColor = new Color(0f, 0f, 0f, 0f); + private Color ambientColor = new Color(0.35f, 0.35f, 0.35f, 1f); + private Color lightColor = Color.white; + + private bool drawGround = false; + + private struct PreviewDrawable + { + public Mesh Mesh; + public Matrix4x4 Matrix; + public Material[] Materials; + } + + [MenuItem("Tools/UnityLibrary/Mesh Thumbnail Grabber")] + public static void Open() + { + var win = GetWindow("Mesh Thumbnail Grabber"); + win.minSize = new Vector2(500, 680); + } + + private const string PrefOrbitX = "MeshThumbGrabber_OrbitX"; + private const string PrefOrbitY = "MeshThumbGrabber_OrbitY"; + private const string PrefZoom = "MeshThumbGrabber_Zoom"; + private const string PrefCamDistMult = "MeshThumbGrabber_CamDistMult"; + private const string PrefLightIntensity = "MeshThumbGrabber_LightIntensity"; + private const string PrefOutputWidth = "MeshThumbGrabber_OutputWidth"; + private const string PrefOutputHeight = "MeshThumbGrabber_OutputHeight"; + private const string PrefBgColorR = "MeshThumbGrabber_BgR"; + private const string PrefBgColorG = "MeshThumbGrabber_BgG"; + private const string PrefBgColorB = "MeshThumbGrabber_BgB"; + private const string PrefBgColorA = "MeshThumbGrabber_BgA"; + private const string PrefAmbientColorR = "MeshThumbGrabber_AmbR"; + private const string PrefAmbientColorG = "MeshThumbGrabber_AmbG"; + private const string PrefAmbientColorB = "MeshThumbGrabber_AmbB"; + private const string PrefAmbientColorA = "MeshThumbGrabber_AmbA"; + private const string PrefLightColorR = "MeshThumbGrabber_LightR"; + private const string PrefLightColorG = "MeshThumbGrabber_LightG"; + private const string PrefLightColorB = "MeshThumbGrabber_LightB"; + private const string PrefLightColorA = "MeshThumbGrabber_LightA"; + private const string PrefDrawGround = "MeshThumbGrabber_DrawGround"; + + private void LoadPrefs() + { + orbit.x = EditorPrefs.GetFloat(PrefOrbitX, orbit.x); + orbit.y = EditorPrefs.GetFloat(PrefOrbitY, orbit.y); + zoom = EditorPrefs.GetFloat(PrefZoom, zoom); + cameraDistanceMultiplier = EditorPrefs.GetFloat(PrefCamDistMult, cameraDistanceMultiplier); + lightIntensity = EditorPrefs.GetFloat(PrefLightIntensity, lightIntensity); + outputWidth = EditorPrefs.GetInt(PrefOutputWidth, outputWidth); + outputHeight = EditorPrefs.GetInt(PrefOutputHeight, outputHeight); + backgroundColor = new Color( + EditorPrefs.GetFloat(PrefBgColorR, backgroundColor.r), + EditorPrefs.GetFloat(PrefBgColorG, backgroundColor.g), + EditorPrefs.GetFloat(PrefBgColorB, backgroundColor.b), + EditorPrefs.GetFloat(PrefBgColorA, backgroundColor.a)); + ambientColor = new Color( + EditorPrefs.GetFloat(PrefAmbientColorR, ambientColor.r), + EditorPrefs.GetFloat(PrefAmbientColorG, ambientColor.g), + EditorPrefs.GetFloat(PrefAmbientColorB, ambientColor.b), + EditorPrefs.GetFloat(PrefAmbientColorA, ambientColor.a)); + lightColor = new Color( + EditorPrefs.GetFloat(PrefLightColorR, lightColor.r), + EditorPrefs.GetFloat(PrefLightColorG, lightColor.g), + EditorPrefs.GetFloat(PrefLightColorB, lightColor.b), + EditorPrefs.GetFloat(PrefLightColorA, lightColor.a)); + drawGround = EditorPrefs.GetBool(PrefDrawGround, drawGround); + } + + private void SavePrefs() + { + EditorPrefs.SetFloat(PrefOrbitX, orbit.x); + EditorPrefs.SetFloat(PrefOrbitY, orbit.y); + EditorPrefs.SetFloat(PrefZoom, zoom); + EditorPrefs.SetFloat(PrefCamDistMult, cameraDistanceMultiplier); + EditorPrefs.SetFloat(PrefLightIntensity, lightIntensity); + EditorPrefs.SetInt(PrefOutputWidth, outputWidth); + EditorPrefs.SetInt(PrefOutputHeight, outputHeight); + EditorPrefs.SetFloat(PrefBgColorR, backgroundColor.r); + EditorPrefs.SetFloat(PrefBgColorG, backgroundColor.g); + EditorPrefs.SetFloat(PrefBgColorB, backgroundColor.b); + EditorPrefs.SetFloat(PrefBgColorA, backgroundColor.a); + EditorPrefs.SetFloat(PrefAmbientColorR, ambientColor.r); + EditorPrefs.SetFloat(PrefAmbientColorG, ambientColor.g); + EditorPrefs.SetFloat(PrefAmbientColorB, ambientColor.b); + EditorPrefs.SetFloat(PrefAmbientColorA, ambientColor.a); + EditorPrefs.SetFloat(PrefLightColorR, lightColor.r); + EditorPrefs.SetFloat(PrefLightColorG, lightColor.g); + EditorPrefs.SetFloat(PrefLightColorB, lightColor.b); + EditorPrefs.SetFloat(PrefLightColorA, lightColor.a); + EditorPrefs.SetBool(PrefDrawGround, drawGround); + } + + private void OnEnable() + { + LoadPrefs(); + CreatePreviewUtility(); + } + + private void OnDisable() + { + SavePrefs(); + Cleanup(); + } + + private void CreatePreviewUtility() + { + if (previewUtility != null) + { + previewUtility.Cleanup(); + } + + previewUtility = new PreviewRenderUtility(true); + previewUtility.cameraFieldOfView = 30f; + + ApplyLightSettings(); + } + + private void Cleanup() + { + DestroyPreviewObjects(); + + if (previewUtility != null) + { + previewUtility.Cleanup(); + previewUtility = null; + } + + ClearExportPreview(); + } + + private void DestroyPreviewObjects() + { + drawables.Clear(); + + if (previewRoot != null) + { + DestroyImmediate(previewRoot); + previewRoot = null; + previewInstance = null; + } + } + + private void ClearExportPreview() + { + if (exportPreviewTexture != null) + { + DestroyImmediate(exportPreviewTexture); + exportPreviewTexture = null; + } + } + + private void OnGUI() + { + EditorGUILayout.Space(); + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUI.BeginChangeCheck(); + + sourceObject = EditorGUILayout.ObjectField( + "Source", + sourceObject, + typeof(Object), + false + ); + + if (EditorGUI.EndChangeCheck()) + { + ClearExportPreview(); + CreatePreviewInstance(); + FitToView(); + Repaint(); + } + } + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUI.BeginChangeCheck(); + + outputWidth = Mathf.Clamp(EditorGUILayout.IntField("Output Width", outputWidth), 16, 8192); + outputHeight = Mathf.Clamp(EditorGUILayout.IntField("Output Height", outputHeight), 16, 8192); + + if (EditorGUI.EndChangeCheck()) + { + ClearExportPreview(); + SavePrefs(); + } + + zoom = EditorGUILayout.Slider("Zoom", zoom, 0.05f, 100f); + cameraDistanceMultiplier = EditorGUILayout.Slider("Distance Multiplier", cameraDistanceMultiplier, 0.1f, 5f); + + EditorGUI.BeginChangeCheck(); + + lightIntensity = EditorGUILayout.Slider("Light Intensity", lightIntensity, 0f, 5f); + ambientColor = EditorGUILayout.ColorField("Ambient Color", ambientColor); + lightColor = EditorGUILayout.ColorField("Light Color", lightColor); + drawGround = EditorGUILayout.Toggle("Draw Ground", drawGround); + + if (EditorGUI.EndChangeCheck()) + { + ApplyLightSettings(); + ClearExportPreview(); + + CenterObjectToOrigin(); + CacheDrawables(); + SavePrefs(); + Repaint(); + } + } + + Rect previewRect = GUILayoutUtility.GetRect( + 10, + 10000, + 10, + 10000, + GUILayout.ExpandWidth(true), + GUILayout.ExpandHeight(true) + ); + + DrawInteractivePreview(previewRect); + + using (new EditorGUILayout.HorizontalScope()) + { + GUI.enabled = sourceObject != null; + + if (GUILayout.Button("Fit To View", GUILayout.Height(28))) + { + FitToView(); + ClearExportPreview(); + SavePrefs(); + Repaint(); + } + + if (GUILayout.Button("Reset Rotation", GUILayout.Height(28))) + { + orbit = new Vector2(135f, -20f); + ClearExportPreview(); + SavePrefs(); + Repaint(); + } + + if (GUILayout.Button("Preview Export Size", GUILayout.Height(28))) + { + UpdateExportPreview(); + } + + if (GUILayout.Button("Save PNG", GUILayout.Height(28))) + { + SavePng(); + } + + GUI.enabled = true; + } + + DrawExportPreviewPanel(); + } + + private void ApplyLightSettings() + { + if (previewUtility == null) + { + return; + } + + previewUtility.lights[0].intensity = lightIntensity; + previewUtility.lights[0].color = lightColor; + previewUtility.lights[0].transform.rotation = Quaternion.Euler(40f, 40f, 0f); + + previewUtility.lights[1].intensity = lightIntensity * 0.35f; + previewUtility.lights[1].color = lightColor; + previewUtility.lights[1].transform.rotation = Quaternion.Euler(340f, 218f, 177f); + } + + private void DrawInteractivePreview(Rect rect) + { + if (previewUtility == null) + { + CreatePreviewUtility(); + } + + if (sourceObject == null) + { + EditorGUI.DrawRect(rect, new Color(0.15f, 0.15f, 0.15f, 1f)); + GUI.Label(rect, "Assign a prefab, model, GameObject, or Mesh", EditorStyles.centeredGreyMiniLabel); + return; + } + + if (previewInstance == null) + { + CreatePreviewInstance(); + FitToView(); + } + + HandlePreviewInput(rect); + + Texture texture = RenderPreview(rect.width, rect.height); + + if (texture != null) + { + GUI.DrawTexture(rect, texture, ScaleMode.StretchToFill, true); + } + } + + private void HandlePreviewInput(Rect rect) + { + Event e = Event.current; + + if (!rect.Contains(e.mousePosition)) + { + return; + } + + if (e.type == EventType.MouseDrag && e.button == 0) + { + orbit += new Vector2(e.delta.x, -e.delta.y); + orbit.y = Mathf.Clamp(orbit.y, -89f, 89f); + + ClearExportPreview(); + + e.Use(); + Repaint(); + } + + if (e.type == EventType.MouseUp && e.button == 0) + { + SavePrefs(); + } + + if (e.type == EventType.ScrollWheel) + { + zoom *= 1f - e.delta.y * 0.08f; + zoom = Mathf.Clamp(zoom, 0.05f, 100f); + + ClearExportPreview(); + SavePrefs(); + + e.Use(); + Repaint(); + } + } + + private void CreatePreviewInstance() + { + DestroyPreviewObjects(); + + if (sourceObject == null) + { + return; + } + + previewRoot = new GameObject("Thumbnail Preview Root"); + previewRoot.hideFlags = HideFlags.HideAndDontSave; + previewRoot.transform.position = Vector3.zero; + previewRoot.transform.rotation = Quaternion.identity; + previewRoot.transform.localScale = Vector3.one; + + GameObject sourceGameObject = sourceObject as GameObject; + Mesh sourceMesh = sourceObject as Mesh; + + if (sourceGameObject != null) + { + previewInstance = Instantiate(sourceGameObject, previewRoot.transform); + previewInstance.hideFlags = HideFlags.HideAndDontSave; + } + else if (sourceMesh != null) + { + previewInstance = new GameObject("Mesh Preview Instance"); + previewInstance.hideFlags = HideFlags.HideAndDontSave; + previewInstance.transform.SetParent(previewRoot.transform, false); + + MeshFilter meshFilter = previewInstance.AddComponent(); + meshFilter.sharedMesh = sourceMesh; + + MeshRenderer meshRenderer = previewInstance.AddComponent(); + meshRenderer.sharedMaterial = GetDefaultMaterial(); + } + else + { + string path = AssetDatabase.GetAssetPath(sourceObject); + GameObject loaded = AssetDatabase.LoadAssetAtPath(path); + + if (loaded != null) + { + previewInstance = Instantiate(loaded, previewRoot.transform); + previewInstance.hideFlags = HideFlags.HideAndDontSave; + } + } + + if (previewInstance == null) + { + DestroyPreviewObjects(); + return; + } + + DisableSceneOnlyComponents(previewInstance); + + previewInstance.transform.localPosition = Vector3.zero; + previewInstance.transform.localRotation = Quaternion.identity; + previewInstance.transform.localScale = Vector3.one; + + CenterObjectToOrigin(); + CacheDrawables(); + } + + private void CenterObjectToOrigin() + { + if (previewRoot == null || previewInstance == null) + { + return; + } + + previewRoot.transform.position = Vector3.zero; + previewRoot.transform.rotation = Quaternion.identity; + previewRoot.transform.localScale = Vector3.one; + + previewInstance.transform.localPosition = Vector3.zero; + + Bounds bounds = GetWorldBounds(previewRoot); + Vector3 centerOffset = bounds.center; + + previewInstance.transform.position -= centerOffset; + + if (drawGround) + { + Bounds centeredBounds = GetWorldBounds(previewRoot); + previewInstance.transform.position -= new Vector3(0f, centeredBounds.min.y, 0f); + } + } + + private void FitToView() + { + if (previewInstance == null || previewUtility == null) + { + return; + } + + CenterObjectToOrigin(); + CacheDrawables(); + + Bounds bounds = GetWorldBounds(previewRoot); + + float radius = Mathf.Max(bounds.extents.magnitude, 0.001f); + float fov = previewUtility.cameraFieldOfView; + float fovRad = fov * Mathf.Deg2Rad; + + fitDistance = radius / Mathf.Sin(fovRad * 0.5f); + fitDistance *= 1.15f; + + zoom = 1f; + } + + private void CacheDrawables() + { + drawables.Clear(); + + if (previewRoot == null) + { + return; + } + + Renderer[] renderers = previewRoot.GetComponentsInChildren(true); + + foreach (Renderer renderer in renderers) + { + if (renderer == null || !renderer.enabled) + { + continue; + } + + MeshFilter meshFilter = renderer.GetComponent(); + + if (meshFilter != null && meshFilter.sharedMesh != null) + { + drawables.Add(new PreviewDrawable + { + Mesh = meshFilter.sharedMesh, + Matrix = renderer.localToWorldMatrix, + Materials = renderer.sharedMaterials + }); + + continue; + } + + SkinnedMeshRenderer skinned = renderer as SkinnedMeshRenderer; + + if (skinned != null && skinned.sharedMesh != null) + { + drawables.Add(new PreviewDrawable + { + Mesh = skinned.sharedMesh, + Matrix = skinned.localToWorldMatrix, + Materials = skinned.sharedMaterials + }); + } + } + } + + private Texture RenderPreview(float width, float height) + { + if (previewUtility == null || previewInstance == null) + { + return null; + } + + Rect rect = new Rect(0f, 0f, Mathf.Max(1f, width), Mathf.Max(1f, height)); + + previewUtility.BeginPreview(rect, GUIStyle.none); + RenderCurrentView(); + return previewUtility.EndPreview(); + } + + private void RenderCurrentView() + { + Camera camera = previewUtility.camera; + + camera.clearFlags = CameraClearFlags.Color; + camera.backgroundColor = backgroundColor; + camera.nearClipPlane = 0.01f; + camera.farClipPlane = 10000f; + camera.fieldOfView = previewUtility.cameraFieldOfView; + camera.allowHDR = false; + camera.allowMSAA = true; + + RenderSettings.ambientLight = ambientColor; + + Quaternion rotation = Quaternion.Euler(-orbit.y, orbit.x, 0f); + + float distance = fitDistance * cameraDistanceMultiplier / Mathf.Max(zoom, 0.001f); + distance = Mathf.Max(distance, 0.01f); + + Vector3 cameraDirection = rotation * Vector3.forward; + + camera.transform.position = -cameraDirection * distance; + camera.transform.rotation = Quaternion.LookRotation(cameraDirection, Vector3.up); + + ApplyLightSettings(); + + DrawCachedRenderers(); + + if (drawGround) + { + DrawGround(); + } + + camera.Render(); + } + + private void DrawCachedRenderers() + { + for (int i = 0; i < drawables.Count; i++) + { + PreviewDrawable drawable = drawables[i]; + + if (drawable.Mesh == null) + { + continue; + } + + DrawMeshWithMaterials(drawable.Mesh, drawable.Matrix, drawable.Materials); + } + } + + private void DrawMeshWithMaterials(Mesh mesh, Matrix4x4 matrix, Material[] materials) + { + if (mesh == null) + { + return; + } + + int subMeshCount = Mathf.Max(1, mesh.subMeshCount); + + for (int i = 0; i < subMeshCount; i++) + { + Material material = GetDefaultMaterial(); + + if (materials != null && i < materials.Length && materials[i] != null) + { + material = materials[i]; + } + + if (material == null) + { + continue; + } + + previewUtility.DrawMesh(mesh, matrix, material, i); + } + } + + private void DrawGround() + { + Mesh planeMesh = Resources.GetBuiltinResource("Plane.fbx"); + + if (planeMesh == null) + { + return; + } + + Bounds bounds = GetWorldBounds(previewRoot); + + float size = Mathf.Max(bounds.size.x, bounds.size.z, 1f) * 1.5f; + + Matrix4x4 matrix = Matrix4x4.TRS( + new Vector3(0f, bounds.min.y, 0f), + Quaternion.identity, + new Vector3(size * 0.1f, 1f, size * 0.1f) + ); + + previewUtility.DrawMesh(planeMesh, matrix, GetDefaultMaterial(), 0); + } + + private Bounds GetWorldBounds(GameObject root) + { + if (root == null) + { + return new Bounds(Vector3.zero, Vector3.one); + } + + Renderer[] renderers = root.GetComponentsInChildren(true); + + bool hasBounds = false; + Bounds bounds = new Bounds(Vector3.zero, Vector3.one); + + foreach (Renderer renderer in renderers) + { + if (renderer == null) + { + continue; + } + + if (!hasBounds) + { + bounds = renderer.bounds; + hasBounds = true; + } + else + { + bounds.Encapsulate(renderer.bounds); + } + } + + if (!hasBounds) + { + return new Bounds(Vector3.zero, Vector3.one); + } + + return bounds; + } + + private static Material GetDefaultMaterial() + { + Material material = AssetDatabase.GetBuiltinExtraResource("Default-Material.mat"); + + if (material != null) + { + return material; + } + + Shader shader = Shader.Find("Standard"); + + if (shader != null) + { + return new Material(shader); + } + + return null; + } + + private static void DisableSceneOnlyComponents(GameObject root) + { + MonoBehaviour[] behaviours = root.GetComponentsInChildren(true); + + foreach (MonoBehaviour behaviour in behaviours) + { + if (behaviour != null) + { + behaviour.enabled = false; + } + } + + Camera[] cameras = root.GetComponentsInChildren(true); + + foreach (Camera camera in cameras) + { + camera.enabled = false; + } + + Light[] lights = root.GetComponentsInChildren(true); + + foreach (Light light in lights) + { + light.enabled = false; + } + } + + private void SavePng() + { + if (sourceObject == null || previewInstance == null) + { + return; + } + + string defaultName = sourceObject.name + "_thumbnail_" + outputWidth + "x" + outputHeight + ".png"; + + string path = EditorUtility.SaveFilePanel( + "Save Thumbnail PNG", + Application.dataPath, + defaultName, + "png" + ); + + if (string.IsNullOrEmpty(path)) + { + return; + } + + Texture2D texture = RenderToTexture2D(outputWidth, outputHeight); + + if (texture == null) + { + Debug.LogError("Failed to render thumbnail."); + return; + } + + byte[] png = texture.EncodeToPNG(); + File.WriteAllBytes(path, png); + + DestroyImmediate(texture); + + AssetDatabase.Refresh(); + + PingSavedAsset(path); + UpdateExportPreview(); + + Debug.Log("Saved thumbnail: " + path); + } + + private Texture2D RenderToTexture2D(int width, int height) + { + if (previewUtility == null || previewInstance == null) + { + return null; + } + + RenderTexture rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); + RenderTexture prevActive = RenderTexture.active; + + try + { + // BeginPreview activates the preview scene lights; we then swap its + // internal RGB target for our ARGB32 RT before camera.Render() fires. + Rect previewRect = new Rect(0f, 0f, width, height); + previewUtility.BeginPreview(previewRect, GUIStyle.none); + + previewUtility.camera.targetTexture = rt; + + RenderCurrentView(); + + // EndPreview would blit its internal RT to screen – skip that and + // read directly from our ARGB32 RT instead. + previewUtility.EndPreview(); + + RenderTexture.active = rt; + + Texture2D copy = new Texture2D(width, height, TextureFormat.RGBA32, false); + copy.ReadPixels(new Rect(0, 0, width, height), 0, 0); + copy.Apply(); + + return copy; + } + finally + { + RenderTexture.active = prevActive; + RenderTexture.ReleaseTemporary(rt); + } + } + + private static void PingSavedAsset(string absolutePath) + { + string projectPath = Path.GetFullPath(Application.dataPath + "/..").Replace("\\", "/"); + string fixedPath = Path.GetFullPath(absolutePath).Replace("\\", "/"); + + if (!fixedPath.StartsWith(projectPath)) + { + EditorUtility.RevealInFinder(absolutePath); + return; + } + + string assetPath = fixedPath.Substring(projectPath.Length + 1); + + Object asset = AssetDatabase.LoadAssetAtPath(assetPath); + + if (asset != null) + { + Selection.activeObject = asset; + EditorGUIUtility.PingObject(asset); + } + else + { + EditorUtility.RevealInFinder(absolutePath); + } + } + + private void UpdateExportPreview() + { + if (sourceObject == null || previewInstance == null) + { + return; + } + + ClearExportPreview(); + + exportPreviewTexture = RenderToTexture2D(outputWidth, outputHeight); + Repaint(); + } + + private void DrawExportPreviewPanel() + { + if (exportPreviewTexture == null) + { + return; + } + + EditorGUILayout.Space(); + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUILayout.LabelField( + "Export Preview: " + exportPreviewTexture.width + "x" + exportPreviewTexture.height + " px", + EditorStyles.boldLabel + ); + + Rect rect = GUILayoutUtility.GetRect( + exportPreviewTexture.width, + exportPreviewTexture.height, + GUILayout.Width(exportPreviewTexture.width), + GUILayout.Height(exportPreviewTexture.height) + ); + + EditorGUI.DrawTextureTransparent( + rect, + exportPreviewTexture, + ScaleMode.StretchToFill + ); + + if (GUILayout.Button("Clear Preview")) + { + ClearExportPreview(); + } + } + } + + } // class +} // namespace diff --git a/Assets/Scripts/Editor/Tools/PivotAligner.cs b/Assets/Scripts/Editor/Tools/PivotAligner.cs new file mode 100644 index 0000000..e24d762 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/PivotAligner.cs @@ -0,0 +1,879 @@ +/// +/// PivotAligner — Unity Editor Window +/// Align two 3D models by picking a custom pivot point (vertex or face center) +/// on the source model, then rotating/translating it around that point. +/// +/// Place this file inside any Editor/ folder in your project. +/// Open via: Tools/UnityLibrary/Pivot Aligner +/// +using UnityEngine; +using UnityEditor; + +namespace UnityLibrary.Tools +{ + public class PivotAligner : EditorWindow + { + enum PickMode { Vertex, Face } + enum ToolState { Idle, Picking, Transforming } + + const string MENU_PATH = "Tools/Pivot Aligner"; + const float GIZMO_RADIUS = 0.06f; + const float GIZMO_CROSS = 0.25f; + + [SerializeField] private GameObject sourceObject; + [SerializeField] private PickMode pickMode = PickMode.Vertex; + + // Rotation + private float rotX; + private float rotY; + private float rotZ; + + private bool showFinetune = false; + private float fineTuneRange = 5f; + private float fineX; + private float fineY; + private float fineZ; + + // Scale + private bool showScale = true; + private bool uniformScale = true; + private float uniformScaleValue = 1f; + private float scaleX = 1f; + private float scaleY = 1f; + private float scaleZ = 1f; + + // Position offset + private bool showPosOffset = false; + private float posOffsetX; + private float posOffsetY; + private float posOffsetZ; + private float finePosRange = 0.1f; + + // Runtime state + private ToolState state = ToolState.Idle; + private Vector3 pivotWorld = Vector3.zero; + private bool hasPivot = false; + + private Vector3 basePosition; + private Quaternion baseRotation; + private Vector3 baseScale; + + private Vector3 highlightPoint = Vector3.zero; + private Vector3 highlightNormal = Vector3.up; + private bool hasHighlight = false; + + private Vector2 scroll; + + private GUIStyle headerStyle; + private GUIStyle sectionStyle; + private GUIStyle stateStyle; + private GUIStyle subLabelStyle; + private bool stylesInit; + + private int undoGroupIndex = 0; + private int lastHotControl = 0; + private string lastUndoLabel = ""; + + [MenuItem(MENU_PATH)] + public static void ShowWindow() + { + PivotAligner win = GetWindow("Pivot Aligner"); + win.minSize = new Vector2(440, 560); + } + + private void OnEnable() + { + SceneView.duringSceneGui += OnSceneGUI; + titleContent = new GUIContent("Pivot Aligner"); + } + + private void OnDisable() + { + SceneView.duringSceneGui -= OnSceneGUI; + CancelPicking(); + } + + private void OnGUI() + { + InitStyles(); + + scroll = EditorGUILayout.BeginScrollView(scroll); + + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("PIVOT ALIGNER", headerStyle); + EditorGUILayout.Space(2); + DrawHR(); + + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("1. Source Model", sectionStyle); + + EditorGUI.BeginChangeCheck(); + sourceObject = (GameObject)EditorGUILayout.ObjectField( + "Game Object", + sourceObject, + typeof(GameObject), + true); + + if (EditorGUI.EndChangeCheck()) + ResetTool(); + + if (sourceObject == null) + { + EditorGUILayout.HelpBox("Assign a GameObject to begin.", MessageType.Info); + EditorGUILayout.EndScrollView(); + return; + } + + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("2. Pick Mode", sectionStyle); + + EditorGUILayout.BeginHorizontal(); + + if (DrawModeButton("Vertex", pickMode == PickMode.Vertex)) + { + pickMode = PickMode.Vertex; + hasHighlight = false; + } + + if (DrawModeButton("Face", pickMode == PickMode.Face)) + { + pickMode = PickMode.Face; + hasHighlight = false; + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("3. Select Pivot Point", sectionStyle); + + using (new EditorGUI.DisabledScope(state == ToolState.Transforming)) + { + if (state != ToolState.Picking) + { + if (GUILayout.Button("Select Target Point in Scene", GUILayout.Height(30))) + BeginPicking(); + } + else + { + Color prev = GUI.backgroundColor; + GUI.backgroundColor = new Color(1f, 0.55f, 0.15f); + + if (GUILayout.Button("Cancel Picking", GUILayout.Height(30))) + CancelPicking(); + + GUI.backgroundColor = prev; + + string modeText = pickMode == PickMode.Vertex ? "vertex" : "face center"; + EditorGUILayout.HelpBox( + "Hover over the model. The nearest " + modeText + " will highlight. Click to confirm pivot.", + MessageType.None); + } + } + + if (hasPivot) + { + EditorGUILayout.Space(2); + + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField("Pivot " + pivotWorld.ToString("F4"), subLabelStyle); + + if (GUILayout.Button("Re-pick", GUILayout.Width(56), GUILayout.Height(18))) + BeginPicking(); + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("4. Rotation Around Pivot", sectionStyle); + + using (new EditorGUI.DisabledScope(!hasPivot)) + { + bool dirty = false; + + EditorGUI.BeginChangeCheck(); + + DrawRotRow("Pitch X", ref rotX); + DrawRotRow("Yaw Y", ref rotY); + DrawRotRow("Roll Z", ref rotZ); + + if (EditorGUI.EndChangeCheck()) + dirty = true; + + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + showFinetune = EditorGUILayout.ToggleLeft("Fine-tune +/-", showFinetune, GUILayout.Width(102)); + + using (new EditorGUI.DisabledScope(!showFinetune)) + fineTuneRange = Mathf.Max(0.001f, EditorGUILayout.FloatField(fineTuneRange, GUILayout.Width(52))); + + EditorGUILayout.LabelField("deg", subLabelStyle, GUILayout.Width(28)); + } + + if (showFinetune) + { + EditorGUI.BeginChangeCheck(); + + fineX = DrawFineRotSlider("Delta X", fineX, fineTuneRange); + fineY = DrawFineRotSlider("Delta Y", fineY, fineTuneRange); + fineZ = DrawFineRotSlider("Delta Z", fineZ, fineTuneRange); + + if (EditorGUI.EndChangeCheck()) + dirty = true; + } + + if (dirty && hasPivot) + ApplyAll("Pivot Aligner - Rotate"); + + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("Reset All Rotation", GUILayout.Height(24))) + ResetRotation(); + + if (showFinetune && GUILayout.Button("Reset Fine", GUILayout.Width(80), GUILayout.Height(24))) + { + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + if (hasPivot) + ApplyAll("Pivot Aligner - Rotate"); + } + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + showScale = EditorGUILayout.Foldout(showScale, "5. Scale Around Pivot", true, sectionStyle); + EditorGUILayout.LabelField("(uses selected point)", subLabelStyle); + } + + if (showScale) + { + using (new EditorGUI.DisabledScope(!hasPivot)) + { + EditorGUI.BeginChangeCheck(); + + uniformScale = EditorGUILayout.ToggleLeft("Uniform Scale", uniformScale); + + if (uniformScale) + { + uniformScaleValue = EditorGUILayout.FloatField("Scale", uniformScaleValue); + uniformScaleValue = Mathf.Max(0.0001f, uniformScaleValue); + + scaleX = uniformScaleValue; + scaleY = uniformScaleValue; + scaleZ = uniformScaleValue; + } + else + { + scaleX = Mathf.Max(0.0001f, EditorGUILayout.FloatField("Scale X", scaleX)); + scaleY = Mathf.Max(0.0001f, EditorGUILayout.FloatField("Scale Y", scaleY)); + scaleZ = Mathf.Max(0.0001f, EditorGUILayout.FloatField("Scale Z", scaleZ)); + + uniformScaleValue = scaleX; + } + + if (EditorGUI.EndChangeCheck() && hasPivot) + ApplyAll("Pivot Aligner - Scale"); + + EditorGUILayout.Space(3); + + if (GUILayout.Button("Reset Scale", GUILayout.Height(24))) + ResetScale(); + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + showPosOffset = EditorGUILayout.Foldout(showPosOffset, "6. Position Offset", true, sectionStyle); + EditorGUILayout.LabelField("(moves pivot too)", subLabelStyle); + } + + if (showPosOffset) + { + using (new EditorGUI.DisabledScope(!hasPivot)) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField("Slider range +/-", subLabelStyle, GUILayout.Width(102)); + finePosRange = Mathf.Max(0.0001f, EditorGUILayout.FloatField(finePosRange, GUILayout.Width(52))); + EditorGUILayout.LabelField("m", subLabelStyle, GUILayout.Width(14)); + } + + EditorGUILayout.Space(2); + + EditorGUI.BeginChangeCheck(); + + posOffsetX = DrawOffsetSliderRow("Offset X", posOffsetX, finePosRange); + posOffsetY = DrawOffsetSliderRow("Offset Y", posOffsetY, finePosRange); + posOffsetZ = DrawOffsetSliderRow("Offset Z", posOffsetZ, finePosRange); + + if (EditorGUI.EndChangeCheck() && hasPivot) + ApplyAll("Pivot Aligner - Move"); + + EditorGUILayout.Space(3); + + if (GUILayout.Button("Reset Position Offset", GUILayout.Height(24))) + ResetPositionOffset(); + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + + using (new EditorGUI.DisabledScope(!hasPivot)) + { + Color prev = GUI.backgroundColor; + GUI.backgroundColor = new Color(0.22f, 0.80f, 0.40f); + + if (GUILayout.Button("Apply and Clear Pivot", GUILayout.Height(36))) + Apply(); + + GUI.backgroundColor = prev; + } + + if (hasPivot) + { + if (GUILayout.Button("Cancel and Revert to Original", GUILayout.Height(26))) + RevertAndReset(); + } + + EditorGUILayout.Space(4); + + string totalInfo = ""; + + if (hasPivot) + { + totalInfo = + "rot(" + + (rotX + fineX).ToString("F3") + ", " + + (rotY + fineY).ToString("F3") + ", " + + (rotZ + fineZ).ToString("F3") + ") deg scale(" + + scaleX.ToString("F3") + ", " + + scaleY.ToString("F3") + ", " + + scaleZ.ToString("F3") + ") pos offset(" + + posOffsetX.ToString("F4") + ", " + + posOffsetY.ToString("F4") + ", " + + posOffsetZ.ToString("F4") + ") m"; + } + + string stateLabel; + + switch (state) + { + case ToolState.Picking: + stateLabel = "PICKING"; + break; + + case ToolState.Transforming: + stateLabel = "TRANSFORMING " + totalInfo; + break; + + default: + stateLabel = "idle"; + break; + } + + EditorGUILayout.LabelField(stateLabel, stateStyle); + EditorGUILayout.Space(4); + + EditorGUILayout.EndScrollView(); + + SceneView.RepaintAll(); + } + + private bool DrawModeButton(string label, bool active) + { + Color prev = GUI.backgroundColor; + + if (active) + GUI.backgroundColor = new Color(0.3f, 0.65f, 1f); + + bool clicked = GUILayout.Button(label, GUILayout.Height(26)); + + GUI.backgroundColor = prev; + + return clicked; + } + + private void DrawRotRow(string label, ref float value) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label, GUILayout.Width(72)); + + value = EditorGUILayout.FloatField(value, GUILayout.Width(74)); + + if (GUILayout.Button("-90", GUILayout.Width(36), GUILayout.Height(18))) + value -= 90f; + + if (GUILayout.Button("-45", GUILayout.Width(36), GUILayout.Height(18))) + value -= 45f; + + if (GUILayout.Button("0", GUILayout.Width(28), GUILayout.Height(18))) + value = 0f; + + if (GUILayout.Button("+45", GUILayout.Width(36), GUILayout.Height(18))) + value += 45f; + + if (GUILayout.Button("+90", GUILayout.Width(36), GUILayout.Height(18))) + value += 90f; + } + } + + private float DrawFineRotSlider(string label, float value, float range) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label, subLabelStyle, GUILayout.Width(76)); + + value = GUILayout.HorizontalSlider(value, -range, range); + value = EditorGUILayout.FloatField(value, GUILayout.Width(64)); + + EditorGUILayout.LabelField("deg", subLabelStyle, GUILayout.Width(28)); + } + + return value; + } + + private float DrawOffsetSliderRow(string label, float value, float range) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label, GUILayout.Width(72)); + + value = GUILayout.HorizontalSlider(value, -range, range); + value = EditorGUILayout.FloatField(value, GUILayout.Width(74)); + + EditorGUILayout.LabelField("m", subLabelStyle, GUILayout.Width(14)); + + if (GUILayout.Button("0", GUILayout.Width(24), GUILayout.Height(18))) + value = 0f; + } + + return value; + } + + private void OnSceneGUI(SceneView sv) + { + if (sourceObject == null) + return; + + if (hasPivot) + DrawPivotGizmo(pivotWorld, Color.cyan); + + if (state == ToolState.Picking) + HandlePicking(sv); + } + + private void HandlePicking(SceneView sv) + { + Event e = Event.current; + + if (e.type == EventType.Layout) + HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive)); + + if (e.type == EventType.MouseMove || e.type == EventType.MouseDown) + { + Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); + + TryRaycast(ray, out hasHighlight, out highlightPoint, out highlightNormal); + + if (hasHighlight) + DrawPivotGizmo(highlightPoint, new Color(1f, 0.8f, 0.1f, 0.9f)); + + if (e.type == EventType.MouseDown && e.button == 0 && hasHighlight) + { + ConfirmPivot(highlightPoint); + e.Use(); + } + + sv.Repaint(); + } + + if (hasHighlight) + DrawPivotGizmo(highlightPoint, new Color(1f, 0.8f, 0.1f, 0.9f)); + } + + private void TryRaycast(Ray ray, out bool hit, out Vector3 point, out Vector3 normal) + { + hit = false; + point = Vector3.zero; + normal = Vector3.up; + + MeshFilter[] filters = sourceObject.GetComponentsInChildren(); + float bestDist = float.MaxValue; + + foreach (MeshFilter mf in filters) + { + if (mf.sharedMesh == null) + continue; + + Mesh mesh = mf.sharedMesh; + Transform t = mf.transform; + + Ray localRay = new Ray( + t.InverseTransformPoint(ray.origin), + t.InverseTransformDirection(ray.direction).normalized); + + Vector3[] verts = mesh.vertices; + int[] tris = mesh.triangles; + Vector3[] normals = mesh.normals; + + for (int i = 0; i < tris.Length; i += 3) + { + Vector3 v0 = verts[tris[i]]; + Vector3 v1 = verts[tris[i + 1]]; + Vector3 v2 = verts[tris[i + 2]]; + + float dist; + float u; + float v; + + if (!RayTriangle(localRay, v0, v1, v2, out dist, out u, out v)) + continue; + + if (dist < 0f || dist >= bestDist) + continue; + + bestDist = dist; + hit = true; + + if (pickMode == PickMode.Vertex) + { + float w = 1f - u - v; + int vi = FindNearestVertex(u, v, w); + + if (vi == 0) + point = t.TransformPoint(v0); + else if (vi == 1) + point = t.TransformPoint(v1); + else + point = t.TransformPoint(v2); + } + else + { + point = t.TransformPoint((v0 + v1 + v2) / 3f); + } + + Vector3 n0 = normals.Length > tris[i] ? normals[tris[i]] : Vector3.up; + Vector3 n1 = normals.Length > tris[i + 1] ? normals[tris[i + 1]] : Vector3.up; + Vector3 n2 = normals.Length > tris[i + 2] ? normals[tris[i + 2]] : Vector3.up; + + normal = t.TransformDirection((n0 * (1f - u - v) + n1 * u + n2 * v).normalized); + } + } + } + + private static bool RayTriangle( + Ray ray, + Vector3 v0, + Vector3 v1, + Vector3 v2, + out float dist, + out float u, + out float v) + { + dist = 0f; + u = 0f; + v = 0f; + + Vector3 e1 = v1 - v0; + Vector3 e2 = v2 - v0; + Vector3 h = Vector3.Cross(ray.direction, e2); + + float det = Vector3.Dot(e1, h); + + if (Mathf.Abs(det) < 1e-6f) + return false; + + float f = 1f / det; + Vector3 s = ray.origin - v0; + + u = f * Vector3.Dot(s, h); + + if (u < 0f || u > 1f) + return false; + + Vector3 q = Vector3.Cross(s, e1); + + v = f * Vector3.Dot(ray.direction, q); + + if (v < 0f || u + v > 1f) + return false; + + dist = f * Vector3.Dot(e2, q); + + return dist > 1e-5f; + } + + private static int FindNearestVertex(float u, float v, float w) + { + if (w >= u && w >= v) + return 0; + + if (u >= w && u >= v) + return 1; + + return 2; + } + + private void DrawPivotGizmo(Vector3 pos, Color color) + { + Handles.color = color; + + float handleSize = HandleUtility.GetHandleSize(pos); + + Handles.SphereHandleCap( + 0, + pos, + Quaternion.identity, + GIZMO_RADIUS * handleSize, + EventType.Repaint); + + float sz = GIZMO_CROSS * handleSize; + + Handles.DrawLine(pos - Vector3.right * sz, pos + Vector3.right * sz); + Handles.DrawLine(pos - Vector3.up * sz, pos + Vector3.up * sz); + Handles.DrawLine(pos - Vector3.forward * sz, pos + Vector3.forward * sz); + + GUIStyle s = new GUIStyle(EditorStyles.miniLabel); + s.normal.textColor = color; + + string label = hasPivot && Vector3.Distance(pos, pivotWorld) < 0.0001f ? "PIVOT" : "point"; + + Handles.Label(pos + Vector3.up * sz * 1.5f, label, s); + } + + private void BeginPicking() + { + if (sourceObject == null) + return; + + state = ToolState.Picking; + hasHighlight = false; + + SceneView.RepaintAll(); + } + + private void CancelPicking() + { + if (state == ToolState.Picking) + state = hasPivot ? ToolState.Transforming : ToolState.Idle; + + hasHighlight = false; + + SceneView.RepaintAll(); + } + + private void ConfirmPivot(Vector3 worldPoint) + { + if (hasPivot && state == ToolState.Transforming) + RevertTransform(); + + pivotWorld = worldPoint; + hasPivot = true; + state = ToolState.Transforming; + hasHighlight = false; + + basePosition = sourceObject.transform.position; + baseRotation = sourceObject.transform.rotation; + baseScale = sourceObject.transform.localScale; + + rotX = 0f; + rotY = 0f; + rotZ = 0f; + + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + scaleX = 1f; + scaleY = 1f; + scaleZ = 1f; + uniformScaleValue = 1f; + + posOffsetX = 0f; + posOffsetY = 0f; + posOffsetZ = 0f; + + Repaint(); + SceneView.RepaintAll(); + } + + private void ApplyAll(string undoLabel = "Pivot Aligner") + { + if (sourceObject == null || !hasPivot) + return; + + int hot = GUIUtility.hotControl; + + if (hot != lastHotControl || undoLabel != lastUndoLabel) + { + undoGroupIndex++; + lastHotControl = hot; + lastUndoLabel = undoLabel; + } + + Undo.RecordObject(sourceObject.transform, undoLabel); + + Quaternion deltaRotation = Quaternion.Euler( + rotX + fineX, + rotY + fineY, + rotZ + fineZ); + + Vector3 scaleMultiplier = new Vector3(scaleX, scaleY, scaleZ); + Vector3 positionOffset = new Vector3(posOffsetX, posOffsetY, posOffsetZ); + + Vector3 baseOffset = basePosition - pivotWorld; + + Vector3 scaledOffset = new Vector3( + baseOffset.x * scaleMultiplier.x, + baseOffset.y * scaleMultiplier.y, + baseOffset.z * scaleMultiplier.z); + + sourceObject.transform.position = + pivotWorld + + deltaRotation * scaledOffset + + positionOffset; + + sourceObject.transform.rotation = deltaRotation * baseRotation; + sourceObject.transform.localScale = Vector3.Scale(baseScale, scaleMultiplier); + + Undo.CollapseUndoOperations(Undo.GetCurrentGroup() - undoGroupIndex + 1); + } + + private void ResetRotation() + { + rotX = 0f; + rotY = 0f; + rotZ = 0f; + + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + ApplyAll("Pivot Aligner - Reset Rotation"); + } + + private void ResetScale() + { + scaleX = 1f; + scaleY = 1f; + scaleZ = 1f; + uniformScaleValue = 1f; + + ApplyAll("Pivot Aligner - Reset Scale"); + } + + private void ResetPositionOffset() + { + posOffsetX = 0f; + posOffsetY = 0f; + posOffsetZ = 0f; + + ApplyAll("Pivot Aligner - Reset Offset"); + } + + private void Apply() + { + if (sourceObject == null) + return; + + Undo.SetCurrentGroupName("Pivot Aligner Apply"); + Undo.CollapseUndoOperations(Undo.GetCurrentGroup()); + + ResetTool(); + } + + private void RevertAndReset() + { + RevertTransform(); + ResetTool(); + } + + private void RevertTransform() + { + if (sourceObject == null || !hasPivot) + return; + + Undo.RecordObject(sourceObject.transform, "Pivot Aligner Revert"); + + sourceObject.transform.position = basePosition; + sourceObject.transform.rotation = baseRotation; + sourceObject.transform.localScale = baseScale; + } + + private void ResetTool() + { + state = ToolState.Idle; + + hasPivot = false; + hasHighlight = false; + + rotX = 0f; + rotY = 0f; + rotZ = 0f; + + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + scaleX = 1f; + scaleY = 1f; + scaleZ = 1f; + uniformScaleValue = 1f; + + posOffsetX = 0f; + posOffsetY = 0f; + posOffsetZ = 0f; + + SceneView.RepaintAll(); + Repaint(); + } + + private void InitStyles() + { + if (stylesInit) + return; + + stylesInit = true; + + headerStyle = new GUIStyle(EditorStyles.boldLabel); + headerStyle.fontSize = 13; + headerStyle.alignment = TextAnchor.MiddleCenter; + headerStyle.normal.textColor = new Color(0.65f, 0.88f, 1f); + + sectionStyle = new GUIStyle(EditorStyles.boldLabel); + sectionStyle.normal.textColor = new Color(0.85f, 0.85f, 0.85f); + + stateStyle = new GUIStyle(EditorStyles.miniLabel); + stateStyle.alignment = TextAnchor.MiddleRight; + stateStyle.normal.textColor = new Color(0.40f, 0.75f, 0.50f); + + subLabelStyle = new GUIStyle(EditorStyles.miniLabel); + subLabelStyle.normal.textColor = new Color(0.55f, 0.55f, 0.55f); + } + + private void DrawHR() + { + Rect r = EditorGUILayout.GetControlRect(false, 1); + EditorGUI.DrawRect(r, new Color(0.35f, 0.35f, 0.35f, 0.6f)); + } + } +} diff --git a/Assets/Scripts/Editor/Tools/README.md b/Assets/Scripts/Editor/Tools/README.md index 20d6e2a..fcdb9c8 100644 --- a/Assets/Scripts/Editor/Tools/README.md +++ b/Assets/Scripts/Editor/Tools/README.md @@ -1,2 +1,5 @@ ### GameViewGridOverlay.cs ![Image](https://github.com/user-attachments/assets/48fbced4-48e0-49fe-9acc-666f5449a958) + +### SceneTextSearchWindow.cs +image diff --git a/Assets/Scripts/Editor/Tools/SceneTextSearchWindow.cs b/Assets/Scripts/Editor/Tools/SceneTextSearchWindow.cs new file mode 100644 index 0000000..8467f3e --- /dev/null +++ b/Assets/Scripts/Editor/Tools/SceneTextSearchWindow.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +namespace UnityLibrary.EditorTools +{ + public class SceneTextSearchWindow : EditorWindow + { + [Serializable] + private class SearchResult + { + public Component component; + public string text; + } + + private string searchTerm = string.Empty; + private string previousSearchTerm = string.Empty; + private bool caseSensitive = false; + private bool automaticSearch = true; + + private readonly List results = new List(); + private readonly HashSet seenComponents = new HashSet(); + private Vector2 scrollPos; + + [MenuItem("Tools/UnityLibrary/Scene Text Search")] + public static void Open() + { + var window = GetWindow("Scene Text Search"); + window.minSize = new Vector2(600, 300); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("Search text in loaded scenes", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.LabelField("Search term", GUILayout.Width(80)); + + string newSearchTerm = EditorGUILayout.TextField(searchTerm); + + if (GUILayout.Button("Search", GUILayout.Width(80))) + { + DoSearch(); + } + + EditorGUILayout.EndHorizontal(); + + if (newSearchTerm != searchTerm) + { + searchTerm = newSearchTerm; + + if (automaticSearch) + { + if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Length > 1) + { + DoSearch(); + } + else + { + results.Clear(); + seenComponents.Clear(); + } + } + + previousSearchTerm = searchTerm; + } + + EditorGUILayout.BeginHorizontal(); + caseSensitive = EditorGUILayout.Toggle("Case sensitive", caseSensitive); + automaticSearch = EditorGUILayout.Toggle("Automatic search", automaticSearch); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField($"Results: {results.Count}", EditorStyles.boldLabel); + + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + foreach (var r in results) + { + if (r.component == null) + continue; + + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.ObjectField(r.component, typeof(Component), true, GUILayout.Width(220)); + + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.TextField(Truncate(r.text, 200)); + } + + EditorGUILayout.EndHorizontal(); + } + EditorGUILayout.EndScrollView(); + } + + private void DoSearch() + { + results.Clear(); + seenComponents.Clear(); + + if (string.IsNullOrEmpty(searchTerm)) + return; + + string term = caseSensitive ? searchTerm : searchTerm.ToLowerInvariant(); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + // UnityEngine.UI.Text + SearchComponents(t => t.text, term); + + // TMP UGUI + SearchComponents(t => t.text, term); + + // TMP 3D + SearchComponents(t => t.text, term); + + // Legacy TextMesh + SearchComponents(t => t.text, term); + + // Generic "other text components" with a string 'text' property/field + SearchGenericTextComponents(term); + + stopwatch.Stop(); + Debug.Log($"SceneTextSearchWindow: Found {results.Count} results in {stopwatch.ElapsedMilliseconds} ms"); + } + + private void SearchComponents(Func getText, string term) where T : Component + { + var objects = Resources.FindObjectsOfTypeAll(); + foreach (var comp in objects) + { + if (!IsSceneObject(comp)) + continue; + + string value = getText(comp); + if (StringMatches(value, term)) + { + AddResult(comp, value); + } + } + } + + private void SearchGenericTextComponents(string term) + { + var monos = Resources.FindObjectsOfTypeAll(); + foreach (var mb in monos) + { + if (!IsSceneObject(mb)) + continue; + + if (seenComponents.Contains(mb)) + continue; + + Type type = mb.GetType(); + + try + { + var prop = type.GetProperty("text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (prop != null && prop.PropertyType == typeof(string) && prop.CanRead) + { + string value = prop.GetValue(mb, null) as string; + if (StringMatches(value, term)) + { + AddResult(mb, value); + continue; + } + } + } + catch + { + } + + try + { + var field = type.GetField("text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field != null && field.FieldType == typeof(string)) + { + string value = field.GetValue(mb) as string; + if (StringMatches(value, term)) + { + AddResult(mb, value); + } + } + } + catch + { + } + } + } + + private bool StringMatches(string value, string term) + { + if (string.IsNullOrEmpty(value)) + return false; + + if (!caseSensitive) + value = value.ToLowerInvariant(); + + return value.Contains(term); + } + + private void AddResult(Component component, string text) + { + if (component == null) + return; + + if (seenComponents.Add(component)) + { + results.Add(new SearchResult + { + component = component, + text = text + }); + } + } + + private static bool IsSceneObject(Component comp) + { + if (comp == null) + return false; + + var go = comp.gameObject; + if (go == null) + return false; + + if (EditorUtility.IsPersistent(go)) + return false; + + if (!go.scene.IsValid() || !go.scene.isLoaded) + return false; + + return true; + } + + private static string Truncate(string input, int maxLength) + { + if (string.IsNullOrEmpty(input)) + return input; + if (input.Length <= maxLength) + return input; + return input.Substring(0, maxLength) + "..."; + } + } +} diff --git a/Assets/Scripts/UI/ToggleEvents.cs b/Assets/Scripts/UI/ToggleEvents.cs new file mode 100644 index 0000000..b4f0952 --- /dev/null +++ b/Assets/Scripts/UI/ToggleEvents.cs @@ -0,0 +1,44 @@ +// easy way to get Toggle state as events (compared to the default OnChanged, which triggers on both..) + +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; + +namespace UnityLibrary.UI +{ + [RequireComponent(typeof(Toggle))] + public class ToggleEvents : MonoBehaviour + { + [Header("Events")] + public UnityEvent OnChecked; + public UnityEvent OnUnchecked; + + private Toggle toggle; + + private void Awake() + { + toggle = GetComponent(); + toggle.onValueChanged.AddListener(HandleToggleChanged); + } + + private void OnDestroy() + { + if (toggle != null) + { + toggle.onValueChanged.RemoveListener(HandleToggleChanged); + } + } + + private void HandleToggleChanged(bool isOn) + { + if (isOn) + { + OnChecked?.Invoke(); + } + else + { + OnUnchecked?.Invoke(); + } + } + } +} diff --git a/Assets/Shaders/3D/Debug/UVChannelDebug.shader b/Assets/Shaders/3D/Debug/UVChannelDebug.shader new file mode 100644 index 0000000..b0b2c86 --- /dev/null +++ b/Assets/Shaders/3D/Debug/UVChannelDebug.shader @@ -0,0 +1,150 @@ +// debug view what channels contain UV data + +Shader "UnityLibrary/Debug/UVChannelDebug" +{ + Properties + { + _MainTex ("Texture", 2D) = "white" {} + _ColorUV0 ("UV0 Color", Color) = (1,0,0,1) + _ColorUV1 ("UV1 Color", Color) = (0,1,0,1) + _ColorUV2 ("UV2 Color", Color) = (0,0,1,1) + _ColorUV3 ("UV3 Color", Color) = (1,1,0,1) + _ColorUV4 ("UV4 Color", Color) = (1,0,1,1) + _ColorUV5 ("UV5 Color", Color) = (0,1,1,1) + _ColorUV6 ("UV6 Color", Color) = (1,1,1,1) + _ColorUV7 ("UV7 Color", Color) = (0,0,0,1) + + [Toggle] _EnableUV0 ("Enable UV0", Float) = 1 + [Toggle] _EnableUV1 ("Enable UV1", Float) = 1 + [Toggle] _EnableUV2 ("Enable UV2", Float) = 1 + [Toggle] _EnableUV3 ("Enable UV3", Float) = 1 + [Toggle] _EnableUV4 ("Enable UV4", Float) = 1 + [Toggle] _EnableUV5 ("Enable UV5", Float) = 1 + [Toggle] _EnableUV6 ("Enable UV6", Float) = 1 + [Toggle] _EnableUV7 ("Enable UV7", Float) = 1 + } + SubShader + { + Tags { "RenderType"="Opaque" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + + #include "UnityCG.cginc" + + struct appdata + { + float4 vertex : POSITION; + float2 uv0 : TEXCOORD0; + float2 uv1 : TEXCOORD1; + float2 uv2 : TEXCOORD2; + float2 uv3 : TEXCOORD3; + float2 uv4 : TEXCOORD4; + float2 uv5 : TEXCOORD5; + float2 uv6 : TEXCOORD6; + float2 uv7 : TEXCOORD7; + }; + + struct v2f + { + float2 uv0 : TEXCOORD0; + float2 uv1 : TEXCOORD1; + float2 uv2 : TEXCOORD2; + float2 uv3 : TEXCOORD3; + float2 uv4 : TEXCOORD4; + float2 uv5 : TEXCOORD5; + float2 uv6 : TEXCOORD6; + float2 uv7 : TEXCOORD7; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + float4 _ColorUV0; + float4 _ColorUV1; + float4 _ColorUV2; + float4 _ColorUV3; + float4 _ColorUV4; + float4 _ColorUV5; + float4 _ColorUV6; + float4 _ColorUV7; + + float _EnableUV0; + float _EnableUV1; + float _EnableUV2; + float _EnableUV3; + float _EnableUV4; + float _EnableUV5; + float _EnableUV6; + float _EnableUV7; + + fixed UVChecker(float2 uv) + { + float2 cell = floor(uv * 8.0); + return fmod(cell.x + cell.y, 2.0); + } + + fixed HasUVData(float2 uv) + { + float variation = fwidth(uv.x) + fwidth(uv.y); + float magnitude = abs(uv.x) + abs(uv.y); + return step(1e-5, max(variation, magnitude)); + } + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + + o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex); + o.uv1 = TRANSFORM_TEX(v.uv1, _MainTex); + o.uv2 = TRANSFORM_TEX(v.uv2, _MainTex); + o.uv3 = TRANSFORM_TEX(v.uv3, _MainTex); + o.uv4 = TRANSFORM_TEX(v.uv4, _MainTex); + o.uv5 = TRANSFORM_TEX(v.uv5, _MainTex); + o.uv6 = TRANSFORM_TEX(v.uv6, _MainTex); + o.uv7 = TRANSFORM_TEX(v.uv7, _MainTex); + + return o; + } + + fixed4 frag (v2f i) : SV_Target + { + fixed4 col0 = tex2D(_MainTex, i.uv0) * _ColorUV0; + fixed4 col1 = tex2D(_MainTex, i.uv1) * _ColorUV1; + fixed4 col2 = tex2D(_MainTex, i.uv2) * _ColorUV2; + fixed4 col3 = tex2D(_MainTex, i.uv3) * _ColorUV3; + fixed4 col4 = tex2D(_MainTex, i.uv4) * _ColorUV4; + fixed4 col5 = tex2D(_MainTex, i.uv5) * _ColorUV5; + fixed4 col6 = tex2D(_MainTex, i.uv6) * _ColorUV6; + fixed4 col7 = tex2D(_MainTex, i.uv7) * _ColorUV7; + + float e0 = _EnableUV0 * HasUVData(i.uv0); + float e1 = _EnableUV1 * HasUVData(i.uv1); + float e2 = _EnableUV2 * HasUVData(i.uv2); + float e3 = _EnableUV3 * HasUVData(i.uv3); + float e4 = _EnableUV4 * HasUVData(i.uv4); + float e5 = _EnableUV5 * HasUVData(i.uv5); + float e6 = _EnableUV6 * HasUVData(i.uv6); + float e7 = _EnableUV7 * HasUVData(i.uv7); + + fixed4 col = + col0 * e0 + + col1 * e1 + + col2 * e2 + + col3 * e3 + + col4 * e4 + + col5 * e5 + + col6 * e6 + + col7 * e7; + + return saturate(col); + } + ENDCG + } + } +}