Built-in skybox (#8275)

# Objective

- Closes https://github.com/bevyengine/bevy/issues/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>
This commit is contained in:
JMS55 2023-04-02 06:57:12 -04:00 committed by GitHub
parent 7a9e77c79c
commit f0f5d79917
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 349 additions and 129 deletions

View File

@ -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;
@ -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>,
>,
@ -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(());
@ -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
@ -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(())
}
}

View File

@ -52,6 +52,7 @@ use bevy_utils::{FloatOrd, HashMap};
use crate::{
prepass::{node::PrepassNode, DepthPrepass},
skybox::SkyboxPlugin,
tonemapping::TonemappingNode,
upscaling::UpscalingNode,
};
@ -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) {

View File

@ -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);

View File

@ -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 {

View File

@ -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));
}
}
}

View File

@ -0,0 +1,52 @@
#import bevy_render::view
@group(0) @binding(0)
var skybox: texture_cube<f32>;
@group(0) @binding(1)
var skybox_sampler: sampler;
@group(0) @binding(2)
var<uniform> view: View;
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) world_position: vec3<f32>,
};
// 3 | 2.
// 2 | : `.
// 1 | x-----x.
// 0 | | s | `.
// -1 | 0-----x.....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.
@vertex
fn skybox_vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
// See the explanation above for how this works.
let clip_position = vec4(
f32(vertex_index & 1u),
f32((vertex_index >> 1u) & 1u),
0.25,
0.5
) * 4.0 - vec4(1.0);
// Use the position on the near clipping plane to avoid -inf world position
// because the far plane of an infinite reverse projection is at infinity.
// NOTE: The clip position has a w component equal to 1.0 so we don't need
// to apply a perspective divide to it before inverse-projecting it.
let world_position_homogeneous = view.inverse_view_proj * vec4(clip_position.xy, 1.0, 1.0);
let world_position = world_position_homogeneous.xyz / world_position_homogeneous.w;
return VertexOutput(clip_position, world_position);
}
@fragment
fn skybox_fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// The skybox cubemap is sampled along the direction from the camera world
// position, to the fragment world position on the near clipping plane
let ray_direction = in.world_position - view.world_position;
// cube maps are left-handed so we negate the z coordinate
return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0));
}

View File

@ -21,8 +21,8 @@ fn environment_map_light(
// Technically we could use textureNumLevels(environment_map_specular) - 1 here, but we use a uniform
// because textureNumLevels() does not work on WebGL2
let radiance_level = perceptual_roughness * f32(lights.environment_map_smallest_specular_mip_level);
let irradiance = textureSample(environment_map_diffuse, environment_map_sampler, N).rgb;
let radiance = textureSampleLevel(environment_map_specular, environment_map_sampler, R, radiance_level).rgb;
let irradiance = textureSample(environment_map_diffuse, environment_map_sampler, vec3(N.xy, -N.z)).rgb;
let radiance = textureSampleLevel(environment_map_specular, environment_map_sampler, vec3(R.xy, -R.z), radiance_level).rgb;
// Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf
// Useful reference: https://bruop.github.io/ibl

View File

@ -4,22 +4,13 @@ use std::f32::consts::PI;
use bevy::{
asset::LoadState,
core_pipeline::Skybox,
input::mouse::MouseMotion,
pbr::{MaterialPipeline, MaterialPipelineKey},
prelude::*,
reflect::TypeUuid,
render::{
mesh::MeshVertexBufferLayout,
render_asset::RenderAssets,
render_resource::{
AsBindGroup, AsBindGroupError, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
OwnedBindingResource, PreparedBindGroup, RenderPipelineDescriptor, SamplerBindingType,
ShaderRef, ShaderStages, SpecializedMeshPipelineError, TextureSampleType,
TextureViewDescriptor, TextureViewDimension,
},
render_resource::{TextureViewDescriptor, TextureViewDimension},
renderer::RenderDevice,
texture::{CompressedImageFormats, FallbackImage},
texture::CompressedImageFormats,
},
};
@ -45,7 +36,6 @@ const CUBEMAPS: &[(&str, CompressedImageFormats)] = &[
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(MaterialPlugin::<CubemapMaterial>::default())
.add_systems(Startup, setup)
.add_systems(
Update,
@ -86,6 +76,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
CameraController::default(),
Skybox(skybox_handle.clone()),
));
// ambient light
@ -145,13 +136,10 @@ fn cycle_cubemap_asset(
}
fn asset_loaded(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut images: ResMut<Assets<Image>>,
mut meshes: ResMut<Assets<Mesh>>,
mut cubemap_materials: ResMut<Assets<CubemapMaterial>>,
mut cubemap: ResMut<Cubemap>,
cubes: Query<&Handle<CubemapMaterial>>,
mut skyboxes: Query<&mut Skybox>,
) {
if !cubemap.is_loaded
&& asset_server.get_load_state(cubemap.image_handle.clone_weak()) == LoadState::Loaded
@ -170,22 +158,8 @@ fn asset_loaded(
});
}
// spawn cube
let mut updated = false;
for handle in cubes.iter() {
if let Some(material) = cubemap_materials.get_mut(handle) {
updated = true;
material.base_color_texture = Some(cubemap.image_handle.clone_weak());
}
}
if !updated {
commands.spawn(MaterialMeshBundle::<CubemapMaterial> {
mesh: meshes.add(Mesh::from(shape::Cube { size: 10000.0 })),
material: cubemap_materials.add(CubemapMaterial {
base_color_texture: Some(cubemap.image_handle.clone_weak()),
}),
..default()
});
for mut skybox in &mut skyboxes {
skybox.0 = cubemap.image_handle.clone();
}
cubemap.is_loaded = true;
@ -201,97 +175,6 @@ fn animate_light_direction(
}
}
#[derive(Debug, Clone, TypeUuid)]
#[uuid = "9509a0f8-3c05-48ee-a13e-a93226c7f488"]
struct CubemapMaterial {
base_color_texture: Option<Handle<Image>>,
}
impl Material for CubemapMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/cubemap_unlit.wgsl".into()
}
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayout,
_key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
descriptor.primitive.cull_mode = None;
Ok(())
}
}
impl AsBindGroup for CubemapMaterial {
type Data = ();
fn as_bind_group(
&self,
layout: &BindGroupLayout,
render_device: &RenderDevice,
images: &RenderAssets<Image>,
_fallback_image: &FallbackImage,
) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError> {
let base_color_texture = self
.base_color_texture
.as_ref()
.ok_or(AsBindGroupError::RetryNextUpdate)?;
let image = images
.get(base_color_texture)
.ok_or(AsBindGroupError::RetryNextUpdate)?;
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&image.texture_view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&image.sampler),
},
],
label: Some("cubemap_texture_material_bind_group"),
layout,
});
Ok(PreparedBindGroup {
bind_group,
bindings: vec![
OwnedBindingResource::TextureView(image.texture_view.clone()),
OwnedBindingResource::Sampler(image.sampler.clone()),
],
data: (),
})
}
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
// Cubemap Base Color Texture
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::Cube,
},
count: None,
},
// Cubemap Base Color Texture Sampler
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
},
],
label: None,
})
}
}
#[derive(Component)]
pub struct CameraController {
pub enabled: bool,