Morph sphere to plane geometry with complex shader material

Hi Three Commu ! :v:t5:

I try to achieve a scene in Webflow with Threejs. On one hand I made a sphere with a liquid distortion effect using fragment shader material.

<!-- Include Three.js -->
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>

<!-- Include OrbitControls -->
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

 <script>
    // Create a scene
    const scene = new THREE.Scene();

    // Create a camera
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 2;

   // Create a renderer with alpha enabled for transparency and add it to the DOM
     const renderer = new THREE.WebGLRenderer({ alpha: true });
     renderer.setSize(window.innerWidth, window.innerHeight);
     document.body.appendChild(renderer.domElement);

    // Add OrbitControls
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; // enables smooth controls
    controls.dampingFactor = 0.25;
    controls.screenSpacePanning = false;
    //controls.minDistance = 3; // minimum distance to zoom
    //controls.maxDistance = 3; // maximum distance to zoom


    // Limit vertical rotation to ±30 degrees
    const verticalLimit = Math.PI / 6; // 30 degrees in radians
    controls.minPolarAngle = Math.PI / 2 - verticalLimit; // 30 degrees from top
    controls.maxPolarAngle = Math.PI / 2 + verticalLimit; // 30 degrees from bottom
   
    // Create the icosahedron geometry
    const ico = new THREE.IcosahedronGeometry(1, 256);

    // Load a texture (image)
    const textureLoader = new THREE.TextureLoader();
    const texture = textureLoader.load('https://myportfolio-webflow.s3.eu-west-3.amazonaws.com/Scene/wall_stickers.jpg');

    // Custom shader material
    const customShaderMaterial = new THREE.ShaderMaterial({
      uniforms: {
        'texture1': { value: texture },
        'tSize': { value: new THREE.Vector2(256, 256) },
        'center': { value: new THREE.Vector2(0.5, 0.5) },
        'angle': { value: 0.0},
        'scale': { value: 1.0 },
        'time': { value: 0.0 },
        'progress': { value: 0.0 },
        'metalness': { value: 1.0 },  // Added metalness uniform
        'roughness': { value: 0.0 }  // Added roughness uniform
      },
      vertexShader: `
        varying vec2 vUv;
        varying vec3 vNormal;
        void main() {
          vUv = uv;
          vNormal = normal;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform vec2 center;
        uniform float angle;
        uniform float scale;
        uniform vec2 tSize;
        uniform sampler2D texture1;
        uniform float time;
        uniform float progress;
		uniform float metalness;
        uniform float roughness;
        varying vec2 vUv;
        varying vec3 vNormal;

        float pattern() {
          float s = sin(angle + time), c = cos(angle + time);
          vec2 tex = vUv * tSize - center;
          vec2 point = vec2(c * tex.x - s * tex.y, s * tex.x + c * tex.y) * scale;
          return (sin(point.x) * sin(point.y)) * 4.0;
        }

        void main() {
          vec2 newUV = vUv;
          vec3 p = vNormal;

          p += 0.1 * cos(scale * 3.0 * p.yzx + time + vec3(1.2, 3.4, 2.1));
          p += 0.1 * cos(scale * 3.7 * p.yzx + 1.4 * time + vec3(2.2, 3.4, 1.7));
          p += 0.1 * cos(scale * 5.0 * p.yzx + 2.6 * time + vec3(4.2, 1.4, 3.1));
          p += 0.3 * cos(scale * 7.0 * p.yzx + 3.6 * time + vec3(10.2, 3.4, 8.1));

          newUV.x = mix(vUv.x, length(p), progress);
          newUV.y = mix(vUv.y, 0.0, progress);

          vec4 color = texture2D(texture1, newUV);

			// Simulate metalness effect
            vec3 metalColor = mix(vec3(0.04), color.rgb, metalness);
            vec3 finalColor = mix(color.rgb, metalColor, metalness);

          gl_FragColor = color;
        }
      `
    });

    // Create a mesh with the geometry and custom shader material
    const mesh = new THREE.Mesh(ico, customShaderMaterial);
    scene.add(mesh); 

    // Animation loop
    function animate(time) {
      requestAnimationFrame(animate);

      // Update uniforms
      customShaderMaterial.uniforms['time'].value = time * 0.0002;  // Convert time to seconds
      customShaderMaterial.uniforms['progress'].value = 0.2;  // Oscillate progress between 0 and 1

      // Update controls
      controls.update();

      // Render scene
      renderer.render(scene, camera);
    }

    animate();

    // Handle window resize
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>

On the other hand I found a morphing ‘sphere to plane’ script in codepen.

<div id="threejs-container" style="width: 100%; height: 100vh;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
  var scene = new THREE.Scene();
  var camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 100);
  camera.position.set(0, 0, 10);
  var renderer = new THREE.WebGLRenderer();
  renderer.setClearColor(0x404040);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.getElementById('threejs-container').appendChild(renderer.domElement);

  var controls = new THREE.OrbitControls(camera, renderer.domElement);

  var planeGeom = new THREE.PlaneBufferGeometry(Math.PI * 5, Math.PI * 2.5, 36, 18);
  planeGeom.morphAttributes.position = [];

  var sphereFormation = [];
  var uvs = planeGeom.attributes.uv;
  var uv = new THREE.Vector2();
  var t = new THREE.Vector3();
  for (let i = 0; i < uvs.count; i++) {
    uv.fromBufferAttribute(uvs, i);
    t.setFromSphericalCoords(2.5, Math.PI * (1 - uv.y), Math.PI * (uv.x - 0.5) * 2);
    sphereFormation.push(t.x, t.y, t.z);
  }
  planeGeom.morphAttributes.position[0] = new THREE.Float32BufferAttribute(sphereFormation, 3);

  var planeMat = new THREE.MeshBasicMaterial({
    map: new THREE.TextureLoader().load('https://images.unsplash.com/photo-1596263576925-d90d63691097?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3422&q=80'),
    morphTargets: true,
    side: THREE.DoubleSide
  });
  var spherePlane = new THREE.Mesh(planeGeom, planeMat);
  scene.add(spherePlane);
  spherePlane.morphTargetInfluences[0] = 0;

  var clock = new THREE.Clock();

  renderer.setAnimationLoop(() => {
    spherePlane.morphTargetInfluences[0] = Math.sin(clock.getElapsedTime()) * 0.5 + 0.5;
    renderer.render(scene, camera);
  });

  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });
</script>

The goal is to combine the 2 scripts using fragment shaders to have the liquid sphere unbending to plane on click similar to that :

Thanks in advance for your help :grinning:

You’ll have to modify or insert something before these 2 lines ^ in the vertex shader.

something like…

vec3 planeNormal = vec3(0.0,0.0,1.0); //Plane normal is always the same in a plane
vec3 planeVertex = vec3(uv,0.0);//Plane vertex is luckily similar to the UV coordinate...

vec3 finalVertex = mix(position,planeVertex,progress); //Mix between sphere and plane by progress 0 to 1 range...
vec3 finalNormal = mix(planeNormal,normal,progress);
vNormal = normalize(finalNormal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(finalVertex, 1.0);
   
1 Like

possibly, needs this
normalize(mix(planeNormal,normal,progress))
? :thinking:

2 Likes

good catch.

Not an abnormal fan I take it? :smiley:

1 Like

Thanksss guys for your precious help, it’s exaclty what I was looking for :ok_hand:t5: :slight_smile:

I tweaked it a bit to add the animation I wanted with 2 different progress to be able to manage them independently.

<script>
    // Create a scene
    const scene = new THREE.Scene();

    // Create a camera
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 2;

    // Create a renderer with alpha enabled for transparency and add it to the DOM
    const renderer = new THREE.WebGLRenderer({ alpha: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // Add OrbitControls
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; // enables smooth controls
    controls.dampingFactor = 0.25;
    controls.screenSpacePanning = false;
    //controls.minDistance = 3; // minimum distance to zoom
    //controls.maxDistance = 3; // maximum distance to zoom

    // Limit vertical rotation to ±30 degrees
    const verticalLimit = Math.PI / 6; // 30 degrees in radians
    controls.minPolarAngle = Math.PI / 2 - verticalLimit; // 30 degrees from top
    controls.maxPolarAngle = Math.PI / 2 + verticalLimit; // 30 degrees from bottom

    // Create the icosahedron geometry
    const ico = new THREE.IcosahedronGeometry(1, 256);

    // Load a texture (image)
    const textureLoader = new THREE.TextureLoader();
    const texture = textureLoader.load('https://myportfolio-webflow.s3.eu-west-3.amazonaws.com/Scene/wall_stickers.jpg');

    // Custom shader material
    const customShaderMaterial = new THREE.ShaderMaterial({
        uniforms: {
            'texture1': { value: texture },
            'tSize': { value: new THREE.Vector2(256, 256) },
            'center': { value: new THREE.Vector2(0.5, 0.5) },
            'angle': { value: 0.0 },
            'scale': { value: 1.0 },
            'time': { value: 0.0 },
            'progress': { value: 0.2 },
            'progress2': { value: 0.3 },
            'metalness': { value: 1.0 },  // Added metalness uniform
            'roughness': { value: 0.0 }  // Added roughness uniform
        },
        vertexShader: `
            uniform float progress2;
            varying vec2 vUv;
            varying vec3 vNormal;
            void main() {
                vUv = uv;
                vec3 planeNormal = vec3(0.0,0.0,1.0); //Plane normal is always the same in a plane
                vec3 planeVertex = vec3(uv,0.0);//Plane vertex is luckily similar to the UV coordinate...

                vec3 finalVertex = mix(position,planeVertex,progress2); //Mix between sphere and plane by progress 0 to 1 range...
                vec3 finalNormal = normalize(mix(planeNormal,normal,progress2));
                vNormal = finalNormal;
                gl_Position = projectionMatrix * modelViewMatrix * vec4(finalVertex, 1.0);
            }
        `,
        fragmentShader: `
            uniform vec2 center;
            uniform float angle;
            uniform float scale;
            uniform vec2 tSize;
            uniform sampler2D texture1;
            uniform float time;
            uniform float progress;
            uniform float metalness;
            uniform float roughness;
            varying vec2 vUv;
            varying vec3 vNormal;

            float pattern() {
                float s = sin(angle + time), c = cos(angle + time);
                vec2 tex = vUv * tSize - center;
                vec2 point = vec2(c * tex.x - s * tex.y, s * tex.x + c * tex.y) * scale;
                return (sin(point.x) * sin(point.y)) * 4.0;
            }

            void main() {
                vec2 newUV = vUv;
                vec3 p = vNormal;

                p += 0.1 * cos(scale * 3.0 * p.yzx + time + vec3(1.2, 3.4, 2.1));
                p += 0.1 * cos(scale * 3.7 * p.yzx + 1.4 * time + vec3(2.2, 3.4, 1.7));
                p += 0.1 * cos(scale * 5.0 * p.yzx + 2.6 * time + vec3(4.2, 1.4, 3.1));
                p += 0.3 * cos(scale * 7.0 * p.yzx + 3.6 * time + vec3(10.2, 3.4, 8.1));

                newUV.x = mix(vUv.x, length(p), progress);
                newUV.y = mix(vUv.y, 0.0, progress);

                vec4 color = texture2D(texture1, newUV);

                // Simulate metalness effect
                vec3 metalColor = mix(vec3(0.04), color.rgb, metalness);
                vec3 finalColor = mix(color.rgb, metalColor, metalness);

                gl_FragColor = color;
            }
        `
    });

    // Create a mesh with the geometry and custom shader material
    const mesh = new THREE.Mesh(ico, customShaderMaterial);
    scene.add(mesh); 

    // Animation loop
    function animate(time) {
        requestAnimationFrame(animate);

        // Update uniforms
        customShaderMaterial.uniforms['time'].value = time * 0.0002;  // Convert time to seconds

        // Update controls
        controls.update();

        // Render scene
        renderer.render(scene, camera);
    }

    animate();

    // Handle window resize
    window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });

    // Handle click event
    document.addEventListener('click', () => {
        // Animate progress from 0.2 to 0 and progress2 from 0.3 to 1
        const duration = 1000; // Duration of the transition in milliseconds
        const startTime = performance.now();

        function updateProgress() {
            const currentTime = performance.now();
            const elapsedTime = currentTime - startTime;
            const t = Math.min(elapsedTime / duration, 1); // Normalize elapsed time to [0, 1]

            // Update the uniform values based on t
            customShaderMaterial.uniforms['progress'].value = 0.2 - 0.2 * t;
            customShaderMaterial.uniforms['progress2'].value = 0.3 + 0.7 * t;

            if (t < 1) {
                requestAnimationFrame(updateProgress); // Continue updating until t reaches 1
            }
        }

        requestAnimationFrame(updateProgress); // Start the animation
    });
</script>

One last thing to make it perfect, as you can see on the video there is a weird torsion while morphing I think because of the rotation center of the plane which is located at the bottom left corner instead of the center as the sphere .

Do you know how to change de vertex shader to make center of sphere fit center of plane while morphing ?

Can I also change the size of the plane in the vertex shader to fit the screen size at the end of the morphing ? :upside_down_face:

Try

vec3 planeVertex = vec3(uv-.5,0.0);
Or

vec3 planeVertex = vec3(uv.yx-.5,0.0);

Or
vec3 planeVertex = vec3((1.-uv)-.5,0.0);

Etc.

1 Like

Thanksss I’m gonna try this :hugs:

1 Like