<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>3D 粒子光环球</title>
</head>
<body>
<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"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.01);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: 'high-performance',
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.rotateSpeed = 0.5;
controls.minDistance = 10;
controls.maxDistance = 30;
camera.position.z = 15;
camera.position.y = 5;
controls.target.set(0, 0, 0);
controls.update();
const pointMaterialShader = {
vertexShader: `
attribute float size;
varying vec3 vColor;
varying float vDistance;
uniform float time;
void main() {
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vDistance = -mvPosition.z;
float pulse = sin(time * 2.0 + length(position)) * 0.15 + 1.0;
vec3 pos = position;
pos.x += sin(time + position.z * 0.5) * 0.05;
pos.y += cos(time + position.x * 0.5) * 0.05;
pos.z += sin(time + position.y * 0.5) * 0.05;
mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = size * (300.0 / -mvPosition.z) * pulse;
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vDistance;
uniform float time;
void main() {
vec2 cxy = 2.0 * gl_PointCoord - 1.0;
float r = dot(cxy, cxy);
if (r > 1.0) discard;
float glow = exp(-r * 2.5);
float outerGlow = exp(-r * 1.5) * 0.3;
vec3 finalColor = vColor * (1.2 + sin(time * 0.5) * 0.1);
finalColor += vec3(0.2, 0.4, 0.6) * outerGlow;
float distanceFade = 1.0 - smoothstep(0.0, 50.0, vDistance);
float intensity = mix(0.7, 1.0, distanceFade);
gl_FragColor = vec4(finalColor * intensity, (glow + outerGlow) * distanceFade);
}
`,
};
function createSpiralSphere(radius, particleCount, colors) {
const geometry = new THREE.BufferGeometry();
const positions = [];
const particleColors = [];
const sizes = [];
for (let i = 0; i < particleCount; i++) {
const phi = Math.acos(-1 + (2 * i) / particleCount);
const theta = Math.sqrt(particleCount * Math.PI) * phi;
const x = radius * Math.sin(phi) * Math.cos(theta);
const y = radius * Math.sin(phi) * Math.sin(theta);
const z = radius * Math.cos(phi);
positions.push(x, y, z);
const colorPos = i / particleCount;
const color1 = colors[Math.floor(colorPos * (colors.length - 1))];
const color2 = colors[Math.ceil(colorPos * (colors.length - 1))];
const mixRatio = (colorPos * (colors.length - 1)) % 1;
const finalColor = new THREE.Color().lerpColors(
color1,
color2,
mixRatio
);
particleColors.push(finalColor.r, finalColor.g, finalColor.b);
sizes.push(Math.random() * 0.15 + 0.08);
}
geometry.setAttribute(
'position',
new THREE.Float32BufferAttribute(positions, 3)
);
geometry.setAttribute(
'color',
new THREE.Float32BufferAttribute(particleColors, 3)
);
geometry.setAttribute(
'size',
new THREE.Float32BufferAttribute(sizes, 1)
);
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
},
vertexShader: pointMaterialShader.vertexShader,
fragmentShader: pointMaterialShader.fragmentShader,
vertexColors: true,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
return new THREE.Points(geometry, material);
}
function createOrbitRings(radius, count, thickness) {
const group = new THREE.Group();
for (let i = 0; i < count; i++) {
const ringGeometry = new THREE.BufferGeometry();
const positions = [];
const colors = [];
const sizes = [];
const particleCount = 3000;
for (let j = 0; j < particleCount; j++) {
const angle = (j / particleCount) * Math.PI * 2;
const radiusVariation = radius + (Math.random() - 0.5) * thickness;
const x = Math.cos(angle) * radiusVariation;
const y = (Math.random() - 0.5) * thickness;
const z = Math.sin(angle) * radiusVariation;
positions.push(x, y, z);
const hue = (i / count) * 0.7 + (j / particleCount) * 0.3;
const color = new THREE.Color().setHSL(hue, 1, 0.6);
color.multiplyScalar(1.2);
colors.push(color.r, color.g, color.b);
sizes.push(Math.random() * 0.12 + 0.06);
}
ringGeometry.setAttribute(
'position',
new THREE.Float32BufferAttribute(positions, 3)
);
ringGeometry.setAttribute(
'color',
new THREE.Float32BufferAttribute(colors, 3)
);
ringGeometry.setAttribute(
'size',
new THREE.Float32BufferAttribute(sizes, 1)
);
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
},
vertexShader: pointMaterialShader.vertexShader,
fragmentShader: pointMaterialShader.fragmentShader,
vertexColors: true,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const ring = new THREE.Points(ringGeometry, material);
ring.rotation.x = Math.random() * Math.PI;
ring.rotation.y = Math.random() * Math.PI;
group.add(ring);
}
return group;
}
const sphereColors = [
new THREE.Color(0x00ffff).multiplyScalar(1.2),
new THREE.Color(0xff1493).multiplyScalar(1.1),
new THREE.Color(0x4169e1).multiplyScalar(1.2),
new THREE.Color(0xff69b4).multiplyScalar(1.1),
new THREE.Color(0x00bfff).multiplyScalar(1.2),
];
const coreSphere = createSpiralSphere(4, 25000, sphereColors);
const orbitRings = createOrbitRings(5.8, 6, 0.4);
const mainGroup = new THREE.Group();
mainGroup.scale.set(1.2, 1.2, 1.2);
mainGroup.add(coreSphere);
mainGroup.add(orbitRings);
scene.add(mainGroup);
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.002;
coreSphere.material.uniforms.time.value = time;
orbitRings.children.forEach((ring) => {
ring.material.uniforms.time.value = time;
});
coreSphere.rotation.y += 0.001;
coreSphere.rotation.x = Math.sin(time * 0.5) * 0.15;
orbitRings.children.forEach((ring, index) => {
const dynamicSpeed =
0.001 * (Math.sin(time * 0.2) + 2.0) * (index + 1);
ring.rotation.z += dynamicSpeed;
ring.rotation.x += dynamicSpeed * 0.6;
ring.rotation.y += dynamicSpeed * 0.4;
});
const breathe = 1 + Math.sin(time * 1.5) * 0.1;
coreSphere.scale.set(breathe, breathe, breathe);
controls.update();
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
});
animate();
</script>
</body>
</html>