Recommended/default scale in Windows 10 breaks SceneView mouse position

I’m using 2020.2.0a19.

I’ve isolated the cause and have a workaround. If this has been posted/reported before, I apologize.

In Windows 10, Display Settings:
6208017--681849--win_10_display_settings.jpg

If you use the Recommended setting of 125% (which it was set to by default), you have to multiply Event.current.mousePosition by 1.25f.

This is the workaround:

float windowsScale = 1.0f; // must be 1.25f if using 125%
Vector2 screenPoint = Event.current.mousePosition * windowsScale;
screenPoint.y = sv.camera.pixelHeight - screenPoint.y;

// Raycast in SceneView will only hit exactly at mouse position with the above.

SceneView sv = SceneView.lastActiveSceneView;
PhysicsScene ps = sv.camera.scene.GetPhysicsScene();
Ray ray = sv.camera.ScreenPointToRay(screenPoint);
if (ps.Raycast(ray.origin, ray.direction, out RaycastHit hit))
{
  Handles.DrawSolidDisc(hit.point, hit.normal, 0.25f);
}
4 Likes

That’s interesting, since my is 100% on a 3440x1440 resolution

6213479--682919--upload_2020-8-17_23-12-2.png

What resolution/monitor do you use? Must be 4k if they’re recommending scaling? Or maybe a laptop screen?

(regardless this is obviously an issue)

Laptop, 1080p

You can use this class to retrieve the current scale factor instead of hard-coding it:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using UnityEditor;

public static class WindowScaleFactor
{
    public static float Current
    {
        get
        {
        #if UNITY_EDITOR_WIN
            return GetDpiForWindow(editorWindow) / 96f;
        #else
            return 1;
        #endif
        }
    }

#if UNITY_EDITOR_WIN
    static IntPtr editorWindow;

    [DllImport("user32")]
    extern static int GetDpiForWindow(IntPtr hWnd);

    [DllImport("user32")]
    extern static int GetWindowThreadProcessId(IntPtr hWnd, out int processId);

    [DllImport("user32")]
    [return: MarshalAs(UnmanagedType.Bool)]
    extern static bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

    [UnmanagedFunctionPointer(CallingConvention.Winapi)]
    [return: MarshalAs(UnmanagedType.Bool)]
    delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    static bool WindowHandleFromProcessId(IntPtr hWnd, IntPtr lParam)
    {
        GetWindowThreadProcessId(hWnd, out int processId);

        if (lParam.ToInt32() == processId)
        {
            editorWindow = hWnd;
            return false;
        }

        return true;
    }

    [InitializeOnLoadMethod]
    static void Initialize()
    {
        EditorApplication.delayCall += () =>
        {
            var process = Process.GetCurrentProcess();
            EnumWindows(WindowHandleFromProcessId, new IntPtr(process.Id));
        };
    }
#endif
}
2 Likes

@TheZombieKiller :sunglasses:

I’ve integrated your solution - works for me!

camera.ScreenPointToRay - needs the scaling applied to mousePosition
HandleUtility.GUIPointToWorldRay - does NOT need the scaling applied to mousePosition

I was about to post that this was only working in some places and not others and I couldn’t figure out why… then took another look and noticed.

Edit: Note that ScreenPointToRay only has this issue in the Editor with a SceneView camera and Event.current.mousePosition, but it works fine in Play Mode / builds with normal camera and Input.mousePosition.

@TheZombieKiller There have been occasions where it would start returning 0.0f at some random time after working in the Editor for a while. It returns the proper value again as soon as you go change scaling in Windows display settings (without reloading the editor or anything). I added a fallback but do you have any idea why that might happen?

#if UNITY_EDITOR_WIN // line 12
      float windowDPI = GetDpiForWindow(editorWindow) / 96f;
      return (windowDPI != 0) ? windowDPI : 1.0f; // fallback

I think GetDpiForWindow will return zero for an invalid or null window handle, so maybe the editorWindow pointer is becoming invalid somehow? Changing the scaling in Windows display settings will cause an editor reload, which would cause the code to search for the window handle again, so it’s a possibility.

You could use this function to check if it’s still a valid window:

[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
extern static bool IsWindow(IntPtr hWnd);

And then call Initialize() if it returns false. You can also try just calling that and re-attempting to get the DPI when you get zero.

EDIT: Scratch that, since Initialize uses delayCall, editorWindow wouldn’t be set immediately after. Calling EnumWindows with the appropriate callback and process id instead is what should be done.

1 Like

The cross-platform solution is to only use the SceneView camera to get the Physics Scene and using HandleUtility.GUIPointToWorldRay instead of ScreenPointToRay during Edit Mode.

First, a generic GetCurrentPhysicsScene function. Works for Edit mode, Prefab Edit mode and Play mode:

public static PhysicsScene GetCurrentPhysicsScene()
{
#if UNITY_EDITOR
  if (Application.isPlaying) return Physics.defaultPhysicsScene;
  if ((SceneView.lastActiveSceneView != null) && (SceneView.lastActiveSceneView.camera != null) && SceneView.lastActiveSceneView.camera.scene.IsValid()) return SceneView.lastActiveSceneView.camera.scene.GetPhysicsScene();
#endif
  return Physics.defaultPhysicsScene;
}

Then:

Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
if (GetCurrentPhysicsScene().Raycast(...
1 Like

Hi @adamgolden ,

Could you please submit a bug report for this issue so we can have a look at it? That would be very helpful in order to get this investigated.

Sure - will do later this morning.

@LeonhardP I’ve confirmed this is still a problem in 2020.2.0b1, existing in both normal Edit mode and Prefab Edit mode.

Case 1275281: Windows Scale breaks SceneView Camera ScreenPointToRay

Edit: Also exists in 2019.4.9f1

Thanks a lot! We’ll look into it.

1 Like

Let us know when there’s a publicly available entry in the issue tracker. This bug breaks some tools we have. It’s already clunky enough to get the mouse position in the scene view, so it’s annoying that that’s broken now.

Note that we’re seeing this bug in 2020.1, so it’s not a 2020.2 issue.

Hello!

The first issue - Physics.defaultPhysicsScene not working in Prefab Edit Mode - is intentional behavior. The PreviewScene used for Prefab Mode uses a local physics scene in order to not ‘pollute’ the default physics world. Prefab Mode has its local editing scene separate from the main scenes (Game scenes). In the repro project from the bug report, raycasting against colliders in Prefab Mode is done in the right way: by getting the local physics and making queries against that. Unity will never instantiate the Prefab Mode objects into the Game world as that would break the game logic there.

The second issue - Event.current.mousePosition giving incorrect coordinates when WindowsScale is not set to 100% - is also expected behavior. In the Game View, the screen coordinates are in pixels, while In the GUI, coordinates are in units independent from the current window scaling. To convert between them, you can multiply/divide by GUIUtility.pixelsPerPoint.

The third issue - Y coordinate of Event.mousePosition is upside down in scene view - is also not a bug, as the point of origin of Event.mousePosition and Camera.ScreenPointToRay is different. As stated in the documentation, Event.mousePosition uses the top-left corner as the origin, while Camera.ScreenPointToRay uses the bottom-left corner as the origin. More information:

2 Likes

Hi @Domas_L - thanks for looking into this and for the detailed explanation. Confirmed it’s all good with a multiplier of EditorGUIUtility.pixelsPerPoint. Thanks again!

@Domas_L :

Another important change is that in addition to #bugs, #regressions, and #crashes, we are incorporating broken workflows and interoperability in our definition as bugs and logging them as such.

Having to multiply by a value every. single. time. we use Event.current.mousePosition is a broken workflow.

Or, rather, we really need a way to get a ray to the mouse in the scene view that’s not dependent on jumping between three different places in the docs to find out how to do that.

4 Likes

@Baste Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); was mentioned earlier, I’m not sure if you noticed that post but it’s what I’ve switched to and so far has been working as expected.

1 Like

Since I came back to this and it might be useful for others;

HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); works, but only during OnSceneGUI. You often want the mouse position at other points - like when you invoke a [MenuItem] or [Shortcut]. At those times, the position is wrong.

The solution I’ve found that has the least hassle is to simply:

[Shortcut("Rain/Play From Mouse Pos", KeyCode.P, ShortcutModifiers.Shift)]
public static void PlayFromMousePos() {
    SceneView.duringSceneGui += PlayFromMousePosImpl;
}

private static void PlayFromMousePosImpl(SceneView obj) {
    var mousePos = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition).origin;
    PlayFrom(mousePos);
    SceneView.duringSceneGui -= PlayFromMousePosImpl;
}

Again, it would be very nice if we had a “works at all times, don’t need to think about it” solution for “where is the mouse pos in relation to this scene view’s world space?”.

4 Likes

This is definitely a broken workflow. I ran into a similar issue with drawing IMGUI elements onto the scene view. It would work fine, until you were at a different windows dpi. Then it’d break. So now I have to multiple every single coord x/y by this.

Perhaps even more frustrating is that every single IMGUI example in the help should be telling you to do this. Or perhaps only the screen view is not taking DPI into account when you put IMGUI elements on it, but the inspector/editor windows do. Either way, it’s terrible.