笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。
任何一款3D引擎必须要有模型的加载处理,Unity3D引擎和UE4引擎使用的是FBX模型文件格式,而开源Ogre引擎使用的是扩展名为mesh的文件格式,另外,市面上其它各个引擎也都有自己的文件格式,在此就不一一列举了。Cocos2d-x引擎使用的是FBX文件的转换格式,转换格式会导致模型的一些信息丢失掉,特别是骨骼信息中的Morph骨骼,在这方面Cocos2d-x引擎并不支持,就不做过多讨论了,本节给读者讲述模型的加载流程,首先需将模型创建成Sprite3D精灵,便于在场景中直接挂接,在场景中创建精灵的调用函数接口如下:
Sprite3D *sprite = Sprite3D::create(_modelFile);
调用了Sprite3D类的接口create函数,下面再具体查看create函数做了哪些工作:
Sprite3D* Sprite3D::create(const std::string& modelPath)
{
CCASSERT(modelPath.length() >= 4, "invalid filename for Sprite3D");
auto sprite = new (std::nothrow) Sprite3D();
if (sprite && sprite->initWithFile(modelPath))
{
sprite->_contentSize = sprite->getBoundingBox().size;
sprite->autorelease();
return sprite;
}
CC_SAFE_DELETE(sprite);
return nullptr;
}
Sprite3D* Sprite3D::create(const std::string& modelPath, const std::string& texturePath)
{
auto sprite = create(modelPath);
if (sprite)
{
sprite->setTexture(texturePath);
}
return sprite;
}
Cocos2d-x引擎提供了两个接口函数,也是函数的重载。第一个函数create是逻辑编写经常使用的函数,它内部调用了接口initWithFile函数,通过函数名字知道它只是做了初始化工作。当然它也做了一下获取包围盒大小工作后面会详细介绍。再看第二个create函数,相比第一个create函数参数多了一个设置纹理路径的参数。在Cocos2d-x引擎中,默认情况下,加载模型可以直接加载材质,这是因为对于默认的材质会在模型文件中引用。另外Cocos2d-x引擎也提供了可自己手动更换材质的接口。在create函数中看不到任何加载模型的语句,接下来介绍调用的唯一接口函数initWithFile是如何实现的:
bool Sprite3D::initWithFile(const std::string& path)
{
_aabbDirty = true;
_meshes.clear();
_meshVertexDatas.clear();
CC_SAFE_RELEASE_NULL(_skeleton);
removeAllAttachNode();
if (loadFromCache(path))
return true;
//定义的模型数据信息
MeshDatas* meshdatas = new (std::nothrow) MeshDatas();
MaterialDatas* materialdatas = new (std::nothrow) MaterialDatas();
NodeDatas* nodeDatas = new (std::nothrow) NodeDatas();
//模型文件加载
if (loadFromFile(path, nodeDatas, meshdatas, materialdatas))
{
if (initFrom(*nodeDatas, *meshdatas, *materialdatas))
{
//增加到缓存里面
auto data = new (std::nothrow) Sprite3DCache::Sprite3DData();
data->materialdatas = materialdatas;
data->nodedatas = nodeDatas;
data->meshVertexDatas = _meshVertexDatas;
for (const auto mesh : _meshes) {
data->glProgramStates.pushBack(mesh->getGLProgramState());
}
Sprite3DCache::getInstance()->addSprite3DData(path, data);
CC_SAFE_DELETE(meshdatas);
_contentSize = getBoundingBox().size;
return true;
}
}
CC_SAFE_DELETE(meshdatas);
CC_SAFE_DELETE(materialdatas);
CC_SAFE_DELETE(nodeDatas);
return false;
}
在initWithFile函数中声明了几个用于存储模型点信息的指针:MeshDatas,MaterialDatas,NodeDatas,同时调用函数loadFromFile加载模型信息,loadFromFile函数的实现如下所示:
bool Sprite3D::loadFromFile(const std::string& path, NodeDatas* nodedatas, MeshDatas* meshdatas, MaterialDatas* materialdatas)
{
std::string fullPath = FileUtils::getInstance()->fullPathForFilename(path);
std::string ext = FileUtils::getInstance()->getFileExtension(path);
//加载obj模型文件
if (ext == ".obj")
{
return Bundle3D::loadObj(*meshdatas, *materialdatas, *nodedatas, fullPath);
}
else if (ext == ".c3b" || ext == ".c3t")
{
//从c3t和c3b文件中加载模型
auto bundle = Bundle3D::createBundle();
if (!bundle->load(fullPath))
{
Bundle3D::destroyBundle(bundle);
return false;
}
auto ret = bundle->loadMeshDatas(*meshdatas)
&& bundle->loadMaterials(*materialdatas) && bundle->loadNodes(*nodedatas);
Bundle3D::destroyBundle(bundle);
return ret;
}
return false;
}
在loadFromFile函数中,终于看到了加载模型的具体语句,抛开obj文件模型,在loadFromFile函数中还调用了Bundle3D的函数接口load,Bundle3D在前面章节中讲过,它主要是处理模型信息的类,接下来进入load函数内部看一下:
bool Bundle3D::load(const std::string& path)
{
if (path.empty())
return false;
if (_path == path)
return true;
getModelRelativePath(path);
boolret = false;
std::string ext = FileUtils::getInstance()->getFileExtension(path);
if (ext == ".c3t")
{
_isBinary = false;
//加载json文件
ret = loadJson(path);
}
else if (ext == ".c3b")
{
_isBinary = true;
//加载二进制文件
ret = loadBinary(path);
}
else
{
CCLOG("warning: %s is invalid file formate", path.c_str());
}
ret?(_path = path):(_path = "");
return ret;
}
load函数调用了加载模型的函数接口loadJson和loadBinary。终于快看到曙光了,模型它相对来说也是一种文本文件,研究一款引擎必须要知道它底层的处理方式。再继续深入下去看loadJson函数的实现内容如下:
bool Bundle3D::loadJson(const std::string& path)
{
clear();
_jsonBuffer = FileUtils::getInstance()->getStringFromFile(path);
if(_jsonReader.ParseInsitu<0>((char*)_jsonBuffer.c_str()).HasParseError())
{
clear();
CCLOG("Parse json failed in Bundle3D::loadJson function");
return false;
}
const rapidjson::Value& mash_data_array = _jsonReader[VERSION];
if (mash_data_array.IsArray()) // 适配于老板本
_version = "1.2";
else
_version = mash_data_array.GetString();
return true;
}
它是通过_jsonReader.ParseInsitu把json文件解释出来,这样真相终于大白了,接下来再看看c3b文件的读取,在游戏中这个格式是使用最多的,它的加载函数loadBinary如下所示:
bool Bundle3D::loadBinary(const std::string& path)
{
clear();
// 获取文件数据
_binaryBuffer.clear();
_binaryBuffer = FileUtils::getInstance()->getDataFromFile(path);
if (_binaryBuffer.isNull())
{
clear();
CCLOG("warning: Failed to read file: %s", path.c_str());
return false;
}
// 初始化bundle数据
_binaryReader.init( (char*)_binaryBuffer.getBytes(), _binaryBuffer.getSize() );
// 读取标示信息
char identifier[] = { 'C', '3', 'B', '\0'};
char sig[4];
if (_binaryReader.read(sig, 1, 4) != 4 || memcmp(sig, identifier, 4) != 0)
{
clear();
CCLOG("warning: Invalid identifier: %s", path.c_str());
return false;
}
// 读取版本号
unsigned char ver[2];
if (_binaryReader.read(ver, 1, 2)!= 2){
CCLOG("warning: Failed to read version:");
return false;
}
char version[20] = {0};
sprintf(version, "%d.%d", ver[0], ver[1]);
_version = version;
// 读取ref表大小
if (_binaryReader.read(&_referenceCount, 4, 1) != 1)
{
clear();
CCLOG("warning: Failed to read ref table size '%s'.", path.c_str());
return false;
}
// 读取所有的refs
CC_SAFE_DELETE_ARRAY(_references);
_references = new (std::nothrow) Reference[_referenceCount];
for (unsigned int i = 0; i <_referenceCount; ++i)
{
if ((_references[i].id = _binaryReader.readString()).empty()||_binaryReader.read(&_references[i].type, 4, 1) != 1 ||_binaryReader.read(&_references[i].offset, 4, 1) != 1)
{
clear();
CCLOG("warning: Failed to read ref number %u for bundle '%s'.", i, path.c_str());
CC_SAFE_DELETE_ARRAY(_references);
return false;
}
}
return true;
}
loadBinary
函数是真正的解释二进制文件,而且其加密方式也是采用的二进制,加密算法会在后面给读者进行讲解,到这里模型的加载就全部完成了。再返回到前面的函数initWithFile,在函数内部还有一个函数initFrom没有讲解,加载完模型后,最终要将其显示出来,而且还要把材质附加到模型上面以及关于模型的骨骼动画等这些操作都是通过如下函数实现的,函数内容如下所示:
bool Sprite3D::initFrom(const NodeDatas& nodeDatas, const MeshDatas& meshdatas, const MaterialDatas& materialdatas)
{
for(const auto& it : meshdatas.meshDatas)
{
if(it)
{
auto meshvertex = MeshVertexData::create(*it);
_meshVertexDatas.pushBack(meshvertex);
}
}
_skeleton = Skeleton3D::create(nodeDatas.skeleton);
CC_SAFE_RETAIN(_skeleton);
for(const auto& it : nodeDatas.nodes)
{
if(it)
{
createNode(it, this, materialdatas, nodeDatas.nodes.size() == 1);
}
}
for(const auto& it : nodeDatas.skeleton)
{
if(it)
{
createAttachSprite3DNode(it,materialdatas);
}
}
genMaterial();
return true;
}
在函数中实现了meshVertex网格顶点以及skeleton骨骼动画信息读取,另外还调用了createNode函数用于处理模型相关的所有信息,函数如下所示:
void Sprite3D::createNode(NodeData* nodedata, Node* root, const MaterialDatas& materialdatas, bool singleSprite)
{
Node* node=nullptr;
for(const auto& it : nodedata->modelNodeDatas)
{
if(it)
{
if(it->bones.size() >0 || singleSprite)
{
if(singleSprite && root!=nullptr)
root->setName(nodedata->id);
auto mesh = Mesh::create(nodedata->id, getMeshIndexData(it->subMeshId));
if(mesh)
{
_meshes.pushBack(mesh);
if (_skeleton&& it->bones.size())
{
auto skin = MeshSkin::create(_skeleton, it->bones, it->invBindPose);
mesh->setSkin(skin);
}
mesh->_visibleChanged = std::bind(&Sprite3D::onAABBDirty, this);
if (it->matrialId == ""&& materialdatas.materials.size())
{
const NTextureData* textureData = materialdatas.materials[0].getTextureData(NTextureData::Usage::Diffuse);
mesh->setTexture(textureData->filename);
}
else
{
const NMaterialData* materialData=materialdatas.getMaterialData(it->matrialId);
if(materialData)
{
const NTextureData* textureData = materialData->getTextureData(NTextureData::Usage::Diffuse);
if(textureData)
{
mesh->setTexture(textureData->filename);
auto tex = mesh->getTexture();
if(tex)
{
Texture2D::TexParams texParams;
texParams.minFilter = GL_LINEAR;
texParams.magFilter = GL_LINEAR;
texParams.wrapS = textureData->wrapS;
texParams.wrapT = textureData->wrapT;
tex->setTexParameters(texParams);
mesh->_isTransparent = (materialData->getTextureData(NTextureData::Usage::Transparency) != nullptr);
}
}
textureData = materialData->getTextureData(NTextureData::Usage::Normal);
if (textureData)
{
auto tex = Director::getInstance()->getTextureCache()->addImage(textureData->filename);
if (tex)
{
Texture2D::TexParams texParams;
texParams.minFilter = GL_LINEAR;
texParams.magFilter = GL_LINEAR;
texParams.wrapS = textureData->wrapS;
texParams.wrapT = textureData->wrapT;
tex->setTexParameters(texParams);
}
mesh->setTexture(tex, NTextureData::Usage::Normal);
}
}
}
Vec3 pos;
Quaternion qua;
Vec3 scale;
nodedata->transform.decompose(&scale, &qua, &pos);
setPosition3D(pos);
setRotationQuat(qua);
setScaleX(scale.x);
setScaleY(scale.y);
setScaleZ(scale.z);
node = this;
}
}
else
{
auto sprite = createSprite3DNode(nodedata,it,materialdatas);
if (sprite)
{
if(root)
{
root->addChild(sprite);
}
}
node=sprite;
}
}
}
if(nodedata->modelNodeDatas.size() ==0 )
{
node= Node::create();
if(node)
{
node->setName(nodedata->id);
// 设置局部转换
Vec3 pos;
Quaternion qua;
Vec3 scale;
nodedata->transform.decompose(&scale, &qua, &pos);
node->setPosition3D(pos);
node->setRotationQuat(qua);
node->setScaleX(scale.x);
node->setScaleY(scale.y);
node->setScaleZ(scale.z);
if(root)
{
root->addChild(node);
}
}
}
for(const auto& it : nodedata->children)
{
createNode(it,node, materialdatas, nodedata->children.size() == 1);
}
}
createNode
函数完成了3D模型的整个加载流程,接下来开始讲解模型的材质渲染,从下节开始材质的渲染操作。