App

scenes.rs(共享场景与项目装配)

use std::f64::consts::{PI, TAU};

use ranim::{
    anims::camera::CameraFrameAnim,
    color::palettes::manim,
    core::transformed::{Transformed, TransformedExt},
    glam::{DAffine3, DVec3},
    items::{
        hierarchy::Node,
        mesh::{
            MeshItem,
            gltf::{gltf, node_tree_from_gltf},
        },
    },
    prelude::*,
    utils::rate_functions::linear,
};

const MODELS: [(&str, &[u8]); 3] = [
    ("belt.glb", include_bytes!("../../assets/models/belt.glb")),
    (
        "storage.glb",
        include_bytes!("../../assets/models/storage.glb"),
    ),
    (
        "generator.glb",
        include_bytes!("../../assets/models/generator.glb"),
    ),
];

/// Loads a model (the loader already converts glTF's Y-up to ranim's Z-up),
/// centers it on its own axes and normalizes its size, so it can be posed
/// by rotating about its vertical axis and placed by shifting its root.
fn load_model(name: &str, bytes: &[u8], size: f64) -> Transformed<Node<MeshItem>, DAffine3> {
    let parsed = gltf::Gltf::from_slice(bytes)
        .unwrap_or_else(|error| panic!("failed to parse embedded {name}: {error}"));
    let tree = node_tree_from_gltf(&parsed.document, |buffer| match buffer.source() {
        gltf::buffer::Source::Bin => parsed.blob.as_deref(),
        gltf::buffer::Source::Uri(_) => None,
    })
    .tree;
    let [min, max] = tree.aabb();
    let extents = max - min;
    // Place the tree by wrapping it — the pose stays external.
    let mut placed = tree.transformed(DAffine3::IDENTITY);
    placed.shift(-(min + max) / 2.0);
    placed.scale_uniform(size / extents.x.max(extents.y).max(extents.z));
    placed
}

/// Rotating showcase of the three dimension-factory machines. Each model is
/// a `Node<MeshItem>` tree; the spin is a per-frame root pose on the tree —
/// no vertex data is ever rewritten, and extraction composes the matrices
/// just like any other frame.
///
/// The POC glTF loader ignores materials, so each machine is tinted with a
/// palette color instead.
#[scene]
#[wasm_demo_doc]
#[output(dir = "./output/gltf_showcase")]
fn gltf_showcase(r: &mut RanimScene) {
    let total_secs = 10.0;
    let fade_secs = 1.0;
    let spin_revolutions = 2.0;

    let mut cam = CameraFrame::from_spherical(65.0 * PI / 180.0, 25.0 * PI / 180.0, 9.5);
    cam.fovy = 40.0 * PI / 180.0;
    r.play(
        cam.orbit(DVec3::ZERO, PI / 5.0)
            .with_duration(total_secs)
            .with_rate_func(linear),
    );

    let slots = [-3.6, 0.0, 3.6];
    let colors = [manim::BLUE_D, manim::TEAL_D, manim::PURPLE_D];
    for (i, ((name, bytes), &slot)) in MODELS.iter().zip(slots.iter()).enumerate() {
        let mut tree = load_model(name, bytes, 2.2);
        tree.set_fill_color(colors[i]);

        // Spin about the model's own vertical axis while fading in over the
        // first second. The closure receives the segment's normalized alpha,
        // so durations are derived from total_secs. Everything is per-frame
        // data on the tree: local geometry stays constant throughout.
        let phase = i as f64 * TAU / 3.0;
        r.play(
            Pure::new(move |alpha| {
                let mut model = tree.clone();
                model.set_fill_opacity(((alpha * total_secs / fade_secs) as f32).min(1.0));
                model.rotate_on_axis(DVec3::Z, alpha * TAU * spin_revolutions + phase);
                model.shift(DVec3::X * slot);
                model
            })
            .with_duration(total_secs),
        );
    }

    r.insert_time_mark(2.0, TimeMark::Capture("preview_models.png".to_string()));
}

/// Register the scenes provided by this example.
pub struct ScenesPlugin;

impl ranim::Plugin for ScenesPlugin {
    fn build(&self, app: &mut bevy_app::App) {
        app.add_scenes([gltf_showcase::scene()]);

        #[cfg(feature = "render")]
        {
            use ranim::{
                core::transformed::Transformed,
                glam::DAffine3,
                items::{RanimItemsPlugin, hierarchy::Node, mesh::MeshItem},
                render::{RenderSetup, extract::ExtractPlugin},
            };

            app.insert_resource(RenderSetup::new(|render_app| {
                render_app.add_plugins((
                    RanimItemsPlugin,
                    ExtractPlugin::<Transformed<Node<MeshItem>, DAffine3>>::default(),
                ));
            }));
        }
    }
}

main.rs(平台入口)

mod scenes;

#[cfg(not(target_arch = "wasm32"))]
fn main() {
    #[cfg(feature = "cmd-render")]
    {
        use ranim::prelude::*;
        let mut app = App::new();
        app.add_plugins(scenes::ScenesPlugin);
        run_cli(app.build());
    }
    #[cfg(not(feature = "cmd-render"))]
    compile_error!("native examples require the `cmd-render` feature (enabled by default)");
}

// The WASM module is driven by JavaScript through app().
#[cfg(target_arch = "wasm32")]
fn main() {}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn app() -> ranim::wasm::WasmApp {
    use ranim::prelude::*;

    let mut app = App::new();
    app.add_plugins(scenes::ScenesPlugin);
    ranim::wasm::WasmApp::new(app.build())
}