Multiple different Particles in the same GPUParticle3D

Godot Version

4.2.2

Question

My Question is simple: is there a way to have different kind of particle mesh in the same system.

For example:
I can add up to 4 draw pass instances in the particle options but they are always rendered all together for every particle.

So is there a way to have some particles to use only pass 1 and some only pass 2?

What I try to archive is something like the Zelda TotK shrines in that multiple signs floating through the air.

Any suggestions?

For your example specifically you could use animation frames with a random offset and 0 playback speed. I use these parameters for a lot of particles with random images.

The material must be set to Particle Billboard, and set H Frames and V Frames to your sprite sheet’s dimensions. The particle process just needs to set offset min to 0, and offset max to 1 or lower if you want to use less frame, for example if you have empty frames in your sprite sheet.

This will use one mesh (usually a quad), one material, and one GPU particle node.

I’m not entirely sure if this is what I’m looking for since I wasn’t able to test it yet but from what I have read by now I think that is definitely the direction I needed to be pushed in.

Thank you very much I will test this and mark your answers as solution if it’s solving my issues.

Here’s an example image using this sprite sheet

th-2613382868

In addition here’s the .tscn file, which contains all the properties I talked about, with some extra for flavor. anim_offset_max and particles_anim_h_frames

[gd_scene load_steps=5 format=3 uid="uid://cxh2etmamltgf"]

[ext_resource type="Texture2D" uid="uid://if7d20ulwbw" path="res://Particles/Spritesheets/th-2613382868.jpg" id="1_4310u"]

[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_nffxd"]
emission_shape = 1
emission_sphere_radius = 5.46
gravity = Vector3(0, 1, 0)
hue_variation_min = -0.26
hue_variation_max = 0.53
anim_offset_max = 1.0
turbulence_enabled = true
turbulence_noise_speed = Vector3(10, 8, 20)

[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_mnovo"]
blend_mode = 1
shading_mode = 0
disable_ambient_light = true
disable_fog = true
vertex_color_use_as_albedo = true
albedo_texture = ExtResource("1_4310u")
billboard_mode = 3
particles_anim_h_frames = 15
particles_anim_v_frames = 8
particles_anim_loop = false

[sub_resource type="QuadMesh" id="QuadMesh_2j20m"]
material = SubResource("StandardMaterial3D_mnovo")

[node name="WordTest" type="Node3D"]

[node name="GPUParticles3D" type="GPUParticles3D" parent="."]
amount = 50
lifetime = 8.0
visibility_aabb = AABB(-5.71641, -5.822, -6.15948, 10.2931, 13.9946, 12.3691)
process_material = SubResource("ParticleProcessMaterial_nffxd")
draw_pass_1 = SubResource("QuadMesh_2j20m")

1 Like

Ok thanks @gertkeno again but in the end this was not exactly what I was looking for.
Two reasons:

  1. I explicitly don’t wanted Billboard to be used since the Particles should be rotating freely in space.
  2. I needed to write my custom process shader anyway because I wanted special behavior I found quite hard to achieve with only tweeking the standard process material

And in the end it turned out what I was doing before opening this questioning here was actually correct but I messed up in assigning the particles a random value to select one of the signs. (I missed a break statement in a for loop…)

However playing around with the billboard solution and converting the standard shader materials into “code shader” convinced me that I was on the correct way before. So here is how I do it (WARNING: Very ugly prototype code with lot of cleanup potential):

Generate a Random Number between 0.0 and 1.0 in the start function of the particle for each particle. Than I map this number between 0 to number of signs I want and save it in the CUSTOM.w (xyz is already used for other stuff in my shader) and that’s the point where I messed up earlier.

// select randomly which sign this should be (the effect will be in the Mesh Shader Material)
	int rand_number = int(rand_from_seed(seed4+uint(TIME))*100.0);
	int sign_count = 4;
	// group up the random dumber in to equally big ranges based on the sign_count...
	for(int i=1; i<= sign_count; i++){
		if(rand_number <= (100/sign_count)*i){
			CUSTOM.w = float(i-1);
			break;
		}
	}

Then in the Material Shader I read out the CUSTOM.w float value in the Vertex function and save it in a varying so it can be accessed later on in the fragment function.

void vertex() {
	sign_number = INSTANCE_CUSTOM.w;
}

In the fragment function I use a overly complicated way to map the UV to the position on that the sign is in a given texture and use that as area as an alpha value.

void fragment() {
	EMISSION = glow_color.rgb*glow_power;
	
	ALBEDO = glow_color.rgb;
	vec2 uv = UV/float(sign_count/2);
	// That makes no cśence because all particles are rendered the same... so what ever the last partikle has as the number will be rendered...
	//find offest in the UV based on random sign number
	float test = sign_number;
	float num_rows=sqrt(float(sign_count));
	float row_pos = mod(test, num_rows)/2.0;
	float col_pos = floor(test/num_rows)/2.0;
	// the col must be shifted...
	if(col_pos <= 0.0){
		col_pos = num_rows-1.0;
	}
	else{
		col_pos = col_pos-1.0;
	}
	uv = uv+vec2(col_pos, row_pos);
	ALPHA = texture(signes_map, uv).x;
}

Again currently very ugly but at least it is working now. Thank’s again @gertkeno your suggestion at east showed me that what I wanted to archive must be possible some way :slight_smile: Maybe some day one unlucky person being as stupid as I was finds this helpful and will save some time :smiley:

This is the result for now:

Full Code:
Custom Process Material Shader Code:

shader_type particles;

uniform float break_speed : hint_range(0.0, 10.0, 0.1);
uniform vec3 expanse;
uniform float scale_facotr : hint_range(0.0, 10.0, 0.1);

uniform float rotation_speed : hint_range(0.0, 100.0, 0.1);

float rand_from_seed(in uint seed) {
  int k;
  int s = int(seed);
  if (s == 0)
    s = 305420679;
  k = s / 127773;
  s = 16807 * (s - k * 127773) - 2836 * k;
  if (s < 0)
    s += 2147483647;
  seed = uint(s);
  return float(seed % uint(65536)) / 65535.0;
}

uint hash(uint x) {
  x = ((x >> uint(16)) ^ x) * uint(73244475);
  x = ((x >> uint(16)) ^ x) * uint(73244475);
  x = (x >> uint(16)) ^ x;
  return x;
}

vec3 rotate_vector_x(vec3 v, float angle) {
	return vec3(
		v.x, 
		cos(angle)*v.y-sin(angle)*v.z,
		sin(angle)*v.y+cos(angle)*v.z);
}

vec3 rotate_vector_y(vec3 v, float angle) {
	return vec3(
		cos(angle)*v.x+sin(angle)*v.z, 
		v.y,
		sin(angle)*v.x*-1.0+cos(angle)*v.z);
}

vec3 rotate_vector_z(vec3 v, float angle) {
	return vec3(
		cos(angle)*v.x-sin(angle)*v.y, 
		sin(angle)*v.x+cos(angle)*v.y,
		v.z);
}

// returns a mat3 representing a new basis after rotating to the given direction
mat3 rotate_basis_to(mat4 transform, vec3 direction, vec3 up) {
	vec3 r = cross(direction, up);
	vec3 t = cross(r, direction);
	return mat3(normalize(vec3(r.x, r.y, r.z)), normalize(vec3(t.x, t.y, t.z)), normalize(vec3(direction.x, direction.y, direction.z)));
}

void start() {
	// generate a seed for each Velocity direction
	uint seed1 = hash(NUMBER + uint(1) + RANDOM_SEED);
	uint seed2 = hash(NUMBER + uint(27) + RANDOM_SEED);
	uint seed3 = hash(NUMBER + uint(111) + RANDOM_SEED);
	uint seed4 = hash(NUMBER + uint(345) + RANDOM_SEED);
	
	// generate a random position around the mid point and save in the custom...
	CUSTOM.x = rand_from_seed(seed1)-0.5;
	CUSTOM.y = rand_from_seed(seed2)-0.5;
	CUSTOM.z = rand_from_seed(seed3)-0.5;
	
	// select randomly which sign this should be (the effect will be in the Mesh Shader Material)
	int rand_number = int(rand_from_seed(seed4+uint(TIME))*100.0);
	int sign_count = 4;
	// group up the random dumber in to equally big ranges based on the sign_count...
	for(int i=1; i<= sign_count; i++){
		if(rand_number <= (100/sign_count)*i){
			CUSTOM.w = float(i-1);
			break;
		}
	}
	
	
	// set position to center
	TRANSFORM[3].xyz = vec3(0.0);
	
	// reset rotation
	mat3 basis = rotate_basis_to(TRANSFORM, CUSTOM.xyz, vec3(0.0, 1.0, 0.0));
	
}

void process() {
	// calculate the target_pos from random start pos and expanse and scale factor
	vec3 target_pos = vec3(CUSTOM.x*expanse.x, CUSTOM.y*expanse.y, CUSTOM.z*expanse.z)*scale_facotr;
	// move particle to target pos if to far away 
	// and ensure that position is never exactly hit by ceeping velocity at a min speed.
	if(distance(TRANSFORM[3].xyz, target_pos) >= 0.05){
		VELOCITY = target_pos-TRANSFORM[3].xyz;
	}
	
	//rotate the particle around x
	//TRANSFORM[1].xyz = rotate_x(TRANSFORM[1].xyz, 5.0*DELTA);
	//TRANSFORM[2].xyz = rotate_x(TRANSFORM[2].xyz, 5.0*DELTA);
	
	//TRANSFORM[0].xyz = rotate_y(TRANSFORM[0].xyz, 2.0*DELTA);
	//TRANSFORM[2].xyz = rotate_y(TRANSFORM[2].xyz, 2.0*DELTA);
	
	uint seed1 = hash(NUMBER + uint(5) + RANDOM_SEED);
	uint seed2 = hash(NUMBER + uint(45) + RANDOM_SEED);
	uint seed3 = hash(NUMBER + uint(452) + RANDOM_SEED);
	uint seed4 = hash(NUMBER + uint(153) + RANDOM_SEED);
	uint seed5 = hash(NUMBER + uint(153) + RANDOM_SEED);
	
	float rot_speed = rotation_speed*rand_from_seed(seed5);
	
	//rotate the particles around y
	vec3 rot_point = vec3(rand_from_seed(seed1), rand_from_seed(seed2), rand_from_seed(seed3));
	//rot_point = rotate_vector_y(rot_point, rand_from_seed(RANDOM_SEED));
	//rot_point = rotate_vector_x(rot_point, rand_from_seed(RANDOM_SEED));
	rot_point = rotate_vector_z(rot_point, (1.5*rand_from_seed(seed4)+TIME*clamp(rand_from_seed(RANDOM_SEED), 0.0, 1.0))*rot_speed);
	
	mat3 basis = rotate_basis_to(TRANSFORM, rot_point, vec3(0.0, 1.0, 0.0));
	TRANSFORM[0].xyz = basis[0];
	TRANSFORM[1].xyz = basis[1];
	TRANSFORM[2].xyz = basis[2];
	
	normalize(TRANSFORM[0].xyz);
	normalize(TRANSFORM[1].xyz);
	normalize(TRANSFORM[2].xyz);
}

Particle Material Custom Shader:

shader_type spatial;
render_mode cull_disabled;

uniform vec4 glow_color : source_color;
uniform float glow_power : hint_range(0.0 , 100.0);

uniform sampler2D signes_map : source_color, filter_linear_mipmap, repeat_enable;
uniform int sign_size;
uniform int sign_count;

varying float sign_number;

void vertex() {
	sign_number = INSTANCE_CUSTOM.w;
	//VERTEX.x = VERTEX.x+sign_number;
}

void fragment() {
	EMISSION = glow_color.rgb*glow_power;
	
	ALBEDO = glow_color.rgb;
	vec2 uv = UV/float(sign_count/2);
	
	// That makes no cśence because all particles are rendered the same... so what ever the last partikle has as the number will be rendered...
	//find offest in the UV based on random sign number
	float test = sign_number;
	float num_rows=sqrt(float(sign_count));
	float row_pos = mod(test, num_rows)/2.0;
	float col_pos = floor(test/num_rows)/2.0;
	// the col must be shifted...
	if(col_pos <= 0.0){
		col_pos = num_rows-1.0;
	}
	else{
		col_pos = col_pos-1.0;
	}
	uv = uv+vec2(col_pos, row_pos);
	ALPHA = texture(signes_map, uv).x;
}

//void light() {
	// Called for every pixel for every light affecting the material.
	// Uncomment to replace the default light processing function with this one.
//}

1 Like