“使用 Three.js 的 WebGL 小实验。一个发光的 3D 漩涡,带有脉动的圆环、旋转的球体和动态粒子轨迹,由 Three.js、WebGL 和 GLSL 着色器提供支持,具有泛光、GPU 动画和交互式 OrbitControls。”

<style>* {margin: 0;padding: 0;overflow: hidden;background-color: #000;}canvas {width: 100%;height: 100vh;display: block;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);}</style><script type="importmap">{"imports": {"three": "https://unpkg.com/three@0.162.0/build/three.module.js","three/addons/controls/OrbitControls.js": "https://unpkg.com/three@0.162.0/examples/jsm/controls/OrbitControls.js","three/addons/postprocessing/EffectComposer.js": "https://unpkg.com/three@0.162.0/examples/jsm/postprocessing/EffectComposer.js","three/addons/postprocessing/RenderPass.js": "https://unpkg.com/three@0.162.0/examples/jsm/postprocessing/RenderPass.js","three/addons/postprocessing/UnrealBloomPass.js": "https://unpkg.com/three@0.162.0/examples/jsm/postprocessing/UnrealBloomPass.js"}}</script><script type="module">import * as THREE from 'three';import { OrbitControls } from 'three/addons/controls/OrbitControls.js';import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';const scene = new THREE.Scene();const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });renderer.setSize(window.innerWidth, window.innerHeight);renderer.setPixelRatio(window.devicePixelRatio);document.body.appendChild(renderer.domElement);const controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;controls.dampingFactor = 0.05;camera.position.set(0, 5, 20);controls.update();const nebulaGeometry = new THREE.SphereGeometry(100, 64, 64);const nebulaMaterial = new THREE.ShaderMaterial({uniforms: {time: { value: 0 }},vertexShader: `varying vec3 vPosition;void main() {vPosition = position;gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);}`,fragmentShader: `uniform float time;varying vec3 vPosition;float noise(vec3 p) {return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);}void main() {vec3 p = normalize(vPosition) * 10.0;float n = noise(p + vec3(time * 0.1));vec3 color = mix(vec3(0.2, 0.1, 0.4), vec3(0.1, 0.4, 0.3), n);float glow = pow(1.0 - length(vPosition) / 100.0, 2.0);gl_FragColor = vec4(color * glow, 0.3);}`,side: THREE.BackSide,transparent: true,blending: THREE.AdditiveBlending});const nebula = new THREE.Mesh(nebulaGeometry, nebulaMaterial);scene.add(nebula);const torusGeometry = new THREE.TorusGeometry(6, 1.5, 32, 100);const torusMaterial = new THREE.ShaderMaterial({uniforms: {time: { value: 0 }},vertexShader: `varying vec3 vNormal;varying vec3 vPosition;uniform float time;void main() {vNormal = normal;vPosition = position;vec3 pos = position;pos *= 1.0 + sin(time * 2.0 + length(position)) * 0.15;gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);}`,fragmentShader: `uniform float time;varying vec3 vNormal;varying vec3 vPosition;void main() {float glow = sin(time * 2.5 + vPosition.x * 1.5) * 0.5 + 0.5;vec3 baseColor = vec3(1.0, 0.2, 0.8);vec3 pulseColor = vec3(0.4, 0.1, 1.0);vec3 color = mix(baseColor, pulseColor, glow);float edge = smoothstep(0.4, 0.6, abs(vNormal.z));gl_FragColor = vec4(color * edge, 1.0);}`,side: THREE.DoubleSide});const torusCore = new THREE.Mesh(torusGeometry, torusMaterial);scene.add(torusCore);const innerSphereGeometry = new THREE.SphereGeometry(2, 32, 32);const innerSphereMaterial = new THREE.ShaderMaterial({uniforms: {time: { value: 0 }},vertexShader: `varying vec3 vNormal;varying vec2 vUv;void main() {vNormal = normal;vUv = uv;gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);}`,fragmentShader: `uniform float time;varying vec3 vNormal;varying vec2 vUv;void main() {vec2 uv = vUv;float t = time * 2.0;float pattern = sin(uv.x * 20.0 + t) * sin(uv.y * 20.0 - t);pattern += sin(uv.x * 15.0 - t * 1.5) * sin(uv.y * 15.0 + t * 1.5) * 0.5;float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0))), 2.0);vec3 baseColor = vec3(0.4, 0.1, 1.0);vec3 glowColor = vec3(1.0, 0.2, 0.8);vec3 finalColor = mix(baseColor, glowColor, pattern);float alpha = (pattern * 0.5 + fresnel * 0.7) * 0.8;gl_FragColor = vec4(finalColor, alpha);}`,transparent: true,blending: THREE.AdditiveBlending});const innerSphere = new THREE.Mesh(innerSphereGeometry, innerSphereMaterial);scene.add(innerSphere);const sphereCount = 12;const spheres = [];const trails = [];const sphereGeometry = new THREE.SphereGeometry(0.8, 32, 32);const sphereMaterial = new THREE.ShaderMaterial({uniforms: {time: { value: 0 }},vertexShader: `varying vec3 vPosition;void main() {vPosition = position;gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);}`,fragmentShader: `uniform float time;varying vec3 vPosition;void main() {float pulse = sin(time * 4.0 + vPosition.y * 2.0) * 0.5 + 0.5;vec3 color = vec3(0.1, 0.8, 0.2) * (pulse * 0.9 + 0.4);float edge = smoothstep(0.3, 0.5, length(vPosition) / 0.8);gl_FragColor = vec4(color * edge, 0.9);}`,transparent: true,blending: THREE.AdditiveBlending});for (let i = 0; i < sphereCount; i++) {const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);const trailGeometry = new THREE.BufferGeometry();const trailPositions = new Float32Array(100 * 3);const trailMaterial = new THREE.LineBasicMaterial({color: 0x33ff66,transparent: true,opacity: 0.3,blending: THREE.AdditiveBlending});const trail = new THREE.Line(trailGeometry, trailMaterial);sphere.userData = {orbitAngle: Math.random() * Math.PI * 2,orbitSpeed: 0.05 + Math.random() * 0.03,orbitRadiusX: 10 + Math.random() * 4,orbitRadiusZ: 8 + Math.random() * 3,trailPositions: trailPositions,trailIndex: 0};spheres.push(sphere);trails.push(trail);scene.add(sphere);scene.add(trail);}const vortexCount = 600;const vortices = new THREE.Group();const vortexGeometry = new THREE.BufferGeometry();const vortexPositions = new Float32Array(vortexCount * 3);const vortexColors = new Float32Array(vortexCount * 3);const vortexSizes = new Float32Array(vortexCount);for (let i = 0; i < vortexCount; i++) {vortexPositions[i * 3] = 0;vortexPositions[i * 3 + 1] = 0;vortexPositions[i * 3 + 2] = 0;const color = new THREE.Color().setHSL(Math.random(), 0.7, 0.6);vortexColors[i * 3] = color.r;vortexColors[i * 3 + 1] = color.g;vortexColors[i * 3 + 2] = color.b;vortexSizes[i] = 0.3 + Math.random() * 0.2;}vortexGeometry.setAttribute('position', new THREE.BufferAttribute(vortexPositions, 3));vortexGeometry.setAttribute('customColor', new THREE.BufferAttribute(vortexColors, 3));vortexGeometry.setAttribute('size', new THREE.BufferAttribute(vortexSizes, 1));const vortexMaterial = new THREE.ShaderMaterial({uniforms: {time: { value: 0 }},vertexShader: `attribute vec3 customColor;attribute float size;varying vec3 vColor;varying float vAlpha;void main() {vColor = customColor;vAlpha = 1.0 - length(position) / 20.0;gl_PointSize = size * (400.0 / -modelViewMatrix[3][2]);gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);}`,fragmentShader: `uniform float time;varying vec3 vColor;varying float vAlpha;void main() {vec2 uv = gl_PointCoord - 0.5;float dist = length(uv);if (dist > 0.5) discard;float shimmer = sin(time * 8.0 + dist * 15.0) * 0.3 + 0.7;float glow = exp(-dist * 5.0);gl_FragColor = vec4(vColor * shimmer, glow * vAlpha * 0.8);}`,transparent: true,blending: THREE.AdditiveBlending});const vortexSystem = new THREE.Points(vortexGeometry, vortexMaterial);vortices.add(vortexSystem);scene.add(vortices);const vortexData = Array(vortexCount).fill().map(() => ({angle: Math.random() * Math.PI * 2,speed: 0.06 + Math.random() * 0.04,distance: 0,maxDistance: 15 + Math.random() * 5,twist: 0.4 + Math.random() * 0.3}));const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);scene.add(ambientLight);const pointLight1 = new THREE.PointLight(0xffe6ff, 2.0, 100);pointLight1.position.set(15, 15, 15);scene.add(pointLight1);const pointLight2 = new THREE.PointLight(0xe6ffec, 1.6, 100);pointLight2.position.set(-15, -15, -15);scene.add(pointLight2);const pointLight3 = new THREE.PointLight(0xe6f0ff, 1.2, 100);pointLight3.position.set(0, 20, -10);scene.add(pointLight3);const composer = new EffectComposer(renderer);const renderPass = new RenderPass(scene, camera);composer.addPass(renderPass);const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.4, 0.6, 0.85);bloomPass.threshold = 0.2;bloomPass.strength = 1.6;bloomPass.radius = 0.6;composer.addPass(bloomPass);let time = 0;function animate() {requestAnimationFrame(animate);time += 0.01;nebulaMaterial.uniforms.time.value = time;torusCore.rotation.y += 0.005;torusCore.rotation.z += 0.003;torusMaterial.uniforms.time.value = time;innerSphereMaterial.uniforms.time.value = time;spheres.forEach((sphere, i) => {const data = sphere.userData;data.orbitAngle += data.orbitSpeed;sphere.position.x = Math.cos(data.orbitAngle) * data.orbitRadiusX;sphere.position.z = Math.sin(data.orbitAngle) * data.orbitRadiusZ;sphere.position.y = Math.sin(data.orbitAngle * 1.5 + time) * 3;sphere.material.uniforms.time.value = time;const trail = trails[i];const pos = data.trailPositions;const idx = data.trailIndex * 3;pos[idx] = sphere.position.x;pos[idx + 1] = sphere.position.y;pos[idx + 2] = sphere.position.z;data.trailIndex = (data.trailIndex + 1) % 100;trail.geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3));trail.geometry.attributes.position.needsUpdate = true;});const vortexPosArray = vortexSystem.geometry.attributes.position.array;for (let i = 0; i < vortexCount; i++) {const data = vortexData[i];data.distance += data.speed;const angle = data.angle + time * data.twist;vortexPosArray[i * 3] = Math.cos(angle) * data.distance * (1.0 + data.twist);vortexPosArray[i * 3 + 1] = Math.sin(angle) * data.distance * 0.6;vortexPosArray[i * 3 + 2] = Math.sin(angle + data.twist) * data.distance;if (data.distance > data.maxDistance) {data.distance = 0;}}vortexSystem.geometry.attributes.position.needsUpdate = true;vortexMaterial.uniforms.time.value = time;controls.update();composer.render();}animate();window.addEventListener('resize', () => {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);composer.setSize(window.innerWidth, window.innerHeight);});</script>
源码:
https://codepen.io/VoXelo/pen/GgRoaow
体验:
https://codepen.io/VoXelo/full/GgRoaow

