【cocos2d-x 源码解析】shader 实现

1 移动设备上Unity的引擎调的就是OpenGL,但是PC上用的的DX,所以PC上Unity就不调OpenGL。目前ios用metal

-----------------基于Cocos2d-x学习OpenGL ES 2.0系列——初识MVP(3

文件结构
cocos2d-x 与 shader 相关的代码在 renderer 目录下

|-cocos
    |-renderer
        |-CCGLProgram.h
        |-CCGLProgram.cpp
        |-CCGLProgramCache.h
        |-CCGLProgramCache.cpp
        |-CCGLProgramState.h
        |-CCGLProgramState.cpp
        |-CCGLProgramStateCache.h
        |-CCGLProgramStateCache.cpp
        |-...
        |-ccShader_PositionColor.vert
        |-ccShaer_PositionColor.frag
        |-ccShader_UI_Gray.frag
        |-...
 

其中比较重要的几个类是 GLProgram,GLProgramCache,GLProgramState 和 GLProgramStateCache;除此之外还定义一些默认的着色器程序,.vert 后缀的文件是顶点着色器程序,.frag 后缀的文件是片段着色器程序。下面通过解析这几个主要类了解着色器的创建过程和使用方法

GLProgramCache
首先从 GLProgramCache 这个类入手,这个类是着色器 Program 的一个缓冲区,也就是说这个类保存了创建好的着色器程序,使用的时候直接从这缓冲中取 Program 即可。和其它缓冲类一样,GLProgramCache 也是单例模式

GLProgramCache* GLProgramCache::getInstance()
{
    if (!_sharedGLProgramCache) {
        _sharedGLProgramCache = new (std::nothrow) GLProgramCache();
        if (!_sharedGLProgramCache->init())
        {
            CC_SAFE_DELETE(_sharedGLProgramCache);
        }
    }
    return _sharedGLProgramCache;
}

第一次调用 getInstance 方法会先创建一个 GLProgramCache 实例,然后调用 init 方法进行初始化

bool GLProgramCache::init()
{
    loadDefaultGLPrograms();

    auto listener = EventListenerCustom::create(Configuration::CONFIG_FILE_LOADED, [this](EventCustom* event){
        reloadDefaultGLProgramsRelativeToLights();
    });

    Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(listener, -1);

    return true;
}

init 方法一个很重要的功能就是调用 loadDefaultGLPrograms 方法,这个方法创建所有默认的着色器程序

void GLProgramCache::loadDefaultGLPrograms()
{
    // Position Texture Color shader
    GLProgram *p = new (std::nothrow) GLProgram();
    loadDefaultGLProgram(p, kShaderType_PositionTextureColor);
    _programs.insert( std::make_pair( GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR, p ) );

    // Position Texture Color without MVP shader
    p = new (std::nothrow) GLProgram();
    loadDefaultGLProgram(p, kShaderType_PositionTextureColor_noMVP);
    _programs.insert( std::make_pair( GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP, p ) );

    //...
}
 

loadDefaultGLPrograms 创建 cocos 自带的所有着色器程序,首先创建一个 GLProgram,然后调用 loadDefaultGLProgram 进行内容填充,再插入到 _programs 字典中去

void GLProgramCache::loadDefaultGLProgram(GLProgram *p, int type)
{
    switch (type) {
        case kShaderType_PositionTextureColor:
            p->initWithByteArrays(ccPositionTextureColor_vert, ccPositionTextureColor_frag);
            break;
        case kShaderType_PositionTextureColor_noMVP:
            //...
        //...
        default:
            CCLOG("cocos2d: %s:%d, error shader type", __FUNCTION__, __LINE__);
            return;
    }
    p->link();
    p->updateUniforms();

    CHECK_GL_ERROR_DEBUG();
}

loadDefaultGLProgram 用于创建单个着色器程序,它接收两个参数,一个是事先创建出来的 GLProgram,一个是着色器类型;然后调用 GLProgram 的 initWithByteArrays 进行内容填充,根据不同的 type 传递不同的着色器源文件,ccPositionTextureColor_vert 这些变量在着色器源文件中定义,即上面说到的 .vert 和 .frag 文件中定义,它们的值是一个字符串,也就是着色器的源代码。这里为什么可以直接使用 ccPositionTextureColor_vert 这些变量呢?这是因为这些变量都是全局变量,而且在 ccShaders.cpp 中添加了所有的引用,ccShaders.cpp 的内容如下

#include "renderer/ccShaders.h"

#define STRINGIFY(A)  #A

NS_CC_BEGIN
//

#include "ccShader_Position_uColor.frag"

#include "ccShader_Position_uColor.vert"

//
#include "ccShader_PositionColor.frag"
#include "ccShader_PositionColor.vert"

//...

NS_CC_END

loadDefaultGLProgram 调用 GLProgram->initWithByteArrays 编译着色器,然后调用 GLProgram->link 链接着色器,然后调用 GLProgram->updateUniforms 设置 uniform 变量的值,至此 GLProgramCache 的初始化工作就完成了

GLProgramCache 缓冲了 cocos 自带的所有着色器程序,要使用 shader program 的时候直接从这个缓冲区取即可,getGLProgram 方法的实现很简单,直接根据 key 值从字典 _programs 中查找

GLProgram* GLProgramCache::getGLProgram(const std::string &key)
{
    auto it = _programs.find(key);
    if( it != _programs.end() )
        return it->second;
    return nullptr;
}
 

另外,还提供了两个导出函数

CC_DEPRECATED_ATTRIBUTE GLProgram * getProgram(const std::string &key) { return getGLProgram(key); }
CC_DEPRECATED_ATTRIBUTE GLProgram * programForKey(const std::string &key){ return getGLProgram(key); }
 
GLProgramCache 初始化时缓冲了一些着色器,如果后面想自己缓冲一些着色器也行,直接调用 addGLProgram 就行

void GLProgramCache::addGLProgram(GLProgram* program, const std::string &key)
{
    // release old one
    auto prev = getGLProgram(key);
    if( prev == program )
        return;

    _programs.erase(key);
    CC_SAFE_RELEASE_NULL(prev);

    if (program)
        program->retain();
    _programs[key] = program;
}
 

GLProgram
首先,GLProgram 定义了着色器程序的 key 值

const char* GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR = "ShaderPositionTextureColor";
const char* GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP = "ShaderPositionTextureColor_noMVP";
const char* GLProgram::SHADER_NAME_POSITION_TEXTURE_ALPHA_TEST = "ShaderPositionTextureColorAlphaTest";
const char* GLProgram::SHADER_NAME_POSITION_TEXTURE_ALPHA_TEST_NO_MV = "ShaderPositionTextureColorAlphaTest_NoMV";
//...

然后,GLProgram 还定义了着色器程序的一段共有代码,主要是定义常见的 uniform 变量,这段代码在着色器编译之前会合并到着色器源代码中去

static const char * COCOS2D_SHADER_UNIFORMS =
        "uniform mat4 CC_PMatrix;\n"
        "uniform mat4 CC_MVMatrix;\n"
        "uniform mat4 CC_MVPMatrix;\n"
        "uniform mat3 CC_NormalMatrix;\n"
        "uniform vec4 CC_Time;\n"
        "uniform vec4 CC_SinTime;\n"
        "uniform vec4 CC_CosTime;\n"
        "uniform vec4 CC_Random01;\n"
        "uniform sampler2D CC_Texture0;\n"
        "uniform sampler2D CC_Texture1;\n"
        "uniform sampler2D CC_Texture2;\n"
        "uniform sampler2D CC_Texture3;\n"
        "//CC INCLUDES END\n\n";

GLProgram 有下面几个工厂函数及初始化函数,主要是通过文件名和通过字符串两种方式来创建着色器

static GLProgram* createWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray);
bool initWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray);

static GLProgram* createWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray, const std::string& compileTimeDefines);
bool initWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray, const std::string& compileTimeDefines);

static GLProgram* createWithFilenames(const std::string& vShaderFilename, const std::string& fShaderFilename);
bool initWithFilenames(const std::string& vShaderFilename, const std::string& fShaderFilename);

static GLProgram* createWithFilenames(const std::string& vShaderFilename, const std::string& fShaderFilename, const std::string& compileTimeDefines);
bool initWithFilenames(const std::string& vShaderFilename, const std::string& fShaderFilename, const std::string& compileTimeDefines);

在这几个方法中,createWithByteArrays 会调用 initWithByteArrays 进行初始化,createWithFilenames 会调用 initWithFilenames 进行初始化,而 initWithFilenames 最终也是调用 initWithByteArrays,前面讲到的 GLProgramCache 创建 GLProgram 也是调用 initWithByteArrays。initWithByteArrays 是真正初始化着色器的地方,这个初始化过程其实就是编译着色器的过程

bool GLProgram::initWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray, const std::string& compileTimeDefines)
{
    _program = glCreateProgram();
    CHECK_GL_ERROR_DEBUG();

    // convert defines here. If we do it in "compileShader" we will do it it twice.
    // a cache for the defines could be useful, but seems like overkill at this point
    std::string replacedDefines = "";
    replaceDefines(compileTimeDefines, replacedDefines);

    _vertShader = _fragShader = 0;

    if (vShaderByteArray)
    {
        if (!compileShader(&_vertShader, GL_VERTEX_SHADER, vShaderByteArray, replacedDefines))
        {
            CCLOG("cocos2d: ERROR: Failed to compile vertex shader");
            return false;
       }
    }

    // Create and compile fragment shader
    if (fShaderByteArray)
    {
        if (!compileShader(&_fragShader, GL_FRAGMENT_SHADER, fShaderByteArray, replacedDefines))
        {
            CCLOG("cocos2d: ERROR: Failed to compile fragment shader");
            return false;
        }
    }

    if (_vertShader)
    {
        glAttachShader(_program, _vertShader);
    }
    CHECK_GL_ERROR_DEBUG();

    if (_fragShader)
    {
        glAttachShader(_program, _fragShader);
    }

    _hashForUniforms.clear();

    CHECK_GL_ERROR_DEBUG();

    return true;
}
 

可以看到,initWithByteArrays 首先会调用 OpenGL 函数 glCreateProgram 创建一个着色器程序 _program,然后调用 compileShader 方法编译着色器,然后判断编译是否成功,编译成功则调用 OpenGL 函数 glAttachShader 将着色器附加到着色器程序 _program。接下来看一下 compileShader 就去做的事

bool GLProgram::compileShader(GLuint* shader, GLenum type, const GLchar* source, const std::string& convertedDefines)
{
    GLint status;

    if (!source)
    {
        return false;
    }

    const GLchar *sources[] = {
#if CC_TARGET_PLATFORM == CC_PLATFORM_WINRT
        (type == GL_VERTEX_SHADER ? "precision mediump float;\n precision mediump int;\n" : "precision mediump float;\n precision mediump int;\n"),
#elif (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32 && CC_TARGET_PLATFORM != CC_PLATFORM_LINUX && CC_TARGET_PLATFORM != CC_PLATFORM_MAC)
        (type == GL_VERTEX_SHADER ? "precision highp float;\n precision highp int;\n" : "precision mediump float;\n precision mediump int;\n"),
#endif
        COCOS2D_SHADER_UNIFORMS,
        convertedDefines.c_str(),
        source
    };

    *shader = glCreateShader(type);
    glShaderSource(*shader, sizeof(sources)/sizeof(*sources), sources, nullptr);
    glCompileShader(*shader);

    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);

    if (! status)
    {
        GLsizei length;
        glGetShaderiv(*shader, GL_SHADER_SOURCE_LENGTH, &length);
        GLchar* src = (GLchar *)malloc(sizeof(GLchar) * length);

        glGetShaderSource(*shader, length, nullptr, src);
        CCLOG("cocos2d: ERROR: Failed to compile shader:\n%s", src);

        if (type == GL_VERTEX_SHADER)
        {
            CCLOG("cocos2d: %s", getVertexShaderLog().c_str());
        }
        else
        {
            CCLOG("cocos2d: %s", getFragmentShaderLog().c_str());
        }
        free(src);

        return false;;
    }
    return (status == GL_TRUE);
}

compileShader 方法首先会修改着色器源代码 source,在其前面加上公共定义的 uniform 变量部分,即前面定义的 COCOS2D_SHADER_UNIFORMS 变量,然后创建 shader,编译,检查错误

至此,initWithByteArrays 做的事就完了,我们再回到 createWithByteArrays 方法和 createWithFilenames 方法

GLProgram* GLProgram::createWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray, const std::string& compileTimeDefines)
{
    auto ret = new (std::nothrow) GLProgram();
    if(ret && ret->initWithByteArrays(vShaderByteArray, fShaderByteArray, compileTimeDefines)) {
        ret->link();
        ret->updateUniforms();
        ret->autorelease();
        return ret;
    }

    CC_SAFE_DELETE(ret);
    return nullptr;
}

GLProgram* GLProgram::createWithFilenames(const std::string& vShaderFilename, const std::string& fShaderFilename, const std::string& compileTimeDefines)
{
    auto ret = new (std::nothrow) GLProgram();
    if(ret && ret->initWithFilenames(vShaderFilename, fShaderFilename, compileTimeDefines)) {
        ret->link();
        ret->updateUniforms();
        ret->autorelease();
        return ret;
    }

    CC_SAFE_DELETE(ret);
    return nullptr;
}

可以看到,调用 initWithByteArrays 编译着色器之后会调用 link 方法链接着色器,调用 updateUniforms 方法设置 uniform 变量的值;前面讲到的 GLProgramCache 创建一个 GLProgram 的过程也是如此,这也是创建一个 shader program 的统一过程

  • 第一步,调用 initWithByteArrays 方法编译着色器
  • 第二步,调用 link 方法链接着色器
  • 第三步,调用 updateUniforms 方法设置 uniform 变量的值

GLProgramState 和 GLProgramStateCache
***通过解析 GLProgram 和 GLProgramCache 这两个类,我们已经知道了一个 shader program 的创建过程,接下来看一下如何使用 shader program ***,在这过程中解析一下 GLProgramState 和 GLProgramStateCache 这两个类。cocos2d-x 中的所有结点都可以使用着色器,即继承自 CCNode 的类都可以设置一个 shader program,CCNode 对外开放了一个接口 setShaderProgram,给一个结点添加一个着色器程序的过程非常简单

//test
auto program = ShaderCache::getInstance()->programForKey(GLProgram::SHADER_NAME_POSITION_GRAYSCALE);
sprite->setShaderProgram(program);
 
setShaderProgram 其实是调用内部的一个私有函数 setGLProgram

CC_DEPRECATED_ATTRIBUTE void setShaderProgram(GLProgram *glprogram) { setGLProgram(glprogram); }
1
void Node::setGLProgram(GLProgram* glProgram)
{
    if (_glProgramState == nullptr || (_glProgramState && _glProgramState->getGLProgram() != glProgram))
    {
        CC_SAFE_RELEASE(_glProgramState);
        _glProgramState = GLProgramState::getOrCreateWithGLProgram(glProgram);
        _glProgramState->retain();

        _glProgramState->setNodeBinding(this);
    }
}
 

通过 setGLProgram 方法可以看出,CCNode 实际上保存的并不是 GLProgram,而是 GLProgramState;GLProgramState 是对 GLProgram 的二次封装,其内部还是保存了一个 GLProgram,通过 GLProgramState 可以将 GLProgram 和 CCNode 进行绑定。GLPorgramState 同样使用一个缓冲 GLProgramStateCache 进行管理,就像 GLProgram 相对于 GLProgramCache 一样;如果多个结点使用了同个 GLProgram,cocos 只会创建一个 GLProgramState,任意一个 Node 修改了 GLProgramState,其它的结点也会受影响,如果不想其它结点受影响,则不要使用 setGLProgram 方法来给结点添加着色器程序,而是手动创建 GLProgramState 然后进行绑定。

setGLProgram 方法接收参数 GLProgram,然后判断是否已经存在和接收的 GLProgram 匹配的 GLProgramState 了,如果不存在再调用 GLProgramState 的静态方法 getOrCreateWithGLProgram 创建一个

GLProgramState* GLProgramState::getOrCreateWithGLProgram(GLProgram *glprogram)
{
    GLProgramState* ret = GLProgramStateCache::getInstance()->getGLProgramState(glprogram);
    return ret;
}
 
getOrCreateWithGLProgram 方法直接从缓冲区 GLProgramStateCache 获取 GLProgramState

GLProgramState* GLProgramStateCache::getGLProgramState(GLProgram* glprogram)
{
    const auto& itr = _glProgramStates.find(glprogram);
    if (itr != _glProgramStates.end())
    {
        return itr->second;
    }

    auto ret = new (std::nothrow) GLProgramState;
    if(ret && ret->init(glprogram)) {
        _glProgramStates.insert(glprogram, ret);
        ret->release();
        return ret;
    }

    CC_SAFE_RELEASE(ret);
    return ret;
}
 

GLProgramStateCache 通过一个字典 _glProgramStates 保存已经创建的 GLProgramState,调用 getGLProgramState 方法时先从字典中查找是否有与 GLProgram 匹配的 GLProgramState,有则返回,没有则创建一个,创建之后调用 GLProgramState->init 方法进行初始化,再插入到字典中去

bool GLProgramState::init(GLProgram* glprogram)
{
    CCASSERT(glprogram, "invalid shader");

    _glprogram = glprogram;
    _glprogram->retain();

    for(auto &attrib : _glprogram->_vertexAttribs) {
        VertexAttribValue value(&attrib.second);
        _attributes[attrib.first] = value;
    }

    for(auto &uniform : _glprogram->_userUniforms) {
        UniformValue value(&uniform.second, _glprogram);
        _uniforms[uniform.second.location] = value;
        _uniformsByName[uniform.first] = uniform.second.location;
    }

    return true;
}
 

init 方法将 GLProgram 保存下来,再将顶点属性和 uniform 变量保存下来

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值