Skip to content
Snippets Groups Projects
Commit 737da2b6 authored by StarArawn's avatar StarArawn
Browse files

Working font example for kayak_font.

parent e81002b8
No related tags found
No related merge requests found
......@@ -8,6 +8,8 @@ edition = "2021"
[dependencies]
anyhow = { version = "1.0" }
bevy = { git = "https://github.com/bevyengine/bevy", rev = "38c7d5eb9e81ab8e1aec03673599b25a9aa0c69c" }
bytemuck = "1.7.2"
crevice = { git = "https://github.com/bevyengine/bevy", rev = "38c7d5eb9e81ab8e1aec03673599b25a9aa0c69c" }
serde = "1.0"
serde_json = "1.0"
serde_path_to_error = "0.1"
use bevy::{
math::Vec2,
prelude::{App as BevyApp, AssetServer, Commands, Handle, Res},
render2::{camera::OrthographicCameraBundle, color::Color},
window::WindowDescriptor,
PipelinedDefaultPlugins,
};
use kayak_font::{KayakFont, KayakFontPlugin};
mod renderer;
use renderer::FontRenderPlugin;
use renderer::Text;
fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_bundle(OrthographicCameraBundle::new_2d());
let font_handle: Handle<KayakFont> = asset_server.load("roboto.kayak_font");
dbg!(font_handle);
commands
.spawn()
.insert(Text {
color: Color::WHITE,
content: "Hello World!".into(),
font_size: 32.0,
position: Vec2::new(5.0, 5.0),
size: Vec2::new(100.0, 100.0),
})
.insert(font_handle);
}
fn main() {
......@@ -20,6 +38,7 @@ fn main() {
})
.add_plugins(PipelinedDefaultPlugins)
.add_plugin(KayakFontPlugin)
.add_plugin(FontRenderPlugin)
.add_startup_system(startup)
.run();
}
use bevy::{
prelude::{Assets, Commands, Handle, Query, Res},
sprite2::Rect,
};
use kayak_font::{CoordinateSystem, KayakFont};
use super::{
pipeline::{ExtractCharBundle, ExtractedChar},
Text,
};
pub fn extract(
mut commands: Commands,
fonts: Res<Assets<KayakFont>>,
texts: Query<(&Text, &Handle<KayakFont>)>,
) {
let mut extracted_texts = Vec::new();
for (text, font_handle) in texts.iter() {
if let Some(font) = fonts.get(font_handle) {
let layouts = font.get_layout(
CoordinateSystem::PositiveYUp,
text.position,
&text.content,
text.font_size,
);
for layout in layouts {
extracted_texts.push(ExtractCharBundle {
extracted_quad: ExtractedChar {
font_handle: Some(font_handle.clone()),
rect: Rect {
min: layout.position,
max: layout.position + layout.size,
},
color: text.color,
vertex_index: 0,
char_id: font.get_char_id(layout.content).unwrap(),
z_index: 0.0,
},
});
}
}
}
commands.spawn_batch(extracted_texts);
}
use bevy::{
core_pipeline::Transparent2d,
prelude::{Assets, HandleUntyped, Plugin, Res, ResMut},
reflect::TypeUuid,
render2::{
render_asset::RenderAssets,
render_phase::DrawFunctions,
render_resource::Shader,
renderer::{RenderDevice, RenderQueue},
texture::Image,
RenderApp, RenderStage,
},
};
use kayak_font::FontTextureCache;
use self::pipeline::{DrawUI, FontPipeline, QuadMeta};
mod extract;
pub mod pipeline;
mod text;
pub use text::*;
pub const FONT_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7604018236855288450);
pub struct FontRenderPlugin;
impl Plugin for FontRenderPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
let unified_shader = Shader::from_wgsl(include_str!("shader.wgsl"));
shaders.set_untracked(FONT_SHADER_HANDLE, unified_shader);
let render_app = app.sub_app(RenderApp);
render_app
.init_resource::<QuadMeta>()
.init_resource::<FontPipeline>()
.add_system_to_stage(RenderStage::Extract, extract::extract)
.add_system_to_stage(RenderStage::Prepare, pipeline::prepare_quads)
.add_system_to_stage(RenderStage::Queue, pipeline::queue_quads)
.add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture);
let draw_quad = DrawUI::new(&mut render_app.world);
render_app
.world
.get_resource::<DrawFunctions<Transparent2d>>()
.unwrap()
.write()
.add(draw_quad);
}
}
fn create_and_update_font_cache_texture(
device: Res<RenderDevice>,
queue: Res<RenderQueue>,
pipeline: Res<FontPipeline>,
mut font_texture_cache: ResMut<FontTextureCache>,
images: Res<RenderAssets<Image>>,
) {
font_texture_cache.process_new(&device, &queue, pipeline.into_inner(), &images);
}
use bevy::{
core::FloatOrd,
core_pipeline::Transparent2d,
ecs::system::{
lifetimeless::{Read, SQuery, SRes},
SystemState,
},
math::{const_vec3, Mat4, Quat, Vec2, Vec3, Vec4},
prelude::{Bundle, Component, Entity, FromWorld, Handle, Query, Res, ResMut, World},
render2::{
color::Color,
render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass},
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendComponent,
BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize, BufferUsages,
BufferVec, CachedPipelineId, ColorTargetState, ColorWrites, FragmentState, FrontFace,
MultisampleState, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache,
RenderPipelineDescriptor, Shader, ShaderStages, TextureFormat, TextureSampleType,
TextureViewDimension, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState,
VertexStepMode,
},
renderer::{RenderDevice, RenderQueue},
texture::{BevyDefault, GpuImage},
view::{ViewUniformOffset, ViewUniforms},
},
sprite2::Rect,
};
use bytemuck::{Pod, Zeroable};
use crevice::std140::AsStd140;
use kayak_font::{FontRenderingPipeline, FontTextureCache, KayakFont};
use super::FONT_SHADER_HANDLE;
pub struct FontPipeline {
view_layout: BindGroupLayout,
pub(crate) font_image_layout: BindGroupLayout,
pipeline: CachedPipelineId,
empty_font_texture: (GpuImage, BindGroup),
}
const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
const_vec3!([0.0, 0.0, 0.0]),
const_vec3!([1.0, 1.0, 0.0]),
const_vec3!([0.0, 1.0, 0.0]),
const_vec3!([0.0, 0.0, 0.0]),
const_vec3!([1.0, 0.0, 0.0]),
const_vec3!([1.0, 1.0, 0.0]),
];
impl FontRenderingPipeline for FontPipeline {
fn get_font_image_layout(&self) -> &BindGroupLayout {
&self.font_image_layout
}
}
impl FromWorld for FontPipeline {
fn from_world(world: &mut World) -> Self {
let world = world.cell();
let render_device = world.get_resource::<RenderDevice>().unwrap();
let mut pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().unwrap();
let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(144),
},
count: None,
}],
label: Some("ui_view_layout"),
});
// Used by fonts
let font_image_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2Array,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler {
comparison: false,
filtering: true,
},
count: None,
},
],
label: Some("text_image_layout"),
});
let vertex_buffer_layout = VertexBufferLayout {
array_stride: 60,
step_mode: VertexStepMode::Vertex,
attributes: vec![
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
VertexAttribute {
format: VertexFormat::Float32x4,
offset: 12,
shader_location: 1,
},
VertexAttribute {
format: VertexFormat::Float32x4,
offset: 28,
shader_location: 2,
},
VertexAttribute {
format: VertexFormat::Float32x4,
offset: 44,
shader_location: 3,
},
],
};
let empty_font_texture = FontTextureCache::get_empty(&render_device, &font_image_layout);
let pipeline_desc = RenderPipelineDescriptor {
vertex: VertexState {
shader: FONT_SHADER_HANDLE.typed::<Shader>(),
entry_point: "vertex".into(),
shader_defs: vec![],
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: FONT_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![ColorTargetState {
format: TextureFormat::bevy_default(),
blend: Some(BlendState {
color: BlendComponent {
src_factor: BlendFactor::SrcAlpha,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
alpha: BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
}),
write_mask: ColorWrites::ALL,
}],
}),
layout: Some(vec![view_layout.clone(), font_image_layout.clone()]),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
polygon_mode: PolygonMode::Fill,
clamp_depth: false,
conservative: false,
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("font_pipeline".into()),
};
FontPipeline {
pipeline: pipeline_cache.queue(pipeline_desc),
view_layout,
font_image_layout,
empty_font_texture,
}
}
}
#[derive(Debug, Bundle)]
pub struct ExtractCharBundle {
pub(crate) extracted_quad: ExtractedChar,
}
#[derive(Debug, Component, Clone)]
pub struct ExtractedChar {
pub rect: Rect,
pub color: Color,
pub vertex_index: usize,
pub char_id: u32,
pub z_index: f32,
pub font_handle: Option<Handle<KayakFont>>,
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct QuadVertex {
pub position: [f32; 3],
pub color: [f32; 4],
pub uv: [f32; 4],
pub pos_size: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, AsStd140)]
struct QuadType {
pub t: i32,
}
pub struct QuadMeta {
vertices: BufferVec<QuadVertex>,
view_bind_group: Option<BindGroup>,
}
impl Default for QuadMeta {
fn default() -> Self {
Self {
vertices: BufferVec::new(BufferUsages::VERTEX),
view_bind_group: None,
}
}
}
pub fn prepare_quads(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut sprite_meta: ResMut<QuadMeta>,
mut extracted_quads: Query<&mut ExtractedChar>,
) {
let extracted_sprite_len = extracted_quads.iter_mut().len();
// don't create buffers when there are no quads
if extracted_sprite_len == 0 {
return;
}
sprite_meta.vertices.clear();
sprite_meta.vertices.reserve(
extracted_sprite_len * QUAD_VERTEX_POSITIONS.len(),
&render_device,
);
for (i, mut extracted_sprite) in extracted_quads.iter_mut().enumerate() {
let sprite_rect = extracted_sprite.rect;
let color = extracted_sprite.color.as_linear_rgba_f32();
let uv_min = Vec2::ZERO;
let uv_max = Vec2::ONE;
let bottom_left = Vec4::new(uv_min.x, uv_max.y, extracted_sprite.char_id as f32, 0.0);
let top_left = Vec4::new(uv_min.x, uv_min.y, extracted_sprite.char_id as f32, 0.0);
let top_right = Vec4::new(uv_max.x, uv_min.y, extracted_sprite.char_id as f32, 0.0);
let bottom_right = Vec4::new(uv_max.x, uv_max.y, extracted_sprite.char_id as f32, 0.0);
let uvs: [[f32; 4]; 6] = [
bottom_left.into(),
top_right.into(),
top_left.into(),
bottom_left.into(),
bottom_right.into(),
top_right.into(),
];
extracted_sprite.vertex_index = i;
for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() {
let world = Mat4::from_scale_rotation_translation(
sprite_rect.size().extend(1.0),
Quat::default(),
sprite_rect.min.extend(0.0),
);
let final_position = (world * Vec3::from(*vertex_position).extend(1.0)).truncate();
sprite_meta.vertices.push(QuadVertex {
position: final_position.into(),
color,
uv: uvs[index],
pos_size: [
sprite_rect.min.x,
sprite_rect.min.y,
sprite_rect.size().x,
sprite_rect.size().y,
],
});
}
}
sprite_meta
.vertices
.write_buffer(&render_device, &render_queue);
}
pub fn queue_quads(
draw_functions: Res<DrawFunctions<Transparent2d>>,
render_device: Res<RenderDevice>,
mut sprite_meta: ResMut<QuadMeta>,
view_uniforms: Res<ViewUniforms>,
quad_pipeline: Res<FontPipeline>,
mut extracted_sprites: Query<(Entity, &ExtractedChar)>,
mut views: Query<&mut RenderPhase<Transparent2d>>,
) {
if let Some(view_binding) = view_uniforms.uniforms.binding() {
sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor {
entries: &[BindGroupEntry {
binding: 0,
resource: view_binding,
}],
label: Some("quad_view_bind_group"),
layout: &quad_pipeline.view_layout,
}));
let draw_quad = draw_functions.read().get_id::<DrawUI>().unwrap();
for mut transparent_phase in views.iter_mut() {
for (entity, quad) in extracted_sprites.iter_mut() {
transparent_phase.add(Transparent2d {
draw_function: draw_quad,
pipeline: quad_pipeline.pipeline,
entity,
sort_key: FloatOrd(quad.z_index),
});
}
}
}
}
pub struct DrawUI {
params: SystemState<(
SRes<QuadMeta>,
SRes<FontPipeline>,
SRes<RenderPipelineCache>,
SRes<FontTextureCache>,
SQuery<Read<ViewUniformOffset>>,
SQuery<Read<ExtractedChar>>,
)>,
}
impl DrawUI {
pub fn new(world: &mut World) -> Self {
Self {
params: SystemState::new(world),
}
}
}
impl Draw<Transparent2d> for DrawUI {
fn draw<'w>(
&mut self,
world: &'w World,
pass: &mut TrackedRenderPass<'w>,
view: Entity,
item: &Transparent2d,
) {
let (quad_meta, unified_pipeline, pipelines, font_texture_cache, views, quads) =
self.params.get(world);
let view_uniform = views.get(view).unwrap();
let quad_meta = quad_meta.into_inner();
let extracted_quad = quads.get(item.entity).unwrap();
if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) {
pass.set_render_pipeline(pipeline);
pass.set_vertex_buffer(0, quad_meta.vertices.buffer().unwrap().slice(..));
pass.set_bind_group(
0,
quad_meta.view_bind_group.as_ref().unwrap(),
&[view_uniform.offset],
);
let unified_pipeline = unified_pipeline.into_inner();
if let Some(font_handle) = extracted_quad.font_handle.as_ref() {
if let Some(image_bindings) =
font_texture_cache.into_inner().get_binding(font_handle)
{
pass.set_bind_group(1, image_bindings, &[]);
} else {
pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]);
}
} else {
pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]);
}
pass.draw(
(extracted_quad.vertex_index * QUAD_VERTEX_POSITIONS.len()) as u32
..((extracted_quad.vertex_index + 1) * QUAD_VERTEX_POSITIONS.len()) as u32,
0..1,
);
}
}
}
[[block]]
struct View {
view_proj: mat4x4<f32>;
world_position: vec3<f32>;
};
[[group(0), binding(0)]]
var<uniform> view: View;
struct VertexOutput {
[[builtin(position)]] position: vec4<f32>;
[[location(0)]] color: vec4<f32>;
[[location(1)]] uv: vec3<f32>;
[[location(2)]] pos: vec2<f32>;
[[location(3)]] size: vec2<f32>;
[[location(4)]] screen_position: vec2<f32>;
[[location(5)]] border_radius: f32;
};
[[stage(vertex)]]
fn vertex(
[[location(0)]] vertex_position: vec3<f32>,
[[location(1)]] vertex_color: vec4<f32>,
[[location(2)]] vertex_uv: vec4<f32>,
[[location(3)]] vertex_pos_size: vec4<f32>,
) -> VertexOutput {
var out: VertexOutput;
out.color = vertex_color;
out.pos = vertex_pos_size.xy;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
out.screen_position = (view.view_proj * vec4<f32>(vertex_position, 1.0)).xy;
out.uv = vertex_uv.xyz;
out.size = vertex_pos_size.zw;
out.border_radius = vertex_uv.w;
return out;
}
[[group(1), binding(0)]]
var font_texture: texture_2d_array<f32>;
[[group(1), binding(1)]]
var font_sampler: sampler;
[[stage(fragment)]]
fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
var px_range = 2.5;
var tex_dimensions = textureDimensions(font_texture);
var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y));
var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, in.uv.y), i32(in.uv.z));
var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b));
var sig_dist = (v - 0.5) * dot(msdf_unit, 0.5 / fwidth(in.uv.xy));
var a = clamp(sig_dist + 0.5, 0.0, 1.0);
return vec4<f32>(in.color.rgb, a);
}
\ No newline at end of file
use bevy::{math::Vec2, prelude::Component, render2::color::Color};
#[derive(Component)]
pub struct Text {
pub content: String,
pub position: Vec2,
pub size: Vec2,
pub font_size: f32,
pub color: Color,
}
......@@ -25,6 +25,12 @@ pub struct LayoutRect {
pub content: char,
}
#[derive(Debug, Clone, Copy)]
pub enum CoordinateSystem {
PositiveYUp,
PositiveYDown,
}
impl KayakFont {
pub fn new(sdf: Sdf, atlas_image: Handle<Image>) -> Self {
Self {
......@@ -46,7 +52,13 @@ impl KayakFont {
self.char_ids.get(&c).and_then(|id| Some(*id))
}
pub fn get_layout(&self, position: Vec2, content: &String, font_size: f32) -> Vec<LayoutRect> {
pub fn get_layout(
&self,
axis_alignment: CoordinateSystem,
position: Vec2,
content: &String,
font_size: f32,
) -> Vec<LayoutRect> {
let mut positions_and_size = Vec::new();
let max_glyph_size = self.sdf.max_glyph_size();
let font_ratio = font_size / self.sdf.atlas.size;
......@@ -66,8 +78,13 @@ impl KayakFont {
None => (0.0, 0.0, 0.0, 0.0),
};
let shift_sign = match axis_alignment {
CoordinateSystem::PositiveYDown => -1.0,
CoordinateSystem::PositiveYUp => 1.0,
};
let position_x = position.x + x + left * font_size;
let position_y = (position.y + (-top * font_size)) + font_size;
let position_y = (position.y + (shift_sign * top * font_size)) + font_size;
positions_and_size.push(LayoutRect {
position: Vec2::new(position_x, position_y),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment