

// =============================================================================
// 0. ๋ชจ๋ ๋ฐ ํฌ๋ ์ดํธ ์ํฌํธ (Imports)
// =============================================================================
// `nannou::prelude::*`๋ nannou ์ ํ๋ฆฌ์ผ์ด์
๊ฐ๋ฐ์ ํ์ํ ๋๋ถ๋ถ์ ๊ธฐ๋ณธ ํ์
๊ณผ ํจ์๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
// ๋ฒกํฐ(Point2, Vec2), ์์(rgba), ์ ํธ๋ฆฌํฐ ํจ์(map_range ๋ฑ)๊ฐ ํฌํจ๋ฉ๋๋ค.
use nannou::prelude::*;
// Perlin ๋
ธ์ด์ฆ ์์ฑ๊ธฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด nannou์ noise ๋ชจ๋์์ Perlin ๊ตฌ์กฐ์ฒด์ NoiseFn ํธ๋ ์ดํธ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
// Perlin์ ์์ฐ์ค๋ฌ์ด ์ฐ์์ ์ธ ๋
ธ์ด์ฆ ํจํด์ ์์ฑํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
use nannou::noise::{NoiseFn, Perlin};
// =============================================================================
// 1. ๋ฉ์ธ ํจ์ (Application Entry Point)
// =============================================================================
// Rust ํ๋ก๊ทธ๋จ์ ์ง์
์ ์
๋๋ค.
// nannou ์ฑ์ ์ด๊ธฐํํ๊ณ ์ด๋ฒคํธ ๋ฃจํ๋ฅผ ์คํํฉ๋๋ค.
fn main() {
nannou::app(model)
.update(update) // ๋งค ํ๋ ์๋ง๋ค ํธ์ถ๋ ์
๋ฐ์ดํธ ํจ์ ์ง์
.run(); // ์ฑ ์คํ ๋ฐ ์ด๋ฒคํธ ๋ฃจํ ์์
}
// =============================================================================
// 2. ํํฐํด ๊ตฌ์กฐ์ฒด ์ ์ (Particle Structure)
// =============================================================================
// `Particle`์ flow field๋ฅผ ๋ฐ๋ผ ์์ง์ด๋ ๊ฐ๋ณ ์
์๋ฅผ ํํํฉ๋๋ค.
struct Particle {
// ํํฐํด์ ํ์ฌ ์์น (2D ์ขํ)
position: Point2,
// ํํฐํด์ ์ด์ ์์น (๊ถค์ ์ ๊ทธ๋ฆฌ๊ธฐ ์ํด ํ์)
prev_position: Point2,
// ํํฐํด์ ํ์ฌ ์๋ ๋ฒกํฐ
velocity: Vec2,
// ํํฐํด์ด ํ๋ฉด ๋ฐ์ผ๋ก ๋๊ฐ๋์ง ์ฌ๋ถ๋ฅผ ์ถ์
// true์ผ ๊ฒฝ์ฐ ์ ์์น์์ ์ฌ์์๋ฉ๋๋ค.
is_out_of_bounds: bool,
}
impl Particle {
// ํํฐํด ์์ฑ์: ๋ฌด์์ ์์น์ ํํฐํด์ ์ด๊ธฐํํฉ๋๋ค.
// `bounds`๋ ํ๋ฉด์ ๊ฒฝ๊ณ๋ฅผ ๋ํ๋ด๋ Rect ๊ตฌ์กฐ์ฒด์
๋๋ค.
fn new(bounds: Rect) -> Self {
// ํ๋ฉด์ ๋ฌด์์ ์์น๋ฅผ ์์ฑํฉ๋๋ค.
// `random_range(min, max)`๋ min๊ณผ max ์ฌ์ด์ ๋ฌด์์ ๊ฐ์ ๋ฐํํฉ๋๋ค.
let x = random_range(bounds.left(), bounds.right());
let y = random_range(bounds.bottom(), bounds.top());
let position = pt2(x, y);
Particle {
position,
prev_position: position, // ์ด๊ธฐ์๋ ์ด์ ์์น์ ํ์ฌ ์์น๊ฐ ๋์ผ
velocity: vec2(0.0, 0.0), // ์ด๊ธฐ ์๋๋ 0
is_out_of_bounds: false,
}
}
// flow field์์ ํ์ ๋ฐ์ ํํฐํด์ ์
๋ฐ์ดํธํฉ๋๋ค.
// `force`๋ ๋
ธ์ด์ฆ ๊ธฐ๋ฐ์ผ๋ก ๊ณ์ฐ๋ ๋ฐฉํฅ ๋ฒกํฐ์
๋๋ค.
fn update(&mut self, force: Vec2, bounds: Rect) {
// ์ด์ ์์น๋ฅผ ํ์ฌ ์์น๋ก ์
๋ฐ์ดํธ (๊ถค์ ๊ทธ๋ฆฌ๊ธฐ์ฉ)
self.prev_position = self.position;
// ๊ฐ์๋ = ํ (๋ดํด์ ์ 2๋ฒ์น F=ma์์ ์ง๋์ 1๋ก ๊ฐ์ )
let acceleration = force;
// ์๋์ ๊ฐ์๋๋ฅผ ๋ํฉ๋๋ค (์๋ ์
๋ฐ์ดํธ)
self.velocity += acceleration;
// ์๋์ ์ ํ์ ๋ก๋๋ค (๋๋ฌด ๋น ๋ฅด๊ฒ ์์ง์ด๋ ๊ฒ์ ๋ฐฉ์ง)
// ๋ฒกํฐ์ ํฌ๊ธฐ๋ฅผ ๊ณ์ฐํ๊ณ , ์ต๋๊ฐ๋ณด๋ค ํฌ๋ฉด ์ ๊ทํ ํ ์ต๋๊ฐ์ ๊ณฑํฉ๋๋ค.
let max_speed = 2.0;
let speed = self.velocity.length();
if speed > max_speed {
self.velocity = self.velocity.normalize() * max_speed;
}
// ์์น๋ฅผ ์๋๋งํผ ์ด๋์ํต๋๋ค
self.position += self.velocity;
// ๊ฒฝ๊ณ ๊ฒ์ฌ: ํํฐํด์ด ํ๋ฉด ๋ฐ์ผ๋ก ๋๊ฐ๋์ง ํ์ธ
if self.position.x < bounds.left()
|| self.position.x > bounds.right()
|| self.position.y < bounds.bottom()
|| self.position.y > bounds.top()
{
self.is_out_of_bounds = true;
}
}
// ํํฐํด์ ์๋ก์ด ๋ฌด์์ ์์น์์ ์ฌ์์ํฉ๋๋ค.
fn reset(&mut self, bounds: Rect) {
let x = random_range(bounds.left(), bounds.right());
let y = random_range(bounds.bottom(), bounds.top());
self.position = pt2(x, y);
self.prev_position = self.position;
self.velocity = vec2(0.0, 0.0);
self.is_out_of_bounds = false;
}
}
// =============================================================================
// 3. ๋ชจ๋ธ ์ ์ (State Structure)
// =============================================================================
// `Model` ๊ตฌ์กฐ์ฒด๋ ์ ํ๋ฆฌ์ผ์ด์
์ ์ ์ฒด ์ํ๋ฅผ ์ ์ฅํฉ๋๋ค.
struct Model {
// Perlin ๋
ธ์ด์ฆ ์์ฑ๊ธฐ ์ธ์คํด์ค
perlin: Perlin,
// ํ๋ฉด์ ๊ทธ๋ฆด ๋ชจ๋ ํํฐํด๋ค์ ๋ฒกํฐ
// ํํฐํด ๊ฐ์๊ฐ ๋ง์์๋ก ๋ ์กฐ๋ฐํ ๋ผ์ธ์ด ๊ทธ๋ ค์ง๋๋ค.
particles: Vec<Particle>,
// ์๊ฐ ์คํ์
: ๋
ธ์ด์ฆ ํ๋๋ฅผ ์๊ฐ์ ๋ฐ๋ผ ๋ณํ์ํค๊ธฐ ์ํ ๋ณ์
// ์ด ๊ฐ์ด ์ฆ๊ฐํ๋ฉด ๋
ธ์ด์ฆ ํจํด์ด ์ ๋๋ฉ์ด์
๋ฉ๋๋ค.
time_offset: f64,
}
// =============================================================================
// 4. ๋ชจ๋ธ ์ด๊ธฐํ ํจ์ (Model Initialization)
// =============================================================================
// `model` ํจ์๋ ์ฑ ์์ ์ ํ ๋ฒ๋ง ํธ์ถ๋๋ฉฐ, ์ด๊ธฐ ์ํ๋ฅผ ์ค์ ํฉ๋๋ค.
fn model(app: &App) -> Model {
// ์๋ก์ด ์ฐฝ์ ์์ฑํ๊ณ ์์ฑ์ ์ค์ ํฉ๋๋ค.
app.new_window()
.size(800, 800) // ์ฐฝ ํฌ๊ธฐ: 800x800 ํฝ์
.title("Perlin Noise Flow Field") // ์ฐฝ ์ ๋ชฉ
.view(view) // ๋ ๋๋ง ํจ์ ์ง์
.build() // ์ฐฝ ๋น๋
.unwrap(); // ์ค๋ฅ ๋ฐ์ ์ ํจ๋
// Perlin ๋
ธ์ด์ฆ ์์ฑ๊ธฐ๋ฅผ ์ด๊ธฐํํฉ๋๋ค.
let perlin = Perlin::new();
// ํํฐํด ๋ฒกํฐ๋ฅผ ์์ฑํฉ๋๋ค.
let mut particles = Vec::new();
// ํ๋ฉด์ ๊ฒฝ๊ณ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
// `app.window_rect()`๋ ํ์ฌ ์ฐฝ์ ๊ฒฝ๊ณ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
let bounds = app.window_rect();
// 2000๊ฐ์ ํํฐํด์ ์์ฑํฉ๋๋ค.
// ์ด ์ซ์๋ฅผ ์กฐ์ ํ๋ฉด ๋ผ์ธ์ ๋ฐ๋๋ฅผ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
for _ in 0..2000 {
particles.push(Particle::new(bounds));
}
// ์ด๊ธฐํ๋ Model์ ๋ฐํํฉ๋๋ค.
Model {
perlin,
particles,
time_offset: 0.0, // ์๊ฐ ์คํ์
์ 0์์ ์์
}
}
// =============================================================================
// 5. ์
๋ฐ์ดํธ ํจ์ (Per-Frame Update Logic)
// =============================================================================
// `update` ํจ์๋ ๋งค ํ๋ ์๋ง๋ค ํธ์ถ๋์ด ์ ๋๋ฉ์ด์
์ ์งํ์ํต๋๋ค.
fn update(app: &App, model: &mut Model, _update: Update) {
// ํ๋ฉด ๊ฒฝ๊ณ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
let bounds = app.window_rect();
// ์๊ฐ ์คํ์
์ ์ฆ๊ฐ์์ผ ๋
ธ์ด์ฆ ํ๋๋ฅผ ๋ณํ์ํต๋๋ค.
// 0.005๋ ๋ณํ ์๋๋ฅผ ์กฐ์ ํ๋ ๊ฐ์
๋๋ค (๊ฐ์ด ํด์๋ก ๋น ๋ฅด๊ฒ ๋ณํจ).
model.time_offset += 0.005;
// ๊ฐ ํํฐํด์ ์
๋ฐ์ดํธํฉ๋๋ค.
for particle in &mut model.particles {
// ํํฐํด์ด ํ๋ฉด ๋ฐ์ผ๋ก ๋๊ฐ๋ค๋ฉด ์ฌ์์ํฉ๋๋ค.
if particle.is_out_of_bounds {
particle.reset(bounds);
}
// ํ์ฌ ํํฐํด ์์น์ ๋ํ ๋
ธ์ด์ฆ ๊ฐ์ ๊ณ์ฐํฉ๋๋ค.
// 3D ๋
ธ์ด์ฆ๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์ธ ๋ฒ์งธ ์ฐจ์์ ์๊ฐ(time_offset)์
๋๋ค.
// - x, y ์ขํ๋ฅผ 0.005๋ฐฐ๋ก ์ค์ผ์ผ๋งํ์ฌ ๋
ธ์ด์ฆ์ "์ฃผํ์"๋ฅผ ์กฐ์ ํฉ๋๋ค.
// (๊ฐ์ด ์์์๋ก ๋ ๋ถ๋๋ฝ๊ณ ํฐ ํจํด์ด ์์ฑ๋จ)
// - time_offset์ ์ถ๊ฐํ์ฌ ์๊ฐ์ ๋ฐ๋ผ ๋
ธ์ด์ฆ ํจํด์ด ๋ณํํฉ๋๋ค.
let noise_val = model.perlin.get([
particle.position.x as f64 * 0.005,
particle.position.y as f64 * 0.005,
model.time_offset,
]);
// ๋
ธ์ด์ฆ ๊ฐ์ ๊ฐ๋๋ก ๋ณํํฉ๋๋ค.
// Perlin ๋
ธ์ด์ฆ๋ -1.0 ~ 1.0 ๊ฐ์ ๋ฐํํ๋ฏ๋ก, ์ด๋ฅผ 0 ~ 2ฯ (360๋) ๋ฒ์๋ก ๋งคํํฉ๋๋ค.
// ์ด ๊ฐ๋๊ฐ ํํฐํด์ด ์ด๋ํ ๋ฐฉํฅ์ ๊ฒฐ์ ํฉ๋๋ค.
let angle = map_range(noise_val, -1.0, 1.0, 0.0, std::f64::consts::TAU);
// ๊ฐ๋๋ฅผ ์ด์ฉํด ๋จ์ ๋ฒกํฐ๋ฅผ ์์ฑํฉ๋๋ค.
// ์ผ๊ฐํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ๋๋ฅผ x, y ์ขํ๋ก ๋ณํํฉ๋๋ค.
// cos(angle)์ x ๋ฐฉํฅ, sin(angle)์ y ๋ฐฉํฅ ์ฑ๋ถ์ ์ ๊ณตํฉ๋๋ค.
// 0.1์ ๊ณฑํ์ฌ ํ์ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํฉ๋๋ค (๊ฐ์ด ํด์๋ก ๋น ๋ฅด๊ฒ ์์ง์).
let force = vec2((angle as f32).cos(), (angle as f32).sin()) * 0.1;
// ํํฐํด์ ํ์ ์ ์ฉํ์ฌ ์
๋ฐ์ดํธํฉ๋๋ค.
particle.update(force, bounds);
}
}
// =============================================================================
// 6. ๋ทฐ ํจ์ (Rendering Logic)
// =============================================================================
// `view` ํจ์๋ ๋งค ํ๋ ์๋ง๋ค ํ๋ฉด์ ๊ทธ๋ฆฝ๋๋ค.
fn view(app: &App, model: &Model, frame: Frame) {
// ๋๋ก์ฐ ์ปจํ
์คํธ๋ฅผ ์์ฑํฉ๋๋ค.
let draw = app.draw();
// ๋ฐฐ๊ฒฝ์ ๋ฐํฌ๋ช
ํ ๊ฒ์์์ผ๋ก ์ค์ ํฉ๋๋ค.
// RGBA ๋ชจ๋๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์ํ๊ฐ(0.05)์ ๋ฎ๊ฒ ์ค์ ํ์ฌ ํ์ด๋ ํจ๊ณผ๋ฅผ ๋ง๋ญ๋๋ค.
// ์ด๋ ๊ฒ ํ๋ฉด ์ด์ ํ๋ ์์ ๋ผ์ธ์ด ์์ํ ์ฌ๋ผ์ง๋ฉด์ ๊ถค์ ํจ๊ณผ๊ฐ ์๊น๋๋ค.
// ์ํ๊ฐ์ด ๋ฎ์์๋ก ๊ถค์ ์ด ๋ ๊ธธ๊ฒ ๋จ์ต๋๋ค.
draw.rect()
.x_y(0.0, 0.0)
.w_h(800.0, 800.0)
.color(rgba(0.0, 0.0, 0.0, 0.05));
// ๊ฐ ํํฐํด์ ๊ถค์ ์ ๊ทธ๋ฆฝ๋๋ค.
for particle in &model.particles {
// ํํฐํด์ ํ์ฌ ์์น์ ์ด์ ์์น ์ฌ์ด์ ์ ์ ๊ทธ๋ฆฝ๋๋ค.
// ์ด๋ ๊ฒ ํ๋ฉด ํํฐํด์ด ์ด๋ํ ๊ฒฝ๋ก๊ฐ ๋ผ์ธ์ผ๋ก ํ์๋ฉ๋๋ค.
draw.line()
.start(particle.prev_position) // ์ ์ ์์์
.end(particle.position) // ์ ์ ๋์
.weight(1.0) // ์ ์ ๋๊ป (ํฝ์
)
.color(rgba(1.0, 1.0, 1.0, 0.5)); // RGBA ๋ชจ๋: ๋ฐํฌ๋ช
ํ ํฐ์
// R=1.0 (๋นจ๊ฐ ์ต๋), G=1.0 (์ด๋ก ์ต๋), B=1.0 (ํ๋ ์ต๋) = ํฐ์
// A=0.5 (๋ฐํฌ๋ช
)
}
// ๋๋ก์ฐ ์ปจํ
์คํธ์ ๋ด์ฉ์ ํ๋ ์ ๋ฒํผ์ ๋ ๋๋งํฉ๋๋ค.
draw.to_frame(app, &frame).unwrap();
}