::BASIC_30_undulated_circle

BamgasiJM·2025년 10월 24일

Nannou <BASIC>

목록 보기
39/41
post-thumbnail

undulated circle

An undulated circle refers to a circular shape whose edge is wavy or rippled instead of perfectly smooth. The undulations can vary in amplitude and frequency, giving the circle a dynamic, organic look similar to a vibrating or water-like surface. This pattern is common in visual design, mathematics, and generative art for creating rhythmic, fluid aesthetics.​

In Geometry and Art

In geometry or computer graphics (e.g., Blender, Nannou, or Grasshopper), an undulated circle can be described as a parametric curve where the radius oscillates with respect to the angle θ. Mathematically, the formula can be expressed as:

r(θ)=R+Asin(nθ)r(θ)=R+Asin(nθ)
where:
RR = base radius
AA = amplitude (how far the wave undulates outward/inward)
nn = number of undulations around the circle

This produces a periodic wave along the circumference, useful for procedural modeling or motion graphics.

Design Uses

Artists and designers often use undulated circles as:

Decorative frames or borders (frilly circular motifs)​

Organic forms in ceramics and sculpture, such as porcelain circles with varied surface height​

Pattern generation in digital illustration or vector art (concentric undulating line designs)​

In Generative Art

In generative art frameworks (like Nannou or Processing), such a circle can be created by varying the polar coordinate radius procedurally. This yields effects resembling ripples, waves, or breathing patterns that feel both geometric and natural.


📝 Rust Code

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

struct Model {
    smooth_points: Vec<Vec2>,
}

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

fn catmull_rom_spline(points: &[Vec2], segments: usize) -> Vec<Vec2> {
    let mut smoothed = Vec::new();
    let n = points.len();

    for i in 0..n {
        // 순환형 (닫힌 도형이므로)
        let p0 = points[(i + n - 1) % n];
        let p1 = points[i];
        let p2 = points[(i + 1) % n];
        let p3 = points[(i + 2) % n];

        for j in 0..segments {
            let t = j as f32 / segments as f32;
            let t2 = t * t;
            let t3 = t2 * t;
            let x = 0.5
                * ((2.0 * p1.x)
                    + (-p0.x + p2.x) * t
                    + (2.0 * p0.x - 5.0 * p1.x + 4.0 * p2.x - p3.x) * t2
                    + (-p0.x + 3.0 * p1.x - 3.0 * p2.x + p3.x) * t3);
            let y = 0.5
                * ((2.0 * p1.y)
                    + (-p0.y + p2.y) * t
                    + (2.0 * p0.y - 5.0 * p1.y + 4.0 * p2.y - p3.y) * t2
                    + (-p0.y + 3.0 * p1.y - 3.0 * p2.y + p3.y) * t3);
            smoothed.push(pt2(x, y));
        }
    }

    smoothed
}

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

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

    for i in 0..num_points {
        let theta = i as f32 * TAU / num_points as f32;
        let r = 400.0 + perlin.get([i as f64 * 0.1, 0.0]) as f32 * 150.0;
        points.push(pt2(theta.cos() * r, theta.sin() * r));
    }

    // 🔹 스무스하게 보간된 점 생성
    let smooth_points = catmull_rom_spline(&points, 10);

    Model { smooth_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);

    // 스무스 곡선 (닫힌 형태)
    draw.polyline()
        .weight(40.0)
        // .weight(random_range(1.0, 2.0))
        .color(WHITE)
        .points_closed(model.smooth_points.clone());

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

📝 Rust Code + Comment

// nannou 라이브러리의 기본 모듈을 임포트합니다. 이 모듈은 Vec2, pt2, TAU 등의 유용한 타입과 상수를 제공하며, Nannou 앱 개발의 필수적인 prelude입니다.
use nannou::prelude::*;
// nannou의 noise 모듈에서 NoiseFn 트레이트와 Perlin 노이즈 생성자를 임포트합니다. NoiseFn은 노이즈 함수의 공통 인터페이스를 정의하며, Perlin은 자연스러운 노이즈를 생성하는 구조체입니다.
use nannou::noise::{NoiseFn, Perlin};

// Model 구조체를 정의합니다. 이는 Nannou 앱의 상태를 저장하는 데 사용되며, 여기서는 부드럽게 보간된 점들의 벡터를 저장합니다.
struct Model {
    // smooth_points: 부드럽게 보간된 점들의 컬렉션. Vec<Vec2> 타입으로, 각 Vec2는 2D 좌표(x, y)를 나타내는 glam 라이브러리의 벡터입니다.
    smooth_points: Vec<Vec2>,
}

// main 함수: 프로그램의 진입점. Nannou 앱을 초기화하고 실행합니다.
fn main() {
    // nannou::app 함수를 호출해 앱을 생성합니다. model 함수를 초기 모델 생성기로, update 함수를 업데이트 콜백으로, run() 메서드로 앱을 시작합니다.
    // app는 Nannou의 핵심 앱 빌더로, 윈도우, 이벤트 루프 등을 관리합니다.
    nannou::app(model).update(update).run();
}

// catmull_rom_spline 함수: 주어진 점들 사이를 Catmull-Rom 스플라인으로 보간해 부드러운 곡선 점들을 생성합니다. (별도 설명 섹션 참조)
fn catmull_rom_spline(points: &[Vec2], segments: usize) -> Vec<Vec2> {
    // smoothed: 보간된 점들을 저장할 빈 벡터. Vec::new()로 초기화되며, 함수가 반환할 결과입니다.
    let mut smoothed = Vec::new();
    // n: 입력 점들의 개수. points 슬라이스의 길이를 저장하며, 루프 반복 횟수를 결정합니다.
    let n = points.len();

    // i 루프: 각 입력 점(i)에 대해 스플라인 세그먼트를 생성합니다. n번 반복해 닫힌 곡선을 만듭니다.
    for i in 0..n {
        // 순환형 (닫힌 도형이므로): 인덱스 모듈로 연산해 이전/이후 점을 순환적으로 가져옵니다.
        // p0: i-1 번째 점 (순환: (i + n - 1) % n). Catmull-Rom 스플라인의 이전 제어점.
        let p0 = points[(i + n - 1) % n];
        // p1: 현재 점 i. 스플라인의 시작점.
        let p1 = points[i];
        // p2: 다음 점 (i+1) % n. 스플라인의 끝점.
        let p2 = points[(i + 1) % n];
        // p3: i+2 번째 점 (순환). 다음 세그먼트의 제어점.
        let p3 = points[(i + 2) % n];

        // j 루프: 각 세그먼트(p1 ~ p2) 내에서 segments 개의 중간 점을 샘플링합니다.
        for j in 0..segments {
            // t: 현재 세그먼트 내 파라미터 (0.0 ~ (segments-1)/segments). 스플라인 공식의 t 값으로, 0에서 1로 선형 보간.
            let t = j as f32 / segments as f32;
            // t2: t의 제곱. 스플라인 공식의 t² 항 계산을 위해.
            let t2 = t * t;
            // t3: t의 세제곱. 스플라인 공식의 t³ 항 계산을 위해.
            let t3 = t2 * t;
            // x: x 좌표 보간 값. Catmull-Rom 스플라인의 표준 공식 적용:
            // 0.5 * (2*p1 + (-p0 + p2)*t + (2*p0 - 5*p1 + 4*p2 - p3)*t² + (-p0 + 3*p1 - 3*p2 + p3)*t³)
            // 이 공식은 4개의 제어점(p0~p3)을 사용해 부드러운 cubic 곡선을 생성하며, t=0에서 p1, t=1에서 p2를 통과합니다.
            let x = 0.5
                * ((2.0 * p1.x)
                    + (-p0.x + p2.x) * t
                    + (2.0 * p0.x - 5.0 * p1.x + 4.0 * p2.x - p3.x) * t2
                    + (-p0.x + 3.0 * p1.x - 3.0 * p2.x + p3.x) * t3);
            // y: y 좌표 보간 값. x와 동일한 공식을 y에 적용.
            let y = 0.5
                * ((2.0 * p1.y)
                    + (-p0.y + p2.y) * t
                    + (2.0 * p0.y - 5.0 * p1.y + 4.0 * p2.y - p3.y) * t2
                    + (-p0.y + 3.0 * p1.y - 3.0 * p2.y + p3.y) * t3);
            // pt2(x, y): 새로운 Vec2 점 생성. pt2는 Nannou prelude의 헬퍼 함수로 Vec2(x, y)를 반환합니다.
            // smoothed.push: 보간된 점을 벡터에 추가.
            smoothed.push(pt2(x, y));
        }
    }

    // smoothed: 모든 세그먼트의 점들이 추가된 벡터를 반환. 총 n * segments 개의 점으로 부드러운 곡선 근사.
    smoothed
}

// model 함수: Nannou 앱의 초기 상태를 설정합니다. &App은 앱 인스턴스 참조, Model을 반환합니다.
fn model(app: &App) -> Model {
    // app.new_window(): 새 윈도우를 생성하는 빌더 체인 시작. Nannou의 윈도우 설정을 위한 메서드입니다.
    // .size(1080, 1080): 윈도우 크기를 1080x1080 픽셀로 설정. width와 height 인수.
    // .view(view): view 함수를 렌더링 콜백으로 등록. view는 매 프레임마다 호출됩니다.
    // .build(): 윈도우 빌더를 완료하고 생성. unwrap()으로 에러 처리 (실패 시 패닉).
    app.new_window()
        .size(1080, 1080)
        .view(view)
        .build()
        .unwrap();

    // num_points: 원형 점의 개수. 120개로 설정해 충분한 분포를 만듭니다.
    let num_points = 120;
    // perlin: Perlin 노이즈 생성자. Perlin::new()는 기본 시드의 Perlin 노이즈를 초기화합니다. get 메서드로 노이즈 값을 쿼리할 수 있습니다.
    let perlin = Perlin::new();
    // points: 기본 점들을 저장할 빈 벡터. 노이즈 적용 전 원형 점들.
    let mut points = Vec::new();

    // i 루프: num_points만큼 반복해 각 점 생성.
    for i in 0..num_points {
        // theta: 각 점의 각도. i를 num_points로 나눈 후 TAU(2π) 곱해 균등 분포. 원형 배치를 위한 극좌표 변환.
        let theta = i as f32 * TAU / num_points as f32;
        // r: 반경. 기본 400.0에 Perlin 노이즈를 적용: get([i*0.1, 0.0])으로 1D 노이즈 생성 (i를 스케일링해 주파수 조정), *150.0으로 진폭 설정. 결과적으로 250~550 범위의 불규칙 반경.
        let r = 400.0 + perlin.get([i as f64 * 0.1, 0.0]) as f32 * 150.0;
        // pt2(theta.cos() * r, theta.sin() * r): 직교좌표 변환. cos/sin으로 x/y 계산, pt2로 Vec2 생성.
        // points.push: 생성된 점을 벡터에 추가.
        points.push(pt2(theta.cos() * r, theta.sin() * r));
    }

    // 🔹 스무스하게 보간된 점 생성: catmull_rom_spline 호출. &points로 슬라이스 참조 전달, segments=10으로 각 세그먼트당 10개 점 샘플링 (총 120*10=1200개 점으로 부드러움 증대).
    let smooth_points = catmull_rom_spline(&points, 10);

    // Model { smooth_points }: 초기화된 Model 인스턴스 반환. smooth_points를 필드에 할당.
    Model { smooth_points }
}

// update 함수: 앱의 업데이트 콜백. 매 프레임 업데이트 시 호출되지만, 여기서는 아무 작업도 하지 않습니다. _ 접두사로 사용되지 않음을 표시.
fn update(_app: &App, _model: &mut Model, _update: Update) {}

// view 함수: 렌더링 콜백. 매 프레임마다 호출되며, &App, &Model, Frame을 인수로 받습니다.
fn view(app: &App, model: &Model, frame: Frame) {
    // draw: app.draw()로 Draw 컨텍스트 생성. 이는 그래픽 명령을 기록하는 빌더-like 객체로, 배경, 선, 도형 등을 그립니다.
    let draw = app.draw();
    // draw.background().color(BLACK): 배경을 검은색으로 채웁니다. background 메서드는 전체 캔버스를 클리어하고 색상을 적용합니다.
    draw.background().color(BLACK);

    // 스무스 곡선 (닫힌 형태): polyline으로 부드러운 점들을 연결해 선 그리기.
    // draw.polyline(): 다각형 선을 그리는 메서드 체인 시작.
    // .weight(40.0): 선의 두께를 40.0으로 설정. (주석된 .weight(random_range(1.0, 2.0))은 랜덤 두께 예시, 현재 비활성.)
    // .color(WHITE): 선 색상을 흰색으로 설정.
    // .points_closed(model.smooth_points.clone()): 닫힌 폴리라인으로 점들을 지정. points_closed는 점 벡터를 받아 첫/마지막 점을 연결하며, clone()으로 소유권 이전.
    draw.polyline()
        .weight(40.0)
        // .weight(random_range(1.0, 2.0))
        .color(WHITE)
        .points_closed(model.smooth_points.clone());

    // draw.to_frame(app, &frame).unwrap(): Draw 명령을 Frame 버퍼에 적용. 렌더링을 완료하고 화면에 출력합니다.
    draw.to_frame(app, &frame).unwrap();
}

Catmull-Rom 스플라인 함수

fn catmull_rom_spline(points: &[Vec2], segments: usize) -> Vec<Vec2>

각진 폴리곤(여기서는 노이즈가 적용된 원형 점들)을 부드러운 곡선으로 변환하기 위해 사용된 핵심 함수입니다. 이 함수는 Catmull-Rom 스플라인 알고리즘을 구현하며, 주어진 점 배열(points) 사이를 cubic(3차) 곡선으로 보간합니다.

Catmull-Rom 스플라인의 개요

  • 목적:
    직선으로 연결된 폴리라인(각진 도형)을 자연스럽고 부드러운 곡선으로 smoothing합니다. 이는 컴퓨터 그래픽스(예: 애니메이션 경로, 폰트 아웃라인)에서 흔히 사용되며, 제어점이 곡선을 "통과"하도록 설계되어 직관적입니다.

  • 제어점:
    각 세그먼트는 4개의 점(p0, p1, p2, p3)을 사용합니다.

    • p0/p3: 인접 세그먼트의 "영향점" (tangent 제어).
    • p1/p2: 세그먼트의 시작/끝점 (곡선이 실제로 통과).
  • 장점:
    • C¹ 연속성(1차 미분 연속): 인접 세그먼트 간에 부드러운 접선(tangent) 연결.
    • 간단한 공식: 3차 다항식으로 계산 가능.
  • 단점: 과도한 곡률(overshoot)이 발생할 수 있으나, 여기서는 노이즈가 적용된 점으로 인해 자연스럽습니다.

  • 순환형 처리: 닫힌 도형(원형)을 위해 모듈 연산(% n)을 사용해 끝과 시작을 연결합니다.

함수 인수와 반환

  • points: &[Vec2]: 입력 슬라이스. 참조로 전달되어 소유권 이전 없이 사용. 각 Vec2는 2D 점(x, y).

  • segments: usize: 각 세그먼트(p1 ~ p2)당 샘플링할 중간 점 수. 예: 10이면 세그먼트당 11개 점(끝포함) 생성. 값이 클수록 부드러움 증가(하지만 성능 저하).

  • 반환: Vec<Vec2>: 보간된 모든 점의 벡터. 총 길이 = points.len() * segments. 이 점들을 polyline에 사용해 부드러운 선으로 렌더링.

  • 알고리즘 세부 (공식 유도)
    함수는 각 입력 점 i에 대해 p1-p2 세그먼트를 처리합니다. 공식은 Catmull-Rom의 표준 parametric equation입니다:

P(t)=12[(2P1)+(P0+P2)t+(2P05P1+4P2P3)t2+(P0+3P13P2+P3)t3]\mathbf{P}(t) = \frac{1}{2} \left[ (2\mathbf{P}_1) + (-\mathbf{P}_0 + \mathbf{P}_2)t + (2\mathbf{P}_0 - 5\mathbf{P}_1 + 4\mathbf{P}_2 - \mathbf{P}_3)t^2 + (-\mathbf{P}_0 + 3\mathbf{P}_1 - 3\mathbf{P}_2 + \mathbf{P}_3)t^3 \right]

  • t: [0, 1] 범위의 파라미터. j 루프로 segments만큼 균등 분할 (t = j / segments).

  • x/y 분리: 벡터 연산 대신 x와 y를 독립적으로 계산 (성능 최적화).

  • 계수 의미:

    • t^0 항: p1 중심.
    • t^1 항: p0-p2 간 선형 보간.
    • t^2 / t^3 항: 곡률 제어 (p0/p3가 영향을 줌).
  • 0.5 곱셈: 공식의 정규화 팩터.

사용 예시와 효과

  • 입력: 120개 노이즈 점 (model 함수에서 생성).
  • 출력: 120 * 10 = 1200개 점 → polyline으로 연결 시, 세그먼트가 매우 짧아 "각진" 느낌 없이 부드러운 곡선처럼 보임.
  • 활용: 반환된 점을 draw.polyline().points_closed()에 직접 전달. weight(10.0)으로 선 두께 설정.
  • 개선 팁: segments를 동적으로 조정하거나, tension 파라미터 추가(기본 Catmull-Rom은 tension=0)로 더 세밀한 제어 가능.
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0개의 댓글