diff --git a/Cargo.toml b/Cargo.toml index 453e18a1485dc7..d982bf05fca28f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2412,6 +2412,17 @@ description = "Demonstrates resizing and responding to resizing a window" category = "Window" wasm = true +[[example]] +name = "ui_material" +path = "examples/ui/ui_material.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_material] +name = "UI Material" +description = "Demonstrates creating and using custom Ui materials" +category = "UI (User Interface)" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/branding/bevy_bird_light.png b/assets/branding/bevy_bird_light.png new file mode 100644 index 00000000000000..ed81a69903d7ec Binary files /dev/null and b/assets/branding/bevy_bird_light.png differ diff --git a/assets/branding/bevy_bird_light.svg b/assets/branding/bevy_bird_light.svg new file mode 100644 index 00000000000000..5346a49b1605d6 --- /dev/null +++ b/assets/branding/bevy_bird_light.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/shaders/circle_shader.wgsl b/assets/shaders/circle_shader.wgsl new file mode 100644 index 00000000000000..94f7445e084091 --- /dev/null +++ b/assets/shaders/circle_shader.wgsl @@ -0,0 +1,21 @@ +// This shader draws a circle with a given input color +#import bevy_ui::ui_vertex_output::UiVertexOutput + +struct CustomUiMaterial { + @location(0) color: vec4 +} + +@group(1) @binding(0) +var input: CustomUiMaterial; + +@fragment +fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + // the UVs are now adjusted around the middle of the rect. + let uv = in.uv * 2.0 - 1.0; + + // circle alpha, the higher the power the harsher the falloff. + let alpha = 1.0 - pow(sqrt(dot(uv, uv)), 100.0); + + return vec4(input.color.rgb, alpha); +} + diff --git a/crates/bevy_asset/src/io/file/file_asset.rs b/crates/bevy_asset/src/io/file/file_asset.rs new file mode 100644 index 00000000000000..b33a2c80bb0e7d --- /dev/null +++ b/crates/bevy_asset/src/io/file/file_asset.rs @@ -0,0 +1,231 @@ +use crate::io::{ + get_meta_path, AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, + Reader, Writer, +}; +use async_fs::{read_dir, File}; +use bevy_utils::BoxedFuture; +use futures_lite::StreamExt; + +use std::path::Path; + +use super::{FileAssetReader, FileAssetWriter}; + +impl AssetReader for FileAssetReader { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + match File::open(&full_path).await { + Ok(file) => { + let reader: Box = Box::new(file); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) + } + } + } + }) + } + + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + let meta_path = get_meta_path(path); + Box::pin(async move { + let full_path = self.root_path.join(meta_path); + match File::open(&full_path).await { + Ok(file) => { + let reader: Box = Box::new(file); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) + } + } + } + }) + } + + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetReaderError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + match read_dir(&full_path).await { + Ok(read_dir) => { + let root_path = self.root_path.clone(); + let mapped_stream = read_dir.filter_map(move |f| { + f.ok().and_then(|dir_entry| { + let path = dir_entry.path(); + // filter out meta files as they are not considered assets + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext.eq_ignore_ascii_case("meta") { + return None; + } + } + let relative_path = path.strip_prefix(&root_path).unwrap(); + Some(relative_path.to_owned()) + }) + }); + let read_dir: Box = Box::new(mapped_stream); + Ok(read_dir) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) + } + } + } + }) + } + + fn is_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result> { + Box::pin(async move { + let full_path = self.root_path.join(path); + let metadata = full_path + .metadata() + .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; + Ok(metadata.file_type().is_dir()) + }) + } +} + +impl AssetWriter for FileAssetWriter { + fn write<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + if let Some(parent) = full_path.parent() { + async_fs::create_dir_all(parent).await?; + } + let file = File::create(&full_path).await?; + let writer: Box = Box::new(file); + Ok(writer) + }) + } + + fn write_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetWriterError>> { + Box::pin(async move { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + if let Some(parent) = full_path.parent() { + async_fs::create_dir_all(parent).await?; + } + let file = File::create(&full_path).await?; + let writer: Box = Box::new(file); + Ok(writer) + }) + } + + fn remove<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + async_fs::remove_file(full_path).await?; + Ok(()) + }) + } + + fn remove_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + async_fs::remove_file(full_path).await?; + Ok(()) + }) + } + + fn remove_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + async_fs::remove_dir_all(full_path).await?; + Ok(()) + }) + } + + fn remove_empty_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + async_fs::remove_dir(full_path).await?; + Ok(()) + }) + } + + fn remove_assets_in_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + async_fs::remove_dir_all(&full_path).await?; + async_fs::create_dir_all(&full_path).await?; + Ok(()) + }) + } + + fn rename<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_old_path = self.root_path.join(old_path); + let full_new_path = self.root_path.join(new_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; + Ok(()) + }) + } + + fn rename_meta<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let old_meta_path = get_meta_path(old_path); + let new_meta_path = get_meta_path(new_path); + let full_old_path = self.root_path.join(old_meta_path); + let full_new_path = self.root_path.join(new_meta_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; + Ok(()) + }) + } +} diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 629fd7dd9c6590..aae016df3bf1da 100644 --- a/crates/bevy_asset/src/io/file/mod.rs +++ b/crates/bevy_asset/src/io/file/mod.rs @@ -1,16 +1,14 @@ #[cfg(feature = "file_watcher")] mod file_watcher; + +#[cfg(feature = "multi-threaded")] +mod file_asset; +#[cfg(not(feature = "multi-threaded"))] +mod sync_file_asset; + #[cfg(feature = "file_watcher")] pub use file_watcher::*; -use crate::io::{ - get_meta_path, AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, - Reader, Writer, -}; -use async_fs::{read_dir, File}; -use bevy_utils::BoxedFuture; -use futures_lite::StreamExt; - use std::{ env, path::{Path, PathBuf}, @@ -72,102 +70,6 @@ impl FileAssetReader { } } -impl AssetReader for FileAssetReader { - fn read<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match File::open(&full_path).await { - Ok(file) => { - let reader: Box = Box::new(file); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } - } - } - }) - } - - fn read_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result>, AssetReaderError>> { - let meta_path = get_meta_path(path); - Box::pin(async move { - let full_path = self.root_path.join(meta_path); - match File::open(&full_path).await { - Ok(file) => { - let reader: Box = Box::new(file); - Ok(reader) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } - } - } - }) - } - - fn read_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetReaderError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - match read_dir(&full_path).await { - Ok(read_dir) => { - let root_path = self.root_path.clone(); - let mapped_stream = read_dir.filter_map(move |f| { - f.ok().and_then(|dir_entry| { - let path = dir_entry.path(); - // filter out meta files as they are not considered assets - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if ext.eq_ignore_ascii_case("meta") { - return None; - } - } - let relative_path = path.strip_prefix(&root_path).unwrap(); - Some(relative_path.to_owned()) - }) - }); - let read_dir: Box = Box::new(mapped_stream); - Ok(read_dir) - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Err(AssetReaderError::NotFound(full_path)) - } else { - Err(e.into()) - } - } - } - }) - } - - fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { - let full_path = self.root_path.join(path); - let metadata = full_path - .metadata() - .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; - Ok(metadata.file_type().is_dir()) - }) - } -} - pub struct FileAssetWriter { root_path: PathBuf, } @@ -183,127 +85,3 @@ impl FileAssetWriter { } } } - -impl AssetWriter for FileAssetWriter { - fn write<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - if let Some(parent) = full_path.parent() { - async_fs::create_dir_all(parent).await?; - } - let file = File::create(&full_path).await?; - let writer: Box = Box::new(file); - Ok(writer) - }) - } - - fn write_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, Result, AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - if let Some(parent) = full_path.parent() { - async_fs::create_dir_all(parent).await?; - } - let file = File::create(&full_path).await?; - let writer: Box = Box::new(file); - Ok(writer) - }) - } - - fn remove<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_file(full_path).await?; - Ok(()) - }) - } - - fn remove_meta<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let meta_path = get_meta_path(path); - let full_path = self.root_path.join(meta_path); - async_fs::remove_file(full_path).await?; - Ok(()) - }) - } - - fn remove_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir_all(full_path).await?; - Ok(()) - }) - } - - fn remove_empty_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir(full_path).await?; - Ok(()) - }) - } - - fn remove_assets_in_directory<'a>( - &'a self, - path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir_all(&full_path).await?; - async_fs::create_dir_all(&full_path).await?; - Ok(()) - }) - } - - fn rename<'a>( - &'a self, - old_path: &'a Path, - new_path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let full_old_path = self.root_path.join(old_path); - let full_new_path = self.root_path.join(new_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; - Ok(()) - }) - } - - fn rename_meta<'a>( - &'a self, - old_path: &'a Path, - new_path: &'a Path, - ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { - Box::pin(async move { - let old_meta_path = get_meta_path(old_path); - let new_meta_path = get_meta_path(new_path); - let full_old_path = self.root_path.join(old_meta_path); - let full_new_path = self.root_path.join(new_meta_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; - Ok(()) - }) - } -} diff --git a/crates/bevy_asset/src/io/file/sync_file_asset.rs b/crates/bevy_asset/src/io/file/sync_file_asset.rs new file mode 100644 index 00000000000000..a8bf573a7ab071 --- /dev/null +++ b/crates/bevy_asset/src/io/file/sync_file_asset.rs @@ -0,0 +1,296 @@ +use futures_io::{AsyncRead, AsyncWrite}; +use futures_lite::Stream; + +use crate::io::{ + get_meta_path, AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, + Reader, Writer, +}; +use bevy_utils::BoxedFuture; + +use std::{ + fs::{read_dir, File}, + io::{Read, Write}, + path::{Path, PathBuf}, + pin::Pin, + task::Poll, +}; + +use super::{FileAssetReader, FileAssetWriter}; + +struct FileReader(File); + +impl AsyncRead for FileReader { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let this = self.get_mut(); + let read = this.0.read(buf); + Poll::Ready(read) + } +} + +struct FileWriter(File); + +impl AsyncWrite for FileWriter { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.get_mut(); + let wrote = this.0.write(buf); + Poll::Ready(wrote) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + let flushed = this.0.flush(); + Poll::Ready(flushed) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} + +struct DirReader(Vec); + +impl Stream for DirReader { + type Item = PathBuf; + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + Poll::Ready(this.0.pop()) + } +} + +impl AssetReader for FileAssetReader { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + match File::open(&full_path) { + Ok(file) => { + let reader: Box = Box::new(FileReader(file)); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) + } + } + } + }) + } + + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + let meta_path = get_meta_path(path); + Box::pin(async move { + let full_path = self.root_path.join(meta_path); + match File::open(&full_path) { + Ok(file) => { + let reader: Box = Box::new(FileReader(file)); + Ok(reader) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) + } + } + } + }) + } + + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetReaderError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + match read_dir(&full_path) { + Ok(read_dir) => { + let root_path = self.root_path.clone(); + let mapped_stream = read_dir.filter_map(move |f| { + f.ok().and_then(|dir_entry| { + let path = dir_entry.path(); + // filter out meta files as they are not considered assets + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext.eq_ignore_ascii_case("meta") { + return None; + } + } + let relative_path = path.strip_prefix(&root_path).unwrap(); + Some(relative_path.to_owned()) + }) + }); + let read_dir: Box = Box::new(DirReader(mapped_stream.collect())); + Ok(read_dir) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Err(AssetReaderError::NotFound(full_path)) + } else { + Err(e.into()) + } + } + } + }) + } + + fn is_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result> { + Box::pin(async move { + let full_path = self.root_path.join(path); + let metadata = full_path + .metadata() + .map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?; + Ok(metadata.file_type().is_dir()) + }) + } +} + +impl AssetWriter for FileAssetWriter { + fn write<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + if let Some(parent) = full_path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = File::create(&full_path)?; + let writer: Box = Box::new(FileWriter(file)); + Ok(writer) + }) + } + + fn write_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetWriterError>> { + Box::pin(async move { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + if let Some(parent) = full_path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = File::create(&full_path)?; + let writer: Box = Box::new(FileWriter(file)); + Ok(writer) + }) + } + + fn remove<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + std::fs::remove_file(full_path)?; + Ok(()) + }) + } + + fn remove_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let meta_path = get_meta_path(path); + let full_path = self.root_path.join(meta_path); + std::fs::remove_file(full_path)?; + Ok(()) + }) + } + + fn remove_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + std::fs::remove_dir_all(full_path)?; + Ok(()) + }) + } + + fn remove_empty_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + std::fs::remove_dir(full_path)?; + Ok(()) + }) + } + + fn remove_assets_in_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_path = self.root_path.join(path); + std::fs::remove_dir_all(&full_path)?; + std::fs::create_dir_all(&full_path)?; + Ok(()) + }) + } + + fn rename<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let full_old_path = self.root_path.join(old_path); + let full_new_path = self.root_path.join(new_path); + if let Some(parent) = full_new_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(full_old_path, full_new_path)?; + Ok(()) + }) + } + + fn rename_meta<'a>( + &'a self, + old_path: &'a Path, + new_path: &'a Path, + ) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> { + Box::pin(async move { + let old_meta_path = get_meta_path(old_path); + let new_meta_path = get_meta_path(new_path); + let full_old_path = self.root_path.join(old_meta_path); + let full_new_path = self.root_path.join(new_meta_path); + if let Some(parent) = full_new_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(full_old_path, full_new_path)?; + Ok(()) + }) + } +} diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 94ab97593dd28a..3a5bbc7d032aa9 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -412,7 +412,7 @@ impl<'a> LoadContext<'a> { &self.asset_path } - /// Gets the source asset path for this load context. + /// Reads the asset at the given path and returns its bytes pub async fn read_asset_bytes<'b, 'c>( &'b mut self, path: impl Into>, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index f7404efbd7654d..f003ed6191f2b1 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -450,10 +450,10 @@ pub fn extract_camera_prepass_phase( ( Entity, &Camera, - Option<&DepthPrepass>, - Option<&NormalPrepass>, - Option<&MotionVectorPrepass>, - Option<&DeferredPrepass>, + Has, + Has, + Has, + Has, ), With, >, @@ -465,33 +465,30 @@ pub fn extract_camera_prepass_phase( if camera.is_active { let mut entity = commands.get_or_spawn(entity); - if depth_prepass.is_some() - || normal_prepass.is_some() - || motion_vector_prepass.is_some() - { + if depth_prepass || normal_prepass || motion_vector_prepass { entity.insert(( RenderPhase::::default(), RenderPhase::::default(), )); } - if deferred_prepass.is_some() { + if deferred_prepass { entity.insert(( RenderPhase::::default(), RenderPhase::::default(), )); } - if depth_prepass.is_some() { + if depth_prepass { entity.insert(DepthPrepass); } - if normal_prepass.is_some() { + if normal_prepass { entity.insert(NormalPrepass); } - if motion_vector_prepass.is_some() { + if motion_vector_prepass { entity.insert(MotionVectorPrepass); } - if deferred_prepass.is_some() { + if deferred_prepass { entity.insert(DeferredPrepass); } } @@ -685,15 +682,17 @@ pub fn prepare_prepass_textures( ( Entity, &ExtractedCamera, - Option<&DepthPrepass>, - Option<&NormalPrepass>, - Option<&MotionVectorPrepass>, - Option<&DeferredPrepass>, + Has, + Has, + Has, + Has, ), - ( + Or<( With>, With>, - ), + With>, + With>, + )>, >, ) { let mut depth_textures = HashMap::default(); @@ -714,7 +713,7 @@ pub fn prepare_prepass_textures( height: physical_target_size.y, }; - let cached_depth_texture = depth_prepass.is_some().then(|| { + let cached_depth_texture = depth_prepass.then(|| { depth_textures .entry(camera.target.clone()) .or_insert_with(|| { @@ -735,7 +734,7 @@ pub fn prepare_prepass_textures( .clone() }); - let cached_normals_texture = normal_prepass.is_some().then(|| { + let cached_normals_texture = normal_prepass.then(|| { normal_textures .entry(camera.target.clone()) .or_insert_with(|| { @@ -757,7 +756,7 @@ pub fn prepare_prepass_textures( .clone() }); - let cached_motion_vectors_texture = motion_vector_prepass.is_some().then(|| { + let cached_motion_vectors_texture = motion_vector_prepass.then(|| { motion_vectors_textures .entry(camera.target.clone()) .or_insert_with(|| { @@ -779,7 +778,7 @@ pub fn prepare_prepass_textures( .clone() }); - let cached_deferred_texture = deferred_prepass.is_some().then(|| { + let cached_deferred_texture = deferred_prepass.then(|| { deferred_textures .entry(camera.target.clone()) .or_insert_with(|| { @@ -801,7 +800,7 @@ pub fn prepare_prepass_textures( .clone() }); - let deferred_lighting_pass_id_texture = deferred_prepass.is_some().then(|| { + let deferred_lighting_pass_id_texture = deferred_prepass.then(|| { deferred_lighting_id_textures .entry(camera.target.clone()) .or_insert_with(|| { diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index ba4de1952c82ea..63b8c764af5ffb 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -55,6 +55,7 @@ pub struct NormalPrepass; pub struct MotionVectorPrepass; /// If added to a [`crate::prelude::Camera3d`] then deferred materials will be rendered to the deferred gbuffer texture and will be available to subsequent passes. +/// Note the default deferred lighting plugin also requires `DepthPrepass` to work correctly. #[derive(Component, Default, Reflect)] pub struct DeferredPrepass; diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 416d7e9a9992ff..2cb016753513b3 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -4,10 +4,14 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; -use bevy_core_pipeline::core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}; +use bevy_core_pipeline::{ + core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, +}; use bevy_ecs::{ prelude::Entity, + query::Has, schedule::IntoSystemConfigs, system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, @@ -69,7 +73,7 @@ impl FromWorld for LineGizmoPipeline { #[derive(PartialEq, Eq, Hash, Clone)] struct LineGizmoPipelineKey { - mesh_key: MeshPipelineKey, + view_key: MeshPipelineKey, strip: bool, perspective: bool, } @@ -87,7 +91,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { shader_defs.push("PERSPECTIVE".into()); } - let format = if key.mesh_key.contains(MeshPipelineKey::HDR) { + let format = if key.view_key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR } else { TextureFormat::bevy_default() @@ -95,7 +99,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { let view_layout = self .mesh_pipeline - .get_view_layout(key.mesh_key.into()) + .get_view_layout(key.view_key.into()) .clone(); let layout = vec![view_layout, self.uniform_layout.clone()]; @@ -127,7 +131,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { bias: DepthBiasState::default(), }), multisample: MultisampleState { - count: key.mesh_key.msaa_samples(), + count: key.view_key.msaa_samples(), mask: !0, alpha_to_coverage_enabled: false, }, @@ -158,19 +162,47 @@ fn queue_line_gizmos_3d( &ExtractedView, &mut RenderPhase, Option<&RenderLayers>, + ( + Has, + Has, + Has, + Has, + ), )>, ) { let draw_function = draw_functions.read().get_id::().unwrap(); - for (view, mut transparent_phase, render_layers) in &mut views { + for ( + view, + mut transparent_phase, + render_layers, + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + ) in &mut views + { let render_layers = render_layers.copied().unwrap_or_default(); if !config.render_layers.intersects(&render_layers) { continue; } - let mesh_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) | MeshPipelineKey::from_hdr(view.hdr); + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + for (entity, handle) in &line_gizmos { let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue; @@ -180,7 +212,7 @@ fn queue_line_gizmos_3d( &pipeline_cache, &pipeline, LineGizmoPipelineKey { - mesh_key, + view_key, strip: line_gizmo.strip, perspective: config.line_perspective, }, diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index c73424c86a0910..8e1cfdd41d34b6 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -58,10 +58,17 @@ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; use bevy_ecs::prelude::*; use bevy_render::{ - camera::CameraUpdateSystem, extract_component::ExtractComponentPlugin, - extract_resource::ExtractResourcePlugin, prelude::Color, render_asset::prepare_assets, - render_graph::RenderGraph, render_phase::sort_phase_system, render_resource::Shader, - texture::Image, view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderSet, + camera::{CameraUpdateSystem, Projection}, + extract_component::ExtractComponentPlugin, + extract_resource::ExtractResourcePlugin, + prelude::Color, + render_asset::prepare_assets, + render_graph::RenderGraph, + render_phase::sort_phase_system, + render_resource::Shader, + texture::Image, + view::VisibilitySystems, + ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::TransformSystem; use environment_map::EnvironmentMapPlugin; @@ -273,7 +280,11 @@ impl Plugin for PbrPlugin { .after(TransformSystem::TransformPropagate) .after(VisibilitySystems::CheckVisibility) .after(CameraUpdateSystem), - update_directional_light_cascades + ( + clear_directional_light_cascades, + build_directional_light_cascades::, + ) + .chain() .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) .after(TransformSystem::TransformPropagate) .after(CameraUpdateSystem), diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 599bd352933f4a..54ea52e5b972c3 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -1,14 +1,13 @@ use std::collections::HashSet; use bevy_ecs::prelude::*; -use bevy_math::{Mat4, Rect, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_reflect::prelude::*; use bevy_render::{ - camera::Camera, + camera::{Camera, CameraProjection}, color::Color, extract_component::ExtractComponent, extract_resource::ExtractResource, - prelude::Projection, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, HalfSpace, Sphere}, render_resource::BufferBindingType, renderer::RenderDevice, @@ -116,7 +115,7 @@ pub struct SpotLight { impl SpotLight { pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; - pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; + pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; } impl Default for SpotLight { @@ -210,7 +209,7 @@ impl Default for DirectionalLight { impl DirectionalLight { pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; - pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; + pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; } /// Controls the resolution of [`DirectionalLight`] shadow maps. @@ -397,9 +396,18 @@ pub struct Cascade { pub(crate) texel_size: f32, } -pub fn update_directional_light_cascades( +pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { + for (directional_light, mut cascades) in lights.iter_mut() { + if !directional_light.shadows_enabled { + continue; + } + cascades.cascades.clear(); + } +} + +pub fn build_directional_light_cascades( directional_light_shadow_map: Res, - views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, + views: Query<(Entity, &GlobalTransform, &P, &Camera)>, mut lights: Query<( &GlobalTransform, &DirectionalLight, @@ -432,7 +440,6 @@ pub fn update_directional_light_cascades( let light_to_world = Mat4::from_quat(transform.compute_transform().rotation); let light_to_world_inverse = light_to_world.inverse(); - cascades.cascades.clear(); for (view_entity, projection, view_to_world) in views.iter().copied() { let camera_to_light_view = light_to_world_inverse * view_to_world; let view_cascades = cascades_config @@ -449,17 +456,8 @@ pub fn update_directional_light_cascades( }; let z_far = -far_bound; - let corners = match projection { - Projection::Perspective(projection) => frustum_corners( - projection.aspect_ratio, - (projection.fov / 2.).tan(), - z_near, - z_far, - ), - Projection::Orthographic(projection) => { - frustum_corners_ortho(projection.area, z_near, z_far) - } - }; + let corners = projection.get_frustum_corners(z_near, z_far); + calculate_cascade( corners, directional_light_shadow_map.size as f32, @@ -473,36 +471,6 @@ pub fn update_directional_light_cascades( } } -fn frustum_corners_ortho(area: Rect, z_near: f32, z_far: f32) -> [Vec3A; 8] { - // NOTE: These vertices are in the specific order required by [`calculate_cascade`]. - [ - Vec3A::new(area.max.x, area.min.y, z_near), // bottom right - Vec3A::new(area.max.x, area.max.y, z_near), // top right - Vec3A::new(area.min.x, area.max.y, z_near), // top left - Vec3A::new(area.min.x, area.min.y, z_near), // bottom left - Vec3A::new(area.max.x, area.min.y, z_far), // bottom right - Vec3A::new(area.max.x, area.max.y, z_far), // top right - Vec3A::new(area.min.x, area.max.y, z_far), // top left - Vec3A::new(area.min.x, area.min.y, z_far), // bottom left - ] -} - -fn frustum_corners(aspect_ratio: f32, tan_half_fov: f32, z_near: f32, z_far: f32) -> [Vec3A; 8] { - let a = z_near.abs() * tan_half_fov; - let b = z_far.abs() * tan_half_fov; - // NOTE: These vertices are in the specific order required by [`calculate_cascade`]. - [ - Vec3A::new(a * aspect_ratio, -a, z_near), // bottom right - Vec3A::new(a * aspect_ratio, a, z_near), // top right - Vec3A::new(-a * aspect_ratio, a, z_near), // top left - Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left - Vec3A::new(b * aspect_ratio, -b, z_far), // bottom right - Vec3A::new(b * aspect_ratio, b, z_far), // top right - Vec3A::new(-b * aspect_ratio, b, z_far), // top left - Vec3A::new(-b * aspect_ratio, -b, z_far), // bottom left - ] -} - /// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. /// The corner vertices should be specified in the following order: /// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane. diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 00e9e4f2f3b399..6f2dfed69e12d2 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -431,11 +431,11 @@ where if key.mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { shader_defs.push("DEFERRED_PREPASS".into()); + } - if layout.contains(Mesh::ATTRIBUTE_COLOR) { - shader_defs.push("VERTEX_COLORS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(6)); - } + if layout.contains(Mesh::ATTRIBUTE_COLOR) { + shader_defs.push("VERTEX_COLORS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(6)); } if key @@ -784,9 +784,6 @@ pub fn queue_prepass_material_meshes( view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; } - let mut opaque_phase_deferred = opaque_deferred_phase.as_mut(); - let mut alpha_mask_phase_deferred = alpha_mask_deferred_phase.as_mut(); - let rangefinder = view.rangefinder3d(); for visible_entity in &visible_entities.entities { @@ -859,7 +856,7 @@ pub fn queue_prepass_material_meshes( match alpha_mode { AlphaMode::Opaque => { if deferred { - opaque_phase_deferred + opaque_deferred_phase .as_mut() .unwrap() .add(Opaque3dDeferred { @@ -870,8 +867,8 @@ pub fn queue_prepass_material_meshes( batch_range: 0..1, dynamic_offset: None, }); - } else { - opaque_phase.as_mut().unwrap().add(Opaque3dPrepass { + } else if let Some(opaque_phase) = opaque_phase.as_mut() { + opaque_phase.add(Opaque3dPrepass { entity: *visible_entity, draw_function: opaque_draw_prepass, pipeline_id, @@ -883,7 +880,7 @@ pub fn queue_prepass_material_meshes( } AlphaMode::Mask(_) => { if deferred { - alpha_mask_phase_deferred + alpha_mask_deferred_phase .as_mut() .unwrap() .add(AlphaMask3dDeferred { @@ -894,8 +891,8 @@ pub fn queue_prepass_material_meshes( batch_range: 0..1, dynamic_offset: None, }); - } else { - alpha_mask_phase.as_mut().unwrap().add(AlphaMask3dPrepass { + } else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() { + alpha_mask_phase.add(AlphaMask3dPrepass { entity: *visible_entity, draw_function: alpha_mask_draw_prepass, pipeline_id, diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 7b62385cac7925..912dc8009f37c1 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; -use bevy_math::{Mat4, Rect, Vec2}; +use bevy_math::{Mat4, Rect, Vec2, Vec3A}; use bevy_reflect::{ std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize, }; @@ -58,6 +58,7 @@ pub trait CameraProjection { fn get_projection_matrix(&self) -> Mat4; fn update(&mut self, width: f32, height: f32); fn far(&self) -> f32; + fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8]; } /// A configurable [`CameraProjection`] that can select its projection type at runtime. @@ -101,6 +102,13 @@ impl CameraProjection for Projection { Projection::Orthographic(projection) => projection.far(), } } + + fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { + match self { + Projection::Perspective(projection) => projection.get_frustum_corners(z_near, z_far), + Projection::Orthographic(projection) => projection.get_frustum_corners(z_near, z_far), + } + } } impl Default for Projection { @@ -153,6 +161,24 @@ impl CameraProjection for PerspectiveProjection { fn far(&self) -> f32 { self.far } + + fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { + let tan_half_fov = (self.fov / 2.).tan(); + let a = z_near.abs() * tan_half_fov; + let b = z_far.abs() * tan_half_fov; + let aspect_ratio = self.aspect_ratio; + // NOTE: These vertices are in the specific order required by [`calculate_cascade`]. + [ + Vec3A::new(a * aspect_ratio, -a, z_near), // bottom right + Vec3A::new(a * aspect_ratio, a, z_near), // top right + Vec3A::new(-a * aspect_ratio, a, z_near), // top left + Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left + Vec3A::new(b * aspect_ratio, -b, z_far), // bottom right + Vec3A::new(b * aspect_ratio, b, z_far), // top right + Vec3A::new(-b * aspect_ratio, b, z_far), // top left + Vec3A::new(-b * aspect_ratio, -b, z_far), // bottom left + ] + } } impl Default for PerspectiveProjection { @@ -309,6 +335,21 @@ impl CameraProjection for OrthographicProjection { fn far(&self) -> f32 { self.far } + + fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { + let area = self.area; + // NOTE: These vertices are in the specific order required by [`calculate_cascade`]. + [ + Vec3A::new(area.max.x, area.min.y, z_near), // bottom right + Vec3A::new(area.max.x, area.max.y, z_near), // top right + Vec3A::new(area.min.x, area.max.y, z_near), // top left + Vec3A::new(area.min.x, area.min.y, z_near), // bottom left + Vec3A::new(area.max.x, area.min.y, z_far), // bottom right + Vec3A::new(area.max.x, area.max.y, z_far), // top right + Vec3A::new(area.min.x, area.max.y, z_far), // top left + Vec3A::new(area.min.x, area.min.y, z_far), // bottom left + ] + } } impl Default for OrthographicProjection { diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 2a465c55273e5c..8b2a98e7e7537d 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -390,19 +390,10 @@ pub fn check_visibility( &InheritedVisibility, &mut ViewVisibility, Option<&RenderLayers>, - &Aabb, + Option<&Aabb>, &GlobalTransform, Has, )>, - mut visible_no_aabb_query: Query< - ( - Entity, - &InheritedVisibility, - &mut ViewVisibility, - Option<&RenderLayers>, - ), - Without, - >, ) { for (mut visible_entities, frustum, maybe_view_mask) in &mut view_query { let view_mask = maybe_view_mask.copied().unwrap_or_default(); @@ -414,7 +405,7 @@ pub fn check_visibility( inherited_visibility, mut view_visibility, maybe_entity_mask, - model_aabb, + maybe_model_aabb, transform, no_frustum_culling, ) = query_item; @@ -430,20 +421,22 @@ pub fn check_visibility( return; } - // If we have an aabb and transform, do frustum culling + // If we have an aabb, do frustum culling if !no_frustum_culling { - let model = transform.affine(); - let model_sphere = Sphere { - center: model.transform_point3a(model_aabb.center), - radius: transform.radius_vec3a(model_aabb.half_extents), - }; - // Do quick sphere-based frustum culling - if !frustum.intersects_sphere(&model_sphere, false) { - return; - } - // If we have an aabb, do aabb-based frustum culling - if !frustum.intersects_obb(model_aabb, &model, true, false) { - return; + if let Some(model_aabb) = maybe_model_aabb { + let model = transform.affine(); + let model_sphere = Sphere { + center: model.transform_point3a(model_aabb.center), + radius: transform.radius_vec3a(model_aabb.half_extents), + }; + // Do quick sphere-based frustum culling + if !frustum.intersects_sphere(&model_sphere, false) { + return; + } + // Do aabb-based frustum culling + if !frustum.intersects_obb(model_aabb, &model, true, false) { + return; + } } } @@ -454,27 +447,6 @@ pub fn check_visibility( cell.set(queue); }); - visible_no_aabb_query.par_iter_mut().for_each(|query_item| { - let (entity, inherited_visibility, mut view_visibility, maybe_entity_mask) = query_item; - - // Skip computing visibility for entities that are configured to be hidden. - // `ViewVisibility` has already been reset in `reset_view_visibility`. - if !inherited_visibility.get() { - return; - } - - let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); - if !view_mask.intersects(&entity_mask) { - return; - } - - view_visibility.set(); - let cell = thread_queues.get_or_default(); - let mut queue = cell.take(); - queue.push(entity); - cell.set(queue); - }); - for cell in &mut thread_queues { visible_entities.entities.append(cell.get_mut()); } diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 799e827257a1d5..9462ef4e770d59 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -4,7 +4,6 @@ use bevy_reflect::ReflectDeserialize; use bevy_reflect::ReflectSerialize; use serde::Deserialize; use serde::Serialize; -use std::fmt::Display; use std::ops::Neg; use std::ops::{Div, DivAssign, Mul, MulAssign}; use thiserror::Error; @@ -156,22 +155,6 @@ impl DivAssign for Val { } } -impl Display for Val { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let (value, suffix) = match self { - Val::Auto => return write!(f, "auto"), - Val::Px(value) => (value, "px"), - Val::Percent(value) => (value, "%"), - Val::Vw(value) => (value, "vw"), - Val::Vh(value) => (value, "vh"), - Val::VMin(value) => (value, "vmin"), - Val::VMax(value) => (value, "vmax"), - }; - value.fmt(f)?; - write!(f, "{suffix}") - } -} - impl Neg for Val { type Output = Val; diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index ddfe5f430f4463..e72eb5ea777d2d 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -8,6 +8,7 @@ pub mod camera_config; pub mod measurement; pub mod node_bundles; +pub mod ui_material; pub mod update; pub mod widget; @@ -29,6 +30,7 @@ pub use geometry::*; pub use layout::*; pub use measurement::*; pub use render::*; +pub use ui_material::*; pub use ui_node::*; use widget::UiImageSize; @@ -36,8 +38,8 @@ use widget::UiImageSize; pub mod prelude { #[doc(hidden)] pub use crate::{ - camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::Button, widget::Label, - Interaction, UiScale, + camera_config::*, geometry::*, node_bundles::*, ui_material::*, ui_node::*, widget::Button, + widget::Label, Interaction, UiMaterialPlugin, UiScale, }; } diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index ac527d2172f805..c2401446bcc345 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -5,7 +5,7 @@ use crate::widget::TextFlags; use crate::{ widget::{Button, UiImageSize}, BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, - UiTextureAtlasImage, ZIndex, + UiMaterial, UiTextureAtlasImage, ZIndex, }; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; @@ -342,3 +342,52 @@ impl Default for ButtonBundle { } } } + +/// A UI node that is rendered using a [`UiMaterial`] +#[derive(Bundle, Clone, Debug)] +pub struct MaterialNodeBundle { + /// Describes the logical size of the node + pub node: Node, + /// Styles which control the layout (size and position) of the node and it's children + /// In some cases these styles also affect how the node drawn/painted. + pub style: Style, + /// The [`UiMaterial`] used to render the node. + pub material: Handle, + /// Whether this node should block interaction with lower nodes + pub focus_policy: FocusPolicy, + /// The transform of the node + /// + /// This field is automatically managed by the UI layout system. + /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. + pub transform: Transform, + /// The global transform of the node + /// + /// This field is automatically managed by the UI layout system. + /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. + pub global_transform: GlobalTransform, + /// Describes the visibility properties of the node + pub visibility: Visibility, + /// Inherited visibility of an entity. + pub inherited_visibility: InheritedVisibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub view_visibility: ViewVisibility, + /// Indicates the depth at which the node should appear in the UI + pub z_index: ZIndex, +} + +impl Default for MaterialNodeBundle { + fn default() -> Self { + Self { + node: Default::default(), + style: Default::default(), + material: Default::default(), + focus_policy: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + inherited_visibility: Default::default(), + view_visibility: Default::default(), + z_index: Default::default(), + } + } +} diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 40985ad237f0fc..f63fd1a0dc36c9 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -1,5 +1,6 @@ mod pipeline; mod render_pass; +mod ui_material_pipeline; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_hierarchy::Parent; @@ -9,6 +10,7 @@ use bevy_render::{render_resource::BindGroupEntries, ExtractSchedule, Render}; use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; +pub use ui_material_pipeline::*; use crate::Outline; use crate::{ @@ -253,7 +255,7 @@ pub fn extract_atlas_uinodes( } } -fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { +pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { match value { Val::Auto => 0., Val::Px(px) => px.max(0.), @@ -695,14 +697,14 @@ impl Default for UiMeta { } } -const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ Vec3::new(-0.5, -0.5, 0.0), Vec3::new(0.5, -0.5, 0.0), Vec3::new(0.5, 0.5, 0.0), Vec3::new(-0.5, 0.5, 0.0), ]; -const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; +pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; #[derive(Component)] pub struct UiBatch { @@ -732,6 +734,7 @@ pub fn queue_uinodes( transparent_phase .items .reserve(extracted_uinodes.uinodes.len()); + for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { transparent_phase.add(TransparentUi { draw_function, @@ -781,11 +784,6 @@ pub fn prepare_uinodes( }; } - #[inline] - fn is_textured(image: AssetId) -> bool { - image != AssetId::default() - } - if let Some(view_binding) = view_uniforms.uniforms.binding() { let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len); @@ -798,7 +796,6 @@ pub fn prepare_uinodes( // Vertex buffer index let mut index = 0; - for mut ui_phase in &mut phases { let mut batch_item_index = 0; let mut batch_image_handle = AssetId::invalid(); @@ -806,11 +803,14 @@ pub fn prepare_uinodes( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; if let Some(extracted_uinode) = extracted_uinodes.uinodes.get(&item.entity) { - let mut existing_batch = batches - .last_mut() - .filter(|_| batch_image_handle == extracted_uinode.image); - - if existing_batch.is_none() { + let mut existing_batch = batches.last_mut(); + + if batch_image_handle == AssetId::invalid() + || existing_batch.is_none() + || (batch_image_handle != AssetId::default() + && extracted_uinode.image != AssetId::default() + && batch_image_handle != extracted_uinode.image) + { if let Some(gpu_image) = gpu_images.get(extracted_uinode.image) { batch_item_index = item_index; batch_image_handle = extracted_uinode.image; @@ -840,9 +840,32 @@ pub fn prepare_uinodes( } else { continue; } + } else if batch_image_handle == AssetId::default() + && extracted_uinode.image != AssetId::default() + { + if let Some(gpu_image) = gpu_images.get(extracted_uinode.image) { + batch_image_handle = extracted_uinode.image; + existing_batch.as_mut().unwrap().1.image = extracted_uinode.image; + + image_bind_groups + .values + .entry(batch_image_handle) + .or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &ui_pipeline.image_layout, + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + } else { + continue; + } } - let mode = if is_textured(extracted_uinode.image) { + let mode = if extracted_uinode.image != AssetId::default() { TEXTURED_QUAD } else { UNTEXTURED_QUAD diff --git a/crates/bevy_ui/src/render/ui_material.wgsl b/crates/bevy_ui/src/render/ui_material.wgsl new file mode 100644 index 00000000000000..db9628559de369 --- /dev/null +++ b/crates/bevy_ui/src/render/ui_material.wgsl @@ -0,0 +1,23 @@ +#import bevy_render::view::View +#import bevy_ui::ui_vertex_output::UiVertexOutput + +@group(0) @binding(0) +var view: View; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2, + @location(2) border_widths: vec4, +) -> UiVertexOutput { + var out: UiVertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + out.border_widths = border_widths; + return out; +} + +@fragment +fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + return vec4(1.0); +} diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs new file mode 100644 index 00000000000000..f952f726e1aebf --- /dev/null +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -0,0 +1,756 @@ +use std::{hash::Hash, marker::PhantomData, ops::Range}; + +use bevy_app::{App, Plugin}; +use bevy_asset::*; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::{Component, Entity, EventReader}, + query::{ROQueryItem, With}, + schedule::IntoSystemConfigs, + storage::SparseSet, + system::lifetimeless::{Read, SRes}, + system::*, + world::{FromWorld, World}, +}; +use bevy_math::{Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_render::{ + extract_component::ExtractComponentPlugin, + render_asset::RenderAssets, + render_phase::*, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, FallbackImage, Image}, + view::*, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::{FloatOrd, HashMap, HashSet}; +use bevy_window::{PrimaryWindow, Window}; +use bytemuck::{Pod, Zeroable}; + +use crate::*; + +pub const UI_MATERIAL_SHADER_HANDLE: Handle = Handle::weak_from_u128(10074188772096983955); + +const UI_VERTEX_OUTPUT_SHADER_HANDLE: Handle = Handle::weak_from_u128(10123618247720234751); + +/// Adds the necessary ECS resources and render logic to enable rendering entities using the given +/// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). +pub struct UiMaterialPlugin(PhantomData); + +impl Default for UiMaterialPlugin { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Plugin for UiMaterialPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!( + app, + UI_VERTEX_OUTPUT_SHADER_HANDLE, + "ui_vertex_output.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + UI_MATERIAL_SHADER_HANDLE, + "ui_material.wgsl", + Shader::from_wgsl + ); + app.init_asset::() + .add_plugins(ExtractComponentPlugin::>::extract_visible()); + + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .add_render_command::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::() + .init_resource::>>() + .add_systems( + ExtractSchedule, + ( + extract_ui_materials::, + extract_ui_material_nodes::.in_set(RenderUiSystem::ExtractNode), + ), + ) + .add_systems( + Render, + ( + prepare_ui_materials::.in_set(RenderSet::PrepareAssets), + queue_ui_material_nodes::.in_set(RenderSet::Queue), + prepare_uimaterial_nodes::.in_set(RenderSet::PrepareBindGroups), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::>(); + } + } +} + +#[derive(Resource)] +pub struct UiMaterialMeta { + vertices: BufferVec, + view_bind_group: Option, +} + +impl Default for UiMaterialMeta { + fn default() -> Self { + Self { + vertices: BufferVec::new(BufferUsages::VERTEX), + view_bind_group: Default::default(), + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct UiMaterialVertex { + pub position: [f32; 3], + pub uv: [f32; 2], + pub border_widths: [f32; 4], +} + +// in this [`UiMaterialPipeline`] there is (currently) no batching going on. +// Therefore the [`UiMaterialBatch`] is more akin to a draw call. +#[derive(Component)] +pub struct UiMaterialBatch { + /// The range of vertices inside the [`UiMaterialMeta`] + pub range: Range, + pub material: AssetId, +} + +/// Render pipeline data for a given [`UiMaterial`] +#[derive(Resource)] +pub struct UiMaterialPipeline { + pub ui_layout: BindGroupLayout, + pub view_layout: BindGroupLayout, + pub vertex_shader: Option>, + pub fragment_shader: Option>, + marker: PhantomData, +} + +impl SpecializedRenderPipeline for UiMaterialPipeline +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type Key = UiMaterialKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let vertex_layout = VertexBufferLayout::from_vertex_formats( + VertexStepMode::Vertex, + vec![ + // position + VertexFormat::Float32x3, + // uv + VertexFormat::Float32x2, + // border_widths + VertexFormat::Float32x4, + ], + ); + let shader_defs = Vec::new(); + + let mut descriptor = RenderPipelineDescriptor { + vertex: VertexState { + shader: UI_MATERIAL_SHADER_HANDLE, + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_layout], + }, + fragment: Some(FragmentState { + shader: UI_MATERIAL_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout: vec![], + push_constant_ranges: Vec::new(), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("ui_material_pipeline".into()), + }; + if let Some(vertex_shader) = &self.vertex_shader { + descriptor.vertex.shader = vertex_shader.clone(); + } + + if let Some(fragment_shader) = &self.fragment_shader { + descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); + } + + descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()]; + + M::specialize(&mut descriptor, key); + + descriptor + } +} + +impl FromWorld for UiMaterialPipeline { + fn from_world(world: &mut World) -> Self { + let asset_server = world.resource::(); + let render_device = world.resource::(); + let ui_layout = M::bind_group_layout(render_device); + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(ViewUniform::min_size()), + }, + count: None, + }], + label: Some("ui_view_layout"), + }); + UiMaterialPipeline { + ui_layout, + view_layout, + vertex_shader: match M::vertex_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + fragment_shader: match M::fragment_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + marker: PhantomData, + } + } +} + +pub type DrawUiMaterial = ( + SetItemPipeline, + SetMatUiViewBindGroup, + SetUiMaterialBindGroup, + DrawUiMaterialNode, +); + +pub struct SetMatUiViewBindGroup(PhantomData); +impl RenderCommand

for SetMatUiViewBindGroup { + type Param = SRes; + type ViewWorldQuery = Read; + type ItemWorldQuery = (); + + fn render<'w>( + _item: &P, + view_uniform: &'w ViewUniformOffset, + _entity: (), + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group( + I, + ui_meta.into_inner().view_bind_group.as_ref().unwrap(), + &[view_uniform.offset], + ); + RenderCommandResult::Success + } +} + +pub struct SetUiMaterialBindGroup(PhantomData); +impl RenderCommand

+ for SetUiMaterialBindGroup +{ + type Param = SRes>; + type ViewWorldQuery = (); + type ItemWorldQuery = Read>; + + fn render<'w>( + _item: &P, + _view: (), + material_handle: ROQueryItem<'_, Self::ItemWorldQuery>, + materials: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let material = materials + .into_inner() + .get(&material_handle.material) + .unwrap(); + pass.set_bind_group(I, &material.bind_group, &[]); + RenderCommandResult::Success + } +} + +pub struct DrawUiMaterialNode(PhantomData); +impl RenderCommand

for DrawUiMaterialNode { + type Param = SRes; + type ViewWorldQuery = (); + type ItemWorldQuery = Read>; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: &'w UiMaterialBatch, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..)); + pass.draw(batch.range.clone(), 0..1); + RenderCommandResult::Success + } +} + +pub struct ExtractedUiMaterialNode { + pub stack_index: usize, + pub transform: Mat4, + pub rect: Rect, + pub border: [f32; 4], + pub material: AssetId, + pub clip: Option, +} + +#[derive(Resource)] +pub struct ExtractedUiMaterialNodes { + pub uinodes: SparseSet>, +} + +impl Default for ExtractedUiMaterialNodes { + fn default() -> Self { + Self { + uinodes: Default::default(), + } + } +} + +pub fn extract_ui_material_nodes( + mut extracted_uinodes: ResMut>, + materials: Extract>>, + ui_stack: Extract>, + uinode_query: Extract< + Query<( + Entity, + &Node, + &Style, + &GlobalTransform, + &Handle, + &ViewVisibility, + Option<&CalculatedClip>, + )>, + >, + windows: Extract>>, + ui_scale: Extract>, +) { + let ui_logical_viewport_size = windows + .get_single() + .map(|window| Vec2::new(window.resolution.width(), window.resolution.height())) + .unwrap_or(Vec2::ZERO) + // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, + // so we have to divide by `UiScale` to get the size of the UI viewport. + / ui_scale.0 as f32; + for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { + if let Ok((entity, uinode, style, transform, handle, view_visibility, clip)) = + uinode_query.get(*entity) + { + // skip invisible nodes + if !view_visibility.get() { + continue; + } + + // Skip loading materials + if !materials.contains(handle) { + continue; + } + + // Both vertical and horizontal percentage border values are calculated based on the width of the parent node + // + let parent_width = uinode.size().x; + let left = + resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size) + / uinode.size().x; + let right = resolve_border_thickness( + style.border.right, + parent_width, + ui_logical_viewport_size, + ) / uinode.size().y; + let top = + resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size) + / uinode.size().y; + let bottom = resolve_border_thickness( + style.border.bottom, + parent_width, + ui_logical_viewport_size, + ) / uinode.size().y; + + extracted_uinodes.uinodes.insert( + entity, + ExtractedUiMaterialNode { + stack_index, + transform: transform.compute_matrix(), + material: handle.id(), + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + border: [left, right, top, bottom], + clip: clip.map(|clip| clip.clip), + }, + ); + }; + } +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_uimaterial_nodes( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut ui_meta: ResMut, + mut extracted_uinodes: ResMut>, + view_uniforms: Res, + ui_material_pipeline: Res>, + mut phases: Query<&mut RenderPhase>, + mut previous_len: Local, +) { + if let Some(view_binding) = view_uniforms.uniforms.binding() { + let mut batches: Vec<(Entity, UiMaterialBatch)> = Vec::with_capacity(*previous_len); + + ui_meta.vertices.clear(); + ui_meta.view_bind_group = Some(render_device.create_bind_group( + "ui_material_view_bind_group", + &ui_material_pipeline.view_layout, + &BindGroupEntries::single(view_binding), + )); + let mut index = 0; + + for mut ui_phase in &mut phases { + let mut batch_item_index = 0; + let mut batch_shader_handle = AssetId::invalid(); + + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + if let Some(extracted_uinode) = extracted_uinodes.uinodes.get(item.entity) { + let mut existing_batch = batches + .last_mut() + .filter(|_| batch_shader_handle == extracted_uinode.material); + + if existing_batch.is_none() { + batch_item_index = item_index; + batch_shader_handle = extracted_uinode.material; + + let new_batch = UiMaterialBatch { + range: index..index, + material: extracted_uinode.material, + }; + + batches.push((item.entity, new_batch)); + + existing_batch = batches.last_mut(); + } + + let uinode_rect = extracted_uinode.rect; + + let rect_size = uinode_rect.size().extend(1.0); + + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + }); + + let positions_diff = if let Some(clip) = extracted_uinode.clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + + let transformed_rect_size = + extracted_uinode.transform.transform_vector3(rect_size); + + // Don't try to cull nodes that have a rotation + // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π + // In those two cases, the culling check can proceed normally as corners will be on + // horizontal / vertical lines + // For all other angles, bypass the culling check + // This does not properly handles all rotations on all axis + if extracted_uinode.transform.x_axis[1] == 0.0 { + // Cull nodes that are completely clipped + if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y + { + continue; + } + } + let uvs = [ + Vec2::new( + uinode_rect.min.x + positions_diff[0].x, + uinode_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[1].x, + uinode_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[2].x, + uinode_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + uinode_rect.min.x + positions_diff[3].x, + uinode_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / uinode_rect.max); + + for i in QUAD_INDICES { + ui_meta.vertices.push(UiMaterialVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + border_widths: extracted_uinode.border, + }); + } + + index += QUAD_INDICES.len() as u32; + existing_batch.unwrap().1.range.end = index; + ui_phase.items[batch_item_index].batch_range_mut().end += 1; + } else { + batch_shader_handle = AssetId::invalid(); + } + } + } + ui_meta.vertices.write_buffer(&render_device, &render_queue); + *previous_len = batches.len(); + commands.insert_or_spawn_batch(batches); + } + extracted_uinodes.uinodes.clear(); +} + +#[derive(Resource, Deref, DerefMut)] +pub struct RenderUiMaterials(HashMap, PreparedUiMaterial>); + +impl Default for RenderUiMaterials { + fn default() -> Self { + Self(Default::default()) + } +} + +pub struct PreparedUiMaterial { + pub bindings: Vec<(u32, OwnedBindingResource)>, + pub bind_group: BindGroup, + pub key: T::Data, +} + +#[derive(Resource)] +pub struct ExtractedUiMaterials { + extracted: Vec<(AssetId, M)>, + removed: Vec>, +} + +impl Default for ExtractedUiMaterials { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + } + } +} + +pub fn extract_ui_materials( + mut commands: Commands, + mut events: Extract>>, + assets: Extract>>, +) { + let mut changed_assets = HashSet::default(); + let mut removed = Vec::new(); + for event in events.read() { + match event { + AssetEvent::Added { id } | AssetEvent::Modified { id } => { + changed_assets.insert(*id); + } + AssetEvent::Removed { id } => { + changed_assets.remove(id); + removed.push(*id); + } + AssetEvent::LoadedWithDependencies { .. } => { + // not implemented + } + } + } + + let mut extracted_assets = Vec::new(); + for id in changed_assets.drain() { + if let Some(asset) = assets.get(id) { + extracted_assets.push((id, asset.clone())); + } + } + + commands.insert_resource(ExtractedUiMaterials { + extracted: extracted_assets, + removed, + }); +} + +pub struct PrepareNextFrameMaterials { + assets: Vec<(AssetId, M)>, +} + +impl Default for PrepareNextFrameMaterials { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +pub fn prepare_ui_materials( + mut prepare_next_frame: Local>, + mut extracted_assets: ResMut>, + mut render_materials: ResMut>, + render_device: Res, + images: Res>, + fallback_image: Res, + pipeline: Res>, +) { + let queued_assets = std::mem::take(&mut prepare_next_frame.assets); + for (id, material) in queued_assets { + match prepare_ui_material( + &material, + &render_device, + &images, + &fallback_image, + &pipeline, + ) { + Ok(prepared_asset) => { + render_materials.insert(id, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((id, material)); + } + } + } + + for removed in std::mem::take(&mut extracted_assets.removed) { + render_materials.remove(&removed); + } + + for (handle, material) in std::mem::take(&mut extracted_assets.extracted) { + match prepare_ui_material( + &material, + &render_device, + &images, + &fallback_image, + &pipeline, + ) { + Ok(prepared_asset) => { + render_materials.insert(handle, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((handle, material)); + } + } + } +} + +fn prepare_ui_material( + material: &M, + render_device: &RenderDevice, + images: &RenderAssets, + fallback_image: &Res, + pipeline: &UiMaterialPipeline, +) -> Result, AsBindGroupError> { + let prepared = + material.as_bind_group(&pipeline.ui_layout, render_device, images, fallback_image)?; + Ok(PreparedUiMaterial { + bindings: prepared.bindings, + bind_group: prepared.bind_group, + key: prepared.data, + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn queue_ui_material_nodes( + extracted_uinodes: Res>, + draw_functions: Res>, + ui_material_pipeline: Res>, + mut pipelines: ResMut>>, + pipeline_cache: Res, + render_materials: Res>, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + let draw_function = draw_functions.read().id::>(); + + for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { + let material = render_materials.get(&extracted_uinode.material).unwrap(); + for (view, mut transparent_phase) in &mut views { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_material_pipeline, + UiMaterialKey { + hdr: view.hdr, + bind_group_data: material.key.clone(), + }, + ); + transparent_phase + .items + .reserve(extracted_uinodes.uinodes.len()); + transparent_phase.add(TransparentUi { + draw_function, + pipeline, + entity: *entity, + sort_key: ( + FloatOrd(extracted_uinode.stack_index as f32), + entity.index(), + ), + batch_range: 0..0, + dynamic_offset: None, + }); + } + } +} diff --git a/crates/bevy_ui/src/render/ui_vertex_output.wgsl b/crates/bevy_ui/src/render/ui_vertex_output.wgsl new file mode 100644 index 00000000000000..de41c52819c64a --- /dev/null +++ b/crates/bevy_ui/src/render/ui_vertex_output.wgsl @@ -0,0 +1,9 @@ +#define_import_path bevy_ui::ui_vertex_output + +// The Vertex output of the default vertex shader for the Ui Material pipeline. +struct UiVertexOutput { + @location(0) uv: vec2, + // The size of the borders in UV space. Order is Left, Right, Top, Bottom. + @location(1) border_widths: vec4, + @builtin(position) position: vec4, +}; diff --git a/crates/bevy_ui/src/ui_material.rs b/crates/bevy_ui/src/ui_material.rs new file mode 100644 index 00000000000000..680d4aa4100e6c --- /dev/null +++ b/crates/bevy_ui/src/ui_material.rs @@ -0,0 +1,145 @@ +use std::hash::Hash; + +use bevy_asset::Asset; +use bevy_render::render_resource::{AsBindGroup, RenderPipelineDescriptor, ShaderRef}; + +/// Materials are used alongside [`UiMaterialPlugin`](crate::UiMaterialPipeline) and [`MaterialNodeBundle`](crate::prelude::MaterialNodeBundle) +/// to spawn entities that are rendered with a specific [`UiMaterial`] type. They serve as an easy to use high level +/// way to render `Node` entities with custom shader logic. +/// +/// `UiMaterials` must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. +/// +/// Materials must also implement [`Asset`] so they can be treated as such. +/// +/// If you are only using the fragment shader, make sure your shader imports the `UiVertexOutput` +/// from `bevy_ui::ui_vertex_output` and uses it as the input of your fragment shader like the +/// example below does. +/// +/// # Example +/// +/// Here is a simple [`UiMaterial`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// check out the [`AsBindGroup`] documentation. +/// ``` +/// # use bevy_ui::prelude::*; +/// # use bevy_ecs::prelude::*; +/// # use bevy_reflect::TypePath; +/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image, color::Color}; +/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// +/// #[derive(AsBindGroup, Asset, TypePath, Debug, Clone)] +/// pub struct CustomMaterial { +/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to +/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. +/// #[uniform(0)] +/// color: Color, +/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just +/// // add the sampler attribute with a different binding index. +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// +/// // All functions on `UiMaterial` have default impls. You only need to implement the +/// // functions that are relevant for your material. +/// impl UiMaterial for CustomMaterial { +/// fn fragment_shader() -> ShaderRef { +/// "shaders/custom_material.wgsl".into() +/// } +/// } +/// +/// // Spawn an entity using `CustomMaterial`. +/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { +/// commands.spawn(MaterialNodeBundle { +/// style: Style { +/// width: Val::Percent(100.0), +/// ..Default::default() +/// }, +/// material: materials.add(CustomMaterial { +/// color: Color::RED, +/// color_texture: asset_server.load("some_image.png"), +/// }), +/// ..Default::default() +/// }); +/// } +/// ``` +/// In WGSL shaders, the material's binding would look like this: +/// +/// If you only use the fragment shader make sure to import `UiVertexOutput` from +/// `bevy_ui::ui_vertex_output` in your wgsl shader. +/// Also note that bind group 0 is always bound to the [`View Uniform`](bevy_render::view::ViewUniform). +/// +/// ```wgsl +/// #import bevy_ui::ui_vertex_output UiVertexOutput +/// +/// struct CustomMaterial { +/// color: vec4, +/// } +/// +/// @group(1) @binding(0) +/// var material: CustomMaterial; +/// @group(1) @binding(1) +/// var color_texture: texture_2d; +/// @group(1) @binding(2) +/// var color_sampler: sampler; +/// +/// @fragment +/// fn fragment(in: UiVertexOutput) -> @location(0) vec4 { +/// +/// } +/// ``` +pub trait UiMaterial: AsBindGroup + Asset + Clone + Sized { + /// Returns this materials vertex shader. If [`ShaderRef::Default`] is returned, the default UI + /// vertex shader will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this materials fragment shader. If [`ShaderRef::Default`] is returned, the default + /// UI fragment shader will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + #[allow(unused_variables)] + #[inline] + fn specialize(descriptor: &mut RenderPipelineDescriptor, key: UiMaterialKey) {} +} + +pub struct UiMaterialKey { + pub hdr: bool, + pub bind_group_data: M::Data, +} + +impl Eq for UiMaterialKey where M::Data: PartialEq {} + +impl PartialEq for UiMaterialKey +where + M::Data: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.hdr == other.hdr && self.bind_group_data == other.bind_group_data + } +} + +impl Clone for UiMaterialKey +where + M::Data: Clone, +{ + fn clone(&self) -> Self { + Self { + hdr: self.hdr, + bind_group_data: self.bind_group_data.clone(), + } + } +} + +impl Hash for UiMaterialKey +where + M::Data: Hash, +{ + fn hash(&self, state: &mut H) { + self.hdr.hash(state); + self.bind_group_data.hash(state); + } +} diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index b77b633dae62bf..7b5c75d38f6d54 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -589,7 +589,7 @@ pub struct WindowResolution { physical_height: u32, /// Code-provided ratio of physical size to logical size. /// - /// Should be used instead `scale_factor` when set. + /// Should be used instead of `scale_factor` when set. scale_factor_override: Option, /// OS-provided ratio of physical size to logical size. /// diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 955f954a6f45fd..91c9858c79293e 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -372,6 +372,7 @@ pub fn winit_runner(mut app: App) { WindowAndInputEventWriters, NonSend, Query<(&mut Window, &mut CachedWindow)>, + NonSend, )> = SystemState::new(&mut app.world); #[cfg(not(target_arch = "wasm32"))] @@ -476,7 +477,7 @@ pub fn winit_runner(mut app: App) { event::Event::WindowEvent { event, window_id, .. } => { - let (mut event_writers, winit_windows, mut windows) = + let (mut event_writers, winit_windows, mut windows, access_kit_adapters) = event_writer_system_state.get_mut(&mut app.world); let Some(window_entity) = winit_windows.get_window_entity(window_id) else { @@ -495,6 +496,18 @@ pub fn winit_runner(mut app: App) { return; }; + // Allow AccessKit to respond to `WindowEvent`s before they reach + // the engine. + if let Some(adapter) = access_kit_adapters.get(&window_entity) { + if let Some(window) = winit_windows.get_window(window_entity) { + // Somewhat surprisingly, this call has meaningful side effects + // See https://github.com/AccessKit/accesskit/issues/300 + // AccessKit might later need to filter events based on this, but we currently do not. + // See https://github.com/bevyengine/bevy/pull/10239#issuecomment-1775572176 + let _ = adapter.on_event(window, &event); + } + } + runner_state.window_event_received = true; match event { @@ -713,20 +726,20 @@ pub fn winit_runner(mut app: App) { event: DeviceEvent::MouseMotion { delta: (x, y) }, .. } => { - let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world); + let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world); event_writers.mouse_motion.send(MouseMotion { delta: Vec2::new(x as f32, y as f32), }); } event::Event::Suspended => { - let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world); + let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world); event_writers.lifetime.send(ApplicationLifetime::Suspended); // Mark the state as `WillSuspend`. This will let the schedule run one last time // before actually suspending to let the application react runner_state.active = ActiveState::WillSuspend; } event::Event::Resumed => { - let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world); + let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world); match runner_state.active { ActiveState::NotYetStarted => { event_writers.lifetime.send(ApplicationLifetime::Started); diff --git a/examples/README.md b/examples/README.md index ed5a095aebd040..db73c982f060c0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -369,6 +369,7 @@ Example | Description [Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI [UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI +[UI Material](../examples/ui/ui_material.rs) | Demonstrates creating and using custom Ui materials [UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI [UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements diff --git a/examples/ui/ui_material.rs b/examples/ui/ui_material.rs new file mode 100644 index 00000000000000..82a2509f836788 --- /dev/null +++ b/examples/ui/ui_material.rs @@ -0,0 +1,65 @@ +//! Demonstrates the use of [`UiMaterials`](UiMaterial) and how to change material values + +use bevy::prelude::*; +use bevy::reflect::TypePath; +use bevy::render::render_resource::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(UiMaterialPlugin::::default()) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn update(time: Res