From 9f193f99b3d046d1aaac0328c5359a69948a2619 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:13:50 +1000 Subject: [PATCH 1/2] extract sprite material functions and bindings to separate modules --- .../bevy_sprite_render/src/sprite_mesh/mod.rs | 5 + .../src/sprite_mesh/sprite_bindings.wgsl | 7 + .../src/sprite_mesh/sprite_functions.wgsl | 206 ++++++++++++++++++ .../src/sprite_mesh/sprite_material.wgsl | 180 +-------------- .../src/sprite_mesh/sprite_types.wgsl | 28 +++ 5 files changed, 250 insertions(+), 176 deletions(-) create mode 100644 crates/bevy_sprite_render/src/sprite_mesh/sprite_bindings.wgsl create mode 100644 crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl create mode 100644 crates/bevy_sprite_render/src/sprite_mesh/sprite_types.wgsl diff --git a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs index 671e568a83761..a8238a5a09157 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs +++ b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs @@ -13,6 +13,7 @@ use bevy_math::{primitives::Rectangle, vec2}; use bevy_mesh::{Mesh, Mesh2d, MeshAttributeCompressionFlags, MeshBuilder, Meshable}; use bevy_platform::collections::HashMap; +use bevy_shader::load_shader_library; use bevy_sprite::{prelude::SpriteMesh, Anchor}; mod sprite_material; @@ -24,6 +25,10 @@ pub struct SpriteMeshPlugin; impl Plugin for SpriteMeshPlugin { fn build(&self, app: &mut bevy_app::App) { + load_shader_library!(app, "sprite_bindings.wgsl"); + load_shader_library!(app, "sprite_functions.wgsl"); + load_shader_library!(app, "sprite_types.wgsl"); + app.add_plugins(SpriteMaterialPlugin); app.add_systems( diff --git a/crates/bevy_sprite_render/src/sprite_mesh/sprite_bindings.wgsl b/crates/bevy_sprite_render/src/sprite_mesh/sprite_bindings.wgsl new file mode 100644 index 0000000000000..9f93ecc991c2f --- /dev/null +++ b/crates/bevy_sprite_render/src/sprite_mesh/sprite_bindings.wgsl @@ -0,0 +1,7 @@ +#define_import_path bevy_sprite::sprite_bindings + +#import bevy_sprite::sprite_types::SpriteMaterial + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var material: SpriteMaterial; +@group(#{MATERIAL_BIND_GROUP}) @binding(1) var texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(2) var texture_sampler: sampler; diff --git a/crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl b/crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl new file mode 100644 index 0000000000000..3a35e161b74bf --- /dev/null +++ b/crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl @@ -0,0 +1,206 @@ +#define_import_path bevy_sprite::sprite_functions + +#import bevy_sprite::{ + sprite_types::{ + SPRITE_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS, + SPRITE_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE, + SPRITE_MATERIAL_FLAGS_ALPHA_MODE_MASK, + SPRITE_MATERIAL_FLAGS_FLIP_X, + SPRITE_MATERIAL_FLAGS_FLIP_Y, + SPRITE_MATERIAL_FLAGS_TILE_X, + SPRITE_MATERIAL_FLAGS_TILE_Y, + }, + sprite_bindings::{material, texture, texture_sampler}, +} + +// Applies all the transformations to the UV and samples the sprite's final color. +fn sample_final_color(uv: vec2) -> vec4 { + let sprite_color = sample_sprite_texture(uv); + return get_final_color(sprite_color); +} + +// Applies all the necessary transformations to the UV and samples the sprite's texture. +fn sample_sprite_texture(uv: vec2) -> vec4 { + let final_uv = get_final_uv(uv); + return textureSample(texture, texture_sampler, final_uv); +} + +// Applies the tint and alpha discard on the sprite color. +fn get_final_color(sprite_color: vec4) -> vec4 { + var output_color = apply_tint(sprite_color); + output_color = alpha_discard(output_color); + return output_color; +} + +// Applies all the necessary transformations to get the final UV that the texture should be sampled from. +fn get_final_uv(uv: vec2) -> vec2 { + var out = uv; + out = apply_flip(out); + out = apply_tiling(out); + out = apply_slicing(out); + out = apply_uv_transform(out); + return out; +} + +// Flips the UV based on the sprite's flip X and Y properties. +fn apply_flip(uv: vec2) -> vec2 { + var out = uv; + if (material.flags & SPRITE_MATERIAL_FLAGS_FLIP_X) != 0u { + out.x = 1.0 - out.x; + } + if (material.flags & SPRITE_MATERIAL_FLAGS_FLIP_Y) != 0u { + out.y = 1.0 - out.y; + } + + return out; +} + +// Applies tiling to the UV based on the sprite's tiling propeties when `image_mode` is `Tiled`. +fn apply_tiling(uv: vec2) -> vec2 { + var out = uv; + if (material.flags & SPRITE_MATERIAL_FLAGS_TILE_X) != 0u { + out.x = (out.x - material.tile_stretch_value.x * floor(out.x / material.tile_stretch_value.x)) / material.tile_stretch_value.x; + } + if (material.flags & SPRITE_MATERIAL_FLAGS_TILE_Y) != 0u { + out.y = (out.y - material.tile_stretch_value.y * floor(out.y / material.tile_stretch_value.y)) / material.tile_stretch_value.y; + } + + return out; +} + +// Applies the sprite's UV transform, +// which is used for sampling the correct region from a texture atlas +// and scaling the sprite when `image_mode` is `Scaled`. +fn apply_uv_transform(uv: vec2) -> vec2 { + return (material.uv_transform * vec3(uv, 1.0)).xy; +} + +// Applies UV slicing based on the sprite's slicing properties when `image_mode` is `Sliced`. +fn apply_slicing(uv: vec2) -> vec2 { + // using this as a temp check for slicing + if material.scale.x == 0.0 { + return uv; + } + + let min_inset_scaled = material.min_inset / material.scale; + let max_inset_scaled = material.max_inset / material.scale; + + let left = uv.x < min_inset_scaled.x; + let right = uv.x > 1.0 - max_inset_scaled.x; + let top = uv.y < min_inset_scaled.y; + let bottom = uv.y > 1.0 - max_inset_scaled.y; + + // top-left corner + if top && left { + return uv * material.scale; + } + + // top-right corner + if top && right { + return vec2( + 1.0 - (1.0 - uv.x) * material.scale.x, + uv.y * material.scale.y, + ); + } + + // bottom-left corner + if bottom && left { + return vec2( + uv.x * material.scale.x, + 1.0 - (1.0 - uv.y) * material.scale.y + ); + } + + // bottom-right corner + if bottom && right { + return vec2(1.0) - (vec2(1.0) - uv) * material.scale; + } + + // top edge + if top { + return vec2( + tile_or_stretch(uv.x, min_inset_scaled.x, 1.0 - max_inset_scaled.x, material.min_inset.x, 1.0 - material.max_inset.x, material.side_stretch_value.x), + uv.y * material.scale.y + ); + } + + // bottom edge + if bottom { + return vec2( + tile_or_stretch(uv.x, min_inset_scaled.x, 1.0 - max_inset_scaled.x, material.min_inset.x, 1.0 - material.max_inset.x, material.side_stretch_value.x), + 1.0 - (1.0 - uv.y) * material.scale.y + ); + } + + // left edge + if left { + return vec2( + uv.x * material.scale.x, + tile_or_stretch(uv.y, min_inset_scaled.y, 1.0 - max_inset_scaled.y, material.min_inset.y, 1.0 - material.max_inset.y, material.side_stretch_value.y) + ); + } + + // right edge + if right { + return vec2( + 1.0 - (1.0 - uv.x) * material.scale.x, + tile_or_stretch(uv.y, min_inset_scaled.y, 1.0 - max_inset_scaled.y, material.min_inset.y, 1.0 - material.max_inset.y, material.side_stretch_value.y) + ); + } + + // center + return vec2( + tile_or_stretch(uv.x, min_inset_scaled.x, 1.0 - max_inset_scaled.x, material.min_inset.x, 1.0 - material.max_inset.x, material.center_stretch_value.x), + tile_or_stretch(uv.y, min_inset_scaled.y, 1.0 - max_inset_scaled.y, material.min_inset.y, 1.0 - material.max_inset.y, material.center_stretch_value.y) + ); +} + +// Applies the tint from the sprite's `color` property. +fn apply_tint(sprite_color: vec4) -> vec4 { + return sprite_color * material.color; +} + +// Discards fragments based on the sprite's `alpha_cutoff` and `alpha_mode`. +fn alpha_discard(output_color: vec4) -> vec4 { + var color = output_color; + let alpha_mode = material.flags & SPRITE_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + + if alpha_mode == SPRITE_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { + // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 + color.a = 1.0; + } + +#ifdef MAY_DISCARD + else if alpha_mode == SPRITE_MATERIAL_FLAGS_ALPHA_MODE_MASK { + if color.a >= material.alpha_cutoff { + // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque + color.a = 1.0; + } else { + // NOTE: output_color.a < in.material.alpha_cutoff should not be rendered + discard; + } + } +#endif // MAY_DISCARD + + return color; +} + +// Maps a point p from [a, b] to [c, d], tiling it if stretch_value is not 0. +fn tile_or_stretch(p: f32, a: f32, b: f32, c: f32, d: f32, stretch_value: f32) -> f32 { + if stretch_value == 0.0 { + return stretch_interval(p, a, b, c, d); + } + return tile_interval(p, a, b, c, d, stretch_value); +} + +// Takes a point p from an interval [a, b] and maps it to a portion of the tile [c, d] +fn tile_interval(p: f32, a: f32, b: f32, c: f32, d: f32, stretch_value: f32) -> f32 { + let value = (p - a) / (b - a); + let tile_value = (value - stretch_value * floor(value / stretch_value)) / stretch_value; + return tile_value * (d - c) + c; +} + +// Takes a point p from an interval [a, b] and translates it to the interval [c, d] +fn stretch_interval(p: f32, a: f32, b: f32, c: f32, d: f32) -> f32 { + return (p - a) / (b - a) * (d - c) + c; +} diff --git a/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wgsl b/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wgsl index 790ff4c63af64..82edce94009bf 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wgsl +++ b/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wgsl @@ -2,7 +2,9 @@ mesh2d_functions as mesh_functions, mesh2d_vertex_output::VertexOutput, mesh2d_view_bindings::view, - mesh2d_vertex_input::{Vertex, decompress_vertex} + mesh2d_vertex_input::{Vertex, decompress_vertex}, + sprite_bindings::material, + sprite_functions, } #ifdef TONEMAP_IN_SHADER @@ -51,67 +53,11 @@ fn vertex(vertex: Vertex) -> VertexOutput { return out; } -struct SpriteMaterial { - color: vec4, - flags: u32, - alpha_cutoff: f32, - vertex_scale: vec2, - vertex_offset: vec2, - uv_transform: mat3x3, - - tile_stretch_value: vec2, - - scale: vec2, - min_inset: vec2, - max_inset: vec2, - side_stretch_value: vec2, - center_stretch_value: vec2, -}; - -const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3221225472u; // (0b11u32 << 30) -const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 30) -const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1073741824u; // (1u32 << 30) -const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2147483648u; // (2u32 << 30) - -const SPRITE_MATERIAL_FLAGS_FLIP_X: u32 = 1u; -const SPRITE_MATERIAL_FLAGS_FLIP_Y: u32 = 2u; -const SPRITE_MATERIAL_FLAGS_TILE_X: u32 = 4u; -const SPRITE_MATERIAL_FLAGS_TILE_Y: u32 = 8u; - -@group(#{MATERIAL_BIND_GROUP}) @binding(0) var material: SpriteMaterial; -@group(#{MATERIAL_BIND_GROUP}) @binding(1) var texture: texture_2d; -@group(#{MATERIAL_BIND_GROUP}) @binding(2) var texture_sampler: sampler; - @fragment fn fragment( mesh: VertexOutput, ) -> @location(0) vec4 { - - var uv = mesh.uv; - - if (material.flags & SPRITE_MATERIAL_FLAGS_FLIP_X) != 0u { - uv.x = 1.0 - uv.x; - } - if (material.flags & SPRITE_MATERIAL_FLAGS_FLIP_Y) != 0u { - uv.y = 1.0 - uv.y; - } - - if (material.flags & SPRITE_MATERIAL_FLAGS_TILE_X) != 0u { - uv.x = (uv.x - material.tile_stretch_value.x * floor(uv.x / material.tile_stretch_value.x)) / material.tile_stretch_value.x; - } - if (material.flags & SPRITE_MATERIAL_FLAGS_TILE_Y) != 0u { - uv.y = (uv.y - material.tile_stretch_value.y * floor(uv.y / material.tile_stretch_value.y)) / material.tile_stretch_value.y; - } - - // using this as a temp check for slicing - if material.scale.x != 0.0 { - uv = apply_slicing(uv); - } - - uv = (material.uv_transform * vec3(uv, 1.0)).xy; - - let sprite_color = textureSample(texture, texture_sampler, uv); - var output_color = alpha_discard(sprite_color * material.color); + var output_color = sprite_functions::sample_final_color(mesh.uv); #ifdef TONEMAP_IN_SHADER output_color = tonemapping::tone_mapping(output_color, view.color_grading); @@ -127,121 +73,3 @@ fn fragment( return output_color; } - -fn apply_slicing(uv: vec2) -> vec2 { - let min_inset_scaled = material.min_inset / material.scale; - let max_inset_scaled = material.max_inset / material.scale; - - let left = uv.x < min_inset_scaled.x; - let right = uv.x > 1.0 - max_inset_scaled.x; - let top = uv.y < min_inset_scaled.y; - let bottom = uv.y > 1.0 - max_inset_scaled.y; - - // top-left corner - if top && left { - return uv * material.scale; - } - - // top-right corner - if top && right { - return vec2( - 1.0 - (1.0 - uv.x) * material.scale.x, - uv.y * material.scale.y, - ); - } - - // bottom-left corner - if bottom && left { - return vec2( - uv.x * material.scale.x, - 1.0 - (1.0 - uv.y) * material.scale.y - ); - } - - // bottom-right corner - if bottom && right { - return vec2(1.0) - (vec2(1.0) - uv) * material.scale; - } - - // top edge - if top { - return vec2( - tile_or_stretch(uv.x, min_inset_scaled.x, 1.0 - max_inset_scaled.x, material.min_inset.x, 1.0 - material.max_inset.x, material.side_stretch_value.x), - uv.y * material.scale.y - ); - } - - // bottom edge - if bottom { - return vec2( - tile_or_stretch(uv.x, min_inset_scaled.x, 1.0 - max_inset_scaled.x, material.min_inset.x, 1.0 - material.max_inset.x, material.side_stretch_value.x), - 1.0 - (1.0 - uv.y) * material.scale.y - ); - } - - // left edge - if left { - return vec2( - uv.x * material.scale.x, - tile_or_stretch(uv.y, min_inset_scaled.y, 1.0 - max_inset_scaled.y, material.min_inset.y, 1.0 - material.max_inset.y, material.side_stretch_value.y) - ); - } - - // right edge - if right { - return vec2( - 1.0 - (1.0 - uv.x) * material.scale.x, - tile_or_stretch(uv.y, min_inset_scaled.y, 1.0 - max_inset_scaled.y, material.min_inset.y, 1.0 - material.max_inset.y, material.side_stretch_value.y) - ); - } - - // center - return vec2( - tile_or_stretch(uv.x, min_inset_scaled.x, 1.0 - max_inset_scaled.x, material.min_inset.x, 1.0 - material.max_inset.x, material.center_stretch_value.x), - tile_or_stretch(uv.y, min_inset_scaled.y, 1.0 - max_inset_scaled.y, material.min_inset.y, 1.0 - material.max_inset.y, material.center_stretch_value.y) - ); -} - -// Maps a point p from [a, b] to [c, d], tiling it if stretch_value is not 0. -fn tile_or_stretch(p: f32, a: f32, b: f32, c: f32, d: f32, stretch_value: f32) -> f32 { - if stretch_value == 0.0 { - return stretch_interval(p, a, b, c, d); - } - return tile_interval(p, a, b, c, d, stretch_value); -} - -// Takes a point p from an interval [a, b] and maps it to a portion of the tile [c, d] -fn tile_interval(p: f32, a: f32, b: f32, c: f32, d: f32, stretch_value: f32) -> f32 { - let value = (p - a) / (b - a); - let tile_value = (value - stretch_value * floor(value / stretch_value)) / stretch_value; - return tile_value * (d - c) + c; -} - -// Takes a point p from an interval [a, b] and translates it to the interval [c, d] -fn stretch_interval(p: f32, a: f32, b: f32, c: f32, d: f32) -> f32 { - return (p - a) / (b - a) * (d - c) + c; -} - -fn alpha_discard(output_color: vec4) -> vec4 { - var color = output_color; - let alpha_mode = material.flags & SPRITE_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; - - if alpha_mode == SPRITE_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { - // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 - color.a = 1.0; - } - -#ifdef MAY_DISCARD - else if alpha_mode == SPRITE_MATERIAL_FLAGS_ALPHA_MODE_MASK { - if color.a >= material.alpha_cutoff { - // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque - color.a = 1.0; - } else { - // NOTE: output_color.a < in.material.alpha_cutoff should not be rendered - discard; - } - } -#endif // MAY_DISCARD - - return color; -} diff --git a/crates/bevy_sprite_render/src/sprite_mesh/sprite_types.wgsl b/crates/bevy_sprite_render/src/sprite_mesh/sprite_types.wgsl new file mode 100644 index 0000000000000..343f0847fb4da --- /dev/null +++ b/crates/bevy_sprite_render/src/sprite_mesh/sprite_types.wgsl @@ -0,0 +1,28 @@ +#define_import_path bevy_sprite::sprite_types + +struct SpriteMaterial { + color: vec4, + flags: u32, + alpha_cutoff: f32, + vertex_scale: vec2, + vertex_offset: vec2, + uv_transform: mat3x3, + + tile_stretch_value: vec2, + + scale: vec2, + min_inset: vec2, + max_inset: vec2, + side_stretch_value: vec2, + center_stretch_value: vec2, +}; + +const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3221225472u; // (0b11u32 << 30) +const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 30) +const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1073741824u; // (1u32 << 30) +const SPRITE_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2147483648u; // (2u32 << 30) + +const SPRITE_MATERIAL_FLAGS_FLIP_X: u32 = 1u; +const SPRITE_MATERIAL_FLAGS_FLIP_Y: u32 = 2u; +const SPRITE_MATERIAL_FLAGS_TILE_X: u32 = 4u; +const SPRITE_MATERIAL_FLAGS_TILE_Y: u32 = 8u; From 2c637960a5b78610321f30a00e29ef31d83f1724 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:35:33 +1000 Subject: [PATCH 2/2] fix typo --- crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl b/crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl index 3a35e161b74bf..53f27de383402 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl +++ b/crates/bevy_sprite_render/src/sprite_mesh/sprite_functions.wgsl @@ -55,7 +55,7 @@ fn apply_flip(uv: vec2) -> vec2 { return out; } -// Applies tiling to the UV based on the sprite's tiling propeties when `image_mode` is `Tiled`. +// Applies tiling to the UV based on the sprite's tiling properties when `image_mode` is `Tiled`. fn apply_tiling(uv: vec2) -> vec2 { var out = uv; if (material.flags & SPRITE_MATERIAL_FLAGS_TILE_X) != 0u {