课程到这里,你有一个能跑的体素引擎。但「能跑」和「能交付」之间,还差最后一步——让它脱离你的开发机器,作为一个独立 App 跑在别人的电脑上。

1. 性能剖析 — 找到真正的瓶颈

很多优化是凭直觉做的,最后优化的地方不是瓶颈。在动手优化之前,先用量化工具找热点。

1.1 RenderDoc — GPU 端分析

RenderDoc 是开源的图形调试器,能抓一帧的所有 OpenGL 调用、看每个 Draw Call 的耗时、检查纹理和缓冲。

抓帧步骤:

  1. 启动 RenderDoc,在 Launch Application 里选你的体素引擎
  2. 跑到一个典型场景(512 Chunk, 120 可见),按 F12 抓帧
  3. 在 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)
  • 预分配 verticesindicescapacity(避免 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。你跟着走完的话,手里应该有一个:

  1. 能画方块世界、能生成地形
  2. 能动态加载 Chunk、多线程不卡
  3. 有漫反射 + 天空光 + AO 的光照
  4. 能用鼠标交互破坏和放置方块
  5. 有自己的 Qt 编辑器面板
  6. 能保存和加载世界
  7. 能打出 Windows/macOS/Linux 的独立安装包

这个引擎到此为止是一个完整的闭环。 在此之上,你可以接任何你想要的扩展:实体系统、AI、红石逻辑、联机——它们都是在这个引擎基础上加模块,而不是改引擎。


架构师复盘(全课程)

整门课写下来,如果让我只挑三条最值得记住的事:

  1. 先设计、再编码。 不是「先写个能跑的 Chunk 再重构」,而是一开始就把接口定好。前面多花两小时设计,后面省二十小时重构。

  2. 数据结构的选择决定性能天花板。 std::vector<Block> vs std::array vs PackedArray 不是一个「风格偏好」的问题——它直接决定你的引擎是 60 FPS 还是 6 FPS。Cache 友好、内存访问模式、面剔除、视锥体剔除——这些不是锦上添花,是地基。

  3. 交付才有价值。 一个只能在你机器上跑的项目,和一个别人能双击打开的 App,是完全不同的两件事。CMake 打包目标、windeployqt、AppImage——花半天做完发布流程,你的体素引擎就从「个人项目」变成了「可以分享的东西」。


系列文章:体素引擎从零构建 | 作者:Logic