Bug 1659418 - Handle inverse projection negative hemisphere r=nical

Our inverse projection logic was correct, just not fully complete.
The problem is that it didn't take into account that the projected point can be in negative hemisphere.
Now we are checking for this and bailing out of building a tight rect if this is the case.

Another small change is in `get_raster_rects`: if we are unable to find a tighter bound by inverse projection, we return the same unclipped rect.
The actual clipping should happen at the picture level anyway.

Differential Revision: https://phabricator.services.mozilla.com/D93087
This commit is contained in:
Dzmitry Malyshau 2020-10-13 13:51:27 +00:00
Родитель 0babc1c9c5
Коммит 1bc435d25f
4 изменённых файлов: 86 добавлений и 9 удалений

Просмотреть файл

@ -7109,12 +7109,14 @@ pub fn get_raster_rects(
);
let unclipped_world_rect = map_to_world.map(&unclipped_raster_rect)?;
let clipped_world_rect = unclipped_world_rect.intersection(&prim_bounding_rect)?;
let clipped_raster_rect = map_to_world.unmap(&clipped_world_rect)?;
let clipped_raster_rect = clipped_raster_rect.intersection(&unclipped_raster_rect)?;
// We don't have to be able to do the back-projection from world into raster.
// Rendering only cares one way, so if that fails, we fall back to the full rect.
let clipped_raster_rect = match map_to_world.unmap(&clipped_world_rect) {
Some(rect) => rect.intersection(&unclipped_raster_rect)?,
None => return Some((unclipped, unclipped)),
};
let clipped = raster_rect_to_device_pixels(
clipped_raster_rect,

Просмотреть файл

@ -385,14 +385,21 @@ impl<Src, Dst> MatrixHelpers<Src, Dst> for Transform3D<f32, Src, Dst> {
self.m21 * self.m21 + self.m22 * self.m22 > limit2
}
/// Find out a point in `Src` that would be projected into the `target`.
fn inverse_project(&self, target: &Point2D<f32, Dst>) -> Option<Point2D<f32, Src>> {
let m: Transform2D<f32, Src, Dst>;
m = Transform2D::new(
// form the linear equation for the hyperplane intersection
let m = Transform2D::<f32, Src, Dst>::new(
self.m11 - target.x * self.m14, self.m12 - target.y * self.m14,
self.m21 - target.x * self.m24, self.m22 - target.y * self.m24,
self.m41 - target.x * self.m44, self.m42 - target.y * self.m44,
);
m.inverse().map(|inv| Point2D::new(inv.m31, inv.m32))
let inv = m.inverse()?;
// we found the point, now check if it maps to the positive hemisphere
if inv.m31 * self.m14 + inv.m32 * self.m24 + self.m44 > 0.0 {
Some(Point2D::new(inv.m31, inv.m32))
} else {
None
}
}
fn inverse_rect_footprint(&self, rect: &Rect<f32, Dst>) -> Option<Rect<f32, Src>> {
@ -609,8 +616,8 @@ use euclid::vec3;
#[cfg(test)]
pub mod test {
use super::*;
use euclid::default::{Point2D, Transform3D};
use euclid::Angle;
use euclid::default::{Point2D, Rect, Size2D, Transform3D};
use euclid::{Angle, approxeq::ApproxEq};
use std::f32::consts::PI;
#[test]
@ -624,6 +631,50 @@ pub mod test {
assert_eq!(m1.inverse_project(&p0), Some(Point2D::new(2.0, 2.0)));
}
#[test]
fn inverse_project_footprint() {
let m = Transform3D::new(
0.477499992, 0.135000005, -1.0, 0.000624999986,
-0.642787635, 0.766044438, 0.0, 0.0,
0.766044438, 0.642787635, 0.0, 0.0,
1137.10986, 113.71286, 402.0, 0.748749971,
);
let r = Rect::new(Point2D::zero(), Size2D::new(804.0, 804.0));
{
let points = &[
r.origin,
r.top_right(),
r.bottom_left(),
r.bottom_right(),
];
let mi = m.inverse().unwrap();
// In this section, we do the forward and backward transformation
// to confirm that its bijective.
// We also do the inverse projection path, and confirm it functions the same way.
println!("Points:");
for p in points {
let pp = m.transform_point2d_homogeneous(*p);
let p3 = pp.to_point3d().unwrap();
let pi = mi.transform_point3d_homogeneous(p3);
let px = pi.to_point2d().unwrap();
let py = m.inverse_project(&pp.to_point2d().unwrap()).unwrap();
println!("\t{:?} -> {:?} -> {:?} -> ({:?} -> {:?}, {:?})", p, pp, p3, pi, px, py);
assert!(px.approx_eq_eps(p, &Point2D::new(0.001, 0.001)));
assert!(py.approx_eq_eps(p, &Point2D::new(0.001, 0.001)));
}
}
// project
let rp = project_rect(&m, &r, &Rect::new(Point2D::zero(), Size2D::new(1000.0, 1000.0))).unwrap();
println!("Projected {:?}", rp);
// one of the points ends up in the negative hemisphere
assert_eq!(m.inverse_project(&rp.origin), None);
// inverse
if let Some(ri) = m.inverse_rect_footprint(&rp) {
// inverse footprint should be larger, since it doesn't know the original Z
assert!(ri.contains_rect(&r), "Inverse {:?}", ri);
}
}
fn validate_convert(xref: &LayoutTransform) {
let so = ScaleOffset::from_transform(xref).unwrap();
let xf = so.to_transform();

Просмотреть файл

@ -0,0 +1,23 @@
# Tests that `get_raster_rects` raster -> world transform is inversible in general,
# but one of the vertices of the world rectangles can't map back to the raster.
---
root:
items:
- type: stacking-context
bounds: 0 0 400 400
perspective: 800
perspective-origin: 50% 200
items:
- type: stacking-context
bounds: 0 0 400 400
transform-style: preserve-3d
transform: rotate-z(40) translate(400, 200, 0)
margin: 100
items:
- type: stacking-context
bounds: 0 0 1000 1000
transform: rotate-y(-75) translate(0, 0, -500)
items:
- type: rect
bounds: [0, 0, 200, 200]
color: red

Просмотреть файл

@ -52,3 +52,4 @@ skip_on(android) == raster-root-scaling.yaml raster-root-scaling-ref.yaml
skip_on(android) == raster-root-scaling-2.yaml raster-root-scaling-2-ref.yaml
# Make sure we don't panic
!= raster-root-huge-scale.yaml blank.yaml
!= non-inversible-world-rect.yaml blank.yaml