

use nannou::prelude::*;
use rand::prelude::*;
use rand_distr::{Normal, Distribution};
#[derive(Clone)]
struct Star {
position: Vec2,
radius: f32,
color: Srgba,
}
struct Model {
stars: Vec<Star>,
rotation_speed: f32,
trail_alpha: f32,
}
impl Model {
fn new(app: &App) -> Self {
app.new_window()
.title("Globular Star Cluster (Refactored)")
.size(1024, 1024)
.view(Self::view)
.build()
.unwrap();
const NUM_STARS: usize = 2000;
const STD_DEV: f32 = 220.0;
let mut rng = thread_rng();
let normal = Normal::new(0.0, STD_DEV).unwrap();
let stars = (0..NUM_STARS)
.map(|_| {
let x = normal.sample(&mut rng);
let y = normal.sample(&mut rng);
let pos = vec2(x, y);
let dist_from_center = pos.length();
let weight = (1.0 - (dist_from_center / (STD_DEV * 3.0)).min(1.0)).powf(2.0);
let base_radius = rng.gen_range(0.3..=2.5);
let radius = base_radius * (0.5 + 1.5 * weight);
let base_alpha = rng.gen_range(0.5..=1.0);
let alpha = base_alpha * (0.3 + 0.7 * weight);
let hue_shift = rng.gen_range(-0.05..=0.05);
let t = weight;
let color = srgba(
0.9 - 0.3 * (1.0 - t) + hue_shift,
0.9 - 0.2 * (1.0 - t),
1.0 - 0.1 * t,
alpha,
);
Star {
position: pos,
radius,
color,
}
})
.collect();
Self {
stars,
rotation_speed: 0.002,
trail_alpha: 0.1,
}
}
fn update(&mut self) {
let theta = self.rotation_speed;
let cos_t = theta.cos();
let sin_t = theta.sin();
for star in &mut self.stars {
let pos = star.position;
star.position = vec2(
pos.x * cos_t - pos.y * sin_t,
pos.x * sin_t + pos.y * cos_t,
);
}
}
fn view(app: &App, model: &Self, frame: Frame) {
let draw = app.draw();
// ์์ ํจ๊ณผ
draw.rect()
.wh(app.window_rect().wh())
.color(srgba(0.0, 0.0, 0.0, model.trail_alpha));
for star in &model.stars {
draw.ellipse()
.xy(star.position)
.radius(star.radius)
.color(star.color);
// ๊ธ๋ก์ฐ ํจ๊ณผ
draw.ellipse()
.xy(star.position)
.radius(star.radius * 1.3)
.color(srgba(
star.color.red,
star.color.green,
star.color.blue,
star.color.alpha * 0.02,
));
}
draw.to_frame(app, &frame).unwrap();
}
}
// ==========================
// nannou entry point
// ==========================
fn main() {
nannou::app(|app| Model::new(app))
.update(|_app, model, _update| model.update())
.run();
}
// Nannou์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ๋ค์ ํ ๋ฒ์ ๊ฐ์ ธ์ต๋๋ค.
// (App, Frame, draw, vec2, srgba, ์์, ๋ํ ๋ฑ)
use nannou::prelude::*;
// ๋์ ์์ฑ์ ์ํ ๊ธฐ๋ณธ ๊ธฐ๋ฅ๋ค (thread_rng, gen_range ๋ฑ)
use rand::prelude::*;
// ํ๋ฅ ๋ถํฌ (ํนํ ์ ๊ท ๋ถํฌ = ๊ฐ์ฐ์์ ๋ถํฌ)๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ํฌ๋ ์ดํธ
use rand_distr::{Normal, Distribution};
// ๊ฐ ๋ณ(Star)์ ์ํ๋ฅผ ์ ์ฅํ๋ ๊ตฌ์กฐ์ฒด
#[derive(Clone)]
struct Star {
position: Vec2, // ๋ณ์ 2D ์์น (x, y ์ขํ)
radius: f32, // ๋ณ์ ๋ฐ์ง๋ฆ (ํฌ๊ธฐ)
color: Srgba, // ๋ณ์ ์์ (RGBA, ์ํ ์ฑ๋ ํฌํจ)
}
// ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒด ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ชจ๋ธ ๊ตฌ์กฐ์ฒด
struct Model {
stars: Vec<Star>, // ํ๋ฉด์ ๊ทธ๋ฆด ๋ชจ๋ ๋ณ์ ๋ชฉ๋ก
rotation_speed: f32, // ์ ์ฒด ๋ณ ์งํฉ์ ํ์ ์๋ (๋ผ๋์/ํ๋ ์ ๋จ์)
trail_alpha: f32, // ์์ ํจ๊ณผ๋ฅผ ์ํ ๊ฒ์์ ๋ ์ด์ด์ ํฌ๋ช
๋ (0.0 ~ 1.0)
}
// Model ๊ตฌ์กฐ์ฒด์ ๋ํ ๋ฉ์๋ ๊ตฌํ
impl Model {
// ์ ํ๋ฆฌ์ผ์ด์
์์ ์ ํ ๋ฒ๋ง ํธ์ถ๋๋ ์ด๊ธฐํ ํจ์
fn new(app: &App) -> Self {
// Nannou ์ฐฝ์ ์ค์ ํ๊ณ ์์ฑํฉ๋๋ค.
app.new_window()
.title("Globular Star Cluster (Refactored)") // ์ฐฝ ์ ๋ชฉ
.size(1024, 1024) // ์ฐฝ ํฌ๊ธฐ: 1024x1024 ํฝ์
.view(Self::view) // ๋ ๋๋ง์ Self::view ๋ฉ์๋ ์ฌ์ฉ
.build()
.unwrap(); // ์ฐฝ ์์ฑ ์คํจ ์ panic (๊ฐ๋จํ ์์ ์์๋ ํ์ฉ)
// ์์ ์ ์: ๋ณ์ ์ด ๊ฐ์์ ์ ๊ท ๋ถํฌ์ ํ์คํธ์ฐจ
const NUM_STARS: usize = 2000; // ๋ณ 2000๊ฐ ์์ฑ
const STD_DEV: f32 = 220.0; // ์ ๊ท ๋ถํฌ์ ํ์คํธ์ฐจ (๋จ์: ํฝ์
)
// ํ์ฌ ์ค๋ ๋ ์ ์ฉ ๋์ ์์ฑ๊ธฐ ์์ฑ
let mut rng = thread_rng();
// ํ๊ท 0.0, ํ์คํธ์ฐจ STD_DEV์ธ ์ ๊ท ๋ถํฌ(๊ฐ์ฐ์์ ๋ถํฌ) ๊ฐ์ฒด ์์ฑ
// ์ด ๋ถํฌ์์ ์ํ๋งํ๋ฉด ์ค์ฌ(0,0) ๊ทผ์ฒ์ ๋ฐ์ง๋ ๊ฐ๋ค์ด ๋์ด
let normal = Normal::new(0.0, STD_DEV).unwrap();
// ๋ณ๋ค์ ์ ๊ท ๋ถํฌ์ ๋ฐ๋ผ ๋ฌด์์๋ก ์์ฑ
let stars = (0..NUM_STARS)
.map(|_| {
// X, Y ์ขํ๋ฅผ ๋
๋ฆฝ์ ์ผ๋ก ์ ๊ท ๋ถํฌ์์ ์ํ๋ง โ 2D ๊ฐ์ฐ์์ ๋ถํฌ ํจ๊ณผ
let x = normal.sample(&mut rng);
let y = normal.sample(&mut rng);
let pos = vec2(x, y); // Vec2 ํ์
์ผ๋ก ์์น ์์ฑ
// ์์ (0,0)์์์ ๊ฑฐ๋ฆฌ ๊ณ์ฐ
let dist_from_center = pos.length();
// ๊ฐ์ค์น(weight): ์ค์ฌ์์ ๋ฉ์ด์ง์๋ก ์์์ง (0~1)
// STD_DEV * 3.0 โ 99.7%์ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋๋ ๋ฐ๊ฒฝ (3ฯ ๊ท์น)
// .min(1.0)์ผ๋ก 1์ ๋์ง ์๋๋ก ๋ณด์ฅ
// .powf(2.0)๋ก ์ค์ฌ ๊ทผ์ฒ๊ฐ ํจ์ฌ ๋ ๊ฐ์กฐ๋๋๋ก ๋น์ ํ ์กฐ์
let weight = (1.0 - (dist_from_center / (STD_DEV * 3.0)).min(1.0)).powf(2.0);
// ๊ธฐ๋ณธ ๋ฐ์ง๋ฆ์ 0.3~2.5 ์ฌ์ด์์ ๋ฌด์์ ์ ํ
let base_radius = rng.gen_range(0.3..=2.5);
// ๊ฐ์ค์น์ ๋ฐ๋ผ ๋ฐ์ง๋ฆ ์กฐ์ : ์ค์ฌ ๋ณ์ผ์๋ก ๋ ํฌ๊ฒ
let radius = base_radius * (0.5 + 1.5 * weight);
// ๊ธฐ๋ณธ ์ํ(ํฌ๋ช
๋)๋ฅผ 0.5~1.0 ์ฌ์ด์์ ๋ฌด์์ ์ ํ
let base_alpha = rng.gen_range(0.5..=1.0);
// ๊ฐ์ค์น์ ๋ฐ๋ผ ์ํ ์กฐ์ : ์ค์ฌ ๋ณ์ผ์๋ก ๋ ๋ถํฌ๋ช
ํ๊ฒ
let alpha = base_alpha * (0.3 + 0.7 * weight);
// ์์ ์กฐ์ : ์ค์ฌ์ ํฐ์์ ๊ฐ๊น์ด ํ๋๋น, ๊ฐ์ฅ์๋ฆฌ๋ ๋ ํ๋์
// hue_shift๋ก ์ฝ๊ฐ์ ์์ ๋ณ๋์ ์ค ์์ฐ์ค๋ฌ์ ์ถ๊ฐ
let hue_shift = rng.gen_range(-0.05..=0.05);
let t = weight; // ๊ฐ๋
์ฑ์ ์ํด weight๋ฅผ t๋ก ๋ณต์ฌ
let color = srgba(
0.9 - 0.3 * (1.0 - t) + hue_shift, // ๋นจ๊ฐ: ์ค์ฌ์ผ์๋ก ๋ฐ์
0.9 - 0.2 * (1.0 - t), // ์ด๋ก
1.0 - 0.1 * t, // ํ๋: ์ค์ฌ์ผ์๋ก ์ฝ๊ฐ ์ด๋์
alpha, // ์์์ ๊ณ์ฐํ ์ํ ๊ฐ ์ฌ์ฉ
);
// Star ๊ตฌ์กฐ์ฒด ์ธ์คํด์ค ์์ฑ
Star {
position: pos,
radius,
color,
}
})
.collect(); // Iterator๋ฅผ Vec<Star>๋ก ๋ณํ
// Model ์ธ์คํด์ค ๋ฐํ
Self {
stars,
rotation_speed: 0.002, // ๋งค์ฐ ๋๋ฆฐ ํ์ ์๋ (๋ผ๋์/ํ๋ ์)
trail_alpha: 0.1, // ์์ ํจ๊ณผ์ ํฌ๋ช
๋: 10% ๋ถํฌ๋ช
}
}
// ๋งค ํ๋ ์๋ง๋ค ํธ์ถ๋์ด ๋ณ๋ค์ ์์น๋ฅผ ์
๋ฐ์ดํธ (ํ์ ์ ์ฉ)
fn update(&mut self) {
let theta = self.rotation_speed; // ํ์ ๊ฐ๋ (๋งค ํ๋ ์ ์ฆ๊ฐ๋)
let cos_t = theta.cos(); // cos(ฮธ) โ ํ์ ํ๋ ฌ ๊ณ์ฐ์ ์ํด ๋ฏธ๋ฆฌ ๊ณ์ฐ
let sin_t = theta.sin(); // sin(ฮธ)
// ๋ชจ๋ ๋ณ์ ๋ํด 2D ํ์ ๋ณํ ์ ์ฉ
for star in &mut self.stars {
let pos = star.position;
// 2D ํ์ ๊ณต์:
// x' = x*cosฮธ - y*sinฮธ
// y' = x*sinฮธ + y*cosฮธ
star.position = vec2(
pos.x * cos_t - pos.y * sin_t,
pos.x * sin_t + pos.y * cos_t,
);
}
}
// ๋งค ํ๋ ์๋ง๋ค ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ํจ์ (๋ ๋๋ง)
fn view(app: &App, model: &Self, frame: Frame) {
let draw = app.draw(); // ๊ทธ๋ฆฌ๊ธฐ ์ปจํ
์คํธ ๊ฐ์ ธ์ค๊ธฐ
// [์์ ํจ๊ณผ] ์ด์ ํ๋ ์์ ๋ด์ฉ์ ์์ ํ ์ง์ฐ์ง ์๊ณ ,
// ์ฝ๊ฐ ํฌ๋ช
ํ ๊ฒ์์ ์ฌ๊ฐํ์ ๋ฎ์ด ์์ํ ํ๋ ค์ง๊ฒ ๋ง๋ฆ
draw.rect()
.wh(app.window_rect().wh()) // ์ฐฝ ์ ์ฒด ํฌ๊ธฐ
.color(srgba(0.0, 0.0, 0.0, model.trail_alpha)); // ๊ฒ์์ + ํฌ๋ช
๋
// ๋ชจ๋ ๋ณ์ ์ํํ๋ฉฐ ๊ทธ๋ฆฌ๊ธฐ
for star in &model.stars {
// 1. ๋ณ์ ๋ณธ์ฒด ๊ทธ๋ฆฌ๊ธฐ
draw.ellipse()
.xy(star.position) // ์์น
.radius(star.radius) // ๋ฐ์ง๋ฆ
.color(star.color); // ์์ (์ํ ํฌํจ)
// 2. ๊ธ๋ก์ฐ(Glow) ํจ๊ณผ: ๋ณธ์ฒด๋ณด๋ค ์ฝ๊ฐ ํฐ ํ์์ ๋งค์ฐ ํฌ๋ช
ํ๊ฒ ๊ทธ๋ ค ๋น ๋ฒ์ง ํํ
draw.ellipse()
.xy(star.position)
.radius(star.radius * 1.3) // ์ฝ 30% ๋ ํฐ ๋ฐ์ง๋ฆ
.color(srgba(
star.color.red,
star.color.green,
star.color.blue,
star.color.alpha * 0.02, // ์ํ๋ฅผ 2%๋ก ๋ฎ์ถค โ ํฌ๋ฏธํ ๊ด์ฑ
));
}
// ๊ทธ๋ฆฐ ๋ด์ฉ์ ์ค์ ํ๋ ์ ๋ฒํผ์ ์ถ๋ ฅ
draw.to_frame(app, &frame).unwrap();
}
}
// ==========================
// Nannou ์ ํ๋ฆฌ์ผ์ด์
์ง์
์
// ==========================
fn main() {
// nannou::app()์ผ๋ก ์ฑ ์์
nannou::app(|app| Model::new(app)) // ์ด๊ธฐ ์ํ ์์ฑ
.update(|_app, model, _update| model.update()) // ๋งค ํ๋ ์ ์
๋ฐ์ดํธ
.run(); // ์ด๋ฒคํธ ๋ฃจํ ์์ ๋ฐ ์คํ
}
rand_distr = "0.4.3" ํ์
๋ฐ๋ ๊ฐ์ค์น (Distance Weighting) : ์ค์ฌ์์ ๋ฉ์ด์ง์๋ก ๋ฐ์ง๋ฆ๊ณผ ์ํ๊ฐ์ด ์ค์ด๋ฆ
์์ ๋ณํ : ์ค์ฌ๋ถ๋ ํฐ์, ์ธ๊ณฝ์ ํธ๋ฅธ์/๋
ธ๋๋น ๋๋ค ์๊ธฐ
ํธ๋ ์ผ ํจ๊ณผ (Motion Blur) : ๋งค ํ๋ ์ ์ด์ง ์ด๋์ด ๋ฐํฌ๋ช
๋ ์ด์ด๋ฅผ ๋ง๊ทธ๋ ค ๋ถ๋๋ฌ์ด ์์ ์์ฑ
๋ถ๋๋ฌ์ด ํ์ฐ ํจ๊ณผ : ์ํ๊ฐ์ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ ๊ณก์ ์ผ๋ก ๊ณ์ฐ (์: alpha = exp(-rยฒ / (2ฯยฒ)))
๐ ์ ์ฒด ์ฝ๋ ๊ฐ์
์ด ํ๋ก๊ทธ๋จ์ ๊ตฌ์์ฑ๋จ(Globular Star Cluster)์ฒ๋ผ ๋ณด์ด๋ 2D ๋ณ๋ค์ ์งํฉ์ ํ๋ฉด์ ๊ทธ๋ฆฝ๋๋ค.
๋ณ๋ค์ ๊ฐ์ฐ์์ ๋ถํฌ(์ ๊ท ๋ถํฌ)๋ฅผ ๋ฐ๋ผ ์ค์ฌ์์ ๋ฐ์ง๋์ด ๋ฐฐ์น๋ฉ๋๋ค.
์ ์ฒด ๋ณ ์งํฉ์ด ์ฒ์ฒํ ํ์ ํ๋ฉฐ, ์์ ํจ๊ณผ(trail)์ ๊ธ๋ก์ฐ(glow) ํจ๊ณผ๋ฅผ ํตํด ์๊ฐ์ ์ผ๋ก ์๋ฆ๋ต๊ฒ ํํ๋ฉ๋๋ค.
์์๊ณผ ํฌ๊ธฐ๋ ์ค์ฌ์์์ ๊ฑฐ๋ฆฌ์ ๋ฐ๋ผ ๊ฐ์ค์น(weight)๋ฅผ ์ ์ฉํด ์์ฐ์ค๋ฝ๊ฒ ๋ณํํฉ๋๋ค.
๐ฆ ์ฌ์ฉ๋ ํฌ๋ ์ดํธ ์ค๋ช
1. nannou::prelude::
Nannou ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํต์ฌ ๊ธฐ๋ฅ๋ค์ ํ ๋ฒ์ ๊ฐ์ ธ์ต๋๋ค.
App, Frame, draw, vec2, srgba, rect, ellipse ๋ฑ ๊ทธ๋ํฝ ๋ฐ ์ฑ ์ ์ด ๊ธฐ๋ฅ ํฌํจ.
2. rand::prelude::
๋์ ์์ฑ์ ์ํ rand ํฌ๋ ์ดํธ์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ.
thread_rng() ๋ฑ์ผ๋ก ์ค๋ ๋ ๋ก์ปฌ ๋์ ์์ฑ๊ธฐ๋ฅผ ์ฌ์ฉ.
3. rand_distr::{Normal, Distribution}
ํ๋ฅ ๋ถํฌ๋ฅผ ์ํ ํ์ฅ ํฌ๋ ์ดํธ.
Normal์ ์ ๊ท ๋ถํฌ(๊ฐ์ฐ์์ ๋ถํฌ)๋ฅผ ์ ์ํฉ๋๋ค.
Distribution ํธ๋ ์์ ํตํด .sample() ๋ฉ์๋๋ก ๋์๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
๐ ์ฐธ๊ณ : rand_distr์ rand์ ํ์ฅ์ผ๋ก, ๋ค์ํ ํ๋ฅ ๋ถํฌ(์ ๊ท, ๊ฐ๋ง, ๋ฒ ํ ๋ฑ)๋ฅผ ์ ๊ณตํฉ๋๋ค.
๊ณต์ ๋ฌธ์: https://docs.rs/rand_distr/latest/rand_distr/