diff --git a/Cargo.lock b/Cargo.lock index 3878820db..5af9b5575 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,7 +893,28 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ec4962977a746d870170532fc92759e04d3dbcae8b7b82e7ca3bb83b1d75277" dependencies = [ +<<<<<<< HEAD "glam 0.24.2", +||||||| parent of 1b1dc0b (Bevy render PoC.) + "glam 0.25.0", +======= + "glam 0.24.2", +] + +[[package]] +name = "bevy_mod_debugdump" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db8601f41ea570b7d32f3177292a608196c59bdf3298001a9e202d5e7439438" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_render", + "bevy_utils", + "once_cell", + "petgraph 0.6.4", + "pretty-type-name", +>>>>>>> 1b1dc0b (Bevy render PoC.) ] [[package]] @@ -917,6 +938,12 @@ name = "bevy_nannou_render" version = "0.1.0" dependencies = [ "bevy", + "bevy_mod_debugdump", + "bevy_nannou_wgpu", + "lyon", + "nannou_core", + "nannou_mesh", + "rusttype", ] [[package]] @@ -924,6 +951,7 @@ name = "bevy_nannou_wgpu" version = "0.1.0" dependencies = [ "bevy", + "wgpu-types", ] [[package]] @@ -5354,6 +5382,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] +<<<<<<< HEAD +||||||| parent of 1b1dc0b (Bevy render PoC.) +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +======= +name = "pretty-type-name" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f73cdaf19b52e6143685c3606206e114a4dfa969d6b14ec3894c88eb38bd4b" + +[[package]] +>>>>>>> 1b1dc0b (Bevy render PoC.) name = "proc-macro-crate" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/bevy_nannou/src/lib.rs b/bevy_nannou/src/lib.rs index 1d0232049..20934fae5 100644 --- a/bevy_nannou/src/lib.rs +++ b/bevy_nannou/src/lib.rs @@ -1,16 +1,42 @@ use bevy::prelude::*; +use bevy_nannou_render::mesh::{Vertex, ViewMesh}; -struct NannouPlugin; +pub struct NannouPlugin; impl Plugin for NannouPlugin { fn build(&self, app: &mut App) { - app.add_plugins(( + app.add_systems(Startup, setup).add_plugins(( bevy_nannou_render::NannouRenderPlugin, bevy_nannou_draw::NannouDrawPlugin, )); } } +fn setup(mut commands: Commands) { + let mut mesh = ViewMesh::default(); + mesh.extend_from_slices( + &[ + Vec3::new(-512.0, -384.0, 0.0), + Vec3::new(-512.0, 384.0, 0.0), + Vec3::new(512.0, 384.0, 0.0), + ], + &[0, 1, 2], + &[Vec4::new(1.0, 0.0, 0.0, 1.0); 3], + &[Vec2::new(0.0, 0.0); 3], + ); + commands.spawn(( + Camera3dBundle { + transform: Transform::from_xyz(0.0, 0.0, -10.0).looking_at(Vec3::ZERO, Vec3::Z), + projection: OrthographicProjection { + ..Default::default() + } + .into(), + ..Default::default() + }, + mesh, + )); +} + #[cfg(test)] mod tests { use bevy::app::App; @@ -18,7 +44,7 @@ mod tests { #[test] fn it_works() { let mut app = App::new(); - app.add_plugins(super::NannouPlugin); + app.add_plugins((bevy::DefaultPlugins, super::NannouPlugin)); app.update(); } } diff --git a/bevy_nannou/src/main.rs b/bevy_nannou/src/main.rs new file mode 100644 index 000000000..bce36c75b --- /dev/null +++ b/bevy_nannou/src/main.rs @@ -0,0 +1,7 @@ +use bevy_nannou::NannouPlugin; + +pub fn main() { + let mut app = bevy::app::App::new(); + app.add_plugins((bevy::DefaultPlugins, NannouPlugin)); + app.run(); +} \ No newline at end of file diff --git a/bevy_nannou_render/Cargo.toml b/bevy_nannou_render/Cargo.toml index ddc52fbc0..17d95fdb7 100644 --- a/bevy_nannou_render/Cargo.toml +++ b/bevy_nannou_render/Cargo.toml @@ -5,3 +5,9 @@ edition = "2021" [dependencies] bevy = { workspace = true } +bevy_nannou_wgpu = { path = "../bevy_nannou_wgpu" } +nannou_core = { path = "../nannou_core" } +nannou_mesh = { path = "../nannou_mesh" } +rusttype = { version = "0.8", features = ["gpu_cache"] } +lyon = "0.17" +bevy_mod_debugdump = "0.9.0" \ No newline at end of file diff --git a/bevy_nannou_render/src/lib.rs b/bevy_nannou_render/src/lib.rs index bf04d53a1..949f5c7b6 100644 --- a/bevy_nannou_render/src/lib.rs +++ b/bevy_nannou_render/src/lib.rs @@ -1,7 +1,130 @@ +use bevy::asset::load_internal_asset; +use bevy::core_pipeline::core_3d; +use bevy::core_pipeline::core_3d::CORE_3D; use bevy::prelude::*; +use bevy::render::{Extract, render_resource as wgpu, RenderSet}; +use bevy::render::{Render, RenderApp}; +use bevy::render::extract_component::ExtractComponentPlugin; +use bevy::render::render_asset::RenderAsset; +use bevy::render::render_graph::{RenderGraphApp, ViewNode, ViewNodeRunner}; +use bevy::render::render_phase::{ + AddRenderCommand, PhaseItem, RenderCommand, +}; +use bevy::render::render_resource::{ + ShaderType, SpecializedRenderPipeline, + SpecializedRenderPipelines, +}; +use bevy::render::renderer::RenderDevice; +use bevy::render::view::ViewUniforms; + +use mesh::ViewMesh; +use pipeline::queue_pipelines; + +use crate::pipeline::{NannouPipeline, NannouViewNode}; + +pub mod mesh; +mod pipeline; +mod text; +// mod reshaper; pub struct NannouRenderPlugin; +pub const NANNOU_SHADER_HANDLE: Handle = Handle::weak_from_u128(43700360588854283521); + impl Plugin for NannouRenderPlugin { - fn build(&self, _app: &mut App) {} + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + NANNOU_SHADER_HANDLE, + "shaders/nannou.wgsl", + Shader::from_wgsl + ); + + app + .add_plugins(ExtractComponentPlugin::::default()); + + + println!("NannouRenderPlugin::build"); + app.get_sub_app_mut(RenderApp) + .unwrap() + .init_resource::>() + .add_systems( + Render, + prepare_view_uniform.in_set(RenderSet::PrepareBindGroups), + ) + .add_systems( + Render, + queue_pipelines + .in_set(RenderSet::PrepareAssets) + ) + .add_render_graph_node::>( + core_3d::graph::NAME, + NannouViewNode::NAME, + ) + .add_render_graph_edges( + CORE_3D, + &[ + core_3d::graph::node::MAIN_TRANSPARENT_PASS, + NannouViewNode::NAME, + core_3d::graph::node::END_MAIN_PASS, + ], + ); + + bevy_mod_debugdump::print_render_graph(app); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + } +} + +fn prepare_view_uniform( + mut commands: Commands, + render_device: Res, + pipline: Res, + view_uniforms: Res, +) { + if let Some(binding) = view_uniforms.uniforms.binding() { + commands.insert_resource(ViewUniformBindGroup::new( + &render_device, + &pipline.view_bind_group_layout, + binding, + )); + } +} +#[derive(Resource)] +struct ViewUniformBindGroup { + bind_group: wgpu::BindGroup, +} + +impl ViewUniformBindGroup { + fn new( + device: &RenderDevice, + layout: &wgpu::BindGroupLayout, + binding: wgpu::BindingResource, + ) -> ViewUniformBindGroup { + let bind_group = bevy_nannou_wgpu::BindGroupBuilder::new() + .binding(binding) + .build(device, layout); + + ViewUniformBindGroup { bind_group } + } +} + +/// A top-level indicator of whether or not +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[repr(u32)] +pub enum VertexMode { + /// Use the color values and ignore the texture coordinates. + Color = 0, + /// Use the texture color and ignore the color values. + Texture = 1, + /// A special mode used by the text primitive. + /// + /// Uses the color values, but multiplies the alpha by the glyph cache texture's red value. + Text = 2, } diff --git a/bevy_nannou_render/src/mesh/builder.rs b/bevy_nannou_render/src/mesh/builder.rs new file mode 100644 index 000000000..22e80d51b --- /dev/null +++ b/bevy_nannou_render/src/mesh/builder.rs @@ -0,0 +1,233 @@ +//! Implementations of the lyon geometry builder traits for the `Mesh`. +//! +//! The aim is to allow for a tessellator to efficiently extend a mesh without using an +//! intermediary buffer. +//! +//! Lyon tessellators assume `f32` data, so we do the same in the following implementations. + +// use lyon::tessellation::{FillVertex, GeometryBuilderError, StrokeVertex, VertexId}; +use lyon::tessellation::geometry_builder::{ + self, FillGeometryBuilder, GeometryBuilder, StrokeGeometryBuilder, +}; +use lyon::tessellation::{FillVertex, GeometryBuilderError, StrokeVertex, VertexId}; + +use nannou_mesh::{Indices, Points, PushIndex, PushVertex}; +use bevy::prelude::*; + +pub struct MeshBuilder<'a, A> { + /// The mesh that is to be extended. + mesh: &'a mut crate::ViewMesh, + /// The number of vertices in the mesh when begin was called. + begin_vertex_count: u32, + /// The number of indices in the mesh when begin was called. + begin_index_count: u32, + /// Transform matrix that also integrates position and orientation here. + transform: Mat4, + /// The way in which vertex attributes should be sourced. + attributes: A, +} + +pub struct SingleColor(crate::mesh::vertex::Color); +pub struct ColorPerPoint; +pub struct TexCoordsPerPoint; + +impl<'a, A> MeshBuilder<'a, A> { + /// Begin extending the mesh. + fn new(mesh: &'a mut crate::ViewMesh, transform: Mat4, attributes: A) -> Self { + MeshBuilder { + mesh, + begin_vertex_count: 0, + begin_index_count: 0, + transform, + attributes, + } + } +} + +impl<'a> MeshBuilder<'a, SingleColor> { + /// Begin extending a mesh rendered with a single colour. + pub fn single_color( + mesh: &'a mut crate::ViewMesh, + transform: Mat4, + color: crate::mesh::vertex::Color, + ) -> Self { + Self::new(mesh, transform, SingleColor(color)) + } +} + +impl<'a> MeshBuilder<'a, ColorPerPoint> { + /// Begin extending a mesh where the path interpolates a unique color per point. + pub fn color_per_point(mesh: &'a mut crate::ViewMesh, transform: Mat4) -> Self { + Self::new(mesh, transform, ColorPerPoint) + } +} + +impl<'a> MeshBuilder<'a, TexCoordsPerPoint> { + /// Begin extending a mesh where the path interpolates a unique texture coordinates per point. + pub fn tex_coords_per_point(mesh: &'a mut crate::ViewMesh, transform: Mat4) -> Self { + Self::new(mesh, transform, TexCoordsPerPoint) + } +} + +impl<'a, A> GeometryBuilder for MeshBuilder<'a, A> { + fn begin_geometry(&mut self) { + self.begin_vertex_count = self.mesh.points().len() as u32; + self.begin_index_count = self.mesh.indices().len() as u32; + } + + fn end_geometry(&mut self) -> geometry_builder::Count { + geometry_builder::Count { + vertices: self.mesh.points().len() as u32 - self.begin_vertex_count, + indices: self.mesh.indices().len() as u32 - self.begin_index_count, + } + } + + fn add_triangle(&mut self, a: VertexId, b: VertexId, c: VertexId) { + self.mesh.push_index(a.to_usize() as u32); + self.mesh.push_index(b.to_usize() as u32); + self.mesh.push_index(c.to_usize() as u32); + } + + fn abort_geometry(&mut self) { + unimplemented!(); + } +} + +impl<'a> FillGeometryBuilder for MeshBuilder<'a, SingleColor> { + fn add_fill_vertex(&mut self, vertex: FillVertex) -> Result { + // Retrieve the index. + let id = VertexId::from_usize(self.mesh.points().len()); + + let position = vertex.position(); + + // Construct and insert the point + let p = Vec2::new(position.x, position.y).extend(0.0); + let point = self.transform.transform_point3(p); + let SingleColor(color) = self.attributes; + let tex_coords = crate::mesh::vertex::default_tex_coords(); + let vertex = crate::mesh::vertex::new(point, color, tex_coords); + self.mesh.push_vertex(vertex); + + // Return the index. + Ok(id) + } +} + +impl<'a> StrokeGeometryBuilder for MeshBuilder<'a, SingleColor> { + fn add_stroke_vertex( + &mut self, + vertex: StrokeVertex, + ) -> Result { + // Retrieve the index. + let id = VertexId::from_usize(self.mesh.points().len()); + + let position = vertex.position(); + + // Construct and insert the point + let p = Vec2::new(position.x, position.y).extend(0.0); + let point = self.transform.transform_point3(p); + let SingleColor(color) = self.attributes; + let tex_coords = crate::mesh::vertex::default_tex_coords(); + let vertex = crate::mesh::vertex::new(point, color, tex_coords); + self.mesh.push_vertex(vertex); + + // Return the index. + Ok(id) + } +} + +impl<'a> FillGeometryBuilder for MeshBuilder<'a, ColorPerPoint> { + fn add_fill_vertex( + &mut self, + mut vertex: FillVertex, + ) -> Result { + // Retrieve the index. + let id = VertexId::from_usize(self.mesh.points().len()); + + let position = vertex.position(); + + // Construct and insert the point + let p = Vec2::new(position.x, position.y).extend(0.0); + let point = self.transform.transform_point3(p); + let col = vertex.interpolated_attributes(); + let color: crate::mesh::vertex::Color = (col[0], col[1], col[2], col[3]).into(); + let tex_coords = crate::mesh::vertex::default_tex_coords(); + let vertex = crate::mesh::vertex::new(point, color, tex_coords); + self.mesh.push_vertex(vertex); + + // Return the index. + Ok(id) + } +} + +impl<'a> StrokeGeometryBuilder for MeshBuilder<'a, ColorPerPoint> { + fn add_stroke_vertex( + &mut self, + mut vertex: StrokeVertex, + ) -> Result { + // Retrieve the index. + let id = VertexId::from_usize(self.mesh.points().len()); + + let position = vertex.position(); + + // Construct and insert the point + let p = Vec2::new(position.x, position.y).extend(0.0); + let point = self.transform.transform_point3(p); + let col = vertex.interpolated_attributes(); + let color: crate::mesh::vertex::Color = (col[0], col[1], col[2], col[3]).into(); + let tex_coords = crate::mesh::vertex::default_tex_coords(); + let vertex = crate::mesh::vertex::new(point, color, tex_coords); + self.mesh.push_vertex(vertex); + + // Return the index. + Ok(id) + } +} + +impl<'a> FillGeometryBuilder for MeshBuilder<'a, TexCoordsPerPoint> { + fn add_fill_vertex( + &mut self, + mut vertex: FillVertex, + ) -> Result { + // Retrieve the index. + let id = VertexId::from_usize(self.mesh.points().len()); + + let position = vertex.position(); + + // Construct and insert the point + let p = Vec2::new(position.x, position.y).extend(0.0); + let point = self.transform.transform_point3(p); + let tc = vertex.interpolated_attributes(); + let tex_coords: crate::mesh::vertex::TexCoords = (tc[0], tc[1]).into(); + let color = crate::mesh::vertex::DEFAULT_VERTEX_COLOR; + let vertex = crate::mesh::vertex::new(point, color, tex_coords); + self.mesh.push_vertex(vertex); + + // Return the index. + Ok(id) + } +} + +impl<'a> StrokeGeometryBuilder for MeshBuilder<'a, TexCoordsPerPoint> { + fn add_stroke_vertex( + &mut self, + mut vertex: StrokeVertex, + ) -> Result { + // Retrieve the index. + let id = VertexId::from_usize(self.mesh.points().len()); + + let position = vertex.position(); + + // Construct and insert the point + let p = Vec2::new(position.x, position.y).extend(0.0); + let point = self.transform.transform_point3(p); + let tc = vertex.interpolated_attributes(); + let tex_coords: crate::mesh::vertex::TexCoords = (tc[0], tc[1]).into(); + let color = crate::mesh::vertex::DEFAULT_VERTEX_COLOR; + let vertex = crate::mesh::vertex::new(point, color, tex_coords); + self.mesh.push_vertex(vertex); + + // Return the index. + Ok(id) + } +} diff --git a/bevy_nannou_render/src/mesh/mod.rs b/bevy_nannou_render/src/mesh/mod.rs new file mode 100644 index 000000000..f526414dd --- /dev/null +++ b/bevy_nannou_render/src/mesh/mod.rs @@ -0,0 +1,283 @@ +//! Items related to the custom mesh type used by the `Draw` API. + +use bevy::prelude::*; +use nannou_mesh as mesh; +use nannou_mesh::{self, MeshPoints, WithColors, WithIndices, WithTexCoords}; +use std::ops::{Deref, DerefMut}; +use bevy::render::extract_component::ExtractComponent; +use crate::mesh::vertex::Point; + +pub mod builder; +pub mod vertex; + +pub use self::builder::MeshBuilder; +pub use self::vertex::Vertex; + +pub type Points = Vec; +pub type Indices = Vec; +pub type Colors = Vec; +pub type TexCoords = Vec; + +/// The inner mesh type used by the **draw::Mesh**. +pub type MeshType = + WithTexCoords, Indices>, Colors>, TexCoords>; + +/// The custom mesh type used internally by the **Draw** API. + +#[derive(Component, Debug, Clone, ExtractComponent)] +pub struct ViewMesh { + mesh: MeshType, +} + +impl ViewMesh { + /// The number of raw vertices contained within the mesh. + pub fn raw_vertex_count(&self) -> usize { + mesh::raw_vertex_count(self) + } + + /// The number of vertices that would be yielded by a **Vertices** iterator for the given mesh. + pub fn vertex_count(&self) -> usize { + mesh::vertex_count(self) + } + + /// The number of triangles that would be yielded by a **Triangles** iterator for the given mesh. + pub fn triangle_count(&self) -> usize { + mesh::triangle_count(self) + } + + /// The **Mesh**'s vertex position channel. + pub fn points(&self) -> &[vertex::Point] { + mesh::Points::points(self) + } + + /// The **Mesh**'s vertex indices channel. + pub fn indices(&self) -> &[u32] { + mesh::Indices::indices(self) + } + + /// The **Mesh**'s vertex colors channel. + pub fn colors(&self) -> &[vertex::Color] { + mesh::Colors::colors(self) + } + + /// The **Mesh**'s vertex texture coordinates channel. + pub fn tex_coords(&self) -> &[vertex::TexCoords] { + mesh::TexCoords::tex_coords(self) + } + + /// Push the given vertex onto the inner channels. + pub fn push_vertex(&mut self, v: Vertex) { + mesh::push_vertex(self, v); + } + + /// Push the given index onto the inner **Indices** channel. + pub fn push_index(&mut self, i: u32) { + mesh::push_index(self, i); + } + + /// Extend the mesh channels with the given vertices. + pub fn extend_vertices(&mut self, vs: I) + where + I: IntoIterator, + { + mesh::extend_vertices(self, vs); + } + + /// Extend the **Mesh** indices channel with the given indices. + pub fn extend_indices(&mut self, is: I) + where + I: IntoIterator, + { + mesh::extend_indices(self, is); + } + + /// Extend the **Mesh** with the given vertices and indices. + pub fn extend(&mut self, vs: V, is: I) + where + V: IntoIterator, + I: IntoIterator, + { + self.extend_vertices(vs); + self.extend_indices(is); + } + + /// Clear all vertices from the mesh. + pub fn clear_vertices(&mut self) { + mesh::clear_vertices(self); + } + + /// Clear all indices from the mesh. + pub fn clear_indices(&mut self) { + mesh::clear_indices(self); + } + + /// Clear all vertices and indices from the mesh. + pub fn clear(&mut self) { + mesh::clear(self); + } + + /// Produce an iterator yielding all raw (non-index-order) vertices. + pub fn raw_vertices(&self) -> mesh::RawVertices<&Self> { + mesh::raw_vertices(self) + } + + /// Consume self and produce an iterator yielding all raw (non-index_order) vertices. + pub fn into_raw_vertices(self) -> mesh::RawVertices { + mesh::raw_vertices(self) + } + + /// Extend the mesh from the given slices. + /// + /// This is faster than `extend` which uses iteration internally. + /// + /// **Panic!**s if the length of the given points, colors and tex_coords slices do not match. + pub fn extend_from_slices( + &mut self, + points: &[vertex::Point], + indices: &[u32], + colors: &[vertex::Color], + tex_coords: &[vertex::TexCoords], + ) { + assert_eq!(points.len(), colors.len()); + assert_eq!(points.len(), tex_coords.len()); + let slices = (tex_coords, (colors, (indices, points))); + mesh::ExtendFromSlice::extend_from_slice(&mut self.mesh, slices); + } + + /// Extend the mesh with the given slices of vertices. + pub fn extend_vertices_from_slices( + &mut self, + points: &[vertex::Point], + colors: &[vertex::Color], + tex_coords: &[vertex::TexCoords], + ) { + self.extend_from_slices(points, &[], colors, tex_coords); + } + + /// Extend the mesh with the given slices of vertices. + pub fn extend_indices_from_slice(&mut self, indices: &[u32]) { + self.extend_from_slices(&[], indices, &[], &[]); + } + + /// Produce an iterator yielding all vertices in the order specified via the vertex indices. + pub fn vertices(&self) -> mesh::Vertices<&Self> { + mesh::vertices(self) + } + + /// Produce an iterator yielding all triangles. + pub fn triangles(&self) -> mesh::Triangles<&Self> { + mesh::triangles(self) + } + + /// Consume self and produce an iterator yielding all vertices in index-order. + pub fn into_vertices(self) -> mesh::Vertices { + mesh::vertices(self) + } + + /// Consume self and produce an iterator yielding all triangles. + pub fn into_triangles(self) -> mesh::Triangles { + mesh::triangles(self) + } +} + +impl Default for ViewMesh { + fn default() -> Self { + let mesh = Default::default(); + ViewMesh { mesh } + } +} + +impl Deref for ViewMesh { + type Target = MeshType; + fn deref(&self) -> &Self::Target { + &self.mesh + } +} + +impl DerefMut for ViewMesh { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.mesh + } +} + +impl mesh::GetVertex for ViewMesh { + type Vertex = Vertex; + fn get_vertex(&self, index: u32) -> Option { + mesh::WithTexCoords::get_vertex(&self.mesh, index) + } +} + +impl mesh::Points for ViewMesh { + type Point = vertex::Point; + type Points = Points; + fn points(&self) -> &Self::Points { + self.mesh.points() + } +} + +impl mesh::Indices for ViewMesh { + type Index = u32; + type Indices = Indices; + fn indices(&self) -> &Self::Indices { + self.mesh.indices() + } +} + +impl mesh::Colors for ViewMesh { + type Color = vertex::Color; + type Colors = Colors; + fn colors(&self) -> &Self::Colors { + self.mesh.colors() + } +} + +impl mesh::TexCoords for ViewMesh { + type TexCoord = Vec2; + type TexCoords = TexCoords; + fn tex_coords(&self) -> &Self::TexCoords { + self.mesh.tex_coords() + } +} + +impl mesh::PushVertex for ViewMesh { + fn push_vertex(&mut self, v: Vertex) { + self.mesh.push_vertex(v); + } +} + +impl mesh::PushIndex for ViewMesh { + type Index = u32; + + fn push_index(&mut self, index: Self::Index) { + self.mesh.push_index(index); + } + + fn extend_indices(&mut self, indices: I) + where + I: IntoIterator, + { + self.mesh.extend_indices(indices); + } +} + +impl mesh::ClearIndices for ViewMesh { + fn clear_indices(&mut self) { + self.mesh.clear_indices(); + } +} + +impl mesh::ClearVertices for ViewMesh { + fn clear_vertices(&mut self) { + self.mesh.clear_vertices(); + } +} + +#[test] +fn test_method_access() { + let mesh: ViewMesh = Default::default(); + assert_eq!(None, mesh::GetVertex::get_vertex(&mesh, 0)); + mesh::Points::points(&mesh); + mesh::Indices::indices(&mesh); + mesh::Colors::colors(&mesh); + mesh::TexCoords::tex_coords(&mesh); +} diff --git a/bevy_nannou_render/src/mesh/vertex.rs b/bevy_nannou_render/src/mesh/vertex.rs new file mode 100644 index 000000000..b10b072a2 --- /dev/null +++ b/bevy_nannou_render/src/mesh/vertex.rs @@ -0,0 +1,168 @@ +use nannou_mesh::vertex::{WithColor, WithTexCoords}; +use bevy::prelude::*; + +pub type Point = Vec3; +pub type Color = Vec4; +pub type TexCoords = Vec2; +pub type Normal = Vec3; +pub type ColoredPoint = WithColor; +pub type ColoredVec2 = WithColor; + +/// The vertex type produced by the **draw::Mesh**'s inner **MeshType**. +pub type Vertex = WithTexCoords, TexCoords>; + +/// The number of channels in the color type. +pub const COLOR_CHANNEL_COUNT: usize = 4; + +pub const DEFAULT_VERTEX_COLOR: Color = Vec4::new(1.0, 1.0, 1.0, 1.0); + +/// Simplified constructor for a **draw::mesh::Vertex**. +pub fn new(point: Point, color: Color, tex_coords: TexCoords) -> Vertex { + WithTexCoords { + tex_coords, + vertex: WithColor { + color, + vertex: point, + }, + } +} + +/// Default texture coordinates, for the case where a type is not textured. +pub fn default_tex_coords() -> TexCoords { + [0.0; 2].into() +} + +/// A type that converts an iterator yielding colored points to an iterator yielding **Vertex**s. +/// +/// Default values are used for tex_coords. +#[derive(Clone, Debug)] +pub struct IterFromColoredPoints { + colored_points: I, +} + +impl IterFromColoredPoints { + /// Produce an iterator that converts an iterator yielding colored points to an iterator + /// yielding **Vertex**s. + /// + /// The default value of `(0.0, 0.0)` is used for tex_coords. + pub fn new

(colored_points: P) -> Self + where + P: IntoIterator>, + I: Iterator>, + { + let colored_points = colored_points.into_iter(); + IterFromColoredPoints { colored_points } + } +} + +impl Iterator for IterFromColoredPoints +where + I: Iterator>, +{ + type Item = Vertex; + fn next(&mut self) -> Option { + self.colored_points.next().map(|vertex| { + let tex_coords = default_tex_coords(); + let vertex = WithTexCoords { tex_coords, vertex }; + vertex + }) + } +} + +/// A type that converts an iterator yielding points to an iterator yielding **Vertex**s. +/// +/// The given `default_color` is used to color every vertex. +/// +/// The default value of `(0.0, 0.0)` is used for tex_coords. +#[derive(Clone, Debug)] +pub struct IterFromPoints { + points: I, + default_color: Color, +} + +/// A type that converts an iterator yielding 2D points to an iterator yielding **Vertex**s. +/// +/// The `z` position for each vertex will be `0.0`. +/// +/// The given `default_color` is used to color every vertex. +/// +/// The default value of `(0.0, 0.0)` is used for tex_coords. +#[derive(Clone, Debug)] +pub struct IterFromVec2s { + points: I, + default_color: Color, +} + +impl IterFromPoints { + /// Produce an iterator that converts an iterator yielding points to an iterator yielding + /// **Vertex**s. + /// + /// The given `default_color` is used to color every vertex. + /// + /// The default value of `(0.0, 0.0)` is used for tex_coords. + pub fn new

(points: P, default_color: Color) -> Self + where + P: IntoIterator, + I: Iterator, + { + let points = points.into_iter(); + IterFromPoints { + points, + default_color, + } + } +} + +impl IterFromVec2s { + /// A type that converts an iterator yielding 2D points to an iterator yielding **Vertex**s. + /// + /// The `z` position for each vertex will be `0.0`. + /// + /// The given `default_color` is used to color every vertex. + /// + /// The default value of `(0.0, 0.0)` is used for tex_coords. + pub fn new

(points: P, default_color: Color) -> Self + where + P: IntoIterator, + I: Iterator, + { + let points = points.into_iter(); + IterFromVec2s { + points, + default_color, + } + } +} + +impl Iterator for IterFromPoints +where + I: Iterator, +{ + type Item = Vertex; + fn next(&mut self) -> Option { + self.points.next().map(|vertex| { + let color = self.default_color; + let vertex = WithColor { vertex, color }; + let tex_coords = default_tex_coords(); + let vertex = WithTexCoords { vertex, tex_coords }; + vertex + }) + } +} + +impl Iterator for IterFromVec2s +where + I: Iterator, +{ + type Item = Vertex; + fn next(&mut self) -> Option { + self.points.next().map(|p| { + let vertex = p.extend(0.0); + let color = self.default_color; + let vertex = WithColor { vertex, color }; + let tex_coords = default_tex_coords(); + let vertex = WithTexCoords { vertex, tex_coords }; + vertex + }) + } +} diff --git a/bevy_nannou_render/src/pipeline.rs b/bevy_nannou_render/src/pipeline.rs new file mode 100644 index 000000000..8617bef70 --- /dev/null +++ b/bevy_nannou_render/src/pipeline.rs @@ -0,0 +1,457 @@ +use bevy::core::cast_slice; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; + +use bevy::ecs::query::QueryItem; +use bevy::prelude::*; +use bevy::render::camera::ExtractedCamera; +use bevy::render::render_graph::{NodeRunError, RenderGraphContext, ViewNode}; +use bevy::render::render_resource as wgpu; +use bevy::render::render_resource::{ + BufferInitDescriptor, CachedRenderPipelineId, PipelineCache, RenderPipelineDescriptor, + SpecializedRenderPipeline, SpecializedRenderPipelines, +}; +use bevy::render::renderer::{RenderContext, RenderDevice}; +use bevy::render::view::{ + ExtractedView, ExtractedWindow, ExtractedWindows, ViewDepthTexture, ViewTarget, ViewUniform, +}; +use bevy::utils; + +use crate::mesh::vertex::Point; +use crate::mesh::{TexCoords, ViewMesh}; +use crate::{text, VertexMode, ViewUniformBindGroup, NANNOU_SHADER_HANDLE}; + +pub struct GlyphCache { + /// Tracks glyphs and their location within the cache. + pub cache: text::GlyphCache<'static>, + /// The buffer used to store the pixels of the glyphs. + pub pixel_buffer: Vec, + /// Will be set to `true` after the cache has been updated if the texture requires re-uploading. + pub requires_upload: bool, +} + +impl GlyphCache { + fn new(size: [u32; 2], scale_tolerance: f32, position_tolerance: f32) -> Self { + let [w, h] = size; + let cache = text::GlyphCache::builder() + .dimensions(w, h) + .scale_tolerance(scale_tolerance) + .position_tolerance(position_tolerance) + .build() + .into(); + let pixel_buffer = vec![0u8; w as usize * h as usize]; + let requires_upload = false; + GlyphCache { + cache, + pixel_buffer, + requires_upload, + } + } +} + +#[derive(Resource)] +pub struct NannouPipeline { + glyph_cache: GlyphCache, + glyph_cache_texture: wgpu::Texture, + text_bind_group_layout: wgpu::BindGroupLayout, + text_bind_group: wgpu::BindGroup, + texture_samplers: HashMap, + texture_bind_group_layout: wgpu::BindGroupLayout, + texture_bind_group: wgpu::BindGroup, + output_color_format: wgpu::TextureFormat, + view_bind_group_layout: wgpu::BindGroupLayout, + view_bind_group: wgpu::BindGroup, + view_buffer: wgpu::Buffer, +} + +#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)] +pub struct NannouPipelineKey { + pub sample_count: u32, + pub depth_format: wgpu::TextureFormat, + pub blend_state: wgpu::BlendState, + pub topology: wgpu::PrimitiveTopology, +} + +impl NannouPipeline { + /// The default sample count + pub const DEFAULT_SAMPLE_COUNT: u32 = 1; + /// The default depth format + pub const DEFAULT_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + pub const DEFAULT_COLOR_BLEND: wgpu::BlendComponent = wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }; + pub const DEFAULT_ALPHA_BLEND: wgpu::BlendComponent = wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }; + /// The default color blend state + pub const DEFAULT_BLEND_STATE: wgpu::BlendState = wgpu::BlendState { + color: Self::DEFAULT_COLOR_BLEND, + alpha: Self::DEFAULT_ALPHA_BLEND, + }; + /// The default primitive topology + pub const DEFAULT_PRIMITIVE_TOPOLOGY: wgpu::PrimitiveTopology = + wgpu::PrimitiveTopology::TriangleList; + pub const GLYPH_CACHE_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm; + + fn render_pipeline( + &self, + color_format: wgpu::TextureFormat, + depth_format: wgpu::TextureFormat, + sample_count: u32, + blend_state: wgpu::BlendState, + topology: wgpu::PrimitiveTopology, + ) -> RenderPipelineDescriptor { + bevy_nannou_wgpu::RenderPipelineBuilder::from_layout( + &[ + self.view_bind_group_layout.clone(), + self.text_bind_group_layout.clone(), + self.texture_bind_group_layout.clone(), + ], + NANNOU_SHADER_HANDLE, + ) + .vertex_entry_point("vertex") + .fragment_shader(NANNOU_SHADER_HANDLE) + .fragment_entry_point("fragment") + .color_format(color_format) + .add_vertex_buffer::(&[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }]) + .add_vertex_buffer::(&[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x3, + offset: 0, + shader_location: 1, + }]) + .add_vertex_buffer::(&[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x3, + offset: 0, + shader_location: 2, + }]) + .add_vertex_buffer::(&[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Uint32, + offset: 0, + shader_location: 3, + }]) + // .depth_format(depth_format) + .sample_count(sample_count) + .color_blend(blend_state.color) + .alpha_blend(blend_state.alpha) + .primitive_topology(topology) + .build() + } +} + +impl SpecializedRenderPipeline for NannouPipeline { + type Key = NannouPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + self.render_pipeline( + self.output_color_format, + key.depth_format, + key.sample_count, + key.blend_state, + key.topology, + ) + } +} + +impl FromWorld for NannouPipeline { + fn from_world(render_world: &mut World) -> Self { + let device = render_world.get_resource::().unwrap(); + + // Create the glyph cache texture. + let text_sampler_desc = bevy_nannou_wgpu::SamplerBuilder::new().into_descriptor(); + let text_sampler_filtering = bevy_nannou_wgpu::sampler_filtering(&text_sampler_desc); + let text_sampler = device.create_sampler(&text_sampler_desc); + let glyph_cache_texture = bevy_nannou_wgpu::TextureBuilder::new() + .size([1024; 2]) + .usage(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST) + .format(Self::GLYPH_CACHE_TEXTURE_FORMAT) + .build(device); + let glyph_cache_texture_view = + glyph_cache_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let glyph_cache = GlyphCache::new([1024; 2], 0.1, 0.1); + + // The default texture for the case where the user has not specified one. + let default_texture = bevy_nannou_wgpu::TextureBuilder::new() + .size([64; 2]) + .usage(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST) + .build(device); + + // Bind group for text. + let text_bind_group_layout = create_text_bind_group_layout(device, text_sampler_filtering); + let text_bind_group = create_text_bind_group( + device, + &text_bind_group_layout, + &text_sampler, + &glyph_cache_texture_view, + ); + + // Initialise the sampler set with the default sampler. + let sampler_desc = bevy_nannou_wgpu::SamplerBuilder::new().into_descriptor(); + let sampler_id = sampler_descriptor_hash(&sampler_desc); + let texture_sampler = device.create_sampler(&sampler_desc); + let texture_samplers = Some((sampler_id, texture_sampler.clone())) + .into_iter() + .collect(); + + // Bind group per user-uploaded texture. + // let texture_bind_group_layouts = Default::default(); + // let texture_bind_groups = Default::default(); + + let texture_bind_group_layout = create_texture_bind_group_layout( + device, + bevy_nannou_wgpu::sampler_filtering(&sampler_desc), + wgpu::TextureSampleType::Float { filterable: true }, + ); + let texture_bind_group = create_texture_bind_group( + device, + &texture_bind_group_layout, + &texture_sampler, + &default_texture.create_view(&wgpu::TextureViewDescriptor::default()), + ); + + // Create the view bind group. + let usage = wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST; + let view_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("nannou Renderer uniform_buffer"), + usage, + size: std::mem::size_of::() as u64, + mapped_at_creation: false, + }); + let view_bind_group_layout = bevy_nannou_wgpu::BindGroupLayoutBuilder::new() + .uniform_buffer(wgpu::ShaderStages::VERTEX, false) + .build(device); + let view_bind_group = bevy_nannou_wgpu::BindGroupBuilder::new() + .buffer::(&view_buffer, 0..1) + .build(device, &view_bind_group_layout); + + NannouPipeline { + glyph_cache, + glyph_cache_texture, + text_bind_group_layout, + text_bind_group, + texture_samplers, + texture_bind_group_layout, + texture_bind_group, + output_color_format: wgpu::TextureFormat::Rgba8UnormSrgb, + view_bind_group_layout, + view_bind_group, + view_buffer, + } + } +} + +fn create_depth_texture(device: &RenderDevice, size: [u32; 2], sample_count: u32) -> wgpu::Texture { + bevy_nannou_wgpu::TextureBuilder::new() + .size(size) + .format(wgpu::TextureFormat::Depth32Float) + .usage(wgpu::TextureUsages::RENDER_ATTACHMENT) + .sample_count(sample_count) + .build(device) +} + +fn create_texture_bind_group_layout( + device: &RenderDevice, + filtering: bool, + texture_sample_type: wgpu::TextureSampleType, +) -> wgpu::BindGroupLayout { + bevy_nannou_wgpu::BindGroupLayoutBuilder::new() + .sampler(wgpu::ShaderStages::FRAGMENT, filtering) + .texture( + wgpu::ShaderStages::FRAGMENT, + false, + wgpu::TextureViewDimension::D2, + texture_sample_type, + ) + .build(device) +} + +fn create_text_bind_group( + device: &RenderDevice, + layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + glyph_cache_texture_view: &wgpu::TextureView, +) -> wgpu::BindGroup { + bevy_nannou_wgpu::BindGroupBuilder::new() + .sampler(sampler) + .texture_view(glyph_cache_texture_view) + .build(device, layout) +} + +fn create_texture_bind_group( + device: &RenderDevice, + layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + texture_view: &wgpu::TextureView, +) -> wgpu::BindGroup { + bevy_nannou_wgpu::BindGroupBuilder::new() + .sampler(sampler) + .texture_view(texture_view) + .build(device, layout) +} + +fn sampler_descriptor_hash(desc: &wgpu::SamplerDescriptor) -> wgpu::SamplerId { + let mut s = std::collections::hash_map::DefaultHasher::new(); + desc.address_mode_u.hash(&mut s); + desc.address_mode_v.hash(&mut s); + desc.address_mode_w.hash(&mut s); + desc.mag_filter.hash(&mut s); + desc.min_filter.hash(&mut s); + desc.mipmap_filter.hash(&mut s); + desc.lod_min_clamp.to_bits().hash(&mut s); + desc.lod_max_clamp.to_bits().hash(&mut s); + desc.compare.hash(&mut s); + desc.anisotropy_clamp.hash(&mut s); + desc.border_color.hash(&mut s); + // TODO: can we just use bevy's version? + let id = s.finish() as u32; + unsafe { std::mem::transmute(id) } +} + +fn create_text_bind_group_layout(device: &RenderDevice, filtering: bool) -> wgpu::BindGroupLayout { + bevy_nannou_wgpu::BindGroupLayoutBuilder::new() + .sampler(wgpu::ShaderStages::FRAGMENT, filtering) + .texture( + wgpu::ShaderStages::FRAGMENT, + false, + wgpu::TextureViewDimension::D2, + wgpu::TextureFormat::R8Unorm + .sample_type(None) + .expect("Expected format to have sample type"), + ) + .build(device) +} + +#[derive(Resource, Deref, DerefMut)] +pub struct NannouPipelines(pub utils::HashMap); + +pub fn queue_pipelines( + mut commands: Commands, + pipeline: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + cameras: Query>, +) { + let pipelines = cameras + .iter() + .filter_map(|entity| { + let key = NannouPipelineKey { + /// TODO: this should be configurable based on a user + /// registered resource, i.e. when spawning a new window + sample_count: NannouPipeline::DEFAULT_SAMPLE_COUNT, + depth_format: NannouPipeline::DEFAULT_DEPTH_FORMAT, + blend_state: NannouPipeline::DEFAULT_BLEND_STATE, + topology: NannouPipeline::DEFAULT_PRIMITIVE_TOPOLOGY, + }; + let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, key); + + Some((entity, pipeline_id)) + }) + .collect(); + + commands.insert_resource(NannouPipelines(pipelines)); +} +pub struct NannouViewNode; + +impl NannouViewNode { + pub const NAME: &'static str = "nannou"; +} + +impl FromWorld for NannouViewNode { + fn from_world(_world: &mut World) -> Self { + NannouViewNode + } +} + +impl ViewNode for NannouViewNode { + type ViewQuery = ( + Entity, + &'static ViewTarget, + &'static ViewMesh, + &'static ViewDepthTexture, + ); + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (entity, target, mesh, depth_texture): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let nannou_pipeline = world.resource::(); + let nannou_pipelines = world.resource::(); + let pipeline_cache = world.resource::(); + let windows = world.resource::(); + let Some(pipeline_id) = nannou_pipelines.get(&entity) else { + return Ok(()); + }; + let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else { + return Ok(()); + }; + + // Create render pass builder. + let render_pass_builder = bevy_nannou_wgpu::RenderPassBuilder::new() + .color_attachment(target.main_texture_view(), |color| color); + + let render_device = render_context.render_device(); + + let vertex_usage = wgpu::BufferUsages::VERTEX; + let points_bytes = cast_slice(&mesh.points()[..]); + let colors_bytes = cast_slice(mesh.colors()); + let tex_coords_bytes = cast_slice(mesh.tex_coords()); + // let modes_bytes = vertex_modes_as_bytes(vertex_mode_buffer); + let indices_bytes = cast_slice(mesh.indices()); + let point_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("nannou Renderer point_buffer"), + contents: points_bytes, + usage: vertex_usage, + }); + let color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("nannou Renderer color_buffer"), + contents: colors_bytes, + usage: vertex_usage, + }); + let tex_coords_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("nannou Renderer tex_coords_buffer"), + contents: tex_coords_bytes, + usage: vertex_usage, + }); + let mode_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("nannou Renderer mode_buffer"), + contents: &[], + usage: vertex_usage, + }); + let index_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("nannou Renderer index_buffer"), + contents: indices_bytes, + usage: wgpu::BufferUsages::INDEX, + }); + + let mut render_pass = render_pass_builder.begin(render_context); + render_pass.set_render_pipeline(pipeline); + + // Set the buffers. + render_pass.set_index_buffer(index_buffer.slice(..), 0, wgpu::IndexFormat::Uint32); + render_pass.set_vertex_buffer(0, point_buffer.slice(..)); + render_pass.set_vertex_buffer(1, color_buffer.slice(..)); + render_pass.set_vertex_buffer(2, tex_coords_buffer.slice(..)); + render_pass.set_vertex_buffer(3, mode_buffer.slice(..)); + + // Set the uniform and text bind groups here. + let uniform_bind_group = world.resource::(); + render_pass.set_bind_group(0, &uniform_bind_group.bind_group, &[]); + render_pass.set_bind_group(1, &nannou_pipeline.text_bind_group, &[]); + render_pass.set_bind_group(2, &nannou_pipeline.texture_bind_group, &[]); + + // Draw the mesh. + let indices = 0..mesh.indices().len() as u32; + render_pass.draw_indexed(indices, 0, 0..1); + Ok(()) + } +} diff --git a/bevy_nannou_render/src/shaders/nannou.wgsl b/bevy_nannou_render/src/shaders/nannou.wgsl new file mode 100644 index 000000000..a05536253 --- /dev/null +++ b/bevy_nannou_render/src/shaders/nannou.wgsl @@ -0,0 +1,69 @@ +#import bevy_render::view::View + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VERTEX ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // + +@group(0) @binding(0) var view: View; + +struct VertexInput { + @location(0) position: vec3, + @location(1) color: vec4, + @location(2) tex_coords: vec2, + @location(3) mode: u32, +}; + +struct VertexOutput { + @location(0) color: vec4, + @location(1) tex_coords: vec2, + @location(2) mode: u32, + @builtin(position) pos: vec4, +}; + +@vertex +fn vertex( + input: VertexInput, +) -> VertexOutput { + let out_pos: vec4 = view.projection * vec4(input.position, 1.0); + return VertexOutput(input.color, input.tex_coords, input.mode, out_pos); + +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FRAGMENT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // + +struct FragmentInput { + @location(0) color: vec4, + @location(1) tex_coords: vec2, + @location(2) mode: u32, +} + +struct FragmentOutput { + @location(0) color: vec4, +}; + +@group(1) @binding(0) var text_sampler: sampler; +@group(1) @binding(1) var text: texture_2d; +@group(2) @binding(0) var tex_sampler: sampler; +@group(2) @binding(1) var tex: texture_2d; + +@fragment +fn fragment( + input: FragmentInput, +) -> FragmentOutput { + let tex_color: vec4 = textureSample(tex, tex_sampler, input.tex_coords); + let text_color: vec4 = textureSample(text, text_sampler, input.tex_coords); + let text_alpha: f32 = text_color.x; + var out_color: vec4; + if (input.mode == u32(0)) { + out_color = input.color; + } else { + if (input.mode == u32(1)) { + out_color = tex_color; + } else { + if (input.mode == u32(2)) { + out_color = vec4(input.color.xyz, input.color.w * text_alpha); + } else { + out_color = vec4(1.0, 0.0, 0.0, 1.0); + } + } + } + return FragmentOutput(out_color); +} diff --git a/bevy_nannou_render/src/text/cursor.rs b/bevy_nannou_render/src/text/cursor.rs new file mode 100644 index 000000000..73cc070ce --- /dev/null +++ b/bevy_nannou_render/src/text/cursor.rs @@ -0,0 +1,461 @@ +//! Logic related to the positioning of the cursor within text. + +use nannou_core::geom::{Range, Rect}; +use crate::text::{self, FontSize, Point, Scalar}; + +/// An index representing the position of a cursor within some text. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Index { + /// The byte index of the line upon which the cursor is situated. + pub line: usize, + /// The index within all possible cursor positions for the line. + /// + /// For example, for the line `foo`, a `char` of `1` would indicate the cursor's position + /// as `f|oo` where `|` is the cursor. + pub char: usize, +} + +/// Every possible cursor position within each line of text yielded by the given iterator. +/// +/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y* +/// axis and `xs` is every possible cursor position along the *x* axis +#[derive(Clone)] +pub struct XysPerLine<'a, I> { + lines_with_rects: I, + font: &'a text::Font, + text: &'a str, + font_size: FontSize, +} + +/// Similarly to `XysPerLine`, yields every possible cursor position within each line of text +/// yielded by the given iterator. +/// +/// Rather than taking an iterator type yielding lines and positioning data, this method +/// constructs its own iterator to do so internally, saving some boilerplate involved in common +/// `XysPerLine` use cases. +/// +/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y* +/// axis and `xs` is every possible cursor position along the *x* axis. +#[derive(Clone)] +pub struct XysPerLineFromText<'a> { + xys_per_line: XysPerLine< + 'a, + std::iter::Zip< + std::iter::Cloned>, + text::line::Rects>>, + >, + >, +} + +/// Each possible cursor position along the *x* axis within a line of text. +/// +/// `Xs` iterators are produced by the `XysPerLine` iterator. +pub struct Xs<'a, 'b> { + next_x: Option, + layout: text::LayoutIter<'a, 'b>, +} + +impl Index { + /// The cursor index of the beginning of the word (block of non-whitespace) before `self`. + /// + /// If `self` is at the beginning of the line, call previous, which returns the last + /// index position of the previous line, or None if it's the first line + /// + /// If `self` points to whitespace, skip past that whitespace, then return the index of + /// the start of the word that precedes the whitespace + /// + /// If `self` is in the middle or end of a word, return the index of the start of that word + pub fn previous_word_start(self, text: &str, mut line_infos: I) -> Option + where + I: Iterator, + { + let Index { line, char } = self; + if char > 0 { + line_infos.nth(line).and_then(|line_info| { + let line_count = line_info.char_range().count(); + let mut chars_rev = (&text[line_info.byte_range()]).chars().rev(); + if char != line_count { + chars_rev.nth(line_count - char - 1); + } + let mut new_char = 0; + let mut hit_non_whitespace = false; + for (i, char_) in chars_rev.enumerate() { + // loop until word starts, then continue until the word ends + if !char_.is_whitespace() { + hit_non_whitespace = true; + } + if char_.is_whitespace() && hit_non_whitespace { + new_char = char - i; + break; + } + } + Some(Index { + line: line, + char: new_char, + }) + }) + } else { + self.previous(line_infos) + } + } + + /// The cursor index of the end of the first word (block of non-whitespace) after `self`. + /// + /// If `self` is at the end of the text, this returns `None`. + /// + /// If `self` is at the end of a line other than the last, this returns the first index of + /// the next line. + /// + /// If `self` points to whitespace, skip past that whitespace, then return the index of + /// the end of the word after the whitespace + /// + /// If `self` is in the middle or start of a word, return the index of the end of that word + pub fn next_word_end(self, text: &str, mut line_infos: I) -> Option + where + I: Iterator, + { + let Index { line, char } = self; + line_infos.nth(line).and_then(|line_info| { + let line_count = line_info.char_range().count(); + if char < line_count { + let mut chars = (&text[line_info.byte_range()]).chars(); + let mut new_char = line_count; + let mut hit_non_whitespace = false; + if char != 0 { + chars.nth(char - 1); + } + for (i, char_) in chars.enumerate() { + // loop until word starts, then continue until the word ends + if !char_.is_whitespace() { + hit_non_whitespace = true; + } + if char_.is_whitespace() && hit_non_whitespace { + new_char = char + i; + break; + } + } + Some(Index { + line: line, + char: new_char, + }) + } else { + line_infos.next().map(|_| Index { + line: line + 1, + char: 0, + }) + } + }) + } + + /// The cursor index that comes before `self`. + /// + /// If `self` is at the beginning of the text, this returns `None`. + /// + /// If `self` is at the beginning of a line other than the first, this returns the last + /// index position of the previous line. + /// + /// If `self` is a position other than the start of a line, it will return the position + /// that is immediately to the left. + pub fn previous(self, mut line_infos: I) -> Option + where + I: Iterator, + { + let Index { line, char } = self; + if char > 0 { + let new_char = char - 1; + line_infos.nth(line).and_then(|info| { + if new_char <= info.char_range().count() { + Some(Index { + line: line, + char: new_char, + }) + } else { + None + } + }) + } else if line > 0 { + let new_line = line - 1; + line_infos.nth(new_line).map(|info| { + let new_char = info.end_char() - info.start_char; + Index { + line: new_line, + char: new_char, + } + }) + } else { + None + } + } + + /// The cursor index that follows `self`. + /// + /// If `self` is at the end of the text, this returns `None`. + /// + /// If `self` is at the end of a line other than the last, this returns the first index of + /// the next line. + /// + /// If `self` is a position other than the end of a line, it will return the position that + /// is immediately to the right. + pub fn next(self, mut line_infos: I) -> Option + where + I: Iterator, + { + let Index { line, char } = self; + line_infos.nth(line).and_then(|info| { + if char >= info.char_range().count() { + line_infos.next().map(|_| Index { + line: line + 1, + char: 0, + }) + } else { + Some(Index { + line: line, + char: char + 1, + }) + } + }) + } + + /// Clamps `self` to the given lines. + /// + /// If `self` would lie after the end of the last line, return the index at the end of the + /// last line. + /// + /// If `line_infos` is empty, returns cursor at line=0 char=0. + pub fn clamp_to_lines(self, line_infos: I) -> Self + where + I: Iterator, + { + let mut last = None; + for (i, info) in line_infos.enumerate() { + if i == self.line { + let num_chars = info.char_range().len(); + let char = std::cmp::min(self.char, num_chars); + return Index { + line: i, + char: char, + }; + } + last = Some((i, info)); + } + match last { + Some((i, info)) => Index { + line: i, + char: info.char_range().len(), + }, + None => Index { line: 0, char: 0 }, + } + } +} + +/// Every possible cursor position within each line of text yielded by the given iterator. +/// +/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y* +/// axis and `xs` is every possible cursor position along the *x* axis +pub fn xys_per_line<'a, I>( + lines_with_rects: I, + font: &'a text::Font, + text: &'a str, + font_size: FontSize, +) -> XysPerLine<'a, I> { + XysPerLine { + lines_with_rects: lines_with_rects, + font: font, + text: text, + font_size: font_size, + } +} + +/// Similarly to `xys_per_line`, this produces an iterator yielding every possible cursor +/// position within each line of text yielded by the given iterator. +/// +/// Rather than taking an iterator yielding lines and their positioning data, this method +/// constructs its own iterator to do so internally, saving some boilerplate involved in common +/// `xys_per_line` use cases. +/// +/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y* +/// axis and `xs` is every possible cursor position along the *x* axis. +pub fn xys_per_line_from_text<'a>( + text: &'a str, + line_infos: &'a [text::line::Info], + font: &'a text::Font, + font_size: FontSize, + max_width: Scalar, + x_align: text::Justify, + line_spacing: Scalar, +) -> XysPerLineFromText<'a> { + let line_infos = line_infos.iter().cloned(); + let line_rects = text::line::rects( + line_infos.clone(), + font_size, + max_width, + x_align, + line_spacing, + ); + let lines = line_infos.clone(); + let lines_with_rects = lines.zip(line_rects.clone()); + XysPerLineFromText { + xys_per_line: text::cursor::xys_per_line(lines_with_rects, font, text, font_size), + } +} + +/// Convert the given character index into a cursor `Index`. +pub fn index_before_char(line_infos: I, char_index: usize) -> Option +where + I: Iterator, +{ + for (i, line_info) in line_infos.enumerate() { + let start_char = line_info.start_char; + let end_char = line_info.end_char(); + if start_char <= char_index && char_index <= end_char { + return Some(Index { + line: i, + char: char_index - start_char, + }); + } + } + None +} + +/// Determine the *xy* location of the cursor at the given cursor `Index`. +pub fn xy_at<'a, I>(xys_per_line: I, idx: Index) -> Option<(Scalar, Range)> +where + I: Iterator, Range)>, +{ + for (i, (xs, y)) in xys_per_line.enumerate() { + if i == idx.line { + for (j, x) in xs.enumerate() { + if j == idx.char { + return Some((x, y)); + } + } + } + } + None +} + +/// Find the closest line for the given `y` position, and return the line index, Xs iterator, and y-range of that line +/// +/// Returns `None` if there are no lines +pub fn closest_line<'a, I>(y_pos: Scalar, xys_per_line: I) -> Option<(usize, Xs<'a, 'a>, Range)> +where + I: Iterator, Range)>, +{ + let mut xys_per_line_enumerated = xys_per_line.enumerate(); + xys_per_line_enumerated + .next() + .and_then(|(first_line_idx, (first_line_xs, first_line_y))| { + let mut closest_line = (first_line_idx, first_line_xs, first_line_y); + let mut closest_diff = (y_pos - first_line_y.middle()).abs(); + for (line_idx, (line_xs, line_y)) in xys_per_line_enumerated { + if line_y.contains(y_pos) { + closest_line = (line_idx, line_xs, line_y); + break; + } else { + let diff = (y_pos - line_y.middle()).abs(); + if diff < closest_diff { + closest_line = (line_idx, line_xs, line_y); + closest_diff = diff; + } else { + break; + } + } + } + Some(closest_line) + }) +} + +/// Find the closest cursor index to the given `xy` position, and the center `Point` of that +/// cursor. +/// +/// Returns `None` if the given `text` is empty. +pub fn closest_cursor_index_and_xy<'a, I>(xy: Point, xys_per_line: I) -> Option<(Index, Point)> +where + I: Iterator, Range)>, +{ + closest_line(xy[1], xys_per_line).and_then( + |(closest_line_idx, closest_line_xs, closest_line_y)| { + let (closest_char_idx, closest_x) = + closest_cursor_index_on_line(xy[0], closest_line_xs); + let index = Index { + line: closest_line_idx, + char: closest_char_idx, + }; + let point = [closest_x, closest_line_y.middle()].into(); + Some((index, point)) + }, + ) +} + +/// Find the closest cursor index to the given `x` position on the given line along with the +/// `x` position of that cursor. +pub fn closest_cursor_index_on_line<'a>(x_pos: Scalar, line_xs: Xs<'a, 'a>) -> (usize, Scalar) { + let mut xs_enumerated = line_xs.enumerate(); + // `xs` always yields at least one `x` (the start of the line). + let (first_idx, first_x) = xs_enumerated.next().unwrap(); + let first_diff = (x_pos - first_x).abs(); + let mut closest = (first_idx, first_x); + let mut closest_diff = first_diff; + for (i, x) in xs_enumerated { + let diff = (x_pos - x).abs(); + if diff < closest_diff { + closest = (i, x); + closest_diff = diff; + } else { + break; + } + } + closest +} + +impl<'a, I> Iterator for XysPerLine<'a, I> +where + I: Iterator, +{ + // The `Range` occupied by the line across the *y* axis, along with an iterator yielding + // each possible cursor position along the *x* axis. + type Item = (Xs<'a, 'a>, Range); + fn next(&mut self) -> Option { + let XysPerLine { + ref mut lines_with_rects, + font, + text, + font_size, + } = *self; + let scale = text::pt_to_scale(font_size); + lines_with_rects.next().map(|(line_info, line_rect)| { + let line = &text[line_info.byte_range()]; + let (x, y) = (line_rect.left() as f32, line_rect.top() as f32); + let point = text::rt::Point { x: x, y: y }; + let y = line_rect.y; + let layout = font.layout(line, scale, point); + let xs = Xs { + next_x: Some(line_rect.x.start), + layout: layout, + }; + (xs, y) + }) + } +} + +impl<'a> Iterator for XysPerLineFromText<'a> { + type Item = (Xs<'a, 'a>, Range); + fn next(&mut self) -> Option { + self.xys_per_line.next() + } +} + +impl<'a, 'b> Iterator for Xs<'a, 'b> { + // Each possible cursor position along the *x* axis. + type Item = Scalar; + fn next(&mut self) -> Option { + self.next_x.map(|x| { + self.next_x = self.layout.next().map(|g| { + g.pixel_bounding_box() + .map(|r| r.max.x as Scalar) + .unwrap_or_else(|| x + g.unpositioned().h_metrics().advance_width as Scalar) + }); + x + }) + } +} diff --git a/bevy_nannou_render/src/text/font.rs b/bevy_nannou_render/src/text/font.rs new file mode 100644 index 000000000..f2c62fdcc --- /dev/null +++ b/bevy_nannou_render/src/text/font.rs @@ -0,0 +1,229 @@ +//! The `font::Id` and `font::Map` types. + +use crate::text::{Font, FontCollection}; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; + +/// A type-safe wrapper around the `FontId`. +/// +/// This is used as both: +/// +/// - The key for the `font::Map`'s inner `HashMap`. +/// - The `font_id` field for the rusttype::gpu_cache::Cache. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Id(usize); + +/// A collection of mappings from `font::Id`s to `rusttype::Font`s. +#[derive(Debug)] +pub struct Map { + next_index: usize, + map: HashMap, +} + +/// An iterator yielding an `Id` for each new `rusttype::Font` inserted into the `Map` via the +/// `insert_collection` method. +pub struct NewIds { + index_range: std::ops::Range, +} + +/// Yields the `Id` for each `Font` within the `Map`. +#[derive(Clone)] +pub struct Ids<'a> { + keys: std::collections::hash_map::Keys<'a, Id, Font>, +} + +/// Returned when loading new fonts from file or bytes. +#[derive(Debug)] +pub enum Error { + /// Some error occurred while loading a `FontCollection` from a file. + Io(std::io::Error), + /// No `Font`s could be yielded from the `FontCollection`. + NoFont, +} + +/// The name of the default directory that is searched for fonts. +pub const DEFAULT_DIRECTORY_NAME: &str = "fonts"; + +impl Id { + /// Returns the inner `usize` from the `Id`. + pub fn index(self) -> usize { + self.0 + } +} + +impl Map { + /// Construct the new, empty `Map`. + pub fn new() -> Self { + Map { + next_index: 0, + map: HashMap::default(), + } + } + + /// Borrow the `rusttype::Font` associated with the given `font::Id`. + pub fn get(&self, id: Id) -> Option<&Font> { + self.map.get(&id) + } + + /// Adds the given `rusttype::Font` to the `Map` and returns a unique `Id` for it. + pub fn insert(&mut self, font: Font) -> Id { + let index = self.next_index; + self.next_index = index.wrapping_add(1); + let id = Id(index); + self.map.insert(id, font); + id + } + + /// Insert a single `Font` into the map by loading it from the given file path. + pub fn insert_from_file

(&mut self, path: P) -> Result + where + P: AsRef, + { + let font = from_file(path)?; + Ok(self.insert(font)) + } + + // /// Adds each font in the given `rusttype::FontCollection` to the `Map` and returns an + // /// iterator yielding a unique `Id` for each. + // pub fn insert_collection(&mut self, collection: FontCollection) -> NewIds { + // let start_index = self.next_index; + // let mut end_index = start_index; + // for index in 0.. { + // match collection.font_at(index) { + // Some(font) => { + // self.insert(font); + // end_index += 1; + // } + // None => break, + // } + // } + // NewIds { index_range: start_index..end_index } + // } + + /// Produces an iterator yielding the `Id` for each `Font` within the `Map`. + pub fn ids(&self) -> Ids { + Ids { + keys: self.map.keys(), + } + } +} + +/// Produce a unique ID for the given font. +pub fn id(font: &Font) -> Id { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + for name in font.font_name_strings() { + name.hash(&mut hasher); + } + Id((hasher.finish() % std::usize::MAX as u64) as usize) +} + +/// Load a `FontCollection` from a file at a given path. +pub fn collection_from_file

(path: P) -> Result +where + P: AsRef, +{ + use std::io::Read; + let path = path.as_ref(); + let mut file = std::fs::File::open(path)?; + let mut file_buffer = Vec::new(); + file.read_to_end(&mut file_buffer)?; + Ok(FontCollection::from_bytes(file_buffer)?) +} + +/// Load a single `Font` from a file at the given path. +pub fn from_file

(path: P) -> Result +where + P: AsRef, +{ + let collection = collection_from_file(path)?; + collection.into_font().or(Err(Error::NoFont)) +} + +/// Load the default notosans font. +/// +/// This function is only available if the `notosans` feature is enabled, which it is by default. +#[cfg(feature = "notosans")] +pub fn default_notosans() -> Font { + let collection = FontCollection::from_bytes(notosans::REGULAR_TTF) + .expect("failed to load the `notosans::REGULAR_TTF` font collection"); + collection + .into_font() + .expect("the `notosans::REGULAR_TTF` font collection contained no fonts") +} + +/// The directory that is searched for default fonts. +pub fn default_directory(assets: &Path) -> PathBuf { + assets.join(DEFAULT_DIRECTORY_NAME) +} + +/// Load the default font. +/// +/// If the `notosans` feature is enabled, this will return the font loaded from +/// `notosans::REGULAR_TTF`. +/// +/// Otherwise this will attempt to locate the `assets/fonts` directory. If the directory exists, +/// the first font that is found will be loaded. If no fonts are found, an error is returned. +#[allow(unreachable_code, unused_variables)] +pub fn default(assets: &Path) -> Result { + #[cfg(feature = "notosans")] + { + return Ok(default_notosans()); + } + + // TODO: load this via bevy's asset system + // Find a font in `assets/fonts`. + // let fonts_dir = default_directory(assets); + // if fonts_dir.exists() && fonts_dir.is_dir() { + // for res in crate::io::walk_dir(&fonts_dir) { + // let entry = match res { + // Ok(e) => e, + // Err(_) => continue, + // }; + // match from_file(entry.path()) { + // Err(_) => continue, + // Ok(font) => return Ok(font), + // } + // } + // } + + Err(Error::NoFont) +} + +impl Iterator for NewIds { + type Item = Id; + fn next(&mut self) -> Option { + self.index_range.next().map(|i| Id(i)) + } +} + +impl<'a> Iterator for Ids<'a> { + type Item = Id; + fn next(&mut self) -> Option { + self.keys.next().map(|&id| id) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl std::error::Error for Error { + fn cause(&self) -> Option<&dyn std::error::Error> { + match *self { + Error::Io(ref e) => Some(e), + Error::NoFont => None, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match *self { + Error::Io(ref e) => std::fmt::Display::fmt(e, f), + Error::NoFont => write!(f, "No `Font` found in the loaded `FontCollection`."), + } + } +} diff --git a/bevy_nannou_render/src/text/glyph.rs b/bevy_nannou_render/src/text/glyph.rs new file mode 100644 index 000000000..39cd481d5 --- /dev/null +++ b/bevy_nannou_render/src/text/glyph.rs @@ -0,0 +1,344 @@ +//! Logic and types specific to individual glyph layout. + +use nannou_core::geom::{Range, Rect}; +use crate::text::{self, FontSize, Scalar, ScaledGlyph}; + +/// Some position along the X axis (used within `CharXs`). +pub type X = Scalar; + +/// The half of the width of some character (used within `CharXs`). +pub type HalfW = Scalar; + +/// An iterator yielding the `Rect` for each `char`'s `Glyph` in the given `text`. +pub struct Rects<'a, 'b> { + /// The *y* axis `Range` of the `Line` for which character `Rect`s are being yielded. + /// + /// Every yielded `Rect` will use this as its `y` `Range`. + y: Range, + /// `PositionedGlyphs` yielded by the RustType `LayoutIter`. + layout: text::LayoutIter<'a, 'b>, +} + +/// An iterator that, for every `(line, line_rect)` pair yielded by the given iterator, +/// produces an iterator that yields a `Rect` for every character in that line. +pub struct RectsPerLine<'a, I> { + lines_with_rects: I, + font: &'a text::Font, + font_size: FontSize, +} + +/// Yields a `Rect` for each selected character in a single line of text. +/// +/// This iterator can only be produced by the `SelectedCharRectsPerLine` iterator. +pub struct SelectedRects<'a, 'b> { + enumerated_rects: std::iter::Enumerate>, + end_char_idx: usize, +} + +/// Yields an iteraor yielding `Rect`s for each selected character in each line of text within +/// the given iterator yielding char `Rect`s. +/// +/// Given some `start` and `end` indices, only `Rect`s for `char`s between these two indices +/// will be produced. +/// +/// All lines that have no selected `Rect`s will be skipped. +pub struct SelectedRectsPerLine<'a, I> { + enumerated_rects_per_line: std::iter::Enumerate>, + start_cursor_idx: text::cursor::Index, + end_cursor_idx: text::cursor::Index, +} + +struct ContourPathEvents { + segments: std::vec::IntoIter, + first: lyon::math::Point, + begin_event: Option, + first_segment_event: Option, + last: Option, +} + +impl<'a, 'b> Iterator for Rects<'a, 'b> { + type Item = (ScaledGlyph<'a>, Rect); + fn next(&mut self) -> Option { + let Rects { ref mut layout, y } = *self; + layout.next().map(|g| { + let left = g.position().x; + let (right, height) = g + .pixel_bounding_box() + .map(|bb| (bb.max.x as Scalar, (bb.max.y - bb.min.y) as Scalar)) + .unwrap_or_else(|| { + let w = g.unpositioned().h_metrics().advance_width as Scalar; + let r = left + w; + let h = 0.0; + (r, h) + }); + let x = Range::new(left, right); + let y = Range::new(y.start, y.start + height); + let r = Rect { x: x, y: y }; + let g = g.into_unpositioned(); + (g, r) + }) + } +} + +impl<'a, I> Iterator for RectsPerLine<'a, I> +where + I: Iterator, +{ + type Item = Rects<'a, 'a>; + fn next(&mut self) -> Option { + let RectsPerLine { + ref mut lines_with_rects, + font, + font_size, + } = *self; + let scale = text::pt_to_scale(font_size); + lines_with_rects.next().map(|(line, line_rect)| { + let (x, y) = (line_rect.left() as f32, line_rect.top() as f32); + let point = text::rt::Point { x: x, y: y }; + Rects { + layout: font.layout(line, scale, point), + y: line_rect.y, + } + }) + } +} + +impl<'a, 'b> Iterator for SelectedRects<'a, 'b> { + type Item = (ScaledGlyph<'a>, Rect); + fn next(&mut self) -> Option { + let SelectedRects { + ref mut enumerated_rects, + end_char_idx, + } = *self; + enumerated_rects.next().and_then( + |(i, rect)| { + if i < end_char_idx { + Some(rect) + } else { + None + } + }, + ) + } +} + +impl<'a, I> Iterator for SelectedRectsPerLine<'a, I> +where + I: Iterator, +{ + type Item = SelectedRects<'a, 'a>; + fn next(&mut self) -> Option { + let SelectedRectsPerLine { + ref mut enumerated_rects_per_line, + start_cursor_idx, + end_cursor_idx, + } = *self; + + enumerated_rects_per_line.next().map(|(i, rects)| { + let end_char_idx = + // If this is the last line, the end is the char after the final selected char. + if i == end_cursor_idx.line { + end_cursor_idx.char + // Otherwise if in range, every char in the line is selected. + } else if start_cursor_idx.line <= i && i < end_cursor_idx.line { + std::u32::MAX as usize + // Otherwise if out of range, no chars are selected. + } else { + 0 + }; + + let mut enumerated_rects = rects.enumerate(); + + // If this is the first line, skip all non-selected chars. + if i == start_cursor_idx.line { + for _ in 0..start_cursor_idx.char { + enumerated_rects.next(); + } + } + + SelectedRects { + enumerated_rects: enumerated_rects, + end_char_idx: end_char_idx, + } + }) + } +} + +impl Iterator for ContourPathEvents { + type Item = lyon::path::PathEvent; + fn next(&mut self) -> Option { + if let Some(event) = self.begin_event.take() { + return Some(event); + } + if let Some(event) = self.first_segment_event.take() { + return Some(event); + } + match self.segments.next() { + None => self.last.take().map(|last| lyon::path::PathEvent::End { + first: self.first, + last, + close: true, + }), + Some(seg) => { + self.last = Some(tp(rt_segment_end(&seg))); + Some(segment_to_event(seg)) + } + } + } +} + +/// Produce an iterator that, for every `(line, line_rect)` pair yielded by the given iterator, +/// produces an iterator that yields a `Rect` for every character in that line. +/// +/// This is useful when information about character positioning is needed when reasoning about +/// text layout. +pub fn rects_per_line<'a, I>( + lines_with_rects: I, + font: &'a text::Font, + font_size: FontSize, +) -> RectsPerLine<'a, I> +where + I: Iterator, +{ + RectsPerLine { + lines_with_rects: lines_with_rects, + font: font, + font_size: font_size, + } +} + +/// Find the index of the character that directly follows the cursor at the given `cursor_idx`. +/// +/// Returns `None` if either the given `cursor::Index` `line` or `idx` fields are out of bounds +/// of the line information yielded by the `line_infos` iterator. +pub fn index_after_cursor(mut line_infos: I, cursor_idx: text::cursor::Index) -> Option +where + I: Iterator, +{ + line_infos.nth(cursor_idx.line).and_then(|line_info| { + let start_char = line_info.start_char; + let end_char = line_info.end_char(); + let char_index = start_char + cursor_idx.char; + if char_index <= end_char { + Some(char_index) + } else { + None + } + }) +} + +/// Produces an iterator that yields iteraors yielding `Rect`s for each selected character in +/// each line of text within the given iterator yielding char `Rect`s. +/// +/// Given some `start` and `end` indices, only `Rect`s for `char`s between these two indices +/// will be produced. +/// +/// All lines that have no selected `Rect`s will be skipped. +pub fn selected_rects_per_line<'a, I>( + lines_with_rects: I, + font: &'a text::Font, + font_size: FontSize, + start: text::cursor::Index, + end: text::cursor::Index, +) -> SelectedRectsPerLine<'a, I> +where + I: Iterator, +{ + SelectedRectsPerLine { + enumerated_rects_per_line: rects_per_line(lines_with_rects, font, font_size).enumerate(), + start_cursor_idx: start, + end_cursor_idx: end, + } +} + +fn rt_segment_start(s: &rusttype::Segment) -> rusttype::Point { + match *s { + rusttype::Segment::Line(ref line) => line.p[0], + rusttype::Segment::Curve(ref curve) => curve.p[0], + } +} + +fn rt_segment_end(s: &rusttype::Segment) -> rusttype::Point { + match *s { + rusttype::Segment::Line(ref line) => line.p[1], + rusttype::Segment::Curve(ref curve) => curve.p[2], + } +} + +// Translate the rusttype point to a nannou compatible one. +fn tp(p: rusttype::Point) -> lyon::math::Point { + lyon::math::point(p.x, p.y) +} + +// The event for moving to the start of a segment. +fn segment_begin_event(s: &rusttype::Segment) -> lyon::path::PathEvent { + let at = tp(rt_segment_start(s)); + lyon::path::PathEvent::Begin { at } +} + +// Convert the rusttype line to a lyon line segment. +fn conv_line_segment(l: &rusttype::Line) -> lyon::path::PathEvent { + let from = tp(l.p[0]); + let to = tp(l.p[1]); + lyon::path::PathEvent::Line { from, to } +} + +// Convert the rusttype curve to a lyon quadratic bezier segment. +fn conv_curve_segment(c: &rusttype::Curve) -> lyon::path::PathEvent { + let from = tp(c.p[0]); + let ctrl = tp(c.p[1]); + let to = tp(c.p[2]); + lyon::path::PathEvent::Quadratic { from, ctrl, to } +} + +// Convert the given rusttype segment to a lyon path event. +fn segment_to_event(s: rusttype::Segment) -> lyon::path::PathEvent { + match s { + rusttype::Segment::Line(ref l) => conv_line_segment(l), + rusttype::Segment::Curve(ref c) => conv_curve_segment(c), + } +} + +/// Convert the given sequence of contours to a `geom::Path`. +/// +/// In the resulting path events [0.0, 0.0] is the bottom left of the rect. +pub fn contours_to_path<'a, I>( + _exact_bounding_box: rusttype::Rect, + contours: I, +) -> impl Iterator +where + I: IntoIterator, +{ + contours.into_iter().flat_map(move |contour| { + let mut segs = contour.segments.into_iter(); + let maybe_first = segs.next(); + maybe_first + .map(move |first_seg| { + let first = tp(rt_segment_start(&first_seg)); + let last = Some(tp(rt_segment_end(&first_seg))); + let begin_event = Some(segment_begin_event(&first_seg)); + let first_segment_event = Some(segment_to_event(first_seg)); + ContourPathEvents { + segments: segs, + first, + last, + begin_event, + first_segment_event, + } + }) + .into_iter() + .flat_map(move |it| it) + }) +} + +/// Produce the lyon path for the given scaled glyph. +/// +/// Returns `None` if `glyph.shape()` or `glyph.exact_bounding_box()` returns `None`. +/// +/// TODO: This could be optimised by caching path events glyph ID and using normalised glyphs. +pub fn path_events(glyph: ScaledGlyph) -> Option> { + glyph + .exact_bounding_box() + .and_then(|bb| glyph.shape().map(|ctrs| (bb, ctrs))) + .map(|(bb, ctrs)| contours_to_path(bb, ctrs)) +} diff --git a/bevy_nannou_render/src/text/layout.rs b/bevy_nannou_render/src/text/layout.rs new file mode 100644 index 000000000..785064d58 --- /dev/null +++ b/bevy_nannou_render/src/text/layout.rs @@ -0,0 +1,159 @@ +//! Items related to the styling of text. + +use crate::text::{Align, Font, FontSize, Justify, Scalar, Wrap}; + +/// A context for building a text layout. +#[derive(Clone, Debug, Default)] +pub struct Builder { + pub line_spacing: Option, + pub line_wrap: Option>, + pub font_size: Option, + pub justify: Option, + pub font: Option>, + pub y_align: Option, +} + +/// Properties related to the layout of multi-line text for a single font and font size. +#[derive(Clone, Debug)] +pub struct Layout { + pub line_spacing: Scalar, + pub line_wrap: Option, + pub justify: Justify, + pub font_size: FontSize, + pub font: Option, + pub y_align: Align, +} + +pub const DEFAULT_LINE_WRAP: Option = Some(Wrap::Whitespace); +pub const DEFAULT_FONT_SIZE: u32 = 12; +pub const DEFAULT_LINE_SPACING: f32 = 0.0; +pub const DEFAULT_JUSTIFY: Justify = Justify::Center; +pub const DEFAULT_Y_ALIGN: Align = Align::Middle; + +impl Builder { + /// The font size to use for the text. + pub fn font_size(mut self, size: FontSize) -> Self { + self.font_size = Some(size); + self + } + + /// Specify whether or not text should be wrapped around some width and how to do so. + /// + /// The default value is `DEFAULT_LINE_WRAP`. + pub fn line_wrap(mut self, line_wrap: Option) -> Self { + self.line_wrap = Some(line_wrap); + self + } + + /// Specify that the **Text** should not wrap lines around the width. + /// + /// Shorthand for `builder.line_wrap(None)`. + pub fn no_line_wrap(self) -> Self { + self.line_wrap(None) + } + + /// Line wrap the **Text** at the beginning of the first word that exceeds the width. + /// + /// Shorthand for `builder.line_wrap(Some(Wrap::Whitespace))`. + pub fn wrap_by_word(self) -> Self { + self.line_wrap(Some(Wrap::Whitespace)) + } + + /// Line wrap the **Text** at the beginning of the first character that exceeds the width. + /// + /// Shorthand for `builder.line_wrap(Some(Wrap::Character))`. + pub fn wrap_by_character(self) -> Self { + self.line_wrap(Some(Wrap::Character)) + } + + /// A method for specifying the `Font` used for displaying the `Text`. + pub fn font(mut self, font: Font) -> Self { + self.font = Some(Some(font)); + self + } + + /// Describe the end along the *x* axis to which the text should be aligned. + pub fn justify(mut self, justify: Justify) -> Self { + self.justify = Some(justify); + self + } + + /// Align the text to the left of its bounding **Rect**'s *x* axis range. + pub fn left_justify(self) -> Self { + self.justify(Justify::Left) + } + + /// Align the text to the middle of its bounding **Rect**'s *x* axis range. + pub fn center_justify(self) -> Self { + self.justify(Justify::Center) + } + + /// Align the text to the right of its bounding **Rect**'s *x* axis range. + pub fn right_justify(self) -> Self { + self.justify(Justify::Right) + } + + /// Specify how much vertical space should separate each line of text. + pub fn line_spacing(mut self, spacing: Scalar) -> Self { + self.line_spacing = Some(spacing); + self + } + + /// Specify how the whole text should be aligned along the y axis of its bounding rectangle + pub fn y_align(mut self, align: Align) -> Self { + self.y_align = Some(align); + self + } + + /// Align the top edge of the text with the top edge of its bounding rectangle. + pub fn align_top(self) -> Self { + self.y_align(Align::End) + } + + /// Align the middle of the text with the middle of the bounding rect along the y axis.. + /// + /// This is the default behaviour. + pub fn align_middle_y(self) -> Self { + self.y_align(Align::Middle) + } + + /// Align the bottom edge of the text with the bottom edge of its bounding rectangle. + pub fn align_bottom(self) -> Self { + self.y_align(Align::Start) + } + + /// Set all the parameters via an existing `Layout` + pub fn layout(mut self, layout: &Layout) -> Self { + self.font = Some(layout.font.clone()); + self.line_spacing(layout.line_spacing) + .line_wrap(layout.line_wrap) + .justify(layout.justify) + .font_size(layout.font_size) + .y_align(layout.y_align) + } + + /// Build the text layout. + pub fn build(self) -> Layout { + Layout { + line_spacing: self.line_spacing.unwrap_or(DEFAULT_LINE_SPACING), + line_wrap: self.line_wrap.unwrap_or(DEFAULT_LINE_WRAP), + justify: self.justify.unwrap_or(DEFAULT_JUSTIFY), + font_size: self.font_size.unwrap_or(DEFAULT_FONT_SIZE), + font: self.font.unwrap_or(None), + y_align: self.y_align.unwrap_or(DEFAULT_Y_ALIGN), + } + } +} + +impl Default for Layout { + fn default() -> Self { + Layout { + line_spacing: DEFAULT_LINE_SPACING, + line_wrap: DEFAULT_LINE_WRAP, + justify: DEFAULT_JUSTIFY, + font_size: DEFAULT_FONT_SIZE, + font: None, + y_align: DEFAULT_Y_ALIGN, + } + } +} diff --git a/bevy_nannou_render/src/text/line.rs b/bevy_nannou_render/src/text/line.rs new file mode 100644 index 000000000..60836b77a --- /dev/null +++ b/bevy_nannou_render/src/text/line.rs @@ -0,0 +1,807 @@ +//! Text handling logic related to individual lines of text. +//! +//! This module is the core of multi-line text handling. + +use nannou_core::geom::{Range, Rect}; +use crate::text::{self, FontSize, Scalar, Wrap}; + +/// The two types of **Break** indices returned by the **WrapIndicesBy** iterators. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Break { + /// A break caused by the text exceeding some maximum width. + Wrap { + /// The byte index at which the break occurs. + byte: usize, + /// The char index at which the string should wrap due to exceeding a maximum width. + char: usize, + /// The byte length which should be skipped in order to reach the first non-whitespace + /// character to use as the beginning of the next line. + len_bytes: usize, + /// The number of chars which should be skipped in order to reach the first non-whitespace + /// character to use as the beginning of the next line. + len_chars: usize, + }, + /// A break caused by a newline character. + Newline { + /// The byte index at which the string should wrap due to exceeding a maximum width. + byte: usize, + /// The char index at which the string should wrap due to exceeding a maximum width. + char: usize, + /// The width of the "newline" token in bytes. + len_bytes: usize, + /// The width of the "newline" token in chars. + len_chars: usize, + }, + /// The end of the string has been reached, with the given length. + End { + /// The ending byte index. + byte: usize, + /// The ending char index. + char: usize, + }, +} + +/// The type yielded by functions dedicated to finding the next line break. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct NextBreak { + /// The reason for the break. + pub break_: Break, + /// The total width of the line. + pub width: Scalar, + /// The maximum heigh of the line. + pub height: Scalar, +} + +/// Information about a single line of text within a `&str`. +/// +/// `Info` is a minimal amount of information that can be stored for efficient reasoning about +/// blocks of text given some `&str`. The `start` and `end_break` can be used for indexing into +/// the `&str`, and the `width` can be used for calculating line `Rect`s, alignment, etc. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Info { + /// The index into the `&str` that represents the first character within the line. + pub start_byte: usize, + /// The character index into the `&str` of the first character in the line. + pub start_char: usize, + /// The index within the `&str` at which this line breaks into a new line, along with the + /// index at which the following line begins. The variant describes whether the break is + /// caused by a `Newline` character or a `Wrap` by the given wrap function. + pub end_break: Break, + /// The total width of all characters within the line. + pub width: Scalar, + /// The greatest height of all characters yielded. + pub height: Scalar, +} + +/// An iterator yielding an `Info` struct for each line in the given `text` wrapped by the +/// given `next_break_fn`. +/// +/// `Infos` is a fundamental part of performing lazy reasoning about text within nannou. +/// +/// Construct an `Infos` iterator via the [infos function](./fn.infos.html) and its two builder +/// methods, [wrap_by_character](./struct.Infos.html#method.wrap_by_character) and +/// [wrap_by_whitespace](./struct.Infos.html#method.wrap_by_whitespace). +pub struct Infos<'a, F> { + text: &'a str, + font: &'a text::Font, + font_size: FontSize, + max_width: Scalar, + next_break_fn: F, + /// The index that indicates the start of the next line to be yielded. + start_byte: usize, + /// The character index that indicates the start of the next line to be yielded. + start_char: usize, + /// The break type of the previously yielded line + last_break: Option, +} + +/// An iterator yielding a `Rect` for each line in +#[derive(Clone)] +pub struct Rects { + infos: I, + x_align: text::Justify, + line_spacing: Scalar, + last_line_top: Scalar, + font_size: FontSize, + next: Option, +} + +/// An iterator yielding a `Rect` for each selected line in a block of text. +/// +/// The yielded `Rect`s represent the selected range within each line of text. +/// +/// Lines that do not contain any selected text will be skipped. +pub struct SelectedRects<'a, I> { + selected_char_rects_per_line: text::glyph::SelectedRectsPerLine<'a, I>, +} + +/// An alias for function pointers that are compatible with the `Block`'s required text +/// wrapping function. +pub type NextBreakFnPtr = fn(&str, &text::Font, FontSize, Scalar) -> NextBreak; + +impl Break { + /// Return the index at which the break occurs. + pub fn byte_index(self) -> usize { + match self { + Break::Wrap { byte, .. } | Break::Newline { byte, .. } | Break::End { byte, .. } => { + byte + } + } + } + + /// Return the index of the `char` at which the break occurs. + /// + /// To clarify, this index is to be used in relation to the `Chars` iterator. + pub fn char_index(self) -> usize { + match self { + Break::Wrap { char, .. } | Break::Newline { char, .. } | Break::End { char, .. } => { + char + } + } + } +} + +impl<'a, F> Clone for Infos<'a, F> +where + F: Clone, +{ + fn clone(&self) -> Self { + Infos { + text: self.text, + font: self.font, + font_size: self.font_size, + max_width: self.max_width, + next_break_fn: self.next_break_fn.clone(), + start_byte: self.start_byte, + start_char: self.start_char, + last_break: None, + } + } +} + +impl Info { + /// The end of the byte index range for indexing into the slice. + pub fn end_byte(&self) -> usize { + self.end_break.byte_index() + } + + /// The end of the index range for indexing into the slice. + pub fn end_char(&self) -> usize { + self.end_break.char_index() + } + + /// The index range for indexing (via bytes) into the original str slice. + pub fn byte_range(self) -> std::ops::Range { + self.start_byte..self.end_byte() + } + + /// The index range for indexing into a `char` iterator over the original str slice. + pub fn char_range(self) -> std::ops::Range { + self.start_char..self.end_char() + } +} + +impl<'a> Infos<'a, NextBreakFnPtr> { + /// Converts `Self` into an `Infos` whose lines are wrapped at the character that first + /// causes the line width to exceed the given `max_width`. + pub fn wrap_by_character(mut self, max_width: Scalar) -> Self { + self.next_break_fn = next_break_by_character; + self.max_width = max_width; + self + } + + /// Converts `Self` into an `Infos` whose lines are wrapped at the whitespace prior to the + /// character that causes the line width to exceed the given `max_width`. + pub fn wrap_by_whitespace(mut self, max_width: Scalar) -> Self { + self.next_break_fn = next_break_by_whitespace; + self.max_width = max_width; + self + } +} + +/// A function for finding the advance width between the given character that also considers +/// the kerning for some previous glyph. +/// +/// This also updates the `last_glyph` with the glyph produced for the given `char`. +/// +/// This is primarily for use within the `next_break` functions below. +/// +/// The following code is adapted from the rusttype::LayoutIter::next src. +fn advance_width_and_height( + ch: char, + font: &text::Font, + scale: text::Scale, + last_glyph: &mut Option, +) -> (Scalar, Scalar) { + let g = font.glyph(ch).scaled(scale); + let kern = last_glyph + .map(|last| font.pair_kerning(scale, last, g.id())) + .unwrap_or(0.0); + let advance_width = g.h_metrics().advance_width; + let height = g + .exact_bounding_box() + .map(|bb| bb.min.y.abs() as Scalar) + .unwrap_or(0.0); + *last_glyph = Some(g.id()); + let adv_w = (kern + advance_width) as Scalar; + (adv_w, height) +} + +/// Returns the next index at which the text naturally breaks via a newline character, +/// along with the width of the line. +fn next_break(text: &str, font: &text::Font, font_size: FontSize) -> NextBreak { + let scale = text::pt_to_scale(font_size); + let mut width = 0.0; + let mut height = 0.0; + let mut char_i = 0; + let mut char_indices = text.char_indices().peekable(); + let mut last_glyph = None; + while let Some((byte_i, ch)) = char_indices.next() { + // Check for a newline. + if ch == '\r' { + if let Some(&(_, '\n')) = char_indices.peek() { + let break_ = Break::Newline { + byte: byte_i, + char: char_i, + len_bytes: 2, // skip "\r\n" + len_chars: 2, + }; + return NextBreak { + break_, + width, + height, + }; + } + } else if ch == '\n' { + let break_ = Break::Newline { + byte: byte_i, + char: char_i, + len_bytes: 1, // skip "\n" + len_chars: 1, + }; + return NextBreak { + break_, + width, + height, + }; + } + + // Update the width. + let (adv_w, h) = advance_width_and_height(ch, font, scale, &mut last_glyph); + width += adv_w; + height = height.max(h); + char_i += 1; + } + let break_ = Break::End { + byte: text.len(), + char: char_i, + }; + NextBreak { + break_, + width, + height, + } +} + +/// Returns the next index at which the text will break by either: +/// - A newline character. +/// - A line wrap at the beginning of the first character exceeding the `max_width`. +/// +/// Also returns the width of each line alongside the Break. +fn next_break_by_character( + text: &str, + font: &text::Font, + font_size: FontSize, + max_width: Scalar, +) -> NextBreak { + let scale = text::pt_to_scale(font_size); + let mut width = 0.0; + let mut height = 0.0; + let mut char_i = 0; + let mut char_indices = text.char_indices().peekable(); + let mut last_glyph = None; + while let Some((byte_i, ch)) = char_indices.next() { + // Check for a newline. + if ch == '\r' { + if let Some(&(_, '\n')) = char_indices.peek() { + let break_ = Break::Newline { + byte: byte_i, + char: char_i, + len_bytes: 2, // skip "\r\n" + len_chars: 2, + }; + return NextBreak { + break_, + width, + height, + }; + } + } else if ch == '\n' { + let break_ = Break::Newline { + byte: byte_i, + char: char_i, + len_bytes: 1, // skip "\n" + len_chars: 1, + }; + return NextBreak { + break_, + width, + height, + }; + } + + // Add the character's width to the width so far. + let (adv_w, h) = advance_width_and_height(ch, font, scale, &mut last_glyph); + let new_width = width + adv_w; + + // Check for a line wrap. + if new_width > max_width { + let break_ = Break::Wrap { + byte: byte_i, + char: char_i, + len_bytes: 0, // skip nothing in middle of word + len_chars: 0, + }; + return NextBreak { + break_, + width, + height, + }; + } + + height = height.max(h); + width = new_width; + char_i += 1; + } + + let break_ = Break::End { + byte: text.len(), + char: char_i, + }; + NextBreak { + break_, + width, + height, + } +} + +/// Returns the next index at which the text will break by either: +/// - A newline character. +/// - A line wrap at the beginning of the whitespace that preceeds the first word +/// exceeding the `max_width`. +/// - A line wrap at the beginning of the first character exceeding the `max_width`, +/// if no whitespace appears for `max_width` characters. +/// +/// Also returns the width the line alongside the Break. +fn next_break_by_whitespace( + text: &str, + font: &text::Font, + font_size: FontSize, + max_width: Scalar, +) -> NextBreak { + struct Last { + byte: usize, + char: usize, + width_before: Scalar, + } + let scale = text::pt_to_scale(font_size); + let mut last_whitespace_start = None; + let mut width = 0.0; + let mut height = 0.0; + let mut char_i = 0; + let mut char_indices = text.char_indices().peekable(); + let mut last_glyph = None; + while let Some((byte_i, ch)) = char_indices.next() { + // Check for a newline. + if ch == '\r' { + if let Some(&(_, '\n')) = char_indices.peek() { + let break_ = Break::Newline { + byte: byte_i, + char: char_i, + len_bytes: 2, // skip "\r\n" + len_chars: 2, + }; + return NextBreak { + break_, + width, + height, + }; + } + } else if ch == '\n' { + let break_ = Break::Newline { + byte: byte_i, + char: char_i, + len_bytes: 1, // skip "\n" + len_chars: 1, + }; + return NextBreak { + break_, + width, + height, + }; + } + + // Add the character's width to the width so far. + let (adv_w, h) = advance_width_and_height(ch, font, scale, &mut last_glyph); + let new_width = width + adv_w; + + // Check for a line wrap. + if width > max_width { + match last_whitespace_start { + Some(Last { + byte, + char, + width_before, + }) => { + let break_ = Break::Wrap { + byte: byte, + char: char, + len_bytes: 1, // skip one whitespace character + len_chars: 1, + }; + let width = width_before; + return NextBreak { + break_, + width, + height, + }; + } + None => { + let break_ = Break::Wrap { + byte: byte_i, + char: char_i, + len_bytes: 0, // skip nothing in middle of word + len_chars: 0, + }; + return NextBreak { + break_, + width, + height, + }; + } + } + } + + // Check for a new whitespace. + if ch.is_whitespace() { + last_whitespace_start = Some(Last { + byte: byte_i, + char: char_i, + width_before: width, + }); + } + + width = new_width; + height = height.max(h); + char_i += 1; + } + + let break_ = Break::End { + byte: text.len(), + char: char_i, + }; + NextBreak { + break_, + width, + height, + } +} + +/// Produce the width of the given line of text including spaces (i.e. ' '). +pub fn width(text: &str, font: &text::Font, font_size: FontSize) -> Scalar { + let scale = text::Scale::uniform(text::pt_to_px(font_size)); + let point = text::rt::Point { x: 0.0, y: 0.0 }; + + let mut total_w = 0.0; + for g in font.layout(text, scale, point) { + match g.pixel_bounding_box() { + Some(bb) => total_w = bb.max.x as f32, + None => total_w += g.unpositioned().h_metrics().advance_width, + } + } + + total_w as Scalar +} + +/// Produce an `Infos` iterator wrapped by the given `next_break_fn`. +pub fn infos_wrapped_by<'a, F>( + text: &'a str, + font: &'a text::Font, + font_size: FontSize, + max_width: Scalar, + next_break_fn: F, +) -> Infos<'a, F> +where + F: for<'b> FnMut(&'b str, &'b text::Font, FontSize, Scalar) -> NextBreak, +{ + Infos { + text: text, + font: font, + font_size: font_size, + max_width: max_width, + next_break_fn: next_break_fn, + start_byte: 0, + start_char: 0, + last_break: None, + } +} + +/// Produce an `Infos` iterator that yields an `Info` for every line in the given text. +/// +/// The produced `Infos` iterator will not wrap the text, and only break each line via newline +/// characters within the text (either `\n` or `\r\n`). +pub fn infos<'a>( + text: &'a str, + font: &'a text::Font, + font_size: FontSize, +) -> Infos<'a, NextBreakFnPtr> { + fn no_wrap( + text: &str, + font: &text::Font, + font_size: FontSize, + _max_width: Scalar, + ) -> NextBreak { + next_break(text, font, font_size) + } + + infos_wrapped_by(text, font, font_size, std::f32::MAX, no_wrap) +} + +/// Simplify the retrieval of line information for text that may or may not be wrapped. +pub fn infos_maybe_wrapped<'a>( + text: &'a str, + font: &'a text::Font, + font_size: FontSize, + maybe_wrap: Option, + max_width: Scalar, +) -> Infos<'a, NextBreakFnPtr> { + match maybe_wrap { + None => infos(text, font, font_size), + Some(Wrap::Character) => infos(text, font, font_size).wrap_by_character(max_width), + Some(Wrap::Whitespace) => infos(text, font, font_size).wrap_by_whitespace(max_width), + } +} + +/// Produce an iterator yielding the bounding `Rect` for each line in the text. +/// +/// Yielded `Rect`s will begin with the top-left of the first line at a [0.0, 0.0]. +/// +/// This function assumes that `font_size` and `max_width` are the same as those used to produce +/// the `Info`s yielded by the `infos` Iterator. +pub fn rects( + mut infos: I, + font_size: FontSize, + max_width: Scalar, + x_align: text::Justify, + line_spacing: Scalar, +) -> Rects +where + I: Iterator, +{ + let first_rect = infos.next().map(|first_info| { + // Calculate the `x` `Range` of the first line `Rect`. + let x_bounds = Range::new(0.0, max_width); + let range = Range::new(0.0, first_info.width); + let x = match x_align { + text::Justify::Left => range.align_start_of(x_bounds), + text::Justify::Center => range.align_middle_of(x_bounds), + text::Justify::Right => range.align_end_of(x_bounds), + }; + let y_start = -(font_size as Scalar); + //let y_end = y_start + first_info.height; + let y_end = y_start + font_size as Scalar; + let y = Range::new(y_start, y_end); + Rect { x: x, y: y } + }); + Rects { + infos: infos, + next: first_rect, + x_align: x_align, + last_line_top: 0.0, + font_size: font_size, + line_spacing: line_spacing, + } +} + +/// Produces an iterator yielding a `Rect` for the selected range in each selected line in a block +/// of text. +/// +/// The yielded `Rect`s represent the selected range within each line of text. +/// +/// Lines that do not contain any selected text will be skipped. +pub fn selected_rects<'a, I>( + lines_with_rects: I, + font: &'a text::Font, + font_size: FontSize, + start: text::cursor::Index, + end: text::cursor::Index, +) -> SelectedRects<'a, I> +where + I: Iterator, +{ + SelectedRects { + selected_char_rects_per_line: text::glyph::selected_rects_per_line( + lines_with_rects, + font, + font_size, + start, + end, + ), + } +} + +impl<'a, F> Iterator for Infos<'a, F> +where + F: for<'b> FnMut(&'b str, &'b text::Font, FontSize, Scalar) -> NextBreak, +{ + type Item = Info; + fn next(&mut self) -> Option { + let Infos { + text, + font, + font_size, + max_width, + ref mut next_break_fn, + ref mut start_byte, + ref mut start_char, + ref mut last_break, + } = *self; + + let next = next_break_fn(&text[*start_byte..], font, font_size, max_width); + match next.break_ { + Break::Newline { .. } | Break::Wrap { .. } => { + let next_break = match next.break_ { + Break::Newline { + byte, + char, + len_bytes, + len_chars, + } => Break::Newline { + byte: *start_byte + byte, + char: *start_char + char, + len_bytes: len_bytes, + len_chars: len_chars, + }, + Break::Wrap { + byte, + char, + len_bytes, + len_chars, + } => Break::Wrap { + byte: *start_byte + byte, + char: *start_char + char, + len_bytes: len_bytes, + len_chars: len_chars, + }, + _ => unreachable!(), + }; + + let info = Info { + start_byte: *start_byte, + start_char: *start_char, + end_break: next_break, + width: next.width, + height: next.height, + }; + + match next.break_ { + Break::Newline { + byte, + char, + len_bytes, + len_chars, + } + | Break::Wrap { + byte, + char, + len_bytes, + len_chars, + } => { + // skip over end-of-break " ", "\n", or "\r\n" + *start_byte = info.start_byte + byte + len_bytes; + *start_char = info.start_char + char + len_chars; + } + _ => unreachable!(), + }; + *last_break = Some(next_break); + Some(info) + } + + Break::End { char, .. } => { + // if the last line ends in a new line, or the entire text is empty, return an + // empty line Info. + let empty_line = { + match *last_break { + Some(last_break_) => match last_break_ { + Break::Newline { .. } => true, + _ => false, + }, + None => true, + } + }; + if *start_byte < text.len() || empty_line { + let total_bytes = text.len(); + let total_chars = *start_char + char; + let end_break = Break::End { + byte: total_bytes, + char: total_chars, + }; + let info = Info { + start_byte: *start_byte, + start_char: *start_char, + end_break: end_break, + width: next.width, + height: next.height, + }; + *start_byte = total_bytes; + *start_char = total_chars; + *last_break = Some(end_break); + Some(info) + } else { + None + } + } + } + } +} + +impl Iterator for Rects +where + I: Iterator, +{ + type Item = Rect; + fn next(&mut self) -> Option { + let Rects { + ref mut next, + ref mut infos, + x_align, + ref mut last_line_top, + font_size, + line_spacing, + } = *self; + next.map(|line_rect| { + *next = infos.next().map(|info| { + let y = { + let line_top = *last_line_top - font_size as Scalar - line_spacing; + *last_line_top = line_top; + let y_start = line_top - font_size as Scalar; + //let y_end = y_start + info.height; + let y_end = y_start + font_size as Scalar; + Range::new(y_start, y_end) + }; + + let x = { + let range = Range::new(0.0, info.width); + match x_align { + text::Justify::Left => range.align_start_of(line_rect.x), + text::Justify::Center => range.align_middle_of(line_rect.x), + text::Justify::Right => range.align_end_of(line_rect.x), + } + }; + + Rect { x: x, y: y } + }); + + line_rect + }) + } +} + +impl<'a, I> Iterator for SelectedRects<'a, I> +where + I: Iterator, +{ + type Item = Rect; + fn next(&mut self) -> Option { + while let Some(mut rects) = self.selected_char_rects_per_line.next() { + if let Some((_, first_rect)) = rects.next() { + let total_selected_rect = rects.fold(first_rect, |mut total, (_, next)| { + total.x.end = next.x.end; + total + }); + return Some(total_selected_rect); + } + } + None + } +} diff --git a/bevy_nannou_render/src/text/mod.rs b/bevy_nannou_render/src/text/mod.rs new file mode 100644 index 000000000..0db7a2300 --- /dev/null +++ b/bevy_nannou_render/src/text/mod.rs @@ -0,0 +1,680 @@ +//! Text layout logic. +//! +//! Currently, this crate is used primarily by the `draw.text()` API but will also play an +//! important role in future GUI work. + +pub mod cursor; +pub mod font; +pub mod glyph; +pub mod layout; +pub mod line; +pub mod rt { + //! Re-exported RustType geometric types. + pub use rusttype::{gpu_cache, point, vector, Point, Rect, Vector}; +} + +// Re-export all relevant rusttype types here. +pub use self::layout::Layout; +pub use rusttype::gpu_cache::Cache as GlyphCache; +pub use rusttype::{Glyph, GlyphId, GlyphIter, LayoutIter, Scale, ScaledGlyph}; + +use nannou_core::geom; +use nannou_core::glam::Vec2; +use std::borrow::Cow; + +/// The RustType `FontCollection` type used by nannou. +pub type FontCollection = rusttype::FontCollection<'static>; +/// The RustType `Font` type used by nannou. +pub type Font = rusttype::Font<'static>; +/// The RustType `PositionedGlyph` type used by nannou. +pub type PositionedGlyph = rusttype::PositionedGlyph<'static>; + +/// The type used for scalar values. +pub type Scalar = nannou_core::geom::scalar::Default; + +/// The point type used when working with text. +pub type Point = nannou_core::geom::Point2; + +/// The type used to specify `FontSize` in font points. +pub type FontSize = u32; + +/// A context for building some **Text**. +pub struct Builder<'a> { + text: Cow<'a, str>, + layout_builder: layout::Builder, +} + +/// An instance of some multi-line text and its layout. +#[derive(Clone)] +pub struct Text<'a> { + text: Cow<'a, str>, + font: Font, + layout: Layout, + line_infos: Vec, + rect: geom::Rect, +} + +/// An iterator yielding each line within the given `text` as a new `&str`, where the start and end +/// indices into each line are provided by the given iterator. +#[derive(Clone)] +pub struct Lines<'a, I> { + text: &'a str, + ranges: I, +} + +/// An alias for the line info iterator yielded by `Text::line_infos`. +pub type TextLineInfos<'a> = line::Infos<'a, line::NextBreakFnPtr>; + +/// An alias for the line iterator yielded by `Text::lines`. +pub type TextLines<'a> = Lines< + 'a, + std::iter::Map, fn(&line::Info) -> std::ops::Range>, +>; + +/// An alias for the line rect iterator used internally within the `Text::line_rects` iterator. +type LineRects<'a> = line::Rects>>; + +/// An alias for the line rect iterator yielded by `Text::line_rects`. +#[derive(Clone)] +pub struct TextLineRects<'a> { + line_rects: LineRects<'a>, + offset: Vec2, +} + +/// An alias for the iterator yielded by `Text::lines_with_rects`. +pub type TextLinesWithRects<'a> = std::iter::Zip, TextLineRects<'a>>; + +/// An alias for the iterator yielded by `Text::glyphs_per_line`. +pub type TextGlyphsPerLine<'a> = glyph::RectsPerLine<'a, TextLinesWithRects<'a>>; + +/// An alias for the iterator yielded by `Text::glyphs`. +pub type TextGlyphs<'a> = std::iter::FlatMap< + TextGlyphsPerLine<'a>, + glyph::Rects<'a, 'a>, + fn(glyph::Rects<'a, 'a>) -> glyph::Rects<'a, 'a>, +>; + +/// Alignment along an axis. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] +pub enum Align { + Start, + Middle, + End, +} + +/// A type used for referring to typographic alignment of `Text`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Justify { + /// Align text to the start of the bounding `Rect`'s *x* axis. + Left, + /// Symmetrically align text along the *y* axis. + Center, + /// Align text to the end of the bounding `Rect`'s *x* axis. + Right, + // /// Align wrapped text to both the start and end of the bounding `Rect`s *x* axis. + // /// + // /// Extra space is added between words in order to achieve this alignment. + // TODO: Full, +} + +/// The way in which text should wrap around the width. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Wrap { + /// Wrap at the first character that exceeds the width. + Character, + /// Wrap at the first word that exceeds the width. + Whitespace, +} + +impl<'a> From> for Builder<'a> { + fn from(text: Cow<'a, str>) -> Self { + let layout_builder = Default::default(); + Builder { + text, + layout_builder, + } + } +} + +impl<'a> From<&'a str> for Builder<'a> { + fn from(s: &'a str) -> Self { + let text = Cow::Borrowed(s); + Self::from(text) + } +} + +impl From for Builder<'static> { + fn from(s: String) -> Self { + let text = Cow::Owned(s); + Self::from(text) + } +} + +impl<'a> Builder<'a> { + /// Apply the given function to the inner text layout. + fn map_layout(mut self, map: F) -> Self + where + F: FnOnce(layout::Builder) -> layout::Builder, + { + self.layout_builder = map(self.layout_builder); + self + } + + /// The font size to use for the text. + pub fn font_size(self, size: FontSize) -> Self { + self.map_layout(|l| l.font_size(size)) + } + + /// Specify whether or not text should be wrapped around some width and how to do so. + /// + /// The default value is `DEFAULT_LINE_WRAP`. + pub fn line_wrap(self, line_wrap: Option) -> Self { + self.map_layout(|l| l.line_wrap(line_wrap)) + } + + /// Specify that the **Text** should not wrap lines around the width. + /// + /// Shorthand for `builder.line_wrap(None)`. + pub fn no_line_wrap(self) -> Self { + self.map_layout(|l| l.no_line_wrap()) + } + + /// Line wrap the **Text** at the beginning of the first word that exceeds the width. + /// + /// Shorthand for `builder.line_wrap(Some(Wrap::Whitespace))`. + pub fn wrap_by_word(self) -> Self { + self.map_layout(|l| l.wrap_by_word()) + } + + /// Line wrap the **Text** at the beginning of the first character that exceeds the width. + /// + /// Shorthand for `builder.line_wrap(Some(Wrap::Character))`. + pub fn wrap_by_character(self) -> Self { + self.map_layout(|l| l.wrap_by_character()) + } + + /// A method for specifying the `Font` used for displaying the `Text`. + pub fn font(self, font: Font) -> Self { + self.map_layout(|l| l.font(font)) + } + + /// Describe the end along the *x* axis to which the text should be aligned. + pub fn justify(self, justify: Justify) -> Self { + self.map_layout(|l| l.justify(justify)) + } + + /// Align the text to the left of its bounding **Rect**'s *x* axis range. + pub fn left_justify(self) -> Self { + self.map_layout(|l| l.left_justify()) + } + + /// Align the text to the middle of its bounding **Rect**'s *x* axis range. + pub fn center_justify(self) -> Self { + self.map_layout(|l| l.center_justify()) + } + + /// Align the text to the right of its bounding **Rect**'s *x* axis range. + pub fn right_justify(self) -> Self { + self.map_layout(|l| l.right_justify()) + } + + /// Specify how much vertical space should separate each line of text. + pub fn line_spacing(self, spacing: Scalar) -> Self { + self.map_layout(|l| l.line_spacing(spacing)) + } + + /// Specify how the whole text should be aligned along the y axis of its bounding rectangle + pub fn y_align(self, align: Align) -> Self { + self.map_layout(|l| l.y_align(align)) + } + + /// Align the top edge of the text with the top edge of its bounding rectangle. + pub fn align_top(self) -> Self { + self.map_layout(|l| l.align_top()) + } + + /// Align the middle of the text with the middle of the bounding rect along the y axis.. + /// + /// This is the default behaviour. + pub fn align_middle_y(self) -> Self { + self.map_layout(|l| l.align_middle_y()) + } + + /// Align the bottom edge of the text with the bottom edge of its bounding rectangle. + pub fn align_bottom(self) -> Self { + self.map_layout(|l| l.align_bottom()) + } + + /// Set all the parameters via an existing `Layout` + pub fn layout(self, layout: &Layout) -> Self { + self.map_layout(|l| l.layout(layout)) + } + + /// Build the text. + /// + /// This iterates over the text in order to pre-calculates the text's multi-line information + /// using the `line::infos` function. + /// + /// The given `rect` will be used for applying the layout including text alignment, positioning + /// of text, multi-line wrapping, etc, + pub fn build(self, rect: geom::Rect) -> Text<'a> { + // let text = self.text; + // let layout = self.layout_builder.build(); + // #[allow(unreachable_code)] + // let font = layout.font.clone().unwrap_or_else(|| { + // #[cfg(feature = "notosans")] + // { + // return font::default_notosans(); + // } + // let assets = nannou_core::app::find_assets_path() + // .expect("failed to detect the assets directory when searching for a default font"); + // font::default(&assets).expect("failed to detect a default font") + // }); + // let max_width = rect.w(); + // let line_infos = + // line::infos_maybe_wrapped(&text, &font, layout.font_size, layout.line_wrap, max_width) + // .collect(); + // Text { + // text, + // font, + // layout, + // line_infos, + // rect, + // } + todo!() + } +} + +impl<'a> Text<'a> { + /// Produce an iterator yielding information about each line. + pub fn line_infos(&self) -> &[line::Info] { + &self.line_infos + } + + /// The full string of text as a slice. + pub fn text(&self) -> &str { + &self.text + } + + /// The layout parameters for this text instance. + pub fn layout(&self) -> &Layout { + &self.layout + } + + /// The font used for this text instance. + pub fn font(&self) -> &Font { + &self.font + } + + /// The number of lines in the text. + pub fn num_lines(&self) -> usize { + self.line_infos.len() + } + + /// The rectangle used to layout and build the text instance. + /// + /// This is the same `Rect` that was passed to the `text::Builder::build` method. + pub fn layout_rect(&self) -> geom::Rect { + self.rect + } + + /// The rectangle that describes the min and max bounds along each axis reached by the text. + pub fn bounding_rect(&self) -> geom::Rect { + let mut r = self.bounding_rect_by_lines(); + let info = match self.line_infos.first() { + None => return geom::Rect::from_w_h(0.0, 0.0), + Some(info) => info, + }; + let line_h = self.layout.font_size as Scalar; + r.y.end -= line_h - info.height; + r + } + + /// The rectangle that describes the min and max bounds along each axis reached by the text. + /// + /// This is similar to `bounding_rect` but assumes that all lines have a height equal to + /// `font_size`, rather than using the exact height. + pub fn bounding_rect_by_lines(&self) -> geom::Rect { + let mut lrs = self.line_rects(); + let lr = match lrs.next() { + None => return geom::Rect::from_w_h(0.0, 0.0), + Some(lr) => lr, + }; + lrs.fold(lr, |acc, lr| { + let x = geom::Range::new(acc.x.start.min(lr.x.start), acc.x.end.max(lr.x.end)); + let y = geom::Range::new(acc.y.start.min(lr.y.start), acc.y.end.max(lr.y.end)); + geom::Rect { x, y } + }) + } + + /// The width of the widest line of text. + pub fn width(&self) -> Scalar { + self.line_infos + .iter() + .fold(0.0, |max, info| max.max(info.width)) + } + + /// The exact height of the full text accounting for font size and line spacing.. + pub fn height(&self) -> Scalar { + let info = match self.line_infos.first() { + None => return 0.0, + Some(info) => info, + }; + exact_height( + info.height, + self.num_lines(), + self.layout.font_size, + self.layout.line_spacing, + ) + } + + /// Determine the total height of a block of text with the given number of lines, font size and + /// `line_spacing` (the space that separates each line of text). + /// + /// The height of all lines of text are assumed to match the `font_size`. If looking for the exact + /// height, see the `exact_height` function. + pub fn height_by_lines(&self) -> Scalar { + height_by_lines( + self.num_lines(), + self.layout.font_size, + self.layout.line_spacing, + ) + } + + /// Produce an iterator yielding each wrapped line within the **Text**. + pub fn lines(&self) -> TextLines { + fn info_byte_range(info: &line::Info) -> std::ops::Range { + info.byte_range() + } + lines(&self.text, self.line_infos.iter().map(info_byte_range)) + } + + /// The bounding rectangle for each line. + pub fn line_rects(&self) -> TextLineRects { + let offset = self.position_offset(); + let line_rects = line::rects( + self.line_infos.iter().cloned(), + self.layout.font_size, + self.rect.w(), + self.layout.justify, + self.layout.line_spacing, + ); + TextLineRects { line_rects, offset } + } + + /// Produce an iterator yielding all lines of text alongside their bounding rects. + pub fn lines_with_rects(&self) -> TextLinesWithRects { + self.lines().zip(self.line_rects()) + } + + /// Produce an iterator yielding iterators yielding every glyph alongside its bounding rect for + /// each line. + pub fn glyphs_per_line(&self) -> TextGlyphsPerLine { + glyph::rects_per_line(self.lines_with_rects(), &self.font, self.layout.font_size) + } + + /// Produce an iterator yielding every glyph alongside its bounding rect. + /// + /// This is the "flattened" version of the `glyphs_per_line` method. + pub fn glyphs(&self) -> TextGlyphs { + self.glyphs_per_line().flat_map(std::convert::identity) + } + + /// Produce an iterator yielding the path events for every glyph in every line. + pub fn path_events<'b>(&'b self) -> impl 'b + Iterator { + use lyon::path::PathEvent; + + // Translate the given lyon point by the given vector. + fn trans_lyon_point(p: &lyon::math::Point, v: Vec2) -> lyon::math::Point { + lyon::math::point(p.x + v.x, p.y + v.y) + } + + // Translate the given path event in 2D space. + fn trans_path_event(e: &PathEvent, v: Vec2) -> PathEvent { + match *e { + PathEvent::Begin { ref at } => PathEvent::Begin { + at: trans_lyon_point(at, v), + }, + PathEvent::Line { ref from, ref to } => PathEvent::Line { + from: trans_lyon_point(from, v), + to: trans_lyon_point(to, v), + }, + PathEvent::Quadratic { + ref from, + ref ctrl, + ref to, + } => PathEvent::Quadratic { + from: trans_lyon_point(from, v), + ctrl: trans_lyon_point(ctrl, v), + to: trans_lyon_point(to, v), + }, + PathEvent::Cubic { + ref from, + ref ctrl1, + ref ctrl2, + ref to, + } => PathEvent::Cubic { + from: trans_lyon_point(from, v), + ctrl1: trans_lyon_point(ctrl1, v), + ctrl2: trans_lyon_point(ctrl2, v), + to: trans_lyon_point(to, v), + }, + PathEvent::End { + ref last, + ref first, + ref close, + } => PathEvent::End { + last: trans_lyon_point(last, v), + first: trans_lyon_point(first, v), + close: *close, + }, + } + } + + self.glyphs().flat_map(|(g, r)| { + glyph::path_events(g) + .into_iter() + .flat_map(|es| es) + .map(move |e| trans_path_event(&e, r.bottom_left().into())) + }) + } + + /// Produce an iterator yielding positioned rusttype glyphs ready for caching. + /// + /// The window dimensions (in logical space) and scale_factor are required to transform glyph + /// positions into rusttype's pixel-space, ready for caching into the rusttype glyph cache + /// pixel buffer. + pub fn rt_glyphs<'b: 'a>( + &'b self, + window_size: Vec2, + scale_factor: Scalar, + ) -> impl 'a + 'b + Iterator { + rt_positioned_glyphs( + self.lines_with_rects(), + &self.font, + self.layout.font_size, + window_size, + scale_factor, + ) + } + + /// Converts this `Text` instance into an instance that owns the inner text string. + pub fn into_owned(self) -> Text<'static> { + let Text { + text, + font, + layout, + line_infos, + rect, + } = self; + let text = Cow::Owned(text.into_owned()); + Text { + text, + font, + layout, + line_infos, + rect, + } + } + + fn position_offset(&self) -> Vec2 { + position_offset( + self.num_lines(), + self.layout.font_size, + self.layout.line_spacing, + self.rect, + self.layout.y_align, + ) + } +} + +impl<'a, I> Iterator for Lines<'a, I> +where + I: Iterator>, +{ + type Item = &'a str; + fn next(&mut self) -> Option { + let Lines { + text, + ref mut ranges, + } = *self; + ranges.next().map(|range| &text[range]) + } +} + +impl<'a> Iterator for TextLineRects<'a> { + type Item = geom::Rect; + fn next(&mut self) -> Option { + self.line_rects.next().map(|r| r.shift(self.offset.into())) + } +} + +/// Determine the total height of a block of text with the given number of lines, font size and +/// `line_spacing` (the space that separates each line of text). +/// +/// The height of all lines of text are assumed to match the `font_size`. If looking for the exact +/// height, see the `exact_height` function. +pub fn height_by_lines(num_lines: usize, font_size: FontSize, line_spacing: Scalar) -> Scalar { + if num_lines > 0 { + num_lines as Scalar * font_size as Scalar + (num_lines - 1) as Scalar * line_spacing + } else { + 0.0 + } +} + +/// Determine the exact height of a block of text. +/// +/// The `first_line_height` can be retrieved via its `line::Info` which can be retrieved via the +/// first element of a `line_infos` iterator. +pub fn exact_height( + first_line_height: Scalar, + num_lines: usize, + font_size: FontSize, + line_spacing: Scalar, +) -> Scalar { + if num_lines > 0 { + let lt_num_lines = num_lines - 1; + let other_lines_height = lt_num_lines as Scalar * font_size as Scalar; + let space_height = lt_num_lines as Scalar * line_spacing; + first_line_height + other_lines_height + space_height + } else { + 0.0 + } +} + +/// Produce an iterator yielding each line within the given `text` as a new `&str`, where the +/// start and end indices into each line are provided by the given iterator. +pub fn lines(text: &str, ranges: I) -> Lines +where + I: Iterator>, +{ + Lines { + text: text, + ranges: ranges, + } +} + +/// The position offset required to shift the associated text into the given bounding rectangle. +/// +/// This function assumes the `max_width` used to produce the `line_infos` is equal to the given +/// `bounding_rect` max width. +pub fn position_offset( + num_lines: usize, + font_size: FontSize, + line_spacing: f32, + bounding_rect: geom::Rect, + y_align: Align, +) -> Vec2 { + let x_offset = bounding_rect.x.start; + let y_offset = { + // Calculate the `y` `Range` of the first line `Rect`. + let total_text_height = height_by_lines(num_lines, font_size, line_spacing); + let total_text_y_range = geom::Range::new(0.0, total_text_height); + let total_text_y = match y_align { + Align::Start => total_text_y_range.align_start_of(bounding_rect.y), + Align::Middle => total_text_y_range.align_middle_of(bounding_rect.y), + Align::End => total_text_y_range.align_end_of(bounding_rect.y), + }; + total_text_y.end + }; + geom::vec2(x_offset, y_offset) +} + +/// Produce the position of each glyph ready for the rusttype glyph cache. +/// +/// Window dimensions are expected in logical coordinates. +pub fn rt_positioned_glyphs<'a, I>( + lines_with_rects: I, + font: &'a Font, + font_size: FontSize, + window_size: Vec2, + scale_factor: Scalar, +) -> impl 'a + Iterator +where + I: IntoIterator, + I::IntoIter: 'a, +{ + // Functions for converting nannou coordinates to rusttype pixel coordinates. + let trans_x = move |x: Scalar| (x + window_size.x / 2.0) * scale_factor as Scalar; + let trans_y = move |y: Scalar| ((-y) + window_size.y / 2.0) * scale_factor as Scalar; + + // Clear the existing glyphs and fill the buffer with glyphs for this Text. + let scale = f32_pt_to_scale(font_size as f32 * scale_factor); + lines_with_rects + .into_iter() + .flat_map(move |(line, line_rect)| { + let (x, y) = ( + trans_x(line_rect.left()) as f32, + trans_y(line_rect.bottom()) as f32, + ); + let point = rt::Point { x: x, y: y }; + font.layout(line, scale, point).map(|g| g.standalone()) + }) +} + +/// Converts the given font size in "points" to its font size in pixels. +/// This is useful for when the font size is not an integer. +pub fn f32_pt_to_px(font_size_in_points: f32) -> f32 { + font_size_in_points * 4.0 / 3.0 +} + +/// Converts the given font size in "points" to a uniform `rusttype::Scale`. +/// This is useful for when the font size is not an integer. +pub fn f32_pt_to_scale(font_size_in_points: f32) -> Scale { + Scale::uniform(f32_pt_to_px(font_size_in_points)) +} + +/// Converts the given font size in "points" to its font size in pixels. +pub fn pt_to_px(font_size_in_points: FontSize) -> f32 { + f32_pt_to_px(font_size_in_points as f32) +} + +/// Converts the given font size in "points" to a uniform `rusttype::Scale`. +pub fn pt_to_scale(font_size_in_points: FontSize) -> Scale { + Scale::uniform(pt_to_px(font_size_in_points)) +} + +/// Begin building a **Text** instance. +pub fn text(s: &str) -> Builder { + Builder::from(s) +} diff --git a/bevy_nannou_wgpu/Cargo.toml b/bevy_nannou_wgpu/Cargo.toml index 02eae7eac..939489a4d 100644 --- a/bevy_nannou_wgpu/Cargo.toml +++ b/bevy_nannou_wgpu/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" [dependencies] bevy = { workspace = true } +# We use bevy's mirrored wgpu types almost always, but in a few rare cases, we need to use wgpu types directly +wgpu-types = "0.17" \ No newline at end of file diff --git a/bevy_nannou_wgpu/src/bind_group_builder.rs b/bevy_nannou_wgpu/src/bind_group_builder.rs new file mode 100644 index 000000000..3f20b06e9 --- /dev/null +++ b/bevy_nannou_wgpu/src/bind_group_builder.rs @@ -0,0 +1,216 @@ +use bevy::prelude::*; +use bevy::render::render_resource as wgpu; +use bevy::render::renderer::RenderDevice; + +/// A type aimed at simplifying the creation of a bind group layout. +#[derive(Debug, Default)] +pub struct BindGroupLayoutBuilder { + bindings: Vec<(wgpu::ShaderStages, wgpu::BindingType)>, +} + +/// Simplified creation of a bind group. +#[derive(Debug, Default)] +pub struct BindGroupBuilder<'a> { + resources: Vec>, +} + +impl BindGroupLayoutBuilder { + /// Begin building the bind group layout. + pub fn new() -> Self { + Self::default() + } + + /// Specify a new binding. + /// + /// The `binding` position of each binding will be inferred as the index within the order that + /// they are added to this builder type. If you require manually specifying the binding + /// location, you may be better off not using the `BindGroupLayoutBuilder` and instead + /// constructing the `BindGroupLayout` and `BindGroup` manually. + pub fn binding(mut self, visibility: wgpu::ShaderStages, ty: wgpu::BindingType) -> Self { + self.bindings.push((visibility, ty)); + self + } + + /// Add a uniform buffer binding to the layout. + pub fn uniform_buffer(self, visibility: wgpu::ShaderStages, has_dynamic_offset: bool) -> Self { + let ty = wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset, + // wgpu 0.5-0.6 TODO: potential perf hit, investigate this field + min_binding_size: None, + }; + self.binding(visibility, ty) + } + + /// Add a storage buffer binding to the layout. + pub fn storage_buffer( + self, + visibility: wgpu::ShaderStages, + has_dynamic_offset: bool, + read_only: bool, + ) -> Self { + let ty = wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only }, + has_dynamic_offset, + // wgpu 0.5-0.6 TODO: potential perf hit, investigate this field + min_binding_size: None, + }; + self.binding(visibility, ty) + } + + /// Add a sampler binding to the layout. + pub fn sampler(self, visibility: wgpu::ShaderStages, filtering: bool) -> Self { + let ty = wgpu::BindingType::Sampler(if filtering { + wgpu::SamplerBindingType::Filtering + } else { + wgpu::SamplerBindingType::NonFiltering + }); + self.binding(visibility, ty) + } + + /// Add a sampler binding to the layout. + pub fn comparison_sampler(self, visibility: wgpu::ShaderStages) -> Self { + let ty = wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison); + self.binding(visibility, ty) + } + + /// Add a texture binding to the layout. + pub fn texture( + self, + visibility: wgpu::ShaderStages, + multisampled: bool, + view_dimension: wgpu::TextureViewDimension, + sample_type: wgpu::TextureSampleType, + ) -> Self { + // fix sample type in certain scenarios (constraint given by wgpu) + let sample_type = if multisampled + && matches!( + sample_type, + wgpu::TextureSampleType::Float { filterable: true } + ) { + wgpu::TextureSampleType::Float { filterable: false } + } else { + sample_type + }; + let ty = wgpu::BindingType::Texture { + multisampled, + view_dimension, + sample_type, + }; + self.binding(visibility, ty) + } + + /// Add a storage texture binding to the layout. + pub fn storage_texture( + self, + visibility: wgpu::ShaderStages, + format: wgpu::TextureFormat, + view_dimension: wgpu::TextureViewDimension, + access: wgpu::StorageTextureAccess, + ) -> Self { + let ty = wgpu::BindingType::StorageTexture { + view_dimension, + format, + access, + }; + self.binding(visibility, ty) + } + + /// Build the bind group layout from the specified parameters. + pub fn build(self, device: &RenderDevice) -> wgpu::BindGroupLayout { + let mut entries = Vec::with_capacity(self.bindings.len()); + for (i, (visibility, ty)) in self.bindings.into_iter().enumerate() { + let layout_binding = wgpu::BindGroupLayoutEntry { + binding: i as u32, + visibility, + ty, + count: None, + }; + entries.push(layout_binding); + } + + let label = Some("nannou bind group layout"); + let desc = wgpu::BindGroupLayoutDescriptor { label, entries: &entries }; + device.create_bind_group_layout(&desc) + } +} + +impl<'a> BindGroupBuilder<'a> { + /// Begin building the bind group. + pub fn new() -> Self { + Self::default() + } + + /// Specify a new binding. + /// + /// The `binding` position of each binding will be inferred as the index within the order that + /// they are added to this builder type. If you require manually specifying the binding + /// location, you may be better off not using the `BindGroupBuilder` and instead constructing + /// the `BindGroupLayout` and `BindGroup` manually. + pub fn binding(mut self, resource: wgpu::BindingResource<'a>) -> Self { + self.resources.push(resource); + self + } + + /// Specify a slice of a buffer to be bound. + /// + /// The given `range` represents the start and end point of the buffer to be bound in bytes. + pub fn buffer_bytes( + self, + buffer: &'a wgpu::Buffer, + offset: wgpu::BufferAddress, + size: Option, + ) -> Self { + let resource = wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer, + offset, + size, + }); + self.binding(resource) + } + + /// Specify a slice of a buffer of elements of type `T` to be bound. + /// + /// This method is similar to `buffer_bytes`, but expects a range of **elements** rather than a + /// range of **bytes**. + /// + /// Type `T` *must* be either `#[repr(C)]` or `#[repr(transparent)]`. + // NOTE: We might want to change this to match the wgpu API by using a NonZeroU64 for size. + pub fn buffer(self, buffer: &'a wgpu::Buffer, range: std::ops::Range) -> Self + // TODO: why was this necessary? + // where + // T: Copy, + { + let size_bytes = std::mem::size_of::() as wgpu::BufferAddress; + let start = range.start as wgpu::BufferAddress * size_bytes; + let end = range.end as wgpu::BufferAddress * size_bytes; + let size = std::num::NonZeroU64::new(end - start).expect("buffer slice must not be empty"); + self.buffer_bytes(buffer, start, Some(size)) + } + + /// Specify a sampler to be bound. + pub fn sampler(self, sampler: &'a wgpu::Sampler) -> Self { + let resource = wgpu::BindingResource::Sampler(sampler); + self.binding(resource) + } + + /// Specify a texture view to be bound. + pub fn texture_view(self, view: &'a wgpu::TextureView) -> Self { + let resource = wgpu::BindingResource::TextureView(view); + self.binding(resource) + } + + /// Build the bind group with the specified resources. + pub fn build(self, device: &RenderDevice, layout: &wgpu::BindGroupLayout) -> wgpu::BindGroup { + let mut entries = Vec::with_capacity(self.resources.len()); + for (i, resource) in self.resources.into_iter().enumerate() { + let binding = wgpu::BindGroupEntry { + binding: i as u32, + resource, + }; + entries.push(binding); + } + let label = Some("nannou bind group"); + device.create_bind_group(label, &layout, &entries) + } +} diff --git a/bevy_nannou_wgpu/src/lib.rs b/bevy_nannou_wgpu/src/lib.rs index 3ccd53493..a75441858 100644 --- a/bevy_nannou_wgpu/src/lib.rs +++ b/bevy_nannou_wgpu/src/lib.rs @@ -1,4 +1,28 @@ +mod render_pipeline_builder; +mod bind_group_builder; +mod sampler_builder; +mod texture; +mod render_pass; + +pub use render_pipeline_builder::RenderPipelineBuilder; +pub use bind_group_builder::{BindGroupBuilder, BindGroupLayoutBuilder}; +pub use texture::TextureBuilder; +pub use sampler_builder::{SamplerBuilder}; +pub use render_pass::{RenderPassBuilder, ColorAttachmentDescriptorBuilder, DepthStencilAttachmentDescriptorBuilder}; + use bevy::prelude::*; +use bevy::render::render_resource as wgpu; + +/// Whether or not the sampler descriptor describes a sampler that might perform linear filtering. +/// +/// This is used to determine the `filtering` field for the sampler binding type variant which +/// assists wgpu with validation. +pub fn sampler_filtering(desc: &wgpu::SamplerDescriptor) -> bool { + match (desc.mag_filter, desc.min_filter, desc.mipmap_filter) { + (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest) => false, + _ => true, + } +} //TODO: Does this need to be a plugin to inject anything into the ECS? Or just utils? struct NannouWgpuPlugin; diff --git a/bevy_nannou_wgpu/src/render_pass.rs b/bevy_nannou_wgpu/src/render_pass.rs new file mode 100644 index 000000000..f3f963c8d --- /dev/null +++ b/bevy_nannou_wgpu/src/render_pass.rs @@ -0,0 +1,225 @@ +use wgpu_types::Color; +use bevy::prelude::*; +use bevy::render::render_phase::TrackedRenderPass; +use bevy::render::render_resource as wgpu; +use bevy::render::renderer::RenderContext; + +/// A builder type to simplify the process of creating a render pass descriptor. +#[derive(Debug, Default)] +pub struct RenderPassBuilder<'a> { + color_attachments: Vec>>, + depth_stencil_attachment: Option>, +} + +/// A builder type to simplify the process of creating a render pass descriptor. +#[derive(Debug)] +pub struct ColorAttachmentDescriptorBuilder<'a> { + descriptor: wgpu::RenderPassColorAttachment<'a>, +} + +/// A builder type to simplify the process of creating a render pass descriptor. +#[derive(Debug)] +pub struct DepthStencilAttachmentDescriptorBuilder<'a> { + descriptor: wgpu::RenderPassDepthStencilAttachment<'a>, +} + +impl<'a> ColorAttachmentDescriptorBuilder<'a> { + pub const DEFAULT_CLEAR_COLOR: Color = Color::TRANSPARENT; + pub const DEFAULT_LOAD_OP: wgpu::LoadOp = wgpu::LoadOp::Clear(Self::DEFAULT_CLEAR_COLOR); + pub const DEFAULT_STORE_OP: bool = true; + + /// Begin building a new render pass color attachment descriptor. + fn new(attachment: &'a wgpu::TextureView) -> Self { + ColorAttachmentDescriptorBuilder { + descriptor: wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(Color::TRANSPARENT), + store: true, + }, + }, + } + } + + /// Specify the resolve target for this render pass color attachment. + pub fn resolve_target(mut self, target: Option<&'a wgpu::TextureView>) -> Self { + self.descriptor.resolve_target = target.map(|t| &**t); + self + } + // + // /// Specify the resolve target for this render pass color attachment. + // pub fn resolve_target_handle(mut self, target: Option<&'a wgpu::TextureView>) -> Self { + // self.descriptor.resolve_target = target; + // self + // } + + /// The beginning-of-pass load operation for this color attachment. + pub fn load_op(mut self, load_op: wgpu::LoadOp) -> Self { + self.descriptor.ops.load = load_op; + self + } + + // /// The end-of-pass store operation for this color attachment. + // pub fn store_op(mut self, store_op: bool::Store) -> Self { + // self.descriptor.ops.store = store_op; + // self + // } +} + +impl<'a> DepthStencilAttachmentDescriptorBuilder<'a> { + pub const DEFAULT_DEPTH_LOAD_OP: wgpu::LoadOp = + wgpu::LoadOp::Clear(Self::DEFAULT_CLEAR_DEPTH); + pub const DEFAULT_DEPTH_STORE_OP: bool = true; + pub const DEFAULT_CLEAR_DEPTH: f32 = 1.0; + pub const DEFAULT_STENCIL_LOAD_OP: wgpu::LoadOp = + wgpu::LoadOp::Clear(Self::DEFAULT_CLEAR_STENCIL); + pub const DEFAULT_STENCIL_STORE_OP: bool = true; + pub const DEFAULT_CLEAR_STENCIL: u32 = 0; + + fn new(attachment: &'a wgpu::TextureView) -> Self { + DepthStencilAttachmentDescriptorBuilder { + descriptor: wgpu::RenderPassDepthStencilAttachment { + view: attachment, + depth_ops: Some(wgpu::Operations { + load: Self::DEFAULT_DEPTH_LOAD_OP, + store: Self::DEFAULT_DEPTH_STORE_OP, + }), + stencil_ops: Some(wgpu::Operations { + load: Self::DEFAULT_STENCIL_LOAD_OP, + store: Self::DEFAULT_STENCIL_STORE_OP, + }), + }, + } + } + + /// The beginning-of-pass load operation for this depth attachment. + pub fn depth_load_op(mut self, load: wgpu::LoadOp) -> Self { + self.descriptor.depth_ops = Some(wgpu::Operations { + load, + store: self.descriptor.depth_ops.expect("no depth ops field").store, + }); + self + } + + /// The end-of-pass store operation for this depth attachment. + pub fn depth_store_op(mut self, store: bool) -> Self { + self.descriptor.depth_ops = Some(wgpu::Operations { + load: self.descriptor.depth_ops.expect("no depth ops field").load, + store, + }); + self + } + + /// The beginning-of-pass load operation for this stencil attachment. + pub fn stencil_load_op(mut self, load: wgpu::LoadOp) -> Self { + self.descriptor.stencil_ops = Some(wgpu::Operations { + load, + store: self + .descriptor + .stencil_ops + .expect("no stencil ops field") + .store, + }); + self + } + + /// The end-of-pass store operation for this stencil attachment. + pub fn stencil_store_op(mut self, store: bool) -> Self { + self.descriptor.stencil_ops = Some(wgpu::Operations { + load: self + .descriptor + .stencil_ops + .expect("no stencil ops field") + .load, + store, + }); + self + } +} + +impl<'a> RenderPassBuilder<'a> { + pub const DEFAULT_COLOR_LOAD_OP: wgpu::LoadOp = + ColorAttachmentDescriptorBuilder::DEFAULT_LOAD_OP; + pub const DEFAULT_COLOR_STORE_OP: bool = ColorAttachmentDescriptorBuilder::DEFAULT_STORE_OP; + pub const DEFAULT_CLEAR_COLOR: Color = ColorAttachmentDescriptorBuilder::DEFAULT_CLEAR_COLOR; + pub const DEFAULT_DEPTH_LOAD_OP: wgpu::LoadOp = + DepthStencilAttachmentDescriptorBuilder::DEFAULT_DEPTH_LOAD_OP; + pub const DEFAULT_DEPTH_STORE_OP: bool = + DepthStencilAttachmentDescriptorBuilder::DEFAULT_DEPTH_STORE_OP; + pub const DEFAULT_CLEAR_DEPTH: f32 = + DepthStencilAttachmentDescriptorBuilder::DEFAULT_CLEAR_DEPTH; + pub const DEFAULT_STENCIL_LOAD_OP: wgpu::LoadOp = + DepthStencilAttachmentDescriptorBuilder::DEFAULT_STENCIL_LOAD_OP; + pub const DEFAULT_STENCIL_STORE_OP: bool = + DepthStencilAttachmentDescriptorBuilder::DEFAULT_STENCIL_STORE_OP; + pub const DEFAULT_CLEAR_STENCIL: u32 = + DepthStencilAttachmentDescriptorBuilder::DEFAULT_CLEAR_STENCIL; + + /// Begin building a new render pass descriptor. + pub fn new() -> Self { + Self::default() + } + + /// Add a single color attachment descriptor to the render pass descriptor. + /// + /// Call this multiple times in succession to add multiple color attachments. + pub fn color_attachment( + mut self, + attachment: &'a wgpu::TextureView, + color_builder: F, + ) -> Self + where + F: FnOnce(ColorAttachmentDescriptorBuilder<'a>) -> ColorAttachmentDescriptorBuilder<'a>, + { + let builder = ColorAttachmentDescriptorBuilder::new(attachment); + let descriptor = color_builder(builder).descriptor; + self.color_attachments.push(Some(descriptor)); + self + } + + /// Add a depth stencil attachment to the render pass. + /// + /// This should only be called once, as only a single depth stencil attachment is valid. Only + /// the attachment submitted last will be used. + pub fn depth_stencil_attachment( + mut self, + attachment: &'a wgpu::TextureView, + depth_stencil_builder: F, + ) -> Self + where + F: FnOnce( + DepthStencilAttachmentDescriptorBuilder<'a>, + ) -> DepthStencilAttachmentDescriptorBuilder<'a>, + { + let builder = DepthStencilAttachmentDescriptorBuilder::new(attachment); + let descriptor = depth_stencil_builder(builder).descriptor; + self.depth_stencil_attachment = Some(descriptor); + self + } + + /// Return the built color and depth attachments. + pub fn into_inner( + self, + ) -> ( + Vec>>, + Option>, + ) { + let RenderPassBuilder { + color_attachments, + depth_stencil_attachment, + } = self; + (color_attachments, depth_stencil_attachment) + } + + /// Begin a render pass with the specified parameters on the given encoder. + pub fn begin(self, render_context: &'a mut RenderContext) -> TrackedRenderPass<'a> { + let (color_attachments, depth_stencil_attachment) = self.into_inner(); + let descriptor = wgpu::RenderPassDescriptor { + label: Some("nannou_render_pass"), + color_attachments: &color_attachments, + depth_stencil_attachment, + }; + render_context.begin_tracked_render_pass(descriptor) + } +} diff --git a/bevy_nannou_wgpu/src/render_pipeline_builder.rs b/bevy_nannou_wgpu/src/render_pipeline_builder.rs new file mode 100644 index 000000000..bbcb5323f --- /dev/null +++ b/bevy_nannou_wgpu/src/render_pipeline_builder.rs @@ -0,0 +1,510 @@ +//! Items aimed at easing the contruction of a render pipeline. +//! +//! Creating a `RenderPipeline` tends to involve a lot of boilerplate that we don't always want to +//! have to consider when writing graphics code. Here we definGe a set of helpers that allow us to +//! simplify the process and fall back to a set of reasonable defaults. +use std::borrow::Cow; +use bevy::prelude::*; +use bevy::render::render_resource as wgpu; + +/// Types that may be directly converted into a pipeline layout descriptor. + +pub trait IntoPipelineLayoutDescriptor<'a> { + /// Convert the type into a pipeline layout descriptor. + fn into_pipeline_layout_descriptor(self) -> wgpu::PipelineLayoutDescriptor<'a>; +} + +/// A builder type to help simplify the construction of a **RenderPipeline**. +/// +/// We've attempted to provide a suite of reasonable defaults in the case that none are provided. +#[derive(Debug)] +pub struct RenderPipelineBuilder<'a> { + layout: &'a [wgpu::BindGroupLayout], + vs_mod: Handle, + fs_mod: Option>, + vs_entry_point: &'a str, + fs_entry_point: &'a str, + primitive: wgpu::PrimitiveState, + color_state: Option, + color_states: &'a [Option], + depth_stencil: Option, + vertex_buffers: Vec, + multisample: wgpu::MultisampleState, +} + +impl<'a> RenderPipelineBuilder<'a> { + // The default entry point used for shaders when unspecified. + pub const DEFAULT_SHADER_ENTRY_POINT: &'static str = "main"; + + // Primitive state. + pub const DEFAULT_FRONT_FACE: wgpu::FrontFace = wgpu::FrontFace::Ccw; + pub const DEFAULT_CULL_MODE: Option = None; + pub const DEFAULT_POLYGON_MODE: wgpu::PolygonMode = wgpu::PolygonMode::Fill; + pub const DEFAULT_PRIMITIVE_TOPOLOGY: wgpu::PrimitiveTopology = + wgpu::PrimitiveTopology::TriangleList; + pub const DEFAULT_PRIMITIVE: wgpu::PrimitiveState = wgpu::PrimitiveState { + topology: Self::DEFAULT_PRIMITIVE_TOPOLOGY, + strip_index_format: None, + front_face: Self::DEFAULT_FRONT_FACE, + cull_mode: Self::DEFAULT_CULL_MODE, + polygon_mode: Self::DEFAULT_POLYGON_MODE, + unclipped_depth: Self::DEFAULT_UNCLIPPED_DEPTH, + conservative: false, + }; + + // Color state defaults. + pub const DEFAULT_COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; + pub const DEFAULT_COLOR_BLEND: wgpu::BlendComponent = wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }; + pub const DEFAULT_ALPHA_BLEND: wgpu::BlendComponent = wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }; + pub const DEFAULT_COLOR_WRITE: wgpu::ColorWrites = wgpu::ColorWrites::ALL; + pub const DEFAULT_BLEND_STATE: wgpu::BlendState = wgpu::BlendState { + color: Self::DEFAULT_COLOR_BLEND, + alpha: Self::DEFAULT_ALPHA_BLEND, + }; + pub const DEFAULT_COLOR_STATE: wgpu::ColorTargetState = wgpu::ColorTargetState { + format: Self::DEFAULT_COLOR_FORMAT, + blend: Some(Self::DEFAULT_BLEND_STATE), + write_mask: Self::DEFAULT_COLOR_WRITE, + }; + + // Depth state defaults. + pub const DEFAULT_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + pub const DEFAULT_DEPTH_WRITE_ENABLED: bool = true; + pub const DEFAULT_DEPTH_COMPARE: wgpu::CompareFunction = wgpu::CompareFunction::LessEqual; + pub const DEFAULT_STENCIL_FRONT: wgpu::StencilFaceState = wgpu::StencilFaceState::IGNORE; + pub const DEFAULT_STENCIL_BACK: wgpu::StencilFaceState = wgpu::StencilFaceState::IGNORE; + pub const DEFAULT_STENCIL_READ_MASK: u32 = 0; + pub const DEFAULT_STENCIL_WRITE_MASK: u32 = 0; + pub const DEFAULT_STENCIL: wgpu::StencilState = wgpu::StencilState { + front: Self::DEFAULT_STENCIL_FRONT, + back: Self::DEFAULT_STENCIL_BACK, + read_mask: Self::DEFAULT_STENCIL_READ_MASK, + write_mask: Self::DEFAULT_STENCIL_WRITE_MASK, + }; + pub const DEFAULT_DEPTH_BIAS_CONSTANT: i32 = 0; + pub const DEFAULT_DEPTH_BIAS_SLOPE_SCALE: f32 = 0.0; + pub const DEFAULT_DEPTH_BIAS_CLAMP: f32 = 0.0; + pub const DEFAULT_DEPTH_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { + constant: Self::DEFAULT_DEPTH_BIAS_CONSTANT, + slope_scale: Self::DEFAULT_DEPTH_BIAS_SLOPE_SCALE, + clamp: Self::DEFAULT_DEPTH_BIAS_CLAMP, + }; + pub const DEFAULT_UNCLIPPED_DEPTH: bool = false; + pub const DEFAULT_DEPTH_STENCIL: wgpu::DepthStencilState = wgpu::DepthStencilState { + format: Self::DEFAULT_DEPTH_FORMAT, + depth_write_enabled: Self::DEFAULT_DEPTH_WRITE_ENABLED, + depth_compare: Self::DEFAULT_DEPTH_COMPARE, + stencil: Self::DEFAULT_STENCIL, + bias: Self::DEFAULT_DEPTH_BIAS, + }; + + // Multisample state. + pub const DEFAULT_SAMPLE_COUNT: u32 = 1; + pub const DEFAULT_SAMPLE_MASK: u64 = !0; + pub const DEFAULT_ALPHA_TO_COVERAGE_ENABLED: bool = false; + pub const DEFAULT_MULTISAMPLE: wgpu::MultisampleState = wgpu::MultisampleState { + count: Self::DEFAULT_SAMPLE_COUNT, + mask: Self::DEFAULT_SAMPLE_MASK, + alpha_to_coverage_enabled: Self::DEFAULT_ALPHA_TO_COVERAGE_ENABLED, + }; + + // Constructors + + pub fn from_layout(layout: &'a [wgpu::BindGroupLayout], vs_mod: Handle) -> Self { + Self::new_inner(layout, vs_mod) + } + + // Shared between constructors. + fn new_inner(layout: &'a [wgpu::BindGroupLayout], vs_mod: Handle) -> Self { + RenderPipelineBuilder { + layout, + vs_mod, + fs_mod: None, + vs_entry_point: Self::DEFAULT_SHADER_ENTRY_POINT, + fs_entry_point: Self::DEFAULT_SHADER_ENTRY_POINT, + color_state: None, + color_states: &[], + primitive: Self::DEFAULT_PRIMITIVE, + depth_stencil: None, + vertex_buffers: vec![], + multisample: Self::DEFAULT_MULTISAMPLE, + } + } + + // Builders + + /// The name of the entry point in the compiled shader. + /// + /// There must be a function that returns void with this name in the shader. + pub fn vertex_entry_point(mut self, entry_point: &'a str) -> Self { + self.vs_entry_point = entry_point; + self + } + + /// The name of the entry point in the compiled shader. + /// + /// There must be a function that returns void with this name in the shader. + pub fn fragment_entry_point(mut self, entry_point: &'a str) -> Self { + self.fs_entry_point = entry_point; + self + } + + /// Specify a compiled fragment shader for the render pipeline. + pub fn fragment_shader(mut self, fs_mod: Handle) -> Self { + self.fs_mod = Some(fs_mod); + self + } + + // Primitive state. + + /// Specify the full primitive state. + /// + /// Describes the state of primitive assembly and rasterization in a render pipeline. + pub fn primitive(mut self, p: wgpu::PrimitiveState) -> Self { + self.primitive = p; + self + } + + /// The face to consider the front for the purpose of culling and stencil operations. + pub fn front_face(mut self, front_face: wgpu::FrontFace) -> Self { + self.primitive.front_face = front_face; + self + } + + /// The face culling mode. + pub fn cull_mode(mut self, cull_mode: Option) -> Self { + self.primitive.cull_mode = cull_mode; + self + } + + /// Specify the primitive topology. + /// + /// This represents the way vertices will be read from the **VertexBuffer**. + pub fn primitive_topology(mut self, topology: wgpu::PrimitiveTopology) -> Self { + self.primitive.topology = topology; + self + } + + /// Controls the way each polygon is rasterized. Can be either `Fill` (default), `Line` or + /// `Point`. + /// + /// Setting this to something other than `Fill` requires `Features::NON_FILL_POLYGON_MODE` to + /// be enabled. + pub fn polygon_mode(mut self, mode: wgpu::PolygonMode) -> Self { + self.primitive.polygon_mode = mode; + self + } + + // Color state. + + /// Specify the full color state for drawing to the output attachment. + /// + /// If you have multiple output attachments, see the `color_states` method. + pub fn color_state(mut self, state: wgpu::ColorTargetState) -> Self { + self.color_state = Some(state); + self + } + + /// The texture formrat of the image that this pipelinew ill render to. + /// + /// Must match the format of the corresponding color attachment. + pub fn color_format(mut self, format: wgpu::TextureFormat) -> Self { + let state = self.color_state.get_or_insert(Self::DEFAULT_COLOR_STATE); + state.format = format; + self + } + + /// The color blending used for this pipeline. + pub fn color_blend(mut self, color_blend: wgpu::BlendComponent) -> Self { + let state = self.color_state.get_or_insert(Self::DEFAULT_COLOR_STATE); + let blend = state.blend.get_or_insert(Self::DEFAULT_BLEND_STATE); + blend.color = color_blend; + + self + } + + /// The alpha blending used for this pipeline. + pub fn alpha_blend(mut self, alpha_blend: wgpu::BlendComponent) -> Self { + let state = self.color_state.get_or_insert(Self::DEFAULT_COLOR_STATE); + let blend = state.blend.get_or_insert(Self::DEFAULT_BLEND_STATE); + blend.alpha = alpha_blend; + + self + } + + /// Mask which enables/disables writes to different color/alpha channel. + pub fn write_mask(mut self, mask: wgpu::ColorWrites) -> Self { + let state = self.color_state.get_or_insert(Self::DEFAULT_COLOR_STATE); + state.write_mask = mask; + self + } + + // Depth / Stencil state + + /// Specify the full depth stencil state. + pub fn depth_stencil(mut self, state: wgpu::DepthStencilState) -> Self { + self.depth_stencil = Some(state); + self + } + + /// Format of the depth/stencil buffer. Must be one of the depth formats. Must match the format + /// of the depth/stencil attachment. + pub fn depth_format(mut self, format: wgpu::TextureFormat) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.format = format; + self + } + + pub fn depth_write_enabled(mut self, enabled: bool) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.depth_write_enabled = enabled; + self + } + + /// Comparison function used to compare depth values in the depth test. + pub fn depth_compare(mut self, compare: wgpu::CompareFunction) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.depth_compare = compare; + self + } + + /// Specify the full set of stencil parameters. + pub fn stencil(mut self, stencil: wgpu::StencilState) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.stencil = stencil; + self + } + + /// Front face mode. + pub fn stencil_front(mut self, stencil: wgpu::StencilFaceState) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.stencil.front = stencil; + self + } + + /// Back face mode. + pub fn stencil_back(mut self, stencil: wgpu::StencilFaceState) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.stencil.back = stencil; + self + } + + /// Stencil values are AND'd with this mask when reading and writing from the stencil buffer. + /// Only low 8 bits are used. + pub fn stencil_read_mask(mut self, mask: u32) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.stencil.read_mask = mask; + self + } + + /// Stencil values are AND'd with this mask when writing to the stencil buffer. + /// Only low 8 bits are used. + pub fn stencil_write_mask(mut self, mask: u32) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.stencil.write_mask = mask; + self + } + + /// Specify the full set of depth bias parameters. + /// + /// Describes the biasing setting for the depth target. + pub fn depth_bias(mut self, bias: wgpu::DepthBiasState) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.bias = bias; + self + } + + /// Constant depth biasing factor, in basic units of the depth format. + pub fn depth_bias_constant(mut self, constant: i32) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.bias.constant = constant; + self + } + + /// Slope depth biasing factor. + pub fn depth_bias_slope_scale(mut self, scale: f32) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.bias.slope_scale = scale; + self + } + + /// Depth bias clamp value (absolute). + pub fn depth_bias_clamp(mut self, clamp: f32) -> Self { + let state = self + .depth_stencil + .get_or_insert(Self::DEFAULT_DEPTH_STENCIL); + state.bias.clamp = clamp; + self + } + + /// If enabled polygon depth is clamped to 0-1 range instead of being clipped. + /// + /// Requires `Features::DEPTH_CLIP_CONTROL` enabled. + pub fn unclipped_depth(mut self, b: bool) -> Self { + self.primitive.unclipped_depth = b; + self + } + + // Vertex buffer methods. + + /// Add a new vertex buffer descriptor to the render pipeline. + pub fn add_vertex_buffer_layout(mut self, d: wgpu::VertexBufferLayout) -> Self { + self.vertex_buffers.push(d); + self + } + + /// Short-hand for adding a descriptor to the render pipeline describing a buffer of vertices + /// of the given vertex type. + /// + /// The vertex stride is assumed to be equal to `size_of::()`. If this is not the case, + /// consider using `add_vertex_buffer_layout` instead. + pub fn add_vertex_buffer(self, attrs: &'static [wgpu::VertexAttribute]) -> Self { + let array_stride = std::mem::size_of::() as wgpu::BufferAddress; + let step_mode = wgpu::VertexStepMode::Vertex; + let descriptor = wgpu::VertexBufferLayout { + array_stride, + step_mode, + attributes: Vec::from(attrs), + }; + self.add_vertex_buffer_layout(descriptor) + } + + /// Short-hand for adding a descriptor to the render pipeline describing a buffer of instances + /// of the given vertex type. + pub fn add_instance_buffer(self, attrs: &'static [wgpu::VertexAttribute]) -> Self { + let array_stride = std::mem::size_of::() as wgpu::BufferAddress; + let step_mode = wgpu::VertexStepMode::Instance; + let descriptor = wgpu::VertexBufferLayout { + array_stride, + step_mode, + attributes: Vec::from(attrs), + }; + self.add_vertex_buffer_layout(descriptor) + } + + // Multisample state. + + /// Specify the full multisample state. + pub fn multisample(mut self, multisample: wgpu::MultisampleState) -> Self { + self.multisample = multisample; + self + } + + /// The number of samples calculated per pixel (for MSAA). + /// + /// For non-multisampled textures, this should be 1 (the default). + pub fn sample_count(mut self, sample_count: u32) -> Self { + self.multisample.count = sample_count; + self + } + + /// Bitmask that restricts the samples of a pixel modified by this pipeline. All samples can be + /// enabled using the value !0 (the default). + pub fn sample_mask(mut self, sample_mask: u64) -> Self { + self.multisample.mask = sample_mask; + self + } + + /// When enabled, produces another sample mask per pixel based on the alpha output value, that + /// is ANDed with the sample_mask and the primitive coverage to restrict the set of samples + /// affected by a primitive. + /// + /// The implicit mask produced for alpha of zero is guaranteed to be zero, and for alpha of one + /// is guaranteed to be all 1-s. + /// + /// Disabled by default. + pub fn alpha_to_coverage_enabled(mut self, b: bool) -> Self { + self.multisample.alpha_to_coverage_enabled = b; + self + } + + + pub fn build(self) -> wgpu::RenderPipelineDescriptor { + let RenderPipelineBuilder { + layout, + vs_mod, + fs_mod, + vs_entry_point, + fs_entry_point, + primitive, + color_state, + color_states, + depth_stencil, + multisample, + vertex_buffers, + } = self; + + let vertex = wgpu::VertexState { + shader: vs_mod, + entry_point: vs_entry_point.to_string().into(), + buffers: Vec::from(&vertex_buffers[..]), + shader_defs: vec![], + }; + + let mut single_color_state = [Some(RenderPipelineBuilder::DEFAULT_COLOR_STATE)]; + let color_states = match (fs_mod.is_some(), color_states.is_empty()) { + (true, true) => { + if let cs @ Some(_) = color_state { + single_color_state[0] = cs; + } + &single_color_state[..] + } + (true, false) => color_states, + (false, true) => panic!("specified color states but no fragment shader"), + (false, false) => match color_state.is_some() { + true => panic!("specified color state fields but no fragment shader"), + false => &[], + }, + }; + let fragment = match (fs_mod, color_states.is_empty()) { + (Some(fs_mod), false) => Some(wgpu::FragmentState { + shader: fs_mod, + entry_point: fs_entry_point.to_string().into(), + targets: Vec::from(color_states), + shader_defs: vec![], + }), + _ => None, + }; + + wgpu::RenderPipelineDescriptor { + label: Some(Cow::from("nannou render pipeline")), + layout: Vec::from(&layout[..]), + push_constant_ranges: vec![], + vertex, + primitive, + depth_stencil, + multisample, + fragment, + } + } +} diff --git a/bevy_nannou_wgpu/src/sampler_builder.rs b/bevy_nannou_wgpu/src/sampler_builder.rs new file mode 100644 index 000000000..f787c1b10 --- /dev/null +++ b/bevy_nannou_wgpu/src/sampler_builder.rs @@ -0,0 +1,153 @@ +use bevy::prelude::*; +use bevy::render::render_resource as wgpu; +use bevy::render::renderer::RenderDevice; +/// Simplifies the construction of a `Sampler` with a set of reasonable defaults. +#[derive(Debug)] +pub struct SamplerBuilder { + pub descriptor: wgpu::SamplerDescriptor<'static>, +} + +impl<'b> SamplerBuilder { + pub const DEFAULT_ADDRESS_MODE_U: wgpu::AddressMode = wgpu::AddressMode::ClampToEdge; + pub const DEFAULT_ADDRESS_MODE_V: wgpu::AddressMode = wgpu::AddressMode::ClampToEdge; + pub const DEFAULT_ADDRESS_MODE_W: wgpu::AddressMode = wgpu::AddressMode::ClampToEdge; + pub const DEFAULT_MAG_FILTER: wgpu::FilterMode = wgpu::FilterMode::Linear; + pub const DEFAULT_MIN_FILTER: wgpu::FilterMode = wgpu::FilterMode::Linear; + pub const DEFAULT_MIPMAP_FILTER: wgpu::FilterMode = wgpu::FilterMode::Nearest; + pub const DEFAULT_LOD_MIN_CLAMP: f32 = 0.0; + pub const DEFAULT_LOD_MAX_CLAMP: f32 = 100.0; + pub const DEFAULT_COMPARE: Option = None; + pub const DEFAULT_ANISOTROPY_CLAMP: u16 = 1; + pub const DEFAULT_LABEL: &'static str = "nannou-sampler"; + pub const DEFAULT_BORDER_COLOR: Option = None; + pub const DEFAULT_DESCRIPTOR: wgpu::SamplerDescriptor<'static> = wgpu::SamplerDescriptor { + address_mode_u: Self::DEFAULT_ADDRESS_MODE_U, + address_mode_v: Self::DEFAULT_ADDRESS_MODE_V, + address_mode_w: Self::DEFAULT_ADDRESS_MODE_W, + mag_filter: Self::DEFAULT_MAG_FILTER, + min_filter: Self::DEFAULT_MIN_FILTER, + mipmap_filter: Self::DEFAULT_MIPMAP_FILTER, + lod_min_clamp: Self::DEFAULT_LOD_MIN_CLAMP, + lod_max_clamp: Self::DEFAULT_LOD_MAX_CLAMP, + compare: Self::DEFAULT_COMPARE, + anisotropy_clamp: Self::DEFAULT_ANISOTROPY_CLAMP, + label: Some(Self::DEFAULT_LABEL), + border_color: Self::DEFAULT_BORDER_COLOR, + }; + + /// Begin building a `Sampler`, starting with the `Default` parameters. + pub fn new() -> Self { + Self::default() + } + + /// How the implementation should behave when sampling outside of the texture coordinates range + /// [0.0, 1.0]. + pub fn address_mode_u(mut self, mode: wgpu::AddressMode) -> Self { + self.descriptor.address_mode_u = mode; + self + } + + /// How the implementation should behave when sampling outside of the texture coordinates range + /// [0.0, 1.0]. + pub fn address_mode_v(mut self, mode: wgpu::AddressMode) -> Self { + self.descriptor.address_mode_v = mode; + self + } + + /// How the implementation should behave when sampling outside of the texture coordinates range + /// [0.0, 1.0]. + pub fn address_mode_w(mut self, mode: wgpu::AddressMode) -> Self { + self.descriptor.address_mode_w = mode; + self + } + + /// How the implementation should behave when sampling outside of the texture coordinates range + /// [0.0, 1.0]. + /// + /// Applies the same address mode to all axes. + pub fn address_mode(self, mode: wgpu::AddressMode) -> Self { + self.address_mode_u(mode) + .address_mode_v(mode) + .address_mode_w(mode) + } + + /// How the implementation should sample from the image when it is respectively larger than the + /// original. + pub fn mag_filter(mut self, filter: wgpu::FilterMode) -> Self { + self.descriptor.mag_filter = filter; + self + } + + /// How the implementation should sample from the image when it is respectively smaller than + /// the original. + pub fn min_filter(mut self, filter: wgpu::FilterMode) -> Self { + self.descriptor.min_filter = filter; + self + } + + /// How the implementation should choose which mipmap to use. + pub fn mipmap_filter(mut self, filter: wgpu::FilterMode) -> Self { + self.descriptor.mipmap_filter = filter; + self + } + + /// The minimum mipmap level to use. + pub fn lod_min_clamp(mut self, min: f32) -> Self { + self.descriptor.lod_min_clamp = min; + self + } + + /// The maximum mipmap level to use. + pub fn lod_max_clamp(mut self, max: f32) -> Self { + self.descriptor.lod_max_clamp = max; + self + } + + /// The comparison function to use, if any. + pub fn compare(mut self, f: Option) -> Self { + self.descriptor.compare = f; + self + } + + /// The anisotropy level to clamp to, if any. + pub fn anisotropy_clamp(mut self, clamp: u16) -> Self { + self.descriptor.anisotropy_clamp = clamp; + self + } + + /// The label to use, if any. + pub fn label(mut self, label: Option<&'static str>) -> Self { + self.descriptor.label = label; + self + } + + /// Calls `device.create_sampler(&self.descriptor)` internally. + pub fn build(&self, device: &RenderDevice) -> wgpu::Sampler { + device.create_sampler(&self.descriptor) + } + + /// Consume the builder and produce the inner `SamplerDescriptor`. + pub fn into_descriptor(self) -> wgpu::SamplerDescriptor<'static> { + self.into() + } +} + +impl Default for SamplerBuilder { + fn default() -> Self { + SamplerBuilder { + descriptor: Self::DEFAULT_DESCRIPTOR, + } + } +} + +impl Into> for SamplerBuilder { + fn into(self) -> wgpu::SamplerDescriptor<'static> { + self.descriptor + } +} + +impl From> for SamplerBuilder { + fn from(descriptor: wgpu::SamplerDescriptor<'static>) -> Self { + SamplerBuilder { descriptor } + } +} diff --git a/bevy_nannou_wgpu/src/texture.rs b/bevy_nannou_wgpu/src/texture.rs new file mode 100644 index 000000000..c19549cb1 --- /dev/null +++ b/bevy_nannou_wgpu/src/texture.rs @@ -0,0 +1,204 @@ +use bevy::render::render_resource as wgpu; +use bevy::render::renderer::RenderDevice; + +/// A type aimed at simplifying the construction of a **Texture**. +/// +/// The builder assumes a set of defaults describing a 128x128, non-multisampled, single-layer, +/// non-linear sRGBA-8 texture. A suite of builder methods may be used to specify the exact +/// properties desired. +#[derive(Debug)] +pub struct TextureBuilder { + descriptor: wgpu::TextureDescriptor<'static>, +} + +impl TextureBuilder { + pub const DEFAULT_SIDE: u32 = 128; + pub const DEFAULT_DEPTH: u32 = 1; + pub const DEFAULT_SIZE: wgpu::Extent3d = wgpu::Extent3d { + width: Self::DEFAULT_SIDE, + height: Self::DEFAULT_SIDE, + depth_or_array_layers: Self::DEFAULT_DEPTH, + }; + pub const DEFAULT_ARRAY_LAYER_COUNT: u32 = 1; + pub const DEFAULT_MIP_LEVEL_COUNT: u32 = 1; + pub const DEFAULT_SAMPLE_COUNT: u32 = 1; + pub const DEFAULT_DIMENSION: wgpu::TextureDimension = wgpu::TextureDimension::D2; + pub const DEFAULT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; + pub const DEFAULT_USAGE: wgpu::TextureUsages = wgpu::TextureUsages::all(); // TODO: is this the right choice? + pub const DEFAULT_DESCRIPTOR: wgpu::TextureDescriptor<'static> = wgpu::TextureDescriptor { + label: Some("nannou Texture"), + size: Self::DEFAULT_SIZE, + mip_level_count: Self::DEFAULT_MIP_LEVEL_COUNT, + sample_count: Self::DEFAULT_SAMPLE_COUNT, + dimension: Self::DEFAULT_DIMENSION, + format: Self::DEFAULT_FORMAT, + usage: Self::DEFAULT_USAGE, + view_formats: &[], + }; + + /// Creates a new `Default` builder + pub fn new() -> Self { + Self::default() + } + + /// Specify the width and height of the texture. + /// + /// Note: On calls to `size`, `depth` and `extent` the `Builder` will attempt to infer the + /// `wgpu::TextureDimension` of its inner `wgpu::TextureDescriptor` by examining its `size` + /// field. Use `TextureBuilder::dimension()` to override this behavior. + pub fn size(mut self, [width, height]: [u32; 2]) -> Self { + self.descriptor.size.width = width; + self.descriptor.size.height = height; + self.infer_dimension_from_size(); + self + } + + /// Specify the depth of the texture. + /// + /// Note: On calls to `size`, `depth` and `extent` the `Builder` will attempt to infer the + /// `wgpu::TextureDimension` of its inner `wgpu::TextureDescriptor` by examining its `size` + /// field. Use `TextureBuilder::dimension()` to override this behavior. + pub fn depth(mut self, depth: u32) -> Self { + self.descriptor.size.depth_or_array_layers = depth; + self.infer_dimension_from_size(); + self + } + + /// Specify the width, height and depth of the texture. + /// + /// Note: On calls to `size`, `depth` and `extent` the `Builder` will attempt to infer the + /// `wgpu::TextureDimension` of its inner `wgpu::TextureDescriptor` by examining its `size` + /// field. Use `TextureBuilder::dimension()` to override this behavior. + pub fn extent(mut self, extent: wgpu::Extent3d) -> Self { + self.descriptor.size = extent; + self.infer_dimension_from_size(); + self + } + + /// Specify the dimension of the texture, overriding inferred dimension. + /// + /// Mainly useful for creating 2d texture arrays -- override dimension with + /// `wgpu::TextureDimension::D2` on a texture with `extent.depth > 1` in order to create a + /// texture array (/ cubemap / cubemap array) instead of a 3d texture. + pub fn dimension(mut self, dimension: wgpu::TextureDimension) -> Self { + self.descriptor.dimension = dimension; + self + } + + /// Specify the number of mip levels of the texture. + pub fn mip_level_count(mut self, count: u32) -> Self { + self.descriptor.mip_level_count = count; + self + } + + /// Specify the number of samples per pixel in the case that the texture is multisampled. + pub fn sample_count(mut self, count: u32) -> Self { + self.descriptor.sample_count = count; + self + } + + /// Specify the texture format. + pub fn format(mut self, format: wgpu::TextureFormat) -> Self { + self.descriptor.format = format; + self + } + + /// Describes to the implementation how the texture is to be used. + /// + /// It is important that the set of usage bits reflects the + pub fn usage(mut self, usage: wgpu::TextureUsages) -> Self { + self.descriptor.usage = usage; + self + } + + // If `depth` is greater than `1` then `D3` is assumed, otherwise if `height` is greater than + // `1` then `D2` is assumed, otherwise `D1` is assumed. + fn infer_dimension_from_size(&mut self) { + if self.descriptor.size.depth_or_array_layers > 1 { + self.descriptor.dimension = wgpu::TextureDimension::D3; + } else if self.descriptor.size.height > 1 { + self.descriptor.dimension = wgpu::TextureDimension::D2; + } else { + self.descriptor.dimension = wgpu::TextureDimension::D1; + } + } + + /// Build the texture resulting from the specified parameters with the given device. + pub fn build(self, device: &RenderDevice) -> wgpu::Texture { + device.create_texture(&self.descriptor) + } + + /// Consumes the builder and returns the resulting `wgpu::TextureDescriptor`. + pub fn into_descriptor(self) -> wgpu::TextureDescriptor<'static> { + self.into() + } +} + +impl Default for TextureBuilder { + fn default() -> Self { + Self { + descriptor: Self::DEFAULT_DESCRIPTOR, + } + } +} + +impl From> for TextureBuilder { + fn from(descriptor: wgpu::TextureDescriptor<'static>) -> Self { + Self { descriptor } + } +} + +impl Into> for TextureBuilder { + fn into(self) -> wgpu::TextureDescriptor<'static> { + self.descriptor + } +} + + +/// The size of the texture data in bytes as described by the given descriptor. +pub fn data_size_bytes(desc: &wgpu::TextureDescriptor) -> usize { + desc.size.width as usize + * desc.size.height as usize + * desc.size.depth_or_array_layers as usize + * format_size_bytes(desc.format) as usize +} + +/// Return the size of the given texture format in bytes. +pub fn format_size_bytes(format: wgpu::TextureFormat) -> u32 { + format + .block_size(None) + .expect("Expected the format to have a block size") as u32 +} + +/// Returns `true` if the given `wgpu::Extent3d`s are equal. +pub fn extent_3d_eq(a: &wgpu::Extent3d, b: &wgpu::Extent3d) -> bool { + a.width == b.width && a.height == b.height && a.depth_or_array_layers == b.depth_or_array_layers +} + +/// Returns `true` if the given texture descriptors are equal. +pub fn descriptor_eq(a: &wgpu::TextureDescriptor, b: &wgpu::TextureDescriptor) -> bool { + extent_3d_eq(&a.size, &b.size) + && a.mip_level_count == b.mip_level_count + && a.sample_count == b.sample_count + && a.dimension == b.dimension + && a.format == b.format + && a.usage == b.usage +} + +/// Used to infer the `TextureAspect` for a `TextureView` from a specific `TextureFormat`. +/// +/// Does the following: +/// +/// - If the format is `Depth32Float` or `Depth24Plus`, `TextureAspect::DepthOnly` is assumed. +/// - Otherwise, `TextureAspect::All` is assumed. +/// +/// Please note that `wgpu::TextureAspect::StencilOnly` can never be inferred with this function. +/// If you require using a `TextureView` as a stencil, consider explicitly specify the +/// `TextureAspect` you require. +pub fn infer_aspect_from_format(format: wgpu::TextureFormat) -> wgpu::TextureAspect { + use wgpu::TextureFormat::*; + match format { + Depth32Float | Depth24Plus => wgpu::TextureAspect::DepthOnly, + _ => wgpu::TextureAspect::All, + } +} diff --git a/examples/draw/draw_textured_polygon.rs b/examples/draw/draw_textured_polygon.rs index 51bc7e9f8..1dde60c63 100644 --- a/examples/draw/draw_textured_polygon.rs +++ b/examples/draw/draw_textured_polygon.rs @@ -6,7 +6,8 @@ fn main() { struct Model { window_id: window::Id, - texture: wgpu::Texture, + texture_a: wgpu::Texture, + texture_b: wgpu::Texture, } fn model(app: &App) -> Model { @@ -15,9 +16,11 @@ fn model(app: &App) -> Model { // Load the image from disk and upload it to a GPU texture. let assets = app.assets_path().unwrap(); let img_path = assets.join("images").join("nature").join("nature_1.jpg"); - let texture = wgpu::Texture::from_path(app, img_path).unwrap(); + let texture_a = wgpu::Texture::from_path(app, img_path).unwrap(); + let img_path = assets.join("images").join("nature").join("nature_2.jpg"); + let texture_b = wgpu::Texture::from_path(app, img_path).unwrap(); - Model { window_id, texture } + Model { window_id, texture_a, texture_b } } // Draw the state of your `Model` into the given `Frame` here. @@ -55,9 +58,17 @@ fn view(app: &App, model: &Model, frame: Frame) { let ellipse_side = win_rect.w().min(win_rect.h()) * 0.75; draw.scale(ellipse_side) .polygon() - .points_textured(&model.texture, points) + .points_textured(&model.texture_a, points.clone()) .rotate(app.time * 0.25); + // Scale the points up to half the window size. + let ellipse_side = win_rect.w().min(win_rect.h()) * 0.75; + draw.scale(ellipse_side) + .polygon() + .points_textured(&model.texture_b, points) + .rotate(app.time * 0.11); + + // Draw to the frame! draw.to_frame(app, &frame).unwrap(); } diff --git a/nannou_egui/src/shaders/fs.wgsl b/nannou_egui/src/shaders/fs.wgsl new file mode 100644 index 000000000..8afcd35a6 --- /dev/null +++ b/nannou_egui/src/shaders/fs.wgsl @@ -0,0 +1,8 @@ +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let texture_color = textureSample(image_texture, image_sampler, in.uv); + // This assumes that texture images are not premultiplied. + let color = in.color * vec4(texture_color.rgb * texture_color.a, texture_color.a); + + return color; +} \ No newline at end of file diff --git a/nannou_egui/src/shaders/vs.wgsl b/nannou_egui/src/shaders/vs.wgsl new file mode 100644 index 000000000..e69de29bb