Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
//! A shader that uses dynamic data like the time since startup.
//! The time data is in the globals binding which is part of the `mesh_view_bindings` shader import.
use bevy::{
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/animate_shader.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {}
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,97 @@
//! This example illustrates how to create a texture for use with a `texture_2d_array<f32>` shader
//! uniform variable.
use bevy::{
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/array_texture.wgsl";
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<ArrayTextureMaterial>::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, create_array_texture)
.run();
}
#[derive(Resource)]
struct LoadingTexture {
is_loaded: bool,
handle: Handle<Image>,
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Start loading the texture.
commands.insert_resource(LoadingTexture {
is_loaded: false,
handle: asset_server.load("textures/array_texture.png"),
});
// light
commands.spawn((
DirectionalLight::default(),
Transform::from_xyz(3.0, 2.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::new(1.5, 0.0, 0.0), Vec3::Y),
));
}
fn create_array_texture(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut loading_texture: ResMut<LoadingTexture>,
mut images: ResMut<Assets<Image>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ArrayTextureMaterial>>,
) {
if loading_texture.is_loaded
|| !asset_server
.load_state(loading_texture.handle.id())
.is_loaded()
{
return;
}
loading_texture.is_loaded = true;
let image = images.get_mut(&loading_texture.handle).unwrap();
// Create a new array texture asset from the loaded texture.
let array_layers = 4;
image.reinterpret_stacked_2d_as_array(array_layers);
// Spawn some cubes using the array texture
let mesh_handle = meshes.add(Cuboid::default());
let material_handle = materials.add(ArrayTextureMaterial {
array_texture: loading_texture.handle.clone(),
});
for x in -5..=5 {
commands.spawn((
Mesh3d(mesh_handle.clone()),
MeshMaterial3d(material_handle.clone()),
Transform::from_xyz(x as f32 + 0.5, 0.0, 0.0),
));
}
}
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct ArrayTextureMaterial {
#[texture(0, dimension = "2d_array")]
#[sampler(1)]
array_texture: Handle<Image>,
}
impl Material for ArrayTextureMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,104 @@
//! Shows that multiple instances of a cube are automatically instanced in one draw call
//! Try running this example in a graphics profiler and all the cubes should be only a single draw call.
//! Also demonstrates how to use `MeshTag` to use external data in a custom material.
use bevy::{
prelude::*,
reflect::TypePath,
render::{
mesh::MeshTag,
render_resource::{AsBindGroup, ShaderRef},
},
};
const SHADER_ASSET_PATH: &str = "shaders/automatic_instancing.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.add_systems(Update, update)
.run();
}
/// Sets up an instanced grid of cubes, where each cube is colored based on an image that is
/// sampled in the vertex shader. The cubes are then animated in a spiral pattern.
///
/// This example demonstrates one use of automatic instancing and how to use `MeshTag` to use
/// external data in a custom material. For example, here we use the "index" of each cube to
/// determine the texel coordinate to sample from the image in the shader.
fn setup(
mut commands: Commands,
assets: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
// We will use this image as our external data for our material to sample from in the vertex shader
let image = assets.load("branding/icon.png");
// Our single mesh handle that will be instanced
let mesh_handle = meshes.add(Cuboid::from_size(Vec3::splat(0.01)));
// Create the custom material with a reference to our texture
// Automatic instancing works with any Material, including the `StandardMaterial`.
// This custom material is used to demonstrate the optional `MeshTag` feature.
let material_handle = materials.add(CustomMaterial {
image: image.clone(),
});
// We're hardcoding the image dimensions for simplicity
let image_dims = UVec2::new(256, 256);
let total_pixels = image_dims.x * image_dims.y;
for index in 0..total_pixels {
// Get x,y from index - x goes left to right, y goes top to bottom
let x = index % image_dims.x;
let y = index / image_dims.x;
// Convert to centered world coordinates
let world_x = (x as f32 - image_dims.x as f32 / 2.0) / 50.0;
let world_y = -((y as f32 - image_dims.y as f32 / 2.0) / 50.0); // Still need negative for world space
commands.spawn((
// For automatic instancing to take effect you need to
// use the same mesh handle and material handle for each instance
Mesh3d(mesh_handle.clone()),
MeshMaterial3d(material_handle.clone()),
// This is an optional component that can be used to help tie external data to a mesh instance
MeshTag(index),
Transform::from_xyz(world_x, world_y, 0.0),
));
}
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
// Animate the transform
fn update(time: Res<Time>, mut transforms: Query<(&mut Transform, &MeshTag)>) {
for (mut transform, index) in transforms.iter_mut() {
// Animate the z position based on time using the index to create a spiral
transform.translation.z = ops::sin(time.elapsed_secs() + index.0 as f32 * 0.01);
}
}
// This struct defines the data that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {
#[texture(0)]
#[sampler(1)]
image: Handle<Image>,
}
impl Material for CustomMaterial {
fn vertex_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,292 @@
//! A compute shader that simulates Conway's Game of Life.
//!
//! Compute shaders use the GPU for computing arbitrary information, that may be independent of what
//! is rendered to the screen.
use bevy::{
prelude::*,
render::{
extract_resource::{ExtractResource, ExtractResourcePlugin},
render_asset::{RenderAssetUsages, RenderAssets},
render_graph::{self, RenderGraph, RenderLabel},
render_resource::{binding_types::texture_storage_2d, *},
renderer::{RenderContext, RenderDevice},
texture::GpuImage,
Render, RenderApp, RenderSet,
},
};
use std::borrow::Cow;
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/game_of_life.wgsl";
const DISPLAY_FACTOR: u32 = 4;
const SIZE: (u32, u32) = (1280 / DISPLAY_FACTOR, 720 / DISPLAY_FACTOR);
const WORKGROUP_SIZE: u32 = 8;
fn main() {
App::new()
.insert_resource(ClearColor(Color::BLACK))
.add_plugins((
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
resolution: (
(SIZE.0 * DISPLAY_FACTOR) as f32,
(SIZE.1 * DISPLAY_FACTOR) as f32,
)
.into(),
// uncomment for unthrottled FPS
// present_mode: bevy::window::PresentMode::AutoNoVsync,
..default()
}),
..default()
})
.set(ImagePlugin::default_nearest()),
GameOfLifeComputePlugin,
))
.add_systems(Startup, setup)
.add_systems(Update, switch_textures)
.run();
}
fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
let mut image = Image::new_fill(
Extent3d {
width: SIZE.0,
height: SIZE.1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 255],
TextureFormat::R32Float,
RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage =
TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING;
let image0 = images.add(image.clone());
let image1 = images.add(image);
commands.spawn((
Sprite {
image: image0.clone(),
custom_size: Some(Vec2::new(SIZE.0 as f32, SIZE.1 as f32)),
..default()
},
Transform::from_scale(Vec3::splat(DISPLAY_FACTOR as f32)),
));
commands.spawn(Camera2d);
commands.insert_resource(GameOfLifeImages {
texture_a: image0,
texture_b: image1,
});
}
// Switch texture to display every frame to show the one that was written to most recently.
fn switch_textures(images: Res<GameOfLifeImages>, mut sprite: Single<&mut Sprite>) {
if sprite.image == images.texture_a {
sprite.image = images.texture_b.clone_weak();
} else {
sprite.image = images.texture_a.clone_weak();
}
}
struct GameOfLifeComputePlugin;
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct GameOfLifeLabel;
impl Plugin for GameOfLifeComputePlugin {
fn build(&self, app: &mut App) {
// Extract the game of life image resource from the main world into the render world
// for operation on by the compute shader and display on the sprite.
app.add_plugins(ExtractResourcePlugin::<GameOfLifeImages>::default());
let render_app = app.sub_app_mut(RenderApp);
render_app.add_systems(
Render,
prepare_bind_group.in_set(RenderSet::PrepareBindGroups),
);
let mut render_graph = render_app.world_mut().resource_mut::<RenderGraph>();
render_graph.add_node(GameOfLifeLabel, GameOfLifeNode::default());
render_graph.add_node_edge(GameOfLifeLabel, bevy::render::graph::CameraDriverLabel);
}
fn finish(&self, app: &mut App) {
let render_app = app.sub_app_mut(RenderApp);
render_app.init_resource::<GameOfLifePipeline>();
}
}
#[derive(Resource, Clone, ExtractResource)]
struct GameOfLifeImages {
texture_a: Handle<Image>,
texture_b: Handle<Image>,
}
#[derive(Resource)]
struct GameOfLifeImageBindGroups([BindGroup; 2]);
fn prepare_bind_group(
mut commands: Commands,
pipeline: Res<GameOfLifePipeline>,
gpu_images: Res<RenderAssets<GpuImage>>,
game_of_life_images: Res<GameOfLifeImages>,
render_device: Res<RenderDevice>,
) {
let view_a = gpu_images.get(&game_of_life_images.texture_a).unwrap();
let view_b = gpu_images.get(&game_of_life_images.texture_b).unwrap();
let bind_group_0 = render_device.create_bind_group(
None,
&pipeline.texture_bind_group_layout,
&BindGroupEntries::sequential((&view_a.texture_view, &view_b.texture_view)),
);
let bind_group_1 = render_device.create_bind_group(
None,
&pipeline.texture_bind_group_layout,
&BindGroupEntries::sequential((&view_b.texture_view, &view_a.texture_view)),
);
commands.insert_resource(GameOfLifeImageBindGroups([bind_group_0, bind_group_1]));
}
#[derive(Resource)]
struct GameOfLifePipeline {
texture_bind_group_layout: BindGroupLayout,
init_pipeline: CachedComputePipelineId,
update_pipeline: CachedComputePipelineId,
}
impl FromWorld for GameOfLifePipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let texture_bind_group_layout = render_device.create_bind_group_layout(
"GameOfLifeImages",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
),
),
);
let shader = world.load_asset(SHADER_ASSET_PATH);
let pipeline_cache = world.resource::<PipelineCache>();
let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: None,
layout: vec![texture_bind_group_layout.clone()],
push_constant_ranges: Vec::new(),
shader: shader.clone(),
shader_defs: vec![],
entry_point: Cow::from("init"),
zero_initialize_workgroup_memory: false,
});
let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: None,
layout: vec![texture_bind_group_layout.clone()],
push_constant_ranges: Vec::new(),
shader,
shader_defs: vec![],
entry_point: Cow::from("update"),
zero_initialize_workgroup_memory: false,
});
GameOfLifePipeline {
texture_bind_group_layout,
init_pipeline,
update_pipeline,
}
}
}
enum GameOfLifeState {
Loading,
Init,
Update(usize),
}
struct GameOfLifeNode {
state: GameOfLifeState,
}
impl Default for GameOfLifeNode {
fn default() -> Self {
Self {
state: GameOfLifeState::Loading,
}
}
}
impl render_graph::Node for GameOfLifeNode {
fn update(&mut self, world: &mut World) {
let pipeline = world.resource::<GameOfLifePipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
// if the corresponding pipeline has loaded, transition to the next stage
match self.state {
GameOfLifeState::Loading => {
match pipeline_cache.get_compute_pipeline_state(pipeline.init_pipeline) {
CachedPipelineState::Ok(_) => {
self.state = GameOfLifeState::Init;
}
CachedPipelineState::Err(err) => {
panic!("Initializing assets/{SHADER_ASSET_PATH}:\n{err}")
}
_ => {}
}
}
GameOfLifeState::Init => {
if let CachedPipelineState::Ok(_) =
pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline)
{
self.state = GameOfLifeState::Update(1);
}
}
GameOfLifeState::Update(0) => {
self.state = GameOfLifeState::Update(1);
}
GameOfLifeState::Update(1) => {
self.state = GameOfLifeState::Update(0);
}
GameOfLifeState::Update(_) => unreachable!(),
}
}
fn run(
&self,
_graph: &mut render_graph::RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), render_graph::NodeRunError> {
let bind_groups = &world.resource::<GameOfLifeImageBindGroups>().0;
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<GameOfLifePipeline>();
let mut pass = render_context
.command_encoder()
.begin_compute_pass(&ComputePassDescriptor::default());
// select the pipeline based on the current state
match self.state {
GameOfLifeState::Loading => {}
GameOfLifeState::Init => {
let init_pipeline = pipeline_cache
.get_compute_pipeline(pipeline.init_pipeline)
.unwrap();
pass.set_bind_group(0, &bind_groups[0], &[]);
pass.set_pipeline(init_pipeline);
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
}
GameOfLifeState::Update(index) => {
let update_pipeline = pipeline_cache
.get_compute_pipeline(pipeline.update_pipeline)
.unwrap();
pass.set_bind_group(0, &bind_groups[index], &[]);
pass.set_pipeline(update_pipeline);
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
}
}
Ok(())
}
}

View File

@@ -0,0 +1,388 @@
//! Demonstrates how to enqueue custom draw commands in a render phase.
//!
//! This example shows how to use the built-in
//! [`bevy_render::render_phase::BinnedRenderPhase`] functionality with a
//! custom [`RenderCommand`] to allow inserting arbitrary GPU drawing logic
//! into Bevy's pipeline. This is not the only way to add custom rendering code
//! into Bevy—render nodes are another, lower-level method—but it does allow
//! for better reuse of parts of Bevy's built-in mesh rendering logic.
use bevy::{
core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT},
ecs::{
component::Tick,
query::ROQueryItem,
system::{lifetimeless::SRes, SystemParamItem},
},
prelude::*,
render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
primitives::Aabb,
render_phase::{
AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, InputUniformIndex, PhaseItem,
RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass,
ViewBinnedRenderPhases,
},
render_resource::{
BufferUsages, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState,
FragmentState, IndexFormat, MultisampleState, PipelineCache, PrimitiveState,
RawBufferVec, RenderPipelineDescriptor, SpecializedRenderPipeline,
SpecializedRenderPipelines, TextureFormat, VertexAttribute, VertexBufferLayout,
VertexFormat, VertexState, VertexStepMode,
},
renderer::{RenderDevice, RenderQueue},
view::{self, ExtractedView, RenderVisibleEntities, VisibilityClass},
Render, RenderApp, RenderSet,
},
};
use bytemuck::{Pod, Zeroable};
/// A marker component that represents an entity that is to be rendered using
/// our custom phase item.
///
/// Note the [`ExtractComponent`] trait implementation: this is necessary to
/// tell Bevy that this object should be pulled into the render world. Also note
/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
/// that entities with this component need to be examined for visibility.
#[derive(Clone, Component, ExtractComponent)]
#[require(VisibilityClass)]
#[component(on_add = view::add_visibility_class::<CustomRenderedEntity>)]
struct CustomRenderedEntity;
/// Holds a reference to our shader.
///
/// This is loaded at app creation time.
#[derive(Resource)]
struct CustomPhasePipeline {
shader: Handle<Shader>,
}
/// A [`RenderCommand`] that binds the vertex and index buffers and issues the
/// draw command for our custom phase item.
struct DrawCustomPhaseItem;
impl<P> RenderCommand<P> for DrawCustomPhaseItem
where
P: PhaseItem,
{
type Param = SRes<CustomPhaseItemBuffers>;
type ViewQuery = ();
type ItemQuery = ();
fn render<'w>(
_: &P,
_: ROQueryItem<'w, Self::ViewQuery>,
_: Option<ROQueryItem<'w, Self::ItemQuery>>,
custom_phase_item_buffers: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
// Borrow check workaround.
let custom_phase_item_buffers = custom_phase_item_buffers.into_inner();
// Tell the GPU where the vertices are.
pass.set_vertex_buffer(
0,
custom_phase_item_buffers
.vertices
.buffer()
.unwrap()
.slice(..),
);
// Tell the GPU where the indices are.
pass.set_index_buffer(
custom_phase_item_buffers
.indices
.buffer()
.unwrap()
.slice(..),
0,
IndexFormat::Uint32,
);
// Draw one triangle (3 vertices).
pass.draw_indexed(0..3, 0, 0..1);
RenderCommandResult::Success
}
}
/// The GPU vertex and index buffers for our custom phase item.
///
/// As the custom phase item is a single triangle, these are uploaded once and
/// then left alone.
#[derive(Resource)]
struct CustomPhaseItemBuffers {
/// The vertices for the single triangle.
///
/// This is a [`RawBufferVec`] because that's the simplest and fastest type
/// of GPU buffer, and [`Vertex`] objects are simple.
vertices: RawBufferVec<Vertex>,
/// The indices of the single triangle.
///
/// As above, this is a [`RawBufferVec`] because `u32` values have trivial
/// size and alignment.
indices: RawBufferVec<u32>,
}
/// The CPU-side structure that describes a single vertex of the triangle.
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
struct Vertex {
/// The 3D position of the triangle vertex.
position: Vec3,
/// Padding.
pad0: u32,
/// The color of the triangle vertex.
color: Vec3,
/// Padding.
pad1: u32,
}
impl Vertex {
/// Creates a new vertex structure.
const fn new(position: Vec3, color: Vec3) -> Vertex {
Vertex {
position,
color,
pad0: 0,
pad1: 0,
}
}
}
/// The custom draw commands that Bevy executes for each entity we enqueue into
/// the render phase.
type DrawCustomPhaseItemCommands = (SetItemPipeline, DrawCustomPhaseItem);
/// A single triangle's worth of vertices, for demonstration purposes.
static VERTICES: [Vertex; 3] = [
Vertex::new(vec3(-0.866, -0.5, 0.5), vec3(1.0, 0.0, 0.0)),
Vertex::new(vec3(0.866, -0.5, 0.5), vec3(0.0, 1.0, 0.0)),
Vertex::new(vec3(0.0, 1.0, 0.5), vec3(0.0, 0.0, 1.0)),
];
/// The entry point.
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_plugins(ExtractComponentPlugin::<CustomRenderedEntity>::default())
.add_systems(Startup, setup);
// We make sure to add these to the render app, not the main app.
app.get_sub_app_mut(RenderApp)
.unwrap()
.init_resource::<CustomPhasePipeline>()
.init_resource::<SpecializedRenderPipelines<CustomPhasePipeline>>()
.add_render_command::<Opaque3d, DrawCustomPhaseItemCommands>()
.add_systems(
Render,
prepare_custom_phase_item_buffers.in_set(RenderSet::Prepare),
)
.add_systems(Render, queue_custom_phase_item.in_set(RenderSet::Queue));
app.run();
}
/// Spawns the objects in the scene.
fn setup(mut commands: Commands) {
// Spawn a single entity that has custom rendering. It'll be extracted into
// the render world via [`ExtractComponent`].
commands.spawn((
Visibility::default(),
Transform::default(),
// This `Aabb` is necessary for the visibility checks to work.
Aabb {
center: Vec3A::ZERO,
half_extents: Vec3A::splat(0.5),
},
CustomRenderedEntity,
));
// Spawn the camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
/// Creates the [`CustomPhaseItemBuffers`] resource.
///
/// This must be done in a startup system because it needs the [`RenderDevice`]
/// and [`RenderQueue`] to exist, and they don't until [`App::run`] is called.
fn prepare_custom_phase_item_buffers(mut commands: Commands) {
commands.init_resource::<CustomPhaseItemBuffers>();
}
/// A render-world system that enqueues the entity with custom rendering into
/// the opaque render phases of each view.
fn queue_custom_phase_item(
pipeline_cache: Res<PipelineCache>,
custom_phase_pipeline: Res<CustomPhasePipeline>,
mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
opaque_draw_functions: Res<DrawFunctions<Opaque3d>>,
mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<CustomPhasePipeline>>,
views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>,
mut next_tick: Local<Tick>,
) {
let draw_custom_phase_item = opaque_draw_functions
.read()
.id::<DrawCustomPhaseItemCommands>();
// Render phases are per-view, so we need to iterate over all views so that
// the entity appears in them. (In this example, we have only one view, but
// it's good practice to loop over all views anyway.)
for (view, view_visible_entities, msaa) in views.iter() {
let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
continue;
};
// Find all the custom rendered entities that are visible from this
// view.
for &entity in view_visible_entities.get::<CustomRenderedEntity>().iter() {
// Ordinarily, the [`SpecializedRenderPipeline::Key`] would contain
// some per-view settings, such as whether the view is HDR, but for
// simplicity's sake we simply hard-code the view's characteristics,
// with the exception of number of MSAA samples.
let pipeline_id = specialized_render_pipelines.specialize(
&pipeline_cache,
&custom_phase_pipeline,
*msaa,
);
// Bump the change tick in order to force Bevy to rebuild the bin.
let this_tick = next_tick.get() + 1;
next_tick.set(this_tick);
// Add the custom render item. We use the
// [`BinnedRenderPhaseType::NonMesh`] type to skip the special
// handling that Bevy has for meshes (preprocessing, indirect
// draws, etc.)
//
// The asset ID is arbitrary; we simply use [`AssetId::invalid`],
// but you can use anything you like. Note that the asset ID need
// not be the ID of a [`Mesh`].
opaque_phase.add(
Opaque3dBatchSetKey {
draw_function: draw_custom_phase_item,
pipeline: pipeline_id,
material_bind_group_index: None,
lightmap_slab: None,
vertex_slab: default(),
index_slab: None,
},
Opaque3dBinKey {
asset_id: AssetId::<Mesh>::invalid().untyped(),
},
entity,
InputUniformIndex::default(),
BinnedRenderPhaseType::NonMesh,
*next_tick,
);
}
}
}
impl SpecializedRenderPipeline for CustomPhasePipeline {
type Key = Msaa;
fn specialize(&self, msaa: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("custom render pipeline".into()),
layout: vec![],
push_constant_ranges: vec![],
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: vec![],
entry_point: "vertex".into(),
buffers: vec![VertexBufferLayout {
array_stride: size_of::<Vertex>() as u64,
step_mode: VertexStepMode::Vertex,
// This needs to match the layout of [`Vertex`].
attributes: vec![
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 16,
shader_location: 1,
},
],
}],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
// Ordinarily, you'd want to check whether the view has the
// HDR format and substitute the appropriate texture format
// here, but we omit that for simplicity.
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
// Note that if your view has no depth buffer this will need to be
// changed.
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: false,
depth_compare: CompareFunction::Always,
stencil: default(),
bias: default(),
}),
multisample: MultisampleState {
count: msaa.samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
zero_initialize_workgroup_memory: false,
}
}
}
impl FromWorld for CustomPhaseItemBuffers {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
// Create the vertex and index buffers.
let mut vbo = RawBufferVec::new(BufferUsages::VERTEX);
let mut ibo = RawBufferVec::new(BufferUsages::INDEX);
for vertex in &VERTICES {
vbo.push(*vertex);
}
for index in 0..3 {
ibo.push(index);
}
// These two lines are required in order to trigger the upload to GPU.
vbo.write_buffer(render_device, render_queue);
ibo.write_buffer(render_device, render_queue);
CustomPhaseItemBuffers {
vertices: vbo,
indices: ibo,
}
}
}
impl FromWorld for CustomPhasePipeline {
fn from_world(world: &mut World) -> Self {
// Load and compile the shader in the background.
let asset_server = world.resource::<AssetServer>();
CustomPhasePipeline {
shader: asset_server.load("shaders/custom_phase_item.wgsl"),
}
}
}

View File

@@ -0,0 +1,371 @@
//! This example shows how to create a custom render pass that runs after the main pass
//! and reads the texture generated by the main pass.
//!
//! The example shader is a very simple implementation of chromatic aberration.
//! To adapt this example for 2D, replace all instances of 3D structures (such as `Core3D`, etc.) with their corresponding 2D counterparts.
//!
//! This is a fairly low level example and assumes some familiarity with rendering concepts and wgpu.
use bevy::{
core_pipeline::{
core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
},
ecs::query::QueryItem,
prelude::*,
render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_graph::{
NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
},
renderer::{RenderContext, RenderDevice},
view::ViewTarget,
RenderApp,
},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, PostProcessPlugin))
.add_systems(Startup, setup)
.add_systems(Update, (rotate, update_settings))
.run();
}
/// It is generally encouraged to set up post processing effects as a plugin
struct PostProcessPlugin;
impl Plugin for PostProcessPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
// The settings will be a component that lives in the main world but will
// be extracted to the render world every frame.
// This makes it possible to control the effect from the main world.
// This plugin will take care of extracting it automatically.
// It's important to derive [`ExtractComponent`] on [`PostProcessingSettings`]
// for this plugin to work correctly.
ExtractComponentPlugin::<PostProcessSettings>::default(),
// The settings will also be the data used in the shader.
// This plugin will prepare the component for the GPU by creating a uniform buffer
// and writing the data to that buffer every frame.
UniformComponentPlugin::<PostProcessSettings>::default(),
));
// We need to get the render app from the main app
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
// Bevy's renderer uses a render graph which is a collection of nodes in a directed acyclic graph.
// It currently runs on each view/camera and executes each node in the specified order.
// It will make sure that any node that needs a dependency from another node
// only runs when that dependency is done.
//
// Each node can execute arbitrary work, but it generally runs at least one render pass.
// A node only has access to the render world, so if you need data from the main world
// you need to extract it manually or with the plugin like above.
// Add a [`Node`] to the [`RenderGraph`]
// The Node needs to impl FromWorld
//
// The [`ViewNodeRunner`] is a special [`Node`] that will automatically run the node for each view
// matching the [`ViewQuery`]
.add_render_graph_node::<ViewNodeRunner<PostProcessNode>>(
// Specify the label of the graph, in this case we want the graph for 3d
Core3d,
// It also needs the label of the node
PostProcessLabel,
)
.add_render_graph_edges(
Core3d,
// Specify the node ordering.
// This will automatically create all required node edges to enforce the given ordering.
(
Node3d::Tonemapping,
PostProcessLabel,
Node3d::EndMainPassPostProcessing,
),
);
}
fn finish(&self, app: &mut App) {
// We need to get the render app from the main app
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
// Initialize the pipeline
.init_resource::<PostProcessPipeline>();
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct PostProcessLabel;
// The post process node used for the render graph
#[derive(Default)]
struct PostProcessNode;
// The ViewNode trait is required by the ViewNodeRunner
impl ViewNode for PostProcessNode {
// The node needs a query to gather data from the ECS in order to do its rendering,
// but it's not a normal system so we need to define it manually.
//
// This query will only run on the view entity
type ViewQuery = (
&'static ViewTarget,
// This makes sure the node only runs on cameras with the PostProcessSettings component
&'static PostProcessSettings,
// As there could be multiple post processing components sent to the GPU (one per camera),
// we need to get the index of the one that is associated with the current view.
&'static DynamicUniformIndex<PostProcessSettings>,
);
// Runs the node logic
// This is where you encode draw commands.
//
// This will run on every view on which the graph is running.
// If you don't want your effect to run on every camera,
// you'll need to make sure you have a marker component as part of [`ViewQuery`]
// to identify which camera(s) should run the effect.
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, _post_process_settings, settings_index): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
// Get the pipeline resource that contains the global data we need
// to create the render pipeline
let post_process_pipeline = world.resource::<PostProcessPipeline>();
// The pipeline cache is a cache of all previously created pipelines.
// It is required to avoid creating a new pipeline each frame,
// which is expensive due to shader compilation.
let pipeline_cache = world.resource::<PipelineCache>();
// Get the pipeline from the cache
let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
else {
return Ok(());
};
// Get the settings uniform binding
let settings_uniforms = world.resource::<ComponentUniforms<PostProcessSettings>>();
let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
return Ok(());
};
// This will start a new "post process write", obtaining two texture
// views from the view target - a `source` and a `destination`.
// `source` is the "current" main texture and you _must_ write into
// `destination` because calling `post_process_write()` on the
// [`ViewTarget`] will internally flip the [`ViewTarget`]'s main
// texture to the `destination` texture. Failing to do so will cause
// the current main texture information to be lost.
let post_process = view_target.post_process_write();
// The bind_group gets created each frame.
//
// Normally, you would create a bind_group in the Queue set,
// but this doesn't work with the post_process_write().
// The reason it doesn't work is because each post_process_write will alternate the source/destination.
// The only way to have the correct source/destination for the bind_group
// is to make sure you get it during the node execution.
let bind_group = render_context.render_device().create_bind_group(
"post_process_bind_group",
&post_process_pipeline.layout,
// It's important for this to match the BindGroupLayout defined in the PostProcessPipeline
&BindGroupEntries::sequential((
// Make sure to use the source view
post_process.source,
// Use the sampler created for the pipeline
&post_process_pipeline.sampler,
// Set the settings binding
settings_binding.clone(),
)),
);
// Begin the render pass
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("post_process_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
// We need to specify the post process destination view here
// to make sure we write to the appropriate texture.
view: post_process.destination,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
// This is mostly just wgpu boilerplate for drawing a fullscreen triangle,
// using the pipeline/bind_group created above
render_pass.set_render_pipeline(pipeline);
// By passing in the index of the post process settings on this view, we ensure
// that in the event that multiple settings were sent to the GPU (as would be the
// case with multiple cameras), we use the correct one.
render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
// This contains global data used by the render pipeline. This will be created once on startup.
#[derive(Resource)]
struct PostProcessPipeline {
layout: BindGroupLayout,
sampler: Sampler,
pipeline_id: CachedRenderPipelineId,
}
impl FromWorld for PostProcessPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
// We need to define the bind group layout used for our pipeline
let layout = render_device.create_bind_group_layout(
"post_process_bind_group_layout",
&BindGroupLayoutEntries::sequential(
// The layout entries will only be visible in the fragment stage
ShaderStages::FRAGMENT,
(
// The screen texture
texture_2d(TextureSampleType::Float { filterable: true }),
// The sampler that will be used to sample the screen texture
sampler(SamplerBindingType::Filtering),
// The settings uniform that will control the effect
uniform_buffer::<PostProcessSettings>(true),
),
),
);
// We can create the sampler here since it won't change at runtime and doesn't depend on the view
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
// Get the shader handle
let shader = world.load_asset(SHADER_ASSET_PATH);
let pipeline_id = world
.resource_mut::<PipelineCache>()
// This will add the pipeline to the cache and queue its creation
.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("post_process_pipeline".into()),
layout: vec![layout.clone()],
// This will setup a fullscreen triangle for the vertex state
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader,
shader_defs: vec![],
// Make sure this matches the entry point of your shader.
// It can be anything as long as it matches here and in the shader.
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
// All of the following properties are not important for this effect so just use the default values.
// This struct doesn't have the Default trait implemented because not all fields can have a default value.
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
});
Self {
layout,
sampler,
pipeline_id,
}
}
}
// This is the component that will get passed to the shader
#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)]
struct PostProcessSettings {
intensity: f32,
// WebGL2 structs must be 16 byte aligned.
#[cfg(feature = "webgl2")]
_webgl2_padding: Vec3,
}
/// Set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// camera
commands.spawn((
Camera3d::default(),
Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
Camera {
clear_color: Color::WHITE.into(),
..default()
},
// Add the setting to the camera.
// This component is also used to determine on which camera to run the post processing effect.
PostProcessSettings {
intensity: 0.02,
..default()
},
));
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
Transform::from_xyz(0.0, 0.5, 0.0),
Rotates,
));
// light
commands.spawn(DirectionalLight {
illuminance: 1_000.,
..default()
});
}
#[derive(Component)]
struct Rotates;
/// Rotates any entity around the x and y axis
fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
for mut transform in &mut query {
transform.rotate_x(0.55 * time.delta_secs());
transform.rotate_z(0.15 * time.delta_secs());
}
}
// Change the intensity over time to show that the effect is controlled from the main world
fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
for mut setting in &mut settings {
let mut intensity = ops::sin(time.elapsed_secs());
// Make it loop periodically
intensity = ops::sin(intensity);
// Remap it to 0..1 because the intensity can't be negative
intensity = intensity * 0.5 + 0.5;
// Scale it to a more reasonable level
intensity *= 0.015;
// Set the intensity.
// This will then be extracted to the render world and uploaded to the GPU automatically by the [`UniformComponentPlugin`]
setting.intensity = intensity;
}
}

View File

@@ -0,0 +1,632 @@
//! This example demonstrates how to write a custom phase
//!
//! Render phases in bevy are used whenever you need to draw a group of meshes in a specific way.
//! For example, bevy's main pass has an opaque phase, a transparent phase for both 2d and 3d.
//! Sometimes, you may want to only draw a subset of meshes before or after the builtin phase. In
//! those situations you need to write your own phase.
//!
//! This example showcases how writing a custom phase to draw a stencil of a bevy mesh could look
//! like. Some shortcuts have been used for simplicity.
//!
//! This example was made for 3d, but a 2d equivalent would be almost identical.
use std::ops::Range;
use bevy::{
core_pipeline::core_3d::graph::{Core3d, Node3d},
ecs::{
query::QueryItem,
system::{lifetimeless::SRes, SystemParamItem},
},
math::FloatOrd,
pbr::{
DrawMesh, MeshInputUniform, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey,
MeshUniform, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup,
},
platform::collections::HashSet,
prelude::*,
render::{
batching::{
gpu_preprocessing::{
batch_and_prepare_sorted_render_phase, IndirectParametersCpuMetadata,
UntypedPhaseIndirectParametersBuffers,
},
GetBatchData, GetFullBatchData,
},
camera::ExtractedCamera,
extract_component::{ExtractComponent, ExtractComponentPlugin},
mesh::{allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh},
render_asset::RenderAssets,
render_graph::{
NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner,
},
render_phase::{
sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId,
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SetItemPipeline, SortedPhaseItem,
SortedRenderPhasePlugin, ViewSortedRenderPhases,
},
render_resource::{
CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace,
MultisampleState, PipelineCache, PolygonMode, PrimitiveState, RenderPassDescriptor,
RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
SpecializedMeshPipelines, TextureFormat, VertexState,
},
renderer::RenderContext,
sync_world::MainEntity,
view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget},
Extract, Render, RenderApp, RenderDebugFlags, RenderSet,
},
};
use nonmax::NonMaxU32;
const SHADER_ASSET_PATH: &str = "shaders/custom_stencil.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MeshStencilPhasePlugin))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// circular base
commands.spawn((
Mesh3d(meshes.add(Circle::new(4.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
));
// cube
// This cube will be rendered by the main pass, but it will also be rendered by our custom
// pass. This should result in an unlit red cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
Transform::from_xyz(0.0, 0.5, 0.0),
// This marker component is used to identify which mesh will be used in our custom pass
// The circle doesn't have it so it won't be rendered in our pass
DrawStencil,
));
// light
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
// disable msaa for simplicity
Msaa::Off,
));
}
#[derive(Component, ExtractComponent, Clone, Copy, Default)]
struct DrawStencil;
struct MeshStencilPhasePlugin;
impl Plugin for MeshStencilPhasePlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<DrawStencil>::default(),
SortedRenderPhasePlugin::<Stencil3d, MeshPipeline>::new(RenderDebugFlags::default()),
));
// We need to get the render app from the main app
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedMeshPipelines<StencilPipeline>>()
.init_resource::<DrawFunctions<Stencil3d>>()
.add_render_command::<Stencil3d, DrawMesh3dStencil>()
.init_resource::<ViewSortedRenderPhases<Stencil3d>>()
.add_systems(ExtractSchedule, extract_camera_phases)
.add_systems(
Render,
(
queue_custom_meshes.in_set(RenderSet::QueueMeshes),
sort_phase_system::<Stencil3d>.in_set(RenderSet::PhaseSort),
batch_and_prepare_sorted_render_phase::<Stencil3d, StencilPipeline>
.in_set(RenderSet::PrepareResources),
),
);
render_app
.add_render_graph_node::<ViewNodeRunner<CustomDrawNode>>(Core3d, CustomDrawPassLabel)
// Tell the node to run after the main pass
.add_render_graph_edges(Core3d, (Node3d::MainOpaquePass, CustomDrawPassLabel));
}
fn finish(&self, app: &mut App) {
// We need to get the render app from the main app
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
// The pipeline needs the RenderDevice to be created and it's only available once plugins
// are initialized
render_app.init_resource::<StencilPipeline>();
}
}
#[derive(Resource)]
struct StencilPipeline {
/// The base mesh pipeline defined by bevy
///
/// Since we want to draw a stencil of an existing bevy mesh we want to reuse the default
/// pipeline as much as possible
mesh_pipeline: MeshPipeline,
/// Stores the shader used for this pipeline directly on the pipeline.
/// This isn't required, it's only done like this for simplicity.
shader_handle: Handle<Shader>,
}
impl FromWorld for StencilPipeline {
fn from_world(world: &mut World) -> Self {
Self {
mesh_pipeline: MeshPipeline::from_world(world),
shader_handle: world.resource::<AssetServer>().load(SHADER_ASSET_PATH),
}
}
}
// For more information on how SpecializedMeshPipeline work, please look at the
// specialized_mesh_pipeline example
impl SpecializedMeshPipeline for StencilPipeline {
type Key = MeshPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
// We will only use the position of the mesh in our shader so we only need to specify that
let mut vertex_attributes = Vec::new();
if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
// Make sure this matches the shader location
vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
}
// This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes
let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
Ok(RenderPipelineDescriptor {
label: Some("Specialized Mesh Pipeline".into()),
// We want to reuse the data from bevy so we use the same bind groups as the default
// mesh pipeline
layout: vec![
// Bind group 0 is the view uniform
self.mesh_pipeline
.get_view_layout(MeshPipelineViewLayoutKey::from(key))
.clone(),
// Bind group 1 is the mesh uniform
self.mesh_pipeline.mesh_layouts.model_only.clone(),
],
push_constant_ranges: vec![],
vertex: VertexState {
shader: self.shader_handle.clone(),
shader_defs: vec![],
entry_point: "vertex".into(),
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: self.shader_handle.clone(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: key.primitive_topology(),
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
polygon_mode: PolygonMode::Fill,
..default()
},
depth_stencil: None,
// It's generally recommended to specialize your pipeline for MSAA,
// but it's not always possible
multisample: MultisampleState::default(),
zero_initialize_workgroup_memory: false,
})
}
}
// We will reuse render commands already defined by bevy to draw a 3d mesh
type DrawMesh3dStencil = (
SetItemPipeline,
// This will set the view bindings in group 0
SetMeshViewBindGroup<0>,
// This will set the mesh bindings in group 1
SetMeshBindGroup<1>,
// This will draw the mesh
DrawMesh,
);
// This is the data required per entity drawn in a custom phase in bevy. More specifically this is the
// data required when using a ViewSortedRenderPhase. This would look differently if we wanted a
// batched render phase. Sorted phases are a bit easier to implement, but a batched phase would
// look similar.
//
// If you want to see how a batched phase implementation looks, you should look at the Opaque2d
// phase.
struct Stencil3d {
pub sort_key: FloatOrd,
pub entity: (Entity, MainEntity),
pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
/// Whether the mesh in question is indexed (uses an index buffer in
/// addition to its vertex buffer).
pub indexed: bool,
}
// For more information about writing a phase item, please look at the custom_phase_item example
impl PhaseItem for Stencil3d {
#[inline]
fn entity(&self) -> Entity {
self.entity.0
}
#[inline]
fn main_entity(&self) -> MainEntity {
self.entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
#[inline]
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl SortedPhaseItem for Stencil3d {
type SortKey = FloatOrd;
#[inline]
fn sort_key(&self) -> Self::SortKey {
self.sort_key
}
#[inline]
fn sort(items: &mut [Self]) {
// bevy normally uses radsort instead of the std slice::sort_by_key
// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.
// Since it is not re-exported by bevy, we just use the std sort for the purpose of the example
items.sort_by_key(SortedPhaseItem::sort_key);
}
#[inline]
fn indexed(&self) -> bool {
self.indexed
}
}
impl CachedRenderPipelinePhaseItem for Stencil3d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline
}
}
impl GetBatchData for StencilPipeline {
type Param = (
SRes<RenderMeshInstances>,
SRes<RenderAssets<RenderMesh>>,
SRes<MeshAllocator>,
);
type CompareData = AssetId<Mesh>;
type BufferData = MeshUniform;
fn get_batch_data(
(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,
(_entity, main_entity): (Entity, MainEntity),
) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
error!(
"`get_batch_data` should never be called in GPU mesh uniform \
building mode"
);
return None;
};
let mesh_instance = mesh_instances.get(&main_entity)?;
let first_vertex_index =
match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {
Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
None => 0,
};
let mesh_uniform = {
let mesh_transforms = &mesh_instance.transforms;
let (local_from_world_transpose_a, local_from_world_transpose_b) =
mesh_transforms.world_from_local.inverse_transpose_3x3();
MeshUniform {
world_from_local: mesh_transforms.world_from_local.to_transpose(),
previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(),
lightmap_uv_rect: UVec2::ZERO,
local_from_world_transpose_a,
local_from_world_transpose_b,
flags: mesh_transforms.flags,
first_vertex_index,
current_skin_index: u32::MAX,
material_and_lightmap_bind_group_slot: 0,
tag: 0,
pad: 0,
}
};
Some((mesh_uniform, None))
}
}
impl GetFullBatchData for StencilPipeline {
type BufferInputData = MeshInputUniform;
fn get_index_and_compare_data(
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
main_entity: MainEntity,
) -> Option<(NonMaxU32, Option<Self::CompareData>)> {
// This should only be called during GPU building.
let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else {
error!(
"`get_index_and_compare_data` should never be called in CPU mesh uniform building \
mode"
);
return None;
};
let mesh_instance = mesh_instances.get(&main_entity)?;
Some((
mesh_instance.current_uniform_index,
mesh_instance
.should_batch()
.then_some(mesh_instance.mesh_asset_id),
))
}
fn get_binned_batch_data(
(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,
main_entity: MainEntity,
) -> Option<Self::BufferData> {
let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
error!(
"`get_binned_batch_data` should never be called in GPU mesh uniform building mode"
);
return None;
};
let mesh_instance = mesh_instances.get(&main_entity)?;
let first_vertex_index =
match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {
Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
None => 0,
};
Some(MeshUniform::new(
&mesh_instance.transforms,
first_vertex_index,
mesh_instance.material_bindings_index.slot,
None,
None,
None,
))
}
fn write_batch_indirect_parameters_metadata(
indexed: bool,
base_output_index: u32,
batch_set_index: Option<NonMaxU32>,
indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers,
indirect_parameters_offset: u32,
) {
// Note that `IndirectParameters` covers both of these structures, even
// though they actually have distinct layouts. See the comment above that
// type for more information.
let indirect_parameters = IndirectParametersCpuMetadata {
base_output_index,
batch_set_index: match batch_set_index {
None => !0,
Some(batch_set_index) => u32::from(batch_set_index),
},
};
if indexed {
indirect_parameters_buffers
.indexed
.set(indirect_parameters_offset, indirect_parameters);
} else {
indirect_parameters_buffers
.non_indexed
.set(indirect_parameters_offset, indirect_parameters);
}
}
fn get_binned_index(
_param: &SystemParamItem<Self::Param>,
_query_item: MainEntity,
) -> Option<NonMaxU32> {
None
}
}
// When defining a phase, we need to extract it from the main world and add it to a resource
// that will be used by the render world. We need to give that resource all views that will use
// that phase
fn extract_camera_phases(
mut stencil_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
cameras: Extract<Query<(Entity, &Camera), With<Camera3d>>>,
mut live_entities: Local<HashSet<RetainedViewEntity>>,
) {
live_entities.clear();
for (main_entity, camera) in &cameras {
if !camera.is_active {
continue;
}
// This is the main camera, so we use the first subview index (0)
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
stencil_phases.insert_or_clear(retained_view_entity);
live_entities.insert(retained_view_entity);
}
// Clear out all dead views.
stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
}
// This is a very important step when writing a custom phase.
//
// This system determines which meshes will be added to the phase.
fn queue_custom_meshes(
custom_draw_functions: Res<DrawFunctions<Stencil3d>>,
mut pipelines: ResMut<SpecializedMeshPipelines<StencilPipeline>>,
pipeline_cache: Res<PipelineCache>,
custom_draw_pipeline: Res<StencilPipeline>,
render_meshes: Res<RenderAssets<RenderMesh>>,
render_mesh_instances: Res<RenderMeshInstances>,
mut custom_render_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
mut views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>,
has_marker: Query<(), With<DrawStencil>>,
) {
for (view, visible_entities, msaa) in &mut views {
let Some(custom_phase) = custom_render_phases.get_mut(&view.retained_view_entity) else {
continue;
};
let draw_custom = custom_draw_functions.read().id::<DrawMesh3dStencil>();
// Create the key based on the view.
// In this case we only care about MSAA and HDR
let view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
| MeshPipelineKey::from_hdr(view.hdr);
let rangefinder = view.rangefinder3d();
// Since our phase can work on any 3d mesh we can reuse the default mesh 3d filter
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
// We only want meshes with the marker component to be queued to our phase.
if has_marker.get(*render_entity).is_err() {
continue;
}
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
else {
continue;
};
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
// Specialize the key for the current mesh entity
// For this example we only specialize based on the mesh topology
// but you could have more complex keys and that's where you'd need to create those keys
let mut mesh_key = view_key;
mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&custom_draw_pipeline,
mesh_key,
&mesh.layout,
);
let pipeline_id = match pipeline_id {
Ok(id) => id,
Err(err) => {
error!("{}", err);
continue;
}
};
let distance = rangefinder.distance_translation(&mesh_instance.translation);
// At this point we have all the data we need to create a phase item and add it to our
// phase
custom_phase.add(Stencil3d {
// Sort the data based on the distance to the view
sort_key: FloatOrd(distance),
entity: (*render_entity, *visible_entity),
pipeline: pipeline_id,
draw_function: draw_custom,
// Sorted phase items aren't batched
batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None,
indexed: mesh.indexed(),
});
}
}
}
// Render label used to order our render graph node that will render our phase
#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]
struct CustomDrawPassLabel;
#[derive(Default)]
struct CustomDrawNode;
impl ViewNode for CustomDrawNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, view, target): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
// First, we need to get our phases resource
let Some(stencil_phases) = world.get_resource::<ViewSortedRenderPhases<Stencil3d>>() else {
return Ok(());
};
// Get the view entity from the graph
let view_entity = graph.view_entity();
// Get the phase for the current view running our node
let Some(stencil_phase) = stencil_phases.get(&view.retained_view_entity) else {
return Ok(());
};
// Render pass setup
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("stencil pass"),
// For the purpose of the example, we will write directly to the view target. A real
// stencil pass would write to a custom texture and that texture would be used in later
// passes to render custom effects using it.
color_attachments: &[Some(target.get_color_attachment())],
// We don't bind any depth buffer for this pass
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Render the phase
// This will execute each draw functions of each phase items queued in this phase
if let Err(err) = stencil_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the stencil phase {err:?}");
}
Ok(())
}
}

View File

@@ -0,0 +1,320 @@
//! A shader that renders a mesh multiple times in one draw call.
//!
//! Bevy will automatically batch and instance your meshes assuming you use the same
//! `Handle<Material>` and `Handle<Mesh>` for all of your instances.
//!
//! This example is intended for advanced users and shows how to make a custom instancing
//! implementation using bevy's low level rendering api.
//! It's generally recommended to try the built-in instancing before going with this approach.
use bevy::{
core_pipeline::core_3d::Transparent3d,
ecs::{
query::QueryItem,
system::{lifetimeless::*, SystemParamItem},
},
pbr::{
MeshPipeline, MeshPipelineKey, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup,
},
prelude::*,
render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
mesh::{
allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh, RenderMeshBufferInfo,
},
render_asset::RenderAssets,
render_phase::{
AddRenderCommand, DrawFunctions, PhaseItem, PhaseItemExtraIndex, RenderCommand,
RenderCommandResult, SetItemPipeline, TrackedRenderPass, ViewSortedRenderPhases,
},
render_resource::*,
renderer::RenderDevice,
sync_world::MainEntity,
view::{ExtractedView, NoFrustumCulling, NoIndirectDrawing},
Render, RenderApp, RenderSet,
},
};
use bytemuck::{Pod, Zeroable};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/instancing.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, CustomMaterialPlugin))
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))),
InstanceMaterialData(
(1..=10)
.flat_map(|x| (1..=10).map(move |y| (x as f32 / 10.0, y as f32 / 10.0)))
.map(|(x, y)| InstanceData {
position: Vec3::new(x * 10.0 - 5.0, y * 10.0 - 5.0, 0.0),
scale: 1.0,
color: LinearRgba::from(Color::hsla(x * 360., y, 0.5, 1.0)).to_f32_array(),
})
.collect(),
),
// NOTE: Frustum culling is done based on the Aabb of the Mesh and the GlobalTransform.
// As the cube is at the origin, if its Aabb moves outside the view frustum, all the
// instanced cubes will be culled.
// The InstanceMaterialData contains the 'GlobalTransform' information for this custom
// instancing, and that is not taken into account with the built-in frustum culling.
// We must disable the built-in frustum culling by adding the `NoFrustumCulling` marker
// component to avoid incorrect culling.
NoFrustumCulling,
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y),
// We need this component because we use `draw_indexed` and `draw`
// instead of `draw_indirect_indexed` and `draw_indirect` in
// `DrawMeshInstanced::render`.
NoIndirectDrawing,
));
}
#[derive(Component, Deref)]
struct InstanceMaterialData(Vec<InstanceData>);
impl ExtractComponent for InstanceMaterialData {
type QueryData = &'static InstanceMaterialData;
type QueryFilter = ();
type Out = Self;
fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option<Self> {
Some(InstanceMaterialData(item.0.clone()))
}
}
struct CustomMaterialPlugin;
impl Plugin for CustomMaterialPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ExtractComponentPlugin::<InstanceMaterialData>::default());
app.sub_app_mut(RenderApp)
.add_render_command::<Transparent3d, DrawCustom>()
.init_resource::<SpecializedMeshPipelines<CustomPipeline>>()
.add_systems(
Render,
(
queue_custom.in_set(RenderSet::QueueMeshes),
prepare_instance_buffers.in_set(RenderSet::PrepareResources),
),
);
}
fn finish(&self, app: &mut App) {
app.sub_app_mut(RenderApp).init_resource::<CustomPipeline>();
}
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
struct InstanceData {
position: Vec3,
scale: f32,
color: [f32; 4],
}
fn queue_custom(
transparent_3d_draw_functions: Res<DrawFunctions<Transparent3d>>,
custom_pipeline: Res<CustomPipeline>,
mut pipelines: ResMut<SpecializedMeshPipelines<CustomPipeline>>,
pipeline_cache: Res<PipelineCache>,
meshes: Res<RenderAssets<RenderMesh>>,
render_mesh_instances: Res<RenderMeshInstances>,
material_meshes: Query<(Entity, &MainEntity), With<InstanceMaterialData>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent3d>>,
views: Query<(&ExtractedView, &Msaa)>,
) {
let draw_custom = transparent_3d_draw_functions.read().id::<DrawCustom>();
for (view, msaa) in &views {
let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
else {
continue;
};
let msaa_key = MeshPipelineKey::from_msaa_samples(msaa.samples());
let view_key = msaa_key | MeshPipelineKey::from_hdr(view.hdr);
let rangefinder = view.rangefinder3d();
for (entity, main_entity) in &material_meshes {
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*main_entity)
else {
continue;
};
let Some(mesh) = meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
let key =
view_key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
let pipeline = pipelines
.specialize(&pipeline_cache, &custom_pipeline, key, &mesh.layout)
.unwrap();
transparent_phase.add(Transparent3d {
entity: (entity, *main_entity),
pipeline,
draw_function: draw_custom,
distance: rangefinder.distance_translation(&mesh_instance.translation),
batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None,
indexed: true,
});
}
}
}
#[derive(Component)]
struct InstanceBuffer {
buffer: Buffer,
length: usize,
}
fn prepare_instance_buffers(
mut commands: Commands,
query: Query<(Entity, &InstanceMaterialData)>,
render_device: Res<RenderDevice>,
) {
for (entity, instance_data) in &query {
let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
label: Some("instance data buffer"),
contents: bytemuck::cast_slice(instance_data.as_slice()),
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
});
commands.entity(entity).insert(InstanceBuffer {
buffer,
length: instance_data.len(),
});
}
}
#[derive(Resource)]
struct CustomPipeline {
shader: Handle<Shader>,
mesh_pipeline: MeshPipeline,
}
impl FromWorld for CustomPipeline {
fn from_world(world: &mut World) -> Self {
let mesh_pipeline = world.resource::<MeshPipeline>();
CustomPipeline {
shader: world.load_asset(SHADER_ASSET_PATH),
mesh_pipeline: mesh_pipeline.clone(),
}
}
}
impl SpecializedMeshPipeline for CustomPipeline {
type Key = MeshPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut descriptor = self.mesh_pipeline.specialize(key, layout)?;
descriptor.vertex.shader = self.shader.clone();
descriptor.vertex.buffers.push(VertexBufferLayout {
array_stride: size_of::<InstanceData>() as u64,
step_mode: VertexStepMode::Instance,
attributes: vec![
VertexAttribute {
format: VertexFormat::Float32x4,
offset: 0,
shader_location: 3, // shader locations 0-2 are taken up by Position, Normal and UV attributes
},
VertexAttribute {
format: VertexFormat::Float32x4,
offset: VertexFormat::Float32x4.size(),
shader_location: 4,
},
],
});
descriptor.fragment.as_mut().unwrap().shader = self.shader.clone();
Ok(descriptor)
}
}
type DrawCustom = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetMeshBindGroup<1>,
DrawMeshInstanced,
);
struct DrawMeshInstanced;
impl<P: PhaseItem> RenderCommand<P> for DrawMeshInstanced {
type Param = (
SRes<RenderAssets<RenderMesh>>,
SRes<RenderMeshInstances>,
SRes<MeshAllocator>,
);
type ViewQuery = ();
type ItemQuery = Read<InstanceBuffer>;
#[inline]
fn render<'w>(
item: &P,
_view: (),
instance_buffer: Option<&'w InstanceBuffer>,
(meshes, render_mesh_instances, mesh_allocator): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
// A borrow check workaround.
let mesh_allocator = mesh_allocator.into_inner();
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(item.main_entity())
else {
return RenderCommandResult::Skip;
};
let Some(gpu_mesh) = meshes.into_inner().get(mesh_instance.mesh_asset_id) else {
return RenderCommandResult::Skip;
};
let Some(instance_buffer) = instance_buffer else {
return RenderCommandResult::Skip;
};
let Some(vertex_buffer_slice) =
mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id)
else {
return RenderCommandResult::Skip;
};
pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
pass.set_vertex_buffer(1, instance_buffer.buffer.slice(..));
match &gpu_mesh.buffer_info {
RenderMeshBufferInfo::Indexed {
index_format,
count,
} => {
let Some(index_buffer_slice) =
mesh_allocator.mesh_index_slice(&mesh_instance.mesh_asset_id)
else {
return RenderCommandResult::Skip;
};
pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format);
pass.draw_indexed(
index_buffer_slice.range.start..(index_buffer_slice.range.start + count),
vertex_buffer_slice.range.start as i32,
0..instance_buffer.length as u32,
);
}
RenderMeshBufferInfo::NonIndexed => {
pass.draw(vertex_buffer_slice.range, 0..instance_buffer.length as u32);
}
}
RenderCommandResult::Success
}
}

View File

@@ -0,0 +1,89 @@
//! A shader that reads a mesh's custom vertex attribute.
use bevy::{
pbr::{MaterialPipeline, MaterialPipelineKey},
prelude::*,
reflect::TypePath,
render::{
mesh::{MeshVertexAttribute, MeshVertexBufferLayoutRef},
render_resource::{
AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError,
VertexFormat,
},
},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/custom_vertex_attribute.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.run();
}
// A "high" random id should be used for custom attributes to ensure consistent sorting and avoid collisions with other attributes.
// See the MeshVertexAttribute docs for more info.
const ATTRIBUTE_BLEND_COLOR: MeshVertexAttribute =
MeshVertexAttribute::new("BlendColor", 988540917, VertexFormat::Float32x4);
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
let mesh = Mesh::from(Cuboid::default())
// Sets the custom attribute
.with_inserted_attribute(
ATTRIBUTE_BLEND_COLOR,
// The cube mesh has 24 vertices (6 faces, 4 vertices per face), so we insert one BlendColor for each
vec![[1.0, 0.0, 0.0, 1.0]; 24],
);
// cube
commands.spawn((
Mesh3d(meshes.add(mesh)),
MeshMaterial3d(materials.add(CustomMaterial {
color: LinearRgba::WHITE,
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {
#[uniform(0)]
color: LinearRgba,
}
impl Material for CustomMaterial {
fn vertex_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
_key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
let vertex_layout = layout.0.get_layout(&[
Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
ATTRIBUTE_BLEND_COLOR.at_shader_location(1),
])?;
descriptor.vertex.buffers = vec![vertex_layout];
Ok(())
}
}

View File

@@ -0,0 +1,88 @@
//! Demonstrates using a custom extension to the `StandardMaterial` to modify the results of the builtin pbr shader.
use bevy::{
color::palettes::basic::RED,
pbr::{ExtendedMaterial, MaterialExtension, OpaqueRendererMethod},
prelude::*,
render::render_resource::*,
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/extended_material.wgsl";
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(MaterialPlugin::<
ExtendedMaterial<StandardMaterial, MyExtension>,
>::default())
.add_systems(Startup, setup)
.add_systems(Update, rotate_things)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, MyExtension>>>,
) {
// sphere
commands.spawn((
Mesh3d(meshes.add(Sphere::new(1.0))),
MeshMaterial3d(materials.add(ExtendedMaterial {
base: StandardMaterial {
base_color: RED.into(),
// can be used in forward or deferred mode
opaque_render_method: OpaqueRendererMethod::Auto,
// in deferred mode, only the PbrInput can be modified (uvs, color and other material properties),
// in forward mode, the output can also be modified after lighting is applied.
// see the fragment shader `extended_material.wgsl` for more info.
// Note: to run in deferred mode, you must also add a `DeferredPrepass` component to the camera and either
// change the above to `OpaqueRendererMethod::Deferred` or add the `DefaultOpaqueRendererMethod` resource.
..Default::default()
},
extension: MyExtension { quantize_steps: 3 },
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// light
commands.spawn((
DirectionalLight::default(),
Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
Rotate,
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
#[derive(Component)]
struct Rotate;
fn rotate_things(mut q: Query<&mut Transform, With<Rotate>>, time: Res<Time>) {
for mut t in &mut q {
t.rotate_y(time.delta_secs());
}
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
struct MyExtension {
// We need to ensure that the bindings of the base material and the extension do not conflict,
// so we start from binding slot 100, leaving slots 0-99 for the base material.
#[uniform(100)]
quantize_steps: u32,
}
impl MaterialExtension for MyExtension {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn deferred_fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,156 @@
//! Demonstrates bindless `ExtendedMaterial`.
use std::f32::consts::FRAC_PI_2;
use bevy::{
color::palettes::{css::RED, tailwind::GRAY_600},
pbr::{ExtendedMaterial, MaterialExtension, MeshMaterial3d},
prelude::*,
render::{
mesh::{SphereKind, SphereMeshBuilder},
render_resource::{AsBindGroup, ShaderRef, ShaderType},
},
utils::default,
};
/// The path to the example material shader.
static SHADER_ASSET_PATH: &str = "shaders/extended_material_bindless.wgsl";
/// The example bindless material extension.
///
/// As usual for material extensions, we need to avoid conflicting with both the
/// binding numbers and bindless indices of the [`StandardMaterial`], so we
/// start both values at 100 and 50 respectively.
///
/// The `#[data(50, ExampleBindlessExtensionUniform, binding_array(101))]`
/// attribute specifies that the plain old data
/// [`ExampleBindlessExtensionUniform`] will be placed into an array with
/// binding 100 and will occupy index 50 in the
/// `ExampleBindlessExtendedMaterialIndices` structure. (See the shader for the
/// definition of that structure.) That corresponds to the following shader
/// declaration:
///
/// ```wgsl
/// @group(2) @binding(100) var<storage> example_extended_material_indices:
/// array<ExampleBindlessExtendedMaterialIndices>;
/// ```
///
/// The `#[bindless(index_table(range(50..53), binding(100)))]` attribute
/// specifies that this material extension should be bindless. The `range`
/// subattribute specifies that this material extension should have its own
/// index table covering bindings 50, 51, and 52. The `binding` subattribute
/// specifies that the extended material index table should be bound to binding
/// 100. This corresponds to the following shader declarations:
///
/// ```wgsl
/// struct ExampleBindlessExtendedMaterialIndices {
/// material: u32, // 50
/// modulate_texture: u32, // 51
/// modulate_texture_sampler: u32, // 52
/// }
///
/// @group(2) @binding(100) var<storage> example_extended_material_indices:
/// array<ExampleBindlessExtendedMaterialIndices>;
/// ```
///
/// We need to use the `index_table` subattribute because the
/// [`StandardMaterial`] bindless index table is bound to binding 0 by default.
/// Thus we need to specify a different binding so that our extended bindless
/// index table doesn't conflict.
#[derive(Asset, Clone, Reflect, AsBindGroup)]
#[data(50, ExampleBindlessExtensionUniform, binding_array(101))]
#[bindless(index_table(range(50..53), binding(100)))]
struct ExampleBindlessExtension {
/// The color we're going to multiply the base color with.
modulate_color: Color,
/// The image we're going to multiply the base color with.
#[texture(51)]
#[sampler(52)]
modulate_texture: Option<Handle<Image>>,
}
/// The GPU-side data structure specifying plain old data for the material
/// extension.
#[derive(Clone, Default, ShaderType)]
struct ExampleBindlessExtensionUniform {
/// The GPU representation of the color we're going to multiply the base
/// color with.
modulate_color: Vec4,
}
impl MaterialExtension for ExampleBindlessExtension {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}
impl<'a> From<&'a ExampleBindlessExtension> for ExampleBindlessExtensionUniform {
fn from(material_extension: &'a ExampleBindlessExtension) -> Self {
// Convert the CPU `ExampleBindlessExtension` structure to its GPU
// format.
ExampleBindlessExtensionUniform {
modulate_color: LinearRgba::from(material_extension.modulate_color).to_vec4(),
}
}
}
/// The entry point.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(MaterialPlugin::<
ExtendedMaterial<StandardMaterial, ExampleBindlessExtension>,
>::default())
.add_systems(Startup, setup)
.add_systems(Update, rotate_sphere)
.run();
}
/// Creates the scene.
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, ExampleBindlessExtension>>>,
) {
// Create a gray sphere, modulated with a red-tinted Bevy logo.
commands.spawn((
Mesh3d(meshes.add(SphereMeshBuilder::new(
1.0,
SphereKind::Uv {
sectors: 20,
stacks: 20,
},
))),
MeshMaterial3d(materials.add(ExtendedMaterial {
base: StandardMaterial {
base_color: GRAY_600.into(),
..default()
},
extension: ExampleBindlessExtension {
modulate_color: RED.into(),
modulate_texture: Some(asset_server.load("textures/uv_checker_bw.png")),
},
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// Create a light.
commands.spawn((
DirectionalLight::default(),
Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// Create a camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn rotate_sphere(mut meshes: Query<&mut Transform, With<Mesh3d>>, time: Res<Time>) {
for mut transform in &mut meshes {
transform.rotation =
Quat::from_euler(EulerRot::YXZ, -time.elapsed_secs(), FRAC_PI_2 * 3.0, 0.0);
}
}

View File

@@ -0,0 +1,80 @@
//! 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,
render::render_resource::{AsBindGroup, ShaderRef},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/fallback_image_test.wgsl";
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
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((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(FallbackTestMaterial {
image_1d: None,
image_2d: None,
image_2d_array: None,
image_cube: None,
image_cube_array: None,
image_3d: None,
})),
));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::new(1.5, 0.0, 0.0), Vec3::Y),
));
}
#[derive(AsBindGroup, Debug, Clone, Asset, TypePath)]
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 {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,222 @@
//! Simple example demonstrating the use of the [`Readback`] component to read back data from the GPU
//! using both a storage buffer and texture.
use bevy::{
prelude::*,
render::{
extract_resource::{ExtractResource, ExtractResourcePlugin},
gpu_readback::{Readback, ReadbackComplete},
render_asset::{RenderAssetUsages, RenderAssets},
render_graph::{self, RenderGraph, RenderLabel},
render_resource::{
binding_types::{storage_buffer, texture_storage_2d},
*,
},
renderer::{RenderContext, RenderDevice},
storage::{GpuShaderStorageBuffer, ShaderStorageBuffer},
texture::GpuImage,
Render, RenderApp, RenderSet,
},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/gpu_readback.wgsl";
// The length of the buffer sent to the gpu
const BUFFER_LEN: usize = 16;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
GpuReadbackPlugin,
ExtractResourcePlugin::<ReadbackBuffer>::default(),
ExtractResourcePlugin::<ReadbackImage>::default(),
))
.insert_resource(ClearColor(Color::BLACK))
.add_systems(Startup, setup)
.run();
}
// We need a plugin to organize all the systems and render node required for this example
struct GpuReadbackPlugin;
impl Plugin for GpuReadbackPlugin {
fn build(&self, _app: &mut App) {}
fn finish(&self, app: &mut App) {
let render_app = app.sub_app_mut(RenderApp);
render_app.init_resource::<ComputePipeline>().add_systems(
Render,
prepare_bind_group
.in_set(RenderSet::PrepareBindGroups)
// We don't need to recreate the bind group every frame
.run_if(not(resource_exists::<GpuBufferBindGroup>)),
);
// Add the compute node as a top level node to the render graph
// This means it will only execute once per frame
render_app
.world_mut()
.resource_mut::<RenderGraph>()
.add_node(ComputeNodeLabel, ComputeNode::default());
}
}
#[derive(Resource, ExtractResource, Clone)]
struct ReadbackBuffer(Handle<ShaderStorageBuffer>);
#[derive(Resource, ExtractResource, Clone)]
struct ReadbackImage(Handle<Image>);
fn setup(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
) {
// Create a storage buffer with some data
let buffer = vec![0u32; BUFFER_LEN];
let mut buffer = ShaderStorageBuffer::from(buffer);
// We need to enable the COPY_SRC usage so we can copy the buffer to the cpu
buffer.buffer_description.usage |= BufferUsages::COPY_SRC;
let buffer = buffers.add(buffer);
// Create a storage texture with some data
let size = Extent3d {
width: BUFFER_LEN as u32,
height: 1,
..default()
};
// We create an uninitialized image since this texture will only be used for getting data out
// of the compute shader, not getting data in, so there's no reason for it to exist on the CPU
let mut image = Image::new_uninit(
size,
TextureDimension::D2,
TextureFormat::R32Uint,
RenderAssetUsages::RENDER_WORLD,
);
// We also need to enable the COPY_SRC, as well as STORAGE_BINDING so we can use it in the
// compute shader
image.texture_descriptor.usage |= TextureUsages::COPY_SRC | TextureUsages::STORAGE_BINDING;
let image = images.add(image);
// Spawn the readback components. For each frame, the data will be read back from the GPU
// asynchronously and trigger the `ReadbackComplete` event on this entity. Despawn the entity
// to stop reading back the data.
commands.spawn(Readback::buffer(buffer.clone())).observe(
|trigger: Trigger<ReadbackComplete>| {
// This matches the type which was used to create the `ShaderStorageBuffer` above,
// and is a convenient way to interpret the data.
let data: Vec<u32> = trigger.event().to_shader_type();
info!("Buffer {:?}", data);
},
);
// This is just a simple way to pass the buffer handle to the render app for our compute node
commands.insert_resource(ReadbackBuffer(buffer));
// Textures can also be read back from the GPU. Pay careful attention to the format of the
// texture, as it will affect how the data is interpreted.
commands.spawn(Readback::texture(image.clone())).observe(
|trigger: Trigger<ReadbackComplete>| {
// You probably want to interpret the data as a color rather than a `ShaderType`,
// but in this case we know the data is a single channel storage texture, so we can
// interpret it as a `Vec<u32>`
let data: Vec<u32> = trigger.event().to_shader_type();
info!("Image {:?}", data);
},
);
commands.insert_resource(ReadbackImage(image));
}
#[derive(Resource)]
struct GpuBufferBindGroup(BindGroup);
fn prepare_bind_group(
mut commands: Commands,
pipeline: Res<ComputePipeline>,
render_device: Res<RenderDevice>,
buffer: Res<ReadbackBuffer>,
image: Res<ReadbackImage>,
buffers: Res<RenderAssets<GpuShaderStorageBuffer>>,
images: Res<RenderAssets<GpuImage>>,
) {
let buffer = buffers.get(&buffer.0).unwrap();
let image = images.get(&image.0).unwrap();
let bind_group = render_device.create_bind_group(
None,
&pipeline.layout,
&BindGroupEntries::sequential((
buffer.buffer.as_entire_buffer_binding(),
image.texture_view.into_binding(),
)),
);
commands.insert_resource(GpuBufferBindGroup(bind_group));
}
#[derive(Resource)]
struct ComputePipeline {
layout: BindGroupLayout,
pipeline: CachedComputePipelineId,
}
impl FromWorld for ComputePipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
None,
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
storage_buffer::<Vec<u32>>(false),
texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly),
),
),
);
let shader = world.load_asset(SHADER_ASSET_PATH);
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("GPU readback compute shader".into()),
layout: vec![layout.clone()],
push_constant_ranges: Vec::new(),
shader: shader.clone(),
shader_defs: Vec::new(),
entry_point: "main".into(),
zero_initialize_workgroup_memory: false,
});
ComputePipeline { layout, pipeline }
}
}
/// Label to identify the node in the render graph
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct ComputeNodeLabel;
/// The node that will execute the compute shader
#[derive(Default)]
struct ComputeNode {}
impl render_graph::Node for ComputeNode {
fn run(
&self,
_graph: &mut render_graph::RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), render_graph::NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<ComputePipeline>();
let bind_group = world.resource::<GpuBufferBindGroup>();
if let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(pipeline.pipeline) {
let mut pass =
render_context
.command_encoder()
.begin_compute_pass(&ComputePassDescriptor {
label: Some("GPU readback compute pass"),
..default()
});
pass.set_bind_group(0, &bind_group.0, &[]);
pass.set_pipeline(init_pipeline);
pass.dispatch_workgroups(BUFFER_LEN as u32, 1, 1);
}
Ok(())
}
}

View File

@@ -0,0 +1,101 @@
//! A shader that uses "shaders defs", which selectively toggle parts of a shader.
use bevy::{
pbr::{MaterialPipeline, MaterialPipelineKey},
prelude::*,
reflect::TypePath,
render::{
mesh::MeshVertexBufferLayoutRef,
render_resource::{
AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError,
},
},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/shader_defs.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
// blue cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {
color: LinearRgba::BLUE,
is_red: false,
})),
Transform::from_xyz(-1.0, 0.5, 0.0),
));
// red cube (with green color overridden by the IS_RED "shader def")
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {
color: LinearRgba::GREEN,
is_red: true,
})),
Transform::from_xyz(1.0, 0.5, 0.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
if key.bind_group_data.is_red {
let fragment = descriptor.fragment.as_mut().unwrap();
fragment.shader_defs.push("IS_RED".into());
}
Ok(())
}
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
#[bind_group_data(CustomMaterialKey)]
struct CustomMaterial {
#[uniform(0)]
color: LinearRgba,
is_red: bool,
}
// This key is used to identify a specific permutation of this material pipeline.
// In this case, we specialize on whether or not to configure the "IS_RED" shader def.
// Specialization keys should be kept as small / cheap to hash as possible,
// as they will be used to look up the pipeline for each drawn entity with this material type.
#[derive(Eq, PartialEq, Hash, Clone)]
struct CustomMaterialKey {
is_red: bool,
}
impl From<&CustomMaterial> for CustomMaterialKey {
fn from(material: &CustomMaterial) -> Self {
Self {
is_red: material.is_red,
}
}
}

View File

@@ -0,0 +1,65 @@
//! A shader and a material that uses it.
use bevy::{
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/custom_material.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
asset_server: Res<AssetServer>,
) {
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {
color: LinearRgba::BLUE,
color_texture: Some(asset_server.load("branding/icon.png")),
alpha_mode: AlphaMode::Blend,
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
// This struct defines the data that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {
#[uniform(0)]
color: LinearRgba,
#[texture(1)]
#[sampler(2)]
color_texture: Option<Handle<Image>>,
alpha_mode: AlphaMode,
}
/// The Material trait is very configurable, but comes with sensible defaults for all methods.
/// You only need to implement functions for features that need non-default behavior. See the Material api docs for details!
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn alpha_mode(&self) -> AlphaMode {
self.alpha_mode
}
}

View File

@@ -0,0 +1,64 @@
//! A shader and a material that uses it.
use bevy::{
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef},
sprite::{AlphaMode2d, Material2d, Material2dPlugin},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/custom_material_2d.wgsl";
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
Material2dPlugin::<CustomMaterial>::default(),
))
.add_systems(Startup, setup)
.run();
}
// Setup a simple 2d scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
asset_server: Res<AssetServer>,
) {
// camera
commands.spawn(Camera2d);
// quad
commands.spawn((
Mesh2d(meshes.add(Rectangle::default())),
MeshMaterial2d(materials.add(CustomMaterial {
color: LinearRgba::BLUE,
color_texture: Some(asset_server.load("branding/icon.png")),
})),
Transform::default().with_scale(Vec3::splat(128.)),
));
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {
#[uniform(0)]
color: LinearRgba,
#[texture(1)]
#[sampler(2)]
color_texture: Option<Handle<Image>>,
}
/// The Material2d trait is very configurable, but comes with sensible defaults for all methods.
/// You only need to implement functions for features that need non-default behavior. See the Material2d api docs for details!
impl Material2d for CustomMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Mask(0.5)
}
}

View File

@@ -0,0 +1,87 @@
//! A material that uses bindless textures.
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderRef, ShaderType};
const SHADER_ASSET_PATH: &str = "shaders/bindless_material.wgsl";
// `#[bindless(limit(4))]` indicates that we want Bevy to group materials into
// bind groups of at most 4 materials each.
// Note that we use the structure-level `#[uniform]` attribute to supply
// ordinary data to the shader.
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
#[uniform(0, BindlessMaterialUniform, binding_array(10))]
#[bindless(limit(4))]
struct BindlessMaterial {
color: LinearRgba,
// This will be exposed to the shader as a binding array of 4 textures and a
// binding array of 4 samplers.
#[texture(1)]
#[sampler(2)]
color_texture: Option<Handle<Image>>,
}
// This buffer will be presented to the shader as `@binding(10)`.
#[derive(ShaderType)]
struct BindlessMaterialUniform {
color: LinearRgba,
}
impl<'a> From<&'a BindlessMaterial> for BindlessMaterialUniform {
fn from(material: &'a BindlessMaterial) -> Self {
BindlessMaterialUniform {
color: material.color,
}
}
}
// The entry point.
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<BindlessMaterial>::default(),
))
.add_systems(Startup, setup)
.run();
}
// Creates a simple scene.
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BindlessMaterial>>,
asset_server: Res<AssetServer>,
) {
// Add a cube with a blue tinted texture.
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(BindlessMaterial {
color: LinearRgba::BLUE,
color_texture: Some(asset_server.load("branding/bevy_logo_dark.png")),
})),
Transform::from_xyz(-2.0, 0.5, 0.0),
));
// Add a cylinder with a red tinted texture.
commands.spawn((
Mesh3d(meshes.add(Cylinder::default())),
MeshMaterial3d(materials.add(BindlessMaterial {
color: LinearRgba::RED,
color_texture: Some(asset_server.load("branding/bevy_logo_light.png")),
})),
Transform::from_xyz(2.0, 0.5, 0.0),
));
// Add a camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
impl Material for BindlessMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,91 @@
//! A shader that uses the GLSL shading language.
use bevy::{
pbr::{MaterialPipeline, MaterialPipelineKey},
prelude::*,
reflect::TypePath,
render::{
mesh::MeshVertexBufferLayoutRef,
render_resource::{
AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError,
},
},
};
/// This example uses shader source files from the assets subdirectory
const VERTEX_SHADER_ASSET_PATH: &str = "shaders/custom_material.vert";
const FRAGMENT_SHADER_ASSET_PATH: &str = "shaders/custom_material.frag";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
asset_server: Res<AssetServer>,
) {
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {
color: LinearRgba::BLUE,
color_texture: Some(asset_server.load("branding/icon.png")),
alpha_mode: AlphaMode::Blend,
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Clone)]
struct CustomMaterial {
#[uniform(0)]
color: LinearRgba,
#[texture(1)]
#[sampler(2)]
color_texture: Option<Handle<Image>>,
alpha_mode: AlphaMode,
}
/// The Material trait is very configurable, but comes with sensible defaults for all methods.
/// You only need to implement functions for features that need non-default behavior. See the Material api docs for details!
/// When using the GLSL shading language for your shader, the specialize method must be overridden.
impl Material for CustomMaterial {
fn vertex_shader() -> ShaderRef {
VERTEX_SHADER_ASSET_PATH.into()
}
fn fragment_shader() -> ShaderRef {
FRAGMENT_SHADER_ASSET_PATH.into()
}
fn alpha_mode(&self) -> AlphaMode {
self.alpha_mode
}
// Bevy assumes by default that vertex shaders use the "vertex" entry point
// and fragment shaders use the "fragment" entry point (for WGSL shaders).
// GLSL uses "main" as the entry point, so we must override the defaults here
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
_key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
descriptor.vertex.entry_point = "main".into();
descriptor.fragment.as_mut().unwrap().entry_point = "main".into();
Ok(())
}
}

View File

@@ -0,0 +1,73 @@
//! A shader that samples a texture with view-independent UV coordinates.
use bevy::{
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef},
};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/custom_material_screenspace_texture.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.add_systems(Update, rotate_camera)
.run();
}
#[derive(Component)]
struct MainCamera;
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut custom_materials: ResMut<Assets<CustomMaterial>>,
mut standard_materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
MeshMaterial3d(standard_materials.add(Color::srgb(0.3, 0.5, 0.3))),
));
commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 8.0, 4.0)));
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(custom_materials.add(CustomMaterial {
texture: asset_server.load(
"models/FlightHelmet/FlightHelmet_Materials_LensesMat_OcclusionRoughMetal.png",
),
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(4.0, 2.5, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
MainCamera,
));
}
fn rotate_camera(mut cam_transform: Single<&mut Transform, With<MainCamera>>, time: Res<Time>) {
cam_transform.rotate_around(
Vec3::ZERO,
Quat::from_axis_angle(Vec3::Y, 45f32.to_radians() * time.delta_secs()),
);
cam_transform.look_at(Vec3::ZERO, Vec3::Y);
}
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {
#[texture(0)]
#[sampler(1)]
texture: Handle<Image>,
}
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,135 @@
//! A shader that uses the WESL shading language.
use bevy::{
pbr::{MaterialPipeline, MaterialPipelineKey},
prelude::*,
reflect::TypePath,
render::{
mesh::MeshVertexBufferLayoutRef,
render_resource::{
AsBindGroup, RenderPipelineDescriptor, ShaderDefVal, ShaderRef,
SpecializedMeshPipelineError,
},
},
};
/// This example uses shader source files from the assets subdirectory
const FRAGMENT_SHADER_ASSET_PATH: &str = "shaders/custom_material.wesl";
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<CustomMaterial>::default(),
CustomMaterialPlugin,
))
.add_systems(Startup, setup)
.add_systems(Update, update)
.run();
}
/// A plugin that loads the custom material shader
pub struct CustomMaterialPlugin;
/// An example utility shader that is used by the custom material
#[expect(
dead_code,
reason = "used to kept a strong handle, shader is referenced by the material"
)]
#[derive(Resource)]
struct UtilityShader(Handle<Shader>);
impl Plugin for CustomMaterialPlugin {
fn build(&self, app: &mut App) {
let handle = app
.world_mut()
.resource_mut::<AssetServer>()
.load::<Shader>("shaders/util.wesl");
app.insert_resource(UtilityShader(handle));
}
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {
time: Vec4::ZERO,
party_mode: false,
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn update(
time: Res<Time>,
mut query: Query<(&MeshMaterial3d<CustomMaterial>, &mut Transform)>,
mut materials: ResMut<Assets<CustomMaterial>>,
keys: Res<ButtonInput<KeyCode>>,
) {
for (material, mut transform) in query.iter_mut() {
let material = materials.get_mut(material).unwrap();
material.time.x = time.elapsed_secs();
if keys.just_pressed(KeyCode::Space) {
material.party_mode = !material.party_mode;
}
if material.party_mode {
transform.rotate(Quat::from_rotation_y(0.005));
}
}
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Clone)]
#[bind_group_data(CustomMaterialKey)]
struct CustomMaterial {
// Needed for 16 bit alignment in WebGL2
#[uniform(0)]
time: Vec4,
party_mode: bool,
}
#[derive(Eq, PartialEq, Hash, Clone)]
struct CustomMaterialKey {
party_mode: bool,
}
impl From<&CustomMaterial> for CustomMaterialKey {
fn from(material: &CustomMaterial) -> Self {
Self {
party_mode: material.party_mode,
}
}
}
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
FRAGMENT_SHADER_ASSET_PATH.into()
}
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
let fragment = descriptor.fragment.as_mut().unwrap();
fragment.shader_defs.push(ShaderDefVal::Bool(
"PARTY_MODE".to_string(),
key.bind_group_data.party_mode,
));
Ok(())
}
}

View File

@@ -0,0 +1,241 @@
//! Bevy has an optional prepass that is controlled per-material. A prepass is a rendering pass that runs before the main pass.
//! It will optionally generate various view textures. Currently it supports depth, normal, and motion vector textures.
//! The textures are not generated for any material using alpha blending.
use bevy::{
core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass},
pbr::{NotShadowCaster, PbrPlugin},
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef, ShaderType},
};
/// This example uses a shader source file from the assets subdirectory
const PREPASS_SHADER_ASSET_PATH: &str = "shaders/show_prepass.wgsl";
const MATERIAL_SHADER_ASSET_PATH: &str = "shaders/custom_material.wgsl";
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(PbrPlugin {
// The prepass is enabled by default on the StandardMaterial,
// but you can disable it if you need to.
//
// prepass_enabled: false,
..default()
}),
MaterialPlugin::<CustomMaterial>::default(),
MaterialPlugin::<PrepassOutputMaterial> {
// This material only needs to read the prepass textures,
// but the meshes using it should not contribute to the prepass render, so we can disable it.
prepass_enabled: false,
..default()
},
))
.add_systems(Startup, setup)
.add_systems(Update, (rotate, toggle_prepass_view))
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
mut std_materials: ResMut<Assets<StandardMaterial>>,
mut depth_materials: ResMut<Assets<PrepassOutputMaterial>>,
asset_server: Res<AssetServer>,
) {
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 3., 5.0).looking_at(Vec3::ZERO, Vec3::Y),
// Disabling MSAA for maximum compatibility. Shader prepass with MSAA needs GPU capability MULTISAMPLED_SHADING
Msaa::Off,
// To enable the prepass you need to add the components associated with the ones you need
// This will write the depth buffer to a texture that you can use in the main pass
DepthPrepass,
// This will generate a texture containing world normals (with normal maps applied)
NormalPrepass,
// This will generate a texture containing screen space pixel motion vectors
MotionVectorPrepass,
));
// plane
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
MeshMaterial3d(std_materials.add(Color::srgb(0.3, 0.5, 0.3))),
));
// A quad that shows the outputs of the prepass
// To make it easy, we just draw a big quad right in front of the camera.
// For a real application, this isn't ideal.
commands.spawn((
Mesh3d(meshes.add(Rectangle::new(20.0, 20.0))),
MeshMaterial3d(depth_materials.add(PrepassOutputMaterial {
settings: ShowPrepassSettings::default(),
})),
Transform::from_xyz(-0.75, 1.25, 3.0).looking_at(Vec3::new(2.0, -2.5, -5.0), Vec3::Y),
NotShadowCaster,
));
// Opaque cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {
color: LinearRgba::WHITE,
color_texture: Some(asset_server.load("branding/icon.png")),
alpha_mode: AlphaMode::Opaque,
})),
Transform::from_xyz(-1.0, 0.5, 0.0),
Rotates,
));
// Cube with alpha mask
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(std_materials.add(StandardMaterial {
alpha_mode: AlphaMode::Mask(1.0),
base_color_texture: Some(asset_server.load("branding/icon.png")),
..default()
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// Cube with alpha blending.
// Transparent materials are ignored by the prepass
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(CustomMaterial {
color: LinearRgba::WHITE,
color_texture: Some(asset_server.load("branding/icon.png")),
alpha_mode: AlphaMode::Blend,
})),
Transform::from_xyz(1.0, 0.5, 0.0),
));
// light
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
commands
.spawn((
Text::default(),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
))
.with_children(|p| {
p.spawn(TextSpan::new("Prepass Output: transparent\n"));
p.spawn(TextSpan::new("\n\n"));
p.spawn(TextSpan::new("Controls\n"));
p.spawn(TextSpan::new("---------------\n"));
p.spawn(TextSpan::new("Space - Change output\n"));
});
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {
#[uniform(0)]
color: LinearRgba,
#[texture(1)]
#[sampler(2)]
color_texture: Option<Handle<Image>>,
alpha_mode: AlphaMode,
}
/// Not shown in this example, but if you need to specialize your material, the specialize
/// function will also be used by the prepass
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
MATERIAL_SHADER_ASSET_PATH.into()
}
fn alpha_mode(&self) -> AlphaMode {
self.alpha_mode
}
// You can override the default shaders used in the prepass if your material does
// anything not supported by the default prepass
// fn prepass_fragment_shader() -> ShaderRef {
// "shaders/custom_material.wgsl".into()
// }
}
#[derive(Component)]
struct Rotates;
fn rotate(mut q: Query<&mut Transform, With<Rotates>>, time: Res<Time>) {
for mut t in q.iter_mut() {
let rot = (ops::sin(time.elapsed_secs()) * 0.5 + 0.5) * std::f32::consts::PI * 2.0;
t.rotation = Quat::from_rotation_z(rot);
}
}
#[derive(Debug, Clone, Default, ShaderType)]
struct ShowPrepassSettings {
show_depth: u32,
show_normals: u32,
show_motion_vectors: u32,
padding_1: u32,
padding_2: u32,
}
// This shader simply loads the prepass texture and outputs it directly
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct PrepassOutputMaterial {
#[uniform(0)]
settings: ShowPrepassSettings,
}
impl Material for PrepassOutputMaterial {
fn fragment_shader() -> ShaderRef {
PREPASS_SHADER_ASSET_PATH.into()
}
// This needs to be transparent in order to show the scene behind the mesh
fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Blend
}
}
/// Every time you press space, it will cycle between transparent, depth and normals view
fn toggle_prepass_view(
mut prepass_view: Local<u32>,
keycode: Res<ButtonInput<KeyCode>>,
material_handle: Single<&MeshMaterial3d<PrepassOutputMaterial>>,
mut materials: ResMut<Assets<PrepassOutputMaterial>>,
text: Single<Entity, With<Text>>,
mut writer: TextUiWriter,
) {
if keycode.just_pressed(KeyCode::Space) {
*prepass_view = (*prepass_view + 1) % 4;
let label = match *prepass_view {
0 => "transparent",
1 => "depth",
2 => "normals",
3 => "motion vectors",
_ => unreachable!(),
};
let text = *text;
*writer.text(text, 1) = format!("Prepass Output: {label}\n");
writer.for_each_color(text, |mut color| {
color.0 = Color::WHITE;
});
let mat = materials.get_mut(*material_handle).unwrap();
mat.settings.show_depth = (*prepass_view == 1) as u32;
mat.settings.show_normals = (*prepass_view == 2) as u32;
mat.settings.show_motion_vectors = (*prepass_view == 3) as u32;
}
}

View File

@@ -0,0 +1,469 @@
//! Demonstrates how to define and use specialized mesh pipeline
//!
//! This example shows how to use the built-in [`SpecializedMeshPipeline`]
//! functionality with a custom [`RenderCommand`] to allow custom mesh rendering with
//! more flexibility than the material api.
//!
//! [`SpecializedMeshPipeline`] let's you customize the entire pipeline used when rendering a mesh.
use bevy::{
core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT},
ecs::{component::Tick, system::StaticSystemParam},
math::{vec3, vec4},
pbr::{
DrawMesh, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey, RenderMeshInstances,
SetMeshBindGroup, SetMeshViewBindGroup,
},
prelude::*,
render::{
batching::{
gpu_preprocessing::{
self, PhaseBatchedInstanceBuffers, PhaseIndirectParametersBuffers,
PreprocessWorkItem, UntypedPhaseBatchedInstanceBuffers,
},
GetBatchData, GetFullBatchData,
},
experimental::occlusion_culling::OcclusionCulling,
extract_component::{ExtractComponent, ExtractComponentPlugin},
mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology, RenderMesh},
render_asset::{RenderAssetUsages, RenderAssets},
render_phase::{
AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, SetItemPipeline,
ViewBinnedRenderPhases,
},
render_resource::{
ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, Face, FragmentState,
FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState,
RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
SpecializedMeshPipelines, TextureFormat, VertexState,
},
view::NoIndirectDrawing,
view::{self, ExtractedView, RenderVisibleEntities, ViewTarget, VisibilityClass},
Render, RenderApp, RenderSet,
},
};
const SHADER_ASSET_PATH: &str = "shaders/specialized_mesh_pipeline.wgsl";
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(CustomRenderedMeshPipelinePlugin)
.add_systems(Startup, setup)
.run();
}
/// Spawns the objects in the scene.
fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
// Build a custom triangle mesh with colors
// We define a custom mesh because the examples only uses a limited
// set of vertex attributes for simplicity
let mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_indices(Indices::U32(vec![0, 1, 2]))
.with_inserted_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![
vec3(-0.5, -0.5, 0.0),
vec3(0.5, -0.5, 0.0),
vec3(0.0, 0.25, 0.0),
],
)
.with_inserted_attribute(
Mesh::ATTRIBUTE_COLOR,
vec![
vec4(1.0, 0.0, 0.0, 1.0),
vec4(0.0, 1.0, 0.0, 1.0),
vec4(0.0, 0.0, 1.0, 1.0),
],
);
// spawn 3 triangles to show that batching works
for (x, y) in [-0.5, 0.0, 0.5].into_iter().zip([-0.25, 0.5, -0.25]) {
// Spawn an entity with all the required components for it to be rendered with our custom pipeline
commands.spawn((
// We use a marker component to identify the mesh that will be rendered
// with our specialized pipeline
CustomRenderedEntity,
// We need to add the mesh handle to the entity
Mesh3d(meshes.add(mesh.clone())),
Transform::from_xyz(x, y, 0.0),
));
}
// Spawn the camera.
commands.spawn((
Camera3d::default(),
// Move the camera back a bit to see all the triangles
Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
// When writing custom rendering code it's generally recommended to use a plugin.
// The main reason for this is that it gives you access to the finish() hook
// which is called after rendering resources are initialized.
struct CustomRenderedMeshPipelinePlugin;
impl Plugin for CustomRenderedMeshPipelinePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ExtractComponentPlugin::<CustomRenderedEntity>::default());
// We make sure to add these to the render app, not the main app.
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
// This is needed to tell bevy about your custom pipeline
.init_resource::<SpecializedMeshPipelines<CustomMeshPipeline>>()
// We need to use a custom draw command so we need to register it
.add_render_command::<Opaque3d, DrawSpecializedPipelineCommands>()
.add_systems(Render, queue_custom_mesh_pipeline.in_set(RenderSet::Queue));
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
// Creating this pipeline needs the RenderDevice and RenderQueue
// which are only available once rendering plugins are initialized.
render_app.init_resource::<CustomMeshPipeline>();
}
}
/// A marker component that represents an entity that is to be rendered using
/// our specialized pipeline.
///
/// Note the [`ExtractComponent`] trait implementation: this is necessary to
/// tell Bevy that this object should be pulled into the render world. Also note
/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
/// that entities with this component need to be examined for visibility.
#[derive(Clone, Component, ExtractComponent)]
#[require(VisibilityClass)]
#[component(on_add = view::add_visibility_class::<CustomRenderedEntity>)]
struct CustomRenderedEntity;
/// The custom draw commands that Bevy executes for each entity we enqueue into
/// the render phase.
type DrawSpecializedPipelineCommands = (
// Set the pipeline
SetItemPipeline,
// Set the view uniform at bind group 0
SetMeshViewBindGroup<0>,
// Set the mesh uniform at bind group 1
SetMeshBindGroup<1>,
// Draw the mesh
DrawMesh,
);
// This contains the state needed to specialize a mesh pipeline
#[derive(Resource)]
struct CustomMeshPipeline {
/// The base mesh pipeline defined by bevy
///
/// This isn't required, but if you want to use a bevy `Mesh` it's easier when you
/// have access to the base `MeshPipeline` that bevy already defines
mesh_pipeline: MeshPipeline,
/// Stores the shader used for this pipeline directly on the pipeline.
/// This isn't required, it's only done like this for simplicity.
shader_handle: Handle<Shader>,
}
impl FromWorld for CustomMeshPipeline {
fn from_world(world: &mut World) -> Self {
// Load the shader
let shader_handle: Handle<Shader> = world.resource::<AssetServer>().load(SHADER_ASSET_PATH);
Self {
mesh_pipeline: MeshPipeline::from_world(world),
shader_handle,
}
}
}
impl SpecializedMeshPipeline for CustomMeshPipeline {
/// Pipeline use keys to determine how to specialize it.
/// The key is also used by the pipeline cache to determine if
/// it needs to create a new pipeline or not
///
/// In this example we just use the base `MeshPipelineKey` defined by bevy, but this could be anything.
/// For example, if you want to make a pipeline with a procedural shader you could add the Handle<Shader> to the key.
type Key = MeshPipelineKey;
fn specialize(
&self,
mesh_key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
// Define the vertex attributes based on a standard bevy [`Mesh`]
let mut vertex_attributes = Vec::new();
if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
// Make sure this matches the shader location
vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
}
if layout.0.contains(Mesh::ATTRIBUTE_COLOR) {
// Make sure this matches the shader location
vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(1));
}
// This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes
let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
Ok(RenderPipelineDescriptor {
label: Some("Specialized Mesh Pipeline".into()),
layout: vec![
// Bind group 0 is the view uniform
self.mesh_pipeline
.get_view_layout(MeshPipelineViewLayoutKey::from(mesh_key))
.clone(),
// Bind group 1 is the mesh uniform
self.mesh_pipeline.mesh_layouts.model_only.clone(),
],
push_constant_ranges: vec![],
vertex: VertexState {
shader: self.shader_handle.clone(),
shader_defs: vec![],
entry_point: "vertex".into(),
// Customize how to store the meshes' vertex attributes in the vertex buffer
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: self.shader_handle.clone(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
// This isn't required, but bevy supports HDR and non-HDR rendering
// so it's generally recommended to specialize the pipeline for that
format: if mesh_key.contains(MeshPipelineKey::HDR) {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
// For this example we only use opaque meshes,
// but if you wanted to use alpha blending you would need to set it here
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: mesh_key.primitive_topology(),
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
polygon_mode: PolygonMode::Fill,
..default()
},
// Note that if your view has no depth buffer this will need to be
// changed.
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: CompareFunction::GreaterEqual,
stencil: default(),
bias: default(),
}),
// It's generally recommended to specialize your pipeline for MSAA,
// but it's not always possible
multisample: MultisampleState {
count: mesh_key.msaa_samples(),
..MultisampleState::default()
},
zero_initialize_workgroup_memory: false,
})
}
}
/// A render-world system that enqueues the entity with custom rendering into
/// the opaque render phases of each view.
fn queue_custom_mesh_pipeline(
pipeline_cache: Res<PipelineCache>,
custom_mesh_pipeline: Res<CustomMeshPipeline>,
(mut opaque_render_phases, opaque_draw_functions): (
ResMut<ViewBinnedRenderPhases<Opaque3d>>,
Res<DrawFunctions<Opaque3d>>,
),
mut specialized_mesh_pipelines: ResMut<SpecializedMeshPipelines<CustomMeshPipeline>>,
views: Query<(
&RenderVisibleEntities,
&ExtractedView,
&Msaa,
Has<NoIndirectDrawing>,
Has<OcclusionCulling>,
)>,
(render_meshes, render_mesh_instances): (
Res<RenderAssets<RenderMesh>>,
Res<RenderMeshInstances>,
),
param: StaticSystemParam<<MeshPipeline as GetBatchData>::Param>,
mut phase_batched_instance_buffers: ResMut<
PhaseBatchedInstanceBuffers<Opaque3d, <MeshPipeline as GetBatchData>::BufferData>,
>,
mut phase_indirect_parameters_buffers: ResMut<PhaseIndirectParametersBuffers<Opaque3d>>,
mut change_tick: Local<Tick>,
) {
let system_param_item = param.into_inner();
let UntypedPhaseBatchedInstanceBuffers {
ref mut data_buffer,
ref mut work_item_buffers,
ref mut late_indexed_indirect_parameters_buffer,
ref mut late_non_indexed_indirect_parameters_buffer,
..
} = phase_batched_instance_buffers.buffers;
// Get the id for our custom draw function
let draw_function_id = opaque_draw_functions
.read()
.id::<DrawSpecializedPipelineCommands>();
// Render phases are per-view, so we need to iterate over all views so that
// the entity appears in them. (In this example, we have only one view, but
// it's good practice to loop over all views anyway.)
for (view_visible_entities, view, msaa, no_indirect_drawing, gpu_occlusion_culling) in
views.iter()
{
let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
continue;
};
// Create *work item buffers* if necessary. Work item buffers store the
// indices of meshes that are to be rendered when indirect drawing is
// enabled.
let work_item_buffer = gpu_preprocessing::get_or_create_work_item_buffer::<Opaque3d>(
work_item_buffers,
view.retained_view_entity,
no_indirect_drawing,
gpu_occlusion_culling,
);
// Initialize those work item buffers in preparation for this new frame.
gpu_preprocessing::init_work_item_buffers(
work_item_buffer,
late_indexed_indirect_parameters_buffer,
late_non_indexed_indirect_parameters_buffer,
);
// Create the key based on the view. In this case we only care about MSAA and HDR
let view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
| MeshPipelineKey::from_hdr(view.hdr);
// Set up a slot to hold information about the batch set we're going to
// create. If there are any of our custom meshes in the scene, we'll
// need this information in order for Bevy to kick off the rendering.
let mut mesh_batch_set_info = None;
// Find all the custom rendered entities that are visible from this
// view.
for &(render_entity, visible_entity) in
view_visible_entities.get::<CustomRenderedEntity>().iter()
{
// Get the mesh instance
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity)
else {
continue;
};
// Get the mesh data
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
// Specialize the key for the current mesh entity
// For this example we only specialize based on the mesh topology
// but you could have more complex keys and that's where you'd need to create those keys
let mut mesh_key = view_key;
mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
// Initialize the batch set information if this was the first custom
// mesh we saw. We'll need that information later to create the
// batch set.
if mesh_batch_set_info.is_none() {
mesh_batch_set_info = Some(MeshBatchSetInfo {
indirect_parameters_index: phase_indirect_parameters_buffers
.buffers
.allocate(mesh.indexed(), 1),
is_indexed: mesh.indexed(),
});
}
let mesh_info = mesh_batch_set_info.unwrap();
// Allocate some input and output indices. We'll need these to
// create the *work item* below.
let Some(input_index) =
MeshPipeline::get_binned_index(&system_param_item, visible_entity)
else {
continue;
};
let output_index = data_buffer.add() as u32;
// Finally, we can specialize the pipeline based on the key
let pipeline_id = specialized_mesh_pipelines
.specialize(
&pipeline_cache,
&custom_mesh_pipeline,
mesh_key,
&mesh.layout,
)
// This should never with this example, but if your pipeline specialization
// can fail you need to handle the error here
.expect("Failed to specialize mesh pipeline");
// Bump the change tick so that Bevy is forced to rebuild the bin.
let next_change_tick = change_tick.get() + 1;
change_tick.set(next_change_tick);
// Add the mesh with our specialized pipeline
opaque_phase.add(
Opaque3dBatchSetKey {
draw_function: draw_function_id,
pipeline: pipeline_id,
material_bind_group_index: None,
vertex_slab: default(),
index_slab: None,
lightmap_slab: None,
},
// The asset ID is arbitrary; we simply use [`AssetId::invalid`],
// but you can use anything you like. Note that the asset ID need
// not be the ID of a [`Mesh`].
Opaque3dBinKey {
asset_id: AssetId::<Mesh>::invalid().untyped(),
},
(render_entity, visible_entity),
mesh_instance.current_uniform_index,
// This example supports batching, but if your pipeline doesn't
// support it you can use `BinnedRenderPhaseType::UnbatchableMesh`
BinnedRenderPhaseType::BatchableMesh,
*change_tick,
);
// Create a *work item*. A work item tells the Bevy renderer to
// transform the mesh on GPU.
work_item_buffer.push(
mesh.indexed(),
PreprocessWorkItem {
input_index: input_index.into(),
output_or_indirect_parameters_index: if no_indirect_drawing {
output_index
} else {
mesh_info.indirect_parameters_index
},
},
);
}
// Now if there were any meshes, we need to add a command to the
// indirect parameters buffer, so that the renderer will end up
// enqueuing a command to draw the mesh.
if let Some(mesh_info) = mesh_batch_set_info {
phase_indirect_parameters_buffers
.buffers
.add_batch_set(mesh_info.is_indexed, mesh_info.indirect_parameters_index);
}
}
}
// If we end up having any custom meshes to draw, this contains information
// needed to create the batch set.
#[derive(Clone, Copy)]
struct MeshBatchSetInfo {
/// The first index of the mesh batch in the indirect parameters buffer.
indirect_parameters_index: u32,
/// Whether the mesh is indexed (has an index buffer).
is_indexed: bool,
}

View File

@@ -0,0 +1,114 @@
//! This example demonstrates how to use a storage buffer with `AsBindGroup` in a custom material.
use bevy::{
prelude::*,
reflect::TypePath,
render::{
mesh::MeshTag,
render_resource::{AsBindGroup, ShaderRef},
storage::ShaderStorageBuffer,
},
};
const SHADER_ASSET_PATH: &str = "shaders/storage_buffer.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, MaterialPlugin::<CustomMaterial>::default()))
.add_systems(Startup, setup)
.add_systems(Update, update)
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
// Example data for the storage buffer
let color_data: Vec<[f32; 4]> = vec![
[1.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
[0.0, 1.0, 1.0, 1.0],
];
let colors = buffers.add(ShaderStorageBuffer::from(color_data));
let mesh_handle = meshes.add(Cuboid::from_size(Vec3::splat(0.3)));
// Create the custom material with the storage buffer
let material_handle = materials.add(CustomMaterial {
colors: colors.clone(),
});
commands.insert_resource(CustomMaterialHandle(material_handle.clone()));
// Spawn cubes with the custom material
let mut current_color_id: u32 = 0;
for i in -6..=6 {
for j in -3..=3 {
commands.spawn((
Mesh3d(mesh_handle.clone()),
MeshMaterial3d(material_handle.clone()),
MeshTag(current_color_id % 5),
Transform::from_xyz(i as f32, j as f32, 0.0),
));
current_color_id += 1;
}
}
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
// Update the material color by time
fn update(
time: Res<Time>,
material_handles: Res<CustomMaterialHandle>,
mut materials: ResMut<Assets<CustomMaterial>>,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
) {
let material = materials.get_mut(&material_handles.0).unwrap();
let buffer = buffers.get_mut(&material.colors).unwrap();
buffer.set_data(
(0..5)
.map(|i| {
let t = time.elapsed_secs() * 5.0;
[
ops::sin(t + i as f32) / 2.0 + 0.5,
ops::sin(t + i as f32 + 2.0) / 2.0 + 0.5,
ops::sin(t + i as f32 + 4.0) / 2.0 + 0.5,
1.0,
]
})
.collect::<Vec<[f32; 4]>>()
.as_slice(),
);
}
// Holds handles to the custom materials
#[derive(Resource)]
struct CustomMaterialHandle(Handle<CustomMaterial>);
// This struct defines the data that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct CustomMaterial {
#[storage(0, read_only)]
colors: Handle<ShaderStorageBuffer>,
}
impl Material for CustomMaterial {
fn vertex_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}

View File

@@ -0,0 +1,197 @@
//! A shader that binds several textures onto one
//! `binding_array<texture<f32>>` shader binding slot and sample non-uniformly.
use bevy::{
ecs::system::{lifetimeless::SRes, SystemParamItem},
prelude::*,
reflect::TypePath,
render::{
render_asset::RenderAssets,
render_resource::{
binding_types::{sampler, texture_2d},
*,
},
renderer::RenderDevice,
texture::{FallbackImage, GpuImage},
RenderApp,
},
};
use std::{num::NonZero, process::exit};
/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/texture_binding_array.wgsl";
fn main() {
let mut app = App::new();
app.add_plugins((
DefaultPlugins.set(ImagePlugin::default_nearest()),
GpuFeatureSupportChecker,
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,
];
struct GpuFeatureSupportChecker;
impl Plugin for GpuFeatureSupportChecker {
fn build(&self, _app: &mut App) {}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
let render_device = render_app.world().resource::<RenderDevice>();
// Check if the device support the required feature. If not, exit the example.
// In a real application, you should setup a fallback for the missing 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"
);
exit(1);
}
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BindlessMaterial>>,
asset_server: Res<AssetServer>,
) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
));
// load 16 textures
let textures: Vec<_> = TILE_ID
.iter()
.map(|id| asset_server.load(format!("textures/rpg/tiles/generic-rpg-tile{id:0>2}.png")))
.collect();
// a cube with multiple textures
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(BindlessMaterial { textures })),
));
}
#[derive(Asset, TypePath, Debug, Clone)]
struct BindlessMaterial {
textures: Vec<Handle<Image>>,
}
impl AsBindGroup for BindlessMaterial {
type Data = ();
type Param = (SRes<RenderAssets<GpuImage>>, SRes<FallbackImage>);
fn as_bind_group(
&self,
layout: &BindGroupLayout,
render_device: &RenderDevice,
(image_assets, fallback_image): &mut SystemParamItem<'_, '_, Self::Param>,
) -> 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 fallback_image = &fallback_image.d2;
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(
"bindless_material_bind_group",
layout,
&BindGroupEntries::sequential((&textures[..], &fallback_image.sampler)),
);
Ok(PreparedBindGroup {
bindings: BindingResources(vec![]),
bind_group,
data: (),
})
}
fn unprepared_bind_group(
&self,
_layout: &BindGroupLayout,
_render_device: &RenderDevice,
_param: &mut SystemParamItem<'_, '_, Self::Param>,
_force_no_bindless: bool,
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError> {
// We implement `as_bind_group`` directly because bindless texture
// arrays can't be owned.
// Or rather, they can be owned, but then you can't make a `&'a [&'a
// TextureView]` from a vec of them in `get_binding()`.
Err(AsBindGroupError::CreateBindGroupDirectly)
}
fn bind_group_layout_entries(_: &RenderDevice, _: bool) -> Vec<BindGroupLayoutEntry>
where
Self: Sized,
{
BindGroupLayoutEntries::with_indices(
// The layout entries will only be visible in the fragment stage
ShaderStages::FRAGMENT,
(
// Screen texture
//
// @group(2) @binding(0) var textures: binding_array<texture_2d<f32>>;
(
0,
texture_2d(TextureSampleType::Float { filterable: true })
.count(NonZero::<u32>::new(MAX_TEXTURE_COUNT as u32).unwrap()),
),
// Sampler
//
// @group(2) @binding(1) var nearest_sampler: sampler;
//
// Note: as with textures, multiple samplers can also be bound
// onto one binding slot:
//
// ```
// sampler(SamplerBindingType::Filtering)
// .count(NonZero::<u32>::new(MAX_TEXTURE_COUNT as u32).unwrap()),
// ```
//
// One may need to pay attention to the limit of sampler binding
// amount on some platforms.
(1, sampler(SamplerBindingType::Filtering)),
),
)
.to_vec()
}
}
impl Material for BindlessMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}