:: BASIC_15_Perlin_Practice_01

BamgasiJM·2025년 9월 29일

Nannou <BASIC>

목록 보기
28/41
post-thumbnail

Nannou에서 Perlin 노이즈 사용하기: 자연스러운 절차적 패턴 만들기

Perlin 노이즈는 창의적 코딩, 시각 예술, 게임 개발 등에서 자연스럽고 유기적인 무작위성을 구현하는 핵심 기법입니다.


🔹 Perlin 노이즈란?

Perlin 노이즈는 Ken Perlin이 1980년대 개발한 절차적 노이즈(Procedural Noise) 알고리즘입니다.
일반적인 랜덤 값은 불연속적이지만, Perlin 노이즈는 인접한 좌표에서 비슷한 값을 반환해 부드러운 변화를 만듭니다.

✨ 핵심 특징

  • 연속성 : 좌표가 조금 바뀌어도 출력 값은 부드럽게 변화
  • 재현 가능성 : 같은 입력 좌표 → 항상 같은 출력 값
  • 다차원 지원 : 1D, 2D, 3D, 심지어 4D(시간 포함)까지 가능
  • 출력 범위 : 일반적으로 -1.0 ~ 1.0 사이의 실수

이 덕분에 구름, 물결, 지형, 불꽃 같은 자연 현상을 효과적으로 흉내 낼 수 있습니다.


🔹 Nannou에서 Perlin 노이즈 사용하기

Nannou는 내부적으로 noise 크레이트를 사용해 Perlin 노이즈를 제공합니다.

1️⃣ 필요한 임포트

use nannou::noise::{NoiseFn, Perlin};
  • Perlin: 노이즈 생성기 구조체
  • NoiseFn: .get() 메서드를 통해 노이즈 값을 계산할 수 있게 해주는 트레이트

2️⃣ Perlin 인스턴스 생성

let perlin = Perlin::new(42); // 42는 임의의 시드 값
  • noise 크레이트 버전 0.9는 u32 타입의 시드(seed) 인자를 요구
  • 매번 다른 결과를 원한다면 rand 크레이트를 사용해 랜덤 시드를 생성

3️⃣ 노이즈 값 계산

let value = perlin.get([x as f64, y as f64]);
  • 입력은 [f64; N] 배열 (2D라면 [x, y])
  • 반환값은 f64, 일반적으로 -1.0 ~ 1.0
    ⚠️ 반드시 f64를 사용해야 합니다. f32는 동작하지 않습니다.

4️⃣ 좌표 스케일링과 값 매핑

Perlin 노이즈는 너무 "느리게" 또는 "빠르게" 변할 수 있으므로, 좌표를 스케일링하는 것이 중요합니다.

let noise_val = perlin.get([p.x as f64 * 0.01, p.y as f64 * 0.01]);
  • * 0.01 → 낮은 주파수: 부드러운 변화
  • * 0.1 → 높은 주파수: 세밀하고 복잡한 패턴
    그리고 노이즈 값을 유용한 범위(예: 0.0~1.0)로 변환하려면 map_range를 사용합니다:
let brightness = map_range(noise_val, -1.0, 1.0, 0.0, 1.0);

🔹 실용적인 사용 예시

🎨 점의 밝기 제어

let n = perlin.get([x * 0.02, y * 0.02]);
let gray = map_range(n, -1.0, 1.0, 0.0, 1.0);
draw.ellipse().color(gray);

🌊 시간 기반 애니메이션

// Model에 시간 저장
struct Model {
    time: f32,
    perlin: Perlin,
}

fn update(app: &App, model: &mut Model, _update: Update) {
    model.time = app.time;
}

// view에서 사용
let n = model.perlin.get([x * 0.02, model.time * 0.1]);

🏔️ 지형 높이 맵 생성

for x in 0..800 {
    for y in 0..800 {
        let height = perlin.get([x as f64 * 0.05, y as f64 * 0.05]);
        let color = if height > 0.3 { rgb(0.2, 0.8, 0.2) } else { rgb(0.1, 0.1, 0.8) };
        draw.rect().x_y(x as f32 - 400.0, y as f32 - 400.0).w_h(1.0, 1.0).color(color);
    }
}

🔹 Fractal Noise (옥타브 기반 확장)

더 자연스러운 결과를 원한다면, 여러 주파수의 Perlin 노이즈를 겹쳐서(octaves) 사용하세요.

fn fractal_noise(perlin: &Perlin, x: f64, y: f64, octaves: usize) -> f64 {
    let mut value = 0.0;
    let mut amplitude = 1.0;
    let mut frequency = 1.0;
    let mut total_amp = 0.0;

    for _ in 0..octaves {
        value += perlin.get([x * frequency, y * frequency]) * amplitude;
        total_amp += amplitude;
        amplitude *= 0.5;   // 진폭 점점 줄이기
        frequency *= 2.0;   // 주파수 점점 높이기
    }

    value / total_amp // 정규화하여 -1.0~1.0 유지
}

이 방식으로 구름, 마블 무늬, 산맥 지형 같은 복잡한 텍스처를 만들 수 있습니다.


🔹 주의사항 & 팁

  • 입력 타입 : 반드시 f64 배열 ([f64; 2]) 사용
  • 스케일링 필수 : 원본 좌표(0~800)를 그대로 넣으면 너무 빠르게 변함 → * 0.01 권장
  • 성능 고려 : 수십만 개의 점에 매 프레임 적용 시 성능 저하 → 필요시 캐싱
  • Simplex Noise(3D 이상에서 더 효율적)도 있지만, Nannou 기본은 Perlin
  • 좌표를 스케일링하세요 (* 0.01)
  • map_range로 값을 유용한 범위로 변환하세요
  • 시간(app.time)과 결합해 애니메이션을 만드세요
  • 옥타브를 쌓아 더 복잡한 패턴을 구현하세요

📝 Rust Code

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

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

struct Model {
    perlin: Perlin,
    points: Vec<Point2>,
}

fn model(app: &App) -> Model {
    app.new_window()
        .size(800, 800)
        .title("Perlin Noise Grid")
        .view(view)
        .build()
        .unwrap();

    let perlin = Perlin::new();
    let mut points = Vec::new();

    let step = 10.0;
    for y in (0..800).step_by(step as usize) {
        for x in (0..800).step_by(step as usize) {
            points.push(pt2(x as f32 - 400.0, y as f32 - 400.0));
        }
    }

    Model { perlin, points }
}

fn update(_app: &App, _model: &mut Model, _update: Update) {
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    draw.background().color(BLACK);

    for p in &model.points {
        let noise_val = model.perlin.get([p.x as f64 * 0.01, p.y as f64 * 0.01]);
        let brightness = map_range(noise_val, -1.0, 1.0, 0.0, 1.0);
        if brightness > 0.5 {
            draw.ellipse().x_y(p.x, p.y).w_h(2.0, 2.0).color(WHITE);
        }
    }

    draw.to_frame(app, &frame).unwrap();
}

📝 Rust Code + Comment by Qwen

// =============================================================================
// 0. 모듈 및 크레이트 임포트 (Imports)
// =============================================================================
// `nannou::prelude::*`는 nannou 애플리케이션 개발에 필요한 대부분의 기본 타입과 함수를 한 번에 가져옵니다.
// 여기에는 벡터(Point2, Vec2), 색상(BLACK, WHITE), 유틸리티 함수(map_range 등)가 포함됩니다.
use nannou::prelude::*;

// Perlin 노이즈 생성기를 사용하기 위해 nannou의 noise 모듈에서 Perlin 구조체와 NoiseFn 트레이트를 가져옵니다.
// Perlin은 procedural noise(절차적 노이즈)를 생성하는 데 사용되며, 자연스러운 텍스처나 패턴을 만들 때 유용합니다.
use nannou::noise::{NoiseFn, Perlin};

// =============================================================================
// 1. 메인 함수 (Application Entry Point)
// =============================================================================
// Rust 프로그램의 진입점입니다.
// `nannou::app(model)`은 nannou 앱을 초기화하고, `model` 함수를 통해 초기 상태를 설정합니다.
// `.update(update)`는 매 프레임마다 호출될 업데이트 로직을 지정합니다.
// `.run()`은 앱을 실행하고 이벤트 루프를 시작합니다.
fn main() {
    nannou::app(model).update(update).run();
}

// =============================================================================
// 2. 모델 정의 (State Structure)
// =============================================================================
// `Model` 구조체는 애플리케이션의 상태를 저장합니다.
// Rust에서 구조체는 관련된 데이터를 그룹화하는 사용자 정의 타입입니다.
struct Model {
    // Perlin 노이즈 생성기 인스턴스.
    // 이는 2D 좌표를 입력받아 -1.0 ~ 1.0 사이의 연속적인 노이즈 값을 반환합니다.
    perlin: Perlin,

    // 화면에 그릴 점(Point2)들의 벡터.
    // `Point2`는 nannou에서 제공하는 2D 좌표 타입으로, x와 y 필드를 가집니다.
    // `Vec<T>`는 Rust의 동적 배열 타입으로, 크기가 런타임에 변할 수 있습니다.
    points: Vec<Point2>,
}

// =============================================================================
// 3. 모델 초기화 함수 (Model Initialization)
// =============================================================================
// `model` 함수는 앱 시작 시 한 번만 호출되며, 초기 상태(`Model`)를 반환합니다.
// `app: &App`은 nannou 앱 인스턴스에 대한 불변 참조입니다.
fn model(app: &App) -> Model {
    // 새로운 창을 생성하고 설정합니다.
    // 메서드 체이닝을 통해 창의 속성을 순차적으로 설정합니다.
    app.new_window()
        .size(800, 800)          // 창 크기를 800x800 픽셀로 설정
        .title("Perlin Noise Grid") // 창 제목 설정
        .view(view)              // 렌더링에 사용할 `view` 함수 지정
        .build()                 // 창 빌드 요청
        .unwrap();               // Result 타입을 처리: 오류 발생 시 패닉 (간단한 예제에서는 허용)

    // Perlin 노이즈 생성기 인스턴스를 생성합니다.
    // 기본 시드로 초기화되며, 항상 같은 시퀀스의 노이즈를 생성합니다.
    let perlin = Perlin::new();

    // 점들을 저장할 빈 벡터를 생성합니다.
    let mut points = Vec::new();

    // 샘플링 포인트 (격자 기반)
    // `step`은 격자 간격을 픽셀 단위로 정의합니다.
    // f32 타입으로 선언되어 있으나, 반복문에서 usize로 캐스팅되어 사용됩니다.
    let step = 10.0;

    // y축 방향으로 0부터 799까지 `step` 간격으로 반복합니다.
    // `(0..800)`은 0 이상 800 미만의 범위(Range)를 생성합니다.
    // `.step_by(n)`은 반복을 n씩 건너뛰게 합니다 (여기서는 10픽셀 간격).
    // 주의: `step as usize`는 부동소수점 값을 정수로 변환하므로, 정확한 간격을 유지하려면 step이 정수여야 합니다.
    for y in (0..800).step_by(step as usize) {
        // x축 방향으로 동일하게 반복합니다.
        for x in (0..800).step_by(step as usize) {
            // `pt2(x, y)`는 nannou에서 제공하는 Point2 생성 매크로입니다.
            // 좌표계를 화면 중앙(0,0) 기준으로 맞추기 위해 400을 뺍니다.
            // 원래 좌표는 (0,0) ~ (800,800)이지만, 이를 (-400,-400) ~ (400,400)으로 변환합니다.
            points.push(pt2(x as f32 - 400.0, y as f32 - 400.0));
        }
    }

    // 초기화된 Model 인스턴스를 반환합니다.
    Model { perlin, points }
}

// =============================================================================
// 4. 업데이트 함수 (Per-Frame Update Logic)
// =============================================================================
// `update` 함수는 매 프레임마다 호출됩니다.
// 이 예제에서는 정적인 이미지를 그리므로, 업데이트 로직이 없습니다.
// `_` 접두사는 사용하지 않는 변수를 명시적으로 무시함을 나타냅니다 (Rust의 관용적 표현).
fn update(_app: &App, _model: &mut Model, _update: Update) {
    // 여기서는 아무 것도 업데이트하지 않음 (정적 이미지)
}

// =============================================================================
// 5. 뷰 함수 (Rendering Logic)
// =============================================================================
// `view` 함수는 매 프레임마다 화면을 그리는 데 사용됩니다.
// `frame: Frame`은 현재 렌더링 대상 프레임 버퍼입니다.
fn view(app: &App, model: &Model, frame: Frame) {
    // `app.draw()`는 새로운 드로우 컨텍스트를 생성합니다.
    // 이 컨텍스트를 통해 도형을 정의하고, 마지막에 프레임에 렌더링합니다.
    let draw = app.draw();

    // 배경을 검은색(BLACK)으로 설정합니다.
    // `draw.background()`는 전체 캔버스를 지정된 색상으로 채웁니다.
    draw.background().color(BLACK);

    // Perlin noise로 점 찍기
    // `model.points` 벡터의 각 점에 대해 반복합니다.
    // `&model.points`는 소유권을 이동시키지 않고 참조만 전달합니다 (성능 최적화).
    for p in &model.points {
        // Perlin 노이즈 값 계산:
        // `model.perlin.get([x, y])`는 2D 좌표를 입력받아 노이즈 값을 반환합니다.
        // 입력 좌표는 f64 타입이어야 하며, 원본 좌표(p.x, p.y)를 0.01배로 스케일링하여
        // 노이즈의 "주파수"를 조절합니다 (값이 작을수록 더 부드러운 변화).
        let noise_val = model.perlin.get([p.x as f64 * 0.01, p.y as f64 * 0.01]);

        // 노이즈 값을 0.0~1.0 범위로 매핑합니다.
        // `map_range(value, in_min, in_max, out_min, out_max)`는 선형 보간을 수행합니다.
        // Perlin 노이즈는 일반적으로 -1.0 ~ 1.0 사이의 값을 반환하므로,
        // 이를 밝기(brightness)로 사용하기 위해 0.0~1.0으로 변환합니다.
        let brightness = map_range(noise_val, -1.0, 1.0, 0.0, 1.0);

        // 밝기가 0.5보다 클 경우에만 점을 그립니다 (임계값 기반 필터링).
        if brightness > 0.5 {
            // 흰색(WHITE) 타원(원)을 그립니다.
            // `.ellipse()`은 타원 드로우 명령을 시작합니다.
            // `.x_y(x, y)`는 타원의 중심 좌표를 설정합니다.
            // `.w_h(width, height)`는 타원의 너비와 높이를 설정합니다 (여기서는 2x2 원).
            // `.color(color)`는 타원의 색상을 설정합니다.
            draw.ellipse().x_y(p.x, p.y).w_h(2.0, 2.0).color(WHITE);
        }
    }

    // 드로우 컨텍스트의 내용을 실제 프레임 버퍼에 렌더링합니다.
    // `.unwrap()`은 렌더링 오류 발생 시 패닉을 유발합니다 (간단한 예제에서는 허용).
    draw.to_frame(app, &frame).unwrap();
}

📝 Rust Code

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

struct Model {
    perlin: Perlin,
}

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

fn model(app: &App) -> Model {
    app.new_window()
        .size(800, 800)
        .view(view)
        .build()
        .unwrap();

    let perlin = Perlin::new(11);
    Model { perlin }
}

fn update(_app: &App, _model: &mut Model, _update: Update) {}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.background().color(WHITE);

    const SCALE: f32 = 2.0;
    const STEP: usize = (800.0 / SCALE) as usize; // 400 steps

    for i in 0..STEP {
        for j in 0..STEP {
            let nx = (i as f64 * SCALE as f64) * 0.02;
            let ny = (j as f64 * SCALE as f64) * 0.02;
            let height = model.perlin.get([nx, ny]);

            let color = if height > 0.3 {
                rgb(0.2, 0.8, 0.2)
            } else {
                rgb(0.1, 0.1, 0.8)
            };

            // 왼쪽 아래 모서리를 (-400, -400)에 맞춤
            let center_x = -400.0 + i as f32 * SCALE + SCALE / 2.0;
            let center_y = -400.0 + j as f32 * SCALE + SCALE / 2.0;
/*
첫 번째 사각형: 중심 = (-400 + 2, -400 + 2) = (-398, -398)
마지막 사각형 (i=199): 중심 = (-400 + 796 + 2, ...) = (398, 398)
사각형 범위: 398 ± 2 → [-400, 400] 꽉 채움!
*/
            draw.rect()
                .x_y(center_x, center_y)
                .w_h(SCALE, SCALE)
                .color(color);
        }
    }

    draw.to_frame(app, &frame).unwrap();
}
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0개의 댓글