如何实现Piano.io网站中WebGL地球的卡片显示与脉冲动画效果?
Piano.io地球动画核心实现方案
一、点位标记与卡片显示
实现思路
- 标记点批量渲染:用Three.js的
InstancedMesh批量处理大量点位标记,兼顾性能;通过GLSL实现广告牌效果,让标记点始终面向相机。 - 卡片交互:借助Three.js射线拾取获取选中点位,在React中渲染对应DOM卡片,可通过
CSS3DRenderer让卡片贴合3D空间位置,或直接计算屏幕坐标定位DOM元素。
最简GLSL标记点着色器
顶点着色器
attribute vec3 offset; // 每个标记点的3D位置偏移 attribute float size; // 标记点基础大小 uniform float uTime; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; void main() { // 广告牌效果:让标记点始终朝向相机 vec3 cameraPos = uViewMatrix[3].xyz; vec3 lookDir = normalize(cameraPos - offset); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = normalize(cross(lookDir, up)); up = normalize(cross(right, lookDir)); vec3 pos = offset + (right * position.x + up * position.y) * size; gl_Position = uProjectionMatrix * uViewMatrix * vec4(pos, 1.0); }
片元着色器
precision mediump float; uniform vec3 uColor; void main() { // 绘制圆形标记点 float dist = distance(gl_PointCoord, vec2(0.5)); if (dist > 0.5) discard; gl_FragColor = vec4(uColor, 1.0); }
React+TypeScript 核心代码片段
import * as THREE from 'three'; import { useRef, useEffect } from 'react'; const EarthMarkers = () => { const sceneRef = useRef<THREE.Scene>(new THREE.Scene()); const instancedMeshRef = useRef<THREE.InstancedMesh | null>(null); const cameraRef = useRef<THREE.PerspectiveCamera>( new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) ); useEffect(() => { // 模拟点位3D坐标数据 const positions = [ new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1) ]; const geometry = new THREE.PlaneGeometry(1, 1); const material = new THREE.ShaderMaterial({ vertexShader: /* 上述顶点着色器代码 */, fragmentShader: /* 上述片元着色器代码 */, uniforms: { uColor: { value: new THREE.Color(0xff4444) }, uTime: { value: 0 } }, transparent: true }); // 创建实例化网格 instancedMeshRef.current = new THREE.InstancedMesh(geometry, material, positions.length); sceneRef.current.add(instancedMeshRef.current); // 设置每个标记点的位置 positions.forEach((pos, index) => { const matrix = new THREE.Matrix4().setPosition(pos); instancedMeshRef.current!.setMatrixAt(index, matrix); }); // 射线拾取监听(简化版) const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); window.addEventListener('click', (e) => { mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, cameraRef.current); const intersects = raycaster.intersectObject(instancedMeshRef.current!); if (intersects.length > 0) { const instanceIndex = intersects[0].instanceId; // 触发React状态更新,显示对应卡片 console.log('选中点位索引:', instanceIndex); } }); // 动画循环 const animate = () => { requestAnimationFrame(animate); instancedMeshRef.current!.material.uniforms.uTime.value += 0.01; }; animate(); }, []); return <div ref={el => el && new THREE.WebGLRenderer({ antialias: true }).setSize(window.innerWidth, window.innerHeight)} />; }; export default EarthMarkers;
二、脉冲式动画效果
实现思路
通过GLSL的时间变量,结合三角函数(sin/cos)计算脉冲强度,控制标记点的缩放、透明度或颜色变化,模拟类似地震波的扩散效果。
最简GLSL脉冲动画代码(集成到标记点着色器)
修改后的顶点着色器(添加脉冲缩放)
attribute vec3 offset; attribute float size; uniform float uTime; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; void main() { // 脉冲缩放因子:基于时间和点位距离的波动 float pulse = 1.0 + sin(uTime * 5.0 + length(offset)) * 0.3; float scaledSize = size * pulse; // 广告牌逻辑 vec3 cameraPos = uViewMatrix[3].xyz; vec3 lookDir = normalize(cameraPos - offset); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = normalize(cross(lookDir, up)); up = normalize(cross(right, lookDir)); vec3 pos = offset + (right * position.x + up * position.y) * scaledSize; gl_Position = uProjectionMatrix * uViewMatrix * vec4(pos, 1.0); }
片元着色器(添加脉冲透明度)
precision mediump float; uniform vec3 uColor; uniform float uTime; attribute vec3 offset; varying float vPulse; void main() { // 脉冲透明度计算 vPulse = 0.5 + 0.5 * sin(uTime * 5.0 + length(offset)); float dist = distance(gl_PointCoord, vec2(0.5)); if (dist > 0.5) discard; gl_FragColor = vec4(uColor, vPulse); }
React+TypeScript 动画驱动
在动画循环中持续更新时间变量即可:
const animate = () => { requestAnimationFrame(animate); const time = performance.now() * 0.001; instancedMeshRef.current!.material.uniforms.uTime.value = time; };
内容的提问来源于stack exchange,提问作者husky
相关产品推荐
相关产品推荐

