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

Removing particles #2

Closed
stnbu opened this issue Oct 15, 2022 · 8 comments
Closed

Removing particles #2

stnbu opened this issue Oct 15, 2022 · 8 comments

Comments

@stnbu
Copy link

stnbu commented Oct 15, 2022

I'm working on a silly game thing using Particular and having a great time. Thanks for the cool toy tool.

In bevy, I wanted to be able to detect a collision and "merge" the particles (think of two planets colliding and forming a new bigger one, but without all the ejecta and excitement).

So it would be great to be able to delete particles within a bevy system. I can already add them. Would it be a problem to allow for deletion?

I see that in your demo you rebuild the whole particle set in the PreUpdate stage. Should I consider doing something similar?

I'd be happy to work on a PR for this. I wonder if it would make sense for ParticleSet also to be implemented as a trait...

@Canleskis
Copy link
Owner

Removing elements stored in vectors can be tricky since can't use the remove method, which uses indexes, but we can't keep track of each particle's index since removing an element could change every element's index. We could use the retain method, but that would require to be able to compare particles and could get pretty expensive when the ParticleSet gets bigger.

The usual alternative is to use a generational arena, but since constructing the ParticleSet is very cheap compared to calculating the accelerations, I recommend doing that (it should have been somewhere in the docs, next release will mention it). Later on we might use a generational arena if constructing the ParticleSet somehow becomes more expensive.

Although a trait implemented on some kinda of storage structure would be possible, that could complicate the API since users would need to create an appropriate structure and ensure the different optimizations can be used on it. The trait would probably need to have at least two methods to describe massive and massless particles in the user's structure.

It could also be done as a set of methods operating over an iterator of particles, but then separating the particles depending on if they have mass or not would need to be done on every method call, which would be very expensive compared to storing them in different structures when they are added.

It's definitely an interesting idea though that I would like to look into as it could allow for more flexibility for users that need it.

@stnbu
Copy link
Author

stnbu commented Oct 16, 2022

Thanks! I am reading/grokking your links. Thanks for the comprehensive response! It's making some sense to me.

Just an aside, I stole your math and tried using a bevy query in place of a particle set. It seems to work (and it's obviously very context/use-specific, so my problems are easier than your problems) and it's probably broken in some subtle way...

#[derive(Component)]
pub struct Momentum {
    velocity: Vec3,
    mass: f32,
}

pub fn freefall(mut query: Query<(Entity, &mut Transform, &mut Momentum)>, time: Res<Time>) {
    let masses = query
        .iter()
        .map(|t| (t.0, t.1.translation, t.2.mass))
        .collect::<Vec<_>>();
    let accelerations = masses.iter().map(|particle1| {
        masses.iter().fold(Vec3::ZERO, |acceleration, particle2| {
            let dir = particle2.1 - particle1.1;
            let mag_2 = dir.length();
            let grav_acc = if mag_2 != 0.0 {
                dir * particle2.2 / (mag_2 * mag_2.sqrt())
            } else {
                dir
            };
            acceleration + grav_acc
        })
    });
    let dt = time.delta_seconds();
    for ((entity, _, mass), force) in masses.iter().zip(accelerations) {
        if let Ok((_, mut transform, mut momentum)) = query.get_mut(*entity) {
            momentum.velocity += (force * dt) / *mass;
            transform.translation += momentum.velocity * dt;
        }
    }
}

I'm building the Vec out of the query with each pass, which is probably expensive. Fully expecting other problems ;-)

@Canleskis
Copy link
Owner

Canleskis commented Oct 16, 2022

My first implementation was also strongly coupled with Bevy's queries, it worked well, but I wanted to decouple it to be able to bench it more thoroughly and extend the feature set, which made Particular happen 😁.

It's more likely that computing the accelerations is a lot more expensive than collection your query into a Vec since there are no maths involved, so that isn't really a problem. In particular, all the particles are collected into two different Vecs that we iterate over before computing the accelerations, and that operation usually takes about 1/100th of the total computing time.

I can see how we could have the ParticleSet being a trait that can be implemented on types such as your query:

Query<(Entity, &mut Transform, &mut Momentum)>

But I think this would end up making the API complicated; I believe it's easier to simply provide a type with a position and a 'mass' than can then be added to a structure that handles all the (pseudo) complicated iteration stuff needed to compute the accelerations. If we were to use such an implementation, this is probably what it would look like for a similar query:

impl ComputableSet for Query<'_, '_, (Entity, &mut GlobalTransform, &mut PointMass)> {
    fn massive_particles(&self) -> Vec<(Vec3, f32)> {
        self.into_iter()
            .filter_map(|(_, transform, mass)| match mass {
                PointMass::HasGravity { mass } => Some((transform.translation(), *mass)),
                PointMass::AffectedByGravity => None,
            })
            .collect()
    }

    fn massless_particles(&self) -> Vec<(Vec3, f32)> {
        self.into_iter()
            .filter_map(|(_, transform, mass)| match mass {
                PointMass::HasGravity { .. }  => None,
                PointMass::AffectedByGravity => Some((transform.translation(), 0.0)),
            })
            .collect()
    }
}

We can use those two methods to compute the accelerations, but having to do (in this example) two match statements for every particle at every acceleration computation would probably get a bit expensive. We also lose some of the handy methods the ParticleSet offers.
I could probably have such a public trait available in Particular that is implemented for the ParticleSet and can be reused for user-defined structures, but I question its utility as in the vast majority of cases the ParticleSet gets the job done.

Edit: Also, you can't implement foreign traits for foreign types, so in most cases this would not work.

If I may ask, why can't you use the ParticleSet in a similar fashion to the way it's used in the demo? Do you need something specific that's not present in the current API (except the remove)?

@stnbu
Copy link
Author

stnbu commented Oct 17, 2022

I wanted to decouple it to be able to bench it more thoroughly and extend the feature set

Oh yeah, I dumbed it way down. I DE-generalized it. Speaking of that kind of thing, I'll probably want to run simulations with my setup with bevy completely out of the way as a way of doing "time travel". It's supposed to be a sort of game, so it'd be nice to let my computer calculate the distant-future universe for different starting conditions. Too much or too few of some things might make for impossible/boring game play. Would be nice to run experiments... quickly. And run thousands of them overnight. What I have now definitely has bevy very much in the way.

those two methods

I'll look into experimenting with this idea. It might fit the bill. In my case, I'll have very few particles and can just assume they're all massive, which simplifies prototyping code also.

Do you need something specific

So far:

  1. Removing (as mentioned)
  2. Adding

I'll be conserving momentum and all that because I'll be merging my "planets". I do this currently by removing the two colliders and spawning their "child". When they make contact, their mass, momentum are weight-averaged and combined. Like actual planets but with less fire.

You can see that in action here. (You'll also encounter the freezing bug that happens every dozen collisions or so... 🤷 ). That corresponds to roughly this state of the repo.

I could probably use particular but this (stealing your acceleration code) worked out to be simpler (and .... maybe kludgy and whatnot. I still got my shaky legs WRT bevy and rust.)


Edit: Simpler because updating the ParticleSet resource separately turns out to complicate the code a lot. Notice I use rapier. I wonder if rapier can do any of this. I'm misusing/abusing a lot of its features too 😁

@stnbu
Copy link
Author

stnbu commented Oct 17, 2022

Ya know... Even "point masses" can get trapped in their own isolated, stable orbits with one-another and behave like a single body. I wonder if a sense of "merging" has any general appeal/use.

@Canleskis Canleskis reopened this Oct 17, 2022
@Canleskis
Copy link
Owner

Canleskis commented Oct 17, 2022

I believe the merging behaviour is reasonably doable using Particular. In my demo code you mentioned the ParticleSet is rebuilt in the PreUpdate stage. This happens at every physic iteration (basically every frame with bevy-rapier). Knowing this, every system that runs in the Update stage (and after the particles are accelerated with Particular's result method), you can do anything to the Bevy entities, which include removing them or creating new ones.
You don't need to interact with the ParticleSet when you remove those entities, as they will simply not be included when the new ParticleSet is created. Same goes for the newly added ones.

If you copy the code from the demo, and then add your system merging the colliding Planets (ensuring it runs after all Particular related systems), you can simply remove the Planets involved in the collision and then add a new one with the rules you defined, ensuring it has the components for the Particular related systems to include them.

I hope I understood your problem well enough and that can help you!

Regarding the time travel thing you mentioned, I have thought about this before to visualize a particle's future position. What you can do using Particular is running the result method with the integrator used by Rapier (it uses symplectic Euler) as many times as needed, collecting the computed positions. It would look something like this:

for (acceleration, particle) in particle_set.result() {
    particle.velocity += acceleration * DT;
    particle.position += particle.velocity * DT;
    particle.points.push(particle.position);
}

Particular is fast enough (without the parallel feature because of the body count) to run these a couple hundred thousand times per second with not many bodies. On my machine, a single step with 50 bodies takes 6 µs.
With the collected points you could draw the predicted path for example, or analyse the data in any way you need. Predicting the Rapier collisions would be a bit more difficult, as it would require multiple pipelines and I don't thing bevy-rapier supports that yet. You would be able to predict when a collision is predicted to happen without Rapier though, by checking every particle's position relative to one another for each collected data points at a given step.

An important point I need to mention regarding bevy-rapier; the physic rate can't be fixed by the user. If you set its configuration to a fixed delta-time, the speed of the simulation will depend on your framerate. If you used the default setup, the physics rate would be the same as your framerate. This can be a problem for things like predicting future positions as the DT used would only be valid for the frames that took that amount of time to be processed. With an integrator such as symplectic Euler, this is very noticeable very quickly, so beware of that if you intend to do that kind of thing.

(missclicked closed the issue)

@stnbu
Copy link
Author

stnbu commented Oct 19, 2022

I hope I understood your problem well enough and that can help you!

For sure. I'm not crazy about my diy situation. I was more about reducing clutter so that I can think more clearly ...because I am still trying to grok all kinds of stuff. I'm thinking I was doing a lot of hand-standing because I didn't/still don't know the direct, obvious solution (for many given rust puzzles).

I'll work on re-adopting Particular once I swat away enough bugs to focus. I've let my feature set exceed my project management skills. Got quite a bit of Ugly Code to tidy up. I'm sure particular will always be about "the" problem at hand and doing the calculations as efficiently as possible. I'm not about wasting G/CPU.

Oh yeah, and "stages" scare me. I need to better understand all that.

rapier..predicting

I probably should just find a simpler but good-enough collision detection scheme and eliminate rapier. I'm probably wasting some resources with who-knows-what rapier is doing under the hood needlessly.

And thanks for the code examples/tips. I'm sure I can drop Particular back in and stop being all confused like I was.

(missclicked closed the issue)

Heh, well. I'm all over the place. It's not exactly a misbehavior or bug in the first place. I'll close it but will respond here as appropriate. No biggie.

@stnbu stnbu closed this as completed Oct 19, 2022
@Canleskis
Copy link
Owner

Canleskis commented Oct 19, 2022

Learning Rust and Bevy can be tricky, there are lots of concepts to wrap your head around. If you need any help or are unsure about something, don't hesitate asking me! I've spent quite some time playing around with Bevy and I believe my grasp on it is pretty firm at this point!

Regarding Rapier, I think it's pretty efficient for what it does and the code is not that wasteful. I think it has more to do with the fact that it's hidden under multiple layers of integration and feature sets especially with Bevy, which makes it look all complicated.
There are crates that I have never personally tested but look promising though, and I think trying them out could only benefit your project!

Stages are part of a bigger problem in Bevy, and they will be going away at some point in the near future (hopefully, stageless was merged last month!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants