ShaderMaterial.depthWrite with THREE.Points and transparency

Hi everyone.

I want to create a cloud of points placed on the surface of a geometry. The desired result should look like this but with circle-shaped particles.

This is the code I am using to instantiate the Points object:

const baseGeometry = new THREE.SphereGeometry(...)
const particlesMaterial = new THREE.ShaderMaterial({
    vertexShader: particleVertexShader,
    fragmentShader: particleFragmentShader,

const pointsPositions = baseGeometry.getAttribute('position').clone()
const particlesGeometry = new THREE.BufferGeometry()
particlesGeometry.setAttribute('position', pointsPositions)

const particles = new THREE.Points(particlesGeometry, particlesMaterial);

In the vertex shader I just increase the gl_PointSize and account for size attenuation using the formula gl_PointSize *= (1.0 / - viewPosition.z).

Now the most relevant part: the fragment shader. I tried different solutions:

  1. get the circle shape using this fragment shader:
float strength = distance(gl_PointCoord, vec2(0.5));
strength = step(0.4, strength);
strength = 1.0 - strength;

vec3 color = vec3(0.5137, 0.8039, 1.0);

gl_FragColor = vec4(color * strength, 1.0);

But with this I can still see the black square outline, as expected.
To solve this, I thought adding blending: THREE.AdditiveBlending to the ShaderMaterial initialization parameters should have solved the issue, but it turns out background particles can be seen through foreground ones as if the latter were semi-transparent. After a second though this made sense given pixel color values were added each other so overlapping particles would result in white pixels.

  1. use the alpha channel to make parts of a particles’ pixels transparent: the fragment code was the same as before but instead of multiplying the strength variable with the color, I used it as the value for the alpha channel of gl_FragColor.
    I added transparent: true to the ShaderMaterial initialization parameters as well to make three.js respect the transparency values (alphaTest: 0.001 didn’t seem to have any effect).

    As you can see in the image, some particles still have the dark square outline, even though it should be transparent. I noticed anyway that only SOME particles have this problem, and that the outline isn’t black as before but it has the background color instead.
    What solved this issue was adding depthWrite: false to the ShaderMaterial initialization parameters.

So, is this the correct way to do this kind of thing? Does it have any drawbacks?
And why does this work? I read several articles and questions about the order three.js uses to render things and from what I understand disabling depthWrite prevents rendering the material to affect the depth buffer [1]. Does this means that rendering materials on already “placed” objects would normally cause their depth buffer to be rewritten, which in turns potentially lead to background objects being rendered on top of foreground ones?

Sorry for the quite long post and thanks in advance to anyone who will help me!

1 Like

You can discard pixels that are further than 0.4, changing this

to this

if (distance(gl_PointCoord, vec2(0.5)) > 0.4) discard;
vec3 color = vec3(0.5137, 0.8039, 1.0);
gl_FragColor = vec4(color, 1.0);

Oh I didn’t though about discard! Is this faster performance-wise than accounting for transparency (now that I can disable it entirely for that material)?