Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"files":{"Cargo.lock":"10aece640a106967d773ce47de8fc953f06595afd21ef201eea495b2b519fdcc","Cargo.toml":"5986d441959a7e2f3ac465d349771b8f454c7a5aab572e0f696a78258605b766","LICENSE-APACHE":"a6cba85bc92e0cff7a450b1d873c0eaa2e9fc96bf472df0247a26bec77bf3ff9","LICENSE-MIT":"508a77d2e7b51d98adeed32648ad124b7b30241a8e70b2e72c99f92d8e5874d1","README.md":"42fd6d04652b26bdb468a2a3c63a7af901cc776fd3fa63c3c7c8a7a5bf948b8a","src/lib.rs":"a15d160bc0f87ac73bdcf905e7c24bfa8a46fcd6a92b763373e6604b12b0eec1","src/mesh2d/color_material.rs":"8c1a5f126ca262554be827cdd5324204df04ac293bae58366855794822046137","src/mesh2d/color_material.wgsl":"62ae6f1423263141c649f03645f846f48501d9a8cf211612dd50c62b2899df64","src/mesh2d/material.rs":"33ba06c43852380e3004a5a690907906ceabeb1734396afe4979265b2bd86cbf","src/mesh2d/mesh.rs":"1868a8c38c4b911c3451ed245ffb90a2b5eef8e8f792f69acfc58fbd09049d7f","src/mesh2d/mesh2d.wgsl":"74c923a48eee5ad50d928e5547acae0e82a4a8b073283c6e92fa77afb618870f","src/mesh2d/mesh2d_bindings.wgsl":"d6b28dd7bb1b7075572a2d5a12465d49150eb6cc3b39f74202be4846cedb0d5b","src/mesh2d/mesh2d_functions.wgsl":"9ec25a7f220499eddcf5461ceccf41f6b040cc375c394a99109b64afd4467c60","src/mesh2d/mesh2d_types.wgsl":"bc0a8d35960854942a4f079335e425270ca78af4678d20dd8ad21dfa1bd62a81","src/mesh2d/mesh2d_vertex_output.wgsl":"444a7f09b354ab3e0158ee64f85e21e146d7bf72196f98f822eb511cd1ea883a","src/mesh2d/mesh2d_view_bindings.wgsl":"2274f3655ce3ffd6794978cb453bd7a3e7b0b2bb9d4d373ec5fa5f36d90fff50","src/mesh2d/mesh2d_view_types.wgsl":"23f87dac240938fab23c4b40b0316ed7082e03efeea948b745e8f1ecad1e9331","src/mesh2d/mod.rs":"04e1ec14862e64ce0e9101e282656bb16d8969f1c9057cb879bfdc89cd98ac31","src/mesh2d/wireframe2d.rs":"ffb1af3892a9758822ff2efbab9930d23a6b3cdbaba36ff6454e2be7b6f007cc","src/mesh2d/wireframe2d.wgsl":"c84d52786eaafc5c2d1d102271c0aa14bffe5758bb411947f242001d810f7d7b","src/picking_backend.rs":"4c8f09723df54a643327904290101fba9241c2365b9160e28cefe5fba4af5c8b","src/render/mod.rs":"9aa7b249e9b3b23db4a04b48484ad0a6b9b8f7fda445dea73c531692c862b0c2","src/render/sprite.wgsl":"982e2cc6c802bdd645c18f3176a93a9aeb80c1381bebe57ed613de5ba6231abe","src/render/sprite_view_bindings.wgsl":"d2efbfe2dd2f45f6c779bcf235977b94b67a6b3d4aedf1a077598d2e91aedc67","src/sprite.rs":"2c248961117d3f9b58a8cb5f69583d0c5a869b6e15ccc3b0fee4a1c4b6d83f2f","src/texture_slice/border_rect.rs":"5225ae138e08c5d319aabbc81e59dc81b20bebbacde19640ab14c0c430539a4e","src/texture_slice/computed_slices.rs":"06c5e9d376daddf78c9937423995a7f01ac255317b3948cc1304d8021744f9a7","src/texture_slice/mod.rs":"45abf0227ea2ad67556949d3aec3e7e0c46f547c743694905b51531efe1fab53","src/texture_slice/slicer.rs":"8c8baff4dc8b4039df00c366fca31c75368900a1fa7d89fc1096c26a1a5550dc"},"package":"6ccae7bab2cb956fb0434004c359e432a3a1a074a6ef4eb471f1fb099f0b620b"}

3192
vendor/bevy_sprite/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

161
vendor/bevy_sprite/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,161 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "bevy_sprite"
version = "0.16.1"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Provides sprite functionality for Bevy Engine"
homepage = "https://bevyengine.org"
readme = "README.md"
keywords = ["bevy"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/bevyengine/bevy"
resolver = "2"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = [
"-Zunstable-options",
"--generate-link-to-definition",
]
[features]
bevy_sprite_picking_backend = [
"bevy_picking",
"bevy_window",
]
webgl = []
webgpu = []
[lib]
name = "bevy_sprite"
path = "src/lib.rs"
[dependencies.bevy_app]
version = "0.16.1"
[dependencies.bevy_asset]
version = "0.16.1"
[dependencies.bevy_color]
version = "0.16.2"
[dependencies.bevy_core_pipeline]
version = "0.16.1"
[dependencies.bevy_derive]
version = "0.16.1"
[dependencies.bevy_ecs]
version = "0.16.1"
[dependencies.bevy_image]
version = "0.16.1"
[dependencies.bevy_math]
version = "0.16.1"
[dependencies.bevy_picking]
version = "0.16.1"
optional = true
[dependencies.bevy_platform]
version = "0.16.1"
features = ["std"]
default-features = false
[dependencies.bevy_reflect]
version = "0.16.1"
[dependencies.bevy_render]
version = "0.16.1"
[dependencies.bevy_transform]
version = "0.16.1"
[dependencies.bevy_utils]
version = "0.16.1"
[dependencies.bevy_window]
version = "0.16.1"
optional = true
[dependencies.bitflags]
version = "2.3"
[dependencies.bytemuck]
version = "1"
features = [
"derive",
"must_cast",
]
[dependencies.derive_more]
version = "1"
features = ["from"]
default-features = false
[dependencies.fixedbitset]
version = "0.5"
[dependencies.nonmax]
version = "0.5"
[dependencies.radsort]
version = "0.1"
[dependencies.tracing]
version = "0.1"
features = ["std"]
default-features = false
[lints.clippy]
alloc_instead_of_core = "warn"
allow_attributes = "warn"
allow_attributes_without_reason = "warn"
doc_markdown = "warn"
manual_let_else = "warn"
match_same_arms = "warn"
needless_lifetimes = "allow"
nonstandard_macro_braces = "warn"
print_stderr = "warn"
print_stdout = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
redundant_closure_for_method_calls = "warn"
redundant_else = "warn"
ref_as_ptr = "warn"
semicolon_if_nothing_returned = "warn"
std_instead_of_alloc = "warn"
std_instead_of_core = "warn"
too_long_first_doc_paragraph = "allow"
too_many_arguments = "allow"
type_complexity = "allow"
undocumented_unsafe_blocks = "warn"
unwrap_or_default = "warn"
[lints.rust]
missing_docs = "warn"
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "warn"
unused_qualifications = "warn"
[lints.rust.unexpected_cfgs]
level = "warn"
priority = 0
check-cfg = ["cfg(docsrs_dep)"]

176
vendor/bevy_sprite/LICENSE-APACHE vendored Normal file
View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

19
vendor/bevy_sprite/LICENSE-MIT vendored Normal file
View File

@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
vendor/bevy_sprite/README.md vendored Normal file
View File

@@ -0,0 +1,7 @@
# Bevy Sprite
[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license)
[![Crates.io](https://img.shields.io/crates/v/bevy_sprite.svg)](https://crates.io/crates/bevy_sprite)
[![Downloads](https://img.shields.io/crates/d/bevy_sprite.svg)](https://crates.io/crates/bevy_sprite)
[![Docs](https://docs.rs/bevy_sprite/badge.svg)](https://docs.rs/bevy_sprite/latest/bevy_sprite/)
[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy)

358
vendor/bevy_sprite/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,358 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
//! Provides 2D sprite rendering functionality.
extern crate alloc;
mod mesh2d;
#[cfg(feature = "bevy_sprite_picking_backend")]
mod picking_backend;
mod render;
mod sprite;
mod texture_slice;
/// The sprite prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[cfg(feature = "bevy_sprite_picking_backend")]
#[doc(hidden)]
pub use crate::picking_backend::{
SpritePickingCamera, SpritePickingMode, SpritePickingPlugin, SpritePickingSettings,
};
#[doc(hidden)]
pub use crate::{
sprite::{Sprite, SpriteImageMode},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, MeshMaterial2d, ScalingMode,
};
}
pub use mesh2d::*;
#[cfg(feature = "bevy_sprite_picking_backend")]
pub use picking_backend::*;
pub use render::*;
pub use sprite::*;
pub use texture_slice::*;
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, AssetEvents, Assets, Handle};
use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d};
use bevy_ecs::prelude::*;
use bevy_image::{prelude::*, TextureAtlasPlugin};
use bevy_render::{
batching::sort_binned_render_phase,
mesh::{Mesh, Mesh2d, MeshAabb},
primitives::Aabb,
render_phase::AddRenderCommand,
render_resource::{Shader, SpecializedRenderPipelines},
view::{NoFrustumCulling, VisibilitySystems},
ExtractSchedule, Render, RenderApp, RenderSet,
};
/// Adds support for 2D sprite rendering.
#[derive(Default)]
pub struct SpritePlugin;
pub const SPRITE_SHADER_HANDLE: Handle<Shader> =
weak_handle!("ed996613-54c0-49bd-81be-1c2d1a0d03c2");
pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("43947210-8df6-459a-8f2a-12f350d174cc");
/// System set for sprite rendering.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum SpriteSystem {
ExtractSprites,
ComputeSlices,
}
impl Plugin for SpritePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
SPRITE_SHADER_HANDLE,
"render/sprite.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
SPRITE_VIEW_BINDINGS_SHADER_HANDLE,
"render/sprite_view_bindings.wgsl",
Shader::from_wgsl
);
if !app.is_plugin_added::<TextureAtlasPlugin>() {
app.add_plugins(TextureAtlasPlugin);
}
app.register_type::<Sprite>()
.register_type::<SpriteImageMode>()
.register_type::<TextureSlicer>()
.register_type::<Anchor>()
.register_type::<Mesh2d>()
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
.add_systems(
PostUpdate,
(
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
(
compute_slices_on_asset_event.before(AssetEvents),
compute_slices_on_sprite_change,
)
.in_set(SpriteSystem::ComputeSlices),
),
);
#[cfg(feature = "bevy_sprite_picking_backend")]
app.add_plugins(SpritePickingPlugin);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<ImageBindGroups>()
.init_resource::<SpecializedRenderPipelines<SpritePipeline>>()
.init_resource::<SpriteMeta>()
.init_resource::<ExtractedSprites>()
.init_resource::<ExtractedSlices>()
.init_resource::<SpriteAssetEvents>()
.add_render_command::<Transparent2d, DrawSprite>()
.add_systems(
ExtractSchedule,
(
extract_sprites.in_set(SpriteSystem::ExtractSprites),
extract_sprite_events,
),
)
.add_systems(
Render,
(
queue_sprites
.in_set(RenderSet::Queue)
.ambiguous_with(queue_material2d_meshes::<ColorMaterial>),
prepare_sprite_image_bind_groups.in_set(RenderSet::PrepareBindGroups),
prepare_sprite_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
sort_binned_render_phase::<Opaque2d>.in_set(RenderSet::PhaseSort),
sort_binned_render_phase::<AlphaMask2d>.in_set(RenderSet::PhaseSort),
),
);
};
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<SpriteBatches>()
.init_resource::<SpritePipeline>();
}
}
}
/// System calculating and inserting an [`Aabb`] component to entities with either:
/// - a `Mesh2d` component,
/// - a `Sprite` and `Handle<Image>` components,
/// and without a [`NoFrustumCulling`] component.
///
/// Used in system set [`VisibilitySystems::CalculateBounds`].
pub fn calculate_bounds_2d(
mut commands: Commands,
meshes: Res<Assets<Mesh>>,
images: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
sprites_to_recalculate_aabb: Query<
(Entity, &Sprite),
(
Or<(Without<Aabb>, Changed<Sprite>)>,
Without<NoFrustumCulling>,
),
>,
) {
for (entity, mesh_handle) in &meshes_without_aabb {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
if let Some(aabb) = mesh.compute_aabb() {
commands.entity(entity).try_insert(aabb);
}
}
}
for (entity, sprite) in &sprites_to_recalculate_aabb {
if let Some(size) = sprite
.custom_size
.or_else(|| sprite.rect.map(|rect| rect.size()))
.or_else(|| match &sprite.texture_atlas {
// We default to the texture size for regular sprites
None => images.get(&sprite.image).map(Image::size_f32),
// We default to the drawn rect for atlas sprites
Some(atlas) => atlas
.texture_rect(&atlases)
.map(|rect| rect.size().as_vec2()),
})
{
let aabb = Aabb {
center: (-sprite.anchor.as_vec() * size).extend(0.0).into(),
half_extents: (0.5 * size).extend(0.0).into(),
};
commands.entity(entity).try_insert(aabb);
}
}
}
#[cfg(test)]
mod test {
use bevy_math::{Rect, Vec2, Vec3A};
use bevy_utils::default;
use super::*;
#[test]
fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() {
// Setup app
let mut app = App::new();
// Add resources and get handle to image
let mut image_assets = Assets::<Image>::default();
let image_handle = image_assets.add(Image::default());
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);
// Add system
app.add_systems(Update, calculate_bounds_2d);
// Add entities
let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
// Verify that the entity does not have an AABB
assert!(!app
.world()
.get_entity(entity)
.expect("Could not find entity")
.contains::<Aabb>());
// Run system
app.update();
// Verify the AABB exists
assert!(app
.world()
.get_entity(entity)
.expect("Could not find entity")
.contains::<Aabb>());
}
#[test]
fn calculate_bounds_2d_update_aabb_when_sprite_custom_size_changes_to_some() {
// Setup app
let mut app = App::new();
// Add resources and get handle to image
let mut image_assets = Assets::<Image>::default();
let image_handle = image_assets.add(Image::default());
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);
// Add system
app.add_systems(Update, calculate_bounds_2d);
// Add entities
let entity = app
.world_mut()
.spawn(Sprite {
custom_size: Some(Vec2::ZERO),
image: image_handle,
..default()
})
.id();
// Create initial AABB
app.update();
// Get the initial AABB
let first_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find initial AABB");
// Change `custom_size` of sprite
let mut binding = app
.world_mut()
.get_entity_mut(entity)
.expect("Could not find entity");
let mut sprite = binding
.get_mut::<Sprite>()
.expect("Could not find sprite component of entity");
sprite.custom_size = Some(Vec2::ONE);
// Re-run the `calculate_bounds_2d` system to get the new AABB
app.update();
// Get the re-calculated AABB
let second_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find second AABB");
// Check that the AABBs are not equal
assert_ne!(first_aabb, second_aabb);
}
#[test]
fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
// Setup app
let mut app = App::new();
// Add resources and get handle to image
let mut image_assets = Assets::<Image>::default();
let image_handle = image_assets.add(Image::default());
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);
// Add system
app.add_systems(Update, calculate_bounds_2d);
// Add entities
let entity = app
.world_mut()
.spawn(Sprite {
rect: Some(Rect::new(0., 0., 0.5, 1.)),
anchor: Anchor::TopRight,
image: image_handle,
..default()
})
.id();
// Create AABB
app.update();
// Get the AABB
let aabb = *app
.world_mut()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find AABB");
// Verify that the AABB is at the expected position
assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
// Verify that the AABB has the expected size
assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
}
}

View File

@@ -0,0 +1,161 @@
use crate::{AlphaMode2d, Material2d, Material2dPlugin};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Asset, AssetApp, Assets, Handle};
use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba};
use bevy_image::Image;
use bevy_math::{Affine2, Mat3, Vec4};
use bevy_reflect::prelude::*;
use bevy_render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage};
pub const COLOR_MATERIAL_SHADER_HANDLE: Handle<Shader> =
weak_handle!("92e0e6e9-ed0b-4db3-89ab-5f65d3678250");
#[derive(Default)]
pub struct ColorMaterialPlugin;
impl Plugin for ColorMaterialPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
COLOR_MATERIAL_SHADER_HANDLE,
"color_material.wgsl",
Shader::from_wgsl
);
app.add_plugins(Material2dPlugin::<ColorMaterial>::default())
.register_asset_reflect::<ColorMaterial>();
// Initialize the default material handle.
app.world_mut()
.resource_mut::<Assets<ColorMaterial>>()
.insert(
&Handle::<ColorMaterial>::default(),
ColorMaterial {
color: Color::srgb(1.0, 0.0, 1.0),
..Default::default()
},
);
}
}
/// A [2d material](Material2d) that renders [2d meshes](crate::Mesh2d) with a texture tinted by a uniform color
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
#[reflect(Default, Debug, Clone)]
#[uniform(0, ColorMaterialUniform)]
pub struct ColorMaterial {
pub color: Color,
pub alpha_mode: AlphaMode2d,
pub uv_transform: Affine2,
#[texture(1)]
#[sampler(2)]
pub texture: Option<Handle<Image>>,
}
impl ColorMaterial {
/// Creates a new material from a given color
pub fn from_color(color: impl Into<Color>) -> Self {
Self::from(color.into())
}
}
impl Default for ColorMaterial {
fn default() -> Self {
ColorMaterial {
color: Color::WHITE,
uv_transform: Affine2::default(),
texture: None,
// TODO should probably default to AlphaMask once supported?
alpha_mode: AlphaMode2d::Blend,
}
}
}
impl From<Color> for ColorMaterial {
fn from(color: Color) -> Self {
ColorMaterial {
color,
alpha_mode: if color.alpha() < 1.0 {
AlphaMode2d::Blend
} else {
AlphaMode2d::Opaque
},
..Default::default()
}
}
}
impl From<Handle<Image>> for ColorMaterial {
fn from(texture: Handle<Image>) -> Self {
ColorMaterial {
texture: Some(texture),
..Default::default()
}
}
}
// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/color_material.wgsl!
bitflags::bitflags! {
#[repr(transparent)]
pub struct ColorMaterialFlags: u32 {
const TEXTURE = 1 << 0;
/// Bitmask reserving bits for the [`AlphaMode2d`]
/// Values are just sequential values bitshifted into
/// the bitmask, and can range from 0 to 3.
const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS;
const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS;
const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS;
const ALPHA_MODE_BLEND = 2 << Self::ALPHA_MODE_SHIFT_BITS;
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
impl ColorMaterialFlags {
const ALPHA_MODE_MASK_BITS: u32 = 0b11;
const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones();
}
/// The GPU representation of the uniform data of a [`ColorMaterial`].
#[derive(Clone, Default, ShaderType)]
pub struct ColorMaterialUniform {
pub color: Vec4,
pub uv_transform: Mat3,
pub flags: u32,
pub alpha_cutoff: f32,
}
impl AsBindGroupShaderType<ColorMaterialUniform> for ColorMaterial {
fn as_bind_group_shader_type(&self, _images: &RenderAssets<GpuImage>) -> ColorMaterialUniform {
let mut flags = ColorMaterialFlags::NONE;
if self.texture.is_some() {
flags |= ColorMaterialFlags::TEXTURE;
}
// Defaults to 0.5 like in 3d
let mut alpha_cutoff = 0.5;
match self.alpha_mode {
AlphaMode2d::Opaque => flags |= ColorMaterialFlags::ALPHA_MODE_OPAQUE,
AlphaMode2d::Mask(c) => {
alpha_cutoff = c;
flags |= ColorMaterialFlags::ALPHA_MODE_MASK;
}
AlphaMode2d::Blend => flags |= ColorMaterialFlags::ALPHA_MODE_BLEND,
};
ColorMaterialUniform {
color: LinearRgba::from(self.color).to_f32_array().into(),
uv_transform: self.uv_transform.into(),
flags: flags.bits(),
alpha_cutoff,
}
}
}
impl Material2d for ColorMaterial {
fn fragment_shader() -> ShaderRef {
COLOR_MATERIAL_SHADER_HANDLE.into()
}
fn alpha_mode(&self) -> AlphaMode2d {
self.alpha_mode
}
}

View File

@@ -0,0 +1,72 @@
#import bevy_sprite::{
mesh2d_vertex_output::VertexOutput,
mesh2d_view_bindings::view,
}
#ifdef TONEMAP_IN_SHADER
#import bevy_core_pipeline::tonemapping
#endif
struct ColorMaterial {
color: vec4<f32>,
uv_transform: mat3x3<f32>,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
alpha_cutoff: f32,
};
const COLOR_MATERIAL_FLAGS_TEXTURE_BIT: u32 = 1u;
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3221225472u; // (0b11u32 << 30)
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 30)
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1073741824u; // (1u32 << 30)
const COLOR_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2147483648u; // (2u32 << 30)
@group(2) @binding(0) var<uniform> material: ColorMaterial;
@group(2) @binding(1) var texture: texture_2d<f32>;
@group(2) @binding(2) var texture_sampler: sampler;
@fragment
fn fragment(
mesh: VertexOutput,
) -> @location(0) vec4<f32> {
var output_color: vec4<f32> = material.color;
#ifdef VERTEX_COLORS
output_color = output_color * mesh.color;
#endif
let uv = (material.uv_transform * vec3(mesh.uv, 1.0)).xy;
if ((material.flags & COLOR_MATERIAL_FLAGS_TEXTURE_BIT) != 0u) {
output_color = output_color * textureSample(texture, texture_sampler, uv);
}
output_color = alpha_discard(material, output_color);
#ifdef TONEMAP_IN_SHADER
output_color = tonemapping::tone_mapping(output_color, view.color_grading);
#endif
return output_color;
}
fn alpha_discard(material: ColorMaterial, output_color: vec4<f32>) -> vec4<f32> {
var color = output_color;
let alpha_mode = material.flags & COLOR_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
if alpha_mode == COLOR_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 == COLOR_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;
}

1004
vendor/bevy_sprite/src/mesh2d/material.rs vendored Normal file

File diff suppressed because it is too large Load Diff

921
vendor/bevy_sprite/src/mesh2d/mesh.rs vendored Normal file
View File

@@ -0,0 +1,921 @@
use bevy_app::Plugin;
use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle};
use crate::{tonemapping_pipeline_key, Material2dBindGroupId};
use bevy_core_pipeline::tonemapping::DebandDither;
use bevy_core_pipeline::{
core_2d::{AlphaMask2d, Camera2d, Opaque2d, Transparent2d, CORE_2D_DEPTH_FORMAT},
tonemapping::{
get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts,
},
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::component::Tick;
use bevy_ecs::system::SystemChangeTick;
use bevy_ecs::{
prelude::*,
query::ROQueryItem,
system::{lifetimeless::*, SystemParamItem, SystemState},
};
use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo};
use bevy_math::{Affine3, Vec4};
use bevy_render::mesh::MeshTag;
use bevy_render::prelude::Msaa;
use bevy_render::RenderSet::PrepareAssets;
use bevy_render::{
batching::{
gpu_preprocessing::IndirectParametersCpuMetadata,
no_gpu_preprocessing::{
self, batch_and_prepare_binned_render_phase, batch_and_prepare_sorted_render_phase,
write_batched_instance_buffer, BatchedInstanceBuffer,
},
GetBatchData, GetFullBatchData, NoAutomaticBatching,
},
globals::{GlobalsBuffer, GlobalsUniform},
mesh::{
allocator::MeshAllocator, Mesh, Mesh2d, MeshVertexBufferLayoutRef, RenderMesh,
RenderMeshBufferInfo,
},
render_asset::RenderAssets,
render_phase::{
sweep_old_entities, PhaseItem, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,
TrackedRenderPass,
},
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue},
sync_world::{MainEntity, MainEntityHashMap},
texture::{DefaultImageSampler, FallbackImage, GpuImage},
view::{
ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility,
},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::GlobalTransform;
use nonmax::NonMaxU32;
use tracing::error;
#[derive(Default)]
pub struct Mesh2dRenderPlugin;
pub const MESH2D_VERTEX_OUTPUT: Handle<Shader> =
weak_handle!("71e279c7-85a0-46ac-9a76-1586cbf506d0");
pub const MESH2D_VIEW_TYPES_HANDLE: Handle<Shader> =
weak_handle!("01087b0d-91e9-46ac-8628-dfe19a7d4b83");
pub const MESH2D_VIEW_BINDINGS_HANDLE: Handle<Shader> =
weak_handle!("fbdd8b80-503d-4688-bcec-db29ab4620b2");
pub const MESH2D_TYPES_HANDLE: Handle<Shader> =
weak_handle!("199f2089-6e99-4348-9bb1-d82816640a7f");
pub const MESH2D_BINDINGS_HANDLE: Handle<Shader> =
weak_handle!("a7bd44cc-0580-4427-9a00-721cf386b6e4");
pub const MESH2D_FUNCTIONS_HANDLE: Handle<Shader> =
weak_handle!("0d08ff71-68c1-4017-83e2-bfc34d285c51");
pub const MESH2D_SHADER_HANDLE: Handle<Shader> =
weak_handle!("91a7602b-df95-4ea3-9d97-076abcb69d91");
impl Plugin for Mesh2dRenderPlugin {
fn build(&self, app: &mut bevy_app::App) {
load_internal_asset!(
app,
MESH2D_VERTEX_OUTPUT,
"mesh2d_vertex_output.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_VIEW_TYPES_HANDLE,
"mesh2d_view_types.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_VIEW_BINDINGS_HANDLE,
"mesh2d_view_bindings.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_TYPES_HANDLE,
"mesh2d_types.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESH2D_FUNCTIONS_HANDLE,
"mesh2d_functions.wgsl",
Shader::from_wgsl
);
load_internal_asset!(app, MESH2D_SHADER_HANDLE, "mesh2d.wgsl", Shader::from_wgsl);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<ViewKeyCache>()
.init_resource::<RenderMesh2dInstances>()
.init_resource::<SpecializedMeshPipelines<Mesh2dPipeline>>()
.add_systems(ExtractSchedule, extract_mesh2d)
.add_systems(
Render,
(
(
sweep_old_entities::<Opaque2d>,
sweep_old_entities::<AlphaMask2d>,
)
.in_set(RenderSet::QueueSweep),
batch_and_prepare_binned_render_phase::<Opaque2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
batch_and_prepare_binned_render_phase::<AlphaMask2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
batch_and_prepare_sorted_render_phase::<Transparent2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
write_batched_instance_buffer::<Mesh2dPipeline>
.in_set(RenderSet::PrepareResourcesFlush),
prepare_mesh2d_bind_group.in_set(RenderSet::PrepareBindGroups),
prepare_mesh2d_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
no_gpu_preprocessing::clear_batched_cpu_instance_buffers::<Mesh2dPipeline>
.in_set(RenderSet::Cleanup)
.after(RenderSet::Render),
),
);
}
}
fn finish(&self, app: &mut bevy_app::App) {
let mut mesh_bindings_shader_defs = Vec::with_capacity(1);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
let render_device = render_app.world().resource::<RenderDevice>();
let batched_instance_buffer =
BatchedInstanceBuffer::<Mesh2dUniform>::new(render_device);
if let Some(per_object_buffer_batch_size) =
GpuArrayBuffer::<Mesh2dUniform>::batch_size(render_device)
{
mesh_bindings_shader_defs.push(ShaderDefVal::UInt(
"PER_OBJECT_BUFFER_BATCH_SIZE".into(),
per_object_buffer_batch_size,
));
}
render_app
.insert_resource(batched_instance_buffer)
.init_resource::<Mesh2dPipeline>()
.init_resource::<ViewKeyCache>()
.init_resource::<ViewSpecializationTicks>()
.add_systems(
Render,
check_views_need_specialization.in_set(PrepareAssets),
);
}
// Load the mesh_bindings shader module here as it depends on runtime information about
// whether storage buffers are supported, or the maximum uniform buffer binding size.
load_internal_asset!(
app,
MESH2D_BINDINGS_HANDLE,
"mesh2d_bindings.wgsl",
Shader::from_wgsl_with_defs,
mesh_bindings_shader_defs
);
}
}
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
pub struct ViewKeyCache(MainEntityHashMap<Mesh2dPipelineKey>);
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
pub struct ViewSpecializationTicks(MainEntityHashMap<Tick>);
pub fn check_views_need_specialization(
mut view_key_cache: ResMut<ViewKeyCache>,
mut view_specialization_ticks: ResMut<ViewSpecializationTicks>,
views: Query<(
&MainEntity,
&ExtractedView,
&Msaa,
Option<&Tonemapping>,
Option<&DebandDither>,
)>,
ticks: SystemChangeTick,
) {
for (view_entity, view, msaa, tonemapping, dither) in &views {
let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
| Mesh2dPipelineKey::from_hdr(view.hdr);
if !view.hdr {
if let Some(tonemapping) = tonemapping {
view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER;
view_key |= tonemapping_pipeline_key(*tonemapping);
}
if let Some(DebandDither::Enabled) = dither {
view_key |= Mesh2dPipelineKey::DEBAND_DITHER;
}
}
if !view_key_cache
.get_mut(view_entity)
.is_some_and(|current_key| *current_key == view_key)
{
view_key_cache.insert(*view_entity, view_key);
view_specialization_ticks.insert(*view_entity, ticks.this_run());
}
}
}
#[derive(Component)]
pub struct Mesh2dTransforms {
pub world_from_local: Affine3,
pub flags: u32,
}
#[derive(ShaderType, Clone, Copy)]
pub struct Mesh2dUniform {
// Affine 4x3 matrix transposed to 3x4
pub world_from_local: [Vec4; 3],
// 3x3 matrix packed in mat2x4 and f32 as:
// [0].xyz, [1].x,
// [1].yz, [2].xy
// [2].z
pub local_from_world_transpose_a: [Vec4; 2],
pub local_from_world_transpose_b: f32,
pub flags: u32,
pub tag: u32,
}
impl Mesh2dUniform {
fn from_components(mesh_transforms: &Mesh2dTransforms, tag: u32) -> Self {
let (local_from_world_transpose_a, local_from_world_transpose_b) =
mesh_transforms.world_from_local.inverse_transpose_3x3();
Self {
world_from_local: mesh_transforms.world_from_local.to_transpose(),
local_from_world_transpose_a,
local_from_world_transpose_b,
flags: mesh_transforms.flags,
tag,
}
}
}
// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/mesh2d.wgsl!
bitflags::bitflags! {
#[repr(transparent)]
pub struct MeshFlags: u32 {
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
pub struct RenderMesh2dInstance {
pub transforms: Mesh2dTransforms,
pub mesh_asset_id: AssetId<Mesh>,
pub material_bind_group_id: Material2dBindGroupId,
pub automatic_batching: bool,
pub tag: u32,
}
#[derive(Default, Resource, Deref, DerefMut)]
pub struct RenderMesh2dInstances(MainEntityHashMap<RenderMesh2dInstance>);
#[derive(Component, Default)]
pub struct Mesh2dMarker;
pub fn extract_mesh2d(
mut render_mesh_instances: ResMut<RenderMesh2dInstances>,
query: Extract<
Query<(
Entity,
&ViewVisibility,
&GlobalTransform,
&Mesh2d,
Option<&MeshTag>,
Has<NoAutomaticBatching>,
)>,
>,
) {
render_mesh_instances.clear();
for (entity, view_visibility, transform, handle, tag, no_automatic_batching) in &query {
if !view_visibility.get() {
continue;
}
render_mesh_instances.insert(
entity.into(),
RenderMesh2dInstance {
transforms: Mesh2dTransforms {
world_from_local: (&transform.affine()).into(),
flags: MeshFlags::empty().bits(),
},
mesh_asset_id: handle.0.id(),
material_bind_group_id: Material2dBindGroupId::default(),
automatic_batching: !no_automatic_batching,
tag: tag.map_or(0, |i| **i),
},
);
}
}
#[derive(Resource, Clone)]
pub struct Mesh2dPipeline {
pub view_layout: BindGroupLayout,
pub mesh_layout: BindGroupLayout,
// This dummy white texture is to be used in place of optional textures
pub dummy_white_gpu_image: GpuImage,
pub per_object_buffer_batch_size: Option<u32>,
}
impl FromWorld for Mesh2dPipeline {
fn from_world(world: &mut World) -> Self {
let mut system_state: SystemState<(
Res<RenderDevice>,
Res<RenderQueue>,
Res<DefaultImageSampler>,
)> = SystemState::new(world);
let (render_device, render_queue, default_sampler) = system_state.get_mut(world);
let render_device = render_device.into_inner();
let tonemapping_lut_entries = get_lut_bind_group_layout_entries();
let view_layout = render_device.create_bind_group_layout(
"mesh2d_view_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX_FRAGMENT,
(
(0, uniform_buffer::<ViewUniform>(true)),
(1, uniform_buffer::<GlobalsUniform>(false)),
(
2,
tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT),
),
(
3,
tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT),
),
),
),
);
let mesh_layout = render_device.create_bind_group_layout(
"mesh2d_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
GpuArrayBuffer::<Mesh2dUniform>::binding_layout(render_device),
),
);
// A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures
let dummy_white_gpu_image = {
let image = Image::default();
let texture = render_device.create_texture(&image.texture_descriptor);
let sampler = match image.sampler {
ImageSampler::Default => (**default_sampler).clone(),
ImageSampler::Descriptor(ref descriptor) => {
render_device.create_sampler(&descriptor.as_wgpu())
}
};
let format_size = image.texture_descriptor.format.pixel_size();
render_queue.write_texture(
texture.as_image_copy(),
image.data.as_ref().expect("Image has no data"),
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(image.width() * format_size as u32),
rows_per_image: None,
},
image.texture_descriptor.size,
);
let texture_view = texture.create_view(&TextureViewDescriptor::default());
GpuImage {
texture,
texture_view,
texture_format: image.texture_descriptor.format,
sampler,
size: image.texture_descriptor.size,
mip_level_count: image.texture_descriptor.mip_level_count,
}
};
Mesh2dPipeline {
view_layout,
mesh_layout,
dummy_white_gpu_image,
per_object_buffer_batch_size: GpuArrayBuffer::<Mesh2dUniform>::batch_size(
render_device,
),
}
}
}
impl Mesh2dPipeline {
pub fn get_image_texture<'a>(
&'a self,
gpu_images: &'a RenderAssets<GpuImage>,
handle_option: &Option<Handle<Image>>,
) -> Option<(&'a TextureView, &'a Sampler)> {
if let Some(handle) = handle_option {
let gpu_image = gpu_images.get(handle)?;
Some((&gpu_image.texture_view, &gpu_image.sampler))
} else {
Some((
&self.dummy_white_gpu_image.texture_view,
&self.dummy_white_gpu_image.sampler,
))
}
}
}
impl GetBatchData for Mesh2dPipeline {
type Param = (
SRes<RenderMesh2dInstances>,
SRes<RenderAssets<RenderMesh>>,
SRes<MeshAllocator>,
);
type CompareData = (Material2dBindGroupId, AssetId<Mesh>);
type BufferData = Mesh2dUniform;
fn get_batch_data(
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
(_entity, main_entity): (Entity, MainEntity),
) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
let mesh_instance = mesh_instances.get(&main_entity)?;
Some((
Mesh2dUniform::from_components(&mesh_instance.transforms, mesh_instance.tag),
mesh_instance.automatic_batching.then_some((
mesh_instance.material_bind_group_id,
mesh_instance.mesh_asset_id,
)),
))
}
}
impl GetFullBatchData for Mesh2dPipeline {
type BufferInputData = ();
fn get_binned_batch_data(
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
main_entity: MainEntity,
) -> Option<Self::BufferData> {
let mesh_instance = mesh_instances.get(&main_entity)?;
Some(Mesh2dUniform::from_components(
&mesh_instance.transforms,
mesh_instance.tag,
))
}
fn get_index_and_compare_data(
_: &SystemParamItem<Self::Param>,
_query_item: MainEntity,
) -> Option<(NonMaxU32, Option<Self::CompareData>)> {
error!(
"`get_index_and_compare_data` is only intended for GPU mesh uniform building, \
but this is not yet implemented for 2d meshes"
);
None
}
fn get_binned_index(
_: &SystemParamItem<Self::Param>,
_query_item: MainEntity,
) -> Option<NonMaxU32> {
error!(
"`get_binned_index` is only intended for GPU mesh uniform building, \
but this is not yet implemented for 2d meshes"
);
None
}
fn write_batch_indirect_parameters_metadata(
indexed: bool,
base_output_index: u32,
batch_set_index: Option<NonMaxU32>,
indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::UntypedPhaseIndirectParametersBuffers,
indirect_parameters_offset: u32,
) {
// Note that `IndirectParameters` covers both of these structures, even
// though they actually have distinct layouts. See the comment above that
// type for more information.
let indirect_parameters = IndirectParametersCpuMetadata {
base_output_index,
batch_set_index: match batch_set_index {
None => !0,
Some(batch_set_index) => u32::from(batch_set_index),
},
};
if indexed {
indirect_parameters_buffer
.indexed
.set(indirect_parameters_offset, indirect_parameters);
} else {
indirect_parameters_buffer
.non_indexed
.set(indirect_parameters_offset, indirect_parameters);
}
}
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
// NOTE: Apparently quadro drivers support up to 64x MSAA.
// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA.
// FIXME: make normals optional?
pub struct Mesh2dPipelineKey: u32 {
const NONE = 0;
const HDR = 1 << 0;
const TONEMAP_IN_SHADER = 1 << 1;
const DEBAND_DITHER = 1 << 2;
const BLEND_ALPHA = 1 << 3;
const MAY_DISCARD = 1 << 4;
const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS;
const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS;
const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS;
}
}
impl Mesh2dPipelineKey {
const MSAA_MASK_BITS: u32 = 0b111;
const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones();
const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111;
const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3;
const TONEMAP_METHOD_MASK_BITS: u32 = 0b111;
const TONEMAP_METHOD_SHIFT_BITS: u32 =
Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones();
pub fn from_msaa_samples(msaa_samples: u32) -> Self {
let msaa_bits =
(msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS;
Self::from_bits_retain(msaa_bits)
}
pub fn from_hdr(hdr: bool) -> Self {
if hdr {
Mesh2dPipelineKey::HDR
} else {
Mesh2dPipelineKey::NONE
}
}
pub fn msaa_samples(&self) -> u32 {
1 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS)
}
pub fn from_primitive_topology(primitive_topology: PrimitiveTopology) -> Self {
let primitive_topology_bits = ((primitive_topology as u32)
& Self::PRIMITIVE_TOPOLOGY_MASK_BITS)
<< Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS;
Self::from_bits_retain(primitive_topology_bits)
}
pub fn primitive_topology(&self) -> PrimitiveTopology {
let primitive_topology_bits = (self.bits() >> Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS)
& Self::PRIMITIVE_TOPOLOGY_MASK_BITS;
match primitive_topology_bits {
x if x == PrimitiveTopology::PointList as u32 => PrimitiveTopology::PointList,
x if x == PrimitiveTopology::LineList as u32 => PrimitiveTopology::LineList,
x if x == PrimitiveTopology::LineStrip as u32 => PrimitiveTopology::LineStrip,
x if x == PrimitiveTopology::TriangleList as u32 => PrimitiveTopology::TriangleList,
x if x == PrimitiveTopology::TriangleStrip as u32 => PrimitiveTopology::TriangleStrip,
_ => PrimitiveTopology::default(),
}
}
}
impl SpecializedMeshPipeline for Mesh2dPipeline {
type Key = Mesh2dPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut shader_defs = Vec::new();
let mut vertex_attributes = Vec::new();
if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
shader_defs.push("VERTEX_POSITIONS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
}
if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) {
shader_defs.push("VERTEX_NORMALS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1));
}
if layout.0.contains(Mesh::ATTRIBUTE_UV_0) {
shader_defs.push("VERTEX_UVS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(2));
}
if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) {
shader_defs.push("VERTEX_TANGENTS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3));
}
if layout.0.contains(Mesh::ATTRIBUTE_COLOR) {
shader_defs.push("VERTEX_COLORS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4));
}
if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) {
shader_defs.push("TONEMAP_IN_SHADER".into());
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
2,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
3,
));
let method = key.intersection(Mesh2dPipelineKey::TONEMAP_METHOD_RESERVED_BITS);
match method {
Mesh2dPipelineKey::TONEMAP_METHOD_NONE => {
shader_defs.push("TONEMAP_METHOD_NONE".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD => {
shader_defs.push("TONEMAP_METHOD_REINHARD".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE => {
shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED => {
shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_AGX => {
shader_defs.push("TONEMAP_METHOD_AGX".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM => {
shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC => {
shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());
}
Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE => {
shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());
}
_ => {}
}
// Debanding is tied to tonemapping in the shader, cannot run without it.
if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) {
shader_defs.push("DEBAND_DITHER".into());
}
}
if key.contains(Mesh2dPipelineKey::MAY_DISCARD) {
shader_defs.push("MAY_DISCARD".into());
}
let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
let format = match key.contains(Mesh2dPipelineKey::HDR) {
true => ViewTarget::TEXTURE_FORMAT_HDR,
false => TextureFormat::bevy_default(),
};
let (depth_write_enabled, label, blend);
if key.contains(Mesh2dPipelineKey::BLEND_ALPHA) {
label = "transparent_mesh2d_pipeline";
blend = Some(BlendState::ALPHA_BLENDING);
depth_write_enabled = false;
} else {
label = "opaque_mesh2d_pipeline";
blend = None;
depth_write_enabled = true;
}
Ok(RenderPipelineDescriptor {
vertex: VertexState {
shader: MESH2D_SHADER_HANDLE,
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: MESH2D_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format,
blend,
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.view_layout.clone(), self.mesh_layout.clone()],
push_constant_ranges: vec![],
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: key.primitive_topology(),
strip_index_format: None,
},
depth_stencil: Some(DepthStencilState {
format: CORE_2D_DEPTH_FORMAT,
depth_write_enabled,
depth_compare: CompareFunction::GreaterEqual,
stencil: StencilState {
front: StencilFaceState::IGNORE,
back: StencilFaceState::IGNORE,
read_mask: 0,
write_mask: 0,
},
bias: DepthBiasState {
constant: 0,
slope_scale: 0.0,
clamp: 0.0,
},
}),
multisample: MultisampleState {
count: key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some(label.into()),
zero_initialize_workgroup_memory: false,
})
}
}
#[derive(Resource)]
pub struct Mesh2dBindGroup {
pub value: BindGroup,
}
pub fn prepare_mesh2d_bind_group(
mut commands: Commands,
mesh2d_pipeline: Res<Mesh2dPipeline>,
render_device: Res<RenderDevice>,
mesh2d_uniforms: Res<BatchedInstanceBuffer<Mesh2dUniform>>,
) {
if let Some(binding) = mesh2d_uniforms.instance_data_binding() {
commands.insert_resource(Mesh2dBindGroup {
value: render_device.create_bind_group(
"mesh2d_bind_group",
&mesh2d_pipeline.mesh_layout,
&BindGroupEntries::single(binding),
),
});
}
}
#[derive(Component)]
pub struct Mesh2dViewBindGroup {
pub value: BindGroup,
}
pub fn prepare_mesh2d_view_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
mesh2d_pipeline: Res<Mesh2dPipeline>,
view_uniforms: Res<ViewUniforms>,
views: Query<(Entity, &Tonemapping), (With<ExtractedView>, With<Camera2d>)>,
globals_buffer: Res<GlobalsBuffer>,
tonemapping_luts: Res<TonemappingLuts>,
images: Res<RenderAssets<GpuImage>>,
fallback_image: Res<FallbackImage>,
) {
let (Some(view_binding), Some(globals)) = (
view_uniforms.uniforms.binding(),
globals_buffer.buffer.binding(),
) else {
return;
};
for (entity, tonemapping) in &views {
let lut_bindings =
get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image);
let view_bind_group = render_device.create_bind_group(
"mesh2d_view_bind_group",
&mesh2d_pipeline.view_layout,
&BindGroupEntries::with_indices((
(0, view_binding.clone()),
(1, globals.clone()),
(2, lut_bindings.0),
(3, lut_bindings.1),
)),
);
commands.entity(entity).insert(Mesh2dViewBindGroup {
value: view_bind_group,
});
}
}
pub struct SetMesh2dViewBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMesh2dViewBindGroup<I> {
type Param = ();
type ViewQuery = (Read<ViewUniformOffset>, Read<Mesh2dViewBindGroup>);
type ItemQuery = ();
#[inline]
fn render<'w>(
_item: &P,
(view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>,
_view: Option<()>,
_param: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
pass.set_bind_group(I, &mesh2d_view_bind_group.value, &[view_uniform.offset]);
RenderCommandResult::Success
}
}
pub struct SetMesh2dBindGroup<const I: usize>;
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMesh2dBindGroup<I> {
type Param = SRes<Mesh2dBindGroup>;
type ViewQuery = ();
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
_item_query: Option<()>,
mesh2d_bind_group: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let mut dynamic_offsets: [u32; 1] = Default::default();
let mut offset_count = 0;
if let PhaseItemExtraIndex::DynamicOffset(dynamic_offset) = item.extra_index() {
dynamic_offsets[offset_count] = dynamic_offset;
offset_count += 1;
}
pass.set_bind_group(
I,
&mesh2d_bind_group.into_inner().value,
&dynamic_offsets[..offset_count],
);
RenderCommandResult::Success
}
}
pub struct DrawMesh2d;
impl<P: PhaseItem> RenderCommand<P> for DrawMesh2d {
type Param = (
SRes<RenderAssets<RenderMesh>>,
SRes<RenderMesh2dInstances>,
SRes<MeshAllocator>,
);
type ViewQuery = ();
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
_item_query: Option<()>,
(meshes, render_mesh2d_instances, mesh_allocator): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let meshes = meshes.into_inner();
let render_mesh2d_instances = render_mesh2d_instances.into_inner();
let mesh_allocator = mesh_allocator.into_inner();
let Some(RenderMesh2dInstance { mesh_asset_id, .. }) =
render_mesh2d_instances.get(&item.main_entity())
else {
return RenderCommandResult::Skip;
};
let Some(gpu_mesh) = meshes.get(*mesh_asset_id) else {
return RenderCommandResult::Skip;
};
let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(mesh_asset_id) else {
return RenderCommandResult::Skip;
};
pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
let batch_range = item.batch_range();
match &gpu_mesh.buffer_info {
RenderMeshBufferInfo::Indexed {
index_format,
count,
} => {
let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(mesh_asset_id)
else {
return RenderCommandResult::Skip;
};
pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format);
pass.draw_indexed(
index_buffer_slice.range.start..(index_buffer_slice.range.start + count),
vertex_buffer_slice.range.start as i32,
batch_range.clone(),
);
}
RenderMeshBufferInfo::NonIndexed => {
pass.draw(vertex_buffer_slice.range, batch_range.clone());
}
}
RenderCommandResult::Success
}
}

View File

@@ -0,0 +1,76 @@
#import bevy_sprite::{
mesh2d_functions as mesh_functions,
mesh2d_vertex_output::VertexOutput,
mesh2d_view_bindings::view,
}
#ifdef TONEMAP_IN_SHADER
#import bevy_core_pipeline::tonemapping
#endif
struct Vertex {
@builtin(instance_index) instance_index: u32,
#ifdef VERTEX_POSITIONS
@location(0) position: vec3<f32>,
#endif
#ifdef VERTEX_NORMALS
@location(1) normal: vec3<f32>,
#endif
#ifdef VERTEX_UVS
@location(2) uv: vec2<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(3) tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(4) color: vec4<f32>,
#endif
};
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef VERTEX_UVS
out.uv = vertex.uv;
#endif
#ifdef VERTEX_POSITIONS
var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index);
out.world_position = mesh_functions::mesh2d_position_local_to_world(
world_from_local,
vec4<f32>(vertex.position, 1.0)
);
out.position = mesh_functions::mesh2d_position_world_to_clip(out.world_position);
#endif
#ifdef VERTEX_NORMALS
out.world_normal = mesh_functions::mesh2d_normal_local_to_world(vertex.normal, vertex.instance_index);
#endif
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh2d_tangent_local_to_world(
world_from_local,
vertex.tangent
);
#endif
#ifdef VERTEX_COLORS
out.color = vertex.color;
#endif
return out;
}
@fragment
fn fragment(
in: VertexOutput,
) -> @location(0) vec4<f32> {
#ifdef VERTEX_COLORS
var color = in.color;
#ifdef TONEMAP_IN_SHADER
color = tonemapping::tone_mapping(color, view.color_grading);
#endif
return color;
#else
return vec4<f32>(1.0, 0.0, 1.0, 1.0);
#endif
}

View File

@@ -0,0 +1,9 @@
#define_import_path bevy_sprite::mesh2d_bindings
#import bevy_sprite::mesh2d_types::Mesh2d
#ifdef PER_OBJECT_BUFFER_BATCH_SIZE
@group(1) @binding(0) var<uniform> mesh: array<Mesh2d, #{PER_OBJECT_BUFFER_BATCH_SIZE}u>;
#else
@group(1) @binding(0) var<storage> mesh: array<Mesh2d>;
#endif // PER_OBJECT_BUFFER_BATCH_SIZE

View File

@@ -0,0 +1,49 @@
#define_import_path bevy_sprite::mesh2d_functions
#import bevy_sprite::{
mesh2d_view_bindings::view,
mesh2d_bindings::mesh,
}
#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack}
fn get_world_from_local(instance_index: u32) -> mat4x4<f32> {
return affine3_to_square(mesh[instance_index].world_from_local);
}
fn mesh2d_position_local_to_world(world_from_local: mat4x4<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
return world_from_local * vertex_position;
}
fn mesh2d_position_world_to_clip(world_position: vec4<f32>) -> vec4<f32> {
return view.clip_from_world * world_position;
}
// NOTE: The intermediate world_position assignment is important
// for precision purposes when using the 'equals' depth comparison
// function.
fn mesh2d_position_local_to_clip(world_from_local: mat4x4<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
let world_position = mesh2d_position_local_to_world(world_from_local, vertex_position);
return mesh2d_position_world_to_clip(world_position);
}
fn mesh2d_normal_local_to_world(vertex_normal: vec3<f32>, instance_index: u32) -> vec3<f32> {
return mat2x4_f32_to_mat3x3_unpack(
mesh[instance_index].local_from_world_transpose_a,
mesh[instance_index].local_from_world_transpose_b,
) * vertex_normal;
}
fn mesh2d_tangent_local_to_world(world_from_local: mat4x4<f32>, vertex_tangent: vec4<f32>) -> vec4<f32> {
return vec4<f32>(
mat3x3<f32>(
world_from_local[0].xyz,
world_from_local[1].xyz,
world_from_local[2].xyz
) * vertex_tangent.xyz,
vertex_tangent.w
);
}
fn get_tag(instance_index: u32) -> u32 {
return mesh[instance_index].tag;
}

View File

@@ -0,0 +1,17 @@
#define_import_path bevy_sprite::mesh2d_types
struct Mesh2d {
// Affine 4x3 matrix transposed to 3x4
// Use bevy_render::maths::affine3_to_square to unpack
world_from_local: mat3x4<f32>,
// 3x3 matrix packed in mat2x4 and f32 as:
// [0].xyz, [1].x,
// [1].yz, [2].xy
// [2].z
// Use bevy_render::maths::mat2x4_f32_to_mat3x3_unpack to unpack
local_from_world_transpose_a: mat2x4<f32>,
local_from_world_transpose_b: f32,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
tag: u32,
};

View File

@@ -0,0 +1,16 @@
#define_import_path bevy_sprite::mesh2d_vertex_output
struct VertexOutput {
// this is `clip position` when the struct is used as a vertex stage output
// and `frag coord` when used as a fragment stage input
@builtin(position) position: vec4<f32>,
@location(0) world_position: vec4<f32>,
@location(1) world_normal: vec3<f32>,
@location(2) uv: vec2<f32>,
#ifdef VERTEX_TANGENTS
@location(3) world_tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(4) color: vec4<f32>,
#endif
}

View File

@@ -0,0 +1,11 @@
#define_import_path bevy_sprite::mesh2d_view_bindings
#import bevy_render::view::View
#import bevy_render::globals::Globals
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<uniform> globals: Globals;
@group(0) @binding(2) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(3) var dt_lut_sampler: sampler;

View File

@@ -0,0 +1,4 @@
#define_import_path bevy_sprite::mesh2d_view_types
#import bevy_render::view
#import bevy_render::globals

9
vendor/bevy_sprite/src/mesh2d/mod.rs vendored Normal file
View File

@@ -0,0 +1,9 @@
mod color_material;
mod material;
mod mesh;
mod wireframe2d;
pub use color_material::*;
pub use material::*;
pub use mesh::*;
pub use wireframe2d::*;

View File

@@ -0,0 +1,879 @@
use crate::{
DrawMesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, SetMesh2dBindGroup,
SetMesh2dViewBindGroup, ViewKeyCache, ViewSpecializationTicks,
};
use bevy_app::{App, Plugin, PostUpdate, Startup, Update};
use bevy_asset::{
load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp,
AssetEvents, AssetId, Assets, Handle, UntypedAssetId,
};
use bevy_color::{Color, ColorToComponents};
use bevy_core_pipeline::core_2d::{
graph::{Core2d, Node2d},
Camera2d,
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Tick,
prelude::*,
query::QueryItem,
system::{lifetimeless::SRes, SystemChangeTick, SystemParamItem},
};
use bevy_platform::{
collections::{HashMap, HashSet},
hash::FixedHasher,
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
batching::gpu_preprocessing::GpuPreprocessingMode,
camera::ExtractedCamera,
extract_resource::ExtractResource,
mesh::{
allocator::{MeshAllocator, SlabId},
Mesh2d, MeshVertexBufferLayoutRef, RenderMesh,
},
prelude::*,
render_asset::{
prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets,
},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_phase::{
AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType,
CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, InputUniformIndex, PhaseItem,
PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult,
SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases,
},
render_resource::*,
renderer::RenderContext,
sync_world::{MainEntity, MainEntityHashMap},
view::{
ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewDepthTexture, ViewTarget,
},
Extract, Render, RenderApp, RenderDebugFlags, RenderSet,
};
use core::{hash::Hash, ops::Range};
use tracing::error;
pub const WIREFRAME_2D_SHADER_HANDLE: Handle<Shader> =
weak_handle!("2d8a3853-2927-4de2-9dc7-3971e7e40970");
/// A [`Plugin`] that draws wireframes for 2D meshes.
///
/// Wireframes currently do not work when using webgl or webgpu.
/// Supported rendering backends:
/// - DX12
/// - Vulkan
/// - Metal
///
/// This is a native only feature.
#[derive(Debug, Default)]
pub struct Wireframe2dPlugin {
/// Debugging flags that can optionally be set when constructing the renderer.
pub debug_flags: RenderDebugFlags,
}
impl Wireframe2dPlugin {
/// Creates a new [`Wireframe2dPlugin`] with the given debug flags.
pub fn new(debug_flags: RenderDebugFlags) -> Self {
Self { debug_flags }
}
}
impl Plugin for Wireframe2dPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
WIREFRAME_2D_SHADER_HANDLE,
"wireframe2d.wgsl",
Shader::from_wgsl
);
app.add_plugins((
BinnedRenderPhasePlugin::<Wireframe2dPhaseItem, Mesh2dPipeline>::new(self.debug_flags),
RenderAssetPlugin::<RenderWireframeMaterial>::default(),
))
.init_asset::<Wireframe2dMaterial>()
.init_resource::<SpecializedMeshPipelines<Wireframe2dPipeline>>()
.register_type::<NoWireframe2d>()
.register_type::<Wireframe2dConfig>()
.register_type::<Wireframe2dColor>()
.init_resource::<Wireframe2dConfig>()
.init_resource::<WireframeEntitiesNeedingSpecialization>()
.add_systems(Startup, setup_global_wireframe_material)
.add_systems(
Update,
(
global_color_changed.run_if(resource_changed::<Wireframe2dConfig>),
wireframe_color_changed,
// Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global
// wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed.
(apply_wireframe_material, apply_global_wireframe_material).chain(),
),
)
.add_systems(
PostUpdate,
check_wireframe_entities_needing_specialization
.after(AssetEvents)
.run_if(resource_exists::<Wireframe2dConfig>),
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<WireframeEntitySpecializationTicks>()
.init_resource::<SpecializedWireframePipelineCache>()
.init_resource::<DrawFunctions<Wireframe2dPhaseItem>>()
.add_render_command::<Wireframe2dPhaseItem, DrawWireframe2d>()
.init_resource::<RenderWireframeInstances>()
.init_resource::<SpecializedMeshPipelines<Wireframe2dPipeline>>()
.add_render_graph_node::<ViewNodeRunner<Wireframe2dNode>>(Core2d, Node2d::Wireframe)
.add_render_graph_edges(
Core2d,
(
Node2d::EndMainPass,
Node2d::Wireframe,
Node2d::PostProcessing,
),
)
.add_systems(
ExtractSchedule,
(
extract_wireframe_2d_camera,
extract_wireframe_entities_needing_specialization,
extract_wireframe_materials,
),
)
.add_systems(
Render,
(
specialize_wireframes
.in_set(RenderSet::PrepareMeshes)
.after(prepare_assets::<RenderWireframeMaterial>)
.after(prepare_assets::<RenderMesh>),
queue_wireframes
.in_set(RenderSet::QueueMeshes)
.after(prepare_assets::<RenderWireframeMaterial>),
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<Wireframe2dPipeline>();
}
}
/// Enables wireframe rendering for any entity it is attached to.
/// It will ignore the [`Wireframe2dConfig`] global setting.
///
/// This requires the [`Wireframe2dPlugin`] to be enabled.
#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq)]
pub struct Wireframe2d;
pub struct Wireframe2dPhaseItem {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: Wireframe2dBatchSetKey,
/// The key, which determines which can be batched.
pub bin_key: Wireframe2dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: (Entity, MainEntity),
/// The ranges of instances.
pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
/// indirect parameters list.
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for Wireframe2dPhaseItem {
fn entity(&self) -> Entity {
self.representative_entity.0
}
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
fn draw_function(&self) -> DrawFunctionId {
self.batch_set_key.draw_function
}
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl CachedRenderPipelinePhaseItem for Wireframe2dPhaseItem {
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
impl BinnedPhaseItem for Wireframe2dPhaseItem {
type BinKey = Wireframe2dBinKey;
type BatchSetKey = Wireframe2dBatchSetKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Wireframe2dBatchSetKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,
/// The wireframe material asset ID.
pub asset_id: UntypedAssetId,
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The ID of the slab of GPU memory that contains vertex data.
///
/// For non-mesh items, you can fill this with 0 if your items can be
/// multi-drawn, or with a unique value if they can't.
pub vertex_slab: SlabId,
/// The ID of the slab of GPU memory that contains index data, if present.
///
/// For non-mesh items, you can safely fill this with `None`.
pub index_slab: Option<SlabId>,
}
impl PhaseItemBatchSetKey for Wireframe2dBatchSetKey {
fn indexed(&self) -> bool {
self.index_slab.is_some()
}
}
/// Data that must be identical in order to *batch* phase items together.
///
/// Note that a *batch set* (if multi-draw is in use) contains multiple batches.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Wireframe2dBinKey {
/// The wireframe mesh asset ID.
pub asset_id: UntypedAssetId,
}
pub struct SetWireframe2dPushConstants;
impl<P: PhaseItem> RenderCommand<P> for SetWireframe2dPushConstants {
type Param = (
SRes<RenderWireframeInstances>,
SRes<RenderAssets<RenderWireframeMaterial>>,
);
type ViewQuery = ();
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
_item_query: Option<()>,
(wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else {
return RenderCommandResult::Failure("No wireframe material found for entity");
};
let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else {
return RenderCommandResult::Failure("No wireframe material found for entity");
};
pass.set_push_constants(
ShaderStages::FRAGMENT,
0,
bytemuck::bytes_of(&wireframe_material.color),
);
RenderCommandResult::Success
}
}
pub type DrawWireframe2d = (
SetItemPipeline,
SetMesh2dViewBindGroup<0>,
SetMesh2dBindGroup<1>,
SetWireframe2dPushConstants,
DrawMesh2d,
);
#[derive(Resource, Clone)]
pub struct Wireframe2dPipeline {
mesh_pipeline: Mesh2dPipeline,
shader: Handle<Shader>,
}
impl FromWorld for Wireframe2dPipeline {
fn from_world(render_world: &mut World) -> Self {
Wireframe2dPipeline {
mesh_pipeline: render_world.resource::<Mesh2dPipeline>().clone(),
shader: WIREFRAME_2D_SHADER_HANDLE,
}
}
}
impl SpecializedMeshPipeline for Wireframe2dPipeline {
type Key = Mesh2dPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayoutRef,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut descriptor = self.mesh_pipeline.specialize(key, layout)?;
descriptor.label = Some("wireframe_2d_pipeline".into());
descriptor.push_constant_ranges.push(PushConstantRange {
stages: ShaderStages::FRAGMENT,
range: 0..16,
});
let fragment = descriptor.fragment.as_mut().unwrap();
fragment.shader = self.shader.clone();
descriptor.primitive.polygon_mode = PolygonMode::Line;
descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0;
Ok(descriptor)
}
}
#[derive(Default)]
struct Wireframe2dNode;
impl ViewNode for Wireframe2dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, view, target, depth): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Some(wireframe_phase) =
world.get_resource::<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>()
else {
return Ok(());
};
let Some(wireframe_phase) = wireframe_phase.get(&view.retained_view_entity) else {
return Ok(());
};
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("wireframe_2d_pass"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if let Err(err) = wireframe_phase.render(&mut render_pass, world, graph.view_entity()) {
error!("Error encountered while rendering the stencil phase {err:?}");
return Err(NodeRunError::DrawError(err));
}
Ok(())
}
}
/// Sets the color of the [`Wireframe2d`] of the entity it is attached to.
///
/// If this component is present but there's no [`Wireframe2d`] component,
/// it will still affect the color of the wireframe when [`Wireframe2dConfig::global`] is set to true.
///
/// This overrides the [`Wireframe2dConfig::default_color`].
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct Wireframe2dColor {
pub color: Color,
}
#[derive(Component, Debug, Clone, Default)]
pub struct ExtractedWireframeColor {
pub color: [f32; 4],
}
/// Disables wireframe rendering for any entity it is attached to.
/// It will ignore the [`Wireframe2dConfig`] global setting.
///
/// This requires the [`Wireframe2dPlugin`] to be enabled.
#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq)]
pub struct NoWireframe2d;
#[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)]
#[reflect(Resource, Debug, Default)]
pub struct Wireframe2dConfig {
/// Whether to show wireframes for all meshes.
/// Can be overridden for individual meshes by adding a [`Wireframe2d`] or [`NoWireframe2d`] component.
pub global: bool,
/// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe2d`] component attached to it will have
/// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe2d`],
/// but no [`Wireframe2dColor`].
pub default_color: Color,
}
#[derive(Asset, Reflect, Clone, Debug, Default)]
#[reflect(Clone, Default)]
pub struct Wireframe2dMaterial {
pub color: Color,
}
pub struct RenderWireframeMaterial {
pub color: [f32; 4],
}
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)]
#[reflect(Component, Default, Clone, PartialEq)]
pub struct Mesh2dWireframe(pub Handle<Wireframe2dMaterial>);
impl AsAssetId for Mesh2dWireframe {
type Asset = Wireframe2dMaterial;
fn as_asset_id(&self) -> AssetId<Self::Asset> {
self.0.id()
}
}
impl RenderAsset for RenderWireframeMaterial {
type SourceAsset = Wireframe2dMaterial;
type Param = ();
fn prepare_asset(
source_asset: Self::SourceAsset,
_asset_id: AssetId<Self::SourceAsset>,
_param: &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
Ok(RenderWireframeMaterial {
color: source_asset.color.to_linear().to_f32_array(),
})
}
}
#[derive(Resource, Deref, DerefMut, Default)]
pub struct RenderWireframeInstances(MainEntityHashMap<AssetId<Wireframe2dMaterial>>);
#[derive(Clone, Resource, Deref, DerefMut, Debug, Default)]
pub struct WireframeEntitiesNeedingSpecialization {
#[deref]
pub entities: Vec<Entity>,
}
#[derive(Resource, Deref, DerefMut, Clone, Debug, Default)]
pub struct WireframeEntitySpecializationTicks {
pub entities: MainEntityHashMap<Tick>,
}
/// Stores the [`SpecializedWireframeViewPipelineCache`] for each view.
#[derive(Resource, Deref, DerefMut, Default)]
pub struct SpecializedWireframePipelineCache {
// view entity -> view pipeline cache
#[deref]
map: HashMap<RetainedViewEntity, SpecializedWireframeViewPipelineCache>,
}
/// Stores the cached render pipeline ID for each entity in a single view, as
/// well as the last time it was changed.
#[derive(Deref, DerefMut, Default)]
pub struct SpecializedWireframeViewPipelineCache {
// material entity -> (tick, pipeline_id)
#[deref]
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
}
#[derive(Resource)]
struct GlobalWireframeMaterial {
// This handle will be reused when the global config is enabled
handle: Handle<Wireframe2dMaterial>,
}
pub fn extract_wireframe_materials(
mut material_instances: ResMut<RenderWireframeInstances>,
changed_meshes_query: Extract<
Query<
(Entity, &ViewVisibility, &Mesh2dWireframe),
Or<(Changed<ViewVisibility>, Changed<Mesh2dWireframe>)>,
>,
>,
mut removed_visibilities_query: Extract<RemovedComponents<ViewVisibility>>,
mut removed_materials_query: Extract<RemovedComponents<Mesh2dWireframe>>,
) {
for (entity, view_visibility, material) in &changed_meshes_query {
if view_visibility.get() {
material_instances.insert(entity.into(), material.id());
} else {
material_instances.remove(&MainEntity::from(entity));
}
}
for entity in removed_visibilities_query
.read()
.chain(removed_materials_query.read())
{
// Only queue a mesh for removal if we didn't pick it up above.
// It's possible that a necessary component was removed and re-added in
// the same frame.
if !changed_meshes_query.contains(entity) {
material_instances.remove(&MainEntity::from(entity));
}
}
}
fn setup_global_wireframe_material(
mut commands: Commands,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
config: Res<Wireframe2dConfig>,
) {
// Create the handle used for the global material
commands.insert_resource(GlobalWireframeMaterial {
handle: materials.add(Wireframe2dMaterial {
color: config.default_color,
}),
});
}
/// Updates the wireframe material of all entities without a [`Wireframe2dColor`] or without a [`Wireframe2d`] component
fn global_color_changed(
config: Res<Wireframe2dConfig>,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
global_material: Res<GlobalWireframeMaterial>,
) {
if let Some(global_material) = materials.get_mut(&global_material.handle) {
global_material.color = config.default_color;
}
}
/// Updates the wireframe material when the color in [`Wireframe2dColor`] changes
fn wireframe_color_changed(
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
mut colors_changed: Query<
(&mut Mesh2dWireframe, &Wireframe2dColor),
(With<Wireframe2d>, Changed<Wireframe2dColor>),
>,
) {
for (mut handle, wireframe_color) in &mut colors_changed {
handle.0 = materials.add(Wireframe2dMaterial {
color: wireframe_color.color,
});
}
}
/// Applies or remove the wireframe material to any mesh with a [`Wireframe2d`] component, and removes it
/// for any mesh with a [`NoWireframe2d`] component.
fn apply_wireframe_material(
mut commands: Commands,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
wireframes: Query<
(Entity, Option<&Wireframe2dColor>),
(With<Wireframe2d>, Without<Mesh2dWireframe>),
>,
no_wireframes: Query<Entity, (With<NoWireframe2d>, With<Mesh2dWireframe>)>,
mut removed_wireframes: RemovedComponents<Wireframe2d>,
global_material: Res<GlobalWireframeMaterial>,
) {
for e in removed_wireframes.read().chain(no_wireframes.iter()) {
if let Ok(mut commands) = commands.get_entity(e) {
commands.remove::<Mesh2dWireframe>();
}
}
let mut material_to_spawn = vec![];
for (e, maybe_color) in &wireframes {
let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
material_to_spawn.push((e, Mesh2dWireframe(material)));
}
commands.try_insert_batch(material_to_spawn);
}
type WireframeFilter = (With<Mesh2d>, Without<Wireframe2d>, Without<NoWireframe2d>);
/// Applies or removes a wireframe material on any mesh without a [`Wireframe2d`] or [`NoWireframe2d`] component.
fn apply_global_wireframe_material(
mut commands: Commands,
config: Res<Wireframe2dConfig>,
meshes_without_material: Query<
(Entity, Option<&Wireframe2dColor>),
(WireframeFilter, Without<Mesh2dWireframe>),
>,
meshes_with_global_material: Query<Entity, (WireframeFilter, With<Mesh2dWireframe>)>,
global_material: Res<GlobalWireframeMaterial>,
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
) {
if config.global {
let mut material_to_spawn = vec![];
for (e, maybe_color) in &meshes_without_material {
let material = get_wireframe_material(maybe_color, &mut materials, &global_material);
// We only add the material handle but not the Wireframe component
// This makes it easy to detect which mesh is using the global material and which ones are user specified
material_to_spawn.push((e, Mesh2dWireframe(material)));
}
commands.try_insert_batch(material_to_spawn);
} else {
for e in &meshes_with_global_material {
commands.entity(e).remove::<Mesh2dWireframe>();
}
}
}
/// Gets a handle to a wireframe material with a fallback on the default material
fn get_wireframe_material(
maybe_color: Option<&Wireframe2dColor>,
wireframe_materials: &mut Assets<Wireframe2dMaterial>,
global_material: &GlobalWireframeMaterial,
) -> Handle<Wireframe2dMaterial> {
if let Some(wireframe_color) = maybe_color {
wireframe_materials.add(Wireframe2dMaterial {
color: wireframe_color.color,
})
} else {
// If there's no color specified we can use the global material since it's already set to use the default_color
global_material.handle.clone()
}
}
fn extract_wireframe_2d_camera(
mut wireframe_2d_phases: ResMut<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
cameras: Extract<Query<(Entity, &Camera), With<Camera2d>>>,
mut live_entities: Local<HashSet<RetainedViewEntity>>,
) {
live_entities.clear();
for (main_entity, camera) in &cameras {
if !camera.is_active {
continue;
}
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
wireframe_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
live_entities.insert(retained_view_entity);
}
// Clear out all dead views.
wireframe_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
}
pub fn extract_wireframe_entities_needing_specialization(
entities_needing_specialization: Extract<Res<WireframeEntitiesNeedingSpecialization>>,
mut entity_specialization_ticks: ResMut<WireframeEntitySpecializationTicks>,
views: Query<&ExtractedView>,
mut specialized_wireframe_pipeline_cache: ResMut<SpecializedWireframePipelineCache>,
mut removed_meshes_query: Extract<RemovedComponents<Mesh2d>>,
ticks: SystemChangeTick,
) {
for entity in entities_needing_specialization.iter() {
// Update the entity's specialization tick with this run's tick
entity_specialization_ticks.insert((*entity).into(), ticks.this_run());
}
for entity in removed_meshes_query.read() {
for view in &views {
if let Some(specialized_wireframe_pipeline_cache) =
specialized_wireframe_pipeline_cache.get_mut(&view.retained_view_entity)
{
specialized_wireframe_pipeline_cache.remove(&MainEntity::from(entity));
}
}
}
}
pub fn check_wireframe_entities_needing_specialization(
needs_specialization: Query<
Entity,
Or<(
Changed<Mesh2d>,
AssetChanged<Mesh2d>,
Changed<Mesh2dWireframe>,
AssetChanged<Mesh2dWireframe>,
)>,
>,
mut entities_needing_specialization: ResMut<WireframeEntitiesNeedingSpecialization>,
) {
entities_needing_specialization.clear();
for entity in &needs_specialization {
entities_needing_specialization.push(entity);
}
}
pub fn specialize_wireframes(
render_meshes: Res<RenderAssets<RenderMesh>>,
render_mesh_instances: Res<RenderMesh2dInstances>,
render_wireframe_instances: Res<RenderWireframeInstances>,
wireframe_phases: Res<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
view_key_cache: Res<ViewKeyCache>,
entity_specialization_ticks: Res<WireframeEntitySpecializationTicks>,
view_specialization_ticks: Res<ViewSpecializationTicks>,
mut specialized_material_pipeline_cache: ResMut<SpecializedWireframePipelineCache>,
mut pipelines: ResMut<SpecializedMeshPipelines<Wireframe2dPipeline>>,
pipeline: Res<Wireframe2dPipeline>,
pipeline_cache: Res<PipelineCache>,
ticks: SystemChangeTick,
) {
// Record the retained IDs of all views so that we can expire old
// pipeline IDs.
let mut all_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
for (view, visible_entities) in &views {
all_views.insert(view.retained_view_entity);
if !wireframe_phases.contains_key(&view.retained_view_entity) {
continue;
}
let Some(view_key) = view_key_cache.get(&view.retained_view_entity.main_entity) else {
continue;
};
let view_tick = view_specialization_ticks
.get(&view.retained_view_entity.main_entity)
.unwrap();
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
.entry(view.retained_view_entity)
.or_default();
for (_, visible_entity) in visible_entities.iter::<Mesh2d>() {
if !render_wireframe_instances.contains_key(visible_entity) {
continue;
};
let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else {
continue;
};
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
let last_specialized_tick = view_specialized_material_pipeline_cache
.get(visible_entity)
.map(|(tick, _)| *tick);
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
view_tick.is_newer_than(tick, ticks.this_run())
|| entity_tick.is_newer_than(tick, ticks.this_run())
});
if !needs_specialization {
continue;
}
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
continue;
};
let mut mesh_key = *view_key;
mesh_key |= Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology());
let pipeline_id =
pipelines.specialize(&pipeline_cache, &pipeline, mesh_key, &mesh.layout);
let pipeline_id = match pipeline_id {
Ok(id) => id,
Err(err) => {
error!("{}", err);
continue;
}
};
view_specialized_material_pipeline_cache
.insert(*visible_entity, (ticks.this_run(), pipeline_id));
}
}
// Delete specialized pipelines belonging to views that have expired.
specialized_material_pipeline_cache
.retain(|retained_view_entity, _| all_views.contains(retained_view_entity));
}
fn queue_wireframes(
custom_draw_functions: Res<DrawFunctions<Wireframe2dPhaseItem>>,
render_mesh_instances: Res<RenderMesh2dInstances>,
mesh_allocator: Res<MeshAllocator>,
specialized_wireframe_pipeline_cache: Res<SpecializedWireframePipelineCache>,
render_wireframe_instances: Res<RenderWireframeInstances>,
mut wireframe_2d_phases: ResMut<ViewBinnedRenderPhases<Wireframe2dPhaseItem>>,
mut views: Query<(&ExtractedView, &RenderVisibleEntities)>,
) {
for (view, visible_entities) in &mut views {
let Some(wireframe_phase) = wireframe_2d_phases.get_mut(&view.retained_view_entity) else {
continue;
};
let draw_wireframe = custom_draw_functions.read().id::<DrawWireframe2d>();
let Some(view_specialized_material_pipeline_cache) =
specialized_wireframe_pipeline_cache.get(&view.retained_view_entity)
else {
continue;
};
for (render_entity, visible_entity) in visible_entities.iter::<Mesh2d>() {
let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else {
continue;
};
let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache
.get(visible_entity)
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
else {
continue;
};
// Skip the entity if it's cached in a bin and up to date.
if wireframe_phase.validate_cached_entity(*visible_entity, current_change_tick) {
continue;
}
let Some(mesh_instance) = render_mesh_instances.get(visible_entity) else {
continue;
};
let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
let bin_key = Wireframe2dBinKey {
asset_id: mesh_instance.mesh_asset_id.untyped(),
};
let batch_set_key = Wireframe2dBatchSetKey {
pipeline: pipeline_id,
asset_id: wireframe_instance.untyped(),
draw_function: draw_wireframe,
vertex_slab: vertex_slab.unwrap_or_default(),
index_slab,
};
wireframe_phase.add(
batch_set_key,
bin_key,
(*render_entity, *visible_entity),
InputUniformIndex::default(),
if mesh_instance.automatic_batching {
BinnedRenderPhaseType::BatchableMesh
} else {
BinnedRenderPhaseType::UnbatchableMesh
},
current_change_tick,
);
}
}
}

View File

@@ -0,0 +1,12 @@
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
struct PushConstants {
color: vec4<f32>
}
var<push_constant> push_constants: PushConstants;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
return push_constants.color;
}

View File

@@ -0,0 +1,251 @@
//! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for
//! sprites with arbitrary transforms. Picking is done based on sprite bounds, not visible pixels.
//! This means a partially transparent sprite is pickable even in its transparent areas.
//!
//! ## Implementation Notes
//!
//! - The `position` reported in `HitData` in in world space, and the `normal` is a normalized
//! vector provided by the target's `GlobalTransform::back()`.
use crate::Sprite;
use bevy_app::prelude::*;
use bevy_asset::prelude::*;
use bevy_color::Alpha;
use bevy_ecs::prelude::*;
use bevy_image::prelude::*;
use bevy_math::{prelude::*, FloatExt};
use bevy_picking::backend::prelude::*;
use bevy_reflect::prelude::*;
use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
/// An optional component that marks cameras that should be used in the [`SpritePickingPlugin`].
///
/// Only needed if [`SpritePickingSettings::require_markers`] is set to `true`, and ignored
/// otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component, Clone)]
pub struct SpritePickingCamera;
/// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels
#[derive(Debug, Clone, Copy, Reflect)]
#[reflect(Debug, Clone)]
pub enum SpritePickingMode {
/// Even if a sprite is picked on a transparent pixel, it should still count within the backend.
/// Only consider the rect of a given sprite.
BoundingBox,
/// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive)
/// Threshold is given as an f32 representing the alpha value in a Bevy Color Value
AlphaThreshold(f32),
}
/// Runtime settings for the [`SpritePickingPlugin`].
#[derive(Resource, Reflect)]
#[reflect(Resource, Default)]
pub struct SpritePickingSettings {
/// When set to `true` sprite picking will only consider cameras marked with
/// [`SpritePickingCamera`].
///
/// This setting is provided to give you fine-grained control over which cameras and entities
/// should be used by the sprite picking backend at runtime.
pub require_markers: bool,
/// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone.
///
/// Defaults to an inclusive alpha threshold of 0.1
pub picking_mode: SpritePickingMode,
}
impl Default for SpritePickingSettings {
fn default() -> Self {
Self {
require_markers: false,
picking_mode: SpritePickingMode::AlphaThreshold(0.1),
}
}
}
/// Enables the sprite picking backend, allowing you to click on, hover over and drag sprites.
#[derive(Clone)]
pub struct SpritePickingPlugin;
impl Plugin for SpritePickingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SpritePickingSettings>()
.register_type::<SpritePickingCamera>()
.register_type::<SpritePickingMode>()
.register_type::<SpritePickingSettings>()
.add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend));
}
}
fn sprite_picking(
pointers: Query<(&PointerId, &PointerLocation)>,
cameras: Query<(
Entity,
&Camera,
&GlobalTransform,
&Projection,
Has<SpritePickingCamera>,
)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
images: Res<Assets<Image>>,
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
settings: Res<SpritePickingSettings>,
sprite_query: Query<(
Entity,
&Sprite,
&GlobalTransform,
&Pickable,
&ViewVisibility,
)>,
mut output: EventWriter<PointerHits>,
) {
let mut sorted_sprites: Vec<_> = sprite_query
.iter()
.filter_map(|(entity, sprite, transform, pickable, vis)| {
if !transform.affine().is_nan() && vis.get() {
Some((entity, sprite, transform, pickable))
} else {
None
}
})
.collect();
// radsort is a stable radix sort that performed better than `slice::sort_by_key`
radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| {
-transform.translation().z
});
let primary_window = primary_window.single().ok();
for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
pointer_location.location().map(|loc| (pointer, loc))
}) {
let mut blocked = false;
let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho), _)) =
cameras
.iter()
.filter(|(_, camera, _, _, cam_can_pick)| {
let marker_requirement = !settings.require_markers || *cam_can_pick;
camera.is_active && marker_requirement
})
.find(|(_, camera, _, _, _)| {
camera
.target
.normalize(primary_window)
.is_some_and(|x| x == location.target)
})
else {
continue;
};
let viewport_pos = camera
.logical_viewport_rect()
.map(|v| v.min)
.unwrap_or_default();
let pos_in_viewport = location.position - viewport_pos;
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else {
continue;
};
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
let picks: Vec<(Entity, HitData)> = sorted_sprites
.iter()
.copied()
.filter_map(|(entity, sprite, sprite_transform, pickable)| {
if blocked {
return None;
}
// Transform cursor line segment to sprite coordinate system
let world_to_sprite = sprite_transform.affine().inverse();
let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
// plane in sprite-local space). It may not intersect if, for example, we're
// viewing the sprite side-on
if cursor_start_sprite.z == cursor_end_sprite.z {
// Cursor ray is parallel to the sprite and misses it
return None;
}
let lerp_factor =
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
if !(0.0..=1.0).contains(&lerp_factor) {
// Lerp factor is out of range, meaning that while an infinite line cast by
// the cursor would intersect the sprite, the sprite is not between the
// camera's near and far planes
return None;
}
// Otherwise we can interpolate the xy of the start and end positions by the
// lerp factor to get the cursor position in sprite space!
let cursor_pos_sprite = cursor_start_sprite
.lerp(cursor_end_sprite, lerp_factor)
.xy();
let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(
cursor_pos_sprite,
&images,
&texture_atlas_layout,
) else {
return None;
};
// Since the pixel space coordinate is `Ok`, we know the cursor is in the bounds of
// the sprite.
let cursor_in_valid_pixels_of_sprite = 'valid_pixel: {
match settings.picking_mode {
SpritePickingMode::AlphaThreshold(cutoff) => {
let Some(image) = images.get(&sprite.image) else {
// [`Sprite::from_color`] returns a defaulted handle.
// This handle doesn't return a valid image, so returning false here would make picking "color sprites" impossible
break 'valid_pixel true;
};
// grab pixel and check alpha
let Ok(color) = image.get_color_at(
cursor_pixel_space.x as u32,
cursor_pixel_space.y as u32,
) else {
// We don't know how to interpret the pixel.
break 'valid_pixel false;
};
// Check the alpha is above the cutoff.
color.alpha() > cutoff
}
SpritePickingMode::BoundingBox => true,
}
};
blocked = cursor_in_valid_pixels_of_sprite && pickable.should_block_lower;
cursor_in_valid_pixels_of_sprite.then(|| {
let hit_pos_world =
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
// Transform point from world to camera space to get the Z distance
let hit_pos_cam = cam_transform
.affine()
.inverse()
.transform_point3(hit_pos_world);
// HitData requires a depth as calculated from the camera's near clipping plane
let depth = -cam_ortho.near - hit_pos_cam.z;
(
entity,
HitData::new(
cam_entity,
depth,
Some(hit_pos_world),
Some(*sprite_transform.back()),
),
)
})
})
.collect();
let order = camera.order as f32;
output.write(PointerHits::new(*pointer, picks, order));
}
}

1068
vendor/bevy_sprite/src/render/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
#ifdef TONEMAP_IN_SHADER
#import bevy_core_pipeline::tonemapping
#endif
#import bevy_render::{
maths::affine3_to_square,
view::View,
}
#import bevy_sprite::sprite_view_bindings::view
struct VertexInput {
@builtin(vertex_index) index: u32,
// NOTE: Instance-rate vertex buffer members prefixed with i_
// NOTE: i_model_transpose_colN are the 3 columns of a 3x4 matrix that is the transpose of the
// affine 4x3 model matrix.
@location(0) i_model_transpose_col0: vec4<f32>,
@location(1) i_model_transpose_col1: vec4<f32>,
@location(2) i_model_transpose_col2: vec4<f32>,
@location(3) i_color: vec4<f32>,
@location(4) i_uv_offset_scale: vec4<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) @interpolate(flat) color: vec4<f32>,
};
@vertex
fn vertex(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
let vertex_position = vec3<f32>(
f32(in.index & 0x1u),
f32((in.index & 0x2u) >> 1u),
0.0
);
out.clip_position = view.clip_from_world * affine3_to_square(mat3x4<f32>(
in.i_model_transpose_col0,
in.i_model_transpose_col1,
in.i_model_transpose_col2,
)) * vec4<f32>(vertex_position, 1.0);
out.uv = vec2<f32>(vertex_position.xy) * in.i_uv_offset_scale.zw + in.i_uv_offset_scale.xy;
out.color = in.i_color;
return out;
}
@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
@group(1) @binding(1) var sprite_sampler: sampler;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
var color = in.color * textureSample(sprite_texture, sprite_sampler, in.uv);
#ifdef TONEMAP_IN_SHADER
color = tonemapping::tone_mapping(color, view.color_grading);
#endif
return color;
}

View File

@@ -0,0 +1,9 @@
#define_import_path bevy_sprite::sprite_view_bindings
#import bevy_render::view::View
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(2) var dt_lut_sampler: sampler;

542
vendor/bevy_sprite/src/sprite.rs vendored Normal file
View File

@@ -0,0 +1,542 @@
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
sync_world::SyncToRenderWorld,
view::{self, Visibility, VisibilityClass},
};
use bevy_transform::components::Transform;
use crate::TextureSlicer;
/// Describes a sprite to be rendered to a 2D camera
#[derive(Component, Debug, Default, Clone, Reflect)]
#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass)]
#[reflect(Component, Default, Debug, Clone)]
#[component(on_add = view::add_visibility_class::<Sprite>)]
pub struct Sprite {
/// The image used to render the sprite
pub image: Handle<Image>,
/// The (optional) texture atlas used to render the sprite
pub texture_atlas: Option<TextureAtlas>,
/// The sprite's color tint
pub color: Color,
/// Flip the sprite along the `X` axis
pub flip_x: bool,
/// Flip the sprite along the `Y` axis
pub flip_y: bool,
/// An optional custom size for the sprite that will be used when rendering, instead of the size
/// of the sprite's image
pub custom_size: Option<Vec2>,
/// An optional rectangle representing the region of the sprite's image to render, instead of rendering
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
///
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// [`Anchor`] point of the sprite in the world
pub anchor: Anchor,
/// How the sprite's image will be scaled.
pub image_mode: SpriteImageMode,
}
impl Sprite {
/// Create a Sprite with a custom size
pub fn sized(custom_size: Vec2) -> Self {
Sprite {
custom_size: Some(custom_size),
..Default::default()
}
}
/// Create a sprite from an image
pub fn from_image(image: Handle<Image>) -> Self {
Self {
image,
..Default::default()
}
}
/// Create a sprite from an image, with an associated texture atlas
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
Self {
image,
texture_atlas: Some(atlas),
..Default::default()
}
}
/// Create a sprite from a solid color
pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
Self {
color: color.into(),
custom_size: Some(size),
..Default::default()
}
}
/// Computes the pixel point where `point_relative_to_sprite` is sampled
/// from in this sprite. `point_relative_to_sprite` must be in the sprite's
/// local frame. Returns an Ok if the point is inside the bounds of the
/// sprite (not just the image), and returns an Err otherwise.
pub fn compute_pixel_space_point(
&self,
point_relative_to_sprite: Vec2,
images: &Assets<Image>,
texture_atlases: &Assets<TextureAtlasLayout>,
) -> Result<Vec2, Vec2> {
let image_size = images
.get(&self.image)
.map(Image::size)
.unwrap_or(UVec2::ONE);
let atlas_rect = self
.texture_atlas
.as_ref()
.and_then(|s| s.texture_rect(texture_atlases))
.map(|r| r.as_rect());
let texture_rect = match (atlas_rect, self.rect) {
(None, None) => Rect::new(0.0, 0.0, image_size.x as f32, image_size.y as f32),
(None, Some(sprite_rect)) => sprite_rect,
(Some(atlas_rect), None) => atlas_rect,
(Some(atlas_rect), Some(mut sprite_rect)) => {
// Make the sprite rect relative to the atlas rect.
sprite_rect.min += atlas_rect.min;
sprite_rect.max += atlas_rect.min;
sprite_rect
}
};
let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size());
let sprite_center = -self.anchor.as_vec() * sprite_size;
let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center;
if self.flip_x {
point_relative_to_sprite_center.x *= -1.0;
}
// Texture coordinates start at the top left, whereas world coordinates start at the bottom
// left. So flip by default, and then don't flip if `flip_y` is set.
if !self.flip_y {
point_relative_to_sprite_center.y *= -1.0;
}
let sprite_to_texture_ratio = {
let texture_size = texture_rect.size();
let div_or_zero = |a, b| if b == 0.0 { 0.0 } else { a / b };
Vec2::new(
div_or_zero(texture_size.x, sprite_size.x),
div_or_zero(texture_size.y, sprite_size.y),
)
};
let point_relative_to_texture =
point_relative_to_sprite_center * sprite_to_texture_ratio + texture_rect.center();
// TODO: Support `SpriteImageMode`.
if texture_rect.contains(point_relative_to_texture) {
Ok(point_relative_to_texture)
} else {
Err(point_relative_to_texture)
}
}
}
impl From<Handle<Image>> for Sprite {
fn from(image: Handle<Image>) -> Self {
Self::from_image(image)
}
}
/// Controls how the image is altered when scaled.
#[derive(Default, Debug, Clone, Reflect, PartialEq)]
#[reflect(Debug, Default, Clone)]
pub enum SpriteImageMode {
/// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
#[default]
Auto,
/// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`].
/// Otherwise no scaling will be applied.
Scale(ScalingMode),
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value`
Tiled {
/// Should the image repeat horizontally
tile_x: bool,
/// Should the image repeat vertically
tile_y: bool,
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above this value.
stretch_value: f32,
},
}
impl SpriteImageMode {
/// Returns true if this mode uses slices internally ([`SpriteImageMode::Sliced`] or [`SpriteImageMode::Tiled`])
#[inline]
pub fn uses_slices(&self) -> bool {
matches!(
self,
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
)
}
/// Returns [`ScalingMode`] if scale is presented or [`Option::None`] otherwise.
#[inline]
#[must_use]
pub const fn scale(&self) -> Option<ScalingMode> {
if let SpriteImageMode::Scale(scale) = self {
Some(*scale)
} else {
None
}
}
}
/// Represents various modes for proportional scaling of a texture.
///
/// Can be used in [`SpriteImageMode::Scale`].
#[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)]
#[reflect(Debug, Default, Clone)]
pub enum ScalingMode {
/// Scale the texture uniformly (maintain the texture's aspect ratio)
/// so that both dimensions (width and height) of the texture will be equal
/// to or larger than the corresponding dimension of the target rectangle.
/// Fill sprite with a centered texture.
#[default]
FillCenter,
/// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
/// One dimension of the texture will match the rectangle's size,
/// while the other dimension may exceed it.
/// The exceeding portion is aligned to the start:
/// * Horizontal overflow is left-aligned if the width exceeds the rectangle.
/// * Vertical overflow is top-aligned if the height exceeds the rectangle.
FillStart,
/// Scales the texture to fill the target rectangle while maintaining its aspect ratio.
/// One dimension of the texture will match the rectangle's size,
/// while the other dimension may exceed it.
/// The exceeding portion is aligned to the end:
/// * Horizontal overflow is right-aligned if the width exceeds the rectangle.
/// * Vertical overflow is bottom-aligned if the height exceeds the rectangle.
FillEnd,
/// Scaling the texture will maintain the original aspect ratio
/// and ensure that the original texture fits entirely inside the rect.
/// At least one axis (x or y) will fit exactly. The result is centered inside the rect.
FitCenter,
/// Scaling the texture will maintain the original aspect ratio
/// and ensure that the original texture fits entirely inside rect.
/// At least one axis (x or y) will fit exactly.
/// Aligns the result to the left and top edges of rect.
FitStart,
/// Scaling the texture will maintain the original aspect ratio
/// and ensure that the original texture fits entirely inside rect.
/// At least one axis (x or y) will fit exactly.
/// Aligns the result to the right and bottom edges of rect.
FitEnd,
}
/// How a sprite is positioned relative to its [`Transform`].
/// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[doc(alias = "pivot")]
pub enum Anchor {
#[default]
Center,
BottomLeft,
BottomCenter,
BottomRight,
CenterLeft,
CenterRight,
TopLeft,
TopCenter,
TopRight,
/// Custom anchor point. Top left is `(-0.5, 0.5)`, center is `(0.0, 0.0)`. The value will
/// be scaled with the sprite size.
Custom(Vec2),
}
impl Anchor {
pub fn as_vec(&self) -> Vec2 {
match self {
Anchor::Center => Vec2::ZERO,
Anchor::BottomLeft => Vec2::new(-0.5, -0.5),
Anchor::BottomCenter => Vec2::new(0.0, -0.5),
Anchor::BottomRight => Vec2::new(0.5, -0.5),
Anchor::CenterLeft => Vec2::new(-0.5, 0.0),
Anchor::CenterRight => Vec2::new(0.5, 0.0),
Anchor::TopLeft => Vec2::new(-0.5, 0.5),
Anchor::TopCenter => Vec2::new(0.0, 0.5),
Anchor::TopRight => Vec2::new(0.5, 0.5),
Anchor::Custom(point) => *point,
}
}
}
#[cfg(test)]
mod tests {
use bevy_asset::{Assets, RenderAssetUsages};
use bevy_color::Color;
use bevy_image::Image;
use bevy_image::{TextureAtlas, TextureAtlasLayout};
use bevy_math::{Rect, URect, UVec2, Vec2};
use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use crate::Anchor;
use super::Sprite;
/// Makes a new image of the specified size.
fn make_image(size: UVec2) -> Image {
Image::new_fill(
Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 255],
TextureFormat::Rgba8Unorm,
RenderAssetUsages::all(),
)
}
#[test]
fn compute_pixel_space_point_for_regular_sprite() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5)));
assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(3.0, 0.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-3.0, 0.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_color_sprite() {
let image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
// This also tests the `custom_size` field.
let sprite = Sprite::from_color(Color::BLACK, Vec2::new(50.0, 100.0));
let compute = |point| {
sprite
.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets)
// Round to remove floating point errors.
.map(|x| (x * 1e5).round() / 1e5)
.map_err(|x| (x * 1e5).round() / 1e5)
};
assert_eq!(compute(Vec2::new(-20.0, -40.0)), Ok(Vec2::new(0.1, 0.9)));
assert_eq!(compute(Vec2::new(0.0, 10.0)), Ok(Vec2::new(0.5, 0.4)));
assert_eq!(compute(Vec2::new(75.0, 100.0)), Err(Vec2::new(2.0, -0.5)));
assert_eq!(compute(Vec2::new(-75.0, -100.0)), Err(Vec2::new(-1.0, 1.5)));
assert_eq!(compute(Vec2::new(-30.0, -40.0)), Err(Vec2::new(-0.1, 0.9)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_bottom_left() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5)));
assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_top_right() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::TopRight,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5)));
assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_flip_x() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
flip_x: true,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5)));
assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(-0.5, 5.0)));
assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(5.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_anchor_flip_y() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
anchor: Anchor::TopRight,
flip_y: true,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5)));
assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5)));
assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0)));
assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_rect() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)),
anchor: Anchor::BottomLeft,
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0)));
// The pixel is outside the rect, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0)));
}
#[test]
fn compute_pixel_space_point_for_texture_atlas_sprite() {
let mut image_assets = Assets::<Image>::default();
let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
size: UVec2::new(5, 10),
textures: vec![URect::new(1, 1, 4, 4)],
});
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
texture_atlas: Some(TextureAtlas {
layout: texture_atlas,
index: 0,
}),
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5)));
}
#[test]
fn compute_pixel_space_point_for_texture_atlas_sprite_with_rect() {
let mut image_assets = Assets::<Image>::default();
let mut texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout {
size: UVec2::new(5, 10),
textures: vec![URect::new(1, 1, 4, 4)],
});
let sprite = Sprite {
image,
anchor: Anchor::BottomLeft,
texture_atlas: Some(TextureAtlas {
layout: texture_atlas,
index: 0,
}),
// The rect is relative to the texture atlas sprite.
rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)),
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5)));
}
#[test]
fn compute_pixel_space_point_for_sprite_with_custom_size_and_rect() {
let mut image_assets = Assets::<Image>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
let image = image_assets.add(make_image(UVec2::new(5, 10)));
let sprite = Sprite {
image,
custom_size: Some(Vec2::new(100.0, 50.0)),
rect: Some(Rect::new(0.0, 0.0, 5.0, 5.0)),
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0)));
assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(2.5, -1.0)));
}
}

View File

@@ -0,0 +1,64 @@
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// Defines the extents of the border of a rectangle.
///
/// This struct is used to represent thickness or offsets from the edges
/// of a rectangle (left, right, top, and bottom), with values increasing inwards.
#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)]
#[reflect(Clone, PartialEq, Default)]
pub struct BorderRect {
/// Extent of the border along the left edge
pub left: f32,
/// Extent of the border along the right edge
pub right: f32,
/// Extent of the border along the top edge
pub top: f32,
/// Extent of the border along the bottom edge
pub bottom: f32,
}
impl BorderRect {
/// An empty border with zero thickness along each edge
pub const ZERO: Self = Self::all(0.);
/// Creates a border with the same `extent` along each edge
#[must_use]
#[inline]
pub const fn all(extent: f32) -> Self {
Self {
left: extent,
right: extent,
top: extent,
bottom: extent,
}
}
/// Creates a new border with the `left` and `right` extents equal to `horizontal`, and `top` and `bottom` extents equal to `vertical`.
#[must_use]
#[inline]
pub const fn axes(horizontal: f32, vertical: f32) -> Self {
Self {
left: horizontal,
right: horizontal,
top: vertical,
bottom: vertical,
}
}
}
impl From<f32> for BorderRect {
fn from(extent: f32) -> Self {
Self::all(extent)
}
}
impl From<[f32; 4]> for BorderRect {
fn from([left, right, top, bottom]: [f32; 4]) -> Self {
Self {
left,
right,
top,
bottom,
}
}
}

View File

@@ -0,0 +1,156 @@
use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout};
use super::TextureSlice;
use bevy_asset::{AssetEvent, Assets};
use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_math::{Rect, Vec2};
use bevy_platform::collections::HashSet;
/// Component storing texture slices for tiled or sliced sprite entities
///
/// This component is automatically inserted and updated
#[derive(Debug, Clone, Component)]
pub struct ComputedTextureSlices(Vec<TextureSlice>);
impl ComputedTextureSlices {
/// Computes [`ExtractedSlice`] iterator from the sprite slices
///
/// # Arguments
///
/// * `sprite` - The sprite component
#[must_use]
pub(crate) fn extract_slices<'a>(
&'a self,
sprite: &'a Sprite,
) -> impl ExactSizeIterator<Item = ExtractedSlice> + 'a {
let mut flip = Vec2::ONE;
if sprite.flip_x {
flip.x *= -1.0;
}
if sprite.flip_y {
flip.y *= -1.0;
}
let anchor = sprite.anchor.as_vec()
* sprite
.custom_size
.unwrap_or(sprite.rect.unwrap_or_default().size());
self.0.iter().map(move |slice| ExtractedSlice {
offset: slice.offset * flip - anchor,
rect: slice.texture_rect,
size: slice.draw_size,
})
}
}
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if the image asset is not loaded
///
/// # Arguments
///
/// * `sprite` - The sprite component with the image handle and image mode
/// * `images` - The image assets, use to retrieve the image dimensions
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
#[must_use]
fn compute_sprite_slices(
sprite: &Sprite,
images: &Assets<Image>,
atlas_layouts: &Assets<TextureAtlasLayout>,
) -> Option<ComputedTextureSlices> {
let (image_size, texture_rect) = match &sprite.texture_atlas {
Some(a) => {
let layout = atlas_layouts.get(&a.layout)?;
(
layout.size.as_vec2(),
layout.textures.get(a.index)?.as_rect(),
)
}
None => {
let image = images.get(&sprite.image)?;
let size = Vec2::new(
image.texture_descriptor.size.width as f32,
image.texture_descriptor.size.height as f32,
);
let rect = sprite.rect.unwrap_or(Rect {
min: Vec2::ZERO,
max: size,
});
(size, rect)
}
};
let slices = match &sprite.image_mode {
SpriteImageMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
} => {
let slice = TextureSlice {
texture_rect,
draw_size: sprite.custom_size.unwrap_or(image_size),
offset: Vec2::ZERO,
};
slice.tiled(*stretch_value, (*tile_x, *tile_y))
}
SpriteImageMode::Auto => {
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
}
SpriteImageMode::Scale(_) => {
unreachable!("Slices should not be computed for SpriteImageMode::Scale")
}
};
Some(ComputedTextureSlices(slices))
}
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on sprite entities with a matching [`SpriteImageMode`]
pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
sprites: Query<(Entity, &Sprite)>,
) {
// We store the asset ids of added/modified image assets
let added_handles: HashSet<_> = events
.read()
.filter_map(|e| match e {
AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id),
_ => None,
})
.collect();
if added_handles.is_empty() {
return;
}
// We recompute the sprite slices for sprite entities with a matching asset handle id
for (entity, sprite) in &sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if !added_handles.contains(&sprite.image.id()) {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}
}
/// System reacting to changes on the [`Sprite`] component to compute the sprite slices
pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
changed_sprites: Query<(Entity, &Sprite), Changed<Sprite>>,
) {
for (entity, sprite) in &changed_sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}
}

View File

@@ -0,0 +1,93 @@
mod border_rect;
mod computed_slices;
mod slicer;
use bevy_math::{Rect, Vec2};
pub use border_rect::BorderRect;
pub use slicer::{SliceScaleMode, TextureSlicer};
pub(crate) use computed_slices::{
compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices,
};
/// Single texture slice, representing a texture rect to draw in a given area
#[derive(Debug, Clone, PartialEq)]
pub struct TextureSlice {
/// texture area to draw
pub texture_rect: Rect,
/// slice draw size
pub draw_size: Vec2,
/// offset of the slice
pub offset: Vec2,
}
impl TextureSlice {
/// Transforms the given slice in a collection of tiled subdivisions.
///
/// # Arguments
///
/// * `stretch_value` - The slice will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* (rect) are above `stretch_value`.
/// * `tile_x` - should the slice be tiled horizontally
/// * `tile_y` - should the slice be tiled vertically
#[must_use]
pub fn tiled(self, stretch_value: f32, (tile_x, tile_y): (bool, bool)) -> Vec<Self> {
if !tile_x && !tile_y {
return vec![self];
}
let stretch_value = stretch_value.max(0.001);
let rect_size = self.texture_rect.size();
// Each tile expected size
let expected_size = Vec2::new(
if tile_x {
// No slice should be less than 1 pixel wide
(rect_size.x * stretch_value).max(1.0)
} else {
self.draw_size.x
},
if tile_y {
// No slice should be less than 1 pixel high
(rect_size.y * stretch_value).max(1.0)
} else {
self.draw_size.y
},
)
.min(self.draw_size);
let mut slices = Vec::new();
let base_offset = Vec2::new(
-self.draw_size.x / 2.0,
self.draw_size.y / 2.0, // Start from top
);
let mut offset = base_offset;
let mut remaining_columns = self.draw_size.y;
while remaining_columns > 0.0 {
let size_y = expected_size.y.min(remaining_columns);
offset.x = base_offset.x;
offset.y -= size_y / 2.0;
let mut remaining_rows = self.draw_size.x;
while remaining_rows > 0.0 {
let size_x = expected_size.x.min(remaining_rows);
offset.x += size_x / 2.0;
let draw_size = Vec2::new(size_x, size_y);
let delta = draw_size / expected_size;
slices.push(Self {
texture_rect: Rect {
min: self.texture_rect.min,
max: self.texture_rect.min + self.texture_rect.size() * delta,
},
draw_size,
offset: self.offset + offset,
});
offset.x += size_x / 2.0;
remaining_rows -= size_x;
}
offset.y -= size_y / 2.0;
remaining_columns -= size_y;
}
if slices.len() > 1_000 {
tracing::warn!("One of your tiled textures has generated {} slices. You might want to use higher stretch values to avoid a great performance cost", slices.len());
}
slices
}
}

View File

@@ -0,0 +1,436 @@
use super::{BorderRect, TextureSlice};
use bevy_math::{vec2, Rect, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes
/// without needing to prepare multiple assets. The associated texture will be split into nine portions,
/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion.
///
/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other
/// sections will be scaled or tiled.
///
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
#[derive(Debug, Clone, Reflect, PartialEq)]
#[reflect(Clone, PartialEq)]
pub struct TextureSlicer {
/// Inset values in pixels that define the four slicing lines dividing the texture into nine sections.
pub border: BorderRect,
/// Defines how the center part of the 9 slices will scale
pub center_scale_mode: SliceScaleMode,
/// Defines how the 4 side parts of the 9 slices will scale
pub sides_scale_mode: SliceScaleMode,
/// Defines the maximum scale of the 4 corner slices (default to `1.0`)
pub max_corner_scale: f32,
}
/// Defines how a texture slice scales when resized
#[derive(Debug, Copy, Clone, Default, Reflect, PartialEq)]
#[reflect(Clone, PartialEq, Default)]
pub enum SliceScaleMode {
/// The slice will be stretched to fit the area
#[default]
Stretch,
/// The slice will be tiled to fit the area
Tile {
/// The slice will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above `stretch_value`.
///
/// Example: `1.0` means that a 10 pixel wide image would repeat after 10 screen pixels.
/// `2.0` means it would repeat after 20 screen pixels.
///
/// Note: The value should be inferior or equal to `1.0` to avoid quality loss.
///
/// Note: the value will be clamped to `0.001` if lower
stretch_value: f32,
},
}
impl TextureSlicer {
/// Computes the 4 corner slices: top left, top right, bottom left, bottom right.
#[must_use]
fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] {
let coef = render_size / base_rect.size();
let BorderRect {
left,
right,
top,
bottom,
} = self.border;
let min_coef = coef.x.min(coef.y).min(self.max_corner_scale);
[
// Top Left Corner
TextureSlice {
texture_rect: Rect {
min: base_rect.min,
max: base_rect.min + vec2(left, top),
},
draw_size: vec2(left, top) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Top Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.min.y),
max: vec2(base_rect.max.x, base_rect.min.y + top),
},
draw_size: vec2(right, top) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
render_size.y - top * min_coef,
) / 2.0,
},
// Bottom Left
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.min.x, base_rect.max.y - bottom),
max: vec2(base_rect.min.x + left, base_rect.max.y),
},
draw_size: vec2(left, bottom) * min_coef,
offset: vec2(
-render_size.x + left * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
// Bottom Right Corner
TextureSlice {
texture_rect: Rect {
min: vec2(base_rect.max.x - right, base_rect.max.y - bottom),
max: base_rect.max,
},
draw_size: vec2(right, bottom) * min_coef,
offset: vec2(
render_size.x - right * min_coef,
-render_size.y + bottom * min_coef,
) / 2.0,
},
]
}
/// Computes the 2 horizontal side slices (left and right borders)
#[must_use]
fn horizontal_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// Left
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(0.0, self.border.top),
max: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
},
draw_size: vec2(
tl_corner.draw_size.x,
render_size.y - (tl_corner.draw_size.y + bl_corner.draw_size.y),
),
offset: vec2(
tl_corner.draw_size.x - render_size.x,
bl_corner.draw_size.y - tl_corner.draw_size.y,
) / 2.0,
},
// Right
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.top,
),
max: base_rect.max - vec2(0.0, self.border.bottom),
},
draw_size: vec2(
tr_corner.draw_size.x,
render_size.y - (tr_corner.draw_size.y + br_corner.draw_size.y),
),
offset: vec2(
render_size.x - tr_corner.draw_size.x,
br_corner.draw_size.y - tr_corner.draw_size.y,
) / 2.0,
},
]
}
/// Computes the 2 vertical side slices (top and bottom borders)
#[must_use]
fn vertical_side_slices(
&self,
[tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4],
base_rect: Rect,
render_size: Vec2,
) -> [TextureSlice; 2] {
[
// Top
TextureSlice {
texture_rect: Rect {
min: base_rect.min + vec2(self.border.left, 0.0),
max: vec2(
base_rect.max.x - self.border.right,
base_rect.min.y + self.border.top,
),
},
draw_size: vec2(
render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x),
tl_corner.draw_size.y,
),
offset: vec2(
tl_corner.draw_size.x - tr_corner.draw_size.x,
render_size.y - tl_corner.draw_size.y,
) / 2.0,
},
// Bottom
TextureSlice {
texture_rect: Rect {
min: vec2(
base_rect.min.x + self.border.left,
base_rect.max.y - self.border.bottom,
),
max: base_rect.max - vec2(self.border.right, 0.0),
},
draw_size: vec2(
render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x),
bl_corner.draw_size.y,
),
offset: vec2(
bl_corner.draw_size.x - br_corner.draw_size.x,
bl_corner.draw_size.y - render_size.y,
) / 2.0,
},
]
}
/// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile,
/// a bigger number of sections will be computed.
///
/// # Arguments
///
/// * `rect` - The section of the texture to slice in 9 parts
/// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used.
// TODO: Support `URect` and `UVec2` instead (See `https://github.com/bevyengine/bevy/pull/11698`)
#[must_use]
pub fn compute_slices(&self, rect: Rect, render_size: Option<Vec2>) -> Vec<TextureSlice> {
let render_size = render_size.unwrap_or_else(|| rect.size());
if self.border.left + self.border.right >= rect.size().x
|| self.border.top + self.border.bottom >= rect.size().y
{
tracing::error!(
"TextureSlicer::border has out of bounds values. No slicing will be applied"
);
return vec![TextureSlice {
texture_rect: rect,
draw_size: render_size,
offset: Vec2::ZERO,
}];
}
let mut slices = Vec::with_capacity(9);
// Corners are in this order: [TL, TR, BL, BR]
let corners = self.corner_slices(rect, render_size);
// Vertical Sides: [T, B]
let vertical_sides = self.vertical_side_slices(&corners, rect, render_size);
// Horizontal Sides: [L, R]
let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size);
// Center
let center = TextureSlice {
texture_rect: Rect {
min: rect.min + vec2(self.border.left, self.border.top),
max: rect.max - vec2(self.border.right, self.border.bottom),
},
draw_size: vec2(
render_size.x - (corners[0].draw_size.x + corners[1].draw_size.x),
render_size.y - (corners[0].draw_size.y + corners[2].draw_size.y),
),
offset: vec2(vertical_sides[0].offset.x, horizontal_sides[0].offset.y),
};
slices.extend(corners);
match self.center_scale_mode {
SliceScaleMode::Stretch => {
slices.push(center);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(center.tiled(stretch_value, (true, true)));
}
}
match self.sides_scale_mode {
SliceScaleMode::Stretch => {
slices.extend(horizontal_sides);
slices.extend(vertical_sides);
}
SliceScaleMode::Tile { stretch_value } => {
slices.extend(
horizontal_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (false, true))),
);
slices.extend(
vertical_sides
.into_iter()
.flat_map(|s| s.tiled(stretch_value, (true, false))),
);
}
}
slices
}
}
impl Default for TextureSlicer {
fn default() -> Self {
Self {
border: Default::default(),
center_scale_mode: Default::default(),
sides_scale_mode: Default::default(),
max_corner_scale: 1.0,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_horizontal_sizes_uniform() {
let slicer = TextureSlicer {
border: BorderRect {
left: 10.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let base_rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_rect = Vec2::splat(100.);
let slices = slicer.corner_slices(base_rect, render_rect);
assert_eq!(
slices[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::splat(10.0)
},
draw_size: Vec2::new(10.0, 10.0),
offset: Vec2::new(-45.0, 45.0),
}
);
}
#[test]
fn test_horizontal_sizes_non_uniform_bigger() {
let slicer = TextureSlicer {
border: BorderRect {
left: 20.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let base_rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_rect = Vec2::splat(100.);
let slices = slicer.corner_slices(base_rect, render_rect);
assert_eq!(
slices[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(20.0, 10.0)
},
draw_size: Vec2::new(20.0, 10.0),
offset: Vec2::new(-40.0, 45.0),
}
);
}
#[test]
fn test_horizontal_sizes_non_uniform_smaller() {
let slicer = TextureSlicer {
border: BorderRect {
left: 5.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_size = Vec2::splat(100.);
let corners = slicer.corner_slices(rect, render_size);
let vertical_sides = slicer.vertical_side_slices(&corners, rect, render_size);
assert_eq!(
corners[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(5.0, 10.0)
},
draw_size: Vec2::new(5.0, 10.0),
offset: Vec2::new(-47.5, 45.0),
}
);
assert_eq!(
vertical_sides[0], // top
TextureSlice {
texture_rect: Rect {
min: Vec2::new(5.0, 0.0),
max: Vec2::new(40.0, 10.0)
},
draw_size: Vec2::new(85.0, 10.0),
offset: Vec2::new(-2.5, 45.0),
}
);
}
#[test]
fn test_horizontal_sizes_non_uniform_zero() {
let slicer = TextureSlicer {
border: BorderRect {
left: 0.,
right: 10.,
top: 10.,
bottom: 10.,
},
center_scale_mode: SliceScaleMode::Stretch,
sides_scale_mode: SliceScaleMode::Stretch,
max_corner_scale: 1.0,
};
let base_rect = Rect {
min: Vec2::ZERO,
max: Vec2::splat(50.),
};
let render_rect = Vec2::splat(100.);
let slices = slicer.corner_slices(base_rect, render_rect);
assert_eq!(
slices[0],
TextureSlice {
texture_rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(0.0, 10.0)
},
draw_size: Vec2::new(0.0, 10.0),
offset: Vec2::new(-50.0, 45.0),
}
);
}
}