diff --git a/Cargo.toml b/Cargo.toml index 18382c9..4e17f34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ bevy = { version = "0.8", default-features = false, features = [ "bevy_asset", "render", ] } +thiserror = "1.0" [dev-dependencies] bevy = { version = "0.8", default-features = false, features = [ diff --git a/examples/shapes.rs b/examples/shapes.rs index 34a2af5..6bdfdfe 100644 --- a/examples/shapes.rs +++ b/examples/shapes.rs @@ -1,6 +1,9 @@ use std::f32::consts::{PI, TAU}; -use bevy::prelude::{shape::Torus, *}; +use bevy::prelude::{ + shape::{Cube, Torus}, + *, +}; use bevy_mod_outline::*; @@ -33,14 +36,11 @@ fn setup( material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()), ..default() }); + let mut cube_mesh = Mesh::from(Cube { size: 1.0 }); + cube_mesh.generate_outline_normals().unwrap(); commands .spawn_bundle(PbrBundle { - mesh: meshes.add(Mesh::from(Torus { - radius: 0.6, - ring_radius: 0.2, - subdivisions_segments: 20, - subdivisions_sides: 10, - })), + mesh: meshes.add(cube_mesh), material: materials.add(Color::rgb(0.1, 0.1, 0.9).into()), transform: Transform::from_xyz(0.0, 1.0, 0.0), ..default() diff --git a/src/lib.rs b/src/lib.rs index 41d2b8c..a92064f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,12 @@ use bevy::prelude::*; use bevy::render::extract_component::{ ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, }; +use bevy::render::mesh::{MeshVertexAttribute, VertexAttributeValues}; use bevy::render::render_graph::RenderGraph; use bevy::render::render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions}; -use bevy::render::render_resource::SpecializedMeshPipelines; +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}; @@ -27,7 +29,14 @@ mod view_uniforms; // See https://alexanderameye.github.io/notes/rendering-outlines/ -/// A component for stenciling meshes during outline rendering +/// The direction to extrude the vertex when rendering the outline. +pub const ATTRIBUTE_OUTLINE_NORMAL: MeshVertexAttribute = MeshVertexAttribute::new( + "Outline_Normal", + 1585570526414773879, + VertexFormat::Float32x3, +); + +/// A component for stenciling meshes during outline rendering. #[derive(Component, Default)] pub struct OutlineStencil; @@ -49,6 +58,61 @@ pub struct Outline { pub colour: Color, } +/// 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), +} + +pub trait OutlineMeshExt { + /// Generates outline normals for the mesh by normalising the sum of 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()); + 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| *e += value).or_insert(value); + } + 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; diff --git a/src/pipeline.rs b/src/pipeline.rs index 2ca7eca..ca969f8 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -22,6 +22,7 @@ use bevy::{ use crate::uniforms::{OutlineFragmentUniform, OutlineVertexUniform}; use crate::view_uniforms::OutlineViewUniform; +use crate::ATTRIBUTE_OUTLINE_NORMAL; pub const COMMON_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9448276477068917228); @@ -108,7 +109,7 @@ impl SpecializedMeshPipeline for OutlinePipeline { fn specialize( &self, (key, pass_type): Self::Key, - layout: &MeshVertexBufferLayout, + mesh_layout: &MeshVertexBufferLayout, ) -> Result { let mut targets = vec![]; let mut bind_layouts = vec![ @@ -135,10 +136,17 @@ impl SpecializedMeshPipeline for OutlinePipeline { bind_layouts.push(self.outline_view_bind_group_layout.clone()); bind_layouts.push(self.outline_bind_group_layout.clone()); - buffer_attrs.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1)); + buffer_attrs.push( + if mesh_layout.contains(ATTRIBUTE_OUTLINE_NORMAL) { + ATTRIBUTE_OUTLINE_NORMAL + } else { + Mesh::ATTRIBUTE_NORMAL + } + .at_shader_location(1), + ); } } - let buffers = vec![layout.get_layout(&buffer_attrs)?]; + let buffers = vec![mesh_layout.get_layout(&buffer_attrs)?]; Ok(RenderPipelineDescriptor { vertex: VertexState { shader: shader.clone().typed::(),