use bevy::{
asset::RenderAssetUsages,
math::FloatOrd,
mesh::{Indices, VertexAttributeValues},
platform::collections::{HashMap, HashSet},
prelude::*,
render::render_resource::{PrimitiveTopology, VertexFormat},
};
use crate::ATTRIBUTE_OUTLINE_NORMAL;
enum IndexIterator<'a> {
ExplicitU16(std::slice::Iter<'a, u16>),
ExplicitU32(std::slice::Iter<'a, u32>),
Implicit(std::ops::Range<usize>),
}
impl<'a> From<&'a Mesh> for IndexIterator<'a> {
fn from(value: &'a Mesh) -> Self {
match value.indices() {
Some(Indices::U16(vec)) => IndexIterator::ExplicitU16(vec.iter()),
Some(Indices::U32(vec)) => IndexIterator::ExplicitU32(vec.iter()),
None => IndexIterator::Implicit(0..value.count_vertices()),
}
}
}
impl Iterator for IndexIterator<'_> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
match self {
IndexIterator::ExplicitU16(iter) => iter.next().map(|val| *val as usize),
IndexIterator::ExplicitU32(iter) => iter.next().map(|val| *val as usize),
IndexIterator::Implicit(iter) => iter.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
IndexIterator::ExplicitU16(iter) => iter.size_hint(),
IndexIterator::ExplicitU32(iter) => iter.size_hint(),
IndexIterator::Implicit(iter) => iter.size_hint(),
}
}
}
impl ExactSizeIterator for IndexIterator<'_> {}
#[derive(Copy, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum GenerateOutlineNormalsFrom {
#[default]
VertexNormal,
FaceNormal,
ExternalBisector,
}
#[derive(Clone, Default)]
pub struct GenerateOutlineNormalsSettings {
from: GenerateOutlineNormalsFrom,
}
impl GenerateOutlineNormalsSettings {
pub fn with_from(mut self, value: GenerateOutlineNormalsFrom) -> Self {
self.from = value;
self
}
}
impl From<GenerateOutlineNormalsFrom> for GenerateOutlineNormalsSettings {
fn from(value: GenerateOutlineNormalsFrom) -> Self {
Self::default().with_from(value)
}
}
#[derive(thiserror::Error, Debug)]
pub enum GenerateOutlineNormalsError {
#[error("unsupported primitive topology '{0:?}'")]
UnsupportedPrimitiveTopology(PrimitiveTopology),
#[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: Sized {
fn generate_outline_normals(
&mut self,
settings: &GenerateOutlineNormalsSettings,
) -> Result<(), GenerateOutlineNormalsError>;
fn with_generated_outline_normals(
self,
settings: &GenerateOutlineNormalsSettings,
) -> Result<Self, GenerateOutlineNormalsError>;
}
impl OutlineMeshExt for Mesh {
fn generate_outline_normals(
&mut self,
settings: &GenerateOutlineNormalsSettings,
) -> Result<(), GenerateOutlineNormalsError> {
if self.primitive_topology() != PrimitiveTopology::TriangleList {
return Err(GenerateOutlineNormalsError::UnsupportedPrimitiveTopology(
self.primitive_topology(),
));
}
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) {
Some(VertexAttributeValues::Float32x3(p))
if settings.from == GenerateOutlineNormalsFrom::VertexNormal =>
{
Some(p)
}
_ => None,
};
let mut map = HashMap::<[FloatOrd; 3], Vec3>::with_capacity(positions.len());
let mut it = IndexIterator::from(&*self);
while let (Some(i0), Some(i1), Some(i2)) = (it.next(), it.next(), it.next()) {
for (j0, j1, j2) in [(i0, i1, i2), (i1, i2, i0), (i2, i0, i1)] {
let p0 = Vec3::from(positions[j0]);
let p1 = Vec3::from(positions[j1]);
let p2 = Vec3::from(positions[j2]);
let angle = (p1 - p0).angle_between(p2 - p0);
let n = map
.entry([FloatOrd(p0.x), FloatOrd(p0.y), FloatOrd(p0.z)])
.or_default();
let vector = if settings.from == GenerateOutlineNormalsFrom::ExternalBisector {
let face_normal = (p1 - p0).cross(p2 - p0);
let perp1 = Dir3::new(face_normal.cross(p0 - p1)).unwrap();
let perp2 = Dir3::new(face_normal.cross(p2 - p0)).unwrap();
perp1.slerp(perp2, 0.5).as_vec3()
} else if let Some(ns) = normals {
Vec3::from(ns[j0])
} else {
(p1 - p0).cross(p2 - p0).normalize_or_zero()
};
*n += angle * vector;
}
}
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)
.copied()
.unwrap_or(Vec3::ZERO)
.normalize_or_zero()
.to_array(),
);
}
self.insert_attribute(
ATTRIBUTE_OUTLINE_NORMAL,
VertexAttributeValues::Float32x3(outlines),
);
Ok(())
}
fn with_generated_outline_normals(
mut self,
settings: &GenerateOutlineNormalsSettings,
) -> Result<Self, GenerateOutlineNormalsError> {
self.generate_outline_normals(settings).map(|_| self)
}
}
fn auto_generate_outline_normals(
mut meshes: ResMut<Assets<Mesh>>,
mut events: MessageReader<'_, '_, AssetEvent<Mesh>>,
mut squelch: Local<HashSet<AssetId<Mesh>>>,
plugin: Res<AutoGenerateOutlineNormalsPlugin>,
) {
for event in events.read() {
match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
if squelch.contains(id) {
squelch.remove(id);
} else if let Some(mut mesh) = meshes.get_mut(*id) {
if mesh.asset_usage.contains(RenderAssetUsages::MAIN_WORLD) {
let _ = mesh.generate_outline_normals(&plugin.settings);
squelch.insert(*id);
}
}
}
AssetEvent::Removed { id } => {
squelch.remove(id);
}
_ => {}
}
}
}
#[derive(Clone, Default, Resource)]
pub struct AutoGenerateOutlineNormalsPlugin {
settings: GenerateOutlineNormalsSettings,
}
impl AutoGenerateOutlineNormalsPlugin {
pub fn new(settings: GenerateOutlineNormalsSettings) -> Self {
Self { settings }
}
}
impl Plugin for AutoGenerateOutlineNormalsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(self.clone())
.add_systems(Update, auto_generate_outline_normals);
}
}