cocos2dx - shader实现任意动画的残影效果

 本节主要讲利用cocos2dx机制实现opengl es shader脚本的绘制

  这里先看下最终效果:

cocos2dx - shader实现任意动画的残影效果_第1张图片                     cocos2dx - shader实现任意动画的残影效果_第2张图片

这里分别实现了灰度效果及残影的效果。

 

一、绘制基类

  这里主要参考了cocos2dx源码中 RenderTexture 的实现,有兴趣的可以了解下。

绘制基类RenderShader主要实现以下方法:

//******************************************************************
// 文件名:    RenderShader.h
// 创建人:    稀饭lei
// 版  本:    1.0
// 描  述:    特效基类
//******************************************************************
#ifndef _RenderShader_H__
#define _RenderShader_H__ 
#include "cocos2d.h"
USING_NS_CC;

// GL纹理坐标组
static const  GLfloat ccRenderTextcord[8] ={
    0.0f, 1.0f,
    1.0f, 1.0f,
    0.0f, 0.0f,
    1.0f, 0.0f,
};
class RenderShader : public Node {
public:
    static RenderShader* create();

    virtual bool LoadByteArray(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray);    //加载 shader文件

    void    begin();                // 用于设置绘制开始要调用的onBegin

    void    end();                    // 用于设置绘制结束要调用的onEnd

    void    push2Draw(Node* node);  // 添加待绘制节点

    virtual void draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) override;  // cocos2dx绘制回调

protected:
    virtual void onBegin();

    virtual void onEnd();

    //virtual void onClear();

    virtual void Render();                // 绘制实现


    virtual bool init();

    virtual void CreateFrameBuffer();    //创建FBO及texture

    RenderShader(void);

    virtual ~RenderShader(void);
 

    GLuint m_nFrameBuffer;            // 用于当前绘制的fbo
 GLuint m_nFrameBufferTexture;    // 用于绑定在fbo中的texture

    GLProgram* m_glprogram;            // GL绘制管理
 GLint m_nOldFBO;                // 原来的fbo
 CustomCommand _beginCommand;
    CustomCommand _endCommand;
    GroupCommand _groupCommand;

    std::set<Node*> sRenderChild;    // 需要绘制的节点
 Size    m_sRenderSize;
};

#endif

这里有几个主要实现:

1、我们用push2Draw(Node* node);  方法将待绘制的节点添加到sRenderChild中等待处理。

2、参考RenderTexture 的实现,我们也利用begin,end将需要绘制的节点利用visit访问添加到当前的RenderShader类所在的render层中进行绘制。

3、在实际绘制过程中调用的onBegin中缓存对应的坐标系,同时绑定我们创建FBO使得visit进来的节点可以绘制到当前FBO中。然后在onEnd中调用render方法将我们的FBO内容绘制原来cocos2dx底层的FBO上。

4、在render()实现如何绘制当前FBO的内容到cocos2dx的FBO上。(实际shader作业的地方)

详细实现代码如下:

#include "RenderShader.h"  
#define STRINGIFY(A)  #A
const char* ccPositionTextureColor_frag_test = STRINGIFY(
\n#ifdef GL_ES\n
  precision lowp float;
\n#endif\n
 
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

void main()
{
    vec4 color = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
    gl_FragColor = vec4(vec3(gray), color.a);
}
);

RenderShader* RenderShader::create()
{
    auto node = new RenderShader();
    if (node  && node->init())
    {
        node->autorelease();
        return node;
    }
    delete node;
    return nullptr;
}

bool RenderShader::init()
{
    if (m_glprogram)
    {
        m_glprogram->release();
        m_glprogram = nullptr;
    }

    CreateFrameBuffer();
  
    m_glprogram = GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, ccPositionTextureColor_frag_test);
    m_glprogram->retain();

    return true;
}

RenderShader::RenderShader(void)
    :m_nFrameBuffer(0), m_nFrameBufferTexture(0), m_glprogram(nullptr)
{
    GLView* glView = Director::getInstance()->getOpenGLView();
    if (!glView)
    {
        return;
    }
    //屏幕大小 
    m_sRenderSize = glView->getFrameSize();
}

RenderShader::~RenderShader(void)
{
    if (m_glprogram)
    {
        m_glprogram->release();
        m_glprogram = nullptr;
    }
    if (m_nFrameBuffer)
    {
        glDeleteFramebuffers(1, &m_nFrameBuffer);
        m_nFrameBuffer = 0;
    }
    if (m_nFrameBufferTexture)
    {
        glDeleteTextures(1, &m_nFrameBufferTexture);
        m_nFrameBufferTexture = 0;
    }
}

bool RenderShader::LoadByteArray(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray)
{
    if (m_glprogram)
    {
        m_glprogram->release();
        m_glprogram = nullptr;
    }

    this->CreateFrameBuffer();

    m_glprogram = GLProgram::createWithByteArrays(vShaderByteArray, fShaderByteArray);
    m_glprogram->retain();

    return true;
}


void RenderShader::onBegin()
{
    if (!m_nFrameBuffer)
    {
        return;
    }
    Director* director = Director::getInstance();
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

    //save old fbo and bind own
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &m_nOldFBO);
    glBindFramebuffer(GL_FRAMEBUFFER, m_nFrameBuffer);
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);
}

void RenderShader::onEnd()
{
    if (!m_nFrameBuffer)
    {
        return;
    }

    // restore viewport for render to direct fbo
    Director *director = Director::getInstance();
    // restore viewport
    director->setViewport();
    this->Render();

    glBindFramebuffer(GL_FRAMEBUFFER, m_nOldFBO);
    director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
    director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
}


void RenderShader::Render()
{
    if (!m_glprogram || !m_nFrameBufferTexture)
    {
        return;
    }

    glBindFramebuffer(GL_FRAMEBUFFER, m_nOldFBO);
    m_glprogram->use();
    m_glprogram->setUniformsForBuiltins();

    //GLuint glTexture = m_glprogram->getUniformLocationForName("CC_Texture0");
    //glActiveTexture(GL_TEXTURE0);
    //glBindTexture(GL_TEXTURE_2D, m_nFrameBufferTexture);
    //glUniform1i(glTexture, 0);
    GL::bindTexture2D(m_nFrameBufferTexture);
    Size sSize = Director::getInstance()->getVisibleSize();
    float x = 0;
    float y = 0;
    float w = sSize.width;
    float h = sSize.height;
    GLfloat vertices[] = {
        x, y + h,
        x + w, y + h,
        x, y,
        x + w, y
    };
    
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, ccRenderTextcord);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);


    /*GL::bindVAO(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);*/
}



void RenderShader::CreateFrameBuffer()
{
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &m_nOldFBO);
    if (m_nFrameBuffer == 0)
    {
        glGenFramebuffers(1, &m_nFrameBuffer);

        if (m_nFrameBuffer == 0)
        {
            CCLOG("m_FilterFrameBuffer == 0");
            return;
        }
    }
    glBindFramebuffer(GL_FRAMEBUFFER, m_nFrameBuffer);

    if (m_nFrameBufferTexture == 0)
    {
        glGenTextures(1, &m_nFrameBufferTexture);  
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, m_nFrameBufferTexture);

        GLsizei nWidth = m_sRenderSize.width;
        GLsizei nHeight = m_sRenderSize.height;
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nWidth, nHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
        GLenum err = glGetError();
        if (err != GL_NO_ERROR)
        {
            CCLOG("cocos2d: Texture2D: Error uploading compressed texture glError: 0x%04X", err);
            return;
        }

    }
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_nFrameBufferTexture, 0);


    GLenum error;
    if ((error = glCheckFramebufferStatus(GL_FRAMEBUFFER)) != GL_FRAMEBUFFER_COMPLETE)
    {
        CCLOG("Failed to make complete framebuffer object 0x%X", error);
        return;
    }

    glBindFramebuffer(GL_FRAMEBUFFER, m_nOldFBO);
}

void RenderShader::begin()
{ 
    Director* director = Director::getInstance();
    CCASSERT(nullptr != director, "Director is null when seting matrix stack");
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

    _groupCommand.init(_globalZOrder);

    Renderer *renderer = Director::getInstance()->getRenderer();
    renderer->addCommand(&_groupCommand);
    renderer->pushGroup(_groupCommand.getRenderQueueID());

    _beginCommand.init(_globalZOrder);
    _beginCommand.func = CC_CALLBACK_0(RenderShader::onBegin, this);

    renderer->addCommand(&_beginCommand);
}

void RenderShader::end()
{
    _endCommand.init(_globalZOrder);
    _endCommand.func = CC_CALLBACK_0(RenderShader::onEnd, this);

    Director* director = Director::getInstance();
    CCASSERT(nullptr != director, "Director is null when seting matrix stack");

    Renderer *renderer = director->getRenderer();
    renderer->addCommand(&_endCommand);
    renderer->popGroup();

    director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
    director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

}

void RenderShader::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    //Begin will create a render group using new render target
    begin();

    ////clear screen
    //_clearCommand.init(_globalZOrder);
    //_clearCommand.func = CC_CALLBACK_0(RenderShader::onClear, this);
    //renderer->addCommand(&_clearCommand);

    //! make sure all children are drawn
    {
        auto it = sRenderChild.begin();
        while (it != sRenderChild.end())
        {
            if (Node* node = *it)
            {
                node->visit(renderer, transform, flags);
                node->release();
            }
            ++it;
        }
        sRenderChild.clear();
    }
    //End will pop the current render group
    end();
}


void    RenderShader::push2Draw(Node* node)
{
    if (node && sRenderChild.find(node) == sRenderChild.end())
    {
        node->retain();
        sRenderChild.insert(node);
    }
    else
    {
        CCLOG(" push same!!!");
    }
}
View Code

这里shader代码实现的是灰度图的效果,其他效果可以自己修改ccPositionTextureColor_frag_test内容。

 

二、实际使用

1、将RenderShader当成正常的Node使用,设置父节点及index坐标等。

        m_pShader = RenderShader::create();
        this->addChild(m_pShader);

 

2、因为RenderShader在draw中对需要绘制的子节点进行了清理,所以需要每次重新对子节点进行添加,添加动画节点如下:

void CPlayer::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{ 
    if (m_pShader)
    {
        m_pShader->push2Draw(m_pNode);
    } 
}

主要事项:RenderShader在Render方法中将内容绘制到原来的FBO中,而cocos2dx对每个需要绘制的同一index层级会在开始的时候单独创建一个FBO来处理。因而如果RenderShader单独一个层级可能导致此时的FBO没有创建,导致没有绘制效果。

到这里我们就实现灰色角色的效果了,使用也依照cocos2dx的用法,相对简单。

 

三、残影效果扩展

主要实现代码如下:

//******************************************************************
// 文件名:    RemindShader.h
// 创建人:    稀饭lei
// 版  本:    1.0
// 描  述:    残影特效
//******************************************************************
#ifndef _RemindShader_H__
#define _RemindShader_H__ 
#include "RenderShader.h"
USING_NS_CC;
#define REMIND_RENDER_COUNT 5  // 残影个数
class RemindShader :public RenderShader
{
public:
    static RemindShader* create();

    virtual void draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) override;
private:
    virtual void onBegin() override;

    //virtual void onClear();

    virtual void Render() override;

    virtual bool init() override;

    virtual void CreateFrameBuffer() override;

    RemindShader(void);

    virtual ~RemindShader(void);

    GLuint m_pTextureArr[REMIND_RENDER_COUNT]; 
      
    int   m_nRenderFrq;            // 渲染间隔帧率

    int      m_nRenderCount;        // 渲染间隔计数

    int   m_nCurTexutreIndex;    // 当前渲染的纹理ID
};

#endif

#include "RemindShader.h"

#define STRINGIFY(A)  #A
const char* ccRemind_fsh = STRINGIFY(
    \n#ifdef GL_ES\n
    precision lowp float;
    \n#endif\n

    varying vec2 v_texCoord;
    uniform float u_alpha;

    void main()
    {
        vec4 pcolor = texture2D(CC_Texture0, v_texCoord) * u_alpha;
        gl_FragColor = pcolor;
    }
    );


RemindShader* RemindShader::create()
{
    auto node = new RemindShader();
    if (node  && node->init())
    {
        node->autorelease();
        return node;
    }
    delete node;
    return nullptr;
}

bool RemindShader::init()
{
    if (m_glprogram)
    {
        m_glprogram->release();
        m_glprogram = nullptr;
    }

    CreateFrameBuffer();
    m_glprogram = GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, ccRemind_fsh);
    m_glprogram->retain();
    return true;
}

RemindShader::RemindShader(void)
: m_nCurTexutreIndex(0), m_nRenderFrq(10), m_nRenderCount(0)
{
    memset(m_pTextureArr, 0, REMIND_RENDER_COUNT);
}

RemindShader::~RemindShader(void)
{
    if (m_pTextureArr[0])
    {
        glDeleteTextures(REMIND_RENDER_COUNT, m_pTextureArr);
        memset(m_pTextureArr, 0, REMIND_RENDER_COUNT);
    }
}
 
void RemindShader::onBegin()
{
    if (!m_nFrameBuffer)
    {
        return;
    }    
    Director* director = Director::getInstance(); 
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
//    director->setProjection(director->getProjection());
//    GLView* glView = director->getOpenGLView();
//    if (!glView)
//    {
//        return;
//    }
//    Size frameSize =glView->getFrameSize();
//    {
//        // Calculate the adjustment ratios based on the old and new projections
//        Size size = director->getWinSizeInPixels();
//        float widthRatio = size.width / frameSize.width;
//        float heightRatio = size.height / frameSize.height;
//        //caculate the projections of size change
//        Mat4 orthoMatrix;
//        Mat4::createOrthographicOffCenter((float)-1.0 / widthRatio, (float)1.0 / widthRatio, (float)-1.0 / heightRatio, (float)1.0 / heightRatio, -1, 1, &orthoMatrix);
//        director->multiplyMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION, orthoMatrix);
//    }
//    
//    //calculate viewport
//    {
//        glViewport(0, 0, (GLsizei)(frameSize.width), (GLsizei)(frameSize.height));
//    }
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &m_nOldFBO);
    // 仅在计数为0才更新新的渲染
    if (m_nRenderCount == 0)
    {
        glBindFramebuffer(GL_FRAMEBUFFER, m_nFrameBuffer);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_pTextureArr[m_nCurTexutreIndex], 0);
        m_nCurTexutreIndex = m_nCurTexutreIndex + 1 < REMIND_RENDER_COUNT ? m_nCurTexutreIndex + 1 : 0;

         glClearColor(0.0, 0.0, 0.0, 0.0);
         glClear(GL_COLOR_BUFFER_BIT);
    }
}

void RemindShader::Render()
{
    if (!m_glprogram )
    {
        return;
    }

    glBindFramebuffer(GL_FRAMEBUFFER, m_nOldFBO);
    m_glprogram->use();
    m_glprogram->setUniformsForBuiltins();

    Size sSize = Director::getInstance()->getVisibleSize();
    float x = 0;
    float y = 0;
    float w = sSize.width;
    float h = sSize.height;

    // 除当前纹理外的纹理存储的都是拖影
    int nBeginIndex = m_nCurTexutreIndex;
    int nCurIndex = nBeginIndex;
    GLfloat falpha = 0.3f;
    float addf = 0.05f;

    // 渲染存在m_pTextureArr队列里的拖影纹理到主场景
    do
    {    
        Point delta = _position;// this->convertToWorldSpace(_position);
        GLfloat vertices[] = {
            x   - delta.x,y+ h - delta.y,
            x+w - delta.x,y+ h - delta.y,
            x - delta.x  , y - delta.y,
            x+w - delta.x, y - delta.y
        };
        //GLuint glTexture = m_glprogram->getUniformLocationForName("CC_Texture0");
        //glActiveTexture(GL_TEXTURE0);
        //glBindTexture(GL_TEXTURE_2D, m_pTextureArr[nCurIndex]);
        //glUniform1i(glTexture, 0);
        GL::bindTexture2D(m_pTextureArr[nCurIndex]);

        GLuint glAlpha = m_glprogram->getUniformLocationForName("u_alpha");
        glUniform1f(glAlpha, falpha);

        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, vertices);
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, ccRenderTextcord);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        falpha += addf;
        addf += 0.025f;
        nCurIndex = ++nCurIndex<REMIND_RENDER_COUNT ? nCurIndex : 0; 
    } while (nCurIndex != nBeginIndex);
//
//    GL::bindVAO(0);
    //glBindBuffer(GL_ARRAY_BUFFER, 0);
    //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
void RemindShader::CreateFrameBuffer()
{

    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &m_nOldFBO);
    if (m_nFrameBuffer == 0)
    {
        glGenFramebuffers(1, &m_nFrameBuffer);

        if (m_nFrameBuffer == 0)
        {
            CCLOG("m_FilterFrameBuffer == 0");
            return;
        }
    }
    GLsizei nWidth = m_sRenderSize.width;
    GLsizei nHeight = m_sRenderSize.height;
    glBindFramebuffer(GL_FRAMEBUFFER, m_nFrameBuffer);
    if (m_pTextureArr[0] == 0)
    {
        glGenTextures(REMIND_RENDER_COUNT, m_pTextureArr);
        for (int i = 0; i < REMIND_RENDER_COUNT; i++)
        {
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, m_pTextureArr[i]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nWidth, nHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
            GLenum err = glGetError();
            if (err != GL_NO_ERROR)
            {
                CCLOG("cocos2d: Texture2D: Error uploading compressed texture glError: 0x%04X", err);
                return;
            }
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_pTextureArr[i], 0);
            glClearColor(0.0, 0.0, 0.0, 0.0);
            glClear(GL_COLOR_BUFFER_BIT);
        }
    }
    glBindFramebuffer(GL_FRAMEBUFFER, m_nOldFBO);
}
  

void RemindShader::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    if (sRenderChild.size() <= 0 )
    {
        if (m_nFrameBuffer)
        {
            // 清理所有缓存的纹理
            glGetIntegerv(GL_FRAMEBUFFER_BINDING, &m_nOldFBO);
            glBindFramebuffer(GL_FRAMEBUFFER, m_nFrameBuffer);
            for (int i = 0; i < REMIND_RENDER_COUNT; i++)
            {
                glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_pTextureArr[i], 0);
                glClearColor(0.0, 0.0, 0.0, 0.0);
                glClear(GL_COLOR_BUFFER_BIT);
            }
            glBindFramebuffer(GL_FRAMEBUFFER, m_nOldFBO);
        }
        m_nRenderCount = m_nRenderFrq - 1;
        return;
    }
    //Begin will create a render group using new render target
    begin();

    if (++m_nRenderCount == m_nRenderFrq)
    {
        //! make sure all children are drawn 
        auto it = sRenderChild.begin();
        while (it != sRenderChild.end())
        {
            if (Node* node = *it)
            {
                node->visit(renderer, transform, flags);
                node->release();
            }
            ++it;
        }
        m_nRenderCount = 0;    // 0 提示需要渲染进去
    }
    sRenderChild.clear();
    //End will pop the current render group
    end();

}
View Code

几个关键点:

1、利用REMIND_RENDER_COUNT控制需要创建的Texture个数,通过每次绘制到不同的Texture上来保存原来的效果。

2、shader中利用u_alpha参数控制残影的透明度,在render中绘制到原来的FBO上时调用不同的透明度来实现残影渐隐的效果。

3、通过m_nRenderFrq来控制间隔的绘制次数,来实现残影的残留效果远近。

这样就实现了残影的效果了,使用的方法还是跟基类一样。

完整代码地址:https://github.com/Steaphans/cocos2dx-Ex/tree/master/Shader

你可能感兴趣的:(cocos2dx - shader实现任意动画的残影效果)