
本文旨在解决 Three.js 中渲染大量 2D 文本标签时遇到的性能瓶颈。通过采用实例几何体(InstancedBufferGeometry)结合纹理图集(Texture Atlas)和自定义着色器(ShaderMaterial)的方法,可以显著提升渲染效率,实现千级甚至更多文本标签的流畅显示,同时保持文本的对齐和可读性。
在 Three.js 应用中,当需要渲染数百甚至数千个 2D 文本标签时,传统的渲染方法如 TextGeometry、troika-three-text 或 CSS2DRenderer 往往会遭遇严重的性能问题,导致帧率骤降。这些方法通常为每个文本标签创建独立的几何体或 DOM 元素,从而增加了大量的绘制调用(draw calls)和 CPU/GPU 开销。为了解决这一挑战,一种高效的策略是利用 WebGL 的实例渲染(Instanced Rendering)能力,结合纹理图集来管理文本内容。
该解决方案的核心思想是将所有文本标签渲染为同一批次的实例平面(instanced planes),并通过一个包含所有文本内容的纹理图集来为这些平面提供纹理。每个实例平面通过其唯一的 gl_InstanceID 从纹理图集中选择并显示对应的文本片段。这种方法将数千个绘制调用合并为少数几个,极大地减轻了渲染负担。
以下是使用 Three.js 实现高性能 2D 文本标签渲染的详细步骤和示例代码。
首先,我们需要一个标准的 Three.js 场景、摄像机、渲染器和轨道控制器。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 高性能2D文本标签</title>
<style>
body {
overflow: hidden;
margin: 0;
}
</style>
</head>
<body>
<script async src="https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js" crossorigin="anonymous"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.158.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.158.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
console.clear();
let scene = new THREE.Scene();
scene.background = new THREE.Color(0xface8d); // 设置背景色
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(3, 5, 8).setLength(40); // 设置摄像机位置
camera.lookAt(scene.position);
let renderer = new THREE.WebGLRenderer({
antialias: true // 开启抗锯齿
});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
// 窗口大小调整事件
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 开启阻尼效果
let light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.setScalar(1);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.5)); // 添加光源
scene.add(new THREE.GridHelper()); // 添加辅助网格getMarkerTexture 函数负责创建一个 Canvas,并将所有文本内容绘制到上面,最终生成一个 THREE.CanvasTexture。
function getMarkerTexture(size, amountW, amountH){
let c = document.createElement("canvas");
c.width = size;
c.height = size;
let ctx = c.getContext("2d");
ctx.fillStyle = "#fff"; // 背景填充白色
ctx.fillRect(0, 0, c.width, c.height);
// 计算每个文本块在图集中的步长
const stepW = c.width / amountW;
const stepH = c.height / amountH;
ctx.font = "bold 40px Arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "#000"; // 文本颜色
let col = new THREE.Color();
let counter = 0;
// 遍历并绘制所有文本到Canvas
for(let y = 0; y < amountH; y++){
for(let x = 0; x < amountW; x++){
let textX = (x + 0.5) * stepW;
let textY = ((amountH - y - 1) + 0.5) * stepH; // 注意y轴方向,Canvas原点在左上角
ctx.fillText(counter.toString(), textX, textY); // 绘制文本
// 绘制边框以区分不同的文本块
ctx.strokeStyle = '#' + col.setHSL(Math.random(), 1, 0.5).getHexString();
ctx.lineWidth = 3;
ctx.strokeRect(x * stepW + 4, y * stepH + 4, stepW - 8, stepH - 8);
counter++;
}
}
let ct = new THREE.CanvasTexture(c);
ct.colorSpace = THREE.SRGBColorSpace; // 设置颜色空间
return ct;
}在这个例子中,amountW 和 amountH 定义了纹理图集中文本块的网格布局(例如 32x64),size 定义了整个纹理图集的边长。counter 用于生成不同的文本内容。
我们创建一个 PlaneGeometry 作为基础,并将其转换为 InstancedBufferGeometry。然后,为每个实例添加一个 instPos 属性来定义其在世界空间中的位置。
let ig = new THREE.InstancedBufferGeometry().copy(new THREE.PlaneGeometry(2, 1)); // 基础平面几何体
ig.instanceCount = Infinity; // 实例数量设置为无限,实际由attribute的长度决定
const amount = 2048; // 文本标签的数量
let instPos = new Float32Array(amount * 3); // 每个实例有3个位置分量 (x, y, z)
for(let i = 0; i < amount; i++){
// 为每个实例生成随机位置
instPos[i * 3 + 0] = THREE.MathUtils.randFloatSpread(50);
instPos[i * 3 + 1] = THREE.MathUtils.randFloatSpread(50);
instPos[i * 3 + 2] = THREE.MathUtils.randFloatSpread(50);
}
// 将位置属性添加到实例几何体
ig.setAttribute("instPos", new THREE.InstancedBufferAttribute(instPos, 3));这是实现纹理图集和实例渲染的关键部分。
let im = new THREE.ShaderMaterial({
uniforms: {
quaternion: {value: new THREE.Quaternion()}, // 用于文本朝向摄像机
markerTexture: {value: getMarkerTexture(4096, 32, 64)}, // 纹理图集
textureDimensions: {value: new THREE.Vector2(32, 64)} // 纹理图集中的文本块网格尺寸
},
vertexShader: `
uniform vec4 quaternion; // 摄像机四元数,用于旋转
uniform vec2 textureDimensions; // 纹理图集网格尺寸 (amountW, amountH)
attribute vec3 instPos; // 实例位置属性
varying vec2 vUv; // 传递给片元着色器的UV坐标
// 四元数旋转函数
vec3 qtransform( vec4 q, vec3 v ){
return v + 2.0*cross(cross(v, q.xyz ) + q.w*v, q.xyz);
}
void main(){
// 将实例位置应用到顶点,并根据摄像机四元数旋转,实现billboard效果
vec3 pos = qtransform(quaternion, position) + instPos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.);
// 根据gl_InstanceID计算在纹理图集中的UV偏移
float iID = float(gl_InstanceID);
float stepW = 1. / textureDimensions.x; // 每个文本块的UV宽度
float stepH = 1. / textureDimensions.y; // 每个文本块的UV高度
float uvX = mod(iID, textureDimensions.x); // 当前实例在图集中的X索引
float uvY = floor(iID / textureDimensions.x); // 当前实例在图集中的Y索引
// 计算最终的UV坐标,uv是原始平面几何体的UV (0-1)
vUv = (vec2(uvX, uvY) + uv) * vec2(stepW, stepH);
}
`,
fragmentShader: `
uniform sampler2D markerTexture; // 纹理图集
varying vec2 vUv; // 从顶点着色器接收的UV坐标
void main(){
vec4 col = texture(markerTexture, vUv); // 从纹理图集采样颜色
gl_FragColor = vec4(col.rgb, 1); // 输出颜色
}
`
});
let io = new THREE.Mesh(ig, im); // 创建实例网格
scene.add(io); // 添加到场景在动画循环中,我们需要更新轨道控制器,并确保文本标签始终面向摄像机。这通过将摄像机的逆四元数传递给着色器实现。
let clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
controls.update(); // 更新轨道控制器
// 将摄像机的逆四元数传递给着色器,使平面始终面向摄像机
im.uniforms.quaternion.value.copy(camera.quaternion).invert();
renderer.render(scene, camera); // 渲染场景
});
</script>
</body>
</html>通过 InstancedBufferGeometry 和 ShaderMaterial 结合纹理图集,我们能够高效地在 Three.js 中渲染大量的 2D 文本标签。这种方法将渲染性能瓶颈从每个文本标签的独立绘制调用转移到一次性生成纹理图集和一次性绘制所有实例上,从而在保持良好视觉效果的同时,实现了卓越的性能。在开发需要展示大量信息(如地图标注、楼层平面图中的房间名称等)的 3D 应用时,这种技术是不可或缺的。
以上就是Three.js 高性能渲染千级以上 2D 文本标签:实例与纹理图集深度教程的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号