Wednesday, November 4, 2009

Fun with Lumas

I've been playing around with the internal format used to store luma maps (for fullbright colours) in DirectQ. A coupla factors are at work here; firstly, DirectQ has been storing them in full XRGB format meaning 32 bits per texel, meaning a whole heap of wasted memory owing to the blend mode used for them ((light + luma) * texture). Secondly, the new r_overbright cvar needs to rescale the lumas on change to prevent them from becoming over-saturated, so in order to do this properly it needs somewhere to stash the original value. I had been storing it in the unused 8 bits, but - and in order to achieve memory savings (not to mention faster texture uploads) - I want to reduce the number of bits per texel. So I chose D3DFMT_A8L8 as a possibly viable alternative (just plain old _L8 is not an option as there is no extra space to store the original value), which is the equivalent of GL_LUMINANCE_ALPHA.

Unfortunately the conversion from raw RGBA data to a luminance format is accomplished by either averaging or adding the 3 channels, rather than doing something sensible (I believe OpenGL does the same so don't bash D3D over it please). Obvious problem here is that some texels have values of stuff like 255/255/128 whereas others have values of 255/0/0. So rather than a single uniform brightness to add to the lightmap where a fullbright colour is rendered, we have a mixture where some colours are dull and others are heavily over-saturated.

It seems simple and obvious now that I've been actually getting in behind the scenes on it, but I've been writing code using this blend for almost 2 years now and had seriously never cottoned on to it.

A complication is the fact that for external textures I use D3DX helper functions where possible, so that I can load a wide variety of image formats without having to write, test and debug code for them. These don't give me access to the raw RGBA data in order to actually do something about it (like take the brightest of the 3).

I think the approach I need to take here is twofold. For the native textures I need to create a second palette at startup time which contains values of 255/255/255 for all fullbright components, and use that for the upload (I upload textures direct as _P8 format so this is easy: just detect if it's a luma and switch the palette pointer). For external textures I'm thinking that I need to initially load them into a surface, then manipulate that, then load that into the final texture. A bit more longwinded than I had hoped, but it seems like the right way.


Update:

Just did the native palette switch trick and it's working beauifully. Lumas now look virtually identical for all values of r_overbright, and also look almost identical to software Quake; no more oversaturated colours - yayyy!!!.

While I was at it, I added an r_lumaintensity cvar, which you can use to control the intensity of lumas in the game (or set it to 0 to switch them off).

What this means is that I've also relaxed my policy on fullbright colours. I suppose it's a victory of that "pragmatism over purity" thing I talk about sometimes; unfortunately the reality is that we have over 13 years worth of damage by texture artists who didn't fully understand the format they were working with to contend with.

0 comments: