在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矩陣。