cocos2d-x 着色器源码GLProgram和GLProgramState

版本: Cocos2dx 3.17

环境: Xcode


简介


上一篇cocos2d-x OpenGL着色器中介绍了在OpenGL中使用着色器。

本篇文章主要讲解在cocos2d-x中关于对着色器的封装使用相关,主要使用类:

  • GLProgram 对着色器程序Program的封装,包含着色器的加载,编译和链接, 以及Uniform和Attribute的绑定和赋值相关
  • GLProgramState 对着色器状态进行的封装,它提供了对于Unifrom和Attribute属性数据的设置相关

GLProgram在全局中仅有一个实例, 而GLProgramState会创建多个。

每个UI元素对应着一个GLProgramState的实例。其继承结构为:

GLProgram
Ref
GLProgramState

频繁的创建GLProgramGLProgramState 都会很影响性能,因此同精灵帧缓存或动画缓存一样,官方提供了:

  • GLProgramCache
  • GLProgramStateCache

其继承结构为:

GLProgramCache
Ref
GLProgramStateCache

GLProgram


在cocos2d-x提供了两种方式用于着色器的创建相关:

  • 使用文件方式创建,顶点着色器的后缀名为.vsh,片段着色器的后缀名为.fsh
  • 使用字符串方式创建

主要接口是:

// 通过shader文件进行创建
static GLProgram* createWithFilenames(const std::string& vShaderFilename, const std::string& fShaderFilename);
// 通过字符串方式进行创建
static GLProgram* createWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray);

他们所做的共同的事情主要如下:

  • 调用GLProgram::initWithByteArrays完成着色器对象的创建,加载源码,编译等
  • 调用GLProgram::link完成程序对象的链接相关,以及顶点属性的绑定相关
  • 调用GLProgram::updateUniforms完成对unifroms变量设定相关

initWithByteArrays: 它所做的主要的事情是:

  1. 通过glCreateProgram创建了程序对象
  2. 通过GLProgram::compileShader完成对顶点和片段着色器的创建,加载及编译等
  3. 通过glAttachShader将着色器挂载到程序对象中。

其主要代码如下:

bool GLProgram::initWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray, ..) {
    // 创建着色器程序对象Program
    _program = glCreateProgram();
    CHECK_GL_ERROR_DEBUG();
  
  	/*
  	创建着色器相关(片段着色器与顶点着色器类似,不再粘贴), 步骤:
  	1. 通过compileShader接口,创建着色器对象,并进行编译等操作
  	2. 将着色器对象(shader)挂载到程序对象(program)上
  	*/
	  _vertShader = _fragShader = 0;
    // 顶点着色器的创建
    if (vShaderByteArray) {
        if (!compileShader(&_vertShader, GL_VERTEX_SHADER, vShaderByteArray, ..)) {
            CCLOG("cocos2d: ERROR: Failed to compile vertex shader");
            return false;
        }
    }

    if (_vertShader) {
        // 将顶点着色器对象挂载到程序对象
        glAttachShader(_program, _vertShader);
    }
    CHECK_GL_ERROR_DEBUG();
    return true;
}
// 着色器对象的创建,编译等
bool GLProgram::compileShader(GLuint *shader, GLenum type, const GLchar* source, const std::string& compileTimeHeaders, const std::string& convertedDefines)
{
    GLint status;

    if (!source)
        return false;

  	// 设定精度类型相关相关
    std::string headersDef;
    if (compileTimeHeaders.empty()) {
#if CC_TARGET_PLATFORM == CC_PLATFORM_WINRT
        headersDef = (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_ANDROID
        headersDef = "#version 100\n precision highp float;\n precision highp int;\n";
#elif (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32 && CC_TARGET_PLATFORM != CC_PLATFORM_LINUX && CC_TARGET_PLATFORM != CC_PLATFORM_MAC)
        headersDef = (type == GL_VERTEX_SHADER ? "precision highp float;\n precision highp int;\n" : "precision mediump float;\n precision mediump int;\n");
#endif
    }else{
        headersDef = compileTimeHeaders;
    }

    const GLchar *sources[] = {headersDef.c_str(), 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);
}

GLProgram::link: 该接口主要做的事情:

  1. 通过接口GLProgram::bindPredefinedVertexAttribs将预定义的顶点属性绑定到程序对象中,比如: a_position、a_color、a_texCoord等
  2. 通过glLinkProgram将着色器链接在一起
  3. 链接成功后,通过parseVertexAttribsparseUniforms解析着色器源码中的attribute和uniform属性相关,并存储下来,方便GLProgramState设定属性值
  4. 通过接口clearShader删除顶点着色器,片段着色器对象
bool GLProgram::link()
{
    CCASSERT(_program != 0, "Cannot link invalid program");
    GLint status = GL_TRUE;
	// 将预定义顶点属性绑定到程序对象中,比如:a_position、a_color、a_texCoord等
	// 注意该接口的使用是在glLinkProgram前
    bindPredefinedVertexAttribs();
	// 通过程序对象将着色器链接在一起,并检测链接状态
    glLinkProgram(_program);
    glGetProgramiv(_program, GL_LINK_STATUS, &status);
    if (status == GL_FALSE) {
        CCLOG("cocos2d: ERROR: Failed to link program: %i", _program);
        GL::deleteProgram(_program);
        _program = 0;
    }
    else {
        // 解析着色器源码中的attribute和uniform属性,并存储下来
        parseVertexAttribs();
        parseUniforms();
		// 该接口主要调用:glDeleteShader删除对应的着色器对象
        clearShader();
    }
    return (status == GL_TRUE);
}

void GLProgram::bindPredefinedVertexAttribs()
{
    static const struct {
        const char *attributeName;
        int location;
    } attribute_locations[] =
    {
        {GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION}, 		
        {GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR},					
        {GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORD},	
        {GLProgram::ATTRIBUTE_NAME_TEX_COORD1, GLProgram::VERTEX_ATTRIB_TEX_COORD1},
        {GLProgram::ATTRIBUTE_NAME_TEX_COORD2, GLProgram::VERTEX_ATTRIB_TEX_COORD2},
        {GLProgram::ATTRIBUTE_NAME_TEX_COORD3, GLProgram::VERTEX_ATTRIB_TEX_COORD3},
        {GLProgram::ATTRIBUTE_NAME_NORMAL, GLProgram::VERTEX_ATTRIB_NORMAL}, 
    };
  	/*
  	ATTRIBUTE_NAME_POSITION 	  "a_position"
  	ATTRIBUTE_NAME_COLOR		  "a_color"
  	ATTRIBUTE_NAME_TEX_COORD1~3	  "a_texCoord1~3"
  	ATTRIBUTE_NAME_NORMAL		  "a_normal
  	*/

    const int size = sizeof(attribute_locations) / sizeof(attribute_locations[0]);
    for(int i=0; i<size;i++) {
        glBindAttribLocation(_program, attribute_locations[i].location, attribute_locations[i].attributeName);
    }
}

GLProgram::updateUniforms主要做的事情:

通过glGetUniformLocation检测着色器中是否设定了对应的uniform属性相关,比如:"CC_PMatrix"等。

如果没有会返回-1。它主要做了两件事:

  1. 获取通用属性相关,存储到_builtInUniforms到,用于每帧刷新时候uniform属性设定
  2. 获取采样器属性相关,并进行设定
void GLProgram::updateUniforms()
{
  	/*
  	矩阵相关
  	UNIFORM_NAME_P_MATRIX: 对应"CC_PMatrix"
  	UNIFORM_NAME_MV_MATRIX: 对应"CC_MVMatrix"
  	UNIFORM_NAME_MVP_MATRIX: 对应"CC_MVPMatrix"
  	UNIFORM_NAME_NORMAL_MATRIX: 对应"CC_NormalMatrix"
  	*/
    _builtInUniforms[UNIFORM_P_MATRIX] = glGetUniformLocation(_program, UNIFORM_NAME_P_MATRIX);
    _builtInUniforms[UNIFORM_MV_MATRIX] = glGetUniformLocation(_program, UNIFORM_NAME_MV_MATRIX);
    _builtInUniforms[UNIFORM_MVP_MATRIX] = glGetUniformLocation(_program, UNIFORM_NAME_MVP_MATRIX);
    _builtInUniforms[UNIFORM_NORMAL_MATRIX] = glGetUniformLocation(_program, UNIFORM_NAME_NORMAL_MATRIX);
	/*
	时间相关
	UNIFORM_NAME_TIME: 对应"CC_Time"
	UNIFORM_NAME_SIN_TIME: 对应"CC_SinTime"
	UNIFORM_NAME_COS_TIME: 对应"CC_CosTime"
	*/
    _builtInUniforms[UNIFORM_TIME] = glGetUniformLocation(_program, UNIFORM_NAME_TIME);
    _builtInUniforms[UNIFORM_SIN_TIME] = glGetUniformLocation(_program, UNIFORM_NAME_SIN_TIME);
    _builtInUniforms[UNIFORM_COS_TIME] = glGetUniformLocation(_program, UNIFORM_NAME_COS_TIME);
	/*
	采样器相关
	UNIFORM_NAME_SAMPLER0: 对应"CC_Texture0"
	UNIFORM_NAME_SAMPLER1: 对应"CC_Texture1"
	UNIFORM_NAME_SAMPLER2: 对应"CC_Texture2"
	UNIFORM_NAME_SAMPLER3: 对应"CC_Texture3"
	*/
    _builtInUniforms[UNIFORM_SAMPLER0] = glGetUniformLocation(_program, UNIFORM_NAME_SAMPLER0);
    _builtInUniforms[UNIFORM_SAMPLER1] = glGetUniformLocation(_program, UNIFORM_NAME_SAMPLER1);
    _builtInUniforms[UNIFORM_SAMPLER2] = glGetUniformLocation(_program, UNIFORM_NAME_SAMPLER2);
    _builtInUniforms[UNIFORM_SAMPLER3] = glGetUniformLocation(_program, UNIFORM_NAME_SAMPLER3);

  	// 通过_flags进行保存对应的标记,在设定对应的属性时,通过该标记来判定
    _flags.usesP = _builtInUniforms[UNIFORM_P_MATRIX] != -1;
    _flags.usesMV = _builtInUniforms[UNIFORM_MV_MATRIX] != -1;
    _flags.usesMVP = _builtInUniforms[UNIFORM_MVP_MATRIX] != -1;
		
  	// 最终会调用:glUseProgram
    this->use();

    // Since sample most probably won't change, set it to 0,1,2,3 now.
    if(_builtInUniforms[UNIFORM_SAMPLER0] != -1)
       setUniformLocationWith1i(_builtInUniforms[UNIFORM_SAMPLER0], 0);
    if(_builtInUniforms[UNIFORM_SAMPLER1] != -1)
        setUniformLocationWith1i(_builtInUniforms[UNIFORM_SAMPLER1], 1);
    if(_builtInUniforms[UNIFORM_SAMPLER2] != -1)
        setUniformLocationWith1i(_builtInUniforms[UNIFORM_SAMPLER2], 2);
    if(_builtInUniforms[UNIFORM_SAMPLER3] != -1)
        setUniformLocationWith1i(_builtInUniforms[UNIFORM_SAMPLER3], 3);
    
    glGetError();
}

关于_flags相关,会在每帧刷新绘制的时候通过接口:GLProgram::setUniformsForBuiltins进行设置。

void GLProgram::setUniformsForBuiltins(const Mat4 &matrixMV)
{
    const auto& matrixP = _director->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
    if (_flags.usesP)
        setUniformLocationWithMatrix4fv(_builtInUniforms[UNIFORM_P_MATRIX], matrixP.m, 1);
		// ...
}


GLProgramState


上面说过,每个UI元素对应着一个GLProgramState的实例对象。它的主要作用有:

  • 通过VertexAttribValue类接口封装了着色器的attribute解析方式相关
  • 通过UniformValue类接口封装了uniform的使用相关
  • 通过GLProgramState::apply将attribute, uniform等属性在每帧刷新的时候应用到OpenGL绘制中
  • 作为参数绑定到对应的UI元素中

在GLProgramState初始化的时候,会完成对VertexAttribValueUniformValue的初始化

// 初始化,参数为: 着色器程序对象
bool GLProgramState::init(GLProgram* glprogram) {
    _glprogram = glprogram;
    _glprogram->retain();

  	// _glprogram->_vertexAttribs是通过parseVertexAttribs从着色器解析出来的attribute属性数据
  	// 通过遍历将数据存储到VertexAttribValue中
    for(auto &attrib : _glprogram->_vertexAttribs) {
        VertexAttribValue value(&attrib.second);
        _attributes[attrib.first] = value;
    }
	// _glprogram->_userUniforms是通过parseUniforms从着色器解析出来的unfiorm属性数据
  	// 通过遍历将数据存储到UniformValue中
    for(auto &uniform : _glprogram->_userUniforms) {
        UniformValue value(&uniform.second, _glprogram);
        _uniforms[uniform.second.location] = std::move(value);
        _uniformsByName[uniform.first] = uniform.second.location;
    }
    return true;
}
  1. VertexAttribValue结构:
// .h
class CC_DLL VertexAttribValue {
public:
	VertexAttribValue(VertexAttrib *vertexAttrib);
    ~VertexAttribValue();
  	void apply();
};

//.cpp
void VertexAttribValue::apply() {
  	// 调用该接口,用于设定顶点属性数据的解析相关
    glVertexAttribPointer(_vertexAttrib->index,  			// 顶点属性位置索引
    	_value.pointer.size,								// 顶点属性大小
        _value.pointer.type,								// 类型
        _value.pointer.normalized,							// 数据是否被标准化
        _value.pointer.stride,								// 步长
        _value.pointer.pointer);							// 位置数据在缓冲区起始位置的偏移量
}

最直观的理解就是,对顶点数据属性进行的封装

  1. UniformValue结构:
// .h
class CC_DLL UniformValue {
public:
    UniformValue(Uniform *uniform, GLProgram* glprogram);
    ~UniformValue();

  	void setFloat(float value);
    void setInt(int value);
    void apply();
};

//.cpp
void UniformValue::apply() {
    if (_type == Type::POINTER){
        switch (_uniform->type) {
            case GL_FLOAT:
                _glprogram->setUniformLocationWith1fv(_uniform->location, _value.floatv.pointer, _value.floatv.size);
                break;
            // ..
        }
     // ..
}

再看下GLProgram设定uniform属性相关

void GLProgramState::setUniformInt(const std::string& uniformName, int value) {
    auto v = getUniformValue(uniformName);
  	v->setInt(value);
}

到这里,UniformValue就是封装了对Uniform属性的使用相关,主要接口有:

void setUniformInt(const std::string& uniformName, int value);
void setUniformFloat(const std::string& uniformName, float value);
void setUniformFloatv(const std::string& uniformName, ssize_t size, const float* pointer);
void setUniformVec2(const std::string& uniformName, const Vec2& value);
void setUniformVec2v(const std::string& uniformName, ssize_t size, const Vec2* pointer);
void setUniformVec3(const std::string& uniformName, const Vec3& value);
void setUniformVec3v(const std::string& uniformName, ssize_t size, const Vec3* pointer);
void setUniformVec4(const std::string& uniformName, const Vec4& value);
void setUniformVec4v(const std::string& uniformName, ssize_t size, const Vec4* pointer);
void setUniformMat4(const std::string& uniformName, const Mat4& value);

  1. 针对于GLProgramState::apply的使用, 它主要做了如下事情:
void GLProgramState::apply(const Mat4& modelView)
{
    applyGLProgram(modelView);				// 激活着色器,并设置uniform属性相关
    applyAttributes();						// 应用设定的attribute属性相关
    applyUniforms();						// 应用设定的uniform属性相关
}

它会在每帧Render调用不同的绘制命令,且执行绘制渲染元素的时候调用。
主要目的是对着色器设置的attribute和uniform的属性数据应用到着色器中进行刷新。

示例:


通过cocosLua我们创建一个置灰的示例看下他们的使用:

local vertex = [[  
    attribute vec4 a_position; 				// 顶点位置
    attribute vec2 a_texCoord;				// 纹理 
    attribute vec4 a_color;					// 颜色

    varying vec4 v_fragmentColor;
    varying vec2 v_texCoord;

    void main()
    {
        gl_Position = CC_PMatrix * a_position;
        v_fragmentColor = a_color;
        v_texCoord = a_texCoord;
    }
]] 

local fragment = [[  
    varying vec4 v_fragmentColor;
    varying vec2 v_texCoord;
    uniform float u_strength;           // 强度[0~1]
    void main()
    {
        vec4 textColor = texture2D(CC_Texture0, v_texCoord);
		// 取颜色的平均值 * 强度,获取灰色
        float color = (textColor.r + textColor.g + textColor.b)/3.0 * u_strength;
        gl_FragColor.rgb = vec3(color);
        gl_FragColor.a = textColor.a;
    } 
]]

function Shader_GrayTest:show() 
  	-- 创建着色器程序
  	local program = cc.GLProgram:createWithByteArrays(vertex, fragment)
  	-- 创建着色器状态对象
    local programState = cc.GLProgramState:getOrCreateWithGLProgram(program)
  	-- 设置uniform数值值
    programState:setUniformFloat("u_strength", 0.8)
  	-- 将状态对象绑定到UI对象中
    node:setGLProgramState(programState)
end 

针对于如上的示例程序,其实可以明白:

  • 关于attribute属性变量,比如a_positiona_texCoorda_color的由来
  • 引擎针对于着色器的使用帮我们已经封装了很多的接口相关, 通过GLProgramState进行调用
  • 通过调用setGLProgramState将着色器对象相关绑定UI对象中,就可以完成shader的对UI元素的绘制

另外,针对于Program的创建, 以及ProgramState的创建,应该使用Cache, 如下的方式更为妥当些:

function getProgramState(key, vertName, fragName)
  	-- 从缓冲中获取对应的program对象
    local program = cc.GLProgramCache:getInstance():getGLProgram(key)
  	-- 如果不存在,则新建
    if not program then 
        program = cc.GLProgram:createWithByteArrays(vertName, fragName)
        cc.GLProgramCache:getInstance():addGLProgram(program, key)
    end 
  	-- 获取状态对象
    local programState = cc.GLProgramState:getOrCreateWithGLProgram(program)
    return programState
end 

完整的示例参考:Demo_Shader

回顾

在上面的内容中,有

通过之前的博客cocos2d-x 渲染机制简介中,我们曾说明过这么一点内容:draw主要用于生成绘制命令:

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags){
    if(_insideBounds) {
        _trianglesCommand.init(_globalZOrder,				// 全局层级
                               _texture,					// 纹理
                               getGLProgramState(),			// GLProgramState的对象
                               _blendFunc, 					// 混合模式
                               _polyInfo.triangles,			// 顶点数据集合
                               transform,					// 矩阵
                               flags);						// 是否为3D标记
        renderer->addCommand(&_trianglesCommand);
    }
}

在绘制命令初始化中,会将GLProgramState的对象发送到绘制命令中。

Renderer::processRenderCommand对不同的绘制命令执行渲染时,就会通过GLProgramState对象执行apply的方法。

void TrianglesCommand::useMaterial() const
{
    GL::bindTexture2D(_textureID);
    GL::blendFunc(_blendType.src, _blendType.dst);
    
  	// 执行apply
  	// 该接口会激活着色器,并将UI元素设定的attribute,uniform属性应用进来
    _glProgramState->apply(_mv);
}

断点跟踪截图为:
请添加图片描述
根据标记部分,可以看到最后会调用glDrawElements绘制。

我们汇总下着色器的大致绘制流程:

  • 应用程序通过文件或者字符串构建GLProgram着色器程序对象
  • GLProgram对象通过OpenGL接口相关完成着色器对象的创建,编译等
  • 链接前绑定一些内置的顶点属性,比如a_postiona_colora_textCoord
  • 链接成功后从着色器源码中提取attribute和unifrom属性相关,保存下来,并删除着色器对象
  • 获取uniform的采样器相关设置属性值
  • 通过GLProgramState构建UI绘制状态对象,保存UI元素的一些attribute和uniform属性相关
  • 该状态对象被绑定到UI对象中
  • Node对象在绘制的时候,会将绘制命令同GLProgramState实例对象一同发送给RenderQueue
  • 在Render进行绘制的时候会调用GLProgram的apply方法使用这些属性相关
  • 最后调用glDrawElements绘制

最后祝大家学习生活愉快!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鹤九日

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值