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.

// packing a float in glsl with multiplication and fract
vec4 packFloat( float depth ) {
  const vec4 bit_shift = vec4(
    256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 );
  const vec4 bit_mask  = vec4(
    0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 );

  // fract is the problem
  vec4 res = fract( depth * bit_shift );

  res -= res.xxyz * bit_mask;
  return res;
}

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.

// packing a float in glsl with multiplication and mod
vec4 packFloat( float depth ) {
  const vec4 bit_shift = vec4(
    256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 );
  const vec4 bit_mask  = vec4(
    0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 );

  // combination of mod and multiplication and division works better
  vec4 res = mod(
    depth * bit_shift * vec4( 255 ),
    vec4( 256 ) ) / vec4( 255 );

  res -= res.xxyz * bit_mask;
  return res;
}

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.

Page 1 of 2 | Next page