use std::f32::consts::PI; use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*, render::view::RenderLayers}; use bevy_hanabi::prelude::*; mod utils; use utils::*; fn main() -> Result<(), Box> { let app_exit = utils::make_test_app("gradient") .add_systems(Startup, setup) .add_systems(Update, update) .run(); app_exit.into_result() } fn setup( asset_server: Res, mut commands: Commands, mut effects: ResMut>, mut meshes: ResMut>, mut materials: ResMut>, ) { // Spawn a camera. For this example, we also demonstrate that we can use an HDR // camera. commands.spawn(( Transform::from_translation(Vec3::Z * 100.), Camera { hdr: true, ..default() }, Camera3d::default(), Tonemapping::None, // For this example, we assign to the camera a specific render layer (3) // different from the default (0) to demonstrate it works. RenderLayers::layer(3), )); let texture_handle: Handle = asset_server.load("cloud.png"); let mut gradient = Gradient::new(); gradient.add_key(0.0, Vec4::new(0.5, 0.5, 0.5, 1.0)); gradient.add_key(0.1, Vec4::new(0.5, 0.5, 0.0, 1.0)); gradient.add_key(0.4, Vec4::new(0.5, 0.0, 0.0, 1.0)); gradient.add_key(1.0, Vec4::splat(0.0)); let writer = ExprWriter::new(); let age = writer.lit(0.).expr(); let init_age = SetAttributeModifier::new(Attribute::AGE, age); let lifetime = writer.lit(5.).expr(); let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, lifetime); let init_pos = SetPositionSphereModifier { center: writer.lit(Vec3::ZERO).expr(), radius: writer.lit(1.).expr(), dimension: ShapeDimension::Volume, }; let init_vel = SetVelocitySphereModifier { center: writer.lit(Vec3::ZERO).expr(), speed: writer.lit(2.).expr(), }; // Use texture slot #0 in ParticleTextureModifier let texture_slot = writer.lit(0u32).expr(); // Define that texture slot (giving it a name for convenience) let mut module = writer.finish(); module.add_texture_slot("color"); let effect = effects.add( EffectAsset::new(32768, Spawner::rate(1000.0.into()), module) .with_name("gradient") .init(init_pos) .init(init_vel) .init(init_age) .init(init_lifetime) .render(ParticleTextureModifier { texture_slot, sample_mapping: ImageSampleMapping::ModulateOpacityFromR, }) .render(ColorOverLifetimeModifier { gradient }), ); commands .spawn(( Name::new("effect"), ParticleEffectBundle::new(effect), // We need to spawn the effect with the same render layer as the camera, otherwise it // won't be rendered. RenderLayers::layer(3), // We need to bind a texture to the slot #0 we created above EffectMaterial { images: vec![texture_handle.clone()], }, )) .with_children(|p| { p.spawn(( Mesh3d(meshes.add(Cuboid { half_size: Vec3::splat(0.5), })), MeshMaterial3d(materials.add(utils::COLOR_RED)), )); }); } /// Calculate a position over a Lemniscate of Bernoulli curve ("infinite /// symbol"). /// /// The fractional part of `time` determines the parametric position over the /// curve. The `radius` of the Lemniscate curve is the distance from the center /// to the edge of any of the left or right loops. The curve loops extend from /// -X to +X. fn lemniscate(time: f32, radius: f32) -> Vec2 { // The Lemniscate is defined in polar coordinates by the equation r² = a² ⨯ // cos(2θ), where a is the radius of the Lemniscate (distance from origin to // loop edge). This equation is defined only for values of θ in the // [-π/4:π/4] range. Each value yields two possible values for r, one // positive and one negative, corresponding to the two loops of the // Lemniscates (left and right). So we solve for θ ∈ [-π/4:π/4], and make θ // vary back and forth in the [-π/4:π/4] range. Then depending on the // direction we flip the sign of r. This produces a continuous // parametrization of the curve. const TWO_PI: f32 = PI * 2.0; const PI_OVER_4: f32 = PI / 4.0; // Scale the parametric position over the curve to the [0:2*π] range let theta = time.fract() * TWO_PI; // This variant produces a linear parametrization of theta (triangular signal). // Because the parameter r changes much faster around 0 when solving the // polar equation, this makes the position "go faster" around the center, // which is generally not wanted. let (theta, sign) = if theta <= PI_OVER_2 // { (theta - PI_OVER_4, 1.0) // } else { // (3.0 * PI_OVER_4 - theta, -1.0) // }; // That alternative variant "slows down" the parametric position around zero by // converting the linear θ variations with a sine function, making it move // slower around the edges ±π/4 where r tends to zero. This does not produce // an exact constant speed, but is visually close. let sign = theta.cos().signum(); let theta = theta.sin() * PI_OVER_4; // Solve the polar equation to build the parametric position. Clamp to positive // values for r2 due to numerical errors infrequently yielding negative values. let r2 = (radius * radius * (theta * 2.0).cos()).max(0.); let r = r2.sqrt().copysign(sign); // Convert to cartesian coordinates let x = r * theta.cos(); let y = r * theta.sin(); Vec2::new(x, y) } fn update(time: Res