Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active March 3, 2026 08:43
Show Gist options
  • Select an option

  • Save yasirkula/06edc780beaa4d8705b3564d60886fa6 to your computer and use it in GitHub Desktop.

Select an option

Save yasirkula/06edc780beaa4d8705b3564d60886fa6 to your computer and use it in GitHub Desktop.
Select the object under the cursor via right click in Unity's Scene window
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;
public class SceneViewObjectPickerContextWindow : EditorWindow
{
private class Entry
{
public readonly Transform Transform;
public readonly List<Entry> Children;
public Entry(Transform transform)
{
Transform = transform;
Children = new List<Entry>(2);
}
}
private readonly List<Transform> transforms = new(16);
private readonly List<string> labels = new(16);
private static Transform hoveredTransform;
private static readonly Vector3[] hoveredTransformCorners = new Vector3[4];
private static double lastRightClickTime;
private static Vector2 lastRightPos;
private static bool blockSceneViewInput;
private static MethodInfo screenFittedRectGetter;
private static FieldInfo editorWindowHostViewGetter;
private static PropertyInfo hostViewContainerWindowGetter;
private const float Padding = 1f;
private float RowHeight => EditorGUIUtility.singleLineHeight;
private GUIStyle RowGUIStyle => "MenuItem";
private static bool CanPickUIObjects
{
get => EditorPrefs.GetBool("SVOPCWUI", true);
set => EditorPrefs.SetBool("SVOPCWUI", value);
}
private static bool CanPickSpriteRenderers
{
get => EditorPrefs.GetBool("SVOPCWSR", true);
set => EditorPrefs.SetBool("SVOPCWSR", value);
}
private static bool CanPickOtherRenderers
{
get => EditorPrefs.GetBool("SVOPCWOR", true);
set => EditorPrefs.SetBool("SVOPCWOR", value);
}
#if UNITY_2022_1_OR_NEWER
private static bool ShowPreciseRendererOutline
{
get => EditorPrefs.GetBool("SVOPCWPRO", true);
set => EditorPrefs.SetBool("SVOPCWPRO", value);
}
#endif
private void ShowContextWindow(List<Entry> entries)
{
StringBuilder sb = new StringBuilder(100);
InitializeResultsRecursive(entries, 0, sb);
GUIStyle rowGUIStyle = RowGUIStyle;
float preferredWidth = 0f;
foreach (string label in labels)
preferredWidth = Mathf.Max(preferredWidth, rowGUIStyle.CalcSize(new GUIContent(label)).x);
Vector2 size = new Vector2(preferredWidth + Padding * 2f, transforms.Count * RowHeight + Padding * 2f);
ShowAsDropDown(new Rect(), size);
Rect rect = new Rect(GUIUtility.GUIToScreenPoint(Event.current.mousePosition) - new Vector2(0f, size.y), size); // Show dropdown above the cursor instead of below the cursor
minSize = maxSize = size; /// These values are changed by <see cref="EditorWindow.ShowAsDropDown"/> so reset them once again
position = GetScreenFittedRect(rect, this);
}
private void InitializeResultsRecursive(List<Entry> entries, int depth, StringBuilder sb)
{
foreach (Entry entry in entries)
{
sb.Length = 0;
transforms.Add(entry.Transform);
labels.Add(sb.Append(' ', depth * 4).Append(entry.Transform.name).ToString());
if (entry.Children.Count > 0)
InitializeResultsRecursive(entry.Children, depth + 1, sb);
}
}
protected void OnEnable()
{
wantsMouseMove = wantsMouseEnterLeaveWindow = true;
wantsLessLayoutEvents = false;
blockSceneViewInput = true;
}
protected void OnDisable()
{
hoveredTransform = null;
SceneView.RepaintAll();
}
protected void OnGUI()
{
Event ev = Event.current;
float rowWidth = position.width - Padding * 2f, rowHeight = RowHeight;
GUIStyle rowGUIStyle = RowGUIStyle;
int hoveredRowIndex = -1;
for (int i = 0; i < transforms.Count; i++)
{
Rect rect = new Rect(Padding, Padding + i * rowHeight, rowWidth, rowHeight);
if (GUI.Button(rect, labels[i], rowGUIStyle))
{
if (transforms[i] != null)
Selection.activeTransform = transforms[i];
blockSceneViewInput = false;
ev.Use();
Close();
GUIUtility.ExitGUI();
}
if (hoveredRowIndex < 0 && ev.type == EventType.MouseMove && rect.Contains(ev.mousePosition))
hoveredRowIndex = i;
}
if (ev.type == EventType.MouseMove || ev.type == EventType.MouseLeaveWindow)
{
Transform newHoveredTransform = (hoveredRowIndex >= 0) ? transforms[hoveredRowIndex] : null;
if (newHoveredTransform != hoveredTransform)
{
hoveredTransform = newHoveredTransform;
Repaint();
SceneView.RepaintAll();
}
}
}
[InitializeOnLoadMethod]
private static void InitializeDuringSceneGUI() => SceneView.duringSceneGui += DuringSceneGUI;
private static void DuringSceneGUI(SceneView sceneView)
{
/// Couldn't get <see cref="EventType.ContextClick"/> to work here in Unity 5.6 so implemented context click detection manually
Event ev = Event.current;
if (ev.type == EventType.MouseDown)
{
if (ev.button == 1)
{
lastRightClickTime = EditorApplication.timeSinceStartup;
lastRightPos = ev.mousePosition;
}
else if (blockSceneViewInput)
{
// User has clicked outside the context window to close it. Ignore this click in Scene view if it's left click
blockSceneViewInput = false;
if (ev.button == 0)
{
GUIUtility.hotControl = 0;
ev.Use();
}
}
}
else if (ev.type == EventType.MouseUp)
{
if (ev.button == 1 && EditorApplication.timeSinceStartup - lastRightClickTime < 0.2 && (ev.mousePosition - lastRightPos).magnitude < 2f)
OnSceneViewRightClicked(sceneView);
}
else if (ev.type == EventType.Repaint)
HighlightHoveredTransformOnSceneView();
}
private static void HighlightHoveredTransformOnSceneView()
{
if (hoveredTransform == null)
return;
if (hoveredTransform.TryGetComponent(out Renderer renderer) && renderer.sharedMaterial != null)
{
// Clear depth buffer so that the highlight will be drawn above everything else (i.e. not blocked by z-testing)
GL.Clear(true, false, default);
// First, redraw the object above everything else (thanks to clearing the depth buffer before)
if (hoveredTransform.TryGetComponent(out MeshFilter meshFilter) && meshFilter.sharedMesh != null)
{
if (renderer.sharedMaterial.SetPass(0))
Graphics.DrawMeshNow(meshFilter.sharedMesh, hoveredTransform.localToWorldMatrix);
}
else if (renderer is SkinnedMeshRenderer skinnedMeshRenderer && skinnedMeshRenderer.sharedMesh != null)
{
Mesh mesh = new();
try
{
skinnedMeshRenderer.BakeMesh(mesh, true);
if (renderer.sharedMaterial.SetPass(0))
Graphics.DrawMeshNow(mesh, hoveredTransform.localToWorldMatrix);
}
finally
{
DestroyImmediate(mesh);
}
}
else if (renderer is SpriteRenderer spriteRenderer && spriteRenderer.sprite != null)
{
Mesh spriteMesh = new()
{
vertices = Array.ConvertAll(spriteRenderer.sprite.vertices, (e) => (Vector3)e),
uv = spriteRenderer.sprite.uv,
triangles = Array.ConvertAll(spriteRenderer.sprite.triangles, (e) => (int)e),
};
Material material = new(renderer.sharedMaterial) { mainTexture = spriteRenderer.sprite.texture };
try
{
if (material.SetPass(0))
Graphics.DrawMeshNow(spriteMesh, hoveredTransform.localToWorldMatrix);
}
finally
{
DestroyImmediate(spriteMesh);
DestroyImmediate(material);
}
}
#if UNITY_2022_1_OR_NEWER
if (ShowPreciseRendererOutline)
{
GameObject[] rendererGO = new GameObject[] { renderer.gameObject };
Handles.DrawOutline(rendererGO, new Color(1f, 1f, 0f, 1f), 0.35f);
Handles.DrawOutline(rendererGO, Color.black, 0f);
}
else
#endif
{
Bounds bounds = renderer.localBounds;
if (renderer is SpriteRenderer)
{
hoveredTransformCorners[0] = hoveredTransform.TransformPoint(bounds.center + new Vector3(-bounds.extents.x, -bounds.extents.y, 0f));
hoveredTransformCorners[1] = hoveredTransform.TransformPoint(bounds.center + new Vector3(-bounds.extents.x, bounds.extents.y, 0f));
hoveredTransformCorners[2] = hoveredTransform.TransformPoint(bounds.center + new Vector3(bounds.extents.x, bounds.extents.y, 0f));
hoveredTransformCorners[3] = hoveredTransform.TransformPoint(bounds.center + new Vector3(bounds.extents.x, -bounds.extents.y, 0f));
Handles.DrawSolidRectangleWithOutline(hoveredTransformCorners, new Color(1f, 1f, 0f, 0.25f), Color.black);
}
else
{
// Draw the bounds of the renderer
Transform pivot = (renderer is SkinnedMeshRenderer skinnedMeshRenderer) ? skinnedMeshRenderer.rootBone : hoveredTransform;
using (new Handles.DrawingScope(new Color(1f, 1f, 0f, 0.25f), Matrix4x4.TRS(pivot.TransformPoint(bounds.center), pivot.rotation, Vector3.Scale(pivot.lossyScale, bounds.size))))
{
bool lighting = Handles.lighting;
Handles.lighting = false;
Handles.CubeHandleCap(0, Vector3.zero, Quaternion.identity, 1f, EventType.Repaint);
Handles.color = Color.black;
Handles.DrawWireCube(Vector3.zero, Vector3.one);
Handles.lighting = lighting;
}
}
}
}
else if (hoveredTransform is RectTransform rectTransform)
{
rectTransform.GetWorldCorners(hoveredTransformCorners);
Handles.DrawSolidRectangleWithOutline(hoveredTransformCorners, new Color(1f, 1f, 0f, 0.25f), Color.black);
}
}
private static void OnSceneViewRightClicked(SceneView sceneView)
{
Vector2 eventMousePos = Event.current.mousePosition;
Vector2 pointerPos = HandleUtility.GUIPointToScreenPixelCoordinate(eventMousePos);
List<Entry> entries = new(8);
List<GameObject> iteratedGameObjects = new(8);
while (true)
{
// Find all GameObjects under the cursor. Logic copied from Unity's own context menu that shows up with CTRL+RMB on Unity 6+.
// https://github.com/Unity-Technologies/UnityCsReference/blob/59b03b8a0f179c0b7e038178c90b6c80b340aa9f/Editor/Mono/SceneView/SceneViewPicking.cs#L147-L178
GameObject gameObject = HandleUtility.PickGameObject(eventMousePos, false, iteratedGameObjects.ToArray(), null);
if (gameObject == null || (entries.Count > 0 && gameObject == entries[^1].Transform.gameObject))
break;
iteratedGameObjects.Add(gameObject);
if (IsGameObjectValid(gameObject, pointerPos, sceneView.camera))
entries.Add(new(gameObject.transform));
}
// Form parent-child relationships
List<Entry> rootEntries = new(entries.Count);
for (int i = entries.Count - 1; i >= 0; i--)
{
Entry entry = entries[i];
Entry parentEntry = null;
int distanceToParentEntry = int.MaxValue;
foreach (Entry candidateParentEntry in entries)
{
if (entry == candidateParentEntry)
continue;
if (entry.Transform.IsChildOf(candidateParentEntry.Transform))
{
int distance = 1;
for (Transform parent = entry.Transform.parent; parent != candidateParentEntry.Transform; parent = parent.parent)
distance++;
if (distance < distanceToParentEntry)
(parentEntry, distanceToParentEntry) = (candidateParentEntry, distance);
}
}
if (parentEntry != null)
parentEntry.Children.Add(entry);
else
rootEntries.Add(entry);
}
// Remove invisible root entries with no children from the results
rootEntries.RemoveAll((canvasEntry) => canvasEntry.Children.Count == 0 && !canvasEntry.Transform.TryGetComponent(out Graphic _) && !canvasEntry.Transform.TryGetComponent(out Renderer _));
// Sort root entries in reverse order (Transform that is closest to cursor should be located at the bottom)
rootEntries.Sort((e1, e2) =>
{
if (e1.Transform.GetComponentInParent<Canvas>(true) is Canvas c1 && e2.Transform.GetComponentInParent<Canvas>(true) is Canvas c2)
{
// Both entries are UI elements, sort by their Canvas sorting orders
int sortingOrderComparison = c1.sortingOrder.CompareTo(c2.sortingOrder);
if (sortingOrderComparison != 0)
return sortingOrderComparison;
}
// Sort by the entries' original order returned by Unity
return entries.IndexOf(e2).CompareTo(entries.IndexOf(e1));
});
// If any results are found, show the context window
if (rootEntries.Count > 0)
CreateInstance<SceneViewObjectPickerContextWindow>().ShowContextWindow(rootEntries);
}
private static bool IsGameObjectValid(GameObject gameObject, Vector2 pointerPos, Camera camera)
{
if (SceneVisibilityManager.instance.IsHidden(gameObject, false))
return false;
if (SceneVisibilityManager.instance.IsPickingDisabled(gameObject, false))
return false;
if (gameObject.TryGetComponent(out Renderer renderer))
{
if (!renderer.enabled)
return false;
return (renderer is SpriteRenderer) ? CanPickSpriteRenderers : CanPickOtherRenderers;
}
if (gameObject.transform is not RectTransform)
return false;
if (!CanPickUIObjects)
return false;
if (gameObject.TryGetComponent(out CanvasRenderer canvasRenderer) && canvasRenderer.cull)
return false;
if (gameObject.TryGetComponent(out Graphic graphic) && !graphic.enabled)
return false;
if (gameObject.GetComponentInParent<Canvas>(true) is Canvas canvas && !canvas.enabled)
return false;
using (ListPool<CanvasGroup>.Get(out var canvasGroups))
{
gameObject.GetComponentsInParent(false, canvasGroups);
foreach (CanvasGroup canvasGroup in canvasGroups)
{
if (!canvasGroup.enabled)
continue;
else if (canvasGroup.alpha == 0f)
return false;
else if (canvasGroup.ignoreParentGroups)
break;
}
}
// If the target is a MaskableGraphic that ignores masks (i.e. visible outside masks) and isn't fully transparent, accept it
if (gameObject.TryGetComponent(out MaskableGraphic maskableGraphic) && !maskableGraphic.maskable && maskableGraphic.color.a > 0f)
return true;
using (ListPool<ICanvasRaycastFilter>.Get(out var raycastFilters))
{
gameObject.GetComponentsInParent(false, raycastFilters);
foreach (ICanvasRaycastFilter raycastFilter in raycastFilters)
{
if (!raycastFilter.IsRaycastLocationValid(pointerPos, camera))
return false;
}
}
return true;
}
/// <summary>
/// Restricts the given Rect within the screen's bounds.
/// </summary>
private static Rect GetScreenFittedRect(Rect originalRect, EditorWindow editorWindow)
{
screenFittedRectGetter ??= typeof(EditorWindow).Assembly.GetType("UnityEditor.ContainerWindow").GetMethod("FitRectToScreen", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
if (screenFittedRectGetter.GetParameters().Length == 3)
return (Rect)screenFittedRectGetter.Invoke(null, new object[] { originalRect, true, true });
else
{
// New version introduced in Unity 2022.3.62f1, Unity 6.0.49f1 and Unity 6.1.0f1.
// Usage example: https://github.com/Unity-Technologies/UnityCsReference/blob/10f8718268a7e34844ba7d59792117c28d75a99b/Editor/Mono/EditorWindow.cs#L1264
editorWindowHostViewGetter ??= typeof(EditorWindow).GetField("m_Parent", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
hostViewContainerWindowGetter ??= typeof(EditorWindow).Assembly.GetType("UnityEditor.HostView").GetProperty("window", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
return (Rect)screenFittedRectGetter.Invoke(null, new object[] { originalRect, originalRect.center, true, hostViewContainerWindowGetter.GetValue(editorWindowHostViewGetter.GetValue(editorWindow), null) });
}
}
[SettingsProvider]
public static SettingsProvider CreatePreferencesGUI()
{
return new SettingsProvider("Project/yasirkula/Scene View Object Picker Context Window", SettingsScope.Project)
{
guiHandler = (searchContext) => PreferencesGUI(),
keywords = new HashSet<string>() { "Scene", "View", "Object", "Picker", "Context", "Window" }
};
}
public static void PreferencesGUI()
{
EditorGUI.BeginChangeCheck();
CanPickUIObjects = EditorGUILayout.ToggleLeft("Can pick UI objects", CanPickUIObjects);
CanPickSpriteRenderers = EditorGUILayout.ToggleLeft("Can pick SpriteRenderers", CanPickSpriteRenderers);
CanPickOtherRenderers = EditorGUILayout.ToggleLeft("Can pick other Renderers (e.g. MeshRenderer)", CanPickOtherRenderers);
#if UNITY_2022_1_OR_NEWER
ShowPreciseRendererOutline = EditorGUILayout.ToggleLeft("Show precise outline for hovered Renderers", ShowPreciseRendererOutline);
#endif
}
}
@yasirkula
Copy link
Author

@HollyRivay I think, for a Collider-free solution, I'd iterate over all Renderers in the scene and raycast against their localBounds (you can get a Ray via HandleUtility.GUIPointToWorldRay(Event.current.position)). Note that you'd have to convert the ray from world space to local space since localBounds is also in local space. To highlight an object, I'd either draw a transparent cube via Handles class or figure out a way to draw a transparent unlit mesh via other functions Unity provide.

@MarkZaytsev
Copy link

MarkZaytsev commented May 26, 2025

@yasirkula Thank you for the tool, it is awesome. But it throws exception in Unity 6.1. It seems they changed the signature for UnityEditor.ContainerWindow.FitRectToScreen. Can you please update the code?
I think the method call should look something like this:

    position = (Rect) screenFittedRectGetter.Invoke(null,
            new object[4]
            {
                new Rect(
                    GUIUtility.GUIToScreenPoint(Event.current.mousePosition)
                    - new Vector2(0f, position.height), position.size),
                Event.current.mousePosition,
                true,
                mouseOverWindow
            });

At least that has fixed error for me.

@yasirkula
Copy link
Author

@MarkZaytsev Thank you for bringing this to my attention. It affects some of my other plugins as well. I've tried resolving it by calling FitRectToScreen the same way EditorWindow does: https://github.com/Unity-Technologies/UnityCsReference/blob/10f8718268a7e34844ba7d59792117c28d75a99b/Editor/Mono/EditorWindow.cs#L1264

@MarkZaytsev
Copy link

@MarkZaytsev Thank you for bringing this to my attention. It affects some of my other plugins as well. I've tried resolving it by calling FitRectToScreen the same way EditorWindow does: https://github.com/Unity-Technologies/UnityCsReference/blob/10f8718268a7e34844ba7d59792117c28d75a99b/Editor/Mono/EditorWindow.cs#L1264

Great, thanks for the quick update)

@PoyrazGoksel
Copy link

To select only visible you could add

                if(rectTransform.GetComponents<Graphic>().Any(e => e.enabled))
                {
                    Entry entry = new(rectTransform);
                    result.Add(entry);
                    result = entry.Children;
                }

In this fuction

        private static void CheckRectTransformRecursive
        (
            RectTransform rectTransform,
            Vector2 pointerPos,
            Camera camera,
            bool culledByCanvasGroup,
            List<Entry> result
        )
        {
            if(RectTransformUtility.RectangleContainsScreenPoint
                (rectTransform, pointerPos, camera)
                && ShouldCheckRectTransform
                (
                    rectTransform,
                    pointerPos,
                    camera,
                    ref culledByCanvasGroup
                ))
            {
                // Show Only Visible
                if(rectTransform.GetComponents<Graphic>().Any(e => e.enabled))
                {
                    Entry entry = new(rectTransform);
                    result.Add(entry);
                    result = entry.Children;
                }
            }

            for(int i = 0,
                childCount = rectTransform.childCount;
                i < childCount;
                i ++)
            {
                RectTransform childRectTransform = rectTransform.GetChild(i) as RectTransform;

                if(childRectTransform != null && childRectTransform.gameObject.activeSelf)
                    CheckRectTransformRecursive
                    (
                        childRectTransform,
                        pointerPos,
                        camera,
                        culledByCanvasGroup,
                        result
                    );
            }
        }

@yasirkula
Copy link
Author

Updated the script to support 3D objects, as well (toggleable via Project Settings):

SceneViewObjectPickerContextWindow

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment