Skip to content
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
merged 4 commits into from
Jul 21, 2023

Conversation

nicopap
Copy link
Contributor

@nicopap nicopap commented Jul 16, 2023

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
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
}

The Gold Kronecker sequence

Rust implementation

Note that the implementation suggested in the linked post assumes floats, we have integers

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
}

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):

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 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

@nicopap nicopap added this to the 0.11.1 milestone Jul 16, 2023
@nicopap 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
@nicopap nicopap requested a review from nakedible July 21, 2023 06:41
@nicopap 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 cart added this pull request to the merge queue Jul 21, 2023
Merged via the queue into bevyengine:main with commit cd92405 Jul 21, 2023
21 checks passed
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)
@nicopap nicopap deleted the quasi-random-bb-colors branch August 30, 2023 13:36
@cart cart mentioned this pull request Oct 13, 2023
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants