:: BASIC_18_perlin_wavy_lines

BamgasiJM·2025년 10월 4일

Nannou <BASIC>

목록 보기
29/41
post-thumbnail

📝 Rust Code

use nannou::prelude::*;

use nannou::noise::{ Perlin, NoiseFn };

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

struct Model {
    _window: window::Id,
    perlin: Perlin,
    layers: usize,
    samples: usize,
    spacing: f32,
    amplitude: f32,
    freq: f64,
    time_speed: f32,
    start_time: std::time::Instant,
}

fn model(app: &App) -> Model {
    let window_id = app
        .new_window()
        .size(1200, 800)
        .title("Layered Line Depth")
        .view(view)
        .build()
        .unwrap();

    let perlin = Perlin::default(); // 시드 값을 명시적으로 전달하거나 Default 사용 (최신 API)

    Model {
        _window: window_id,
        perlin,
        layers: 80,
        samples: 400,
        spacing: 8.0,
        amplitude: 120.0,
        freq: 0.003,
        time_speed: 0.2,
        start_time: std::time::Instant::now(),
    }
}

fn update(_app: &App, _model: &mut Model, _update: Update) {
    // 업데이트 로직이 필요하면 여기에 추가

}

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 win = app.window_rect();

    // app.time 대신 경과 시간 계산
    let t = model.start_time.elapsed().as_secs_f32();
    let x_min = win.left();
    let x_max = win.right();
    let width = x_max - x_min;
    let center_y = 0.0;
    for i in 0..model.layers {
        let depth = ((i as f32) / ((model.layers - 1) as f32)) * 2.0 - 1.0;
        let base_y = center_y + depth * ((model.layers as f32) * 0.5 * model.spacing);
        let stroke_w = map_range(depth, -1.0, 1.0, 0.6, 2.6);
        let alpha = map_range(depth, -1.0, 1.0, 0.45, 1.0);
        let mut pts: Vec<Point2> = Vec::with_capacity(model.samples);
        for si in 0..model.samples {
            let u = (si as f32) / ((model.samples - 1) as f32);
            let x = x_min + u * width;
            let nx = (x as f64) * model.freq;
            let ny = (i as f64) * 0.05 + (t as f64) * (model.time_speed as f64);
            let noise_val = model.perlin.get([nx, ny]);
            let dy = (noise_val as f32) * model.amplitude * (1.0 - depth.abs());
            let edge_falloff = cubic_ease(map_range(u, 0.0, 1.0, -1.0, 1.0).abs());
            let final_y = base_y + dy * (1.0 - 0.7 * edge_falloff);
            pts.push(pt2(x, final_y));
        }

        draw.polyline().weight(stroke_w).points(pts).rgba(0.9, 0.9, 0.9, alpha);
    }
    draw.to_frame(app, &frame).unwrap();
}

fn cubic_ease(x: f32) -> f32 {
    let x = x.clamp(0.0, 1.0);
    x * x * (3.0 - 2.0 * x)
}

📝 Rust Code + Comment

use nannou::prelude::*;
// nannou에 포함된 노이즈 라이브러리에서 Perlin 노이즈와 NoiseFn 트레이트를 가져옵니다.
// Perlin은 절차적 노이즈를 생성하는 데 사용되고, NoiseFn은 .get() 메서드를 제공합니다.
use nannou::noise::{Perlin, NoiseFn};

// 프로그램의 진입점(entry point)입니다.
fn main() {
    // nannou 애플리케이션을 설정하고 실행합니다.
    // model 함수로 초기 상태를 만들고, update 함수로 매 프레임 상태를 갱신하며, view 함수로 화면에 그립니다.
    // .run()이 호출되면 이벤트 루프가 시작됩니다.
    nannou::app(model).update(update).run();
}

// 애플리케이션의 전체 상태(state)를 저장하는 구조체입니다.
// 이 구조체의 인스턴스가 model, update, view 함수에 전달됩니다.
struct Model {
    // 생성된 창의 ID를 저장합니다. 직접 사용하지 않더라도 창을 유지하기 위해 소유권(ownership)을 가져야 합니다.
    // 변수명 앞에 '_'를 붙여 사용하지 않는 변수임을 명시합니다.
    _window: window::Id,  
    // Perlin 노이즈 생성기 인스턴스입니다. 파형을 만드는 데 사용됩니다.
    perlin: Perlin,
    // 화면에 그릴 선의 총 개수 (레이어 수)
    layers: usize,
    // 각 선을 구성하는 점의 개수 (샘플 수)
    samples: usize,
    // 각 레이어(선) 사이의 y축 간격
    spacing: f32,
    // 노이즈가 선의 형태에 미치는 영향력의 크기 (진폭)
    amplitude: f32,
    // 노이즈 공간의 스케일을 조절하는 값 (주파수). 값이 작을수록 부드러운 노이즈가 생성됩니다.
    freq: f64,
    // 애니메이션의 시간 흐름 속도를 조절합니다.
    time_speed: f32,
    // 애니메이션 시작 시점을 기록하여 경과 시간을 측정하는 데 사용됩니다.
    start_time: std::time::Instant,
}

// 애플리케이션이 처음 시작될 때 한 번만 호출되는 함수입니다.
// Model 구조체를 초기화하고 반환합니다.
fn model(app: &App) -> Model {
    // 새로운 창을 생성하고 설정합니다.
    let window_id = app
        .new_window()                         // 새 창 빌더 시작
        .size(1200, 800)                      // 창 크기 설정
        .title("Layered Line Depth - Nannou") // 창 제목 설정
        .view(view)                           // 이 창을 그릴 때 사용할 함수를 'view'로 지정
        .build()                              // 설정을 바탕으로 창 생성
        .unwrap();                            // 창 생성이 실패하면 패닉 발생 (보통 실패하지 않음)

    // Perlin 노이즈 생성기를 초기화합니다.
    // 최신 noise-rs API에서는 .default()를 사용하여 기본 시드(seed) 값으로 생성하는 것을 권장합니다.
    // 매번 실행 시 동일한 패턴을 보장하려면 Perlin::new(seed_value) 처럼 시드를 명시할 수 있습니다.
    let perlin = Perlin::default();
    
    // Model 구조체의 각 필드에 초기값을 설정하여 인스턴스를 생성하고 반환합니다.
    Model {
        _window: window_id,
        perlin,
        layers: 80,
        samples: 400,
        spacing: 8.0,
        amplitude: 120.0,
        freq: 0.003,
        time_speed: 0.2,
        // 프로그램 시작 시점의 시간을 기록합니다. `app.time`보다 정밀하고 프레임에 독립적인 시간 제어가 가능합니다.
        start_time: std::time::Instant::now(),
    }
}

// 매 프레임마다 호출되어 Model의 상태를 업데이트하는 함수입니다.
// 이 예제에서는 애니메이션 로직이 view 함수 내의 시간 계산으로 처리되므로, update 함수는 비워둡니다.
fn update(_app: &App, _model: &mut Model, _update: Update) {
    // 상태 업데이트 로직이 필요하면 여기에 추가합니다.
}

// 매 프레임마다 화면을 그리는 함수입니다.
// App과 Model의 현재 상태를 읽어서 프레임에 그림을 그립니다.
fn view(app: &App, model: &Model, frame: Frame) {
    // 그리기를 위한 Draw 객체를 생성합니다.
    let draw = app.draw();
    // 배경을 hsla 모드로 칠합니다. (짙은 회색)
    draw.background().(hsla(0.0, 0.0, 0.02, 1.0));

    // 창의 사각형 영역 정보를 가져옵니다. (left, right, bottom, top 좌표)
    let win = app.window_rect();
    
    // model에 저장된 시작 시간으로부터 현재까지 경과된 시간을 초 단위(f32)로 계산합니다.
    // 이 't' 값은 애니메이션을 만드는 데 사용됩니다.
    let t = model.start_time.elapsed().as_secs_f32();

    // 창의 왼쪽, 오른쪽 x좌표와 전체 너비를 계산합니다.
    let x_min = win.left();
    let x_max = win.right();
    let width = x_max - x_min;
    // 모든 선의 y축 기준점이 될 중앙 y좌표입니다.
    let center_y = 0.0;

    // 설정된 레이어 수만큼 반복하여 각 선을 그립니다.
    for i in 0..model.layers {
        // 현재 레이어의 깊이(depth)를 계산합니다. -1.0 (뒤) ~ 1.0 (앞) 범위의 값입니다.
        let depth = (i as f32 / (model.layers - 1) as f32) * 2.0 - 1.0;
        // 깊이에 따라 선의 기본 y 위치를 계산합니다. 뒤에 있을수록 위쪽에, 앞에 있을수록 아래쪽에 배치됩니다.
        let base_y = center_y + depth * (model.layers as f32 * 0.5 * model.spacing);

        // 깊이에 따라 선의 굵기(stroke_w)와 투명도(alpha)를 조절합니다.
        // 뒤에 있는 선일수록 가늘고 투명하게, 앞에 있는 선일수록 굵고 진하게 보입니다.
        let stroke_w = map_range(depth, -1.0, 1.0, 0.6, 2.6);
        let alpha = map_range(depth, -1.0, 1.0, 0.45, 1.0);

        // 현재 선을 구성할 점(Point2)들을 담을 벡터를 생성합니다.
        // with_capacity를 사용해 미리 필요한 만큼 메모리를 할당하여 성능을 최적화합니다.
        let mut pts: Vec<Point2> = Vec::with_capacity(model.samples);

        // 설정된 샘플 수만큼 반복하여 선 위의 각 점의 위치를 계산합니다.
        for si in 0..model.samples {
            // 현재 샘플의 위치를 0.0 ~ 1.0 범위로 정규화합니다.
            let u = si as f32 / (model.samples - 1) as f32;
            // 정규화된 위치 'u'를 사용해 실제 x 좌표를 계산합니다.
            let x = x_min + u * width;

            // Perlin 노이즈 함수에 입력할 2차원 좌표를 계산합니다. f64 타입으로 변환해야 합니다.
            // nx: 공간적인 차원. x 좌표에 주파수를 곱해 노이즈 스케일을 조절합니다.
            let nx = (x as f64) * model.freq;
            // ny: 시간과 깊이를 결합한 차원. 레이어 인덱스(i)와 시간(t)을 조합하여 각 선이 다르게, 그리고 시간에 따라 움직이게 합니다.
            let ny = (i as f64) * 0.05 + (t as f64) * (model.time_speed as f64);

            // 계산된 [nx, ny] 좌표로 Perlin 노이즈 값을 가져옵니다. 이 값은 -1.0 ~ 1.0 범위입니다.
            let noise_val = model.perlin.get([nx, ny]);
            // 노이즈 값에 진폭(amplitude)과 깊이 보정치를 곱하여 최종 y축 변위(dy)를 계산합니다.
            // (1.0 - depth.abs())는 중앙에 가까운 선일수록 변위가 커지게 하는 효과를 줍니다.
            let dy = noise_val as f32 * model.amplitude * (1.0 - depth.abs());

            // 화면 가장자리로 갈수록 선의 변위를 줄여주는 효과(falloff)를 계산합니다.
            // 1. u(0~1)를 -1~1 범위로 매핑하고 절대값을 취하면, 중앙(0)에서 가장자리(1)로 가는 값이 됩니다.
            // 2. 이 값을 cubic_ease 함수에 넣어 부드러운 곡선 값을 얻습니다.
            let edge_falloff = cubic_ease(map_range(u, 0.0, 1.0, -1.0, 1.0).abs());
            // 기본 y 위치에 노이즈 변위를 더하여 최종 y 좌표를 계산합니다.
            // 가장자리(edge_falloff가 1에 가까움)일수록 노이즈 변위(dy)가 줄어들도록 합니다.
            let final_y = base_y + dy * (1.0 - 0.7 * edge_falloff);

            // 계산된 (x, y) 좌표를 점 벡터에 추가합니다.
            pts.push(pt2(x, final_y));
        }

        // 점 벡터(pts)를 사용하여 하나의 연결된 선(polyline)을 그립니다.
        draw.polyline()
            .weight(stroke_w) // 선의 굵기 설정
            .points(pts) // 선을 구성할 점들 전달
            .rgba(0.0, 0.0, 0.0, alpha); // 선의 색상(검정)과 투명도 설정
    }

    // 지금까지 'draw' 객체에 쌓인 모든 그리기 명령을 실제 프레임에 렌더링합니다.
    draw.to_frame(app, &frame).unwrap();
}

// 0.0 ~ 1.0 사이의 입력값 x를 받아 부드러운 S자 곡선(Smoothstep)으로 변환하는 함수입니다.
// 이 함수는 가장자리 효과를 자연스럽게 만드는 데 사용됩니다.
fn cubic_ease(x: f32) -> f32 {
    // 입력값이 0.0 ~ 1.0 범위를 벗어나지 않도록 강제합니다.
    let x = x.clamp(0.0, 1.0);
    // Smoothstep 공식: 3x^2 - 2x^3
    x * x * (3.0 - 2.0 * x)
}

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

0개의 댓글