一、着色器基础
要掌握着色器编程,就是要掌握OpenGL基础(OpengGL渲染管线)。相关知识在OpenGL部分。
OpenGL的工作就是将3D模型(很多的顶点,每个顶点是一个3D坐标)显示到2D屏幕上。整个这个流程统称为OpenGL渲染管线(render pipline),这个管线分成了多个阶段,每个阶段完成一个特定的工作,每个阶段都可以由GPU并行完成:
- 1、创建3D顶点数据
- 简单的如3个顶点构成的一个三角形、4个顶点构成两个三角形然后组合成一个矩形(Sprite)
- 复杂的如加载一个3D模型,那将有数以千计的顶点数据(网格数据,mesh),
- 2、顶点着色器
- 上面的顶点数据都传入顶点着色器中进行处理,主要是进行MVP转换(model模型,view视图,projection投影),模型转换主要完成顶点的仿射变换(旋转、缩放、平移),我们在cocos中setPosition、setScale、MoveTo、MoveBy就是在这里完成。视图变换主要是设置观察这些顶点的视角,屏幕上显示的就是这个视角,投影变换主要完成3D到2D的投影工作,非常复杂。
- 3、细分着色器(Optional)
- 可以将模型细化,可能会产生新的顶点数据。
- 4、几何着色器(Optional)
- 将前面的顶点数据,按图片为单位,一批一批传入,也就是说几何着色器处理的是一个个的图元,可以改变图元(2个顶点构成的线段,新增一个顶点构成一个三角形图元)、抛弃图元。
- 5、……,这里还有一些七七八八的步骤,比如在屏幕外面的图元抛弃、看不到的图元抛弃
- 6、光栅化
- 假设一个虚拟的屏幕,只显示这些顶点数据构成的模型,光栅化就是生成显示这些模型的像素,准确的讲应该叫片段(Fragment),没必要真的生成,一个片段就包含了一个像素的所有信息。注意这个像素是仅仅显示这些模型的,还要和实际屏幕上的像素进行混合,才是最终的颜色。
- 7、片段着色器
- 处理前面生成的一个一个的片段,最终要输出对应这个像素的颜色。简单的比如给每个像素设置为白色,那像素就全是白色。
- 8、七七八八的测试
- 决定要不要这个片段,比如Scissor Test,模板测试,深度测试
- 9、混合Blending
- 立即艰辛最终抵达这里的片段正式和当前屏幕上对应位置的像素进行颜色混合。然后更新到屏幕上。
着色器程序是一种运行在GPU上的程序,由GLSL(GL Shading Language)编写,GLSL是在C基础上建立的编程语言,语法基本类似,与C代码兼容的(只要符合双方的语法)。
二、OpenGL程序
cocos2dx借助OpenGL图形库来完成界面显示工作。所以cocos2dx程序本质上就是一个OpenGL程序。
语雀内容
三、OpenGL Shader
OpenGL中如何使用着色器。
语雀内容
四、Cocos Shader UML
1、GLProgram
2、GLProgramCache
3、GLProgramState
4、GLProgramStateCache
五、Shader实践
1、Sprite的Shader
我们来看一下Sprite中是如何使用Shader的。
// ***********************************************************
// ********** CCNode.cpp 中
// ***********************************************************
// 每一个Node都有一个ProgramState
class CC_DLL Node : public Ref
{
protected:
// 在Node节点中保存了一个ProgramState,可以这样认为,有了ProgramState就是有了Program
// 因为一个ProgramState是肯定对应一个program的,不能独立存在。
GLProgramState *_glProgramState;
GLProgram* getGLProgram() const;
GLProgramState *getGLProgramState() const;
// 设置渲染这个节点所使用的shader
virtual void setGLProgram(GLProgram *glprogram); // 其实就是set _glProgramState
virtual void setGLProgramState(GLProgramState *glProgramState);
}
// ***********************************************************
// ********** CCSprite.cpp 中
// ***********************************************************
// 在创建Sprite时,设置好了Shader
void Sprite::setTexture(Texture2D *texture)
{
if(_glProgramState == nullptr)
{
// 使用的shader:SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP
// 对应的顶点着色器:"renderer/ccShader_PositionTextureColor_noMVP.frag"
// 对应的片段着色器:"renderer/ccShader_PositionTextureColor_noMVP.vert"
// 将在下面展示着色器源代码。
setGLProgramState(
GLProgramState::getOrCreateWithGLProgramName(
GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP, texture));
}
......
}
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) {
......
#endif
{
_trianglesCommand.init(_globalZOrder,
_texture,
getGLProgramState(), // 传入使用的GLProgram及ProgramState
_blendFunc,
_polyInfo.triangles,
transform,
flags);
renderer->addCommand(&_trianglesCommand);
......
}
}
着色器源代码如下;
// ***************************************************
// *********** Sprite 的 顶 点 着 色 器
// ***************************************************
// "renderer/ccShader_PositionTextureColor_noMVP.frag"
uniform mat4 CC_PMatrix; // 投影矩阵,没有使用MVP的原因是,顶点坐标已经转换成了世界坐标。
attribute vec4 a_position; // location = 0
attribute vec4 a_color; // location = 1
attribute vec2 a_texCoord; // location = 2
#ifdef GL_ES
varying lowp vec4 v_fragmentColor;
varying mediump vec2 v_texCoord;
#else // 假设平台是win32
varying vec4 v_fragmentColor; // 将顶点数据中的颜色数据传递给片段着色器进行混合
varying vec2 v_texCoord; // 将顶点数据中的纹理坐标传递给片段着色器进行采样
#endif
void main()
{
gl_Position = CC_PMatrix * a_position; // 得到裁剪坐标
v_fragmentColor = a_color; // 顶点颜色
v_texCoord = a_texCoord; // 该顶点对应的纹理坐标
}
// ***************************************************
// *********** Sprite 的 片 段 着 色 器
// ***************************************************
// "renderer/ccShader_PositionTextureColor_noMVP.vert"
uniform sampler2D CC_Texture0; // 纹理的采样器,对应纹理单元GL_TEXTURE0
#ifdef GL_ES
precision lowp float;
#endif
varying vec4 v_fragmentColor; // 顶点颜色
varying vec2 v_texCoord; // 顶点纹理坐标
void main()
{
// 片段的纹理插值颜色: texture2D(CC_Texture0, v_texCoord);
// 从程序中动态传入的颜色:v_fragmentColor
// 乘法就是将这两者颜色混合,得到这个片段的最终颜色。
gl_FragColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
}
2、ETC1 Alpha support
ETC1压缩纹理是不支持alpha通道的。我们可以借助工具制作一张ETC1压缩纹理,和一张alpha通道的压缩纹理。然后通过片段着色器将这两张纹理混合,就可以了。
auto etc1_file = "Images/etc1-alpha.pkm"; // etc1压缩纹理
// 对应alpha纹理路径:"Images/etc1-alpha.pkm@alpha",在TextureCache::addImage中已经
// 加了etc1的alpha support逻辑,如果Image是etc格式,则还会试着加载@alpha结尾的文件作为alpha texture
auto sprite = Sprite::create(etc1_file);
addChild(sprite);
// cocos已经内置etc1 alpha support的Shader了,
// 初始就加载了这个Shader。这个Shader的顶点着色器和上面一样,不同的就是片段着色器,源代码在下面列出。
auto glProgram = GLProgramCache::getInstance()->getProgram(GLProgram::SHADER_NAME_ETC1AS_POSITION_TEXTURE_COLOR_NO_MVP);
sprite->setGLProgram(glProgram);
// ***************************************************************
// ************* ETC1 Alpha support 片 段 着 色 器
// ***************************************************************
// "renderer/ccShader_PositionTextureColor_noMVP.vert"
uniform sampler2D CC_Texture0; // etc1 texture,对应纹理单元GL_TEXTURE0
uniform sampler2D CC_Texture1; // alpha texture,对应纹理单元GL_TEXTURE1
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
void main() {
vec4 texColor = vec4(
texture2D(CC_Texture0, v_texCoord).rgb, // etc1 texture的rgb
texture2D(CC_Texture1, v_texCoord).r); // alpha texture的a
// 使用了alpha预乘减少混合阶段的计算量。
// 那Sprite的混合模式要设置成BlendFunc::ALPHA_PREMULTIPLIED来配合。
// 索性cocos已经帮我们完成了。见下面Sprite源代码。
texColor.rgb *= texColor.a;
gl_FragColor = v_fragmentColor * texColor;
}
// ***************************************************************
// ************* Sprite.cpp 部分源代码
// ***************************************************************
// 在将绘制指令发送只绘制队列之前,肯定会执行这个,更新一下到底是用哪种blend模式
void Sprite::updateBlendFunc(void) {
CCASSERT(_renderMode != RenderMode::QUAD_BATCHNODE, "CCSprite: updateBlendFunc doesn't work when the sprite is rendered using a SpriteBatchNode");
// it is possible to have an untextured sprite
if (! _texture || ! _texture->hasPremultipliedAlpha()) // 纹理如何没有开启alpha预乘
{
// 设置成不开启,实际blend算法是这样的:{GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA};
_blendFunc = BlendFunc::ALPHA_NON_PREMULTIPLIED;
setOpacityModifyRGB(false);
}
else
{
//开启alpha预乘,blend模式是这样的: {GL_ONE, GL_ONE_MINUS_SRC_ALPHA};
_blendFunc = BlendFunc::ALPHA_PREMULTIPLIED;
setOpacityModifyRGB(true);
}
}
bool Texture2D::initWithImage(Image *image, PixelFormat format) {
......
else if (image->isCompressed()) {
......
// 在Image内部已经设计好了,所加载的image file是否会支持alpha预乘
_hasPremultipliedAlpha = image->hasPremultipliedAlpha();
return true;
}
}