最近要给一个三维可视化Demo加一批标注字符,翻来翻去试了三四套方案,最后发现 VTK 里 “3D Text” 这个概念其实藏了两套完全不同的实现。如果你也正在做三维场景里的标签、注释、信息牌,或者打算把文字变成真正参与光照、裁剪、拾取的几何体,那这篇文章应该能让你少走不少弯路。它适合已经能搭起基础 VTK 渲染管线、现在正想往场景里加文字的读者,也适合那些被“文字显示出来又看不见、一旋转就消失、字号怎么调都不对”折磨的人。
先说结论:VTK 的 3D 文字,大致分成“矢量几何文字”和“纹理字卡文字”两条路。前者把每个字符拆成三角面片,放进正常的渲染管线,能缩放、能光照、能裁剪;后者则是一张贴在世界坐标位置的文字纹理,永远正对相机,处理动态中文标注非常顺手。两条路线不能混着理解,否则就会遇到“为什么我的文字一旋转就消失”这类看起来玄学、实际原理很清晰的问题。下面我把两条路线的底层逻辑、代码链路和实操坑位都展开讲。
1. 先别急着写代码:两种“3D文字”根本不是一回事
VTK 里能称得上 3D 文本的类不少,但我实际用下来,真正在项目里扛大梁的就两个方向:vtkVectorText和vtkTextActor3D。很多资料不会把它们的差异讲透,等你把代码跑起来、再一做旋转或者换一个中文字符串,才会意识到选型没选对。
1.1 vtkVectorText:把字符拆成三角面片
vtkVectorText的内部逻辑是:把你传入的字符串逐字符转换成一组向量轮廓,再把这些轮廓三角化成vtkPolyData,最后像普通几何体一样交给vtkPolyDataMapper去渲染。换句话说,它生成的东西和球体、圆柱体本质上没有任何区别,就是顶点、三角形、法向量。这也意味着它天然能吃下一整套几何处理流程:vtkTransform旋转、vtkClipPolyData裁剪、vtkLinearExtrusionFilter挤出厚度、vtkSTLWriter导出模型。
用它对文字做缩放时,文字会和旁边的模型保持绝对一致的尺度关系。模型放大十倍,文字也会跟着放大十倍。这在科学可视化里很有用,尤其是你想让标注的物理尺寸和测量结果对应起来的时候。它的缺点是:文字默认长在 Z=0 的平面上,本身是一张极其扁平的薄片,从侧面看几乎是一条线;同时因为没有“字号”这个概念,不能用SetFontSize(14)这种直觉API来控制大小,只能通过变换矩阵调整。
实际场景里我经常用它做“跟随模型的标签”,因为它本质上就是几何体,可以像其他部件一样被拾取、被射线命中、被导出,不会出现“只能看不能碰”的尴尬。
1.2 vtkTextActor3D:一张永远面向相机的字卡
vtkTextActor3D走的是另一条路。它内部用字体渲染引擎把文字画到一张纹理上,然后把这张纹理贴到一个小的四边形上,并保证这个四边形始终面向当前相机。你不需要关心它转向的问题,哪个角度看到的都是正面文字。更关键是它走的是 FreeType 字体渲染,所以它能显示中文、韩文、日文这些非英文字符,也能设置常规的字体大小、斜体、加粗、阴影。
代价是:它不再是一个真正的三维几何体。它的三角形只有一张四边形面片,不能做挤出、不能做表面光照、不能精确地被多边形裁剪。它的文字大小默认以屏幕像素为单位,不会随相机推远拉近而变大变小,这让它非常适合做某些“固定信息牌”,但在需要跟模型一起缩放、配合空间尺寸的标注场景里就会显得比例失调。
选型时我问自己一个问题:如果我把相机推得很近,文字应该跟着模型一起变大,还是始终保持同样大小?前者闭眼选vtkVectorText,后者在vtkTextActor3D里会更顺手。
1.3 选型不是靠喜好,而是看这四个维度
我把两类方案的核心差异整理成一张表,后续做需求判断时可以拿来直接对照。
| 对比维度 | vtkVectorText | vtkTextActor3D |
|---|---|---|
| 输出类型 | vtkPolyData,真实几何 | 文字纹理 + 公告板四边形 |
| 文字大小 | 靠 Bounds + 变换缩放 | SetFontSize,像素字号 |
| 随模型缩放 | 会,保持世界尺度一致 | 不会,默认保持屏幕大小 |
| 中文字符 | 基本不可用 | 支持,需配置字体文件 |
| 朝向控制 | 手动旋转或配合 vtkFollower | 自动面向相机 |
| 参与裁切/导出 | 完全支持 | 不行 |
| 拾取精度 | 三角面片级命中 | 四边形包围盒命中 |
| 渲染开销 | 文字笔画越多三角形越多 | 一个四边形 + 纹理,开销低 |
这张表不是绝对的,比如vtkTextActor3D也可以通过显式设置 actor 的SetScale来间接控制屏幕尺寸,但宏观上差异是稳定的。我把这个对照表贴在工位上用了很久,每次同事来问“VTK 文字怎么搞”,我都是先请对方按表自测一下。
2. 跑通 vtkVectorText:从 Hello World 到尺寸可控
vtkVectorText是最接近“标准几何体”的文本方案,所以我先把它完整跑通。这一段给出的代码都是可以直接粘贴进工程里的最小示例,你可以在里面继续加自己的东西。
2.1 一个最简的 Hello World 工程
假设你已经有一个能够创建vtkRenderer、vtkRenderWindow、vtkRenderWindowInteractor的基础环境,加入文字只需要额外做四件事:创建文本源、创建 Mapper、创建 Actor、把 Actor 丢给 Renderer。
#include <vtkSmartPointer.h> #include <vtkVectorText.h> #include <vtkPolyDataMapper.h> #include <vtkActor.h> #include <vtkRenderer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> int main() { vtkSmartPointer<vtkVectorText> textSource = vtkSmartPointer<vtkVectorText>::New(); textSource->SetText("VTK 3D Text"); vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper->SetInputConnection(textSource->GetOutputPort()); vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New(); actor->SetMapper(mapper); actor->GetProperty()->SetColor(1.0, 0.8, 0.2); vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New(); renderer->AddActor(actor); renderer->SetBackground(0.1, 0.1, 0.15); vtkSmartPointer<vtkRenderWindow> window = vtkSmartPointer<vtkRenderWindow>::New(); window->AddRenderer(renderer); window->SetSize(800, 600); vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New(); interactor->SetRenderWindow(window); interactor->Start(); return 0; }这段代码跑起来后,你会看到一行黄色文字,它非常小,而且所有的轮廓都在 XY 平面内展开。如果什么都不做,直接用默认相机,文字可能只占屏幕的一小块区域。很多人第一次看到会以为程序写错了,其实没有,只是这个类的“原生坐标”本来就不是给你直接用的。
2.2 为什么没有 SetFontSize?用 Bounds 控制世界尺寸
vtkVectorText没有字号接口,原因是它内嵌的字体是矢量轮廓,不涉及栅格化。字符尺寸由字体定义决定,默认情况下整行文字的高度通常不足几个单位。要让它按照你想要的“世界坐标尺寸”显示,正确做法是读取它的包围盒(Bounding Box),然后计算缩放系数。
vtkSmartPointer<vtkTransform> transform = vtkSmartPointer<vtkTransform>::New(); double bounds[6]; textSource->GetOutput()->GetBounds(bounds); double height = bounds[3] - bounds[1]; double centerX = (bounds[0] + bounds[1]) * 0.5; double centerY = (bounds[2] + bounds[3]) * 0.5; double targetHeight = 2.0; // 希望文字在场景中高度约为 2 个世界单位 double scale = targetHeight / height; transform->PostMultiply(); transform->Scale(scale, scale, scale); transform->Translate(-centerX, -centerY, 0.0); vtkSmartPointer<vtkTransformPolyDataFilter> transformFilter = vtkSmartPointer<vtkTransformPolyDataFilter>::New(); transformFilter->SetInputConnection(textSource->GetOutputPort()); transformFilter->SetTransform(transform); mapper->SetInputConnection(transformFilter->GetOutputPort());这里我做了两件事:一是把文字等比缩放到目标高度,二是把文字的中心平移到局部坐标系原点。第二个操作不是可有可无,它决定了后续vtkActor的SetPosition到底是作用在文字的哪个点上。如果不做居中,SetPosition(1,2,3)会把文字的某个角落放到这个位置,而不是文字中心,这在做精确标注时会非常别扭。
targetHeight的取值完全取决于你的场景尺度。我在一个医学可视化 Demo 里,模型尺寸通常在 200 个世界单位左右,标签高度设在 5 到 10 之间;在另一个机械零件的场景里,模型只有 20 个单位,标签高度就调成 0.8。这里的经验是:不要试图给所有场景定一个固定字号,先用GetBounds量出模型高度,再按模型高度的 1/20 到 1/30 取标签高度,视觉上最舒服。
2.3 朝向与背面剔除:转个视角字就没了
vtkVectorText生成的文字默认躺在 Z=0 平面。如果你不做任何旋转,文字长轴沿 X 展开、高度沿 Y,正面朝向 Z 轴正方向。默认渲染器开启背面剔除,所以当你把相机转到文字背面,文字会整个消失,看起来就像“模型穿模了”。
这个问题我踩过很多次。解决方式有两种:如果文字是独立展示的,简单点就直接关掉背面剔除:
actor->GetProperty()->SetBackfaceCulling(false);但我不推荐把它当通用方案,因为背面不剔除以后,文字从背面看是镜像的,视觉上很乱。更合理的做法是让文字始终面向相机,也就是后面要讲的vtkFollower方案,这个技巧对vtkVectorText尤其重要。
如果你追求的是“真的有厚度”的立体文字,那可以把文字先挤出,再用常规光照:
#include <vtkLinearExtrusionFilter.h> vtkSmartPointer<vtkLinearExtrusionFilter> extrude = vtkSmartPointer<vtkLinearExtrusionFilter>::New(); extrude->SetInputConnection(textSource->GetOutputPort()); extrude->SetExtrusionTypeToVector(); extrude->SetVector(0.0, 0.0, 0.2); extrude->Update();挤出 0.2 个单位后,文字就从一张薄片变成了带有厚度的实体,旋转时能看到侧壁,光照下也有明暗变化。这个方法用来生成 3D 打印用的文字模型特别合适。
3. 做标注绕不开的 vtkTextActor3D:中文、动态改字与遮挡处理
项目里最多出现的需求其实是“给某个点/组织加一行中文标签”。这时候如果继续用vtkVectorText,你会遇到中文全部无法显示的问题,然后不得不转向vtkTextActor3D。这三节是我把vtkTextActor3D用顺以后的完整心得。
3.1 中文字形与字体文件的配置
vtkVectorText内嵌的矢量字体只覆盖 ASCII 字符集,你给它传中文字符串,它只会跳过或者画出一堆毫无意义的多边形。要用中文,必须走vtkTextActor3D的 FreeType 渲染路径,同时给字体一个明确的字体文件路径,否则字体回退策略会留下一个很丑的默认字形。
#include <vtkTextActor3D.h> #include <vtkTextProperty.h> vtkSmartPointer<vtkTextActor3D> textActor = vtkSmartPointer<vtkTextActor3D>::New(); textActor->SetInput("样品点 A"); textActor->SetPosition(10.0, 20.0, 30.0); vtkSmartPointer<vtkTextProperty> textProperty = vtkSmartPointer<vtkTextProperty>::New(); textProperty->SetFontSize(18); textProperty->SetColor(0.1, 0.9, 0.2); textProperty->SetFontFamily(VTK_FONT_FILE); textProperty->SetFontFile("/path/to/your/font/SimHei.ttf"); textActor->SetTextProperty(textProperty);这里最容易忽略的一个点是VTK_FONT_FILE这个字体族枚举。只设置SetFontFamily(VTK_FONT_FILE)还不够,必须同时设置SetFontFile,而且这个路径必须是实际存在的 TTF 或 OTF 文件。开发机上能用,换一台机器跑就白板,多数是字体路径没打包进去。我现在的习惯是把字体文件拷到项目资源目录下,用相对路径拼接,而不是直接写死绝对路径。
SetPosition设置的是文字纹理四边形锚点在三维空间中的位置。和vtkVectorText不一样,它没有“几何中心”的概念,所以你在设置位置时要想清楚:这个坐标点是文字的中心、左上角还是左下角?实测表明它默认以文字左下角附近的点为锚点,如果你希望文字居中,需要根据文字尺寸手动做偏移,或者干脆在设置位置时预留一个修正量。
3.2 动态改字:别只更新一次就以为万事大吉
很多标注场景不是静态的,比如鼠标点击某个点后,标签要跟着变内容。vtkTextActor3D支持动态SetInput,但具体表现比我预想中要敏感一些。我的建议是需要改字时,不要只调用一次SetInput就完事,最好显式触发一下更新:
textActor->SetInput("新的标签内容"); textActor->Modified(); renderWindow->Render();Modified()会强制 VTK 把该对象的 MTime 更新,让下游的渲染逻辑意识到内容变了。如果省略这行,纹理缓存没有失效,你会发现下次刷新可能还是旧文字,这种“改了但没完全改”的状态排查起来非常耗时间。
如果场景里有一批文字需要同步更新,最好不要遍历所有vtkTextActor3D逐个SetInput再逐个Render,那样会触发多次重绘,UI 会肉眼可见地卡顿。正确的批量更新姿势是:先把所有需要修改的对象改完,最后统一调用一次renderWindow->Render()。VTK 是按 MTime 来判定是否重算的,同一个渲染帧内多个对象同时变化是没有任何问题的。
3.3 遮挡关系:要么被模型挡,要么强行置顶
vtkTextActor3D本质上参与深度测试,所以当模型挡住标签时,标签会被模型“吃掉”,这是符合真实三维渲染常识的。但标注类应用里,我们通常希望标签能一直浮在模型表面之上,所以需要主动关掉深度测试:
textActor->GetProperty()->SetDepthTest(false);关闭深度测试以后,标签会渲染在场景所有物体之上,无论相机怎么转都不会被遮住。代价是它和模型的遮挡关系不再可信,如果场景里存在多个标签之间互相遮挡,它们会按渲染顺序直接覆盖,而不是按真实深度排序。同一个场景中最好把“必须被遮”和“必须置顶”的标签分开放到两个 Renderer 或者两组渲染状态里,不要混在一起。
另一个提升可读性的小技巧是给标签加背景和边框。vtkTextActor3D的GetTextProperty()里可以设置背景颜色和边框宽度,虽然它的背景也是随纹理走的,效果比纯文字好很多。实测下来,深色场景配半透明深色背景能大幅减少文字和背景物体之间的视觉混淆,但要注意背景色不宜和文字颜色形成太接近的搭配,否则就白加了。
4. 实战:做一个跟随相机、又贴着模型的标签
既然两条路线都搞清楚了,把它们组合起来才是真正的工程能力。这一节我详细拆解一个我最常用的组合方案:用vtkVectorText生成文字几何体,再用vtkFollower让它跟随相机,最终得到一个“贴在世界坐标里、又永远面向你、还能随模型缩放”的标签。我给它起名叫“跟随式标签”,它在很多手术导航、地质剖面标注、机械装配指导项目里都适用。
4.1 这个标签需要同时满足三个约束
设计这个标签之前,我先明确三个约束,避免做到一半才发现方向错了:
- 标签位置要钉在模型的空间坐标上,不能因为相机旋转就漂移。
- 文字要始终正对相机,不能被转到背面看镜像。
- 文字大小要随场景缩放,保证从远处拉近时比例不失调。
这三个约束分别对应vtkFollower的相机跟随、vtkVectorText的几何缩放、vtkFollower和模型共享世界坐标这一特性。三者缺一不可。
4.2 核心代码与逐段解释
整体实现我分两步走:第一步用vtkVectorText生成文字并居中缩放到目标尺寸;第二步外面包一层vtkFollower,挂到指定坐标点上。
#include <vtkFollower.h> #include <vtkTransform.h> #include <vtkTransformPolyDataFilter.h> // 1. 生成文字并处理尺寸 vtkSmartPointer<vtkVectorText> textSource = vtkSmartPointer<vtkVectorText>::New(); textSource->SetText("P1"); double b[6]; textSource->GetOutput()->GetBounds(b); double cx = (b[0] + b[1]) * 0.5; double cy = (b[2] + b[3]) * 0.5; double textHeight = b[3] - b[2]; double targetHeight = 4.0; double scale = targetHeight / textHeight; vtkSmartPointer<vtkTransform> transform = vtkSmartPointer<vtkTransform>::New(); transform->PostMultiply(); transform->Scale(scale, scale, scale); transform->Translate(-cx, -cy, 0.0); vtkSmartPointer<vtkTransformPolyDataFilter> transformFilter = vtkSmartPointer<vtkTransformPolyDataFilter>::New(); transformFilter->SetInputConnection(textSource->GetOutputPort()); transformFilter->SetTransform(transform); // 2. Mapper + Follower vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper->SetInputConnection(transformFilter->GetOutputPort()); vtkSmartPointer<vtkFollower> follower = vtkSmartPointer<vtkFollower>::New(); follower->SetMapper(mapper); follower->SetCamera(renderer->GetActiveCamera()); follower->SetPosition(anchorX, anchorY, anchorZ); follower->GetProperty()->SetColor(1.0, 0.85, 0.1); renderer->AddActor(follower);这段代码最核心的两个动作是:
- 在
vtkTransform里用PostMultiply后先把文字缩放到目标高度,再把几何中心平移到原点。这样vtkFollower的SetPosition设置的就是文字中心点,而不是某个字符角落。 vtkFollower必须调用SetCamera,它靠相机位置来计算自己的朝向。如果你忘了这行,vtkFollower和一个普通vtkActor没区别,文字照样会被转到背面。
anchorX、anchorY、anchorZ是你要标注的那个三维点的坐标。我一般会在一个数据点周围同时创建几个这样的vtkFollower,让它们分别显示坐标值、类型名、序号,位置沿 Y 轴往上错开一点,避免全部叠在一起。
4.3 没居中的时候,标签位置总差一截
从vtkVectorText直接拿到的文字几何体,它的实际边界并不一定以字符串中心为原点。尤其是在文字内容不对称的时候,比如 “P1” 和 “L10”,左边距和右边距不一样,最终显示出来的锚点会明显偏移。如果不做居中处理,你看到的现象是:SetPosition明明设在同一坐标,但 “P1” 和 “L10” 两个标签的实际位置却不齐。
居中处理的本质是把vtkTransformPolyDataFilter里的平移量设置为文字包围盒中心的反方向,这一步做完后,文字几何体就在局部坐标的原点附近展开。之后vtkFollower的位置就是唯一锚点,你再也不用关心文字内部到底长什么样。
如果你需要让标签在某个点上方偏移一段距离,比如显示高度信息,建议把偏移量也放进SetPosition,比如follower->SetPosition(x, y + 2.0, z),而不是再嵌套一层vtkTransform。Follower 的变换优先级和普通 Actor 不一样,在外面嵌套 Transform 容易搞乱相机跟随逻辑,我试过一次以后就不再用这种自找麻烦的方式了。
5. 去掉“纸面概念”之后:性能、拾取和导出
把标签显示出来只是第一步,真正放到实际项目里,你还会遇到批量生成、鼠标点击、模型导出这些后续需求。这三块单独拎出来讲,因为它们和 3D 文本本身的关联不算强,但恰恰是决定方案能不能落地的关键。
5.1 几十个标签一起上,三种优化手段很有用
如果你的场景里只有两个三个标签,前面的代码随便写,性能都不是问题。但一旦要批量生成几十个标签,比如医学影像里对每个解剖结构都加上名称缩写,就必须考虑渲染开销。
对于文字几何方案,每个vtkVectorText都可能生成成百上千个三角形,几十个文本同时存在,几何总量会非常可观。我常用的优化手段有三种:
- 静态文本合并:如果文字内容永远不变,可以先把所有文本几何体经过
vtkAppendPolyData合并成一个 PolyData,再用一个vtkActor渲染,渲染开销直接从几十个 Actor 降到 1 个。代价是合并后无法单独控制每个标签的位置,所以合并前一定要把各文本的变换矩阵提前预乘进去。 - 相同文本共享源:如果多个标签内容一样,比如一排 “A” 标记,可以让共享同一个
vtkVectorText和vtkPolyDataMapper,只有vtkActor的SetPosition不同。这类数据是最划算的,因为文字几何体在显存里只有一份。 - 混合方案:对需要动态更新的少量标签用
vtkFollower + vtkVectorText,对大批量静态文字使用vtkTextActor3D或合并后的几何体。不要试图让一种方案通吃所有场景,混合使用往往才最舒服。
另外要注意,vtkTextActor3D的默认渲染路径是 VtkImageActor 式的纹理公告板,文本内容如果完全相同,纹理可以复用;但不同内容之间就很难再合并优化了。批量生成时优先考虑内容分组,同一组文字共享字体和着色器,能显著减少状态切换。
5.2 让文字可以被鼠标点击选中
有些交互需要用户点选某个标签,而不是点选模型本身。对vtkVectorText这类几何文字来说,它就是一个普通 Actor,可以参与vtkPicker系的命中测试,拾取到的也是三角面片级几何,精度相当不错。
不过vtkFollower在拾取上有一个特殊性:它的内部变换会在渲染前根据相机朝向进行更新,但拾取逻辑不一定同步。稳妥的做法是在交互器回调里用最简单的屏幕坐标到世界坐标最近邻判断,而不是完全依赖 Picker 的几何拾取:
vtkSmartPointer<vtkCellPicker> picker = vtkSmartPointer<vtkCellPicker>::New(); picker->Pick(eventPosition[0], eventPosition[1], 0, renderer); vtkActor* pickedActor = picker->GetActor(); if (pickedActor == follower) { // 用户点中了这个文字标签 }如果拾取结果不准确,先检查 Follower 的SetCamera是否设置成功,再检查文字几何体是否已经经过居中处理。很多拾取偏差其实不是拾取器的错,而是文字几何体本身偏移了。把文字中心和 Follower 位置对齐以后,拾取命中框会和你看到的文字位置完全重合。
对vtkTextActor3D而言,它的拾取区域本质上是公告板四边形,鼠标在文字周围一圈空白处也可能被判定为命中。如果你需要精确到字符级别的拾取,vtkTextActor3D并不合适,换成vtkVectorText或者提前把拾取区域缩小到字符笔画范围内才更靠谱。
5.3 把文字导出成可打印的实体铭牌
vtkVectorText一个被低估的用途是生成 3D 打印的实体文字。因为它输出的是 Polygon Data,可以直接接vtkSTLWriter写出 STL。不过有个前提:原始文字只是个平面薄片,厚度为零,打印机不认。所以导出之前必须先用vtkLinearExtrusionFilter挤出厚度。
#include <vtkSTLWriter.h> vtkSmartPointer<vtkLinearExtrusionFilter> extrude = vtkSmartPointer<vtkLinearExtrusionFilter>::New(); extrude->SetInputConnection(transformFilter->GetOutputPort()); extrude->SetExtrusionTypeToVector(); extrude->SetVector(0.0, 0.0, 1.0); extrude->SetScaleFactor(0.5); // 挤出厚度,单位跟随场景 vtkSmartPointer<vtkSTLWriter> writer = vtkSmartPointer<vtkSTLWriter>::New(); writer->SetFileName("text_label.stl"); writer->SetInputConnection(extrude->GetOutputPort()); writer->Write();这里用SetScaleFactor而不是SetVector是更稳妥的做法,因为SetVector的长度同时影响挤出方向和距离,而SetScaleFactor单独控制厚度,理解起来直观很多。挤出厚度一般设为 0.3 到 0.5 个世界单位就能在打印件上有清晰凸起效果;如果厚度提得太高,文字笔画之间的空隙会变浅,反而降低分辨率。
实测过一次 3D 打印文字之后,我对vtkVectorText的定位彻底变了:它不只是场景里的标注工具,还可以当作一个轻量级文字建模器用。配合vtkTransform对齐方向和位置,加上vtkSTLWriter输出,一套流程下来连 CAD 建模软件都不用打开,就能直接做出带文字的实体模型。
我后来做标注类可视化时,基本上固定这样一套流程:需要“真实存在于三维空间”的跟随标签,优先用vtkVectorText + vtkFollower;需要显示中文、字号固定、允许被遮挡的信息牌,用vtkTextActor3D;两者都搞不定的复杂排版需求,再考虑直接在 Qt 层叠加 2D 注释,但那已经是另一种开发思路了。VTK 的 3D Text 本身并不复杂,复杂的是你得想清楚这次点击鼠标、这次旋转视角、这次放大缩小之后,你希望那段文字还能不能保持住自己的形态。