Skip to content

Fractal Example#

Example's page

This page describes an example listed in app category.

Fractal viewer application demonstrating usage of custom shaders.

Fractal

If you are familiar with OpenGL, you can use custom shaders with AUI. In this example, we're using such shader to display a GPU-computed Mandelbrot set.

Source Code#

Repository

CMakeLists.txt#

1
2
3
4
5
6
7
8
if (NOT (AUI_PLATFORM_WIN OR AUI_PLATFORM_LINUX OR AUI_PLATFORM_MACOS))
    return()
endif ()

aui_executable(aui.example.fractal)
aui_compile_assets(aui.example.fractal)

aui_link(aui.example.fractal PRIVATE aui::core aui::views)

src/JumpToCoordsWindow.cpp#

//
// Created by alex2772 on 12/10/20.
//

#include <AUI/Layout/AVerticalLayout.h>
#include <AUI/Layout/AAdvancedGridLayout.h>
#include "JumpToCoordsWindow.h"
#include <AUI/Util/UIBuildingHelpers.h>
#include <AUI/View/ATextField.h>
#include <AUI/View/AButton.h>
#include <AUI/Platform/AMessageBox.h>

using namespace declarative;

JumpToCoordsWindow::JumpToCoordsWindow(_<FractalView> fractalView, AWindow* parent)
  : AWindow("Jump to coords", 854_dp, 500_dp, parent, WindowStyle::NO_RESIZE) {
    auto re = _new<ATextField>();
    auto im = _new<ATextField>();
    auto scale = _new<ATextField>();

    auto pos = fractalView->getPlotPosition();
    re->setText(AString::number(pos.x));
    im->setText(AString::number(pos.y));
    scale->setText(AString::number(fractalView->getPlotScale()));

    setContents(Vertical {
      _form({
        { "Re="_as, re },
        { "Im="_as, im },
        { "Scale="_as, scale },
      }),
      Horizontal {
        SpacerExpanding {},
        _new<AButton>("Jump").connect(
            &AButton::clicked, this,
            [&, fractalView, re, im, scale]() {
                try {
                    auto dRe = std::stod((*re->text()).toStdString());
                    auto dIm = -std::stod((*re->text()).toStdString());
                    auto dScale = std::stod((*re->text()).toStdString());
                    fractalView->setPlotPositionAndScale(glm::dvec2 { dRe, dIm }, dScale);
                    close();
                } catch (...) {
                    AMessageBox::show(this, "Error", "Please check your values are valid numbers.");
                }
            }) AUI_LET { it->setDefault(); },
        _new<AButton>("Cancel").connect(&AButton::clicked, me::close),
      },
    });

    pack();
}

src/FractalView.cpp#

//
// Created by alex2772 on 12/9/20.
//

#include "FractalView.h"
#include "AUI/Render/IRenderer.h"
#include <glm/gtc/matrix_transform.hpp>

static gl::Program::Uniform UNIFORM_TR("tr");
static gl::Program::Uniform UNIFORM_SQ("sq");
static gl::Program::Uniform UNIFORM_RATIO("ratio");
static gl::Program::Uniform UNIFORM_ITERATIONS("iterations");

FractalView::FractalView() : mTransform(1.f) {
    setExpanding();
    mShader.load(R"(
#version 330 core

in vec4 pos;
in vec2 uv;
out vec2 pass_uv;
uniform float ratio;
uniform mat4 SL_uniform_transform;

void main() {
    gl_Position = SL_uniform_transform * pos;
    pass_uv = (uv - 0.5) * 2.0;
    pass_uv.x *= ratio;
}
)",
    R"(
#version 330 core

in vec2 pass_uv;

#define product(a, b) vec2(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x)
#define conjugate(a) vec2(a.x,-a.y)
#define divide(a, b) vec2(((a.x*b.x+a.y*b.y)/(b.x*b.x+b.y*b.y)),((a.y*b.x-a.x*b.y)/(b.x*b.x+b.y*b.y)))

uniform float c;
uniform int iterations;
uniform mat4 tr;
uniform sampler2D tex;

out vec4 color;

void main() {
    vec2 tmp_pass_uv = (tr * vec4(pass_uv, 0.0, 1.0)).xy;
    vec2 v = vec2(0, 0);
    int i = 0;
    for (; i < iterations; ++i) {
        v = product(v, v) + tmp_pass_uv;
        vec2 tmp = v * v;
        if ((v.x + v.y) > 4) {
            break;
        }
    }
    if (i == iterations) {
        color = vec4(0, 0, 0, 1);
    } else {
        float theta = float(i) / float(iterations);
        color = texture(tex, vec2(theta, 0));
    }
}
)", { "pos", "uv" }, gl::GLSLOptions { .custom = true });
    mShader.compile();
    mShader.use();
    mShader.set(UNIFORM_TR, mTransform);
    mShader.set(UNIFORM_SQ, 1.f);

    mTexture = _new<gl::Texture2D>();
    mTexture->tex2D(*AImage::fromUrl(":img/color_scheme_wikipedia.png"));
}

void FractalView::render(ARenderContext context) {
    AView::render(context);

    mShader.use();
    mTexture->bind();
    context.render.rectangle(ACustomShaderBrush {}, { 0, 0 }, getSize());
}

void FractalView::setSize(glm::ivec2 size) {
    AView::setSize(size);
    mShader.use();
    mShader.set(UNIFORM_RATIO, mAspectRatio = float(size.x) / float(size.y));
}

void FractalView::setIterations(unsigned it) {
    mShader.use();
    mShader.set(UNIFORM_ITERATIONS, int(it));
}

void FractalView::onScroll(const AScrollEvent& event) {
    AView::onScroll(event);
    auto projectedPos = (glm::dvec2(event.origin) / glm::dvec2(getSize()) - glm::dvec2(0.5)) * 2.0;
    projectedPos.x *= mAspectRatio;
    mTransform = glm::translate(mTransform, glm::vec3 { projectedPos, 0.0 });
    mTransform = glm::scale(mTransform, glm::vec3(1.0 - event.delta.y / 1000.0));
    mTransform = glm::translate(mTransform, -glm::vec3 { projectedPos, 0.0 });

    handleMatrixUpdated();

    redraw();
}

void FractalView::reset() {
    mTransform = glm::dmat4(1.0);
    handleMatrixUpdated();
    redraw();
}

void FractalView::handleMatrixUpdated() {
    mShader.use();
    mShader.set(UNIFORM_TR, mTransform);
    emit centerPosChanged(getPlotPosition(), getPlotScale());
}

void FractalView::onKeyDown(AInput::Key key) {
    AView::onKeyDown(key);
    onKeyRepeat(key);
}

void FractalView::onKeyRepeat(AInput::Key key) {
    AView::onKeyRepeat(key);
    constexpr float SPEED = 0.2f;
    switch (key) {
        case AInput::UP:
            mTransform = glm::translate(mTransform, { 0, -SPEED, 0 });
            break;
        case AInput::DOWN:
            mTransform = glm::translate(mTransform, { 0, SPEED, 0 });
            break;
        case AInput::LEFT:
            mTransform = glm::translate(mTransform, { -SPEED, 0, 0 });
            break;
        case AInput::RIGHT:
            mTransform = glm::translate(mTransform, { SPEED, 0, 0 });
            break;
        case AInput::PAGEDOWN:
            mTransform = glm::scale(mTransform, glm::vec3 { 0.99 });
            break;
        case AInput::PAGEUP:
            mTransform = glm::scale(mTransform, glm::vec3 { 1.01 });
            break;

        default:
            return;
    }
    handleMatrixUpdated();
}

glm::dvec2 FractalView::getPlotPosition() const { return glm::dvec2(mTransform[3]); }

double FractalView::getPlotScale() const { return mTransform[0][0]; }

void FractalView::setPlotPositionAndScale(glm::dvec2 position, double scale) {
    mTransform = glm::dmat4(scale);
    mTransform[3] = glm::dvec4 { position, 0, 1 };
    handleMatrixUpdated();
}

src/FractalWindow.h#

1
2
3
4
5
6
7
8
#pragma once

#include <AUI/Platform/AWindow.h>

class FractalWindow : public AWindow {
public:
    FractalWindow();
};

src/FractalView.h#

#pragma once

#include <AUI/View/AView.h>
#include <AUI/GL/Program.h>
#include <AUI/GL/Texture2D.h>

class FractalView : public AView {
private:
    gl::Program mShader;
    _<gl::Texture2D> mTexture;
    glm::mat4 mTransform;

    float mAspectRatio;

    void handleMatrixUpdated();

public:
    FractalView();
    void render(ARenderContext context) override;

    void reset();

    void setIterations(unsigned it);

    void onKeyDown(AInput::Key key) override;

    void onKeyRepeat(AInput::Key key) override;

    void onScroll(const AScrollEvent& event) override;

    void setSize(glm::ivec2 size) override;

    gl::Program& getShader() { return mShader; }

    const _<gl::Texture2D>& getTexture() const { return mTexture; }

    glm::dvec2 getPlotPosition() const;
    double getPlotScale() const;

    void setPlotPositionAndScale(glm::dvec2 position, double scale);

signals:

    emits<glm::dvec2, double> centerPosChanged;
};

src/JumpToCoordsWindow.h#

1
2
3
4
5
6
7
8
9
#pragma once

#include <AUI/Platform/AWindow.h>
#include "FractalView.h"

class JumpToCoordsWindow : public AWindow {
public:
    explicit JumpToCoordsWindow(_<FractalView> fractalView, AWindow* parent);
};

src/main.cpp#

1
2
3
4
5
6
7
#include <AUI/Platform/Entry.h>
#include "FractalWindow.h"

AUI_ENTRY {
    _new<FractalWindow>()->show();
    return 0;
}

src/FractalWindow.cpp#

//
// Created by alex2772 on 12/9/20.
//

#include <AUI/View/ANumberPicker.h>
#include <AUI/View/AButton.h>
#include <AUI/Traits/strings.h>
#include "FractalWindow.h"
#include "FractalView.h"
#include "JumpToCoordsWindow.h"
#include <AUI/Util/UIBuildingHelpers.h>
#include <AUI/ASS/ASS.h>

using namespace ass;
using namespace declarative;

FractalWindow::FractalWindow() : AWindow("Mandelbrot set") {
    setLayout(std::make_unique<AHorizontalLayout>());

    auto centerPosDisplay = _new<ALabel>("-");
    {
        centerPosDisplay->setCustomStyle({
          BackgroundSolid { 0x80000000_argb },
          Padding { 4_dp },
          TextColor { 0xffffff_rgb },
          FontSize { 11_pt },
        });
    }

    auto fractal = _new<FractalView>();
    connect(fractal->centerPosChanged, this, [centerPosDisplay](const glm::dvec2& newPos, double scale) {
        centerPosDisplay->setText("Center position: {} {}, scale: {}"_format(newPos.x, -newPos.y, scale));
    });

    setContents(Horizontal {
      Stacked::Expanding {
        fractal,
        Vertical::Expanding {
          SpacerExpanding {},
          Horizontal {
            SpacerExpanding {},
            centerPosDisplay,
          },
        },
      },
      Vertical {
        _new<AButton>("Identity").connect(&AButton::clicked, AUI_SLOT(fractal)::reset),
        _new<AButton>("Jump to coords...")
            .connect(&AButton::clicked, this, [&, fractal]() { _new<JumpToCoordsWindow>(fractal, this)->show(); }),
        _new<ALabel>("Iterations:"),
        _new<ANumberPicker>().connect(
            &ANumberPicker::valueChanged, this, [fractal](int v) { fractal->setIterations(v); }) AUI_LET {
                it->setMax(1000);
                it->setValue(350);
            },
      },
    });

    fractal->focus();
}