mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
Expand FallbackImage
to include a GpuImage
for each possible TextureViewDimension
(#6974)
# Objective Fixes #6920 ## Solution From the issue discussion: > From looking at the `AsBindGroup` derive macro implementation, the fallback image's `TextureView` is used when the binding's `Option<Handle<Image>>` is `None`. Because this relies on already having a view that matches the desired binding dimensions, I think the solution will require creating a separate `GpuImage` for each possible `TextureViewDimension`. --- ## Changelog Users can now rely on `FallbackImage` to work with a texture binding of any dimension.
This commit is contained in:
parent
96b9b6c8ad
commit
64405469a5
6 changed files with 245 additions and 19 deletions
|
@ -2052,6 +2052,13 @@ hidden = true
|
|||
name = "window_resizing"
|
||||
path = "examples/window/window_resizing.rs"
|
||||
|
||||
[[example]]
|
||||
name = "fallback_image"
|
||||
path = "examples/shader/fallback_image.rs"
|
||||
|
||||
[package.metadata.example.fallback_image]
|
||||
hidden = true
|
||||
|
||||
[package.metadata.example.window_resizing]
|
||||
name = "Window Resizing"
|
||||
description = "Demonstrates resizing and responding to resizing a window"
|
||||
|
|
39
assets/shaders/fallback_image_test.wgsl
Normal file
39
assets/shaders/fallback_image_test.wgsl
Normal file
|
@ -0,0 +1,39 @@
|
|||
#import bevy_pbr::mesh_view_bindings
|
||||
#import bevy_pbr::mesh_bindings
|
||||
|
||||
@group(1) @binding(0)
|
||||
var test_texture_1d: texture_1d<f32>;
|
||||
@group(1) @binding(1)
|
||||
var test_texture_1d_sampler: sampler;
|
||||
|
||||
@group(1) @binding(2)
|
||||
var test_texture_2d: texture_2d<f32>;
|
||||
@group(1) @binding(3)
|
||||
var test_texture_2d_sampler: sampler;
|
||||
|
||||
@group(1) @binding(4)
|
||||
var test_texture_2d_array: texture_2d_array<f32>;
|
||||
@group(1) @binding(5)
|
||||
var test_texture_2d_array_sampler: sampler;
|
||||
|
||||
@group(1) @binding(6)
|
||||
var test_texture_cube: texture_cube<f32>;
|
||||
@group(1) @binding(7)
|
||||
var test_texture_cube_sampler: sampler;
|
||||
|
||||
@group(1) @binding(8)
|
||||
var test_texture_cube_array: texture_cube_array<f32>;
|
||||
@group(1) @binding(9)
|
||||
var test_texture_cube_array_sampler: sampler;
|
||||
|
||||
@group(1) @binding(10)
|
||||
var test_texture_3d: texture_3d<f32>;
|
||||
@group(1) @binding(11)
|
||||
var test_texture_3d_sampler: sampler;
|
||||
|
||||
struct FragmentInput {
|
||||
#import bevy_pbr::mesh_vertex_output
|
||||
};
|
||||
|
||||
@fragment
|
||||
fn fragment(in: FragmentInput) {}
|
|
@ -117,6 +117,19 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
|
||||
// Read field-level attributes
|
||||
for field in fields.iter() {
|
||||
// Search ahead for texture attributes so we can use them with any
|
||||
// corresponding sampler attribute.
|
||||
let mut tex_attrs = None;
|
||||
for attr in &field.attrs {
|
||||
let Some(attr_ident) = attr.path().get_ident() else {
|
||||
continue;
|
||||
};
|
||||
if attr_ident == TEXTURE_ATTRIBUTE_NAME {
|
||||
let (_binding_index, nested_meta_items) = get_binding_nested_attr(attr)?;
|
||||
tex_attrs = Some(get_texture_attrs(nested_meta_items)?);
|
||||
}
|
||||
}
|
||||
|
||||
for attr in &field.attrs {
|
||||
let Some(attr_ident) = attr.path().get_ident() else {
|
||||
continue;
|
||||
|
@ -255,18 +268,20 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
sample_type,
|
||||
multisampled,
|
||||
visibility,
|
||||
} = get_texture_attrs(nested_meta_items)?;
|
||||
} = tex_attrs.as_ref().unwrap();
|
||||
|
||||
let visibility =
|
||||
visibility.hygenic_quote("e! { #render_path::render_resource });
|
||||
|
||||
let fallback_image = get_fallback_image(&render_path, *dimension);
|
||||
|
||||
binding_impls.push(quote! {
|
||||
#render_path::render_resource::OwnedBindingResource::TextureView({
|
||||
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
|
||||
if let Some(handle) = handle {
|
||||
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
|
||||
} else {
|
||||
fallback_image.texture_view.clone()
|
||||
#fallback_image.texture_view.clone()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -288,18 +303,24 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
let SamplerAttrs {
|
||||
sampler_binding_type,
|
||||
visibility,
|
||||
..
|
||||
} = get_sampler_attrs(nested_meta_items)?;
|
||||
let TextureAttrs { dimension, .. } = tex_attrs
|
||||
.as_ref()
|
||||
.expect("sampler attribute must have matching texture attribute");
|
||||
|
||||
let visibility =
|
||||
visibility.hygenic_quote("e! { #render_path::render_resource });
|
||||
|
||||
let fallback_image = get_fallback_image(&render_path, *dimension);
|
||||
|
||||
binding_impls.push(quote! {
|
||||
#render_path::render_resource::OwnedBindingResource::Sampler({
|
||||
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
|
||||
if let Some(handle) = handle {
|
||||
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone()
|
||||
} else {
|
||||
fallback_image.sampler.clone()
|
||||
#fallback_image.sampler.clone()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -457,6 +478,22 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
}))
|
||||
}
|
||||
|
||||
fn get_fallback_image(
|
||||
render_path: &syn::Path,
|
||||
dimension: BindingTextureDimension,
|
||||
) -> proc_macro2::TokenStream {
|
||||
quote! {
|
||||
match #render_path::render_resource::#dimension {
|
||||
#render_path::render_resource::TextureViewDimension::D1 => &fallback_image.d1,
|
||||
#render_path::render_resource::TextureViewDimension::D2 => &fallback_image.d2,
|
||||
#render_path::render_resource::TextureViewDimension::D2Array => &fallback_image.d2_array,
|
||||
#render_path::render_resource::TextureViewDimension::Cube => &fallback_image.cube,
|
||||
#render_path::render_resource::TextureViewDimension::CubeArray => &fallback_image.cube_array,
|
||||
#render_path::render_resource::TextureViewDimension::D3 => &fallback_image.d3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the arguments for the `uniform` binding attribute.
|
||||
///
|
||||
/// If parsed, represents an attribute
|
||||
|
@ -637,7 +674,7 @@ fn get_visibility_flag_value(meta: Meta) -> Result<ShaderStageVisibility> {
|
|||
Ok(ShaderStageVisibility::Flags(visibility))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
enum BindingTextureDimension {
|
||||
D1,
|
||||
#[default]
|
||||
|
|
|
@ -6,7 +6,7 @@ use bevy_ecs::{
|
|||
};
|
||||
use bevy_math::Vec2;
|
||||
use bevy_utils::HashMap;
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
||||
use wgpu::{Extent3d, TextureFormat};
|
||||
|
||||
use crate::{
|
||||
prelude::Image,
|
||||
|
@ -20,8 +20,21 @@ use crate::{
|
|||
///
|
||||
/// Defaults to a 1x1 fully opaque white texture, (1.0, 1.0, 1.0, 1.0) which makes multiplying
|
||||
/// it with other colors a no-op.
|
||||
#[derive(Resource, Deref)]
|
||||
pub struct FallbackImage(GpuImage);
|
||||
#[derive(Resource)]
|
||||
pub struct FallbackImage {
|
||||
/// Fallback image for [`TextureViewDimension::D1`].
|
||||
pub d1: GpuImage,
|
||||
/// Fallback image for [`TextureViewDimension::D2`].
|
||||
pub d2: GpuImage,
|
||||
/// Fallback image for [`TextureViewDimension::D2Array`].
|
||||
pub d2_array: GpuImage,
|
||||
/// Fallback image for [`TextureViewDimension::Cube`].
|
||||
pub cube: GpuImage,
|
||||
/// Fallback image for [`TextureViewDimension::CubeArray`].
|
||||
pub cube_array: GpuImage,
|
||||
/// Fallback image for [`TextureViewDimension::D3`].
|
||||
pub d3: GpuImage,
|
||||
}
|
||||
|
||||
/// A [`RenderApp`](crate::RenderApp) resource that contains a _zero-filled_ "fallback image",
|
||||
/// which can be used in place of [`FallbackImage`], when a fully transparent or black fallback
|
||||
|
@ -54,14 +67,18 @@ fn fallback_image_new(
|
|||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: match dimension {
|
||||
TextureViewDimension::Cube => 6,
|
||||
TextureViewDimension::Cube | TextureViewDimension::CubeArray => 6,
|
||||
_ => 1,
|
||||
},
|
||||
};
|
||||
|
||||
let mut image = Image::new_fill(extents, TextureDimension::D2, &data, format);
|
||||
let image_dimension = dimension.compatible_texture_dimension();
|
||||
|
||||
let mut image = Image::new_fill(extents, image_dimension, &data, format);
|
||||
image.texture_descriptor.sample_count = samples;
|
||||
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
|
||||
if image_dimension == TextureDimension::D2 {
|
||||
image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
|
||||
}
|
||||
|
||||
// We can't create textures with data when it's a depth texture or when using multiple samples
|
||||
let texture = if format.is_depth_stencil_format() || samples > 1 {
|
||||
|
@ -97,15 +114,62 @@ impl FromWorld for FallbackImage {
|
|||
let render_device = world.resource::<RenderDevice>();
|
||||
let render_queue = world.resource::<RenderQueue>();
|
||||
let default_sampler = world.resource::<DefaultImageSampler>();
|
||||
Self(fallback_image_new(
|
||||
render_device,
|
||||
render_queue,
|
||||
default_sampler,
|
||||
TextureFormat::bevy_default(),
|
||||
TextureViewDimension::D2,
|
||||
1,
|
||||
255,
|
||||
))
|
||||
Self {
|
||||
d1: fallback_image_new(
|
||||
render_device,
|
||||
render_queue,
|
||||
default_sampler,
|
||||
TextureFormat::bevy_default(),
|
||||
TextureViewDimension::D1,
|
||||
1,
|
||||
255,
|
||||
),
|
||||
d2: fallback_image_new(
|
||||
render_device,
|
||||
render_queue,
|
||||
default_sampler,
|
||||
TextureFormat::bevy_default(),
|
||||
TextureViewDimension::D2,
|
||||
1,
|
||||
255,
|
||||
),
|
||||
d2_array: fallback_image_new(
|
||||
render_device,
|
||||
render_queue,
|
||||
default_sampler,
|
||||
TextureFormat::bevy_default(),
|
||||
TextureViewDimension::D2Array,
|
||||
1,
|
||||
255,
|
||||
),
|
||||
cube: fallback_image_new(
|
||||
render_device,
|
||||
render_queue,
|
||||
default_sampler,
|
||||
TextureFormat::bevy_default(),
|
||||
TextureViewDimension::Cube,
|
||||
1,
|
||||
255,
|
||||
),
|
||||
cube_array: fallback_image_new(
|
||||
render_device,
|
||||
render_queue,
|
||||
default_sampler,
|
||||
TextureFormat::bevy_default(),
|
||||
TextureViewDimension::CubeArray,
|
||||
1,
|
||||
255,
|
||||
),
|
||||
d3: fallback_image_new(
|
||||
render_device,
|
||||
render_queue,
|
||||
default_sampler,
|
||||
TextureFormat::bevy_default(),
|
||||
TextureViewDimension::D3,
|
||||
1,
|
||||
255,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
77
examples/shader/fallback_image.rs
Normal file
77
examples/shader/fallback_image.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
//! This example tests that all texture dimensions are supported by
|
||||
//! `FallbackImage`.
|
||||
//!
|
||||
//! When running this example, you should expect to see a window that only draws
|
||||
//! the clear color. The test material does not shade any geometry; this example
|
||||
//! only tests that the images are initialized and bound so that the app does
|
||||
//! not panic.
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
reflect::{TypePath, TypeUuid},
|
||||
render::render_resource::{AsBindGroup, ShaderRef},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugin(MaterialPlugin::<FallbackTestMaterial>::default())
|
||||
.add_systems(Startup, setup)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<FallbackTestMaterial>>,
|
||||
) {
|
||||
commands.spawn(MaterialMeshBundle {
|
||||
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
|
||||
material: materials.add(FallbackTestMaterial {
|
||||
image_1d: None,
|
||||
image_2d: None,
|
||||
image_2d_array: None,
|
||||
image_cube: None,
|
||||
image_cube_array: None,
|
||||
image_3d: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::new(1.5, 0.0, 0.0), Vec3::Y),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(AsBindGroup, Debug, Clone, TypePath, TypeUuid)]
|
||||
#[uuid = "d4890167-0e16-4bfc-b812-434717f20409"]
|
||||
struct FallbackTestMaterial {
|
||||
#[texture(0, dimension = "1d")]
|
||||
#[sampler(1)]
|
||||
image_1d: Option<Handle<Image>>,
|
||||
|
||||
#[texture(2, dimension = "2d")]
|
||||
#[sampler(3)]
|
||||
image_2d: Option<Handle<Image>>,
|
||||
|
||||
#[texture(4, dimension = "2d_array")]
|
||||
#[sampler(5)]
|
||||
image_2d_array: Option<Handle<Image>>,
|
||||
|
||||
#[texture(6, dimension = "cube")]
|
||||
#[sampler(7)]
|
||||
image_cube: Option<Handle<Image>>,
|
||||
|
||||
#[texture(8, dimension = "cube_array")]
|
||||
#[sampler(9)]
|
||||
image_cube_array: Option<Handle<Image>>,
|
||||
|
||||
#[texture(10, dimension = "3d")]
|
||||
#[sampler(11)]
|
||||
image_3d: Option<Handle<Image>>,
|
||||
}
|
||||
|
||||
impl Material for FallbackTestMaterial {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"shaders/fallback_image_test.wgsl".into()
|
||||
}
|
||||
}
|
|
@ -110,6 +110,8 @@ impl AsBindGroup for BindlessMaterial {
|
|||
}
|
||||
}
|
||||
|
||||
let fallback_image = &fallback_image.d2;
|
||||
|
||||
let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
|
||||
|
||||
// convert bevy's resource types to WGPU's references
|
||||
|
|
Loading…
Reference in a new issue