bevy_toon_material 0.1.1

A toon shader in the style of Windwaker or Breath of the Wild
Documentation
use bevy::asset::load_internal_asset;
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, AsBindGroupShaderType, ShaderRef, ShaderType};

pub const TOON_SHADER_HANDLE: Handle<Shader> =
    Handle::weak_from_u128(113635888040890716522362398121248594352);

pub struct ToonShaderPlugin;

impl Plugin for ToonShaderPlugin {
    fn build(&self, app: &mut App) {
        load_internal_asset!(app, TOON_SHADER_HANDLE, "toon.wgsl", Shader::from_wgsl);
        app.add_plugins(MaterialPlugin::<ToonMaterial>::default())
            .add_systems(Update, update_shader);
    }
}

#[derive(Component)]
pub struct ToonLight;

#[derive(Component)]
pub struct ToonCamera;

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
#[uniform(0, ToonShaderUniform)]
pub struct ToonMaterial {
    pub base_color: LinearRgba,
    pub light_direction: Vec3,
    pub light_color: LinearRgba,
    pub camera_position: Vec3,
    pub ambient_color: LinearRgba,
    pub rim_amount: f32,
    pub rim_color: LinearRgba,
    pub rim_threshold: f32,
    pub band_count: u32,
    #[texture(1)]
    #[sampler(2)]
    pub texture: Option<Handle<Image>>,
}

#[derive(Default, Clone, ShaderType)]
pub struct ToonShaderUniform {
    pub base_color: LinearRgba,
    pub light_direction: Vec3,
    pub light_color: LinearRgba,
    pub camera_position: Vec3,
    pub ambient_color: LinearRgba,
    pub rim_amount: f32,
    pub rim_color: LinearRgba,
    pub rim_threshold: f32,
    pub band_count: u32,
}

impl Material for ToonMaterial {
    fn fragment_shader() -> ShaderRef {
        TOON_SHADER_HANDLE.into()
    }
    fn specialize(
        _pipeline: &bevy::pbr::MaterialPipeline<Self>,
        _descriptor: &mut bevy::render::render_resource::RenderPipelineDescriptor,
        _layout: &bevy::render::mesh::MeshVertexBufferLayoutRef,
        _key: bevy::pbr::MaterialPipelineKey<Self>,
    ) -> Result<(), bevy::render::render_resource::SpecializedMeshPipelineError> {
        Ok(())
    }
}

impl Default for ToonMaterial {
    fn default() -> Self {
        ToonMaterial {
            base_color: LinearRgba::WHITE,
            light_direction: Vec3::ZERO,
            light_color: LinearRgba::WHITE,
            camera_position: Vec3::ZERO,
            ambient_color: LinearRgba::new(0.4, 0.4, 0.4, 1.0),
            rim_amount: 0.716,
            rim_color: LinearRgba::WHITE,
            rim_threshold: 0.1,
            band_count: 0,
            texture: None,
        }
    }
}

impl AsBindGroupShaderType<ToonShaderUniform> for ToonMaterial {
    fn as_bind_group_shader_type(
        &self,
        _: &bevy::render::render_asset::RenderAssets<bevy::render::texture::GpuImage>,
    ) -> ToonShaderUniform {
        ToonShaderUniform {
            base_color: self.base_color,
            light_direction: self.light_direction,
            light_color: self.light_color,
            camera_position: self.camera_position,
            ambient_color: self.ambient_color,
            rim_amount: self.rim_amount,
            rim_color: self.rim_color,
            rim_threshold: self.rim_threshold,
            band_count: self.band_count,
        }
    }
}

pub fn update_shader(
    toon_light: Single<(&DirectionalLight, &Transform), With<ToonLight>>,
    camera_position: Single<&Transform, With<ToonCamera>>,
    mut toon: ResMut<Assets<ToonMaterial>>,
) {
    let (light, transform) = toon_light.into_inner();
    let cam_transform = camera_position.into_inner().translation;
    for (_, toon_shader) in toon.iter_mut() {
        toon_shader.light_direction = *transform.back();
        toon_shader.light_color = light.color.to_linear();
        toon_shader.camera_position = cam_transform;
    }
}