2d图形渲染本质是只有两大类,CPU渲染和GPU渲染。
现代计算机图形基本上都采用GPU渲染了。
但在一些低端设备和钳入式小程序依然存在大量靠CPU软渲染实现2D图形渲染。
如skia它支持cpu渲染(软渲染),也支持gpu渲染。默认就是GPU渲染。前端2d渲染引擎几乎都是使用webgl(gpu)来实现的如mapbox,pixijs,mesh2d等
但我觉得CPU软渲染不会过时,用typescript写主要用于学习,性能不是主要的。相比c++主要调试更方便。光栅化的代码主要参考,c和c++图形库cairo和agg
软渲染实现流程
- Path 矢量路径(类似Path2D,管理绘制命令),支持绘制命令(moveTo,LineTo,cubicTo,等),计算边界、路径扁平化 、变换矩阵
- Context 上下文,管理paint状态栈和暴露对外的绘制命令,paint 对象(包含填充的样式和规则),clip裁剪路径功能,相当把裁剪路径渲染光栅化一遍得到span,再与实际绘制的span做一个相交处理.就是在clip路径内的像素就显示
- Outline 从Path对象导出矢量路径到outline中,如果是Stroke绘制,要生轮廓,专业一点叫多边形偏移,像clipperJs库就有这种功能(Stroke绘制的填充,通过LineWidth,lineJoin,lineCap,miterlimit等属性,生成对应的轮廓)
- Outline 26.6定点数整数路径(使用Bigint类型,后续可以进32位以上的位运算或负数的位运算),用于光栅化时遍历,生成Span {x,y,len,coverage}对象)用于渲染扫描线,
- 光栅化扫描转换 (光栅化的过程会生成一系列的cell和span,这种模式叫灰度抗锯齿光栅化,计算像素的覆盖率(alpha)来调整像素颜色(src*alpha+dst*(1-alpha)) 。这块参考agg,freetype),如果是GPU渲染的化,这块就更简单,直接把路径用mapbox的earcut库,把路径三角化丢给GPU自己渲染。GPU的抗锯齿方案就更多了,MSAA、FXAA、SMAA、TXAA等
- Blend颜色合成 (rgb颜色、渐变颜色、纹理颜色(提取图片的颜色))如果是GPU渲染,这块都在片段着色器完成了
数据结构
- Path,Point,Matrix,Rect 路径和矩形窗口裁剪
- Context,Surface 上下文和像素缓冲区
- Paint,Gradient,Color,Texture,LineJoin,LineCap,FillRule 图形样式
- Outline,Span,Cell,Raster 图形轮廓,光栅化
TypeScript
export class Span {
x: int = 0; // 起始像素的 x 坐标
len: int = 0; // 像素段长度,从 x 开始向右延伸 len 像素
y: int = 0; // 当前 span 所在的 y 坐标(扫描线行号)
coverage: int= 0; // 灰度覆盖值(0-255),表示此 span 的覆盖程度
};
export class Cell {
x: int = 0 // 当前 cell 的 x 坐标(整数像素位置)
cover: int = 0 // 覆盖计数器(累加 y 子像素上的覆盖值)
area: int = 0 // 面积累加器(用于精细抗锯齿灰度计算)
next: TCell | null = null // 下一个 cell
}
export class Raster{
ex: int= 0; // 当前点的 x 坐标(子像素精度)
ey: int= 0; // 当前点的 y 坐标(子像素精度)
min_ex: int= 0 // 所有轮廓点中的最小 x(边界框)
max_ex: int= 0; // 最大 x
min_ey: int= 0 // 最小 y
max_ey: int= 0; // 最大 y
count_ex: int= 0 // 扫描线宽度(x 像素数量)
count_ey: int= 0; // 扫描线高度(y 像素数量)
area: int= 0; // 当前正在处理的单元格的临时面积值
cover: int = 0; // 当前单元格的覆盖值
invalid: int = 0; // 标记某些内部状态是否失效
cells: int[] = []; // 所有 cell 的缓冲区,用于构建覆盖图像
num_cells: int= 0; // 当前已使用的 cell 数量
x: int= 0 // 逻辑处理用的当前像素 x 坐标
y: int= 0; // 当前像素 y 坐标(整数)
outline!: Outline; // 要扫描转换的轮廓路径
clip_box!: Rect; // 可选裁剪框(bounding box)
gray_spans: Span[] = new Array(MAX_GRAY_SPANS).fill(0).map(() => new Span());
// 缓存生成的灰度 span,用于最终输出或回调
num_gray_spans: int = 0; // 当前灰度 span 数量
skip_spans: int = 0; // 是否跳过 span 渲染(例如用于裁剪)
render_span: FT_Raster_Span_Func | null = null;// 渲染 span 的回调函数(最终输出 span)
ycells: (TCell | null)[] = [];// 每个 y 扫描线所对应的 cell
ycount: TPos = 0; // 当前 ycells 的总行数
}
代码API类canvas方式
TypeScript
import { Context } from './ctx'
import { FillRule, LineCap, LineJoin } from './paint';
import {Surface} from './surface'
const surface=Surface.create(500,500)
const ctx=Context.create(surface)
const canvas=document.getElementById('canvas') as HTMLCanvasElement;
canvas.width=surface.width
canvas.height=surface.height
const nativeCtx=canvas.getContext('2d')!
ctx.save()
ctx.newPath()
ctx.setSourceRGB(1,0,0)
var gradinet=ctx.setSourceRadialGradient(250,250,80,250,250,0)
gradinet.addStopRgb(0,1,0,0)
gradinet.addStopRgb(0.5,0,1,0)
gradinet.addStopRgb(1,0,0,1)
ctx.setLineWidth(10)
ctx.setLineJoin(LineJoin.MITER)
//ctx.setDash([10,5],0)
ctx.rectangle(200,200,100,100)
ctx.fill()
ctx.restore()
ctx.save()
ctx.newPath()
ctx.setLineJoin(LineJoin.ROUND)
ctx.setLineCap(LineCap.ROUND)
ctx.setLineWidth(10)
ctx.setSourceRGB(1,0,0)
ctx.moveTo(100,100)
ctx.lineTo(200,100)
ctx.lineTo(100,200)
ctx.stroke()
ctx.restore()
ctx.save()
ctx.newPath()
ctx.setLineJoin(LineJoin.MITER)
ctx.setLineCap(LineCap.SQUARE)
ctx.setLineWidth(10)
ctx.setSourceRGB(1,0,0)
ctx.moveTo(40,300)
ctx.lineTo(160,300)
ctx.lineTo(40,400)
ctx.stroke()
ctx.restore()
ctx.save()
ctx.newPath()
ctx.setLineJoin(LineJoin.MITER)
ctx.setLineCap(LineCap.SQUARE)
ctx.setLineWidth(10)
ctx.setDash([10,25,10],0)
ctx.setSourceRGB(1,0,0)
ctx.moveTo(240,330)
ctx.lineTo(460,330)
ctx.stroke()
ctx.restore()
ctx.save()
ctx.newPath()
ctx.setSourceRGB(1,0,0)
ctx.arc(300,100,50,0,Math.PI*2,false)
ctx.clip()
ctx.rectangle(250,50,100,100)
ctx.fill()
ctx.restore()
nativeCtx.putImageData(new ImageData(surface.pixels!,surface.width,surface.height),0,0)
效果
