How to Make Particles Moving along a Path?

Godot Version

4.2.1

Question

Hi Everyone

How to make particles moving along a path in Godot, like the example below?

Unity Surge plugin - Wrangling a particle system

Afaik, there’s no built-in equivalent to this. You’ll need to make a custom particle process shader. Tesselate the curve and encode the positions into a texture. In the shader, sample the texture with particle lifetime as a sampling coordinate and position the particle there. For “variable stroke” effect you can encode the stroke radius into alpha channel of the texture and let the particle pick its distance from the path between zero and that value.

Wow, that sounds complicated. I think Unity and Unreal possess the directable particles with splines feature since ten years ago…

Is there no Godot plugin for moving particles along a path? How can Godot developers build dynamic particle effects that run through bezier curves?

There may be a plugin but I’m not aware of it.

It’s not really complicated though. How badly do you need it?

There’s also an option to use plain mesh instances instead of particles. In that case you won’t need a shader. See my example here:

What’s the exact effect you’re after?

Thank you for the example of multiple objects flying through a path. That’s really useful.

I just need to move the particles inside complex pipe network. In Unity, we can’t simulate water flow in long and complicated pipes, because the water flow simulation will kill the frame rate. This is why I used particles to represent water flow in Unity and I thought Godot possessed a similar spline particle feature

Can you show a visual mockup of the effect, with the actual pipe paths?

It’s similar to these:

Particles through Pipe

Water through Pipes

So here’s a quick proof of concept.
Note that I hardcoded most of parameters for brevity. This can be expanded with features and quality-of-life stuff to meet your specific needs.

If you want to use other built in particle features, enable them in particle process material, convert the material to shader and append this shader code to the converted code.

image

Script:

@tool extends GPUParticles3D
var setting_up := false

@export var path: Path3D:
	set(value):
		path = value
		if path:
			path.curve.changed.connect(func(): if not setting_up: setup())

func setup() -> void:
	setting_up = true
	const TEXTURE_SIZE := 64
	path.curve.bake_interval = 0.01
	var image: Image = Image.create_empty(TEXTURE_SIZE, TEXTURE_SIZE, false, Image.FORMAT_RGBAF)
	var points = path.curve.get_baked_points()
	var pixel := Vector2i()
	for point in points:
		image.set_pixelv(pixel, Color(point.x, point.y, point.z))
		pixel.x += 1
		if pixel.x == TEXTURE_SIZE:
			pixel.x = 0
			pixel.y += 1	
	var texture := ImageTexture.create_from_image(image)
	process_material.set_shader_parameter("position_texture", texture)
	process_material.set_shader_parameter("position_count", points.size())
	setting_up = false	

Particle process shader:

shader_type particles;

uniform sampler2D position_texture;
uniform int position_count;
uniform float pipe_radius = 1.0;

vec3 rotate_axis_angle(vec3 p, vec3 axis, float angle){
	vec3 acrossp = cross(axis, p);
	return p + sin(angle) * acrossp + (1.0 - cos(angle)) * cross(axis, acrossp);
}

float random (vec2 st) {return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);}

void start() {
	if(RESTART_POSITION){
		CUSTOM.y = 0.0;
	}
}

void process() {
	CUSTOM.y += DELTA / LIFETIME; // store lifetime percent in CUSTOM.y
	float lifetime_percent = CUSTOM.y;
	
	float position_along_path = lifetime_percent * float(position_count - 1);
	int position_index = int(position_along_path);
	float position_fract = fract(position_along_path);
	
	ivec2 texture_size = textureSize(position_texture, 0);
	vec2 seed = vec2(float(INDEX), 0.0);
	
	vec3 p1 = texelFetch(position_texture, ivec2(position_index % texture_size.x, position_index / texture_size.x), 0).rgb;
	vec3 p2 = texelFetch(position_texture, ivec2((position_index + 1) % texture_size.x, (position_index + 1) / texture_size.x), 0).rgb;
	TRANSFORM[3].xyz = mix(p1, p2, position_fract) + normalize(cross(normalize(p2 - p1), vec3(0.0, 1.0, 0.0))) * pipe_radius * random(seed);
	TRANSFORM[3].xyz = p1 + rotate_axis_angle(TRANSFORM[3].xyz - p1, normalize(p2 - p1), TAU * random(seed + 10.0));
}

And here it is combined with built in particle process shader:

Thank you! It looks exactly like the Unity Surge plugin. Unfortunately, I can’t see anything when I play the engine, maybe because I’m a total noob in Godot. Here’s my project, made with Godot v4.7.2:

Project

Or could you please share your project?

Make sure to:

  • Assign the script to the particles node
  • Assign the path node to the path property of the particle node
  • Draw an actual curve for the path node.

Thanks again! It works