mirror of
https://github.com/bevyengine/bevy
synced 2024-12-21 10:33:08 +00:00
08969a24b8
This changes how render logic is composed to make it much more modular. Previously, all extraction logic was centralized for a given "type" of rendered thing. For example, we extracted meshes into a vector of ExtractedMesh, which contained the mesh and material asset handles, the transform, etc. We looked up bindings for "drawn things" using their index in the `Vec<ExtractedMesh>`. This worked fine for built in rendering, but made it hard to reuse logic for "custom" rendering. It also prevented us from reusing things like "extracted transforms" across contexts. To make rendering more modular, I made a number of changes: * Entities now drive rendering: * We extract "render components" from "app components" and store them _on_ entities. No more centralized uber lists! We now have true "ECS-driven rendering" * To make this perform well, I implemented #2673 in upstream Bevy for fast batch insertions into specific entities. This was merged into the `pipelined-rendering` branch here: #2815 * Reworked the `Draw` abstraction: * Generic `PhaseItems`: each draw phase can define its own type of "rendered thing", which can define its own "sort key" * Ported the 2d, 3d, and shadow phases to the new PhaseItem impl (currently Transparent2d, Transparent3d, and Shadow PhaseItems) * `Draw` trait and and `DrawFunctions` are now generic on PhaseItem * Modular / Ergonomic `DrawFunctions` via `RenderCommands` * RenderCommand is a trait that runs an ECS query and produces one or more RenderPass calls. Types implementing this trait can be composed to create a final DrawFunction. For example the DrawPbr DrawFunction is created from the following DrawCommand tuple. Const generics are used to set specific bind group locations: ```rust pub type DrawPbr = ( SetPbrPipeline, SetMeshViewBindGroup<0>, SetStandardMaterialBindGroup<1>, SetTransformBindGroup<2>, DrawMesh, ); ``` * The new `custom_shader_pipelined` example illustrates how the commands above can be reused to create a custom draw function: ```rust type DrawCustom = ( SetCustomMaterialPipeline, SetMeshViewBindGroup<0>, SetTransformBindGroup<2>, DrawMesh, ); ``` * ExtractComponentPlugin and UniformComponentPlugin: * Simple, standardized ways to easily extract individual components and write them to GPU buffers * Ported PBR and Sprite rendering to the new primitives above. * Removed staging buffer from UniformVec in favor of direct Queue usage * Makes UniformVec much easier to use and more ergonomic. Completely removes the need for custom render graph nodes in these contexts (see the PbrNode and view Node removals and the much simpler call patterns in the relevant Prepare systems). * Added a many_cubes_pipelined example to benchmark baseline 3d rendering performance and ensure there were no major regressions during this port. Avoiding regressions was challenging given that the old approach of extracting into centralized vectors is basically the "optimal" approach. However thanks to a various ECS optimizations and render logic rephrasing, we pretty much break even on this benchmark! * Lifetimeless SystemParams: this will be a bit divisive, but as we continue to embrace "trait driven systems" (ex: ExtractComponentPlugin, UniformComponentPlugin, DrawCommand), the ergonomics of `(Query<'static, 'static, (&'static A, &'static B, &'static)>, Res<'static, C>)` were getting very hard to bear. As a compromise, I added "static type aliases" for the relevant SystemParams. The previous example can now be expressed like this: `(SQuery<(Read<A>, Read<B>)>, SRes<C>)`. If anyone has better ideas / conflicting opinions, please let me know! * RunSystem trait: a way to define Systems via a trait with a SystemParam associated type. This is used to implement the various plugins mentioned above. I also added SystemParamItem and QueryItem type aliases to make "trait stye" ecs interactions nicer on the eyes (and fingers). * RenderAsset retrying: ensures that render assets are only created when they are "ready" and allows us to create bind groups directly inside render assets (which significantly simplified the StandardMaterial code). I think ultimately we should swap this out on "asset dependency" events to wait for dependencies to load, but this will require significant asset system changes. * Updated some built in shaders to account for missing MeshUniform fields
811 lines
31 KiB
Rust
811 lines
31 KiB
Rust
use crate::{
|
|
AmbientLight, DirectionalLight, DirectionalLightShadowMap, MeshUniform, NotShadowCaster,
|
|
PbrShaders, PointLight, PointLightShadowMap, TransformBindGroup,
|
|
};
|
|
use bevy_asset::Handle;
|
|
use bevy_core::FloatOrd;
|
|
use bevy_core_pipeline::Transparent3d;
|
|
use bevy_ecs::{
|
|
prelude::*,
|
|
system::{lifetimeless::*, SystemState},
|
|
};
|
|
use bevy_math::{const_vec3, Mat4, Vec3, Vec4};
|
|
use bevy_render2::{
|
|
camera::CameraProjection,
|
|
color::Color,
|
|
mesh::Mesh,
|
|
render_asset::RenderAssets,
|
|
render_component::DynamicUniformIndex,
|
|
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
|
render_phase::{
|
|
Draw, DrawFunctionId, DrawFunctions, PhaseItem, RenderPhase, TrackedRenderPass,
|
|
},
|
|
render_resource::*,
|
|
renderer::{RenderContext, RenderDevice, RenderQueue},
|
|
shader::Shader,
|
|
texture::*,
|
|
view::{ExtractedView, ViewUniformOffset, ViewUniforms},
|
|
};
|
|
use bevy_transform::components::GlobalTransform;
|
|
use crevice::std140::AsStd140;
|
|
use std::num::NonZeroU32;
|
|
|
|
pub struct ExtractedAmbientLight {
|
|
color: Color,
|
|
brightness: f32,
|
|
}
|
|
|
|
pub struct ExtractedPointLight {
|
|
color: Color,
|
|
/// luminous intensity in lumens per steradian
|
|
intensity: f32,
|
|
range: f32,
|
|
radius: f32,
|
|
transform: GlobalTransform,
|
|
shadow_depth_bias: f32,
|
|
shadow_normal_bias: f32,
|
|
}
|
|
|
|
pub type ExtractedPointLightShadowMap = PointLightShadowMap;
|
|
|
|
pub struct ExtractedDirectionalLight {
|
|
color: Color,
|
|
illuminance: f32,
|
|
direction: Vec3,
|
|
projection: Mat4,
|
|
shadow_depth_bias: f32,
|
|
shadow_normal_bias: f32,
|
|
}
|
|
|
|
pub type ExtractedDirectionalLightShadowMap = DirectionalLightShadowMap;
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, AsStd140, Default, Debug)]
|
|
pub struct GpuPointLight {
|
|
projection: Mat4,
|
|
color: Vec4,
|
|
position: Vec3,
|
|
inverse_square_range: f32,
|
|
radius: f32,
|
|
near: f32,
|
|
far: f32,
|
|
shadow_depth_bias: f32,
|
|
shadow_normal_bias: f32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, AsStd140, Default, Debug)]
|
|
pub struct GpuDirectionalLight {
|
|
view_projection: Mat4,
|
|
color: Vec4,
|
|
dir_to_light: Vec3,
|
|
shadow_depth_bias: f32,
|
|
shadow_normal_bias: f32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, Debug, AsStd140)]
|
|
pub struct GpuLights {
|
|
// TODO: this comes first to work around a WGSL alignment issue. We need to solve this issue before releasing the renderer rework
|
|
point_lights: [GpuPointLight; MAX_POINT_LIGHTS],
|
|
directional_lights: [GpuDirectionalLight; MAX_DIRECTIONAL_LIGHTS],
|
|
ambient_color: Vec4,
|
|
n_point_lights: u32,
|
|
n_directional_lights: u32,
|
|
}
|
|
|
|
// NOTE: this must be kept in sync with the same constants in pbr.frag
|
|
pub const MAX_POINT_LIGHTS: usize = 10;
|
|
pub const MAX_DIRECTIONAL_LIGHTS: usize = 1;
|
|
pub const POINT_SHADOW_LAYERS: u32 = (6 * MAX_POINT_LIGHTS) as u32;
|
|
pub const DIRECTIONAL_SHADOW_LAYERS: u32 = MAX_DIRECTIONAL_LIGHTS as u32;
|
|
pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float;
|
|
|
|
pub struct ShadowShaders {
|
|
pub shader_module: ShaderModule,
|
|
pub pipeline: RenderPipeline,
|
|
pub view_layout: BindGroupLayout,
|
|
pub point_light_sampler: Sampler,
|
|
pub directional_light_sampler: Sampler,
|
|
}
|
|
|
|
// 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_device = world.get_resource::<RenderDevice>().unwrap();
|
|
let pbr_shaders = world.get_resource::<PbrShaders>().unwrap();
|
|
let shader = Shader::from_wgsl(include_str!("depth.wgsl"));
|
|
let shader_module = render_device.create_shader_module(&shader);
|
|
|
|
let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
|
entries: &[
|
|
// View
|
|
BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: ShaderStage::VERTEX | ShaderStage::FRAGMENT,
|
|
ty: BindingType::Buffer {
|
|
ty: BufferBindingType::Uniform,
|
|
has_dynamic_offset: true,
|
|
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
|
|
// Context: https://github.com/LPGhatguy/crevice/issues/29
|
|
min_binding_size: BufferSize::new(144),
|
|
},
|
|
count: None,
|
|
},
|
|
],
|
|
label: None,
|
|
});
|
|
|
|
let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
|
label: None,
|
|
push_constant_ranges: &[],
|
|
bind_group_layouts: &[&view_layout, &pbr_shaders.mesh_layout],
|
|
});
|
|
|
|
let pipeline = render_device.create_render_pipeline(&RenderPipelineDescriptor {
|
|
label: None,
|
|
vertex: VertexState {
|
|
buffers: &[VertexBufferLayout {
|
|
array_stride: 32,
|
|
step_mode: InputStepMode::Vertex,
|
|
attributes: &[
|
|
// Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically))
|
|
VertexAttribute {
|
|
format: VertexFormat::Float32x3,
|
|
offset: 12,
|
|
shader_location: 0,
|
|
},
|
|
// Normal
|
|
VertexAttribute {
|
|
format: VertexFormat::Float32x3,
|
|
offset: 0,
|
|
shader_location: 1,
|
|
},
|
|
// Uv
|
|
VertexAttribute {
|
|
format: VertexFormat::Float32x2,
|
|
offset: 24,
|
|
shader_location: 2,
|
|
},
|
|
],
|
|
}],
|
|
module: &shader_module,
|
|
entry_point: "vertex",
|
|
},
|
|
fragment: None,
|
|
depth_stencil: Some(DepthStencilState {
|
|
format: SHADOW_FORMAT,
|
|
depth_write_enabled: true,
|
|
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,
|
|
},
|
|
}),
|
|
layout: Some(&pipeline_layout),
|
|
multisample: MultisampleState::default(),
|
|
primitive: PrimitiveState {
|
|
topology: PrimitiveTopology::TriangleList,
|
|
strip_index_format: None,
|
|
front_face: FrontFace::Ccw,
|
|
cull_mode: None,
|
|
polygon_mode: PolygonMode::Fill,
|
|
clamp_depth: false,
|
|
conservative: false,
|
|
},
|
|
});
|
|
|
|
ShadowShaders {
|
|
shader_module,
|
|
pipeline,
|
|
view_layout,
|
|
point_light_sampler: render_device.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: Some(CompareFunction::GreaterEqual),
|
|
..Default::default()
|
|
}),
|
|
directional_light_sampler: render_device.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: Some(CompareFunction::GreaterEqual),
|
|
..Default::default()
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: ultimately these could be filtered down to lights relevant to actual views
|
|
pub fn extract_lights(
|
|
mut commands: Commands,
|
|
ambient_light: Res<AmbientLight>,
|
|
point_light_shadow_map: Res<PointLightShadowMap>,
|
|
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
|
|
point_lights: Query<(Entity, &PointLight, &GlobalTransform)>,
|
|
directional_lights: Query<(Entity, &DirectionalLight, &GlobalTransform)>,
|
|
) {
|
|
commands.insert_resource(ExtractedAmbientLight {
|
|
color: ambient_light.color,
|
|
brightness: ambient_light.brightness,
|
|
});
|
|
commands.insert_resource::<ExtractedPointLightShadowMap>(point_light_shadow_map.clone());
|
|
commands.insert_resource::<ExtractedDirectionalLightShadowMap>(
|
|
directional_light_shadow_map.clone(),
|
|
);
|
|
// This is the point light shadow map texel size for one face of the cube as a distance of 1.0
|
|
// world unit from the light.
|
|
// point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels
|
|
// PI / 4.0 is half the cube face fov, tan(PI / 4.0) = 1.0, so this simplifies to:
|
|
// point_light_texel_size = 2.0 / cube face width in texels
|
|
// NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to:
|
|
// https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/
|
|
let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32;
|
|
for (entity, point_light, transform) in point_lights.iter() {
|
|
commands.get_or_spawn(entity).insert(ExtractedPointLight {
|
|
color: point_light.color,
|
|
// NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian
|
|
// for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
|
|
// for details.
|
|
intensity: point_light.intensity / (4.0 * std::f32::consts::PI),
|
|
range: point_light.range,
|
|
radius: point_light.radius,
|
|
transform: *transform,
|
|
shadow_depth_bias: point_light.shadow_depth_bias,
|
|
// The factor of SQRT_2 is for the worst-case diagonal offset
|
|
shadow_normal_bias: point_light.shadow_normal_bias
|
|
* point_light_texel_size
|
|
* std::f32::consts::SQRT_2,
|
|
});
|
|
}
|
|
for (entity, directional_light, transform) in directional_lights.iter() {
|
|
// Calulate the directional light shadow map texel size using the largest x,y dimension of
|
|
// the orthographic projection divided by the shadow map resolution
|
|
// NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to:
|
|
// https://catlikecoding.com/unity/tutorials/custom-srp/directional-shadows/
|
|
let largest_dimension = (directional_light.shadow_projection.right
|
|
- directional_light.shadow_projection.left)
|
|
.max(
|
|
directional_light.shadow_projection.top
|
|
- directional_light.shadow_projection.bottom,
|
|
);
|
|
let directional_light_texel_size =
|
|
largest_dimension / directional_light_shadow_map.size as f32;
|
|
commands
|
|
.get_or_spawn(entity)
|
|
.insert(ExtractedDirectionalLight {
|
|
color: directional_light.color,
|
|
illuminance: directional_light.illuminance,
|
|
direction: transform.forward(),
|
|
projection: directional_light.shadow_projection.get_projection_matrix(),
|
|
shadow_depth_bias: directional_light.shadow_depth_bias,
|
|
// The factor of SQRT_2 is for the worst-case diagonal offset
|
|
shadow_normal_bias: directional_light.shadow_normal_bias
|
|
* directional_light_texel_size
|
|
* std::f32::consts::SQRT_2,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Can't do `Vec3::Y * -1.0` because mul isn't const
|
|
const NEGATIVE_X: Vec3 = const_vec3!([-1.0, 0.0, 0.0]);
|
|
const NEGATIVE_Y: Vec3 = const_vec3!([0.0, -1.0, 0.0]);
|
|
const NEGATIVE_Z: Vec3 = const_vec3!([0.0, 0.0, -1.0]);
|
|
|
|
struct CubeMapFace {
|
|
target: Vec3,
|
|
up: Vec3,
|
|
}
|
|
|
|
// see https://www.khronos.org/opengl/wiki/Cubemap_Texture
|
|
const CUBE_MAP_FACES: [CubeMapFace; 6] = [
|
|
// 0 GL_TEXTURE_CUBE_MAP_POSITIVE_X
|
|
CubeMapFace {
|
|
target: NEGATIVE_X,
|
|
up: NEGATIVE_Y,
|
|
},
|
|
// 1 GL_TEXTURE_CUBE_MAP_NEGATIVE_X
|
|
CubeMapFace {
|
|
target: Vec3::X,
|
|
up: NEGATIVE_Y,
|
|
},
|
|
// 2 GL_TEXTURE_CUBE_MAP_POSITIVE_Y
|
|
CubeMapFace {
|
|
target: NEGATIVE_Y,
|
|
up: Vec3::Z,
|
|
},
|
|
// 3 GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
|
|
CubeMapFace {
|
|
target: Vec3::Y,
|
|
up: NEGATIVE_Z,
|
|
},
|
|
// 4 GL_TEXTURE_CUBE_MAP_POSITIVE_Z
|
|
CubeMapFace {
|
|
target: NEGATIVE_Z,
|
|
up: NEGATIVE_Y,
|
|
},
|
|
// 5 GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
|
|
CubeMapFace {
|
|
target: Vec3::Z,
|
|
up: NEGATIVE_Y,
|
|
},
|
|
];
|
|
|
|
fn face_index_to_name(face_index: usize) -> &'static str {
|
|
match face_index {
|
|
0 => "+x",
|
|
1 => "-x",
|
|
2 => "+y",
|
|
3 => "-y",
|
|
4 => "+z",
|
|
5 => "-z",
|
|
_ => "invalid",
|
|
}
|
|
}
|
|
|
|
pub struct ViewLight {
|
|
pub depth_texture_view: TextureView,
|
|
pub pass_name: String,
|
|
}
|
|
|
|
pub struct ViewLights {
|
|
pub point_light_depth_texture: Texture,
|
|
pub point_light_depth_texture_view: TextureView,
|
|
pub directional_light_depth_texture: Texture,
|
|
pub directional_light_depth_texture_view: TextureView,
|
|
pub lights: Vec<Entity>,
|
|
pub gpu_light_binding_index: u32,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct LightMeta {
|
|
pub view_gpu_lights: DynamicUniformVec<GpuLights>,
|
|
pub shadow_view_bind_group: Option<BindGroup>,
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn prepare_lights(
|
|
mut commands: Commands,
|
|
mut texture_cache: ResMut<TextureCache>,
|
|
render_device: Res<RenderDevice>,
|
|
render_queue: Res<RenderQueue>,
|
|
mut light_meta: ResMut<LightMeta>,
|
|
views: Query<Entity, With<RenderPhase<Transparent3d>>>,
|
|
ambient_light: Res<ExtractedAmbientLight>,
|
|
point_light_shadow_map: Res<ExtractedPointLightShadowMap>,
|
|
directional_light_shadow_map: Res<ExtractedDirectionalLightShadowMap>,
|
|
point_lights: Query<&ExtractedPointLight>,
|
|
directional_lights: Query<&ExtractedDirectionalLight>,
|
|
) {
|
|
// 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_device);
|
|
|
|
let ambient_color = ambient_light.color.as_rgba_linear() * ambient_light.brightness;
|
|
// set up light data for each view
|
|
for entity in views.iter() {
|
|
let point_light_depth_texture = texture_cache.get(
|
|
&render_device,
|
|
TextureDescriptor {
|
|
size: Extent3d {
|
|
width: point_light_shadow_map.size as u32,
|
|
height: point_light_shadow_map.size as u32,
|
|
depth_or_array_layers: POINT_SHADOW_LAYERS,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: SHADOW_FORMAT,
|
|
usage: TextureUsage::RENDER_ATTACHMENT | TextureUsage::SAMPLED,
|
|
label: None,
|
|
},
|
|
);
|
|
let directional_light_depth_texture = texture_cache.get(
|
|
&render_device,
|
|
TextureDescriptor {
|
|
size: Extent3d {
|
|
width: directional_light_shadow_map.size as u32,
|
|
height: directional_light_shadow_map.size as u32,
|
|
depth_or_array_layers: DIRECTIONAL_SHADOW_LAYERS,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: SHADOW_FORMAT,
|
|
usage: TextureUsage::RENDER_ATTACHMENT | TextureUsage::SAMPLED,
|
|
label: None,
|
|
},
|
|
);
|
|
let mut view_lights = Vec::new();
|
|
|
|
let mut gpu_lights = GpuLights {
|
|
ambient_color: ambient_color.into(),
|
|
n_point_lights: point_lights.iter().len() as u32,
|
|
n_directional_lights: directional_lights.iter().len() as u32,
|
|
point_lights: [GpuPointLight::default(); MAX_POINT_LIGHTS],
|
|
directional_lights: [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS],
|
|
};
|
|
|
|
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
|
|
for (light_index, light) in point_lights.iter().enumerate().take(MAX_POINT_LIGHTS) {
|
|
let projection =
|
|
Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1);
|
|
|
|
// ignore scale because we don't want to effectively scale light radius and range
|
|
// by applying those as a view transform to shadow map rendering of objects
|
|
// and ignore rotation because we want the shadow map projections to align with the axes
|
|
let view_translation = GlobalTransform::from_translation(light.transform.translation);
|
|
|
|
for (face_index, CubeMapFace { target, up }) in CUBE_MAP_FACES.iter().enumerate() {
|
|
// use the cubemap projection direction
|
|
let view_rotation = GlobalTransform::identity().looking_at(*target, *up);
|
|
|
|
let depth_texture_view =
|
|
point_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: None,
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::D2),
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer: (light_index * 6 + face_index) as u32,
|
|
array_layer_count: NonZeroU32::new(1),
|
|
});
|
|
|
|
let view_light_entity = commands
|
|
.spawn()
|
|
.insert_bundle((
|
|
ViewLight {
|
|
depth_texture_view,
|
|
pass_name: format!(
|
|
"shadow pass point light {} {}",
|
|
light_index,
|
|
face_index_to_name(face_index)
|
|
),
|
|
},
|
|
ExtractedView {
|
|
width: point_light_shadow_map.size as u32,
|
|
height: point_light_shadow_map.size as u32,
|
|
transform: view_translation * view_rotation,
|
|
projection,
|
|
},
|
|
RenderPhase::<Shadow>::default(),
|
|
))
|
|
.id();
|
|
view_lights.push(view_light_entity);
|
|
}
|
|
|
|
gpu_lights.point_lights[light_index] = GpuPointLight {
|
|
projection,
|
|
// premultiply color by intensity
|
|
// we don't use the alpha at all, so no reason to multiply only [0..3]
|
|
color: (light.color.as_rgba_linear() * light.intensity).into(),
|
|
radius: light.radius,
|
|
position: light.transform.translation,
|
|
inverse_square_range: 1.0 / (light.range * light.range),
|
|
near: 0.1,
|
|
far: light.range,
|
|
shadow_depth_bias: light.shadow_depth_bias,
|
|
shadow_normal_bias: light.shadow_normal_bias,
|
|
};
|
|
}
|
|
|
|
for (i, light) in directional_lights
|
|
.iter()
|
|
.enumerate()
|
|
.take(MAX_DIRECTIONAL_LIGHTS)
|
|
{
|
|
// direction is negated to be ready for N.L
|
|
let dir_to_light = -light.direction;
|
|
|
|
// convert from illuminance (lux) to candelas
|
|
//
|
|
// exposure is hard coded at the moment but should be replaced
|
|
// by values coming from the camera
|
|
// see: https://google.github.io/filament/Filament.html#imagingpipeline/physicallybasedcamera/exposuresettings
|
|
const APERTURE: f32 = 4.0;
|
|
const SHUTTER_SPEED: f32 = 1.0 / 250.0;
|
|
const SENSITIVITY: f32 = 100.0;
|
|
let ev100 =
|
|
f32::log2(APERTURE * APERTURE / SHUTTER_SPEED) - f32::log2(SENSITIVITY / 100.0);
|
|
let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2);
|
|
let intensity = light.illuminance * exposure;
|
|
|
|
// NOTE: A directional light seems to have to have an eye position on the line along the direction of the light
|
|
// through the world origin. I (Rob Swain) do not yet understand why it cannot be translated away from this.
|
|
let view = Mat4::look_at_rh(Vec3::ZERO, light.direction, Vec3::Y);
|
|
// NOTE: This orthographic projection defines the volume within which shadows from a directional light can be cast
|
|
let projection = light.projection;
|
|
|
|
gpu_lights.directional_lights[i] = GpuDirectionalLight {
|
|
// premultiply color by intensity
|
|
// we don't use the alpha at all, so no reason to multiply only [0..3]
|
|
color: (light.color.as_rgba_linear() * intensity).into(),
|
|
dir_to_light,
|
|
// NOTE: * view is correct, it should not be view.inverse() here
|
|
view_projection: projection * view,
|
|
shadow_depth_bias: light.shadow_depth_bias,
|
|
shadow_normal_bias: light.shadow_normal_bias,
|
|
};
|
|
|
|
let depth_texture_view =
|
|
directional_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: None,
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::D2),
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer: i as u32,
|
|
array_layer_count: NonZeroU32::new(1),
|
|
});
|
|
|
|
let view_light_entity = commands
|
|
.spawn()
|
|
.insert_bundle((
|
|
ViewLight {
|
|
depth_texture_view,
|
|
pass_name: format!("shadow pass directional light {}", i),
|
|
},
|
|
ExtractedView {
|
|
width: directional_light_shadow_map.size as u32,
|
|
height: directional_light_shadow_map.size as u32,
|
|
transform: GlobalTransform::from_matrix(view.inverse()),
|
|
projection,
|
|
},
|
|
RenderPhase::<Shadow>::default(),
|
|
))
|
|
.id();
|
|
view_lights.push(view_light_entity);
|
|
}
|
|
let point_light_depth_texture_view =
|
|
point_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: None,
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::CubeArray),
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer: 0,
|
|
array_layer_count: None,
|
|
});
|
|
let directional_light_depth_texture_view = directional_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: None,
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::D2Array),
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer: 0,
|
|
array_layer_count: None,
|
|
});
|
|
|
|
commands.entity(entity).insert(ViewLights {
|
|
point_light_depth_texture: point_light_depth_texture.texture,
|
|
point_light_depth_texture_view,
|
|
directional_light_depth_texture: directional_light_depth_texture.texture,
|
|
directional_light_depth_texture_view,
|
|
lights: view_lights,
|
|
gpu_light_binding_index: light_meta.view_gpu_lights.push(gpu_lights),
|
|
});
|
|
}
|
|
|
|
light_meta.view_gpu_lights.write_buffer(&render_queue);
|
|
}
|
|
|
|
pub fn queue_shadow_view_bind_group(
|
|
render_device: Res<RenderDevice>,
|
|
shadow_shaders: Res<ShadowShaders>,
|
|
mut light_meta: ResMut<LightMeta>,
|
|
view_uniforms: Res<ViewUniforms>,
|
|
) {
|
|
if let Some(view_binding) = view_uniforms.uniforms.binding() {
|
|
light_meta.shadow_view_bind_group =
|
|
Some(render_device.create_bind_group(&BindGroupDescriptor {
|
|
entries: &[BindGroupEntry {
|
|
binding: 0,
|
|
resource: view_binding,
|
|
}],
|
|
label: None,
|
|
layout: &shadow_shaders.view_layout,
|
|
}));
|
|
}
|
|
}
|
|
|
|
pub fn queue_shadows(
|
|
shadow_draw_functions: Res<DrawFunctions<Shadow>>,
|
|
casting_meshes: Query<Entity, (With<Handle<Mesh>>, Without<NotShadowCaster>)>,
|
|
mut view_lights: Query<&ViewLights>,
|
|
mut view_light_shadow_phases: Query<&mut RenderPhase<Shadow>>,
|
|
) {
|
|
for view_lights in view_lights.iter_mut() {
|
|
// ultimately lights should check meshes for relevancy (ex: light views can "see" different meshes than the main view can)
|
|
let draw_shadow_mesh = shadow_draw_functions
|
|
.read()
|
|
.get_id::<DrawShadowMesh>()
|
|
.unwrap();
|
|
for view_light_entity in view_lights.lights.iter().copied() {
|
|
let mut shadow_phase = view_light_shadow_phases.get_mut(view_light_entity).unwrap();
|
|
// TODO: this should only queue up meshes that are actually visible by each "light view"
|
|
for entity in casting_meshes.iter() {
|
|
shadow_phase.add(Shadow {
|
|
draw_function: draw_shadow_mesh,
|
|
entity,
|
|
distance: 0.0, // TODO: sort back-to-front
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Shadow {
|
|
pub distance: f32,
|
|
pub entity: Entity,
|
|
pub draw_function: DrawFunctionId,
|
|
}
|
|
|
|
impl PhaseItem for Shadow {
|
|
type SortKey = FloatOrd;
|
|
|
|
#[inline]
|
|
fn sort_key(&self) -> Self::SortKey {
|
|
FloatOrd(self.distance)
|
|
}
|
|
|
|
#[inline]
|
|
fn draw_function(&self) -> DrawFunctionId {
|
|
self.draw_function
|
|
}
|
|
}
|
|
|
|
pub struct ShadowPassNode {
|
|
main_view_query: QueryState<&'static ViewLights>,
|
|
view_light_query: QueryState<(&'static ViewLight, &'static RenderPhase<Shadow>)>,
|
|
}
|
|
|
|
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 RenderContext,
|
|
world: &World,
|
|
) -> Result<(), NodeRunError> {
|
|
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
|
|
if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) {
|
|
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 = RenderPassDescriptor {
|
|
label: Some(&view_light.pass_name),
|
|
color_attachments: &[],
|
|
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
|
|
view: &view_light.depth_texture_view,
|
|
depth_ops: Some(Operations {
|
|
load: LoadOp::Clear(0.0),
|
|
store: true,
|
|
}),
|
|
stencil_ops: None,
|
|
}),
|
|
};
|
|
|
|
let draw_functions = world.get_resource::<DrawFunctions<Shadow>>().unwrap();
|
|
|
|
let render_pass = render_context
|
|
.command_encoder
|
|
.begin_render_pass(&pass_descriptor);
|
|
let mut draw_functions = draw_functions.write();
|
|
let mut tracked_pass = TrackedRenderPass::new(render_pass);
|
|
for item in shadow_phase.items.iter() {
|
|
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
|
draw_function.draw(world, &mut tracked_pass, view_light_entity, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct DrawShadowMesh {
|
|
params: SystemState<(
|
|
SRes<ShadowShaders>,
|
|
SRes<LightMeta>,
|
|
SRes<TransformBindGroup>,
|
|
SRes<RenderAssets<Mesh>>,
|
|
SQuery<(Read<DynamicUniformIndex<MeshUniform>>, Read<Handle<Mesh>>)>,
|
|
SQuery<Read<ViewUniformOffset>>,
|
|
)>,
|
|
}
|
|
|
|
impl DrawShadowMesh {
|
|
pub fn new(world: &mut World) -> Self {
|
|
Self {
|
|
params: SystemState::new(world),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Draw<Shadow> for DrawShadowMesh {
|
|
fn draw<'w>(
|
|
&mut self,
|
|
world: &'w World,
|
|
pass: &mut TrackedRenderPass<'w>,
|
|
view: Entity,
|
|
item: &Shadow,
|
|
) {
|
|
let (shadow_shaders, light_meta, transform_bind_group, meshes, items, views) =
|
|
self.params.get(world);
|
|
let (transform_index, mesh_handle) = items.get(item.entity).unwrap();
|
|
let view_uniform_offset = views.get(view).unwrap();
|
|
pass.set_render_pipeline(&shadow_shaders.into_inner().pipeline);
|
|
pass.set_bind_group(
|
|
0,
|
|
light_meta
|
|
.into_inner()
|
|
.shadow_view_bind_group
|
|
.as_ref()
|
|
.unwrap(),
|
|
&[view_uniform_offset.offset],
|
|
);
|
|
|
|
pass.set_bind_group(
|
|
1,
|
|
&transform_bind_group.into_inner().value,
|
|
&[transform_index.index()],
|
|
);
|
|
|
|
let gpu_mesh = meshes.into_inner().get(mesh_handle).unwrap();
|
|
pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..));
|
|
if let Some(index_info) = &gpu_mesh.index_info {
|
|
pass.set_index_buffer(index_info.buffer.slice(..), 0, IndexFormat::Uint32);
|
|
pass.draw_indexed(0..index_info.count, 0, 0..1);
|
|
} else {
|
|
panic!("non-indexed drawing not supported yet")
|
|
}
|
|
}
|
|
}
|