Last active
March 13, 2025 20:24
-
-
Save ethereumdegen/8892d71ae07c822531e16dad61146476 to your computer and use it in GitHub Desktop.
Complex extension material for bevy game engine (0.15)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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