webxr_api/
hittest.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::iter::FromIterator;
6
7use euclid::{Point3D, RigidTransform3D, Rotation3D, Vector3D};
8use serde::{Deserialize, Serialize};
9
10use crate::{ApiSpace, Native, Space};
11
12#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
13/// <https://immersive-web.github.io/hit-test/#xrray>
14pub struct Ray<Space> {
15    /// The origin of the ray
16    pub origin: Vector3D<f32, Space>,
17    /// The direction of the ray. Must be normalized.
18    pub direction: Vector3D<f32, Space>,
19}
20
21#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
22/// <https://immersive-web.github.io/hit-test/#enumdef-xrhittesttrackabletype>
23pub enum EntityType {
24    Point,
25    Plane,
26    Mesh,
27}
28
29#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
30/// <https://immersive-web.github.io/hit-test/#dictdef-xrhittestoptionsinit>
31pub struct HitTestSource {
32    pub id: HitTestId,
33    pub space: Space,
34    pub ray: Ray<ApiSpace>,
35    pub types: EntityTypes,
36}
37
38#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
39pub struct HitTestId(pub u32);
40
41#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
42/// Vec<EntityType>, but better
43pub struct EntityTypes {
44    pub point: bool,
45    pub plane: bool,
46    pub mesh: bool,
47}
48
49#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
50pub struct HitTestResult {
51    pub id: HitTestId,
52    pub space: RigidTransform3D<f32, HitTestSpace, Native>,
53}
54
55#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
56/// The coordinate space of a hit test result
57pub struct HitTestSpace;
58
59#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
60pub struct Triangle {
61    pub first: Point3D<f32, Native>,
62    pub second: Point3D<f32, Native>,
63    pub third: Point3D<f32, Native>,
64}
65
66impl EntityTypes {
67    pub fn is_type(self, ty: EntityType) -> bool {
68        match ty {
69            EntityType::Point => self.point,
70            EntityType::Plane => self.plane,
71            EntityType::Mesh => self.mesh,
72        }
73    }
74
75    pub fn add_type(&mut self, ty: EntityType) {
76        match ty {
77            EntityType::Point => self.point = true,
78            EntityType::Plane => self.plane = true,
79            EntityType::Mesh => self.mesh = true,
80        }
81    }
82}
83
84impl FromIterator<EntityType> for EntityTypes {
85    fn from_iter<T>(iter: T) -> Self
86    where
87        T: IntoIterator<Item = EntityType>,
88    {
89        iter.into_iter().fold(Default::default(), |mut acc, e| {
90            acc.add_type(e);
91            acc
92        })
93    }
94}
95
96impl Triangle {
97    /// <https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm>
98    pub fn intersect(
99        self,
100        ray: Ray<Native>,
101    ) -> Option<RigidTransform3D<f32, HitTestSpace, Native>> {
102        let Triangle {
103            first: v0,
104            second: v1,
105            third: v2,
106        } = self;
107
108        let edge1 = v1 - v0;
109        let edge2 = v2 - v0;
110
111        let h = ray.direction.cross(edge2);
112        let a = edge1.dot(h);
113        if a > -f32::EPSILON && a < f32::EPSILON {
114            // ray is parallel to triangle
115            return None;
116        }
117
118        let f = 1. / a;
119
120        let s = ray.origin - v0.to_vector();
121
122        // barycentric coordinate of intersection point u
123        let u = f * s.dot(h);
124        // barycentric coordinates have range (0, 1)
125        if !(0. ..=1.).contains(&u) {
126            // the intersection is outside the triangle
127            return None;
128        }
129
130        let q = s.cross(edge1);
131        // barycentric coordinate of intersection point v
132        let v = f * ray.direction.dot(q);
133
134        // barycentric coordinates have range (0, 1)
135        // and their sum must not be greater than 1
136        if v < 0. || u + v > 1. {
137            // the intersection is outside the triangle
138            return None;
139        }
140
141        let t = f * edge2.dot(q);
142
143        if t > f32::EPSILON {
144            let origin = ray.origin + ray.direction * t;
145
146            // this is not part of the Möller-Trumbore algorithm, the hit test spec
147            // requires it has an orientation such that the Y axis points along
148            // the triangle normal
149            let normal = edge1.cross(edge2).normalize();
150            let y = Vector3D::new(0., 1., 0.);
151            let dot = normal.dot(y);
152            let rotation = if dot > -f32::EPSILON && dot < f32::EPSILON {
153                // vectors are parallel, return the vector itself
154                // XXXManishearth it's possible for the vectors to be
155                // antiparallel, unclear if normals need to be flipped
156                Rotation3D::identity()
157            } else {
158                let axis = normal.cross(y);
159                let cos = normal.dot(y);
160                // This is Rotation3D::around_axis(axis.normalize(), theta), however
161                // that is just Rotation3D::quaternion(axis.normalize().xyz * sin, cos),
162                // which is Rotation3D::quaternion(cross, dot)
163                Rotation3D::quaternion(axis.x, axis.y, axis.z, cos)
164            };
165
166            return Some(RigidTransform3D::new(rotation, origin));
167        }
168
169        // triangle is behind ray
170        None
171    }
172}