bevy_keith 0.1.0

2D graphics library for the Bevy game engine
Documentation
// FIXME : #import from Bevy itself...
struct ColorGrading {
    exposure: f32,
    gamma: f32,
    pre_saturation: f32,
    post_saturation: f32,
}

// FIXME : #import from Bevy itself...
struct View {
    view_proj: mat4x4<f32>,
    unjittered_view_proj: mat4x4<f32>,
    inverse_view_proj: mat4x4<f32>,
    view: mat4x4<f32>,
    inverse_view: mat4x4<f32>,
    projection: mat4x4<f32>,
    inverse_projection: mat4x4<f32>,
    world_position: vec3<f32>,
    exposure: f32,
    // viewport(x_origin, y_origin, width, height)
    viewport: vec4<f32>,
    frustum: array<vec4<f32>, 6>,
    color_grading: ColorGrading,
    mip_bias: f32,
    render_layers: u32,
}

// Keep in sync with GpuPrimitiveKind
const PRIM_RECT: u32 = 0u;
const PRIM_GLYPH: u32 = 1u;
const PRIM_LINE: u32 = 2u;
const PRIM_QUARTER_PIE: u32 = 3u;

/// Serialized primitives buffer.
struct Primitives {
    elems: array<f32>,
}

/// Offset where the list of primitives for a tile starts,
/// and number of consecutive primitives for that tile.
struct OffsetAndCount {
    /// Offset into Tile::primitives[].
    offset: u32,
    /// Number of consecutive primitive indices in Tile::primitives[].
    count: u32,
}

struct Tiles {
    /// Packed index and kind of primitives.
    primitives: array<u32>,
}

@group(0) @binding(0)
var<uniform> view: View;

@group(1) @binding(0)
var<storage, read> primitives: Primitives;
@group(1) @binding(1)
var<storage, read> tiles: Tiles;
@group(1) @binding(2)
var<storage, read> offsets_and_counts: array<OffsetAndCount>;

@group(2) @binding(0)
var quad_texture: texture_2d<f32>;
@group(2) @binding(1)
var quad_sampler: sampler;

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
}

struct Extras {
    color: vec4<f32>,
    radius: f32,
}

struct Rect {
    center: vec2<f32>,
    half_size: vec2<f32>,
    extras: Extras,
}

struct Line {
    p0: vec2<f32>,
    p1: vec2<f32>,
    thickness: f32,
    extras: Extras,
}

struct QPie {
    origin: vec2<f32>,
    radii: vec2<f32>,
    extras: Extras,
}

const TILE_SIZE = vec2<f32>(8., 8.);

/// Get the total number of tiles in the buffer.
fn get_tile_count() -> u32 {
    // By design the offset and count buffer has one entry per tile
    return arrayLength(&offsets_and_counts);
}

fn get_tile_dim() -> vec2<u32> {
    let xy = ceil(view.viewport.zw / TILE_SIZE);
    return vec2<u32>(u32(xy.x), u32(xy.y));
}

struct PrimitiveInfo {
    index: u32,
    kind: u32,
    textured: bool,
    bordered: bool,
}

fn unpack_primitive_index(value: u32) -> PrimitiveInfo {
    let index = (value & 0x07FFFFFFu);
    let bordered = (value & 0x08000000u) != 0u;
    let kind = (value & 0x70000000u) >> 28u;
    let textured = (value & 0x80000000u) != 0u;
    return PrimitiveInfo(index, kind, textured, bordered);
}

fn get_vertex_pos(vertex_index: u32) -> vec2<f32> {
    switch vertex_index {
        case 0u { return vec2<f32>(-1., -1.); }
        case 1u { return vec2<f32>(3., -1.); }
        case 2u { return vec2<f32>(-1., 3.); }
        default { return vec2<f32>(1e38, 1e38); }
    }
}

fn read_rect(offset: u32) -> Rect {
    var rect: Rect;

    let x = primitives.elems[offset];
    let y = primitives.elems[offset + 1u];
    rect.center = vec2<f32>(x, y);
    
    let hw = primitives.elems[offset + 2u];
    let hh = primitives.elems[offset + 3u];
    rect.half_size = vec2<f32>(hw, hh);

    rect.extras.radius = primitives.elems[offset + 4u];
    
    let c = primitives.elems[offset + 5u];
    let uc: u32 = bitcast<u32>(c);
    rect.extras.color = unpack4x8unorm(uc);

    return rect;
}

fn read_line(offset: u32) -> Line {
    var line: Line;

    let p0x = primitives.elems[offset];
    let p0y = primitives.elems[offset + 1u];
    line.p0 = vec2<f32>(p0x, p0y);
    
    let p1x = primitives.elems[offset + 2u];
    let p1y = primitives.elems[offset + 3u];
    line.p1 = vec2<f32>(p1x, p1y);

    let c = primitives.elems[offset + 4u];
    let uc: u32 = bitcast<u32>(c);
    line.extras.color = unpack4x8unorm(uc);

    line.thickness = primitives.elems[offset + 5u];

    line.extras.radius = 0.0; // TODO

    return line;
}

fn read_qpie(offset: u32) -> QPie {
    var qpie: QPie;

    let x = primitives.elems[offset];
    let y = primitives.elems[offset + 1u];
    qpie.origin = vec2<f32>(x, y);

    let rx = primitives.elems[offset + 2u];
    let ry = primitives.elems[offset + 3u];
    qpie.radii = vec2<f32>(rx, ry);

    let c = primitives.elems[offset + 4u];
    let uc: u32 = bitcast<u32>(c);
    qpie.extras.color = unpack4x8unorm(uc);

    qpie.extras.radius = 0.0; // TODO

    return qpie;
}

/// Signed distance to an axis-aligned rectangle.
fn sd_rect(p: vec2<f32>, rect: Rect) -> f32 {
    let delta = abs(p - rect.center) - rect.half_size + rect.extras.radius;
    return length(max(delta, vec2<f32>(0))) + max(min(delta.x, 0.), min(delta.y, 0.)) - rect.extras.radius;
}

/// Signed distance to an infinitely thin line segment.
fn sd_segment(p0: vec2<f32>, p1: vec2<f32>, p: vec2<f32>) -> f32 {
    let p0p = p - p0;
    let p01 = p1 - p0;
    let h = saturate(dot(p0p, p01) / dot(p01, p01));
    return length(p0p - p01 * h);
}

/// Signed distance to a line (thick segment).
fn sd_line(p0: vec2<f32>, p1: vec2<f32>, thickness: f32, p: vec2<f32>) -> f32 {
    let dir = p1 - p0;
    let d = normalize(dir);
    let center = p0 + dir / 2.;
    let rot_delta = mat2x2<f32>(d.x, -d.y, d.y, d.x) * (p - center);
    let delta = abs(rot_delta) - vec2<f32>(length(dir), thickness) * 0.5;
    return length(max(delta, vec2<f32>(0))) + max(min(delta.x, 0.), min(delta.y, 0.));
}

/// Calculate the anti-aliased coverage of a pixel based on its SDF distance.
fn aa_coverage(dist: f32) -> f32 {
    // The mathematical border is exactly at 'dist'. But we want a smooth edge between the two pixels
    // directly before and after the border. So we need to map the pixels at d=-0.5 and d=+0.5 to the
    // coverage values 1. and 0., respectively. This gives the best result for axis-aligned edges,
    // and gives an acceptable 1-px wide smoothing for all other edges.
    return smoothstep(1., 0., dist + 0.5);
}

fn sdf_rect(offset: u32, canvas_pos: vec2<f32>) -> vec4<f32> {
    let rect = read_rect(offset);
    let dist = sd_rect(canvas_pos, rect);
    let alpha = rect.extras.color.a * aa_coverage(dist);
    return vec4<f32>(rect.extras.color.rgb, alpha);
}

fn sdf_glyph(offset: u32, canvas_pos: vec2<f32>) -> vec4<f32> {
    let rect = read_rect(offset);
    let dist = sd_rect(canvas_pos, rect);
    let alpha = rect.extras.color.a * aa_coverage(dist);

    let uv_x = primitives.elems[offset + 6u];
    let uv_y = primitives.elems[offset + 7u];
    let uv_sx = primitives.elems[offset + 8u];
    let uv_sy = primitives.elems[offset + 9u];
    let uv_origin = vec2<f32>(uv_x, uv_y);
    let uv_scale = vec2<f32>(uv_sx, uv_sy);
    let uv = (canvas_pos - rect.center) * uv_scale + uv_origin;
    let tex = textureSample(quad_texture, quad_sampler, uv);

    return vec4<f32>(rect.extras.color.rgb, alpha * tex.a * rect.extras.color.a);
}

fn sdf_line(offset: u32, canvas_pos: vec2<f32>) -> vec4<f32> {
    let line = read_line(offset);
    let dist = sd_line(line.p0, line.p1, line.thickness, canvas_pos) - line.extras.radius;
    let alpha = line.extras.color.a * aa_coverage(dist);
    return vec4<f32>(line.extras.color.rgb, alpha);
}

@vertex
fn vertex(
    @builtin(vertex_index) vertex_index: u32,
) -> VertexOutput {
    var out: VertexOutput;
    out.position = vec4<f32>(get_vertex_pos(vertex_index), 0.0, 1.0);
    return out;
}

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    // Find the tile this fragment is part of
    let tile_pos = floor(in.position.xy / TILE_SIZE);
    let tile_dim = get_tile_dim();
    let tile_index = u32(tile_pos.y) * tile_dim.x + u32(tile_pos.x);

    let canvas_pos = in.position.xy;
    var color = vec4<f32>();

    // Loop over all primitives for that tile, and accumulate color
    let prim_offset = offsets_and_counts[tile_index].offset;
    let prim_count = offsets_and_counts[tile_index].count;
    for (var i = prim_offset; i < prim_offset + prim_count; i += 1u) {
        let prim_info = unpack_primitive_index(tiles.primitives[i]);
        var offset: u32;
        var dist: f32;
        var new_color: vec4<f32>;
        var uv_origin: vec2<f32>;
        var coverage: f32;
        switch prim_info.kind {
            case PRIM_RECT {
                let rect = read_rect(prim_info.index);
                uv_origin = rect.center;
                dist = sd_rect(canvas_pos, rect);
                coverage = aa_coverage(dist);
                let alpha = rect.extras.color.a * coverage;
                new_color = vec4<f32>(rect.extras.color.rgb, alpha);
                offset = 6u + prim_info.index;
            }
            case PRIM_GLYPH {
                let rect = read_rect(prim_info.index);
                uv_origin = rect.center;
                dist = sd_rect(canvas_pos, rect);
                coverage = aa_coverage(dist);
                let alpha = rect.extras.color.a * coverage;

                let uv_x = primitives.elems[prim_info.index + 6u];
                let uv_y = primitives.elems[prim_info.index + 7u];
                let uv_sx = primitives.elems[prim_info.index + 8u];
                let uv_sy = primitives.elems[prim_info.index + 9u];
                let uv_origin0 = vec2<f32>(uv_x, uv_y);
                let uv_scale = vec2<f32>(uv_sx, uv_sy);
                let uv = (canvas_pos - rect.center) * uv_scale + uv_origin0;
                let tex = textureSample(quad_texture, quad_sampler, uv);

                new_color = vec4<f32>(rect.extras.color.rgb, alpha * tex.a * rect.extras.color.a);
                offset = 10u + prim_info.index;
            }
            case PRIM_LINE {
                let line = read_line(prim_info.index);
                dist = sd_line(line.p0, line.p1, line.thickness, canvas_pos) - line.extras.radius;
                coverage = aa_coverage(dist);
                let alpha = line.extras.color.a * coverage;
                new_color = vec4<f32>(line.extras.color.rgb, alpha);
                uv_origin = (line.p0 + line.p1) / 2.;
                offset = 6u + prim_info.index;
            }
            default {}
        }

        let new_alpha = mix(color.a, 1.0, new_color.a);
        let rgb = mix(color.rgb, new_color.rgb, new_color.a);
        color = vec4<f32>(rgb, new_alpha);
            
        var off = offset;
        if (prim_info.textured) {
            let uv_x = primitives.elems[off + 0u];
            let uv_y = primitives.elems[off + 1u];
            let uv_sx = primitives.elems[off + 2u];
            let uv_sy = primitives.elems[off + 3u];
            let uv_offset = vec2<f32>(uv_x, uv_y);
            let uv_scale = vec2<f32>(uv_sx, uv_sy);
            let uv = fma(canvas_pos - uv_origin, uv_scale, uv_offset);
            let tex_color = textureSample(quad_texture, quad_sampler, uv).rgb;
            color = vec4<f32>(tex_color * color.rgb, color.a);
            off += 4u;
        }

        if (prim_info.bordered) {
            let border_width = primitives.elems[off + 0u];
            let bc = primitives.elems[off + 1u];
            let ubc: u32 = bitcast<u32>(bc);
            let border_color = unpack4x8unorm(ubc);
            let dist2 = dist + border_width;
            let alpha2 = aa_coverage(dist2);
            color = vec4<f32>(mix(color.rgb, border_color.rgb, (1. - alpha2) * coverage), color.a);
        }
    }

    return color;
}