VulkanSceneGraph学习教程(十七)

摘要:本章系统介绍了 VulkanSceneGraph (VSG) 中用于高质量文字渲染的 vsg::text 模块。该模块已集成到 VSG 核心库,采用有向距离场 (SDF) 图集技术渲染字形,适用于 HUD、标注和调试信息等场景。文章详细讲解了核心类(TextFontTextGroup)的使用方法,包括字体加载、文字创建与渲染、性能优化注意事项(如视锥剔除)以及常见问题的解决方案,并提供了完整的代码示例。

​第 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),渲染单段文字
FontSDF 字形图集(包含 atlasglyphMetricscharmap
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);
}

关键点说明:

  1. setup() 调用时机:必须在调用 technique->extents() 之前执行 text->setup(),否则 technique 为空指针。
  2. 包围体计算technique->extents() 返回的是文字在局部坐标系中的轴对齐包围盒(AABB),考虑了当前字体、字号和文字内容。
  3. CullNode 作用CullNode 会根据相机视锥体自动剔除其子节点,当文字完全在视锥体外时,VSG 不会提交渲染命令。
  4. 包围盒调整:如果文字有动态效果(如动画、缩放),可能需要手动扩大包围盒以确保文字始终在剔除范围内。

进阶用法:结合 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;
}

代码说明:

  1. Billboard 技术:通过 BillboardCallback 类在每帧更新文字变换矩阵,使文字始终朝向相机。关键步骤是提取相机视图矩阵的旋转部分并取其逆矩阵。
  2. 文字位置:文字初始位置在立方体上方 1 米处(vsg::translate(0.0, 1.0, 0.0)),通过 MatrixTransform 节点实现。
  3. 文字对齐:设置 horizontalAlignmentverticalAlignmentCENTER_ALIGNMENT,使文字在中心点对齐。
  4. 3D 场景:使用透视投影(vsg::Perspective)创建 3D 场景,包含一个彩色立方体和一个始终朝向相机的文字标签。
  5. 交互控制:添加 Trackball 控制器,支持鼠标拖拽旋转和滚轮缩放,方便观察 Billboard 效果。
  6. 资源管理:完整包含资源加载、相机设置、渲染循环和错误处理。

运行效果:

  • 窗口中央显示一个橙色立方体
  • 立方体上方 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 章《模型查看器》:在查看器里叠加标注文字。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是慎独

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值