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:
parent
7a9e77c79c
commit
f0f5d79917
@ -2,15 +2,18 @@ use crate::{
|
|||||||
clear_color::{ClearColor, ClearColorConfig},
|
clear_color::{ClearColor, ClearColorConfig},
|
||||||
core_3d::{Camera3d, Opaque3d},
|
core_3d::{Camera3d, Opaque3d},
|
||||||
prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass},
|
prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass},
|
||||||
|
skybox::{SkyboxBindGroup, SkyboxPipelineId},
|
||||||
};
|
};
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
camera::ExtractedCamera,
|
camera::ExtractedCamera,
|
||||||
render_graph::{Node, NodeRunError, RenderGraphContext},
|
render_graph::{Node, NodeRunError, RenderGraphContext},
|
||||||
render_phase::RenderPhase,
|
render_phase::RenderPhase,
|
||||||
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
|
render_resource::{
|
||||||
|
LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor,
|
||||||
|
},
|
||||||
renderer::RenderContext,
|
renderer::RenderContext,
|
||||||
view::{ExtractedView, ViewDepthTexture, ViewTarget},
|
view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "trace")]
|
#[cfg(feature = "trace")]
|
||||||
use bevy_utils::tracing::info_span;
|
use bevy_utils::tracing::info_span;
|
||||||
@ -30,6 +33,9 @@ pub struct MainOpaquePass3dNode {
|
|||||||
Option<&'static DepthPrepass>,
|
Option<&'static DepthPrepass>,
|
||||||
Option<&'static NormalPrepass>,
|
Option<&'static NormalPrepass>,
|
||||||
Option<&'static MotionVectorPrepass>,
|
Option<&'static MotionVectorPrepass>,
|
||||||
|
Option<&'static SkyboxPipelineId>,
|
||||||
|
Option<&'static SkyboxBindGroup>,
|
||||||
|
&'static ViewUniformOffset,
|
||||||
),
|
),
|
||||||
With<ExtractedView>,
|
With<ExtractedView>,
|
||||||
>,
|
>,
|
||||||
@ -64,7 +70,10 @@ impl Node for MainOpaquePass3dNode {
|
|||||||
depth,
|
depth,
|
||||||
depth_prepass,
|
depth_prepass,
|
||||||
normal_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 {
|
)) = self.query.get_manual(world, view_entity) else {
|
||||||
// No window
|
// No window
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -75,6 +84,7 @@ impl Node for MainOpaquePass3dNode {
|
|||||||
#[cfg(feature = "trace")]
|
#[cfg(feature = "trace")]
|
||||||
let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered();
|
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 {
|
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
|
||||||
label: Some("main_opaque_pass_3d"),
|
label: Some("main_opaque_pass_3d"),
|
||||||
// NOTE: The opaque pass loads the color
|
// NOTE: The opaque pass loads the color
|
||||||
@ -115,12 +125,26 @@ impl Node for MainOpaquePass3dNode {
|
|||||||
render_pass.set_camera_viewport(viewport);
|
render_pass.set_camera_viewport(viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opaque draws
|
||||||
opaque_phase.render(&mut render_pass, world, view_entity);
|
opaque_phase.render(&mut render_pass, world, view_entity);
|
||||||
|
|
||||||
|
// Alpha draws
|
||||||
if !alpha_mask_phase.items.is_empty() {
|
if !alpha_mask_phase.items.is_empty() {
|
||||||
alpha_mask_phase.render(&mut render_pass, world, view_entity);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ use bevy_utils::{FloatOrd, HashMap};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
prepass::{node::PrepassNode, DepthPrepass},
|
prepass::{node::PrepassNode, DepthPrepass},
|
||||||
|
skybox::SkyboxPlugin,
|
||||||
tonemapping::TonemappingNode,
|
tonemapping::TonemappingNode,
|
||||||
upscaling::UpscalingNode,
|
upscaling::UpscalingNode,
|
||||||
};
|
};
|
||||||
@ -62,6 +63,7 @@ impl Plugin for Core3dPlugin {
|
|||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.register_type::<Camera3d>()
|
app.register_type::<Camera3d>()
|
||||||
.register_type::<Camera3dDepthLoadOp>()
|
.register_type::<Camera3dDepthLoadOp>()
|
||||||
|
.add_plugin(SkyboxPlugin)
|
||||||
.add_plugin(ExtractComponentPlugin::<Camera3d>::default());
|
.add_plugin(ExtractComponentPlugin::<Camera3d>::default());
|
||||||
|
|
||||||
let render_app = match app.get_sub_app_mut(RenderApp) {
|
let render_app = match app.get_sub_app_mut(RenderApp) {
|
||||||
|
@ -7,8 +7,26 @@ struct FullscreenVertexOutput {
|
|||||||
uv: vec2<f32>,
|
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
|
@vertex
|
||||||
fn fullscreen_vertex_shader(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput {
|
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 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);
|
let clip_position = vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
|
||||||
|
|
||||||
|
@ -7,10 +7,13 @@ pub mod fullscreen_vertex_shader;
|
|||||||
pub mod fxaa;
|
pub mod fxaa;
|
||||||
pub mod msaa_writeback;
|
pub mod msaa_writeback;
|
||||||
pub mod prepass;
|
pub mod prepass;
|
||||||
|
mod skybox;
|
||||||
mod taa;
|
mod taa;
|
||||||
pub mod tonemapping;
|
pub mod tonemapping;
|
||||||
pub mod upscaling;
|
pub mod upscaling;
|
||||||
|
|
||||||
|
pub use skybox::Skybox;
|
||||||
|
|
||||||
/// Experimental features that are not yet finished. Please report any issues you encounter!
|
/// Experimental features that are not yet finished. Please report any issues you encounter!
|
||||||
pub mod experimental {
|
pub mod experimental {
|
||||||
pub mod taa {
|
pub mod taa {
|
||||||
|
238
crates/bevy_core_pipeline/src/skybox/mod.rs
Normal file
238
crates/bevy_core_pipeline/src/skybox/mod.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
crates/bevy_core_pipeline/src/skybox/skybox.wgsl
Normal file
52
crates/bevy_core_pipeline/src/skybox/skybox.wgsl
Normal 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));
|
||||||
|
}
|
@ -21,8 +21,8 @@ fn environment_map_light(
|
|||||||
// Technically we could use textureNumLevels(environment_map_specular) - 1 here, but we use a uniform
|
// Technically we could use textureNumLevels(environment_map_specular) - 1 here, but we use a uniform
|
||||||
// because textureNumLevels() does not work on WebGL2
|
// because textureNumLevels() does not work on WebGL2
|
||||||
let radiance_level = perceptual_roughness * f32(lights.environment_map_smallest_specular_mip_level);
|
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 irradiance = textureSample(environment_map_diffuse, environment_map_sampler, vec3(N.xy, -N.z)).rgb;
|
||||||
let radiance = textureSampleLevel(environment_map_specular, environment_map_sampler, R, radiance_level).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
|
// Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf
|
||||||
// Useful reference: https://bruop.github.io/ibl
|
// Useful reference: https://bruop.github.io/ibl
|
||||||
|
@ -4,22 +4,13 @@ use std::f32::consts::PI;
|
|||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
asset::LoadState,
|
asset::LoadState,
|
||||||
|
core_pipeline::Skybox,
|
||||||
input::mouse::MouseMotion,
|
input::mouse::MouseMotion,
|
||||||
pbr::{MaterialPipeline, MaterialPipelineKey},
|
|
||||||
prelude::*,
|
prelude::*,
|
||||||
reflect::TypeUuid,
|
|
||||||
render::{
|
render::{
|
||||||
mesh::MeshVertexBufferLayout,
|
render_resource::{TextureViewDescriptor, TextureViewDimension},
|
||||||
render_asset::RenderAssets,
|
|
||||||
render_resource::{
|
|
||||||
AsBindGroup, AsBindGroupError, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
|
|
||||||
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
|
|
||||||
OwnedBindingResource, PreparedBindGroup, RenderPipelineDescriptor, SamplerBindingType,
|
|
||||||
ShaderRef, ShaderStages, SpecializedMeshPipelineError, TextureSampleType,
|
|
||||||
TextureViewDescriptor, TextureViewDimension,
|
|
||||||
},
|
|
||||||
renderer::RenderDevice,
|
renderer::RenderDevice,
|
||||||
texture::{CompressedImageFormats, FallbackImage},
|
texture::CompressedImageFormats,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,7 +36,6 @@ const CUBEMAPS: &[(&str, CompressedImageFormats)] = &[
|
|||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins)
|
.add_plugins(DefaultPlugins)
|
||||||
.add_plugin(MaterialPlugin::<CubemapMaterial>::default())
|
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
@ -86,6 +76,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
CameraController::default(),
|
CameraController::default(),
|
||||||
|
Skybox(skybox_handle.clone()),
|
||||||
));
|
));
|
||||||
|
|
||||||
// ambient light
|
// ambient light
|
||||||
@ -145,13 +136,10 @@ fn cycle_cubemap_asset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn asset_loaded(
|
fn asset_loaded(
|
||||||
mut commands: Commands,
|
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
mut images: ResMut<Assets<Image>>,
|
mut images: ResMut<Assets<Image>>,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
mut cubemap_materials: ResMut<Assets<CubemapMaterial>>,
|
|
||||||
mut cubemap: ResMut<Cubemap>,
|
mut cubemap: ResMut<Cubemap>,
|
||||||
cubes: Query<&Handle<CubemapMaterial>>,
|
mut skyboxes: Query<&mut Skybox>,
|
||||||
) {
|
) {
|
||||||
if !cubemap.is_loaded
|
if !cubemap.is_loaded
|
||||||
&& asset_server.get_load_state(cubemap.image_handle.clone_weak()) == LoadState::Loaded
|
&& asset_server.get_load_state(cubemap.image_handle.clone_weak()) == LoadState::Loaded
|
||||||
@ -170,22 +158,8 @@ fn asset_loaded(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// spawn cube
|
for mut skybox in &mut skyboxes {
|
||||||
let mut updated = false;
|
skybox.0 = cubemap.image_handle.clone();
|
||||||
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()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cubemap.is_loaded = true;
|
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)]
|
#[derive(Component)]
|
||||||
pub struct CameraController {
|
pub struct CameraController {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
Loading…
Reference in New Issue
Block a user