168 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			168 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! A shader that binds several textures onto one
 | 
						|
//! `binding_array<texture<f32>>` shader binding slot and sample non-uniformly.
 | 
						|
 | 
						|
use bevy::{
 | 
						|
    prelude::*,
 | 
						|
    reflect::TypeUuid,
 | 
						|
    render::{
 | 
						|
        render_asset::RenderAssets,
 | 
						|
        render_resource::{AsBindGroupError, PreparedBindGroup, *},
 | 
						|
        renderer::RenderDevice,
 | 
						|
        texture::FallbackImage,
 | 
						|
    },
 | 
						|
};
 | 
						|
use std::num::NonZeroU32;
 | 
						|
 | 
						|
fn main() {
 | 
						|
    let mut app = App::new();
 | 
						|
    app.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()));
 | 
						|
 | 
						|
    let render_device = app.world.resource::<RenderDevice>();
 | 
						|
 | 
						|
    // check if the device support the required feature
 | 
						|
    if !render_device
 | 
						|
        .features()
 | 
						|
        .contains(WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING)
 | 
						|
    {
 | 
						|
        error!(
 | 
						|
            "Render device doesn't support feature \
 | 
						|
            SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, \
 | 
						|
            which is required for texture binding arrays"
 | 
						|
        );
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    app.add_plugin(MaterialPlugin::<BindlessMaterial>::default())
 | 
						|
        .add_systems(Startup, setup)
 | 
						|
        .run();
 | 
						|
}
 | 
						|
 | 
						|
const MAX_TEXTURE_COUNT: usize = 16;
 | 
						|
const TILE_ID: [usize; 16] = [
 | 
						|
    19, 23, 4, 33, 12, 69, 30, 48, 10, 65, 40, 47, 57, 41, 44, 46,
 | 
						|
];
 | 
						|
 | 
						|
fn setup(
 | 
						|
    mut commands: Commands,
 | 
						|
    mut meshes: ResMut<Assets<Mesh>>,
 | 
						|
    mut materials: ResMut<Assets<BindlessMaterial>>,
 | 
						|
    asset_server: Res<AssetServer>,
 | 
						|
) {
 | 
						|
    commands.spawn(Camera3dBundle {
 | 
						|
        transform: Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
 | 
						|
        ..Default::default()
 | 
						|
    });
 | 
						|
 | 
						|
    // load 16 textures
 | 
						|
    let textures: Vec<_> = TILE_ID
 | 
						|
        .iter()
 | 
						|
        .map(|id| {
 | 
						|
            let path = format!("textures/rpg/tiles/generic-rpg-tile{id:0>2}.png");
 | 
						|
            asset_server.load(path)
 | 
						|
        })
 | 
						|
        .collect();
 | 
						|
 | 
						|
    // a cube with multiple textures
 | 
						|
    commands.spawn(MaterialMeshBundle {
 | 
						|
        mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
 | 
						|
        material: materials.add(BindlessMaterial { textures }),
 | 
						|
        ..Default::default()
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Debug, Clone, TypeUuid)]
 | 
						|
#[uuid = "8dd2b424-45a2-4a53-ac29-7ce356b2d5fe"]
 | 
						|
struct BindlessMaterial {
 | 
						|
    textures: Vec<Handle<Image>>,
 | 
						|
}
 | 
						|
 | 
						|
impl AsBindGroup for BindlessMaterial {
 | 
						|
    type Data = ();
 | 
						|
 | 
						|
    fn as_bind_group(
 | 
						|
        &self,
 | 
						|
        layout: &BindGroupLayout,
 | 
						|
        render_device: &RenderDevice,
 | 
						|
        image_assets: &RenderAssets<Image>,
 | 
						|
        fallback_image: &FallbackImage,
 | 
						|
    ) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError> {
 | 
						|
        // retrieve the render resources from handles
 | 
						|
        let mut images = vec![];
 | 
						|
        for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) {
 | 
						|
            match image_assets.get(handle) {
 | 
						|
                Some(image) => images.push(image),
 | 
						|
                None => return Err(AsBindGroupError::RetryNextUpdate),
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
 | 
						|
 | 
						|
        // convert bevy's resource types to WGPU's references
 | 
						|
        let mut textures: Vec<_> = textures.into_iter().map(|texture| &**texture).collect();
 | 
						|
 | 
						|
        // fill in up to the first `MAX_TEXTURE_COUNT` textures and samplers to the arrays
 | 
						|
        for (id, image) in images.into_iter().enumerate() {
 | 
						|
            textures[id] = &*image.texture_view;
 | 
						|
        }
 | 
						|
 | 
						|
        let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
 | 
						|
            label: "bindless_material_bind_group".into(),
 | 
						|
            layout,
 | 
						|
            entries: &[
 | 
						|
                BindGroupEntry {
 | 
						|
                    binding: 0,
 | 
						|
                    resource: BindingResource::TextureViewArray(&textures[..]),
 | 
						|
                },
 | 
						|
                BindGroupEntry {
 | 
						|
                    binding: 1,
 | 
						|
                    resource: BindingResource::Sampler(&fallback_image.sampler),
 | 
						|
                },
 | 
						|
            ],
 | 
						|
        });
 | 
						|
 | 
						|
        Ok(PreparedBindGroup {
 | 
						|
            bindings: vec![],
 | 
						|
            bind_group,
 | 
						|
            data: (),
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout
 | 
						|
    where
 | 
						|
        Self: Sized,
 | 
						|
    {
 | 
						|
        render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
 | 
						|
            label: "bindless_material_layout".into(),
 | 
						|
            entries: &[
 | 
						|
                // @group(1) @binding(0) var textures: binding_array<texture_2d<f32>>;
 | 
						|
                BindGroupLayoutEntry {
 | 
						|
                    binding: 0,
 | 
						|
                    visibility: ShaderStages::FRAGMENT,
 | 
						|
                    ty: BindingType::Texture {
 | 
						|
                        sample_type: TextureSampleType::Float { filterable: true },
 | 
						|
                        view_dimension: TextureViewDimension::D2,
 | 
						|
                        multisampled: false,
 | 
						|
                    },
 | 
						|
                    count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
 | 
						|
                },
 | 
						|
                // @group(1) @binding(1) var nearest_sampler: sampler;
 | 
						|
                BindGroupLayoutEntry {
 | 
						|
                    binding: 1,
 | 
						|
                    visibility: ShaderStages::FRAGMENT,
 | 
						|
                    ty: BindingType::Sampler(SamplerBindingType::Filtering),
 | 
						|
                    count: None,
 | 
						|
                    // Note: as textures, multiple samplers can also be bound onto one binding slot.
 | 
						|
                    // One may need to pay attention to the limit of sampler binding amount on some platforms.
 | 
						|
                    // count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
 | 
						|
                },
 | 
						|
            ],
 | 
						|
        })
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl Material for BindlessMaterial {
 | 
						|
    fn fragment_shader() -> ShaderRef {
 | 
						|
        "shaders/texture_binding_array.wgsl".into()
 | 
						|
    }
 | 
						|
}
 |