From 94e1742b440657d5340c0bc4a5b553ad6bc7b362 Mon Sep 17 00:00:00 2001 From: Robin KAY Date: Sun, 28 Aug 2022 11:00:20 +0100 Subject: [PATCH] Add AutoGenerateOutlineNormalsPluign. --- src/generate.rs | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 87 ++++------------------------------- 2 files changed, 128 insertions(+), 79 deletions(-) create mode 100644 src/generate.rs diff --git a/src/generate.rs b/src/generate.rs new file mode 100644 index 0000000..a51f087 --- /dev/null +++ b/src/generate.rs @@ -0,0 +1,120 @@ +use bevy::{ + prelude::*, + render::{mesh::VertexAttributeValues, render_resource::VertexFormat}, + utils::{FloatOrd, HashMap, HashSet}, +}; + +use crate::ATTRIBUTE_OUTLINE_NORMAL; + +/// Failed to generate outline normals for the mesh. +#[derive(thiserror::Error, Debug)] +pub enum GenerateOutlineNormalsError { + #[error("missing vertex attributes '{0}'")] + MissingVertexAttribute(&'static str), + #[error("the '{0}' vertex attribute should have {1:?} format, but had {2:?} format")] + InvalidVertexAttributeFormat(&'static str, VertexFormat, VertexFormat), +} + +/// Extension methods for [`Mesh`]. +pub trait OutlineMeshExt { + /// Generates outline normals for the mesh from the regular normals. + /// + /// Vertex extrusion only works for meshes with smooth surface normals. Hard edges cause + /// visual artefacts. This function generates faux-smooth normals for outlining purposes + /// by grouping vertices by their position and averaging the normals at each point. These + /// outline normals are then inserted as a separate vertex attribute so that the regular + /// normals remain untouched. However, insofar as the outline normals are not + /// perpendicular to the surface of the mesh, this technique may result in non-uniform + /// outline thickness. + /// + /// This function will silently do nothing if the outline normals would be equal to the + /// regular normals. + fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError>; +} + +impl OutlineMeshExt for Mesh { + fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError> { + let positions = match self.attribute(Mesh::ATTRIBUTE_POSITION).ok_or( + GenerateOutlineNormalsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name), + )? { + VertexAttributeValues::Float32x3(p) => Ok(p), + v => Err(GenerateOutlineNormalsError::InvalidVertexAttributeFormat( + Mesh::ATTRIBUTE_POSITION.name, + VertexFormat::Float32x3, + v.into(), + )), + }?; + let normals = match self.attribute(Mesh::ATTRIBUTE_NORMAL).ok_or( + GenerateOutlineNormalsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name), + )? { + VertexAttributeValues::Float32x3(n) => Ok(n), + v => Err(GenerateOutlineNormalsError::InvalidVertexAttributeFormat( + Mesh::ATTRIBUTE_NORMAL.name, + VertexFormat::Float32x3, + v.into(), + )), + }?; + let mut map = HashMap::with_capacity(positions.len()); + let mut modified = false; + for (p, n) in positions.iter().zip(normals.iter()) { + let key = [FloatOrd(p[0]), FloatOrd(p[1]), FloatOrd(p[2])]; + let value = Vec3::from_array(*n); + map.entry(key) + .and_modify(|e| { + modified = true; + *e += value + }) + .or_insert(value); + } + if modified { + let mut outlines = Vec::with_capacity(positions.len()); + for p in positions.iter() { + let key = [FloatOrd(p[0]), FloatOrd(p[1]), FloatOrd(p[2])]; + outlines.push(map.get(&key).unwrap().normalize_or_zero().to_array()); + } + self.insert_attribute( + ATTRIBUTE_OUTLINE_NORMAL, + VertexAttributeValues::Float32x3(outlines), + ); + } + Ok(()) + } +} + +fn auto_generate_outline_normals( + mut meshes: ResMut>, + mut events: EventReader<'_, '_, AssetEvent>, + mut squelch: Local>>, +) { + for event in events.iter() { + println!("{:?}", event); + match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + if squelch.contains(handle) { + // Suppress modification events created by this system + squelch.remove(handle); + } else if let Some(mesh) = meshes.get_mut(handle) { + let _ = mesh.generate_outline_normals(); + squelch.insert(handle.clone_weak()); + } + } + AssetEvent::Removed { handle } => { + squelch.remove(handle); + } + } + } +} + +/// Automatically runs [`generate_outline_normals`](OutlineMeshExt::generate_outline_normals) +/// on every mesh. +/// +/// This is provided as a convenience for simple projects. It runs the outline normal +/// generator every time a mesh asset is created or modified without consideration for +/// whether this is necessary or appropriate. +pub struct AutoGenerateOutlineNormalsPlugin; + +impl Plugin for AutoGenerateOutlineNormalsPlugin { + fn build(&self, app: &mut App) { + app.add_system(auto_generate_outline_normals); + } +} diff --git a/src/lib.rs b/src/lib.rs index 69fdd41..3b3dcda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,8 +10,10 @@ //! an object from being filled it. This must be added to any entity which needs to appear on //! top of an outline. //! -//! Vertex extrusion works best with meshes that have smooth surfaces. For meshes with hard -//! edges, see the [`OutlineMeshExt::generate_outline_normals`] function. +//! Vertex extrusion works best with meshes that have smooth surfaces. To avoid visual +//! artefacts when outlining meshes with hard edges, see the +//! [`OutlineMeshExt::generate_outline_normals`] function and the +//! [`AutoGenerateOutlineNormalsPlugin`]. use bevy::asset::load_internal_asset; use bevy::ecs::query::QueryItem; @@ -19,12 +21,11 @@ use bevy::prelude::*; use bevy::render::extract_component::{ ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, }; -use bevy::render::mesh::{MeshVertexAttribute, VertexAttributeValues}; +use bevy::render::mesh::MeshVertexAttribute; use bevy::render::render_graph::RenderGraph; use bevy::render::render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions}; use bevy::render::render_resource::{SpecializedMeshPipelines, VertexFormat}; use bevy::render::{RenderApp, RenderStage}; -use bevy::utils::{FloatOrd, HashMap}; use crate::draw::{queue_outline_mesh, queue_outline_stencil_mesh, DrawOutline, DrawStencil}; use crate::node::{OpaqueOutline, OutlineNode, StencilOutline, TransparentOutline}; @@ -40,11 +41,14 @@ use crate::view_uniforms::{ }; mod draw; +mod generate; mod node; mod pipeline; mod uniforms; mod view_uniforms; +pub use generate::*; + // See https://alexanderameye.github.io/notes/rendering-outlines/ /// The direction to extrude the vertex when rendering the outline. @@ -88,81 +92,6 @@ pub struct OutlineBundle { pub stencil: OutlineStencil, } -/// Failed to generate outline normals for the mesh. -#[derive(thiserror::Error, Debug)] -pub enum GenerateOutlineNormalsError { - #[error("missing vertex attributes '{0}'")] - MissingVertexAttribute(&'static str), - #[error("the '{0}' vertex attribute should have {1:?} format, but had {2:?} format")] - InvalidVertexAttributeFormat(&'static str, VertexFormat, VertexFormat), -} - -/// Extension methods for [`Mesh`]. -pub trait OutlineMeshExt { - /// Generates outline normals for the mesh from the regular normals. - /// - /// Vertex extrusion only works for meshes with smooth surface normals. Hard edges cause - /// visual artefacts. This function generates faux-smooth normals for outlining purposes - /// by grouping vertices by their position and averaging the normals at each point. These - /// outline normals are then inserted as a separate vertex attribute so that the regular - /// normals remain untouched. However, insofar as the outline normals are not - /// perpendicular to the surface of the mesh, this technique may result in non-uniform - /// outline thickness. - /// - /// This function will silently do nothing if the outline normals would be equal to the - /// regular normals. - fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError>; -} - -impl OutlineMeshExt for Mesh { - fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError> { - let positions = match self.attribute(Mesh::ATTRIBUTE_POSITION).ok_or( - GenerateOutlineNormalsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name), - )? { - VertexAttributeValues::Float32x3(p) => Ok(p), - v => Err(GenerateOutlineNormalsError::InvalidVertexAttributeFormat( - Mesh::ATTRIBUTE_POSITION.name, - VertexFormat::Float32x3, - v.into(), - )), - }?; - let normals = match self.attribute(Mesh::ATTRIBUTE_NORMAL).ok_or( - GenerateOutlineNormalsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name), - )? { - VertexAttributeValues::Float32x3(n) => Ok(n), - v => Err(GenerateOutlineNormalsError::InvalidVertexAttributeFormat( - Mesh::ATTRIBUTE_NORMAL.name, - VertexFormat::Float32x3, - v.into(), - )), - }?; - let mut map = HashMap::with_capacity(positions.len()); - let mut modified = false; - for (p, n) in positions.iter().zip(normals.iter()) { - let key = [FloatOrd(p[0]), FloatOrd(p[1]), FloatOrd(p[2])]; - let value = Vec3::from_array(*n); - map.entry(key) - .and_modify(|e| { - modified = true; - *e += value - }) - .or_insert(value); - } - if modified { - let mut outlines = Vec::with_capacity(positions.len()); - for p in positions.iter() { - let key = [FloatOrd(p[0]), FloatOrd(p[1]), FloatOrd(p[2])]; - outlines.push(map.get(&key).unwrap().normalize_or_zero().to_array()); - } - self.insert_attribute( - ATTRIBUTE_OUTLINE_NORMAL, - VertexAttributeValues::Float32x3(outlines), - ); - } - Ok(()) - } -} - /// Adds support for rendering outlines. pub struct OutlinePlugin;