Common Rendering Recipes
Minimal, copy-pasteable examples for frequent Bevy 0.15+ rendering tasks.
1. Animated Sprite Sheet
Load a sprite sheet atlas and cycle through frames with a timer.
use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) // pixel-art friendly .add_systems(Startup, setup) .add_systems(Update, animate_sprite) .run(); } #[derive(Component)] struct AnimationConfig { first_frame: usize, last_frame: usize, timer: Timer, } fn setup( mut commands: Commands, asset_server: Res<AssetServer>, mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>, ) { commands.spawn(Camera2d); let texture = asset_server.load("characters/player_run.png"); // 6 frames in a horizontal strip, each 32x32 pixels let layout = TextureAtlasLayout::from_grid(UVec2::new(32, 32), 6, 1, None, None); let layout_handle = texture_atlas_layouts.add(layout); commands.spawn(( Sprite { image: texture, texture_atlas: Some(TextureAtlas { layout: layout_handle, index: 0, }), ..default() }, Transform::from_scale(Vec3::splat(4.0)), // scale up for visibility AnimationConfig { first_frame: 0, last_frame: 5, timer: Timer::from_seconds(0.1, TimerMode::Repeating), }, )); } fn animate_sprite( time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut Sprite)>, ) { for (mut config, mut sprite) in &mut query { config.timer.tick(time.delta()); if config.timer.just_finished() { if let Some(atlas) = &mut sprite.texture_atlas { atlas.index = if atlas.index >= config.last_frame { config.first_frame } else { atlas.index + 1 }; } } } }
2. 3D Scene with Lighting
Ground plane, a lit object, directional light, and ambient light.
use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .run(); } fn setup( mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>, ) { // Ground plane commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.3, 0.5, 0.3), perceptual_roughness: 0.9, ..default() })), Transform::default(), )); // Lit cube commands.spawn(( Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.8, 0.2, 0.2), metallic: 0.3, perceptual_roughness: 0.4, ..default() })), Transform::from_xyz(0.0, 0.5, 0.0), )); // Directional light (sun) commands.spawn(( DirectionalLight { illuminance: 15_000.0, shadows_enabled: true, ..default() }, Transform::default().looking_at(Vec3::new(-1.0, -2.0, -1.5), Vec3::Y), )); // Ambient fill commands.insert_resource(AmbientLight { color: Color::srgb(0.6, 0.7, 1.0), brightness: 200.0, }); // Camera commands.spawn(( Camera3d::default(), Transform::from_xyz(-3.0, 3.0, 5.0).looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y), )); }
3. Split-Screen Two-Camera Setup
Left half shows one camera, right half shows another.
use bevy::{prelude::*, render::camera::Viewport}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, update_viewports) .run(); } #[derive(Component)] struct LeftCamera; #[derive(Component)] struct RightCamera; fn setup( mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>, ) { // Shared scene content commands.spawn(( Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), MeshMaterial3d(materials.add(Color::srgb(0.8, 0.2, 0.2))), Transform::from_xyz(0.0, 0.5, 0.0), )); commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), Transform::default(), )); commands.spawn(( DirectionalLight { shadows_enabled: true, ..default() }, Transform::default().looking_at(Vec3::new(-1.0, -1.0, -1.0), Vec3::Y), )); // Left camera — renders first, clears background commands.spawn(( Camera3d::default(), Camera { order: 0, clear_color: ClearColorConfig::Custom(Color::srgb(0.1, 0.1, 0.2)), ..default() }, Transform::from_xyz(-3.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), LeftCamera, )); // Right camera — renders second, does not clear the left half commands.spawn(( Camera3d::default(), Camera { order: 1, clear_color: ClearColorConfig::None, ..default() }, Transform::from_xyz(5.0, 3.0, -3.0).looking_at(Vec3::ZERO, Vec3::Y), RightCamera, )); } fn update_viewports( windows: Query<&Window>, mut left_camera: Query<&mut Camera, (With<LeftCamera>, Without<RightCamera>)>, mut right_camera: Query<&mut Camera, (With<RightCamera>, Without<LeftCamera>)>, ) { let Ok(window) = windows.single() else { return }; let width = window.physical_width(); let height = window.physical_height(); let half_width = width / 2; if let Ok(mut cam) = left_camera.single_mut() { cam.viewport = Some(Viewport { physical_position: UVec2::ZERO, physical_size: UVec2::new(half_width, height), ..default() }); } if let Ok(mut cam) = right_camera.single_mut() { cam.viewport = Some(Viewport { physical_position: UVec2::new(half_width, 0), physical_size: UVec2::new(width - half_width, height), ..default() }); } }
4. Loading and Displaying a GLTF Model
Load a .glb file and spawn its scene.
use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .run(); } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { // Load the GLTF scene (Scene0 is the first/default scene) commands.spawn(( SceneRoot(asset_server.load("models/FlightHelmet.glb#Scene0")), Transform::from_xyz(0.0, 0.0, 0.0) .with_scale(Vec3::splat(3.0)), )); // Lighting commands.spawn(( DirectionalLight { illuminance: 20_000.0, shadows_enabled: true, ..default() }, Transform::default().looking_at(Vec3::new(-1.0, -1.0, -1.0), Vec3::Y), )); commands.insert_resource(AmbientLight { color: Color::WHITE, brightness: 300.0, }); // Camera commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 1.5, 4.0).looking_at(Vec3::new(0.0, 0.8, 0.0), Vec3::Y), )); }
5. Billboard Text That Always Faces the Camera
Spawn Text2d in 3D space and rotate it each frame to face the camera.
use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, billboard_face_camera) .run(); } #[derive(Component)] struct Billboard; fn setup( mut commands: Commands, asset_server: Res<AssetServer>, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>, ) { // A cube to anchor the label to let cube = commands.spawn(( Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), MeshMaterial3d(materials.add(Color::srgb(0.2, 0.5, 0.8))), Transform::from_xyz(0.0, 0.5, 0.0), )).id(); // Billboard text as a child, floating above the cube let font = asset_server.load("fonts/FiraSans-Bold.ttf"); commands.spawn(( Text2d::new("Hello!"), TextFont { font, font_size: 36.0, ..default() }, TextColor(Color::WHITE), Transform::from_xyz(0.0, 1.2, 0.0).with_scale(Vec3::splat(0.01)), // scale down for 3D space Billboard, )).set_parent(cube); // Lighting and camera commands.spawn(( DirectionalLight { shadows_enabled: true, ..default() }, Transform::default().looking_at(Vec3::new(-1.0, -1.0, -1.0), Vec3::Y), )); commands.spawn(( Camera3d::default(), Transform::from_xyz(3.0, 3.0, 3.0).looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y), )); } fn billboard_face_camera( camera_query: Query<&GlobalTransform, With<Camera3d>>, mut billboards: Query<&mut Transform, (With<Billboard>, Without<Camera3d>)>, ) { let Ok(camera_global) = camera_query.single() else { return }; let camera_position = camera_global.translation(); for mut transform in &mut billboards { // Compute the direction from the billboard to the camera, ignoring Y to stay upright let direction = camera_position - transform.translation; if direction.length_squared() > 0.001 { transform.look_to(direction, Vec3::Y); } } }
6. Render-to-Texture (Camera Rendering to Image Used as Material)
Render a scene from a secondary camera into an image, then apply that image as a texture on a 3D object.
use bevy::{ prelude::*, render::{ camera::RenderTarget, render_resource::{ Extent3d, TextureDimension, TextureFormat, TextureUsages, }, view::RenderLayers, }, }; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, rotate_cube) .run(); } #[derive(Component)] struct RotatingCube; fn setup( mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>, mut images: ResMut<Assets<Image>>, ) { // Create the render target image let size = Extent3d { width: 512, height: 512, depth_or_array_layers: 1, }; let mut render_image = Image::new_fill( size, TextureDimension::D2, &[0, 0, 0, 255], TextureFormat::Bgra8UnormSrgb, bevy::render::render_asset::RenderAssetUsages::default(), ); render_image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT; let render_image_handle = images.add(render_image); // --- Sub-scene rendered by the offscreen camera (layer 1) --- // A spinning cube only visible to the offscreen camera commands.spawn(( Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(1.0, 0.3, 0.1), ..default() })), Transform::from_xyz(0.0, 0.0, 0.0), RenderLayers::layer(1), RotatingCube, )); // Light for the sub-scene commands.spawn(( PointLight { intensity: 2_000_000.0, shadows_enabled: true, ..default() }, Transform::from_xyz(3.0, 4.0, 3.0), RenderLayers::layer(1), )); // Offscreen camera rendering into the image commands.spawn(( Camera3d::default(), Camera { target: RenderTarget::Image(render_image_handle.clone().into()), clear_color: ClearColorConfig::Custom(Color::srgb(0.1, 0.1, 0.15)), ..default() }, Transform::from_xyz(0.0, 2.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y), RenderLayers::layer(1), )); // --- Main scene (layer 0) --- // A plane that uses the render texture as its material commands.spawn(( Mesh3d(meshes.add(Cuboid::new(3.0, 2.0, 0.1))), MeshMaterial3d(materials.add(StandardMaterial { base_color_texture: Some(render_image_handle), unlit: true, ..default() })), Transform::from_xyz(0.0, 1.0, 0.0), RenderLayers::layer(0), )); // Light for main scene commands.spawn(( PointLight { intensity: 1_000_000.0, ..default() }, Transform::from_xyz(4.0, 5.0, 4.0), RenderLayers::layer(0), )); // Main camera commands.spawn(( Camera3d::default(), Camera { order: 1, // render after the offscreen camera ..default() }, Transform::from_xyz(0.0, 1.5, 5.0).looking_at(Vec3::new(0.0, 1.0, 0.0), Vec3::Y), RenderLayers::layer(0), )); } fn rotate_cube(time: Res<Time>, mut query: Query<&mut Transform, With<RotatingCube>>) { for mut transform in &mut query { transform.rotate_y(time.delta_secs() * 1.5); transform.rotate_x(time.delta_secs() * 0.7); } }