:基于独立的渲染线程绘制彩色三角形)
Horse3D 游戏引擎研发笔记一基于独立的渲染线程绘制彩色三角形一、为什么要独立渲染线程二、Qt 6.4 官方的多线程渲染建议三、项目结构四、核心实现4.1 渲染线程 RenderThread4.2 显示端 IScreen4.3 CMake 与部署五、踩坑记录5.1 界面闪烁5.2 三角形消失 / 黑屏5.3 RenderThread::metaObject 链接错误5.4 VS 里运行找不到 Qt DLL六、当前成果七、下一步目标在 Qt 6.4 OpenGL Core 环境下把渲染逻辑放到独立线程执行GUI 线程只负责把渲染结果呈现出来并最终以动态库形式组织渲染模块。一、为什么要独立渲染线程在传统的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 渲染线程 RenderThreadRenderThread继承QThread负责持有与 GUI 共享的QOpenGLContext。在QOffscreenSurface上渲染三角形到 FBO。维护双缓冲 FBO每帧渲染完成后交换并暴露 front texture ID。通过QWaitCondition实现 “按需渲染”GUI 请求一帧线程渲染一帧渲染完成发frameReady()信号。关键代码片段voidRenderThread::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();emitframeReady();}m_context-doneCurrent();}voidRenderThread::renderNext(){QMutexLockerlocker(m_requestMutex);m_waitCondition.wakeOne();}使用glFinish()是为了确保 GPU 真正完成绘制避免 GUI 线程拿到还没写满的纹理。4.2 显示端 IScreenIScreen继承QOpenGLWidget和QOpenGLFunctions_3_3_Core职责只有三个在 GUI 线程创建QOffscreenSurface。创建共享上下文并移交给RenderThread。在paintGL()里把RenderThread的 front texture 用全屏四边形画出来。共享上下文创建是重点。Windows 上wglShareLists要求 GUI 线程不能持有当前 OpenGL 上下文所以必须先doneCurrent()voidIScreen::initializeGL(){initializeOpenGLFunctions();initDisplayResources();m_offscreenSurfacenewQOpenGLSurface();m_offscreenSurface-setFormat(context()-format());m_offscreenSurface-create();doneCurrent();// 释放 GUI 上下文否则 wglShareLists 会失败QOpenGLContext*workerContextnewQOpenGLContext();workerContext-setShareContext(context());workerContext-setFormat(context()-format());if(!workerContext-create()){qWarning(IScreen: failed to create worker context);deleteworkerContext;return;}m_renderThreadnewRenderThread(workerContext,this);workerContext-moveToThread(m_renderThread);connect(m_renderThread,RenderThread::frameReady,this,QOverload::of(IScreen::update));m_renderThread-start();m_renderThread-renderNext();}paintGL()里采样纹理voidIScreen::paintGL(){glClear(GL_COLOR_BUFFER_BIT);GLuint texm_renderThread-frontTexture();if(tex0)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.3RenderThread::metaObject链接错误RenderThread头文件写了Q_OBJECT但Dragon库目标没有开AUTOMOC导致 moc 生成的元对象代码没有编译进库。解决在Dragon/CMakeLists.txt里set(CMAKE_AUTOMOC ON)。5.4 VS 里运行找不到 Qt DLLwindeployqt已经把 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 游戏引擎从零开始的研发过程欢迎交流。