rectangle_occlusion.rs (7526B)
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 61 use euclid::point2; 62 use smallvec::SmallVec; 63 use api::units::*; 64 65 /// A visible part of a rectangle after occlusion culling. 66 #[derive(Debug, PartialEq)] 67 pub 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. 73 pub struct FrontToBackBuilder<K> { 74 opaque_items: Vec<Item<K>>, 75 alpha_items: Vec<Item<K>>, 76 } 77 78 impl<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 147 fn 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 }