Skia绘制代码分析

Posted Redrain

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Skia绘制代码分析相关的知识,希望对你有一定的参考价值。

原文出处:http://blog.csdn.net/hgl868/article/details/45584187

路径绘制尽管使用频率相对于图像绘制、文本绘制低,但却是非常重要的一个基本特性。所有不规则图形(椭圆、圆角矩形、三角形、简单的文字),最后都避不开路径绘制。

而且,若自己实现一个2D引擎,这块内容是很具有参考意义的,用OpenGL的话,图像采样等都很少关注了,对对坐标就好。但菱角、圆弧、曲线等如何绘制仍然是一个难题,这时就可以参考Skia中drawPath的实现。
由于涉及较多的图形学知识,本章就不讲相关公式了,只讲讲基本的流程。
一、SkPath类
在之前的图像绘制并没有介绍SkBitmap,因为SkBitmap相对而言比较容易理解,网上文章也多。但这次的SkPath不同,研究它怎么用是需要一点精力的,因此在这里先做介绍。
1、SkPath结构

去除成员函数之后,我们看到SkPath包括这几个成员,注释中补充了说明:

class SK_API SkPath
    //SkPath中的主要内容,SkAutoTUnref是自解引用,之所以这么设计,是为了复制SkPath时,省去份量较多的点复制(只复制引用)。
    //由一系列线段组成
    SkAutoTUnref<SkPathRef> fPathRef;
    int                 fLastMoveToIndex;
    uint8_t             fFillType;//如下四种类型之一
    /*enum FillType
        kWinding_FillType,//绘制所有线段包围成的区域
        kEvenOdd_FillType,//绘制被所有线段包围奇数次的区域)
        kInverseWinding_FillType,//kWinding_FillType取反,即绘制不在该区域的点
        kInverseEvenOdd_FillType//第二种type取反
        */
    mutable uint8_t     fConvexity;//凹凸性,临时计算
    mutable uint8_t     fDirection;//方向,顺时针/逆时针,临时计算
#ifdef SK_BUILD_FOR_android
    const SkPath*       fSourcePath;//Hwui中使用,暂不关注
#endif
;

关于 fFillType中 kWinding_FillType和 kEvenOdd_FillType的区别,可看SkPath::contains。这是判断点是否在不规则几何体内的经典代码(),很有参考意义。

SkPathRef的内容如下:

class SkPathRef

private:
    mutable SkRect      fBounds;//边界,临时计算
    uint8_t             fSegmentMask;//表示这个Path含有哪些种类的形状
    mutable uint8_t     fBoundsIsDirty;//缓存fBounds使用,表示 fBounds是否需要重新计算
    mutable SkBool8     fIsFinite;    // only meaningful if bounds are valid
    mutable SkBool8     fIsOval;
    /*skia不使用stl库而采用的一套容器方案,具体不细说,可看下 SkPath::Iter 的实现*/
    SkPoint*            fPoints; // points to begining of the allocation
    uint8_t*            fVerbs; // points just past the end of the allocation (verbs grow backwards)
    int                 fVerbCnt;
    int                 fPointCnt;
    size_t              fFreeSpace; // redundant but saves computation
    SkTDArray<SkScalar> fConicWeights;
    mutable uint32_t    fGenerationID;
;

2、SkPath的主要类型:

kMove_Verb:表示需要移动起点
kLine_Verb:直线
kQuad_Verb:二次曲线
kConic_Verb:圆锥曲线
kCubic_Verb:三次曲线
kClose_Verb:表闭合到某点
kDone_Verb:表结束

#include "SkPath.h"
#include "SkCanvas.h"
#include "SkBitmap.h"

int main()

    SkBitmap dst;
    dst.allocN32Pixels(1000, 1000);
    SkCanvas c(dst);
    SkPath path;
    /*一个三角形*/
    path.moveTo(300,0);
    path.lineTo(400,100);
    path.lineTo(200,100);
    path.close();
    /*椭圆*/
    SkRect oval;
    oval.set(0, 0, 500, 600);
    path.addOval(oval);
    c.drawPath(path);
    return 1;

二、drawPath流程


填充算法说明
我们跟进最重要的函数 sk_fill_path,如下为代码:

void sk_fill_path(const SkPath& path, const SkIRect* clipRect, SkBlitter* blitter,
                  int start_y, int stop_y, int shiftEdgesUp,
                  const SkRegion& clipRgn)
    SkASSERT(&path && blitter);
    SkEdgeBuilder   builder;
    int count = builder.build(path, clipRect, shiftEdgesUp);
    SkEdge**    list = builder.edgeList();
    if (count < 2)
        if (path.isInverseFillType())
            /*
             *  Since we are in inverse-fill, our caller has already drawn above
             *  our top (start_y) and will draw below our bottom (stop_y). Thus
             *  we need to restrict our drawing to the intersection of the clip
             *  and those two limits.
             */
            SkIRect rect = clipRgn.getBounds();
            if (rect.fTop < start_y)
                rect.fTop = start_y;
           
            if (rect.fBottom > stop_y)
                rect.fBottom = stop_y;
           
            if (!rect.isEmpty())
                blitter->blitRect(rect.fLeft << shiftEdgesUp,
                                  rect.fTop << shiftEdgesUp,
                                  rect.width() << shiftEdgesUp,
                                  rect.height() << shiftEdgesUp);
           
       
        return;
   
    SkEdge headEdge, tailEdge, *last;
    // this returns the first and last edge after they're sorted into a dlink list
    SkEdge* edge = sort_edges(list, count, &last);
    headEdge.fPrev = NULL;
    headEdge.fNext = edge;
    headEdge.fFirstY = kEDGE_HEAD_Y;
    headEdge.fX = SK_MinS32;
    edge->fPrev = &headEdge;
    tailEdge.fPrev = last;
    tailEdge.fNext = NULL;
    tailEdge.fFirstY = kEDGE_TAIL_Y;
    last->fNext = &tailEdge;
    // now edge is the head of the sorted linklist
    start_y <<= shiftEdgesUp;
    stop_y <<= shiftEdgesUp;
    if (clipRect && start_y < clipRect->fTop)
        start_y = clipRect->fTop;
   
    if (clipRect && stop_y > clipRect->fBottom)
        stop_y = clipRect->fBottom;
   
    InverseBlitter  ib;
    PrePostProc     proc = NULL;
    if (path.isInverseFillType())
        ib.setBlitter(blitter, clipRgn.getBounds(), shiftEdgesUp);
        blitter = &ib;
        proc = PrePostInverseBlitterProc;
   
    if (path.isConvex() && (NULL == proc))
        walk_convex_edges(&headEdge, path.getFillType(), blitter, start_y, stop_y, NULL);
    else
        walk_edges(&headEdge, path.getFillType(), blitter, start_y, stop_y, proc);
   

不考虑 Inverse 的情况,主要就是两步:
(1)生成一系列边:SkEdge
(2)遍历渲染各边所围出来的区域

凸集的渲染比较简单,因为可以保证,任意两条边+闭合线所围成区域一定需要渲染:
(1)取初始的两条边,分别为:左和右。
(2)渲染左右边+闭合边所围成的区域(一般为三角,当两边平行时取矩形)

(3)迭代刷新左右两边(如果是曲线需要刷新多次)

static void walk_convex_edges(SkEdge* prevHead, SkPath::FillType,
                              SkBlitter* blitter, int start_y, int stop_y,
                              PrePostProc proc)
    validate_sort(prevHead->fNext);


    SkEdge* leftE = prevHead->fNext;
    SkEdge* riteE = leftE->fNext;
    SkEdge* currE = riteE->fNext;
#if 0
    int local_top = leftE->fFirstY;
    SkASSERT(local_top == riteE->fFirstY);
#else
    // our edge choppers for curves can result in the initial edges
    // not lining up, so we take the max.
    int local_top = SkMax32(leftE->fFirstY, riteE->fFirstY);
#endif
    SkASSERT(local_top >= start_y);
    for (;;)
        SkASSERT(leftE->fFirstY <= stop_y);
        SkASSERT(riteE->fFirstY <= stop_y);
        if (leftE->fX > riteE->fX || (leftE->fX == riteE->fX &&
                                      leftE->fDX > riteE->fDX))
            SkTSwap(leftE, riteE);
       
        int local_bot = SkMin32(leftE->fLastY, riteE->fLastY);
        local_bot = SkMin32(local_bot, stop_y - 1);
        SkASSERT(local_top <= local_bot);
        SkFixed left = leftE->fX;
        SkFixed dLeft = leftE->fDX;
        SkFixed rite = riteE->fX;
        SkFixed dRite = riteE->fDX;
        int count = local_bot - local_top;
        SkASSERT(count >= 0);
        if (0 == (dLeft | dRite))
            int L = SkFixedRoundToInt(left);
            int R = SkFixedRoundToInt(rite);
            if (L < R)
                count += 1;
                blitter->blitRect(L, local_top, R - L, count);
                left += count * dLeft;
                rite += count * dRite;
           
            local_top = local_bot + 1;
        else
            do
                int L = SkFixedRoundToInt(left);
                int R = SkFixedRoundToInt(rite);
                if (L < R)
                    blitter->blitH(L, local_top, R - L);
               
                left += dLeft;
                rite += dRite;
                local_top += 1;
            while (--count >= 0);
       
        leftE->fX = left;
        riteE->fX = rite;
        if (update_edge(leftE, local_bot))
            if (currE->fFirstY >= stop_y)
                break;
           
            leftE = currE;
            currE = currE->fNext;
       
        if (update_edge(riteE, local_bot))
            if (currE->fFirstY >= stop_y)
                break;
           
            riteE = currE;
            currE = currE->fNext;
       
        SkASSERT(leftE);
        SkASSERT(riteE);
        // check our bottom clip
        SkASSERT(local_top == local_bot + 1);
        if (local_top >= stop_y)
            break;
       
   

凹集或者判断不了凹凸性就比较复杂,需要一条线一条线去渲染,每次渲染还得判断奇偶性:

代码如下,不分析了:

static void walk_edges(SkEdge* prevHead, SkPath::FillType fillType,
                       SkBlitter* blitter, int start_y, int stop_y,
                       PrePostProc proc)
    validate_sort(prevHead->fNext);


    int curr_y = start_y;
    // returns 1 for evenodd, -1 for winding, regardless of inverse-ness
    int windingMask = (fillType & 1) ? 1 : -1;


    for (;;)
        int     w = 0;
        int     left SK_INIT_TO_AVOID_WARNING;
        bool    in_interval = false;
        SkEdge* currE = prevHead->fNext;
        SkFixed prevX = prevHead->fX;


        validate_edges_for_y(currE, curr_y);


        if (proc)
            proc(blitter, curr_y, PREPOST_START);    // pre-proc
       


        while (currE->fFirstY <= curr_y)
            SkASSERT(currE->fLastY >= curr_y);


            int x = SkFixedRoundToInt(currE->fX);
            w += currE->fWinding;
            if ((w & windingMask) == 0) // we finished an interval
                SkASSERT(in_interval);
                int width = x - left;
                SkASSERT(width >= 0);
                if (width)
                    blitter->blitH(left, curr_y, width);
                in_interval = false;
            else if (!in_interval)
                left = x;
                in_interval = true;
           


            SkEdge* next = currE->fNext;
            SkFixed newX;


            if (currE->fLastY == curr_y)    // are we done with this edge?
                if (currE->fCurveCount < 0)
                    if (((SkCubicEdge*)currE)->updateCubic())
                        SkASSERT(currE->fFirstY == curr_y + 1);


                        newX = currE->fX;
                        goto NEXT_X;
                   
                else if (currE->fCurveCount > 0)
                    if (((SkQuadraticEdge*)currE)->updateQuadratic())
                        newX = currE->fX;
                        goto NEXT_X;
                   
               
                remove_edge(currE);
            else
                SkASSERT(currE->fLastY > curr_y);
                newX = currE->fX + currE->fDX;
                currE->fX = newX;
            NEXT_X:
                if (newX < prevX) // ripple currE backwards until it is x-sorted
                    backward_insert_edge_based_on_x(currE  SkPARAM(curr_y));
                else
                    prevX = newX;
               
           
            currE = next;
            SkASSERT(currE);
       


        if (proc)
            proc(blitter, curr_y, PREPOST_END);    // post-proc
       


        curr_y += 1;
        if (curr_y >= stop_y)
            break;
       
        // now currE points to the first edge with a Yint larger than curr_y
        insert_new_edges(currE, curr_y);
   


drawPath是绘制所有不规则形体的函数,带入Bitmap的Shader,可以制作不规则形体的图片。对于凸集,Skia的渲染主要也是切成三角片后渲染,和OpenGL类似。而对于凹集,则是扫描线了。渲染的实现和绘制图片一样,构建Blitter,调用Blitter的blit函数族渲染。


文字绘制

文字绘制主要包括编码转换(主要是中文)、字形解析(点线或image)和实际渲染三个步骤。在这个过程中,字形解析和实际渲染均是耗时步骤。Skia对文字解析的结果做了一套缓存机制。在中文字较多,使用多种字体,绘制的样式(粗/斜体)有变化时,这个缓存会变得很大,因此Skia文字缓存做了内存上的限制。

1、SkPaint

文字绘制与SkPaint的属性相关很大,先回头看下SkPaint相关的属性

class SkPaint

private
    SkTypeface*     fTypeface;//字体
    SkPathEffect*   fPathEffect;//路径绘制效果
    SkShader*       fShader;//取色器
    SkXfermode*     fXfermode;//混合模式,类似OpenGL里面的Blend设置
    SkColorFilter*  fColorFilter;//图像绘制时,自定义图像采样函数时使用
    SkMaskFilter*   fMaskFilter;//路径绘制时,按有无像素做进一步自定义改进处理时使用
    SkRasterizer*   fRasterizer;//路径绘制时自定义生成像素点的算法时使用
    SkDrawLooper*   fLooper;//循环绘制,SkCanvas里面的第二重循环,一般不用关注
    SkImageFilter*  fImageFilter;//SkCanvas的第一重循环,绘制后做后处理用,一般不用关注
    SkAnnotation*   fAnnotation;//暂时没用到的属性


    SkScalar        fTextSize;//文字大小


    SkScalar        fTextScaleX;//文字水平方向上的拉伸,仅用于PDF绘制
    SkScalar        fTextSkewX;//文字横向扭曲度,仅用于PDF绘制


    SkColor         fColor;//纯色,在fShader为空时使用
    SkScalar        fWidth;//带边界时(kStroke_Style/kStrokeAndFill_Style)生效,边界的宽度 
    SkScalar        fMiterLimit;//drawPath时,连接各个path片断时,要求的圆滑连接阈值,Join 类型为默认的kMiter_Join时无效
    /*一组不超过32位的属性*/
    union
        struct
            // all of these bitfields should add up to 32
            unsigned        fFlags : 16;//包含所有的0/1二值属性:
            /*
               kAntiAlias_Flag       = 0x01,//是否抗锯齿
               kDither_Flag          = 0x04,//是否做抖动处理
               kUnderlineText_Flag   = 0x08,//是否绘制文字下划线
               kStrikeThruText_Flag  = 0x10,//目前未看到其作用
               kFakeBoldText_Flag    = 0x20,
               kLinearText_Flag      = 0x40,
               kSubpixelText_Flag    = 0x80,//文字像素精确采样
               kDevKernText_Flag     = 0x100
               kLCDRenderText_Flag   = 0x200
               kEmbeddedBitmapText_Flag = 0x400,
               kAutoHinting_Flag     = 0x800,
               kVerticalText_Flag    = 0x1000,//是否竖向绘制文字
               kGenA8FromLCD_Flag    = 0x2000,
               kDistanceFieldTextTEMP_Flag = 0x4000,
               kAllFlags = 0xFFFF
             */


            unsigned        fTextAlign : 2;//文字对齐方式,取值如下:
            /*
            enum Align
                kLeft_Align,//左对齐
                kCenter_Align,//居中
                kRight_Align,//右对齐
            ;
            */


            unsigned        fCapType : 2;//边界连接类型,分无连接,圆角连接,半方形连接
            unsigned        fJoinType : 2;//Path片断连接类型
            
            unsigned        fStyle : 2;//绘制模式,填充边界/区域
            /*
               enum Style
               kFill_Style, //填充区域
               kStroke_Style,//绘制边界
               kStrokeAndFill_Style,//填充区域并绘制边界
               ;
             */


            unsigned        fTextEncoding : 2;//文字编码格式,支持如下几种
            enum TextEncoding
                kUTF8_TextEncoding,//utf-8,默认格式
                kUTF16_TextEncoding,
                kUTF32_TextEncoding,
                kGlyphID_TextEncoding
            ;


            unsigned        fHinting : 2;
            unsigned        fFilterLevel : 2;//在图像绘制时提到的采样质量要求
            //unsigned      fFreeBits : 2;
        ;
        uint32_t fBitfields;
    ;
    uint32_t fDirtyBits;//记录哪些属性被改变了,以便更新相关的缓存
;


SkCanvas
绘制文字和下划线

SkDraw

两种绘制方式:

(1)将文字解析为路径,然后绘制路径,缓存路径(drawText_asPaths)。

void SkDraw::drawText_asPat

以上是关于Skia绘制代码分析的主要内容,如果未能解决你的问题,请参考以下文章

Pango 颜色字形

用C#代码写,ArcEngine开发,在加载的地图上绘制多边形后,导出绘制区域内的所有矢量要素(点线面)

如何在 Gtk3 中在开罗上下文中使用 Skia 绘制

Cesium实战项目1.点线面实体绘制

三维空间中绘制点线面UV贴图,万能的BufferGeometry(three.js实战4)

cesium 学习 基础绘制(点线面)