课程到这里,你有一个能跑的体素引擎。但「能跑」和「能交付」之间,还差最后一步——让它脱离你的开发机器,作为一个独立 App 跑在别人的电脑上。
1. 性能剖析 — 找到真正的瓶颈
很多优化是凭直觉做的,最后优化的地方不是瓶颈。在动手优化之前,先用量化工具找热点。
1.1 RenderDoc — GPU 端分析
RenderDoc 是开源的图形调试器,能抓一帧的所有 OpenGL 调用、看每个 Draw Call 的耗时、检查纹理和缓冲。
抓帧步骤:
- 启动 RenderDoc,在 Launch Application 里选你的体素引擎
- 跑到一个典型场景(512 Chunk, 120 可见),按 F12 抓帧
- 在 Event Browser 里看 Draw Call 时间分布
Event Browser - Frame 0 (16.7ms @ 60Hz)
────────────────────────────────────────
glClear 0.12ms
Chunk::draw (×120) 4.8ms ← 占 29%
grass_instanced 0.8ms ← 占 5%
flower_instanced 0.3ms
GUI overlay 0.1ms
swapBuffers 10.5ms ← 占 63%(vsync 等待)
────────────────────────────────────────
发现:Chunk 渲染只占 29%,大部分时间在等 vsync。 这说明 GPU 端完全不是瓶颈——渲染能力还有很大余量。瓶颈在 CPU 端的 Chunk 加载和 Mesh 生成。
1.2 Qt Creator Profiler — CPU 端分析
Qt Creator 内置了 perf 分析器(Linux)或可以用 Visual Studio 的性能探查器(Windows)。
Function Self Time Called
──────────────────────────────────────────────
Chunk::rebuildMesh() 42.3ms 3 ← 占 60%
Chunk::isFaceVisible() 18.1ms 2.3M
TerrainGenerator::blockFromHeight() 5.2ms 256K
Camera::viewMatrix() 0.3ms 210
ChunkManager::update() 0.1ms 210
发现:rebuildMesh() 和 isFaceVisible() 吃掉了大部分 CPU 时间。 这两个函数已经在工作线程里跑了,优化空间不大。但如果单个 Chunk 重建超过 40ms,玩家快速移动时可能会有 1-2 帧的卡顿。
优化建议:
- 把
isFaceVisible的遍历顺序改成y→z→x(第 3 章已经做了) - 对同一个 Chunk 的连续修改做合并(5 秒内修改了 3 次,只重建 1 次 Mesh)
- 预分配
vertices和indices的capacity(避免 vector 多次扩容)
1.3 优化后的 Chunk::rebuildMesh
void Chunk::rebuildMesh() {
static thread_local std::vector<Vertex> s_vertices;
static thread_local std::vector<GLuint> s_indices;
s_vertices.clear();
s_indices.clear();
// 预分配:假设 1/3 的方块有可见面
size_t estimatedFaces = TOTAL / 4;
s_vertices.reserve(estimatedFaces * 24);
s_indices.reserve(estimatedFaces * 36);
// ... 构建逻辑(和第 3 章相同)...
m_pendingMesh.upload(s_vertices, s_indices);
}
thread_local 是关键。 多个工作线程各自有一份 s_vertices / s_indices 的 buffer,互相不干扰,而且不需要每次 new + delete。上线前后对比:重建耗时从 42ms → 28ms(省了 vector 反复分配的时间)。
2. 跨平台打包
代码写好了,但别人的电脑上没有 Qt 开发环境。三种主流平台的打包方案:
2.1 Windows — windeployqt
:: deploy.bat — Windows 一键打包
@echo off
set BIN_DIR=build\release
set DEPLOY_DIR=deploy\VoxelEngine
:: 1. 清理旧包
rmdir /s /q %DEPLOY_DIR%
mkdir %DEPLOY_DIR%
:: 2. 复制可执行文件
copy %BIN_DIR%\voxel-engine.exe %DEPLOY_DIR%\
:: 3. windeployqt 自动复制所有 DLL
windeployqt %DEPLOY_DIR%\voxel-engine.exe
:: 4. 复制额外依赖(着色器、纹理、zstd DLL)
mkdir %DEPLOY_DIR%\shaders
xcopy shaders %DEPLOY_DIR%\shaders /E
mkdir %DEPLOY_DIR%\assets
xcopy assets %DEPLOY_DIR%\assets /E
copy zstd.dll %DEPLOY_DIR%\
echo Done! Package at: %DEPLOY_DIR%
windeployqt 会自动检测你的 exe 依赖了哪些 Qt 模块,把对应的 DLL 复制到部署目录。加上 VC++ Redistributable 就能在任意 Windows 机器上跑。
2.2 macOS — Bundle
#!/bin/bash
# deploy-macos.sh — macOS 一键打包
APP_DIR="deploy/VoxelEngine.app"
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/Contents/MacOS"
mkdir -p "$APP_DIR/Contents/Resources"
# 1. 复制可执行文件
cp build/voxel-engine "$APP_DIR/Contents/MacOS/"
# 2. macdeployqt 自动处理 framework
macdeployqt "$APP_DIR"
# 3. 复制资源
cp -r shaders "$APP_DIR/Contents/Resources/"
cp -r assets "$APP_DIR/Contents/Resources/"
# 4. 签名(可选,用于分发)
codesign --deep -s "Developer ID" "$APP_DIR"
echo "Bundle created: $APP_DIR"
2.3 Linux — AppImage
#!/bin/bash
# deploy-linux.sh — AppImage 打包
APP_DIR="deploy/VoxelEngine.AppDir"
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/usr/bin"
mkdir -p "$APP_DIR/usr/share/voxel-engine"
cp build/voxel-engine "$APP_DIR/usr/bin/"
cp -r shaders "$APP_DIR/usr/share/voxel-engine/"
cp -r assets "$APP_DIR/usr/share/voxel-engine/"
# 创建 .desktop 文件
cat > "$APP_DIR/voxel-engine.desktop" << EOF
[Desktop Entry]
Name=Voxel Engine
Exec=voxel-engine
Icon=voxel-engine
Type=Application
Categories=Graphics;
EOF
# 下载 linuxdeploy + AppImage 工具
linuxdeploy-x86_64.AppImage --appdir "$APP_DIR" \
--plugin qt --output appimage
echo "AppImage created!"
2.4 CMake 统一打包目标
# 在 CMakeLists.txt 中加打包目标
if(WIN32)
add_custom_target(deploy
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/shaders $<TARGET_FILE_DIR:voxel-engine>/shaders
COMMAND ${CMAKE_SOURCE_DIR}/shaders $<TARGET_FILE_DIR:voxel-engine>/assets
COMMAND windeployqt $<TARGET_FILE:voxel-engine>
COMMENT "Packaging for Windows..."
)
elseif(APPLE)
add_custom_target(deploy
COMMAND macdeployqt voxel-engine.app
COMMENT "Packaging for macOS..."
)
else()
add_custom_target(deploy
COMMAND ${CMAKE_SOURCE_DIR}/scripts/deploy-linux.sh
COMMENT "Packaging for Linux..."
)
endif()
之后只需要 cmake --build . --target deploy 就自动产出发布包。
3. 发布前最后的 checklist
graph TD
CHECK1["□ 所有 shader 文件放进发布包"]
CHECK2["□ 纹理图集 atlas.png 在包里"]
CHECK3["□ 没有硬编码路径(用 QStandardPaths)"]
CHECK4["□ 在干净机器上测试(没装 Qt 的环境)"]
CHECK5["□ 版本号 + README + LICENSE"]
CHECK6["□ GitHub Release 上传"]
CHECK1 --> CHECK2
CHECK2 --> CHECK3
CHECK3 --> CHECK4
CHECK4 --> CHECK5
CHECK5 --> CHECK6
style CHECK6 fill:#2ecc71,color:#fff
最大的坑: 硬编码路径。"C:/Users/logic/projects/shaders/" 在你的机器上工作,在别人机器上直接崩溃。用 QCoreApplication::applicationDirPath() 做相对路径:
QString shaderPath = QCoreApplication::applicationDirPath()
+ "/shaders/chunk.vert";
4. 终点 — 课程闭合
graph TD
CH0["第0章<br/>工程化地基"]
CH1["第1章<br/>渲染基石"]
CH2["第2章<br/>第一个方块"]
CH3["第3章<br/>Chunk系统"]
CH4["第4章<br/>动态加载"]
CH5["第5章<br/>地形生成"]
CH6["第6章<br/>射线拾取"]
CH7["第7章<br/>光照与AO"]
CH8["第8章<br/>Qt编辑器"]
CH9["第9章<br/>持久化"]
CH10["第10章<br/>性能优化"]
CH11["第11章<br/>发布"]
CH0 --> CH1 --> CH2 --> CH3 --> CH4 --> CH5 --> CH6
CH6 --> CH7 --> CH8 --> CH9 --> CH10 --> CH11
style CH0 fill:#4a90d9,color:#fff
style CH11 fill:#2ecc71,color:#fff
12 章,从黑窗口到独立 App。你跟着走完的话,手里应该有一个:
- 能画方块世界、能生成地形
- 能动态加载 Chunk、多线程不卡
- 有漫反射 + 天空光 + AO 的光照
- 能用鼠标交互破坏和放置方块
- 有自己的 Qt 编辑器面板
- 能保存和加载世界
- 能打出 Windows/macOS/Linux 的独立安装包
这个引擎到此为止是一个完整的闭环。 在此之上,你可以接任何你想要的扩展:实体系统、AI、红石逻辑、联机——它们都是在这个引擎基础上加模块,而不是改引擎。
架构师复盘(全课程)
整门课写下来,如果让我只挑三条最值得记住的事:
-
先设计、再编码。 不是「先写个能跑的 Chunk 再重构」,而是一开始就把接口定好。前面多花两小时设计,后面省二十小时重构。
-
数据结构的选择决定性能天花板。
std::vector<Block>vsstd::arrayvsPackedArray不是一个「风格偏好」的问题——它直接决定你的引擎是 60 FPS 还是 6 FPS。Cache 友好、内存访问模式、面剔除、视锥体剔除——这些不是锦上添花,是地基。 -
交付才有价值。 一个只能在你机器上跑的项目,和一个别人能双击打开的 App,是完全不同的两件事。CMake 打包目标、windeployqt、AppImage——花半天做完发布流程,你的体素引擎就从「个人项目」变成了「可以分享的东西」。
系列文章:体素引擎从零构建 | 作者:Logic