Skip to content

Instantly share code, notes, and snippets.

@ethereumdegen
Last active March 13, 2025 20:24
Show Gist options
  • Save ethereumdegen/8892d71ae07c822531e16dad61146476 to your computer and use it in GitHub Desktop.
Save ethereumdegen/8892d71ae07c822531e16dad61146476 to your computer and use it in GitHub Desktop.
Complex extension material for bevy game engine (0.15)
#import bevy_pbr::{
pbr_fragment::pbr_input_from_standard_material,
pbr_functions::alpha_discard,
mesh_view_bindings as view_bindings,
}
#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 CharacterMaterialUniforms {
tint_color: vec4<f32>,
};
@group(2) @binding(20)
var<uniform> custom_uniforms: CharacterMaterialUniforms;
@group(2) @binding(100) var mask: texture_2d<f32>;
@group(2) @binding(101) var mask_sampler: sampler;
@group(2) @binding(102) var<uniform> highlight_color: vec4<f32>;
@group(2) @binding(103) var<uniform> shadow_color: vec4<f32>;
@group(2) @binding(104) var<uniform> rim_color: vec4<f32>;
// https://github.com/janhohenheim/bevy_wind_waker_shader
// close, yes, it uses the lighting result rgb then multiplies by some constants to give a "perceived luminance" value, then that samples the mask yep
@fragment
fn fragment(
in: VertexOutput,
@builtin(front_facing) is_front: bool,
) -> FragmentOutput {
var pbr_input = pbr_input_from_standard_material(in, is_front);
// 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
// remove and store texture
let texture = pbr_input.material.base_color;
pbr_input.material.base_color = vec4<f32>(1.0, 1.0, 1.0, 1.0);
var out: FragmentOutput;
out.color = apply_pbr_lighting(pbr_input); //apply lighting to a white texture to understand just the lighting
let lighting_average = (out.color.r + out.color.g + out.color.b ) / 3.0 ;
// Source for cel shading: https://www.youtube.com/watch?v=mnxs6CR6Zrk]
// sample mask at the current fragment's intensity as u to get the cutoff
let uv = vec2<f32>(lighting_average, 0.0);
let quantization = textureSample(mask, mask_sampler, uv);
out.color = mix(shadow_color, highlight_color, quantization);
// apply rim highlights. Inspired by Breath of the Wild: https://www.youtube.com/watch?v=By7qcgaqGI4
let eye = normalize(view_bindings::view.world_position.xyz - in.world_position.xyz);
let rim = 1.0 - abs(dot(eye, in.world_normal));
let rim_factor = rim * rim * rim * rim;
out.color = mix(out.color, rim_color, rim_factor);
// Reapply texture
out.color = out.color * texture;
pbr_input.material.base_color = texture;
// 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);
// Modify the final result based on tint_color
if (all(custom_uniforms.tint_color.rgb > vec3<f32>(1.0, 1.0, 1.0))) {
out.color += custom_uniforms.tint_color - vec4<f32>(1.0, 1.0, 1.0, 1.0);
} else {
out.color *= custom_uniforms.tint_color;
}
out.color= clamp(out.color, vec4<f32>(0.0), vec4<f32>(1.0));
// how come there is bloom shimmer !?
#endif
return out;
}
use crate::materials::extension_material_link::BuildableUsingWorld;
use bevy::asset::embedded_asset;
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::*;
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use super::shaders::cel_mask_texture::get_cel_mask_texture_embedded_path;
pub fn character_cel_material_plugin(app: &mut App) {
app.add_plugins(MaterialPlugin::<
//NEED THIS
CharacterCelMaterial,
>::default());
}
pub type CharacterCelMaterial = ExtendedMaterial<StandardMaterial, CharacterCelMaterialBase>;
pub fn build_character_material(
original_material: StandardMaterial,
mask_image: Handle<Image>, //from embedded
) -> CharacterCelMaterial {
ExtendedMaterial {
base: original_material, //from blender
extension: CharacterCelMaterialBase::build(
mask_image, // asset_server.load("embedded://assets/cel_mask.png")
),
}
}
//pub type AnimatedMaterialExtension = ExtendedMaterial<StandardMaterial, AnimatedMaterial>;
//pub type CharacterMaterialBundle = MaterialMeshBundle<CharacterMaterial >;
#[derive(Clone, ShaderType, Debug)]
pub struct CharacterMaterialUniforms {
pub tint_color: LinearRgba,
// pub accelerations: Vec4,
}
impl Default for CharacterMaterialUniforms {
fn default() -> Self {
info!("build default CharacterMaterialUniforms");
Self {
tint_color: Color::WHITE.into(),
// accelerations: Vec4::default(),
}
}
}
#[derive(Clone, ShaderType, Debug)]
pub struct ClothMaterialUniforms {
pub accelerations: Vec4,
}
impl Default for ClothMaterialUniforms {
fn default() -> Self {
Self {
accelerations: Vec4::default(),
}
}
}
#[derive(Asset, AsBindGroup, TypePath, Debug, Clone, Default)]
pub struct CharacterCelMaterialBase {
// 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(20)]
pub character_uniforms: CharacterMaterialUniforms,
#[uniform(30)]
pub cloth_uniforms: ClothMaterialUniforms,
#[texture(100)]
#[sampler(101)]
mask: Handle<Image>,
/// The parts of the model that are facing the light source and are not in shadow.
#[uniform(102)]
pub highlight_color: LinearRgba,
/// The parts of the model that are not facing the light source and are in shadow.
#[uniform(103)]
pub shadow_color: LinearRgba,
/// The color of the edge of the model, which gets a slight specular highlight to make the model pop.
#[uniform(104)]
pub rim_color: LinearRgba,
}
impl BuildableUsingWorld for CharacterCelMaterialBase {
fn build_with_world(world: &mut World) -> Self {
//load in the mask image !!
let cel_mask_image_handle = world.load_asset(get_cel_mask_texture_embedded_path());
Self::build(cel_mask_image_handle)
}
}
impl CharacterCelMaterialBase {
fn build(mask_image: Handle<Image>) -> Self {
let highlight_color = Srgba::hex("ADBBB7").unwrap();
let shadow_color = Srgba::hex("8E978D").unwrap();
let rim_color = Srgba::hex("EEEEEE").unwrap();
Self {
character_uniforms: CharacterMaterialUniforms::default(),
cloth_uniforms: ClothMaterialUniforms::default(),
mask: mask_image,
highlight_color: highlight_color.into(),
shadow_color: shadow_color.into(),
rim_color: rim_color.into(),
}
}
}
impl CharacterCelMaterialBase {
pub fn set_tint_alpha(&mut self, alpha: f32) {
self.character_uniforms.tint_color.alpha = alpha;
}
pub fn set_tint_rgb(&mut self, rgb: LinearRgba) {
self.character_uniforms.tint_color.red = rgb.red;
self.character_uniforms.tint_color.green = rgb.green;
self.character_uniforms.tint_color.blue = rgb.blue;
}
}
impl MaterialExtension for CharacterCelMaterialBase {
fn fragment_shader() -> ShaderRef {
// CHARACTER_MATERIAL_SHADER_HANDLE.into()
"shaders/character_cel.wgsl".into()
}
fn vertex_shader() -> ShaderRef {
"shaders/cloth.wgsl".into()
}
}
#import bevy_pbr::mesh_functions::{mesh_position_local_to_clip, get_world_from_local, mesh_position_local_to_world}
#import bevy_pbr::mesh_functions;
#import bevy_pbr::{
view_transformations::position_world_to_clip,
skinning,
morph::morph,
mesh_view_bindings::view,
mesh_view_bindings::globals,
mesh_view_bindings as view_bindings,
pbr_bindings,
pbr_types,
pbr_functions,
pbr_fragment::pbr_input_from_standard_material,
pbr_functions::{
prepare_world_normal,
apply_normal_mapping,
calculate_view
},
pbr_types::{STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT},
}
#ifdef PREPASS_PIPELINE
#import bevy_pbr::{
prepass_io::{FragmentOutput},
pbr_deferred_functions::deferred_output,
}
#else
#import bevy_pbr::{
forward_io::{VertexOutput, FragmentOutput},
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing, apply_fog, alpha_discard},
}
#endif
#import bevy_core_pipeline::tonemapping::tone_mapping
#import bevy_pbr::pbr_types::StandardMaterial
struct ClothMaterialUniforms {
accelerations: vec4<f32>, // x, y, z linear accel + yaw rotational accel
};
@group(2) @binding(30)
var<uniform> custom_uniforms: ClothMaterialUniforms;
//@group(2) @binding(100) var mask: texture_2d<f32>;
//@group(2) @binding(101) var mask_sampler: sampler;
#ifdef PREPASS_PIPELINE
/*
struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>,
@location(1) blend_color: vec4<f32>,
@location(2) uv: vec2<f32>,
};
*/
@vertex
fn vertex(vertex: bevy_pbr::prepass_io:Vertex) -> bevy_pbr::prepass_io::VertexOutput {
var out: bevy_pbr::prepass_io::VertexOutput;
var local_psn_output = vertex.position;
// local_psn_output.y = vertex.position.y * (1.0 + sin( time_base + vertex.position.x) * 0.20);
// local_psn_output.x = vertex.position.x * (1.0 + cos( time_base + vertex.position.y) * 0.10 * vertex.position.y);
out.world_position = mesh_position_local_to_world(
get_world_from_local(vertex.instance_index),
vec4<f32>(local_position, 1.0)
);
out.position = mesh_position_local_to_clip(
get_world_from_local(vertex.instance_index),
vec4<f32>(local_psn_output, 1.0),
);
#ifdef VERTEX_UVS_A
out.uv = vertex.uv ;
#endif
return out;
}
#else
/*
struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
};*/
@vertex
fn vertex(vertex_no_morph: bevy_pbr::forward_io::Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex_no_morph);
#else
var vertex = vertex_no_morph;
#endif
var influence = 0.0; // vertex.uv.y - 0.1 ;
#ifdef VERTEX_UVS_A
influence = vertex.uv.y - 0.1 ;
#endif
influence = clamp( influence , 0.0, 1.0 ) ;
let influence_cubed = influence * influence * influence ;
//FOR TESTING
//
// -----
/*
// Sample mask to determine influence amount
let influence = textureSample(mask, mask_sampler, vertex.uv).r;
*/
// Get acceleration values
// Transform the modified position
// out.color = vertex.blend_color ;
let mesh_world_from_local = mesh_functions::get_world_from_local(vertex.instance_index);
//https://github.com/bevyengine/bevy/blob/main/crates/bevy_pbr/src/render/mesh.wgsl
//skinned means RIGGED
#ifdef SKINNED
var world_from_local = skinning::skin_model(vertex.joint_indices, vertex.joint_weights);
#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_functions::get_world_from_local(vertex_no_morph.instance_index);
#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
// MESS W POSITIONS HERE
let accel_x = custom_uniforms.accelerations.x * influence_cubed;
let accel_y = custom_uniforms.accelerations.y * influence_cubed ;
let accel_z = custom_uniforms.accelerations.z * influence_cubed ;
let rot_yaw = custom_uniforms.accelerations.w * influence_cubed ;
// Apply acceleration-based displacement with mask influence
var new_local_position = out.position;
// The farther from the root (higher Y value), the more influence
// let y_factor = vertex.position.y;
// Apply linear acceleration
new_local_position.x += accel_x ;
new_local_position.y += accel_y ; // Less effect on Y to keep length
new_local_position.z += accel_z ;
// Apply yaw rotation (around Y axis)
{
let angle = rot_yaw * 0.01 ;
let cos_angle = cos(angle);
let sin_angle = sin(angle);
let original_x = new_local_position.x;
let original_z = new_local_position.z;
new_local_position.x = original_x * cos_angle - original_z * sin_angle;
new_local_position.z = original_z * cos_angle + original_x * sin_angle ;
}
out.position = new_local_position;
//------
#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
return out;
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment