🌕️

RUST MOON / 例え月が見えなくても

に公開

はじめに

秋も深まり、もうすぐ十五夜。
みなさんも一年で最も美しい月を楽しみにしていることでしょう。

しかし天候というものは不確実なもので、曇りや雨でせっかくの月が見えないこともあります。
そんなときのために、しっかりとプランBを用意しておきたいですよね。

RUST MOON

ということで Rust を使って月を作りました。

GACKT の8枚目のアルバム「LAST MOON」から着想を得ました。
約6年にも及ぶ GACKT の MOON プロジェクトの最終章。
僕はこのアルバムを聴いたことはありませんが、荘厳で幻想的ですよね。
GACKT の曲の中では Vanilla がすきです。

環境構築

Rust を install するには コチラ をご覧ください。
今回は bevy という Rust 製の Game Engine を使ってみましょう。
bevy は高機能なライブラリで docs はもちろん example も整備されています。

cargo init rust-moon
cd rust-moon
cargo add bevy

bevy が install できているか確認しましょう。
src/main.rs に次のコードで上書きします。

use bevy::prelude::*;

fn main() {
    App::new().run();
}

cargo r で実行しエラーが 出ないこと を確認しましょう。
特に何もなく終了できていれば OK です。

月の画像を用意する

お借りした画像は GitHub などで再配布可能なのかわからなかったので repo には含めていません。
コチラ から各自 DL してください。

rust-moon/assets/moon.png として配置します。

構成

RUST MOON では大きく分けて3つの要素でできています。

  • Background(背景のグラデーション)
  • Stars
  • Moon

フォルダ構成は次のとおりです。

.
├── assets
│   ├── background.wgsl
│   ├── moon.png
│   └── stars.wgsl
├── Cargo.lock
├── Cargo.toml
└── src
    ├── components.rs
    ├── constants.rs
    ├── main.rs
    ├── materials.rs
    └── systems.rs

bevy は wgsl を動的に読み込むため assets フォルダに配置するようです。
この example のコメントには次のように書かれています。

This example uses a shader source file from the assets subdirectory

src/ 以下の役割について簡単に説明します。

components.rs

Moon, Background, Stars の構造体を定義 を定義します。

use bevy::prelude::*;
use crate::materials::{BackgroundMaterial, StarsMaterial};

#[derive(Component)]
pub struct Moon;

#[derive(Component)]
pub struct Background(pub Handle<BackgroundMaterial>);

#[derive(Component)]
pub struct Stars(pub Handle<StarsMaterial>);

constants.rs

定数を定義します。
後で調整したくなりそうなものを別に切り出してみました。
出現箇所付近に定義したほうが管理しやすいものもありそうですね。
色相などの前後の処理を見ないと把握できないものは、特に定数化せずマジックナンバーのままの方がわかりやすいですね……

/// ウィンドウのタイトル
pub const WINDOW_TITLE: &str = "RUST MOON";
/// 背景と星空のメッシュサイズ(正方形の一辺の長さ)
pub const BACKGROUND_SIZE: f32 = 2000.0;
/// 月の基本的な明るさの強度(0.0〜1.0)
pub const MOON_BASE_INTENSITY: f32 = 0.95;
/// 月のグロー効果の振幅(明るさの変化幅)
pub const MOON_GLOW_AMPLITUDE: f32 = 0.05;
/// 月のグロー効果のアニメーション速度(Hz)
pub const MOON_GLOW_FREQUENCY: f32 = 3.0;
/// 月の色相の変化幅(0.0〜1.0)
pub const MOON_COLOR_VARIATION: f32 = 0.1;
/// 月の色相変化のアニメーション速度(Hz)
pub const MOON_COLOR_FREQUENCY: f32 = 2.5;

materials.rs

shader とマッピングする Material を定義します。

use bevy::{
    prelude::*,
    reflect::TypePath,
    render::render_resource::{AsBindGroup, ShaderRef},
    sprite::Material2d,
};

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct BackgroundMaterial {
    // wgsl に引き渡したいパラメータを設定
    #[uniform(0)]
    pub color_top: LinearRgba,
    #[uniform(1)]
    pub color_bottom: LinearRgba,
}

impl Material2d for BackgroundMaterial {
    // ShaderRef は `assets/` フォルダを自動的に指定するため
    // assets は書かなくて ok
    // assets 外に wgsl を置くことはできない?かもしれない
    // できなさそう
    fn fragment_shader() -> ShaderRef {
        "background.wgsl".into()
    }
}

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct StarsMaterial {
    #[uniform(0)]
    pub time: f32,
}

impl Material2d for StarsMaterial {
    fn fragment_shader() -> ShaderRef {
        "stars.wgsl".into()
    }

    fn alpha_mode(&self) -> bevy::sprite::AlphaMode2d {
        // stars は背景を透過して重ねたい
        bevy::sprite::AlphaMode2d::Blend
    }
}

systems.rs

アプリの動きを定義します。

use bevy::{
    prelude::*,
    render::mesh::Mesh2d,
    sprite::MeshMaterial2d,
};
use crate::{
    components::{Background, Moon, Stars},
    constants::*,
    materials::{BackgroundMaterial, StarsMaterial},
};

pub fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut background_materials: ResMut<Assets<BackgroundMaterial>>,
    mut stars_materials: ResMut<Assets<StarsMaterial>>,
    asset_server: Res<AssetServer>,
) {
    commands.spawn(Camera2d);

    // 背景グラデーション
    let background_material = background_materials.add(BackgroundMaterial {
        color_top: LinearRgba::rgb(0.0, 0.0, 0.1),
        color_bottom: LinearRgba::rgb(0.1, 0.05, 0.2),
    });

    commands.spawn((
        Mesh2d(meshes.add(Rectangle::new(BACKGROUND_SIZE, BACKGROUND_SIZE))),
        MeshMaterial2d(background_material.clone()),
        Transform::from_xyz(0.0, 0.0, -2.0),
        Background(background_material),
    ));

    // 星空レイヤー
    let stars_material = stars_materials.add(StarsMaterial {
        time: 0.0,
    });

    commands.spawn((
        Mesh2d(meshes.add(Rectangle::new(BACKGROUND_SIZE, BACKGROUND_SIZE))),
        MeshMaterial2d(stars_material.clone()),
        Transform::from_xyz(0.0, 0.0, -1.0),
        Stars(stars_material),
    ));

    // 月
    let moon = asset_server.load("moon.png");
    commands.spawn((
        Sprite {
            image: moon,
            color: Color::srgb(1.0, MOON_BASE_INTENSITY, 0.7),
            ..default()
        },
        Transform::from_scale(Vec3::splat(1.0)),
        Moon,
    ));
}

pub fn animate_moon_glow(
    time: Res<Time>,
    // Moon への参照
    mut moon_query: Query<&mut Sprite, With<Moon>>
) {
    let elapsed = time.elapsed_secs();

    for mut sprite in moon_query.iter_mut() {
        // sinによる色のアニメーション
        let intensity = MOON_BASE_INTENSITY + MOON_GLOW_AMPLITUDE * (elapsed * MOON_GLOW_FREQUENCY).sin();
        sprite.color = Color::srgb(1.0, intensity, 0.7 + MOON_COLOR_VARIATION * (elapsed * MOON_COLOR_FREQUENCY).sin());
    }
}

pub fn update_stars_time(
    time: Res<Time>,
    // stars への参照
    stars_query: Query<&Stars>,
    // material の time を変更したいので mutable な参照を取得する
    mut materials: ResMut<Assets<StarsMaterial>>,
) {
    for stars in stars_query.iter() {
        if let Some(material) = materials.get_mut(&stars.0) {
            material.time = time.elapsed_secs();
        }
    }
}

main.rs

アプリのエントリーポイントですので、シンプルですね。

mod components;
mod constants;
mod materials;
mod systems;

use bevy::{
    prelude::*,
    sprite::Material2dPlugin,
    window::{Window, WindowPlugin},
};
use constants::WINDOW_TITLE;
use materials::{BackgroundMaterial, StarsMaterial};
use systems::{animate_moon_glow, setup, update_stars_time};

fn main() {
    App::new()
        .add_plugins((
            // window のタイトルを設定
            // 何も指定しないと App になる
            DefaultPlugins.set(WindowPlugin {
                primary_window: Some(Window {
                    title: WINDOW_TITLE.to_string(),
                    ..default()
                }),
                ..default()
            }),
            Material2dPlugin::<BackgroundMaterial>::default(),
            Material2dPlugin::<StarsMaterial>::default(),
        ))
        // init
        .add_systems(Startup, setup)
        // 毎フレーム処理する機能
        .add_systems(Update, (animate_moon_glow, update_stars_time))
        .run();
}

月の色を変える

普段の皆さんは、黄金に輝く月を楽しんでいると思います。
しかし機関の命令により、幼馴染をその手にかけなくてはいけない時もありますよね。
その際は紅く狂った月が求められます。

ちょっと変えてみましょう。
systems.rs を開いてください。

まずは setup で月の初期化を変えます。

// 月
let moon = asset_server.load("moon.png");
commands.spawn((
    Sprite {
        image: moon,
        color: Color::srgb(1.0, 0.0, 0.0),  // 真っ赤
        ..default()
    },
    Transform::from_scale(Vec3::splat(1.0)),
    Moon,
));

次に animate_moon_glow 関数も変えてみましょう。

pub fn animate_moon_glow(time: Res<Time>, mut moon_query: Query<&mut Sprite, With<Moon>>) {
    let elapsed = time.elapsed_secs();

    for mut sprite in moon_query.iter_mut() {
        let intensity =
            MOON_BASE_INTENSITY + MOON_GLOW_AMPLITUDE * (elapsed * MOON_GLOW_FREQUENCY).sin();
        // 赤い月のグロー効果(赤の濃淡のみ変化)
        sprite.color = Color::srgb(
            intensity,
            0.0,
            0.0,
        );
    }
}

shader もちょっとみてみよう

wgsl は WebGPU ですね。
Rust に似た言語でとても洗練されています。

// background.wgsl
// bevy が提供している型を import
#import bevy_sprite::mesh2d_vertex_output::VertexOutput

@group(2) @binding(0) var<uniform> color_top: vec4<f32>;
@group(2) @binding(1) var<uniform> color_bottom: vec4<f32>;

// fragment shader としてマーク
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    let t = in.uv.y;
    let color = mix(color_bottom, color_top, t);

    return vec4<f32>(color.rgb, 1.0);
}

すべてのピクセルを塗っていくため is_star という判定を使って、星を描画するか決定します。

// stars.wgsl
#import bevy_sprite::mesh2d_vertex_output::VertexOutput

@group(2) @binding(0) var<uniform> time: f32;

fn hash2(p: vec2<f32>) -> f32 {
    return fract(sin(dot(p, vec2<f32>(12.9898, 78.233))) * 43758.5453);
}

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    // 複数のスケールで星を生成
    var star_glow = 0.0;

    // 3つの異なるスケールで星を生成
    for (var layer = 0; layer < 3; layer++) {
        let scale = 30.0 + f32(layer) * 25.0;
        let uv_scaled = in.uv * scale;
        let cell = floor(uv_scaled);
        let local_uv = fract(uv_scaled);

        // レイヤーごとに異なる確率
        let star_probability = 0.07 - f32(layer) * 0.015;
        let is_star = hash2(cell + vec2<f32>(f32(layer) * 100.0, 0.0)) < star_probability;

        if (is_star) {
            // 星の中心をランダムに配置
            let star_pos = vec2<f32>(
                hash2(cell + vec2<f32>(1.0, f32(layer))),
                hash2(cell + vec2<f32>(f32(layer), 1.0))
            );
            let dist = distance(local_uv, star_pos);

            // 星のサイズをランダムに
            let star_size = 0.06 + hash2(cell + vec2<f32>(2.0, f32(layer))) * 0.06;

            // きらめきアニメーション
            let twinkle_speed = 1.0 + hash2(cell + vec2<f32>(3.0, 3.0)) * 3.0;
            let twinkle_offset = hash2(cell + vec2<f32>(4.0, 4.0)) * 6.28;
            let twinkle = sin(time * twinkle_speed + twinkle_offset) * 0.5 + 0.5;

            if (dist < star_size) {
                // 星の中心ほど明るく
                let brightness = 1.0 - (dist / star_size);
                let star_intensity = brightness * brightness * brightness;

                // グロー効果の追加(きらめきを適用)
                star_glow += star_intensity * 2.0 * (0.5 + twinkle * 0.5);
            } else if (dist < star_size * 3.0) {
                // 星の周りのグロー効果
                let glow_brightness = 1.0 - (dist / (star_size * 3.0));
                star_glow += glow_brightness * glow_brightness * 0.3 * twinkle;
            }
        }
    }

    // 星の色(白から淡い青白い色)
    let star_color = vec4<f32>(
        star_glow * 0.95,
        star_glow * 0.97,
        star_glow * 1.0,
        min(star_glow, 1.0)
    );

    return star_color;
}

さいごに

十五夜の準備が整いましたね。
静かにその瞬間(とき)を待ちましょう。

ドクターメイト

Discussion