做了两年的cocos引擎底3D层优化和特效,以及对应的编辑器开发。原本的计划中,参考Unity3D引擎,优化cocos渲染流程,实现批次渲染,BRDF渲染,卡通渲染,多材质渲染,延迟渲染,模型阴影,以及HDR,次表面散射,中国风特效,砖石特效等,并制作对应的编辑器。而今只实现了底层优化,批次渲染,BRDF渲染,卡通渲染,多材质渲以及对应编辑器制作。自感部门危险,这几天整理下做过的工作,记录下来,也算对自己工作一个回顾总结。
windows系统中,首先进入main()函数:
// create the application instance
AppDelegate app;
int ret = Application::getInstance()->run();
然后,进入run函数:
//初始引擎环境,包括GLView创建(最终实现根据不同的平台进入不同实现),目录设置,LUA绑定C++实现
(cocos自身实现和项目加入的三方SDK实现)
if (!applicationDidFinishLaunching())
return 1;
。。。。。。。
//渲染主循环,根据设置的FPS,while函数每隔一定时间执行一次,执行内容包括项目设置的各种定时器
(scheduler,update函数),以及最重要的渲染刷新。
while(!glview->windowShouldClose())
{
QueryPerformanceCounter(&nNow);
if (nNow.QuadPart - nLast.QuadPart > _animationInterval.QuadPart)
director->mainLoop();
}
android环境中,Cocos2dxRenderer继承自GLSurfaceView.Renderer,andriod启动后主循环进入Render的OnDrawFrame(此为android开发的知识):
public void onDrawFrame(final GL10 gl) {
//类似winodws的while主循环函数,每隔一定时间进行调用一次nativeRender
if (sAnimationInterval <= 1.0 / 60 * Cocos2dxRenderer.NANOSECONDSPERSECOND) {
Cocos2dxRenderer.nativeRender();
} else {
final long now = System.nanoTime();
final long interval = now - this.mLastTickInNanoSeconds;
if (interval < Cocos2dxRenderer.sAnimationInterval) {
try {
Thread.sleep();
} catch (final Exception e) {
}
}
this.mLastTickInNanoSeconds = System.nanoTime();
Cocos2dxRenderer.nativeRender();
}
}
nativeRender是一个静态函数,在Java_org_cocos2dx_lib_Cocos2dxRenderer中实现,通过JNI转换后调用C++也调用mainLoop函数。
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {
cocos2d::Director::getInstance()->mainLoop();
}
mainLoop中执行drawScene功能:
//此处的update函数是各种update,action,动画,定时器的入口,如果项目的网络接口部分也是在cocos的
update函数实现,此时也是网络接收的入口,这就是为什么进入后台后网络阻塞,进入前台后一下接收到很多
消息的原因,类似捕鱼游戏,如果不想做恢复可以让程序在进入后台后,仍然执行update。
if (! _paused)
_scheduler->update(_deltaTime);
//压入模型矩阵
pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
if (_runningScene)
{
//此处为遍历场景的所有节点的数据以及遍历renderQue进行渲染,包括vist,draw,render
_openGLView->renderScene(_runningScene, _renderer);
}
进入遍历函数renderScene,此函数调用scene->render(renderer, Mat4::IDENTITY, nullptr)进入下面,其中renderer为Director传递进来的参数,由Director维护,整个游戏过程和Director一样,只有一个渲染器render:
for (const auto& camera : getCameras())
{
//遍历每个相机下的模型
Camera::_visitingCamera = camera;
//设置相机投影矩阵
if (eyeProjection)
camera->setAdditionalProjection(*eyeProjection * camera->getProjectionMatrix().getInversed());
camera->setAdditionalTransform(eyeTransform.getInversed());
//压入和加载投影矩阵,后面专门开一张将cocos的矩阵流程
director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION, Camera::_visitingCamera->getViewProjectionMatrix());
//设置当前相机的FBO和视口
camera->apply();
camera->clearBackground();
//遍历节点生成meshCommand加入渲染队列
visit(renderer, transform, 0);
//渲染
renderer->render();
}
visit是个虚函数,所有的节点类都是基于Node的子类,最终遍历会进入具体类的vist中,主要优化的3D部分,这里以sprite3D为例子:
bool visibleByCamera = isVisitableByVisitingCamera();
//按正常的设计,这里应该为visibleByCamera&&!_children.empty(),
如果不被当前摄像机可见则不进行遍历,然而,cocos有个设计和这个理念是相悖的,如果一个3D物体加入
一个2D的attchnode节点的粒子动画,那么这个粒子就不会被2D摄像机看到,因为父亲节点被2D摄像机给
排除了,所以明知是个无用的遍历,还是要遍历下
if(!_children.empty())
{
//根据_localZOrder的顺序进行孩子节点排序,cocos的Order排序有三种依据LocalZOrder、
GlobalZOrder、OrderOfArrival,优先级为GlobalZOrder>LocalZOrder>OrderOfArrival
sortAllChildren();
// 首先遍历_localZOrder<0的孩子节点
for( ; i < _children.size(); i++ )
{
auto node = _children.at(i);
if (node && node->getLocalZOrder() < 0)
node->visit(renderer, _modelViewTransform, flags);
else
break;
}
// 遍历自身的模型数据
if (visibleByCamera)
this->draw(renderer, _modelViewTransform, flags);
//最后遍历_localZOrder>0的孩子节点
for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
(*it)->visit(renderer, _modelViewTransform, flags);
}
else if (visibleByCamera)
{
this->draw(renderer, _modelViewTransform, flags);
}
Sprite3D的draw函数如下:
Color4F color(getDisplayedColor());
color.a = getDisplayedOpacity() / 255.0f;//此处应加一个判断,当透明度为零时,不进行绘制
for (auto mesh: _meshes)
{
//面片生成meshCommd
mesh->draw(renderer,
.......);
}
Mesh::draw的详情如下:
void Mesh::draw()
{
_meshCommand.init(globalZ,
_material,
getVertexBuffer(),//创建顶点缓冲区
getIndexBuffer(),//创建索引缓存区
getPrimitiveType(),//获得绘制类别,线,多边形,三角形
getIndexFormat(),//索引格式
getIndexCount(),//索引数
transform,//模型矩阵
flags);//3d,透明等的标志
_material->getStateBlock()->setDepthWrite(true);//cocos的深度设置有问题,透明时不应该开
启深度写入功能,透明物体的渲染顺序需要在c3t中手动调整,cocos的bug
_material->getStateBlock()->setBlend(_force2DQueue || isTransparent);//设置透明混合
//shader中传入Uniform变量,cocos变量写死,如果加入新特效需要加入新的传入参数,
const auto scene = Director::getInstance()->getRunningScene();
auto technique = _material->_currentTechnique;
for(const auto pass : technique->_passes)
{
auto programState = pass->getGLProgramState();
programState->setUniformVec4("u_color", color);
//传入光照的uniform
if (scene && scene->getLights().size() > 0)
setLightUniforms(pass, scene, color, lightMask);
}
//加入_meshCommand到渲染队列中
renderer->addCommand(&_meshCommand);
}
addCommand的函数如下:
void Renderer::addCommand(RenderCommand* command)
{
int renderQueue =_commandGroupStack.top();
addCommand(command, renderQueue);
}
addCommnd函数如下:
void Renderer::addCommand(RenderCommand* command, int renderQueue)
{
_renderGroups[renderQueue].push_back(command);
}
push_back函数如下:
void RenderQueue::push_back(RenderCommand* command)
{
float z = command->getGlobalOrder();
if(z < 0)
_commands[QUEUE_GROUP::GLOBALZ_NEG].push_back(command);
else if(z > 0)
_commands[QUEUE_GROUP::GLOBALZ_POS].push_back(command);
else
{
if(command->is3D())
{
if(command->isTransparent())
_commands[QUEUE_GROUP::TRANSPARENT_3D].push_back(command);
else
_commands[QUEUE_GROUP::OPAQUE_3D].push_back(command);
}
else
_commands[QUEUE_GROUP::GLOBALZ_ZERO].push_back(command);
}
}
RenderQueue在加入新commd时,会根据command的global和透明属性将command分到一下五个类别中
enum QUEUE_GROUP
{
GLOBALZ_NEG = 0,//globalZorder<0最先绘制
OPAQUE_3D = 1,//globalZorder=0,但是属于3D不透明物体
TRANSPARENT_3D = 2,//globalZorder=0,但是属于3D透明物体
GLOBALZ_ZERO = 3,//globalZorder=0,2D物体
GLOBALZ_POS = 4,globalZorder>0最后绘制
};
renderScene的 visit函数执行完后,进入renderer->render()中:
void Renderer::render()
{
for (auto &renderqueue : _renderGroups)
//首先排序渲染队列
renderqueue.sort();
visitRenderQueue(_renderGroups[0]);
}
void RenderQueue::sort()
{
//不同类别调用不用的排序算法
std::sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]), std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]), compare3DCommand);
std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_NEG]), std::end(_commands[QUEUE_GROUP::GLOBALZ_NEG]), compareRenderCommand);
std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_POS]), std::end(_commands[QUEUE_GROUP::GLOBALZ_POS]), compareRenderCommand);
}
排序渲染队列后,进入遍历渲染队列的函数:
void Renderer::visitRenderQueue(RenderQueue& queue)
{
//处理globaZorder<0队列
const auto& zNegQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_NEG);
if (zNegQueue.size() > 0)
{
。。。。。。//渲染属性设置
for (auto it = zNegQueue.cbegin(); it != zNegQueue.cend(); ++it)
processRenderCommand(*it);//进入渲染
flush();
}
//处理3D不透明队列
const auto& opaqueQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::OPAQUE_3D);
if (opaqueQueue.size() > 0)
{
。。。。。。
}
//处理3D透明队列
const auto& transQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::TRANSPARENT_3D);
if (transQueue.size() > 0)
{
。。。。。。
}
//处理globaLZorder等于0的非3D队列
const auto& zZeroQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_ZERO);
if (zZeroQueue.size() > 0)
{
。。。。。
}
//处理Global-Z > 0 队列
const auto& zPosQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_POS);
if (zPosQueue.size() > 0)
{
。。。。。
}
//恢复到默认状态,包括深度写入,背面或者前面渲染,深度测试
queue.restoreRenderState();
}
processRenderCommand出来渲染命令如下:
void Renderer::processRenderCommand(RenderCommand* command)
{
//根据渲染类型调用不同的渲染指令,指令类型如下
enum class Type
{
UNKNOWN_COMMAND,
QUAD_COMMAND,//多边形渲染指令
/**自定义渲染,可用于加入自定制的渲染命令,可用于自定义3D批次渲染.*/
CUSTOM_COMMAND,
/**批次渲染使用图集纹理中的节点*/
BATCH_COMMAND,
/**指令集合,其他指令的集合,需要遍历其子节点指令*/
GROUP_COMMAND,
/**网格渲染指令,以三角面为绘制单元的绘制指令*/
MESH_COMMAND,
/**原生指令绘制点,线,多边形.*/
PRIMITIVE_COMMAND,
/**Triangles command, used to draw triangles.*/
TRIANGLES_COMMAND
};
auto commandType = command->getType();
if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
{
auto cmd = static_cast<TrianglesCommand*>(command);
drawBatchedTriangles();
_queuedTriangleCommands.push_back(cmd);
_filledIndex += cmd->getIndexCount();
_filledVertex += cmd->getVertexCount();
}
else if()
........
}
下面网格渲染指令为例,如下代码:
void MeshCommand::execute()
{
if (_material)
{
// 创建缓存区内容已经在mesh::Draw中完成
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);//绑定顶点缓冲区
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);//绑定索引缓冲区
{
//设置渲染面
_material->getStateBlock()->setCullFaceSide(pass->getCullFaceSide());
// 传递模型属性(设置顶点属性,Uniform变量,各种矩阵变量)
_glProgramState->apply(_mv);
// 绘制
glDrawElements(_primitive, (GLsizei)_indexCount, _indexFormat, 0);
// 统计绘制的顶点数
CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, _indexCount);
}
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
} else
{
_glProgramState->apply(_mv);
//纹理传入
applyRenderState();
// 绘制
glDrawElements(_primitive, (GLsizei)_indexCount, _indexFormat, 0);
CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, _indexCount);
}
}
到此,cocos完成渲染一个渲染指令,后面指令遍历完成swapbuffer后显示到屏幕。