Skip to content

Feature/graphic/render queue - #75

Open
YuanSang0512 wants to merge 18 commits into
gkit-org:mainfrom
YuanSang0512:feature/graphic/render_queue
Open

Feature/graphic/render queue#75
YuanSang0512 wants to merge 18 commits into
gkit-org:mainfrom
YuanSang0512:feature/graphic/render_queue

Conversation

@YuanSang0512

Copy link
Copy Markdown
Contributor

PR: feat(graphic): 落地渲染队列(排序 + 批量执行 + RenderObject)

描述

概述

在 graphic 模块落地 .vscode/渲染队列设计方案.md 中的**渲染队列(RenderQueue)**设计:Renderer::draw 不再立即执行,而是把一条 RenderCommand 记录进 RenderQueue,由 flush() 统一排序、并经前端 RenderDevice 抽象批量执行。配套落地了可复用的 RenderObject 绘制单元(几何 + 材质 + 状态,隐藏 VAO/VBO/IBO)、Material 资源结构、前端 RenderState/UniformValue 值类型,以及 resource/ + render/ 头文件拆分。最后的健壮性修复补齐了 FBO 尺寸、按目标 viewport、命令状态快照与入队期 shader 校验。

改动内容

1. 前端值类型(include/gkit/graphic/render/RenderState.hppinclude/gkit/graphic/resource/UniformBuffer.hpp)

  • 新增 RenderState 快照类型,把深度/混合/剔除/模板打包成一个值(排序键 + 命令自携带状态)。
  • 新增 UniformValue(std::variant<int, float, Vector3, Vector4, Matrix3, Matrix4>)+ UniformData(按名逐条赋值)与 UboBlock(批量路径引用)三种 uniform 载体。
  • MAX_TEXTURE_SLOTS 移入 config.hpp,作为引擎级纹理槽上限。

2. 渲染队列(RenderCommand.hppRenderQueue.hpp/cpp)

  • 新增 RenderCommand 值类型:引用目标(FrameBuffer*)与绘制单元,携带 optional<Viewport>RenderState 快照、经材质引用的纹理/uniform 源,以及排序元数据(transparentdepth_key)和逐命令清屏。
  • RenderQueue::submit() 入队;flush() 稳定排序(FBO 目标排在屏幕之前,保证后处理能采样 FBO;blend/透明分组;不透明 front-to-back、透明 back-to-front)后逐条执行:切换目标 → 按目标 viewport → 清屏 → 增量应用状态 → 绑定 shader/纹理/uniform → device.draw(_instance)

3. RenderObject 绘制单元(RenderObject.hpp/cpp)

  • 可复用绘制单元:用户提供 vertices + indices + VertexBufferLayout + Material,VAO/VBO/IBO 首次绘制时懒创建并缓存(ensure_uploaded),对调用方隐藏。
  • 携带材质、渲染状态、实例数、透明类别、深度键与逐绘制清屏标志。

4. Material 资源(resource/Material.hpp)

  • 新增 Material 结构:shader 指针 + 纹理槽 + UniformData + UboBlock,按引用共享、由资源系统持有。

5. StateManager → 后端内部增量应用器(backend/opengl/StateManager.*)

  • 去掉单例;StateManager 成为 opengl::Device 的私有成员。
  • RenderDevice 新增 apply_state(RenderState),Device 转发给状态管理器的 dirty-flag 增量应用器;前端执行器完全看不到该类型。

6. 后端资源工厂接线(RenderDevice.hppbackend/opengl/Device.*create_device.cpp)

  • RenderDevice 新增 set_viewport / draw / draw_instance / clear 执行入口。
  • Device 构造时强制全量状态同步,保证 shadow 状态启动即与 GL 一致、永不漂移。

7. Renderer 集成(Renderer.hpp/cpp)

  • draw(RenderObject&, target, viewport) 把对象快照成命令并入队(不再立即绘制);flush() 执行队列。
  • draw 提供默认 target/viewport 参数(屏幕目标 / 全屏)。
  • Shader 校验:shader 为空或无效(编译/链接失败)的绘制在入队时即被拒绝并记录错误日志,而不是在 flush 时崩溃。

8. FBO 与 viewport 正确性(FrameBuffer.*Texture.*)

  • FrameBuffer 暴露 width()/height();attach_color_texture 把帧缓冲纹理调整为 FBO 尺寸,不再写死全局 SCR_WIDTH/SCR_HEIGHT
  • 命令 viewport 默认取渲染目标尺寸(FBO 或屏幕),非窗口尺寸 FBO 无需显式传参即可获得正确 viewport。

9. 头文件布局(include/gkit/graphic/)

  • 拆分为 resource/(缓冲、纹理、shader、帧缓冲、材质…)与 render/(Renderer、RenderDevice、RenderObject、RenderQueue、RenderCommand、RenderState)。gkit.hppCMakeLists.txt 同步更新。

10. 测试(test/graphic/test_render.cpp,新增 shader)

  • test_window 改名为 test_render:彩色三角形渲染到 FBO,后处理四边形采样 FBO 纹理(反色)上屏,再加一个半尺寸三角形做对比——覆盖按目标 viewport、逐命令清屏与目标切换。
  • 新增 color_triangle.shaderpost_process.shaderalpha_triangle.shader(带 u_alpha uniform);每帧清屏。
  • 透明三角形:复用三角形形状偏移 50px 左下,SrcAlpha/OneMinusSrcAlpha 混合(u_alpha=0.8),深度测试开启;给不透明物体也启用深度测试,让屏幕深度真实参与分层。
  • 模板遮罩:FBO 上做模板测试——模板三角形偏移 50px 右上,Always/Replacestencil=1;只清 FBO 颜色(保留模板);随后三角形 NotEqual(1) 绘制,模板区域内被遮住留空。

绘制工作流

test_render 单帧的实际流程(所有 draw() 只入队,flush() 时排序 + 批量执行):

① 清屏       renderer.clear(ColorDepth)          屏幕颜色+深度清空
② Draw1      stencil_triangle_obj → FBO          清 FBO(All)+写 stencil=1(偏移50px右上)
③ Draw2      masked_triangle_obj → FBO           只清 FBO 颜色(保留模板),NotEqual 遮住模板区
④ Draw3      quad_obj → 屏幕                     后处理四边形,采样 FBO 纹理(反色)
⑤ Draw4      triangle_obj → 屏幕                 半尺寸三角形对比
⑥ Draw5      alpha_triangle_obj → 屏幕           透明三角形,混合 + 深度测试
⑦ flush()    排序(目标/透明/深度)后逐条执行
⑧ SwapWindow 交换缓冲

排序后的实际执行顺序:Draw1 → Draw2(FBO 目标在前)→ Draw3 → Draw4 → Draw5(屏幕目标在后,透明最后)。这与入队顺序一致,因为排序按 target 分组。

单次绘制的完整调用链

renderer.draw(triangle_obj, fbo.get()) 为例,从用户侧到最终呈现的每一层:

用户调用:renderer.draw(obj, target, viewport)
    │
    │  【入队阶段 —— Renderer::draw】
    ├─ 1. 校验 shader
    │      obj.material.shader 为空?→ 拒绝入队 + 记 Error 日志
    │      shader->is_valid() 为假(编译/链接失败)?→ 拒绝入队 + 记 Error 日志
    ├─ 2. 构造 RenderCommand(值类型快照)
    │      object       = &obj
    │      target       = fbo(或 nullptr=屏幕)
    │      viewport     = 显式值 或 默认 {0,0,SCR_WIDTH,SCR_HEIGHT}
    │      state        = obj.state 快照(深度/混合/剔除/模板)
    │      instance_count / transparent / depth_key / clear / clear_flags
    └─ 3. RenderQueue::submit(cmd)   → 拷贝进 commands 数组
             │
             │  【执行阶段 —— 每帧末尾 Renderer::flush()】
             ├─ 4. RenderQueue::flush(device)
             │      stable_sort(commands):target 分档 → blend/透明分组 → 深度排序
             │      (不透明 front-to-back / 透明 back-to-front)
             │
             └─ 5. 逐条执行当前命令
                  a. 目标切换:cmd.target != last_target → bind()/unbind() FBO
                  b. set_viewport(cmd.viewport.value_or(目标尺寸))
                  c. cmd.clear → device.clear(cmd.clear_flags)
                  d. device.apply_state(cmd.state)
                     └─ StateManager::apply → 只对变化部分调 glEnable/glBlendFunc/...
                  e. obj.ensure_uploaded(device)   [首次]创建 VBO/IBO/VAO 并缓存
                  f. material.shader->bind()       (入队期已保证非空且有效)
                  g. bind_textures(material)       textures[i]->bind(i) → glActiveTexture
                  h. apply_uniforms(material)      set_uniform_*(name, value)
                  i. device.draw(vao, ibo, *shader)
                     └─ opengl::Device::draw:shader.bind → va.bind → ib.bind → glDrawElements
             │
             └─ 6. flush 结束:清空 commands;若有 FBO 绑着则 unbind 回屏幕

呈现:像素写入当前绑定的 framebuffer → SDL_GL_SwapWindow 显示到窗口

关键点:

  • 绘制是延迟的:draw() 只入队,真正的 GPU 调用全在 flush()。同一 RenderObject 可在多帧复用(GPU 资源缓存)。
  • 状态是快照:命令携带的是 draw() 时的 obj.state 拷贝,之后改 obj.state 不影响已入队命令。
  • shader 校验在入队期:null/无效 shader 在 draw() 就被拒,不会拖到 flush() 崩溃。

注意事项

  • 屏幕默认无模板缓冲:SDL 默认 SDL_GL_DEPTH_SIZE=16(有深度)但 SDL_GL_STENCIL_SIZE=0(无模板)。模板测试目前只能在 FBO 上用(其 RenderBuffer 是 GL_DEPTH24_STENCIL8)。若要在屏幕做模板遮罩,需先 SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8)
  • 深度测试禁用 = 不写深度:GL 规则是深度测试关闭时片段不写深度缓冲。模板三角形特意关深度测试,避免它把 z=0 写入 FBO 深度而让被遮三角形深度测试失败。若想让它参与深度分层,需显式 depth.enabled = true
  • glClear(Color) 保留模板:清屏不受模板测试影响,且 ClearFlags::Color 不清模板——这是"清画面但不清模板标记"能实现的前提。
  • 状态跨命令自动恢复:屏幕命令默认 stencil.enabled=false,StateManager 增量应用会在切到屏幕命令时自动关闭模板,模板状态不会泄漏到后续屏幕绘制。
  • 排序是全序,不可表达多趟顺序:sort_key 把 FBO 目标整体排在屏幕前,无法表达"屏幕命令夹在两次 FBO 绘制之间"的多趟(如 shadow map)场景——这是当前单队列模型的设计局限。
  • sort_key 未纳入 shader:排序只按 target/blend/透明分组,同目标内 shader 不参与排序,可能出现 shader 来回切换、无法批处理。

Commits

Commit 描述
9125886 feat(graphic): add frontend RenderState value type
a9f72e8 refactor(graphic): hide StateManager inside Device, add apply_state
566854e feat(graphic): add uniform types (UniformValue/UniformData/UboBlock)
1a65498 feat(graphic): add RenderCommand and RenderQueue
e742184 feat(graphic): sort render queue and apply uniforms
e49f93e feat(graphic): route Renderer::draw through the render queue
3827ab0 feat(graphic): add RenderObject reusable draw unit
43a0137 refactor(graphic): split graphic headers into resource/ and render/
3432b06 feat(graphic): per-command viewport; cleanup Renderer draw API
fea1e14 feat(graphic): per-command clear of the render target
ee6940a feat(graphic): add Material struct; move MAX_TEXTURE_SLOTS to config
e3d83dd refactor(graphic): RenderObject takes data + Material, hides VAO/VBO/IBO
2e4ba19 feat(graphic): default target/viewport args for Renderer::draw
a03cba8 test(graphic): rename test_window to test_render, use default draw args
248b03c fix(graphic): harden render queue pipeline
f1fdd95 test(graphic): cover blending, depth, and stencil masking in test_render

设计说明

  • 范围:单线程、单队列,暂不上多线程命令缓冲。
  • 命令自携带状态:每条 RenderCommand 入队时快照自己的 RenderState,同一 RenderObject 的两条命令可携带不同状态。
  • RenderObject 作为绘制单元:用户接口为 renderer.draw(object, target, viewport),VAO/VBO/IBO 生命周期在内部。
  • shader 有效性是入队期契约:空或编译失败的 shader 在入队时被拒绝并记日志,而非 flush 期崩溃。
  • 尚未实现(设计文档 §8):面向 Vulkan/多线程的命令缓冲字节序列化、StorageBuffer/UniformBuffer 后端、draw_instance 的逐实例缓冲接线、纹理资源模块集成。

验证

  • gcc(Debug preset,启用测试)构建通过:gkit_graphic + test_render
  • test_render.exe 完整跑完帧循环(FBO 模板遮罩 → 后处理四边形 → 对比三角形 → 透明三角形 → 屏幕)无崩溃、无 GL 错误。
  • 渲染队列健壮性修复(FBO 尺寸、viewport、状态快照、shader 校验)与新增的透明/模板测试均经重新构建 + 运行验证。

- Add RenderState.hpp with DepthState/BlendState/CullFaceState/StencilState
  moved as frontend value types (was nested in opengl StateManager)
- RenderState combines the four states with operator==/!= for sorting keys
  and incremental state application (rendering queue design Step 1)
- StateManager migration to reference these frontend types happens in Step 2
- RenderDevice: add virtual apply_state(const RenderState&) so the frontend
  executor/backend apply state through the abstract device only
- StateManager: drop Singleton inheritance, become a plain class owned by
  opengl::Device; replace set_* + apply() with incremental apply(RenderState)
  that only applies changed components (depth/blend/cull/stencil)
- StateManager uses the frontend RenderState types (Step 1); removes nested
  state structs, dirty flags, and get_* accessors (no external users)
- opengl::Device holds a StateManager member and forwards apply_state

Rendering queue design Step 2
- UniformBuffer.hpp now defines the type-erased UniformValue variant
  (int/float/Vector3/Vector4/Matrix3/Matrix4), the simple-path UniformData
  (name->value list) and the batch-path UboBlock (struct reference + binding)
- UniformBuffer class stays a placeholder pending backend impl
- Rendering queue design Step 3
- RenderCommand: value-type draw command carrying target/va/ib/shader/
  RenderState/UniformData/UboBlock/texture slots/transparent/instance_count
  (references resources, does not own); MAX_TEXTURE_SLOTS = 8
- RenderQueue: submit() collects commands, flush() applies state via
  RenderDevice::apply_state and draws via device.draw/draw_instance
- Basic flush executes in submission order; sorting lands in Step 5

Rendering queue design Step 4
- RenderQueue::flush sorts commands: opaque front-to-back, transparent
  back-to-front, grouped by state/transparency to reduce state switches
- Apply simple-path uniforms (UniformData) via std::visit dispatch to the
  shader's set_uniform_* (int/float/Vector3/Vector4/Matrix3/Matrix4)
- RenderCommand::shader is now non-const (uniforms are mutated during exec)
- UBO batch upload left as TODO pending UniformBuffer backend

Rendering queue design Step 5
- Renderer now enqueues RenderCommand instead of drawing immediately
- add Renderer::flush() that executes the queued commands via RenderQueue
- draw/draw_instance take non-const Shader& (uniforms mutated on execute)
- test_window: build commands with target/textures/uniforms and flush each
  frame (post-processing pipeline now expressed as queued commands)

Rendering queue design Step 6
- RenderObject: geometry + material + state as a reusable draw unit
  (fields mirror RenderCommand, to_command() builds a command per frame)
- Renderer::draw(const RenderObject&) enqueues via to_command()
- Upper-layer draw interface evolution (design §7.1, direction 3)

Rendering queue design Step 7
- include/gkit/graphic/resource/: GPU resource abstractions (Buffer,
  VertexBuffer, IndexBuffer, VertexArray, Shader, Texture, FrameBuffer,
  RenderBuffer, StorageBuffer, UniformBuffer)
- include/gkit/graphic/render/: render pipeline types (Renderer,
  RenderDevice, RenderQueue, RenderCommand, RenderState, RenderObject)
- graphic root keeps only config.hpp and VertexBufferLayout.hpp
- update all includes across src/include/test to new paths
- Add Viewport value type; RenderCommand/RenderObject carry a per-target
  viewport so FBO commands use FBO size and screen commands use window size
  (GL viewport is global state, must be set per command)
- RenderDevice::set_viewport(Viewport) abstract interface; opengl Device
  implements via glViewport; RenderQueue sets it before each command
- Renderer: keep only draw(const RenderObject&); remove simplified
  draw(va,ib,shader) and draw_instance (covered by RenderObject)
- test_window: FBO is half the window (400x400), each RenderObject carries
  its viewport; drop manual fbo->set_viewport in the loop
- RenderQueue sorts FBO-targeted commands before screen commands and
  unbinds the previous FBO when switching targets
- RenderCommand/RenderObject gain clear + clear_flags: a command can request
  the target (FBO or screen) be cleared before drawing
- RenderQueue::flush clears the bound target when cmd.clear is set
- test_window: triangle_to_fbo clears the FBO color/depth attachment before
  drawing, so the post-processing quad samples a clean framebuffer
- New render/Material.hpp: shader + texture slots + UniformData + UboBlock
  (resources held by pointer reference, not owned, shareable across objects)
- Move MAX_TEXTURE_SLOTS from RenderCommand to graphic/config.hpp so both
  Material and RenderCommand use it without a dependency between them
- Remove the duplicate definition from RenderCommand

RenderObject refactor Step 1
- RenderObject now owns CPU vertex/index data and a Material; GPU resources
  (VBO/IBO/VAO) are lazily created and cached on first draw via ensure_uploaded
- RenderCommand holds only RenderObject* + per-draw controls (target, viewport,
  clear, sorting metadata); geometry/material/state read from the object
- Renderer::draw(RenderObject&, target, viewport) enqueues the command
- RenderQueue executes: switch target, set viewport, clear, apply state,
  lazily upload geometry, bind material shader/textures, apply uniforms, draw
- Material moved to graphic/resource/ (it references shader/texture resources)
- MAX_TEXTURE_SLOTS moved to config.hpp (shared by Material and RenderCommand)
- test_window: builds objects from vertex/index arrays + Material, no manual
  VAO/VBO/IBO/shader

RenderObject refactor (design doc RenderObject重构方案)
- draw(RenderObject&, target = nullptr, viewport = full window)
- allows draw(obj) to render to the screen at full window by default
- Rename test/graphic/test_window.cpp to test_render.cpp (GLOB picks it up,
  produces test_render.exe)
- Use new RenderObject API with default target/viewport: renderer.draw(obj)
  for screen full-window, explicit target for FBO draws
- Size FBO color textures from the FBO, not the global SCR_WIDTH/SCR_HEIGHT
- Default per-command viewport to the render target size (optional viewport)
- Snapshot RenderState into RenderCommand so per-draw state is self-contained
- Force GL state sync on Device construction (initial shadow-state divergence)
- Reject draw commands with missing or invalid shaders at enqueue time
- Clear the screen once per frame in the render test
- Add a translucent triangle (alpha shader + SrcAlpha blend, 1:1 blend at
  u_alpha=0.8), offset 50px to the bottom-left, depth-tested between the
  quad and the comparison triangle
- Enable depth testing on the opaque objects so screen depth is real
- Add a stencil-mask pass in the FBO: a stencil triangle offset 50px up-right
  writes stencil=1 (Always/Replace), the FBO color is then cleared (keeping
  stencil), and the next triangle draws with NotEqual(1) so the masked region
  is left empty
@YuanSang0512

Copy link
Copy Markdown
Contributor Author

gkit::graphic::RenderObject 用户手册

1. 它是什么

RenderObject 是一个由 CPU 数据定义的、可复用的绘制单元:你提供顶点/索引数据 + 顶点布局 + 材质 + 渲染状态,VAO / VBO / IBO 的创建与绑定全部隐藏,在首次绘制时懒创建并缓存在对象内部。

对比旧 API:以前你要手动 create_vertex_array() / create_vertex_buffer() / add_buffer() 搭一套缓冲管线;现在只声明数据即可,一次性定义、反复 draw

头文件:RenderObject.hpp

2. 所有权模型(务必先看)

数据 谁拥有 生命周期
vertices / indices RenderObject(构造时拷贝) 跟随对象
material.shader / material.textures 外部(指针引用,不拥有) 由资源系统管理,可被多个 RenderObject 共享
GPU 缓冲(VAO/VBO/IBO) RenderObject(懒创建、缓存) 首次 draw 触发,之后复用

⚠️ 材质里的 shader/textures 只是指针。被引用的 shader/texture 必须在对象存活期间保持有效,删除前要保证不再被绘制。

3. 构造

RenderObject obj(
    const std::vector<float>& vertices,   // 交错排列的顶点数据
    const std::vector<uint32_t>& indices, // 索引
    const VertexBufferLayout& layout,     // 顶点属性布局
    const Material& material              // 材质(shader + 纹理槽 + uniforms)
);

layoutpush<T>(count) 声明每个属性的类型和分量数,push 的顺序即内存里的交错顺序:

VertexBufferLayout layout;
layout.push<float>(3); // position:vec3
layout.push<float>(2); // uv:vec2
layout.push<float>(4); // color:vec4
// 每顶点跨距 = (3+2+4) × 4 = 36 字节

布局类型只支持 float / uint32_t / unsigned char(详情见 VertexBufferLayout.hpp)。

4. 成员详解

4.1 Material material — 材质

Material.hpp

material.shader = shader.get();          // 必填,且必须有效(见 §6 坑1)
material.textures[0] = &fbo_texture;     // 最多 MAX_TEXTURE_SLOTS = 8 个槽
material.texture_count = 1;              // 实际用到的槽数
material.uniforms.values.push_back({"u_alpha", 0.8f}); // 逐条赋值
  • shader:指针引用,必填。无 shader 或编译失败的 shader 会让 draw() 拒绝入队并打印 error 日志。
  • uniforms(简单路径):name → value 列表,flush 时逐个调 set_uniform_*。支持 int / float / Vector3 / Vector4 / Matrix3 / Matrix4
  • ubo(批量路径):预留,UniformBuffer 后端尚未实现。

4.2 RenderState state — 渲染状态

RenderState.hpp,由四个子状态组成,默认全部禁用

obj.state.depth.enabled       = true;                          // 深度测试
obj.state.depth.compare_func  = CompareFunc::Less;             // 默认 Less
obj.state.depth.write_mask    = true;                          // 是否写深度

obj.state.blend.enabled       = true;                          // 混合
obj.state.blend.src_rgb       = BlendFunc::SrcAlpha;           // 源 RGB 因子
obj.state.blend.dst_rgb       = BlendFunc::OneMinusSrcAlpha;   // 目标 RGB 因子
// 还有 src_alpha / dst_alpha / equation(默认 Add)

obj.state.cull_face.enabled   = false;                         // 面剔除(默认关闭)
obj.state.cull_face.mode      = CullFaceMode::Back;            // 剔除面
obj.state.cull_face.front_face = FrontFace::CounterClockwise;  // 正面绕序

obj.state.stencil.enabled     = false;                         // 模板测试
obj.state.stencil.compare_func = CompareFunc::Always;
obj.state.stencil.ref         = 1;                             // 参考值
// 还有 read_mask / write_mask / fail / z_fail / z_pass

4.3 排序控制 — transparentdepth_key

这两个字段决定对象在渲染队列里的绘制顺序(排序逻辑见 RenderQueue.cpp):

obj.transparent = true;   // 排序类别:false=不透明,true=透明
obj.depth_key   = 0.0f;   // 排序键
类别 绘制顺序 depth_key 含义
不透明(transparent=false 的先画 越小越近,近优先(front-to-back)
透明(transparent=true 的后画 越小越远,远优先(back-to-front)

为什么透明要反过来? 透明物体靠混合叠加,必须先画远处、再画近处,后画的近物才能正确地叠加在远物之上。所以透明物体要设一个比不透明物体更小的 depth_key 让它排最后。

整体排序优先级(由队列保证):

  1. FBO 目标命令先画,屏幕命令后画(这样后处理才能采样 FBO 结果)
  2. 不混合 → 混合
  3. 不透明 → 透明
  4. 组内再按 depth_key

4.4 绘制控制 — instance_count / clear / clear_flags

obj.instance_count = 1;                          // >1 时走实例化绘制
obj.clear          = false;                      // 绘制前是否清目标
obj.clear_flags    = ClearFlags::All;            // 清什么:Color / Depth / Stencil / ColorDepth / All

注意 clear逐命令的:一次 draw() 前的清屏只对该命令生效,不会影响其他 draw。

5. 与 Renderer 配合

auto& renderer = gkit::graphic::Renderer::instance();
renderer.init();                                  // 默认 OpenGL 后端

// 定义一次
gkit::graphic::RenderObject triangle_obj(vertices, indices, layout, material);

// 每帧:入队 → 执行
renderer.draw(triangle_obj);                      // 目标=屏幕,视口=全窗口
renderer.draw(triangle_obj, fbo.get());           // 渲染到 FBO
renderer.draw(triangle_obj, nullptr, viewport);   // 自定义视口
renderer.flush();                                 // 排序 + 应用状态 + 真正绘制

三个关键点:

  • draw() 只入队,flush() 才真正画。每帧在 swap buffer 前调一次 flush()
  • draw() 接收非 const 引用:执行时可能懒上传 GPU 资源。
  • 每次 draw() 都会把对象的 state 快照进命令。所以同一个 RenderObject 可以用不同状态多次 draw——例如先关深度画一次、再开深度画一次。

6. 坑与注意

  1. shader 必须有效draw() 会拒绝无 shader 或编译/链接失败的对象,并打 error 日志(见 Renderer.cpp)。
  2. 默认视口是全窗口(SCR 500×500),不是目标尺寸。渲染到尺寸不同的离屏 FBO 时,务必显式传匹配 FBO 的 Viewport,否则 GL 全局视口会错位。
  3. 透明物体要自己管理深度:如果目标上先画了不透明物体,记得给透明物体开深度测试(state.depth.enabled = true),否则会渲染错层;深度关闭时不写深度缓冲。
  4. FBO 必须先于屏幕命令:队列已强制排序,但如果你手写多遍后处理链,注意每个 FBO 都要用独立的命令。
  5. 对象的生命周期:对象须存活到 flush() 执行完;传入命令的是对象指针。

7. 完整示例

一个混合 + 深度测试的最小场景(完整可运行版见 test_render.cpp):

// —— 初始化 ——
auto& renderer = gkit::graphic::Renderer::instance();
renderer.init();
auto& device = renderer.get_device();

// —— 几何:彩色三角形(位置 + 颜色)——
std::vector<float> vertices = { /* x,y,z, r,g,b ×3 */ };
std::vector<uint32_t> indices = {0, 1, 2};
gkit::graphic::VertexBufferLayout layout;
layout.push<float>(3); layout.push<float>(3);

// —— 材质 ——
gkit::graphic::Material tri_material;
tri_material.shader = device.create_shader("color_triangle.shader").get();
// 若 shader 带 uniform:
tri_material.uniforms.values.push_back({"u_alpha", 0.8f});

// —— 定义对象 ——
gkit::graphic::RenderObject tri_obj(vertices, indices, layout, tri_material);
tri_obj.state.depth.enabled = true;                    // 开深度测试
tri_obj.transparent         = true;                    // 透明类,排后面
tri_obj.depth_key           = 0.0f;                    // 最近,最后画
tri_obj.state.blend.enabled = true;                    // 开混合
tri_obj.state.blend.src_rgb = gkit::graphic::BlendFunc::SrcAlpha;
tri_obj.state.blend.dst_rgb = gkit::graphic::BlendFunc::OneMinusSrcAlpha;

// —— 渲染循环 ——
while (running) {
    renderer.clear(gkit::graphic::ClearFlags::ColorDepth);
    renderer.draw(tri_obj);   // 入队
    renderer.flush();         // 执行
    SDL_GL_SwapWindow(window);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant