bevy_core_pipeline/skybox/
mod.rs

1use bevy_app::{App, Plugin};
2use bevy_asset::{load_internal_asset, Handle};
3use bevy_ecs::{
4    prelude::{Component, Entity},
5    query::{QueryItem, With},
6    schedule::IntoSystemConfigs,
7    system::{Commands, Query, Res, ResMut, Resource},
8};
9use bevy_image::{BevyDefault, Image};
10use bevy_math::{Mat4, Quat};
11use bevy_render::{
12    camera::Exposure,
13    extract_component::{
14        ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
15        UniformComponentPlugin,
16    },
17    render_asset::RenderAssets,
18    render_resource::{
19        binding_types::{sampler, texture_cube, uniform_buffer},
20        *,
21    },
22    renderer::RenderDevice,
23    texture::GpuImage,
24    view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms},
25    Render, RenderApp, RenderSet,
26};
27use bevy_transform::components::Transform;
28use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE};
29
30use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms};
31
32const SKYBOX_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(55594763423201);
33
34pub mod prepass;
35
36pub struct SkyboxPlugin;
37
38impl Plugin for SkyboxPlugin {
39    fn build(&self, app: &mut App) {
40        load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl);
41        load_internal_asset!(
42            app,
43            SKYBOX_PREPASS_SHADER_HANDLE,
44            "skybox_prepass.wgsl",
45            Shader::from_wgsl
46        );
47
48        app.add_plugins((
49            ExtractComponentPlugin::<Skybox>::default(),
50            UniformComponentPlugin::<SkyboxUniforms>::default(),
51        ));
52
53        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
54            return;
55        };
56        render_app
57            .init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
58            .init_resource::<SpecializedRenderPipelines<SkyboxPrepassPipeline>>()
59            .init_resource::<PreviousViewUniforms>()
60            .add_systems(
61                Render,
62                (
63                    prepare_skybox_pipelines.in_set(RenderSet::Prepare),
64                    prepass::prepare_skybox_prepass_pipelines.in_set(RenderSet::Prepare),
65                    prepare_skybox_bind_groups.in_set(RenderSet::PrepareBindGroups),
66                    prepass::prepare_skybox_prepass_bind_groups
67                        .in_set(RenderSet::PrepareBindGroups),
68                ),
69            );
70    }
71
72    fn finish(&self, app: &mut App) {
73        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
74            return;
75        };
76        let render_device = render_app.world().resource::<RenderDevice>().clone();
77        render_app
78            .insert_resource(SkyboxPipeline::new(&render_device))
79            .init_resource::<SkyboxPrepassPipeline>();
80    }
81}
82
83/// Adds a skybox to a 3D camera, based on a cubemap texture.
84///
85/// Note that this component does not (currently) affect the scene's lighting.
86/// To do so, use `EnvironmentMapLight` alongside this component.
87///
88/// See also <https://en.wikipedia.org/wiki/Skybox_(video_games)>.
89#[derive(Component, Clone)]
90pub struct Skybox {
91    pub image: Handle<Image>,
92    /// Scale factor applied to the skybox image.
93    /// After applying this multiplier to the image samples, the resulting values should
94    /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
95    pub brightness: f32,
96
97    /// View space rotation applied to the skybox cubemap.
98    /// This is useful for users who require a different axis, such as the Z-axis, to serve
99    /// as the vertical axis.
100    pub rotation: Quat,
101}
102
103impl Default for Skybox {
104    fn default() -> Self {
105        Skybox {
106            image: Handle::default(),
107            brightness: 0.0,
108            rotation: Quat::IDENTITY,
109        }
110    }
111}
112
113impl ExtractComponent for Skybox {
114    type QueryData = (&'static Self, Option<&'static Exposure>);
115    type QueryFilter = ();
116    type Out = (Self, SkyboxUniforms);
117
118    fn extract_component((skybox, exposure): QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
119        let exposure = exposure
120            .map(Exposure::exposure)
121            .unwrap_or_else(|| Exposure::default().exposure());
122
123        Some((
124            skybox.clone(),
125            SkyboxUniforms {
126                brightness: skybox.brightness * exposure,
127                transform: Transform::from_rotation(skybox.rotation)
128                    .compute_matrix()
129                    .inverse(),
130                #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
131                _wasm_padding_8b: 0,
132                #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
133                _wasm_padding_12b: 0,
134                #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
135                _wasm_padding_16b: 0,
136            },
137        ))
138    }
139}
140
141// TODO: Replace with a push constant once WebGPU gets support for that
142#[derive(Component, ShaderType, Clone)]
143pub struct SkyboxUniforms {
144    brightness: f32,
145    transform: Mat4,
146    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
147    _wasm_padding_8b: u32,
148    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
149    _wasm_padding_12b: u32,
150    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
151    _wasm_padding_16b: u32,
152}
153
154#[derive(Resource)]
155struct SkyboxPipeline {
156    bind_group_layout: BindGroupLayout,
157}
158
159impl SkyboxPipeline {
160    fn new(render_device: &RenderDevice) -> Self {
161        Self {
162            bind_group_layout: render_device.create_bind_group_layout(
163                "skybox_bind_group_layout",
164                &BindGroupLayoutEntries::sequential(
165                    ShaderStages::FRAGMENT,
166                    (
167                        texture_cube(TextureSampleType::Float { filterable: true }),
168                        sampler(SamplerBindingType::Filtering),
169                        uniform_buffer::<ViewUniform>(true)
170                            .visibility(ShaderStages::VERTEX_FRAGMENT),
171                        uniform_buffer::<SkyboxUniforms>(true),
172                    ),
173                ),
174            ),
175        }
176    }
177}
178
179#[derive(PartialEq, Eq, Hash, Clone, Copy)]
180struct SkyboxPipelineKey {
181    hdr: bool,
182    samples: u32,
183    depth_format: TextureFormat,
184}
185
186impl SpecializedRenderPipeline for SkyboxPipeline {
187    type Key = SkyboxPipelineKey;
188
189    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
190        RenderPipelineDescriptor {
191            label: Some("skybox_pipeline".into()),
192            layout: vec![self.bind_group_layout.clone()],
193            push_constant_ranges: Vec::new(),
194            vertex: VertexState {
195                shader: SKYBOX_SHADER_HANDLE,
196                shader_defs: Vec::new(),
197                entry_point: "skybox_vertex".into(),
198                buffers: Vec::new(),
199            },
200            primitive: PrimitiveState::default(),
201            depth_stencil: Some(DepthStencilState {
202                format: key.depth_format,
203                depth_write_enabled: false,
204                depth_compare: CompareFunction::GreaterEqual,
205                stencil: StencilState {
206                    front: StencilFaceState::IGNORE,
207                    back: StencilFaceState::IGNORE,
208                    read_mask: 0,
209                    write_mask: 0,
210                },
211                bias: DepthBiasState {
212                    constant: 0,
213                    slope_scale: 0.0,
214                    clamp: 0.0,
215                },
216            }),
217            multisample: MultisampleState {
218                count: key.samples,
219                mask: !0,
220                alpha_to_coverage_enabled: false,
221            },
222            fragment: Some(FragmentState {
223                shader: SKYBOX_SHADER_HANDLE,
224                shader_defs: Vec::new(),
225                entry_point: "skybox_fragment".into(),
226                targets: vec![Some(ColorTargetState {
227                    format: if key.hdr {
228                        ViewTarget::TEXTURE_FORMAT_HDR
229                    } else {
230                        TextureFormat::bevy_default()
231                    },
232                    // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases.
233                    blend: None,
234                    write_mask: ColorWrites::ALL,
235                })],
236            }),
237            zero_initialize_workgroup_memory: false,
238        }
239    }
240}
241
242#[derive(Component)]
243pub struct SkyboxPipelineId(pub CachedRenderPipelineId);
244
245fn prepare_skybox_pipelines(
246    mut commands: Commands,
247    pipeline_cache: Res<PipelineCache>,
248    mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPipeline>>,
249    pipeline: Res<SkyboxPipeline>,
250    views: Query<(Entity, &ExtractedView, &Msaa), With<Skybox>>,
251) {
252    for (entity, view, msaa) in &views {
253        let pipeline_id = pipelines.specialize(
254            &pipeline_cache,
255            &pipeline,
256            SkyboxPipelineKey {
257                hdr: view.hdr,
258                samples: msaa.samples(),
259                depth_format: CORE_3D_DEPTH_FORMAT,
260            },
261        );
262
263        commands
264            .entity(entity)
265            .insert(SkyboxPipelineId(pipeline_id));
266    }
267}
268
269#[derive(Component)]
270pub struct SkyboxBindGroup(pub (BindGroup, u32));
271
272fn prepare_skybox_bind_groups(
273    mut commands: Commands,
274    pipeline: Res<SkyboxPipeline>,
275    view_uniforms: Res<ViewUniforms>,
276    skybox_uniforms: Res<ComponentUniforms<SkyboxUniforms>>,
277    images: Res<RenderAssets<GpuImage>>,
278    render_device: Res<RenderDevice>,
279    views: Query<(Entity, &Skybox, &DynamicUniformIndex<SkyboxUniforms>)>,
280) {
281    for (entity, skybox, skybox_uniform_index) in &views {
282        if let (Some(skybox), Some(view_uniforms), Some(skybox_uniforms)) = (
283            images.get(&skybox.image),
284            view_uniforms.uniforms.binding(),
285            skybox_uniforms.binding(),
286        ) {
287            let bind_group = render_device.create_bind_group(
288                "skybox_bind_group",
289                &pipeline.bind_group_layout,
290                &BindGroupEntries::sequential((
291                    &skybox.texture_view,
292                    &skybox.sampler,
293                    view_uniforms,
294                    skybox_uniforms,
295                )),
296            );
297
298            commands
299                .entity(entity)
300                .insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index())));
301        }
302    }
303}