First Person Rendering in Unity 5 (writeup)

(This is copied from this post on my blog)

If you’ve worked with Unity long enough, you’ll know quite well that, almost since the dawn of Unity itself, a problem has plagued Unity users to no end - how to properly render meshes in first person view.

Up till now the solution was to render with a separate camera. This worked great in old versions of Unity, where light and shadow were really barely a concern. Then dynamic shadowing was introduced, and this method broke down - weapons in first person didn’t receive shadows from scene geometry. It got a bit better with Unity 3.5, since you could use the new Light Probe feature to at least make your weapon vaguely look like it was receiving shadow from the scene. Then along came Unity 5 with Enlighten, and the light probe method is now broken since light probes don’t bake shadow information for realtime lights, and since dual lightmaps are gone realtime lights are necessary for dynamic shadows.

So, back to the drawing board. How do we render a first person mesh in Unity 5? First, let’s evaluate the options before I get to the solution I’ve come up with, so bear with me :wink:

Keep in mind I’m assuming this is in the context of a game targeted at decent gaming PC rigs (not mobile). I’m therefore assuming deferred rendering is used.

Overlay Camera

I’ve already described why this one doesn’t work, but we’ll cover it for completeness. So, you put your first person renderers on their own layer, make sure the main camera doesn’t render that layer, and have a separate weapon camera set to clear Depth Only which renders just that layer.
And it doesn’t work. Even though the second camera is set to Depth Only, it still appears to be clearing a solid color (grey in my case) and overwriting the first camera.
No big deal, we’ll just set it to Forward and - well, hold on, it still doesn’t work? Right, Unity appears to have broken something here in Unity 5. This would have worked in Unity 4, though. So right there, this time-honored method is totally off the table now. What else do we have?

Really Tiny Gun


Ain’t that a cute little gun?

Turns out this is actually sort of the method Unreal Tournament 4 is using. You just scale the gun down really, really small so that it doesn’t clip the environment. Oh, OK, so if that’s what UT4 is doing then our problem is solved, right?
Sadly, no. I would have liked the solution to really be that simple, but this introduces problems of its own.

For one, you don’t get a custom FOV. Sounds like no big deal but actually you usually want to render your gun with a lower FOV than your main camera. Especially for a shooter, where players might be cranking their field of view all the way up to 90, which can introduce major distortion for your weapon models.

But then, there’s another even bigger problem - that of floating point precision. Just 500 meters away from origin the gun had a vicious wobble that reminded me of a PS1 game. That alone makes this technique completely unusable for me, as 500m should be well within the limits of floating point precision. I’m not sure how Unreal is handling it, potentially via smarter concatenation of matrices. In any case, looks like this isn’t going to work.

Is there any other way of rendering the gun? Turns out the answer is yes.

Custom Shader & Custom Projection Matrix

I can already feel your interest waning. “Wait, hold on, custom shader? You mean those third party shaders I’m using aren’t going to work anymore? And what about surface shaders?”

OK, wait, bear with me! I wanted to make this as easy as possible on myself, so actually it’s practically a one-line fix for a shader!

So, first, projection. The idea behind this is that I use a custom projection matrix which is borrowed from a separate disabled camera via camera.projectionMatrix, set up as a global shader variable. Additionally, I use that matrix in a custom version of UnityCG.cginc, which I copied from the unity editor CGInclude folder. Basically, there’s a UnityObjectToClipPos function in there. It’s responsible for applying the MVP matrix (model view projection) to the vertices, which projects them onto the screen. Surface shaders internally use this function, which is a good thing for us.

OK, first thing’s first - I copied UnityCG.cginc and dumped it into my project directory. Not in the Assets folder, mind you. Just outside it, in the root of your project (this is important, because shader includes are relative to the project directory, not the Assets directory). Now, whenever a shader is compiled, it will compile in this new custom UnityCG.cginc file, rather than Unity’s built in one. So far so good.

Next, I modified the UnityObjectToClipPos function. Remember, this code will be copied into a shader when it’s compiled, so we can actually check for defined symbols. So I made two versions of the function - if a shader defines the FIRST_PERSON symbol, it compiles a version of that function which constructs a custom MVP matrix from it (and also multiplies z position by 0.5, which will offset our weapon’s depth value and keep it from intersecting scene geometry):

    float4x4 _CustomProjMatrix;
 
    #if defined(FIRST_PERSON)
        // Tranforms position from object to homogenous space
        inline float4 UnityObjectToClipPos( in float3 pos )
        {
            float4x4 mvp = mul( mul( _CustomProjMatrix, UNITY_MATRIX_V ), unity_ObjectToWorld );
            float4 ret = mul( mvp, float4(pos, 1.0) );
         
            // hack to fix vertically flipped vertices post-projection
            ret.y *= -1;
         
            // hack to offset depth value to avoid scene geometry intersection
            // todo: check if it works in OpenGL?
            ret.z *= 0.5;
         
            return ret;
        }
    #else
        // Tranforms position from object to homogenous space
        inline float4 UnityObjectToClipPos( in float3 pos )
        {
        #if defined(UNITY_SINGLE_PASS_STEREO) || defined(UNITY_USE_CONCATENATED_MATRICES)
            // More efficient than computing M*VP matrix product
            return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
        #else
            return mul(UNITY_MATRIX_MVP, float4(pos, 1.0));
        #endif
        }
    #endif

Then, I’ll modify an example surface shader to add support for the new function. Turns out this is super easy! All you need is this:

    #pragma multi_compile __ FIRST_PERSON

Super easy.

Next, we need a script which actually prepares this custom matrix. And here it is:

    using UnityEngine;
    using System.Collections;
 
    public class SetWeaponProjectionMatrix : MonoBehaviour
    {
        public Camera SourceCam;
     
        void LateUpdate()
        {
            Shader.SetGlobalMatrix("_CustomProjMatrix", SourceCam.projectionMatrix);
        }
    }

And, finally, a script which enables the FIRST_PERSON feature on a renderer’s material:

    using UnityEngine;
    using System.Collections;
 
    public class EnableFirstPersonShaderVariant : MonoBehaviour
    {
        void Awake()
        {
            GetComponent<Renderer>().material.EnableKeyword("FIRST_PERSON");
        }
    }

And that’s it. Surprisingly, it was not that hard at all. If you want to see the results, here’s a video of this technique in action:

Further Notes: 5.4 Beta 16 & Motion Vectors
Now, one final note - this doesn’t quite work for B16’s new Motion Vector Buffer feature without a few extra steps. Unfortunately, here’s where things start to get a bit more manual.
For one, you need to change how you set up the projection matrices:

    using UnityEngine;
    using System.Collections;
 
    public class SetWeaponProjectionMatrix : MonoBehaviour
    {
        public Camera SourceCam;
 
        private Matrix4x4 prevProjMatrix;
        private Matrix4x4 lastView;
 
        private Camera cam;
 
        void Awake()
        {
            prevProjMatrix = SourceCam.projectionMatrix;
            lastView = SourceCam.transform.worldToLocalMatrix;
        }
 
        void OnRenderImage(RenderTexture src, RenderTexture dest)
        {
            prevProjMatrix = SourceCam.projectionMatrix;
            lastView = SourceCam.worldToCameraMatrix;
 
            Shader.SetGlobalMatrix("_PrevCustomVP", prevProjMatrix * lastView);
            Shader.SetGlobalMatrix("_CustomProjMatrix", SourceCam.projectionMatrix);
 
            Graphics.Blit(src, dest);
        }
    }

Why OnRenderImage and not, say, Camera.onPreRender? I tried the latter, and it didn’t work. I still don’t really know why. Meanwhile, OnRenderImage does work even though it introduces that extra draw call from the blit (which kinda bugs me, but until I have a better solution it’ll have to do).

Next, you need to modify your surface shader to add a custom motion vector pass. Here’s what mine looks like:

    Pass
        {
            CGINCLUDE

    #include "UnityCG.cginc"

            struct MotionVertexInput
            {
                float4 vertex : POSITION;
                float3 oldPos : NORMAL;
            };

            struct v2f_motion_vectors
            {
                float4 transferPos : TEXCOORD0;
                float4 transferPosOld : TEXCOORD1;
                float4 pos : SV_POSITION;
            };

            float4x4 _PreviousVP;
            float4x4 _PreviousM;
            bool _HasLastPositionData;
            float _MotionVectorDepthBias;

            float4x4 _PrevCustomVP;

            v2f_motion_vectors vert_motion_vectors(MotionVertexInput v)
            {
                v2f_motion_vectors o;

                o.pos = UnityObjectToClipPos(v.vertex);

                // this works around an issue with dynamic batching
                // potentially remove in 5.4 when we use instancing
    #if defined(UNITY_REVERSED_Z)
                o.pos.z -= _MotionVectorDepthBias * o.pos.w;
    #else
                o.pos.z += _MotionVectorDepthBias * o.pos.w;
    #endif

                o.transferPos = o.pos;

    // Here, we're manually applying the custom projection matrix
    // note that we're not actually using the last frame's camera view*proj matrix,
    // for first person weapons it's unnecessary (you don't want blur from camera motion on them anyway)
    #if defined(FIRST_PERSON)
                o.transferPosOld = mul(_PrevCustomVP, mul(_PreviousM, _HasLastPositionData ? float4(v.oldPos, 1) : v.vertex));
                o.transferPosOld.y *= -1;
    #else
                o.transferPosOld = mul(_PreviousVP, mul(_PreviousM, _HasLastPositionData ? float4(v.oldPos, 1) : v.vertex));
    #endif

                return o;
            }

            float4 frag_motion_vectors(v2f_motion_vectors i) : SV_Target
            {
                float3 hPos = (i.transferPos.xyz / i.transferPos.w);
                float3 hPosOld = (i.transferPosOld.xyz / i.transferPosOld.w);

                // V is the viewport position at this pixel in the range 0 to 1.
                float2 vPos = (hPos.xy + 1.0f) / 2.0f;
                float2 vPosOld = (hPosOld.xy + 1.0f) / 2.0f;

    #if UNITY_UV_STARTS_AT_TOP
                vPos.y = 1.0 - vPos.y;
                vPosOld.y = 1.0 - vPosOld.y;
    #endif
                half2 uvDiff = vPos - vPosOld;
                return half4(uvDiff, 0, 1);
            }
            ENDCG

            Tags {
                "LightMode" = "MotionVectors"
            }

        Name "MOTIONVECTORS"

            ZTest LEqual
            Cull Off
            ZWrite Off

            CGPROGRAM
            #pragma multi_compile __ FIRST_PERSON
            #pragma vertex vert_motion_vectors
            #pragma fragment frag_motion_vectors
            ENDCG
        }

This just gets pasted in right after the ENDCG of the surface shader.
So, unfortunately it does make supporting custom shaders more of a pain, but it’s still not that bad. And the results seem pretty good!

EDIT
Thanks to Fuzzy_Slippers for helping me catch this one - replaced “shader_feature” with “multi_compile” so that Unity doesn’t “helpfully” strip out the first person variant in a build.

6 Likes

Great share! Might show my team this as we’re running into this problem too.

Also noticed you’re using my ak12 asset haha! Pretty cool to see that around some video and games.

Thanks.
Actually, I’m not using that one. This is the one I’m using: Unity Asset Store - The Best Assets for Game Making
I could go ahead and buy yours to give a try as well, if you’d like :slight_smile:

That is my one lol

Oh, really? The different publisher name threw me off XD

Yeah, rebranding lol, but if you check my weapon attachments link in my signature you can see it links to the same publisher.

I downloaded the 5.4 beta and couldn’t get this method to work at first but I believe it was because all of my existing shaders called mul(UNITY_MATRIX_MVP, v.vertex) instead of UnityObjectToClipPos. Updating to that seems to be working fine. What are motion vectors used for? Motion blur? It wouldn’t seem like worth the extra drawcalls/fiddle just to get motion blur on your weapons.

Well, motion vectors are used for a number of things. Temporal anti-aliasing relies on it for instance. But yes, they’re useful for motion blur. And if you’re using motion vectors at all you DO need to implement the shader on the weapon!
In the case of motion blur if you don’t implement the custom pass, it will just be completely blurred out because the built-in motion vector shader needs to also be updated to use the correct vertex shader, or if you don’t render it into the motion buffer at all it will just be blurred by the background (imagine facing a wall and then moving right - the weapon will actually be blurred horizontally because the motion blur effect is using the wall’s motion vectors).
In the case of everything else, you’re going to get wrong results for temporal effects.

EDIT: Oh, and it is important to note that there’s no harm in including that pass. If you don’t have the camera set to render motion vectors, that pass will just not be used (so it won’t add draw calls).

Ah well I guess it is worth adding the extra elements to my shaders then. Most everything in my project uses custom shaders but I do use Standard in a couple of places but never anything rendered on the view model layer. Would I need to implement the extra pass on the Standard shader for those or do they not need it since they are always rendered normally?

Was this possible before 5.4 or did this require some of the new 5.4 features? This is really cool stuff! Way more sensible than the multi camera approach. I was getting image effects by rendering the scene twice and then combining into a third camera and I’m sure that was pretty heavy performance wise. If you end up making that first person controller I’ll definitely purchase it to support your efforts.

AFAIK you only need to implement the pass on anything with a custom vertex shader. Surface shaders already have their own motion vector pass.

I don’t know when the UnityObjectToClipPos was introduced? I just sort of found out that everything was using that function, and realized that I could override it with a custom UnityCG replacement. Maybe that was introduced in 5.4?

EDIT: Oh, and I’ll give that first person controller thing more thought. I may need to migrate it to a new project since the one it’s in has become a cluttered sandbox of sorts for testing graphics effects and such XD

Did you ever try this in a standalone build? So far I can’t get it to work. I thought it might be because of the custom UnityCG.cginc so I decided to create my own cginc with a custom ClipPos function and #include that in my shaders instead which works fine in the editor but isn’t working in builds. No errors just doesn’t render properly.

Uh…
… no… >.>

Not until you mentioned it. And it doesn’t work.

Well that’s embarrassing. However, it looks like it might be due to Unity stripping out “unused” shader_feature variants. Give me a bit to test and see if this is truly the case and whether there’s a solution…

EDIT: YES! That was the case. So it turns out I had a massive misunderstanding about how shader_feature vs multi_compile worked. Basically what’s happening is it’s stripping out the first person variant because no material in the project has that keyword defined (because the keyword is set from script at runtime instead of from the material in the editor).

So if you just replace all of the shader_feature lines with this instead:

#pragma multi_compile __ FIRST_PERSON

It will not strip the variants in a build and will work.

Thanks that worked perfectly. I never would have guessed that. Always something new to learn with shaders I guess.

I’m glad I could find a solution XD
Also may try and find time to put together a demo project of this in case somebody just wants to download a project and see how it works instead of going through my crappy tutorial :wink:

That’s a great solution for rendering the gun! But how would it work practically?

Typically in my shooters, I’ll use an empty GameObject for instance creation–it’s just put at the end of the gun’s barrel. When I spawn the shooting effects, it’ll just emit from that point. Sure, the effects can just use the same rendering technique and you can see them through the walls. But what if you had to shoot a bullet from that point? (Or raycast for that matter) The projectile will end up already inside the wall because its physical position will be beyond the player’s collider.

You might say, “just move the bullet spawner back into the gun,” and sure this would work. But, (and this is just me thinking of the top of my head) what if your bullet gained strength over time? The time the bullet has been active will be slightly more then what it appears to be for the player. If you ever needed that information, the “time since spawn” value would be off.

The above isn’t much of a problem on it’s own; you could compensate with some simple offsets. However, this is where it starts to fall apart: what if your Doom-esque fireball is larger then your gun? You can’t spawn it inside the collider, because it will be shown through the weapon. And you can’t spawn it outside because it could be through a wall. How would you work around this?

Actually, it sounds weird but most games basically spawn projectiles at the player’s eyeballs, directly from the camera’s center. It sounds like it shouldn’t work, but seriously play Left 4 Dead for example and throw a pipe bomb or bile jar. It spawns from the screen’s center. Doing it that way IMHO is a significantly better option than spawning from the gun’s actual barrel - otherwise, the point the crosshair is showing and the point the gun will actually hit could be a bit off, especially when facing a wall. Most players at this point expect their shots to hit at the exact center of screen.

Yeah I split my weapons between visual fx and physics. Physics/raycasts/etc all act on crosshair center and ignore whatever the weapon is doing. If I’m going to do muzzle flashes or whatever I spawn them from the barrel and plot a motion that integrates it with the physics action.

In motion in real time it is really difficult to notice this kind of thing if you aren’t the dev staring at it until your eyes bleed. You’d be surprised the number of proper commercial games that just half ass this and you’ve never noticed. This gets a little trickier in multiplayer I presume but my game is single player only which you can play here Abyss Crawlers by Pixel Phantasm if you are curious how it works out in practice with PhobicGunner’s render technique implemented. Though I have few firearms being fantasy.

I think what is most important is getting player intention. If you can anticipate what the player is intending to do and make that happen that always trumps visual or physics fidelity.

Hello! And is there an example? Does it work on version 5.5?

@PhobicGunner is this technique still works?
Edit : it is working suprisingly. need to do some tweaks in 2017.1 especially for the motion vector part

@Reanimate_L did you fixed the part of motion vectors?