A high-performance, real-time visualizer for complex-valued functions f : C → C in 2D and 3D.
A high-performance, real-time visualizer for complex-valued functions f : C → C in 2D and 3D.
This is a complex function plotter implemented in C++ and OpenGL, ported to the web via Web Assembly and WebGL! It is implemented from scratch, with no outside dependencies, and has full support for every elementary function, and some non-elementary ones (Riemann-Zeta, Gamma and factorial). It provides supports to analytic differentiation, 3D plots with user-defined height maps, animations, and everything that might come in handy for someone working with complex analysis. It is essentially Desmos, but for complex functions!
This short re-ship’s purpose is mainly solving some issues that people pointed out, which would leave the project ultimately incomplete had they not been fixed. It was a very fun project to develop, that now, hopefully for real, I can finally put to rest!
If you don’t know the math behind this, and would like to understand it, please check out this documentation I prepared: https://github.com/Sekqies/complex-plotter/blob/main/docs/the-basics.md
Some other useful documentation I have written for this project
That’s it for now, folks! Happy plotting!
We got quality of life improvements, and better 3D function plotting!
In my latest ship, I got many comments about two problems:
In developing this, I made the UI adapt to the user’s Device Pixel Ratio (DPR), which is essentially how many real pixels in your screen there is per ‘logical’ pixel (which is what programs will use to do their math). My laptop has a DPR of 2, meaning that the UI looks small to medium sized. Most users, however, have a DPR of 1, meaning the UI gets twice as large. Fixing this was a matter of making the UI fixed-size, and allow it to be resized manually by the user, to fit their preferences.
The long loading times were due to the fact I was preallocating 8 threads to run in Arbitrary Precision Mode, and not allowing the browser to be ‘flexible’ with it. Formerly, if the browser didn’t have space to allocate those threads, the program would just crash. Now, it can move on with as low as one thread (which will make arbitrary precision slower, but it’s better than not loading at all).
Also, since loading times are inevitable, I added a loading screen.
Finally, I thought it’d be good to give 3D rendering mode a few touches. First, to switch the camera view from free (like floating in a video game) to orbiting (meaning it rotates around the render). Second, I hooked up the third dimension to be things other than |f(z)|, and instead be user-defined.
Attached, what’s new!
Commits
0d926ef: Loading screen
2f51b58: Scaling issue fix
eb6e22c: Camera quality of life changes
beddb08: Reduce web build size for faster loading time
Issues
Issue #54: Project doesn’t load in Edge, and in older devices
Log in to leave a comment
I made a high-performance, arbitrary precision complex function plotter in OpenGL and C++! It was implemented from scratch, and contains no outside dependencies. It is like Desmos or Geogebra, but for complex functions!
It provides support to every elementary function, and a select few of non-elementary ones (Riemann-Zeta function, Gamma function, factorial). It supports animations, 3D rendering, and, most recently, arbitrary precision for “infinite” zooming. It runs in the browser through Web Assembly and WebGL as well as the desktop.
This is a passion project I first tried to implement four years ago, but never succeeded. Now, it’s up! I can’t put into words how happy I am to know that I’ve finally pulled finished implementing this. Arbitrary Precision Rendering, in particular, was something I thought was impossible to implement in a project of this scale, but I was (thankfully), proved wrong.
If you want to understand the math behind it, I wrote some documentation, with animations used Manim here
The C++ and Linux binaries are available here
And, if you want to see how this benchmarks against other similar tools, and see optimizations, check here
Enjoy, and happy rendering!
The complex function plotter is done!
A little bit of personal lore here: I made another complex function plotter four years ago, when I was still in highschool. It was clunky, but worked fine enough. Still, I felt it missed some things, which led me to work on this project.
Now, I have accomplished all things I have set out to do. Bugfixes aside, I believe I can put this project to rest now.
First, I worked on making the high precision plots reactive. They take a while to render, so having them shut down the main thread while the user waits on a blank screen isn’t very fun. Now, each thread updates the canvas for every pixel they render. This allows the user to see the progress being made!
Also, some reconfigurations were needed in the web version, since the browser needs special permissions to run Web Workers (the equivalent to threads for browsers)
Then, some quality of life changes: I added an amortization factor so the graph takes longer to converge to white (and black), and fixed a parsing mistake one of my beta testers caught.
Finally, documentation and benchmarking! I could always tell this tool was fast, but never how fast. I took upon myself to benchmark my own tool, and some other project’s (since they are opensource, I can manually modify the code to do so). The result were great: this project outperforms everything else by a factor of 50.
And that’s it for now folks!
Attached, our high precision plotting in real time!
Commits
9dd1515: Async image generation
4266aaa: Add threading for web version
ded05c1: Update documentation and perform benchmarking
2e32e99: Fixed issue where expressions with negations would be evaluated incorrectly
2e10128: Fixed issue where moving the screen would cause the high precision plot to shift as well.
Log in to leave a comment
ARBITRARY PRECISION IS UPON US!
Keeping things short, I tried, and failed, to implement my own GPU, GLSL arbitrary precision math library. Why? Because nobody else has! And for good reason.
(add(x,y) -> z). This causes the GPU to run out of registers, because returning arrays means allocating temporary memory(add(x,y,z) -> void). This worked at first, but for minimally nested functions (like cos(sin(z))), the GPU tries, and fails, to unroll the loops. So the shader doesn’t compileAt this point, I realized that true arbitrary precision is impossible in the GPU. Thankfully, GLSL translates pretty neatly to C++, so I could just, at build step, transform my complex functions into C++, and do all my arbitrary precision in the CPU!
It is very slow, but it’s meant for very high detail images. So, completely fine! Attached, some plots!
Commits
2895bc0: UHPM renders, but not properly
6d39bca: UHPM works, but not for big expressions
5d96733: High precision zooming
43e4423: GPU to CPU transpiling
f262c96: CPU rendering!
Issues
#Issue #40: Arbitrary precision in the CPU
Issue #41: GLSL to C++ Transpiler
-#48: Synchronizing GLSL and C++ functions
-#49: Running shaders in the CPU
Issue #50: Reoptimize UHPM
-#51: Rewrite math library to use inout
-#52: Expanding nested expressions in transpiler #52
-#53: Using registers in transpiler #53
Log in to leave a comment
HA! This is technically not a 10 hour devlog.
My general devlogging philosophy is to devlog per feature, not per day of work. What I consider to be “done” is, generally, something that produces a visual change. Naturally, some features are more complex than others, and will therefore take more time. This is one of those features.
Two months ago, when I wrote Issue #16, I wanted to add a way to render plots with arbitrary precision. This is a complicated step that first involves writing an arbitrary precision library, with every elementary function, in GLSL (with all of the limitations the language has, like no dynamic memory allocation, strict limits to 32 bit registers, no carry bit, etc, etc), then translating all of my existing complex functions to use these high-precision equivalents, then writing another high precision library for C++ so then, we can maybe start trying to plot things.
I can’t bring myself to bruteforce most of these things, so this means a lot of parser writing to transpile lowp GLSL to highp GLSL.
I got about 80% of that done. This means no actual high precision plots yet! (As this involves messing with UI, and other complicated components that would far overshot three hours).
The good news is that I got copy and paste to work!
Again, I apologize for the long devlog, and lack of any features. Attached, the copy and pasting working, and my local library transpiled to work with higher precision!
Commits
1897b06, 0064d3e, 217e5f6, e6e820c: Four operations, trigonometric, exponential, logarithm, power, square root, hyperbolic GLSL-specific functions implementations
c0c161f: Transpiling to GLSL
0bec9d9,de15c3a: High precision in C++
[27832a8,20817d1,10af13c,65b657a]: Sending and creating highp shaders
Issues
Issues #16, #38, #39, #42, #43, #44, #46: Ultra High Precision Mode
Log in to leave a comment
These seven hours of devlogging time are mostly due to three things: partial derivatives, non-elementary functions and getting this project to work on the web. As you’ll see, we got two out of three on this one!
First, our tool supports derivatives relative to z (denoted as d/dz). The rules for it are simple: anything that isn’t dependent on z is 0 and otherwise we apply some rules according to a table.
The problem is that z = x + iy, and each variable x,y is partially dependent on z. They can’t be equal to 0, since d/dz(x + iy) = d/dz(z) != d/dz(0). To fix this, we set some “sanity” rules, and set d/dz(x) = 0.5 and d/dz(y) = -0.5i. This fixes our problem!
Second, some users requested non-elementary functions (that is, functions we can only ever approximate, since their definitions are infinite). A good time was spent finding good, fast approximations, and most importantly fixing their ‘explosions’ to infinite, which at times resulted in their values becoming NaN. This is partially solved by first evaluating the function’s logarithm (which grows far, far slower), and exponentiating it back for its regular value.
Third, the web thing. I wanted to get two things working:
Attached, some plots of our new functions!
Commits
3f015c0: Add exporting plots as images
f3271a2: Added popup when exporting
7255bf4: Implemented the zeta and gamma function
Log in to leave a comment
A month later, we’re in the world wide web!
From the beginning of the project, I kept telling myself that I would, eventually, port it to Web Assembly. Many architectural choices were made with this in mind (not using compute shaders, sticking to version 330 features, using ImGUI for the frontend, not using a file system, etc).
I just knew that this would be sort of a headache, and kept delaying it, but it comes a time at our lives where we have to accept that not everyone wants to download a sketchy binary from github. So we had to port it to web assembly!
For those not familiar with the tech, WebAssembly (wasm, for short), is essentially the low-level language of the web. The advantage is that it’s faster than javascript, and many languages can compile down to it - and that includes C++!
Our graphics core is entirely made in OpenGL, but thankfully, a tool named Emscripten also handles this conversion by turning OpenGL into WebGL (the web equivalent). So, we just had to rewrite our code to handle things specific to the web.
Our main change was the textures that we were using to send data to our shaders. Originally, this was done through samplerBuffers, which don’t exist in the version OpenGL runs. This made us have to change our data to 2D textures, sampler2D. This requires a little more math to get data from the texture, but it’s small enough to be a change applied both to the desktop and web version (to avoid refactoring headache).
The rest is changing the main loop logic (to be step-based), the event callbacks, and creeating a basic UI. Attached, our code running in the website!
Take a look for yourself in: https://sekqies.github.io/complex-plotter/
Commits
04e5437: Working web assembly build - ImGUI not working
181a62c: Finished web assembly porting
5ea3105: Add web deploy
…and 7 other commits fixing github actions
Log in to leave a comment
🔥 AVD marked your project as well cooked! As a prize for your nicely cooked project, look out for a bonus prize in the mail :)
I built a high performance complex function plotter in C++ and OpenGL! It is a general purpose plotter (like Desmos or Geograbra), but exclusively for complex functions. It is able to plot both in 2D and 3D any elementary function, compute derivatives, create animations, show spatial warping and so much more, all with consistent >100 FPS in modern GPUs. It’s a swiss army knife for complex analysis!
The project’s documentation goes into depth on what it does, how it does that, and the math behind it, with a bunch of images and animations for visual learners like me! I strongly recommend reading it, if you intend to understand what is going on.
I take great pride on the fact that this project was made almost entirely from scratch. All the complex function definitions, rendering logic, parsing, symbolic differentiation, interpreting, compiling, etc are specific to this project, rather than relying on external dependencies. And, it does all that with very, very high performance!
I’m also very happy with how the documentation worked out. Seriously, it took a lot of work, so take a look at it! It will help you understand what all the pretty colors in your screen mean.
Working around the GPU’s abstraction limitations was a hassle (limited precision, no conditional branching, expensive interactions with the CPU), but was ultimately worth it for the performance. Also, getting to go back and re-learn all the complex analysis I had long forgotten, and actually applying it to the project, was loads of fun!
I hope you like using this tool as much as I enjoyed developing it!
Best,
-Sekqies
This is, by far, the devlog with the most hours registered this project has ever had. So, what new, cool thing did I implement? Nothing! This time was almost entirely dedicated writing documentation (my kryptonite), and preparing the project to be released.
One of the main challenges I knew I’d have to face when starting this project was explaining it to other people. The math behind it is quite complex (get it?), so I knew that, eventually, I’d have to write some sort of educational guide to tell people what it does, and how to use it. More than nine hours later, we’re done!
I figured most people learn better visualy, so I poured some of my time to learn how to use Manim, the animation software 3blue1brown uses for his videos. I made about a dozen animations with it, and wrote a guide to introduce a laymen into complex analysis and function plotting.
Additionally, I also documented how the tool works internally (basically doing the same thing that I already did with the devlogs, but to more depth), and its features.
This, plus an install guide, getting a bunch of screenshots and videos from the plotter to put in README, makes us have completed our project’s documentation!
A small portion of this time was also dedicated to making the project build to a single executable, for it to have a favicon, and other small quality-of-life changes. Next we’ll bugfix, and this project will be ready to ship!
Attached, some of the animations!
Related Commits
Commit a160c09: Finished basic settings for deployment
Commit fae6ce0: Added grid warping
Commit 42ee549: Finished the-basics documentation
Commit 5bb3f81: Added advanced.md and assets
Commit 6a5f185: Added images to features.md, started README.md
Commit 30b3560: Finished readme.md
Log in to leave a comment
We’ve got a usable UI, and the good stuff that comes necessary for any good math program, now!
First, let’s get the developer stuff out of the way. Before, if we wanted to add a new function, we’d have to write code in 6 distinct spots, as discussed in Issue #31, a consequence of my #ifndef clauses black magic. This, now, is all done in load time,making my life far easier.
Now, for the math stuff. I’ll make a rare use of bullet points, I:
Our most important addition is, by far, hovering to show a function’s value. All our functions are calculated on shaders, so we’d either have to hard code a C++ counterpart for ever GLSL function, or find a way to send data from the shader to the main program.
The problem is, while it’s easy to send data to shaders (through uniform variables), it’s quite difficult to do the opposite. And even if there was an easy way to do this, remember: our fragment shader runs for every pixel, so they all would be sending data to the CPU at the same time, which would be far too expensive.
Instead, what we do is create a 1x1 pixel shader, and send it a copy of our function, plus the mouse coordinates. It then shares a single vec4 variable that stores both z (the input) and f(z) (the output). Voilá, we can get things from the shader, now!
Drawing the grid is just some additional math in the fragment shader, and the UI changes are, well, UI.
Related Commits
Commit 1d123a6: Automated interpreter shader creation
Commit ba3f299: UI and constant folder overhaul
Commit 5893ae0: Inspector done!
Commit 1ff791c: Added grid
Related Issues
Issue #31
Log in to leave a comment
Unfortunately, it’s time to do our laundry.
With 3D rendering complete, all of the essential features of the plotter are done. This means that, had we wanted to, we could ship this as a fully-fledged math engine and leave the job of actually turning it into an application to someone else. But we can’t be doing that! Therefore, it’s time for us to start making way to shipping the project.
First of all, I wanted this plotter to be complete, meaning that it implements every elementary function (those being, in simple terms, a set of well-behaved functions that mathematicians use). This is simply a manner of writing the already existing real and imaginary components of these functions (and their derivatives), which are well-known and defined.
Is this essentially just writing boilerplate? Yes! Thankfully, this is not my first rodeo implementing these functions, so I could port a good amount of code from an old project. This is all done now: all elementary functions and their derivatives have been implemented!
Now, for the engine-specific things: so far we have been writing shaders into files and reading them at runtime. This works, but adds unnecesary file I/O operations, and makes it impossible to port our entire project as one .exe file. So we had to remove that!
One way to do this would be by writing our GLSL code in strings, but this removes linting. Instead, I implemented a dynamic source string builder at build time.
Attached, the new functions!
Related Commits
Commit 7923ee3: File conversion to strings at build-time
Commit 5ba94c0: Complete removal of file reading logic
Commit 0a56357: Finished implementing elementary functions
Commit 48d08fc: Derivatives of elementary functions
Related Issues
Issue #30: Switch from shader files to strings
Log in to leave a comment
We can do things in 3D now!
This might make you ask what are we plotting in this additional dimension, and the answer is simple: nothing that we weren’t showing already
In our plotter, we use brightness to represent magnitude (how large a number is). Black corresponds to zero, and white corresponds to a very high number, making roots of functions look like “sinks”. It turns out that it’s far easier to visualize these “sinks”, “valleys” and “peaks” with the help of a third dimension (akin to looking at a satellite picture of a mountain range vs being there).
There’s many ways to render functions in 3D, but the one I chose is by laying out a large 256x256 grid of “mini-planes” that are molded to look like the function (there are methods with ‘infinite’ precision but they are horribly slow and, if we want more precision, we can just add more segments, anyway!). This required me to create a Mesh struct and functions to handle their creation and use.
Also, since all this logic operates on vertices, we have to move the 3D code into a vertex shader. This required massive refactoring of pretty much all of our preprocessing functions, since they were all rigged to work with the fragment shader. We also had to do more dependency injection trickery to make sure the 2D and 3D shaders remain synchronized (of course).
Finally, we had to create a custom camera to actually move around our 3D plot, and update our state variables to see if the plot is 3D.
Attached, the third dimension! :O
Related Issues:
Issue #29: 3D Plotting
Related Commits:
Commit 4a0ab12: Fragment and vertex shader for 3D
Commit 0a17f71: Refactor of preprocessor logic
Commit 614238a: Created mesh grid
Commit 4bfd2ed: 3D Rendering done
Commit 0a17f71: Working camera!
Log in to leave a comment
We can take derivatives of functions now!
For those not familiar with the term, a derivative is a higher-order function (meaning it takes a function as a parameter, and outputs another function). This is not a feature natively supported by our shader’s language (GLSL), so we have to do all the higher-order logic ourselves in the parser. Which makes sense: if d(z^2) = 2z, and the user types d(z^2), we can just send 2z to the shader.
Our stack is great for evaluating functions numerically, but not optimal for symbolic manipulation. An Abstract Syntax Tree, or AST for short, works far better for this, because it simulates the “recursive” nature of expressions. So, we had to convert our RPN queue to an AST, and then back to RPN. Some time put into that!
Now, there is a generic way to calculate any derivative, but it would require us to work with infinitesimally small quantities, which just do not exist in computers. So, if we tried to use it, we’d get a rough approximation at best, and a very innacurate result at worst. This, however, can be fixed with a derivative table, falling back to this numeric method whenever needed
All this higher-order tree manipulation is of intermediate difficult when using raw pointers (int*) but quickly becomes hell when using C++’s smart pointers (std::unique_ptr). Safe to say, this difficulty is what caused most of these dev hours.
Attached, the new stuff!
Related Issues:
Issue #8: “Computing higher order functions”, and it’s sub-issues:
* Issue #9: Stack into syntax tree
* Issue #10: Syntax tree into RPN stack
* Issue #11: Differentiation
* Issue #12: Operator rules
* Issue #13: Numerical differentiation
* Issue #14: Analytic differentiation
Related Commits:
Commit b56b9fa: ast -> stack, stack -> ast
Commit f410dba: Finished implementing derivatives
Log in to leave a comment
We’ve got got compiled shaders up and running!
All the benefits and reasoning behind doing this were throughouly discussed in Issue #23. What matters is now we have our interpreted shader as a “preview”, and whenever we want improved performance, it’s just a matter of hitting “enter” and we’re done!
First, we had to convert our stack of opcodes into a GLSL string. After the type overhaul, this was simple enough of a task.
Since I’m dead-set on having a semi-decent developer experience while coding with my fragment shader (shader/plotter.frag), I had to do some string manipulation trickery to guarantee that all important function and coloring definitions are shared between shader modes.
This in turn required me to modify our existing Shader class dependency to accept strings rather than file paths to compile, and to create a new CompilerShader class to handle all the aforementioned string manipulation. There are some optimizations still there to be made to increase compile time (namely ommiting function definitions we’re not using), but this is not necessary for the time being, as compile times are low.
Also, since compile times were shown to be tiny, the async proposal related to Issue #28 might not be necessary.
This also involved changing our global function state and callback functions to take a reference to different shaders, so we can change them on the fly.
That’s all for this devlog. Attached, a video of me rendering a huge function with the interpreter vs compiler: the performance difference is palpable!
Related commits:
Commit 5f38f07: Converting stack into glsl infix string
Commit 8f40852: CompilerShader class done
Commit 48d3fed: Added shader switching for compilation
Related issues
Issue #28 and its subissues.
Log in to leave a comment
Longest time without a devlog yet! So we’ve implemented something amazing and new, right? We’ll get a new nice feature, right??? WRONG! This coding session was mostly concerning architectural details for the project, so we don’t have any new “features” per se, that is, besides developer experience and documentation.
First, the non-code part: performance and precision. We’ve been using a stack that’s dynamically interpreted at runtime for our shaders so far, which obviously hinders performance when compared to a compiled string. This is exceptionally true for very large expressions, like computing z^327manually, which makes the interpreted version unusable.
I tried many different precision techniques in the meantime (these demos are not documented). Some of them, like double-double or emulated double (two floats for one double) didn’t work at all at medium performance cost. Others, like using the native double type gave awesome precision, at the cost of the program running about 40x slower. This made me conclude that, if we want high precision, we lose performance. And we can’t have that added to the performance loss of the interpreter.
Now, since we wanted to implement a compiler, I’d have to convert TokenOperators into GLSL strings that represents each function. I started writing a to_glsl_string function, but suddenly a wave of clarity crashed down on me and I asked myself what the hell I was doing. At that point of that project, every time I wanted to add a new function, I’d have to modify code at 6 different spots! 6!!!!
Hence, we had to stop our math fest and do some refactoring. I hope you can forgive me.
Related Commits
Commit 1ed13ea: Overhaul of type system
Related Issues:
Issue #23: Add compiled shaders (Goes into more detail - highly recommended read!)
Log in to leave a comment
We can write functions in the program now! Before, we had a hardcoded string that was directly put into parser::parse(), which required a recompilation every time we wanted to see a new function. Of course, we can’t be having that!
You might think that this is a simple feature to implement (just read a textbox in your render loop, right?), but it is most certainly not! Why? Because, so far, we were assuming the strings sent to our parser are well-formatted. So that means we were expecting strings that:
2 * 3 +)2.3324.2 * 3)z * skibidi * dopdopdop))z*(z)Implementing the UI itself was more complicated than i thought and introduced an entirely new dependency: ImGui. I could use the user’s operating system’s native components for the GUI, but that would make it impossible to port the project to Web Assembly as is. So yeah, there’s that. It’s not the prettiest so far, but it works! Attached, there is me trying out this new feature.
Related commits:
Commit d30ec7c: Added text validation, added ImGui support
Commit d37dd49: Text input working, but small
Commit f7da8cd
Related issues:
Issue #18: Inputting functions at runtime
Log in to leave a comment
A careful eye might have caught on that in our old code, we were setting a uniform for u_range, u_resolutionand u_range by hand once before the rendering loop. Why? Well, because I hadn’t bothered to allow user input to change them dynamically before.
I have implemented callback functions for panning, zooming and resizing the window. Now, you can explore the plot of a function! I plan on, in the future, changing the cursor to a custom “hand open” and “hand closed” cursor. Another nice note: this is running is super high FPS (around ~1000fps), but that is likely because we’re rendering a simple function with a small stack size. For an expression with ~200 stack_operators, this drops to around 200fps, which is still good enough!
Here is a video of me panning and zooming around the function of the previous devlog!
Log in to leave a comment
We can finally send things to the shader! This means that our input (like z*x + z*(y*x)/(2x) will be interpreted, transformed into a TokenOperator stack, broken into its unsigned char and glm::vec2 counterparts and sent to the shader.
Along the way, I ran through a bug where simple expressions like “z+x” wouldn’t render. This would happen because of a logic error with the ordering of operations, making so SHADER_SUM had a value lower than SHADER_VALUE_BOUNDARY, which made the shader interpreter treat it like a constant. This bug was a slight headache to fix, but it’s fixed!
Here’s the input function rendered!
Related commits:
Commit 076fd8d: Sending things to the shader done!
Commit ea2d8bc: Added #ifndef guard clauses to solve definition errors
Commit 2594ed3: Finished synchronization - fragment shader now shows 73 errors. Uh oh.
Related issues:
Issue #4: Sending things to the shader
Log in to leave a comment
This is a project that has been in development for a little bit, so if you want to see what has been going on, you can see it here: https://github.com/Sekqies/complex-plotter/issues
The bottom line is that we had a bunch of C++ and GLSL constants, and we needed a way to keep them synchronized. We could rely on a watchful eye to do so, but this is bug prone and dangerous, as discussed in issue 4: https://github.com/Sekqies/complex-plotter/issues/4 . So we had to fix it!
Now, we have a build process that sets the constants in both GLSL and C++. This means we can start sending the stack as Operators instead of magic numbers!
Below attached is the colored domain produced by that code (that corresponds to) f(z) = (1+i)z
Log in to leave a comment