-
Notifications
You must be signed in to change notification settings - Fork 9
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
Comments
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. |
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 |
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. 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)? |
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.
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.
So far:
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 |
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. |
I believe the merging behaviour is reasonably doable using Particular. In my demo code you mentioned the 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. 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 (missclicked closed the issue) |
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.
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.
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. |
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. 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!) |
I'm working on a silly game thing using Particular and having a great time. Thanks for the cool
toytool.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...The text was updated successfully, but these errors were encountered: