如何优化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.
相关产品推荐
相关产品推荐

