本节摘要:UI 与文字不该用三维三角形硬拼。Direct2D 承接矢量图形与合成,DirectWrite 承接排版,两者经互操作层与 Direct3D 共用一块画布。本节讲清分工与集成方式,并给出 HUD 文本渲染的完整路径。
某团队早期项目坚持"一切都是三角形":字体先用工具烘焙成多边形轮廓,UI 面板用着色器画圆角。三个月后维护噩梦成真——中文字库动辄数万字符,烘焙的多边形数据以百兆计;改一行文案要重新出资产;不同字号要重烘;字体渲染的细微锯齿在高分屏上无处可藏。返工方案是 Direct2D 与 DirectWrite:文字在运行时按需排版光栅化,面板用几何与画刷描述。教训先行:三维管线擅长"每一帧都在变的世界",二维与文字有自己更高效的专门工具——用对工具比统一世界观重要。
Direct2D 与 DirectWrite 的分工像绘图与排版两个工种。Direct2D 管矢量绘图与合成:矩形、路径、渐变、位图、图层混合,API 面向"画什么形状、什么颜色、怎么叠加"。它的内部实现聪明——几何图元最终也会变成三角形与像素工作,但驱动侧针对二维场景做了批量优化,用的人不必关心。DirectWrite 管文字排版:字体枚举、字形测量、换行断行、双向文本、OpenType 特性,产出的是"字形与位置"的排版结果,再交给 Direct2D 绘制。文字渲染的坑大多在排版而非绘制——不同语言的方向、字距与换行规则,手写必然漏,专业组件把这些都收进去了。

互操作的关键是让两套 API 看见同一块表面。以在交换链后备缓冲上叠 HUD 为例,路径分三步:
一 · 建桥:用交换链缓冲创建 ID2D1DeviceContext(经 DXGI 表面包装) 二 · 排序:三维先画(D3D 命令),UI 后画(D2D 绘制),层次由顺序决定 三 · 上屏:Present 照旧——两套 API 只是轮流在同一画布上作画
文本绘制的代码骨架(省略初始化):
// 排版:DirectWrite 建文本格式与布局(字号、字重、对齐) ComPtr<IDWriteTextFormat> fmt; dwriteFactory->CreateTextFormat(L"Microsoft YaHei", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 24.0f, L"zh-cn", fmt.GetAddressOf()); // 绘制:Direct2D 在共享表面上画排版结果 d2dContext->BeginDraw(); d2dContext->DrawTextW(text.c_str(), (UINT32)text.size(), fmt.Get(), D2D1::RectF(20, 20, 400, 80), blackBrush.Get()); d2dContext->EndDraw(); // 内部会生成并提交对应的三维命令
性能上有一条纪律:高频文本缓存成位图。DrawTextW 每次都走排版与光栅化,帧帧执行浪费;血条数值这类每帧变化的内容没办法,得分与技能名这类稳定文本应绘制进离屏位图、之后每帧只贴图。这个"排版一次、多次贴图"的模式,与三维世界"加载一次、多次引用"的资源纪律同构。
因为"把字画出来"背后压着一整条排版流水线。第一步是字符到字形的映射:同一个码点在不同字体里对应不同轮廓,一个"字"还可能拆成多个字形组合。第二步是轮廓到像素:字形是贝塞尔曲线描述的矢量轮廓,栅格化时要决定抗锯齿策略——灰度平滑还是亚像素定位,小字号还要处理微调的取舍。第三步中文场景特有的深水区:字体回退——一段混排文本里中英日文与表情符号各归各的字体,缺字的字体链式兜底;断行规则——中文可以在任意字符间断行而英文要按词,标点不能悬在行首。这些规则 DirectWrite 全都内置,自研文本渲染的团队最后几乎都栽在这几处——开头那个案例里"中文标点断行一堆问题",栽的就是第三步。文字渲染的成本不在画,而在把这些几十年沉淀的排版常识补齐;能用现成排版引擎,就别自己重写一遍行业史。
Direct2D 还有一块常被忽略的能力:效果系统(Effects)。它是一张 GPU 加速的图像处理节点图——模糊、阴影、色彩矩阵、卷积、高光阈值等几十种效果现成可链,节点输出接节点输入,整张图一次提交 GPU 执行。对桌面工具类软件(图片批处理、截图标注、简单滤镜)这是省下整套自研后处理的捷径;对游戏,它也能在 UI 层做些轻量后期(血条泛光、界面高斯模糊衬底)。用法上它与绘制模型同构:效果对象接图像源,产出仍是可继续合成的表面。记住它的存在即可——需要图像处理时,先查效果目录有没有现成节点,往往比伸手写计算着色器更快更稳;两者的能力边界,正好由下一节的计算着色器补完。
这类"清晰度悬案"多半栽在 DPI 上。Direct2D 与 DirectWrite 都是 DPI 感知的:文本以逻辑坐标排版,最终乘以缩放系数落到物理像素。发虚的根源是用错基准——按 96 DPI 的假设把位图渲染到高缩放屏上,系统把位图拉伸,文字边缘糊掉。正解是让渲染分辨率跟随窗口的实际物理尺寸(缩放系数变化时重建渲染目标),把"逻辑坐标排版、物理像素呈现"的两个世界显式对齐。多显示器混合缩放(一台 100% 一台 150%)是重灾区:窗口跨屏移动时缩放系数变化,不处理重建的界面必然有一侧发虚。这类问题在文本上最刺眼——字形笔画只有一两个像素宽,半像素的错位肉眼立现;这也反过来说明为什么排版引擎必须独立于分辨率工作。
什么时候用 Direct2D,什么时候仍该用三维管线画二维?给出实用边界:全屏 UI 框架、编辑器界面、数据可视化——Direct2D 的矢量与合成模型更高效,圆角、虚线、渐变都是一行参数;与三维场景深度耦合的二维效果——准星投影、地形轮廓标注、屏幕空间特效——留在 Direct3D 里做,它们需要深度信息或投影变换,D2D 够不着。边界之内各用所长。
案例收尾:某策略游戏的战局界面重做,原来 UI 全在引擎的三维管线上拼(团队自写文本渲染,中文标点断行问题一堆),迁移方案是三维场景照旧、UI 层整体换 Direct2D 与 DirectWrite,文本排版问题清零,圆角面板从"着色器黑科技"变成普通 API 调用;代价是引入两套 API 的初始化与互操作复杂度——工程上这笔账通常划算,前提是团队吃透本章的集成三步。
二维电路接完,接下来借 GPU 的算力干图形之外的活:计算着色器与 DirectML 的实战场,下一节开算。