<script src="https://cdn.jsdelivr.net/npm/three@0.115.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/controls/OrbitControls.js"></script>
<button id="btnRepeat" style="position: absolute; margin: 5px;">Repeat</button>
body {
  overflow: hidden;
  margin: 0;
}
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
  60,
  window.innerWidth / window.innerHeight,
  1,
  1000
);
camera.position.set(0, 8, 13);
camera.lookAt(0, 5, 0);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x101010);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", onWindowResize, false);

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

scene.add(new THREE.GridHelper(10, 10));

var rMin = 5;
var rMax = 15;
var hMin = 0;
var hMax = 5;

var centerH = 6;

var delayMax = 1; // seconds
var colors = [
  0xd50f30,
  0x039bdc,
  0xfc5411,
  0x1a2683,
  0xfbe23d,
  0x12923b
];
var c = new THREE.Color();
var MAX_POINTS = 5000;
var pointsCount = 0;
var points = []; //3

var delay = [];  //1
var color = [];  //3
var turns = [];  //1
var turnsMin = 5;
var turnsMax = 10;
var angle = [];
while( pointsCount < MAX_POINTS){
  
  let a = THREE.Math.randFloat(0, Math.PI * 2);
  let vec = new THREE.Vector3().setFromCylindricalCoords(
    THREE.Math.randFloat(rMin, rMax),
    a,
    THREE.Math.randFloat(hMin, hMax)
  );

  points.push(vec);

  delay.push(THREE.Math.randFloat(-delayMax,0));
  
  c.set(colors[THREE.Math.randInt(0, colors.length - 1)]);
  color.push(c.r, c.g, c.b);
  
  angle.push(a);
  
  turns.push(THREE.Math.randFloat(turnsMin, turnsMax));
  
  pointsCount ++;
}

var pointsTornadoGeom = new THREE.BufferGeometry().setFromPoints(points);
pointsTornadoGeom.setAttribute("color", new THREE.BufferAttribute(new Float32Array(color), 3));
pointsTornadoGeom.setAttribute("delay", new THREE.BufferAttribute(new Float32Array(delay), 1));
pointsTornadoGeom.setAttribute("angle", new THREE.BufferAttribute(new Float32Array(angle), 1));
pointsTornadoGeom.setAttribute("turns", new THREE.BufferAttribute(new Float32Array(turns), 1));

var pointsTornadoMat = new THREE.PointsMaterial(
  {
  vertexColors: THREE.VertexColors, 
    size: 0.2, 
    map: new THREE.TextureLoader().load("https://threejs.org/examples/textures/sprites/ball.png", tex => {
      tex.center.setScalar(0.5);
      tex.rotation = -Math.PI * 0.5;
    }), 
    alphaTest: 0.5
  }
);

var uniformsTornado = {
  time: {value: 0},
  centerH: {value: centerH},
  duration: {value: 10 - delayMax}, // seconds
  turnsMax: {value: turnsMax}
};

pointsTornadoMat.onBeforeCompile = shader => {
  shader.uniforms.time = uniformsTornado.time;
  shader.uniforms.centerH = uniformsTornado.centerH;
  shader.uniforms.duration = uniformsTornado.duration;
  shader.uniforms.turnsMax = uniformsTornado.turnsMax;
  
  shader.vertexShader = `
    uniform float time;
    uniform float centerH;
    uniform float duration;
    uniform float turnsMax;
    
    attribute float delay;
    attribute float angle;
    attribute float turns;

    varying float vRatio;
` + shader.vertexShader;
  
  shader.vertexShader = shader.vertexShader.replace(
    `#include <begin_vertex>`,
    `#include <begin_vertex>
    
    float t = time + delay;
    t = t < 0. ? 0. : t;

    float tRatio = 0.;
    tRatio = clamp( (t * (turnsMax / turns)) / duration, 0., 1.);
    vRatio = step(0.01, tRatio) - step(0.999, tRatio);
    
    float durationA = PI * 2. * turns * tRatio;;
    float A = angle + durationA;    

    transformed.x = cos(A);
    transformed.z = sin(-A);
    
    float l = length(position.xz);
    transformed.xz *= l * mix(1., 0., tRatio);

    transformed.y = mix(position.y, centerH, tRatio);
   
`
  );
  
  shader.vertexShader = shader.vertexShader.replace(
    `gl_PointSize = size;`,
    `gl_PointSize = size * smoothstep(0.01, 0.1, tRatio);`
  );
  
  shader.fragmentShader = `
    varying float vRatio;
` + shader.fragmentShader;
  shader.fragmentShader = shader.fragmentShader.replace(
    `#include <clipping_planes_fragment>`,
    `
    if (floor(vRatio + 0.5) < 1.) discard;
    #include <clipping_planes_fragment>`
  );
};



var pointsTornado = new THREE.Points(pointsTornadoGeom, pointsTornadoMat);
scene.add(pointsTornado);

var clock = new THREE.Clock();
var t = 0;

btnRepeat.addEventListener("click", event => {t = 0;}, false);

renderer.setAnimationLoop(() => {
  
  t += clock.getDelta();
  uniformsTornado.time.value = t;
  renderer.render(scene, camera);
  
});

function onWindowResize() {
  
  var width = window.innerWidth;
  var height = window.innerHeight;
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
  
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.