Path: blob/main/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.rs
9368 views
use core::f32;12use bevy_app::Plugin;3use bevy_color::{Color, ColorToComponents};4use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};5use bevy_math::{vec2, Affine2, Mat3, Rect, Vec2, Vec4};67use bevy_asset::{embedded_asset, embedded_path, Asset, AssetApp, AssetPath, Handle};89use bevy_reflect::Reflect;10use bevy_render::{11render_asset::RenderAssets,12render_resource::{AsBindGroup, AsBindGroupShaderType, ShaderType},13};14use bevy_shader::ShaderRef;15use bevy_sprite::{16prelude::SpriteMesh, SliceScaleMode, SpriteAlphaMode, SpriteImageMode, SpriteScalingMode,17};1819use crate::{AlphaMode2d, Material2d, Material2dPlugin};20use core::hash::Hash;2122pub struct SpriteMaterialPlugin;2324impl Plugin for SpriteMaterialPlugin {25fn build(&self, app: &mut bevy_app::App) {26embedded_asset!(app, "sprite_material.wgsl");2728app.add_plugins(Material2dPlugin::<SpriteMaterial>::default())29.register_asset_reflect::<SpriteMaterial>();30}31}3233#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default, PartialEq)]34#[reflect(Debug, Clone)]35#[uniform(0, SpriteMaterialUniform)]36pub struct SpriteMaterial {37#[texture(1)]38#[sampler(2)]39pub image: Handle<Image>,40pub texture_atlas: Option<TextureAtlas>,41pub color: Color,42pub flip_x: bool,43pub flip_y: bool,44pub custom_size: Option<Vec2>,45pub rect: Option<Rect>,46pub image_mode: SpriteImageMode,47pub alpha_mode: AlphaMode2d,48pub anchor: Vec2,49pub texture_atlas_layout: Option<TextureAtlasLayout>,50pub texture_atlas_index: usize,51}5253// NOTE: These must match the bit flags in bevy_sprite_render/src/sprite_mesh/sprite_materials.wgsl!54bitflags::bitflags! {55#[repr(transparent)]56pub struct SpriteMaterialFlags: u32 {57const FLIP_X = 1;58const FLIP_Y = 2;59const TILE_X = 4;60const TILE_Y = 8;61/// Bitmask reserving bits for the [`AlphaMode2d`]62/// Values are just sequential values bitshifted into63/// the bitmask, and can range from 0 to 3.64const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS;65const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS;66const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS;67const ALPHA_MODE_BLEND = 2 << Self::ALPHA_MODE_SHIFT_BITS;68const NONE = 0;69const UNINITIALIZED = 0xFFFF;70}71}7273impl SpriteMaterialFlags {74const ALPHA_MODE_MASK_BITS: u32 = 0b11;75const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones();76}7778#[derive(ShaderType, Default)]79pub struct SpriteMaterialUniform {80pub color: Vec4,81pub flags: u32,82pub alpha_cutoff: f32,83pub vertex_scale: Vec2,84pub vertex_offset: Vec2,85pub uv_transform: Mat3,8687// tile shader def88pub tile_stretch_value: Vec2,8990// slice shader def91pub scale: Vec2,92pub min_inset: Vec2,93pub max_inset: Vec2,94pub side_stretch_value: Vec2,95pub center_stretch_value: Vec2,96}9798impl AsBindGroupShaderType<SpriteMaterialUniform> for SpriteMaterial {99fn as_bind_group_shader_type(100&self,101images: &RenderAssets<bevy_render::texture::GpuImage>,102) -> SpriteMaterialUniform {103let Some(image) = images.get(self.image.id()) else {104return SpriteMaterialUniform::default();105};106107let mut flags = SpriteMaterialFlags::NONE;108let mut alpha_cutoff = 0.5;109match self.alpha_mode {110AlphaMode2d::Opaque => flags |= SpriteMaterialFlags::ALPHA_MODE_OPAQUE,111AlphaMode2d::Mask(c) => {112alpha_cutoff = c;113flags |= SpriteMaterialFlags::ALPHA_MODE_MASK;114}115AlphaMode2d::Blend => flags |= SpriteMaterialFlags::ALPHA_MODE_BLEND,116};117118if self.flip_x {119flags |= SpriteMaterialFlags::FLIP_X;120}121if self.flip_y {122flags |= SpriteMaterialFlags::FLIP_Y;123}124125let mut image_size = image.size_2d().as_vec2();126127let mut quad_size = image_size;128let mut quad_offset = Vec2::ZERO;129let mut uv_transform = Affine2::default();130131if let Some(texture_atlas_layout) = &self.texture_atlas_layout {132let index = self133.texture_atlas_index134.clamp(0, texture_atlas_layout.textures.len() - 1);135136let rect = texture_atlas_layout.textures[index].as_rect();137138let ratio = rect.size() / image_size;139140uv_transform *= Affine2::from_scale(ratio);141uv_transform *= Affine2::from_translation(vec2(142rect.min.x / rect.size().x,143rect.min.y / rect.size().y,144));145146quad_size = rect.size();147image_size = rect.size();148}149150// rect selects a slice of the image to render, map the uv and change the quad scale to match the rect151if let Some(rect) = self.rect {152let ratio = rect.size() / image_size;153154uv_transform *= Affine2::from_scale(ratio);155uv_transform *= Affine2::from_translation(vec2(156rect.min.x / rect.size().x,157rect.min.y / rect.size().y,158));159160quad_size = rect.size();161image_size = rect.size();162}163164let mut tile_stretch_value = Vec2::ZERO;165166let mut scale = Vec2::ZERO;167let mut min_inset = Vec2::ZERO;168let mut max_inset = Vec2::ZERO;169let mut side_stretch_value = Vec2::ZERO;170let mut center_stretch_value = Vec2::ZERO;171172if let Some(custom_size) = self.custom_size {173match &self.image_mode {174SpriteImageMode::Auto => {175quad_size = custom_size;176}177SpriteImageMode::Scale(scaling_mode) => {178let quad_ratio = quad_size.x / quad_size.y;179let custom_ratio = custom_size.x / custom_size.y;180181let fill_size = || {182if quad_ratio > custom_ratio {183vec2(custom_size.y * quad_ratio, custom_size.y)184} else {185vec2(custom_size.x, custom_size.x / quad_ratio)186}187};188189let fit_size = || {190if quad_ratio > custom_ratio {191vec2(custom_size.x, custom_size.x / quad_ratio)192} else {193vec2(custom_size.y * quad_ratio, custom_size.y)194}195};196197match scaling_mode {198// Filling requires scaling the texture and cutting out the 'overflow'199// which is why we need to manipulate the UV.200SpriteScalingMode::FillCenter => {201let fill_size = fill_size();202uv_transform *= Affine2::from_scale(custom_size / fill_size);203uv_transform *= Affine2::from_translation(204(fill_size - custom_size) * 0.5 / custom_size,205);206quad_size = custom_size;207}208SpriteScalingMode::FillStart => {209let fill_size = fill_size();210uv_transform *= Affine2::from_scale(custom_size / fill_size);211quad_size = custom_size;212}213SpriteScalingMode::FillEnd => {214let fill_size = fill_size();215uv_transform *= Affine2::from_scale(custom_size / fill_size);216uv_transform *=217Affine2::from_translation((fill_size - custom_size) / custom_size);218quad_size = custom_size;219}220221// Fitting is easier since the whole texture will still be visible,222// so it's enough to just translate the quad and keep the UV as is.223SpriteScalingMode::FitCenter => {224let fit_size = fit_size();225quad_size = fit_size;226}227SpriteScalingMode::FitStart => {228let fit_size = fit_size();229quad_offset -= (custom_size - fit_size) * 0.5;230quad_size = fit_size;231}232SpriteScalingMode::FitEnd => {233let fit_size = fit_size();234quad_offset += (custom_size - fit_size) * 0.5;235quad_size = fit_size;236}237}238}239SpriteImageMode::Tiled {240tile_x,241tile_y,242stretch_value,243} => {244if *tile_x {245flags |= SpriteMaterialFlags::TILE_X;246}247if *tile_y {248flags |= SpriteMaterialFlags::TILE_Y;249}250251// This is the [0-1] x and y of where the UV should start repeating.252// E.g. if the stretch_value x is 0.2 and the UV x is 0.5, it will be mapped to 0.1 (0.5 - 0.2 * 2)253// and then be stretched over [0, 0.2] by translating it to (0.1 / 0.2) = 0.5,254// so it corresponds to the center of the texture.255tile_stretch_value = (image_size * stretch_value) / custom_size;256quad_size = custom_size;257}258SpriteImageMode::Sliced(slicer) => {259let quad_ratio = quad_size.x / quad_size.y;260let custom_ratio = custom_size.x / custom_size.y;261262if quad_ratio > custom_ratio {263scale = vec2(1.0, quad_ratio / custom_ratio);264} else {265scale = vec2(custom_ratio / quad_ratio, 1.0);266}267268min_inset = slicer.border.min_inset / quad_size;269max_inset = slicer.border.max_inset / quad_size;270271let corner_scale = slicer.max_corner_scale.clamp(f32::EPSILON, 1.0);272scale /= corner_scale;273274if let SliceScaleMode::Tile { stretch_value } = slicer.sides_scale_mode {275side_stretch_value = stretch_value276* (image_size * (1.0 - max_inset - min_inset))277/ (custom_size * (1.0 - max_inset / scale - min_inset / scale));278}279280if let SliceScaleMode::Tile { stretch_value } = slicer.center_scale_mode {281center_stretch_value = stretch_value282* (image_size * (1.0 - max_inset - min_inset))283/ (custom_size * (1.0 - max_inset / scale - min_inset / scale));284}285286quad_size = custom_size;287}288}289}290291quad_offset -= quad_size * self.anchor;292293SpriteMaterialUniform {294color: self.color.to_linear().to_vec4(),295flags: flags.bits(),296alpha_cutoff,297vertex_scale: quad_size,298vertex_offset: quad_offset,299uv_transform: uv_transform.into(),300301tile_stretch_value,302303scale,304min_inset,305max_inset,306side_stretch_value,307center_stretch_value,308}309}310}311312impl Material2d for SpriteMaterial {313fn vertex_shader() -> ShaderRef {314ShaderRef::Path(315AssetPath::from_path_buf(embedded_path!("sprite_material.wgsl"))316.with_source("embedded"),317)318}319320fn fragment_shader() -> ShaderRef {321ShaderRef::Path(322AssetPath::from_path_buf(embedded_path!("sprite_material.wgsl"))323.with_source("embedded"),324)325}326327fn alpha_mode(&self) -> AlphaMode2d {328self.alpha_mode329}330}331332impl SpriteMaterial {333/// Use the [`SpriteMesh`] to build a new material.334pub fn from_sprite_mesh(sprite: SpriteMesh) -> Self {335// convert SpriteAlphaMode to AlphaMode2d.336// (see the comment above SpriteAlphaMode for why these are different)337let alpha_mode = match sprite.alpha_mode {338SpriteAlphaMode::Blend => AlphaMode2d::Blend,339SpriteAlphaMode::Opaque => AlphaMode2d::Opaque,340SpriteAlphaMode::Mask(x) => AlphaMode2d::Mask(x),341};342343SpriteMaterial {344image: sprite.image,345texture_atlas: sprite.texture_atlas,346color: sprite.color,347flip_x: sprite.flip_x,348flip_y: sprite.flip_y,349custom_size: sprite.custom_size,350rect: sprite.rect,351image_mode: sprite.image_mode,352alpha_mode,353texture_atlas_layout: None,354texture_atlas_index: 0,355anchor: Vec2::ZERO,356}357}358}359360361