three.jsglslshader

Push particles away from mouseposition in glsl and three js


I have the following setup for my THREE.Points Object:

        this.particleGeometry = new THREE.BufferGeometry()
        this.particleMaterial = new THREE.ShaderMaterial(
            {
                vertexShader: vshader,
                fragmentShader: fshader,
                blending: THREE.AdditiveBlending,
                depthWrite: false,
                uniforms: {
                    uTime: new THREE.Uniform(0),
                    uMousePosition: this.mousePosition
                }
            }
        )

and then some code to place points in the BufferGeometry on a sphere. That is working fine.

Particlesphere

I also set up a Raycaster to track the mouse position intersecting a hidden plane and then update the uniform uMousePosition accordingly. That also works fine, I get the mouse position sent to my vertex shader.

Now I am trying to make the particles that are in a certain distance d to the mouse push away from it where the closest ones are pushed most of course, and also apply a gravity back to their original position to restore everything after time.

So here is what I have in my vertex shader:

void main() {
float lerp(float a, float b, float amount) {
    return a + (b - a) * amount;
}

void main() {
    vec3 p = position;

    float dist = min(distance(p, mousePosition), 1.);

    float lerpFactor = .2;

    p.x = lerp(p.x, position.x * dist, lerpFactor);
    p.y = lerp(p.y, position.y * dist, lerpFactor);
    p.z = lerp(p.z, position.z * dist, lerpFactor);//Mouse is always in z=0

    vec4 mvPosition = modelViewMatrix * vec4(p, 1.);
    gl_PointSize = 30. * (1. / -mvPosition.z );
    gl_Position = projectionMatrix * mvPosition;
}
}

And here is what it looks like when the mouse is outside the sphere (added a small sphere that moves with the mouseposition to indicate the mouseposition)

enter image description here

And here when the mouse is inside:

enter image description here

Outside already looks kind of correct, but mouse inside only moves the particles closer back to their original position, where it should push them further outside instead. I guess I somehow have to determine the direction of the distance.

Also, the lerp method does not lerp, the particles directly jump to their position.

So I wonder how I get the correct distance to the mouse to always move the particles in a certain area and also how to animate the lerp / gravity effect.


Solution

  • That's how you could do it as a first approximation:

    body{
      overflow: hidden;
      margin: 0;
    }
    <script type="module">
    import * as THREE from "https://cdn.skypack.dev/three@0.136.0";
    import {OrbitControls} from "https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/OrbitControls.js";
    import * as BufferGeometryUtils from "https://cdn.skypack.dev/three@0.136.0/examples/jsm/utils/BufferGeometryUtils.js";
    
    let scene = new THREE.Scene();
    let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 100);
    camera.position.set(0, 0, 10);
    let renderer = new THREE.WebGLRenderer();
    renderer.setSize(innerWidth, innerHeight);
    document.body.appendChild(renderer.domElement);
    
    let controls = new OrbitControls(camera, renderer.domElement);
    
    let marker = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 8), new THREE.MeshBasicMaterial({color: "red", wireframe: true}));
    scene.add(marker);
    
    let g = new THREE.IcosahedronGeometry(4, 20);
    g = BufferGeometryUtils.mergeVertices(g);
    let uniforms = {
      mousePos: {value: new THREE.Vector3()}
    }
    let m = new THREE.PointsMaterial({
      size: 0.1,
      onBeforeCompile: shader => {
        shader.uniforms.mousePos = uniforms.mousePos;
        shader.vertexShader = `
          uniform vec3 mousePos;
          ${shader.vertexShader}
        `.replace(
          `#include <begin_vertex>`,
          `#include <begin_vertex>
            
            vec3 seg = position - mousePos;
            vec3 dir = normalize(seg);
            float dist = length(seg);
            if (dist < 2.){
              float force = clamp(1. / (dist * dist), 0., 1.);
              transformed += dir * force;
            }
          
          `
        );
        console.log(shader.vertexShader);
      }
    });
    let p = new THREE.Points(g, m);
    scene.add(p);
    
    let clock = new THREE.Clock();
    
    renderer.setAnimationLoop( _ => {
      let t = clock.getElapsedTime();
      marker.position.x = Math.sin(t * 0.5) * 5;
      marker.position.y = Math.cos(t * 0.3) * 5;
      uniforms.mousePos.value.copy(marker.position);
      renderer.render(scene, camera);
    })
    </script>