Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LEDs: add rainbow effects #11

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

Conversation

acheronfail
Copy link
Contributor

@acheronfail acheronfail commented Aug 15, 2024

Setting the headlights to Solid with white, and then setting the taillights to Rainbow Cycle allows users to replicate the Mullet effect from the Float package.

Using Rainbow Cycle on the front and the back replicates the "Rave" effect from the Float package.

Rainbow Fade has also been added, which was inspired by the Float package's "RGB Fade" effect.
Renamed to RGB Fade after using color_wheel.

@acheronfail
Copy link
Contributor Author

acheronfail commented Aug 15, 2024

Doesn't look that great with the camera I have, but this'll do for a preview.

rainbow_cycle_vid.mp4
rainbow_fade_vid.mp4

src/leds.c Outdated Show resolved Hide resolved
@acheronfail acheronfail force-pushed the led/rainbow branch 2 times, most recently from fae1b23 to fae1b87 Compare August 15, 2024 08:11
@acheronfail
Copy link
Contributor Author

Here's the RGB Fade using color_wheel instead. I think it still looks good, plus using color_wheel let me remove a almost all of the code for the previous fade implementation I had!

rgb_fade_vid.mp4

@acheronfail acheronfail marked this pull request as ready for review August 15, 2024 08:14
@lukash
Copy link
Owner

lukash commented Aug 15, 2024

Ok, it doesn't really go through all the colors very consistently though, I also noticed it tends to just return shades of red, green and blue. It's a deficiency of the color_wheel() function, maybe it could be improved... Definitely not necessary for this PR, but would you maybe like to give it a try? Ideally it should go through the rainbow in a consistent way.

@acheronfail
Copy link
Contributor Author

Definitely not necessary for this PR, but would you maybe like to give it a try? Ideally it should go through the rainbow in a consistent way.

yep, in that case I’ll rename it back to “Rainbow Fade” and I’ll have a crack at improving color_wheel 👍

@acheronfail
Copy link
Contributor Author

After trying out a bunch of different ways to interpolate the colours (tried smoothing RGB, tried HSV values, etc) I found that this provides a much more "natural" looking fade between the colours of the rainbow.

Again, my camera isn't the best, it looks much better in person, but here's the demo of it:

rainbow_fade_vid.mp4

Copy link
Owner

@lukash lukash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, the colors look way better, thank you.

The change to color_wheel() definitely deserves to be in its own commit, please.

Besides that, a couple comments. If at some point you'd like to exit the rabbit hole, let me know (I'd probably try to finish it off myself at some point).

src/leds.c Outdated Show resolved Hide resolved
src/leds.c Outdated Show resolved Hide resolved
Fix: improved accuracy of generated rainbow colors for LEDs
@acheronfail
Copy link
Contributor Author

acheronfail commented Aug 16, 2024

Alrighty, I separated the commits, and had a crack at creating an optimised version which avoids the trig functions, and instead uses a bunch of multiplications with a few divisions. It almost perfectly matches the previous implementation.

I'm not really sure how I can profile these changes though? Aside from statically analysing it myself?
I threw everything into https://godbolt.org/ but I couldn't really see any major improvements with my new version... 😅

If you have any pointers I'm all ears! I've been using this tiny tool I created to test the LED colours without actually having to write the new package to my wheel: https://github.com/acheronfail/led_test since writing it over bluetooth each time was getting rather cumbersome...

Using "Solid" on the headlights and "Rainbow Cycle" on the taillights
can replicate the "Mullet" setting on the Float package.

Using "Rainbow Cycle" on both replicates the "Rave" effect from the
Float package.

Feature: add Rainbow Fade and Rainbow Cycle effects
@lukash
Copy link
Owner

lukash commented Aug 16, 2024

You've already done more instrumentation than I'd bother with 😀

I'm not sure how representative the profiling can be on x86_64, given all the differences, and assuming you're running and measuring a lot of iterations. What you could do is running the code in a tight loop on the VESC and monitor CPU usage, which you can see either in LispBM scripting or possibly in Terminal when you list threads (where it's time-based). There may be other ways too, these are the simple ones I know of.

If you could provide performance measurements for the three implementations that'd be great.

IMO if you set up the loop so that you get CPU load between 20-80% for all three versions and just record the percentage from LispBM it'll be a very representative result.

As for the Taylor Series, they should be good enough. I still think it could possibly be quite a bit simpler (my single quadratic function looked very close) but I haven't done the hard work so I'll leave that up to you unless the performance is somehow bad.

Thanks for the work you're putting into this!

@lukash
Copy link
Owner

lukash commented Aug 17, 2024

IMO if you set up the loop so that you get CPU load between 20-80% for all three versions and just record the percentage from LispBM it'll be a very representative result.

Thinking about this, it's not correct, or rather not trivial to do right. But the old "run in a loop and measure time" can be done quite easily on the LED thread:

diff --git a/src/leds.c b/src/leds.c
index 465082e..cd20ee1 100644
--- a/src/leds.c
+++ b/src/leds.c
@@ -762,6 +762,14 @@ void leds_update(Leds *leds, const State *state, FootpadSensorState fs_state) {
         return;
     }

+    if (fs_state == FS_LEFT) {
+        log_msg("WHEEL STARTT");
+        for (uint32_t i = 0; i < 10000000; ++i) {
+            volatile uint32_t a = color_wheel(42);
+        }
+        log_msg("WHEEL FINISH");
+    }
+
     if (leds->cfg->on) {
         if (leds->on_off_fade == 0.0f) {
             full_animation_reset(leds, current_time);

I measured the following:
Old implementation with shifts: 0.573s
Implementation with Taylor series: 0.570s
Implementation with sinf: 130s

So the sinf is about 230x slower 😁 (I also tried to remove the acosf() from the TAU definition and it was the same, so the compiler did manage to optimize that out it seems).

The Taylor series implementation is faster than I thought :) Having another look at it, it seems the color_wheel() input of 0..255 is mapped onto input range 0..TAU for the taylor series, this could potentially be simplified if the Taylor series took the 0..255 directly. The way it is, there are some unnecessary arithmetic operations. Not a real performance concern, just code simplicity. I'll leave it up to you if you want to go to that length.

Otherwise, no issue with the code. Just tested it and to me it seems the color_wheel() now mainly cycles through the combination colors and doesn't really show red, green or blue though. I think maybe the curve should be a sine in the end? At least that's worked best for me in anim_fade().

Last note, on speed. I think we should set some consistent speed baseline (as makes sense for each effect). E.g. the Strobe effect I added switches after 1 second and the user is expected t oset the speed faster if they want. I did merge the Felony at fast speed, but now I wonder if we should make these slow by default (1s period) and have the user set the faster speed? I'm mainly after consistency here, I don't want one effect to be at 1s period and another at 0.05s...

@acheronfail
Copy link
Contributor Author

Oh, awesome results!

Thinking about this, it's not correct, or rather not trivial to do right. But the old "run in a loop and measure time" can be done quite easily on the LED thread:

Question, where do I see these logs? Is there somewhere in VESC Tool?

The way it is, there are some unnecessary arithmetic operations. Not a real performance concern, just code simplicity. I'll leave it up to you if you want to go to that length.

Have a look at this commit and tell me what you think: acheronfail@fae1078 - I changed color_wheel to take a float, and also simplified a bunch of the operations as much as I could.

Otherwise, no issue with the code. Just tested it and to me it seems the color_wheel() now mainly cycles through the combination colors and doesn't really show red, green or blue though. I think maybe the curve should be a sine in the end? At least that's worked best for me in anim_fade().

The Taylor Series logic is approximating sine, I think the reason we don't see the red/green/blue is because of the offsets between the colours. For example, to get red we'd need only the red calculation to be at the top of the curve, while the other two are lower. But in its current state, I've been offsetting each of the colors about 1/3 away from each other on the curve, so there won't be a time we see the primary colours...

Last note, on speed. I think we should set some consistent speed baseline (as makes sense for each effect). E.g. the Strobe effect I added switches after 1 second and the user is expected t oset the speed faster if they want. I did merge the Felony at fast speed, but now I wonder if we should make these slow by default (1s period) and have the user set the faster speed? I'm mainly after consistency here, I don't want one effect to be at 1s period and another at 0.05s...

I agree with that, I also changed that in acheronfail@fae1078, so if you think that looks good I'll bring that into this branch and split things into their own commits.

@lukash
Copy link
Owner

lukash commented Aug 18, 2024

Question, where do I see these logs? Is there somewhere in VESC Tool?

Yes, in the LispBM Scripting tab under Dev Tools, which also allows to restart the package, watch CPU and RAM usage and run lisp.

Have a look at this commit and tell me what you think: acheronfail@fae1078 - I changed color_wheel to take a float, and also simplified a bunch of the operations as much as I could.

Well that's one thing, another thing is you're converting input to the 0-tau range, but if the Taylor series was made to work with the input verbatim this wouldn't be needed. It's just a small thing though.

The Taylor Series logic is approximating sine, I think the reason we don't see the red/green/blue is because of the offsets between the colours. For example, to get red we'd need only the red calculation to be at the top of the curve, while the other two are lower. But in its current state, I've been offsetting each of the colors about 1/3 away from each other on the curve, so there won't be a time we see the primary colours...

Your Taylor Series is not approximating sine, it's approximating a sqrt(sine(...)), quite a different function. I kinda assumed if you would just use sine, that would render the base colors better. But not sure how that is with the three channels offset. Maybe it needs to be done a bit differently still.

I agree with that, I also changed that in acheronfail@fae1078, so if you think that looks good I'll bring that into this branch and split things into their own commits.

I'm still a bit undecisive on the timing thing - what should be the baseline? I mean making everything have 1s period makes some sense, but for some effects it'll be too fast (e.g. your rainbow fade going through all colors in 1s will likely not be very good). Other effects, e.g. the Knight Rider have a defined "standard" speed. Maybe we should use 1s period for the blinking effects and just something that works well for the other ones.

If you don't mind, update the timing of the effects in this PR to what you think is best and we can later fix up Felony to have consistent timing (other effects might need updating too, I can handle that).

I'm not at home this week, my responses might be slow. I'd like to play with the color_wheel() colors and see how it is myself, since we're at it, I'd like it to have a nice and consistent rainbow 🙂.

@acheronfail
Copy link
Contributor Author

acheronfail commented Aug 18, 2024

Your Taylor Series is not approximating sine, it's approximating a sqrt(sine(...)), quite a different function. I kinda assumed if you would just use sine, that would render the base colors better. But not sure how that is with the three channels offset. Maybe it needs to be done a bit differently still.

My apologies, I didn't write that clearly enough. I was referring to my latest commit, which I linked from the previous post. If you look here: https://github.com/acheronfail/refloat/blob/fae10786a56c8ce62171f9d053447a8e2192617d/src/leds.c#L103-L123 the Taylor Series does indeed approximate sine, and I also readjusted the calculations so TAU is no longer needed, etc. So that version is indeed using sine, but you still don't get the primary colours due to the offsets being used.

I'm still a bit undecisive on the timing thing - what should be the baseline? I mean making everything have 1s period makes some sense, but for some effects it'll be too fast (e.g. your rainbow fade going through all colors in 1s will likely not be very good). Other effects, e.g. the Knight Rider have a defined "standard" speed. Maybe we should use 1s period for the blinking effects and just something that works well for the other ones.

I'm not 100% sure either, but we can decide that later I think.

I'm not at home this week, my responses might be slow. I'd like to play with the color_wheel() colors and see how it is myself, since we're at it, I'd like it to have a nice and consistent rainbow 🙂.

I'd also like some improvements to color_wheel too, so no stress. I'm in no rush to get this out, just had a bit of spare time and enjoyed tackling the problem. If I get any more time this week I'll have another go!

@aeraglyx
Copy link
Contributor

If we assume the LEDs are linear and have the same primaries as sRGB, we could use Björn Ottosson's oklab for some nice gradients? Could look something like this:

static uint32_t color_wheel(uint8_t pos) {
    float lightness = 0.75f;
    float chroma = 0.1275f;
    float hue_rad = TAU * pos / 256.0f;

    float a_ = chroma * cosf(hue_rad);
    float b_ = chroma * sinf(hue_rad);

    float l_ = lightness + 0.3963377774f * a_ + 0.2158037573f * b_;
    float m_ = lightness - 0.1055613458f * a_ - 0.0638541728f * b_;
    float s_ = lightness - 0.0894841775f * a_ - 1.2914855480f * b_;

    float l = l_ * l_ * l_;
    float m = m_ * m_ * m_;
    float s = s_ * s_ * s_;

    float r = +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s;
    float b = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s;
    float g = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s;

    return ((uint8_t) (r * 255) << 16) | ((uint8_t) (g * 255) << 8) | (uint8_t) (b * 255);
}

I haven't properly tested the code (I have an ADV), but after trying it in Blender I got this gradient:

image

The values for lightness and chroma were eyeballed to be as large as possible without any of the output channels clipping. Trig functions could use that Bhaskara approximation or something.

It doesn't go through 100% red, green and blue, because if it did it couldn't be perceptually uniform. Here's a cool website that demonstrates how it works as a color picker.

@acheronfail
Copy link
Contributor Author

Oh nice, I'll have to learn how to do that with Blender, being able to visualise that is really cool!

Tonight I'll try out this code on the VESC and report back how it goes.

@acheronfail
Copy link
Contributor Author

@aeraglyx I had a crack at implementing it, this was my code:

// Generate colors using the OKLAB colorspace, see: https://bottosson.github.io/posts/oklab/
static uint32_t color_wheel(uint8_t pos) {
    // See: https://oklch.com/#75,0.13,360,100
    float lightness = 0.75f;
    float chroma = 0.1275f;

    float cos_pos = 2 * (float) pos / 256.0f;
    float sin_pos = fabsf(cos_pos - 0.65f);
    float a = chroma * (2 * cosine_progress(cos_pos) - 1);
    float b = chroma * (2 * cosine_progress(sin_pos) - 1);

    float l_ = lightness + 0.3963377774f * a + 0.2158037573f * b;
    float m_ = lightness - 0.1055613458f * a - 0.0638541728f * b;
    float s_ = lightness - 0.0894841775f * a - 1.2914855480f * b;

    float l = l_ * l_ * l_;
    float m = m_ * m_ * m_;
    float s = s_ * s_ * s_;

    // convert to lRGB colorspace
    float lr = +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s;
    float lg = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s;
    float lb = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s;

    // and finally to RGB
    float rgb_r = lr > 0.0031308f ? 1.055f * powf(lr, 1.0f / 2.4f) - 0.055f : lr * 12.92f;
    float rgb_g = lg > 0.0031308f ? 1.055f * powf(lg, 1.0f / 2.4f) - 0.055f : lg * 12.92f;
    float rgb_b = lb > 0.0031308f ? 1.055f * powf(lb, 1.0f / 2.4f) - 0.055f : lb * 12.92f;

    return ((uint8_t) (rgb_r * 255) << 16) | ((uint8_t) (rgb_g * 255) << 8) |
        (uint8_t) (rgb_b * 255);
}

And here's what it looks like:

oklab.mp4

It does seem to cycle nicely, but I find the greens too bright compared to the rest (the glare in the video shows that a little bit). I had a decent crack at trying to tweak it, but with OKLAB it appears that those greens are quite bright.

@lukash
Copy link
Owner

lukash commented Aug 28, 2024

Hey, I don't have time to share now, but I had a go at tweaking @acheronfail's original attempt a couple days ago to reasonably satisfactory results. I also did a very quick test of the OKLCH, I'll post a comparison.

@aeraglyx
Copy link
Contributor

@acheronfail Thanks for checking it out!

Why is the sRGB transform at the end needed? I sort of expected the LEDs use something like PWM for brightness, in which case it should just stay linear I think. The green channel peak in the oklab sweep is noticeably darker to compensate for green being perceived too strongly, but the ~1/2.2 gamma would make them more equal at the peaks if that makes sense (so green would seem relatively brighter again). Could be wrong though..

And just for fun, here are the individual channels plotted against hue:

oklab_rgb_sweep

So maybe the simplest solution would really be just scaled sine waves. If oklab ends up working, I tried to very roughly match it with cosines on Desmos.

@lukash
Copy link
Owner

lukash commented Aug 28, 2024

So, first on my tweak of @acheronfail's original approach, to me it seemed that due to nonlinearities of the LEDs (and/or possible other factors, I don't really know) the sine blending wasn't working well and I perceived the sine waves need to be kind of squished. So instead of feeding them a linear function f(x) = x, I used a "to the power of n" transition (not sure I'm calling things the right names here).

Plot demonstration on desmos

You can see a regular sine wave as blue, orange is the "transition" of x and purple is the resulting modified sine wave.

I applied this with different exponents to the three channels to create a uniform rainbow pattern. The exponents can be easily tweaked to shift the widths of the particular colors.

Here's a demonstration of the result with direct comparison to @aeraglyx's okchl implementation:

PXL_20240828_101805695.TS.mp4

(first is my modification, which is switched to okchl and then back and forth again)

So my modification is for the most part quite uniform and shows all the hues with relatively uniform widths. It shows more saturated colors and the yellow in particular is quite a bit wider than in okchl. okchl colors are more muted (mentioned by @aeraglyx as intentional). I quite like it in this particular rainbow effect, but I was thinking about this and I thought we could use this hue generating function in the future to specify the colors in the config (instead of the fixed enum of colors that is there now). In effect we'd be expressing the colors using hue (stored as 1 byte), chroma and lightness. I'd like it to cover all the possible colors that can be achieved with the RGB LEDs as well as possible. Maybe okchl is the way to go, using higher chroma to get the pure primary colors (which the proposed color_wheel doesn't do)? Maybe the hack I came up with will work better in practice?

I mean ideally we'd somehow transform the LED input to make them close to linear, but I felt it's not possible with a simple gamma function (hence I settled with a simple quadratic function for gamma, which I found not being great).

Anyway, code for my modification as well as the okchl function used in the demo is here: https://github.com/lukash/refloat/tree/color-wheel

You can use the portion and offset variables to show just a section of the hue range, which I used to tune the exponents of the polynomial functions.

Hope the above makes sense, it's late, I'm tired and don't have too much time for this... 😅

@acheronfail
Copy link
Contributor Author

acheronfail commented Aug 28, 2024

Why is the sRGB transform at the end needed?

Perhaps I implemented incorrectly at the beginning, but I found that the original transformation caused very dim and unpleasant effects - I then looked up some libraries that supported okchl and saw that they converted to sRGB after a similar transformation, which provided slightly better results... That said, looking at @lukash's demo, I think it was a case of PEBKAC. 😅

I do quite like the effect you created there, @lukash, with the LEDs showing a rainbow one by one. Would you be happy to merge this in if I took that combined with the okchl, and then made it cycle left-to-right through the rainbow? I think that would at least be a good start.

I was thinking about this and I thought we could use this hue generating function in the future to specify the colors in the config (instead of the fixed enum of colors that is there now). In effect we'd be expressing the colors using hue (stored as 1 byte), chroma and lightness. I'd like it to cover all the possible colors that can be achieved with the RGB LEDs as well as possible. Maybe okchl is the way to go, using higher chroma to get the pure primary colors (which the proposed color_wheel doesn't do)? Maybe the hack I came up with will work better in practice?

I think this is a good idea, but probably a future improvement and not for the addition of these rainbow effects?

@aeraglyx
Copy link
Contributor

I'd like it to cover all the possible colors that can be achieved with the RGB LEDs as well as possible. Maybe okchl is the way to go, using higher chroma to get the pure primary colors (which the proposed color_wheel doesn't do)?

Oklch can do all sRGB colors (including primaries), but not all LCH combinations yield valid colors, so the final RGB values need to be clipped in some way. There is Okhsv, which pretty much solves that, but it's more complex and compromises other things.

I definitely like the idea of specifying config colors in some LCH/HSV space instead of enums. Even if the typical HSV was used, rainbows could still leverage the improved method (looks pretty good!) or Oklch.

The reason I brought up Oklab in the first place was that I don't really like how RGB strips and stuff usually "pulse" with changing hue (blue is dark etc.), maybe the rainbow's output channels could be scaled a bit to compensate for that? But that's just a tiny nitpick, also who knows how much this will vary across different hardware...

@acheronfail acheronfail marked this pull request as draft October 21, 2024 22:23
@acheronfail
Copy link
Contributor Author

Sorry, I don't have the time right now to commit to finishing this at the moment (that, and the LEDs on my board aren't working very well either, so that makes it difficult to write LED code! 😅)

In the meantime I've converted this PR to a draft to indicate it's not ready.

If anyone else wants to pick it up please do, but for the time being I'm putting this on the shelf!

@lukash
Copy link
Owner

lukash commented Oct 26, 2024

@acheronfail no worries. I've been slowly messing with the OKLCH implementation and wanna come up with a decent color_wheel() function for the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants