use bevy::asset::uuid_handle;
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::{AsBindGroup, ShaderType};
use bevy::shader::ShaderRef;
use bevy::sprite_render::{Material2d, Material2dPlugin, MeshMaterial2d};
use bevy_bitmap_text::*;
const FONT_NAME: &str = "SourceHanSansCN-Light";
const SIZE: u32 = 48;
const RAINBOW_SHADER_HANDLE: Handle<Shader> = uuid_handle!("f00ba4ca-feba-be12-3456-780000000001");
const RAINBOW_WGSL: &str = r#"
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
struct RainbowParams {
time: f32,
speed: f32,
period: f32,
lightness: f32,
}
@group(2) @binding(0) var glyph_texture: texture_2d<f32>;
@group(2) @binding(1) var glyph_sampler: sampler;
@group(2) @binding(2) var<uniform> params: RainbowParams;
fn hsl_to_rgb(h: f32, s: f32, l: f32) -> vec3<f32> {
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
let h6 = h * 6.0;
let x = c * (1.0 - abs(h6 % 2.0 - 1.0));
var rgb: vec3<f32>;
if h6 < 1.0 { rgb = vec3(c, x, 0.0); }
else if h6 < 2.0 { rgb = vec3(x, c, 0.0); }
else if h6 < 3.0 { rgb = vec3(0.0, c, x); }
else if h6 < 4.0 { rgb = vec3(0.0, x, c); }
else if h6 < 5.0 { rgb = vec3(x, 0.0, c); }
else { rgb = vec3(c, 0.0, x); }
let m = l - c * 0.5;
return rgb + m;
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let tex = textureSample(glyph_texture, glyph_sampler, in.uv);
if tex.a < 0.01 { discard; }
let hue = fract(in.world_position.x / params.period + params.time * params.speed);
let rgb = hsl_to_rgb(hue, 1.0, params.lightness);
return vec4(rgb * tex.a, tex.a);
}
"#;
#[derive(Asset, TypePath, AsBindGroup, Clone)]
struct RainbowGlyphMaterial {
#[texture(0)]
#[sampler(1)]
atlas_texture: Handle<Image>,
#[uniform(2)]
params: RainbowParams,
}
#[derive(Clone, Copy, ShaderType)]
struct RainbowParams {
time: f32,
speed: f32,
period: f32,
lightness: f32,
}
impl Material2d for RainbowGlyphMaterial {
fn fragment_shader() -> ShaderRef {
RAINBOW_SHADER_HANDLE.into()
}
}
#[derive(Component)]
struct ShaderRainbow;
#[derive(Component)]
struct ConvertedToMesh;
const WAVE_AMPLITUDE: f32 = 8.0;
const WAVE_FREQUENCY: f32 = 4.0;
const WAVE_PHASE_STEP: f32 = 0.5;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "bevy_bitmap_text — shader rainbow".into(),
resolution: (960, 640).into(),
..default()
}),
..default()
}))
.add_plugins(BitmapTextPlugin::default())
.add_plugins(Material2dPlugin::<RainbowGlyphMaterial>::default());
{
let mut shaders = app.world_mut().resource_mut::<Assets<Shader>>();
let _ = shaders.insert(
RAINBOW_SHADER_HANDLE.id(),
Shader::from_wgsl(RAINBOW_WGSL, "rainbow_glyph.wgsl"),
);
}
app.add_systems(Startup, setup)
.add_systems(PostUpdate, convert_glyphs_to_mesh.after(BitmapTextSet))
.add_systems(Update, (update_rainbow_time, wave_animation_system))
.run();
}
fn setup(mut commands: Commands, mut cache: ResMut<DynamicGlyphCache>) {
commands.spawn(Camera2d);
let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/assets/SourceHanSansCN-Light.otf");
let font_data = std::fs::read(&font_path).expect("Failed to read font file");
cache
.add_font(FontId::from_name(FONT_NAME), &font_data)
.expect("Failed to load font");
commands.spawn((
ShaderRainbow,
TextBlock::new("Shader Rainbow 着色器彩虹渐变!"),
TextBlockStyling {
font: FontId::from_name(FONT_NAME),
size_px: SIZE,
world_scale: SIZE as f32,
align: TextAlign::Left,
anchor: TextAnchor::CENTER,
line_height: 1.4,
..default()
},
Transform::from_xyz(0.0, 0.0, 0.0),
));
}
fn convert_glyphs_to_mesh(
mut commands: Commands,
cache: Res<DynamicGlyphCache>,
images: Res<Assets<Image>>,
rainbow_query: Query<&Children, With<ShaderRainbow>>,
glyph_query: Query<(Entity, &Sprite), (With<GlyphEntity>, Without<ConvertedToMesh>)>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<RainbowGlyphMaterial>>,
) {
let atlas_handle = &cache.atlas_image;
let Some(atlas_img) = images.get(atlas_handle) else {
return;
};
let atlas_size = Vec2::new(atlas_img.width() as f32, atlas_img.height() as f32);
for children in rainbow_query.iter() {
for child in children.iter() {
let Ok((entity, sprite)) = glyph_query.get(child) else {
continue;
};
let Some(custom_size) = sprite.custom_size else {
continue;
};
let Some(pixel_rect) = sprite.rect else {
continue;
};
let mesh_handle = build_glyph_mesh(&mut meshes, custom_size, pixel_rect, atlas_size);
let material_handle = materials.add(RainbowGlyphMaterial {
atlas_texture: atlas_handle.clone(),
params: RainbowParams {
time: 0.0,
speed: 0.2,
period: 500.0,
lightness: 0.6,
},
});
commands.entity(entity).remove::<Sprite>().insert((
Mesh2d(mesh_handle),
MeshMaterial2d(material_handle),
ConvertedToMesh,
));
}
}
}
fn build_glyph_mesh(
meshes: &mut Assets<Mesh>,
size: Vec2,
pixel_rect: Rect,
atlas_size: Vec2,
) -> Handle<Mesh> {
let uv_min = pixel_rect.min / atlas_size;
let uv_max = pixel_rect.max / atlas_size;
let mut mesh = Rectangle::new(size.x, size.y).mesh().build();
mesh.insert_attribute(
Mesh::ATTRIBUTE_UV_0,
vec![
[uv_max.x, uv_min.y], [uv_min.x, uv_min.y], [uv_min.x, uv_max.y], [uv_max.x, uv_max.y], ],
);
meshes.add(mesh)
}
fn update_rainbow_time(time: Res<Time>, mut materials: ResMut<Assets<RainbowGlyphMaterial>>) {
for (_, material) in materials.iter_mut() {
material.params.time = time.elapsed_secs();
}
}
fn wave_animation_system(
time: Res<Time>,
rainbow_query: Query<&Children, With<ShaderRainbow>>,
mut glyph_query: Query<(&GlyphEntity, &GlyphBaseOffset, &mut Transform)>,
) {
let t = time.elapsed_secs();
for children in rainbow_query.iter() {
for child in children.iter() {
let Ok((glyph, base, mut transform)) = glyph_query.get_mut(child) else {
continue;
};
let phase = glyph.char_index as f32 * WAVE_PHASE_STEP;
let offset = (t * WAVE_FREQUENCY + phase).sin() * WAVE_AMPLITUDE;
transform.translation.x = base.0.x;
transform.translation.y = base.0.y + offset;
}
}
}