Skip to content

Commit f0f5d79

Browse files
JMS55superdumprobtfm
authored
Built-in skybox (#8275)
# Objective - Closes #8008 ## Solution - Add a skybox plugin that renders a fullscreen triangle, and then modifies the vertices in a vertex shader to enforce that it renders as a skybox background. - Skybox is run at the end of MainOpaquePass3dNode. - In the future, it would be nice to get something like bevy_atmosphere built-in, and have a default skybox+environment map light. --- ## Changelog - Added `Skybox`. - `EnvironmentMapLight` now renders in the correct orientation. ## Migration Guide - Flip `EnvironmentMapLight` maps if needed to match how they previously rendered (which was backwards). --------- Co-authored-by: Robert Swain <robert.swain@gmail.com> Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com>
1 parent 7a9e77c commit f0f5d79

File tree

8 files changed

+349
-129
lines changed

8 files changed

+349
-129
lines changed

crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ use crate::{
22
clear_color::{ClearColor, ClearColorConfig},
33
core_3d::{Camera3d, Opaque3d},
44
prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass},
5+
skybox::{SkyboxBindGroup, SkyboxPipelineId},
56
};
67
use bevy_ecs::prelude::*;
78
use bevy_render::{
89
camera::ExtractedCamera,
910
render_graph::{Node, NodeRunError, RenderGraphContext},
1011
render_phase::RenderPhase,
11-
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
12+
render_resource::{
13+
LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor,
14+
},
1215
renderer::RenderContext,
13-
view::{ExtractedView, ViewDepthTexture, ViewTarget},
16+
view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset},
1417
};
1518
#[cfg(feature = "trace")]
1619
use bevy_utils::tracing::info_span;
@@ -30,6 +33,9 @@ pub struct MainOpaquePass3dNode {
3033
Option<&'static DepthPrepass>,
3134
Option<&'static NormalPrepass>,
3235
Option<&'static MotionVectorPrepass>,
36+
Option<&'static SkyboxPipelineId>,
37+
Option<&'static SkyboxBindGroup>,
38+
&'static ViewUniformOffset,
3339
),
3440
With<ExtractedView>,
3541
>,
@@ -64,7 +70,10 @@ impl Node for MainOpaquePass3dNode {
6470
depth,
6571
depth_prepass,
6672
normal_prepass,
67-
motion_vector_prepass
73+
motion_vector_prepass,
74+
skybox_pipeline,
75+
skybox_bind_group,
76+
view_uniform_offset,
6877
)) = self.query.get_manual(world, view_entity) else {
6978
// No window
7079
return Ok(());
@@ -75,6 +84,7 @@ impl Node for MainOpaquePass3dNode {
7584
#[cfg(feature = "trace")]
7685
let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered();
7786

87+
// Setup render pass
7888
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
7989
label: Some("main_opaque_pass_3d"),
8090
// NOTE: The opaque pass loads the color
@@ -115,12 +125,26 @@ impl Node for MainOpaquePass3dNode {
115125
render_pass.set_camera_viewport(viewport);
116126
}
117127

128+
// Opaque draws
118129
opaque_phase.render(&mut render_pass, world, view_entity);
119130

131+
// Alpha draws
120132
if !alpha_mask_phase.items.is_empty() {
121133
alpha_mask_phase.render(&mut render_pass, world, view_entity);
122134
}
123135

136+
// Draw the skybox using a fullscreen triangle
137+
if let (Some(skybox_pipeline), Some(skybox_bind_group)) =
138+
(skybox_pipeline, skybox_bind_group)
139+
{
140+
let pipeline_cache = world.resource::<PipelineCache>();
141+
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) {
142+
render_pass.set_render_pipeline(pipeline);
143+
render_pass.set_bind_group(0, &skybox_bind_group.0, &[view_uniform_offset.offset]);
144+
render_pass.draw(0..3, 0..1);
145+
}
146+
}
147+
124148
Ok(())
125149
}
126150
}

crates/bevy_core_pipeline/src/core_3d/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use bevy_utils::{FloatOrd, HashMap};
5252

5353
use crate::{
5454
prepass::{node::PrepassNode, DepthPrepass},
55+
skybox::SkyboxPlugin,
5556
tonemapping::TonemappingNode,
5657
upscaling::UpscalingNode,
5758
};
@@ -62,6 +63,7 @@ impl Plugin for Core3dPlugin {
6263
fn build(&self, app: &mut App) {
6364
app.register_type::<Camera3d>()
6465
.register_type::<Camera3dDepthLoadOp>()
66+
.add_plugin(SkyboxPlugin)
6567
.add_plugin(ExtractComponentPlugin::<Camera3d>::default());
6668

6769
let render_app = match app.get_sub_app_mut(RenderApp) {

crates/bevy_core_pipeline/src/fullscreen_vertex_shader/fullscreen.wgsl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,26 @@ struct FullscreenVertexOutput {
77
uv: vec2<f32>,
88
};
99

10+
// This vertex shader produces the following, when drawn using indices 0..3:
11+
//
12+
// 1 | 0-----x.....2
13+
// 0 | | s | . ´
14+
// -1 | x_____x´
15+
// -2 | : .´
16+
// -3 | 1´
17+
// +---------------
18+
// -1 0 1 2 3
19+
//
20+
// The axes are clip-space x and y. The region marked s is the visible region.
21+
// The digits in the corners of the right-angled triangle are the vertex
22+
// indices.
23+
//
24+
// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0.
25+
// This means that the UV gets interpolated to 1,1 at the bottom-right corner
26+
// of the clip-space rectangle that is at 1,-1 in clip space.
1027
@vertex
1128
fn fullscreen_vertex_shader(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput {
29+
// See the explanation above for how this works
1230
let uv = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
1331
let clip_position = vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
1432

crates/bevy_core_pipeline/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ pub mod fullscreen_vertex_shader;
77
pub mod fxaa;
88
pub mod msaa_writeback;
99
pub mod prepass;
10+
mod skybox;
1011
mod taa;
1112
pub mod tonemapping;
1213
pub mod upscaling;
1314

15+
pub use skybox::Skybox;
16+
1417
/// Experimental features that are not yet finished. Please report any issues you encounter!
1518
pub mod experimental {
1619
pub mod taa {
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
use bevy_app::{App, Plugin};
2+
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
3+
use bevy_ecs::{
4+
prelude::{Component, Entity},
5+
query::With,
6+
schedule::IntoSystemConfigs,
7+
system::{Commands, Query, Res, ResMut, Resource},
8+
};
9+
use bevy_reflect::TypeUuid;
10+
use bevy_render::{
11+
extract_component::{ExtractComponent, ExtractComponentPlugin},
12+
render_asset::RenderAssets,
13+
render_resource::{
14+
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
15+
BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType,
16+
CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState,
17+
DepthStencilState, FragmentState, MultisampleState, PipelineCache, PrimitiveState,
18+
RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, ShaderType,
19+
SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState,
20+
TextureFormat, TextureSampleType, TextureViewDimension, VertexState,
21+
},
22+
renderer::RenderDevice,
23+
texture::{BevyDefault, Image},
24+
view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms},
25+
Render, RenderApp, RenderSet,
26+
};
27+
28+
const SKYBOX_SHADER_HANDLE: HandleUntyped =
29+
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 55594763423201);
30+
31+
pub struct SkyboxPlugin;
32+
33+
impl Plugin for SkyboxPlugin {
34+
fn build(&self, app: &mut App) {
35+
load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl);
36+
37+
app.add_plugin(ExtractComponentPlugin::<Skybox>::default());
38+
39+
let render_app = match app.get_sub_app_mut(RenderApp) {
40+
Ok(render_app) => render_app,
41+
Err(_) => return,
42+
};
43+
44+
let render_device = render_app.world.resource::<RenderDevice>().clone();
45+
46+
render_app
47+
.insert_resource(SkyboxPipeline::new(&render_device))
48+
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
49+
.add_systems(
50+
Render,
51+
(
52+
prepare_skybox_pipelines.in_set(RenderSet::Prepare),
53+
queue_skybox_bind_groups.in_set(RenderSet::Queue),
54+
),
55+
);
56+
}
57+
}
58+
59+
/// Adds a skybox to a 3D camera, based on a cubemap texture.
60+
///
61+
/// Note that this component does not (currently) affect the scene's lighting.
62+
/// To do so, use `EnvironmentMapLight` alongside this component.
63+
///
64+
/// See also <https://en.wikipedia.org/wiki/Skybox_(video_games)>.
65+
#[derive(Component, ExtractComponent, Clone)]
66+
pub struct Skybox(pub Handle<Image>);
67+
68+
#[derive(Resource)]
69+
struct SkyboxPipeline {
70+
bind_group_layout: BindGroupLayout,
71+
}
72+
73+
impl SkyboxPipeline {
74+
fn new(render_device: &RenderDevice) -> Self {
75+
let bind_group_layout_descriptor = BindGroupLayoutDescriptor {
76+
label: Some("skybox_bind_group_layout"),
77+
entries: &[
78+
BindGroupLayoutEntry {
79+
binding: 0,
80+
visibility: ShaderStages::FRAGMENT,
81+
ty: BindingType::Texture {
82+
sample_type: TextureSampleType::Float { filterable: true },
83+
view_dimension: TextureViewDimension::Cube,
84+
multisampled: false,
85+
},
86+
count: None,
87+
},
88+
BindGroupLayoutEntry {
89+
binding: 1,
90+
visibility: ShaderStages::FRAGMENT,
91+
ty: BindingType::Sampler(SamplerBindingType::Filtering),
92+
count: None,
93+
},
94+
BindGroupLayoutEntry {
95+
binding: 2,
96+
visibility: ShaderStages::VERTEX_FRAGMENT,
97+
ty: BindingType::Buffer {
98+
ty: BufferBindingType::Uniform,
99+
has_dynamic_offset: true,
100+
min_binding_size: Some(ViewUniform::min_size()),
101+
},
102+
count: None,
103+
},
104+
],
105+
};
106+
107+
Self {
108+
bind_group_layout: render_device
109+
.create_bind_group_layout(&bind_group_layout_descriptor),
110+
}
111+
}
112+
}
113+
114+
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
115+
struct SkyboxPipelineKey {
116+
hdr: bool,
117+
samples: u32,
118+
}
119+
120+
impl SpecializedRenderPipeline for SkyboxPipeline {
121+
type Key = SkyboxPipelineKey;
122+
123+
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
124+
RenderPipelineDescriptor {
125+
label: Some("skybox_pipeline".into()),
126+
layout: vec![self.bind_group_layout.clone()],
127+
push_constant_ranges: Vec::new(),
128+
vertex: VertexState {
129+
shader: SKYBOX_SHADER_HANDLE.typed(),
130+
shader_defs: Vec::new(),
131+
entry_point: "skybox_vertex".into(),
132+
buffers: Vec::new(),
133+
},
134+
primitive: PrimitiveState::default(),
135+
depth_stencil: Some(DepthStencilState {
136+
format: TextureFormat::Depth32Float,
137+
depth_write_enabled: false,
138+
depth_compare: CompareFunction::GreaterEqual,
139+
stencil: StencilState {
140+
front: StencilFaceState::IGNORE,
141+
back: StencilFaceState::IGNORE,
142+
read_mask: 0,
143+
write_mask: 0,
144+
},
145+
bias: DepthBiasState {
146+
constant: 0,
147+
slope_scale: 0.0,
148+
clamp: 0.0,
149+
},
150+
}),
151+
multisample: MultisampleState {
152+
count: key.samples,
153+
mask: !0,
154+
alpha_to_coverage_enabled: false,
155+
},
156+
fragment: Some(FragmentState {
157+
shader: SKYBOX_SHADER_HANDLE.typed(),
158+
shader_defs: Vec::new(),
159+
entry_point: "skybox_fragment".into(),
160+
targets: vec![Some(ColorTargetState {
161+
format: if key.hdr {
162+
ViewTarget::TEXTURE_FORMAT_HDR
163+
} else {
164+
TextureFormat::bevy_default()
165+
},
166+
blend: Some(BlendState::REPLACE),
167+
write_mask: ColorWrites::ALL,
168+
})],
169+
}),
170+
}
171+
}
172+
}
173+
174+
#[derive(Component)]
175+
pub struct SkyboxPipelineId(pub CachedRenderPipelineId);
176+
177+
fn prepare_skybox_pipelines(
178+
mut commands: Commands,
179+
pipeline_cache: Res<PipelineCache>,
180+
mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPipeline>>,
181+
pipeline: Res<SkyboxPipeline>,
182+
msaa: Res<Msaa>,
183+
views: Query<(Entity, &ExtractedView), With<Skybox>>,
184+
) {
185+
for (entity, view) in &views {
186+
let pipeline_id = pipelines.specialize(
187+
&pipeline_cache,
188+
&pipeline,
189+
SkyboxPipelineKey {
190+
hdr: view.hdr,
191+
samples: msaa.samples(),
192+
},
193+
);
194+
195+
commands
196+
.entity(entity)
197+
.insert(SkyboxPipelineId(pipeline_id));
198+
}
199+
}
200+
201+
#[derive(Component)]
202+
pub struct SkyboxBindGroup(pub BindGroup);
203+
204+
fn queue_skybox_bind_groups(
205+
mut commands: Commands,
206+
pipeline: Res<SkyboxPipeline>,
207+
view_uniforms: Res<ViewUniforms>,
208+
images: Res<RenderAssets<Image>>,
209+
render_device: Res<RenderDevice>,
210+
views: Query<(Entity, &Skybox)>,
211+
) {
212+
for (entity, skybox) in &views {
213+
if let (Some(skybox), Some(view_uniforms)) =
214+
(images.get(&skybox.0), view_uniforms.uniforms.binding())
215+
{
216+
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
217+
label: Some("skybox_bind_group"),
218+
layout: &pipeline.bind_group_layout,
219+
entries: &[
220+
BindGroupEntry {
221+
binding: 0,
222+
resource: BindingResource::TextureView(&skybox.texture_view),
223+
},
224+
BindGroupEntry {
225+
binding: 1,
226+
resource: BindingResource::Sampler(&skybox.sampler),
227+
},
228+
BindGroupEntry {
229+
binding: 2,
230+
resource: view_uniforms,
231+
},
232+
],
233+
});
234+
235+
commands.entity(entity).insert(SkyboxBindGroup(bind_group));
236+
}
237+
}
238+
}

0 commit comments

Comments
 (0)