Resolution override (#19817)

Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
Co-authored-by: atlv <email@atlasdostal.com>
This commit is contained in:
charlotte 🌸 2025-06-27 09:30:54 -07:00 committed by GitHub
parent 410ca48023
commit a0b90cd618
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 67 additions and 24 deletions

View File

@ -4,7 +4,7 @@ use crate::{
}; };
use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_ecs::{prelude::World, query::QueryItem};
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::{ExtractedCamera, MainPassResolutionOverride},
diagnostic::RecordDiagnostics, diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
@ -31,6 +31,7 @@ impl ViewNode for MainOpaquePass3dNode {
Option<&'static SkyboxPipelineId>, Option<&'static SkyboxPipelineId>,
Option<&'static SkyboxBindGroup>, Option<&'static SkyboxBindGroup>,
&'static ViewUniformOffset, &'static ViewUniformOffset,
Option<&'static MainPassResolutionOverride>,
); );
fn run<'w>( fn run<'w>(
@ -45,6 +46,7 @@ impl ViewNode for MainOpaquePass3dNode {
skybox_pipeline, skybox_pipeline,
skybox_bind_group, skybox_bind_group,
view_uniform_offset, view_uniform_offset,
resolution_override,
): QueryItem<'w, '_, Self::ViewQuery>, ): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World, world: &'w World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
@ -90,7 +92,7 @@ impl ViewNode for MainOpaquePass3dNode {
let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d"); let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d");
if let Some(viewport) = camera.viewport.as_ref() { 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 // Opaque draws

View File

@ -3,7 +3,7 @@ use crate::core_3d::Transmissive3d;
use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_image::ToExtents; use bevy_image::ToExtents;
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::{ExtractedCamera, MainPassResolutionOverride},
render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases, render_phase::ViewSortedRenderPhases,
render_resource::{RenderPassDescriptor, StoreOp}, render_resource::{RenderPassDescriptor, StoreOp},
@ -28,13 +28,16 @@ impl ViewNode for MainTransmissivePass3dNode {
&'static ViewTarget, &'static ViewTarget,
Option<&'static ViewTransmissionTexture>, Option<&'static ViewTransmissionTexture>,
&'static ViewDepthTexture, &'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
); );
fn run( fn run(
&self, &self,
graph: &mut RenderGraphContext, graph: &mut RenderGraphContext,
render_context: &mut RenderContext, render_context: &mut RenderContext,
(camera, view, camera_3d, target, transmission, depth): QueryItem<Self::ViewQuery>, (camera, view, camera_3d, target, transmission, depth, resolution_override): QueryItem<
Self::ViewQuery,
>,
world: &World, world: &World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity(); let view_entity = graph.view_entity();
@ -108,7 +111,7 @@ impl ViewNode for MainTransmissivePass3dNode {
render_context.begin_tracked_render_pass(render_pass_descriptor); render_context.begin_tracked_render_pass(render_pass_descriptor);
if let Some(viewport) = camera.viewport.as_ref() { 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) { if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) {

View File

@ -1,7 +1,7 @@
use crate::core_3d::Transparent3d; use crate::core_3d::Transparent3d;
use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::{ExtractedCamera, MainPassResolutionOverride},
diagnostic::RecordDiagnostics, diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases, render_phase::ViewSortedRenderPhases,
@ -24,12 +24,13 @@ impl ViewNode for MainTransparentPass3dNode {
&'static ExtractedView, &'static ExtractedView,
&'static ViewTarget, &'static ViewTarget,
&'static ViewDepthTexture, &'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
); );
fn run( fn run(
&self, &self,
graph: &mut RenderGraphContext, graph: &mut RenderGraphContext,
render_context: &mut RenderContext, render_context: &mut RenderContext,
(camera, view, target, depth): QueryItem<Self::ViewQuery>, (camera, view, target, depth, resolution_override): QueryItem<Self::ViewQuery>,
world: &World, world: &World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity(); 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"); let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d");
if let Some(viewport) = camera.viewport.as_ref() { 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) { if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) {

View File

@ -1,4 +1,5 @@
use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::camera::MainPassResolutionOverride;
use bevy_render::experimental::occlusion_culling::OcclusionCulling; use bevy_render::experimental::occlusion_culling::OcclusionCulling;
use bevy_render::render_graph::ViewNode; use bevy_render::render_graph::ViewNode;
@ -66,6 +67,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode {
&'static ExtractedView, &'static ExtractedView,
&'static ViewDepthTexture, &'static ViewDepthTexture,
&'static ViewPrepassTextures, &'static ViewPrepassTextures,
Option<&'static MainPassResolutionOverride>,
Has<OcclusionCulling>, Has<OcclusionCulling>,
Has<NoIndirectDrawing>, Has<NoIndirectDrawing>,
); );
@ -77,7 +79,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode {
view_query: QueryItem<'w, '_, Self::ViewQuery>, view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World, world: &'w World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query; let (.., occlusion_culling, no_indirect_drawing) = view_query;
if !occlusion_culling || no_indirect_drawing { if !occlusion_culling || no_indirect_drawing {
return Ok(()); return Ok(());
} }
@ -105,7 +107,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode {
fn run_deferred_prepass<'w>( fn run_deferred_prepass<'w>(
graph: &mut RenderGraphContext, graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>, 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, 'w,
'_, '_,
<LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery, <LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery,
@ -220,7 +222,7 @@ fn run_deferred_prepass<'w>(
}); });
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
if let Some(viewport) = camera.viewport.as_ref() { 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 // Opaque draws

View File

@ -1,6 +1,6 @@
use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::{ExtractedCamera, MainPassResolutionOverride},
render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode}, render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode},
render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor}, render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor},
renderer::RenderContext, renderer::RenderContext,
@ -23,13 +23,14 @@ impl ViewNode for OitResolveNode {
&'static ViewUniformOffset, &'static ViewUniformOffset,
&'static OitResolvePipelineId, &'static OitResolvePipelineId,
&'static ViewDepthTexture, &'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
); );
fn run( fn run(
&self, &self,
_graph: &mut RenderGraphContext, _graph: &mut RenderGraphContext,
render_context: &mut RenderContext, 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, Self::ViewQuery,
>, >,
world: &World, world: &World,
@ -63,7 +64,7 @@ impl ViewNode for OitResolveNode {
}); });
if let Some(viewport) = camera.viewport.as_ref() { 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); render_pass.set_render_pipeline(pipeline);

View File

@ -1,6 +1,6 @@
use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::{ExtractedCamera, MainPassResolutionOverride},
diagnostic::RecordDiagnostics, diagnostic::RecordDiagnostics,
experimental::occlusion_culling::OcclusionCulling, experimental::occlusion_culling::OcclusionCulling,
render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_graph::{NodeRunError, RenderGraphContext, ViewNode},
@ -64,6 +64,7 @@ impl ViewNode for LatePrepassNode {
Option<&'static RenderSkyboxPrepassPipeline>, Option<&'static RenderSkyboxPrepassPipeline>,
Option<&'static SkyboxPrepassBindGroup>, Option<&'static SkyboxPrepassBindGroup>,
Option<&'static PreviousViewUniformOffset>, Option<&'static PreviousViewUniformOffset>,
Option<&'static MainPassResolutionOverride>,
Has<OcclusionCulling>, Has<OcclusionCulling>,
Has<NoIndirectDrawing>, Has<NoIndirectDrawing>,
Has<DeferredPrepass>, Has<DeferredPrepass>,
@ -78,7 +79,7 @@ impl ViewNode for LatePrepassNode {
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
// We only need a late prepass if we have occlusion culling and indirect // We only need a late prepass if we have occlusion culling and indirect
// drawing. // drawing.
let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query; let (.., occlusion_culling, no_indirect_drawing, _) = query;
if !occlusion_culling || no_indirect_drawing { if !occlusion_culling || no_indirect_drawing {
return Ok(()); return Ok(());
} }
@ -109,6 +110,7 @@ fn run_prepass<'w>(
skybox_prepass_pipeline, skybox_prepass_pipeline,
skybox_prepass_bind_group, skybox_prepass_bind_group,
view_prev_uniform_offset, view_prev_uniform_offset,
resolution_override,
_, _,
_, _,
has_deferred, has_deferred,
@ -183,7 +185,7 @@ fn run_prepass<'w>(
let pass_span = diagnostics.pass_span(&mut render_pass, label); let pass_span = diagnostics.pass_span(&mut render_pass, label);
if let Some(viewport) = camera.viewport.as_ref() { 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 // Opaque draws

View File

@ -18,7 +18,7 @@ use bevy_ecs::{
world::World, world::World,
}; };
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::{ExtractedCamera, MainPassResolutionOverride},
render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{ render_resource::{
LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor,
@ -42,6 +42,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
&'static ViewLightProbesUniformOffset, &'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset, &'static ViewScreenSpaceReflectionsUniformOffset,
&'static ViewEnvironmentMapUniformOffset, &'static ViewEnvironmentMapUniformOffset,
Option<&'static MainPassResolutionOverride>,
&'static MeshletViewMaterialsMainOpaquePass, &'static MeshletViewMaterialsMainOpaquePass,
&'static MeshletViewBindGroups, &'static MeshletViewBindGroups,
&'static MeshletViewResources, &'static MeshletViewResources,
@ -61,6 +62,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
view_light_probes_offset, view_light_probes_offset,
view_ssr_offset, view_ssr_offset,
view_environment_map_offset, view_environment_map_offset,
resolution_override,
meshlet_view_materials, meshlet_view_materials,
meshlet_view_bind_groups, meshlet_view_bind_groups,
meshlet_view_resources, meshlet_view_resources,
@ -101,7 +103,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode {
occlusion_query_set: None, occlusion_query_set: None,
}); });
if let Some(viewport) = camera.viewport.as_ref() { 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( render_pass.set_bind_group(
@ -147,6 +149,7 @@ impl ViewNode for MeshletPrepassNode {
&'static ViewPrepassTextures, &'static ViewPrepassTextures,
&'static ViewUniformOffset, &'static ViewUniformOffset,
&'static PreviousViewUniformOffset, &'static PreviousViewUniformOffset,
Option<&'static MainPassResolutionOverride>,
Has<MotionVectorPrepass>, Has<MotionVectorPrepass>,
&'static MeshletViewMaterialsPrepass, &'static MeshletViewMaterialsPrepass,
&'static MeshletViewBindGroups, &'static MeshletViewBindGroups,
@ -162,6 +165,7 @@ impl ViewNode for MeshletPrepassNode {
view_prepass_textures, view_prepass_textures,
view_uniform_offset, view_uniform_offset,
previous_view_uniform_offset, previous_view_uniform_offset,
resolution_override,
view_has_motion_vector_prepass, view_has_motion_vector_prepass,
meshlet_view_materials, meshlet_view_materials,
meshlet_view_bind_groups, meshlet_view_bind_groups,
@ -219,7 +223,7 @@ impl ViewNode for MeshletPrepassNode {
occlusion_query_set: None, occlusion_query_set: None,
}); });
if let Some(viewport) = camera.viewport.as_ref() { 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 { if view_has_motion_vector_prepass {
@ -270,6 +274,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode {
&'static ViewPrepassTextures, &'static ViewPrepassTextures,
&'static ViewUniformOffset, &'static ViewUniformOffset,
&'static PreviousViewUniformOffset, &'static PreviousViewUniformOffset,
Option<&'static MainPassResolutionOverride>,
Has<MotionVectorPrepass>, Has<MotionVectorPrepass>,
&'static MeshletViewMaterialsDeferredGBufferPrepass, &'static MeshletViewMaterialsDeferredGBufferPrepass,
&'static MeshletViewBindGroups, &'static MeshletViewBindGroups,
@ -285,6 +290,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode {
view_prepass_textures, view_prepass_textures,
view_uniform_offset, view_uniform_offset,
previous_view_uniform_offset, previous_view_uniform_offset,
resolution_override,
view_has_motion_vector_prepass, view_has_motion_vector_prepass,
meshlet_view_materials, meshlet_view_materials,
meshlet_view_bind_groups, meshlet_view_bind_groups,
@ -347,7 +353,7 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode {
occlusion_query_set: None, occlusion_query_set: None,
}); });
if let Some(viewport) = camera.viewport.as_ref() { 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 { if view_has_motion_vector_prepass {

View File

@ -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. /// Settings to define a camera sub view.
@ -1366,6 +1377,19 @@ impl TemporalJitter {
#[reflect(Default, Component)] #[reflect(Default, Component)]
pub struct MipBias(pub f32); 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 { impl Default for MipBias {
fn default() -> Self { fn default() -> Self {
Self(-1.0) Self(-1.0)

View File

@ -29,6 +29,7 @@ impl Plugin for CameraPlugin {
.register_type::<Exposure>() .register_type::<Exposure>()
.register_type::<TemporalJitter>() .register_type::<TemporalJitter>()
.register_type::<MipBias>() .register_type::<MipBias>()
.register_type::<MainPassResolutionOverride>()
.init_resource::<ManualTextureViews>() .init_resource::<ManualTextureViews>()
.init_resource::<ClearColor>() .init_resource::<ClearColor>()
.add_plugins(( .add_plugins((

View File

@ -34,7 +34,7 @@ use bevy::{
}, },
GetBatchData, GetFullBatchData, GetBatchData, GetFullBatchData,
}, },
camera::ExtractedCamera, camera::{ExtractedCamera, MainPassResolutionOverride},
extract_component::{ExtractComponent, ExtractComponentPlugin}, extract_component::{ExtractComponent, ExtractComponentPlugin},
mesh::{allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh}, mesh::{allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh},
render_asset::RenderAssets, render_asset::RenderAssets,
@ -589,13 +589,14 @@ impl ViewNode for CustomDrawNode {
&'static ExtractedCamera, &'static ExtractedCamera,
&'static ExtractedView, &'static ExtractedView,
&'static ViewTarget, &'static ViewTarget,
Option<&'static MainPassResolutionOverride>,
); );
fn run<'w>( fn run<'w>(
&self, &self,
graph: &mut RenderGraphContext, graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>, render_context: &mut RenderContext<'w>,
(camera, view, target): QueryItem<'w, '_, Self::ViewQuery>, (camera, view, target, resolution_override): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World, world: &'w World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
// First, we need to get our phases resource // First, we need to get our phases resource
@ -625,7 +626,7 @@ impl ViewNode for CustomDrawNode {
}); });
if let Some(viewport) = camera.viewport.as_ref() { 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 // Render the phase