๐Ÿ”ฎ :: Fluid Lattice ver.2

BamgasiJMยท2025๋…„ 9์›” 18์ผ

Nannou <Generative Art>

๋ชฉ๋ก ๋ณด๊ธฐ
20/55
post-thumbnail

๐Ÿ“ Rust Code

use nannou::prelude::*;
use std::collections::HashMap;

const GRID_SIZE: usize = 100;  // 100x100 = 10,000 ์ž…์ž
const PARTICLE_SPACING: f32 = 8.0;
const VOID_RADIUS: f32 = 100.0;
const REPULSION_FORCE: f32 = 2.0;
const DAMPING: f32 = 0.95;

// ๊ณต๊ฐ„ ๊ทธ๋ฆฌ๋“œ ์…€ ํฌ๊ธฐ (void ๋ฐ˜๊ฒฝ๋ณด๋‹ค ์•ฝ๊ฐ„ ์ž‘๊ฒŒ)
const GRID_CELL_SIZE: f32 = VOID_RADIUS * 0.8;

#[derive(Clone, Copy)]
struct Particle {
    position: Vec2,
    velocity: Vec2,
    home_position: Vec2,
}

struct Model {
    particles: Vec<Particle>,
    window_rect: Rect,
}

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

fn model(app: &App) -> Model {
    let window = app.new_window().size(1000, 1000).view(view).build().unwrap();
    let window_rect = app.window(window).unwrap().rect();

    let mut particles = Vec::with_capacity(GRID_SIZE * GRID_SIZE);
    let start_x = - ((GRID_SIZE as f32 - 1.0) / 2.0) * PARTICLE_SPACING;
    let start_y = - ((GRID_SIZE as f32 - 1.0) / 2.0) * PARTICLE_SPACING;

    for i in 0..GRID_SIZE {
        for j in 0..GRID_SIZE {
            let pos_x = start_x + i as f32 * PARTICLE_SPACING;
            let pos_y = start_y + j as f32 * PARTICLE_SPACING;
            let pos = vec2(pos_x, pos_y);
            particles.push(Particle {
                position: pos,
                velocity: vec2(0.0, 0.0),
                home_position: pos,
            });
        }
    }

    Model {
        particles,
        window_rect,
    }
}

// ์œ„์น˜๋ฅผ ๊ทธ๋ฆฌ๋“œ ์…€ ์ธ๋ฑ์Šค๋กœ ๋ณ€ํ™˜
fn position_to_grid(pos: Vec2, window_rect: Rect) -> (i32, i32) {
    let relative_x = pos.x - window_rect.left();
    let relative_y = pos.y - window_rect.bottom();
    let grid_x = (relative_x / GRID_CELL_SIZE).floor() as i32;
    let grid_y = (relative_y / GRID_CELL_SIZE).floor() as i32;
    (grid_x, grid_y)
}

// ๋งˆ์šฐ์Šค ๋ฐ˜๊ฒฝ ๋‚ด ๊ทธ๋ฆฌ๋“œ ์…€๋“ค์„ ์ฐพ๊ธฐ
fn get_nearby_grid_cells(mouse: Vec2, window_rect: Rect) -> Vec<(i32, i32)> {
    let (center_x, center_y) = position_to_grid(mouse, window_rect);
    let radius_in_cells = (VOID_RADIUS / GRID_CELL_SIZE).ceil() as i32;
    
    let mut cells = Vec::new();
    for dx in -radius_in_cells..=radius_in_cells {
        for dy in -radius_in_cells..=radius_in_cells {
            let cell_x = center_x + dx;
            let cell_y = center_y + dy;
            // ๊ทธ๋ฆฌ๋“œ ์…€ ์ค‘์‹ฌ์—์„œ ๋งˆ์šฐ์Šค๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ ํ™•์ธ
            let cell_center = vec2(
                window_rect.left() + (cell_x as f32 + 0.5) * GRID_CELL_SIZE,
                window_rect.bottom() + (cell_y as f32 + 0.5) * GRID_CELL_SIZE,
            );
            if (cell_center - mouse).length() <= VOID_RADIUS + GRID_CELL_SIZE {
                cells.push((cell_x, cell_y));
            }
        }
    }
    cells
}

fn update(app: &App, model: &mut Model, update: Update) {
    let mouse = app.mouse.position();
    let dt = update.since_last.as_secs_f32();

    // 1. ๊ณต๊ฐ„ ๊ทธ๋ฆฌ๋“œ๋กœ ์ž…์ž ์ธ๋ฑ์‹ฑ (HashMap ์‚ฌ์šฉ)
    let mut spatial_grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
    
    for (i, p) in model.particles.iter().enumerate() {
        let (grid_x, grid_y) = position_to_grid(p.position, model.window_rect);
        spatial_grid
            .entry((grid_x, grid_y))
            .or_insert_with(Vec::new)
            .push(i);
    }

    // 2. ๊ทผ์ฒ˜ ๊ทธ๋ฆฌ๋“œ ์…€๋“ค ์ฐพ๊ธฐ
    let nearby_cells = get_nearby_grid_cells(mouse, model.window_rect);

    // 3. void ์˜์—ญ ๋‚ด ์ž…์ž๋งŒ repulsion ์ ์šฉ
    let mut affected_particles: std::collections::HashSet<usize> = std::collections::HashSet::new();
    
    for (cell_x, cell_y) in nearby_cells {
        if let Some(indices) = spatial_grid.get(&(cell_x, cell_y)) {
            for &index in indices {
                affected_particles.insert(index);
            }
        }
    }

    // ์‹ค์ œ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ์œผ๋กœ ํ•„ํ„ฐ๋ง (๊ทธ๋ฆฌ๋“œ ์˜ค๋ฒ„ํ—ค๋“œ ์ œ๊ฑฐ)
    for &index in &affected_particles {
        let p = &mut model.particles[index];
        let to_mouse = mouse - p.position;
        let dist = to_mouse.length();
        if dist < VOID_RADIUS && dist > 0.0 {
            let repulsion_dir = -to_mouse.normalize();
            let force = repulsion_dir * (REPULSION_FORCE * (1.0 - dist / VOID_RADIUS));
            p.velocity += force;
        }
    }

    // 4. ๋ชจ๋“  ์ž…์ž์— ๊ณตํ†ต ์—…๋ฐ์ดํŠธ
    for p in model.particles.iter_mut() {
        let to_home = p.home_position - p.position;
        p.velocity += to_home * 0.01;
        p.position += p.velocity * dt * 60.0;
        p.velocity *= DAMPING;
        p.position = p.position.clamp(model.window_rect.bottom_left(), model.window_rect.top_right());
    }
}

fn event(_app: &App, model: &mut Model, event: Event) {
    if let Event::WindowEvent {
        id: _,
        simple: Some(WindowEvent::KeyPressed(Key::N)),
    } = event {
        for p in model.particles.iter_mut() {
            p.position = p.home_position;
            p.velocity = vec2(0.0, 0.0);
        }
    }
}

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

    for p in &model.particles {
        draw.ellipse()
            .xy(p.position)
            .radius(1.5)
            .color(PINK);
    }

    // ๋””๋ฒ„๊น…: void ์˜์—ญ ์‹œ๊ฐํ™” (์„ ํƒ์ )
    let mouse = app.mouse.position();
    draw.ellipse()
        .xy(mouse)
        .radius(VOID_RADIUS)
        .no_fill()
        .stroke(WHITE)
        .stroke_weight(0.0);

    draw.to_frame(app, &frame).unwrap();
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€