syrillian_render 0.7.1

Renderer of the Syrillian Game Engine
Documentation
use crate::ObjectHash;
use crate::cache::glyph::{GlyphRenderData, generate_glyph_geometry_stream};
use crate::model_uniform::ModelUniform;
use crate::proxies::MeshUniformIndex;
use crate::rendering::RenderPassType;
use crate::rendering::picking::hash_to_rgba;
use crate::strobe::UiDrawContext;
use crate::strobe::ui_element::{Rect, UiElement};
use glamx::{Vec2, Vec3};
use syrillian_asset::shader::immediates::TextImmediate;
use syrillian_asset::{HFont, HShader};
use syrillian_utils::color::hsv_to_rgb;
use wgpu::BufferUsages;
use wgpu::util::DeviceExt;

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum TextAlignment {
    Left,
    Right,
    Center,
}

#[derive(Debug, Clone)]
pub struct UiText {
    pub draw_order: u32,
    pub font: HFont,
    pub alignment: TextAlignment,
    pub letter_spacing_em: f32,
    pub size_em: f32,
    pub color: Vec3,
    pub rainbow: bool,
    pub text: String,
    pub object_hash: ObjectHash,
}

impl UiText {
    pub fn new(text: impl Into<String>) -> Self {
        Self {
            draw_order: 0,
            font: HFont::DEFAULT,
            alignment: TextAlignment::Left,
            letter_spacing_em: 0.0,
            size_em: 1.0,
            color: Vec3::ONE,
            rainbow: false,
            text: text.into(),
            object_hash: ObjectHash::default(),
        }
    }

    pub fn color(mut self, color: Vec3) -> Self {
        self.color = color;
        self
    }

    pub fn font_size(mut self, size: f32) -> Self {
        self.size_em = size;
        self
    }

    pub fn font(mut self, font: HFont) -> Self {
        self.font = font;
        self
    }

    pub fn letter_spacing(mut self, spacing: f32) -> Self {
        self.letter_spacing_em = spacing;
        self
    }

    pub fn align(mut self, alignment: TextAlignment) -> Self {
        self.alignment = alignment;
        self
    }

    pub fn rainbow(mut self, rainbow: bool) -> Self {
        self.rainbow = rainbow;
        self
    }

    pub fn click_listener(mut self, hash: ObjectHash) -> Self {
        self.object_hash = hash;
        self
    }
}

impl UiElement for UiText {
    fn draw_order(&self) -> u32 {
        self.draw_order
    }

    fn render(&self, ctx: &mut UiDrawContext, rect: Rect) {
        let pos = match self.alignment {
            TextAlignment::Left => rect.min(),
            TextAlignment::Center => rect.position + Vec2::new(rect.size.x * 0.5, 0.0),
            TextAlignment::Right => rect.position + Vec2::new(rect.size.x, 0.0),
        };

        self.render_internal(ctx, pos);
    }

    fn measure(&self, ctx: &mut UiDrawContext) -> Vec2 {
        let font = ctx.cache().font(self.font);
        font.request_glyphs(self.text.chars());

        let glyphs: Vec<GlyphRenderData> = generate_glyph_geometry_stream(
            &self.text,
            &font,
            self.alignment,
            1.0,
            self.letter_spacing_em,
        );

        if glyphs.is_empty() {
            return Vec2::ZERO;
        }

        let mut min = Vec2::new(f32::MAX, f32::MAX);
        let mut max = Vec2::new(f32::MIN, f32::MIN);

        for glyph in glyphs {
            for v in glyph.vertices() {
                min = min.min(Vec2::new(v.pos[0], v.pos[1]));
                max = max.max(Vec2::new(v.pos[0], v.pos[1]));
            }
        }

        if min.x > max.x || min.y > max.y {
            return Vec2::ZERO;
        }

        (max - min) * self.size_em
    }
}

impl UiText {
    fn render_internal(&self, ctx: &mut UiDrawContext, position: Vec2) {
        let shader = match ctx.gpu_ctx().pass_type {
            RenderPassType::Color2D => Some(ctx.cache().shader(HShader::TEXT_2D)),
            RenderPassType::PickingUi => Some(ctx.cache().shader(HShader::TEXT_2D_PICKING)),
            _ => None,
        };
        let Some(shader) = shader else {
            return;
        };

        let font = ctx.cache().font(self.font);
        font.request_glyphs(self.text.chars());
        let _ = font.pump(ctx.cache(), &ctx.state().queue, 10);

        let glyphs: Vec<GlyphRenderData> = generate_glyph_geometry_stream(
            &self.text,
            &font,
            self.alignment,
            1.0,
            self.letter_spacing_em,
        );

        if glyphs.is_empty() {
            return;
        }

        let mut cached_text = ctx.ui_text_data().clone();

        let glyph_bytes = bytemuck::cast_slice(&glyphs[..]);
        if (cached_text.glyph_vbo.size() as usize) < glyph_bytes.len() {
            cached_text.glyph_vbo =
                ctx.state()
                    .device
                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                        label: Some("Strobe Text Glyphs"),
                        contents: glyph_bytes,
                        usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
                    });
        } else {
            ctx.state()
                .queue
                .write_buffer(&cached_text.glyph_vbo, 0, glyph_bytes);
        }

        let model = ModelUniform::empty();
        ctx.state().queue.write_buffer(
            cached_text.uniform.buffer(MeshUniformIndex::MeshData),
            0,
            bytemuck::bytes_of(&model),
        );

        let mut pc = TextImmediate {
            position,
            em_scale: self.size_em,
            msdf_range_px: 4.0,
            color: self.color,
            padding: 0,
        };

        if self.rainbow {
            let time = ctx.start_time().elapsed().as_secs_f32() * 100.0;
            pc.color = hsv_to_rgb(time % 360.0, 1.0, 1.0);
        }

        if ctx.gpu_ctx().pass_type == RenderPassType::PickingUi {
            let color = hash_to_rgba(self.object_hash);
            pc.color = Vec3::new(color[0], color[1], color[2]);
        }

        let mut pass = ctx.gpu_ctx().pass.write();
        crate::must_pipeline!(pipeline = shader, ctx.gpu_ctx().pass_type => return);

        pass.set_pipeline(pipeline);
        pass.set_bind_group(
            shader.bind_groups().render,
            ctx.gpu_ctx().render_bind_group,
            &[],
        );

        let groups = shader.bind_groups();
        if let Some(idx) = groups.model {
            pass.set_bind_group(idx, cached_text.uniform.bind_group(), &[]);
        }
        if let Some(idx) = groups.material {
            let material = ctx.cache().material_instance(font.atlas());
            pass.set_bind_group(idx, &material.bind_group, &[]);
        }

        pass.set_immediates(0, bytemuck::bytes_of(&pc));
        pass.set_vertex_buffer(0, cached_text.glyph_vbo.slice(..));
        pass.draw(0..glyphs.len() as u32 * 6, 0..1);
    }
}