๐Ÿ”ฎ :: Bouncing 22000 Balls

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

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::prelude::*;
use nannou::rand::{random_f32, random_range};

const CIRCLE_RADIUS: f32 = 0.5; // Global constant for circle radius

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

struct Circle {
    position: Point2,
    velocity: Vec2,
}

struct Model {
    circles: Vec<Circle>,
    color: Rgba,
}

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

    let num_circles = 22000; // Number of circles
    let win = app.window_rect();
    let half_width = win.w() / 2.0;
    let half_height = win.h() / 2.0;
    let mut circles = Vec::with_capacity(num_circles);

    for _ in 0..num_circles {
        let position = pt2(
            random_range(-half_width + CIRCLE_RADIUS, half_width - CIRCLE_RADIUS),
            random_range(-half_height + CIRCLE_RADIUS, half_height - CIRCLE_RADIUS),
        );
        let speed = random_range(0.5, 1.0);
        let angle = random_f32() * TAU;
        let velocity = vec2(angle.cos(), angle.sin()) * speed;

        circles.push(Circle { position, velocity });
    }

    Model {
        circles,
        color: rgba(1.0, 0.5, 0.75, 1.0), // pink
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let win = app.window_rect();
    let half_width = win.w() / 2.0 - CIRCLE_RADIUS; // Adjust for radius
    let half_height = win.h() / 2.0 - CIRCLE_RADIUS;

    for circle in &mut model.circles {
        circle.position += circle.velocity;

        // Bounce off walls
        if circle.position.x.abs() > half_width {
            circle.velocity.x = -circle.velocity.x;
            circle.position.x = circle.position.x.clamp(-half_width, half_width);
        }
        if circle.position.y.abs() > half_height {
            circle.velocity.y = -circle.velocity.y;
            circle.position.y = circle.position.y.clamp(-half_height, half_height);
        }
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    let win = app.window_rect();

    // Clear to black only on first frame
    if frame.nth() == 0 {
        draw.background().color(BLACK);
    }

    // Draw semi-transparent rect for trail effect
    draw.rect()
        .wh(win.wh())
        .xy(win.xy())
        .color(rgba(0.0, 0.0, 0.0, 0.05));

    // Draw each circle
    for circle in &model.circles {
        draw.ellipse()
            .xy(circle.position)
            .radius(CIRCLE_RADIUS)
            .color(model.color);
    }

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

๐Ÿ“ Line-by-Line ๋ถ„์„

1. Nannou์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ๊ณผ ๋‚œ์ˆ˜ ์ƒ์„ฑ ํ•จ์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ์˜ด

use nannou::prelude::*;
use nannou::rand::{random_f32, random_range};
  • prelude::* : ์ž์ฃผ ์“ฐ๋Š” ํƒ€์ž…/ํ•จ์ˆ˜(์˜ˆ: Point2, Vec2, Rgba, TAU ๋“ฑ)๋ฅผ ๋ชจ๋‘ ํฌํ•จ.
  • random_f32() : 0.0~1.0 ์‚ฌ์ด์˜ ๋ถ€๋™์†Œ์ˆ˜์  ๋‚œ์ˆ˜.
  • random_range(a, b) : a ์ด์ƒ b ๋ฏธ๋งŒ์˜ ์‹ค์ˆ˜ ๋‚œ์ˆ˜.

2. ๋ชจ๋“  ์›์˜ ๋ฐ˜์ง€๋ฆ„์„ ๊ณ ์ •ํ•˜๋Š” ์ „์—ญ ์ƒ์ˆ˜ ์ƒ์„ฑ

const CIRCLE_RADIUS: f32 = 0.3; // Global constant for circle radius

3. Nannou ์•ฑ ์‹คํ–‰ ์ง„์ž…์  (main ํ•จ์ˆ˜)

fn main() { 
	nannou::app(model).update(update).run();
}
  • model: ์ดˆ๊ธฐ ์ƒํƒœ ์ƒ์„ฑ ํ•จ์ˆ˜.
  • update: ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ƒํƒœ ๊ฐฑ์‹ .
  • run(): ๋ฃจํ”„ ์‹œ์ž‘ โ†’ ์ฐฝ ์ƒ์„ฑ, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ, ๋ Œ๋”๋ง ๋ฐ˜๋ณต.

4. ๊ฐ ์›์˜ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ตฌ์กฐ์ฒด

struct Circle {
	position: Point2,
    velocity: Vec2,
}
  • position: ํ˜„์žฌ ํ™”๋ฉด ์ขŒํ‘œ (x, y).
  • velocity: ์†๋„ ๋ฒกํ„ฐ โ€” ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ด ๊ฐ’์„ position์— ๋”ํ•ด ์›€์ง์ž„ ๊ตฌํ˜„.

5. ์ „์ฒด ์•ฑ์˜ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ชจ๋ธ ๊ตฌ์กฐ์ฒด

struct Model {
	circle: Vec<Circle>,
    color: Rgba.
}
  • circles: ๋ชจ๋“  ์›์˜ ๋ฐฐ์—ด (15,000๊ฐœ).
  • color: ๋ชจ๋“  ์›์— ์ ์šฉํ•  ์ƒ‰์ƒ โ€” ๋ฐ˜ํˆฌ๋ช… ํ•‘ํฌ.

6. ๐Ÿง  model() ํ•จ์ˆ˜

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

์•ฑ ์ดˆ๊ธฐํ™” : 1000x1000 ํฌ๊ธฐ์˜ ์ฐฝ์„ ์ƒ์„ฑํ•˜๊ณ , view ํ•จ์ˆ˜๋ฅผ ๋ Œ๋”๋Ÿฌ๋กœ ์ง€์ •

  • _window: ์ƒ์„ฑ๋œ ์ฐฝ์˜ ํ•ธ๋“ค (์‚ฌ์šฉ ์•ˆ ํ•ด์„œ _ ์ ‘๋‘์‚ฌ).
  • .view(view): ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ˜ธ์ถœ๋  ๊ทธ๋ฆฌ๊ธฐ ํ•จ์ˆ˜ ์ง€์ •.
    let num_circles = 15000; // Number of circles
    let win = app.window_rect();
    let half_width = win.w() / 2.0;
    let half_height = win.h() / 2.0;
    let mut circles = Vec::with_capacity(num_circles);

์› ์ดˆ๊ธฐํ™”๋ฅผ ์œ„ํ•œ ์ค€๋น„

  • win.window_rect(): ์ฐฝ์˜ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜• ๋ฐ˜ํ™˜ โ†’ ์ค‘์‹ฌ (0,0), ํญ/๋†’์ด ๊ธฐ์ค€.
  • half_width/height: ํ™”๋ฉด ๋ฐ˜ ๋„ˆ๋น„/๋†’์ด โ€” ์›์ด ํ™”๋ฉด ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก ์ œํ•œํ•˜๊ธฐ ์œ„ํ•จ.
  • Vec::with_capacity: ๋ฉ”๋ชจ๋ฆฌ ๋ฏธ๋ฆฌ ํ• ๋‹น โ†’ ์„ฑ๋Šฅ ํ–ฅ์ƒ (๊ถŒ์žฅ).
    for _ in 0..num_circles {
        let position = pt2(
            random_range(-half_width + CIRCLE_RADIUS, half_width - CIRCLE_RADIUS),
            random_range(-half_height + CIRCLE_RADIUS, half_height - CIRCLE_RADIUS),
        );

๊ฐ ์›์˜ ์ดˆ๊ธฐ ์œ„์น˜๋ฅผ ๋ฌด์ž‘์œ„๋กœ ์„ค์ •

  • pt2(x, y): Point2 ์ƒ์„ฑ์ž.
  • + CIRCLE_RADIUS: ์›์ด ํ™”๋ฉด ๋ฐ–์œผ๋กœ ์‚์ ธ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก ์—ฌ์œ  ๊ณต๊ฐ„ ํ™•๋ณด.
        let speed = random_range(0.5, 1.0);
        let angle = random_f32() * TAU;
        let velocity = vec2(angle.cos(), angle.sin()) * speed;

๊ฐ ์›์˜ ์ดˆ๊ธฐ ์†๋„ ๋ฒกํ„ฐ(velocity) ์„ค์ • : ๋ฌด์ž‘์œ„ ๋ฐฉํ–ฅ๊ณผ ์†๋„๊ฐ’ ์ด์šฉ

  • TAU = 2ฯ€: ์›์ฃผ์œจ์˜ 2๋ฐฐ โ†’ 0~TAU๋Š” 0~360๋„.
  • vec2(cos, sin): ๋‹จ์œ„ ๋ฒกํ„ฐ โ†’ ๋ฐฉํ–ฅ๋งŒ ํ‘œํ˜„.
  • * speed: ์†๋„ ํฌ๊ธฐ ์กฐ์ ˆ (0.5~1.0).
        circles.push(Circle { position, velocity });
    }

์ƒ์„ฑ๋œ ์›์„ ๋ฒกํ„ฐ์— ์ถ”๊ฐ€

    Model {
        circles,
        color: rgba(1.0, 0.5, 0.75, 1.0), // Semi-transparent pink
    }
}

์ตœ์ข… ๋ชจ๋ธ ๋ฐ˜ํ™˜.

  • ์ƒ‰์ƒ: ๋นจ๊ฐ• 100%, ๋…น์ƒ‰ 50%, ํŒŒ๋ž‘ 75%, ์•ŒํŒŒ 100% โ†’ ๋ถˆํˆฌ๋ช… ํ•‘ํฌ
  • ํŠธ๋ ˆ์ผ ํšจ๊ณผ๋Š” ์•„๋ž˜ view์—์„œ ๋ณ„๋„๋กœ ๊ตฌํ˜„๋จ.

7. ๐Ÿ”„ update() ํ•จ์ˆ˜ โ€” ๋งค ํ”„๋ ˆ์ž„ ์ƒํƒœ ๊ฐฑ์‹ 

fn update(app: &App, model: &mut Model, _update: Update) {
    let win = app.window_rect();
    let half_width = win.w() / 2.0 - CIRCLE_RADIUS;
    let half_height = win.h() / 2.0 - CIRCLE_RADIUS;

ํ™”๋ฉด ๊ฒฝ๊ณ„ ๊ณ„์‚ฐ. ์›์˜ ๋ฐ˜์ง€๋ฆ„์„ ๋บŒ โ†’ ์›์˜ ๊ฐ€์žฅ์ž๋ฆฌ๊ฐ€ ๊ฒฝ๊ณ„์— ๋‹ฟ์„ ๋•Œ ํŠ•๊ธฐ๋„๋ก.

    for circle in &mut model.circles {
        circle.position += circle.velocity;

์œ„์น˜ ์—…๋ฐ์ดํŠธ โ€” ์†๋„๋ฅผ ๋”ํ•จ์œผ๋กœ์จ ์›€์ง์ž„ ๊ตฌํ˜„.

  • ๋ฌผ๋ฆฌํ•™์˜ x = x + v * dt์™€ ์œ ์‚ฌํ•˜๋‚˜, ์—ฌ๊ธฐ์„œ๋Š” dt=1๋กœ ๊ณ ์ • (ํ”„๋ ˆ์ž„๋‹น 1๋‹จ์œ„ ์ด๋™).
        // Bounce off walls
        if circle.position.x.abs() > half_width {
            circle.velocity.x = -circle.velocity.x;
            circle.position.x = circle.position.x.clamp(-half_width, half_width);
        }
        if circle.position.y.abs() > half_height {
            circle.velocity.y = -circle.velocity.y;
            circle.position.y = circle.position.y.clamp(-half_height, half_height);
        }
    }
}

๋ฒฝ ์ถฉ๋Œ ๊ฐ์ง€ ๋ฐ ๋ฐ˜์‚ฌ ๊ตฌํ˜„

  • .abs() > half_width: ํ™”๋ฉด ์ขŒ/์šฐ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์—ˆ๋Š”์ง€ ํ™•์ธ.
  • velocity.x = -velocity.x: x์ถ• ์†๋„ ๋ฐ˜์ „ โ†’ ํŠ•๊น€ ํšจ๊ณผ.
  • .clamp(...): ํ˜น์‹œ๋ผ๋„ ๊ฒฝ๊ณ„๋ฅผ ์‚ด์ง ๋„˜์—ˆ์„ ๊ฒฝ์šฐ ๊ฐ•์ œ๋กœ ๊ฒฝ๊ณ„ ๋‚ด๋กœ ๋‹น๊ฒจ์˜ด โ†’ ๋ˆ„์  ์˜ค์ฐจ ๋ฐฉ์ง€.

8. ๐ŸŽจ view() ํ•จ์ˆ˜ โ€” ๋ Œ๋”๋ง

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    let win = app.window_rect();

๊ทธ๋ฆฌ๊ธฐ ์ปจํ…์ŠคํŠธ์™€ ์ฐฝ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ

    // Clear to black only on first frame
    if frame.nth() == 0 {
        draw.background().color(BLACK);
    }

์ฒซ ํ”„๋ ˆ์ž„์—๋งŒ ๋ฐฐ๊ฒฝ์„ ๊ฒ€์ •์ƒ‰์œผ๋กœ ํด๋ฆฌ์–ด

  • ์ดํ›„ ํ”„๋ ˆ์ž„์—์„œ๋Š” ํด๋ฆฌ์–ดํ•˜์ง€ ์•Š๊ณ  ๋‚จ๊ฒจ์„œ trail ํšจ๊ณผ๋ฅผ ๊ตฌํ˜„ํ•จ.
    // Draw semi-transparent rect for trail effect
    draw.rect()
        .wh(win.wh())
        .xy(win.xy())
        .color(rgba(0.0, 0.0, 0.0, 0.05));

์ „์ฒด ํ™”๋ฉด์— ๊ฑฐ์˜ ํˆฌ๋ช…ํ•œ ๊ฒ€์ • ์‚ฌ๊ฐํ˜•์„ ๊ทธ๋ฆผ

  • ์•ŒํŒŒ๊ฐ’ 0.05 โ†’ ์ด์ „ ํ”„๋ ˆ์ž„์„ 5% ์ •๋„ ๋‚จ๊ธฐ๊ณ  95% ์–ด๋‘ก๊ฒŒ โ†’ ๋ถ€๋“œ๋Ÿฌ์šด ์ž”์ƒ ํšจ๊ณผ.
  • ์ด ๋ฐฉ์‹์œผ๋กœ "์ง€์šฐ์ง€ ์•Š๊ณ  ๋ง๊ทธ๋ฆฌ๋Š”" ํŠธ๋ ˆ์ผ ๊ตฌํ˜„.
    // Draw each circle
    for circle in &model.circles {
        draw.ellipse()
            .xy(circle.position)
            .radius(CIRCLE_RADIUS)
            .color(model.color);
    }

๋ชจ๋“  ์›์„ ํ˜„์žฌ ์œ„์น˜์— ๊ทธ๋ฆผ

  • .ellipse(): ์› ๋˜๋Š” ํƒ€์› ์ƒ์„ฑ โ€” ์—ฌ๊ธฐ์„  ๋ฐ˜์ง€๋ฆ„์ด ๊ฐ™์•„ ์›.
  • .radius(CIRCLE_RADIUS): ์ „์—ญ ์ƒ์ˆ˜ ์‚ฌ์šฉ โ†’ ์ผ๊ด€๋œ ํฌ๊ธฐ.
  • .color(model.color): ๋ชจ๋ธ์— ์ €์žฅ๋œ ์ƒ‰์ƒ ์‚ฌ์šฉ.
    draw.to_frame(app, &frame).unwrap();
}

๊ทธ๋ฆฐ ๋‚ด์šฉ์„ ์‹ค์ œ ํ”„๋ ˆ์ž„ ๋ฒ„ํผ์— ์ถœ๋ ฅ

  • ์‹คํŒจ ์‹œ panic โ†’ unwrap ์‚ฌ์šฉ (๊ฐ„๋‹จํ•œ ์˜ˆ์ œ์—์„  ๊ดœ์ฐฎ์Œ).

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

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