Cocos2d-x pgeRippleSprite OpenGL ES2.0 Shader版本

来源:互联网 发布:最伤感流行网络歌曲 编辑:程序博客网 时间:2024/06/05 05:30

前几年移植过一篇水波纹的cocos2d-x实现, 但是是使用OpenGL ES2.0以前的版本实现的,计算纹理坐标是使用CPU计算的,当水波纹较多时fps较低。

而且由于当时不会openGL,所以一直没有改。最近将OpenGL比较系统的学习了一遍,现在移植了一个Shader版本,能够显著提高效率。


1. pgeRippleSiprite.h  

//

//  pgeRippleSprite.h

//  test2dx

//

//  Created by limeng on 14-1-16.

//

//


#ifndef __test2dx__pgeRippleSprite__

#define __test2dx__pgeRippleSprite__


#include <iostream>

#include <list>

//

//  pgeRippleSprite.h

//  rippleDemo

//

//  Created by Lars Birkemose on 02/12/11.

//  Copyright 2011 Protec Electronics. All rights reserved.

//

// --------------------------------------------------------------------------

// import headers



// porting to cplusplus by wanghong.li (wanghong.li1029@163.com)on 04/01/12

// All rights reserved



#include "cocos2d.h"

#include <list>

#include "ccTypes.h"


USING_NS_CC;





// --------------------------------------------------------------------------

// defines



#define RIPPLE_DEFAULT_QUAD_COUNT_X             100

#define RIPPLE_DEFAULT_QUAD_COUNT_Y          60


#define RIPPLE_BASE_GAIN                        0.1f       // an internal constant



#define RIPPLE_DEFAULT_RADIUS                   400      // radius in pixels //半径

#define RIPPLE_DEFAULT_RIPPLE_CYCLE             0.4f      // timing on ripple ( 1/frequenzy ) //帧频

#define RIPPLE_DEFAULT_LIFESPAN                 4.0f       // entire ripple lifespan //时长



#define RIPPLE_CHILD_MODIFIER                   2.0f



// --------------------------------------------------------------------------

// typedefs



typedef enum {

    RIPPLE_TYPE_RUBBER,                                    // a soft rubber sheet

    RIPPLE_TYPE_GEL,                                       // high viscosity fluid

    RIPPLE_TYPE_WATER,                                     // low viscosity fluid

} RIPPLE_TYPE;



typedef enum {

    RIPPLE_CHILD_LEFT,

    RIPPLE_CHILD_TOP,

    RIPPLE_CHILD_RIGHT,

    RIPPLE_CHILD_BOTTOM,

    RIPPLE_CHILD_COUNT

} RIPPLE_CHILD;



typedefstruct _rippleData {

   bool                    parent;                        // ripple is a parent

   bool                    childCreated[4 ];              // child created ( in the 4 direction )

   RIPPLE_TYPE             rippleType;                    // type of ripple ( se update: )

    cocos2d::CCPoint        center;                        // ripple center ( but you just knew that, didn't you? )

   cocos2d::CCPoint        centerCoordinate;              // ripple center in texture coordinates

   float                   radius;                        // radius at which ripple has faded 100%

   float                   strength;                      // ripple strength

   float                   runtime;                       // current run time

   float                   currentRadius;                 // current radius

   float                   rippleCycle;                   // ripple cycle timing

   float                   lifespan;                      // total life span

} rippleData;



// --------------------------------------------------------------------------

// pgeRippleSprite





typedefstd::list<rippleData*>::iterator         RIPPLE_DATA_LIST;

typedefstd::list<rippleData*>::reverse_iterator REVERSE_RIPPLE_DATA_LIST;



class CCpgeRippleSprite :publiccocos2d::CCNode

{

private:

    ccV3F_C4B_T2F_Quad m_sQuad;

    

   void setTextureRect(constCCRect& rect,bool rotated,constCCSize& untrimmedSize);

   void setTextureCoords(CCRect rect);

   void setMyTexture(CCTexture2D *texture);

   void setBatchNode(CCSpriteBatchNode *pobSpriteBatchNode);

   CCPoint m_obOffsetPosition;

public:

    CCpgeRippleSprite();

    ~CCpgeRippleSprite();

    

    

    CC_SYNTHESIZE(cocos2d::CCTexture2D*,m_texture, Texture)

   CC_SYNTHESIZE(int,m_quadCountX, QuadCountX)

   CC_SYNTHESIZE(int,m_quadCountY, QuadCountY)

   CC_SYNTHESIZE(int,m_VerticesPrStrip, VerticesPrStrip)

   CC_SYNTHESIZE(int,m_bufferSize, BuffSize)

    CC_SYNTHESIZE(cocos2d::CCPoint*,m_vertice, Vertice)

    CC_SYNTHESIZE(cocos2d::CCPoint*,m_textureCoordinate, TextureCoordinate)

   CC_SYNTHESIZE_READONLY(float*,m_edgeVertice, EdgeVertice)

    CC_SYNTHESIZE_READONLY_PASS_BY_REF(std::list<rippleData*>,m_rippleList, RippleList)

    

    

public:

   staticCCpgeRippleSprite* rippleSpriteWithFile(constchar* filename);

    

    

   bool initWithFile(constchar* filename);

   virtualvoid draw();

   void  update(float dt);

   void  addRipple(cocos2d::CCPoint &pos,RIPPLE_TYPE type, float strength);

    

    

protected:

   bool  initShader();

   void  tesselate();

   void  addRippleChild(rippleData* parent,RIPPLE_CHILD type);

    

protected:

   int    m_texture_max_idx;

   int    m_ripple_num_idx;

   bool   ripple_dirty_;

};





#endif /* defined(__test2dx__pgeRippleSprite__) */


pgeRippleSprite.cpp 

//

//  pgeRippleSprite.cpp

//  test2dx

//

//  Created by limeng on 14-1-16.

//

//


#include "pgeRippleSprite.h"

#include "CCGL.h"


using namespace cocos2d;


#define RippleEdgeAttr    4



CCpgeRippleSprite*CCpgeRippleSprite::rippleSpriteWithFile(constchar* filename)

{

   CCpgeRippleSprite* pgeRippleSprite =new CCpgeRippleSprite();

   if(pgeRippleSprite && pgeRippleSprite->initWithFile(filename))

    {

        pgeRippleSprite->autorelease();

       return pgeRippleSprite;

    }

    

    

    CC_SAFE_DELETE(pgeRippleSprite);

    return NULL;

}



CCpgeRippleSprite::CCpgeRippleSprite()

:m_texture(NULL),

m_vertice(NULL),

m_textureCoordinate(NULL),

m_edgeVertice(NULL),

m_texture_max_idx(0),

m_ripple_num_idx(0),

ripple_dirty_(false)

{

    

    

}



CCpgeRippleSprite::~CCpgeRippleSprite()

{

    CC_SAFE_RELEASE(m_texture);

    CC_SAFE_DELETE_ARRAY(m_vertice);

    CC_SAFE_DELETE_ARRAY(m_textureCoordinate);

    CC_SAFE_DELETE_ARRAY(m_edgeVertice);

    

    

    RIPPLE_DATA_LIST iterBegin =m_rippleList.begin();

    

    

   while (iterBegin !=m_rippleList.end())

    {

       rippleData* date = *iterBegin;

        

        

        CC_SAFE_DELETE(date);

        

        

        

        iterBegin++;

    }

    m_rippleList.clear();

}



boolCCpgeRippleSprite::initWithFile(constchar* filename)

{

#if 0

    CCGLProgram * glShaderProgram = CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTexture);

   this->setShaderProgram(glShaderProgram);

#endif 

    

   if (!initShader())

    {

        return false;

    }

    

    m_texture =CCTextureCache::sharedTextureCache()->addImage(filename);

    if (!m_texture)

    {

        return false;

    }

   m_texture->retain();

    

    m_vertice = NULL;

    m_textureCoordinate =NULL;

    CC_SAFE_DELETE_ARRAY(m_vertice);

    CC_SAFE_DELETE_ARRAY(m_textureCoordinate);

    CC_SAFE_DELETE_ARRAY(m_edgeVertice);

   m_quadCountX = RIPPLE_DEFAULT_QUAD_COUNT_X;

   m_quadCountY = RIPPLE_DEFAULT_QUAD_COUNT_Y;

    

    tesselate();

    

    schedule(schedule_selector(CCpgeRippleSprite::update));

    m_pShaderProgram->use();

    

   GLfloat texture_max[] = {m_texture->getMaxS(),m_texture->getMaxT()};

    m_pShaderProgram->setUniformLocationWith2fv(m_texture_max_idx, texture_max,1);

    CHECK_GL_ERROR_DEBUG();

    

    return true;

}


bool CCpgeRippleSprite::initShader()

{

   CCGLProgram* glProgram =new CCGLProgram();

    if (!glProgram->initWithVertexShaderFilename("shaders/ripple.vsh","shaders/ripple.fsh"))

    {

        return false;

    }

    

    glProgram->addAttribute(kCCAttributeNamePosition,kCCVertexAttrib_Position);

    glProgram->addAttribute(kCCAttributeNameTexCoord,kCCVertexAttrib_TexCoords);

    glProgram->addAttribute("a_edge",RippleEdgeAttr);

    

    glProgram->link();

    glProgram->updateUniforms();

    

    m_texture_max_idx = glProgram->getUniformLocationForName("texture_max");

    m_ripple_num_idx = glProgram->getUniformLocationForName("ripple_num");

    

    CHECK_GL_ERROR_DEBUG();

    

   setShaderProgram(glProgram);

    

    return true;

}


void CCpgeRippleSprite::draw()

{

    CC_NODE_DRAW_SETUP();

    

    

   if (m_texture !=NULL)

    {

        ccGLBindTexture2D(m_texture->getName() );

        ccGLEnableVertexAttribs (kCCVertexAttribFlag_Position |kCCVertexAttribFlag_TexCoords  );

        glEnableVertexAttribArray(RippleEdgeAttr);

    }

    

    // vertex

    glVertexAttribPointer(kCCVertexAttrib_Position,2,GL_FLOAT,GL_FALSE,0, m_vertice);

    

   if (m_texture !=NULL)

    {

        // texCoods

        //glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, ( m_rippleList.size() == 0 ) ? m_textureCoordinate : m_rippleCoordinate );

        glVertexAttribPointer(kCCVertexAttrib_TexCoords,2,GL_FLOAT,GL_FALSE,0, m_textureCoordinate);

        glVertexAttribPointer(RippleEdgeAttr,1,GL_FLOAT,GL_FALSE,0,m_edgeVertice);

    }

    

    // color

//    glVertexAttribPointer(kCCVertexAttrib_Color, 2, GL_FLOAT, GL_FALSE, 0, m_vertice);

    

   for (int strip =0; strip <m_quadCountY; strip ++ ) {

        glDrawArrays(GL_TRIANGLE_STRIP, strip *m_VerticesPrStrip,m_VerticesPrStrip );

    }

    

//    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    

    CHECK_GL_ERROR_DEBUG();

    

    CC_INCREMENT_GL_DRAWS(1);

    

   CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite,"CCSprite - draw");

}



void CCpgeRippleSprite::tesselate()

{

   int vertexPos =0;

   CCPoint normalized;

    

    

    CC_SAFE_DELETE_ARRAY(m_vertice);

    CC_SAFE_DELETE_ARRAY(m_textureCoordinate);

    CC_SAFE_DELETE_ARRAY(m_edgeVertice);

    

    

    m_VerticesPrStrip =2 * (m_quadCountX +1);

    m_bufferSize =m_VerticesPrStrip *m_quadCountY;

    

    

    //allocate buffers

    

    

    m_vertice = new CCPoint[m_bufferSize];

    m_textureCoordinate =newCCPoint[m_bufferSize];

    m_edgeVertice =newfloat[m_bufferSize];

    

    memset(m_edgeVertice,0,sizeof(int) *m_bufferSize);

    

    vertexPos =0;

    

   CCLOG("循环开始");

   for (int y =0; y <m_quadCountY; y++)

    {

       for (int x =0; x < (m_quadCountX +1); x++)

        {

           for (int yy =0; yy <2; yy ++ ) {

                

                

                // first simply calculate a normalized position into rectangle

                normalized.x = (float )x / (float )m_quadCountX;

                normalized.y = (float )( y + yy ) / (float )m_quadCountY;

                

                

                // calculate vertex by multiplying rectangle ( texture ) size

               CCSize contentSize =m_texture->getContentSize();

               m_vertice[ vertexPos ] =ccp( normalized.x * contentSize.width, normalized.y * contentSize.height);

                

                

                // adjust texture coordinates according to texture size

                // as a texture is always in the power of 2, maxS and maxT are the fragment of the size actually used

                // invert y on texture coordinates

               m_textureCoordinate[ vertexPos ] =ccp( normalized.x * m_texture->getMaxS(), m_texture->getMaxT()- ( normalized.y *m_texture->getMaxT() ) );

                

                

                // check if vertice is an edge vertice, because edge vertices are never modified to keep outline consistent

               if (( x ==0 ) ||

                    ( x ==m_quadCountX ) ||

                    ( ( y ==0 ) && ( yy ==0 ) ) ||

                    ( ( y == (m_quadCountY -1 ) ) && ( yy >0 ) ))

                {

                   m_edgeVertice[vertexPos] =1.0f;

                }

                

                // next buffer pos

                vertexPos ++;

            }

        }

    }

   CCLOG("循环结束");

}



void CCpgeRippleSprite::addRipple(cocos2d::CCPoint &pos,RIPPLE_TYPE type, float strength)

{

   rippleData* newRipple;

    

    

    // allocate new ripple

    newRipple =newrippleData;

    

    

    // initialize ripple

    newRipple->parent =true;

   for (int count =0; count <4; count ++ ) newRipple->childCreated[ count ] =false;

    newRipple->rippleType = type;

    newRipple->center = pos;

    

    

   CCSize contentSize =m_texture->getContentSize();

    newRipple->centerCoordinate =ccp( pos.x / contentSize.width *m_texture->getMaxS(),m_texture->getMaxT() - ( pos.y / contentSize.height *m_texture->getMaxT()) );

    newRipple->radius =RIPPLE_DEFAULT_RADIUS;// * strength;

    newRipple->strength = strength;

    newRipple->runtime =0;

    newRipple->currentRadius =0;

    newRipple->rippleCycle =RIPPLE_DEFAULT_RIPPLE_CYCLE;

    newRipple->lifespan =RIPPLE_DEFAULT_LIFESPAN;

    

    

    // add ripple to running list

   m_rippleList.push_back(newRipple);

    ripple_dirty_ =true;

}



void CCpgeRippleSprite::addRippleChild(rippleData* parent,RIPPLE_CHILD type)

{

   rippleData* newRipple;

   CCPoint pos;

    

    

    // allocate new ripple

    newRipple =newrippleData;

    

    

    // new ripple is pretty much a copy of its parent

   memcpy( newRipple, parent,sizeof( rippleData ) );

    

    

    // not a parent

    newRipple->parent =false;

    

    

   CCSize winSize =CCDirector::sharedDirector()->getWinSize();

    // mirror position

   switch ( type ) {

        caseRIPPLE_CHILD_LEFT:

            pos =ccp( -parent->center.x, parent->center.y );

           break;

        caseRIPPLE_CHILD_TOP:

            pos =ccp( parent->center.x, winSize.height + ( winSize.height - parent->center.y ) );

           break;

        caseRIPPLE_CHILD_RIGHT:

            pos =ccp( winSize.width + ( winSize.width - parent->center.x ), parent->center.y );

           break;

        caseRIPPLE_CHILD_BOTTOM:

       default:

            pos =ccp( parent->center.x, -parent->center.y );

           break;

    }

    

    

    newRipple->center = pos;

    

    

   CCSize contentSize =m_texture->getContentSize();

    

    

    newRipple->centerCoordinate =ccp( pos.x / contentSize.width *m_texture->getMaxS(),m_texture->getMaxT()- ( pos.y / contentSize.height *m_texture->getMaxT()) );

    newRipple->strength *=RIPPLE_CHILD_MODIFIER;

    

    

    // indicate child used

    parent->childCreated[ type ] =true;

    

    

    // add ripple to running list

   m_rippleList.push_back(newRipple);

    ripple_dirty_ =true;

}



void CCpgeRippleSprite::update(float dt)

{

   rippleData* ripple =NULL;

    

    // test if any ripples at all

   if (m_rippleList.size() ==0 )return

    

    // scan through running ripples

    // the scan is backwards, so that ripples can be removed on the fly

    

    

   CCSize winSize =CCDirector::sharedDirector()->getWinSize();

    

    

    REVERSE_RIPPLE_DATA_LIST iterRipple =m_rippleList.rbegin();

    

    

   while (  iterRipple !=m_rippleList.rend())

    {

        // get ripple data

        ripple = *iterRipple;

        

        // calculate radius

        ripple->currentRadius = ripple->radius * ripple->runtime / ripple->lifespan;

        

        

        // check if ripple should expire

        ripple->runtime += dt;

       if ( ripple->runtime >= ripple->lifespan )

        {

            // free memory, and remove from list

           CC_SAFE_DELETE( ripple );

            

            

           RIPPLE_DATA_LIST it = --iterRipple.base() ;

           RIPPLE_DATA_LIST it_after_del =m_rippleList.erase(it);

            iterRipple =std::list<rippleData*>::reverse_iterator(it_after_del);

           ripple_dirty_ =true;

        }

       else

        {

            // check for creation of child ripples

           if ( ripple->parent ==true ) {

                

               // left ripple

               if ( ( ripple->childCreated[RIPPLE_CHILD_LEFT ] ==false ) && ( ripple->currentRadius > ripple->center.x ) ) {

                   addRippleChild(ripple,RIPPLE_CHILD_LEFT);

                }

                

                

               // top ripple

               if ( ( ripple->childCreated[RIPPLE_CHILD_TOP ] ==false ) && ( ripple->currentRadius > winSize.height - ripple->center.y ) ) {

                   addRippleChild(ripple,RIPPLE_CHILD_TOP);

                }

                

                

               // right ripple

               if ( ( ripple->childCreated[RIPPLE_CHILD_RIGHT ] ==false ) && ( ripple->currentRadius > winSize.width - ripple->center.x ) ) {

                    addRippleChild(ripple,RIPPLE_CHILD_RIGHT);

                }

                

                

               // bottom ripple

               if ( ( ripple->childCreated[RIPPLE_CHILD_BOTTOM ] ==false ) && ( ripple->currentRadius > ripple->center.y ) ) {

                    addRippleChild(ripple,RIPPLE_CHILD_BOTTOM);

                }

            }

            iterRipple++;

        }

    }

    

    iterRipple =m_rippleList.rbegin();

    

   int ripple_index =0;

    m_pShaderProgram->use();

    m_pShaderProgram->setUniformLocationWith1i(m_ripple_num_idx, (int)(m_rippleList.size()));

    CHECK_GL_ERROR_DEBUG();

    

   while (  iterRipple !=m_rippleList.rend())

    {

        // get ripple data

        ripple = *iterRipple;

        

       char ripple_attr_name[128] = {0};

       sprintf(ripple_attr_name,"ripples[%d].center", ripple_index);

        

       GLint ripple_center =m_pShaderProgram->getUniformLocationForName(ripple_attr_name);

       m_pShaderProgram->setUniformLocationWith2fv(ripple_center, (GLfloat*)(&ripple->center),1);

        CHECK_GL_ERROR_DEBUG();

        

       sprintf(ripple_attr_name,"ripples[%d].coor_center", ripple_index);

       GLint ripple_coor_center =m_pShaderProgram->getUniformLocationForName(ripple_attr_name);

       m_pShaderProgram->setUniformLocationWith2fv(ripple_coor_center, (GLfloat*)(&ripple->centerCoordinate),1);

        CHECK_GL_ERROR_DEBUG();

        

       sprintf(ripple_attr_name,"ripples[%d].ripple_type", ripple_index);

       GLint rippe_type =m_pShaderProgram->getUniformLocationForName(ripple_attr_name);

       m_pShaderProgram->setUniformLocationWith1i(rippe_type, (GLint)ripple->rippleType);

        CHECK_GL_ERROR_DEBUG();

        

       sprintf(ripple_attr_name,"ripples[%d].run_time", ripple_index);

       GLint ripple_run_time =m_pShaderProgram->getUniformLocationForName(ripple_attr_name);

       m_pShaderProgram->setUniformLocationWith1f(ripple_run_time, (GLfloat)ripple->runtime);

        CHECK_GL_ERROR_DEBUG();

        

       sprintf(ripple_attr_name,"ripples[%d].current_radius", ripple_index);

       GLint ripple_radius =m_pShaderProgram->getUniformLocationForName(ripple_attr_name);

       m_pShaderProgram->setUniformLocationWith1f(ripple_radius, (GLfloat)ripple->currentRadius);

        CHECK_GL_ERROR_DEBUG();

        

        iterRipple++;

        ripple_index++;

    }

}


Shader文件我放在Resources/shaders文件夹下,代码中hard code指定的。所以如果要正确运行必须也放在这个文件夹下。

顶点着色器(ripple.vsh)

attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute float a_edge;


struct RippleData
{
    vec2  center;
    vec2  coor_center;
    int   ripple_type;
    float run_time;
    float current_radius;
};


uniform vec2  texture_max;
uniform int   ripple_num;
uniform RippleData ripples[100];


#ifdef GL_ES
varying mediump vec2 v_texCoord;
#else
varying vec2 v_texCoord;
#endif


void main()
{
    float PI=3.1415927;
    float ripple_cycle = 0.4;
    float ripple_radius = 400.0;
    float life_span = 4.0;
    gl_Position = CC_MVPMatrix * a_position;
    vec2 vertex_pos = a_position.xy;
    if (ripple_num == 0 || a_edge == 1.0)
    {
        v_texCoord = a_texCoord;
    }
    else
    {
        v_texCoord = a_texCoord;
        for (int i = 0; i < ripple_num; i++)
        {
            float ripple_distance = distance(ripples[i].center, vertex_pos);
            float correction = 0.0;
            if (ripple_distance < ripples[i].current_radius)
            {
                if (ripples[i].ripple_type == 0)
                {
                   correction = sin(2.0 * PI * ripples[i].run_time / ripple_cycle);
                }
                else if (ripples[i].ripple_type == 1)
                {
                   correction = sin(2.0 * PI * (ripples[i].current_radius - ripple_distance)/ ripple_radius * life_span / ripple_cycle);
                }
                else
                {
                   correction = (ripple_radius * ripple_cycle / life_span)/(ripples[i].current_radius - ripple_distance);
                   if (correction > 1.0) correction = 1.0;
                   correction = correction * correction;
                   correction = sin(2.0 * PI * (ripples[i].current_radius - ripple_distance) / ripple_radius * life_span / ripple_cycle) * correction;


                }
                correction = correction * (1.0 - ripple_distance / ripples[i].current_radius);
                correction = correction * (1.0 - ripples[i].run_time / life_span);
                correction = correction * 0.1;
                correction = correction * 2.0;            
                correction = correction / distance(ripples[i].coor_center, v_texCoord);
                v_texCoord = v_texCoord + (v_texCoord - ripples[i].coor_center) * correction;
                v_texCoord = clamp(v_texCoord, vec2(0.0, 0.0), texture_max); 
            }
        }
    }
}

片段着色器(ripple.fsh)

#ifdef GL_ES                                                            
precision lowp float;                                              
#endif                                                                
                                                                     
varying vec2 v_texCoord;                                        
uniform sampler2D CC_Texture0;                                 
                                                                  
void main()                                                  
{                                                               
    gl_FragColor =  texture2D(CC_Texture0, v_texCoord);    
}                                                            


客户端用法,在点击时调用addRipple即可。

cocos2d::CCTouch* pTouch = (cocos2d::CCTouch*)pTouches->anyObject();

cocos2d::CCPoint touchLocation = pTouch->getLocation();// Get the touch position

touchLocation =m_rippleSprite->convertToNodeSpace(touchLocation);

m_rippleSprite->addRipple(touchLocation,RIPPLE_TYPE_WATER,1.0);


PS:由于着色器的Uniform变量的个数有限,所以我设置了最多只能有100个Ripple。而且此处实现边界水波纹反弹是使用在沿对应的边界建立一个镜像Ripple的方式来实现的。所以一个水波纹在到边界处有可能会变成4个。所以如果产生的水波纹太多,并且水波纹的life_span很长,可能会出现问题。目前考虑到得是每秒点击12次,声明周期是2S, 24 * 4 < 100, 所以能满足需求。

另外,感谢右半边翅膀Vincent改写的2.0 版本draw函数。
0 0
原创粉丝点击