Skip to content

Commit

Permalink
insert code block for force field in update shader
Browse files Browse the repository at this point in the history
  • Loading branch information
eliotbo committed Apr 6, 2022
1 parent 0dc831d commit 0eda38c
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 140 deletions.
41 changes: 18 additions & 23 deletions examples/force_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ fn setup(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let mut camera = OrthographicCameraBundle::new_3d();
camera.orthographic_projection.scale = 1.2;
camera.transform.translation.z = camera.orthographic_projection.far / 2.0 * 1.0;
// let mut camera = OrthographicCameraBundle::new_3d();
let mut camera = PerspectiveCameraBundle::new_3d();
// camera.orthographic_projection.scale = 1.2;
camera.transform.translation.z = 6.0;
commands.spawn_bundle(camera);

let attractor1_position = Vec3::new(0.01, 0.0, 0.0);
Expand Down Expand Up @@ -96,19 +97,21 @@ fn setup(
..Default::default()
})
.update(bevy_hanabi::PullingForceFieldModifier::new(vec![
PullingForceFieldParam {
ForceFieldParam {
position_or_direction: attractor2_position,
max_radius: 1000000.0,
min_radius: BALL_RADIUS * 6.0,
mass: -1.0,
force_type: PullingForceType::Linear,
mass: 3.0,
force_type: ForceType::Linear,
conform_to_sphere: true,
},
PullingForceFieldParam {
ForceFieldParam {
position_or_direction: attractor1_position,
max_radius: 1000000.0,
min_radius: BALL_RADIUS * 6.0,
mass: -0.5,
force_type: PullingForceType::Quadratic,
force_type: ForceType::Quadratic,
conform_to_sphere: false,
},
]))
.render(SizeOverLifetimeModifier {
Expand Down Expand Up @@ -142,13 +145,12 @@ pub struct MousePosition {

fn record_mouse_events_system(
mut cursor_moved_events: EventReader<CursorMoved>,
mut cursor_res: ResMut<MousePosition>,
mut mouse_position: ResMut<MousePosition>,
mut windows: ResMut<Windows>,
cam_transform_query: Query<&Transform, With<OrthographicProjection>>,
cam_ortho_query: Query<&OrthographicProjection>,
cam_transform_query: Query<&Transform, With<PerspectiveProjection>>,
) {
for event in cursor_moved_events.iter() {
let cursor_in_pixels = event.position; // lower left is origin
let cursor_in_pixels = event.position;
let window_size = Vec2::new(
windows.get_primary_mut().unwrap().width(),
windows.get_primary_mut().unwrap().height(),
Expand All @@ -158,18 +160,11 @@ fn record_mouse_events_system(

let cam_transform = cam_transform_query.iter().next().unwrap();

let mut scale = 1.0;

for ortho in cam_ortho_query.iter() {
scale = ortho.scale;
}

let cursor_vec4: Vec4 = cam_transform.compute_matrix()
* screen_position.extend(0.0).extend(1.0 / (scale))
* scale
/ 350.0; // Why 350?
// TODO: use bevy_mod_picking instead
let cursor_vec4: Vec4 =
cam_transform.compute_matrix() * screen_position.extend(0.0).extend(1.0) / 145.0;

let cursor_pos = Vec2::new(cursor_vec4.x, cursor_vec4.y);
cursor_res.position = cursor_pos;
mouse_position.position = cursor_pos;
}
}
11 changes: 6 additions & 5 deletions src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ use bevy::{
use serde::{Deserialize, Serialize};

use crate::{
modifiers::PullingForceFieldParam, modifiers::FFNUM, Gradient, InitModifier, RenderModifier,
Spawner, UpdateModifier,
modifiers::{ForceFieldParam, FFNUM},
Gradient, InitModifier, RenderModifier, Spawner, UpdateModifier,
};

#[derive(Default, Clone)]
pub struct InitLayout {
pub position_code: String,
pub force_field_code: String,
}

#[derive(Default, Clone, Copy)]
pub struct UpdateLayout {
/// Constant accelereation to apply to all particles.
/// Generally used to simulate some kind of gravity.
pub accel: Vec3,
pub force_field: [PullingForceFieldParam; FFNUM],
pub box_lower_bounds: Vec3,
pub box_upper_bounds: Vec3,
/// Array of force field components with a maximum number of components determined by [`FFNUM`].
pub force_field: [ForceFieldParam; FFNUM],
}

#[derive(Default, Clone)]
Expand All @@ -35,6 +35,7 @@ pub struct RenderLayout {
pub particle_texture: Option<Handle<Image>>,

pub lifetime_color_gradient: Option<Gradient<Vec4>>,

pub size_color_gradient: Option<Gradient<Vec2>>,
}

Expand Down
8 changes: 4 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ pub use asset::EffectAsset;
pub use bundle::ParticleEffectBundle;
pub use gradient::{Gradient, GradientKey};
pub use modifiers::{
AccelModifier, ColorOverLifetimeModifier, InitModifier, ParticleTextureModifier,
PositionCircleModifier, PositionSphereModifier, PullingForceFieldModifier,
PullingForceFieldParam, PullingForceType, RenderModifier, ShapeDimension,
SizeOverLifetimeModifier, UpdateModifier, FFNUM,
AccelModifier, ColorOverLifetimeModifier, ForceFieldParam, ForceType, InitModifier,
ParticleTextureModifier, PositionCircleModifier, PositionSphereModifier,
PullingForceFieldModifier, RenderModifier, ShapeDimension, SizeOverLifetimeModifier,
UpdateModifier, FFNUM,
};
pub use plugin::HanabiPlugin;
pub use render::EffectCacheId;
Expand Down
47 changes: 28 additions & 19 deletions src/modifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ impl UpdateModifier for AccelModifier {

/// The [`PullingForceFieldParam`] allows for either a constant force accross all of space, or a
/// pulling/repulsing force originating from a point source.
#[derive(Clone, Copy)]
pub enum PullingForceType {
#[derive(Clone, Copy, PartialEq)]
pub enum ForceType {
/// Constant field in all space. The position_or_direction field is interpreted as a direction.
/// For this particular case, the radii field of [`PullingForceFieldParam`] are ignored.
Constant,
Expand All @@ -247,29 +247,33 @@ pub enum PullingForceType {

/// Cubic pulling force. The position_or_direction field is interpreted as the source position.
Cubic,

/// None is the default. No force field is applied.
None,
}

impl Default for PullingForceType {
impl Default for ForceType {
fn default() -> Self {
PullingForceType::Constant
ForceType::Constant
}
}

impl PullingForceType {
impl ForceType {
/// Converts the instance of PullingForceType into an integer, preparing to be sent to the GPU.
pub fn to_int(self) -> i32 {
match self {
PullingForceType::Constant => 0,
PullingForceType::Linear => 1,
PullingForceType::Quadratic => 2,
PullingForceType::Cubic => 3,
ForceType::Constant => 0,
ForceType::Linear => 1,
ForceType::Quadratic => 2,
ForceType::Cubic => 3,
ForceType::None => -1,
}
}
}

/// Parameters for the components making the force field.
#[derive(Clone, Copy)]
pub struct PullingForceFieldParam {
pub struct ForceFieldParam {
/// The particle_update.wgsl shader interprets this field as a position when the force type is set
/// to either [`PullingForceType::Linear`], [`PullingForceType::Quadratic`] or [`PullingForceType::Cubic`].
/// In the case of a [`PullingForceType::Constant`], the field is interpreted is as the direction
Expand All @@ -282,23 +286,28 @@ pub struct PullingForceFieldParam {
/// For a pulling/repulsing force, this is the minimum radius of the sphere of influence, inside of which
/// the force field is null, avoiding the singularity at the source position.
pub min_radius: f32,
/// Mass is proportional to the intensity of the force. Note that the particles_update.wgsl shader will
/// The intensity of the force is proportional to mass. Note that the particles_update.wgsl shader will
/// ignore all subsequent force field components after it encounters a component with a mass of zero.
/// To change the force from an attracting one to a repulsive one, simply set the mass to a negative value.
pub mass: f32,
/// Defines whether the field is constant or a pulling/repulsing force with an exponent of either one,
/// two or three.
pub force_type: PullingForceType,
pub force_type: ForceType,
/// If set to true, the particles that enter within the `min_radius` will conform to a sphere around the
/// source position, appearing like a recharging effect.
pub conform_to_sphere: bool,
}

impl Default for PullingForceFieldParam {
impl Default for ForceFieldParam {
fn default() -> Self {
// defaults to no force field (a mass of 0)
PullingForceFieldParam {
ForceFieldParam {
position_or_direction: Vec3::new(0., 0., 0.),
min_radius: 0.1,
max_radius: 0.0,
mass: 0.,
force_type: PullingForceType::Constant,
force_type: ForceType::None,
conform_to_sphere: false,
}
}
}
Expand All @@ -308,7 +317,7 @@ impl Default for PullingForceFieldParam {
#[derive(Default, Clone, Copy)]
pub struct PullingForceFieldModifier {
/// Array of force field components.
pub force_field: [PullingForceFieldParam; FFNUM],
pub force_field: [ForceFieldParam; FFNUM],
}

impl PullingForceFieldModifier {
Expand All @@ -317,8 +326,8 @@ impl PullingForceFieldModifier {
/// # Panics
///
/// Panics if the number of attractors exceeds [`FFNUM`].
pub fn new(point_attractors: Vec<PullingForceFieldParam>) -> Self {
let mut force_field = [PullingForceFieldParam::default(); FFNUM];
pub fn new(point_attractors: Vec<ForceFieldParam>) -> Self {
let mut force_field = [ForceFieldParam::default(); FFNUM];

for (i, p_attractor) in point_attractors.iter().enumerate() {
if i > FFNUM {
Expand All @@ -332,7 +341,7 @@ impl PullingForceFieldModifier {

/// Perhaps will be deleted in the future.
// Delete me?
pub fn add_or_replace(&mut self, point_attractor: PullingForceFieldParam, index: usize) {
pub fn add_or_replace(&mut self, point_attractor: ForceFieldParam, index: usize) {
self.force_field[index] = point_attractor;
}
}
Expand Down
94 changes: 94 additions & 0 deletions src/render/force_field_code.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// force field acceleration: note that the particles do not have a mass as of yet,
// or we could say that the particles all have a mass of one, which means F = 1 * a.
var ff_acceleration: vec3<f32> = vec3<f32>(0.0);
var not_conformed_to_sphere: f32 = 1.0;

var unit_p2p_conformed: vec3<f32> = vec3<f32>(0.0);
var conforming_source: vec3<f32> = vec3<f32>(0.0);
var conforming_radius: f32 = 0.0;

for (var kk: i32 = 0; kk < 16; kk=kk+1) {
// As soon as a field component has a null mass, skip it and all subsequent ones.
// Is this better than not having the if statement in the first place?
// Likely answer:
// The if statement is probably good in this case because all the particles will encounter
// the same number of field components.
if (spawner.force_field[kk].mass == 0.0) {
break;
}

let particle_to_point_source = vPos - spawner.force_field[kk].position_or_direction;
let distance = length(particle_to_point_source);
let unit_p2p = normalize(particle_to_point_source) ;

let min_dist_check = step(spawner.force_field[kk].min_radius, distance);
let max_dist_check = 1.0 - step(spawner.force_field[kk].max_radius, distance);
let force_type_check = 1.0 - step(f32(spawner.force_field[kk].force_type), 0.5); // 1.0 when constant field

// this turns into 0 when the field is an attractor and the particle is inside the min_radius and the source
// is an attractor.
if (spawner.force_field[kk].conform_to_sphere > 0.5) {
not_conformed_to_sphere = not_conformed_to_sphere
* max(min_dist_check, -(sign(spawner.force_field[kk].mass) - 1.0) / 2.0);

unit_p2p_conformed =
unit_p2p_conformed
+ (1.0 - not_conformed_to_sphere)
* unit_p2p
* (1.0 - min_dist_check);

conforming_source =
conforming_source
+ (1.0 - not_conformed_to_sphere)
* spawner.force_field[kk].position_or_direction
* (1.0 - min_dist_check);

conforming_radius = conforming_radius
+ (1.0 - not_conformed_to_sphere)
* spawner.force_field[kk].min_radius / 1.2
* (1.0 - min_dist_check);
}

let constant_field = (1.0 - force_type_check) * normalize(spawner.force_field[kk].position_or_direction);

let point_source_force =
- force_type_check * unit_p2p
* min_dist_check * max_dist_check
* spawner.force_field[kk].mass /
(0.0000001 + pow(distance, f32(spawner.force_field[kk].force_type)));


let force_component = constant_field + point_source_force;

// if the particle is within the min_radius of a source, then forget about
// the other sources and only use the conformed field, thus the "* min_dist_check"
ff_acceleration = ff_acceleration * min_dist_check + force_component;
}

// conform to a sphere of radius min_radius/2 by projecting the velocity vector
// onto a plane that is tangent to the sphere.
let eps = vec3<f32>(0.000001);
let projected_on_sphere = vVel - proj(unit_p2p_conformed + eps, vVel + eps);
let conformed_field =
(1.0 - not_conformed_to_sphere) * normalize(projected_on_sphere) * length(vVel);

///////////// End of force field computation /////////////


// // Euler integration
vVel = (vVel + (spawner.accel * sim_params.dt) + (ff_acceleration * sim_params.dt))
* not_conformed_to_sphere + conformed_field;


// let temp_vPos = vPos;
vPos = (vPos + (vVel * sim_params.dt));


// project on the sphere if within conforming distance
let pos_to_source = conforming_source - vPos ;
let difference = length(pos_to_source) - conforming_radius;
vPos = vPos + difference * normalize(pos_to_source ) * (1.0 - not_conformed_to_sphere) ;

// // commented because of the potential bug where dt could be zero, although the simulation
// // works anyways, needs investigation
// vVel = (vPos - temp_vPos) / sim_params.dt;
Loading

0 comments on commit 0eda38c

Please sign in to comment.