App

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

//! Z-fighting / draw-order verification.
//!
//! Ideal behavior: items occupying the same position are drawn in scene
//! insertion order (the order they were added to the scene), and the result
//! is stable (no per-pixel flicker between the two colors).
//!
//! Every item is played as its own cell (`r.play(item.show())`), so the frame
//! contains all items with scene order = play order. Pairs of coplanar items
//! sit at the same position; the *later-inserted* (blue) item should win
//! everywhere in the overlap, deterministically.
//!
//! Scenes:
//! - `opaque_front`       : straight-on camera (control — insertion order works).
//! - `opaque_angled`      : angled ortho camera — reproduction: the
//!   later-inserted blue circle loses chunks of the overlap to the red square.
//! - `translucent_front`  : translucent fills go through the OIT path, whose
//!   compositing order for depth ties is arrival order, not insertion order.
//! - `z_precision`        : z-gap sweep, straight-on (control — all clean).
//! - `z_precision_angled` : z-gap sweep, angled. Each square has its own
//!   color so the streaks identify which square's fragments win: the fighting
//!   appears for gaps around 1e-4..1e-2 and disappears again at 1e-1.
//! - `z_sweep`            : one pair whose z separation sweeps through zero.
//! - `flicker`            : camera orbiting one pair — the winning color
//!   changes over time on identical scene state (temporal instability).

use ranim::{
    anims::camera::CameraFrameAnim,
    color::{AlphaColor, Srgb, palettes::manim},
    glam::{DVec3, dvec3},
    items::vitem::geometry::{Circle, Square},
    prelude::*,
    utils::rate_functions::linear,
};

fn square(color: AlphaColor<Srgb>) -> Transformed<Square, Translation> {
    Square::new(2.0)
        .with(|s| {
            s.set_fill_color(color);
        })
        .transformed(Translation(dvec3(0.0, 0.0, 0.0)))
}

fn circle(color: AlphaColor<Srgb>) -> Transformed<Circle, Translation> {
    Circle::new(1.2)
        .with(|c| {
            c.set_fill_color(color);
        })
        .transformed(Translation(dvec3(0.0, 0.0, 0.0)))
}

/// Play `red` then `blue` at the same position `pos` for the whole scene.
///
/// Two separate cells: red has the lower scene order and should lose the
/// overlap against blue everywhere.
macro_rules! pair {
    ($r:expr, $red:expr, $blue:expr, $pos:expr, $total:expr) => {{
        let mut red = $red;
        let mut blue = $blue;
        red.transform = Translation($pos);
        blue.transform = Translation($pos);
        $r.play(red.show().with_duration($total));
        $r.play(blue.show().with_duration($total));
    }};
}

/// red square + blue circle pair, both lifted to `z`.
macro_rules! pair_z {
    ($r:expr, $x:expr, $z:expr, $total:expr) => {{
        pair!(
            $r,
            square(manim::RED_C),
            circle(manim::BLUE_C),
            dvec3($x, 0.0, $z),
            $total
        );
    }};
}

fn angled_ortho_camera() -> CameraFrame {
    let mut cam = CameraFrame::from_spherical(60f64.to_radians(), 30f64.to_radians(), 30.0);
    cam.perspective_blend = 0.0;
    cam
}

fn setup_camera(r: &mut RanimScene, dur: f64) {
    r.play(CameraFrame::default().show().with_duration(dur));
}

fn capture(r: &mut RanimScene, total: f64, name: &str) {
    r.insert_time_mark(total - 0.2, TimeMark::Capture(name.to_string()));
}

/// Straight-on ortho camera:
/// - left pair: identical squares at z=0 (control, insertion order)
/// - middle pair: square + circle at z=0
/// - right pair: square + circle at z=0.5
#[scene]
#[output(dir = "./output/z_fighting")]
fn opaque_front(r: &mut RanimScene) {
    let total = 2.2;
    setup_camera(r, total);

    pair!(
        r,
        square(manim::RED_C),
        square(manim::BLUE_C),
        dvec3(-4.5, 0.0, 0.0),
        total
    );
    pair_z!(r, 0.0, 0.0, total);
    pair_z!(r, 4.5, 0.5, total);

    capture(r, total, "opaque_front.png");
}

/// Same layout as `opaque_front`, but the ortho camera looks from an angle,
/// so the items' planes are no longer perpendicular to the view direction.
#[scene]
#[output(dir = "./output/z_fighting")]
fn opaque_angled(r: &mut RanimScene) {
    let total = 2.2;
    r.play(angled_ortho_camera().show().with_duration(total));

    pair!(
        r,
        square(manim::RED_C),
        square(manim::BLUE_C),
        dvec3(-4.5, 0.0, 0.0),
        total
    );
    pair_z!(r, 0.0, 0.0, total);
    pair_z!(r, 4.5, 0.5, total);

    capture(r, total, "opaque_angled.png");
}

/// Straight-on camera, translucent fills (OIT path):
/// - left pair: red then blue, both 50% alpha, same position
/// - right pair: opaque red, then 50% blue on top
#[scene]
#[output(dir = "./output/z_fighting")]
fn translucent_front(r: &mut RanimScene) {
    let total = 2.0;
    setup_camera(r, total);

    pair!(
        r,
        square(manim::RED_C.with_alpha(0.5)),
        square(manim::BLUE_C.with_alpha(0.5)),
        dvec3(-4.5, 0.0, 0.5),
        total
    );
    pair!(
        r,
        square(manim::RED_C),
        square(manim::BLUE_C.with_alpha(0.5)),
        dvec3(4.5, 0.0, 0.5),
        total
    );

    capture(r, total, "translucent_front.png");
}

/// square + circle pairs (different geometry, same plane) lifted to z=0.5,
/// with the blue circle separated by a growing z-gap, straight-on camera.
/// Control: insertion order holds for every gap.
#[scene]
#[output(dir = "./output/z_fighting")]
fn z_precision(r: &mut RanimScene) {
    let total = 3.4;
    setup_camera(r, total);

    let gaps = [0.0, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1];
    for (i, gap) in gaps.iter().enumerate() {
        let x = -5.0 + 2.0 * i as f64;
        pair_z!(r, x, 0.5 + gap, total);
    }

    capture(r, total, "z_precision.png");
}

/// Same gaps as `z_precision` but seen from an angle. Each square has its own
/// fill color so the streaks identify which square's fragments win.
#[scene]
#[output(dir = "./output/z_fighting")]
fn z_precision_angled(r: &mut RanimScene) {
    let total = 3.4;
    r.play(angled_ortho_camera().show().with_duration(total));

    let gaps = [0.0, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1];
    let sq_colors = [
        manim::RED_C,
        manim::GREEN_C,
        manim::YELLOW_C,
        manim::PURPLE_C,
        manim::MAROON_C,
        manim::GOLD_A,
    ];
    for (i, gap) in gaps.iter().enumerate() {
        let x = -5.0 + 2.0 * i as f64;
        pair!(
            r,
            square(sq_colors[i]),
            circle(manim::BLUE_C),
            dvec3(x, 0.0, 0.5 + gap),
            total
        );
    }

    capture(r, total, "z_precision_angled.png");
}

/// Fixed angled camera; the blue circle's z sweeps linearly through the red
/// square's plane (z 0.45 -> 0.55, square at 0.5). The winner flips cleanly
/// at delta = 0 at this screen position; the same gap fights at others.
#[scene]
#[output(dir = "./output/z_fighting")]
fn z_sweep(r: &mut RanimScene) {
    let total = 4.0;
    r.play(angled_ortho_camera().show().with_duration(total));

    let mut sq = square(manim::RED_C);
    sq.transform = Translation(dvec3(0.0, 0.0, 0.5));
    r.play(sq.show().with_duration(total));

    r.play(
        Pure::new(|alpha| {
            let z = 0.45 + 0.1 * alpha;
            let mut ci = circle(manim::BLUE_C);
            ci.transform = Translation(dvec3(0.0, 0.0, z));
            ci
        })
        .with_duration(total)
        .with_rate_func(linear),
    );
}

/// Slowly orbit a perspective camera around the square+circle pair to show
/// that the winning color changes over time (instability).
#[scene]
#[output(dir = "./output/z_fighting")]
fn flicker(r: &mut RanimScene) {
    let total = 4.0;
    let mut cam = CameraFrame::from_spherical(60f64.to_radians(), 20f64.to_radians(), 8.0);
    cam.fovy = 50f64.to_radians();
    r.play(
        cam.orbit(DVec3::ZERO, 0.4)
            .with_duration(total)
            .with_rate_func(linear),
    );
    pair_z!(r, 0.0, 0.5, total);
}

/// 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([
            opaque_front::scene(),
            opaque_angled::scene(),
            translucent_front::scene(),
            z_precision::scene(),
            z_precision_angled::scene(),
            z_sweep::scene(),
            flicker::scene(),
        ]);

        #[cfg(feature = "render")]
        {
            use ranim::{
                core::traits::Translation,
                core::transformed::Transformed,
                items::{
                    RanimItemsPlugin,
                    vitem::geometry::{Circle, Square},
                },
                render::{RenderSetup, extract::ExtractPlugin},
            };

            app.insert_resource(RenderSetup::new(|render_app| {
                render_app.add_plugins((
                    RanimItemsPlugin,
                    ExtractPlugin::<Transformed<Square, Translation>>::default(),
                    ExtractPlugin::<Transformed<Circle, Translation>>::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())
}