Skip to content

Instantly share code, notes, and snippets.

@ChristopherBiscardi
Created January 18, 2025 23:57
Show Gist options
  • Save ChristopherBiscardi/c0511e6205b0d60cb2271ea11e3cc942 to your computer and use it in GitHub Desktop.
Save ChristopherBiscardi/c0511e6205b0d60cb2271ea11e3cc942 to your computer and use it in GitHub Desktop.
bevy 0.15 Custom Attribute StandardMaterial Extension Shader 360 flip
#import bevy_pbr::{
pbr_fragment::pbr_input_from_standard_material,
pbr_functions::alpha_discard,
}
#ifdef PREPASS_PIPELINE
#import bevy_pbr::{
prepass_io::{VertexOutput, FragmentOutput},
pbr_deferred_functions::deferred_output,
}
#else
#import bevy_pbr::{
forward_io::{VertexOutput, FragmentOutput},
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
}
#endif
struct MyExtendedMaterial {
quantize_steps: u32,
}
@group(2) @binding(100)
var<uniform> my_extended_material: MyExtendedMaterial;
@fragment
fn fragment(
in: VertexOutput,
@location(8) blend: vec4f,
@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;
pbr_input.material.base_color = blend;
// 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;
}
//! 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::{
mesh::{
MeshVertexAttribute, MeshVertexBufferLayoutRef,
},
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();
}
// 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,
);
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<
Assets<
ExtendedMaterial<StandardMaterial, MyExtension>,
>,
>,
) {
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![[0.0, 1.0, 0.0, 1.0]; 24],
);
// sphere
commands.spawn((
Mesh3d(meshes.add(mesh)),
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 vertex_shader() -> ShaderRef {
"vertex.wgsl".into()
}
fn fragment_shader() -> ShaderRef {
"fragment.wgsl".into()
}
fn deferred_fragment_shader() -> ShaderRef {
"fragment.wgsl".into()
}
fn specialize(
_pipeline: &bevy::pbr::MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
_key: bevy::pbr::MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
// layout.0.get_layout(attribute_descriptors);
let vertex_layout = layout.0.get_layout(&[
Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
Mesh::ATTRIBUTE_NORMAL.at_shader_location(1),
Mesh::ATTRIBUTE_UV_0.at_shader_location(2),
// Mesh::ATTRIBUTE_TANGENT.at_shader_location(3),
// Mesh::ATTRIBUTE_COLOR.at_shader_location(4),
ATTRIBUTE_BLEND_COLOR.at_shader_location(8),
])?;
descriptor.vertex.buffers = vec![vertex_layout];
Ok(())
}
}
// struct VertexOutput {
// // This is `clip position` when the struct is used as a vertex stage output
// // and `frag coord` when used as a fragment stage input
// @builtin(position) position: vec4<f32>,
// @location(0) world_position: vec4<f32>,
// @location(1) world_normal: vec3<f32>,
// #ifdef VERTEX_UVS_A
// @location(2) uv: vec2<f32>,
// #endif
// #ifdef VERTEX_UVS_B
// @location(3) uv_b: vec2<f32>,
// #endif
// #ifdef VERTEX_TANGENTS
// @location(4) world_tangent: vec4<f32>,
// #endif
// #ifdef VERTEX_COLORS
// @location(5) color: vec4<f32>,
// #endif
// #ifdef VERTEX_OUTPUT_INSTANCE_INDEX
// @location(6) @interpolate(flat) instance_index: u32,
// #endif
// #ifdef VISIBILITY_RANGE_DITHER
// @location(7) @interpolate(flat) visibility_range_dither: i32,
// #endif
// }
#import bevy_pbr::{
mesh_bindings::mesh,
mesh_functions,
skinning,
morph::morph,
forward_io::{Vertex, VertexOutput},
view_transformations::position_world_to_clip,
}
#ifdef MORPH_TARGETS
fn morph_vertex(vertex_in: Vertex) -> Vertex {
var vertex = vertex_in;
let first_vertex = mesh[vertex.instance_index].first_vertex_index;
let vertex_index = vertex.index - first_vertex;
let weight_count = bevy_pbr::morph::layer_count();
for (var i: u32 = 0u; i < weight_count; i ++) {
let weight = bevy_pbr::morph::weight_at(i);
if weight == 0.0 {
continue;
}
vertex.position += weight * morph(vertex_index, bevy_pbr::morph::position_offset, i);
#ifdef VERTEX_NORMALS
vertex.normal += weight * morph(vertex_index, bevy_pbr::morph::normal_offset, i);
#endif
#ifdef VERTEX_TANGENTS
vertex.tangent += vec4(weight * morph(vertex_index, bevy_pbr::morph::tangent_offset, i), 0.0);
#endif
}
return vertex;
}
#endif
struct MyOutput {
// This is `clip position` when the struct is used as a vertex stage output
// and `frag coord` when used as a fragment stage input
@builtin(position) position: vec4<f32>,
@location(0) world_position: vec4<f32>,
@location(1) world_normal: vec3<f32>,
#ifdef VERTEX_UVS_A
@location(2) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(3) uv_b: vec2<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(4) world_tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(5) color: vec4<f32>,
#endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
@location(6) @interpolate(flat) instance_index: u32,
#endif
#ifdef VISIBILITY_RANGE_DITHER
@location(7) @interpolate(flat) visibility_range_dither: i32,
#endif
@location(8) blend: vec4f
}
@vertex
fn vertex(
vertex_no_morph: Vertex,
@location(8) blend: vec4f
) -> MyOutput {
var out: MyOutput;
#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex_no_morph);
#else
var vertex = vertex_no_morph;
#endif
let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
#ifdef SKINNED
var world_from_local = skinning::skin_model(
vertex.joint_indices,
vertex.joint_weights,
vertex_no_morph.instance_index
);
#else
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416 .
var world_from_local = mesh_world_from_local;
#endif
#ifdef VERTEX_NORMALS
#ifdef SKINNED
out.world_normal = skinning::skin_normals(world_from_local, vertex.normal);
#else
out.world_normal = mesh_functions::mesh_normal_local_to_world(
vertex.normal,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif
#endif
#ifdef VERTEX_POSITIONS
out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4<f32>(vertex.position, 1.0));
out.position = position_world_to_clip(out.world_position.xyz);
#endif
#ifdef VERTEX_UVS_A
out.uv = vertex.uv;
#endif
#ifdef VERTEX_UVS_B
out.uv_b = vertex.uv_b;
#endif
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh_tangent_local_to_world(
world_from_local,
vertex.tangent,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif
#ifdef VERTEX_COLORS
out.color = vertex.color;
#endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
out.instance_index = vertex_no_morph.instance_index;
#endif
#ifdef VISIBILITY_RANGE_DITHER
out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level(
vertex_no_morph.instance_index, mesh_world_from_local[3]);
#endif
out.blend = blend;
return out;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment