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