// 首页 3D 粒子地球(banner 右侧) // 改造为:大量粒子从远处飞入,最终聚合成一个球体,并缓慢自转 // 依赖:全局 THREE(在 index.html 中通过 CDN 引入) (function () { function initGlobe() { var container = document.getElementById("bannerGlobe"); if (!container) { console.warn("[Globe] bannerGlobe container not found"); return; } if (typeof THREE === "undefined") { console.warn( "[Globe] THREE is undefined, please ensure three.min.js is loaded" ); return; } var width = container.clientWidth || 400; var height = container.clientHeight || 400; // 基础 Three.js 场景 var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 1000); // 初始视角,稍微偏上俯视球体 camera.position.set(0, 0.8, 7.5); camera.lookAt(0, 0, 0); var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setSize(width, height); renderer.setClearColor(0x000000, 0); // 透明背景 container.appendChild(renderer.domElement); // 柔和光照 var ambient = new THREE.AmbientLight(0xffffff, 0.65); scene.add(ambient); var dirLight = new THREE.DirectionalLight(0xffffff, 0.9); dirLight.position.set(5, 3, 5); scene.add(dirLight); // ------------------------------- // 粒子球体:从远处飞入,最终聚合成球 // ------------------------------- // 可调参数:你可以根据效果需求自行修改 var PARTICLE_COUNT = 800; // 粒子数量,越多越实,但性能负担也越大 var SPHERE_RADIUS = 3.1; // 球体半径(世界单位,调大可让球在竖直方向更“撑满”) var SPHERE_OFFSET_X = 3.5; // 球心在 X 轴上的偏移量,用于将球体放在画布偏右的位置 var START_RADIUS_MIN = 10.0; // 粒子起始距离下限(调大一点,让更多粒子从画布边缘外飞入) var START_RADIUS_MAX = 18.0; // 粒子起始距离上限 var FORMATION_DURATION = 13000; // 粒子从散开到聚合成球的时间(毫秒) var ROTATION_SPEED = 0.25; // 成形后的自转速度(弧度/秒,约等于旧地球的速度) // 准备缓动函数(飞入时用 easeOutCubic,让粒子接近球面时减速) function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } // 准备粒子数据:startPositions / targetPositions / current positions / size var geometry = new THREE.BufferGeometry(); var positions = new Float32Array(PARTICLE_COUNT * 3); var startPositions = new Float32Array(PARTICLE_COUNT * 3); var targetPositions = new Float32Array(PARTICLE_COUNT * 3); var sizes = new Float32Array(PARTICLE_COUNT); // 随机在球壳表面生成点(均匀分布),作为目标位置 function randomPointOnSphere(radius) { // 使用均匀球面采样:theta = 2πu, phi = arccos(2v - 1) var u = Math.random(); var v = Math.random(); var theta = 2 * Math.PI * u; var phi = Math.acos(2 * v - 1); var sinPhi = Math.sin(phi); var x = radius * sinPhi * Math.cos(theta); var y = radius * Math.cos(phi); var z = radius * sinPhi * Math.sin(theta); return { x: x, y: y, z: z }; } function randomPointFarFromCenter(minR, maxR) { var radius = minR + Math.random() * (maxR - minR); var u = Math.random(); var v = Math.random(); var theta = 2 * Math.PI * u; var phi = Math.acos(2 * v - 1); var sinPhi = Math.sin(phi); var x = radius * sinPhi * Math.cos(theta); var y = radius * Math.cos(phi); var z = radius * sinPhi * Math.sin(theta); return { x: x, y: y, z: z }; } for (var i = 0; i < PARTICLE_COUNT; i++) { var i3 = i * 3; var target = randomPointOnSphere(SPHERE_RADIUS); targetPositions[i3] = target.x; targetPositions[i3 + 1] = target.y; targetPositions[i3 + 2] = target.z; var start = randomPointFarFromCenter(START_RADIUS_MIN, START_RADIUS_MAX); startPositions[i3] = start.x; startPositions[i3 + 1] = start.y; startPositions[i3 + 2] = start.z; // 初始位置就是起始位置(粒子还未飞入) positions[i3] = start.x; positions[i3 + 1] = start.y; positions[i3 + 2] = start.z; // 粒子尺寸:每个粒子随机一个基础大小,后续在顶点着色器中再做距离衰减 // 你可以调节下方这两个数,控制整体粒子大小的分布范围 sizes[i] = 1.0 + Math.random() * 1.5; // 1.0 ~ 2.5 之间,避免大颗粒过于“糊” } geometry.setAttribute( "position", new THREE.BufferAttribute(positions, 3) ); geometry.setAttribute( "size", new THREE.BufferAttribute(sizes, 1) ); // 使用自定义着色器,让粒子可以拥有不同大小 var material = new THREE.ShaderMaterial({ uniforms: { uColor: { value: new THREE.Color(0x38bdf8) }, // 粒子主色 uOpacity: { value: 1.0 }, uSize: { value: 16.0 }, // 全局尺寸基准,越大粒子越大 uPixelRatio: { value: Math.min(window.devicePixelRatio || 1, 2) }, }, vertexShader: ` attribute float size; uniform float uSize; uniform float uPixelRatio; void main() { vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * mvPosition; // 简单的距离衰减,让远处粒子稍微小一些 float dist = length(mvPosition.xyz); float att = 1.0 / (0.1 + dist * 0.35); gl_PointSize = size * uSize * uPixelRatio * att; } `, fragmentShader: ` uniform vec3 uColor; uniform float uOpacity; void main() { // 将点渲染成相对清晰的圆形(中心亮,边缘略微柔和) vec2 c = gl_PointCoord - vec2(0.5); float d = length(c); // 0.35 以内基本保持满色,0.35~0.5 做少量抗锯齿过渡 float mask = smoothstep(0.5, 0.35, d); if (mask <= 0.0) discard; float alpha = uOpacity * mask; gl_FragColor = vec4(uColor, alpha); } `, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); var particleSphere = new THREE.Points(geometry, material); scene.add(particleSphere); // 也可以增加一个非常淡的内层球体,进一步强调轮廓(如不需要可注释掉) var innerSphereGeom = new THREE.SphereGeometry(SPHERE_RADIUS * 0.98, 48, 48); var innerSphereMat = new THREE.MeshBasicMaterial({ color: 0x0f172a, transparent: true, opacity: 0.0, // 表面颜色完全透明,只保留粒子视觉 side: THREE.BackSide, }); var innerSphereMesh = new THREE.Mesh(innerSphereGeom, innerSphereMat); scene.add(innerSphereMesh); // 将球体整体向右偏移,让球体位于画布偏右侧,避免挡住左侧文案 particleSphere.position.x = SPHERE_OFFSET_X; innerSphereMesh.position.x = SPHERE_OFFSET_X; // 暴露给调试用 if (typeof window !== "undefined") { window.__particleGlobe = { scene: scene, camera: camera, renderer: renderer, particleSphere: particleSphere, }; } // 自适应窗口大小 function onResize() { if (!container) return; var w = container.clientWidth || width; var h = container.clientHeight || height; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); // 同步像素比,避免在缩放/视网膜屏下粒子尺寸异常 if (material && material.uniforms && material.uniforms.uPixelRatio) { material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 2); } } window.addEventListener("resize", onResize); // 动画循环:粒子从远处飞入,逐渐聚合成球体,并缓慢自转 var startTime = performance.now(); var lastTime = startTime; function animate() { requestAnimationFrame(animate); var now = performance.now(); var delta = (now - lastTime) / 1000; var elapsed = now - startTime; lastTime = now; // 粒子从起始位置插值到球面目标位置 var t = Math.min(1, elapsed / FORMATION_DURATION); var eased = easeOutCubic(t); var i, i3; for (i = 0; i < PARTICLE_COUNT; i++) { i3 = i * 3; var sx = startPositions[i3]; var sy = startPositions[i3 + 1]; var sz = startPositions[i3 + 2]; var tx = targetPositions[i3]; var ty = targetPositions[i3 + 1]; var tz = targetPositions[i3 + 2]; positions[i3] = sx + (tx - sx) * eased; positions[i3 + 1] = sy + (ty - sy) * eased; positions[i3 + 2] = sz + (tz - sz) * eased; } geometry.attributes.position.needsUpdate = true; // 整体自转 particleSphere.rotation.y += ROTATION_SPEED * delta; innerSphereMesh.rotation.y += ROTATION_SPEED * delta * 0.9; renderer.render(scene, camera); } animate(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initGlobe); } else { initGlobe(); } })();