“使用 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