Avoiding Ugly Camera Clipping In Unity3D

In 3d games, a lot of attention by developers is focused on elements of the engine that might hinder the players’ immersion in the game. These can be big projects, like defeating the uncanny valley (see my last post on realistic characters), or little eye-catching bugs or features of a game that drastically differ from how a user expects things to look.

The ugly reality of near clipping

In the world of 3d games, we rarely deal with volumetric objects (objects that have an ‘inside’), but rather most of our assets are hollow on the inside. This saves a lot of performance and usually doesn’t matter for the player unless the game needs it (for example a game where you need to dig into the ground, or a flight simulator with volumetric clouds).
A game’s camera usually has a specific area in which it will render these objects, consisting of a near plane and a far plane. If an object is too close or too far, it simply won’t be rendered by the camera. However, if an object just barely intersects with the near plane, the ugly reality of hollow objects shows itself to the user: 3d models are not only hollow, but you can see through them when the camera clips part of the object!

clipping

This is a problem that probably any game with free movement tackles at some point.
Now it would be possible to use shaders that don’t cull the backfaces of your models, but it’ll still look hollow, just with a rendered backface. (Like a hollow box)

The usual solution is to place a collider on the camera and on the objects in question and the problem is solved. Some more advanced systems in third person games will also move the camera around so that the physics system doesn’t push the camera into an undesirable position for the user. This is great for most games, as it mimics real life almost perfectly since you don’t normally stick your eyes into things (ouch!).

Limitations of collider-based anti-clipping methods

In my current project, the user is able to freely move the camera around in a room full of objects which might clip into the camera. Using colliders, in this case, would be detrimental to the way the game is played since the camera would constantly collide with objects, and the user would need to think about maneuvring the camera around more than he thinks of the actual task he want’s to accomplish.

Let’s say the camera is currently in the bathroom since the user sent one of the ai’s off to use the shower. Now, the user wants to tell the ai that it should go and watch some tv after it is done in the bathroom. The user would have to somehow squeeze the camera through the door, the hallway, avoid things hanging from the ceiling and other stuff in the room, only to finally reach the living room and click on the couch. This becomes annoying extremely fast!

A better solution!

I did not want to let the user face the ugly truth of lies and deception within hollow walls in the room, so I needed some better way of hiding this fact. The project already makes use of an outline shader (free!) which applies a post-effect outline around objects that are clicked on. Since this shader already works with mesh data and draws an image over the object, I set out to modify it to fill in the culled areas.
While the process was pretty easy in the end, it took ages to figure out how the code worked in the first place, so I’ll spare you that part.

The outline shader consists (in simplified terms) of two shaders, an outlinebuffershader and the outlineshader itself. The buffer will take the meshes data and texture, apply a tint to it and the outlineshader will take that output and place an outline and a fill color inside the outline over it.

Coding time!

To modify this behavior, I just had to tweak the buffershader and use the existing ruleset by which the outlineshader will place the outline and fill.
To do this, I set the buffershader to

CULL OFF

so that it now also considers backfaces.
The outline shader will only shade areas whose color value on the input texture is above a certain threshold, which is why the buffer shader usually does this:

float alpha = c.a * 99999999;
o.Albedo = _Color * alpha;

for areas that should be processed by the outline shader. If albedo isn’t multiplied by that factor, the outline shader will not process it, and no fill or outline will be drawn on top.
Thus, the process to modify this is quite simple.

The buffershader should take the backfaces, process them like usual (multiply the albedo with the factor) so a fill is drawn on top. When a front face comes along, the buffershader shouldn’t multiply the output by the factor, which will make the outline shader discard that part. This creates a mask over the filled backfaces so that the fill is only drawn on the backfaces that are actually visible (those where the camera can see through the object through the clipped frontfaces (see picture at the beginning of the post).

To pull this off, the input to the surface shader needs these variables:

struct Input
{
float2 uv_MainTex;
float3 vertexNormal;
float3 vertexPosition;
}

these should be receiving their values in the vert function:

 void vert(inout appdata_full v, out Input o)
                {
                    #if defined(PIXELSNAP_ON)
                    v.vertex = UnityPixelSnap(v.vertex);
                    #endif

                    UNITY_INITIALIZE_OUTPUT(Input, o);
                    o.vertexNormal = UnityObjectToWorldNormal(v.normal);
                    o.vertexPosition = mul(unity_ObjectToWorld, v.vertex).xyz; ;
               }

Then, a new function is defined, which will tell us whether a vertex faces towards the camera or away from it (it will do this by taking the camera perspective into account, and not just use the camera forward vector, which would lead to bugs):

  bool isFacingForward(float3 vertexNormal, float3 vertexPosition) {
   return dot(normalize(UnityWorldSpaceViewDir(vertexPosition)), vertexNormal) <span id="mce_SELREST_start" style="overflow: hidden; line-height: 0;"></span><span id="mce_SELREST_start" style="overflow: hidden; line-height: 0;"></span><= 0;
   }

The function above returns true if the dot-product of the vector from the vertexPositon to the camera and the vertex normal is below or equal to 0, which means the normal of the vertex is pointing away from the camera (normal points towards camera == false).

Finally, the surface shader is modified to use this function to decide if the albedo should be multiplied by the factor:

void surf(Input IN, inout SurfaceOutput o)
                {
                    fixed4 c = tex2D(_MainTex, IN.uv_MainTex);// * IN.color;
                    if (c.a < _OutlineAlphaCutoff ) discard;
 
                    float alpha = c.a * 99999999;
 
                    o.Albedo = _Color * alpha;
 
                    o.Alpha = alpha;
 
                    if (!isFacingForward(IN.vertexNormal, IN.vertexPosition))
                        o.Albedo = fixed4(0,0,0,0);
 
                    o.Emission = o.Albedo;
                }

Here, we simply check if the vertex is pointing at the camera and if it is (!isFacingForward meaning the normal points towards the camera, my choice of naming wasn’t the best), the factor is omitted.

These are all the modifications that need to be done, although my final version integrates this into the asset a lot better, so the asset can still work normally and only does this masking when the user checks a bool for the object.

The final result can be seen here (left is without, middle is with the new shader, right shows the difference again):

Conclusions

I’m pretty happy with the result. It looks good and the user can still freely move the camera, which is a big plus of this method. Also, the effect is quite cheap and quickly applied to new objects and those objects can still use whatever shader they want, which was the main reason for this project. There are other shaders out there which do similar things, but those require changing the object’s shader, which would have cost me a lot of shader-freedom.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s