Deep Sea Jellyfish

Code snippet ported from the artist @yuruyurau. Check out his stuff!

code.tsx

import * as THREE from "three";

const scene = new THREE.Scene();
scene.background = null; // transparent to show CSS gradient
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 2000);
camera.position.set(1.1, 364.0, -0.6);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, canvas: document.querySelector("canvas")! });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));

const N = 12000;
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(N * 3), 3));
geometry.setAttribute("color", new THREE.BufferAttribute(new Float32Array(N * 3), 3));
geometry.setAttribute("size", new THREE.BufferAttribute(new Float32Array(N), 1));

const material = new THREE.ShaderMaterial({
  uniforms: { opacity: { value: 0.85 } },
  vertexShader: `
    attribute float size;
    attribute vec3 color;
    varying float vDepth;
    varying vec3 vColor;
    void main() {
      vColor = color;
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      gl_PointSize = size * (450.0 / -mv.z);
      gl_Position = projectionMatrix * mv;
      vDepth = -mv.z;
    }`,
  fragmentShader: `
    uniform float opacity;
    varying float vDepth;
    varying vec3 vColor;
    void main() {
      float d = length(gl_PointCoord - 0.5);
      if (d > 0.5) discard;
      float a = opacity * smoothstep(0.5, 0.0, d) * smoothstep(600.0, 100.0, vDepth);
      gl_FragColor = vec4(vColor * a, a);
    }`,
  transparent: true,
  depthWrite: false,
  blending: THREE.AdditiveBlending
});

scene.add(new THREE.Points(geometry, material));

const tmpColor = new THREE.Color();
let t = 0;

function animate() {
  requestAnimationFrame(animate);
  t += Math.PI / 120;

  const pos = geometry.attributes.position.array as Float32Array;
  const col = geometry.attributes.color.array as Float32Array;
  const siz = geometry.attributes.size.array as Float32Array;

  for (let i = 0; i < N; i++) {
    const y = i / 345;
    const k = (y < 11 ? 6 + Math.sin((y | 0) ^ 8) * 6 : y / 5 + Math.cos(y / 2)) * Math.cos(i - t / 4);
    const e = y / 7 - 13;
    const d = Math.hypot(k, e) + Math.sin(e / 4 + t) / 2;
    const angle = d / 2 + 1 - t / 2;
    const q = y * k / d * (3 + Math.sin(d * 2 + y / 2 - t * 4));
    const r = 60 + q;

    pos[i * 3] = r * Math.cos(angle);
    pos[i * 3 + 1] = d * 29 - 170;
    pos[i * 3 + 2] = r * Math.sin(angle);

    const hue = (y / 30 + Math.sin(angle * 2) * 0.1 + d * 0.02 + t / 4) % 1;
    tmpColor.setHSL(hue < 0 ? hue + 1 : hue, 0.9, 0.55 + Math.sin(d + t * 2) * 0.1);
    col[i * 3] = tmpColor.r;
    col[i * 3 + 1] = tmpColor.g;
    col[i * 3 + 2] = tmpColor.b;

    siz[i] = 5.5 + Math.sin(d + t) * 1.2;
  }

  geometry.attributes.position.needsUpdate = true;
  geometry.attributes.color.needsUpdate = true;
  geometry.attributes.size.needsUpdate = true;

  renderer.render(scene, camera);
}

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

animate();

Markdown source · More bulbs by samples · Typebulb home