
本文旨在解决 three.js 中渲染上千个 2d 文本标签时遇到的性能瓶颈。通过深入探讨传统的 textgeometry、troika-three-text 和 css2drenderer 等方法的局限性,提出并详细阐述了使用 `instancedbuffergeometry` 结合纹理图集(texture atlas)的优化方案。该方案能够显著减少绘制调用,大幅提升渲染效率,为大规模场景下的文本显示提供了高效且专业的解决方案。
在 Three.js 应用中,当需要渲染大量 2D 文本标签时(例如,在楼层平面图上显示房间名称或区域信息,数量可能达到上千个甚至更多),传统的渲染方法往往会遭遇严重的性能问题。诸如 TextGeometry 生成复杂几何体、troika-three-text 虽有优化但仍需独立处理每个文本,以及 CSS2dRenderer 依赖 DOM 元素进行渲染,这些方法在处理海量文本时都会导致过高的绘制调用(draw calls)和浏览器性能负担,从而造成帧率骤降。
为了克服这一挑战,一个高效的解决方案是结合使用 Three.js 的实例化渲染(Instancing)和纹理图集(Texture Atlas)技术。
该优化方案的核心思想是:
这种方法将原本多个独立的绘制调用合并为极少数甚至一个绘制调用,极大地减轻了 GPU 的负担,从而实现高性能渲染。
以下将通过一个完整的 Three.js 示例代码,详细讲解如何实现这一高性能 2D 文本标签渲染方案。
首先,确保页面的 body 元素没有默认的边距和溢出,以便 Three.js 渲染器能够全屏显示。
<!DOCTYPE html>
<html>
<head>
<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.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module" src="main.js"></script> <!-- 你的 Three.js 代码将在这里 -->
</body>
</html>将以下 Three.js 代码保存到 main.js 文件中。
设置 Three.js 场景、相机、渲染器、轨道控制器和光源。
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", (event) => {
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()); // 添加网格辅助线这是关键一步,我们需要创建一个 Canvas 元素,将所有文本绘制到这个画布上,然后将其转换为 THREE.CanvasTexture。
/**
* 生成包含所有文本标签的纹理图集
* @param {number} size 纹理图集的边长(正方形)
* @param {number} amountW 纹理图集横向文本数量
* @param {number} amountH 纹理图集纵向文本数量
* @returns {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;
// 遍历并绘制每个文本标签
for (let y = 0; y < amountH; y++) {
for (let x = 0; x < amountW; x++) {
// 计算文本绘制中心点
let textX = (x + 0.5) * stepW;
// 注意:Canvas的Y轴方向与Three.js UV坐标可能相反,这里调整了Y轴计算
let textY = ((amountH - y - 1) + 0.5) * stepH;
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;
}在 getMarkerTexture 函数中,我们将文本内容(这里是递增的数字)绘制到画布上,并为每个文本单元绘制了一个彩色边框,以便于调试和观察纹理图集。
实例化渲染的核心是 InstancedBufferGeometry 和自定义的 ShaderMaterial。
// 创建实例化几何体:基于 PlaneGeometry
let ig = new THREE.InstancedBufferGeometry().copy(new THREE.PlaneGeometry(2, 1)); // 每个平面大小为 2x1
ig.instanceCount = Infinity; // 实例数量可以无限,实际由 instPos 决定
const amount = 2048; // 实例数量
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));
// 创建着色器材质
let im = new THREE.ShaderMaterial({
uniforms: {
quaternion: { value: new THREE.Quaternion() }, // 用于文本朝向相机的四元数
markerTexture: { value: getMarkerTexture(4096, 32, 64) }, // 纹理图集,这里假设 4096x4096 大小,包含 32x64 个文本
textureDimensions: { value: new THREE.Vector2(32, 64) } // 纹理图集的布局尺寸
},
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(){
// 旋转顶点,使其始终面向相机(billboarding效果),然后加上实例位置
vec3 pos = qtransform(quaternion, position) + instPos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.);
// 根据实例ID计算在纹理图集中的 UV 偏移
float iID = float(gl_InstanceID); // 当前实例的 ID
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); // 输出颜色
}
`
});
let io = new THREE.Mesh(ig, im); // 创建实例化网格
scene.add(io); // 添加到场景着色器代码解析:
在动画循环中,更新轨道控制器,并确保文本始终面向相机。
let clock = new THREE.Clock();
renderer.setAnimationLoop((_) => {
let t = clock.getElapsedTime();
controls.update(); // 更新轨道控制器
// 使文本平面始终面向相机
im.uniforms.quaternion.value.copy(camera.quaternion).invert();
renderer.render(scene, camera); // 渲染场景
});im.uniforms.quaternion.value.copy(camera.quaternion).invert(); 这一行是实现文本平面“billboarding”(始终面向相机)效果的关键。它将相机的旋转四元数取反,然后应用到每个实例的顶点上,使得每个平面在本地坐标系中被旋转,从而在世界坐标系中看起来始终正对着相机。
通过结合 Three.js 的实例化渲染和纹理图集技术,我们可以高效地在 3D 场景中渲染大量 2D 文本标签。这种方法通过减少绘制调用、优化数据传输,显著提升了大规模场景的渲染性能,为开发者提供了强大的工具来构建复杂且流畅的交互式 3D 应用。虽然存在一些动态更新和复杂文本处理的考量,但对于大多数需要显示大量静态或半静态文本标签的场景,这无疑是一个卓越的解决方案。
以上就是Three.js 高性能渲染大量 2D 文本标签:使用实例化与纹理图集优化的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号