本篇的目的是要了解:
- 裁剪操作
- 裁剪效果分析
- 渲染状态以及异步操作对裁剪的影响
- 裁剪实现的原理。实际上渲染的流程也是如此。只是渲染的目标缓冲区不一样
今天的这一章很重要,如果想让你的2D游戏引擎或UI引擎具有高速的渲染速度,那么局部脏区重绘是关键技术之一。而该技术的基础是裁剪!
我们今天来关注一下一个很关键且有点难度的操作:clip
1. 添加代码到draw函数中,用于表示是否是裁剪还是绘制操作:
drawRect(rc, style = "red", isFill = true, isClip = false, compsiteOp = '') {
let ctx = this.context;
//如果isClip为true,就进行裁剪而不是绘制
if (isClip == true) {
ctx.beginPath();
//使用clip而不是fillRect等操作,因为我们只需要路径,而不需要颜色填充
ctx.rect(rc.x, rc.y, rc.width, rc.height);
//路径绘制,使用stroke或fill
//路径裁剪,使用clip
ctx.clip();
//直接退出
return;
}
ctx.save();
if (compsiteOp != null && compsiteOp.length != 0)
ctx.globalCompositeOperation = compsiteOp;
ctx.beginPath();
if (isFill) {
ctx.fillStyle = style;
ctx.fillRect(rc.x, rc.y, rc.width, rc.height);
} else {
ctx.strokeStyle = style;
ctx.strokeRect(rc.x, rc.y, rc.width, rc.height);
}
ctx.restore();
}
测试代码如下:
let canvas = document.getElementById("myCanvas");
let context = canvas.getContext('2d');
let render = new BLFRender(context);
render.clear();
//没有裁剪前的绘制
//网格
render.drawGrid('white', 'black', 20, 20);
render.pushStates();
render.setShadowState();
render.setLineState();
//红色填充rect
render.drawRect(new Rect(12, 22, 80, 40));
//下面代码开始进行裁剪操作
//*****下面函数最后一个参数为true,表示clip******
render.drawRect(new Rect(112, 22, 80, 40), 'blue', false, true);
//裁剪后的绘制
render.drawCircle(new Circle(170, 70, 50));
render.drawCircle(new Circle(390, 70, 50), 'blue', false);
/*
下面两个函数共同点:
1. 圆心都为【600,60】,半径都为50px
2. 起始角都为30度,结束角都为180度
3. isClosed都设置为false,不封闭arc路径
下面两个函数不同点:
1. 上面函数isCCW(是否逆时针) = false,style = red
2. 下面函数isCCW(是否逆时针) = true, style = blue
*/
render.drawArc(new Arc(490, 70, 50, 30, 180, false), 'red', false);
render.drawArc(new Arc(490, 70, 50, 30, 180, false, true), 'blue', false);
//文字渲染效果,大家可以最后几个参数,查看字体,各种对齐方式
render.setTextState();
render.drawText(200, 40);
render.drawText(100, 80);
let pts = [new Point(550, 25), new Point(650, 25), new Point(650, 90)];
//大家可以修改最后两个参数,可以看到是否封闭路径,是否填充路径等效果
render.drawPoints(pts, 'blue', false, false);
let image = new Image();
let image1 = new Image();
image.src = "./data/doom3.png";
image1.src = "./data/ardunio_nano.jpg";
image.onload = function(e) {
let pattern = render.createPattern(image);
if (pattern) {
//即使裁剪了,你会发现图片绘制代码依旧ok
//这是因为图片载入是异步操作,可能的情况是所有的图形都绘制完成了
//才会绘制图像
render.drawCircle(new Circle(200, 200, 100), pattern);
render.drawCircle(new Circle(200, 200, 100), "rgba(255,0,0,0.5)");
render.drawArc(new Arc(420, 200, 100, 30, 180), pattern);
}
}
image1.onload = function(e) {
let pattern = render.createPattern(image1);
if (pattern) {
render.drawRect(new Rect(25, 320, 500, 200), pattern);
}
}
//渐变色
let colors = [{
weight: 0.1,
color: "blue"
}, {
weight: 0.2,
color: 'green'
}, {
weight: 0.7,
color: 'red'
}];
let coords = [550, 225, 750, 225]; //线性 4个参数[x0,y0,x1,y1] 组成一条直线
let g = render.createGradient(coords, colors);
let rc = new Rect(550, 200, 200, 50);
render.drawRect(rc, g);
render.drawCoords(rc);
coords = [650, 400, 30, 650, 400, 100]; ///放射性 6 个参数[x0,y0,r0,x1,y1,r1]组成内圆和外圆
g = render.createGradient(coords /*, colors*/ );
render.drawCircle(new Circle(650, 400, 100), g);
render.popStates();
进行裁剪前后的效果图:
2. 分析一下clip后的效果:
网格背景是在clip之前进行绘制的,因此没有被裁剪掉,这是正确的
红色rect也是在clip之前进行绘制的,因此没有被裁剪掉,这也是正确的
接下来的操作是进行裁剪以及裁剪后的绘制操作
绿色框起来的部分中的蓝色rect是裁剪区域,之后的所有绘制操作如果在蓝色区域中的部分,会显示。不在蓝色区域部分的形状,则不显示
红色圆部分及文字部分与蓝色rect相交部分显示出来,其他部分都裁剪掉了
其他线框图形和渐变色填充的图形因为没有和蓝色的rect相交,因此都被裁剪掉,不显示。这是非常正确的
但是你会发现图像绘制部分是在clip后进行绘制的,但却没被裁剪掉,出问题了!
3. 影响裁剪的因素:
渲染状态的save/restore严重影响裁剪范围:
仔细看一下drawRect的代码,你会发现,当isClip=true时候,并没有使用context.save/restore对,而进行绘制时候,使用了context.save/restore对。
这是因为: clip的有效范围在save() /restore()之间异步过程也严重影响裁剪范围:
上面也说过,在clip后,图像依旧显示出来。这是因为image的载入和绘制是异步过程。在图像绘制的时候,可能其他绘制都已经结束,渲染状态恢复到初始化无裁剪状态。然后图像才载入完成,进行显示。
解决方案:
- 在图像全部载入后在再进行全部的绘制操作。
- 精心的设计save/restore的影响范围
例如测试代码之所以出现图像问题,是因为:
//网格
render.drawGrid('white', 'black', 20, 20);
render.pushStates();
和
render.popStates();
这两段代码的原因.
如果将pushStates()/popStates()这两句代码注释掉,就显示正常了!
因为:
- 如果使用push/pop进行状态保存和恢复,那么如果图像载入完成的时间>pop时间点,则此时clip区域被恢复到原始的clip size[canvas.width,canvas.height],这样就没有任何裁剪效果了
- 如果没有使用push/pop,则依旧保留原来裁剪状态,即使图像异步载入时间再长,也不影响裁剪范围!
4. 裁剪实现的原理:
gl/dx等API在底层实现的过程定义了三个很重要的缓存区:
颜色缓冲区(rgba color buffer),也就是渲染表面,其大小为canvas.width * canvas.height * 4byte[rgba格式],你所有的draw操作,最后形成的图像就是保存在这个颜色缓冲区中
深度缓冲区(depth buffer)。这是记录每个像素点对应的深度值。3D专用。用于减少重绘。以后在webgl中,要重点了解并掌握该技术。强大无比的东西。
模板缓冲区(stencil buffer)。这个缓冲区在2d应用中,最大的作用就是进行路径的裁剪区域标记
原理图:(随便画的,见笑)
由此可见,做裁剪需要两次操作:
设置矢量图形数据,然后调用相应的命令(rect,arc等),然后进行clip操作,进行光栅化,但是不着色(颜色计算),而是进行像素标记,写入模板缓冲区
在进行正常绘制时,调用fill/stroke时,进行当前绘制形体与模板包围rect相交性测试,如果相交,然后再进行模板值比较,例如:stencils[row][colum].value == 1的话,说明是在裁剪区域,将当前像素pixels[row][colum].rgba的颜色数据写入颜色缓冲区。
在3D中,这个称为模板测试,是很重要一个功能。
原理很简单,应该很容易理解!
实际上,现在的浏览器的渲染部分基本都是使用硬件加速绘制的,canva2d是建立在opengl(es)/directX的基础上的。除非硬件不支持,才切换到软渲染。