import * as THREE from "three";
import { OrbitControls } from "jsm/controls/OrbitControls.js";
import { EffectComposer } from "jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "jsm/postprocessing/UnrealBloomPass.js";
import { SimplexNoise } from "jsm/math/SimplexNoise.js";
import GUI from 'jsm/libs/lil-gui.module.min.js';
const simplex = new SimplexNoise();
const w = window.innerWidth;
const h = window.innerHeight;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.035);
const camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.body.appendChild(renderer.domElement);
let controls = new OrbitControls(camera, renderer.domElement);
Object.assign(controls, {
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(w, h), 1.5, 0.4, 100);
const composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
const loopGroup = new THREE.Group();
loopGroup.userData.update = (timeStamp) => {};
function getRandomColor() {
const randomIndex = Math.floor(Math.random() * colorPalette.length);
return colorPalette[randomIndex];
function getRandomSize(minSize, maxSize) {
return Math.random() * (maxSize - minSize) + minSize;
function createWigglingLoop(segments, time, offset = 0) {
for (let i = 0; i <= segments; i++) {
const theta = (i / segments) * Math.PI * 2;
const x = Math.cos(theta) * (2 + simplex.noise3d(time + Math.cos(theta), time + Math.sin(theta), time) * noiseFactor);
const y = Math.sin(theta) * (2 + simplex.noise3d(time + Math.cos(theta), time + Math.sin(theta), time) * noiseFactor);
const z = simplex.noise3d(time + Math.cos(theta), time - Math.sin(theta), time) * noiseFactor;
points.push(new THREE.Vector3(x, y, z));
function getRandomSpherePoint({ radius = 10 }) {
const minRadius = radius * 0.25;
const maxRadius = radius - minRadius;
const range = Math.random() * maxRadius + minRadius;
const theta = 2 * Math.PI * u;
const phi = Math.acos(2 * v - 1);
x: range * Math.sin(phi) * Math.cos(theta),
y: range * Math.sin(phi) * Math.sin(theta),
z: range * Math.cos(phi),
function getRandomTorusPoint({ radiusTorus = 25, tubeRadius = 10 }) {
const u = Math.random() * Math.PI * 2;
const v = Math.random() * Math.PI * 2;
const x = (radiusTorus + tubeRadius * Math.cos(v)) * Math.cos(u);
const y = tubeRadius * Math.sin(v);
const z = (radiusTorus + tubeRadius * Math.cos(v)) * Math.sin(u);
const loopGeometry = new THREE.BufferGeometry();
const positions = new Float32Array((segments + 1) * 3);
loopGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const instancedGeometry = new THREE.InstancedBufferGeometry();
instancedGeometry.index = loopGeometry.index;
instancedGeometry.attributes.position = loopGeometry.attributes.position;
const offsets = new Float32Array(numLoops * 3);
const instanceColors = new Float32Array(numLoops * 3);
const sizes = new Float32Array(numLoops);
const rotations = new Float32Array(numLoops * 3);
for (let i = 0; i < numLoops; i++) {
const randomColor = new THREE.Color(getRandomColor());
instanceColors[i * 3] = randomColor.r;
instanceColors[i * 3 + 1] = randomColor.g;
instanceColors[i * 3 + 2] = randomColor.b;
sizes[i] = getRandomSize(minLoopSize, maxLoopSize);
const { x, y, z } = getRandomSpherePoint({ radius });
rotations[i * 3] = Math.random() * 2 * Math.PI;
rotations[i * 3 + 1] = Math.random() * 2 * Math.PI;
rotations[i * 3 + 2] = Math.random() * 2 * Math.PI;
instancedGeometry.setAttribute('offset', new THREE.InstancedBufferAttribute(offsets, 3));
instancedGeometry.setAttribute('instanceColor', new THREE.InstancedBufferAttribute(instanceColors, 3));
instancedGeometry.setAttribute('size', new THREE.InstancedBufferAttribute(sizes, 1));
instancedGeometry.setAttribute('rotation', new THREE.InstancedBufferAttribute(rotations, 3));
const material = new THREE.ShaderMaterial({
attribute vec3 instanceColor;
vec3 newPosition = position * size;
mat3 rotationMatrix = mat3(
cos(rotation.y) * cos(rotation.z), -cos(rotation.y) * sin(rotation.z), sin(rotation.y),
cos(rotation.x) * sin(rotation.z) + sin(rotation.x) * sin(rotation.y) * cos(rotation.z), cos(rotation.x) * cos(rotation.z) - sin(rotation.x) * sin(rotation.y) * sin(rotation.z), -sin(rotation.x) * cos(rotation.y),
sin(rotation.x) * sin(rotation.z) - cos(rotation.x) * sin(rotation.y) * cos(rotation.z), sin(rotation.x) * cos(rotation.z) + cos(rotation.x) * sin(rotation.y) * sin(rotation.z), cos(rotation.x) * cos(rotation.y)
newPosition = rotationMatrix * newPosition + offset;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
gl_FragColor = vec4(vColor, 1.0);
const mesh = new THREE.Line(instancedGeometry, material);
function animate(timeStamp = 0) {
requestAnimationFrame(animate);
const positions = loopGeometry.attributes.position.array;
for (let i = 0; i < numLoops; i++) {
const loopPoints = createWigglingLoop(segments, timeStamp * 0.001 + i / 10);
for (let j = 0; j <= segments; j++) {
positions[j * 3 + 1] = p.y;
positions[j * 3 + 2] = p.z;
loopGeometry.attributes.position.needsUpdate = true;
composer.render(scene, camera);
function handleWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
window.addEventListener("resize", handleWindowResize, false);
gui.add(settings, 'autoRotate').name('Auto Rotate').onChange(value => {
controls.autoRotate = value;
gui.add(settings, 'pointType', ['Sphere', 'Torus']).onChange(value => {
for (let i = 0; i < numLoops; i++) {
const { x, y, z } = (value === 'Sphere') ? getRandomSpherePoint({ radius }) : getRandomTorusPoint({ radiusTorus, tubeRadius });
instancedGeometry.attributes.offset.needsUpdate = true;