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 变量保存下来