

use nannou::prelude::*;
use nannou::noise::{NoiseFn, Perlin};
use rand::Rng;
// === ์ค์ ์์ ===
const WIN_W: u32 = 800;
const WIN_H: u32 = 800;
const RADIUS: f32 = 200.0;
const NUM_FLARE: usize = 5000;
const CLUSTERS: usize = 12;
const TAU: f32 = std::f32::consts::TAU;
const CLUSTER_SPREAD: f32 = 0.08 * TAU;
const MIN_LEN: f32 = 30.0;
const MAX_LEN: f32 = 170.0;
const START_THICK: f32 = 0.8;
const END_THICK: f32 = 0.1;
const START_ALPHA: f32 = 0.8;
const END_ALPHA: f32 = 0.1;
const SEGMENTS: usize = 20;
// ๋
ธ์ด์ฆ ๊ด๋ จ
const NOISE_SCALE: f64 = 2.0;
const NOISE_AMPLITUDE: f32 = 10.0;
const NOISE_SPEED: f64 = 0.2;
fn main() {
nannou::app(model)
.update(update)
.view(view)
.size(WIN_W, WIN_H)
.run();
}
struct Model {
flares: Vec<Flare>,
perlin: Perlin,
time: f64,
}
struct Flare {
angle: f32,
length: f32,
seed: f64,
cluster_id: usize,
}
fn model(app: &App) -> Model {
let _window = app
.new_window()
.title("Clustered Curved Solar Flares (Animated)")
.build()
.unwrap();
let mut rng = rand::thread_rng();
let perlin = Perlin::new();
let flares = generate_flares(&mut rng);
Model {
flares,
perlin,
time: 0.0,
}
}
fn generate_flares<R: Rng>(rng: &mut R) -> Vec<Flare> {
let mut flares = Vec::with_capacity(NUM_FLARE);
let flares_per_cluster = NUM_FLARE / CLUSTERS;
for c in 0..CLUSTERS {
let cluster_angle = (c as f32) / (CLUSTERS as f32) * TAU;
let cluster_seed = rng.gen_range(0.0..1000.0);
for _ in 0..flares_per_cluster {
let angle = cluster_angle + rng.gen_range(-CLUSTER_SPREAD..CLUSTER_SPREAD);
let length = rng.gen_range(MIN_LEN..=MAX_LEN);
let seed = cluster_seed + rng.gen_range(0.0..100.0);
flares.push(Flare {
angle,
length,
seed,
cluster_id: c,
});
}
}
flares
}
fn update(_app: &App, model: &mut Model, update: Update) {
model.time += update.since_last.as_secs_f64() * NOISE_SPEED;
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(hsla(0.0, 0.0, 0.02, 1.0));
let center = pt2(0.0, 0.0);
let white = hsla(0.0, 0.0, 0.9, 1.0);
// ์ค์ฌ ์ (ํญ์ฑ)
draw.ellipse()
.no_fill()
.stroke(white)
.stroke_weight(1.0)
.xy(center)
.w_h(RADIUS * 2.0, RADIUS * 2.0);
// ํ๋ ์ด๋ค
for flare in &model.flares {
draw_flare(&draw, flare, center, &model.perlin, model.time);
}
draw.to_frame(app, &frame).unwrap();
}
fn draw_flare(draw: &Draw, flare: &Flare, center: Point2, perlin: &Perlin, time: f64) {
let dir = vec2(flare.angle.cos(), flare.angle.sin());
let perp = vec2(-dir.y, dir.x);
let base_center = center + dir * RADIUS;
for s in 0..SEGMENTS {
let t0 = s as f32 / SEGMENTS as f32;
let t1 = (s + 1) as f32 / SEGMENTS as f32;
// Perlin ๊ธฐ๋ฐ ํ์ด์ง + ์๊ฐ ์ ๋๋ฉ์ด์
let n0 = perlin.get([t0 as f64 * NOISE_SCALE, flare.seed, time]) as f32;
let n1 = perlin.get([t1 as f64 * NOISE_SCALE, flare.seed, time]) as f32;
let offset0 = perp * (n0 * NOISE_AMPLITUDE);
let offset1 = perp * (n1 * NOISE_AMPLITUDE);
let p0 = base_center + dir * (flare.length * t0) + offset0;
let p1 = base_center + dir * (flare.length * t1) + offset1;
let w0 = lerp(START_THICK, END_THICK, t0);
let w1 = lerp(START_THICK, END_THICK, t1);
let a0 = lerp(START_ALPHA, END_ALPHA, t0);
let a1 = lerp(START_ALPHA, END_ALPHA, t1);
let alpha = (a0 + a1) * 0.5;
let color = hsla(0.0, 0.0, 1.0, alpha);
let left0 = p0 + perp * (w0 * 0.5);
let right0 = p0 - perp * (w0 * 0.5);
let left1 = p1 + perp * (w1 * 0.5);
let right1 = p1 - perp * (w1 * 0.5);
draw.tri()
.points(left0, right0, left1)
.color(color);
draw.tri()
.points(right0, right1, left1)
.color(color);
}
}
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
// Nannou์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ๊ณผ ํผ๋ฆฐ ๋
ธ์ด์ฆ, ๋๋ค ์์ฑ๊ธฐ ์ฌ์ฉ์ ์ํ ๋ชจ๋ ์ํฌํธ
use nannou::prelude::*;
use nannou::noise::{NoiseFn, Perlin}; // ํผ๋ฆฐ ๋
ธ์ด์ฆ ํจ์
use rand::Rng; // ๋๋ค ์ซ์ ์์ฑ๊ธฐ ํธ๋ ์
// === ์ค์ ์์: ์ฝ๋ ์ ์ฒด์์ ์ฌ์ฉ๋๋ ๊ณ ์ ๊ฐ ์ ์ ===
// ์ฐฝ ํฌ๊ธฐ (ํฝ์
)
const WIN_W: u32 = 800;
const WIN_H: u32 = 800;
// ์ค์ฌ ์(ํญ์ฑ)์ ๋ฐ์ง๋ฆ
const RADIUS: f32 = 200.0;
// ์์ฑํ ํ๋ ์ด(ํ์ ํ๋ ์ด) ๊ฐ์
const NUM_FLARE: usize = 5000;
// ํ๋ ์ด๋ฅผ ๋ช ๊ฐ์ ํด๋ฌ์คํฐ(๋ฌด๋ฆฌ)๋ก ๋๋์ง
const CLUSTERS: usize = 12;
// ์์ฃผ์จ ร 2 (360๋) โ Nannou๋ TAU๋ฅผ ๊ธฐ๋ณธ ์์๋ก ์ ๊ณตํ์ง๋ง ๋ช
์์ ์ ์
const TAU: f32 = std::f32::consts::TAU;
// ๊ฐ ํด๋ฌ์คํฐ ๋ด์์ ํ๋ ์ด๊ฐ ํผ์ง๋ ๊ฐ๋ ๋ฒ์ (์ ์ฒด ์์ฃผ์ 8%)
const CLUSTER_SPREAD: f32 = 0.08 * TAU;
// ํ๋ ์ด ๊ธธ์ด ๋ฒ์ (ํฝ์
)
const MIN_LEN: f32 = 30.0;
const MAX_LEN: f32 = 170.0;
// ํ๋ ์ด ๋๊ป: ์์(๋ฟ๋ฆฌ)๊ณผ ๋(๋๋ถ๋ถ)์ ์ ๋๊ป
const START_THICK: f32 = 0.8; // ๋ฟ๋ฆฌ ๋ถ๋ถ์ด ๋๊บผ์
const END_THICK: f32 = 0.1; // ๋ ๋ถ๋ถ์ด ์์
// ํ๋ ์ด ํฌ๋ช
๋: ์์๊ณผ ๋์ ์ํ๊ฐ
const START_ALPHA: f32 = 0.8; // ๋ฟ๋ฆฌ ๋ถ๋ถ์ด ์ ๋ช
const END_ALPHA: f32 = 0.1; // ๋ ๋ถ๋ถ์ด ํ๋ฆฟ
// ๊ฐ ํ๋ ์ด๋ฅผ ๋ช ๊ฐ์ ์ธ๊ทธ๋จผํธ(๊ตฌ๊ฐ)๋ก ๋๋์ง โ ๊ณก์ ํํ ์ ๋ฐ๋
const SEGMENTS: usize = 20;
// ๋
ธ์ด์ฆ ์ ๋๋ฉ์ด์
๊ด๋ จ ์์
const NOISE_SCALE: f64 = 2.0; // ๋
ธ์ด์ฆ ์
๋ ฅ ์ขํ ์ค์ผ์ผ โ ๊ฐ์ด ํด์๋ก ๋ ์ธ๋ฐํ ํจํด
const NOISE_AMPLITUDE: f32 = 10.0; // ๋
ธ์ด์ฆ ์ถ๋ ฅ ์งํญ โ ํ๋ ์ด ํ์ด์ง ์ ๋
const NOISE_SPEED: f64 = 0.2; // ์๊ฐ ํ๋ฆ ์๋ โ ์ ๋๋ฉ์ด์
์๋ ์กฐ์
// ํ๋ก๊ทธ๋จ ์ง์
์
fn main() {
nannou::app(model) // ์ด๊ธฐ ์ํ ์์ฑ ํจ์
.update(update) // ๋งค ํ๋ ์ ์ํ ๊ฐฑ์
.view(view) // ๋ ๋๋ง ํจ์
.size(WIN_W, WIN_H) // ์ฐฝ ํฌ๊ธฐ ์ค์ (์ฑ ๋ ๋ฒจ์์ ์ง์ )
.run(); // ์คํ
}
// ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒด ์ํ ๊ตฌ์กฐ์ฒด
struct Model {
flares: Vec<Flare>, // ๋ชจ๋ ํ๋ ์ด ๋ฐ์ดํฐ
perlin: Perlin, // ํผ๋ฆฐ ๋
ธ์ด์ฆ ์์ฑ๊ธฐ ์ธ์คํด์ค (์ฌ์ฌ์ฉ)
time: f64, // ์ ๋๋ฉ์ด์
์๊ฐ (์ด ๋จ์)
}
// ๊ฐ๋ณ ํ๋ ์ด์ ์ ์ ์์ฑ์ ์ ์ฅํ๋ ๊ตฌ์กฐ์ฒด
// โ ์ ๋๋ฉ์ด์
์ view์์ ์ค์๊ฐ ๊ณ์ฐ๋๋ฏ๋ก, ์ฌ๊ธฐ์ ์ด๊ธฐ ์ํ๋ง ์ ์ฅ
struct Flare {
angle: f32, // ์ค์ฌ์์ ๋ป์ด๋๊ฐ๋ ๊ฐ๋ (๋ผ๋์)
length: f32, // ํ๋ ์ด ์ด ๊ธธ์ด
seed: f64, // ๋
ธ์ด์ฆ ํจํด ๊ณ ์ ์๋ณ์ (๊ฐ์ ํด๋ฌ์คํฐ ๋ด์์๋ ๋ค์์ฑ ํ๋ณด)
cluster_id: usize, // ์์ ํด๋ฌ์คํฐ ID (๋๋ฒ๊น
/ํ์ฅ์ฉ)
}
// ์ด๊ธฐํ ํจ์: ์ฐฝ ์์ฑ ๋ฐ ํ๋ ์ด ๋ฐ์ดํฐ ์ค๋น
fn model(app: &App) -> Model {
// ์ฐฝ ์์ฑ ๋ฐ ์ ๋ชฉ ์ค์
let _window = app
.new_window()
.title("Clustered Curved Solar Flares (Animated)")
.build()
.unwrap();
// ๋๋ค ์์ฑ๊ธฐ ์ธ์คํด์ค ์์ฑ (์ค๋ ๋ ์์ )
let mut rng = rand::thread_rng();
// ํผ๋ฆฐ ๋
ธ์ด์ฆ ์์ฑ๊ธฐ ์์ฑ
let perlin = Perlin::new();
// ํ๋ ์ด ๋ฐ์ดํฐ ์์ฑ
let flares = generate_flares(&mut rng);
// ์ด๊ธฐ Model ๋ฐํ
Model {
flares,
perlin,
time: 0.0, // ์๊ฐ 0๋ถํฐ ์์
}
}
// ํ๋ ์ด ๋ฐ์ดํฐ๋ฅผ ํด๋ฌ์คํฐ ๋จ์๋ก ์์ฑํ๋ ํจ์
// R: Rng โ ์ ๋ค๋ฆญ์ผ๋ก ๋๋ค ์์ฑ๊ธฐ ํ์
์ ์ถ์ํ (์ ์ฐ์ฑ ํฅ์)
fn generate_flares<R: Rng>(rng: &mut R) -> Vec<Flare> {
// ๋ฉ๋ชจ๋ฆฌ ์ฌ์ ํ ๋น (์ฑ๋ฅ ์ต์ ํ)
let mut flares = Vec::with_capacity(NUM_FLARE);
// ํด๋ฌ์คํฐ๋น ๋ช ๊ฐ์ ํ๋ ์ด๋ฅผ ๋ฐฐ์ ํ ์ง ๊ณ์ฐ
let flares_per_cluster = NUM_FLARE / CLUSTERS;
// ๊ฐ ํด๋ฌ์คํฐ ์์ฑ
for c in 0..CLUSTERS {
// ํด๋ฌ์คํฐ ์ค์ฌ ๊ฐ๋: ์์ CLUSTERS ๋ฑ๋ถ
let cluster_angle = (c as f32) / (CLUSTERS as f32) * TAU;
// ํด๋ฌ์คํฐ ์ ์ฒด์ ๊ณต์ ๋๋ ๋๋ค ์๋ (๋ด๋ถ ๋ค์์ฑ ํ๋ณด์ฉ)
let cluster_seed = rng.gen_range(0.0..1000.0);
// ํด๋ฌ์คํฐ ๋ด ํ๋ ์ด ์์ฑ
for _ in 0..flares_per_cluster {
// ์ค์ฌ ๊ฐ๋ ์ฃผ๋ณ์ ๋๋ค ํผ์ง
let angle = cluster_angle + rng.gen_range(-CLUSTER_SPREAD..CLUSTER_SPREAD);
// ๊ธธ์ด ๋๋ค ์ค์
let length = rng.gen_range(MIN_LEN..=MAX_LEN);
// ๊ณ ์ ์๋: ํด๋ฌ์คํฐ ์๋ + ์ถ๊ฐ ๋๋ค โ ๊ฐ์ ํด๋ฌ์คํฐ ๋ด์์๋ ๋ค๋ฅธ ๋
ธ์ด์ฆ ํจํด
let seed = cluster_seed + rng.gen_range(0.0..100.0);
// Flare ์ธ์คํด์ค ์์ฑ ๋ฐ ์ถ๊ฐ
flares.push(Flare {
angle,
length,
seed,
cluster_id: c,
});
}
}
flares
}
// ๋งค ํ๋ ์ ์๊ฐ ์
๋ฐ์ดํธ ํจ์
fn update(_app: &App, model: &mut Model, update: Update) {
// ๊ฒฝ๊ณผ ์๊ฐ(์ด) ร ์๋ ๊ณ์ โ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์
์๊ฐ ํ๋ฆ
model.time += update.since_last.as_secs_f64() * NOISE_SPEED;
}
// ๋งค ํ๋ ์ ๋ ๋๋ง ํจ์
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
// ๋งค์ฐ ์ด๋์ด ๋ฐฐ๊ฒฝ (์์ํ ๊ฒ์ )
draw.background().color(hsla(0.0, 0.0, 0.02, 1.0));
// ํ๋ฉด ์ค์ ์ขํ
let center = pt2(0.0, 0.0);
// ํฐ์ (ํญ์ฑ๊ณผ ํ๋ ์ด ์์ ๊ธฐ์ค)
let white = hsla(0.0, 0.0, 0.9, 1.0);
// ์ค์ฌ ์ (ํญ์ฑ): ํ
๋๋ฆฌ๋ง ์๊ณ ์ฑ์ฐ๊ธฐ ์์
draw.ellipse()
.no_fill() // ๋ด๋ถ ์ฑ์ฐ๊ธฐ ์์
.stroke(white) // ํ
๋๋ฆฌ ์์
.stroke_weight(1.0) // ํ
๋๋ฆฌ ๋๊ป
.xy(center) // ์ค์ฌ ์์น
.w_h(RADIUS * 2.0, RADIUS * 2.0); // ์ง๋ฆ = ๋ฐ์ง๋ฆ ร 2
// ๋ชจ๋ ํ๋ ์ด ๊ทธ๋ฆฌ๊ธฐ
for flare in &model.flares {
draw_flare(&draw, flare, center, &model.perlin, model.time);
}
// ํ๋ ์์ ๊ทธ๋ฆฌ๊ธฐ ๋ช
๋ น ์ ์ฉ
draw.to_frame(app, &frame).unwrap();
}
// ๊ฐ๋ณ ํ๋ ์ด๋ฅผ ๊ณก์ ํํ๋ก ๊ทธ๋ฆฌ๋ ํจ์
// - draw: ๊ทธ๋ฆฌ๊ธฐ ๋ช
๋ น ๊ฐ์ฒด (๋ถ๋ณ ์ฐธ์กฐ)
// - flare: ๊ทธ๋ฆด ํ๋ ์ด ๋ฐ์ดํฐ
// - center: ํญ์ฑ ์ค์ฌ ์ขํ
// - perlin: ๋
ธ์ด์ฆ ์์ฑ๊ธฐ (๋ถ๋ณ ์ฐธ์กฐ)
// - time: ํ์ฌ ์ ๋๋ฉ์ด์
์๊ฐ
fn draw_flare(draw: &Draw, flare: &Flare, center: Point2, perlin: &Perlin, time: f64) {
// ํ๋ ์ด ๋ฐฉํฅ ๋จ์ ๋ฒกํฐ (๊ฐ๋ โ ๋ฒกํฐ ๋ณํ)
let dir = vec2(flare.angle.cos(), flare.angle.sin());
// ๋ฐฉํฅ์ ์์ง์ธ ๋ฒกํฐ (๋๊ป ๊ณ์ฐ์ฉ)
let perp = vec2(-dir.y, dir.x);
// ํ๋ ์ด ์์์ : ํญ์ฑ ํ๋ฉด (๋ฐ์ง๋ฆ๋งํผ ๋จ์ด์ง ์ง์ )
let base_center = center + dir * RADIUS;
// ํ๋ ์ด๋ฅผ SEGMENTS ๊ฐ์ ๊ตฌ๊ฐ์ผ๋ก ๋๋์ด ๊ณก์ ํํ
for s in 0..SEGMENTS {
// ํ์ฌ ๊ตฌ๊ฐ์ ์์(t0)๊ณผ ๋(t1) ๋น์จ (0.0 ~ 1.0)
let t0 = s as f32 / SEGMENTS as f32;
let t1 = (s + 1) as f32 / SEGMENTS as f32;
// ํผ๋ฆฐ ๋
ธ์ด์ฆ ๊ธฐ๋ฐ ํ์ด์ง ๊ณ์ฐ:
// - x: ๊ตฌ๊ฐ ์์น (t0, t1) ร ์ค์ผ์ผ
// - y: ํ๋ ์ด ๊ณ ์ ์๋ โ ๊ณ ์ ํจํด ๋ณด์ฅ
// - z: ์๊ฐ โ ์ ๋๋ฉ์ด์
let n0 = perlin.get([t0 as f64 * NOISE_SCALE, flare.seed, time]) as f32;
let n1 = perlin.get([t1 as f64 * NOISE_SCALE, flare.seed, time]) as f32;
// ์์ง ๋ฐฉํฅ์ผ๋ก ํ์ด์ง ์คํ์
๊ณ์ฐ
let offset0 = perp * (n0 * NOISE_AMPLITUDE);
let offset1 = perp * (n1 * NOISE_AMPLITUDE);
// ์ค์ 3D ๊ณก์ ์์ ์ ๊ณ์ฐ:
// - ๊ธฐ๋ณธ ์ง์ ์์น + ๋
ธ์ด์ฆ ์คํ์
let p0 = base_center + dir * (flare.length * t0) + offset0;
let p1 = base_center + dir * (flare.length * t1) + offset1;
// ๋๊ป์ ํฌ๋ช
๋๋ฅผ ์ ํ ๋ณด๊ฐ(lerp)์ผ๋ก ๋ถ๋๋ฝ๊ฒ ๋ณํ
let w0 = lerp(START_THICK, END_THICK, t0);
let w1 = lerp(START_THICK, END_THICK, t1);
let a0 = lerp(START_ALPHA, END_ALPHA, t0);
let a1 = lerp(START_ALPHA, END_ALPHA, t1);
// ํ๊ท ์ํ๊ฐ์ผ๋ก ์์ ๊ฒฐ์ (๊ฐ๋จํ ๊ทผ์ฌ)
let alpha = (a0 + a1) * 0.5;
let color = hsla(0.0, 0.0, 1.0, alpha); // ํฐ์, ์ํ๋ง ๋ณํ
// ์ฌ๋ค๋ฆฌ๊ผด์ ๋ ๊ฐ์ ์ผ๊ฐํ์ผ๋ก ๋ถํ ํ์ฌ ๊ทธ๋ฆฌ๊ธฐ:
// - ์ผ์ชฝ/์ค๋ฅธ์ชฝ ๊ฒฝ๊ณ์ ๊ณ์ฐ (๋๊ป ๊ธฐ๋ฐ)
let left0 = p0 + perp * (w0 * 0.5);
let right0 = p0 - perp * (w0 * 0.5);
let left1 = p1 + perp * (w1 * 0.5);
let right1 = p1 - perp * (w1 * 0.5);
// ์ฒซ ๋ฒ์งธ ์ผ๊ฐํ (์ผ์ชฝ ๋ฐ)
draw.tri()
.points(left0, right0, left1)
.color(color);
// ๋ ๋ฒ์งธ ์ผ๊ฐํ (์ค๋ฅธ์ชฝ ๋ฐ)
draw.tri()
.points(right0, right1, left1)
.color(color);
}
}
// ์ ํ ๋ณด๊ฐ(Linear Interpolation) ํจ์
// - a: ์์๊ฐ
// - b: ๋๊ฐ
// - t: ๋ณด๊ฐ ๋น์จ (0.0=์์, 1.0=๋)
// ์: lerp(10.0, 20.0, 0.5) โ 15.0
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
#[inline] ์ดํธ๋ฆฌ๋ทฐํธ#[inline] ํจ์: ์ปดํ์ผ ์ ํจ์ ์ฝ๋๋ฅผ ํธ์ถ ์์น์ ์ง์ ์ฝ์
โ ํจ์ ํธ์ถ ์ค๋ฒํค๋ ์ ๊ฑฐlerp, min, max)์ ์ ํฉ#[inline(always)]์ ๊ฐ์ ์ธ๋ผ์ธ (์ํํ ์ ์์)lerp๋ ํ๋ ์ด ํ๋๋น 4๋ฒ, ์ ์ฒด 5000๊ฐ ํ๋ ์ด ร 20 ์ธ๊ทธ๋จผํธ = 40๋ง ๋ฒ ์ด์ ํธ์ถ๐ก Rust ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ง์ ์ํ ํจ์(
f32::min,clamp๋ฑ)๋#[inline]์ผ๋ก ์ ์๋์ด ์์ต๋๋ค.