webrender/
rectangle_occlusion.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 http://mozilla.org/MPL/2.0/. */
4
5//! A simple occlusion culling algorithm for axis-aligned rectangles.
6//!
7//! ## Output
8//!
9//! Occlusion culling results in two lists of rectangles:
10//! 
11//! - The opaque list should be rendered first. None of its rectangles overlap so order doesn't matter
12//!   within the opaque pass.
13//! - The non-opaque list (or alpha list) which should be rendered in back-to-front order after the opaque pass.
14//!
15//! The output has minimal overdraw (no overdraw at all for opaque items and as little as possible for alpha ones).
16//!
17//! ## Algorithm overview
18//!
19//! The occlusion culling algorithm works in front-to-back order, accumulating rectangle in opaque and non-opaque lists.
20//! Each time a rectangle is added, it is first tested against existing opaque rectangles and potentially split into visible
21//! sub-rectangles, or even discarded completely. The front-to-back order ensures that once a rectangle is added it does not
22//! have to be modified again, making the underlying data structure trivial (append-only).
23//!
24//! ## splitting
25//!
26//! Partially visible rectangles are split into up to 4 visible sub-rectangles by each intersecting occluder.
27//!
28//! ```ascii
29//!  +----------------------+       +----------------------+
30//!  | rectangle            |       |                      |
31//!  |                      |       |                      |
32//!  |  +-----------+       |       +--+-----------+-------+
33//!  |  |occluder   |       |  -->  |  |\\\\\\\\\\\|       |
34//!  |  +-----------+       |       +--+-----------+-------+
35//!  |                      |       |                      |
36//!  +----------------------+       +----------------------+
37//! ```
38//!
39//! In the example above the rectangle is split into 4 visible parts with the central occluded part left out.
40//!
41//! This implementation favors longer horizontal bands instead creating nine-patches to deal with the corners.
42//! The advantage is that it produces less rectangles which is good for the performance of the algorithm and
43//! for SWGL which likes long horizontal spans, however it would cause artifacts if the resulting rectangles
44//! were to be drawn with a non-axis-aligned transformation.
45//!
46//! ## Performance
47//!
48//! The cost of the algorithm grows with the number of opaque rectangle as each new rectangle is tested against
49//! all previously added opaque rectangles.
50//!
51//! Note that opaque rectangles can either be added as opaque or non-opaque. This means a trade-off between
52//! overdraw and number of rectangles can be explored to adjust performance: Small opaque rectangles, especially
53//! towards the front of the scene, could be added as non-opaque to avoid causing many splits while adding only 
54//! a small amount of overdraw.
55//!
56//! This implementation is intended to be used with a small number of (opaque) items. A similar implementation
57//! could use a spatial acceleration structure for opaque rectangles to perform better with a large amount of
58//! occluders.
59//!
60
61use euclid::point2;
62use smallvec::SmallVec;
63use api::units::*;
64
65/// A visible part of a rectangle after occlusion culling.
66#[derive(Debug, PartialEq)]
67pub struct Item<K> {
68    pub rectangle: DeviceBox2D,
69    pub key: K,
70}
71
72/// A builder that applies occlusion culling with rectangles provided in front-to-back order.
73pub struct FrontToBackBuilder<K> {
74    opaque_items: Vec<Item<K>>,
75    alpha_items: Vec<Item<K>>,
76}
77
78impl<K> FrontToBackBuilder<K> where K: Copy {
79
80    /// Pre-allocating constructor.
81    pub fn with_capacity(opaque: usize, alpha: usize) -> Self {
82        FrontToBackBuilder {
83            opaque_items: Vec::with_capacity(opaque),
84            alpha_items: Vec::with_capacity(alpha),
85        }
86    }
87
88    /// Add a rectangle, potentially splitting it and discarding the occluded parts if any.
89    ///
90    /// Returns true the rectangle is at least partially visible.
91    pub fn add(&mut self, rect: &DeviceBox2D, is_opaque: bool, key: K) -> bool {
92        let mut fragments: SmallVec<[DeviceBox2D; 16]> = SmallVec::new();
93        fragments.push(*rect);
94
95        for item in &self.opaque_items {
96            if fragments.is_empty() {
97                break;
98            }
99            if item.rectangle.intersects(rect) {
100                apply_occluder(&item.rectangle, &mut fragments);
101            }
102        }
103
104        let list = if is_opaque {
105            &mut self.opaque_items
106        } else {
107            &mut self.alpha_items
108        };
109
110        for rect in &fragments {
111            list.push(Item {
112                rectangle: *rect,
113                key,
114            });
115        }
116
117        !fragments.is_empty()
118    }
119
120    /// Returns true if the provided rect is at least partially visible, without adding it.
121    pub fn test(&self, rect: &DeviceBox2D) -> bool {
122        let mut fragments: SmallVec<[DeviceBox2D; 16]> = SmallVec::new();
123        fragments.push(*rect);
124
125        for item in &self.opaque_items {
126            if item.rectangle.intersects(rect) {
127                apply_occluder(&item.rectangle, &mut fragments);
128            }
129        }
130
131        !fragments.is_empty()
132    }
133
134    /// The visible opaque rectangles (front-to-back order).
135    pub fn opaque_items(&self) -> &[Item<K>] {
136        &self.opaque_items
137    }
138
139    /// The visible non-opaque rectangles (front-to-back order).
140    pub fn alpha_items(&self) -> &[Item<K>] {
141        &self.alpha_items
142    }
143}
144
145
146// Split out the parts of the rects in the provided vector
147fn apply_occluder(occluder: &DeviceBox2D, rects: &mut SmallVec<[DeviceBox2D; 16]>) {
148    // Iterate in reverse order so that we can push new rects at the back without
149    // visiting them;
150    let mut i = rects.len() - 1;
151    loop {
152        let r = rects[i];
153
154        if r.intersects(occluder) {
155            let top = r.min.y < occluder.min.y;
156            let bottom = r.max.y > occluder.max.y;
157            let left = r.min.x < occluder.min.x;
158            let right = r.max.x > occluder.max.x;
159
160            if top {
161                rects.push(DeviceBox2D {
162                    min: r.min,
163                    max: point2(r.max.x, occluder.min.y),
164                });
165            }
166
167            if bottom {
168                rects.push(DeviceBox2D {
169                    min: point2(r.min.x, occluder.max.y),
170                    max: r.max,
171                });
172            }
173
174            if left {
175                let min_y = r.min.y.max(occluder.min.y);
176                let max_y = r.max.y.min(occluder.max.y);
177                rects.push(DeviceBox2D {
178                    min: point2(r.min.x, min_y),
179                    max: point2(occluder.min.x, max_y),
180                });
181            }
182
183            if right {
184                let min_y = r.min.y.max(occluder.min.y);
185                let max_y = r.max.y.min(occluder.max.y);
186                rects.push(DeviceBox2D {
187                    min: point2(occluder.max.x, min_y),
188                    max: point2(r.max.x, max_y),
189                });
190            }
191
192            // Remove the original rectangle, replacing it with
193            // one of the new ones we just added, or popping it
194            // if it is the last item.
195            if i == rects.len() {
196                rects.pop();
197            } else {
198                rects.swap_remove(i);
199            }
200        }
201
202        if i == 0 {
203            break;
204        }
205
206        i -= 1;
207    }
208}