0

0

Three.js 高性能渲染千级 2D 文本标签:实例化几何体与纹理图集实践

DDD

DDD

发布时间:2025-11-26 16:54:01

|

768人浏览过

|

来源于php中文网

原创

Three.js 高性能渲染千级 2D 文本标签:实例化几何体与纹理图集实践

在 three.js 中渲染大量 2d 文本标签常面临性能瓶颈。本教程提供一种高效解决方案,利用实例化几何体(instancedbuffergeometry)显著减少 draw call,并结合纹理图集(texture atlas)将所有文本预渲染至一张纹理,通过着色器在每个实例上选择性采样,从而实现千级以上 2d 文本标签的流畅渲染,同时保持良好的视觉效果和可扩展性。

引言:大规模 2D 文本渲染的挑战

在 Three.js 等 3D 渲染引擎中,当需要显示成百上千个 2D 文本标签时,传统的渲染方法往往会遇到严重的性能问题。常见的尝试包括:

  • THREE.TextGeometry:为每个文本生成独立的 3D 几何体,导致几何体数量庞大,CPU 和 GPU 开销剧增。
  • troika-three-text:虽然提供了更优化的文本渲染,但在处理上千个独立文本时,仍然可能因其内部的批处理和更新机制而面临性能瓶颈。
  • CSS2DRenderer 或 CSS3DRenderer:利用 DOM 元素渲染文本,虽然渲染质量高且易于样式控制,但每个文本对应一个 DOM 元素,大量的 DOM 操作和浏览器布局计算会显著拖慢性能,尤其是在 3D 场景中需要频繁更新位置时。

这些方法在小规模应用中表现良好,但在需要渲染如楼层平面图上千个房间名称等场景时,其性能瓶颈便凸显出来。核心问题在于,每渲染一个文本,都会产生额外的 Draw Call、几何体处理或 DOM 操作开销。

核心方案:实例化几何体与纹理图集

为了高效渲染大量 2D 文本标签,我们可以采用实例化几何体(InstancedBufferGeometry)结合纹理图集(Texture Atlas)的策略。

  1. 实例化几何体(InstancedBufferGeometry)
    • 原理:允许使用一个 Draw Call 渲染多个具有相同几何结构但不同属性(如位置、旋转、颜色、纹理偏移等)的实例。
    • 优势:极大地减少了 Draw Call 数量,从而降低了 CPU 与 GPU 之间的通信开销,显著提升渲染性能。
  2. 纹理图集(Texture Atlas)
    • 原理:将所有需要显示的文本预先绘制到一张大的纹理图像上。每个文本占据图集中的一个特定区域。
    • 优势:避免了为每个文本单独加载和绑定纹理,减少了纹理切换的开销。在着色器中,通过计算每个实例在图集中的 UV 坐标偏移,可以采样到对应的文本图像。

这种组合方案将文本渲染的重担从 CPU 转移到 GPU,利用 GPU 的并行处理能力,实现高性能的大规模文本显示。

实现步骤与代码解析

下面我们将通过一个 Three.js 示例来详细阐述如何实现这一方案。

1. HTML 与 CSS 基础设置

首先,准备一个基本的 HTML 页面和一些 CSS 样式来确保 Three.js 渲染器能正确显示。

人民网AIGC-X
人民网AIGC-X

国内科研机构联合推出的AI生成内容检测工具

下载
<!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>

2. Three.js 场景初始化

导入 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());

3. 动态生成纹理图集

创建一个函数 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,确保文本在每个单元格内居中。

4. 创建实例化几何体

使用 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 场景中的世界坐标。

5. 编写着色器材质

自定义 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; // 每个单元格

相关文章

数码产品性能查询
数码产品性能查询

该软件包括了市面上所有手机CPU,手机跑分情况,电脑CPU,电脑产品信息等等,方便需要大家查阅数码产品最新情况,了解产品特性,能够进行对比选择最具性价比的商品。

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
js正则表达式
js正则表达式

php中文网为大家提供各种js正则表达式语法大全以及各种js正则表达式使用的方法,还有更多js正则表达式的相关文章、相关下载、相关课程,供大家免费下载体验。

530

2023.06.20

js获取当前时间
js获取当前时间

JS全称JavaScript,是一种具有函数优先的轻量级,解释型或即时编译型的编程语言;它是一种属于网络的高级脚本语言,主要用于Web,常用来为网页添加各式各样的动态功能。js怎么获取当前时间呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

576

2023.07.28

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

760

2023.08.03

js是什么意思
js是什么意思

JS是JavaScript的缩写,它是一种广泛应用于网页开发的脚本语言。JavaScript是一种解释性的、基于对象和事件驱动的编程语言,通常用于为网页增加交互性和动态性。它可以在网页上实现复杂的功能和效果,如表单验证、页面元素操作、动画效果、数据交互等。

6206

2023.08.17

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

492

2023.09.01

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

Js中concat和push的区别
Js中concat和push的区别

Js中concat和push的区别:1、concat用于将两个或多个数组合并成一个新数组,并返回这个新数组,而push用于向数组的末尾添加一个或多个元素,并返回修改后的数组的新长度;2、concat不会修改原始数组,是创建新的数组,而push会修改原数组,将新元素添加到原数组的末尾等等。本专题为大家提供concat和push相关的文章、下载、课程内容,供大家免费下载体验。

240

2023.09.14

js截取字符串的方法介绍
js截取字符串的方法介绍

JavaScript字符串截取方法,包括substring、slice、substr、charAt和split方法。这些方法可以根据具体需求,灵活地截取字符串的不同部分。在实际开发中,根据具体情况选择合适的方法进行字符串截取,能够提高代码的效率和可读性 。

303

2023.09.21

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Sass 教程
Sass 教程

共14课时 | 0.9万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

CSS教程
CSS教程

共754课时 | 42.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号