JTalton's picture

Unproject

I have been testing to figure out how to get the most accurate results for unproject.
If anyone has any suggestions on how to make the unproject more accurate, I'd love to hear them.

Notes:

  • ReadPixel on nvidia with component float is more accurate than uint.
  • Need to offset view coordinates by half a pixel for accuracy.
  • Of course 32-bit depth buffer is most accurate.

Rotating a view around a flat plane and testing 3 different unproject functions.
Below shows the max inaccuracy found for the unprojected z result.
MAX Result1: 0.0035 Result2: 0.0046 Result3: 0.0568

The best inaccuracy is 0.0035. This means that any of the x, y, z values could be off by that much. I was hoping for more accuracy than that, but doing the math, that may be the best we can get. Depth of 249 / 32-bits = 0.000000058 accuracy per bit which is somewhat close to what I am seeing from ReadPixel. Compounded by the matrix calculations, we could lose the extra precision.

In perspective projection, the transformed depth coordinate (like the x and y coordinates) is subject to perspective division by the w coordinate. As the transformed depth coordinate moves farther away from the near clipping plane, its location becomes increasingly less precise

Testing this, the near returned Z is =0.000005 and the far returned Z is 0.000280 which supports the fact that the projection and depth interaction can add significant inaccuracies.

class Program : GameWindow
{
    static Program Instance = new Program();
    static void Main(string[] args) { Instance.Run(); }
 
    public Program()
        : base(1000, 1000, new OpenTK.Graphics.GraphicsMode(new OpenTK.Graphics.ColorFormat(32), 32, 0)) // 32-bit depth
    {
        Mouse.ButtonDown += Mouse_ButtonDown;
        Mouse.ButtonUp += Mouse_ButtonUp;
        Mouse.Move += Mouse_Move;
    }
 
    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        GL.Viewport(0, 0, Width, Height);
    }
 
    double time;
    protected override void OnRenderFrame(FrameEventArgs e)
    {
        time += e.Time;
 
        GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
        GL.Enable(EnableCap.DepthTest);
 
        Vector3d eyePosition = new Vector3d(90, 90, 120);
        Vector3d targetPosition = new Vector3d(0, 0, 0);
        Vector3d upHint = Vector3d.UnitZ;
        Vector3d up = Vector3d.Normalize(Vector3d.Cross(Vector3d.Cross(targetPosition - eyePosition, upHint), targetPosition - eyePosition));
 
        Matrix4d viewMatrix = Matrix4d.CreatePerspectiveFieldOfView(1, (double)Width / (double)Height, 1, 250);
        viewMatrix = Matrix4d.Mult(Matrix4d.LookAt(eyePosition, targetPosition, up), viewMatrix);
        viewMatrix = Matrix4d.Mult(Matrix4d.Rotate(new Vector3d(3.4,5.5,2.6), time), viewMatrix);
 
        GL.PushMatrix();
        {
            GL.MultMatrix(ref viewMatrix);
            GL.Begin(BeginMode.Quads);
            {
                GL.Vertex2(-100, -100);
                GL.Vertex2(-100, 100);
                GL.Vertex2(100, 100);
                GL.Vertex2(100, -100);
            }
            GL.End();
        }
        GL.PopMatrix();
 
        if (mouseDown && mouseIsInWindow)
        {
            try
            {
                Vector3d result1 = Unproject(viewMatrix, Width, Height, mouseX, mouseY);
                Vector3d result2 = Unproject2(viewMatrix, Width, Height, mouseX, mouseY);
                Vector3d result3 = Unproject3(viewMatrix, Width, Height, mouseX, mouseY);
                Log(string.Format(" Result1:{0} Result2:{1} Result3:{2}", string.Format("{0,8:0.00000}", result1.Z), string.Format("{0,8:0.00000}", result2.Z), string.Format("{0,8:0.00000}", result3.Z)));
 
                if (result1Largest < Math.Abs(result1.Z)) result1Largest = Math.Abs(result1.Z);
                if (result2Largest < Math.Abs(result2.Z)) result2Largest = Math.Abs(result2.Z);
                if (result3Largest < Math.Abs(result3.Z)) result3Largest = Math.Abs(result3.Z);
            }
            catch { }
        }
 
        SwapBuffers();
 
        base.OnRenderFrame(e);
    }
 
    double result1Largest = 0;
    double result2Largest = 0;
    double result3Largest = 0;
 
    bool mouseDown = false;
    void Mouse_ButtonDown(object sender, OpenTK.Input.MouseButtonEventArgs e)
    {
        mouseDown = true;
        UpdateMouse(e.X, e.Y);
    }
 
    void Mouse_ButtonUp(object sender, OpenTK.Input.MouseButtonEventArgs e)
    {
        mouseDown = false;
        UpdateMouse(e.X, e.Y);
        Log(string.Format("MAX Result1:{0} Result2:{1} Result3:{2}", string.Format("{0,8:0.00000}", result1Largest), string.Format("{0,8:0.00000}", result2Largest), string.Format("{0,8:0.00000}", result3Largest)));
 
    }
 
    void Mouse_Move(object sender, OpenTK.Input.MouseMoveEventArgs e)
    {
        UpdateMouse(e.X, e.Y);
    }
 
    bool mouseIsInWindow = false;
    int mouseX;
    int mouseY;
    void UpdateMouse(int x, int y)
    {
        mouseX = x;
        mouseY = y;
        mouseIsInWindow = mouseX >= 0 && mouseX < Width && mouseY >= 0 && mouseY < Height;
    }
 
    void Log(string logMessage)
    {
        Console.WriteLine(logMessage);
        Trace.WriteLine(logMessage);
    }
 
    /// <summary> Unproject maps the specified view coordinates to world corrdinates</summary>
    public static Vector3d Unproject(Matrix4d viewMatrix, int viewWidth, int viewHeight, int viewX, int viewY)
    {
        viewY = viewHeight - viewY - 1; // Invert Y as Window Coordinates are opposite
        Debug.Assert(viewWidth >= 0 && viewHeight >= 0 && viewX >= 0 && viewX < viewWidth && viewY >= 0 && viewY < viewHeight);
 
        float depth = 0;
        GL.ReadPixels<float>(viewX, viewY, 1, 1, PixelFormat.DepthComponent, PixelType.Float, ref depth);
        if (depth == 1) throw new Exception();
 
        // ReadPixel gets the value in the center of the pixel
        // viewX and viewY are based on the lower left corner of the pixel
        // Offset the viewX and viewY by a half a pixel for accuracy.
        Vector4d viewPosition = new Vector4d
        (
            (((double)viewX + 0.5) / (viewWidth)) * 2.0 - 1.0,  // Map X to -1 to 1 range
            (((double)viewY + 0.5) / (viewHeight)) * 2.0 - 1.0, // Map Y to -1 to 1 range
            depth * 2.0 - 1.0,                  // Map Z to -1 to 1 range
            1.0
        );
 
        Vector4d temp = Vector4d.Transform(viewPosition, Matrix4d.Invert(viewMatrix));
        return new Vector3d(temp.X, temp.Y, temp.Z) / temp.W;
    }
 
    // Try the Unproject with an int depth component
    public static Vector3d Unproject2(Matrix4d viewMatrix, int viewWidth, int viewHeight, int viewX, int viewY)
    {
        viewY = viewHeight - viewY - 1; // Invert Y as Window Coordinates are opposite
        Debug.Assert(viewWidth >= 0 && viewHeight >= 0 && viewX >= 0 && viewX < viewWidth && viewY >= 0 && viewY < viewHeight);
 
        uint depth = 0;
        GL.ReadPixels<uint>(viewX, viewY, 1, 1, PixelFormat.DepthComponent, PixelType.UnsignedInt, ref depth);
        if (depth == 1) throw new Exception();
 
        Vector4d viewPosition = new Vector4d
        (
            (((double)viewX + 0.5) / (viewWidth)) * 2.0 - 1.0,  // Map X to -1 to 1 range
            (((double)viewY + 0.5) / (viewHeight)) * 2.0 - 1.0, // Map Y to -1 to 1 range
            depth * 2.0 / uint.MaxValue - 1.0,                  // Map Z to -1 to 1 range
            1.0
        );
 
        Vector4d temp = Vector4d.Transform(viewPosition, Matrix4d.Invert(viewMatrix));
        return new Vector3d(temp.X, temp.Y, temp.Z) / temp.W;
    }
 
    // Try the Unproject without adjusting the view for the half pixel offset
    public static Vector3d Unproject3(Matrix4d viewMatrix, int viewWidth, int viewHeight, int viewX, int viewY)
    {
        viewY = viewHeight - viewY - 1; // Invert Y as Window Coordinates are opposite
        Debug.Assert(viewWidth >= 0 && viewHeight >= 0 && viewX >= 0 && viewX < viewWidth && viewY >= 0 && viewY < viewHeight);
 
        uint depth = 0;
        GL.ReadPixels<uint>(viewX, viewY, 1, 1, PixelFormat.DepthComponent, PixelType.UnsignedInt, ref depth);
        if (depth == 1) throw new Exception();
 
        Vector4d viewPosition = new Vector4d
        (
            ((double)viewX / (viewWidth - 1)) * 2.0 - 1.0,  // Map X to -1 to 1 range
            ((double)viewY / (viewHeight - 1)) * 2.0 - 1.0, // Map Y to -1 to 1 range
            depth * 2.0 / uint.MaxValue - 1.0,              // Map Z to -1 to 1 range
            1.0
        );
 
        Vector4d temp = Vector4d.Transform(viewPosition, Matrix4d.Invert(viewMatrix));
        return new Vector3d(temp.X, temp.Y, temp.Z) / temp.W;
    }
}

Windows 7 - nVidia 8800 GT