在three.js中动态创建2D文本

43
我有一个在three.js中创建的3D模型。基于一些数据,我想创建一组由小文本标签装饰的箭头。这些标签应该是2D的。
看起来我有两个选择:要么使用单独的画布元素创建纹理,然后在3D模型中使用它,要么在3D模型的画布元素上方使用HTML。
我想知道如何处理这个问题。哪种方法是“正确”的?欢迎提供任何建议和示例代码!
8个回答

50
如果您不介意文本始终在顶部(例如,如果对象被其他东西阻挡,其文本标签仍将可见且位于所有其他内容的顶部),并且文本不会受到任何渲染效果(如光/阴影等)的影响,则 HTML 是我认为最简单的方法。以下是一些示例代码:
var text2 = document.createElement('div');
text2.style.position = 'absolute';
//text2.style.zIndex = 1;    // if you still don't see the label, try uncommenting this
text2.style.width = 100;
text2.style.height = 100;
text2.style.backgroundColor = "blue";
text2.innerHTML = "hi there!";
text2.style.top = 200 + 'px';
text2.style.left = 200 + 'px';
document.body.appendChild(text2);

将style.top和style.left变量中的数字200替换为您希望放置文本的y和x坐标(分别)。它们将是正值,其中(0,0)是画布的左上角。如果需要指导如何从画布中的3D点投影到浏览器窗口中的2D像素点,请使用以下代码片段:
function toXYCoords (pos) {
        var vector = projector.projectVector(pos.clone(), camera);
        vector.x = (vector.x + 1)/2 * window.innerWidth;
        vector.y = -(vector.y - 1)/2 * window.innerHeight;
        return vector;
}

确保您先调用了 camera.updateMatrixWorld()。


谢谢你的回答!我自己投影坐标时遇到了一些问题,但后来发现我在投影向量之前忘记做camera.updateMatrixWorld()了。结果是不可用的值远离屏幕。 - anders
嘿,谢谢您的回答,我在Three r79中尝试了一下,结果出现了“projector未定义”的错误,有没有解决方法?谢谢。 - Hicham Zouarhi
当我移动物体或旋转相机时,我必须手动更新文本位置。有没有一种方法可以使这个更新自动化?例如,如果我想在场景中的每个物体上方制作2D标签,同时一切都在移动。 - Prokop Hapala
Prokop Hapala--听起来你最好的选择是将标签作为3D场景的一部分;请参见@LeeStemkoski下面的解决方案。 - lobotmcj
@kronuus 你好,能否看一下我的问题?https://stackoverflow.com/questions/61631545/add-text-with-geometry-in-threejs - Firzok Nadeem
CSS2DRenderer是目前这种方法的实现,它可以自动为您定位。 - Leonid Shevtsov

25

请查看演示

TextGeometry会消耗大量内存,你需要加载包含每个字母几何形状的字体。如果场景中有超过100个文本网格,我的浏览器就会崩溃。

你可以在画布上绘制文本,并通过Sprite将其包含在场景中。但问题是你需要计算字体大小。如果你有一个移动的相机,那么你需要定期计算字体大小。否则,如果你使用相机靠近文本,文本将变得模糊。

我编写了一个TextSprite类,它会自动根据与相机的距离和渲染器元素的大小计算最佳字体大小。关键是使用Object3D类的回调函数.onBeforeRender,该函数接收相机和渲染器。

let sprite = new THREE.TextSprite({
  text: 'Hello World!',
  fontFamily: 'Arial, Helvetica, sans-serif',
  fontSize: 12,
  color: '#ffbbff',
});
scene.add(sprite);

你可以实时更改文本和字体。
sprite.text = 'Hello Stack Overflow!';
sprite.fontFamily = 'Tahoma, Geneva, sans-serif';
sprite.fontSize = 24;

1
呃,我并没有这样做,而是使用了从相机投影的方法来查找一个 HTML 元素的位置,然后使用绝对定位进行定位。虽然我有点希望我用了这个方法。干得好!:D - dylnmc
想提醒一下,如果您正在呈现数百个TextSprites,则此解决方案效果并不理想。性能受到影响。 - Devin Haslam
@SeregPie 你好,能否请您看一下我的问题 https://stackoverflow.com/questions/61631545/add-text-with-geometry-in-threejs ?谢谢。 - Firzok Nadeem
尝试了演示版,但屏幕上没有任何可见内容(Firefox 83,32位)。 - steveOw
@steveOw 哦,谢谢。我现在已经修复了。AFrame进行了更新,unpkg无法解析该包。 - SeregPie

20

我第一次学习时使用了你的示例 :) 非常感谢您提供的优秀教程!不过我想知道,为什么使用画布方法会导致文本模糊(至少在我的机器上呈现出这样的效果)? - anders
也许这是抗锯齿的一个副作用? - Stemkoski
还有,我相信这个程序使用了一些已不包含在Three.js中的函数。 - Single Entity
2
模糊的文本似乎是由于three.js将精灵呈现为正方形,即使画布可能比高度宽。如果您将精灵缩放以具有与画布相同的比例,则应该会看起来更好。 - Doguleez

10

更新:此方法现已过时,CPU 占用率高,请勿使用。

我找到了另一种只基于 three.js 的解决方案。

var textShapes = THREE.FontUtils.generateShapes( text, options );
var text = new THREE.ShapeGeometry( textShapes );
var textMesh = new THREE.Mesh( text, new THREE.MeshBasicMaterial( { color: 0xff0000 } ) ) ;
scene.add(textMesh);
// Example text options : {'font' : 'helvetiker','weight' : 'normal', 'style' : 'normal','size' : 100,'curveSegments' : 300};

现在要动态编辑这段文字,

var textShapes = THREE.FontUtils.generateShapes( text, options );
var text = new THREE.ShapeGeometry( textShapes );
textMesh.geometry = text;
textMesh.geometry.needsUpdate = true;

4
需要注意的是,这是一种非常昂贵的方法来创建2D文本。如果您想要显示标准的2D文本,使用画布中的HTML更为明智。 - Single Entity
你在定义文本之前使用了它,请检查变量名称。 - Broda Noel
1
我认为现在已经不推荐使用那个了,而是推荐使用TextShape - Miquel

8
我已经在NPM上发布了一个模块,可以帮你完成这个任务:https://github.com/gamestdio/three-text2d 它允许你使用SpriteMesh来渲染文本,而无需手动处理画布。 示例:
var Text2D = require('three-text2d').Text2D

var text = new Text2D("Hello world!", { font: '30px Arial', fillStyle: '#000000', antialias: true })
scene.add(text) 

5
你能否举个例子来说明如何使用你的模块?我刚开始使用ThreeJS,所以我只有一个简单的hello-world例子。我认为不能在其中使用require('three-text2d'),那么我应该使用像browserify这样的包将你的包与ThreeJS一起编译吗?谢谢! - Tyler Chong

6
你可以创建一个2D画布并使用CSS将其定位到WebGL画布的顶部。然后,您可以使用2D画布上下文提供的ctx.fillText(text,x,y)ctx.strokeText(text,x,y)方法在其上绘制文本。通过使用此方法,您可以绘制除文本以外的其他内容,例如箭头和计量器。
您可以像@kronuus建议的那样使用该方法将3D空间中的点转换为2D空间。

这比位于我的threejs画布顶部的HTML元素更高效吗? - croraf

3

@LeeStemkoski的回答非常有用。但是代码需要稍作修改,以便文本跟随箭头移动、旋转等。请参见下面的代码:

                        var canvas1 = document.createElement('canvas');
                        var context1 = canvas1.getContext('2d');
                        context1.font = "Bold 10px Arial";
                        context1.fillStyle = "rgba(255,0,0,1)";
                        context1.fillText('Hello, world!', 0, 60);

                        // canvas contents will be used for a texture
                        var texture1 = new THREE.Texture(canvas1)
                        texture1.needsUpdate = true;

                        var material1 = new THREE.MeshBasicMaterial({ map: texture1, side: THREE.DoubleSide });
                        material1.transparent = true;

                        var mesh1 = new THREE.Mesh(
                            new THREE.PlaneGeometry(50, 10),
                            material1
                          );
                        mesh1.position.set(25, 5, -5);
                        mesh1.rotation.x = -0.9;
                        shape.add(mesh1);
                        // Note that mesh1 gets added to the shape and not to the scene

                       scene.add(shape)

正如你所看到的,关键是将文本 (mesh1) 添加到形状(你的“箭头”)而不是添加到场景中

希望这可以帮助你。


1

来自Three js的“几何/文本/形状”:https://threejs.org/examples/?q=text#webgl_geometry_text_shapes

我简化了代码,只保留必需的部分以添加2D动态文本:

var loader = new THREE.FontLoader();
loader.load( 'assets/fonts/helvetiker_regular.typeface.json', function ( font ) {   
    var textPositions = [[1, 1, 1],
                         [2, 2, 2]];

    var textMessages = ['Text 1', 'Text 2'];

    var textSizes = [0.1, 0.2];

    var textName = ['text1', 'text2'];

    var textsNumber = textPositions.length;     

    for (var i = 0; i < textsNumber; i++) {

方法'generateShapes'允许创建文字形状。
        var textsShapes = font.generateShapes( textMessages[i], textSizes[i] );
        var textsGeometry = new THREE.ShapeBufferGeometry( textsShapes );    
        var textsMaterial = new THREE.MeshBasicMaterial({color: 0xeeeeee});

        var text = new THREE.Mesh(textsGeometry, textsMaterial);
        text.position.set(textPositions[i][0], textPositions[i][1], textPositions[i][2]);
        text.name = textName[i]; 

        scene.add(text);
    }

}); 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接