Skip to main content

skybox/
skybox.rs

1//! Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats
2
3#[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))]
4use bevy::anti_alias::taa::TemporalAntiAliasing;
5
6use bevy::{
7    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
8    image::CompressedImageFormats,
9    light::Skybox,
10    pbr::ScreenSpaceAmbientOcclusion,
11    prelude::*,
12    render::{
13        render_resource::{TextureViewDescriptor, TextureViewDimension},
14        renderer::RenderDevice,
15    },
16};
17use std::f32::consts::PI;
18
19const CUBEMAPS: &[(&str, CompressedImageFormats)] = &[
20    (
21        "textures/Ryfjallet_cubemap.png",
22        CompressedImageFormats::NONE,
23    ),
24    (
25        "textures/Ryfjallet_cubemap_astc4x4.ktx2",
26        CompressedImageFormats::ASTC_LDR,
27    ),
28    (
29        "textures/Ryfjallet_cubemap_bc7.ktx2",
30        CompressedImageFormats::BC,
31    ),
32    (
33        "textures/Ryfjallet_cubemap_etc2.ktx2",
34        CompressedImageFormats::ETC2,
35    ),
36];
37
38fn main() {
39    App::new()
40        .add_plugins(DefaultPlugins)
41        .add_plugins(FreeCameraPlugin)
42        .add_systems(Startup, setup)
43        .add_systems(
44            Update,
45            (
46                cycle_cubemap_asset,
47                asset_loaded.after(cycle_cubemap_asset),
48                animate_light_direction,
49            ),
50        )
51        .run();
52}
53
54#[derive(Resource)]
55struct Cubemap {
56    is_loaded: bool,
57    index: usize,
58    image_handle: Handle<Image>,
59}
60
61fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
62    // directional 'sun' light
63    commands.spawn((
64        DirectionalLight {
65            illuminance: 32000.0,
66            ..default()
67        },
68        Transform::from_xyz(0.0, 2.0, 0.0).with_rotation(Quat::from_rotation_x(-PI / 4.)),
69    ));
70
71    let skybox_handle = asset_server.load(CUBEMAPS[0].0);
72    // camera
73    commands.spawn((
74        Camera3d::default(),
75        Msaa::Off,
76        #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))]
77        TemporalAntiAliasing::default(),
78        ScreenSpaceAmbientOcclusion::default(),
79        Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
80        FreeCamera::default(),
81        Skybox {
82            image: Some(skybox_handle.clone()),
83            brightness: 1000.0,
84            ..default()
85        },
86    ));
87
88    // ambient light
89    // NOTE: The ambient light is used to scale how bright the environment map is so with a bright
90    // environment map, use an appropriate color and brightness to match
91    commands.insert_resource(GlobalAmbientLight {
92        color: Color::srgb_u8(210, 220, 240),
93        brightness: 1.0,
94        ..default()
95    });
96
97    commands.insert_resource(Cubemap {
98        is_loaded: false,
99        index: 0,
100        image_handle: skybox_handle,
101    });
102}
103
104const CUBEMAP_SWAP_DELAY: f32 = 3.0;
105
106fn cycle_cubemap_asset(
107    time: Res<Time>,
108    mut next_swap: Local<f32>,
109    mut cubemap: ResMut<Cubemap>,
110    asset_server: Res<AssetServer>,
111    render_device: Res<RenderDevice>,
112) {
113    let now = time.elapsed_secs();
114    if *next_swap == 0.0 {
115        *next_swap = now + CUBEMAP_SWAP_DELAY;
116        return;
117    } else if now < *next_swap {
118        return;
119    }
120    *next_swap += CUBEMAP_SWAP_DELAY;
121
122    let supported_compressed_formats =
123        CompressedImageFormats::from_features(render_device.features());
124
125    let mut new_index = cubemap.index;
126    for _ in 0..CUBEMAPS.len() {
127        new_index = (new_index + 1) % CUBEMAPS.len();
128        if supported_compressed_formats.contains(CUBEMAPS[new_index].1) {
129            break;
130        }
131        info!(
132            "Skipping format which is not supported by current hardware: {:?}",
133            CUBEMAPS[new_index]
134        );
135    }
136
137    // Skip swapping to the same texture. Useful for when ktx2, zstd, or compressed texture support
138    // is missing
139    if new_index == cubemap.index {
140        return;
141    }
142
143    cubemap.index = new_index;
144    cubemap.image_handle = asset_server.load(CUBEMAPS[cubemap.index].0);
145    cubemap.is_loaded = false;
146}
147
148fn asset_loaded(
149    asset_server: Res<AssetServer>,
150    mut images: ResMut<Assets<Image>>,
151    mut cubemap: ResMut<Cubemap>,
152    mut skyboxes: Query<&mut Skybox>,
153) {
154    if !cubemap.is_loaded && asset_server.load_state(&cubemap.image_handle).is_loaded() {
155        info!("Swapping to {}...", CUBEMAPS[cubemap.index].0);
156        let mut image = images.get_mut(&cubemap.image_handle).unwrap();
157        // NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture,
158        // so they appear as one texture. The following code reconfigures the texture as necessary.
159        if image.texture_descriptor.array_layer_count() == 1 {
160            let layers = image.height() / image.width();
161            image
162                .reinterpret_stacked_2d_as_array(layers)
163                .expect("asset should be 2d texture and height will always be evenly divisible with the given layers");
164            image.texture_view_descriptor = Some(TextureViewDescriptor {
165                dimension: Some(TextureViewDimension::Cube),
166                ..default()
167            });
168        }
169
170        for mut skybox in &mut skyboxes {
171            skybox.image = Some(cubemap.image_handle.clone());
172        }
173
174        cubemap.is_loaded = true;
175    }
176}
177
178fn animate_light_direction(
179    time: Res<Time>,
180    mut query: Query<&mut Transform, With<DirectionalLight>>,
181) {
182    for mut transform in &mut query {
183        transform.rotate_y(time.delta_secs() * 0.5);
184    }
185}