# Non-linear LED Response Workaround?

I want to create an effect that has a pixel LED slowly going from black to orangered to mimic a component slowly getting red hot.

In theory it’s simple enough, but the results aren’t quite as realisic as I’d like.

The issue seems to be that brightness increases are non-linear, so rather than the brightness emerging really slowly, it’s nothing, nothing, nothing, on quite a bit, then on full.
My code is below and I’ve set the duration to 40,000 which is already quite long.

I guess my question is can we build a style that accounts for the non-linearity of the LED? For instance, can we set it to go from Rgb<0,0,0> to Rgb<20,2,0> over 40,000 milliseconds, then Rgb<20,2,0> to Rgb<125,15,0> over 10,000 ms? Is there way of going up in stages like that? And would it work?

Any thoughts welcome.
Here’s the code I’ve been using:

1 Like

Try the “Metal Forge” Heat Up Ignition Effect in my library under Ignition Options on the Enhancements screen (there’s a corresponding Retraction Effect too).

The problem here isn’t that the LED is nonlinear, because it is. The problem is that it is linear, but human eyes are not. Human eyes have a response that is closer to response = \sqrt[3]{brightness} What this means is that if you increase a black led by a tiny amount, it looks like a big change. If you increase a white led by the same amount, it does not look like a big change.

Because neopixels are linear, and only have 8 bits of precision, the darkest we can make it without turning it off, is 1/255th of it’s maximum brightness. Unfortunately, this still looks fairly bright to the human eye. ProffieOS tries to work around this by doing dithering, which adds some additional levels by rapidly switching the LED between 0 and 1 values, but even that is not enough at very low levels.

There are a couple of ways to possibly get this to work.

1. Cover up the led with some tape or something, then use higher values. By reducing the maximum brightness in this way, we’re also gaining more precision in absolute brightness.
2. Use regular LEDs instead of neopixels. The ProffieOS PWM has 32768 levels, which should be enough.
3. Use dotstar/Apa102 LEDs. These leds have more bits controlling the brightness, leading to much more accuracy in the lower levels. (Not entirely sure how much, because I haven’t tried it.)

To gain more control over the curve and colors, you probably want to use TrConcat<> to concatenate multiple transitions together.

1 Like

Thanks so much guys - legends as always!
I see what you mean about the nature of LED light completely Prof - makes perfect sense. And for my purposes, as I’ve just learned from breaking down Fernando’s code, Trconcat<> was indeed the answer. I broke it down to four steps, with the first literally just going from zero to Rgb<3,0,0> over 4,000 milliseconds, which worked pretty well. The code is below.

I’ll post up a video when I’ve had a chance to shoot one. The way I’ve used it is a really fun effect on this particular hilt.

Many thanks again. In both your debt as always.

1 Like

Just been refining this and have gone to an ignition first step/retraction last step of Rgb<2,0,0> over 4000ms. The overall effect looks great and really natural, but when the LED fades out, it just flickers very slightly at the end, presumably because it’s struggling to illuminate with just 1/127th of its rated full power. I actually like how it looks, but it occurred to me to ask, could this cause damage to the LED, or indeed the Proffieboard, over time?

The flickering probably comes from the dithering, and it’s nothing to worry about.

1 Like

I was trying to create an exponential wave for a seismic charge and I ended up using a Gradient based on an exponential function, only to find that the result was a double exponential on the Neopixel! So I changed it to a linear rise and on the Neopixel it was clear to see that it still looked exponential. TVs have a similar effect with Gamma, which is something like X^2.2.
There is one important thing I learned though which you may want to know. Instead of RGB, try using IntRGB. It’s a lot more precise. Now Fett263 brought up a good point that even if the math goes up to 16bit values the NeoPixels cannot, and will eventually dither to fill in the missing values. But this still may be of great use to you.
I think if I had to do what you’re suggesting I would use a Remap<> function where the input is based on a Trigger<> and the function to map is a Gradient<>, the. You can “build” the gradient based on a square root function like using a spreadsheet or something. The demo Gradient<> has like three colors in it (RGB I think) but you can actually put in a lot more functions. This is where it will be important to use IntRGB<>. I put in like 32 at least.

1 Like

ProffieOS uses linear RGB (16-bit) values, and so does neopixels unfortunately. (leads to bad precision at low levels) TVs, monitors and similar things all use an EOTF which has a gamma near 2.2. The proffieOS style editor will correct the gamma values, but not the gamut, so colors will show up close to the final color in the proffieOS style editor.

I have no idea what IntRGB is though.

Now, if your function ended up having some exponentials in it, there may be reasons for that otherside of pixel EOTFs, but it may also be that your eyes deceive you… If you really want to know you have to use a light meter or something.

2 Likes

Oh my you’re correct. I made a mistake. It’s RGB16<>, not, IntRGB<>! Sorry for the misinformation!
I assume RGB16<> provides the same normalized numbers as RGB<>, but in RGB16 the range [0,65535] is normalized to [0.0,1.0] while in RGB the range [0,255] is normalized to [0.0,1.0].
So let’s say you really wanted to emulate a linear gradient in RGB<> by setting up 10 steps from 0 to 255. Then for each of the values you take the 2.2 root, which for this example I’ll simplify to the square root. Sqrt(255.0) = 15.9 ~ 16, while Sqrt(128.0) = 11.3 ~ 11.
However, if we do this same exercise with RBG16<> then Sqrt(65535) = 255.9 ~ 256 and Sqrt(32768)=181.0~181. There’s less error when using RGB<16> so it’s probably a much better scale to use.
Sorry again for the mistake, hopefully there’s something useful to my theory here.

1 Like

That’s not really how that works. The only difference between RGB8 and RGB16 is precision, the gamma (or EOTF) is a separate thing.

When doing a gamma transformation, you need to normalize the numbers to the range 0-1 first though. Since RGB16 32768 and RGB8 128 both transform to 0.5 when you normalize the numbers, the gamma transformation will come out the same way for RGB8 and RGB16.

ProffieOS uses a gamma of 1.0 though, so the linear values and the RGB values are the same. This is also why ProffieOS needs to use RGB16…

A gamma of 2.2 means that the difference between 0, 1 and 2 are very very small, while the difference between 254, 255 and 256 are much bigger. This works well because our eyes adjust, so we can see very small differences in dim colors, but we can’t see very small differences between bright colors.

When using gamma 1.0, the difference betwen 0 and 1 is exactly the same size as the difference between 254 and 255. In practical terms, that means that the difference between 0 and 1 is quite large to the human eye, which would make smooth transitions between dim colors impossible.

By using RGB16, we gain a lot more precision. The difference between 0 and 1 is still exactly the same as between 65534 and 65535, but that difference is 256 times smaller than if we had used RGB8. RGB16 has much higher precision than what is required for bright colors, but in return, we get to do blending in linear space, where 1+1 is always 2. (Adding and subtracting colors in gamma 2.2 space can produce some weird effects sometimes.)

Historically, the gamma 2.2 comes from the response curve of CRT displays, but it also turns out to be a neat trick to reduce the amount of information needed to accurately display a picture. RGB8 has some limitations, but with gamma 2.2, it can represent most of what the human eye can see.

1 Like