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
66pub 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#[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 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 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}