Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

View-Space Normals Affected by Camera Rotation?

Discussion in 'Shaders' started by Deleted User, Apr 10, 2017.

  1. Deleted User

    Deleted User

    Guest

    Is there a way to modify View-Space surface normals so that they don't look different when the camera is rotated in place? Below you can see that the normals look as expected when centered on the screen but when the camera is rotated (but not translated!) the normals change.



    The result I want is for the sphere to look EXACTLY the same no matter where it is on screen. Is this possible?

    Here is my shader code:

    Code (csharp):
    1. Shader "Unlit/ViewSpaceNormals" {
    2.         SubShader {
    3.             Pass {
    4.                 CGPROGRAM
    5.                 #pragma vertex vert
    6.                 #pragma fragment frag      
    7.                 #include "UnityCG.cginc"
    8.  
    9.                 struct v2f {
    10.                 half3 worldNormal : TEXCOORD0;
    11.                 float4 pos : SV_POSITION;
    12.                 };
    13.  
    14.                 v2f vert(float4 vertex : POSITION, float3 normal : NORMAL) {
    15.                     v2f o;
    16.                     o.pos = UnityObjectToClipPos(vertex);
    17.                     o.worldNormal = UnityObjectToWorldNormal(normal);
    18.                     return o;
    19.                 }
    20.  
    21.                 fixed4 frag(v2f i) : SV_Target {
    22.                     fixed4 c = 0;
    23.                     c.rgb = mul(UNITY_MATRIX_V, i.worldNormal);
    24.                     return c;
    25.                 }
    26.                 ENDCG
    27.             }
    28.         }
    29.     }
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
  3. Deleted User

    Deleted User

    Guest

    This almost seems right. The only problem now is that the X/Y components will flip signs when I do a 180 degree rotation of the camera around the sphere mesh around the X or Y axis respectively. Seems like this shouldn't happen if it's relative to the camera?
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Yep, the code I posted is a bit of a hack, so it doesn't surprise me if it falls down like that. The resulting "perspective corrected normal" is more a guess than the correct way to do this. The corrected method would require some either a few basic if statements, or two more cross() calls, or using an inverse transform projection matrix (which Unity doesn't supply to the shader and would be relatively expensive to calculate, though possible).
     
    Last edited: Apr 10, 2017
  5. Deleted User

    Deleted User

    Guest

    Agh that's rough. Which would be less expensive, the two cross calls or the if statements?
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Probably the if statements most of the time.
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Curiously I can't reproduce the issue you're seeing, so I'm not even sure if statements can fix it (since I don't know what's causing it).
     
  8. Deleted User

    Deleted User

    Guest

    It turns out it was a problem in my shader. I am writing my shader in Shader Forge and my viewDir was being calculated incorrectly. I was calculating it by taking the world position and transforming it into view space.

    Since Shader Forge forced me to work in the fragment function I had to get it with i.posWorld.rgb-_WorldSpaceCameraPos instead.

    UPDATE:

    It looks like with flat faces I get this unwanted result where the entire surface of a face has different normals even though they should all be the same:



    Is there a way to correct this?
     
    Last edited by a moderator: Apr 11, 2017
  9. Deleted User

    Deleted User

    Guest

    @bgolus I was wondering, why does your example remove the Z component of the resulting screen-space normal? I need a 3D normal for my use case.
     
    Last edited by a moderator: Apr 12, 2017
  10. Deleted User

    Deleted User

    Guest

    Actually I found this post:

    But they didn't explain how I could use UNITY_MATRIX_IT_MV to get the inverse projection matrix...
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Because it was written with the intent to be used with a "Matcap" shader, so that data didn't need to be calculated or transferred.

    UNITY_MATRIX_IT_MV is the "correct" way to calculate the view normal from the model's vertex normal. I say that in quotes because I do a lot of stuff for Single Pass Stereo VR, and in that case the UNITY_MATRIX_IT_MV is calculated in the shader from the transpose of the world to object transform and inverse view matrix, my code is algebraically identical and faster. Otherwise you could just do:

    float3 viewNorm = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz));

    But that's still just the same view normal we're already calculating and not a "perspective corrected" one, though it technically will be a little faster if you're not doing VR since in non-VR Unity supplies that matrix to the shaders.

    Just do a dot product between the viewDir and viewNorm to get the Z component.
     
    IgorAherne likes this.
  12. Deleted User

    Deleted User

    Guest

    @bgolus Sorry, I was mistaken about UNITY_MATRIX_IT_MV I thought it could be used to get a perspective corrected normal what I actually should have said was UNITY_MATRIX_P.

    I was thinking if I had the inverse of UNITY_MATRIX_P wouldn't I be able to multiply it by the view-space normals to get perspective corrected normals? Just as an alternative to your matcap method.

    What I actually need is the Z component of the perspective corrected normals, using the dot product of viewDir and viewNorm give me normals that haven't been perspective corrected.
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    It would seem like using the projection matrix would be the answer here, but it's not quite that simple. The projection matrix would be more accurately described as skewing the normal, it doesn't rotate it, so the z component will be kind of more correct (but also still wrong), the x and y components won't change at all, and a normalized normal will be really wrong.
     
  14. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    A perspective corrected Z is exactly what the dot product gives you. Use this to replace line 48 from that original shader:

    viewNorm = float3(-viewCross.y, viewCross.x, dot(viewNorm, -viewDir));
     
  15. Deleted User

    Deleted User

    Guest

    @bgolus Ugh! I guess I'm really outside of my element here. Your matcap solution works for my use case it's just that I don't get the Z component, which I need, and the other part is I don't really understand WHY your method works as I am inexperienced with matrix manipulation.

    I kind of have a possible solution for the Z component but I haven't figured it out entirely yet: Basically Z would be 1 when X and Y and zero, but I'm having trouble with the part where Z should be 0 if X is 1 but Y is 0. I just need to figure out some trigonometry I think?
     
  16. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Technically the z could also be derived using some basic trig like this:

    viewNorm = float3(-viewCross.y, viewCross.x, 0);
    viewNorm.z = sqrt(1 - saturate(dot(viewNorm.xy, viewNorm.xy)));

    But then you're doing a sqrt and a dot product instead of just a dot product to get the same result.
     
  17. Deleted User

    Deleted User

    Guest

    Oh hey, nevermind! This is exactly what I wanted!

    Thanks for the help, I would have given up by now without your examples!
     
  18. Deleted User

    Deleted User

    Guest

    @bgolus I hate to ask for more help, but I'm seeing a strange phenomenon where when I get too close to the mesh the normals generated by your code start to bend inwards:



    I posturized the normals for visibility. You can see they're straight at the initial distance but as you get closer you can see the bands start to bend inwards towars the center. I can't figure out why either, I mean your normal trick used the view direction to the camera point in space so I don't think it has anything to do with the near clipping plane and that's where my ideas run out.
     
  19. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Here's the thing, it's not that they're straight to begin with, they're always curved inward like that, it's just that it's only noticeable when it starts to get that large on screen. However they appear straight on the smaller sphere by a quirk of the sphere geometry countering the curve caused by the view direction.

    Think about what the view normals look like before the perspective correction, they bend "outward". Well the perspective corrected normals are essentially normals viewed from inside of a sphere (the normalized view direction), and bend "inward".

    The only way to get flat normals that are always straight is to not use the perspective correction, and don't actually use a sphere, just use a sprite with a normal map.
     
  20. Deleted User

    Deleted User

    Guest

    @bgolus It must be something else then :(

    All this has been towards my efforts to make a screen-space refraction shader and I'm so close except for when the camera is too close to the surface the refracted UVs start to turn inside out!



    I noticed that the SAME thing with Keijiro Takahashi's Pseudo Refraction Shader here:
    https://github.com/keijiro/UnityRef...ction Shader/Shaders/Pseudo Refraction.shader

    Although that uses Cubemap sampling instead of screen grab texture and I had written it off as a weird cubemap thing. Now I'm just stumped!
     
  21. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Because that's what happens in the real world too...
     
  22. Deleted User

    Deleted User

    Guest

    Wow! Ok... I am very confused now, how is my code producing this effect if my refraction code is just:

    Code (CSharp):
    1. return refract(cameraRay, viewSpaceNormal, refractionIdx);
    If cameraRay is float3(0,0,-1) and viewSpaceNormal is your perspective-corrected normal and I assume that the viewSpaceNormal is camera-distance-agnostic (except for the bending I mentioned earlier) then somehow the shader is mysteriously factoring in camera distance?

    I'm baffled!
     
  23. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Okay, so your problem is cameraRay shouldn't be "0,0,-1", that should be the viewDir (which should be the unnormalized viewPos normalized in the fragment shader), and the non-perspective corrected viewNormal. Using my perspective correction is resulting in basically the same thing, but adding unnecessary math.
     
  24. Deleted User

    Deleted User

    Guest

    That's giving me the wrong results though. What I'm doing now is I'm refracting the imaginary ray from the pixel as it lies on the near-clipping plane towards the surface of the mesh, and that's why I need your corrected normals because the "viewDir" for each of those rays will always be perpendicular to the near clipping plane, so 0,0,-1 in this imaginary view-space.

    I'm sampling the grab pass texture, not a cubemap like in Keijiro's shader so I'm sampling a flat surface not a sphere.
     
  25. Svpam111

    Svpam111

    Joined:
    Aug 19, 2015
    Posts:
    4
    There is a part of code from https://github.com/unity3d-jp/UnityChanToonShaderVer2_Project does what you need
    Code (CSharp):
    1. Shader "Unlit/MatcapSimple"
    2. {
    3.     Properties
    4.     {
    5.       _Color("Main Color", Color) = (0.5,0.5,0.5,1)
    6.       _MatCap("MatCap (RGB)", 2D) = "white" {}
    7.     }
    8.  
    9.     Subshader
    10.     {
    11.         Tags { "RenderType" = "Opaque" }
    12.  
    13.         Pass
    14.         {
    15.             CGPROGRAM
    16.                 #pragma vertex vert
    17.                 #pragma fragment frag
    18.                 #include "UnityCG.cginc"
    19.  
    20.                 struct v2f
    21.                 {
    22.                     float4 pos    : SV_POSITION;
    23.                     float3 viewNormal : TEXCOORD3;
    24.                     float4 worldPos: TEXCOORD6;
    25.                 };
    26.  
    27.                 v2f vert(appdata_base v)
    28.                 {
    29.                     v2f o;
    30.                     o.pos = UnityObjectToClipPos(v.vertex);
    31.  
    32.                     // transform normal vectors from model space to world space
    33.                     float3 worldNorm =
    34.                         normalize(
    35.                             unity_WorldToObject[0].xyz * v.normal.x +
    36.                             unity_WorldToObject[1].xyz * v.normal.y +
    37.                             unity_WorldToObject[2].xyz * v.normal.z
    38.                             );
    39.  
    40.                     o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    41.  
    42.                     o.viewNormal = mul((float3x3)UNITY_MATRIX_V, worldNorm);
    43.  
    44.                     return o;
    45.                 }
    46.  
    47.                 uniform float4 _Color;
    48.                 uniform sampler2D _MatCap;
    49.  
    50.                 float4 frag(v2f i) : COLOR
    51.                 {
    52.                     float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
    53.  
    54.                     float3 NormalBlend_MatcapUV_Detail = i.viewNormal.rgb * float3(-1, -1, 1);
    55.                     float3 NormalBlend_MatcapUV_Base = (mul(UNITY_MATRIX_V, float4(viewDirection, 0)).rgb*float3(-1, -1, 1)) + float3(0, 0, 1);
    56.  
    57.                     float3 noSknewViewNormal = NormalBlend_MatcapUV_Base *
    58.                         dot(NormalBlend_MatcapUV_Base, NormalBlend_MatcapUV_Detail) / NormalBlend_MatcapUV_Base.b - NormalBlend_MatcapUV_Detail;
    59.  
    60.                     return  float4(noSknewViewNormal,1);
    61.                 }
    62.             ENDCG
    63.         }
    64.     }
    65.     Fallback "VertexLit"
    66. }