Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_pbr/src/volumetric_fog/render.rs
6602 views
1
//! Rendering of fog volumes.
2
3
use core::array;
4
5
use bevy_asset::{load_embedded_asset, AssetId, AssetServer, Handle};
6
use bevy_camera::Camera3d;
7
use bevy_color::ColorToComponents as _;
8
use bevy_core_pipeline::prepass::{
9
DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass,
10
};
11
use bevy_derive::{Deref, DerefMut};
12
use bevy_ecs::{
13
component::Component,
14
entity::Entity,
15
query::{Has, QueryItem, With},
16
resource::Resource,
17
system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut},
18
world::World,
19
};
20
use bevy_image::{BevyDefault, Image};
21
use bevy_light::{FogVolume, VolumetricFog, VolumetricLight};
22
use bevy_math::{vec4, Affine3A, Mat4, Vec3, Vec3A, Vec4};
23
use bevy_mesh::{Mesh, MeshVertexBufferLayoutRef};
24
use bevy_render::{
25
diagnostic::RecordDiagnostics,
26
mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo},
27
render_asset::RenderAssets,
28
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
29
render_resource::{
30
binding_types::{
31
sampler, texture_3d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer,
32
},
33
BindGroupLayout, BindGroupLayoutEntries, BindingResource, BlendComponent, BlendFactor,
34
BlendOperation, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites,
35
DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, Operations,
36
PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,
37
RenderPipelineDescriptor, SamplerBindingType, ShaderStages, ShaderType,
38
SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat,
39
TextureSampleType, TextureUsages, VertexState,
40
},
41
renderer::{RenderContext, RenderDevice, RenderQueue},
42
sync_world::RenderEntity,
43
texture::GpuImage,
44
view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset},
45
Extract,
46
};
47
use bevy_shader::Shader;
48
use bevy_transform::components::GlobalTransform;
49
use bevy_utils::prelude::default;
50
use bitflags::bitflags;
51
52
use crate::{
53
MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup,
54
ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset,
55
ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset,
56
};
57
58
use super::FogAssets;
59
60
bitflags! {
61
/// Flags that describe the bind group layout used to render volumetric fog.
62
#[derive(Clone, Copy, PartialEq)]
63
struct VolumetricFogBindGroupLayoutKey: u8 {
64
/// The framebuffer is multisampled.
65
const MULTISAMPLED = 0x1;
66
/// The volumetric fog has a 3D voxel density texture.
67
const DENSITY_TEXTURE = 0x2;
68
}
69
}
70
71
bitflags! {
72
/// Flags that describe the rasterization pipeline used to render volumetric
73
/// fog.
74
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
75
struct VolumetricFogPipelineKeyFlags: u8 {
76
/// The view's color format has high dynamic range.
77
const HDR = 0x1;
78
/// The volumetric fog has a 3D voxel density texture.
79
const DENSITY_TEXTURE = 0x2;
80
}
81
}
82
83
/// The total number of bind group layouts.
84
///
85
/// This is the total number of combinations of all
86
/// [`VolumetricFogBindGroupLayoutKey`] flags.
87
const VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT: usize =
88
VolumetricFogBindGroupLayoutKey::all().bits() as usize + 1;
89
90
/// A matrix that converts from local 1×1×1 space to UVW 3D density texture
91
/// space.
92
static UVW_FROM_LOCAL: Mat4 = Mat4::from_cols(
93
vec4(1.0, 0.0, 0.0, 0.0),
94
vec4(0.0, 1.0, 0.0, 0.0),
95
vec4(0.0, 0.0, 1.0, 0.0),
96
vec4(0.5, 0.5, 0.5, 1.0),
97
);
98
99
/// The GPU pipeline for the volumetric fog postprocessing effect.
100
#[derive(Resource)]
101
pub struct VolumetricFogPipeline {
102
/// A reference to the shared set of mesh pipeline view layouts.
103
mesh_view_layouts: MeshPipelineViewLayouts,
104
105
/// All bind group layouts.
106
///
107
/// Since there aren't too many of these, we precompile them all.
108
volumetric_view_bind_group_layouts: [BindGroupLayout; VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT],
109
110
// The shader asset handle.
111
shader: Handle<Shader>,
112
}
113
114
/// The two render pipelines that we use for fog volumes: one for when a 3D
115
/// density texture is present and one for when it isn't.
116
#[derive(Component)]
117
pub struct ViewVolumetricFogPipelines {
118
/// The render pipeline that we use when no density texture is present, and
119
/// the density distribution is uniform.
120
pub textureless: CachedRenderPipelineId,
121
/// The render pipeline that we use when a density texture is present.
122
pub textured: CachedRenderPipelineId,
123
}
124
125
/// The node in the render graph, part of the postprocessing stack, that
126
/// implements volumetric fog.
127
#[derive(Default)]
128
pub struct VolumetricFogNode;
129
130
/// Identifies a single specialization of the volumetric fog shader.
131
#[derive(PartialEq, Eq, Hash, Clone)]
132
pub struct VolumetricFogPipelineKey {
133
/// The layout of the view, which is needed for the raymarching.
134
mesh_pipeline_view_key: MeshPipelineViewLayoutKey,
135
136
/// The vertex buffer layout of the primitive.
137
///
138
/// Both planes (used when the camera is inside the fog volume) and cubes
139
/// (used when the camera is outside the fog volume) use identical vertex
140
/// buffer layouts, so we only need one of them.
141
vertex_buffer_layout: MeshVertexBufferLayoutRef,
142
143
/// Flags that specify features on the pipeline key.
144
flags: VolumetricFogPipelineKeyFlags,
145
}
146
147
/// The same as [`VolumetricFog`] and [`FogVolume`], but formatted for
148
/// the GPU.
149
///
150
/// See the documentation of those structures for more information on these
151
/// fields.
152
#[derive(ShaderType)]
153
pub struct VolumetricFogUniform {
154
clip_from_local: Mat4,
155
156
/// The transform from world space to 3D density texture UVW space.
157
uvw_from_world: Mat4,
158
159
/// View-space plane equations of the far faces of the fog volume cuboid.
160
///
161
/// The vector takes the form V = (N, -N⋅Q), where N is the normal of the
162
/// plane and Q is any point in it, in view space. The equation of the plane
163
/// for homogeneous point P = (Px, Py, Pz, Pw) is V⋅P = 0.
164
far_planes: [Vec4; 3],
165
166
fog_color: Vec3,
167
light_tint: Vec3,
168
ambient_color: Vec3,
169
ambient_intensity: f32,
170
step_count: u32,
171
172
/// The radius of a sphere that bounds the fog volume in view space.
173
bounding_radius: f32,
174
175
absorption: f32,
176
scattering: f32,
177
density: f32,
178
density_texture_offset: Vec3,
179
scattering_asymmetry: f32,
180
light_intensity: f32,
181
jitter_strength: f32,
182
}
183
184
/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the
185
/// [`VolumetricFogUniform`] for a specific view.
186
#[derive(Component, Deref, DerefMut)]
187
pub struct ViewVolumetricFog(Vec<ViewFogVolume>);
188
189
/// Information that the render world needs to maintain about each fog volume.
190
pub struct ViewFogVolume {
191
/// The 3D voxel density texture for this volume, if present.
192
density_texture: Option<AssetId<Image>>,
193
/// The offset of this view's [`VolumetricFogUniform`] structure within the
194
/// [`VolumetricFogUniformBuffer`].
195
uniform_buffer_offset: u32,
196
/// True if the camera is outside the fog volume; false if it's inside the
197
/// fog volume.
198
exterior: bool,
199
}
200
201
/// The GPU buffer that stores the [`VolumetricFogUniform`] data.
202
#[derive(Resource, Default, Deref, DerefMut)]
203
pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer<VolumetricFogUniform>);
204
205
pub fn init_volumetric_fog_pipeline(
206
mut commands: Commands,
207
render_device: Res<RenderDevice>,
208
mesh_view_layouts: Res<MeshPipelineViewLayouts>,
209
asset_server: Res<AssetServer>,
210
) {
211
// Create the bind group layout entries common to all bind group
212
// layouts.
213
let base_bind_group_layout_entries = &BindGroupLayoutEntries::single(
214
ShaderStages::VERTEX_FRAGMENT,
215
// `volumetric_fog`
216
uniform_buffer::<VolumetricFogUniform>(true),
217
);
218
219
// For every combination of `VolumetricFogBindGroupLayoutKey` bits,
220
// create a bind group layout.
221
let bind_group_layouts = array::from_fn(|bits| {
222
let flags = VolumetricFogBindGroupLayoutKey::from_bits_retain(bits as u8);
223
224
let mut bind_group_layout_entries = base_bind_group_layout_entries.to_vec();
225
226
// `depth_texture`
227
bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices(
228
ShaderStages::FRAGMENT,
229
((
230
1,
231
if flags.contains(VolumetricFogBindGroupLayoutKey::MULTISAMPLED) {
232
texture_depth_2d_multisampled()
233
} else {
234
texture_depth_2d()
235
},
236
),),
237
));
238
239
// `density_texture` and `density_sampler`
240
if flags.contains(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE) {
241
bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices(
242
ShaderStages::FRAGMENT,
243
(
244
(2, texture_3d(TextureSampleType::Float { filterable: true })),
245
(3, sampler(SamplerBindingType::Filtering)),
246
),
247
));
248
}
249
250
// Create the bind group layout.
251
let description = flags.bind_group_layout_description();
252
render_device.create_bind_group_layout(&*description, &bind_group_layout_entries)
253
});
254
255
commands.insert_resource(VolumetricFogPipeline {
256
mesh_view_layouts: mesh_view_layouts.clone(),
257
volumetric_view_bind_group_layouts: bind_group_layouts,
258
shader: load_embedded_asset!(asset_server.as_ref(), "volumetric_fog.wgsl"),
259
});
260
}
261
262
/// Extracts [`VolumetricFog`], [`FogVolume`], and [`VolumetricLight`]s
263
/// from the main world to the render world.
264
pub fn extract_volumetric_fog(
265
mut commands: Commands,
266
view_targets: Extract<Query<(RenderEntity, &VolumetricFog)>>,
267
fog_volumes: Extract<Query<(RenderEntity, &FogVolume, &GlobalTransform)>>,
268
volumetric_lights: Extract<Query<(RenderEntity, &VolumetricLight)>>,
269
) {
270
if volumetric_lights.is_empty() {
271
// TODO: needs better way to handle clean up in render world
272
for (entity, ..) in view_targets.iter() {
273
commands
274
.entity(entity)
275
.remove::<(VolumetricFog, ViewVolumetricFogPipelines, ViewVolumetricFog)>();
276
}
277
for (entity, ..) in fog_volumes.iter() {
278
commands.entity(entity).remove::<FogVolume>();
279
}
280
return;
281
}
282
283
for (entity, volumetric_fog) in view_targets.iter() {
284
commands
285
.get_entity(entity)
286
.expect("Volumetric fog entity wasn't synced.")
287
.insert(*volumetric_fog);
288
}
289
290
for (entity, fog_volume, fog_transform) in fog_volumes.iter() {
291
commands
292
.get_entity(entity)
293
.expect("Fog volume entity wasn't synced.")
294
.insert((*fog_volume).clone())
295
.insert(*fog_transform);
296
}
297
298
for (entity, volumetric_light) in volumetric_lights.iter() {
299
commands
300
.get_entity(entity)
301
.expect("Volumetric light entity wasn't synced.")
302
.insert(*volumetric_light);
303
}
304
}
305
306
impl ViewNode for VolumetricFogNode {
307
type ViewQuery = (
308
Read<ViewTarget>,
309
Read<ViewDepthTexture>,
310
Read<ViewVolumetricFogPipelines>,
311
Read<ViewUniformOffset>,
312
Read<ViewLightsUniformOffset>,
313
Read<ViewFogUniformOffset>,
314
Read<ViewLightProbesUniformOffset>,
315
Read<ViewVolumetricFog>,
316
Read<MeshViewBindGroup>,
317
Read<ViewScreenSpaceReflectionsUniformOffset>,
318
Read<Msaa>,
319
Read<ViewEnvironmentMapUniformOffset>,
320
);
321
322
fn run<'w>(
323
&self,
324
_: &mut RenderGraphContext,
325
render_context: &mut RenderContext<'w>,
326
(
327
view_target,
328
view_depth_texture,
329
view_volumetric_lighting_pipelines,
330
view_uniform_offset,
331
view_lights_offset,
332
view_fog_offset,
333
view_light_probes_offset,
334
view_fog_volumes,
335
view_bind_group,
336
view_ssr_offset,
337
msaa,
338
view_environment_map_offset,
339
): QueryItem<'w, '_, Self::ViewQuery>,
340
world: &'w World,
341
) -> Result<(), NodeRunError> {
342
let pipeline_cache = world.resource::<PipelineCache>();
343
let volumetric_lighting_pipeline = world.resource::<VolumetricFogPipeline>();
344
let volumetric_lighting_uniform_buffers = world.resource::<VolumetricFogUniformBuffer>();
345
let image_assets = world.resource::<RenderAssets<GpuImage>>();
346
let mesh_allocator = world.resource::<MeshAllocator>();
347
348
// Fetch the uniform buffer and binding.
349
let (
350
Some(textureless_pipeline),
351
Some(textured_pipeline),
352
Some(volumetric_lighting_uniform_buffer_binding),
353
) = (
354
pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textureless),
355
pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textured),
356
volumetric_lighting_uniform_buffers.binding(),
357
)
358
else {
359
return Ok(());
360
};
361
362
let diagnostics = render_context.diagnostic_recorder();
363
render_context
364
.command_encoder()
365
.push_debug_group("volumetric_lighting");
366
let time_span =
367
diagnostics.time_span(render_context.command_encoder(), "volumetric_lighting");
368
369
let fog_assets = world.resource::<FogAssets>();
370
let render_meshes = world.resource::<RenderAssets<RenderMesh>>();
371
372
for view_fog_volume in view_fog_volumes.iter() {
373
// If the camera is outside the fog volume, pick the cube mesh;
374
// otherwise, pick the plane mesh. In the latter case we'll be
375
// effectively rendering a full-screen quad.
376
let mesh_handle = if view_fog_volume.exterior {
377
fog_assets.cube_mesh.clone()
378
} else {
379
fog_assets.plane_mesh.clone()
380
};
381
382
let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_handle.id())
383
else {
384
continue;
385
};
386
387
let density_image = view_fog_volume
388
.density_texture
389
.and_then(|density_texture| image_assets.get(density_texture));
390
391
// Pick the right pipeline, depending on whether a density texture
392
// is present or not.
393
let pipeline = if density_image.is_some() {
394
textured_pipeline
395
} else {
396
textureless_pipeline
397
};
398
399
// This should always succeed, but if the asset was unloaded don't
400
// panic.
401
let Some(render_mesh) = render_meshes.get(&mesh_handle) else {
402
return Ok(());
403
};
404
405
// Create the bind group for the view.
406
//
407
// TODO: Cache this.
408
409
let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty();
410
bind_group_layout_key.set(
411
VolumetricFogBindGroupLayoutKey::MULTISAMPLED,
412
!matches!(*msaa, Msaa::Off),
413
);
414
415
// Create the bind group entries. The ones relating to the density
416
// texture will only be filled in if that texture is present.
417
let mut bind_group_entries = DynamicBindGroupEntries::sequential((
418
volumetric_lighting_uniform_buffer_binding.clone(),
419
BindingResource::TextureView(view_depth_texture.view()),
420
));
421
if let Some(density_image) = density_image {
422
bind_group_layout_key.insert(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE);
423
bind_group_entries = bind_group_entries.extend_sequential((
424
BindingResource::TextureView(&density_image.texture_view),
425
BindingResource::Sampler(&density_image.sampler),
426
));
427
}
428
429
let volumetric_view_bind_group_layout = &volumetric_lighting_pipeline
430
.volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize];
431
432
let volumetric_view_bind_group = render_context.render_device().create_bind_group(
433
None,
434
volumetric_view_bind_group_layout,
435
&bind_group_entries,
436
);
437
438
let render_pass_descriptor = RenderPassDescriptor {
439
label: Some("volumetric lighting pass"),
440
color_attachments: &[Some(RenderPassColorAttachment {
441
view: view_target.main_texture_view(),
442
depth_slice: None,
443
resolve_target: None,
444
ops: Operations {
445
load: LoadOp::Load,
446
store: StoreOp::Store,
447
},
448
})],
449
depth_stencil_attachment: None,
450
timestamp_writes: None,
451
occlusion_query_set: None,
452
};
453
454
let mut render_pass = render_context
455
.command_encoder()
456
.begin_render_pass(&render_pass_descriptor);
457
458
render_pass.set_vertex_buffer(0, *vertex_buffer_slice.buffer.slice(..));
459
render_pass.set_pipeline(pipeline);
460
render_pass.set_bind_group(
461
0,
462
&view_bind_group.main,
463
&[
464
view_uniform_offset.offset,
465
view_lights_offset.offset,
466
view_fog_offset.offset,
467
**view_light_probes_offset,
468
**view_ssr_offset,
469
**view_environment_map_offset,
470
],
471
);
472
render_pass.set_bind_group(
473
1,
474
&volumetric_view_bind_group,
475
&[view_fog_volume.uniform_buffer_offset],
476
);
477
478
// Draw elements or arrays, as appropriate.
479
match &render_mesh.buffer_info {
480
RenderMeshBufferInfo::Indexed {
481
index_format,
482
count,
483
} => {
484
let Some(index_buffer_slice) =
485
mesh_allocator.mesh_index_slice(&mesh_handle.id())
486
else {
487
continue;
488
};
489
490
render_pass
491
.set_index_buffer(*index_buffer_slice.buffer.slice(..), *index_format);
492
render_pass.draw_indexed(
493
index_buffer_slice.range.start..(index_buffer_slice.range.start + count),
494
vertex_buffer_slice.range.start as i32,
495
0..1,
496
);
497
}
498
RenderMeshBufferInfo::NonIndexed => {
499
render_pass.draw(vertex_buffer_slice.range, 0..1);
500
}
501
}
502
}
503
504
time_span.end(render_context.command_encoder());
505
render_context.command_encoder().pop_debug_group();
506
507
Ok(())
508
}
509
}
510
511
impl SpecializedRenderPipeline for VolumetricFogPipeline {
512
type Key = VolumetricFogPipelineKey;
513
514
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
515
// We always use hardware 2x2 filtering for sampling the shadow map; the
516
// more accurate versions with percentage-closer filtering aren't worth
517
// the overhead.
518
let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()];
519
520
// We need a separate layout for MSAA and non-MSAA, as well as one for
521
// the presence or absence of the density texture.
522
let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty();
523
bind_group_layout_key.set(
524
VolumetricFogBindGroupLayoutKey::MULTISAMPLED,
525
key.mesh_pipeline_view_key
526
.contains(MeshPipelineViewLayoutKey::MULTISAMPLED),
527
);
528
bind_group_layout_key.set(
529
VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE,
530
key.flags
531
.contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE),
532
);
533
534
let volumetric_view_bind_group_layout =
535
self.volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize].clone();
536
537
// Both the cube and plane have the same vertex layout, so we don't need
538
// to distinguish between the two.
539
let vertex_format = key
540
.vertex_buffer_layout
541
.0
542
.get_layout(&[Mesh::ATTRIBUTE_POSITION.at_shader_location(0)])
543
.expect("Failed to get vertex layout for volumetric fog hull");
544
545
if key
546
.mesh_pipeline_view_key
547
.contains(MeshPipelineViewLayoutKey::MULTISAMPLED)
548
{
549
shader_defs.push("MULTISAMPLED".into());
550
}
551
552
if key
553
.flags
554
.contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE)
555
{
556
shader_defs.push("DENSITY_TEXTURE".into());
557
}
558
559
let layout = self
560
.mesh_view_layouts
561
.get_view_layout(key.mesh_pipeline_view_key);
562
let layout = vec![
563
layout.main_layout.clone(),
564
volumetric_view_bind_group_layout.clone(),
565
];
566
567
RenderPipelineDescriptor {
568
label: Some("volumetric lighting pipeline".into()),
569
layout,
570
vertex: VertexState {
571
shader: self.shader.clone(),
572
shader_defs: shader_defs.clone(),
573
buffers: vec![vertex_format],
574
..default()
575
},
576
primitive: PrimitiveState {
577
cull_mode: Some(Face::Back),
578
..default()
579
},
580
fragment: Some(FragmentState {
581
shader: self.shader.clone(),
582
shader_defs,
583
targets: vec![Some(ColorTargetState {
584
format: if key.flags.contains(VolumetricFogPipelineKeyFlags::HDR) {
585
ViewTarget::TEXTURE_FORMAT_HDR
586
} else {
587
TextureFormat::bevy_default()
588
},
589
// Blend on top of what's already in the framebuffer. Doing
590
// the alpha blending with the hardware blender allows us to
591
// avoid having to use intermediate render targets.
592
blend: Some(BlendState {
593
color: BlendComponent {
594
src_factor: BlendFactor::One,
595
dst_factor: BlendFactor::OneMinusSrcAlpha,
596
operation: BlendOperation::Add,
597
},
598
alpha: BlendComponent {
599
src_factor: BlendFactor::Zero,
600
dst_factor: BlendFactor::One,
601
operation: BlendOperation::Add,
602
},
603
}),
604
write_mask: ColorWrites::ALL,
605
})],
606
..default()
607
}),
608
..default()
609
}
610
}
611
}
612
613
/// Specializes volumetric fog pipelines for all views with that effect enabled.
614
pub fn prepare_volumetric_fog_pipelines(
615
mut commands: Commands,
616
pipeline_cache: Res<PipelineCache>,
617
mut pipelines: ResMut<SpecializedRenderPipelines<VolumetricFogPipeline>>,
618
volumetric_lighting_pipeline: Res<VolumetricFogPipeline>,
619
fog_assets: Res<FogAssets>,
620
view_targets: Query<
621
(
622
Entity,
623
&ExtractedView,
624
&Msaa,
625
Has<NormalPrepass>,
626
Has<DepthPrepass>,
627
Has<MotionVectorPrepass>,
628
Has<DeferredPrepass>,
629
),
630
With<VolumetricFog>,
631
>,
632
meshes: Res<RenderAssets<RenderMesh>>,
633
) {
634
let Some(plane_mesh) = meshes.get(&fog_assets.plane_mesh) else {
635
// There's an off chance that the mesh won't be prepared yet if `RenderAssetBytesPerFrame` limiting is in use.
636
return;
637
};
638
639
for (
640
entity,
641
view,
642
msaa,
643
normal_prepass,
644
depth_prepass,
645
motion_vector_prepass,
646
deferred_prepass,
647
) in view_targets.iter()
648
{
649
// Create a mesh pipeline view layout key corresponding to the view.
650
let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa);
651
mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass);
652
mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass);
653
mesh_pipeline_view_key.set(
654
MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS,
655
motion_vector_prepass,
656
);
657
mesh_pipeline_view_key.set(
658
MeshPipelineViewLayoutKey::DEFERRED_PREPASS,
659
deferred_prepass,
660
);
661
662
let mut textureless_flags = VolumetricFogPipelineKeyFlags::empty();
663
textureless_flags.set(VolumetricFogPipelineKeyFlags::HDR, view.hdr);
664
665
// Specialize the pipeline.
666
let textureless_pipeline_key = VolumetricFogPipelineKey {
667
mesh_pipeline_view_key,
668
vertex_buffer_layout: plane_mesh.layout.clone(),
669
flags: textureless_flags,
670
};
671
let textureless_pipeline_id = pipelines.specialize(
672
&pipeline_cache,
673
&volumetric_lighting_pipeline,
674
textureless_pipeline_key.clone(),
675
);
676
let textured_pipeline_id = pipelines.specialize(
677
&pipeline_cache,
678
&volumetric_lighting_pipeline,
679
VolumetricFogPipelineKey {
680
flags: textureless_pipeline_key.flags
681
| VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE,
682
..textureless_pipeline_key
683
},
684
);
685
686
commands.entity(entity).insert(ViewVolumetricFogPipelines {
687
textureless: textureless_pipeline_id,
688
textured: textured_pipeline_id,
689
});
690
}
691
}
692
693
/// A system that converts [`VolumetricFog`] into [`VolumetricFogUniform`]s.
694
pub fn prepare_volumetric_fog_uniforms(
695
mut commands: Commands,
696
mut volumetric_lighting_uniform_buffer: ResMut<VolumetricFogUniformBuffer>,
697
view_targets: Query<(Entity, &ExtractedView, &VolumetricFog)>,
698
fog_volumes: Query<(Entity, &FogVolume, &GlobalTransform)>,
699
render_device: Res<RenderDevice>,
700
render_queue: Res<RenderQueue>,
701
mut local_from_world_matrices: Local<Vec<Affine3A>>,
702
) {
703
// Do this up front to avoid O(n^2) matrix inversion.
704
local_from_world_matrices.clear();
705
for (_, _, fog_transform) in fog_volumes.iter() {
706
local_from_world_matrices.push(fog_transform.affine().inverse());
707
}
708
709
let uniform_count = view_targets.iter().len() * local_from_world_matrices.len();
710
711
let Some(mut writer) =
712
volumetric_lighting_uniform_buffer.get_writer(uniform_count, &render_device, &render_queue)
713
else {
714
return;
715
};
716
717
for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() {
718
let world_from_view = extracted_view.world_from_view.affine();
719
720
let mut view_fog_volumes = vec![];
721
722
for ((_, fog_volume, _), local_from_world) in
723
fog_volumes.iter().zip(local_from_world_matrices.iter())
724
{
725
// Calculate the transforms to and from 1×1×1 local space.
726
let local_from_view = *local_from_world * world_from_view;
727
let view_from_local = local_from_view.inverse();
728
729
// Determine whether the camera is inside or outside the volume, and
730
// calculate the clip space transform.
731
let interior = camera_is_inside_fog_volume(&local_from_view);
732
let hull_clip_from_local = calculate_fog_volume_clip_from_local_transforms(
733
interior,
734
&extracted_view.clip_from_view,
735
&view_from_local,
736
);
737
738
// Calculate the radius of the sphere that bounds the fog volume.
739
let bounding_radius = view_from_local
740
.transform_vector3a(Vec3A::splat(0.5))
741
.length();
742
743
// Write out our uniform.
744
let uniform_buffer_offset = writer.write(&VolumetricFogUniform {
745
clip_from_local: hull_clip_from_local,
746
uvw_from_world: UVW_FROM_LOCAL * *local_from_world,
747
far_planes: get_far_planes(&view_from_local),
748
fog_color: fog_volume.fog_color.to_linear().to_vec3(),
749
light_tint: fog_volume.light_tint.to_linear().to_vec3(),
750
ambient_color: volumetric_fog.ambient_color.to_linear().to_vec3(),
751
ambient_intensity: volumetric_fog.ambient_intensity,
752
step_count: volumetric_fog.step_count,
753
bounding_radius,
754
absorption: fog_volume.absorption,
755
scattering: fog_volume.scattering,
756
density: fog_volume.density_factor,
757
density_texture_offset: fog_volume.density_texture_offset,
758
scattering_asymmetry: fog_volume.scattering_asymmetry,
759
light_intensity: fog_volume.light_intensity,
760
jitter_strength: volumetric_fog.jitter,
761
});
762
763
view_fog_volumes.push(ViewFogVolume {
764
uniform_buffer_offset,
765
exterior: !interior,
766
density_texture: fog_volume.density_texture.as_ref().map(Handle::id),
767
});
768
}
769
770
commands
771
.entity(view_entity)
772
.insert(ViewVolumetricFog(view_fog_volumes));
773
}
774
}
775
776
/// A system that marks all view depth textures as readable in shaders.
777
///
778
/// The volumetric lighting pass needs to do this, and it doesn't happen by
779
/// default.
780
pub fn prepare_view_depth_textures_for_volumetric_fog(
781
mut view_targets: Query<&mut Camera3d>,
782
fog_volumes: Query<&VolumetricFog>,
783
) {
784
if fog_volumes.is_empty() {
785
return;
786
}
787
788
for mut camera in view_targets.iter_mut() {
789
camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
790
}
791
}
792
793
fn get_far_planes(view_from_local: &Affine3A) -> [Vec4; 3] {
794
let (mut far_planes, mut next_index) = ([Vec4::ZERO; 3], 0);
795
796
for &local_normal in &[
797
Vec3A::X,
798
Vec3A::NEG_X,
799
Vec3A::Y,
800
Vec3A::NEG_Y,
801
Vec3A::Z,
802
Vec3A::NEG_Z,
803
] {
804
let view_normal = view_from_local
805
.transform_vector3a(local_normal)
806
.normalize_or_zero();
807
if view_normal.z <= 0.0 {
808
continue;
809
}
810
811
let view_position = view_from_local.transform_point3a(-local_normal * 0.5);
812
let plane_coords = view_normal.extend(-view_normal.dot(view_position));
813
814
far_planes[next_index] = plane_coords;
815
next_index += 1;
816
if next_index == far_planes.len() {
817
continue;
818
}
819
}
820
821
far_planes
822
}
823
824
impl VolumetricFogBindGroupLayoutKey {
825
/// Creates an appropriate debug description for the bind group layout with
826
/// these flags.
827
fn bind_group_layout_description(&self) -> String {
828
if self.is_empty() {
829
return "volumetric lighting view bind group layout".to_owned();
830
}
831
832
format!(
833
"volumetric lighting view bind group layout ({})",
834
self.iter()
835
.filter_map(|flag| {
836
if flag == VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE {
837
Some("density texture")
838
} else if flag == VolumetricFogBindGroupLayoutKey::MULTISAMPLED {
839
Some("multisampled")
840
} else {
841
None
842
}
843
})
844
.collect::<Vec<_>>()
845
.join(", ")
846
)
847
}
848
}
849
850
/// Given the transform from the view to the 1×1×1 cube in local fog volume
851
/// space, returns true if the camera is inside the volume.
852
fn camera_is_inside_fog_volume(local_from_view: &Affine3A) -> bool {
853
local_from_view
854
.translation
855
.abs()
856
.cmple(Vec3A::splat(0.5))
857
.all()
858
}
859
860
/// Given the local transforms, returns the matrix that transforms model space
861
/// to clip space.
862
fn calculate_fog_volume_clip_from_local_transforms(
863
interior: bool,
864
clip_from_view: &Mat4,
865
view_from_local: &Affine3A,
866
) -> Mat4 {
867
if !interior {
868
return *clip_from_view * Mat4::from(*view_from_local);
869
}
870
871
// If the camera is inside the fog volume, then we'll be rendering a full
872
// screen quad. The shader will start its raymarch at the fragment depth
873
// value, however, so we need to make sure that the depth of the full screen
874
// quad is at the near clip plane `z_near`.
875
let z_near = clip_from_view.w_axis[2];
876
Mat4::from_cols(
877
vec4(z_near, 0.0, 0.0, 0.0),
878
vec4(0.0, z_near, 0.0, 0.0),
879
vec4(0.0, 0.0, 0.0, 0.0),
880
vec4(0.0, 0.0, z_near, z_near),
881
)
882
}
883
884