Setting Up OpenGL Shaders for Fractal Rendering

Mục tiêu: A guide to creating and compiling OpenGL shaders for rendering a fractal pattern using Rust and the glium crate.


Let’s dive into setting up OpenGL shaders for rendering the fractal pattern. Shaders are essential for rendering graphics in OpenGL as they run on the GPU and determine how pixels are drawn.

Understanding OpenGL Shaders

OpenGL uses two primary types of shaders:

  1. Vertex Shader: Runs on the vertex processor and is responsible for transforming vertices and passing data to the fragment shader.
  2. Fragment Shader: Runs on the fragment processor and determines the final color of each pixel.

For our fractal generator, we’ll create simple shaders to get started.

Writing the Shaders

Vertex Shader (vertex_shader.glsl)

#version 150 core

in vec2 position; // Input vertex position

void main() {
    gl_Position = vec4(position, 0.0, 1.0); // Convert 2D position to 4D OpenGL position
}

Fragment Shader (fragment_shader.glsl)

#version 150 core

out vec4 outColor; // Output color for each fragment

uniform vec4 color; // Color uniform passed from Rust code

void main() {
    outColor = color; // Set the output color
}

Creating and Compiling Shaders in Rust

We’ll use the glium crate to handle OpenGL operations. Here’s how to create and compile the shaders:

use glium::glsl::include_glsl;
use glium::{Display, Program};

// Create a Display (OpenGL context)
let display = glium::glutin::DisplayBuilder::new()
    .with_vsync(true)
    .build(glium::glutin::get_current_context().unwrap())
    .unwrap();

// Include the shaders
let vertex_shader_src = include_glsl!("vertex_shader.glsl");
let fragment_shader_src = include_glsl!("fragment_shader.glsl");

// Create the shader program
let program = Program::from_source(&display, vertex_shader_src, fragment_shader_src)
    .expect("Failed to compile shaders");

Explanation of the Code

  • Vertex Shader:
  • Takes a position input attribute
  • Outputs the transformed position using gl_Position
  • The position is converted from 2D to 4D as required by OpenGL
  • Fragment Shader:
  • Takes a color uniform
  • Sets the output color for each fragment
  • The color will be passed from Rust code
  • Rust Code:
  • Creates an OpenGL display
  • Includes the shader files
  • Compiles and links the shaders into a program

Next Steps

  1. Create Buffer for Pixel Data: Implement a buffer to hold the fractal pixel data
  2. Implement Color Gradient: Create a function to generate color gradients
  3. Draw Fractal Pattern: Use the shader program to draw the fractal pattern

Further Reading

Create Buffer for Pixel Data in OpenGL

Mục tiêu: Set up a buffer to hold pixel data for generating a Mandelbrot fractal using OpenGL in Rust.


Creating a Buffer to Hold Pixel Data for Fractal Generation

Now that we’ve set up our window, the next crucial step is to create a buffer to hold the pixel data for our fractal. This buffer will store the computed values of the Mandelbrot set that will be used to generate the visual representation.

In OpenGL, we typically use Vertex Buffer Objects (VBOs) to store vertex data. However, since we’re dealing with pixel data for the entire screen, we’ll create a buffer that will be used as a texture in our shaders. This approach allows us to efficiently transfer and manipulate large amounts of data.

Understanding the Buffer Creation Process

  1. Buffer Types: We’ll be using GL_ARRAY_BUFFER for storing our pixel data.
  2. Buffer Usage: Since we’re generating the fractal on the CPU and then transferring it to the GPU, we’ll use GL_STATIC_DRAW as our usage hint.

Implementing the Buffer Creation

Here’s how we can create the buffer:

// Create a buffer to hold our pixel data
let mut buffer: usize = 0;
unsafe {
    gl::GenBuffers(1, &mut buffer);
}

// Bind the buffer to the GL_ARRAY_BUFFER target
unsafe {
    gl::BindBuffer(gl::ARRAY_BUFFER, buffer);
}

// Allocate space for the buffer (currently empty)
// We'll fill this buffer later with our fractal data
let size = (width * height * 4) as isize; // 4 bytes per pixel (RGBA)
unsafe {
    gl::BufferData(gl::ARRAY_BUFFER, size, std::ptr::null(), gl::STATIC_DRAW);
}

// Create a Vertex Array Object (VAO) to describe how we'll access the buffer
let mut vao: usize = 0;
unsafe {
    gl::GenVertexArrays(1, &mut vao);
}

// Bind the VAO and specify the vertex attributes
unsafe {
    gl::BindVertexArray(vao);
    // Each vertex has a position (x, y) that will map to our screen coordinates
    gl::VertexAttribPointer(
        0, // Attribute index (matches the shader's layout location)
        2, // Size of each vertex (x, y)
        gl::FLOAT, // Type of each component
        gl::FALSE, // Normalized?
        0, // Stride (bytes between consecutive vertices)
        std::ptr::null() // Offset of the first vertex
    );
    gl::EnableVertexAttribArray(0);
}

// Unbind the VAO and buffer
unsafe {
    gl::BindBuffer(gl::ARRAY_BUFFER, 0);
    gl::BindVertexArray(0);
}

Explanation of the Code

  1. Buffer Creation: We create a buffer using gl::GenBuffers and bind it using gl::BindBuffer. This buffer will hold our pixel data.
  2. Buffer Allocation: We allocate space for our buffer using gl::BufferData. Initially, we’re just reserving space since we’ll fill the buffer later with our fractal data.
  3. Vertex Array Object (VAO): The VAO is created using gl::GenVertexArrays and bound using gl::BindVertexArray. The VAO is essential for telling OpenGL how to interpret the data in our buffer.
  4. Vertex Attribute Pointer: Using gl::VertexAttribPointer, we specify how the data in our buffer should be interpreted. In this case, we’re telling OpenGL that each vertex consists of 2 floating-point numbers (x, y coordinates).
  5. Enabling Attributes: We enable the vertex attribute array using gl::EnableVertexAttribArray.
  6. Unbinding: After setting up our VAO and buffer, we unbind them to prevent unintended modifications.

Next Steps

Now that we’ve set up our buffer, the next step is to:

  1. Set up OpenGL Shaders: Create vertex and fragment shaders to process our buffer data and render it to the screen.
  2. Implement Fractal Calculation: Write the functions that will compute the Mandelbrot set and fill our buffer with the appropriate data.

Error Handling

Make sure to include error checking after each OpenGL operation. This can be done using a helper function:

fn check_errors() {
    let error = unsafe { gl::GetError() };
    if error != gl::NO_ERROR {
        eprintln!("OpenGL error: {:?}", error);
    }
}

Call this function after each major OpenGL operation to catch any errors early.

Further Reading

Color Gradient Function for Fractal Visualization

Mục tiêu: Implementing a color gradient function to enhance fractal visualization by mapping iteration counts to colors.


Implementing a Color Gradient Function for Fractal Visualization

To enhance the visual appeal of our Mandelbrot set, we’ll implement a color gradient function. This function will map the iteration counts to specific colors, creating a vibrant and visually striking representation of the fractal.

Understanding Color Gradients

A color gradient function takes a numerical value (in our case, the iteration count) and maps it to a color. This mapping can be done in various ways, but for this implementation, we’ll use a HSL (Hue, Saturation, Lightness) to RGB conversion approach. This allows us to create smooth color transitions.

HSL to RGB Conversion

The HSL color model is intuitive for creating gradients because:

  • Hue determines the color (e.g., red, blue, green)
  • Saturation determines the color’s intensity
  • Lightness determines how bright the color is

We’ll convert HSL values to RGB for rendering.

Implementing the Gradient Function

Here’s how we’ll implement the gradient function:

  1. Hue Calculation: The hue will be based on the iteration count, creating a rainbow effect.
  2. Saturation and Lightness: These will be adjusted to create vibrant colors.
  3. Edge Case Handling: Points that remain bounded (iteration count = max_iterations) will be colored dark brown.

GLSL Fragment Shader Implementation

We’ll implement this logic in our fragment shader:

#version 330 core

in vec2 position;  // Interpolated position (from vertex shader)

out vec4 frag_color;  // Output color

uniform samplerBuffer iteration_data;  // Buffer containing iteration counts

// Convert HSL to RGB
vec3 hsl_to_rgb(vec3 hsl) {
    vec3 rgb;
    float h = hsl.x;
    float s = hsl.y;
    float l = hsl.z;

    float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
    float p = 2.0 * l - q;

    rgb.r = max(min(0.0, h + 1.0/3.0), 1.0);
    rgb.g = max(min(0.0, h       ), 1.0);
    rgb.b = max(min(0.0, h - 1.0/3.0), 1.0);

    rgb = vec3(
        l - (p * ((rgb.r - p) < 0.0 ? p - rgb.r : rgb.r)),
        l - (q * ((rgb.g - q) < 0.0 ? q - rgb.g : rgb.g)),
        l - (p * ((rgb.b - p) < 0.0 ? p - rgb.b : rgb.b))
    );

    return rgb;
}

void main() {
    // Get iteration count from buffer
    uint index = uint(floor(position.x) + floor(position.y) * 800.0);  // Assuming 800x800 window
    uint count = texelFetchBuffer(iteration_data, index).r;

    if (count == 0) {
        // Point is in the set - color it dark brown
        frag_color = vec4(0.2, 0.1, 0.0, 1.0);
        return;
    }

    // Calculate color based on iteration count
    float hue = (count / 255.0) * 0.7;  // 0.7 cycles through most of the color spectrum
    float saturation = 0.8;
    float lightness = 0.5;

    vec3 hsl = vec3(hue, saturation, lightness);
    vec3 rgb = hsl_to_rgb(hsl);

    frag_color = vec4(rgb, 1.0);
}

Setting Up the Buffer

In Rust, we’ll create a buffer to hold our iteration counts:

// Create buffer
let mut buffer: Vec<u8> = Vec::new();
for y in 0..HEIGHT {
    for x in 0..WIDTH {
        let index = y * WIDTH + x;
        buffer.push(iteration_data[index] as u8);
    }
}

unsafe {
    gl.BufferData(
        gl_BUFFER_BUFFER,
        buffer.len() as isize,
        buffer.as_ptr() as *const _,
        gl.STATIC_DRAW,
    );
}

Complete Pipeline

  1. Vertex Shader: Passes the position attribute
  2. Fragment Shader: Implements our color gradient logic
  3. Buffer: Holds iteration counts
  4. Texture: Used to sample buffer data in the fragment shader

Enhancements

  • Multiple Color Schemes: Implement different gradient functions
  • Real-time Adjustments: Allow users to modify color parameters
  • Performance Optimizations: Use Rust’s parallelism features

Further Reading

Rendering a Fractal Pattern with OpenGL Shaders

Mục tiêu: A step-by-step guide to rendering a fractal pattern using OpenGL shaders, including buffer creation and rendering logic.


To draw the fractal pattern using the computed data, we’ll need to set up OpenGL shaders, create a buffer for pixel data, and implement the rendering logic. Here’s how to do it step by step:

Setting Up OpenGL Shaders

First, we need to create shaders for our OpenGL pipeline. Shaders are small programs that run on the GPU. We’ll need two shaders:

  1. Vertex Shader: This will handle the position of our pixels
  2. Fragment Shader: This will handle the color calculation for each pixel

Here’s the code for the shaders:

// Vertex shader
const VERTEX_SHADER_SOURCE: &str = r"#
    #version 330 core
    layout (location = 0) in vec2 position;
    out vec2 uv;

    void main()
    {
        gl_Position = vec4(position, 0.0, 1.0);
        uv = position * 0.5 + 0.5;
    }
"#;

// Fragment shader
const FRAGMENT_SHADER_SOURCE: &str = r"#
    #version 330 core
    in vec2 uv;
    out vec4 color;

    uniform sampler2D tex;

    void main()
    {
        float value = texture(tex, uv).r;
        color = vec4(value, value, value, 1.0);
    }
"#;

Creating Buffer Objects

We’ll create a Vertex Buffer Object (VBO) to hold our pixel positions and a Frame Buffer Object (FBO) for off-screen rendering.

// Create VBO
let mut vbo = 0;
unsafe {
    gl::GenBuffers(1, &mut vbo);
    gl::BindBuffer(gl::ARRAY_BUFFER, vbo);

    // We'll update this buffer later with our pixel data
    gl::BufferData(gl::ARRAY_BUFFER, (width * height * std::mem::size_of::<f32>()) as GLsizeiptr, std::ptr::null(), gl::DYNAMIC_DRAW);
}

Implementing the Rendering Logic

Now we’ll create a function to render our fractal data:

fn render_fractal(&mut self) {
    // Bind our FBO for rendering
    unsafe {
        gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo);
    }

    // Clear the screen
    unsafe {
        gl::ClearColor(0.3, 0.3, 0.3, 1.0);
        gl::Clear(gl::COLOR_BUFFER_BIT);
    }

    // Draw our fractal
    unsafe {
        gl::UseProgram(self.program);

        // Update the texture with our fractal data
        gl::ActiveTexture(gl::TEXTURE0);
        gl::BindTexture(gl::TEXTURE_2D, self.texture);

        // Update the VBO with our pixel data
        gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo);
        gl::BufferData(gl::ARRAY_BUFFER, 
                        (self.width * self.height * std::mem::size_of::<f32>()) as GLsizeiptr, 
                        self.pixel_data.as_ptr() as *const _, 
                        gl::DYNAMIC_DRAW);

        // Enable vertex attributes
        gl::EnableVertexAttribArray(0);
        gl::VertexAttribPointer(0, 2, gl::FLOAT, gl::FALSE, 0, std::ptr::null());

        // Draw the pixels
        gl::DrawArrays(gl::POINTS, 0, (self.width * self.height) as i32);

        // Disable vertex attributes
        gl::DisableVertexAttribArray(0);
    }

    // Unbind FBO
    unsafe {
        gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
    }
}

Handling Window Resizing

We need to handle window resizing events to maintain the correct aspect ratio:

fn handle_resize(&mut self, width: i32, height: i32) {
    self.width = width as u32;
    self.height = height as u32;

    // Update the viewport
    unsafe {
        gl::Viewport(0, 0, width as i32, height as i32);
    }

    // Recreate the FBO and texture with new dimensions
    self.create_frame_buffer();
    self.create_texture();

    // Update the VBO size
    unsafe {
        gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo);
        gl::BufferData(gl::ARRAY_BUFFER, 
                        (self.width * self.height * std::mem::size_of::<f32>()) as GLsizeiptr, 
                        std::ptr::null(), 
                        gl::DYNAMIC_DRAW);
    }
}

Next Steps

After completing this task, you should:

  1. Test the rendering with different fractal parameters
  2. Implement color gradients for better visualization
  3. Add zoom and pan functionality
  4. Optimize the rendering performance

Further Reading

Handling Window Resizing Events in Rust Fractal Generator

Mục tiêu: Implement window resizing functionality in a Rust fractal generator to adjust the fractal display when the window size changes.


Handling Window Resizing Events in Rust Fractal Generator

Handling window resizing events is an essential part of creating a responsive graphical application. In this task, we’ll implement window resizing functionality for our fractal generator. This will ensure that the fractal properly adjusts when the window size changes.

Why Window Resizing is Important

  • User Experience: Users often expect applications to handle window resizing gracefully.
  • Display Consistency: The fractal should maintain proper proportions and fit within the window boundaries.
  • Performance: Resizing should trigger recalculation and redrawing of the fractal with the new window dimensions.

Approach

  1. Modify Window Struct: Add window size fields to our window struct to track current dimensions.
  2. Implement Resize Handler: Create a function to handle resize events and update the window dimensions.
  3. Update Viewport: When the window resizes, we’ll need to update the OpenGL viewport to match the new window size.
  4. Recalculate Fractal: After resizing, we’ll need to recalculate the fractal with the new window dimensions.

Solution Code

// Add this to your window struct
#[derive(Debug)]
struct Window {
    winit_window: winit::Window,
    width: f32,
    height: f32,
}

// Implement the resize handler
fn handle_resize_event(window: &mut Window, new_size: winit::dpi::PhysicalSize<u32>) {
    window.width = new_size.width as f32;
    window.height = new_size.height as f32;
    unsafe {
        gl::Viewport(0, 0, new_size.width as i32, new_size.height as i32);
    }
    // Recalculate and redraw the fractal with new window dimensions
    calculate_and_draw_fractal(window);
}

// Update your event loop to handle resize events
event_loop.run(move |event, _, control_flow| {
    *control_flow = winit::ControlFlow::Wait;

    match event {
        winit::Event::WindowEvent { event: winit::WindowEvent::Resized(size), .. } => {
            handle_resize_event(&mut window, size);
        }
        // Handle other events...
        _ => (),
    }
});

Explanation

  • Window Struct Modification: We added width and height fields to our Window struct to keep track of the current window dimensions.
  • Resize Handler Function: The handle_resize_event function updates the window dimensions and recalculates the fractal whenever the window is resized.
  • OpenGL Viewport Update: We use OpenGL’s Viewport function to adjust the rendering area to match the new window size.
  • Event Loop Integration: The event loop now checks for resize events and triggers our resize handler when they occur.

Next Steps

  • Zoom and Pan Functionality: Implement mouse and keyboard controls for zooming and panning the fractal.
  • Color Gradient Optimization: Enhance the visual appearance by implementing smooth color gradients.
  • Performance Optimization: Consider using multi-threading or parallel processing to speed up fractal calculations.

Further Reading

Testing Fractal Color Schemes

Mục tiêu: Implement and test different color schemes for fractal rendering using Rust.


Testing Rendering with Different Color Schemes

Now that we have our fractal rendering setup, let’s explore one of the most creative aspects of fractal visualization: color schemes. The way we color our fractal can dramatically change its appearance and can help reveal different patterns and structures within the set.

Why Color Schemes Matter

The color scheme determines how we map the iteration counts of each point in the fractal to actual colors. Different schemes can:

  • Highlight different levels of detail in the fractal
  • Create more aesthetically pleasing visualizations
  • Help in understanding the mathematical properties of the set
  • Improve visibility of certain features

In this task, we’ll implement and test several different color schemes to see how they affect our visualization.

Implementing Color Schemes

First, let’s define a few different color schemes. We’ll create a color module in our project to keep things organized.

// src/color.rs
pub enum ColorScheme {
    SimpleRGB,
    HSL,
    HCL,
}

pub fn get_color(iteration: u32, max_iter: u32, scheme: ColorScheme) -> (f32, f32, f32) {
    match scheme {
        ColorScheme::SimpleRGB => {
            // Simple RGB scheme that cycles through colors based on iteration count
            let hue = (iteration as f32) / (max_iter as f32);
            let r = f32::sin(6.28318 * hue) / 2.0 + 0.5;
            let g = f32::sin(6.28318 * hue + 2.0944) / 2.0 + 0.5;
            let b = f32::sin(6.28318 * hue + 4.18879) / 2.0 + 0.5;
            (r, g, b)
        }
        ColorScheme::HSL => {
            // HSL color space mapping
            let hue = (iteration as f32) / (max_iter as f32);
            let saturation = 1.0;
            let lightness = 0.5;

            let c = (1.0 - lightness.abs()) * saturation;
            let x = c * (6.0 * hue).sin();
            let y = c * (6.0 * hue + 6.28318).sin();

            let r = lightness + (c * (6.0 * hue - 6.28318).sin() + x) * 0.5;
            let g = lightness + x * 0.5 - y * 0.5;
            let b = lightness - x * 0.5 - y * 0.5;

            (r, g, b)
        }
        ColorScheme::HCL => {
            // HCL color space mapping (similar to HSL but perceptually uniform)
            let hue = (iteration as f32) / (max_iter as f32);
            let saturation = 1.0;
            let lightness = 0.5;

            let q = if lightness < 0.5 {
                lightness * (1.0 + saturation)
            } else {
                lightness + saturation - lightness * saturation
            };

            let s = q / lightness;
            let c = (1.0 - lightness.abs()) * saturation;
            let x = c * (6.0 * hue).sin();
            let y = c * (6.0 * hue + 6.28318).sin();

            let r = lightness + x;
            let g = lightness + y;
            let b = lightness - x - y;

            (r, g, b)
        }
    }
}

Updating Our Renderer

Now let’s update our renderer to use these color schemes. We’ll modify our rendering function to accept a ColorScheme parameter.

// src/main.rs
fn render_fractal(
    gl: &gl::Gl,
    width: usize,
    height: usize,
    x_min: f64,
    x_max: f64,
    y_min: f64,
    y_max: f64,
    max_iter: u32,
    scheme: color::ColorScheme,
) {
    let mut data = Vec::new();

    // Generate pixel data
    for y in 0..height {
        for x in 0..width {
            let x_ratio = (x as f64) / ((width - 1) as f64);
            let y_ratio = (y as f64) / ((height - 1) as f64);
            let x0 = x_min + x_ratio * (x_max - x_min);
            let y0 = y_min + y_ratio * (y_max - y_min);

            let mut iteration = 0;
            let mut zx = 0.0;
            let mut zy = 0.0;

            while iteration < max_iter && zx.hypot(zy) <= 2.0 {
                let tmp = zx * zx - zy * zy + x0;
                zy = 2.0 * zx * zy + y0;
                zx = tmp;
                iteration += 1;
            }

            let (r, g, b) = color::get_color(iteration, max_iter, scheme);
            data.push(r);
            data.push(g);
            data.push(b);
        }
    }

    unsafe {
        gl.DrawBuffer(gl::BACK);
        gl.Clear(gl::COLOR_BUFFER_BIT);

        // Create and update texture
        let mut texture = 0;
        gl.GenTextures(1, &mut texture);
        gl.BindTexture(gl::TEXTURE_2D, texture);
        gl.TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32);
        gl.TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
        gl.TexImage2D(
            gl::TEXTURE_2D,
            0,
            gl::RGB as i32,
            width as i32,
            height as i32,
            0,
            gl::RGB,
            gl::FLOAT,
            data.as_ptr() as *const f32,
        );

        // Draw the texture to screen
        gl.Viewport(0, 0, width as i32, height as i32);
        gl.ClearColor(0.0, 0.0, 0.0, 1.0);
        gl.Clear(gl::COLOR_BUFFER_BIT);

        // Use our simple texture shader
        gl.UseProgram(0); // Assuming we have a simple texture shader

        let vertices = [
            -1.0, -1.0,
            1.0, -1.0,
            -1.0, 1.0,
            1.0, 1.0,
        ];

        let vao = 0;
        let vbo = 0;
        gl.GenVertexArrays(1, &mut vao);
        gl.BindVertexArray(vao);

        gl.GenBuffers(1, &mut vbo);
        gl.BindBuffer(gl::ARRAY_BUFFER, vbo);
        gl.BufferData(
            gl::ARRAY_BUFFER,
            (vertices.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr,
            vertices.as_ptr() as *const f32,
            gl::STATIC_DRAW,
        );

        gl.VertexAttribPointer(0, 2, gl::FLOAT, gl::FALSE, 0, std::ptr::null());
        gl.EnableVertexAttribArray(0);

        gl.DrawArrays(gl::TRIANGLE_STRIP, 0, 4);

        // Cleanup
        gl.DeleteVertexArrays(1, &vao);
        gl.DeleteBuffers(1, &vbo);
        gl.DeleteTextures(1, &texture);
    }
}

Testing Different Schemes

Let’s create a function to test our different color schemes. We’ll add keyboard controls to switch between schemes.

// src/main.rs
use glium::glutin::{Event, WindowEvent, ControlFlow};

struct State {
    scheme: color::ColorScheme,
}

impl State {
    fn new() -> Self {
        State {
            scheme: color::ColorScheme::SimpleRGB,
        }
    }

    fn handle_event(&mut self, event: &Event<WindowEvent>) -> ControlFlow {
        match event.event_type {
            glium::glutin::Event::WindowEvent { event, .. } => match event {
                WindowEvent::KeyboardInput { input, .. } => {
                    if let Some(key) = input.virtual_keycode {
                        match key {
                            glium::glutin::VirtualKeyCode::A => {
                                self.scheme = color::ColorScheme::SimpleRGB;
                            }
                            glium::glutin::VirtualKeyCode::S => {
                                self.scheme = color::ColorScheme::HSL;
                            }
                            glium::glutin::VirtualKeyCode::D => {
                                self.scheme = color::ColorScheme::HCL;
                            }
                            _ => (),
                        }
                    }
                }
                _ => (),
            },
            _ => (),
        }

        ControlFlow::Continue
    }
}

Updating the Main Loop

Finally, let’s update our main loop to use the state and render with the selected color scheme.

// src/main.rs
fn main() {
    // ... previous initialization code ...

    let mut state = State::new();

    main_loop.move_window(window);

    'main_loop: loop {
        let mut target = display.draw_to_texture(glium::Texture2d::empty_dimensions());
        let (width, height) = target.get_dimensions();

        // Render the fractal with current color scheme
        render_fractal(
            &display.gl(),
            width as usize,
            height as usize,
            x_min,
            x_max,
            y_min,
            y_max,
            max_iter,
            state.scheme,
        );

        target.finish().unwrap();

        for event in display.poll_events() {
            if state.handle_event(&event).is_break() {
                break 'main_loop;
            }
        }
    }
}

Testing the Color Schemes

Now you can run your program and use the keyboard to switch between different color schemes:

  • A: Simple RGB scheme
  • S: HSL scheme
  • D: HCL scheme

Observe how each scheme affects the visualization of the fractal. The HSL and HCL schemes should provide more perceptually uniform color mappings, while the Simple RGB scheme provides a basic but clear visualization.

Enhancements

  1. Add More Schemes: Implement additional color schemes like grayscale, inverted colors, or even custom artistic schemes.
  2. Scheme Parameters: Add parameters to control aspects of the color schemes (e.g., saturation, lightness ranges).
  3. Real-time Switching: Improve the keyboard controls to allow real-time switching without needing to re-render the entire fractal.
  4. Custom Schemes: Allow users to define their own color schemes through configuration files or at runtime.

Further Reading