第 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 世界单位,背景层使用不同视差系数
相机位置、层深度和纹理重复
↓
GLSL 源码、编译、uniform 与 RenderStates
↓
最终合成、能力检测、错误日志和普通绘制
可验收结果
近层位移大于远层,所有偏移可由同一公式复算
实验二:逐步执行状态轨迹
依次执行 读取相机 → 计算层偏移 → 检查 Shader → 设置 uniform → 绘制或回退。每一步只选中一个阶段,并持续检查“每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配”。
Deterministic state trace
第 21 章:视差背景、OpenGL 与 Shaders:状态执行轨迹
每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配
相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用
实验三:单一故障与同输入恢复
注入“着色器编译失败后仍无条件用空 Shader 绘制,最终得到黑屏却没有错误日志”,定位第一项不一致;撤销后用完全相同的“Shader 不可用”重放。只有中间状态和最终输出一起恢复才算修复。
Fault injection · clean replay
第 21 章:视差背景、OpenGL 与 Shaders:故障注入与恢复
单一故障:着色器编译失败后仍无条件用空 Shader 绘制,最终得到黑屏却没有错误日志
第 1 次重放使用同一源码、资源、初始状态和输入序列
每层偏移由同一相机状态和层系数确定,Shader 失败时仍有可见回退路径,uniform 与目标程序匹配
相机位置、层系数、计算后偏移、Shader::isAvailable、loadFromFile 结果、uniform 值和回退 draw 调用
同输入重放后,所有权、状态更新、可见输出和诊断证据重新一致
易错边界与工程取舍
从“远山为什么移动得更慢”开始
↡多个背景层按不同相机位移比例移动,从二维画面产生深度感的技术。远层变化小,近层变化大。关键不是每帧给背景加一个速度,而是从相机的绝对位移派生偏移;否则暂停、瞬移和浮点累计都会让背景漂移。
↡把相机世界位移转换成某一背景层纹理偏移的比例,通常远层接近零、近层接近一。先预测:相机向右移动 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 而不露空
↡为覆盖当前 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 类集中管理背景表现
↡拥有视差层配置、背景几何和 shader 资源,并根据 CameraSnapshot 提交背景绘制的表现组件。不移动 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 图形模块底层用于执行 GPU 绘制和 shader 程序的状态式图形 API。本章不绕过 SFML 重写渲染器,而是理解 sf::Shader 在有效 OpenGL context 中编译和绑定程序。
可用于雨幕色调、背景波动或颜色渐变,但不能改变平台碰撞等玩法事实。
↡OpenGL Shading Language,用于编写 SFML shader 源码的着色语言。版本与驱动能力必须匹配,编译失败日志要保留文件名和阶段。
能力检测、加载与降级
↡启动时检查当前图形环境是否支持 shader,并据此选择 GPU 效果或普通绘制。不能把开发机支持当成所有设备支持。
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 的契约
↡由 CPU 在 draw 前设置、对一次 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 必须完整且局部
↡一次 SFML draw 使用的 transform、texture、shader 和 blend mode 状态集合。每个 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 的帧协议
↡相机提交、层偏移计算、uniform 更新、背景绘制和普通世界绘制的固定顺序。逻辑提交后生成 CameraSnapshot;CameraGraphics 从绝对中心计算各层偏移;更新本帧 uniform;按背景层级提交 DrawItem;Renderer 在 main view 下绘制背景、世界、天气,再绘制 radar 与 HUD。resize 先更新 CameraSnapshot 和像素分辨率,下一帧统一生效。
若暂停,CameraSnapshot 和逻辑世界冻结。是否让纯视觉 shader 继续变化由设计决定,但必须使用独立 presentation time 明确表达,不能偶然读取墙钟。 同一固定回放在启用和关闭 shader 时必须产生完全相同的平台、火球、得分与终局事件序列;任何差异都说明表现层越过了只读边界。
completed game 不是“能跑起来”
↡从 Home 到一局结束的功能闭环,并通过规则、资源、性能、失败恢复和构建验证的完成状态。本章的完成游戏门禁覆盖:菜单 Start/Pause/Restart/Quit;可达平台与 Player 控制;Animator;雨与火球;空间音频;main/radar/HUD;视差与 shader 降级;完整 GameOver。
↡在无旧缓存和本地隐式资源的环境中重新配置、编译、复制资产并启动游戏的验证。能暴露大小写路径、漏打包 shader、工作目录依赖和未声明资源。完成游戏必须从发布目录运行,而不只从 IDE 工程根目录运行。
性能、失败与视觉验收
↡为 update、碰撞、粒子、draw calls、shader 和音频 voice 设定并测量的每帧成本上限。固定场景记录对象数、活跃雨滴、火球数、DrawItem、calls、纹理切换、voice 和 CPU 时间。视差层数或 shader 增加后比较基线,不用平均 FPS 掩盖卡顿尖峰。
↡逐项关闭 shader、声音、雨效或资源,验证降级后核心游戏仍可完成的测试。视觉验收检查负坐标环绕、超宽屏覆盖、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 需要流程、规则、资源、性能、失败注入和干净构建共同证明
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 视差背景
背景层按不同相机位移比例移动产生深度感的技术。
- 视差因子
把相机位移转换为背景层偏移的深度比例。
- 层偏移
由相机绝对位移和因子计算的纹理位置。
- 正模环绕
把正负偏移稳定映射到一个纹理周期的计算。
- 平铺覆盖
覆盖当前 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:实现无接缝视差。 给定相机可正负移动和可 resize 的条件,写出偏移与平铺副本算法。
- 问题 2:设计 shader 降级。 模拟不支持 shader、编译失败和 uniform 配置错误,说明画面如何保持可用。
- 问题 3:证明游戏完成。 设计一条从干净构建到退出的验收路径,并列出需要记录的规则与性能证据。