Path: blob/main/crates/bevy_pbr/src/volumetric_fog/render.rs
6602 views
//! Rendering of fog volumes.12use core::array;34use bevy_asset::{load_embedded_asset, AssetId, AssetServer, Handle};5use bevy_camera::Camera3d;6use bevy_color::ColorToComponents as _;7use bevy_core_pipeline::prepass::{8DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass,9};10use bevy_derive::{Deref, DerefMut};11use bevy_ecs::{12component::Component,13entity::Entity,14query::{Has, QueryItem, With},15resource::Resource,16system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut},17world::World,18};19use bevy_image::{BevyDefault, Image};20use bevy_light::{FogVolume, VolumetricFog, VolumetricLight};21use bevy_math::{vec4, Affine3A, Mat4, Vec3, Vec3A, Vec4};22use bevy_mesh::{Mesh, MeshVertexBufferLayoutRef};23use bevy_render::{24diagnostic::RecordDiagnostics,25mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo},26render_asset::RenderAssets,27render_graph::{NodeRunError, RenderGraphContext, ViewNode},28render_resource::{29binding_types::{30sampler, texture_3d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer,31},32BindGroupLayout, BindGroupLayoutEntries, BindingResource, BlendComponent, BlendFactor,33BlendOperation, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites,34DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, Operations,35PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,36RenderPipelineDescriptor, SamplerBindingType, ShaderStages, ShaderType,37SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat,38TextureSampleType, TextureUsages, VertexState,39},40renderer::{RenderContext, RenderDevice, RenderQueue},41sync_world::RenderEntity,42texture::GpuImage,43view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset},44Extract,45};46use bevy_shader::Shader;47use bevy_transform::components::GlobalTransform;48use bevy_utils::prelude::default;49use bitflags::bitflags;5051use crate::{52MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup,53ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset,54ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset,55};5657use super::FogAssets;5859bitflags! {60/// Flags that describe the bind group layout used to render volumetric fog.61#[derive(Clone, Copy, PartialEq)]62struct VolumetricFogBindGroupLayoutKey: u8 {63/// The framebuffer is multisampled.64const MULTISAMPLED = 0x1;65/// The volumetric fog has a 3D voxel density texture.66const DENSITY_TEXTURE = 0x2;67}68}6970bitflags! {71/// Flags that describe the rasterization pipeline used to render volumetric72/// fog.73#[derive(Clone, Copy, PartialEq, Eq, Hash)]74struct VolumetricFogPipelineKeyFlags: u8 {75/// The view's color format has high dynamic range.76const HDR = 0x1;77/// The volumetric fog has a 3D voxel density texture.78const DENSITY_TEXTURE = 0x2;79}80}8182/// The total number of bind group layouts.83///84/// This is the total number of combinations of all85/// [`VolumetricFogBindGroupLayoutKey`] flags.86const VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT: usize =87VolumetricFogBindGroupLayoutKey::all().bits() as usize + 1;8889/// A matrix that converts from local 1×1×1 space to UVW 3D density texture90/// space.91static UVW_FROM_LOCAL: Mat4 = Mat4::from_cols(92vec4(1.0, 0.0, 0.0, 0.0),93vec4(0.0, 1.0, 0.0, 0.0),94vec4(0.0, 0.0, 1.0, 0.0),95vec4(0.5, 0.5, 0.5, 1.0),96);9798/// The GPU pipeline for the volumetric fog postprocessing effect.99#[derive(Resource)]100pub struct VolumetricFogPipeline {101/// A reference to the shared set of mesh pipeline view layouts.102mesh_view_layouts: MeshPipelineViewLayouts,103104/// All bind group layouts.105///106/// Since there aren't too many of these, we precompile them all.107volumetric_view_bind_group_layouts: [BindGroupLayout; VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT],108109// The shader asset handle.110shader: Handle<Shader>,111}112113/// The two render pipelines that we use for fog volumes: one for when a 3D114/// density texture is present and one for when it isn't.115#[derive(Component)]116pub struct ViewVolumetricFogPipelines {117/// The render pipeline that we use when no density texture is present, and118/// the density distribution is uniform.119pub textureless: CachedRenderPipelineId,120/// The render pipeline that we use when a density texture is present.121pub textured: CachedRenderPipelineId,122}123124/// The node in the render graph, part of the postprocessing stack, that125/// implements volumetric fog.126#[derive(Default)]127pub struct VolumetricFogNode;128129/// Identifies a single specialization of the volumetric fog shader.130#[derive(PartialEq, Eq, Hash, Clone)]131pub struct VolumetricFogPipelineKey {132/// The layout of the view, which is needed for the raymarching.133mesh_pipeline_view_key: MeshPipelineViewLayoutKey,134135/// The vertex buffer layout of the primitive.136///137/// Both planes (used when the camera is inside the fog volume) and cubes138/// (used when the camera is outside the fog volume) use identical vertex139/// buffer layouts, so we only need one of them.140vertex_buffer_layout: MeshVertexBufferLayoutRef,141142/// Flags that specify features on the pipeline key.143flags: VolumetricFogPipelineKeyFlags,144}145146/// The same as [`VolumetricFog`] and [`FogVolume`], but formatted for147/// the GPU.148///149/// See the documentation of those structures for more information on these150/// fields.151#[derive(ShaderType)]152pub struct VolumetricFogUniform {153clip_from_local: Mat4,154155/// The transform from world space to 3D density texture UVW space.156uvw_from_world: Mat4,157158/// View-space plane equations of the far faces of the fog volume cuboid.159///160/// The vector takes the form V = (N, -N⋅Q), where N is the normal of the161/// plane and Q is any point in it, in view space. The equation of the plane162/// for homogeneous point P = (Px, Py, Pz, Pw) is V⋅P = 0.163far_planes: [Vec4; 3],164165fog_color: Vec3,166light_tint: Vec3,167ambient_color: Vec3,168ambient_intensity: f32,169step_count: u32,170171/// The radius of a sphere that bounds the fog volume in view space.172bounding_radius: f32,173174absorption: f32,175scattering: f32,176density: f32,177density_texture_offset: Vec3,178scattering_asymmetry: f32,179light_intensity: f32,180jitter_strength: f32,181}182183/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the184/// [`VolumetricFogUniform`] for a specific view.185#[derive(Component, Deref, DerefMut)]186pub struct ViewVolumetricFog(Vec<ViewFogVolume>);187188/// Information that the render world needs to maintain about each fog volume.189pub struct ViewFogVolume {190/// The 3D voxel density texture for this volume, if present.191density_texture: Option<AssetId<Image>>,192/// The offset of this view's [`VolumetricFogUniform`] structure within the193/// [`VolumetricFogUniformBuffer`].194uniform_buffer_offset: u32,195/// True if the camera is outside the fog volume; false if it's inside the196/// fog volume.197exterior: bool,198}199200/// The GPU buffer that stores the [`VolumetricFogUniform`] data.201#[derive(Resource, Default, Deref, DerefMut)]202pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer<VolumetricFogUniform>);203204pub fn init_volumetric_fog_pipeline(205mut commands: Commands,206render_device: Res<RenderDevice>,207mesh_view_layouts: Res<MeshPipelineViewLayouts>,208asset_server: Res<AssetServer>,209) {210// Create the bind group layout entries common to all bind group211// layouts.212let base_bind_group_layout_entries = &BindGroupLayoutEntries::single(213ShaderStages::VERTEX_FRAGMENT,214// `volumetric_fog`215uniform_buffer::<VolumetricFogUniform>(true),216);217218// For every combination of `VolumetricFogBindGroupLayoutKey` bits,219// create a bind group layout.220let bind_group_layouts = array::from_fn(|bits| {221let flags = VolumetricFogBindGroupLayoutKey::from_bits_retain(bits as u8);222223let mut bind_group_layout_entries = base_bind_group_layout_entries.to_vec();224225// `depth_texture`226bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices(227ShaderStages::FRAGMENT,228((2291,230if flags.contains(VolumetricFogBindGroupLayoutKey::MULTISAMPLED) {231texture_depth_2d_multisampled()232} else {233texture_depth_2d()234},235),),236));237238// `density_texture` and `density_sampler`239if flags.contains(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE) {240bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices(241ShaderStages::FRAGMENT,242(243(2, texture_3d(TextureSampleType::Float { filterable: true })),244(3, sampler(SamplerBindingType::Filtering)),245),246));247}248249// Create the bind group layout.250let description = flags.bind_group_layout_description();251render_device.create_bind_group_layout(&*description, &bind_group_layout_entries)252});253254commands.insert_resource(VolumetricFogPipeline {255mesh_view_layouts: mesh_view_layouts.clone(),256volumetric_view_bind_group_layouts: bind_group_layouts,257shader: load_embedded_asset!(asset_server.as_ref(), "volumetric_fog.wgsl"),258});259}260261/// Extracts [`VolumetricFog`], [`FogVolume`], and [`VolumetricLight`]s262/// from the main world to the render world.263pub fn extract_volumetric_fog(264mut commands: Commands,265view_targets: Extract<Query<(RenderEntity, &VolumetricFog)>>,266fog_volumes: Extract<Query<(RenderEntity, &FogVolume, &GlobalTransform)>>,267volumetric_lights: Extract<Query<(RenderEntity, &VolumetricLight)>>,268) {269if volumetric_lights.is_empty() {270// TODO: needs better way to handle clean up in render world271for (entity, ..) in view_targets.iter() {272commands273.entity(entity)274.remove::<(VolumetricFog, ViewVolumetricFogPipelines, ViewVolumetricFog)>();275}276for (entity, ..) in fog_volumes.iter() {277commands.entity(entity).remove::<FogVolume>();278}279return;280}281282for (entity, volumetric_fog) in view_targets.iter() {283commands284.get_entity(entity)285.expect("Volumetric fog entity wasn't synced.")286.insert(*volumetric_fog);287}288289for (entity, fog_volume, fog_transform) in fog_volumes.iter() {290commands291.get_entity(entity)292.expect("Fog volume entity wasn't synced.")293.insert((*fog_volume).clone())294.insert(*fog_transform);295}296297for (entity, volumetric_light) in volumetric_lights.iter() {298commands299.get_entity(entity)300.expect("Volumetric light entity wasn't synced.")301.insert(*volumetric_light);302}303}304305impl ViewNode for VolumetricFogNode {306type ViewQuery = (307Read<ViewTarget>,308Read<ViewDepthTexture>,309Read<ViewVolumetricFogPipelines>,310Read<ViewUniformOffset>,311Read<ViewLightsUniformOffset>,312Read<ViewFogUniformOffset>,313Read<ViewLightProbesUniformOffset>,314Read<ViewVolumetricFog>,315Read<MeshViewBindGroup>,316Read<ViewScreenSpaceReflectionsUniformOffset>,317Read<Msaa>,318Read<ViewEnvironmentMapUniformOffset>,319);320321fn run<'w>(322&self,323_: &mut RenderGraphContext,324render_context: &mut RenderContext<'w>,325(326view_target,327view_depth_texture,328view_volumetric_lighting_pipelines,329view_uniform_offset,330view_lights_offset,331view_fog_offset,332view_light_probes_offset,333view_fog_volumes,334view_bind_group,335view_ssr_offset,336msaa,337view_environment_map_offset,338): QueryItem<'w, '_, Self::ViewQuery>,339world: &'w World,340) -> Result<(), NodeRunError> {341let pipeline_cache = world.resource::<PipelineCache>();342let volumetric_lighting_pipeline = world.resource::<VolumetricFogPipeline>();343let volumetric_lighting_uniform_buffers = world.resource::<VolumetricFogUniformBuffer>();344let image_assets = world.resource::<RenderAssets<GpuImage>>();345let mesh_allocator = world.resource::<MeshAllocator>();346347// Fetch the uniform buffer and binding.348let (349Some(textureless_pipeline),350Some(textured_pipeline),351Some(volumetric_lighting_uniform_buffer_binding),352) = (353pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textureless),354pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textured),355volumetric_lighting_uniform_buffers.binding(),356)357else {358return Ok(());359};360361let diagnostics = render_context.diagnostic_recorder();362render_context363.command_encoder()364.push_debug_group("volumetric_lighting");365let time_span =366diagnostics.time_span(render_context.command_encoder(), "volumetric_lighting");367368let fog_assets = world.resource::<FogAssets>();369let render_meshes = world.resource::<RenderAssets<RenderMesh>>();370371for view_fog_volume in view_fog_volumes.iter() {372// If the camera is outside the fog volume, pick the cube mesh;373// otherwise, pick the plane mesh. In the latter case we'll be374// effectively rendering a full-screen quad.375let mesh_handle = if view_fog_volume.exterior {376fog_assets.cube_mesh.clone()377} else {378fog_assets.plane_mesh.clone()379};380381let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_handle.id())382else {383continue;384};385386let density_image = view_fog_volume387.density_texture388.and_then(|density_texture| image_assets.get(density_texture));389390// Pick the right pipeline, depending on whether a density texture391// is present or not.392let pipeline = if density_image.is_some() {393textured_pipeline394} else {395textureless_pipeline396};397398// This should always succeed, but if the asset was unloaded don't399// panic.400let Some(render_mesh) = render_meshes.get(&mesh_handle) else {401return Ok(());402};403404// Create the bind group for the view.405//406// TODO: Cache this.407408let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty();409bind_group_layout_key.set(410VolumetricFogBindGroupLayoutKey::MULTISAMPLED,411!matches!(*msaa, Msaa::Off),412);413414// Create the bind group entries. The ones relating to the density415// texture will only be filled in if that texture is present.416let mut bind_group_entries = DynamicBindGroupEntries::sequential((417volumetric_lighting_uniform_buffer_binding.clone(),418BindingResource::TextureView(view_depth_texture.view()),419));420if let Some(density_image) = density_image {421bind_group_layout_key.insert(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE);422bind_group_entries = bind_group_entries.extend_sequential((423BindingResource::TextureView(&density_image.texture_view),424BindingResource::Sampler(&density_image.sampler),425));426}427428let volumetric_view_bind_group_layout = &volumetric_lighting_pipeline429.volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize];430431let volumetric_view_bind_group = render_context.render_device().create_bind_group(432None,433volumetric_view_bind_group_layout,434&bind_group_entries,435);436437let render_pass_descriptor = RenderPassDescriptor {438label: Some("volumetric lighting pass"),439color_attachments: &[Some(RenderPassColorAttachment {440view: view_target.main_texture_view(),441depth_slice: None,442resolve_target: None,443ops: Operations {444load: LoadOp::Load,445store: StoreOp::Store,446},447})],448depth_stencil_attachment: None,449timestamp_writes: None,450occlusion_query_set: None,451};452453let mut render_pass = render_context454.command_encoder()455.begin_render_pass(&render_pass_descriptor);456457render_pass.set_vertex_buffer(0, *vertex_buffer_slice.buffer.slice(..));458render_pass.set_pipeline(pipeline);459render_pass.set_bind_group(4600,461&view_bind_group.main,462&[463view_uniform_offset.offset,464view_lights_offset.offset,465view_fog_offset.offset,466**view_light_probes_offset,467**view_ssr_offset,468**view_environment_map_offset,469],470);471render_pass.set_bind_group(4721,473&volumetric_view_bind_group,474&[view_fog_volume.uniform_buffer_offset],475);476477// Draw elements or arrays, as appropriate.478match &render_mesh.buffer_info {479RenderMeshBufferInfo::Indexed {480index_format,481count,482} => {483let Some(index_buffer_slice) =484mesh_allocator.mesh_index_slice(&mesh_handle.id())485else {486continue;487};488489render_pass490.set_index_buffer(*index_buffer_slice.buffer.slice(..), *index_format);491render_pass.draw_indexed(492index_buffer_slice.range.start..(index_buffer_slice.range.start + count),493vertex_buffer_slice.range.start as i32,4940..1,495);496}497RenderMeshBufferInfo::NonIndexed => {498render_pass.draw(vertex_buffer_slice.range, 0..1);499}500}501}502503time_span.end(render_context.command_encoder());504render_context.command_encoder().pop_debug_group();505506Ok(())507}508}509510impl SpecializedRenderPipeline for VolumetricFogPipeline {511type Key = VolumetricFogPipelineKey;512513fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {514// We always use hardware 2x2 filtering for sampling the shadow map; the515// more accurate versions with percentage-closer filtering aren't worth516// the overhead.517let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()];518519// We need a separate layout for MSAA and non-MSAA, as well as one for520// the presence or absence of the density texture.521let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty();522bind_group_layout_key.set(523VolumetricFogBindGroupLayoutKey::MULTISAMPLED,524key.mesh_pipeline_view_key525.contains(MeshPipelineViewLayoutKey::MULTISAMPLED),526);527bind_group_layout_key.set(528VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE,529key.flags530.contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE),531);532533let volumetric_view_bind_group_layout =534self.volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize].clone();535536// Both the cube and plane have the same vertex layout, so we don't need537// to distinguish between the two.538let vertex_format = key539.vertex_buffer_layout540.0541.get_layout(&[Mesh::ATTRIBUTE_POSITION.at_shader_location(0)])542.expect("Failed to get vertex layout for volumetric fog hull");543544if key545.mesh_pipeline_view_key546.contains(MeshPipelineViewLayoutKey::MULTISAMPLED)547{548shader_defs.push("MULTISAMPLED".into());549}550551if key552.flags553.contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE)554{555shader_defs.push("DENSITY_TEXTURE".into());556}557558let layout = self559.mesh_view_layouts560.get_view_layout(key.mesh_pipeline_view_key);561let layout = vec![562layout.main_layout.clone(),563volumetric_view_bind_group_layout.clone(),564];565566RenderPipelineDescriptor {567label: Some("volumetric lighting pipeline".into()),568layout,569vertex: VertexState {570shader: self.shader.clone(),571shader_defs: shader_defs.clone(),572buffers: vec![vertex_format],573..default()574},575primitive: PrimitiveState {576cull_mode: Some(Face::Back),577..default()578},579fragment: Some(FragmentState {580shader: self.shader.clone(),581shader_defs,582targets: vec![Some(ColorTargetState {583format: if key.flags.contains(VolumetricFogPipelineKeyFlags::HDR) {584ViewTarget::TEXTURE_FORMAT_HDR585} else {586TextureFormat::bevy_default()587},588// Blend on top of what's already in the framebuffer. Doing589// the alpha blending with the hardware blender allows us to590// avoid having to use intermediate render targets.591blend: Some(BlendState {592color: BlendComponent {593src_factor: BlendFactor::One,594dst_factor: BlendFactor::OneMinusSrcAlpha,595operation: BlendOperation::Add,596},597alpha: BlendComponent {598src_factor: BlendFactor::Zero,599dst_factor: BlendFactor::One,600operation: BlendOperation::Add,601},602}),603write_mask: ColorWrites::ALL,604})],605..default()606}),607..default()608}609}610}611612/// Specializes volumetric fog pipelines for all views with that effect enabled.613pub fn prepare_volumetric_fog_pipelines(614mut commands: Commands,615pipeline_cache: Res<PipelineCache>,616mut pipelines: ResMut<SpecializedRenderPipelines<VolumetricFogPipeline>>,617volumetric_lighting_pipeline: Res<VolumetricFogPipeline>,618fog_assets: Res<FogAssets>,619view_targets: Query<620(621Entity,622&ExtractedView,623&Msaa,624Has<NormalPrepass>,625Has<DepthPrepass>,626Has<MotionVectorPrepass>,627Has<DeferredPrepass>,628),629With<VolumetricFog>,630>,631meshes: Res<RenderAssets<RenderMesh>>,632) {633let Some(plane_mesh) = meshes.get(&fog_assets.plane_mesh) else {634// There's an off chance that the mesh won't be prepared yet if `RenderAssetBytesPerFrame` limiting is in use.635return;636};637638for (639entity,640view,641msaa,642normal_prepass,643depth_prepass,644motion_vector_prepass,645deferred_prepass,646) in view_targets.iter()647{648// Create a mesh pipeline view layout key corresponding to the view.649let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa);650mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass);651mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass);652mesh_pipeline_view_key.set(653MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS,654motion_vector_prepass,655);656mesh_pipeline_view_key.set(657MeshPipelineViewLayoutKey::DEFERRED_PREPASS,658deferred_prepass,659);660661let mut textureless_flags = VolumetricFogPipelineKeyFlags::empty();662textureless_flags.set(VolumetricFogPipelineKeyFlags::HDR, view.hdr);663664// Specialize the pipeline.665let textureless_pipeline_key = VolumetricFogPipelineKey {666mesh_pipeline_view_key,667vertex_buffer_layout: plane_mesh.layout.clone(),668flags: textureless_flags,669};670let textureless_pipeline_id = pipelines.specialize(671&pipeline_cache,672&volumetric_lighting_pipeline,673textureless_pipeline_key.clone(),674);675let textured_pipeline_id = pipelines.specialize(676&pipeline_cache,677&volumetric_lighting_pipeline,678VolumetricFogPipelineKey {679flags: textureless_pipeline_key.flags680| VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE,681..textureless_pipeline_key682},683);684685commands.entity(entity).insert(ViewVolumetricFogPipelines {686textureless: textureless_pipeline_id,687textured: textured_pipeline_id,688});689}690}691692/// A system that converts [`VolumetricFog`] into [`VolumetricFogUniform`]s.693pub fn prepare_volumetric_fog_uniforms(694mut commands: Commands,695mut volumetric_lighting_uniform_buffer: ResMut<VolumetricFogUniformBuffer>,696view_targets: Query<(Entity, &ExtractedView, &VolumetricFog)>,697fog_volumes: Query<(Entity, &FogVolume, &GlobalTransform)>,698render_device: Res<RenderDevice>,699render_queue: Res<RenderQueue>,700mut local_from_world_matrices: Local<Vec<Affine3A>>,701) {702// Do this up front to avoid O(n^2) matrix inversion.703local_from_world_matrices.clear();704for (_, _, fog_transform) in fog_volumes.iter() {705local_from_world_matrices.push(fog_transform.affine().inverse());706}707708let uniform_count = view_targets.iter().len() * local_from_world_matrices.len();709710let Some(mut writer) =711volumetric_lighting_uniform_buffer.get_writer(uniform_count, &render_device, &render_queue)712else {713return;714};715716for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() {717let world_from_view = extracted_view.world_from_view.affine();718719let mut view_fog_volumes = vec![];720721for ((_, fog_volume, _), local_from_world) in722fog_volumes.iter().zip(local_from_world_matrices.iter())723{724// Calculate the transforms to and from 1×1×1 local space.725let local_from_view = *local_from_world * world_from_view;726let view_from_local = local_from_view.inverse();727728// Determine whether the camera is inside or outside the volume, and729// calculate the clip space transform.730let interior = camera_is_inside_fog_volume(&local_from_view);731let hull_clip_from_local = calculate_fog_volume_clip_from_local_transforms(732interior,733&extracted_view.clip_from_view,734&view_from_local,735);736737// Calculate the radius of the sphere that bounds the fog volume.738let bounding_radius = view_from_local739.transform_vector3a(Vec3A::splat(0.5))740.length();741742// Write out our uniform.743let uniform_buffer_offset = writer.write(&VolumetricFogUniform {744clip_from_local: hull_clip_from_local,745uvw_from_world: UVW_FROM_LOCAL * *local_from_world,746far_planes: get_far_planes(&view_from_local),747fog_color: fog_volume.fog_color.to_linear().to_vec3(),748light_tint: fog_volume.light_tint.to_linear().to_vec3(),749ambient_color: volumetric_fog.ambient_color.to_linear().to_vec3(),750ambient_intensity: volumetric_fog.ambient_intensity,751step_count: volumetric_fog.step_count,752bounding_radius,753absorption: fog_volume.absorption,754scattering: fog_volume.scattering,755density: fog_volume.density_factor,756density_texture_offset: fog_volume.density_texture_offset,757scattering_asymmetry: fog_volume.scattering_asymmetry,758light_intensity: fog_volume.light_intensity,759jitter_strength: volumetric_fog.jitter,760});761762view_fog_volumes.push(ViewFogVolume {763uniform_buffer_offset,764exterior: !interior,765density_texture: fog_volume.density_texture.as_ref().map(Handle::id),766});767}768769commands770.entity(view_entity)771.insert(ViewVolumetricFog(view_fog_volumes));772}773}774775/// A system that marks all view depth textures as readable in shaders.776///777/// The volumetric lighting pass needs to do this, and it doesn't happen by778/// default.779pub fn prepare_view_depth_textures_for_volumetric_fog(780mut view_targets: Query<&mut Camera3d>,781fog_volumes: Query<&VolumetricFog>,782) {783if fog_volumes.is_empty() {784return;785}786787for mut camera in view_targets.iter_mut() {788camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();789}790}791792fn get_far_planes(view_from_local: &Affine3A) -> [Vec4; 3] {793let (mut far_planes, mut next_index) = ([Vec4::ZERO; 3], 0);794795for &local_normal in &[796Vec3A::X,797Vec3A::NEG_X,798Vec3A::Y,799Vec3A::NEG_Y,800Vec3A::Z,801Vec3A::NEG_Z,802] {803let view_normal = view_from_local804.transform_vector3a(local_normal)805.normalize_or_zero();806if view_normal.z <= 0.0 {807continue;808}809810let view_position = view_from_local.transform_point3a(-local_normal * 0.5);811let plane_coords = view_normal.extend(-view_normal.dot(view_position));812813far_planes[next_index] = plane_coords;814next_index += 1;815if next_index == far_planes.len() {816continue;817}818}819820far_planes821}822823impl VolumetricFogBindGroupLayoutKey {824/// Creates an appropriate debug description for the bind group layout with825/// these flags.826fn bind_group_layout_description(&self) -> String {827if self.is_empty() {828return "volumetric lighting view bind group layout".to_owned();829}830831format!(832"volumetric lighting view bind group layout ({})",833self.iter()834.filter_map(|flag| {835if flag == VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE {836Some("density texture")837} else if flag == VolumetricFogBindGroupLayoutKey::MULTISAMPLED {838Some("multisampled")839} else {840None841}842})843.collect::<Vec<_>>()844.join(", ")845)846}847}848849/// Given the transform from the view to the 1×1×1 cube in local fog volume850/// space, returns true if the camera is inside the volume.851fn camera_is_inside_fog_volume(local_from_view: &Affine3A) -> bool {852local_from_view853.translation854.abs()855.cmple(Vec3A::splat(0.5))856.all()857}858859/// Given the local transforms, returns the matrix that transforms model space860/// to clip space.861fn calculate_fog_volume_clip_from_local_transforms(862interior: bool,863clip_from_view: &Mat4,864view_from_local: &Affine3A,865) -> Mat4 {866if !interior {867return *clip_from_view * Mat4::from(*view_from_local);868}869870// If the camera is inside the fog volume, then we'll be rendering a full871// screen quad. The shader will start its raymarch at the fragment depth872// value, however, so we need to make sure that the depth of the full screen873// quad is at the near clip plane `z_near`.874let z_near = clip_from_view.w_axis[2];875Mat4::from_cols(876vec4(z_near, 0.0, 0.0, 0.0),877vec4(0.0, z_near, 0.0, 0.0),878vec4(0.0, 0.0, 0.0, 0.0),879vec4(0.0, 0.0, z_near, z_near),880)881}882883884