Post 133

No More Banding

The 2D filter chain now carries floating-point precision from node to node instead of rounding to 8 bits at every hop, so deep filter stacks stop banding and highlights above white survive the trip. Photos and 3D renders get the same treatment, which is a polite way of saying they previously did not. And Export Image now saves the frame you are actually looking at.

Stack three filters on a gradient, pull the levels down and back up, and you expect to get your gradient back. What you got was a staircase.

A grey gradient rendered as thirteen hard vertical bands

That is one ramp, black to white, sent through three Brightness nodes whose gains multiply to exactly 1.0. Net effect: nothing. It should come out the way it went in. Instead it came out as thirteen slabs, because each node handed the next one an 8-bit texture, and 8 bits is 256 values total. Crush the ramp to 5% of its range and you have thirteen values left to describe it. Expand it back up and you get thirteen values, now spread very confidently across the whole frame. The information was gone two nodes ago; the last node just made the absence bigger.

Here is the same chain now.

The same gradient rendered smoothly with no visible banding

Every filter now passes 16-bit float to the next one, and the rounding to screen-friendly 8-bit happens once, at the very end, where it belongs. The chain quantizes when it hands the picture to your monitor, not eleven times on the way there.

Above white

The other thing 8 bits could not do is hold a value brighter than white. Not “held it badly”. Could not hold it. Anything over 1.0 got clamped flat on store, so a filter that pushed a highlight up and a later filter that pulled it back down would meet in the middle and find the highlight had been shaved off and left behind somewhere upstream.

This is not an exotic case. Brighten-then-grade is a normal grading move. Bloom exists specifically to do something interesting with the parts of an image above white. Now the headroom survives the trip, so you can push into it and come back out.

Values still clip when they finally hit the screen, because screens are like that. What you do about it is your call: put a Tonemap in the chain and choose how the highlights roll off. Lux is not going to secretly apply one behind your back and quietly restyle every patch you already made.

Photos and 3D renders, too

The first cut of this fix had a hole in it, and it was a big one: it only helped chains that started at a Ramp, a Noise, or a Solid Color. Start your chain at an imported photo or at a 3D render instead, and you got the staircase anyway. Those are, of course, the two things people actually start chains with.

The reason is a genuinely awkward detail. An imported PNG and a 3D render both arrive tagged as sRGB, which means the GPU quietly converts them to linear light the moment a shader reads them. Promote one of those to floating point and you have a buffer full of linear values sitting next to a buffer full of screen-referred values, both of them just numbers, with nothing to tell them apart. Guess wrong when you write it to the screen and the whole frame comes out several times too dark.

So the buffers now carry a note saying which they are. Read a photo, work in linear, convert back exactly once on the way to the display. Your gradient stays smooth whether it came from a Ramp node or a camera.

Saving the thing you are looking at

Teaching the chain to work in float means teaching the other end how to read it, and the display was not the only other end. The Export Image node is also a way out of Lux, and it had not been told. Feed it a filtered picture and it took a 16-bit buffer, insisted it was an 8-bit one, and fell over. The window showed your frame quite happily the whole time.

Export now goes through the same conversion the screen does, which gives you the only guarantee that matters here: the file on disk is the frame you were looking at when you hit the trigger. That includes 3D renders and photos, where getting it wrong would have handed you a file about five times too dark while the preview looked perfect. Those are the exports you would not have noticed until much later, in someone else’s edit.

While we were in there we found that Export Image had not written a file in some time. It saves to a temporary name and renames it into place, so a crash cannot leave you with half a PNG, but the encoder was picking its format by reading the file extension, and the temporary name does not have a useful one. It had been politely failing into the log ever since. It writes files now.

What this does not change

Nothing you have already made should look different. The float buffers carry the same values the 8-bit ones did, just without the rounding, so the same picture comes out the other end, minus the staircase. Every existing render snapshot passed untouched, which was the point. This buys precision, not a new look.

← All posts