用Three.js实现高性能3D场景渲染的实战经验分享
先看效果,再看代码
我上周给一个数据看板加了个 3D 地球仪,需求就一句:“能转、能点、能显示热点城市”。没说用 Three.js,但打开 Figma 稿子一看——带光照、带纹理、带 hover 缩放动画,纯 CSS 3D 撑不住,WebGL 是绕不过去了。于是翻文档、查示例、删了三遍 node_modules,最后跑通的代码其实就 120 行核心逻辑。下面直接贴我最终留下的、亲测在 Chrome/Firefox/Safari(Mac)都稳的版本。
最简地球:Canvas + SphereGeometry + Texture
别一上来就搞 OrbitControls 或 GLTF 加载器,先让球转起来。我试过用 CSS3DRenderer,结果 iOS 上 touchmove 卡成 PPT;也试过 CSS2DRenderer 做标签,但缩放时文字糊得根本没法读。最终还是老老实实走 WebGL 路线。
关键点:纹理图我用的是 NASA 公开的 Blue Marble(2048×1024),直接丢进 TextureLoader 就行,不用自己写 shader。注意一点:Three.js 默认把纹理 UV 坐标从左上角开始算,而 NASA 图是“地理坐标系”(赤道居中),所以必须手动翻转 Y:
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('https://jztheme.com/assets/earth.jpg', () => {
texture.colorSpace = THREE.SRGBColorSpace;
texture.flipY = false; // 关键!NASA 图是正向 Y,不翻
});
然后建球:
const geometry = new THREE.SphereGeometry(1, 64, 64);
const material = new THREE.MeshPhongMaterial({
map: texture,
specular: new THREE.Color(0x333333),
shininess: 5
});
const earth = new THREE.Mesh(geometry, material);
scene.add(earth);
这里注意下,我踩过坑:SphereGeometry 的 widthSegments 和 heightSegments 别设太低(比如 16),不然旋转时能看到明显的多边形锯齿;也别设太高(比如 256),否则低端安卓机直接卡死。64 是目前我压测下来最平衡的值。
让它动起来:requestAnimationFrame + 自旋 + 鼠标拖拽
自旋很简单:
function animate() {
requestAnimationFrame(animate);
earth.rotation.y += 0.001;
renderer.render(scene, camera);
}
animate();
但用户要拖拽呢?别急着抄官方 OrbitControls 示例。我一开始用了它,结果发现两个问题:
- 双指缩放会触发
touchstart→touchmove→touchend,但OrbitControls在移动端对touchmove的节流太激进,导致缩放延迟半秒 - 它默认开启 damping(阻尼),每次拖完还会晃两下,产品经理说“像喝醉了”,立马禁用
所以我换成了手写控制逻辑,核心就三件事:记录起始位置、计算 delta、应用 rotation。代码比 OrbitControls 少一半,还干净:
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
renderer.domElement.addEventListener('mousedown', (e) => {
isDragging = true;
previousMousePosition = { x: e.clientX, y: e.clientY };
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
const deltaMove = {
x: e.clientX - previousMousePosition.x,
y: e.clientY - previousMousePosition.y
};
earth.rotation.y += deltaMove.x * 0.01;
earth.rotation.x += deltaMove.y * 0.01;
previousMousePosition = { x: e.clientX, y: e.clientY };
}
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
Touch 版本同理,只是监听 touchstart/touchmove/touchend,记得 e.preventDefault() 防止页面滚动。这个方案亲测有效,iOS Safari、Android Chrome 都丝滑,而且没任何第三方依赖。
点击城市:射线检测(Raycaster)别漏这三步
需求里“能点”是指点击城市弹出 info card。Three.js 的 Raycaster 是标准解法,但我第一次写的时候漏了三步,折腾半天没反应:
- 没调
raycaster.setFromCamera(mouse, camera)—— 错把屏幕坐标当世界坐标传了 - 没把可点击对象加进
objects数组 —— 只 push 了 earth,忘了城市 marker 是单独的Mesh - 没处理
event.clientX/Y到标准化设备坐标(NDC)的转换 —— 忘了除以 canvas 宽高再乘 2 减 1
正确姿势(简化版):
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(cityMarkers); // cityMarkers 是所有城市 Mesh 数组
if (intersects.length > 0) {
const city = intersects[0].object.userData.city;
showCityInfo(city); // 自定义弹窗
}
});
这里有个小技巧:cityMarkers 不要用 Sprite,改用带 userData 的 Mesh(哪怕只是个 PlaneGeometry),因为 Sprite 的 raycast 默认是关的,得手动 sprite.material.depthTest = true,容易漏。
踩坑提醒:这三点一定注意
- Canvas 尺寸别用 CSS 缩放:
<canvas style="width:100%; height:400px">看起来没问题,但 Three.js 渲染分辨率会按原始 size 算,导致模糊。必须同步设置canvas.width/canvas.height,或者用renderer.setSize(width, height)动态重设 - Light 一定要加:新手常忘加光源,结果球是黑的。
AmbientLight+DirectionalLight是最低配组合,别省 - dispose() 别偷懒:页面切换时,记得
texture.dispose()、geometry.dispose()、material.dispose()。我上次没清 geometry,内存涨到 800MB,用户切页卡死,被 QA 追着骂了半小时
这个场景最好用:动态热点 + 实时数据驱动
我们后来加了实时航班热力图,不是静态贴图,而是每 5 秒 fetch 一次 API:
async function updateHeatmap() {
const res = await fetch('https://jztheme.com/api/flights');
const data = await res.json();
// 把经纬度转成球面坐标,生成新 points
const points = data.map(d => latLngToVector3(d.lat, d.lng, 1.02));
// 用 BufferGeometry + PointsMaterial 替换旧点集
}
这种动态更新,PointsMaterial 比 Mesh 性能高得多,2000 个点也不卡。原理就是把每个点当成一个像素,GPU 统一批处理,而不是渲染 2000 个独立 mesh。细节不多讲,后续单开一篇聊 buffer geometry 批量更新技巧。
以上是我踩坑后的总结,希望对你有帮助
这个技术的拓展用法还有很多,比如用 ShaderMaterial 实现大气辉光、用 GLTF 加载真实城市建筑模型、甚至接 WebRTC 做多人协同标注——这些我都在项目里试过,有些跑通了,有些还在调兼容性。后续会继续分享这类博客,重点讲“什么能用、什么不能用、为什么不能用”。
如果你也遇到 Three.js 的奇怪白屏、黑球、点击无响应,欢迎评论区交流。我大概率也踩过——毕竟,谁还没被 THREE.WebGLRenderer: Context lost. 折磨过呢。

暂无评论