A Small Shadow Map Improvement

Shad­ows are a great way to relay infor­ma­tion in a 3D ren­der­ing. They can help demon­strate dis­tances between two objects such as a bounc­ing ball and the ground. They also relay fur­ther infor­ma­tion to the struc­ture of an object as they give a sec­ond sil­hou­ette from the per­spec­tive of the light cast­ing the shadow. In this arti­cle, I will demon­strate a very small but impor­tant improve­ment for THREEjs’s shadow ren­der­ing, a one line change to the shader code.

Depth Pack­ing

THREEjs, our cho­sen graph­ics engine, like many real­time 3D ren­der­ers, casts shad­ows using a tech­nique called shadow maps. A map is a term used to describe image data used on a GPU. A map can be gen­er­ated by ren­der­ing a given scene and then stor­ing the color infor­ma­tion for that cre­ated image. A shadow map is a gen­er­ated map that instead of stor­ing color, stores the dis­tance from a light in the scene to the sur­face of a ren­dered object.

Cor­rectly gen­er­at­ing the shadow map can be non-obvious since there are ways to get val­ues that may be vis­i­bly close until you put it through some scrutiny. One part of this work is often referred to as pack­ing where we take a piece of data and store it using a for­mat other than its native rep­re­sen­ta­tion. Specif­i­cally for shadow maps we need to pack the depth, rep­re­sented as a dec­i­mal num­ber between 0, really close, and 1, really far.

Pack­ing is some­what tricky in GLSL, the pro­gram­ming lan­guage for shaders in WebGL. The ear­lier ver­sions of GLSL, and the ones avail­able on most mobile devices and in WebGL, did not allow bit­wise oper­a­tors that fre­quent more gen­eral pro­gram­ming lan­guages and are the nor­mal choice for per­form­ing pack­ing work. Instead, using a float­ing point mod­u­lus func­tion is the only way.

It is not unusual for a piece of soft­ware to use pub­lished code from oth­ers, espe­cially open source soft­ware like THREEjs. Peo­ple fig­ured out one way to pack the depth value in GLSL a while ago and it seems to have cir­cu­lated a lot either with­out peo­ple ques­tion­ing or pos­si­bly accept­ing the error it pro­duces to a more accu­rate and slightly more costly source.

Fract, short­hand for the word “frac­tion” or the part of a dec­i­mal value after the dot, is equiv­a­lent to a mod­u­lus func­tion with a pre­de­fined mod of value 1. This may open it to opti­miza­tions that the gen­eral mod­u­lus func­tion can­not achieve. How­ever its use in this spe­cific case will cause errors in the less pre­cise com­po­nents being stored. Maps can have dif­fer­ent pre­ci­sions depend­ing on how much space a devel­oper wants it to take up in the GPU’s mem­ory (RAM) ver­sus the avail­able pre­ci­sion. An indi­vid­ual cell in our shadow map has 4 com­po­nents, each stor­ing 256 pos­si­ble val­ues. On the CPU side these val­ues would be inter­preted as binary val­ues of 0 to 255. On the GPU side they are inter­preted as dec­i­mal val­ues of 0 to 1. Since fract is a mod 1 func­tion, no com­po­nent in our map will store a value of 1 instead stor­ing a value of 0.

Instead we want to mul­ti­ply our com­po­nents by 255 and mod by 256 and then divide back to 255.

So first we can say this will cause some val­ues to be incor­rectly stored at the high­est com­po­nent value of 255. But if you con­sider more val­ues in the lower com­po­nents, some of them will be stored as some­thing lower than it should.

We can see from the above image, that using fract, the image on the left, adds an implicit and likely, in our case def­i­nitely, unde­sired off­set. If this off­set was a fixed amount maybe it would be an accept­able opti­miza­tion for the error but it is actu­ally depen­dent on the higher pre­ci­sion com­po­nents cre­ated by pack­ing the float value.

This chart shows what hap­pens to the sec­ond com­po­nent if the largest pre­ci­sion part equates to 128 in inte­ger value or 0.501… in float­ing point. There is a shift in the third com­po­nent, the sec­ond largest pre­ci­sion part as it is in lit­tle endian, when using the fract func­tion equal to the stored value of the last com­po­nent, the largest pre­ci­sion part. If we con­sider this dif­fer­ence with a scene that is 256 units in depth, around halfway through the scene, the shadow val­ues shift from a half world unit deeper to a half world unit closer. On a self-shadowing sur­face, this would cause the half to be com­puted to be in shadow that should not be in shadow and another half to not be in shadow when it should.

On aver­age this shift is worse halfway through the scene. But the jumps between those shifts will be pos­si­bly jar­ring effect depend­ing on what you are ren­der­ing. In our case where most ren­dered objects have hard edges, it becomes reg­u­larly obvious.

A Physics Use Case

Another great use case for this change in value pack­ing is using the GPU for cal­cu­lat­ing physics col­li­sions. This is actu­ally why I stum­bled upon this error in THREEjs. In one of my off hour exper­i­ments I needed to read and store val­ues for a par­ti­cle based physics sim­u­la­tion I wanted to exe­cute on the GPU. So I used the code THREEjs used to do that same job.

I started with just read­ing and then writ­ing the same val­ues out to see if I had set it up cor­rectly in my toy exam­ple. When some val­ues were returned as very dif­fer­ent val­ues I thought I had some­thing wrong but after run­ning the math by hand and think­ing about the source I could explain the above errors. Using the change I made in my toy physics exam­ple I found this had obvi­ous graph­i­cal changes to results from THREEjs in our game.

Why Should I Write About This Tiny Change?

The code for pack­ing depth infor­ma­tion in THREEjs did not orig­i­nate from the THREEjs’s authors. The depth pack­ing snip­pet of GLSL has been float­ing around the inter­net for at least 5 years.

Links ref­er­enced in THREEjs’s source:

Another source I found:

Look­ing around fur­ther you can find some other slightly dif­fer­ent ver­sions but they all seem to use fract. Maybe I am wrong in think­ing this is a bet­ter solu­tion since so many oth­ers seem to have found fract suit­able for their needs but I thought I would share my lengthy ideas on why fract should not be used in the case of GLSL depth packing.

Video

Here is a video to try and help explain further.

Z God­dard

Z, a fan of hats and danc­ing to bad music, devel­ops games and code in Unity3D and WebGL. Always look­ing at new tech­nolo­gies for games, he has big dreams for Go.

More PostsTwit­ter

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>