Rust + SDL2 - Software Rasterization - We would like to develop with Software Rasterization

Hello everyone,
I am new for Rust Community. Thanks for welcome-saying!

I have tested with ChatGPT but ChatGPT is really catastrophically.
I create simple Software Rasterization on SDL2 with Rust. I find funny because rust is really faster than C# or python.
I already build against static version of executable without required loaded dynamic libraries.
build.rs at root directory of my sdl2_project:

fn main() {
    println!("cargo:rustc-link-lib=static=SDL2main");
    println!("cargo:rustc-link-lib=static=SDL2-static");
    println!("cargo:rustc-link-search=native=C:/SDL2/lib");

    println!("cargo:rustc-link-lib=dylib=AdvAPI32");
    println!("cargo:rustc-link-lib=dylib=bcrypt");
    println!("cargo:rustc-link-lib=dylib=User32");
    println!("cargo:rustc-link-lib=dylib=Ole32");
    println!("cargo:rustc-link-lib=dylib=Gdi32");
    println!("cargo:rustc-link-lib=dylib=Imm32");
    println!("cargo:rustc-link-lib=dylib=WinMM");
    println!("cargo:rustc-link-lib=dylib=OleAut32");
    println!("cargo:rustc-link-lib=dylib=SetupAPI");
    println!("cargo:rustc-link-lib=dylib=cfgmgr32");
    println!("cargo:rustc-link-lib=dylib=shell32");
    println!("cargo:rustc-link-lib=dylib=Version");
    println!("cargo:rustc-link-lib=dylib=libcmt");
    println!("cargo:rustc-link-lib=static=libvcruntime");
    println!("cargo:rustc-link-lib=static=libucrt");
    println!("cargo:rustc-link-lib=static=ucrt");
}

And I need to create config.toml in %USERPROFILE%\.cargo\

# Windows 64 bit programs
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "link-arg=libvcruntime.lib", "-Ctarget-feature=+crt-static", "-Zunstable-options"]

[build]
rustflags = ["-Ctarget-feature=+crt-static"]

I want to show - how do I draw lined triangle on SDL2's SDL_Surface - It would like to make as Software Rasterization.

extern crate sdl2;

use sdl2::pixels::Color;
use sdl2::surface::Surface;
use sdl2::pixels::PixelFormatEnum;

// Struct to handle screen coordinate transformations
struct ScreenCoordinator {
    window_width: i32,
    window_height: i32,
}

impl ScreenCoordinator {
    fn new(window_width: i32, window_height: i32) -> Self {
        Self {
            window_width,
            window_height,
        }
    }

    fn to_screen_coords(&self, x: f32, y: f32) -> (i32, i32) {
        let screen_x = ((x + 1.0) * (self.window_width as f32 / 2.0)) as i32;
        let screen_y = ((1.0 - y) * (self.window_height as f32 / 2.0)) as i32; // Flip Y-axis
        (screen_x, screen_y)
    }
}

// Lined triangle
fn draw_triangle(
    surface: &mut Surface, 
    p1: (f32, f32), 
    p2: (f32, f32), 
    p3: (f32, f32), 
    color: Color, 
    coordinator: &ScreenCoordinator
) {
    let width = surface.width() as i32;
    let height = surface.height() as i32;
    let pitch = surface.pitch() as i32;

    let pixels = surface.without_lock_mut().unwrap();

    let mut draw_line = |(x0, y0): (f32, f32), (x1, y1): (f32, f32)| {
        let (mut x0, mut y0) = coordinator.to_screen_coords(x0, y0);
        let (x1, y1) = coordinator.to_screen_coords(x1, y1);
        let dx = (x1 - x0).abs();
        let sx = if x0 < x1 { 1 } else { -1 };
        let dy = -(y1 - y0).abs();
        let sy = if y0 < y1 { 1 } else { -1 };
        let mut err = dx + dy;

        loop {
            if x0 >= 0 && x0 < width && y0 >= 0 && y0 < height {
                let offset = (y0 * pitch + x0 * 3) as usize;
                pixels[offset] = color.r;
                pixels[offset + 1] = color.g;
                pixels[offset + 2] = color.b;
            }

            if x0 == x1 && y0 == y1 {
                break;
            }

            let e2 = 2 * err;
            if e2 >= dy {
                err += dy;
                x0 += sx;
            }
            if e2 <= dx {
                err += dx;
                y0 += sy;
            }
        }
    };

    draw_line(p1, p2);  // Ensure p1 and p2 are (f32, f32)
    draw_line(p2, p3);  // Ensure p2 and p3 are (f32, f32)
    draw_line(p3, p1);  // Ensure p3 and p1 are (f32, f32)
}

fn main() {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();

    let window_width = 800;
    let window_height = 600;

    let window = video_subsystem
        .window("Rust SDL2: 02 Lined Triangle", window_width as u32, window_height as u32)
        .position_centered()
        .build()
        .unwrap();

    // Create a surface with the same dimensions as the window
    let mut surface = Surface::new(window_width, window_height, PixelFormatEnum::RGB24).unwrap();

    let background_color = Color::RGB(255 / 10, 255 / 10, 255 / 10); // Red background

    // Clear the surface with the background color
    surface.fill_rect(None, background_color).unwrap();

    // Initialize the ScreenCoordinator using the `new` method
    let coordinator = ScreenCoordinator::new(window_width as i32, window_height as i32);

    // Draw a triangle using OpenGL-like coordinates
    draw_triangle(&mut surface, (0.0, 0.5), (0.5, -0.5), (-0.5, -0.5), Color::RGB(255, 85, 0), &coordinator);

    // Create a canvas to present the surface to the window
    let mut canvas = window.into_canvas().software().build().unwrap();

    // Convert the surface to a texture
    let texture_creator = canvas.texture_creator();
    let texture = surface.as_texture(&texture_creator).unwrap();

    // Present the texture in the window
    canvas.copy(&texture, None, None).unwrap();
    canvas.present();

    // Wait for a quit event
    let mut event_pump = sdl_context.event_pump().unwrap();
    loop {
        for event in event_pump.poll_iter() {
            use sdl2::event::Event;
            use sdl2::keyboard::Keycode;

            match event {
                Event::Quit {..} |
                Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
                    return;
                },
                _ => {}
            }
        }
    }
}

But I tried to fill triangle -> It doesn't show me. How do I fix it? if I change draw_triangle to draw_filled_triangle and build with cargo and it doesn't see triangle.

I hope you have to help me if you have experience of Software Rasterization. Thank you for supporting me!

I explain about terms:
ScreenCoordinator it works like OpenGL's coordinator like website LearningOpenGL - You know that.
Lined triangle - it draws only 3 lines into naked triangle.
Filled triangle - it draws a filled shape triangle like OpenGL's shape.
I hope you understand my English. Sorry my bad English!
Greeting from Germany :slight_smile:

Do you know how do I get filled triangle from pixels by SDL_Surface ( Surface ).

// EDIT I got successful "Filled Triangle"

// Function to fill a triangle
fn fill_triangle(
    surface: &mut Surface, 
    p1: (f32, f32), 
    p2: (f32, f32), 
    p3: (f32, f32), 
    color: Color, 
    coordinator: &ScreenCoordinator
) {
    let width = surface.width() as i32;
    let height = surface.height() as i32;
    let pitch = surface.pitch() as i32;

    let pixels = surface.without_lock_mut().unwrap();

    // Convert points to screen coordinates
    let (x1, y1) = coordinator.to_screen_coords(p1.0, p1.1);
    let (x2, y2) = coordinator.to_screen_coords(p2.0, p2.1);
    let (x3, y3) = coordinator.to_screen_coords(p3.0, p3.1);

    // Sort the vertices by y-coordinate ascending (y1 <= y2 <= y3)
    let (x1, y1, x2, y2, x3, y3) = if y1 > y2 {
        if y2 > y3 {
            (x3, y3, x2, y2, x1, y1)
        } else if y1 > y3 {
            (x2, y2, x3, y3, x1, y1)
        } else {
            (x2, y2, x1, y1, x3, y3)
        }
    } else {
        if y1 > y3 {
            (x3, y3, x1, y1, x2, y2)
        } else if y2 > y3 {
            (x1, y1, x3, y3, x2, y2)
        } else {
            (x1, y1, x2, y2, x3, y3)
        }
    };

    // Helper function to draw a horizontal line
    let mut draw_horizontal_line = |y: i32, x_min: i32, x_max: i32| {
        for x in x_min..=x_max {
            if x >= 0 && x < width && y >= 0 && y < height {
                let offset = (y * pitch + x * 3) as usize;
                pixels[offset] = color.r;
                pixels[offset + 1] = color.g;
                pixels[offset + 2] = color.b;
            }
        }
    };

    // Fill bottom flat triangle
    if y2 != y1 {
        for y in y1..=y2 {
            let alpha = (y - y1) as f32 / (y2 - y1) as f32;
            let beta = (y - y1) as f32 / (y3 - y1) as f32;

            let x_start = x1 + ((x2 - x1) as f32 * alpha) as i32;
            let x_end = x1 + ((x3 - x1) as f32 * beta) as i32;

            draw_horizontal_line(y, x_start.min(x_end), x_start.max(x_end));
        }
    }

    // Fill top flat triangle
    if y3 != y2 {
        for y in y2..=y3 {
            let alpha = (y - y2) as f32 / (y3 - y2) as f32;
            let beta = (y - y1) as f32 / (y3 - y1) as f32;

            let x_start = x2 + ((x3 - x2) as f32 * alpha) as i32;
            let x_end = x1 + ((x3 - x1) as f32 * beta) as i32;

            draw_horizontal_line(y, x_start.min(x_end), x_start.max(x_end));
        }
    }
}

And example:

fill_triangle(&mut surface, (0.0, 0.5), (0.5, -0.5), (-0.5, -0.5), Color::RGB(255, 85, 0), &coordinator);

And I continue more details for Software Rasterization

2 Likes

Update: Change to "Rust SDL2: Filled Rectangle"

I will continue to different colors :slight_smile:

1 Like

And colorful rectangle :slight_smile:

// Update I found fixed solution.

fn barycentric(
    p: (i32, i32),
    p1: (i32, i32),
    p2: (i32, i32),
    p3: (i32, i32),
) -> (f32, f32, f32) {
    let denominator = (p2.1 - p3.1) as f32 * (p1.0 - p3.0) as f32
        + (p3.0 - p2.0) as f32 * (p1.1 - p3.1) as f32;

    let lambda1 = ((p2.1 - p3.1) as f32 * (p.0 - p3.0) as f32
        + (p3.0 - p2.0) as f32 * (p.1 - p3.1) as f32)
        / denominator;

    let lambda2 = ((p3.1 - p1.1) as f32 * (p.0 - p3.0) as f32
        + (p1.0 - p3.0) as f32 * (p.1 - p3.1) as f32)
        / denominator;

    let lambda3 = 1.0 - lambda1 - lambda2;

    (lambda1, lambda2, lambda3)
}

// Interpolates color using barycentric coordinates
fn interpolate_color_barycentric(
    lambda: (f32, f32, f32),
    c1: Color,
    c2: Color,
    c3: Color,
) -> Color {
    Color::RGB(
        (c1.r as f32 * lambda.0 + c2.r as f32 * lambda.1 + c3.r as f32 * lambda.2) as u8,
        (c1.g as f32 * lambda.0 + c2.g as f32 * lambda.1 + c3.g as f32 * lambda.2) as u8,
        (c1.b as f32 * lambda.0 + c2.b as f32 * lambda.1 + c3.b as f32 * lambda.2) as u8,
    )
}

// Function to fill a triangle with vertex colors
fn fill_triangle_with_colors(
    surface: &mut Surface, 
    p1: (f32, f32, Color), 
    p2: (f32, f32, Color), 
    p3: (f32, f32, Color), 
    coordinator: &ScreenCoordinator
) {
    let width = surface.width() as i32;
    let height = surface.height() as i32;
    let pitch = surface.pitch() as i32;

    let pixels = surface.without_lock_mut().unwrap();

    // Convert points to screen coordinates
    let p1 = (coordinator.to_screen_coords(p1.0, p1.1), p1.2);
    let p2 = (coordinator.to_screen_coords(p2.0, p2.1), p2.2);
    let p3 = (coordinator.to_screen_coords(p3.0, p3.1), p3.2);

    // Bounding box for the triangle
    let min_x = p1.0.0.min(p2.0.0).min(p3.0.0).max(0);
    let max_x = p1.0.0.max(p2.0.0).max(p3.0.0).min(width - 1);
    let min_y = p1.0.1.min(p2.0.1).min(p3.0.1).max(0);
    let max_y = p1.0.1.max(p2.0.1).max(p3.0.1).min(height - 1);

    for y in min_y..=max_y {
        for x in min_x..=max_x {
            let lambda = barycentric((x, y), p1.0, p2.0, p3.0);
            if lambda.0 >= 0.0 && lambda.1 >= 0.0 && lambda.2 >= 0.0 {
                let color = interpolate_color_barycentric(lambda, p1.1, p2.1, p3.1);
                let offset = (y * pitch + x * 3) as usize;
                pixels[offset] = color.r;
                pixels[offset + 1] = color.g;
                pixels[offset + 2] = color.b;
            }
        }
    }
}

in fn main

// Draw the first triangle with red, blue, and green vertices
fill_triangle_with_colors(
    &mut surface,
    (0.5, 0.5, Color::RGB(255, 0, 0)),  // Red - Top Right
    (0.5, -0.5, Color::RGB(0, 0, 255)), // Blue - Bottom Right <- FIXED!
    (-0.5, -0.5, Color::RGB(0, 255, 0)),// Green - Bottom Left <- FIXED!
    &coordinator
);

// Draw the second triangle with red, green, and yellow vertices
fill_triangle_with_colors(
    &mut surface,
    (0.5, 0.5, Color::RGB(255, 0, 0)),   // Red - Top Right
    (-0.5, -0.5, Color::RGB(0, 255, 0)),  // Green - Bottom Left
    (-0.5, 0.5, Color::RGB(255, 255, 0)),// Yellow - Top Left
    &coordinator
);

Now I got fixed solution. Buh!

1 Like

Yay I have got successful with loading texture :smiley:

fn fill_triangle_with_uv(
    surface: &mut Surface, 
    texture: &Surface, 
    p1: (f32, f32, f32, f32), // (x, y, u, v)
    p2: (f32, f32, f32, f32),
    p3: (f32, f32, f32, f32),
    coordinator: &ScreenCoordinator
) {
    let width = surface.width() as i32;
    let height = surface.height() as i32;
    let pitch = surface.pitch() as i32;

    let surface_pixels = surface.without_lock_mut().unwrap();
    let texture_pixels = texture.without_lock().unwrap();

    let tex_width = texture.width() as i32;
    let tex_height = texture.height() as i32;

    // Convert points to screen coordinates
    let p1 = (coordinator.to_screen_coords(p1.0, p1.1), (p1.2, p1.3));
    let p2 = (coordinator.to_screen_coords(p2.0, p2.1), (p2.2, p2.3));
    let p3 = (coordinator.to_screen_coords(p3.0, p3.1), (p3.2, p3.3));

    // Bounding box for the triangle
    let min_x = p1.0.0.min(p2.0.0).min(p3.0.0).max(0);
    let max_x = p1.0.0.max(p2.0.0).max(p3.0.0).min(width - 1);
    let min_y = p1.0.1.min(p2.0.1).min(p3.0.1).max(0);
    let max_y = p1.0.1.max(p2.0.1).max(p3.0.1).min(height - 1);

    for y in min_y..=max_y {
        for x in min_x..=max_x {
            let lambda = barycentric((x, y), p1.0, p2.0, p3.0);
            if lambda.0 >= 0.0 && lambda.1 >= 0.0 && lambda.2 >= 0.0 {
                let u = lambda.0 * p1.1.0 + lambda.1 * p2.1.0 + lambda.2 * p3.1.0;
                let v = lambda.0 * p1.1.1 + lambda.1 * p2.1.1 + lambda.2 * p3.1.1;

                let tex_x = (u * tex_width as f32).clamp(0.0, (tex_width - 1) as f32) as i32;
                let tex_y = (v * tex_height as f32).clamp(0.0, (tex_height - 1) as f32) as i32;

                let tex_offset = (tex_y * tex_width + tex_x) as usize * 4;
                let offset = (y * pitch + x * 3) as usize;

                surface_pixels[offset] = texture_pixels[tex_offset];
                surface_pixels[offset + 1] = texture_pixels[tex_offset + 1];
                surface_pixels[offset + 2] = texture_pixels[tex_offset + 2];
            }
        }
    }
}

And in fn main
After let mut surface ....

let texture_path = Path::new("tex.png");
let texture_surface = Surface::from_file(texture_path).unwrap();

After let coordinator ....

// Draw the first triangle with texture
fill_triangle_with_uv(
    &mut surface,
    &texture_surface,
    (0.5, 0.5, 1.0, 1.0),  
    (0.5, -0.5, 1.0, 0.0), 
    (-0.5, -0.5, 0.0, 0.0),
    &coordinator
);

// Draw the second triangle with texture
fill_triangle_with_uv(
    &mut surface,
    &texture_surface,
    (0.5, 0.5, 1.0, 1.0),   
    (-0.5, -0.5, 0.0, 0.0), 
    (-0.5, 0.5, 0.0, 1.0),
    &coordinator
);

Download if you want to get my homemade checker ( tex.png )
tex.png

Enjoy your software rendering!

1 Like

Woohooo! Transformation ( Moving Rectangle )

Check out Youtube!

I have changed from after let mut surface ....

// Translation variables
let mut translate_x = 0.0f32;
let mut direction = 1.0f32;
let speed = 0.5f32;
let max_translation = 1.0f32;

// Timer to keep track of the frame time
let mut last_time = Instant::now();

All into loop

loop {
    for event in event_pump.poll_iter() {
        use sdl2::event::Event;
        use sdl2::keyboard::Keycode;

        match event {
            Event::Quit {..} |
            Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
                return;
            },
            _ => {}
        }
    }
    
    // Calculate delta time
    let current_time = Instant::now();
    let delta_time = current_time.duration_since(last_time).as_secs_f32();
    last_time = current_time;

    // Update translation
    translate_x += direction * speed * delta_time;

    // Check for boundary and reverse direction if necessary
    if translate_x.abs() > max_translation {
        direction *= -1.0;
    }

    // Clear the surface with the background color
    surface.fill_rect(None, background_color).unwrap();
    // Apply the translation to the triangle and draw it
    fill_triangle_with_uv(
        &mut surface,
        &texture_surface,
        (0.5 + translate_x, 0.5, 1.0, 1.0),  // Top Right
        (0.5 + translate_x, -0.5, 1.0, 0.0), // Bottom Right
        (-0.5 + translate_x, -0.5, 0.0, 0.0), // Bottom Left
        &coordinator
    );

    fill_triangle_with_uv(
        &mut surface,
        &texture_surface,
        (0.5 + translate_x, 0.5, 1.0, 1.0),   // Top Right
        (-0.5 + translate_x, -0.5, 0.0, 0.0), // Bottom Left
        (-0.5 + translate_x, 0.5, 0.0, 1.0), // Top Left
        &coordinator
    );

    let texture = surface.as_texture(&texture_creator).unwrap();

    // Present the texture in the window
    canvas.copy(&texture, None, None).unwrap();
    canvas.present();
}

That is all. I am very happy because I get successful this. I would like to explain YouTube near future.

Enjoy and happy coding!

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.