实测!Three.js 实现动态粒子螺旋与星环环绕动画

这个页面采用极简的 HTML 结构,完全依赖 JavaScript 和 Three.js 创建复杂的 3D 视觉效果。CSS 样式主要用于确保 Canvas 元素的全屏显示和居中定位,而所有的视觉效果(粒子、发光、动画)都通过 Three.js 的渲染能力和自定义着色器实现。整体设计注重视觉冲击力和用户交互体验,通过粒子系统、动态光影和流畅动画创造出一个引人入胜的沉浸式 3D 环境。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <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>
</head>

<body>
    <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>

代码解析

全局样式

css 复制代码
* {
    margin: 0;
    padding: 0;
    overflow: hidden;
    background-color: #000;
}

重置所有元素的内外边距

隐藏溢出内容(防止滚动条出现)

设置黑色背景(与 3D 场景保持一致)

Canvas 样式

css 复制代码
canvas {
    width: 100%;
    height: 100vh;
    display: block;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

全屏尺寸显示(覆盖整个视口)

固定定位,确保在页面滚动时保持居中

使用 transform 实现精确居中定位

全屏沉浸式体验

整个页面被黑色背景覆盖

Canvas 元素无缝填充整个视口

无任何 UI 元素干扰,专注于 3D 内容

高性能渲染优化

javascript 复制代码
renderer = new THREE.WebGLRenderer({
    antialias: true,
    powerPreference: "high-performance"
});

启用抗锯齿以提升视觉质量

优先使用高性能 GPU 渲染模式

适配设备像素比以在高 DPI 屏幕上保持清晰度

响应式设计

javascript 复制代码
window.addEventListener('resize', () => {
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
    renderer.setSize(width, height);
});

监听窗口大小变化

动态调整相机视口和渲染器尺寸

确保 3D 场景在任何屏幕尺寸下都能正确显示

视觉效果优化

使用 AdditiveBlending 混合模式创建粒子发光效果

设置 depthWrite: false 避免粒子间的深度冲突

通过自定义着色器实现动态光效和脉动效果

交互体验

javascript 复制代码
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

轨道控制器允许用户旋转、缩放和平移场景

阻尼效果提供流畅的惯性感

限制最小 / 最大距离防止视角过近或过远


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

相关推荐
@大迁世界7 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路16 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug20 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213821 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中43 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星1 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全