Support scale factor for image render targets (#16796)

# Objective

I have something of a niche use case. I have a camera rendering pixel
art with a scale factor set, and another camera that renders to an
off-screen texture which is supposed to match the main camera exactly.
However, when computing camera target info, Bevy [hardcodes a scale
factor of
1.0](116c2b02fe/crates/bevy_render/src/camera/camera.rs (L828))
for image targets which means that my main camera and my image target
camera get different `OrthographicProjections` calculated.

## Solution

This PR adds an `ImageRenderTarget` struct which allows scale factors to
be specified.

## Testing

I tested the affected examples on macOS and they still work. This is an
additive change and should not break any existing code, apart from what
is trivially fixable by following compiler error messages.

---

## Migration Guide

`RenderTarget::Image` now takes an `ImageRenderTarget` instead of a
`Handle<Image>`. You can call `handle.into()` to construct an
`ImageRenderTarget` using the same settings as before.
This commit is contained in:
Martin Svanberg 2024-12-17 21:21:40 +01:00 committed by GitHub
parent d51dee627f
commit 39f9e07b5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 47 additions and 22 deletions

View File

@ -28,7 +28,7 @@ use bevy_ecs::{
world::DeferredWorld,
};
use bevy_image::Image;
use bevy_math::{ops, vec2, Dir3, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3};
use bevy_math::{ops, vec2, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3};
use bevy_reflect::prelude::*;
use bevy_render_macros::ExtractComponent;
use bevy_transform::components::{GlobalTransform, Transform};
@ -718,12 +718,37 @@ pub enum RenderTarget {
/// Window to which the camera's view is rendered.
Window(WindowRef),
/// Image to which the camera's view is rendered.
Image(Handle<Image>),
Image(ImageRenderTarget),
/// Texture View to which the camera's view is rendered.
/// Useful when the texture view needs to be created outside of Bevy, for example OpenXR.
TextureView(ManualTextureViewHandle),
}
/// A render target that renders to an [`Image`].
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ImageRenderTarget {
/// The image to render to.
pub handle: Handle<Image>,
/// The scale factor of the render target image, corresponding to the scale
/// factor for a window target. This should almost always be 1.0.
pub scale_factor: FloatOrd,
}
impl From<Handle<Image>> for RenderTarget {
fn from(handle: Handle<Image>) -> Self {
Self::Image(handle.into())
}
}
impl From<Handle<Image>> for ImageRenderTarget {
fn from(handle: Handle<Image>) -> Self {
Self {
handle,
scale_factor: FloatOrd(1.0),
}
}
}
impl Default for RenderTarget {
fn default() -> Self {
Self::Window(Default::default())
@ -738,7 +763,7 @@ pub enum NormalizedRenderTarget {
/// Window to which the camera's view is rendered.
Window(NormalizedWindowRef),
/// Image to which the camera's view is rendered.
Image(Handle<Image>),
Image(ImageRenderTarget),
/// Texture View to which the camera's view is rendered.
/// Useful when the texture view needs to be created outside of Bevy, for example OpenXR.
TextureView(ManualTextureViewHandle),
@ -759,8 +784,8 @@ impl RenderTarget {
/// Get a handle to the render target's image,
/// or `None` if the render target is another variant.
pub fn as_image(&self) -> Option<&Handle<Image>> {
if let Self::Image(handle) = self {
Some(handle)
if let Self::Image(image_target) = self {
Some(&image_target.handle)
} else {
None
}
@ -778,9 +803,9 @@ impl NormalizedRenderTarget {
NormalizedRenderTarget::Window(window_ref) => windows
.get(&window_ref.entity())
.and_then(|window| window.swap_chain_texture_view.as_ref()),
NormalizedRenderTarget::Image(image_handle) => {
images.get(image_handle).map(|image| &image.texture_view)
}
NormalizedRenderTarget::Image(image_target) => images
.get(&image_target.handle)
.map(|image| &image.texture_view),
NormalizedRenderTarget::TextureView(id) => {
manual_texture_views.get(id).map(|tex| &tex.texture_view)
}
@ -798,9 +823,9 @@ impl NormalizedRenderTarget {
NormalizedRenderTarget::Window(window_ref) => windows
.get(&window_ref.entity())
.and_then(|window| window.swap_chain_texture_format),
NormalizedRenderTarget::Image(image_handle) => {
images.get(image_handle).map(|image| image.texture_format)
}
NormalizedRenderTarget::Image(image_target) => images
.get(&image_target.handle)
.map(|image| image.texture_format),
NormalizedRenderTarget::TextureView(id) => {
manual_texture_views.get(id).map(|tex| tex.format)
}
@ -821,11 +846,11 @@ impl NormalizedRenderTarget {
physical_size: window.physical_size(),
scale_factor: window.resolution.scale_factor(),
}),
NormalizedRenderTarget::Image(image_handle) => {
let image = images.get(image_handle)?;
NormalizedRenderTarget::Image(image_target) => {
let image = images.get(&image_target.handle)?;
Some(RenderTargetInfo {
physical_size: image.size(),
scale_factor: 1.0,
scale_factor: image_target.scale_factor.0,
})
}
NormalizedRenderTarget::TextureView(id) => {
@ -847,8 +872,8 @@ impl NormalizedRenderTarget {
NormalizedRenderTarget::Window(window_ref) => {
changed_window_ids.contains(&window_ref.entity())
}
NormalizedRenderTarget::Image(image_handle) => {
changed_image_handles.contains(&image_handle.id())
NormalizedRenderTarget::Image(image_target) => {
changed_image_handles.contains(&image_target.handle.id())
}
NormalizedRenderTarget::TextureView(_) => true,
}

View File

@ -95,7 +95,7 @@ impl Screenshot {
/// Capture a screenshot of the provided render target image.
pub fn image(image: Handle<Image>) -> Self {
Self(RenderTarget::Image(image))
Self(RenderTarget::Image(image.into()))
}
/// Capture a screenshot of the provided manual texture view.
@ -297,7 +297,7 @@ fn prepare_screenshots(
);
}
NormalizedRenderTarget::Image(image) => {
let Some(gpu_image) = images.get(image) else {
let Some(gpu_image) = images.get(&image.handle) else {
warn!("Unknown image for screenshot, skipping: {:?}", image);
continue;
};
@ -533,7 +533,7 @@ pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEnc
);
}
NormalizedRenderTarget::Image(image) => {
let Some(gpu_image) = gpu_images.get(image) else {
let Some(gpu_image) = gpu_images.get(&image.handle) else {
warn!("Unknown image for screenshot, skipping: {:?}", image);
continue;
};

View File

@ -117,7 +117,7 @@ fn setup_camera(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
Camera {
// render before the "main pass" camera
order: -1,
target: RenderTarget::Image(image_handle.clone()),
target: RenderTarget::Image(image_handle.clone().into()),
..default()
},
Msaa::Off,

View File

@ -268,7 +268,7 @@ fn setup_render_target(
scene_controller.state = SceneState::Render(pre_roll_frames);
scene_controller.name = scene_name;
RenderTarget::Image(render_target_image_handle)
RenderTarget::Image(render_target_image_handle.into())
}
/// Setups image saver

View File

@ -57,7 +57,7 @@ fn setup(
.spawn((
Camera2d,
Camera {
target: RenderTarget::Image(image_handle.clone()),
target: RenderTarget::Image(image_handle.clone().into()),
..default()
},
))