实测!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/[email protected]/build/three.module.js",
            "three/addons/controls/OrbitControls.js": "https://unpkg.com/[email protected]/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;

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

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

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


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

相关推荐
GH小杨几秒前
JS之Dom模型和Bom模型
前端·javascript·html
星月心城1 小时前
JS深入之从原型到原型链
前端·javascript
MessiGo1 小时前
Javascript 编程基础(5)面向对象 | 5.2、原型系统
开发语言·javascript·原型模式
你的人类朋友1 小时前
🤔Token 存储方案有哪些
前端·javascript·后端
烛阴1 小时前
从零开始:使用Node.js和Cheerio进行轻量级网页数据提取
前端·javascript·后端
liuyang___2 小时前
日期的数据格式转换
前端·后端·学习·node.js·node
西哥写代码2 小时前
基于cornerstone3D的dicom影像浏览器 第三十一章 从PACS服务加载图像
javascript·pacs·dicom
贩卖纯净水.3 小时前
webpack其余配置
前端·webpack·node.js
码上奶茶3 小时前
HTML 列表、表格、表单
前端·html·表格·标签·列表·文本·表单