Progress Update #27
Editing Linear Color Curves in Game
This sprint was focused on creating the ability to change and edit color gradients in-game. Before I started I searched the internet to see if anyone had already done this before but to no avail; I had to create a solution completely from scratch. (Note: I will use the words 'gradient' and 'linear color curve' interchangeably, they describe the same thing)
The Idea
The essence of what I wanted to create was a replica of the in-engine color curve editor. This is a rectangle that renders the gradient and a space above it that has smaller elements (which I dubbed "keys") which are positioned at the location of the color and have a small space to show what color is being used.
e.g.
The Widgets
To replicate this I needed two widgets, one for the gradient, and one to use as a template for the keys. The gradient itself is an image on top of an invisible button because at the time I did not know that images had an "On Mouse Button Down" event. Live and learn I guess. Anyway, the keys are a custom image of two parts: one of the key and one of the colored part. Then above the key is an info panel that is toggled then the key is selected. This panel has a color wheel to change the color, a spin box to adjust the 'time' between 0-1, and a button to remove the key from the gradient.
It was at this point I realized that much of the functionality of the widgets would only be available to C++. I then found an answerhub thread (warning, the thread kind of devolves into people bickering and is limited in the actual information that it puts forth. This is a better resource for learning) that mentioned the basics of making a C++ widget. I also found that you need to add the meta UPROPERTY tag "BindWidget" to be able to edit widget components in the UMG editor. Once all that was set up it was time to get some functionality.
Functionality
First I listed the features I wanted. These were:
- Adding keys
- Removing keys
- Changing the color of keys
- Changing the time of keys
- Construct the widget with the keys that already exist on the gradient
Looking at the source code for UCurveLinearColor I could see that it is comprised of 4 FRichCurve, one for each color channel (red, green, blue, alpha in that order). I could also see that FRichCurve has public (but not blueprint callable) functions for all of what I wanted to do: AddKey(), DeleteKey(), UpdateOrAddKey(), SetKeyTime(), and SetKeyValue().
I started off trying to add keys to the curve. To do this I made a function AddKey() in the C++ widget class which took as parameters a time from 0-1 along the curve and a color that defaults to white.
Two things to note are that 1) line 14 lerps between 0-512 because that is the x size of the gradient image and 2) the FKeyHandle that is used when adding a key is the same for each float curve.
Once I had that going I went to get the location of the click in order to pass the correct time to this function. I followed this thread to get the local canvas position of the click and ended up with this as the logic that adds a key:
Removing keys, or changing their color or time are pretty self explanatory:
With all that done I went on to constructing the preexisting keys on the gradient. This took a stupid amount of time to figure out so I'll cut out much of the failures. The crux of the issue was to do with the handles. When I was adding a key you may remember I was setting the handle for all of the curves to point to the same handle, but by default all of the float curves (RGBA) have different handles and a curve only has handles when they change. So in my test curve (red to blue) the red and blue curves have two handles but the green curve only has one. My solution for this was as follows:
Lines 16-25 we get all the unique keys (unique because I was getting duplicates for some reason) and add them to an array Keys. Then we sort that array by time high to low and RGB low to high (this is so all keys of the same time will always be ordered by red value then blue then green then alpha). Shoutout to this stack overflow thread for describing the method of sorting by multiple variables. Line 28 describes a template key with one FKeyInfo for each color channel. The loop starting on line 30 goes through Keys making a key for every unique time value that exists in that array. The reason that it needs to happen this way is so that when I call ConstructKey(), the handles are all deleted and set to the same handle or a handle is created if one does not already exist at that time value.
I really don't know if this is the best way of doing this, but it is what I was able to get working with two days of the sprint remaining. At this point though I had to move on to implementing this in-game.
The Implementation
The implementation of this widget was more involved than I would have expected. I essentially had to replicate what I did with the noise layers; making a combo box string that would populate with options to edit the current gradient, make a completely new one, or chose any of the already existing gradients.
Once that was done, the very last thing I had to do was to make the changes to the gradient effect the object they are applied to. There are currently four places where gradients are used: ocean color for terrestrial planets, biome color for terrestrial planets, color for the surface of gas giants, and color for ring system components. Because I didn't have much time left at this point the solution that I threw together was rushed and I don't like the way it turned out. Like it works fine, but it breaks some 'best practices'.
The way that it works is that the color curve widget has a UObject variable 'ObjectToUpdate' and an enum ColorCurveType which has all of the current possible uses as stated in the last paragraph. Then when the widget gets a call that the gradient has been updated it calls a function on the ObjectToUpdate based on the color curve type. So if the color curve type is biome or ocean then we assume that the ObjectToUpdate is a planet and we can call OnColorSettingsUpdated(). If the ColorCurveType is RingSystem then we assume that Object to update is a ring system component and we call SetGradient() on that etc.
I don't like this solution because it requires the object to be passed all the way down through each widget and it means we have to assume type based on an enum that could get incorrectly set. In any case, it'll work for now. I still need to find a way to save new gradients and changes to existing gradients in a packaged game though.
Comments
Post a Comment