Bevy Toon Shader 实现方案

Concept: Cartoon Shading (Cel Shading) in Bevy 0.18 Scope: Custom Material implementation for ARPG character rendering Related: bevy-rendering, stylized-realism-game-art, bevy


Problem Statement

Existing toon shader crates (bevy_toon_material, bevy_toon_shader, bevy_shader_mtoon) max out at Bevy 0.15. For Bevy 0.18 projects (e.g., GoldMiner, ARPG), a custom Material implementation is required.


Core Algorithm

1. Diffuse Banding

The key visual signature of toon shading is quantized light response:

let n_dot_l = dot(light_direction, normal);
var light_intensity = 0.0;
 
if band_count > 0 {
    // Hard bands: e.g., band_count=3 gives 0, 1/3, 2/3, 1.0
    let x = round(n_dot_l * f32(band_count));
    light_intensity = x / f32(band_count);
} else {
    // Smooth toon with tight threshold
    light_intensity = smoothstep(0.0, 0.01, n_dot_l);
}

2. Specular Highlight

Blinn-Phong with sharp step function for cartoon look:

let half_vec = normalize(light_direction + view_direction);
let n_dot_h = dot(normal, half_vec);
let spec_intensity = pow(n_dot_h * light_intensity, glossiness * glossiness);
let spec_smooth = smoothstep(0.005, 0.01, spec_intensity);

3. Rim Lighting

View-dependent edge glow, common in anime-style rendering:

let rim_dot = 1.0 - dot(view_direction, normal);
var rim_intensity = rim_dot * pow(n_dot_l, rim_threshold);
rim_intensity = smoothstep(rim_amount - 0.01, rim_amount + 0.01, rim_intensity);

4. Final Composite

return base_color * (ambient + diffuse + specular + rim);

Bevy 0.18 Implementation

Material Struct

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

Key API Points

  • MaterialPlugin::<ToonMaterial>::default() registers the material
  • MeshMaterial3d<ToonMaterial> component applies it to meshes
  • VertexOutput provides world_normal, world_position, uv
  • #{MATERIAL_BIND_GROUP} macro expands to correct group index
  • AsBindGroup derive auto-generates bind group layout

Per-Frame Update System

Light direction, light color, and camera position must be synced from scene entities to material uniforms every frame. This is done via ResMut<Assets<ToonMaterial>> iteration.


Design Decisions

DecisionRationale
Custom Material over MaterialExtensionFull control over fragment shader; StandardMaterial PBR is irrelevant for stylized look
Single directional lightToon shading typically uses a single “key light” for readability; multiple lights create visual noise
No shadow map integrationShadow receiving in toon requires special handling (hard shadow bands); deferred for future iteration
Global light/camera in per-material uniformSimplest implementation; optimizable later via global uniform buffer

ARPG Integration Notes

Visual Separation Strategy

Toon-shaded characters naturally separate from PBR environments. Three-layer approach:

  1. Character layer: ToonMaterial, band_count=3~5, warm rim
  2. Environment layer: StandardMaterial with PBR
  3. Skill VFX layer: Separate additive/custom shaders with bloom

Per-Character Tuning

Each character has independent Handle<ToonMaterial> allowing:

  • Different base_color tints
  • Different band_count (hero=3, boss=2 for harder look)
  • Different rim_color (ally=blue rim, enemy=red rim)

Performance Considerations

  • update_toon_materials iterates all Assets<ToonMaterial> per frame
  • With 100+ materials, consider moving light/camera to global uniform
  • MaterialPlugin handles batching automatically

mosaic 美术研究(2026-07-29 / 2026-08-10)

docs/research/2026-07-29-arpg-art-style-deep-dive.md(2026-07-29):有效方向是可读性分层(角色从环境 lift),不是「画得更细」。Bevy 0.19 cel 走官方 extended_material post-lighting 量化;不要移植钉死 0.15 的 bevy_toon_material。mosaic 当时 sim 是 entity-per-tile,presentation 是单 mesh 挤出——per-tile 描边外推不成立。

docs/research/2026-08-10-stylized-toon-terrain-rendering.md(2026-08-10):风格化地形渲染与角色 toon 不是同一套预算。Torchlight 2/3 对照:TL2 不是 cel/toon 合同;TL3 是风格化 UE4 PBR,不是 toon。不要把 TL 画风写成 SideDawn 锁。

SideDawn 产品名是等距 3D toon ARPG,但今日 preview 没有角色 toon 管线。见 sidedawnarpg-character-proportions

References