-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace AHash with a good sequence for entity AABB colors #9175
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
nicopap
added
C-Feature
A new feature, making something new possible
C-Usability
A targeted quality-of-life change that makes Bevy easier to use
A-Gizmos
Visual editor and debug gizmos
labels
Jul 16, 2023
nakedible
reviewed
Jul 16, 2023
mockersf
reviewed
Jul 21, 2023
mockersf
approved these changes
Jul 21, 2023
nakedible
approved these changes
Jul 21, 2023
nicopap
added
the
S-Ready-For-Final-Review
This PR has been approved by the community. It's ready for a maintainer to consider merging it
label
Jul 21, 2023
cart
approved these changes
Jul 21, 2023
cart
pushed a commit
that referenced
this pull request
Aug 10, 2023
# 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)
43 tasks
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
A-Gizmos
Visual editor and debug gizmos
C-Feature
A new feature, making something new possible
C-Usability
A targeted quality-of-life change that makes Bevy easier to use
S-Ready-For-Final-Review
This PR has been approved by the community. It's ready for a maintainer to consider merging it
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Objective
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. 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
Rust implementation
The Gold Kronecker sequence
Rust implementation
Note that the implementation suggested in the linked post assumes floats, we have integers
Comparison of the sequences
So they are both pretty good. Both only have a single (!) division and two
u32 as f32
conversions.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):
quasi_random_sequence-2023-07-16.mp4
Here, we repeat the experiment, but with with
entity.to_bits()
instead of a sequence:quasi_random_entity-2023-07-16.mp4
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. Why?
tueoqs R function multiplies a rational/float and takes the fractional part of the result
(x/Φ) % 1
. We start with an integeru32
. 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 byu32::MAX / Φ
) is probably faster.Acknowledgements
inspi
on discord linked me to https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ and the wikipedia article.u32
rather than thef32
.nakedible
for suggesting theindex()
overto_bits()
which considerably reduces generated code (goes from 50 to 11 instructions)