第 13 章 加载 3D 模型
摘要:本章系统介绍了在 VulkanSceneGraph (VSG) 中加载外部 3D 模型的核心流程。重点阐述了如何使用统一的 vsg::read() 函数加载 glTF/OBJ 等格式的模型,强调了 vsgXchange 组件库在提供格式支持中的关键作用,并详细说明了 CMake 的链接配置方法。此外,本章还解释了加载后得到的 Node 子树结构、如何通过 vsg::Options 控制加载行为(如坐标轴转换和资源路径),并提供了一个完整的模型查看器示例。最后,总结了常见问题的排查思路,为后续的模型变换、动画和纹理学习奠定了基础。
本章定位:实际项目很少手搓几何体——vsgXchange 组件库让 vsg::read() 直接吃 glTF / OBJ / 图像 / 字体。本章讲清「一行读取、挂入场景图」。
13.1 本章目标
- 用 vsg::read() 加载 glTF / OBJ 模型;
- 了解 vsgXchange 在背后提供的格式支持;
- 正确链接 vsgXchange 并配置 CMake;
- 知道加载进来的 Node 已经自带几何体、状态与描述符。
13.2 前置准备
- 第 3 章已装好 VSG 并安装了 vsgXchange;
- 第 6 章理解场景图节点。
13.3 统一入口:vsg::read()
VSG 的 IO 系统有一个统一读取函数(位于 vsg/io/read.h):
ref_ptr<Object> read(const Path& filename, ref_ptr<const Options> options = {});
它根据文件扩展名挑选已注册的 ReaderWriter。vsgXchange 在链接后自动注册对 glTF/OBJ/KTX2/PNG/字体等的支持。
// 读取 glTF(vsgXchange 提供支持)
auto object = vsg::read("models/duck.gltf");
if (!object)
{
std::cerr << "加载失败:请确认文件存在且已链接 vsgXchange" << std::endl;
return 1;
}
auto model = vsg::ref_ptr<vsg::Node>(object); // 转换为 Node 并挂入场景图
root->addChild(model);
⚠️ 必须链接 vsgXchange:核心库 vsg 只认自家的 .vsgt/.vsgb 等格式;glTF/OBJ/PNG 全靠 vsgXchange。没装它,vsg::read("x.gltf") 会返回 null。
13.4 CMake:链接 vsgXchange
find_package(vsg REQUIRED)
find_package(vsgXchange REQUIRED) # 额外引入模型/图像支持
target_link_libraries(my_app PRIVATE vsg::vsg vsgXchange::vsgXchange)
vsgXchange 会按需拉入 tinygltf、libpng、ktx 等底层依赖(多数通过 FetchContent 自动处理)。
13.5 加载进来的节点长什么样
vsgXchange 的 glTF 读取器会把文件解析成一棵完整的 Node 子树:
- 网格 → vsg::Geometry(顶点/索引/绘制命令,见第 7 章);
- 材质/纹理 → 自动生成 DescriptorImage + Sampler,挂到 StateGroup(见第 10 章);
- 变换层级 → vsg::MatrixTransform;
- 动画 → vsg::Animation + TransformSampler(见第 14 章)。
也就是说,你拿到的已经是「可渲染」的场景图,通常只需套一层 MatrixTransform 调整位置/缩放即可。
13.6 加载选项:vsg::Options
需要坐标系转换(glTF 是 Y-up,VSG 常 Z-up)、缩放、或指定字体/图像搜索路径时,传 Options:
auto options = vsg::Options::create();
options->paths = vsg::Paths{"models/", "textures/"}; // 资源搜索路径
options->fileCache = vsg::getFileCache(); // 共享已加载对象
auto model = vsg::read("duck.gltf", options);
glTF 与 VSG 的坐标轴差异通常由 vsgXchange 在读取时按 options 处理;遇到模型「躺平」,多半是 up-axis 没设对,检查 Options 的轴向/缩放设置。
13.7 完整示例:加载并查看一个模型
下面是一个完整的、可直接编译运行的 VSG 程序示例,演示如何加载 glTF 模型并创建一个简单的查看器。
#include <vsg/all.h>
#include <iostream>
int main(int argc, char** argv)
{
// 1. 解析命令行参数(模型文件路径)
vsg::CommandLine arguments(&argc, argv);
auto modelFile = arguments.value(std::string("models/duck.gltf"), "–model");
auto width = arguments.value(1024, "–width");
auto height = arguments.value(768, "–height");
// 2. 创建窗口和查看器
auto traits = vsg::WindowTraits::create();
traits->width = width;
traits->height = height;
traits->title = "VSG Model Viewer";
auto window = vsg::Window::create(traits);
if (!window)
{
std::cerr << "无法创建窗口" << std::endl;
return 1;
}
auto viewer = vsg::Viewer::create();
viewer->addWindow(window);
// 3. 加载模型(需要已链接 vsgXchange)
auto options = vsg::Options::create();
options->paths = vsg::getEnvPaths("VSG_FILE_PATH"); // 从环境变量获取搜索路径
options->paths.push_back("models/"); // 添加本地模型目录
options->paths.push_back("textures/"); // 添加纹理目录
auto model = vsg::read<vsg::Node>(modelFile, options);
if (!model)
{
std::cerr << "模型加载失败: " << modelFile << std::endl;
std::cerr << "请确认:" << std::endl;
std::cerr << " 1. 文件路径正确" << std::endl;
std::cerr << " 2. 已安装并链接 vsgXchange" << std::endl;
std::cerr << " 3. 依赖的纹理文件在搜索路径中" << std::endl;
return 1;
}
// 4. 创建场景图根节点
auto root = vsg::Group::create();
// 5. 添加变换节点调整模型位置和大小
auto transform = vsg::MatrixTransform::create();
// 将模型居中并适当缩放
transform->matrix = vsg::translate(0.0, 0.0, 0.0) * vsg::scale(0.5, 0.5, 0.5);
transform->addChild(model);
root->addChild(transform);
// 6. 设置相机
auto camera = vsg::Camera::create();
// 透视投影
double aspect = static_cast<double>(width) / static_cast<double>(height);
camera->projectionMatrix = vsg::Perspective::create(60.0, aspect, 0.1, 100.0);
// 观察矩阵:相机在 (0, 2, 5),看向原点,上方向为 Y 轴
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());
// 7. 创建渲染图和命令图
auto renderGraph = vsg::createRenderGraphForView(window, camera, root);
auto commandGraph = vsg::createCommandGraphForView(window, camera, root);
// 8. 配置查看器
viewer->assignRecordAndSubmitTaskAndPresentation({commandGraph});
// 9. 编译场景图
auto compileResult = viewer->compile();
if (!compileResult)
{
std::cerr << "场景图编译失败" << std::endl;
return 1;
}
// 10. 添加轨道相机控制器(方便交互查看)
auto trackball = vsg::Trackball::create(camera, window);
viewer->addEventHandler(trackball);
// 11. 主渲染循环
std::cout << "成功加载模型: " << modelFile << std::endl;
std::cout << "按 ESC 键退出程序" << std::endl;
while (viewer->advanceToNextFrame())
{
// 处理窗口事件(鼠标、键盘等)
viewer->handleEvents();
// 更新场景(动画、相机等)
viewer->update();
// 记录命令缓冲区并提交
viewer->recordAndSubmit();
// 呈现到屏幕
viewer->present();
}
return 0;
}
CMakeLists.txt 配置
确保你的 CMakeLists.txt 正确链接了 vsgXchange:
cmake_minimum_required(VERSION 3.12)
project(model_viewer)
find_package(vsg REQUIRED)
find_package(vsgXchange REQUIRED) # 关键:引入模型/图像支持
add_executable(model_viewer main.cpp)
target_link_libraries(model_viewer PRIVATE
vsg::vsg
vsgXchange::vsgXchange # 关键:链接 vsgXchange
)
运行说明
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=/path/to/vsg/install
make
./model_viewer –model models/duck.gltf –width 1280 –height 720
- 鼠标左键拖动:旋转模型
- 鼠标右键拖动:平移模型
- 鼠标滚轮:缩放
- ESC 键:退出程序
提示:如果模型显示异常(如颠倒、大小不合适),可以调整 transform->matrix 中的变换参数,或在 Options 中设置 upAxis 和 scale。
13.8 常见问题
| vsg::read 返回 null | 未链接 vsgXchange 或文件/路径错误 | 确认 target_link_libraries(… vsgXchange::vsgXchange);检查路径 |
| 模型颠倒/躺平 | up-axis / 缩放设置不正确 | 在 Options 中设置轴向与缩放,或使用外层 MatrixTransform 旋转 |
| 纹理丢失 | 纹理文件不在搜索路径中 | 使用 Options::paths 指定资源目录 |
| 编译报找不到 glTF 符号 | 只安装了核心库 | 安装并链接 vsgXchange |
13.9 性能优化与高级用法
在掌握了基础加载流程后,了解一些高级用法和性能优化技巧能让你在复杂项目中更高效地使用 VSG 的模型加载功能。
1. 资源复用:避免重复加载
当多个场景需要加载同一模型或纹理时,重复从磁盘读取会浪费 I/O 和内存。VSG 提供了两种资源复用机制:
使用 vsg::SharedObjects 全局共享
// 创建全局共享对象管理器
auto sharedObjects = vsg::SharedObjects::create();
// 第一次加载时注册到共享管理器
auto options = vsg::Options::create();
options->sharedObjects = sharedObjects;
auto model1 = vsg::read("models/duck.gltf", options);
// 后续加载相同文件时,直接从共享管理器获取
auto model2 = vsg::read("models/duck.gltf", options); // 复用已加载的资源
使用 Options::fileCache 文件级缓存
// 创建带文件缓存的 Options
auto options = vsg::Options::create();
options->fileCache = vsg::getFileCache(); // 获取全局文件缓存
// 多次读取同一文件,只有第一次会真正从磁盘加载
auto texture1 = vsg::read("textures/diffuse.png", options);
auto texture2 = vsg::read("textures/diffuse.png", options); // 从缓存获取
适用场景:
- SharedObjects:适用于运行时需要多次创建相同模型的场景(如游戏中的 NPC、道具)
- fileCache:适用于纹理、字体等静态资源的重复加载
2. 类型安全读取:vsg::read_cast<T>()
标准 vsg::read() 返回 ref_ptr<Object>,需要手动类型转换。使用 read_cast 可以在读取时直接进行类型检查:
// 传统方式:需要手动转换和检查
auto object = vsg::read("models/duck.gltf");
if (object)
{
auto node = vsg::ref_ptr<vsg::Node>(object); // 手动转换
if (node) root->addChild(node);
}
// 类型安全方式:直接获取指定类型
auto node = vsg::read_cast<vsg::Node>("models/duck.gltf");
if (node) root->addChild(node); // node 已经是正确的类型
// 也可以用于读取特定类型的资源
auto image = vsg::read_cast<vsg::Data>("textures/diffuse.png");
auto font = vsg::read_cast<vsg::Font>("fonts/arial.ttf");
优势:
- 编译时类型安全,避免运行时类型错误
- 代码更简洁,减少手动类型转换
- 清晰的意图表达,提高代码可读性
3. 异步加载:提升启动和切换性能
对于大型模型或网络资源,同步加载会阻塞主线程。VSG 提供了异步加载机制:
#include <vsg/threading/OperationThreads.h>
#include <vsg/io/read.h>
// 1. 创建操作线程池(建议在程序初始化时创建)
auto operationThreads = vsg::OperationThreads::create(2); // 2个工作线程
// 2. 创建支持异步的 Options
auto options = vsg::Options::create();
options->operationThreads = operationThreads;
// 3. 异步读取模型
auto future = vsg::readAsync<vsg::Node>("models/large_scene.gltf", options);
// 主线程可以继续做其他事情…
setupCamera();
setupLighting();
// 4. 需要模型时检查是否加载完成
if (future.valid() && future.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
{
auto model = future.get();
if (model) root->addChild(model);
}
else
{
// 可以显示加载进度条或占位模型
showLoadingIndicator();
// 阻塞等待(如果必须立即使用)
// auto model = future.get();
}
适用场景:
- 大型场景加载:游戏关卡切换、CAD 模型查看器
- 网络资源:从网络加载模型或纹理
- 后台预加载:预加载下一场景的资源
- 响应式 UI:避免加载卡住用户界面
注意事项:
- 异步加载的资源在添加到场景图前不能访问
- 需要合理管理线程池大小,避免创建过多线程
- 异步加载失败时,future.get() 可能抛出异常,需要适当处理
4. 组合使用示例
在实际项目中,通常会组合使用这些技术:
class ResourceManager {
public:
ResourceManager() {
// 创建共享对象和线程池
sharedObjects = vsg::SharedObjects::create();
operationThreads = vsg::OperationThreads::create(4);
options = vsg::Options::create();
options->sharedObjects = sharedObjects;
options->operationThreads = operationThreads;
options->fileCache = vsg::getFileCache();
}
vsg::ref_ptr<vsg::Node> loadModelAsync(const std::string& path) {
// 检查是否已缓存
if (auto cached = sharedObjects->get<vsg::Node>(path))
return cached;
// 异步加载
auto future = vsg::readAsync<vsg::Node>(path, options);
pendingFutures[path] = std::move(future);
return nullptr;
}
bool tryGetModel(const std::string& path, vsg::ref_ptr<vsg::Node>& outModel) {
if (auto it = pendingFutures.find(path); it != pendingFutures.end()) {
if (it->second.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
outModel = it->second.get();
pendingFutures.erase(it);
return true;
}
}
return false;
}
private:
vsg::ref_ptr<vsg::SharedObjects> sharedObjects;
vsg::ref_ptr<vsg::OperationThreads> operationThreads;
vsg::ref_ptr<vsg::Options> options;
std::unordered_map<std::string, std::future<vsg::ref_ptr<vsg::Node>>> pendingFutures;
};
性能建议:
- 小文件(< 10MB)使用同步加载 + 缓存即可
- 中等文件(10-100MB)考虑使用异步加载
- 超大文件(> 100MB)需要分块加载或使用流式加载(如果格式支持)
- 移动设备上适当减少线程池大小(1-2个线程)
13.9 小结
- vsg::read(path) 是统一入口,按扩展名选 ReaderWriter;
- glTF/OBJ/图像/字体支持来自组件库 vsgXchange,务必 find_package + 链接;
- 加载结果已是完整可渲染的 Node 子树,直接挂场景图即可;
- 用 vsg::Options 控制路径、轴向与缓存。
13.10 延伸阅读与下一章预告
- 第 14 章《变换与动画》:让加载的模型动起来(Animation + TransformSampler);
- 第 15 章《纹理与材质》:理解 glTF 里的纹理/材质是怎么变成 DescriptorImage 的;
- 第 24 章《简易 3D 模型查看器》:把本章读模型 + 轨道相机 + 光照整合成一个完整程序。
网硕互联帮助中心




评论前必须登录!
注册