Kolori
- 11 Devlogs
- 142 Total hours
Create beautiful art using mathematical functions -- domain coloring graph explorer
Create beautiful art using mathematical functions -- domain coloring graph explorer
Managed to get everything working <3 The main issue was not assigning texture units to the sampler uniforms whenever the shaders were rebuilt 🤦. Either way, the feature works fully now.
The rest that’s left before making a release is chasing some bugs that are left: e.g. crashes on Windows on startup and access violation errors when making a screenshot on fullscreen (also specifically on Windows).
Below is the Joukowski function, as I briefly mentioned before in the prior devlog.
After much, much longer than I’d like to admit, I finally got “sliced” coloring to work! Naturally we need a better name, but that can wait.
So, for Kolori, we use OpenGL to render scenes on the GPU. Specifically, we target at minimum desktop OpenGL 3.3, and the GLSL shaders are written for GL ES 3.0 (we plan to target WebGL in the future, maybe moving to WebGPU later. Maybe.). We implement the “slices” coloring mode as follows:
Though, I did still do that inadvertently. Slice colors are managed by color edit widgets. Each widget’s ID (which must be unique) is generated using the index of the slice color it controls, casted to a byte. Thus, the number of colors cannot exceed 256. Something I hope to fix in the future.
Also, an interesting note: GLSL for GLES 3.0 doesn’t support the
sampler1Dtype (which literally represents a 1D texture), so one would have to use the type that represents a 2D texture, but set the height of the texture to 1, to emulate the same behavior and structure.
This means that there are now two textures we handle in the shaders: the image texture that holds a user-uploaded image for the “Use an Image” coloring mode, and the slices texture that holds the selected colors for the “(Pizza) Slices” coloring mode.
The major obstacle that was ran into was texture data mix-up: the data for the image texture would always overwrite the data for the slices texture, and it would even seem that the two textures were pointing to the same data, even though I assigned separate texture units to each one, and made sure to set the active texture each time before binding textures.
See the demo below :)
They don’t really start to look like “pizza slices” until you have 5+ slices. There are some really interesting graphs you can create using this, such as the graph of the Joukowski function, defined as: f(z)=(z+1/z)/2 (unfortunately not pictured below).
Mainly, two new features/fixes:
.deb files in releases in the future aswell)In past releases, only an executable was published for Linux platforms. This is not a proper solution for distribution–different Linux systems may differ in ABI, available shared libraries, and packages. For instance, on WSL in my Windows machine, SDL3 is not available in the default repository, leaving me unable to build or even run the application. AppImage builds bundle the executable and everything needed for running the application into one file–perfect for distributing. “One app = one file”, or whatever those nerds say.
To run on a Linux system, users will only need to do the following:
.AppImage file executable permissionsBefore, when you put in an image, it would be squished down into a square, distorting the image along its longest dimension. For images with a 1:1 aspect ratio, this is fine. For other images? Not so fine. We now apply proper scaling in the vertex shader pass to textures so that image aspect ratio is preserved in the canvas.
Expect a new coloring mode: “Slices”.
This will split the canvas into a number of “slices”–similar to how the four quadrants of the cartesian plane split it up into four slices itself
I’ve finally added image coloring to Kolori! Kolori can now take an image as input, by either selecting a image file using a native file dialog or dragging and dropping a image onto the window! The image below was created using the feature–using one of the gallery images as the input image.
The hard part about this was, not actually loading the image file, reading the pixel data, uploading that into the GPU, and rendering it, but instead managing the file dialog by which a user can upload an image into the application.
We use SDL3 for windowing and make use of the SDL_ShowOpenFileDialog function to, well, show an open file dialog. I initially put all of the image loading and OpenGL calls into the dialog callback, but something strange happened then. All of my OpenGL calls were triggering an GL_INVALID_OPERATION error–and specifically on Windows–but why? All of my OpenGL calls were fine, no invalid arguments passed in (or in the wrong order), so everything should’ve been fine.
The answer lies in the SDL_ShowOpenFileDialog and how OpenGL works with multiple threads. SDL_ShowOpenFileDialog is an asynchronous function–it will return immediately, and the result of the dialog will be passed on to the callback. However, the callback function may be called on a different thread depending on the operating system, which explains the difference in effect on Windows, and the GL_INVALID_OPERATION errors were caused by the absence of an active OpenGL context on the new thread.
SDL3 does not provide an API for setting the current thread’s OpenGL context, so I had to cope. I moved most of the business logic out of the callback function, only keeping the image loading logic, and instead performed the texture uploading into the UI rendering logic, where we check if there is any new image data that happened to be uploaded, upload the data onto the GPU as a texture and show it on screen.
Bumped version from 0.1.0 to 0.2.0! See it here: https://github.com/neroist/kolori/releases/tag/0.2.0. Some interesting changes are that there is now a VSync toggle + a frame rate slider with support for unlimited frame rate (as much as your GPU can handle), build system improvements–so that Kolori is one make release away on all platforms that have make, and the UI is now DPI-aware and will scale based on the monitor’s DPI (we do not change scaling however if the monitor were to change–something to fix in a future release).
We also changed our color input sliders to display RGB colors in their integral components from 0 to 255–what you would expect, rather than a decimal display. See it below!
Wrote a full-length devlog about a core feature of Kolori and its implementation, but it got deleted for being “too long”. Lets try again then, with a piece that only demands a bit of your time.
Kolori offers users three (at the moment) coloring modes: HSL, HSLuv, and custom palette generation. How do these work?
Unlike HSL and HSLuv, using a custom palette to color the complex plane is much more simple. Kolori lets the user input four arbitrary RGB colors. We then use Inigo Quilez’s cosine-based formula for procedural palette
generation to determine the color of a pixel. An article of his that describes it is here: https://iquilezles.org/articles/palettes/.
The shader code that does it all:
// From https://iquilezles.org/articles/palettes/
vec4 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d)
{
return vec4(a + b*cos(TAU * (c*t+d)), 1);
}
Our a, b, c, and d are the colors the user has selected, and t is an input variable to vary. However, one would notice that t is a scalar, while we work in the complex numbers. How would we map the plane onto the real line in order to get a valid t to input into palette? The answer we chose is simple: we take the projection onto the real axis–i.e. just taking the real part of the complex number This is an answer that a user would expect and is pointed out in the user guide. Furthermore, by not choosing ahead of time for the user, we allow the user to exert more control over what they wish to generate.
Was preparing for first our ship, but then discovered that my application was leaking memory–badly. Memory usage would climb by a gigabyte in <2 minutes. Worse than any browser you’ve ever seen.
To start, I use Odin, which has a tracking allocator that lets you see where you didn’t free up memory, or perhaps made bad frees. On program exit, it prints out how many bytes you leaked, and where you made the allocations. I had been using this allocator, and it had not reported anything.
Furthermore, upon, admittedly, a very cursory look into my code, I couldn’t find anything that would’ve caused any leaks either–all allocations were eventually freed: whether that be manually or via Odin’s temporary allocator (a really nice feature of the language). LSan hadn’t pointed out anything, and Valgrind didn’t report anything scary, only ~200KiB of memory usage. I had even patched myself a custom allocator, which would print to stdout whenever an allocation was made with it, and I saw nothing concerning at all. Only small allocations were made which were, again, eventually freed.
My application uses SDL3 for windowing and ImGui for GUI, so perhaps one of those has a memory leak in it? I’m not sure. For such a large library, I doubt that’s the case. Though, when running valgrind with --leak-check=full --show-leak-kinds=all I did keep seeing SDL pop up, but it didn’t report any outright leaks.
Upon researching potential external sources for this memory leak, I found https://github.com/libsdl-org/SDL/issues/15192, which describes my issue exactly. It seems that there is no memory leak in SDL, ImGui, or whatever I’m doing, but instead a driver issue, which is interesting. The example that was given to reproduce the leak is quite minimal, so I wonder how that got past their testing. Furthermore, if this is indeed a driver-level issue, why is it that no other applications running on this device leak memory this egregiously? In any case, the leak is not my fault, and out of my hands.
Learned how cool rubberducking is. I was having a problem fixing a bug with the parser (as described below), and I went to write a devlog about it. As I was describing the bug, I immediately had an idea about how to fix it, and lo and behold, it worked. The original post was going to be as follows:
Fixed a bug where trailing input after the parser successfully completes is not disallowed. This most commonly occurs, as I’ve found, after the use of a comma character. The parser simply does not see it, and instead only parses the expression it sees before it.
Internally, a comma character is used to delimit function arguments, so this kind of behavior is not totally useless. However, for situations like picrel, it just seems strange. doesn’t it?
This usually would be an easy bug to fix. We currently use a recursive descent parser (considering moving to a pratt parser) for parsing mathematical expression. All that honestly would’ve needed to be done was adding a check at the procedure that parses the top level grammar rule: “OK, are we at the end of our input? We should be. If not, return an error.”
(Here, I realized that if we literally call this procedure while parsing function calls, it really isn’t too wise to immediately ask “are we done yet?” if we very much aren’t done yet. I then added the check into a procedure that isn’t called by the parser and we should definitely be done by the time that check is ran.)
(excuse the amount of time, i’m doing general refactors + debugging as well as working on other features)
Implemented adjusting the gamma correction amount. Normally, with the gamma correction, areas close to zero would appear black, and areas that have large magnitudes would appear white. However, you might not always want this, and instead would prefer only pure hue.
Set the gamma correction amount to zero to reduce the amount that colors’ lightness values are altered. An example is shown where we use this feature to produce a colorful, moving spiral. Note that when gamma correction is not zero, as demonstrated, the effect does not work as well.
The project has come a long way (see the 30 hours of work that went into this). We now support:
As well as being multiplatform for both Linux and Windows (you can go build and run this, right now).
What to expect:
Check it out on github: https://github.com/neroist/kolori
Finally! SDL3 & the GPU shaders are working.
FWIW – this is the graph of f(z)=z, the identity transformation.
I will make a devlog on how these images are generated later.