:: BASIC_07_star_rotation

BamgasiJM·2025년 9월 19일

Nannou <BASIC>

목록 보기
20/41
post-thumbnail

📝 Rust Code

use nannou::prelude::*;

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

struct Model {
    rotation: f32,
}

fn model(_app: &App) -> Model {
    _app.new_window().size(600, 600).view(view).build().unwrap();
    Model { rotation: 0.0 }
}

fn update(_app: &App, model: &mut Model, update: Update) {
    model.rotation += update.since_last.as_secs_f32() * 30.0;  // Rotate 30 degrees per second
}

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

    const NUM_POINTS: usize = 5;  // Number of star points
    let radius_outer = 200.0;
    let radius_inner = 100.0;

    let mut points = Vec::new();  // Vector to store points (Rust: Vec<T> dynamic array)
    for i in 0..NUM_POINTS * 2 {
        let radius = if i % 2 == 0 { radius_outer } else { radius_inner };
        let angle = deg_to_rad((i as f32 * 360.0 / (NUM_POINTS as f32 * 2.0)) + model.rotation);
        points.push(vec2(angle.cos() * radius, angle.sin() * radius));
    }

    // Apply rotation and scale to the Draw context
    let scale_factor = 1.0 + (app.time.sin() * 0.2);  // Scale varies with time
    let rotation = deg_to_rad(model.rotation * 0.5);  // Additional rotation

    draw.rotate(rotation)
        .scale(scale_factor)
        .polygon()
        .points(points)
        .color(PLUM);

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

📝 Rust Code + Comment

// nannou::prelude::* 는 Nannou 라이브러리의 가장 필수적이고 자주 사용되는
// 요소들(함수, 타입, 상수 등)을 모두 현재 스코프(scope)로 가져오는 역할을 합니다.
// 이렇게 하면 매번 nannou::something::something 처럼 길게 쓰지 않고도
// App, Frame, draw, WHITE, PLUM 등과 같은 이름들을 바로 사용할 수 있어 코드가 간결해집니다.
use nannou::prelude::*;

// 모든 Rust 프로그램은 main 함수에서 시작됩니다.
fn main() {
    // nannou::app() 함수는 Nannou 애플리케이션을 설정하고 시작하는 역할을 합니다.
    // model 함수를 전달하여 앱의 초기 상태(Model)를 설정하고,
    // update 함수를 전달하여 매 프레임마다 상태를 갱신하는 로직을 지정합니다.
    // .run()은 이 모든 설정을 바탕으로 앱을 실제로 실행시킵니다.
    nannou::app(model).update(update).run();
}

// Model 구조체(struct)는 애플리케이션의 '상태(state)'를 저장하기 위한 데이터 구조입니다.
// 이 앱에서는 별의 현재 회전 각도(rotation)를 저장하기 위해 사용합니다.
// f32는 32비트 부동소수점 숫자 타입으로, 소수점을 포함하는 숫자를 나타냅니다.
struct Model {
    rotation: f32,
}

// model 함수는 Nannou 앱이 시작될 때 단 한 번 호출됩니다.
// 앱의 초기 상태(Model)를 설정하고 창(window)을 생성하는 역할을 합니다.
// _app: &App 인자는 앱의 전반적인 상태에 접근할 수 있게 해주지만, 이 함수에서는 사용하지 않습니다.
fn model(_app: &App) -> Model {
    // _app.new_window()를 통해 새 창을 생성하는 빌더(builder)를 시작합니다.
    _app.new_window()
        // .size() 메소드로 창의 가로, 세로 크기를 600픽셀로 설정합니다.
        .size(600, 600)
        // .view() 메소드는 이 창에 그림을 그릴 때 사용할 함수를 지정합니다. 여기서는 view 함수를 사용합니다.
        .view(view)
        // .build() 메소드는 지금까지의 설정을 바탕으로 실제 창을 생성합니다.
        // unwrap()은 창 생성이 실패할 경우 프로그램을 패닉(panic) 상태로 만듭니다.
        // 여기서는 창 생성이 거의 항상 성공한다고 가정하고 간단하게 사용합니다.
        .build()
        .unwrap();
    
    // Model 구조체의 인스턴스(실체)를 생성하여 반환합니다.
    // rotation 필드를 0.0으로 초기화하여 별이 처음에는 회전하지 않은 상태로 시작하게 합니다.
    Model { rotation: 0.0 }
}

// update 함수는 매 프레임마다 호출되어 앱의 상태(Model)를 갱신합니다.
// 애니메이션 로직을 처리하는 곳입니다.
// _app: &App - 앱의 전반적인 상태 (여기서는 사용 안 함)
// model: &mut Model - 앱의 상태를 '가변적으로(mutably)' 빌려와서 수정할 수 있도록 합니다.
// update: Update - 이전 프레임과의 시간 차이 등 업데이트 관련 정보를 담고 있습니다.
fn update(_app: &App, model: &mut Model, update: Update) {
    // model의 rotation 값에 이전 프레임 이후 흐른 시간(초 단위) * 30.0을 더해줍니다.
    // update.since_last.as_secs_f32()는 이전 프레임과 현재 프레임 사이의 시간 간격을 f32 타입의 초 단위로 반환합니다.
    // 이를 통해 컴퓨터 성능과 관계없이 일관된 속도로 회전하게 됩니다. (초당 30도)
    model.rotation += update.since_last.as_secs_f32() * 30.0;
}

// view 함수는 매 프레임마다 update 함수가 실행된 후 호출됩니다.
// 현재 앱의 상태(Model)를 바탕으로 화면에 실제로 그림을 그리는 역할을 합니다.
// app: &App - 그리기 도구(draw)를 얻기 위해 필요합니다.
// model: &Model - 현재 상태(회전값 등)를 '불변적으로(immutably)' 빌려와서 읽기만 합니다.
// frame: Frame - 그림을 그릴 캔버스(프레임) 자체를 나타냅니다.
fn view(app: &App, model: &Model, frame: Frame) {
    // app.draw()는 현재 프레임에 그림을 그릴 수 있는 Draw 인스턴스를 생성합니다.
    let draw = app.draw();
    // draw.background()로 배경을 그리기 시작하고, .color(WHITE)로 색상을 흰색으로 지정합니다.
    draw.background().color(WHITE);

    // 상수를 정의합니다. const는 컴파일 시간에 값이 결정되는 불변 변수입니다.
    const NUM_POINTS: usize = 5;  // 별의 꼭짓점 개수 (5개 -> 오각별)
    let radius_outer = 200.0;     // 별의 바깥쪽 꼭짓점까지의 반지름
    let radius_inner = 100.0;     // 별의 안쪽 꼭짓점까지의 반지름

    // 별의 꼭짓점 좌표들을 저장할 동적 배열(Vector)을 생성합니다.
    // Vec<T>는 Rust의 표준 라이브러리가 제공하는 가변 크기 배열입니다.
    let mut points = Vec::new();
    // 0부터 (5 * 2 - 1)까지, 즉 0부터 9까지 총 10번 반복합니다. (바깥점 5개, 안쪽점 5개)
    for i in 0..NUM_POINTS * 2 {
        // i가 짝수이면 바깥쪽 반지름을, 홀수이면 안쪽 반지름을 사용합니다.
        // Rust에서 if는 표현식(expression)이므로, 결과값을 변수에 바로 할당할 수 있습니다.
        let radius = if i % 2 == 0 { radius_outer } else { radius_inner };
        // 각 꼭짓점의 각도를 계산합니다.
        // 1. i를 f32 타입으로 변환합니다.
        // 2. 전체 360도를 총 꼭짓점 개수(10)로 나눈 각도에 i를 곱해 기본 위치를 정합니다.
        // 3. model.rotation을 더해 시간에 따라 회전시킵니다.
        // 4. deg_to_rad() 함수로 각도(degree)를 라디안(radian)으로 변환합니다. (삼각함수는 라디안 사용)
        let angle = deg_to_rad((i as f32 * 360.0 / (NUM_POINTS as f32 * 2.0)) + model.rotation);
        // 계산된 각도와 반지름을 사용하여 데카르트 좌표(x, y)를 계산하고,
        // vec2() 함수로 2D 벡터를 만들어 points 벡터에 추가(push)합니다.
        points.push(vec2(angle.cos() * radius, angle.sin() * radius));
    }

    // 그리기 컨텍스트(Draw)에 변환(Transform)을 적용합니다.
    // app.time은 앱 시작 후 흐른 총 시간(초)입니다.
    // sin 함수를 이용해 -0.2에서 +0.2 사이를 주기적으로 변하는 값을 만들고 1.0을 더해
    // 스케일 팩터가 0.8에서 1.2 사이를 부드럽게 오가도록 합니다. (пульсирующий эффект)
    let scale_factor = 1.0 + (app.time.sin() * 0.2);
    // 추가적인 회전을 적용합니다. model.rotation의 절반 속도로 회전합니다.
    let rotation = deg_to_rad(model.rotation * 0.5);

    // .rotate()와 .scale() 메소드를 체이닝(chaining)하여 Draw 객체에 변환을 적용합니다.
    // 이 변환들은 이후에 그리는 모든 도형에 영향을 줍니다.
    draw.rotate(rotation)
        .scale(scale_factor)
        // .polygon()으로 다각형을 그리기 시작합니다.
        .polygon()
        // .points()로 다각형을 구성할 꼭짓점들의 리스트(points 벡터)를 전달합니다.
        .points(points)
        // .color()로 다각형의 색상을 자두색(PLUM)으로 지정합니다.
        .color(PLUM);

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

📝 구조 및 개념 상세 설명

1. Nannou 앱의 기본 구조: model, update, view

Nannou는 많은 인터랙티브 그래픽 프레임워크처럼 Model-Update-View (또는 Model-View-Controller) 아키텍처를 따릅니다.

🧩 Model: 앱의 상태(State)를 담는 데이터 구조입니다. 이 코드에서는 별의 회전 각도(rotation)가 바로 상태입니다. 화면에 보이는 모든 것은 이 Model 데이터로부터 계산됩니다.

🧩 model() 함수: 앱이 시작될 때 단 한 번 실행됩니다. 창을 설정하고 Model의 초기 상태를 만들어 반환하는 역할을 합니다.

🧩 update() 함수: 매 프레임마다 view 함수보다 먼저 호출됩니다. Model의 상태를 변경하는 역할을 합니다. 즉, 애니메이션 로직(시간에 따라 rotation 값을 증가시키는 것)이 여기에 들어갑니다.

🧩 view() 함수: 매 프레임마다 update 함수 실행 직후에 호출됩니다. update 함수를 거쳐 변경된 Model의 현재 상태를 바탕으로 화면에 그림을 그립니다.

이러한 구조는 데이터(상태)로직(갱신, 그리기)을 명확하게 분리하여 코드를 체계적으로 관리할 수 있게 해줍니다.


2. 언더스코어( _ ) 접두사의 의미와 필요성

코드에서 model 함수와 update 함수를 보면 _app: &App 처럼 인자 이름 앞에 _가 붙어있습니다.

Rust 컴파일러는 매우 엄격해서, 선언된 변수나 함수 인자가 코드 내에서 한 번도 사용되지 않으면 "unused variable" 경고를 발생시킵니다. 이는 개발자가 실수로 변수를 만들고 사용하는 것을 잊었을 가능성을 알려주는 유용한 기능입니다.

하지만 modelupdate 함수의 경우, Nannou 프레임워크가 정해놓은 함수 시그니처(function signature)를 반드시 따라야 합니다. Nannou는 model 함수를 호출할 때 항상 &App 타입의 인자를 전달하도록 설계되어 있습니다. 비록 우리 코드에서는 이 인자가 필요 없더라도, Nannou가 우리 함수를 올바르게 호출하려면 그 자리에 인자를 받아야만 합니다.

이때 변수명 앞에 _를 붙이면, "나는 이 변수가 존재해야 한다는 것은 알지만, 의도적으로 사용하지 않을 것입니다." 라고 컴파일러에게 알려주는 역할을 합니다. 결과적으로 컴파일러는 불필요한 경고 메시지를 보여주지 않습니다.

Q. 아예 인자를 코드에 넣지 않으면 안되나요?
A. 안됩니다. 앞서 설명했듯이 Nannou 프레임워크는 model 함수를 호출할 때 fn(&App) -> Model 이라는 타입의 함수를 기대합니다. 만약 우리가 fn() -> Model 처럼 인자 없이 함수를 정의하면, Nannou가 기대하는 함수 타입과 달라서 컴파일 에러가 발생합니다.

비유: 은행 창구에 가서 특정 서류 양식을 제출해야 한다고 상상해보세요. 그 양식에는 '추천인' 칸이 있지만, 당신은 추천인이 필요 없습니다. 그렇다고 해서 '추천인' 칸이 아예 없는 다른 양식을 제출할 수는 없습니다. 반드시 정해진 양식에 '해당 없음' 이라고 쓰거나 비워두는 것처럼, 함수도 정해진 인자 목록(양식)을 그대로 유지해야 합니다. _를 붙이는 것은 '이 칸은 있지만 제게는 필요 없습니다'라고 표시하는 것과 같습니다.


3. app: App 에서 대소문자 차이

Rust에서는 변수와 타입의 이름을 짓는 데 강력한 네이밍 컨벤션(naming convention)이 있습니다.

  • App (파스칼 케이스, PascalCase): struct, enum, trait 등 타입(Type)의 이름을 나타냅니다. '설계도'나 '틀'에 해당합니다.
  • app (스네이크 케이스, snake_case): 변수, 함수 이름 등 값(Value)을 나타냅니다. App이라는 설계도로 만들어진 '실체' 또는 '인스턴스'입니다.

따라서 app: &App 라는 표현은 다음과 같이 해석할 수 있습니다.
" app 이라는 이름의 변수가 있으며, 이 변수의 타입은 App 이라는 타입의 참조자(&)입니다. "

이 컨벤션은 코드의 가독성을 매우 높여줍니다. 대문자로 시작하면 '아, 이건 타입이구나', 소문자로 시작하면 '이건 실제 데이터를 담고 있는 변수구나'라고 즉시 파악할 수 있습니다.


4. 주요 Rust 문법 포인트

  • struct Model { ... }: 구조체(Structure). 관련된 여러 데이터를 묶어서 새로운 타입을 만드는 방법입니다. C/C++의 struct나 객체 지향 언어의 클래스와 유사한 개념입니다.

  • let draw = ...;: 변수 선언. let 키워드는 변수를 선언할 때 사용합니다. 기본적으로 Rust의 변수는 불변(immutable)입니다. 즉, 한 번 값이 할당되면 바꿀 수 없습니다.

  • let mut points = ...;: 가변 변수 선언. mut 키워드를 붙이면 해당 변수를 가변(mutable)으로 만들어 값을 변경할 수 있게 됩니다. points 벡터에 push를 통해 새 꼭짓점을 추가해야 하므로 mut이 필요합니다.

  • model: &mut Model: 가변 참조자(Mutable Reference). update 함수는 Modelrotation 값을 직접 수정해야 합니다. &mut는 데이터의 소유권을 가져오지 않으면서 해당 데이터를 '빌려서 수정할 권한'을 얻는 방법입니다.

  • model: &Model: 불변 참조자(Immutable Reference). view 함수는 Modelrotation 값을 읽기만 할 뿐, 수정하지는 않습니다. &는 데이터를 '빌려서 읽을 권한'만 얻는 방법입니다. Rust의 이런 소유권(Ownership)대여(Borrowing) 시스템은 메모리 안전성을 보장하는 핵심적인 특징입니다.

  • if i % 2 == 0 { ... } else { ... }: Rust에서 if-else표현식(expression)입니다. 이는 if-else 블록 전체가 하나의 값을 반환할 수 있다는 의미입니다. 그래서 let radius = if ... 처럼 if-else의 결과를 변수에 바로 할당하는 간결한 코드를 작성할 수 있습니다.


✅ ⚙️🛠️📄📚🎨🖼️🧩🧠🧮🕹️🧿⏳🎛️🔮🌌🪩🗂️📝🧰🔒

아보카도
Sample
Important
Highlighting
비색
적황색
purple
hot pink
yellow
살구색
orange
green
blue

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

0개의 댓글