allow extensions to StandardMaterial (#7820)

# Objective

allow extending `Material`s (including the built in `StandardMaterial`)
with custom vertex/fragment shaders and additional data, to easily get
pbr lighting with custom modifications, or otherwise extend a base
material.

# Solution

- added `ExtendedMaterial<B: Material, E: MaterialExtension>` which
contains a base material and a user-defined extension.
- added example `extended_material` showing how to use it
- modified AsBindGroup to have "unprepared" functions that return raw
resources / layout entries so that the extended material can combine
them

note: doesn't currently work with array resources, as i can't figure out
how to make the OwnedBindingResource::get_binding() work, as wgpu
requires a `&'a[&'a TextureView]` and i have a `Vec<TextureView>`.

# Migration Guide

manual implementations of `AsBindGroup` will need to be adjusted, the
changes are pretty straightforward and can be seen in the diff for e.g.
the `texture_binding_array` example.

---------

Co-authored-by: Robert Swain <robert.swain@gmail.com>
This commit is contained in:
robtfm 2023-10-17 22:28:08 +01:00 committed by GitHub
parent de8a6007b7
commit c99351f7c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 856 additions and 355 deletions

View file

@ -1720,6 +1720,16 @@ description = "A shader and a material that uses it"
category = "Shaders"
wasm = true
[[example]]
name = "extended_material"
path = "examples/shader/extended_material.rs"
[package.metadata.example.extended_material]
name = "Extended Material"
description = "A custom shader that builds on the standard material"
category = "Shaders"
wasm = true
[[example]]
name = "shader_prepass"
path = "examples/shader/shader_prepass.rs"

View file

@ -0,0 +1,53 @@
#import bevy_pbr::pbr_fragment pbr_input_from_standard_material
#import bevy_pbr::pbr_functions alpha_discard
#ifdef PREPASS_PIPELINE
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
#import bevy_pbr::pbr_deferred_functions deferred_output
#else
#import bevy_pbr::forward_io VertexOutput, FragmentOutput
#import bevy_pbr::pbr_functions apply_pbr_lighting, main_pass_post_lighting_processing
#endif
struct MyExtendedMaterial {
quantize_steps: u32,
}
@group(1) @binding(100)
var<uniform> my_extended_material: MyExtendedMaterial;
@fragment
fn fragment(
in: VertexOutput,
@builtin(front_facing) is_front: bool,
) -> FragmentOutput {
// generate a PbrInput struct from the StandardMaterial bindings
var pbr_input = pbr_input_from_standard_material(in, is_front);
// we can optionally modify the input before lighting and alpha_discard is applied
pbr_input.material.base_color.b = pbr_input.material.base_color.r;
// alpha discard
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
#ifdef PREPASS_PIPELINE
// in deferred mode we can't modify anything after that, as lighting is run in a separate fullscreen shader.
let out = deferred_output(in, pbr_input);
#else
var out: FragmentOutput;
// apply lighting
out.color = apply_pbr_lighting(pbr_input);
// we can optionally modify the lit color before post-processing is applied
out.color = vec4<f32>(vec4<u32>(out.color * f32(my_extended_material.quantize_steps))) / f32(my_extended_material.quantize_steps);
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
// note this does not include fullscreen postprocessing effects like bloom.
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
// we can optionally modify the final result here
out.color = out.color * 2.0;
#endif
return out;
}

View file

@ -67,28 +67,12 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
pbr_input.occlusion = min(pbr_input.occlusion, ssao_multibounce);
#endif // SCREEN_SPACE_AMBIENT_OCCLUSION
output_color = pbr_functions::pbr(pbr_input);
output_color = pbr_functions::apply_pbr_lighting(pbr_input);
} else {
output_color = pbr_input.material.base_color;
}
// fog
if (fog.mode != FOG_MODE_OFF && (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) {
output_color = pbr_functions::apply_fog(fog, output_color, pbr_input.world_position.xyz, view.world_position.xyz);
}
#ifdef TONEMAP_IN_SHADER
output_color = tone_mapping(output_color, view.color_grading);
#ifdef DEBAND_DITHER
var output_rgb = output_color.rgb;
output_rgb = powsafe(output_rgb, 1.0 / 2.2);
output_rgb = output_rgb + screen_space_dither(frag_coord.xy);
// This conversion back to linear space is required because our output texture format is
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
output_rgb = powsafe(output_rgb, 2.2);
output_color = vec4(output_rgb, output_color.a);
#endif
#endif
output_color = pbr_functions::main_pass_post_lighting_processing(pbr_input, output_color);
return output_color;
}

View file

@ -1,4 +1,5 @@
#define_import_path bevy_pbr::pbr_deferred_functions
#import bevy_pbr::pbr_types PbrInput, standard_material_new, STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT
#import bevy_pbr::pbr_deferred_types as deferred_types
#import bevy_pbr::pbr_functions as pbr_functions
@ -6,6 +7,11 @@
#import bevy_pbr::mesh_view_bindings as view_bindings
#import bevy_pbr::mesh_view_bindings view
#import bevy_pbr::utils octahedral_encode, octahedral_decode
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
#ifdef MOTION_VECTOR_PREPASS
#import bevy_pbr::pbr_prepass_functions calculate_motion_vector
#endif
// ---------------------------
// from https://github.com/DGriffin91/bevy_coordinate_systems/blob/main/src/transformations.wgsl
@ -126,4 +132,23 @@ fn pbr_input_from_deferred_gbuffer(frag_coord: vec4<f32>, gbuffer: vec4<u32>) ->
return pbr;
}
#ifdef PREPASS_PIPELINE
fn deferred_output(in: VertexOutput, pbr_input: PbrInput) -> FragmentOutput {
var out: FragmentOutput;
// gbuffer
out.deferred = deferred_gbuffer_from_pbr_input(pbr_input);
// lighting pass id (used to determine which lighting shader to run for the fragment)
out.deferred_lighting_pass_id = pbr_input.material.deferred_lighting_pass_id;
// normal if required
#ifdef NORMAL_PREPASS
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
#endif
// motion vectors if required
#ifdef MOTION_VECTOR_PREPASS
out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position);
#endif
return out;
}
#endif

View file

@ -0,0 +1,257 @@
use bevy_asset::{Asset, Handle};
use bevy_reflect::TypePath;
use bevy_render::{
mesh::MeshVertexBufferLayout,
render_asset::RenderAssets,
render_resource::{
AsBindGroup, AsBindGroupError, BindGroupLayout, RenderPipelineDescriptor, Shader,
ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup,
},
renderer::RenderDevice,
texture::{FallbackImage, Image},
};
use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshPipelineKey};
pub struct MaterialExtensionPipeline {
pub mesh_pipeline: MeshPipeline,
pub material_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
}
pub struct MaterialExtensionKey<E: MaterialExtension> {
pub mesh_key: MeshPipelineKey,
pub bind_group_data: E::Data,
}
/// A subset of the `Material` trait for defining extensions to a base `Material`, such as the builtin `StandardMaterial`.
/// A user type implementing the trait should be used as the `E` generic param in an `ExtendedMaterial` struct.
pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized {
/// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the base material mesh vertex shader
/// will be used.
fn vertex_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the base material mesh fragment shader
/// will be used.
#[allow(unused_variables)]
fn fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader
/// will be used.
fn prepass_vertex_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material prepass fragment shader
/// will be used.
#[allow(unused_variables)]
fn prepass_fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the base material deferred vertex shader
/// will be used.
fn deferred_vertex_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material deferred fragment shader
/// will be used.
#[allow(unused_variables)]
fn deferred_fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's
/// [`MaterialPipelineKey`] and [`MeshVertexBufferLayout`] as input.
/// Specialization for the base material is applied before this function is called.
#[allow(unused_variables)]
#[inline]
fn specialize(
pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayout,
key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
Ok(())
}
}
/// A material that extends a base [`Material`] with additional shaders and data.
///
/// The data from both materials will be combined and made available to the shader
/// so that shader functions built for the base material (and referencing the base material
/// bindings) will work as expected, and custom alterations based on custom data can also be used.
///
/// If the extension `E` returns a non-default result from `vertex_shader()` it will be used in place of the base
/// material's vertex shader.
///
/// If the extension `E` returns a non-default result from `fragment_shader()` it will be used in place of the base
/// fragment shader.
///
/// When used with `StandardMaterial` as the base, all the standard material fields are
/// present, so the `pbr_fragment` shader functions can be called from the extension shader (see
/// the `extended_material` example).
#[derive(Asset, Clone, TypePath)]
pub struct ExtendedMaterial<B: Material, E: MaterialExtension> {
pub base: B,
pub extension: E,
}
impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
type Data = (<B as AsBindGroup>::Data, <E as AsBindGroup>::Data);
fn unprepared_bind_group(
&self,
layout: &BindGroupLayout,
render_device: &RenderDevice,
images: &RenderAssets<Image>,
fallback_image: &FallbackImage,
) -> Result<bevy_render::render_resource::UnpreparedBindGroup<Self::Data>, AsBindGroupError>
{
// add together the bindings of the base material and the user material
let UnpreparedBindGroup {
mut bindings,
data: base_data,
} = B::unprepared_bind_group(&self.base, layout, render_device, images, fallback_image)?;
let extended_bindgroup = E::unprepared_bind_group(
&self.extension,
layout,
render_device,
images,
fallback_image,
)?;
bindings.extend(extended_bindgroup.bindings);
Ok(UnpreparedBindGroup {
bindings,
data: (base_data, extended_bindgroup.data),
})
}
fn bind_group_layout_entries(
render_device: &RenderDevice,
) -> Vec<bevy_render::render_resource::BindGroupLayoutEntry>
where
Self: Sized,
{
// add together the bindings of the standard material and the user material
let mut entries = B::bind_group_layout_entries(render_device);
entries.extend(E::bind_group_layout_entries(render_device));
entries
}
}
impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
fn vertex_shader() -> bevy_render::render_resource::ShaderRef {
match E::vertex_shader() {
ShaderRef::Default => B::vertex_shader(),
specified => specified,
}
}
fn fragment_shader() -> bevy_render::render_resource::ShaderRef {
match E::fragment_shader() {
ShaderRef::Default => B::fragment_shader(),
specified => specified,
}
}
fn prepass_vertex_shader() -> bevy_render::render_resource::ShaderRef {
match E::prepass_vertex_shader() {
ShaderRef::Default => B::prepass_vertex_shader(),
specified => specified,
}
}
fn prepass_fragment_shader() -> bevy_render::render_resource::ShaderRef {
match E::prepass_fragment_shader() {
ShaderRef::Default => B::prepass_fragment_shader(),
specified => specified,
}
}
fn deferred_vertex_shader() -> bevy_render::render_resource::ShaderRef {
match E::deferred_vertex_shader() {
ShaderRef::Default => B::deferred_vertex_shader(),
specified => specified,
}
}
fn deferred_fragment_shader() -> bevy_render::render_resource::ShaderRef {
match E::deferred_fragment_shader() {
ShaderRef::Default => B::deferred_fragment_shader(),
specified => specified,
}
}
fn alpha_mode(&self) -> crate::AlphaMode {
B::alpha_mode(&self.base)
}
fn depth_bias(&self) -> f32 {
B::depth_bias(&self.base)
}
fn opaque_render_method(&self) -> crate::OpaqueRendererMethod {
B::opaque_render_method(&self.base)
}
fn specialize(
pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayout,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
// Call the base material's specialize function
let MaterialPipeline::<Self> {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
..
} = pipeline.clone();
let base_pipeline = MaterialPipeline::<B> {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
marker: Default::default(),
};
let base_key = MaterialPipelineKey::<B> {
mesh_key: key.mesh_key,
bind_group_data: key.bind_group_data.0,
};
B::specialize(&base_pipeline, descriptor, layout, base_key)?;
// Call the extended material's specialize function afterwards
let MaterialPipeline::<Self> {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
..
} = pipeline.clone();
E::specialize(
&MaterialExtensionPipeline {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
},
descriptor,
layout,
MaterialExtensionKey {
mesh_key: key.mesh_key,
bind_group_data: key.bind_group_data.1,
},
)
}
}

View file

@ -6,6 +6,7 @@ mod alpha;
mod bundle;
pub mod deferred;
mod environment_map;
mod extended_material;
mod fog;
mod light;
mod material;
@ -18,6 +19,7 @@ mod ssao;
pub use alpha::*;
pub use bundle::*;
pub use environment_map::EnvironmentMapLight;
pub use extended_material::*;
pub use fog::*;
pub use light::*;
pub use material::*;
@ -73,6 +75,7 @@ pub const CLUSTERED_FORWARD_HANDLE: Handle<Shader> = Handle::weak_from_u128(1668
pub const PBR_LIGHTING_HANDLE: Handle<Shader> = Handle::weak_from_u128(14170772752254856967);
pub const SHADOWS_HANDLE: Handle<Shader> = Handle::weak_from_u128(11350275143789590502);
pub const SHADOW_SAMPLING_HANDLE: Handle<Shader> = Handle::weak_from_u128(3145627513789590502);
pub const PBR_FRAGMENT_HANDLE: Handle<Shader> = Handle::weak_from_u128(2295049283805286543);
pub const PBR_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4805239651767701046);
pub const PBR_PREPASS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(9407115064344201137);
pub const PBR_FUNCTIONS_HANDLE: Handle<Shader> = Handle::weak_from_u128(16550102964439850292);
@ -172,6 +175,12 @@ impl Plugin for PbrPlugin {
"render/pbr_ambient.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_FRAGMENT_HANDLE,
"render/pbr_fragment.wgsl",
Shader::from_wgsl
);
load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,

View file

@ -297,7 +297,7 @@ pub struct MaterialPipeline<M: Material> {
pub material_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
marker: PhantomData<M>,
pub marker: PhantomData<M>,
}
impl<M: Material> Clone for MaterialPipeline<M> {
@ -693,7 +693,7 @@ pub struct MaterialProperties {
/// Data prepared for a [`Material`] instance.
pub struct PreparedMaterial<T: Material> {
pub bindings: Vec<OwnedBindingResource>,
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bind_group: BindGroup,
pub key: T::Data,
pub properties: MaterialProperties,

View file

@ -1,228 +1,43 @@
#define_import_path bevy_pbr::fragment
#import bevy_pbr::pbr_functions as pbr_functions
#import bevy_pbr::pbr_bindings as pbr_bindings
#import bevy_pbr::pbr_types as pbr_types
#import bevy_pbr::mesh_bindings mesh
#import bevy_pbr::mesh_view_bindings view, fog, screen_space_ambient_occlusion_texture
#import bevy_pbr::mesh_view_types FOG_MODE_OFF
#import bevy_core_pipeline::tonemapping screen_space_dither, powsafe, tone_mapping
#import bevy_pbr::parallax_mapping parallaxed_uv
#import bevy_pbr::prepass_utils
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
#import bevy_pbr::gtao_utils gtao_multibounce
#endif
#import bevy_pbr::pbr_functions alpha_discard
#import bevy_pbr::pbr_fragment pbr_input_from_standard_material
#ifdef PREPASS_PIPELINE
#import bevy_pbr::pbr_deferred_functions deferred_gbuffer_from_pbr_input
#import bevy_pbr::pbr_prepass_functions calculate_motion_vector
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
#else // PREPASS_PIPELINE
#import bevy_pbr::forward_io VertexOutput, FragmentOutput
#endif // PREPASS_PIPELINE
#ifdef MOTION_VECTOR_PREPASS
@group(0) @binding(2)
var<uniform> previous_view_proj: mat4x4<f32>;
#endif // MOTION_VECTOR_PREPASS
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
#import bevy_pbr::pbr_deferred_functions deferred_output
#else
#import bevy_pbr::forward_io VertexOutput, FragmentOutput
#import bevy_pbr::pbr_functions apply_pbr_lighting, main_pass_post_lighting_processing
#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_UNLIT_BIT
#endif
@fragment
fn fragment(
in: VertexOutput,
@builtin(front_facing) is_front: bool,
) -> FragmentOutput {
var out: FragmentOutput;
// generate a PbrInput struct from the StandardMaterial bindings
var pbr_input = pbr_input_from_standard_material(in, is_front);
// calculate unlit color
// ---------------------
var unlit_color: vec4<f32> = pbr_bindings::material.base_color;
// alpha discard
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
let is_orthographic = view.projection[3].w == 1.0;
let V = pbr_functions::calculate_view(in.world_position, is_orthographic);
#ifdef VERTEX_UVS
var uv = in.uv;
#ifdef VERTEX_TANGENTS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) {
let N = in.world_normal;
let T = in.world_tangent.xyz;
let B = in.world_tangent.w * cross(N, T);
// Transform V from fragment to camera in world space to tangent space.
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
uv = parallaxed_uv(
pbr_bindings::material.parallax_depth_scale,
pbr_bindings::material.max_parallax_layer_count,
pbr_bindings::material.max_relief_mapping_search_steps,
uv,
// Flip the direction of Vt to go toward the surface to make the
// parallax mapping algorithm easier to understand and reason
// about.
-Vt,
);
}
#endif
#endif
#ifdef VERTEX_COLORS
unlit_color = unlit_color * in.color;
#endif
#ifdef VERTEX_UVS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
unlit_color = unlit_color * textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias);
}
#endif
// gather pbr lighting data
// ------------------
var pbr_input: pbr_types::PbrInput;
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
// Prepare a 'processed' StandardMaterial by sampling all textures to resolve
// the material members
pbr_input.material.reflectance = pbr_bindings::material.reflectance;
pbr_input.material.flags = pbr_bindings::material.flags;
pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff;
pbr_input.frag_coord = in.position;
pbr_input.world_position = in.world_position;
pbr_input.is_orthographic = is_orthographic;
pbr_input.flags = mesh[in.instance_index].flags;
// emmissive
// TODO use .a for exposure compensation in HDR
var emissive: vec4<f32> = pbr_bindings::material.emissive;
#ifdef VERTEX_UVS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
emissive = vec4<f32>(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0);
}
#endif
pbr_input.material.emissive = emissive;
// metallic and perceptual roughness
var metallic: f32 = pbr_bindings::material.metallic;
var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness;
#ifdef VERTEX_UVS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias);
// Sampling from GLTF standard channels for now
metallic = metallic * metallic_roughness.b;
perceptual_roughness = perceptual_roughness * metallic_roughness.g;
}
#endif
pbr_input.material.metallic = metallic;
pbr_input.material.perceptual_roughness = perceptual_roughness;
// occlusion
// TODO: Split into diffuse/specular occlusion?
var occlusion: vec3<f32> = vec3(1.0);
#ifdef VERTEX_UVS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r);
}
#endif
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.position.xy), 0i).r;
let ssao_multibounce = gtao_multibounce(ssao, unlit_color.rgb);
occlusion = min(occlusion, ssao_multibounce);
#endif
pbr_input.occlusion = occlusion;
// world normal
pbr_input.world_normal = pbr_functions::prepare_world_normal(
in.world_normal,
(pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u,
is_front,
);
// N (normal vector)
#ifdef LOAD_PREPASS_NORMALS
pbr_input.N = bevy_pbr::prepass_utils::prepass_normal(in.position, 0u);
#ifdef PREPASS_PIPELINE
// write the gbuffer, lighting pass id, and optionally normal and motion_vector textures
let out = deferred_output(in, pbr_input);
#else
pbr_input.N = pbr_functions::apply_normal_mapping(
pbr_bindings::material.flags,
pbr_input.world_normal,
#ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP
in.world_tangent,
#endif
#endif
#ifdef VERTEX_UVS
uv,
#endif
view.mip_bias,
);
#endif
// V (view vector)
pbr_input.V = V;
} else { // if UNLIT_BIT != 0
#ifdef PREPASS_PIPELINE
// in deferred mode, we need to fill some of the pbr input data even for unlit materials
// to pass through the gbuffer to the deferred lighting shader
pbr_input = pbr_types::pbr_input_new();
pbr_input.flags = mesh[in.instance_index].flags;
pbr_input.material.flags = pbr_bindings::material.flags;
pbr_input.world_position = in.world_position;
pbr_input.world_normal = in.world_normal;
pbr_input.frag_coord = in.position;
#endif
}
// apply alpha discard
// -------------------
// note even though this is based on the unlit color, it must be done after all texture samples for uniform control flow
unlit_color = pbr_functions::alpha_discard(pbr_bindings::material, unlit_color);
pbr_input.material.base_color = unlit_color;
// generate output
// ---------------
#ifdef PREPASS_PIPELINE
// write the gbuffer
out.deferred = deferred_gbuffer_from_pbr_input(pbr_input);
out.deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id;
#ifdef NORMAL_PREPASS
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
#endif
#ifdef MOTION_VECTOR_PREPASS
out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position);
#endif // MOTION_VECTOR_PREPASS
#else // PREPASS_PIPELINE
// in forward mode, we calculate the lit color immediately, and then apply some post-lighting effects here.
// in deferred mode the lit color and these effects will be calculated in the deferred lighting shader
var output_color = unlit_color;
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
output_color = pbr_functions::pbr(pbr_input);
var out: FragmentOutput;
if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u {
out.color = apply_pbr_lighting(pbr_input);
} else {
out.color = pbr_input.material.base_color;
}
if (fog.mode != FOG_MODE_OFF && (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) {
output_color = pbr_functions::apply_fog(fog, output_color, in.world_position.xyz, view.world_position.xyz);
}
#ifdef TONEMAP_IN_SHADER
output_color = tone_mapping(output_color, view.color_grading);
#ifdef DEBAND_DITHER
var output_rgb = output_color.rgb;
output_rgb = powsafe(output_rgb, 1.0 / 2.2);
output_rgb = output_rgb + screen_space_dither(in.position.xy);
// This conversion back to linear space is required because our output texture format is
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
output_rgb = powsafe(output_rgb, 2.2);
output_color = vec4(output_rgb, output_color.a);
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
// note this does not include fullscreen postprocessing effects like bloom.
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
#endif
#endif
#ifdef PREMULTIPLY_ALPHA
output_color = pbr_functions::premultiply_alpha(pbr_bindings::material.flags, output_color);
#endif
// write the final pixel color
out.color = output_color;
#endif // PREPASS_PIPELINE
return out;
}

View file

@ -0,0 +1,163 @@
#define_import_path bevy_pbr::pbr_fragment
#import bevy_pbr::pbr_functions as pbr_functions
#import bevy_pbr::pbr_bindings as pbr_bindings
#import bevy_pbr::pbr_types as pbr_types
#import bevy_pbr::prepass_utils
#import bevy_pbr::mesh_bindings mesh
#import bevy_pbr::mesh_view_bindings view, screen_space_ambient_occlusion_texture
#import bevy_pbr::parallax_mapping parallaxed_uv
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
#import bevy_pbr::gtao_utils gtao_multibounce
#endif
#ifdef PREPASS_PIPELINE
#import bevy_pbr::prepass_io VertexOutput
#else
#import bevy_pbr::forward_io VertexOutput
#endif
// prepare a basic PbrInput from the vertex stage output, mesh binding and view binding
fn pbr_input_from_vertex_output(
in: VertexOutput,
is_front: bool,
double_sided: bool,
) -> pbr_types::PbrInput {
var pbr_input: pbr_types::PbrInput = pbr_types::pbr_input_new();
pbr_input.flags = mesh[in.instance_index].flags;
pbr_input.is_orthographic = view.projection[3].w == 1.0;
pbr_input.V = pbr_functions::calculate_view(in.world_position, pbr_input.is_orthographic);
pbr_input.frag_coord = in.position;
pbr_input.world_position = in.world_position;
#ifdef VERTEX_COLORS
pbr_input.material.base_color = in.color;
#endif
pbr_input.world_normal = pbr_functions::prepare_world_normal(
in.world_normal,
double_sided,
is_front,
);
#ifdef LOAD_PREPASS_NORMALS
pbr_input.N = bevy_pbr::prepass_utils::prepass_normal(in.position, 0u);
#else
pbr_input.N = normalize(pbr_input.world_normal);
#endif
return pbr_input;
}
// Prepare a full PbrInput by sampling all textures to resolve
// the material members
fn pbr_input_from_standard_material(
in: VertexOutput,
is_front: bool,
) -> pbr_types::PbrInput {
let double_sided = (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u;
var pbr_input: pbr_types::PbrInput = pbr_input_from_vertex_output(in, is_front, double_sided);
pbr_input.material.flags = pbr_bindings::material.flags;
pbr_input.material.base_color *= pbr_bindings::material.base_color;
pbr_input.material.deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id;
#ifdef VERTEX_UVS
var uv = in.uv;
#ifdef VERTEX_TANGENTS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) {
let V = pbr_input.V;
let N = in.world_normal;
let T = in.world_tangent.xyz;
let B = in.world_tangent.w * cross(N, T);
// Transform V from fragment to camera in world space to tangent space.
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
uv = parallaxed_uv(
pbr_bindings::material.parallax_depth_scale,
pbr_bindings::material.max_parallax_layer_count,
pbr_bindings::material.max_relief_mapping_search_steps,
uv,
// Flip the direction of Vt to go toward the surface to make the
// parallax mapping algorithm easier to understand and reason
// about.
-Vt,
);
}
#endif // VERTEX_TANGENTS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
pbr_input.material.base_color *= textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias);
}
#endif // VERTEX_UVS
pbr_input.material.flags = pbr_bindings::material.flags;
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
pbr_input.material.reflectance = pbr_bindings::material.reflectance;
pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff;
// emissive
// TODO use .a for exposure compensation in HDR
var emissive: vec4<f32> = pbr_bindings::material.emissive;
#ifdef VERTEX_UVS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
emissive = vec4<f32>(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0);
}
#endif
pbr_input.material.emissive = emissive;
// metallic and perceptual roughness
var metallic: f32 = pbr_bindings::material.metallic;
var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness;
#ifdef VERTEX_UVS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias);
// Sampling from GLTF standard channels for now
metallic *= metallic_roughness.b;
perceptual_roughness *= metallic_roughness.g;
}
#endif
pbr_input.material.metallic = metallic;
pbr_input.material.perceptual_roughness = perceptual_roughness;
// occlusion
// TODO: Split into diffuse/specular occlusion?
var occlusion: vec3<f32> = vec3(1.0);
#ifdef VERTEX_UVS
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r);
}
#endif
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.position.xy), 0i).r;
let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb);
occlusion = min(occlusion, ssao_multibounce);
#endif
pbr_input.occlusion = occlusion;
// N (normal vector)
#ifndef LOAD_PREPASS_NORMALS
pbr_input.N = pbr_functions::apply_normal_mapping(
pbr_bindings::material.flags,
pbr_input.world_normal,
#ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP
in.world_tangent,
#endif
#endif
#ifdef VERTEX_UVS
uv,
#endif
view.mip_bias,
);
#endif
}
return pbr_input;
}

View file

@ -11,11 +11,12 @@
#import bevy_pbr::lighting as lighting
#import bevy_pbr::clustered_forward as clustering
#import bevy_pbr::shadows as shadows
#import bevy_pbr::fog as fog
#import bevy_pbr::fog
#import bevy_pbr::ambient as ambient
#ifdef ENVIRONMENT_MAP
#import bevy_pbr::environment_map
#endif
#import bevy_core_pipeline::tonemapping screen_space_dither, powsafe, tone_mapping
#import bevy_pbr::mesh_types MESH_FLAGS_SHADOW_RECEIVER_BIT
@ -137,7 +138,7 @@ fn calculate_view(
}
#ifndef PREPASS_FRAGMENT
fn pbr(
fn apply_pbr_lighting(
in: pbr_types::PbrInput,
) -> vec4<f32> {
var output_color: vec4<f32> = in.material.base_color;
@ -247,7 +248,6 @@ fn pbr(
}
#endif // PREPASS_FRAGMENT
#ifndef PREPASS_FRAGMENT
fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4<f32>, fragment_world_position: vec3<f32>, view_world_position: vec3<f32>) -> vec4<f32> {
let view_to_world = fragment_world_position.xyz - view_world_position.xyz;
@ -274,18 +274,17 @@ fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4<f32>, fragment_
}
if fog_params.mode == mesh_view_types::FOG_MODE_LINEAR {
return fog::linear_fog(fog_params, input_color, distance, scattering);
return bevy_pbr::fog::linear_fog(fog_params, input_color, distance, scattering);
} else if fog_params.mode == mesh_view_types::FOG_MODE_EXPONENTIAL {
return fog::exponential_fog(fog_params, input_color, distance, scattering);
return bevy_pbr::fog::exponential_fog(fog_params, input_color, distance, scattering);
} else if fog_params.mode == mesh_view_types::FOG_MODE_EXPONENTIAL_SQUARED {
return fog::exponential_squared_fog(fog_params, input_color, distance, scattering);
return bevy_pbr::fog::exponential_squared_fog(fog_params, input_color, distance, scattering);
} else if fog_params.mode == mesh_view_types::FOG_MODE_ATMOSPHERIC {
return fog::atmospheric_fog(fog_params, input_color, distance, scattering);
return bevy_pbr::fog::atmospheric_fog(fog_params, input_color, distance, scattering);
} else {
return input_color;
}
}
#endif // PREPASS_FRAGMENT
#ifdef PREMULTIPLY_ALPHA
fn premultiply_alpha(standard_material_flags: u32, color: vec4<f32>) -> vec4<f32> {
@ -338,3 +337,34 @@ fn premultiply_alpha(standard_material_flags: u32, color: vec4<f32>) -> vec4<f32
#endif
}
#endif
// fog, alpha premultiply
// for non-hdr cameras, tonemapping and debanding
fn main_pass_post_lighting_processing(
pbr_input: pbr_types::PbrInput,
input_color: vec4<f32>,
) -> vec4<f32> {
var output_color = input_color;
// fog
if (view_bindings::fog.mode != mesh_view_types::FOG_MODE_OFF && (pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) {
output_color = apply_fog(view_bindings::fog, output_color, pbr_input.world_position.xyz, view_bindings::view.world_position.xyz);
}
#ifdef TONEMAP_IN_SHADER
output_color = tone_mapping(output_color, view_bindings::view.color_grading);
#ifdef DEBAND_DITHER
var output_rgb = output_color.rgb;
output_rgb = powsafe(output_rgb, 1.0 / 2.2);
output_rgb += screen_space_dither(pbr_input.frag_coord.xy);
// This conversion back to linear space is required because our output texture format is
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
output_rgb = powsafe(output_rgb, 2.2);
output_color = vec4(output_rgb, output_color.a);
#endif
#endif
#ifdef PREMULTIPLY_ALPHA
output_color = premultiply_alpha(pbr_input.material.flags, output_color);
#endif
return output_color;
}

View file

@ -43,7 +43,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let mut binding_states: Vec<BindingState> = Vec::new();
let mut binding_impls = Vec::new();
let mut bind_group_entries = Vec::new();
let mut binding_layouts = Vec::new();
let mut attr_prepared_data_ident = None;
@ -63,13 +62,16 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new());
let converted: #converted_shader_type = self.as_bind_group_shader_type(images);
buffer.write(&converted).unwrap();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
)
}});
binding_layouts.push(quote!{
@ -85,14 +87,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
}
});
let binding_vec_index = bind_group_entries.len();
bind_group_entries.push(quote! {
#render_path::render_resource::BindGroupEntry {
binding: #binding_index,
resource: bindings[#binding_vec_index].get_binding(),
}
});
let required_len = binding_index as usize + 1;
if required_len > binding_states.len() {
binding_states.resize(required_len, BindingState::Free);
@ -164,13 +158,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
_ => {
// only populate bind group entries for non-uniforms
// uniform entries are deferred until the end
let binding_vec_index = bind_group_entries.len();
bind_group_entries.push(quote! {
#render_path::render_resource::BindGroupEntry {
binding: #binding_index,
resource: bindings[#binding_vec_index].get_binding(),
}
});
BindingState::Occupied {
binding_type,
ident: field_name,
@ -230,22 +217,28 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
if buffer {
binding_impls.push(quote! {
#render_path::render_resource::OwnedBindingResource::Buffer({
self.#field_name.clone()
})
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer({
self.#field_name.clone()
})
)
});
} else {
binding_impls.push(quote! {{
use #render_path::render_resource::AsBindGroupShaderType;
let mut buffer = #render_path::render_resource::encase::StorageBuffer::new(Vec::new());
buffer.write(&self.#field_name).unwrap();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::STORAGE,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::STORAGE,
contents: buffer.as_ref(),
},
))
)
}});
}
@ -276,14 +269,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let fallback_image = get_fallback_image(&render_path, *dimension);
binding_impls.push(quote! {
#render_path::render_resource::OwnedBindingResource::TextureView({
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
}
})
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::TextureView({
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
}
})
)
});
binding_layouts.push(quote! {
@ -315,14 +311,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let fallback_image = get_fallback_image(&render_path, *dimension);
binding_impls.push(quote! {
#render_path::render_resource::OwnedBindingResource::Sampler({
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone()
} else {
#fallback_image.sampler.clone()
}
})
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Sampler({
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
if let Some(handle) = handle {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone()
} else {
#fallback_image.sampler.clone()
}
})
)
});
binding_layouts.push(quote!{
@ -340,17 +339,12 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
// Produce impls for fields with uniform bindings
let struct_name = &ast.ident;
let struct_name_literal = struct_name.to_string();
let struct_name_literal = struct_name_literal.as_str();
let mut field_struct_impls = Vec::new();
for (binding_index, binding_state) in binding_states.iter().enumerate() {
let binding_index = binding_index as u32;
if let BindingState::OccupiedMergeableUniform { uniform_fields } = binding_state {
let binding_vec_index = bind_group_entries.len();
bind_group_entries.push(quote! {
#render_path::render_resource::BindGroupEntry {
binding: #binding_index,
resource: bindings[#binding_vec_index].get_binding(),
}
});
// single field uniform bindings for a given index can use a straightforward binding
if uniform_fields.len() == 1 {
let field = &uniform_fields[0];
@ -359,13 +353,16 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
binding_impls.push(quote! {{
let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new());
buffer.write(&self.#field_name).unwrap();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
)
}});
binding_layouts.push(quote!{
@ -402,13 +399,16 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
buffer.write(&#uniform_struct_name {
#(#field_name: &self.#field_name,)*
}).unwrap();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
)
}});
binding_layouts.push(quote!{
@ -443,36 +443,28 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
impl #impl_generics #render_path::render_resource::AsBindGroup for #struct_name #ty_generics #where_clause {
type Data = #prepared_data;
fn as_bind_group(
fn label() -> Option<&'static str> {
Some(#struct_name_literal)
}
fn unprepared_bind_group(
&self,
layout: &#render_path::render_resource::BindGroupLayout,
render_device: &#render_path::renderer::RenderDevice,
images: &#render_path::render_asset::RenderAssets<#render_path::texture::Image>,
fallback_image: &#render_path::texture::FallbackImage,
) -> Result<#render_path::render_resource::PreparedBindGroup<Self::Data>, #render_path::render_resource::AsBindGroupError> {
) -> Result<#render_path::render_resource::UnpreparedBindGroup<Self::Data>, #render_path::render_resource::AsBindGroupError> {
let bindings = vec![#(#binding_impls,)*];
let bind_group = {
let descriptor = #render_path::render_resource::BindGroupDescriptor {
entries: &[#(#bind_group_entries,)*],
label: None,
layout: &layout,
};
render_device.create_bind_group(&descriptor)
};
Ok(#render_path::render_resource::PreparedBindGroup {
Ok(#render_path::render_resource::UnpreparedBindGroup {
bindings,
bind_group,
data: #get_prepared_data,
})
}
fn bind_group_layout(render_device: &#render_path::renderer::RenderDevice) -> #render_path::render_resource::BindGroupLayout {
render_device.create_bind_group_layout(&#render_path::render_resource::BindGroupLayoutDescriptor {
entries: &[#(#binding_layouts,)*],
label: None,
})
fn bind_group_layout_entries(render_device: &#render_path::renderer::RenderDevice) -> Vec<#render_path::render_resource::BindGroupLayoutEntry> {
vec![#(#binding_layouts,)*]
}
}
}))

View file

@ -9,7 +9,10 @@ use crate::{
pub use bevy_render_macros::AsBindGroup;
use encase::ShaderType;
use std::ops::Deref;
use wgpu::BindingResource;
use wgpu::{
BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
BindingResource,
};
define_atomic_id!(BindGroupId);
render_resource_wrapper!(ErasedBindGroup, wgpu::BindGroup);
@ -262,6 +265,11 @@ pub trait AsBindGroup {
/// Data that will be stored alongside the "prepared" bind group.
type Data: Send + Sync;
/// label
fn label() -> Option<&'static str> {
None
}
/// Creates a bind group for `self` matching the layout defined in [`AsBindGroup::bind_group_layout`].
fn as_bind_group(
&self,
@ -269,10 +277,56 @@ pub trait AsBindGroup {
render_device: &RenderDevice,
images: &RenderAssets<Image>,
fallback_image: &FallbackImage,
) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError>;
) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError> {
let UnpreparedBindGroup { bindings, data } =
Self::unprepared_bind_group(self, layout, render_device, images, fallback_image)?;
let entries = bindings
.iter()
.map(|(index, binding)| BindGroupEntry {
binding: *index,
resource: binding.get_binding(),
})
.collect::<Vec<_>>();
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
label: Self::label(),
layout,
entries: &entries,
});
Ok(PreparedBindGroup {
bindings,
bind_group,
data,
})
}
/// Returns a vec of (binding index, `OwnedBindingResource`).
/// In cases where `OwnedBindingResource` is not available (as for bindless texture arrays currently),
/// an implementor may define `as_bind_group` directly. This may prevent certain features
/// from working correctly.
fn unprepared_bind_group(
&self,
layout: &BindGroupLayout,
render_device: &RenderDevice,
images: &RenderAssets<Image>,
fallback_image: &FallbackImage,
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError>;
/// Creates the bind group layout matching all bind groups returned by [`AsBindGroup::as_bind_group`]
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout
where
Self: Sized,
{
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Self::label(),
entries: &Self::bind_group_layout_entries(render_device),
})
}
/// Returns a vec of bind group layout entries
fn bind_group_layout_entries(render_device: &RenderDevice) -> Vec<BindGroupLayoutEntry>
where
Self: Sized;
}
@ -285,14 +339,21 @@ pub enum AsBindGroupError {
/// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`].
pub struct PreparedBindGroup<T> {
pub bindings: Vec<OwnedBindingResource>,
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bind_group: BindGroup,
pub data: T,
}
/// a map containing `OwnedBindingResource`s, keyed by the target binding index
pub struct UnpreparedBindGroup<T> {
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub data: T,
}
/// An owned binding resource of any type (ex: a [`Buffer`], [`TextureView`], etc).
/// This is used by types like [`PreparedBindGroup`] to hold a single list of all
/// render resources used by bindings.
#[derive(Debug)]
pub enum OwnedBindingResource {
Buffer(Buffer),
TextureView(TextureView),

View file

@ -462,7 +462,7 @@ pub struct Material2dBindGroupId(Option<BindGroupId>);
/// Data prepared for a [`Material2d`] instance.
pub struct PreparedMaterial2d<T: Material2d> {
pub bindings: Vec<OwnedBindingResource>,
pub bindings: Vec<(u32, OwnedBindingResource)>,
pub bind_group: BindGroup,
pub key: T::Data,
}

View file

@ -292,6 +292,7 @@ Example | Description
[Array Texture](../examples/shader/array_texture.rs) | A shader that shows how to reuse the core bevy PBR shading functionality in a custom material that obtains the base color from an array texture.
[Compute - Game of Life](../examples/shader/compute_shader_game_of_life.rs) | A compute shader that simulates Conway's Game of Life
[Custom Vertex Attribute](../examples/shader/custom_vertex_attribute.rs) | A shader that reads a mesh's custom vertex attribute
[Extended Material](../examples/shader/extended_material.rs) | A custom shader that builds on the standard material
[Instancing](../examples/shader/shader_instancing.rs) | A shader that renders a mesh multiple times in one draw call
[Material](../examples/shader/shader_material.rs) | A shader and a material that uses it
[Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language

View file

@ -0,0 +1,92 @@
//! Demonstrates using a custom extension to the `StandardMaterial` to modify the results of the builtin pbr shader.
use bevy::reflect::TypePath;
use bevy::{
pbr::{ExtendedMaterial, MaterialExtension, OpaqueRendererMethod},
prelude::*,
render::render_resource::*,
};
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(MaterialMeshBundle {
mesh: meshes.add(
Mesh::try_from(shape::Icosphere {
radius: 1.0,
subdivisions: 5,
})
.unwrap(),
),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
material: materials.add(ExtendedMaterial {
base: StandardMaterial {
base_color: Color::RED,
// 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 },
}),
..default()
});
// light
commands.spawn((PointLightBundle::default(), Rotate));
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
#[derive(Component)]
struct Rotate;
fn rotate_things(mut q: Query<&mut Transform, With<Rotate>>, time: Res<Time>) {
for mut t in q.iter_mut() {
t.translation = Vec3::new(
time.elapsed_seconds().sin(),
0.5,
time.elapsed_seconds().cos(),
) * 4.0;
}
}
#[derive(Asset, AsBindGroup, TypePath, 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 {
"shaders/extended_material.wgsl".into()
}
fn deferred_fragment_shader() -> ShaderRef {
"shaders/extended_material.wgsl".into()
}
}

View file

@ -141,36 +141,45 @@ impl AsBindGroup for BindlessMaterial {
})
}
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout
fn unprepared_bind_group(
&self,
_: &BindGroupLayout,
_: &RenderDevice,
_: &RenderAssets<Image>,
_: &FallbackImage,
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError> {
// we implement as_bind_group directly because
panic!("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().
}
fn bind_group_layout_entries(_: &RenderDevice) -> Vec<BindGroupLayoutEntry>
where
Self: Sized,
{
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: "bindless_material_layout".into(),
entries: &[
// @group(1) @binding(0) var textures: binding_array<texture_2d<f32>>;
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
vec![
// @group(1) @binding(0) var textures: binding_array<texture_2d<f32>>;
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
// @group(1) @binding(1) var nearest_sampler: sampler;
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
// Note: as textures, multiple samplers can also be bound onto one binding slot.
// One may need to pay attention to the limit of sampler binding amount on some platforms.
// count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
],
})
count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
// @group(1) @binding(1) var nearest_sampler: sampler;
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
// Note: as textures, multiple samplers can also be bound onto one binding slot.
// One may need to pay attention to the limit of sampler binding amount on some platforms.
// count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
]
}
}