Trouble with screenPos

I have a shader where I’m trying to clip() every other pixel, to create a checkerdbox pattern.

The problem is, the main forward pass of my shader isn’t clipping always the same pixels as the shadowcaster pass is. And yet, as far as I can tell, they use identical code for determining which pixels to clip.

But often the shadow pass will clip every wrong pixel (meaning all its pixels are shifted by 1). And if I adjust the vertical size of the game view window, it screws around with it as I move the vertical size of the window, so that on some frames the shadow pass clips all the same pixels as the forward pass is clipping, and on other frames it clips all the wrong pixels (the pixels that the forward pass didn’t clip). Which makes no sense since they use the same code, they should always clip the same pixels?

For the forward pass vertex:

o.screenPosition = mul(UNITY_MATRIX_MVP, v.vertex);

For the shadow pass vertex:

o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.screenPos = o.pos;

Then, the clip in the forward pass:

float checkerdBox(float2 screenUVs) {
    float2 px = floor(_ScreenParams.xy * screenUVs);
    return (fmod(px.x + px.y, 2) == 1) ? 0 : 1;
}

surf (...)
{
    #if UNITY_UV_STARTS_AT_TOP
        float grabSign = -_ProjectionParams.x;
    #else
        float grabSign = _ProjectionParams.x;
    #endif

    IN.screenPosition = float4(IN.screenPosition.xy / IN.screenPosition.w, 0, 0);
    IN.screenPosition.y *= _ProjectionParams.x;
    float2 screenUVs = float2(1, grabSign)*IN.screenPosition.xy*0.5 + 0.5;

    clip(checkerdBox(screenUVs) - 0.5);
}

And the clip in the shadow pass (should be identical):

float checkerdBox(float2 screenUVs) {
    float2 px = floor(_ScreenParams.xy * screenUVs);
    return (fmod(px.x + px.y, 2) == 1) ? 0 : 1;
}

frag (...)
{
    #if UNITY_UV_STARTS_AT_TOP
        float grabSign = -_ProjectionParams.x;
    #else
        float grabSign = _ProjectionParams.x;
    #endif

    IN.screenPos = float4(IN.screenPos.xy / IN.screenPos.w, 0, 0);
    IN.screenPos.y *= _ProjectionParams.x;
    float2 screenUVs = float2(1, grabSign)*IN.screenPos.xy*0.5 + 0.5;

    clip(checkerdBox(screenUVs) - 0.5);
}

My theory is that the issue has something to do with the fact that my forward pass is a SURFACE shader, whereas my shadow pass is a vert / frag shader. And I’m assuming that somewhere, Unity’s surface code generator thing is messing with my screenPos or screenPosition math, so that they are not identical.

If I comment out the line “IN.screenPos.y *= _ProjectionParams.x” from the shadow pass, then it works almost perfectly… only very rarely is there a single pixel here or there that isn’t clipped identically between the two passes (see second image below). But commenting out that line in the shadow pass, but leaving it in the forward, deeply disturbs me. Something is wrong and I don’t feel like that’s the correct solution. Anyone know what I’m doing wrong here?

The whole _ProjectionParams.x & UNITY_UV_STARTS_AT_TOP thing is really painfully confusing. I don’t really have a perfect answer for you, though I might suggest using the VPOS semantic instead of extrapolating the pixel positions from the clip space and resolution.

For the surface shader you should be able to add:
UNITY_VPOS_TYPE screenPos : VPOS;
to your Input struct and have it work.

For the vertex / fragment shader you can just sample i.pos directly if you’re not intending to us DX9. I think you can just do do:
UNITY_VPOS_TYPE screenPos = i.pos;

Alternatively use the _NOPOS version of the shadow vertex shader macros and have SV_POSITION not be part of the v2f struct, and have the same line as for the surface input struct as part of your fragment shader function. Check out the shader semantics documentation for a more thorough example.

1 Like

Thanks for the info again, you’ve always been really helpful @bgolus :slight_smile:

I did what you suggested for the fragment shader, and it worked. But it doesn’t seem to work in the surface shader.

Unity hijacks any variable named “screenPos” in the surface shader, and computes it with ComputeScreenPosition(o.pos).

So if I add “UNITY_VPOS_TYPE screenPos : VPOS;” to the Input struct, then the compiled surface shader says “needs screen space position: YES” and it converts it to: “float4 screenPos : TEXCOORD5;” during compilation, and then it computes it with: “o.screenPos = ComputeScreenPos (UnityObjectToClipPos(v.vertex));” so it’s not using VPOS.

But if I avoid using screenPos, and if I instead add “UNITY_VPOS_TYPE screenPosition : VPOS;” to the Input struct, then the compiled surface shader says “needs screen space position: no” and it converts it to: “float4 custompack0 : TEXCOORD5; // screenPosition” during compilation. So VPOS it converts to TEXCOORD…And it then does no math at all on it, so the screenPosition.xy is always be 0,0 for every pixel.

I think I’m going to need to bite the bullet and convert my whole surface shader to a fragment shader. Because I have a fragment shader that does this checkerdbox pattern perfectly, using the “o.screenPos = mul(UNITY_MATRIX_MVP, v.vertex);” in both the forward and shadow pass, and it works fine when both are fragment shaders. The problem only seems to occur if one is a surface, and the other is a fragment.

Ah, yes.I couldn’t remember for sure if VPOS worked or not. I came across this exact issue with something I was working on and ended up going the same route, though I had already gone to directly manipulating the generated shader before point because I was doing alpha to coverage which the surface shader also falls down on.

To be fair this might not actually fix anything either…

Right, it fixed nothing.

So here’s what I learned in a whole day of pain and agony.

  1. It doesn’t happen in the scene view window, only in the game view window.
  2. It caused by the game view windows aspect ratio.
  3. If you switch to aspect ratio 16:9, it doesn’t ever happen.
  4. Pretty much all the other aspect ratios glitch, including of course “free aspect”.

I’m still researching this but for now, I have submitted a bug report (Case 841237).

I’m also attaching here my bug report example scene, which includes three different methods I put together for calculating screen space. All three fail, depending on the game view window size / aspect ratio.

I guess to solve this, there needs to be some better way to get the screen space that takes the aspect ratio into consideration, but ugh, I’m not exactly an expert in aspect ratio glitches.

What still bothers me is I have shaders that I thought were working, I’ll see if I can drag one of them into this scene and see if I can get one of them to work here. Reason I haven’t done that yet is they are big bulky shaders that require all kinds of external mesh data. But I’ll see if I can strip one down and see if it would work in this scene.

2820270–205218–ScreenPos_BugReport.unitypackage (399 KB)

Erm I was mistaken about the aspect ratio. Only reason I thought that was having an effect is because, on that particular aspect ratio, I wasn’t actually adjusting the pixels when I was moving the window size. Stupid me. Once I moved the screen in a way that actually made the picture change size, the glitch came right back, even on 16:9. So scratch what I said above about aspect ratios.

Yeah, the issue is simply a question of if the pixel count starts from the top or the bottom of the screen, and wanting them to be the same for both the depth and final render passes. When the framebuffer is even height or odd height you’ll get different outcomes for if the patterns line up or not if they’re not both starting from the same side. So let’s work through this…

There are two reasons for the flip.

One is DirectX vs OpenGL, where DirectX renders starting from the top left, and OpenGL is from the bottom left. Unity actually flips the projection matrix upside down to resolve this. The value _ProjectionParams.x is used to denote if it’s been flipped, with DirectX being -1. This could probably be ignored since you’re not rendering depth and to the screen with different APIs, but I don’t know if Unity always renders DirectX flipped or not, it’s plausible the depth isn’t?

The other is MSAA with DirectX, which for some reason renders to the render target upside down (bottom left) compared to non-MSAA. The UNITY_UV_STARTS_AT_TOP define I believe is basically a “We’re rendering using DirectX” toggle rather than a “DirectX & MSAA” toggle. To check for MSAA you are supposed to test if the texture you’re sampling has a negative y texel size. However I don’t think this is necessary to check for either since this should only be a problem for image effects / grab passes since this case isn’t one of you sampling a render texture and the depth texture is never anti-aliased. The clip space position itself isn’t flipped with MSAA, just the resulting texture.

However the VPOS value might get flipped with DirectX MSAA vs non-MSAA, and honestly I’m not sure how to check for that if it does, so my suggestion of VPOS might actually be a bad one if that’s the case. I’m not sure if when you tried VPOS if you were using it straight or if you were trying to apply the UNITY_UV_STARTS_AT_TOP and _ProjectionParams.x to it, which I would suggest not doing if you were. It might “just work” if you use that value straight.

If you did use VPOS straight and it didn’t work then you might want to try passing the clip position like you’re doing, then do no more than (i.screenPos.xy / i.screenPos.w) * _ScreenParams.xy; completely ignoring both the define and projection flip.

I just tried turning MSAA off to see if it made any difference, and it made no difference. For my VPOS shader, I’m using it straight. You can’t actually multiply VPOS by .w because VPOS is a vector2 on DX9? So I’m pretty sure just that fact alone would mean that you’re not supposed to need to multiply it by anything.

Okay so here’s where I’m at so far on this.

I found that adjusting the width of the window made no difference on “free aspect” mode… only the height. And I took a wild guess that it was messing up if the height of the window was or wasn’t an even number of pixels. So I made an even/odd check function, and anyway, here’s what I did:

screenPos.y *= _ProjectionParams.x;
screenPos.y = fmod(_ScreenParams.y, 2) == 1 ? -screenPos.y : screenPos.y;

Basically this code uses the “straight” VPOS method, but as you can see it goes ahead and flips the Y according to the projectionparams.x, which I’m not sure if that absolutely necessary but anyway. And then, if the size of the game view is an Odd number (fmod is an even/odd check) then it flips it again.

This code is ONLY put into the shadow pass. I’m pretty sure it doesn’t work if you put it into the forward pass. You leave the forward pass as just an unedited VPOS. However, I imagine it would work the same if you used any of the other methods of calculating pixels (for example, o.screenPosition = mul(UNITY_MATRIX_MVP, v.vertex)

This seems to work on all the aspect ratios.

I must say though, I cringe at this solution. Mainly because I have no way to test it on anything except my computer. Do you think this even/odd flip will work on everyone’s computer? It’s working on mine. ;p

I’m attaching the updated scene with the solution. In the attached scene there are 3 cubes. The blue colored one is the one that is working with this solution.

2820991–205267–ScreenPos_Working.unitypackage (400 KB)

I should probably point out that flipping the whole image may result in a image that is technically upside-down, which wouldn’t be correct for most people. But in my case, I only need to make a checkerdbox pattern, so for me it doesn’t matter if the image is upside-down.

I suppose if my code is actually resulting in a possible up-side-down image, it might be possible as an alternate solution to simply subtract (or add?) a pixel to the shadow pass’s Y value… rather than flip the whole image up-side-down, if that’s what I’m doing.

Anyway, but my concern is 1) I’m not checking the UNITY_UV_STARTS_AT_TOP, and I don’t know if it’s a good or a bad thing that I’m not checking that. And 2) I don’t know if it is good or bad that I’m multiplying the y by _ProjectionParams.x. And 3) I don’t know if anything I’m doing is good or bad, or if it will work on everyone’s computer.

Interesting side note: now it is glitching in the SCENE view window, but not in the game view window. Whereas before it was only glitching in the game view window, but never in the scene view. Weird stuff.

Did some tests and found something really odd. In OpenGL mode _ProjectionParams.x is 1 for the shadow pass, the camera depth pass, and when rendering to the screen in both scene and game views … as expected. In DirectX11 they’re all using -1 as expected … except for the game view when rendering to the screen it is 1 and not -1! MSAA has as no effect on _ProjectionParams.x value, as documented.

I have no idea why the game view is different, and it also makes VPOS more annoying / ugly to use when trying to get the depth and screen to match.

However I also found that using Unity’s built in ComputeScreenPos will handle everything properly.

For a surface shader, just use the built in float4 screenPos; in the Input struct, and for vert frag shaders use o.screenPos = ComputeScreenPos(o.pos); in the vertex shader.
In the fragment / surface shader use:
int2 screenPixel = (IN.screenPos.xy / IN.screenPos.w) * _ScreenParams.xy;
clip(-frac((screenPixel.x + screenPixel.y) * 0.5));

You could also do float2 screenPixel = floor((IN.screenPos.xy / IN.screenPos.w) * _ScreenParams.xy); if you want to avoid the float / int conversion.

1 Like