Skip to content

Instantly share code, notes, and snippets.

@ChristopherBiscardi
Last active July 15, 2022 11:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ChristopherBiscardi/36582828800a03e0ed4b56e0b3d3a7d1 to your computer and use it in GitHub Desktop.
Save ChristopherBiscardi/36582828800a03e0ed4b56e0b3d3a7d1 to your computer and use it in GitHub Desktop.
Bevy: Extending StandardMaterial
use bevy::{
asset::AssetServerSettings,
pbr::{
MaterialPipeline, MaterialPipelineKey,
StandardMaterialKey, StandardMaterialUniform,
},
prelude::*,
reflect::TypeUuid,
render::{
mesh::{
MeshVertexBufferLayout, VertexAttributeValues,
},
primitives::Frustum,
render_asset::RenderAssets,
render_resource::{
AsBindGroup, AsBindGroupShaderType, Face,
RenderPipelineDescriptor, ShaderRef,
ShaderType, SpecializedMeshPipelineError,
TextureFormat,
},
},
};
use bevy_shader_utils::ShaderUtilsPlugin;
fn main() {
App::new()
.insert_resource(ClearColor(
Color::hex("071f3c").unwrap(),
))
.insert_resource(AssetServerSettings {
watch_for_changes: true,
..default()
})
.add_plugins(DefaultPlugins)
.add_plugin(ShaderUtilsPlugin)
.add_plugin(
MaterialPlugin::<CustomMaterial>::default(),
)
.add_startup_system(setup)
.add_system(change_color)
.add_system(animate_light_direction)
.run();
}
#[derive(Component)]
struct Cube;
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut custom_materials: ResMut<Assets<CustomMaterial>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
let mut mesh = Mesh::from(shape::UVSphere {
radius: 1.0,
..default()
});
if let Some(VertexAttributeValues::Float32x3(
positions,
)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION)
{
let colors: Vec<[f32; 4]> = positions
.iter()
.map(|[r, g, b]| {
[
(1. - *r) / 2.,
(1. - *g) / 2.,
(1. - *b) / 2.,
1.,
]
})
.collect();
mesh.insert_attribute(
Mesh::ATTRIBUTE_COLOR,
colors,
);
}
commands.spawn().insert_bundle(MaterialMeshBundle {
mesh: meshes.add(mesh),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
material: custom_materials.add(CustomMaterial {
time: 0.,
..default()
}),
..default()
});
// camera
commands.spawn_bundle(Camera3dBundle {
transform: Transform::from_xyz(-2.0, 2.5, 5.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
// directional 'sun' light
const HALF_SIZE: f32 = 10.0;
commands.spawn_bundle(DirectionalLightBundle {
directional_light: DirectionalLight {
// Configure the projection to better fit the scene
shadow_projection: OrthographicProjection {
left: -HALF_SIZE,
right: HALF_SIZE,
bottom: -HALF_SIZE,
top: HALF_SIZE,
near: -10.0 * HALF_SIZE,
far: 10.0 * HALF_SIZE,
..default()
},
shadows_enabled: false,
..default()
},
transform: Transform {
translation: Vec3::new(0.0, 2.0, 0.0),
rotation: Quat::from_rotation_x(
-std::f32::consts::FRAC_PI_4,
),
..default()
},
..default()
});
}
fn animate_light_direction(
time: Res<Time>,
mut query: Query<
&mut Transform,
With<DirectionalLight>,
>,
) {
for mut transform in query.iter_mut() {
transform.rotate_y(time.delta_seconds() * 0.5);
}
}
fn change_color(
mut materials: ResMut<Assets<CustomMaterial>>,
time: Res<Time>,
) {
for material in materials.iter_mut() {
material.1.time =
time.seconds_since_startup() as f32;
}
}
// 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 vertex_shader() -> ShaderRef {
"shaders/vertex_shader.wgsl".into()
}
fn fragment_shader() -> ShaderRef {
"shaders/pbr_extension.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode {
self.sm.alpha_mode
}
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayout,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
if key.bind_group_data.normal_map {
descriptor
.fragment
.as_mut()
.unwrap()
.shader_defs
.push(String::from(
"STANDARDMATERIAL_NORMAL_MAP",
));
}
descriptor.primitive.cull_mode = None;
if let Some(label) = &mut descriptor.label {
*label =
format!("custom_pbr_{}", *label).into();
}
Ok(())
}
}
// This is the struct that will be passed to your shader
#[derive(AsBindGroup, TypeUuid, Debug, Clone, Default)]
#[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"]
#[bind_group_data(CustomMaterialKey)]
#[uniform(0, CustomMaterialUniform)]
pub struct CustomMaterial {
#[uniform(12)]
time: f32,
sm: StandardMaterial,
}
// Standard stuff for CustomMaterial
/// The GPU representation of the uniform data of a [`StandardMaterial`].
#[derive(Clone, Default, ShaderType)]
pub struct CustomMaterialUniform {
pub time: f32,
/// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything
/// in between.
pub base_color: Vec4,
// Use a color for user friendliness even though we technically don't use the alpha channel
// Might be used in the future for exposure correction in HDR
pub emissive: Vec4,
/// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader
/// Defaults to minimum of 0.089
pub roughness: f32,
/// From [0.0, 1.0], dielectric to pure metallic
pub metallic: f32,
/// Specular intensity for non-metals on a linear scale of [0.0, 1.0]
/// defaults to 0.5 which is mapped to 4% reflectance in the shader
pub reflectance: f32,
pub flags: u32,
/// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque,
/// and any below means fully transparent.
pub alpha_cutoff: f32,
}
impl AsBindGroupShaderType<CustomMaterialUniform>
for CustomMaterial
{
fn as_bind_group_shader_type(
&self,
images: &RenderAssets<Image>,
) -> CustomMaterialUniform {
let sm_uniform: StandardMaterialUniform =
self.sm.as_bind_group_shader_type(images);
CustomMaterialUniform {
time: self.time,
base_color: sm_uniform.base_color,
emissive: sm_uniform.emissive.into(),
roughness: sm_uniform.roughness,
metallic: sm_uniform.metallic,
reflectance: sm_uniform.reflectance,
flags: sm_uniform.flags,
alpha_cutoff: sm_uniform.alpha_cutoff,
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct CustomMaterialKey {
normal_map: bool,
cull_mode: Option<Face>,
}
impl From<&CustomMaterial> for CustomMaterialKey {
fn from(material: &CustomMaterial) -> Self {
CustomMaterialKey {
normal_map: material
.sm
.normal_map_texture
.is_some(),
cull_mode: material.sm.cull_mode,
}
}
}
2022-07-15T10:53:42.906489Z ERROR wgpu::backend::direct: Handling wgpu errors as fatal by default
thread 'main' panicked at 'wgpu error: Validation Error
Caused by:
In Device::create_render_pipeline
note: label = `custom_pbr_opaque_mesh_pipeline`
error matching FRAGMENT shader requirements against the pipeline
shader global ResourceBinding { group: 1, binding: 1 } is not available in the layout pipeline layout
binding is missing from the pipeline layout
', /Users/chris/.cargo/registry/src/github.com-1ecc6299db9ec823/wgpu-0.12.0/src/backend/direct.rs:2273:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
#import bevy_pbr::mesh_view_bindings
// #import bevy_pbr::pbr_bindings
// #import bevy_pbr::pbr_types
struct StandardMaterial {
time: f32;
base_color: vec4<f32>;
emissive: vec4<f32>;
perceptual_roughness: f32;
metallic: f32;
reflectance: f32;
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32;
alpha_cutoff: f32;
};
let STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u;
let STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 2u;
let STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u;
let STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u;
let STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u;
let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 64u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u;
let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 512u;
let STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 1024u;
// Creates a StandardMaterial with default values
fn standard_material_new() -> StandardMaterial {
var material: StandardMaterial;
// NOTE: Keep in-sync with src/pbr_material.rs!
material.base_color = vec4<f32>(1.0, 1.0, 1.0, 1.0);
material.emissive = vec4<f32>(0.0, 0.0, 0.0, 1.0);
material.perceptual_roughness = 0.089;
material.metallic = 0.01;
material.reflectance = 0.5;
material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE;
material.alpha_cutoff = 0.5;
material.time = 0.0;
return material;
}
#import bevy_pbr::mesh_bindings
#import bevy_pbr::utils
#import bevy_pbr::clustered_forward
#import bevy_pbr::lighting
#import bevy_pbr::shadows
#import bevy_pbr::pbr_functions
#import bevy_shader_utils::simplex_noise_3d
#import bevy_shader_utils::simplex_noise_2d
[[group(1), binding(0)]]
var<uniform> material: StandardMaterial;
[[group(1), binding(1)]]
var base_color_texture: texture_2d<f32>;
[[group(1), binding(2)]]
var base_color_sampler: sampler;
[[group(1), binding(3)]]
var emissive_texture: texture_2d<f32>;
[[group(1), binding(4)]]
var emissive_sampler: sampler;
[[group(1), binding(5)]]
var metallic_roughness_texture: texture_2d<f32>;
[[group(1), binding(6)]]
var metallic_roughness_sampler: sampler;
[[group(1), binding(7)]]
var occlusion_texture: texture_2d<f32>;
[[group(1), binding(8)]]
var occlusion_sampler: sampler;
[[group(1), binding(9)]]
var normal_map_texture: texture_2d<f32>;
[[group(1), binding(10)]]
var normal_map_sampler: sampler;
struct FragmentInput {
[[builtin(front_facing)]] is_front: bool;
[[builtin(position)]] frag_coord: vec4<f32>;
[[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>;
#ifdef VERTEX_UVS
[[location(2)]] uv: vec2<f32>;
#endif
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
#ifdef VERTEX_COLORS
[[location(4)]] color: vec4<f32>;
#endif
};
[[stage(fragment)]]
fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
// var output_color = vec4<f32>(0.533, 0.533, 0.80, 1.0);
var output_color: vec4<f32> = material.base_color;
// #ifdef VERTEX_COLORS
// output_color = output_color * in.color;
// #endif
#ifdef VERTEX_UVS
if ((material.flags & STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
output_color = output_color * textureSample(base_color_texture, base_color_sampler, in.uv);
}
#endif
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
if ((material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
// Prepare a 'processed' StandardMaterial by sampling all textures to resolve
// the material members
var pbr_input: PbrInput;
pbr_input.material.base_color = output_color;
pbr_input.material.reflectance = material.reflectance;
pbr_input.material.flags = material.flags;
pbr_input.material.alpha_cutoff = material.alpha_cutoff;
// TODO use .a for exposure compensation in HDR
var emissive: vec4<f32> = material.emissive;
#ifdef VERTEX_UVS
if ((material.flags & STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
emissive = vec4<f32>(emissive.rgb * textureSample(emissive_texture, emissive_sampler, in.uv).rgb, 1.0);
}
#endif
pbr_input.material.emissive = emissive;
var metallic: f32 = material.metallic;
var perceptual_roughness: f32 = material.perceptual_roughness;
#ifdef VERTEX_UVS
if ((material.flags & STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
let metallic_roughness = textureSample(metallic_roughness_texture, metallic_roughness_sampler, in.uv);
// 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;
var occlusion: f32 = 1.0;
#ifdef VERTEX_UVS
if ((material.flags & STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
occlusion = textureSample(occlusion_texture, occlusion_sampler, in.uv).r;
}
#endif
pbr_input.occlusion = occlusion;
pbr_input.frag_coord = in.frag_coord;
pbr_input.world_position = in.world_position;
pbr_input.world_normal = in.world_normal;
pbr_input.is_orthographic = view.projection[3].w == 1.0;
pbr_input.N = prepare_normal(
material.flags,
in.world_normal,
#ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP
in.world_tangent,
#endif
#endif
#ifdef VERTEX_UVS
in.uv,
#endif
in.is_front,
);
pbr_input.V = calculate_view(in.world_position, pbr_input.is_orthographic);
output_color = tone_mapping(pbr(pbr_input));
}
// return output_color;
// custom code
var base_color = output_color;
// var base_color = vec4<f32>(1.0,0.4,1.0,1.0);
var noise_step = 5.0;
// var base_color = vec3<f32>(0.533, 0.533, 0.80);
// var noise = simplexNoise3(vec3<f32>(in.frag_coord.x * noise_step, in.frag_coord.y * noise_step, in.frag_coord.z * noise_step));
var noise = simplexNoise3(vec3<f32>(in.color.x * noise_step, in.color.y * noise_step, in.color.z * noise_step));
var threshold = sin(material.time);
var alpha = step(noise, threshold);
// var edge_color = vec3<f32>(0.0, 1.0, 0.8);
var edge_color = output_color * 3.0;
var border_step = smoothStep(threshold - 0.2, threshold + 0.2, noise);
var dissolve_border = vec3<f32>(edge_color.x * border_step, edge_color.y * border_step, edge_color.z * border_step);
var output_color = vec4<f32>(
base_color.x + dissolve_border.x,
base_color.y + dissolve_border.y,
base_color.z + dissolve_border.z,
alpha
);
if (output_color.a == 0.0) { discard; } else {
return output_color;
}
}
#import bevy_pbr::mesh_view_bindings
#import bevy_pbr::mesh_bindings
// NOTE: Bindings must come before functions that use them!
#import bevy_pbr::mesh_functions
#import bevy_shader_utils::simplex_noise_3d
#import bevy_shader_utils::simplex_noise_2d
struct StandardMaterial {
time: f32;
// base_color: vec4<f32>;
// emissive: vec4<f32>;
// perceptual_roughness: f32;
// metallic: f32;
// reflectance: f32;
// // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
// flags: u32;
// alpha_cutoff: f32;
};
[[group(1), binding(0)]]
var<uniform> material: StandardMaterial;
struct Vertex {
[[location(0)]] position: vec3<f32>;
[[location(1)]] normal: vec3<f32>;
#ifdef VERTEX_UVS
[[location(2)]] uv: vec2<f32>;
#endif
#ifdef VERTEX_TANGENTS
[[location(3)]] tangent: vec4<f32>;
#endif
#ifdef VERTEX_COLORS
[[location(4)]] color: vec4<f32>;
#endif
#ifdef SKINNED
[[location(5)]] joint_indices: vec4<u32>;
[[location(6)]] joint_weights: vec4<f32>;
#endif
};
struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
[[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>;
#ifdef VERTEX_UVS
[[location(2)]] uv: vec2<f32>;
#endif
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
#ifdef VERTEX_COLORS
[[location(4)]] color: vec4<f32>;
#endif
};
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef SKINNED
var model = skin_model(vertex.joint_indices, vertex.joint_weights);
out.world_normal = skin_normals(model, vertex.normal);
#else
var model = mesh.model;
out.world_normal = mesh_normal_local_to_world(vertex.normal);
#endif
out.world_position = mesh_position_local_to_world(model, vec4<f32>(vertex.position, 1.0));
#ifdef VERTEX_UVS
out.uv = vertex.uv;
#endif
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_tangent_local_to_world(model, vertex.tangent);
#endif
#ifdef VERTEX_COLORS
out.color = vertex.color;
#endif
out.clip_position = mesh_position_world_to_clip(out.world_position);
out.color = vec4<f32>(vertex.position.x, vertex.position.y, vertex.position.z, 1.0);
return out;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment