3.5 2D图形渲染基础
2D图形渲染是很多应用的基础,网页的UI、App界面、2D游戏等都是通过2D渲染技术绘制出来的。理解2D渲染的基本原理,能够帮助我们写出性能更好、效果更优的图形应用。
2D渲染管线基本流程
渲染管线(Rendering Pipeline)是指将图形数据转换为屏幕像素的整个过程,2D渲染管线相对3D来说比较简单,主要包含以下几个步骤:
1. 几何处理阶段
- 顶点输入:输入要绘制的图形的顶点坐标,比如矩形的四个顶点,三角形的三个顶点
- 坐标变换:将顶点从模型坐标转换到屏幕坐标,包括平移、旋转、缩放等变换
- 裁剪:裁剪掉屏幕外的部分,只保留需要绘制的内容
2. 光栅化阶段
- 扫描转换:将矢量图形(点、线、多边形)转换为像素网格上的片元(Fragment,也就是候选像素)
- 属性插值:计算每个片元的颜色、透明度、纹理坐标等属性,通过顶点属性插值得到
3. 片元处理阶段
- 纹理采样:如果使用纹理,根据纹理坐标采样纹理的颜色
- 颜色计算:计算每个片元的最终颜色,包括填充色、描边色、特效等
- 混合测试:和帧缓冲区中已经存在的像素进行混合,处理半透明效果
- 写入帧缓冲区:将最终的像素颜色写入到帧缓冲区中
常见2D渲染API
1. Canvas API
Canvas是HTML5提供的2D绘图API,通过JavaScript操作像素来绘制图形:
- 特点:
- 立即模式渲染,调用绘制函数后立即绘制到画布上
- 适合绘制大量动态的图形,比如游戏、数据可视化
- 性能好,适合复杂的动画和大量元素的场景
- 缩放会失真,因为是位图绘制
- 适用场景:2D游戏、复杂动画、数据可视化图表、图像处理
2. SVG(Scalable Vector Graphics)
SVG是基于XML的矢量图形格式,也是浏览器原生支持的2D绘图技术:
- 特点:
- 保留模式渲染,图形作为DOM节点存在,可以用CSS和JavaScript操作
- 矢量图形,缩放不失真
- 支持事件绑定,可以交互
- 元素太多时性能较差
- 适用场景:图标、插图、简单动画、需要交互的图形、需要适配不同分辨率的场景
3. Skia
Skia是Google开发的开源2D图形库,是Chrome浏览器、Android系统、Flutter框架的底层渲染引擎:
- 特点:
- 跨平台,性能优异
- 支持硬件加速
- 功能全面,支持各种2D绘制功能
- 是很多上层框架的底层依赖
4. Cairo
Cairo是开源的2D图形库,常用于GTK、GNOME等Linux桌面环境:
- 特点:
- 跨平台,支持多种输出后端
- 矢量渲染质量高
- 适合桌面应用的UI渲染
5. Direct2D
微软开发的Windows平台的2D渲染API,支持硬件加速,是Windows应用的主要2D渲染接口。
坐标系统与变换
常见的坐标系统
- 模型坐标(局部坐标):相对于图形自身原点的坐标
- 世界坐标:相对于整个画布原点的坐标
- 屏幕坐标(设备坐标):相对于屏幕左上角的坐标,通常左上角是(0,0),x轴向右,y轴向下,这和数学上的笛卡尔坐标系y轴向上不同,要特别注意。
常见的坐标变换
所有的坐标变换都可以通过矩阵乘法来实现:
1. 平移(Translate)
将图形移动到指定位置:
x' = x + tx
y' = y + ty
2. 旋转(Rotate)
绕原点旋转θ角度(顺时针为正,因为y轴向下):
x' = x * cosθ - y * sinθ
y' = x * sinθ + y * cosθ
3. 缩放(Scale)
沿x轴和y轴缩放:
x' = x * sx
y' = y * sy
4. 仿射变换(Affine Transform)
平移、旋转、缩放的组合,可以表示为3x3的矩阵:
[ a c tx ]
[ b d ty ]
[ 0 0 1 ]
变换公式:
x' = a*x + c*y + tx
y' = b*x + d*y + ty
多个变换可以通过矩阵乘法组合成一个变换矩阵,一次性应用到所有顶点,提升性能。
合成与混合模式
当多个图形重叠时,需要处理它们之间的合成关系,混合模式(Blend Mode)定义了上层像素和下层像素如何混合。
基本混合公式
结果颜色 = 上层颜色 * 源因子 + 下层颜色 * 目标因子
常见的混合模式
- 正常(Normal):默认模式,上层像素覆盖下层像素,如果上层有透明度,根据Alpha值混合:
结果 = 上层颜色 * 上层Alpha + 下层颜色 * (1 - 上层Alpha) - 正片叠底(Multiply):上下层颜色相乘,结果更暗,类似于两张透明胶片叠在一起
- 滤色(Screen):上下层颜色的补色相乘再取补,结果更亮,类似于两张幻灯片叠加投影
- 叠加(Overlay):结合正片叠底和滤色,亮部更亮,暗部更暗,增加对比度
- 相加(Add):上下层颜色相加,结果更亮,常用于发光、火焰等效果
- 差值(Difference):上下层颜色相减的绝对值,常用于反色效果
不同的混合模式可以实现各种特效,很多绘图软件和Canvas都支持这些混合模式。
2D渲染性能优化要点
2D渲染的性能问题通常出现在绘制大量元素或者复杂动画的场景,这里分享一些优化技巧:
1. 减少绘制调用(Draw Call)
- 绘制调用是GPU执行的绘制命令,每次绘制调用都有 overhead
- 尽量将多个小的绘制合并为一个大的绘制,比如将多个小图形合并到一个大的Canvas中
- 使用批处理技术,一次性绘制多个元素
2. 避免不必要的重绘
- 只重绘变化的区域,不要每次都重绘整个画布
- 使用脏矩形(Dirty Rectangle)技术,只更新需要变化的区域
- 离屏渲染:将不经常变化的内容先绘制到离屏Canvas上,需要的时候直接把整个离屏Canvas绘制到主画布上,避免每次都重复绘制这些元素
3. 利用硬件加速
- 现代GPU对2D渲染也有很好的加速支持,尽量使用支持硬件加速的渲染API
- 避免频繁读写像素数据,比如Canvas的getImageData/putImageData操作很慢,尽量少用
- 纹理尽量使用2的幂次方尺寸,GPU处理起来效率更高
4. 优化复杂绘制
- 大量重复的图形可以使用缓存,绘制一次后复用结果
- 矢量图形的光栅化比较耗时,复杂的矢量图形可以缓存为位图
- 动画尽量用transform和opacity属性,这些属性可以由GPU合成,不需要重绘
5. 避免过度绘制
- 过度绘制(Overdraw)是指同一个像素被多次绘制,浪费性能
- 尽量按照从后到前的顺序绘制,不需要绘制被完全遮挡的部分
- 对于半透明元素,要注意绘制顺序,从后往前绘制
Canvas vs SVG性能对比
- 元素数量少(<1000),需要交互:SVG性能更好,因为作为DOM节点操作方便
- 元素数量多(>1000),动态变化:Canvas性能更好,因为没有DOM overhead
常见的2D渲染问题
1. 图形边缘锯齿
- 原因:光栅化时没有做抗锯齿处理
- 解决:开启抗锯齿(AA),现在的渲染API默认都会开启抗锯齿
2. 文字模糊
- 原因:Canvas绘制文字时坐标不是整数,或者画布缩放后没有正确处理
- 解决:绘制时坐标取整,高DPI屏幕下将画布尺寸放大为CSS尺寸的2倍,然后用CSS缩小显示,提升清晰度
3. 动画卡顿
- 原因:绘制时间太长,达不到60fps
- 解决:优化绘制流程,减少绘制调用,利用硬件加速,将复杂计算放到Web Worker中
4. 半透明混合异常
- 原因:绘制顺序不对,或者混合模式设置错误
- 解决:半透明元素要从后往前绘制,使用正确的混合模式,注意预乘Alpha的问题
思考问题
- Canvas和SVG各有什么优缺点?分别适合什么场景?
- 什么是渲染管线?2D渲染管线主要包含哪几个阶段?
- 为什么y轴向下的屏幕坐标系和数学上的y轴向上的坐标系不同?开发中要注意什么?
- 提升2D渲染性能有哪些常用的优化技巧?