Skip to main content

bevy_core_pipeline/skybox/
mod.rs

1use bevy_app::{App, Plugin};
2use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle};
3use bevy_camera::Exposure;
4use bevy_ecs::{
5    prelude::{Component, Entity},
6    query::With,
7    resource::Resource,
8    schedule::IntoScheduleConfigs,
9    system::{Commands, Local, Query, Res, ResMut},
10};
11use bevy_light::Skybox;
12use bevy_log::warn_once;
13use bevy_math::Mat4;
14use bevy_render::{
15    extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
16    render_asset::RenderAssets,
17    render_resource::{
18        binding_types::{sampler, texture_cube, uniform_buffer},
19        *,
20    },
21    renderer::RenderDevice,
22    sync_component::{SyncComponent, SyncComponentPlugin},
23    sync_world::RenderEntity,
24    texture::GpuImage,
25    view::{ExtractedView, Msaa, ViewUniform, ViewUniforms},
26    Extract, ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems,
27};
28use bevy_shader::Shader;
29use bevy_transform::components::Transform;
30use bevy_utils::default;
31
32use crate::core_3d::CORE_3D_DEPTH_FORMAT;
33
34pub struct SkyboxPlugin;
35
36impl Plugin for SkyboxPlugin {
37    fn build(&self, app: &mut App) {
38        embedded_asset!(app, "skybox.wgsl");
39
40        app.add_plugins((
41            SyncComponentPlugin::<Skybox, Self>::default(),
42            UniformComponentPlugin::<SkyboxUniforms>::default(),
43        ));
44
45        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
46            return;
47        };
48        render_app
49            .init_gpu_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
50            .add_systems(ExtractSchedule, extract_skybox)
51            .add_systems(RenderStartup, init_skybox_pipeline)
52            .add_systems(
53                Render,
54                (
55                    prepare_skybox_pipelines.in_set(RenderSystems::Prepare),
56                    prepare_skybox_bind_groups.in_set(RenderSystems::PrepareBindGroups),
57                ),
58            );
59    }
60}
61
62impl SyncComponent<SkyboxPlugin> for Skybox {
63    type Target = (Self, SkyboxUniforms, SkyboxPipelineId, SkyboxBindGroup);
64}
65
66// This is needed because of the orphan rule not allowing implementing
67// foreign trait ExtractComponent on foreign type Skybox
68pub fn extract_skybox(
69    mut commands: Commands,
70    mut previous_len: Local<usize>,
71    query: Extract<Query<(RenderEntity, &Skybox, Option<&Exposure>)>>,
72) {
73    let mut values = Vec::with_capacity(*previous_len);
74    for (entity, skybox, exposure) in &query {
75        let exposure = exposure
76            .map(Exposure::exposure)
77            .unwrap_or_else(|| Exposure::default().exposure());
78        let uniforms = SkyboxUniforms {
79            brightness: skybox.brightness * exposure,
80            transform: Transform::from_rotation(skybox.rotation.inverse()).to_matrix(),
81            #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
82            _webgl2_padding_8b: 0,
83            #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
84            _webgl2_padding_12b: 0,
85            #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
86            _webgl2_padding_16b: 0,
87        };
88        values.push((entity, (skybox.clone(), uniforms)));
89    }
90    *previous_len = values.len();
91    commands.try_insert_batch(values);
92}
93
94// TODO: Replace with a push constant once WebGPU gets support for that
95#[derive(Component, ShaderType, Clone)]
96pub struct SkyboxUniforms {
97    brightness: f32,
98    transform: Mat4,
99    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
100    _webgl2_padding_8b: u32,
101    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
102    _webgl2_padding_12b: u32,
103    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
104    _webgl2_padding_16b: u32,
105}
106
107#[derive(Resource)]
108struct SkyboxPipeline {
109    bind_group_layout: BindGroupLayoutDescriptor,
110    shader: Handle<Shader>,
111}
112
113impl SkyboxPipeline {
114    fn new(shader: Handle<Shader>) -> Self {
115        Self {
116            bind_group_layout: BindGroupLayoutDescriptor::new(
117                "skybox_bind_group_layout",
118                &BindGroupLayoutEntries::sequential(
119                    ShaderStages::FRAGMENT,
120                    (
121                        texture_cube(TextureSampleType::Float { filterable: true }),
122                        sampler(SamplerBindingType::Filtering),
123                        uniform_buffer::<ViewUniform>(true)
124                            .visibility(ShaderStages::VERTEX_FRAGMENT),
125                        uniform_buffer::<SkyboxUniforms>(true),
126                    ),
127                ),
128            ),
129            shader,
130        }
131    }
132}
133
134fn init_skybox_pipeline(mut commands: Commands, asset_server: Res<AssetServer>) {
135    let shader = load_embedded_asset!(asset_server.as_ref(), "skybox.wgsl");
136    commands.insert_resource(SkyboxPipeline::new(shader));
137}
138
139#[derive(PartialEq, Eq, Hash, Clone, Copy)]
140struct SkyboxPipelineKey {
141    target_format: TextureFormat,
142    samples: u32,
143    depth_format: TextureFormat,
144}
145
146impl SpecializedRenderPipeline for SkyboxPipeline {
147    type Key = SkyboxPipelineKey;
148
149    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
150        RenderPipelineDescriptor {
151            label: Some("skybox_pipeline".into()),
152            layout: vec![self.bind_group_layout.clone()],
153            vertex: VertexState {
154                shader: self.shader.clone(),
155                ..default()
156            },
157            depth_stencil: Some(DepthStencilState {
158                format: key.depth_format,
159                depth_write_enabled: Some(false),
160                depth_compare: Some(CompareFunction::GreaterEqual),
161                stencil: StencilState {
162                    front: StencilFaceState::IGNORE,
163                    back: StencilFaceState::IGNORE,
164                    read_mask: 0,
165                    write_mask: 0,
166                },
167                bias: DepthBiasState {
168                    constant: 0,
169                    slope_scale: 0.0,
170                    clamp: 0.0,
171                },
172            }),
173            multisample: MultisampleState {
174                count: key.samples,
175                mask: !0,
176                alpha_to_coverage_enabled: false,
177            },
178            fragment: Some(FragmentState {
179                shader: self.shader.clone(),
180                targets: vec![Some(ColorTargetState {
181                    format: key.target_format,
182                    // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases.
183                    blend: None,
184                    write_mask: ColorWrites::ALL,
185                })],
186                ..default()
187            }),
188            ..default()
189        }
190    }
191}
192
193#[derive(Component)]
194pub struct SkyboxPipelineId(pub CachedRenderPipelineId);
195
196fn prepare_skybox_pipelines(
197    mut commands: Commands,
198    pipeline_cache: Res<PipelineCache>,
199    mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPipeline>>,
200    pipeline: Res<SkyboxPipeline>,
201    cameras: Query<(Entity, &ExtractedView, &Msaa), With<Skybox>>,
202) {
203    for (entity, view, msaa) in &cameras {
204        let pipeline_id = pipelines.specialize(
205            &pipeline_cache,
206            &pipeline,
207            SkyboxPipelineKey {
208                target_format: view.target_format,
209                samples: msaa.samples(),
210                depth_format: CORE_3D_DEPTH_FORMAT,
211            },
212        );
213
214        commands
215            .entity(entity)
216            .insert(SkyboxPipelineId(pipeline_id));
217    }
218}
219
220#[derive(Component)]
221pub struct SkyboxBindGroup(pub (BindGroup, u32));
222
223fn prepare_skybox_bind_groups(
224    mut commands: Commands,
225    pipeline: Res<SkyboxPipeline>,
226    view_uniforms: Res<ViewUniforms>,
227    skybox_uniforms: Res<ComponentUniforms<SkyboxUniforms>>,
228    images: Res<RenderAssets<GpuImage>>,
229    render_device: Res<RenderDevice>,
230    pipeline_cache: Res<PipelineCache>,
231    views: Query<(Entity, &Skybox, &DynamicUniformIndex<SkyboxUniforms>)>,
232) {
233    for (entity, skybox, skybox_uniform_index) in &views {
234        if let (Some(image_handle), Some(view_uniforms), Some(skybox_uniforms)) = (
235            &skybox.image,
236            view_uniforms.uniforms.binding(),
237            skybox_uniforms.binding(),
238        ) && let Some(image) = images.get(image_handle)
239            && sanity_check_skybox_image_and_warn(entity, skybox, image)
240        {
241            let bind_group = render_device.create_bind_group(
242                "skybox_bind_group",
243                &pipeline_cache.get_bind_group_layout(&pipeline.bind_group_layout),
244                &BindGroupEntries::sequential((
245                    &image.texture_view,
246                    &image.sampler,
247                    view_uniforms,
248                    skybox_uniforms,
249                )),
250            );
251
252            commands
253                .entity(entity)
254                .insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index())));
255        } else {
256            commands.entity(entity).remove::<SkyboxBindGroup>();
257        }
258    }
259}
260
261fn sanity_check_skybox_image_and_warn(entity: Entity, skybox: &Skybox, image: &GpuImage) -> bool {
262    let texture_view_dimension: Option<TextureViewDimension> = image
263        .texture_view_descriptor
264        .as_ref()
265        .and_then(|desc| desc.dimension);
266    let dimension_ok = texture_view_dimension == Some(TextureViewDimension::Cube);
267    if !dimension_ok {
268        // The texture view is not a cubemap and will fail validation if rendered.
269        // In this case, we ignore the skybox so as not to break rendering.
270        //
271        // There are other possible misconfigurations which will fail and which we do not
272        // catch here, but this is a common mistake (passing an unaltered 2D image to `Skybox`).
273        warn_once!(
274            "skybox {entity}'s image {image:?} has texture view dimension \
275                        {texture_view_dimension:?}, but it must be TextureViewDimension::Cube \
276                        to render a skybox",
277            image = skybox.image
278        );
279    }
280    dimension_ok
281}