ScreenPointToRay and ViewportPointToRay not working with VR

Environment:
- Unity 5.6
- VR is active and rendering to the HMD

My goal is to raycast from the mouse into the scene, with a VR camera.

When using ScreenPointToRay on a VR camera the ray's direction is way off. Here's an image when my cursor was in the center:

When using ViewportPointToRay the ray's direction is much better but still not perfect. Here's an image showing the difference (sorry for mouse pic, my screen capturer wasn't picking up the mouse):

Here's the test code to reproduce in a clean Unity 5.6 project and scene:

using UnityEngine;

public class Raycaster : MonoBehaviour
{
    public Camera sourceCamera;
    public bool useScreenPoint;
    public bool useViewport;
    public Transform viewportSphere;
    public float sphereDistance;

    private void Update () {
        Vector3 mousePos = Input.mousePosition;
        Ray ray = new Ray();

        if (useScreenPoint) {
            ray = sourceCamera.ScreenPointToRay(mousePos);
        }
        else if (useViewport) {
            float normalWidth = mousePos.x / Screen.width;
            float normalHeight = mousePos.y / Screen.height;
            Vector3 viewMousePos = new Vector3(normalWidth, normalHeight, 0);
            ray = sourceCamera.ViewportPointToRay(viewMousePos);

            if (viewportSphere != null) {
                viewportSphere.transform.position = ray.origin + ray.direction * sphereDistance;
            }
        }
        Debug.DrawRay(ray.origin, ray.direction, Color.red);
    }
}

The simple solution to this would be to use another camera that displays just to the monitor, however the project is very performance restricted and the new camera would be displaying exactly the same thing as the existing VR camera.

Has anybody had to tackle this or have any ideas on how to solve this without an extra camera?

First off what is the camera? Is it just a single camera set to both eyes or is it the left or right eye? In VR raycasts should actually be done from the midpoint between the two eyes. If the camera is set to both this is fine, if it's one or the other you should calculate the midpoint before raycasting and likely have this in your rig as a transform.

Once you have that you can just have the origin be the position and the direction be that midpoints position + transform.forward.

@greggtwep16 In these screenshots I'm using a single camera set to both eyes. However, I have repro'd the same problem when I switch to left and right eyes too (that's the setup in the actual project I'm working on).

I'm a bit confused by the rest of your comment. I might not have been clear enough with what I'm trying to achieve. I want to get the ray from the mouse into the scene, as ViewportPointToRay and ScreenPointToRay are supposed to do - whereas your solution seems to be the solution for getting the raycast from the head in the direction of the gaze.

To visualize the use case: there is one person in VR and one person not in VR but on the PC using the mouse to click on things the VR person is looking at. As I mentioned in my comment the easiest way to do this would be using an extra camera dedicated to the non-VR view, however I'd like to avoid this for performance reasons.

Ah, my mistake. Didn’t realize you wanted it from the mouse and not the HMD. I can’t say I’ve used the mouse much since most of the Time in VR I’ve been using controllers. That being said at least from the two screenshots the first one had the ray way lower than it should have been. The second had it slightly higher and your mouse position was a bit higher than center. I’m wondering if it’s basically the VR distoritions meaning if the mouse was exactly in the center if it’d be correct and then the further from center you are the more you are off (both vertically and left/right). You should be able to run some tests to see if that’s the case. If so, then it’s off because of the distortion and you’d want to undo that distortion before you call those methods if you didn’t want another camera. I’m not familiar if there is a Unity method for doing this but maybe there is some info posted on the internet. Just a guess based on your screenshots.

Yeah I think you're right re: VR distortions. I've had some really great fun trying to undo the distortion just by trial and error... Today's task is trying to do it in a proper way from any online sources I can find. If anybody else reading this has any ideas on how to solve this I'm all ears.

There's still the problem that Unity's ViewportPointToRay and ScreenPointToRay are simply wrong when using them in VR. I hope Unity updates their docs to say that its either not supported, or fixes the problem (fingers crossed).

2 Likes

maybe could set new camera to render only some empty layer?

@mgear I'm going to try that shortly.

I've also cross-posted this to the Oculus Unity forum: https://forums.oculus.com/developer/discussion/52886/screenpointtoray-and-viewportpointtoray-not-working-with-vr/p1?new=1

You can definitely do that in a second camera. Like OP said in the original post it will take up additional resources but you can perhaps minimize these.

I'm hoping that it will work with the camera "disabled" and therefore not rendering (stranger things have happened with Unity APIs...)

how about just hiding the default mouse cursor and drawing your own cursor, same place as the sphere ?

Have thought about that too, the problem is the person in VR will see the virtual cursor.

(Keep the ideas coming please!)

I haven't confirmed this but I believe the same problem happens when using "OnMouseEnter/Exit" and a VR camera. I'm guessing these problems all have the same root cause.

Yes, that would have the same cause. I’ve actually done that on raycasting to a Unity GUI based on the mouse logic and it’s off in a pattern that looks like the distortion in VR. My actual usecase was a laser pointer so I used a second camera from the origin of the laser pointer and it’s orientation, but sounds like you are going for something different than a laser pointer.

I have an approximate solution! I added a camera on a gameObject below the MainCamera, disabled it (although it needs to be enabled for one frame to get the correct FoV due to a Unity quirk), set the FoV to 2/3 the FoV of the VR camera (have only tested with Oculus which has an FoV of 96.6) and then I apply a linear transform to the position.... and it kinda works! I'll post code when I've done a bit more testing.

My solution only works for the aspect ratio of the window. When I change the aspect ratio the y-axis is wrong.

I ended up filing a Unity bug for this exact same issue, as using Screen.Width/Height worked fine with VR prior to Unity 5.4. It turns out however that after 5.4 you should instead use VRSettings.eyeTextureWidth/eyeTextureHeight respectively when dealing with the width/height of a VR headset.

https://docs.unity3d.com/ScriptReference/VR.VRSettings.html

While it makes sense on a technical level that you should be able to raycast from a screen point to the world space accurately, the user experience of using the mouse this way would be quite confusing. Moving the mouse from one eye to the other would create a location loop snap (because both eyes are viewing the same area). So if you had a laser pointer lighting up the contact point and you moved from right eye to left eye in screen space, you would notice the light jump back to the right suddenly.

Every time I've seen a cursor or needed to use the mouse it's either been a cursor in close 3D space, on a flat 3D surface in the scene, or completely relative motion (and invisible). I would recommend using one of these solutions.

Edit: Perhaps an invisible object close to the VR camera which moves relatively with the mouse in front of the camera at a fixed Z distance. Hide the mouse and rotate the object to face away from the camera center and raycast forward. You'll have to play with how much the object moves when you move the mouse but it shouldn't be too hard to find a good balance.

Have you come up with a reliable solution to this? I have tried a solution based on what is written here, and although it is much more accurate than using the main camera directly, it is still significantly off. I've tried the code below. The goal is to place an object in the VR scene when the user at the PC clicks on the scene. In addition to what is here, I have also tried using ViewportPointToRay instead of ScreenPointToRay, and also delaying the raycast one cycle, neither of which changed things. I tried changing the ratio instead of using XRSettings.eyeTextureWidth / XRSettings.eyeTextureHeight, and although that moved the point, I was unable to find an accurate ratio.

In the code below, camera is a second camera, not the main camera. It has the target eye set to None, because if I leave it at Both like the main camera, Unity won't let me change the fov.

public class Sparkler : MonoBehaviour {
    [SerializeField] private GameObject sparkle;  // The actual visible object.
    [SerializeField] private Camera camera;


    private void Update() {
        if (Input.GetMouseButtonDown(0)) {
            camera.gameObject.SetActive(true);
            camera.transform.position = Camera.main.transform.position;
            camera.transform.rotation = Camera.main.transform.rotation;
            camera.fieldOfView = Camera.main.fieldOfView * XRSettings.eyeTextureWidth / XRSettings.eyeTextureHeight;
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit)) {
                sparkle.transform.position = hit.point;
                sparkle.SetActive(true);
            }
            camera.gameObject.SetActive(false);
        }
        if (Input.GetMouseButtonUp(0)) {
            sparkle.SetActive(false);
        }
    }
}

Apologies, the above code line 12 should read

Ray ray = camera.ScreenPointToRay(Input.mousePosition);

Unfortunately, for some reason I couldn't edit it.

Have you managed to get a more precise method to work?