Skip to content

Commit

Permalink
Built-in skybox (#8275)
Browse files Browse the repository at this point in the history
# 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>
  • Loading branch information
3 people authored Apr 2, 2023
1 parent 7a9e77c commit f0f5d79
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 129 deletions.
30 changes: 27 additions & 3 deletions crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ use crate::{
clear_color::{ClearColor, ClearColorConfig},
core_3d::{Camera3d, Opaque3d},
prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass},
skybox::{SkyboxBindGroup, SkyboxPipelineId},
};
use bevy_ecs::prelude::*;
use bevy_render::{
camera::ExtractedCamera,
render_graph::{Node, NodeRunError, RenderGraphContext},
render_phase::RenderPhase,
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
render_resource::{
LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor,
},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset},
};
#[cfg(feature = "trace")]
use bevy_utils::tracing::info_span;
Expand All @@ -30,6 +33,9 @@ pub struct MainOpaquePass3dNode {
Option<&'static DepthPrepass>,
Option<&'static NormalPrepass>,
Option<&'static MotionVectorPrepass>,
Option<&'static SkyboxPipelineId>,
Option<&'static SkyboxBindGroup>,
&'static ViewUniformOffset,
),
With<ExtractedView>,
>,
Expand Down Expand Up @@ -64,7 +70,10 @@ impl Node for MainOpaquePass3dNode {
depth,
depth_prepass,
normal_prepass,
motion_vector_prepass
motion_vector_prepass,
skybox_pipeline,
skybox_bind_group,
view_uniform_offset,
)) = self.query.get_manual(world, view_entity) else {
// No window
return Ok(());
Expand All @@ -75,6 +84,7 @@ impl Node for MainOpaquePass3dNode {
#[cfg(feature = "trace")]
let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered();

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

// Opaque draws
opaque_phase.render(&mut render_pass, world, view_entity);

// Alpha draws
if !alpha_mask_phase.items.is_empty() {
alpha_mask_phase.render(&mut render_pass, world, view_entity);
}

// Draw the skybox using a fullscreen triangle
if let (Some(skybox_pipeline), Some(skybox_bind_group)) =
(skybox_pipeline, skybox_bind_group)
{
let pipeline_cache = world.resource::<PipelineCache>();
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) {
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, &skybox_bind_group.0, &[view_uniform_offset.offset]);
render_pass.draw(0..3, 0..1);
}
}

Ok(())
}
}
2 changes: 2 additions & 0 deletions crates/bevy_core_pipeline/src/core_3d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ use bevy_utils::{FloatOrd, HashMap};

use crate::{
prepass::{node::PrepassNode, DepthPrepass},
skybox::SkyboxPlugin,
tonemapping::TonemappingNode,
upscaling::UpscalingNode,
};
Expand All @@ -62,6 +63,7 @@ impl Plugin for Core3dPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Camera3d>()
.register_type::<Camera3dDepthLoadOp>()
.add_plugin(SkyboxPlugin)
.add_plugin(ExtractComponentPlugin::<Camera3d>::default());

let render_app = match app.get_sub_app_mut(RenderApp) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,26 @@ struct FullscreenVertexOutput {
uv: vec2<f32>,
};

// This vertex shader produces the following, when drawn using indices 0..3:
//
// 1 | 0-----x.....2
// 0 | | s | . ´
// -1 | x_____x´
// -2 | : .´
// -3 | 1´
// +---------------
// -1 0 1 2 3
//
// The axes are clip-space x and y. The region marked s is the visible region.
// The digits in the corners of the right-angled triangle are the vertex
// indices.
//
// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0.
// This means that the UV gets interpolated to 1,1 at the bottom-right corner
// of the clip-space rectangle that is at 1,-1 in clip space.
@vertex
fn fullscreen_vertex_shader(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput {
// See the explanation above for how this works
let uv = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
let clip_position = vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);

Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_core_pipeline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ pub mod fullscreen_vertex_shader;
pub mod fxaa;
pub mod msaa_writeback;
pub mod prepass;
mod skybox;
mod taa;
pub mod tonemapping;
pub mod upscaling;

pub use skybox::Skybox;

/// Experimental features that are not yet finished. Please report any issues you encounter!
pub mod experimental {
pub mod taa {
Expand Down
238 changes: 238 additions & 0 deletions crates/bevy_core_pipeline/src/skybox/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
use bevy_ecs::{
prelude::{Component, Entity},
query::With,
schedule::IntoSystemConfigs,
system::{Commands, Query, Res, ResMut, Resource},
};
use bevy_reflect::TypeUuid;
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::RenderAssets,
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType,
CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState,
DepthStencilState, FragmentState, MultisampleState, PipelineCache, PrimitiveState,
RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, ShaderType,
SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState,
TextureFormat, TextureSampleType, TextureViewDimension, VertexState,
},
renderer::RenderDevice,
texture::{BevyDefault, Image},
view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms},
Render, RenderApp, RenderSet,
};

const SKYBOX_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 55594763423201);

pub struct SkyboxPlugin;

impl Plugin for SkyboxPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl);

app.add_plugin(ExtractComponentPlugin::<Skybox>::default());

let render_app = match app.get_sub_app_mut(RenderApp) {
Ok(render_app) => render_app,
Err(_) => return,
};

let render_device = render_app.world.resource::<RenderDevice>().clone();

render_app
.insert_resource(SkyboxPipeline::new(&render_device))
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
.add_systems(
Render,
(
prepare_skybox_pipelines.in_set(RenderSet::Prepare),
queue_skybox_bind_groups.in_set(RenderSet::Queue),
),
);
}
}

/// Adds a skybox to a 3D camera, based on a cubemap texture.
///
/// Note that this component does not (currently) affect the scene's lighting.
/// To do so, use `EnvironmentMapLight` alongside this component.
///
/// See also <https://en.wikipedia.org/wiki/Skybox_(video_games)>.
#[derive(Component, ExtractComponent, Clone)]
pub struct Skybox(pub Handle<Image>);

#[derive(Resource)]
struct SkyboxPipeline {
bind_group_layout: BindGroupLayout,
}

impl SkyboxPipeline {
fn new(render_device: &RenderDevice) -> Self {
let bind_group_layout_descriptor = BindGroupLayoutDescriptor {
label: Some("skybox_bind_group_layout"),
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::Cube,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::VERTEX_FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(ViewUniform::min_size()),
},
count: None,
},
],
};

Self {
bind_group_layout: render_device
.create_bind_group_layout(&bind_group_layout_descriptor),
}
}
}

#[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct SkyboxPipelineKey {
hdr: bool,
samples: u32,
}

impl SpecializedRenderPipeline for SkyboxPipeline {
type Key = SkyboxPipelineKey;

fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("skybox_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
push_constant_ranges: Vec::new(),
vertex: VertexState {
shader: SKYBOX_SHADER_HANDLE.typed(),
shader_defs: Vec::new(),
entry_point: "skybox_vertex".into(),
buffers: Vec::new(),
},
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth32Float,
depth_write_enabled: false,
depth_compare: CompareFunction::GreaterEqual,
stencil: StencilState {
front: StencilFaceState::IGNORE,
back: StencilFaceState::IGNORE,
read_mask: 0,
write_mask: 0,
},
bias: DepthBiasState {
constant: 0,
slope_scale: 0.0,
clamp: 0.0,
},
}),
multisample: MultisampleState {
count: key.samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(FragmentState {
shader: SKYBOX_SHADER_HANDLE.typed(),
shader_defs: Vec::new(),
entry_point: "skybox_fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: Some(BlendState::REPLACE),
write_mask: ColorWrites::ALL,
})],
}),
}
}
}

#[derive(Component)]
pub struct SkyboxPipelineId(pub CachedRenderPipelineId);

fn prepare_skybox_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPipeline>>,
pipeline: Res<SkyboxPipeline>,
msaa: Res<Msaa>,
views: Query<(Entity, &ExtractedView), With<Skybox>>,
) {
for (entity, view) in &views {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
SkyboxPipelineKey {
hdr: view.hdr,
samples: msaa.samples(),
},
);

commands
.entity(entity)
.insert(SkyboxPipelineId(pipeline_id));
}
}

#[derive(Component)]
pub struct SkyboxBindGroup(pub BindGroup);

fn queue_skybox_bind_groups(
mut commands: Commands,
pipeline: Res<SkyboxPipeline>,
view_uniforms: Res<ViewUniforms>,
images: Res<RenderAssets<Image>>,
render_device: Res<RenderDevice>,
views: Query<(Entity, &Skybox)>,
) {
for (entity, skybox) in &views {
if let (Some(skybox), Some(view_uniforms)) =
(images.get(&skybox.0), view_uniforms.uniforms.binding())
{
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
label: Some("skybox_bind_group"),
layout: &pipeline.bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&skybox.texture_view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&skybox.sampler),
},
BindGroupEntry {
binding: 2,
resource: view_uniforms,
},
],
});

commands.entity(entity).insert(SkyboxBindGroup(bind_group));
}
}
}
Loading

0 comments on commit f0f5d79

Please sign in to comment.