-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Copy Segment FX #4124
base: 0_15
Are you sure you want to change the base?
Copy Segment FX #4124
Conversation
- copies the source segment - brightness of segment is relative to source segment - optionally shifts the color hue - invert, transpose, mirror work - if source or targets do not match in size, smallest size is copied - unused pixels fade to black (allows overlapping segments) - if invalid source ID is set, segment just fades to black - added a rgb2hsv conversion function as the fastled variant is inaccurate and buggy note: 1D to 2D and vice versa is not supported
just realised: if the source segment is inverted, the targed does not invert (which is ok) but if the source is mirrored, only half of the pixels are copied unless the mirror settings in the target segment are the same. |
My quick suggestion would be to use I don't have a good setup for testing this but I'll give it a go when I can. |
I did not know about this distinction, that is actually what I was looking for :) |
that does not seem to work as intended. the numbers are correct (i.e. the width is not halfed if mirrored) but one half still stays black. |
We are being foiled by |
wled00/FX.cpp
Outdated
if(SEGMENT.custom2 > 0) // color shifting enabled | ||
{ | ||
CHSV pxHSV = rgb2hsv(sourcecolor); //convert to HSV | ||
pxHSV.h += SEGMENT.custom2; // shift hue | ||
hsv2rgb_spectrum(pxHSV, sourcecolor); // convert back to RGB | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gets copy and pasted below - probably it wants to be a little static function instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, can you help out? I do not know what the best way to implement it is. just adding a function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, pretty much. Something like:
static CRGB shift_hue(CRGB sourcecolor, uint8_t amount) {
if (amount > 0) {
CHSV pxHSV = rgb2hsv(sourcecolor); //convert to HSV
pxHSV.h += amount; // shift hue
hsv2rgb_spectrum(pxHSV, sourcecolor); // convert back to RGB
}
return sourcecolor;
}
My inclination would be to leave it here in FX.cpp (hence static
), to allow the compiler to inline it if it wants to. Organizationally though it might be a better fit for colors.cpp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, I though you meant some fancy C++ macro magic ;)
but good point, will add this to color utils
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the shift_hue
function is on a critical path (like pixel copy), then it would be smarter to put the static function into fx.cpp so the compiler can inline. Inlined functions are faster (no overhead due to call-and-return).
But the compiler will not inline things that are defined in a different .cpp file.
@DedeHai looks like start offsets are missing. strip.setPixelColorXY(start + width() - xX - 1, startY + height() - yY - 1, tmpCol); |
correct, can confirm that adding |
target segment now also copies the source settings (mirror, grouping, spacing, transpose) so the target looks exactly like the source. inverting can still be set independently. note: the bug in line 214 of FX_2Dfcn.cpp missing `start` needs to be fixed for it to work properly in 2D.
did some testing on the latest commit. it works fine with one exception: setting spacing other than 0 seems to screw things up. the target segment does weird things and I have no idea how to fix it. |
This feels wrong to me - I want to set my own mirroring or spacing settings on the destination segment and they should be respected, not overridden. However I expect the 'source data' to be the rendered output, including the source's mirroring/reversing/grouping etc. Spacing is where things get tough, though. Spacing+offset is for creating interleaved segments. My gut feeling is to pretend the gaps don't exist when copying -- leave it to the destination segment to decide how to render those. I think this might be a bit tricky; I'll sketch some code. |
it does feel wrong, but you cannot copy data that just is not there. like if source is mirrored, target cannot be 'unmirrored' without re-calculating the FX. 'reverse' is still possible (and is not copied). what I did not test at all yet: what happens if global buffer is disabled. |
I don't think that's true - your original logic with That was the original concern that spawned this discussion, I thought; we wanted the copy to operate on the "rendered" mirrored/reversed/grouped FX output, so if the destination segment had no flags set, it would look completely identical to the source segment. I would then expect that any flags set on the destination are applied normally to the rendered copy. To implement this, it looks like we'd need a new "getRenderedPixelColor()" that has slightly different index calculation logic. I have a working 1D version at home, sorry I didn't get time to publish it last night. That said: if we don't want to go that way, I think the reasonable alternative is to ask the user to manually harmonize the segment flags they want. For example, if the source segment is 8x16 (mirrored, so the FX calculates 8x8), I think it's reasonable for the copy operation to treat that as an 8x8 source (eg. exactly your original code); and then it's up to the user to set the mirror flag on the destination segment if they also want it mirrored. To sum up: I think either we should copy from either the "rendered" space, for which we need new getPixelColor flavors; or from the "FX" space, ignoring the source segment flags, as your original code does. In either case the destination segment render flags should be set by the user, I really don't think we should override them. Note I've completely passed over "spacing" and "offset" here -- IMO these options should be ignored by the copy operation as their purpose is to specify which pixels are /not/ part of the segment, and thus shouldn't be considered for the copy. |
I tried using .length() instead of virtual and confirmed the loop went over the full index range but if mirror of source is active, it sill only gets half of the data, the rest is still black (which is weird). when you say 'rendered space' that is the buffer that is sent out and 'FX space' is that same buffer but with 'mapped' access right? I still don't fully grasp on what is mapped how as in the code it is really hard to grasp where a pixel color actually gets written to (or read from). |
I did implement "raw" get/setPixelColor (in one of my POCs) that did no translations but it proved wrong and there were always ways that it did not work properly. |
This is the expected behavior of
I'd defined "render space" as "the pixels that are actually written to" without any gaps. This might just be my idiosyncratic way of of thinking about it though... The way I think of it is:
[ 1, 2, 3, 4 ] FX space is mapped to a "render space" via mirror, transpose, reverse, grouping. (There are currently no functions to access this; I had proposed it as new concept to use as a copy source.) mirror - note length is doubled from FX space grouping=2, mirror, reverse - length x2 for mirror, x2 again for grouping "Render space" is then mapped to "strip space" with start, stop, spacing and offset. Note that spacing is applied in between groups. "strip space" can be accessed via start=3, spacing = 1, offset = 0, mirror start=0, spacing = 1, offset = 1, mirror start=0, spacing = 1, offset = 0, grouping=2, mirror, reverse - length x2 for mirror, x2 again for grouping Finally "strip space" is mapped to actual LED addresses using ledmap.json. (Not shown, but I mention for completeness.) Anyways it sounds like the right thing to copy is FX space. I still feel strongly that we should not be editing the segment properties in the FX function -- the software expects those to be inputs, not outputs, and you'll have a bunch of weird side effects on the UI and config management as the software doesn't expect those to be writable by FX.
Yeah I'd had a feeling we could get away with something there, but I hadn't worked through all the implications yet. There's also the 1D->2D conversion to consider; |
Ah get it, if you access outside of 'virtual space' it does not return a valid color, hence getting black. edit: |
🤔 I think @willmmiles has described it quite accurate, maybe just a bit over-compilcated.
As you want to copy effects ( = segments), I thinks its enough to copy what The target segment might not be the same size as the source segment -> so maybe implement your own version of "mirror" and "mirrorY", to preserve the overall look. But don't try to duplicate segments settings because the result will not look the same any way. Dimensions might be completely different, so the mirror axis will usually not match with the source segment. Edit: sorry made a mistake im my original comment - |
PS: if you need a few nasty testcases
|
I'd say its better to always use the "right" versions - 1D or 2D - if you are on Actually this function is a good example how to handle 1D and 2D Lines 1101 to 1111 in b9080f9
(just except for the |
I think a lot of the challenge here comes from some of the corner cases. If I have a segment 1 set as1x8, with mirror enabled, I get virtualLength() == 4, values [1 2 3 4], and what is drawn on segment 1 is [1 2 3 4 4 3 2 1]. If I then set "Copy" on Segment 2 (also 1x8) - what should I get? The simple implementation (434ba3f) will give me [1 2 3 4 X X X X], where X is off. I have to set 'mirror' on segment 2 to make it actually look like a copy of segment 1. This seems counterintuitive, why don't I get a "whole" copy? Supposing we accept that logic - if segment 1 is mirrored, we should render the mirrored output on segment 2. Mirror, reverse, and transpose all seem reasonable to copy from -- though as @softhack007 notes, copying the flags won't do what we're expecting if the segment sizes don't match. Grouping? Maybe - this is somewhat less clear. Spacing? Definitely not, weird stuff will happen. What if segment 2 is 1x16, and has the mirror flag set on it? With the naiive code, I'd get [ 1 2 3 4 X X X X X X X X 4 3 2 1]? Or should I expect [1 2 3 4 4 3 2 1 1 2 3 4 4 3 2 1]? This is why I added that idea of "render space" above - to capture what I think we want as the "copy source". Anyhow, I suggest https://github.com/willmmiles/WLED/tree/pr/DedeHai/4124 as an example implementation - it's lame but I think it should copy the "obvious" way without disrespecting the target segment settings. Note I've only tested on 1D, I don't have a 2D system on my bench right now. :( Note that we don't need to check the source segment sizes - getRenderedPixelXY checks the coordinate validity and returns black if it's out of range - so we can also omit the fadetoblack, too. |
I tested temporarily setting the target segment settings to the sorce settings (which can be enabled in the FX by a check). This works but is not perfect (does not solve the things mentioned about non-equal sizes, mirroring will leave a gap in the center but that is a separate issue). Edit: |
I dug out a 2D panel and tested the implementation I sketched last night: https://github.com/willmmiles/WLED/tree/pr/DedeHai/4124 -- it seems like it handled every case I tried reasonably well, even 1D->2D conversion; though it's still missing the 2D->1D conversion case. Give it a go and let me know what you think? |
I tested your version, there is a few bugs which I fixed and now it works amazingly well and does exactly what we discussed. There is still some cases I want to run but generally this is the way to go. |
Apparently I don't do enough with 2D. ;) Possibly the offset field is carrying some historical value, as it's not used in the standard 2D calculations. I can think of two ways to approach fixing that: either (a) separate the 1D and 2D |
- added color adjust function with support for hue, lightness and brightness - colors can now be adjusted in hue, saturation and brightness - added `getRenderedPixelXY()` function (credit @willmmiles) - source is now accurately copied, even 1D->2D is possible - fix for segment offset not being zero in 2D in `getRenderedPixelXY()` - added checkmark to copy source grouping/spacing (may remove again) - tested many different scenarios, everything seems to work
latest commit now seems to handle all test cases (even the nasty ones). There are a few things that do not work properly though:
edit: edit2: |
The pixels omitted by spacing are not part of the segment at all -- they're expected to belong to some other segment. Spacing+Offset lets you set up "interleaved" segments with different FX, eg with (spacing = 1, seg1: offset=0, seg2: offset=1) you can arrange [1 2 1 2 1 2 1 2] where you can set different FX on 1s and 2s. |
Following on by that: I intentionally omitted source grouping and spacing in the getRenderedPixel() sketch. Grouping could perhaps be included, but spacing doesn't make sense -- the pixels skipped over by "spacing" are (meant to be) part of some other segment; including pixels from Segment 2 in a copy of Segment 1 seems wrong to me. In the end it was a judgement call - mirror, reverse, transpose all feel to me like they're "part of" the segment FX rendering; while grouping and spacing feel more like they're part of the rendered FX->output strip mapping. YMMV, though. |
- removed 'copy grouping/spacing' option - added 'Switch axis' instead (in 2D -> 1D copy this chooses x or y axis to be copied) - optimized for code size and speed by not using CRGB (where possible) and not returning struct in `rgb2hsv()`
I have it mostly done now (if there are no objections to latest changes). |
If we're committed to this approach, I think it should be defined as a member of |
added alias function to accept CRGB value. this is more versatile for future uses. also removed some whitespaces
@DedeHai please merge blending styles branch (to a new branch) to see how different blending affects your new effect. |
sorry for messing up the blending styles branch... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not in favour of this PR as in my opinion copying segments (as different as they may be) is not the correct approach. It may also lead to user frustration in the long run.
As the underlying issue is "synchronised" segments (i.e. segments that mimic each other) a correct approach would be to execute effect function original segment uses on the destination segment with parameters (and timers and PRNG) of the original segment.
that would indeed be the best approach BUT: how do you control it? are FX with the same parameters always in sync (if started with the same timebase)? This whole endevour is mostly to sync particle effects, which are frame based (and I have no intention to change that as it makes collisions and speeds way more complex to calculate and therefore a lot slower, the only workaround would be to have fixed frame rates but that poses new issues). |
Consider the complexity of overlapping segments. |
overlapping segments work fine, but you are correct that it is a bit complex i.e. the segment layering has to be done correctly. This is already the case for overlapping segments, adding copy FX on top does not add much to complexity. |
I disagree. Consider the following valid example: Segment 2 will show artifacts from underlying Segment 0 which is unwanted. I would consider this a flaw and would report a bug as I only want to copy Segment 1 and not the underlying segment. If it is to be done, it has to be done correctly or not at all. |
I think the implementation of "copy the rendered buffer including all underlays at that segment layer" is probably the correct behavior. It's true there might be some counterintuitive cases, but it offers a lot of opportunity for clever compositions. With some easy extensions, there are some really cool tricks we could use it for.
As with all powerful features -- like segment overlays themselves! -- there's going to be some sharp edges and cases where it takes a little planning to compose the FX you want to see. Still, I think this feature offers a surprisingly large amount of new capability for a minimum of code. My $0.02, though! |
Yes if that was optional. Now it is not. The solution is to optionally enable individual segment buffer for rendered pixels. So that user has an option if he/she wants underlying segments copied or not.
I do not disagree with that. To answer @DedeHai regarding "effect blending styles": Yes, not all styles will look great with every effect. I particularly dislike "push" variants as the outcome may be weird to say the least. Yet, they are there to be in line with #3669 . Still, the user has an option to select them or not. The code is also largely untested and there may be bugs still present. |
I fully agree with @willmmiles: while there are some limits in the current state of things, this PR adds a lot of value for the average use case.
I see no problem in adding this later when (or if) individual segment buffers are implemented
same goes for the copy FX :) edit: |
- Bonus: saves over 1.2kB of flash -also added a struct to handle HSV with 16bit hue better (including some conversions, can be extended easily) - the functions are optimized for speed and flash use. They are faster and more accurate than what fastled offers (and use much less flash). - replaced colorHStoRGB() with a call to the new hsv2rgb() function, saving even more flash - the 16bit hue calculations result in an almost perfect conversion from RGB to HSV and back, the maximum error was 1/255 in the cases I tested.
just FYI: with the latest commit the code size shrinks by 1.2kB, making it 200bytes smaller in total than vanilla 0.15 :) |
You have started to mix two different things into this PR. |
that is a good point. the changes are kind of 'required' to improve this PR, but keeping optimizations separated is cleaner. Will move it over to the optimization PR and revert the changes here. |
IMO no need as this PR will be merged after the optimisations one. If you keep source files identical no ill should come out of it. |
Added an FX that copies the selected source segment:
note: 1D to 2D and vice versa is not supported