mirror of
https://github.com/bevyengine/bevy
synced 2024-12-30 15:03:23 +00:00
424 lines
15 KiB
Rust
424 lines
15 KiB
Rust
use crate::{render::MeshViewBindGroups, ExtractedMeshes, PointLight};
|
|
use bevy_ecs::{prelude::*, system::SystemState};
|
|
use bevy_math::{Mat4, Vec3, Vec4};
|
|
use bevy_render2::{
|
|
color::Color,
|
|
core_pipeline::Transparent3dPhase,
|
|
pass::*,
|
|
pipeline::*,
|
|
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
|
render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass},
|
|
render_resource::{DynamicUniformVec, SamplerId, TextureId, TextureViewId},
|
|
renderer::{RenderContext, RenderResources},
|
|
shader::{Shader, ShaderStage, ShaderStages},
|
|
texture::*,
|
|
view::{ExtractedView, ViewUniform},
|
|
};
|
|
use bevy_transform::components::GlobalTransform;
|
|
use crevice::std140::AsStd140;
|
|
use std::num::NonZeroU32;
|
|
|
|
pub struct ExtractedPointLight {
|
|
color: Color,
|
|
intensity: f32,
|
|
range: f32,
|
|
radius: f32,
|
|
transform: GlobalTransform,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, AsStd140, Default, Debug)]
|
|
pub struct GpuLight {
|
|
color: Vec4,
|
|
range: f32,
|
|
radius: f32,
|
|
position: Vec3,
|
|
view_proj: Mat4,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, Debug, AsStd140)]
|
|
pub struct GpuLights {
|
|
len: u32,
|
|
lights: [GpuLight; MAX_POINT_LIGHTS],
|
|
}
|
|
|
|
// NOTE: this must be kept in sync MAX_POINT_LIGHTS in pbr.frag
|
|
pub const MAX_POINT_LIGHTS: usize = 10;
|
|
pub const SHADOW_SIZE: Extent3d = Extent3d {
|
|
width: 1024,
|
|
height: 1024,
|
|
depth_or_array_layers: MAX_POINT_LIGHTS as u32,
|
|
};
|
|
pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float;
|
|
|
|
pub struct ShadowShaders {
|
|
pub pipeline: PipelineId,
|
|
pub pipeline_descriptor: RenderPipelineDescriptor,
|
|
pub light_sampler: SamplerId,
|
|
}
|
|
|
|
// TODO: this pattern for initializing the shaders / pipeline isn't ideal. this should be handled by the asset system
|
|
impl FromWorld for ShadowShaders {
|
|
fn from_world(world: &mut World) -> Self {
|
|
let render_resources = world.get_resource::<RenderResources>().unwrap();
|
|
let vertex_shader = Shader::from_glsl(ShaderStage::Vertex, include_str!("pbr.vert"))
|
|
.get_spirv_shader(None)
|
|
.unwrap();
|
|
let vertex_layout = vertex_shader.reflect_layout(true).unwrap();
|
|
|
|
let mut pipeline_layout = PipelineLayout::from_shader_layouts(&mut [vertex_layout]);
|
|
|
|
let vertex = render_resources.create_shader_module(&vertex_shader);
|
|
|
|
pipeline_layout.vertex_buffer_descriptors = vec![VertexBufferLayout {
|
|
stride: 32,
|
|
name: "Vertex".into(),
|
|
step_mode: InputStepMode::Vertex,
|
|
attributes: vec![
|
|
// GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically)
|
|
VertexAttribute {
|
|
name: "Vertex_Position".into(),
|
|
format: VertexFormat::Float32x3,
|
|
offset: 12,
|
|
shader_location: 0,
|
|
},
|
|
VertexAttribute {
|
|
name: "Vertex_Normals".into(),
|
|
format: VertexFormat::Float32x3,
|
|
offset: 0,
|
|
shader_location: 1,
|
|
},
|
|
VertexAttribute {
|
|
name: "Vertex_Uv".into(),
|
|
format: VertexFormat::Float32x2,
|
|
offset: 24,
|
|
shader_location: 2,
|
|
},
|
|
],
|
|
}];
|
|
|
|
pipeline_layout.bind_group_mut(0).bindings[0].set_dynamic(true);
|
|
pipeline_layout.bind_group_mut(1).bindings[0].set_dynamic(true);
|
|
pipeline_layout.update_bind_group_ids();
|
|
|
|
let pipeline_descriptor = RenderPipelineDescriptor {
|
|
depth_stencil: Some(DepthStencilState {
|
|
format: SHADOW_FORMAT,
|
|
depth_write_enabled: true,
|
|
depth_compare: CompareFunction::LessEqual,
|
|
stencil: StencilState {
|
|
front: StencilFaceState::IGNORE,
|
|
back: StencilFaceState::IGNORE,
|
|
read_mask: 0,
|
|
write_mask: 0,
|
|
},
|
|
bias: DepthBiasState {
|
|
constant: 2,
|
|
slope_scale: 2.0,
|
|
clamp: 0.0,
|
|
},
|
|
}),
|
|
primitive: PrimitiveState {
|
|
topology: PrimitiveTopology::TriangleList,
|
|
cull_mode: Some(Face::Back),
|
|
// TODO: detect if this feature is enabled
|
|
clamp_depth: false,
|
|
..Default::default()
|
|
},
|
|
color_target_states: vec![],
|
|
..RenderPipelineDescriptor::new(
|
|
ShaderStages {
|
|
vertex,
|
|
fragment: None,
|
|
},
|
|
pipeline_layout,
|
|
)
|
|
};
|
|
|
|
let pipeline = render_resources.create_render_pipeline(&pipeline_descriptor);
|
|
|
|
ShadowShaders {
|
|
pipeline,
|
|
pipeline_descriptor,
|
|
light_sampler: render_resources.create_sampler(&SamplerDescriptor {
|
|
address_mode_u: AddressMode::ClampToEdge,
|
|
address_mode_v: AddressMode::ClampToEdge,
|
|
address_mode_w: AddressMode::ClampToEdge,
|
|
mag_filter: FilterMode::Linear,
|
|
min_filter: FilterMode::Linear,
|
|
mipmap_filter: FilterMode::Nearest,
|
|
compare_function: Some(CompareFunction::LessEqual),
|
|
..Default::default()
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: ultimately these could be filtered down to lights relevant to actual views
|
|
pub fn extract_lights(
|
|
mut commands: Commands,
|
|
lights: Query<(Entity, &PointLight, &GlobalTransform)>,
|
|
) {
|
|
for (entity, light, transform) in lights.iter() {
|
|
commands.get_or_spawn(entity).insert(ExtractedPointLight {
|
|
color: light.color,
|
|
intensity: light.intensity,
|
|
range: light.range,
|
|
radius: light.radius,
|
|
transform: transform.clone(),
|
|
});
|
|
}
|
|
}
|
|
|
|
pub struct ViewLight {
|
|
pub depth_texture: TextureViewId,
|
|
}
|
|
|
|
pub struct ViewLights {
|
|
pub light_depth_texture: TextureId,
|
|
pub light_depth_texture_view: TextureViewId,
|
|
pub lights: Vec<Entity>,
|
|
pub gpu_light_binding_index: u32,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct LightMeta {
|
|
pub view_gpu_lights: DynamicUniformVec<GpuLights>,
|
|
}
|
|
|
|
pub fn prepare_lights(
|
|
mut commands: Commands,
|
|
mut texture_cache: ResMut<TextureCache>,
|
|
render_resources: Res<RenderResources>,
|
|
mut light_meta: ResMut<LightMeta>,
|
|
views: Query<Entity, With<RenderPhase<Transparent3dPhase>>>,
|
|
lights: Query<&ExtractedPointLight>,
|
|
) {
|
|
// PERF: view.iter().count() could be views.iter().len() if we implemented ExactSizeIterator for archetype-only filters
|
|
light_meta
|
|
.view_gpu_lights
|
|
.reserve_and_clear(views.iter().count(), &render_resources);
|
|
|
|
// set up light data for each view
|
|
for entity in views.iter() {
|
|
let light_depth_texture = texture_cache.get(
|
|
&render_resources,
|
|
TextureDescriptor {
|
|
size: SHADOW_SIZE,
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: SHADOW_FORMAT,
|
|
usage: TextureUsage::RENDER_ATTACHMENT | TextureUsage::SAMPLED,
|
|
..Default::default()
|
|
},
|
|
);
|
|
let mut view_lights = Vec::new();
|
|
|
|
let mut gpu_lights = GpuLights {
|
|
len: lights.iter().len() as u32,
|
|
lights: [GpuLight::default(); MAX_POINT_LIGHTS],
|
|
};
|
|
|
|
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
|
|
for (i, light) in lights.iter().enumerate().take(MAX_POINT_LIGHTS) {
|
|
let depth_texture_view = render_resources.create_texture_view(
|
|
light_depth_texture.texture,
|
|
TextureViewDescriptor {
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::D2),
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
level_count: None,
|
|
base_array_layer: i as u32,
|
|
array_layer_count: NonZeroU32::new(1),
|
|
},
|
|
);
|
|
|
|
let view_transform = GlobalTransform::from_translation(light.transform.translation)
|
|
.looking_at(Vec3::default(), Vec3::Y);
|
|
// TODO: configure light projection based on light configuration
|
|
let projection = Mat4::perspective_rh(1.0472, 1.0, 1.0, 20.0);
|
|
|
|
gpu_lights.lights[i] = GpuLight {
|
|
// premultiply color by intensity
|
|
// we don't use the alpha at all, so no reason to multiply only [0..3]
|
|
color: (light.color * light.intensity).into(),
|
|
radius: light.radius.into(),
|
|
position: light.transform.translation.into(),
|
|
range: 1.0 / (light.range * light.range),
|
|
// this could technically be copied to the gpu from the light's ViewUniforms
|
|
view_proj: projection * view_transform.compute_matrix().inverse(),
|
|
};
|
|
|
|
let view_light_entity = commands
|
|
.spawn()
|
|
.insert_bundle((
|
|
ViewLight {
|
|
depth_texture: depth_texture_view,
|
|
},
|
|
ExtractedView {
|
|
width: SHADOW_SIZE.width,
|
|
height: SHADOW_SIZE.height,
|
|
transform: view_transform.clone(),
|
|
projection,
|
|
},
|
|
RenderPhase::<ShadowPhase>::default(),
|
|
))
|
|
.id();
|
|
view_lights.push(view_light_entity);
|
|
}
|
|
|
|
commands.entity(entity).insert(ViewLights {
|
|
light_depth_texture: light_depth_texture.texture,
|
|
light_depth_texture_view: light_depth_texture.default_view,
|
|
lights: view_lights,
|
|
gpu_light_binding_index: light_meta.view_gpu_lights.push(gpu_lights),
|
|
});
|
|
}
|
|
|
|
light_meta
|
|
.view_gpu_lights
|
|
.write_to_staging_buffer(&render_resources);
|
|
}
|
|
|
|
// TODO: we can remove this once we move to RAII
|
|
pub fn cleanup_view_lights(render_resources: Res<RenderResources>, query: Query<&ViewLight>) {
|
|
for view_light in query.iter() {
|
|
render_resources.remove_texture_view(view_light.depth_texture);
|
|
}
|
|
}
|
|
|
|
pub struct ShadowPhase;
|
|
|
|
pub struct ShadowPassNode {
|
|
main_view_query: QueryState<&'static ViewLights>,
|
|
view_light_query: QueryState<(&'static ViewLight, &'static RenderPhase<ShadowPhase>)>,
|
|
}
|
|
|
|
impl ShadowPassNode {
|
|
pub const IN_VIEW: &'static str = "view";
|
|
|
|
pub fn new(world: &mut World) -> Self {
|
|
Self {
|
|
main_view_query: QueryState::new(world),
|
|
view_light_query: QueryState::new(world),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Node for ShadowPassNode {
|
|
fn input(&self) -> Vec<SlotInfo> {
|
|
vec![SlotInfo::new(ShadowPassNode::IN_VIEW, SlotType::Entity)]
|
|
}
|
|
|
|
fn update(&mut self, world: &mut World) {
|
|
self.main_view_query.update_archetypes(world);
|
|
self.view_light_query.update_archetypes(world);
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
graph: &mut RenderGraphContext,
|
|
render_context: &mut dyn RenderContext,
|
|
world: &World,
|
|
) -> Result<(), NodeRunError> {
|
|
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
|
|
let view_lights = self.main_view_query.get_manual(world, view_entity).unwrap();
|
|
for view_light_entity in view_lights.lights.iter().copied() {
|
|
let (view_light, shadow_phase) = self
|
|
.view_light_query
|
|
.get_manual(world, view_light_entity)
|
|
.unwrap();
|
|
let pass_descriptor = PassDescriptor {
|
|
color_attachments: Vec::new(),
|
|
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
|
|
attachment: TextureAttachment::Id(view_light.depth_texture),
|
|
depth_ops: Some(Operations {
|
|
load: LoadOp::Clear(1.0),
|
|
store: true,
|
|
}),
|
|
stencil_ops: None,
|
|
}),
|
|
sample_count: 1,
|
|
};
|
|
|
|
let draw_functions = world.get_resource::<DrawFunctions>().unwrap();
|
|
|
|
render_context.begin_render_pass(
|
|
&pass_descriptor,
|
|
&mut |render_pass: &mut dyn RenderPass| {
|
|
let mut draw_functions = draw_functions.write();
|
|
let mut tracked_pass = TrackedRenderPass::new(render_pass);
|
|
for drawable in shadow_phase.drawn_things.iter() {
|
|
let draw_function = draw_functions.get_mut(drawable.draw_function).unwrap();
|
|
draw_function.draw(
|
|
world,
|
|
&mut tracked_pass,
|
|
view_light_entity,
|
|
drawable.draw_key,
|
|
drawable.sort_key,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
type DrawShadowMeshParams<'a> = (
|
|
Res<'a, ShadowShaders>,
|
|
Res<'a, ExtractedMeshes>,
|
|
Query<'a, (&'a ViewUniform, &'a MeshViewBindGroups)>,
|
|
);
|
|
pub struct DrawShadowMesh {
|
|
params: SystemState<DrawShadowMeshParams<'static>>,
|
|
}
|
|
|
|
impl DrawShadowMesh {
|
|
pub fn new(world: &mut World) -> Self {
|
|
Self {
|
|
params: SystemState::new(world),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Draw for DrawShadowMesh {
|
|
fn draw(
|
|
&mut self,
|
|
world: &World,
|
|
pass: &mut TrackedRenderPass,
|
|
view: Entity,
|
|
draw_key: usize,
|
|
_sort_key: usize,
|
|
) {
|
|
let (shadow_shaders, extracted_meshes, views) = self.params.get(world);
|
|
let (view_uniforms, mesh_view_bind_groups) = views.get(view).unwrap();
|
|
let layout = &shadow_shaders.pipeline_descriptor.layout;
|
|
let extracted_mesh = &extracted_meshes.meshes[draw_key];
|
|
pass.set_pipeline(shadow_shaders.pipeline);
|
|
pass.set_bind_group(
|
|
0,
|
|
layout.bind_group(0).id,
|
|
mesh_view_bind_groups.view_bind_group,
|
|
Some(&[view_uniforms.view_uniform_offset]),
|
|
);
|
|
|
|
pass.set_bind_group(
|
|
1,
|
|
layout.bind_group(1).id,
|
|
mesh_view_bind_groups.mesh_transform_bind_group,
|
|
Some(&[extracted_mesh.transform_binding_offset]),
|
|
);
|
|
pass.set_vertex_buffer(0, extracted_mesh.vertex_buffer, 0);
|
|
if let Some(index_info) = &extracted_mesh.index_info {
|
|
pass.set_index_buffer(index_info.buffer, 0, IndexFormat::Uint32);
|
|
pass.draw_indexed(0..index_info.count, 0, 0..1);
|
|
} else {
|
|
panic!("non-indexed drawing not supported yet")
|
|
}
|
|
}
|
|
}
|