Julia Set Explorer¶
Description (GitHub README.md)¶
A mini app to visualise Julia sets based on the mouse position.
- inspired by this video: https://youtu.be/nr8biZfSZ3Y?si=WUcSGoKV80Fr1rsS (6:47)
Optimisations¶
- Added precomputed colour lookup table to avoid repeated arithmetic
- Flattened 2D loops to use 1D index maths instead, which makes vectorisation easier
- Used a raw pixel buffer to update the final RGBA data directly
- Enabled compiler autovectorisation (SIMD) by compiling as a release build with -Ofast and -march=native (biggest performance boost)
- Reduced unnecessary branching inside loops
- Direct texture updates from raw pixel buffer
- Parallelised using OpenMP
License¶
No license granted. All rights reserved.
In [ ]:
// 30.05.25
// Charlot Eberlein
// inspired by this video: https://youtu.be/nr8biZfSZ3Y?si=WUcSGoKV80Fr1rsS (6:47)
// namespaces
#include <SFML/Graphics.hpp>
#include <cstdint>
#include <complex>
#include <optional>
#include <iostream>
#include <omp.h> // parallelisation using OpenMP
// constants
#define WIDTH 600
#define HEIGHT 750
#define MAXITER 20
// draw julia set
void fractal (std::vector<uint8_t>& raw_px, const std::vector<sf::Color>& palette, float left, float top, float xside, float yside, float cx, float cy)
{
// determine scaling based on window dimensions
float xscale = xside / WIDTH;
float yscale = yside / HEIGHT; // keep aspect ratio of fractal
#pragma omp parallel for // parallelisation using OpenMP
for (int i=0; i<WIDTH*HEIGHT; i++) {
// flattened 2D for loop
int x = i % WIDTH;
int y = i / WIDTH;
// complex, z - not necessarily centered at (0,0)
float zx = x * xscale + left; // re
float zy = y * yscale + top; // im
float tempx;
int count = 0;
// cache squared values for speed
float zx2 = zx*zx;
float zy2 = zy*zy;
while ((zx2 + zy2 < 4.0f) && (count<MAXITER)) {
// recurrence relationship: z = z*z + c
// optimised to speed up calculations by not doing complex arithmetic
tempx = zx2 - zy2 + cx;
zy = 2.0f*zx*zy + cy;
zx = tempx;
zx2 = zx*zx;
zy2 = zy*zy;
count++;
}
// mapping iteration count to colour
int j = 4 * (y * WIDTH + x);
// use pre-computed look-up table
auto& c = palette[count];
raw_px[j] = c.r;
raw_px[j + 1] = c.g;
raw_px[j + 2] = c.b;
raw_px[j + 3] = 255;
}
}
// plotting the fractal
int main()
{
// create window
sf::RenderWindow window(sf::VideoMode({WIDTH,HEIGHT}), "Plotting Julia sets with C++");
// default viewpoint parameters
float left = -1.5f;
float top = -1.5f;
float xside = 3.0f;
float yside = 3.0f * HEIGHT/WIDTH;
float cx = -0.8f;
float cy = 0.156f;
// create look-up table for colour palette
std::vector<sf::Color> palette(MAXITER+1);
for (int i = 0; i <= MAXITER; ++i) {
if (i == MAXITER) palette[i] = sf::Color::Black;
else palette[i] = sf::Color(10*i,5*i,15*i);
}
// create pixel buffer & raw pixel data buffer
std::vector<uint8_t> raw_px(WIDTH*HEIGHT*4);
// compute initial fractal
fractal(raw_px, palette, left, top, xside, yside, cx, cy);
// initialise texture and sprite
sf::Texture texture(sf::Vector2u(WIDTH,HEIGHT));
sf::Sprite sprite(texture);
// update initial texture
texture.update(raw_px.data(), sf::Vector2u(WIDTH,HEIGHT), sf::Vector2u(0,0));
// main program loop
while (window.isOpen()) {
while (const std::optional<sf::Event> event = window.pollEvent()) {
// exit window when closing
if (event->is<sf::Event::Closed>()) {
window.close();
}
if (const auto* e = (event->getIf<sf::Event::MouseMoved>())) {
int mouseX = static_cast<int>(e->position.x);
int mouseY = static_cast<int>(e->position.y);
float cx = left + (mouseX/(float)WIDTH) * xside;
float cy = top + (mouseY/(float)HEIGHT) * yside;
// compute new fractal
fractal(raw_px, palette, left, top, xside, yside, cx, cy);
// update texture
texture.update(raw_px.data(), sf::Vector2u(WIDTH,HEIGHT), sf::Vector2u(0,0));
}
}
// clear previous state
window.clear();
// draw sprite on screen
window.draw(sprite);
// update window
window.display();
}
return 0;
}