mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 15:14:50 +00:00
Allow mix of hdr and non-hdr cameras to same render target (#13419)
Changes: - Track whether an output texture has been written to yet and only clear it on the first write. - Use `ClearColorConfig` on `CameraOutputMode` instead of a raw `LoadOp`. - Track whether a output texture has been seen when specializing the upscaling pipeline and use alpha blending for extra cameras rendering to that texture that do not specify an explicit blend mode. Fixes #6754 ## Testing Tested against provided test case in issue: ![image](https://github.com/bevyengine/bevy/assets/10366310/d066f069-87fb-4249-a4d9-b6cb1751971b) --- ## Changelog - Allow cameras rendering to the same output texture with mixed hdr to work correctly. ## Migration Guide - - Change `CameraOutputMode` to use `ClearColorConfig` instead of `LoadOp`.
This commit is contained in:
parent
2d11d9a48d
commit
43204447e8
6 changed files with 191 additions and 116 deletions
|
@ -4,6 +4,7 @@ use bevy_ecs::prelude::*;
|
|||
use bevy_render::camera::{CameraOutputMode, ExtractedCamera};
|
||||
use bevy_render::view::ViewTarget;
|
||||
use bevy_render::{render_resource::*, Render, RenderApp, RenderSet};
|
||||
use bevy_utils::HashSet;
|
||||
|
||||
mod node;
|
||||
|
||||
|
@ -32,16 +33,32 @@ fn prepare_view_upscaling_pipelines(
|
|||
blit_pipeline: Res<BlitPipeline>,
|
||||
view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>,
|
||||
) {
|
||||
let mut output_textures = HashSet::new();
|
||||
for (entity, view_target, camera) in view_targets.iter() {
|
||||
let out_texture_id = view_target.out_texture().id();
|
||||
let blend_state = if let Some(ExtractedCamera {
|
||||
output_mode: CameraOutputMode::Write { blend_state, .. },
|
||||
..
|
||||
}) = camera
|
||||
{
|
||||
*blend_state
|
||||
match *blend_state {
|
||||
None => {
|
||||
// If we've already seen this output for a camera and it doesn't have a output blend
|
||||
// mode configured, default to alpha blend so that we don't accidentally overwrite
|
||||
// the output texture
|
||||
if output_textures.contains(&out_texture_id) {
|
||||
Some(BlendState::ALPHA_BLENDING)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => *blend_state,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
output_textures.insert(out_texture_id);
|
||||
|
||||
let key = BlitPipelineKey {
|
||||
texture_format: view_target.out_texture_format(),
|
||||
blend_state,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline};
|
||||
use bevy_ecs::{prelude::*, query::QueryItem};
|
||||
use bevy_render::camera::{ClearColor, ClearColorConfig};
|
||||
use bevy_render::{
|
||||
camera::{CameraOutputMode, ExtractedCamera},
|
||||
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
|
||||
render_resource::{
|
||||
BindGroup, BindGroupEntries, LoadOp, Operations, PipelineCache, RenderPassColorAttachment,
|
||||
RenderPassDescriptor, StoreOp, TextureViewId,
|
||||
BindGroup, BindGroupEntries, PipelineCache, RenderPassDescriptor, TextureViewId,
|
||||
},
|
||||
renderer::RenderContext,
|
||||
view::ViewTarget,
|
||||
|
@ -33,19 +33,22 @@ impl ViewNode for UpscalingNode {
|
|||
) -> Result<(), NodeRunError> {
|
||||
let pipeline_cache = world.get_resource::<PipelineCache>().unwrap();
|
||||
let blit_pipeline = world.get_resource::<BlitPipeline>().unwrap();
|
||||
let clear_color_global = world.get_resource::<ClearColor>().unwrap();
|
||||
|
||||
let color_attachment_load_op = if let Some(camera) = camera {
|
||||
let clear_color = if let Some(camera) = camera {
|
||||
match camera.output_mode {
|
||||
CameraOutputMode::Write {
|
||||
color_attachment_load_op,
|
||||
..
|
||||
} => color_attachment_load_op,
|
||||
CameraOutputMode::Write { clear_color, .. } => clear_color,
|
||||
CameraOutputMode::Skip => return Ok(()),
|
||||
}
|
||||
} else {
|
||||
LoadOp::Clear(Default::default())
|
||||
ClearColorConfig::Default
|
||||
};
|
||||
|
||||
let clear_color = match clear_color {
|
||||
ClearColorConfig::Default => Some(clear_color_global.0),
|
||||
ClearColorConfig::Custom(color) => Some(color),
|
||||
ClearColorConfig::None => None,
|
||||
};
|
||||
let converted_clear_color = clear_color.map(|color| color.into());
|
||||
let upscaled_texture = target.main_texture_view();
|
||||
|
||||
let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap();
|
||||
|
@ -69,14 +72,9 @@ impl ViewNode for UpscalingNode {
|
|||
|
||||
let pass_descriptor = RenderPassDescriptor {
|
||||
label: Some("upscaling_pass"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: target.out_texture(),
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: color_attachment_load_op,
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
color_attachments: &[Some(
|
||||
target.out_texture_color_attachment(converted_clear_color),
|
||||
)],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
|
|
|
@ -35,7 +35,7 @@ use bevy_window::{
|
|||
WindowScaleFactorChanged,
|
||||
};
|
||||
use std::ops::Range;
|
||||
use wgpu::{BlendState, LoadOp, TextureFormat, TextureUsages};
|
||||
use wgpu::{BlendState, TextureFormat, TextureUsages};
|
||||
|
||||
use super::{ClearColorConfig, Projection};
|
||||
|
||||
|
@ -488,9 +488,8 @@ pub enum CameraOutputMode {
|
|||
Write {
|
||||
/// The blend state that will be used by the pipeline that writes the intermediate render textures to the final render target texture.
|
||||
blend_state: Option<BlendState>,
|
||||
/// The color attachment load operation that will be used by the pipeline that writes the intermediate render textures to the final render
|
||||
/// target texture.
|
||||
color_attachment_load_op: LoadOp<wgpu::Color>,
|
||||
/// The clear color operation to perform on the final render target texture.
|
||||
clear_color: ClearColorConfig,
|
||||
},
|
||||
/// Skips writing the camera output to the configured render target. The output will remain in the
|
||||
/// Render Target's "intermediate" textures, which a camera with a higher order should write to the render target
|
||||
|
@ -505,7 +504,7 @@ impl Default for CameraOutputMode {
|
|||
fn default() -> Self {
|
||||
CameraOutputMode::Write {
|
||||
blend_state: None,
|
||||
color_attachment_load_op: LoadOp::Clear(Default::default()),
|
||||
clear_color: ClearColorConfig::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -824,6 +823,7 @@ pub struct ExtractedCamera {
|
|||
pub clear_color: ClearColorConfig,
|
||||
pub sorted_camera_index_for_target: usize,
|
||||
pub exposure: f32,
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
pub fn extract_cameras(
|
||||
|
@ -897,12 +897,13 @@ pub fn extract_cameras(
|
|||
order: camera.order,
|
||||
output_mode: camera.output_mode,
|
||||
msaa_writeback: camera.msaa_writeback,
|
||||
clear_color: camera.clear_color.clone(),
|
||||
clear_color: camera.clear_color,
|
||||
// this will be set in sort_cameras
|
||||
sorted_camera_index_for_target: 0,
|
||||
exposure: exposure
|
||||
.map(|e| e.exposure())
|
||||
.unwrap_or_else(|| Exposure::default().exposure()),
|
||||
hdr: camera.hdr,
|
||||
},
|
||||
ExtractedView {
|
||||
clip_from_view: camera.clip_from_view(),
|
||||
|
@ -954,6 +955,7 @@ pub struct SortedCamera {
|
|||
pub entity: Entity,
|
||||
pub order: isize,
|
||||
pub target: Option<NormalizedRenderTarget>,
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
pub fn sort_cameras(
|
||||
|
@ -966,6 +968,7 @@ pub fn sort_cameras(
|
|||
entity,
|
||||
order: camera.order,
|
||||
target: camera.target.clone(),
|
||||
hdr: camera.hdr,
|
||||
});
|
||||
}
|
||||
// sort by order and ensure within an order, RenderTargets of the same type are packed together
|
||||
|
@ -986,7 +989,9 @@ pub fn sort_cameras(
|
|||
}
|
||||
}
|
||||
if let Some(target) = &sorted_camera.target {
|
||||
let count = target_counts.entry(target.clone()).or_insert(0usize);
|
||||
let count = target_counts
|
||||
.entry((target.clone(), sorted_camera.hdr))
|
||||
.or_insert(0usize);
|
||||
let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap();
|
||||
camera.sorted_camera_index_for_target = *count;
|
||||
*count += 1;
|
||||
|
|
|
@ -6,7 +6,7 @@ use bevy_reflect::prelude::*;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// For a camera, specifies the color used to clear the viewport before rendering.
|
||||
#[derive(Reflect, Serialize, Deserialize, Clone, Debug, Default)]
|
||||
#[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default)]
|
||||
#[reflect(Serialize, Deserialize, Default)]
|
||||
pub enum ClearColorConfig {
|
||||
/// The clear color is taken from the world's [`ClearColor`] resource.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::CachedTexture;
|
||||
use crate::render_resource::TextureView;
|
||||
use crate::render_resource::{TextureFormat, TextureView};
|
||||
use bevy_color::LinearRgba;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
|
@ -120,3 +120,41 @@ impl DepthAttachment {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for a [`TextureView`] that is used as a [`RenderPassColorAttachment`] for a view
|
||||
/// target's final output texture.
|
||||
#[derive(Clone)]
|
||||
pub struct OutputColorAttachment {
|
||||
pub view: TextureView,
|
||||
pub format: TextureFormat,
|
||||
is_first_call: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl OutputColorAttachment {
|
||||
pub fn new(view: TextureView, format: TextureFormat) -> Self {
|
||||
Self {
|
||||
view,
|
||||
format,
|
||||
is_first_call: Arc::new(AtomicBool::new(true)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get this texture view as an attachment. The attachment will be cleared with a value of
|
||||
/// the provided `clear_color` if this is the first time calling this function, otherwise it
|
||||
/// will be loaded.
|
||||
pub fn get_attachment(&self, clear_color: Option<LinearRgba>) -> RenderPassColorAttachment {
|
||||
let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst);
|
||||
|
||||
RenderPassColorAttachment {
|
||||
view: &self.view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: match (clear_color, first_call) {
|
||||
(Some(clear_color), true) => LoadOp::Clear(clear_color.into()),
|
||||
(None, _) | (Some(_), false) => LoadOp::Load,
|
||||
},
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,13 @@ use crate::{
|
|||
render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
texture::{
|
||||
BevyDefault, CachedTexture, ColorAttachment, DepthAttachment, GpuImage, TextureCache,
|
||||
BevyDefault, CachedTexture, ColorAttachment, DepthAttachment, GpuImage,
|
||||
OutputColorAttachment, TextureCache,
|
||||
},
|
||||
Render, RenderApp, RenderSet,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_color::LinearRgba;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
|
@ -451,8 +453,7 @@ pub struct ViewTarget {
|
|||
/// 0 represents `main_textures.a`, 1 represents `main_textures.b`
|
||||
/// This is shared across view targets with the same render target
|
||||
main_texture: Arc<AtomicUsize>,
|
||||
out_texture: TextureView,
|
||||
out_texture_format: TextureFormat,
|
||||
out_texture: OutputColorAttachment,
|
||||
}
|
||||
|
||||
pub struct PostProcessWrite<'a> {
|
||||
|
@ -644,13 +645,20 @@ impl ViewTarget {
|
|||
/// The final texture this view will render to.
|
||||
#[inline]
|
||||
pub fn out_texture(&self) -> &TextureView {
|
||||
&self.out_texture
|
||||
&self.out_texture.view
|
||||
}
|
||||
|
||||
pub fn out_texture_color_attachment(
|
||||
&self,
|
||||
clear_color: Option<LinearRgba>,
|
||||
) -> RenderPassColorAttachment {
|
||||
self.out_texture.get_attachment(clear_color)
|
||||
}
|
||||
|
||||
/// The format of the final texture this view will render to
|
||||
#[inline]
|
||||
pub fn out_texture_format(&self) -> TextureFormat {
|
||||
self.out_texture_format
|
||||
self.out_texture.format
|
||||
}
|
||||
|
||||
/// This will start a new "post process write", which assumes that the caller
|
||||
|
@ -802,12 +810,24 @@ pub fn prepare_view_targets(
|
|||
manual_texture_views: Res<ManualTextureViews>,
|
||||
) {
|
||||
let mut textures = HashMap::default();
|
||||
let mut output_textures = HashMap::default();
|
||||
for (entity, camera, view, texture_usage) in cameras.iter() {
|
||||
if let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) {
|
||||
if let (Some(out_texture_view), Some(out_texture_format)) = (
|
||||
target.get_texture_view(&windows, &images, &manual_texture_views),
|
||||
target.get_texture_format(&windows, &images, &manual_texture_views),
|
||||
) {
|
||||
let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(out_texture) = output_textures.entry(target.clone()).or_insert_with(|| {
|
||||
target
|
||||
.get_texture_view(&windows, &images, &manual_texture_views)
|
||||
.zip(target.get_texture_format(&windows, &images, &manual_texture_views))
|
||||
.map(|(view, format)| {
|
||||
OutputColorAttachment::new(view.clone(), format.add_srgb_suffix())
|
||||
})
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let size = Extent3d {
|
||||
width: target_size.x,
|
||||
height: target_size.y,
|
||||
|
@ -891,10 +911,7 @@ pub fn prepare_view_targets(
|
|||
main_texture: main_textures.main_texture.clone(),
|
||||
main_textures,
|
||||
main_texture_format,
|
||||
out_texture: out_texture_view.clone(),
|
||||
out_texture_format: out_texture_format.add_srgb_suffix(),
|
||||
out_texture: out_texture.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue