You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化Three.js中CanvasTexture批量创建文本标签的性能?

如何让复用单个Canvas的Three.js文本标签方案正常工作?

我需要通过Three.js的CanvasTexture创建数千个文本标签,目前有两种实现方案:

  • 方案A:为每个标签创建独立Canvas,能正确显示所有标签,但创建数千个时性能较差;
  • 方案B:仅创建一个Canvas并复用(将var ctx = document.createElement('canvas').getContext('2d')置于循环外),性能更优,但所有标签显示相同内容。

请问是否有办法让方案B正常工作?


方案A

// 创建场景
var scene = new THREE.Scene();

// 创建相机
var width = window.innerWidth;
var height = window.innerHeight;
var aspect = width / height;
var camera = new THREE.PerspectiveCamera(75, aspect, 1, 1000);
camera.position.set(0,0,5);

// 创建渲染器
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);

// 创建几何体
var cubeGeometry = new THREE.BoxGeometry(1,1,1);
var material = new THREE.MeshNormalMaterial({ color: 0xff0000 });
var cube = new THREE.Mesh(cubeGeometry, material);
cube.rotation.set(15,15,0);
scene.add(cube);

// 创建标签位置
var parentObject = new THREE.Object3D();
var pointObjects = [];

for (var i = 0; i < 3; i++) {
  for (var j = 0; j < 3; j++) {
    pointObjects.push({ x: i, y: j, z: 0 });
  }
}

// 创建标签
var labelBaseScale = .002;

pointObjects.forEach(function (element, index) {
    var textToDisplay = index;
    var ctx = document.createElement('canvas').getContext('2d');
    ctx.canvas.width = 1000;
    ctx.canvas.height = 80;
    ctx.fillStyle = 'rgba(255, 255, 255, 0)';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    ctx.font = 100 + 'px monospace';
    ctx.textBaseline = 'top';
    ctx.translate(2, 2);
    ctx.fillStyle = '#747474';
    ctx.fillText(textToDisplay, 0, 0);

    var texture = new THREE.CanvasTexture(ctx.canvas);
    texture.needsUpdate = true;
    texture.minFilter = THREE.LinearFilter;
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping;

    var labelMaterial = new THREE.SpriteMaterial({
      map: texture,
      transparent: true,
    });

    var label = new THREE.Sprite(labelMaterial);
    label.position.x = element.x;
    label.position.y = element.y;
    label.position.z = element.z;
    label.scale.x = ctx.canvas.width  * labelBaseScale;
    label.scale.y = ctx.canvas.height * labelBaseScale;
  
  parentObject.add(label);
});

scene.add(parentObject);

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

animate();

方案B

// 创建场景
var scene = new THREE.Scene();

// 创建相机
var width = window.innerWidth;
var height = window.innerHeight;
var aspect = width / height;
var camera = new THREE.PerspectiveCamera(75, aspect, 1, 1000);
camera.position.set(0,0,5);

// 创建渲染器
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);

// 创建几何体
var cubeGeometry = new THREE.BoxGeometry(1,1,1);
var material = new THREE.MeshNormalMaterial({ color: 0xff0000 });
var cube = new THREE.Mesh(cubeGeometry, material);
cube.rotation.set(15,15,0);
scene.add(cube);

// 创建标签位置
var parentObject = new THREE.Object3D();
var pointObjects = [];

for (var i = 0; i < 3; i++) {
  for (var j = 0; j < 3; j++) {
    pointObjects.push({ x: i, y: j, z: 0 });
  }
}

// 创建标签
var labelBaseScale = .002;
var ctx = document.createElement('canvas').getContext('2d');

pointObjects.forEach(function (element, index) {
    var textToDisplay = index;
    ctx.canvas.width = 1000;
    ctx.canvas.height = 80;
    ctx.fillStyle = 'rgba(255, 255, 255, 0)';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    ctx.font = 100 + 'px monospace';
    ctx.textBaseline = 'top';
    ctx.translate(2, 2);
    ctx.fillStyle = '#747474';
    ctx.fillText(textToDisplay, 0, 0);

    var texture = new THREE.CanvasTexture(ctx.canvas);
    texture.needsUpdate = true;
    texture.minFilter = THREE.LinearFilter;
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping;

    var labelMaterial = new THREE.SpriteMaterial({
      map: texture,
      transparent: true,
    });

    var label = new THREE.Sprite(labelMaterial);
    label.position.x = element.x;
    label.position.y = element.y;
    label.position.z = element.z;
    label.scale.x = ctx.canvas.width  * labelBaseScale;
    label.scale.y = ctx.canvas.height * labelBaseScale;
  
  parentObject.add(label);
});

scene.add(parentObject);

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

animate();

解决方案

方案B失效的核心原因是:所有CanvasTexture共享了同一个Canvas元素的引用。当你在循环中更新Canvas内容时,之前创建的所有Texture都会指向这个被修改后的Canvas,最终所有标签显示最后一次绘制的内容。

要让复用单个Canvas的方案正常工作,需要在每次绘制新文本后,克隆Canvas的像素数据,让每个Texture拥有独立的数据源,具体修改后的代码如下:

// 创建场景
var scene = new THREE.Scene();

// 创建相机
var width = window.innerWidth;
var height = window.innerHeight;
var aspect = width / height;
var camera = new THREE.PerspectiveCamera(75, aspect, 1, 1000);
camera.position.set(0,0,5);

// 创建渲染器
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);

// 创建几何体
var cubeGeometry = new THREE.BoxGeometry(1,1,1);
var material = new THREE.MeshNormalMaterial({ color: 0xff0000 });
var cube = new THREE.Mesh(cubeGeometry, material);
cube.rotation.set(15,15,0);
scene.add(cube);

// 创建标签位置
var parentObject = new THREE.Object3D();
var pointObjects = [];

for (var i = 0; i < 3; i++) {
  for (var j = 0; j < 3; j++) {
    pointObjects.push({ x: i, y: j, z: 0 });
  }
}

// 创建标签
var labelBaseScale = .002;
var ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 1000;
ctx.canvas.height = 80;

pointObjects.forEach(function (element, index) {
    var textToDisplay = index;
    // 清空画布
    ctx.fillStyle = 'rgba(255, 255, 255, 0)';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    // 重置变换(避免translate累积导致偏移)
    ctx.setTransform(1, 0, 0, 1, 0, 0);

    ctx.font = 100 + 'px monospace';
    ctx.textBaseline = 'top';
    ctx.translate(2, 2);
    ctx.fillStyle = '#747474';
    ctx.fillText(textToDisplay, 0, 0);

    // 克隆Canvas像素数据,生成独立数据源
    var cloneCanvas = document.createElement('canvas');
    cloneCanvas.width = ctx.canvas.width;
    cloneCanvas.height = ctx.canvas.height;
    var cloneCtx = cloneCanvas.getContext('2d');
    cloneCtx.drawImage(ctx.canvas, 0, 0);

    // 使用克隆后的Canvas创建Texture
    var texture = new THREE.CanvasTexture(cloneCanvas);
    texture.minFilter = THREE.LinearFilter;
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping;

    var labelMaterial = new THREE.SpriteMaterial({
      map: texture,
      transparent: true,
    });

    var label = new THREE.Sprite(labelMaterial);
    label.position.x = element.x;
    label.position.y = element.y;
    label.position.z = element.z;
    label.scale.x = ctx.canvas.width  * labelBaseScale;
    label.scale.y = ctx.canvas.height * labelBaseScale;
  
  parentObject.add(label);
});

scene.add(parentObject);

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

animate();

关键优化点说明

  • 重置Canvas变换:每次绘制前调用ctx.setTransform(1, 0, 0, 1, 0, 0),避免循环中translate的变换累积导致文本偏移;
  • 克隆Canvas:通过drawImage将原Canvas内容复制到新Canvas,确保每个Texture拥有独立的图像数据,既复用了原Canvas的绘制上下文,又避免了数据共享问题;
  • 性能平衡:相比方案A创建数千个Canvas,这种方式只创建一个绘制用Canvas,其余为克隆的轻量级Canvas,在性能和内存占用上更优。

内容的提问来源于stack exchange,提问作者Chris A.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 16:44:55