// PlasmaGlobe.tsx "use client"; import React, { useEffect, useRef } from "react"; import { Renderer, Program, Mesh, Triangle } from "ogl"; interface PlasmaGlobeProps { speed?: number; // global time speed multiplier intensity?: number; // color intensity multiplier } const VERTEX_SHADER = `#version 300 es in vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); } `; /* Adapted & simplified plasma globe fragment shader (from Shadertoy-style code). Replaces texture-based noise with small procedural noise functions so it runs without external textures. Uses uniforms: - uTime (float) - uResolution (vec2) - uMouse (vec2) - uSpeed (float) - uIntensity (float) NOTE: keep an eye on precision and performance on low-end GPUs. */ const FRAGMENT_SHADER = `#version 300 es precision highp float; out vec4 fragColor; uniform float uTime; uniform vec2 uResolution; uniform vec2 uMouse; uniform float uSpeed; uniform float uIntensity; #define NUM_RAYS 13.0 #define VOLUMETRIC_STEPS 19 #define MAX_ITER 35 #define FAR 6.0 // small 2x2 rotation matrix mat2 mm2(float a){ float c = cos(a), s = sin(a); return mat2(c, -s, s, c); } // simple hash-based random float hash1(float n){ return fract(sin(n)*43758.5453); } float hash2(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); } // value noise from vec3 -> float (cheap, not high quality but ok) float noise3(vec3 p){ // grid cell vec3 ip = floor(p); vec3 fp = fract(p); fp = fp*fp*(3.0-2.0*fp); float n000 = hash2(ip.xy + ip.z*7.0); float n100 = hash2(ip.xy + vec2(1.0,0.0) + ip.z*7.0); float n010 = hash2(ip.xy + vec2(0.0,1.0) + ip.z*7.0); float n110 = hash2(ip.xy + vec2(1.0,1.0) + ip.z*7.0); float nx0 = mix(n000, n100, fp.x); float nx1 = mix(n010, n110, fp.x); float nxy = mix(nx0, nx1, fp.y); // incorporate z as small modulation using hash float nz = mix(nxy, hash1(ip.z + 1.0), fp.z); return nz; } // light-weight fractal noise (based on noise3) float flow(vec3 p, float t){ float rz = 0.0; vec3 bp = p; float z = 2.0; // a few octaves for (int i = 1; i < 5; i++){ p += t * 0.1; rz += (sin(noise3(p + t*0.8) * 6.0) * 0.5 + 0.5) / z; p = mix(bp, p, 0.6); z *= 2.0; p *= 2.01; p *= mat3( 0.00, 0.80, 0.60, -0.80, 0.36, -0.48, -0.60, -0.48, 0.64 ); } return rz; } // helper used to create wavy variations (low-frequency) float sins(float x, float t){ float rz = 0.0; float z = 2.0; for (int i = 0; i < 3; i++){ rz += abs(fract(x * 1.4) - 0.5) / z; x *= 1.3; z *= 1.15; x -= t * 0.65 * z; } return rz; } float segm(vec3 p, vec3 a, vec3 b){ vec3 pa = p - a; vec3 ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba * h) * 0.5; } vec3 path(float i, float d, float t){ // produce a pseudo-random endpoint on unit sphere influenced by i and d float sns2 = sins(d + i * 0.5, t) * 0.22; float sns = sins(d + i * 0.6, t) * 0.21; float a1 = (hash1(i * 10.569) - 0.5) * 6.2 + sns2; float a2 = (hash1(i * 4.732) - 0.5) * 6.2 + sns; vec3 en = vec3(0.0, 0.0, 1.0); en.xz *= mat2(cos(a1), -sin(a1), sin(a1), cos(a1)); en.xy *= mat2(cos(a2), -sin(a2), sin(a2), cos(a2)); return en; } vec2 map(vec3 p, float i, float t){ float lp = length(p); vec3 bg = vec3(0.0); vec3 en = path(i, lp, t); float ins = smoothstep(0.11, 0.46, lp); float outs = 0.15 + smoothstep(0.0, 0.15, abs(lp - 1.0)); p *= ins * outs; float id = ins * outs; float rz = segm(p, bg, en) - 0.011; return vec2(rz, id); } // sphere-ray intersection helper vec2 iSphere2(vec3 ro, vec3 rd){ vec3 oc = ro; float b = dot(oc, rd); float c = dot(oc, oc) - 1.0; float h = b*b - c; if (h < 0.0) return vec2(-1.0); return vec2((-b - sqrt(h)), (-b + sqrt(h))); } // volumetric march (accumulates light along a ray) vec3 vmarch(vec3 ro, vec3 rd, float j, vec3 orig, float t){ vec3 p = ro; vec3 sum = vec3(0.0); for (int i = 0; i < VOLUMETRIC_STEPS; i++){ vec2 r = map(p, j, t); p += rd * 0.03; float lp = length(p); // create color base per-step vec3 col = sin(vec3(1.05, 2.5, 1.52) * 3.94 + r.y) * 0.85 + 0.4; col *= smoothstep(0.0, 0.015, -r.x); col *= smoothstep(0.04, 0.2, abs(lp - 1.1)); col *= smoothstep(0.1, 0.34, lp); // noise modulation float n = noise3(vec3(lp * 2.0 + j * 13.0 + t * 5.0)); // attenuate with distance from origin and add float denom = max(0.0001, log(max(0.0001, distance(p, orig) - 2.0)) + 0.75); sum += abs(col) * 5.0 * (1.2 - n * 1.1) / denom; } return sum; } // ray-marching distance estimator to sphere-like structures float march(vec3 ro, vec3 rd, float startf, float maxd, float j, float t){ float precis = 0.001; float h = 0.5; float d = startf; for (int i = 0; i < MAX_ITER; i++){ if (abs(h) < precis || d > maxd) break; d += h * 1.2; float res = map(ro + rd * d, j, t).x; h = res; } return d; } void main(){ // Normalized coords (-0.5..0.5) vec2 uv = (gl_FragCoord.xy / uResolution.xy) - 0.5; uv.x *= uResolution.x / uResolution.y; vec2 um = (uMouse.xy / uResolution.xy) - 0.5; // camera setup vec3 ro = vec3(0.0, 0.0, 5.0); vec3 rd = normalize(vec3(uv * 0.7, -1.5)); mat2 mx = mm2(uTime * 0.4 + um.x * 6.0); mat2 my = mm2(uTime * 0.3 + um.y * 6.0); ro.xz *= mx; rd.xz *= mx; ro.xy *= my; rd.xy *= my; vec3 bro = ro; vec3 brd = rd; vec3 col = vec3(0.0125, 0.0, 0.025); // multiple rays to create many filaments for (float j = 1.0; j < NUM_RAYS + 1.0; j++){ ro = bro; rd = brd; mat2 mm = mm2((uTime * 0.1 + ((j + 1.0) * 5.1)) * j * 0.25); ro.xy *= mm; rd.xy *= mm; ro.xz *= mm; rd.xz *= mm; float rz = march(ro, rd, 2.5, FAR, j, uTime); if (rz >= FAR) continue; vec3 pos = ro + rz * rd; col = max(col, vmarch(pos, rd, j, bro, uTime)); } ro = bro; rd = brd; vec2 sph = iSphere2(ro, rd); if (sph.x > 0.0){ vec3 pos = ro + rd * sph.x; vec3 pos2 = ro + rd * sph.y; vec3 rf = reflect(rd, normalize(pos)); vec3 rf2 = reflect(rd, normalize(pos2)); float nz = (-log(abs(flow(rf * 1.2, uTime) - 0.01) + 0.00001)); float nz2 = (-log(abs(flow(rf2 * 1.2, -uTime) - 0.01) + 0.00001)); col += (0.1 * nz * nz * vec3(0.12, 0.12, 0.5) + 0.05 * nz2 * nz2 * vec3(0.55, 0.2, 0.55)) * 0.8; } // final tone mapping & intensity col *= (1.0 + uIntensity * 0.6); col = pow(clamp(col, 0.0, 10.0), vec3(1.5)); fragColor = vec4(col * 1.3, 1.0); } `; export default function PlasmaGlobe({ speed = 1.0, intensity = 1.0, }: PlasmaGlobeProps) { const containerRef = useRef(null); const mouseRef = useRef({ x: 0, y: 0 }); useEffect(() => { const container = containerRef.current; if (!container) return; // create renderer const renderer = new Renderer({ alpha: true, antialias: true }); const gl = renderer.gl; gl.clearColor(0, 0, 0, 0); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); // geometry const geometry = new Triangle(gl); // program const program = new Program(gl, { vertex: VERTEX_SHADER, fragment: FRAGMENT_SHADER, uniforms: { uTime: { value: 0 }, uResolution: { value: [container.offsetWidth, container.offsetHeight] }, uMouse: { value: [0, 0] }, uSpeed: { value: speed }, uIntensity: { value: intensity }, }, }); const mesh = new Mesh(gl, { geometry, program }); container.appendChild(gl.canvas); // resize const resize = () => { const width = container.offsetWidth; const height = container.offsetHeight; renderer.setSize(width, height); program.uniforms.uResolution.value = [width, height]; }; window.addEventListener("resize", resize); resize(); // mouse smoothing const onMouse = (e: MouseEvent) => { mouseRef.current.x += (e.clientX - mouseRef.current.x) * 0.08; mouseRef.current.y += (e.clientY - mouseRef.current.y) * 0.08; }; window.addEventListener("mousemove", onMouse); let rafId = 0; const loop = (t: number) => { rafId = requestAnimationFrame(loop); // uTime passed in seconds, multiplied by speed program.uniforms.uTime.value = (t * 0.001) * speed; program.uniforms.uMouse.value = [mouseRef.current.x, mouseRef.current.y]; program.uniforms.uIntensity.value = intensity; renderer.render({ scene: mesh }); }; rafId = requestAnimationFrame(loop); return () => { cancelAnimationFrame(rafId); window.removeEventListener("resize", resize); window.removeEventListener("mousemove", onMouse); if (gl.canvas.parentNode === container) container.removeChild(gl.canvas); gl.getExtension("WEBGL_lose_context")?.loseContext(); }; }, [speed, intensity]); return (
); }