A problem with transparency for point geomerty

I have a sphere set to drawn with point primitives and a material with custom shaders.

If I pass vertex colors that are not transparent, I can see, when rotating the sphere, that all points pass the depth test correctly.

Now I apply a green color to the scene background, and supply vertex colors that have only red and blue components and transparency.

The sphere looks like this ( I added a white rectangle around the part in question):

test_alpha

I can see that all points blend with the scene background because they all have some green color in them.
But some of them, when they overlap with each other, do not blend - the rectangle on top keeps it color above the background and the bottom rectangle the same.

Some of them do blend though.

Why does it happen like this and how do I fix it?

My other question is that I set transparency to 0.1, which is pretty low, but the amount of green points acquire from the background is about a half. Is this blending not linear?

Thank you!

The code:

<body>

    <script type='x-shader/x-vertex' id='cities_vert'>

        attribute vec4 clr;
        out vec4 rgba;

        void main() {

        gl_PointSize = 20.0;

        vec4 camSpace = modelViewMatrix * vec4( position, 1.0 );
        gl_Position = projectionMatrix * camSpace;

        rgba = clr;

        }
    </script>

    <script type='x-shader/x-fragment' id='cities_frag'>

        in vec4 rgba;

        void main() {

        gl_FragColor = rgba;

        }
    </script>

    <div id="container"></div>

    <script type="module">

        import * as THREE from './js/three.module.js';

        import { OrbitControls } from './js/OrbitControls.js';

        import Stats from './js/stats.module.js';

        // perf utils

        let stats;

        // math utils

        // https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
        function HSVtoRGB(h, s, v) { // [0,360], [0,1], [0,1] => [0,1], [0,1], [0,1]
            function chan(n) {
                var k = (n + h / 60) % 6;
                return v - v * s * Math.max(0, Math.min(k, 4 - k, 1));
            }
            return [chan(5), chan(3), chan(1)];
        }

        // viewport utils

        let viewportMargins = [20, 50, 50, 50];

        function getViewport() {
            var vp = [
                window.innerWidth - viewportMargins[1] - viewportMargins[3],
                window.innerHeight - viewportMargins[0] - viewportMargins[2]];
            return vp;
        }

        // THREE globals

        let camera, controls, scene, renderer;

        // world globals

        let globe;

        init();
        animate();

        function init() {

            // scene

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x003300);

            var vp = getViewport();

            // camera

            camera = new THREE.PerspectiveCamera(60, vp[0] / vp[1], 1, 1000);
            camera.position.set(0, 0, 300);

            // renderer

            renderer = new THREE.WebGLRenderer({ antialias: true });
        
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(vp[0], vp[1], true, viewportMargins);
            document.body.appendChild(renderer.domElement);

            // stats

            stats = new Stats();
            container.appendChild(stats.dom);

            // controls

            controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
            controls.dampingFactor = 0.05;
            controls.enablePan = false;
            controls.minDistance = 10;
            controls.maxDistance = 1000;
            controls.minPolarAngle = 0;
            controls.maxPolarAngle = Math.PI;

            // world

            // globe

            var cities_geom = new THREE.SphereBufferGeometry(102, 20, 20);

            var cities_cnt = cities_geom.attributes.position.count;

            var cities_clr = new Float32Array(cities_cnt * 4);

            
            for (var i = 0; i < cities_cnt * 4; i += 4) {
                var h = 360 * Math.random();
                [cities_clr[i], cities_clr[i + 1], cities_clr[i + 2]] = [Math.random(), 0.0, Math.random()]; // HSVtoRGB(h, 1.0, 1.0);
                cities_clr[i + 3] = 0.1;
            }

            cities_geom.setAttribute('clr', new THREE.Float32BufferAttribute(cities_clr, 4));

            var cities_shaders = new THREE.ShaderMaterial({
                vertexShader: document.getElementById('cities_vert').textContent,
                fragmentShader: document.getElementById('cities_frag').textContent,
                transparent: true
            });

            var cities = new THREE.Points(cities_geom, cities_shaders);

            globe = new THREE.Group();
            globe.add(cities);
            scene.add(globe);

            const ambientLight = new THREE.AmbientLight(0xffffff, 1);
            scene.add(ambientLight);

            // window

            window.addEventListener('resize', onWindowResize, false);

        }

        function onWindowResize() {

            var vp = getViewport();
            camera.aspect = vp[0] / vp[1];
            camera.updateProjectionMatrix();

            renderer.setSize(vp[0], vp[1], true, viewportMargins);

        }

        function animate() {

            requestAnimationFrame(animate);

            controls.update();

            render();

            stats.update();

        }

        function render() {

            renderer.render(scene, camera);

        }

    </script>
</body>

Points aren’t sorted at all, you need to do this yourself or use additive blending where order doesn’t matter.

However you should disable depthWrite when working with transparency. Without being back to front ordered depth test cannot work properly and some more distant points will be in front of closer ones.

This is not about depth test, as I said depth test works properly here, so points are in fact sorted. Try to rotate the sphere and see for yourself, their z-order is properly recalculated.

It’s about points blending with the background but not with each other.
In any depth order (and their order is correct) and all of them being transparent, they should create blend color when they overlap, and they don’t.

This is not about depth test, as I said depth test works properly here, so points are in fact sorted. Try to rotate the sphere and see for yourself, their z-order is properly recalculated.

If you enable depth write then some pixels will not be rendered and therefore not blend because they will be discarded due to another point having rendered in front of it first. When rendering transparent objects you typically disable writing to the depth buffer for this reason. However if you disable writing to the depth buffer you’ll find that the points aren’t necessarily blended in the order you expect again because points are not sorted before they are rendered. Perhaps it will be good enough for your usecase, though.

Thanks! That gives me some insight to study it further, all this stuff probably takes years to know in detail…

/cc

That’s what i meant, depth test for transparency cannot work properly when depth write is writing to depth not in back to front order.

Depth write cannot be used with transpareny in a reasonable way since it will cause anything drawn later behind getting cropped.

Sorting however would be still necessary if you wan’t correct blending order and far points not suddenly being visible in front of close points.