
本文针对three.js中渲染大量2d文本标签时遇到的性能瓶颈,提出了一种高效的解决方案。通过结合instancedbuffergeometry和纹理图集技术,可以在场景中流畅地显示千级甚至更多带有文本的2d平面,同时实现文本裁剪效果,显著提升渲染性能,避免传统方法的卡顿问题。
理解传统方法的性能瓶颈
在Three.js应用中,当需要渲染数百甚至数千个2D文本标签时,常见的TextGeometry、troika-three-text或CSS2dRenderer等方法往往会遇到严重的性能问题,导致帧率下降和用户体验不佳。这主要是因为:
- TextGeometry: 为每个文本生成复杂的几何体,增加了顶点数量和渲染开销。
- troika-three-text: 虽然优化了文本渲染,但每个文本仍然可能是一个独立的网格,导致大量的绘制调用(Draw Call)。
- CSS2dRenderer: 使用DOM元素进行渲染,浏览器在处理大量DOM元素时性能会急剧下降,且与WebGL场景的深度排序和裁剪集成较为复杂。
这些方法在元素数量较少时表现良好,但面对千级规模的文本标签时,其为每个元素独立进行的计算、绘制调用或DOM操作会迅速累积,成为性能瓶颈。
核心优化策略:实例化与纹理图集
为了高效渲染大量2D文本标签,我们可以采用实例化(Instancing)和纹理图集(Texture Atlas)相结合的策略。
实例化(Instancing): THREE.InstancedBufferGeometry允许我们使用一个几何体定义来渲染多个对象。所有实例共享相同的几何体数据,但可以通过额外的实例属性(如位置、旋转、颜色、纹理偏移等)来区分它们。这极大地减少了CPU到GPU的数据传输量和GPU的绘制调用次数,因为所有实例都在一次绘制调用中完成渲染。
纹理图集(Texture Atlas): 纹理图集是将多个小纹理(例如本例中的不同文本标签)打包到一个大纹理中。通过在着色器中计算每个实例的UV坐标偏移,我们可以从同一个大纹理中选择性地渲染不同的子区域。这减少了纹理切换的开销,进一步优化了渲染性能。
结合这两种技术,我们可以创建一个单一的实例化网格,其几何体是一个简单的PlaneGeometry,而每个平面上的文本则通过从预先生成的纹理图集中采样来显示。
实现步骤
以下是使用实例化和纹理图集在Three.js中高性能渲染大量2D文本标签的详细步骤。
1. 环境准备
首先,确保你的HTML文件中包含Three.js库的引入,并设置好基本的场景、相机、渲染器和控制器。
Three.js 高性能2D文本标签渲染
2. 创建纹理图集
使用HTML canvas 元素在JavaScript中动态生成包含所有文本标签的纹理图集。每个文本标签将被绘制到图集的一个小区域内。
// ... (接上文代码) ...
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; // 用于生成文本内容
// 在纹理图集上绘制所有文本和边框
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轴方向可能需要翻转
// 绘制文本
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++;
}
}
// 创建Three.js纹理
let ct = new THREE.CanvasTexture(c);
ct.colorSpace = THREE.SRGBColorSpace; // 设置颜色空间
return ct;
}- size: 纹理图集的总尺寸(例如4096x4096)。
- amountW, amountH: 图集横向和纵向可以容纳的文本数量。例如,如果 size 是4096,amountW 是32,那么每个文本的宽度区域是 4096/32 = 128 像素。
- ctx.fillText(counter.toString(), textX, textY): 绘制文本内容。
- ctx.strokeRect: 可选地为每个文本区域绘制一个边框,有助于调试和可视化。
- THREE.CanvasTexture: 将生成的canvas转换为Three.js纹理。
3. 构建实例化几何体和材质
使用InstancedBufferGeometry作为基础几何体,并创建一个ShaderMaterial来处理实例属性和纹理图集采样。
// ... (接上文代码) ...
// 创建实例化几何体,基于一个简单的平面
let ig = new THREE.InstancedBufferGeometry().copy(new THREE.PlaneGeometry(2, 1));
const amount = 2048; // 需要渲染的实例数量
ig.instanceCount = amount; // 设置实例数量
// 为每个实例添加位置属性
let instPos = new Float32Array(amount * 3);
for (let i = 0; i < amount; i++) {
instPos[i * 3 + 0] = THREE.MathUtils.randFloatSpread(50); // X
instPos[i * 3 + 1] = THREE.MathUtils.randFloatSpread(50); // Y
instPos[i * 3 + 2] = THREE.MathUtils.randFloatSpread(50); // Z
}
ig.setAttribute("instPos", new THREE.InstancedBufferAttribute(instPos, 3));
// 获取纹理图集
const textureAtlasSize = 4096;
const textureAtlasAmountW = 32;
const textureAtlasAmountH = 64; // 32 * 64 = 2048,正好对应实例数量
let markerTexture = getMarkerTexture(textureAtlasSize, textureAtlasAmountW, textureAtlasAmountH);
// 创建着色器材质
let im = new THREE.ShaderMaterial({
uniforms: {
quaternion: { value: new THREE.Quaternion() }, // 用于Billboard效果
markerTexture: { value: markerTexture },
textureDimensions: { value: new THREE.Vector2(textureAtlasAmountW, textureAtlasAmountH) }
},
vertexShader: `
uniform vec4 quaternion; // 相机四元数的逆,用于使平面始终面向相机
uniform vec2 textureDimensions; // 纹理图集中的单元格数量 (宽, 高)
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; // 每个单元格在U方向的归一化宽度
float stepH = 1. / textureDimensions.y; // 每个单元格在V方向的归一化高度
float uvX = mod(iID, textureDimensions.x); // 当前实例在图集中的列索引
float uvY = floor(iID / textureDimensions.x); // 当前实例在图集中的行索引
// 计算最终的UV坐标,将几何体的UV映射到图集中的特定单元格
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); // 输出颜色,这里假设文本背景是白色,文本是黑色,所以直接用RGB
}
`
});
// 创建实例化网格并添加到场景
let io = new THREE.Mesh(ig, im);
scene.add(io);- InstancedBufferGeometry().copy(new THREE.PlaneGeometry(2, 1)): 使用PlaneGeometry作为每个实例的基础形状。
- ig.setAttribute("instPos", new THREE.InstancedBufferAttribute(instPos, 3)): 添加一个名为instPos的实例属性,它包含每个实例的3D位置。
- ShaderMaterial:
- uniforms: 传递全局数据到着色器,如纹理图集、图集尺寸和用于billboard效果的相机四元数。
- vertexShader:
- attribute vec3 instPos: 接收每个实例的位置。
- qtransform: 这个函数将几何体的顶点(position)根据相机的四元数进行旋转,实现平面始终面向相机的Billboard效果。
- gl_InstanceID: WebGL内置变量,表示当前正在渲染的实例的ID。
- uvX, uvY: 根据gl_InstanceID计算出当前实例在纹理图集中的行和列索引。
- vUv = (vec2(uvX, uvY) + uv) * vec2(stepW, stepH): 关键步骤,将原始几何体的UV坐标(uv)缩放到纹理图集中的一个单元格内,并偏移到正确的位置。
- fragmentShader:
- uniform sampler2D markerTexture: 接收纹理图集。
- texture(markerTexture, vUv): 使用计算出的UV坐标从纹理图集采样颜色。
注意事项与扩展
文本裁剪(Overflow Hidden): 这种方法天然地实现了文本的裁剪效果。因为文本是绘制在纹理图集中的一个固定大小区域内,然后映射到一个固定大小的PlaneGeometry上。如果文本内容超出了这个区域,它在纹理上就会被裁剪掉,进而显示在平面上时也会被裁剪。你可以通过调整PlaneGeometry的尺寸和getMarkerTexture中stepW/stepH来控制文本区域的大小。
文本质量与图集分辨率: 纹理图集的分辨率(size)和每个文本单元格的大小(stepW, stepH)直接影响文本的清晰度。如果文本过小或图集分辨率不足,文本可能会模糊。需要根据实际需求和性能预算进行权衡。
动态文本更新: 如果文本内容需要频繁动态更新,此方法会比较复杂。每次文本内容变化可能需要重新生成部分或整个纹理图集,这会带来一定的CPU和GPU开销。对于不经常变化的文本,此方法非常高效。
Billboard效果: 顶点着色器中的qtransform函数确保了每个2D文本平面始终面向相机,无论相机如何移动,文本都不会出现透视变形,保持良好的可读性。
其他实例属性: 除了位置,你还可以为每个实例添加更多属性,如颜色、旋转、缩放等,通过InstancedBufferAttribute传递到着色器,实现更丰富的效果。
总结
通过将Three.js的实例化渲染与纹理图集技术相结合,我们能够以极高的性能渲染成千上万个2D文本标签。这种方法通过减少绘制调用和优化纹理访问,有效解决了传统方法在处理大量元素时的性能瓶颈。它不仅提供了流畅的渲染体验,还自然地实现了文本裁剪和Billboard等实用效果,是构建复杂三维场景中大量2D信息展示的理想选择。











