|
| 1 | +use super::{GenerateTangentsError, Mesh}; |
| 2 | +use bevy_math::{Vec2, Vec3A, Vec4}; |
| 3 | +use wgpu_types::{PrimitiveTopology, VertexFormat}; |
| 4 | + |
| 5 | +struct TriangleIndexIter<'a, I>(&'a mut I); |
| 6 | + |
| 7 | +impl<'a, I> Iterator for TriangleIndexIter<'a, I> |
| 8 | +where |
| 9 | + I: Iterator<Item = usize>, |
| 10 | +{ |
| 11 | + type Item = [usize; 3]; |
| 12 | + fn next(&mut self) -> Option<[usize; 3]> { |
| 13 | + let i = &mut self.0; |
| 14 | + match (i.next(), i.next(), i.next()) { |
| 15 | + (Some(i1), Some(i2), Some(i3)) => Some([i1, i2, i3]), |
| 16 | + _ => None, |
| 17 | + } |
| 18 | + } |
| 19 | +} |
| 20 | + |
| 21 | +pub(crate) fn generate_tangents_for_mesh( |
| 22 | + mesh: &Mesh, |
| 23 | +) -> Result<Vec<[f32; 4]>, GenerateTangentsError> { |
| 24 | + let positions = mesh.attribute(Mesh::ATTRIBUTE_POSITION); |
| 25 | + let normals = mesh.attribute(Mesh::ATTRIBUTE_NORMAL); |
| 26 | + let uvs = mesh.attribute(Mesh::ATTRIBUTE_UV_0); |
| 27 | + let indices = mesh.indices(); |
| 28 | + let primitive_topology = mesh.primitive_topology(); |
| 29 | + |
| 30 | + if primitive_topology != PrimitiveTopology::TriangleList { |
| 31 | + return Err(GenerateTangentsError::UnsupportedTopology( |
| 32 | + primitive_topology, |
| 33 | + )); |
| 34 | + } |
| 35 | + |
| 36 | + match (positions, normals, uvs, indices) { |
| 37 | + (None, _, _, _) => Err(GenerateTangentsError::MissingVertexAttribute( |
| 38 | + Mesh::ATTRIBUTE_POSITION.name, |
| 39 | + )), |
| 40 | + (_, None, _, _) => Err(GenerateTangentsError::MissingVertexAttribute( |
| 41 | + Mesh::ATTRIBUTE_NORMAL.name, |
| 42 | + )), |
| 43 | + (_, _, None, _) => Err(GenerateTangentsError::MissingVertexAttribute( |
| 44 | + Mesh::ATTRIBUTE_UV_0.name, |
| 45 | + )), |
| 46 | + (_, _, _, None) => Err(GenerateTangentsError::MissingIndices), |
| 47 | + (Some(positions), Some(normals), Some(uvs), Some(indices)) => { |
| 48 | + let positions = positions.as_float3().ok_or( |
| 49 | + GenerateTangentsError::InvalidVertexAttributeFormat( |
| 50 | + Mesh::ATTRIBUTE_POSITION.name, |
| 51 | + VertexFormat::Float32x3, |
| 52 | + ), |
| 53 | + )?; |
| 54 | + let normals = |
| 55 | + normals |
| 56 | + .as_float3() |
| 57 | + .ok_or(GenerateTangentsError::InvalidVertexAttributeFormat( |
| 58 | + Mesh::ATTRIBUTE_NORMAL.name, |
| 59 | + VertexFormat::Float32x3, |
| 60 | + ))?; |
| 61 | + let uvs = |
| 62 | + uvs.as_float2() |
| 63 | + .ok_or(GenerateTangentsError::InvalidVertexAttributeFormat( |
| 64 | + Mesh::ATTRIBUTE_UV_0.name, |
| 65 | + VertexFormat::Float32x2, |
| 66 | + ))?; |
| 67 | + let vertex_count = positions.len(); |
| 68 | + let mut tangents = vec![Vec3A::ZERO; vertex_count]; |
| 69 | + let mut bi_tangents = vec![Vec3A::ZERO; vertex_count]; |
| 70 | + |
| 71 | + for [i1, i2, i3] in TriangleIndexIter(&mut indices.iter()) { |
| 72 | + let v1 = Vec3A::from_array(positions[i1]); |
| 73 | + let v2 = Vec3A::from_array(positions[i2]); |
| 74 | + let v3 = Vec3A::from_array(positions[i3]); |
| 75 | + |
| 76 | + let w1 = Vec2::from(uvs[i1]); |
| 77 | + let w2 = Vec2::from(uvs[i2]); |
| 78 | + let w3 = Vec2::from(uvs[i3]); |
| 79 | + |
| 80 | + let delta_pos1 = v2 - v1; |
| 81 | + let delta_pos2 = v3 - v1; |
| 82 | + |
| 83 | + let delta_uv1 = w2 - w1; |
| 84 | + let delta_uv2 = w3 - w1; |
| 85 | + |
| 86 | + let determinant = delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x; |
| 87 | + |
| 88 | + // check for degenerate triangles |
| 89 | + if determinant.abs() > 1e-6 { |
| 90 | + let r = 1.0 / determinant; |
| 91 | + let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r; |
| 92 | + let bi_tangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * r; |
| 93 | + |
| 94 | + tangents[i1] += tangent; |
| 95 | + tangents[i2] += tangent; |
| 96 | + tangents[i3] += tangent; |
| 97 | + |
| 98 | + bi_tangents[i1] += bi_tangent; |
| 99 | + bi_tangents[i2] += bi_tangent; |
| 100 | + bi_tangents[i3] += bi_tangent; |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + let mut result_tangents = Vec::with_capacity(vertex_count); |
| 105 | + |
| 106 | + for i in 0..vertex_count { |
| 107 | + let normal = Vec3A::from_array(normals[i]); |
| 108 | + let tangent = tangents[i].normalize(); |
| 109 | + // Gram-Schmidt orthogonalization |
| 110 | + let tangent = (tangent - normal * normal.dot(tangent)).normalize(); |
| 111 | + let bi_tangent = bi_tangents[i]; |
| 112 | + let handedness = if normal.cross(tangent).dot(bi_tangent) > 0.0 { |
| 113 | + 1.0 |
| 114 | + } else { |
| 115 | + -1.0 |
| 116 | + }; |
| 117 | + // Both the gram-schmidt and mikktspace algorithms seem to assume left-handedness, |
| 118 | + // so we flip the sign to correct for this. The extra multiplication here is |
| 119 | + // negligible and it's done as a separate step to better document that it's |
| 120 | + // a deviation from the general algorithm. The generated mikktspace tangents are |
| 121 | + // also post processed to flip the sign. |
| 122 | + let handedness = handedness * -1.0; |
| 123 | + result_tangents |
| 124 | + .push(Vec4::new(tangent.x, tangent.y, tangent.z, handedness).to_array()); |
| 125 | + } |
| 126 | + |
| 127 | + Ok(result_tangents) |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +#[cfg(test)] |
| 133 | +mod tests { |
| 134 | + use bevy_math::{primitives::*, Vec2, Vec3, Vec3A}; |
| 135 | + |
| 136 | + use crate::{Mesh, TangentStrategy}; |
| 137 | + |
| 138 | + // The tangents should be very close for simple shapes |
| 139 | + fn compare_tangents(mut mesh: Mesh) { |
| 140 | + let hq_tangents: Vec<[f32; 4]> = { |
| 141 | + mesh.remove_attribute(Mesh::ATTRIBUTE_TANGENT); |
| 142 | + mesh.compute_tangents(TangentStrategy::HighQuality) |
| 143 | + .expect("compute_tangents(HighQuality)"); |
| 144 | + mesh.attribute(Mesh::ATTRIBUTE_TANGENT) |
| 145 | + .expect("hq_tangents.attribute(tangent)") |
| 146 | + .as_float4() |
| 147 | + .expect("hq_tangents.as_float4") |
| 148 | + .to_vec() |
| 149 | + }; |
| 150 | + |
| 151 | + let fa_tangents: Vec<[f32; 4]> = { |
| 152 | + mesh.remove_attribute(Mesh::ATTRIBUTE_TANGENT); |
| 153 | + mesh.compute_tangents(TangentStrategy::FastApproximation) |
| 154 | + .expect("compute_tangents(FastApproximation)"); |
| 155 | + mesh.attribute(Mesh::ATTRIBUTE_TANGENT) |
| 156 | + .expect("fa_tangents.attribute(tangent)") |
| 157 | + .as_float4() |
| 158 | + .expect("fa_tangents.as_float4") |
| 159 | + .to_vec() |
| 160 | + }; |
| 161 | + |
| 162 | + for (hq, fa) in hq_tangents.iter().zip(fa_tangents.iter()) { |
| 163 | + assert_eq!(hq[3], fa[3], "handedness"); |
| 164 | + let hq = Vec3A::from_slice(hq); |
| 165 | + let fa = Vec3A::from_slice(fa); |
| 166 | + let angle = hq.angle_between(fa); |
| 167 | + let threshold = 15.0f32.to_radians(); |
| 168 | + assert!( |
| 169 | + angle < threshold, |
| 170 | + "tangents differ significantly: hq = {:?}, fa = {:?}, angle.to_degrees() = {}", |
| 171 | + hq, |
| 172 | + fa, |
| 173 | + angle.to_degrees() |
| 174 | + ); |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + #[test] |
| 179 | + fn cuboid() { |
| 180 | + compare_tangents(Mesh::from(Cuboid::new(10.0, 10.0, 10.0))); |
| 181 | + } |
| 182 | + |
| 183 | + #[test] |
| 184 | + fn capsule3d() { |
| 185 | + compare_tangents(Mesh::from(Capsule3d::new(10.0, 10.0))); |
| 186 | + } |
| 187 | + |
| 188 | + #[test] |
| 189 | + fn plane3d() { |
| 190 | + compare_tangents(Mesh::from(Plane3d::new(Vec3::Y, Vec2::splat(10.0)))); |
| 191 | + } |
| 192 | + |
| 193 | + #[test] |
| 194 | + fn cylinder() { |
| 195 | + compare_tangents(Mesh::from(Cylinder::new(10.0, 10.0))); |
| 196 | + } |
| 197 | + |
| 198 | + #[test] |
| 199 | + fn torus() { |
| 200 | + compare_tangents(Mesh::from(Torus::new(10.0, 100.0))); |
| 201 | + } |
| 202 | + |
| 203 | + #[test] |
| 204 | + fn rhombus() { |
| 205 | + compare_tangents(Mesh::from(Rhombus::new(10.0, 100.0))); |
| 206 | + } |
| 207 | + |
| 208 | + #[test] |
| 209 | + fn tetrahedron() { |
| 210 | + compare_tangents(Mesh::from(Tetrahedron::default())); |
| 211 | + } |
| 212 | + |
| 213 | + #[test] |
| 214 | + fn cone() { |
| 215 | + compare_tangents(Mesh::from(Cone::new(10.0, 100.0))); |
| 216 | + } |
| 217 | +} |
0 commit comments