Search code examples
javascriptvectorgraphicsthree.jsparticle-system

How to define a cloud of particles relative to a vector path with Three.js?


I've been researching and playing with examples of particle clouds in Three.js. Most use shape geometries to define a field of particles, or parameters for distributing them randomly throughout the field of view. What I would like to do is create a particle cloud in which each particle has a relative proximity to an invisible vector path. For example, if I defined a lightly curved vector path, all the particles might float within a consistent radius along that invisible and then maybe taper toward the ends to form a hotdog-shaped cloud of particles. So, I know how to create particles and I know how to create vector paths, how do I link these two things together? Thanks!


Solution

  • You can define the path using two points. Let those points be p and q and let v = p - q. Any point M lying on the path must satisfy the vector equation

    M = (1 - lambda) * p + lambda * q
    

    for some 0 <= lambda <= 1. Thus, you can generate a random point on the path by generating a random lambda and using its value in the equation above:

    // p and q are instances of THREE.Vector3
    function pointOnPath(p, q) {
       var lambda = Math.random();
       var scaledp = (new THREE.Vector3()).copy(p).multiplyScalar(1 - lambda);
       var scaleq = (new THREE.Vector3()).copy(q).multiplyScalar(lambda);
       var result = (new THREE.Vector3()).addVectors(scaledp, scaledq);
       return result;
    } 
    

    Next, you want to modify the computed coordinates with some small radius so that they circle around the path. You do that by adding a small vector offset. How do we compute that vector then?

    The vector we are after lies in a plane that's perpendicular to the line from p to q. There are an infinite number of vectors satisfying the above condition, two of them being e1 = (v.y, -v.x, 0) and e2 = (v.z, 0, -v.x). Any vector of the form lambda * e1 + mu * e2 will also be perpendicular to the v. Thus, we need only generate lambda and mu and everything's ready.

    NOTE: lambda and mu must be random numbers in the interval [-1; 1], not [0; 1]. Since we are normalizing the offset vector, the interval [-0.5; 0.5] will suffice because normalization will map it to [-1; 1]

    function getVectorOffset(p, q, radius) {
        var v = (new THREE.Vector3()).subVectors(q, p);
        v.normalize();
        var e1 = new THREE.Vector3(v.y, -v.x, 0),
            e2 = new THREE.Vector3(v.z, 0, -v.x);
        e1.normalize();
        e2.normalize();
    
        var lambda = Math.random() - 0.5,
            mu = Math.random() - 0.5;
        var offset = e1.multiplyScalar(lambda).add(e2.multiplyScalar(mu));
        offset.normalize();
        offset.multiplyScalar(radius) // multiply the compute offset by the radius you'd like it to circle around
    
        return offset;
    
    }
    

    Finally, to generate your desired point:

    function pointOnHotDog(p, q, radius) {
        return pointOnPath(p, q).add(getVectorOffset(p, q, radius));
    }
    

    Here's a working jsfiddle