VulkanSceneGraph学习教程(十一)

第 11 章 相机与视图

摘要: 本章系统介绍了 VulkanSceneGraph 中相机与视图的核心概念。相机(vsg::Camera)由投影矩阵(Perspective/Orthographic)、视图矩阵(LookAt)和视口状态三部分组成,决定了观察场景的视角与范围。视图(vsg::View)将相机与要渲染的场景子图绑定,作为渲染图(RenderGraph)的直接子节点。本章详细讲解了如何创建和配置相机、使用便捷函数实现窗口缩放同步、驱动相机动画以及实现多视图渲染,并提供了常见问题的解决方案。

本章定位 :相机决定「从哪看、看多大范围」。Camera 由投影矩阵、视图矩阵、视口三部分组成;View 把相机与一段子图配对,是 RenderGraph 的直接子节点。


11.1 本章目标

  • 理解 vsg::Camera 的三个核心成员(投影 / 视图 / 视口);
  • 会用 vsg::Perspective / vsg::Orthographicvsg::LookAt
  • 理解 vsg::View 把「相机 + 子图」绑定在一起;
  • 知道窗口缩放时视口如何自动更新,以及每帧如何驱动相机动画。

11.2 前置准备

  • 第 6 章(场景图)与第 12 章(RenderGraph/CommandGraph,建议先读概念)已读;
  • 了解投影矩阵 / 视图矩阵的基本概念。

11.3 Camera 的三个成员

vsg::Camera 持有一个相机所需的全部矩阵与视口:

成员 类型 作用
projectionMatrix ref_ptr<ProjectionMatrix> 投影(透视/正交)
viewMatrix ref_ptr<ViewMatrix> 视图(相机世界变换的逆)
viewportState ref_ptr<ViewportState> 视口与裁剪区域
复制代码
auto camera = vsg::Camera::create();
camera->projectionMatrix = vsg::Perspective::create(
    60.0,   // 垂直视场角 (度)
    static_cast<double>(window->extent2D().width) / window->extent2D().height, // 宽高比
    0.1,    // 近平面
    10.0);  // 远平面
camera->viewMatrix = vsg::LookAt::create(
    vsg::dvec3(0.0, 0.0, 3.0),  // eye 眼睛位置
    vsg::dvec3(0.0, 0.0, 0.0),  // center 注视点
    vsg::dvec3(0.0, 1.0, 0.0)); // up 上方向
camera->viewportState = vsg::ViewportState::create(window->extent2D());

11.4 投影矩阵:PerspectiveOrthographic

  • vsg::Perspective::create(fovY, aspectRatio, near, far):透视投影(近大远小);
  • vsg::Orthographic::create(left, right, bottom, top, near, far):正交投影(无透视,适合 2D / CAD)。

二者都继承 ProjectionMatrix,可作为 camera->projectionMatrix 赋给相机。

⚠️ 宽高比 :务必用窗口真实宽高比(window->extent2D().width / height),否则画面会被拉伸。用 createRenderGraphForView 时,缩放由 WindowResizeHandler 自动同步(见第 12 章)。


11.5 视图矩阵:LookAt

vsg::LookAt::create(eye, center, up) 生成「看向 center」的视图矩阵(内部是相机世界变换的逆)。参数为 dvec3(双精度),适合大世界坐标。


11.6 View:相机 + 子图

vsg::View 是一个 Group,把相机要渲染的子图 配对,并作为 RenderGraph 的子节点:

复制代码
auto view = vsg::View::create(camera, scenegraph);
// View 是 Group,可继续 addChild;viewID 在构造时自动分配

View 的关键成员:cameraviewID(自动分配,用于区分多视图)、mask(遍历掩码)、features(如 RECORD_LIGHTS / RECORD_SHADOW_MAPS)、overridePipelineStates

复制代码
// 典型装配(推荐用便捷函数,见第 12 章)
auto renderGraph = vsg::createRenderGraphForView(window, camera, scenegraph);

createRenderGraphForView 内部会创建 View,并接好 WindowResizeHandler 把相机视口与窗口缩放同步。


11.7 驱动相机:动画与多视图

每帧更新(轨道相机)

复制代码
// 帧循环里
double t = ...;
camera->viewMatrix = vsg::LookAt::create(
    vsg::dvec3(std::sin(t)*3.0, 0.0, std::cos(t)*3.0), // 绕原点旋转
    vsg::dvec3(0.0, 0.0, 0.0),
    vsg::dvec3(0.0, 1.0, 0.0));
// 重新 compile / updateViewer 视情况而定(纯 viewMatrix 改动通常无需重编译,录制时实时读取)

完整相机动画示例

下面是一个完整的、可运行的 VSG 相机动画示例,包含三种驱动方式:

cpp 复制代码
#include <vsg/all.h>
int main(int argc, char** argv)
{
// 1. 创建窗口与场景
auto windowTraits = vsg::WindowTraits::create();
windowTraits->width = 1280;
windowTraits->height = 720;
auto window = vsg::Window::create(windowTraits);
// 简单场景:一个立方体
auto scenegraph = vsg::Group::create();
auto transform = vsg::MatrixTransform::create();
auto cube = vsg::Builder::createBox(vsg::vec3(0.0f, 0.0f, 0.0f), 1.0f);
transform-&gt;addChild(cube);
scenegraph-&gt;addChild(transform);
// 2. 创建相机
auto camera = vsg::Camera::create();
camera-&gt;projectionMatrix = vsg::Perspective::create(
60.0,
static_cast&lt;double&gt;(window-&gt;extent2D().width) / window-&gt;extent2D().height,
0.1,
100.0);
camera-&gt;viewMatrix = vsg::LookAt::create(
vsg::dvec3(0.0, 0.0, 5.0),  // 初始位置
vsg::dvec3(0.0, 0.0, 0.0),  // 注视点
vsg::dvec3(0.0, 1.0, 0.0)); // 上方向
camera-&gt;viewportState = vsg::ViewportState::create(window-&gt;extent2D());
// 3. 创建渲染图
auto renderGraph = vsg::createRenderGraphForView(window, camera, scenegraph);
auto commandGraph = vsg::createCommandGraphForView(window, camera, scenegraph);
auto viewer = vsg::Viewer::create();
viewer-&gt;addWindow(window);
viewer-&gt;assignRecordAndSubmitTaskAndPresentation({commandGraph});
// 4. 方法一:帧循环中直接更新相机位置(轨道相机)
auto updateCamera = [&amp;](vsg::Camera&amp; cam, double time) {
// 绕Y轴旋转的轨道相机
double radius = 5.0;
double angle = time * 0.5; // 每秒旋转180度
vsg::dvec3 eye(
std::sin(angle) * radius,
2.0,  // 保持一定高度
std::cos(angle) * radius
);
cam.viewMatrix = vsg::LookAt::create(eye, vsg::dvec3(0.0, 0.0, 0.0), vsg::dvec3(0.0, 1.0, 0.0));
// 注意:仅修改 viewMatrix 通常不需要调用 viewer-&amp;gt;compile()
// 因为 viewMatrix 在录制命令时实时读取,不涉及管线状态变更
// 但如果修改了 projectionMatrix 或 viewportState,可能需要重编译
};
// 5. 方法二:使用 vsg::AnimationPath(预定义路径)
auto animationPath = vsg::AnimationPath::create();
animationPath-&gt;add(vsg::AnimationPath::KeyTime{0.0, vsg::dvec3(0.0, 0.0, 5.0)});
animationPath-&gt;add(vsg::AnimationPath::KeyTime{5.0, vsg::dvec3(5.0, 2.0, 0.0)});
animationPath-&gt;add(vsg::AnimationPath::KeyTime{10.0, vsg::dvec3(0.0, 0.0, -5.0)});
animationPath-&gt;add(vsg::AnimationPath::KeyTime{15.0, vsg::dvec3(-5.0, 2.0, 0.0)});
animationPath-&gt;add(vsg::AnimationPath::KeyTime{20.0, vsg::dvec3(0.0, 0.0, 5.0)});
// 6. 方法三:使用 vsg::MatrixTransform 驱动相机节点
// 将相机挂载到变换节点上,通过动画系统驱动变换
auto cameraTransform = vsg::MatrixTransform::create();
cameraTransform-&gt;matrix = vsg::translate(0.0, 0.0, 5.0);
// 可以给 cameraTransform 添加 vsg::Animation 或 vsg::KeyFrameAnimation
// 7. 帧循环
viewer-&gt;compile();
auto startTime = vsg::clock::now();
while (viewer-&gt;advanceToNextFrame())
{
double time = std::chrono::duration&lt;double&gt;(vsg::clock::now() - startTime).count();
// 使用方法一:直接更新
updateCamera(*camera, time);
// 或者使用方法二:从动画路径获取位置
// auto position = animationPath-&amp;gt;interpolate(time);
// camera-&amp;gt;viewMatrix = vsg::LookAt::create(position, vsg::dvec3(0.0, 0.0, 0.0), vsg::dvec3(0.0, 1.0, 0.0));
// 或者使用方法三:更新变换矩阵
// double angle = time * 0.3;
// cameraTransform-&amp;gt;matrix = vsg::rotate(angle, vsg::dvec3(0.0, 1.0, 0.0)) * vsg::translate(0.0, 0.0, 5.0);
// camera-&amp;gt;viewMatrix = vsg::inverse(cameraTransform-&amp;gt;matrix);
// 何时需要调用 viewer-&amp;gt;compile() 或 viewer-&amp;gt;updateViewer():
// 1. 首次创建渲染图后必须调用 viewer-&amp;gt;compile()
// 2. 修改了 projectionMatrix、viewportState 或任何影响管线状态的内容后需要重编译
// 3. 仅修改 viewMatrix 通常不需要重编译,因为它在每帧录制时读取
// 4. viewer-&amp;gt;updateViewer() 在添加/移除节点、修改场景结构时调用
viewer-&amp;gt;handleEvents();
viewer-&amp;gt;update();
viewer-&amp;gt;recordAndSubmit();
viewer-&amp;gt;present();
}
return 0;
}

关键说明:

  1. 帧循环更新 :示例展示了在 while (viewer->advanceToNextFrame()) 循环中更新相机位置的完整逻辑。
  2. 三种驱动方案
    • 直接计算:在帧循环中实时计算相机位置(如轨道相机)。
    • vsg::AnimationPath:预定义关键帧路径,适合固定轨迹的相机运动。
    • vsg::MatrixTransform:将相机作为变换节点的子节点,通过动画系统驱动变换矩阵。
  3. 编译与更新时机
    • viewer->compile() :首次创建渲染图后必须调用;修改 projectionMatrixviewportState 或任何管线状态后需要重编译。
    • viewer->updateViewer():添加/移除场景节点、修改场景结构时调用。
    • 仅改 viewMatrix:通常无需重编译,因为视图矩阵在每帧命令录制时实时读取。

每帧更新(轨道相机)

复制代码
// 帧循环里
double t = ...;
camera->viewMatrix = vsg::LookAt::create(
    vsg::dvec3(std::sin(t)*3.0, 0.0, std::cos(t)*3.0), // 绕原点旋转
    vsg::dvec3(0.0, 0.0, 0.0),
    vsg::dvec3(0.0, 1.0, 0.0));
// 重新 compile / updateViewer 视情况而定(纯 viewMatrix 改动通常无需重编译,录制时实时读取)

性能与边界条件

在实现相机动画与多视图时,除了掌握驱动方案,还需关注性能与数值精度等边界条件,避免出现渲染异常或性能瓶颈。

1. 频繁更新 viewMatrix 的性能影响与优化

每帧更新 viewMatrix 是相机动画的常见操作。虽然 VSG 在命令录制时实时读取视图矩阵,避免了管线重编译,但频繁赋值仍会触发场景图遍历与状态更新。若场景复杂或视图数量多,可能影响帧率。

优化建议:

  • 使用 dirty() 标记 :当相机节点(或包含相机的变换节点)被修改后,调用 camera->dirty()view->dirty() 标记该分支为"脏"状态。这能确保 VSG 的增量更新机制仅处理受影响的部分,避免全图遍历。
  • 批量更新:若同一帧内需要更新多个相机的视图矩阵,尽量在一次遍历中完成,减少状态切换开销。
  • 避免冗余计算:在动画循环中,若相机位置未变化(例如相机静止或动画暂停),可跳过矩阵重算与赋值。
cpp 复制代码
// 优化示例:仅在位置变化时更新并标记脏状态
void updateCameraIfNeeded(vsg::ref_ptr<vsg::Camera> camera, const vsg::dvec3& newEye) {
    static vsg::dvec3 lastEye;
    if (newEye != lastEye) {
        camera->viewMatrix = vsg::LookAt::create(newEye, vsg::dvec3(0.0, 0.0, 0.0), vsg::dvec3(0.0, 1.0, 0.0));
        camera->dirty(); // 标记相机为脏,触发增量更新
        lastEye = newEye;
    }
}
2. 投影矩阵 near/far 平面设置与 Z-fighting

投影矩阵的 near(近平面)和 far(远平面)值设置不当会导致深度缓冲精度问题,常见现象为 Z-fighting(闪烁)或物体在特定距离消失。

常见问题与建议:

  • near 值过小或为 0:near 值不能为 0 或极小的正数(如 1e-6)。过小的 near 值会大幅压缩近处的深度精度,导致靠近相机的物体出现 Z-fighting。建议 near ≥ 0.1(单位与场景尺度一致)。
  • far 值过大:far 值过大(如 1e6)会拉伸深度范围,降低整体深度精度。在透视投影中,深度非线性分布,远处精度本就较低,过大的 far 会加剧远处物体的 Z-fighting。
  • near/far 比值过大:深度缓冲精度有限(通常为 24 位)。若 far/near 比值超过 10⁶,深度误差将变得明显。建议 far/near ≤ 10⁵,并尽量根据场景实际可见范围设置。

推荐值范围:

  • 透视投影 :near 通常在 0.1~1.0 之间,far 根据场景大小设定,一般不超过 1000.0。例如:vsg::Perspective::create(60.0, aspect, 0.1, 100.0)
  • 正交投影 :near/far 可设置得更大,但仍需避免比值过大。例如:vsg::Orthographic::create(-10, 10, -10, 10, 0.1, 1000.0)
3. 大世界坐标与双精度(dvec3)使用

当场景坐标范围很大(如地理信息系统、太空模拟)时,使用单精度浮点数(floatvec3)会导致严重的精度丢失,表现为相机抖动、物体位置漂移或深度测试异常。

必要提醒:

  • VSG 原生支持双精度vsg::LookAt::create()vsg::dvec3vsg::dmat4 等均使用双精度(double)。在大世界场景中,务必使用双精度类型定义相机位置、注视点等参数。
  • 矩阵计算精度 :视图矩阵与投影矩阵的计算应全程使用双精度,避免中间转换丢失精度。VSG 的矩阵运算(如 vsg::inverse())会自动保持输入精度。
  • 着色器中的精度处理:虽然 CPU 端使用双精度,但 GPU 着色器通常只支持单精度。需通过"相对坐标"或"相机相对偏移"策略,将世界坐标转换为相机相对坐标后再传入着色器,以保持渲染精度。
cpp 复制代码
// 大世界坐标下推荐使用双精度
vsg::dvec3 eye(1000000.0, 50000.0, 200000.0); // 大坐标
vsg::dvec3 center(1000100.0, 50000.0, 200100.0);
camera->viewMatrix = vsg::LookAt::create(eye, center, vsg::dvec3(0.0, 1.0, 0.0));
// 错误示例:使用单精度会导致精度丢失
// vsg::vec3 eye(1000000.0f, 50000.0f, 200000.0f); // 可能产生舍入误差

多视图(分屏 / 画中画) :创建多个 Camera + View,分别 addChild 到同一个 RenderGraph(各自 renderArea 不同),即可一窗多视角。


11.8 常见问题

现象 原因 解决
画面拉伸 宽高比错误 window->extent2D() 真实比例;或交给 createRenderGraphForView 自动处理
缩放后黑边/变形 没同步视口 createRenderGraphForView(自带 WindowResizeHandler
相机「穿模」 near/far 设置不当 调小 near、调大 far,或改正交投影
多视图重叠 renderArea 没分开 为每个 View 设不同的 ViewportState/renderArea

11.9 小结

  • Camera = projectionMatrix(Perspective/Orthographic) + viewMatrix(LookAt) + viewportState
  • View 把相机与子图配对,是 RenderGraph 的子节点,viewID 自动分配;
  • 窗口缩放的视口同步交给 createRenderGraphForViewWindowResizeHandler
  • 每帧改 viewMatrix 即可做相机动画;多 View 实现一窗多视角。

11.10 延伸阅读与下一章预告

  • 第 12 章《RenderGraph 与 CommandGraph》:View 如何被包进 RenderGraph 并录制成命令;
  • 第 14 章《变换与动画》:相机动画也可用 CameraSampler 驱动;
  • 第 19 章《拾取与交互》:射线从相机出发,做鼠标拾取。
相关推荐
超爱西西鸭1 小时前
思维导图编辑体验:触摸交互与节点操作(ArkTS)
学习·华为·harmonyos
stolentime1 小时前
AT_agc066_a [AGC066A] Adjacent Difference题解
c++·算法·贪心算法·构造
Accerlator1 小时前
从 Demo 到生产级别的 Agent 项目
学习
程序猿编码1 小时前
不用PyTorch,我用C++手写了一个能认手写数字的神经网络
c++·pytorch·神经网络·大模型
不会代码的小猴1 小时前
4. 控件学习2
开发语言·c++·笔记·qt
海兰2 小时前
【Kafka学习3】Apache Kafka 本地部署环境快速入门指南
学习·kafka·apache
世人万千丶2 小时前
HarmonyOS 股票 K 线图完整源码与金融图表模板
学习·华为·金融·harmonyos·鸿蒙
闻道且行之2 小时前
图片处理助手|C++ 手搓离线 AI 抠图工具,U2Net 原理到落地一次讲透
开发语言·c++·人工智能·神经网络·opencv·计算机视觉
玖釉-3 小时前
nvpro_core2 源码与架构解析:NVIDIA Vulkan 图形开发基础框架
c++·windows·图形渲染