Getting normals relative to camera view

Hi, I’m quite new at shader programming and I’m trying to put together a matcap shader for a project. I’ve looked at the some current implementations as free assets from the Asset Store, but none of them function as I would expect. They all seem to use the normals relative to the camera direction, but not the direction between the camera and the object which is what I would expect is the correct approach. For instance this shader from @Jean-Moreno does the following vert() calculations, I’ve been in contact with him and he was also unable to solve it, and I’ve tried modifying the calculations myself to make the normals relative to the camera position but have so far been unsuccessful)

v2f vert (vertexdata v)
{
    v2f o;
    o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.color = v.color;
                 
    float3 worldNorm = normalize(
        unity_WorldToObject[0].xyz * v.normal.x
        + unity_WorldToObject[1].xyz * v.normal.y
        + unity_WorldToObject[2].xyz * v.normal.z);

    worldNorm = mul((float3x3)UNITY_MATRIX_V, worldNorm);

    o.cap.xy = worldNorm.xy * 0.5 + 0.5;
                 
    return o;
}

The way this behaves is as follows,
I’m using a simple matcap image with red edges just to illustrate:

Here’s a case where the object is more or less centered, and then it’s behaving correctly, first image has no red edge, then I rotate the camera and when the edge is approaching 90deg from the camera it turns red:
→

And here’s a scenario when it doesn’t work as expected. First image has no black edge, then I move the camera sideways (note, not rotating, so the camera direction is the same), and the edge continues to be black even though in the second image it’s approaching 90 deg from the view.
→

Anyone out there that knows how to solve this? (like @Daniel-Brauer who seems to be a shader guru here at the forums)

Thanks in advance!

Matcap shaders are generally used because they’re significantly cheaper to calculate than actual lighting. To get the “correct” normals would actually significantly complicate the shader over the average Matcap shader, hence why you can’t find one that works like you expect. I’m actually surprised the snippet you posted works the way it does, most of the time I see Matcap shaders just use normalize(mul(UNITY_MATRIX_MV, v.normal)) and be done with it, though the above will handle non-uniform scaled meshes correctly.

Really the calculations would still be less than those needed for something like Unity’s own Standard shader.

Shader "Unlit/Matcap"
{
    Properties
    {
        _Matcap ("Matcap", 2D) = "white"
        [Toggle] _PerspectiveCorrection ("Use Perspective Correction", Float) = 1.0
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 cap : TEXCOORD0;
            };

            sampler2D _Matcap;
            bool _PerspectiveCorrection;

            v2f vert (appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                            
                float3 worldNorm = UnityObjectToWorldNormal(v.normal);
                float3 viewNorm = mul((float3x3)UNITY_MATRIX_V, worldNorm);

                if (_PerspectiveCorrection)
                {
                    // get view space position of vertex
                    float3 viewPos = UnityObjectToViewPos(v.vertex);
                    float3 viewDir = normalize(viewPos);

                    // get vector perpendicular to both view direction and view normal
                    float3 viewCross = cross(viewDir, viewNorm);
                   
                    // swizzle perpendicular vector components to create a new perspective corrected view normal
                    viewNorm = float3(-viewCross.y, viewCross.x, 0.0);
                }
            
                o.cap = viewNorm.xy * 0.5 + 0.5;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_Matcap, i.cap);
                return col;
            }

            ENDCG
        }
    }
}
9 Likes

Thanks a lot @bgolus ! This definitely behaves more according to what I’m looking for. It does not fill the whole edge of the cube with the outer matcap color though at certain angles, which is a bit strange? But maybe that’s a necessary effect of compensating for the perspective or can that be fixed as well somehow?

Eg on the cube in the screendump the whole right edge should have the same normal relative to the camera even though it’s in perspective, no?

No. It was the same normal using the old method because the normal and “view direction” (camera forward vector) are both constant across the screen. With the perspective correction the “view direction” varies across the screen as it’s now the angle from the camera position to that vertex position. The normal being constant was the reason it looked funny before!

The issue you’re seeing now is the calculations are being done at the vertex level and not per-pixel (again, because it’s cheaper). However for something like a box you see this break down in a way the old method didn’t. You’d have to modify the shader I posted to pass both the viewNorm and viewPos (not viewDir!) to the fragment shader and do lines 42 - 51 in the fragment shader.

2 Likes

Hi bgolus!
In some android devices (e.g. devices with Adreno 506 GPU) “viewDir” not working properly, in specific camera angles it works fine but in most angles texture deformed very much and completely messed up. is there any alternative for “viewDir”? is there any way to fix it?
Thanks in advance!

Edit:
In rotation (0, 180, 0) it looks fine but when camera rotates texture messed up.

It seems there is a bug in calculation of UNITY_MATRIX_V in some Android devices. bug reported to unity.
Quick fix: calculate view matrix in C# and pass it to shader.