第 21 章:视差背景、OpenGL 与 Shaders

第 21 章:视差背景、OpenGL 与 Shaders:保留第三版项目代码讲解,并以源码—状态—输出切片、确定性轨迹和故障重放完成验收。

学习目标

  • 能解释“第 21 章:视差背景、OpenGL 与 Shaders”如何用相机位移驱动多层视差,并在 SFML 2.6 Shader/OpenGL 边界内加载、设置 uniform、绘制和回退
  • 能逐项定位 视差背景(parallax backgrounds)、opengl、着色器(shaders)、cameragraphics 类(cameragraphics class)、完成游戏(completed game),说明它们位于源码、运行状态还是可见输出边界
  • 能按 读取相机 → 计算层偏移 → 检查 Shader → 设置 uniform → 绘制或回退 重放“多层视差移动”,持续检查“每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配”
  • 能注入“着色器编译失败后仍无条件用空 Shader 绘制,最终得到黑屏却没有错误日志”,从相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用找到第一个不一致并用同输入恢复

第三版来源、工具链与版本边界

“第 21 章:视差背景、OpenGL 与 Shaders”对齐 Packt 2024 年第三版的对应章节范围,并以官方公开代码仓库和 SFML 官方文档核对本页可公开验证的工程事实。本页是独立中文重写,不复现原书正文,也不把商品页、目录或代码仓库冒充完整原版。

在“第 21 章:视差背景、OpenGL 与 Shaders”中,第三版示例使用 SFML 2.6.1 时代 API。本页保留与本章有关的 2.6 系列合同;SFML 3 的事件、角度、时长和构造接口差异不会被静默回填。升级工具链时必须单独记录本章迁移补丁,不能把版本不匹配误判为“视差背景(parallax backgrounds)”概念错误。

  • Packt:Beginning C++ Game Programming, Third Edition:在“第 21 章:视差背景、OpenGL 与 Shaders”中,核对 2024 年第三版、C++20、SFML、四个项目、21 个正式教学章节及章节次序;不把商品页当作正文全文。
  • PacktPublishing:第三版官方代码仓库:在“第 21 章:视差背景、OpenGL 与 Shaders”中,核对 Timber、Pong、ZombieShooter、Run 四个项目的公开代码与资源组织;代码许可证不等于原书正文授权。
  • SFML 2.6 官方教程:在“第 21 章:视差背景、OpenGL 与 Shaders”中,核对本书使用的 SFML 2.6 系列 Window、Graphics、View、VertexArray、Shader 与 Audio API 语义。

正式概念与运行状态合同

视差背景(parallax backgrounds)

这个正式目录节点落在 视差状态 边界:相机位置、层深度和纹理重复。在本页中,它参与“用相机位移驱动多层视差,并在 SFML 2.6 Shader/OpenGL 边界内加载、设置 uniform、绘制和回退”。验收时保存相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用,不能只凭最终画面判断。

opengl

这个正式目录节点落在 Shader 管线 边界:GLSL 源码、编译、uniform 与 RenderStates。在本页中,它参与“用相机位移驱动多层视差,并在 SFML 2.6 Shader/OpenGL 边界内加载、设置 uniform、绘制和回退”。验收时保存每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配,不能只凭最终画面判断。

着色器(shaders)

这个正式目录节点落在 完成与回退 边界:最终合成、能力检测、错误日志和普通绘制。在本页中,它参与“用相机位移驱动多层视差,并在 SFML 2.6 Shader/OpenGL 边界内加载、设置 uniform、绘制和回退”。验收时保存相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用,不能只凭最终画面判断。

cameragraphics 类(cameragraphics class)

这个正式目录节点落在 视差状态 边界:相机位置、层深度和纹理重复。在本页中,它参与“用相机位移驱动多层视差,并在 SFML 2.6 Shader/OpenGL 边界内加载、设置 uniform、绘制和回退”。验收时保存每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配,不能只凭最终画面判断。

完成游戏(completed game)

这个正式目录节点落在 Shader 管线 边界:GLSL 源码、编译、uniform 与 RenderStates。在本页中,它参与“用相机位移驱动多层视差,并在 SFML 2.6 Shader/OpenGL 边界内加载、设置 uniform、绘制和回退”。验收时保存相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用,不能只凭最终画面判断。

验收项本页合同
最小正常场景相机向右移动 100 世界单位,背景层使用不同视差系数
边界或恢复场景能力检测或编译返回失败
必须保持每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配
单一故障着色器编译失败后仍无条件用空 Shader 绘制,最终得到黑屏却没有错误日志
可观察证据相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用

先预测,再操作三个本页实验

实验一:从源码到可见结果

先预测“多层视差移动”会怎样穿过 视差状态 → Shader 管线 → 完成与回退,再切换场景和正式概念。每次操作都必须能回到同一初始状态。

Source · state · visible result

第 21 章:视差背景、OpenGL 与 Shaders:可运行切片

用相机位移驱动多层视差,并在 SFML 2.6 Shader/OpenGL 边界内加载、设置 uniform、绘制和回退

选择输入或构建场景

定位正式概念

bcgp3-21 · 当前切片

视差背景(parallax backgrounds)相机向右移动 100 世界单位,背景层使用不同视差系数

边界 1视差状态

相机位置、层深度和纹理重复

边界 2Shader 管线

GLSL 源码、编译、uniform 与 RenderStates

边界 3完成与回退

最终合成、能力检测、错误日志和普通绘制

可验收结果

近层位移大于远层,所有偏移可由同一公式复算

实验二:逐步执行状态轨迹

依次执行 读取相机 → 计算层偏移 → 检查 Shader → 设置 uniform → 绘制或回退。每一步只选中一个阶段,并持续检查“每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配”。

Deterministic state trace

第 21 章:视差背景、OpenGL 与 Shaders:状态执行轨迹

逐步执行当前 1 / 5
始终保持

每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配

本步证据

相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用

实验三:单一故障与同输入恢复

注入“着色器编译失败后仍无条件用空 Shader 绘制,最终得到黑屏却没有错误日志”,定位第一项不一致;撤销后用完全相同的“Shader 不可用”重放。只有中间状态和最终输出一起恢复才算修复。

Fault injection · clean replay

第 21 章:视差背景、OpenGL 与 Shaders:故障注入与恢复

单一故障:着色器编译失败后仍无条件用空 Shader 绘制,最终得到黑屏却没有错误日志

1. 固定构建与输入一致

第 1 次重放使用同一源码、资源、初始状态和输入序列

2. 程序状态一致

每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配

3. 诊断证据一致

相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用

4. 恢复判断一致

同输入重放后,所有权、状态更新、可见输出和诊断证据重新一致

易错边界与工程取舍

从“远山为什么移动得更慢”开始

远层变化小,近层变化大。关键不是每帧给背景加一个速度,而是从相机的绝对位移派生偏移;否则暂停、瞬移和浮点累计都会让背景漂移。

先预测:相机向右移动 100 世界单位,factor 为 0.2 与 0.8 的两层各应移动多少?若两层同速,深度感为何消失?

从绝对相机位置计算层偏移

cameraCenter - origin 得到相机位移,再乘 factor。背景 Sprite 在世界中可以跟随 View 覆盖区域,但纹理采样偏移由公式决定。

float parallaxOffset(float cameraX, float originX,
                     float factor, float textureWidth)
{
    const float raw{(cameraX - originX) * factor};
    const float wrapped{std::fmod(raw, textureWidth)};
    return wrapped < 0.0F ? wrapped + textureWidth : wrapped;
}

C++ fmod 对负数可返回负值,要显式修正。纹理需可平铺;不连续边缘会周期性暴露接缝。

覆盖整个 View 而不露空

至少从可见左边界前一个周期画到右边界后一个周期。resize 或 zoom 后重新计算所需副本数,而不是假设永远两张。

void ParallaxLayer::submit(const CameraSnapshot& camera, DrawQueue& queue) const
{
    const float offset{parallaxOffset(camera.center.x, originX_,
                                      factor_, textureSize_.x)};
    const int copies{static_cast<int>(
        std::ceil(camera.size.x / textureSize_.x)) + 2};
    for (int index = -1; index < copies; ++index) {
        queue.push(makeDrawItem(
            camera.left + index * textureSize_.x - offset,
            renderLayer_));
    }
}

透明前景要在实体之后,远景要在世界之前。radar view 通常跳过复杂背景,保持导航可读。

CameraGraphics 类集中管理背景表现

不移动 Camera,不读取 Player,也不推进游戏时间。它接收已提交的 CameraSnapshot 和 presentation time。

class CameraGraphics {
public:
    explicit CameraGraphics(const BackgroundCatalog& catalog);
    void submit(const CameraSnapshot& camera,
                float presentationTime,
                DrawQueue& queue) const;
 
private:
    std::vector<ParallaxLayer> layers_;
    std::optional<sf::Shader> weatherShader_;
};

Shader 不可用时仍提交普通 Sprite。资源所有权长于所有 DrawItem,避免队列保存悬空指针。

SFML、OpenGL 与 GLSL 的边界

本章不绕过 SFML 重写渲染器,而是理解 sf::Shader 在有效 OpenGL context 中编译和绑定程序。

可用于雨幕色调、背景波动或颜色渐变,但不能改变平台碰撞等玩法事实。

版本与驱动能力必须匹配,编译失败日志要保留文件名和阶段。

能力检测、加载与降级

不能把开发机支持当成所有设备支持。

std::optional<sf::Shader> loadWeatherShader(const AssetPath& path)
{
    if (!sf::Shader::isAvailable())
        return std::nullopt;
 
    sf::Shader shader;
    if (!shader.loadFromFile(path.fragment, sf::Shader::Fragment))
        return std::nullopt;
    return shader;
}

降级应在日志和诊断 HUD 中可见,但不弹出阻塞每帧的错误框。

Uniform 是 CPU 与 shader 的契约

命名与单位要固定。时间可用 presentation time;若希望暂停时效果冻结,就只在 Playing 累加。

uniform sampler2D texture;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_strength;
 
void main()
{
    vec2 uv = gl_TexCoord[0].xy;
    float wave = sin(uv.y * 24.0 + u_time * 1.5) * u_strength;
    vec4 color = texture2D(texture, vec2(uv.x + wave, uv.y));
    gl_FragColor = gl_Color * color;
}

分辨率为像素还是逻辑 View 尺寸必须写清。缺失 uniform 时效果应使用安全默认值。

RenderStates 必须完整且局部

每个 DrawItem 显式携带所需状态,不能依赖上一次绘制残留。

sf::RenderStates states;
states.texture = &layer.texture();
states.shader = shader_ ? &*shader_ : nullptr;
states.blendMode = sf::BlendAlpha;
if (states.shader) {
    shader_->setUniform("u_time", presentationTime);
    shader_->setUniform("u_resolution", camera.pixelSize);
    shader_->setUniform("u_strength", layer.strength());
}
target.draw(layer.vertices(), states);

SFML 帮助管理很多 OpenGL 状态,但自定义 OpenGL 调用仍需 pushGLStates/popGLStates 或完整恢复,并注意成本。

视差与 shader 的帧协议

逻辑提交后生成 CameraSnapshot;CameraGraphics 从绝对中心计算各层偏移;更新本帧 uniform;按背景层级提交 DrawItem;Renderer 在 main view 下绘制背景、世界、天气,再绘制 radar 与 HUD。resize 先更新 CameraSnapshot 和像素分辨率,下一帧统一生效。

若暂停,CameraSnapshot 和逻辑世界冻结。是否让纯视觉 shader 继续变化由设计决定,但必须使用独立 presentation time 明确表达,不能偶然读取墙钟。 同一固定回放在启用和关闭 shader 时必须产生完全相同的平台、火球、得分与终局事件序列;任何差异都说明表现层越过了只读边界。

completed game 不是“能跑起来”

本章的完成游戏门禁覆盖:菜单 Start/Pause/Restart/Quit;可达平台与 Player 控制;Animator;雨与火球;空间音频;main/radar/HUD;视差与 shader 降级;完整 GameOver。

能暴露大小写路径、漏打包 shader、工作目录依赖和未声明资源。完成游戏必须从发布目录运行,而不只从 IDE 工程根目录运行。

性能、失败与视觉验收

固定场景记录对象数、活跃雨滴、火球数、DrawItem、calls、纹理切换、voice 和 CPU 时间。视差层数或 shader 增加后比较基线,不用平均 FPS 掩盖卡顿尖峰。

视觉验收检查负坐标环绕、超宽屏覆盖、resize、暂停、相机瞬移、shader 开关前后构图,以及 radar/HUD 不受背景状态污染。

全章回归清单

  • 相机左右移动与瞬移时,各层偏移由绝对位置派生,负坐标无跳变。
  • 普通、窄屏、超宽屏和 zoom 下背景副本覆盖 View,纹理接缝不可见。
  • shader capability 不可用、GLSL 编译失败和 uniform 缺失均走明确降级。
  • shader 绘制后普通实体、radar、HUD 的纹理、颜色和 blend 不受污染。
  • 固定回放下启用或关闭视觉 shader,平台、伤害、得分和 GameOver 完全一致。
  • 从干净发布目录走 Start、Pause、Resume、Fireball、GameOver、Restart、Quit 全路径。

小结

  • 视差背景从绝对相机位移乘视差因子得到偏移,并用正模与动态平铺覆盖 View
  • CameraGraphics 类集中提交背景表现,不控制相机或玩法
  • SFML 在 OpenGL 上执行 GLSL shader,uniform 和 RenderStates 是明确契约
  • capability 检测与普通 Sprite 降级保证不同图形环境仍可玩
  • completed game 需要流程、规则、资源、性能、失败注入和干净构建共同证明

资料与写作方式声明

本章以Beginning C++ Game Programming, Third Edition, Chapter 21权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

视差背景

背景层按不同相机位移比例移动产生深度感的技术。

视差因子

把相机位移转换为背景层偏移的深度比例。

层偏移

由相机绝对位移和因子计算的纹理位置。

正模环绕

把正负偏移稳定映射到一个纹理周期的计算。

平铺覆盖

覆盖当前 View 所需的相邻背景副本集合。

背景层级

远景、后景、世界和前景之间的绘制顺序。

CameraGraphics 类

根据 CameraSnapshot 提交视差层和 shader 背景的组件。

CameraSnapshot

提交后的相机中心、尺寸与可见范围。

OpenGL

SFML 图形底层用于 GPU 绘制和 shader 的图形 API。

shader
在 GPU 顶点或片段阶段运行的程序。
GLSL

OpenGL Shading Language,编写 shader 的语言。

shader capability

启动时检测当前环境是否支持 shader 的结果。

图形降级

shader 不可用时保持普通绘制的替代路径。

uniform

CPU 在 draw 前传给 shader 的稳定参数。

uniform 契约

uniform 名称、类型、单位、更新时机和默认值约定。

RenderStates

一次 SFML 绘制使用的纹理、shader、变换和混合状态。

GPU 状态泄漏

错误继承前一绘制项状态导致的画面异常。

背景帧协议

相机提交、层计算、uniform 与背景绘制的顺序。

completed game

通过功能、规则、资源、性能和构建验证的完整游戏。

干净构建

无旧缓存与隐式资源时重新构建并运行的验证。

帧预算

各更新和绘制阶段允许且实际测得的每帧成本。

失败注入

主动关闭能力或资源以验证降级与恢复的测试。

练习

  1. 问题 1:实现无接缝视差。 给定相机可正负移动和可 resize 的条件,写出偏移与平铺副本算法。
  1. 问题 2:设计 shader 降级。 模拟不支持 shader、编译失败和 uniform 配置错误,说明画面如何保持可用。
  1. 问题 3:证明游戏完成。 设计一条从干净构建到退出的验收路径,并列出需要记录的规则与性能证据。

讨论

评论区加载中…