在cocos2dx 2.x中,如果我们要对sprite更换片段shader,写成:
myProgram->initWithByteArrays(ccPositionTextureColor_vert, myFragSource);
但是到3.x中,上面做法会导致显示出来的sprite坐标不对。
于是看3.x代码,发现在3.x中,Sprite的默认shader不再是kCCShader_PositionTextureColor(即
GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR),而是改成了GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP(见Sprite::initWithTexture(...)函数实现)。
因此应该用:
myProgram->initWithByteArrays(ccPositionTextureColor_noMVP_vert, myFragSource);
显示就正常了。
对比一下ccPositionTextureColor_vert和ccPositionTextureColor_noMVP_vert的代码:
const char* ccPositionTextureColor_vert = STRINGIFY(
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;
\n#ifdef GL_ES\n
varying lowp vec4 v_fragmentColor;
varying mediump vec2 v_texCoord;
\n#else\n
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
\n#endif\n
void main()
{
gl_Position = CC_MVPMatrix * a_position;
v_fragmentColor = a_color;
v_texCoord = a_texCoord;
}
);
const char* ccPositionTextureColor_noMVP_vert = STRINGIFY(
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;
\n#ifdef GL_ES\n
varying lowp vec4 v_fragmentColor;
varying mediump vec2 v_texCoord;
\n#else\n
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
\n#endif\n
void main()
{
gl_Position = CC_PMatrix * a_position;
v_fragmentColor = a_color;
v_texCoord = a_texCoord;
}
);
可见唯一区别就是顶点坐标a_position乘的矩阵不同,ccPositionTextureColor_vert中乘的是CC_MVPMatrix,ccPositionTextureColor_noMVP_vert中乘的是CC_PMatrix。
于是就明白了:
在2.x的立即模式中,传入shader的顶点坐标为局部坐标,所以需要在shader中乘以MVP矩阵来求屏幕坐标。
在3.x的command模式中,传入shader的顶点坐标已预先转化成了世界坐标(即已乘过MV矩阵),所以在shader中就只乘P矩阵即可。
于是引出两个问题:
问题1,为什么command模式下,sprite要预先转化成世界坐标?
因为如果所有顶点都转化到了相同的空间(比如世界空间),那么它们的变换矩阵M就同一了(可以作为一个uniform变量),于是这些顶点可以一次性提交,从而减少了draw call数量。这也正是command机制的目的所在。不过需要注意的是,3.x中也并非所有的节点都使用了noMVP的形式,因为并不是所有渲染对象都如sprite这样顶点数很少适合展平做batch -- 假设有几个MV矩阵不同的顶点数很多的mesh,如果也像处理sprite一样进行展平一次性提交,虽然draw call数量下来了,可是cpu做顶点变换的开销却会大幅增加,其结果很可能反而不如多用几个draw call,但把顶点变换转移到gpu(shader)中去做效率高。所以在对一个节点更换shader的时候,要注意看一下其默认shader是有MVP还是noMVP。
问题2,sprite预先转化成世界坐标的代码在哪?
在void Renderer::visitRenderQueue(const RenderQueue& queue)中的if ( RenderCommand::Type::QUAD_COMMAND == commandType )分支中有一句fillQuads(cmd)。fillQuads(...)的实现如下:
void Renderer::fillQuads(const QuadCommand *cmd)
{
memcpy(_quadVerts + _numberQuads * 4, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount());
const Mat4& modelView = cmd->getModelView();
for(ssize_t i=0; i< cmd->getQuadCount() * 4; ++i)
{
V3F_C4B_T2F *q = &_quadVerts[i + _numberQuads * 4];
Vec3 *vec1 = (Vec3*)&q->vertices;
modelView.transformPoint(vec1);
}
_numberQuads += cmd->getQuadCount();
}
其中的for循环就是在对quads顶点数组中的每个坐标乘以MV矩阵。