๐Ÿ”ฎ :: Fluid Lattice ver.1

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

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::prelude::*;

const GRID_SIZE: usize = 100;  // 50x50 grid
const PARTICLE_SPACING: f32 = 10.0;  // Particle spacing (adjusted for screen size)
const VOID_RADIUS: f32 = 100.0;  // Void area radius (around mouse)
const REPULSION_FORCE: f32 = 2.0;  // Repulsion force strength
const DAMPING: f32 = 0.95;  // Velocity damping (to slow down particles)

struct Particle {
    position: Vec2,
    velocity: Vec2,
    home_position: Vec2,  // Initial position (for reset)
}

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

fn main() {
    nannou::app(model)
        .update(update)
        .event(event)  // Add event handling for key input
        .run();
}

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

    let mut particles = Vec::new();
    let start_x = - (GRID_SIZE as f32 / 2.0) * PARTICLE_SPACING;
    let start_y = - (GRID_SIZE as f32 / 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 update(app: &App, model: &mut Model, update: Update) {
    let mouse = app.mouse.position();
    let dt = update.since_last.as_secs_f32();

    for p in model.particles.iter_mut() {
        let to_mouse = mouse - p.position;
        let dist = to_mouse.length();  // Use length() instead of magnitude()

        if dist < VOID_RADIUS && dist > 0.0 {
            // Void effect: calculate direction to flee from mouse
            let repulsion_dir = -to_mouse.normalize();  // Opposite direction
            let force = repulsion_dir * (REPULSION_FORCE * (1.0 - dist / VOID_RADIUS));  // Force based on distance
            p.velocity += force;
        }

        // Pull slightly toward home position (stabilization)
        let to_home = p.home_position - p.position;
        p.velocity += to_home * 0.01;

        // Apply velocity and damping
        p.position += p.velocity * dt * 60.0;  // Frame-rate independent
        p.velocity *= DAMPING;

        // Clamp position to window boundaries (optional: keep particles on screen)
        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 {
        // Reset particles to home position when 'n' is pressed
        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);  // Black background

    for p in &model.particles {
        draw.ellipse()
            .xy(p.position)
            .radius(2.0)  // Small white dots
            .color(WHITE);
    }

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

์ฝ”๋“œ ์„ค๋ช…๊ณผ ๋ฌธ๋ฒ• ํฌ์ธํŠธ

50x50 ๊ทธ๋ฆฌ๋“œ์˜ ์ž…์ž(์ž‘์€ ํฐ ์ )๋“ค์„ ๋ธ”๋ž™ ๋ฐฐ๊ฒฝ์— ๋ฐฐ์น˜ํ•˜๊ณ , ๋งˆ์šฐ์Šค ์›€์ง์ž„์— ๋”ฐ๋ผ "void" ํšจ๊ณผ(๋งˆ์šฐ์Šค ์ฃผ์œ„ ๋ฐ˜๊ฒฝ ๋‚ด ์ž…์ž๋“ค์ด ๋„๋ง๊ฐ)๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. 'n' ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋ชจ๋“  ์ž…์ž๊ฐ€ ์ดˆ๊ธฐ ๊ทธ๋ฆฌ๋“œ ์œ„์น˜๋กœ ๋ฆฌ์…‹๋ฉ๋‹ˆ๋‹ค.

์ฃผ์š” nannou API ์‚ฌ์šฉ

  • app.mouse.position(): ๋งˆ์šฐ์Šค ์œ„์น˜ ์‹ค์‹œ๊ฐ„ ์ถ”์ . ์ธํ„ฐ๋ž™์…˜์˜ ํ•ต์‹ฌ.
  • draw.background().color(BLACK): ๋ธ”๋ž™ ๋ฐฐ๊ฒฝ ์„ค์ •.
  • draw.ellipse().xy(pos).radius(2.0).color(WHITE): ์ž‘์€ ์›(์ )์œผ๋กœ ์ž…์ž ๊ทธ๋ฆฌ๊ธฐ.
  • app.new_window().size(800, 800): ์ฐฝ ํฌ๊ธฐ ์„ค์ •. ๊ทธ๋ฆฌ๋“œ ๊ฐ„๊ฒฉ์ด ํ™”๋ฉด์— ๋งž๊ฒŒ ๋ฐฐ์น˜๋จ.
  • update.since_last.as_secs_f32(): ํ”„๋ ˆ์ž„ ๊ฐ„ ์‹œ๊ฐ„(delta time)์œผ๋กœ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜. ์‹ค์‹œ๊ฐ„ ์›€์ง์ž„ ๋ณด์žฅ.
  • event ํ•จ์ˆ˜ ์ถ”๊ฐ€: ํ‚ค ์ž…๋ ฅ ์ฒ˜๋ฆฌ. nannou์˜ Event::WindowEvent์™€ KeyPressed๋กœ 'n' ํ‚ค ๊ฐ์ง€.

Rust ๋ฌธ๋ฒ•

  • ๊ตฌ์กฐ์ฒด(Struct): Particle ๊ตฌ์กฐ์ฒด๋กœ ๊ฐ ์ž…์ž์˜ ์œ„์น˜(position), ์†๋„(velocity), ์ดˆ๊ธฐ ์œ„์น˜(home_position) ์ €์žฅ. ๋ฐ์ดํ„ฐ ์บก์Аํ™”.
  • ๋ฒกํ„ฐ(Vec2): nannou์˜ vec2(x, y)๋กœ 2D ๋ฒกํ„ฐ ์‚ฌ์šฉ. ์—ฐ์‚ฐ ์˜ˆ: to_mouse = mouse - p.position, magnitude()๋กœ ๊ฑฐ๋ฆฌ, normalize()๋กœ ๋‹จ์œ„ ๋ฒกํ„ฐ.
  • ๋ฃจํ”„์™€ ๋ฒกํ„ฐ(Vec): particles: Vec๋กœ ๋™์  ๋ฐฐ์—ด. ์ดˆ๊ธฐํ™” ์‹œ ์ค‘์ฒฉ for ๋ฃจํ”„๋กœ 50x50 ์ƒ์„ฑ. iter_mut()์œผ๋กœ mutable ๋ฐ˜๋ณต.
  • ํด๋žจํ”„(clamp): p.position.clamp(rect.bottom_left(), rect.top_right())๋กœ ํ™”๋ฉด ๊ฒฝ๊ณ„ ์ œํ•œ (์ž…์ž๊ฐ€ ํ™”๋ฉด ๋ฐ–์œผ๋กœ ์•ˆ ๋‚˜๊ฐ).
  • ์ƒ์ˆ˜(const): const GRID_SIZE: usize = 50;์ฒ˜๋Ÿผ ์ปดํŒŒ์ผ ํƒ€์ž„ ์ƒ์ˆ˜ ์ •์˜. ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ.
  • ์—ด๊ฑฐํ˜•(Enum)๊ณผ ํŒจํ„ด ๋งค์นญ: event ํ•จ์ˆ˜์—์„œ if let Event::WindowEvent { ... } = event ํŒจํ„ด์œผ๋กœ ํ‚ค ์ด๋ฒคํŠธ ํ•„ํ„ฐ๋ง. Rust์˜ ์•ˆ์ „ํ•œ ํŒจํ„ด ๋งค์นญ.
  • ํƒ€์ž… ๋ณ€ํ™˜: i as f32๋กœ usize๋ฅผ f32๋กœ ์บ์ŠคํŒ…. nannou์—์„œ ์ž์ฃผ ์‚ฌ์šฉ.

๋™์ž‘ ์›๋ฆฌ

  1. ์ดˆ๊ธฐํ™”(model): 50x50 ๊ทธ๋ฆฌ๋“œ ๊ณ„์‚ฐ. ๊ฐ ์ž…์ž์˜ ํ™ˆ ์œ„์น˜ ์ €์žฅ.
  2. ์—…๋ฐ์ดํŠธ(update): ๋งค ํ”„๋ ˆ์ž„ ๋งˆ์šฐ์Šค์™€ ์ž…์ž ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ. VOID_RADIUS ๋‚ด์ด๋ฉด ๋ฐ˜๋ฐœ๋ ฅ(repulsion) ์ ์šฉ. ์†๋„์— ๊ฐ์‡ (damping)๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฉˆ์ถค. ํ™ˆ ์œ„์น˜๋กœ ์•ฝ๊ฐ„ ๋Œ์–ด๋‹น๊ฒจ ์•ˆ์ •ํ™”.
  3. ๋ทฐ(view): ๋ธ”๋ž™ ๋ฐฐ๊ฒฝ์— ํฐ ์ ๋“ค ๋ Œ๋”๋ง.
  4. ์ด๋ฒคํŠธ(event): 'n' ํ‚ค๋กœ ๋ชจ๋“  ์ž…์ž ๋ฆฌ์…‹.
  5. ์ธํ„ฐ๋ž™์…˜: ๋งˆ์šฐ์Šค ์›€์ง์ด๋ฉด void์ฒ˜๋Ÿผ ์ž…์ž๋“ค์ด ๋ฐ€๋ ค๋‚˜๊ฐ. ๋ฉ€์–ด์ง€๋ฉด ์„œ์„œํžˆ ์›์œ„์น˜๋กœ ๋ณต๊ท€.

ํŒ๊ณผ ์ˆ˜์ • ์•„์ด๋””์–ด

  • ์„ฑ๋Šฅ: 2500๊ฐœ ์ž…์ž(50*50)๋ผ ๋ถ€๋“œ๋Ÿฌ์›€. ๋” ๋งŽ์œผ๋ฉด ์ตœ์ ํ™” ํ•„์š” (e.g., ์ฟผ๋“œํŠธ๋ฆฌ).
  • void ์‹œ๊ฐํ™”: ๋งˆ์šฐ์Šค ์ฃผ์œ„์— ํˆฌ๋ช… ์› ์ถ”๊ฐ€: draw.ellipse().xy(mouse).radius(VOID_RADIUS).color(rgba(0.0, 0.0, 0.0, 0.2));
  • PARTICLE_SPACING ์กฐ์ •์œผ๋กœ ๋ฐ€๋„ ๋ณ€๊ฒฝ. REPULSION_FORCE๋กœ ๋„๋ง ์†๋„ ํŠœ๋‹.
  • ์ฐฝ ํฌ๊ธฐ 800x800๋กœ ์„ค์ •ํ–ˆ์ง€๋งŒ, window_rect๋กœ ๋™์  ์กฐ์ •๋จ.
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

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