bevy_core_pipeline/skybox/
mod.rs

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