ðŸ”Ū :: Marble Portrait

BamgasiJM·2025년 10ė›” 17ėž

Nannou <Generative Art>

ëŠĐ록 ëģīęļ°
47/55
post-thumbnail

📝 Rust Code

use nannou::prelude::*;
use nannou::image::{DynamicImage, Luma};
use rand::seq::SliceRandom;
use rand::Rng;

// ė„Īė •ę°’
const WINDOW_W: u32 = 1080;
const WINDOW_H: u32 = 1080;
const PARTICLE_RADIUS: f32 = 5.0;
const MAX_PARTICLES: usize = 4000;
const SAMPLE_STEP: usize = 1;
const THRESHOLD: u8 = 170;
const TARGET_IMAGE: &str = "demon_bird.jpg";

// ė„ąëŠĨ ėĩœė í™”
const GRID_CELL_SIZE: f32 = 20.0;
const K_SPRING: f32 = 80.0;
const DAMPING: f32 = 0.9;
const MAX_ACC: f32 = 2000.0;
const COLLISION_DAMPING: f32 = 0.6;
const BOUND_DAMPING: f32 = -0.4;

fn main() {
    nannou::app(model).update(update).run();
}

#[derive(Clone, Copy)]
struct Particle {
    pos: Vec2,
    vel: Vec2,
    acc: Vec2,
    radius: f32,
    target: Vec2,
    color: Srgb<u8>,
}

struct SpatialGrid {
    cell_size: f32,
    grid: std::collections::HashMap<(i32, i32), Vec<usize>>,
}

impl SpatialGrid {
    fn new(cell_size: f32) -> Self {
        SpatialGrid {
            cell_size,
            grid: std::collections::HashMap::new(),
        }
    }

    fn get_cell(&self, pos: Vec2) -> (i32, i32) {
        (
            (pos.x / self.cell_size).floor() as i32,
            (pos.y / self.cell_size).floor() as i32,
        )
    }

    fn build(&mut self, particles: &[Particle]) {
        self.grid.clear();
        for (i, p) in particles.iter().enumerate() {
            let cell = self.get_cell(p.pos);
            self.grid.entry(cell).or_insert_with(Vec::new).push(i);
        }
    }

    fn get_neighbors(&self, pos: Vec2) -> Vec<usize> {
        let (cx, cy) = self.get_cell(pos);
        let mut neighbors = Vec::new();

        for dx in -1..=1 {
            for dy in -1..=1 {
                if let Some(indices) = self.grid.get(&(cx + dx, cy + dy)) {
                    neighbors.extend(indices);
                }
            }
        }
        neighbors
    }
}

struct Model {
    particles: Vec<Particle>,
    spatial_grid: SpatialGrid,
    _window: window::Id,
    bg_color: Srgb<u8>,
}

fn model(app: &App) -> Model {
    let _window = app
        .new_window()
        .size(WINDOW_W, WINDOW_H)
        .view(view)
        .build()
        .unwrap();

    let assets = app.assets_path().expect("failed to find assets folder");
    let img_path = assets.join(TARGET_IMAGE);
    let dyn_img = nannou::image::open(&img_path).expect("failed to load image from assets");

    let mut target_points = mask_points_from_image(&dyn_img, SAMPLE_STEP, THRESHOLD, WINDOW_W, WINDOW_H);

    let mut rng = rand::thread_rng();
    target_points.shuffle(&mut rng);
    if target_points.len() > MAX_PARTICLES {
        target_points.truncate(MAX_PARTICLES);
    }

    let particles = target_points
        .iter()
        .map(|&t| {
            let pos = vec2(
                rng.gen_range(-(WINDOW_W as f32) / 2.0..(WINDOW_W as f32) / 2.0),
                rng.gen_range(-(WINDOW_H as f32) / 2.0..(WINDOW_H as f32) / 2.0),
            );
            Particle {
                pos,
                vel: vec2(0.0, 0.0),
                acc: vec2(0.0, 0.0),
                radius: PARTICLE_RADIUS,
                target: t,
                color: random_color_like(&mut rng),
            }
        })
        .collect();

    Model {
        particles,
        spatial_grid: SpatialGrid::new(GRID_CELL_SIZE),
        _window,
        bg_color: srgb8(245, 223, 210),
    }
}

fn mask_points_from_image(
    img: &DynamicImage,
    step: usize,
    threshold: u8,
    window_w: u32,
    window_h: u32,
) -> Vec<Vec2> {
    let gray = img.to_luma8();
    let (w, h) = gray.dimensions();

    let sx = window_w as f32 / w as f32;
    let sy = window_h as f32 / h as f32;
    let scale = sx.min(sy);

    let img_width_on_screen = (w as f32) * scale;
    let img_height_on_screen = (h as f32) * scale;

    let left = -img_width_on_screen / 2.0;
    let bottom = -img_height_on_screen / 2.0;

    let mut points = Vec::new();

    for y in (0..h).step_by(step) {
        for x in (0..w).step_by(step) {
            let Luma([v]) = gray.get_pixel(x, y);
            if *v < threshold {
                let fx = left + (x as f32 + 0.5) * scale;
                let fy = bottom + ((h - y) as f32 + 0.5) * scale;
                points.push(vec2(fx, fy));
            }
        }
    }

    points
}

fn random_color_like<R: Rng>(rng: &mut R) -> Srgb<u8> {
    const PALETTE: [(u8, u8, u8); 6] = [
        (233, 94, 105),
        (234, 148, 129),
        (193, 64, 90),
        (166, 102, 70),
        (224, 130, 149),
        (183, 45, 82),
    ];
    let (r, g, b) = PALETTE[rng.gen_range(0..PALETTE.len())];
    srgb8(r, g, b)
}

#[inline]
fn apply_force(particles: &mut [Particle]) {
    for p in particles.iter_mut() {
        let to_target = p.target - p.pos;
        let dist_sq = to_target.length_squared();
        
        if dist_sq > 0.0001 {
            let dist = dist_sq.sqrt();
            p.acc = (to_target / dist) * (K_SPRING * dist);
        } else {
            p.acc = Vec2::ZERO;
        }
    }
}

#[inline]
fn clamp_acceleration(particles: &mut [Particle]) {
    let max_acc_sq = MAX_ACC * MAX_ACC;
    for p in particles.iter_mut() {
        let acc_sq = p.acc.length_squared();
        if acc_sq > max_acc_sq {
            p.acc = p.acc.normalize() * MAX_ACC;
        }
    }
}

#[inline]
fn integrate(particles: &mut [Particle], dt: f32) {
    for p in particles.iter_mut() {
        p.vel += p.acc * dt;
        p.vel *= DAMPING;
        p.pos += p.vel * dt;
    }
}

fn resolve_collisions(particles: &mut [Particle], spatial_grid: &mut SpatialGrid) {
    spatial_grid.build(particles);

    let mut collision_pairs = Vec::new();
    
    for i in 0..particles.len() {
        let pos = particles[i].pos;
        let neighbors = spatial_grid.get_neighbors(pos);

        for &j in &neighbors {
            if i >= j {
                continue;
            }

            let delta = particles[j].pos - particles[i].pos;
            let dist_sq = delta.length_squared();
            let min_dist = particles[i].radius + particles[j].radius;
            let min_dist_sq = min_dist * min_dist;

            if dist_sq > 0.0001 && dist_sq < min_dist_sq {
                collision_pairs.push((i, j, delta, dist_sq.sqrt()));
            }
        }
    }

    for (i, j, delta, dist) in collision_pairs {
        let overlap = 0.5 * (particles[i].radius + particles[j].radius - dist + 0.0001);
        let ndelta = delta / dist;
        particles[i].pos -= ndelta * overlap;
        particles[j].pos += ndelta * overlap;

        let rel_vel = particles[j].vel - particles[i].vel;
        let sep_vel = rel_vel.dot(ndelta);
        if sep_vel < 0.0 {
            let impulse = -sep_vel * COLLISION_DAMPING;
            let imp_vec = ndelta * impulse * 0.5;
            particles[i].vel -= imp_vec;
            particles[j].vel += imp_vec;
        }
    }
}

#[inline]
fn resolve_bounds(particles: &mut [Particle]) {
    let half_w = WINDOW_W as f32 / 2.0;
    let half_h = WINDOW_H as f32 / 2.0;

    for p in particles.iter_mut() {
        if p.pos.x < -half_w + p.radius {
            p.pos.x = -half_w + p.radius;
            p.vel.x *= BOUND_DAMPING;
        } else if p.pos.x > half_w - p.radius {
            p.pos.x = half_w - p.radius;
            p.vel.x *= BOUND_DAMPING;
        }
        if p.pos.y < -half_h + p.radius {
            p.pos.y = -half_h + p.radius;
            p.vel.y *= BOUND_DAMPING;
        } else if p.pos.y > half_h - p.radius {
            p.pos.y = half_h - p.radius;
            p.vel.y *= BOUND_DAMPING;
        }
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let dt = app
        .duration
        .since_prev_update
        .as_secs_f32()
        .min(0.033);

    apply_force(&mut model.particles);
    clamp_acceleration(&mut model.particles);
    integrate(&mut model.particles, dt);
    resolve_collisions(&mut model.particles, &mut model.spatial_grid);
    resolve_bounds(&mut model.particles);
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.background().color(model.bg_color);

    for p in model.particles.iter() {
        draw.ellipse()
            .xy(p.pos)
            .radius(p.radius)
            .color(p.color);
    }

    draw.to_frame(app, &frame).unwrap();
}
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0ę°œė˜ 댓ęļ€