Morph Image Particle| Creating a Particle-Based Face Transition Effect

Hi everyone,
I’m new to working with particles in Three.js and would appreciate some guidance. I’m trying to create a particle effect with the following sequence:

  1. Particles initially form the front view of a person’s face.
  2. They disperse and move.
  3. Then, from a side view, they reassemble into the person’s face again.
    The particles are spherical and have a neon-like glow.

Since I’m a beginner with particle systems, I’d love to hear suggestions on the best approach. Should I use shaders, physics-based animations, or any specific ones?

Here’s a reference video:

Any advice or guidance would be greatly appreciated. Thanks in advance!

I have done one part
But I have a problem converting the first image to the second image
Can you check and help me with How To morph one image into another?

my images:


My code here:

fragment.glsl:

uniform sampler2D uPictureTexture;
uniform sampler2D uGlowTexture; 
uniform float uProgress;
uniform sampler2D uSecondPictureTexture; 
uniform float uSecondProgress; 
varying vec3 vColor;
varying vec2 vUv;
void main() {
    vec2 uv = gl_PointCoord;
    float distanceToCenter = length(uv - vec2(0.5));
    float circleAlpha = smoothstep(0.5, 0.49, distanceToCenter); 
    vec4 circleColor = vec4(vColor, circleAlpha);
    vec4 glowColor = texture2D(uGlowTexture, uv);
    vec4 imageColor = texture2D(uPictureTexture, vUv);
    vec4 imageColor2 = texture2D(uSecondPictureTexture, vUv);
    if (imageColor.r < 0.1) {
        discard;
    } 
    vec4 finalColor = mix(circleColor, glowColor, uProgress);
    finalColor = mix(finalColor, imageColor2, uSecondProgress);
    finalColor.a *= circleAlpha;
    gl_FragColor = finalColor;
    #include <tonemapping_fragment>
    #include <colorspace_fragment>
}

vertex.glsl:

uniform float uProgress;
uniform float uSecondProgress; 
uniform sampler2D uPictureTexture;
uniform sampler2D uSecondPictureTexture; 
uniform sampler2D uDisplacementTexture;
uniform vec2 uResolution;
attribute float aIntensity;
attribute float aAngle;
attribute vec2 aUv;
varying vec3 vColor;
varying vec2 vUv; 
void main() {
    float radius = 5.0;
    float randomRadius = sqrt(aIntensity) * radius;
    vec3 initialPosition = vec3(
        randomRadius * cos(aAngle),
        randomRadius * sin(aAngle),
        0.0
    );
    vec3 targetPosition = vec3(
        (aUv.x - 0.5) * 10.0,
        (aUv.y - 0.5) * 10.0,
        0.0
    );
    vec3 targetPosition2 = vec3(
        (aUv.x - 0.5) * 10.0 ,
        (aUv.y - 0.5) * 10.0,
        0.0
    );
    vec3 finalPosition = mix(initialPosition, targetPosition, uProgress);
 if (uProgress >= 0.99) {
        finalPosition = mix(finalPosition, targetPosition2, uSecondProgress);
    }
    float displacementIntensity = texture2D(uDisplacementTexture, aUv).r;
    displacementIntensity = smoothstep(0.1, 0.3, displacementIntensity);
    finalPosition += vec3(cos(aAngle), sin(aAngle), 0.0) * displacementIntensity * aIntensity * 3.0;
    vec4 modelPosition = modelMatrix * vec4(finalPosition, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
    float pictureIntensity = texture2D(uPictureTexture, aUv).r;
    gl_PointSize = 0.15 * pictureIntensity * uResolution.y;
     gl_PointSize *= (1.0 / -viewPosition.z);
    vColor = vec3(pow(pictureIntensity, 2.0));
    vUv = aUv;
}

Js file:

const mypictureTexture = textureLoader.load('./static/02-2.jpg');
const mypictureTexture2 = textureLoader.load('./static/01-2.jpg');
const glowTexture = textureLoader.load('./static/glow.png'); 
// Material
const particlesMaterial = new THREE.ShaderMaterial({
    vertexShader: particlesVertexShader,
    fragmentShader: particlesFragmentShader,
    uniforms: {
        uProgress: { value: 0 }, 
        uSecondProgress: { value: 0 }, 
        uPictureTexture: new THREE.Uniform(mypictureTexture),
        uSecondPictureTexture: new THREE.Uniform(mypictureTexture2), 
        uGlowTexture: new THREE.Uniform(glowTexture), 
        uDisplacementTexture: new THREE.Uniform(displacement.texture),
        uResolution: new THREE.Uniform(new THREE.Vector2(sizes.width * sizes.pixelRatio, sizes.height * sizes.pixelRatio))
    },
    blending: THREE.AdditiveBlending
});

// Particles
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);

let progress = 0;
const animateProgress = () => {
    progress += 0.005; 
    particlesMaterial.uniforms.uProgress.value = progress;
    if (progress < 1) {
        requestAnimationFrame(animateProgress);
    }
    else {
        setTimeout(animateSecondProgress, 3000); 
    }
};
setTimeout(animateProgress, 2000); 
let secondProgress = 0;
const animateSecondProgress = () => {
    secondProgress += 0.005;
    particlesMaterial.uniforms.uSecondProgress.value = secondProgress;
    if (secondProgress < 1) {
        requestAnimationFrame(animateSecondProgress);
    }
};