闲聊js11: 创建一个演示用的渲染库9(关键的裁剪操作)

本篇的目的是要了解:

  1. 裁剪操作
  2. 裁剪效果分析
  3. 渲染状态以及异步操作对裁剪的影响
  4. 裁剪实现的原理。实际上渲染的流程也是如此。只是渲染的目标缓冲区不一样

今天的这一章很重要,如果想让你的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();

进行裁剪前后的效果图:

闲聊js11: 创建一个演示用的渲染库9(关键的裁剪操作)_第1张图片
clip前后对比图.jpg

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的载入和绘制是异步过程。在图像绘制的时候,可能其他绘制都已经结束,渲染状态恢复到初始化无裁剪状态。然后图像才载入完成,进行显示。

解决方案:

  1. 在图像全部载入后在再进行全部的绘制操作。
  2. 精心的设计save/restore的影响范围

例如测试代码之所以出现图像问题,是因为:

        //网格
        render.drawGrid('white', 'black', 20, 20);

         render.pushStates();

         render.popStates();

这两段代码的原因.

如果将pushStates()/popStates()这两句代码注释掉,就显示正常了!

闲聊js11: 创建一个演示用的渲染库9(关键的裁剪操作)_第2张图片
clip正常效果.png

因为:

  • 如果使用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应用中,最大的作用就是进行路径的裁剪区域标记

原理图:(随便画的,见笑)

闲聊js11: 创建一个演示用的渲染库9(关键的裁剪操作)_第3张图片
缓冲区.png

由此可见,做裁剪需要两次操作:

设置矢量图形数据,然后调用相应的命令(rect,arc等),然后进行clip操作,进行光栅化,但是不着色(颜色计算),而是进行像素标记,写入模板缓冲区

在进行正常绘制时,调用fill/stroke时,进行当前绘制形体与模板包围rect相交性测试,如果相交,然后再进行模板值比较,例如:stencils[row][colum].value == 1的话,说明是在裁剪区域,将当前像素pixels[row][colum].rgba的颜色数据写入颜色缓冲区。
在3D中,这个称为模板测试,是很重要一个功能。

原理很简单,应该很容易理解!

实际上,现在的浏览器的渲染部分基本都是使用硬件加速绘制的,canva2d是建立在opengl(es)/directX的基础上的。除非硬件不支持,才切换到软渲染。

你可能感兴趣的:(闲聊js11: 创建一个演示用的渲染库9(关键的裁剪操作))