Three.js 实战:使用 DOM/CSS 打造高性能 3D 文字
2026年1月20日 22:21
Three.js 实战:使用 DOM/CSS 打造高性能 3D 文字
在 Three.js 中渲染文字有多种方案,本文介绍一种高性能且灵活的方案:CSS2DRenderer。它能将 DOM 元素无缝嵌入 3D 场景,同时保留 CSS 的全部能力。
为什么选择 DOM/CSS 方案?
| 方案 | 优点 | 缺点 |
|---|---|---|
| TextGeometry | 真正的 3D 几何体 | 性能开销大,需加载字体文件 |
| CSS2DRenderer | 清晰锐利、CSS 全特性、高性能 | 无法被 3D 物体遮挡 |
CSS2DRenderer 的核心优势:
- 文字永远清晰:浏览器原生渲染,不受 3D 缩放影响
-
CSS 全特性:阴影、渐变、动画、
backdrop-filter磨砂玻璃效果 - 性能优异:DOM 渲染与 WebGL 渲染分离,互不干扰
核心实现
1. 初始化双渲染器
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
// WebGL 渲染器(渲染 3D 场景)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
// CSS2D 渲染器(渲染 DOM 标签)
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
Object.assign(labelRenderer.domElement.style, {
position: 'absolute',
top: '0',
pointerEvents: 'none' // 关键:让鼠标事件穿透
});
document.body.appendChild(labelRenderer.domElement);
关键点:pointerEvents: 'none' 让鼠标事件穿透 DOM 层,否则无法拖拽 3D 场景。
2. 创建 CSS2D 标签
const createLabel = (text, position) => {
const div = document.createElement('div');
div.className = 'label';
div.textContent = text;
const label = new CSS2DObject(div);
label.position.copy(position);
return label;
};
// 将标签添加到 3D 物体上
earth.add(createLabel('Earth', new THREE.Vector3(0, 1.5, 0)));
标签添加到 earth 网格后,会自动跟随地球的旋转和位移。
3. 双渲染器同步渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera); // 渲染 3D 场景
labelRenderer.render(scene, camera); // 渲染 DOM 标签
}
4. CSS 样式
.label {
color: #FFF;
font-family: 'Helvetica Neue', sans-serif;
font-weight: bold;
padding: 5px 10px;
background: rgba(0, 0, 0, 0.6);
border-radius: 4px;
backdrop-filter: blur(4px); /* 磨砂玻璃效果 */
}
backdrop-filter: blur() 实现的磨砂玻璃效果,在纯 WebGL 中需要复杂的后处理才能实现,而 CSS 一行代码搞定。
完整代码
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
// 场景、相机
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.z = 5;
// WebGL 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);
// CSS2D 渲染器
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
Object.assign(labelRenderer.domElement.style, {
position: 'absolute',
top: '0',
pointerEvents: 'none'
});
document.body.appendChild(labelRenderer.domElement);
// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 创建地球
const earth = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 32),
new THREE.MeshStandardMaterial({ color: 0x2233ff, roughness: 0.5 })
);
scene.add(earth);
// 生成地球纹理
const canvas = Object.assign(document.createElement('canvas'), { width: 512, height: 512 });
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#1e90ff';
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = '#228b22';
for (let i = 0; i < 20; i++) {
ctx.beginPath();
ctx.arc(Math.random() * 512, Math.random() * 512, Math.random() * 50 + 20, 0, Math.PI * 2);
ctx.fill();
}
earth.material.map = new THREE.CanvasTexture(canvas);
// 创建标签工厂函数
const createLabel = (text, position) => {
const div = document.createElement('div');
div.className = 'label';
div.textContent = text;
const label = new CSS2DObject(div);
label.position.copy(position);
return label;
};
earth.add(createLabel('Earth', new THREE.Vector3(0, 1.5, 0)));
// 光源
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 3, 5);
scene.add(dirLight);
// 动画循环
(function animate() {
requestAnimationFrame(animate);
earth.rotation.y += 0.005;
controls.update();
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
})();
// 响应窗口变化
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
labelRenderer.setSize(innerWidth, innerHeight);
});
📂 核心代码与完整示例: my-three-app
总结
如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货