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);
}