Correct way of converting to/from ScreenSpace (Screen.width, Screen.height) position in a shader

I am struggling quite a lot to understand what is the correct way of converting from ScreenSpace of the type (Screen.width, Screen.heingth) to Object/World spaces and the other way around. I have tried many different approaches and none of them results correct.

To illustrate them, I have set up a geometry shader that builds a quad on the fly and is supposed to place these quads at the screen position of the vertices of the mesh to which the shader is assigned to. So, just get my example shader, assign it to a material, assign any texture to it and them that material to a Cube. What should happen is that the Cube should disappear and in its place, each vertex of the Cube should become a quad facing the camera at the position of each vertex:

Shader code

Shader "Custom/ScreenSpaceTest"
{
    Properties
    {
        _SpriteTex("Base (RGB)", 2D) = "white" {}
    }

        SubShader
    {
        Pass
    {

        Tags{ "RenderType" = "Geometry" }
        LOD 100
        Lighting Off
        Cull off
        ZWrite on


        CGPROGRAM
#pragma enable_d3d11_debug_symbols
#pragma target 5.0
#pragma vertex VS_Main
#pragma fragment FS_Main
#pragma geometry GS_Main
#include "UnityCG.cginc"

        // **************************************************************
        // Data structures                                                *
        // **************************************************************
    struct entershader {
        float4 vertex : POSITION;
        float3 tex0 : TEXCOORD0;
        float4 color : COLOR;
    };

    struct GS_INPUT
    {
        float4    pos        : POSITION;
        float3    normal    : NORMAL;
        float2  tex0    : TEXCOORD0;
    };

    struct FS_INPUT
    {
        float4    pos        : POSITION;
        float2  tex0    : TEXCOORD0;
    };


    // **************************************************************
    // Vars                                                            *
    // **************************************************************

    Texture2D _SpriteTex;
    SamplerState sampler_SpriteTex;


    // **************************************************************
    // Shader Programs                                                *
    // **************************************************************

    // Vertex Shader ------------------------------------------------
    GS_INPUT VS_Main(entershader v)
    {
        GS_INPUT output = (GS_INPUT)0;

        output.pos = v.vertex;
        output.tex0 = float2(0, 0);
        return output;
    }



    // Geometry Shader -----------------------------------------------------
    [maxvertexcount(4)]
    void GS_Main(point GS_INPUT p[1], inout TriangleStream<FS_INPUT> triStream)
    {
        float halfSize = 20.0f; //half the size of the quad to be created below

        float4 screenpos;

        //####The lines below test different possibilities of getting the ScreenSpace position. All will fail:
            screenpos = ComputeScreenPos(mul(UNITY_MATRIX_MVP, p[0].pos));
            //screenpos = ComputeScreenPos(p[0].pos);
            //screenpos = ComputeScreenPos(mul(_Object2World, p[0].pos));


            //Now uncomment the line below to see how everything is working great from this point on
            //screenpos = float4(_ScreenParams.x- halfSize, _ScreenParams.y- halfSize, 0, 1);


        //Let's create a quad of size halfSize * 2:

        float4 v[4];
        v[0] = float4(screenpos.x - halfSize, screenpos.y - halfSize, 0, 1.0f);
        v[1] = float4(screenpos.x - halfSize, screenpos.y + halfSize, 0, 1.0f);
        v[2] = float4(screenpos.x + halfSize, screenpos.y - halfSize, 0, 1.0f);
        v[3] = float4(screenpos.x + halfSize, screenpos.y + halfSize, 0, 1.0f);


        //The code below transforms the quad vertices from screen space to final clip space. It seems to be working properly
        float4 final[4];

        final[0] = float4(
            2.0 * v[0].x / _ScreenParams.x - 1.0,
            _ProjectionParams.x * (2.0 * v[0].y / _ScreenParams.y - 1.0),
            _ProjectionParams.y, // near plane is at -1.0 or at 0.0
            1.0);

        final[1] = float4(
            2.0 * v[1].x / _ScreenParams.x - 1.0,
            _ProjectionParams.x * (2.0 * v[1].y / _ScreenParams.y - 1.0),
            _ProjectionParams.y, // near plane is at -1.0 or at 0.0
            1.0);

        final[2] = float4(
            2.0 * v[2].x / _ScreenParams.x - 1.0,
            _ProjectionParams.x * (2.0 * v[2].y / _ScreenParams.y - 1.0),
            _ProjectionParams.y, // near plane is at -1.0 or at 0.0
            1.0);

        final[3] = float4(
            2.0 * v[3].x / _ScreenParams.x - 1.0,
            _ProjectionParams.x * (2.0 * v[3].y / _ScreenParams.y - 1.0),
            _ProjectionParams.y, // near plane is at -1.0 or at 0.0
            1.0);


        //Now we just have to pass the newly generated quad to the triangle stream that goes out geometry shaders:
        FS_INPUT pIn;
        pIn.pos = final[0];
        pIn.tex0 = float2(1.0f, 0.0f);
        triStream.Append(pIn);

        pIn.pos = final[1];
        pIn.tex0 = float2(1.0f, 1.0f);
        triStream.Append(pIn);

        pIn.pos = final[2];
        pIn.tex0 = float2(0.0f, 0.0f);
        triStream.Append(pIn);

        pIn.pos = final[3];
        pIn.tex0 = float2(0.0f, 1.0f);
        triStream.Append(pIn);
    }



    // Fragment Shader -----------------------------------------------
    float4 FS_Main(FS_INPUT input) : COLOR
    {
        return _SpriteTex.Sample(sampler_SpriteTex, input.tex0);
    }

        ENDCG
    }
    }
}

It does not work, however. Lines 84, 85 and 86 bring three different attempts of calculating the ScreenPosition of the vertices. You can comment out each one, save and re-try to see what happens. Lastly, to prove that the error is in that part of the code (i.e on the screen space conversion), you can also un-comment line 90, which manually passes the up-right corner of the screen as the place to display the quads. This works properly.

Could anyone shed some light on how can I achieve the correct ScreenSpace position conversion in lines 84-86? Also, if a overly generous soul can also show me how to convert the other way around, I would be impossibly too happy.

Thanks!

ComputeScreenPos() takes a vertex position in clip space. The un-commented line in the above is correct. However that just gets you a 0.0 - 1.0 range for x and y. If you want the actual pixel locations you need to multiply that by _ScreenParams.xy.

However that won’t necessarily get you perfect pixel snapping. Unity has a function for that which you can try to decode on your own in UnityCG.cginc.

// snaps post-transformed position to screen pixels
inline float4 UnityPixelSnap (float4 pos)
{
    float2 hpc = _ScreenParams.xy * 0.5f;
    #ifdef UNITY_HALF_TEXEL_OFFSET
    float2 hpcO = float2(-0.5f, 0.5f);
    #else
    float2 hpcO = float2(0,0);
    #endif   
    float2 pixelPos = round ((pos.xy / pos.w) * hpc);
    pos.xy = (pixelPos + hpcO) / hpc * pos.w;
    return pos;
}