Lesson10:渲染层入门
系列:MiniCAD第 12 / 17 篇
- MiniCAD 初级教程课程规划
- C++ 基础课:读懂 MiniCAD 所需的语言知识
- Lesson01:项目能跑起来
- Lesson02:整体架构地图
- Lesson03:程序入口与主循环
- Lesson04:输入系统与消息映射
- Lesson05:绘图工具如何工作
- Lesson06:Core 核心层:数学、几何、对象与实体
- Lesson07:命令模式与撤销重做
- Lesson08:选择、拾取与夹点编辑
- Lesson09:视口、相机与坐标转换
- Lesson10:渲染层入门
- Lesson11:文字与字体系统
- Lesson12:吸附与约束系统
- Lesson13:命令行系统
- Lesson14:多文档管理
- Lesson15:综合实战:扩展一个 MiniCAD 功能
目录
学习目标
读懂 IRenderer 接口设计,理解两种顶点类型和两种图元类型,掌握 Viewport::Render() 的完整渲染顺序,理解场景矩阵和屏幕矩阵的使用时机,以及 D3D11 和 WebGL 如何通过同一接口工作。
相关源码
src/Render/IRenderer.hsrc/Render/IRenderTarget.hsrc/Render/VertexTypes.hppsrc/Render/RendererFactory.hppsrc/Viewport/Viewport.h/Viewport.cppsrc/Viewport/ViewState.h
顶点类型
struct Vertex_P3_C4 {
Math::Float3 pos; // xyz 位置
Math::Float4 color; // rgba 颜色
};
struct Vertex_P3_C4_UV {
Math::Float3 pos;
Math::Float4 color;
Math::Float2 uv; // 纹理坐标(文字用)
};
Float3/Float4 vs Point3/Vec3/Color4:渲染层用 Float3/Float4(单精度,GPU 格式),Core 层用 Point3/Vec3/Color4(双精度,计算用)。DrawContext 负责在两者之间做类型转换:
// DrawContext 中的转换
m_verts.push_back({
{ (float)pt.x, (float)pt.y, (float)pt.z },
{ (float)col.r, (float)col.g, (float)col.b, (float)col.a }
});
IRenderer 接口
class IRenderer {
virtual void BeginFrame(IRenderTarget& target, const ViewportDesc& viewport) = 0;
virtual void EndFrame() = 0;
virtual void Submit(
span<const Vertex_P3_C4> verts,
const Mat4& viewProj,
PrimitiveType type,
bool depth = true,
bool blend = false) = 0;
virtual void SubmitTextured(
span<const Vertex_P3_C4_UV> verts,
const Mat4& viewProj,
void* nativeSRV,
bool depth = false,
bool blend = true) = 0;
virtual void* GetNativeDevice() = 0;
};
enum class PrimitiveType { Line, Triangle };
参数说明:
| 参数 | 含义 |
|---|---|
IRenderTarget& | 渲染目标(D3D11 RTV 或 WebGL Framebuffer) |
ViewportDesc | 视口区域(x, y, width, height, minDepth, maxDepth) |
span<verts> | 零拷贝顶点数据引用 |
viewProj | MVP 矩阵(场景用相机矩阵,UI 用屏幕正交矩阵) |
PrimitiveType::Line | 线列表:每两个顶点一条线段 |
PrimitiveType::Triangle | 三角形列表:每三个顶点一个三角形 |
depth | 是否启用深度测试(2D 场景通常不需要) |
blend | 是否启用 alpha 混合(半透明元素需要) |
nativeSRV | D3D11:ID3D11ShaderResourceView*;WebGL:GLuint 纹理 ID |
RendererFactory:平台路由
// RendererFactory.hpp
#ifdef MINICAD_WEB
// WebGLRenderer 构造
#else
// D3D11Renderer 构造
#endif
CreateRenderer(RendererCreateInfo) 根据编译时宏返回对应实现。调用方(MainWindow::InitD3D11 或 WebMain::main)只持有 IRenderer*,不知道底层是 D3D11 还是WebGL。
Viewport::Render 完整渲染顺序
以下是每帧 Viewport::Render(viewState) 的执行步骤:
1. BeginFrame(renderTarget, viewportDesc)
└─ 绑定渲染目标,设置视口矩形
2. 绘制网格(Grid) ← 屏幕正交矩阵
└─ 等间距水平/垂直虚线
3. 绘制坐标轴(Axis) ← 场景矩阵(随相机移动)
└─ X 轴红色,Y 轴绿色
4. 绘制 Gizmo(左下角坐标指示器) ← 屏幕正交矩阵(固定位置)
5. 绘制光标框(Cursor) ← 屏幕正交矩阵(跟随鼠标)
└─ 十字光标 + 中心小方框
6. 绘制场景实体:Submit(viewState.Scene, viewProj, Line)
└─ 所有 Entity::Draw 产生的线段顶点
7. 绘制 Overlay:Submit(viewState.Overlay, viewProj, Line)
└─ 工具预览线、约束辅助线
8. 绘制文字:SubmitTextured(viewState.TextScene, viewProj, fontTexture, blend=true)
└─ TextEntity 产生的纹理四边形,使用 ImGui 字体图集
9. 绘制吸附标记:BuildSnapGeometry → Submit(snap, screenOrtho, Triangle)
└─ 端点吸附:小方框;中点:三角形;最近点:X 形
10. 绘制选择框:BuildSelectionGeometry → Submit(fill + border, screenOrtho, Triangle+Line)
└─ 半透明填充 + 实线边框
11. 绘制夹点:BuildGripGeometry → Submit(fill + border, screenOrtho, Triangle+Line)
└─ 每个夹点:实心小方块 + 边框
12. EndFrame()
└─ D3D11:Present 前的收尾;WebGL:flush
两套矩阵的使用规则
| 元素 | 使用矩阵 | 原因 |
|---|---|---|
| 场景实体(线段、圆等) | camera.GetViewProj() | 随相机平移/缩放 |
| 工具预览(Overlay) | camera.GetViewProj() | 是图纸上的几何 |
| 文字(TextScene) | camera.GetViewProj() | 文字附着在图纸上,随相机变换 |
| 网格 | 屏幕正交 | 始终铺满视口 |
| 夹点方块 | 屏幕正交 | 固定像素大小,不随缩放变化 |
| 吸附标记 | 屏幕正交 | 固定像素大小 |
| 选择框 | 屏幕正交 | 像素拖拽,与图纸无关 |
| 光标框 | 屏幕正交 | 跟随鼠标像素位置 |
为什么文字用 SubmitTextured
TextEntity(单行文字)的字形来自 ImGui 字体图集(一张预烘焙的字形纹理)。DrawContext::EmitText() 把每个字符生成两个三角形(一个矩形),UV 坐标指向字形在图集中的位置。渲染时需要把这张纹理绑定到 shader 才能采样——这就是 SubmitTextured 的 nativeSRV 参数。
MTextEntity(多行文字,SHX/TTF 矢量字体)则走 DrawLine 路径,字形是线段,直接进入 m_sceneVertices,走普通的 Submit。
D3D11Renderer 要点(了解)
BeginFrame:调用ClearRenderTargetView,设置RSSetViewportsSubmit(PrimitiveType::Line):IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST)Submit(PrimitiveType::Triangle):IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST)- 每次
Submit把顶点数据Map → memcpy → Unmap到动态 Vertex Buffer
WebGLRenderer 要点(了解)
BeginFrame:glViewport,glClearSubmit(PrimitiveType::Line):glDrawArrays(GL_LINES, ...)Submit(PrimitiveType::Triangle):glDrawArrays(GL_TRIANGLES, ...)- 每次
Submit用glBufferData上传数据(动态缓冲)
两者都是**"每帧重传顶点"**的简单模型,没有静态 VBO 池或分批合并优化。
拓展练习
- 追踪一个圆形实体从
CircleEntity::Draw()到IRenderer::Submit()的完整路径,数一数中间经过了几次数据传递。 SubmitTextured的nativeSRV参数类型是void*,为什么不用模板或接口?这种设计有什么优缺点?- 如果要新增一种渲染效果(例如:选中的实体用虚线显示),需要改动哪些文件?修改范围有多大?
- 夹点、吸附标记用
PrimitiveType::Triangle而不是Line,为什么?(提示:想想如何绘制一个实心方块)