nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
struct VertexInput {
    @location(0) offset: vec2<f32>,
    @location(1) uv: vec2<f32>,
};

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) tex_coords: vec2<f32>,
    @location(1) color: vec4<f32>,
    @location(2) @interpolate(flat) texture_slot: u32,
    @location(3) @interpolate(flat) texture_slot2: u32,
    @location(4) blend_factor: f32,
    @location(5) @interpolate(flat) gradient_type: u32,
    @location(6) gradient_param_a: f32,
    @location(7) gradient_param_b: f32,
    @location(8) @interpolate(flat) advanced_blend_mode: u32,
};

struct SpriteInstance {
    position: vec2<f32>,
    size: vec2<f32>,
    uv_min: vec2<f32>,
    uv_max: vec2<f32>,
    color: vec4<f32>,
    rotation_scale: vec4<f32>,
    anchor: vec2<f32>,
    depth: f32,
    texture_slot: u32,
    texture_slot2: u32,
    blend_factor: f32,
    gradient_type: u32,
    gradient_param_a: f32,
    gradient_param_b: f32,
    advanced_blend_mode: u32,
    _padding0: f32,
    _padding1: f32,
};

struct GlobalUniforms {
    view_projection: mat4x4<f32>,
    screen_size: vec2<f32>,
    atlas_slots_per_row: f32,
    atlas_slot_uv_size: f32,
};

@group(0) @binding(0)
var<uniform> globals: GlobalUniforms;

@group(0) @binding(1)
var<storage, read> instances: array<SpriteInstance>;

@group(1) @binding(0)
var sprite_atlas: texture_2d<f32>;

@group(1) @binding(1)
var sprite_sampler: sampler;

@group(2) @binding(0)
var background_tex: texture_2d<f32>;

@group(2) @binding(1)
var background_sampler: sampler;

@vertex
fn vs_main(
    in: VertexInput,
    @builtin(instance_index) instance_index: u32,
) -> VertexOutput {
    let instance = instances[instance_index];

    let anchored = in.offset - instance.anchor;
    let scaled_corner = anchored * instance.size * instance.rotation_scale.zw;

    let cos_r = instance.rotation_scale.x;
    let sin_r = instance.rotation_scale.y;
    let rotated = vec2<f32>(
        scaled_corner.x * cos_r - scaled_corner.y * sin_r,
        scaled_corner.x * sin_r + scaled_corner.y * cos_r
    );

    let world_position = instance.position + rotated;

    let uv = mix(instance.uv_min, instance.uv_max, in.uv);

    var output: VertexOutput;
    output.position = globals.view_projection * vec4<f32>(world_position, instance.depth, 1.0);
    output.tex_coords = uv;
    output.color = instance.color;
    output.texture_slot = instance.texture_slot;
    output.texture_slot2 = instance.texture_slot2;
    output.blend_factor = instance.blend_factor;
    output.gradient_type = instance.gradient_type;
    output.gradient_param_a = instance.gradient_param_a;
    output.gradient_param_b = instance.gradient_param_b;
    output.advanced_blend_mode = instance.advanced_blend_mode;

    return output;
}

fn sample_atlas_slot(slot: u32, uv: vec2<f32>) -> vec4<f32> {
    let slots_per_row = u32(globals.atlas_slots_per_row);
    let row = f32(slot / slots_per_row);
    let col = f32(slot % slots_per_row);

    let slot_u = col * globals.atlas_slot_uv_size;
    let slot_v = row * globals.atlas_slot_uv_size;

    let atlas_uv = vec2<f32>(
        slot_u + uv.x * globals.atlas_slot_uv_size,
        slot_v + uv.y * globals.atlas_slot_uv_size
    );

    return textureSampleLevel(sprite_atlas, sprite_sampler, atlas_uv, 0.0);
}

fn compute_sprite_color(in: VertexOutput) -> vec4<f32> {
    if in.gradient_type == 0u {
        let tex1 = sample_atlas_slot(in.texture_slot, in.tex_coords);
        let tex2 = sample_atlas_slot(in.texture_slot2, in.tex_coords);
        let blended = mix(tex1, tex2, in.blend_factor);
        return blended * in.color;
    }

    let shape_alpha = sample_atlas_slot(in.texture_slot, in.tex_coords).a;
    var t: f32;
    if in.gradient_type == 1u {
        let centered_uv = in.tex_coords - vec2<f32>(0.5, 0.5);
        t = dot(centered_uv, vec2<f32>(in.gradient_param_b, in.gradient_param_a)) + 0.5;
        t = clamp(t, 0.0, 1.0);
    } else {
        let center = vec2<f32>(0.5 + in.gradient_param_a, 0.5 + in.gradient_param_b);
        t = length(in.tex_coords - center) * 2.0;
        t = clamp(t, 0.0, 1.0);
    }
    let lut_uv = vec2<f32>(t, 0.5);
    let gradient_color = sample_atlas_slot(in.texture_slot2, lut_uv);
    return gradient_color * vec4<f32>(1.0, 1.0, 1.0, shape_alpha) * in.color;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    return compute_sprite_color(in);
}

fn overlay_channel(bg: f32, src: f32) -> f32 {
    if bg < 0.5 {
        return 2.0 * bg * src;
    }
    return 1.0 - 2.0 * (1.0 - bg) * (1.0 - src);
}

fn color_dodge_channel(bg: f32, src: f32) -> f32 {
    if bg <= 0.0 {
        return 0.0;
    }
    if src >= 1.0 {
        return 1.0;
    }
    return min(bg / (1.0 - src), 1.0);
}

fn color_burn_channel(bg: f32, src: f32) -> f32 {
    if bg >= 1.0 {
        return 1.0;
    }
    if src <= 0.0 {
        return 0.0;
    }
    return 1.0 - min((1.0 - bg) / src, 1.0);
}

fn overlay_blend(bg: vec3<f32>, src: vec3<f32>) -> vec3<f32> {
    return vec3<f32>(
        overlay_channel(bg.r, src.r),
        overlay_channel(bg.g, src.g),
        overlay_channel(bg.b, src.b)
    );
}

fn color_dodge_blend(bg: vec3<f32>, src: vec3<f32>) -> vec3<f32> {
    return vec3<f32>(
        color_dodge_channel(bg.r, src.r),
        color_dodge_channel(bg.g, src.g),
        color_dodge_channel(bg.b, src.b)
    );
}

fn color_burn_blend(bg: vec3<f32>, src: vec3<f32>) -> vec3<f32> {
    return vec3<f32>(
        color_burn_channel(bg.r, src.r),
        color_burn_channel(bg.g, src.g),
        color_burn_channel(bg.b, src.b)
    );
}

@fragment
fn fs_main_advanced(in: VertexOutput) -> @location(0) vec4<f32> {
    let src = compute_sprite_color(in);
    let screen_uv = in.position.xy / globals.screen_size;
    let bg = textureSampleLevel(background_tex, background_sampler, screen_uv, 0.0);

    var blended: vec3<f32>;
    switch in.advanced_blend_mode {
        case 1u: { blended = overlay_blend(bg.rgb, src.rgb); }
        case 2u: { blended = min(bg.rgb, src.rgb); }
        case 3u: { blended = max(bg.rgb, src.rgb); }
        case 4u: { blended = color_dodge_blend(bg.rgb, src.rgb); }
        case 5u: { blended = color_burn_blend(bg.rgb, src.rgb); }
        case 6u: { blended = abs(src.rgb - bg.rgb); }
        case 7u: { blended = src.rgb + bg.rgb - 2.0 * src.rgb * bg.rgb; }
        default: { blended = src.rgb; }
    }

    let out_rgb = blended * src.a + bg.rgb * (1.0 - src.a);
    let out_a = src.a + bg.a * (1.0 - src.a);
    return vec4<f32>(out_rgb, out_a);
}