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())
}