Metaball effect with png images

Hi,

I am trying to create a meatball-style effect with PNG images. The images are transparent with black shapes. My current idea is to have two scenes one where I place the textures in and then render it with a ShaderMaterial onto the first scene.

But I don’t know how to calculate the meatball effect in the fragment shader. I am unsure if this is the best solution to archive this effect. I would like them both to be always the same size.

An example of the meatball effect style I am after:
1e90c9df23aa46b9ff343f0a5c978300

I am also experiencing some weird resizing issues where the scene2 resizes faster than the scene1. It is noticeable when you open the developer tools and the scene height narrows.

Here is my ThreeJs code and a CodeSandbox demo.

const size = { width: window.innerWidth, height: window.innerHeight };

// Renderer
const renderer = new THREE.WebGLRenderer({ preserveDrawingBuffer: true, transparent: true, stencil: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(size.width, size.height);
appElement.appendChild(renderer.domElement);
renderer.setClearColor(0x000000, 0);

// Camera
const camera = new THREE.PerspectiveCamera(70, size.width / size.height, 0.1, 1000);
const zoom = 600;
camera.position.z = zoom;
camera.aspect = size.width / size.height;
camera.fov = 2 * Math.atan(size.height / 2 / zoom) * (180 / Math.PI);

// Scene's
const scene = new THREE.Scene();
const scene2 = new THREE.Scene();
scene.background = new THREE.Color("#FFCA27");
scene2.background = new THREE.Color("#FF0000");

// Render Target
const settings = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat, stencilBuffer: false };
const renderTarget = new THREE.WebGLRenderTarget(size.width, size.height, settings);

// Shader Material
const shaderMaterial = new THREE.ShaderMaterial({
    vertexShader: `
        varying vec2 vUv;       
                
        void main(){
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            vUv = uv;
        }    
    `,
    fragmentShader: `
        uniform sampler2D uTexture;
        uniform float uTime;
        varying vec2 vUv;

        void main( void ) {
            vec4 texture = texture2D( uTexture, vUv ).rgba;
            float threshold = 0.1;

            gl_FragColor = vec4( texture);
        }
    `,
    uniforms: {
        uTexture: { value: renderTarget.texture },
        uTime: { value: 0 },
        uResolution: { value: new THREE.Vector2(size.width, size.height) },
    },
    transparent: true,
});

function createPlane(texture, scale = 1) {
    const width = texture.image.width * scale;
    const height = texture.image.height * scale;

    const geometry = new THREE.PlaneGeometry(width, height);
    const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true });
    const plane = new THREE.Mesh(geometry, material);
    return plane;
}

loadTextures(["./assets/texture1.png", "./assets/texture2.png"]).then((textures) => {
    const [texture1, texture2] = textures;

    const plane_1 = createPlane(texture1, 0.1);
    const plane_2 = createPlane(texture2, 0.1);

    plane_2.position.x = 140;

    scene2.add(plane_1);
    scene2.add(plane_2);
});

// Full-screen quad geometry
const fullScreenQuad = new THREE.PlaneGeometry(size.width, size.height);
const postProcessMesh = new THREE.Mesh(fullScreenQuad, shaderMaterial);
scene.add(postProcessMesh);

function render() {
    renderer.setRenderTarget(renderTarget);
    renderer.render(scene2, camera);
    renderer.setRenderTarget(null);

    renderer.render(scene, camera);

    window.requestAnimationFrame(render);
}
render();

Your post just inspired me to try the same concept that I demonstrated in your previous thread about metaballs (blur+contrast postprocessing effects), but now with shaders. The result looks similar to what you want, but it is not using your approach. Nevertheless, here it a short video:

4 Likes

@PavelBoytchev At one point I considered using a similar method, but it significantly alters the original texture shape, such as closing gaps. Although this approach could be quite useful, I need to maintain the integrity of the original texture shapes as much as possible.

Your solution looks really cool! would love to know how you did it!

One idea I have would be to calculate the distance from one texture to the other and if they are in a certain threshold fill the distance with black pixels. But I have no idea how to do this.