

use nannou::prelude::*;
const NUM_PARTICLES: usize = 8_000;
const MIN_DIST: f32 = 300.0;
const MAX_DIST: f32 = 600.0;
const PARTICLE_RADIUS: f32 = 1.0;
const OBSTACLE_RADIUS: f32 = 100.0;
const FORCE_MAG: f32 = 0.08;
const FRICTION: f32 = 0.99;
const ELASTICITY: f32 = 2.5;
const ENERGY_LOSS: f32 = 0.9;
const MAX_COLLISION: u32 = 15;
const STOP_VEL: f32 = 0.1;
const WINDOW_SIZE: f32 = 800.0;
fn main() {
nannou::app(model).update(update).run();
}
struct Particle {
position: Vec2,
velocity: Vec2,
collision_count: u32,
is_stopped: bool,
}
struct Model {
particles: Vec<Particle>,
}
fn model(app: &App) -> Model {
app.new_window()
.size(WINDOW_SIZE as u32, WINDOW_SIZE as u32)
.view(view)
.build()
.unwrap();
let mut particles = Vec::with_capacity(NUM_PARTICLES);
for _ in 0..NUM_PARTICLES {
let angle = random_range(0.0, TAU);
let dist = random_range(MIN_DIST, MAX_DIST);
let position = vec2(angle.cos(), angle.sin()) * dist;
particles.push(Particle {
position,
velocity: Vec2::ZERO,
collision_count: 0,
is_stopped: false,
});
}
Model { particles }
}
fn update(_app: &App, model: &mut Model, _update: Update) {
let obstacle_pos = vec2(0.0, 0.0);
for p in model.particles.iter_mut() {
if p.is_stopped {
continue;
}
let dir = (obstacle_pos - p.position).normalize_or_zero();
p.velocity += dir * FORCE_MAG;
p.velocity *= FRICTION;
p.position += p.velocity;
// ์ถฉ๋ ์ฒ๋ฆฌ ํจ์ ๋ถ๋ฆฌํ๋ฉด ๋ ๊น๋
handle_collision(p, obstacle_pos);
}
}
fn handle_collision(p: &mut Particle, obstacle_pos: Vec2) {
let to_particle = p.position - obstacle_pos;
let dist = to_particle.length();
let min_dist = OBSTACLE_RADIUS + PARTICLE_RADIUS;
if dist < min_dist {
p.collision_count += 1;
let normal = to_particle.normalize_or_zero();
p.position = obstacle_pos + normal * min_dist;
let v_dot_n = p.velocity.dot(normal);
p.velocity -= ELASTICITY * v_dot_n * normal;
p.velocity *= ENERGY_LOSS;
if p.collision_count >= MAX_COLLISION || p.velocity.length() < STOP_VEL {
p.is_stopped = true;
p.velocity = Vec2::ZERO;
}
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
let frame_no = frame.nth();
if frame_no == 0 {
draw.background().color(hsla(0.0, 0.0, 0.0, 1.0));
} else {
draw.rect()
.w_h(WINDOW_SIZE, WINDOW_SIZE)
.color(hsla(0.0, 0.0, 0.0, 0.1));
}
draw.ellipse()
.xy(pt2(0.0, 0.0))
.radius(OBSTACLE_RADIUS)
.color(hsla(0.0, 0.0, 0.0, 0.1));
for p in &model.particles {
draw.ellipse()
.xy(p.position)
.radius(PARTICLE_RADIUS)
.color(rgba(0.9, 0.9, 0.9, 0.5));
}
draw.to_frame(app, &frame).unwrap();
}
// =============================================================================
// 0. ์ ์ญ ์์ ์ ์ (Global Simulation Parameters)
// =============================================================================
// ์
์ ์์คํ
์ ๋์์ ์ ์ดํ๋ ์กฐ์ ๊ฐ๋ฅํ ํ๋ผ๋ฏธํฐ๋ค.
// ์ด ๊ฐ์ ์์ ํ๋ฉด ์๋ฎฌ๋ ์ด์
์ ์๋, ๋ฐ๋, ์ถฉ๋ ๋ฐ์ ๋ฑ์ ์ฝ๊ฒ ์คํํ ์ ์์.
/// ์ด ์
์ ์: ๋ง์์๋ก ์๊ฐ์ ์ผ๋ก ํ๋ถํ์ง๋ง ์ฑ๋ฅ ๋ถํ ์ฆ๊ฐ
const NUM_PARTICLES: usize = 8_000;
/// ์
์ ์ด๊ธฐ ๋ฐฐ์น ์ต์ ๊ฑฐ๋ฆฌ (์ค์ฌ์ ๊ธฐ์ค, ํฝ์
๋จ์)
/// ๋๋ฌด ๊ฐ๊น์ฐ๋ฉด ์์ ์ ๊ณผ๋ํ ์ถฉ๋ ๋ฐ์
const MIN_DIST: f32 = 300.0;
/// ์
์ ์ด๊ธฐ ๋ฐฐ์น ์ต๋ ๊ฑฐ๋ฆฌ (์ค์ฌ์ ๊ธฐ์ค, ํฝ์
๋จ์)
const MAX_DIST: f32 = 600.0;
/// ๊ฐ๋ณ ์
์์ ์๊ฐ์ ๋ฐ์ง๋ฆ (๋ ๋๋ง ํฌ๊ธฐ, ํฝ์
๋จ์)
const PARTICLE_RADIUS: f32 = 1.0;
/// ์ค์ ์ฅ์ ๋ฌผ(์)์ ๋ฐ์ง๋ฆ (ํฝ์
๋จ์)
const OBSTACLE_RADIUS: f32 = 100.0;
/// ์ค์ ์ฅ์ ๋ฌผ์ด ์
์์ ๊ฐํ๋ ์ธ๋ ฅ์ ์ธ๊ธฐ
/// ๊ฐ์ด ํด์๋ก ์
์๊ฐ ๋ ๋น ๋ฅด๊ฒ ์ค์ฌ์ผ๋ก ๋๋ฆผ
const FORCE_MAG: f32 = 0.08;
/// ๊ฐ์ ๋ง์ฐฐ ๊ณ์ (0.0 ~ 1.0)
/// 1.0 = ๋ง์ฐฐ ์์, 0.9 = ๋งค ํ๋ ์ 10% ๊ฐ์
const FRICTION: f32 = 0.99;
/// ์ถฉ๋ ํ์ฑ ๊ณ์ (๋ฐ๋ฐ ๊ฐ๋)
/// 1.0 = ์์ ํ์ฑ ์ถฉ๋, >1.0 = ์๋์ง ์ฆํญ (๊ณผ๋ํ ํ๊น), <1.0 = ํก์
const ELASTICITY: f32 = 2.5;
/// ์ถฉ๋ ํ ์๋์ง ์์ค ๋น์จ (0.0 ~ 1.0)
/// 1.0 = ์๋์ง ๋ณด์กด, 0.9 = 10% ์๋์ง ์์ค
const ENERGY_LOSS: f32 = 0.9;
/// ์
์๊ฐ ๋ฉ์ถ๊ธฐ ์ ๊น์ง ํ์ฉ๋๋ ์ต๋ ์ถฉ๋ ํ์
/// ์ด ํ์๋ฅผ ์ด๊ณผํ๋ฉด ์
์๋ ์ ์ง๋จ (์ฑ๋ฅ/์๊ฐ์ ์์ ํ)
const MAX_COLLISION: u32 = 15;
/// ์
์๊ฐ "์ ์งํ๋ค"๊ณ ๊ฐ์ฃผ๋๋ ์๋ ์๊ณ๊ฐ (ํฝ์
/ํ๋ ์)
/// ์ด๋ณด๋ค ๋๋ฆฌ๋ฉด ์์ง์์ด ๋์ ๋์ง ์์ผ๋ฏ๋ก ์ ์ง ์ฒ๋ฆฌ
const STOP_VEL: f32 = 0.1;
/// ์๋์ฐ ํฌ๊ธฐ (๋๋น = ๋์ด = ์ ์ฌ๊ฐํ)
const WINDOW_SIZE: f32 = 800.0;
// =============================================================================
// 1. ๋ฉ์ธ ํจ์ (Application Entry Point)
// =============================================================================
// nannou ์ ํ๋ฆฌ์ผ์ด์
์์์ .
// `model`, `update`, `view` ์ฝ๋ฐฑ์ ๋ฑ๋กํ๊ณ ์คํ.
use nannou::prelude::*;
fn main() {
nannou::app(model).update(update).run();
}
// =============================================================================
// 2. ์
์ ๊ตฌ์กฐ์ฒด ์ ์ (Particle State)
// =============================================================================
// ๊ฐ ์
์์ ๋ฌผ๋ฆฌ ์ํ๋ฅผ ์ ์ฅํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ.
struct Particle {
/// ํ์ฌ ์์น (์๋์ฐ ์ค์ฌ์ด (0,0)์ธ ์๋ ์ขํ๊ณ)
position: Vec2,
/// ํ์ฌ ์๋ (ํ๋ ์๋น ์ด๋ ๋ฒกํฐ)
velocity: Vec2,
/// ์ค์ ์ฅ์ ๋ฌผ๊ณผ์ ์ถฉ๋ ํ์
/// ์ผ์ ํ์ ์ด์ ์ถฉ๋ ์ ์
์๋ฅผ ์ ์ง์์ผ ์ฑ๋ฅ/์๊ฐ์ ์์ ํ
collision_count: u32,
/// ์
์๊ฐ ๋ ์ด์ ์์ง์ด์ง ์๋์ง ์ฌ๋ถ
/// true์ด๋ฉด ์
๋ฐ์ดํธ ๋ฐ ๋ ๋๋ง ์ต์ ํ ๊ฐ๋ฅ
is_stopped: bool,
}
// =============================================================================
// 3. ๋ชจ๋ธ ์ ์ (Simulation State)
// =============================================================================
// ์ ์ฒด ์๋ฎฌ๋ ์ด์
์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ตฌ์กฐ์ฒด.
// ๋ชจ๋ ์
์์ ์ฅ์ ๋ฌผ ์ ๋ณด๋ฅผ ํฌํจ.
struct Model {
/// ๋ชจ๋ ์
์ ์ธ์คํด์ค๋ฅผ ์ ์ฅํ๋ ๋ฒกํฐ
particles: Vec<Particle>,
}
// =============================================================================
// 4. ๋ชจ๋ธ ์ด๊ธฐํ ํจ์ (Setup Simulation)
// =============================================================================
// ์ ํ๋ฆฌ์ผ์ด์
์์ ์ ํ ๋ฒ๋ง ํธ์ถ๋จ.
// ์๋์ฐ ์์ฑ, ์
์ ์ด๊ธฐ ๋ฐฐ์น ๋ฑ์ ์ํ.
fn model(app: &App) -> Model {
// ์๋์ฐ ์์ฑ: ํฌ๊ธฐ ์ค์ ๋ฐ ๋ทฐ ์ฝ๋ฐฑ ๋ฑ๋ก
app.new_window()
.size(WINDOW_SIZE as u32, WINDOW_SIZE as u32)
.view(view)
.build()
.unwrap();
// ์
์ ๋ฒกํฐ ์ฌ์ ํ ๋น (์ฑ๋ฅ ์ต์ ํ)
let mut particles = Vec::with_capacity(NUM_PARTICLES);
// ๊ฐ ์
์๋ฅผ ์ํ์ผ๋ก ๋ฌด์์ ๋ฐฐ์น
for _ in 0..NUM_PARTICLES {
// 0 ~ 2ฯ ์ฌ์ด์ ๋ฌด์์ ๊ฐ๋ (TAU = 2ฯ)
let angle = random_range(0.0, TAU);
// MIN_DIST ~ MAX_DIST ์ฌ์ด์ ๋ฌด์์ ๊ฑฐ๋ฆฌ
let dist = random_range(MIN_DIST, MAX_DIST);
// ๊ทน์ขํ โ ์ง๊ต์ขํ ๋ณํ: (r*cosฮธ, r*sinฮธ)
let position = vec2(angle.cos(), angle.sin()) * dist;
// ์ด๊ธฐ ์๋๋ 0, ์ถฉ๋ ํ์ 0, ์ ์ง ์ํ ์๋
particles.push(Particle {
position,
velocity: Vec2::ZERO,
collision_count: 0,
is_stopped: false,
});
}
Model { particles }
}
// =============================================================================
// 5. ์
๋ฐ์ดํธ ํจ์ (Per-Frame Physics Simulation)
// =============================================================================
// ๋งค ํ๋ ์๋ง๋ค ์
์์ ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์
์ ์ํ.
// ์ธ๋ ฅ ์ ์ฉ โ ์๋/์์น ์
๋ฐ์ดํธ โ ์ถฉ๋ ์ฒ๋ฆฌ ์์๋ก ์งํ.
fn update(_app: &App, model: &mut Model, _update: Update) {
// ์ค์ ์ฅ์ ๋ฌผ์ ์์น (์๋์ฐ ์ค์ฌ)
let obstacle_pos = vec2(0.0, 0.0);
// ๋ชจ๋ ์
์์ ๋ํด ๋ฌผ๋ฆฌ ์
๋ฐ์ดํธ ์ํ
for p in model.particles.iter_mut() {
// ์ด๋ฏธ ์ ์ง๋ ์
์๋ ๊ฑด๋๋ (์ฑ๋ฅ ์ต์ ํ)
if p.is_stopped {
continue;
}
// 1. ์ค์ ์ฅ์ ๋ฌผ ๋ฐฉํฅ์ผ๋ก ์ธ๋ ฅ(force) ์ ์ฉ
// (์ฅ์ ๋ฌผ - ์
์ ์์น) = ์
์๋ฅผ ์ฅ์ ๋ฌผ ์ชฝ์ผ๋ก ๋์ด๋น๊ธฐ๋ ๋ฐฉํฅ ๋ฒกํฐ
let dir = (obstacle_pos - p.position).normalize_or_zero();
p.velocity += dir * FORCE_MAG;
// 2. ๋ง์ฐฐ(friction) ์ ์ฉ: ์๋๋ฅผ ์ฝ๊ฐ ๊ฐ์์์ผ ์์ฐ์ค๋ฌ์ด ๊ฐ์ ๊ตฌํ
p.velocity *= FRICTION;
// 3. ์์น ์
๋ฐ์ดํธ: ํ์ฌ ์๋๋งํผ ์ด๋
p.position += p.velocity;
// 4. ์ถฉ๋ ์ฒ๋ฆฌ: ์ฅ์ ๋ฌผ๊ณผ์ ์ถฉ๋ ์ฌ๋ถ ํ์ธ ๋ฐ ๋ฐ์
handle_collision(p, obstacle_pos);
}
}
// =============================================================================
// 6. ์ถฉ๋ ์ฒ๋ฆฌ ํจ์ (Collision Response Logic)
// =============================================================================
// ์
์๊ฐ ์ค์ ์ฅ์ ๋ฌผ(์)๊ณผ ์ถฉ๋ํ๋์ง ๊ฒ์ฌํ๊ณ ,
// ์ถฉ๋ ์ ์์น ๋ณด์ , ์๋ ๋ฐ์ฌ, ์๋์ง ์์ค, ์ ์ง ์กฐ๊ฑด ๋ฑ์ ์ฒ๋ฆฌ.
fn handle_collision(p: &mut Particle, obstacle_pos: Vec2) {
// ์
์ ์ค์ฌ์์ ์ฅ์ ๋ฌผ ์ค์ฌ๊น์ง์ ๋ฒกํฐ
let to_particle = p.position - obstacle_pos;
// ๋ ์ค์ฌ ์ฌ์ด์ ๊ฑฐ๋ฆฌ
let dist = to_particle.length();
// ์ถฉ๋ ํ๋จ ๊ธฐ์ค ๊ฑฐ๋ฆฌ = ์ฅ์ ๋ฌผ ๋ฐ์ง๋ฆ + ์
์ ๋ฐ์ง๋ฆ
let min_dist = OBSTACLE_RADIUS + PARTICLE_RADIUS;
// ์ถฉ๋ ๋ฐ์ ์กฐ๊ฑด: ์ค์ ๊ฑฐ๋ฆฌ < ์ต์ ํ์ฉ ๊ฑฐ๋ฆฌ
if dist < min_dist {
// ์ถฉ๋ ํ์ ์ฆ๊ฐ
p.collision_count += 1;
// ๋ฒ์ ๋ฒกํฐ: ์ฅ์ ๋ฌผ ์ค์ฌ์์ ์
์ ๋ฐฉํฅ์ผ๋ก ํฅํจ (์ถฉ๋ ๋ฉด์ ์์ง ๋ฐฉํฅ)
let normal = to_particle.normalize_or_zero();
// ์์น ๋ณด์ : ์
์๋ฅผ ์ฅ์ ๋ฌผ ํ๋ฉด์ ๋ฑ ๋ง๋๋ก ์ด๋ (์นจํฌ ๋ฐฉ์ง)
p.position = obstacle_pos + normal * min_dist;
// ์๋ ๋ฒกํฐ๋ฅผ ๋ฒ์ ๋ฐฉํฅ์ผ๋ก ํฌ์ โ ์ถฉ๋ ๋ฐฉํฅ ์ฑ๋ถ๋ง ์ถ์ถ
let v_dot_n = p.velocity.dot(normal);
// ํ์ฑ ๋ฐ์ฌ:
// v' = v - (1 + elasticity) * (vยทn) * n
// ์ฌ๊ธฐ์๋ (1 + elasticity) ๋์ `ELASTICITY`๋ฅผ ์ง์ ์ฌ์ฉ
// ELASTICITY > 1.0์ด๋ฉด ๋ฐ์ฌ ์ ์๋ ์ฆํญ (๊ณผ๋ํ ํ๊น ํจ๊ณผ)
p.velocity -= ELASTICITY * v_dot_n * normal;
// ์๋์ง ์์ค ์ ์ฉ: ์ถฉ๋ ํ ์๋ ๊ฐ์
p.velocity *= ENERGY_LOSS;
// ์ ์ง ์กฐ๊ฑด ์ฒดํฌ:
// - ์ต๋ ์ถฉ๋ ํ์ ๋๋ฌ OR
// - ์๋๊ฐ ๋งค์ฐ ๋๋ ค์ ธ ๋์ ๋์ง ์์
if p.collision_count >= MAX_COLLISION || p.velocity.length() < STOP_VEL {
p.is_stopped = true;
p.velocity = Vec2::ZERO; // ์์ ์ ์ง
}
}
}
// =============================================================================
// 7. ๋ทฐ ํจ์ (Rendering)
// =============================================================================
// ๋งค ํ๋ ์ ํ๋ฉด์ ์๋ฎฌ๋ ์ด์
๊ฒฐ๊ณผ๋ฅผ ๋ ๋๋ง.
// ๋ฐฐ๊ฒฝ ํ์ด๋ ํจ๊ณผ, ์ฅ์ ๋ฌผ, ์
์ ๋ฑ์ ๊ทธ๋ฆผ.
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
// ํ๋ ์ ๋ฒํธ ๊ฐ์ ธ์ค๊ธฐ (์ฒซ ํ๋ ์ ์ฌ๋ถ ํ์ธ์ฉ)
let frame_no = frame.nth();
if frame_no == 0 {
// ์ฒซ ํ๋ ์: ์์ ๊ฒ์ ๋ฐฐ๊ฒฝ์ผ๋ก ์ด๊ธฐํ
draw.background().color(hsla(0.0, 0.0, 0.0, 1.0));
} else {
// ์ดํ ํ๋ ์: ์ฝ๊ฐ ํฌ๋ช
ํ ๊ฒ์ ์ฌ๊ฐํ์ ์ ์ฒด ํ๋ฉด์ ๋ฎ์ด
// ์ด์ ํ๋ ์์ ํ์ ์ ์์ํ ํ๋ฆฌ๊ฒ ๋ง๋ฆ (trail ํจ๊ณผ)
draw.rect()
.w_h(WINDOW_SIZE, WINDOW_SIZE)
.color(hsla(0.0, 0.0, 0.0, 0.1)); // 10% ๋ถํฌ๋ช
๋
}
// ์ค์ ์ฅ์ ๋ฌผ ๊ทธ๋ฆฌ๊ธฐ (ํฌ๋ช
ํ ์)
draw.ellipse()
.xy(pt2(0.0, 0.0)) // ์ค์ฌ ์ขํ
.radius(OBSTACLE_RADIUS) // ๋ฐ์ง๋ฆ
.color(hsla(0.0, 0.0, 0.0, 0.1)); // ๋งค์ฐ ํฌ๋ช
ํ ๊ฒ์
// ๋ชจ๋ ์
์ ๊ทธ๋ฆฌ๊ธฐ
for p in &model.particles {
draw.ellipse()
.xy(p.position) // ์
์ ์์น
.radius(PARTICLE_RADIUS) // ์
์ ํฌ๊ธฐ
.color(rgba(0.9, 0.9, 0.9, 0.5)); // ๋ฐ์ ํ์, 50% ํฌ๋ช
}
// GPU์ ๊ทธ๋ฆฌ๊ธฐ ๋ช
๋ น ์ ์ถ
draw.to_frame(app, &frame).unwrap();
}