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