前言
本文介绍了cocos2d-x中CCGLProgram这个类,这篇文章假设读者已经了解OpenGL ES程序的基本渲染流程:顶点和片段着色器的源代码的编写、着色器的创建、program的创建、源代码绑定、编译、链接、OpenGL客户端-服务端传值等。CCGLProgram这个类正是对OpenGL ES渲染流程中涉及到着色器的编译、链接、传值等部分的抽象。
本文先总起介绍CCGLProgram所做的工作,然后分析其源代码中的关键实现,最后给出CCGLProgram的使用实例。
总体上认识CCGLProgram
CCGLProgram,人如其名,它是一个OpenGL Program,通过一个顶点着色器和一个片段着色器代码(对于不支持shader即时编译功能的显卡,传入编译好的二进制着色器数据)来创建一个CCGLProgram.
CCGLProgram内部做下面这些工作:
compileShader方法,内部调用glCreateShader,glShaderSource,glCompileShader等OpenGL API创建建顶点和片段着色器,并编译
调用glCreateProgram等OpenGL API完成program创建
定义了8个预定义的uniform,
updateUniforms方法,完成预定义的8个uniform的绑定
setUniformsForBuiltins方法完成在绘制开始时,给预定义的uniform的传值工作
使用一个哈希表
m_pHashForUniforms
作为缓存,保存每一个uniform的值,如果不发生变化就不会重复上传该uniform的值到OpenGL服务器
CCGLProgram关键代码分析
成员变量
@H_301_36@GLuint m_uProgram; // 着色器程序,glCreateProgram()的返回值 GLuint m_uVertShader; // 顶点着色器,glCreateShader(GL_VERTEX_SHADER)返回值 GLuint m_uFragShader; // 片段着色器,glCreateShader(GL_FRAGMENT_SHADER)返回值 GLint m_uUniforms[kCCUniform_MAX]; // 预定以的8个uniform常量 struct _hashUniformEntry* m_pHashForUniforms; // 着色器程序中uniform常量的缓存 bool m_bUsesTime; // 是否在着色器程序中使用时间 bool m_hasShaderCompiler; // GPU是否支持online compile,即是否有shader的编译功能构造类方法
@H_301_36@// 1. 使用字节数组来初始化 bool initWithVertexShaderByteArray(const GLchar* vShaderByteArray,const GLchar* fShaderByteArray); // 2. 使用两个着色器文件名字来初始化 bool initWithVertexShaderFilename(const char* vShaderFilename,const char* fShaderFilename); // 3. 在不支持即时编译的平台上,使用预编译的shader字节数组来初始化 #if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) || (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) /** Initializes the CCGLProgram with precompiled shader program */ bool initWithPrecompiledProgramByteArray(const GLchar* vShaderByteArray,const GLchar* fShaderByteArray); #endif我还没搞过预编译的shader,主要来说说前两种初始化方法的实现:
@H_301_36@// 使用文件名的初始化方式内部调用了第一种初始化方式。 bool CCGLProgram::initWithVertexShaderFilename(const char* vShaderFilename,const char* fShaderFilename) { const GLchar * vertexSource = (GLchar*) CCString::createWithContentsOfFile(CCFileUtils::sharedFileUtils()->fullPathForFilename(vShaderFilename).c_str())->getCString(); const GLchar * fragmentSource = (GLchar*) CCString::createWithContentsOfFile(CCFileUtils::sharedFileUtils()->fullPathForFilename(fShaderFilename).c_str())->getCString(); return initWithVertexShaderByteArray(vertexSource,fragmentSource); } bool CCGLProgram::initWithVertexShaderByteArray(const GLchar* vShaderByteArray,const GLchar* fShaderByteArray) { // 在不支持即时编译的平台使用initWithPrecompiledProgramByteArray方法。 #if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) || (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) GLboolean hasCompiler = false; glGetBooleanv(GL_SHADER_COMPILER,&hasCompiler); m_hasShaderCompiler = (hasCompiler == GL_TRUE); if(!m_hasShaderCompiler) { return initWithPrecompiledProgramByteArray(vShaderByteArray,fShaderByteArray); } #endif // 创建program m_uProgram = glCreateProgram(); CHECK_GL_ERROR_DEBUG(); m_uVertShader = m_uFragShader = 0; if (vShaderByteArray) { // 创建并编译顶点着色器 if (!compileShader(&m_uVertShader,GL_VERTEX_SHADER,vShaderByteArray)) { CCLOG("cocos2d: ERROR: Failed to compile vertex shader"); return false; } } // Create and compile fragment shader if (fShaderByteArray) { // 创建并编译片段着色器 if (!compileShader(&m_uFragShader,GL_FRAGMENT_SHADER,fShaderByteArray)) { CCLOG("cocos2d: ERROR: Failed to compile fragment shader"); return false; } } if (m_uVertShader) { // 顶点着色器关联到program glAttachShader(m_uProgram,m_uVertShader); } CHECK_GL_ERROR_DEBUG(); if (m_uFragShader) { // 片段着色器关联到program glAttachShader(m_uProgram,m_uFragShader); } m_pHashForUniforms = NULL; CHECK_GL_ERROR_DEBUG(); #if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) m_shaderId = CCPrecompiledShaders::sharedPrecompiledShaders()->addShaders(vShaderByteArray,fShaderByteArray); #endif return true; }辅助类方法
1. 着色器的编译
@H_301_36@bool CCGLProgram::compileShader(GLuint * shader,GLenum type,const GLchar* source) { GLint status; if (!source) { return false; } // 为着色器添加全局的uniform常量 const GLchar *sources[] = { #if (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 mediump float;\n"),#endif "uniform mat4 CC_PMatrix;\n" "uniform mat4 CC_MVMatrix;\n" "uniform mat4 CC_MVPMatrix;\n" "uniform vec4 CC_Time;\n" "uniform vec4 CC_SinTime;\n" "uniform vec4 CC_CosTime;\n" "uniform vec4 CC_Random01;\n" "//CC INCLUDES END\n\n",source,}; // 创建shader,上传源码,编译 *shader = glCreateShader(type); glShaderSource(*shader,sizeof(sources)/sizeof(*sources),sources,NULL); 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,NULL,src); CCLOG("cocos2d: ERROR: Failed to compile shader:\n%s",src); if (type == GL_VERTEX_SHADER) { CCLOG("cocos2d: %s",vertexShaderLog()); } else { CCLOG("cocos2d: %s",fragmentShaderLog()); } free(src); #if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) return false; #else abort(); #endif } return (status == GL_TRUE); }2. 链接程序
@H_301_36@bool CCGLProgram::link() { CCAssert(m_uProgram != 0,"Cannot link invalid program"); #if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) || (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) if(!m_hasShaderCompiler) { // precompiled shader program is already linked return true; } #endif GLint status = GL_TRUE; // 链接 glLinkProgram(m_uProgram); // 删除掉两个着色器程序, if (m_uVertShader) { glDeleteShader(m_uVertShader); } if (m_uFragShader) { glDeleteShader(m_uFragShader); } // 这里对应了CCGLProgram的析构函数里的两个assert,在析构CCGLProgram的时候,不需要删除着色器程序了 m_uVertShader = m_uFragShader = 0; #if DEBUG || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) || (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) glGetProgramiv(m_uProgram,GL_LINK_STATUS,&status); // 如果链接失败,删除program if (status == GL_FALSE) { CCLOG("cocos2d: ERROR: Failed to link program: %i",m_uProgram); ccGLDeleteProgram(m_uProgram); m_uProgram = 0; } #endif #if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) if (status == GL_TRUE) { CCPrecompiledShaders::sharedPrecompiledShaders()->addProgram(m_uProgram,m_shaderId); } #endif return (status == GL_TRUE); }3. use.
在进行绘制之前进行调用
@H_301_36@void CCGLProgram::use() { ccGLUseProgram(m_uProgram); }4. uniform缓存检查,返回false代表不要更新uniform的值
@H_301_36@bool CCGLProgram::updateUniformLocation(GLint location,GLvoid* data,unsigned int bytes) { if (location < 0) { return false; } bool updated = true; tHashUniformEntry *element = NULL; HASH_FIND_INT(m_pHashForUniforms,&location,element); if (! element) { element = (tHashUniformEntry*)malloc( sizeof(*element) ); // key element->location = location; // value element->value = malloc( bytes ); memcpy(element->value,data,bytes ); HASH_ADD_INT(m_pHashForUniforms,location,element); } else { if (memcmp(element->value,bytes) == 0) { updated = false; } else { memcpy(element->value,bytes); } } return updated; }5. log函数
@H_301_36@// 顶点着色器的log const char* CCGLProgram::vertexShaderLog() { return this->logForOpenGLObject(m_uVertShader,(GLInfoFunction)&glGetShaderiv,(GLLogFunction)&glGetShaderInfoLog); } // 片段着色器的log const char* CCGLProgram::fragmentShaderLog() { return this->logForOpenGLObject(m_uFragShader,(GLLogFunction)&glGetShaderInfoLog); } // program的log const char* CCGLProgram::programLog() { return this->logForOpenGLObject(m_uProgram,(GLInfoFunction)&glGetProgramiv,(GLLogFunction)&glGetProgramInfoLog); }绘制过程相关
1. 预定义的8个uniform的赋值
@H_301_36@void CCGLProgram::setUniformsForBuiltins() { kmMat4 matrixP; kmMat4 matrixMV; kmMat4 matrixMVP; kmGLGetMatrix(KM_GL_PROJECTION,&matrixP); kmGLGetMatrix(KM_GL_MODELVIEW,&matrixMV); kmMat4Multiply(&matrixMVP,&matrixP,&matrixMV); // 给投影矩阵传值 setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformPMatrix],matrixP.mat,1); setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVMatrix],matrixMV.mat,1); setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVPMatrix],matrixMVP.mat,1); // 如果使用time,给time uniform传值 if(m_bUsesTime) { CCDirector *director = CCDirector::sharedDirector(); // This doesn't give the most accurate global time value. // Cocos2D doesn't store a high precision time value,so this will have to do. // Getting Mach time per frame per shader using time could be extremely expensive. float time = director->getTotalFrames() * director->getAnimationInterval(); setUniformLocationWith4f(m_uUniforms[kCCUniformTime],time/10.0,time,time*2,time*4); setUniformLocationWith4f(m_uUniforms[kCCUniformSinTime],time/8.0,time/4.0,time/2.0,sinf(time)); setUniformLocationWith4f(m_uUniforms[kCCUniformCosTime],cosf(time)); } // 随机数 if (m_uUniforms[kCCUniformRandom01] != -1) { setUniformLocationWith4f(m_uUniforms[kCCUniformRandom01],CCRANDOM_0_1(),CCRANDOM_0_1()); } }2. 更新uniform的位置。在创建完program,并且在使用其进行绘制之前,调用此函数来绑定shader中预定义的8个uniform常量
@H_301_36@void CCGLProgram::updateUniforms() { m_uUniforms[kCCUniformPMatrix] = glGetUniformLocation(m_uProgram,kCCUniformPMatrix_s); m_uUniforms[kCCUniformMVMatrix] = glGetUniformLocation(m_uProgram,kCCUniformMVMatrix_s); m_uUniforms[kCCUniformMVPMatrix] = glGetUniformLocation(m_uProgram,kCCUniformMVPMatrix_s); m_uUniforms[kCCUniformTime] = glGetUniformLocation(m_uProgram,kCCUniformTime_s); m_uUniforms[kCCUniformSinTime] = glGetUniformLocation(m_uProgram,kCCUniformSinTime_s); m_uUniforms[kCCUniformCosTime] = glGetUniformLocation(m_uProgram,kCCUniformCosTime_s); m_bUsesTime = ( m_uUniforms[kCCUniformTime] != -1 || m_uUniforms[kCCUniformSinTime] != -1 || m_uUniforms[kCCUniformCosTime] != -1 ); m_uUniforms[kCCUniformRandom01] = glGetUniformLocation(m_uProgram,kCCUniformRandom01_s); m_uUniforms[kCCUniformSampler] = glGetUniformLocation(m_uProgram,kCCUniformSampler_s); this->use(); // Since sample most probably won't change,set it to 0 now. this->setUniformLocationWith1i(m_uUniforms[kCCUniformSampler],0); }3. 绑定属性变量
@H_301_36@void CCGLProgram::addAttribute(const char* attributeName,GLuint index) { glBindAttribLocation(m_uProgram,index,attributeName); }4. 上传uniform值
这类函数都是以 setUniformLocationWith开头,先检查uniform缓存,再决定是否需要上传新的uniform值。
@H_301_36@void CCGLProgram::setUniformLocationWith1f(GLint location,GLfloat f1) { bool updated = updateUniformLocation(location,&f1,sizeof(f1)*1); if( updated ) { glUniform1f( (GLint)location,f1); } } void CCGLProgram::setUniformLocationWith2f(GLint location,GLfloat f1,GLfloat f2) { GLfloat floats[2] = {f1,f2}; bool updated = updateUniformLocation(location,floats,sizeof(floats)); if( updated ) { glUniform2f( (GLint)location,f1,f2); } } void CCGLProgram::setUniformLocationWith3f(GLint location,GLfloat f2,GLfloat f3) { GLfloat floats[3] = {f1,f2,f3}; bool updated = updateUniformLocation(location,sizeof(floats)); if( updated ) { glUniform3f( (GLint)location,f3); } } // ...... 还有很多这种函数,不再列举。CCGLProgram使用实例
在cocos引擎自身中,有一个类对CCGLProgram用的最多,它就是:CCShaderCache
从名字就能知道,这个类专门就是用来缓存shader的,这个shader就是CCGLProgram类型的对象实例。
在第一次获取CCShaderCache这个单例类的示例的时候,会调用下面这个方法:loadDefaultShader(),它会往字典里存放一些预定以的shader program。
@H_301_36@void CCShaderCache::loadDefaultShaders() { // Position Texture Color shader CCGLProgram *p = new CCGLProgram(); // 创建一个CCGLProgram对象 loadDefaultShader(p,kCCShaderType_PositionTextureColor); // 初始化program m_pPrograms->setObject(p,kCCShader_PositionTextureColor); // 放入字典 p->release(); // 仅在字典里维持一个引用计数 // 下面省略了其他的默认shader program的创建过程 // Position Texture Color alpha test // // Position,Color shader // // // Position Texture shader // // // Position,Texture attribs,1 Color as uniform shader // // // Position Texture A8 Color shader // // // Position and 1 color passed as a uniform (to simulate glColor4ub ) // // // Position,Legth(TexCoords,Color (used by Draw Node basically ) // // // ControlSwitch // } // 下面是初始化CCGLProgram的过程 void CCShaderCache::loadDefaultShader(CCGLProgram *p,int type) { switch (type) { case kCCShaderType_PositionTextureColor: // 使用自带的shader源代码来初始化program. p->initWithVertexShaderByteArray(ccPositionTextureColor_vert,ccPositionTextureColor_frag); // 绑定三个顶点属性,三个顶点属性分别对应顶点着色器中的三个attribute /* attribute vec4 a_position; attribute vec2 a_texCoord; attribute vec4 a_color; #ifdef GL_ES varying lowp vec4 v_fragmentColor; varying mediump vec2 v_texCoord; #else varying vec4 v_fragmentColor; varying vec2 v_texCoord; #endif void main() { gl_Position = CC_MVPMatrix * a_position; v_fragmentColor = a_color; v_texCoord = a_texCoord; } */ p->addAttribute(kCCAttributeNamePosition,kCCVertexAttrib_Position); p->addAttribute(kCCAttributeNameColor,kCCVertexAttrib_Color); p->addAttribute(kCCAttributeNameTexCoord,kCCVertexAttrib_TexCoords); break; // 省略了其他默认shader的初始化过程 case kCCShaderType_PositionTextureColorAlphaTest: case kCCShaderType_PositionColor: case kCCShaderType_PositionTexture: case kCCShaderType_PositionTexture_uColor: case kCCShaderType_PositionTextureA8Color: case kCCShaderType_Position_uColor: case kCCShaderType_PositionLengthTexureColor: case kCCShaderType_ControlSwitch: default: CCLOG("cocos2d: %s:%d,error shader type",__FUNCTION__,__LINE__); return; } // 链接program p->link(); // 绑定shader中预定义的8个uniform p->updateUniforms(); CHECK_GL_ERROR_DEBUG(); }经过:initWithVertexShaderByteArray,link,updateUniforms这三个步骤,CCGLProgram就已经进入就绪状态,可以被用来进行绘制了。
下面来看CCGLProgram在绘制时候的使用,以CCSprite的绘制为例,因为它使用了刚才CCShaderCache中的第一个default shader: kCCShader_PositionTextureColor.
@H_301_36@void CCSprite::draw(void) { // 省略掉了无关代码...... // 涉及到CCGLProgram的使用的就这一句,这个宏的定义在下面 CC_NODE_DRAW_SETUP(); glDrawArrays(GL_TRIANGLE_STRIP,0,4); // 省略掉了无关代码...... CC_INCREMENT_GL_DRAWS(1); } // defined in ccMacros.h #define CC_NODE_DRAW_SETUP() \ do { \ ccGLEnable(m_eGLServerState); \ CCAssert(getShaderProgram(),"No shader program set for this node"); \ { \ getShaderProgram()->use(); \ getShaderProgram()->setUniformsForBuiltins(); \ } \ } while(0)可以看到,在绘制的时候,调用了CCGLProgram的use()和setUniformsForBuiltins()这两个方法。
因此,对CCGLProgram的使用过程可以总结如下:
创建阶段
1. new CCGLProgram()
2. initWithVertexShaderByteArray 或者 initWithVertexShaderFilename 来完成顶点着色器和片段着色器的定义和编译
3. [可选]:如果有的话,绑定自定义的attribute
4. link() 链接program
5. updateUniforms 绑定预定义的8个uniform
6. [可选]:如果有的话,绑定自定义的uniform
绘制使用阶段
7. setShaderProgram(program) 设置为活动的program
开始绘制
8. use() 使用program
9. setUniformsForBuiltins() 设置预定义的8个uniform的值 8,9两步可以用CC_NODE_DRAW_SETUP
这个宏来完成。
10. 其它的对shader中attribute和uniform的操作,这里已经跟CCGLProgram关系不大。
作者水平有限,对相关知识的理解和总结难免有错误,还望给予指正,非常感谢!