mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
cd92405dbd
# Objective - #8960 isn't optimal for very distinct AABB colors, it can be improved ## Solution We want a function that maps sequential values (entities concurrently living in a scene _usually_ have ids that are sequential) into very different colors (the hue component of the color, to be specific) What we are looking for is a [so-called "low discrepancy" sequence](https://en.wikipedia.org/wiki/Low-discrepancy_sequence). ie: a function `f` such as for integers in a given range (eg: 101, 102, 103…), `f(i)` returns a rational number in the [0..1] range, such as `|f(i) - f(i±1)| ≈ 0.5` (maximum difference of images for neighboring preimages) AHash is a good random hasher, but it has relatively high discrepancy, so we need something else. Known good low discrepancy sequences are: #### The [Van Der Corput sequence](https://en.wikipedia.org/wiki/Van_der_Corput_sequence) <details><summary>Rust implementation</summary> ```rust fn van_der_corput(bits: u64) -> f32 { let leading_zeros = if bits == 0 { 0 } else { bits.leading_zeros() }; let nominator = bits.reverse_bits() >> leading_zeros; let denominator = bits.next_power_of_two(); nominator as f32 / denominator as f32 } ``` </details> #### The [Gold Kronecker sequence](https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) <details><summary>Rust implementation</summary> Note that the implementation suggested in the linked post assumes floats, we have integers ```rust fn gold_kronecker(bits: u64) -> f32 { const U64_MAX_F: f32 = u64::MAX as f32; // (u64::MAX / Φ) rounded down const FRAC_U64MAX_GOLDEN_RATIO: u64 = 11400714819323198485; bits.wrapping_mul(FRAC_U64MAX_GOLDEN_RATIO) as f32 / U64_MAX_F } ``` </details> ### Comparison of the sequences So they are both pretty good. Both only have a single (!) division and two `u32 as f32` conversions. - Kronecker is resilient to regular sequence (eg: 100, 102, 104, 106) while this kills Van Der Corput (consider that potentially one entity out of two spawned might be a mesh) I made a small app to compare the two sequences, available at: https://gist.github.com/nicopap/5dd9bd6700c6a9a9cf90c9199941883e At the top, we have Van Der Corput, at the bottom we have the Gold Kronecker. In the video, we spawn a vertical line at the position on screen where the x coordinate is the image of the sequence. The preimages are 1,2,3,4,… The ideal algorithm would always have the largest possible gap between each line (imagine the screen x coordinate as the color hue): https://github.com/bevyengine/bevy/assets/26321040/349aa8f8-f669-43ba-9842-f9a46945e25c Here, we repeat the experiment, but with with `entity.to_bits()` instead of a sequence: https://github.com/bevyengine/bevy/assets/26321040/516cea27-7135-4daa-a4e7-edfd1781d119 Notice how Van Der Corput tend to bunch the lines on a single side of the screen. This is because we always skip odd-numbered entities. Gold Kronecker seems always worse than Van Der Corput, but it is resilient to finicky stuff like entity indices being multiples of a number rather than purely sequential, so I prefer it over Van Der Corput, since we can't really predict how distributed the entity indices will be. ### Chosen implementation You'll notice this PR's implementation is not the Golden ratio-based Kronecker sequence as described in [tueoqs](https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/). Why? tueoqs R function multiplies a rational/float and takes the fractional part of the result `(x/Φ) % 1`. We start with an integer `u32`. So instead of converting into float and dividing by Φ (mod 1) we directly divide by Φ as integer (mod 2³²) both operations are equivalent, the integer division (which is actually a multiplication by `u32::MAX / Φ`) is probably faster. ## Acknowledgements - `inspi` on discord linked me to https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ and the wikipedia article. - [this blog post](https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/) for the idea of multiplying the `u32` rather than the `f32`. - `nakedible` for suggesting the `index()` over `to_bits()` which considerably reduces generated code (goes from 50 to 11 instructions) |
||
---|---|---|
.. | ||
src | ||
Cargo.toml |