欢迎光临
我们一直在努力

Horse3D 游戏引擎研发笔记(一):基于独立的渲染线程绘制彩色三角形

Horse3D 游戏引擎研发笔记(一):基于独立的渲染线程绘制彩色三角形

  • Bilibili 同步视频
  • 一、为什么要独立渲染线程
  • 二、Qt 6.4 官方的多线程渲染建议
  • 三、项目结构
  • 四、核心实现
    • 4.1 渲染线程 RenderThread
    • 4.2 显示端 IScreen
    • 4.3 CMake 与部署
  • 五、踩坑记录
    • 5.1 界面闪烁
    • 5.2 三角形消失 / 黑屏
    • 5.3 `RenderThread::metaObject` 链接错误
    • 5.4 VS 里运行找不到 Qt DLL
  • 六、当前成果
  • 七、下一步

目标:在 Qt 6.4 + OpenGL Core 环境下,把渲染逻辑放到独立线程执行,GUI 线程只负责把渲染结果呈现出来,并最终以动态库形式组织渲染模块。

Bilibili 同步视频

Horse3D 游戏引擎研发笔记(一):基于独立的渲染线程绘制彩色三角形

一、为什么要独立渲染线程

在传统的 QOpenGLWidget 用法里,渲染、事件处理、UI 布局全部跑在 GUI 线程。当场景复杂之后,一帧的 paintGL 可能直接阻塞消息循环,导致窗口卡顿、输入延迟。

把渲染拆到独立线程的好处很直接:

  • GUI 线程只干 “显示” 这一件事,帧率稳定。

  • 渲染线程可以充分利用多核 CPU/GPU,做更重的几何、材质、后处理计算。

  • 后期更容易扩展成命令队列、多 Pass、异步资源加载等引擎常见结构。

但 Qt 的 OpenGL 上下文对线程敏感,直接跨线程访问 FBO、VAO 会触发驱动层访问冲突。因此需要先理解 Qt 官方推荐的多线程模型。

二、Qt 6.4 官方的多线程渲染建议

Qt 6.4 文档在 QOpenGLWidget 的 Threading 段落里给出了两条路:

  • 工作线程做离屏渲染,主线程采样纹理 工作线程持有与 GUI 上下文共享的第二个 QOpenGLContext,渲染到自己的 FBO,然后把纹理 ID 交给 GUI 线程显示。

  • 直接把 QOpenGLWidget 的上下文移到工作线程 需要重载 paintEvent 为空,并小心地通过 moveToThread 切换上下文亲和性,渲染完再切回 GUI 线程触发合成。

  • 第一条路更符合引擎思路:渲染管线自己管理 target,显示端只负责 present。我们项目最终采用的就是这条路。

    三、项目结构

    e:WorkSpacesoftwarer-horse
    ├── CMakeLists.txt # 顶层:统一输出目录、add_subdirectory
    ├── Dragon # 渲染核心动态库
    │ ├── CMakeLists.txt
    │ ├── dragon_global.h # DLL 导出宏
    │ ├── RenderThread.h / .cpp # 独立渲染线程
    │ ├── Screen.h / .cpp # IScreen:基于 QOpenGLWidget 的显示端
    ├── softwarer-horse # 可执行程序
    │ ├── CMakeLists.txt
    │ ├── main.cpp
    │ ├── softwarerhorse.h / .cpp # 主窗口

    Dragon 是 SHARED 库,softwarer-horse.exe 链接 Dragon.lib,运行时依赖 Dragon.dll。

    四、核心实现

    4.1 渲染线程 RenderThread

    RenderThread 继承 QThread,负责:

    • 持有与 GUI 共享的 QOpenGLContext。

    • 在 QOffscreenSurface 上渲染三角形到 FBO。

    • 维护双缓冲 FBO,每帧渲染完成后交换,并暴露 front texture ID。

    • 通过 QWaitCondition 实现 “按需渲染”:GUI 请求一帧,线程渲染一帧,渲染完成发 frameReady() 信号。

    关键代码片段:

    void RenderThread::run()
    {
    if (!m_context->makeCurrent(m_surface)) {
    qWarning("RenderThread: makeCurrent failed");
    return;
    }
    initializeOpenGLFunctions();
    initRenderResources();

    while (!m_stop) {
    m_requestMutex.lock();
    m_waitCondition.wait(&m_requestMutex);
    m_requestMutex.unlock();

    if (m_stop) break;

    renderFrame();
    glFinish();

    emit frameReady();
    }

    m_context->doneCurrent();
    }

    void RenderThread::renderNext()
    {
    QMutexLocker locker(&m_requestMutex);
    m_waitCondition.wakeOne();
    }

    使用 glFinish() 是为了确保 GPU 真正完成绘制,避免 GUI 线程拿到还没写满的纹理。

    4.2 显示端 IScreen

    IScreen 继承 QOpenGLWidget 和 QOpenGLFunctions_3_3_Core,职责只有三个:

  • 在 GUI 线程创建 QOffscreenSurface。

  • 创建共享上下文并移交给 RenderThread。

  • 在 paintGL() 里把 RenderThread 的 front texture 用全屏四边形画出来。

  • 共享上下文创建是重点。Windows 上 wglShareLists 要求 GUI 线程不能持有当前 OpenGL 上下文,所以必须先 doneCurrent():

    void IScreen::initializeGL()
    {
    initializeOpenGLFunctions();
    initDisplayResources();

    m_offscreenSurface = new QOpenGLSurface();
    m_offscreenSurface->setFormat(context()->format());
    m_offscreenSurface->create();

    doneCurrent(); // 释放 GUI 上下文,否则 wglShareLists 会失败

    QOpenGLContext *workerContext = new QOpenGLContext();
    workerContext->setShareContext(context());
    workerContext->setFormat(context()->format());
    if (!workerContext->create()) {
    qWarning("IScreen: failed to create worker context");
    delete workerContext;
    return;
    }

    m_renderThread = new RenderThread(workerContext, this);
    workerContext->moveToThread(m_renderThread);

    connect(m_renderThread, &RenderThread::frameReady,
    this, QOverload<>::of(&IScreen::update));

    m_renderThread->start();
    m_renderThread->renderNext();
    }

    paintGL() 里采样纹理:

    void IScreen::paintGL()
    {
    glClear(GL_COLOR_BUFFER_BIT);

    GLuint tex = m_renderThread->frontTexture();
    if (tex == 0) return;

    m_program->bind();
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, tex);
    m_program->setUniformValue("tex", 0);

    glBindVertexArray(m_vao);
    glDrawArrays(GL_TRIANGLES, 0, 6);

    m_renderThread->renderNext();
    }

    注意 paintGL() 显示完才会请求下一帧,形成 “显示 → 请求 → 渲染 → 显示” 的节奏,避免读写竞争导致的闪烁或黑屏。

    4.3 CMake 与部署

    顶层 CMakeLists.txt 统一输出目录:

    set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/Library)
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/Library)
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/Runtime)

    这样 Dragon.dll 和 softwarer-horse.exe 会在同一目录,避免运行时找不到 DLL。

    为了让 VS 里按 F5 也能正确加载 Qt DLL,给可执行目标加了调试器工作目录属性:

    set_target_properties(${PROJECT_NAME} PROPERTIES
    WIN32_EXECUTABLE TRUE
    VS_DEBUGGER_WORKING_DIRECTORY "$<TARGET_FILE_DIR:${PROJECT_NAME}>"
    )

    同时用 windeployqt 在 post-build 阶段自动把 Qt 运行时依赖复制到 exe 目录:

    if(WIN32)
    get_target_property(_qt_core_location Qt${QT_VERSION_MAJOR}::Core IMPORTED_LOCATION_RELEASE)
    get_filename_component(_qt_bin_dir ${_qt_core_location} DIRECTORY)
    find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS ${_qt_bin_dir} NO_DEFAULT_PATH)
    if(WINDEPLOYQT_EXECUTABLE)
    add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
    COMMAND ${WINDEPLOYQT_EXECUTABLE}
    –dir "$<TARGET_FILE_DIR:${PROJECT_NAME}>"
    "$<TARGET_FILE:${PROJECT_NAME}>"
    )
    endif()
    endif()

    五、踩坑记录

    5.1 界面闪烁

    最初 RenderThread::run() 里是无条件循环渲染,主线程显示节奏和渲染节奏不同步,导致读到未完成帧。解决办法是改成按需渲染:主线程显示完请求下一帧,线程渲染完通过信号通知更新。

    5.2 三角形消失 / 黑屏

    共享上下文创建失败。根本原因是 Windows 上 wglShareLists 要求 GUI 线程先释放当前上下文,所以必须在创建 worker context 前调用 doneCurrent()。

    5.3 RenderThread::metaObject 链接错误

    RenderThread 头文件写了 Q_OBJECT,但 Dragon 库目标没有开 AUTOMOC,导致 moc 生成的元对象代码没有编译进库。解决:在 Dragon/CMakeLists.txt 里 set(CMAKE_AUTOMOC ON)。

    5.4 VS 里运行找不到 Qt DLL

    windeployqt 已经把 DLL 复制到 Runtime/ 目录,但 VS 默认调试工作目录不是 exe 所在目录。解决:设置 VS_DEBUGGER_WORKING_DIRECTORY 为 $<TARGET_FILE_DIR:${PROJECT_NAME}>。

    六、当前成果

    现在项目已经能稳定运行:

    • 独立渲染线程在离屏 FBO 上绘制彩色三角形。

    • GUI 线程通过共享纹理采样并显示。

    • 渲染模块打包为 Dragon.dll,主程序通过导入库链接。

    • VS 直接 F5 运行无额外配置。

    七、下一步

    • 把顶点、材质、 uniform、相机等概念抽象成引擎资源类。

    • 引入渲染命令队列,让主线程以 “提交命令” 的方式驱动渲染线程。

    • 支持 Dear ImGui 或 Qt Widget 叠加调试 UI。

    • 尝试把渲染线程渲染目标升级为多采样 FBO + 后处理链。


    Horse3D 游戏引擎研发笔记(一):基于独立的渲染线程绘制彩色三角形

    本系列记录 Horse3D 游戏引擎从零开始的研发过程,欢迎交流。

    赞(0)
    未经允许不得转载:171主机测评 » Horse3D 游戏引擎研发笔记(一):基于独立的渲染线程绘制彩色三角形
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址