I am building a generative terrain and environment and I have a few particle systems that I'm using to place foliage, largely inspired by devmar's tutorial here. The GPUParticles3D have worked well for grass, but I don't currently have any collisions working, making it more difficult to get working for trees or debris. From what I've seen, collisions are now possible for GPU particles in Godot 4, but I can't seem to find out how to enable them. Does anyone have any resources or advice that would put me in the right direction? The code that I am using for the foliage is based on the shader that devmar made available:

[gd_resource type="Shader" format=3 uid="uid://cmyeclhrvhn2o"]

[resource]
code = "shader_type particles;

uniform float instance_rows;
uniform float instance_rot_randomize = 2.5;
uniform float instance_spacing = 1.0;
uniform float instance_scale_x = 1.0;
uniform	float instance_scale_y = 1.0;
uniform float instance_scale_z = 1.0;
uniform float instance_scale_randomize = 1.0;
uniform float instance_pos_randomize : hint_range(0.0, 50.0) =  2.0;
uniform float instance_scale_min = 0.2;
uniform float instance_scale_max = 1.2;
uniform float _coverage_altitude = 10.0;
uniform float _coverage_range = 100.0;
uniform float _slope_coverage = 0.5;
uniform float clumping_strength : hint_range(0.0, 100.0) = 20.0;
uniform mat3 terrain_normal_basis;

uniform sampler2D map_heightmap;
uniform sampler2D map_normalmap;
uniform bool instance_orient_to_normal = false;
uniform float instance_orientation_influence : hint_range(0.0, 10.0) = 1.0;
uniform float __terrain_amplitude;
uniform vec2 map_heightmap_size = vec2(2000.0, 2000.0);
uniform vec2 map_normalmap_size = vec2(2000.0, 2000.0);
uniform sampler2D map_clumpmap;

float get_height(vec2 pos) {
	pos -= 0.5 * map_heightmap_size;
	pos /= map_heightmap_size;	
	return 2.0*__terrain_amplitude * texture(map_heightmap, pos).r;
}

vec3 unpack_normalmap(vec3 rgb) {
vec3 n = rgb.xzy * 2.0 - vec3(1.0);
n.z *= -1.0;
return n;
}	

vec3 get_normal(vec2 pos) {
	pos -= 0.5 * map_normalmap_size;
	pos /= map_normalmap_size;
	return terrain_normal_basis * unpack_normalmap(texture(map_normalmap, pos).rgb);
}

float get_clumps(vec2 pos) {
	pos -= 0.5 * map_heightmap_size;
	pos /= map_heightmap_size;	
	return 100.0 * texture(map_clumpmap, pos).r;
}

void start() {	
	// get position
	vec3 pos = vec3(0.0, 0.0, 0.0);
	pos.z = float(INDEX);
	pos.x = mod(pos.z, instance_rows);
	pos.z = (pos.z - pos.x) / instance_rows;
	
	// center the emitter
	pos.x -= instance_rows * 0.5;
	pos.z -= instance_rows * 0.5;
	
	// apply instance spacing
	pos *= instance_spacing;
	
	// center on position of emitter
	pos.x += EMISSION_TRANSFORM[3][0] - mod(EMISSION_TRANSFORM[3][0], instance_spacing);
	pos.z += EMISSION_TRANSFORM[3][2] - mod(EMISSION_TRANSFORM[3][2], instance_spacing);
	
	// add noise 
	vec3 noise = texture(map_heightmap, pos.xz * 0.1).rgb;
	pos.x += noise.z * instance_spacing * instance_pos_randomize;
	pos.z -= noise.x * instance_spacing * instance_pos_randomize;

	// apply height
	pos.y = get_height(pos.xz);	
	float y2 = get_height(pos.xz + vec2(1.0, 0.0));
	float y3 = get_height(pos.xz + vec2(0.0, 1.0));
	float y4 = get_clumps(pos.xz);
	
	// hide parts from steep areas
	if (abs(y2 - pos.y) > _slope_coverage) {
		pos.y = -10000.0;
	} else if (abs(y3 - pos.y) > _slope_coverage) {
		pos.y = -10000.0;
	}	
	if (abs(pos.y) < _coverage_altitude*5.0-_coverage_range) {
		pos.y = -10000.0;
	} 	
	if (abs(pos.y) > _coverage_altitude*5.0+_coverage_range) {
		pos.y = -10000.0;
	} 	
	
	// clumping
	vec3 clumps = texture(map_clumpmap, pos.zx).rgb;
	if (abs(y4) < clumping_strength) {
		pos.y = -10000.0;
	}
	
	// rotate transform
	vec3 tex_scale = vec3(1.0) ;

	vec3 base_scale = vec3(mix(instance_scale_min, instance_scale_max, noise.x)*instance_scale_x,mix(instance_scale_min, instance_scale_max, noise.y)*instance_scale_y,mix(instance_scale_min, instance_scale_max, noise.z)*instance_scale_z );
	base_scale *= base_scale*instance_scale_randomize;
	base_scale = sign(base_scale) * max(abs(base_scale), 0.1);
	
	// update transform
	pos.x += cos( noise.x * instance_pos_randomize) * base_scale.x  ;
	pos.z += sin( noise.z * instance_pos_randomize) * base_scale.z ; 
	
	// re-transform to orient to normal if the setting is on	
	if (instance_orient_to_normal == true){
	TRANSFORM[1].xyz = get_normal(pos.xz) * instance_orientation_influence;
	TRANSFORM[2].xyz = sin(get_normal(pos.zx) * faceforward(vec3(1.0),TRANSFORM[1].xyz, vec3(0.0, 0.0, 1.0))) * instance_orientation_influence ;
	TRANSFORM[0].xyz = sin(get_normal(pos.zx) * faceforward(vec3(1.0),TRANSFORM[1].xyz, vec3(1.0, 0.0, 0.0))) * instance_orientation_influence ;
	}
	else {
		TRANSFORM[0].xyz = vec3(0.0);
		TRANSFORM[1].xyz = vec3(0.0);
		TRANSFORM[2].xyz = vec3(0.0);}
	
	// finalize transform positioning and scaling including randomizations	
	TRANSFORM[3][0] = pos.x;
	TRANSFORM[3][1] = pos.y;
	TRANSFORM[3][2] = pos.z;
	
	TRANSFORM[0][0] = cos(noise.x * instance_rot_randomize) * base_scale.x;
	TRANSFORM[0][2] = -sin(noise.x * instance_rot_randomize) * base_scale.x;
	TRANSFORM[2][0] = sin(noise.z * instance_rot_randomize) * base_scale.z;
	TRANSFORM[2][2] = cos(noise.z * instance_rot_randomize) * base_scale.z;
	
	TRANSFORM[0][0]  = base_scale.x;
	TRANSFORM[1][1] =  base_scale.y;
	TRANSFORM[2][2]  = base_scale.z;

}	"
  • aceslowman
    I don't think that's possible, since the particles geneated is restricted to primitive mesh type, and those primitive mesh type can't attach collision box like vanilla MeshInstance3D.

Did you try changing the collison size value in subemitter of GPUParticle inspector tab?

I am using a billboard stuff, so I didn't test if that option works or not, or how it works with different types of mesh generated.

    MagickPanda
    Like the collision_base_size parameter? Unfortunately adjusting that value doesn't seem to have an effect. I'm not using a sub emitter for my particle system either, here is my setup:

    hmmm I will do a test when I have access to to computer later.

    Video demonstrating GPUParticle3D emitted sphere colliding with box collider: (too lazy to create test scene, just used my game scene..)

    Don't forget to generate AABB after adjusting parameters of Particle3D

    Collison size setting for generated sphere:

    Last but not least, add Particle collision node to object or terrain:

    Thank you for this @MagickPanda ! I still don't have collisions working, but I've noticed something that might be relevant. The process material, when using ParticleProcessMaterial, offers a section for enabling collisions, and after converting to a new shader material, this option is no longer available. Could this be the issue? Is it possible to enable collisions within a particle shader?

    I don't think the collision setting is inside shader inspector, I tried with sphere primitive and standard shader, and they work flawlessly, only the aabb and collision size options affect tbe result it seems.

    @MagickPanda I think I understand now. I set up a test scene and managed to get the particles to collide with shapes just as you said, but this made me realize that my particles arent set up in this way. What I am looking for is for the particles to behave similar to static bodies so that the CharacterBody3D can collide with it via move_and_slide().

      aceslowman
      I don't think that's possible, since the particles geneated is restricted to primitive mesh type, and those primitive mesh type can't attach collision box like vanilla MeshInstance3D.

        MagickPanda Great, thank you! That at least confirms to me what I can and can't manage with the GPUParticles, sounds like, at least for a generative terrain, I'll be able to do fine with using it for foliage and non-colliding things like that, but for things like trees and debris I'll stick with a MultiMesh.