
在 three.js 中渲染大量 2d 文本标签常面临性能瓶颈。本教程提供一种高效解决方案,利用实例化几何体(instancedbuffergeometry)显著减少 draw call,并结合纹理图集(texture atlas)将所有文本预渲染至一张纹理,通过着色器在每个实例上选择性采样,从而实现千级以上 2d 文本标签的流畅渲染,同时保持良好的视觉效果和可扩展性。
在 Three.js 等 3D 渲染引擎中,当需要显示成百上千个 2D 文本标签时,传统的渲染方法往往会遇到严重的性能问题。常见的尝试包括:
这些方法在小规模应用中表现良好,但在需要渲染如楼层平面图上千个房间名称等场景时,其性能瓶颈便凸显出来。核心问题在于,每渲染一个文本,都会产生额外的 Draw Call、几何体处理或 DOM 操作开销。
为了高效渲染大量 2D 文本标签,我们可以采用实例化几何体(InstancedBufferGeometry)结合纹理图集(Texture Atlas)的策略。
这种组合方案将文本渲染的重担从 CPU 转移到 GPU,利用 GPU 的并行处理能力,实现高性能的大规模文本显示。
下面我们将通过一个 Three.js 示例来详细阐述如何实现这一方案。
首先,准备一个基本的 HTML 页面和一些 CSS 样式来确保 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 type="module">
// Three.js 核心代码将在此处
</script>
</body>
</html>导入 Three.js 模块,并设置基础的场景、相机、渲染器和轨道控制器。
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// 场景、相机、渲染器设置
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,它使用 HTML Canvas 动态生成一张包含所有文本的纹理图集。
/**
* 生成包含多个文本的纹理图集
* @param {number} size 纹理图集的边长(例如 4096)
* @param {number} amountW 图集宽度方向上的文本数量
* @param {number} amountH 图集高度方向上的文本数量
* @returns {THREE.CanvasTexture} 生成的 Three.js 纹理
*/
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++) {
// 计算文本绘制中心点
// 注意:y轴方向的计算 (amountH - y - 1) 是为了匹配Three.js纹理的UV坐标系
// Three.js的UV坐标原点在左下角,Canvas的Y轴向下
let textX = (x + 0.5) * stepW;
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++;
}
}
// 创建 Three.js 纹理
let ct = new THREE.CanvasTexture(c);
ct.colorSpace = THREE.SRGBColorSpace; // 设置颜色空间
return ct;
}此函数创建了一个 Canvas,将多个文本(在此示例中是数字)绘制到其不同的子区域中。amountW 和 amountH 定义了图集网格的尺寸,stepW 和 stepH 定义了每个文本单元的大小。通过调整 textX 和 textY,确保文本在每个单元格内居中。
使用 THREE.InstancedBufferGeometry 来创建大量的平面,每个平面将显示一个文本标签。
// 创建一个 PlaneGeometry 作为实例的原型
let ig = new THREE.InstancedBufferGeometry().copy(new THREE.PlaneGeometry(2, 1));
ig.instanceCount = Infinity; // 设置实例数量为无限,或指定具体数量
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));这里我们创建了一个 PlaneGeometry 作为每个文本标签的显示面。InstancedBufferGeometry 复制了这个平面几何体,并通过 setAttribute 添加了一个名为 instPos 的实例化属性,用于存储每个文本标签在 3D 场景中的世界坐标。
自定义 THREE.ShaderMaterial 来利用实例化属性和纹理图集。
let im = new THREE.ShaderMaterial({
uniforms: {
quaternion: {value: new THREE.Quaternion()}, // 用于使文本面向相机
markerTexture: {value: getMarkerTexture(4096, 32, 64)}, // 纹理图集
textureDimensions: {value: new THREE.Vector2(32, 64)} // 图集网格尺寸 (amountW, amountH)
},
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(){
// 将平面顶点旋转以面向相机,然后平移到实例位置
vec3 pos = qtransform(quaternion, position) + instPos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.);
// 根据 gl_InstanceID 计算当前实例在纹理图集中的 UV 偏移
float iID = float(gl_InstanceID); // 当前实例的 ID
float stepW = 1. / textureDimensions.x; // 每个单元格以上就是Three.js 高性能渲染千级 2D 文本标签:实例化几何体与纹理图集实践的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号