From a0b90cd6189996cbfa62a533f1308e3ac649f929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 27 Jun 2025 09:30:54 -0700 Subject: [PATCH] Resolution override (#19817) Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> Co-authored-by: atlv --- .../src/core_3d/main_opaque_pass_3d_node.rs | 6 +++-- .../core_3d/main_transmissive_pass_3d_node.rs | 9 ++++--- .../core_3d/main_transparent_pass_3d_node.rs | 7 +++--- .../bevy_core_pipeline/src/deferred/node.rs | 8 ++++--- .../src/oit/resolve/node.rs | 7 +++--- crates/bevy_core_pipeline/src/prepass/node.rs | 8 ++++--- .../src/meshlet/material_shade_nodes.rs | 14 +++++++---- crates/bevy_render/src/camera/camera.rs | 24 +++++++++++++++++++ crates/bevy_render/src/camera/mod.rs | 1 + examples/shader/custom_render_phase.rs | 7 +++--- 10 files changed, 67 insertions(+), 24 deletions(-) diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index b19268ac1f..c5ee7a798d 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, @@ -31,6 +31,7 @@ impl ViewNode for MainOpaquePass3dNode { Option<&'static SkyboxPipelineId>, Option<&'static SkyboxBindGroup>, &'static ViewUniformOffset, + Option<&'static MainPassResolutionOverride>, ); fn run<'w>( @@ -45,6 +46,7 @@ impl ViewNode for MainOpaquePass3dNode { skybox_pipeline, skybox_bind_group, view_uniform_offset, + resolution_override, ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { @@ -90,7 +92,7 @@ impl ViewNode for MainOpaquePass3dNode { let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d"); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs index 8c656171e7..393167227f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs @@ -3,7 +3,7 @@ use crate::core_3d::Transmissive3d; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_image::ToExtents; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, render_resource::{RenderPassDescriptor, StoreOp}, @@ -28,13 +28,16 @@ impl ViewNode for MainTransmissivePass3dNode { &'static ViewTarget, Option<&'static ViewTransmissionTexture>, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view, camera_3d, target, transmission, depth): QueryItem, + (camera, view, camera_3d, target, transmission, depth, resolution_override): QueryItem< + Self::ViewQuery, + >, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -108,7 +111,7 @@ impl ViewNode for MainTransmissivePass3dNode { render_context.begin_tracked_render_pass(render_pass_descriptor); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) { diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs index 36fe8417c4..0c70ec23a0 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs @@ -1,7 +1,7 @@ use crate::core_3d::Transparent3d; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, @@ -24,12 +24,13 @@ impl ViewNode for MainTransparentPass3dNode { &'static ExtractedView, &'static ViewTarget, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view, target, depth): QueryItem, + (camera, view, target, depth, resolution_override): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -69,7 +70,7 @@ impl ViewNode for MainTransparentPass3dNode { let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d"); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) { diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index e786d2a222..ab87fccee6 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -1,4 +1,5 @@ use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_render::camera::MainPassResolutionOverride; use bevy_render::experimental::occlusion_culling::OcclusionCulling; use bevy_render::render_graph::ViewNode; @@ -66,6 +67,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { &'static ExtractedView, &'static ViewDepthTexture, &'static ViewPrepassTextures, + Option<&'static MainPassResolutionOverride>, Has, Has, ); @@ -77,7 +79,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { - let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query; + let (.., occlusion_culling, no_indirect_drawing) = view_query; if !occlusion_culling || no_indirect_drawing { return Ok(()); } @@ -105,7 +107,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { fn run_deferred_prepass<'w>( graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem< + (camera, extracted_view, view_depth_texture, view_prepass_textures, resolution_override, _, _): QueryItem< 'w, '_, ::ViewQuery, @@ -220,7 +222,7 @@ fn run_deferred_prepass<'w>( }); let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_core_pipeline/src/oit/resolve/node.rs b/crates/bevy_core_pipeline/src/oit/resolve/node.rs index 14d42235f1..77352e5ecb 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/node.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/node.rs @@ -1,6 +1,6 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode}, render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor}, renderer::RenderContext, @@ -23,13 +23,14 @@ impl ViewNode for OitResolveNode { &'static ViewUniformOffset, &'static OitResolvePipelineId, &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, ); fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view_target, view_uniform, oit_resolve_pipeline_id, depth): QueryItem< + (camera, view_target, view_uniform, oit_resolve_pipeline_id, depth, resolution_override): QueryItem< Self::ViewQuery, >, world: &World, @@ -63,7 +64,7 @@ impl ViewNode for OitResolveNode { }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } render_pass.set_render_pipeline(pipeline); diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 500cc0a42b..6193aa4f0e 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -1,6 +1,6 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, diagnostic::RecordDiagnostics, experimental::occlusion_culling::OcclusionCulling, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, @@ -64,6 +64,7 @@ impl ViewNode for LatePrepassNode { Option<&'static RenderSkyboxPrepassPipeline>, Option<&'static SkyboxPrepassBindGroup>, Option<&'static PreviousViewUniformOffset>, + Option<&'static MainPassResolutionOverride>, Has, Has, Has, @@ -78,7 +79,7 @@ impl ViewNode for LatePrepassNode { ) -> Result<(), NodeRunError> { // We only need a late prepass if we have occlusion culling and indirect // drawing. - let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query; + let (.., occlusion_culling, no_indirect_drawing, _) = query; if !occlusion_culling || no_indirect_drawing { return Ok(()); } @@ -109,6 +110,7 @@ fn run_prepass<'w>( skybox_prepass_pipeline, skybox_prepass_bind_group, view_prev_uniform_offset, + resolution_override, _, _, has_deferred, @@ -183,7 +185,7 @@ fn run_prepass<'w>( let pass_span = diagnostics.pass_span(&mut render_pass, label); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Opaque draws diff --git a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs index cb05de38fb..9791ef2b02 100644 --- a/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs +++ b/crates/bevy_pbr/src/meshlet/material_shade_nodes.rs @@ -18,7 +18,7 @@ use bevy_ecs::{ world::World, }; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_resource::{ LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, @@ -42,6 +42,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { &'static ViewLightProbesUniformOffset, &'static ViewScreenSpaceReflectionsUniformOffset, &'static ViewEnvironmentMapUniformOffset, + Option<&'static MainPassResolutionOverride>, &'static MeshletViewMaterialsMainOpaquePass, &'static MeshletViewBindGroups, &'static MeshletViewResources, @@ -61,6 +62,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { view_light_probes_offset, view_ssr_offset, view_environment_map_offset, + resolution_override, meshlet_view_materials, meshlet_view_bind_groups, meshlet_view_resources, @@ -101,7 +103,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } render_pass.set_bind_group( @@ -147,6 +149,7 @@ impl ViewNode for MeshletPrepassNode { &'static ViewPrepassTextures, &'static ViewUniformOffset, &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, Has, &'static MeshletViewMaterialsPrepass, &'static MeshletViewBindGroups, @@ -162,6 +165,7 @@ impl ViewNode for MeshletPrepassNode { view_prepass_textures, view_uniform_offset, previous_view_uniform_offset, + resolution_override, view_has_motion_vector_prepass, meshlet_view_materials, meshlet_view_bind_groups, @@ -219,7 +223,7 @@ impl ViewNode for MeshletPrepassNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if view_has_motion_vector_prepass { @@ -270,6 +274,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { &'static ViewPrepassTextures, &'static ViewUniformOffset, &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, Has, &'static MeshletViewMaterialsDeferredGBufferPrepass, &'static MeshletViewBindGroups, @@ -285,6 +290,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { view_prepass_textures, view_uniform_offset, previous_view_uniform_offset, + resolution_override, view_has_motion_vector_prepass, meshlet_view_materials, meshlet_view_bind_groups, @@ -347,7 +353,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { occlusion_query_set: None, }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } if view_has_motion_vector_prepass { diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 2732a44316..20ec3f9c9f 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -111,6 +111,17 @@ impl Viewport { } } } + + pub fn with_override( + &self, + main_pass_resolution_override: Option<&MainPassResolutionOverride>, + ) -> Self { + let mut viewport = self.clone(); + if let Some(override_size) = main_pass_resolution_override { + viewport.physical_size = **override_size; + } + viewport + } } /// Settings to define a camera sub view. @@ -1366,6 +1377,19 @@ impl TemporalJitter { #[reflect(Default, Component)] pub struct MipBias(pub f32); +/// Override the resolution a 3d camera's main pass is rendered at. +/// +/// Does not affect post processing. +/// +/// ## Usage +/// +/// * Insert this component on a 3d camera entity in the render world. +/// * The resolution override must be smaller than the camera's viewport size. +/// * The resolution override is specified in physical pixels. +#[derive(Component, Reflect, Deref)] +#[reflect(Component)] +pub struct MainPassResolutionOverride(pub UVec2); + impl Default for MipBias { fn default() -> Self { Self(-1.0) diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index a2470a7660..1b2a3bdfd3 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -29,6 +29,7 @@ impl Plugin for CameraPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .init_resource::() .init_resource::() .add_plugins(( diff --git a/examples/shader/custom_render_phase.rs b/examples/shader/custom_render_phase.rs index 309954bbe3..30ef057429 100644 --- a/examples/shader/custom_render_phase.rs +++ b/examples/shader/custom_render_phase.rs @@ -34,7 +34,7 @@ use bevy::{ }, GetBatchData, GetFullBatchData, }, - camera::ExtractedCamera, + camera::{ExtractedCamera, MainPassResolutionOverride}, extract_component::{ExtractComponent, ExtractComponentPlugin}, mesh::{allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::RenderAssets, @@ -589,13 +589,14 @@ impl ViewNode for CustomDrawNode { &'static ExtractedCamera, &'static ExtractedView, &'static ViewTarget, + Option<&'static MainPassResolutionOverride>, ); fn run<'w>( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target): QueryItem<'w, '_, Self::ViewQuery>, + (camera, view, target, resolution_override): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // First, we need to get our phases resource @@ -625,7 +626,7 @@ impl ViewNode for CustomDrawNode { }); if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + render_pass.set_camera_viewport(&viewport.with_override(resolution_override)); } // Render the phase