You are browsing as a guest. Sign up (or log in) to start making projects!

Kolori

  • 8 Devlogs
  • 99 Total hours

Create beautiful art using mathematical functions -- domain coloring graph explorer

Ship #1 Changes requested

Kolori is a desktop application that lets you create beautiful pieces of art from mathematical expressions.

#

Kolori features a custom hand-written recursive descent parser to parse mathematical expressions--which it then uses to transpile your input down to low-level shader instructions to render images to the screen on the GPU.

#

Kolori also supports rendering not only images, but animations using the "**t**" variable in the input expression, which grows linearly with **time** elapsed--the speed at which is adjustable.

#

Kolori offers four coloring modes that can generate different images based on your input:
* **HSL**: Uses the HSL color space to generate pixel colors
* **HSLuv**: Uses the HSLuv color space, a perceptually uniform alternative to HSL, to generate pixel colors
* **Custom Palette Generation**: Input 4 colors of your choosing so Kolori can generate a custom color palette based on them, to determine pixel colors
* **Use an Image**: Select an image using your file manager, or just drag and drop one onto the window for Kolori to sample pixel colors from the input image to determine pixel colors.

---

Making this project was a bit of an adventure, as I had never worked with libraries such as ImGui, SDL, and OpenGL before. The biggest challenge when creating the program was managing memory correctly--which makes me appreciate languages like Nim even more--and code organization.

  • 8 devlogs
  • 99h
Try project → See source code →
Open comments for this post

5h 39m 26s logged

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.

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.

Replying to @nikooo

0
0
Open comments for this post

13h 32m 50s logged

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!

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!

Replying to @nikooo

0
1
Open comments for this post

5h 18m 46s logged

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?

Custom palette generation

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.

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?

Custom palette generation

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.

Replying to @nikooo

0
1
Open comments for this post

12h 21m 35s logged

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.

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.

Replying to @nikooo

0
0
Open comments for this post

3h 50m 52s logged

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.)

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.)

Replying to @nikooo

0
0
Open comments for this post

13h 8m 41s logged

(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.

(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.

Replying to @nikooo

0
0
Open comments for this post

32h 48m 5s logged

The project has come a long way (see the 30 hours of work that went into this). We now support:

  • arbitrary function input
  • on-the-fly graphing and error checking
  • a “t” time variable, allowing you to animate graphs
  • being able to speed up, slow down, and pause animations
  • screenshots (not imaged below)
  • zoom in/out
  • panning

As well as being multiplatform for both Linux and Windows (you can go build and run this, right now).

What to expect:

  • the ability to change coloring method (hsl, hsluv, palette generation)
  • the ability to use an image for coloring
  • WASM web builds
  • coordinate inspector to see where exactly in the complex plane you are

Check it out on github: https://github.com/neroist/kolori

The project has come a long way (see the 30 hours of work that went into this). We now support:

  • arbitrary function input
  • on-the-fly graphing and error checking
  • a “t” time variable, allowing you to animate graphs
  • being able to speed up, slow down, and pause animations
  • screenshots (not imaged below)
  • zoom in/out
  • panning

As well as being multiplatform for both Linux and Windows (you can go build and run this, right now).

What to expect:

  • the ability to change coloring method (hsl, hsluv, palette generation)
  • the ability to use an image for coloring
  • WASM web builds
  • coordinate inspector to see where exactly in the complex plane you are

Check it out on github: https://github.com/neroist/kolori

Replying to @nikooo

0
2
Open comments for this post

12h 49m 3s logged

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.

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.

Replying to @nikooo

0
2

Followers

Loading…