摘要:本章系统介绍了 VulkanSceneGraph (VSG) 中用于高质量文字渲染的 vsg::text 模块。该模块已集成到 VSG 核心库,采用有向距离场 (SDF) 图集技术渲染字形,适用于 HUD、标注和调试信息等场景。文章详细讲解了核心类(Text、Font、TextGroup)的使用方法,包括字体加载、文字创建与渲染、性能优化注意事项(如视锥剔除)以及常见问题的解决方案,并提供了完整的代码示例。
第 17 章 文字渲染
本章定位:叠加文字(HUD、标注、调试信息)需要高质量字形渲染。
vsg::text模块(已集成到核心,非外部库)用 SDF(有向距离场)图集渲染文字。
17.1 本章目标
- 了解
vsg::text模块的核心类型:Text/Font/TextGroup; - 加载字体并创建一段文字;
- 理解
Text::setup()的作用; - 知道文字节点默认不包含视锥剔除,需要时用
CullNode/LOD装饰。
17.2 前置准备
- 第 6 章(场景图节点)已学习;
- 一个字体资源(
.ttf或预生成的字体图集)。文本模块由 VSG 核心提供,无需额外链接。
17.3 vsg::text 模块构成
| 类型 | 说明 |
|---|---|
Text | 文字节点(继承 Node),渲染单段文字 |
Font | SDF 字形图集(包含 atlas、glyphMetrics、charmap) |
TextLayout | 文字布局(位置/对齐/换行) |
TextTechnique | 渲染后端(CpuLayoutTechnique / GpuLayoutTechnique) |
TextGroup | 多段文字容器(批量/实例化字形) |
createTextShaderSet(options) | 生成文字专用 ShaderSet |
⚠️
vsg::text已并入核心仓库(include/vsg/text/),不要再去find_package(vsgText)。
17.4 Text 的关键成员
class Text : public Inherit<Node, Text> {
ref_ptr<Font> font;
ref_ptr<ShaderSet> shaderSet;
ref_ptr<TextTechnique> technique;
ref_ptr<TextLayout> layout;
ref_ptr<Data> text; // 字符数据(stringValue / ushortArray / ...)
void setup(uint32_t minimumAllocation = 0, ref_ptr<const Options> options = {});
};
text 是字符数据,常用 vsg::stringValue::create("Hello VSG");font 是 SDF 字体图集;setup() 会创建真正的渲染后端(technique),必须在挂入场景图前调用。
17.5 加载字体
字体图集通过文本模块的读取器加载(通常用 vsg::read 读取 .ttf 或预生成的字体资源):
auto font = vsg::read<vsg::Font>("fonts/arial.ttf");
if (!font) { std::cerr << "字体加载失败\n"; return 1; }
字体资源具体格式取决于文本模块提供的读取器;若你的资产是预生成的 SDF 字体图集,直接用对应扩展名读取即可。生产环境推荐预生成图集以避免运行时栅格化开销。
17.6 创建并渲染一段文字
auto font = vsg::read<vsg::Font>("fonts/arial.ttf");
auto text = vsg::Text::create();
text->font = font;
text->text = vsg::stringValue::create("Hello, VulkanSceneGraph!");
text->setup(); // 关键:创建渲染后端(technique)
// 用文本专用 ShaderSet(可选;也可复用 Options 中的 shaderSet["text"])
auto textShader = vsg::createTextShaderSet();
// 让文字的 StateGroup 使用该 shaderSet(见下方说明)
root->addChild(text);
Text默认带一个technique,它内部已经把几何体/状态准备好;若需要自定义管线,可在setup()前给text->shaderSet赋值(如createTextShaderSet(options)的结果)。
17.7 TextGroup:多段文字
需要同时渲染多行/多块文字时,用 TextGroup 把它们合成一组,便于批量更新与实例化:
auto textGroup = vsg::TextGroup::create();
textGroup->font = font;
textGroup->addChild(text1);
textGroup->addChild(text2);
textGroup->setup();
17.8 性能与裁剪注意
Text默认不带视锥剔除与 LOD(头文件注释明确说明);- 若文字出现在大场景中,用
CullNode/LOD装饰Text,并在setup()后用technique->extents()设置包围体; - 频繁变动的文字(如 FPS)每帧更新
text数据即可,setup()只在首次或字号/字体变化时调用。
下面是一个具体的代码示例,展示如何为 Text 节点添加 CullNode 装饰,并使用 technique->extents() 设置包围体以实现视锥剔除:
// 创建文字节点
auto text = vsg::Text::create();
text->font = font;
text->text = vsg::stringValue::create("场景中的文字标签");
text->setup(); // 必须先调用 setup() 以创建 technique
// 获取 technique 并计算包围体
if (auto technique = text->technique)
{
// 获取文字的实际包围盒(在局部坐标系中)
vsg::box extents = technique->extents();
// 如果需要,可以手动调整包围盒大小(例如添加边距)
// extents.min -= vsg::vec3(0.1f, 0.1f, 0.0f);
// extents.max += vsg::vec3(0.1f, 0.1f, 0.0f);
// 创建 CullNode 并设置包围体
auto cullNode = vsg::CullNode::create(extents);
cullNode->addChild(text);
// 将 CullNode 添加到场景图中
root->addChild(cullNode);
}
else
{
// 如果没有 technique,直接添加 text(无剔除)
root->addChild(text);
}
关键点说明:
setup()调用时机:必须在调用technique->extents()之前执行text->setup(),否则technique为空指针。- 包围体计算:
technique->extents()返回的是文字在局部坐标系中的轴对齐包围盒(AABB),考虑了当前字体、字号和文字内容。 - CullNode 作用:
CullNode会根据相机视锥体自动剔除其子节点,当文字完全在视锥体外时,VSG 不会提交渲染命令。 - 包围盒调整:如果文字有动态效果(如动画、缩放),可能需要手动扩大包围盒以确保文字始终在剔除范围内。
进阶用法:结合 LOD
// 创建不同细节级别的文字(例如不同字号)
auto textHigh = vsg::Text::create();
textHigh->font = font;
textHigh->text = vsg::stringValue::create("高细节文字");
textHigh->setup();
auto textLow = vsg::Text::create();
textLow->font = font;
textLow->text = vsg::stringValue::create("低细节文字");
textLow->setup();
// 创建 LOD 节点
auto lod = vsg::LOD::create();
lod->addChild(vsg::LOD::Child{0.0, 50.0, textHigh}); // 0-50 米用高细节
lod->addChild(vsg::LOD::Child{50.0, 200.0, textLow}); // 50-200 米用低细节
// 用 CullNode 包装 LOD
auto cullNode = vsg::CullNode::create(technique->extents());
cullNode->addChild(lod);
root->addChild(cullNode);
17.9 完整示例:场景中央显示文字
本节提供两个完整的文字渲染示例:第一个是简单的 2D HUD 文字显示,第二个是更复杂的 3D 场景文字标注实战。
17.9.1 基础示例:2D HUD 文字
#include <vsg/all.h>
#include <vsg/text/Font.h>
#include <vsg/text/Text.h>
int main(int argc, char** argv)
{
// 1. 创建窗口和查看器
auto traits = vsg::WindowTraits::create(800, 600, "VSG Text - 2D HUD");
traits->debugLayer = true;
traits->apiDumpLayer = false;
auto window = vsg::Window::create(traits);
if (!window)
{
std::cerr << "无法创建窗口" << std::endl;
return 1;
}
auto viewer = vsg::Viewer::create();
viewer->addWindow(window);
// 2. 加载字体
auto font = vsg::read<vsg::Font>("fonts/arial.ttf");
if (!font)
{
std::cerr << "字体加载失败,请确保 fonts/arial.ttf 文件存在" << std::endl;
return 1;
}
// 3. 创建文字节点
auto text = vsg::Text::create();
text->font = font;
text->text = vsg::stringValue::create("VulkanSceneGraph 文字渲染");
text->setup(); // 关键:必须在添加到场景图前调用
// 4. 创建场景图
auto root = vsg::Group::create();
root->addChild(text);
// 5. 创建正交相机(2D HUD)
auto camera = vsg::Camera::create();
camera->projectionMatrix = vsg::Orthographic::create(-1.0, 1.0, -1.0, 1.0, 0.0, 10.0);
camera->viewMatrix = vsg::LookAt::create(vsg::dvec3(0, 0, 1), vsg::dvec3(0, 0, 0), vsg::dvec3(0, 1, 0));
camera->viewportState = vsg::ViewportState::create(window->extent2D());
// 6. 创建渲染图
auto commandGraph = vsg::createCommandGraphForView(window, camera, root);
viewer->assignRecordAndSubmitTaskAndPresentation({commandGraph});
// 7. 编译和运行
auto cr = viewer->compile();
if (!cr)
{
std::cerr << "编译失败" << std::endl;
return 1;
}
// 主循环
while (viewer->advanceToNextFrame())
{
viewer->handleEvents();
viewer->update();
viewer->recordAndSubmit();
viewer->present();
}
return 0;
}
上例用正交相机把文字当 2D HUD 显示;若要 3D 世界中漂浮的文字,改用透视相机并把
Text放进对应MatrixTransform。
17.9.2 实战示例:3D 场景文字标注(Billboard 技术)
以下是一个完整的、可运行的 3D 场景文字标注示例,创建一个 3D 立方体,并在其上方 1 米处用 Text 节点添加一个动态的、始终朝向相机的"标签"文字,使用 MatrixTransform 和 Billboard 技术实现:
#include <vsg/all.h>
#include <vsg/text/Font.h>
#include <vsg/text/Text.h>
#include <vsg/nodes/MatrixTransform.h>
#include <vsg/maths/transform.h>
#include <iostream>
// Billboard 更新回调:使文字始终朝向相机
class BillboardCallback : public vsg::Visitor
{
public:
BillboardCallback(vsg::ref_ptr<vsg::MatrixTransform> textTransform,
vsg::ref_ptr<vsg::Camera> camera)
: _textTransform(textTransform), _camera(camera) {}
void apply(vsg::FrameEvent& frameEvent) override
{
if (!_textTransform || !_camera) return;
// 获取相机视图矩阵
auto viewMatrix = _camera->viewMatrix->transform();
// 提取相机的旋转部分(去除平移和缩放)
vsg::dmat4 rotationMatrix = viewMatrix;
rotationMatrix[3] = vsg::dvec4(0.0, 0.0, 0.0, 1.0); // 移除平移
// 计算 Billboard 矩阵:文字位置 + 相机旋转的逆(使文字朝向相机)
vsg::dmat4 billboardMatrix = vsg::translate(0.0, 1.0, 0.0) * // 在立方体上方 1 米
vsg::inverse(rotationMatrix);
_textTransform->matrix = billboardMatrix;
}
private:
vsg::ref_ptr<vsg::MatrixTransform> _textTransform;
vsg::ref_ptr<vsg::Camera> _camera;
};
int main(int argc, char** argv)
{
// 1. 创建窗口和查看器
auto traits = vsg::WindowTraits::create(1024, 768, "VSG 3D 文字标注");
traits->debugLayer = true;
traits->apiDumpLayer = false;
auto window = vsg::Window::create(traits);
if (!window)
{
std::cerr << "无法创建窗口" << std::endl;
return 1;
}
auto viewer = vsg::Viewer::create();
viewer->addWindow(window);
// 2. 加载字体
auto font = vsg::read<vsg::Font>("fonts/arial.ttf");
if (!font)
{
std::cerr << "字体加载失败,请确保 fonts/arial.ttf 文件存在" << std::endl;
std::cerr << "可以尝试使用绝对路径,如:/usr/share/fonts/truetype/arial.ttf" << std::endl;
return 1;
}
// 3. 创建 3D 立方体
auto cube = vsg::createBox(vsg::vec3(-0.5f, -0.5f, -0.5f), vsg::vec3(0.5f, 0.5f, 0.5f));
auto cube_descriptors = vsg::DescriptorSet::create(
vsg::DescriptorSetLayouts{{0, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, nullptr}},
vsg::Descriptors{vsg::DescriptorBuffer::create(vsg::vec4Array::create({{1.0f, 0.5f, 0.2f, 1.0f}}), 0, 0)});
auto cube_stateGroup = vsg::StateGroup::create();
cube_stateGroup->add(vsg::BindDescriptorSet::create(VK_PIPELINE_BIND_POINT_GRAPHICS,
viewer->getOrCreatePipelineLayout(),
0, cube_descriptors));
cube_stateGroup->addChild(cube);
// 4. 创建文字标签(Billboard)
auto text = vsg::Text::create();
text->font = font;
text->text = vsg::stringValue::create("3D 立方体标签");
text->layout->horizontalAlignment = vsg::TextLayout::CENTER_ALIGNMENT; // 水平居中
text->layout->verticalAlignment = vsg::TextLayout::CENTER_ALIGNMENT; // 垂直居中
text->setup();
// 5. 创建文字变换节点(初始位置在立方体上方 1 米处)
auto textTransform = vsg::MatrixTransform::create();
textTransform->matrix = vsg::translate(0.0, 1.0, 0.0); // 初始位置
textTransform->addChild(text);
// 6. 创建场景图
auto root = vsg::Group::create();
root->addChild(cube_stateGroup); // 立方体
root->addChild(textTransform); // 文字标签
// 7. 创建透视相机
auto camera = vsg::Camera::create();
// 透视投影
double aspectRatio = static_cast<double>(window->extent2D().width) /
static_cast<double>(window->extent2D().height);
camera->projectionMatrix = vsg::Perspective::create(60.0, aspectRatio, 0.1, 100.0);
// 相机位置:从 (0, 2, 5) 看向原点
camera->viewMatrix = vsg::LookAt::create(
vsg::dvec3(0.0, 2.0, 5.0), // 眼睛位置
vsg::dvec3(0.0, 0.0, 0.0), // 观察点
vsg::dvec3(0.0, 1.0, 0.0) // 上方向
);
camera->viewportState = vsg::ViewportState::create(window->extent2D());
// 8. 创建 Billboard 回调
auto billboardCallback = vsg::BillboardCallback::create(textTransform, camera);
viewer->addEventHandler(billboardCallback);
// 9. 添加相机控制器(方便交互)
auto trackball = vsg::Trackball::create(camera);
viewer->addEventHandler(trackball);
// 10. 创建渲染图
auto commandGraph = vsg::createCommandGraphForView(window, camera, root);
viewer->assignRecordAndSubmitTaskAndPresentation({commandGraph});
// 11. 编译和运行
auto cr = viewer->compile();
if (!cr)
{
std::cerr << "编译失败" << std::endl;
return 1;
}
std::cout << "3D 文字标注示例运行中..." << std::endl;
std::cout << "鼠标拖拽:旋转视角" << std::endl;
std::cout << "鼠标滚轮:缩放" << std::endl;
std::cout << "注意观察:文字标签始终朝向相机(Billboard 效果)" << std::endl;
// 主循环
while (viewer->advanceToNextFrame())
{
viewer->handleEvents();
viewer->update();
viewer->recordAndSubmit();
viewer->present();
}
return 0;
}
代码说明:
- Billboard 技术:通过
BillboardCallback类在每帧更新文字变换矩阵,使文字始终朝向相机。关键步骤是提取相机视图矩阵的旋转部分并取其逆矩阵。 - 文字位置:文字初始位置在立方体上方 1 米处(
vsg::translate(0.0, 1.0, 0.0)),通过MatrixTransform节点实现。 - 文字对齐:设置
horizontalAlignment和verticalAlignment为CENTER_ALIGNMENT,使文字在中心点对齐。 - 3D 场景:使用透视投影(
vsg::Perspective)创建 3D 场景,包含一个彩色立方体和一个始终朝向相机的文字标签。 - 交互控制:添加
Trackball控制器,支持鼠标拖拽旋转和滚轮缩放,方便观察 Billboard 效果。 - 资源管理:完整包含资源加载、相机设置、渲染循环和错误处理。
运行效果:
- 窗口中央显示一个橙色立方体
- 立方体上方 1 米处显示"3D 立方体标签"文字
- 当用户旋转相机时,文字标签会自动调整方向,始终正面朝向相机
- 文字位置相对于立方体保持固定(上方 1 米)
编译运行:
# 编译(假设使用 CMake)
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
运行(确保 fonts/arial.ttf 存在)
./3d_text_label
17.10 常见问题
| 现象 | 原因 | 解决 |
|---|---|---|
| 文字不显示 | 没调用 setup(),或 font 为空 | 挂入场景前调用 text->setup(),确认 font 已加载 |
| --- | --- | --- |
编译报 vsgText 找不到 | 误把 text 当外部库 | text 已并入核心,直接 #include <vsg/text/Text.h> |
| 文字被裁掉 | 默认无视锥剔除,或正交范围不对 | 用 CullNode 装饰;检查正交投影范围 |
| 字体模糊 | SDF 图集分辨率不足 | 用更高分辨率的预生成字体图集 |
诊断示例:字体加载失败
当字体文件路径错误或格式不支持时,vsg::read<vsg::Font>() 会返回空指针。建议使用以下代码进行诊断:
#include <vsg/text/Font.h>
#include <vsg/io/read.h>
#include <iostream>
// 1. 检查 font 指针
auto font = vsg::read<vsg::Font>("fonts/arial.ttf");
if (!font)
{
// 使用 VSG 日志系统输出错误
vsg::error("字体加载失败:无法读取 fonts/arial.ttf");
// 检查文件是否存在
if (!vsg::fileExists("fonts/arial.ttf"))
{
vsg::warn("字体文件不存在,请检查路径:fonts/arial.ttf");
}
// 尝试列出支持的扩展名
auto supported = vsg::ReaderWriter::supportedExtensions();
vsg::info("VSG 支持的字体扩展名:");
for (auto& ext : supported)
{
if (ext.find("ttf") != std::string::npos ||
ext.find("otf") != std::string::npos ||
ext.find("font") != std::string::npos)
{
vsg::info(" - ", ext);
}
}
return 1; // 退出或使用默认字体
}
// 2. 验证字体属性
vsg::info("字体加载成功:", font->className());
vsg::info("字符集大小:", font->charmap.size());
vsg::info("图集尺寸:", font->atlas->width(), "x", font->atlas->height());
诊断示例:文字不显示
文字不显示通常是因为 setup() 未调用或调用时机不当。以下代码展示完整的诊断流程:
#include <vsg/text/Text.h>
#include <iostream>
// 创建文字节点
auto text = vsg::Text::create();
text->font = font;
text->text = vsg::stringValue::create("测试文字");
// 1. 检查 font 指针
if (!text->font)
{
vsg::error("Text 节点的 font 指针为空!");
vsg::info("请确保:1) 字体文件存在 2) vsg::read 成功返回");
return;
}
// 2. 调用 setup() 并检查状态
text->setup();
// 3. 验证 setup() 后的关键成员
if (!text->technique)
{
vsg::error("Text::setup() 后 technique 仍为空!");
vsg::info("可能原因:");
vsg::info(" - ShaderSet 未正确配置");
vsg::info(" - 显卡不支持所需的 Vulkan 特性");
vsg::info(" - Options 中的 allocator 配置错误");
// 检查 shaderSet
if (!text->shaderSet)
{
vsg::warn("shaderSet 为空,尝试使用默认文本着色器");
text->shaderSet = vsg::createTextShaderSet();
text->setup(); // 重新 setup
}
}
// 4. 检查渲染状态
if (auto technique = text->technique)
{
vsg::info("Text technique 类型:", technique->className());
// 检查包围体
auto extents = technique->extents();
vsg::info("文字包围体:min=", extents.min, ", max=", extents.max);
if (extents.min == extents.max)
{
vsg::warn("文字包围体为零体积,可能文字内容为空或字体未包含所需字符");
}
}
else
{
vsg::error("无法获取 technique,文字将无法渲染");
}
// 5. 添加到场景前的最终检查
if (text->font && text->technique)
{
vsg::info("文字节点准备就绪,可以添加到场景图");
root->addChild(text);
}
else
{
vsg::fatal("文字节点初始化失败,请检查上述错误信息");
}
VSG 日志系统使用提示:
vsg::error():输出错误信息(红色)vsg::warn():输出警告信息(黄色)vsg::info():输出一般信息(白色)vsg::debug():输出调试信息(需启用 VSG_DEBUG 宏)vsg::fatal():输出致命错误并终止程序
通过组合使用指针检查、setup() 后状态验证和 VSG 日志系统,可以快速定位字体加载和文字显示问题。
17.11 小结
vsg::text已并入核心,用 SDF 图集高质量渲染文字;Text需设font+text,并在挂入前setup();- 字体经文本模块读取器加载;多段文字用
TextGroup; Text默认无视锥剔除,大场景用CullNode/LOD装饰。
17.12 延伸阅读与下一章预告
- 第 22 章《ImGui 集成》:另一种更灵活的 2D UI/调试面板方案;
- 第 6 章《场景图基础》:
CullNode/LOD装饰文字; - 第 24 章《模型查看器》:在查看器里叠加标注文字。
&spm=1001.2101.3001.5002&articleId=163864592&d=1&t=3&u=cefd5a2057824a45af8aca570c74e016)
193

被折叠的 条评论
为什么被折叠?



