pbrt_r3/core/lightdistrib/
spatial.rs

1use super::lightdistrib::*;
2use crate::core::base::*;
3use crate::core::geometry::*;
4use crate::core::interaction::*;
5use crate::core::light::*;
6use crate::core::lowdiscrepancy::*;
7use crate::core::medium::*;
8use crate::core::sampling::*;
9use crate::core::scene::*;
10use crate::core::stats::*;
11//use std::collections::HashMap;
12use std::sync::Arc;
13use std::sync::Mutex;
14
15thread_local!(static N_CREATED: StatCounter = StatCounter::new("SpatialLightDistribution/Distributions created"));
16thread_local!(static N_PROBES_PER_LOOKUP: StatIntDistribution = StatIntDistribution::new("SpatialLightDistribution/Hash probes per lookup"));
17
18#[derive(Debug, Clone)]
19struct HashEntry {
20    packed_pos: u64,
21    distribution: Arc<Distribution1D>,
22}
23
24// A spatially-varying light distribution that adjusts the probability of
25// sampling a light source based on an estimate of its contribution to a
26// region of space.  A fixed voxel grid is imposed over the scene bounds
27// and a sampling distribution is computed as needed for each voxel.
28pub struct SpatialLightDistribution {
29    world_bound: Bounds3f,
30    lights: Vec<Arc<dyn Light>>,
31    voxels: [u32; 3],
32    hash_table: Vec<Mutex<Option<HashEntry>>>,
33}
34
35impl SpatialLightDistribution {
36    pub fn new(scene: &Scene, max_voxels: u32) -> Self {
37        let mut voxels = [1, 1, 1];
38
39        let b = scene.world_bound();
40        let diag = b.diagonal();
41        let bmax = diag[b.maximum_extent()];
42        for i in 0..3 {
43            voxels[i] =
44                (Float::ceil(diag[i] / bmax * max_voxels as Float) as u32).clamp(1, max_voxels);
45
46            // In the Lookup() method, we require that 20 or fewer bits be
47            // sufficient to represent each coordinate value. It's fairly hard
48            // to imagine that this would ever be a problem.
49            assert!(voxels[i] < (1 << 20));
50        }
51
52        let hash_table_size = (4 * voxels[0] * voxels[1] * voxels[2]) as usize;
53        let mut hash_table = Vec::new();
54        for _ in 0..hash_table_size {
55            hash_table.push(Mutex::new(None));
56        }
57        SpatialLightDistribution {
58            world_bound: b,
59            lights: scene.lights.clone(),
60            voxels,
61            hash_table,
62        }
63    }
64
65    pub fn make_hash(&self, packed_pos: u64) -> u64 {
66        let hash_table_size = self.hash_table.len() as u64;
67
68        // Compute a hash value from the packed voxel coordinates.  We could
69        // just take packedPos mod the hash table size, but since packedPos
70        // isn't necessarily well distributed on its own, it's worthwhile to do
71        // a little work to make sure that its bits values are individually
72        // fairly random. For details of and motivation for the following, see:
73        // http://zimbry.blogspot.ch/2011/09/better-bit-mixing-improving-on.html
74        let mut hash: u64 = packed_pos;
75        hash ^= hash.wrapping_shr(31); // hash >> 31
76        hash = hash.wrapping_mul(0x7fb5d329728ea185); //hash *= 0x7fb5d329728ea185;
77        hash ^= hash.wrapping_shr(27); //hash >> 27)
78        hash = hash.wrapping_mul(0x81dadef4bc2dd44d); //hash *= 0x81dadef4bc2dd44d;
79        hash ^= hash.wrapping_shr(33); //(hash >> 33);
80        hash %= hash_table_size;
81        return hash;
82    }
83
84    pub fn get_hash_key(&self, p: &Point3f) -> (u64, u64, [u32; 3]) {
85        // First, compute integer voxel coordinates for the given point |p|
86        // with respect to the overall voxel grid.
87        let bounds = self.world_bound.clone();
88        let offset = bounds.offset(p);
89        let mut offset = [offset[0], offset[1], offset[2]];
90        for i in 0..3 {
91            offset[i] = offset[i].clamp(0.0, 1.0);
92        }
93        let mut pi: [u32; 3] = [0; 3];
94        for i in 0..3 {
95            // The clamp should almost never be necessary, but is there to be
96            // robust to computed intersection points being slightly outside
97            // the scene bounds due to floating-point roundoff error.
98            let voxels_i = self.voxels[i];
99            pi[i] = u32::clamp(
100                (offset[i] * voxels_i as Float) as u32,
101                0,
102                (voxels_i - 1) as u32,
103            );
104        }
105        // Pack the 3D integer voxel coordinates into a single 64-bit value.
106        //let packed_pos = ((pi[0] as u64) << 40) as u64 | ((pi[1] as u64) << 20) as u64 | (pi[2]) as u64;
107        let packed_pos = ((pi[0] as u64).wrapping_shl(40)) as u64
108            | ((pi[1] as u64).wrapping_shl(20)) as u64
109            | (pi[2]) as u64;
110        return (packed_pos, self.make_hash(packed_pos), pi);
111    }
112
113    fn compute_distribution(&self, pi: &[u32; 3]) -> Arc<Distribution1D> {
114        N_CREATED.with(|c| c.inc());
115
116        // Compute the world-space bounding box of the voxel corresponding to
117        // |pi|.
118        let world_bound = self.world_bound.clone();
119        let voxels = self.voxels.as_ref();
120        let p0 = Point3f::new(
121            pi[0] as Float / voxels[0] as Float,
122            pi[1] as Float / voxels[1] as Float,
123            pi[2] as Float / voxels[2] as Float,
124        );
125        let p1 = Point3f::new(
126            (pi[0] + 1) as Float / voxels[0] as Float,
127            (pi[1] + 1) as Float / voxels[1] as Float,
128            (pi[2] + 1) as Float / voxels[2] as Float,
129        );
130        let pp0 = world_bound.lerp(&p0);
131        let pp1 = world_bound.lerp(&p1);
132        let voxel_bounds = Bounds3f::new(&pp0, &pp1);
133
134        // Compute the sampling distribution. Sample a number of points inside
135        // voxelBounds using a 3D Halton sequence; at each one, sample each
136        // light source and compute a weight based on Li/pdf for the light's
137        // sample (ignoring visibility between the point in the voxel and the
138        // point on the light source) as an approximation to how much the light
139        // is likely to contribute to illumination in the voxel.
140        let lights = &self.lights;
141        let n_samples: usize = 128;
142        let lsz = lights.len();
143        let mut light_contrib = vec![0.0; lsz];
144        for i in 0..n_samples {
145            let t = Point3f::new(
146                radical_inverse(0, i as u64),
147                radical_inverse(1, i as u64),
148                radical_inverse(2, i as u64),
149            );
150            let po = voxel_bounds.lerp(&t);
151
152            //p, n, perr, wo, time, mi
153            let intr = Interaction::from(BaseInteraction {
154                p: po, //0
155                time: 0.0,
156                p_error: Vector3f::zero(),        //2
157                n: Normal3f::zero(),              //1
158                wo: Vector3f::new(1.0, 0.0, 0.0), //3
159                medium_interface: MediumInterface::new(),
160            });
161
162            // Use the next two Halton dimensions to sample a point on the
163            // light source.
164            let u = Point2f::new(radical_inverse(3, i as u64), radical_inverse(4, i as u64));
165            for j in 0..lsz {
166                let light = lights[j].as_ref();
167                if let Some((li, _wi, pdf, _vis)) = light.sample_li(&intr, &u) {
168                    if pdf > 0.0 {
169                        // TODO: look at tracing shadow rays / computing beam
170                        // transmittance.  Probably shouldn't give those full weight
171                        // but instead e.g. have an occluded shadow ray scale down
172                        // the contribution by 10 or something.
173                        light_contrib[j] += li.y() / pdf;
174                    }
175                }
176            }
177        }
178
179        // We don't want to leave any lights with a zero probability; it's
180        // possible that a light contributes to points in the voxel even though
181        // we didn't find such a point when sampling above.  Therefore, compute
182        // a minimum (small) weight and ensure that all lights are given at
183        // least the corresponding probability.
184        let sum_contrib: Float = light_contrib.iter().sum();
185        let avg_contrib = sum_contrib / ((n_samples * lsz) as Float);
186        let min_contrib = if avg_contrib > 0.0 {
187            0.001 * avg_contrib
188        } else {
189            1.0
190        };
191        for i in 0..lsz {
192            light_contrib[i] = Float::max(min_contrib, light_contrib[i]);
193        }
194        //
195        return Arc::new(Distribution1D::new(&light_contrib));
196    }
197}
198
199impl LightDistribution for SpatialLightDistribution {
200    fn lookup(&self, p: &Point3f) -> Arc<Distribution1D> {
201        let (packed_pos, mut hash, pi) = self.get_hash_key(p);
202        assert!((hash as usize) < self.hash_table.len());
203
204        // Now, see if the hash table already has an entry for the voxel. We'll
205        // use quadratic probing when the hash table entry is already used for
206        // another value; step stores the square root of the probe step.
207        let mut step = 1;
208        let mut n_probes = 0;
209        loop {
210            n_probes += 1;
211            let mut hash_entry = self.hash_table[hash as usize].lock().unwrap();
212            // Does the hash table entry at offset |hash| match the current point?
213            if let Some(entry) = hash_entry.as_ref() {
214                let entry_packed_pos = entry.packed_pos;
215                if entry_packed_pos == packed_pos {
216                    // Yes! Most of the time, there should already by a light
217                    // sampling distribution available.
218                    return entry.distribution.clone();
219                } else {
220                    // The hash table entry we're checking has already been
221                    // allocated for another voxel. Advance to the next entry with
222                    // quadratic probing.
223                    hash += step * step;
224                    let hash_table_size = self.hash_table.len() as u64;
225                    if hash >= hash_table_size {
226                        hash %= hash_table_size;
227                    }
228                    step += 1;
229                }
230            } else {
231                // We have found an invalid entry. (Though this may have
232                // changed since the load into entryPackedPos above.)  Use an
233                // atomic compare/exchange to try to claim this entry for the
234                // current position.
235                let distrib = self.compute_distribution(&pi);
236                *hash_entry = Some(HashEntry {
237                    packed_pos,
238                    distribution: distrib.clone(),
239                });
240
241                N_PROBES_PER_LOOKUP.with(|c| c.add(n_probes));
242                return distrib;
243            }
244        }
245    }
246}
247
248unsafe impl Sync for SpatialLightDistribution {}
249unsafe impl Send for SpatialLightDistribution {}