Files
bevy/examples/3d/render_to_texture.rs
Luo Zhihao 2e9ef6988c Fix non-srgb RenderTarget::Image (#22090)
# Objective

Fixes
https://github.com/bevyengine/bevy/pull/22031#issuecomment-3640036590.
Fixes #15201.
#22031 makes `ViewTarget::out_texture_format` to return the underlying
texture’s format, which causes some issues:
1. `ExtractedWindow::swap_chain_texture_view` always uses srgb view. But
the underlying swap chain texture in WebGPU can be Bgra8unorm, leading
to format mismatch between the render pipeline and the render pass.
2. We can no longer use srgb view for non-srgb target texture, it will
panic due to incompatible pipeline:
```rs
    let mut image = Image::new_target_texture(512, 512, TextureFormat::Rgba8Unorm);
    image.texture_view_descriptor = Some(bevy_render::render_resource::TextureViewDescriptor {
        format: Some(TextureFormat::Rgba8UnormSrgb),
        ..Default::default()
    });
    image.texture_descriptor.view_formats = &[TextureFormat::Rgba8UnormSrgb];
```

## Solution

Reverts #22031.
Renames some `format` to `view_format` explicitly.
Adds `view_format` to `GpuImage` and `Image::new_target_texture` so we
can make render pipeline match render pass texture view.

## Testing
Tested `render_to_texture` and `screenshot` examples on linux and
webgpu.
<details>
<summary>The rendered Rgba8Unorm texture with or without srgb view in
MeshMaterial3d looks the same, but the underlaying data is
different:</summary>

With srgb view:
<img width="912" height="640" alt="屏幕截图_20251212_223355"
src="https://github.com/user-attachments/assets/d320bad9-d11a-4d3d-93a9-879af6413658"
/>

Without srgb view:
<img width="912" height="640" alt="屏幕截图_20251212_223313"
src="https://github.com/user-attachments/assets/522abf23-9c85-468d-8d17-a94495ee4452"
/>

</details>
2025-12-14 21:39:02 +00:00

124 lines
4.0 KiB
Rust

//! Shows how to render to a texture. Useful for mirrors, UI, or exporting images.
use std::f32::consts::PI;
use bevy::camera::RenderTarget;
use bevy::{camera::visibility::RenderLayers, prelude::*, render::render_resource::TextureFormat};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (cube_rotator_system, rotator_system))
.run();
}
// Marks the first pass cube (rendered to a texture.)
#[derive(Component)]
struct FirstPassCube;
// Marks the main pass cube, to which the texture is applied.
#[derive(Component)]
struct MainPassCube;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
) {
// This is the texture that will be rendered to.
let image = Image::new_target_texture(
512,
512,
TextureFormat::Rgba8Unorm,
Some(TextureFormat::Rgba8UnormSrgb),
);
let image_handle = images.add(image);
let cube_handle = meshes.add(Cuboid::new(4.0, 4.0, 4.0));
let cube_material_handle = materials.add(StandardMaterial {
base_color: Color::srgb(0.8, 0.7, 0.6),
reflectance: 0.02,
unlit: false,
..default()
});
// This specifies the layer used for the first pass, which will be attached to the first pass camera and cube.
let first_pass_layer = RenderLayers::layer(1);
// The cube that will be rendered to the texture.
commands.spawn((
Mesh3d(cube_handle),
MeshMaterial3d(cube_material_handle),
Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)),
FirstPassCube,
first_pass_layer.clone(),
));
// Light
// NOTE: we add the light to both layers so it affects both the rendered-to-texture cube, and the cube on which we display the texture
// Setting the layer to RenderLayers::layer(0) would cause the main view to be lit, but the rendered-to-texture cube to be unlit.
// Setting the layer to RenderLayers::layer(1) would cause the rendered-to-texture cube to be lit, but the main view to be unlit.
commands.spawn((
PointLight::default(),
Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)),
RenderLayers::layer(0).with(1),
));
commands.spawn((
Camera3d::default(),
Camera {
// render before the "main pass" camera
order: -1,
clear_color: Color::WHITE.into(),
..default()
},
RenderTarget::Image(image_handle.clone().into()),
Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)).looking_at(Vec3::ZERO, Vec3::Y),
first_pass_layer,
));
let cube_size = 4.0;
let cube_handle = meshes.add(Cuboid::new(cube_size, cube_size, cube_size));
// This material has the texture that has been rendered.
let material_handle = materials.add(StandardMaterial {
base_color_texture: Some(image_handle),
reflectance: 0.02,
unlit: false,
..default()
});
// Main pass cube, with material containing the rendered first pass texture.
commands.spawn((
Mesh3d(cube_handle),
MeshMaterial3d(material_handle),
Transform::from_xyz(0.0, 0.0, 1.5).with_rotation(Quat::from_rotation_x(-PI / 5.0)),
MainPassCube,
));
// The main pass camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
/// Rotates the inner cube (first pass)
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<FirstPassCube>>) {
for mut transform in &mut query {
transform.rotate_x(1.5 * time.delta_secs());
transform.rotate_z(1.3 * time.delta_secs());
}
}
/// Rotates the outer cube (main pass)
fn cube_rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<MainPassCube>>) {
for mut transform in &mut query {
transform.rotate_x(1.0 * time.delta_secs());
transform.rotate_y(0.7 * time.delta_secs());
}
}